From d8cf69b38a5e586f3b96532b92165af23e8cc59d Mon Sep 17 00:00:00 2001 From: kobaltgit Date: Thu, 22 Oct 2026 12:34:58 +0300 Subject: [PATCH 1/3] fix(ui-egui): expand layer visibility eye hitbox and render sunken well (#8467) --- crates/ui-egui/src/layer_row_ui/tests.rs | 35 ++++++++++++++++++++++++ crates/ui-egui/src/panels.rs | 24 ++++++++++++---- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/layer_row_ui/tests.rs b/crates/ui-egui/src/layer_row_ui/tests.rs index c47f9522a2..a5c9ed3834 100644 --- a/crates/ui-egui/src/layer_row_ui/tests.rs +++ b/crates/ui-egui/src/layer_row_ui/tests.rs @@ -638,3 +638,38 @@ fn clicking_the_lock_icon_on_a_row_unlocks_the_layer() { assert!(doc.background().is_none(), "the Background became a normal layer"); assert!(!doc.layer(bg).unwrap().locks.any()); } + +#[test] +fn eye_column_edges_toggle_visibility_without_selecting_layer() { + // #8467: clicks at the far left or right edge of the eye column must toggle visibility + // rather than falling through to select the row. + let mut s = photocraft_engine::Session::new(); + s.execute("file.new", json!({"width": 64, "height": 48})).unwrap(); + let l1 = s.execute("layer.new.layer", json!({"name": "Top"})).unwrap()["layer"].as_u64().unwrap(); + let l0 = s.execute("layer.new.layer", json!({"name": "Bottom"})).unwrap()["layer"].as_u64().unwrap(); + s.execute("layer.select", json!({"layer": l0})).unwrap(); + + let mut h = harness(s, 1.0, "promedium", 290.0); + let visible = |h: &Harness<'_, PhotocraftApp>, id: u64| h.state().session.active().unwrap().doc.layer(photocraft_doc::LayerId(id)).unwrap().visible; + let selected = |h: &Harness<'_, PhotocraftApp>| h.state().session.active().unwrap().active_layer.unwrap().0; + + let row_for = |h: &Harness<'_, PhotocraftApp>, id: u64| { + recorded(&h.ctx).into_iter().find(|r| r.layer == id).unwrap().row + }; + + let top_row = row_for(&h, l1); + // Click at the far left edge of the eye column (left + 1.0, top + 1.0) + let left_edge = pos2(top_row.left() + 1.0, top_row.top() + 1.0); + click(&mut h, left_edge); + + assert!(!visible(&h, l1), "click on left edge of eye column should hide layer"); + assert_eq!(selected(&h), l0, "click on eye column must not select the layer"); + + // Click at the right edge of the eye column (left + 29.0, bottom - 1.0) + let right_edge = pos2(top_row.left() + 29.0, top_row.bottom() - 1.0); + click(&mut h, right_edge); + + assert!(visible(&h, l1), "click on right edge of eye column should show layer again"); + assert_eq!(selected(&h), l0, "click on eye column must not select the layer"); +} + diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 65efb1c130..2530e5b923 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -3209,14 +3209,28 @@ fn layer_row( painter.line_segment([rect.left_bottom(), rect.right_bottom()], Stroke::new(1.0, t.separator)); } let mut x = rect.left() + 6.0; - let eye = Rect::from_min_size(pos2(x, rect.center().y - 11.0), vec2(22.0, 22.0)); - // The eye takes drags too (so a drag starting on it never reorders the row): dragging down - // the eyes gives every row swept over the visibility the first eye toggled to. - let eye_resp = ui.interact(eye, ui.id().with(("eye", l.id.0)), Sense::click_and_drag()); - // A hidden layer's eye box is left empty (still clickable). + // The whole eye column takes drags too (so a drag starting on it never reorders the row): dragging down + // the eyes gives every row swept over the visibility the first eye toggled to (#8467). + let eye_resp = ui.interact(eye_cell, ui.id().with(("eye", l.id.0)), Sense::click_and_drag()); + // A hidden layer's eye box shows a sunken well indicating its eye socket (#8467). if l.visible { // Centre the drawing in the column while preserving the visibility hit area. icons::paint(ui, eye_cell, "eye", 15.0, t.layer_label_icon(l.label)); + } else { + let well = Rect::from_center_size(eye_cell.center(), vec2(12.0, 12.0)); + if t.bevel { + crate::widgets::surface(ui, well, t.field, false); + } else { + let (hi, lo) = if t.dark() { + (Color32::from_black_alpha(65), Color32::from_white_alpha(35)) + } else { + (Color32::from_black_alpha(55), Color32::from_white_alpha(70)) + }; + painter.line_segment([well.left_bottom(), well.left_top()], Stroke::new(1.0, hi)); + painter.line_segment([well.left_top(), well.right_top()], Stroke::new(1.0, hi)); + painter.line_segment([well.right_top(), well.right_bottom()], Stroke::new(1.0, lo)); + painter.line_segment([well.right_bottom(), well.left_bottom()], Stroke::new(1.0, lo)); + } } eye_sweep(ctx, l, rect, &eye_resp, actions); if eye_resp.clicked() { From ffdfa8b3f932bd2afe1e49e51479f3ae71651ae5 Mon Sep 17 00:00:00 2001 From: kobaltgit Date: Thu, 22 Oct 2026 12:53:08 +0300 Subject: [PATCH 2/3] style(ui-egui): fill hidden layer well interior with theme field color --- crates/ui-egui/src/panels.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 2530e5b923..f6c2de510a 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -3221,6 +3221,7 @@ fn layer_row( if t.bevel { crate::widgets::surface(ui, well, t.field, false); } else { + painter.rect_filled(well, t.radius_sm, t.field); let (hi, lo) = if t.dark() { (Color32::from_black_alpha(65), Color32::from_white_alpha(35)) } else { From 77caa32765dbfc87ee363e1f0cbf5418e2bb5c1c Mon Sep 17 00:00:00 2001 From: David Mann Date: Thu, 22 Oct 2026 21:10:34 +0200 Subject: [PATCH 3/3] Integration: keep a hidden layer's eye box empty (#8496) The PR widened the eye hit area to the whole column (#8467) but also painted a 12x12 sunken well in a hidden layer's eye box. The maintainer decided on #1662 that a hidden layer's eye box stays empty, as in Photoshop (and the hidden fx-row eye still does), so the well goes; the full-column hit area and its test stay. New test a_hidden_layers_eye_box_paints_nothing checks, in all five themes, that nothing is drawn inside a hidden layer's eye column while a visible layer's eye is; it fails with the well. Also drops the trailing blank line at EOF of layer_row_ui/tests.rs (cargo fmt). Co-authored-by: kobaltgit <159009675+kobaltgit@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/ui-egui/src/layer_row_ui/tests.rs | 35 ++++++++++++++++++++++-- crates/ui-egui/src/panels.rs | 18 +----------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/crates/ui-egui/src/layer_row_ui/tests.rs b/crates/ui-egui/src/layer_row_ui/tests.rs index a5c9ed3834..26bd8ee538 100644 --- a/crates/ui-egui/src/layer_row_ui/tests.rs +++ b/crates/ui-egui/src/layer_row_ui/tests.rs @@ -653,9 +653,7 @@ fn eye_column_edges_toggle_visibility_without_selecting_layer() { let visible = |h: &Harness<'_, PhotocraftApp>, id: u64| h.state().session.active().unwrap().doc.layer(photocraft_doc::LayerId(id)).unwrap().visible; let selected = |h: &Harness<'_, PhotocraftApp>| h.state().session.active().unwrap().active_layer.unwrap().0; - let row_for = |h: &Harness<'_, PhotocraftApp>, id: u64| { - recorded(&h.ctx).into_iter().find(|r| r.layer == id).unwrap().row - }; + let row_for = |h: &Harness<'_, PhotocraftApp>, id: u64| recorded(&h.ctx).into_iter().find(|r| r.layer == id).unwrap().row; let top_row = row_for(&h, l1); // Click at the far left edge of the eye column (left + 1.0, top + 1.0) @@ -673,3 +671,34 @@ fn eye_column_edges_toggle_visibility_without_selecting_layer() { assert_eq!(selected(&h), l0, "click on eye column must not select the layer"); } +/// #1662: a hidden layer's eye box stays empty, as in Photoshop (only a visible layer paints its +/// eye), in every theme; the widened hit area (#8467) changes nothing that is drawn. +#[test] +fn a_hidden_layers_eye_box_paints_nothing() { + fn bounds(shape: &egui::Shape, out: &mut Vec) { + match shape { + egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| bounds(s, out)), + s => out.push(s.visual_bounding_rect()), + } + } + for theme in ["promedium", "pro", "studio", "studiolight", "classic"] { + let mut s = photocraft_engine::Session::new(); + s.execute("file.new", json!({"width": 64, "height": 48})).unwrap(); + let hidden = s.execute("layer.new.layer", json!({"name": "Hidden"})).unwrap()["layer"].as_u64().unwrap(); + s.execute("layer.setProps", json!({"layer": hidden, "visible": false})).unwrap(); + let shown = s.execute("layer.new.layer", json!({"name": "Shown"})).unwrap()["layer"].as_u64().unwrap(); + let h = harness(s, 1.0, theme, 290.0); + let mut drawn = Vec::new(); + for clipped in &h.output().shapes { + bounds(&clipped.shape, &mut drawn); + } + let rows = recorded(&h.ctx); + let in_eye_box = |id: u64| { + let row = rows.iter().find(|r| r.layer == id).unwrap().row; + let cell = Rect::from_min_max(row.left_top(), pos2(row.left() + 30.0, row.bottom())).shrink(1.0); + drawn.iter().filter(|b| b.is_positive() && cell.contains_rect(**b)).count() + }; + assert!(in_eye_box(shown) > 0, "{theme}: a visible layer paints its eye"); + assert_eq!(in_eye_box(hidden), 0, "{theme}: a hidden layer's eye box is left empty"); + } +} diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index f6c2de510a..6e52b37b76 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -3212,26 +3212,10 @@ fn layer_row( // The whole eye column takes drags too (so a drag starting on it never reorders the row): dragging down // the eyes gives every row swept over the visibility the first eye toggled to (#8467). let eye_resp = ui.interact(eye_cell, ui.id().with(("eye", l.id.0)), Sense::click_and_drag()); - // A hidden layer's eye box shows a sunken well indicating its eye socket (#8467). + // A hidden layer's eye box is left empty (still clickable; the whole column takes the click, #8467). if l.visible { // Centre the drawing in the column while preserving the visibility hit area. icons::paint(ui, eye_cell, "eye", 15.0, t.layer_label_icon(l.label)); - } else { - let well = Rect::from_center_size(eye_cell.center(), vec2(12.0, 12.0)); - if t.bevel { - crate::widgets::surface(ui, well, t.field, false); - } else { - painter.rect_filled(well, t.radius_sm, t.field); - let (hi, lo) = if t.dark() { - (Color32::from_black_alpha(65), Color32::from_white_alpha(35)) - } else { - (Color32::from_black_alpha(55), Color32::from_white_alpha(70)) - }; - painter.line_segment([well.left_bottom(), well.left_top()], Stroke::new(1.0, hi)); - painter.line_segment([well.left_top(), well.right_top()], Stroke::new(1.0, hi)); - painter.line_segment([well.right_top(), well.right_bottom()], Stroke::new(1.0, lo)); - painter.line_segment([well.right_bottom(), well.left_bottom()], Stroke::new(1.0, lo)); - } } eye_sweep(ctx, l, rect, &eye_resp, actions); if eye_resp.clicked() {