Implement stashed state (#601)

Add update_stashed pass
Add is_still_interactive method
This commit is contained in:
Olivier FAURE 2024-09-23 10:49:28 +02:00 committed by GitHub
parent 2642a9e146
commit 07dab9b73c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 101 additions and 25 deletions

View File

@ -331,8 +331,7 @@ impl_context_method!(
/// Check is widget is stashed.
///
/// **Note:** Stashed widgets are a WIP feature
// FIXME - take stashed parents into account
/// **Note:** Stashed widgets are a WIP feature.
pub fn is_stashed(&self) -> bool {
self.widget_state.is_stashed
}
@ -579,14 +578,10 @@ impl_context_method!(
///
/// This will *not* trigger a layout pass.
///
/// **Note:** Stashed widgets are a WIP feature
/// **Note:** Stashed widgets are a WIP feature.
pub fn set_stashed(&mut self, child: &mut WidgetPod<impl Widget>, stashed: bool) {
if self.get_child_state_mut(child).is_stashed != stashed {
self.widget_state.children_changed = true;
self.widget_state.update_focus_chain = true;
}
self.get_child_state_mut(child).is_stashed = stashed;
self.get_child_state_mut(child).needs_update_stashed = true;
self.get_child_state_mut(child).is_explicitly_stashed = stashed;
}
}
);

View File

@ -77,6 +77,13 @@ pub(crate) fn run_update_pointer_pass(root: &mut RenderRoot, root_state: &mut Wi
let pointer_pos = root.last_mouse_pos.map(|pos| (pos.x, pos.y).into());
// -- UPDATE HOVERED WIDGETS --
// Release pointer capture if target can no longer hold it.
if let Some(id) = root.state.pointer_capture_target {
if !root.is_still_interactive(id) {
// TODO - Send PointerLeave event
root.state.pointer_capture_target = None;
}
}
let mut next_hovered_widget = if let Some(pos) = pointer_pos {
// TODO - Apply scale?
@ -175,10 +182,10 @@ pub(crate) fn run_update_pointer_pass(root: &mut RenderRoot, root_state: &mut Wi
// ----------------
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
// If the focused widget is disabled, stashed 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 {
if !root.is_still_interactive(id) {
root.state.next_focused_widget = None;
}
}
@ -288,16 +295,11 @@ fn update_disabled_for_widget(
.item
.lifecycle(&mut ctx, &LifeCycle::DisabledChanged(disabled));
state.item.is_disabled = disabled;
state.item.update_focus_chain = true;
}
state.item.needs_update_disabled = false;
if disabled && global_state.next_focused_widget == Some(id) {
// This may get overwritten. That's ok, because either way the
// focused widget, if there's one, won't be disabled.
global_state.next_focused_widget = None;
}
let parent_state = state.item;
recurse_on_children(
id,
@ -319,6 +321,61 @@ pub(crate) fn run_update_disabled_pass(root: &mut RenderRoot) {
// ----------------
// TODO - Document the stashed pass.
// *Stashed* is for widgets that are no longer "part of the graph". So they can't get keyboard events, don't get painted, etc, but should keep some state.
// The stereotypical use case would be the contents of hidden tabs in a "tab group" widget.
// Scrolled-out widgets are *not* stashed.
#[allow(clippy::only_used_in_recursion)]
fn update_stashed_for_widget(
global_state: &mut RenderRootState,
mut widget: ArenaMut<'_, Box<dyn Widget>>,
state: ArenaMut<'_, WidgetState>,
parent_stashed: bool,
) {
let _span = widget.item.make_trace_span().entered();
let id = state.item.id;
let stashed = state.item.is_explicitly_stashed || parent_stashed;
if !state.item.needs_update_stashed && stashed == state.item.is_stashed {
return;
}
if stashed != state.item.is_stashed {
// TODO - Send update event
state.item.is_stashed = stashed;
state.item.update_focus_chain = true;
// Note: We don't need request_repaint because stashing doesn't actually change
// how widgets are painted, only how the Scenes they create are composed.
state.item.needs_paint = true;
state.item.needs_accessibility = true;
// TODO - Remove once accessibility can be composed, same as above.
state.item.request_accessibility = true;
}
state.item.needs_update_stashed = false;
let parent_state = state.item;
recurse_on_children(
id,
widget.reborrow_mut(),
state.children,
|widget, mut state| {
update_stashed_for_widget(global_state, widget, state.reborrow_mut(), stashed);
parent_state.merge_up(state.item);
},
);
}
pub(crate) fn run_update_stashed_pass(root: &mut RenderRoot) {
let _span = info_span!("update_stashed").entered();
let (root_widget, root_state) = root.widget_arena.get_pair_mut(root.root.id());
update_stashed_for_widget(&mut root.state, root_widget, root_state, false);
}
// ----------------
// This pass will update scroll positions in cases where a widget has requested to be
// scrolled into view (usually a textbox getting text events).
// Each parent that implements scrolling will update its scroll position to ensure the

View File

@ -27,7 +27,7 @@ use crate::passes::paint::root_paint;
use crate::passes::update::{
run_update_anim_pass, run_update_disabled_pass, run_update_focus_chain_pass,
run_update_focus_pass, run_update_new_widgets_pass, run_update_pointer_pass,
run_update_scroll_pass,
run_update_scroll_pass, run_update_stashed_pass,
};
use crate::text::TextBrush;
use crate::tree_arena::TreeArena;
@ -505,11 +505,14 @@ impl RenderRoot {
root_compose(self, widget_state);
}
// Update the disabled state if necessary
// Update the disabled and stashed state if necessary
// Always do this before updating the focus-chain
if self.root_state().needs_update_disabled {
run_update_disabled_pass(self);
}
if self.root_state().needs_update_stashed {
run_update_stashed_pass(self);
}
// Update the focus-chain if necessary
// Always do this before sending focus change, since this event updates the focus chain.
@ -546,6 +549,17 @@ impl RenderRoot {
}
}
// Checks whether the given id points to a widget that is "interactive".
// i.e. not disabled or stashed.
// Only interactive widgets can have text focus or pointer capture.
pub(crate) fn is_still_interactive(&self, id: WidgetId) -> bool {
let Some(state) = self.widget_arena.widget_states.find(id.to_raw()) else {
return false;
};
!state.item.is_stashed && !state.item.is_disabled
}
pub(crate) fn widget_from_focus_chain(&mut self, forward: bool) -> Option<WidgetId> {
let focused_widget = self.state.focused_widget;
let focused_idx = focused_widget.and_then(|focused_widget| {

View File

@ -194,7 +194,9 @@ impl<'w> WidgetRef<'w, dyn Widget> {
}
// TODO - Use Widget::get_child_at_pos method
if let Some(child) = innermost_widget.children().into_iter().rev().find(|child| {
!child.widget.skip_pointer() && child.state().window_layout_rect().contains(pos)
!child.state().is_stashed
&& !child.widget.skip_pointer()
&& child.state().window_layout_rect().contains(pos)
}) {
innermost_widget = child;
} else {

View File

@ -107,8 +107,10 @@ pub struct WidgetState {
/// An animation must run on this widget or a descendant
pub(crate) needs_anim: bool,
/// This widget or a descendant changed its `explicitly_disabled` value
/// This widget or a descendant changed its `is_explicitly_disabled` value
pub(crate) needs_update_disabled: bool,
/// This widget or a descendant changed its `is_explicitly_stashed` value
pub(crate) needs_update_stashed: bool,
pub(crate) update_focus_chain: bool,
@ -127,6 +129,12 @@ pub struct WidgetState {
/// This widget or an ancestor has been disabled.
pub(crate) is_disabled: bool,
// TODO - Document concept of "stashing".
/// This widget has been stashed.
pub(crate) is_explicitly_stashed: bool,
/// This widget or an ancestor has been stashed.
pub(crate) is_stashed: bool,
pub(crate) is_hot: bool,
/// In the focused path, starting from window and ending at the focused widget.
@ -136,9 +144,6 @@ pub struct WidgetState {
/// Whether this specific widget is in the focus chain.
pub(crate) in_focus_chain: bool,
// TODO - document
pub(crate) is_stashed: bool,
// --- DEBUG INFO ---
// Used in event/lifecycle/etc methods that are expected to be called recursively
// on a widget's children, to make sure each child was visited.
@ -169,7 +174,9 @@ impl WidgetState {
translation: Vec2::ZERO,
translation_changed: false,
is_explicitly_disabled: false,
is_explicitly_stashed: false,
is_disabled: false,
is_stashed: false,
baseline_offset: 0.0,
is_new: true,
is_hot: false,
@ -186,12 +193,12 @@ impl WidgetState {
request_anim: true,
needs_anim: true,
needs_update_disabled: true,
needs_update_stashed: true,
focus_chain: Vec::new(),
children_changed: true,
cursor: None,
text_registrations: Vec::new(),
update_focus_chain: true,
is_stashed: false,
#[cfg(debug_assertions)]
needs_visit: VisitBool(false.into()),
#[cfg(debug_assertions)]
@ -216,6 +223,7 @@ impl WidgetState {
request_anim: false,
needs_anim: false,
needs_update_disabled: false,
needs_update_stashed: false,
children_changed: false,
update_focus_chain: false,
..WidgetState::new(id, "<root>")