M8: a double-click on a layer's icon zooms to it, as in Figma

Measured in Figma's web app on 2026-11-12: a double-click on a layer's icon in the Layers panel zooms the canvas to the layer and starts no rename; a double-click on the name renames. The row now checks whether the double-click fell within the rect it paints the icon in, and if so selects the layer and runs view.zoomSelection. A double-click elsewhere on the row is unchanged.

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 10:55:40 -05:00
co-authored by Claude Sonnet 5.5
parent 0d6ce1124c
commit 9a29a3cbb7
3 changed files with 59 additions and 5 deletions
@@ -841,3 +841,46 @@ fn search_fields_keep_their_text_in_the_middle() {
let off = off_centre(&mut u, "Search assets");
assert!(off < 1.0, "Search assets is {off} points off the middle");
}
/// 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]
fn a_double_click_on_a_layers_icon_zooms_to_it_instead_of_renaming() {
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 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 view = |u: &Ui| {
let v = &u.app.session.ds().unwrap().view;
(v.cx, v.cy, v.zoom)
};
let before = view(&u);
for _ in 0..2 {
u.button(icon, true);
u.button(icon, false);
}
u.frame(vec![]);
assert_eq!(u.app.ui.rename, None, "a double-click on the icon renames nothing");
assert_eq!(u.app.session.selection(), vec![far], "the layer is selected");
let after = view(&u);
assert_ne!(after, before, "the view moved");
u.app.session.execute("view.set", &json!({"cx": before.0, "cy": before.1, "zoom": before.2})).unwrap();
u.app.session.execute("view.zoomSelection", &json!({})).unwrap();
assert_eq!(after, view(&u), "the view frames the layer, as view.zoomSelection does");
}
+13 -3
View File
@@ -1239,7 +1239,8 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
let icon = node_icon(&doc, row.id);
let shown = n.visible && !row.dimmed;
let icon_col = row_color(&t, is_comp || icon == Icon::Slot, shown, t.icon, t.text_tertiary);
icons::paint(ui.painter(), Rect::from_center_size(pos2(x0 + 22.0, r.center().y), vec2(14.0, 14.0)), icon, icon_col);
let icon_rect = Rect::from_center_size(pos2(x0 + 22.0, r.center().y), vec2(14.0, 14.0));
icons::paint(ui.painter(), icon_rect, icon, icon_col);
let name_x = x0 + 36.0;
if app.ui.rename.is_some_and(|rn| rn.id == row.id) {
if let Some(new) = rename_field(app, ui, row.id, row.depth, &n.name, r) {
@@ -1260,8 +1261,17 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
if resp.clicked() {
clicked = Some((row.id, ui.input(|i| i.modifiers)));
}
if resp.double_clicked() && !crate::menus::dev_mode_refuses(app, "object.rename") {
app.ui.rename = Some(Rename { id: row.id, start: true });
if resp.double_clicked() {
// On the icon it zooms to the layer and renames nothing (measured in Figma's web
// app, 2026-11-12); elsewhere on the row it renames.
if resp.interact_pointer_pos().is_some_and(|p| icon_rect.contains(p)) {
if sel.as_slice() != [row.id] {
app.run("select.set", json!({"ids": [row.id]}));
}
app.run("view.zoomSelection", json!({}));
} else if !crate::menus::dev_mode_refuses(app, "object.rename") {
app.ui.rename = Some(Rename { id: row.id, start: true });
}
}
if resp.secondary_clicked() {
if !sel.contains(&row.id) {
+3 -2
View File
File diff suppressed because one or more lines are too long