From 24143f4290055732e943244ae1a4d73ce3565d91 Mon Sep 17 00:00:00 2001 From: tweoss Date: Sat, 27 Jan 2024 10:18:36 -0800 Subject: [PATCH 01/34] add scaling layers, interaction with scaled items --- crates/egui/src/context.rs | 23 +++++++-- crates/egui/src/layers.rs | 8 +-- crates/egui/src/memory.rs | 51 +++++++++++++++++-- .../egui_demo_lib/src/demo/drag_and_drop.rs | 2 +- crates/epaint/src/mesh.rs | 3 +- crates/epaint/src/shape.rs | 38 +++++++++----- examples/test_viewports/src/main.rs | 2 +- 7 files changed, 100 insertions(+), 27 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 89f44fb1ba6..4102023761f 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -12,7 +12,7 @@ use crate::{ input_state::*, layers::GraphicLayers, load::{Bytes, Loaders, SizedTexture}, - memory::Options, + memory::{LayerTransform, Options}, os::OperatingSystem, output::FullOutput, util::IdTypeMap, @@ -2070,9 +2070,14 @@ impl Context { /// Move all the graphics at the given layer. /// /// Can be used to implement drag-and-drop (see relevant demo). - pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) { + pub fn transform_layer(&self, layer_id: LayerId, delta: Vec2, scale: f32) { if delta != Vec2::ZERO { - self.graphics_mut(|g| g.entry(layer_id).translate(delta)); + let transform = LayerTransform { + translation: delta, + scale, + }; + self.graphics_mut(|g| g.entry(layer_id).transform(&transform)); + self.memory_mut(|m| m.layer_transforms_mut().insert(layer_id, transform)); } } @@ -2101,6 +2106,10 @@ impl Context { /// /// See also [`Response::contains_pointer`]. pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { + let transform = self + .memory(|m| m.layer_transforms().get(&layer_id).cloned()) + .unwrap_or_default(); + let rect = transform.apply(rect); if !rect.is_positive() { return false; } @@ -2141,6 +2150,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, @@ -2164,7 +2179,7 @@ impl Context { // which means there are no widgets covering us. break; } - if !blocking.rect.contains(pointer_pos) { + if !transform.apply(blocking.rect).contains(pointer_pos) { continue; } if sense.interactive() && !blocking.sense.interactive() { diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 74eb45a3f14..988233219cc 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -1,7 +1,7 @@ //! Handles paint layers, i.e. how things //! are sometimes painted behind or in front of other things. -use crate::{Id, *}; +use crate::{memory::LayerTransform, Id, *}; use epaint::{ClippedShape, Shape}; /// Different layer categories @@ -159,10 +159,10 @@ impl PaintList { } /// Translate each [`Shape`] and clip rectangle by this much, in-place - pub fn translate(&mut self, delta: Vec2) { + pub fn transform(&mut self, transform: &LayerTransform) { for ClippedShape { clip_rect, shape } in &mut self.0 { - *clip_rect = clip_rect.translate(delta); - shape.translate(delta); + *clip_rect = transform.apply(*clip_rect); + shape.transform(transform.translation, transform.scale); } } diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index d1881c93450..3cb668db685 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -1,5 +1,7 @@ #![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs +use ahash::HashMap; + use crate::{ area, vec2, window::{self, WindowInteraction}, @@ -88,7 +90,7 @@ pub struct Memory { // ------------------------------------------------- // Per-viewport: areas: ViewportIdMap, - + layer_transforms: HashMap, #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) interactions: ViewportIdMap, @@ -107,6 +109,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(), }; @@ -151,6 +154,30 @@ impl FocusDirection { } } +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct LayerTransform { + pub translation: Vec2, + pub scale: f32, +} + +impl Default for LayerTransform { + fn default() -> Self { + Self { + translation: Vec2::default(), + scale: 1.0, + } + } +} + +impl LayerTransform { + pub(crate) fn apply(&self, rect: Rect) -> Rect { + Rect::from_center_size( + (rect.center() + self.translation) * self.scale, + rect.size() * self.scale, + ) + } +} + // ---------------------------------------------------------------------------- /// Some global options that you can read and write. @@ -605,9 +632,18 @@ impl Memory { self.areas.entry(self.viewport_id).or_default() } + pub fn layer_transforms(&self) -> &HashMap { + &self.layer_transforms + } + + pub fn layer_transforms_mut(&mut self) -> &mut HashMap { + &mut self.layer_transforms + } + /// 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. @@ -883,7 +919,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) { @@ -894,6 +935,10 @@ impl Areas { rect = rect.expand(resize_interact_radius_side); } + if let Some(transform) = layer_transforms.get(&layer) { + rect = transform.apply(rect); + } + if rect.contains(pos) { return Some(*layer); } diff --git a/crates/egui_demo_lib/src/demo/drag_and_drop.rs b/crates/egui_demo_lib/src/demo/drag_and_drop.rs index 37c21238047..55ab2815493 100644 --- a/crates/egui_demo_lib/src/demo/drag_and_drop.rs +++ b/crates/egui_demo_lib/src/demo/drag_and_drop.rs @@ -27,7 +27,7 @@ pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) { if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { let delta = pointer_pos - response.rect.center(); - ui.ctx().translate_layer(layer_id, delta); + ui.ctx().transform_layer(layer_id, delta); } } } diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index e33d950ec0f..6921ea6bf15 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -269,9 +269,10 @@ impl Mesh { } /// Translate location by this much, in-place - pub fn translate(&mut self, delta: Vec2) { + pub fn transform(&mut self, delta: Vec2, scale: f32) { for v in &mut self.vertices { v.pos += delta; + v.pos = (v.pos.to_vec2() * scale).to_pos2(); } } diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 61b9d75e92b..504e510e0bf 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -356,48 +356,60 @@ impl Shape { } /// Move the shape by this many points, in-place. - pub fn translate(&mut self, delta: Vec2) { + pub fn transform(&mut self, delta: Vec2, scale: f32) { + let transform_point = |p: &mut Pos2| *p = ((*p + delta).to_vec2() * scale).to_pos2(); + let transform_rect = |r: &mut Rect| { + transform_point(&mut r.min); + transform_point(&mut r.max); + }; match self { Self::Noop => {} Self::Vec(shapes) => { for shape in shapes { - shape.translate(delta); + shape.transform(delta, scale); } } Self::Circle(circle_shape) => { - circle_shape.center += delta; + transform_point(&mut circle_shape.center); + circle_shape.radius *= scale; } Self::LineSegment { points, .. } => { for p in points { - *p += delta; + transform_point(p); } } Self::Path(path_shape) => { for p in &mut path_shape.points { - *p += delta; + transform_point(p); } } Self::Rect(rect_shape) => { - rect_shape.rect = rect_shape.rect.translate(delta); + transform_rect(&mut rect_shape.rect); } Self::Text(text_shape) => { - text_shape.pos += delta; + transform_point(&mut text_shape.pos); + let mut galley = (*text_shape.galley).clone(); + for row in galley.rows.iter_mut() { + row.visuals.mesh.transform(Vec2::ZERO, scale); + } + + text_shape.galley = Arc::new(galley); } Self::Mesh(mesh) => { - mesh.translate(delta); + mesh.transform(delta, scale); } Self::QuadraticBezier(bezier_shape) => { - bezier_shape.points[0] += delta; - bezier_shape.points[1] += delta; - bezier_shape.points[2] += delta; + transform_point(&mut bezier_shape.points[0]); + transform_point(&mut bezier_shape.points[1]); + transform_point(&mut bezier_shape.points[2]); } Self::CubicBezier(cubic_curve) => { for p in &mut cubic_curve.points { - *p += delta; + transform_point(p); } } Self::Callback(shape) => { - shape.rect = shape.rect.translate(delta); + transform_rect(&mut shape.rect); } } } diff --git a/examples/test_viewports/src/main.rs b/examples/test_viewports/src/main.rs index e31516e4011..68982ba982f 100644 --- a/examples/test_viewports/src/main.rs +++ b/examples/test_viewports/src/main.rs @@ -428,7 +428,7 @@ 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().transform_layer(layer_id, delta); } res From 0f6f82ceaec13c490072930ea3b015beb734201a Mon Sep 17 00:00:00 2001 From: tweoss Date: Sat, 27 Jan 2024 13:58:08 -0800 Subject: [PATCH 02/34] fix style --- crates/egui/src/memory.rs | 9 +++++++-- crates/egui_demo_lib/src/demo/drag_and_drop.rs | 2 +- crates/epaint/src/shape.rs | 2 +- examples/test_viewports/src/main.rs | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 3cb668db685..205eba19b98 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -91,6 +91,7 @@ pub struct Memory { // Per-viewport: areas: ViewportIdMap, layer_transforms: HashMap, + #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) interactions: ViewportIdMap, @@ -154,7 +155,9 @@ impl FocusDirection { } } -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "persistence", serde(default))] pub struct LayerTransform { pub translation: Vec2, pub scale: f32, @@ -632,10 +635,12 @@ impl Memory { self.areas.entry(self.viewport_id).or_default() } + /// Access layer transformations. pub fn layer_transforms(&self) -> &HashMap { &self.layer_transforms } + /// Access layer transformations. pub fn layer_transforms_mut(&mut self) -> &mut HashMap { &mut self.layer_transforms } @@ -935,7 +940,7 @@ impl Areas { rect = rect.expand(resize_interact_radius_side); } - if let Some(transform) = layer_transforms.get(&layer) { + if let Some(transform) = layer_transforms.get(layer) { rect = transform.apply(rect); } diff --git a/crates/egui_demo_lib/src/demo/drag_and_drop.rs b/crates/egui_demo_lib/src/demo/drag_and_drop.rs index 55ab2815493..0cea35f5b5a 100644 --- a/crates/egui_demo_lib/src/demo/drag_and_drop.rs +++ b/crates/egui_demo_lib/src/demo/drag_and_drop.rs @@ -27,7 +27,7 @@ pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) { if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { let delta = pointer_pos - response.rect.center(); - ui.ctx().transform_layer(layer_id, delta); + ui.ctx().transform_layer(layer_id, delta, 1.0); } } } diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 504e510e0bf..4e5be9aeed6 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -389,7 +389,7 @@ impl Shape { Self::Text(text_shape) => { transform_point(&mut text_shape.pos); let mut galley = (*text_shape.galley).clone(); - for row in galley.rows.iter_mut() { + for row in &mut galley.rows { row.visuals.mesh.transform(Vec2::ZERO, scale); } diff --git a/examples/test_viewports/src/main.rs b/examples/test_viewports/src/main.rs index 68982ba982f..915c6e940e8 100644 --- a/examples/test_viewports/src/main.rs +++ b/examples/test_viewports/src/main.rs @@ -428,7 +428,7 @@ fn drag_source( if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { let delta = pointer_pos - res.response.rect.center(); - ui.ctx().transform_layer(layer_id, delta); + ui.ctx().transform_layer(layer_id, delta, 1.0); } res From 141433dec4404acf5f947d3158a07bc6a2a4acf6 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Sun, 28 Jan 2024 18:30:35 -0800 Subject: [PATCH 03/34] add pan_zoom demo --- .../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 | 108 ++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 crates/egui_demo_lib/src/demo/pan_zoom.rs 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..5918f6f1987 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -0,0 +1,108 @@ +use egui::{Area, Sense}; + +#[derive(Clone, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct PanZoom { + pan: egui::Vec2, + zoom: 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(200.0) + .default_height(200.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) { + // On initialization, zoom is 0 + if self.zoom == 0.0 { + self.zoom = 1.0; + } + + let (id, rect) = ui.allocate_space(ui.available_size()); + let response = ui.interact(rect, id, egui::Sense::click_and_drag()); + // Uncomment to allow dragging the background as well. + // self.pan += response.drag_delta() / self.zoom; + + // Plot-like reset + if response.double_clicked() { + self.zoom = 1.0; + self.pan = egui::Vec2::ZERO; + } + + if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) { + // Ignore if some other widget is covering this container. + if response.rect.contains(pointer) { + let original_zoom = self.zoom; + self.zoom *= ui.ctx().input(|i| i.zoom_delta()); + let delta = pointer / self.zoom - pointer / original_zoom; + self.pan += delta; + + // Keep mouse centered. + self.pan += ui.ctx().input(|i| i.raw_scroll_delta) / self.zoom; + } + } + + let current_size = ui.min_rect(); + let layer_ids = [ + ( + current_size.left_top() + egui::Vec2::new(10.0, 10.0), + "top left!", + ), + ( + current_size.left_bottom() + egui::Vec2::new(10.0, -10.0), + "bottom left?", + ), + ( + current_size.right_bottom() + egui::Vec2::new(-10.0, -10.0), + "right bottom :D", + ), + ( + current_size.right_top() + egui::Vec2::new(-10.0, 10.0), + "right top ):", + ), + ] + .iter() + .map(|(pos, msg)| { + Area::new(*msg) + .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| { + let rect = egui::Rect::from_min_max( + (rect.min / self.zoom) - self.pan, + (rect.max / self.zoom) - self.pan, + ); + ui.set_clip_rect(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); + ui.button(*msg).clicked(); + }); + }) + .response + .layer_id + }) + .for_each(|id| { + ui.ctx().transform_layer(id, self.pan, self.zoom); + }); + } +} From c001c5ae02c22268c481bc3d6473998a90409855 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Sun, 28 Jan 2024 18:30:35 -0800 Subject: [PATCH 04/34] add pan_zoom demo --- .../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 | 108 ++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 crates/egui_demo_lib/src/demo/pan_zoom.rs 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..5918f6f1987 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -0,0 +1,108 @@ +use egui::{Area, Sense}; + +#[derive(Clone, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct PanZoom { + pan: egui::Vec2, + zoom: 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(200.0) + .default_height(200.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) { + // On initialization, zoom is 0 + if self.zoom == 0.0 { + self.zoom = 1.0; + } + + let (id, rect) = ui.allocate_space(ui.available_size()); + let response = ui.interact(rect, id, egui::Sense::click_and_drag()); + // Uncomment to allow dragging the background as well. + // self.pan += response.drag_delta() / self.zoom; + + // Plot-like reset + if response.double_clicked() { + self.zoom = 1.0; + self.pan = egui::Vec2::ZERO; + } + + if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) { + // Ignore if some other widget is covering this container. + if response.rect.contains(pointer) { + let original_zoom = self.zoom; + self.zoom *= ui.ctx().input(|i| i.zoom_delta()); + let delta = pointer / self.zoom - pointer / original_zoom; + self.pan += delta; + + // Keep mouse centered. + self.pan += ui.ctx().input(|i| i.raw_scroll_delta) / self.zoom; + } + } + + let current_size = ui.min_rect(); + let layer_ids = [ + ( + current_size.left_top() + egui::Vec2::new(10.0, 10.0), + "top left!", + ), + ( + current_size.left_bottom() + egui::Vec2::new(10.0, -10.0), + "bottom left?", + ), + ( + current_size.right_bottom() + egui::Vec2::new(-10.0, -10.0), + "right bottom :D", + ), + ( + current_size.right_top() + egui::Vec2::new(-10.0, 10.0), + "right top ):", + ), + ] + .iter() + .map(|(pos, msg)| { + Area::new(*msg) + .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| { + let rect = egui::Rect::from_min_max( + (rect.min / self.zoom) - self.pan, + (rect.max / self.zoom) - self.pan, + ); + ui.set_clip_rect(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); + ui.button(*msg).clicked(); + }); + }) + .response + .layer_id + }) + .for_each(|id| { + ui.ctx().transform_layer(id, self.pan, self.zoom); + }); + } +} From 085c59393bbca05fc77de5c7ee7b0669ab287734 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Sun, 28 Jan 2024 18:36:11 -0800 Subject: [PATCH 05/34] fix warnings --- crates/egui_demo_lib/src/demo/pan_zoom.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 5918f6f1987..873c642d2d4 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -1,5 +1,3 @@ -use egui::{Area, Sense}; - #[derive(Clone, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PanZoom { @@ -57,7 +55,7 @@ impl super::View for PanZoom { } let current_size = ui.min_rect(); - let layer_ids = [ + [ ( current_size.left_top() + egui::Vec2::new(10.0, 10.0), "top left!", @@ -77,7 +75,7 @@ impl super::View for PanZoom { ] .iter() .map(|(pos, msg)| { - Area::new(*msg) + egui::Area::new(*msg) .default_pos(*pos) // Need to cover up the pan_zoom demo window, // but may also cover over other windows. From 264726bbe73d901ff0d9ac54808bc62741a36d63 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Mon, 29 Jan 2024 13:04:11 -0800 Subject: [PATCH 06/34] clean up --- .../egui_demo_lib/src/demo/drag_and_drop.rs | 76 ------------------- 1 file changed, 76 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/drag_and_drop.rs b/crates/egui_demo_lib/src/demo/drag_and_drop.rs index 0c033df4fe2..29bda3350ac 100644 --- a/crates/egui_demo_lib/src/demo/drag_and_drop.rs +++ b/crates/egui_demo_lib/src/demo/drag_and_drop.rs @@ -1,81 +1,5 @@ use egui::*; -// <<<<<<< HEAD -// pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) { -// let is_being_dragged = ui.memory(|mem| mem.is_being_dragged(id)); - -// if !is_being_dragged { -// let response = ui.scope(body).response; - -// // Check for drags: -// let response = ui.interact(response.rect, id, Sense::drag()); -// if response.hovered() { -// ui.ctx().set_cursor_icon(CursorIcon::Grab); -// } -// } else { -// ui.ctx().set_cursor_icon(CursorIcon::Grabbing); - -// // Paint the body to a new layer: -// let layer_id = LayerId::new(Order::Tooltip, id); -// let response = ui.with_layer_id(layer_id, body).response; - -// // Now we move the visuals of the body to where the mouse is. -// // Normally you need to decide a location for a widget first, -// // because otherwise that widget cannot interact with the mouse. -// // However, a dragged component cannot be interacted with anyway -// // (anything with `Order::Tooltip` always gets an empty [`Response`]) -// // So this is fine! - -// if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { -// let delta = pointer_pos - response.rect.center(); -// ui.ctx().transform_layer(layer_id, delta, 1.0); -// } -// } -// } - -// pub fn drop_target( -// ui: &mut Ui, -// can_accept_what_is_being_dragged: bool, -// body: impl FnOnce(&mut Ui) -> R, -// ) -> InnerResponse { -// let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged()); - -// let margin = Vec2::splat(4.0); - -// let outer_rect_bounds = ui.available_rect_before_wrap(); -// let inner_rect = outer_rect_bounds.shrink2(margin); -// let where_to_put_background = ui.painter().add(Shape::Noop); -// let mut content_ui = ui.child_ui(inner_rect, *ui.layout()); -// let ret = body(&mut content_ui); -// let outer_rect = Rect::from_min_max(outer_rect_bounds.min, content_ui.min_rect().max + margin); -// let (rect, response) = ui.allocate_at_least(outer_rect.size(), Sense::hover()); - -// // NOTE: we use `response.contains_pointer` here instead of `hovered`, because -// // `hovered` is always false when another widget is being dragged. -// let style = -// if is_being_dragged && can_accept_what_is_being_dragged && response.contains_pointer() { -// ui.visuals().widgets.active -// } else { -// ui.visuals().widgets.inactive -// }; - -// let mut fill = style.bg_fill; -// let mut stroke = style.bg_stroke; -// if is_being_dragged && !can_accept_what_is_being_dragged { -// fill = ui.visuals().gray_out(fill); -// stroke.color = ui.visuals().gray_out(stroke.color); -// } - -// ui.painter().set( -// where_to_put_background, -// epaint::RectShape::new(rect, style.rounding, fill, stroke), -// ); - -// InnerResponse::new(ret, response) -// } - -// ======= -// >>>>>>> master #[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct DragAndDropDemo { From 17bb75e1fc1a9c1c4ea72ccd7a6fd2f467bafa63 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Wed, 31 Jan 2024 16:30:45 -0800 Subject: [PATCH 07/34] port TSTransform to emath, make transform persist --- crates/egui/src/context.rs | 42 ++++++----- crates/egui/src/layers.rs | 22 ++++-- crates/egui/src/memory.rs | 37 ++-------- crates/egui/src/ui.rs | 3 +- crates/egui_demo_lib/src/demo/pan_zoom.rs | 59 ++++++--------- crates/emath/src/lib.rs | 2 + crates/emath/src/ts_transform.rs | 90 +++++++++++++++++++++++ crates/epaint/src/mesh.rs | 7 +- crates/epaint/src/shape.rs | 36 ++++----- examples/test_viewports/src/main.rs | 5 +- 10 files changed, 185 insertions(+), 118 deletions(-) create mode 100644 crates/emath/src/ts_transform.rs diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 5238fd31a6b..9aa1aacdb14 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3,7 +3,9 @@ use std::{borrow::Cow, cell::RefCell, 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, @@ -12,7 +14,7 @@ use crate::{ input_state::*, layers::GraphicLayers, load::{Bytes, Loaders, SizedTexture}, - memory::{LayerTransform, Options}, + memory::Options, os::OperatingSystem, output::FullOutput, util::IdTypeMap, @@ -1778,7 +1780,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()); if viewport.input.wants_repaint() { self.request_repaint(ended_viewport_id); @@ -2068,18 +2072,11 @@ impl Context { } impl Context { - /// Move all the graphics at the given layer. - /// - /// Can be used to implement drag-and-drop (see relevant demo). - pub fn transform_layer(&self, layer_id: LayerId, delta: Vec2, scale: f32) { - if delta != Vec2::ZERO { - let transform = LayerTransform { - translation: delta, - scale, - }; - self.graphics_mut(|g| g.entry(layer_id).transform(&transform)); - self.memory_mut(|m| m.layer_transforms_mut().insert(layer_id, transform)); - } + /// Transform the graphics at the given layer. + /// + /// 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| m.layer_transforms_mut().insert(layer_id, transform)); } /// Top-most layer at the given position. @@ -2107,10 +2104,13 @@ impl Context { /// /// See also [`Response::contains_pointer`]. pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { - let transform = self - .memory(|m| m.layer_transforms().get(&layer_id).cloned()) - .unwrap_or_default(); - let rect = transform.apply(rect); + 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; } @@ -2173,6 +2173,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.invert_pos(pointer_pos); if let Some(rects) = viewport.layer_rects_prev_frame.get(&layer_id) { for blocking in rects.iter().rev() { if blocking.id == id { @@ -2180,7 +2182,7 @@ impl Context { // which means there are no widgets covering us. break; } - if !transform.apply(blocking.rect).contains(pointer_pos) { + if !blocking.rect.contains(pointer_pos) { continue; } if sense.interactive() && !blocking.sense.interactive() { diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 988233219cc..5515616a865 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -1,8 +1,8 @@ //! Handles paint layers, i.e. how things //! are sometimes painted behind or in front of other things. -use crate::{memory::LayerTransform, Id, *}; -use epaint::{ClippedShape, Shape}; +use crate::{Id, *}; +use epaint::{emath::TSTransform, ClippedShape, Shape}; /// Different layer categories #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] @@ -159,10 +159,10 @@ impl PaintList { } /// Translate each [`Shape`] and clip rectangle by this much, in-place - pub fn transform(&mut self, transform: &LayerTransform) { + pub fn transform(&mut self, transform: TSTransform) { for ClippedShape { clip_rect, shape } in &mut self.0 { - *clip_rect = transform.apply(*clip_rect); - shape.transform(transform.translation, transform.scale); + *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 205eba19b98..3aaa20265a1 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -1,6 +1,7 @@ #![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs use ahash::HashMap; +use epaint::emath::TSTransform; use crate::{ area, vec2, @@ -90,7 +91,7 @@ pub struct Memory { // ------------------------------------------------- // Per-viewport: areas: ViewportIdMap, - layer_transforms: HashMap, + layer_transforms: HashMap, #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) interactions: ViewportIdMap, @@ -155,32 +156,6 @@ impl FocusDirection { } } -#[derive(Clone, Debug)] -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "persistence", serde(default))] -pub struct LayerTransform { - pub translation: Vec2, - pub scale: f32, -} - -impl Default for LayerTransform { - fn default() -> Self { - Self { - translation: Vec2::default(), - scale: 1.0, - } - } -} - -impl LayerTransform { - pub(crate) fn apply(&self, rect: Rect) -> Rect { - Rect::from_center_size( - (rect.center() + self.translation) * self.scale, - rect.size() * self.scale, - ) - } -} - // ---------------------------------------------------------------------------- /// Some global options that you can read and write. @@ -636,12 +611,12 @@ impl Memory { } /// Access layer transformations. - pub fn layer_transforms(&self) -> &HashMap { + pub fn layer_transforms(&self) -> &HashMap { &self.layer_transforms } /// Access layer transformations. - pub fn layer_transforms_mut(&mut self) -> &mut HashMap { + pub fn layer_transforms_mut(&mut self) -> &mut HashMap { &mut self.layer_transforms } @@ -928,7 +903,7 @@ impl Areas { &self, pos: Pos2, resize_interact_radius_side: f32, - layer_transforms: &HashMap, + layer_transforms: &HashMap, ) -> Option { for layer in self.order.iter().rev() { if self.is_visible(layer) { @@ -941,7 +916,7 @@ impl Areas { } if let Some(transform) = layer_transforms.get(layer) { - rect = transform.apply(rect); + rect = *transform * rect; } if rect.contains(pos) { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index c7680f0c99d..9a5123f7d87 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2153,7 +2153,8 @@ impl Ui { if let Some(pointer_pos) = self.ctx().pointer_interact_pos() { let delta = pointer_pos - response.rect.center(); - self.ctx().transform_layer(layer_id, delta, 1.0); + 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/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 873c642d2d4..8ac49c904f1 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -1,8 +1,9 @@ +use egui::emath::TSTransform; + #[derive(Clone, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PanZoom { - pan: egui::Vec2, - zoom: f32, + transform: TSTransform, } impl Eq for PanZoom {} @@ -25,37 +26,33 @@ impl super::Demo for PanZoom { impl super::View for PanZoom { fn ui(&mut self, ui: &mut egui::Ui) { - // On initialization, zoom is 0 - if self.zoom == 0.0 { - self.zoom = 1.0; - } - let (id, rect) = ui.allocate_space(ui.available_size()); let response = ui.interact(rect, id, egui::Sense::click_and_drag()); // Uncomment to allow dragging the background as well. - // self.pan += response.drag_delta() / self.zoom; + // self.transform.translation += response.drag_delta() / self.transform.scaling; // Plot-like reset if response.double_clicked() { - self.zoom = 1.0; - self.pan = egui::Vec2::ZERO; + self.transform = TSTransform::default(); } if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) { - // Ignore if some other widget is covering this container. - if response.rect.contains(pointer) { - let original_zoom = self.zoom; - self.zoom *= ui.ctx().input(|i| i.zoom_delta()); - let delta = pointer / self.zoom - pointer / original_zoom; - self.pan += delta; - + // Note: doesn't catch zooming / panning if a button in this PanZoom container is hovered. + if response.hovered() { + let original_zoom = self.transform.scaling; + let new_zoom = original_zoom * ui.ctx().input(|i| i.zoom_delta()); // Keep mouse centered. - self.pan += ui.ctx().input(|i| i.raw_scroll_delta) / self.zoom; + let pan_delta = pointer / new_zoom - pointer / original_zoom; + // Handle scrolling. + let pan_delta = pan_delta + ui.ctx().input(|i| i.raw_scroll_delta) / original_zoom; + + self.transform.scaling = new_zoom; + self.transform.translation += pan_delta; } } let current_size = ui.min_rect(); - [ + for (pos, msg) in [ ( current_size.left_top() + egui::Vec2::new(10.0, 10.0), "top left!", @@ -72,20 +69,14 @@ impl super::View for PanZoom { current_size.right_top() + egui::Vec2::new(-10.0, 10.0), "right top ):", ), - ] - .iter() - .map(|(pos, msg)| { - egui::Area::new(*msg) - .default_pos(*pos) + ] { + let id = egui::Area::new(msg) + .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| { - let rect = egui::Rect::from_min_max( - (rect.min / self.zoom) - self.pan, - (rect.max / self.zoom) - self.pan, - ); - ui.set_clip_rect(rect); + ui.set_clip_rect(self.transform.invert_rect(rect)); egui::Frame::default() .rounding(egui::Rounding::same(4.0)) .inner_margin(egui::Margin::same(8.0)) @@ -93,14 +84,12 @@ impl super::View for PanZoom { .fill(ui.style().visuals.panel_fill) .show(ui, |ui| { ui.style_mut().wrap = Some(false); - ui.button(*msg).clicked(); + ui.button(msg).clicked(); }); }) .response - .layer_id - }) - .for_each(|id| { - ui.ctx().transform_layer(id, self.pan, self.zoom); - }); + .layer_id; + ui.ctx().set_transform_layer(id, self.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..48c6f52144c --- /dev/null +++ b/crates/emath/src/ts_transform.rs @@ -0,0 +1,90 @@ +use crate::{Pos2, Rect, Vec2}; + +/// Linearly transforms positions via a translation, then a scaling. +/// +/// [`TSTransform`] first translates points, then scales them with the scaling origin +/// at `0, 0` (the top left corner) +#[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 { + /// Translation amount. + pub translation: Vec2, + /// Scaling amount after translation, scaled around (0, 0). + pub scaling: f32, +} + +impl Eq for TSTransform {} + +impl Default for TSTransform { + #[inline] + fn default() -> Self { + TSTransform { + translation: Vec2::ZERO, + scaling: 1.0, + } + } +} + +impl TSTransform { + /// The translation is applied first, then scaling around 0, 0. + pub fn new(translation: Vec2, scaling: f32) -> Self { + Self { + translation, + scaling, + } + } + + pub fn from_translation(translation: Vec2) -> Self { + Self::new(translation, 1.0) + } + + pub fn from_scaling(scaling: f32) -> Self { + Self::new(Vec2::ZERO, scaling) + } + + /// Reverses the transformation from screen space to layer space. + pub fn invert_pos(&self, pos: Pos2) -> Pos2 { + // First, reverse scale around 0, 0, then reverse transform. + let pos = pos / self.scaling; + pos - self.translation + } + + /// Reverses the transformation from screen space to layer space. + pub fn invert_rect(&self, rect: Rect) -> Rect { + Rect::from_min_max(self.invert_pos(rect.min), self.invert_pos(rect.max)) + } + + /// Transforms the given coordinate by translation then scaling. + pub fn mul_pos(&self, pos: Pos2) -> Pos2 { + let pos = pos + self.translation; + pos * self.scaling + } + + /// Transforms the given rectangle by translation then scaling. + 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; + + fn mul(self, pos: Pos2) -> Pos2 { + self.mul_pos(pos) + } +} + +/// Transforms the position. +impl std::ops::Mul for TSTransform { + type Output = Rect; + + fn mul(self, rect: Rect) -> Rect { + self.mul_rect(rect) + } +} diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index 6921ea6bf15..30b9c5a78f5 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -268,11 +268,10 @@ impl Mesh { output } - /// Translate location by this much, in-place - pub fn transform(&mut self, delta: Vec2, scale: f32) { + /// Transform the mesh in-place with the given transform. + pub fn transform(&mut self, transform: TSTransform) { for v in &mut self.vertices { - v.pos += delta; - v.pos = (v.pos.to_vec2() * scale).to_pos2(); + v.pos = transform * v.pos; } } diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 4e5be9aeed6..bd5d9861b13 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -356,60 +356,56 @@ impl Shape { } /// Move the shape by this many points, in-place. - pub fn transform(&mut self, delta: Vec2, scale: f32) { - let transform_point = |p: &mut Pos2| *p = ((*p + delta).to_vec2() * scale).to_pos2(); - let transform_rect = |r: &mut Rect| { - transform_point(&mut r.min); - transform_point(&mut r.max); - }; + pub fn transform(&mut self, transform: TSTransform) { match self { Self::Noop => {} Self::Vec(shapes) => { for shape in shapes { - shape.transform(delta, scale); + shape.transform(transform); } } Self::Circle(circle_shape) => { - transform_point(&mut circle_shape.center); - circle_shape.radius *= scale; + circle_shape.center = transform * circle_shape.center; + circle_shape.radius *= transform.scaling; } Self::LineSegment { points, .. } => { for p in points { - transform_point(p); + *p = transform * *p; } } Self::Path(path_shape) => { for p in &mut path_shape.points { - transform_point(p); + *p = transform * *p; } } Self::Rect(rect_shape) => { - transform_rect(&mut rect_shape.rect); + rect_shape.rect = transform * rect_shape.rect; } Self::Text(text_shape) => { - transform_point(&mut text_shape.pos); + text_shape.pos = transform * text_shape.pos; let mut galley = (*text_shape.galley).clone(); + let scale_transform = TSTransform::from_scaling(transform.scaling); for row in &mut galley.rows { - row.visuals.mesh.transform(Vec2::ZERO, scale); + row.visuals.mesh.transform(scale_transform); } text_shape.galley = Arc::new(galley); } Self::Mesh(mesh) => { - mesh.transform(delta, scale); + mesh.transform(transform); } Self::QuadraticBezier(bezier_shape) => { - transform_point(&mut bezier_shape.points[0]); - transform_point(&mut bezier_shape.points[1]); - transform_point(&mut bezier_shape.points[2]); + 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]; } Self::CubicBezier(cubic_curve) => { for p in &mut cubic_curve.points { - transform_point(p); + *p = transform * *p; } } Self::Callback(shape) => { - transform_rect(&mut shape.rect); + shape.rect = transform * shape.rect; } } } diff --git a/examples/test_viewports/src/main.rs b/examples/test_viewports/src/main.rs index 915c6e940e8..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().transform_layer(layer_id, delta, 1.0); + ui.ctx().set_transform_layer( + layer_id, + eframe::emath::TSTransform::from_translation(delta), + ); } res From b2342dcd66f2f27b71d6e429bb09ea2795188395 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Wed, 31 Jan 2024 16:38:03 -0800 Subject: [PATCH 08/34] use smooth scrolling in demo --- crates/egui_demo_lib/src/demo/pan_zoom.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 8ac49c904f1..4afc966719f 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -44,7 +44,8 @@ impl super::View for PanZoom { // Keep mouse centered. let pan_delta = pointer / new_zoom - pointer / original_zoom; // Handle scrolling. - let pan_delta = pan_delta + ui.ctx().input(|i| i.raw_scroll_delta) / original_zoom; + let pan_delta = + pan_delta + ui.ctx().input(|i| i.smooth_scroll_delta) / original_zoom; self.transform.scaling = new_zoom; self.transform.translation += pan_delta; From ac2ae9683521026a88a17866f9d130f698538def Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Wed, 31 Jan 2024 16:38:32 -0800 Subject: [PATCH 09/34] update docstring in layers.rs --- crates/egui/src/layers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 5515616a865..4bdcb968033 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -158,7 +158,7 @@ impl PaintList { self.0[idx.0].shape = Shape::Noop; } - /// Translate each [`Shape`] and clip rectangle by this much, in-place + /// 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 = transform.mul_rect(*clip_rect); From 998a6e6b1f30e4f0f9cf318fb40200e7fb31205c Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Wed, 31 Jan 2024 16:47:59 -0800 Subject: [PATCH 10/34] fix cranky, style --- crates/emath/src/ts_transform.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/emath/src/ts_transform.rs b/crates/emath/src/ts_transform.rs index 48c6f52144c..3b61865fb81 100644 --- a/crates/emath/src/ts_transform.rs +++ b/crates/emath/src/ts_transform.rs @@ -11,6 +11,7 @@ use crate::{Pos2, Rect, Vec2}; pub struct TSTransform { /// Translation amount. pub translation: Vec2, + /// Scaling amount after translation, scaled around (0, 0). pub scaling: f32, } @@ -20,7 +21,7 @@ impl Eq for TSTransform {} impl Default for TSTransform { #[inline] fn default() -> Self { - TSTransform { + Self { translation: Vec2::ZERO, scaling: 1.0, } From b0d73d0d6fba39a1508ddc38079def186e7f64f0 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Thu, 1 Feb 2024 09:11:12 -0800 Subject: [PATCH 11/34] Update crates/egui/src/context.rs doc comment Co-authored-by: Emil Ernerfeldt --- crates/egui/src/context.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 9aa1aacdb14..c657e62d12c 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2072,7 +2072,9 @@ impl Context { } impl Context { - /// Transform the graphics at the given layer. + /// Transform the graphics of the given layer. + /// + /// 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) { From 18347d36cb443ce5285a36ef351e7d81944547e2 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Thu, 1 Feb 2024 09:45:41 -0800 Subject: [PATCH 12/34] add TSTransform doctests, switch transform order --- crates/egui_demo_lib/src/demo/pan_zoom.rs | 8 ++- crates/emath/src/ts_transform.rs | 71 ++++++++++++++++++----- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 4afc966719f..7b1c83e45b7 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -41,8 +41,14 @@ impl super::View for PanZoom { if response.hovered() { let original_zoom = self.transform.scaling; let new_zoom = original_zoom * ui.ctx().input(|i| i.zoom_delta()); + + let layer_pos = self.transform.invert_pos(pointer); + let new_transform = TSTransform::new(self.transform.translation, new_zoom); + let new_pos = new_transform * layer_pos; + // Keep mouse centered. - let pan_delta = pointer / new_zoom - pointer / original_zoom; + let pan_delta = pointer - new_pos; + // Handle scrolling. let pan_delta = pan_delta + ui.ctx().input(|i| i.smooth_scroll_delta) / original_zoom; diff --git a/crates/emath/src/ts_transform.rs b/crates/emath/src/ts_transform.rs index 3b61865fb81..014b3f0060a 100644 --- a/crates/emath/src/ts_transform.rs +++ b/crates/emath/src/ts_transform.rs @@ -9,11 +9,11 @@ use crate::{Pos2, Rect, Vec2}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] pub struct TSTransform { - /// Translation amount. - pub translation: Vec2, - - /// Scaling amount after translation, scaled around (0, 0). + /// Scaling applied first, scaled around (0, 0). pub scaling: f32, + + /// Translation amount, applied after translation. + pub translation: Vec2, } impl Eq for TSTransform {} @@ -21,15 +21,18 @@ impl Eq for TSTransform {} impl Default for TSTransform { #[inline] fn default() -> Self { - Self { - translation: Vec2::ZERO, - scaling: 1.0, - } + Self::IDENTITY } } impl TSTransform { + pub const IDENTITY: Self = Self { + translation: Vec2::ZERO, + scaling: 1.0, + }; + /// The translation is applied first, then scaling around 0, 0. + #[inline] pub fn new(translation: Vec2, scaling: f32) -> Self { Self { translation, @@ -37,33 +40,73 @@ impl TSTransform { } } + #[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) } /// Reverses the transformation from screen space to layer space. + /// + /// ``` + /// # 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); + /// assert_eq!(ts.invert_pos(p1), pos2(0.0, 0.0)); + /// assert_eq!(ts.invert_pos(p2), pos2(5.0, 1.0)); + /// ``` + #[inline] pub fn invert_pos(&self, pos: Pos2) -> Pos2 { - // First, reverse scale around 0, 0, then reverse transform. - let pos = pos / self.scaling; - pos - self.translation + // First, reverse translation, then reverse scaling. + (pos - self.translation) / self.scaling } /// Reverses the transformation from screen space to layer space. + /// + /// ``` + /// # use emath::{pos2, vec2, Rect, TSTransform}; + /// let rect = Rect::from_min_max(pos2(16.0, 15.0), pos2(46.0, 30.0)); + /// let ts = TSTransform::new(vec2(1.0, 0.0), 3.0); + /// let inverted = ts.invert_rect(rect); + /// assert_eq!(inverted.min, pos2(5.0, 5.0)); + /// assert_eq!(inverted.max, pos2(15.0, 10.0)); + /// ``` + #[inline] pub fn invert_rect(&self, rect: Rect) -> Rect { Rect::from_min_max(self.invert_pos(rect.min), self.invert_pos(rect.max)) } /// Transforms the given coordinate by translation then scaling. + /// + /// ``` + /// # 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 { - let pos = pos + self.translation; - pos * self.scaling + self.scaling * pos + self.translation } /// Transforms the given rectangle by translation then scaling. + /// + /// ``` + /// # 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), @@ -76,6 +119,7 @@ impl TSTransform { impl std::ops::Mul for TSTransform { type Output = Pos2; + #[inline] fn mul(self, pos: Pos2) -> Pos2 { self.mul_pos(pos) } @@ -85,6 +129,7 @@ impl std::ops::Mul for TSTransform { impl std::ops::Mul for TSTransform { type Output = Rect; + #[inline] fn mul(self, rect: Rect) -> Rect { self.mul_rect(rect) } From e593bbfd94f35ded56a4f5f5d2ffc91647c70b13 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Thu, 1 Feb 2024 09:46:12 -0800 Subject: [PATCH 13/34] minor refactoring --- crates/egui/src/context.rs | 25 +++++++++++++++---------- crates/egui/src/memory.rs | 14 +++----------- crates/epaint/src/shape.rs | 5 +++-- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index c657e62d12c..90e72ff2145 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1782,7 +1782,7 @@ impl ContextImpl { let shapes = viewport .graphics - .drain(self.memory.areas().order(), self.memory.layer_transforms()); + .drain(self.memory.areas().order(), &self.memory.layer_transforms); if viewport.input.wants_repaint() { self.request_repaint(ended_viewport_id); @@ -2078,7 +2078,13 @@ impl Context { /// /// 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| m.layer_transforms_mut().insert(layer_id, transform)); + 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. @@ -2106,13 +2112,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 - }; + 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; } @@ -2155,7 +2160,7 @@ impl Context { self.write(|ctx| { let transform = ctx .memory - .layer_transforms() + .layer_transforms .get(&layer_id) .cloned() .unwrap_or_default(); diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 3aaa20265a1..e75cff2bec6 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -88,10 +88,12 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] everything_is_visible: bool, + /// Transforms per layer + pub layer_transforms: HashMap, + // ------------------------------------------------- // Per-viewport: areas: ViewportIdMap, - layer_transforms: HashMap, #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) interactions: ViewportIdMap, @@ -610,16 +612,6 @@ impl Memory { self.areas.entry(self.viewport_id).or_default() } - /// Access layer transformations. - pub fn layer_transforms(&self) -> &HashMap { - &self.layer_transforms - } - - /// Access layer transformations. - pub fn layer_transforms_mut(&mut self) -> &mut HashMap { - &mut self.layer_transforms - } - /// Top-most layer at the given position. pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option { self.areas() diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index bd5d9861b13..0ebb07267b7 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -384,9 +384,10 @@ impl Shape { Self::Text(text_shape) => { text_shape.pos = transform * text_shape.pos; let mut galley = (*text_shape.galley).clone(); - let scale_transform = TSTransform::from_scaling(transform.scaling); for row in &mut galley.rows { - row.visuals.mesh.transform(scale_transform); + for v in &mut row.visuals.mesh.vertices { + v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); + } } text_shape.galley = Arc::new(galley); From 227bd6a2027d2c0c3e52b42e7e2f93f0d8399d1b Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Thu, 1 Feb 2024 09:56:08 -0800 Subject: [PATCH 14/34] add invert method for TSTransform --- crates/egui/src/context.rs | 2 +- crates/egui_demo_lib/src/demo/pan_zoom.rs | 4 ++-- crates/emath/src/ts_transform.rs | 25 +++++------------------ 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 90e72ff2145..b9a233273aa 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2181,7 +2181,7 @@ impl Context { 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.invert_pos(pointer_pos); + let pointer_pos = transform.inverse() * pointer_pos; if let Some(rects) = viewport.layer_rects_prev_frame.get(&layer_id) { for blocking in rects.iter().rev() { if blocking.id == id { diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 7b1c83e45b7..224323194b7 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -42,7 +42,7 @@ impl super::View for PanZoom { let original_zoom = self.transform.scaling; let new_zoom = original_zoom * ui.ctx().input(|i| i.zoom_delta()); - let layer_pos = self.transform.invert_pos(pointer); + let layer_pos = self.transform.inverse() * pointer; let new_transform = TSTransform::new(self.transform.translation, new_zoom); let new_pos = new_transform * layer_pos; @@ -83,7 +83,7 @@ impl super::View for PanZoom { // but may also cover over other windows. .order(egui::Order::Foreground) .show(ui.ctx(), |ui| { - ui.set_clip_rect(self.transform.invert_rect(rect)); + ui.set_clip_rect(self.transform.inverse() * rect); egui::Frame::default() .rounding(egui::Rounding::same(4.0)) .inner_margin(egui::Margin::same(8.0)) diff --git a/crates/emath/src/ts_transform.rs b/crates/emath/src/ts_transform.rs index 014b3f0060a..e6f02cfc4ca 100644 --- a/crates/emath/src/ts_transform.rs +++ b/crates/emath/src/ts_transform.rs @@ -57,28 +57,13 @@ impl 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); - /// assert_eq!(ts.invert_pos(p1), pos2(0.0, 0.0)); - /// assert_eq!(ts.invert_pos(p2), pos2(5.0, 1.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)); /// ``` #[inline] - pub fn invert_pos(&self, pos: Pos2) -> Pos2 { - // First, reverse translation, then reverse scaling. - (pos - self.translation) / self.scaling - } - - /// Reverses the transformation from screen space to layer space. - /// - /// ``` - /// # use emath::{pos2, vec2, Rect, TSTransform}; - /// let rect = Rect::from_min_max(pos2(16.0, 15.0), pos2(46.0, 30.0)); - /// let ts = TSTransform::new(vec2(1.0, 0.0), 3.0); - /// let inverted = ts.invert_rect(rect); - /// assert_eq!(inverted.min, pos2(5.0, 5.0)); - /// assert_eq!(inverted.max, pos2(15.0, 10.0)); - /// ``` - #[inline] - pub fn invert_rect(&self, rect: Rect) -> Rect { - Rect::from_min_max(self.invert_pos(rect.min), self.invert_pos(rect.max)) + pub fn inverse(&self) -> Self { + Self::new(-self.translation / self.scaling, 1.0 / self.scaling) } /// Transforms the given coordinate by translation then scaling. From 5902ffd4f51c21ff1814830fe048b0a0a882b48e Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Thu, 1 Feb 2024 10:00:03 -0800 Subject: [PATCH 15/34] add back translate for mesh. --- crates/epaint/src/mesh.rs | 7 +++++++ crates/epaint/src/shape.rs | 2 ++ 2 files changed, 9 insertions(+) diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index 30b9c5a78f5..787c10d8200 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -268,6 +268,13 @@ impl Mesh { output } + /// Translate location by this much, in-place + pub fn translate(&mut self, delta: Vec2) { + for v in &mut self.vertices { + v.pos += delta; + } + } + /// Transform the mesh in-place with the given transform. pub fn transform(&mut self, transform: TSTransform) { for v in &mut self.vertices { diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 0ebb07267b7..26e655f32d7 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -383,6 +383,8 @@ impl Shape { } Self::Text(text_shape) => { text_shape.pos = transform * text_shape.pos; + + // Scale text: let mut galley = (*text_shape.galley).clone(); for row in &mut galley.rows { for v in &mut row.visuals.mesh.vertices { From 90903245df0a4f2209b9362064dc351be78e031d Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Sat, 10 Feb 2024 10:11:50 -0800 Subject: [PATCH 16/34] Update docstring crates/emath/src/ts_transform.rs Co-authored-by: Emil Ernerfeldt --- crates/emath/src/ts_transform.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/emath/src/ts_transform.rs b/crates/emath/src/ts_transform.rs index e6f02cfc4ca..a0883386a4d 100644 --- a/crates/emath/src/ts_transform.rs +++ b/crates/emath/src/ts_transform.rs @@ -50,7 +50,7 @@ impl TSTransform { Self::new(Vec2::ZERO, scaling) } - /// Reverses the transformation from screen space to layer space. + /// Inverts the transform. /// /// ``` /// # use emath::{pos2, vec2, TSTransform}; From 6178cd8a0b8e6ef233fcc7ae281d0546bdba6113 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Sat, 10 Feb 2024 10:27:48 -0800 Subject: [PATCH 17/34] simplify pan_zoom logic, multiply TSTransforms --- crates/egui_demo_lib/src/demo/pan_zoom.rs | 24 ++++++++++------------- crates/emath/src/ts_transform.rs | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 224323194b7..944c81434f0 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -39,22 +39,18 @@ impl super::View for PanZoom { 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 original_zoom = self.transform.scaling; - let new_zoom = original_zoom * ui.ctx().input(|i| i.zoom_delta()); + let pointer_in_layer = self.transform.inverse() * pointer; + let zoom_delta = ui.ctx().input(|i| i.zoom_delta()); + let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta); - let layer_pos = self.transform.inverse() * pointer; - let new_transform = TSTransform::new(self.transform.translation, new_zoom); - let new_pos = new_transform * layer_pos; + // 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()); - // Keep mouse centered. - let pan_delta = pointer - new_pos; - - // Handle scrolling. - let pan_delta = - pan_delta + ui.ctx().input(|i| i.smooth_scroll_delta) / original_zoom; - - self.transform.scaling = new_zoom; - self.transform.translation += pan_delta; + // Pan: + self.transform = TSTransform::from_translation(pan_delta) * self.transform; } } diff --git a/crates/emath/src/ts_transform.rs b/crates/emath/src/ts_transform.rs index a0883386a4d..5ad97072f14 100644 --- a/crates/emath/src/ts_transform.rs +++ b/crates/emath/src/ts_transform.rs @@ -119,3 +119,23 @@ impl std::ops::Mul for TSTransform { self.mul_rect(rect) } } + +impl std::ops::Mul for TSTransform { + type Output = TSTransform; + + #[inline] + /// ``` + /// # 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: TSTransform) -> Self::Output { + // Apply rhs first. + TSTransform { + scaling: self.scaling * rhs.scaling, + translation: self.translation + self.scaling * rhs.translation, + } + } +} From f1df40abea766a2b8ebdf4a71cae0e523e6855b2 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Sat, 10 Feb 2024 10:43:58 -0800 Subject: [PATCH 18/34] fix TSTransform doc comments --- crates/emath/src/ts_transform.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/emath/src/ts_transform.rs b/crates/emath/src/ts_transform.rs index 5ad97072f14..9dc5afcb971 100644 --- a/crates/emath/src/ts_transform.rs +++ b/crates/emath/src/ts_transform.rs @@ -2,8 +2,8 @@ use crate::{Pos2, Rect, Vec2}; /// Linearly transforms positions via a translation, then a scaling. /// -/// [`TSTransform`] first translates points, then scales them with the scaling origin -/// at `0, 0` (the top left corner) +/// [`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))] @@ -12,7 +12,7 @@ pub struct TSTransform { /// Scaling applied first, scaled around (0, 0). pub scaling: f32, - /// Translation amount, applied after translation. + /// Translation amount, applied after scaling. pub translation: Vec2, } @@ -31,8 +31,9 @@ impl TSTransform { scaling: 1.0, }; - /// The translation is applied first, then scaling around 0, 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, @@ -60,13 +61,15 @@ impl TSTransform { /// 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 by translation then scaling. + /// Transforms the given coordinate. /// /// ``` /// # use emath::{pos2, vec2, TSTransform}; @@ -81,7 +84,7 @@ impl TSTransform { self.scaling * pos + self.translation } - /// Transforms the given rectangle by translation then scaling. + /// Transforms the given rectangle. /// /// ``` /// # use emath::{pos2, vec2, Rect, TSTransform}; @@ -110,7 +113,7 @@ impl std::ops::Mul for TSTransform { } } -/// Transforms the position. +/// Transforms the rectangle. impl std::ops::Mul for TSTransform { type Output = Rect; @@ -124,6 +127,8 @@ impl std::ops::Mul for TSTransform { type Output = TSTransform; #[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); From 05ba7c82cc9f2560335c2ade8555d77a01291608 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Sat, 10 Feb 2024 10:50:04 -0800 Subject: [PATCH 19/34] optimize arcs in transforming galley --- crates/epaint/src/shape.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 1ccc4bce81c..09490ca32a8 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -385,14 +385,12 @@ impl Shape { text_shape.pos = transform * text_shape.pos; // Scale text: - let mut galley = (*text_shape.galley).clone(); + let galley = Arc::make_mut(&mut text_shape.galley); for row in &mut galley.rows { for v in &mut row.visuals.mesh.vertices { v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); } } - - text_shape.galley = Arc::new(galley); } Self::Mesh(mesh) => { mesh.transform(transform); From be325f0f87d07c2e0911fd355004f15b1b7c6e0d Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Sat, 10 Feb 2024 11:00:19 -0800 Subject: [PATCH 20/34] scale thickness of strokes when transforming --- crates/epaint/src/shape.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 09490ca32a8..ceab53d94b7 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -356,6 +356,9 @@ impl Shape { } /// Move the shape by this many points, in-place. + /// + /// If using a [`PaintCallback`], note that only the rect is scaled, so + /// scaling the thickness of shapes within the callback should be considered. pub fn transform(&mut self, transform: TSTransform) { match self { Self::Noop => {} @@ -367,19 +370,23 @@ impl Shape { Self::Circle(circle_shape) => { 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 = transform * *p; } + stroke.width *= transform.scaling; } Self::Path(path_shape) => { for p in &mut path_shape.points { *p = transform * *p; } + path_shape.stroke.width *= transform.scaling; } Self::Rect(rect_shape) => { rect_shape.rect = transform * rect_shape.rect; + rect_shape.stroke.width *= transform.scaling; } Self::Text(text_shape) => { text_shape.pos = transform * text_shape.pos; @@ -399,11 +406,13 @@ impl Shape { 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 = transform * *p; } + cubic_curve.stroke.width *= transform.scaling; } Self::Callback(shape) => { shape.rect = transform * shape.rect; From 0b892d7b9744e4d0b8b552668fb8d86bc4a3e6f3 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Sat, 10 Feb 2024 11:07:01 -0800 Subject: [PATCH 21/34] clarify paintcallback scaling comment --- crates/epaint/src/shape.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index ceab53d94b7..0448bf5f697 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -357,8 +357,8 @@ impl Shape { /// Move the shape by this many points, in-place. /// - /// If using a [`PaintCallback`], note that only the rect is scaled, so - /// scaling the thickness of shapes within the callback should be considered. + /// 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 => {} From 2526e5be0fc9e2f202252be20178bb8d3f31c5c0 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Sat, 10 Feb 2024 11:10:19 -0800 Subject: [PATCH 22/34] fix cranky --- crates/emath/src/ts_transform.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/emath/src/ts_transform.rs b/crates/emath/src/ts_transform.rs index 9dc5afcb971..4a761191bac 100644 --- a/crates/emath/src/ts_transform.rs +++ b/crates/emath/src/ts_transform.rs @@ -123,8 +123,8 @@ impl std::ops::Mul for TSTransform { } } -impl std::ops::Mul for TSTransform { - type Output = TSTransform; +impl std::ops::Mul for TSTransform { + type Output = Self; #[inline] /// Applies the right hand side transform, then the left hand side. @@ -136,9 +136,9 @@ impl std::ops::Mul for TSTransform { /// let ts_combined = TSTransform::new(vec2(2.0, -1.0), 6.0); /// assert_eq!(ts_combined, ts2 * ts1); /// ``` - fn mul(self, rhs: TSTransform) -> Self::Output { + fn mul(self, rhs: Self) -> Self::Output { // Apply rhs first. - TSTransform { + Self { scaling: self.scaling * rhs.scaling, translation: self.translation + self.scaling * rhs.translation, } From 663a1b387f75de07d4c0820b3a59982029665f78 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Thu, 15 Feb 2024 16:16:57 -0800 Subject: [PATCH 23/34] fix drag ratio --- crates/egui_demo_lib/src/demo/pan_zoom.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 944c81434f0..7fda008247b 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -28,8 +28,8 @@ impl super::View for PanZoom { fn ui(&mut self, ui: &mut egui::Ui) { let (id, rect) = ui.allocate_space(ui.available_size()); let response = ui.interact(rect, id, egui::Sense::click_and_drag()); - // Uncomment to allow dragging the background as well. - // self.transform.translation += response.drag_delta() / self.transform.scaling; + // Allow dragging the background as well. + self.transform.translation += response.drag_delta(); // Plot-like reset if response.double_clicked() { From 09c4a50895b4f996a59d76187f52305b64e603f4 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Thu, 15 Feb 2024 16:17:33 -0800 Subject: [PATCH 24/34] apply transform to mouse input positions --- crates/egui/src/context.rs | 6 ++++++ crates/egui/src/response.rs | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 1db432e7fb6..9f410461797 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1247,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 { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 2cef13e93f6..225e0125609 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 = pos.map(|p| transform * p) + } + pos } else { None } From edafee57f79601f2ff0cecab098fa0a34fe7b106 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Thu, 15 Feb 2024 16:17:53 -0800 Subject: [PATCH 25/34] add drag_value example --- crates/egui_demo_lib/src/demo/pan_zoom.rs | 27 +++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 7fda008247b..8752c4b1e48 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -4,6 +4,7 @@ use egui::emath::TSTransform; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PanZoom { transform: TSTransform, + drag_value: f32, } impl Eq for PanZoom {} @@ -55,25 +56,37 @@ impl super::View for PanZoom { } let current_size = ui.min_rect(); - for (pos, msg) in [ + for (id, pos, callback) in [ ( + "a", current_size.left_top() + egui::Vec2::new(10.0, 10.0), - "top left!", + Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!")) + as Box egui::Response>, ), ( + "b", current_size.left_bottom() + egui::Vec2::new(10.0, -10.0), - "bottom left?", + Box::new(|ui: &mut egui::Ui, _| ui.button("bottom left?")), ), ( + "c", current_size.right_bottom() + egui::Vec2::new(-10.0, -10.0), - "right bottom :D", + Box::new(|ui: &mut egui::Ui, _| ui.button("right bottom :D")), ), ( + "d", current_size.right_top() + egui::Vec2::new(-10.0, 10.0), - "right top ):", + Box::new(|ui: &mut egui::Ui, _| ui.button("right top ):")), + ), + ( + "e", + current_size.center(), + Box::new(|ui, state| { + ui.add(egui::Slider::new(&mut state.drag_value, 0.0..=100.0).text("My value")) + }), ), ] { - let id = egui::Area::new(msg) + 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. @@ -87,7 +100,7 @@ impl super::View for PanZoom { .fill(ui.style().visuals.panel_fill) .show(ui, |ui| { ui.style_mut().wrap = Some(false); - ui.button(msg).clicked(); + callback(ui, self) }); }) .response From 7fd7e7cb0680ef2ea56368a66387760f2a50e655 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Thu, 15 Feb 2024 16:29:03 -0800 Subject: [PATCH 26/34] fix formatting --- crates/egui/src/response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 225e0125609..335e7674637 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -407,7 +407,7 @@ impl Response { .ctx .memory(|m| m.layer_transforms.get(&self.layer_id).cloned()) { - pos = pos.map(|p| transform * p) + pos = pos.map(|p| transform * p); } pos } else { From 499492e6d08826122967b07539897fd04a15e33a Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Fri, 16 Feb 2024 10:45:50 -0800 Subject: [PATCH 27/34] fix area: use response (not ctx) pointer delta --- crates/egui/src/containers/area.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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()) From 3c27b3b52b25f6aca2bdf5fa555c76c782c08bd1 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Fri, 16 Feb 2024 10:46:05 -0800 Subject: [PATCH 28/34] refactor hover_pos transform in response.rs --- crates/egui/src/response.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 335e7674637..39c3c0a2009 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -402,14 +402,14 @@ impl Response { #[inline] pub fn hover_pos(&self) -> Option { if self.hovered() { - let mut pos = 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 = pos.map(|p| transform * p); + pos = transform * pos; } - pos + Some(pos) } else { None } From bd465360273e774fac9d1e7f030321b0e5ae2563 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Fri, 16 Feb 2024 10:46:30 -0800 Subject: [PATCH 29/34] add label and link to pan_zoom demo --- crates/egui_demo_lib/src/demo/pan_zoom.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 8752c4b1e48..b660b27d94e 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -27,6 +27,15 @@ impl super::Demo for PanZoom { 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. From b0afe908c31d56d9e2c501bebd444942dd095d22 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Fri, 16 Feb 2024 15:59:22 -0800 Subject: [PATCH 30/34] add tracking inner contents to outer window --- crates/egui_demo_lib/src/demo/pan_zoom.rs | 28 +++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index b660b27d94e..6a56c58a5f4 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -17,8 +17,8 @@ impl super::Demo for PanZoom { fn show(&mut self, ctx: &egui::Context, open: &mut bool) { use super::View as _; let window = egui::Window::new("Pan Zoom") - .default_width(200.0) - .default_height(200.0) + .default_width(300.0) + .default_height(300.0) .vscroll(false) .open(open); window.show(ctx, |ui| self.ui(ui)); @@ -39,17 +39,22 @@ impl super::View for PanZoom { 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. - self.transform.translation += response.drag_delta(); + 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 = self.transform.inverse() * pointer; + 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); @@ -64,32 +69,31 @@ impl super::View for PanZoom { } } - let current_size = ui.min_rect(); for (id, pos, callback) in [ ( "a", - current_size.left_top() + egui::Vec2::new(10.0, 10.0), + egui::Pos2::new(0.0, 0.0), Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!")) as Box egui::Response>, ), ( "b", - current_size.left_bottom() + egui::Vec2::new(10.0, -10.0), + egui::Pos2::new(0.0, 120.0), Box::new(|ui: &mut egui::Ui, _| ui.button("bottom left?")), ), ( "c", - current_size.right_bottom() + egui::Vec2::new(-10.0, -10.0), + egui::Pos2::new(120.0, 120.0), Box::new(|ui: &mut egui::Ui, _| ui.button("right bottom :D")), ), ( "d", - current_size.right_top() + egui::Vec2::new(-10.0, 10.0), + egui::Pos2::new(120.0, 0.0), Box::new(|ui: &mut egui::Ui, _| ui.button("right top ):")), ), ( "e", - current_size.center(), + egui::Pos2::new(60.0, 60.0), Box::new(|ui, state| { ui.add(egui::Slider::new(&mut state.drag_value, 0.0..=100.0).text("My value")) }), @@ -101,7 +105,7 @@ impl super::View for PanZoom { // but may also cover over other windows. .order(egui::Order::Foreground) .show(ui.ctx(), |ui| { - ui.set_clip_rect(self.transform.inverse() * rect); + ui.set_clip_rect(transform.inverse() * rect); egui::Frame::default() .rounding(egui::Rounding::same(4.0)) .inner_margin(egui::Margin::same(8.0)) @@ -114,7 +118,7 @@ impl super::View for PanZoom { }) .response .layer_id; - ui.ctx().set_transform_layer(id, self.transform); + ui.ctx().set_transform_layer(id, transform); } } } From c6ca843628c6d0c736acd5e1d85c0d119f2ec57d Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Fri, 16 Feb 2024 22:23:18 -0800 Subject: [PATCH 31/34] fix text inner bounds --- crates/epaint/src/shape.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 0448bf5f697..ffc7b174be4 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -394,10 +394,14 @@ impl Shape { // 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.transform(transform); From f6851ab81382919f04851d3b50be4ed7ff6c0b35 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Fri, 16 Feb 2024 22:23:24 -0800 Subject: [PATCH 32/34] add painter example --- crates/egui_demo_lib/src/demo/pan_zoom.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 6a56c58a5f4..6aa42179c4a 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -95,6 +95,18 @@ impl super::View for PanZoom { "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")) }), ), From f4ed1046649ef68e0521817cc664542490bea422 Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Fri, 16 Feb 2024 22:50:44 -0800 Subject: [PATCH 33/34] fix pointer interaction for transforms in SidePanel --- crates/egui/src/containers/panel.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 2ae769e7dc0..a99ac533634 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 From b10909304891c5ab55d0ef7ab2b0113d28b0caaa Mon Sep 17 00:00:00 2001 From: Francis Chua Date: Fri, 16 Feb 2024 22:57:06 -0800 Subject: [PATCH 34/34] fix topbottom panel pointer position --- crates/egui/src/containers/panel.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index a99ac533634..705dfd733cd 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -716,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