diff --git a/ROADMAP.md b/ROADMAP.md index 9604db66..8510a394 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-28 · **Change:** minor (M4: zero-length round-capped dashes draw dots; dash expansion uses the existing safety budget; extracted from #1143; Arabic draft catalog; scaled instances keep their scale when their main is rebuilt (#922); .fig embedded images are copied once per blob, not once per hash (#1059); page ruler guides survive save and reopen (#1476); colour picker LCH and OKLCH models; non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** minor (the toolbar on one 32 pt grid, without its Resources button (#1210); M4: zero-length round-capped dashes draw dots; dash expansion uses the existing safety budget; extracted from #1143; Arabic draft catalog; scaled instances keep their scale when their main is rebuilt (#922); .fig embedded images are copied once per blob, not once per hash (#1059); page ruler guides survive save and reopen (#1476); colour picker LCH and OKLCH models; non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -352,6 +352,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-29 | minor | The toolbar on one 32 pt grid (M4, #1210): the pill's cells, padding, gaps and divider share one grid in every state, and the Resources button is gone, as in UI3; the Assets panel opens from the left rail, Shift+I, Alt+2 and View ▸ Panels ▸ Assets. Numbers unchanged | | 2026-10-28 | minor | Arabic draft: 1,857 UI strings, 620 command labels, 31 menu wordings; count-neutral plurals and web font gate. Native review and panel mirroring remain open; grade and estimates unchanged. Evidence: `crates/ui-egui/locales/ar.toml`, i18n catalog tests and [translation notes](docs/translating.md#arabic-draft) | | 2026-10-28 | minor | M3, #922: instances scaled with the Scale tool keep their layers' scaled size and points when their main is rebuilt (`size`/`position` overrides recorded on scale). Numbers unchanged. | | 2026-10-28 | minor | M6, #1059: `.fig` import copies embedded image blobs once per blob, not once per hash naming them. Numbers unchanged. | diff --git a/crates/ui-egui/locales/ar.toml b/crates/ui-egui/locales/ar.toml index f252e114..88489ccf 100644 --- a/crates/ui-egui/locales/ar.toml +++ b/crates/ui-egui/locales/ar.toml @@ -1204,7 +1204,6 @@ status = "draft — needs native review" "Resolve" = "حل" "Resolve change" = "حل التغيير" "Resolved" = "محلول" -"Resources" = "الموارد" "Responsive" = "متجاوب" "Restart" = "إعادة البدء" "Restore" = "استعادة" diff --git a/crates/ui-egui/locales/bn.toml b/crates/ui-egui/locales/bn.toml index f9bdc180..b98051ef 100644 --- a/crates/ui-egui/locales/bn.toml +++ b/crates/ui-egui/locales/bn.toml @@ -1164,7 +1164,6 @@ status = "draft — needs native review" "Resolve" = "সমাধান করুন" "Resolve change" = "পরিবর্তন সমাধান করুন" "Resolved" = "সমাধান করা হয়েছে" -"Resources" = "রিসোর্সসমূহ" "Responsive" = "রেসপনসিভ" "Restart" = "পুনরায় শুরু করুন" "Restore" = "পুনরুদ্ধার করুন" diff --git a/crates/ui-egui/locales/cs.toml b/crates/ui-egui/locales/cs.toml index f8f86df9..b603fe5d 100644 --- a/crates/ui-egui/locales/cs.toml +++ b/crates/ui-egui/locales/cs.toml @@ -1142,7 +1142,6 @@ status = "draft — needs native review" "Resolve" = "Vyřešit" "Resolve change" = "Vyřešit změnu" "Resolved" = "Vyřešené" -"Resources" = "Zdroje" "Responsive" = "Responzivní" "Restart" = "Znovu spustit" "Restore" = "Obnovit" diff --git a/crates/ui-egui/locales/de.toml b/crates/ui-egui/locales/de.toml index 127900c9..700960a5 100644 --- a/crates/ui-egui/locales/de.toml +++ b/crates/ui-egui/locales/de.toml @@ -1112,7 +1112,6 @@ status = "draft — needs native review" "Resolve" = "Erledigen" "Resolve change" = "Änderung erledigen" "Resolved" = "Erledigt" -"Resources" = "Ressourcen" "Responsive" = "Responsiv" "Restart" = "Neu starten" "Restore" = "Wiederherstellen" diff --git a/crates/ui-egui/locales/en.toml b/crates/ui-egui/locales/en.toml index 8c1442a4..87fa9f1d 100644 --- a/crates/ui-egui/locales/en.toml +++ b/crates/ui-egui/locales/en.toml @@ -1202,7 +1202,6 @@ name = "English" "Resolve" = "Resolve" "Resolve change" = "Resolve change" "Resolved" = "Resolved" -"Resources" = "Resources" "Responsive" = "Responsive" "Restart" = "Restart" "Restore" = "Restore" diff --git a/crates/ui-egui/locales/es.toml b/crates/ui-egui/locales/es.toml index dd6b4ed1..4e91bb9b 100644 --- a/crates/ui-egui/locales/es.toml +++ b/crates/ui-egui/locales/es.toml @@ -1198,7 +1198,6 @@ status = "draft — needs native review" "Resolve" = "Resolver" "Resolve change" = "Resolver cambio" "Resolved" = "Resuelto" -"Resources" = "Recursos" "Responsive" = "Adaptable" "Restart" = "Reiniciar" "Restore" = "Restaurar" diff --git a/crates/ui-egui/locales/fi.toml b/crates/ui-egui/locales/fi.toml index f1a3bbc0..13f30246 100644 --- a/crates/ui-egui/locales/fi.toml +++ b/crates/ui-egui/locales/fi.toml @@ -1168,7 +1168,6 @@ status = "draft — needs native review" "Resolve" = "Ratkaise" "Resolve change" = "Ratkaise muutos" "Resolved" = "Ratkaistu" -"Resources" = "Resurssit" "Responsive" = "Mukautuva" "Restart" = "Aloita uudelleen" "Restore" = "Palauta" diff --git a/crates/ui-egui/locales/fr.toml b/crates/ui-egui/locales/fr.toml index 7c80a3e3..b290d396 100644 --- a/crates/ui-egui/locales/fr.toml +++ b/crates/ui-egui/locales/fr.toml @@ -1169,7 +1169,6 @@ status = "draft — needs native review" "Resolve" = "Résoudre" "Resolve change" = "Résoudre la modification" "Resolved" = "Résolu" -"Resources" = "Ressources" "Responsive" = "Adaptatif" "Restart" = "Recommencer" "Restore" = "Restaurer" diff --git a/crates/ui-egui/locales/id.toml b/crates/ui-egui/locales/id.toml index b1755027..1610e6e5 100644 --- a/crates/ui-egui/locales/id.toml +++ b/crates/ui-egui/locales/id.toml @@ -1148,7 +1148,6 @@ status = "draft — needs native review" "Resolve" = "Selesaikan" "Resolve change" = "Selesaikan perubahan" "Resolved" = "Terselesaikan" -"Resources" = "Sumber daya" "Responsive" = "Responsif" "Restart" = "Mulai ulang" "Restore" = "Pulihkan" diff --git a/crates/ui-egui/locales/it.toml b/crates/ui-egui/locales/it.toml index d31ab7b6..5af1304a 100644 --- a/crates/ui-egui/locales/it.toml +++ b/crates/ui-egui/locales/it.toml @@ -1128,7 +1128,6 @@ status = "draft — needs native review" "Resolve" = "Risolvi" "Resolve change" = "Risolvi la modifica" "Resolved" = "Risolto" -"Resources" = "Risorse" "Responsive" = "Responsive" "Restart" = "Riavvia" "Restore" = "Ripristina" diff --git a/crates/ui-egui/locales/ja.toml b/crates/ui-egui/locales/ja.toml index ec117721..b64ef900 100644 --- a/crates/ui-egui/locales/ja.toml +++ b/crates/ui-egui/locales/ja.toml @@ -1156,7 +1156,6 @@ status = "draft — needs native review" "Resolve" = "解決" "Resolve change" = "変更を解決" "Resolved" = "解決済み" -"Resources" = "リソース" "Responsive" = "レスポンシブ" "Restart" = "再起動" "Restore" = "復元" diff --git a/crates/ui-egui/locales/ka.toml b/crates/ui-egui/locales/ka.toml index b5d19efb..9d2bddc1 100644 --- a/crates/ui-egui/locales/ka.toml +++ b/crates/ui-egui/locales/ka.toml @@ -1179,7 +1179,6 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv "Resolve" = "გადაწყვეტა" "Resolve change" = "ცვლილების გადაწყვეტა" "Resolved" = "გადაწყვეტილია" -"Resources" = "რესურსები" "Responsive" = "ადაპტური" "Restart" = "გადატვირთვა" "Restore" = "აღდგენა" diff --git a/crates/ui-egui/locales/ko.toml b/crates/ui-egui/locales/ko.toml index 397d9507..0b4bc210 100644 --- a/crates/ui-egui/locales/ko.toml +++ b/crates/ui-egui/locales/ko.toml @@ -1106,7 +1106,6 @@ status = "draft — needs native review" "Resolve" = "해결" "Resolve change" = "변경 사항 해결" "Resolved" = "해결됨" -"Resources" = "리소스" "Responsive" = "반응형" "Restart" = "다시 시작" "Restore" = "복원" diff --git a/crates/ui-egui/locales/nl.toml b/crates/ui-egui/locales/nl.toml index d9905f2c..8399482b 100644 --- a/crates/ui-egui/locales/nl.toml +++ b/crates/ui-egui/locales/nl.toml @@ -1168,7 +1168,6 @@ status = "draft — needs native review" "Resolve" = "Oplossen" "Resolve change" = "Wijziging oplossen" "Resolved" = "Opgelost" -"Resources" = "Bronnen" "Responsive" = "Responsive" "Restart" = "Opnieuw starten" "Restore" = "Terugzetten" diff --git a/crates/ui-egui/locales/pl.toml b/crates/ui-egui/locales/pl.toml index c935fca1..03b16de2 100644 --- a/crates/ui-egui/locales/pl.toml +++ b/crates/ui-egui/locales/pl.toml @@ -1172,7 +1172,6 @@ name = "Polski" "Resolve" = "Rozwiąż" "Resolve change" = "Rozwiąż zmianę" "Resolved" = "Rozwiązany" -"Resources" = "Zasoby" "Responsive" = "Responsywny" "Restart" = "Uruchom ponownie" "Restore" = "Przywróć" diff --git a/crates/ui-egui/locales/pt-BR.toml b/crates/ui-egui/locales/pt-BR.toml index 16d6eab7..d641aba5 100644 --- a/crates/ui-egui/locales/pt-BR.toml +++ b/crates/ui-egui/locales/pt-BR.toml @@ -1105,7 +1105,6 @@ status = "draft — needs native review" "Resolve" = "Resolver" "Resolve change" = "Resolver alteração" "Resolved" = "Resolvido" -"Resources" = "Recursos" "Responsive" = "Responsivo" "Restart" = "Reiniciar" "Restore" = "Restaurar" diff --git a/crates/ui-egui/locales/ru.toml b/crates/ui-egui/locales/ru.toml index d25f7d14..ad1ae8f0 100644 --- a/crates/ui-egui/locales/ru.toml +++ b/crates/ui-egui/locales/ru.toml @@ -1172,7 +1172,6 @@ name = "Русский" "Resolve" = "Решить" "Resolve change" = "Разрешить изменение" "Resolved" = "Решённые" -"Resources" = "Ресурсы" "Responsive" = "Адаптивно" "Restart" = "Начать заново" "Restore" = "Восстановить" diff --git a/crates/ui-egui/locales/sv.toml b/crates/ui-egui/locales/sv.toml index 8718b060..5f9d1e14 100644 --- a/crates/ui-egui/locales/sv.toml +++ b/crates/ui-egui/locales/sv.toml @@ -1199,7 +1199,6 @@ name = "Svenska" "Resolve" = "Lös" "Resolve change" = "Lös ändring" "Resolved" = "Löst" -"Resources" = "Resurser" "Responsive" = "Responsiv" "Restart" = "Starta om" "Restore" = "Återställ" diff --git a/crates/ui-egui/locales/th.toml b/crates/ui-egui/locales/th.toml index 560975a1..60b2c6eb 100644 --- a/crates/ui-egui/locales/th.toml +++ b/crates/ui-egui/locales/th.toml @@ -1161,7 +1161,6 @@ status = "draft — needs native review" "Resolve" = "แก้ไขแล้ว" "Resolve change" = "แก้ไขการเปลี่ยนแปลง" "Resolved" = "แก้ไขแล้ว" -"Resources" = "แหล่งข้อมูล" "Responsive" = "รีสปอนซีฟ" "Restart" = "เริ่มใหม่" "Restore" = "กู้คืน" diff --git a/crates/ui-egui/locales/tr.toml b/crates/ui-egui/locales/tr.toml index a36e21d2..7a640563 100644 --- a/crates/ui-egui/locales/tr.toml +++ b/crates/ui-egui/locales/tr.toml @@ -1146,7 +1146,6 @@ name = "Türkçe" "Resolve" = "Çöz" "Resolve change" = "Değişikliği çöz" "Resolved" = "Çözüldü" -"Resources" = "Kaynaklar" "Responsive" = "Duyarlı" "Restart" = "Yeniden başlat" "Restore" = "Geri yükle" diff --git a/crates/ui-egui/locales/vi.toml b/crates/ui-egui/locales/vi.toml index e130afbf..0b2b2280 100644 --- a/crates/ui-egui/locales/vi.toml +++ b/crates/ui-egui/locales/vi.toml @@ -1129,7 +1129,6 @@ status = "draft — needs native review" "Resolve" = "Giải quyết" "Resolve change" = "Giải quyết thay đổi" "Resolved" = "Đã giải quyết" -"Resources" = "Tài nguyên" "Responsive" = "Đáp ứng" "Restart" = "Bắt đầu lại" "Restore" = "Khôi phục" diff --git a/crates/ui-egui/locales/zh-Hans.toml b/crates/ui-egui/locales/zh-Hans.toml index 1081afad..2b78f8d3 100644 --- a/crates/ui-egui/locales/zh-Hans.toml +++ b/crates/ui-egui/locales/zh-Hans.toml @@ -1114,7 +1114,6 @@ status = "draft — needs native review" "Resolve" = "解决" "Resolve change" = "解决更改" "Resolved" = "已解决" -"Resources" = "资源" "Responsive" = "响应式" "Restart" = "重新开始" "Restore" = "恢复" diff --git a/crates/ui-egui/src/board_picker_tests.rs b/crates/ui-egui/src/board_picker_tests.rs index c5860525..e5c1666b 100644 --- a/crates/ui-egui/src/board_picker_tests.rs +++ b/crates/ui-egui/src/board_picker_tests.rs @@ -27,7 +27,7 @@ fn board() -> Ui { /// The templates button: left of the Design panel's button at the board toolbar's right end. fn templates_button(u: &Ui) -> Pos2 { let p = crate::toolbar::pill_rect(u.app.canvas.rect, crate::toolbar::bar_width(&u.app)); - pos2(p.max.x - 8.0 - 32.0 - 4.0 - 16.0, p.center().y) + pos2(p.max.x - 8.0 - 32.0 - 8.0 - 16.0, p.center().y) } fn count(u: &Ui, f: impl Fn(&BoardItem) -> bool) -> usize { diff --git a/crates/ui-egui/src/board_ui_tests.rs b/crates/ui-egui/src/board_ui_tests.rs index e8592467..e4758ca7 100644 --- a/crates/ui-egui/src/board_ui_tests.rs +++ b/crates/ui-egui/src/board_ui_tests.rs @@ -39,10 +39,10 @@ fn pill(u: &Ui) -> Rect { } /// The left edge of board toolbar group `i` (button 32 wide, a 16-wide caret 2 after it on the -/// groups with flyouts, 4 between): move, ink, sticky, shapes, connector, text, section, stamp, +/// groups with flyouts, 8 between): move, ink, sticky, shapes, connector, text, section, stamp, /// comment. fn group_x(u: &Ui, i: usize) -> f32 { - const WIDTHS: [f32; 9] = [54.0, 54.0, 54.0, 54.0, 36.0, 36.0, 36.0, 54.0, 36.0]; + const WIDTHS: [f32; 9] = [58.0, 58.0, 58.0, 58.0, 40.0, 40.0, 40.0, 58.0, 40.0]; pill(u).min.x + 8.0 + WIDTHS.iter().take(i).sum::() } diff --git a/crates/ui-egui/src/resources_tests.rs b/crates/ui-egui/src/resources_tests.rs index 42869098..d105f24d 100644 --- a/crates/ui-egui/src/resources_tests.rs +++ b/crates/ui-egui/src/resources_tests.rs @@ -1,19 +1,14 @@ -//! The toolbar's Resources button and Shift+I open the Assets panel (#710), from a collapsed -//! sidebar and from the version history too. +//! Shift+I opens the Assets panel (#710), from a collapsed sidebar and from the version history +//! too — and the toolbar has no Resources button for it any more (the left rail's Assets tab, +//! Shift+I, Alt+2 and View ▸ Panels ▸ Assets are the ways in). use egui::accesskit::Role; -use egui::{Event, Key, Modifiers, Pos2, pos2}; +use egui::{Event, Key, Modifiers}; use figcraft_engine::Session; use crate::LeftTab; use crate::layers_drag_tests::Ui; -fn click(u: &mut Ui, p: Pos2) { - u.button(p, true); - u.button(p, false); - u.frame(vec![]); -} - fn shift_i(u: &mut Ui) { let modifiers = Modifiers::SHIFT; u.frame(vec![ @@ -23,22 +18,11 @@ fn shift_i(u: &mut Ui) { u.frame(vec![]); } -/// The middle of the toolbar's Resources button, found in the AccessKit tree. -fn resources_button(u: &mut Ui) -> Pos2 { - u.ctx.enable_accesskit(); - u.frame(vec![]); - let out = u.frame_output(vec![]); - let nodes = out.platform_output.accesskit_update.expect("an AccessKit update").nodes; - let n = nodes.iter().map(|(_, n)| n).find(|n| n.role() == Role::Button && n.label() == Some("Resources")).expect("Resources"); - let b = n.bounds().expect("bounds"); - pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32) -} - fn assets_shown(u: &Ui) -> bool { u.app.ui.left_tab == LeftTab::Assets && !u.app.ui.left_collapsed && !u.app.ui.versions.open } -/// The three states the sidebar can be in when Resources is asked for. +/// The three states the sidebar can be in when Shift+I is pressed. fn starts() -> Vec<(&'static str, Ui)> { let shown = Ui::new(Session::new()); let mut collapsed = Ui::new(Session::new()); @@ -48,15 +32,6 @@ fn starts() -> Vec<(&'static str, Ui)> { vec![("layers", shown), ("collapsed", collapsed), ("version history", versions)] } -#[test] -fn the_resources_button_opens_the_assets_panel() { - for (name, mut u) in starts() { - let at = resources_button(&mut u); - click(&mut u, at); - assert!(assets_shown(&u), "{name}: the Assets panel is open"); - } -} - #[test] fn shift_i_opens_the_assets_panel() { for (name, mut u) in starts() { @@ -65,3 +40,27 @@ fn shift_i_opens_the_assets_panel() { assert!(assets_shown(&u), "{name}: the Assets panel is open"); } } + +/// The toolbar's buttons, as screen readers get them. +fn toolbar_labels(u: &mut Ui) -> Vec { + u.ctx.enable_accesskit(); + u.frame(vec![]); + let out = u.frame_output(vec![]); + out.platform_output + .accesskit_update + .expect("an AccessKit update") + .nodes + .iter() + .filter(|(_, n)| n.role() == Role::Button) + .filter_map(|(_, n)| n.label().map(str::to_string)) + .collect() +} + +#[test] +fn the_toolbar_has_no_resources_button() { + let mut u = Ui::new(Session::new()); + let labels = toolbar_labels(&mut u); + // Actions stays (the palette's button); Resources went (#1210). + assert!(labels.iter().any(|l| l == "Actions"), "Actions is in the toolbar: {labels:?}"); + assert!(!labels.iter().any(|l| l == "Resources"), "no Resources button: {labels:?}"); +} diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index e8832802..c5955ec0 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -73,11 +73,6 @@ pub fn tip(label: &'static str, id: &str) -> &'static str { cached_tip(label, shortcut(id)) } -/// A tooltip naming a key ("Resources ⇧I"). -pub fn key_tip(label: &'static str, key: &'static str) -> &'static str { - cached_tip(label, Some(key)) -} - /// Panels ask for their tooltips every frame: each (label, key, platform) is built once. fn cached_tip(label: &'static str, key: Option<&'static str>) -> &'static str { type Entry = (Platform, &'static str, &'static str, &'static str); diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index a5795a8c..dcb0ccf0 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -21,6 +21,19 @@ const CARET: f32 = 14.0; const FLYOUT_W: f32 = 16.0; const FLYOUT_GAP: f32 = 2.0; +// The pill's grid: every state of it (the tools, the vector and shape builder ones) is laid out on +// one 32 pt cell, PAD in from its ends and GAP between its top-level items, the divider included. +/// A toolbar button's size: the cell everything in the pill is placed on. +const TOOL_CELL: f32 = 32.0; +/// The pill's left and right padding. +const PAD: f32 = 8.0; +/// Between the pill's top-level items — the divider included. +const GAP: f32 = 8.0; +/// The divider's width. +const DIVIDER: f32 = 1.0; +/// The mode switcher's marks: smaller than the tools' (13–14 pt). +const MODE_GLYPH: f32 = 14.0; + /// `GLYPH`'s box centred in `r`. fn glyph_rect(r: Rect) -> Rect { Rect::from_center_size(r.center(), vec2(GLYPH, GLYPH)) @@ -129,17 +142,18 @@ pub(crate) fn bar_width(app: &FigApp) -> f32 { main_width(if board { BOARD_GROUPS } else { GROUPS }, board) } -/// The main toolbar pill's width: the tool groups with their flyout carets, Resources and -/// Actions, and the mode switcher (boards: the Design panel's button instead). +/// The main toolbar pill's width: the tool groups with their flyout carets, Actions, the divider +/// and the mode switcher (boards: the templates and the Design panel's buttons instead). fn main_width(groups: &[Group], board: bool) -> f32 { - let (cell, gap) = (TOOL_CELL, 4.0); - let carets = groups.iter().filter(|g| g.has_flyout()).count() as f32 * (FLYOUT_GAP + FLYOUT_W); - let end = if board { 2.0 * cell + gap + 16.0 } else { 4.0 * 34.0 + 24.0 }; - groups.len() as f32 * (cell + gap) + carets + 2.0 * (cell + gap) + 16.0 + end + let tools: f32 = groups.iter().map(|g| TOOL_CELL + if g.has_flyout() { FLYOUT_GAP + FLYOUT_W } else { 0.0 }).sum(); + // The end, and the gaps that aren't a group's own: after Actions and after the divider (and + // between a board's two buttons, which the end counts as one item). + let (end, gaps) = if board { (2.0 * TOOL_CELL, 3.0) } else { (4.0 * TOOL_CELL, 2.0) }; + PAD * 2.0 + tools + (groups.len() as f32 + gaps) * GAP + TOOL_CELL + DIVIDER + end } -/// The main toolbar pill: the tool groups with their flyouts, Resources and Actions, then the -/// Draw · Design · Prototype · Inspect mode switcher (on a board, the Design panel's button). +/// The main toolbar pill: the tool groups with their flyouts, Actions, then the +/// Draw · Design · Prototype · Inspect mode switcher (on a board, its two buttons). fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens, board: bool) { let groups = if board { BOARD_GROUPS } else { GROUPS }; let width = main_width(groups, board); @@ -148,16 +162,16 @@ fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens, board: let scroll = pill_scroll(ui, rect, width); let outer_clip = ui.clip_rect(); ui.set_clip_rect(rect.intersect(outer_clip)); - let mut x = rect.min.x + 8.0 - scroll; + let mut x = rect.min.x + PAD - scroll; for g in groups { x = group_cell(app, ui, g, pos2(x, cy), rect.min.y, t); } - x = side_buttons(app, ui, x, cy, t); + x = actions_button(app, ui, x, cy, t); divider(ui, t, x, cy); - x += 8.0; + x += DIVIDER + GAP; if board { // Boards have no prototypes or Dev Mode; the Design panel is hidden until asked for. - x = crate::board_picker::button(app, ui, pos2(x, cy), rect.min.y, t); + x = crate::board_picker::button(app, ui, pos2(x, cy), rect.min.y, t) + GAP; let r = Rect::from_center_size(pos2(x + TOOL_CELL / 2.0, cy), vec2(TOOL_CELL, TOOL_CELL)); let tip = crate::shortcuts::tip(tr("Design panel"), "view.design"); if tool_cell(ui, t, r, "board_panel", Icon::Panels, app.ui.board_panel, tip).clicked() { @@ -185,9 +199,6 @@ fn pill_scroll(ui: &Ui, rect: Rect, width: f32) -> f32 { scroll } -/// A toolbar button's size. -const TOOL_CELL: f32 = 32.0; - /// Tool group `g` at `at` (its left edge, the pill's middle): the shown tool's button, its flyout /// caret and, while open, its flyout above the pill (whose top is `top`). Where the next starts. fn group_cell(app: &mut FigApp, ui: &mut Ui, g: &Group, at: egui::Pos2, top: f32, t: &Tokens) -> f32 { @@ -223,7 +234,7 @@ fn group_cell(app: &mut FigApp, ui: &mut Ui, g: &Group, at: egui::Pos2, top: f32 options_flyout(app, ui, g, pos2(r.min.x, top - 8.0)); } } - x + 4.0 + x + GAP } /// The caret after group `g`'s button (at `at`, its left edge and middle): opens or closes its @@ -245,36 +256,23 @@ fn flyout_caret(app: &mut FigApp, ui: &mut Ui, g: &Group, at: egui::Pos2, t: &To } } -/// Resources (components, plugins: the Assets panel) and the Actions palette's button, from `x`. -/// Where the next part starts. -fn side_buttons(app: &mut FigApp, ui: &mut Ui, mut x: f32, cy: f32, t: &Tokens) -> f32 { - for (icon, tip, id) in [ - (Icon::Assets, crate::shortcuts::key_tip(tr("Resources"), "Shift+I"), "resources"), - (Icon::Actions, crate::shortcuts::tip(tr("Actions"), "app.actions"), "actions"), - ] { - let r = Rect::from_center_size(pos2(x + TOOL_CELL / 2.0, cy), vec2(TOOL_CELL, TOOL_CELL)); - let resp = ui.interact(r, ui.scope_id().with(id), Sense::click()).tip(tip); - if id == "actions" { - app.ui.actions_rect = Some(r); - } - // The Actions button reads on while its panel is open; clicking it again closes it. - let actions_open = id == "actions" && app.ui.palette.is_some(); - if actions_open { - ui.painter().rect_filled(r, CornerRadius::same(5), t.accent); - } else if resp.hovered() { - ui.painter().rect_filled(r, CornerRadius::same(5), t.hover); - } - icons::paint(ui.painter(), glyph_rect(r), icon, if actions_open { t.accent_on } else { t.icon }); - if resp.clicked() { - if id == "actions" { - crate::menus::toggle_palette(&mut app.ui); - } else { - app.run("view.assets", json!({})); - } - } - x += TOOL_CELL + 4.0; +/// The Actions palette's button, from `x`. Where the next part starts. +fn actions_button(app: &mut FigApp, ui: &mut Ui, x: f32, cy: f32, t: &Tokens) -> f32 { + let r = Rect::from_center_size(pos2(x + TOOL_CELL / 2.0, cy), vec2(TOOL_CELL, TOOL_CELL)); + let resp = ui.interact(r, ui.scope_id().with("actions"), Sense::click()).tip(crate::shortcuts::tip(tr("Actions"), "app.actions")); + app.ui.actions_rect = Some(r); + // The Actions button reads on while its panel is open; clicking it again closes it. + let open = app.ui.palette.is_some(); + if open { + ui.painter().rect_filled(r, CornerRadius::same(5), t.accent); + } else if resp.hovered() { + ui.painter().rect_filled(r, CornerRadius::same(5), t.hover); } - x + icons::paint(ui.painter(), glyph_rect(r), Icon::Actions, if open { t.accent_on } else { t.icon }); + if resp.clicked() { + crate::menus::toggle_palette(&mut app.ui); + } + x + TOOL_CELL + GAP } /// The mode switcher from `x`: Draw · Design · Prototype · Inspect (code). @@ -286,19 +284,24 @@ fn mode_switcher(app: &mut FigApp, ui: &mut Ui, x: f32, cy: f32, t: &Tokens) { crate::RightTab::Prototype => 2, crate::RightTab::Inspect => 3, }; - let seg = Rect::from_min_size(pos2(x, cy - 18.0), vec2(4.0 * 34.0 + 4.0, 36.0)); - ui.painter().rect_filled(seg, CornerRadius::same(8), t.field); + let seg = Rect::from_min_size(pos2(x, cy - TOOL_CELL / 2.0), vec2(4.0 * TOOL_CELL, TOOL_CELL)); + ui.painter().rect_filled(seg, CornerRadius::same(5), t.field); for (i, (icon, tip)) in modes.iter().enumerate() { - let r = Rect::from_min_size(pos2(seg.min.x + 2.0 + i as f32 * 34.0, seg.min.y + 2.0), vec2(34.0, 32.0)); + let r = Rect::from_min_size(pos2(seg.min.x + i as f32 * TOOL_CELL, seg.min.y), vec2(TOOL_CELL, TOOL_CELL)); let resp = ui.interact(r, ui.scope_id().with(("mode", i)), Sense::click()).on_hover_text(*tip); a11y::toggle(&resp, tip, i == active); if i == active { - ui.painter().rect_filled(r, CornerRadius::same(6), t.panel); - ui.painter().rect_stroke(r, CornerRadius::same(6), Stroke::new(1.0, t.border), StrokeKind::Inside); + // The current mode lifts with a plain inset, no outline. + ui.painter().rect_filled(r.shrink(2.0), CornerRadius::same(3), t.panel); } else if resp.hovered() { - ui.painter().rect_filled(r, CornerRadius::same(6), t.hover); + ui.painter().rect_filled(r, CornerRadius::same(5), t.hover); } - icons::paint(ui.painter(), glyph_rect(r), *icon, if i == active { t.accent_text } else { t.icon_secondary }); + icons::paint( + ui.painter(), + Rect::from_center_size(r.center(), vec2(MODE_GLYPH, MODE_GLYPH)), + *icon, + if i == active { t.accent_text } else { t.icon_secondary }, + ); if resp.clicked() { pick_mode(app, i); } @@ -359,15 +362,14 @@ fn view_only_toolbar(app: &mut FigApp, ui: &mut Ui, why: &'static str) { app.run("tool.set", json!({"tool": "move"})); } let label = if why == figcraft_engine::live::PUBLISHING { tr("Publishing…") } else { tr("View only") }; - let cell = 32.0; let text = ui.painter().layout_no_wrap(label.to_string(), crate::theme::semibold(11.5), t.text); let badge_w = 12.0 + 14.0 + 6.0 + text.size().x + 12.0; - let width = 2.0 * (cell + 4.0) + 8.0 + 12.0 + badge_w + 8.0; + let width = PAD * 2.0 + 2.0 * (TOOL_CELL + GAP) + DIVIDER + GAP + badge_w; let rect = pill(ui, &mut app.ui.nav, app.canvas.rect, width, &t); let cy = rect.center().y; - let mut x = rect.min.x + 8.0; + let mut x = rect.min.x + PAD; for (tool, icon) in [(ToolId::Move, Icon::Move), (ToolId::Hand, Icon::Hand)] { - let r = Rect::from_center_size(pos2(x + cell / 2.0, cy), vec2(cell, cell)); + let r = Rect::from_center_size(pos2(x + TOOL_CELL / 2.0, cy), vec2(TOOL_CELL, TOOL_CELL)); let active = app.session.tool.tool == tool; let resp = tool_cell(ui, &t, r, ("tool", tool.id()), icon, active, &format!("{} {}", tr(tool.label()), pretty_shortcut(tool.shortcut()))); if active { @@ -376,10 +378,10 @@ fn view_only_toolbar(app: &mut FigApp, ui: &mut Ui, why: &'static str) { if resp.clicked() { app.run("tool.set", json!({"tool": tool.id()})); } - x += cell + 4.0; + x += TOOL_CELL + GAP; } divider(ui, &t, x, cy); - let badge = Rect::from_min_size(pos2(x + 12.0, cy - 13.0), vec2(badge_w, 26.0)); + let badge = Rect::from_min_size(pos2(x + DIVIDER + GAP, cy - 13.0), vec2(badge_w, 26.0)); let resp = ui.interact(badge, ui.scope_id().with("view_only"), Sense::hover()).on_hover_text(why); a11y::status(&resp, label, why); ui.painter().rect_filled(badge, CornerRadius::same(13), t.field); @@ -391,15 +393,14 @@ fn view_only_toolbar(app: &mut FigApp, ui: &mut Ui, why: &'static str) { fn edit_toolbar(app: &mut FigApp, ui: &mut Ui, current: VTool) { let t = Tokens::get(ui.ctx()); let canvas = app.canvas.rect; - let cell = 32.0; - let width = VTool::ALL.len() as f32 * (cell + 4.0) + 8.0 + 12.0 + DONE_W + 8.0; + let width = VTool::ALL.len() as f32 * (TOOL_CELL + GAP) + PAD * 2.0 + DIVIDER + GAP + DONE_W; let rect = pill(ui, &mut app.ui.nav, canvas, width, &t); let cy = rect.center().y; - let mut x = rect.min.x + 8.0; + let mut x = rect.min.x + PAD; // Holding Cmd with the Move tool bends: show it. let held = current == VTool::Move && ui.input(|i| i.modifiers.command); for tool in VTool::ALL { - let r = Rect::from_center_size(pos2(x + cell / 2.0, cy), vec2(cell, cell)); + let r = Rect::from_center_size(pos2(x + TOOL_CELL / 2.0, cy), vec2(TOOL_CELL, TOOL_CELL)); let tip = if tool == VTool::Bend { trf("{tool} hold {key}", &[("tool", &tr(tool.label())), ("key", &pretty_shortcut(tool.shortcut()))]) } else { @@ -423,7 +424,7 @@ fn edit_toolbar(app: &mut FigApp, ui: &mut Ui, current: VTool) { if resp.clicked() { app.run("vector.edit.tool", json!({"tool": tool.id()})); } - x += cell + 4.0; + x += TOOL_CELL + GAP; } if done_button(ui, &t, x, cy, &format!("{} Esc", tr("Done editing"))) { app.run("vector.edit.end", json!({})); @@ -433,7 +434,6 @@ fn edit_toolbar(app: &mut FigApp, ui: &mut Ui, current: VTool) { /// The pill shown while the shape builder is active: Merge and Remove modes, a hint and Done. fn builder_toolbar(app: &mut FigApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); - let cell = 32.0; let remove = app.session.tool.builder_remove; let has_regions = figcraft_engine::shapebuilder::current(&app.session).is_some_and(|a| !a.regions.is_empty()); let alt = pretty_shortcut("Alt"); @@ -445,12 +445,12 @@ fn builder_toolbar(app: &mut FigApp, ui: &mut Ui) { trf("Click or drag to merge · {alt} removes", &[("alt", &alt)]) }; let galley = ui.painter().layout_no_wrap(hint, regular(12.0), t.text_secondary); - let width = 2.0 * (cell + 4.0) + 8.0 + 12.0 + galley.size().x + 12.0 + 12.0 + DONE_W + 8.0; + let width = PAD * 2.0 + 2.0 * (TOOL_CELL + GAP) + 2.0 * (DIVIDER + GAP) + galley.size().x + GAP + DONE_W; let rect = pill(ui, &mut app.ui.nav, app.canvas.rect, width, &t); let cy = rect.center().y; - let mut x = rect.min.x + 8.0; + let mut x = rect.min.x + PAD; for (mode, icon, tip) in [("merge", Icon::ShapeBuilder, tr("Merge regions")), ("remove", Icon::ShapeBuilderRemove, tr("Remove regions"))] { - let r = Rect::from_center_size(pos2(x + cell / 2.0, cy), vec2(cell, cell)); + let r = Rect::from_center_size(pos2(x + TOOL_CELL / 2.0, cy), vec2(TOOL_CELL, TOOL_CELL)); let active = (mode == "remove") == remove; let resp = tool_cell(ui, &t, r, ("sbmode", mode), icon, active, tip); if active { @@ -459,13 +459,13 @@ fn builder_toolbar(app: &mut FigApp, ui: &mut Ui) { if resp.clicked() { app.run("shapeBuilder.mode", json!({"mode": mode})); } - x += cell + 4.0; + x += TOOL_CELL + GAP; } divider(ui, &t, x, cy); - x += 12.0; + x += DIVIDER + GAP; let w = galley.size().x; ui.painter().galley(pos2(x, cy - galley.size().y / 2.0), galley, t.text_secondary); - x += w + 12.0; + x += w + GAP; if done_button(ui, &t, x, cy, &format!("{} {}", tr("Done"), pretty_shortcut("Enter"))) { app.run("tool.set", json!({"tool": "move"})); } @@ -515,9 +515,10 @@ pub(crate) fn tool_cell(ui: &mut Ui, t: &Tokens, r: Rect, id: impl egui::AsIdSal resp } -/// A vertical separator just after `x`. +/// A vertical separator just after `x`: DIVIDER wide, as tall as the pill's content. fn divider(ui: &Ui, t: &Tokens, x: f32, cy: f32) { - ui.painter().line_segment([pos2(x + 2.0, cy - 14.0), pos2(x + 2.0, cy + 14.0)], Stroke::new(1.0, t.border)); + let half = TOOL_CELL / 2.0; + ui.painter().line_segment([pos2(x + DIVIDER / 2.0, cy - half), pos2(x + DIVIDER / 2.0, cy + half)], Stroke::new(DIVIDER, t.border)); } const DONE_W: f32 = 56.0; @@ -525,7 +526,7 @@ const DONE_W: f32 = 56.0; /// A divider at `x` and the accent Done button after it; true when clicked. fn done_button(ui: &mut Ui, t: &Tokens, x: f32, cy: f32, tip: &str) -> bool { divider(ui, t, x, cy); - let done = Rect::from_min_size(pos2(x + 12.0, cy - 14.0), vec2(DONE_W, 28.0)); + let done = Rect::from_min_size(pos2(x + DIVIDER + GAP, cy - 14.0), vec2(DONE_W, 28.0)); let resp = ui.interact(done, ui.scope_id().with("tool_done"), Sense::click()).tip(tip); ui.painter().rect_filled(done, CornerRadius::same(6), if resp.hovered() { t.accent_hover } else { t.accent }); ui.painter().text(done.center(), Align2::CENTER_CENTER, tr("Done"), crate::theme::semibold(11.5), t.accent_on); @@ -642,12 +643,33 @@ mod tests { let phone = Rect::from_min_size(pos2(0.0, 0.0), vec2(375.0, 700.0)); let pill = pill_rect(phone, full); assert!(pill.width() <= phone.width() - 16.0 && pill.min.x >= 8.0 && pill.max.x <= phone.max.x - 8.0, "{pill:?}"); + // Narrower than its items, so it scrolls under a drag or the wheel (#1209). + assert!(full > pill.width(), "{full} in {}", pill.width()); // A desktop canvas keeps the full width, and a degenerate one doesn't go negative. let wide = Rect::from_min_size(pos2(0.0, 0.0), vec2(1400.0, 800.0)); assert_eq!(pill_rect(wide, full).width(), full); assert_eq!(pill_rect(Rect::from_min_size(pos2(0.0, 0.0), vec2(4.0, 4.0)), full).width(), 0.0); } + /// The width the pill asks for is the width its items take — the groups, Actions, the divider + /// and the end (the mode switcher, or a board's two buttons), PAD in from either end and one + /// GAP apart — so its last item ends exactly PAD from its right edge. + #[test] + fn the_pill_is_as_wide_as_its_items() { + let canvas = Rect::from_min_size(pos2(0.0, 0.0), vec2(1400.0, 800.0)); + for (groups, board) in [(GROUPS, false), (BOARD_GROUPS, true)] { + let pill = pill_rect(canvas, main_width(groups, board)); + let mut x = pill.min.x + PAD; + for g in groups { + x += TOOL_CELL + if g.has_flyout() { FLYOUT_GAP + FLYOUT_W } else { 0.0 } + GAP; + } + x += TOOL_CELL + GAP; // Actions + x += DIVIDER + GAP; // the divider + let end = if board { 2.0 * TOOL_CELL + GAP } else { 4.0 * TOOL_CELL }; + assert!((x + end - (pill.max.x - PAD)).abs() < 0.01, "board {board}: {} vs {}", x + end, pill.max.x - PAD); + } + } + /// The stroke widths `icon` paints with in a `size` box at `ppp` pixels per point. fn widths(ppp: f32, icon: Icon, size: f32) -> Vec { let ctx = egui::Context::default(); diff --git a/crates/ui-egui/src/widgets/board_picker.rs b/crates/ui-egui/src/widgets/board_picker.rs index 6f7e509a..766bfece 100644 --- a/crates/ui-egui/src/widgets/board_picker.rs +++ b/crates/ui-egui/src/widgets/board_picker.rs @@ -25,7 +25,8 @@ const GAP: f32 = 8.0; const TITLE_H: f32 = 26.0; /// The templates button at `at` (its left edge, the toolbar's middle) and, while open, its picker -/// above the toolbar (whose top is `top`). Where the next button starts. +/// above the toolbar (whose top is `top`). The button's right edge: the toolbar puts the next +/// button a GAP after it. pub(crate) fn button(app: &mut FigApp, ui: &mut Ui, at: Pos2, top: f32, t: &Tokens) -> f32 { let r = Rect::from_min_size(pos2(at.x, at.y - 16.0), vec2(32.0, 32.0)); let open = app.ui.tool_flyout == Some(KEY); @@ -39,7 +40,7 @@ pub(crate) fn button(app: &mut FigApp, ui: &mut Ui, at: Pos2, top: f32, t: &Toke if app.ui.tool_flyout == Some(KEY) { picker(app, ui, r, top - 8.0, t); } - r.max.x + 4.0 + r.max.x } fn area_id() -> egui::Id { diff --git a/docs/boards-parity.md b/docs/boards-parity.md index 69526a18..116295b4 100644 --- a/docs/boards-parity.md +++ b/docs/boards-parity.md @@ -27,7 +27,7 @@ all end with the same geometry. | Feature | Status | Notes | |---|---|---| | Board files: File ▸ New board, the home screen's New board card, a light page | done | `file.newBoard`; the kind shows in `inspect_document`, tabs and file rows (a note mark; design files a pen nib) | -| Board toolbar at the bottom | done | Move/Hand, Marker/Highlighter/Eraser, Sticky (colour flyout), Shapes (flyout), Connector, Text, Section, Stamp (flyout), Comment, Resources, Actions, the Design panel's button | +| Board toolbar at the bottom | done | Move/Hand, Marker/Highlighter/Eraser, Sticky (colour flyout), Shapes (flyout), Connector, Text, Section, Stamp (flyout), Comment, Actions, then the templates and the Design panel's buttons — on the same 32 pt grid as a design file's (no Resources button, as there) | | Design panel hidden by default; a contextual bar above the selection | done | colour, shape, text size (S/M/L/XL), connector straight/elbow/curved and its arrow; the Design panel opens with its button or View ▸ Design panel | | Prototyping and Dev Mode refuse on boards | done | `proto.*`, `dev.*`, `view.prototype`, `view.devMode` say why | | Sticky notes | done | a square frame (200 wide, growing taller through auto layout), 8 soft colours, a soft shadow, the note's text size shrinking as it grows (24 → 11, by characters and lines), the author's name at the bottom; a click places one and starts typing; Enter or a double-click edits; Shift+Cmd/Ctrl+Enter adds the next one beside it | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 2604cfc9..02bf1859 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (colour picker LCH and OKLCH models; an XML (Android Vector Drawable) export format; Context menus: the Menu key reaches egui 0.37 but is unbound (row done → partial); the Text on a path tool; board tool keys pinned; community fixes: the text box by dragging, the font picker keys, the pen path on leaving Design, a still canvas in vector edit mode, a scrolling pages list, colour emoji, readable new text in frames, system theme on first run; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths; a rename selects the whole name, so typing replaces it; Escape or Enter after a pen path stays on the same vector (#760); IME composition kept on through idle frames of a text edit, candidates at the caret (#747); Selection colors for mixed selections and smart-selection spacing (#753); submenus with a hover delay and a safe triangle; the Actions palette's tabs, ranked search, highlight and recents; menu items that ask for a value; editor fixes from the community batches; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** minor (the toolbar on one 32 pt grid, without its Resources button (#1210); colour picker LCH and OKLCH models; an XML (Android Vector Drawable) export format; Context menus: the Menu key reaches egui 0.37 but is unbound (row done → partial); the Text on a path tool; board tool keys pinned; community fixes: the text box by dragging, the font picker keys, the pen path on leaving Design, a still canvas in vector edit mode, a scrolling pages list, colour emoji, readable new text in frames, system theme on first run; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths; a rename selects the whole name, so typing replaces it; Escape or Enter after a pen path stays on the same vector (#760); IME composition kept on through idle frames of a text edit, candidates at the caret (#747); Selection colors for mixed selections and smart-selection spacing (#753); submenus with a hover delay and a safe triangle; the Actions palette's tabs, ranked search, highlight and recents; menu items that ask for a value; editor fixes from the community batches; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7) Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys, drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status: @@ -43,7 +43,7 @@ interaction feel not yet compared. | Rename: double click, F2 / Cmd+R, Tab to the next row; several layers open Rename layers (match, regular expressions, current name and number codes, start value, preview) | done | numbered in stacking order, pinned | | Find layers (filter by name) | done | not covered by the documentation | | Pages: add, rename, drag to reorder, duplicate ("Copy of"), delete, dividers (a blank page named with a hyphen) | done | the line between Pages and Layers drags to size the pages list, kept in the preferences; a long list scrolls in its own area and a page dropped outside the visible list stays put (#272) (`layers_behaviour_tests`) | -| Assets panel: folders from page, frames and slash names (fold away), search, thumbnails, drag onto the canvas or into frames and auto layout, Alt-drop swap, enabled libraries | done | Shift+I (Quick insert) and the toolbar's Resources open it (they didn't, #710) | +| Assets panel: folders from page, frames and slash names (fold away), search, thumbnails, drag onto the canvas or into frames and auto layout, Alt-drop swap, enabled libraries | done | Shift+I (Quick insert), Alt+2 and View ▸ Panels ▸ Assets open it; the toolbar's Resources button was removed (the left rail's Assets tab and those shortcuts are the ways in) | | Hide when publishing from the Assets panel's context menu | done | a right-click on one of this file's components (also Libraries ▸ Hide when publishing for the selection, `component.hideWhenPublishing {ids?, on?}`): the model's sign of it is the name, so hiding puts a `.` before the name and showing takes the leading `.` and `_` away (one undo step, saved and synced like a rename; a variant or a layer inside hides its whole component or set); a hidden component shows a crossed eye on its tile and a dimmer name, the menu item its check mark; the next publish leaves it out and brings it back when shown again (`hide_publishing_tests`). Library components (read-only here) have no such menu | ## Selection and transform @@ -173,6 +173,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look | Date | Change | Summary | |---|---|---| +| 2026-10-29 | minor | Toolbar (#1210): every state of the pill (tools, vector edit, shape builder, view only) is laid out on one 32 pt cell, 8 pt in from its ends and 8 pt between its items, with a 1 pt divider and 14 pt mode-switcher marks; the Resources button is gone, as in UI3 (the left rail's Assets tab, Shift+I, Alt+2 and View ▸ Panels ▸ Assets open the Assets panel); a narrow window still scrolls the pill (#1209) (`resources_tests`, `toolbar` tests). Numbers unchanged | | 2026-10-25 | minor | Context menus: the Menu key is no longer an egui limitation at 0.37 (egui-winit maps the native Menu/ContextMenu key to egui's `Key::Menu`), but FigCraft normalizes no binding for `Menu`, so it does not open a context menu yet; Shift+F10 still does, with the same behaviour. The row moves from done to partial. Numbers unchanged | | 2026-10-24 | minor | Selection colors: a row's Styles menu also lists the document's color variables, and picking one binds it to every fill and stroke the row stands for, replacing a color style there (`selection.bindColorVariable`; a text range with fills of its own takes the variable's value); a row linked to a style or variable has a Detach button that unlinks it and keeps the color (`selection.detachColor`). Still partial: no library styles or variables, no new style or variable from a row | | 2026-10-24 | minor | The Text on a path tool (`tool.textOnPath`, in the Text flyout): a click on a shape's or vector's path puts new text on it, starting at the click, with the path's fill (when one shows) and effects, as the help center's Guide to text describes; the path layer is removed, and an edit left empty puts it back with no undo step (`draw_tools_tests`, `cursor_tests`) |