feat(ui): a key field takes a mouse button, and is :capturing while it listens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 22:04:47 +03:00
parent 6bd3228990
commit d15cc79822
9 changed files with 90 additions and 3 deletions

View file

@ -488,7 +488,10 @@ applies to both:
- `<input type="key" label value shown>`: Enter or a click starts it listening, and the next key
is its value - Tab, the arrows and Enter included. Esc stops it listening, Backspace clears it
(0), and `shown` is what it says for its value instead of the keyboard's label. While it
listens `ui_capturing()` is true, so the program can leave its own keys alone.
listens `ui_capturing()` is true, so the program can leave its own keys alone, and it is
`:capturing` for a stylesheet. A mouse button pressed while it listens is its value too:
`UI_MOUSE_LEFT` (256), `UI_MOUSE_RIGHT` (257) or `UI_MOUSE_MIDDLE` (258), past every key code,
shown as "Left click", "Right click" or "Middle click"; that press does nothing else.
Any control's `note="..."` is a line of help under its label: `.ui-labels > .ui-label +
.ui-note`.

View file

@ -102,6 +102,8 @@ What a game's screens found missing, now in `ludic.ui`:
`image_w` / `image_h`) and `ui_object_fit` for natives;
- `translate="no"` keeps an element's text as written; a title of several lines is translated whole,
else line by line;
- `<input type="key">` takes a mouse button (`UI_MOUSE_LEFT` / `RIGHT` / `MIDDLE`, 256-258) and is
`:capturing` while it listens;
- a transition lands exactly on its end value (it had stopped a rounding error short of it, at every
frame rate).

View file

@ -0,0 +1,55 @@
# ui_keycap.ludic — a key field takes a mouse button as well as a key: listening (and :capturing,
# which a stylesheet can colour), the next press of the left, right or middle button anywhere is its
# value (UI_MOUSE_LEFT / RIGHT / MIDDLE), named for what it is, and that press presses nothing else.
import "ludic.ui"
program UiKeycap {
numbers float
var bind: int = 32
var pressed: int = 0
view Keys {
bind = bind
pressed = pressed
on set_bind(v: int) { bind = v }
on press() { pressed = pressed + 1 }
}
const PAGE: string = "<ui><style>.k:capturing { background: #ff0000 }</style><screen name='k'><input type='key' class='k' label='Jump' value='{bind}' on-change='set_bind(event.value)'/><button on-click='press()'>b</button></screen></ui>"
function frame(x: float, y: float, b: int) -> UiNode {
let i = new UiInput
i.x = x
i.y = y
i.down = b == 0
i.down_right = b == 1
i.down_middle = b == 2
ui_input(i)
ui_show("k", view_keys(), 0.0, 0.0, 400.0, 400.0)
let r: UiNode = ui_nodes("k", view_keys())
ui_place(r, 0.0, 0.0, 400.0, 400.0)
return r
}
function listen(k: UiNode) -> int {
frame(k.x + 5.0, k.y + 5.0, 0)
frame(k.x + 5.0, k.y + 5.0, -1)
return frame(-1.0, -1.0, -1).children[0].bg
}
function take(at: UiNode, b: int) -> string {
frame(at.x + 5.0, at.y + 5.0, b)
frame(at.x + 5.0, at.y + 5.0, -1)
let r = frame(-1.0, -1.0, -1)
let f = r.children[0]
return `{bind} {f.children[1].children[0].text} {f.bg}`
}
entry {
ui_backend(new UiBackend)
ui_load_text(PAGE, "k.xml")
let r = frame(-1.0, -1.0, -1)
let k = r.children[0]
let btn = r.children[1]
let red = listen(k)
let right = take(btn, 1)
listen(k)
let middle = take(btn, 2)
listen(k)
let left = take(btn, 0)
print(`listening {red} | {right} | {middle} | {left} | pressed {pressed}`)
}
}

View file

@ -46,7 +46,9 @@ function ct_key_pressed_away(hit: UiNode) -> void {
function ct_key_text(n: UiNode) -> string {
if ct_capture == n.key { return ui_tr("press a key...") }
if Value.kind(ui_attr(n, "shown")) != 0 { return ui_tr(ui_attr_text(n, "shown")) }
return Input.key_label(Value.as_int(ui_attr(n, "value")))
let v = Value.as_int(ui_attr(n, "value"))
if v >= UI_MOUSE_LEFT and v <= UI_MOUSE_MIDDLE { return ct_mouse_name(v) }
return Input.key_label(v)
}
# a key field is listening: the host should leave the keys alone until it is done (it stays true
# for the frame in which Esc or a key ended it, so that key is not read twice)

View file

@ -0,0 +1,22 @@
# controls_key_mouse.ludic - a key field listening takes a mouse button too: the left, right or
# middle button pressed anywhere is its value, as UI_MOUSE_LEFT, UI_MOUSE_RIGHT or UI_MOUSE_MIDDLE
# (past every key code, for the host to map), and that press does nothing else
export const UI_MOUSE_LEFT: int = 256
export const UI_MOUSE_RIGHT: int = 257
export const UI_MOUSE_MIDDLE: int = 258
function ct_key_mouse(root: UiNode) -> bool {
if ct_capture == "" or ct_capture_at == an_frame { return false }
let b = pe_pressed(in_now, in_last)
if b < 0 { return false }
let n = fr_node(root, ct_capture)
ct_capture = ""
ct_capture_seen = an_frame
fr_press_key = ""
if n != null { ct_change(n, Value.int(UI_MOUSE_LEFT + b)) }
return true
}
function ct_mouse_name(v: int) -> string {
if v == UI_MOUSE_RIGHT { return ui_tr("Right click") }
if v == UI_MOUSE_MIDDLE { return ui_tr("Middle click") }
return ui_tr("Left click")
}

View file

@ -19,6 +19,7 @@ import "controls.ludic"
import "controls_build.ludic"
import "controls_parts.ludic"
import "controls_key.ludic"
import "controls_key_mouse.ludic"
import "controls_number.ludic"
import "input.ludic"
import "input_pad.ludic"

View file

@ -17,7 +17,7 @@ function fr_pointer(root: UiNode) -> void {
fr_hover = keys
tt_track(scope, i.x, i.y)
if top != null and pp_pointer(top) { return }
if sc_drag != "" { return }
if sc_drag != "" or ct_key_mouse(root) { return }
let hit = fr_hit(scope, i.x, i.y)
pe_frame(root, scope)
if i.down and not in_was_down { fr_press(hit, i.x, i.y) }

View file

@ -76,6 +76,7 @@ function sel_pseudo_ok(p: string, arg: string, n: UiNode) -> bool {
if p == "root" { return n.parent == null }
if p == "checked" { return ui_attr_on(n, "checked") }
if p == "active" { return fr_active(n) }
if p == "capturing" { return ct_capture != "" and n.key == ct_capture }
if p == "nth-child" { return sel_nth(arg, n.index + 1) }
ui_err(`:{p}: there is no such pseudo-class here`)
return false

View file

@ -772,6 +772,7 @@ function cmd_dev_test() -> int {
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_fill", "", "0,0 2x20 #030303 .. 98,0 2x20 #fcfcfc | 0,20 50x2 #f2000d .. 0,38 50x2 #0d00f2 .. 0,58 50x2 #0000ff of 70 | 0,85 100x50 | -50,160 200x100 clipped 1 | ratio 90 knob 60", "ui_fill.ludic (L11: linear-gradient backgrounds, object-fit contain and cover, aspect-ratio for boxes and natives)")
controller_case("library/ui_translate", "", "Merhaba Hello Hello Merhaba | c/Kaydet ve cik | Satir bir/Satir iki | Line one/Line two", "ui_translate.ludic (L11: translate=no and yes, a title of several lines translated whole or line by line)")
controller_case("library/ui_keycap", "", "listening 16711680 | 257 Right click -2 | 258 Middle click -2 | 256 Left click -2 | pressed 0", "ui_keycap.ludic (L11: a key field takes the left, right and middle mouse buttons, and is :capturing while it listens)")
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)")