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 3 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
1 change: 1 addition & 0 deletions masonry/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ impl EventCtx<'_> {
// to deliver on the "last focus request wins" promise.
let id = self.widget_id();
self.global_state.next_focused_widget = Some(id);
self.global_state.ghost_focus = Some(id);
}

/// Transfer [text focus] to the widget with the given `WidgetId`.
Expand Down
18 changes: 15 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`].
- 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 All @@ -71,6 +75,12 @@ Active focus is the default one; inactive focus is when the window your app runs

In that case, we still mark the widget as focused, but with a different color to signal that e.g. typing on the keyboard won't actually affect it.

### Ghost focus

If a user clicks on a non-focusable widget, future tab events will still navigate the widget tree as if that widget was focused.

This concept is referred to internally as "ghost focus".


## Disabled

Expand Down Expand Up @@ -108,3 +118,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
29 changes: 24 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.ghost_focus = Some(target_widget_id);
PoignardAzur marked this conversation as resolved.
Show resolved Hide resolved

// 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,10 @@ 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;
root.global_state.ghost_focus = next_focused_widget;
handled = Handled::Yes;
}
}
Expand Down
2 changes: 2 additions & 0 deletions masonry/src/passes/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,8 @@ pub(crate) fn run_update_focus_pass(root: &mut RenderRoot) {
if widget_state.accepts_text_input {
root.global_state.emit_signal(RenderRootSignal::StartIme);
}

root.global_state.ghost_focus = Some(next_focused);
} else {
root.global_state.is_ime_active = false;
}
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 @@ -73,6 +73,10 @@ pub(crate) struct RenderRootState {
pub(crate) signal_queue: VecDeque<RenderRootSignal>,
pub(crate) focused_widget: Option<WidgetId>,
pub(crate) focused_path: Vec<WidgetId>,
/// The most recently clicked widget.
///
/// When tab-focusing, this will be used as the base.
pub(crate) ghost_focus: Option<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 @@ -197,6 +201,7 @@ impl RenderRoot {
signal_queue: VecDeque::new(),
focused_widget: None,
focused_path: Vec::new(),
ghost_focus: None,
next_focused_widget: None,
scroll_request_targets: Vec::new(),
hovered_path: Vec::new(),
Expand Down Expand Up @@ -616,7 +621,7 @@ 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.ghost_focus;
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be:

Suggested change
let focused_widget = self.global_state.ghost_focus;
let focused_widget = self.global_state.focused_widget.or(self.global_state.ghost_focus);

That is, if there is an actual currently focused widget, we should start from that for tab-focusing and the like.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mh, fair enough. I would let me remove a lot of code that tries to keep ghost focus and regular focus synchronized.

let focused_idx = focused_widget.and_then(|focused_widget| {
self.focus_chain()
.iter()
Expand Down
Loading