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

Merged
merged 29 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3136ecb
Initial dirty hack to get transforms per widget working
Philipp-M Nov 18, 2024
1606302
Handle pointer-events correctly, with a BVH (hieararchy is the widget…
Philipp-M Nov 22, 2024
fd8cba1
Fix Text pointer events in `TextArea` when a transform is applied.
Philipp-M Nov 22, 2024
61a0b13
Remove unused code
Philipp-M Nov 22, 2024
7f0b1a7
Make find_widget_at_pos behavior more correct
Philipp-M Nov 23, 2024
e9d09b7
Fix stack overflow in tests
Philipp-M Nov 23, 2024
8b6f311
Fix `get_ime_area()`
Philipp-M Nov 24, 2024
151d0b8
Replace `window_layout_rect` with `bbox` (and thus fix accessibility).
Philipp-M Nov 24, 2024
e2ad4de
Rename bbox to bounding_rect as it's more appropriate in 2D space
Philipp-M Nov 25, 2024
045af4d
Revert unnecessary changes
Philipp-M Nov 25, 2024
d8126e3
Factor the widgets transform out of the widget trait to the widget st…
Philipp-M Dec 7, 2024
e73c9ce
Move `Transformable` trait below the view modules, and add basic docu…
Philipp-M Dec 7, 2024
4b4969e
xilem: Wire up all views with transforms
Philipp-M Dec 7, 2024
90ae403
Fix clippy lints
Philipp-M Dec 7, 2024
961caf8
* Rename translation to scroll translation
Philipp-M Dec 20, 2024
a715a43
Refactor
Philipp-M Dec 27, 2024
506b178
Merge branch 'main' into transforms
Philipp-M Dec 27, 2024
4cbfc65
Fix clippy lint (`OneOf` -> `Self`)
Philipp-M Dec 27, 2024
4945288
Add transforms example and expose `masonry::{Affine, Vec2}` in Xilem
Philipp-M Dec 29, 2024
fecb99b
Merge branch 'main' into transforms
Philipp-M Jan 11, 2025
0ea14ad
Minimal improvements to the transforms example
Philipp-M Jan 11, 2025
a6e67b1
Add basic tests for transforms
Philipp-M Jan 11, 2025
deca314
Restore http_cats example without transforms
Philipp-M Jan 11, 2025
7b99baa
Add comments and handle paint_insets with the bounding_rect
Philipp-M Jan 11, 2025
bdd4bb5
Handle accesskit transforms
Philipp-M Jan 13, 2025
2478870
Revert mason example animated transform hacks
Philipp-M Jan 13, 2025
535bb50
Merge branch 'main' into transforms
Philipp-M Jan 13, 2025
5d1b0ef
Remove merge artifacts
Philipp-M Jan 13, 2025
98976b7
Add doc-comment to `local_position`
Philipp-M Jan 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# LFS settings
# If changing, also change in .github/workflows/ci.yml
masonry/src/widget/screenshots/*.png filter=lfs diff=lfs merge=lfs -text
masonry/src/**/screenshots/*.png filter=lfs diff=lfs merge=lfs -text
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ jobs:
with:
path: .git/lfs
# The files stored in git lfs are all in this folder
key: masonry-lfs-${{ hashFiles('masonry/src/widget/screenshots/*.png') }}
key: masonry-lfs-${{ hashFiles('masonry/src/**/screenshots/*.png') }}
restore-keys: masonry-lfs-
enableCrossOsArchive: true

Expand Down Expand Up @@ -248,7 +248,7 @@ jobs:
with:
path: .git/lfs
# The files stored in git lfs are all in this folder
key: masonry-lfs-${{ hashFiles('masonry/src/widget/screenshots/*.png') }}
key: masonry-lfs-${{ hashFiles('masonry/src/**/screenshots/*.png') }}
enableCrossOsArchive: true

- name: Checkout LFS files
Expand Down
51 changes: 32 additions & 19 deletions masonry/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ use dpi::LogicalPosition;
use parley::{FontContext, LayoutContext};
use tracing::{trace, warn};
use tree_arena::{ArenaMutChildren, ArenaRefChildren};
use vello::kurbo::Vec2;
use winit::window::ResizeDirection;

use crate::action::Action;
use crate::passes::layout::run_layout_on;
use crate::render_root::{MutateCallback, RenderRootSignal, RenderRootState};
use crate::text::BrushIndex;
use crate::theme::get_debug_color;
use crate::widget::{WidgetMut, WidgetRef, WidgetState};
use crate::widget::{CreateWidget, WidgetMut, WidgetRef, WidgetState};
use crate::{
AllowRawMut, BoxConstraints, Color, Insets, Point, Rect, Size, Widget, WidgetId, WidgetPod,
Affine, AllowRawMut, BoxConstraints, Color, Insets, Point, Rect, Size, Vec2, Widget, WidgetId,
WidgetPod,
};

// Note - Most methods defined in this file revolve around `WidgetState` fields.
Expand Down Expand Up @@ -482,7 +482,7 @@ impl LayoutCtx<'_> {
}
if origin != self.get_child_state_mut(child).origin {
self.get_child_state_mut(child).origin = origin;
self.get_child_state_mut(child).translation_changed = true;
self.get_child_state_mut(child).transform_changed = true;
}
self.get_child_state_mut(child)
.is_expecting_place_child_call = false;
Expand Down Expand Up @@ -524,7 +524,7 @@ impl LayoutCtx<'_> {
) -> Insets {
self.assert_layout_done(child, "compute_insets_from_child");
self.assert_placed(child, "compute_insets_from_child");
let parent_bounds = Rect::ZERO.with_size(my_size);
let parent_bounds = my_size.to_rect();
let union_paint_rect = self
.get_child_state(child)
.paint_rect()
Expand Down Expand Up @@ -652,10 +652,10 @@ impl ComposeCtx<'_> {
self.widget_state.needs_compose
}

/// Set a translation for the child widget.
/// Set the scroll translation for the child widget.
///
/// The translation is applied on top of the position from [`LayoutCtx::place_child`].
pub fn set_child_translation<W: Widget>(
pub fn set_child_scroll_translation<W: Widget>(
&mut self,
child: &mut WidgetPod<W>,
translation: Vec2,
Expand All @@ -666,17 +666,17 @@ impl ComposeCtx<'_> {
|| translation.y.is_infinite()
{
debug_panic!(
"Error in {}: trying to call 'set_child_translation' with child '{}' {} with invalid translation {:?}",
"Error in {}: trying to call 'set_child_scroll_translation' with child '{}' {} with invalid translation {:?}",
self.widget_id(),
self.get_child(child).short_type_name(),
child.id(),
translation,
);
}
let child = self.get_child_state_mut(child);
if translation != child.translation {
child.translation = translation;
child.translation_changed = true;
if translation != child.scroll_translation {
child.scroll_translation = translation;
child.transform_changed = true;
}
}
}
Expand Down Expand Up @@ -723,11 +723,9 @@ impl_context_method!(
self.widget_state.window_origin()
}

/// The layout rect of the widget in window coordinates.
///
/// Combines the [size](Self::size) and [window origin](Self::window_origin).
pub fn window_layout_rect(&self) -> Rect {
self.widget_state.window_layout_rect()
/// The axis aligned bounding rect of this widget in window coordinates.
pub fn bounding_rect(&self) -> Rect {
self.widget_state.bounding_rect()
}

// TODO - Remove? See above.
Expand All @@ -750,7 +748,7 @@ impl_context_method!(
///
/// The returned point is relative to the content area; it excludes window chrome.
pub fn to_window(&self, widget_point: Point) -> Point {
self.window_origin() + widget_point.to_vec2()
self.widget_state.window_transform * widget_point
}
}
);
Expand Down Expand Up @@ -939,6 +937,13 @@ impl_context_method!(MutateCtx<'_>, EventCtx<'_>, UpdateCtx<'_>, {
self.request_layout();
}

/// Indicate that the transform of this widget has changed.
pub fn transform_changed(&mut self) {
trace!("transform_changed");
self.widget_state.transform_changed = true;
self.request_compose();
}

/// Indicate that a child is about to be removed from the tree.
///
/// Container widgets should avoid dropping `WidgetPod`s. Instead, they should
Expand Down Expand Up @@ -969,6 +974,14 @@ impl_context_method!(MutateCtx<'_>, EventCtx<'_>, UpdateCtx<'_>, {
self.widget_state.needs_update_disabled = true;
self.widget_state.is_explicitly_disabled = disabled;
}

/// Set the transform for this widget.
///
/// It behaves similarly as CSS transforms
pub fn set_transform(&mut self, transform: Affine) {
self.widget_state.transform = transform;
self.transform_changed();
}
});

// --- MARK: OTHER METHODS ---
Expand Down Expand Up @@ -1117,7 +1130,7 @@ impl RegisterCtx<'_> {
/// Container widgets should call this on all their children in
/// their implementation of [`Widget::register_children`].
pub fn register_child(&mut self, child: &mut WidgetPod<impl Widget>) {
let Some(widget) = child.take_inner() else {
let Some(CreateWidget { widget, transform }) = child.take_inner() else {
return;
};

Expand All @@ -1127,7 +1140,7 @@ impl RegisterCtx<'_> {
}

let id = child.id();
let state = WidgetState::new(child.id(), widget.short_type_name());
let state = WidgetState::new(child.id(), widget.short_type_name(), transform);

self.widget_children.insert_child(id, Box::new(widget));
self.widget_state_children.insert_child(id, state);
Expand Down
8 changes: 8 additions & 0 deletions masonry/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use std::path::PathBuf;

use vello::kurbo::Point;
use winit::event::{Force, Ime, KeyEvent, Modifiers};
use winit::keyboard::ModifiersState;

Expand Down Expand Up @@ -430,6 +431,13 @@ impl PointerEvent {
}
}

// TODO Logical/PhysicalPosition as return type instead?
/// Returns the position of this event in local (the widget's) coordinate space.
pub fn local_position(&self, ctx: &crate::EventCtx) -> Point {
let position = self.pointer_state().position;
ctx.widget_state.window_transform.inverse() * Point::new(position.x, position.y)
}

/// Create a [`PointerEvent::PointerLeave`] event with dummy values.
///
/// This is used internally to create synthetic `PointerLeave` events when pointer
Expand Down
21 changes: 15 additions & 6 deletions masonry/src/passes/accessibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,8 @@ fn build_accessibility_tree(
tree_update,
rebuild_all,
};
let mut node = build_access_node(widget.item, &mut ctx);
let mut node = build_access_node(widget.item, &mut ctx, scale_factor);
widget.item.accessibility(&mut ctx, &mut node);
if let Some(scale_factor) = scale_factor {
node.set_transform(accesskit::Affine::scale(scale_factor));
}

let id: NodeId = ctx.widget_state.id.into();
if ctx.global_state.trace.access {
Expand Down Expand Up @@ -89,9 +86,21 @@ fn build_accessibility_tree(
}

// --- MARK: BUILD NODE ---
fn build_access_node(widget: &mut dyn Widget, ctx: &mut AccessCtx) -> Node {
fn build_access_node(
widget: &mut dyn Widget,
ctx: &mut AccessCtx,
scale_factor: Option<f64>,
) -> Node {
let mut node = Node::new(widget.accessibility_role());
node.set_bounds(to_accesskit_rect(ctx.widget_state.window_layout_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 don't think this is correct. This should be the "layout" rect, not the bounding rect.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, reading the docs of accesskit, I think there's more to it, as the node can be transformed. I've pushed (untested) code how I think it needs to be handled according to the docs, I currently don't have a setup to quickly evaluate this. Maybe you can check again? I'm not sure though whether the scroll translation should be applied though...

node.set_bounds(to_accesskit_rect(ctx.widget_state.size.to_rect()));

let local_translation = ctx.widget_state.scroll_translation + ctx.widget_state.origin.to_vec2();
let mut local_transform = ctx.widget_state.transform.then_translate(local_translation);

if let Some(scale_factor) = scale_factor {
local_transform = local_transform.pre_scale(scale_factor);
}
node.set_transform(accesskit::Affine::new(local_transform.as_coeffs()));

node.set_children(
widget
Expand Down
44 changes: 33 additions & 11 deletions masonry/src/passes/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use tracing::info_span;
use tree_arena::ArenaMut;
use vello::kurbo::Vec2;
use vello::kurbo::Affine;

use crate::passes::{enter_span_if, recurse_on_children};
use crate::render_root::{RenderRoot, RenderRootState};
Expand All @@ -14,8 +14,8 @@ fn compose_widget(
global_state: &mut RenderRootState,
mut widget: ArenaMut<'_, Box<dyn Widget>>,
mut state: ArenaMut<'_, WidgetState>,
parent_moved: bool,
parent_translation: Vec2,
parent_transformed: bool,
parent_window_transform: Affine,
) {
let _span = enter_span_if(
global_state.trace.compose,
Expand All @@ -24,14 +24,21 @@ fn compose_widget(
state.reborrow(),
);

let moved = parent_moved || state.item.translation_changed;
let translation = parent_translation + state.item.translation + state.item.origin.to_vec2();
state.item.window_origin = translation.to_point();
let transformed = parent_transformed || state.item.transform_changed;

if !parent_moved && !state.item.translation_changed && !state.item.needs_compose {
if !transformed && !state.item.needs_compose {
return;
}

// the translation needs to be applied *after* applying the transform, as translation by scrolling should be within the transformed coordinate space. Same is true for the (layout) origin, to behave similar as in CSS.
let local_translation = state.item.scroll_translation + state.item.origin.to_vec2();

state.item.window_transform =
parent_window_transform * state.item.transform.then_translate(local_translation);

let local_rect = state.item.size.to_rect() + state.item.paint_insets;
state.item.bounding_rect = state.item.window_transform.transform_rect_bbox(local_rect);

let mut ctx = ComposeCtx {
global_state,
widget_state: state.item,
Expand All @@ -49,9 +56,10 @@ fn compose_widget(

state.item.needs_compose = false;
state.item.request_compose = false;
state.item.translation_changed = false;
state.item.transform_changed = false;

let id = state.item.id;
let parent_transform = state.item.window_transform;
let parent_state = state.item;
recurse_on_children(
id,
Expand All @@ -62,9 +70,23 @@ fn compose_widget(
global_state,
widget,
state.reborrow_mut(),
moved,
translation,
transformed,
parent_transform,
);
let parent_bounding_rect = parent_state.bounding_rect;

// This could be further optimized by more tightly clipping the child bounding rect according to the clip path.
let clipped_child_bounding_rect = if let Some(clip_path) = parent_state.clip_path {
let clip_path_bounding_rect =
parent_state.window_transform.transform_rect_bbox(clip_path);
state.item.bounding_rect.intersect(clip_path_bounding_rect)
} else {
state.item.bounding_rect
};
if !clipped_child_bounding_rect.is_zero_area() {
parent_state.bounding_rect =
parent_bounding_rect.union(clipped_child_bounding_rect);
}
parent_state.merge_up(state.item);
},
);
Expand All @@ -87,6 +109,6 @@ pub(crate) fn run_compose_pass(root: &mut RenderRoot) {
root_widget,
root_state,
false,
Vec2::ZERO,
Affine::IDENTITY,
);
}
11 changes: 6 additions & 5 deletions masonry/src/passes/paint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ use std::collections::HashMap;

use tracing::{info_span, trace};
use tree_arena::ArenaMut;
use vello::kurbo::{Affine, Stroke};
use vello::peniko::Mix;
use vello::Scene;

use crate::paint_scene_helpers::stroke;
use crate::passes::{enter_span_if, recurse_on_children};
use crate::render_root::{RenderRoot, RenderRootState};
use crate::theme::get_debug_color;
Expand Down Expand Up @@ -54,7 +54,7 @@ fn paint_widget(

let clip = state.item.clip_path;
let has_clip = clip.is_some();
let transform = Affine::translate(state.item.window_origin.to_vec2());
let transform = state.item.window_transform;
let scene = scenes.get(&id).unwrap();

if let Some(clip) = clip {
Expand All @@ -64,7 +64,7 @@ fn paint_widget(
complete_scene.append(scene, Some(transform));

let id = state.item.id;
let size = state.item.size;
let bounding_rect = state.item.bounding_rect;
let parent_state = state.item;
recurse_on_children(
id,
Expand Down Expand Up @@ -92,11 +92,12 @@ fn paint_widget(
},
);

// draw the global axis aligned bounding rect of the widget
if debug_paint {
const BORDER_WIDTH: f64 = 1.0;
let rect = size.to_rect().inset(BORDER_WIDTH / -2.0);
let color = get_debug_color(id.to_raw());
complete_scene.stroke(&Stroke::new(BORDER_WIDTH), transform, color, None, &rect);
let rect = bounding_rect.inset(BORDER_WIDTH / -2.0);
stroke(complete_scene, &rect, color, BORDER_WIDTH);
}

if has_clip {
Expand Down
2 changes: 1 addition & 1 deletion masonry/src/passes/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ pub(crate) fn run_update_scroll_pass(root: &mut RenderRoot) {
// is more accurate.

let state = &ctx.widget_state;
target_rect = target_rect + state.translation + state.origin.to_vec2();
target_rect = target_rect + state.scroll_translation + state.origin.to_vec2();
});
}
}
Expand Down
4 changes: 2 additions & 2 deletions masonry/src/testing/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,8 +467,8 @@ impl TestHarness {
#[track_caller]
pub fn mouse_move_to(&mut self, id: WidgetId) {
let widget = self.get_widget(id);
let widget_rect = widget.ctx().window_layout_rect();
let widget_center = widget_rect.center();
let local_widget_center = (widget.ctx().size() / 2.0).to_vec2().to_point();
let widget_center = widget.ctx().widget_state.window_transform * local_widget_center;

if !widget.ctx().accepts_pointer_interaction() {
panic!("Widget {id} doesn't accept pointer events");
Expand Down
Loading
Loading