diff --git a/masonry/src/event.rs b/masonry/src/event.rs index e002ce535..063ff48a8 100644 --- a/masonry/src/event.rs +++ b/masonry/src/event.rs @@ -320,14 +320,6 @@ pub enum LifeCycle { pub enum InternalLifeCycle { /// Used to route the `WidgetAdded` event to the required widgets. RouteWidgetAdded, - - /// Used to route the `FocusChanged` event. - RouteFocusChanged { - /// the widget that is losing focus, if any - old: Option, - /// the widget that is gaining focus, if any - new: Option, - }, } /// Event indicating status changes within the widget hierarchy. @@ -354,6 +346,9 @@ pub enum StatusChange { /// /// [`EventCtx::is_focused`]: crate::EventCtx::is_focused FocusChanged(bool), + + /// Called when a widget becomes or no longer is parent of a focused widget. + ChildFocusChanged(bool), } impl PointerEvent { @@ -519,7 +514,6 @@ impl LifeCycle { match self { LifeCycle::Internal(internal) => match internal { InternalLifeCycle::RouteWidgetAdded => "RouteWidgetAdded", - InternalLifeCycle::RouteFocusChanged { .. } => "RouteFocusChanged", }, LifeCycle::WidgetAdded => "WidgetAdded", LifeCycle::AnimFrame(_) => "AnimFrame", @@ -541,9 +535,7 @@ impl InternalLifeCycle { /// [`Event::should_propagate_to_hidden`]: Event::should_propagate_to_hidden pub fn should_propagate_to_hidden(&self) -> bool { match self { - InternalLifeCycle::RouteWidgetAdded | InternalLifeCycle::RouteFocusChanged { .. } => { - true - } + InternalLifeCycle::RouteWidgetAdded => true, } } } diff --git a/masonry/src/passes/event.rs b/masonry/src/passes/event.rs index c360d80f2..ae0e2dd04 100644 --- a/masonry/src/passes/event.rs +++ b/masonry/src/passes/event.rs @@ -3,6 +3,7 @@ use dpi::LogicalPosition; use tracing::{debug, info_span, trace}; +use winit::event::ElementState; use winit::keyboard::{KeyCode, PhysicalKey}; use crate::passes::merge_state_up; @@ -72,7 +73,7 @@ fn run_event_pass( target_widget_id = parent_id; } - // Pass root widget state to synthetic state create at beginning of pass + // Merge root widget state with synthetic state created at beginning of pass root_state.merge_up(root.widget_arena.get_state_mut(root.root.id()).item); Handled::from(is_handled) @@ -153,7 +154,10 @@ pub(crate) fn root_on_text_event( // Handle Tab focus if let TextEvent::KeyboardKey(key, mods) = event { - if handled == Handled::No && key.physical_key == PhysicalKey::Code(KeyCode::Tab) { + if handled == Handled::No + && key.physical_key == PhysicalKey::Code(KeyCode::Tab) + && key.state == ElementState::Pressed + { if !mods.shift_key() { root.state.next_focused_widget = root.widget_from_focus_chain(true); } else { diff --git a/masonry/src/passes/update.rs b/masonry/src/passes/update.rs index 2f715e8eb..d4cd27f4c 100644 --- a/masonry/src/passes/update.rs +++ b/masonry/src/passes/update.rs @@ -48,6 +48,31 @@ fn run_targeted_update_pass( } } +// TODO - Replace LifecycleCtx with UpdateCtx +fn run_single_update_pass( + root: &mut RenderRoot, + target: Option, + mut pass_fn: impl FnMut(&mut dyn Widget, &mut LifeCycleCtx), +) { + if let Some(widget_id) = target { + let (widget_mut, state_mut) = root.widget_arena.get_pair_mut(widget_id); + + let mut ctx = LifeCycleCtx { + global_state: &mut root.state, + widget_state: state_mut.item, + widget_state_children: state_mut.children, + widget_children: widget_mut.children, + }; + pass_fn(widget_mut.item, &mut ctx); + } + + let mut current_id = target; + while let Some(widget_id) = current_id { + merge_state_up(&mut root.widget_arena, widget_id); + current_id = root.widget_arena.parent_of(widget_id); + } +} + pub(crate) fn run_update_pointer_pass(root: &mut RenderRoot, root_state: &mut WidgetState) { let pointer_pos = root.last_mouse_pos.map(|pos| (pos.x, pos.y).into()); @@ -143,7 +168,96 @@ pub(crate) fn run_update_pointer_pass(root: &mut RenderRoot, root_state: &mut Wi root.state.cursor_icon = new_cursor; root.state.hovered_path = next_hovered_path; - // Pass root widget state to synthetic state create at beginning of pass + // Merge root widget state with synthetic state created at beginning of pass + root_state.merge_up(root.widget_arena.get_state_mut(root.root.id()).item); +} + +// ---------------- + +pub(crate) fn run_update_focus_pass(root: &mut RenderRoot, root_state: &mut WidgetState) { + // If the focused widget ends up disabled or removed, we set + // the focused id to None + if let Some(id) = root.state.next_focused_widget { + if !root.widget_arena.has(id) || root.widget_arena.get_state_mut(id).item.is_disabled { + root.state.next_focused_widget = None; + } + } + + let prev_focused = root.state.focused_widget; + let next_focused = root.state.next_focused_widget; + + // "Focused path" means the focused widget, and all its parents. + let prev_focused_path = std::mem::take(&mut root.state.focused_path); + let next_focused_path = get_id_path(root, next_focused); + + let mut focused_set = HashSet::new(); + for widget_id in &next_focused_path { + focused_set.insert(*widget_id); + } + + trace!("prev_focused_path: {:?}", prev_focused_path); + trace!("next_focused_path: {:?}", next_focused_path); + + // This is the same algorithm as the one in + // run_update_pointer_pass + // See comment in that function. + + fn update_focused_status_of( + root: &mut RenderRoot, + widget_id: WidgetId, + focused_set: &HashSet, + ) { + run_targeted_update_pass(root, Some(widget_id), |widget, ctx| { + let has_focus = focused_set.contains(&ctx.widget_id()); + + if ctx.widget_state.has_focus != has_focus { + widget.on_status_change(ctx, &StatusChange::ChildFocusChanged(has_focus)); + } + ctx.widget_state.has_focus = has_focus; + }); + } + + // TODO - Make sure widgets are iterated from the bottom up. + // TODO - Document the iteration order for update_focus pass. + for widget_id in prev_focused_path.iter().copied() { + if root.widget_arena.has(widget_id) + && root.widget_arena.get_state_mut(widget_id).item.has_focus + != focused_set.contains(&widget_id) + { + update_focused_status_of(root, widget_id, &focused_set); + } + } + for widget_id in next_focused_path.iter().copied() { + if root.widget_arena.has(widget_id) + && root.widget_arena.get_state_mut(widget_id).item.has_focus + != focused_set.contains(&widget_id) + { + update_focused_status_of(root, widget_id, &focused_set); + } + } + + if prev_focused != next_focused { + run_single_update_pass(root, prev_focused, |widget, ctx| { + widget.on_status_change(ctx, &StatusChange::FocusChanged(false)); + }); + run_single_update_pass(root, next_focused, |widget, ctx| { + widget.on_status_change(ctx, &StatusChange::FocusChanged(true)); + }); + + // TODO: discriminate between text focus, and non-text focus. + root.state + .signal_queue + .push_back(if next_focused.is_some() { + RenderRootSignal::StartIme + } else { + RenderRootSignal::EndIme + }); + } + + root.state.focused_widget = root.state.next_focused_widget; + root.state.focused_path = next_focused_path; + + // Merge root widget state with synthetic state created at beginning of pass root_state.merge_up(root.widget_arena.get_state_mut(root.root.id()).item); } diff --git a/masonry/src/render_root.rs b/masonry/src/render_root.rs index 390092afd..8210db87f 100644 --- a/masonry/src/render_root.rs +++ b/masonry/src/render_root.rs @@ -26,7 +26,8 @@ use crate::passes::layout::root_layout; use crate::passes::mutate::{mutate_widget, run_mutate_pass}; use crate::passes::paint::root_paint; use crate::passes::update::{ - run_update_anim_pass, run_update_disabled_pass, run_update_pointer_pass, run_update_scroll_pass, + run_update_anim_pass, run_update_disabled_pass, run_update_focus_pass, run_update_pointer_pass, + run_update_scroll_pass, }; use crate::text::TextBrush; use crate::tree_arena::TreeArena; @@ -63,6 +64,7 @@ pub(crate) struct RenderRootState { pub(crate) debug_logger: DebugLogger, pub(crate) signal_queue: VecDeque, pub(crate) focused_widget: Option, + pub(crate) focused_path: Vec, pub(crate) next_focused_widget: Option, pub(crate) scroll_request_targets: Vec<(WidgetId, Rect)>, pub(crate) hovered_path: Vec, @@ -136,6 +138,7 @@ impl RenderRoot { debug_logger: DebugLogger::new(false), signal_queue: VecDeque::new(), focused_widget: None, + focused_path: Vec::new(), next_focused_widget: None, scroll_request_targets: Vec::new(), hovered_path: Vec::new(), @@ -342,9 +345,6 @@ impl RenderRoot { &mut self, f: impl FnOnce(WidgetMut<'_, Box>) -> R, ) -> R { - // TODO - Factor out into a "pre-event" function? - self.state.next_focused_widget = self.state.focused_widget; - let res = mutate_widget(self, self.root.id(), |mut widget_mut| { // Our WidgetArena stores all widgets as Box, but the "true" // type of our root widget is *also* Box. We downcast so we @@ -375,9 +375,6 @@ impl RenderRoot { id: WidgetId, f: impl FnOnce(WidgetMut<'_, Box>) -> R, ) -> R { - // TODO - Factor out into a "pre-event" function? - self.state.next_focused_widget = self.state.focused_widget; - let res = mutate_widget(self, id, f); let mut root_state = self.widget_arena.get_state_mut(self.root.id()).item.clone(); @@ -390,9 +387,6 @@ impl RenderRoot { fn root_on_pointer_event(&mut self, event: PointerEvent) -> Handled { let mut dummy_state = WidgetState::synthetic(self.root.id(), self.get_kurbo_size()); - // TODO - Factor out into a "pre-event" function? - self.state.next_focused_widget = self.state.focused_widget; - let handled = root_on_pointer_event(self, &mut dummy_state, &event); run_update_pointer_pass(self, &mut dummy_state); @@ -406,10 +400,8 @@ impl RenderRoot { fn root_on_text_event(&mut self, event: TextEvent) -> Handled { let mut dummy_state = WidgetState::synthetic(self.root.id(), self.get_kurbo_size()); - // TODO - Factor out into a "pre-event" function? - self.state.next_focused_widget = self.state.focused_widget; - let handled = root_on_text_event(self, &mut dummy_state, &event); + run_update_focus_pass(self, &mut dummy_state); self.post_event_processing(&mut dummy_state); self.get_root_widget().debug_validate(false); @@ -431,9 +423,6 @@ impl RenderRoot { data: event.data, }; - // TODO - Factor out into a "pre-event" function? - self.state.next_focused_widget = self.state.focused_widget; - root_on_access_event(self, &mut dummy_state, &event); self.post_event_processing(&mut dummy_state); @@ -556,8 +545,6 @@ impl RenderRoot { self.root_lifecycle(event); } - self.update_focus(); - if self.root_state().request_anim { self.state .signal_queue @@ -583,28 +570,6 @@ impl RenderRoot { } } - fn update_focus(&mut self) { - let old = self.state.focused_widget; - let new = self.state.next_focused_widget; - - // TODO - // Skip change if requested widget is disabled - - // Only send RouteFocusChanged in case there's actual change - if old != new { - let event = LifeCycle::Internal(InternalLifeCycle::RouteFocusChanged { old, new }); - self.state.focused_widget = new; - self.root_lifecycle(event); - - // TODO: discriminate between text focus, and non-text focus. - self.state.signal_queue.push_back(if new.is_some() { - RenderRootSignal::StartIme - } else { - RenderRootSignal::EndIme - }); - } - } - pub(crate) fn widget_from_focus_chain(&mut self, forward: bool) -> Option { self.state.focused_widget.and_then(|focus| { self.focus_chain() diff --git a/masonry/src/widget/textbox.rs b/masonry/src/widget/textbox.rs index ac96b4acd..b0681d6a5 100644 --- a/masonry/src/widget/textbox.rs +++ b/masonry/src/widget/textbox.rs @@ -255,6 +255,7 @@ impl Widget for Textbox { ctx.request_layout(); } LifeCycle::BuildFocusChain => { + ctx.register_for_focus(); // TODO: This will always be empty if !self.editor.text().links().is_empty() { tracing::warn!("Links present in text, but not yet integrated"); diff --git a/masonry/src/widget/widget_pod.rs b/masonry/src/widget/widget_pod.rs index 77fa8dea2..24ad7dfa7 100644 --- a/masonry/src/widget/widget_pod.rs +++ b/masonry/src/widget/widget_pod.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use smallvec::SmallVec; -use tracing::{info_span, trace}; +use tracing::trace; use crate::tree_arena::ArenaRefChildren; use crate::widget::WidgetState; -use crate::{InternalLifeCycle, LifeCycle, LifeCycleCtx, StatusChange, Widget, WidgetId}; +use crate::{InternalLifeCycle, LifeCycle, LifeCycleCtx, Widget, WidgetId}; // TODO - rewrite links in doc @@ -295,35 +295,11 @@ impl WidgetPod { let widget = widget_mut.item; let state = state_mut.item; - // when routing a status change event, if we are at our target - // we may send an extra event after the actual event - let mut extra_event = None; - let had_focus = state.has_focus; let call_widget = match event { LifeCycle::Internal(internal) => match internal { InternalLifeCycle::RouteWidgetAdded => state.children_changed, - InternalLifeCycle::RouteFocusChanged { old, new } => { - let this_changed = if *old == Some(self.id()) { - Some(false) - } else if *new == Some(self.id()) { - Some(true) - } else { - None - }; - - if let Some(change) = this_changed { - state.has_focus = change; - extra_event = Some(StatusChange::FocusChanged(change)); - } else { - state.has_focus = false; - } - - // TODO - This returns a lot of false positives. - // We'll remove this code soon anyway. - matches!((old, new), (Some(_), _) | (_, Some(_))) - } }, LifeCycle::WidgetAdded => { trace!( @@ -364,19 +340,6 @@ impl WidgetPod { widget.lifecycle(&mut inner_ctx, event); } - if let Some(event) = extra_event.as_ref() { - let mut inner_ctx = LifeCycleCtx { - global_state: parent_ctx.global_state, - widget_state: state, - widget_state_children: state_mut.children.reborrow_mut(), - widget_children: widget_mut.children.reborrow_mut(), - }; - - // We add a span so that inner logs are marked as being in an on_status_change pass - let _span = info_span!("on_status_change").entered(); - widget.on_status_change(&mut inner_ctx, event); - } - // Sync our state with our parent's state after the event! match event { @@ -415,6 +378,6 @@ impl WidgetPod { .expect("WidgetPod: inner widget not found in widget tree"); parent_ctx.widget_state.merge_up(state_mut.item); - call_widget || extra_event.is_some() + call_widget } }