M8: an icon double-click keeps its selection when a modifier is held

egui reports a click on a double-click's release too, so the row's queued click ran after the zoom: with Ctrl held, a double-click on an unselected layer's icon toggled it back out of the selection, and Shift could restore a range. The icon branch now drops the queued click.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 queued; Sonnet 5.5 measured and wrote at medium
This commit is contained in:
Trevor Brown
2026-11-12 12:48:18 -05:00
co-authored by Claude Sonnet 5.5
parent 9a29a3cbb7
commit cf80dc1afa
2 changed files with 46 additions and 14 deletions
+44 -14
View File
@@ -842,6 +842,24 @@ fn search_fields_keep_their_text_in_the_middle() {
assert!(off < 1.0, "Search assets is {off} points off the middle");
}
/// The centre of `id`'s icon, a top-level row: the row's left edge is the first point from the
/// left at which the pointer is over it, and the icon's centre is 8 (margin) + 4 + 22 points
/// further right.
fn icon_of(u: &mut Ui, id: NodeId) -> Pos2 {
let y = u.row_at(id).y;
let mut left = None;
for x in 0..150 {
let p = Pos2::new(x as f32, y);
u.app.session.ds_mut().unwrap().hover = None;
u.frame(vec![Event::PointerMoved(p)]);
if u.app.session.ds().unwrap().hover == Some(id) {
left = Some(x as f32);
break;
}
}
Pos2::new(left.expect("the row has a left edge") + 34.0, y)
}
/// Measured in Figma's web app, 2026-11-12: a double-click on a layer's icon zooms the canvas to
/// the layer and starts no rename (a double-click on the name renames, as before).
#[test]
@@ -852,20 +870,7 @@ fn a_double_click_on_a_layers_icon_zooms_to_it_instead_of_renaming() {
s.execute("select.none", &json!({})).unwrap();
let mut u = Ui::new(s);
u.frame(vec![]);
let y = u.row_at(far).y;
// The row's left edge: the first point from the left at which the pointer is over it. At depth
// 0 the icon's centre is 8 (margin) + 4 + 22 points further right.
let mut left = None;
for x in 0..150 {
let p = Pos2::new(x as f32, y);
u.app.session.ds_mut().unwrap().hover = None;
u.frame(vec![Event::PointerMoved(p)]);
if u.app.session.ds().unwrap().hover == Some(far) {
left = Some(x as f32);
break;
}
}
let icon = Pos2::new(left.expect("the row has a left edge") + 34.0, y);
let icon = icon_of(&mut u, far);
let view = |u: &Ui| {
let v = &u.app.session.ds().unwrap().view;
(v.cx, v.cy, v.zoom)
@@ -884,3 +889,28 @@ fn a_double_click_on_a_layers_icon_zooms_to_it_instead_of_renaming() {
u.app.session.execute("view.zoomSelection", &json!({})).unwrap();
assert_eq!(after, view(&u), "the view frames the layer, as view.zoomSelection does");
}
/// A double-click's release is also a click on the row; with Ctrl held it must not toggle the layer
/// back out of the selection the zoom just made, nor can Shift restore a range.
#[test]
fn an_icon_double_click_keeps_its_selection_when_a_modifier_is_held() {
let mut s = Session::new();
rect(&mut s, "near", 0.0, None);
let far = rect(&mut s, "far", 3000.0, None);
s.execute("select.none", &json!({})).unwrap();
let mut u = Ui::new(s);
u.frame(vec![]);
let icon = icon_of(&mut u, far);
let before = (u.app.session.ds().unwrap().view.cx, u.app.session.ds().unwrap().view.zoom);
for _ in 0..2 {
for pressed in [true, false] {
let e = Event::PointerButton { pos: icon, button: egui::PointerButton::Primary, pressed, modifiers: CMD };
u.frame_with(vec![Event::PointerMoved(icon), e], CMD);
}
}
u.frame(vec![]);
assert_eq!(u.app.session.selection(), vec![far], "the layer stays selected");
assert_eq!(u.app.ui.rename, None);
let v = &u.app.session.ds().unwrap().view;
assert_ne!((v.cx, v.zoom), before, "the view frames the layer");
}
+2
View File
@@ -1269,6 +1269,8 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
app.run("select.set", json!({"ids": [row.id]}));
}
app.run("view.zoomSelection", json!({}));
// The release is also a click: it must not toggle or extend the selection.
clicked = None;
} else if !crate::menus::dev_mode_refuses(app, "object.rename") {
app.ui.rename = Some(Rename { id: row.id, start: true });
}