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

Compare commits

..

7 Commits

Author SHA1 Message Date
Björn Ritzl
05f91dd763 Make sure focus_lost messages is received
Fixes #42
2019-03-17 21:50:01 +01:00
Björn Ritzl
b57609f061 Added additional tests and increased test framework version 2019-03-08 11:37:49 +01:00
Björn Ritzl
57946d27bf Added is_preloading() and when_loaded() 2019-03-08 10:28:39 +01:00
Björn Ritzl
1bc0ae09ee Simplified coroutine usage when running screen code 2019-03-08 09:11:37 +01:00
Björn Ritzl
66bdde41ed Make sure to pcall when invoking callbacks 2019-03-08 09:08:34 +01:00
Björn Ritzl
35dd0c3f70 Changed from listeners to transition_listeners table to be more explicit 2019-03-08 09:07:12 +01:00
Björn Ritzl
7d986ada1b pcall in the callback tracker 2019-03-08 09:03:29 +01:00
12 changed files with 434 additions and 77 deletions

View File

@@ -334,8 +334,26 @@ Preload a Monarch screen. This will load but not enable the screen. This is usef
* ```callback``` (function) - Optional function to call when the screen is preloaded.
### monarch.is_preloading(screen_id)
Check if a Monarch screen is preloading (via monarch.preload() or the Preload screen setting).
**PARAMETERS**
* ```screen_id``` (hash) - Id of the screen to check
**RETURN**
* ```preloading``` (boolean) - True if the screen is preloading.
### monarch.when_preloaded(screen_id, callback)
Invoke a callback when a screen has been preloaded.
**PARAMETERS**
* ```screen_id``` (hash) - Id of the screen to check
* ```callback``` (function) - Function to call when the screen has been preloaded.
### monarch.unload(screen_id, [callback])
Unload a preloaded Monarch screen. A preloaded screen will automatically get unloaded when hidden, but this function can be useful if a screen has been preloaded and it needs to be unloaded again.
Unload a preloaded Monarch screen. A preloaded screen will automatically get unloaded when hidden, but this function can be useful if a screen has been preloaded and it needs to be unloaded again without actually hiding it.
**PARAMETERS**
* ```screen_id``` (hash) - Id of the screen to unload.

View File

