3
0
mirror of https://github.com/britzl/monarch.git synced 2025-11-26 19:00:53 +01:00

Compare commits

..

8 Commits
3.4.0 ... 3.4.4

Author SHA1 Message Date
Björn Ritzl
17df189089 Replace existing transition
Fixes #82
2022-01-13 19:40:24 +01:00
Björn Ritzl
387a1805eb Added option to change timestep for screen when below popup
Fixes #73
2021-09-19 23:08:31 +02:00
Björn Ritzl
e26da2c6c5 Removed old change log file 2021-09-19 23:05:43 +02:00
Björn Ritzl
c75b1b0044 A screen added with no_stack should not manipulate the stack when hidden 2021-08-19 14:53:05 +02:00
Björn Ritzl
d3799a93ff Make sure that callbacks aren't invoked more than once 2021-08-19 14:35:05 +02:00
Björn Ritzl
76d4ca2927 Added monarch.clear() 2021-07-20 00:27:34 +02:00
Björn Ritzl
9e81b3a327 Removed one-frame delay during show/back-in
If the screen had a transition the delay caused the screen to render in its initial position for one frame before the transition was applied
2021-07-01 15:04:52 +02:00
Björn Ritzl
4927d6e766 Update stack earlier when showing new screen
Fixes #76
2021-06-27 00:34:32 +02:00
9 changed files with 195 additions and 124 deletions

View File

@@ -1,60 +0,0 @@
## Monarch 2.8.0 [britzl released 2018-06-10]
NEW: Prevent show/hide operations while busy showing/hiding another screen
FIX: Make sure to properly finish active transitions when layout changes
## Monarch 2.7.0 [britzl released 2018-06-04]
NEW: Added monarch.top([offset]) and monarch.bottom([offset]) to get screen id of top and bottom screens (w. optional offset)
NEW: Transition messages now contain `next_screen` or `previous_screen`
## Monarch 2.6.1 [britzl released 2018-06-04]
FIX: Check if screen has already been preloaded before trying to preload it again (the callback will still be invoked).
## Monarch 2.6.0 [britzl released 2018-06-03]
NEW: monarch.preload() to load but not show a screen. Useful for content heavy screens that you wish to show without delay.
## Monarch 2.5.0 [britzl released 2018-06-01]
NEW: Transitions will send a `transition_done` message to the creator of the transition to notify that the transition has finished. The `message` will contain which transition that was finished.
## Monarch 2.4.0 [britzl released 2018-05-26]
NEW: Screen transitions are remembered so that they can be replayed when the screen layout changes.
## Monarch 2.3.0 [britzl released 2018-03-24]
CHANGE: The functions in monarch.lua that previously only accepted a hash as screen id now also accepts strings (and does the conversion internally)
## Monarch 2.2.0 [britzl released 2018-03-19]
NEW: Transitions now handle layout changes (via `layout_changed` message)
NEW: Transitions can now be notified of changes in window size using transition.window_resize(width, height)
## Monarch 2.1 [britzl released 2017-12-27]
NEW: Added Popup on Popup flag that allows a popup to be shown on top of another popup
## Monarch 2.0 [britzl released 2017-12-08]
BREAKING CHANGE: If you are using custom screen transitions (ie your own transition functions) you need to make a change to the function. The previous function signature was ```(node, to, easing, duration, delay, url)``` where ```url``` was the URL to where the ```transition_done``` message was supposed to be posted. The new function signature for a transition function is: ```(node, to, easing, duration, delay, cb)``` where ```cb``` is a function that should be invoked when the transition is completed.
FIX: Fixed issues related to screen transitions.
FIX: Code cleanup to reduce code duplication.
FIX: Improved documentation regarding transitions.
## Monarch 1.4 [britzl released 2017-12-06]
FIX: Several bugfixes for specific corner cases.
## Monarch 1.3 [britzl released 2017-12-01]
FIX: monarch.back(data, cb) set the data on the previous screen not the new current screen.
NEW: monarch.is_top(id)
NEW: monarch.get_stack()
NEW: monarch.in_stack(id)
## Monarch 1.2 [britzl released 2017-11-28]
NEW: Message id constants exposed from the Monarch module
NEW: Focus lost/gained contains id of next/previous screen
## Monarch 1.1 [britzl released 2017-11-22]
FIX: Bugfixes for transitions and state under certain circumstances
NEW: Added 'reload' option to show() command.
## Monarch 1.0 [britzl released 2017-09-28]
First public stable release
## Monarch 0.9 [britzl released 2017-09-17]

