From 9fc8d6443c372cdde3379ac84b989f83aa14395e Mon Sep 17 00:00:00 2001 From: AlienTech Software Date: Wed, 11 Nov 2026 08:51:04 -0800 Subject: [PATCH 1/6] fix(ui-egui): RTL paragraph icon has its arrow right of the pilcrow The arrow head sits to the right of the pilcrow and points left, as in OneNote. A failing test first showed the arrow at x 3 and the pilcrow at x 23. The paired visual reference is updated to the same geometry. Co-Authored-By: Claude Haiku 5.5 --- crates/ui-egui/src/icons.rs | 58 ++++++++++++++++++++-------- crates/ui-egui/tests/icons_visual.rs | 22 +++-------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index e6f1f771..4fe4887a 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -532,26 +532,16 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) { fill(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), path(&[(3.0, 17.0), (7.0, 20.0), (3.0, 23.0)]), ], + // The pilcrow sits left of the arrow head, which points left at the right edge: `[ q< ]`. Icon::ParaRtl => vec![ fill( - &[ - (17.0, 4.0), - (15.0, 4.0), - (12.0, 5.0), - (10.0, 7.0), - (9.0, 9.0), - (9.0, 11.0), - (10.0, 13.0), - (12.0, 14.5), - (15.0, 15.0), - (17.0, 15.0), - ], + &[(11.0, 4.0), (9.0, 4.0), (6.0, 5.0), (4.0, 7.0), (3.0, 9.0), (3.0, 11.0), (4.0, 13.0), (6.0, 14.5), (9.0, 15.0), (11.0, 15.0)], color, ), - fill(&[(16.0, 4.0), (23.0, 4.0), (23.0, 6.0), (16.0, 6.0)], color), - fill(&[(16.0, 14.0), (18.0, 14.0), (18.0, 22.0), (16.0, 22.0)], color), - fill(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), - path(&[(7.0, 17.0), (3.0, 20.0), (7.0, 23.0)]), + fill(&[(10.0, 4.0), (17.0, 4.0), (17.0, 6.0), (10.0, 6.0)], color), + fill(&[(10.0, 14.0), (12.0, 14.0), (12.0, 22.0), (10.0, 22.0)], color), + fill(&[(15.0, 4.0), (17.0, 4.0), (17.0, 22.0), (15.0, 22.0)], color), + path(&[(23.0, 17.0), (19.0, 20.0), (23.0, 23.0)]), ], Icon::ParaAuto => vec![ fill( @@ -774,6 +764,42 @@ mod tests { out.textures_delta.clear(); } + /// The points of the filled shapes and of the open shapes `paint` draws for `icon` on a 24 + /// point square. + fn painted(icon: Icon) -> (Vec>, Vec>) { + let ctx = egui::Context::default(); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { + let painter = ui.painter().clone(); + paint(&painter, Rect::from_min_size(pos2(0.0, 0.0), egui::vec2(24.0, 24.0)), icon, Color32::WHITE); + }); + out.textures_delta.clear(); + let (mut filled, mut open) = (Vec::new(), Vec::new()); + for clipped in out.shapes { + if let egui::Shape::Path(path) = clipped.shape { + if path.fill.a() > 0 { + filled.push(path.points); + } else { + open.push(path.points); + } + } + } + (filled, open) + } + + /// SUP-005: the right-to-left paragraph icon has its arrow head to the right of the pilcrow, + /// pointing left, as in Word: `[ q< ]`. The pilcrow must not reach the arrow. + #[test] + fn the_rtl_paragraph_arrow_sits_right_of_the_pilcrow() { + let (filled, open) = painted(Icon::ParaRtl); + let pilcrow_right = filled.iter().flatten().map(|p| p.x).fold(f32::MIN, f32::max); + let arrow = open.iter().find(|pts| pts.len() == 3).expect("the arrow head is an open three-point path"); + let arrow_left = arrow.iter().map(|p| p.x).fold(f32::MAX, f32::min); + assert!(arrow_left > pilcrow_right, "the arrow starts right of the pilcrow: arrow {arrow_left}, pilcrow {pilcrow_right}"); + // The arrow head points left: its tip is the left-most point, and it sits on the middle line. + let tip = arrow.iter().min_by(|a, b| a.x.total_cmp(&b.x)).expect("a point"); + assert!((tip.y - 20.0).abs() < 0.01, "the tip is on the middle of the head: {tip:?}"); + } + /// The shading keeps a spine visible on a light colour and on a dark one. #[test] fn shading_moves_away_from_the_colour() { diff --git a/crates/ui-egui/tests/icons_visual.rs b/crates/ui-egui/tests/icons_visual.rs index dc9bc6d1..806ea920 100644 --- a/crates/ui-egui/tests/icons_visual.rs +++ b/crates/ui-egui/tests/icons_visual.rs @@ -498,26 +498,16 @@ fn reference(painter: &Painter, origin: Pos2, color: Color32, icon: Icon, nudge: poly(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), open(&[(3.0, 17.0), (7.0, 20.0), (3.0, 23.0)]), ], + // SUP-005: the arrow head is right of the pilcrow, pointing left: `[ q< ]`. Icon::ParaRtl => vec![ poly( - &[ - (17.0, 4.0), - (15.0, 4.0), - (12.0, 5.0), - (10.0, 7.0), - (9.0, 9.0), - (9.0, 11.0), - (10.0, 13.0), - (12.0, 14.5), - (15.0, 15.0), - (17.0, 15.0), - ], + &[(11.0, 4.0), (9.0, 4.0), (6.0, 5.0), (4.0, 7.0), (3.0, 9.0), (3.0, 11.0), (4.0, 13.0), (6.0, 14.5), (9.0, 15.0), (11.0, 15.0)], color, ), - poly(&[(16.0, 4.0), (23.0, 4.0), (23.0, 6.0), (16.0, 6.0)], color), - poly(&[(16.0, 14.0), (18.0, 14.0), (18.0, 22.0), (16.0, 22.0)], color), - poly(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), - open(&[(7.0, 17.0), (3.0, 20.0), (7.0, 23.0)]), + poly(&[(10.0, 4.0), (17.0, 4.0), (17.0, 6.0), (10.0, 6.0)], color), + poly(&[(10.0, 14.0), (12.0, 14.0), (12.0, 22.0), (10.0, 22.0)], color), + poly(&[(15.0, 4.0), (17.0, 4.0), (17.0, 22.0), (15.0, 22.0)], color), + open(&[(23.0, 17.0), (19.0, 20.0), (23.0, 23.0)]), ], Icon::ParaAuto => vec![ poly( From 8e8a7f4090a6b5cde62a60f8c90bea21ea72c1fd Mon Sep 17 00:00:00 2001 From: AlienTech Software Date: Wed, 11 Nov 2026 11:16:26 -0800 Subject: [PATCH 2/6] fix(ui-egui): raise the RTL arrow and space it from the pilcrow The owner asked for the arrow a little higher and spaced, as Word has it. The head moves up 5 points and its tip moves 1 point right, so there is a 3 point gap from the pilcrow stem. The paired reference matches. Co-Authored-By: Claude Haiku 5.5 --- crates/ui-egui/src/icons.rs | 6 ++++-- crates/ui-egui/tests/icons_visual.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 4fe4887a..773fb512 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -541,7 +541,7 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) { fill(&[(10.0, 4.0), (17.0, 4.0), (17.0, 6.0), (10.0, 6.0)], color), fill(&[(10.0, 14.0), (12.0, 14.0), (12.0, 22.0), (10.0, 22.0)], color), fill(&[(15.0, 4.0), (17.0, 4.0), (17.0, 22.0), (15.0, 22.0)], color), - path(&[(23.0, 17.0), (19.0, 20.0), (23.0, 23.0)]), + path(&[(23.0, 12.0), (20.0, 15.0), (23.0, 18.0)]), ], Icon::ParaAuto => vec![ fill( @@ -797,7 +797,9 @@ mod tests { assert!(arrow_left > pilcrow_right, "the arrow starts right of the pilcrow: arrow {arrow_left}, pilcrow {pilcrow_right}"); // The arrow head points left: its tip is the left-most point, and it sits on the middle line. let tip = arrow.iter().min_by(|a, b| a.x.total_cmp(&b.x)).expect("a point"); - assert!((tip.y - 20.0).abs() < 0.01, "the tip is on the middle of the head: {tip:?}"); + assert!((tip.y - 15.0).abs() < 0.01, "the tip is raised to the middle of the head at 15: {tip:?}"); + // A gap of at least 3 points between the pilcrow stem and the arrow tip, as Word has. + assert!(tip.x - pilcrow_right >= 3.0, "the arrow is spaced from the pilcrow: {tip:?}"); } /// The shading keeps a spine visible on a light colour and on a dark one. diff --git a/crates/ui-egui/tests/icons_visual.rs b/crates/ui-egui/tests/icons_visual.rs index 806ea920..2da358c9 100644 --- a/crates/ui-egui/tests/icons_visual.rs +++ b/crates/ui-egui/tests/icons_visual.rs @@ -507,7 +507,7 @@ fn reference(painter: &Painter, origin: Pos2, color: Color32, icon: Icon, nudge: poly(&[(10.0, 4.0), (17.0, 4.0), (17.0, 6.0), (10.0, 6.0)], color), poly(&[(10.0, 14.0), (12.0, 14.0), (12.0, 22.0), (10.0, 22.0)], color), poly(&[(15.0, 4.0), (17.0, 4.0), (17.0, 22.0), (15.0, 22.0)], color), - open(&[(23.0, 17.0), (19.0, 20.0), (23.0, 23.0)]), + open(&[(23.0, 12.0), (20.0, 15.0), (23.0, 18.0)]), ], Icon::ParaAuto => vec![ poly( From 71b6031b2e671702baa7133941987af336841265 Mon Sep 17 00:00:00 2001 From: AlienTech Software Date: Wed, 11 Nov 2026 12:04:42 -0800 Subject: [PATCH 3/6] fix(ui-egui): raise the LTR arrow to match the RTL one The LTR direction icon's arrow head moves up 5 points, level with the RTL icon's, so the pair reads as one set. A failing test came first. The paired reference matches. Co-Authored-By: Claude Haiku 5.5 --- crates/ui-egui/src/icons.rs | 15 ++++++++++++++- crates/ui-egui/tests/icons_visual.rs | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 773fb512..bf2324cc 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -530,7 +530,7 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) { fill(&[(16.0, 4.0), (23.0, 4.0), (23.0, 6.0), (16.0, 6.0)], color), fill(&[(16.0, 14.0), (18.0, 14.0), (18.0, 22.0), (16.0, 22.0)], color), fill(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), - path(&[(3.0, 17.0), (7.0, 20.0), (3.0, 23.0)]), + path(&[(3.0, 12.0), (7.0, 15.0), (3.0, 18.0)]), ], // The pilcrow sits left of the arrow head, which points left at the right edge: `[ q< ]`. Icon::ParaRtl => vec![ @@ -802,6 +802,19 @@ mod tests { assert!(tip.x - pilcrow_right >= 3.0, "the arrow is spaced from the pilcrow: {tip:?}"); } + /// The LTR and RTL direction icons carry their arrow heads at the same height, so the pair + /// reads as one set. + #[test] + fn the_ltr_and_rtl_arrow_heads_sit_at_the_same_height() { + let centre = |icon: Icon| { + let (_, open) = painted(icon); + let arrow = open.iter().find(|pts| pts.len() == 3).expect("an arrow head").clone(); + arrow.iter().map(|p| p.y).sum::() / 3.0 + }; + let (rtl, ltr) = (centre(Icon::ParaRtl), centre(Icon::ParaLtr)); + assert!((rtl - ltr).abs() < 0.01, "the LTR arrow head is level with the RTL one: rtl {rtl}, ltr {ltr}"); + } + /// The shading keeps a spine visible on a light colour and on a dark one. #[test] fn shading_moves_away_from_the_colour() { diff --git a/crates/ui-egui/tests/icons_visual.rs b/crates/ui-egui/tests/icons_visual.rs index 2da358c9..c709efbf 100644 --- a/crates/ui-egui/tests/icons_visual.rs +++ b/crates/ui-egui/tests/icons_visual.rs @@ -496,7 +496,7 @@ fn reference(painter: &Painter, origin: Pos2, color: Color32, icon: Icon, nudge: poly(&[(16.0, 4.0), (23.0, 4.0), (23.0, 6.0), (16.0, 6.0)], color), poly(&[(16.0, 14.0), (18.0, 14.0), (18.0, 22.0), (16.0, 22.0)], color), poly(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), - open(&[(3.0, 17.0), (7.0, 20.0), (3.0, 23.0)]), + open(&[(3.0, 12.0), (7.0, 15.0), (3.0, 18.0)]), ], // SUP-005: the arrow head is right of the pilcrow, pointing left: `[ q< ]`. Icon::ParaRtl => vec![ From fd9e89a2641a3f930fee9974c103cc67205a3f8b Mon Sep 17 00:00:00 2001 From: AlienTech Software Date: Wed, 11 Nov 2026 12:11:03 -0800 Subject: [PATCH 4/6] fix(ui-egui): make the LTR and RTL arrow heads the same size The LTR arrow head was 4 points wide and the RTL one 3. The LTR tip moves in by one point, so both heads are 3 by 6 points. A failing test came first, and the paired reference matches. Co-Authored-By: Claude Haiku 5.5 --- crates/ui-egui/src/icons.rs | 18 +++++++++++++++++- crates/ui-egui/tests/icons_visual.rs | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index bf2324cc..08ec5d32 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -530,7 +530,7 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) { fill(&[(16.0, 4.0), (23.0, 4.0), (23.0, 6.0), (16.0, 6.0)], color), fill(&[(16.0, 14.0), (18.0, 14.0), (18.0, 22.0), (16.0, 22.0)], color), fill(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), - path(&[(3.0, 12.0), (7.0, 15.0), (3.0, 18.0)]), + path(&[(3.0, 12.0), (6.0, 15.0), (3.0, 18.0)]), ], // The pilcrow sits left of the arrow head, which points left at the right edge: `[ q< ]`. Icon::ParaRtl => vec![ @@ -815,6 +815,22 @@ mod tests { assert!((rtl - ltr).abs() < 0.01, "the LTR arrow head is level with the RTL one: rtl {rtl}, ltr {ltr}"); } + /// The LTR and RTL direction icons carry arrow heads of the same size. + #[test] + fn the_ltr_and_rtl_arrow_heads_are_the_same_size() { + let size = |icon: Icon| { + let (_, open) = painted(icon); + let arrow = open.iter().find(|pts| pts.len() == 3).expect("an arrow head").clone(); + let xs = arrow.iter().map(|p| p.x); + let ys = arrow.iter().map(|p| p.y); + let width = xs.clone().fold(f32::MIN, f32::max) - xs.fold(f32::MAX, f32::min); + let height = ys.clone().fold(f32::MIN, f32::max) - ys.fold(f32::MAX, f32::min); + (width, height) + }; + let (rtl, ltr) = (size(Icon::ParaRtl), size(Icon::ParaLtr)); + assert!((rtl.0 - ltr.0).abs() < 0.01 && (rtl.1 - ltr.1).abs() < 0.01, "the arrow heads match in size: rtl {rtl:?}, ltr {ltr:?}"); + } + /// The shading keeps a spine visible on a light colour and on a dark one. #[test] fn shading_moves_away_from_the_colour() { diff --git a/crates/ui-egui/tests/icons_visual.rs b/crates/ui-egui/tests/icons_visual.rs index c709efbf..6fe5a263 100644 --- a/crates/ui-egui/tests/icons_visual.rs +++ b/crates/ui-egui/tests/icons_visual.rs @@ -496,7 +496,7 @@ fn reference(painter: &Painter, origin: Pos2, color: Color32, icon: Icon, nudge: poly(&[(16.0, 4.0), (23.0, 4.0), (23.0, 6.0), (16.0, 6.0)], color), poly(&[(16.0, 14.0), (18.0, 14.0), (18.0, 22.0), (16.0, 22.0)], color), poly(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), - open(&[(3.0, 12.0), (7.0, 15.0), (3.0, 18.0)]), + open(&[(3.0, 12.0), (6.0, 15.0), (3.0, 18.0)]), ], // SUP-005: the arrow head is right of the pilcrow, pointing left: `[ q< ]`. Icon::ParaRtl => vec![ From 14fc584911c3de9a1ab690eec1cf709bc21bdd8a Mon Sep 17 00:00:00 2001 From: AlienTech Software Date: Wed, 11 Nov 2026 12:38:56 -0800 Subject: [PATCH 5/6] fix(ui-egui): smaller pilcrow and larger arrow heads on direction icons The owner asked for a larger arrow head and a smaller pilcrow. The pilcrow is scaled to 85 percent of its size and the arrow head is 4 by 8 points. The LTR icon mirrors the RTL one. Tests were written first, and the paired reference matches. Co-Authored-By: Claude Haiku 5.5 --- crates/ui-egui/src/icons.rs | 61 +++++++++++++++++++--------- crates/ui-egui/tests/icons_visual.rs | 49 +++++++++++++--------- 2 files changed, 71 insertions(+), 39 deletions(-) diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 08ec5d32..a50f6edc 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -514,34 +514,45 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) { Icon::ParaLtr => vec![ fill( &[ - (17.0, 4.0), - (15.0, 4.0), - (12.0, 5.0), - (10.0, 7.0), - (9.0, 9.0), - (9.0, 11.0), - (10.0, 13.0), - (12.0, 14.5), - (15.0, 15.0), - (17.0, 15.0), + (14.2, 4.0), + (15.9, 4.0), + (18.45, 4.85), + (20.15, 6.55), + (21.0, 8.25), + (21.0, 9.95), + (20.15, 11.65), + (18.45, 12.92), + (15.9, 13.35), + (14.2, 13.35), ], color, ), - fill(&[(16.0, 4.0), (23.0, 4.0), (23.0, 6.0), (16.0, 6.0)], color), - fill(&[(16.0, 14.0), (18.0, 14.0), (18.0, 22.0), (16.0, 22.0)], color), - fill(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), - path(&[(3.0, 12.0), (6.0, 15.0), (3.0, 18.0)]), + fill(&[(15.05, 4.0), (9.1, 4.0), (9.1, 5.7), (15.05, 5.7)], color), + fill(&[(15.05, 12.5), (13.35, 12.5), (13.35, 19.3), (15.05, 19.3)], color), + fill(&[(10.8, 4.0), (9.1, 4.0), (9.1, 19.3), (10.8, 19.3)], color), + path(&[(1.0, 9.0), (5.0, 13.0), (1.0, 17.0)]), ], // The pilcrow sits left of the arrow head, which points left at the right edge: `[ q< ]`. Icon::ParaRtl => vec![ fill( - &[(11.0, 4.0), (9.0, 4.0), (6.0, 5.0), (4.0, 7.0), (3.0, 9.0), (3.0, 11.0), (4.0, 13.0), (6.0, 14.5), (9.0, 15.0), (11.0, 15.0)], + &[ + (9.8, 4.0), + (8.1, 4.0), + (5.55, 4.85), + (3.85, 6.55), + (3.0, 8.25), + (3.0, 9.95), + (3.85, 11.65), + (5.55, 12.92), + (8.1, 13.35), + (9.8, 13.35), + ], color, ), - fill(&[(10.0, 4.0), (17.0, 4.0), (17.0, 6.0), (10.0, 6.0)], color), - fill(&[(10.0, 14.0), (12.0, 14.0), (12.0, 22.0), (10.0, 22.0)], color), - fill(&[(15.0, 4.0), (17.0, 4.0), (17.0, 22.0), (15.0, 22.0)], color), - path(&[(23.0, 12.0), (20.0, 15.0), (23.0, 18.0)]), + fill(&[(8.95, 4.0), (14.9, 4.0), (14.9, 5.7), (8.95, 5.7)], color), + fill(&[(8.95, 12.5), (10.65, 12.5), (10.65, 19.3), (8.95, 19.3)], color), + fill(&[(13.2, 4.0), (14.9, 4.0), (14.9, 19.3), (13.2, 19.3)], color), + path(&[(23.0, 9.0), (19.0, 13.0), (23.0, 17.0)]), ], Icon::ParaAuto => vec![ fill( @@ -797,7 +808,7 @@ mod tests { assert!(arrow_left > pilcrow_right, "the arrow starts right of the pilcrow: arrow {arrow_left}, pilcrow {pilcrow_right}"); // The arrow head points left: its tip is the left-most point, and it sits on the middle line. let tip = arrow.iter().min_by(|a, b| a.x.total_cmp(&b.x)).expect("a point"); - assert!((tip.y - 15.0).abs() < 0.01, "the tip is raised to the middle of the head at 15: {tip:?}"); + assert!((tip.y - 13.0).abs() < 0.01, "the tip is on the middle of the head at 13: {tip:?}"); // A gap of at least 3 points between the pilcrow stem and the arrow tip, as Word has. assert!(tip.x - pilcrow_right >= 3.0, "the arrow is spaced from the pilcrow: {tip:?}"); } @@ -829,6 +840,16 @@ mod tests { }; let (rtl, ltr) = (size(Icon::ParaRtl), size(Icon::ParaLtr)); assert!((rtl.0 - ltr.0).abs() < 0.01 && (rtl.1 - ltr.1).abs() < 0.01, "the arrow heads match in size: rtl {rtl:?}, ltr {ltr:?}"); + assert!((rtl.0 - 4.0).abs() < 0.01 && (rtl.1 - 8.0).abs() < 0.01, "the arrow head is 4 by 8 points: {rtl:?}"); + } + + /// The pilcrow is 15 points tall or less, so the arrow head has room (owner, 2026-11-11). + #[test] + fn the_pilcrow_is_smaller_than_before() { + let (filled, _) = painted(Icon::ParaRtl); + let ys = filled.iter().flatten().map(|p| p.y); + let height = ys.clone().fold(f32::MIN, f32::max) - ys.fold(f32::MAX, f32::min); + assert!(height <= 16.0, "the pilcrow is at most 16 points tall: {height}"); } /// The shading keeps a spine visible on a light colour and on a dark one. diff --git a/crates/ui-egui/tests/icons_visual.rs b/crates/ui-egui/tests/icons_visual.rs index 6fe5a263..784f4a33 100644 --- a/crates/ui-egui/tests/icons_visual.rs +++ b/crates/ui-egui/tests/icons_visual.rs @@ -480,34 +480,45 @@ fn reference(painter: &Painter, origin: Pos2, color: Color32, icon: Icon, nudge: Icon::ParaLtr => vec![ poly( &[ - (17.0, 4.0), - (15.0, 4.0), - (12.0, 5.0), - (10.0, 7.0), - (9.0, 9.0), - (9.0, 11.0), - (10.0, 13.0), - (12.0, 14.5), - (15.0, 15.0), - (17.0, 15.0), + (14.2, 4.0), + (15.9, 4.0), + (18.45, 4.85), + (20.15, 6.55), + (21.0, 8.25), + (21.0, 9.95), + (20.15, 11.65), + (18.45, 12.92), + (15.9, 13.35), + (14.2, 13.35), ], color, ), - poly(&[(16.0, 4.0), (23.0, 4.0), (23.0, 6.0), (16.0, 6.0)], color), - poly(&[(16.0, 14.0), (18.0, 14.0), (18.0, 22.0), (16.0, 22.0)], color), - poly(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), - open(&[(3.0, 12.0), (6.0, 15.0), (3.0, 18.0)]), + poly(&[(15.05, 4.0), (9.1, 4.0), (9.1, 5.7), (15.05, 5.7)], color), + poly(&[(15.05, 12.5), (13.35, 12.5), (13.35, 19.3), (15.05, 19.3)], color), + poly(&[(10.8, 4.0), (9.1, 4.0), (9.1, 19.3), (10.8, 19.3)], color), + open(&[(1.0, 9.0), (5.0, 13.0), (1.0, 17.0)]), ], // SUP-005: the arrow head is right of the pilcrow, pointing left: `[ q< ]`. Icon::ParaRtl => vec![ poly( - &[(11.0, 4.0), (9.0, 4.0), (6.0, 5.0), (4.0, 7.0), (3.0, 9.0), (3.0, 11.0), (4.0, 13.0), (6.0, 14.5), (9.0, 15.0), (11.0, 15.0)], + &[ + (9.8, 4.0), + (8.1, 4.0), + (5.55, 4.85), + (3.85, 6.55), + (3.0, 8.25), + (3.0, 9.95), + (3.85, 11.65), + (5.55, 12.92), + (8.1, 13.35), + (9.8, 13.35), + ], color, ), - poly(&[(10.0, 4.0), (17.0, 4.0), (17.0, 6.0), (10.0, 6.0)], color), - poly(&[(10.0, 14.0), (12.0, 14.0), (12.0, 22.0), (10.0, 22.0)], color), - poly(&[(15.0, 4.0), (17.0, 4.0), (17.0, 22.0), (15.0, 22.0)], color), - open(&[(23.0, 12.0), (20.0, 15.0), (23.0, 18.0)]), + poly(&[(8.95, 4.0), (14.9, 4.0), (14.9, 5.7), (8.95, 5.7)], color), + poly(&[(8.95, 12.5), (10.65, 12.5), (10.65, 19.3), (8.95, 19.3)], color), + poly(&[(13.2, 4.0), (14.9, 4.0), (14.9, 19.3), (13.2, 19.3)], color), + open(&[(23.0, 9.0), (19.0, 13.0), (23.0, 17.0)]), ], Icon::ParaAuto => vec![ poly( From 739d0c8edddae48a8067fda1836a0a494e962266 Mon Sep 17 00:00:00 2001 From: AlienTech Software Date: Wed, 11 Nov 2026 12:45:55 -0800 Subject: [PATCH 6/6] fix(ui-egui): keep the pilcrow; the arrow alone shows the direction The owner asked for the pilcrow to keep its size and shape. The arrow head's position and direction now carry the paragraph direction. The RTL pilcrow moves left to leave room for the arrow, and the LTR pilcrow keeps its original place. A test checks that the pilcrow is not mirrored. Co-Authored-By: Claude Haiku 5.5 --- crates/ui-egui/src/icons.rs | 78 +++++++++++++++++----------- crates/ui-egui/tests/icons_visual.rs | 45 ++++++---------- 2 files changed, 64 insertions(+), 59 deletions(-) diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index a50f6edc..337ec435 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -514,44 +514,33 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) { Icon::ParaLtr => vec![ fill( &[ - (14.2, 4.0), - (15.9, 4.0), - (18.45, 4.85), - (20.15, 6.55), - (21.0, 8.25), - (21.0, 9.95), - (20.15, 11.65), - (18.45, 12.92), - (15.9, 13.35), - (14.2, 13.35), + (17.0, 4.0), + (15.0, 4.0), + (12.0, 5.0), + (10.0, 7.0), + (9.0, 9.0), + (9.0, 11.0), + (10.0, 13.0), + (12.0, 14.5), + (15.0, 15.0), + (17.0, 15.0), ], color, ), - fill(&[(15.05, 4.0), (9.1, 4.0), (9.1, 5.7), (15.05, 5.7)], color), - fill(&[(15.05, 12.5), (13.35, 12.5), (13.35, 19.3), (15.05, 19.3)], color), - fill(&[(10.8, 4.0), (9.1, 4.0), (9.1, 19.3), (10.8, 19.3)], color), + fill(&[(16.0, 4.0), (23.0, 4.0), (23.0, 6.0), (16.0, 6.0)], color), + fill(&[(16.0, 14.0), (18.0, 14.0), (18.0, 22.0), (16.0, 22.0)], color), + fill(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), path(&[(1.0, 9.0), (5.0, 13.0), (1.0, 17.0)]), ], // The pilcrow sits left of the arrow head, which points left at the right edge: `[ q< ]`. Icon::ParaRtl => vec![ fill( - &[ - (9.8, 4.0), - (8.1, 4.0), - (5.55, 4.85), - (3.85, 6.55), - (3.0, 8.25), - (3.0, 9.95), - (3.85, 11.65), - (5.55, 12.92), - (8.1, 13.35), - (9.8, 13.35), - ], + &[(10.0, 4.0), (8.0, 4.0), (5.0, 5.0), (3.0, 7.0), (2.0, 9.0), (2.0, 11.0), (3.0, 13.0), (5.0, 14.5), (8.0, 15.0), (10.0, 15.0)], color, ), - fill(&[(8.95, 4.0), (14.9, 4.0), (14.9, 5.7), (8.95, 5.7)], color), - fill(&[(8.95, 12.5), (10.65, 12.5), (10.65, 19.3), (8.95, 19.3)], color), - fill(&[(13.2, 4.0), (14.9, 4.0), (14.9, 19.3), (13.2, 19.3)], color), + fill(&[(9.0, 4.0), (16.0, 4.0), (16.0, 6.0), (9.0, 6.0)], color), + fill(&[(9.0, 14.0), (11.0, 14.0), (11.0, 22.0), (9.0, 22.0)], color), + fill(&[(14.0, 4.0), (16.0, 4.0), (16.0, 22.0), (14.0, 22.0)], color), path(&[(23.0, 9.0), (19.0, 13.0), (23.0, 17.0)]), ], Icon::ParaAuto => vec![ @@ -843,13 +832,40 @@ mod tests { assert!((rtl.0 - 4.0).abs() < 0.01 && (rtl.1 - 8.0).abs() < 0.01, "the arrow head is 4 by 8 points: {rtl:?}"); } - /// The pilcrow is 15 points tall or less, so the arrow head has room (owner, 2026-11-11). + /// The pilcrow keeps its original 18 point height: only the arrow shows the direction (owner, 2026-11-11). #[test] - fn the_pilcrow_is_smaller_than_before() { + fn the_pilcrow_keeps_its_size() { let (filled, _) = painted(Icon::ParaRtl); let ys = filled.iter().flatten().map(|p| p.y); let height = ys.clone().fold(f32::MIN, f32::max) - ys.fold(f32::MAX, f32::min); - assert!(height <= 16.0, "the pilcrow is at most 16 points tall: {height}"); + assert!((height - 18.0).abs() < 0.01, "the pilcrow is 18 points tall: {height}"); + } + + /// The pilcrow keeps its normal shape in both direction icons: its stems stand on the right of + /// its bowl. A mirrored pilcrow is wrong (owner, 2026-11-11). + #[test] + fn the_pilcrow_is_not_mirrored_in_either_direction() { + let centre_x = |pts: &[Pos2]| { + let xs = pts.iter().map(|p| p.x); + (xs.clone().fold(f32::MAX, f32::min) + xs.fold(f32::MIN, f32::max)) / 2.0 + }; + for icon in [Icon::ParaRtl, Icon::ParaLtr] { + let (filled, _) = painted(icon); + let bowl = filled.iter().max_by_key(|pts| pts.len()).expect("the bowl").as_slice(); + let stems: Vec<&Vec> = filled + .iter() + .filter(|pts| { + pts.len() == 4 && { + let ys = pts.iter().map(|p| p.y); + ys.clone().fold(f32::MIN, f32::max) - ys.fold(f32::MAX, f32::min) > 10.0 + } + }) + .collect(); + assert!(!stems.is_empty(), "{icon:?} has its stems"); + for stem in stems { + assert!(centre_x(stem.as_slice()) > centre_x(bowl), "{icon:?}: the stems stand right of the bowl"); + } + } } /// The shading keeps a spine visible on a light colour and on a dark one. diff --git a/crates/ui-egui/tests/icons_visual.rs b/crates/ui-egui/tests/icons_visual.rs index 784f4a33..fb6a0404 100644 --- a/crates/ui-egui/tests/icons_visual.rs +++ b/crates/ui-egui/tests/icons_visual.rs @@ -480,44 +480,33 @@ fn reference(painter: &Painter, origin: Pos2, color: Color32, icon: Icon, nudge: Icon::ParaLtr => vec![ poly( &[ - (14.2, 4.0), - (15.9, 4.0), - (18.45, 4.85), - (20.15, 6.55), - (21.0, 8.25), - (21.0, 9.95), - (20.15, 11.65), - (18.45, 12.92), - (15.9, 13.35), - (14.2, 13.35), + (17.0, 4.0), + (15.0, 4.0), + (12.0, 5.0), + (10.0, 7.0), + (9.0, 9.0), + (9.0, 11.0), + (10.0, 13.0), + (12.0, 14.5), + (15.0, 15.0), + (17.0, 15.0), ], color, ), - poly(&[(15.05, 4.0), (9.1, 4.0), (9.1, 5.7), (15.05, 5.7)], color), - poly(&[(15.05, 12.5), (13.35, 12.5), (13.35, 19.3), (15.05, 19.3)], color), - poly(&[(10.8, 4.0), (9.1, 4.0), (9.1, 19.3), (10.8, 19.3)], color), + poly(&[(16.0, 4.0), (23.0, 4.0), (23.0, 6.0), (16.0, 6.0)], color), + poly(&[(16.0, 14.0), (18.0, 14.0), (18.0, 22.0), (16.0, 22.0)], color), + poly(&[(21.0, 4.0), (23.0, 4.0), (23.0, 22.0), (21.0, 22.0)], color), open(&[(1.0, 9.0), (5.0, 13.0), (1.0, 17.0)]), ], // SUP-005: the arrow head is right of the pilcrow, pointing left: `[ q< ]`. Icon::ParaRtl => vec![ poly( - &[ - (9.8, 4.0), - (8.1, 4.0), - (5.55, 4.85), - (3.85, 6.55), - (3.0, 8.25), - (3.0, 9.95), - (3.85, 11.65), - (5.55, 12.92), - (8.1, 13.35), - (9.8, 13.35), - ], + &[(10.0, 4.0), (8.0, 4.0), (5.0, 5.0), (3.0, 7.0), (2.0, 9.0), (2.0, 11.0), (3.0, 13.0), (5.0, 14.5), (8.0, 15.0), (10.0, 15.0)], color, ), - poly(&[(8.95, 4.0), (14.9, 4.0), (14.9, 5.7), (8.95, 5.7)], color), - poly(&[(8.95, 12.5), (10.65, 12.5), (10.65, 19.3), (8.95, 19.3)], color), - poly(&[(13.2, 4.0), (14.9, 4.0), (14.9, 19.3), (13.2, 19.3)], color), + poly(&[(9.0, 4.0), (16.0, 4.0), (16.0, 6.0), (9.0, 6.0)], color), + poly(&[(9.0, 14.0), (11.0, 14.0), (11.0, 22.0), (9.0, 22.0)], color), + poly(&[(14.0, 4.0), (16.0, 4.0), (16.0, 22.0), (14.0, 22.0)], color), open(&[(23.0, 9.0), (19.0, 13.0), (23.0, 17.0)]), ], Icon::ParaAuto => vec![