From a91438db8d5feaa3880f8da6ec4830ae054093c8 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Tue, 20 Oct 2026 23:55:05 +0200 Subject: [PATCH 1/6] Properties: corner shape and stroke type lists show their glyphs, as in InDesign Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/i18n.rs | 2 + crates/ui-egui/src/i18n/ar.rs | 2 + crates/ui-egui/src/i18n/cs.rs | 2 + crates/ui-egui/src/i18n/de.rs | 2 + crates/ui-egui/src/i18n/it.rs | 2 + crates/ui-egui/src/i18n/ja.rs | 2 + crates/ui-egui/src/i18n/ka.rs | 2 + crates/ui-egui/src/i18n/pl.rs | 2 + crates/ui-egui/src/i18n/pt_br.rs | 2 + crates/ui-egui/src/i18n/ru.rs | 2 + crates/ui-egui/src/i18n/uk.rs | 2 + crates/ui-egui/src/panels/properties.rs | 140 ++++++++++++++++++------ 13 files changed, 130 insertions(+), 33 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 0128924c0..e1e4b8510 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-20** · Properties › Appearance: the corner shape list shows each shape's glyph beside its name, drawn as InDesign draws them (Fancy has its own glyph now), and the stroke type list shows each type's line; both dropdowns are named for screen readers. No readiness percentage change. - **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. - **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change. diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index efe089ef9..b5494731c 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -89,6 +89,8 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Cut", ["Ausschneiden", "Couper", "Cortar", "カット", "剪切"]), ("Copy", ["Kopieren", "Copier", "Copiar", "コピー", "复制"]), ("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]), + ("Stroke Type", ["Stroke Type", "Type de contour", "Stroke Type", "Stroke Type", "描边类型"]), + ("Corner Shape", ["Corner Shape", "Forme d'angle", "Corner Shape", "Corner Shape", "角形状"]), ("Hide Rulers", ["Lineale ausblenden", "Masquer les règles", "Ocultar reglas", "定規を隠す", "隐藏标尺"]), ("Inches Decimal", ["Zoll dezimal", "Pouces décimaux", "Pulgadas decimales", "インチ(10 進)", "十进制英寸"]), ( diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index b050de38b..7c7406c21 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("Stroke Type", "نوع الحد"), + ("Corner Shape", "شكل الزاوية"), ("Paste Vector Art", "لصق رسم متجه"), ("Baseline Options", "خيارات خط الأساس"), ("Fixed Number", "عدد ثابت"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index e77e72a1c..bc28598ae 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -31,6 +31,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Vyjmout"), ("Copy", "Kopírovat"), ("Paste", "Vložit"), + ("Stroke Type", "Typ tahu"), + ("Corner Shape", "Tvar rohu"), ("Reverse Path", "Obrátit cestu"), ("Set Anchor", "Nastavit kotvu"), ("Clear", "Vymazat"), diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index efc128858..05a4b5b25 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -31,6 +31,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Ausschneiden"), ("Copy", "Kopieren"), ("Paste", "Einfügen"), + ("Stroke Type", "Konturtyp"), + ("Corner Shape", "Eckenform"), ("Inches Decimal", "Zoll dezimal"), ("Back", "Zurück"), ("Warning", "Warnung"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index a918a9829..ce38d066e 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("Stroke Type", "Tipo traccia"), + ("Corner Shape", "Forma angolo"), ("Paste Vector Art", "Incolla grafica vettoriale"), ("Baseline Options", "Opzioni linea di base"), ("Fixed Number", "Numero fisso"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 5647ca940..bd69f6628 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -30,6 +30,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "カット"), ("Copy", "コピー"), ("Paste", "ペースト"), + ("Stroke Type", "線の種類"), + ("Corner Shape", "角のシェイプ"), ("Paste Vector Art", "ベクトルアートをペースト"), ("Baseline Options", "ベースラインオプション"), ("Column Rules", "段落境界線"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index d938c2dc7..5a0736c09 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -31,6 +31,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "ამოჭრა"), ("Copy", "კოპირება"), ("Paste", "ჩასმა"), + ("Stroke Type", "კონტურის ტიპი"), + ("Corner Shape", "კუთხის ფორმა"), ("Inches Decimal", "ინჩები (ათობითი)"), ("Back", "უკან"), ("Warning", "გაფრთხილება"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 68d685d99..4b01ab6af 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -29,6 +29,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Wytnij"), ("Copy", "Kopiuj"), ("Paste", "Wklej"), + ("Stroke Type", "Rodzaj obrysu"), + ("Corner Shape", "Kształt narożnika"), ("Clear", "Wyczyść"), ("Duplicate", "Powiel"), ("Select All", "Zaznacz wszystko"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 8521de594..75406e9b0 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("Stroke Type", "Tipo de traçado"), + ("Corner Shape", "Forma do canto"), ("Paste Vector Art", "Colar arte vetorial"), ("Baseline Options", "Opções de linha de base"), ("Fixed Number", "Número fixo"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 251555ca5..72df9edd4 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -33,6 +33,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вырезать"), ("Copy", "Копировать"), ("Paste", "Вставить"), + ("Stroke Type", "Тип обводки"), + ("Corner Shape", "Форма угла"), ("Clear", "Очистить"), ("Duplicate", "Дублировать"), ("Select All", "Выделить все"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index dd4a1434d..f50e567fc 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -29,6 +29,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("Stroke Type", "Тип обведення"), + ("Corner Shape", "Форма кута"), ("Paste Vector Art", "Вставити векторну графіку"), ("Baseline Options", "Параметри базової лінії"), ("Fixed Number", "Фіксована кількість"), diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 44acf6e2c..c28f02958 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -674,11 +674,11 @@ pub(crate) fn stroke_type_dropdown(app: &mut DesignApp, ui: &mut Ui, cur: &str, ("Thin - Thin", "thinThin"), ("Thick - Thick", "thickThick"), ]; - let resp = preview_dropdown(ui, w, |p, r| paint_stroke_type(p, r, cur)); + let resp = preview_dropdown(ui, w, crate::i18n::tr(&app.ui.language, "Stroke Type"), |p, r| paint_stroke_type(p, r, cur, egui::Color32::BLACK)); egui::Popup::menu(&resp).show(|ui| { - ui.set_min_width(140.0); + ui.set_min_width(170.0); for (label, kind) in TYPES { - if ui.selectable_label(*kind == cur, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).clicked() { + if preview_row(ui, *kind == cur, crate::i18n::tr(&app.ui.language, label), 40.0, |p, r, c| paint_stroke_type(p, r, kind, c)).clicked() { let ty = if *kind == "dashed" { json!({"kind": "dashed", "pattern": [12.0, 4.0]}) } else { json!({"kind": kind}) }; let _ = app.run("object.stroke", json!({"type": ty})); ui.close(); @@ -687,8 +687,7 @@ pub(crate) fn stroke_type_dropdown(app: &mut DesignApp, ui: &mut Ui, cur: &str, }); } -fn paint_stroke_type(p: &egui::Painter, r: Rect, kind: &str) { - let c = egui::Color32::BLACK; +fn paint_stroke_type(p: &egui::Painter, r: Rect, kind: &str, c: egui::Color32) { let (x0, x1, y) = (r.min.x + 5.0, r.max.x - 5.0, r.center().y); match kind { "dashed" => { @@ -721,10 +720,35 @@ fn paint_stroke_type(p: &egui::Painter, r: Rect, kind: &str) { } } +/// A row of a preview dropdown's list, as InDesign's: a small preview of the choice (`preview_w` +/// wide), then its name. Selected rows are highlighted. +fn preview_row(ui: &mut Ui, selected: bool, label: &str, preview_w: f32, paint: impl FnOnce(&egui::Painter, Rect, egui::Color32)) -> egui::Response { + let t = Tokens::get(ui.ctx()); + // As wide as the preview and the longest name (the popup otherwise grows to all it's offered). + let w = preview_w + 130.0; + let (r, resp) = ui.allocate_exact_size(vec2(w, 22.0), Sense::click()); + if selected || resp.hovered() { + ui.painter().rect_filled(r, 2.0, if selected { t.row_selected } else { t.hover }); + } + let ink = if selected { t.text_strong } else { t.text }; + paint(ui.painter(), Rect::from_min_size(r.min + vec2(4.0, 0.0), vec2(preview_w, 22.0)), ink); + crate::rtl::paint( + ui.painter(), + pos2(r.min.x + preview_w + 12.0, r.center().y), + egui::Align2::LEFT_CENTER, + label, + egui::FontId::proportional(12.0), + ink, + ); + resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, selected, label)); + resp +} + /// A dropdown whose face is a light preview box (stroke type, corner shape). -fn preview_dropdown(ui: &mut Ui, w: f32, paint: impl FnOnce(&egui::Painter, Rect)) -> egui::Response { +fn preview_dropdown(ui: &mut Ui, w: f32, label: &str, paint: impl FnOnce(&egui::Painter, Rect)) -> egui::Response { let t = Tokens::get(ui.ctx()); let (r, resp) = ui.allocate_exact_size(vec2(w, FIELD_H), Sense::click()); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::ComboBox, true, label)); let face = Rect::from_min_max(r.min, pos2(r.max.x - widgets::SEG_W, r.max.y)); ui.painter().rect_filled(face, 0.0, egui::Color32::from_gray(0xdf)); paint(ui.painter(), face); @@ -735,38 +759,56 @@ fn preview_dropdown(ui: &mut Ui, w: f32, paint: impl FnOnce(&egui::Painter, Rect resp } -fn paint_corner_shape(p: &egui::Painter, r: Rect, shape: CornerShape) { - let c = egui::Color32::BLACK; - let s = Stroke::new(1.5, c); - let (x0, y0, x1, y1) = (r.min.x + 4.0, r.min.y + 4.0, r.max.x - 9.0, r.max.y - 3.0); - let k = 8.0; - let pts: Vec = match shape { - CornerShape::None => vec![pos2(x0, y0), pos2(x1, y0), pos2(x1, y1)], - CornerShape::Bevel => vec![pos2(x0, y0), pos2(x1 - k, y0), pos2(x1, y0 + k), pos2(x1, y1)], - CornerShape::Inset => vec![pos2(x0, y0), pos2(x1 - k, y0), pos2(x1 - k, y0 + k), pos2(x1, y0 + k), pos2(x1, y1)], - CornerShape::Rounded | CornerShape::InverseRounded | CornerShape::Fancy => { - let mut v = vec![pos2(x0, y0)]; - for i in 0..=8 { - let a = i as f32 / 8.0 * std::f32::consts::FRAC_PI_2; - v.push(if shape == CornerShape::InverseRounded { - pos2(x1 - k * a.cos(), y0 + k * a.sin()) - } else { - pos2(x1 - k + k * a.sin(), y0 + k - k * a.cos()) - }); - } - v.push(pos2(x1, y1)); - v - } +/// A corner shape's glyph, as InDesign draws it in its corner lists: the top edge running into the +/// corner and the right edge leaving it, in a square centred in `r`. +fn paint_corner_shape(p: &egui::Painter, r: Rect, shape: CornerShape, c: egui::Color32) { + let side = (r.width() - 3.0).min(r.height() - 3.0).max(4.0); + let o = pos2(r.center().x - side / 2.0, r.center().y - side / 2.0); + let at = |x: f32, y: f32| pos2(o.x + x * side, o.y + y * side); + let quarter = |cx: f32, cy: f32, rad: f32, from: f32, to: f32| -> Vec { + (0..=10).map(|i| from + (to - from) * i as f32 / 10.0).map(|a| at(cx + rad * a.cos(), cy + rad * a.sin())).collect() }; - p.add(egui::Shape::line(pts, s)); + use std::f32::consts::{FRAC_PI_2, PI}; + let stroke = Stroke::new(1.6, c); + let line = |pts: Vec| { + p.add(egui::Shape::line(pts, stroke)); + }; + match shape { + CornerShape::None => line(vec![at(0.0, 0.0), at(1.0, 0.0), at(1.0, 1.0)]), + // The corner cut by a diagonal, half the side. + CornerShape::Bevel => line(vec![at(0.0, 0.0), at(0.5, 0.0), at(1.0, 0.5), at(1.0, 1.0)]), + // A square notch. + CornerShape::Inset => line(vec![at(0.0, 0.0), at(0.5, 0.0), at(0.5, 0.45), at(1.0, 0.45), at(1.0, 1.0)]), + // A concave quarter circle centred on the corner. + CornerShape::InverseRounded => { + let mut v = vec![at(0.0, 0.0)]; + v.extend(quarter(1.0, 0.0, 0.5, PI, FRAC_PI_2)); + v.push(at(1.0, 1.0)); + line(v) + } + // A large convex arc. + CornerShape::Rounded => { + let mut v = vec![at(0.0, 0.0)]; + v.extend(quarter(0.08, 0.92, 0.92, -FRAC_PI_2, 0.0)); + v.push(at(1.0, 1.0)); + line(v) + } + // Steps into the corner with a small square loop between them. + CornerShape::Fancy => { + line(vec![at(0.0, 0.0), at(0.38, 0.0), at(0.38, 0.2), at(0.52, 0.2)]); + p.rect_stroke(Rect::from_min_max(at(0.52, 0.06), at(0.76, 0.3)), 0.0, stroke, StrokeKind::Middle); + line(vec![at(0.76, 0.3), at(0.76, 0.48), at(1.0, 0.48), at(1.0, 1.0)]); + } + } } pub(crate) fn corner_type_dropdown(app: &mut DesignApp, ui: &mut Ui, shape: CornerShape, size: f64, width: f32) { - let resp = preview_dropdown(ui, width, |p, r| paint_corner_shape(p, r, shape)); + let resp = + preview_dropdown(ui, width, crate::i18n::tr(&app.ui.language, "Corner Shape"), |p, r| paint_corner_shape(p, r, shape, egui::Color32::BLACK)); egui::Popup::menu(&resp).show(|ui| { - ui.set_min_width(140.0); + ui.set_min_width(150.0); for s in CornerShape::ALL { - if ui.selectable_label(s == shape, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, s.label()))).clicked() { + if preview_row(ui, s == shape, crate::i18n::tr(&app.ui.language, s.label()), 22.0, |p, r, c| paint_corner_shape(p, r, s, c)).clicked() { let _ = app.run("object.cornerOptions", json!({"shape": s, "size": if size > 0.0 { size } else { 12.0 }})); ui.close(); } @@ -3343,6 +3385,38 @@ mod tests { assert!((r.x0 - 200.0).abs() < 0.5 && (r.y1 - 200.0).abs() < 0.5 && (r.x1 - 300.0).abs() < 0.5, "{r:?}"); } + /// The corner shape list shows each shape's glyph beside its name, as InDesign's; a row picks + /// the shape and the current one is marked. + #[test] + fn the_corner_shape_list_has_glyph_rows_and_picks_a_shape() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let id = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "shape": "rectangle"})).unwrap()["id"].as_u64().unwrap(); + app.run("selection.set", json!({"ids": [id]})).unwrap(); + let mut h = panel(app); + h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Corner Shape").click(); + h.run_steps(3); + let rows = ["None", "Fancy", "Bevel", "Inset", "Inverse Rounded", "Rounded"]; + for r in rows { + let _ = h.get_by_role_and_label(egui::accesskit::Role::Button, r); + } + h.get_by_role_and_label(egui::accesskit::Role::Button, "Bevel").click(); + h.run_steps(3); + let it = h.state().session.active().unwrap().doc.item(designcraft_doc::ids::ItemId(id)).unwrap().clone(); + assert!(it.corners.corners.iter().all(|c| c.shape == designcraft_geom::corners::CornerShape::Bevel), "{:?}", it.corners); + h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Corner Shape").click(); + h.run_steps(3); + use egui_kittest::kittest::NodeT; + let n = h.get_by_role_and_label(egui::accesskit::Role::Button, "Bevel"); + assert_eq!(n.accesskit_node().toggled(), Some(egui::accesskit::Toggled::True), "the current shape is marked"); + // The stroke type list has the same rows, by name. + h.key_press(egui::Key::Escape); + h.run_steps(2); + h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Stroke Type").click(); + h.run_steps(3); + let _ = h.get_by_role_and_label(egui::accesskit::Role::Button, "Thick - Thin"); + } + const CYAN: &str = "C=100 M=0 Y=0 K=0"; #[test] @@ -3746,7 +3820,7 @@ fn flex_section(app: &mut DesignApp, ui: &mut Ui) { let cur = serde_json::to_value(cur).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default(); let icon = FLEX_JUSTIFY.iter().find(|j| j.0 == cur).map(|j| j.1).unwrap_or("flex-start"); let resp = place(ui, sub(b, 0.0, 0.0, 58.0, FIELD_H), |ui| { - preview_dropdown(ui, 58.0, |p, r| icons::paint(p, r.shrink2(vec2(6.0, 2.0)), icon, egui::Color32::from_gray(60))) + preview_dropdown(ui, 58.0, &tr("Justification"), |p, r| icons::paint(p, r.shrink2(vec2(6.0, 2.0)), icon, egui::Color32::from_gray(60))) }); egui::Popup::menu(&resp).show(|ui| { ui.set_min_width(150.0); From f66621ff5f8703973677069bce040b002a0b0532 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Wed, 21 Oct 2026 00:30:03 +0200 Subject: [PATCH 2/6] i18n: Spanish for the new labels Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/i18n.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index b5494731c..d5208664d 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -89,8 +89,8 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Cut", ["Ausschneiden", "Couper", "Cortar", "カット", "剪切"]), ("Copy", ["Kopieren", "Copier", "Copiar", "コピー", "复制"]), ("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]), - ("Stroke Type", ["Stroke Type", "Type de contour", "Stroke Type", "Stroke Type", "描边类型"]), - ("Corner Shape", ["Corner Shape", "Forme d'angle", "Corner Shape", "Corner Shape", "角形状"]), + ("Stroke Type", ["Stroke Type", "Type de contour", "Tipo de trazo", "Stroke Type", "描边类型"]), + ("Corner Shape", ["Corner Shape", "Forme d'angle", "Forma de esquina", "Corner Shape", "角形状"]), ("Hide Rulers", ["Lineale ausblenden", "Masquer les règles", "Ocultar reglas", "定規を隠す", "隐藏标尺"]), ("Inches Decimal", ["Zoll dezimal", "Pouces décimaux", "Pulgadas decimales", "インチ(10 進)", "十进制英寸"]), ( From f3800df46ba5376babdfdf53cc31cf16efbdfb3f Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:06:21 +0200 Subject: [PATCH 3/6] Properties: corner and stroke type rows fit their translated names Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/properties.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index c28f02958..e3e12257f 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -724,8 +724,11 @@ fn paint_stroke_type(p: &egui::Painter, r: Rect, kind: &str, c: egui::Color32) { /// wide), then its name. Selected rows are highlighted. fn preview_row(ui: &mut Ui, selected: bool, label: &str, preview_w: f32, paint: impl FnOnce(&egui::Painter, Rect, egui::Color32)) -> egui::Response { let t = Tokens::get(ui.ctx()); - // As wide as the preview and the longest name (the popup otherwise grows to all it's offered). - let w = preview_w + 130.0; + // As wide as the preview and its name, translated (at least 130 pt for the name), and as the + // popup once its widest row has set it: the rows' highlights then end together. + let font = egui::FontId::proportional(12.0); + let text_w = ui.fonts_mut(|f| f.layout_no_wrap(label.to_string(), font, egui::Color32::WHITE).size().x); + let w = (preview_w + 12.0 + text_w + 12.0).max(preview_w + 130.0).max(ui.available_width().min(600.0)); let (r, resp) = ui.allocate_exact_size(vec2(w, 22.0), Sense::click()); if selected || resp.hovered() { ui.painter().rect_filled(r, 2.0, if selected { t.row_selected } else { t.hover }); @@ -3387,6 +3390,24 @@ mod tests { /// The corner shape list shows each shape's glyph beside its name, as InDesign's; a row picks /// the shape and the current one is marked. + /// A row is as wide as its translated name needs: a long one isn't cut at a fixed width. + #[test] + fn preview_rows_fit_long_translated_names() { + let ctx = egui::Context::default(); + let long = "Скругленный прямоугольник с обратным углом"; + let mut widths = (0.0, 0.0); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { + egui::Area::new(egui::Id::new("rows")).show(ui.ctx(), |ui| { + ui.set_max_width(10.0); + let r = preview_row(ui, false, long, 40.0, |_, _, _| {}); + let text = ui.fonts_mut(|f| f.layout_no_wrap(long.to_string(), egui::FontId::proportional(12.0), egui::Color32::WHITE).size().x); + widths = (r.rect.width(), 40.0 + 12.0 + text); + }); + }); + out.textures_delta.clear(); + assert!(widths.0 >= widths.1, "{widths:?}"); + } + #[test] fn the_corner_shape_list_has_glyph_rows_and_picks_a_shape() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); From 1a11284bc72205a05e49ccc8baddeafff89368a2 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 22 Oct 2026 01:39:01 +0200 Subject: [PATCH 4/6] Properties tests: the corner shape list's doc comment back on its test Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/properties.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index e3e12257f..f3705f9b5 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -3388,8 +3388,6 @@ mod tests { assert!((r.x0 - 200.0).abs() < 0.5 && (r.y1 - 200.0).abs() < 0.5 && (r.x1 - 300.0).abs() < 0.5, "{r:?}"); } - /// The corner shape list shows each shape's glyph beside its name, as InDesign's; a row picks - /// the shape and the current one is marked. /// A row is as wide as its translated name needs: a long one isn't cut at a fixed width. #[test] fn preview_rows_fit_long_translated_names() { @@ -3408,6 +3406,8 @@ mod tests { assert!(widths.0 >= widths.1, "{widths:?}"); } + /// The corner shape list shows each shape's glyph beside its name, as InDesign's; a row picks + /// the shape and the current one is marked. #[test] fn the_corner_shape_list_has_glyph_rows_and_picks_a_shape() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); From e3fd046ecc23595f62031319e28fbbd823177a9f Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 20:50:36 +0100 Subject: [PATCH 5/6] rustfmt Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/properties/tests.rs | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/crates/ui-egui/src/panels/properties/tests.rs b/crates/ui-egui/src/panels/properties/tests.rs index 7bb88cc2c..5c9feea47 100644 --- a/crates/ui-egui/src/panels/properties/tests.rs +++ b/crates/ui-egui/src/panels/properties/tests.rs @@ -710,55 +710,55 @@ fn several_shapes_show_pathfinder_and_intersect_combines_them() { assert!((r.x0 - 200.0).abs() < 0.5 && (r.y1 - 200.0).abs() < 0.5 && (r.x1 - 300.0).abs() < 0.5, "{r:?}"); } - /// A row is as wide as its translated name needs: a long one isn't cut at a fixed width. - #[test] - fn preview_rows_fit_long_translated_names() { - let ctx = egui::Context::default(); - let long = "Скругленный прямоугольник с обратным углом"; - let mut widths = (0.0, 0.0); - let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { - egui::Area::new(egui::Id::new("rows")).show(ui.ctx(), |ui| { - ui.set_max_width(10.0); - let r = preview_row(ui, false, long, 40.0, |_, _, _| {}); - let text = ui.fonts_mut(|f| f.layout_no_wrap(long.to_string(), egui::FontId::proportional(12.0), egui::Color32::WHITE).size().x); - widths = (r.rect.width(), 40.0 + 12.0 + text); - }); +/// A row is as wide as its translated name needs: a long one isn't cut at a fixed width. +#[test] +fn preview_rows_fit_long_translated_names() { + let ctx = egui::Context::default(); + let long = "Скругленный прямоугольник с обратным углом"; + let mut widths = (0.0, 0.0); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { + egui::Area::new(egui::Id::new("rows")).show(ui.ctx(), |ui| { + ui.set_max_width(10.0); + let r = preview_row(ui, false, long, 40.0, |_, _, _| {}); + let text = ui.fonts_mut(|f| f.layout_no_wrap(long.to_string(), egui::FontId::proportional(12.0), egui::Color32::WHITE).size().x); + widths = (r.rect.width(), 40.0 + 12.0 + text); }); - out.textures_delta.clear(); - assert!(widths.0 >= widths.1, "{widths:?}"); - } + }); + out.textures_delta.clear(); + assert!(widths.0 >= widths.1, "{widths:?}"); +} - /// The corner shape list shows each shape's glyph beside its name, as InDesign's; a row picks - /// the shape and the current one is marked. - #[test] - fn the_corner_shape_list_has_glyph_rows_and_picks_a_shape() { - let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); - app.run("file.new", json!({})).unwrap(); - let id = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "shape": "rectangle"})).unwrap()["id"].as_u64().unwrap(); - app.run("selection.set", json!({"ids": [id]})).unwrap(); - let mut h = panel(app); - h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Corner Shape").click(); - h.run_steps(3); - let rows = ["None", "Fancy", "Bevel", "Inset", "Inverse Rounded", "Rounded"]; - for r in rows { - let _ = h.get_by_role_and_label(egui::accesskit::Role::Button, r); - } - h.get_by_role_and_label(egui::accesskit::Role::Button, "Bevel").click(); - h.run_steps(3); - let it = h.state().session.active().unwrap().doc.item(designcraft_doc::ids::ItemId(id)).unwrap().clone(); - assert!(it.corners.corners.iter().all(|c| c.shape == designcraft_geom::corners::CornerShape::Bevel), "{:?}", it.corners); - h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Corner Shape").click(); - h.run_steps(3); - use egui_kittest::kittest::NodeT; - let n = h.get_by_role_and_label(egui::accesskit::Role::Button, "Bevel"); - assert_eq!(n.accesskit_node().toggled(), Some(egui::accesskit::Toggled::True), "the current shape is marked"); - // The stroke type list has the same rows, by name. - h.key_press(egui::Key::Escape); - h.run_steps(2); - h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Stroke Type").click(); - h.run_steps(3); - let _ = h.get_by_role_and_label(egui::accesskit::Role::Button, "Thick - Thin"); +/// The corner shape list shows each shape's glyph beside its name, as InDesign's; a row picks +/// the shape and the current one is marked. +#[test] +fn the_corner_shape_list_has_glyph_rows_and_picks_a_shape() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let id = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "shape": "rectangle"})).unwrap()["id"].as_u64().unwrap(); + app.run("selection.set", json!({"ids": [id]})).unwrap(); + let mut h = panel(app); + h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Corner Shape").click(); + h.run_steps(3); + let rows = ["None", "Fancy", "Bevel", "Inset", "Inverse Rounded", "Rounded"]; + for r in rows { + let _ = h.get_by_role_and_label(egui::accesskit::Role::Button, r); } + h.get_by_role_and_label(egui::accesskit::Role::Button, "Bevel").click(); + h.run_steps(3); + let it = h.state().session.active().unwrap().doc.item(designcraft_doc::ids::ItemId(id)).unwrap().clone(); + assert!(it.corners.corners.iter().all(|c| c.shape == designcraft_geom::corners::CornerShape::Bevel), "{:?}", it.corners); + h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Corner Shape").click(); + h.run_steps(3); + use egui_kittest::kittest::NodeT; + let n = h.get_by_role_and_label(egui::accesskit::Role::Button, "Bevel"); + assert_eq!(n.accesskit_node().toggled(), Some(egui::accesskit::Toggled::True), "the current shape is marked"); + // The stroke type list has the same rows, by name. + h.key_press(egui::Key::Escape); + h.run_steps(2); + h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Stroke Type").click(); + h.run_steps(3); + let _ = h.get_by_role_and_label(egui::accesskit::Role::Button, "Thick - Thin"); +} const CYAN: &str = "C=100 M=0 Y=0 K=0"; From e5b17c21845f3a224e8b52da7f6f856aadc60322 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 20:56:41 +0100 Subject: [PATCH 6/6] egui 0.37: the renamed APIs in this branch's code Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/properties/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/properties/tests.rs b/crates/ui-egui/src/panels/properties/tests.rs index 5c9feea47..4458dae4f 100644 --- a/crates/ui-egui/src/panels/properties/tests.rs +++ b/crates/ui-egui/src/panels/properties/tests.rs @@ -717,7 +717,7 @@ fn preview_rows_fit_long_translated_names() { let long = "Скругленный прямоугольник с обратным углом"; let mut widths = (0.0, 0.0); let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { - egui::Area::new(egui::Id::new("rows")).show(ui.ctx(), |ui| { + egui::Area::new(egui::Id::unique("rows")).show(ui.ctx(), |ui| { ui.set_max_width(10.0); let r = preview_row(ui, false, long, 40.0, |_, _, _| {}); let text = ui.fonts_mut(|f| f.layout_no_wrap(long.to_string(), egui::FontId::proportional(12.0), egui::Color32::WHITE).size().x);