diff --git a/README.md b/README.md index 95a595d..9cc07a7 100644 --- a/README.md +++ b/README.md @@ -923,7 +923,7 @@ works inside surfaces, docks and windows alike. - **A stack of frames.** Layout is a stack of frames on the context (`blit.layout.Frame`): a content box, a pen, the axis items advance along, the gap between them and how they align across it. Every container (panel, - window, popup, dock, scroll region, columns, stack) pushes its frame at its + window, popup, dock, scroll region, columns, stack, flow) pushes its frame at its begin and pops it at its end, so a panel opened inside a window leaves the window's layout where it was. Outside any container, widgets lay out down a column over the screen. @@ -944,6 +944,27 @@ works inside surfaces, docks and windows alike. blit.widget.text_field(?ctx, "find", ?find, "find"); blit.widget.end_stack(?ctx); ``` +- **Flows.** `begin_flow(?ctx, key, s)`/`end_flow` open a flow as the next + item of the current frame: its items go left to right, and one that would + pass the right edge starts the next row. A row is as tall as its tallest + item, and its items sit in that height as `align` says, read from what the + row measured last frame. An item wider than a whole row takes the row alone + at its width and cuts its label to it, as a control placed by hand does + with `blit.text.fit_box` or `fit_pair`. `blit.widget.flow(?ctx)` gives the + options at the layout style's gaps (`margin.r` between items, `margin.b` + between rows): adjust `gap`, `row_gap`, `align`, the flow's own `w` and `h` + and `item_w`/`item_h`, as for a stack. A flow is sized in its parent as a + stack is, and its width fitted to its content is its items in one row, so + `limit(fit(), 0, max)` wraps at `max`. Flows and stacks nest in each other, + and in windows, docked bodies and overlays. + ```mach + blit.widget.size_next(?ctx, blit.layout.limit(blit.layout.fit(), 0.0::f32, max_w), blit.layout.auto()); + blit.widget.begin_flow(?ctx, "readout", blit.widget.flow(?ctx)); + # one item per label and value pair, cut to the row when wider than it + val r: blit.context.Area = blit.widget.place(?ctx, blit.layout.fit(), blit.layout.fit(), pair_w, line_h); + blit.text.fit_pair(?ctx, blit.theme.LABEL, "gen", 3, faint, gen, ink, pair_gap, r.y0, r); + blit.widget.end_flow(?ctx); + ``` - **Sizing.** Each side of an item is `blit.layout.fixed(px)`, `fit()` (what its content needs), `fill(weight)` (a share of the space the other items leave) or `frac(f)` (a fraction of the space left), and `limit(s, min, max)` clamps diff --git a/demo/gallery/src/sections/layout.mach b/demo/gallery/src/sections/layout.mach index 75c6e0b..36e09d9 100644 --- a/demo/gallery/src/sections/layout.mach +++ b/demo/gallery/src/sections/layout.mach @@ -1,13 +1,16 @@ -# layout: stacks with sizing rules, same-line rows, columns and controls -# placed by hand +# layout: stacks with sizing rules, flows, same-line rows, columns and +# controls placed by hand # # layout is a stack of frames. a stack lays its items along an axis, each # side sized fixed, fit to its content, filling a share of what is left or a -# fraction of it. same_line puts the next item beside the last, columns lay -# whole widgets side by side, and place hands back a rect for anything drawn +# fraction of it. a flow lays its items left to right and wraps them onto +# rows. same_line puts the next item beside the last, columns lay whole +# widgets side by side, and place hands back a rect for anything drawn # outside blit's widgets. use std.types.size.usize; +use std.types.string.str; +use std.types.string.str_len; use blit; @@ -60,6 +63,38 @@ pub fun show(ctx: *blit.context.Context, user: ptr) { blit.widget.size_next(ctx, blit.layout.limit(blit.layout.frac(0.5::f32), blit.context.px(ctx, 120.0::f32), 0.0::f32), blit.layout.auto()); blit.widget.button(ctx, "half, at least 120"); + # a flow: tags wrap onto rows as the width runs out + blit.controls.separator_label(ctx, "A flow"); + var tags: blit.layout.Flow = blit.widget.flow(ctx); + tags.item_w = blit.layout.fit(); + blit.widget.begin_flow(ctx, "tags", tags); + val names: [9]str = [9]str{"layout", "stacks", "flows", "rows", "gaps", "alignment", "wrapping", "sizing", "truncation"}; + var k: usize = 0; + for (k < 9) { + blit.widget.button(ctx, names[k]); + k = k + 1; + } + blit.widget.end_flow(ctx); + + # label and value pairs, centered in their rows, each cut to the row + # when it is wider than one + var pairs: blit.layout.Flow = blit.widget.flow(ctx); + pairs.align = blit.layout.Align.center{}; + blit.widget.begin_flow(ctx, "pairs", pairs); + val t: *blit.theme.Theme = blit.context.theme_of(ctx); + val sp: f32 = blit.context.px(ctx, 6.0::f32); + val lh: f32 = blit.context.line_height(ctx); + val keys: [4]str = [4]str{"generation", "population", "rule", "note"}; + val vals: [4]str = [4]str{"1,024", "53,211", "B3/S23", "a value long enough to be cut when the row is narrow"}; + var p: usize = 0; + for (p < 4) { + val w: f32 = blit.context.text_width(ctx, keys[p]) + sp + blit.context.text_width(ctx, vals[p]); + val r: blit.context.Area = blit.widget.place(ctx, blit.layout.fit(), blit.layout.fit(), w, lh); + blit.text.fit_pair(ctx, blit.theme.LABEL, keys[p], str_len(keys[p]), t.text_faint, vals[p], t.text, sp, r.y0, r); + p = p + 1; + } + blit.widget.end_flow(ctx); + blit.controls.separator_label(ctx, "Same line"); blit.widget.text(ctx, "label"); blit.widget.same_line(ctx); @@ -76,7 +111,6 @@ pub fun show(ctx: *blit.context.Context, user: ptr) { # a control drawn by hand takes its place in the layout like a widget blit.controls.separator_label(ctx, "Placed by hand"); - val t: *blit.theme.Theme = blit.context.theme_of(ctx); val h: f32 = blit.context.px(ctx, 24.0::f32); val r: blit.context.Area = blit.widget.place(ctx, blit.layout.fill(1.0::f32), blit.layout.fixed(h), 0.0::f32, h); val w: f32 = (r.x1 - r.x0) / 8.0::f32; diff --git a/src/dock.mach b/src/dock.mach index bf938b4..b3d825d 100644 --- a/src/dock.mach +++ b/src/dock.mach @@ -62,6 +62,7 @@ use std.types.option.opt; use std.types.result.res; use std.types.size.usize; use std.types.string.str; +use std.types.string.str_len; use allo: std.allocator; use std.allocator.page; @@ -76,6 +77,7 @@ use bid: blit.id; use blit.input; use blit.interact; use blit.layout; +use blit.overlay; use blit.state; use blit.tabs; use blit.theme; @@ -2341,3 +2343,93 @@ test scroll__wheel_stops_at_the_band_a_popup_opens { if (sel freed.err) { ret 1; } ret 0; } + +# what the flow probe lays out: a docked window, a floating one and an +# overlay, each holding the same flow, and the rects its items took in each +#[testing] +rec Flows { + wins: [2]widget.Window; + r: [3][3]context.Area; +} + +# a label wider than any row of the probe's flows +#[testing] +val LONG: str = "a label far too long to fit in any row of this flow"; + +# a flow at most 100 wide, centered in its rows: two items sharing a row, a +# button wider than a whole row, and an item after it +#[testing] +fun flow_of(ctx: *context.Context, r: *context.Area) { + var s: layout.Flow = widget.flow(ctx); + s.align = layout.Align.center{}; + widget.size_next(ctx, layout.limit(layout.fit(), 0.0::f32, 100.0::f32), layout.auto()); + widget.begin_flow(ctx, "flow", s); + r[0] = widget.place(ctx, layout.fit(), layout.fit(), 60.0::f32, 10.0::f32); + r[1] = widget.place(ctx, layout.fit(), layout.fit(), 30.0::f32, 20.0::f32); + widget.size_next(ctx, layout.fit(), layout.auto()); + widget.button(ctx, LONG); + r[2] = widget.place(ctx, layout.fit(), layout.fit(), 20.0::f32, 10.0::f32); + widget.end_flow(ctx); +} + +#[testing] +fun flows_ui(ctx: *context.Context, user: ptr) { + val fl: *Flows = user::*Flows; + val sid: u64 = context.id_of(ctx, "main"); + if (empty(ctx, sid)) { add(ctx, root(ctx, sid), context.id_of(ctx, "A")); } + space(ctx, "main", context.Area{x0: 0.0::f32, y0: 0.0::f32, x1: 200.0::f32, y1: 300.0::f32}); + var i: usize = 0; + for (i < 2) { + val w: widget.WindowArea = widget.begin_window(ctx, fl.wins[i]); + if (w.body != 0) { flow_of(ctx, ?fl.r[i][0]); } + widget.end_window(ctx, w); + i = i + 1; + } + var o: overlay.Overlay = overlay.on_surface(overlay.BOTTOM_RIGHT); + val sh: overlay.Shown = overlay.begin(ctx, "hud", o); + flow_of(ctx, ?fl.r[2][0]); + overlay.end(ctx, sh); +} + +test flow__wraps_alike_in_a_docked_window_a_window_and_an_overlay { + var al: allo.Allocator; + val made: err[allo.Error] = page.make(?al); + if (sel made.err) { ret 1; } + var fl: Flows; + fl.wins[0] = widget.window("A", 0.0::f32, 0.0::f32, 160.0::f32, 160.0::f32); + fl.wins[1] = widget.window("B", 220.0::f32, 20.0::f32, 160.0::f32, 160.0::f32); + var d: driver.Driver; + val e: err[allo.Error] = driver.init(?d, ?al, 600.0::f32, 400.0::f32, flows_ui, (?fl)::ptr); + if (sel e.err) { ret 1; } + driver.step(?d); + driver.settle(?d); + if (node_of(?d.ctx, win(?d, "A")) == 0 || node_of(?d.ctx, win(?d, "B")) != 0) { ret 2; } + + val g: f32 = context.px(?d.ctx, context.look_of(?d.ctx, theme.LAYOUT).margin.r); + val rg: f32 = context.px(?d.ctx, context.look_of(?d.ctx, theme.LAYOUT).margin.b); + val paths: [3]str = [3]str{"A", "B", "hud"}; + var i: usize = 0; + for (i < 3) { + val r: *context.Area = ?fl.r[i][0]; + val x: f32 = r[0].x0; + val y: f32 = r[1].y0; + # the first row is as tall as its taller item, the shorter centered in it + if (r[0].y0 != y + 5.0::f32 || r[1].x0 != x + 60.0::f32 + g) { ret 3; } + if (r[2].x0 != x) { ret 4; } + # the button took the second row alone, cut to the flow's 100 pixels, + # and the last item the third row + val b: u64 = bid.of_label(driver.find(?d, paths[i]), LONG); + val br: opt[context.Area] = driver.rect(?d, b); + if (sel br.none) { ret 5; } + val bh: f32 = br.some.y1 - br.some.y0; + if (br.some.x1 - br.some.x0 != 100.0::f32) { ret 6; } + if (r[2].y0 != y + 20.0::f32 + rg + bh + rg) { ret 7; } + val shown: str = driver.text_of(?d, b); + if (str_len(shown) == 0 || str_len(shown) >= str_len(LONG)) { ret 8; } + i = i + 1; + } + + val freed: err[allo.Error] = driver.free(?d); + if (sel freed.err) { ret 1; } + ret 0; +} diff --git a/src/layout.mach b/src/layout.mach index b6acedb..1a903d2 100644 --- a/src/layout.mach +++ b/src/layout.mach @@ -27,6 +27,15 @@ # # a frame also measures what it holds: need_w and need_h are the natural size # of its content, which a stack fitted to its content takes the next frame. +# +# a flow is a frame laid out along x that wraps: an item that would pass the +# right edge of a row already holding one starts the next row, a row gap below +# the bottom of the row before. a row is as tall as its tallest item, and its +# items size and align across against that height, as the flow's owner read it +# for the row from last frame (row_len). an item wider than a whole row takes +# the row alone, at the row's width, and cuts its own content to it. fill and +# frac along x take the rest of the row or a fraction of it. a flow whose width +# is open never wraps, so its natural width is its items in one row. use std.types.bool.bool; use std.types.bool.false; @@ -97,6 +106,25 @@ pub rec Stack { item_h: Size; } +# the options a flow opens with +# --- +# align: where its items sit across their row's height +# gap: pixels between items in a row +# row_gap: pixels between rows +# w: how the flow's own width is sized in its parent, auto for fill +# h: how the flow's own height is sized in its parent, auto for fit +# item_w: the width rule its items take unless they say otherwise, auto for each widget's own +# item_h: the height rule its items take unless they say otherwise, auto for each widget's own +pub rec Flow { + align: Align; + gap: f32; + row_gap: f32; + w: Size; + h: Size; + item_w: Size; + item_h: Size; +} + # one level of layout, in the local pixels of the space it was opened in # --- # axis: the direction items advance in @@ -132,6 +160,14 @@ pub rec Stack { # rule_w: the frame's own width rule in its parent # rule_h: the frame's own height rule in its parent # hidden: 1 when the frame lays out only to measure, drawing and claiming nothing +# flow: 1 when items wrap onto rows (see rows) +# row_gap: pixels between a flow's rows +# row: the flow's current row, from 0 +# row_n: items placed in the current row +# row_len: height the current row measured last frame, 0 when not known +# next_len: height the next row measured last frame, 0 when not known +# done_h: natural height of the row the last wrap closed +# synced: rows whose measures the frame's owner has read, row + 1 once current pub rec Frame { axis: Axis; align: Align; @@ -166,6 +202,14 @@ pub rec Frame { rule_w: Size; rule_h: Size; hidden: u8; + flow: u8; + row_gap: f32; + row: usize; + row_n: usize; + row_len: f32; + next_len: f32; + done_h: f32; + synced: usize; } # a placed item: its rect and what it adds to its frame's measure @@ -179,6 +223,7 @@ pub rec Frame { # weight: fill weight along the frame's axis, 0 when the item does not fill # fit_w: 1 when the width is the natural width because nothing else sized it # fit_h: 1 when the height is the natural height because nothing else sized it +# wrap: 1 when the item starts a flow's next row pub rec Item { x: f32; y: f32; @@ -189,6 +234,7 @@ pub rec Item { weight: f32; fit_w: u8; fit_h: u8; + wrap: u8; } # the rule deferring to the frame and the widget @@ -260,6 +306,42 @@ pub fun column(x: f32, y: f32, w: f32, h: f32, gap: f32) Frame { ret f; } +# a flow's options: start-aligned, gap pixels between items and row_gap +# between rows, filling its parent's width and fitting its content's height +# --- +# gap: pixels between items in a row +# row_gap: pixels between rows +pub fun flow(gap: f32, row_gap: f32) Flow { + var s: Flow; + s.gap = gap; + s.row_gap = row_gap; + ret s; +} + +# a frame flowing items into rows w wide from (x, y), growing down with them +# +# the width is open when w is not positive: the items share one row. +# --- +# x: left in local pixels +# y: top in local pixels +# w: width in pixels, not positive for one row that never wraps +# gap: pixels between items in a row +# row_gap: pixels between rows +pub fun rows(x: f32, y: f32, w: f32, gap: f32, row_gap: f32) Frame { + var f: Frame = column(x, y, w, 0.0::f32, gap); + if (w <= 0.0::f32) { f.open = f.open | OPEN_W; } + f.axis = Axis.horizontal{}; + f.flow = 1; + f.row_gap = row_gap; + f.line_y0 = y; + f.line_y1 = y; + ret f; +} + +# how far past a row's edge an item may reach and still fit, a sixty-fourth of +# a pixel, so float rounding of the pen never wraps an item that fits exactly +val ROW_SLACK: f32 = 0.015625; + fun maxf(a: f32, b: f32) f32 { if (a > b) { ret a; } ret b; @@ -356,6 +438,10 @@ pub fun resolve(f: *Frame, sw: Size, sh: Size, nat_w: f32, nat_h: f32) Item { val wide: bool = (f.open & OPEN_W) == 0; val tall: bool = (f.open & OPEN_H) == 0; + if (f.flow != 0) { + in_row(f, ?it, rw, rh, nat_w, nat_h); + ret it; + } if (across(f)) { it.x = f.x; it.w = along(f, rw, nat_w, f.w, x1 - f.x, wide, ?it.fit_w); @@ -382,11 +468,69 @@ pub fun resolve(f: *Frame, sw: Size, sh: Size, nat_w: f32, nat_h: f32) Item { ret it; } +# the rect of the next item in a flow: at the pen, or at the start of the next +# row when it would pass the edge of a row already holding an item +fun in_row(f: *Frame, it: *Item, rw: Size, rh: Size, nat_w: f32, nat_h: f32) { + val x1: f32 = f.x0 + f.w; + val wide: bool = (f.open & OPEN_W) == 0; + var top: f32 = f.y; + var len: f32 = f.row_len; + it.x = f.x; + it.w = along(f, rw, nat_w, f.w, x1 - f.x, wide, ?it.fit_w); + if (wide && f.row_n > 0 && it.x + it.w > x1 + ROW_SLACK) { + it.wrap = 1; + it.x = f.x0; + top = f.line_y1 + f.row_gap; + len = f.next_len; + it.w = along(f, rw, nat_w, f.w, f.w, wide, ?it.fit_w); + } + if (wide && it.w > f.w) { it.w = f.w; } + it.h = over(f, rh, nat_h, len, ?it.fit_h); + it.y = top + offset(f, len, it.h); +} + +# place an item in a flow's row, starting the next row when it wraps +fun commit_row(f: *Frame, it: Item) { + if (it.wrap != 0) { + f.done_h = f.line_h; + f.need_h = f.need_h + f.row_gap; + f.y = f.line_y1 + f.row_gap; + f.line_y0 = f.y; + f.line_y1 = f.y; + f.line_w = 0.0::f32; + f.line_h = 0.0::f32; + f.row = f.row + 1; + f.row_n = 0; + f.row_len = f.next_len; + f.next_len = 0.0::f32; + } + if (f.bare == 0) { + if (f.items > 0) { f.need_w = f.need_w + f.gap; } + if (f.row_n > 0) { f.line_w = f.line_w + f.gap; } + } + f.bare = 0; + f.need_w = f.need_w + it.nat_w; + f.line_w = f.line_w + it.nat_w; + if (it.nat_h > f.line_h) { + f.need_h = f.need_h + it.nat_h - f.line_h; + f.line_h = it.nat_h; + } + f.line_x1 = it.x + it.w; + f.line_y1 = maxf(f.line_y1, it.y + it.h); + f.x = it.x + it.w + f.gap; + f.row_n = f.row_n + 1; + f.items = f.items + 1; +} + # place an item resolve gave, moving the pen past it and adding it to the measure # --- # f: frame # it: the item pub fun commit(f: *Frame, it: Item) { + if (f.flow != 0) { + commit_row(f, it); + ret; + } val spaced: bool = f.items > 0 && f.bare == 0; f.bare = 0; if (across(f)) { @@ -584,3 +728,68 @@ test layout__same_line_and_skip { if (c.y != 40.0::f32 || c.need_h != 40.0::f32) { ret 5; } ret 0; } + +test layout__flow_wraps_rows_and_gives_a_wide_item_the_row { + # 100 wide, 4 between items, 6 between rows + var f: Frame = rows(0.0::f32, 0.0::f32, 100.0::f32, 4.0::f32, 6.0::f32); + val a: Item = resolve(?f, fit(), fit(), 40.0::f32, 10.0::f32); + commit(?f, a); + # 44 + 56 meets the edge exactly and stays + val b: Item = resolve(?f, fit(), fit(), 56.0::f32, 16.0::f32); + if (b.wrap != 0 || b.x != 44.0::f32 || b.y != 0.0::f32) { ret 1; } + commit(?f, b); + # the next wraps below the row's tallest item + val c: Item = resolve(?f, fit(), fit(), 10.0::f32, 8.0::f32); + if (c.wrap != 1 || c.x != 0.0::f32 || c.y != 22.0::f32) { ret 2; } + commit(?f, c); + if (f.row != 1 || f.done_h != 16.0::f32) { ret 3; } + # wider than a row: it wraps and takes the row, cut to its width + val d: Item = resolve(?f, fit(), fit(), 150.0::f32, 8.0::f32); + if (d.wrap != 1 || d.x != 0.0::f32 || d.y != 36.0::f32 || d.w != 100.0::f32) { ret 4; } + commit(?f, d); + # and the item after it starts the row below + val e: Item = resolve(?f, fill(1.0::f32), fit(), 10.0::f32, 8.0::f32); + if (e.wrap != 1 || e.y != 50.0::f32 || e.w != 100.0::f32) { ret 5; } + commit(?f, e); + + # the natural width is the items in one row, the height the rows stacked + if (f.need_w != 40.0::f32 + 56.0::f32 + 10.0::f32 + 150.0::f32 + 10.0::f32 + 16.0::f32) { ret 6; } + if (f.need_h != 16.0::f32 + 8.0::f32 * 3.0::f32 + 18.0::f32) { ret 7; } + + # a first item wider than the row has nothing to wrap from + var g: Frame = rows(0.0::f32, 0.0::f32, 100.0::f32, 4.0::f32, 6.0::f32); + val h: Item = resolve(?g, fit(), fit(), 150.0::f32, 8.0::f32); + if (h.wrap != 0 || h.w != 100.0::f32) { ret 8; } + + # an open width never wraps or cuts + var o: Frame = rows(0.0::f32, 0.0::f32, 0.0::f32, 4.0::f32, 6.0::f32); + commit(?o, resolve(?o, fit(), fit(), 150.0::f32, 8.0::f32)); + val k: Item = resolve(?o, fit(), fit(), 150.0::f32, 8.0::f32); + if (k.wrap != 0 || k.x != 154.0::f32 || k.w != 150.0::f32) { ret 9; } + ret 0; +} + +test layout__flow_aligns_items_in_their_row { + # each row as tall as last frame measured it + var f: Frame = rows(0.0::f32, 0.0::f32, 100.0::f32, 4.0::f32, 6.0::f32); + f.align = Align.center{}; + f.row_len = 20.0::f32; + f.next_len = 12.0::f32; + val a: Item = resolve(?f, fit(), fit(), 60.0::f32, 8.0::f32); + if (a.y != 6.0::f32) { ret 1; } + commit(?f, a); + commit(?f, resolve(?f, fit(), fit(), 30.0::f32, 20.0::f32)); + # the wrapped item centers in the next row's height + val b: Item = resolve(?f, fit(), fit(), 60.0::f32, 8.0::f32); + if (b.wrap != 1 || b.y != 28.0::f32) { ret 2; } + commit(?f, b); + if (f.row_len != 12.0::f32 || f.next_len != 0.0::f32) { ret 3; } + + # stretch and fill across take the row's height + f.align = Align.stretch{}; + if (resolve(?f, fit(), fit(), 10.0::f32, 8.0::f32).h != 12.0::f32) { ret 4; } + if (resolve(?f, fit(), fill(1.0::f32), 10.0::f32, 8.0::f32).h != 12.0::f32) { ret 5; } + f.align = Align.end{}; + if (resolve(?f, fit(), fit(), 10.0::f32, 8.0::f32).y != 26.0::f32 + 4.0::f32) { ret 6; } + ret 0; +} diff --git a/src/widget.mach b/src/widget.mach index d1840d9..2d0d86a 100644 --- a/src/widget.mach +++ b/src/widget.mach @@ -5,9 +5,9 @@ # layout is the context's stack of frames (see blit.layout). every container # pushes the frame its content is laid out in and pops it at its end, and # begin_stack opens a horizontal or vertical stack as an item of the current -# frame. a widget asks the top frame for its rect with its own size rules and -# the size its content needs, which size_next and a stack's item rules -# override. same_line puts an item beside the last one. the helpers place rows +# frame, and begin_flow a flow, whose items wrap onto rows. a widget asks the +# top frame for its rect with its own size rules and the size its content +# needs, which size_next and a stack's or a flow's item rules override. same_line puts an item beside the last one. the helpers place rows # by hand: space and advance move the pen, cell_x0 and cell_x1 split the # column into equal cells for widgets that take a rect (button_at), and # begin_columns lays whole widgets side by side. the gaps and padding come @@ -232,7 +232,9 @@ fun top(ctx: *context.Context) *layout.Frame { if (context.layout_depth(ctx) == 0) { context.push_layout(ctx, layout.column(0.0::f32, 0.0::f32, ctx.sw, ctx.sh, gap(ctx))); } - ret context.top_layout(ctx); + val f: *layout.Frame = context.top_layout(ctx); + if (f.flow != 0 && f.synced != f.row + 1) { sync_rows(ctx, f); } + ret f; } # the default width of a widget that spans the space it is given @@ -370,6 +372,12 @@ fun guessing(f: *layout.Frame, s: layout.Stack, it: layout.Item) bool { over = it.fit_h; } if (main == 0 || over != 0) { ret true; } + ret by_size(f, it); +} + +# whether f aligns an item opening at it by a size the item has not measured: +# centered or end-aligned across f's axis while fitted to its content there +fun by_size(f: *layout.Frame, it: layout.Item) bool { var cross: u8 = it.fit_w; if (layout.across(f)) { cross = it.fit_h; } ret cross != 0 && f.inline == 0 && ((sel f.align.center) || (sel f.align.end)); @@ -413,13 +421,19 @@ pub fun begin_stack(ctx: *context.Context, key: str, s: layout.Stack) { c.item_h = s.item_h; c.prev_fixed = m.fixed; c.prev_weight = m.weight; - c.id = id; - c.slot = it; - c.rule_w = rw; - c.rule_h = rh; - if (m.seen == 0 && guessing(f, s, it)) { c.hidden = 1; } - context.push_layout(ctx, c); - if (c.hidden != 0) { + enter(ctx, ?c, id, it, rw, rh, m.seen == 0 && guessing(f, s, it)); +} + +# push a stack's or a flow's frame c, opened as item it of the current frame +# under the rules rw and rh, laying it out only to measure when hide is set +fun enter(ctx: *context.Context, c: *layout.Frame, id: u64, it: layout.Item, rw: layout.Size, rh: layout.Size, hide: bool) { + c.id = id; + c.slot = it; + c.rule_w = rw; + c.rule_h = rh; + if (hide) { c.hidden = 1; } + context.push_layout(ctx, @c); + if (hide) { context.push_measure(ctx); context.wake_at(ctx, ctx.in.time); } @@ -432,6 +446,12 @@ pub fun begin_stack(ctx: *context.Context, key: str, s: layout.Stack) { # --- # ctx: context pub fun end_stack(ctx: *context.Context) { + leave(ctx); +} + +# pop a stack's or a flow's frame, keep what it measured and place it in its +# parent +fun leave(ctx: *context.Context) { val c: layout.Frame = @context.top_layout(ctx); context.pop_layout(ctx); if (c.hidden != 0) { context.pop_measure(ctx); } @@ -452,6 +472,108 @@ pub fun end_stack(ctx: *context.Context) { layout.commit(top(ctx), it); } +# a flow's options, at the layout style's gaps: margin.r between items in a +# row and margin.b between rows, start-aligned, filling its parent's width +# and fitting its content's height +# --- +# ctx: context +# ret: options to adjust and pass to begin_flow +pub fun flow(ctx: *context.Context) layout.Flow { + ret layout.flow(across(ctx), gap(ctx)); +} + +# the natural height a flow's row measured, kept for the next frame +# --- +# h: the tallest natural height of the row's items +rec Row { + h: f32; +} + +# the id a flow keeps row n's measure under +fun row_id(f: *layout.Frame, n: usize) u64 { + ret bid.child_int(f.id, n::u64); +} + +# the height row n of a flow measured last frame, 0 when it has none +fun row_len(ctx: *context.Context, f: *layout.Frame, n: usize) f32 { + val r: *Row = state.find[Row](ctx, row_id(f, n)); + if (r == nil) { ret 0.0::f32; } + ret r.h; +} + +# keep the height of a flow's row n for the next frame +fun keep_row(ctx: *context.Context, f: *layout.Frame, n: usize, h: f32) { + state.get[Row](ctx, row_id(f, n)).h = h; +} + +# bring a flow's row measures up to its current row: keep the row a wrap +# closed, and read the height the row after this one measured last frame +fun sync_rows(ctx: *context.Context, f: *layout.Frame) { + if (f.id != 0) { + if (f.row > 0) { keep_row(ctx, f, f.row - 1, f.done_h); } + f.next_len = row_len(ctx, f, f.row + 1); + } + f.synced = f.row + 1; +} + +# open a flow as the next item of the current frame +# +# the flow's items go left to right, s.gap apart, and an item that would pass +# the flow's right edge starts a new row, s.row_gap below the last (see +# blit.layout). a row is as tall as its tallest item, and its items sit across +# that height as s.align says, read from what the row measured last frame, +# kept in the state store under the flow's id. an item wider than a whole row +# takes the row alone at the row's width, and a widget cuts its label to it as +# its kind's overflow says, as a control placed by hand does with +# blit.text.fit_box or fit_pair. the flow is sized in its parent as a stack +# is (see begin_stack). a width fitted to the content is its items in one +# row, so a flow sized by layout.limit(layout.fit(), 0, max) wraps at max and +# is as narrow as its items when they fit in less. a flow that is not +# start-aligned, or that its parent aligns by its size, lays out its first +# frame only to measure, as a stack does. flows and stacks nest in each other. +# pair it with end_flow. +# --- +# ctx: context +# key: the flow's key in the current id scope +# s: its options (see flow) +pub fun begin_flow(ctx: *context.Context, key: str, s: layout.Flow) { + val id: u64 = context.id_of(ctx, key); + val m: *Measure = state.get[Measure](ctx, id); + val f: *layout.Frame = top(ctx); + val rw: layout.Size = layout.pick(f.next_w, s.w, layout.pick(f.item_w, wide(), wide())); + val rh: layout.Size = layout.pick(f.next_h, s.h, layout.pick(f.item_h, layout.fit(), layout.fit())); + val it: layout.Item = layout.resolve(f, rw, rh, m.w, m.h); + f.next_w = layout.auto(); + f.next_h = layout.auto(); + + # a width fitted to the content wraps at its largest size, if it has one + var w: f32 = it.w; + if (it.fit_w != 0) { w = rw.max; } + var c: layout.Frame = layout.rows(it.x, it.y, w, s.gap, s.row_gap); + c.h = it.h; + if (it.fit_h == 0) { c.open = c.open & layout.OPEN_W; } + c.align = s.align; + c.item_w = s.item_w; + c.item_h = s.item_h; + c.id = id; + c.row_len = row_len(ctx, ?c, 0); + val hide: bool = m.seen == 0 && (!(sel s.align.start) || by_size(f, it)); + enter(ctx, ?c, id, it, rw, rh, hide); +} + +# close the flow begin_flow opened and place it in its parent +# +# a side fitted to the content takes the size measured this frame: a height +# its rows stacked, a width its items in one row up to the width's largest +# size. +# --- +# ctx: context +pub fun end_flow(ctx: *context.Context) { + val f: *layout.Frame = top(ctx); + if (f.id != 0 && f.items > 0) { keep_row(ctx, f, f.row, f.line_h); } + leave(ctx); +} + # move the pen past an item len pixels long along the current frame's axis, # and the gap after it #