mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
* Checkboxes: a pressed box shows it, as in Photoshop (#4476) Pressing a checkbox, its label or a Brush Settings section row gave no feedback until the release. While pressed, an unchecked box now darkens a little and a checked one turns a little less opaque. widgets::check_box paints the box for both the shared checkbox and the Brush Settings section list (which drew its own). The section boxes now get the field fill like every other checkbox. Tests: section_boxes_show_a_pressed_state, checkboxes_show_a_pressed_state_from_their_label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Checkboxes: checkbox-only Brush Settings rows show the pressed state too main made the rows without settings (Wet Edges, Build-up, ...) one checkbox across the row; a press anywhere on such a row presses its box. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
45ce763ebb
commit
bb4e55ebb4
@@ -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 {
|
||||
|
||||
@@ -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<Color32> {
|
||||
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)));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user