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

masonry: reimplement Widget::get_child_at_pos #565

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

tomcur
Copy link
Contributor

@tomcur tomcur commented Aug 29, 2024

This is a stab at resolving code comment

// TODO - Use Widget::get_child_at_pos method
if let Some(child) = innermost_widget.children().into_iter().find(|child| {
!child.widget.skip_pointer() && child.state().window_layout_rect().contains(pos)
}) {
innermost_widget = child;
} else {
break;
}

This may or may not be a good resolution. In this implementation Widget::get_child_at_pos gets the signature

get_child_at_pos<'w>(&self, children: &[WidgetRef<'w, dyn Widget>], pos: Point) -> Option<WidgetRef<'w, dyn Widget>>

with children a slice of WidgetRef children in the same order as returned by Widget::children_ids. The default Widget::get_child_at_pos implementation assumes children are in increasing "z-order", picking the last child in case of overlapping children. If that assumption is kept, it should be documented, but perhaps a better solution is needed.

Copy link
Contributor

@waywardmonkeys waywardmonkeys left a comment

Choose a reason for hiding this comment

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

I don't know about the overall strategy that is desired for this, but I'm definitely interested in seeing this feature be underway.

Would be good to have a couple of tests as well though, including checking skip_pointer and some other edge cases.

masonry/src/widget/widget_ref.rs Outdated Show resolved Hide resolved
@PoignardAzur
Copy link
Contributor

The default Widget::get_child_at_pos implementation assumes children are in increasing "z-order", picking the last child in case of overlapping children. If that assumption is kept, it should be documented, but perhaps a better solution is needed.

That assumption is likely to remain, since it's how children are painted right now, and the cursor should general pick the widget whose pixels are "visible" under the mouse.

I'm not crazy about the approach of passing an array of WidgetRef, though. I would lean towards passing a nont-mutable LifecycleCtx (soon to be renamed to UpdateCtx) instead. In the cases where that method would be useful (eg a very large container), you want to avoid any kind of O(N) work.

@tomcur
Copy link
Contributor Author

tomcur commented Aug 30, 2024

I'm not crazy about the approach of passing an array of WidgetRef, though. I would lean towards passing a nont-mutable LifecycleCtx (soon to be renamed to UpdateCtx) instead. In the cases where that method would be useful (eg a very large container), you want to avoid any kind of O(N) work.

That's a good point.

Currently LifeCycleCtx (UpdateCtx) requires mutable-everything:

/// A context provided to the [`lifecycle`] method on widgets.
///
/// [`lifecycle`]: Widget::lifecycle
pub struct LifeCycleCtx<'a> {
pub(crate) global_state: &'a mut RenderRootState,
pub(crate) widget_state: &'a mut WidgetState,
pub(crate) widget_state_children: ArenaMutChildren<'a, WidgetState>,
pub(crate) widget_children: ArenaMutChildren<'a, Box<dyn Widget>>,
}

That would mean WidgetRef::find_widget_at_pos would need mutable references to construct LifeCycleCtx, but it by design holds shared references. Perhaps a new context type with shared references is required? Alternatively, WidgetMut could get the find_widget_at_pos method. Though querying for a child seems like it should be possible through a shared ref.

@waywardmonkeys
Copy link
Contributor

It would be nice if this supported the sort of use case where one might want an acceleration structure (such as a quad tree) for a widget-canvas with a lot of widgets.

@tomcur
Copy link
Contributor Author

tomcur commented Aug 30, 2024

It would be nice if this supported the sort of use case where one might want an acceleration structure (such as a quad tree) for a widget-canvas with a lot of widgets.

Indeed, that's a good use-case. If, as @PoignardAzur suggested, a cheaply-constructable context type is passed to the widget, the widget can choose to either do a linear search through its children (for example, sticking to the default behavior), or it can use whatever domain knowledge it has (for example, your use-case of a canvas with a quad tree for searching).

@PoignardAzur
Copy link
Contributor

That would mean WidgetRef::find_widget_at_pos would need mutable references to construct LifeCycleCtx, but it by design holds shared references.

Oh, that's right. That makes things more awkward.

Perhaps a new context type with shared references is required?

Probably, yes, but I'm not happy about adding yet another context type.

@tomcur
Copy link
Contributor Author

tomcur commented Aug 30, 2024

Instead of creating a new context type, WidgetRef can be provided to the widget. The signature then becomes:

fn get_child_at_pos<'w>(
    &self,
    widget_ref: WidgetRef<'w, dyn Widget>,
    pos: Point,
) -> Option<WidgetRef<'w, dyn Widget>>

I've pushed a commit with this change to illustrate. It is a bit circular, though: WidgetRef carries a reference to the widget it is now calling with a reference to itself.

