diff --git a/crates/ui-egui/src/colorpicker.rs b/crates/ui-egui/src/colorpicker.rs index 4539d0c0a..13fb16b9c 100644 --- a/crates/ui-egui/src/colorpicker.rs +++ b/crates/ui-egui/src/colorpicker.rs @@ -465,8 +465,14 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) { let pos = beside.unwrap_or(pos2(screen.max.x - app.ui.right_width - 260.0, 140.0)); // The colour picker's own header — title 16 in, the X 8 in from the right — is the shared // popover chrome now; the picker keeps its body. - let mut pop = app.ui.picker_popover.take().unwrap_or_else(|| Popover::new("color_picker", tr("Custom")).pad(0.0).close_on_outside(false)); - let closed = pop.show(ctx, Open::At(pos), Hold::default(), |ui| { + let mut pop = app.ui.picker_popover.take().unwrap_or_else(|| Popover::new("color_picker", tr("Custom")).pad(0.0)); + // Closing is the shared widget's: the X, Escape and a press outside it. Presses in the panel + // beside it keep it (swatches and fields there switch the colour it edits); the control that + // opened it is held too, so pressing it again reaches its own toggle. + let panel = Rect::from_min_max(pos2(screen.max.x - app.ui.right_width, screen.min.y), screen.max); + let mut held: Vec = vec![panel]; + held.extend(app.ui.picker_button); + let closed = pop.show(ctx, Open::At(pos), Hold { rects: &held, escape: false }, |ui| { ui.spacing_mut().item_spacing = vec2(8.0, 8.0); // Paint type. if !matches!( @@ -855,10 +861,20 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) { /// The colour picker closes (its X, Escape, or another popover took over): pending edits commit /// and the picker with its popover go. +/// Escape with the picker open closes it (the selection stays). +pub fn close_if_open(app: &mut FigApp) -> bool { + if app.ui.picker.is_none() { + return false; + } + close(app); + true +} + pub fn close(app: &mut FigApp) { finish(app); app.ui.picker = None; app.ui.picker_popover = None; + app.ui.picker_button = None; } /// Swatches wrapped in rows inset 16 points from both sides of the picker. diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 6f1f669a9..9b8ee845d 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -490,6 +490,9 @@ pub struct UiState { pub picker: Option, /// The colour picker's popover while it is open (its place survives a drag). pub picker_popover: Option, + /// The control that opened the picker, refreshed every frame it is drawn (the panel scrolls): + /// a press in it is held out of the popover's closing, so its own click toggles the picker. + pub picker_button: Option, pub context_menu: Option<(egui::Pos2, Vec)>, /// The context menu was asked for from the keyboard (`view.contextMenu`): it opens at the /// focused layer row or the canvas selection and takes the keyboard. @@ -635,6 +638,7 @@ impl Default for UiState { preview: None, picker: None, picker_popover: None, + picker_button: None, context_menu: None, key_menu: false, key_box: None, diff --git a/crates/ui-egui/src/panel_remove_tests.rs b/crates/ui-egui/src/panel_remove_tests.rs index 3805dca38..5a45d54ea 100644 --- a/crates/ui-egui/src/panel_remove_tests.rs +++ b/crates/ui-egui/src/panel_remove_tests.rs @@ -167,3 +167,36 @@ fn adding_a_fill_to_mixed_fills_opens_no_picker() { assert_eq!((doc.node(a).unwrap().fills.len(), doc.node(b).unwrap().fills.len()), (2, 3)); assert_eq!(u.app.ui.picker, None); } + +/// The shared popover's closing reaches the colour picker: a press on the canvas closes it. +#[test] +fn a_press_on_the_canvas_closes_the_colour_picker() { + let mut s = Session::new(); + let r = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap()); + s.execute("fill.add", &json!({"ids": [r], "color": "#FF0000"})).unwrap(); + s.execute("select.set", &json!({"ids": [r]})).unwrap(); + let mut u = Ui::new(s); + u.ctx.enable_accesskit(); + let sw = u.ctx.read_response(Id::new(("swatch", true, 0usize))).unwrap().rect.center(); + click(&mut u, sw); + assert!(u.app.ui.picker.is_some(), "the swatch opens the picker"); + click(&mut u, Pos2::new(320.0, 300.0)); + assert_eq!(u.app.ui.picker, None, "a press on the canvas closes the picker"); +} + +/// Pressing the swatch that opened the picker again closes it: its press is held out of the +/// popover's outside-press closing, so the swatch's own toggle gets the click. +#[test] +fn the_swatch_that_opened_the_colour_picker_closes_it_again() { + let mut s = Session::new(); + let r = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap()); + s.execute("fill.add", &json!({"ids": [r], "color": "#FF0000"})).unwrap(); + s.execute("select.set", &json!({"ids": [r]})).unwrap(); + let mut u = Ui::new(s); + u.ctx.enable_accesskit(); + let sw = u.ctx.read_response(Id::new(("swatch", true, 0usize))).unwrap().rect.center(); + click(&mut u, sw); + assert!(u.app.ui.picker.is_some(), "the swatch opens the picker"); + click(&mut u, sw); + assert_eq!(u.app.ui.picker, None, "the same swatch closes it again"); +} diff --git a/crates/ui-egui/src/right.rs b/crates/ui-egui/src/right.rs index 759c4254a..85cc9ec73 100644 --- a/crates/ui-egui/src/right.rs +++ b/crates/ui-egui/src/right.rs @@ -1547,6 +1547,9 @@ fn paint_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, p: &Paint, let sw = Rect::from_min_size(pos2(r.min.x + 4.0, r.center().y - 8.0), vec2(16.0, 16.0)); widgets::paint_swatch(ui.painter(), sw, p); let sresp = ui.interact(sw.expand(2.0), Id::new(("swatch", fills, i)), Sense::click()); + if app.ui.picker.as_ref() == Some(&crate::colorpicker::PickerTarget::Paint { fills, index: i, ids: ids.clone() }) { + app.ui.picker_button = Some(sresp.rect); + } if sresp.clicked() { app.toggle_picker(crate::colorpicker::PickerTarget::Paint { fills, index: i, ids: ids.clone() }, Some(sresp.rect.min.y)); } diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index f31979600..1c0f3d49f 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -241,6 +241,11 @@ fn key_event(app: &mut FigApp, key: Key, physical: Option, m: Modifiers, re if key == Key::Escape && crate::selection_spacing::cancel(app, ctx) { return true; } + // The colour picker open: Escape closes it here, before the binding deselects — unless one of + // its own dropdowns is open, which takes this Escape. + if key == Key::Escape && !egui::Popup::is_any_open(ctx) && crate::colorpicker::close_if_open(app) { + return true; + } if key_box(app, key, m) { return true; }