From 89b6055f9cc8954e0a27d17bae2d10f089592330 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 10 Sep 2024 11:04:13 +0200 Subject: [PATCH] Add `Ui::with_visual_transform` (#5055) * [X] I have followed the instructions in the PR template This allows you to transform widgets without having to put them on a new layer. Example usage: https://github.com/user-attachments/assets/6b547782-f15e-42ce-835f-e8febe8d2d65 ```rust use eframe::egui; use eframe::egui::{Button, Frame, InnerResponse, Label, Pos2, RichText, UiBuilder, Widget}; use eframe::emath::TSTransform; use eframe::NativeOptions; use egui::{CentralPanel, Sense, WidgetInfo}; pub fn main() -> eframe::Result { eframe::run_simple_native("focus test", NativeOptions::default(), |ctx, _frame| { CentralPanel::default().show(ctx, |ui| { let response = ui.ctx().read_response(ui.next_auto_id()); let pressed = response .as_ref() .is_some_and(|r| r.is_pointer_button_down_on()); let hovered = response.as_ref().is_some_and(|r| r.hovered()); let target_scale = match (pressed, hovered) { (true, _) => 0.94, (_, true) => 1.06, _ => 1.0, }; let scale = ui .ctx() .animate_value_with_time(ui.id().with("Down"), target_scale, 0.1); let mut center = response .as_ref() .map(|r| r.rect.center()) .unwrap_or_else(|| Pos2::new(0.0, 0.0)); if center.any_nan() { center = Pos2::new(0.0, 0.0); } let transform = TSTransform::from_translation(center.to_vec2()) * TSTransform::from_scaling(scale) * TSTransform::from_translation(-center.to_vec2()); ui.with_visual_transform(transform, |ui| { Button::new(RichText::new("Yaaaay").size(20.0)) .sense(Sense::click()) .ui(ui) }); }); }) } ``` --- crates/egui/src/layers.rs | 14 +++++++++++++- crates/egui/src/ui.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 3a657ba013f..66bbd338387 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -124,10 +124,14 @@ impl PaintList { self.0.is_empty() } + pub fn next_idx(&self) -> ShapeIdx { + ShapeIdx(self.0.len()) + } + /// Returns the index of the new [`Shape`] that can be used with `PaintList::set`. #[inline(always)] pub fn add(&mut self, clip_rect: Rect, shape: Shape) -> ShapeIdx { - let idx = ShapeIdx(self.0.len()); + let idx = self.next_idx(); self.0.push(ClippedShape { clip_rect, shape }); idx } @@ -171,6 +175,14 @@ impl PaintList { } } + /// Transform each [`Shape`] and clip rectangle in range by this much, in-place + pub fn transform_range(&mut self, start: ShapeIdx, end: ShapeIdx, transform: TSTransform) { + for ClippedShape { clip_rect, shape } in &mut self.0[start.0..end.0] { + *clip_rect = transform.mul_rect(*clip_rect); + shape.transform(transform); + } + } + /// Read-only access to all held shapes. pub fn all_entries(&self) -> impl ExactSizeIterator { self.0.iter() diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index ac31bf4856c..0a16bafe5cd 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2690,6 +2690,33 @@ impl Ui { (InnerResponse { inner, response }, payload) } + + /// Create a new Scope and transform its contents via a [`emath::TSTransform`]. + /// This only affects visuals, inputs will not be transformed. So this is mostly useful + /// to create visual effects on interactions, e.g. scaling a button on hover / click. + /// + /// Check out [`Context::set_transform_layer`] for a persistent transform that also affects + /// inputs. + pub fn with_visual_transform( + &mut self, + transform: emath::TSTransform, + add_contents: impl FnOnce(&mut Self) -> R, + ) -> InnerResponse { + let start_idx = self.ctx().graphics(|gx| { + gx.get(self.layer_id()) + .map_or(crate::layers::ShapeIdx(0), |l| l.next_idx()) + }); + + let r = self.scope_dyn(UiBuilder::new(), Box::new(add_contents)); + + self.ctx().graphics_mut(|g| { + let list = g.entry(self.layer_id()); + let end_idx = list.next_idx(); + list.transform_range(start_idx, end_idx, transform); + }); + + r + } } /// # Menus