0

Note: I need this information because I'm working on an alternative widget library for awesome-wm.

In short, how do I redraw only portions of a cairo drawing for a layout?

After looking at the source code in wibox/drawable.lua and wibox/hierarchy.lua, as far as I understand, when an element is updated (let's say an element's layout changed), the way this is redrawn goes as follows:

  • First, the layout is recomputed starting from the invalid element down to its children and grandchildren, etc.
  • Second, a bunch of masks are created based on these relayouted elements, which will be "combined" when they overlap and used later to skip drawing parts of the drawing which didn't change.
  • Then, in another function named hierarchy:draw, the drawing happens, and here I assume only the widget(s) that were changed get redrawn and then, since the mask was applied previously, only those portions would go through the mask and be "redrawn" on the final image.

If this is how the redrawing happens, then I would also like to implement something of the sort. If not, I would be happy to be corrected.
The problem I'm facing is my inexperience and confusion with cairo. What I would most appreciate as an answer would be a commented code block containing a skimmed down, simplified version of this updating-layouting-redrawing process.

1 Answers1

2

Okay, so... yeah... first: Your descriptions looks fine to me.

I'll extend a bit on your points:

First, the layout is recomputed starting from the invalid element down to its children and grandchildren, etc.

Yup. This is done via wibox.hierachy:update. The important part here is that this function gets self._dirty_area as its argument. This is a cairo region which is basically a list of pixel-aligned rectangles (and overlap is automatically removed by cairo).

Second, a bunch of masks are created based on these relayouted elements, which will be "combined" when they overlap and used later to skip drawing parts of the drawing which didn't change.

Yeah, well. The code for the cairo region already removed all overlap while building the region.

The rest of your statement seems correct. The code here first checks whether there is anything to draw at all:

    if self._dirty_area:is_empty() then
        return
    end

The above calls cairo_region_is_empty.

Next, it adds each of the rectangles to the current path:

    for i = 0, self._dirty_area:num_rectangles() - 1 do
        local rect = self._dirty_area:get_rectangle(i)
        cr:rectangle(rect.x, rect.y, rect.width, rect.height)
    end

It clears the _dirty_area so that from now on all "things will need to be redrawn" are tracked in a new, initially empty region:

    self._dirty_area = cairo.Region.create()

Finally, it clips to the rectangles that we just added:

    cr:clip()

What is a clip? Let's ask the docs:

The current clip region affects all drawing operations by effectively masking out any changes to the surface that are outside the current clip region.

Basically: All drawing will only happen inside of the (previous) self._dirty_area. Attempts to draw outside of this just have no effect at all.

Next, the code in drawable.lua draws the background, calls self._widget_hierarchy:draw() to do the actual drawing and finally calls self.drawable:refresh(). This last bit of code is what actually makes the drawing visible (AwesomeWM uses something like double buffering).

Another important ingredient is the function empty_clip. This is used in wibox.hierarchy:draw() to just skip all drawing if it would be completely clipped away: https://github.com/awesomeWM/awesome/blob/6ca2fbb31c5cdf50b946b50f3f814f39a8f1cfbe/lib/wibox/hierarchy.lua#L338

(Oh well and of course to prevent widgets from drawing outside of their "assigned area", the code first clips all drawing to the area of the widget. Only after that it checks for an empty clip, thus effectively checking whether the widget covers any of our "needs to be redrawn"-area.)

Another possibly important ingredient: The drawing code uses cr:save() / cr:restore(). restore() restores the state that was present at the last save(). This includes the clip area. This is the only good way to undo a :clip() (the other way is :reset_clip() but that deletes the whole clip area and thus does not just undo the last :clip(), but undos all of them).

Uli Schlachter
  • 9,337
  • 1
  • 23
  • 39
  • Great answer, thanks. One thing I didn't quite get is: after we clip to only the regions we want to draw, does the lua code in awesome only try to redraw the element that needs to be redrawn? or does it just go from the root, tries to redraw everything, but only the parts that are inside the created mask end up being redrawn? – DesertCarMechanic Sep 15 '22 at 14:34
  • It tries drawing everything. But when drawing widgets that are completely outside of the to-be-redrawn area, the clip area ends up empty, meaning "nothing can be drawn anymore". This situation is detected via the `empty_clip` I mentioned in my answer. In this case, a widget (and all its children) are skipped and are not redrawn. – Uli Schlachter Sep 16 '22 at 15:54
  • Perhaps to extend on "clip ends up being empty": Imagine first clipping to the rectangle at 0x0 with size 10x10. So, `cr:rectangle(0, 0, 10, 10) cr:clip()`. Next we clip somewhere completely different, e.g. `cr:rectangle(100, 100, 10, 10) cr:clip()`. Now, only the pixels that are inside both areas can be drawn to, but these two rectangles do not overlap, so the clip area is now empty. `empty_clip` would detect this. – Uli Schlachter Sep 16 '22 at 15:56
  • don't you mean that if you create two clips, one at rectangle(0, 0, 10, 10), one at rectangle(100, 100, 10, 10), any element that has any portion of it inside of any of these two rectangles, will end up being redrawn? Maybe I'm misunderstanding, but in my mind when I clip to certain areas I'm basically thinking of it as it creating a "hole" through which the pixels of the drawing can go through. Am I getting something wrong? – DesertCarMechanic Sep 26 '22 at 14:08
  • Maybe to give a bit more context as to what I'm currently doing: let's say I have two elements that need a redraw. One is at `{x=0, y=0, width=200, height=100}`, the other is at `{x=0, y=0, width = 100, height = 200}`. What I currently do, is that I just call `cr:rectangle` on each one of them, and then `cr:clip`. My mental model of what happens is that cairo creates a shape which is the union of those two rects, and when I call for example `cr:rectangle(300, 300, 10, 10); cr:fill()`, cairo will check if this new rectangle is inside that shape, and just not redraw it. Is this correct? – DesertCarMechanic Sep 26 '22 at 14:50
  • Yup, you are right. The difference to my example above is that I called `clip()` after each rectangle. A new call to `clip()` can only make the "hole" (as you call it) smaller. Initially, it covers the whole surface and each `clip()` makes it smaller. Since my example has two disjoint rectangles, the hole ends up being empty. In your example, the hole will be as you expect: Both rectangles. – Uli Schlachter Sep 29 '22 at 15:38