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 playing = require("playing")
local theme = require("theme") local theme = require("theme")
local playback = require("playback") local playback = require("playback")
local screen = require("screen")
local browser = {} return screen:new {
createUi = function(self)
function browser.create(opts) self.root = lvgl.Object(nil, {
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 {
flex = { flex = {
flex_direction = "column", flex_direction = "column",
flex_wrap = "wrap", flex_wrap = "wrap",
@ -38,100 +19,113 @@ function browser.create(opts)
align_content = "flex-start", align_content = "flex-start",
}, },
w = lvgl.HOR_RES(), w = lvgl.HOR_RES(),
h = lvgl.SIZE_CONTENT, h = lvgl.VER_RES(),
pad_left = 4, })
pad_right = 4, self.root:center()
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,
}
local buttons = header:Object({ self.status_bar = widgets.StatusBar(self.root, {
flex = { title = self.title,
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 = 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, { if self.breadcrumb then
w = lvgl.PCT(100), local header = self.root:Object {
h = lvgl.PCT(100), flex = {
flex_grow = 1, flex_direction = "column",
scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, 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") header:Label {
back:onClicked(backstack.pop) text = self.breadcrumb,
back:add_style(theme.list_item) text_font = font.fusion_10,
}
screen.focused_item = 0 local buttons = header:Object({
screen.last_item = 0 flex = {
screen.add_item = function(item) flex_direction = "row",
if not item then return end flex_wrap = "wrap",
screen.last_item = screen.last_item + 1 justify_content = "flex-end",
local this_item = screen.last_item align_items = "center",
local btn = screen.list:add_btn(nil, tostring(item)) align_content = "center",
btn:onClicked(function() },
local contents = item:contents() w = lvgl.PCT(100),
if type(contents) == "userdata" then h = lvgl.SIZE_CONTENT,
backstack.push(function() pad_column = 4,
return browser.create({ })
title = opts.title, local original_iterator = self.iterator:clone()
iterator = contents, local enqueue = widgets.IconBtn(buttons, "//lua/img/enqueue.png", "Enqueue")
breadcrumb = tostring(item), enqueue:onClicked(function()
}) queue.add(original_iterator)
end) playback.playing:set(true)
else end)
-- enqueue:add_flag(lvgl.FLAG.HIDDEN)
local play = widgets.IconBtn(buttons, "//lua/img/play_small.png", "Play")
play:onClicked(function()
queue.clear() queue.clear()
queue.add(contents) queue.add(original_iterator)
playback.playing:set(true) playback.playing:set(true)
backstack.push(playing) backstack.push(playing)
end end
end) )
btn:onevent(lvgl.EVENT.FOCUSED, function() end
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
for _ = 1, 8 do self.list = lvgl.List(self.root, {
local val = opts.iterator() w = lvgl.PCT(100),
if not val then break end h = lvgl.PCT(100),
screen.add_item(val) flex_grow = 1,
end scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF,
})
return screen local back = self.list:add_btn(nil, "< Back")
end 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 widgets = require("widgets")
local font = require("font") local font = require("font")
local theme = require("theme") local theme = require("theme")
local screen = require("screen")
local function show_license(text) local function show_license(text)
backstack.push(function() backstack.push(screen:new {
local screen = widgets.MenuScreen { createUi = function(self)
show_back = true, self.menu = widgets.MenuScreen {
title = "Licenses", show_back = true,
} title = "Licenses",
screen.root:Label { }
w = lvgl.PCT(100), self.menu.root:Label {
h = lvgl.SIZE_CONTENT, w = lvgl.PCT(100),
text_font = font.fusion_10, h = lvgl.SIZE_CONTENT,
text = text, text_font = font.fusion_10,
} text = text,
end) }
end
})
end end
local function gpl(copyright) local function gpl(copyright)
@ -175,4 +178,6 @@ return function()
library("tremor", "bsd", function() library("tremor", "bsd", function()
xiphbsd("Copyright (c) 2002, Xiph.org Foundation") xiphbsd("Copyright (c) 2002, Xiph.org Foundation")
end) end)
return menu
end end

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

@ -4,6 +4,7 @@ local backstack = require("backstack")
local font = require("font") local font = require("font")
local playback = require("playback") local playback = require("playback")
local queue = require("queue") local queue = require("queue")
local screen = require("screen")
local img = { local img = {
play = "//lua/img/play.png", play = "//lua/img/play.png",
@ -18,219 +19,227 @@ local img = {
repeat_disabled = "//lua/img/repeat_disabled.png", repeat_disabled = "//lua/img/repeat_disabled.png",
} }
return function(opts) local is_now_playing_shown = false
local screen = {}
screen.root = lvgl.Object(nil, { return screen:new {
flex = { createUi = function(self)
flex_direction = "column", self.root = lvgl.Object(nil, {
flex_wrap = "wrap", flex = {
justify_content = "center", flex_direction = "column",
align_items = "center", flex_wrap = "wrap",
align_content = "center", justify_content = "center",
}, align_items = "center",
w = lvgl.HOR_RES(), align_content = "center",
h = lvgl.VER_RES(), },
}) w = lvgl.HOR_RES(),
screen.root:center() h = lvgl.VER_RES(),
})
screen.status_bar = widgets.StatusBar(screen.root, { self.root:center()
back_cb = backstack.pop,
transparent_bg = true, self.status_bar = widgets.StatusBar(self.root, {
}) back_cb = backstack.pop,
transparent_bg = true,
local info = screen.root:Object { })
flex = {
flex_direction = "column", local info = self.root:Object {
flex_wrap = "wrap", flex = {
justify_content = "center", flex_direction = "column",
align_items = "center", flex_wrap = "wrap",
align_content = "center", justify_content = "center",
}, align_items = "center",
w = lvgl.PCT(100), align_content = "center",
h = lvgl.SIZE_CONTENT, },
flex_grow = 1, w = lvgl.PCT(100),
} h = lvgl.SIZE_CONTENT,
flex_grow = 1,
local artist = info:Label { }
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT, local artist = info:Label {
text = "", w = lvgl.PCT(100),
text_font = font.fusion_10, h = lvgl.SIZE_CONTENT,
text_align = 2, text = "",
} text_font = font.fusion_10,
text_align = 2,
local title = info:Label { }
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT, local title = info:Label {
text = "", w = lvgl.PCT(100),
text_align = 2, h = lvgl.SIZE_CONTENT,
} text = "",
text_align = 2,
local playlist = screen.root:Object { }
flex = {
flex_direction = "row", local playlist = self.root:Object {
justify_content = "center", flex = {
align_items = "center", flex_direction = "row",
align_content = "center", justify_content = "center",
}, align_items = "center",
w = lvgl.PCT(100), align_content = "center",
h = lvgl.SIZE_CONTENT, },
} w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
playlist:Object({ w = 3, h = 1 }) -- spacer }
local cur_time = playlist:Label { playlist:Object({ w = 3, h = 1 }) -- spacer
w = lvgl.SIZE_CONTENT,
h = lvgl.SIZE_CONTENT, local cur_time = playlist:Label {
text = "", w = lvgl.SIZE_CONTENT,
text_font = font.fusion_10, h = lvgl.SIZE_CONTENT,
} text = "",
text_font = font.fusion_10,
playlist:Object({ flex_grow = 1, h = 1 }) -- spacer }
local playlist_pos = playlist:Label { playlist:Object({ flex_grow = 1, h = 1 }) -- spacer
text = "",
text_font = font.fusion_10, local playlist_pos = playlist:Label {
} text = "",
playlist:Label { text_font = font.fusion_10,
text = "/", }
text_font = font.fusion_10, playlist:Label {
} text = "/",
local playlist_total = playlist:Label { text_font = font.fusion_10,
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 { playlist:Object({ flex_grow = 1, h = 1 }) -- spacer
w = lvgl.SIZE_CONTENT,
h = lvgl.SIZE_CONTENT, local end_time = playlist:Label {
align = lvgl.ALIGN.RIGHT_MID, w = lvgl.SIZE_CONTENT,
text = "", h = lvgl.SIZE_CONTENT,
text_font = font.fusion_10, align = lvgl.ALIGN.RIGHT_MID,
} text = "",
playlist:Object({ w = 3, h = 1 }) -- spacer text_font = font.fusion_10,
}
local scrubber = screen.root:Slider { playlist:Object({ w = 3, h = 1 }) -- spacer
w = lvgl.PCT(100),
h = 5, local scrubber = self.root:Slider {
range = { min = 0, max = 100 }, w = lvgl.PCT(100),
value = 0, h = 5,
} range = { min = 0, max = 100 },
value = 0,
scrubber:onevent(lvgl.EVENT.RELEASED, function() }
playback.position:set(scrubber:value())
end) scrubber:onevent(lvgl.EVENT.RELEASED, function()
playback.position:set(scrubber:value())
local controls = screen.root:Object { end)
flex = {
flex_direction = "row", local controls = self.root:Object {
justify_content = "center", flex = {
align_items = "center", flex_direction = "row",
align_content = "center", justify_content = "center",
}, align_items = "center",
w = lvgl.PCT(100), align_content = "center",
h = lvgl.SIZE_CONTENT, },
pad_column = 8, w = lvgl.PCT(100),
pad_all = 2, h = lvgl.SIZE_CONTENT,
} pad_column = 8,
pad_all = 2,
}
controls:Object({ flex_grow = 1, h = 1 }) -- spacer
local repeat_btn = controls:Button {} controls:Object({ flex_grow = 1, h = 1 }) -- spacer
repeat_btn:onClicked(function()
queue.repeat_track:set(not queue.repeat_track:get()) local repeat_btn = controls:Button {}
end) repeat_btn:onClicked(function()
local repeat_img = repeat_btn:Image { src = img.repeat_enabled } queue.repeat_track:set(not queue.repeat_track:get())
end)
local prev_btn = controls:Button {} local repeat_img = repeat_btn:Image { src = img.repeat_enabled }
prev_btn:onClicked(queue.previous)
local prev_img = prev_btn:Image { src = img.prev_disabled } local prev_btn = controls:Button {}
prev_btn:onClicked(queue.previous)
local play_pause_btn = controls:Button {} local prev_img = prev_btn:Image { src = img.prev_disabled }
play_pause_btn:onClicked(function()
playback.playing:set(not playback.playing:get()) local play_pause_btn = controls:Button {}
end) play_pause_btn:onClicked(function()
play_pause_btn:focus() playback.playing:set(not playback.playing:get())
local play_pause_img = play_pause_btn:Image { src = img.pause } end)
play_pause_btn:focus()
local next_btn = controls:Button {} local play_pause_img = play_pause_btn:Image { src = img.pause }
next_btn:onClicked(queue.next)
local next_img = next_btn:Image { src = img.next_disabled } local next_btn = controls:Button {}
next_btn:onClicked(queue.next)
local shuffle_btn = controls:Button {} local next_img = next_btn:Image { src = img.next_disabled }
shuffle_btn:onClicked(function()
queue.random:set(not queue.random:get()) local shuffle_btn = controls:Button {}
end) shuffle_btn:onClicked(function()
local shuffle_img = shuffle_btn:Image { src = img.shuffle } queue.random:set(not queue.random:get())
end)
controls:Object({ flex_grow = 1, h = 1 }) -- spacer 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 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 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 controls = require("controls")
local bluetooth = require("bluetooth") local bluetooth = require("bluetooth")
local database = require("database") local database = require("database")
local screen = require("screen")
local settings = {}
local function SettingsScreen(title) local function SettingsScreen(title)
local menu = widgets.MenuScreen { local menu = widgets.MenuScreen {
@ -31,323 +30,331 @@ local function SettingsScreen(title)
return menu return menu
end end
function settings.bluetooth() local BluetoothSettings = screen:new {
local menu = SettingsScreen("Bluetooth") createUi = function(self)
self.menu = SettingsScreen("Bluetooth")
local enable_container = menu.content:Object {
flex = { local enable_container = self.menu.content:Object {
flex_direction = "row", flex = {
justify_content = "flex-start", flex_direction = "row",
align_items = "content", justify_content = "flex-start",
align_content = "flex-start", align_items = "content",
}, align_content = "flex-start",
w = lvgl.PCT(100), },
h = lvgl.SIZE_CONTENT, w = lvgl.PCT(100),
pad_bottom = 1, h = lvgl.SIZE_CONTENT,
} pad_bottom = 1,
enable_container:Label { text = "Enable", flex_grow = 1 } }
local enable_sw = enable_container:Switch {} enable_container:Label { text = "Enable", flex_grow = 1 }
enable_sw:onevent(lvgl.EVENT.VALUE_CHANGED, function() local enable_sw = enable_container:Switch {}
local enabled = enable_sw:enabled() enable_sw:onevent(lvgl.EVENT.VALUE_CHANGED, function()
bluetooth.enabled:set(enabled) local enabled = enable_sw:enabled()
end) bluetooth.enabled:set(enabled)
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
end) end)
}
end
function settings.headphones() self.menu.content:Label {
local menu = SettingsScreen("Headphones") 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 { self.menu.content:Label {
text = "Maximum volume limit", text = "Nearby Devices",
}:add_style(theme.settings_title) 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 { self.menu.content:Label {
options = "Line Level (-10 dB)\nCD Level (+6 dB)\nMaximum (+10dB)", text = "Left/Right balance",
selected = 1, }:add_style(theme.settings_title)
}
local limits = { -10, 6, 10 } local balance = self.menu.content:Slider {
volume_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() w = lvgl.PCT(100),
-- luavgl dropdown binding uses 0-based indexing :( h = 5,
local selection = volume_chooser:get('selected') + 1 range = { min = -100, max = 100 },
volume.limit_db:set(limits[selection]) value = 0,
end) }
balance:onevent(lvgl.EVENT.VALUE_CHANGED, function()
menu.content:Label { volume.left_bias:set(balance:value())
text = "Left/Right balance", end)
}: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)
local balance_label = menu.content:Label {} local balance_label = self.menu.content:Label {}
menu.bindings = { self.bindings = {
volume.limit_db:bind(function(limit) volume.limit_db:bind(function(limit)
for i = 1, #limits do for i = 1, #limits do
if limits[i] == limit then if limits[i] == limit then
volume_chooser:set { selected = i - 1 } volume_chooser:set { selected = i - 1 }
end
end end
end end),
end), volume.left_bias:bind(function(bias)
volume.left_bias:bind(function(bias) balance:set {
balance:set { value = bias
value = bias
}
if bias < 0 then
balance_label:set {
text = string.format("Left %.2fdB", bias / 4)
} }
elseif bias > 0 then if bias < 0 then
balance_label:set { balance_label:set {
text = string.format("Right %.2fdB", -bias / 4) text = string.format("Left %.2fdB", bias / 4)
} }
else elseif bias > 0 then
balance_label:set { text = "Balanced" } balance_label:set {
end text = string.format("Right %.2fdB", -bias / 4)
end), }
} else
balance_label:set { text = "Balanced" }
return menu end
end end),
}
function settings.display() end
local menu = SettingsScreen("Display") }
local brightness_title = menu.content:Object { local DisplaySettings = screen:new {
flex = { createUi = function(self)
flex_direction = "row", self.menu = SettingsScreen("Display")
justify_content = "flex-start",
align_items = "flex-start", local brightness_title = self.menu.content:Object {
align_content = "flex-start", flex = {
}, flex_direction = "row",
w = lvgl.PCT(100), justify_content = "flex-start",
h = lvgl.SIZE_CONTENT, align_items = "flex-start",
} align_content = "flex-start",
brightness_title:Label { text = "Brightness", flex_grow = 1 } },
local brightness_pct = brightness_title:Label {} w = lvgl.PCT(100),
brightness_pct:add_style(theme.settings_title) h = lvgl.SIZE_CONTENT,
}
local brightness = menu.content:Slider { brightness_title:Label { text = "Brightness", flex_grow = 1 }
w = lvgl.PCT(100), local brightness_pct = brightness_title:Label {}
h = 5, brightness_pct:add_style(theme.settings_title)
range = { min = 0, max = 100 },
value = display.brightness:get(), local brightness = self.menu.content:Slider {
} w = lvgl.PCT(100),
brightness:onevent(lvgl.EVENT.VALUE_CHANGED, function() h = 5,
display.brightness:set(brightness:value()) range = { min = 0, max = 100 },
end) value = display.brightness:get(),
}
menu.bindings = { brightness:onevent(lvgl.EVENT.VALUE_CHANGED, function()
display.brightness:bind(function(b) display.brightness:set(brightness:value())
brightness_pct:set { text = tostring(b) .. "%" }
end) end)
}
return menu self.bindings = {
end display.brightness:bind(function(b)
brightness_pct:set { text = tostring(b) .. "%" }
end)
}
end
}
function settings.input() local InputSettings = screen:new {
local menu = SettingsScreen("Input Method") createUi = function(self)
self.menu = SettingsScreen("Input Method")
menu.content:Label { self.menu.content:Label {
text = "Control scheme", text = "Control scheme",
}:add_style(theme.settings_title) }:add_style(theme.settings_title)
local schemes = controls.schemes() local schemes = controls.schemes()
local option_to_scheme = {} local option_to_scheme = {}
local scheme_to_option = {} local scheme_to_option = {}
local option_idx = 0 local option_idx = 0
local options = "" local options = ""
for i, v in pairs(schemes) do for i, v in pairs(schemes) do
option_to_scheme[option_idx] = i option_to_scheme[option_idx] = i
scheme_to_option[i] = option_idx scheme_to_option[i] = option_idx
if option_idx > 0 then if option_idx > 0 then
options = options .. "\n" options = options .. "\n"
end
options = options .. v
option_idx = option_idx + 1
end end
options = options .. v
option_idx = option_idx + 1
end
local controls_chooser = menu.content:Dropdown {
options = options,
}
menu.bindings = { local controls_chooser = self.menu.content:Dropdown {
controls.scheme:bind(function(s) options = options,
local option = scheme_to_option[s] }
controls_chooser:set({ selected = option })
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) 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 { self.menu.content:Label {
w = lvgl.PCT(100), text = "Scroll Sensitivity",
h = lvgl.SIZE_CONTENT, }:add_style(theme.settings_title)
flex = {
flex_direction = "row", local slider_scale = 4; -- Power steering
justify_content = "center", local sensitivity = self.menu.content:Slider {
align_items = "space-evenly", w = lvgl.PCT(90),
align_content = "center", h = 5,
}, range = { min = 0, max = 255 / slider_scale },
pad_top = 4, value = controls.scroll_sensitivity:get() / slider_scale,
pad_column = 4, }
} sensitivity:onevent(lvgl.EVENT.VALUE_CHANGED, function()
actions_container:add_style(theme.list_item) controls.scroll_sensitivity:set(sensitivity:value() * slider_scale)
end)
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)
end end
}
local function submenu(name, fn)
local item = menu.list:add_btn(nil, name) local DatabaseSettings = screen:new {
item:onClicked(function() createUi = function(self)
backstack.push(fn) 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) end)
item:add_style(theme.list_item)
end 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") local LicensesScreen = screen:new {
submenu("Bluetooth", settings.bluetooth) createUi = function(self)
submenu("Headphones", settings.headphones) 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") local function submenu(name, class)
submenu("Display", settings.display) local item = self.list:add_btn(nil, name)
submenu("Input Method", settings.input) item:onClicked(function()
backstack.push(class:new())
end)
item:add_style(theme.list_item)
end
section("System") section("Audio")
submenu("Database", settings.database) submenu("Bluetooth", BluetoothSettings)
submenu("Firmware", settings.firmware) submenu("Headphones", HeadphonesSettings)
submenu("Licenses", function()
return require("licenses")()
end)
return menu section("Interface")
end 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( idf_component_register(
SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp" SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp"
"lua_queue.cpp" "lua_version.cpp" "lua_controls.cpp" "registry.cpp" "lua_queue.cpp" "lua_version.cpp" "lua_controls.cpp" "registry.cpp"
"lua_screen.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database"
"esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term" "esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term"

@ -19,6 +19,7 @@
#include "lua_controls.hpp" #include "lua_controls.hpp"
#include "lua_database.hpp" #include "lua_database.hpp"
#include "lua_queue.hpp" #include "lua_queue.hpp"
#include "lua_screen.hpp"
#include "lua_version.hpp" #include "lua_version.hpp"
#include "lvgl.h" #include "lvgl.h"
@ -84,6 +85,7 @@ auto Bridge::installBaseModules(lua_State* L) -> void {
RegisterDatabaseModule(L); RegisterDatabaseModule(L);
RegisterQueueModule(L); RegisterQueueModule(L);
RegisterVersionModule(L); RegisterVersionModule(L);
RegisterScreenModule(L);
} }
auto Bridge::installLvgl(lua_State* L) -> void { 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(); Screen();
virtual ~Screen(); virtual ~Screen();
virtual auto onShown() -> void {}
virtual auto onHidden() -> void {}
auto root() -> lv_obj_t* { return root_; } auto root() -> lv_obj_t* { return root_; }
auto content() -> lv_obj_t* { return content_; } auto content() -> lv_obj_t* { return content_; }
auto alert() -> lv_obj_t* { return alert_; } auto alert() -> lv_obj_t* { return alert_; }

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

@ -7,8 +7,10 @@
#include "screen_lua.hpp" #include "screen_lua.hpp"
#include "core/lv_obj_tree.h" #include "core/lv_obj_tree.h"
#include "lua.h"
#include "lua.hpp" #include "lua.hpp"
#include "lua_thread.hpp"
#include "luavgl.h" #include "luavgl.h"
namespace ui { 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 { auto Lua::SetObjRef(lua_State* s) -> void {
assert(s_ == nullptr); assert(s_ == nullptr);
s_ = s; s_ = s;

@ -125,21 +125,24 @@ lua::Property UiState::sPlaybackPlaying{
}}; }};
lua::Property UiState::sPlaybackTrack{}; lua::Property UiState::sPlaybackTrack{};
lua::Property UiState::sPlaybackPosition{0, [](const lua::LuaValue& val) { lua::Property UiState::sPlaybackPosition{
int current_val = std::get<int>(sPlaybackPosition.Get()); 0, [](const lua::LuaValue& val) {
if (!std::holds_alternative<int>(val)) { int current_val = std::get<int>(sPlaybackPosition.Get());
return false; if (!std::holds_alternative<int>(val)) {
}
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; return false;
} }
events::Audio().Dispatch(audio::SeekFile{.offset = (uint32_t)new_val, .filename = std::get<audio::Track>(track).filepath}); int new_val = std::get<int>(val);
} if (current_val != new_val) {
return true; 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::sQueuePosition{0};
lua::Property UiState::sQueueSize{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) { void UiState::PushScreen(std::shared_ptr<Screen> screen) {
lv_obj_set_parent(sAlertContainer, screen->alert());
if (sCurrentScreen) { if (sCurrentScreen) {
sCurrentScreen->onHidden();
sScreens.push(sCurrentScreen); sScreens.push(sCurrentScreen);
} }
sCurrentScreen = screen; sCurrentScreen = screen;
lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert()); sCurrentScreen->onShown();
} }
int UiState::PopScreen() { int UiState::PopScreen() {
if (sScreens.empty()) { if (sScreens.empty()) {
return 0; return 0;
} }
sCurrentScreen = sScreens.top(); lv_obj_set_parent(sAlertContainer, sScreens.top()->alert());
lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert());
sCurrentScreen->onHidden();
sCurrentScreen = sScreens.top();
sScreens.pop(); sScreens.pop();
sCurrentScreen->onShown();
return sScreens.size(); return sScreens.size();
} }
@ -539,7 +550,7 @@ void Lua::entry() {
auto Lua::PushLuaScreen(lua_State* s) -> int { auto Lua::PushLuaScreen(lua_State* s) -> int {
// Ensure the arg looks right before continuing. // 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 // 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 // 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()); lv_group_set_default(new_screen->group());
// Call the constructor for this screen. // Call the constructor for this screen.
lua_settop(s, 1); // Make sure the function is actually at top of stack // lua_settop(s, 1); // Make sure the screen is actually at top of stack
lua::CallProtected(s, 0, 1); 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); new_screen->SetObjRef(s);
// Finally, push the now-initialised screen as if it were a regular C++ // 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 { 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; return 1;
} }

Loading…
Cancel
Save