diff --git a/LANGUAGE.md b/LANGUAGE.md index d6b69b0b..0dfebdbd 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -457,6 +457,9 @@ applies to both: anywhere) or by Enter or the pad's A held on it; it is `:active` in between. - A scroll box (`overflow: auto` or ``) takes the wheel, has a scrollbar that can be dragged (`scrollbar-color`), clips what it holds and pulls the focused control into view. + - `scroll-top="{px}"` holds a scroll box at that offset every frame; the wheel, the bar and the + focus still move it for the frame and say so with `on-scroll` (`event.value`), for the program + to keep. `ui_scroll_set("id", px)` (an id or a key) moves a box once. - The scrollbar is `.ui-scrollbar` with a `.ui-thumb`, styled like a control's parts. A press on the thumb holds it where it was taken and the pointer moves it in proportion; a press on the track jumps the thumb's middle there and holds it. A press on the bar presses nothing under it. diff --git a/changes/defaults-views-templates.md b/changes/defaults-views-templates.md index f58cb2b3..ff116e89 100644 --- a/changes/defaults-views-templates.md +++ b/changes/defaults-views-templates.md @@ -96,6 +96,8 @@ What a game's screens found missing, now in `ludic.ui`: as a percentage or a `calc()` of one, of the containing block; - a nine-slice's corners are clamped to half the box in each direction on its own and cut on whole pixels (`ui_nine_cuts`), so a small key cap has no seam; +- `scroll-top="{px}"` holds a scroll box at an offset, with `on-scroll` when the player moves it; + `ui_scroll_set(id, px)` moves one once; - a transition lands exactly on its end value (it had stopped a rounding error short of it, at every frame rate). diff --git a/examples/library/ui_scroll_hold.ludic b/examples/library/ui_scroll_hold.ludic new file mode 100644 index 00000000..6808ae19 --- /dev/null +++ b/examples/library/ui_scroll_hold.ludic @@ -0,0 +1,32 @@ +# ui_scroll_hold.ludic — scroll-top holds a box at an offset: one follows its state, which on-scroll +# keeps in step with the wheel; one stays where it is held; and ui_scroll_set moves another once. +import "ludic.ui" +import "ui_scroll_hold_parts/Held.ludic" +program UiScrollHold { + numbers float + function frame(x: float, y: float, wheel: float) -> UiNode { + let i = new UiInput + i.x = x + i.y = y + i.wheel = wheel + ui_input(i) + ui_show("Held", null, 0.0, 0.0, 400.0, 600.0) + let root: UiNode = ui_nodes("Held", null) + ui_place(root, 0.0, 0.0, 400.0, 600.0) + return root.children[0] + } + function at(p: UiNode) -> string { return `{int(p.children[0].scroll_y)} {int(p.children[1].scroll_y)} {int(p.children[2].scroll_y)} {p.children[3].text}` } + entry { + ui_backend(new UiBackend) + let p = frame(-1.0, -1.0, 0.0) + let first = at(frame(-1.0, -1.0, 0.0)) + frame(p.children[0].x + 5.0, p.children[0].y + 5.0, 1.0) + let followed = at(frame(-1.0, -1.0, 0.0)) + frame(p.children[1].x + 5.0, p.children[1].y + 5.0, -1.0) + let fixed = at(frame(-1.0, -1.0, 0.0)) + ui_scroll_set("free", 70.0) + let set = at(frame(-1.0, -1.0, 0.0)) + frame(p.children[2].x + 5.0, p.children[2].y + 5.0, 1.0) + print(`{first} | {followed} | {fixed} | {set} | {at(frame(-1.0, -1.0, 0.0))}`) + } +} diff --git a/examples/library/ui_scroll_hold_parts/Held.lss b/examples/library/ui_scroll_hold_parts/Held.lss new file mode 100644 index 00000000..3d39fcdf --- /dev/null +++ b/examples/library/ui_scroll_hold_parts/Held.lss @@ -0,0 +1,3 @@ +.page { width: 400px; gap: 4px } +.list { overflow: auto; width: 200px; height: 100px } +p { height: 40px } diff --git a/examples/library/ui_scroll_hold_parts/Held.ludic b/examples/library/ui_scroll_hold_parts/Held.ludic new file mode 100644 index 00000000..a3c2c779 --- /dev/null +++ b/examples/library/ui_scroll_hold_parts/Held.ludic @@ -0,0 +1,5 @@ +# Held.ludic - three scroll boxes: one held at its state's offset and told when it moves, one held +# at a fixed offset, and one the program moves once +component Held { + state top: float = 120.0 +} diff --git a/examples/library/ui_scroll_hold_parts/Held.xml b/examples/library/ui_scroll_hold_parts/Held.xml new file mode 100644 index 00000000..94d975cb --- /dev/null +++ b/examples/library/ui_scroll_hold_parts/Held.xml @@ -0,0 +1,6 @@ +
+

{r}

+

{r}

+

{r}

+

top {top}

