mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-25 10:56:48 -06:00
- Frame and section name labels follow a rotated frame's top edge as shown (turned with it,
cut to the edge's length; a mirrored frame's label stays upright, a frame turned past 90
degrees takes its label along). Component icons, section pills, eyes and status badges turn too.
- The canvas publishes each label's area to the engine (ToolState::labels): with the Move and
Scale tools, hovering a label highlights its frame and pressing it selects and drags the frame
(one undo step); labels of locked, hidden, deleted or other pages' frames do nothing.
- Right-to-left paragraphs mirror lists and indents: bullets and numbers (".1") on the right,
wrapped lines hanging from the right, the paragraph indent on the right; trailing spaces of a
right-to-left line hang outside it instead of pushing its text out of a right-aligned box.
Canvas, PNG, PDF and SVG all draw the shared layout.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
64 lines
3.1 KiB
Rust
64 lines
3.1 KiB
Rust
//! List markers of right-to-left paragraphs on the right, in what the canvas and PNG export draw
|
|
//! and in the outlines the SVG export writes: the bullet ends at the box's right edge with a clear
|
|
//! gap between it and the text on its left. (The PDF export draws the same laid-out glyphs.)
|
|
|
|
use figcraft_doc::{Document, ListStyle, NodeId, NodeKind, TextAlignH, TextAutoResize, TextData};
|
|
|
|
use super::*;
|
|
|
|
const W: f64 = 300.0;
|
|
|
|
/// A 300-wide right-aligned bulleted Hebrew paragraph at the origin.
|
|
fn doc() -> (Document, NodeId, TextData) {
|
|
let mut td = TextData::new("\u{05E9}\u{05DC}\u{05D5}\u{05DD} \u{05E2}\u{05D5}\u{05DC}\u{05DD}");
|
|
td.style.font_size = 20.0;
|
|
td.list = ListStyle::Bulleted;
|
|
td.align_h = TextAlignH::Right;
|
|
td.auto_resize = TextAutoResize::Height;
|
|
let mut d = Document::new("rtl");
|
|
let p = d.first_page().unwrap();
|
|
let id = d.create(p, NodeKind::Text(td.clone()), "T", 0.0, 0.0, W, 40.0).unwrap();
|
|
(d, id, td)
|
|
}
|
|
|
|
/// The bullet's span and where the text ends, from the layout; None without Hebrew fonts.
|
|
fn spans(td: &TextData) -> Option<((f64, f64), f64)> {
|
|
static ONCE: std::sync::Once = std::sync::Once::new();
|
|
ONCE.call_once(figcraft_text::scan_system_fonts);
|
|
let l = figcraft_text::layout(td, Some(W), None);
|
|
if l.glyphs.is_empty() || l.glyphs.iter().any(|g| g.gid == 0) {
|
|
eprintln!("no font for Hebrew; skipping");
|
|
return None;
|
|
}
|
|
let m = l.glyphs.iter().find(|g| g.char_start == g.char_end)?;
|
|
let text_right = l.glyphs.iter().filter(|g| g.char_end > g.char_start).map(|g| g.x + g.advance).fold(f64::MIN, f64::max);
|
|
Some(((m.x, m.x + m.advance), text_right))
|
|
}
|
|
|
|
#[test]
|
|
fn the_bullet_of_a_right_to_left_paragraph_draws_on_the_right() {
|
|
let (d, id, td) = doc();
|
|
let Some(((m0, m1), text_right)) = spans(&td) else { return };
|
|
assert!(m0 > text_right + 8.0 && (m1 - W).abs() < 0.5, "{m0}..{m1}, text to {text_right}");
|
|
let mut pix = Pixmap::new(W as u32, 40).unwrap();
|
|
render_nodes_into(&d, &[id], &View { origin: (0.0, 0.0), scale: 1.0, width: W as u32, height: 40 }, &mut pix);
|
|
let inked = |x: u32| (0..40).any(|y| pix.pixel(x, y).is_some_and(|p| p.alpha() > 64));
|
|
assert!((m0.ceil() as u32..m1.floor() as u32).any(inked), "the bullet is drawn");
|
|
assert!(!(text_right.ceil() as u32 + 2..m0.floor() as u32 - 1).any(inked), "nothing between the text and the bullet");
|
|
assert!((0..text_right as u32).any(inked), "the text is drawn");
|
|
// The outline the SVG export writes: the same gap.
|
|
let (path, _) = draw::node_outline(&d, d.node(id).unwrap()).unwrap();
|
|
let xs: Vec<f64> = path
|
|
.elements()
|
|
.iter()
|
|
.flat_map(|e| match e {
|
|
figcraft_geom::kurbo::PathEl::MoveTo(p) | figcraft_geom::kurbo::PathEl::LineTo(p) => vec![p.x],
|
|
figcraft_geom::kurbo::PathEl::QuadTo(a, p) => vec![a.x, p.x],
|
|
figcraft_geom::kurbo::PathEl::CurveTo(a, b, p) => vec![a.x, b.x, p.x],
|
|
figcraft_geom::kurbo::PathEl::ClosePath => vec![],
|
|
})
|
|
.collect();
|
|
assert!(xs.iter().any(|x| (m0..=m1).contains(x)));
|
|
assert!(!xs.iter().any(|x| *x > text_right + 1.0 && *x < m0 - 1.0), "{xs:?}");
|
|
}
|