mirror of https://github.com/linebender/xilem
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:
parent
2fa8a055bd
commit
4746766d89
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue