Layout Management
Two of the main goals of xpGUI are:
For WinAPI, we can easily/in fact make the gH/Vbox "virtual" and calculate the needed offsets, and in practice it turns out much easier to effectively disable the internal layout manager of GTK, use GtkFixed containers (tellingly IUP does so too), and use the same logic as WinAPI (albeit non-virtual).
Both GTK and HTML/JS allow parent and child controls to be created in any order, then "glued together" by a separate routine call, such as gtk_container_add(), gtk_fixed_put(), or parent.appendChild(), whereas WinAPI demands a parent-first declaration order and terminates with ERROR_TLW_WITH_WSCHILD = 1406 for non-compliance. In contrast, xpGUI requires a child-first declaration order, so that nested declarations can work and/or you have something to put in the child[ren] parameter when declaring a container, and uses a separate automatic gMap() phase to comply with any (esp WINAPI) backend-specific constraints.
Most visible elements have a "natural" size, the notable exceptions being gTreeView() and gCanvas(), and the canvas-derived controls gGraph(), gList(), and gTable(). Those need to be either given an explict user size or allowed to expand into some size explicitly specified on one of their parent containers. Apart from that, we can easily accumulate the sizes to give a complete dialog a perfectly usable natural size as well, and all without the programmer having to specify any sizes or positions at all. Often that may look a little cramped, but spacing around and between interface elements can easily be specified using the MARGIN, GAP, and SPACE attributes. Interface elements also have an EXPAND attribute to control whether/how they fill/react to their parent container having excess space.
The layout manager is responsible for initially determining some sizes, and also swings into action when a dialog is manually resized, expanding and arranging elements as needed to fully occupy it, hopefully in an aesthetically pleasing manner. Note the accumulated sizes may also form a natural MINSIZE for the dialog, and setting a dialog size that otherwise seems unnecessary may still be of benefit when the top right "restore down" middle button is clicked.
In summary this requires very little effort on the part of the programmer, with no tedious pixel counting/accumulating needed at all.
In JavaScript, the margins can be set with anything between one and four values:
50px: all four margins will be 50px
50px 10px: the top and bottom margins will be 50px, left and right margins will be 10px
50px 10px 20px: the top margin will be 50px, left and right 10px, and bottom 20px
50px 10px 20px 30px: the top margin will be 50px, right 10px, bottom 20px, and left 30px
and we may as well adopt a near-identical but cross-platform scheme, ie/eg 50 or[...] "50x10x20x30" or {50,10,20,30}.
(Aside: The ability to specify margins in units other than pixels will/may be added at a later date.)
In other words, margin, border, and padding all spell trbl
.
It turns out we only really need a (fixed width 1) border on just six controls:
gButton, gDatePick, gDropDown, gFrame, gProgressBar, and gText.
You would not want any padding on a progress bar, a gFrame can rely on the margin of its sole child, and the default spacing for the other four is is perfectly acceptable and probably better specified by auto-center of a SIZE anyway, hence (and to get it out the door a bit quicker) xpGUI does not have any border or padding settings.
In many but not all cases xpGUI[.e/.js] actually has to set wxh as in the above diagram, but that little detail should be completely abstracted away. The natural and user sizes include any such automatic border and padding, but not its own margin, though the margins of any child elements are obviously properly taken into account.
Hence the layout manager only has to deal with/worry about margins, which makes my life at least much simpler.
There is no getting around that basic model, whatever we do simply has to be expressible in terms of nested containers with margins.
No particular problem there, just explicitly stating a simple and completely undeniable fact. While there are in fact options for both a GAP and equivalents for SPACE on a flexbox which we will no doubt take advantage of, were there not (or issues arise) we could achieve what we need by tweaking the margins, which is probably what a flexbox quietly does anyway. It is not so much that model being actually reimplemented on the desktop, but rather that everything we say and do regarding sizes and positioning is or can be expressed in those or similar terms.
Naturally the decorations may subtly differ between backends, such that you will get a different client size for a given window size, or vice-versa, and it is the job of xpGUI to abstract that away as best it can, and get things recognisable and serviceable. However, there will also be differences in the appearance and sizes of several native controls, such as a gButton(), and depending on how fussy you are, it is amost inevitable there will be some minor platform-specific tweaks in the final polish. You might, for example, want to specify different initial window sizes on GTK and WinAPI, to get the (also different) optimal client sizes, once all the accumulated differences have been taken into account. On a cheerier note, if a gCanvas() is the prime focus, or indeed full-screen, that is measured in pixels and on similar hardware will be pretty much identical no matter what OS or backend is used.
gNormalise(gdx ids, string direction="BOTH") [the alias gNormalize() can also be used]
ids: a seuence(/group) of elements to be normalised.
direction: can also be "HEIGHT" or "WIDTH".
This routine must be invoked before any elements are mapped/displayed (or not at all).
All elements must be in the same gDialog(), but can be anywhere within the layout hierarcy, specifically in different gH/Vbox. Elements can be normalised widthwise in one group(/call) and heightwise in one other, but there is no iterating/propagating against multiple groups. Note that elements can still expand/shrink to fit their containers after normalisation.
Epandable containers are naturally normalised in the cross orientation of any parent container, for instance should a gVbox have three child containers they would all get the same width, unless/the only way to stop that being to make a child container non-expandable. Containers also naturally "inherit" any normalisation of their children (with added margins, etc). Hence attempts to apply normalisation to any containers could give highly unpredictable or even fatal results, esp. should any of its [grand]children also be in the same group.
Note that normalisation is a pretty low priority on desktop/Phix, and way further down under pwa/p2js.
The intention is that these attributes should all be set before the first gShow, updates to them after that are not expected to be particularly well supported, though an explicit gRedraw() might help. One common issue is that a gLabel() might get a natural size from the initial text, and updates to that appear clipped. The correct solution is to ensure it is properly expanded/aligned from the get-go, rather than try to force updates to the natural size and corresponding reductions to some surrounding spacing. Likewise try and design a UI so that controls stay in the same place rather than jiggle about when different app-specific options are selected, except as positioned by the layout manager for any given containing dialog size.
It would be absurd for xpGUI.js not to take advantage of all that power, and it would be absurd to replicate every bit of all that functionality in xpGUI.e for desktop/Phix, equally CSS grid may be even more powerful, but beyond our needs. The following is a severely abridged definition of a flexbox, limited to those parts that xpGUI.js actually uses (and is probably more for my benefit than yours).
element.style.display = "none" - when VISIBLE is false.
container.style.display = "flex" - a gH/vbox().
[not used: block|compact|grid|initial|inherit|inline[-*]|list-item|marker|run-in|table[-*]]
container.style.flexDirection = "row" - a gHbox().
container.style.flexDirection = "column" - a gVbox().
[not used: row-reverse|column-reverse|initial|inherit]
container.style.justifyContent = "flex-start" - SPACE=RIGHT/BOTTOM.
container.style.justifyContent = "flex-end" - SPACE=LEFT/TOP.
container.style.justifyContent = "center" - SPACE=CENTRE.
container.style.justifyContent = "space-between" - SPACE=BETWEEN.
container.style.justifyContent = "space-evenly" - SPACE=AROUND.
[above is primary axis. not used: space-around|initial|inherit]
[space-evenly splits 1-1-1-1, whereas space-around is 1-2-2-1, which I find ugly.]
container.style.alignItems - not used: see alignSelf.
container.style.flexWrap - nowrap / not used: wrap|wrap-reverse|initial|inherit.
container.style.alignContent - not used (due to nowrap).
child.style.alignSelf = "stretch" - expandable child (SPACE ignored).
child.style.alignSelf = "center" - non-expandable child with SPACE=CENTRE/AROUND.
child.style.alignSelf = "flex-start" - non-expandable child with SPACE=RIGHT/BOTTOM/BETWEEN.
child.style.alignSelf = "flex-end" - non-expandable child with SPACE=LEFT/TOP.
[above is cross axis. not used: auto|baseline|initial|inherit]
child.style.flexBasis = "auto" - expandable child, but see note.
child.style.flexBasis = number - child of a gSplit() [when FRAC!=-1].
child.style.flexBasis = "0" - non-expandable child.
[above is primary axis. not used: initial|inherit]
child.style.order - not used.
child.style.flexGrow - not used: (number|initial|inherit) relies on flexBasis instead.
child.style.flexShrink - not used: (number|initial|inherit) relies on flexBasis instead.
[the shorthands flex (grow/shrink/basis) and flexFlow (dir/wrap) are for puny humans, not xpGUI.js!]
container.style.columnGap = "Npx" - GAP on a gHbox().
container.style.rowGap = "Npx" - GAP on a gVbox().
[the gap shorthand is for puny humans, GAP is one or t’other, not both.]
element.style.cssFloat - none / not used: left|right|initial|inherit.
A flex-basis of "auto" lets us down badly on a gCanvas, stretching it without redrawing while maintaining the aspect ratio, causing them to overflow the container and generally look awful, hence xpGUI.js still needs to manually resize them.
Otherwise a pretty straightforward mapping. Should you want to learn more about flexbox, I cannot recommend anything better than https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/
While (hopefully) this may make little sense right now, the natural sizes are in fact the most critical aspect of the layout manager: without natural sizes you simply cannot display anything naturally. Being out by even a single pixel is pretty much guaranteed to find a way to accumulate across or down the screen to something pretty ugly pretty fast. It is not helped when a backend (esp GTK3) is obstinately opinionated about such matters and simply ignores anything it deems "too small". What I have tended to do is paste a screenshot, or three dozen, into mspaint, clip, zoom in, and display a grid, then carefully count pixels. The ugliest parts of xpGUI.e are where such efforts have resulted in ad-hoc calculations and little constants scattered seemingly at random throughout that source, and even worse, in different places for different backends.
Note there is no intention to achieve pixel-perfect consistency between different backends, since they have different window decorations and fonts, and even GTK2 and GTK3 on the same box are visually rather (rudely/arrogantly) distinct. Besides, should you display a screenshot of a Windows program on a Linux desktop, or vice versa, it might look a bit odd and out-of-place, and you are probably more likely to want to hide the standard window decorations in a browser than on the desktop. Things should however be reasonably similar and more importantly perfectly/intuitively use-able. That said, there is no prohibition on or expectation you will never need minor platform-specific tweaks, and at least some uses/tests of gUseGTK() on Windows do actually attain pixel perfection, albeit often with subtly different background colours.
- Reasonably consistent behaviour across different backends/platforms.
- Simple layout declarations that are naturally resizeable. [if RESIZE!=NO]
- HTML/CSS/JavaScript uses nested <div> and a box model.
- WinAPI demands parent-first declaration and pixel offsets.
- GTK is very highly opinionated, but we only need <1% of it.
For WinAPI, we can easily/in fact make the gH/Vbox "virtual" and calculate the needed offsets, and in practice it turns out much easier to effectively disable the internal layout manager of GTK, use GtkFixed containers (tellingly IUP does so too), and use the same logic as WinAPI (albeit non-virtual).
Both GTK and HTML/JS allow parent and child controls to be created in any order, then "glued together" by a separate routine call, such as gtk_container_add(), gtk_fixed_put(), or parent.appendChild(), whereas WinAPI demands a parent-first declaration order and terminates with ERROR_TLW_WITH_WSCHILD = 1406 for non-compliance. In contrast, xpGUI requires a child-first declaration order, so that nested declarations can work and/or you have something to put in the child[ren] parameter when declaring a container, and uses a separate automatic gMap() phase to comply with any (esp WINAPI) backend-specific constraints.
Most visible elements have a "natural" size, the notable exceptions being gTreeView() and gCanvas(), and the canvas-derived controls gGraph(), gList(), and gTable(). Those need to be either given an explict user size or allowed to expand into some size explicitly specified on one of their parent containers. Apart from that, we can easily accumulate the sizes to give a complete dialog a perfectly usable natural size as well, and all without the programmer having to specify any sizes or positions at all. Often that may look a little cramped, but spacing around and between interface elements can easily be specified using the MARGIN, GAP, and SPACE attributes. Interface elements also have an EXPAND attribute to control whether/how they fill/react to their parent container having excess space.
The layout manager is responsible for initially determining some sizes, and also swings into action when a dialog is manually resized, expanding and arranging elements as needed to fully occupy it, hopefully in an aesthetically pleasing manner. Note the accumulated sizes may also form a natural MINSIZE for the dialog, and setting a dialog size that otherwise seems unnecessary may still be of benefit when the top right "restore down" middle button is clicked.
In summary this requires very little effort on the part of the programmer, with no tedious pixel counting/accumulating needed at all.
Sole Child
When a single visible element is the sole child of a container, especially a gDialog(), the rule book is essentially thrown out the window and that element is automatically expanded to fill the entire container, which is often precisely what is wanted for a gCanvas() [etc], gFrame(), gSplit(), gTabs(), and gTreeView(), but less likely on say a gButton() - there is in fact no guarantee of consistent behaviour for some of the "dafter" cases on different backends (esp GTK), that is when no gH/Vbox are involved. For instance the gButton() example sneaks in a crafty little gHbox(), and without that the button would completely fill the dialog. Note that in general such behaviour is not deliberately or explicitly implemented or enforced by xpGUI, but rather instead (sometimes) inherited from whatever backend it is using, though there may well be some code which assumes it would happen or irons out some backend difference and that way effectively does implement/enforce it. At the time of writing it does not seem to affect/afflict a gFrame(), unless that is explicitly told to expand.The HTML/CSS Box Model

