Line data Source code
1 : // Copyright 2014 The Flutter Authors.
2 : // Copyright 2021 Suragch.
3 : // All rights reserved.
4 : // Use of this source code is governed by a BSD-style license that can be
5 : // found in the LICENSE file.
6 :
7 : import 'dart:ui' as ui show Gradient, Shader;
8 :
9 : import 'package:flutter/foundation.dart';
10 : import 'package:flutter/rendering.dart';
11 : import 'package:flutter/widgets.dart';
12 : import 'package:mongol/src/base/mongol_text_align.dart';
13 :
14 : import '../base/mongol_text_painter.dart';
15 :
16 : // TODO: This is a horizontal elipsis. Should we use a Mongolian elipsis U+1801?
17 : const String _kEllipsis = '\u2026';
18 :
19 : /// A render object that displays a paragraph of vertical Mongolian text.
20 : class MongolRenderParagraph extends RenderBox
21 : with
22 : ContainerRenderObjectMixin<RenderBox, TextParentData>,
23 : RenderBoxContainerDefaultsMixin<RenderBox, TextParentData>,
24 : RelayoutWhenSystemFontsChangeMixin {
25 : /// Creates a vertical paragraph render object.
26 : ///
27 : /// The [maxLines] property may be null (and indeed defaults to null), but if
28 : /// it is not null, it must be greater than zero.
29 8 : MongolRenderParagraph(
30 : TextSpan text, {
31 : MongolTextAlign textAlign = MongolTextAlign.top,
32 : bool softWrap = true,
33 : TextOverflow overflow = TextOverflow.clip,
34 : double textScaleFactor = 1.0,
35 : int? maxLines,
36 1 : }) : assert(maxLines == null || maxLines > 0),
37 : _softWrap = softWrap,
38 : _overflow = overflow,
39 8 : _textPainter = MongolTextPainter(
40 : text: text,
41 : textAlign: textAlign,
42 : textScaleFactor: textScaleFactor,
43 : maxLines: maxLines,
44 8 : ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
45 : );
46 :
47 0 : @override
48 : void setupParentData(RenderBox child) {
49 0 : if (child.parentData is! TextParentData) {
50 0 : child.parentData = TextParentData();
51 : }
52 : }
53 :
54 : final MongolTextPainter _textPainter;
55 :
56 : /// The text to display
57 3 : TextSpan get text => _textPainter.text!;
58 6 : set text(TextSpan value) {
59 18 : switch (_textPainter.text!.compareTo(value)) {
60 6 : case RenderComparison.identical:
61 5 : case RenderComparison.metadata:
62 : return;
63 5 : case RenderComparison.paint:
64 6 : _textPainter.text = value;
65 3 : markNeedsPaint();
66 : break;
67 3 : case RenderComparison.layout:
68 6 : _textPainter.text = value;
69 3 : markNeedsLayout();
70 : break;
71 : }
72 : }
73 :
74 : /// How the text should be aligned vertically.
75 0 : MongolTextAlign get textAlign => _textPainter.textAlign;
76 6 : set textAlign(MongolTextAlign value) {
77 18 : if (_textPainter.textAlign == value) {
78 : return;
79 : }
80 2 : _textPainter.textAlign = value;
81 1 : markNeedsPaint();
82 : }
83 :
84 : /// Whether the text should break at soft line breaks.
85 : ///
86 : /// If false, the glyphs in the text will be positioned as if there was
87 : /// unlimited vertical space.
88 : ///
89 : /// If [softWrap] is false, [overflow] and [textAlign] may have unexpected
90 : /// effects.
91 16 : bool get softWrap => _softWrap;
92 : bool _softWrap;
93 6 : set softWrap(bool value) {
94 12 : if (_softWrap == value) {
95 : return;
96 : }
97 1 : _softWrap = value;
98 1 : markNeedsLayout();
99 : }
100 :
101 : /// How visual overflow should be handled.
102 4 : TextOverflow get overflow => _overflow;
103 : TextOverflow _overflow;
104 6 : set overflow(TextOverflow value) {
105 12 : if (_overflow == value) {
106 : return;
107 : }
108 1 : _overflow = value;
109 3 : _textPainter.ellipsis = value == TextOverflow.ellipsis ? _kEllipsis : null;
110 1 : markNeedsLayout();
111 : }
112 :
113 : /// The number of font pixels for each logical pixel.
114 : ///
115 : /// For example, if the text scale factor is 1.5, text will be 50% larger than
116 : /// the specified font size.
117 3 : double get textScaleFactor => _textPainter.textScaleFactor;
118 6 : set textScaleFactor(double value) {
119 18 : if (_textPainter.textScaleFactor == value) return;
120 4 : _textPainter.textScaleFactor = value;
121 2 : markNeedsLayout();
122 : }
123 :
124 : /// An optional maximum number of lines for the text to span, wrapping if
125 : /// necessary. If the text exceeds the given number of lines, it will be
126 : /// truncated according to [overflow] and [softWrap].
127 0 : int? get maxLines => _textPainter.maxLines;
128 :
129 : /// The value may be null. If it is not null, then it must be greater than
130 : /// zero.
131 6 : set maxLines(int? value) {
132 2 : assert(value == null || value > 0);
133 18 : if (_textPainter.maxLines == value) {
134 : return;
135 : }
136 2 : _textPainter.maxLines = value;
137 1 : _overflowShader = null;
138 1 : markNeedsLayout();
139 : }
140 :
141 : bool _needsClipping = false;
142 : ui.Shader? _overflowShader;
143 :
144 : /// Whether this paragraph currently has a [dart:ui.Shader] for its overflow
145 : /// effect.
146 : ///
147 : /// Used to test this object. Not for use in production.
148 0 : @visibleForTesting
149 0 : bool get debugHasOverflowShader => _overflowShader != null;
150 :
151 8 : void _layoutText({
152 : double minHeight = 0.0,
153 : double maxHeight = double.infinity,
154 : }) {
155 12 : final heightMatters = softWrap || overflow == TextOverflow.ellipsis;
156 16 : _textPainter.layout(
157 : minHeight: minHeight,
158 : maxHeight: heightMatters ? maxHeight : double.infinity,
159 : );
160 : }
161 :
162 8 : void _layoutTextWithConstraints(BoxConstraints constraints) {
163 8 : _layoutText(
164 8 : minHeight: constraints.minHeight,
165 8 : maxHeight: constraints.maxHeight,
166 : );
167 : }
168 :
169 5 : @override
170 : double computeMinIntrinsicHeight(double width) {
171 5 : _layoutText();
172 10 : return _textPainter.minIntrinsicHeight;
173 : }
174 :
175 5 : @override
176 : double computeMaxIntrinsicHeight(double width) {
177 5 : _layoutText();
178 10 : return _textPainter.maxIntrinsicHeight;
179 : }
180 :
181 5 : double _computeIntrinsicWidth(double height) {
182 5 : _layoutText(minHeight: height, maxHeight: height);
183 10 : return _textPainter.width;
184 : }
185 :
186 5 : @override
187 : double computeMinIntrinsicWidth(double height) {
188 5 : return _computeIntrinsicWidth(height);
189 : }
190 :
191 5 : @override
192 : double computeMaxIntrinsicWidth(double height) {
193 5 : return _computeIntrinsicWidth(height);
194 : }
195 :
196 2 : @override
197 : double computeDistanceToActualBaseline(TextBaseline baseline) {
198 2 : assert(!debugNeedsLayout);
199 4 : assert(constraints.debugAssertIsValid());
200 4 : _layoutTextWithConstraints(constraints);
201 4 : return _textPainter.computeDistanceToActualBaseline(baseline);
202 : }
203 :
204 4 : @override
205 : bool hitTestSelf(Offset position) => true;
206 :
207 4 : @override
208 : void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
209 4 : assert(debugHandleEvent(event, entry));
210 4 : if (event is! PointerDownEvent) return;
211 8 : _layoutTextWithConstraints(constraints);
212 4 : final offset = entry.localPosition;
213 8 : final position = _textPainter.getPositionForOffset(offset);
214 12 : final span = _textPainter.text!.getSpanForPosition(position);
215 : if (span == null) {
216 : return;
217 : }
218 4 : if (span is TextSpan) {
219 4 : span.recognizer?.addPointer(event);
220 : }
221 : }
222 :
223 8 : @override
224 : void performLayout() {
225 8 : final constraints = this.constraints;
226 8 : _layoutTextWithConstraints(constraints);
227 : // final textSize = _textPainter.size;
228 : // size = constraints.constrain(textSize);
229 :
230 : // We grab _textPainter.size and _textPainter.didExceedMaxLines here because
231 : // assigning to `size` will trigger us to validate our intrinsic sizes,
232 : // which will change _textPainter's layout because the intrinsic size
233 : // calculations are destructive. Other _textPainter state will also be
234 : // affected. See also MongolRenderEditable which has a similar issue.
235 16 : final textSize = _textPainter.size;
236 16 : final textDidExceedMaxLines = _textPainter.didExceedMaxLines;
237 16 : size = constraints.constrain(textSize);
238 :
239 : final didOverflowWidth =
240 32 : size.width < textSize.width || textDidExceedMaxLines;
241 32 : final didOverflowHeight = size.height < textSize.height;
242 : final hasVisualOverflow = didOverflowHeight || didOverflowWidth;
243 : if (hasVisualOverflow) {
244 1 : switch (_overflow) {
245 1 : case TextOverflow.visible:
246 0 : _needsClipping = false;
247 0 : _overflowShader = null;
248 : break;
249 1 : case TextOverflow.clip:
250 1 : case TextOverflow.ellipsis:
251 1 : _needsClipping = true;
252 1 : _overflowShader = null;
253 : break;
254 1 : case TextOverflow.fade:
255 1 : _needsClipping = true;
256 1 : final fadeSizePainter = MongolTextPainter(
257 4 : text: TextSpan(style: _textPainter.text!.style, text: '\u2026'),
258 1 : textScaleFactor: textScaleFactor,
259 1 : )..layout();
260 : if (didOverflowWidth) {
261 : double fadeEnd, fadeStart;
262 2 : fadeEnd = size.height;
263 2 : fadeStart = fadeEnd - fadeSizePainter.height;
264 2 : _overflowShader = ui.Gradient.linear(
265 1 : Offset(0.0, fadeStart),
266 1 : Offset(0.0, fadeEnd),
267 1 : <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
268 : );
269 : } else {
270 0 : final fadeEnd = size.width;
271 0 : final fadeStart = fadeEnd - fadeSizePainter.width / 2.0;
272 0 : _overflowShader = ui.Gradient.linear(
273 0 : Offset(fadeStart, 0.0),
274 0 : Offset(fadeEnd, 0.0),
275 0 : <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
276 : );
277 : }
278 : break;
279 : }
280 : } else {
281 8 : _needsClipping = false;
282 8 : _overflowShader = null;
283 : }
284 : }
285 :
286 8 : @override
287 : void paint(PaintingContext context, Offset offset) {
288 16 : _layoutTextWithConstraints(constraints);
289 8 : assert(() {
290 : if (debugRepaintTextRainbowEnabled) {
291 0 : final paint = Paint()..color = debugCurrentRepaintColor.toColor();
292 0 : context.canvas.drawRect(offset & size, paint);
293 : }
294 : return true;
295 8 : }());
296 :
297 8 : if (_needsClipping) {
298 2 : final bounds = offset & size;
299 1 : if (_overflowShader != null) {
300 : // This layer limits what the shader below blends with to be just the
301 : // text (as opposed to the text and its background).
302 3 : context.canvas.saveLayer(bounds, Paint());
303 : } else {
304 2 : context.canvas.save();
305 : }
306 2 : context.canvas.clipRect(bounds);
307 : }
308 :
309 24 : _textPainter.paint(context.canvas, offset);
310 :
311 8 : if (_needsClipping) {
312 1 : if (_overflowShader != null) {
313 4 : context.canvas.translate(offset.dx, offset.dy);
314 1 : final paint = Paint()
315 1 : ..blendMode = BlendMode.modulate
316 2 : ..shader = _overflowShader;
317 4 : context.canvas.drawRect(Offset.zero & size, paint);
318 : }
319 2 : context.canvas.restore();
320 : }
321 : }
322 :
323 0 : @override
324 : List<DiagnosticsNode> debugDescribeChildren() {
325 0 : return <DiagnosticsNode>[
326 0 : text.toDiagnosticsNode(
327 : name: 'text', style: DiagnosticsTreeStyle.transition)
328 : ];
329 : }
330 :
331 0 : @override
332 : void debugFillProperties(DiagnosticPropertiesBuilder properties) {
333 0 : super.debugFillProperties(properties);
334 0 : properties.add(EnumProperty<MongolTextAlign>('textAlign', textAlign));
335 0 : properties.add(FlagProperty(
336 : 'softWrap',
337 0 : value: softWrap,
338 : ifTrue: 'wrapping at box height',
339 : ifFalse: 'no wrapping except at line break characters',
340 : showName: true,
341 : ));
342 0 : properties.add(EnumProperty<TextOverflow>('overflow', overflow));
343 0 : properties.add(
344 0 : DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: 1.0));
345 0 : properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
346 : }
347 : }
|