View File

@@ -33,6 +33,13 @@ Hide a screen that has been shown using the `no_stack` option. If used on a scre
* `success` (boolean) - True if the process of hiding the screen was started successfully.
## monarch.clear([callback])
Clear the stack of screens completely. Any visible screen will be hidden by navigating back out from them. This operation will be added to the queue if Monarch is busy.
**PARAMETERS**
* `callback` (function) - Optional function to call when the stack has been cleared.
## monarch.back([data], [callback])
Go back to a previous Monarch screen. This operation will be added to the queue if Monarch is busy.
@@ -142,6 +149,14 @@ Check if a Monarch screen is visible.
* `exists` (boolean) - True if the screen is visible.
## monarch.set_timestep_below_popup(screen_id, timestep)
Set the timestep to apply for a screen when below a popup.
**PARAMETERS**
* `screen_id` (string|hash) - Id of the screen to change timestep setting for
* `timestep` (number) - Timestep to apply
## monarch.add_listener([url])
Add a URL that will be notified of navigation events.

View File

@@ -1,7 +1,7 @@
[project]
title = Monarch
version = 0.9
dependencies = https://github.com/britzl/deftest/archive/2.7.0.zip
dependencies#0 = https://github.com/britzl/deftest/archive/2.7.0.zip
[bootstrap]
main_collection = /example/advanced/advanced.collectionc

View File

