Something I've found has helped is to write my own GUI library.
...but make use of what you are given as well, is my advice.
I have a horrible tendency to re-write the most basic graphics primitives[1] from the ground up with the most basic peek-n-poke graphics method I can find in a graphics library (or via a direct hardware call). Totally circumvents (and loses the efficiency from) any inherent library functions that have all the chance in the world of being hardware-aware insofar as stuff like GPU functionality. But it's an old, old habit from times past[2] when it was easier to write my own memory-location-setting code.
I like the control, but obviously I don't take the advantages of the rest of the prepared lib (where applicable). (That's
one reason why I'm not that much of a publisher of my own 'toy' projects!)
OTOH, it does help to understand some of the principles concerned. (And I'm always learning new things, as I'm optimising and re-using functionality.)
[1] Solid and anti-aliased (according to an arbitrary overlap value) pixels using the most basic pixel-setting code or function, then from that solid and anti-aliased (and 'end-only' anti-aliased') lines, then from that triangle surrounds/flooding/shading/bitmapping plots (with and without anti-aliased edges, or even mixed), from which more convex shapes arise, and by which (with a little LOD and LOS ordering) I can get any form of 3D I want [and, in a couple of implementations, from which I can get any
4D I want.
], the limit only being processing power.
[2] You see, I always trust "object.setpixel[x,y]" or its equivalent to do
exactly what I want (once I have a handle on if it's TL- or BR-origined, typically, and if that's at 0,0 or 1,1 in this particular environment, which are the most typical dislocations of system one used to encounter In The Old Days
TM!)