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

Make pointer events affect focus #822

Merged
merged 4 commits into from
Jan 14, 2025
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
12 changes: 9 additions & 3 deletions masonry/src/doc/06_masonry_concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,13 @@ Focus marks whether a widget receives text events.

To give a simple example, when you click a textbox, the textbox gets focus: anything you type on your keyboard will be sent to that textbox.

Focus can be changed with the tab key, or by clicking on a widget, both which Masonry automatically handles.
Masonry will only give focus to widgets that accept focus (see [`Widget::accepts_focus`]).
Widgets can also use context types to request focus.
Focus will be changed:

- When users press the Tab key: Masonry will automatically pick the next widget in the tree that accepts focus [`Widget::accepts_focus`]. (If no widget is currently focused, its starting point will be the most recently clicked widget.)
- When users click outside the currently focused widget: Masonry will automatically remove focus.

Widgets that want to gain focus when clicked should call [`EventCtx::request_focus`] inside [`Widget::on_pointer_event`].
Other context types can also request focus.

If a widget gains or loses focus it will get a [`FocusChanged`] event.

Expand Down Expand Up @@ -108,3 +112,5 @@ They should not be relied upon to check code correctness, but are meant to help
[`PointerLeave`]: crate::PointerEvent::PointerLeave
[`FocusChanged`]: crate::Update::FocusChanged
[`Widget::accepts_focus`]: crate::Widget::accepts_focus
[`EventCtx::request_focus`]: crate::EventCtx::request_focus
[`Widget::on_pointer_event`]: crate::Widget::on_pointer_event
28 changes: 23 additions & 5 deletions masonry/src/passes/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,26 @@ pub(crate) fn run_on_pointer_event_pass(root: &mut RenderRoot, event: &PointerEv

let target_widget_id = get_pointer_target(root, event.position());

if matches!(event, PointerEvent::PointerDown(..)) {
if let Some(target_widget_id) = target_widget_id {
// The next tab event assign focus around this widget.
root.global_state.most_recently_clicked_widget = Some(target_widget_id);

// If we click outside of the focused widget, we clear the focus.
if let Some(focused_widget) = root.global_state.focused_widget {
// Focused_widget isn't ancestor of target_widget_id
if !root
.widget_arena
.states
.get_id_path(target_widget_id)
Copy link
Member

Choose a reason for hiding this comment

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

It's not clear to be in what context a possibly focusable widget could be nested inside another possibly focusable widget (that is, why this isn't just target_widget_id != focused_widget)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't disagree, but I don't want to make it a hard rule either.

.contains(&focused_widget.to_raw())
{
root.global_state.next_focused_widget = None;
}
}
}
}

let handled = run_event_pass(
root,
target_widget_id,
Expand Down Expand Up @@ -174,11 +194,9 @@ pub(crate) fn run_on_text_event_pass(root: &mut RenderRoot, event: &TextEvent) -
&& key.state == ElementState::Pressed
&& handled == Handled::No
{
if !mods.shift_key() {
root.global_state.next_focused_widget = root.widget_from_focus_chain(true);
} else {
root.global_state.next_focused_widget = root.widget_from_focus_chain(false);
}
let forward = !mods.shift_key();
let next_focused_widget = root.widget_from_focus_chain(forward);
root.global_state.next_focused_widget = next_focused_widget;
handled = Handled::Yes;
}
}
Expand Down
7 changes: 6 additions & 1 deletion masonry/src/render_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub(crate) struct RenderRootState {
pub(crate) focused_widget: Option<WidgetId>,
pub(crate) focused_path: Vec<WidgetId>,
pub(crate) next_focused_widget: Option<WidgetId>,
pub(crate) most_recently_clicked_widget: Option<WidgetId>,
Copy link
Member

Choose a reason for hiding this comment

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

It might be worth having just a sprinkling of documentation/explanation here (the same could be said about most of these fields, but this one is a bit non-obvious)

pub(crate) scroll_request_targets: Vec<(WidgetId, Rect)>,
pub(crate) hovered_path: Vec<WidgetId>,
pub(crate) pointer_capture_target: Option<WidgetId>,
Expand Down Expand Up @@ -198,6 +199,7 @@ impl RenderRoot {
focused_widget: None,
focused_path: Vec::new(),
next_focused_widget: None,
most_recently_clicked_widget: None,
scroll_request_targets: Vec::new(),
hovered_path: Vec::new(),
pointer_capture_target: None,
Expand Down Expand Up @@ -616,7 +618,10 @@ impl RenderRoot {
}

pub(crate) fn widget_from_focus_chain(&mut self, forward: bool) -> Option<WidgetId> {
let focused_widget = self.global_state.focused_widget;
let focused_widget = self
.global_state
.focused_widget
.or(self.global_state.most_recently_clicked_widget);
let focused_idx = focused_widget.and_then(|focused_widget| {
self.focus_chain()
.iter()
Expand Down
Loading