diff --git a/crates/engine/src/cmd/arrange.rs b/crates/engine/src/cmd/arrange.rs index 2c088cfa..26afec87 100644 --- a/crates/engine/src/cmd/arrange.rs +++ b/crates/engine/src/cmd/arrange.rs @@ -145,21 +145,28 @@ pub fn rotate_world(doc: &mut Document, id: NodeId, pivot: Point, deg: f64) -> R Ok(()) } +/// Whether aligning `ids` would move anything: a layer alone on the page has nothing to align to. +pub fn can_align(doc: &Document, ids: &[NodeId]) -> bool { + align_target(doc, &roots(doc, ids)).is_some() +} + +/// What `roots` align to: one layer to its parent frame, several to their combined bounds. +fn align_target(doc: &Document, roots: &[NodeId]) -> Option { + match roots { + [] => None, + [id] => { + let parent = doc.node(*id)?.parent.filter(|pp| !doc.node(*pp).is_some_and(Node::is_page))?; + Some(doc.world_bounds(parent).unwrap_or_default()) + } + _ => Some(doc.bounds_of(roots).unwrap_or_default()), + } +} + fn align(s: &mut Session, p: &Value, a: Al) -> Result { let ids = req_targets("align", s, p)?; let doc = s.doc_mut()?; let ids = roots(doc, &ids); - // One layer aligns within its parent frame; several align to their combined bounds. - let target: Rect = if ids.len() == 1 { - let Some(id) = ids.first() else { return ok() }; - let parent = doc.get(*id)?.parent; - match parent.filter(|pp| !doc.node(*pp).is_some_and(Node::is_page)) { - Some(pp) => doc.world_bounds(pp).unwrap_or_default(), - None => return ok(), - } - } else { - doc.bounds_of(&ids).unwrap_or_default() - }; + let Some(target) = align_target(doc, &ids) else { return ok() }; for i in ids { let Some(b) = doc.world_bounds(i) else { continue }; let (dx, dy) = match a { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 40da2f1d..bfcc1ec3 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -253,6 +253,27 @@ fn align_distribute_zorder() { assert_eq!(s.doc().unwrap().node(page).unwrap().children[0], c); } +#[test] +fn a_layer_alone_on_the_page_has_nothing_to_align_to() { + use crate::cmd::arrange::can_align; + let mut s = sess(); + let f = id(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 200, "height": 100})).unwrap()); + let inner = id(&s.execute("shape.rectangle", &json!({"x": 20, "y": 30, "width": 10, "height": 10, "parent": f})).unwrap()); + let loose = id(&s.execute("shape.rectangle", &json!({"x": 300, "y": 5, "width": 10, "height": 10})).unwrap()); + let doc = s.doc().unwrap(); + assert!(!can_align(doc, &[])); + assert!(!can_align(doc, &[loose]), "alone on the page"); + assert!(!can_align(doc, &[f, inner]), "a frame with its own child is the frame alone"); + assert!(can_align(doc, &[inner]), "one in a frame aligns to it"); + assert!(can_align(doc, &[f, loose]), "two align to each other"); + // Where the buttons are off, the command still does nothing (and no harm). + let before = doc.world_bounds(loose); + s.execute("align.right", &json!({"ids": [loose]})).unwrap(); + assert_eq!(s.doc().unwrap().world_bounds(loose), before); + s.execute("align.right", &json!({"ids": [inner]})).unwrap(); + assert_eq!(s.doc().unwrap().world_bounds(inner).unwrap().x1, 200.0); +} + #[test] fn copy_paste_and_duplicate() { let mut s = sess(); diff --git a/crates/ui-egui/locales/de.toml b/crates/ui-egui/locales/de.toml index 7552cca3..affda97d 100644 --- a/crates/ui-egui/locales/de.toml +++ b/crates/ui-egui/locales/de.toml @@ -17,6 +17,7 @@ status = "draft — needs native review" "1…9, 0" = "1…9, 0" "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PRs, {commits} Commits, +{added} / −{deleted} Zeilen (Δ {delta}), +{bin_added} / −{bin_deleted} Binärdateien, {first} – {last}" "A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "Eine Sammlung dieser Datei mit diesen Modi und Variablen: Überschreiben Sie ihre Werte je Modus und fügen Sie eigene Variablen hinzu" +"A layer on its own aligns to its frame: select more layers, or one in a frame" = "Eine einzelne Ebene richtet sich an ihrem Rahmen aus: Wählen Sie weitere Ebenen oder eine Ebene in einem Rahmen" "A spring plays until it settles: its mass, stiffness and damping set how long" = "Eine Feder läuft, bis sie zur Ruhe kommt: Masse, Steifigkeit und Dämpfung bestimmen die Dauer" "About" = "Über" "About FigCraft" = "Über FigCraft" diff --git a/crates/ui-egui/locales/en.toml b/crates/ui-egui/locales/en.toml index c35b1677..08ac64ba 100644 --- a/crates/ui-egui/locales/en.toml +++ b/crates/ui-egui/locales/en.toml @@ -16,6 +16,7 @@ name = "English" "1…9, 0" = "1…9, 0" "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" "A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "A collection of this file with these modes and variables: override their values per mode and add variables of its own" +"A layer on its own aligns to its frame: select more layers, or one in a frame" = "A layer on its own aligns to its frame: select more layers, or one in a frame" "A responsive product page: hero, features, figures, a quote and a footer" = "A responsive product page: hero, features, figures, a quote and a footer" "A spring plays until it settles: its mass, stiffness and damping set how long" = "A spring plays until it settles: its mass, stiffness and damping set how long" "A title, an agenda, a big figure, a chart and a closing slide" = "A title, an agenda, a big figure, a chart and a closing slide" diff --git a/crates/ui-egui/locales/es.toml b/crates/ui-egui/locales/es.toml index 6514ac7b..3200e891 100644 --- a/crates/ui-egui/locales/es.toml +++ b/crates/ui-egui/locales/es.toml @@ -17,6 +17,7 @@ status = "draft — needs native review" "1…9, 0" = "1…9, 0" "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PR, {commits} commits, +{added} / −{deleted} líneas (Δ {delta}), +{bin_added} / −{bin_deleted} recursos binarios, {first} – {last}" "A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "Una colección de este archivo con estos modos y variables: modifica sus valores por modo y añade variables propias" +"A layer on its own aligns to its frame: select more layers, or one in a frame" = "Una capa sola se alinea con su marco: selecciona más capas o una dentro de un marco" "A responsive product page: hero, features, figures, a quote and a footer" = "Una página de producto adaptable: cabecera, funciones, cifras, una cita y un pie" "A spring plays until it settles: its mass, stiffness and damping set how long" = "Un muelle se reproduce hasta que se asienta: su masa, rigidez y amortiguación fijan cuánto dura" "A title, an agenda, a big figure, a chart and a closing slide" = "Un título, una agenda, una gran cifra, un gráfico y una diapositiva de cierre" diff --git a/crates/ui-egui/locales/fr.toml b/crates/ui-egui/locales/fr.toml index c8665934..69798340 100644 --- a/crates/ui-egui/locales/fr.toml +++ b/crates/ui-egui/locales/fr.toml @@ -17,6 +17,7 @@ status = "draft — needs native review" "1…9, 0" = "1…9, 0" "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login} : {prs} PR, {commits} commits, +{added} / −{deleted} lignes (Δ {delta}), +{bin_added} / −{bin_deleted} ressources binaires, {first} – {last}" "A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "Une collection de ce fichier avec ces modes et ces variables : remplacez leurs valeurs par mode et ajoutez vos propres variables" +"A layer on its own aligns to its frame: select more layers, or one in a frame" = "Un calque seul s’aligne sur son cadre : sélectionnez d’autres calques, ou un calque dans un cadre" "A spring plays until it settles: its mass, stiffness and damping set how long" = "Un ressort joue jusqu’à ce qu’il se stabilise : sa masse, sa rigidité et son amortissement en fixent la durée" "About" = "À propos" "About FigCraft" = "À propos de FigCraft" diff --git a/crates/ui-egui/locales/ja.toml b/crates/ui-egui/locales/ja.toml index f7b9cf9f..23a0f5e2 100644 --- a/crates/ui-egui/locales/ja.toml +++ b/crates/ui-egui/locales/ja.toml @@ -19,6 +19,7 @@ status = "draft — needs native review" "1…9, 0" = "1…9, 0" "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}:PR {prs} 件、コミット {commits} 件、+{added} / −{deleted} 行(Δ {delta})、バイナリアセット +{bin_added} / −{bin_deleted}、{first} – {last}" "A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "このファイルのコレクションとそのモード・変数:モードごとに値を上書きし、独自の変数を追加できる" +"A layer on its own aligns to its frame: select more layers, or one in a frame" = "単独のレイヤーはフレームに整列します:レイヤーを追加で選択するか、フレーム内のレイヤーを選択" "A spring plays until it settles: its mass, stiffness and damping set how long" = "スプリングは静止するまで再生。長さは質量・剛性・減衰で決まる" "About" = "情報" "About FigCraft" = "FigCraft について" diff --git a/crates/ui-egui/locales/ko.toml b/crates/ui-egui/locales/ko.toml index 59bb2386..aed3529a 100644 --- a/crates/ui-egui/locales/ko.toml +++ b/crates/ui-egui/locales/ko.toml @@ -18,6 +18,7 @@ status = "draft — needs native review" "1…9, 0" = "1…9, 0" "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: PR {prs}개, 커밋 {commits}개, +{added} / −{deleted}줄 (Δ {delta}), 바이너리 에셋 +{bin_added} / −{bin_deleted}, {first} – {last}" "A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "이 파일의 컬렉션과 그 모드 및 변수: 모드별로 값을 재정의하고 자체 변수를 추가할 수 있음" +"A layer on its own aligns to its frame: select more layers, or one in a frame" = "레이어 하나는 프레임에 맞춰 정렬됩니다: 레이어를 더 선택하거나 프레임 안의 레이어를 선택하세요" "A spring plays until it settles: its mass, stiffness and damping set how long" = "스프링은 멈출 때까지 재생됨. 길이는 질량, 강성, 감쇠로 결정됨" "About" = "정보" "About FigCraft" = "FigCraft 정보" diff --git a/crates/ui-egui/locales/pt-BR.toml b/crates/ui-egui/locales/pt-BR.toml index a692a8ce..97500236 100644 --- a/crates/ui-egui/locales/pt-BR.toml +++ b/crates/ui-egui/locales/pt-BR.toml @@ -16,6 +16,7 @@ status = "draft — needs native review" "1…9, 0" = "1…9, 0" "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} linhas (Δ {delta}), +{bin_added} / −{bin_deleted} arquivos binários, {first} – {last}" "A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "Uma coleção deste arquivo com estes modos e variáveis: substitua os valores por modo e adicione variáveis próprias" +"A layer on its own aligns to its frame: select more layers, or one in a frame" = "Uma camada sozinha se alinha ao seu quadro: selecione mais camadas ou uma dentro de um quadro" "A spring plays until it settles: its mass, stiffness and damping set how long" = "Uma mola se move até se estabilizar: sua massa, rigidez e amortecimento definem a duração" "About" = "Sobre" "About FigCraft" = "Sobre o FigCraft" diff --git a/crates/ui-egui/locales/zh-Hans.toml b/crates/ui-egui/locales/zh-Hans.toml index d2431df0..d1887735 100644 --- a/crates/ui-egui/locales/zh-Hans.toml +++ b/crates/ui-egui/locales/zh-Hans.toml @@ -19,6 +19,7 @@ status = "draft — needs native review" "1…9, 0" = "1…9, 0" "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}:{prs} 个 PR,{commits} 次提交,+{added} / −{deleted} 行(Δ {delta}),二进制资源 +{bin_added} / −{bin_deleted},{first} – {last}" "A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "本文件中带有这些模式和变量的集合:可按模式覆盖其值,并添加自己的变量" +"A layer on its own aligns to its frame: select more layers, or one in a frame" = "单个图层与其所在画框对齐:请再选择其他图层,或选择画框内的图层" "A spring plays until it settles: its mass, stiffness and damping set how long" = "弹簧动画播放至静止,时长由质量、刚度和阻尼决定" "About" = "关于" "About FigCraft" = "关于 FigCraft" diff --git a/crates/ui-egui/src/a11y_tests.rs b/crates/ui-egui/src/a11y_tests.rs index 3fe85876..d041a5b3 100644 --- a/crates/ui-egui/src/a11y_tests.rs +++ b/crates/ui-egui/src/a11y_tests.rs @@ -388,3 +388,18 @@ fn every_accessibility_path_runs_with_the_tree_on_without_nested_context_locks() let out = u.frame_output(vec![]); assert!(out.platform_output.accesskit_update.is_some()); } + +#[test] +fn the_align_buttons_are_off_for_a_layer_alone_on_the_page() { + let (mut u, a, b) = setup(); + let align_left = |nodes: &[Node]| { + nodes.iter().find(|n| n.label().is_some_and(|l| l.starts_with("Align left"))).map(Node::is_disabled).expect("the Align left button") + }; + assert!(align_left(&tree(&mut u)), "Rect A alone on the page: nothing to align to"); + u.app.run("select.set", json!({"ids": [a, b]})); + assert!(!align_left(&tree(&mut u)), "two layers align to each other"); + let f = id(u.app.session.execute("frame.create", &json!({"x": 300, "y": 0, "width": 100, "height": 100})).unwrap()); + let inner = id(u.app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 10, "height": 10, "parent": f})).unwrap()); + u.app.run("select.set", json!({"ids": [inner]})); + assert!(!align_left(&tree(&mut u)), "one in a frame aligns to it"); +} diff --git a/crates/ui-egui/src/right.rs b/crates/ui-egui/src/right.rs index d1244ff4..6be28780 100644 --- a/crates/ui-egui/src/right.rs +++ b/crates/ui-egui/src/right.rs @@ -599,12 +599,15 @@ fn position_fields(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId] (Icon::AlignBottom, tip(tr("Align bottom"), cmds[5])), ]; let w = col / 3.0; - if let Some(i) = widgets::segmented(ui, &items[..3], None, w) { + let picked = ui.add_enabled_ui(figcraft_engine::cmd::arrange::can_align(doc, sel), |ui| { + let h = widgets::segmented(ui, &items[..3], None, w); + let v = widgets::segmented(ui, &items[3..], None, w).map(|i| i + 3); + h.or(v) + }); + picked.response.on_disabled_hover_text(tr("A layer on its own aligns to its frame: select more layers, or one in a frame")); + if let Some(i) = picked.inner { app.run(cmds[i], json!({"ids": ids})); } - if let Some(i) = widgets::segmented(ui, &items[3..], None, w) { - app.run(cmds[i + 3], json!({"ids": ids})); - } if sel.len() > 2 { let more = icons::button(ui, Icon::DistributeH, 24.0, false, tr("Distribute and tidy")); menu_popup(&more).show(|ui| {