1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Add layer transforms, interaction in layer (#3906)

⚠️ 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 <fchua@puffer5.stanford.edu>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Francis Chua
2024-02-17 02:02:56 -08:00
committed by GitHub
parent f7fc3b0154
commit 069d7a634d
15 changed files with 441 additions and 35 deletions

View File

@@ -32,6 +32,7 @@ impl Default for Demos {
Box::<super::MiscDemoWindow>::default(),
Box::<super::multi_touch::MultiTouch>::default(),
Box::<super::painting::Painting>::default(),
Box::<super::pan_zoom::PanZoom>::default(),
Box::<super::panels::Panels>::default(),
Box::<super::plot_demo::PlotDemo>::default(),
Box::<super::scrolling::Scrolling>::default(),

View File

@@ -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;

View File

@@ -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<dyn Fn(&mut egui::Ui, &mut Self) -> 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);
}
}
}