In JavaScript, the margins can be set with anything between one and four values:
50px: all four margins will be 50px
50px 10px: the top and bottom margins will be 50px, left and right margins will be 10px
50px 10px 20px: the top margin will be 50px, left and right 10px, and bottom 20px
50px 10px 20px 30px: the top margin will be 50px, right 10px, bottom 20px, and left 30px
and we may as well adopt a near-identical but cross-platform scheme, ie/eg 50 or[...] "50x10x20x30" or {50,10,20,30}.
(Aside: The ability to specify margins in units other than pixels will/may be added at a later date.)
In other words, margin, border, and padding all spell trbl
.
It turns out we only really need a (fixed width 1) border on just six controls:
gButton, gDatePick, gDropDown, gFrame, gProgressBar, and gText.
You would not want any padding on a progress bar, a gFrame can rely on the margin of its sole child, and the default spacing for the other four is is perfectly acceptable and probably better specified by auto-center of a SIZE anyway, hence (and to get it out the door a bit quicker) xpGUI does not have any border or padding settings.
In many but not all cases xpGUI[.e/.js] actually has to set wxh as in the above diagram, but that little detail should be completely abstracted away. The natural and user sizes include any such automatic border and padding, but not its own margin, though the margins of any child elements are obviously properly taken into account.
Hence the layout manager only has to deal with/worry about margins, which makes my life at least much simpler.
There is no getting around that basic model, whatever we do simply has to be expressible in terms of nested containers with margins.
No particular problem there, just explicitly stating a simple and completely undeniable fact. While there are in fact options for both a GAP and equivalents for SPACE on a flexbox which we will no doubt take advantage of, were there not (or issues arise) we could achieve what we need by tweaking the margins, which is probably what a flexbox quietly does anyway. It is not so much that model being actually reimplemented on the desktop, but rather that everything we say and do regarding sizes and positioning is or can be expressed in those or similar terms.
Window vs. Client sizes
Some controls have both "window" and "client" sizes, for example:|
|
|
|
Naturally the decorations may subtly differ between backends, such that you will get a different client size for a given window size, or vice-versa, and it is the job of xpGUI to abstract that away as best it can, and get things recognisable and serviceable. However, there will also be differences in the appearance and sizes of several native controls, such as a gButton(), and depending on how fussy you are, it is amost inevitable there will be some minor platform-specific tweaks in the final polish. You might, for example, want to specify different initial window sizes on GTK and WinAPI, to get the (also different) optimal client sizes, once all the accumulated differences have been taken into account. On a cheerier note, if a gCanvas() is the prime focus, or indeed full-screen, that is measured in pixels and on similar hardware will be pretty much identical no matter what OS or backend is used.
Normalisation
Sometimes you may want to line things up, which is achieved by setting them to the same (largest) natural width/height/both:gNormalise(gdx ids, string direction="BOTH") [the alias gNormalize() can also be used]
ids: a seuence(/group) of elements to be normalised.
direction: can also be "HEIGHT" or "WIDTH".
This routine must be invoked before any elements are mapped/displayed (or not at all).
All elements must be in the same gDialog(), but can be anywhere within the layout hierarcy, specifically in different gH/Vbox. Elements can be normalised widthwise in one group(/call) and heightwise in one other, but there is no iterating/propagating against multiple groups. Note that elements can still expand/shrink to fit their containers after normalisation.
Epandable containers are naturally normalised in the cross orientation of any parent container, for instance should a gVbox have three child containers they would all get the same width, unless/the only way to stop that being to make a child container non-expandable. Containers also naturally "inherit" any normalisation of their children (with added margins, etc). Hence attempts to apply normalisation to any containers could give highly unpredictable or even fatal results, esp. should any of its [grand]children also be in the same group.
Note that normalisation is a pretty low priority on desktop/Phix, and way further down under pwa/p2js.
Attributes
Layout management observes and is controlled by just a small handful of attributes:- SIZE: Allows the natural size to be overidden.
- MAXSIZE/MINSIZE: Should be self-explanatory.
- EXPAND: Allows an element or container to grow, widthwise, heightwise, or both.
- MARGIN: As per the box model above.
- GAP/SPACE: (gH/Vbox only) Additional space between/around elements.
The intention is that these attributes should all be set before the first gShow, updates to them after that are not expected to be particularly well supported, though an explicit gRedraw() might help. One common issue is that a gLabel() might get a natural size from the initial text, and updates to that appear clipped. The correct solution is to ensure it is properly expanded/aligned from the get-go, rather than try to force updates to the natural size and corresponding reductions to some surrounding spacing. Likewise try and design a UI so that controls stay in the same place rather than jiggle about when different app-specific options are selected, except as positioned by the layout manager for any given containing dialog size.
Inheritance
Note that the inheritance model differs somewhat between pGUI and xpGUI. While some aspects such as fonts inherit as you might hope and expect, I found having GAP/MARGIN apply to all child elements as well as the immediate (container) just annoying (You want an extra 5 pixels? Sure, here you go, and that five-level nested sub-child gets 25 pixels, that’s what you meant, obviously. Having N-prefixed (etc) versions of all these attributes never felt quite right either. Of course whenever inheritance is not right for desktop/Phix there is a whole heap of extra work to stop CSS from being stupid as well.) Hopefully gSetAttribute({elements},..) should help when that sort of thing is actually wanted. Of course no-one should expect legacy/freshly ported code to run flawlessly without at least some tweaks anyway.CSS flexbox (implementation in xpGUI.js for the HTML/CSS/JavaScript backend)
Delightfully complicated, delivers a frankly ludicrous level of control over positioning and alignment of items.It would be absurd for xpGUI.js not to take advantage of all that power, and it would be absurd to replicate every bit of all that functionality in xpGUI.e for desktop/Phix, equally CSS grid may be even more powerful, but beyond our needs. The following is a severely abridged definition of a flexbox, limited to those parts that xpGUI.js actually uses (and is probably more for my benefit than yours).
element.style.display = "none" - when VISIBLE is false.
container.style.display = "flex" - a gH/vbox().
[not used: block|compact|grid|initial|inherit|inline[-*]|list-item|marker|run-in|table[-*]]
container.style.flexDirection = "row" - a gHbox().
container.style.flexDirection = "column" - a gVbox().
[not used: row-reverse|column-reverse|initial|inherit]
container.style.justifyContent = "flex-start" - SPACE=RIGHT/BOTTOM.
container.style.justifyContent = "flex-end" - SPACE=LEFT/TOP.
container.style.justifyContent = "center" - SPACE=CENTRE.
container.style.justifyContent = "space-between" - SPACE=BETWEEN.
container.style.justifyContent = "space-evenly" - SPACE=AROUND.
[above is primary axis. not used: space-around|initial|inherit]
[space-evenly splits 1-1-1-1, whereas space-around is 1-2-2-1, which I find ugly.]
container.style.alignItems - not used: see alignSelf.
container.style.flexWrap - nowrap / not used: wrap|wrap-reverse|initial|inherit.
container.style.alignContent - not used (due to nowrap).
child.style.alignSelf = "stretch" - expandable child (SPACE ignored).
child.style.alignSelf = "center" - non-expandable child with SPACE=CENTRE/AROUND.
child.style.alignSelf = "flex-start" - non-expandable child with SPACE=RIGHT/BOTTOM/BETWEEN.
child.style.alignSelf = "flex-end" - non-expandable child with SPACE=LEFT/TOP.
[above is cross axis. not used: auto|baseline|initial|inherit]
child.style.flexBasis = "auto" - expandable child, but see note.
child.style.flexBasis = number - child of a gSplit() [when FRAC!=-1].
child.style.flexBasis = "0" - non-expandable child.
[above is primary axis. not used: initial|inherit]
child.style.order - not used.
child.style.flexGrow - not used: (number|initial|inherit) relies on flexBasis instead.
child.style.flexShrink - not used: (number|initial|inherit) relies on flexBasis instead.
[the shorthands flex (grow/shrink/basis) and flexFlow (dir/wrap) are for puny humans, not xpGUI.js!]
container.style.columnGap = "Npx" - GAP on a gHbox().
container.style.rowGap = "Npx" - GAP on a gVbox().
[the gap shorthand is for puny humans, GAP is one or t’other, not both.]
element.style.cssFloat - none / not used: left|right|initial|inherit.
A flex-basis of "auto" lets us down badly on a gCanvas, stretching it without redrawing while maintaining the aspect ratio, causing them to overflow the container and generally look awful, hence xpGUI.js still needs to manually resize them.
Otherwise a pretty straightforward mapping. Should you want to learn more about flexbox, I cannot recommend anything better than https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/
Implementation (in xpGUI.e for the GTK and WinAPI backends)
The layout manager works by juggling a whole heap of sizes: natural, user, min, max, normalised, and final, and applying margins, initially accumulating sizes up the hierarchy, and then distributing spacing/expansion downwards. Much of the code is a little vague and/or experimental, rather than adhering to some clear easily digestible overall design, since there isn’t one - these docs are the closest you’ll get to that. The reality is my efforts to come up with a proper design certainly helped to simplify things a great deal at first, but beyond a certain point started going in circles, so after that I just started trying to use it, and fixing the many inevitable "bugs" as and when they got in the way. Should you want to study the layout management code, the relevent (local) routines in xpGUI.e all begin with "xpg_lm_"/are the only ones that contain "lm", and are all post-mapping:- xpg_lm_get_dialog_decoration_size() (fudged under GTK)
- xpg_lm_set_element_sizes() (set [SZ_NATURAL_W/H], elements only, no margins)
- xpg_lm_gather_normal_groups() (for next)
- xpg_lm_normalise_sizes() (set [SZ_NORMAL_W/H])
- xpg_lm_accumulate_sizes() (initial/sum/min [SZ_W/H], containers only, adds margin+gap)
- xpg_lm_apply_space() (for next, implements the SPACE attribute on h/vbox)
- xpg_lm_disperse_user_sizes() (final/expanded [SZ_W/H/X/Y], applies/respects margin+gap)
- xpg_lm_apply_sizes_and_offsets() (apply [SZ_W/H/X/Y])
- xpg_lm_dump_ctrls() (debug aid)
While (hopefully) this may make little sense right now, the natural sizes are in fact the most critical aspect of the layout manager: without natural sizes you simply cannot display anything naturally. Being out by even a single pixel is pretty much guaranteed to find a way to accumulate across or down the screen to something pretty ugly pretty fast. It is not helped when a backend (esp GTK3) is obstinately opinionated about such matters and simply ignores anything it deems "too small". What I have tended to do is paste a screenshot, or three dozen, into mspaint, clip, zoom in, and display a grid, then carefully count pixels. The ugliest parts of xpGUI.e are where such efforts have resulted in ad-hoc calculations and little constants scattered seemingly at random throughout that source, and even worse, in different places for different backends.
Note there is no intention to achieve pixel-perfect consistency between different backends, since they have different window decorations and fonts, and even GTK2 and GTK3 on the same box are visually rather (rudely/arrogantly) distinct. Besides, should you display a screenshot of a Windows program on a Linux desktop, or vice versa, it might look a bit odd and out-of-place, and you are probably more likely to want to hide the standard window decorations in a browser than on the desktop. Things should however be reasonably similar and more importantly perfectly/intuitively use-able. That said, there is no prohibition on or expectation you will never need minor platform-specific tweaks, and at least some uses/tests of gUseGTK() on Windows do actually attain pixel perfection, albeit often with subtly different background colours.


