diff --git a/LANGUAGE.md b/LANGUAGE.md index a9421b1b..bf409864 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -371,8 +371,10 @@ program Pack { dispatching needs no state parameter. - **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 - (`core_tick_all`); and wherever the program calls `drain_actions()` (an `entry` program, a test, - a loop of its own). Draining runs the actions in the order they were dispatched, and each action's + (`core_tick_all`); when ludic.ui has run a frame's presses (`ui_show`, `ui_press`), so a button'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 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 diff --git a/changes/ui-press-drain.md b/changes/ui-press-drain.md new file mode 100644 index 00000000..bf682862 --- /dev/null +++ b/changes/ui-press-drain.md @@ -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. diff --git a/examples/actions/ui_press.ludic b/examples/actions/ui_press.ludic new file mode 100644 index 00000000..3c37e5e9 --- /dev/null +++ b/examples/actions/ui_press.ludic @@ -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}`) + } +} diff --git a/examples/actions/ui_press_parts/Adder.ludic b/examples/actions/ui_press_parts/Adder.ludic new file mode 100644 index 00000000..d3a5c8f9 --- /dev/null +++ b/examples/actions/ui_press_parts/Adder.ludic @@ -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 } } +} diff --git a/examples/actions/ui_press_parts/Adder.xml b/examples/actions/ui_press_parts/Adder.xml new file mode 100644 index 00000000..2b736719 --- /dev/null +++ b/examples/actions/ui_press_parts/Adder.xml @@ -0,0 +1,3 @@ +
+ +
diff --git a/packages/ludic.ui/frame.ludic b/packages/ludic.ui/frame.ludic index 8e9c6ecd..c9745e05 100644 --- a/packages/ludic.ui/frame.ludic +++ b/packages/ludic.ui/frame.ludic @@ -1,5 +1,7 @@ # 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 { 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) lc_run(ui_st, screen) 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 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 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) } + drain_actions() } # 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) } diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index 301633f5..57f15a19 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -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_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)") + 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)") actions_deps_case() deps_reach_case()