egui icon indicating copy to clipboard operation
egui copied to clipboard

Z-ordering of shapes

Open emilk opened this issue 3 years ago • 4 comments

Problem

egui and epaint are currently based on "painters algorithm", i.e. painting things from back-to-front in order to get some shapes to cover others. This means when painting widgets you need to do so back-to-front. Sometimes this is difficult, especially since immediate mode ties interaction and painting together. Sometimes you want to paint things that go in the background last, or things that go in the foreground first.

For instance, when painting the egui::Frame we need to wait until the contents has been added before we know how big to paint the frame. So the code adds a dummy Shape::Noop and gets a ShapeIdx where the background it later put. This is a bit ugly.

Proposed solution

One way to make this less cumbersome is to introduce a Z value for each Shape. This could be an integer or float, and shapes with a lower Z will always be behind those with a higher Z. Shapes with the same Z are painted in the order they are added.

This could be implemented by having PaintList store Vec<(ZIndex, ClippedShape)>, where ZIndex is a a typedef for e.g. i32. Each PaintList would do a stable sort on the Z value before draining in GraphicLayers::drain.

egui::Painter could have a setting for which Z value it uses so that one can set it for a Painter and then pass the Painter to a function that does the painting, with that function being agnostic to what Z it is painting to (just as it is agnostic to what paint layer it is painting to).

Questions

What type for Z index: i32, i64, f32, f64?

Should we have standard indexes for foreground and background?

What are the performance implications of this?

Should we still have separate PaintLists in GraphicLayers, or just put the LayerId together with ZIndex and ClippedShape and stable-sort on that too while we're at it?

Applications

  • Separator lines between panels should be on top of the panel contents
  • Window shadows should be behind all windows in the same layer (e.g. behind all popups)

emilk avatar Apr 19 '22 12:04 emilk

Hi! I'd like to tackle this. I think this would help simplify things a lot. As you say, widget call order is coupling together interaction and draw order in a way that makes some things more cumbersome than they should.

The plan you outline above sounds good. But I have a few questions / comments.

First, how is this going to interact with the order in the Areas memory object? Right now, GraphicLayers::drain is special cased for areas and those are ordered using their LayerIds. Would the layer ordering mechanism be kept as-is, and z-index only used to sort things within a layer?

What type for Z index: i32, i64, f32, f64?

Float sounds like the most flexible, because you can always sneak in a shape between every pair of shapes (as long as you're not within precision limits, but it's still better than an integer). As for 32 or 64 bits, I'm ambivalent. Egui seems to be using f32 everywhere else (Pos2, Rect, ...) so it sounds like f32 might be a good option here too.

Should we still have separate PaintLists in GraphicLayers, or just put the LayerId together with ZIndex and ClippedShape and stable-sort on that too while we're at it?

I assume you mean sorting on a compound key (LayerId, ZIndex), i.e. sorting first by LayerId, then by ZIndex? I'm not fully sure about the performance implications of that. Sounds like the hierarchical approach (i.e. as it is right now) would imply sorting more times, but for smaller lists. Can't say much without benchmarking.

What are the performance implications of this?

Presumably, there will be a performance hit. I'm not sure if trying to keep things sorted incrementally (by not using a Vec, but some other data structure that allows fast insertion and keeps things sorted) would help soften the blow. Again, can't say much without benchmarking.

I'm back-linking to the issue from my repo so people can track discussion: https://github.com/setzer22/egui_node_graph/issues/71.

setzer22 avatar Nov 14 '22 12:11 setzer22

Thanks for thinking about this @setzer22!

As you notice, most of the work is design rather than implementation :)

This is all related to https://github.com/emilk/egui/pull/2244 (overlapping widgets) too. Ideally we should be able to add widgets to the same Window to different ZIndex in any order, and have the top ZIndex get the interaction.

I don't have a helpful plan yet (and don't really have time to think about it this week), but this feature is something I really want to get solved.

Another use case for this feature is the resize-edges of Panels, which should be painted on top of all other panels, but below windows.

emilk avatar Nov 16 '22 11:11 emilk

Thanks! :smile:

As you notice, most of the work is design rather than implementation :)

With some guidance, I'd be more than happy to lead the implementation efforts for this :smile:

Overlapping widgets

You could say overlapping interactive widgets is my exact use case, so I'm very interested in this too. The nodes in my library are conceptually just widgets you can drag around, even if they might look like a window. Implementing them as windows would be wrong, as that would have all sorts of weird interactions when drawing a graph editor inside a window.

I have no problem handling the ordering of widget draw calls on my end as a user (and I already do that), but right now there's no good mechanism to detach interactivity from draw order. Some sort of Z-index sounds like the perfect solution to that.

setzer22 avatar Nov 16 '22 12:11 setzer22

I think we should split LayerId into two struct:s with different responsibilities.

One would be AreaLayer, the other ZLayer:

pub struct AreaLayerId {
    pub order: Order,
    pub id: Id,
}

pub struct ZLayer {
    pub area_layer: AreaLayer
    pub z: ZOrder,
}

pub struct ZOrder(pub i32);

AreaLayer would be used in Areas to order windows etc.

ZLayer would be used in Painter, Context::interact and in GraphicLayers.

emilk avatar Nov 29 '22 13:11 emilk