Going the context route, the context could look as follows. Some of the methods on WidgetRef (like WidgetRef::find_widget_by_id) could be moved/duplicated to QueryCtx.

/// A context provided to widget methods that need read-only (shared) access.
pub struct QueryCtx<'a> {
    pub(crate) global_state: &'a RenderRootState,
    pub(crate) widget_state: &'a WidgetState,
    pub(crate) widget_state_children: ArenaRefChildren<'a, WidgetState>,
    pub(crate) widget_children: ArenaRefChildren<'a, Box<dyn Widget>>,
}

Note QueryCtx itself would not be sufficient to iterate over child WidgetRef, as a reference to the widget is needed to call Widget::children_ids. This is no problem inside Widget::get_child_at_pos, of course.

pub fn children(&self) -> SmallVec<[WidgetRef<'w, dyn Widget>; 16]> {
let parent_id = self.widget_state.id.to_raw();
self.widget
.children_ids()

@PoignardAzur
Copy link
Contributor

Sounds good. Fair warning, I won't have time to look at this over the weekend, but I'll have a look on Monday.

@tomcur
Copy link
Contributor Author

tomcur commented Sep 1, 2024

A context type plus a Widget is similar to a WidgetRef (a WidgetRef can be constructed from those two), with the option of having additional fields. In the example above, QueryCtx has a global_state: &RenderRootState field like the other context types (though shared, not mutable), so it can have methods like QueryCtx::has_pointer_capture.

Unlike WidgetRef, context types don't hold references to the Widget itself, so calling a Widget with a QueryCtx doesn't have that somewhat inelegant circular referencing.

Existing Widget methods requiring state receive it through mut ref context types, using a shared ref QueryCtx is perhaps less surprising than using a WidgetRef. It also prevents the Widget API from needing to know about WidgetRef at all.

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.

Some "drive-by" thoughts, pending Olivier's review.

masonry/src/widget/widget.rs Outdated Show resolved Hide resolved
masonry/src/widget/widget.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@PoignardAzur PoignardAzur left a comment

Choose a reason for hiding this comment

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

Something that occurs to me reading this is we probably want to rename find_widget_at_pos to find_widget_under_cursor across the crate. It's of scope for now though.

I think the WidgetRef based API is too awkward. I'd rather have one based on a new QueryCtx type.

masonry/src/widget/widget.rs Outdated Show resolved Hide resolved
@tomcur
Copy link
Contributor Author

tomcur commented Sep 10, 2024

Rebased onto main, and added commits. One introduces QueryCtx, replacing WidgetRef in the method call, but otherwise introduces minimal changes.

The commit after explores a more substantial change. WidgetRef and WidgetMut are conceptually similar. WidgetMut is a struct of a Widget and a MutateCtx. For symmetry, in this second commit WidgetRef becomes a struct of a Widget and a QueryCtx.

Some thoughts:

  1. MutateCtx::get_mut was already used to get a WidgetMut, I've added QueryCtx::get to get a WidgetRef.
  2. QueryCtx::get should perhaps take a &WidgetPod, like how MutateCtx::get_mut takes a &mut WidgetPod. It currently takes a WidgetId, because Widget::children_ids returns just that: ids, not pods. Maybe we can construct WidgetPod in the default Widget::get_child_at_pos method implementation by assuming WidgetPodInner::Inserted.
  3. QueryCtx having access to global state, like all other context types, has some ramifications. For example, to construct a WidgetRef containing a QueryCtx, having access to a widget's state is no longer enough, and means WidgetArena cannot construct a WidgetRef by itself anymore. In that commit I've moved the method for getting widget refs to RenderRoot::get_widget, which already had a method for getting a WidgetMut through RenderRoot::edit_widget.

(Zulip thread: https://xi.zulipchat.com/#narrow/stream/317477-masonry/topic/.60WidgetRef.60.20and.20.60WidgetMut.60)

masonry/src/widget/widget.rs Show resolved Hide resolved
masonry/src/widget/widget.rs Outdated Show resolved Hide resolved
masonry/src/widget/widget.rs Outdated Show resolved Hide resolved
masonry/src/widget/widget.rs Outdated Show resolved Hide resolved
masonry/src/widget/widget.rs Outdated Show resolved Hide resolved
masonry/src/widget/widget.rs Outdated Show resolved Hide resolved
Comment on lines 198 to 200
// Get child from the widget arena to bind it to the arena's lifetime. Is there a
// way around this?
let child = root.get_widget(child.id()).unwrap();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are lifetime issues here: child widgets fetched from Widget::get_child_at_pos can't be returned from the function because they don't live long enough. To get around this, the render root is passed in to refetch widgets with a longer lifetime from the global widget arena, but maybe there's a more elegant solution.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this exact kind of lifetime issue (when you want to return a borrow to something pointed to by a local, but rustc thinks you're returning a borrow to the local) has bitten me before.

I'm not sure there's an elegant solution, but finding one is a mid-high priority.

Copy link
Contributor

@PoignardAzur PoignardAzur left a comment

Choose a reason for hiding this comment

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

LGTM at a skim. Will look more in-depth later.

Comment on lines 198 to 200
// Get child from the widget arena to bind it to the arena's lifetime. Is there a
// way around this?
let child = root.get_widget(child.id()).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this exact kind of lifetime issue (when you want to return a borrow to something pointed to by a local, but rustc thinks you're returning a borrow to the local) has bitten me before.

I'm not sure there's an elegant solution, but finding one is a mid-high priority.

masonry/src/widget/widget.rs Show resolved Hide resolved
PoignardAzur
PoignardAzur previously approved these changes Sep 11, 2024
@PoignardAzur PoignardAzur dismissed their stale review September 11, 2024 12:27

Didn't mean to approve.

Comment on lines 185 to 192
if let Some(clip) = innermost_widget.state().clip {
let relative_pos = pos.to_vec2() - innermost_widget.state().window_origin.to_vec2();
// If the widget has a clip, the point must be inside
// else we don't iterate over children.
if !clip.contains(relative_pos.to_point()) {
// If the widget has a clip, the point must be inside, else we don't iterate over
// children.
let pos = pos - innermost_widget.state().window_origin().to_vec2();
if !clip.contains(pos) {
break;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This part should probably be in Widget::get_child_at_pos.

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, Widget::get_child_at_pos now checks the clip path of children.

Because WidgetRef::find_widget_at_pos does explicitly check the layout rect of self before recursing down the tree (else it returns None), I've added a check of the clip path of self, too.

///
/// The child return is a direct child, not eg a grand-child. The position is in
/// relative coordinates. (Eg `(0,0)` is the top-left corner of `self`).
/// Has a default implementation that can be overridden to search children more efficiently.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should implementations overriding the default uphold certain conditions?

Custom implementations should probably uphold the guarantee that pos is actually inside the returned child's layout rect. Must custom custom implementations also uphold the guarantee that pos is contained in the clip path, and that in case of overlap the last child in "z-order" is chosen?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes to all of these.

Ideally we should even implement some best-effort checks in debug_assertions mode to enforce that these conditions are upheld.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The low effort version is to check the child contains the position in its layout rect and clip path.

The conditions uniquely identify a child, so we could do the default search alongside Widget::get_child_at_pos and check the same children are returned. For widgets with many children—where custom implementations are most likely—this would be slow. Valid options are to accept that cost or to limit this exhaustive check to widgets with N children for some value of N.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, most of that is beyond the scope of this PR.

We could probably add a TODO comment and keep it to that for now.

Valid options are to accept that cost or to limit this exhaustive check to widgets with N children for some value of N.

Sure. I was thinking we'd probably need to add a child_count() method for cases where building a child array is needlessly expensive (eg a Flex with thousands of children).

Copy link
Contributor

@PoignardAzur PoignardAzur left a comment

Choose a reason for hiding this comment

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

This is roughly what a idiomatic version looks like. The key thing is that since QueryCtx is Copy, you can take it by value in QueryCtx::get, and that means you can request "smaller" lifetimes.

masonry/src/contexts.rs Outdated Show resolved Hide resolved
masonry/src/widget/widget.rs Outdated Show resolved Hide resolved
masonry/src/widget/widget_ref.rs Outdated Show resolved Hide resolved
masonry/src/widget/widget.rs Outdated Show resolved Hide resolved
masonry/src/widget/widget_ref.rs Outdated Show resolved Hide resolved
ctx: QueryCtx<'c>,
pos: Point,
) -> Option<WidgetRef<'c, dyn Widget>> {
let relative_pos = pos - ctx.widget_state.window_origin().to_vec2();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

QueryCtx::widget_state is pub(crate), we can access it here but user code currently can't.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, same with the clip field access.

Copy link
Contributor

@PoignardAzur PoignardAzur left a comment

Choose a reason for hiding this comment

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

LGTM.

You could probably add a few methods to QueryCtx so that the default implementation doesn't need to use private fields, but either way, it's a good first implementation.

// Methods to get a child WidgetRef from a parent.
impl<'w> QueryCtx<'w> {
/// Return a [`WidgetRef`] to a child widget.
pub fn get(self, child: WidgetId) -> WidgetRef<'w, dyn Widget> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably take WidgetPod for consistency with other context methods.

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