Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement update_focus pass #538

Merged
merged 4 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future PR, but not a big fan of this boolean arg here, would be nicer to have Focus::Next Focus::Prev or something instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that part of the code is due for a refactor.

} 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