@@ -1,4 +1,5 @@
local callback_tracker = require "monarch.utils.callback_tracker"
local async = require "monarch.utils.async"
local M = {}
@@ -185,7 +186,7 @@ function M.is_visible(id)
assert(id, "You must provide a screen id")
id = tohash(id)
assert(screens[id], ("There is no screen registered with id %s"):format(tostring(id)))
return screens[id].loaded
return screens[id].visible
end
@@ -570,6 +571,7 @@ local function show_out(screen, next_screen, wait_for_transition, cb)
local current_is_popup = screen.popup
if (not next_is_popup and not current_is_popup) or (current_is_popup) then
transition(screen, M.TRANSITION.SHOW_OUT, { next_screen = next_screen.id }, wait_for_transition)
screen.visible = false
unload(screen)
elseif next_is_popup then
change_timestep(screen)
@@ -590,21 +592,22 @@ local function show_in(screen, previous_screen, reload, add_to_stack, wait_for_t
log("show_in() reloading", screen.id)
unload(screen, reload)
end
if add_to_stack then
stack[#stack + 1] = screen
end
local ok, err = load(screen)
if not ok then
log("show_in()", err)
if add_to_stack then
stack[#stack] = nil
end
active_transition_count = active_transition_count - 1
notify_transition_listeners(M.SCREEN_TRANSITION_FAILED, { screen = screen.id })
return
end
-- wait until screen has had a chance to render
cowait(0)
cowait(0)
if add_to_stack then
stack[#stack + 1] = screen
end
reset_timestep(screen)
transition(screen, M.TRANSITION.SHOW_IN, { previous_screen = previous_screen and previous_screen.id }, wait_for_transition)
screen.visible = true
acquire_input(screen)
focus_gained(screen, previous_screen)
active_transition_count = active_transition_count - 1
@@ -626,13 +629,11 @@ local function back_in(screen, previous_screen, wait_for_transition, cb)
notify_transition_listeners(M.SCREEN_TRANSITION_FAILED, { screen = screen.id })
return
end
-- wait until screen has had a chance to render
cowait(0)
cowait(0)
reset_timestep(screen)
if previous_screen and not previous_screen.popup then
transition(screen, M.TRANSITION.BACK_IN, { previous_screen = previous_screen.id }, wait_for_transition)
end
screen.visible = true
acquire_input(screen)
focus_gained(screen, previous_screen)
active_transition_count = active_transition_count - 1
@@ -653,6 +654,7 @@ local function back_out(screen, next_screen, wait_for_transition, cb)
reset_timestep(next_screen)
end
transition(screen, M.TRANSITION.BACK_OUT, { next_screen = next_screen and next_screen.id }, wait_for_transition)
screen.visible = false
unload(screen)
active_transition_count = active_transition_count - 1
notify_transition_listeners(M.SCREEN_TRANSITION_OUT_FINISHED, { screen = screen.id, next_screen = next_screen and next_screen.id })
@@ -743,8 +745,9 @@ function M.show(id, options, data, cb)
pop = pop - 1
end
stack[#stack] = nil
show_out(top, screen, WAIT_FOR_TRANSITION, callbacks.track())
callbacks.yield_until_done()
async(function(await, resume)
await(show_out, top, screen, WAIT_FOR_TRANSITION, resume)
end)
top = stack[#stack]
end
@@ -783,15 +786,16 @@ function M.show(id, options, data, cb)
local same_screen = top and top.id == screen.id
if same_screen or (options and options.sequential) then
if top then
show_out(top, screen, WAIT_FOR_TRANSITION, callbacks.track())
callbacks.yield_until_done()
async(function(await, resume)
await(show_out, top, screen, WAIT_FOR_TRANSITION, resume)
end)
end
show_in(screen, top, options and options.reload, add_to_stack, WAIT_FOR_TRANSITION, callbacks.track())
else
-- show screen
local cb = callbacks.track()
show_in(screen, top, options and options.reload, add_to_stack, DO_NOT_WAIT_FOR_TRANSITION, function()
if top and not top.popup then
if add_to_stack and top and not top.popup then
show_out(top, screen, WAIT_FOR_TRANSITION, callbacks.track())
end
cb()
@@ -825,7 +829,7 @@ end
-- Hide a screen. The screen must either be at the top of the stack or
-- visible but not added to the stack (through the no_stack option)
-- @param id (string|hash) - Id of the screen to show
-- @param id (string|hash) - Id of the screen to .hide
-- @param cb (function) - Optional callback to invoke when the screen is hidden
-- @return true if successfully hiding, false if busy or for some other reason unable to hide the screen
function M.hide(id, cb)
@@ -862,6 +866,34 @@ function M.hide(id, cb)
end
-- Clear stack completely. Any visible screens will be hidden by navigating back out
-- from them.
-- @param cb (function) - Optional callback to invoke when the stack has been cleared
function M.clear(cb)
log("clear() queuing action")
queue_action(function(action_done, action_error)
async(function(await, resume)
local top = stack[#stack]
while top and top.visible do
stack[#stack] = nil
await(back_out, top, screen, WAIT_FOR_TRANSITION, resume)
top = stack[#stack]
end
while stack[#stack] do
table.remove(stack)
end
pcallfn(cb)
pcallfn(action_done)
end)
end)
end
-- Go back to the previous screen in the stack.
-- @param data (*) - Optional data to set for the previous screen
-- @param cb (function) - Optional callback to invoke when the previous screen is visible again
@@ -870,6 +902,7 @@ function M.back(data, cb)
queue_action(function(action_done)
local callbacks = callback_tracker()
local back_cb = callbacks.track()
local screen = table.remove(stack)
if screen then
log("back()", screen.id)
@@ -881,7 +914,7 @@ function M.back(data, cb)
if data then
top.data = data
end
back_in(top, screen, WAIT_FOR_TRANSITION, callbacks.track())
back_in(top, screen, WAIT_FOR_TRANSITION, back_cb)
end)
else
if top then
@@ -889,10 +922,10 @@ function M.back(data, cb)
top.data = data
end
back_in(top, screen, DO_NOT_WAIT_FOR_TRANSITION, function()
back_out(screen, top, WAIT_FOR_TRANSITION, callbacks.track())
back_out(screen, top, WAIT_FOR_TRANSITION, back_cb)
end)
else
back_out(screen, top, WAIT_FOR_TRANSITION, callbacks.track())
back_out(screen, top, WAIT_FOR_TRANSITION, back_cb)
end
end
end
@@ -1122,6 +1155,19 @@ function M.bottom(offset)
return screen and screen.id
end
--- Set the timestep to apply for a screen when below a popup
-- @param id (string|hash) Id of the screen to change timestep setting for
-- @param timestep (number) Timestep to apply
function M.set_timestep_below_popup(id, timestep)
assert(id, "You must provide a screen id")
assert(timestep, "You must provide a timestep")
id = tohash(id)
assert(screens[id], ("There is no screen registered with id %s"):format(tostring(id)))
screens[id].timestep_below_popup = timestep
end
local function url_to_key(url)
return (url.socket or hash("")) .. (url.path or hash("")) .. (url.fragment or hash(""))
end

View File

@@ -121,11 +121,15 @@ local function create()
local current_transition = nil
local function create_transition(transition_id, node, fn, easing, duration, delay)
assert(transition_id, "You must provide a valid transition id")
assert(node, "You must provide a node")
assert(fn, "You must provide a transition function")
local t = transitions[transition_id]
-- find if there's already a transition for the node in
-- question and if so update it instead of creating a new
-- transition
for _,transition in ipairs(t) do
for _,transition in ipairs(t.transitions) do
if transition.node == node then
transition.fn = fn
transition.easing = easing

47
monarch/utils/async.lua Normal file
View File

@@ -0,0 +1,47 @@
local M = {}
local NOT_STARTED = "not_started"
local YIELDED = "yielded"
local RESUMED = "resumed"
local RUNNING = "running"
function M.async(fn)
local co = coroutine.running()
local state = NOT_STARTED
local function await(fn, ...)
state = RUNNING
fn(...)
if state ~= RUNNING then
return
end
state = YIELDED
local r = { coroutine.yield() }
return unpack(r)
end
local function resume(...)
if state ~= RUNNING then
state = RUNNING
local ok, err = coroutine.resume(co, ...)
if not ok then
print(err)
print(debug.traceback())
end
end
end
if co then
return fn(await, resume)
else
co = coroutine.create(fn)
return resume(await, resume)
end
end
return setmetatable(M, {
__call = function(t, ...)
return M.async(...)
end
})

View File

@@ -6,13 +6,18 @@ function M.create()
local callback = nil
local callback_count = 0
local all_callbacks_done = false
local function is_done()
return callback_count == 0
end
local function invoke_if_done()
if all_callbacks_done then
print("Warning: The same callback will be invoked twice from the callback tracker!", id or "")
end
if callback_count == 0 and callback then
all_callbacks_done = true
local ok, err = pcall(callback)
if not ok then print(err) end
end
@@ -41,20 +46,6 @@ function M.create()
invoke_if_done()
end
function instance.yield_until_done()
local co = coroutine.running()
callback = function()
local ok, err = coroutine.resume(co)
if not ok then
print(err)
end
end
invoke_if_done()
if not is_done() then
coroutine.yield()
end
end
return instance
end

View File

@@ -39,7 +39,7 @@ return function()
local screens_instances = {}
local function is_shown(screen_id)
local function is_visible(screen_id)
return monarch.is_visible(screen_id)
end
@@ -65,9 +65,6 @@ return function()
local function wait_until_visible(screen_id)
return wait_timeout(is_visible, screen_id)
end
local function wait_until_shown(screen_id)
return wait_timeout(is_shown, screen_id)
end
local function wait_until_hidden(screen_id)
return wait_timeout(is_hidden, screen_id)
end
@@ -145,23 +142,23 @@ return function()
assert(not monarch.is_visible(SCREEN1))
monarch.show(SCREEN1)
assert(wait_until_stack({ SCREEN1 }))
assert(monarch.is_visible(SCREEN1))
assert(wait_until_visible(SCREEN1))
monarch.show(SCREEN2)
assert(wait_until_stack({ SCREEN1, SCREEN2 }))
assert(not monarch.is_visible(SCREEN1))
assert(monarch.is_visible(SCREEN2))
assert(wait_until_hidden(SCREEN1))
assert(wait_until_visible(SCREEN2))
monarch.show(POPUP1)
assert(wait_until_stack({ SCREEN1, SCREEN2, POPUP1 }))
assert(not monarch.is_visible(SCREEN1))
assert(monarch.is_visible(SCREEN2))
assert(monarch.is_visible(POPUP1))
assert(wait_until_hidden(SCREEN1))
assert(wait_until_visible(SCREEN2))
assert(wait_until_visible(POPUP1))
end)
it("should be able to show a screen without adding it to the stack", function()
monarch.show(BACKGROUND, { no_stack = true })
assert(wait_until_shown(BACKGROUND), "Background was never shown")
assert(wait_until_visible(BACKGROUND), "Background was never shown")
assert(wait_until_stack({ }))
monarch.show(SCREEN1)
@@ -175,7 +172,7 @@ return function()
monarch.show(BACKGROUND, { no_stack = true })
assert(wait_until_not_busy())
assert(wait_until_shown(BACKGROUND))
assert(wait_until_visible(BACKGROUND))
assert(wait_until_stack({ SCREEN1 }))
monarch.show(SCREEN2)
@@ -184,13 +181,13 @@ return function()
monarch.back()
assert(wait_until_not_busy())
assert(wait_until_shown(SCREEN1))
assert(wait_until_visible(SCREEN1))
assert(wait_until_stack({ SCREEN1 }))
end)
it("should be able to hide a screen not added to the stack", function()
monarch.show(BACKGROUND, { no_stack = true })
assert(wait_until_shown(BACKGROUND), "Background was never shown")
assert(wait_until_visible(BACKGROUND), "Background was never shown")
assert_stack({ })
monarch.hide(BACKGROUND)
@@ -213,18 +210,18 @@ return function()
it("should be able to pass data to a screen when showing it or going back to it", function()
local data1 = { foo = "bar" }
monarch.show(SCREEN1, nil, data1)
assert(wait_until_shown(SCREEN1), "Screen1 was never shown")
assert(wait_until_visible(SCREEN1), "Screen1 was never shown")
local data2 = { boo = "car" }
monarch.show(SCREEN2, nil, data2)
assert(wait_until_shown(SCREEN2), "Screen2 was never shown")
assert(wait_until_visible(SCREEN2), "Screen2 was never shown")
assert(monarch.data(SCREEN1) == data1, "Expected data on screen1 doesn't match actual data")
assert(monarch.data(SCREEN2) == data2, "Expected data on screen2 doesn't match actual data")
local data_back = { going = "back" }
monarch.back(data_back)
assert(wait_until_shown(SCREEN1))
assert(wait_until_visible(SCREEN1))
assert(monarch.data(SCREEN1) == data_back, "Expected data on screen1 doesn't match actual data")
end)
@@ -368,7 +365,7 @@ return function()
it("should be busy while transition is running", function()
monarch.show(TRANSITION1)
assert(monarch.is_busy())
assert(wait_until_shown(TRANSITION1), "Transition1 was never shown")
assert(wait_until_visible(TRANSITION1), "Transition1 was never shown")
assert(wait_until_not_busy())
end)
@@ -386,7 +383,7 @@ return function()
-- previously a call to preload() while also showing a screen would
-- lock up monarch. See issue #32
monarch.preload(TRANSITION1)
assert(wait_until_shown(TRANSITION1), "Transition1 was never shown")
assert(wait_until_visible(TRANSITION1), "Transition1 was never shown")
end)
it("should be able to notify listeners of navigation events", function()
@@ -438,12 +435,12 @@ return function()
it("should be able to show a screen even while it is preloading", function()
monarch.show(SCREEN_PRELOAD, nil, { count = 1 })
assert(wait_until_shown(SCREEN_PRELOAD), "Screen_preload was never shown")
assert(wait_until_visible(SCREEN_PRELOAD), "Screen_preload was never shown")
end)
it("should be able to preload a screen and always keep it loaded", function()
monarch.show(SCREEN_PRELOAD)
assert(wait_until_shown(SCREEN_PRELOAD), "Screen_preload was never shown")
assert(wait_until_visible(SCREEN_PRELOAD), "Screen_preload was never shown")
monarch.back()
assert(wait_until_hidden(SCREEN_PRELOAD), "Screen_preload was never hidden")
assert(monarch.is_preloaded(SCREEN_PRELOAD))
@@ -451,12 +448,12 @@ return function()
it("should be able to reload a preloaded screen", function()
monarch.show(SCREEN_PRELOAD, nil, { count = 1 })
assert(wait_until_shown(SCREEN_PRELOAD), "Screen_preload was never shown")
assert(wait_until_visible(SCREEN_PRELOAD), "Screen_preload was never shown")
-- first time the screen gets loaded it will increment the count
assert(monarch.data(SCREEN_PRELOAD).count == 2)
monarch.show(SCREEN_PRELOAD, { clear = true, reload = true }, { count = 1 })
assert(wait_until_shown(SCREEN_PRELOAD), "Screen_preload was never shown")
assert(wait_until_visible(SCREEN_PRELOAD), "Screen_preload was never shown")
-- second time the screen gets shown it will be reloaded and increment the count
assert(monarch.data(SCREEN_PRELOAD).count == 2)
end)
@@ -470,9 +467,11 @@ return function()
assert(wait_until_stack({ SCREEN1 }))
monarch.show(FOCUS1)
assert(wait_until_stack({ SCREEN1, FOCUS1 }))
assert(wait_until_visible(FOCUS1))
assert(_G.focus1_gained)
monarch.show(SCREEN1)
assert(wait_until_stack({ SCREEN1, FOCUS1, SCREEN1 }))
assert(wait_until_hidden(FOCUS1))
assert(_G.focus1_lost)
end)
@@ -483,14 +482,14 @@ return function()
-- proxy screen
monarch.show(SCREEN1)
wait_until_shown(SCREEN1)
wait_until_visible(SCREEN1)
assert(monarch.post(SCREEN1, "foobar"), "Expected monarch.post() to return true")
cowait(0.1)
assert(_G.screen1_foobar, "Screen1 never received a message")
-- factory screen
monarch.show(SCREEN2)
wait_until_shown(SCREEN2)
wait_until_visible(SCREEN2)
assert(monarch.post(SCREEN2, "foobar"), "Expected monarch.post() to return true")
cowait(0.1)
assert(_G.screen2_foobar, "Screen2 never received a message")
@@ -503,7 +502,7 @@ return function()
-- proxy screen
monarch.show(SCREEN1)
wait_until_shown(SCREEN1)
wait_until_visible(SCREEN1)
assert(monarch.post(SCREEN1, "foobar", { foo = "bar" }), "Expected monarch.post() to return true")
cowait(0.1)
assert(_G.screen1_foobar, "Screen1 never received a message")
@@ -511,7 +510,7 @@ return function()
-- factory screen
monarch.show(SCREEN2)
wait_until_shown(SCREEN2)
wait_until_visible(SCREEN2)
assert(monarch.post(SCREEN2, "foobar", { foo = "bar" }), "Expected monarch.post() to return true")
cowait(0.1)
assert(_G.screen2_foobar, "Screen2 never received a message")
@@ -525,6 +524,7 @@ return function()
monarch.show(SCREEN1)
monarch.show(SCREEN2)
assert(wait_until_stack({ SCREEN1, SCREEN2 }))
assert(wait_until_hidden(SCREEN1))
local ok, err = monarch.post(SCREEN1, "foobar")
assert(not ok and err, "Expected monarch.post() to return false plus an error message")
cowait(0.1)
@@ -534,7 +534,7 @@ return function()
it("should not be able to post messages to proxy screens without a receiver url", function()
monarch.show(POPUP1)
wait_until_shown(POPUP1)
wait_until_visible(POPUP1)
local ok, err = monarch.post(POPUP1, "foobar")
assert(not ok and err, "Expected monarch.post() to return false plus an error message")
end)

View File

@@ -8,6 +8,12 @@ local easing = require "monarch.transitions.easings"
return function()
local function wait_timeout(fn, ...)
local args = { ... }
cowait(function() return fn(unpack(args)) end, 5)
return fn(...)
end
describe("transitions", function()
before(function()
mock_msg.mock()
@@ -22,6 +28,28 @@ describe("transitions", function()
end)
it("should replace an existing transition with a new one", function()
local one = false
function dummy_transition1(node, to, easing, duration, delay, cb)
one = true
end
local two = false
function dummy_transition2(node, to, easing, duration, delay, cb)
two = true
end
local node = gui.new_box_node(vmath.vector3(), vmath.vector3(100, 100, 0))
local duration = 2
local t = transitions.create(node)
t.show_in(dummy_transition1, easing.OUT, duration, delay or 0)
t.show_in(dummy_transition2, easing.OUT, duration, delay or 0)
t.handle(monarch.TRANSITION.SHOW_IN)
wait_timeout(function() return one or two end)
assert(two)
assert(not one)
end)
it("should replay and immediately finish on layout change", function()
function dummy_transition(node, to, easing, duration, delay, cb)
print("dummy transition")