diff --git a/LANGUAGE.md b/LANGUAGE.md
index e5577cd5..8179a05f 100644
--- a/LANGUAGE.md
+++ b/LANGUAGE.md
@@ -488,7 +488,10 @@ applies to both:
- ``: 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`.
diff --git a/changes/defaults-views-templates.md b/changes/defaults-views-templates.md
index c72f1a27..d4d09e30 100644
--- a/changes/defaults-views-templates.md
+++ b/changes/defaults-views-templates.md
@@ -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;
+- `` 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).
diff --git a/examples/library/ui_keycap.ludic b/examples/library/ui_keycap.ludic
new file mode 100644
index 00000000..39e9898f
--- /dev/null
+++ b/examples/library/ui_keycap.ludic
@@ -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 = ""
+ 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}`)
+ }
+}
diff --git a/packages/ludic.ui/controls_key.ludic b/packages/ludic.ui/controls_key.ludic
index 945d284b..837e8a51 100644
--- a/packages/ludic.ui/controls_key.ludic
+++ b/packages/ludic.ui/controls_key.ludic
@@ -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)
diff --git a/packages/ludic.ui/controls_key_mouse.ludic b/packages/ludic.ui/controls_key_mouse.ludic
new file mode 100644
index 00000000..7c021afe
--- /dev/null
+++ b/packages/ludic.ui/controls_key_mouse.ludic
@@ -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")
+}
diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic
index f9e674b0..5ce0a015 100644
--- a/packages/ludic.ui/index.ludic
+++ b/packages/ludic.ui/index.ludic
@@ -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"
diff --git a/packages/ludic.ui/pointer.ludic b/packages/ludic.ui/pointer.ludic
index 14a544e5..2889af42 100644
--- a/packages/ludic.ui/pointer.ludic
+++ b/packages/ludic.ui/pointer.ludic
@@ -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) }
diff --git a/packages/ludic.ui/select_match.ludic b/packages/ludic.ui/select_match.ludic
index e117db61..26d60fa9 100644
--- a/packages/ludic.ui/select_match.ludic
+++ b/packages/ludic.ui/select_match.ludic
@@ -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
diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic
index 3a6ae557..b69a3fc0 100644
--- a/tools/ludic-cli/test.ludic
+++ b/tools/ludic-cli/test.ludic
@@ -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)")