diff --git a/packages/sanity/flutter_sanity_portable_text/example/lib/main.dart b/packages/sanity/flutter_sanity_portable_text/example/lib/main.dart index 8a30e400..51547686 100644 --- a/packages/sanity/flutter_sanity_portable_text/example/lib/main.dart +++ b/packages/sanity/flutter_sanity_portable_text/example/lib/main.dart @@ -71,6 +71,28 @@ void _registerCustomMark() { return style; }, + spanBuilder: (context, markDef, text, style) { + return WidgetSpan( + child: Container( + color: Colors.black12, + padding: const EdgeInsets.only(right: 4), + child: GestureDetector( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.bolt, + color: (markDef as CustomMarkDef).color, + size: 20, + ), + Text(text, style: style), + ], + ), + onTap: () => debugPrint('Tapped on $markDef'), + ), + ), + ); + }, fromJson: (json) => CustomMarkDef(color: json['color'], key: json['key']), ); } diff --git a/packages/sanity/flutter_sanity_portable_text/lib/model/markdef_descriptor.dart b/packages/sanity/flutter_sanity_portable_text/lib/model/markdef_descriptor.dart index 524003dc..99157d01 100644 --- a/packages/sanity/flutter_sanity_portable_text/lib/model/markdef_descriptor.dart +++ b/packages/sanity/flutter_sanity_portable_text/lib/model/markdef_descriptor.dart @@ -6,12 +6,6 @@ import '../flutter_sanity_portable_text.dart'; part 'markdef_descriptor.g.dart'; -/// Builds a [GestureRecognizer] for a given [MarkDef]. -typedef GestureRecognizerBuilder = GestureRecognizer Function( - BuildContext context, - MarkDef mark, -); - /// Builds a [TextStyle] for a given [MarkDef]. typedef MarkDefTextStyleBuilder = TextStyle Function( BuildContext context, @@ -24,6 +18,14 @@ typedef MarkDefFromJson = MarkDef Function( Map json, ); +/// Builds an [InlineSpan] for a given [MarkDef]. +typedef MarkDefSpanBuilder = InlineSpan Function( + BuildContext context, + MarkDef mark, + String text, + TextStyle style, +); + /// Describes a [MarkDef] and its associated builders. final class MarkDefDescriptor { /// The schema type of the mark. @@ -40,13 +42,13 @@ final class MarkDefDescriptor { final MarkDefTextStyleBuilder? styleBuilder; /// Builds a [GestureRecognizer] for a given [MarkDef]. - final GestureRecognizerBuilder? recognizerBuilder; + final MarkDefSpanBuilder? spanBuilder; MarkDefDescriptor({ required this.schemaType, required this.fromJson, this.styleBuilder, - this.recognizerBuilder, + this.spanBuilder, }); } diff --git a/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_block.dart b/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_block.dart index 13bb544d..d267635e 100644 --- a/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_block.dart +++ b/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_block.dart @@ -1,5 +1,4 @@ import 'package:collection/collection.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import '../flutter_sanity_portable_text.dart'; @@ -22,7 +21,7 @@ class PortableTextBlock extends StatelessWidget { final config = PortableTextConfig.shared; final spans = model.children - .map((final span) => _convertSpan(span, Theme.of(context), context)) + .map((final span) => _buildInlineSpan(span, Theme.of(context), context)) .toList(growable: false); final content = Text.rich( @@ -47,7 +46,7 @@ class PortableTextBlock extends StatelessWidget { ); } - InlineSpan _convertSpan( + InlineSpan _buildInlineSpan( final Span span, final ThemeData theme, final BuildContext context, @@ -79,10 +78,9 @@ class PortableTextBlock extends StatelessWidget { pendingMarkDefs.add(markDef); } - GestureRecognizer? recognizer; - int totalRecognizers = 0; String? errorText; + // Build the style across all marks for (final markDef in pendingMarkDefs) { final descriptor = config.markDefs[markDef.type]; if (descriptor == null) { @@ -91,31 +89,49 @@ class PortableTextBlock extends StatelessWidget { } style = descriptor.styleBuilder?.call(context, markDef, style) ?? style; + } + + // Now build the final InlineSpan, if any + InlineSpan? inlineSpan; + int totalSpans = 0; - recognizer = descriptor.recognizerBuilder?.call(context, markDef); + // Only go ahead if there are no errors from previous step + if (errorText == null) { + for (final markDef in pendingMarkDefs) { + final descriptor = config.markDefs[markDef.type]; + inlineSpan = + descriptor?.spanBuilder?.call(context, markDef, span.text, style); - if (recognizer != null) { - totalRecognizers++; + if (inlineSpan == null) { + continue; + } - if (totalRecognizers > 1) { - errorText = - 'There can only be one recognizer for a set of MarkDefs. We found more than one.'; + totalSpans++; + if (totalSpans > 1) { + final markDefChain = pendingMarkDefs.map((e) => e.type).join(' -> '); + errorText = ''' +We currently support a single custom markDef generating the InlineSpan. +We found $totalSpans. The chain of markDefs was: $markDefChain. + +Suggestion: Try to refactor your custom markDef chain to only generate a single InlineSpan. +You can rely on TextStyles instead for custom styling.'''; + break; } } } - return errorText != null - ? WidgetSpan( - child: ErrorView( - message: errorText, - asBlock: false, - ), - ) - : TextSpan( - text: span.text, - recognizer: recognizer, - style: style, - ); + if (errorText != null) { + return WidgetSpan( + child: GestureDetector( + child: ErrorView( + message: errorText, + asBlock: false, + ), + ), + ); + } + + return inlineSpan ?? TextSpan(text: span.text, style: style); } InlineSpan _bulletMark(final BuildContext context) { diff --git a/packages/system/vyuh_feature_system/lib/feature.dart b/packages/system/vyuh_feature_system/lib/feature.dart index 38661c5b..07aa5602 100644 --- a/packages/system/vyuh_feature_system/lib/feature.dart +++ b/packages/system/vyuh_feature_system/lib/feature.dart @@ -1,4 +1,3 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.dart'; import 'package:go_router/go_router.dart'; @@ -114,11 +113,34 @@ final feature = FeatureDescriptor( decoration: TextDecoration.underline, ); }, - recognizerBuilder: (context, def) { - return TapGestureRecognizer() - ..onTap = () { - (def as InvokeActionMarkDef).action.execute(context); - }; + spanBuilder: (context, def, text, style) { + return WidgetSpan( + child: GestureDetector( + onTap: () { + (def as InvokeActionMarkDef).action.execute(context); + }, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + direction: Axis.horizontal, + alignment: WrapAlignment.start, + children: [ + Text( + text, + style: style, + softWrap: false, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.left, + ), + Icon( + Icons.arrow_forward, + size: 16, + color: Theme.of(context).colorScheme.primary, + ), + ], + ), + ), + ); }, ) ],