fix(ui): 0.R4 - an action a UI button dispatches is reduced before the frame is presented
ludic.ui runs a frame's presses after drawing it, and what they dispatched waited for the end of the phase - after the host had presented - so a button's change showed a frame late. Decided: drain, not a phase per action. ui_show and ui_press drain the queue once the presses have run, so the code after them and the frame presented next see the change; a host that runs presses some other way calls drain_actions() before it presents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d46f0adb5e
commit
259b4d9b35
7 changed files with 46 additions and 3 deletions
|
|
@ -371,8 +371,10 @@ program Pack {
|
||||||
dispatching needs no state parameter.
|
dispatching needs no state parameter.
|
||||||
- **When the queue is drained:** at the end of every phase of the frame loop (so what the `Input`
|
- **When the queue is drained:** at the end of every phase of the frame loop (so what the `Input`
|
||||||
phase dispatches is reduced before `Update`); after every phase of the ludic.base system runner
|
phase dispatches is reduced before `Update`); after every phase of the ludic.base system runner
|
||||||
(`core_tick_all`); and wherever the program calls `drain_actions()` (an `entry` program, a test,
|
(`core_tick_all`); when ludic.ui has run a frame's presses (`ui_show`, `ui_press`), so a button's
|
||||||
a loop of its own). Draining runs the actions in the order they were dispatched, and each action's
|
action is reduced before the host presents the frame, not a frame later; and wherever the program
|
||||||
|
calls `drain_actions()` (an `entry` program, a test, a loop of its own - and a host that runs UI
|
||||||
|
presses some other way calls it before it presents). Draining runs the actions in the order they were dispatched, and each action's
|
||||||
reducers in the order of their states' names - never the order of imports - so the same actions
|
reducers in the order of their states' names - never the order of imports - so the same actions
|
||||||
make the same changes on every machine and in a replay.
|
make the same changes on every machine and in a replay.
|
||||||
- **An action a reducer dispatches** is queued behind the rest and reduced in the same drain, never
|
- **An action a reducer dispatches** is queued behind the rest and reduced in the same drain, never
|
||||||
|
|
|
||||||
7
changes/ui-press-drain.md
Normal file
7
changes/ui-press-drain.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**An action a UI button dispatches is reduced before the frame is presented.** ludic.ui runs a frame's
|
||||||
|
presses after drawing it, and an action they dispatched waited for the end of the phase - after the
|
||||||
|
host had presented - so what the button changed showed a frame late. `ui_show` and `ui_press` now
|
||||||
|
drain the queue once the presses have run. A host that runs presses some other way calls
|
||||||
|
`drain_actions()` before it presents.
|
||||||
22
examples/actions/ui_press.ludic
Normal file
22
examples/actions/ui_press.ludic
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# ui_press.ludic - 0.R4: an action a UI control dispatches is reduced before the UI hands control
|
||||||
|
# back, so the code after it - and the frame the host presents - already sees the change
|
||||||
|
import "ludic.ui"
|
||||||
|
program UiPress {
|
||||||
|
numbers float
|
||||||
|
state Score { points: int = 0 }
|
||||||
|
action Scored { n: int = 1 }
|
||||||
|
reducer Score on Scored(s: mut Score, a: Scored) { s.points += a.n }
|
||||||
|
import "ui_press_parts/Adder.ludic"
|
||||||
|
function frame(ui_st: mut UiState) -> UiNode {
|
||||||
|
let root: UiNode = ui_nodes(ui_st, "Adder", null)
|
||||||
|
ui_place(ui_st, root, 0.0, 0.0, 300.0, 200.0)
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
entry (ui_st: mut UiState, score: Score) {
|
||||||
|
let root = frame(ui_st)
|
||||||
|
ui_press(ui_st, root.children[0].children[0])
|
||||||
|
let after_one = score.points
|
||||||
|
ui_press(ui_st, root.children[0].children[0])
|
||||||
|
print(`{after_one} {score.points}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
4
examples/actions/ui_press_parts/Adder.ludic
Normal file
4
examples/actions/ui_press_parts/Adder.ludic
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Adder.ludic - a button that says what happened and changes nothing itself
|
||||||
|
component Adder {
|
||||||
|
on add(n: int) { dispatch Scored { n: n } }
|
||||||
|
}
|
||||||
3
examples/actions/ui_press_parts/Adder.xml
Normal file
3
examples/actions/ui_press_parts/Adder.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
<button on-click="add(5)">+5</button>
|
||||||
|
</div>
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
# frame.ludic - a screen shown: built from the view's model, laid out, drawn, and its presses
|
# frame.ludic - a screen shown: built from the view's model, laid out, drawn, and its presses
|
||||||
# run once the frame is drawn, so nothing the view does can change a frame half way through it
|
# run once the frame is drawn, so nothing the view does can change a frame half way through it;
|
||||||
|
# the actions they dispatch are reduced before ui_show returns, so the frame presented next is built
|
||||||
|
# from what they changed
|
||||||
|
|
||||||
export function ui_show(ui_st: mut UiState, screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
|
export function ui_show(ui_st: mut UiState, screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
|
||||||
in_read(ui_st)
|
in_read(ui_st)
|
||||||
|
|
@ -25,6 +27,7 @@ export function ui_show(ui_st: mut UiState, screen: string, view: UiView, x: flo
|
||||||
dv_after(ui_st, screen, root)
|
dv_after(ui_st, screen, root)
|
||||||
lc_run(ui_st, screen)
|
lc_run(ui_st, screen)
|
||||||
nt_fired_run(ui_st)
|
nt_fired_run(ui_st)
|
||||||
|
drain_actions() # 0.R4: what the presses dispatched is reduced before the host presents
|
||||||
}
|
}
|
||||||
# the screen's nodes for this frame, not yet laid out - for a test, or a host that places them
|
# the screen's nodes for this frame, not yet laid out - for a test, or a host that places them
|
||||||
export function ui_nodes(ui_st: mut UiState, screen: string, view: UiView) -> UiNode {
|
export function ui_nodes(ui_st: mut UiState, screen: string, view: UiView) -> UiNode {
|
||||||
|
|
@ -78,6 +81,7 @@ export function ui_place(ui_st: mut UiState, root: UiNode, x: float, y: float, w
|
||||||
# a press, as if the pointer had made it
|
# a press, as if the pointer had made it
|
||||||
export function ui_press(ui_st: mut UiState, n: UiNode) -> void {
|
export function ui_press(ui_st: mut UiState, n: UiNode) -> void {
|
||||||
if n.act != null and n.enabled { act_run(ui_st, n.act, n.env) }
|
if n.act != null and n.enabled { act_run(ui_st, n.act, n.env) }
|
||||||
|
drain_actions()
|
||||||
}
|
}
|
||||||
# any event, as if the element had fired it - for a test
|
# any event, as if the element had fired it - for a test
|
||||||
export function ui_event(ui_st: mut UiState, n: UiNode, name: string, value: Val) -> void { nt_run(ui_st, n, name, value) }
|
export function ui_event(ui_st: mut UiState, n: UiNode, name: string, value: Val) -> void { nt_run(ui_st, n, name, value) }
|
||||||
|
|
|
||||||
|
|
@ -1012,6 +1012,7 @@ function cmd_dev_test() -> int {
|
||||||
reject_case("rejected/layer_reach", "hud uses items.item_count", "a layered module is still held to its uses outside the layer")
|
reject_case("rejected/layer_reach", "hud uses items.item_count", "a layered module is still held to its uses outside the layer")
|
||||||
reject_case("rejected/layer_cycle", "go round in a circle: items -> layer app -> items", "a cycle through a layer and out of it is refused")
|
reject_case("rejected/layer_cycle", "go round in a circle: items -> layer app -> items", "a cycle through a layer and out of it is refused")
|
||||||
feat_case("actions/pack", "", "0 1 9 13 picked 7 picked 9 too heavy", "pack.ludic (0.R: actions, reducers - one per state, in the order of the states' names - dispatch, drain_actions, an action a reducer dispatches goes behind)")
|
feat_case("actions/pack", "", "0 1 9 13 picked 7 picked 9 too heavy", "pack.ludic (0.R: actions, reducers - one per state, in the order of the states' names - dispatch, drain_actions, an action a reducer dispatches goes behind)")
|
||||||
|
controller_case("actions/ui_press", "", "5 10", "ui_press.ludic (0.R4: an action a UI press dispatches is reduced before ludic.ui hands control back)")
|
||||||
feat_case("actions/modules", "", "2", "modules.ludic (0.R: an action in one module, its reducer in another)")
|
feat_case("actions/modules", "", "2", "modules.ludic (0.R: an action in one module, its reducer in another)")
|
||||||
actions_deps_case()
|
actions_deps_case()
|
||||||
deps_reach_case()
|
deps_reach_case()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue