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:
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.
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).