Summary#
Osy# ships a small set of layout primitives you compose your UI from:
| Primitive | Arranges its children |
|---|---|
Stack | in a column (top to bottom) |
Row | in a row (left to right) |
Box | a single container with no intrinsic direction |
[Composable]
component ProductCard(string name, string price) {
action AddToCart() { }
render {
Stack(gap: 2) {
Text(name);
Row(justify: Justify.SpaceBetween) {
Text(price);
Button("Add to cart", onPress: AddToCart);
}
}
}
}Signature#
Stack / Row / Box — flexbox layout with gap, align, justify
Stack(stickToBottom: true) { … } // follow new content, but never fight the reader
Stack(stickToBottom: following) { … } // …and tell me when they scroll awayDescription#
Spacing — gap#
gap sets the space between a layout's children, as a step on the spacing scale (a whole number). Larger
numbers mean more space; gap: 0 (the default) means no gap.
Stack(gap: 4) { … } // more space between rows
Row(gap: 1) { … } // a little space between columnsAlignment — align and justify#
align and justify are built in — they need no using, and no UI kit. The platform maps them straight to
flexbox, so a typo (align: Align.Centre) is a compile error rather than a silent no-op, and no kit can change what
Align.Center means.
Write the value qualified — Align.Center, never a bare Center. In an argument slot a bare capitalised name
could be a theme token, an enum member or a style keyword, and all three are spelled alike; the group name is what
says which vocabulary you meant. align's group is Align and justify's is Justify, so the value always reads
as group.member. A bare name there is refused, and the refusal names the spelling to write.
align positions children on the cross axis, justify distributes them along the main axis (the axis the
primitive lays out on — vertical for Stack, horizontal for Row).
align | effect |
|---|---|
Align.Start | pack to the start |
Align.Center | center |
Align.End | pack to the end |
Align.Stretch | stretch to fill |
Align.Baseline | align text baselines |
justify | effect |
|---|---|
Justify.Start / Justify.Center / Justify.End | pack to the start / center / end |
Justify.SpaceBetween | equal space between children |
Justify.SpaceAround | equal space around each child |
Justify.SpaceEvenly | equal space between and at the edges |
Row(align: Align.Center, justify: Justify.SpaceBetween) {
Text("Title");
Button("Action", onPress: Act);
}These names are fixed (they map to the browser's flexbox model), so a typo like align: Align.Centre is a compile
error, not a silent no-op.
Following new content — stickToBottom#
A surface that grows while someone is reading it — a chat transcript, a log, a build console — should show the newest content. But it must not yank a reader who has deliberately scrolled up to re-read something earlier. That is the rule everybody gets wrong, and it is one word here:
component Transcript(string[] Lines) {
render {
Stack(overflowY: Overflow.Auto, gap: 2, stickToBottom: true) {
foreach (var line in Lines) { Text(line); }
}
}
}It applies to a scrolling container — one with overflowY: Overflow.Auto. New content scrolls into view while the reader
is at the bottom; the moment they scroll up, following stops, and it resumes by itself when they scroll back down.
Knowing whether it is following, and jumping back
Give it a bool field instead of a literal and the field becomes the container's following state, in both
directions. The container writes false into it when the reader scrolls away and true when they return — so your
app can show a jump to latest affordance — and setting it back to true yourself scrolls to the bottom and
resumes following:
component Chat(string[] Lines) {
bool following = true;
action Jump() { following = true; }
render {
Box {
Stack(overflowY: Overflow.Auto, gap: 2, stickToBottom: following) {
foreach (var line in Lines) { Text(line); }
}
if (!following) {
Button("Jump to latest", onPress: Jump);
}
}
}
}The button is yours to draw and place — the platform ships none. Setting the field is the only way to scroll a container from Osy#, which is why the write half exists at all.
Note the difference between the two forms: stickToBottom: <a bool expression> is an on/off switch ("follow only
while the Live tab is open"), while stickToBottom: <a field you can assign> is the following state. With a
field, the behaviour stays on for as long as the container exists — a false means "not following right now", not
"switched off" — which is what lets the reader resume simply by scrolling back down.
See also#
- component — declaring a component and its
renderblock. - [[ui-component#render-tree]] — the full render vocabulary (text, conditionals, loops, bindings).
- Markdown — rendering markdown text — rendering a message's text inside a transcript, including while it is still arriving.