diff --git a/LANGUAGE.md b/LANGUAGE.md
index 5b9cec36..d802a789 100644
--- a/LANGUAGE.md
+++ b/LANGUAGE.md
@@ -449,6 +449,11 @@ applies to both:
track jumps the thumb's middle there and holds it. A press on the bar presses nothing under it.
- `:focus`, `:focus-visible` and `:focus-within` style the focus, and `outline` (which follows
`border-radius`) draws the ring.
+ - The first gamepad works it too: the d-pad and the left stick move the focus as the arrows do,
+ left and right step a range or a select, A is Enter and B is Escape (it closes a popover). A
+ direction held, on the pad or the keyboard, presses once, again after 0.42 s and then every
+ 0.11 s, on the ui clock. A host sets `held_up` / `held_down` / `held_left` / `held_right`,
+ `pad_a` and `pad_b` on a `UiInput` to drive the same.
- A test drives all of it with `ui_input(i)`.
- **Controls are built in**, each made of plain parts a stylesheet styles (`.ui-label`,
`.ui-track`, `.ui-knob`, `.ui-fill`, `.ui-thumb`, `.ui-field`, `.ui-value`, `.ui-caret`,
diff --git a/changes/defaults-views-templates.md b/changes/defaults-views-templates.md
index 8be7744a..65043f6e 100644
--- a/changes/defaults-views-templates.md
+++ b/changes/defaults-views-templates.md
@@ -78,6 +78,9 @@ What a game's screens found missing, now in `ludic.ui`:
a press on the track jumps the thumb's middle there, and neither presses what is under the bar;
- a popover's own controls take its presses whatever lies under it, a press outside only closes it,
and while one is up the scroll boxes outside it do not take the pointer;
+- the first gamepad moves the focus (d-pad, left stick), steps ranges and selects, and presses (A)
+ and goes back (B); a held direction, on the pad or the arrow keys, repeats after 0.42 s and then
+ every 0.11 s on the ui clock (`UiInput.held_*`, `pad_a`, `pad_b` for a host);
- 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_gamepad.ludic b/examples/library/ui_gamepad.ludic
new file mode 100644
index 00000000..dc4a27bb
--- /dev/null
+++ b/examples/library/ui_gamepad.ludic
@@ -0,0 +1,52 @@
+# ui_gamepad.ludic — the pad and held keys, on the program's clock: a direction held presses once,
+# again after 0.42 s and every 0.11 s after that, at any frame rate; A presses what has the focus,
+# left and right step a range and a select, and B closes a popover.
+import "ludic.ui"
+import "ui_gamepad_parts/Pad.ludic"
+program UiGamepad {
+ numbers float
+ var now: float = 0.0
+ function clock() -> float { return now }
+ function frame(i: UiInput) -> string {
+ ui_input(i)
+ ui_show("Pad", null, 0.0, 0.0, 400.0, 600.0)
+ let page = ui_nodes("Pad", null).children[0]
+ return page.children[len(page.children) - 1].text
+ }
+ # `what` held for `secs` at `hz`, then let go
+ function hold(what: string, secs: float, hz: int) -> string {
+ let frames = int(secs * float(hz) + 0.5)
+ for k in 0 .. frames + 1 {
+ now = now + 1.0 / float(hz)
+ let i = new UiInput
+ i.held_up = what == "up"
+ i.held_down = what == "down"
+ i.held_left = what == "left"
+ i.held_right = what == "right"
+ i.pad_a = what == "a"
+ i.pad_b = what == "b"
+ frame(i)
+ }
+ now = now + 1.0 / float(hz)
+ return frame(new UiInput)
+ }
+ function tap(what: string) -> string { return hold(what, 0.0, 60) }
+ entry {
+ ui_backend(new UiBackend)
+ ui_clock(fn clock)
+ frame(new UiInput)
+ hold("down", 1.0, 240)
+ let fast = tap("a")
+ hold("down", 1.0, 60)
+ hold("up", 0.5, 60)
+ let slow = tap("a")
+ for k in 0 .. 8 { tap("down") }
+ let vol = hold("right", 0.5, 60)
+ tap("down")
+ let q = tap("left")
+ tap("down")
+ let open = tap("a")
+ let shut = tap("b")
+ print(`{fast} | {slow} | {vol} | {q} | {open} | {shut}`)
+ }
+}
diff --git a/examples/library/ui_gamepad_parts/Pad.ludic b/examples/library/ui_gamepad_parts/Pad.ludic
new file mode 100644
index 00000000..6d3776cc
--- /dev/null
+++ b/examples/library/ui_gamepad_parts/Pad.ludic
@@ -0,0 +1,7 @@
+# Pad.ludic - eight buttons, a range, a select and a button that opens a popover, for a gamepad
+component Pad {
+ state last: int = -1
+ state vol: int = 0
+ state q: int = 0
+ state open: bool = false
+}
diff --git a/examples/library/ui_gamepad_parts/Pad.xml b/examples/library/ui_gamepad_parts/Pad.xml
new file mode 100644
index 00000000..0596d59f
--- /dev/null
+++ b/examples/library/ui_gamepad_parts/Pad.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
last {last} vol {vol} q {q} open {open}
+
diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic
index 38ff3596..9b2f93dc 100644
--- a/packages/ludic.ui/index.ludic
+++ b/packages/ludic.ui/index.ludic
@@ -21,6 +21,7 @@ import "controls_parts.ludic"
import "controls_key.ludic"
import "controls_number.ludic"
import "input.ludic"
+import "input_pad.ludic"
import "focus.ludic"
import "scroll.ludic"
import "scroll_bar.ludic"
diff --git a/packages/ludic.ui/input.ludic b/packages/ludic.ui/input.ludic
index ac62809b..f0c80289 100644
--- a/packages/ludic.ui/input.ludic
+++ b/packages/ludic.ui/input.ludic
@@ -1,5 +1,6 @@
# input.ludic - what the player did this frame, read once from the runtime's Input: the pointer, its
-# button, the wheel, the keys a UI answers and what was typed. A test or a host can set it instead.
+# button, the wheel, the keys a UI answers, the first pad and what was typed. A test or a host can set
+# it instead (input_pad.ludic makes a held direction repeat).
export property UiInput {
x: float = -1.0
y: float = -1.0
@@ -17,6 +18,12 @@ export property UiInput {
backspace: bool = false
typed: string = ""
key: int = 0 # any key pressed this frame, for a key-capture field
+ held_up: bool = false # a direction held - an arrow key, the d-pad or the left stick -
+ held_down: bool = false # which presses its arrow now and repeats while it is held
+ held_left: bool = false
+ held_right: bool = false
+ pad_a: bool = false # the pad's south button held: Enter
+ pad_b: bool = false # its east button held: Escape
}
var in_now: UiInput = new UiInput
var in_was_down: bool = false
@@ -45,7 +52,9 @@ function in_read() -> void {
i.backspace = Input.key_pressed(Key.Backspace)
i.typed = Input.text()
i.key = in_any_key()
+ in_held(i)
}
+ in_repeat(i)
in_now = i
}
function in_any_key() -> int {
@@ -57,6 +66,7 @@ function in_any_key() -> int {
# this frame's input, given by a test or a host that reads its own
export function ui_input(i: UiInput) -> void {
in_was_down = in_now.down
+ in_repeat(i)
in_now = i
in_given = true
}
diff --git a/packages/ludic.ui/input_pad.ludic b/packages/ludic.ui/input_pad.ludic
new file mode 100644
index 00000000..cbfa896c
--- /dev/null
+++ b/packages/ludic.ui/input_pad.ludic
@@ -0,0 +1,53 @@
+# input_pad.ludic - the first gamepad, and a held direction: the d-pad and the left stick move the
+# focus as the arrows do, A is Enter and B is Escape; any direction held presses once, then again
+# after 0.42 s and every 0.11 s after that, on the ui clock
+const RP_FIRST: float = 0.42
+const RP_EVERY: float = 0.11
+const PAD_STICK: float = 0.5
+var rp_next: []float = [-1.0, -1.0, -1.0, -1.0]
+var rp_a: bool = false
+var rp_b: bool = false
+# what is held this frame: the arrow keys, the d-pad (SDL's order: 11 up, 12 down, 13 left, 14
+# right) and the stick past half way along its larger axis
+function in_held(i: UiInput) -> void {
+ i.held_up = Input.key_down(Key.Up)
+ i.held_down = Input.key_down(Key.Down)
+ i.held_left = Input.key_down(Key.Left)
+ i.held_right = Input.key_down(Key.Right)
+ if not Input.pad_connected(0) { return }
+ let ax = float(Input.pad_axis(0, 0))
+ let ay = float(Input.pad_axis(0, 1))
+ let flat = Math.abs(ax) >= Math.abs(ay)
+ i.held_up = i.held_up or Input.pad_button(0, 11) or (not flat and ay < 0.0 - PAD_STICK)
+ i.held_down = i.held_down or Input.pad_button(0, 12) or (not flat and ay > PAD_STICK)
+ i.held_left = i.held_left or Input.pad_button(0, 13) or (flat and ax < 0.0 - PAD_STICK)
+ i.held_right = i.held_right or Input.pad_button(0, 14) or (flat and ax > PAD_STICK)
+ i.pad_a = Input.pad_button(0, 0)
+ i.pad_b = Input.pad_button(0, 1)
+}
+# the held directions and buttons as this frame's presses
+function in_repeat(i: UiInput) -> void {
+ let now = an_now()
+ if rp_fire(0, i.held_up, now) { i.up = true }
+ if rp_fire(1, i.held_down, now) { i.down_key = true }
+ if rp_fire(2, i.held_left, now) { i.left = true }
+ if rp_fire(3, i.held_right, now) { i.right = true }
+ if i.pad_a and not rp_a { i.enter = true }
+ if i.pad_b and not rp_b { i.escape = true }
+ rp_a = i.pad_a
+ rp_b = i.pad_b
+}
+function rp_fire(k: int, held: bool, now: float) -> bool {
+ if not held {
+ rp_next[k] = -1.0
+ return false
+ }
+ if rp_next[k] < 0.0 {
+ rp_next[k] = now + RP_FIRST
+ return true
+ }
+ if now + 0.000001 < rp_next[k] { return false }
+ rp_next[k] = rp_next[k] + RP_EVERY
+ if rp_next[k] <= now { rp_next[k] = now + RP_EVERY }
+ return true
+}
diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic
index bcbf9a49..339279b1 100644
--- a/tools/ludic-cli/test.ludic
+++ b/tools/ludic-cli/test.ludic
@@ -763,6 +763,7 @@ function cmd_dev_test() -> int {
controller_case("library/ui_popover_mouse", "", "over the cell 1 | opened 1 dropped 0 closed 0 true | opened 1 dropped 1 closed 0 false | opened 2 dropped 1 closed 1 false | opened 3 dropped 1 closed 2 false", "ui_popover_mouse.ludic (L11: a popover takes the mouse - its own button wins over the cell under it, and a press outside closes it and presses nothing)")
controller_case("library/ui_scrollbar", "", "grab 0 moved 120 far 300 back 60 kept 60 | jump 270 | wheel 230 | pressed 1", "ui_scrollbar.ludic (L11: a scrollbar's thumb held where it was taken and dragged, a press on the track jumps there, the wheel, nothing under the bar pressed)")
controller_case("library/ui_ease", "", "60: 1480 680 slid 1 landed 1 left 0; 120: 1480 680 slid 1 landed 1 left 0; 180: 1480 680 slid 1 landed 1 left 0; 240: 1480 680 slid 1 landed 1 left 0;", "ui_ease.ludic (L11: a transition and an animation at 60, 120, 180 and 240 Hz are the same half way and land exactly on their targets)")
+ controller_case("library/ui_gamepad", "", "last 6 vol 0 q 0 open false | last 0 vol 0 q 0 open false | last 0 vol 2 q 0 open false | last 0 vol 2 q 2 open false | last 0 vol 2 q 2 open true | last 0 vol 2 q 2 open false", "ui_gamepad.ludic (L11: the pad and held keys - a held direction repeats on the clock at any rate, A presses, left and right step, B closes a popover)")
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)")