Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A panning / zooming container #1811

Closed
setzer22 opened this issue Jul 8, 2022 · 14 comments · Fixed by #3906
Closed

A panning / zooming container #1811

setzer22 opened this issue Jul 8, 2022 · 14 comments · Fixed by #3906
Labels
feature New feature or request

Comments

@setzer22
Copy link
Contributor

setzer22 commented Jul 8, 2022

Is your feature request related to a problem? Please describe.
I work on the egui_node_graph library, which can be used to create node-graph based applications in egui. One particular feature of these kind of applications is being able to zoom and pan the graph view, as showcased in the gif below. To this date, I don't think that's possible to implement in egui in a portable way, but I managed to make it work for a specific integration (winit + wgpu) and a bunch of hacks.

I believe this can also be an interesting feature in other kinds of visualization-heavy applications: Right now there are things like the Plot view, which have this behavior available, but being able to use the full power of egui in a Zoomable / Pannable container feels like it would be the cleanest solution in many situations.

Describe the solution you'd like
It would be great if egui could offer a portable solution to create some sort of container in which you can apply pan and zoom. Tentatively, the API could look something like this?:

egui::PanZoomContainer::new("graph_area")
    .with_pan(current_pan)
    .with_zoom(current_zoom)
    .show(|ui| {
        // Here, you get a `ui` disconnected from the parent, with an infinite canvas to draw on.
    });

Describe alternatives you've considered
The current alternative, which is working for me but is highly non-portable and requires a bunch of patches on top of egui. The core idea is that I build two egui contexts, the one with the graph is rendered into a texture, and then that texture is drawn in the "parent" context as an egui::Image. This requires lots of messing around with the pixels_per_point, the offsets of the nodes and the quads that are being sent to wgpu (in the integration) in a very precise way to prevent visual glitches. I also intercept winit events that go into the child instance to offset mouse positions and such: Not fun at all, as one might imagine 😅 Nothing would make me happier than being able to throw away all this code.

I'm not sure I know enough about egui internals to see how a good implementation for this might look, but I assume it's possible, since egui is doing the rasterization: It would need to make sure it scales and offsets any meshes inside the Pan/Zoom area. It should also make sure a high-resolution texture with the fonts is available, since scaling up the fonts might look blurry otherwise.

Additional context
egui_zoom

@setzer22 setzer22 added the feature New feature or request label Jul 8, 2022
@edulecom
Copy link

edulecom commented Jul 8, 2022

I would like to use egui_node_graph library in my project. The graph can be very big and for viewing and navigating zoom would be of great help to users. Without seeing the whole graph it is much harder to track all links/dependencies.

@Barugon
Copy link
Contributor

Barugon commented Aug 6, 2022

This can be done using a ScrollArea. See this code as an example.

@setzer22
Copy link
Contributor Author

setzer22 commented Aug 9, 2022

This can be done using a ScrollArea. See this code as an example.

@Barugon Not sure I follow. I understand ScrollArea lets you do panning, but does it allow zooming? What I mean is zooming in a way that all widgets and text would be scaled uniformly and without glitches, and regardless of whether they obey properties like spacing

@Barugon
Copy link
Contributor

Barugon commented Aug 9, 2022

@Barugon Not sure I follow. I understand ScrollArea lets you do panning, but does it allow zooming? What I mean is zooming in a way that all widgets and text would be scaled uniformly and without glitches, and regardless of whether they obey properties like spacing

Yeah, that might be pretty tricky. Maybe you could use Context::set_pixels_per_point for scaling the UI?

@setzer22
Copy link
Contributor Author

setzer22 commented Aug 11, 2022

Yeah, that might be pretty tricky. Maybe you could use Context::set_pixels_per_point for scaling the UI?

Changing pixels per point would scale all the UI uniformly. You can think of this feature as a way to change pixels_per_point only for elements drawn inside a container. As far as I know, there is no way to achieve this in egui as it is today, because pixels_per_point is a global property and the layout engine doesn't have a notion of varying pixels_per_point.

@Barugon
Copy link
Contributor

Barugon commented Aug 11, 2022

Yeah, that might be pretty tricky. Maybe you could use Context::set_pixels_per_point for scaling the UI?

Changing pixels per point would scale all the UI uniformly. You can think of this feature as a way to change pixels_per_point only for elements drawn inside a container. As far as I know, there is no way to achieve this in egui as it is today, because pixels_per_point is a global property and the layout engine doesn't have a notion of varying pixels_per_point.

Yeah, I tried it and it won't work. egui needs some way to change an inner ui scale.

@gzp79
Copy link

gzp79 commented Sep 28, 2022

