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

Support transforms for each widget #753

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

Philipp-M
Copy link
Contributor

@Philipp-M Philipp-M commented Nov 18, 2024

Checkout this zulip thread for more info/discussion.

This is far from the final state, and at this point should more serve for discussion.

See the xilem mason or http_cats example, which I have abused to get an initial demo working.

@Philipp-M
Copy link
Contributor Author

I found a way to avoid the boilerplate on the masonry side, by using WidgetState and adding payload (just the transform for now) to the WidgetPodInner.
If we're having more of those custom global widget attributes, a structure of arrays for these attributes (separate arena?) might make sense (as well as a builder pattern for creating WidgetPods).

But in this state I'm happy enough to merge this (after documentation and tests). As mentioned on zulip, avoiding even more boilerplate on the xilem level, is probably not reasonable, as either the API suffers (by using composition types, which doesn't allow further setting attributes of the underlying views after using a transform wrapper), or requires something like in xilem_web by only using composition types, which would be a big refactor (and a lot more complexity and code)...

Anyway to get a feeling for the added boilerplate on the xilem side, I've implemented transforms now for all views (and masonry also supports it for all widgets). I think it's not too bad and a good trade-off between complexity and boilerplate.

So apart of missing tests and docs, I think this is finished (but I might have missed things, so manual testing for now is appreciated).

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.

Sorry for the very late review.

This is not something anyone likes to hear, but I'm pretty confident this needs to be split into multiple PRs.

This PR essentially does three things:

  • Replace the translation property with a general transform.
  • Correct the pointer search algorithm to account for OOB sub-children.
  • Add a way to set transform directly from creation.

I think all three of these changes would be worth their own PR. The second in particular feels like it needs some concepts (declared layout rect vs computed layout rect) to be reified and documented, or at least considered.

let widget_rect = self.get_widget(id).ctx().window_layout_rect();
let widget_rect = self.get_widget(id).ctx().bounding_rect();
Copy link
Contributor

Choose a reason for hiding this comment

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

I have a major concern with this PR that shows up in a few places.

It seems the PR conflates two things: the bounding rect as the widget's self-expressed size plus its assigned position, and the bounding rect as the total area that needs to be invalidated if the widget is repainted.

So for example, let's say we have a clickable area, and that area has a child that overflows a thousand pixels right of the area. To click the area, we want to place the cursor in the middle of its self-reported rectangle, and not the rectangle that is the union of the parent and the out-of-bounds child.

Comment on lines +33 to +37
let local_translation = state.item.translation + state.item.origin.to_vec2();

state.item.window_transform =
parent_window_transform * state.item.transform.then_translate(local_translation);
state.item.window_origin = state.item.window_transform.translation().to_point();
Copy link
Contributor

Choose a reason for hiding this comment

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

This code first applies the widget's transform, then the translation. Is that the right order? At first I was thinking it should be opposite, but I guess if the transform includes a rotation, you don't want the position vector to be rotated too.

We might want to add a comment to clarify this.

pub fn new(text: impl Into<ArcStr>) -> Button {
pub fn new(text: impl Into<ArcStr>) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a dealbreaker, but I think this PR has a few too many drive-by fixes given its already large size.

Comment on lines +331 to 356
if widget.ctx.widget_state.bounding_rect.contains(pos) {
let local_pos = widget.ctx().widget_state.window_transform.inverse() * pos;

if Some(false) == widget.ctx.clip_path().map(|clip| clip.contains(local_pos)) {
return None;
}

// Assumes `Self::children_ids` is in increasing "z-order", picking the last child in case
// of overlapping children.
for child_id in widget.children_ids().iter().rev() {
let child_ref = widget.ctx.get(*child_id);
if let Some(child) = child_ref.widget.find_widget_at_pos(child_ref.ctx, pos) {
return Some(child);
}
}
if !widget.ctx.is_stashed()
&& widget.ctx.accepts_pointer_interaction()
&& widget.ctx.size().to_rect().contains(local_pos)
{
return Some(child);
Some(*widget)
} else {
None
}
} else {
None
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a core change in the PR, and it's one with a lot of semantic importance, so I'm giving it extra scrutiny. Some notes:

  • This code has a lot of rightward drift that could be avoided with early returns.
  • Again the difference between "declared layout rect" and "bounding rect of self and children" shows up. I think your code handles it correctly, but it does so implicitly. I think that distinction should be documented in a few places and given special attention in this function.
  • I think is_stashed should be considered before the children. A stashed widget is never going to have un-stashed children. accepts_pointer_interaction is probably fine here, if we want to behave like pointer-events: none in the DOM, but that should be documented.

Comment on lines +52 to +55
/// Create a new widget pod with a custom transform.
pub fn new_with_transform(inner: W, transform: Affine) -> WidgetPod<W> {
Self::new_with_id_and_transform(inner, WidgetId::next(), transform)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, so I get why the PR does it this way, but I'm somewhat uncomfortable with adding a new constructor and all the special-casing that comes with it for a single attribute.

I don't know how else to do it, ideally it should be part of the styling refactor, but I don't want to block on that.

Comment on lines +82 to 83
parent_state.bounding_rect = parent_state.bounding_rect.union(state.item.bounding_rect);
parent_state.merge_up(state.item);
Copy link
Contributor

Choose a reason for hiding this comment

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

By the way, this is needlessly pessimistic when the parent widget has a clipping rect.

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.

2 participants