-
Notifications
You must be signed in to change notification settings - Fork 126
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 event and update_pointer passes from pass spec RFC #488
Conversation
dc44cd8
to
213b11d
Compare
Unit tests pass, examples seem to work fine, and the last commit fixed a performance-cliff-in-waiting. If we want to implement the RFC step by step, now would be a good point to merge. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just started looking into this. And quickly tested the examples in xilem. The Textbox widget seems to lead to a panic when hovering over it. Should be easy to reproduce by running the mason
example.
@@ -333,6 +326,7 @@ impl_context_method!( | |||
// --- MARK: CURSOR --- | |||
// Cursor-related impls. | |||
impl_context_method!(EventCtx<'_>, { | |||
// TODO - Rewrite doc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if there's something missing yet in the code, but it would help reviewing this IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm leaving this for a later PR.
masonry/src/contexts.rs
Outdated
// TODO - Document | ||
// TODO - Figure out cases where widget should be notified of pointer capture | ||
// loss | ||
// TODO - Panic when event isn't PointerDown |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder whether this should probably be done in this PR? It would make reviewing easier at least. (Same for the other methods)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a quick-and-dirty assertion. Documentation will be for a later PR.
Will look into the crash. |
@@ -0,0 +1,344 @@ | |||
// Copyright 2024 the Xilem Authors | |||
// SPDX-License-Identifier: Apache-2.0 | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Module level comments, if no other comments were added, would help to understand this PR. Same is true of other files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a lot of that RFC (that seems to document a lot) as module comments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll move a lot of the RFC into documentation once the port is complete. Right now I'm focusing on implementation.
fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) { | ||
self.child.on_pointer_event(ctx, event); | ||
} | ||
fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these end up having a default implementation on Widget
and not have blank ones everywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm split. On the one hand, some widgets won't need to implement them. On the other hand, filling in default implementations take like two minutes, and it's good to be mindful about what methods you implement. I lean towards "no unless >95% of widgets are expected not to implement the method".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the other hand, it is a bunch of boilerplate and it means you can't search for meaningful implementations of the method rather than "all implementations, even the empty ones".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I also think default implementations may reduce unnecessary clutter and I think are one of the main advantages of avoiding the hierarchical child-calling we had previously.
Instead I think good documentation should help the implementer of that trait what they actually need to implement for their widget.
Reasons for default impls get even more vaild, when we add more of these methods to the trait (which I'm pretty sure will happen). So we could extend the trait more deliberately without having to think about all the boilerplate or breaking changes that it causes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not so sure, but I guess it doesn't hurt to add a default impl.
Empty implementations can be removed in a follow-up PR.
I've fixed that crash with the EDIT: It's fixed. |
I'm essentially okay with this in the current state as an incremental step towards a better place. I haven't run it myself, just reviewed code and RFCs. I think it would benefit strongly from additional documentation and comments within the code, even if it were largely adapted from some of the RFC text. I'll defer to @DJMcNab or @Philipp-M for an actual approval as they're more up to speed on some of this than I am. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks mostly good (Daniel should probably look over this as well, I likely have missed stuff (so far)).
I'm all for more incremental PRs of that RFC, though it would be nice, when they're more atomic/single isolated changes like the arena changes (and ideally including already doc-comments, and a little bit more polished).
It would make reviewing easier (and likely faster) and more effective, like splitting single changes that seem to be mostly consolidated out of a working branch. I know that may increase a little bit the workload, but I think also results in higher code-quality merged in main (and less numerous half-finished building-blocks).
(I'll review more tomorrow, way too late here...)
@@ -0,0 +1,344 @@ | |||
// Copyright 2024 the Xilem Authors | |||
// SPDX-License-Identifier: Apache-2.0 | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a lot of that RFC (that seems to document a lot) as module comments?
masonry/src/passes/event.rs
Outdated
let mut pass_fn = pass_fn; | ||
|
||
let mut target_widget_id = target; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: you could just use target
/pass_fn
directly, by declaring it mut
in the function signature (or is this intentional?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done for pass_fn. Renamed target_widget_id for something clearer instead.
masonry/src/passes/event.rs
Outdated
} | ||
|
||
merge_state_up(&mut root.widget_arena, widget_id, root_state); | ||
target_widget_id = parent_id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So unlike browser behavior, this doesn't do a capturing phase (yet?), only bubbling.
(Just noticing)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We decided against a capturing phase, in the hope that it won't be necessary.
if let Some(capture_target) = root.state.pointer_capture_target { | ||
if next_hovered_widget != Some(capture_target) { | ||
next_hovered_widget = None; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if let Some(capture_target) = root.state.pointer_capture_target { | |
if next_hovered_widget != Some(capture_target) { | |
next_hovered_widget = None; | |
} | |
} | |
let pointer_capture_target = root.state.pointer_capture_target; | |
if pointer_capture_target.is_some() && pointer_capture_target != next_hovered_widget { | |
next_hovered_widget = None; | |
} |
(could reuse that pointer_capture_target
below)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why you're resolving/ignoring my comments without any action on it (not the first time I notice this)?
That's not really encouraging for reviewing TBH.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case it's because I don't want to merge that change. I don't think it improves readability.
It was kind of a dick move to mark it as resolved without explaining the reason, sorry.
I think every other comment you'd made I've either addressed in a follow-up commit or explained why I wouldn't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Note that sometimes I mark a comment as resolved after I wrote the change that addresses it, but before I've pushed it. It helps me keep track of what to do, but maybe it's not worth the confusion it creates for reviewers.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm fine with a short comment, explaining that (although I disagree), generally I'd prefer, when the author of a review-comment resolves it, unless it's obviously resolved, as (and don't take this personal) it looks like silencing concerns.
I think every other comment you'd made I've either addressed
No, the nits for example not (although being nits, it's not being really important), but a short answer to e.g. "or is this intentional?" would've been nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I also don't think that this suggestion is better)
// This algorithm is written to be resilient to future changes like reparenting and multiple | ||
// cursors. In theory it's O(Depth² * CursorCount) in the worst case, which isn't too bad | ||
// (cursor count is usually 1 or 2, depth is usually small), but in practice it's almost | ||
// always O(Depth * CursorCount) because we only need to update the hovered status of the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that then more like the average case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not average or amortized, because by default after you visit a widget you immediately visit the next one in next_hovered_path
(its parent).
To be specific, worst case for the algorithm below that comment is O(Depth * CursorCount)
unless a widget has been reparented (which currently isn't possible but might be in the future) in which case it's O(Depth² * CursorCount)
.
(The above assumes that accessing a widget is O(1)
, which isn't the case but will be in the future.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so the factor of
graph TB;
A;
B;
A-->C;
A-->D;
C-->E;
where E was hovered over, and is now something like:
graph TB;
A;
A-->D;
B-->C;
D-->E;
even if E is still hovered over?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup.
fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) { | ||
self.child.on_pointer_event(ctx, event); | ||
} | ||
fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I also think default implementations may reduce unnecessary clutter and I think are one of the main advantages of avoiding the hierarchical child-calling we had previously.
Instead I think good documentation should help the implementer of that trait what they actually need to implement for their widget.
Reasons for default impls get even more vaild, when we add more of these methods to the trait (which I'm pretty sure will happen). So we could extend the trait more deliberately without having to think about all the boilerplate or breaking changes that it causes.
I think the PR description needs some details, and a link to the RFC. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've not reasoned about this from the top down, but I really like the core of the new code!
Not approving to allow time for review feedback to be handled.
@@ -30,15 +30,15 @@ impl Checkbox { | |||
pub fn new(checked: bool, text: impl Into<ArcStr>) -> Checkbox { | |||
Checkbox { | |||
checked, | |||
label: WidgetPod::new(Label::new(text)), | |||
label: WidgetPod::new(Label::new(text).with_skip_pointer(true)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When would a label not skip pointer handling?
I guess maybe if we get link support within labels?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Links and selecting text (though I think maybe we have the Prose widget for that now?).
I guess we could go ahead and say they're always skipped and only Prose gets to have links. I haven't really thought this through and I don't think it's a priority.
@@ -570,7 +582,7 @@ impl TestHarness { | |||
// Remove '<test_name>.new.png' file if it exists | |||
let _ = std::fs::remove_file(&new_path); | |||
new_image.save(&new_path).unwrap(); | |||
panic!("No reference file"); | |||
panic!("Snapshot test '{test_name}' failed: No reference file"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great. I am planning on making these snapshot tests much more user-friendly, but this is a good start.
// This algorithm is written to be resilient to future changes like reparenting and multiple | ||
// cursors. In theory it's O(Depth² * CursorCount) in the worst case, which isn't too bad | ||
// (cursor count is usually 1 or 2, depth is usually small), but in practice it's almost | ||
// always O(Depth * CursorCount) because we only need to update the hovered status of the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so the factor of
graph TB;
A;
B;
A-->C;
A-->D;
C-->E;
where E was hovered over, and is now something like:
graph TB;
A;
A-->D;
B-->C;
D-->E;
even if E is still hovered over?
masonry/src/passes/update.rs
Outdated
}); | ||
} | ||
|
||
// TODO - Make sure widgets are iterated from the bottom up |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// TODO - Make sure widgets are iterated from the bottom up | |
// TODO - Make sure widgets are iterated from the bottom up for efficiency. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not just for efficiency, we want iteration order to be predictable. We'll probably want to document it too.
masonry/src/passes/update.rs
Outdated
root.state.hovered_path = next_hovered_path; | ||
|
||
// -- UPDATE CURSOR -- | ||
// TODO - Rewrite more cleanly |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really know, but I do want to leave that comment for a future cleanup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did end up rewriting it. I think it's a lot more readable now.
masonry/src/passes/update.rs
Outdated
// -- UPDATE CURSOR -- | ||
// TODO - Rewrite more cleanly | ||
let cursor_changed = next_hovered_widget | ||
.is_some_and(|id| root.widget_arena.get_state_mut(id).item.cursor_changed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be a mem::take
of the cursor_changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, you meant to skip the later part that set this to false? Yeah, in retrospect that was a good idea. It's moot now.
masonry/src/passes/update.rs
Outdated
.is_some_and(|id| root.widget_arena.get_state_mut(id).item.cursor_changed); | ||
if hovered_widget != next_hovered_widget || cursor_changed { | ||
let cursor; | ||
if let Some(capture_target) = root.state.pointer_capture_target { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm presuming the case where the capture target's cursor_changed
is set is handled elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... Crap, I don't think it is. Good catch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-reading this, I think I was assuming capture_target
would always be the same as hovered_widget
, but that's not actually the case (hovered_widget
could be None it we'd still want to check the capture target).
In the end I just removed the cursor_changed
part for expediency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like this code! It's reasonably clear what's happening, and why.
I think there could be a few more comments to make understanding a bit easier.
Seems like that was poor timing - just did a review pass. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at it again, nothing in these review comments is especially controversial, and so we should be fine to land once these are addressed.
(I would however like to echo the concern about review comments not being appropriately actioned)
Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
if let Some(capture_target) = root.state.pointer_capture_target { | ||
if next_hovered_widget != Some(capture_target) { | ||
next_hovered_widget = None; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I also don't think that this suggestion is better)
Yay! |
…mprove docs Continuation of 59ee615 (linebender#488). Makes `has_pointer_capture` available on all context types except `LayoutCtx`, like `is_active` used to be.
Improve documentation of pointer capture. Continuation of 59ee615 (linebender#488). Makes `has_pointer_capture` available on all context types except `LayoutCtx`, like `is_active` used to be.
Improve documentation of pointer capture. Continuation of 59ee615 (linebender#488). Makes `has_pointer_capture` available on all context types except `LayoutCtx`, like `is_active` used to be.
Improve documentation of pointer capture. Continuation of 59ee615 (linebender#488). Makes `has_pointer_capture` available on all context types except `LayoutCtx`, like `is_active` used to be.
Improve documentation of pointer capture. Continuation of 59ee615 (linebender#488). Makes `has_pointer_capture` available on all context types except `LayoutCtx`, like `is_active` used to be.
Improve documentation of pointer capture. Continuation of 59ee615 (linebender#488). Makes `has_pointer_capture` available on all context types except `LayoutCtx`, like `is_active` used to be.
`WidgetState::merge_up` no longer actually mutates `child_state` since linebender#488 and linebender#599, but it may do so again in the future. See also this Zulip thread: https://xi.zulipchat.com/#narrow/stream/317477-masonry/topic/WidgetState.3A.3Amerge_up.20no.20longer.20needs.20mutable.20child.20state
`WidgetState::merge_up` no longer actually mutates `child_state` since #488 and #599, but it may do so again in the future. See also this Zulip thread: https://xi.zulipchat.com/#narrow/stream/317477-masonry/topic/WidgetState.3A.3Amerge_up.20no.20longer.20needs.20mutable.20child.20state
This is a first step in implementing the "Pass Specification" RFC:
linebender/rfcs#7
Create a
passes
module.Create event passes.
Create the update_pointer pass.
Remove
WidgetPod::update_hot_state
method.Move mouse-cursor-handling code to update_pointer pass.
Implement pointer capture.
Refactor the TreeArena code.