diff --git a/src/lua/include/property.hpp b/src/lua/include/property.hpp index f19fdeec..724261be 100644 --- a/src/lua/include/property.hpp +++ b/src/lua/include/property.hpp @@ -33,17 +33,33 @@ class Property { public: Property() : Property(std::monostate{}) {} Property(const LuaValue&); - Property(const LuaValue&, std::function); + Property(const LuaValue&, std::function filter); - auto Get() -> const LuaValue& { return *value_; } + auto get() -> const LuaValue& { return *value_; } - auto IsTwoWay() -> bool { return cb_.has_value(); } + /* + * Assigns a new value to this property, bypassing the filter fn. All + * bindings will be marked as dirty, and if active, will be reapplied. + */ + auto setDirect(const LuaValue&) -> void; + /* + * Invokes the filter fn, and if successful, assigns the new value to this + * property. All bindings will be marked as dirty, and if active, will be + * reapplied. + */ + auto set(const LuaValue&) -> bool; - auto PushValue(lua_State& s) -> int; - auto PopValue(lua_State& s) -> bool; - auto Update(const LuaValue& new_val) -> void; + /* Returns whether or not this Property can be written from Lua. */ + auto isTwoWay() -> bool { return cb_.has_value(); } - auto AddLuaBinding(lua_State*, int ref) -> void; + auto pushValue(lua_State& s) -> int; + auto popValue(lua_State& s) -> bool; + + /* Reapplies all active, dirty bindings associated with this Property. */ + auto reapplyAll() -> void; + + auto addLuaBinding(lua_State*, int ref) -> void; + auto applySingle(lua_State*, int ref, bool mark_dirty) -> bool; private: std::unique_ptr value_; @@ -51,6 +67,28 @@ class Property { std::pmr::vector> bindings_; }; +/* + * Container for a Lua function that should be invoked whenever a Property's + * value changes, as well as some extra accounting metadata. + */ +struct Binding { + /* Checks the value at idx is a Binding, returning a pointer to it if so. */ + static auto get(lua_State*, int idx) -> Binding*; + /* + * If the value at idx is a dirty, active Binding, applies the current value + * from its Property. Returns false if the binding was active and dirty, but + * invoking the Lua callback failed. + */ + static auto apply(lua_State*, int idx) -> bool; + + Property* property; + bool active; + bool dirty; +}; + +static_assert(std::is_trivially_destructible()); +static_assert(std::is_trivially_copy_assignable()); + class PropertyBindings { public: PropertyBindings(); diff --git a/src/lua/property.cpp b/src/lua/property.cpp index e136ad90..634a6a26 100644 --- a/src/lua/property.cpp +++ b/src/lua/property.cpp @@ -29,9 +29,28 @@ namespace lua { static const char kPropertyMetatable[] = "property"; static const char kFunctionMetatable[] = "c_func"; +static const char kBindingMetatable[] = "binding"; static const char kBindingsTable[] = "bindings"; static const char kBinderKey[] = "binder"; +auto Binding::get(lua_State* L, int idx) -> Binding* { + return reinterpret_cast(luaL_testudata(L, idx, kBindingMetatable)); +} + +auto Binding::apply(lua_State* L, int idx) -> bool { + Binding* b = get(L, idx); + if (b->dirty && b->active) { + b->dirty = false; + // The binding needs to be reapplied. Push the Lua callback, then its arg. + lua_getiuservalue(L, idx, 1); + b->property->pushValue(*L); + + // Invoke the callback. + return CallProtected(L, 1, 0) == LUA_OK; + } + return true; +} + static auto check_property(lua_State* state) -> Property* { void* data = luaL_checkudata(state, 1, kPropertyMetatable); luaL_argcheck(state, data != NULL, 1, "`property` expected"); @@ -40,14 +59,14 @@ static auto check_property(lua_State* state) -> Property* { static auto property_get(lua_State* state) -> int { Property* p = check_property(state); - p->PushValue(*state); + p->pushValue(*state); return 1; } static auto property_set(lua_State* state) -> int { Property* p = check_property(state); - luaL_argcheck(state, p->IsTwoWay(), 1, "property is read-only"); - bool valid = p->PopValue(*state); + luaL_argcheck(state, p->isTwoWay(), 1, "property is read-only"); + bool valid = p->popValue(*state); lua_pushboolean(state, valid); return 1; } @@ -56,35 +75,40 @@ static auto property_bind(lua_State* state) -> int { Property* p = check_property(state); luaL_checktype(state, 2, LUA_TFUNCTION); - // Copy the function, as we need to invoke it then store our reference. - lua_pushvalue(state, 2); - // ...and another copy, since we return the original closure. - lua_pushvalue(state, 2); - - p->PushValue(*state); - CallProtected(state, 1, 0); // Invoke the initial binding. - + // Fetch the table of live bindings. lua_pushstring(state, kBindingsTable); lua_gettable(state, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] - lua_insert(state, -2); // Move bindings to the bottom, with fn above. - int ref = luaL_ref(state, -2); // bindings[ref] = fn - p->AddLuaBinding(state, ref); + // Create the userdata holding the new binding's metadata. + Binding* binding = + reinterpret_cast(lua_newuserdatauv(state, sizeof(Binding), 1)); + *binding = Binding{.property = p, .active = true, .dirty = true}; + luaL_setmetatable(state, kBindingMetatable); + + // Associate the callback function with the new binding. + lua_pushvalue(state, 2); + lua_setiuservalue(state, -2, 1); + + // Put a reference to the binding into the bindings table, so that we can + // look it up later. + lua_pushvalue(state, -1); + int binding_ref = luaL_ref(state, 3); - // Pop the bindings table, leaving one of the copies of the callback fn at - // the top of the stack. - lua_pop(state, 1); + // Tell the property about the new binding. This was also perform the initial + // bind. + p->addLuaBinding(state, binding_ref); + // Return the only remaining strong reference to the new Binding. return 1; } static auto property_tostring(lua_State* state) -> int { Property* p = check_property(state); - p->PushValue(*state); + p->pushValue(*state); std::stringstream str{}; str << "property { " << luaL_tolstring(state, -1, NULL); - if (!p->IsTwoWay()) { + if (!p->isTwoWay()) { str << ", read-only"; } str << " }"; @@ -140,6 +164,11 @@ auto PropertyBindings::install(lua_State* L) -> void { // We've finished setting up the metatable, so pop it. lua_pop(L, 1); + // Create the metatable responsible for each Binding. This metatable is empty + // as it's only used for identification. + luaL_newmetatable(L, kBindingMetatable); + lua_pop(L, 1); + // Create a weak table in the registry to hold live bindings. lua_pushstring(L, kBindingsTable); lua_newtable(L); // bindings = {} @@ -198,6 +227,19 @@ Property::Property(const LuaValue& val, cb_(cb), bindings_(&memory::kSpiRamResource) {} +auto Property::setDirect(const LuaValue& val) -> void { + *value_ = val; + reapplyAll(); +} + +auto Property::set(const LuaValue& val) -> bool { + if (cb_ && !std::invoke(*cb_, val)) { + return false; + } + setDirect(val); + return true; +} + static auto pushTagValue(lua_State* L, const database::TagValue& val) -> void { std::visit( [&](auto&& arg) { @@ -276,7 +318,7 @@ static void pushDevice(lua_State* L, const drivers::bluetooth::Device& dev) { lua_rawset(L, -3); } -auto Property::PushValue(lua_State& s) -> int { +auto Property::pushValue(lua_State& s) -> int { std::visit( [&](auto&& arg) { using T = std::decay_t; @@ -336,7 +378,7 @@ auto popRichType(lua_State* L) -> LuaValue { return std::monostate{}; } -auto Property::PopValue(lua_State& s) -> bool { +auto Property::popValue(lua_State& s) -> bool { LuaValue new_val; switch (lua_type(&s, 2)) { case LUA_TNIL: @@ -366,40 +408,53 @@ auto Property::PopValue(lua_State& s) -> bool { } } - if (cb_ && std::invoke(*cb_, new_val)) { - Update(new_val); - return true; - } - return false; + return set(new_val); } -auto Property::Update(const LuaValue& v) -> void { - *value_ = v; - +auto Property::reapplyAll() -> void { for (int i = bindings_.size() - 1; i >= 0; i--) { auto& b = bindings_[i]; + if (!applySingle(b.first, b.second, true)) { + // Remove the binding if we weren't able to apply it. This is usually due + // to the binding getting GC'd. + bindings_.erase(bindings_.begin() + i); + } + } +} - int top = lua_gettop(b.first); +auto Property::applySingle(lua_State* L, int ref, bool mark_dirty) -> bool { + int top = lua_gettop(L); - lua_pushstring(b.first, kBindingsTable); - lua_gettable(b.first, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] - int type = lua_rawgeti(b.first, -1, b.second); // push bindings[i] + // Push the table of bindings. + lua_pushstring(L, kBindingsTable); + lua_gettable(L, LUA_REGISTRYINDEX); - // Has closure has been GCed? - if (type == LUA_TNIL) { - // Remove the binding. - bindings_.erase(bindings_.begin() + i); - } else { - PushValue(*b.first); // push the argument - CallProtected(b.first, 1, 0); // invoke the closure - } + // Resolve the reference. + int type = lua_rawgeti(L, -1, ref); + if (type == LUA_TNIL) { + lua_settop(L, top); + return false; + } + + // Defensively check that the ref was actually for a Binding. + Binding* b = Binding::get(L, -1); + if (!b) { + lua_settop(L, top); + return false; + } - lua_settop(b.first, top); // clean up after ourselves + if (mark_dirty) { + b->dirty = true; } + + bool ret = Binding::apply(L, -1); + lua_settop(L, top); + return ret; } -auto Property::AddLuaBinding(lua_State* state, int ref) -> void { +auto Property::addLuaBinding(lua_State* state, int ref) -> void { bindings_.push_back({state, ref}); + applySingle(state, ref, true); } } // namespace lua diff --git a/src/ui/include/screen_lua.hpp b/src/ui/include/screen_lua.hpp index 41d97a1e..8a463bad 100644 --- a/src/ui/include/screen_lua.hpp +++ b/src/ui/include/screen_lua.hpp @@ -8,6 +8,7 @@ #include "lua.hpp" +#include "property.hpp" #include "screen.hpp" namespace ui { @@ -26,6 +27,11 @@ class Lua : public Screen { auto SetObjRef(lua_State*) -> void; private: + /* Invokes a method on this screen's Lua counterpart. */ + auto callMethod(std::string name) -> void; + /* Applies fn to each binding in this screen's `bindings` field. */ + auto forEachBinding(std::function fn) -> void; + lua_State* s_; std::optional obj_ref_; }; diff --git a/src/ui/screen_lua.cpp b/src/ui/screen_lua.cpp index d43c7ee7..685e43cb 100644 --- a/src/ui/screen_lua.cpp +++ b/src/ui/screen_lua.cpp @@ -9,6 +9,7 @@ #include "core/lv_obj_tree.h" #include "lua.h" #include "lua.hpp" +#include "property.hpp" #include "themes.hpp" #include "lua_thread.hpp" @@ -28,28 +29,46 @@ Lua::~Lua() { } auto Lua::onShown() -> void { + callMethod("onShown"); + forEachBinding([&](lua::Binding* b) { b->active = true; }); +} + +auto Lua::onHidden() -> void { + callMethod("onHidden"); + forEachBinding([&](lua::Binding* b) { b->active = false; }); +} + +auto Lua::canPop() -> bool { if (!s_ || !obj_ref_) { - return; + return true; } lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_); - lua_pushliteral(s_, "onShown"); + lua_pushliteral(s_, "canPop"); if (lua_gettable(s_, -2) == LUA_TFUNCTION) { + // If we got a callback instead of a value, then invoke it to turn it into + // value. lua_pushvalue(s_, -2); - lua::CallProtected(s_, 1, 0); - } else { - lua_pop(s_, 1); + lua::CallProtected(s_, 1, 1); } + bool ret = lua_toboolean(s_, -1); - lua_pop(s_, 1); + lua_pop(s_, 2); + return ret; } -auto Lua::onHidden() -> void { +auto Lua::SetObjRef(lua_State* s) -> void { + assert(s_ == nullptr); + s_ = s; + obj_ref_ = luaL_ref(s, LUA_REGISTRYINDEX); +} + +auto Lua::callMethod(std::string name) -> void { if (!s_ || !obj_ref_) { return; } lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_); - lua_pushliteral(s_, "onHidden"); + lua_pushlstring(s_, name.data(), name.size()); if (lua_gettable(s_, -2) == LUA_TFUNCTION) { lua_pushvalue(s_, -2); @@ -61,29 +80,28 @@ auto Lua::onHidden() -> void { lua_pop(s_, 1); } -auto Lua::canPop() -> bool { +auto Lua::forEachBinding(std::function fn) -> void { if (!s_ || !obj_ref_) { - return true; + return; } lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_); - lua_pushliteral(s_, "canPop"); + lua_pushliteral(s_, "bindings"); - if (lua_gettable(s_, -2) == LUA_TFUNCTION) { - // If we got a callback instead of a value, then invoke it to turn it into - // value. - lua_pushvalue(s_, -2); - lua::CallProtected(s_, 1, 1); + if (lua_gettable(s_, -2) != LUA_TTABLE) { + lua_pop(s_, 2); + return; } - bool ret = lua_toboolean(s_, -1); - lua_pop(s_, 2); - return ret; -} + lua_pushnil(s_); + while (lua_next(s_, -2) != 0) { + lua::Binding* b = lua::Binding::get(s_, -1); + if (b) { + std::invoke(fn, b); + } + lua_pop(s_, 1); + } -auto Lua::SetObjRef(lua_State* s) -> void { - assert(s_ == nullptr); - s_ = s; - obj_ref_ = luaL_ref(s, LUA_REGISTRYINDEX); + lua_pop(s_, 2); } } // namespace screens diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 1c296ac7..1305e764 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -132,13 +132,13 @@ lua::Property UiState::sPlaybackPlaying{ lua::Property UiState::sPlaybackTrack{}; lua::Property UiState::sPlaybackPosition{ 0, [](const lua::LuaValue& val) { - int current_val = std::get(sPlaybackPosition.Get()); + int current_val = std::get(sPlaybackPosition.get()); if (!std::holds_alternative(val)) { return false; } int new_val = std::get(val); if (current_val != new_val) { - auto track = sPlaybackTrack.Get(); + auto track = sPlaybackTrack.get(); if (!std::holds_alternative(track)) { return false; } @@ -320,20 +320,20 @@ int UiState::PopScreen() { void UiState::react(const system_fsm::KeyLockChanged& ev) { sDisplay->SetDisplayOn(!ev.locking); sInput->lock(ev.locking); - sLockSwitch.Update(ev.locking); + sLockSwitch.setDirect(ev.locking); } void UiState::react(const system_fsm::SamdUsbStatusChanged& ev) { - sUsbMassStorageBusy.Update(ev.new_status == - drivers::Samd::UsbStatus::kAttachedBusy); + sUsbMassStorageBusy.setDirect(ev.new_status == + drivers::Samd::UsbStatus::kAttachedBusy); } void UiState::react(const database::event::UpdateStarted&) { - sDatabaseUpdating.Update(true); + sDatabaseUpdating.setDirect(true); } void UiState::react(const database::event::UpdateFinished&) { - sDatabaseUpdating.Update(false); + sDatabaseUpdating.setDirect(false); } void UiState::react(const internal::DismissAlerts&) { @@ -341,46 +341,46 @@ void UiState::react(const internal::DismissAlerts&) { } void UiState::react(const system_fsm::BatteryStateChanged& ev) { - sBatteryPct.Update(static_cast(ev.new_state.percent)); - sBatteryMv.Update(static_cast(ev.new_state.millivolts)); - sBatteryCharging.Update(ev.new_state.is_charging); + sBatteryPct.setDirect(static_cast(ev.new_state.percent)); + sBatteryMv.setDirect(static_cast(ev.new_state.millivolts)); + sBatteryCharging.setDirect(ev.new_state.is_charging); } void UiState::react(const audio::QueueUpdate&) { auto& queue = sServices->track_queue(); - sQueueSize.Update(static_cast(queue.totalSize())); + sQueueSize.setDirect(static_cast(queue.totalSize())); int current_pos = queue.currentPosition(); if (queue.current()) { current_pos++; } - sQueuePosition.Update(current_pos); - sQueueRandom.Update(queue.random()); - sQueueRepeat.Update(queue.repeat()); - sQueueReplay.Update(queue.replay()); + sQueuePosition.setDirect(current_pos); + sQueueRandom.setDirect(queue.random()); + sQueueRepeat.setDirect(queue.repeat()); + sQueueReplay.setDirect(queue.replay()); } void UiState::react(const audio::PlaybackUpdate& ev) { if (ev.current_track) { - sPlaybackTrack.Update(*ev.current_track); + sPlaybackTrack.setDirect(*ev.current_track); } else { - sPlaybackTrack.Update(std::monostate{}); + sPlaybackTrack.setDirect(std::monostate{}); } - sPlaybackPlaying.Update(!ev.paused); - sPlaybackPosition.Update(static_cast(ev.track_position.value_or(0))); + sPlaybackPlaying.setDirect(!ev.paused); + sPlaybackPosition.setDirect(static_cast(ev.track_position.value_or(0))); } void UiState::react(const audio::VolumeChanged& ev) { - sVolumeCurrentPct.Update(static_cast(ev.percent)); - sVolumeCurrentDb.Update(static_cast(ev.db)); + sVolumeCurrentPct.setDirect(static_cast(ev.percent)); + sVolumeCurrentDb.setDirect(static_cast(ev.db)); } void UiState::react(const audio::VolumeBalanceChanged& ev) { - sVolumeLeftBias.Update(ev.left_bias); + sVolumeLeftBias.setDirect(ev.left_bias); } void UiState::react(const audio::VolumeLimitChanged& ev) { - sVolumeLimit.Update(ev.new_limit_db); + sVolumeLimit.setDirect(ev.new_limit_db); } void UiState::react(const system_fsm::BluetoothEvent& ev) { @@ -388,19 +388,19 @@ void UiState::react(const system_fsm::BluetoothEvent& ev) { auto dev = bt.ConnectedDevice(); switch (ev.event) { case drivers::bluetooth::Event::kKnownDevicesChanged: - sBluetoothDevices.Update(bt.KnownDevices()); + sBluetoothDevices.setDirect(bt.KnownDevices()); break; case drivers::bluetooth::Event::kConnectionStateChanged: - sBluetoothConnected.Update(bt.IsConnected()); + sBluetoothConnected.setDirect(bt.IsConnected()); if (dev) { - sBluetoothPairedDevice.Update(drivers::bluetooth::Device{ + sBluetoothPairedDevice.setDirect(drivers::bluetooth::Device{ .address = dev->mac, .name = {dev->name.data(), dev->name.size()}, .class_of_device = 0, .signal_strength = 0, }); } else { - sBluetoothPairedDevice.Update(std::monostate{}); + sBluetoothPairedDevice.setDirect(std::monostate{}); } break; case drivers::bluetooth::Event::kPreferredDeviceChanged: @@ -428,7 +428,7 @@ void Splash::react(const system_fsm::BootComplete& ev) { themes::Theme::instance()->Apply(); int brightness = sServices->nvs().ScreenBrightness(); - sDisplayBrightness.Update(brightness); + sDisplayBrightness.setDirect(brightness); sDisplay->SetBrightness(brightness); sDeviceFactory = std::make_unique(sServices); @@ -530,12 +530,12 @@ void Lua::entry() { {"msc_busy", &sUsbMassStorageBusy}, }); - sDatabaseAutoUpdate.Update(sServices->nvs().DbAutoIndex()); + sDatabaseAutoUpdate.setDirect(sServices->nvs().DbAutoIndex()); auto bt = sServices->bluetooth(); - sBluetoothEnabled.Update(bt.IsEnabled()); - sBluetoothConnected.Update(bt.IsConnected()); - sBluetoothDevices.Update(bt.KnownDevices()); + sBluetoothEnabled.setDirect(bt.IsEnabled()); + sBluetoothConnected.setDirect(bt.IsConnected()); + sBluetoothDevices.setDirect(bt.KnownDevices()); sCurrentScreen.reset(); sLua->RunScript("/sdcard/config.lua");