Skip to content

Commit c0e624a

Browse files
authored
Add configuration option to override default text rendering (#2470)
* Add configuration option to override default text rendering with a textSpanBuilder
1 parent 44bcf90 commit c0e624a

File tree

9 files changed

+122
-28
lines changed

9 files changed

+122
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
### Added
1414

1515
- Bosnian (bs), Macedonian (mk) and Gujarati (gu) language translations [#2455](https://github.com/singerdmx/flutter-quill/pull/2455).
16+
- `textSpanBuilder` to `QuillEditorConfig` to allow overriding how text content is rendered.
1617

1718
### Changed
1819

lib/src/editor/config/editor_config.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import '../raw_editor/raw_editor.dart';
1414
import '../widgets/default_styles.dart';
1515
import '../widgets/delegate.dart';
1616
import '../widgets/link.dart';
17+
import '../widgets/text/utils/text_block_utils.dart';
1718
import 'search_config.dart';
1819

1920
// IMPORTANT For project authors: The QuillEditorConfig.copyWith()
@@ -54,6 +55,7 @@ class QuillEditorConfig {
5455
@experimental this.onKeyPressed,
5556
this.enableAlwaysIndentOnTab = false,
5657
this.embedBuilders,
58+
this.textSpanBuilder = defaultSpanBuilder,
5759
this.unknownEmbedBuilder,
5860
@experimental this.searchConfig = const QuillSearchConfig(),
5961
this.linkActionPickerDelegate = defaultLinkActionPickerDelegate,
@@ -361,6 +363,8 @@ class QuillEditorConfig {
361363
final CustomStyleBuilder? customStyleBuilder;
362364
final CustomRecognizerBuilder? customRecognizerBuilder;
363365

366+
final TextSpanBuilder textSpanBuilder;
367+
364368
/// See [search](https://github.com/singerdmx/flutter-quill/blob/master/doc/configurations/search.md)
365369
/// page for docs.
366370
@experimental
@@ -485,6 +489,7 @@ class QuillEditorConfig {
485489
bool Function(TapUpDetails details, TextPosition Function(Offset offset))?
486490
onTapUp,
487491
Iterable<EmbedBuilder>? embedBuilders,
492+
TextSpanBuilder? textSpanBuilder,
488493
EmbedBuilder? unknownEmbedBuilder,
489494
CustomStyleBuilder? customStyleBuilder,
490495
CustomRecognizerBuilder? customRecognizerBuilder,
@@ -545,6 +550,7 @@ class QuillEditorConfig {
545550
onTapUp: onTapUp ?? this.onTapUp,
546551
onTapDown: onTapDown ?? this.onTapDown,
547552
embedBuilders: embedBuilders ?? this.embedBuilders,
553+
textSpanBuilder: textSpanBuilder ?? this.textSpanBuilder,
548554
unknownEmbedBuilder: unknownEmbedBuilder ?? this.unknownEmbedBuilder,
549555
customStyleBuilder: customStyleBuilder ?? this.customStyleBuilder,
550556
customRecognizerBuilder:

lib/src/editor/editor.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ class QuillEditorState extends State<QuillEditor>
303303
enableInteractiveSelection: configurations.enableInteractiveSelection,
304304
scrollPhysics: configurations.scrollPhysics,
305305
embedBuilder: _getEmbedBuilder,
306+
textSpanBuilder: configurations.textSpanBuilder,
306307
linkActionPickerDelegate: configurations.linkActionPickerDelegate,
307308
customStyleBuilder: configurations.customStyleBuilder,
308309
customRecognizerBuilder: configurations.customRecognizerBuilder,

lib/src/editor/raw_editor/config/raw_editor_config.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '../../../editor/widgets/default_styles.dart';
1212
import '../../../editor/widgets/delegate.dart';
1313
import '../../../editor/widgets/link.dart';
1414
import '../../../toolbar/theme/quill_dialog_theme.dart';
15+
import '../../widgets/text/utils/text_block_utils.dart';
1516
import '../builders/leading_block_builder.dart';
1617
import 'events/events.dart';
1718

@@ -25,6 +26,7 @@ class QuillRawEditorConfig {
2526
required this.selectionColor,
2627
required this.selectionCtrls,
2728
required this.embedBuilder,
29+
required this.textSpanBuilder,
2830
required this.autoFocus,
2931
required this.characterShortcutEvents,
3032
required this.spaceShortcutEvents,
@@ -360,6 +362,9 @@ class QuillRawEditorConfig {
360362
final bool floatingCursorDisabled;
361363
final List<String> customLinkPrefixes;
362364

365+
/// Used to build the [InlineSpan]s containing text content.
366+
final TextSpanBuilder textSpanBuilder;
367+
363368
/// Configures the dialog theme.
364369
final QuillDialogTheme? dialogTheme;
365370

lib/src/editor/raw_editor/raw_editor_state.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ class QuillRawEditorState extends EditorState
607607
? const EdgeInsets.all(16)
608608
: null,
609609
embedBuilder: widget.config.embedBuilder,
610+
textSpanBuilder: widget.config.textSpanBuilder,
610611
linkActionPicker: _linkActionPicker,
611612
onLaunchUrl: widget.config.onLaunchUrl,
612613
cursorCont: _cursorCont,
@@ -643,6 +644,7 @@ class QuillRawEditorState extends EditorState
643644
line: node,
644645
textDirection: _textDirection,
645646
embedBuilder: widget.config.embedBuilder,
647+
textSpanBuilder: widget.config.textSpanBuilder,
646648
customStyleBuilder: widget.config.customStyleBuilder,
647649
customRecognizerBuilder: widget.config.customRecognizerBuilder,
648650
styles: _styles!,

lib/src/editor/widgets/text/text_block.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class EditableTextBlock extends StatelessWidget {
7070
required this.hasFocus,
7171
required this.contentPadding,
7272
required this.embedBuilder,
73+
required this.textSpanBuilder,
7374
required this.linkActionPicker,
7475
required this.cursorCont,
7576
required this.indentLevelCounts,
@@ -100,6 +101,7 @@ class EditableTextBlock extends StatelessWidget {
100101
final bool hasFocus;
101102
final EdgeInsets? contentPadding;
102103
final EmbedsBuilder embedBuilder;
104+
final TextSpanBuilder textSpanBuilder;
103105
final LinkActionPicker linkActionPicker;
104106
final ValueChanged<String>? onLaunchUrl;
105107
final CustomRecognizerBuilder? customRecognizerBuilder;
@@ -186,6 +188,7 @@ class EditableTextBlock extends StatelessWidget {
186188
line: line,
187189
textDirection: textDirection,
188190
embedBuilder: embedBuilder,
191+
textSpanBuilder: textSpanBuilder,
189192
customStyleBuilder: customStyleBuilder,
190193
styles: styles!,
191194
readOnly: readOnly,

lib/src/editor/widgets/text/text_line.dart

Lines changed: 76 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class TextLine extends StatefulWidget {
2424
const TextLine({
2525
required this.line,
2626
required this.embedBuilder,
27+
required this.textSpanBuilder,
2728
required this.styles,
2829
required this.readOnly,
2930
required this.controller,
@@ -40,6 +41,7 @@ class TextLine extends StatefulWidget {
4041
final Line line;
4142
final TextDirection? textDirection;
4243
final EmbedsBuilder embedBuilder;
44+
final TextSpanBuilder textSpanBuilder;
4345
final DefaultStyles styles;
4446
final bool readOnly;
4547
final QuillController controller;
@@ -192,7 +194,12 @@ class _TextLineState extends State<TextLine> {
192194
InlineSpan _getTextSpanForWholeLine() {
193195
var lineStyle = _getLineStyle(widget.styles);
194196
if (!widget.line.hasEmbed) {
195-
return _buildTextSpan(widget.styles, widget.line.children, lineStyle);
197+
return _buildTextSpan(
198+
widget.styles,
199+
widget.line.children,
200+
lineStyle,
201+
widget.textSpanBuilder,
202+
);
196203
}
197204

198205
// The line could contain more than one Embed & more than one Text
@@ -201,8 +208,12 @@ class _TextLineState extends State<TextLine> {
201208
for (var child in widget.line.children) {
202209
if (child is Embed) {
203210
if (textNodes.isNotEmpty) {
204-
textSpanChildren
205-
.add(_buildTextSpan(widget.styles, textNodes, lineStyle));
211+
textSpanChildren.add(_buildTextSpan(
212+
widget.styles,
213+
textNodes,
214+
lineStyle,
215+
widget.textSpanBuilder,
216+
));
206217
textNodes = LinkedList<Node>();
207218
}
208219
// Creates correct node for custom embed
@@ -243,7 +254,12 @@ class _TextLineState extends State<TextLine> {
243254
}
244255

245256
if (textNodes.isNotEmpty) {
246-
textSpanChildren.add(_buildTextSpan(widget.styles, textNodes, lineStyle));
257+
textSpanChildren.add(_buildTextSpan(
258+
widget.styles,
259+
textNodes,
260+
lineStyle,
261+
widget.textSpanBuilder,
262+
));
247263
}
248264

249265
return TextSpan(style: lineStyle, children: textSpanChildren);
@@ -263,10 +279,11 @@ class _TextLineState extends State<TextLine> {
263279
return TextAlign.start;
264280
}
265281

266-
TextSpan _buildTextSpan(
282+
InlineSpan _buildTextSpan(
267283
DefaultStyles defaultStyles,
268284
LinkedList<Node> nodes,
269285
TextStyle lineStyle,
286+
TextSpanBuilder textSpanBuilder,
270287
) {
271288
if (nodes.isEmpty && kIsWeb) {
272289
nodes = LinkedList<Node>()..add(leaf.QuillText('\u{200B}'));
@@ -280,20 +297,28 @@ class _TextLineState extends State<TextLine> {
280297

281298
if (isComposingRangeOutOfLine) {
282299
final children = nodes
283-
.map((node) =>
284-
_getTextSpanFromNode(defaultStyles, node, widget.line.style))
300+
.map((node) => _getTextSpanFromNode(
301+
defaultStyles,
302+
node,
303+
widget.line.style,
304+
textSpanBuilder,
305+
))
285306
.toList(growable: false);
286307
return TextSpan(children: children, style: lineStyle);
287308
}
288309

289310
final children = nodes.expand((node) {
290-
final child =
291-
_getTextSpanFromNode(defaultStyles, node, widget.line.style);
311+
final child = _getTextSpanFromNode(
312+
defaultStyles,
313+
node,
314+
widget.line.style,
315+
textSpanBuilder,
316+
);
292317
final isNodeInComposingRange =
293318
node.documentOffset <= widget.composingRange.start &&
294319
widget.composingRange.end <= node.documentOffset + node.length;
295320
if (isNodeInComposingRange) {
296-
return _splitAndApplyComposingStyle(node, child);
321+
return _splitAndApplyComposingStyle(node, child, textSpanBuilder);
297322
} else {
298323
return [child];
299324
}
@@ -304,7 +329,11 @@ class _TextLineState extends State<TextLine> {
304329

305330
// split the text nodes into composing and non-composing nodes
306331
// and apply the composing style to the composing nodes
307-
List<InlineSpan> _splitAndApplyComposingStyle(Node node, InlineSpan child) {
332+
List<InlineSpan> _splitAndApplyComposingStyle(
333+
Node node,
334+
InlineSpan child,
335+
TextSpanBuilder textSpanBuilder,
336+
) {
308337
assert(widget.composingRange.isValid && !widget.composingRange.isCollapsed);
309338

310339
final composingStart = widget.composingRange.start - node.documentOffset;
@@ -319,18 +348,33 @@ class _TextLineState extends State<TextLine> {
319348
?.merge(const TextStyle(decoration: TextDecoration.underline)) ??
320349
const TextStyle(decoration: TextDecoration.underline);
321350

351+
final isLink = node.style.attributes[Attribute.link.key]?.value != null;
352+
final recognizer = _getRecognizer(node, isLink);
353+
322354
return [
323-
TextSpan(
324-
text: textBefore,
325-
style: child.style,
355+
textSpanBuilder(
356+
context,
357+
node,
358+
0,
359+
textBefore,
360+
child.style,
361+
recognizer,
326362
),
327-
TextSpan(
328-
text: textComposing,
329-
style: composingStyle,
363+
textSpanBuilder(
364+
context,
365+
node,
366+
composingStart,
367+
textComposing,
368+
composingStyle,
369+
recognizer,
330370
),
331-
TextSpan(
332-
text: textAfter,
333-
style: child.style,
371+
textSpanBuilder(
372+
context,
373+
node,
374+
composingEnd,
375+
textAfter,
376+
child.style,
377+
recognizer,
334378
),
335379
];
336380
}
@@ -461,7 +505,11 @@ class _TextLineState extends State<TextLine> {
461505
}
462506

463507
InlineSpan _getTextSpanFromNode(
464-
DefaultStyles defaultStyles, Node node, Style lineStyle) {
508+
DefaultStyles defaultStyles,
509+
Node node,
510+
Style lineStyle,
511+
TextSpanBuilder textSpanBuilder,
512+
) {
465513
final textNode = node as leaf.QuillText;
466514
final nodeStyle = textNode.style;
467515
final isLink = nodeStyle.containsKey(Attribute.link.key) &&
@@ -480,11 +528,13 @@ class _TextLineState extends State<TextLine> {
480528
}
481529

482530
final recognizer = _getRecognizer(node, isLink);
483-
return TextSpan(
484-
text: textNode.value,
485-
style: style,
486-
recognizer: recognizer,
487-
mouseCursor: (recognizer != null) ? SystemMouseCursors.click : null,
531+
return textSpanBuilder(
532+
context,
533+
textNode,
534+
0,
535+
textNode.value,
536+
style,
537+
recognizer,
488538
);
489539
}
490540

lib/src/editor/widgets/text/utils/text_block_utils.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import 'package:flutter/gestures.dart';
12
import 'package:flutter/material.dart';
3+
24
import '../../../../common/structs/horizontal_spacing.dart';
35
import '../../../../document/attribute.dart';
46
import '../../../../document/nodes/block.dart';
7+
import '../../../../document/nodes/node.dart';
58
import '../../default_styles.dart';
69

710
typedef LeadingBlockIndentWidth = HorizontalSpacing Function(
@@ -13,6 +16,30 @@ typedef LeadingBlockIndentWidth = HorizontalSpacing Function(
1316
typedef LeadingBlockNumberPointWidth = double Function(
1417
double fontSize, int count);
1518

19+
typedef TextSpanBuilder = InlineSpan Function(
20+
BuildContext context,
21+
Node node,
22+
int nodeOffset,
23+
String text,
24+
TextStyle? style,
25+
GestureRecognizer? recognizer,
26+
);
27+
28+
TextSpan defaultSpanBuilder(
29+
BuildContext context,
30+
Node node,
31+
int textOffset,
32+
String text,
33+
TextStyle? style,
34+
GestureRecognizer? recognizer,
35+
) =>
36+
TextSpan(
37+
text: text,
38+
style: style,
39+
recognizer: recognizer,
40+
mouseCursor: (recognizer != null) ? SystemMouseCursors.click : null,
41+
);
42+
1643
abstract final class TextBlockUtils {
1744
/// Get the horizontalSpacing using the default
1845
/// implementation provided by [Flutter Quill]

lib/src/toolbar/buttons/font_family_button.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import '../../common/utils/widgets.dart';
44
import '../../document/attribute.dart';
55
import '../../l10n/extensions/localizations_ext.dart';
66
import '../base_button/base_value_button.dart';
7-
87
import '../simple_toolbar.dart';
98

109
class QuillToolbarFontFamilyButton extends QuillToolbarBaseButton<
@@ -18,7 +17,7 @@ class QuillToolbarFontFamilyButton extends QuillToolbarBaseButton<
1817
/// over the [baseOptions].
1918
super.baseOptions,
2019
super.key,
21-
}) : assert(options.items?.isNotEmpty ?? (true)),
20+
}) : assert(options.items?.isNotEmpty ?? true),
2221
assert(
2322
options.initialValue == null || options.initialValue!.isNotEmpty,
2423
);

0 commit comments

Comments
 (0)