Writing a custom widget in Gtk4. It works but seems inefficient. It is called Smeter and shows a signal level.
Beyond some given level, the color changes from green to red.
Every time the widget is redrawn, all its elements are redrawn through cairo commands.
But most of the widget stays identical to the previous one, and only a part changes.
With the inspecter I recorded some frames.
Imagine two consecutive frames with a value both in the red.
In that case new drawing could be limited to a new red part. The total image being previous contour, previous green part, and new red part.
Can these individual parts of the total drawing be stored and reused, and only redrawn when needed?
Since you have not specifies which version of GTK you use, I will assume you’re using GTK 4.
Also, note that I’m not the greatest expert when it comes to GTK rendering.
However, as I understand it, the best approach would be to use child widgets.
To quote the GTK docs:
To avoid excessive work when generating scene graphs, GTK caches render nodes. Each widget keeps a reference to its render node (which in turn, will refer to the render nodes of children, and grandchildren, and so on), and will reuse that node during the Paint phase.
GTK 4 itself also uses child widgets extensively, as part of their method “everything is a widget”:
[…] For example the trough and the slider of a GtkScale are fully formed sub-widgets which can maintain their own state and receive input like any other widget.
So, what you probably want to do is create a child widget for each element which could change seperately, with your main widget combining them. This way, when the state of one of the child widget changes, only that child widget would need be redrawn.
Additionally, I would recommend looking into using render nodes instead of Cairo. AFAIK Cairo is still only using the CPU, while with the render nodes you can directly target the GPU-using Vulkan or OpenGL renderers.
Thanks. Added Gtk4 indeed. Still puzzled though, after reading the docs. Towards the end it states: “GTK keeps the render nodes produced by your snapshot() function and reuses them until you tell it that your widget needs to be drawn again by calling gdk_widget_queue_draw().”
After a value update call from the main app and some logic in the widget code to compare the new value to the previous value to know what needs redrawing, would that queue_draw function be applied just to the child widgets that need change? Also puzzled about your comment about using Cairo. The docs still show Cairo being used but its drawing result becoming nodes in a Snapshot when done in Gtk4.
When you draw with Cairo, GTK will take the contents of the Cairo surface and upload them to the GPU as a texture object; this means that, from the perspective of the toolkit, a Cairo render is the same as an image, like an icon. GTK does not do invalidation for regions of a texture, which means you have to redraw a Cairo surface every time.
One option to make things slightly faster is to cache Cairo paths, or cache intermediate states as separate image surfaces, and then composite them only at the end on the Cairo context that GTK gives you.
You can also use the GSK path API to draw your paths; there are some limits to it, as it’s not meant to be a full replacement of the Cairo API.
Finally, you can separate the various components into widgets, and let GTK handle the redrawing for you; this is not always possible to achieve for every shape of layout.
Thanks! Would like to try the Cairo path caching, and alternatively the GSK path API which has an arc_to function which should be able to do the circle segments I need. Do you know of some example code for these two approaches?
The gtk4-demo application has examples of both Cairo drawing and GSK paths, including their code; you should be able to install it from your distribution, otherwise you can build GTK from source.
You can look at Gtksourceview how they use caches for their spaces drawer.
The symbols are only draw once with GSK APIs, then stored in a cache->node variable, which gets rendered at the expected position by gtk_snapshot_append_node.
If you look up in the git history, you will see the old Cairo code doing the same thing.
thanks for the help and references.
The radio signal strength meter that I am designing will be frequently updated and hence my concern for repeating calculations when things don’t change.
Constant are a background in the shape of the round part of the Greek capital letter omega and some gauge lines and captions. Variable will be part of that background arc partially being overdrawn with green, when the level is below a certain threshold, and continuing in red from there if it is above the threshold.
So, depending on the new value, always the background and captions can be reused, and often also the full green part of the arc. New to be drawn are only a piece of green, when below the threshold, or a piece of red when above, and a value number. Hope this explains the idea.
started writing the code using the example of caching given and the gskpathbuilder commands. It turns out to be much more code than needed in Cairo because the circle segment primitive is lacking. One needs to use arc_to which requires calculus of a tangents crossing point and only works for segments up to 180 degrees, when tangents would cross in infinity. So the larger segments need to be split in two halves. All doable but it makes me wonder whether this in the end will be more efficient than Cairo. Is this gskpathbuilder API completely fixed, or is there still a chance of more primitives being added in the future? Having the circle segment would much simplify my code. Also under the hood I expect what goes on with the generic arc, where a circle is a special case, to be more complex than when the special characteristics of a circle can be used, its constant radius.
Nice example of another approach, using Stroke to blank out part of a circle! Thanks.
I don’t really grasp the relation between the path and stroke yet.
In this example of a circle, the circle is defined with center coordinates and a radius.
But essentially an abstract concept needing Stroke to show something.
The documentation mentions “The path is going around the circle in clockwise direction”. So worth adding would be that its starting point, for the purpose of stroking, is considered to be the crossing of the circle and the positive x axis. (I presume that is the case) The same applies to the other path that is closed in itself, the rounded rectangle. Latter definition, using graphene_rect_t, again defines a center point and width and height, not where a stroking action would start.
The other thing not clear to me is how to introduce the link to definitions in css, e.g. to define colors.
GTK can only obtain the current foreground color from CSS, with the gtk_widget_get_color() API.
Either hardcode the colors in the code (i.e. not controllable by CSS), or use several widgets as layers, one per color, giving then each a specific class. The latter approach is also quite interesting in terms of performances, because if you have a special widget for e.g. the background, then you can redraw only the green/red part without at all redrawing the bg widget, so you won’t even need to do special node caching, gtk will do it for you.
Have now code running that draws the circle segment background, gauge lines, and green and red foreground, depending on signal level to be shown on the scale.
Using gsk.pathbuilder, stroke, dash, and use of cached render nodes that don’t change when only the level changes, and no Cairo.
Now want to show text strings at the end of the gauge lines with the level the gauge line represents, i.e. a scale.
Was looking at gtk/gtk-demo/path_text.c but it is very complex.
Q1: What are the minimum steps, when already using pathbuilder for circles and lines and already having the coordinates where the text string should land, to actually make text appear?
Q2: Found a number of things in the documentation of gsk.pathbuilder.add_circle, gsk.stroke and gsk.set_dash that are missing or unclear. Figured it out via trial and error. What is the preferred way to suggest improvements to the documentation? Some examples: undefined are where stroke starts on a closed path, that a positive dash offset works counter clockwise when the path is closed, what happens when the sum of pos. dash lengths exceeds the closed path length, and the effect of offset on latter. In other words, can the end of a list with dash definitions bite its own start when the path is closed?