mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
🐛 Keep distributed and justified lines inside the text box
SkParagraph moves each line right by half the letter-spacing of its first character. That centres span tracking, but the spacing added to single characters (distribution and justification gaps, squeezing, compression and ruby room) must only open the gap after them. A distributed label of two characters started half a gap in and overflowed the box by as much: "国家" in a 200px box drew 国 92px from the left edge. Builders of horizontal text record how far each adjusted character's letter-spacing departs from its span tracking, and `LayoutParagraph` takes back half of that on each line, next to its baseline shifts: painting, glyph and placeholder rects, line metrics, hit testing and text paths. Span tracking keeps its half spacing before the line. AI-assisted-by: claude-opus-5-5
This commit is contained in:
parent
2d56f26d5f
commit
db3e6e639d
@ -1,5 +1,5 @@
|
||||
use super::{filters, RenderState, Shape, SurfaceId, DEFAULT_EMOJI_FONT};
|
||||
use crate::shapes::text_paragraph::LayoutParagraph;
|
||||
use crate::shapes::text_paragraph::{HorizontalBuilder, LayoutParagraph};
|
||||
use crate::{
|
||||
error::Result,
|
||||
math::Rect,
|
||||
@ -14,7 +14,7 @@ use crate::{
|
||||
use skia_safe::{
|
||||
self as skia,
|
||||
canvas::SaveLayerRec,
|
||||
textlayout::{ParagraphBuilder, RectHeightStyle, RectWidthStyle, StyleMetrics, TextDecoration},
|
||||
textlayout::{RectHeightStyle, RectWidthStyle, StyleMetrics, TextDecoration},
|
||||
Canvas, ImageFilter, Paint,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
@ -181,7 +181,7 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
let adjustment = adjustments
|
||||
.as_ref()
|
||||
.and_then(|adjustments| adjustments[index].as_ref());
|
||||
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
|
||||
let mut stroke_paragraphs_map: std::collections::HashMap<usize, HorizontalBuilder> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for (span_index, (((span, text), sheds), extra)) in paragraph
|
||||
@ -226,7 +226,7 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
paragraph_style.set_text_align(skia::textlayout::TextAlign::Left);
|
||||
}
|
||||
}
|
||||
ParagraphBuilder::new(¶graph_style, fonts)
|
||||
HorizontalBuilder::new(¶graph_style, fonts)
|
||||
});
|
||||
let stroke_paint = stroke_paint.clone();
|
||||
let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent();
|
||||
@ -241,7 +241,7 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
}
|
||||
}
|
||||
|
||||
let stroke_paragraphs: Vec<ParagraphBuilder> = (0..stroke_paragraphs_map.len())
|
||||
let stroke_paragraphs: ParagraphBuilderGroup = (0..stroke_paragraphs_map.len())
|
||||
.filter_map(|i| stroke_paragraphs_map.remove(&i))
|
||||
.collect();
|
||||
|
||||
@ -320,7 +320,7 @@ pub fn render_with_bounds_outset(
|
||||
render_state: Option<&mut RenderState>,
|
||||
canvas: Option<&Canvas>,
|
||||
shape: &Shape,
|
||||
paragraph_builders: &mut [Vec<ParagraphBuilder>],
|
||||
paragraph_builders: &mut [ParagraphBuilderGroup],
|
||||
surface_id: Option<SurfaceId>,
|
||||
shadow: Option<&Paint>,
|
||||
blur: Option<&ImageFilter>,
|
||||
@ -348,7 +348,7 @@ pub fn render_with_bounds_outset(
|
||||
pub fn render_with_bounds_outset_overlay_emoji(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builders: &mut [Vec<ParagraphBuilder>],
|
||||
paragraph_builders: &mut [ParagraphBuilderGroup],
|
||||
shadow: Option<&Paint>,
|
||||
blur: Option<&ImageFilter>,
|
||||
stroke_bounds_outset: f32,
|
||||
@ -440,7 +440,7 @@ fn render_with_bounds_outset_inner(
|
||||
render_state: Option<&mut RenderState>,
|
||||
canvas: Option<&Canvas>,
|
||||
shape: &Shape,
|
||||
paragraph_builders: &mut [Vec<ParagraphBuilder>],
|
||||
paragraph_builders: &mut [ParagraphBuilderGroup],
|
||||
surface_id: Option<SurfaceId>,
|
||||
shadow: Option<&Paint>,
|
||||
blur: Option<&ImageFilter>,
|
||||
@ -491,7 +491,7 @@ pub fn render(
|
||||
render_state: Option<&mut RenderState>,
|
||||
canvas: Option<&Canvas>,
|
||||
shape: &Shape,
|
||||
paragraph_builders: &mut [Vec<ParagraphBuilder>],
|
||||
paragraph_builders: &mut [ParagraphBuilderGroup],
|
||||
surface_id: Option<SurfaceId>,
|
||||
shadow: Option<&Paint>,
|
||||
blur: Option<&ImageFilter>,
|
||||
@ -517,7 +517,7 @@ pub fn render(
|
||||
pub fn render_overlay_emoji(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builders: &mut [Vec<ParagraphBuilder>],
|
||||
paragraph_builders: &mut [ParagraphBuilderGroup],
|
||||
shadow: Option<&Paint>,
|
||||
blur: Option<&ImageFilter>,
|
||||
fill_inset: Option<f32>,
|
||||
@ -659,7 +659,7 @@ fn paint_from_cached_layout(canvas: &Canvas, shape: &Shape, text_content: &TextC
|
||||
fn render_text_on_canvas(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builders: &mut [Vec<ParagraphBuilder>],
|
||||
paragraph_builders: &mut [ParagraphBuilderGroup],
|
||||
shadow: Option<&Paint>,
|
||||
blur: Option<&ImageFilter>,
|
||||
fill_inset: Option<f32>,
|
||||
@ -736,7 +736,7 @@ fn render_text_on_canvas(
|
||||
pub fn paint_text_paragraphs(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
paragraph_builder_groups: &mut [ParagraphBuilderGroup],
|
||||
) {
|
||||
paint_text_with_emoji_overlay(canvas, shape, paragraph_builder_groups, false);
|
||||
}
|
||||
@ -745,7 +745,7 @@ pub fn paint_text_paragraphs(
|
||||
fn paint_text(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
paragraph_builder_groups: &mut [ParagraphBuilderGroup],
|
||||
) {
|
||||
paint_text_with_emoji_overlay(canvas, shape, paragraph_builder_groups, false);
|
||||
}
|
||||
@ -771,7 +771,7 @@ pub fn paint_text_mask(canvas: &Canvas, shape: &Shape) {
|
||||
fn paint_text_with_emoji_overlay(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
paragraph_builder_groups: &mut [ParagraphBuilderGroup],
|
||||
overlay_emoji: bool,
|
||||
) {
|
||||
let text_content = shape.get_text_content();
|
||||
@ -1064,8 +1064,8 @@ fn paint_emoji_opaque(
|
||||
pub fn render_emoji_overlay(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
emoji_builders: &mut [Vec<ParagraphBuilder>],
|
||||
deco_builders: &mut [Vec<ParagraphBuilder>],
|
||||
emoji_builders: &mut [ParagraphBuilderGroup],
|
||||
deco_builders: &mut [ParagraphBuilderGroup],
|
||||
surface_id: SurfaceId,
|
||||
blur: Option<&ImageFilter>,
|
||||
) {
|
||||
@ -1122,7 +1122,7 @@ pub fn render_emoji_overlay(
|
||||
fn draw_text(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
paragraph_builder_groups: &mut [ParagraphBuilderGroup],
|
||||
layer_opacity: Option<f32>,
|
||||
overlay_emoji: bool,
|
||||
) {
|
||||
@ -1156,9 +1156,9 @@ fn draw_text(
|
||||
fn render_masked_stroke_on_canvas(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
mask_builders: &mut [Vec<ParagraphBuilder>],
|
||||
stroke_builders: &mut [Vec<ParagraphBuilder>],
|
||||
fill_builders: Option<&mut [Vec<ParagraphBuilder>]>,
|
||||
mask_builders: &mut [ParagraphBuilderGroup],
|
||||
stroke_builders: &mut [ParagraphBuilderGroup],
|
||||
fill_builders: Option<&mut [ParagraphBuilderGroup]>,
|
||||
stroke_mask_blend: skia::BlendMode,
|
||||
blur: Option<&ImageFilter>,
|
||||
layer_opacity: Option<f32>,
|
||||
@ -1236,9 +1236,9 @@ fn render_masked_stroke(
|
||||
render_state: Option<&mut RenderState>,
|
||||
canvas: Option<&Canvas>,
|
||||
shape: &Shape,
|
||||
mask_builders: &mut [Vec<ParagraphBuilder>],
|
||||
stroke_builders: &mut [Vec<ParagraphBuilder>],
|
||||
mut fill_builders: Option<&mut [Vec<ParagraphBuilder>]>,
|
||||
mask_builders: &mut [ParagraphBuilderGroup],
|
||||
stroke_builders: &mut [ParagraphBuilderGroup],
|
||||
mut fill_builders: Option<&mut [ParagraphBuilderGroup]>,
|
||||
stroke_mask_blend: skia::BlendMode,
|
||||
surface_id: Option<SurfaceId>,
|
||||
blur: Option<&ImageFilter>,
|
||||
@ -1318,8 +1318,8 @@ pub fn render_inner_stroke(
|
||||
render_state: Option<&mut RenderState>,
|
||||
canvas: Option<&Canvas>,
|
||||
shape: &Shape,
|
||||
stroke_builders: &mut [Vec<ParagraphBuilder>],
|
||||
fill_builders: &mut [Vec<ParagraphBuilder>],
|
||||
stroke_builders: &mut [ParagraphBuilderGroup],
|
||||
fill_builders: &mut [ParagraphBuilderGroup],
|
||||
surface_id: Option<SurfaceId>,
|
||||
blur: Option<&ImageFilter>,
|
||||
stroke_bounds_outset: f32,
|
||||
@ -1346,7 +1346,7 @@ pub fn render_outer_stroke(
|
||||
render_state: Option<&mut RenderState>,
|
||||
canvas: Option<&Canvas>,
|
||||
shape: &Shape,
|
||||
stroke_builders: &mut [Vec<ParagraphBuilder>],
|
||||
stroke_builders: &mut [ParagraphBuilderGroup],
|
||||
surface_id: Option<SurfaceId>,
|
||||
blur: Option<&ImageFilter>,
|
||||
stroke_bounds_outset: f32,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use crate::render::text::decoration_segments;
|
||||
use crate::shapes::text_paragraph::LayoutParagraph;
|
||||
use crate::shapes::text_paragraph::{HorizontalBuilder, LayoutParagraph};
|
||||
use crate::{
|
||||
math::{Bounds, Matrix, Rect},
|
||||
render::{default_font, DEFAULT_EMOJI_FONT},
|
||||
@ -39,7 +39,7 @@ use crate::utils::{get_fallback_fonts, get_font_collection};
|
||||
use crate::Uuid;
|
||||
|
||||
// TODO: maybe move this to the wasm module?
|
||||
pub type ParagraphBuilderGroup = Vec<ParagraphBuilder>;
|
||||
pub type ParagraphBuilderGroup = Vec<HorizontalBuilder>;
|
||||
|
||||
/// True when the modifier changes the text layout container (resize), as opposed
|
||||
/// to rotation/move where glyph layout can be reused.
|
||||
@ -784,13 +784,13 @@ impl TextContent {
|
||||
paragraph: &Paragraph,
|
||||
plan: &HorizontalParagraphPlan,
|
||||
extra_sheds: &[Vec<(usize, f32)>],
|
||||
) -> ParagraphBuilder {
|
||||
) -> HorizontalBuilder {
|
||||
let fonts = get_font_collection();
|
||||
let fallback_fonts = get_fallback_fonts();
|
||||
let mut paragraph_style = paragraph.paragraph_to_style();
|
||||
paragraph_style.set_apply_rounding_hack(false);
|
||||
paragraph_style.set_text_align(skia::textlayout::TextAlign::Left);
|
||||
let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
|
||||
let mut builder = HorizontalBuilder::new(¶graph_style, fonts);
|
||||
for (span_index, ((span, text), sheds)) in paragraph
|
||||
.children()
|
||||
.iter()
|
||||
@ -1400,7 +1400,7 @@ impl TextContent {
|
||||
// The justify spacing already fills the lines.
|
||||
paragraph_style.set_text_align(skia::textlayout::TextAlign::Left);
|
||||
}
|
||||
let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
|
||||
let mut builder = HorizontalBuilder::new(¶graph_style, fonts);
|
||||
let mut has_text = false;
|
||||
for (span_index, (((span, text), plan_sheds), ruby_extra)) in paragraph
|
||||
.children()
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::shapes::text_paragraph::LayoutParagraph;
|
||||
use crate::shapes::text_paragraph::{LayoutParagraph, SpacedParagraphBuilder};
|
||||
// Horizontal Japanese layout on top of SkParagraph: the kinsoku layout
|
||||
// texts, punctuation aki, offset maps between source, layout and builder
|
||||
// text, and the ruby, emphasis and warichu painters.
|
||||
@ -164,7 +164,7 @@ fn horizontal_policy_spacing(paragraph: &Paragraph, texts: &[String]) -> Vec<Vec
|
||||
/// set narrower by their amounts; `extra` (from `horizontal_ruby_spacing`)
|
||||
/// adds letter-spacing to characters.
|
||||
pub(crate) fn add_horizontal_span(
|
||||
builder: &mut ParagraphBuilder,
|
||||
builder: &mut impl SpacedParagraphBuilder,
|
||||
span: &TextSpan,
|
||||
builder_text: &str,
|
||||
sheds: &[(usize, f32)],
|
||||
@ -173,6 +173,7 @@ pub(crate) fn add_horizontal_span(
|
||||
fonts: &skia::textlayout::FontCollection,
|
||||
) {
|
||||
if span.is_warichu() {
|
||||
let builder = builder.paragraph_builder();
|
||||
if builder_text.starts_with(kinsoku::FORCED_LINE_BREAK) {
|
||||
builder.add_text(kinsoku::FORCED_LINE_BREAK.to_string());
|
||||
}
|
||||
@ -214,9 +215,10 @@ pub(crate) fn add_horizontal_span(
|
||||
/// builder text stays unchanged: those at `sheds` lose their amount and
|
||||
/// those in `extra` take its added spacing. Skia tracks every cluster,
|
||||
/// including the zero-width word joiners the kinsoku pass inserts, so those
|
||||
/// take no letter-spacing.
|
||||
/// take no letter-spacing. The builder records how far each adjusted
|
||||
/// character departs from the span tracking.
|
||||
fn add_text_with_sheds(
|
||||
builder: &mut ParagraphBuilder,
|
||||
builder: &mut impl SpacedParagraphBuilder,
|
||||
span: &TextSpan,
|
||||
text: &str,
|
||||
sheds: &[(usize, f32)],
|
||||
@ -237,7 +239,12 @@ fn add_text_with_sheds(
|
||||
}
|
||||
}
|
||||
let mut piece_start = 0;
|
||||
// UTF-16 offset of the span text in the builder, read at the first piece.
|
||||
let mut builder_start = None;
|
||||
let mut utf16 = 0;
|
||||
for (index, (byte, ch)) in text.char_indices().enumerate() {
|
||||
let at = utf16;
|
||||
utf16 += ch.len_utf16();
|
||||
let (added, shed) = amounts[index];
|
||||
let letter_spacing = if kinsoku::is_inserted_zero_width(ch) {
|
||||
0.0
|
||||
@ -247,16 +254,29 @@ fn add_text_with_sheds(
|
||||
if letter_spacing == tracking {
|
||||
continue;
|
||||
}
|
||||
let start = *builder_start.get_or_insert_with(|| {
|
||||
builder
|
||||
.paragraph_builder()
|
||||
.get_text()
|
||||
.encode_utf16()
|
||||
.count()
|
||||
});
|
||||
builder.add_extra_spacing(start + at, letter_spacing - tracking);
|
||||
let mut style = text_style.clone();
|
||||
style.set_letter_spacing(letter_spacing);
|
||||
let end = byte + ch.len_utf8();
|
||||
add_text_with_tabs(builder, &text[piece_start..byte], span.font_size);
|
||||
builder.push_style(&style);
|
||||
builder.add_text(&text[byte..end]);
|
||||
builder.pop();
|
||||
let paragraph_builder = builder.paragraph_builder();
|
||||
add_text_with_tabs(paragraph_builder, &text[piece_start..byte], span.font_size);
|
||||
paragraph_builder.push_style(&style);
|
||||
paragraph_builder.add_text(&text[byte..end]);
|
||||
paragraph_builder.pop();
|
||||
piece_start = end;
|
||||
}
|
||||
add_text_with_tabs(builder, &text[piece_start..], span.font_size);
|
||||
add_text_with_tabs(
|
||||
builder.paragraph_builder(),
|
||||
&text[piece_start..],
|
||||
span.font_size,
|
||||
);
|
||||
}
|
||||
|
||||
/// The horizontal passes of one paragraph that layout, painting, position
|
||||
|
||||
@ -2078,3 +2078,93 @@ fn horizontal_emphasis_export_bounds_include_every_adjacent_ruby_mark() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixed-width `TextContent` of one paragraph, laid out like the renderer.
|
||||
fn aligned_content(text: &str, tracking: f32, align: TextAlign, width: f32) -> TextContent {
|
||||
let mut paragraph = Paragraph::new(
|
||||
align,
|
||||
TextDirection::LTR,
|
||||
None,
|
||||
None,
|
||||
1.0,
|
||||
tracking,
|
||||
vec![TextSpan {
|
||||
fills: vec![crate::shapes::Fill::Solid(crate::shapes::SolidColor(
|
||||
skia::Color::BLACK,
|
||||
))],
|
||||
..make_span(text, tracking)
|
||||
}],
|
||||
);
|
||||
paragraph.set_language("zh-Hans".to_string());
|
||||
let bounds = crate::math::Rect::from_xywh(0.0, 0.0, width, 200.0);
|
||||
let mut content = TextContent::new(bounds, crate::shapes::GrowType::Fixed);
|
||||
content.add_paragraph(paragraph);
|
||||
content.update_layout(bounds);
|
||||
content
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spread_lines_start_and_end_at_the_box_edges() {
|
||||
use skia::textlayout::{RectHeightStyle, RectWidthStyle};
|
||||
init_state();
|
||||
let mut resources =
|
||||
crate::render::RenderResources::try_new_headless().expect("headless resources");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
for (align, text, width) in [
|
||||
(TextAlign::Distribute, "国家", 200.0),
|
||||
(TextAlign::Distribute, "手机号", 200.0),
|
||||
(TextAlign::Justify, "国家国家国家国家国家国家国家国家", 50.0),
|
||||
] {
|
||||
let content = aligned_content(text, 0.0, align, width);
|
||||
let layout = &content.layout.paragraphs[0][0];
|
||||
let lines = layout.get_line_metrics();
|
||||
let spread = if align == TextAlign::Distribute {
|
||||
lines.len()
|
||||
} else {
|
||||
lines.len() - 1
|
||||
};
|
||||
assert!(spread > 0, "{text}: no spread line");
|
||||
let rect = |at: usize| {
|
||||
layout.get_rects_for_range(at..at + 1, RectHeightStyle::Tight, RectWidthStyle::Tight)[0]
|
||||
.rect
|
||||
};
|
||||
for line in lines.iter().take(spread) {
|
||||
let first = rect(line.start_index);
|
||||
let last = rect(line.end_excluding_whitespaces - 1);
|
||||
assert!(
|
||||
first.left.abs() < 0.1,
|
||||
"{text}: line starts at {}",
|
||||
first.left
|
||||
);
|
||||
assert!(line.left.abs() < 0.1, "{text}: line left {}", line.left);
|
||||
assert!(
|
||||
(last.right - width).abs() < 0.1,
|
||||
"{text}: line ends at {}, box {width}",
|
||||
last.right
|
||||
);
|
||||
let hit = layout.get_glyph_position_at_coordinate((first.left + 1.0, first.center_y()));
|
||||
assert_eq!(hit.position as usize, line.start_index, "{text}: hit test");
|
||||
}
|
||||
// The painted ink starts at the box edge too, not half a gap in.
|
||||
let mut surface = skia::surfaces::raster_n32_premul((400, 200)).unwrap();
|
||||
layout.paint(surface.canvas(), (100.0, 0.0));
|
||||
let pixels = surface.peek_pixels().unwrap();
|
||||
let ink_left = (0..400)
|
||||
.find(|x| (0..200).any(|y| pixels.get_color((*x, y)).a() > 0))
|
||||
.expect("visible ink");
|
||||
assert!(
|
||||
(100..104).contains(&ink_left),
|
||||
"{text}: ink starts at {}",
|
||||
ink_left - 100
|
||||
);
|
||||
}
|
||||
// Span tracking keeps SkParagraph's half spacing before the line.
|
||||
let content = aligned_content("国家", 10.0, TextAlign::Left, 200.0);
|
||||
let layout = &content.layout.paragraphs[0][0];
|
||||
let first = layout.get_rects_for_range(0..1, RectHeightStyle::Tight, RectWidthStyle::Tight)[0];
|
||||
assert!(
|
||||
(first.rect.left - 5.0).abs() < 0.01,
|
||||
"tracked line starts at {}",
|
||||
first.rect.left
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,32 +1,115 @@
|
||||
//! SkParagraph rounds every line height and baseline, even with its rounding
|
||||
//! hack disabled. Keep its shaping and horizontal layout, but use the original
|
||||
//! ascent/descent for vertical geometry and painting.
|
||||
//!
|
||||
//! SkParagraph also moves each line by half the letter-spacing of its first
|
||||
//! character. That centers span tracking, but letter-spacing added to single
|
||||
//! characters (justification, distribution, squeezing, ruby room) must only
|
||||
//! open the gap after them, so each line takes back half of that extra.
|
||||
|
||||
use skia_safe::{
|
||||
textlayout::{
|
||||
LineMetrics, Paragraph, ParagraphBuilder, PositionWithAffinity, RectHeightStyle,
|
||||
RectWidthStyle, TextBox,
|
||||
FontCollection, LineMetrics, Paragraph, ParagraphBuilder, ParagraphStyle,
|
||||
PositionWithAffinity, RectHeightStyle, RectWidthStyle, TextBox,
|
||||
},
|
||||
Canvas, Paint, Point, TextBlob, TextBlobBuilder,
|
||||
};
|
||||
use std::ops::{Deref, DerefMut, Range};
|
||||
|
||||
/// A builder `LayoutParagraph` can lay out, with the letter-spacing its
|
||||
/// characters take beyond their span tracking.
|
||||
pub trait SpacedParagraphBuilder {
|
||||
fn paragraph_builder(&mut self) -> &mut ParagraphBuilder;
|
||||
|
||||
/// Records `amount` of extra letter-spacing on the character at the
|
||||
/// builder's UTF-16 offset `at`.
|
||||
fn add_extra_spacing(&mut self, _at: usize, _amount: f32) {}
|
||||
|
||||
/// Extra letter-spacing as (UTF-16 offset, amount), sorted by offset.
|
||||
fn extra_spacing(&self) -> &[(usize, f32)] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
impl SpacedParagraphBuilder for ParagraphBuilder {
|
||||
fn paragraph_builder(&mut self) -> &mut ParagraphBuilder {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Paragraph builder of horizontal text, which records the extra
|
||||
/// letter-spacing of its characters.
|
||||
pub struct HorizontalBuilder {
|
||||
builder: ParagraphBuilder,
|
||||
extra_spacing: Vec<(usize, f32)>,
|
||||
}
|
||||
|
||||
impl HorizontalBuilder {
|
||||
pub fn new(style: &ParagraphStyle, fonts: impl Into<FontCollection>) -> Self {
|
||||
Self {
|
||||
builder: ParagraphBuilder::new(style, fonts),
|
||||
extra_spacing: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpacedParagraphBuilder for HorizontalBuilder {
|
||||
fn paragraph_builder(&mut self) -> &mut ParagraphBuilder {
|
||||
&mut self.builder
|
||||
}
|
||||
|
||||
fn add_extra_spacing(&mut self, at: usize, amount: f32) {
|
||||
self.extra_spacing.push((at, amount));
|
||||
}
|
||||
|
||||
fn extra_spacing(&self) -> &[(usize, f32)] {
|
||||
&self.extra_spacing
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for HorizontalBuilder {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("HorizontalBuilder")
|
||||
.field("extra_spacing", &self.extra_spacing)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for HorizontalBuilder {
|
||||
type Target = ParagraphBuilder;
|
||||
fn deref(&self) -> &ParagraphBuilder {
|
||||
&self.builder
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for HorizontalBuilder {
|
||||
fn deref_mut(&mut self) -> &mut ParagraphBuilder {
|
||||
&mut self.builder
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LayoutParagraph {
|
||||
inner: Paragraph,
|
||||
text: String,
|
||||
extra_spacing: Vec<(usize, f32)>,
|
||||
shifts: Vec<f32>,
|
||||
x_shifts: Vec<f32>,
|
||||
height: f32,
|
||||
runs: Vec<(TextBlob, Paint, Point)>,
|
||||
}
|
||||
|
||||
impl LayoutParagraph {
|
||||
pub fn build(builder: &mut ParagraphBuilder) -> Self {
|
||||
pub fn build(builder: &mut impl SpacedParagraphBuilder) -> Self {
|
||||
let extra_spacing = builder.extra_spacing().to_vec();
|
||||
let builder = builder.paragraph_builder();
|
||||
let text = builder.get_text().to_owned();
|
||||
Self {
|
||||
inner: builder.build(),
|
||||
text,
|
||||
extra_spacing,
|
||||
shifts: Vec::new(),
|
||||
x_shifts: Vec::new(),
|
||||
height: 0.0,
|
||||
runs: Vec::new(),
|
||||
}
|
||||
@ -36,6 +119,7 @@ impl LayoutParagraph {
|
||||
self.inner.layout(width);
|
||||
let lines = self.inner.get_line_metrics();
|
||||
self.shifts.clear();
|
||||
self.x_shifts.clear();
|
||||
self.height = 0.0;
|
||||
// Keep the first baseline anchored as before. Only remove drift
|
||||
// between lines; this also preserves existing one-line layouts.
|
||||
@ -46,12 +130,22 @@ impl LayoutParagraph {
|
||||
self.shifts
|
||||
.push(self.height + line.ascent as f32 + anchor - line.baseline as f32);
|
||||
self.height += (line.ascent + line.descent) as f32;
|
||||
let first = self
|
||||
.extra_spacing
|
||||
.binary_search_by_key(&line.start_index, |(at, _)| *at)
|
||||
.map_or(0.0, |found| self.extra_spacing[found].1);
|
||||
self.x_shifts.push(-first / 2.0);
|
||||
}
|
||||
if lines.is_empty() {
|
||||
self.height = self.inner.height();
|
||||
}
|
||||
self.runs.clear();
|
||||
if self.shifts.iter().all(|shift| shift.abs() < 0.00001) {
|
||||
if self
|
||||
.shifts
|
||||
.iter()
|
||||
.chain(&self.x_shifts)
|
||||
.all(|shift| shift.abs() < 0.00001)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Copy the paints before visiting: Skia exposes style metrics as
|
||||
@ -77,6 +171,7 @@ impl LayoutParagraph {
|
||||
boundaries.dedup();
|
||||
let text = &self.text;
|
||||
let shifts = &self.shifts;
|
||||
let x_shifts = &self.x_shifts;
|
||||
let runs = &mut self.runs;
|
||||
self.inner.visit(|line, info| {
|
||||
let Some(info) = info.filter(|info| info.count() > 0) else {
|
||||
@ -115,6 +210,7 @@ impl LayoutParagraph {
|
||||
}
|
||||
if let Some(blob) = builder.make() {
|
||||
let mut origin = info.origin();
|
||||
origin.x += x_shifts.get(line).copied().unwrap_or(0.0);
|
||||
origin.y += shifts.get(line).copied().unwrap_or(0.0);
|
||||
runs.push((blob, paint, origin));
|
||||
}
|
||||
@ -125,6 +221,10 @@ impl LayoutParagraph {
|
||||
self.shifts.get(line).copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
pub fn line_x_shift(&self, line: usize) -> f32 {
|
||||
self.x_shifts.get(line).copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
pub fn height(&self) -> f32 {
|
||||
self.height
|
||||
}
|
||||
@ -136,6 +236,7 @@ impl LayoutParagraph {
|
||||
pub fn get_line_metrics(&self) -> Vec<LineMetrics<'_>> {
|
||||
let mut lines = self.inner.get_line_metrics();
|
||||
for (index, line) in lines.iter_mut().enumerate() {
|
||||
line.left += self.line_x_shift(index) as f64;
|
||||
line.baseline += self.line_shift(index) as f64;
|
||||
line.height = line.ascent + line.descent;
|
||||
}
|
||||
@ -157,7 +258,9 @@ impl LayoutParagraph {
|
||||
}
|
||||
let mut boxes = self.inner.get_rects_for_range(start..end, height, width);
|
||||
for textbox in &mut boxes {
|
||||
textbox.rect.offset((0.0, self.line_shift(index)));
|
||||
textbox
|
||||
.rect
|
||||
.offset((self.line_x_shift(index), self.line_shift(index)));
|
||||
}
|
||||
result.extend(boxes);
|
||||
}
|
||||
@ -173,7 +276,9 @@ impl LayoutParagraph {
|
||||
.iter()
|
||||
.position(|line| center < (line.baseline + line.descent) as f32)
|
||||
.unwrap_or(lines.len().saturating_sub(1));
|
||||
textbox.rect.offset((0.0, self.line_shift(index)));
|
||||
textbox
|
||||
.rect
|
||||
.offset((self.line_x_shift(index), self.line_shift(index)));
|
||||
}
|
||||
boxes
|
||||
}
|
||||
@ -188,6 +293,7 @@ impl LayoutParagraph {
|
||||
.iter()
|
||||
.position(|line| point.y < (line.baseline + line.descent) as f32)
|
||||
.unwrap_or(lines.len().saturating_sub(1));
|
||||
point.x -= self.line_x_shift(index);
|
||||
point.y -= self.line_shift(index);
|
||||
self.inner.get_glyph_position_at_coordinate(point)
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@ impl TextPaths {
|
||||
}
|
||||
|
||||
let shifts: Vec<_> = (0..paragraph.get_line_metrics().len())
|
||||
.map(|line| paragraph.line_shift(line))
|
||||
.map(|line| Point::new(paragraph.line_x_shift(line), paragraph.line_shift(line)))
|
||||
.collect();
|
||||
paragraph.visit(|line: usize, info: Option<&VisitorInfo>| {
|
||||
let Some(info) = info else {
|
||||
@ -86,7 +86,7 @@ impl TextPaths {
|
||||
};
|
||||
|
||||
let font = info.font();
|
||||
let run_origin = origin + info.origin() + Point::new(0.0, shifts[line]);
|
||||
let run_origin = origin + info.origin() + shifts[line];
|
||||
let mut builder = skia::PathBuilder::new();
|
||||
let mut has_glyphs = false;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user