mirror of https://github.com/linebender/xilem
Support transforms for each widget (#753)
Adds basic support for transforms (`kurbo::Affine`) of widgets in Masonry and Xilem, similarly as CSS transforms. A new example `transforms` shows this in a playful way. The pointer-intersection logic needed a core change to handle now possibly overlapping widgets. Checkout [this](https://xi.zulipchat.com/#narrow/channel/317477-masonry/topic/Add.20.60Affine.60.20transform.20to.20.60Widget.60.20trait.3F) zulip thread for more info/discussion.
This commit is contained in:
parent
8fd5bde369
commit
9bb33dd1c6
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -8,7 +8,6 @@ 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;
|
||||
|
@ -16,9 +15,10 @@ 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.
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -666,7 +666,7 @@ 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(),
|
||||
|
@ -674,9 +674,9 @@ impl ComposeCtx<'_> {
|
|||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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 ---
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use vello::kurbo::Point;
|
||||
use winit::event::{Force, Ime, KeyEvent, Modifiers};
|
||||
use winit::keyboard::ModifiersState;
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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()));
|
||||
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
|
||||
|
|
|
@ -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};
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
|
@ -87,6 +109,6 @@ pub(crate) fn run_compose_pass(root: &mut RenderRoot) {
|
|||
root_widget,
|
||||
root_state,
|
||||
false,
|
||||
Vec2::ZERO,
|
||||
Affine::IDENTITY,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -18,7 +18,7 @@ use tracing::trace_span;
|
|||
use vello::Scene;
|
||||
|
||||
use crate::event::{PointerEvent, TextEvent};
|
||||
use crate::widget::widget::get_child_at_pos;
|
||||
use crate::widget::widget::{find_widget_at_pos, AsDynWidget as _};
|
||||
use crate::widget::{SizedBox, WidgetRef};
|
||||
use crate::{
|
||||
AccessCtx, AccessEvent, AsAny, BoxConstraints, ComposeCtx, CursorIcon, EventCtx, LayoutCtx,
|
||||
|
@ -401,12 +401,18 @@ impl<S: 'static> Widget for ModularWidget<S> {
|
|||
CursorIcon::Default
|
||||
}
|
||||
|
||||
fn get_child_at_pos<'c>(
|
||||
&self,
|
||||
fn find_widget_at_pos<'c>(
|
||||
&'c self,
|
||||
ctx: QueryCtx<'c>,
|
||||
pos: Point,
|
||||
) -> Option<WidgetRef<'c, dyn Widget>> {
|
||||
get_child_at_pos(self, ctx, pos)
|
||||
find_widget_at_pos(
|
||||
&WidgetRef {
|
||||
widget: self.as_dyn(),
|
||||
ctx,
|
||||
},
|
||||
pos,
|
||||
)
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
|
@ -601,12 +607,12 @@ impl<W: Widget> Widget for Recorder<W> {
|
|||
self.child.get_cursor(ctx, pos)
|
||||
}
|
||||
|
||||
fn get_child_at_pos<'c>(
|
||||
&self,
|
||||
fn find_widget_at_pos<'c>(
|
||||
&'c self,
|
||||
ctx: QueryCtx<'c>,
|
||||
pos: Point,
|
||||
) -> Option<WidgetRef<'c, dyn Widget>> {
|
||||
self.child.get_child_at_pos(ctx, pos)
|
||||
self.child.find_widget_at_pos(ctx, pos)
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
|
|
|
@ -1146,7 +1146,7 @@ impl Widget for Flex {
|
|||
// or be clipped (e.g. if its parent is a Portal).
|
||||
let my_size: Size = self.direction.pack(major, minor_dim).into();
|
||||
|
||||
let my_bounds = Rect::ZERO.with_size(my_size);
|
||||
let my_bounds = my_size.to_rect();
|
||||
let insets = child_paint_rect - my_bounds;
|
||||
ctx.set_paint_insets(insets);
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ pub use widget_ref::WidgetRef;
|
|||
pub use zstack::{Alignment, ChildAlignment, HorizontalAlignment, VerticalAlignment, ZStack};
|
||||
|
||||
pub(crate) use widget_arena::WidgetArena;
|
||||
pub(crate) use widget_pod::CreateWidget;
|
||||
pub(crate) use widget_state::WidgetState;
|
||||
|
||||
use crate::{Affine, Size};
|
||||
|
|
|
@ -426,7 +426,7 @@ impl<W: Widget> Widget for Portal<W> {
|
|||
}
|
||||
|
||||
fn compose(&mut self, ctx: &mut ComposeCtx) {
|
||||
ctx.set_child_translation(&mut self.child, Vec2::new(0.0, -self.viewport_pos.y));
|
||||
ctx.set_child_scroll_translation(&mut self.child, Vec2::new(0.0, -self.viewport_pos.y));
|
||||
}
|
||||
|
||||
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut Scene) {}
|
||||
|
|
|
@ -119,14 +119,12 @@ impl ScrollBar {
|
|||
impl Widget for ScrollBar {
|
||||
fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) {
|
||||
match event {
|
||||
PointerEvent::PointerDown(_, state) => {
|
||||
PointerEvent::PointerDown(_, _) => {
|
||||
ctx.capture_pointer();
|
||||
|
||||
let cursor_min_length = theme::SCROLLBAR_MIN_SIZE;
|
||||
let cursor_rect = self.get_cursor_rect(ctx.size(), cursor_min_length);
|
||||
|
||||
let mouse_pos =
|
||||
Point::new(state.position.x, state.position.y) - ctx.window_origin().to_vec2();
|
||||
let mouse_pos = event.local_position(ctx);
|
||||
if cursor_rect.contains(mouse_pos) {
|
||||
let (z0, z1) = self.axis.major_span(cursor_rect);
|
||||
let mouse_major = self.axis.major_pos(mouse_pos);
|
||||
|
@ -139,16 +137,14 @@ impl Widget for ScrollBar {
|
|||
};
|
||||
ctx.request_render();
|
||||
}
|
||||
PointerEvent::PointerMove(state) => {
|
||||
let mouse_pos =
|
||||
Point::new(state.position.x, state.position.y) - ctx.window_origin().to_vec2();
|
||||
PointerEvent::PointerMove(_) => {
|
||||
if let Some(grab_anchor) = self.grab_anchor {
|
||||
let cursor_min_length = theme::SCROLLBAR_MIN_SIZE;
|
||||
self.cursor_progress = self.progress_from_mouse_pos(
|
||||
ctx.size(),
|
||||
cursor_min_length,
|
||||
grab_anchor,
|
||||
mouse_pos,
|
||||
event.local_position(ctx),
|
||||
);
|
||||
self.moved = true;
|
||||
}
|
||||
|
|
|
@ -11,4 +11,5 @@ mod lifecycle_disable;
|
|||
mod lifecycle_focus;
|
||||
mod safety_rails;
|
||||
mod status_change;
|
||||
mod transforms;
|
||||
mod widget_tree;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e5aa3920a321bcd64db228eff1ebe5b8e9186893ef00ad1a10f30ab8c6d1f35
|
||||
size 21630
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:14d51a14e7d9f74fc47f419941f9b36784cf39337e07239ba5c1a80ed6ff6d67
|
||||
size 15905
|
|
@ -72,10 +72,10 @@ fn propagate_hovered() {
|
|||
|
||||
harness.mouse_move_to(empty);
|
||||
|
||||
dbg!(harness.get_widget(button).ctx().window_layout_rect());
|
||||
dbg!(harness.get_widget(pad).ctx().window_layout_rect());
|
||||
dbg!(harness.get_widget(root).ctx().window_layout_rect());
|
||||
dbg!(harness.get_widget(empty).ctx().window_layout_rect());
|
||||
dbg!(harness.get_widget(button).ctx().bounding_rect());
|
||||
dbg!(harness.get_widget(pad).ctx().bounding_rect());
|
||||
dbg!(harness.get_widget(root).ctx().bounding_rect());
|
||||
dbg!(harness.get_widget(empty).ctx().bounding_rect());
|
||||
|
||||
eprintln!("root: {root:?}");
|
||||
eprintln!("empty: {empty:?}");
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2025 the Xilem Authors and the Druid Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Tests related to transforms.
|
||||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use vello::kurbo::{Affine, Vec2};
|
||||
use vello::peniko::color::palette;
|
||||
|
||||
use crate::testing::TestHarness;
|
||||
use crate::widget::{Alignment, Button, ChildAlignment, Label, SizedBox, ZStack};
|
||||
use crate::{assert_render_snapshot, PointerButton, Widget, WidgetPod};
|
||||
|
||||
fn blue_box(inner: impl Widget) -> SizedBox {
|
||||
SizedBox::new(inner)
|
||||
.width(200.)
|
||||
.height(100.)
|
||||
.background(palette::css::BLUE)
|
||||
.border(palette::css::TEAL, 2.)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transforms_translation_rotation() {
|
||||
let translation = Vec2::new(100.0, 50.0);
|
||||
let transformed_widget = WidgetPod::new_with_transform(
|
||||
blue_box(Label::new("Background")),
|
||||
// Currently there's no support for changing the transform-origin, which is currently at the top left.
|
||||
// This rotates around the center of the widget
|
||||
Affine::translate(-translation)
|
||||
.then_rotate(PI * 0.25)
|
||||
.then_translate(translation),
|
||||
)
|
||||
.boxed();
|
||||
let widget = ZStack::new().with_child_pod(transformed_widget, ChildAlignment::ParentAligned);
|
||||
|
||||
let mut harness = TestHarness::create(widget);
|
||||
assert_render_snapshot!(harness, "transforms_translation_rotation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transforms_pointer_events() {
|
||||
let transformed_widget = WidgetPod::new_with_transform(
|
||||
blue_box(
|
||||
ZStack::new().with_child(Button::new("Should be pressed"), Alignment::BottomTrailing),
|
||||
),
|
||||
Affine::rotate(PI * 0.125).then_translate(Vec2::new(100.0, 50.0)),
|
||||
)
|
||||
.boxed();
|
||||
let widget = ZStack::new().with_child_pod(transformed_widget, ChildAlignment::ParentAligned);
|
||||
|
||||
let mut harness = TestHarness::create(widget);
|
||||
harness.mouse_move((335.0, 350.0)); // Should hit the last "d" of the button text
|
||||
harness.mouse_button_press(PointerButton::Primary);
|
||||
assert_render_snapshot!(harness, "transforms_pointer_events");
|
||||
}
|
|
@ -485,15 +485,11 @@ impl<const EDITABLE: bool> Widget for TextArea<EDITABLE> {
|
|||
return;
|
||||
}
|
||||
|
||||
let window_origin = ctx.widget_state.window_origin();
|
||||
let (fctx, lctx) = ctx.text_contexts();
|
||||
let is_rtl = self.editor.layout(fctx, lctx).is_rtl();
|
||||
let inner_origin = Point::new(
|
||||
window_origin.x + self.padding.get_left(is_rtl),
|
||||
window_origin.y + self.padding.top,
|
||||
);
|
||||
let padding = Vec2::new(self.padding.get_left(is_rtl), self.padding.top);
|
||||
match event {
|
||||
PointerEvent::PointerDown(button, state) => {
|
||||
PointerEvent::PointerDown(button, _) => {
|
||||
if !ctx.is_disabled() && *button == PointerButton::Primary {
|
||||
let now = Instant::now();
|
||||
if let Some(last) = self.last_click_time.take() {
|
||||
|
@ -507,7 +503,7 @@ impl<const EDITABLE: bool> Widget for TextArea<EDITABLE> {
|
|||
}
|
||||
self.last_click_time = Some(now);
|
||||
let click_count = self.click_count;
|
||||
let cursor_pos = Point::new(state.position.x, state.position.y) - inner_origin;
|
||||
let cursor_pos = event.local_position(ctx) - padding;
|
||||
let (fctx, lctx) = ctx.text_contexts();
|
||||
let mut drv = self.editor.driver(fctx, lctx);
|
||||
match click_count {
|
||||
|
@ -525,9 +521,9 @@ impl<const EDITABLE: bool> Widget for TextArea<EDITABLE> {
|
|||
ctx.capture_pointer();
|
||||
}
|
||||
}
|
||||
PointerEvent::PointerMove(state) => {
|
||||
PointerEvent::PointerMove(_) => {
|
||||
if !ctx.is_disabled() && ctx.has_pointer_capture() {
|
||||
let cursor_pos = Point::new(state.position.x, state.position.y) - inner_origin;
|
||||
let cursor_pos = event.local_position(ctx) - padding;
|
||||
let (fctx, lctx) = ctx.text_contexts();
|
||||
self.editor
|
||||
.driver(fctx, lctx)
|
||||
|
|
|
@ -51,6 +51,23 @@ impl WidgetId {
|
|||
}
|
||||
}
|
||||
|
||||
/// A trait to access a `Widget` as trait object. It is implemented for all types that implement `Widget`.
|
||||
pub trait AsDynWidget {
|
||||
fn as_dyn(&self) -> &dyn Widget;
|
||||
fn as_mut_dyn(&mut self) -> &mut dyn Widget;
|
||||
}
|
||||
|
||||
impl<T: Widget> AsDynWidget for T {
|
||||
fn as_dyn(&self) -> &dyn Widget {
|
||||
self as &dyn Widget
|
||||
}
|
||||
|
||||
fn as_mut_dyn(&mut self) -> &mut dyn Widget {
|
||||
self as &mut dyn Widget
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - Add tutorial: implementing a widget - See https://github.com/linebender/xilem/issues/376
|
||||
/// The trait implemented by all widgets.
|
||||
///
|
||||
/// For details on how to implement this trait, see the [tutorials](crate::doc).
|
||||
|
@ -76,7 +93,7 @@ impl WidgetId {
|
|||
/// through [`WidgetPod`](crate::WidgetPod)s. Widget methods are called by Masonry, and a
|
||||
/// widget should only be mutated either during a method call or through a [`WidgetMut`](crate::widget::WidgetMut).
|
||||
#[allow(unused_variables)]
|
||||
pub trait Widget: AsAny {
|
||||
pub trait Widget: AsAny + AsDynWidget {
|
||||
/// Handle a pointer event.
|
||||
///
|
||||
/// Pointer events will target the widget under the pointer, and then the
|
||||
|
@ -255,23 +272,28 @@ pub trait Widget: AsAny {
|
|||
|
||||
// --- Auto-generated implementations ---
|
||||
|
||||
/// Return which child, if any, has the given `pos` in its layout rect. In case of overlapping
|
||||
/// children, the last child as determined by [`Widget::children_ids`] is chosen. No child is
|
||||
/// returned if `pos` is outside the widget's clip path.
|
||||
/// Return the first innermost widget composed by this (including `self`), that contains/intersects with `pos` and accepts pointer interaction, if any.
|
||||
///
|
||||
/// The child returned is a direct child, not e.g. a grand-child.
|
||||
/// In case of overlapping children, the last child as determined by [`Widget::children_ids`] is chosen. No widget is
|
||||
/// returned if `pos` is outside the widget's clip path.
|
||||
///
|
||||
/// Has a default implementation that can be overridden to search children more efficiently.
|
||||
/// Custom implementations must uphold the conditions outlined above.
|
||||
///
|
||||
/// **pos** - the position in global coordinates (e.g. `(0,0)` is the top-left corner of the
|
||||
/// window).
|
||||
fn get_child_at_pos<'c>(
|
||||
&self,
|
||||
fn find_widget_at_pos<'c>(
|
||||
&'c self,
|
||||
ctx: QueryCtx<'c>,
|
||||
pos: Point,
|
||||
) -> Option<WidgetRef<'c, dyn Widget>> {
|
||||
get_child_at_pos(self, ctx, pos)
|
||||
find_widget_at_pos(
|
||||
&WidgetRef {
|
||||
widget: self.as_dyn(),
|
||||
ctx,
|
||||
},
|
||||
pos,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the (verbose) type name of the widget for debugging purposes.
|
||||
|
@ -313,35 +335,38 @@ pub trait Widget: AsAny {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_child_at_pos<'c>(
|
||||
widget: &(impl Widget + ?Sized),
|
||||
ctx: QueryCtx<'c>,
|
||||
/// See [`Widget::find_widget_at_pos`] for more details.
|
||||
pub fn find_widget_at_pos<'c>(
|
||||
widget: &WidgetRef<'c, dyn Widget>,
|
||||
pos: Point,
|
||||
) -> Option<WidgetRef<'c, dyn Widget>> {
|
||||
let relative_pos = pos - ctx.window_origin().to_vec2();
|
||||
if !ctx
|
||||
.clip_path()
|
||||
.is_none_or(|clip| clip.contains(relative_pos))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if widget.ctx.widget_state.bounding_rect.contains(pos) {
|
||||
let local_pos = widget.ctx().widget_state.window_transform.inverse() * pos;
|
||||
|
||||
// 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 = ctx.get(*child_id);
|
||||
|
||||
// The position must be inside the child's layout and inside the child's clip path (if
|
||||
// any).
|
||||
if !child.ctx().is_stashed()
|
||||
&& child.ctx().accepts_pointer_interaction()
|
||||
&& child.ctx().window_layout_rect().contains(pos)
|
||||
if widget.ctx.is_stashed()
|
||||
|| Some(false) == widget.ctx.clip_path().map(|clip| clip.contains(local_pos))
|
||||
{
|
||||
return Some(child);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
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.accepts_pointer_interaction()
|
||||
&& widget.ctx.size().to_rect().contains(local_pos)
|
||||
{
|
||||
Some(*widget)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker trait for Widgets whose parents can get a raw mutable reference to them.
|
||||
|
@ -502,12 +527,12 @@ impl Widget for Box<dyn Widget> {
|
|||
self.deref().get_cursor(ctx, pos)
|
||||
}
|
||||
|
||||
fn get_child_at_pos<'c>(
|
||||
&self,
|
||||
fn find_widget_at_pos<'c>(
|
||||
&'c self,
|
||||
ctx: QueryCtx<'c>,
|
||||
pos: Point,
|
||||
) -> Option<WidgetRef<'c, dyn Widget>> {
|
||||
self.deref().get_child_at_pos(ctx, pos)
|
||||
self.deref().find_widget_at_pos(ctx, pos)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contexts::MutateCtx;
|
||||
use crate::Widget;
|
||||
use crate::{Affine, Widget};
|
||||
|
||||
// TODO - Document extension trait workaround.
|
||||
// See https://xi.zulipchat.com/#narrow/stream/317477-masonry/topic/Thoughts.20on.20simplifying.20WidgetMut/near/436478885
|
||||
|
@ -47,6 +47,13 @@ impl<W: Widget> WidgetMut<'_, W> {
|
|||
widget,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the local transform of this widget.
|
||||
///
|
||||
/// It behaves similarly as CSS transforms.
|
||||
pub fn set_transform(&mut self, transform: Affine) {
|
||||
self.ctx.set_transform(transform);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetMut<'_, Box<dyn Widget>> {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2018 the Xilem Authors and the Druid Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{Widget, WidgetId};
|
||||
use crate::{Affine, Widget, WidgetId};
|
||||
|
||||
/// A container for one widget in the hierarchy.
|
||||
///
|
||||
|
@ -20,8 +20,13 @@ pub struct WidgetPod<W> {
|
|||
// through context methods where they already have access to the arena.
|
||||
// Implementing that requires solving non-trivial design questions.
|
||||
|
||||
pub(crate) struct CreateWidget<W> {
|
||||
pub(crate) widget: W,
|
||||
pub(crate) transform: Affine,
|
||||
}
|
||||
|
||||
enum WidgetPodInner<W> {
|
||||
Created(W),
|
||||
Create(CreateWidget<W>),
|
||||
Inserted,
|
||||
}
|
||||
|
||||
|
@ -37,19 +42,31 @@ impl<W: Widget> WidgetPod<W> {
|
|||
|
||||
/// Create a new widget pod with fixed id.
|
||||
pub fn new_with_id(inner: W, id: WidgetId) -> Self {
|
||||
Self::new_with_id_and_transform(inner, id, Affine::IDENTITY)
|
||||
}
|
||||
|
||||
/// Create a new widget pod with a custom transform.
|
||||
pub fn new_with_transform(inner: W, transform: Affine) -> Self {
|
||||
Self::new_with_id_and_transform(inner, WidgetId::next(), transform)
|
||||
}
|
||||
|
||||
pub fn new_with_id_and_transform(inner: W, id: WidgetId, transform: Affine) -> Self {
|
||||
Self {
|
||||
id,
|
||||
inner: WidgetPodInner::Created(inner),
|
||||
inner: WidgetPodInner::Create(CreateWidget {
|
||||
widget: inner,
|
||||
transform,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn incomplete(&self) -> bool {
|
||||
matches!(self.inner, WidgetPodInner::Created(_))
|
||||
matches!(self.inner, WidgetPodInner::Create(_))
|
||||
}
|
||||
|
||||
pub(crate) fn take_inner(&mut self) -> Option<W> {
|
||||
pub(crate) fn take_inner(&mut self) -> Option<CreateWidget<W>> {
|
||||
match std::mem::replace(&mut self.inner, WidgetPodInner::Inserted) {
|
||||
WidgetPodInner::Created(widget) => Some(widget),
|
||||
WidgetPodInner::Create(widget) => Some(widget),
|
||||
WidgetPodInner::Inserted => None,
|
||||
}
|
||||
}
|
||||
|
@ -66,11 +83,9 @@ impl<W: Widget + 'static> WidgetPod<W> {
|
|||
/// Convert a `WidgetPod` containing a widget of a specific concrete type
|
||||
/// into a dynamically boxed widget.
|
||||
pub fn boxed(self) -> WidgetPod<Box<dyn Widget>> {
|
||||
match self.inner {
|
||||
WidgetPodInner::Created(inner) => WidgetPod::new_with_id(Box::new(inner), self.id),
|
||||
WidgetPodInner::Inserted => {
|
||||
panic!("Cannot box a widget after it has been inserted into the widget graph")
|
||||
}
|
||||
}
|
||||
let WidgetPodInner::Create(inner) = self.inner else {
|
||||
panic!("Cannot box a widget after it has been inserted into the widget graph")
|
||||
};
|
||||
WidgetPod::new_with_id_and_transform(Box::new(inner.widget), self.id, inner.transform)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,29 +159,13 @@ impl WidgetRef<'_, dyn Widget> {
|
|||
}
|
||||
|
||||
/// Recursively find the innermost widget at the given position, using
|
||||
/// [`Widget::get_child_at_pos`] to descend the widget tree. If `self` does not contain the
|
||||
/// [`Widget::find_widget_at_pos`] to descend the widget tree. If `self` does not contain the
|
||||
/// given position in its layout rect or clip path, this returns `None`.
|
||||
///
|
||||
/// **pos** - the position in global coordinates (e.g. `(0,0)` is the top-left corner of the
|
||||
/// window).
|
||||
pub fn find_widget_at_pos(&self, pos: Point) -> Option<Self> {
|
||||
let mut innermost_widget = *self;
|
||||
|
||||
if !self.ctx.window_layout_rect().contains(pos) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// TODO: add debug assertion to check whether the child returned by
|
||||
// `Widget::get_child_at_pos` upholds the conditions of that method. See
|
||||
// https://github.com/linebender/xilem/pull/565#discussion_r1756536870
|
||||
while let Some(child) = innermost_widget
|
||||
.widget
|
||||
.get_child_at_pos(innermost_widget.ctx, pos)
|
||||
{
|
||||
innermost_widget = child;
|
||||
}
|
||||
|
||||
Some(innermost_widget)
|
||||
self.widget.find_widget_at_pos(self.ctx, pos)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2018 the Xilem Authors and the Druid Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use vello::kurbo::{Insets, Point, Rect, Size, Vec2};
|
||||
use vello::kurbo::{Affine, Insets, Point, Rect, Size, Vec2};
|
||||
|
||||
use crate::WidgetId;
|
||||
|
||||
|
@ -58,11 +58,9 @@ pub(crate) struct WidgetState {
|
|||
/// The size of the widget; this is the value returned by the widget's layout
|
||||
/// method.
|
||||
pub(crate) size: Size,
|
||||
/// The origin of the widget in the parent's coordinate space; together with
|
||||
/// The origin of the widget in the `window_transform` coordinate space; together with
|
||||
/// `size` these constitute the widget's layout rect.
|
||||
pub(crate) origin: Point,
|
||||
/// The origin of the widget in the window coordinate space;
|
||||
pub(crate) window_origin: Point,
|
||||
/// The insets applied to the layout rect to generate the paint rect.
|
||||
/// In general, these will be zero; the exception is for things like
|
||||
/// drop shadows or overflowing text.
|
||||
|
@ -70,6 +68,8 @@ pub(crate) struct WidgetState {
|
|||
// TODO - Document
|
||||
// The computed paint rect, in local coordinates.
|
||||
pub(crate) local_paint_rect: Rect,
|
||||
/// An axis aligned bounding rect (AABB in 2D), containing itself and all its descendents in window coordinates. Includes `paint_insets`.
|
||||
pub(crate) bounding_rect: Rect,
|
||||
/// The offset of the baseline relative to the bottom of the widget.
|
||||
///
|
||||
/// In general, this will be zero; the bottom of the widget will be considered
|
||||
|
@ -96,9 +96,14 @@ pub(crate) struct WidgetState {
|
|||
// efficiently hold an arbitrary shape.
|
||||
pub(crate) clip_path: Option<Rect>,
|
||||
|
||||
// TODO - Handle matrix transforms
|
||||
pub(crate) translation: Vec2,
|
||||
pub(crate) translation_changed: bool,
|
||||
/// This is being computed out of all ancestor transforms and `translation`
|
||||
pub(crate) window_transform: Affine,
|
||||
/// Local transform of this widget in the parent coordinate space.
|
||||
pub(crate) transform: Affine,
|
||||
/// translation applied by scrolling, this is applied after applying `transform` to this widget.
|
||||
pub(crate) scroll_translation: Vec2,
|
||||
/// The `transform` or `scroll_translation` has changed.
|
||||
pub(crate) transform_changed: bool,
|
||||
|
||||
// --- PASSES ---
|
||||
/// `WidgetAdded` hasn't been sent to this widget yet.
|
||||
|
@ -168,11 +173,10 @@ pub(crate) struct WidgetState {
|
|||
}
|
||||
|
||||
impl WidgetState {
|
||||
pub(crate) fn new(id: WidgetId, widget_name: &'static str) -> Self {
|
||||
pub(crate) fn new(id: WidgetId, widget_name: &'static str, transform: Affine) -> Self {
|
||||
Self {
|
||||
id,
|
||||
origin: Point::ORIGIN,
|
||||
window_origin: Point::ORIGIN,
|
||||
size: Size::ZERO,
|
||||
is_expecting_place_child_call: false,
|
||||
paint_insets: Insets::ZERO,
|
||||
|
@ -182,8 +186,8 @@ impl WidgetState {
|
|||
accepts_text_input: false,
|
||||
ime_area: None,
|
||||
clip_path: Default::default(),
|
||||
translation: Vec2::ZERO,
|
||||
translation_changed: false,
|
||||
scroll_translation: Vec2::ZERO,
|
||||
transform_changed: false,
|
||||
is_explicitly_disabled: false,
|
||||
is_explicitly_stashed: false,
|
||||
is_disabled: false,
|
||||
|
@ -209,6 +213,9 @@ impl WidgetState {
|
|||
needs_update_focus_chain: true,
|
||||
#[cfg(debug_assertions)]
|
||||
widget_name,
|
||||
window_transform: Affine::IDENTITY,
|
||||
bounding_rect: Rect::ZERO,
|
||||
transform,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,7 +239,7 @@ impl WidgetState {
|
|||
needs_update_stashed: false,
|
||||
children_changed: false,
|
||||
needs_update_focus_chain: false,
|
||||
..Self::new(id, "<root>")
|
||||
..Self::new(id, "<root>", Affine::IDENTITY)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,23 +274,24 @@ impl WidgetState {
|
|||
Rect::from_origin_size(self.origin, self.size)
|
||||
}
|
||||
|
||||
/// The [`layout_rect`](Self::layout_rect) in window coordinates.
|
||||
/// The axis aligned bounding rect of this widget in window coordinates. Includes `paint_insets`.
|
||||
///
|
||||
/// This might not map to a visible area of the screen, eg if the widget is scrolled
|
||||
/// away.
|
||||
pub fn window_layout_rect(&self) -> Rect {
|
||||
Rect::from_origin_size(self.window_origin(), self.size)
|
||||
pub fn bounding_rect(&self) -> Rect {
|
||||
self.bounding_rect
|
||||
}
|
||||
|
||||
/// Returns the area being edited by an IME, in global coordinates.
|
||||
///
|
||||
/// By default, returns the same as [`Self::window_layout_rect`].
|
||||
/// By default, returns the same as [`Self::bounding_rect`].
|
||||
pub(crate) fn get_ime_area(&self) -> Rect {
|
||||
self.ime_area.unwrap_or_else(|| self.size.to_rect()) + self.window_origin.to_vec2()
|
||||
self.window_transform
|
||||
.transform_rect_bbox(self.ime_area.unwrap_or_else(|| self.size.to_rect()))
|
||||
}
|
||||
|
||||
pub(crate) fn window_origin(&self) -> Point {
|
||||
self.window_origin
|
||||
self.window_transform.translation().to_point()
|
||||
}
|
||||
|
||||
pub(crate) fn needs_rewrite_passes(&self) -> bool {
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright 2024 the Xilem Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! The transform for all views can be modified similar to CSS transforms.
|
||||
|
||||
use std::f64::consts::{PI, TAU};
|
||||
|
||||
use winit::error::EventLoopError;
|
||||
use xilem::view::{button, grid, label, sized_box, GridExt as _, Transformable as _};
|
||||
use xilem::{Color, EventLoop, Vec2, WidgetView, Xilem};
|
||||
|
||||
struct TransformsGame {
|
||||
rotation: f64,
|
||||
translation: Vec2,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl TransformsGame {
|
||||
fn view(&mut self) -> impl WidgetView<Self> {
|
||||
let rotation_correct = (self.rotation % TAU).abs() < 0.001;
|
||||
let scale_correct = self.scale >= 0.99 && self.scale <= 1.01;
|
||||
let translation_correct = self.translation.x == 0.0 && self.translation.y == 0.0;
|
||||
let everything_correct = rotation_correct && scale_correct && translation_correct;
|
||||
|
||||
let status = if everything_correct {
|
||||
label("Great success!")
|
||||
.brush(Color::new([0.0, 0.0, 1.0, 1.0]))
|
||||
.text_size(30.0)
|
||||
} else {
|
||||
let rotation_mark = if rotation_correct { "✓" } else { "⨯" };
|
||||
let scale_mark = if scale_correct { "✓" } else { "⨯" };
|
||||
let translation_mark = if translation_correct { "✓" } else { "⨯" };
|
||||
label(format!(
|
||||
"rotation: {rotation_mark}\nscale: {scale_mark}\ntranslation: {translation_mark}"
|
||||
))
|
||||
};
|
||||
|
||||
let bg_color = if everything_correct {
|
||||
[0.0, 1.0, 0.0, 1.0]
|
||||
} else {
|
||||
[1.0, 0.0, 0.0, 0.2]
|
||||
};
|
||||
|
||||
// Every view can be transformed similar as with CSS transforms in the web.
|
||||
// Currently only 2D transforms are supported.
|
||||
// Note that the order of the transformations is relevant.
|
||||
let transformed_status = sized_box(status)
|
||||
.background(Color::new(bg_color))
|
||||
.translate(self.translation)
|
||||
.rotate(self.rotation)
|
||||
.scale(self.scale);
|
||||
|
||||
let controls = (
|
||||
button("↶", |this: &mut Self| {
|
||||
this.rotation -= PI * 0.125;
|
||||
})
|
||||
.grid_pos(0, 0),
|
||||
button("↑", |this: &mut Self| {
|
||||
this.translation.y -= 10.0;
|
||||
})
|
||||
.grid_pos(1, 0),
|
||||
button("↷", |this: &mut Self| {
|
||||
this.rotation += PI * 0.125;
|
||||
})
|
||||
.grid_pos(2, 0),
|
||||
button("←", |this: &mut Self| {
|
||||
this.translation.x -= 10.0;
|
||||
})
|
||||
.grid_pos(0, 1),
|
||||
button("→", |this: &mut Self| {
|
||||
this.translation.x += 10.0;
|
||||
})
|
||||
.grid_pos(2, 1),
|
||||
button("-", |this: &mut Self| {
|
||||
// 2 ^ (1/3) for 3 clicks to reach the target.
|
||||
this.scale /= 1.2599210498948732;
|
||||
})
|
||||
.grid_pos(0, 2),
|
||||
button("↓", |this: &mut Self| {
|
||||
this.translation.y += 10.0;
|
||||
})
|
||||
.grid_pos(1, 2),
|
||||
button("+", |this: &mut Self| {
|
||||
this.scale *= 1.2599210498948732;
|
||||
})
|
||||
.grid_pos(2, 2),
|
||||
);
|
||||
|
||||
grid((controls, transformed_status.grid_pos(1, 1)), 3, 3)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), EventLoopError> {
|
||||
let app = Xilem::new(
|
||||
TransformsGame {
|
||||
rotation: PI * 0.25,
|
||||
translation: Vec2::new(20.0, 30.0),
|
||||
scale: 2.0,
|
||||
},
|
||||
TransformsGame::view,
|
||||
);
|
||||
app.run_windowed(EventLoop::with_user_event(), "Transforms".into())?;
|
||||
Ok(())
|
||||
}
|
|
@ -53,7 +53,7 @@ use crate::core::{
|
|||
ViewPathTracker, ViewSequence,
|
||||
};
|
||||
pub use masonry::event_loop_runner::{EventLoop, EventLoopBuilder};
|
||||
pub use masonry::{dpi, palette, Color, FontWeight, TextAlignment};
|
||||
pub use masonry::{dpi, palette, Affine, Color, FontWeight, TextAlignment, Vec2};
|
||||
pub use xilem_core as core;
|
||||
|
||||
/// Tokio is the async runner used with Xilem.
|
||||
|
@ -292,6 +292,12 @@ impl ViewCtx {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn new_pod_with_transform<W: Widget>(&mut self, widget: W, transform: Affine) -> Pod<W> {
|
||||
Pod {
|
||||
inner: WidgetPod::new_with_transform(widget, transform),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn boxed_pod<W: Widget>(&mut self, pod: Pod<W>) -> Pod<Box<dyn Widget>> {
|
||||
Pod {
|
||||
inner: pod.inner.boxed(),
|
||||
|
|
|
@ -13,7 +13,8 @@ use vello::Scene;
|
|||
|
||||
use crate::core::one_of::OneOf;
|
||||
use crate::core::Mut;
|
||||
use crate::{Pod, ViewCtx};
|
||||
use crate::view::Transformable;
|
||||
use crate::{Affine, Pod, ViewCtx};
|
||||
|
||||
impl<
|
||||
A: Widget,
|
||||
|
@ -142,6 +143,33 @@ impl<
|
|||
}
|
||||
}
|
||||
|
||||
impl<A, B, C, D, E, F, G, H, I> Transformable for OneOf<A, B, C, D, E, F, G, H, I>
|
||||
where
|
||||
A: Transformable,
|
||||
B: Transformable,
|
||||
C: Transformable,
|
||||
D: Transformable,
|
||||
E: Transformable,
|
||||
F: Transformable,
|
||||
G: Transformable,
|
||||
H: Transformable,
|
||||
I: Transformable,
|
||||
{
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
match self {
|
||||
Self::A(w) => w.transform_mut(),
|
||||
Self::B(w) => w.transform_mut(),
|
||||
Self::C(w) => w.transform_mut(),
|
||||
Self::D(w) => w.transform_mut(),
|
||||
Self::E(w) => w.transform_mut(),
|
||||
Self::F(w) => w.transform_mut(),
|
||||
Self::G(w) => w.transform_mut(),
|
||||
Self::H(w) => w.transform_mut(),
|
||||
Self::I(w) => w.transform_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::core::one_of::PhantomElementCtx for ViewCtx {
|
||||
type PhantomElement = Pod<Box<dyn Widget>>;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ pub use masonry::PointerButton;
|
|||
|
||||
use crate::core::{DynMessage, Mut, View, ViewMarker};
|
||||
use crate::view::Label;
|
||||
use crate::{MessageResult, Pod, ViewCtx, ViewId};
|
||||
use crate::{Affine, MessageResult, Pod, ViewCtx, ViewId};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
/// A button which calls `callback` when the primary mouse button (normally left) is pressed.
|
||||
pub fn button<State, Action>(
|
||||
|
@ -17,6 +19,7 @@ pub fn button<State, Action>(
|
|||
{
|
||||
Button {
|
||||
label: label.into(),
|
||||
transform: Affine::IDENTITY,
|
||||
callback: move |state: &mut State, button| match button {
|
||||
PointerButton::Primary => MessageResult::Action(callback(state)),
|
||||
_ => MessageResult::Nop,
|
||||
|
@ -32,6 +35,7 @@ pub fn button_any_pointer<State, Action>(
|
|||
{
|
||||
Button {
|
||||
label: label.into(),
|
||||
transform: Affine::IDENTITY,
|
||||
callback: move |state: &mut State, button| MessageResult::Action(callback(state, button)),
|
||||
}
|
||||
}
|
||||
|
@ -39,9 +43,16 @@ pub fn button_any_pointer<State, Action>(
|
|||
#[must_use = "View values do nothing unless provided to Xilem."]
|
||||
pub struct Button<F> {
|
||||
label: Label,
|
||||
transform: Affine,
|
||||
callback: F,
|
||||
}
|
||||
|
||||
impl<F> Transformable for Button<F> {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> ViewMarker for Button<F> {}
|
||||
impl<F, State, Action> View<State, Action, ViewCtx> for Button<F>
|
||||
where
|
||||
|
@ -52,15 +63,18 @@ where
|
|||
|
||||
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||
ctx.with_leaf_action_widget(|ctx| {
|
||||
ctx.new_pod(widget::Button::from_label(
|
||||
// TODO: Use `Label::build` here - currently impossible because `Pod` uses `WidgetPod` internally
|
||||
widget::Label::new(self.label.label.clone())
|
||||
.with_brush(self.label.text_brush.clone())
|
||||
.with_alignment(self.label.alignment)
|
||||
.with_style(StyleProperty::FontSize(self.label.text_size))
|
||||
.with_style(StyleProperty::FontWeight(self.label.weight))
|
||||
.with_style(StyleProperty::FontStack(self.label.font.clone())),
|
||||
))
|
||||
ctx.new_pod_with_transform(
|
||||
widget::Button::from_label(
|
||||
// TODO: Use `Label::build` here - currently impossible because `Pod` uses `WidgetPod` internally
|
||||
widget::Label::new(self.label.label.clone())
|
||||
.with_brush(self.label.text_brush.clone())
|
||||
.with_alignment(self.label.alignment)
|
||||
.with_style(StyleProperty::FontSize(self.label.text_size))
|
||||
.with_style(StyleProperty::FontWeight(self.label.weight))
|
||||
.with_style(StyleProperty::FontStack(self.label.font.clone())),
|
||||
),
|
||||
self.transform,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -71,6 +85,10 @@ where
|
|||
ctx: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
|
||||
<Label as View<State, Action, ViewCtx>>::rebuild(
|
||||
&self.label,
|
||||
&prev.label,
|
||||
|
|
|
@ -5,7 +5,9 @@ use masonry::text::ArcStr;
|
|||
use masonry::widget;
|
||||
|
||||
use crate::core::{DynMessage, Mut, ViewMarker};
|
||||
use crate::{MessageResult, Pod, View, ViewCtx, ViewId};
|
||||
use crate::{Affine, MessageResult, Pod, View, ViewCtx, ViewId};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
pub fn checkbox<F, State, Action>(
|
||||
label: impl Into<ArcStr>,
|
||||
|
@ -19,6 +21,7 @@ where
|
|||
label: label.into(),
|
||||
callback,
|
||||
checked,
|
||||
transform: Affine::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,6 +30,13 @@ pub struct Checkbox<F> {
|
|||
label: ArcStr,
|
||||
checked: bool,
|
||||
callback: F,
|
||||
transform: Affine,
|
||||
}
|
||||
|
||||
impl<F> Transformable for Checkbox<F> {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> ViewMarker for Checkbox<F> {}
|
||||
|
@ -39,7 +49,10 @@ where
|
|||
|
||||
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||
ctx.with_leaf_action_widget(|ctx| {
|
||||
ctx.new_pod(widget::Checkbox::new(self.checked, self.label.clone()))
|
||||
ctx.new_pod_with_transform(
|
||||
widget::Checkbox::new(self.checked, self.label.clone()),
|
||||
self.transform,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -50,6 +63,9 @@ where
|
|||
_ctx: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
if prev.label != self.label {
|
||||
widget::Checkbox::set_text(&mut element, self.label.clone());
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::core::{
|
|||
AppendVec, DynMessage, ElementSplice, MessageResult, Mut, SuperElement, View, ViewElement,
|
||||
ViewId, ViewMarker, ViewPathTracker, ViewSequence,
|
||||
};
|
||||
use crate::{AnyWidgetView, Pod, ViewCtx, WidgetView};
|
||||
use crate::{Affine, AnyWidgetView, Pod, ViewCtx, WidgetView};
|
||||
|
||||
pub fn flex<State, Action, Seq: FlexSequence<State, Action>>(
|
||||
sequence: Seq,
|
||||
|
@ -19,6 +19,7 @@ pub fn flex<State, Action, Seq: FlexSequence<State, Action>>(
|
|||
Flex {
|
||||
sequence,
|
||||
axis: Axis::Vertical,
|
||||
transform: Affine::IDENTITY,
|
||||
cross_axis_alignment: CrossAxisAlignment::Center,
|
||||
main_axis_alignment: MainAxisAlignment::Start,
|
||||
fill_major_axis: false,
|
||||
|
@ -31,6 +32,7 @@ pub fn flex<State, Action, Seq: FlexSequence<State, Action>>(
|
|||
pub struct Flex<Seq, State, Action = ()> {
|
||||
sequence: Seq,
|
||||
axis: Axis,
|
||||
transform: Affine,
|
||||
cross_axis_alignment: CrossAxisAlignment,
|
||||
main_axis_alignment: MainAxisAlignment,
|
||||
fill_major_axis: bool,
|
||||
|
@ -85,6 +87,12 @@ impl<Seq, State, Action> Flex<Seq, State, Action> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<Seq, State, Action> Transformable for Flex<Seq, State, Action> {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl<Seq, State, Action> ViewMarker for Flex<Seq, State, Action> {}
|
||||
impl<State, Action, Seq> View<State, Action, ViewCtx> for Flex<Seq, State, Action>
|
||||
where
|
||||
|
@ -113,7 +121,8 @@ where
|
|||
FlexElement::FlexSpacer(flex) => widget.with_flex_spacer(flex),
|
||||
}
|
||||
}
|
||||
(ctx.new_pod(widget), seq_state)
|
||||
let pod = ctx.new_pod_with_transform(widget, self.transform);
|
||||
(pod, seq_state)
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
|
@ -123,6 +132,9 @@ where
|
|||
ctx: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
if prev.axis != self.axis {
|
||||
widget::Flex::set_direction(&mut element, self.axis);
|
||||
}
|
||||
|
@ -634,6 +646,8 @@ mod hidden {
|
|||
}
|
||||
use hidden::AnyFlexChildState;
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
impl<State, Action> ViewMarker for AnyFlexChild<State, Action> {}
|
||||
impl<State, Action> View<State, Action, ViewCtx> for AnyFlexChild<State, Action>
|
||||
where
|
||||
|
|
|
@ -10,7 +10,9 @@ use crate::core::{
|
|||
AppendVec, DynMessage, ElementSplice, MessageResult, Mut, SuperElement, View, ViewElement,
|
||||
ViewId, ViewMarker, ViewSequence,
|
||||
};
|
||||
use crate::{Pod, ViewCtx, WidgetView};
|
||||
use crate::{Affine, Pod, ViewCtx, WidgetView};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
pub fn grid<State, Action, Seq: GridSequence<State, Action>>(
|
||||
sequence: Seq,
|
||||
|
@ -23,6 +25,7 @@ pub fn grid<State, Action, Seq: GridSequence<State, Action>>(
|
|||
phantom: PhantomData,
|
||||
height,
|
||||
width,
|
||||
transform: Affine::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +35,7 @@ pub struct Grid<Seq, State, Action = ()> {
|
|||
spacing: f64,
|
||||
width: i32,
|
||||
height: i32,
|
||||
transform: Affine,
|
||||
/// Used to associate the State and Action in the call to `.grid()` with the State and Action
|
||||
/// used in the View implementation, to allow inference to flow backwards, allowing State and
|
||||
/// Action to be inferred properly
|
||||
|
@ -50,6 +54,12 @@ impl<Seq, State, Action> Grid<Seq, State, Action> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<Seq, State, Action> Transformable for Grid<Seq, State, Action> {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl<Seq, State, Action> ViewMarker for Grid<Seq, State, Action> {}
|
||||
|
||||
impl<State, Action, Seq> View<State, Action, ViewCtx> for Grid<Seq, State, Action>
|
||||
|
@ -72,7 +82,8 @@ where
|
|||
GridElement::Child(child, params) => widget.with_child_pod(child.inner, params),
|
||||
}
|
||||
}
|
||||
(ctx.new_pod(widget), seq_state)
|
||||
let pod = ctx.new_pod_with_transform(widget, self.transform);
|
||||
(pod, seq_state)
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
|
@ -82,6 +93,9 @@ where
|
|||
ctx: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
if prev.height != self.height {
|
||||
widget::Grid::set_height(&mut element, self.height);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
use masonry::widget::{self, ObjectFit};
|
||||
|
||||
use crate::core::{DynMessage, Mut, ViewMarker};
|
||||
use crate::{MessageResult, Pod, View, ViewCtx, ViewId};
|
||||
use crate::{Affine, MessageResult, Pod, View, ViewCtx, ViewId};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
/// Displays the bitmap `image`.
|
||||
///
|
||||
|
@ -25,6 +27,7 @@ pub fn image(image: &vello::peniko::Image) -> Image {
|
|||
// easier than documenting that cloning is cheap.
|
||||
image: image.clone(),
|
||||
object_fit: ObjectFit::default(),
|
||||
transform: Affine::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,6 +38,7 @@ pub fn image(image: &vello::peniko::Image) -> Image {
|
|||
pub struct Image {
|
||||
image: vello::peniko::Image,
|
||||
object_fit: ObjectFit,
|
||||
transform: Affine,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
|
@ -45,13 +49,21 @@ impl Image {
|
|||
}
|
||||
}
|
||||
|
||||
impl Transformable for Image {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewMarker for Image {}
|
||||
impl<State, Action> View<State, Action, ViewCtx> for Image {
|
||||
type Element = Pod<widget::Image>;
|
||||
type ViewState = ();
|
||||
|
||||
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||
(ctx.new_pod(widget::Image::new(self.image.clone())), ())
|
||||
let pod =
|
||||
ctx.new_pod_with_transform(widget::Image::new(self.image.clone()), self.transform);
|
||||
(pod, ())
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
|
@ -61,6 +73,9 @@ impl<State, Action> View<State, Action, ViewCtx> for Image {
|
|||
_: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
if prev.object_fit != self.object_fit {
|
||||
widget::Image::set_fit_mode(&mut element, self.object_fit);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ use masonry::widget;
|
|||
use vello::peniko::Brush;
|
||||
|
||||
use crate::core::{DynMessage, Mut, ViewMarker};
|
||||
use crate::{Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId};
|
||||
use crate::{Affine, Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
pub fn label(label: impl Into<ArcStr>) -> Label {
|
||||
Label {
|
||||
|
@ -17,6 +19,7 @@ pub fn label(label: impl Into<ArcStr>) -> Label {
|
|||
text_size: masonry::theme::TEXT_SIZE_NORMAL,
|
||||
weight: FontWeight::NORMAL,
|
||||
font: FontStack::List(std::borrow::Cow::Borrowed(&[])),
|
||||
transform: Affine::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +32,7 @@ pub struct Label {
|
|||
pub(in crate::view) text_size: f32,
|
||||
pub(in crate::view) weight: FontWeight,
|
||||
pub(in crate::view) font: FontStack<'static>, // TODO: add more attributes of `masonry::widget::Label`
|
||||
pub(in crate::view) transform: Affine,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
|
@ -64,6 +68,12 @@ impl Label {
|
|||
}
|
||||
}
|
||||
|
||||
impl Transformable for Label {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Label
|
||||
where
|
||||
T: Into<ArcStr>,
|
||||
|
@ -79,13 +89,14 @@ impl<State, Action> View<State, Action, ViewCtx> for Label {
|
|||
type ViewState = ();
|
||||
|
||||
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||
let widget_pod = ctx.new_pod(
|
||||
let widget_pod = ctx.new_pod_with_transform(
|
||||
widget::Label::new(self.label.clone())
|
||||
.with_brush(self.text_brush.clone())
|
||||
.with_alignment(self.alignment)
|
||||
.with_style(StyleProperty::FontSize(self.text_size))
|
||||
.with_style(StyleProperty::FontWeight(self.weight))
|
||||
.with_style(StyleProperty::FontStack(self.font.clone())),
|
||||
self.transform,
|
||||
);
|
||||
(widget_pod, ())
|
||||
}
|
||||
|
@ -97,6 +108,9 @@ impl<State, Action> View<State, Action, ViewCtx> for Label {
|
|||
_ctx: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
if prev.label != self.label {
|
||||
widget::Label::set_text(&mut element, self.label.clone());
|
||||
}
|
||||
|
|
|
@ -50,3 +50,42 @@ pub use portal::*;
|
|||
|
||||
mod zstack;
|
||||
pub use zstack::*;
|
||||
|
||||
/// An extension trait, to allow common transformations of the views transform.
|
||||
pub trait Transformable: Sized {
|
||||
fn transform_mut(&mut self) -> &mut crate::Affine;
|
||||
|
||||
#[must_use]
|
||||
fn rotate(mut self, radians: f64) -> Self {
|
||||
let transform = self.transform_mut();
|
||||
*transform = transform.then_rotate(radians);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn scale(mut self, uniform: f64) -> Self {
|
||||
let transform = self.transform_mut();
|
||||
*transform = transform.then_scale(uniform);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn scale_non_uniform(mut self, x: f64, y: f64) -> Self {
|
||||
let transform = self.transform_mut();
|
||||
*transform = transform.then_scale_non_uniform(x, y);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn translate(mut self, v: impl Into<crate::Vec2>) -> Self {
|
||||
let transform = self.transform_mut();
|
||||
*transform = transform.then_translate(v.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn transform(mut self, v: impl Into<crate::Affine>) -> Self {
|
||||
*self.transform_mut() *= v.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@ use std::marker::PhantomData;
|
|||
use masonry::widget;
|
||||
|
||||
use crate::core::{DynMessage, Mut, ViewMarker};
|
||||
use crate::{MessageResult, Pod, View, ViewCtx, ViewId, WidgetView};
|
||||
use crate::{Affine, MessageResult, Pod, View, ViewCtx, ViewId, WidgetView};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
/// A view which puts `child` into a scrollable region.
|
||||
///
|
||||
|
@ -17,6 +19,7 @@ where
|
|||
{
|
||||
Portal {
|
||||
child,
|
||||
transform: Affine::IDENTITY,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +27,7 @@ where
|
|||
#[must_use = "View values do nothing unless provided to Xilem."]
|
||||
pub struct Portal<V, State, Action> {
|
||||
child: V,
|
||||
transform: Affine,
|
||||
phantom: PhantomData<(State, Action)>,
|
||||
}
|
||||
|
||||
|
@ -41,7 +45,8 @@ where
|
|||
// The Portal `View` doesn't get any messages directly (yet - scroll events?), so doesn't need to
|
||||
// use ctx.with_id.
|
||||
let (child, child_state) = self.child.build(ctx);
|
||||
let widget_pod = ctx.new_pod(widget::Portal::new_pod(child.inner));
|
||||
let widget_pod =
|
||||
ctx.new_pod_with_transform(widget::Portal::new_pod(child.inner), self.transform);
|
||||
(widget_pod, child_state)
|
||||
}
|
||||
|
||||
|
@ -52,6 +57,9 @@ where
|
|||
ctx: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
let child_element = widget::Portal::child_mut(&mut element);
|
||||
self.child
|
||||
.rebuild(&prev.child, view_state, ctx, child_element);
|
||||
|
@ -77,3 +85,9 @@ where
|
|||
self.child.message(view_state, id_path, message, app_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, State, Action> Transformable for Portal<V, State, Action> {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,26 @@
|
|||
use masonry::widget;
|
||||
|
||||
use crate::core::{DynMessage, Mut, ViewMarker};
|
||||
use crate::{MessageResult, Pod, View, ViewCtx, ViewId};
|
||||
use crate::{Affine, MessageResult, Pod, View, ViewCtx, ViewId};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
pub fn progress_bar(progress: Option<f64>) -> ProgressBar {
|
||||
ProgressBar { progress }
|
||||
ProgressBar {
|
||||
progress,
|
||||
transform: Affine::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProgressBar {
|
||||
progress: Option<f64>,
|
||||
transform: Affine,
|
||||
}
|
||||
|
||||
impl Transformable for ProgressBar {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewMarker for ProgressBar {}
|
||||
|
@ -20,7 +32,9 @@ impl<State, Action> View<State, Action, ViewCtx> for ProgressBar {
|
|||
type ViewState = ();
|
||||
|
||||
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||
ctx.with_leaf_action_widget(|ctx| ctx.new_pod(widget::ProgressBar::new(self.progress)))
|
||||
ctx.with_leaf_action_widget(|ctx| {
|
||||
ctx.new_pod_with_transform(widget::ProgressBar::new(self.progress), self.transform)
|
||||
})
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
|
@ -30,6 +44,9 @@ impl<State, Action> View<State, Action, ViewCtx> for ProgressBar {
|
|||
_ctx: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
if prev.progress != self.progress {
|
||||
widget::ProgressBar::set_progress(&mut element, self.progress);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@ use masonry::widget::{self, LineBreaking};
|
|||
use vello::peniko::Brush;
|
||||
|
||||
use crate::core::{DynMessage, Mut, ViewMarker};
|
||||
use crate::{Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId};
|
||||
use crate::{Affine, Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
pub fn prose(content: impl Into<ArcStr>) -> Prose {
|
||||
Prose {
|
||||
|
@ -15,6 +17,7 @@ pub fn prose(content: impl Into<ArcStr>) -> Prose {
|
|||
alignment: TextAlignment::default(),
|
||||
text_size: masonry::theme::TEXT_SIZE_NORMAL,
|
||||
line_break_mode: LineBreaking::WordWrap,
|
||||
transform: Affine::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,6 +39,7 @@ pub struct Prose {
|
|||
alignment: TextAlignment,
|
||||
text_size: f32,
|
||||
line_break_mode: LineBreaking,
|
||||
transform: Affine,
|
||||
// TODO: disabled: bool,
|
||||
// TODO: add more attributes of `masonry::widget::Prose`
|
||||
}
|
||||
|
@ -67,6 +71,12 @@ fn line_break_clips(linebreaking: LineBreaking) -> bool {
|
|||
matches!(linebreaking, LineBreaking::Clip | LineBreaking::WordWrap)
|
||||
}
|
||||
|
||||
impl Transformable for Prose {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewMarker for Prose {}
|
||||
impl<State, Action> View<State, Action, ViewCtx> for Prose {
|
||||
type Element = Pod<widget::Prose>;
|
||||
|
@ -78,9 +88,10 @@ impl<State, Action> View<State, Action, ViewCtx> for Prose {
|
|||
.with_alignment(self.alignment)
|
||||
.with_style(StyleProperty::FontSize(self.text_size))
|
||||
.with_word_wrap(self.line_break_mode == LineBreaking::WordWrap);
|
||||
let widget_pod = ctx.new_pod(
|
||||
let widget_pod = ctx.new_pod_with_transform(
|
||||
widget::Prose::from_text_area(text_area)
|
||||
.with_clip(line_break_clips(self.line_break_mode)),
|
||||
self.transform,
|
||||
);
|
||||
(widget_pod, ())
|
||||
}
|
||||
|
@ -92,6 +103,9 @@ impl<State, Action> View<State, Action, ViewCtx> for Prose {
|
|||
_ctx: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
let mut text_area = widget::Prose::text_mut(&mut element);
|
||||
if prev.content != self.content {
|
||||
widget::TextArea::reset_text(&mut text_area, &self.content);
|
||||
|
|
|
@ -9,7 +9,9 @@ use vello::kurbo::RoundedRectRadii;
|
|||
use vello::peniko::Brush;
|
||||
|
||||
use crate::core::{DynMessage, Mut, View, ViewId, ViewMarker};
|
||||
use crate::{Pod, ViewCtx, WidgetView};
|
||||
use crate::{Affine, Pod, ViewCtx, WidgetView};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
/// A widget with predefined size.
|
||||
///
|
||||
|
@ -29,6 +31,7 @@ where
|
|||
corner_radius: RoundedRectRadii::from_single_radius(0.0),
|
||||
padding: Padding::ZERO,
|
||||
phantom: PhantomData,
|
||||
transform: Affine::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,6 +45,7 @@ pub struct SizedBox<V, State, Action = ()> {
|
|||
corner_radius: RoundedRectRadii,
|
||||
padding: Padding,
|
||||
phantom: PhantomData<fn() -> (State, Action)>,
|
||||
transform: Affine,
|
||||
}
|
||||
|
||||
impl<V, State, Action> SizedBox<V, State, Action> {
|
||||
|
@ -121,6 +125,12 @@ impl<V, State, Action> SizedBox<V, State, Action> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<V, State, Action> Transformable for SizedBox<V, State, Action> {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, State, Action> ViewMarker for SizedBox<V, State, Action> {}
|
||||
impl<V, State, Action> View<State, Action, ViewCtx> for SizedBox<V, State, Action>
|
||||
where
|
||||
|
@ -144,7 +154,8 @@ where
|
|||
if let Some(border) = &self.border {
|
||||
widget = widget.border(border.brush.clone(), border.width);
|
||||
}
|
||||
(ctx.new_pod(widget), child_state)
|
||||
let pod = ctx.new_pod_with_transform(widget, self.transform);
|
||||
(pod, child_state)
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
|
@ -154,6 +165,9 @@ where
|
|||
ctx: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
if self.width != prev.width {
|
||||
match self.width {
|
||||
Some(width) => widget::SizedBox::set_width(&mut element, width),
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
use masonry::{widget, Color};
|
||||
|
||||
use crate::core::{DynMessage, Mut, ViewMarker};
|
||||
use crate::{MessageResult, Pod, View, ViewCtx, ViewId};
|
||||
use crate::{Affine, MessageResult, Pod, View, ViewCtx, ViewId};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
/// An indefinite spinner.
|
||||
///
|
||||
|
@ -32,7 +34,10 @@ use crate::{MessageResult, Pod, View, ViewCtx, ViewId};
|
|||
/// }
|
||||
/// ```
|
||||
pub fn spinner() -> Spinner {
|
||||
Spinner { color: None }
|
||||
Spinner {
|
||||
color: None,
|
||||
transform: Affine::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`View`] created by [`spinner`].
|
||||
|
@ -41,6 +46,7 @@ pub fn spinner() -> Spinner {
|
|||
#[must_use = "View values do nothing unless provided to Xilem."]
|
||||
pub struct Spinner {
|
||||
color: Option<Color>,
|
||||
transform: Affine,
|
||||
}
|
||||
|
||||
impl Spinner {
|
||||
|
@ -51,13 +57,20 @@ impl Spinner {
|
|||
}
|
||||
}
|
||||
|
||||
impl Transformable for Spinner {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewMarker for Spinner {}
|
||||
impl<State, Action> View<State, Action, ViewCtx> for Spinner {
|
||||
type Element = Pod<widget::Spinner>;
|
||||
type ViewState = ();
|
||||
|
||||
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||
(ctx.new_pod(widget::Spinner::new()), ())
|
||||
let pod = ctx.new_pod_with_transform(widget::Spinner::new(), self.transform);
|
||||
(pod, ())
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
|
@ -67,6 +80,9 @@ impl<State, Action> View<State, Action, ViewCtx> for Spinner {
|
|||
_: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
if prev.color != self.color {
|
||||
match self.color {
|
||||
Some(color) => widget::Spinner::set_color(&mut element, color),
|
||||
|
|
|
@ -5,7 +5,9 @@ use masonry::widget;
|
|||
use vello::peniko::Brush;
|
||||
|
||||
use crate::core::{DynMessage, Mut, View, ViewMarker};
|
||||
use crate::{Color, MessageResult, Pod, TextAlignment, ViewCtx, ViewId};
|
||||
use crate::{Affine, Color, MessageResult, Pod, TextAlignment, ViewCtx, ViewId};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
// FIXME - A major problem of the current approach (always setting the textbox contents)
|
||||
// is that if the user forgets to hook up the modify the state's contents in the callback,
|
||||
|
@ -24,6 +26,7 @@ where
|
|||
on_enter: None,
|
||||
text_brush: Color::WHITE.into(),
|
||||
alignment: TextAlignment::default(),
|
||||
transform: Affine::IDENTITY,
|
||||
// TODO?: disabled: false,
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +38,7 @@ pub struct Textbox<State, Action> {
|
|||
on_enter: Option<Callback<State, Action>>,
|
||||
text_brush: Brush,
|
||||
alignment: TextAlignment,
|
||||
transform: Affine,
|
||||
// TODO: add more attributes of `masonry::widget::TextBox`
|
||||
}
|
||||
|
||||
|
@ -59,6 +63,12 @@ impl<State, Action> Textbox<State, Action> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<State, Action> Transformable for Textbox<State, Action> {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl<State, Action> ViewMarker for Textbox<State, Action> {}
|
||||
impl<State: 'static, Action: 'static> View<State, Action, ViewCtx> for Textbox<State, Action> {
|
||||
type Element = Pod<widget::Textbox>;
|
||||
|
@ -74,7 +84,7 @@ impl<State: 'static, Action: 'static> View<State, Action, ViewCtx> for Textbox<S
|
|||
// Ensure that the actions from the *inner* TextArea get routed correctly.
|
||||
let id = textbox.area_pod().id();
|
||||
ctx.record_action(id);
|
||||
let widget_pod = ctx.new_pod(textbox);
|
||||
let widget_pod = ctx.new_pod_with_transform(textbox, self.transform);
|
||||
(widget_pod, ())
|
||||
}
|
||||
|
||||
|
@ -85,6 +95,9 @@ impl<State: 'static, Action: 'static> View<State, Action, ViewCtx> for Textbox<S
|
|||
_ctx: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if prev.transform != self.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
let mut text_area = widget::Textbox::text_mut(&mut element);
|
||||
|
||||
// Unlike the other properties, we don't compare to the previous value;
|
||||
|
|
|
@ -10,7 +10,7 @@ use xilem_core::ViewPathTracker;
|
|||
use crate::core::{DynMessage, Mut, ViewMarker};
|
||||
use crate::{MessageResult, Pod, View, ViewCtx, ViewId};
|
||||
|
||||
use super::{label, Label};
|
||||
use super::{label, Label, Transformable};
|
||||
|
||||
/// A view for displaying non-editable text, with a variable [weight](masonry::parley::style::FontWeight).
|
||||
pub fn variable_label(text: impl Into<ArcStr>) -> VariableLabel {
|
||||
|
@ -84,7 +84,11 @@ impl VariableLabel {
|
|||
}
|
||||
}
|
||||
|
||||
impl VariableLabel {}
|
||||
impl Transformable for VariableLabel {
|
||||
fn transform_mut(&mut self) -> &mut crate::Affine {
|
||||
&mut self.label.transform
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewMarker for VariableLabel {}
|
||||
impl<State, Action> View<State, Action, ViewCtx> for VariableLabel {
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
AppendVec, DynMessage, ElementSplice, Mut, SuperElement, View, ViewElement, ViewMarker,
|
||||
ViewSequence,
|
||||
},
|
||||
Pod, ViewCtx, WidgetView,
|
||||
Affine, Pod, ViewCtx, WidgetView,
|
||||
};
|
||||
use masonry::{
|
||||
widget::{self, Alignment, ChildAlignment, WidgetMut},
|
||||
|
@ -18,6 +18,8 @@ use masonry::{
|
|||
};
|
||||
use xilem_core::{MessageResult, ViewId};
|
||||
|
||||
use super::Transformable;
|
||||
|
||||
/// A widget that lays out its children on top of each other.
|
||||
/// The children are laid out back to front.
|
||||
///
|
||||
|
@ -40,6 +42,7 @@ pub fn zstack<State, Action, Seq: ZStackSequence<State, Action>>(sequence: Seq)
|
|||
ZStack {
|
||||
sequence,
|
||||
alignment: Alignment::default(),
|
||||
transform: Affine::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,6 +53,7 @@ pub fn zstack<State, Action, Seq: ZStackSequence<State, Action>>(sequence: Seq)
|
|||
pub struct ZStack<Seq> {
|
||||
sequence: Seq,
|
||||
alignment: Alignment,
|
||||
transform: Affine,
|
||||
}
|
||||
|
||||
impl<Seq> ZStack<Seq> {
|
||||
|
@ -78,7 +82,8 @@ where
|
|||
for child in elements.into_inner() {
|
||||
widget = widget.with_child_pod(child.widget.inner, child.alignment);
|
||||
}
|
||||
(ctx.new_pod(widget), seq_state)
|
||||
let pod = ctx.new_pod_with_transform(widget, self.transform);
|
||||
(pod, seq_state)
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
|
@ -88,6 +93,10 @@ where
|
|||
ctx: &mut ViewCtx,
|
||||
mut element: Mut<Self::Element>,
|
||||
) {
|
||||
if self.transform != prev.transform {
|
||||
element.set_transform(self.transform);
|
||||
}
|
||||
|
||||
if self.alignment != prev.alignment {
|
||||
widget::ZStack::set_alignment(&mut element, self.alignment);
|
||||
}
|
||||
|
@ -121,6 +130,12 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<Seq> Transformable for ZStack<Seq> {
|
||||
fn transform_mut(&mut self) -> &mut Affine {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
// --- MARK: ZStackExt ---
|
||||
|
||||
/// A trait that extends a [`WidgetView`] with methods to provide parameters for a parent [`ZStack`].
|
||||
|
|
Loading…
Reference in New Issue