Window: panels shown from the edge hide only after the pointer moves away or a hold ends

The hide timer started on every frame with no pointer over the window, so
panels shown by window.panels.peek hid 0.3 s later while the pointer had not
moved. It now starts when the pointer moves, is released or leaves the window,
and a pointer entering the window counts as a move on its first frame.

When a press, a dialog, a popup or a flyout stops holding the panels open
with the pointer away from them, the timer starts as well, so closing a
flyout with Escape hides them without the pointer moving.
This commit is contained in:
dotCooCoo
2026-11-15 06:52:18 -08:00
parent 3075889f0c
commit c4754df6df
2 changed files with 54 additions and 7 deletions
+15 -7
View File
@@ -41,6 +41,8 @@ struct Shown {
edge: Edge,
/// When the pointer left them; `None` while it is over them or has not moved since they showed.
left_at: Option<f64>,
/// Whether a press, a modal dialog, a popup or a flyout held them open last frame.
held: bool,
}
/// The session's state of the panels shown from the edges.
@@ -102,9 +104,11 @@ pub(super) fn update(app: &mut VectorcraftApp, ctx: &egui::Context, area: egui::
return None;
}
let (now, pointer, down, moved) = ctx.input(|i| {
// A release counts as moving away: a press may have held the panels while it left them.
let moved =
i.pointer.delta() != egui::Vec2::ZERO || i.pointer.any_released() || i.events.iter().any(|e| matches!(e, egui::Event::PointerGone));
// A release counts as moving away: a press may have held the panels while it left them. A
// pointer entering the window has no delta on its first frame, so the move event counts too.
let moved = i.pointer.delta() != egui::Vec2::ZERO
|| i.pointer.any_released()
|| i.events.iter().any(|e| matches!(e, egui::Event::PointerMoved(_) | egui::Event::PointerGone));
(i.time, i.pointer.hover_pos(), i.pointer.any_down(), moved)
});
// A workspace switched to while panels show may dock none on that side.
@@ -123,11 +127,15 @@ pub(super) fn update(app: &mut VectorcraftApp, ctx: &egui::Context, area: egui::
Edge::Right => p.x > uncovered.right(),
}
});
if over || down || blocked || flyout {
let held = down || blocked || flyout;
if over || held {
shown.left_at = None;
} else if moved || pointer.is_none() {
} else if moved || shown.held {
// A hold that ends with the pointer away from them counts as moving away, as closing a
// popup or flyout with Escape does not move the pointer.
shown.left_at.get_or_insert(now);
}
shown.held = held;
match shown.left_at {
Some(left) if now - left >= GRACE => {
st.shown = None;
@@ -160,7 +168,7 @@ pub(super) fn update(app: &mut VectorcraftApp, ctx: &egui::Context, area: egui::
};
if now - since >= DELAY {
st.resting = None;
st.shown = Some(Shown { edge, left_at: None });
st.shown = Some(Shown { edge, left_at: None, held: false });
return Some(edge);
}
st.resting = Some((edge, since));
@@ -233,7 +241,7 @@ pub(super) fn command(app: &mut VectorcraftApp, params: &Value) -> Result<Value,
}
let st = &mut app.dock_rt.peek;
st.resting = None;
st.shown = Some(Shown { edge, left_at: None });
st.shown = Some(Shown { edge, left_at: None, held: false });
Ok(json!(edge.id()))
}
None => {
+39
View File
@@ -642,6 +642,45 @@ fn window_panels_peek_rejects_what_it_cannot_show_and_changes_nothing() {
h.app.ui.screen_mode = 0;
}
#[test]
fn panels_shown_by_window_panels_peek_stay_until_the_pointer_moves_or_leaves_the_window() {
let mut h = Harness::new();
let wide = hide_with_tab(&mut h);
assert_eq!(h.app.run("window.panels.peek", json!({"side": "right"})).unwrap(), json!("right"));
wait(&mut h, peek::GRACE * 2.0);
assert!(h.tab("properties").is_some(), "with no pointer movement, the panels stay");
assert_eq!(inspect(&h.app, &h.ctx)["peek"], json!("right"));
h.frame(vec![Event::PointerMoved(wide.center())]);
wait(&mut h, peek::GRACE * 2.0);
assert!(h.tab("properties").is_none(), "the pointer moved away from them, so they hide");
// The pointer leaving the window counts as moving away.
h.app.run("window.panels.peek", json!({"side": "right"})).unwrap();
wait(&mut h, peek::GRACE * 2.0);
assert!(h.tab("properties").is_some());
h.frame(vec![Event::PointerGone]);
wait(&mut h, peek::GRACE * 2.0);
assert!(h.tab("properties").is_none(), "the pointer left the window, so they hide");
assert_eq!(inspect(&h.app, &h.ctx)["peek"], Value::Null);
}
#[test]
fn panels_held_by_a_flyout_hide_after_it_closes_with_the_pointer_still_away_from_them() {
let mut h = Harness::new();
let wide = hide_with_tab(&mut h);
// The pointer rests on the canvas, then outside the window, while a flyout holds the panels.
for away in [vec![Event::PointerMoved(wide.center())], vec![Event::PointerGone]] {
h.app.run("window.panels.peek", json!({"side": "right"})).unwrap();
h.app.ui.open_panel = Some("swatches".into());
h.frame(away);
wait(&mut h, peek::GRACE * 2.0);
assert!(h.tab("properties").is_some(), "an open flyout keeps them");
h.app.ui.open_panel = None;
wait(&mut h, peek::GRACE * 2.0);
assert!(h.tab("properties").is_none(), "the flyout closed with the pointer away from them, so they hide");
assert_eq!(inspect(&h.app, &h.ctx)["peek"], Value::Null);
}
}
#[test]
fn edge_panels_are_the_docked_panels_on_each_side_of_the_canvas() {
let layout = default_from(DockTab::ALL[0], &[], false, None, None);