From 2683b8dfed9d3eb6bd00640aafedf74862115b26 Mon Sep 17 00:00:00 2001 From: lillith <35337584+0xlillith@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:58:49 +0200 Subject: [PATCH 1/7] fix: allow horizontal and vertical alignment of sketch points --- crates/ui-egui/src/tools.rs | 29 +++++++- crates/ui-egui/tests/point_alignment.rs | 90 +++++++++++++++++++++++++ docs/book/src/guide/sketching.md | 4 ++ 3 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 crates/ui-egui/tests/point_alignment.rs diff --git a/crates/ui-egui/src/tools.rs b/crates/ui-egui/src/tools.rs index adbefac1..a2a68433 100644 --- a/crates/ui-egui/src/tools.rs +++ b/crates/ui-egui/src/tools.rs @@ -14,6 +14,8 @@ pub enum Kind { Draw(usize), /// Pick sketch entities; `n` picks run the command. Pick(usize), + /// Pick one straight line, or two distinct points to align. + HorizontalVertical, Dimension, /// A tool from `sketch_tools` (it handles its own clicks). Ext, @@ -56,7 +58,7 @@ impl Tool { "sketch.slot.overall" => ("sketch.slot.overall", Kind::Draw(3)), "sketch.point" => ("sketch.point", Kind::Draw(1)), "sketch.dimension" => ("sketch.dimension", Kind::Dimension), - "sketch.constraint.horizontal_vertical" => ("sketch.constraint.horizontal_vertical", Kind::Pick(1)), + "sketch.constraint.horizontal_vertical" => ("sketch.constraint.horizontal_vertical", Kind::HorizontalVertical), "sketch.constraint.fix" => ("sketch.constraint.fix", Kind::Pick(1)), "sketch.constraint.coincident" => ("sketch.constraint.coincident", Kind::Pick(2)), "sketch.constraint.tangent" => ("sketch.constraint.tangent", Kind::Pick(2)), @@ -93,6 +95,7 @@ pub fn hint(id: &str) -> String { "sketch.arc.three_point" => "Arc: click start, end, then a point on the arc".into(), "sketch.arc.center_point" => "Arc: click centre and start, then click end or type a signed sweep".into(), "sketch.dimension" => "Dimension: pick one or two entities, then click to place the value".into(), + "sketch.constraint.horizontal_vertical" => "Horizontal/Vertical: pick a line or two points to align".into(), "sketch.slot.center_to_center" | "sketch.slot.overall" => "Slot: click both ends, then the width".into(), _ if id.starts_with("sketch.constraint.") => "Constraint: pick the sketch entities".into(), _ => crate::sketch_tools::hint(id).unwrap_or_else(|| "Click in the sketch".into()), @@ -143,6 +146,29 @@ pub fn on_click(app: &mut SolveApp, proj: &Proj, pos: Pos2) { return; } } + Kind::HorizontalVertical => { + let hits = pick(app, proj, pos); + let hit = hits.iter().find(|hit| match hit { + Hit::SketchPoint { sketch, .. } | Hit::SketchCurve { sketch, .. } => Some(*sketch) == app.session.active_sketch, + _ => false, + }); + match hit { + Some(Hit::SketchPoint { id, .. }) => { + if let Some(first) = tool.picks.first() { + if first != id { + let _ = app.run(tool.cmd, json!({"points": [first, id]})); + tool.picks.clear(); + } + } else { + tool.picks.push(id.clone()); + } + } + Some(Hit::SketchCurve { id, straight: true, .. }) if tool.picks.is_empty() => { + let _ = app.run(tool.cmd, json!({"line": id})); + } + _ => {} + } + } Kind::Pick(n) => { let hits = pick(app, proj, pos); if let Some(id) = hits.iter().find_map(|h| match h { @@ -292,7 +318,6 @@ fn run_shape(app: &mut SolveApp, t: &Tool) -> Result, String> { fn run_pick(app: &mut SolveApp, t: &Tool) { let k = &t.picks; let params = match t.cmd { - "sketch.constraint.horizontal_vertical" => json!({"line": k[0]}), "sketch.constraint.fix" => json!({"entity": k[0]}), "sketch.constraint.coincident" => json!({"a": k[0], "b": k[1]}), "sketch.constraint.midpoint" => json!({"point": k[0], "line": k[1]}), diff --git a/crates/ui-egui/tests/point_alignment.rs b/crates/ui-egui/tests/point_alignment.rs new file mode 100644 index 00000000..45c6e126 --- /dev/null +++ b/crates/ui-egui/tests/point_alignment.rs @@ -0,0 +1,90 @@ +//! Horizontal/Vertical accepts either one line or two distinct sketch points. + +use serde_json::json; +use solvecraft_engine::sketch::ConstraintKind; +use solvecraft_ui_egui::scenario::Harness; + +#[test] +fn horizontal_vertical_aligns_origin_and_circle_center() { + for (center, horizontal) in [([20.0, 3.0], true), ([3.0, 20.0], false)] { + for origin_first in [true, false] { + let mut h = Harness::new(); + h.run(&json!([ + {"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}}, + {"call":"engine.execute","params":{"command":"sketch.circle.center","params":{"center":center,"radius":5}}}, + {"call":"ui.view","params":{"view":"top"}}, + {"start":"sketch.constraint.horizontal_vertical"} + ])) + .unwrap(); + let (first, second) = if origin_first { ([0.0, 0.0], center) } else { (center, [0.0, 0.0]) }; + let undo = h.app.session.undo.len(); + h.run(&json!([{"click":{"sketch":first}}, {"expect":{"errors":0}}])).unwrap(); + assert_eq!(h.app.tool.as_ref().unwrap().picks.len(), 1); + assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error)); + assert_eq!(h.app.session.undo.len(), undo); + // Repeating the first point must not complete a self-constraint. + h.run(&json!([{"click":{"sketch":first}}, {"click":{"sketch":second}}, {"expect":{"errors":0}}])).unwrap(); + let aligned = |h: &Harness| { + let sk = &h.app.session.model.state().sketches[0].sketch; + let p = sk.point(sk.resolve_point("c1.center").unwrap()).unwrap(); + if horizontal { p.y } else { p.x } + }; + assert!(aligned(&h).abs() < 1e-6); + let sk = &h.app.session.model.state().sketches[0].sketch; + assert!(sk.constraints.iter().any(|c| if horizontal { + matches!(c.kind, ConstraintKind::HorizontalPoints { .. }) + } else { + matches!(c.kind, ConstraintKind::VerticalPoints { .. }) + })); + assert_eq!(h.app.session.undo.len(), undo + 1); + assert!(h.app.tool.as_ref().unwrap().picks.is_empty()); + h.run(&json!([{"call":"engine.execute","params":{"command":"edit.undo","params":{}}}])).unwrap(); + assert!((aligned(&h) - 3.0).abs() < 1e-6); + h.run(&json!([{"call":"engine.execute","params":{"command":"edit.redo","params":{}}}])).unwrap(); + assert!(aligned(&h).abs() < 1e-6); + } + } +} + +#[test] +fn horizontal_vertical_still_accepts_one_line() { + let mut h = Harness::new(); + h.run(&json!([ + {"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}}, + {"call":"engine.execute","params":{"command":"sketch.line","params":{"points":[[10,10],[40,13]],"infer":false}}}, + {"call":"ui.view","params":{"view":"top"}}, + {"start":"sketch.constraint.horizontal_vertical"}, + {"click":{"sketch":[25,11.5]}}, + {"expect":{"errors":0}} + ])) + .unwrap(); + let sk = &h.app.session.model.state().sketches[0].sketch; + assert!(sk.constraints.iter().any(|c| matches!(c.kind, ConstraintKind::Horizontal { .. }))); + let a = sk.point(sk.resolve_point("l1.start").unwrap()).unwrap(); + let b = sk.point(sk.resolve_point("l1.end").unwrap()).unwrap(); + assert!((a.y - b.y).abs() < 1e-6); +} + +#[test] +fn horizontal_vertical_ignores_curves_and_cancels_a_pending_point() { + let mut h = Harness::new(); + h.run(&json!([ + {"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}}, + {"call":"engine.execute","params":{"command":"sketch.circle.center","params":{"center":[20,3],"radius":5}}}, + {"call":"ui.view","params":{"view":"top"}}, + {"start":"sketch.constraint.horizontal_vertical"}, + {"click":{"sketch":[25,3]}} + ])) + .unwrap(); + assert!(h.app.tool.as_ref().unwrap().picks.is_empty()); + assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error)); + let undo = h.app.session.undo.len(); + h.run(&json!([{"click":{"sketch":[0,0]}}, {"click":{"sketch":[25,3]}}])).unwrap(); + assert_eq!(h.app.tool.as_ref().unwrap().picks.len(), 1); + h.run(&json!([{"key":"Escape"}])).unwrap(); + assert!(h.app.tool.is_none()); + assert_eq!(h.app.session.undo.len(), undo); + let sk = &h.app.session.model.state().sketches[0].sketch; + let p = sk.point(sk.resolve_point("c1.center").unwrap()).unwrap(); + assert!((p.y - 3.0).abs() < 1e-6); +} diff --git a/docs/book/src/guide/sketching.md b/docs/book/src/guide/sketching.md index de24f0d4..38b908ab 100644 --- a/docs/book/src/guide/sketching.md +++ b/docs/book/src/guide/sketching.md @@ -54,6 +54,10 @@ fresh link once the source is available; their stored point values are preserved ## Constraints and dimensions +**Horizontal/Vertical** accepts a click on a line or clicks on two distinct sketch points, +including the origin, endpoints and circle centres. For two points, it chooses horizontal +or vertical alignment from their current positions. Esc cancels a pending first point. + The solver is SolveCraft's own (damped Gauss–Newton with degree-of-freedom analysis). Constraints: coincident, horizontal/vertical, parallel, perpendicular, tangent, equal, concentric, collinear, midpoint, symmetry, fix, curvature (G2) and polygon. **Sketch Dimension** (D) adds linear, From 5cc65fe46bfbac10b1210e1416f4b8b5126b9c31 Mon Sep 17 00:00:00 2001 From: lillith <35337584+0xlillith@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:09:01 +0200 Subject: [PATCH 2/7] fix: apply curve-pair constraints to sketch preselection --- crates/ui-egui/src/lib.rs | 21 +++ .../ui-egui/tests/constraint_preselection.rs | 123 ++++++++++++++++++ docs/book/src/guide/sketching.md | 5 + 3 files changed, 149 insertions(+) create mode 100644 crates/ui-egui/tests/constraint_preselection.rs diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 11577212..ff1a316c 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -517,6 +517,27 @@ impl SolveApp { self.last_command = Some((spec.id.to_string(), spec.label.to_string())); } if let Some(mut t) = tools::Tool::for_command(id) { + // Curve-pair constraints can consume an exact preselection. Leave ambiguous + // selections to the ordinary pick tool; never silently constrain a subset. + if matches!( + id, + "sketch.constraint.parallel" + | "sketch.constraint.perpendicular" + | "sketch.constraint.equal" + | "sketch.constraint.tangent" + | "sketch.constraint.concentric" + | "sketch.constraint.collinear" + ) && let [solvecraft_engine::Sel::SketchCurve { id: a }, solvecraft_engine::Sel::SketchCurve { id: b }] = + self.session.selection.as_slice() + && a != b + { + let params = json!({ "a": a, "b": b }); + if self.run(id, params).is_ok() { + let _ = self.session.execute("select.clear", &json!({})); + self.set_status("Constraint applied", false); + } + return; + } if id == "sketch.dimension" { t.picks = self .session diff --git a/crates/ui-egui/tests/constraint_preselection.rs b/crates/ui-egui/tests/constraint_preselection.rs new file mode 100644 index 00000000..215d5fb5 --- /dev/null +++ b/crates/ui-egui/tests/constraint_preselection.rs @@ -0,0 +1,123 @@ +//! Constraint tools accept a selected curve pair as well as tool-first clicks. + +use serde_json::json; +use solvecraft_engine::{Sel, sketch::ConstraintKind}; +use solvecraft_ui_egui::scenario::Harness; + +fn lines() -> Harness { + let mut h = Harness::new(); + h.run(&json!([ + {"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}}, + {"call":"engine.execute","params":{"command":"sketch.line","params":{"points":[[10,10],[40,10]],"infer":false}}}, + {"call":"engine.execute","params":{"command":"sketch.line","params":{"points":[[10,30],[40,35]],"infer":false}}}, + {"call":"ui.view","params":{"view":"top"}} + ])) + .unwrap(); + h +} + +fn select_lines(h: &mut Harness) { + h.run(&json!([ + {"click":{"sketch":[25,10]}}, + {"click":{"sketch":[25,32.5]},"shift":true} + ])) + .unwrap(); + assert_eq!(h.app.session.selection, vec![Sel::SketchCurve { id: "l1".into() }, Sel::SketchCurve { id: "l2".into() }]); +} + +#[test] +fn selected_lines_apply_constraints_with_one_undo_step() { + for command in ["parallel", "perpendicular", "equal", "collinear"] { + let mut h = lines(); + select_lines(&mut h); + let before = h.app.session.doc.clone(); + let undo = h.app.session.undo.len(); + h.run(&json!([{"start":format!("sketch.constraint.{command}")}])).unwrap(); + assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error), "{command}"); + let sk = &h.app.session.model.state().sketches[0].sketch; + assert!( + sk.constraints.iter().any(|c| match command { + "parallel" => matches!(c.kind, ConstraintKind::Parallel { .. }), + "perpendicular" => matches!(c.kind, ConstraintKind::Perpendicular { .. }), + "equal" => matches!(c.kind, ConstraintKind::Equal { .. }), + _ => matches!(c.kind, ConstraintKind::Collinear { .. }), + }), + "{command}" + ); + let after = h.app.session.doc.clone(); + assert_eq!(h.app.session.undo.len(), undo + 1); + assert!(h.app.session.selection.is_empty()); + assert!(h.app.tool.is_none()); + h.app.session.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(serde_json::to_value(&h.app.session.doc).unwrap(), serde_json::to_value(before).unwrap()); + h.app.session.execute("edit.redo", &json!({})).unwrap(); + assert_eq!(serde_json::to_value(&h.app.session.doc).unwrap(), serde_json::to_value(after).unwrap()); + } +} + +#[test] +fn selected_circles_apply_constraints() { + for command in ["tangent", "concentric", "equal"] { + let mut h = Harness::new(); + h.run(&json!([ + {"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}}, + {"call":"engine.execute","params":{"command":"sketch.circle.center","params":{"center":[10,10],"radius":5}}}, + {"call":"engine.execute","params":{"command":"sketch.circle.center","params":{"center":[25,12],"radius":7}}}, + {"call":"ui.view","params":{"view":"top"}}, + {"click":{"sketch":[10,15]}}, + {"click":{"sketch":[25,19]},"shift":true} + ])) + .unwrap(); + assert_eq!(h.app.session.selection.len(), 2); + let undo = h.app.session.undo.len(); + h.run(&json!([{"start":format!("sketch.constraint.{command}")}])).unwrap(); + assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error), "{command}"); + assert_eq!(h.app.session.undo.len(), undo + 1, "{command}"); + let sk = &h.app.session.model.state().sketches[0].sketch; + assert!(sk.constraints.iter().any(|c| match command { + "tangent" => matches!(c.kind, ConstraintKind::Tangent { .. }), + "concentric" => matches!(c.kind, ConstraintKind::Concentric { .. }), + _ => matches!(c.kind, ConstraintKind::Equal { .. }), + })); + assert!(h.app.session.selection.is_empty()); + } +} + +#[test] +fn ambiguous_selection_preserves_tool_first_workflow() { + for selection in [ + vec![], + vec![Sel::SketchCurve { id: "l1".into() }], + vec![Sel::SketchCurve { id: "l1".into() }, Sel::SketchCurve { id: "l2".into() }, Sel::SketchPoint { id: "origin".into() }], + ] { + let mut h = lines(); + h.app.session.selection = selection; + let undo = h.app.session.undo.len(); + h.run(&json!([{"start":"sketch.constraint.parallel"}])).unwrap(); + assert_eq!(h.app.session.undo.len(), undo); + assert!(h.app.tool.as_ref().unwrap().picks.is_empty()); + h.run(&json!([{"click":{"sketch":[25,10]}}, {"click":{"sketch":[25,32.5]}}])).unwrap(); + assert_eq!(h.app.session.undo.len(), undo + 1); + assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error)); + } +} + +#[test] +fn rejected_preselection_preserves_document_and_error() { + let mut h = lines(); + h.app.session.execute("sketch.constraint.fix", &json!({"entities":["l1","l2"]})).unwrap(); + select_lines(&mut h); + let before = serde_json::to_value(&h.app.session.doc).unwrap(); + let undo = h.app.session.undo.len(); + let selection = h.app.session.selection.clone(); + h.run(&json!([{"start":"sketch.constraint.parallel"}])).unwrap(); + assert!(h.app.status.as_ref().is_some_and(|(_, _, error)| *error)); + assert_eq!(serde_json::to_value(&h.app.session.doc).unwrap(), before); + assert_eq!(h.app.session.undo.len(), undo); + assert_eq!(h.app.session.selection, selection); + // Correct the conflict and retry without reselecting; the old error must disappear. + h.app.session.execute("sketch.constraint.fix", &json!({"entities":["l1","l2"]})).unwrap(); + h.run(&json!([{"start":"sketch.constraint.parallel"}])).unwrap(); + assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error)); + assert!(h.app.session.selection.is_empty()); +} diff --git a/docs/book/src/guide/sketching.md b/docs/book/src/guide/sketching.md index de24f0d4..7bb00f1b 100644 --- a/docs/book/src/guide/sketching.md +++ b/docs/book/src/guide/sketching.md @@ -60,6 +60,11 @@ midpoint, symmetry, fix, curvature (G2) and polygon. **Sketch Dimension** (D) ad aligned, horizontal, vertical, radius, diameter and angle dimensions. Each is a parameter (`d1`, `d2`, …), so it can hold an expression. +For Parallel, Perpendicular, Equal, Tangent, Concentric and Collinear, select exactly two +sketch curves (Shift-click to add the second) and choose the constraint to apply it immediately. +You can also start the constraint first and then pick its curves. An incomplete or mixed +selection starts that pick workflow; a rejected constraint keeps the selection for correction. + Under-constrained geometry is blue and fully constrained geometry turns dark, and the status shows the remaining degrees of freedom. A constraint or dimension that would over-constrain the sketch is refused rather than applied. Drag a point to move it; the solver keeps every From 7c667eda536e1bbaa0d42a374955c310d0748b4b Mon Sep 17 00:00:00 2001 From: lillith <35337584+0xlillith@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:24:42 +0200 Subject: [PATCH 3/7] test: verify constrained line geometry after preselection and redo --- .../ui-egui/tests/constraint_preselection.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/ui-egui/tests/constraint_preselection.rs b/crates/ui-egui/tests/constraint_preselection.rs index 215d5fb5..6787aee7 100644 --- a/crates/ui-egui/tests/constraint_preselection.rs +++ b/crates/ui-egui/tests/constraint_preselection.rs @@ -25,6 +25,29 @@ fn select_lines(h: &mut Harness) { assert_eq!(h.app.session.selection, vec![Sel::SketchCurve { id: "l1".into() }, Sel::SketchCurve { id: "l2".into() }]); } +fn assert_line_geometry(h: &Harness, command: &str) { + let sk = &h.app.session.model.state().sketches[0].sketch; + let point = |name| sk.point(sk.resolve_point(name).unwrap()).unwrap(); + let a = point("l1.start"); + let b = point("l2.start"); + let u = point("l1.end") - a; + let v = point("l2.end") - b; + // Normalization also rejects a collapsed line; zero-length vectors must not + // accidentally satisfy the parallel/perpendicular checks. + let du = u.normalized().unwrap(); + let dv = v.normalized().unwrap(); + match command { + "parallel" => assert!(du.cross(dv).abs() < 1e-6), + "perpendicular" => assert!(du.dot(dv).abs() < 1e-6), + "equal" => assert!((u.len() - v.len()).abs() < 1e-6), + "collinear" => { + assert!(du.cross(dv).abs() < 1e-6); + assert!((b - a).cross(du).abs() < 1e-6); + } + _ => panic!("unexpected line constraint: {command}"), + } +} + #[test] fn selected_lines_apply_constraints_with_one_undo_step() { for command in ["parallel", "perpendicular", "equal", "collinear"] { @@ -44,6 +67,7 @@ fn selected_lines_apply_constraints_with_one_undo_step() { }), "{command}" ); + assert_line_geometry(&h, command); let after = h.app.session.doc.clone(); assert_eq!(h.app.session.undo.len(), undo + 1); assert!(h.app.session.selection.is_empty()); @@ -52,6 +76,7 @@ fn selected_lines_apply_constraints_with_one_undo_step() { assert_eq!(serde_json::to_value(&h.app.session.doc).unwrap(), serde_json::to_value(before).unwrap()); h.app.session.execute("edit.redo", &json!({})).unwrap(); assert_eq!(serde_json::to_value(&h.app.session.doc).unwrap(), serde_json::to_value(after).unwrap()); + assert_line_geometry(&h, command); } } From 3afd6ae4472aaa0d267e292ec5c6c59f02546732 Mon Sep 17 00:00:00 2001 From: Mithril-3d Date: Sun, 11 Oct 2026 12:41:41 +0900 Subject: [PATCH 4/7] feat: add multi-language (i18n) support with Japanese UI --- apps/solvecraft/src/main.rs | 1 + crates/ui-egui/src/browser.rs | 9 +- crates/ui-egui/src/context_menu.rs | 13 +- crates/ui-egui/src/dialogs.rs | 31 +- crates/ui-egui/src/documents.rs | 32 +- crates/ui-egui/src/fonts.rs | 46 +++ crates/ui-egui/src/i18n.rs | 424 +++++++++++++++++++++++++++ crates/ui-egui/src/lib.rs | 8 + crates/ui-egui/src/prefs.rs | 73 +++-- crates/ui-egui/src/sketch_palette.rs | 31 +- crates/ui-egui/src/toolbar.rs | 59 ++-- 11 files changed, 647 insertions(+), 80 deletions(-) create mode 100644 crates/ui-egui/src/fonts.rs create mode 100644 crates/ui-egui/src/i18n.rs diff --git a/apps/solvecraft/src/main.rs b/apps/solvecraft/src/main.rs index f7e14d3f..0d9cd71b 100644 --- a/apps/solvecraft/src/main.rs +++ b/apps/solvecraft/src/main.rs @@ -169,6 +169,7 @@ fn main() -> eframe::Result { "SolveCraft", options, Box::new(move |cc| { + solvecraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx); let mut app = SolveApp::new(Session::default(), services()); app.custom_titlebar = CUSTOM_TITLEBAR; app.integrated_titlebar = cfg!(target_os = "macos"); diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index 5fbc0c8f..cedabc0c 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -351,7 +351,8 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp { x += 21.0; } let col = row.color.unwrap_or(if row.dim { t.text_dim } else { t.text }); - let galley = ui.painter().layout_no_wrap(row.label.to_string(), FontId::proportional(12.5), col); + let label_display = crate::i18n::tr_dyn(row.label); + let galley = ui.painter().layout_no_wrap(label_display, FontId::proportional(12.5), col); let lw = galley.size().x; ui.painter().galley(pos2(x, cy - galley.size().y / 2.0), galley, col); let mut bx = (x + lw + 6.0).min(r.right() - 16.0); @@ -362,9 +363,9 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp { } crate::scenario::publish_handle(&format!("row:{}", row.label), r.center()); let tip = match hover_on { - 1 => Some(if row.fold == Some(true) { "Collapse" } else { "Expand" }), - 2 => Some(if row.eye == Some(true) { "Hide" } else { "Show" }), - 3 => Some(if row.radio == Some(true) { "Active component" } else { "Activate" }), + 1 => Some(if row.fold == Some(true) { crate::i18n::tr("Collapse") } else { crate::i18n::tr("Expand") }), + 2 => Some(if row.eye == Some(true) { crate::i18n::tr("Hide") } else { crate::i18n::tr("Show") }), + 3 => Some(if row.radio == Some(true) { crate::i18n::tr("Active component") } else { crate::i18n::tr("Activate") }), _ => None, }; let resp = match tip { diff --git a/crates/ui-egui/src/context_menu.rs b/crates/ui-egui/src/context_menu.rs index 3b9ba9ab..c5dbeaaa 100644 --- a/crates/ui-egui/src/context_menu.rs +++ b/crates/ui-egui/src/context_menu.rs @@ -1273,7 +1273,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { painter.circle_filled(c, 3.0, t.text_dim); } for (k, item) in radial.iter().enumerate() { - let galley = painter.layout_no_wrap(item.label.clone(), font.clone(), if item.enabled { t.text } else { t.text_dim }); + let display_label = crate::i18n::tr(&item.label); + let galley = painter.layout_no_wrap(display_label.to_string(), font.clone(), if item.enabled { t.text } else { t.text_dim }); let size = vec2(galley.size().x + 40.0, 28.0); let rr = slot_rect(c, k, size); let resp = ui.interact(rr, ui.id().with(("radial", k)), Sense::click()); @@ -1295,7 +1296,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { picked = Some(item.clone()); } if item.enabled { - resp.on_hover_text(item.shortcut.clone()); + resp.on_hover_text(crate::i18n::tr(&item.shortcut).to_string()); } } }); @@ -1393,7 +1394,7 @@ pub fn list_row_lit(ui: &mut egui::Ui, item: &Item, lit: bool) -> Option { ui.painter().text( pos2(r.left() + 8.0, r.center().y + 2.0), Align2::LEFT_CENTER, - item.label.to_uppercase(), + crate::i18n::tr(&item.label).to_uppercase(), FontId::proportional(10.5), t.text_dim, ); @@ -1415,9 +1416,11 @@ pub fn list_row_lit(ui: &mut egui::Ui, item: &Item, lit: bool) -> Option { ); } let text = if item.enabled { t.text } else { t.text_dim.gamma_multiply(0.75) }; - ui.painter().text(pos2(r.left() + 32.0, r.center().y), Align2::LEFT_CENTER, &item.label, FontId::proportional(13.0), text); + let display_label = crate::i18n::tr(&item.label); + ui.painter().text(pos2(r.left() + 32.0, r.center().y), Align2::LEFT_CENTER, display_label, FontId::proportional(13.0), text); if !item.shortcut.is_empty() { - ui.painter().text(pos2(r.right() - 10.0, r.center().y), Align2::RIGHT_CENTER, &item.shortcut, FontId::proportional(11.5), t.text_dim); + let display_shortcut = crate::i18n::tr(&item.shortcut); + ui.painter().text(pos2(r.right() - 10.0, r.center().y), Align2::RIGHT_CENTER, display_shortcut, FontId::proportional(11.5), t.text_dim); } (item.enabled && resp.clicked()).then(|| item.clone()) } diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 5038b79e..63e24a03 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -745,9 +745,10 @@ pub(crate) fn field(ui: &mut egui::Ui, s: &mut String) -> bool { pub(crate) fn combo(ui: &mut egui::Ui, id: &str, labels: &[&str], sel: &mut usize) { let row = ROW.with(|r| r.borrow().clone()); - let r = egui::ComboBox::from_id_salt(id).selected_text(labels.get(*sel).copied().unwrap_or("")).width(FIELD_W).show_ui(ui, |ui| { + let selected = labels.get(*sel).copied().map(tr).unwrap_or(""); + let r = egui::ComboBox::from_id_salt(id).selected_text(selected).width(FIELD_W).show_ui(ui, |ui| { for (i, l) in labels.iter().enumerate() { - let item = ui.selectable_value(sel, i, *l); + let item = ui.selectable_value(sel, i, tr(l)); crate::scenario::publish_handle(&format!("dialog:{row}:{l}"), item.rect.center()); } }); @@ -757,6 +758,11 @@ pub(crate) fn combo(ui: &mut egui::Ui, id: &str, labels: &[&str], sel: &mut usiz thread_local! { /// The label of the dialog row being laid out (its widgets publish `dialog: