From 069d7a634d98af7be6c838bad933f75e3e3dc1af Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Sat, 17 Feb 2024 02:02:56 -0800 Subject: [PATCH] Add layer transforms, interaction in layer (#3906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️ Removes `Context::translate_layer`, replacing it with a sticky `set_transform_layer` Adds the capability to scale layers. Allows interaction with scaled and transformed widgets inside transformed layers. I've also added a demo of how to have zooming and panning in a window (see the video below). This probably closes #1811. Having a panning and zooming container would just be creating a new `Area` with a new id, and applying zooming and panning with `ctx.transform_layer`. I've run the github workflow scripts in my repository, so hopefully the formatting and `cargo cranky` is satisfied. I'm not sure if all call sites where transforms would be relevant have been handled. This might also be missing are transforming clipping rects, but I'm not sure where / how to accomplish that. In the demo, the clipping rect is transformed to match, which seems to work. https://github.com/emilk/egui/assets/70821802/77e7e743-cdfe-402f-86e3-7744b3ee7b0f --------- Co-authored-by: tweoss Co-authored-by: Emil Ernerfeldt --- crates/egui/src/containers/area.rs | 2 +- crates/egui/src/containers/panel.rs | 16 ++ crates/egui/src/context.rs | 46 +++++- crates/egui/src/layers.rs | 22 ++- crates/egui/src/memory.rs | 21 ++- crates/egui/src/response.rs | 18 ++- crates/egui/src/ui.rs | 3 +- .../src/demo/demo_app_windows.rs | 1 + crates/egui_demo_lib/src/demo/mod.rs | 1 + crates/egui_demo_lib/src/demo/pan_zoom.rs | 136 ++++++++++++++++ crates/emath/src/lib.rs | 2 + crates/emath/src/ts_transform.rs | 146 ++++++++++++++++++ crates/epaint/src/mesh.rs | 7 + crates/epaint/src/shape.rs | 50 ++++-- examples/test_viewports/src/main.rs | 5 +- 15 files changed, 441 insertions(+), 35 deletions(-) create mode 100644 crates/egui_demo_lib/src/demo/pan_zoom.rs create mode 100644 crates/emath/src/ts_transform.rs diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 8925e06efa7..4aff4edb3d0 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -331,7 +331,7 @@ impl Area { ); if movable && move_response.dragged() { - state.pivot_pos += ctx.input(|i| i.pointer.delta()); + state.pivot_pos += move_response.drag_delta(); } if (move_response.dragged() || move_response.clicked()) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 2ae769e7dc0..705dfd733cd 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -247,6 +247,14 @@ impl SidePanel { .ctx() .layer_id_at(pointer) .map_or(true, |top_layer_id| top_layer_id == ui.layer_id()); + let pointer = if let Some(transform) = ui + .ctx() + .memory(|m| m.layer_transforms.get(&ui.layer_id()).cloned()) + { + transform.inverse() * pointer + } else { + pointer + }; let resize_x = side.opposite().side_x(panel_rect); let mouse_over_resize_line = we_are_on_top @@ -708,6 +716,14 @@ impl TopBottomPanel { .ctx() .layer_id_at(pointer) .map_or(true, |top_layer_id| top_layer_id == ui.layer_id()); + let pointer = if let Some(transform) = ui + .ctx() + .memory(|m| m.layer_transforms.get(&ui.layer_id()).cloned()) + { + transform.inverse() * pointer + } else { + pointer + }; let resize_y = side.opposite().side_y(panel_rect); let mouse_over_resize_line = we_are_on_top diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 67bcb48fab5..adfde717f53 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3,7 +3,9 @@ use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; use ahash::HashMap; -use epaint::{mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *}; +use epaint::{ + emath::TSTransform, mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *, +}; use crate::{ animation_manager::AnimationManager, @@ -1245,6 +1247,12 @@ impl Context { let is_interacted_with = res.is_pointer_button_down_on || clicked || res.drag_released; if is_interacted_with { res.interact_pointer_pos = input.pointer.interact_pos(); + if let (Some(transform), Some(pos)) = ( + memory.layer_transforms.get(&res.layer_id), + &mut res.interact_pointer_pos, + ) { + *pos = transform.inverse() * *pos; + } } if input.pointer.any_down() && !res.is_pointer_button_down_on { @@ -1958,7 +1966,9 @@ impl ContextImpl { } } - let shapes = viewport.graphics.drain(self.memory.areas().order()); + let shapes = viewport + .graphics + .drain(self.memory.areas().order(), &self.memory.layer_transforms); let mut repaint_needed = false; @@ -2266,13 +2276,19 @@ impl Context { } impl Context { - /// Move all the graphics at the given layer. + /// Transform the graphics of the given layer. /// - /// Can be used to implement drag-and-drop (see relevant demo). - pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) { - if delta != Vec2::ZERO { - self.graphics_mut(|g| g.entry(layer_id).translate(delta)); - } + /// This is a sticky setting, remembered from one frame to the next. + /// + /// Can be used to implement pan and zoom (see relevant demo). + pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) { + self.memory_mut(|m| { + if transform == TSTransform::IDENTITY { + m.layer_transforms.remove(&layer_id) + } else { + m.layer_transforms.insert(layer_id, transform) + } + }); } /// Top-most layer at the given position. @@ -2302,6 +2318,12 @@ impl Context { /// /// See also [`Response::contains_pointer`]. pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { + let rect = + if let Some(transform) = self.memory(|m| m.layer_transforms.get(&layer_id).cloned()) { + transform * rect + } else { + rect + }; if !rect.is_positive() { return false; } @@ -2348,6 +2370,12 @@ impl Context { let mut blocking_widget = None; self.write(|ctx| { + let transform = ctx + .memory + .layer_transforms + .get(&layer_id) + .cloned() + .unwrap_or_default(); let viewport = ctx.viewport(); // We add all widgets here, even non-interactive ones, @@ -2367,6 +2395,8 @@ impl Context { if contains_pointer { let pointer_pos = viewport.input.pointer.interact_pos(); if let Some(pointer_pos) = pointer_pos { + // Apply the inverse transformation of this layer to the pointer pos. + let pointer_pos = transform.inverse() * pointer_pos; if let Some(rects) = viewport.layer_rects_prev_frame.by_layer.get(&layer_id) { // Iterate backwards, i.e. topmost widgets first. for blocking in rects.iter().rev() { diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index e4dcb913eb1..2c97017106f 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -2,7 +2,7 @@ //! are sometimes painted behind or in front of other things. use crate::{Id, *}; -use epaint::{ClippedShape, Shape}; +use epaint::{emath::TSTransform, ClippedShape, Shape}; /// Different layer categories #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] @@ -158,11 +158,11 @@ impl PaintList { self.0[idx.0].shape = Shape::Noop; } - /// Translate each [`Shape`] and clip rectangle by this much, in-place - pub fn translate(&mut self, delta: Vec2) { + /// Transform each [`Shape`] and clip rectangle by this much, in-place + pub fn transform(&mut self, transform: TSTransform) { for ClippedShape { clip_rect, shape } in &mut self.0 { - *clip_rect = clip_rect.translate(delta); - shape.translate(delta); + *clip_rect = transform.mul_rect(*clip_rect); + shape.transform(transform); } } @@ -194,7 +194,11 @@ impl GraphicLayers { self.0[layer_id.order as usize].get_mut(&layer_id.id) } - pub fn drain(&mut self, area_order: &[LayerId]) -> Vec { + pub fn drain( + &mut self, + area_order: &[LayerId], + transforms: &ahash::HashMap, + ) -> Vec { crate::profile_function!(); let mut all_shapes: Vec<_> = Default::default(); @@ -211,6 +215,12 @@ impl GraphicLayers { for layer_id in area_order { if layer_id.order == order { if let Some(list) = order_map.get_mut(&layer_id.id) { + if let Some(transform) = transforms.get(layer_id) { + for clipped_shape in &mut list.0 { + clipped_shape.clip_rect = *transform * clipped_shape.clip_rect; + clipped_shape.shape.transform(*transform); + } + } all_shapes.append(&mut list.0); } } diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 8b969dc4bb5..f222b300b7a 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -1,5 +1,8 @@ #![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs +use ahash::HashMap; +use epaint::emath::TSTransform; + use crate::{ area, vec2, window::{self, WindowInteraction}, @@ -85,6 +88,9 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] everything_is_visible: bool, + /// Transforms per layer + pub layer_transforms: HashMap, + // ------------------------------------------------- // Per-viewport: areas: ViewportIdMap, @@ -107,6 +113,7 @@ impl Default for Memory { viewport_id: Default::default(), window_interactions: Default::default(), areas: Default::default(), + layer_transforms: Default::default(), popup: Default::default(), everything_is_visible: Default::default(), }; @@ -672,7 +679,8 @@ impl Memory { /// Top-most layer at the given position. pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option { - self.areas().layer_id_at(pos, resize_interact_radius_side) + self.areas() + .layer_id_at(pos, resize_interact_radius_side, &self.layer_transforms) } /// An iterator over all layers. Back-to-front. Top is last. @@ -948,7 +956,12 @@ impl Areas { } /// Top-most layer at the given position. - pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option { + pub fn layer_id_at( + &self, + pos: Pos2, + resize_interact_radius_side: f32, + layer_transforms: &HashMap, + ) -> Option { for layer in self.order.iter().rev() { if self.is_visible(layer) { if let Some(state) = self.areas.get(&layer.id) { @@ -959,6 +972,10 @@ impl Areas { rect = rect.expand(resize_interact_radius_side); } + if let Some(transform) = layer_transforms.get(layer) { + rect = *transform * rect; + } + if rect.contains(pos) { return Some(*layer); } diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index ff7c7da8f87..a6570014cfa 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -330,7 +330,14 @@ impl Response { #[inline] pub fn drag_delta(&self) -> Vec2 { if self.dragged() { - self.ctx.input(|i| i.pointer.delta()) + let mut delta = self.ctx.input(|i| i.pointer.delta()); + if let Some(scaling) = self + .ctx + .memory(|m| m.layer_transforms.get(&self.layer_id).map(|t| t.scaling)) + { + delta /= scaling; + } + delta } else { Vec2::ZERO } @@ -395,7 +402,14 @@ impl Response { #[inline] pub fn hover_pos(&self) -> Option { if self.hovered() { - self.ctx.input(|i| i.pointer.hover_pos()) + let mut pos = self.ctx.input(|i| i.pointer.hover_pos())?; + if let Some(transform) = self + .ctx + .memory(|m| m.layer_transforms.get(&self.layer_id).cloned()) + { + pos = transform * pos; + } + Some(pos) } else { None } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 087d2dd8fbe..c5244900c88 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2176,7 +2176,8 @@ impl Ui { if let Some(pointer_pos) = self.ctx().pointer_interact_pos() { let delta = pointer_pos - response.rect.center(); - self.ctx().translate_layer(layer_id, delta); + self.ctx() + .set_transform_layer(layer_id, emath::TSTransform::from_translation(delta)); } InnerResponse::new(inner, response) diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 5f4cd001b05..de1d96cddf4 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -32,6 +32,7 @@ impl Default for Demos { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index f041b00db59..d54498bbcf9 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -19,6 +19,7 @@ pub mod misc_demo_window; pub mod multi_touch; pub mod paint_bezier; pub mod painting; +pub mod pan_zoom; pub mod panels; pub mod password; pub mod plot_demo; diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs new file mode 100644 index 00000000000..6aa42179c4a --- /dev/null +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -0,0 +1,136 @@ +use egui::emath::TSTransform; + +#[derive(Clone, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct PanZoom { + transform: TSTransform, + drag_value: f32, +} + +impl Eq for PanZoom {} + +impl super::Demo for PanZoom { + fn name(&self) -> &'static str { + "🗖 Pan Zoom" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + use super::View as _; + let window = egui::Window::new("Pan Zoom") + .default_width(300.0) + .default_height(300.0) + .vscroll(false) + .open(open); + window.show(ctx, |ui| self.ui(ui)); + } +} + +impl super::View for PanZoom { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.label( + "Pan, zoom in, and zoom out with scrolling (see the plot demo for more instructions). \ + Double click on the background to reset.", + ); + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); + ui.separator(); + + let (id, rect) = ui.allocate_space(ui.available_size()); + let response = ui.interact(rect, id, egui::Sense::click_and_drag()); + // Allow dragging the background as well. + if response.dragged() { + self.transform.translation += response.drag_delta(); + } + + // Plot-like reset + if response.double_clicked() { + self.transform = TSTransform::default(); + } + + let transform = + TSTransform::from_translation(ui.min_rect().left_top().to_vec2()) * self.transform; + + if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) { + // Note: doesn't catch zooming / panning if a button in this PanZoom container is hovered. + if response.hovered() { + let pointer_in_layer = transform.inverse() * pointer; + let zoom_delta = ui.ctx().input(|i| i.zoom_delta()); + let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta); + + // Zoom in on pointer: + self.transform = self.transform + * TSTransform::from_translation(pointer_in_layer.to_vec2()) + * TSTransform::from_scaling(zoom_delta) + * TSTransform::from_translation(-pointer_in_layer.to_vec2()); + + // Pan: + self.transform = TSTransform::from_translation(pan_delta) * self.transform; + } + } + + for (id, pos, callback) in [ + ( + "a", + egui::Pos2::new(0.0, 0.0), + Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!")) + as Box egui::Response>, + ), + ( + "b", + egui::Pos2::new(0.0, 120.0), + Box::new(|ui: &mut egui::Ui, _| ui.button("bottom left?")), + ), + ( + "c", + egui::Pos2::new(120.0, 120.0), + Box::new(|ui: &mut egui::Ui, _| ui.button("right bottom :D")), + ), + ( + "d", + egui::Pos2::new(120.0, 0.0), + Box::new(|ui: &mut egui::Ui, _| ui.button("right top ):")), + ), + ( + "e", + egui::Pos2::new(60.0, 60.0), + Box::new(|ui, state| { + use egui::epaint::*; + // Smiley face. + let painter = ui.painter(); + painter.add(CircleShape::filled(pos2(0.0, -10.0), 1.0, Color32::YELLOW)); + painter.add(CircleShape::filled(pos2(10.0, -10.0), 1.0, Color32::YELLOW)); + painter.add(QuadraticBezierShape::from_points_stroke( + [pos2(0.0, 0.0), pos2(5.0, 3.0), pos2(10.0, 0.0)], + false, + Color32::TRANSPARENT, + Stroke::new(1.0, Color32::YELLOW), + )); + + ui.add(egui::Slider::new(&mut state.drag_value, 0.0..=100.0).text("My value")) + }), + ), + ] { + let id = egui::Area::new(id) + .default_pos(pos) + // Need to cover up the pan_zoom demo window, + // but may also cover over other windows. + .order(egui::Order::Foreground) + .show(ui.ctx(), |ui| { + ui.set_clip_rect(transform.inverse() * rect); + egui::Frame::default() + .rounding(egui::Rounding::same(4.0)) + .inner_margin(egui::Margin::same(8.0)) + .stroke(ui.ctx().style().visuals.window_stroke) + .fill(ui.style().visuals.panel_fill) + .show(ui, |ui| { + ui.style_mut().wrap = Some(false); + callback(ui, self) + }); + }) + .response + .layer_id; + ui.ctx().set_transform_layer(id, transform); + } + } +} diff --git a/crates/emath/src/lib.rs b/crates/emath/src/lib.rs index 4c6947bd074..d77091667a8 100644 --- a/crates/emath/src/lib.rs +++ b/crates/emath/src/lib.rs @@ -36,6 +36,7 @@ mod rect; mod rect_transform; mod rot2; pub mod smart_aim; +mod ts_transform; mod vec2; mod vec2b; @@ -48,6 +49,7 @@ pub use { rect::*, rect_transform::*, rot2::*, + ts_transform::*, vec2::*, vec2b::*, }; diff --git a/crates/emath/src/ts_transform.rs b/crates/emath/src/ts_transform.rs new file mode 100644 index 00000000000..4a761191bac --- /dev/null +++ b/crates/emath/src/ts_transform.rs @@ -0,0 +1,146 @@ +use crate::{Pos2, Rect, Vec2}; + +/// Linearly transforms positions via a translation, then a scaling. +/// +/// [`TSTransform`] first scales points with the scaling origin at `0, 0` +/// (the top left corner), then translates them. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] +pub struct TSTransform { + /// Scaling applied first, scaled around (0, 0). + pub scaling: f32, + + /// Translation amount, applied after scaling. + pub translation: Vec2, +} + +impl Eq for TSTransform {} + +impl Default for TSTransform { + #[inline] + fn default() -> Self { + Self::IDENTITY + } +} + +impl TSTransform { + pub const IDENTITY: Self = Self { + translation: Vec2::ZERO, + scaling: 1.0, + }; + + #[inline] + /// Creates a new translation that first scales points around + /// `(0, 0)`, then translates them. + pub fn new(translation: Vec2, scaling: f32) -> Self { + Self { + translation, + scaling, + } + } + + #[inline] + pub fn from_translation(translation: Vec2) -> Self { + Self::new(translation, 1.0) + } + + #[inline] + pub fn from_scaling(scaling: f32) -> Self { + Self::new(Vec2::ZERO, scaling) + } + + /// Inverts the transform. + /// + /// ``` + /// # use emath::{pos2, vec2, TSTransform}; + /// let p1 = pos2(2.0, 3.0); + /// let p2 = pos2(12.0, 5.0); + /// let ts = TSTransform::new(vec2(2.0, 3.0), 2.0); + /// let inv = ts.inverse(); + /// assert_eq!(inv.mul_pos(p1), pos2(0.0, 0.0)); + /// assert_eq!(inv.mul_pos(p2), pos2(5.0, 1.0)); + /// + /// assert_eq!(ts.inverse().inverse(), ts); + /// ``` + #[inline] + pub fn inverse(&self) -> Self { + Self::new(-self.translation / self.scaling, 1.0 / self.scaling) + } + + /// Transforms the given coordinate. + /// + /// ``` + /// # use emath::{pos2, vec2, TSTransform}; + /// let p1 = pos2(0.0, 0.0); + /// let p2 = pos2(5.0, 1.0); + /// let ts = TSTransform::new(vec2(2.0, 3.0), 2.0); + /// assert_eq!(ts.mul_pos(p1), pos2(2.0, 3.0)); + /// assert_eq!(ts.mul_pos(p2), pos2(12.0, 5.0)); + /// ``` + #[inline] + pub fn mul_pos(&self, pos: Pos2) -> Pos2 { + self.scaling * pos + self.translation + } + + /// Transforms the given rectangle. + /// + /// ``` + /// # use emath::{pos2, vec2, Rect, TSTransform}; + /// let rect = Rect::from_min_max(pos2(5.0, 5.0), pos2(15.0, 10.0)); + /// let ts = TSTransform::new(vec2(1.0, 0.0), 3.0); + /// let transformed = ts.mul_rect(rect); + /// assert_eq!(transformed.min, pos2(16.0, 15.0)); + /// assert_eq!(transformed.max, pos2(46.0, 30.0)); + /// ``` + #[inline] + pub fn mul_rect(&self, rect: Rect) -> Rect { + Rect { + min: self.mul_pos(rect.min), + max: self.mul_pos(rect.max), + } + } +} + +/// Transforms the position. +impl std::ops::Mul for TSTransform { + type Output = Pos2; + + #[inline] + fn mul(self, pos: Pos2) -> Pos2 { + self.mul_pos(pos) + } +} + +/// Transforms the rectangle. +impl std::ops::Mul for TSTransform { + type Output = Rect; + + #[inline] + fn mul(self, rect: Rect) -> Rect { + self.mul_rect(rect) + } +} + +impl std::ops::Mul for TSTransform { + type Output = Self; + + #[inline] + /// Applies the right hand side transform, then the left hand side. + /// + /// ``` + /// # use emath::{TSTransform, vec2}; + /// let ts1 = TSTransform::new(vec2(1.0, 0.0), 2.0); + /// let ts2 = TSTransform::new(vec2(-1.0, -1.0), 3.0); + /// let ts_combined = TSTransform::new(vec2(2.0, -1.0), 6.0); + /// assert_eq!(ts_combined, ts2 * ts1); + /// ``` + fn mul(self, rhs: Self) -> Self::Output { + // Apply rhs first. + Self { + scaling: self.scaling * rhs.scaling, + translation: self.translation + self.scaling * rhs.translation, + } + } +} diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index 2f85214e1e0..7d5a51965d9 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -278,6 +278,13 @@ impl Mesh { } } + /// Transform the mesh in-place with the given transform. + pub fn transform(&mut self, transform: TSTransform) { + for v in &mut self.vertices { + v.pos = transform * v.pos; + } + } + /// Rotate by some angle about an origin, in-place. /// /// Origin is a position in screen space. diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index ceb5dd072e5..ffc7b174be4 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -356,48 +356,70 @@ impl Shape { } /// Move the shape by this many points, in-place. - pub fn translate(&mut self, delta: Vec2) { + /// + /// If using a [`PaintCallback`], note that only the rect is scaled as opposed + /// to other shapes where the stroke is also scaled. + pub fn transform(&mut self, transform: TSTransform) { match self { Self::Noop => {} Self::Vec(shapes) => { for shape in shapes { - shape.translate(delta); + shape.transform(transform); } } Self::Circle(circle_shape) => { - circle_shape.center += delta; + circle_shape.center = transform * circle_shape.center; + circle_shape.radius *= transform.scaling; + circle_shape.stroke.width *= transform.scaling; } - Self::LineSegment { points, .. } => { + Self::LineSegment { points, stroke } => { for p in points { - *p += delta; + *p = transform * *p; } + stroke.width *= transform.scaling; } Self::Path(path_shape) => { for p in &mut path_shape.points { - *p += delta; + *p = transform * *p; } + path_shape.stroke.width *= transform.scaling; } Self::Rect(rect_shape) => { - rect_shape.rect = rect_shape.rect.translate(delta); + rect_shape.rect = transform * rect_shape.rect; + rect_shape.stroke.width *= transform.scaling; } Self::Text(text_shape) => { - text_shape.pos += delta; + text_shape.pos = transform * text_shape.pos; + + // Scale text: + let galley = Arc::make_mut(&mut text_shape.galley); + for row in &mut galley.rows { + row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds; + for v in &mut row.visuals.mesh.vertices { + v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); + } + } + + galley.mesh_bounds = transform.scaling * galley.mesh_bounds; + galley.rect = transform.scaling * galley.rect; } Self::Mesh(mesh) => { - mesh.translate(delta); + mesh.transform(transform); } Self::QuadraticBezier(bezier_shape) => { - bezier_shape.points[0] += delta; - bezier_shape.points[1] += delta; - bezier_shape.points[2] += delta; + bezier_shape.points[0] = transform * bezier_shape.points[0]; + bezier_shape.points[1] = transform * bezier_shape.points[1]; + bezier_shape.points[2] = transform * bezier_shape.points[2]; + bezier_shape.stroke.width *= transform.scaling; } Self::CubicBezier(cubic_curve) => { for p in &mut cubic_curve.points { - *p += delta; + *p = transform * *p; } + cubic_curve.stroke.width *= transform.scaling; } Self::Callback(shape) => { - shape.rect = shape.rect.translate(delta); + shape.rect = transform * shape.rect; } } } diff --git a/examples/test_viewports/src/main.rs b/examples/test_viewports/src/main.rs index e31516e4011..791cf8e4efc 100644 --- a/examples/test_viewports/src/main.rs +++ b/examples/test_viewports/src/main.rs @@ -428,7 +428,10 @@ fn drag_source( if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { let delta = pointer_pos - res.response.rect.center(); - ui.ctx().translate_layer(layer_id, delta); + ui.ctx().set_transform_layer( + layer_id, + eframe::emath::TSTransform::from_translation(delta), + ); } res