Fix #577: the gradient stop popover stays open after the click that opens it

The Gradient panel's colour chip opens the stop popover on a click. The
popover closes on a click elsewhere, and the opening click itself, which
is released in the frame the popover first draws and lies outside it,
counted as one: the popover closed at once (a double-click got through,
as the popover lets the double-click that opens it from the canvas pass).

The popover now records the frame it first drew in and only a click in a
later frame closes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-09 03:48:27 +07:00
parent 28846a98a0
commit ba93f62bcb
2 changed files with 44 additions and 8 deletions

File diff suppressed because one or more lines are too long

View File

@ -29,6 +29,9 @@ use crate::widgets::{self, Live};
pub(super) const SPEC: DialogSpec = DialogSpec::window(show, confirm);
/// The field holding the frame the popover was first drawn in.
const OPENED: &str = "__opened";
/// What the popover edits: the selected stop of the gradient behind the active proxy (with all
/// its stops), or the selected point of a freeform gradient.
enum Target {
@ -131,12 +134,18 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
});
})
.response;
if let (Some(tab), Some(d)) = (tab, app.ui.dialog.as_mut()) {
d.fields.insert("tab".into(), json!(tab));
// The frame it opened in (its first drawn).
let frame = ctx.cumulative_frame_nr();
let opened = d.fields.get(OPENED).and_then(Value::as_u64).unwrap_or(frame);
if let Some(d) = app.ui.dialog.as_mut() {
if let Some(tab) = tab {
d.fields.insert("tab".into(), json!(tab));
}
d.fields.insert(OPENED.into(), json!(opened));
}
// The double-click that opened the popover lands on its chip, outside it: only a later click
// elsewhere closes it.
if resp.clicked_elsewhere() && !ctx.input(|i| i.pointer.button_double_clicked(egui::PointerButton::Primary)) {
// The click that opened the popover (the Gradient panel's colour chip) or the double-click
// (a stop's chip) lands outside it: only a later click elsewhere closes it (#577).
if frame > opened && resp.clicked_elsewhere() && !ctx.input(|i| i.pointer.button_double_clicked(egui::PointerButton::Primary)) {
app.ui.dialog = None;
}
}
@ -246,10 +255,18 @@ mod tests {
}
}
thread_local! {
// One context per test, so frames follow one another as in the app.
static CTX: egui::Context = {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
ctx
};
}
/// One headless frame of the dialog layer and the shortcuts.
fn frame(app: &mut VectorcraftApp, events: Vec<egui::Event>) {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let ctx = CTX.with(Clone::clone);
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
crate::shortcuts::handle(app, ui.ctx());
super::super::show(app, ui.ctx());
@ -312,6 +329,25 @@ mod tests {
assert!(app.ui.dialog.is_none(), "a click elsewhere closes it");
}
/// #577: the Gradient panel's colour chip opens it with a click whose release lands in the
/// frame it opens in, outside it: that click doesn't close it.
#[test]
fn the_click_that_opens_it_from_the_panel_keeps_it_open() {
let mut app = app();
let at = egui::pos2(5.0, 5.0);
frame(&mut app, vec![egui::Event::PointerMoved(at), click(at, true)]);
// The panel opens it while the click is released.
app.ui.dialog = Some(Dialog::new("gradientStop", json!({"index": 1, "screen": [200.0, 200.0], "tab": "color"})));
frame(&mut app, vec![click(at, false)]);
for _ in 0..3 {
frame(&mut app, vec![]);
}
assert!(app.ui.dialog.is_some(), "the opening click must not close it");
let away = egui::pos2(5.0, 600.0);
frame(&mut app, vec![egui::Event::PointerMoved(away), click(away, true), click(away, false)]);
assert!(app.ui.dialog.is_none(), "a later click elsewhere closes it");
}
#[test]
fn the_popover_closes_when_its_stop_goes_away() {
let mut app = app();