Skip to content

Commit

Permalink
Implement update_focus pass (#538)
Browse files Browse the repository at this point in the history
Make Textbox Widget tab-focusable.

This is part of the Pass Specification RFC:
linebender/rfcs#7
  • Loading branch information
PoignardAzur authored Sep 12, 2024
1 parent 2fa8a05 commit 4746766
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 95 deletions.
16 changes: 4 additions & 12 deletions masonry/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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",
Expand All @@ -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,
}
}
}
8 changes: 6 additions & 2 deletions masonry/src/passes/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand 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)
Expand Down Expand Up @@ -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 {
Expand Down
116 changes: 115 additions & 1 deletion masonry/src/passes/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down Expand Up @@ -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);
}

Expand Down
45 changes: 5 additions & 40 deletions masonry/src/render_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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);

Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -556,8 +545,6 @@ impl RenderRoot {
self.root_lifecycle(event);
}

self.update_focus();

if self.root_state().request_anim {
self.state
.signal_queue
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions masonry/src/widget/textbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading

0 comments on commit 4746766

Please sign in to comment.