Implement update_focus pass (#538)

Make Textbox Widget tab-focusable.

This is part of the Pass Specification RFC:
https://github.com/linebender/rfcs/pull/7
This commit is contained in:
Olivier FAURE 2024-09-12 11:29:21 +00:00 committed by GitHub
parent 2fa8a055bd
commit 4746766d89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 134 additions and 95 deletions

View File

@ -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<WidgetId>,
/// the widget that is gaining focus, if any
new: Option<WidgetId>,
},
}
/// 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,
}
}
}

View File

@ -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<E>(
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 {

View File

@ -48,6 +48,31 @@ fn run_targeted_update_pass(
}
}
// TODO - Replace LifecycleCtx with UpdateCtx
fn run_single_update_pass(
root: &mut RenderRoot,
target: Option<WidgetId>,
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<WidgetId>,
) {
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);
}

View File

@ -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<RenderRootSignal>,
pub(crate) focused_widget: Option<WidgetId>,
pub(crate) focused_path: Vec<WidgetId>,
pub(crate) next_focused_widget: Option<WidgetId>,
pub(crate) scroll_request_targets: Vec<(WidgetId, Rect)>,
pub(crate) hovered_path: Vec<WidgetId>,
@ -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<dyn Widget>>) -> 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<dyn Widget>, but the "true"
// type of our root widget is *also* Box<dyn Widget>. We downcast so we
@ -375,9 +375,6 @@ impl RenderRoot {
id: WidgetId,
f: impl FnOnce(WidgetMut<'_, Box<dyn Widget>>) -> 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<WidgetId> {
self.state.focused_widget.and_then(|focus| {
self.focus_chain()

View File

@ -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");

View File

@ -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<W: Widget> WidgetPod<W> {
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<W: Widget> WidgetPod<W> {
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<W: Widget> WidgetPod<W> {
.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
}
}