diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e3b163c..2a4bd971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.4.0] +* Improve inline code style. + ## [2.3.3] * Improves selection rects to have consistent height regardless of individual segment text styles. diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index af1469c1..dab9cc03 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -import 'style_widgets/style_widgets.dart'; +import '../../flutter_quill.dart'; +import '../../models/documents/style.dart'; class QuillStyles extends InheritedWidget { const QuillStyles({ @@ -27,6 +28,8 @@ class QuillStyles extends InheritedWidget { } } +/// Style theme applied to a block of rich text, including single-line +/// paragraphs. class DefaultTextBlockStyle { DefaultTextBlockStyle( this.style, @@ -35,15 +38,88 @@ class DefaultTextBlockStyle { this.decoration, ); + /// Base text style for a text block. final TextStyle style; + /// Vertical spacing around a text block. final Tuple2 verticalSpacing; + /// Vertical spacing for individual lines within a text block. + /// final Tuple2 lineSpacing; + /// Decoration of a text block. + /// + /// Decoration, if present, is painted in the content area, excluding + /// any [spacing]. final BoxDecoration? decoration; } +/// Theme data for inline code. +class InlineCodeStyle { + InlineCodeStyle({ + required this.style, + this.heading1, + this.heading2, + this.heading3, + this.backgroundColor, + this.radius, + }); + + /// Base text style for an inline code. + final TextStyle style; + + /// Style override for inline code in headings level 1. + final TextStyle? heading1; + + /// Style override for inline code in headings level 2. + final TextStyle? heading2; + + /// Style override for inline code in headings level 3. + final TextStyle? heading3; + + /// Background color for inline code. + final Color? backgroundColor; + + /// Radius used when paining the background. + final Radius? radius; + + /// Returns effective style to use for inline code for the specified + /// [lineStyle]. + TextStyle styleFor(Style lineStyle) { + if (lineStyle.containsKey(Attribute.h1.key)) { + return heading1 ?? style; + } + if (lineStyle.containsKey(Attribute.h2.key)) { + return heading2 ?? style; + } + if (lineStyle.containsKey(Attribute.h3.key)) { + return heading3 ?? style; + } + return style; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! InlineCodeStyle) { + return false; + } + return other.style == style && + other.heading1 == heading1 && + other.heading2 == heading2 && + other.heading3 == heading3 && + other.backgroundColor == backgroundColor && + other.radius == radius; + } + + @override + int get hashCode => + Object.hash(style, heading1, heading2, heading3, backgroundColor, radius); +} + class DefaultListBlockStyle extends DefaultTextBlockStyle { DefaultListBlockStyle( TextStyle style, @@ -91,7 +167,9 @@ class DefaultStyles { final TextStyle? small; final TextStyle? underline; final TextStyle? strikeThrough; - final TextStyle? inlineCode; + + /// Theme of inline code. + final InlineCodeStyle? inlineCode; final TextStyle? sizeSmall; // 'small' final TextStyle? sizeLarge; // 'large' final TextStyle? sizeHuge; // 'huge' @@ -129,6 +207,12 @@ class DefaultStyles { throw UnimplementedError(); } + final inlineCodeStyle = TextStyle( + fontSize: 14, + color: themeData.colorScheme.primaryVariant.withOpacity(0.8), + fontFamily: fontFamily, + ); + return DefaultStyles( h1: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( @@ -167,10 +251,19 @@ class DefaultStyles { small: const TextStyle(fontSize: 12, color: Colors.black45), underline: const TextStyle(decoration: TextDecoration.underline), strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough), - inlineCode: TextStyle( - color: Colors.blue.shade900.withOpacity(0.9), - fontFamily: fontFamily, - fontSize: 13, + inlineCode: InlineCodeStyle( + backgroundColor: Colors.grey.shade100, + radius: const Radius.circular(3), + style: inlineCodeStyle, + heading1: inlineCodeStyle.copyWith( + fontSize: 32, + fontWeight: FontWeight.w300, + ), + heading2: inlineCodeStyle.copyWith(fontSize: 22), + heading3: inlineCodeStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w500, + ), ), link: TextStyle( color: themeData.colorScheme.secondary, diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index d2762f7c..d75daffd 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -12,6 +12,7 @@ import '../models/documents/nodes/leaf.dart' as leaf; import '../models/documents/nodes/leaf.dart'; import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/node.dart'; +import '../models/documents/style.dart'; import '../utils/color.dart'; import 'box.dart'; import 'cursor.dart'; @@ -118,7 +119,7 @@ class TextLine extends StatelessWidget { TextSpan _buildTextSpan(DefaultStyles defaultStyles, LinkedList nodes, TextStyle lineStyle) { final children = nodes - .map((node) => _getTextSpanFromNode(defaultStyles, node)) + .map((node) => _getTextSpanFromNode(defaultStyles, node, line.style)) .toList(growable: false); return TextSpan(children: children, style: lineStyle); @@ -179,9 +180,10 @@ class TextLine extends StatelessWidget { return textStyle; } - TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) { + TextSpan _getTextSpanFromNode( + DefaultStyles defaultStyles, Node node, Style lineStyle) { final textNode = node as leaf.Text; - final style = textNode.style; + final nodeStyle = textNode.style; var res = const TextStyle(); // This is inline text style final color = textNode.style.attributes[Attribute.color.key]; var hasLink = false; @@ -193,9 +195,8 @@ class TextLine extends StatelessWidget { Attribute.link.key: defaultStyles.link, Attribute.underline.key: defaultStyles.underline, Attribute.strikeThrough.key: defaultStyles.strikeThrough, - Attribute.inlineCode.key: defaultStyles.inlineCode, }.forEach((k, s) { - if (style.values.any((v) => v.key == k)) { + if (nodeStyle.values.any((v) => v.key == k)) { if (k == Attribute.underline.key || k == Attribute.strikeThrough.key) { var textColor = defaultStyles.color; if (color?.value is String) { @@ -212,6 +213,10 @@ class TextLine extends StatelessWidget { } }); + if (nodeStyle.containsKey(Attribute.inlineCode.key)) { + res = _merge(res, defaultStyles.inlineCode!.styleFor(lineStyle)); + } + final font = textNode.style.attributes[Attribute.font.key]; if (font != null && font.value != null) { res = res.merge(TextStyle(fontFamily: font.value)); @@ -323,6 +328,7 @@ class EditableTextLine extends RenderObjectWidget { @override RenderObject createRenderObject(BuildContext context) { + final defaultStyles = DefaultStyles.getInstance(context); return RenderEditableTextLine( line, textDirection, @@ -332,12 +338,14 @@ class EditableTextLine extends RenderObjectWidget { devicePixelRatio, _getPadding(), color, - cursorCont); + cursorCont, + defaultStyles.inlineCode!); } @override void updateRenderObject( BuildContext context, covariant RenderEditableTextLine renderObject) { + final defaultStyles = DefaultStyles.getInstance(context); renderObject ..setLine(line) ..setPadding(_getPadding()) @@ -347,7 +355,8 @@ class EditableTextLine extends RenderObjectWidget { ..setEnableInteractiveSelection(enableInteractiveSelection) ..hasFocus = hasFocus ..setDevicePixelRatio(devicePixelRatio) - ..setCursorCont(cursorCont); + ..setCursorCont(cursorCont) + ..setInlineCodeStyle(defaultStyles.inlineCode!); } EdgeInsetsGeometry _getPadding() { @@ -361,17 +370,18 @@ class EditableTextLine extends RenderObjectWidget { enum TextLineSlot { LEADING, BODY } class RenderEditableTextLine extends RenderEditableBox { + /// Creates new editable paragraph render box. RenderEditableTextLine( - this.line, - this.textDirection, - this.textSelection, - this.enableInteractiveSelection, - this.hasFocus, - this.devicePixelRatio, - this.padding, - this.color, - this.cursorCont, - ); + this.line, + this.textDirection, + this.textSelection, + this.enableInteractiveSelection, + this.hasFocus, + this.devicePixelRatio, + this.padding, + this.color, + this.cursorCont, + this.inlineCodeStyle); RenderBox? _leading; RenderContentProxyBox? _body; @@ -388,6 +398,7 @@ class RenderEditableTextLine extends RenderEditableBox { bool? _containsCursor; List? _selectedRects; late Rect _caretPrototype; + InlineCodeStyle inlineCodeStyle; final Map children = {}; Iterable get _children sync* { @@ -497,6 +508,14 @@ class RenderEditableTextLine extends RenderEditableBox { _body = _updateChild(_body, b, TextLineSlot.BODY) as RenderContentProxyBox?; } + void setInlineCodeStyle(InlineCodeStyle newStyle) { + if (inlineCodeStyle == newStyle) return; + inlineCodeStyle = newStyle; + markNeedsLayout(); + } + + // Start selection implementation + bool containsTextSelection() { return line.documentOffset <= textSelection.end && textSelection.start <= line.documentOffset + line.length - 1; @@ -870,6 +889,31 @@ class RenderEditableTextLine extends RenderEditableBox { final parentData = _body!.parentData as BoxParentData; final effectiveOffset = offset + parentData.offset; + if (inlineCodeStyle.backgroundColor != null) { + for (final item in line.children) { + if (item is! leaf.Text || + !item.style.containsKey(Attribute.inlineCode.key)) { + continue; + } + final textRange = TextSelection( + baseOffset: item.offset, extentOffset: item.offset + item.length); + final rects = _body!.getBoxesForSelection(textRange); + final paint = Paint()..color = inlineCodeStyle.backgroundColor!; + for (final box in rects) { + final rect = box.toRect().translate(0, 1).shift(effectiveOffset); + if (inlineCodeStyle.radius == null) { + final paintRect = Rect.fromLTRB( + rect.left - 2, rect.top, rect.right + 2, rect.bottom); + context.canvas.drawRect(paintRect, paint); + } else { + final paintRect = RRect.fromLTRBR(rect.left - 2, rect.top, + rect.right + 2, rect.bottom, inlineCodeStyle.radius!); + context.canvas.drawRRect(paintRect, paint); + } + } + } + } + if (hasFocus && cursorCont.show.value && containsCursor() &&