From 742e911652b41cc1bed3428a422533ee0709adfd Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 19 Oct 2026 23:58:26 +0700 Subject: [PATCH] #832: the per-side fields are two plain rows like Individual corners (toggle in the first); shut H/V fields read Mixed when their sides disagree; no comment naming the incumbent --- crates/ui-egui/src/layout_panel.rs | 143 +++++++----------- .../ui-egui/src/padding_individual_tests.rs | 6 +- 2 files changed, 60 insertions(+), 89 deletions(-) diff --git a/crates/ui-egui/src/layout_panel.rs b/crates/ui-egui/src/layout_panel.rs index 6c8d6693..b5ff1d45 100644 --- a/crates/ui-egui/src/layout_panel.rs +++ b/crates/ui-egui/src/layout_panel.rs @@ -4,7 +4,7 @@ //! alignment in its cell, and clip content. Everything goes through `node.set` props or `layout.*` //! commands. -use egui::{Align, Align2, CornerRadius, Id, Layout, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; +use egui::{Align2, CornerRadius, Id, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; use figcraft_doc::*; use serde_json::json; @@ -401,97 +401,66 @@ fn alignment(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], l: &AutoLayout) { } } -/// The Spacing row: gap fields, then the H/V padding pair (or the 2x2 per-side -/// grid while the individual-padding toggle is open). +/// The Spacing row: the H/V padding pair, or, with Individual padding on, two rows of one field +/// per side (like Individual corners). fn spacing(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], l: &AutoLayout) { let col = widgets::col_width(ui.available_width()); let [pt, pr, pb, pl] = l.padding; let open = ui.data(|d| d.get_temp::(Id::new("indep_padding"))).unwrap_or(false); - if open { - // Each side on its own, like Figma's individual padding: a 2x2 grid of sides with - // the toggle at its right, replacing the H/V row while open. Every measure copies - // a plain row: 32px rows, the panel's own inter-row gap, 8px between the fields. - let gap = ui.spacing().item_spacing.y; - let total = 32.0 * 2.0 + gap; - let fields_w = col * 2.0 + 8.0; - let top = ("paddingTop", tr("Top padding"), Icon::PaddingTop, pt, 0); - let right = ("paddingRight", tr("Right padding"), Icon::PaddingRight, pr, 1); - let bottom = ("paddingBottom", tr("Bottom padding"), Icon::PaddingBottom, pb, 2); - let left = ("paddingLeft", tr("Left padding"), Icon::PaddingLeft, pl, 3); - block(ui, total, 0.0, |ui| { - ui.allocate_ui_with_layout(vec2(fields_w, total), Layout::top_down(Align::LEFT), |ui| { - ui.spacing_mut().item_spacing.y = 0.0; - // The horizontal pair (left, right) on the left, the vertical pair on the right. - for (r, pair) in [[&left, &top], [&right, &bottom]].iter().enumerate() { - if r > 0 { - ui.add_space(gap); - } - ui.allocate_ui_with_layout(vec2(fields_w, 32.0), Layout::left_to_right(Align::Center), |ui| { - ui.spacing_mut().item_spacing.x = 8.0; - for (key, name, icon, v, k) in pair.iter() { - let e = crate::variable_ui::number_field( - app, - ui, - ids, - key, - Id::new(("padding", *k)), - Lead::Icon(*icon, name), - Some(*v), - "", - col, - ); - app.field(ids, key, e, |v| json!(v.max(0.0))); - } - }); - } - }); - // The toggle keeps the shut row's rect: full-height strip, button 4px - // down, centred in the top 32px exactly like the shut row. - ui.allocate_ui_with_layout(vec2(24.0, total), Layout::top_down(Align::Center), |ui| { - ui.add_space(4.0); - if icons::button(ui, Icon::IndependentPadding, 24.0, true, tr("Individual padding")).clicked() { - ui.data_mut(|d| d.insert_temp(Id::new("indep_padding"), false)); - } - }); - }); - return; - } - row(ui, 32.0, |ui| { - // Sides that disagree show both values ("16,54") instead of "Mixed". - let h_pair = format!("{},{}", widgets::fmt_num(pl), widgets::fmt_num(pr)); - let h_empty = if pl == pr { tr("Mixed") } else { h_pair.as_str() }; - let e = crate::variable_ui::number_field_or( - app, - ui, - ids, - "paddingHorizontal", - Id::new("al_padh"), - (pl == pr).then_some(pl), - col, - h_empty, - Lead::Icon(Icon::PaddingH, tr("Horizontal padding")), - "", - ); - app.field(ids, "paddingHorizontal", e, |v| json!(v.max(0.0))); - let v_pair = format!("{},{}", widgets::fmt_num(pt), widgets::fmt_num(pb)); - let v_empty = if pt == pb { tr("Mixed") } else { v_pair.as_str() }; - let e = crate::variable_ui::number_field_or( - app, - ui, - ids, - "paddingVertical", - Id::new("al_padv"), - (pt == pb).then_some(pt), - col, - v_empty, - Lead::Icon(Icon::PaddingV, tr("Vertical padding")), - "", - ); - app.field(ids, "paddingVertical", e, |v| json!(v.max(0.0))); - if icons::button(ui, Icon::IndependentPadding, 24.0, false, tr("Individual padding")).clicked() { - ui.data_mut(|d| d.insert_temp(Id::new("indep_padding"), true)); + let toggle = |ui: &mut Ui| { + if icons::button(ui, Icon::IndependentPadding, 24.0, open, tr("Individual padding")).clicked() { + ui.data_mut(|d| d.insert_temp(Id::new("indep_padding"), !open)); } - }); + }; + if open { + // `l` is the selection's common layout, so its sides are common too. `k` indexes + // [top, right, bottom, left]. + let sides = [ + ("paddingLeft", tr("Left padding"), Icon::PaddingLeft, pl, 3), + ("paddingTop", tr("Top padding"), Icon::PaddingTop, pt, 0), + ("paddingRight", tr("Right padding"), Icon::PaddingRight, pr, 1), + ("paddingBottom", tr("Bottom padding"), Icon::PaddingBottom, pb, 2), + ]; + for (r, pair) in sides.chunks(2).enumerate() { + row(ui, 32.0, |ui| { + for (key, name, icon, v, k) in pair { + let e = crate::variable_ui::number_field(app, ui, ids, key, Id::new(("padding", *k)), Lead::Icon(*icon, name), Some(*v), "", col); + app.field(ids, key, e, |v| json!(v.max(0.0))); + } + if r == 0 { + toggle(ui); + } + }); + } + } else { + row(ui, 32.0, |ui| { + let e = crate::variable_ui::number_field( + app, + ui, + ids, + "paddingHorizontal", + Id::new("al_padh"), + Lead::Icon(Icon::PaddingH, tr("Horizontal padding")), + (pl == pr).then_some(pl), + "", + col, + ); + app.field(ids, "paddingHorizontal", e, |v| json!(v.max(0.0))); + let e = crate::variable_ui::number_field( + app, + ui, + ids, + "paddingVertical", + Id::new("al_padv"), + Lead::Icon(Icon::PaddingV, tr("Vertical padding")), + (pt == pb).then_some(pt), + "", + col, + ); + app.field(ids, "paddingVertical", e, |v| json!(v.max(0.0))); + toggle(ui); + }); + } if l.mode != LayoutMode::Grid && l.wrap { // Wrapped rows (horizontal) or columns (vertical): the gap between them. let vertical = l.mode == LayoutMode::Vertical; diff --git a/crates/ui-egui/src/padding_individual_tests.rs b/crates/ui-egui/src/padding_individual_tests.rs index 28a207bc..bc95886e 100644 --- a/crates/ui-egui/src/padding_individual_tests.rs +++ b/crates/ui-egui/src/padding_individual_tests.rs @@ -90,13 +90,15 @@ fn named_field(u: &mut Ui, label: &str) -> Pos2 { pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32) } -/// The shut pair fields edit their own axis: horizontal touches left and right -/// only, vertical touches top and bottom only. +/// The shut pair fields read Mixed when their sides disagree and edit their own axis: horizontal +/// touches left and right only, vertical touches top and bottom only. #[test] fn shut_pair_fields_edit_their_own_axis() { let (mut u, f) = scene(); u.app.run("node.set", json!({"ids": [f], "props": {"paddingLeft": 16.0, "paddingRight": 54.0}})); assert_eq!(padding(&u, f), [10.0, 54.0, 10.0, 16.0]); + let shown = crate::live_tests::texts(&crate::live_tests::flat(&u.frame_output(vec![]).shapes)); + assert!(shown.iter().any(|(t, _, _)| t == "Mixed"), "sides that disagree read Mixed: {shown:?}"); let h = named_field(&mut u, "Horizontal padding"); click(&mut u, h); u.frame(vec![Event::Text("20".into())]);