mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
fix(ui): disable alignment when the selection can't be aligned
One layer directly on the page has nothing to align to (one layer aligns to its parent frame), so the six Alignment buttons looked active and did nothing. They are now disabled there, with a hover explaining why. - cmd/arrange.rs: align_target (unchanged rule) and pub can_align. - right.rs: the align rows in add_enabled_ui + on_disabled_hover_text. - The hover text in every catalog. Tests: a_layer_alone_on_the_page_has_nothing_to_align_to (engine), the_align_buttons_are_off_for_a_layer_alone_on_the_page (AccessKit). Refs #256 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bcedfeba10
commit
9c0b441119
@@ -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<Rect> {
|
||||
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<Value> {
|
||||
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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 について"
|
||||
|
||||
@@ -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 정보"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user