Skip to content

Commit

Permalink
Add layer transforms, interaction in layer (#3906)
Browse files Browse the repository at this point in the history
⚠️ 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 <[email protected]>
Co-authored-by: Emil Ernerfeldt <[email protected]>
  • Loading branch information
3 people authored Feb 17, 2024
1 parent f7fc3b0 commit 069d7a6
Show file tree
Hide file tree
Showing 15 changed files with 441 additions and 35 deletions.
2 changes: 1 addition & 1 deletion crates/egui/src/containers/area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
16 changes: 16 additions & 0 deletions crates/egui/src/containers/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -708,6 +716,14 @@ impl TopBottomPanel {
.ctx()
.layer_id_at(pointer)
.map_or(true, |top_layer_id| top_layer_id == ui.layer_id());
let pointer = if let Some(transform) = ui
.ctx()
.memory(|m| m.layer_transforms.get(&ui.layer_id()).cloned())
{
transform.inverse() * pointer
} else {
pointer
};

let resize_y = side.opposite().side_y(panel_rect);
let mouse_over_resize_line = we_are_on_top
Expand Down
46 changes: 38 additions & 8 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};

use ahash::HashMap;
use epaint::{mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *};
use epaint::{
emath::TSTransform, mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *,
};

use crate::{
animation_manager::AnimationManager,
Expand Down Expand Up @@ -1245,6 +1247,12 @@ impl Context {
let is_interacted_with = res.is_pointer_button_down_on || clicked || res.drag_released;
if is_interacted_with {
res.interact_pointer_pos = input.pointer.interact_pos();
if let (Some(transform), Some(pos)) = (
memory.layer_transforms.get(&res.layer_id),
&mut res.interact_pointer_pos,
) {
*pos = transform.inverse() * *pos;
}
}

if input.pointer.any_down() && !res.is_pointer_button_down_on {
Expand Down Expand Up @@ -1958,7 +1966,9 @@ impl ContextImpl {
}
}

let shapes = viewport.graphics.drain(self.memory.areas().order());
let shapes = viewport
.graphics
.drain(self.memory.areas().order(), &self.memory.layer_transforms);

let mut repaint_needed = false;

Expand Down Expand Up @@ -2266,13 +2276,19 @@ impl Context {
}

impl Context {
/// Move all the graphics at the given layer.
/// Transform the graphics of the given layer.
///
/// Can be used to implement drag-and-drop (see relevant demo).
pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) {
if delta != Vec2::ZERO {
self.graphics_mut(|g| g.entry(layer_id).translate(delta));
}
/// This is a sticky setting, remembered from one frame to the next.
///
/// Can be used to implement pan and zoom (see relevant demo).
pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) {
self.memory_mut(|m| {
if transform == TSTransform::IDENTITY {
m.layer_transforms.remove(&layer_id)
} else {
m.layer_transforms.insert(layer_id, transform)
}
});
}

/// Top-most layer at the given position.
Expand Down Expand Up @@ -2302,6 +2318,12 @@ impl Context {
///
/// See also [`Response::contains_pointer`].
pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool {
let rect =
if let Some(transform) = self.memory(|m| m.layer_transforms.get(&layer_id).cloned()) {
transform * rect
} else {
rect
};
if !rect.is_positive() {
return false;
}
Expand Down Expand Up @@ -2348,6 +2370,12 @@ impl Context {
let mut blocking_widget = None;

self.write(|ctx| {
let transform = ctx
.memory
.layer_transforms
.get(&layer_id)
.cloned()
.unwrap_or_default();
let viewport = ctx.viewport();

// We add all widgets here, even non-interactive ones,
Expand All @@ -2367,6 +2395,8 @@ impl Context {
if contains_pointer {
let pointer_pos = viewport.input.pointer.interact_pos();
if let Some(pointer_pos) = pointer_pos {
// Apply the inverse transformation of this layer to the pointer pos.
let pointer_pos = transform.inverse() * pointer_pos;
if let Some(rects) = viewport.layer_rects_prev_frame.by_layer.get(&layer_id) {
// Iterate backwards, i.e. topmost widgets first.
for blocking in rects.iter().rev() {
Expand Down
22 changes: 16 additions & 6 deletions crates/egui/src/layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! are sometimes painted behind or in front of other things.
use crate::{Id, *};
use epaint::{ClippedShape, Shape};
use epaint::{emath::TSTransform, ClippedShape, Shape};

/// Different layer categories
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
Expand Down Expand Up @@ -158,11 +158,11 @@ impl PaintList {
self.0[idx.0].shape = Shape::Noop;
}

/// Translate each [`Shape`] and clip rectangle by this much, in-place
pub fn translate(&mut self, delta: Vec2) {
/// Transform each [`Shape`] and clip rectangle by this much, in-place
pub fn transform(&mut self, transform: TSTransform) {
for ClippedShape { clip_rect, shape } in &mut self.0 {
*clip_rect = clip_rect.translate(delta);
shape.translate(delta);
*clip_rect = transform.mul_rect(*clip_rect);
shape.transform(transform);
}
}

Expand Down Expand Up @@ -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<ClippedShape> {
pub fn drain(
&mut self,
area_order: &[LayerId],
transforms: &ahash::HashMap<LayerId, TSTransform>,
) -> Vec<ClippedShape> {
crate::profile_function!();

let mut all_shapes: Vec<_> = Default::default();
Expand All @@ -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);
}
}
Expand Down
21 changes: 19 additions & 2 deletions crates/egui/src/memory.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs

use ahash::HashMap;
use epaint::emath::TSTransform;

use crate::{
area, vec2,
window::{self, WindowInteraction},
Expand Down Expand Up @@ -85,6 +88,9 @@ pub struct Memory {
#[cfg_attr(feature = "persistence", serde(skip))]
everything_is_visible: bool,

/// Transforms per layer
pub layer_transforms: HashMap<LayerId, TSTransform>,

// -------------------------------------------------
// Per-viewport:
areas: ViewportIdMap<Areas>,
Expand All @@ -107,6 +113,7 @@ impl Default for Memory {
viewport_id: Default::default(),
window_interactions: Default::default(),
areas: Default::default(),
layer_transforms: Default::default(),
popup: Default::default(),
everything_is_visible: Default::default(),
};
Expand Down Expand Up @@ -672,7 +679,8 @@ impl Memory {

/// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<LayerId> {
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.
Expand Down Expand Up @@ -948,7 +956,12 @@ impl Areas {
}

/// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<LayerId> {
pub fn layer_id_at(
&self,
pos: Pos2,
resize_interact_radius_side: f32,
layer_transforms: &HashMap<LayerId, TSTransform>,
) -> Option<LayerId> {
for layer in self.order.iter().rev() {
if self.is_visible(layer) {
if let Some(state) = self.areas.get(&layer.id) {
Expand All @@ -959,6 +972,10 @@ impl Areas {
rect = rect.expand(resize_interact_radius_side);
}

if let Some(transform) = layer_transforms.get(layer) {
rect = *transform * rect;
}

if rect.contains(pos) {
return Some(*layer);
}
Expand Down
18 changes: 16 additions & 2 deletions crates/egui/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -395,7 +402,14 @@ impl Response {
#[inline]
pub fn hover_pos(&self) -> Option<Pos2> {
if self.hovered() {
self.ctx.input(|i| i.pointer.hover_pos())
let mut pos = self.ctx.input(|i| i.pointer.hover_pos())?;
if let Some(transform) = self
.ctx
.memory(|m| m.layer_transforms.get(&self.layer_id).cloned())
{
pos = transform * pos;
}
Some(pos)
} else {
None
}
Expand Down
3 changes: 2 additions & 1 deletion crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2176,7 +2176,8 @@ impl Ui {

if let Some(pointer_pos) = self.ctx().pointer_interact_pos() {
let delta = pointer_pos - response.rect.center();
self.ctx().translate_layer(layer_id, delta);
self.ctx()
.set_transform_layer(layer_id, emath::TSTransform::from_translation(delta));
}

InnerResponse::new(inner, response)
Expand Down
1 change: 1 addition & 0 deletions crates/egui_demo_lib/src/demo/demo_app_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions crates/egui_demo_lib/src/demo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 069d7a6

Please sign in to comment.