Merge #1030: M1: individual corner fields sit at their corners, each with its own icon (#1002) (@muamerETS)

This commit is contained in:
bflatastic
2026-10-24 09:13:48 +07:00
4 changed files with 125 additions and 38 deletions
@@ -0,0 +1,75 @@
//! Individual corners in the Design panel, driven headless: the four corner fields sit where
//! their corners are (top left, top right / bottom left, bottom right) and each sets its own.
use egui::accesskit::Role;
use egui::{Event, Id, Key, Modifiers, Pos2, Rect, pos2};
use figcraft_doc::NodeId;
use figcraft_engine::Session;
use serde_json::json;
use crate::layers_drag_tests::Ui;
/// A rectangle rounded only on its left corners (top left and bottom left 30), selected.
fn scene() -> (Ui, NodeId) {
let mut s = Session::new();
let r = NodeId(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 200, "height": 120})).unwrap()["id"].as_u64().unwrap());
s.execute("node.set", &json!({"ids": [r], "props": {"topLeftRadius": 30, "bottomLeftRadius": 30}})).unwrap();
s.execute("select.set", &json!({"ids": [r]})).unwrap();
(Ui::new(s), r)
}
/// The rectangle's radii as `[top left, top right, bottom right, bottom left]`.
fn radii(u: &Ui, r: NodeId) -> [f64; 4] {
u.app.session.doc().unwrap().node(r).unwrap().corners.radii
}
/// 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)
}
/// Corner field `k` (an index into the radii).
fn field(u: &Ui, k: usize) -> Rect {
u.ctx.read_response(Id::new(("corner", k)).with("field")).expect("corner field shows").rect
}
/// The fields sit at their corners, and typing in the bottom right one sets only that corner in a
/// single undo step (#1002: the bottom row showed bottom right on the left).
#[test]
fn corner_fields_sit_at_their_corners_and_set_their_own() {
let (mut u, r) = scene();
let t = named_button(&mut u, "Individual corners");
click(&mut u, t);
let (tl, tr, br, bl) = (field(&u, 0).center(), field(&u, 1).center(), field(&u, 2).center(), field(&u, 3).center());
assert!(tl.x < tr.x && (tl.y - tr.y).abs() < 1.0, "top row: top left, then top right");
assert!(bl.x < br.x && (bl.y - br.y).abs() < 1.0, "bottom row: bottom left, then bottom right");
assert!(bl.y > tl.y && (bl.x - tl.x).abs() < 1.0, "bottom left sits under top left");
let history = u.app.session.undo_depth().0;
click(&mut u, br);
u.frame(vec![Event::Text("12".into())]);
enter(&mut u);
u.frame(vec![]);
assert_eq!(radii(&u, r), [30.0, 0.0, 12.0, 30.0]);
assert_eq!(u.app.session.undo_depth().0, history + 1);
}
+23 -1
View File
@@ -83,6 +83,11 @@ icons! {
FlipH,
FlipV,
CornerRadius,
/// Individual corners: the rounded corner each field sets.
CornerTopLeft,
CornerTopRight,
CornerBottomRight,
CornerBottomLeft,
Angle,
IndependentCorners,
/// Individual padding: a frame with one inward tick per side, each set alone.
@@ -719,7 +724,10 @@ fn draw(p: &P, icon: Icon) {
p.poly(&[(3.5, 5.5), (12.5, 5.5), (12.5, 1.5)], true);
p.fill(&[(3.0, 9.0), (13.0, 9.0), (13.0, 14.0)]);
}
CornerRadius => p.corner(2.5, 2.5, 1.0, 1.0, 11.5, 6.0),
CornerRadius | CornerTopLeft => p.corner(2.5, 2.5, 1.0, 1.0, 11.5, 6.0),
CornerTopRight => p.corner(13.5, 2.5, -1.0, 1.0, 11.5, 6.0),
CornerBottomRight => p.corner(13.5, 13.5, -1.0, -1.0, 11.5, 6.0),
CornerBottomLeft => p.corner(2.5, 13.5, 1.0, -1.0, 11.5, 6.0),
Angle => {
// Two rays from a vertex, the arc between them marking the angle (all unhinted, so
// the arc meets the rays at every size).
@@ -1417,4 +1425,18 @@ mod tests {
let inside = (0..h.len()).all(|i| side(i) >= 0.0) || (0..h.len()).all(|i| side(i) <= 0.0);
assert!(inside, "the handle covers the blade's base at {base:?}");
}
/// Each corner's icon shows its own corner: no two of the four draw alike.
#[test]
fn corner_icons_differ() {
let drawn: Vec<String> = [Icon::CornerTopLeft, Icon::CornerTopRight, Icon::CornerBottomRight, Icon::CornerBottomLeft]
.iter()
.map(|&icon| format!("{:?}", shapes_at_32(icon)))
.collect();
for (i, a) in drawn.iter().enumerate() {
for b in &drawn[i + 1..] {
assert_ne!(a, b);
}
}
}
}
+2
View File
@@ -98,6 +98,8 @@ mod comments_tests;
#[cfg(test)]
mod component_props_reorder_tests;
#[cfg(test)]
mod corner_individual_tests;
#[cfg(test)]
mod cursors_tests;
#[cfg(test)]
mod design_paste_tests;
+25 -37
View File
@@ -881,44 +881,32 @@ fn appearance(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
});
if indep_ok && ui.data(|d| d.get_temp::<bool>(Id::new("indep_corners"))).unwrap_or(false) {
let radii = common(doc, sel, |n| n.corners.radii);
let keys = [
("topLeftRadius", tr("Top left radius")),
("topRightRadius", tr("Top right radius")),
("bottomRightRadius", tr("Bottom right radius")),
("bottomLeftRadius", tr("Bottom left radius")),
// `k` indexes `radii` ([top left, top right, bottom right, bottom left]); the fields sit
// where their corners are: top left, top right / bottom left, bottom right.
let corners = [
("topLeftRadius", tr("Top left radius"), Icon::CornerTopLeft, 0),
("topRightRadius", tr("Top right radius"), Icon::CornerTopRight, 1),
("bottomLeftRadius", tr("Bottom left radius"), Icon::CornerBottomLeft, 3),
("bottomRightRadius", tr("Bottom right radius"), Icon::CornerBottomRight, 2),
];
row(ui, 32.0, |ui| {
for (k, (key, name)) in keys.iter().enumerate().take(2) {
let e = crate::variable_ui::number_field(
app,
ui,
&ids,
key,
Id::new(("corner", k)),
Lead::Icon(Icon::CornerRadius, name),
radii.map(|r| r[k]),
"",
col,
);
app.field(&ids, key, e, |v| json!(v.max(0.0)));
}
});
row(ui, 32.0, |ui| {
for (k, (key, name)) in keys.iter().enumerate().skip(2) {
let e = crate::variable_ui::number_field(
app,
ui,
&ids,
key,
Id::new(("corner", k)),
Lead::Icon(Icon::CornerRadius, name),
radii.map(|r| r[k]),
"",
col,
);
app.field(&ids, key, e, |v| json!(v.max(0.0)));
}
});
for pair in corners.chunks(2) {
row(ui, 32.0, |ui| {
for (key, name, icon, k) in pair {
let e = crate::variable_ui::number_field(
app,
ui,
&ids,
key,
Id::new(("corner", *k)),
Lead::Icon(*icon, name),
radii.map(|r| r[*k]),
"",
col,
);
app.field(&ids, key, e, |v| json!(v.max(0.0)));
}
});
}
let sm = common(doc, sel, |n| (n.corners.smoothing * 100.0).round());
widgets::sublabels(ui, tr("Corner smoothing"), None);
row(ui, 32.0, |ui| {