blob: 2d57b5e13ff2bca3bb3db3975fcf3d2bc1edf889 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:ui/ui.dart' as ui;
import '../html/bitmap_canvas.dart';
import '../html/painting.dart';
import 'canvas_paragraph.dart';
import 'layout_service.dart';
import 'paragraph.dart';
/// Responsible for painting a [CanvasParagraph] on a [BitmapCanvas].
class TextPaintService {
TextPaintService(this.paragraph);
final CanvasParagraph paragraph;
void paint(BitmapCanvas canvas, ui.Offset offset) {
// Loop through all the lines, for each line, loop through all the boxes and
// paint them. The boxes have enough information so they can be painted
// individually.
final List<EngineLineMetrics> lines = paragraph.computeLineMetrics();
if (lines.isEmpty) {
return;
}
final EngineLineMetrics lastLine = lines.last;
for (final EngineLineMetrics line in lines) {
if (line.boxes.isEmpty) {
continue;
}
final RangeBox lastBox = line.boxes.last;
final double justifyPerSpaceBox =
_calculateJustifyPerSpaceBox(paragraph, line, lastLine, lastBox);
ui.Offset justifiedOffset = offset;
for (final RangeBox box in line.boxes) {
final bool isTrailingSpaceBox =
box == lastBox && box is SpanBox && box.isSpaceOnly;
// Don't paint background for the trailing space in the line.
if (!isTrailingSpaceBox) {
_paintBackground(canvas, justifiedOffset, line, box, justifyPerSpaceBox);
}
_paintText(canvas, justifiedOffset, line, box);
if (box is SpanBox && box.isSpaceOnly && justifyPerSpaceBox != 0.0) {
justifiedOffset = justifiedOffset.translate(justifyPerSpaceBox, 0.0);
}
}
}
}
void _paintBackground(
BitmapCanvas canvas,
ui.Offset offset,
EngineLineMetrics line,
RangeBox box,
double justifyPerSpaceBox,
) {
if (box is SpanBox) {
final FlatTextSpan span = box.span;
// Paint the background of the box, if the span has a background.
final SurfacePaint? background = span.style.background as SurfacePaint?;
if (background != null) {
ui.Rect rect = box.toTextBox(line).toRect().shift(offset);
if (box.isSpaceOnly) {
rect = ui.Rect.fromPoints(
rect.topLeft,
rect.bottomRight.translate(justifyPerSpaceBox, 0.0),
);
}
canvas.drawRect(rect, background.paintData);
}
}
}
void _paintText(
BitmapCanvas canvas,
ui.Offset offset,
EngineLineMetrics line,
RangeBox box,
) {
// There's no text to paint in placeholder spans.
if (box is SpanBox) {
final FlatTextSpan span = box.span;
_applySpanStyleToCanvas(span, canvas);
final double x = offset.dx + line.left + box.left;
final double y = offset.dy + line.baseline;
// Don't paint the text for space-only boxes. This is just an
// optimization, it doesn't have any effect on the output.
if (!box.isSpaceOnly) {
final String text = paragraph.toPlainText().substring(
box.start.index,
box.end.indexWithoutTrailingNewlines,
);
final double? letterSpacing = span.style.letterSpacing;
if (letterSpacing == null || letterSpacing == 0.0) {
canvas.fillText(text, x, y, shadows: span.style.shadows);
} else {
// TODO(mdebbar): Implement letter-spacing on canvas more efficiently:
// https://github.com/flutter/flutter/issues/51234
double charX = x;
final int len = text.length;
for (int i = 0; i < len; i++) {
final String char = text[i];
canvas.fillText(char, charX.roundToDouble(), y,
shadows: span.style.shadows);
charX += letterSpacing + canvas.measureText(char).width!;
}
}
}
// Paint the ellipsis using the same span styles.
final String? ellipsis = line.ellipsis;
if (ellipsis != null && box == line.boxes.last) {
final double x = offset.dx + line.left + box.right;
canvas.fillText(ellipsis, x, y);
}
canvas.tearDownPaint();
}
}
void _applySpanStyleToCanvas(FlatTextSpan span, BitmapCanvas canvas) {
final SurfacePaint? paint;
final ui.Paint? foreground = span.style.foreground;
if (foreground != null) {
paint = foreground as SurfacePaint;
} else {
paint = (ui.Paint()..color = span.style.color!) as SurfacePaint;
}
canvas.setCssFont(span.style.cssFontString);
canvas.setUpPaint(paint.paintData, null);
}
}
/// Calculates for the given [line], the amount of extra width that needs to be
/// added to each space box in order to align the line with the rest of the
/// paragraph.
double _calculateJustifyPerSpaceBox(
CanvasParagraph paragraph,
EngineLineMetrics line,
EngineLineMetrics lastLine,
RangeBox lastBox,
) {
// Don't apply any justification on the last line.
if (line != lastLine &&
paragraph.width.isFinite &&
paragraph.paragraphStyle.textAlign == ui.TextAlign.justify) {
final double justifyTotal = paragraph.width - line.width;
int spaceBoxesToJustify = line.spaceBoxCount;
// If the last box is a space box, we can't use it to justify text.
if (lastBox is SpanBox && lastBox.isSpaceOnly) {
spaceBoxesToJustify--;
}
if (spaceBoxesToJustify > 0) {
return justifyTotal / spaceBoxesToJustify;
}
}
return 0.0;
}