mirror of
https://github.com/emilk/egui.git
synced 2026-09-03 23:30:04 -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:
@@ -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())
|
||||
|
||||
@@ -247,6 +247,14 @@ impl SidePanel {
|
||||
.ctx()
|
||||
.layer_id_at(pointer)
|
||||
.map_or(true, |top_layer_id| top_layer_id == ui.layer_id());
|
||||
let pointer = if let Some(transform) = ui
|
||||
.ctx()
|
||||
.memory(|m| m.layer_transforms.get(&ui.layer_id()).cloned())
|
||||
{
|
||||
transform.inverse() * pointer
|
||||
} else {
|
||||
pointer
|
||||
};
|
||||
|
||||
let resize_x = side.opposite().side_x(panel_rect);
|
||||
let mouse_over_resize_line = we_are_on_top
|
||||
@@ -708,6 +716,14 @@ impl TopBottomPanel {
|
||||
.ctx()
|
||||
.layer_id_at(pointer)
|
||||
.map_or(true, |top_layer_id| top_layer_id == ui.layer_id());
|
||||
let pointer = if let Some(transform) = ui
|
||||
.ctx()
|
||||
.memory(|m| m.layer_transforms.get(&ui.layer_id()).cloned())
|
||||
{
|
||||
transform.inverse() * pointer
|
||||
} else {
|
||||
pointer
|
||||
};
|
||||
|
||||
let resize_y = side.opposite().side_y(panel_rect);
|
||||
let mouse_over_resize_line = we_are_on_top
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};
|
||||
|
||||
use ahash::HashMap;
|
||||
use epaint::{mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *};
|
||||
use epaint::{
|
||||
emath::TSTransform, mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
animation_manager::AnimationManager,
|
||||
@@ -1245,6 +1247,12 @@ impl Context {
|
||||
let is_interacted_with = res.is_pointer_button_down_on || clicked || res.drag_released;
|
||||
if is_interacted_with {
|
||||
res.interact_pointer_pos = input.pointer.interact_pos();
|
||||
if let (Some(transform), Some(pos)) = (
|
||||
memory.layer_transforms.get(&res.layer_id),
|
||||
&mut res.interact_pointer_pos,
|
||||
) {
|
||||
*pos = transform.inverse() * *pos;
|
||||
}
|
||||
}
|
||||
|
||||
if input.pointer.any_down() && !res.is_pointer_button_down_on {
|
||||
@@ -1958,7 +1966,9 @@ impl ContextImpl {
|
||||
}
|
||||
}
|
||||
|
||||
let shapes = viewport.graphics.drain(self.memory.areas().order());
|
||||
let shapes = viewport
|
||||
.graphics
|
||||
.drain(self.memory.areas().order(), &self.memory.layer_transforms);
|
||||
|
||||
let mut repaint_needed = false;
|
||||
|
||||
@@ -2266,13 +2276,19 @@ impl Context {
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Move all the graphics at the given layer.
|
||||
/// Transform the graphics of the given layer.
|
||||
///
|
||||
/// Can be used to implement drag-and-drop (see relevant demo).
|
||||
pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) {
|
||||
if delta != Vec2::ZERO {
|
||||
self.graphics_mut(|g| g.entry(layer_id).translate(delta));
|
||||
}
|
||||
/// This is a sticky setting, remembered from one frame to the next.
|
||||
///
|
||||
/// Can be used to implement pan and zoom (see relevant demo).
|
||||
pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) {
|
||||
self.memory_mut(|m| {
|
||||
if transform == TSTransform::IDENTITY {
|
||||
m.layer_transforms.remove(&layer_id)
|
||||
} else {
|
||||
m.layer_transforms.insert(layer_id, transform)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Top-most layer at the given position.
|
||||
@@ -2302,6 +2318,12 @@ impl Context {
|
||||
///
|
||||
/// See also [`Response::contains_pointer`].
|
||||
pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool {
|
||||
let rect =
|
||||
if let Some(transform) = self.memory(|m| m.layer_transforms.get(&layer_id).cloned()) {
|
||||
transform * rect
|
||||
} else {
|
||||
rect
|
||||
};
|
||||
if !rect.is_positive() {
|
||||
return false;
|
||||
}
|
||||
@@ -2348,6 +2370,12 @@ impl Context {
|
||||
let mut blocking_widget = None;
|
||||
|
||||
self.write(|ctx| {
|
||||
let transform = ctx
|
||||
.memory
|
||||
.layer_transforms
|
||||
.get(&layer_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let viewport = ctx.viewport();
|
||||
|
||||
// We add all widgets here, even non-interactive ones,
|
||||
@@ -2367,6 +2395,8 @@ impl Context {
|
||||
if contains_pointer {
|
||||
let pointer_pos = viewport.input.pointer.interact_pos();
|
||||
if let Some(pointer_pos) = pointer_pos {
|
||||
// Apply the inverse transformation of this layer to the pointer pos.
|
||||
let pointer_pos = transform.inverse() * pointer_pos;
|
||||
if let Some(rects) = viewport.layer_rects_prev_frame.by_layer.get(&layer_id) {
|
||||
// Iterate backwards, i.e. topmost widgets first.
|
||||
for blocking in rects.iter().rev() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! are sometimes painted behind or in front of other things.
|
||||
|
||||
use crate::{Id, *};
|
||||
use epaint::{ClippedShape, Shape};
|
||||
use epaint::{emath::TSTransform, ClippedShape, Shape};
|
||||
|
||||
/// Different layer categories
|
||||
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
|
||||
@@ -158,11 +158,11 @@ impl PaintList {
|
||||
self.0[idx.0].shape = Shape::Noop;
|
||||
}
|
||||
|
||||
/// Translate each [`Shape`] and clip rectangle by this much, in-place
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
/// Transform each [`Shape`] and clip rectangle by this much, in-place
|
||||
pub fn transform(&mut self, transform: TSTransform) {
|
||||
for ClippedShape { clip_rect, shape } in &mut self.0 {
|
||||
*clip_rect = clip_rect.translate(delta);
|
||||
shape.translate(delta);
|
||||
*clip_rect = transform.mul_rect(*clip_rect);
|
||||
shape.transform(transform);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +194,11 @@ impl GraphicLayers {
|
||||
self.0[layer_id.order as usize].get_mut(&layer_id.id)
|
||||
}
|
||||
|
||||
pub fn drain(&mut self, area_order: &[LayerId]) -> Vec<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();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs
|
||||
|
||||
use ahash::HashMap;
|
||||
use epaint::emath::TSTransform;
|
||||
|
||||
use crate::{
|
||||
area, vec2,
|
||||
window::{self, WindowInteraction},
|
||||
@@ -85,6 +88,9 @@ pub struct Memory {
|
||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
everything_is_visible: bool,
|
||||
|
||||
/// Transforms per layer
|
||||
pub layer_transforms: HashMap<LayerId, TSTransform>,
|
||||
|
||||
// -------------------------------------------------
|
||||
// Per-viewport:
|
||||
areas: ViewportIdMap<Areas>,
|
||||
@@ -107,6 +113,7 @@ impl Default for Memory {
|
||||
viewport_id: Default::default(),
|
||||
window_interactions: Default::default(),
|
||||
areas: Default::default(),
|
||||
layer_transforms: Default::default(),
|
||||
popup: Default::default(),
|
||||
everything_is_visible: Default::default(),
|
||||
};
|
||||
@@ -672,7 +679,8 @@ impl Memory {
|
||||
|
||||
/// Top-most layer at the given position.
|
||||
pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<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.
|
||||
@@ -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) {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user