mirror of
https://github.com/penpot/penpot.git
synced 2026-08-07 21:38:48 +00:00
fixes horizontal furigana
This commit is contained in:
parent
6b26950bdc
commit
9544cb3908
@ -1727,6 +1727,11 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !text_content.is_vertical() && self.options.is_text_grid_visible() {
|
||||
let canvas = self.surfaces.canvas_and_mark_dirty(fills_surface_id);
|
||||
text::paint_horizontal_grid(canvas, &shape, text_content);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
|
||||
@ -11,7 +11,9 @@ use crate::{
|
||||
use skia_safe::{
|
||||
self as skia,
|
||||
canvas::SaveLayerRec,
|
||||
textlayout::{ParagraphBuilder, StyleMetrics, TextDecoration, TextStyle},
|
||||
textlayout::{
|
||||
ParagraphBuilder, RectHeightStyle, RectWidthStyle, StyleMetrics, TextDecoration, TextStyle,
|
||||
},
|
||||
Canvas, ImageFilter, Paint,
|
||||
};
|
||||
|
||||
@ -100,6 +102,62 @@ pub fn render_vertical_text(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Paint a viewport-only grid over SkParagraph horizontal text. Blue outlines
|
||||
/// show line boxes, green boxes show the per-scalar tight rectangles returned
|
||||
/// by SkParagraph, and amber rules show baselines. This mirrors the vertical
|
||||
/// text grid while making horizontal annotation anchors inspectable.
|
||||
pub fn paint_horizontal_grid(canvas: &Canvas, shape: &Shape, text_content: &TextContent) {
|
||||
let mut builders = text_content.paragraph_builder_group_from_text(None);
|
||||
let layout = calculate_text_layout_data(shape, text_content, &mut builders, true);
|
||||
|
||||
let mut line_paint = Paint::default();
|
||||
line_paint.set_anti_alias(true);
|
||||
line_paint.set_style(skia::PaintStyle::Stroke);
|
||||
line_paint.set_stroke_width(1.0);
|
||||
line_paint.set_color(skia::Color::from_argb(0xAA, 0x2F, 0x80, 0xED));
|
||||
|
||||
let mut glyph_paint = Paint::default();
|
||||
glyph_paint.set_anti_alias(true);
|
||||
glyph_paint.set_style(skia::PaintStyle::Stroke);
|
||||
glyph_paint.set_stroke_width(1.0);
|
||||
glyph_paint.set_color(skia::Color::from_argb(0x99, 0x27, 0xAE, 0x60));
|
||||
|
||||
let mut baseline_paint = Paint::default();
|
||||
baseline_paint.set_anti_alias(true);
|
||||
baseline_paint.set_stroke_width(1.0);
|
||||
baseline_paint.set_color(skia::Color::from_argb(0xCC, 0xEB, 0x57, 0x57));
|
||||
|
||||
for paragraph in &layout.paragraphs {
|
||||
for line in paragraph.paragraph.get_line_metrics() {
|
||||
let baseline = line.baseline as f32;
|
||||
let top = paragraph.y + baseline - line.ascent as f32;
|
||||
let left = paragraph.x + line.left as f32;
|
||||
let width = line.width as f32;
|
||||
canvas.draw_rect(
|
||||
Rect::from_xywh(left, top, width, line.height as f32),
|
||||
&line_paint,
|
||||
);
|
||||
canvas.draw_line(
|
||||
(left, paragraph.y + baseline),
|
||||
(left + width, paragraph.y + baseline),
|
||||
&baseline_paint,
|
||||
);
|
||||
|
||||
for offset in line.start_index..line.end_index {
|
||||
for textbox in paragraph.paragraph.get_rects_for_range(
|
||||
offset..offset + 1,
|
||||
RectHeightStyle::Tight,
|
||||
RectWidthStyle::Tight,
|
||||
) {
|
||||
let mut rect = textbox.rect;
|
||||
rect.offset((paragraph.x, paragraph.y));
|
||||
canvas.draw_rect(rect, &glyph_paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stroke_paragraph_builder_group_from_text(
|
||||
text_content: &TextContent,
|
||||
stroke: &Stroke,
|
||||
|
||||
@ -2190,6 +2190,16 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_annotation_uses_the_base_em_not_typographic_leading() {
|
||||
let rect = skia::Rect::from_xywh(0.0, 66.0, 56.0, 90.0);
|
||||
|
||||
assert_eq!(
|
||||
crate::shapes::text_japanese::horizontal_annotation_over_top(rect, 56.0),
|
||||
88.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_warichu_collapses_to_one_builder_position() {
|
||||
init_state();
|
||||
|
||||
@ -522,6 +522,18 @@ pub(crate) fn horizontal_span_style(
|
||||
})
|
||||
}
|
||||
|
||||
/// Top of the horizontal base em inside SkParagraph's typographic rectangle.
|
||||
/// Its tight rect can include substantial ascender-side padding; anchoring an
|
||||
/// over annotation to that rect's top therefore leaves a visible gap above CJK
|
||||
/// ink. The em is bottom-aligned to the rect, matching the baseline model used
|
||||
/// by the horizontal painter. The ruby showcase needs a 12 px clearance at
|
||||
/// 56 px: the original 6 px adjustment still left it 6 px too close to the
|
||||
/// kanji. Keep the 3/14-em value proportional at other text sizes.
|
||||
pub(crate) fn horizontal_annotation_over_top(rect: skia::Rect, font_size: f32) -> f32 {
|
||||
let font_size = font_size.max(0.0);
|
||||
rect.top.max(rect.bottom - font_size) - font_size * (3.0 / 14.0)
|
||||
}
|
||||
|
||||
/// Paint horizontal emphasis marks (圏点 / bouten) above their base glyphs.
|
||||
/// The base paragraph retains its normal metrics; interlinear collision and
|
||||
/// automatic line-gap expansion remain a separate layout policy.
|
||||
@ -549,8 +561,26 @@ pub(crate) fn paint_horizontal_emphasis(
|
||||
style.set_height_override(true);
|
||||
style.set_letter_spacing(0.0);
|
||||
let mark_paragraph = warichu_mini_paragraph(&mark.to_string(), &style, f32::MAX);
|
||||
let mark_width = mark_paragraph.longest_line();
|
||||
let mark_height = mark_paragraph.height();
|
||||
let mark_ink = mark_paragraph
|
||||
.get_rects_for_range(
|
||||
0..mark.len_utf16(),
|
||||
RectHeightStyle::Tight,
|
||||
RectWidthStyle::Tight,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|textbox| textbox.rect)
|
||||
.reduce(|mut rect, next| {
|
||||
rect.join(next);
|
||||
rect
|
||||
});
|
||||
let mark_width = mark_ink
|
||||
.as_ref()
|
||||
.map(|rect| rect.width())
|
||||
.unwrap_or_else(|| mark_paragraph.longest_line());
|
||||
let mark_ink_bottom = mark_ink
|
||||
.as_ref()
|
||||
.map(|rect| rect.bottom())
|
||||
.unwrap_or_else(|| mark_paragraph.height());
|
||||
for placement in placements
|
||||
.iter()
|
||||
.filter(|placement| placement.span == range.span && placement.mark == mark)
|
||||
@ -564,7 +594,9 @@ pub(crate) fn paint_horizontal_emphasis(
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let mark_y = y + placement.rect.top() - mark_height - ruby_offset;
|
||||
let mark_y = y + horizontal_annotation_over_top(placement.rect, span.font_size)
|
||||
- mark_ink_bottom
|
||||
- ruby_offset;
|
||||
mark_paragraph.paint(canvas, (mark_x, mark_y));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3103,6 +3103,24 @@ fn next_horizontal_ruby_range(
|
||||
offset_map.to_shifted(start)..offset_map.to_shifted(*utf16_cursor)
|
||||
}
|
||||
|
||||
/// Baseline adjustment that attaches a glyph's visible ink edge to its base
|
||||
/// strip. Font-wide ascender/descender metrics include leading which makes
|
||||
/// horizontal ruby visibly detached for many Japanese faces.
|
||||
fn horizontal_ruby_ink_edge(font: &Font, glyph: GlyphId, fallback: f32, over: bool) -> f32 {
|
||||
let mut bounds = [skia::Rect::default()];
|
||||
font.get_bounds(&[glyph], &mut bounds, None);
|
||||
let bound = bounds[0];
|
||||
if bound.right > bound.left && bound.bottom > bound.top {
|
||||
if over {
|
||||
bound.bottom
|
||||
} else {
|
||||
bound.top
|
||||
}
|
||||
} else {
|
||||
fallback
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint ruby annotations for one horizontally laid-out paragraph. Draw-only:
|
||||
/// base rects come from the already laid-out skparagraph
|
||||
/// (`get_rects_for_range`), the annotation is shaped at half the span size
|
||||
@ -3220,8 +3238,26 @@ pub fn paint_horizontal_ruby(
|
||||
let run = &shaped[*run_index];
|
||||
let (_, metrics) = run.font.metrics();
|
||||
let baseline = match span.ruby_side {
|
||||
RubySide::Over => y + rect_box.rect.top() - metrics.descent,
|
||||
RubySide::Under => y + rect_box.rect.bottom() - metrics.ascent,
|
||||
RubySide::Over => {
|
||||
y + super::text_japanese::horizontal_annotation_over_top(
|
||||
rect_box.rect,
|
||||
span.font_size,
|
||||
) - horizontal_ruby_ink_edge(
|
||||
&run.font,
|
||||
run.glyphs[*glyph],
|
||||
metrics.descent,
|
||||
true,
|
||||
)
|
||||
}
|
||||
RubySide::Under => {
|
||||
y + rect_box.rect.bottom()
|
||||
- horizontal_ruby_ink_edge(
|
||||
&run.font,
|
||||
run.glyphs[*glyph],
|
||||
metrics.ascent,
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
let mut builder = TextBlobBuilder::new();
|
||||
let (out_glyphs, points) = builder.alloc_run_pos(&run.font, 1, None);
|
||||
@ -6551,6 +6587,27 @@ mod tests {
|
||||
assert!((shift - metrics_shift).abs() > 0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_ruby_attaches_to_visible_glyph_ink() {
|
||||
let layout = layout_content(&make_content(&["a"], 1000.0), 1000.0);
|
||||
let CellKind::Rotated { run } = layout.cells[0].kind else {
|
||||
panic!("expected a rotated run");
|
||||
};
|
||||
let run = &layout.runs[run];
|
||||
let mut bounds = [skia::Rect::default()];
|
||||
run.font.get_bounds(&[run.glyphs[0]], &mut bounds, None);
|
||||
|
||||
assert!(
|
||||
(horizontal_ruby_ink_edge(&run.font, run.glyphs[0], 0.0, true) - bounds[0].bottom)
|
||||
.abs()
|
||||
< f32::EPSILON
|
||||
);
|
||||
assert!(
|
||||
(horizontal_ruby_ink_edge(&run.font, run.glyphs[0], 0.0, false) - bounds[0].top).abs()
|
||||
< f32::EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn justify_fills_non_last_columns() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user