From 57a689b8c804ae3a38ae615e330f56004e584341 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Tue, 7 Nov 2023 20:16:59 -0800 Subject: [PATCH] Window focus/occlusion, recursive focus/activation --- Cargo.lock | 18 +++++----- src/context.rs | 58 +++++++++++++++++++++++++------- src/value.rs | 11 +++++++ src/widget.rs | 5 +++ src/window.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 152 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11aabe0..b725e79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,9 +427,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" dependencies = [ "libc", "windows-sys 0.48.0", @@ -579,9 +579,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "js-sys", @@ -853,7 +853,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.1.0" -source = "git+https://github.com/khonsulabs/kludgine#2650642bddb0cdc37b788cda6c552ded11e73b48" +source = "git+https://github.com/khonsulabs/kludgine#fd2078efb7212db0f07120b80440699b00ec2a2b" dependencies = [ "ahash", "alot", @@ -1510,18 +1510,18 @@ checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" [[package]] name = "serde" -version = "1.0.191" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a834c4821019838224821468552240d4d95d14e751986442c816572d39a080c9" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.191" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fa52d5646bce91b680189fe5b1c049d2ea38dabb4e2e7c8d00ca12cfbfbcfd" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", diff --git a/src/context.rs b/src/context.rs index abeed0d..1b8e2c0 100644 --- a/src/context.rs +++ b/src/context.rs @@ -172,8 +172,21 @@ impl<'context, 'window> EventContext<'context, 'window> { } pub(crate) fn apply_pending_state(&mut self) { - let active = self.pending_state.active.clone(); - if self.current_node.tree.active_widget() != active.as_ref().map(ManagedWidget::id) { + const MAX_ITERS: u8 = 100; + // These two blocks apply active/focus in a loop to pick up the event + // where during the process of calling deactivate/blur or activate/focus + // the active/focus widget is changed again. This can lead to infinite + // loops, which is a programmer error. However, rather than block + // forever, we log a message that this is happening and break. + + let mut activation_changes = 0; + while activation_changes < MAX_ITERS { + let active = self.pending_state.active.clone(); + if self.current_node.tree.active_widget() == active.as_ref().map(ManagedWidget::id) { + break; + } + activation_changes += 1; + let new = match self.current_node.tree.activate(active.as_ref()) { Ok(old) => { if let Some(old) = old { @@ -185,19 +198,33 @@ impl<'context, 'window> EventContext<'context, 'window> { Err(_) => false, }; if new { - if let Some(active) = &active { + if let Some(active) = self.pending_state.active.clone() { active .lock() .as_widget() - .activate(&mut self.for_other(active)); + .activate(&mut self.for_other(&active)); } self.pending_state.active = active; + } else { + break; } } - let focus = self.pending_state.focus.clone(); - if self.current_node.tree.focused_widget() != focus.as_ref().map(ManagedWidget::id) { - let focus = focus.and_then(|mut focus| loop { + if activation_changes == MAX_ITERS { + eprintln!( + "activation change force stopped after {activation_changes} sequential changes" + ); + } + + let mut focus_changes = 0; + while focus_changes < MAX_ITERS { + let focus = self.pending_state.focus.clone(); + if self.current_node.tree.focused_widget() == focus.as_ref().map(ManagedWidget::id) { + break; + } + focus_changes += 1; + + self.pending_state.focus = focus.and_then(|mut focus| loop { if focus .lock() .as_widget() @@ -210,7 +237,11 @@ impl<'context, 'window> EventContext<'context, 'window> { break self.next_focus_after(focus, VisualOrder::left_to_right()); } }); - let new = match self.current_node.tree.focus(focus.as_ref()) { + let new = match self + .current_node + .tree + .focus(self.pending_state.focus.as_ref()) + { Ok(old) => { if let Some(old) = old { let mut old_context = self.for_other(&old); @@ -221,12 +252,17 @@ impl<'context, 'window> EventContext<'context, 'window> { Err(_) => false, }; if new { - if let Some(focus) = &focus { - focus.lock().as_widget().focus(&mut self.for_other(focus)); + if let Some(focus) = self.pending_state.focus.clone() { + focus.lock().as_widget().focus(&mut self.for_other(&focus)); } - self.pending_state.focus = focus; + } else { + break; } } + + if focus_changes == MAX_ITERS { + eprintln!("focus change force stopped after {focus_changes} sequential changes"); + } } fn next_focus_after( diff --git a/src/value.rs b/src/value.rs index 1f6cabc..0cd0535 100644 --- a/src/value.rs +++ b/src/value.rs @@ -54,6 +54,17 @@ impl Dynamic { self.0.for_each(move |gen| for_each(&gen.value)); } + /// Attaches `for_each` to this value so that it is invoked each time the + /// value's contents are updated. This function returns `self`. + #[must_use] + pub fn with_for_each(self, mut for_each: F) -> Self + where + F: for<'a> FnMut(&'a T) + Send + 'static, + { + self.0.for_each(move |gen| for_each(&gen.value)); + self + } + /// Creates a new dynamic value that contains the result of invoking `map` /// each time this value is changed. pub fn map_each(&self, mut map: F) -> Dynamic diff --git a/src/widget.rs b/src/widget.rs index 3744fc0..31f5b1b 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -172,6 +172,11 @@ pub trait MakeWidget: Sized { /// Returns a new widget. fn make_widget(self) -> WidgetInstance; + /// Returns a new window containing `self` as the root widget. + fn into_window(self) -> Window { + Window::new(self.make_widget()) + } + /// Associates `styles` with this widget. /// /// This is equivalent to `Style::new(styles, self)`. diff --git a/src/window.rs b/src/window.rs index 0311f10..fc3af4c 100644 --- a/src/window.rs +++ b/src/window.rs @@ -28,6 +28,7 @@ use crate::graphics::Graphics; use crate::styles::components::VisualOrder; use crate::tree::Tree; use crate::utils::ModifiersExt; +use crate::value::Dynamic; use crate::widget::{EventHandling, ManagedWidget, Widget, WidgetInstance, HANDLED, IGNORED}; use crate::window::sealed::WindowCommand; use crate::{ConstraintLimit, Run}; @@ -46,7 +47,9 @@ where { context: Behavior::Context, /// The attributes of this window. - pub attributes: WindowAttributes, + attributes: WindowAttributes, + occluded: Option>, + focused: Option>, } impl Default for Window @@ -68,6 +71,34 @@ impl Window { { Self::new(WidgetInstance::new(widget)) } + + /// Sets `focused` to be the dynamic updated when this window's focus status + /// is changed. + /// + /// When the window is focused for user input, the dynamic will contain + /// `true`. + /// + /// `focused` will be initialized with an initial state + /// of `false`. + pub fn with_focused(mut self, focused: Dynamic) -> Self { + focused.update(false); + self.focused = Some(focused); + self + } + + /// Sets `occluded` to be the dynamic updated when this window's occlusion + /// status is changed. + /// + /// When the window is occluded (completely hidden/offscreen/minimized), the + /// dynamic will contain `true`. If the window is at least partially + /// visible, this value will contain `true`. + /// + /// `occluded` will be initialized with an initial state of `false`. + pub fn with_occluded(mut self, occluded: Dynamic) -> Self { + occluded.update(false); + self.occluded = Some(occluded); + self + } } impl Window @@ -98,6 +129,8 @@ where ..WindowAttributes::default() }, context, + occluded: None, + focused: None, } } } @@ -107,12 +140,14 @@ where Behavior: WindowBehavior, { fn run(self) -> crate::Result { - GooeyWindow::::run_with(AssertUnwindSafe(( - self.context, - RefCell::new(sealed::WindowSettings { + GooeyWindow::::run_with(AssertUnwindSafe(sealed::Context { + user: self.context, + settings: RefCell::new(sealed::WindowSettings { attributes: Some(self.attributes), + occluded: self.occluded, + focused: self.focused, }), - ))) + })) } } @@ -157,6 +192,8 @@ struct GooeyWindow { mouse_state: MouseState, redraw_status: RedrawStatus, initial_frame: bool, + occluded: Option>, + focused: Option>, } impl GooeyWindow @@ -174,15 +211,17 @@ impl kludgine::app::WindowBehavior for GooeyWindow where T: WindowBehavior, { - type Context = AssertUnwindSafe<(T::Context, RefCell)>; + type Context = AssertUnwindSafe>; fn initialize( mut window: RunningWindow<'_>, _graphics: &mut kludgine::Graphics<'_>, - context: Self::Context, + AssertUnwindSafe(context): Self::Context, ) -> Self { - let mut behavior = T::initialize(&mut window, context.0 .0); + let mut behavior = T::initialize(&mut window, context.user); let root = Tree::default().push_boxed(behavior.make_root(), None); + let occluded = context.settings.borrow_mut().occluded.take(); + let focused = context.settings.borrow_mut().focused.take(); Self { behavior, @@ -196,6 +235,8 @@ where }, redraw_status: RedrawStatus::default(), initial_frame: true, + occluded, + focused, } } @@ -232,6 +273,26 @@ where } } + fn focus_changed( + &mut self, + window: kludgine::app::Window<'_, WindowCommand>, + _kludgine: &mut Kludgine, + ) { + if let Some(focused) = &self.focused { + focused.update(window.focused()); + } + } + + fn occlusion_changed( + &mut self, + window: kludgine::app::Window<'_, WindowCommand>, + _kludgine: &mut Kludgine, + ) { + if let Some(occluded) = &self.occluded { + occluded.update(window.ocluded()); + } + } + fn render<'pass>( &'pass mut self, _window: RunningWindow<'_>, @@ -246,7 +307,7 @@ where context: &Self::Context, ) -> kludgine::app::WindowAttributes { context - .1 + .settings .borrow_mut() .attributes .take() @@ -558,10 +619,20 @@ struct MouseState { } pub(crate) mod sealed { + use std::cell::RefCell; + + use crate::value::Dynamic; use crate::window::WindowAttributes; + pub struct Context { + pub user: C, + pub settings: RefCell, + } + pub struct WindowSettings { pub attributes: Option, + pub occluded: Option>, + pub focused: Option>, } pub enum WindowCommand {