@@ -281,6 +281,11 @@ embedded_instances {
" type: PROPERTY_TYPE_URL\n"
" }\n"
" properties {\n"
" id: \"focus_url\"\n"
" value: \"about:/go#about\"\n"
" type: PROPERTY_TYPE_URL\n"
" }\n"
" properties {\n"
" id: \"preload\"\n"
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"

View File

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

View File

@@ -39,8 +39,8 @@ local screens = {}
-- the current stack of screens
local stack = {}
-- navigation listeners
local listeners = {}
-- transition listeners
local transition_listeners = {}
-- the number of active transitions
-- monarch is considered busy while there are active transitions
@@ -61,9 +61,25 @@ local function tohash(s)
return hash_lookup[s]
end
local function notify_listeners(message_id, message)
log("notify_listeners()", message_id)
for _,url in pairs(listeners) do
local function pcallfn(fn, ...)
if fn then
local ok, err = pcall(fn, ...)
if not ok then print(err) end
end
end
local function cowait(delay)
local co = coroutine.running()
assert(co, "You must run this form within a coroutine")
timer.delay(delay, false, function()
coroutine.resume(co)
end)
coroutine.yield()
end
local function notify_transition_listeners(message_id, message)
log("notify_transition_listeners()", message_id)
for _,url in pairs(transition_listeners) do
msg.post(url, message_id, message or {})
end
end
@@ -134,6 +150,7 @@ local function register(id, settings)
popup = settings and settings.popup,
popup_on_popup = settings and settings.popup_on_popup,
timestep_below_popup = settings and settings.timestep_below_popup or 1,
preload_listeners = {},
}
return screens[id]
end
@@ -357,6 +374,11 @@ local function focus_lost(screen, next_screen)
log("focus_lost()", screen.id)
if screen.focus_url then
msg.post(screen.focus_url, M.FOCUS.LOST, { id = next_screen and next_screen.id })
-- if there's no transition on the screen losing focus and it gets
-- unloaded this will happen before the focus_lost message reaches
-- the focus_url
-- we add a delay to ensure the message queue has time to be processed
cowait(0)
else
log("focus_lost() no focus url - ignoring")
end
@@ -376,11 +398,20 @@ local function reset_timestep(screen)
end
end
local function disable(screen, next_screen)
log("disable()", screen.id)
local function run_coroutine(screen, cb, fn)
local co
co = coroutine.create(function()
screen.co = co
fn()
screen.co = nil
pcallfn(cb)
end)
assert(coroutine.resume(co))
end
local function disable(screen, next_screen)
log("disable()", screen.id)
run_coroutine(screen, nil, function()
change_context(screen)
release_input(screen)
focus_lost(screen, next_screen)
@@ -389,34 +420,24 @@ local function disable(screen, next_screen)
else
reset_timestep(screen)
end
screen.co = nil
if cb then cb() end
end)
assert(coroutine.resume(co))
end
local function enable(screen, previous_screen)
log("enable()", screen.id)
local co
co = coroutine.create(function()
screen.co = co
run_coroutine(screen, nil, function()
change_context(screen)
acquire_input(screen)
focus_gained(screen, previous_screen)
reset_timestep(screen)
screen.co = nil
if cb then cb() end
end)
assert(coroutine.resume(co))
end
local function show_out(screen, next_screen, cb)
log("show_out()", screen.id)
local co
co = coroutine.create(function()
run_coroutine(screen, cb, function()
active_transition_count = active_transition_count + 1
notify_listeners(M.SCREEN_TRANSITION_OUT_STARTED, { screen = screen.id, next_screen = next_screen.id })
screen.co = co
notify_transition_listeners(M.SCREEN_TRANSITION_OUT_STARTED, { screen = screen.id, next_screen = next_screen.id })
change_context(screen)
release_input(screen)
focus_lost(screen, next_screen)
@@ -431,21 +452,16 @@ local function show_out(screen, next_screen, cb)
elseif next_is_popup then
change_timestep(screen)
end
screen.co = nil
active_transition_count = active_transition_count - 1
if cb then cb() end
notify_listeners(M.SCREEN_TRANSITION_OUT_FINISHED, { screen = screen.id, next_screen = next_screen.id })
notify_transition_listeners(M.SCREEN_TRANSITION_OUT_FINISHED, { screen = screen.id, next_screen = next_screen.id })
end)
coroutine.resume(co)
end
local function show_in(screen, previous_screen, reload, add_to_stack, cb)
log("show_in()", screen.id)
local co
co = coroutine.create(function()
run_coroutine(screen, cb, function()
active_transition_count = active_transition_count + 1
notify_listeners(M.SCREEN_TRANSITION_IN_STARTED, { screen = screen.id, previous_screen = previous_screen and previous_screen.id })
screen.co = co
notify_transition_listeners(M.SCREEN_TRANSITION_IN_STARTED, { screen = screen.id, previous_screen = previous_screen and previous_screen.id })
change_context(screen)
if reload and screen.loaded then
log("show_in() reloading", screen.id)
@@ -459,21 +475,16 @@ local function show_in(screen, previous_screen, reload, add_to_stack, cb)
transition(screen, M.TRANSITION.SHOW_IN, { previous_screen = previous_screen and previous_screen.id })
acquire_input(screen)
focus_gained(screen, previous_screen)
screen.co = nil
active_transition_count = active_transition_count - 1
if cb then cb() end
notify_listeners(M.SCREEN_TRANSITION_IN_FINISHED, { screen = screen.id, previous_screen = previous_screen and previous_screen.id })
notify_transition_listeners(M.SCREEN_TRANSITION_IN_FINISHED, { screen = screen.id, previous_screen = previous_screen and previous_screen.id })
end)
coroutine.resume(co)
end
local function back_in(screen, previous_screen, cb)
log("back_in()", screen.id)
local co
co = coroutine.create(function()
run_coroutine(screen, cb, function()
active_transition_count = active_transition_count + 1
notify_listeners(M.SCREEN_TRANSITION_IN_STARTED, { screen = screen.id, previous_screen = previous_screen and previous_screen.id })
screen.co = co
notify_transition_listeners(M.SCREEN_TRANSITION_IN_STARTED, { screen = screen.id, previous_screen = previous_screen and previous_screen.id })
change_context(screen)
load(screen)
reset_timestep(screen)
@@ -482,21 +493,16 @@ local function back_in(screen, previous_screen, cb)
end
acquire_input(screen)
focus_gained(screen, previous_screen)
screen.co = nil
active_transition_count = active_transition_count - 1
if cb then cb() end
notify_listeners(M.SCREEN_TRANSITION_IN_FINISHED, { screen = screen.id, previous_screen = previous_screen and previous_screen.id })
notify_transition_listeners(M.SCREEN_TRANSITION_IN_FINISHED, { screen = screen.id, previous_screen = previous_screen and previous_screen.id })
end)
coroutine.resume(co)
end
local function back_out(screen, next_screen, cb)
log("back_out()", screen.id)
local co
co = coroutine.create(function()
notify_listeners(M.SCREEN_TRANSITION_OUT_STARTED, { screen = screen.id, next_screen = next_screen and next_screen.id })
run_coroutine(screen, cb, function()
notify_transition_listeners(M.SCREEN_TRANSITION_OUT_STARTED, { screen = screen.id, next_screen = next_screen and next_screen.id })
active_transition_count = active_transition_count + 1
screen.co = co
change_context(screen)
release_input(screen)
focus_lost(screen, next_screen)
@@ -505,12 +511,9 @@ local function back_out(screen, next_screen, cb)
end
transition(screen, M.TRANSITION.BACK_OUT, { next_screen = next_screen and next_screen.id })
unload(screen)
screen.co = nil
active_transition_count = active_transition_count - 1
if cb then cb() end
notify_listeners(M.SCREEN_TRANSITION_OUT_FINISHED, { screen = screen.id, next_screen = next_screen and next_screen.id })
notify_transition_listeners(M.SCREEN_TRANSITION_OUT_FINISHED, { screen = screen.id, next_screen = next_screen and next_screen.id })
end)
coroutine.resume(co)
end
@@ -611,7 +614,15 @@ function M.show(id, options, data, cb)
end
end
-- show screen
-- show screen, wait until preloaded if it is already preloading
-- this can typpically happen if you do a show() on app start for a
-- screen that has Preload set to true
if M.is_preloading(id) then
M.when_preloaded(id, function()
coroutine.resume(co)
end)
coroutine.yield()
end
show_in(screen, top, options and options.reload, add_to_stack, callbacks.track())
if cb then callbacks.when_done(cb) end
@@ -637,6 +648,7 @@ function M.hide(id, cb)
assert(screens[id], ("There is no screen registered with id %s"):format(tostring(id)))
local screen = screens[id]
log("hide()", screen.id)
if M.in_stack(id) then
if not M.is_top(id) then
log("hide() you can only hide the screen at the top of the stack", id)
@@ -646,8 +658,8 @@ function M.hide(id, cb)
else
if M.is_visible(id) then
back_out(screen, nil, cb)
elseif cb then
cb()
else
pcallfn(cb)
end
end
return true
@@ -696,6 +708,36 @@ function M.back(data, cb)
end
--- Check if a screen is preloading via monarch.preload() or automatically
-- via the Preload screen option
-- @param id Screen id
-- @return true if preloading
function M.is_preloading(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)))
local screen = screens[id]
return screen.preloading
end
--- Invoke a callback when a specific screen has been preloaded
-- This is mainly useful on app start when wanting to show a screen that
-- has the Preload flag set (since it will immediately start to load which
-- would prevent a call to monarch.show from having any effect).
function M.when_preloaded(id, cb)
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)))
local screen = screens[id]
if screen.preloaded or screen.loaded then
pcallfn(cb, id)
else
screen.preload_listeners[#screen.preload_listeners + 1] = cb
end
end
--- Preload a screen. This will load but not enable and show a screen. Useful for "heavier" screens
-- that you wish to show without any delay.
-- @param id (string|hash) - Id of the screen to preload
@@ -713,22 +755,31 @@ function M.preload(id, cb)
local screen = screens[id]
log("preload()", screen.id)
if screen.preloaded or screen.loaded then
if cb then cb() end
pcallfn(cb)
return true
end
local co
co = coroutine.create(function()
screen.co = co
local function when_preloaded()
-- invoke any listeners added using monarch.when_preloaded()
while #screen.preload_listeners > 0 do
pcallfn(table.remove(screen.preload_listeners), id)
end
-- invoke the normal callback
pcallfn(cb)
end
run_coroutine(screen, when_preloaded, function()
screen.preloading = true
change_context(screen)
preload(screen)
log("preload() done", screen.id)
if cb then cb() end
screen.preloading = false
end)
assert(coroutine.resume(co))
return true
end
--- Unload a preloaded monarch screen
-- @param id (string|hash) - Id of the screen to unload
-- @param cb (function) - Optional callback to invoke when screen is unloaded
function M.unload(id, cb)
if M.is_busy() then
log("unload() monarch is busy, ignoring request")
@@ -744,19 +795,16 @@ function M.unload(id, cb)
end
local screen = screens[id]
log("unload()", screen.id)
if not screen.preloaded and not screen.loaded then
log("unload() screen is not loaded", tostring(id))
if cb then cb() end
pcallfn(cb)
return true
end
local co
co = coroutine.create(function()
screen.co = co
run_coroutine(screen, cb, function()
change_context(screen)
unload(screen)
if cb then cb() end
end)
assert(coroutine.resume(co))
return true
end
@@ -828,7 +876,7 @@ end
-- @param url The url to notify, nil for current url
function M.add_listener(url)
url = url or msg.url()
listeners[url_to_key(url)] = url
transition_listeners[url_to_key(url)] = url
end
@@ -836,7 +884,7 @@ end
-- @param url The url to remove, nil for current url
function M.remove_listener(url)
url = url or msg.url()
listeners[url_to_key(url)] = nil
transition_listeners[url_to_key(url)] = nil
end

View File

@@ -7,15 +7,20 @@ function M.create()
local callback = nil
local callback_count = 0
local function invoke_if_done()
if callback_count == 0 and callback then
local ok, err = pcall(callback)
if not ok then print(err) end
end
end
--- Create a callback function and track when it is done
-- @return Callback function
function instance.track()
callback_count = callback_count + 1
return function()
callback_count = callback_count - 1
if callback_count == 0 and callback then
callback()
end
invoke_if_done()
end
end
@@ -23,9 +28,7 @@ function M.create()
-- @param cb Function to call when all
function instance.when_done(cb)
callback = cb
if callback_count == 0 then
callback()
end
invoke_if_done()
end
return instance

View File

@@ -0,0 +1,37 @@
name: "focus1"
scale_along_z: 0
embedded_instances {
id: "go"
data: "components {\n"
" id: \"focus1\"\n"
" component: \"/test/data/focus1.gui\"\n"
" position {\n"
" x: 0.0\n"
" y: 0.0\n"
" z: 0.0\n"
" }\n"
" rotation {\n"
" x: 0.0\n"
" y: 0.0\n"
" z: 0.0\n"
" w: 1.0\n"
" }\n"
"}\n"
""
position {
x: 0.0
y: 0.0
z: 0.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale3 {
x: 1.0
y: 1.0
z: 1.0
}
}

131
test/data/focus1.gui Normal file
View File

@@ -0,0 +1,131 @@
script: "/test/data/focus1.gui_script"
fonts {
name: "example"
font: "/assets/example.font"
}
background_color {
x: 0.0
y: 0.0
z: 0.0
w: 0.0
}
nodes {
position {
x: 320.0
y: 568.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: ""
id: "box"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
layer: ""
inherit_alpha: true
slice9 {
x: 0.0
y: 0.0
z: 0.0
w: 0.0
}
clipping_mode: CLIPPING_MODE_NONE
clipping_visible: true
clipping_inverted: false
alpha: 1.0
template_node_child: false
size_mode: SIZE_MODE_AUTO
}
nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "1"
font: "example"
id: "text"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
outline {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
adjust_mode: ADJUST_MODE_FIT
line_break: false
parent: "box"
layer: ""
inherit_alpha: true
alpha: 1.0
outline_alpha: 1.0
shadow_alpha: 1.0
template_node_child: false
text_leading: 1.0
text_tracking: 0.0
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT
max_nodes: 512

View File

@@ -0,0 +1,9 @@
local monarch = require "monarch.monarch"
function on_message(self, message_id, message, sender)
if message_id == monarch.FOCUS.GAINED then
_G.focus1_gained = true
elseif message_id == monarch.FOCUS.LOST then
_G.focus1_lost = true
end
end

View File

@@ -96,7 +96,7 @@ nodes {
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "1"
text: "FOCUS 1"
font: "example"
id: "text"
xanchor: XANCHOR_NONE

View File

@@ -471,3 +471,66 @@ embedded_instances {
z: 1.0
}
}
embedded_instances {
id: "focus1"
data: "components {\n"
" id: \"screen_proxy\"\n"
" component: \"/monarch/screen_proxy.script\"\n"
" position {\n"
" x: 0.0\n"
" y: 0.0\n"
" z: 0.0\n"
" }\n"
" rotation {\n"
" x: 0.0\n"
" y: 0.0\n"
" z: 0.0\n"
" w: 1.0\n"
" }\n"
" properties {\n"
" id: \"screen_id\"\n"
" value: \"focus1\"\n"
" type: PROPERTY_TYPE_HASH\n"
" }\n"
" properties {\n"
" id: \"focus_url\"\n"
" value: \"focus1:/go#focus1\"\n"
" type: PROPERTY_TYPE_URL\n"
" }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionproxy\"\n"
" type: \"collectionproxy\"\n"
" data: \"collection: \\\"/test/data/focus1.collection\\\"\\n"
"exclude: false\\n"
"\"\n"
" position {\n"
" x: 0.0\n"
" y: 0.0\n"
" z: 0.0\n"
" }\n"
" rotation {\n"
" x: 0.0\n"
" y: 0.0\n"
" z: 0.0\n"
" w: 1.0\n"
" }\n"
"}\n"
""
position {
x: 0.0
y: 0.0
z: 0.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale3 {
x: 1.0
y: 1.0
z: 1.0
}
}

View File

@@ -5,5 +5,8 @@ local test_monarch = require "test.test_monarch"
function init(self)
deftest.add(test_monarch)
deftest.run({ coverage = { enabled = true }})
deftest.run({
coverage = { enabled = true },
--pattern = "preload",
})
end

View File

@@ -7,6 +7,7 @@ local SCREEN1_STR = hash("screen1")
local SCREEN1 = hash(SCREEN1_STR)
local SCREEN2 = hash("screen2")
local SCREEN_PRELOAD = hash("screen_preload")
local FOCUS1 = hash("focus1")
local BACKGROUND = hash("background")
local POPUP1 = hash("popup1")
local POPUP2 = hash("popup2")
@@ -34,6 +35,14 @@ return function()
return fn(...)
end
local function wait_until_done(fn)
local is_done = false
local function done()
is_done = true
end
fn(done)
wait_timeout(function() return is_done end)
end
local function wait_until_visible(screen_id)
return wait_timeout(is_visible, screen_id)
end
@@ -302,6 +311,16 @@ return function()
assert(wait_until_not_busy())
end)
it("should be able to preload a screen and wait for it", function()
assert(not monarch.is_preloading(TRANSITION1))
monarch.preload(TRANSITION1)
assert(monarch.is_preloading(TRANSITION1))
wait_until_done(function(done)
monarch.when_preloaded(TRANSITION1, done)
end)
assert(not monarch.is_preloading(TRANSITION1))
end)
it("should ignore any preload calls while busy", function()
monarch.show(TRANSITION1)
-- previously a call to preload() while also showing a screen would
@@ -357,6 +376,11 @@ return function()
assert(mock_msg.messages(URL1)[10].message.screen == SCREEN1)
end)
it("should be able to show a screen even while it is preloading", function()
assert(monarch.is_preloading(SCREEN_PRELOAD))
monarch.show(SCREEN_PRELOAD, nil, { count = 1 })
assert(wait_until_shown(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, nil, { count = 1 })
@@ -382,5 +406,21 @@ return function()
-- second time the screen gets shown it will be reloaded and increment the count
assert(monarch.data(SCREEN_PRELOAD).count == 2)
end)
it("should send focus messages", function()
_G.focus1_gained = nil
_G.focus1_lost = nil
monarch.show(SCREEN1)
assert(wait_until_shown(SCREEN1), "Screen1 was never shown")
monarch.show(FOCUS1)
assert(wait_until_shown(FOCUS1), "Screen1 was never shown")
assert(_G.focus1_gained)
monarch.show(SCREEN1)
assert(wait_until_shown(SCREEN1), "Screen1 was never shown")
assert(wait_until_hidden(FOCUS1), "Focus1 was never hidden")
assert(_G.focus1_lost)
end)
end)
end