mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 23:59:16 +00:00
craft-ui: end drags and resizes on a focus loss, not while a window reports no focus
egui-winit starts RawInput::focused false and only a winit Focused event sets it. A window that egui never heard was focused cancelled every tab, group and window drag and every resize on its first frame. Gestures now end on a focused-to-unfocused transition (or Escape); a window that never reported focus still drags while the pointer is pressed in it.
This commit is contained in:
1 parent
09e14347d3
commit
ba00e9fd9e
10 files changed
+134
-12
No files matched your search
@@ -180,7 +180,7 @@ impl<'a> FloatingFrame<'a> {
|
||||
let mut started = None;
|
||||
let mut responses = Vec::new();
|
||||
let limits = PanelLimits { min: self.limits.min.max(vec2(40.0, RESIZE_BAND + TITLE_HEIGHT + 20.0)).min(self.limits.max), ..self.limits };
|
||||
let canceled = ui.input(|input| input.key_pressed(Key::Escape) || !input.focused);
|
||||
let canceled = ui.input(|input| input.key_pressed(Key::Escape)) || crate::focus::lost(ui.ctx());
|
||||
for (name, handle, direction) in handles(rect) {
|
||||
if !super::targets::usable(handle) {
|
||||
continue;
|
||||
@@ -409,7 +409,7 @@ pub fn live_move(ctx: &Context, bar: &Response, rect: Rect, bounds: Rect) -> Opt
|
||||
}
|
||||
let (start, origin) = ctx.data(|data| data.get_temp::<(Rect, Pos2)>(key))?;
|
||||
let forget = || ctx.data_mut(|data| data.remove::<(Rect, Pos2)>(key));
|
||||
if ctx.input(|input| input.key_pressed(Key::Escape) || !input.focused) {
|
||||
if ctx.input(|input| input.key_pressed(Key::Escape)) || crate::focus::lost(ctx) {
|
||||
forget();
|
||||
return Some(MoveStep::Canceled(start));
|
||||
}
|
||||
|
||||
@@ -84,7 +84,23 @@ impl Driver {
|
||||
}
|
||||
|
||||
fn with(layout: Layout<u32>, content: TestContent, right_to_left: bool) -> Self {
|
||||
let mut driver = Self { ctx: Context::default(), layout, content, focused: true, right_to_left, shapes: Vec::new(), time: 0.0 };
|
||||
Self::start(Self { ctx: Context::default(), layout, content, focused: true, right_to_left, shapes: Vec::new(), time: 0.0 })
|
||||
}
|
||||
|
||||
/// A window egui never heard has focus (macOS can make it key without a `Focused` event).
|
||||
fn never_focused(layout: Layout<u32>) -> Self {
|
||||
Self::start(Self {
|
||||
ctx: Context::default(),
|
||||
layout,
|
||||
content: TestContent::default(),
|
||||
focused: false,
|
||||
right_to_left: false,
|
||||
shapes: Vec::new(),
|
||||
time: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
fn start(mut driver: Self) -> Self {
|
||||
// Floating areas settle after their sizing pass.
|
||||
for _ in 0..3 {
|
||||
driver.step(vec![]);
|
||||
@@ -515,6 +531,31 @@ fn a_column_s_first_tab_dropped_on_its_own_column_edge_anchors_on_another_panel(
|
||||
assert_eq!(d.layout, layout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_that_never_reported_focus_drags_tabs_and_losing_focus_cancels_a_drag() {
|
||||
let mut layout = docked(Side::Start, &[&[&[2]], &[&[3, 4]]]);
|
||||
if let Some(Node::Split { size, .. }) = &mut layout.root {
|
||||
*size = SplitSize::FixedFirst(480.0);
|
||||
}
|
||||
let to = pos2(484.0, 300.0);
|
||||
let mut d = Driver::never_focused(layout.clone());
|
||||
let out = d.drag(d.tab(3).center(), to);
|
||||
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 4, placement: Placement::Column { after: true } }]);
|
||||
|
||||
// Focus that arrives during the drag and goes again ends it.
|
||||
let mut d = Driver::never_focused(layout.clone());
|
||||
d.hold(d.tab(3).center(), to);
|
||||
d.focused = true;
|
||||
d.step(vec![Event::WindowFocused(true)]);
|
||||
assert!(is_dragging::<u32>(&d.ctx, Id::new(AREA)));
|
||||
d.focused = false;
|
||||
d.step(vec![Event::WindowFocused(false)]);
|
||||
let out = d.step(vec![button(to, false)]);
|
||||
assert!(out.actions.is_empty(), "{out:?}");
|
||||
assert_eq!(d.layout, layout);
|
||||
assert!(!is_dragging::<u32>(&d.ctx, Id::new(AREA)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dock_wider_than_the_window_shrinks_to_fit_and_resizes() {
|
||||
let widths = render::fit_widths(&[(600.0, Some(40.0)), (600.0, Some(40.0)), (48.0, None)], 800.0);
|
||||
|
||||
@@ -940,6 +940,19 @@ fn escape_in_a_focused_native_window_cancels_a_main_window_drag() {
|
||||
assert!(!super::is_dragging::<u32>(&d.ctx, d.id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_main_window_without_focus_still_drags_a_tab_into_another_group() {
|
||||
let mut d = driver(layout(false));
|
||||
d.host().focus = None;
|
||||
d.settle();
|
||||
let tab = d.hit(&Hit::Tab(1)).unwrap().center();
|
||||
let over = d.hit(&Hit::Tab(3)).unwrap().center();
|
||||
d.hold(tab, over);
|
||||
assert!(super::is_dragging::<u32>(&d.ctx, d.id));
|
||||
let out = d.release(over);
|
||||
assert!(out.actions.iter().any(|a| matches!(a, Action::Move { panel: 1, .. })), "{:?}", out.actions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_moving_to_a_native_window_mid_drag_keeps_the_drag() {
|
||||
let mut d = driver(two_windows());
|
||||
|
||||
@@ -81,8 +81,8 @@ enum Dest {
|
||||
pub(in crate::docking) struct Cross<P> {
|
||||
/// Viewports are native: drags cross windows.
|
||||
pub on: bool,
|
||||
/// One of the application's windows has focus.
|
||||
pub focused: bool,
|
||||
/// The application's windows lost focus this pass: one had it on the last pass, none now.
|
||||
pub focus_lost: bool,
|
||||
/// The main window's content on the desktop, where known.
|
||||
pub main: Option<Rect>,
|
||||
/// Shown native windows' desktop rects, front first.
|
||||
@@ -109,7 +109,7 @@ impl<P> Default for Cross<P> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
on: false,
|
||||
focused: true,
|
||||
focus_lost: false,
|
||||
main: None,
|
||||
windows: Vec::new(),
|
||||
surfaces: Vec::new(),
|
||||
@@ -432,7 +432,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
pointer: at,
|
||||
down: pointer.down,
|
||||
released: pointer.released,
|
||||
focused: self.cross.focused,
|
||||
focused: !self.cross.focus_lost,
|
||||
gone: false,
|
||||
escape: pointer.escape,
|
||||
command: pointer.command,
|
||||
|
||||
@@ -153,7 +153,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
&& let Some(drag) = ui.ctx().data(|d| d.get_temp::<GroupDrag<P>>(self.id.with("group-drag")))
|
||||
&& drag.gesture == GroupGesture::Window
|
||||
&& let Some(pointer) = ui.ctx().pointer_interact_pos()
|
||||
&& !ui.input(|i| i.key_pressed(Key::Escape) || !i.focused)
|
||||
&& !ui.input(|i| i.key_pressed(Key::Escape))
|
||||
&& !crate::focus::lost(ui.ctx())
|
||||
{
|
||||
// From the press origin, so fractional pointer steps do not accumulate rounding.
|
||||
let moved = clamp_float(drag.source.translate(pointer - drag.origin), self.float_bounds);
|
||||
|
||||
@@ -251,7 +251,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let focused = i.focused || infos().any(|v| v.focused == Some(true)) || infos().all(|v| v.focused.is_none());
|
||||
(focused, i.time)
|
||||
});
|
||||
self.cross.focused = focused;
|
||||
self.cross.focus_lost = crate::focus::turned_false(ctx, self.id.with("app-focus"), focused);
|
||||
let key = self.id.with("native-visibility");
|
||||
let (last, was) = ctx.data(|d| d.get_temp::<(f64, bool)>(key)).unwrap_or((now, true));
|
||||
let last = if focused { now } else { last };
|
||||
@@ -525,7 +525,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
ui.ctx().send_viewport_cmd(ViewportCommand::OuterPosition(to - frame));
|
||||
r.cross.moved = Some(to);
|
||||
};
|
||||
if pointer.escape || !self.cross.focused {
|
||||
if pointer.escape || self.cross.focus_lost {
|
||||
if pointer.escape {
|
||||
move_to(self, drag.start);
|
||||
ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape));
|
||||
|
||||
@@ -27,6 +27,7 @@ pub(super) struct DragInput {
|
||||
pub pointer: Option<Pos2>,
|
||||
pub down: bool,
|
||||
pub released: bool,
|
||||
/// The window kept its focus: false on the pass it loses it (see [`crate::focus`]).
|
||||
pub focused: bool,
|
||||
/// The pointer left the window.
|
||||
pub gone: bool,
|
||||
@@ -41,11 +42,12 @@ impl DragInput {
|
||||
/// This window's own input.
|
||||
fn of(ctx: &Context) -> Self {
|
||||
let command = float_only(ctx);
|
||||
let focus_lost = crate::focus::lost(ctx);
|
||||
ctx.input(|i| Self {
|
||||
pointer: i.pointer.interact_pos().filter(|p| p.is_finite()),
|
||||
down: i.pointer.primary_down(),
|
||||
released: i.pointer.primary_released(),
|
||||
focused: i.focused,
|
||||
focused: !focus_lost,
|
||||
gone: !i.pointer.has_pointer(),
|
||||
escape: i.key_pressed(Key::Escape),
|
||||
command,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
//! Window focus as pointer gestures read it.
|
||||
//!
|
||||
//! A drag or resize ends when its window loses focus, not while the window merely reports no
|
||||
//! focus. egui-winit starts `RawInput::focused` false and only a winit `Focused` event sets it
|
||||
//! (on macOS to `has_focus()` at the time the event is read), so a window can take pointer
|
||||
//! input for a long time while egui reports it unfocused, at first launch until the user
|
||||
//! switches to another application and back. Gestures in such a window still run.
|
||||
|
||||
use egui::{Context, Id};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Seen {
|
||||
pass: u64,
|
||||
before: bool,
|
||||
now: bool,
|
||||
}
|
||||
|
||||
/// Whether `now` is false and was true on the previous pass, as recorded under `key`. With no
|
||||
/// record from the previous pass there is no transition, so a gesture asks on every pass it
|
||||
/// runs. Asking again in the same pass gives the same answer.
|
||||
pub(crate) fn turned_false(ctx: &Context, key: Id, now: bool) -> bool {
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
ctx.data_mut(|d| {
|
||||
let seen = match d.get_temp::<Seen>(key) {
|
||||
Some(seen) if seen.pass == pass => seen,
|
||||
Some(seen) if seen.pass + 1 == pass => Seen { pass, before: seen.now, now },
|
||||
_ => Seen { pass, before: now, now },
|
||||
};
|
||||
d.insert_temp(key, seen);
|
||||
seen.before && !seen.now
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the window being drawn lost focus this pass: focused on the previous pass, not now.
|
||||
pub(crate) fn lost(ctx: &Context) -> bool {
|
||||
let focused = ctx.input(|i| i.focused);
|
||||
turned_false(ctx, Id::new("craft-ui-window-focus").with(ctx.viewport_id()), focused)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use egui::RawInput;
|
||||
|
||||
fn pass(ctx: &Context, focused: bool) -> bool {
|
||||
let mut lost_now = false;
|
||||
let mut out = ctx.run_ui(RawInput { focused, ..Default::default() }, |ui| {
|
||||
lost_now = lost(ui.ctx());
|
||||
assert_eq!(lost(ui.ctx()), lost_now, "asking twice in a pass");
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
lost_now
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_focused_to_unfocused_pass_is_a_loss() {
|
||||
let ctx = Context::default();
|
||||
assert!(!pass(&ctx, false), "a window that never had focus has not lost it");
|
||||
assert!(!pass(&ctx, false));
|
||||
assert!(!pass(&ctx, true));
|
||||
assert!(pass(&ctx, false));
|
||||
assert!(!pass(&ctx, false), "the loss is one pass");
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
pub mod buttons;
|
||||
pub mod docking;
|
||||
mod focus;
|
||||
pub mod icons;
|
||||
pub mod layout;
|
||||
pub mod panes;
|
||||
|
||||
@@ -559,7 +559,7 @@ impl PaneHost {
|
||||
memory.drag_out = Some(DragOut { pane: s.id, canceled: false });
|
||||
}
|
||||
let Some(mut drag) = memory.drag_out.filter(|d| d.pane == s.id) else { return };
|
||||
if ctx.input(|input| input.key_pressed(Key::Escape) || !input.focused) {
|
||||
if ctx.input(|input| input.key_pressed(Key::Escape)) || crate::focus::lost(ctx) {
|
||||
drag.canceled = true;
|
||||
}
|
||||
let near = rect.expand(DOCK_SNAP);
|
||||
|
||||
Reference in new issue
Block a user