-
Notifications
You must be signed in to change notification settings - Fork 109
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
base: main
Are you sure you want to change the base?
Conversation
f56694b
to
f569230
Compare
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 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.
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 |
That's a good point. Currently LifeCycleCtx (UpdateCtx) requires mutable-everything: Lines 65 to 73 in 6c49516
That would mean |
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). |
Oh, that's right. That makes things more awkward.
Probably, yes, but I'm not happy about adding yet another context type. |
Instead of creating a new context type, 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: Going the context route, the context could look as follows. Some of the methods on /// 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 xilem/masonry/src/widget/widget_ref.rs Lines 109 to 112 in 6c49516
|
Sounds good. Fair warning, I won't have time to look at this over the weekend, but I'll have a look on Monday. |
A context type plus a Unlike Existing |
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.
Some "drive-by" thoughts, pending Olivier's review.
be65d65
to
48c8325
Compare
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.
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.
a5ae090
to
381d996
Compare
381d996
to
cb2dcaa
Compare
Rebased onto main, and added commits. One introduces The commit after explores a more substantial change. Some thoughts:
(Zulip thread: https://xi.zulipchat.com/#narrow/stream/317477-masonry/topic/.60WidgetRef.60.20and.20.60WidgetMut.60) |
masonry/src/widget/widget_ref.rs
Outdated
// 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(); |
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.
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.
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, 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.
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.
LGTM at a skim. Will look more in-depth later.
masonry/src/widget/widget_ref.rs
Outdated
// 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(); |
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, 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_ref.rs
Outdated
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; | ||
} | ||
} |
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.
This part should probably be in Widget::get_child_at_pos
.
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, 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. |
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 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?
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.
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.
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.
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.
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.
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).
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.
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.
ctx: QueryCtx<'c>, | ||
pos: Point, | ||
) -> Option<WidgetRef<'c, dyn Widget>> { | ||
let relative_pos = pos - ctx.widget_state.window_origin().to_vec2(); |
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.
QueryCtx::widget_state
is pub(crate)
, we can access it here but user code currently can'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.
Right, same with the clip
field access.
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.
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> { |
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.
This should probably take WidgetPod
for consistency with other context methods.
This is a stab at resolving code comment
xilem/masonry/src/widget/widget_ref.rs
Lines 192 to 199 in ac95f25
This may or may not be a good resolution. In this implementation
Widget::get_child_at_pos
gets the signaturewith
children
a slice ofWidgetRef
children in the same order as returned byWidget::children_ids
. The defaultWidget::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.