FYI: Another option that seems to be working for me without altering the pixel_per_point is

  • create scaled style and set it for the ctx before drawing the nodes (and don't forget to reset it after)
    • scale font size, margins, sizes in the whole Style struct.
  • draw nodes in an Area
    • store the location of the nodes in some graph space and apply pan/zoom transformation on it by setting the current_pos and updating location after dragging
    • set the clip_rect for the ui before calling the node rendering

It seems to be working quite well. The only issue I found is with some Resize widgets as they seem to preserve some maximum size after the zoom in/out and it creates some glitches. I guess this is the reason I could not use a window and had to fall back to Area as the main container for the graph nodes.

@gzp79
Copy link

gzp79 commented Oct 3, 2022

FYI: https://github.com/gzp-crey/shine
here is a POC version of the above concept highly inspired by https://github.com/setzer22/blackjack

sample

@juancampa
Copy link
Contributor

Another potential API for this would be to add a transform property to layers. Layers already support translation, but maybe this concept can be generalized to a more general transform i.e. a matrix.

One limitation of translate_layer is that it breaks input positions, so we would have to somehow transform the input with the inverse matrix when interacting with these layers.

@YgorSouza
Copy link
Contributor

The Mesh has translate and rotate methods. Adding a scale method is trivial, and a matrix transform is probably fine as well, with something like nalgebra.

/// Translate location by this much, in-place
pub fn translate(&mut self, delta: Vec2) {
for v in &mut self.vertices {
v.pos += delta;
}
}
/// Rotate by some angle about an origin, in-place.
///
/// Origin is a position in screen space.
pub fn rotate(&mut self, rot: Rot2, origin: Pos2) {
for v in &mut self.vertices {
v.pos = origin + rot * (v.pos - origin);
}
}
}

Calling these methods after tessellating the shapes (i.e., at the end of this method) allows us to do some interesting things.

Screencast.from.2023-09-17.21-55-10.webm

But to use that for a pan/zoom container, the code would have to be refactored to identify which mesh/shape is in which layer, to apply the appropriate transforms only to that layer (or each shape would have to have a copy of the transform). And the pointer coordinates within the UI corresponding to the transformed layer would have to be remapped with the inverse transform, so any widget within the layer would catch the events correctly. So I guess this method would have to update the transform matrix instead of applying the translation directly to the shapes within, so the same transform could be used to remap the pointer and then to translate/rotate/scale the meshes.

Also, the clip_rect from each shape would have to be transformed as well, somehow. But I guess if we forego rotation and arbitrary matrix transforms and focus on the translate/scale that the issue asks for, the clip_rect problem becomes trivial. In fact, in that case the transform could be stored in each shape as a second Rect (maybe called transform_rect), that is initially set to ((0,0), (1,1)), and is modified by each call to translate() or scale(), and then taken into account when tessellating the shape. I'll try to test that out when I have some time.

@setzer22
Copy link
Contributor Author

@YgorSouza That takes care of display, but you also need to consider about event handling. In egui, widgets do event handling at draw time, so even if you could scale / translate the end result, you wouldn't be able to click any buttons in the scaled version.

@Engid
Copy link

Engid commented Oct 6, 2023

Hey @setzer22 , I'm interested in this issue, but I'm trying to follow the development. It looks like some things changed in egui 0.19, per this PR for blackjack, but I'm not sure what you were able to change (the PR is pretty big and not obvious to me). What is still missing in egui? Thank you!

@setzer22
Copy link
Contributor Author

setzer22 commented Oct 9, 2023

Hi @Engid 👋

What is still missing in egui?

I've been a bit out of the loop as of late, but as far as I understand, there has been no movement on the egui side. There's still no supported way to zoom or pan a UI container. The thing I have for blackjack is still the hack you can see described at the top of this post. The PR you link to helped clear some of the nasty parts in it, but the core of the implementation (two egui contexts, spoof winit events...) remains just as I described above.

I am currently working on migrating the blackjack UI to a lower level architecture based on top of epaint. This is the solution I found that works for me, since I can benefit from the nice drawing APIs in egui as well as the wide back-end support while getting more control over how primitives are drawn.

@Tweoss
Copy link
Contributor

Tweoss commented Feb 1, 2024

I think the linked PR should close this issue. As in the added pan+zoom demo, we will be able to translate and scale individual layers. This does mean each and every area/window will need to have the transformation applied to it (since the granularity is by LayerId). Pointer interactions are reverse transformed into layer space to be handled correctly.

emilk added a commit that referenced this issue Feb 17, 2024
⚠️ 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>
hacknus pushed a commit to hacknus/egui that referenced this issue Oct 30, 2024
⚠️ 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 emilk#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants