egui icon indicating copy to clipboard operation
egui copied to clipboard

A panning / zooming container

Open setzer22 opened this issue 3 years ago • 6 comments

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 :sweat_smile: 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 avatar Jul 08 '22 06:07 setzer22

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.

edulecom avatar Jul 08 '22 10:07 edulecom

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

Barugon avatar Aug 06 '22 17:08 Barugon

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

setzer22 avatar Aug 09 '22 16:08 setzer22

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

Barugon avatar Aug 09 '22 19:08 Barugon

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.

setzer22 avatar Aug 11 '22 09:08 setzer22

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.

Barugon avatar Aug 11 '22 13:08 Barugon

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 avatar Sep 28 '22 07:09 gzp79

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

gzp79 avatar Oct 03 '22 13:10 gzp79

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.

juancampa avatar Jun 29 '23 15:06 juancampa

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.

https://github.com/emilk/egui/blob/9c4f55b1f4c87362decada09dffece832eea40aa/crates/epaint/src/mesh.rs#L271-L286

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.

YgorSouza avatar Sep 17 '23 21:09 YgorSouza

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

setzer22 avatar Sep 23 '23 13:09 setzer22

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!

Engid avatar Oct 06 '23 19:10 Engid

Hi @Engid :wave:

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.

setzer22 avatar Oct 09 '23 10:10 setzer22

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.

Tweoss avatar Feb 01 '24 00:02 Tweoss