mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-05 18:20:01 +00:00
Merge pull request #832 from Ahmed5Emad/seperate-padding
adding individual padding controls in Layout panel
This commit is contained in:
7 files changed
+251
-30
No files matched your search
@@ -149,6 +149,35 @@ mod tests {
|
||||
assert!(s.execute("node.set", &json!({"id": b, "props": {"absolute": "maybe"}})).is_err());
|
||||
}
|
||||
|
||||
/// Each side key writes its own padding slot and leaves the other three alone.
|
||||
#[test]
|
||||
fn each_side_padding_sets_its_side_alone() {
|
||||
let mut s = Session::new();
|
||||
let (f, _, _) = al_frame(&mut s);
|
||||
// One side at a time: the other three stay 0 ([top, right, bottom, left]).
|
||||
for (key, v, want) in [
|
||||
("paddingTop", 8.0, [8.0, 0.0, 0.0, 0.0]),
|
||||
("paddingRight", 12.0, [0.0, 12.0, 0.0, 0.0]),
|
||||
("paddingBottom", 16.0, [0.0, 0.0, 16.0, 0.0]),
|
||||
("paddingLeft", 20.0, [0.0, 0.0, 0.0, 20.0]),
|
||||
] {
|
||||
ex(&mut s, "node.set", json!({"id": f, "props": {"padding": 0}}));
|
||||
ex(&mut s, "node.set", json!({"id": f, "props": {key: v}}));
|
||||
assert_eq!(node(&s, f).layout.as_ref().unwrap().padding, want, "{key}");
|
||||
}
|
||||
// The [t, r, b, l] array and the axis pairs agree with the four sides.
|
||||
ex(&mut s, "node.set", json!({"id": f, "props": {"padding": [8, 12, 16, 20]}}));
|
||||
assert_eq!(node(&s, f).layout.as_ref().unwrap().padding, [8.0, 12.0, 16.0, 20.0]);
|
||||
ex(&mut s, "node.set", json!({"id": f, "props": {"paddingHorizontal": 4}}));
|
||||
assert_eq!(node(&s, f).layout.as_ref().unwrap().padding, [8.0, 4.0, 16.0, 4.0]);
|
||||
ex(&mut s, "node.set", json!({"id": f, "props": {"paddingVertical": 2}}));
|
||||
assert_eq!(node(&s, f).layout.as_ref().unwrap().padding, [2.0, 4.0, 2.0, 4.0]);
|
||||
// Negatives clamp to 0; junk is refused.
|
||||
ex(&mut s, "node.set", json!({"id": f, "props": {"paddingTop": -5}}));
|
||||
assert_eq!(node(&s, f).layout.as_ref().unwrap().padding[0], 0.0);
|
||||
assert!(s.execute("node.set", &json!({"id": f, "props": {"paddingLeft": "wide"}})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn baseline_alignment_lines_up_text() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -172,6 +172,7 @@ name = "English"
|
||||
"Both directions" = "Both directions"
|
||||
"Bottom" = "Bottom"
|
||||
"Bottom left radius" = "Bottom left radius"
|
||||
"Bottom padding" = "Bottom padding"
|
||||
"Bottom right radius" = "Bottom right radius"
|
||||
"Branch" = "Branch"
|
||||
"Branch archived" = "Branch archived"
|
||||
@@ -633,6 +634,7 @@ name = "English"
|
||||
"Included" = "Included"
|
||||
"Included in layout" = "Included in layout"
|
||||
"Individual corners" = "Individual corners"
|
||||
"Individual padding" = "Individual padding"
|
||||
"Individual strokes" = "Individual strokes"
|
||||
"Ink" = "Ink"
|
||||
"Inner shadow" = "Inner shadow"
|
||||
@@ -703,6 +705,7 @@ name = "English"
|
||||
"Leaves" = "Leaves"
|
||||
"Left" = "Left"
|
||||
"Left & right" = "Left & right"
|
||||
"Left padding" = "Left padding"
|
||||
"Left side" = "Left side"
|
||||
"Left side width" = "Left side width"
|
||||
"Letter case" = "Letter case"
|
||||
@@ -1137,6 +1140,7 @@ name = "English"
|
||||
"Review and merge" = "Review and merge"
|
||||
"Ribbon" = "Ribbon"
|
||||
"Right" = "Right"
|
||||
"Right padding" = "Right padding"
|
||||
"Right side" = "Right side"
|
||||
"Right side width" = "Right side width"
|
||||
"Rotate 90°" = "Rotate 90°"
|
||||
@@ -1406,6 +1410,7 @@ name = "English"
|
||||
"Top" = "Top"
|
||||
"Top & bottom" = "Top & bottom"
|
||||
"Top left radius" = "Top left radius"
|
||||
"Top padding" = "Top padding"
|
||||
"Top right radius" = "Top right radius"
|
||||
"Transform" = "Transform"
|
||||
"Triangle arrow" = "Triangle arrow"
|
||||
|
||||
@@ -175,6 +175,7 @@ status = "draft — needs native review"
|
||||
"Both directions" = "Ambas direcciones"
|
||||
"Bottom" = "Abajo"
|
||||
"Bottom left radius" = "Radio inferior izquierdo"
|
||||
"Bottom padding" = "Margen interno inferior"
|
||||
"Bottom right radius" = "Radio inferior derecho"
|
||||
"Branch" = "Rama"
|
||||
"Branch archived" = "Rama archivada"
|
||||
@@ -634,6 +635,7 @@ status = "draft — needs native review"
|
||||
"Included" = "Incluidos"
|
||||
"Included in layout" = "Incluidos en el diseño"
|
||||
"Individual corners" = "Esquinas individuales"
|
||||
"Individual padding" = "Margen interno individual"
|
||||
"Individual strokes" = "Trazos individuales"
|
||||
"Ink" = "Tinta"
|
||||
"Inner shadow" = "Sombra interior"
|
||||
@@ -704,6 +706,7 @@ status = "draft — needs native review"
|
||||
"Leaves" = "Hojas"
|
||||
"Left" = "Izquierda"
|
||||
"Left & right" = "Izquierda y derecha"
|
||||
"Left padding" = "Margen interno izquierdo"
|
||||
"Left side" = "Lado izquierdo"
|
||||
"Left side width" = "Ancho del lado izquierdo"
|
||||
"Letter case" = "Mayúsculas y minúsculas"
|
||||
@@ -1138,6 +1141,7 @@ status = "draft — needs native review"
|
||||
"Review and merge" = "Revisar y fusionar"
|
||||
"Ribbon" = "Cinta"
|
||||
"Right" = "Derecha"
|
||||
"Right padding" = "Margen interno derecho"
|
||||
"Right side" = "Lado derecho"
|
||||
"Right side width" = "Ancho del lado derecho"
|
||||
"Rotate 90°" = "Girar 90°"
|
||||
@@ -1407,6 +1411,7 @@ status = "draft — needs native review"
|
||||
"Top" = "Arriba"
|
||||
"Top & bottom" = "Arriba y abajo"
|
||||
"Top left radius" = "Radio superior izquierdo"
|
||||
"Top padding" = "Margen interno superior"
|
||||
"Top right radius" = "Radio superior derecho"
|
||||
"Transform" = "Transformar"
|
||||
"Triangle arrow" = "Flecha triangular"
|
||||
|
||||
@@ -85,6 +85,8 @@ icons! {
|
||||
CornerRadius,
|
||||
Angle,
|
||||
IndependentCorners,
|
||||
/// Individual padding: a frame with one inward tick per side, each set alone.
|
||||
IndependentPadding,
|
||||
LinkOn,
|
||||
LinkOff,
|
||||
LayoutH,
|
||||
@@ -139,6 +141,11 @@ icons! {
|
||||
Fixed,
|
||||
PaddingH,
|
||||
PaddingV,
|
||||
/// Individual padding sides: a frame with the controlled side filled.
|
||||
PaddingTop,
|
||||
PaddingRight,
|
||||
PaddingBottom,
|
||||
PaddingLeft,
|
||||
Gap,
|
||||
Reset,
|
||||
Detach,
|
||||
@@ -713,6 +720,14 @@ fn draw(p: &P, icon: Icon) {
|
||||
p.corner(x, y, dx, dy, 4.0, 2.0);
|
||||
}
|
||||
}
|
||||
IndependentPadding => {
|
||||
// A frame outline with one inward tick per side: each side's padding, set alone.
|
||||
p.rect(2.5, 2.5, 13.5, 13.5, 2.0);
|
||||
p.line(8.0, 2.5, 8.0, 5.0);
|
||||
p.line(13.5, 8.0, 11.0, 8.0);
|
||||
p.line(8.0, 13.5, 8.0, 11.0);
|
||||
p.line(2.5, 8.0, 5.0, 8.0);
|
||||
}
|
||||
LinkOn | Link => {
|
||||
p.path(&capsule((4.0, 12.0), (6.5, 9.5), 2.4), true);
|
||||
p.path(&capsule((9.5, 6.5), (12.0, 4.0), 2.4), true);
|
||||
@@ -930,12 +945,32 @@ fn draw(p: &P, icon: Icon) {
|
||||
p.line(3.0, 7.5, 13.0, 7.5);
|
||||
}
|
||||
PaddingH => {
|
||||
p.rect(2.5, 3.5, 13.5, 12.5, 1.0);
|
||||
p.fill_rect(5.0, 6.0, 11.0, 10.0, 0.0);
|
||||
// Horizontal padding: bars down the padded left and right sides in their frame.
|
||||
p.rect(2.5, 2.5, 13.5, 13.5, 1.0);
|
||||
p.fill_rect(2.5, 2.5, 5.5, 13.5, 1.0);
|
||||
p.fill_rect(10.5, 2.5, 13.5, 13.5, 1.0);
|
||||
}
|
||||
PaddingV => {
|
||||
p.rect(3.5, 2.5, 12.5, 13.5, 1.0);
|
||||
p.fill_rect(6.0, 5.0, 10.0, 11.0, 0.0);
|
||||
// Vertical padding: bars across the padded top and bottom in their frame.
|
||||
p.rect(2.5, 2.5, 13.5, 13.5, 1.0);
|
||||
p.fill_rect(2.5, 2.5, 13.5, 5.5, 1.0);
|
||||
p.fill_rect(2.5, 10.5, 13.5, 13.5, 1.0);
|
||||
}
|
||||
PaddingTop => {
|
||||
p.rect(2.5, 2.5, 13.5, 13.5, 1.0);
|
||||
p.fill_rect(2.5, 2.5, 13.5, 5.5, 1.0);
|
||||
}
|
||||
PaddingRight => {
|
||||
p.rect(2.5, 2.5, 13.5, 13.5, 1.0);
|
||||
p.fill_rect(10.5, 2.5, 13.5, 13.5, 1.0);
|
||||
}
|
||||
PaddingBottom => {
|
||||
p.rect(2.5, 2.5, 13.5, 13.5, 1.0);
|
||||
p.fill_rect(2.5, 10.5, 13.5, 13.5, 1.0);
|
||||
}
|
||||
PaddingLeft => {
|
||||
p.rect(2.5, 2.5, 13.5, 13.5, 1.0);
|
||||
p.fill_rect(2.5, 2.5, 5.5, 13.5, 1.0);
|
||||
}
|
||||
Online => {
|
||||
p.circle(8.0, 8.0, 5.5);
|
||||
|
||||
@@ -401,35 +401,66 @@ fn alignment(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], l: &AutoLayout) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
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)));
|
||||
});
|
||||
let open = ui.data(|d| d.get_temp::<bool>(Id::new("indep_padding"))).unwrap_or(false);
|
||||
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;
|
||||
|
||||
@@ -156,6 +156,8 @@ mod online_tests;
|
||||
#[cfg(test)]
|
||||
mod open_issues_tests;
|
||||
#[cfg(test)]
|
||||
mod padding_individual_tests;
|
||||
#[cfg(test)]
|
||||
mod palette_tests;
|
||||
#[cfg(test)]
|
||||
mod panel_remove_tests;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
//! Individual padding in the Design panel, driven headless: the four side fields only
|
||||
//! appear once the toggle opens, and typing in one sets that side alone.
|
||||
|
||||
use egui::accesskit::Role;
|
||||
use egui::{Event, Id, Key, Modifiers, Pos2, pos2};
|
||||
use figcraft_doc::NodeId;
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::layers_drag_tests::Ui;
|
||||
|
||||
/// An auto layout frame with uniform padding 10, selected.
|
||||
fn scene() -> (Ui, NodeId) {
|
||||
let mut s = Session::new();
|
||||
let f =
|
||||
NodeId(
|
||||
s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 300, "height": 200, "props": {"layout": "horizontal", "padding": 10}}))
|
||||
.unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap(),
|
||||
);
|
||||
s.execute("select.set", &json!({"ids": [f]})).unwrap();
|
||||
(Ui::new(s), f)
|
||||
}
|
||||
|
||||
/// The frame's padding as `[top, right, bottom, left]`.
|
||||
fn padding(u: &Ui, f: NodeId) -> [f64; 4] {
|
||||
u.app.session.doc().unwrap().node(f).unwrap().layout.clone().unwrap().padding
|
||||
}
|
||||
|
||||
/// A full press-and-release at `p`, then settling one frame.
|
||||
fn click(u: &mut Ui, p: Pos2) {
|
||||
u.button(p, true);
|
||||
u.button(p, false);
|
||||
u.frame(vec![]);
|
||||
}
|
||||
|
||||
/// A press-and-release of Enter, committing the focused number field.
|
||||
fn enter(u: &mut Ui) {
|
||||
u.frame(vec![
|
||||
Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE },
|
||||
Event::Key { key: Key::Enter, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE },
|
||||
]);
|
||||
}
|
||||
|
||||
/// The middle of the toggle button named `label`, found in the AccessKit tree.
|
||||
fn named_button(u: &mut Ui, label: &str) -> 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(label)).expect(label);
|
||||
let b = n.bounds().expect("bounds");
|
||||
pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32)
|
||||
}
|
||||
|
||||
/// The side fields stay hidden until the toggle opens, then typing in one sets
|
||||
/// that side alone in a single undo step.
|
||||
#[test]
|
||||
fn side_fields_show_only_once_the_toggle_opens_and_set_one_side() {
|
||||
let (mut u, f) = scene();
|
||||
assert_eq!(padding(&u, f), [10.0; 4]);
|
||||
// Shut: no per-side fields.
|
||||
assert!(u.ctx.read_response(Id::new(("padding", 0)).with("field")).is_none(), "top field hidden until toggled");
|
||||
// Click the toggle: the four sides appear.
|
||||
let t = named_button(&mut u, "Individual padding");
|
||||
click(&mut u, t);
|
||||
let top = u.ctx.read_response(Id::new(("padding", 0)).with("field")).expect("top field shows").rect.center();
|
||||
assert!(u.ctx.read_response(Id::new(("padding", 3)).with("field")).is_some(), "left field shows");
|
||||
// Typing in the top field sets only the top, in one undo step.
|
||||
let history = u.app.session.undo_depth().0;
|
||||
click(&mut u, top);
|
||||
u.frame(vec![Event::Text("8".into())]);
|
||||
enter(&mut u);
|
||||
u.frame(vec![]);
|
||||
assert_eq!(padding(&u, f), [8.0, 10.0, 10.0, 10.0]);
|
||||
assert_eq!(u.app.session.undo_depth().0, history + 1);
|
||||
u.app.run("edit.undo", json!({}));
|
||||
assert_eq!(padding(&u, f), [10.0; 4]);
|
||||
}
|
||||
|
||||
/// The middle of the shut pair field with accessible name `label`.
|
||||
fn named_field(u: &mut Ui, label: &str) -> 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(label)).expect(label);
|
||||
let b = n.bounds().expect("bounds");
|
||||
pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32)
|
||||
}
|
||||
|
||||
/// 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())]);
|
||||
enter(&mut u);
|
||||
u.frame(vec![]);
|
||||
assert_eq!(padding(&u, f), [10.0, 20.0, 10.0, 20.0]);
|
||||
let v = named_field(&mut u, "Vertical padding");
|
||||
click(&mut u, v);
|
||||
u.frame(vec![Event::Text("7".into())]);
|
||||
enter(&mut u);
|
||||
u.frame(vec![]);
|
||||
assert_eq!(padding(&u, f), [7.0, 20.0, 7.0, 20.0]);
|
||||
}
|
||||
Reference in new issue
Block a user