Before Width: | Height: | Size: 623 B After Width: | Height: | Size: 623 B |
Before Width: | Height: | Size: 617 B After Width: | Height: | Size: 617 B |
Before Width: | Height: | Size: 617 B After Width: | Height: | Size: 617 B |
Before Width: | Height: | Size: 618 B After Width: | Height: | Size: 618 B |
Before Width: | Height: | Size: 622 B After Width: | Height: | Size: 622 B |
Before Width: | Height: | Size: 614 B After Width: | Height: | Size: 614 B |
Before Width: | Height: | Size: 618 B After Width: | Height: | Size: 618 B |
After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 654 B After Width: | Height: | Size: 654 B |
Before Width: | Height: | Size: 608 B After Width: | Height: | Size: 608 B |
Before Width: | Height: | Size: 616 B After Width: | Height: | Size: 616 B |
@ -0,0 +1,37 @@ |
|||||||
|
local lvgl = require("lvgl") |
||||||
|
|
||||||
|
local backstack = { |
||||||
|
root = lvgl.Object(nil, { |
||||||
|
w = lvgl.HOR_RES(), |
||||||
|
h = lvgl.VER_RES(), |
||||||
|
}), |
||||||
|
stack = {}, |
||||||
|
} |
||||||
|
|
||||||
|
function backstack:Top() |
||||||
|
return self.stack[#self.stack] |
||||||
|
end |
||||||
|
|
||||||
|
function backstack:SetTopParent(parent) |
||||||
|
local top = self:Top() |
||||||
|
if top and top.root then |
||||||
|
top.root:set_parent(parent) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
function backstack:Push(screen) |
||||||
|
self:SetTopParent(nil) |
||||||
|
table.insert(self.stack, screen) |
||||||
|
self:SetTopParent(self.root) |
||||||
|
end |
||||||
|
|
||||||
|
function backstack:Pop(num) |
||||||
|
num = num or 1 |
||||||
|
for _ = 1, num do |
||||||
|
local removed = table.remove(self.stack) |
||||||
|
removed.root:delete() |
||||||
|
end |
||||||
|
self:SetTopParent(self.root) |
||||||
|
end |
||||||
|
|
||||||
|
return backstack |
@ -1 +1,3 @@ |
|||||||
require("main_menu"):Create() |
local backstack = require("backstack") |
||||||
|
local main_menu = require("main_menu"):Create(backstack.root) |
||||||
|
backstack:Push(main_menu) |
||||||
|
@ -1,37 +1,96 @@ |
|||||||
local lvgl = require("lvgl") |
local lvgl = require("lvgl") |
||||||
|
local power = require("power") |
||||||
|
local bluetooth = require("bluetooth") |
||||||
|
local playback = require("playback") |
||||||
|
|
||||||
local widgets = {} |
local widgets = {} |
||||||
|
|
||||||
function widgets.StatusBar(parent) |
function widgets.StatusBar(parent, opts) |
||||||
local container = parent:Object { |
local status_bar = {} |
||||||
|
|
||||||
|
status_bar.root = parent:Object { |
||||||
flex = { |
flex = { |
||||||
flex_direction = "row", |
flex_direction = "row", |
||||||
justify_content = "flex-start", |
justify_content = "flex-start", |
||||||
align_items = "center", |
align_items = "center", |
||||||
align_content = "center", |
align_content = "center", |
||||||
}, |
}, |
||||||
w = lvgl.HOR_RES(), |
w = lvgl.HOR_RES(), |
||||||
h = 16, |
h = 18, |
||||||
} |
} |
||||||
|
|
||||||
container:Label { |
if opts.back_cb then |
||||||
w = lvgl.SIZE_CONTENT, |
status_bar.back = status_bar.root:Label { |
||||||
h = 12, |
w = lvgl.SIZE_CONTENT, |
||||||
text = "<", |
h = 12, |
||||||
} |
text = "<", |
||||||
|
} |
||||||
|
status_bar.back:onClicked(opts.back_cb) |
||||||
|
end |
||||||
|
|
||||||
container:Label { |
status_bar.title = status_bar.root:Label { |
||||||
w = lvgl.PCT(100), |
w = lvgl.PCT(100), |
||||||
h = 16, |
h = 16, |
||||||
text = "cool title", |
text = "", |
||||||
flex_grow = 1, |
flex_grow = 1, |
||||||
} |
} |
||||||
|
if opts.title then |
||||||
|
status_bar.title.set { text = opts.title } |
||||||
|
end |
||||||
|
|
||||||
container:Label { |
status_bar.playing = status_bar.root:Image {} |
||||||
w = lvgl.SIZE_CONTENT, |
status_bar.bluetooth = status_bar.root:Image {} |
||||||
h = 16, |
status_bar.battery = status_bar.root:Image {} |
||||||
text = "69%", |
|
||||||
|
status_bar.bindings = { |
||||||
|
power.battery_pct:bind(function(percent) |
||||||
|
local src |
||||||
|
if percent >= 95 then |
||||||
|
src = "battery_full.png" |
||||||
|
elseif percent >= 75 then |
||||||
|
src = "battery_80.png" |
||||||
|
elseif percent >= 55 then |
||||||
|
src = "battery_60.png" |
||||||
|
elseif percent >= 35 then |
||||||
|
src = "battery_40.png" |
||||||
|
elseif percent >= 15 then |
||||||
|
src = "battery_20.png" |
||||||
|
else |
||||||
|
src = "battery_empty.png" |
||||||
|
end |
||||||
|
status_bar.battery:set_src("//lua/assets/" .. src) |
||||||
|
end), |
||||||
|
playback.playing:bind(function(playing) |
||||||
|
if playing then |
||||||
|
status_bar.playing:set_src("//lua/assets/play.png") |
||||||
|
else |
||||||
|
status_bar.playing:set_src("//lua/assets/pause.png") |
||||||
|
end |
||||||
|
end), |
||||||
|
playback.track:bind(function(track) |
||||||
|
if track then |
||||||
|
status_bar.playing:clear_flag(lvgl.FLAG.HIDDEN) |
||||||
|
else |
||||||
|
status_bar.playing:add_flag(lvgl.FLAG.HIDDEN) |
||||||
|
end |
||||||
|
end), |
||||||
|
bluetooth.enabled:bind(function(en) |
||||||
|
if en then |
||||||
|
status_bar.bluetooth:clear_flag(lvgl.FLAG.HIDDEN) |
||||||
|
else |
||||||
|
status_bar.bluetooth:add_flag(lvgl.FLAG.HIDDEN) |
||||||
|
end |
||||||
|
end), |
||||||
|
bluetooth.connected:bind(function(connected) |
||||||
|
if connected then |
||||||
|
status_bar.bluetooth:set_src("//lua/assets/bt_conn.png") |
||||||
|
else |
||||||
|
status_bar.bluetooth:set_src("//lua/assets/bt.png") |
||||||
|
end |
||||||
|
end), |
||||||
} |
} |
||||||
|
|
||||||
|
return status_bar |
||||||
end |
end |
||||||
|
|
||||||
return widgets |
return widgets |
||||||
|
@ -0,0 +1,47 @@ |
|||||||
|
/*
|
||||||
|
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: GPL-3.0-only |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
|
||||||
|
#include "lua.hpp" |
||||||
|
#include "lvgl.h" |
||||||
|
#include "service_locator.hpp" |
||||||
|
|
||||||
|
namespace lua { |
||||||
|
|
||||||
|
using LuaValue = std::variant<std::monostate, int, float, bool, std::string>; |
||||||
|
|
||||||
|
class Property { |
||||||
|
public: |
||||||
|
Property() : Property(std::monostate{}) {} |
||||||
|
Property(const LuaValue&); |
||||||
|
Property(const LuaValue&, std::function<bool(const LuaValue&)>); |
||||||
|
|
||||||
|
auto IsTwoWay() -> bool { return cb_.has_value(); } |
||||||
|
|
||||||
|
auto PushValue(lua_State& s) -> int; |
||||||
|
auto PopValue(lua_State& s) -> bool; |
||||||
|
auto Update(const LuaValue& new_val) -> void; |
||||||
|
|
||||||
|
auto AddLuaBinding(lua_State*, int ref) -> void; |
||||||
|
|
||||||
|
private: |
||||||
|
LuaValue value_; |
||||||
|
std::optional<std::function<bool(const LuaValue&)>> cb_; |
||||||
|
std::vector<std::pair<lua_State*, int>> bindings_; |
||||||
|
}; |
||||||
|
|
||||||
|
class PropertyBindings { |
||||||
|
public: |
||||||
|
PropertyBindings(lua_State&); |
||||||
|
|
||||||
|
auto Register(lua_State*, Property*) -> void; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace lua
|
@ -0,0 +1,196 @@ |
|||||||
|
/*
|
||||||
|
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: GPL-3.0-only |
||||||
|
*/ |
||||||
|
|
||||||
|
#include "property.hpp" |
||||||
|
|
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
|
||||||
|
#include "lua.h" |
||||||
|
#include "lua.hpp" |
||||||
|
#include "lvgl.h" |
||||||
|
#include "service_locator.hpp" |
||||||
|
|
||||||
|
namespace lua { |
||||||
|
|
||||||
|
static const char kMetatableName[] = "property"; |
||||||
|
static const char kBindingsTable[] = "bindings"; |
||||||
|
|
||||||
|
static auto check_property(lua_State* state) -> Property* { |
||||||
|
void* data = luaL_checkudata(state, 1, kMetatableName); |
||||||
|
luaL_argcheck(state, data != NULL, 1, "`property` expected"); |
||||||
|
return *reinterpret_cast<Property**>(data); |
||||||
|
} |
||||||
|
|
||||||
|
static auto property_get(lua_State* state) -> int { |
||||||
|
Property* p = check_property(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); |
||||||
|
lua_pushboolean(state, valid); |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
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); |
||||||
|
|
||||||
|
// FIXME: This should ideally be lua_pcall, for safety.
|
||||||
|
p->PushValue(*state); |
||||||
|
lua_call(state, 1, 0); // Invoke the initial binding.
|
||||||
|
|
||||||
|
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); |
||||||
|
|
||||||
|
// Pop the bindings table, leaving one of the copiesw of the callback fn at
|
||||||
|
// the top of the stack.
|
||||||
|
lua_pop(state, 1); |
||||||
|
|
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
static const struct luaL_Reg kPropertyBindingFuncs[] = {{"get", property_get}, |
||||||
|
{"set", property_set}, |
||||||
|
{"bind", property_bind}, |
||||||
|
{NULL, NULL}}; |
||||||
|
|
||||||
|
PropertyBindings::PropertyBindings(lua_State& s) { |
||||||
|
// Create the metatable responsible for the Property API.
|
||||||
|
luaL_newmetatable(&s, kMetatableName); |
||||||
|
|
||||||
|
lua_pushliteral(&s, "__index"); |
||||||
|
lua_pushvalue(&s, -2); |
||||||
|
lua_settable(&s, -3); // metatable.__index = metatable
|
||||||
|
|
||||||
|
// Add our binding funcs (get, set, bind) to the metatable.
|
||||||
|
luaL_setfuncs(&s, kPropertyBindingFuncs, 0); |
||||||
|
|
||||||
|
// Create a weak table in the registry to hold live bindings.
|
||||||
|
lua_pushstring(&s, kBindingsTable); |
||||||
|
lua_newtable(&s); // bindings = {}
|
||||||
|
|
||||||
|
// Metatable for the weak table. Values are weak.
|
||||||
|
lua_newtable(&s); // meta = {}
|
||||||
|
lua_pushliteral(&s, "__mode"); |
||||||
|
lua_pushliteral(&s, "v"); |
||||||
|
lua_settable(&s, -3); // meta.__mode='v'
|
||||||
|
lua_setmetatable(&s, -2); // setmetatable(bindings, meta)
|
||||||
|
|
||||||
|
lua_settable(&s, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] = bindings
|
||||||
|
} |
||||||
|
|
||||||
|
auto PropertyBindings::Register(lua_State* s, Property* prop) -> void { |
||||||
|
Property** data = |
||||||
|
reinterpret_cast<Property**>(lua_newuserdata(s, sizeof(Property*))); |
||||||
|
*data = prop; |
||||||
|
|
||||||
|
luaL_setmetatable(s, kMetatableName); |
||||||
|
} |
||||||
|
|
||||||
|
template <class... Ts> |
||||||
|
inline constexpr bool always_false_v = false; |
||||||
|
|
||||||
|
Property::Property(const LuaValue& val) : value_(val), cb_() {} |
||||||
|
|
||||||
|
Property::Property(const LuaValue& val, |
||||||
|
std::function<bool(const LuaValue& val)> cb) |
||||||
|
: value_(val), cb_(cb) {} |
||||||
|
|
||||||
|
auto Property::PushValue(lua_State& s) -> int { |
||||||
|
std::visit( |
||||||
|
[&](auto&& arg) { |
||||||
|
using T = std::decay_t<decltype(arg)>; |
||||||
|
if constexpr (std::is_same_v<T, std::monostate>) { |
||||||
|
lua_pushnil(&s); |
||||||
|
} else if constexpr (std::is_same_v<T, int>) { |
||||||
|
lua_pushinteger(&s, arg); |
||||||
|
} else if constexpr (std::is_same_v<T, float>) { |
||||||
|
lua_pushnumber(&s, arg); |
||||||
|
} else if constexpr (std::is_same_v<T, bool>) { |
||||||
|
lua_pushboolean(&s, arg); |
||||||
|
} else if constexpr (std::is_same_v<T, std::string>) { |
||||||
|
lua_pushstring(&s, arg.c_str()); |
||||||
|
} else { |
||||||
|
static_assert(always_false_v<T>, "PushValue missing type"); |
||||||
|
} |
||||||
|
}, |
||||||
|
value_); |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
auto Property::PopValue(lua_State& s) -> bool { |
||||||
|
LuaValue new_val; |
||||||
|
switch (lua_type(&s, 2)) { |
||||||
|
case LUA_TNIL: |
||||||
|
new_val = std::monostate{}; |
||||||
|
break; |
||||||
|
case LUA_TNUMBER: |
||||||
|
if (lua_isinteger(&s, 2)) { |
||||||
|
new_val = lua_tointeger(&s, 2); |
||||||
|
} else { |
||||||
|
new_val = lua_tonumber(&s, 2); |
||||||
|
} |
||||||
|
break; |
||||||
|
case LUA_TBOOLEAN: |
||||||
|
new_val = lua_toboolean(&s, 2); |
||||||
|
break; |
||||||
|
case LUA_TSTRING: |
||||||
|
new_val = lua_tostring(&s, 2); |
||||||
|
break; |
||||||
|
default: |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (cb_ && std::invoke(*cb_, new_val)) { |
||||||
|
Update(new_val); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
auto Property::Update(const LuaValue& v) -> void { |
||||||
|
value_ = v; |
||||||
|
|
||||||
|
for (int i = bindings_.size() - 1; i >= 0; i--) { |
||||||
|
auto& b = bindings_[i]; |
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
// Has closure has been GCed?
|
||||||
|
if (type == LUA_TNIL) { |
||||||
|
// Clean up after ourselves.
|
||||||
|
lua_pop(b.first, 1); |
||||||
|
// Remove the binding.
|
||||||
|
bindings_.erase(bindings_.begin() + i); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
PushValue(*b.first); // push the argument
|
||||||
|
lua_call(b.first, 1, 0); // invoke the closure
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
auto Property::AddLuaBinding(lua_State* state, int ref) -> void { |
||||||
|
bindings_.push_back({state, ref}); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace lua
|