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 event and update_pointer passes from pass spec RFC #488

Merged
merged 28 commits into from
Aug 13, 2024

Conversation

PoignardAzur
Copy link
Contributor

@PoignardAzur PoignardAzur commented Aug 5, 2024

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.

@PoignardAzur PoignardAzur changed the title Implement and UPDATE_POINTER passes from pass spec RFC Implement event and UPDATE_POINTER passes from pass spec RFC Aug 8, 2024
@PoignardAzur
Copy link
Contributor Author

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.

Copy link
Contributor

@Philipp-M Philipp-M left a 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
Copy link
Contributor

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.

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'm leaving this for a later PR.

Comment on lines 611 to 576
// TODO - Document
// TODO - Figure out cases where widget should be notified of pointer capture
// loss
// TODO - Panic when event isn't PointerDown
Copy link
Contributor

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)

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've added a quick-and-dirty assertion. Documentation will be for a later PR.

@PoignardAzur
Copy link
Contributor Author

Will look into the crash.

@@ -0,0 +1,344 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0

Copy link
Contributor

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.

Copy link
Contributor

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?

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'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) {}
Copy link
Contributor

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?

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'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".

Copy link
Contributor

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".

Copy link
Contributor

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.

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'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.

@PoignardAzur
Copy link
Contributor Author

PoignardAzur commented Aug 9, 2024

I've fixed that crash with the mason example. I'm getting some flickering from the cursor for some reason, will look into that.

EDIT: It's fixed.

@waywardmonkeys
Copy link
Contributor

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.

Copy link
Contributor

@Philipp-M Philipp-M left a 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

Copy link
Contributor

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?

Comment on lines 206 to 208
let mut pass_fn = pass_fn;

let mut target_widget_id = target;
Copy link
Contributor

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?)

Copy link
Contributor Author

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.

}

merge_state_up(&mut root.widget_arena, widget_id, root_state);
target_widget_id = parent_id;
Copy link
Contributor

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)

Copy link
Member

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.

Comment on lines +116 to +120
if let Some(capture_target) = root.state.pointer_capture_target {
if next_hovered_widget != Some(capture_target) {
next_hovered_widget = None;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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)

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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.)

Copy link
Contributor

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.

Copy link
Member

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.)

Copy link
Member

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 $$\text{Depth}^{2}$$ occurs if at every item in the tree, the parent has changed. I.e. the tree used to be:

graph TB;
    A;
    B;
    A-->C;
    A-->D;
    C-->E;
Loading

where E was hovered over, and is now something like:

graph TB;
    A;
    A-->D;
    B-->C;
    D-->E;
Loading

even if E is still hovered over?

Copy link
Contributor Author

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) {}
Copy link
Contributor

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.

@DJMcNab
Copy link
Member

DJMcNab commented Aug 12, 2024

I think the PR description needs some details, and a link to the RFC.

Copy link
Member

@DJMcNab DJMcNab left a 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)),
Copy link
Member

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?

Copy link
Contributor Author

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");
Copy link
Member

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
Copy link
Member

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 $$\text{Depth}^{2}$$ occurs if at every item in the tree, the parent has changed. I.e. the tree used to be:

graph TB;
    A;
    B;
    A-->C;
    A-->D;
    C-->E;
Loading

where E was hovered over, and is now something like:

graph TB;
    A;
    A-->D;
    B-->C;
    D-->E;
Loading

even if E is still hovered over?

});
}

// TODO - Make sure widgets are iterated from the bottom up
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// TODO - Make sure widgets are iterated from the bottom up
// TODO - Make sure widgets are iterated from the bottom up for efficiency.

Copy link
Contributor Author

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.

root.state.hovered_path = next_hovered_path;

// -- UPDATE CURSOR --
// TODO - Rewrite more cleanly
Copy link
Member

Choose a reason for hiding this comment

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

How?

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 really know, but I do want to leave that comment for a future cleanup.

Copy link
Contributor Author

@PoignardAzur PoignardAzur Aug 13, 2024

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.

// -- 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);
Copy link
Member

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?

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 think so?

Copy link
Contributor Author

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.

.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 {
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

@DJMcNab
Copy link
Member

DJMcNab commented Aug 12, 2024

Seems like that was poor timing - just did a review pass.

Copy link
Member

@DJMcNab DJMcNab left a 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)

Comment on lines +116 to +120
if let Some(capture_target) = root.state.pointer_capture_target {
if next_hovered_widget != Some(capture_target) {
next_hovered_widget = None;
}
}
Copy link
Member

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)

@PoignardAzur PoignardAzur added this pull request to the merge queue Aug 13, 2024
Merged via the queue into main with commit 59ee615 Aug 13, 2024
17 checks passed
@PoignardAzur PoignardAzur deleted the implement_pass_spec branch August 13, 2024 08:56
@waywardmonkeys
Copy link
Contributor

Yay!

tomcur added a commit to tomcur/xilem that referenced this pull request Aug 29, 2024
…mprove docs

Continuation of 59ee615
(linebender#488).

Makes `has_pointer_capture` available on all context types except
`LayoutCtx`, like `is_active` used to be.
tomcur added a commit to tomcur/xilem that referenced this pull request Aug 29, 2024
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.
tomcur added a commit to tomcur/xilem that referenced this pull request Aug 29, 2024
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.
tomcur added a commit to tomcur/xilem that referenced this pull request Aug 30, 2024
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.
github-merge-queue bot pushed a commit that referenced this pull request Sep 2, 2024
Fixes scrolling `Portal` on scrollbar drag by recomposing instead of
relayouting.

Regression probably caused in 59ee615
(#488) or
ff7635e
(#522).
tomcur added a commit to tomcur/xilem that referenced this pull request Sep 3, 2024
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.
tomcur added a commit to tomcur/xilem that referenced this pull request Sep 3, 2024
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.
github-merge-queue bot pushed a commit that referenced this pull request Sep 10, 2024
)

Also improve documentation of pointer capture.

Continuation of 59ee615
(#488).

Makes `has_pointer_capture` available on all context types except
`LayoutCtx`, like `is_active` used to be.
tomcur added a commit to tomcur/xilem that referenced this pull request Sep 27, 2024
github-merge-queue bot pushed a commit that referenced this pull request Sep 27, 2024
`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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants