Merge pull request #1521 from Carbocode/feat/corner-smoothing-scrub

M1: add a draggable corner smoothing icon
This commit is contained in:
BFlat
2026-10-30 00:24:22 +07:00
committed by GitHub
5 changed files with 88 additions and 4 deletions
@@ -70,6 +70,75 @@ fn ios_button_sets_corner_smoothing_to_60_percent() {
assert_eq!(u.app.session.undo_depth().0, history + 1);
}
/// The visible smoothing icon uses the same scrub gesture, cursor and modifier rates as the
/// other numeric fields; the whole drag is one undo step, including when clamped at either end.
#[test]
fn corner_smoothing_icon_scrubs_with_modifiers_and_one_undo_step() {
for (dx, modifiers, want) in
[(16.0, Modifiers::NONE, 0.66), (8.0, Modifiers::SHIFT, 1.0), (16.0, Modifiers::ALT, 0.516), (-80.0, Modifiers::NONE, 0.0)]
{
let (mut u, r) = scene();
u.app.run("node.set", json!({"ids": [r], "props": {"cornerSmoothing": 0.5}}));
let toggle = named_button(&mut u, "Individual corners");
click(&mut u, toggle);
let lead = u.ctx.read_response(Id::unique("smoothing").with("lead")).expect("smoothing icon").rect;
assert_eq!(lead.width(), 24.0, "the icon has the same drag target as other fields");
let from = lead.center();
let out = u.frame_output(vec![Event::PointerMoved(from)]);
assert_eq!(out.platform_output.cursor_icon, egui::CursorIcon::ResizeHorizontal);
let history = u.app.session.undo_depth().0;
u.frame_with(vec![Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers }], modifiers);
for step in 1..=4 {
u.frame_with(vec![Event::PointerMoved(pos2(from.x + dx * step as f32 / 4.0, from.y))], modifiers);
}
let smoothing = |u: &Ui| u.app.session.doc().unwrap().node(r).unwrap().corners.smoothing;
assert!((smoothing(&u) - want).abs() < 1e-9, "the drag updates the canvas before release");
u.frame_with(
vec![Event::PointerButton { pos: pos2(from.x + dx, from.y), button: egui::PointerButton::Primary, pressed: false, modifiers }],
modifiers,
);
u.frame(vec![]);
assert!((smoothing(&u) - want).abs() < 1e-9, "release keeps the scrubbed value");
assert_eq!(u.app.session.undo_depth().0, history + 1);
u.app.session.undo().unwrap();
assert_eq!(smoothing(&u), 0.5);
u.app.session.redo().unwrap();
assert!((smoothing(&u) - want).abs() < 1e-9);
}
}
/// Fine drags keep their fractional baseline across gestures, including a later plain drag.
#[test]
fn consecutive_smoothing_drags_preserve_fractional_percentages() {
let (mut u, r) = scene();
u.app.run("node.set", json!({"ids": [r], "props": {"cornerSmoothing": 0.5}}));
let toggle = named_button(&mut u, "Individual corners");
click(&mut u, toggle);
let history = u.app.session.undo_depth().0;
let smoothing = |u: &Ui| u.app.session.doc().unwrap().node(r).unwrap().corners.smoothing;
for (step, (dx, modifiers, want)) in
[(16.0, Modifiers::ALT, 0.516), (16.0, Modifiers::ALT, 0.532), (8.0, Modifiers::NONE, 0.612)].into_iter().enumerate()
{
let from = u.ctx.read_response(Id::unique("smoothing").with("lead")).unwrap().rect.center();
u.frame_with(vec![Event::PointerMoved(from)], modifiers);
u.frame_with(vec![Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers }], modifiers);
for k in 1..=4 {
u.frame_with(vec![Event::PointerMoved(pos2(from.x + dx * k as f32 / 4.0, from.y))], modifiers);
}
u.frame_with(
vec![Event::PointerButton { pos: pos2(from.x + dx, from.y), button: egui::PointerButton::Primary, pressed: false, modifiers }],
modifiers,
);
u.frame(vec![]);
assert!((smoothing(&u) - want).abs() < 1e-9, "gesture {step}: expected {want}, got {}", smoothing(&u));
assert_eq!(u.app.session.undo_depth().0, history + step + 1);
}
for want in [0.532, 0.516, 0.5] {
u.app.session.undo().unwrap();
assert!((smoothing(&u) - want).abs() < 1e-9, "undo restores the precise value");
}
}
/// 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]
+2
View File
@@ -89,6 +89,8 @@ icons! {
FlipH,
FlipV,
CornerRadius,
/// A smooth corner with its two tangent endpoints.
CornerSmoothing,
/// Individual corners: the rounded corner each field sets.
CornerTopLeft,
CornerTopRight,
+12
View File
@@ -510,6 +510,18 @@ fn draw(p: &P, icon: Icon) {
p.fill(&[(3.0, 9.0), (13.0, 9.0), (13.0, 14.0)]);
}
CornerRadius | CornerTopLeft => p.corner(2.5, 2.5, 1.0, 1.0, 11.5, 6.0),
CornerSmoothing => {
// An original superellipse corner, with dots marking its tangent endpoints.
let points: Vec<_> = (0..=24)
.map(|i| {
let a = i as f32 / 24.0 * FRAC_PI_2;
(2.5 + 11.0 * (1.0 - a.cos().max(0.0).sqrt()), 2.5 + 11.0 * (1.0 - a.sin().max(0.0).sqrt()))
})
.collect();
p.path(&points, false);
p.dot(2.5, 13.5, 1.0);
p.dot(13.5, 2.5, 1.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),
+2 -2
View File
@@ -113,10 +113,10 @@ pub(super) fn appearance(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[N
}
});
}
let sm = common(doc, sel, |n| (n.corners.smoothing * 100.0).round());
let sm = common(doc, sel, |n| n.corners.smoothing * 100.0);
widgets::sublabels(ui, tr("Corner smoothing"), None);
row(ui, 32.0, |ui| {
let e = widgets::number_field(ui, Id::unique("smoothing"), Lead::None, sm, "%", col, 1.0);
let e = widgets::number_field(ui, Id::unique("smoothing"), Lead::Icon(Icon::CornerSmoothing, tr("Corner smoothing")), sm, "%", col, 1.0);
app.field(&ids, "cornerSmoothing", e, |v| json!((v / 100.0).clamp(0.0, 1.0)));
if widgets::button(ui, tr("iOS"), false, true).on_hover_text(tr("Set corner smoothing to 60%")).clicked() {
app.set_props(&ids, json!({"cornerSmoothing": 0.6}));
+3 -2
View File
File diff suppressed because one or more lines are too long