1
0
Fork 0
mirror of https://github.com/Reuh/ubiquitousse.git synced 2025-10-27 17:19:31 +00:00
ubiquitousse/signal/signal.can

334 lines
10 KiB
Text

--[[-- Simple signal / observer pattern implementation for Lua.
No dependency.
Optional dependency: LÖVE to hook into LÖVE events.
The returned module also acts as a global `SignalRegistry`, so you can call the `:bind`, `:emit`, etc. methods directly on the module
if you don't need to isolate your signals in separate registries.
@module signal
@usage
local signal = require("ubiquitousse.signal")
-- Bind a function to a "hit" signal
signal:bind("hit", function(enemy)
print(enemy.." was hit!")
end)
-- Somewhere else in your code: will call every function bound to "hit" signal with "invader" argument
signal:emit("hit", "invader")
-- We also provides a predefined SignalRegistry (signal.event) which emit signals on LÖVE callbacks
-- You can initialize it with:
signal.registerEvents()
signal.event:bind("update", function(dt) print("called every update") end)
signal.event:bind("keypressed", function(key, scancode) print("pressed key "..key) end)
-- etc., for every LÖVE callback
--]]
let signal
--- Signal registry.
--
-- A SignalRegistry is a separate ubiquitousse.signal instance: its signals will be independant from other registries.
-- @type SignalRegistry
let registry_mt = {
--- Map of signals to list of listeners.
-- @ftype {["name"]={fn,[fn]=1,...}}
signals = {},
--- List of registries chained to this registry.
-- @ftype { registry, ... }
chained = {},
--- Bind a function to a signal name.
-- @tparam string name the name of the signal
-- @tparam function fn the function to bind to the signal
bind = :(name, fn)
assert(not @has(name, fn), ("function %s already bound to signal %s"):format(fn, name))
if not @signals[name] then
@signals[name] = {}
end
table.insert(@signals[name], fn)
return @
end,
--- Returns true if fn is bound to the signal.
-- @tparam string name the name of the signal
-- @tparam function fn the function
has = :(name, fn)
if not @signals[name] then
return false
end
for _, f in ipairs(@signals[name]) do
if f == fn then
return true
end
end
return false
end,
--- Unbind a function from a signal name.
-- @tparam string name the name of the signal
-- @tparam function fn the function to unbind to the signal
unbind = :(name, fn)
if not @signals[name] then
@signals[name] = {}
end
for i=#@signals[name], 1, -1 do
local f = @signals[name][i]
if f == fn then
table.remove(@signals[name], i)
return @
end
end
error(("function %s not bound to signal %s"):format(fn, name))
end,
--- Unbind a function from every signal whose name match the pattern.
-- @tparam string pat Lua pattern string
-- @tparam function fn the function to unbind to the signals
unbindPattern = :(pat, fn)
return @_patternize("unbind", pat, fn)
end,
--- Remove every bound function to a signal name.
-- @tparam string name the name of the signal
clear = :(name)
@signals[name] = nil
end,
--- Remove every bound function to every signal whose name match the pattern.
-- @tparam string pat Lua string pattern
clearPattern = :(pat)
return @_patternize("clear", pat)
end,
--- Emit a signal, i.e. call every function bound to it, with the given arguments.
-- @tparam string name the name of the signal
-- @param ... arguments to pass to the functions bound to this signal
emit = :(name, ...)
if @signals[name] then
for _, fn in ipairs(@signals[name]) do
fn(...)
end
end
for _, c in ipairs(@chained) do
c:emit(name, ...)
end
return @
end,
--- Emit to every signal whose name match the pattern.
-- @tparam string pat Lua pattern string
-- @param ... arguments to pass to the functions bound to each signal
emitPattern = :(pat, ...)
return @_patternize("emit", pat, ...)
end,
--- Chain another regsitry to this registry.
-- I.e., after an event is emitted in this registry it will be automatically emitted in the other registry.
-- Several registries can be chained to a single registry.
-- @tparam SignalRegistry registry
chain = :(registry)
if not registry then
registry = signal.new()
end
table.insert(@chained, registry)
return registry
end,
--- Unchain a specific registry from the registry chaining list.
-- Will error if the regsitry is not in the chaining list.
-- @tparam SignalRegistry registry
unchain = :(registry)
for i=#@chained, 1, -1 do
if @chained[i] == registry then
table.remove(@chained, i)
return @
end
end
error("the givent registry is not chained with this registry")
end,
_patternize = :(method, pat, ...)
for name in pairs(@signals) do
if name:match(pat) then
@[method](@, name, ...)
end
end
end
}
registry_mt.__index = registry_mt
--- Signal group.
--
-- A SignalGroup is a list of (registry, signal name, function) triplets.
-- When the group is active, all of these triplets will bind the specified signal name to the specified function in the specified registry.
-- When the group is paused, all of these triplets are unbound.
--
-- This can be used to maintain a list of signal bindings where every one should be either disabled or enabled at the same time.
-- For example you may maintain a signal group of signals you want to be emitted when your game is running, and disabled when the game is paused
-- (like inputs, update, simulation step, etc. signals).
--
-- @type SignalGroup
let group_mt = {
--- Indicate if the signal group if currently paused or not.
-- @ftype boolean
paused = false,
--- List of triplets in the group.
-- @ftype { {registry, "signal name", function}, ... }
binds = {},
--- Bind a function to a signal name in the given registry.
-- This handles binding the function on its own; you do not need to call `SignalRegistry:bind` manually.
-- If the group is paused, this will not bind the function immediately but only on the next time this group is resumed (as expected).
-- @tparam SignalRegistry registry to bind the signal in
-- @tparam string name the name of the signal
-- @tparam function fn the function to bind to the signal
bind = :(registry, name, fn)
table.insert(@binds, { registry, name, fn })
if not @paused then registry:bind(name, fn) end
end,
--- Remove every bound triplet in the group.
clear = :()
if not @paused then
for _, b in ipairs(@binds) do
b[1]:unbind(b[2], b[3])
end
end
@binds = {}
end,
--- Pause the group.
-- The signals bound to this group will be disabled in their given registries.
pause = :()
assert(not @paused, "event group is already paused")
@paused = true
for _, b in ipairs(@binds) do
b[1]:unbind(b[2], b[3])
end
end,
--- Resume the group.
-- The signals bound to this group will be enabled in their given registries.
resume = :()
assert(@paused, "event group is not paused")
@paused = false
for _, b in ipairs(@binds) do
b[1]:bind(b[2], b[3])
end
end
}
group_mt.__index = group_mt
--- Module.
--
-- @section module
signal = {
--- Creates and return a new SignalRegistry.
-- @treturn SignalRegistry
new = ()
return setmetatable({ signals = {}, chained = {} }, registry_mt)
end,
--- Creates and return a new SignalGroup.
-- @treturn SignalGroup
group = ()
return setmetatable({ binds = {} }, group_mt)
end,
-- Global SignalRegistry.
signals = {},
bind = (...)
return registry_mt.bind(signal, ...)
end,
has = (...)
return registry_mt.has(signal, ...)
end,
unbind = (...)
return registry_mt.unbind(signal, ...)
end,
unbindPattern = (...)
return registry_mt.unbindPattern(signal, ...)
end,
clear = (...)
return registry_mt.clear(signal, ...)
end,
clearPattern = (...)
return registry_mt.clearPattern(signal, ...)
end,
emit = (...)
return registry_mt.emit(signal, ...)
end,
emitPattern = (...)
return registry_mt.emitPattern(signal, ...)
end,
--- `SignalRegistry` which will be used to bind signals that need to be called on LÖVE events; other ubiquitousse modules may bind to this registry
-- if avaible.
--
-- For example, every ubiquitousse module with a "update" function will bind it to the "update" signal in the registry;
-- you can then call this signal on each game update to update every ubiquitousse module easily.
--
-- You will need to call `registerEvents` for the signal to be called on LÖVE callbacks automatically (otherwise you will have to emit the events
-- from the LÖVE callbacks manually).
--
-- List of signals available: "displayrotated", "draw", "load", "lowmemory", "quit", "update",
-- "directorydropped", "filedropped", "focus", "mousefocus", "resize", "visible",
-- "keypressed", "keyreleased", "textedited", "textinput",
-- "mousemoved", "mousepressed", "mousereleased", "wheelmoved",
-- "gamepadaxis", "gamepadpressed", "gamepadreleased",
-- "joystickadded", "joystickaxis", "joystickhat", "joystickpressed", "joystickreleased", "joystickremoved",
-- "touchmoved", "touchpressed", "touchreleased".
--
-- @ftype SignalRegistry
event = nil,
--- Call this function to hook `signal.event` signals to LÖVE events.
-- This means overriding every existing LÖVE callback. If a callback is already defined, the new one will call the old function along with the signal:emit.
-- @require love
registerEvents = ()
local callbacks = { -- everything except run, errorhandler, threaderror
"displayrotated", "draw", "load", "lowmemory", "quit", "update",
"directorydropped", "filedropped", "focus", "mousefocus", "resize", "visible",
"keypressed", "keyreleased", "textedited", "textinput",
"mousemoved", "mousepressed", "mousereleased", "wheelmoved",
"gamepadaxis", "gamepadpressed", "gamepadreleased",
"joystickadded", "joystickaxis", "joystickhat", "joystickpressed", "joystickreleased", "joystickremoved",
"touchmoved", "touchpressed", "touchreleased"
}
local event = signal.event
for _, callback in ipairs(callbacks) do
if callback == "update" then
if love[callback] then
local old = love[callback]
love[callback] = function(dt)
old(dt)
event:emit(callback, dt)
end
else
love[callback] = function(dt)
event:emit(callback, dt)
end
end
else
if love[callback] then
local old = love[callback]
love[callback] = function(...)
old(...)
event:emit(callback, ...)
end
else
love[callback] = function(...)
event:emit(callback, ...)
end
end
end
end
end
}
signal.event = signal.new()
return signal