+
diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic index 184f3b8a..0bc7eadc 100644 --- a/packages/ludic.ui/index.ludic +++ b/packages/ludic.ui/index.ludic @@ -25,6 +25,7 @@ import "input_pad.ludic" import "focus.ludic" import "scroll.ludic" import "scroll_bar.ludic" +import "scroll_held.ludic" import "popover.ludic" import "popover_anchor.ludic" import "popover_within.ludic" diff --git a/packages/ludic.ui/scroll.ludic b/packages/ludic.ui/scroll.ludic index 22e51b4a..0e5f7592 100644 --- a/packages/ludic.ui/scroll.ludic +++ b/packages/ludic.ui/scroll.ludic @@ -32,9 +32,11 @@ function sc_walk(n: UiNode, live: bool) -> void { if n.kind == UI_SCROLL { let room = n.ch - ly_vin(n) let most = Math.max(n.content_h - ly_vin(n) - room, 0.0) - var off = sc_get(n.key) + let held = sc_held(n, sc_get(n.key), live) + var off = held if live { off = sc_input(n, off, room, most) } off = Math.clamp(off, 0.0, most) + if live and off != Math.clamp(held, 0.0, most) { ui_fire(n, "scroll", Value.float(off / ui_px(1.0))) } sc_put(n.key, off) n.scroll_y = off if off != 0.0 { diff --git a/packages/ludic.ui/scroll_held.ludic b/packages/ludic.ui/scroll_held.ludic new file mode 100644 index 00000000..8e959ab8 --- /dev/null +++ b/packages/ludic.ui/scroll_held.ludic @@ -0,0 +1,29 @@ +# scroll_held.ludic - a scroll box held at an offset: scroll-top="{px}" puts it there every frame +# (the wheel, the bar and the focus still move it, and say so with on-scroll and event.value, for +# the program to keep or not), and ui_scroll_set(id or key, px) moves it once +var sc_want_keys: []string = new []string +var sc_want_px: []float = new []float +export function ui_scroll_set(key: string, px: float) -> void { + push(sc_want_keys, key) + push(sc_want_px, px) +} +# how far a box is scrolled before this frame's input: what it was, what ui_scroll_set asked for, +# or what its scroll-top says +function sc_held(n: UiNode, off: float, live: bool) -> float { + var at = off + if live { + let keys = new []string + let pxs = new []float + for i in 0 .. len(sc_want_keys) { + if sc_want_keys[i] == n.key or (n.id != "" and sc_want_keys[i] == n.id) { at = ui_px(sc_want_px[i]) } else { + push(keys, sc_want_keys[i]) + push(pxs, sc_want_px[i]) + } + } + sc_want_keys = keys + sc_want_px = pxs + } + let v = ui_attr(n, "scroll-top") + if Value.kind(v) != 0 { at = ui_px(el_num(vl_text(v))) } + return at +} diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index 63b4ec94..74a68beb 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -769,6 +769,7 @@ function cmd_dev_test() -> int { controller_case("library/ui_text", "", "Photograph the Be...@100 y0 | Short@160 y20 | Medium words@133 y40 | lines 5 160 one two@160 y66 three@160 y98 | tall 30 | em 20 24 | tie #00ff00", "ui_text.ludic (L11: text-fit shrinks then cuts, line-height, em against the final font size, a sheet over its imports on a tie)") controller_case("library/ui_calc", "", "min 200 max 82 clamp 50 21 | p 150,60 q 255,80 r 30", "ui_calc.ludic (L11: min(), max() and clamp(), and insets as percentages and calc() of the containing block)") controller_case("library/ui_nine", "", "cap 0 12 18 30/ 0 8 8 16 | wide 0 12 88 100/ 0 12 28 40 | odd 10 21 21 31/ 4 8 8 13", "ui_nine.ludic (L11: a nine-slice's corners clamped to half the box each way, on whole pixels)") + controller_case("library/ui_scroll_hold", "", "120 50 0 top 120 | 80 50 0 top 80 | 80 50 0 top 80 | 80 50 70 top 80 | 80 50 30 top 80", "ui_scroll_hold.ludic (L11: scroll-top holds a box at an offset, on-scroll follows the wheel, ui_scroll_set moves one once)") controller_case("library/ui_inputs", "", "count 12 12 32 0.5 0 | 20 32 0.5 0 | 19 32 0.5 0 | How many to carry | Space bar listening 1 19 9 0.5 0 | 19 9 0.5 0 | 19 0 0.5 0 | 60% 0.50 s | 19 0 0.6 9 | 7!", "ui_inputs.ludic (L11: number fields, key capture with Esc and Backspace, notes, range formats, an event named set, a string prop given a number)") controller_case("library/ui_boxes", "", "list 120 of 200 | text at 32 152 wide, 3 lines, row 60 | zero 0x0 | half 140x36 pad 4 2 | order abc at 16 | fill 98x8 of 200x12", "ui_boxes.ludic (L11: flex-shrink to the leftover room, text wrapping beside its siblings, zero sizes, calc(), order, the range track as a row)") controller_case("library/ui_look", "", "root div.in.i.m pad 5 margin 9 color #ff0000 text Hi 2,53 #102030 | text Hi 0,50 #eeeeee | opacity 0.25 | nine 40 #ffffff nine 40 #ff0000 of 2 image photo.png #ffffff | image icon:tent #00ff00 | text first line 29,205 #eeeeee | text second line 29,223 #eeeeee by id at 84,50 | flipped at 246,268 100x32", "ui_look.ludic (L11: nested components styled by specificity, text-shadow, ui_opacity, border-image tints, pictures, tooltip lines, anchored popovers)")