use prototype inheritance for lua screens, rather than functions

this gives us a way to give each screen nice little hooks, like
'onShown' and 'onHidden'. later we can use these hooks to disable
bindings for screens that aren't in-use.
custom
jacqueline 1 year ago
parent 53c4ea7805
commit ef72b25660
  1. 210
      lua/browser.lua
  2. 29
      lua/licenses.lua
  3. 61
      lua/main_menu.lua
  4. 439
      lua/playing.lua
  5. 597
      lua/settings.lua
  6. 1
      src/lua/CMakeLists.txt
  7. 2
      src/lua/bridge.cpp
  8. 15
      src/lua/include/lua_screen.hpp
  9. 75
      src/lua/lua_screen.cpp
  10. 3
      src/ui/include/screen.hpp
  11. 3
      src/ui/include/screen_lua.hpp
  12. 36
      src/ui/screen_lua.cpp
  13. 58
      src/ui/ui_fsm.cpp

@ -6,30 +6,11 @@ local queue = require("queue")
local playing = require("playing")
local theme = require("theme")
local playback = require("playback")
local screen = require("screen")
local browser = {}
function browser.create(opts)
local screen = {}
screen.root = lvgl.Object(nil, {
flex = {
flex_direction = "column",
flex_wrap = "wrap",
justify_content = "flex-start",
align_items = "flex-start",
align_content = "flex-start",
},
w = lvgl.HOR_RES(),
h = lvgl.VER_RES(),
})
screen.root:center()
screen.status_bar = widgets.StatusBar(screen.root, {
title = opts.title,
})
if opts.breadcrumb then
local header = screen.root:Object {
return screen:new {
createUi = function(self)
self.root = lvgl.Object(nil, {
flex = {
flex_direction = "column",
flex_wrap = "wrap",
@ -38,100 +19,113 @@ function browser.create(opts)
align_content = "flex-start",
},
w = lvgl.HOR_RES(),
h = lvgl.SIZE_CONTENT,
pad_left = 4,
pad_right = 4,
pad_bottom = 2,
bg_opa = lvgl.OPA(100),
bg_color = "#fafafa",
scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF,
}
header:Label {
text = opts.breadcrumb,
text_font = font.fusion_10,
}
h = lvgl.VER_RES(),
})
self.root:center()
local buttons = header:Object({
flex = {
flex_direction = "row",
flex_wrap = "wrap",
justify_content = "flex-end",
align_items = "center",
align_content = "center",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
pad_column = 4,
self.status_bar = widgets.StatusBar(self.root, {
title = self.title,
})
local original_iterator = opts.iterator:clone()
local enqueue = widgets.IconBtn(buttons, "//lua/img/enqueue.png", "Enqueue")
enqueue:onClicked(function()
queue.add(original_iterator)
playback.playing:set(true)
end)
-- enqueue:add_flag(lvgl.FLAG.HIDDEN)
local play = widgets.IconBtn(buttons, "//lua/img/play_small.png", "Play")
play:onClicked(function()
queue.clear()
queue.add(original_iterator)
playback.playing:set(true)
backstack.push(playing)
end
)
end
screen.list = lvgl.List(screen.root, {
w = lvgl.PCT(100),
h = lvgl.PCT(100),
flex_grow = 1,
scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF,
})
if self.breadcrumb then
local header = self.root:Object {
flex = {
flex_direction = "column",
flex_wrap = "wrap",
justify_content = "flex-start",
align_items = "flex-start",
align_content = "flex-start",
},
w = lvgl.HOR_RES(),
h = lvgl.SIZE_CONTENT,
pad_left = 4,
pad_right = 4,
pad_bottom = 2,
bg_opa = lvgl.OPA(100),
bg_color = "#fafafa",
scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF,
}
local back = screen.list:add_btn(nil, "< Back")
back:onClicked(backstack.pop)
back:add_style(theme.list_item)
header:Label {
text = self.breadcrumb,
text_font = font.fusion_10,
}
screen.focused_item = 0
screen.last_item = 0
screen.add_item = function(item)
if not item then return end
screen.last_item = screen.last_item + 1
local this_item = screen.last_item
local btn = screen.list:add_btn(nil, tostring(item))
btn:onClicked(function()
local contents = item:contents()
if type(contents) == "userdata" then
backstack.push(function()
return browser.create({
title = opts.title,
iterator = contents,
breadcrumb = tostring(item),
})
end)
else
local buttons = header:Object({
flex = {
flex_direction = "row",
flex_wrap = "wrap",
justify_content = "flex-end",
align_items = "center",
align_content = "center",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
pad_column = 4,
})
local original_iterator = self.iterator:clone()
local enqueue = widgets.IconBtn(buttons, "//lua/img/enqueue.png", "Enqueue")
enqueue:onClicked(function()
queue.add(original_iterator)
playback.playing:set(true)
end)
-- enqueue:add_flag(lvgl.FLAG.HIDDEN)
local play = widgets.IconBtn(buttons, "//lua/img/play_small.png", "Play")
play:onClicked(function()
queue.clear()
queue.add(contents)
queue.add(original_iterator)
playback.playing:set(true)
backstack.push(playing)
end
end)
btn:onevent(lvgl.EVENT.FOCUSED, function()
screen.focused_item = this_item
if screen.last_item - 5 < this_item then
screen.add_item(opts.iterator())
end
end)
btn:add_style(theme.list_item)
end
)
end
for _ = 1, 8 do
local val = opts.iterator()
if not val then break end
screen.add_item(val)
end
self.list = lvgl.List(self.root, {
w = lvgl.PCT(100),
h = lvgl.PCT(100),
flex_grow = 1,
scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF,
})
return screen
end
local back = self.list:add_btn(nil, "< Back")
back:onClicked(backstack.pop)
back:add_style(theme.list_item)
return browser.create
self.focused_item = 0
self.last_item = 0
self.add_item = function(item)
if not item then return end
self.last_item = self.last_item + 1
local this_item = self.last_item
local btn = self.list:add_btn(nil, tostring(item))
btn:onClicked(function()
local contents = item:contents()
if type(contents) == "userdata" then
backstack.push(require("browser"):new {
title = self.title,
iterator = contents,
breadcrumb = tostring(item),
})
else
queue.clear()
queue.add(contents)
playback.playing:set(true)
backstack.push(playing:new())
end
end)
btn:onevent(lvgl.EVENT.FOCUSED, function()
self.focused_item = this_item
if self.last_item - 5 < this_item then
self.add_item(self.iterator())
end
end)
btn:add_style(theme.list_item)
end
for _ = 1, 8 do
local val = self.iterator()
if not val then break end
self.add_item(val)
end
end
}

@ -2,20 +2,23 @@ local backstack = require("backstack")
local widgets = require("widgets")
local font = require("font")
local theme = require("theme")
local screen = require("screen")
local function show_license(text)
backstack.push(function()
local screen = widgets.MenuScreen {
show_back = true,
title = "Licenses",
}
screen.root:Label {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
text_font = font.fusion_10,
text = text,
}
end)
backstack.push(screen:new {
createUi = function(self)
self.menu = widgets.MenuScreen {
show_back = true,
title = "Licenses",
}
self.menu.root:Label {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
text_font = font.fusion_10,
text = text,
}
end
})
end
local function gpl(copyright)
@ -175,4 +178,6 @@ return function()
library("tremor", "bsd", function()
xiphbsd("Copyright (c) 2002, Xiph.org Foundation")
end)
return menu
end

@ -5,41 +5,42 @@ local backstack = require("backstack")
local browser = require("browser")
local playing = require("playing")
local theme = require("theme")
local screen = require("screen")
return function()
local menu = widgets.MenuScreen({})
return screen:new {
createUi = function()
local menu = widgets.MenuScreen({})
menu.list = lvgl.List(menu.root, {
w = lvgl.PCT(100),
h = lvgl.PCT(100),
flex_grow = 1,
})
menu.list = lvgl.List(menu.root, {
w = lvgl.PCT(100),
h = lvgl.PCT(100),
flex_grow = 1,
})
local now_playing = menu.list:add_btn(nil, "Now Playing")
now_playing:onClicked(function()
backstack.push(playing)
end)
now_playing:add_style(theme.list_item)
local now_playing = menu.list:add_btn(nil, "Now Playing")
now_playing:onClicked(function()
backstack.push(playing:new())
end)
now_playing:add_style(theme.list_item)
local indexes = database.indexes()
for _, idx in ipairs(indexes) do
local btn = menu.list:add_btn(nil, tostring(idx))
btn:onClicked(function()
backstack.push(function()
return browser {
local indexes = database.indexes()
for _, idx in ipairs(indexes) do
local btn = menu.list:add_btn(nil, tostring(idx))
btn:onClicked(function()
backstack.push(browser:new {
title = tostring(idx),
iterator = idx:iter()
}
iterator = idx:iter(),
})
end)
end)
btn:add_style(theme.list_item)
end
btn:add_style(theme.list_item)
end
local settings = menu.list:add_btn(nil, "Settings")
settings:onClicked(function()
backstack.push(require("settings").root)
end)
settings:add_style(theme.list_item)
local settings = menu.list:add_btn(nil, "Settings")
settings:onClicked(function()
backstack.push(require("settings"):new())
end)
settings:add_style(theme.list_item)
return menu
end
return menu
end,
}

@ -4,6 +4,7 @@ local backstack = require("backstack")
local font = require("font")
local playback = require("playback")
local queue = require("queue")
local screen = require("screen")
local img = {
play = "//lua/img/play.png",
@ -18,219 +19,227 @@ local img = {
repeat_disabled = "//lua/img/repeat_disabled.png",
}
return function(opts)
local screen = {}
screen.root = lvgl.Object(nil, {
flex = {
flex_direction = "column",
flex_wrap = "wrap",
justify_content = "center",
align_items = "center",
align_content = "center",
},
w = lvgl.HOR_RES(),
h = lvgl.VER_RES(),
})
screen.root:center()
screen.status_bar = widgets.StatusBar(screen.root, {
back_cb = backstack.pop,
transparent_bg = true,
})
local info = screen.root:Object {
flex = {
flex_direction = "column",
flex_wrap = "wrap",
justify_content = "center",
align_items = "center",
align_content = "center",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
flex_grow = 1,
}
local artist = info:Label {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
text = "",
text_font = font.fusion_10,
text_align = 2,
}
local title = info:Label {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
text = "",
text_align = 2,
}
local playlist = screen.root:Object {
flex = {
flex_direction = "row",
justify_content = "center",
align_items = "center",
align_content = "center",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
}
playlist:Object({ w = 3, h = 1 }) -- spacer
local cur_time = playlist:Label {
w = lvgl.SIZE_CONTENT,
h = lvgl.SIZE_CONTENT,
text = "",
text_font = font.fusion_10,
}
playlist:Object({ flex_grow = 1, h = 1 }) -- spacer
local playlist_pos = playlist:Label {
text = "",
text_font = font.fusion_10,
}
playlist:Label {
text = "/",
text_font = font.fusion_10,
}
local playlist_total = playlist:Label {
text = "",
text_font = font.fusion_10,
}
playlist:Object({ flex_grow = 1, h = 1 }) -- spacer
local end_time = playlist:Label {
w = lvgl.SIZE_CONTENT,
h = lvgl.SIZE_CONTENT,
align = lvgl.ALIGN.RIGHT_MID,
text = "",
text_font = font.fusion_10,
}
playlist:Object({ w = 3, h = 1 }) -- spacer
local scrubber = screen.root:Slider {
w = lvgl.PCT(100),
h = 5,
range = { min = 0, max = 100 },
value = 0,
}
scrubber:onevent(lvgl.EVENT.RELEASED, function()
playback.position:set(scrubber:value())
end)
local controls = screen.root:Object {
flex = {
flex_direction = "row",
justify_content = "center",
align_items = "center",
align_content = "center",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
pad_column = 8,
pad_all = 2,
}
controls:Object({ flex_grow = 1, h = 1 }) -- spacer
local repeat_btn = controls:Button {}
repeat_btn:onClicked(function()
queue.repeat_track:set(not queue.repeat_track:get())
end)
local repeat_img = repeat_btn:Image { src = img.repeat_enabled }
local prev_btn = controls:Button {}
prev_btn:onClicked(queue.previous)
local prev_img = prev_btn:Image { src = img.prev_disabled }
local play_pause_btn = controls:Button {}
play_pause_btn:onClicked(function()
playback.playing:set(not playback.playing:get())
end)
play_pause_btn:focus()
local play_pause_img = play_pause_btn:Image { src = img.pause }
local next_btn = controls:Button {}
next_btn:onClicked(queue.next)
local next_img = next_btn:Image { src = img.next_disabled }
local shuffle_btn = controls:Button {}
shuffle_btn:onClicked(function()
queue.random:set(not queue.random:get())
end)
local shuffle_img = shuffle_btn:Image { src = img.shuffle }
controls:Object({ flex_grow = 1, h = 1 }) -- spacer
local format_time = function(time)
return string.format("%d:%02d", time // 60, time % 60)
local is_now_playing_shown = false
return screen:new {
createUi = function(self)
self.root = lvgl.Object(nil, {
flex = {
flex_direction = "column",
flex_wrap = "wrap",
justify_content = "center",
align_items = "center",
align_content = "center",
},
w = lvgl.HOR_RES(),
h = lvgl.VER_RES(),
})
self.root:center()
self.status_bar = widgets.StatusBar(self.root, {
back_cb = backstack.pop,
transparent_bg = true,
})
local info = self.root:Object {
flex = {
flex_direction = "column",
flex_wrap = "wrap",
justify_content = "center",
align_items = "center",
align_content = "center",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
flex_grow = 1,
}
local artist = info:Label {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
text = "",
text_font = font.fusion_10,
text_align = 2,
}
local title = info:Label {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
text = "",
text_align = 2,
}
local playlist = self.root:Object {
flex = {
flex_direction = "row",
justify_content = "center",
align_items = "center",
align_content = "center",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
}
playlist:Object({ w = 3, h = 1 }) -- spacer
local cur_time = playlist:Label {
w = lvgl.SIZE_CONTENT,
h = lvgl.SIZE_CONTENT,
text = "",
text_font = font.fusion_10,
}
playlist:Object({ flex_grow = 1, h = 1 }) -- spacer
local playlist_pos = playlist:Label {
text = "",
text_font = font.fusion_10,
}
playlist:Label {
text = "/",
text_font = font.fusion_10,
}
local playlist_total = playlist:Label {
text = "",
text_font = font.fusion_10,
}
playlist:Object({ flex_grow = 1, h = 1 }) -- spacer
local end_time = playlist:Label {
w = lvgl.SIZE_CONTENT,
h = lvgl.SIZE_CONTENT,
align = lvgl.ALIGN.RIGHT_MID,
text = "",
text_font = font.fusion_10,
}
playlist:Object({ w = 3, h = 1 }) -- spacer
local scrubber = self.root:Slider {
w = lvgl.PCT(100),
h = 5,
range = { min = 0, max = 100 },
value = 0,
}
scrubber:onevent(lvgl.EVENT.RELEASED, function()
playback.position:set(scrubber:value())
end)
local controls = self.root:Object {
flex = {
flex_direction = "row",
justify_content = "center",
align_items = "center",
align_content = "center",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
pad_column = 8,
pad_all = 2,
}
controls:Object({ flex_grow = 1, h = 1 }) -- spacer
local repeat_btn = controls:Button {}
repeat_btn:onClicked(function()
queue.repeat_track:set(not queue.repeat_track:get())
end)
local repeat_img = repeat_btn:Image { src = img.repeat_enabled }
local prev_btn = controls:Button {}
prev_btn:onClicked(queue.previous)
local prev_img = prev_btn:Image { src = img.prev_disabled }
local play_pause_btn = controls:Button {}
play_pause_btn:onClicked(function()
playback.playing:set(not playback.playing:get())
end)
play_pause_btn:focus()
local play_pause_img = play_pause_btn:Image { src = img.pause }
local next_btn = controls:Button {}
next_btn:onClicked(queue.next)
local next_img = next_btn:Image { src = img.next_disabled }
local shuffle_btn = controls:Button {}
shuffle_btn:onClicked(function()
queue.random:set(not queue.random:get())
end)
local shuffle_img = shuffle_btn:Image { src = img.shuffle }
controls:Object({ flex_grow = 1, h = 1 }) -- spacer
local format_time = function(time)
return string.format("%d:%02d", time // 60, time % 60)
end
self.bindings = {
playback.playing:bind(function(playing)
if playing then
play_pause_img:set_src(img.pause)
else
play_pause_img:set_src(img.play)
end
end),
playback.position:bind(function(pos)
if not pos then return end
cur_time:set {
text = format_time(pos)
}
if not scrubber:is_dragged() then
scrubber:set { value = pos }
end
end),
playback.track:bind(function(track)
if not track then return end
end_time:set {
text = format_time(track.duration)
}
title:set { text = track.title }
artist:set { text = track.artist }
scrubber:set {
range = { min = 0, max = track.duration }
}
end),
queue.position:bind(function(pos)
if not pos then return end
playlist_pos:set { text = tostring(pos) }
next_img:set_src(
pos < queue.size:get() and img.next or img.next_disabled
)
prev_img:set_src(
pos > 1 and img.prev or img.prev_disabled
)
end),
queue.random:bind(function(shuffling)
if shuffling then
shuffle_img:set_src(img.shuffle)
else
shuffle_img:set_src(img.shuffle_disabled)
end
end),
queue.repeat_track:bind(function(en)
if en then
repeat_img:set_src(img.repeat_enabled)
else
repeat_img:set_src(img.repeat_disabled)
end
end),
queue.size:bind(function(num)
if not num then return end
playlist_total:set { text = tostring(num) }
end),
}
end,
onShown = function() is_now_playing_shown = true end,
onHidden = function() is_now_playing_shown = false end,
pushIfNotShown = function(self)
if not is_now_playing_shown then
backstack.push(self:new())
end
end
screen.bindings = {
playback.playing:bind(function(playing)
if playing then
play_pause_img:set_src(img.pause)
else
play_pause_img:set_src(img.play)
end
end),
playback.position:bind(function(pos)
if not pos then return end
cur_time:set {
text = format_time(pos)
}
if not scrubber:is_dragged() then
scrubber:set { value = pos }
end
end),
playback.track:bind(function(track)
if not track then return end
end_time:set {
text = format_time(track.duration)
}
title:set { text = track.title }
artist:set { text = track.artist }
scrubber:set {
range = { min = 0, max = track.duration }
}
end),
queue.position:bind(function(pos)
if not pos then return end
playlist_pos:set { text = tostring(pos) }
next_img:set_src(
pos < queue.size:get() and img.next or img.next_disabled
)
prev_img:set_src(
pos > 1 and img.prev or img.prev_disabled
)
end),
queue.random:bind(function(shuffling)
if shuffling then
shuffle_img:set_src(img.shuffle)
else
shuffle_img:set_src(img.shuffle_disabled)
end
end),
queue.repeat_track:bind(function(en)
if en then
repeat_img:set_src(img.repeat_enabled)
else
repeat_img:set_src(img.repeat_disabled)
end
end),
queue.size:bind(function(num)
if not num then return end
playlist_total:set { text = tostring(num) }
end),
}
return screen
end
}

@ -7,8 +7,7 @@ local display = require("display")
local controls = require("controls")
local bluetooth = require("bluetooth")
local database = require("database")
local settings = {}
local screen = require("screen")
local function SettingsScreen(title)
local menu = widgets.MenuScreen {
@ -31,323 +30,331 @@ local function SettingsScreen(title)
return menu
end
function settings.bluetooth()
local menu = SettingsScreen("Bluetooth")
local enable_container = menu.content:Object {
flex = {
flex_direction = "row",
justify_content = "flex-start",
align_items = "content",
align_content = "flex-start",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
pad_bottom = 1,
}
enable_container:Label { text = "Enable", flex_grow = 1 }
local enable_sw = enable_container:Switch {}
enable_sw:onevent(lvgl.EVENT.VALUE_CHANGED, function()
local enabled = enable_sw:enabled()
bluetooth.enabled:set(enabled)
end)
menu.content:Label {
text = "Paired Device",
pad_bottom = 1,
}:add_style(theme.settings_title)
local paired_container = menu.content:Object {
flex = {
flex_direction = "row",
justify_content = "flex-start",
align_items = "flex-start",
align_content = "flex-start",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
pad_bottom = 2,
}
local paired_device = paired_container:Label {
flex_grow = 1,
}
local clear_paired = paired_container:Button {}
clear_paired:Label { text = "x" }
clear_paired:onClicked(function()
bluetooth.paired_device:set()
end)
menu.content:Label {
text = "Nearby Devices",
pad_bottom = 1,
}:add_style(theme.settings_title)
local devices = menu.content:List {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
}
menu.bindings = {
bluetooth.enabled:bind(function(en)
if en then
enable_sw:add_state(lvgl.STATE.CHECKED)
else
enable_sw:clear_state(lvgl.STATE.CHECKED)
end
end),
bluetooth.paired_device:bind(function(device)
if device then
paired_device:set { text = device.name }
clear_paired:clear_flag(lvgl.FLAG.HIDDEN)
else
paired_device:set { text = "None" }
clear_paired:add_flag(lvgl.FLAG.HIDDEN)
end
end),
bluetooth.devices:bind(function(devs)
devices:clean()
for _, dev in pairs(devs) do
devices:add_btn(nil, dev.name):onClicked(function()
bluetooth.paired_device:set(dev)
end)
end
local BluetoothSettings = screen:new {
createUi = function(self)
self.menu = SettingsScreen("Bluetooth")
local enable_container = self.menu.content:Object {
flex = {
flex_direction = "row",
justify_content = "flex-start",
align_items = "content",
align_content = "flex-start",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
pad_bottom = 1,
}
enable_container:Label { text = "Enable", flex_grow = 1 }
local enable_sw = enable_container:Switch {}
enable_sw:onevent(lvgl.EVENT.VALUE_CHANGED, function()
local enabled = enable_sw:enabled()
bluetooth.enabled:set(enabled)
end)
}
end
function settings.headphones()
local menu = SettingsScreen("Headphones")
self.menu.content:Label {
text = "Paired Device",
pad_bottom = 1,
}:add_style(theme.settings_title)
local paired_container = self.menu.content:Object {
flex = {
flex_direction = "row",
justify_content = "flex-start",
align_items = "flex-start",
align_content = "flex-start",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
pad_bottom = 2,
}
local paired_device = paired_container:Label {
flex_grow = 1,
}
local clear_paired = paired_container:Button {}
clear_paired:Label { text = "x" }
clear_paired:onClicked(function()
bluetooth.paired_device:set()
end)
menu.content:Label {
text = "Maximum volume limit",
}:add_style(theme.settings_title)
self.menu.content:Label {
text = "Nearby Devices",
pad_bottom = 1,
}:add_style(theme.settings_title)
local devices = self.menu.content:List {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
}
self.bindings = {
bluetooth.enabled:bind(function(en)
if en then
enable_sw:add_state(lvgl.STATE.CHECKED)
else
enable_sw:clear_state(lvgl.STATE.CHECKED)
end
end),
bluetooth.paired_device:bind(function(device)
if device then
paired_device:set { text = device.name }
clear_paired:clear_flag(lvgl.FLAG.HIDDEN)
else
paired_device:set { text = "None" }
clear_paired:add_flag(lvgl.FLAG.HIDDEN)
end
end),
bluetooth.devices:bind(function(devs)
devices:clean()
for _, dev in pairs(devs) do
devices:add_btn(nil, dev.name):onClicked(function()
bluetooth.paired_device:set(dev)
end)
end
end)
}
end
}
local HeadphonesSettings = screen:new {
createUi = function(self)
self.menu = SettingsScreen("Headphones")
self.menu.content:Label {
text = "Maximum volume limit",
}:add_style(theme.settings_title)
local volume_chooser = self.menu.content:Dropdown {
options = "Line Level (-10 dB)\nCD Level (+6 dB)\nMaximum (+10dB)",
selected = 1,
}
local limits = { -10, 6, 10 }
volume_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function()
-- luavgl dropdown binding uses 0-based indexing :(
local selection = volume_chooser:get('selected') + 1
volume.limit_db:set(limits[selection])
end)
local volume_chooser = menu.content:Dropdown {
options = "Line Level (-10 dB)\nCD Level (+6 dB)\nMaximum (+10dB)",
selected = 1,
}
local limits = { -10, 6, 10 }
volume_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function()
-- luavgl dropdown binding uses 0-based indexing :(
local selection = volume_chooser:get('selected') + 1
volume.limit_db:set(limits[selection])
end)
menu.content:Label {
text = "Left/Right balance",
}:add_style(theme.settings_title)
local balance = menu.content:Slider {
w = lvgl.PCT(100),
h = 5,
range = { min = -100, max = 100 },
value = 0,
}
balance:onevent(lvgl.EVENT.VALUE_CHANGED, function()
volume.left_bias:set(balance:value())
end)
self.menu.content:Label {
text = "Left/Right balance",
}:add_style(theme.settings_title)
local balance = self.menu.content:Slider {
w = lvgl.PCT(100),
h = 5,
range = { min = -100, max = 100 },
value = 0,
}
balance:onevent(lvgl.EVENT.VALUE_CHANGED, function()
volume.left_bias:set(balance:value())
end)
local balance_label = menu.content:Label {}
local balance_label = self.menu.content:Label {}
menu.bindings = {
volume.limit_db:bind(function(limit)
for i = 1, #limits do
if limits[i] == limit then
volume_chooser:set { selected = i - 1 }
self.bindings = {
volume.limit_db:bind(function(limit)
for i = 1, #limits do
if limits[i] == limit then
volume_chooser:set { selected = i - 1 }
end
end
end
end),
volume.left_bias:bind(function(bias)
balance:set {
value = bias
}
if bias < 0 then
balance_label:set {
text = string.format("Left %.2fdB", bias / 4)
end),
volume.left_bias:bind(function(bias)
balance:set {
value = bias
}
elseif bias > 0 then
balance_label:set {
text = string.format("Right %.2fdB", -bias / 4)
}
else
balance_label:set { text = "Balanced" }
end
end),
}
return menu
end
function settings.display()
local menu = SettingsScreen("Display")
local brightness_title = menu.content:Object {
flex = {
flex_direction = "row",
justify_content = "flex-start",
align_items = "flex-start",
align_content = "flex-start",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
}
brightness_title:Label { text = "Brightness", flex_grow = 1 }
local brightness_pct = brightness_title:Label {}
brightness_pct:add_style(theme.settings_title)
local brightness = menu.content:Slider {
w = lvgl.PCT(100),
h = 5,
range = { min = 0, max = 100 },
value = display.brightness:get(),
}
brightness:onevent(lvgl.EVENT.VALUE_CHANGED, function()
display.brightness:set(brightness:value())
end)
menu.bindings = {
display.brightness:bind(function(b)
brightness_pct:set { text = tostring(b) .. "%" }
if bias < 0 then
balance_label:set {
text = string.format("Left %.2fdB", bias / 4)
}
elseif bias > 0 then
balance_label:set {
text = string.format("Right %.2fdB", -bias / 4)
}
else
balance_label:set { text = "Balanced" }
end
end),
}
end
}
local DisplaySettings = screen:new {
createUi = function(self)
self.menu = SettingsScreen("Display")
local brightness_title = self.menu.content:Object {
flex = {
flex_direction = "row",
justify_content = "flex-start",
align_items = "flex-start",
align_content = "flex-start",
},
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
}
brightness_title:Label { text = "Brightness", flex_grow = 1 }
local brightness_pct = brightness_title:Label {}
brightness_pct:add_style(theme.settings_title)
local brightness = self.menu.content:Slider {
w = lvgl.PCT(100),
h = 5,
range = { min = 0, max = 100 },
value = display.brightness:get(),
}
brightness:onevent(lvgl.EVENT.VALUE_CHANGED, function()
display.brightness:set(brightness:value())
end)
}
return menu
end
self.bindings = {
display.brightness:bind(function(b)
brightness_pct:set { text = tostring(b) .. "%" }
end)
}
end
}
function settings.input()
local menu = SettingsScreen("Input Method")
local InputSettings = screen:new {
createUi = function(self)
self.menu = SettingsScreen("Input Method")
menu.content:Label {
text = "Control scheme",
}:add_style(theme.settings_title)
self.menu.content:Label {
text = "Control scheme",
}:add_style(theme.settings_title)
local schemes = controls.schemes()
local option_to_scheme = {}
local scheme_to_option = {}
local schemes = controls.schemes()
local option_to_scheme = {}
local scheme_to_option = {}
local option_idx = 0
local options = ""
local option_idx = 0
local options = ""
for i, v in pairs(schemes) do
option_to_scheme[option_idx] = i
scheme_to_option[i] = option_idx
if option_idx > 0 then
options = options .. "\n"
for i, v in pairs(schemes) do
option_to_scheme[option_idx] = i
scheme_to_option[i] = option_idx
if option_idx > 0 then
options = options .. "\n"
end
options = options .. v
option_idx = option_idx + 1
end
options = options .. v
option_idx = option_idx + 1
end
local controls_chooser = menu.content:Dropdown {
options = options,
}
menu.bindings = {
controls.scheme:bind(function(s)
local option = scheme_to_option[s]
controls_chooser:set({ selected = option })
local controls_chooser = self.menu.content:Dropdown {
options = options,
}
self.bindings = {
controls.scheme:bind(function(s)
local option = scheme_to_option[s]
controls_chooser:set({ selected = option })
end)
}
controls_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function()
local option = controls_chooser:get('selected')
local scheme = option_to_scheme[option]
controls.scheme:set(scheme)
end)
}
controls_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function()
local option = controls_chooser:get('selected')
local scheme = option_to_scheme[option]
controls.scheme:set(scheme)
end)
menu.content:Label {
text = "Scroll Sensitivity",
}:add_style(theme.settings_title)
local slider_scale = 4; -- Power steering
local sensitivity = menu.content:Slider {
w = lvgl.PCT(90),
h = 5,
range = { min = 0, max = 255/slider_scale },
value = controls.scroll_sensitivity:get()/slider_scale,
}
sensitivity:onevent(lvgl.EVENT.VALUE_CHANGED, function()
controls.scroll_sensitivity:set(sensitivity:value()*slider_scale)
end)
return menu
end
function settings.database()
local menu = SettingsScreen("Database")
local db = require("database")
widgets.Row(menu.content, "Schema version", db.version())
widgets.Row(menu.content, "Size on disk", string.format("%.1f KiB", db.size() / 1024))
local actions_container = menu.content:Object {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
flex = {
flex_direction = "row",
justify_content = "center",
align_items = "space-evenly",
align_content = "center",
},
pad_top = 4,
pad_column = 4,
}
actions_container:add_style(theme.list_item)
local update = actions_container:Button {}
update:Label { text = "Update" }
update:onClicked(function()
database.update()
end)
end
function settings.firmware()
local menu = SettingsScreen("Firmware")
local version = require("version")
widgets.Row(menu.content, "ESP32", version.esp())
widgets.Row(menu.content, "SAMD21", version.samd())
widgets.Row(menu.content, "Collator", version.collator())
end
function settings.root()
local menu = widgets.MenuScreen {
show_back = true,
title = "Settings",
}
menu.list = menu.root:List {
w = lvgl.PCT(100),
h = lvgl.PCT(100),
flex_grow = 1,
}
local function section(name)
menu.list:add_text(name):add_style(theme.list_heading)
self.menu.content:Label {
text = "Scroll Sensitivity",
}:add_style(theme.settings_title)
local slider_scale = 4; -- Power steering
local sensitivity = self.menu.content:Slider {
w = lvgl.PCT(90),
h = 5,
range = { min = 0, max = 255 / slider_scale },
value = controls.scroll_sensitivity:get() / slider_scale,
}
sensitivity:onevent(lvgl.EVENT.VALUE_CHANGED, function()
controls.scroll_sensitivity:set(sensitivity:value() * slider_scale)
end)
end
local function submenu(name, fn)
local item = menu.list:add_btn(nil, name)
item:onClicked(function()
backstack.push(fn)
}
local DatabaseSettings = screen:new {
createUi = function(self)
self.menu = SettingsScreen("Database")
local db = require("database")
widgets.Row(self.menu.content, "Schema version", db.version())
widgets.Row(self.menu.content, "Size on disk", string.format("%.1f KiB", db.size() / 1024))
local actions_container = self.menu.content:Object {
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
flex = {
flex_direction = "row",
justify_content = "center",
align_items = "space-evenly",
align_content = "center",
},
pad_top = 4,
pad_column = 4,
}
actions_container:add_style(theme.list_item)
local update = actions_container:Button {}
update:Label { text = "Update" }
update:onClicked(function()
database.update()
end)
item:add_style(theme.list_item)
end
}
local FirmwareSettings = screen:new {
createUi = function(self)
self.menu = SettingsScreen("Firmware")
local version = require("version")
widgets.Row(self.menu.content, "ESP32", version.esp())
widgets.Row(self.menu.content, "SAMD21", version.samd())
widgets.Row(self.menu.content, "Collator", version.collator())
end
}
section("Audio")
submenu("Bluetooth", settings.bluetooth)
submenu("Headphones", settings.headphones)
local LicensesScreen = screen:new {
createUi = function(self)
self.root = require("licenses")()
end
}
return screen:new {
createUi = function(self)
self.menu = widgets.MenuScreen {
show_back = true,
title = "Settings",
}
self.list = self.menu.root:List {
w = lvgl.PCT(100),
h = lvgl.PCT(100),
flex_grow = 1,
}
local function section(name)
self.list:add_text(name):add_style(theme.list_heading)
end
section("Interface")
submenu("Display", settings.display)
submenu("Input Method", settings.input)
local function submenu(name, class)
local item = self.list:add_btn(nil, name)
item:onClicked(function()
backstack.push(class:new())
end)
item:add_style(theme.list_item)
end
section("System")
submenu("Database", settings.database)
submenu("Firmware", settings.firmware)
submenu("Licenses", function()
return require("licenses")()
end)
section("Audio")
submenu("Bluetooth", BluetoothSettings)
submenu("Headphones", HeadphonesSettings)
return menu
end
section("Interface")
submenu("Display", DisplaySettings)
submenu("Input Method", InputSettings)
return settings
section("System")
submenu("Database", DatabaseSettings)
submenu("Firmware", FirmwareSettings)
submenu("Licenses", LicensesScreen)
end
}

@ -5,6 +5,7 @@
idf_component_register(
SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp"
"lua_queue.cpp" "lua_version.cpp" "lua_controls.cpp" "registry.cpp"
"lua_screen.cpp"
INCLUDE_DIRS "include"
REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database"
"esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term"

@ -19,6 +19,7 @@
#include "lua_controls.hpp"
#include "lua_database.hpp"
#include "lua_queue.hpp"
#include "lua_screen.hpp"
#include "lua_version.hpp"
#include "lvgl.h"
@ -84,6 +85,7 @@ auto Bridge::installBaseModules(lua_State* L) -> void {
RegisterDatabaseModule(L);
RegisterQueueModule(L);
RegisterVersionModule(L);
RegisterScreenModule(L);
}
auto Bridge::installLvgl(lua_State* L) -> void {

@ -0,0 +1,15 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include "lua.hpp"
namespace lua {
auto RegisterScreenModule(lua_State*) -> void;
} // namespace lua

@ -0,0 +1,75 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "lua_screen.hpp"
#include <memory>
#include <string>
#include "lua.hpp"
#include "esp_log.h"
#include "lauxlib.h"
#include "lua.h"
#include "lvgl.h"
#include "bridge.hpp"
#include "database.hpp"
#include "event_queue.hpp"
#include "index.hpp"
#include "property.hpp"
#include "service_locator.hpp"
#include "track.hpp"
#include "track_queue.hpp"
#include "ui_events.hpp"
namespace lua {
static auto screen_new(lua_State* L) -> int {
// o = o or {}
if (lua_gettop(L) != 2) {
lua_settop(L, 1);
lua_newtable(L);
}
// Swap o and self on the stack.
lua_insert(L, 1);
lua_pushliteral(L, "__index");
lua_pushvalue(L, 1);
lua_settable(L, 1); // self.__index = self
lua_setmetatable(L, 1); // setmetatable(o, self)
return 1; // return o
}
static auto screen_noop(lua_State* state) -> int {
return 0;
}
static const struct luaL_Reg kScreenFuncs[] = {{"new", screen_new},
{"createUi", screen_noop},
{"onShown", screen_noop},
{"onHidden", screen_noop},
{NULL, NULL}};
static auto lua_screen(lua_State* state) -> int {
luaL_newlib(state, kScreenFuncs);
lua_pushliteral(state, "__index");
lua_pushvalue(state, -2);
lua_rawset(state, -3);
return 1;
}
auto RegisterScreenModule(lua_State* s) -> void {
luaL_requiref(s, "screen", lua_screen, true);
lua_pop(s, 1);
}
} // namespace lua

@ -27,6 +27,9 @@ class Screen {
Screen();
virtual ~Screen();
virtual auto onShown() -> void {}
virtual auto onHidden() -> void {}
auto root() -> lv_obj_t* { return root_; }
auto content() -> lv_obj_t* { return content_; }
auto alert() -> lv_obj_t* { return alert_; }

@ -18,6 +18,9 @@ class Lua : public Screen {
Lua();
~Lua();
auto onShown() -> void override;
auto onHidden() -> void override;
auto SetObjRef(lua_State*) -> void;
private:

@ -7,8 +7,10 @@
#include "screen_lua.hpp"
#include "core/lv_obj_tree.h"
#include "lua.h"
#include "lua.hpp"
#include "lua_thread.hpp"
#include "luavgl.h"
namespace ui {
@ -22,6 +24,40 @@ Lua::~Lua() {
}
}
auto Lua::onShown() -> void {
if (!s_ || !obj_ref_) {
return;
}
lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_);
lua_pushliteral(s_, "onShown");
if (lua_gettable(s_, -2) == LUA_TFUNCTION) {
lua_pushvalue(s_, -2);
lua::CallProtected(s_, 1, 0);
} else {
lua_pop(s_, 1);
}
lua_pop(s_, 1);
}
auto Lua::onHidden() -> void {
if (!s_ || !obj_ref_) {
return;
}
lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_);
lua_pushliteral(s_, "onHidden");
if (lua_gettable(s_, -2) == LUA_TFUNCTION) {
lua_pushvalue(s_, -2);
lua::CallProtected(s_, 1, 0);
} else {
lua_pop(s_, 1);
}
lua_pop(s_, 1);
}
auto Lua::SetObjRef(lua_State* s) -> void {
assert(s_ == nullptr);
s_ = s;

@ -125,21 +125,24 @@ lua::Property UiState::sPlaybackPlaying{
}};
lua::Property UiState::sPlaybackTrack{};
lua::Property UiState::sPlaybackPosition{0, [](const lua::LuaValue& val) {
int current_val = std::get<int>(sPlaybackPosition.Get());
if (!std::holds_alternative<int>(val)) {
return false;
}
int new_val = std::get<int>(val);
if (current_val != new_val) {
auto track = sPlaybackTrack.Get();
if (!std::holds_alternative<audio::Track>(track)) {
lua::Property UiState::sPlaybackPosition{
0, [](const lua::LuaValue& val) {
int current_val = std::get<int>(sPlaybackPosition.Get());
if (!std::holds_alternative<int>(val)) {
return false;
}
events::Audio().Dispatch(audio::SeekFile{.offset = (uint32_t)new_val, .filename = std::get<audio::Track>(track).filepath});
}
return true;
}};
int new_val = std::get<int>(val);
if (current_val != new_val) {
auto track = sPlaybackTrack.Get();
if (!std::holds_alternative<audio::Track>(track)) {
return false;
}
events::Audio().Dispatch(audio::SeekFile{
.offset = (uint32_t)new_val,
.filename = std::get<audio::Track>(track).filepath});
}
return true;
}};
lua::Property UiState::sQueuePosition{0};
lua::Property UiState::sQueueSize{0};
@ -294,21 +297,29 @@ auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool {
}
void UiState::PushScreen(std::shared_ptr<Screen> screen) {
lv_obj_set_parent(sAlertContainer, screen->alert());
if (sCurrentScreen) {
sCurrentScreen->onHidden();
sScreens.push(sCurrentScreen);
}
sCurrentScreen = screen;
lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert());
sCurrentScreen->onShown();
}
int UiState::PopScreen() {
if (sScreens.empty()) {
return 0;
}
sCurrentScreen = sScreens.top();
lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert());
lv_obj_set_parent(sAlertContainer, sScreens.top()->alert());
sCurrentScreen->onHidden();
sCurrentScreen = sScreens.top();
sScreens.pop();
sCurrentScreen->onShown();
return sScreens.size();
}
@ -539,7 +550,7 @@ void Lua::entry() {
auto Lua::PushLuaScreen(lua_State* s) -> int {
// Ensure the arg looks right before continuing.
luaL_checktype(s, 1, LUA_TFUNCTION);
luaL_checktype(s, 1, LUA_TTABLE);
// First, create a new plain old Screen object. We will use its root and
// group for the Lua screen. Allocate it in external ram so that arbitrarily
@ -554,10 +565,15 @@ auto Lua::PushLuaScreen(lua_State* s) -> int {
lv_group_set_default(new_screen->group());
// Call the constructor for this screen.
lua_settop(s, 1); // Make sure the function is actually at top of stack
lua::CallProtected(s, 0, 1);
// lua_settop(s, 1); // Make sure the screen is actually at top of stack
lua_pushliteral(s, "createUi");
if (lua_gettable(s, 1) == LUA_TFUNCTION) {
lua_pushvalue(s, 1);
lua::CallProtected(s, 1, 0);
}
// Store the reference for the table the constructor returned.
// Store the reference for this screen's table.
lua_settop(s, 1);
new_screen->SetObjRef(s);
// Finally, push the now-initialised screen as if it were a regular C++
@ -585,7 +601,7 @@ auto Lua::PopLuaScreen(lua_State* s) -> int {
}
auto Lua::Ticks(lua_State* s) -> int {
lua_pushinteger(s, esp_timer_get_time()/1000);
lua_pushinteger(s, esp_timer_get_time() / 1000);
return 1;
}

Loading…
Cancel
Save