plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
ee3e4e6
— Peter John
2026-11-04T14:56:25+05:30
Input steps 1 and 2: shared Input with keys, onInput on actors
- docs/backlog.md +1 -1
- docs/game-scene-styles.md +22 -29
- plum-examples/games/functional/game_state_view.plum +70 -34
- plum-examples/games/imperative/game.plum +19 -8
- plum-examples/games/imperative/game_scene.plum +24 -10
- plum-examples/games/imperative/scene.plum +18 -7
- plum-examples/games/pure/game_pure_scene.plum +28 -18
- plum-game/web-test/fixtures/stress_pure_scene.plum +1 -1
- plum-game/web-test/fixtures/stress_state_view.plum +15 -16
- plum-std/game/Input.plum +166 -0
- plum-std/game/functional/Ui.plum +13 -25
- plum-std/game/imperative/Scene.plum +71 -2
- plum-std/game/pure/PureScene.plum +83 -79
- plum-std/game/pure/SceneInput.plum +118 -65
docs/backlog.md
CHANGED
|
@@ -39,4 +39,4 @@ Open work, in the order the user queued it, plus smaller items found during buil
|
|
|
39
39
|
- **Retire PureScene?** — after the component-style functional game, decide whether `std/game/pure` + `games/pure` stay. (Decision record §4b)
|
|
40
40
|
- **`adder(1)(2)`** (calling a returned closure directly) is not supported. (functional-gaps plan)
|
|
41
41
|
- **Fusion past a closure-spec copy on `Nil` miscompiles** — in `drive.rs` `quiet`, resolving `Nil.fold$c0(..)` to `List.fold$c0` (read-only) lets a value move past it, and codegen then fails with `unknown method 'List.fold$c0'` (5 `closure_spec_test` cases). `typeNameMethodKey` skips `$` copies until this is fixed.
|
|
42
|
-
- **Input model** —
|
|
42
|
+
- **Input model** — steps 1 (shared `Input`, keys included) and 2 (`onInput` on actors, focus) built 2026-11-04; step 3 (typed `Msg` events) in [game-scene-styles.md](game-scene-styles.md) section 6.
|
docs/game-scene-styles.md
CHANGED
|
@@ -55,7 +55,7 @@ Cost: pure updates run in place when the compiler proves uniqueness. Functional
|
|
|
55
55
|
|
|
56
56
|
1. **Fixed timestep.** `plum_bridge.js` passes the real frame time (`dt = (now - last) / 1000`) to `update`. The simulation must instead advance in fixed steps (for example 1/60 s), with the shell accumulating real time and running 0..n steps per frame.
|
|
57
57
|
2. **Recorded seed.** `init` calls `io.randomSeed()`; a replay must record and supply that seed.
|
|
58
|
-
3. **Recorded input.**
|
|
58
|
+
3. **Recorded input.** Done for input: `Input.poll` is the only input read, and the games feed every polled `Input` into their state (section 5). Recording is a matter of saving each step's `Polled.inputs`.
|
|
59
59
|
4. **Host values in state.** The functional game stores the screen size (`sw`, `sh`) and gets `Resize` messages; these must be in the input log too, or kept out of the simulation.
|
|
60
60
|
5. **Checksums.** A per-step hash of the simulation state, to detect desyncs.
|
|
61
61
|
|
|
@@ -67,25 +67,25 @@ Cost: pure updates run in place when the compiler proves uniqueness. Functional
|
|
|
67
67
|
|
|
68
68
|
**Recommendation:** build deterministic simulation on the functional style first. Its `Msg` stream is already the replay log.
|
|
69
69
|
|
|
70
|
-
## 5. Input
|
|
70
|
+
## 5. Input
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Steps 1 and 2 of the proposal below are built (2026-11-04).
|
|
73
|
+
|
|
74
|
+
- **One `Input` type** (`std/game/Input`): `PointerDown(x, y) | PointerUp(x, y) | PointerMove(x, y) | KeyDown(key) | KeyUp(key)`. `Input.poll(io, held) -> Polled(inputs, held)` is the only code that reads input from `io`. It compares the host's mouse button and every `Key` code with what was held after the last poll. `Held` (`isDown(key)`, `after(input)`) tracks held keys from the inputs, so games never call `io.isKeyDown`. `input.position()`, `input.through(m)` and `input.isPointer()` are helpers.
|
|
75
|
+
- **One `UiKind`/`UiEvent`**, also in `std/game/Input`, used by both `PureScene` and `Ui`.
|
|
73
76
|
|
|
74
77
|
| | Imperative | Pure | Functional |
|
|
75
78
|
|---|---|---|---|
|
|
76
|
-
| Pointer |
|
|
77
|
-
| Keyboard |
|
|
78
|
-
| Event type | none | `UiEvent(id: Int, kind: UiKind)` | its own `UiEvent(id: Int, kind: UiKind)` |
|
|
79
|
+
| Pointer | `stage.input(e)`: the topmost actor under the pointer, then each parent, until one's `onInput` returns true | `stage.input(e)` / `stage.frame(dt, inputs)`: hit path, then `node.route` child first; `Button` reacts, `Custom` asks `actor.onInput` | `state.update(Event(input))` -> `render().hitId(x, y)` -> `Ui.step` -> `UiEvent`s |
|
|
80
|
+
| Keyboard | A press gives the actor the focus; keys go to it, then its parents | A press gives the node the focus; keys go to it (then its ancestors); keys no node consumes come back in `StageHandled.keys` | `Event(KeyDown/KeyUp)` updates `State.held`; the game reads `held.isDown(..)` |
|
|
79
81
|
| Capture (press, drag off, release) | none | yes | yes |
|
|
82
|
+
| Events | `onInput -> Bool` (consumed) | `UiEvent(id, kind)`, `Handled.consumed` | `UiEvent(id, kind)` |
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
The example games and fixtures poll once per frame and feed every input in. Nothing else reads input from the host.
|
|
82
85
|
|
|
83
|
-
- **Two event types.** `PureScene` and `Ui` each define `UiKind`/`UiEvent`/pointer kinds with the same meaning.
|
|
84
|
-
|
|
86
|
+
Remaining problem: events still identify UI elements by integer id (`e.id == Id.Restart && e.kind == Click`); step 3 addresses it.
|
|
85
|
-
- **No input hook for game actors.** In pure, a `Custom` actor has only `act` and `draw`; only the built-in `Button` reacts to the pointer. In imperative, actors have `hit` but no input method.
|
|
86
|
-
- **Keyboard bypasses everything.** It is read directly from `io`, so it is not part of any event stream: there is no focus, and it is not recorded (determinism item 3).
|
|
87
87
|
|
|
88
|
-
## 6. Proposal: one input model with `onInput`
|
|
88
|
+
## 6. Proposal: one input model with `onInput`
|
|
89
89
|
|
|
90
90
|
The sketch `scene_graph.pfl` (repo root, not tracked) designs input for a scene2d-style pure scene:
|
|
91
91
|
|
|
@@ -102,23 +102,17 @@ Its ideas:
|
|
|
102
102
|
- one `Input` type that includes keys;
|
|
103
103
|
- an overridable `on_input` on every actor;
|
|
104
104
|
- bubbling along the hit path, child first and then each parent, with messages flowing back up;
|
|
105
|
-
- **typed messages**: a button carries the game's own `Msg` value
|
|
106
|
-
|
|
107
|
-
Plum's pure scene already has the path routing (`route` delivers child first, then each ancestor) and adds pointer capture, which the sketch lacks. The rest maps onto Plum in steps, ordered so each one stands alone.
|
|
105
|
+
- **typed messages**: a button carries the game's own `Msg` value.
|
|
108
106
|
|
|
109
|
-
### Step 1: one shared `Input` type, keys included (
|
|
107
|
+
### Step 1: one shared `Input` type, keys included (built)
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
- Define one shared `UiKind`/`UiEvent`, and delete the duplicates in `PureScene` and `Ui`.
|
|
113
|
-
- Games keep held keys in their state (`Keys`, updated from `KeyDown`/`KeyUp`) instead of calling `io.isKeyDown`.
|
|
114
|
-
- **Gain:** one recorded input list per step (determinism item 3), and one vocabulary across styles.
|
|
109
|
+
`std/game/Input.plum`: see section 5. Gain: one recorded input list per step (determinism item 3), one vocabulary across styles.
|
|
115
110
|
|
|
116
|
-
### Step 2: `onInput` for game actors (
|
|
111
|
+
### Step 2: `onInput` for game actors (built)
|
|
117
112
|
|
|
118
|
-
- **Pure:** `trait Actor`
|
|
119
|
-
- **Imperative:** `trait Actor`
|
|
120
|
-
- **Functional:** no methods on nodes
|
|
121
|
-
- **Keyboard focus:** `UiState`/`Stage` keep a focused id. `KeyDown` goes to the focused node's `onInput` first; unconsumed keys go to the game.
|
|
113
|
+
- **Pure:** `trait Actor` has `fun onInput(self, n: Node, e: Input, over: Bool) -> Handled` (default: unchanged, nothing raised, not consumed). `Node.onInput` handles `Button` itself and asks a `Custom` node's actor. `Handled` has `consumed: Bool`; `route` skips the ancestors of a node that consumed the input. Only groups have children, so for pointers this matters once groups react; for keys it decides what the game receives.
|
|
114
|
+
- **Imperative:** `trait Actor` has `fun onInput(self, e: Input) -> Bool` (e in the actor's coordinates; true = consumed). `stage.input(e) -> Bool` bubbles from the hit actor through `parent`. A press sets `stage.focus`; keys bubble from the focused actor.
|
|
115
|
+
- **Functional:** no methods on nodes (they are data). Inputs reach `State.update` as `Event(input)` messages.
|
|
122
116
|
|
|
123
117
|
### Step 3 (optional): typed messages instead of ids (needs generics)
|
|
124
118
|
|
|
@@ -126,11 +120,10 @@ Plum's pure scene already has the path routing (`route` delivers child first, th
|
|
|
126
120
|
- **Pure:** `Node[Msg]`, `Button(.., onClick: Msg)`, `Actor[Msg]`.
|
|
127
121
|
- **This reopens a decision:** generic `Node[K]`/`Actor[Msg]` was rejected on 2026-11-03 ("Generics can be tried later"), and generic traits are not supported yet. A generic *enum* (`SceneNode[Msg]`) is supported, so the functional style could try it first, but fusion of `render().draw(io)` must be checked on a generic `SceneNode`.
|
|
128
122
|
|
|
129
|
-
###
|
|
123
|
+
### Next
|
|
130
124
|
|
|
131
|
-
1. Do steps 1 and 2: no language work, shared vocabulary, and they unblock deterministic replay.
|
|
132
|
-
|
|
125
|
+
1. Step 3 on the functional style only, kept if fusion still holds.
|
|
133
|
-
|
|
126
|
+
2. Decide whether PureScene stays (backlog: "Retire PureScene?").
|
|
134
127
|
|
|
135
128
|
### Open questions
|
|
136
129
|
|
plum-examples/games/functional/game_state_view.plum
CHANGED
|
@@ -9,15 +9,17 @@
|
|
|
9
9
|
# nothing is changed in place) and `render(self, ..)` returns its `SceneNode` picture.
|
|
10
10
|
# `State` is the same for the whole game: the components, the score, a `Random` and the
|
|
11
11
|
# screen size. `Msg` is what can happen to it (a tick of time, a resize, a restart, a
|
|
12
|
-
# Ui event); `State.update(msg)` is a short sequence of named steps and
|
|
12
|
+
# player input, a Ui event); `State.update(msg)` is a short sequence of named steps and
|
|
13
13
|
# `State.render()` builds a fresh picture every frame, which `SceneNode.draw` interprets
|
|
14
14
|
# (the compiler fuses the two, so the tree is never built). There is no retained
|
|
15
|
-
# scene: the restart button is a `Ui.button` in the picture
|
|
15
|
+
# scene: the restart button is a `Ui.button` in the picture. The host polls the
|
|
16
|
+
# player's input once a frame (`Input.poll`, its only input read) and sends each
|
|
17
|
+
# `Input` to the state as an `Event`: keys update the held keys that steer the ship,
|
|
16
|
-
#
|
|
18
|
+
# and a pointer input finds what is under it with `SceneNode.hitId` on the picture the
|
|
17
19
|
# player is looking at, runs `Ui.step`, and feeds each `UiEvent` to `State.update`.
|
|
18
20
|
# The game-over title and the ship's hurt flash are `Anim`s of time since they began.
|
|
19
|
-
# The `World` record is the host shell: it holds the current `State`,
|
|
21
|
+
# The `World` record is the host shell: it holds the current `State`, polls the
|
|
20
|
-
# and mouse into messages, and plays sounds.
|
|
22
|
+
# keyboard and mouse into messages, and plays sounds.
|
|
21
23
|
module game_state_view
|
|
22
24
|
import std/Str
|
|
23
25
|
import std/Int
|
|
@@ -32,6 +34,7 @@ import std/game/Game
|
|
|
32
34
|
import std/game/functional/FunctionalScene
|
|
33
35
|
import std/game/functional/Anim
|
|
34
36
|
import std/game/functional/Ui
|
|
37
|
+
import std/game/Input
|
|
35
38
|
import std/Io
|
|
36
39
|
|
|
37
40
|
primitive Rules =
|
|
@@ -253,6 +256,7 @@ enum Msg =
|
|
|
253
256
|
| Tick(dt: Float, ax: Float, ay: Float)
|
|
254
257
|
| Resize(w: Float, h: Float)
|
|
255
258
|
| Restart(seed: Int)
|
|
259
|
+
| Event(input: Input)
|
|
256
260
|
| UiEv(event: UiEvent)
|
|
257
261
|
|
|
258
262
|
# Everything that changes during a run. `sheet` and `target` are resource handles
|
|
@@ -272,6 +276,7 @@ record State(
|
|
|
272
276
|
hurtAt: Float,
|
|
273
277
|
overAt: Float,
|
|
274
278
|
ui: UiState,
|
|
279
|
+
held: Held,
|
|
275
280
|
sw: Float,
|
|
276
281
|
sh: Float,
|
|
277
282
|
sheet: Int,
|
|
@@ -295,6 +300,7 @@ record State(
|
|
|
295
300
|
hurtAt: -1.0,
|
|
296
301
|
overAt: -1.0,
|
|
297
302
|
ui: Ui.init(),
|
|
303
|
+
held: Held.none(),
|
|
298
304
|
sw: sw,
|
|
299
305
|
sh: sh,
|
|
300
306
|
sheet: sheet,
|
|
@@ -307,6 +313,7 @@ record State(
|
|
|
307
313
|
Tick(dt, ax, ay) => self.tick(dt, ax, ay)
|
|
308
314
|
Resize(w, h) => State(..self, sw: w, sh: h)
|
|
309
315
|
Restart(seed) => self.restarted(seed)
|
|
316
|
+
Event(input) => self.onInput(input)
|
|
310
317
|
UiEv(event) => self.onUi(event)
|
|
311
318
|
|
|
312
319
|
# One frame of time. Once the game is over the actors only animate.
|
|
@@ -391,7 +398,24 @@ record State(
|
|
|
391
398
|
|
|
392
399
|
fun restarted(self, seed: Int) -> State =
|
|
393
400
|
fresh := State.start(seed, self.best.max(self.score), self.sheet, self.target, self.sw, self.sh)
|
|
394
|
-
State(..fresh, ui: self.ui)
|
|
401
|
+
State(..fresh, ui: self.ui, held: self.held)
|
|
402
|
+
|
|
403
|
+
# A player input: keys update the held keys (Enter or R restarts a finished game),
|
|
404
|
+
# a pointer input goes through the Ui.
|
|
405
|
+
fun onInput(self, e: Input) -> State =
|
|
406
|
+
s := State(..self, held: self.held.after(e))
|
|
407
|
+
if e.isPointer()
|
|
408
|
+
return s.feed(e)
|
|
409
|
+
if s.over && isRestartKey(e)
|
|
410
|
+
return s.restarted(s.rng.nextInt().value)
|
|
411
|
+
s
|
|
412
|
+
|
|
413
|
+
# The steering axes from the held arrow or WASD keys.
|
|
414
|
+
fun axisX(self) -> Float =
|
|
415
|
+
axis(self.held.isDown(Key.Left) || self.held.isDown(Key.KeyA), self.held.isDown(Key.Right) || self.held.isDown(Key.KeyD))
|
|
416
|
+
|
|
417
|
+
fun axisY(self) -> Float =
|
|
418
|
+
axis(self.held.isDown(Key.Up) || self.held.isDown(Key.KeyW), self.held.isDown(Key.Down) || self.held.isDown(Key.KeyS))
|
|
395
419
|
|
|
396
420
|
# A click on the restart button restarts a finished game; other events change nothing.
|
|
397
421
|
fun onUi(self, e: UiEvent) -> State =
|
|
@@ -399,8 +423,9 @@ record State(
|
|
|
399
423
|
|
|
400
424
|
# One pointer input: finds what is under it in the current picture, runs the Ui, and
|
|
401
425
|
# feeds each raised event to `update`.
|
|
402
|
-
fun feed(self, input:
|
|
426
|
+
fun feed(self, input: Input) -> State =
|
|
427
|
+
pos := input.position() ?: return self
|
|
403
|
-
r := Ui.step(self.ui, input, self.render().hitId(
|
|
428
|
+
r := Ui.step(self.ui, input, self.render().hitId(pos.x, pos.y))
|
|
404
429
|
r.events.fold(State(..self, ui: r.ui), |s, e| s.update(UiEv(event: e)))
|
|
405
430
|
|
|
406
431
|
# --- render ---
|
|
@@ -481,17 +506,14 @@ record State(
|
|
|
481
506
|
fun onRestart(self, x: Float, y: Float) -> Bool =
|
|
482
507
|
{self.render().hitId(x, y) ?: -1} == Id.Restart
|
|
483
508
|
|
|
509
|
+
# Enter or R going down.
|
|
484
|
-
fun
|
|
510
|
+
fun isRestartKey(e: Input) -> Bool = match e
|
|
485
|
-
if down && !wasDown
|
|
486
|
-
PointerDown
|
|
487
|
-
|
|
511
|
+
KeyDown(k) => k == Key.Enter || k == Key.KeyR
|
|
488
|
-
PointerUp
|
|
489
|
-
else
|
|
490
|
-
|
|
512
|
+
_ => false
|
|
491
513
|
|
|
492
514
|
# --- the host shell ---
|
|
493
515
|
|
|
494
|
-
record World(mut state: State, mut
|
|
516
|
+
record World(mut state: State, mut held: Held, pickupSound: Int, hitSound: Int, overSound: Int, musicSound: Int)
|
|
495
517
|
|
|
496
518
|
fun musicVolume() -> Float =
|
|
497
519
|
0.3
|
|
@@ -505,7 +527,7 @@ export fun init(io: Io) -> World =
|
|
|
505
527
|
s := State.start(io.randomSeed(), 0, io.textureLoad("sprites.png"), io.textureCreate(1600, 1200), Rules.ArenaW, Rules.ArenaH)
|
|
506
528
|
w := World(
|
|
507
529
|
state: s,
|
|
508
|
-
|
|
530
|
+
held: Held.none(),
|
|
509
531
|
pickupSound: io.soundLoad("pickup.wav"),
|
|
510
532
|
hitSound: io.soundLoad("hit.wav"),
|
|
511
533
|
overSound: io.soundLoad("gameover.wav"),
|
|
@@ -522,25 +544,20 @@ fun axis(negative: Bool, positive: Bool) -> Float =
|
|
|
522
544
|
else
|
|
523
545
|
0.0
|
|
524
546
|
|
|
547
|
+
# Polls the host once (the only input read), sends every input to the state, then
|
|
548
|
+
# one tick steered by the held keys; plays sounds for what changed.
|
|
525
549
|
export fun update(io: Io, w: World, dt: Float) =
|
|
550
|
+
polled := Input.poll(io, w.held)
|
|
551
|
+
w.held = polled.held
|
|
526
552
|
s := w.state.update(Resize(w: io.screenWidth(), h: io.screenHeight()))
|
|
553
|
+
clicked := s
|
|
527
|
-
|
|
554
|
+
for i := range polled.inputs.length()
|
|
528
|
-
m := io.mousePos()
|
|
529
|
-
|
|
555
|
+
clicked = clicked.update(Event(input: polled.inputs.at(i)))
|
|
530
|
-
w.wasDown = down
|
|
531
556
|
if s.over && !clicked.over
|
|
532
557
|
w.state = clicked
|
|
533
558
|
startMusic(io, w)
|
|
534
559
|
return
|
|
535
|
-
if clicked.over && {io.isKeyPressed(Key.Enter) || io.isKeyPressed(Key.KeyR)}
|
|
536
|
-
w.state = clicked.update(Restart(seed: clicked.rng.nextInt().value))
|
|
537
|
-
startMusic(io, w)
|
|
538
|
-
return
|
|
539
|
-
left := io.isKeyDown(Key.Left) || io.isKeyDown(Key.KeyA)
|
|
540
|
-
right := io.isKeyDown(Key.Right) || io.isKeyDown(Key.KeyD)
|
|
541
|
-
up := io.isKeyDown(Key.Up) || io.isKeyDown(Key.KeyW)
|
|
542
|
-
downKey := io.isKeyDown(Key.Down) || io.isKeyDown(Key.KeyS)
|
|
543
|
-
t := clicked.update(Tick(dt: dt, ax:
|
|
560
|
+
t := clicked.update(Tick(dt: dt, ax: clicked.axisX(), ay: clicked.axisY()))
|
|
544
561
|
if t.score > clicked.score
|
|
545
562
|
io.soundPlay(w.pickupSound, 0.8)
|
|
546
563
|
if t.lives < clicked.lives
|
|
@@ -807,17 +824,17 @@ test "view_node_count"
|
|
|
807
824
|
test "restart_click_through_ui"
|
|
808
825
|
e := emptyState()
|
|
809
826
|
s := forceOver(State(..e, score: 90))
|
|
810
|
-
down := s.feed(
|
|
827
|
+
down := s.feed(PointerDown(x: 400.0, y: 364.0))
|
|
811
828
|
assert down.over
|
|
812
829
|
assert Ui.pressed(down.ui, Id.Restart)
|
|
813
|
-
up := down.feed(
|
|
830
|
+
up := down.feed(PointerUp(x: 400.0, y: 364.0))
|
|
814
831
|
assert !up.over
|
|
815
832
|
assert up.score == 0
|
|
816
833
|
assert up.best == 90
|
|
817
834
|
assert up.lives == Rules.StartLives
|
|
818
|
-
off := s.feed(
|
|
835
|
+
off := s.feed(PointerDown(x: 400.0, y: 364.0)).feed(PointerUp(x: 100.0, y: 100.0))
|
|
819
836
|
assert off.over
|
|
820
|
-
miss := s.feed(
|
|
837
|
+
miss := s.feed(PointerDown(x: 100.0, y: 100.0)).feed(PointerUp(x: 100.0, y: 100.0))
|
|
821
838
|
assert miss.over
|
|
822
839
|
|
|
823
840
|
test "hit_flash_pose_at_fixed_times"
|
|
@@ -834,3 +851,22 @@ test "title_anim_pose_at_fixed_times"
|
|
|
834
851
|
assert near(titlePose(0.8).scaleX, 1.17)
|
|
835
852
|
assert near(titlePose(1.3).scaleX, 1.3)
|
|
836
853
|
assert near(titlePose(1.8).scaleY, 1.17)
|
|
854
|
+
|
|
855
|
+
test "held keys from input events steer the ship"
|
|
856
|
+
s := emptyState().update(Event(input: KeyDown(key: Key.Left))).update(Event(input: KeyDown(key: Key.KeyS)))
|
|
857
|
+
assert s.axisX() == -1.0
|
|
858
|
+
assert s.axisY() == 1.0
|
|
859
|
+
t := s.update(Event(input: KeyUp(key: Key.Left)))
|
|
860
|
+
assert t.axisX() == 0.0
|
|
861
|
+
assert t.axisY() == 1.0
|
|
862
|
+
|
|
863
|
+
test "enter restarts a finished game and nothing else"
|
|
864
|
+
playing := emptyState().update(Event(input: KeyDown(key: Key.Enter)))
|
|
865
|
+
assert playing.score == 0
|
|
866
|
+
assert !playing.over
|
|
867
|
+
e := emptyState()
|
|
868
|
+
s := forceOver(State(..e, score: 40))
|
|
869
|
+
r := s.update(Event(input: KeyDown(key: Key.Enter)))
|
|
870
|
+
assert !r.over
|
|
871
|
+
assert r.best == 40
|
|
872
|
+
assert r.held.isDown(Key.Enter)
|
plum-examples/games/imperative/game.plum
CHANGED
|
@@ -16,6 +16,7 @@ import std/Random
|
|
|
16
16
|
import std/game/Vec2
|
|
17
17
|
import std/game/Color
|
|
18
18
|
import std/game/Game
|
|
19
|
+
import std/game/Input
|
|
19
20
|
import std/Io
|
|
20
21
|
|
|
21
22
|
primitive Rules =
|
|
@@ -47,6 +48,7 @@ record World(
|
|
|
47
48
|
mut hurt: Float,
|
|
48
49
|
mut clock: Float,
|
|
49
50
|
mut over: Bool,
|
|
51
|
+
mut held: Held,
|
|
50
52
|
sheet: Int,
|
|
51
53
|
pickupSound: Int,
|
|
52
54
|
hitSound: Int,
|
|
@@ -57,7 +59,7 @@ record World(
|
|
|
57
59
|
ship := Body(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, vx: 0.0, vy: 0.0, angle: 0.0)
|
|
58
60
|
w := World(
|
|
59
61
|
ship: ship, orbs: Slice[Body](), mines: Slice[Body](), rng: Random.seeded(seed), score: 0, best: 0,
|
|
60
|
-
lives: Rules.StartLives, hurt: 0.0, clock: 0.0, over: false, sheet: sheet,
|
|
62
|
+
lives: Rules.StartLives, hurt: 0.0, clock: 0.0, over: false, held: Held.none(), sheet: sheet,
|
|
61
63
|
pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
|
|
62
64
|
for i := range Rules.OrbCount
|
|
63
65
|
sp := spawn(w.rng, ship, 120.0, 40.0)
|
|
@@ -165,14 +167,23 @@ record World(
|
|
|
165
167
|
twinkle := 0.25 + 0.2 * Math.sin(self.clock * 2.0 + Float(i))
|
|
166
168
|
io.drawCircle(sx, sy, 1.0 + Float(i % 3) * 0.5, Color(r: 0.7, g: 0.8, b: 1.0, a: twinkle))
|
|
167
169
|
|
|
170
|
+
# Polls the host once (the only input read): Enter or R restarts a finished game,
|
|
171
|
+
# the held keys steer.
|
|
168
172
|
fun update(self, io: Io, dt: Float) =
|
|
173
|
+
polled := Input.poll(io, self.held)
|
|
174
|
+
self.held = polled.held
|
|
175
|
+
for i := range polled.inputs.length()
|
|
176
|
+
match polled.inputs.at(i)
|
|
177
|
+
KeyDown(k) =>
|
|
169
|
-
|
|
178
|
+
if self.over && {k == Key.Enter || k == Key.KeyR}
|
|
170
|
-
|
|
179
|
+
self.restart()
|
|
171
|
-
|
|
180
|
+
self.startMusic(io)
|
|
181
|
+
_ => 0
|
|
182
|
+
h := self.held
|
|
172
|
-
left :=
|
|
183
|
+
left := h.isDown(Key.Left) || h.isDown(Key.KeyA)
|
|
173
|
-
right :=
|
|
184
|
+
right := h.isDown(Key.Right) || h.isDown(Key.KeyD)
|
|
174
|
-
up :=
|
|
185
|
+
up := h.isDown(Key.Up) || h.isDown(Key.KeyW)
|
|
175
|
-
down :=
|
|
186
|
+
down := h.isDown(Key.Down) || h.isDown(Key.KeyS)
|
|
176
187
|
score := self.score
|
|
177
188
|
lives := self.lives
|
|
178
189
|
self.step(axis(left, right), axis(up, down), dt)
|
plum-examples/games/imperative/game_scene.plum
CHANGED
|
@@ -31,6 +31,7 @@ import std/game/Vec2
|
|
|
31
31
|
import std/game/Color
|
|
32
32
|
import std/game/Game
|
|
33
33
|
import std/game/imperative/Scene
|
|
34
|
+
import std/game/Input
|
|
34
35
|
import std/Io
|
|
35
36
|
|
|
36
37
|
primitive Rules =
|
|
@@ -237,8 +238,13 @@ record Star(Actor(x, y, width, height), seed: Float, mut clock: Float) =
|
|
|
237
238
|
# The whole game: it embeds the Stage (camera and root) and has its layers, typed handles to the actors the rules
|
|
238
239
|
# touch, and the run's state. `mines` and `orbs` are the layer Groups; `mineList`
|
|
239
240
|
# and `orbList` are the same actors, typed.
|
|
241
|
+
# Enter or R going down.
|
|
242
|
+
fun isRestartKey(e: Input) -> Bool = match e
|
|
243
|
+
KeyDown(k) => k == Key.Enter || k == Key.KeyR
|
|
244
|
+
_ => false
|
|
245
|
+
|
|
240
246
|
record World(
|
|
241
|
-
Stage(camX, camY, camScale),
|
|
247
|
+
Stage(camX, camY, camScale, focus),
|
|
242
248
|
ship: Ship,
|
|
243
249
|
mines: Group,
|
|
244
250
|
orbs: Group,
|
|
@@ -254,6 +260,7 @@ record World(
|
|
|
254
260
|
mut lives: Int,
|
|
255
261
|
mut over: Bool,
|
|
256
262
|
mut clock: Float,
|
|
263
|
+
mut held: Held,
|
|
257
264
|
sheet: Int,
|
|
258
265
|
pickupSound: Int,
|
|
259
266
|
hitSound: Int,
|
|
@@ -290,9 +297,9 @@ record World(
|
|
|
290
297
|
overlay.add(Text.create("Press Enter to play again", 24.0, Color.White).at(270.0, 316.0))
|
|
291
298
|
hud.add(overlay)
|
|
292
299
|
w := World(
|
|
293
|
-
camX: 0.0, camY: 0.0, camScale: 1.0, ship: ship, mines: mines, orbs: orbs, mineList: Slice[Mine](), orbList: Slice[Orb](),
|
|
300
|
+
camX: 0.0, camY: 0.0, camScale: 1.0, focus: null, ship: ship, mines: mines, orbs: orbs, mineList: Slice[Mine](), orbList: Slice[Orb](),
|
|
294
301
|
overlay: overlay, scoreText: scoreText, bestText: bestText, lifeIcons: icons,
|
|
295
|
-
rng: Random.seeded(seed), score: 0, best: 0, lives: Rules.StartLives, over: false, clock: 0.0, sheet: sheet,
|
|
302
|
+
rng: Random.seeded(seed), score: 0, best: 0, lives: Rules.StartLives, over: false, clock: 0.0, held: Held.none(), sheet: sheet,
|
|
296
303
|
pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
|
|
297
304
|
w.root.add(Rect.create(Rules.ArenaW, Rules.ArenaH, Color.rgb(0.07, 0.08, 0.16)))
|
|
298
305
|
w.root.add(stars)
|
|
@@ -412,14 +419,21 @@ record World(
|
|
|
412
419
|
io.soundStop(self.musicSound)
|
|
413
420
|
io.soundLoop(self.musicSound, musicVolume())
|
|
414
421
|
|
|
422
|
+
# Polls the host once (the only input read), offers each input to the stage and
|
|
423
|
+
# handles what no actor consumed: Enter or R restarts a finished game.
|
|
415
424
|
fun update(self, io: Io, dt: Float) =
|
|
425
|
+
polled := Input.poll(io, self.held)
|
|
426
|
+
self.held = polled.held
|
|
427
|
+
for i := range polled.inputs.length()
|
|
428
|
+
e := polled.inputs.at(i)
|
|
416
|
-
|
|
429
|
+
if !self.input(e) && self.over && isRestartKey(e)
|
|
417
|
-
|
|
430
|
+
self.restart()
|
|
418
|
-
|
|
431
|
+
self.startMusic(io)
|
|
432
|
+
h := self.held
|
|
419
|
-
left :=
|
|
433
|
+
left := h.isDown(Key.Left) || h.isDown(Key.KeyA)
|
|
420
|
-
right :=
|
|
434
|
+
right := h.isDown(Key.Right) || h.isDown(Key.KeyD)
|
|
421
|
-
up :=
|
|
435
|
+
up := h.isDown(Key.Up) || h.isDown(Key.KeyW)
|
|
422
|
-
down :=
|
|
436
|
+
down := h.isDown(Key.Down) || h.isDown(Key.KeyS)
|
|
423
437
|
score := self.score
|
|
424
438
|
lives := self.lives
|
|
425
439
|
self.step(axis(left, right), axis(up, down), dt)
|
plum-examples/games/imperative/scene.plum
CHANGED
|
@@ -21,6 +21,7 @@ import std/game/Vec2
|
|
|
21
21
|
import std/game/Color
|
|
22
22
|
import std/game/Game
|
|
23
23
|
import std/game/imperative/Scene
|
|
24
|
+
import std/game/Input
|
|
24
25
|
import std/Io
|
|
25
26
|
|
|
26
27
|
primitive Layout =
|
|
@@ -102,6 +103,7 @@ record Sky(
|
|
|
102
103
|
mut selected: Int,
|
|
103
104
|
mut clock: Float,
|
|
104
105
|
mut paused: Bool,
|
|
106
|
+
mut held: Held,
|
|
105
107
|
map: Int)
|
|
106
108
|
|
|
107
109
|
# A body sprite and its (hidden) halo in a group `slot` centred on the group's own
|
|
@@ -133,7 +135,7 @@ fun newSky(sheet: Int) -> Sky =
|
|
|
133
135
|
label.y = 12.0
|
|
134
136
|
sky := Sky(
|
|
135
137
|
stage: stage, bodies: Slice[Actor](), halos: Slice[Actor](), names: Slice[Str](),
|
|
136
|
-
sun: sun, label: label, crowd: Group(), front: null, selected: -1, clock: 0.0, paused: false, map: -1)
|
|
138
|
+
sun: sun, label: label, crowd: Group(), front: null, selected: -1, clock: 0.0, paused: false, held: Held.none(), map: -1)
|
|
137
139
|
# faint orbit rings, added first so every body draws (and is hit) above them
|
|
138
140
|
for i := range Layout.Planets
|
|
139
141
|
r := 90.0 + Float(i) * 60.0
|
|
@@ -223,15 +225,24 @@ export fun init(io: Io) -> Sky =
|
|
|
223
225
|
sky.map = io.textureCreate(Layout.MapW, Layout.MapH)
|
|
224
226
|
sky
|
|
225
227
|
|
|
228
|
+
# Polls the host once (the only input read): Space pauses, a press selects a body.
|
|
226
229
|
export fun update(io: Io, sky: Sky, dt: Float) =
|
|
230
|
+
polled := Input.poll(io, sky.held)
|
|
231
|
+
sky.held = polled.held
|
|
232
|
+
for i := range polled.inputs.length()
|
|
233
|
+
match polled.inputs.at(i)
|
|
234
|
+
KeyDown(k) =>
|
|
227
|
-
|
|
235
|
+
if k == Key.Space
|
|
228
|
-
|
|
236
|
+
sky.paused = !sky.paused
|
|
237
|
+
_ => 0
|
|
229
238
|
tick(sky, dt)
|
|
230
239
|
fit(sky, io.screenWidth(), io.screenHeight())
|
|
231
|
-
|
|
240
|
+
for i := range polled.inputs.length()
|
|
232
|
-
|
|
241
|
+
match polled.inputs.at(i)
|
|
242
|
+
PointerDown(x, y) =>
|
|
233
|
-
|
|
243
|
+
p := sky.stage.screenToStage(x, y)
|
|
234
|
-
|
|
244
|
+
clickAt(sky, p.x, p.y)
|
|
245
|
+
_ => 0
|
|
235
246
|
|
|
236
247
|
# Redraws the minimap texture (every body as a dot, the selected one highlighted)
|
|
237
248
|
# and shows it in the bottom-right corner of the screen.
|
plum-examples/games/pure/game_pure_scene.plum
CHANGED
|
@@ -28,6 +28,7 @@ import std/game/Game
|
|
|
28
28
|
import std/game/pure/Actions
|
|
29
29
|
import std/game/pure/PureScene
|
|
30
30
|
import std/game/pure/SceneInput
|
|
31
|
+
import std/game/Input
|
|
31
32
|
import std/Io
|
|
32
33
|
|
|
33
34
|
primitive Rules =
|
|
@@ -377,7 +378,7 @@ record World(
|
|
|
377
378
|
mut lives: Int,
|
|
378
379
|
mut over: Bool,
|
|
379
380
|
mut clock: Float,
|
|
380
|
-
mut
|
|
381
|
+
mut held: Held,
|
|
381
382
|
sheet: Int,
|
|
382
383
|
mut pickupSound: Int,
|
|
383
384
|
mut hitSound: Int,
|
|
@@ -390,16 +391,16 @@ fun newWorld(seed: Int, sheet: Int) -> World =
|
|
|
390
391
|
next := spots.rng
|
|
391
392
|
World(
|
|
392
393
|
stage: Stage.create(buildRoot(spots, sheet, 0)), ship: ship, rng: next, mines: mineCount(0), score: 0, best: 0,
|
|
393
|
-
lives: Rules.StartLives, over: false, clock: 0.0,
|
|
394
|
+
lives: Rules.StartLives, over: false, clock: 0.0, held: Held.none(), sheet: sheet,
|
|
394
395
|
pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
|
|
395
396
|
|
|
396
397
|
fun modifyRoot(st: Stage, id: Int, f: fn(Node) -> Node) -> Stage =
|
|
397
398
|
match st
|
|
398
|
-
Stage(root, camera, captured) => Stage(root: root.modify(id, f), camera: camera, captured: captured)
|
|
399
|
+
Stage(root, camera, captured, focus) => Stage(root: root.modify(id, f), camera: camera, captured: captured, focus: focus)
|
|
399
400
|
|
|
400
401
|
fun withCamera(st: Stage, camera: Affine) -> Stage =
|
|
401
402
|
match st
|
|
402
|
-
Stage(root, _, captured) => Stage(root: root, camera: camera, captured: captured)
|
|
403
|
+
Stage(root, _, captured, focus) => Stage(root: root, camera: camera, captured: captured, focus: focus)
|
|
403
404
|
|
|
404
405
|
fun swap(w: World, id: Int, f: fn(Node) -> Node) =
|
|
405
406
|
w.stage = modifyRoot(w.stage, id, f)
|
|
@@ -415,7 +416,7 @@ fun restart(w: World) =
|
|
|
415
416
|
spots := startSpots(w.rng, w.ship)
|
|
416
417
|
next := spots.rng
|
|
417
418
|
w.rng = next
|
|
418
|
-
w.stage = Stage(root: buildRoot(spots, w.sheet, w.best), camera: w.stage.camera, captured: null)
|
|
419
|
+
w.stage = Stage(root: buildRoot(spots, w.sheet, w.best), camera: w.stage.camera, captured: null, focus: null)
|
|
419
420
|
|
|
420
421
|
# Copies the score, best score, lives and game-over panel into the HUD actors.
|
|
421
422
|
fun syncHud(w: World) =
|
|
@@ -569,16 +570,25 @@ fun axis(negative: Bool, positive: Bool) -> Float =
|
|
|
569
570
|
else
|
|
570
571
|
0.0
|
|
571
572
|
|
|
573
|
+
# Enter or R going down.
|
|
574
|
+
fun isRestartKey(e: Input) -> Bool = match e
|
|
575
|
+
KeyDown(k) => k == Key.Enter || k == Key.KeyR
|
|
576
|
+
_ => false
|
|
577
|
+
|
|
578
|
+
# Polls the host once (the only input read); the held keys steer the ship.
|
|
572
579
|
export fun update(io: Io, w: World, dt: Float) =
|
|
580
|
+
polled := Input.poll(io, w.held)
|
|
581
|
+
inputs := polled.inputs
|
|
582
|
+
w.held = polled.held
|
|
583
|
+
for i := range inputs.length()
|
|
573
|
-
|
|
584
|
+
if w.over && isRestartKey(inputs.at(i))
|
|
574
|
-
|
|
585
|
+
restart(w)
|
|
575
|
-
|
|
586
|
+
startMusic(io, w)
|
|
576
|
-
inputs := Input.poll(io, w.wasDown)
|
|
577
|
-
|
|
587
|
+
h := w.held
|
|
578
|
-
left :=
|
|
588
|
+
left := h.isDown(Key.Left) || h.isDown(Key.KeyA)
|
|
579
|
-
right :=
|
|
589
|
+
right := h.isDown(Key.Right) || h.isDown(Key.KeyD)
|
|
580
|
-
up :=
|
|
590
|
+
up := h.isDown(Key.Up) || h.isDown(Key.KeyW)
|
|
581
|
-
down :=
|
|
591
|
+
down := h.isDown(Key.Down) || h.isDown(Key.KeyS)
|
|
582
592
|
score := w.score
|
|
583
593
|
lives := w.lives
|
|
584
594
|
restarted := advance(w, axis(left, right), axis(up, down), dt, inputs)
|
|
@@ -825,8 +835,8 @@ test "restart_button_click_restarts"
|
|
|
825
835
|
w.score = 30
|
|
826
836
|
forceOver(w)
|
|
827
837
|
assert find(w.stage.root, Id.Restart).visible
|
|
828
|
-
down :=
|
|
838
|
+
down := PointerDown(x: 400.0, y: 364.0)
|
|
829
|
-
up :=
|
|
839
|
+
up := PointerUp(x: 400.0, y: 364.0)
|
|
830
840
|
taps := Slice[Input]()
|
|
831
841
|
taps.push(down)
|
|
832
842
|
taps.push(up)
|
|
@@ -841,8 +851,8 @@ test "a click on the restart button while playing does nothing"
|
|
|
841
851
|
w := emptyWorld()
|
|
842
852
|
w.score = 30
|
|
843
853
|
taps := Slice[Input]()
|
|
844
|
-
taps.push(
|
|
854
|
+
taps.push(PointerDown(x: 400.0, y: 364.0))
|
|
845
|
-
taps.push(
|
|
855
|
+
taps.push(PointerUp(x: 400.0, y: 364.0))
|
|
846
856
|
restarted := advance(w, 0.0, 0.0, 0.016, taps)
|
|
847
857
|
assert !restarted
|
|
848
858
|
assert w.score == 30
|
plum-game/web-test/fixtures/stress_pure_scene.plum
CHANGED
|
@@ -104,7 +104,7 @@ fun buildWorld(sheet: Int, white: Int) -> World =
|
|
|
104
104
|
root.push(countLabel(30000 + i, i, Float(i * 30 % Counts.WorldW), Float(i * 17 % Counts.WorldH)))
|
|
105
105
|
top := Node.group(1, 0.0, 0.0, Float(Counts.WorldW), Float(Counts.WorldH), root)
|
|
106
106
|
cam := Affine(a: 0.8, b: 0.0, c: 0.0, d: 0.8, e: 0.0, f: 0.0)
|
|
107
|
-
World(sheet: sheet, stage: Stage(root: top, camera: cam, captured: null))
|
|
107
|
+
World(sheet: sheet, stage: Stage(root: top, camera: cam, captured: null, focus: null))
|
|
108
108
|
|
|
109
109
|
fun whiteTexture(io: Io) -> Int =
|
|
110
110
|
t := io.textureCreate(1, 1)
|
plum-game/web-test/fixtures/stress_state_view.plum
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# plus 200 animated sprites (Pulsers, pose = Anim.at(anim, clock - start)) and
|
|
12
12
|
# 20 Ui.button buttons (tagged ids 40000..40019, screen space, outside the camera).
|
|
13
13
|
# `State.ui` is a Ui.UiState fed by mouse input: update turns the mouse into a
|
|
14
|
-
#
|
|
14
|
+
# shared Input, hit-tests the button layer with SceneNode.hitId, and calls Ui.step.
|
|
15
15
|
module stress_state_view
|
|
16
16
|
import std/Str
|
|
17
17
|
import std/Int
|
|
@@ -24,6 +24,7 @@ import std/game/Game
|
|
|
24
24
|
import std/game/functional/FunctionalScene
|
|
25
25
|
import std/game/functional/Anim
|
|
26
26
|
import std/game/functional/Ui
|
|
27
|
+
import std/game/Input
|
|
27
28
|
import std/Io
|
|
28
29
|
|
|
29
30
|
primitive Counts =
|
|
@@ -142,8 +143,9 @@ fun countClicks(es: List[UiEvent]) -> Int = match es
|
|
|
142
143
|
Cons(e, t) => {if e.kind == Click then 1 else 0} + countClicks(t)
|
|
143
144
|
|
|
144
145
|
# Feeds one pointer input through Ui.step against the button layer.
|
|
145
|
-
fun stepUi(s: State, input:
|
|
146
|
+
fun stepUi(s: State, input: Input) -> State =
|
|
147
|
+
pos := input.position() ?: return s
|
|
146
|
-
hit := buttonsView(s.ui).hitId(
|
|
148
|
+
hit := buttonsView(s.ui).hitId(pos.x, pos.y)
|
|
147
149
|
r := Ui.step(s.ui, input, hit)
|
|
148
150
|
State(
|
|
149
151
|
sheet: s.sheet, white: s.white, spinners: s.spinners, drifters: s.drifters, counters: s.counters,
|
|
@@ -199,21 +201,18 @@ fun whiteTexture(io: Io) -> Int =
|
|
|
199
201
|
io.renderTarget(-1)
|
|
200
202
|
t
|
|
201
203
|
|
|
202
|
-
record World(mut state: State, mut
|
|
204
|
+
record World(mut state: State, mut held: Held)
|
|
203
205
|
|
|
204
206
|
export fun init(io: Io) -> World =
|
|
205
|
-
World(state: newState(io.textureLoad("sprites.png"), whiteTexture(io)),
|
|
207
|
+
World(state: newState(io.textureLoad("sprites.png"), whiteTexture(io)), held: Held.none())
|
|
206
208
|
|
|
207
209
|
export fun update(io: Io, w: World, dt: Float) =
|
|
210
|
+
polled := Input.poll(io, w.held)
|
|
211
|
+
w.held = polled.held
|
|
208
|
-
|
|
212
|
+
s := step(w.state, dt)
|
|
209
|
-
|
|
213
|
+
for i := range polled.inputs.length()
|
|
210
|
-
kind := PointerMove
|
|
211
|
-
if down && !w.wasDown
|
|
212
|
-
kind = PointerDown
|
|
213
|
-
|
|
214
|
+
s = stepUi(s, polled.inputs.at(i))
|
|
214
|
-
kind = PointerUp
|
|
215
|
-
w.
|
|
215
|
+
w.state = s
|
|
216
|
-
w.state = stepUi(step(w.state, dt), UiInput(kind: kind, x: m.x, y: m.y))
|
|
217
216
|
|
|
218
217
|
export fun draw(io: Io, w: World) =
|
|
219
218
|
io.clear(Color.rgb(0.05, 0.06, 0.1))
|
|
@@ -249,8 +248,8 @@ test "view has one node per actor"
|
|
|
249
248
|
|
|
250
249
|
test "button click counts and pulsers animate"
|
|
251
250
|
s := newState(0, 0)
|
|
252
|
-
down := stepUi(s,
|
|
251
|
+
down := stepUi(s, PointerDown(x: 20.0, y: 20.0))
|
|
253
|
-
up := stepUi(down,
|
|
252
|
+
up := stepUi(down, PointerUp(x: 20.0, y: 20.0))
|
|
254
253
|
assert up.clicks == 1
|
|
255
254
|
assert Ui.hovered(up.ui, 40000)
|
|
256
255
|
p := newPulser(0)
|
plum-std/game/Input.plum
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
module std
|
|
2
|
+
import std/Int
|
|
3
|
+
import std/Float
|
|
4
|
+
import std/Slice
|
|
5
|
+
import std/game/Vec2
|
|
6
|
+
import std/game/Affine
|
|
7
|
+
import std/game/Game
|
|
8
|
+
import std/Io
|
|
9
|
+
|
|
10
|
+
# Player input shared by every scene style. `Input.poll` is the only code that reads
|
|
11
|
+
# input from the host: a game that feeds every polled `Input` into its state reads
|
|
12
|
+
# nothing else, so a recorded list of inputs replays the game exactly (see
|
|
13
|
+
# docs/game-scene-styles.md, "Determinism").
|
|
14
|
+
|
|
15
|
+
# One input: a pointer event in screen pixels, or a key going down or up.
|
|
16
|
+
enum Input =
|
|
17
|
+
| PointerDown(x: Float, y: Float)
|
|
18
|
+
| PointerUp(x: Float, y: Float)
|
|
19
|
+
| PointerMove(x: Float, y: Float)
|
|
20
|
+
| KeyDown(key: Int)
|
|
21
|
+
| KeyUp(key: Int)
|
|
22
|
+
|
|
23
|
+
# This frame's inputs: the host's mouse button and keys compared with `prev`, what
|
|
24
|
+
# was held after the last poll. One pointer input (down, up or move) comes first,
|
|
25
|
+
# then a KeyDown or KeyUp for each key that changed, in key-code order.
|
|
26
|
+
fun poll(io: Io, prev: Held) -> Polled =
|
|
27
|
+
out := Slice[Input]()
|
|
28
|
+
m := io.mousePos()
|
|
29
|
+
down := io.isMouseDown(Mouse.Left)
|
|
30
|
+
if down && !prev.mouse
|
|
31
|
+
out.push(PointerDown(x: m.x, y: m.y))
|
|
32
|
+
else if !down && prev.mouse
|
|
33
|
+
out.push(PointerUp(x: m.x, y: m.y))
|
|
34
|
+
else
|
|
35
|
+
out.push(PointerMove(x: m.x, y: m.y))
|
|
36
|
+
keys := Slice[Int]()
|
|
37
|
+
codes := Input.keyCodes()
|
|
38
|
+
for i := range codes.length()
|
|
39
|
+
k := codes.at(i)
|
|
40
|
+
now := io.isKeyDown(k)
|
|
41
|
+
was := prev.isDown(k)
|
|
42
|
+
if now
|
|
43
|
+
keys.push(k)
|
|
44
|
+
if now && !was
|
|
45
|
+
out.push(KeyDown(key: k))
|
|
46
|
+
if !now && was
|
|
47
|
+
out.push(KeyUp(key: k))
|
|
48
|
+
Polled(inputs: out, held: Held(mouse: down, keys: keys))
|
|
49
|
+
|
|
50
|
+
# Every `Key` code in std/game/Game, in increasing order.
|
|
51
|
+
fun keyCodes() -> Slice[Int] =
|
|
52
|
+
out := Slice(Key.Space)
|
|
53
|
+
for i := range 10
|
|
54
|
+
out.push(Key.Num0 + i)
|
|
55
|
+
for i := range 26
|
|
56
|
+
out.push(Key.KeyA + i)
|
|
57
|
+
for i := range 4
|
|
58
|
+
out.push(Key.Escape + i)
|
|
59
|
+
for i := range 4
|
|
60
|
+
out.push(Key.Right + i)
|
|
61
|
+
out.push(Key.LeftShift)
|
|
62
|
+
out.push(Key.LeftControl)
|
|
63
|
+
out
|
|
64
|
+
|
|
65
|
+
# The pointer position, or null for a key.
|
|
66
|
+
fun position(self) -> Vec2? = match self
|
|
67
|
+
PointerDown(x, y) => Vec2(x: x, y: y)
|
|
68
|
+
PointerUp(x, y) => Vec2(x: x, y: y)
|
|
69
|
+
PointerMove(x, y) => Vec2(x: x, y: y)
|
|
70
|
+
_ => null
|
|
71
|
+
|
|
72
|
+
# The input with its position seen through `m` (a key is unchanged).
|
|
73
|
+
fun through(self, m: Affine) -> Input = match self
|
|
74
|
+
PointerDown(x, y) => Input.pointerAt(PointerDown(x: x, y: y), m.apply(Vec2(x: x, y: y)))
|
|
75
|
+
PointerUp(x, y) => Input.pointerAt(PointerUp(x: x, y: y), m.apply(Vec2(x: x, y: y)))
|
|
76
|
+
PointerMove(x, y) => Input.pointerAt(PointerMove(x: x, y: y), m.apply(Vec2(x: x, y: y)))
|
|
77
|
+
_ => self
|
|
78
|
+
|
|
79
|
+
# The pointer input `e` (same kind) at `p`.
|
|
80
|
+
fun pointerAt(e: Input, p: Vec2) -> Input = match e
|
|
81
|
+
PointerDown(_, _) => PointerDown(x: p.x, y: p.y)
|
|
82
|
+
PointerUp(_, _) => PointerUp(x: p.x, y: p.y)
|
|
83
|
+
_ => PointerMove(x: p.x, y: p.y)
|
|
84
|
+
|
|
85
|
+
fun isPointer(self) -> Bool =
|
|
86
|
+
self.position() != null
|
|
87
|
+
|
|
88
|
+
# What is held: the left mouse button and the keys that are down.
|
|
89
|
+
record Held(mouse: Bool, keys: Slice[Int]) =
|
|
90
|
+
fun none() -> Held =
|
|
91
|
+
Held(mouse: false, keys: Slice[Int]())
|
|
92
|
+
|
|
93
|
+
fun isDown(self, key: Int) -> Bool =
|
|
94
|
+
self.keys.contains(key)
|
|
95
|
+
|
|
96
|
+
# What is held after `e`: KeyDown adds its key, KeyUp removes it, a pointer down
|
|
97
|
+
# or up sets `mouse`.
|
|
98
|
+
fun after(self, e: Input) -> Held = match e
|
|
99
|
+
KeyDown(key) => if self.isDown(key) then self else Held(mouse: self.mouse, keys: self.keysWith(key, true))
|
|
100
|
+
KeyUp(key) => Held(mouse: self.mouse, keys: self.keysWith(key, false))
|
|
101
|
+
PointerDown(_, _) => Held(mouse: true, keys: self.keys)
|
|
102
|
+
PointerUp(_, _) => Held(mouse: false, keys: self.keys)
|
|
103
|
+
_ => self
|
|
104
|
+
|
|
105
|
+
# A fresh copy of the held keys with `key` added (`add`) or removed.
|
|
106
|
+
fun keysWith(self, key: Int, add: Bool) -> Slice[Int] =
|
|
107
|
+
out := Slice[Int]()
|
|
108
|
+
for i := range self.keys.length()
|
|
109
|
+
if self.keys.at(i) != key
|
|
110
|
+
out.push(self.keys.at(i))
|
|
111
|
+
if add
|
|
112
|
+
out.push(key)
|
|
113
|
+
out
|
|
114
|
+
|
|
115
|
+
# A poll's inputs and what is held after them (pass it to the next poll).
|
|
116
|
+
record Polled(inputs: Slice[Input], held: Held)
|
|
117
|
+
|
|
118
|
+
# What happened to a UI element: pressed on it, released after a press on it,
|
|
119
|
+
# clicked (pressed and released on it), or the pointer entered or left it.
|
|
120
|
+
enum UiKind =
|
|
121
|
+
| Press
|
|
122
|
+
| Release
|
|
123
|
+
| Click
|
|
124
|
+
| Enter
|
|
125
|
+
| Exit
|
|
126
|
+
|
|
127
|
+
record UiEvent(id: Int, kind: UiKind)
|
|
128
|
+
|
|
129
|
+
fun inHeld(keys: Slice[Int]) -> Held =
|
|
130
|
+
Held(mouse: false, keys: keys)
|
|
131
|
+
|
|
132
|
+
test "held_after_tracks_keys_and_mouse"
|
|
133
|
+
h := Held.none().after(KeyDown(key: Key.KeyA)).after(KeyDown(key: Key.Left)).after(PointerDown(x: 1.0, y: 2.0))
|
|
134
|
+
assert h.isDown(Key.KeyA)
|
|
135
|
+
assert h.isDown(Key.Left)
|
|
136
|
+
assert h.mouse
|
|
137
|
+
h2 := h.after(KeyUp(key: Key.KeyA)).after(PointerUp(x: 1.0, y: 2.0))
|
|
138
|
+
assert !h2.isDown(Key.KeyA)
|
|
139
|
+
assert h2.isDown(Key.Left)
|
|
140
|
+
assert !h2.mouse
|
|
141
|
+
assert h.isDown(Key.KeyA)
|
|
142
|
+
|
|
143
|
+
test "key_down_twice_keeps_one_entry"
|
|
144
|
+
h := Held.none().after(KeyDown(key: Key.KeyA)).after(KeyDown(key: Key.KeyA))
|
|
145
|
+
assert h.keys.length() == 1
|
|
146
|
+
|
|
147
|
+
test "key_codes_cover_every_key_constant"
|
|
148
|
+
codes := Input.keyCodes()
|
|
149
|
+
assert codes.length() == 47
|
|
150
|
+
assert inHeld(codes).isDown(Key.Space)
|
|
151
|
+
assert inHeld(codes).isDown(Key.Num9)
|
|
152
|
+
assert inHeld(codes).isDown(Key.KeyZ)
|
|
153
|
+
assert inHeld(codes).isDown(Key.Backspace)
|
|
154
|
+
assert inHeld(codes).isDown(Key.Up)
|
|
155
|
+
assert inHeld(codes).isDown(Key.LeftControl)
|
|
156
|
+
|
|
157
|
+
test "through_moves_pointers_only"
|
|
158
|
+
m := Affine(a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: 10.0, f: 0.0)
|
|
159
|
+
if p := PointerDown(x: 1.0, y: 2.0).through(m).position()
|
|
160
|
+
assert p.x == 11.0
|
|
161
|
+
assert p.y == 2.0
|
|
162
|
+
else
|
|
163
|
+
assert false
|
|
164
|
+
assert KeyDown(key: 5).through(m).position() == null
|
|
165
|
+
assert !KeyDown(key: 5).isPointer()
|
|
166
|
+
assert PointerMove(x: 0.0, y: 0.0).isPointer()
|
plum-std/game/functional/Ui.plum
CHANGED
|
@@ -6,6 +6,7 @@ import std/Slice
|
|
|
6
6
|
import std/List
|
|
7
7
|
import std/game/Color
|
|
8
8
|
import std/game/functional/FunctionalScene
|
|
9
|
+
import std/game/Input
|
|
9
10
|
|
|
10
11
|
# A pure UI layer for the functional scene style. `Ui.step` takes the previous
|
|
11
12
|
# UiState, one pointer input and the id under the pointer (from SceneNode.hitId on
|
|
@@ -13,22 +14,6 @@ import std/game/functional/FunctionalScene
|
|
|
13
14
|
# button rules as PureScene: Down captures the pressed id; while captured only
|
|
14
15
|
# that id gets Enter/Exit; Up releases (Click only when still over it).
|
|
15
16
|
|
|
16
|
-
enum UiKind =
|
|
17
|
-
| Press
|
|
18
|
-
| Release
|
|
19
|
-
| Click
|
|
20
|
-
| Enter
|
|
21
|
-
| Exit
|
|
22
|
-
|
|
23
|
-
record UiEvent(id: Int, kind: UiKind)
|
|
24
|
-
|
|
25
|
-
enum UiInputKind =
|
|
26
|
-
| PointerDown
|
|
27
|
-
| PointerUp
|
|
28
|
-
| PointerMove
|
|
29
|
-
|
|
30
|
-
record UiInput(kind: UiInputKind, x: Float, y: Float)
|
|
31
|
-
|
|
32
17
|
record UiState(x: Float, y: Float, down: Bool, hot: Int?, captured: Int?)
|
|
33
18
|
|
|
34
19
|
record UiStep(ui: UiState, events: List[UiEvent])
|
|
@@ -54,7 +39,9 @@ primitive Ui =
|
|
|
54
39
|
fun init() -> UiState =
|
|
55
40
|
UiState(x: 0.0, y: 0.0, down: false, hot: null, captured: null)
|
|
56
41
|
|
|
42
|
+
# A key changes nothing here (keys go to the game).
|
|
57
|
-
fun step(ui: UiState, input:
|
|
43
|
+
fun step(ui: UiState, input: Input, hit: Int?) -> UiStep =
|
|
44
|
+
pos := input.position() ?: return UiStep(ui: ui, events: Nil)
|
|
58
45
|
out := Slice[UiEvent]()
|
|
59
46
|
# a capture left over without a held button is stale
|
|
60
47
|
cap: Int? := ui.captured
|
|
@@ -62,8 +49,8 @@ primitive Ui =
|
|
|
62
49
|
cap = null
|
|
63
50
|
hot: Int? := ui.hot
|
|
64
51
|
down := ui.down
|
|
65
|
-
match input
|
|
52
|
+
match input
|
|
66
|
-
PointerDown =>
|
|
53
|
+
PointerDown(_, _) =>
|
|
67
54
|
cap = null
|
|
68
55
|
uiHover(out, hot, hit)
|
|
69
56
|
hot = hit
|
|
@@ -71,7 +58,7 @@ primitive Ui =
|
|
|
71
58
|
if h := hit
|
|
72
59
|
out.push(UiEvent(id: h, kind: Press))
|
|
73
60
|
cap = h
|
|
74
|
-
PointerMove =>
|
|
61
|
+
PointerMove(_, _) =>
|
|
75
62
|
if c := cap
|
|
76
63
|
now: Int? := null
|
|
77
64
|
if uiSame(hit, cap)
|
|
@@ -81,7 +68,7 @@ primitive Ui =
|
|
|
81
68
|
else
|
|
82
69
|
uiHover(out, hot, hit)
|
|
83
70
|
hot = hit
|
|
84
|
-
PointerUp =>
|
|
71
|
+
PointerUp(_, _) =>
|
|
85
72
|
if c := cap
|
|
86
73
|
out.push(UiEvent(id: c, kind: Release))
|
|
87
74
|
if uiSame(hit, cap)
|
|
@@ -92,7 +79,8 @@ primitive Ui =
|
|
|
92
79
|
uiHover(out, hot, hit)
|
|
93
80
|
hot = hit
|
|
94
81
|
down = false
|
|
82
|
+
_ => 0
|
|
95
|
-
UiStep(ui: UiState(x:
|
|
83
|
+
UiStep(ui: UiState(x: pos.x, y: pos.y, down: down, hot: hot, captured: cap), events: List.fromSlice(out))
|
|
96
84
|
|
|
97
85
|
fun pressed(ui: UiState, id: Int) -> Bool =
|
|
98
86
|
if c := ui.captured
|
|
@@ -123,11 +111,11 @@ fun uiPressedColor() -> Color = Color.rgb(0.6, 0.2, 0.2)
|
|
|
123
111
|
|
|
124
112
|
fun uiKinds(s: UiStep) -> List[UiKind] = s.events.map(|e| e.kind)
|
|
125
113
|
|
|
126
|
-
fun uiDown(x: Float) ->
|
|
114
|
+
fun uiDown(x: Float) -> Input = PointerDown(x: x, y: 0.0)
|
|
127
115
|
|
|
128
|
-
fun uiUp(x: Float) ->
|
|
116
|
+
fun uiUp(x: Float) -> Input = PointerUp(x: x, y: 0.0)
|
|
129
117
|
|
|
130
|
-
fun uiMove(x: Float) ->
|
|
118
|
+
fun uiMove(x: Float) -> Input = PointerMove(x: x, y: 0.0)
|
|
131
119
|
|
|
132
120
|
fun uiCount(s: UiStep, k: UiKind) -> Int =
|
|
133
121
|
n := 0
|
plum-std/game/imperative/Scene.plum
CHANGED
|
@@ -9,6 +9,7 @@ import std/game/Color
|
|
|
9
9
|
import std/Io
|
|
10
10
|
import std/game/Affine
|
|
11
11
|
import std/game/Game
|
|
12
|
+
import std/game/Input
|
|
12
13
|
|
|
13
14
|
# A Scene2D-like scene graph: a tree of actors under a `Stage`. `Actor` is a trait;
|
|
14
15
|
# every kind of actor is a `record` that embeds it (`Group`, `Sprite`, `Rect`,
|
|
@@ -100,6 +101,11 @@ trait Actor(
|
|
|
100
101
|
fun hit(self, p: Vec2) -> Bool =
|
|
101
102
|
self.width > 0.0 && self.height > 0.0 && p.x >= 0.0 && p.x <= self.width && p.y >= 0.0 && p.y <= self.height
|
|
102
103
|
|
|
104
|
+
# Handles `e`, with a pointer position in this actor's own coordinates. Return true
|
|
105
|
+
# to consume it; otherwise it goes on to the parent. The default ignores it.
|
|
106
|
+
fun onInput(self, e: Input) -> Bool =
|
|
107
|
+
false
|
|
108
|
+
|
|
103
109
|
fun at(self, x: Float, y: Float) -> Actor =
|
|
104
110
|
self.x = x
|
|
105
111
|
self.y = y
|
|
@@ -468,13 +474,13 @@ fun hitNode(n: Actor, p: Vec2) -> Actor? =
|
|
|
468
474
|
|
|
469
475
|
# A root actor plus a camera (the world point at the screen's top-left and the
|
|
470
476
|
# pixels per world unit, as in `setCamera`).
|
|
471
|
-
record Stage(mut camX: Float, mut camY: Float, mut camScale: Float) =
|
|
477
|
+
record Stage(mut camX: Float, mut camY: Float, mut camScale: Float, mut focus: Actor?) =
|
|
472
478
|
|
|
473
479
|
fun init(self) =
|
|
474
480
|
self.root: Actor = Group()
|
|
475
481
|
|
|
476
482
|
fun create() -> Stage =
|
|
477
|
-
Stage(camX: 0.0, camY: 0.0, camScale: 1.0)
|
|
483
|
+
Stage(camX: 0.0, camY: 0.0, camScale: 1.0, focus: null)
|
|
478
484
|
|
|
479
485
|
# Runs every actor's `act`, parents before children, in insertion order.
|
|
480
486
|
fun act(self, dt: Float) =
|
|
@@ -489,12 +495,48 @@ record Stage(mut camX: Float, mut camY: Float, mut camScale: Float) =
|
|
|
489
495
|
fun hit(self, x: Float, y: Float) -> Actor? =
|
|
490
496
|
self.root.hitTree(Vec2(x: x, y: y))
|
|
491
497
|
|
|
498
|
+
# Delivers one input and says whether an actor consumed it. A pointer input goes
|
|
499
|
+
# to the topmost actor under it, then each parent in turn until one's `onInput`
|
|
500
|
+
# returns true; a press also gives that actor the keyboard focus. A key goes to the
|
|
501
|
+
# focused actor, then its parents. Unconsumed input is the game's to handle.
|
|
502
|
+
fun input(self, e: Input) -> Bool =
|
|
503
|
+
if p := e.position()
|
|
504
|
+
sp := self.screenToStage(p.x, p.y)
|
|
505
|
+
target := self.hit(sp.x, sp.y)
|
|
506
|
+
match e
|
|
507
|
+
PointerDown(_, _) =>
|
|
508
|
+
self.focus = target
|
|
509
|
+
_ => 0
|
|
510
|
+
if a := target
|
|
511
|
+
return Stage.bubble(a, Input.pointerAt(e, sp))
|
|
512
|
+
return false
|
|
513
|
+
if f := self.focus
|
|
514
|
+
return Stage.bubble(f, e)
|
|
515
|
+
false
|
|
516
|
+
|
|
517
|
+
# Offers `e` (in stage coordinates) to `a`, then each parent, until one consumes it.
|
|
518
|
+
fun bubble(a: Actor, e: Input) -> Bool =
|
|
519
|
+
cur: Actor? := a
|
|
520
|
+
while c := cur
|
|
521
|
+
if c.onInput(e.through(worldMatrix(c).inverse()))
|
|
522
|
+
return true
|
|
523
|
+
cur = c.parent
|
|
524
|
+
false
|
|
525
|
+
|
|
492
526
|
# A screen pixel in stage coordinates under this camera.
|
|
493
527
|
fun screenToStage(self, sx: Float, sy: Float) -> Vec2 =
|
|
494
528
|
Vec2(x: sx / self.camScale + self.camX, y: sy / self.camScale + self.camY)
|
|
495
529
|
|
|
496
530
|
# --- tests ---
|
|
497
531
|
|
|
532
|
+
# Counts the inputs it is offered and the last pointer x it saw; consumes them if `eat`.
|
|
533
|
+
record Catcher(Actor(width, height), mut got: Int, mut lastX: Float, eat: Bool) =
|
|
534
|
+
override fun onInput(self, e: Input) -> Bool =
|
|
535
|
+
self.got = self.got + 1
|
|
536
|
+
if p := e.position()
|
|
537
|
+
self.lastX = p.x
|
|
538
|
+
self.eat
|
|
539
|
+
|
|
498
540
|
fun sceneNear(a: Float, b: Float) -> Bool =
|
|
499
541
|
{a - b}.abs() < 0.0001
|
|
500
542
|
|
|
@@ -776,3 +818,30 @@ test "a child without a size is never culled"
|
|
|
776
818
|
slot.add(Rect.create(10.0, 10.0, Color.Red).at(-890.0, -890.0))
|
|
777
819
|
g.setCullingArea(0.0, 0.0, 100.0, 100.0)
|
|
778
820
|
assert flatten(g).length() == 3
|
|
821
|
+
|
|
822
|
+
test "input bubbles from the hit actor to its parents until consumed"
|
|
823
|
+
stage := Stage.create()
|
|
824
|
+
outer := Catcher(width: 100.0, height: 100.0, got: 0, lastX: 0.0, eat: true)
|
|
825
|
+
inner := Catcher(width: 10.0, height: 10.0, got: 0, lastX: 0.0, eat: false)
|
|
826
|
+
inner.x = 20.0
|
|
827
|
+
outer.add(inner)
|
|
828
|
+
stage.root.add(outer)
|
|
829
|
+
assert stage.input(PointerDown(x: 25.0, y: 5.0))
|
|
830
|
+
assert inner.got == 1
|
|
831
|
+
assert inner.lastX == 5.0
|
|
832
|
+
assert outer.got == 1
|
|
833
|
+
assert outer.lastX == 25.0
|
|
834
|
+
assert !stage.input(PointerDown(x: 500.0, y: 5.0))
|
|
835
|
+
|
|
836
|
+
test "a press gives the keyboard focus; keys bubble from it"
|
|
837
|
+
stage := Stage.create()
|
|
838
|
+
outer := Catcher(width: 100.0, height: 100.0, got: 0, lastX: 0.0, eat: false)
|
|
839
|
+
inner := Catcher(width: 10.0, height: 10.0, got: 0, lastX: 0.0, eat: false)
|
|
840
|
+
outer.add(inner)
|
|
841
|
+
stage.root.add(outer)
|
|
842
|
+
assert !stage.input(KeyDown(key: Key.KeyA))
|
|
843
|
+
assert inner.got == 0
|
|
844
|
+
_ := stage.input(PointerDown(x: 5.0, y: 5.0))
|
|
845
|
+
assert !stage.input(KeyDown(key: Key.KeyA))
|
|
846
|
+
assert inner.got == 2
|
|
847
|
+
assert outer.got == 2
|
plum-std/game/pure/PureScene.plum
CHANGED
|
@@ -10,36 +10,22 @@ import std/Io
|
|
|
10
10
|
import std/game/Affine
|
|
11
11
|
import std/game/Game
|
|
12
12
|
import std/game/pure/Actions
|
|
13
|
+
import std/game/Input
|
|
13
14
|
|
|
14
15
|
# A pure scene graph: a node is a record (`Node`) holding a `Transform` and
|
|
15
16
|
# an `enum Kind`. Group, Sprite, Label and Button are built in; games add their
|
|
16
17
|
# own behaviour with `Custom(actor: Actor)`, a behaviour-only trait their records
|
|
17
18
|
# embed (like Rust's `Box<dyn Actor>`). A node is a unique value: `update`,
|
|
18
|
-
# `
|
|
19
|
+
# `onInput`, `modify` and `route` take it, change it in place and hand it back;
|
|
19
20
|
# readers borrow. Only `drawTree` talks to the host, through `std/game/Game`.
|
|
21
|
+
# Input is the shared `Input` of std/game/Input; `route` delivers it along a hit
|
|
22
|
+
# path, child first, then each ancestor, until a node consumes it.
|
|
20
23
|
|
|
21
24
|
enum Touchable =
|
|
22
25
|
| Enabled
|
|
23
26
|
| Disabled
|
|
24
27
|
| ChildrenOnly
|
|
25
28
|
|
|
26
|
-
enum PointerKind =
|
|
27
|
-
| Down
|
|
28
|
-
| Up
|
|
29
|
-
| Move
|
|
30
|
-
|
|
31
|
-
# A pointer event in the actor's own coordinates; `over` says it is on the actor.
|
|
32
|
-
record Pointer(kind: PointerKind, x: Float, y: Float, over: Bool)
|
|
33
|
-
|
|
34
|
-
enum UiKind =
|
|
35
|
-
| Press
|
|
36
|
-
| Release
|
|
37
|
-
| Click
|
|
38
|
-
| Enter
|
|
39
|
-
| Exit
|
|
40
|
-
|
|
41
|
-
record UiEvent(id: Int, kind: UiKind)
|
|
42
|
-
|
|
43
29
|
# The node's position, size, origin, scale and rotation.
|
|
44
30
|
record Transform(x: Float, y: Float, width: Float, height: Float, originX: Float, originY: Float, scaleX: Float, scaleY: Float, rotation: Float) =
|
|
45
31
|
|
|
@@ -264,63 +250,71 @@ record Node(
|
|
|
264
250
|
|
|
265
251
|
# --- input ---
|
|
266
252
|
|
|
253
|
+
# Handles `e` (in the node's own coordinates; `over` says the pointer is on the
|
|
267
|
-
#
|
|
254
|
+
# node) and returns the new node, the events it raised and whether it consumed `e`.
|
|
268
|
-
#
|
|
255
|
+
# A Button reacts to the pointer; a Custom node asks its actor's `onInput`.
|
|
269
|
-
fun
|
|
256
|
+
fun onInput(self, e: Input, over: Bool) -> Handled =
|
|
270
257
|
n := self
|
|
271
258
|
ev := Slice[UiEvent]()
|
|
272
259
|
match n.kind
|
|
273
260
|
Button(text, upTex, downTex, wasPressed, wasOver) =>
|
|
274
261
|
pressed := wasPressed
|
|
275
|
-
|
|
262
|
+
now := wasOver
|
|
263
|
+
match e
|
|
276
|
-
|
|
264
|
+
PointerDown(_, _) =>
|
|
277
|
-
|
|
265
|
+
pressed = true
|
|
278
|
-
|
|
266
|
+
now = over
|
|
279
|
-
|
|
267
|
+
ev.push(UiEvent(id: n.id, kind: Press))
|
|
280
|
-
|
|
268
|
+
PointerMove(_, _) =>
|
|
281
|
-
|
|
269
|
+
now = over
|
|
282
|
-
|
|
270
|
+
if over && !wasOver
|
|
283
|
-
|
|
271
|
+
ev.push(UiEvent(id: n.id, kind: Enter))
|
|
284
|
-
|
|
272
|
+
if !over && wasOver
|
|
285
|
-
|
|
273
|
+
ev.push(UiEvent(id: n.id, kind: Exit))
|
|
286
|
-
|
|
274
|
+
PointerUp(_, _) =>
|
|
287
|
-
|
|
275
|
+
now = over
|
|
288
|
-
|
|
276
|
+
if wasPressed
|
|
289
|
-
|
|
277
|
+
pressed = false
|
|
290
|
-
|
|
278
|
+
ev.push(UiEvent(id: n.id, kind: Release))
|
|
291
|
-
|
|
279
|
+
if over
|
|
292
|
-
|
|
280
|
+
ev.push(UiEvent(id: n.id, kind: Click))
|
|
281
|
+
_ => 0
|
|
293
|
-
n = Node(..n, kind: Button(text: text, upTex: upTex, downTex: downTex, pressed: pressed, over:
|
|
282
|
+
n = Node(..n, kind: Button(text: text, upTex: upTex, downTex: downTex, pressed: pressed, over: now))
|
|
283
|
+
Custom(actor) =>
|
|
284
|
+
return actor.onInput(n, e, over)
|
|
294
285
|
_ => 0
|
|
295
|
-
Handled(node: n, events: ev)
|
|
286
|
+
Handled(node: n, events: ev, consumed: false)
|
|
296
287
|
|
|
297
|
-
# Delivers `e` (in the coordinates of the parent) along `path`: the child on
|
|
288
|
+
# Delivers `e` (in the coordinates of the parent) along `path`: the child on the
|
|
298
|
-
#
|
|
289
|
+
# path first, then this node, unless a child consumed it. A path that runs out
|
|
299
|
-
# actor it can.
|
|
290
|
+
# reaches the deepest actor it can.
|
|
300
|
-
fun route(self, path: Slice[Int], depth: Int, e:
|
|
291
|
+
fun route(self, path: Slice[Int], depth: Int, e: Input, over: Bool) -> Handled =
|
|
301
292
|
events := Slice[UiEvent]()
|
|
302
|
-
|
|
293
|
+
self.routeInto(path, depth, e, over, events)
|
|
303
|
-
|
|
294
|
+
|
|
304
|
-
|
|
305
|
-
# `route`, appending the raised events to `events`
|
|
295
|
+
# `route`, appending the raised events to `events` (the returned Handled holds it).
|
|
306
|
-
fun routeInto(self, path: Slice[Int], depth: Int, e:
|
|
296
|
+
fun routeInto(self, path: Slice[Int], depth: Int, e: Input, over: Bool, events: Slice[UiEvent]) -> Handled =
|
|
307
|
-
|
|
297
|
+
local := e
|
|
308
|
-
|
|
298
|
+
if q := self.localMatrix().invert()
|
|
309
|
-
if q := inv
|
|
310
|
-
|
|
299
|
+
local = e.through(q)
|
|
311
|
-
|
|
300
|
+
consumed := false
|
|
312
301
|
if depth < path.length()
|
|
313
302
|
match self.kind
|
|
314
303
|
Group(ks) =>
|
|
315
304
|
i := path.at(depth)
|
|
316
305
|
if i >= 0 && i < ks.length()
|
|
317
306
|
c := ks.takeAt(i)
|
|
318
|
-
|
|
307
|
+
h := c.routeInto(path, depth + 1, local, over, events)
|
|
308
|
+
consumed = h.consumed
|
|
309
|
+
ks.putAt(i, h.intoNode())
|
|
319
310
|
_ => 0
|
|
311
|
+
if consumed
|
|
312
|
+
return Handled(node: self, events: events, consumed: true)
|
|
320
|
-
h := self.
|
|
313
|
+
h := self.onInput(local, over)
|
|
321
314
|
for j := range h.events.length()
|
|
322
315
|
events.push(h.events.at(j))
|
|
323
|
-
h.
|
|
316
|
+
used := h.consumed
|
|
317
|
+
Handled(node: h.intoNode(), events: events, consumed: used)
|
|
324
318
|
|
|
325
319
|
enum Kind =
|
|
326
320
|
| Group(children: Slice[Node])
|
|
@@ -339,13 +333,20 @@ trait Actor =
|
|
|
339
333
|
fun draw(self, io: Io, n: Node, m: Affine, tint: Color) =
|
|
340
334
|
return
|
|
341
335
|
|
|
336
|
+
# Handles `e` (in the node's own coordinates; `over` says the pointer is on the
|
|
337
|
+
# node). Return `consumed: true` to keep it from the node's ancestors. The
|
|
338
|
+
# default ignores it.
|
|
339
|
+
fun onInput(self, n: Node, e: Input, over: Bool) -> Handled =
|
|
340
|
+
Handled(node: n, events: Slice[UiEvent](), consumed: false)
|
|
341
|
+
|
|
342
|
-
# The node after handling an
|
|
342
|
+
# The node after handling an input, the events it raised, and whether it consumed
|
|
343
|
+
# the input (its ancestors then do not see it).
|
|
343
|
-
record Handled(node: Node, events: Slice[UiEvent]) =
|
|
344
|
+
record Handled(node: Node, events: Slice[UiEvent], consumed: Bool) =
|
|
344
345
|
|
|
345
|
-
# The node, moved out; read `events` first, they stay behind.
|
|
346
|
+
# The node, moved out; read `events` and `consumed` first, they stay behind.
|
|
346
347
|
fun intoNode(self) -> Node =
|
|
347
348
|
match self
|
|
348
|
-
Handled(n, _) => n
|
|
349
|
+
Handled(n, _, _) => n
|
|
349
350
|
|
|
350
351
|
fun psKinds(h: Handled) -> Slice[UiKind] =
|
|
351
352
|
out := Slice[UiKind]()
|
|
@@ -365,8 +366,11 @@ fun psClicks(h: Handled) -> Int =
|
|
|
365
366
|
n = n + 1
|
|
366
367
|
n
|
|
367
368
|
|
|
369
|
+
fun psDown() -> Input = PointerDown(x: 1.0, y: 1.0)
|
|
370
|
+
|
|
371
|
+
fun psUp() -> Input = PointerUp(x: 1.0, y: 1.0)
|
|
372
|
+
|
|
368
|
-
fun
|
|
373
|
+
fun psMove() -> Input = PointerMove(x: 1.0, y: 1.0)
|
|
369
|
-
Pointer(kind: kind, x: 1.0, y: 1.0, over: over)
|
|
370
374
|
|
|
371
375
|
# A game actor: counts its draws and moves its node right by 1 on each act.
|
|
372
376
|
record Beacon(Actor, mut draws: Int) =
|
|
@@ -492,11 +496,11 @@ test "local_matrix_translates"
|
|
|
492
496
|
|
|
493
497
|
test "button_click_needs_press_and_over"
|
|
494
498
|
b := Node.button(7, "go", 1, 2, 0.0, 0.0, 10.0, 10.0)
|
|
495
|
-
h1 := b.
|
|
499
|
+
h1 := b.onInput(psDown(), true)
|
|
496
500
|
ks := psKinds(h1)
|
|
497
501
|
n1 := h1.events.length()
|
|
498
502
|
id1 := h1.events.at(0).id
|
|
499
|
-
h2 := h1.intoNode().
|
|
503
|
+
h2 := h1.intoNode().onInput(psUp(), true)
|
|
500
504
|
ks.push(h2.events.at(0).kind)
|
|
501
505
|
ks.push(h2.events.at(1).kind)
|
|
502
506
|
assert n1 == 1
|
|
@@ -509,25 +513,25 @@ test "button_click_needs_press_and_over"
|
|
|
509
513
|
|
|
510
514
|
test "button_release_elsewhere_no_click"
|
|
511
515
|
b := Node.button(7, "go", 1, 2, 0.0, 0.0, 10.0, 10.0)
|
|
512
|
-
h1 := b.
|
|
516
|
+
h1 := b.onInput(psDown(), true)
|
|
513
|
-
h2 := h1.intoNode().
|
|
517
|
+
h2 := h1.intoNode().onInput(psUp(), false)
|
|
514
518
|
assert h2.events.length() == 1
|
|
515
519
|
assert h2.events.at(0).kind == Release
|
|
516
520
|
assert !psIsPressed(h2.intoNode())
|
|
517
521
|
|
|
518
522
|
test "button_drag_off_and_back_one_click"
|
|
519
523
|
b := Node.button(7, "go", 1, 2, 0.0, 0.0, 10.0, 10.0)
|
|
520
|
-
a1 := b.
|
|
524
|
+
a1 := b.onInput(psDown(), true).intoNode()
|
|
521
|
-
h2 := a1.
|
|
525
|
+
h2 := a1.onInput(psMove(), false)
|
|
522
526
|
c2 := psClicks(h2)
|
|
523
|
-
h3 := h2.intoNode().
|
|
527
|
+
h3 := h2.intoNode().onInput(psMove(), true)
|
|
524
528
|
c3 := psClicks(h3)
|
|
525
|
-
h4 := h3.intoNode().
|
|
529
|
+
h4 := h3.intoNode().onInput(psUp(), true)
|
|
526
530
|
assert c2 + c3 + psClicks(h4) == 1
|
|
527
531
|
|
|
528
532
|
test "on_pointer_hands_back_changed_actor"
|
|
529
533
|
b := Node.button(7, "go", 1, 2, 0.0, 0.0, 10.0, 10.0)
|
|
530
|
-
h := b.
|
|
534
|
+
h := b.onInput(psDown(), true)
|
|
531
535
|
assert psIsPressed(h.intoNode())
|
|
532
536
|
|
|
533
537
|
test "custom_actor_act_and_draw_dispatch"
|
|
@@ -554,17 +558,17 @@ test "hit_box_and_children_of"
|
|
|
554
558
|
|
|
555
559
|
test "button_move_raises_enter_then_exit"
|
|
556
560
|
b := Node.button(7, "go", 1, 2, 0.0, 0.0, 10.0, 10.0)
|
|
557
|
-
h1 := b.
|
|
561
|
+
h1 := b.onInput(psMove(), true)
|
|
558
562
|
assert h1.events.length() == 1
|
|
559
563
|
assert h1.events.at(0).kind == Enter
|
|
560
|
-
h2 := h1.intoNode().
|
|
564
|
+
h2 := h1.intoNode().onInput(psMove(), false)
|
|
561
565
|
assert h2.events.length() == 1
|
|
562
566
|
assert h2.events.at(0).kind == Exit
|
|
563
567
|
assert !psIsPressed(h2.intoNode())
|
|
564
568
|
|
|
565
569
|
test "on_pointer_non_button_raises_nothing"
|
|
566
570
|
s := Node.sprite(1, 0, 0.0, 0.0, 10.0, 10.0)
|
|
567
|
-
h := s.
|
|
571
|
+
h := s.onInput(psDown(), true)
|
|
568
572
|
assert h.events.length() == 0
|
|
569
573
|
assert h.intoNode().id == 1
|
|
570
574
|
|
|
@@ -638,10 +642,10 @@ test "modify_changes_first_match_only"
|
|
|
638
642
|
|
|
639
643
|
test "route_child_first_then_group"
|
|
640
644
|
g := ttButtonRoot()
|
|
641
|
-
h1 := g.route(ttZero(), 0,
|
|
645
|
+
h1 := g.route(ttZero(), 0, PointerDown(x: 20.0, y: 20.0), true)
|
|
642
646
|
n1 := h1.events.length()
|
|
643
647
|
k1 := h1.events.at(0).kind
|
|
644
|
-
h2 := h1.intoNode().route(ttZero(), 0,
|
|
648
|
+
h2 := h1.intoNode().route(ttZero(), 0, PointerUp(x: 20.0, y: 20.0), true)
|
|
645
649
|
assert n1 == 1
|
|
646
650
|
assert k1 == Press
|
|
647
651
|
assert h2.events.length() == 2
|
|
@@ -651,7 +655,7 @@ test "route_child_first_then_group"
|
|
|
651
655
|
|
|
652
656
|
test "stale_path_reaches_group_only"
|
|
653
657
|
nine := Slice(9)
|
|
654
|
-
h := ttButtonRoot().route(nine, 0,
|
|
658
|
+
h := ttButtonRoot().route(nine, 0, PointerDown(x: 20.0, y: 20.0), true)
|
|
655
659
|
assert h.events.length() == 0
|
|
656
660
|
|
|
657
661
|
test "compaction_keeps_order"
|
plum-std/game/pure/SceneInput.plum
CHANGED
|
@@ -10,35 +10,16 @@ import std/game/Affine
|
|
|
10
10
|
import std/game/Game
|
|
11
11
|
import std/game/pure/Actions
|
|
12
12
|
import std/game/pure/PureScene
|
|
13
|
+
import std/game/Input
|
|
13
14
|
|
|
14
|
-
# The root of a pure scene tree, its camera
|
|
15
|
+
# The root of a pure scene tree, its camera, pointer capture and keyboard focus. A
|
|
15
|
-
# unique value: `input` and `frame` take it, change it in place and hand
|
|
16
|
+
# Stage is an unique value: `input` and `frame` take it, change it in place and hand
|
|
16
|
-
# inside a StageHandled (`intoStage` moves it out). A press captures the
|
|
17
|
+
# it back inside a StageHandled (`intoStage` moves it out). A press captures the
|
|
17
18
|
# actor it landed on: moves and the release go to that actor until the release,
|
|
18
|
-
# even when the pointer leaves it (scene2d's touch focus). The
|
|
19
|
+
# even when the pointer leaves it (scene2d's touch focus). The pressed actor also
|
|
20
|
+
# takes the keyboard focus: keys go to it first (then its ancestors), and keys no
|
|
21
|
+
# node consumes come back in `StageHandled.keys` for the game. The id stored with a
|
|
19
|
-
#
|
|
22
|
+
# path guards against the tree changing under it.
|
|
20
|
-
|
|
21
|
-
enum InputKind =
|
|
22
|
-
| PointerDown
|
|
23
|
-
| PointerUp
|
|
24
|
-
| PointerMove
|
|
25
|
-
|
|
26
|
-
# A pointer input in screen coordinates.
|
|
27
|
-
record Input(kind: InputKind, x: Float, y: Float) =
|
|
28
|
-
|
|
29
|
-
# This frame's pointer inputs from the host mouse: a press, a release, or a
|
|
30
|
-
# move. `wasDown` is whether the button was down last frame.
|
|
31
|
-
fun poll(io: Io, wasDown: Bool) -> Slice[Input] =
|
|
32
|
-
m := io.mousePos()
|
|
33
|
-
down := io.isMouseDown(0)
|
|
34
|
-
out := Slice[Input]()
|
|
35
|
-
if down && !wasDown
|
|
36
|
-
out.push(Input(kind: PointerDown, x: m.x, y: m.y))
|
|
37
|
-
else if !down && wasDown
|
|
38
|
-
out.push(Input(kind: PointerUp, x: m.x, y: m.y))
|
|
39
|
-
else
|
|
40
|
-
out.push(Input(kind: PointerMove, x: m.x, y: m.y))
|
|
41
|
-
out
|
|
42
23
|
|
|
43
24
|
record Capture(path: Slice[Int], id: Int) =
|
|
44
25
|
|
|
@@ -52,73 +33,96 @@ record Capture(path: Slice[Int], id: Int) =
|
|
|
52
33
|
return false
|
|
53
34
|
true
|
|
54
35
|
|
|
36
|
+
# The stage after some inputs, the UI events raised and the keys no node consumed.
|
|
55
|
-
record StageHandled(stage: Stage, events: Slice[UiEvent]) =
|
|
37
|
+
record StageHandled(stage: Stage, events: Slice[UiEvent], keys: Slice[Input]) =
|
|
56
38
|
|
|
57
|
-
# The stage, moved out; read `events` first, they stay behind.
|
|
39
|
+
# The stage, moved out; read `events` and `keys` first, they stay behind.
|
|
58
40
|
fun intoStage(self) -> Stage =
|
|
59
41
|
match self
|
|
60
|
-
StageHandled(s, _) => s
|
|
42
|
+
StageHandled(s, _, _) => s
|
|
61
43
|
|
|
62
|
-
record Stage(root: Node, camera: Affine, captured: Capture?) =
|
|
44
|
+
record Stage(root: Node, camera: Affine, captured: Capture?, focus: Int?) =
|
|
63
45
|
|
|
64
|
-
# A stage over `root` with no camera transform
|
|
46
|
+
# A stage over `root` with no camera transform, nothing captured and no focus.
|
|
65
47
|
fun create(root: Node) -> Stage =
|
|
66
|
-
Stage(root: root, camera: Affine.Identity, captured: null)
|
|
48
|
+
Stage(root: root, camera: Affine.Identity, captured: null, focus: null)
|
|
67
49
|
|
|
68
50
|
# Handles one input: routes it, returns the new stage and the events raised.
|
|
69
51
|
fun input(self, e: Input) -> StageHandled =
|
|
52
|
+
if !e.isPointer()
|
|
53
|
+
return self.key(e)
|
|
70
54
|
none := Slice[UiEvent]()
|
|
71
|
-
inv := self.camera.invert()
|
|
72
|
-
if inv == null
|
|
73
|
-
|
|
55
|
+
q := self.camera.invert() ?: return StageHandled(stage: self, events: none, keys: Slice[Input]())
|
|
74
|
-
world := Vec2(x: e.x, y: e.y)
|
|
75
|
-
if q := inv
|
|
76
|
-
|
|
56
|
+
world := e.through(q)
|
|
57
|
+
at := world.position() ?: Vec2(x: 0.0, y: 0.0)
|
|
77
|
-
hit := self.root.findHit(Affine.Identity,
|
|
58
|
+
hit := self.root.findHit(Affine.Identity, at)
|
|
78
59
|
# A capture is valid only while its path still leads to its actor.
|
|
79
60
|
cap: Capture? := null
|
|
80
61
|
if c := self.captured
|
|
81
62
|
if self.root.idAt(c.path, 0) == c.id
|
|
82
63
|
cap = c
|
|
64
|
+
match world
|
|
83
|
-
|
|
65
|
+
PointerDown(_, _) =>
|
|
84
|
-
|
|
66
|
+
if p := hit
|
|
85
|
-
|
|
67
|
+
id := self.root.idAt(p, 0)
|
|
86
|
-
|
|
68
|
+
return Stage(..self, focus: id).deliver(p, world, true, Capture(path: p, id: id))
|
|
87
|
-
|
|
69
|
+
return StageHandled(stage: Stage(..self, captured: null, focus: null), events: none, keys: Slice[Input]())
|
|
70
|
+
_ => 0
|
|
88
71
|
if c := cap
|
|
89
72
|
over := false
|
|
90
73
|
if p := hit
|
|
91
74
|
over = c.within(p)
|
|
75
|
+
match world
|
|
92
|
-
|
|
76
|
+
PointerUp(_, _) =>
|
|
93
|
-
|
|
77
|
+
return self.deliver(c.path, world, over, null)
|
|
78
|
+
_ => 0
|
|
94
|
-
return self.deliver(c.path,
|
|
79
|
+
return self.deliver(c.path, world, over, c)
|
|
95
80
|
# Hover without a press: route to whatever is under the pointer.
|
|
81
|
+
match world
|
|
96
|
-
|
|
82
|
+
PointerMove(_, _) =>
|
|
97
|
-
|
|
83
|
+
if p := hit
|
|
98
|
-
|
|
84
|
+
return self.deliver(p, world, true, null)
|
|
85
|
+
_ => 0
|
|
99
|
-
StageHandled(stage: self.withCapture(null), events: none)
|
|
86
|
+
StageHandled(stage: self.withCapture(null), events: none, keys: Slice[Input]())
|
|
87
|
+
|
|
88
|
+
# A key goes to the focused actor (then its ancestors); unconsumed, it is returned.
|
|
89
|
+
fun key(self, e: Input) -> StageHandled =
|
|
90
|
+
left := Slice(e)
|
|
91
|
+
if f := self.focus
|
|
92
|
+
if p := self.root.findPath(f)
|
|
93
|
+
events := Slice[UiEvent]()
|
|
94
|
+
match self
|
|
95
|
+
Stage(root, camera, captured, focus) =>
|
|
96
|
+
h := root.routeInto(p, 0, e, true, events)
|
|
97
|
+
if h.consumed
|
|
98
|
+
left = Slice[Input]()
|
|
99
|
+
return StageHandled(stage: Stage(root: h.intoNode(), camera: camera, captured: captured, focus: focus), events: events, keys: left)
|
|
100
|
+
StageHandled(stage: self, events: Slice[UiEvent](), keys: left)
|
|
100
101
|
|
|
101
102
|
fun withCapture(self, c: Capture?) -> Stage =
|
|
102
103
|
Stage(..self, captured: c)
|
|
103
104
|
|
|
104
|
-
fun deliver(self, path: Slice[Int],
|
|
105
|
+
fun deliver(self, path: Slice[Int], e: Input, over: Bool, c: Capture?) -> StageHandled =
|
|
105
106
|
events := Slice[UiEvent]()
|
|
106
107
|
match self
|
|
107
|
-
Stage(root, camera, _) =>
|
|
108
|
+
Stage(root, camera, _, focus) =>
|
|
108
|
-
|
|
109
|
+
h := root.routeInto(path, 0, e, over, events)
|
|
109
|
-
StageHandled(stage: Stage(root:
|
|
110
|
+
StageHandled(stage: Stage(root: h.intoNode(), camera: camera, captured: c, focus: focus), events: events, keys: Slice[Input]())
|
|
110
111
|
|
|
111
112
|
# Handles the inputs in order, then advances the tree by `dt`.
|
|
112
113
|
fun frame(self, dt: Float, inputs: Slice[Input]) -> StageHandled =
|
|
113
114
|
cur := self
|
|
114
115
|
out := Slice[UiEvent]()
|
|
116
|
+
keys := Slice[Input]()
|
|
115
117
|
for i := range inputs.length()
|
|
116
118
|
h := cur.input(inputs.at(i))
|
|
117
119
|
for j := range h.events.length()
|
|
118
120
|
out.push(h.events.at(j))
|
|
121
|
+
for j := range h.keys.length()
|
|
122
|
+
keys.push(h.keys.at(j))
|
|
119
123
|
cur = h.intoStage()
|
|
120
124
|
match cur
|
|
121
|
-
Stage(root, camera, captured) => StageHandled(stage: Stage(root: root.update(dt), camera: camera, captured: captured), events: out)
|
|
125
|
+
Stage(root, camera, captured, focus) => StageHandled(stage: Stage(root: root.update(dt), camera: camera, captured: captured, focus: focus), events: out, keys: keys)
|
|
122
126
|
|
|
123
127
|
# Paints the tree through the camera.
|
|
124
128
|
fun draw(self, io: Io) =
|
|
@@ -146,13 +150,13 @@ fun inOverOf(a: Node) -> Bool =
|
|
|
146
150
|
_ => false
|
|
147
151
|
|
|
148
152
|
fun inDown(x: Float, y: Float) -> Input =
|
|
149
|
-
|
|
153
|
+
PointerDown(x: x, y: y)
|
|
150
154
|
|
|
151
155
|
fun inUp(x: Float, y: Float) -> Input =
|
|
152
|
-
|
|
156
|
+
PointerUp(x: x, y: y)
|
|
153
157
|
|
|
154
158
|
fun inMove(x: Float, y: Float) -> Input =
|
|
155
|
-
|
|
159
|
+
PointerMove(x: x, y: y)
|
|
156
160
|
|
|
157
161
|
fun inClicks(events: Slice[UiEvent]) -> Int =
|
|
158
162
|
n := 0
|
|
@@ -169,6 +173,29 @@ fun inXformTree() -> Node =
|
|
|
169
173
|
me: Node := g2
|
|
170
174
|
me
|
|
171
175
|
|
|
176
|
+
# A custom actor that moves its node right on its key and reports presses.
|
|
177
|
+
record KeyMover(Actor, key: Int) =
|
|
178
|
+
override fun onInput(self, n: Node, e: Input, over: Bool) -> Handled =
|
|
179
|
+
match e
|
|
180
|
+
KeyDown(k) =>
|
|
181
|
+
if k == self.key
|
|
182
|
+
return Handled(node: Node(..n, x: n.x + 1.0), events: Slice[UiEvent](), consumed: true)
|
|
183
|
+
PointerDown(_, _) =>
|
|
184
|
+
return Handled(node: n, events: Slice(UiEvent(id: n.id, kind: Press)), consumed: true)
|
|
185
|
+
_ => 0
|
|
186
|
+
Handled(node: n, events: Slice[UiEvent](), consumed: false)
|
|
187
|
+
|
|
188
|
+
fun inMoverTree() -> Node =
|
|
189
|
+
m: Node := Node.custom(8, 10.0, 10.0, 50.0, 50.0, KeyMover(key: Key.KeyA))
|
|
190
|
+
m2 := Node(..m, touchable: Enabled)
|
|
191
|
+
g: Node := Node.group(1, 0.0, 0.0, 800.0, 600.0, Slice(m2))
|
|
192
|
+
g
|
|
193
|
+
|
|
194
|
+
fun inFirstX(s: Stage) -> Float =
|
|
195
|
+
if k := s.root.children()
|
|
196
|
+
return k.at(0).x
|
|
197
|
+
0.0 - 1.0
|
|
198
|
+
|
|
172
199
|
test "press_sets_capture"
|
|
173
200
|
h := Stage.create(inBtnTree()).input(inDown(20.0, 20.0))
|
|
174
201
|
assert h.events.length() == 1
|
|
@@ -201,7 +228,7 @@ test "drag_off_and_back_one_click"
|
|
|
201
228
|
test "stale_capture_dropped"
|
|
202
229
|
h1 := Stage.create(inBtnTree()).input(inDown(20.0, 20.0))
|
|
203
230
|
empty: Node := Node.group(1, 0.0, 0.0, 800.0, 600.0, Slice[Node]())
|
|
204
|
-
swapped := Stage(root: empty, camera: h1.stage.camera, captured: h1.stage.captured)
|
|
231
|
+
swapped := Stage(root: empty, camera: h1.stage.camera, captured: h1.stage.captured, focus: null)
|
|
205
232
|
h2 := swapped.input(inUp(20.0, 20.0))
|
|
206
233
|
assert h2.events.length() == 0
|
|
207
234
|
assert h2.stage.captured == null
|
|
@@ -213,7 +240,7 @@ test "hover_without_press_routes_move"
|
|
|
213
240
|
|
|
214
241
|
test "camera_maps_screen_to_world"
|
|
215
242
|
cam := Affine(a: 2.0, b: 0.0, c: 0.0, d: 2.0, e: 0.0, f: 0.0)
|
|
216
|
-
s := Stage(root: inBtnTree(), camera: cam, captured: null)
|
|
243
|
+
s := Stage(root: inBtnTree(), camera: cam, captured: null, focus: null)
|
|
217
244
|
h := s.input(inDown(40.0, 40.0))
|
|
218
245
|
assert h.events.length() == 1
|
|
219
246
|
assert h.events.at(0).kind == Press
|
|
@@ -243,13 +270,39 @@ test "hit_and_route_agree_for_rotated_scaled_group"
|
|
|
243
270
|
t := inXformTree()
|
|
244
271
|
hit := t.findHit(Affine.Identity, Vec2(x: 60.0, y: 40.0))
|
|
245
272
|
assert hit != null
|
|
246
|
-
h := Stage(root: t, camera: Affine.Identity, captured: null).input(
|
|
273
|
+
h := Stage(root: t, camera: Affine.Identity, captured: null, focus: null).input(inDown(60.0, 40.0))
|
|
247
274
|
assert h.events.length() == 1
|
|
248
275
|
assert h.events.at(0).id == 5
|
|
249
|
-
miss := Stage(root: inXformTree(), camera: Affine.Identity, captured: null).input(
|
|
276
|
+
miss := Stage(root: inXformTree(), camera: Affine.Identity, captured: null, focus: null).input(inDown(20.0, 20.0))
|
|
250
277
|
assert miss.events.length() == 0
|
|
251
278
|
|
|
252
279
|
test "input_changes_stage_in_place"
|
|
253
|
-
h := Stage.create(inBtnTree()).input(
|
|
280
|
+
h := Stage.create(inBtnTree()).input(inDown(20.0, 20.0))
|
|
254
281
|
assert h.stage.captured != null
|
|
255
282
|
assert inBtnPressed(h.stage)
|
|
283
|
+
|
|
284
|
+
test "custom_actor_gets_pointer_through_on_input"
|
|
285
|
+
h := Stage.create(inMoverTree()).input(inDown(20.0, 20.0))
|
|
286
|
+
assert h.events.length() == 1
|
|
287
|
+
assert h.events.at(0).id == 8
|
|
288
|
+
assert {h.stage.focus ?: -1} == 8
|
|
289
|
+
|
|
290
|
+
test "focused_actor_consumes_its_key"
|
|
291
|
+
h1 := Stage.create(inMoverTree()).input(inDown(20.0, 20.0))
|
|
292
|
+
h2 := h1.intoStage().input(KeyDown(key: Key.KeyA))
|
|
293
|
+
assert h2.keys.length() == 0
|
|
294
|
+
assert inFirstX(h2.stage) == 11.0
|
|
295
|
+
|
|
296
|
+
test "unconsumed_and_unfocused_keys_go_to_the_game"
|
|
297
|
+
h1 := Stage.create(inMoverTree()).input(KeyDown(key: Key.KeyA))
|
|
298
|
+
assert h1.keys.length() == 1
|
|
299
|
+
assert inFirstX(h1.stage) == 10.0
|
|
300
|
+
h2 := Stage.create(inMoverTree()).input(inDown(20.0, 20.0))
|
|
301
|
+
h3 := h2.intoStage().input(KeyDown(key: Key.KeyB))
|
|
302
|
+
assert h3.keys.length() == 1
|
|
303
|
+
|
|
304
|
+
test "frame_collects_unconsumed_keys"
|
|
305
|
+
ins := Slice(inDown(20.0, 20.0), KeyDown(key: Key.KeyA), KeyDown(key: Key.Space))
|
|
306
|
+
h := Stage.create(inMoverTree()).frame(0.0, ins)
|
|
307
|
+
assert h.keys.length() == 1
|
|
308
|
+
assert inFirstX(h.stage) == 11.0
|