mirror of
https://github.com/penpot/penpot.git
synced 2026-08-26 22:58:43 +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| {
|
self.surfaces.apply_mut(surface_ids, |s| {
|
||||||
|
|||||||
@ -11,7 +11,9 @@ use crate::{
|
|||||||
use skia_safe::{
|
use skia_safe::{
|
||||||
self as skia,
|
self as skia,
|
||||||
canvas::SaveLayerRec,
|
canvas::SaveLayerRec,
|
||||||
textlayout::{ParagraphBuilder, StyleMetrics, TextDecoration, TextStyle},
|
textlayout::{
|
||||||
|
ParagraphBuilder, RectHeightStyle, RectWidthStyle, StyleMetrics, TextDecoration, TextStyle,
|
||||||
|
},
|
||||||
Canvas, ImageFilter, Paint,
|
Canvas, ImageFilter, Paint,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -100,6 +102,62 @@ pub fn render_vertical_text(
|
|||||||
Ok(())
|
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(
|
pub fn stroke_paragraph_builder_group_from_text(
|
||||||
text_content: &TextContent,
|
text_content: &TextContent,
|
||||||
stroke: &Stroke,
|
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]
|
#[test]
|
||||||
fn horizontal_warichu_collapses_to_one_builder_position() {
|
fn horizontal_warichu_collapses_to_one_builder_position() {
|
||||||
init_state();
|
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.
|
/// Paint horizontal emphasis marks (圏点 / bouten) above their base glyphs.
|
||||||
/// The base paragraph retains its normal metrics; interlinear collision and
|
/// The base paragraph retains its normal metrics; interlinear collision and
|
||||||
/// automatic line-gap expansion remain a separate layout policy.
|
/// 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_height_override(true);
|
||||||
style.set_letter_spacing(0.0);
|
style.set_letter_spacing(0.0);
|
||||||
let mark_paragraph = warichu_mini_paragraph(&mark.to_string(), &style, f32::MAX);
|
let mark_paragraph = warichu_mini_paragraph(&mark.to_string(), &style, f32::MAX);
|
||||||
let mark_width = mark_paragraph.longest_line();
|
let mark_ink = mark_paragraph
|
||||||
let mark_height = mark_paragraph.height();
|
.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
|
for placement in placements
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|placement| placement.span == range.span && placement.mark == mark)
|
.filter(|placement| placement.span == range.span && placement.mark == mark)
|
||||||
@ -564,7 +594,9 @@ pub(crate) fn paint_horizontal_emphasis(
|
|||||||
} else {
|
} else {
|
||||||
0.0
|
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));
|
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)
|
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:
|
/// Paint ruby annotations for one horizontally laid-out paragraph. Draw-only:
|
||||||
/// base rects come from the already laid-out skparagraph
|
/// base rects come from the already laid-out skparagraph
|
||||||
/// (`get_rects_for_range`), the annotation is shaped at half the span size
|
/// (`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 run = &shaped[*run_index];
|
||||||
let (_, metrics) = run.font.metrics();
|
let (_, metrics) = run.font.metrics();
|
||||||
let baseline = match span.ruby_side {
|
let baseline = match span.ruby_side {
|
||||||
RubySide::Over => y + rect_box.rect.top() - metrics.descent,
|
RubySide::Over => {
|
||||||
RubySide::Under => y + rect_box.rect.bottom() - metrics.ascent,
|
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 mut builder = TextBlobBuilder::new();
|
||||||
let (out_glyphs, points) = builder.alloc_run_pos(&run.font, 1, None);
|
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);
|
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]
|
#[test]
|
||||||
fn justify_fills_non_last_columns() {
|
fn justify_fills_non_last_columns() {
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user