Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

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

## [11.0.0-dev.21] - 2025-01-21

Expand Down
6 changes: 6 additions & 0 deletions lib/src/editor/config/editor_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '../raw_editor/raw_editor.dart';
import '../widgets/default_styles.dart';
import '../widgets/delegate.dart';
import '../widgets/link.dart';
import '../widgets/text/utils/text_block_utils.dart';
import 'search_config.dart';

// IMPORTANT For project authors: The QuillEditorConfig.copyWith()
Expand Down Expand Up @@ -54,6 +55,7 @@ class QuillEditorConfig {
@experimental this.onKeyPressed,
this.enableAlwaysIndentOnTab = false,
this.embedBuilders,
this.textSpanBuilder = defaultSpanBuilder,
this.unknownEmbedBuilder,
@experimental this.searchConfig = const QuillSearchConfig(),
this.linkActionPickerDelegate = defaultLinkActionPickerDelegate,
Expand Down Expand Up @@ -361,6 +363,8 @@ class QuillEditorConfig {
final CustomStyleBuilder? customStyleBuilder;
final CustomRecognizerBuilder? customRecognizerBuilder;

final TextSpanBuilder textSpanBuilder;

/// See [search](https://github.com/singerdmx/flutter-quill/blob/master/doc/configurations/search.md)
/// page for docs.
@experimental
Expand Down Expand Up @@ -485,6 +489,7 @@ class QuillEditorConfig {
bool Function(TapUpDetails details, TextPosition Function(Offset offset))?
onTapUp,
Iterable<EmbedBuilder>? embedBuilders,
TextSpanBuilder? textSpanBuilder,
EmbedBuilder? unknownEmbedBuilder,
CustomStyleBuilder? customStyleBuilder,
CustomRecognizerBuilder? customRecognizerBuilder,
Expand Down Expand Up @@ -545,6 +550,7 @@ class QuillEditorConfig {
onTapUp: onTapUp ?? this.onTapUp,
onTapDown: onTapDown ?? this.onTapDown,
embedBuilders: embedBuilders ?? this.embedBuilders,
textSpanBuilder: textSpanBuilder ?? this.textSpanBuilder,
unknownEmbedBuilder: unknownEmbedBuilder ?? this.unknownEmbedBuilder,
customStyleBuilder: customStyleBuilder ?? this.customStyleBuilder,
customRecognizerBuilder:
Expand Down
1 change: 1 addition & 0 deletions lib/src/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ class QuillEditorState extends State<QuillEditor>
enableInteractiveSelection: configurations.enableInteractiveSelection,
scrollPhysics: configurations.scrollPhysics,
embedBuilder: _getEmbedBuilder,
textSpanBuilder: configurations.textSpanBuilder,
linkActionPickerDelegate: configurations.linkActionPickerDelegate,
customStyleBuilder: configurations.customStyleBuilder,
customRecognizerBuilder: configurations.customRecognizerBuilder,
Expand Down
5 changes: 5 additions & 0 deletions lib/src/editor/raw_editor/config/raw_editor_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import '../../../editor/widgets/default_styles.dart';
import '../../../editor/widgets/delegate.dart';
import '../../../editor/widgets/link.dart';
import '../../../toolbar/theme/quill_dialog_theme.dart';
import '../../widgets/text/utils/text_block_utils.dart';
import '../builders/leading_block_builder.dart';
import 'events/events.dart';

Expand All @@ -25,6 +26,7 @@ class QuillRawEditorConfig {
required this.selectionColor,
required this.selectionCtrls,
required this.embedBuilder,
required this.textSpanBuilder,
required this.autoFocus,
required this.characterShortcutEvents,
required this.spaceShortcutEvents,
Expand Down Expand Up @@ -360,6 +362,9 @@ class QuillRawEditorConfig {
final bool floatingCursorDisabled;
final List<String> customLinkPrefixes;

/// Used to build the [InlineSpan]s containing text content.
final TextSpanBuilder textSpanBuilder;

/// Configures the dialog theme.
final QuillDialogTheme? dialogTheme;

Expand Down
2 changes: 2 additions & 0 deletions lib/src/editor/raw_editor/raw_editor_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ class QuillRawEditorState extends EditorState
? const EdgeInsets.all(16)
: null,
embedBuilder: widget.config.embedBuilder,
textSpanBuilder: widget.config.textSpanBuilder,
linkActionPicker: _linkActionPicker,
onLaunchUrl: widget.config.onLaunchUrl,
cursorCont: _cursorCont,
Expand Down Expand Up @@ -643,6 +644,7 @@ class QuillRawEditorState extends EditorState
line: node,
textDirection: _textDirection,
embedBuilder: widget.config.embedBuilder,
textSpanBuilder: widget.config.textSpanBuilder,
customStyleBuilder: widget.config.customStyleBuilder,
customRecognizerBuilder: widget.config.customRecognizerBuilder,
styles: _styles!,
Expand Down
3 changes: 3 additions & 0 deletions lib/src/editor/widgets/text/text_block.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class EditableTextBlock extends StatelessWidget {
required this.hasFocus,
required this.contentPadding,
required this.embedBuilder,
required this.textSpanBuilder,
required this.linkActionPicker,
required this.cursorCont,
required this.indentLevelCounts,
Expand Down Expand Up @@ -100,6 +101,7 @@ class EditableTextBlock extends StatelessWidget {
final bool hasFocus;
final EdgeInsets? contentPadding;
final EmbedsBuilder embedBuilder;
final TextSpanBuilder textSpanBuilder;
final LinkActionPicker linkActionPicker;
final ValueChanged<String>? onLaunchUrl;
final CustomRecognizerBuilder? customRecognizerBuilder;
Expand Down Expand Up @@ -186,6 +188,7 @@ class EditableTextBlock extends StatelessWidget {
line: line,
textDirection: textDirection,
embedBuilder: embedBuilder,
textSpanBuilder: textSpanBuilder,
customStyleBuilder: customStyleBuilder,
styles: styles!,
readOnly: readOnly,
Expand Down
102 changes: 76 additions & 26 deletions lib/src/editor/widgets/text/text_line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class TextLine extends StatefulWidget {
const TextLine({
required this.line,
required this.embedBuilder,
required this.textSpanBuilder,
required this.styles,
required this.readOnly,
required this.controller,
Expand All @@ -40,6 +41,7 @@ class TextLine extends StatefulWidget {
final Line line;
final TextDirection? textDirection;
final EmbedsBuilder embedBuilder;
final TextSpanBuilder textSpanBuilder;
final DefaultStyles styles;
final bool readOnly;
final QuillController controller;
Expand Down Expand Up @@ -192,7 +194,12 @@ class _TextLineState extends State<TextLine> {
InlineSpan _getTextSpanForWholeLine() {
var lineStyle = _getLineStyle(widget.styles);
if (!widget.line.hasEmbed) {
return _buildTextSpan(widget.styles, widget.line.children, lineStyle);
return _buildTextSpan(
widget.styles,
widget.line.children,
lineStyle,
widget.textSpanBuilder,
);
}

// The line could contain more than one Embed & more than one Text
Expand All @@ -201,8 +208,12 @@ class _TextLineState extends State<TextLine> {
for (var child in widget.line.children) {
if (child is Embed) {
if (textNodes.isNotEmpty) {
textSpanChildren
.add(_buildTextSpan(widget.styles, textNodes, lineStyle));
textSpanChildren.add(_buildTextSpan(
widget.styles,
textNodes,
lineStyle,
widget.textSpanBuilder,
));
textNodes = LinkedList<Node>();
}
// Creates correct node for custom embed
Expand Down Expand Up @@ -243,7 +254,12 @@ class _TextLineState extends State<TextLine> {
}

if (textNodes.isNotEmpty) {
textSpanChildren.add(_buildTextSpan(widget.styles, textNodes, lineStyle));
textSpanChildren.add(_buildTextSpan(
widget.styles,
textNodes,
lineStyle,
widget.textSpanBuilder,
));
}

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

TextSpan _buildTextSpan(
InlineSpan _buildTextSpan(
DefaultStyles defaultStyles,
LinkedList<Node> nodes,
TextStyle lineStyle,
TextSpanBuilder textSpanBuilder,
) {
if (nodes.isEmpty && kIsWeb) {
nodes = LinkedList<Node>()..add(leaf.QuillText('\u{200B}'));
Expand All @@ -280,20 +297,28 @@ class _TextLineState extends State<TextLine> {

if (isComposingRangeOutOfLine) {
final children = nodes
.map((node) =>
_getTextSpanFromNode(defaultStyles, node, widget.line.style))
.map((node) => _getTextSpanFromNode(
defaultStyles,
node,
widget.line.style,
textSpanBuilder,
))
.toList(growable: false);
return TextSpan(children: children, style: lineStyle);
}

final children = nodes.expand((node) {
final child =
_getTextSpanFromNode(defaultStyles, node, widget.line.style);
final child = _getTextSpanFromNode(
defaultStyles,
node,
widget.line.style,
textSpanBuilder,
);
final isNodeInComposingRange =
node.documentOffset <= widget.composingRange.start &&
widget.composingRange.end <= node.documentOffset + node.length;
if (isNodeInComposingRange) {
return _splitAndApplyComposingStyle(node, child);
return _splitAndApplyComposingStyle(node, child, textSpanBuilder);
} else {
return [child];
}
Expand All @@ -304,7 +329,11 @@ class _TextLineState extends State<TextLine> {

// split the text nodes into composing and non-composing nodes
// and apply the composing style to the composing nodes
List<InlineSpan> _splitAndApplyComposingStyle(Node node, InlineSpan child) {
List<InlineSpan> _splitAndApplyComposingStyle(
Node node,
InlineSpan child,
TextSpanBuilder textSpanBuilder,
) {
assert(widget.composingRange.isValid && !widget.composingRange.isCollapsed);

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

final isLink = node.style.attributes[Attribute.link.key]?.value != null;
final recognizer = _getRecognizer(node, isLink);

return [
TextSpan(
text: textBefore,
style: child.style,
textSpanBuilder(
context,
node,
0,
textBefore,
child.style,
recognizer,
),
TextSpan(
text: textComposing,
style: composingStyle,
textSpanBuilder(
context,
node,
composingStart,
textComposing,
composingStyle,
recognizer,
),
TextSpan(
text: textAfter,
style: child.style,
textSpanBuilder(
context,
node,
composingEnd,
textAfter,
child.style,
recognizer,
),
];
}
Expand Down Expand Up @@ -461,7 +505,11 @@ class _TextLineState extends State<TextLine> {
}

InlineSpan _getTextSpanFromNode(
DefaultStyles defaultStyles, Node node, Style lineStyle) {
DefaultStyles defaultStyles,
Node node,
Style lineStyle,
TextSpanBuilder textSpanBuilder,
) {
final textNode = node as leaf.QuillText;
final nodeStyle = textNode.style;
final isLink = nodeStyle.containsKey(Attribute.link.key) &&
Expand All @@ -480,11 +528,13 @@ class _TextLineState extends State<TextLine> {
}

final recognizer = _getRecognizer(node, isLink);
return TextSpan(
text: textNode.value,
style: style,
recognizer: recognizer,
mouseCursor: (recognizer != null) ? SystemMouseCursors.click : null,
return textSpanBuilder(
context,
textNode,
0,
textNode.value,
style,
recognizer,
);
}

Expand Down
27 changes: 27 additions & 0 deletions lib/src/editor/widgets/text/utils/text_block_utils.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

import '../../../../common/structs/horizontal_spacing.dart';
import '../../../../document/attribute.dart';
import '../../../../document/nodes/block.dart';
import '../../../../document/nodes/node.dart';
import '../../default_styles.dart';

typedef LeadingBlockIndentWidth = HorizontalSpacing Function(
Expand All @@ -13,6 +16,30 @@ typedef LeadingBlockIndentWidth = HorizontalSpacing Function(
typedef LeadingBlockNumberPointWidth = double Function(
double fontSize, int count);

typedef TextSpanBuilder = InlineSpan Function(
BuildContext context,
Node node,
int nodeOffset,
String text,
TextStyle? style,
GestureRecognizer? recognizer,
);

TextSpan defaultSpanBuilder(
BuildContext context,
Node node,
int textOffset,
String text,
TextStyle? style,
GestureRecognizer? recognizer,
) =>
TextSpan(
text: text,
style: style,
recognizer: recognizer,
mouseCursor: (recognizer != null) ? SystemMouseCursors.click : null,
);

abstract final class TextBlockUtils {
/// Get the horizontalSpacing using the default
/// implementation provided by [Flutter Quill]
Expand Down
3 changes: 1 addition & 2 deletions lib/src/toolbar/buttons/font_family_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import '../../common/utils/widgets.dart';
import '../../document/attribute.dart';
import '../../l10n/extensions/localizations_ext.dart';
import '../base_button/base_value_button.dart';

import '../simple_toolbar.dart';

class QuillToolbarFontFamilyButton extends QuillToolbarBaseButton<
Expand All @@ -18,7 +17,7 @@ class QuillToolbarFontFamilyButton extends QuillToolbarBaseButton<
/// over the [baseOptions].
super.baseOptions,
super.key,
}) : assert(options.items?.isNotEmpty ?? (true)),
}) : assert(options.items?.isNotEmpty ?? true),
assert(
options.initialValue == null || options.initialValue!.isNotEmpty,
);
Expand Down
Loading