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.Between) {
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.Between | equal space between children |
Justify.Around | equal space around each child |
Justify.Evenly | equal space between and at the edges |
Row(align: Align.Center, justify: Justify.Between) {
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.
The value can come from anywhere#
Align and Justify are ordinary types, so anything of that type is a legal value — a local, a component
parameter, a field, a method's result, a switch, an element of a list. A component that lets its caller decide
how a row is arranged declares the parameter and passes it through, exactly as it would in C#:
[Composable]
component Toolbar(Justify arrange, string label) {
render {
Row(justify: arrange, gap: 2) { Text(label); }
}
}
[Composable]
component Bar(bool compact) {
Align lane = Align.Center;
render {
Stack(align: lane, gap: 2) {
Toolbar(arrange: compact ? Justify.Center : Justify.Between, label: "Tools");
}
}
}A value the compiler can see — a member, or a conditional over members — is resolved where it is written and costs the page nothing. A value it can only know the type of is looked up when the page renders, and the vocabulary travels with that page so nothing else pays for it.
The type is checked either way: justify: takes a Justify, so an Align, a number or a string is a compile
error naming the whole set. Justify? is how a component makes the argument optional — a null one arranges nothing,
the same as leaving it out.
The one value that is refused is a generic component's type parameter — component Passer<T>(T v) used as
justify: v. A generic body is compiled once for every call site, so which vocabulary T stands for is not known
in it and a caller could bind it to anything. Declare the parameter as Justify (as Toolbar above does) and pass
it through; the compiler says exactly that if you hit it.
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.