diff --git a/crates/ui-egui/src/brush_panel.rs b/crates/ui-egui/src/brush_panel.rs index e388b60f51..d91e6ff459 100644 --- a/crates/ui-egui/src/brush_panel.rs +++ b/crates/ui-egui/src/brush_panel.rs @@ -295,15 +295,12 @@ fn section_list(app: &mut PhotocraftApp, ui: &mut egui::Ui, b: &mut BrushSetting }); resp }); - if *flag { - ui.painter().rect_filled(br, 2.0, t.accent); - let (a, m, c) = (br.left_center() + vec2(3.0, 0.5), br.center_bottom() + vec2(-1.0, -3.5), br.right_top() + vec2(-3.0, 3.5)); - ui.painter().line_segment([a, m], Stroke::new(1.6, Color32::WHITE)); - ui.painter().line_segment([m, c], Stroke::new(1.6, Color32::WHITE)); - } else { - ui.painter().rect_stroke(br, 2.0, Stroke::new(1.2, t.text_faint), egui::StrokeKind::Inside); - } let on_lock = resp.interact_pointer_pos().is_some_and(|p| p.x > r.right() - 22.0); + // A press on the row (but not its lock) presses the box too: a release there + // turns the section on, or toggles a checkbox-only row (#4476). + let pressed = box_resp.as_ref().is_some_and(|b| b.is_pointer_button_down_on()) || (resp.is_pointer_button_down_on() && !on_lock); + let hovered = box_resp.as_ref().is_some_and(|b| b.hovered()) || (toggle_only && resp.hovered()); + widgets::check_box(ui.painter(), br, *flag, hovered, pressed, &t); if box_resp.is_some_and(|resp| resp.clicked()) || (toggle_only && resp.clicked() && !on_lock) { *flag = !*flag; } else if !toggle_only && resp.clicked() && !on_lock { diff --git a/crates/ui-egui/src/brush_panel_tests.rs b/crates/ui-egui/src/brush_panel_tests.rs index 7723bede12..133d849930 100644 --- a/crates/ui-egui/src/brush_panel_tests.rs +++ b/crates/ui-egui/src/brush_panel_tests.rs @@ -1285,3 +1285,101 @@ fn brushes_button_at_top_of_brush_settings_opens_brushes_panel() { assert!(h.state().ui.panels.brush_settings); assert_eq!(h.state().ui.brush_tab, 1); } + +/// The fill painted for the check box in `rect` in the last frame (the topmost filled rect there). +fn box_fill(h: &Harness<'_, PhotocraftApp>, rect: egui::Rect) -> Option { + h.output() + .shapes + .iter() + .filter_map(|c| match &c.shape { + egui::Shape::Rect(r) if (r.rect.min - rect.min).length() < 1.0 && (r.rect.max - rect.max).length() < 1.0 && r.fill != Color32::TRANSPARENT => { + Some(r.fill) + } + _ => None, + }) + .next_back() +} + +/// #4476: while the mouse is down on a section's row or its box, the box shows it's pressed, as +/// in Photoshop: an unchecked box darkens, a checked one turns less opaque. The release then +/// acts as before. +#[test] +fn section_boxes_show_a_pressed_state() { + use egui_kittest::kittest::Queryable; + let mut h = app_harness(); + h.key_press(egui::Key::F5); + h.run_steps(3); + let t = Tokens::get(&h.ctx); + let boxr = h.get_by_label("Enable Shape Dynamics").rect(); + let row = h.get_by_label("Shape Dynamics").rect(); + assert!(!h.state().session.tools.brush.shape_dynamics.enabled); + assert_eq!(box_fill(&h, boxr), Some(widgets::check_box_fill(false, false, &t))); + // Held on the row's name: the unchecked box darkens; the release turns the section on. + let at = egui::pos2(row.center().x, row.center().y); + h.drag_at(at); + h.run_steps(2); + assert_eq!(box_fill(&h, boxr), Some(widgets::check_box_fill(false, true, &t)), "pressed, unchecked"); + h.drop_at(at); + h.run_steps(3); + assert!(h.state().session.tools.brush.shape_dynamics.enabled); + assert_eq!(box_fill(&h, boxr), Some(widgets::check_box_fill(true, false, &t))); + // Held on the checked box itself: less opaque, then the release turns it off. + h.drag_at(boxr.center()); + h.run_steps(2); + assert_eq!(box_fill(&h, boxr), Some(widgets::check_box_fill(true, true, &t)), "pressed, checked"); + h.drop_at(boxr.center()); + h.run_steps(3); + assert!(!h.state().session.tools.brush.shape_dynamics.enabled); + // A checkbox-only row (no settings of its own) is one checkbox across the row: a press + // anywhere on it presses its box, and the release toggles it. + let wet = h.get_by_label("Wet Edges").rect(); + let wet_box = egui::Rect::from_center_size(egui::pos2(wet.left() + 12.0, wet.center().y), vec2(13.0, 13.0)); + let was = h.state().session.tools.brush.wet_edges; + h.drag_at(wet.center()); + h.run_steps(2); + assert_eq!(box_fill(&h, wet_box), Some(widgets::check_box_fill(was, true, &t)), "pressed checkbox-only row"); + h.drop_at(wet.center()); + h.run_steps(3); + assert_ne!(h.state().session.tools.brush.wet_edges, was); + // The four looks are distinct. + let looks = [(false, false), (false, true), (true, false), (true, true)].map(|(on, pressed)| widgets::check_box_fill(on, pressed, &t)); + for (i, a) in looks.iter().enumerate() { + assert!(looks.iter().skip(i + 1).all(|b| b != a), "{looks:?}"); + } +} + +/// The shared check box (options bars, dialogs) does the same, and pressing its label counts. +#[test] +fn checkboxes_show_a_pressed_state_from_their_label() { + use egui_kittest::kittest::Queryable; + let mut h = Harness::builder().with_size(vec2(300.0, 100.0)).build_ui_state( + |ui, on: &mut bool| { + widgets::checkbox(ui, on, "Sample All Layers"); + }, + false, + ); + h.run(); + let t = Tokens::get(&h.ctx); + let label = h.get_by_label("Sample All Layers").rect(); + let boxr = egui::Rect::from_min_size(egui::pos2(label.left() - 6.0 - 14.0, label.center().y - 7.0), vec2(14.0, 14.0)); + let fill = |h: &Harness<'_, bool>| { + h.output() + .shapes + .iter() + .filter_map(|c| match &c.shape { + egui::Shape::Rect(r) if (r.rect.center() - boxr.center()).length() < 1.5 && r.rect.width() < 15.0 && r.fill != Color32::TRANSPARENT => { + Some(r.fill) + } + _ => None, + }) + .next_back() + }; + assert_eq!(fill(&h), Some(widgets::check_box_fill(false, false, &t))); + h.drag_at(label.center()); + h.run(); + assert_eq!(fill(&h), Some(widgets::check_box_fill(false, true, &t)), "pressing the label presses the box"); + h.drop_at(label.center()); + h.run(); + assert!(*h.state(), "the release checks it"); + assert_eq!(fill(&h), Some(widgets::check_box_fill(true, false, &t))); +} diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 32f5cd457d..474bef66e4 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -1603,21 +1603,12 @@ pub fn checkbox(ui: &mut Ui, on: &mut bool, label: &str) -> Response { ui.spacing_mut().item_spacing.x = 6.0; let (rect, resp) = ui.allocate_exact_size(vec2(14.0, 14.0), Sense::click()); let resp = resp.on_hover_cursor(egui::CursorIcon::PointingHand); - let p = ui.painter(); - if *on { - p.rect_filled(rect, 2.0, t.accent); - let a = rect.left_center() + vec2(3.0, 0.5); - let b = rect.center_bottom() + vec2(-1.0, -3.5); - let c = rect.right_top() + vec2(-3.0, 3.5); - p.line_segment([a, b], Stroke::new(1.8, Color32::WHITE)); - p.line_segment([b, c], Stroke::new(1.8, Color32::WHITE)); - } else { - p.rect_filled(rect, 2.0, t.field); - p.rect_stroke(rect, 2.0, Stroke::new(1.5, if resp.hovered() { t.text_dim } else { t.text_faint }), StrokeKind::Inside); - } let l = ui .add(egui::Label::new(egui::RichText::new(tl!(label)).color(t.text_dim)).selectable(false).sense(Sense::click())) .on_hover_cursor(egui::CursorIcon::PointingHand); + // Pressing the label presses the box (#4476). + let pressed = resp.is_pointer_button_down_on() || l.is_pointer_button_down_on(); + check_box(ui.painter(), rect, *on, resp.hovered() || l.hovered(), pressed, &t); resp.union(l) }) .inner; @@ -1628,6 +1619,34 @@ pub fn checkbox(ui: &mut Ui, on: &mut bool, label: &str) -> Response { resp } +/// The fill of a check box: the accent when `on`, else the field colour. While it's pressed +/// (the mouse is down on it or on its label) Photoshop shows that a release will act: an +/// unchecked box darkens a little and a checked one turns a little less opaque (#4476). +pub fn check_box_fill(on: bool, pressed: bool, t: &Tokens) -> Color32 { + match (on, pressed) { + (true, false) => t.accent, + (true, true) => t.accent.gamma_multiply(0.75), + (false, false) => t.field, + (false, true) => t.field.lerp_to_gamma(Color32::BLACK, 0.18), + } +} + +/// Paint a check box in `rect`: [`check_box_fill`] with a white tick when `on`, else an +/// outline that brightens while `hovered` or `pressed`. +pub fn check_box(p: &egui::Painter, rect: Rect, on: bool, hovered: bool, pressed: bool, t: &Tokens) { + p.rect_filled(rect, 2.0, check_box_fill(on, pressed, t)); + if on { + let a = rect.left_center() + vec2(3.0, 0.5); + let b = rect.center_bottom() + vec2(-1.0, -3.5); + let c = rect.right_top() + vec2(-3.0, 3.5); + let tick = Stroke::new(rect.width() * 0.125, Color32::WHITE); + p.line_segment([a, b], tick); + p.line_segment([b, c], tick); + } else { + p.rect_stroke(rect, 2.0, Stroke::new(1.5, if hovered || pressed { t.text_dim } else { t.text_faint }), StrokeKind::Inside); + } +} + /// Small chevron for dropdowns (replaces egui's filled triangle). pub fn chevron_icon(ui: &Ui, rect: Rect, visuals: &egui::style::WidgetVisuals, _open: bool) { let c = rect.center();