mirror of
https://github.com/NixOS/mobile-nixos.git
synced 2024-12-17 21:11:34 +03:00
5e88e7efcf
This should *maybe* remove the Generations tab, as it's empty.
310 lines
7.8 KiB
Ruby
310 lines
7.8 KiB
Ruby
# Refreshing at 120 times per second *really* helps with the drag operations
|
|
# responsiveness. At 60 it feels a bit sluggish.
|
|
# This likely comes from the naïve implementation that we are not refreshing at
|
|
# 60 times per seconds, but rather, refresh and wait 1/60th of a second. This
|
|
# makes the refresh rate a tad slower.
|
|
# Boosting to 120 doesn't seem to have ill effects. It's simply refreshed more.
|
|
REFRESH_RATE = 120
|
|
|
|
# UI constants
|
|
NIXOS_LIGHT_HUE = 205
|
|
NIXOS_DARK_HUE = 220
|
|
|
|
# File with boot selection
|
|
SELECTIONS = "/run/boot/selection.json"
|
|
|
|
# Define the arguments
|
|
Args.define({
|
|
resolution: nil,
|
|
})
|
|
|
|
# This is used by the "simulator".
|
|
if Args.get(:resolution)
|
|
pair = Args.get(:resolution).split("x")
|
|
unless pair.length == 2
|
|
$stderr.puts "--resolution <width>x<height>"
|
|
exit 2
|
|
end
|
|
LVGL::Hacks.monitor_width = pair.first.to_i
|
|
LVGL::Hacks.monitor_height = pair.last.to_i
|
|
else
|
|
LVGL::Hacks.monitor_width = 720
|
|
LVGL::Hacks.monitor_height = 1280
|
|
end
|
|
|
|
# Get exclusive control of the framebuffer
|
|
VTConsole.map_console(0)
|
|
|
|
# Prepare LVGL
|
|
LVGL::Hacks.init()
|
|
# And switch to the desired theme
|
|
LVGL::Hacks.theme_night(NIXOS_LIGHT_HUE)
|
|
|
|
# Unsightly hacks {{{
|
|
|
|
# Dummy container to get its style.
|
|
LVGL::LVContainer.new.tap do |container|
|
|
style = container.get_style(LVGL::CONT_STYLE::MAIN)
|
|
# TODO: Determine what this "constant" is
|
|
# This ends up being different depending on the generations, probably the DPI
|
|
# This is used to "fix" some layouting issues where filling fails.
|
|
# 1280 == 10
|
|
# 1920 == 15
|
|
$fix_padding = style.body_padding_inner
|
|
end
|
|
|
|
# }}}
|
|
|
|
# Wraps an LVGL widget.
|
|
class Widget
|
|
def initialize(widget)
|
|
@widget = widget
|
|
end
|
|
def method_missing(*args)
|
|
@widget.send(*args)
|
|
end
|
|
end
|
|
|
|
# Implements a clock as a wrapped LVLabel.
|
|
class Clock < Widget
|
|
def initialize(parent)
|
|
super(LVGL::LVLabel.new(parent))
|
|
set_align(LVGL::LABEL_ALIGN::LEFT)
|
|
set_long_mode(LVGL::LABEL_LONG::CROP)
|
|
|
|
# Update the text once
|
|
update_clock
|
|
|
|
# Then register a task to update regularly.
|
|
@task = LVGL::Hacks::LVTask.create_task(250, LVGL::TASK_PRIO::MID, ->() do
|
|
update_clock
|
|
end)
|
|
end
|
|
|
|
def update_clock()
|
|
set_text(Time.now.strftime("%T"))
|
|
end
|
|
end
|
|
|
|
# Big ball of code to build the UI.
|
|
# Should be refactored in discrete "Widget" things.
|
|
class UI
|
|
attr_reader :container
|
|
def initialize()
|
|
screen
|
|
header
|
|
logo
|
|
container
|
|
end
|
|
|
|
def screen()
|
|
@screen = LVGL::LVContainer.new()
|
|
#@screen.set_fit2(LVGL::FIT::FILL, LVGL::FIT::TIGHT)
|
|
@screen.set_layout(LVGL::LAYOUT::COL_M)
|
|
|
|
style = @screen.get_style(LVGL::CONT_STYLE::MAIN).dup
|
|
@screen.set_style(LVGL::CONT_STYLE::MAIN, style)
|
|
style.body_padding_top = 0
|
|
style.body_padding_left = 0
|
|
style.body_padding_right = 0
|
|
style.body_padding_bottom = 0
|
|
end
|
|
|
|
def container()
|
|
@tabview = LVGL::LVTabview.new(@screen)
|
|
@tabview.set_sliding(false)
|
|
@tabview.set_anim_time(0)
|
|
|
|
tab_style = @tabview.get_style(LVGL::TABVIEW_STYLE::BG).dup
|
|
@tabview.set_style(LVGL::TABVIEW_STYLE::BG, tab_style)
|
|
#tab_style.body_main_color = 0xFF0000FF
|
|
#tab_style.body_grad_color = 0xFF0000FF
|
|
|
|
@tabs = {}
|
|
@tabs[:default] = @tabview.add_tab("Boot options")
|
|
@tabs[:generations] = @tabview.add_tab("Generations")
|
|
|
|
@tabs.each do |id, tab|
|
|
page = LVGL::LVPage.new(tab)
|
|
|
|
# It seems tabview auto-height is broken :/
|
|
# This assumes there is nothing *after* the tabview.
|
|
@tabview.set_height(
|
|
@screen.get_height - @tabview.get_y
|
|
)
|
|
|
|
# "tabview" filling is seemingly broken...
|
|
page.set_width(
|
|
tab.get_width_fit - page.get_x*2
|
|
)
|
|
page.set_height(
|
|
tab.get_height_fit - page.get_y -
|
|
tab.get_style(LVGL::CONT_STYLE::MAIN).body_padding_inner - $fix_padding
|
|
)
|
|
style = LVGL::LVStyle::STYLE_TRANSP.dup
|
|
style.body_padding_top = 0
|
|
style.body_padding_left = 0
|
|
style.body_padding_right = 0
|
|
style.body_padding_bottom = 0
|
|
#style.body_main_color = 0xFFFF0000
|
|
#style.body_grad_color = 0xFFFF0000
|
|
page.set_style(LVGL::PAGE_STYLE::BG, style)
|
|
page.set_style(LVGL::PAGE_STYLE::SCRL, style)
|
|
page.set_scrl_layout(LVGL::LAYOUT::COL_M)
|
|
|
|
instance_variable_set("@#{id}_page".to_sym, page)
|
|
end
|
|
end
|
|
|
|
def header()
|
|
@header = LVGL::LVContainer.new(@screen)
|
|
|
|
header_style = @header.get_style(LVGL::CONT_STYLE::MAIN).dup
|
|
@header.set_style(LVGL::CONT_STYLE::MAIN, header_style)
|
|
header_style.glass = 1
|
|
header_style.body_radius = 0
|
|
#header_style.body_main_color = 0xFF0000FF
|
|
#header_style.body_grad_color = 0xFF0000FF
|
|
|
|
@header.set_fit2(LVGL::FIT::FILL, LVGL::FIT::TIGHT)
|
|
@header.set_layout(LVGL::LAYOUT::PRETTY)
|
|
|
|
# Split 50/50
|
|
child_width = (
|
|
@screen.get_width -
|
|
header_style.body_padding_left -
|
|
header_style.body_padding_right -
|
|
header_style.body_padding_inner*2
|
|
) / 2
|
|
|
|
@clock = Clock.new(@header)
|
|
@clock.set_width(child_width)
|
|
|
|
@tagline = LVGL::LVLabel.new(@header)
|
|
@tagline.set_text("Recovery Menu")
|
|
@tagline.set_long_mode(LVGL::LABEL_LONG::SROLL_CIRC)
|
|
@tagline.set_align(LVGL::LABEL_ALIGN::RIGHT)
|
|
@tagline.set_width(child_width)
|
|
end
|
|
|
|
def logo()
|
|
file = nil
|
|
file = "/etc/logo.svg" if File.exist?("/etc/logo.svg")
|
|
file = "./logo.svg" if File.exist?("./logo.svg")
|
|
unless file
|
|
return
|
|
end
|
|
if @screen.get_height > @screen.get_width
|
|
LVGL::LVNanoSVG.resize_next_width((@screen.get_width_fit * 0.8).to_i)
|
|
else
|
|
# Detecting the available space where the layout will stretch into is
|
|
# apparently hard with lvgl, thus we rely on the vertical resolution.
|
|
# Meh, that's not *so* bad.
|
|
# While it's a crude approximation, for layouting it's fine.
|
|
LVGL::LVNanoSVG.resize_next_height((@screen.get_height * 0.15).to_i)
|
|
end
|
|
|
|
@logo = LVGL::LVImage.new(@screen)
|
|
@logo.set_src(file)
|
|
end
|
|
|
|
def button(label, page: nil)
|
|
if page
|
|
page = instance_variable_get("@#{page}_page".to_sym)
|
|
else
|
|
page = @default_page
|
|
end
|
|
|
|
btn = LVGL::LVButton.new(page)
|
|
btn.set_layout(LVGL::LAYOUT::COL_M)
|
|
btn.set_ink_in_time(200)
|
|
btn.set_ink_wait_time(100)
|
|
btn.set_ink_out_time(500)
|
|
btn.set_fit2(LVGL::FIT::FILL, LVGL::FIT::TIGHT)
|
|
btn.glue_obj(true)
|
|
|
|
LVGL::LVLabel.new(btn).tap do |obj|
|
|
obj.set_text(label)
|
|
end
|
|
|
|
btn
|
|
end
|
|
end
|
|
|
|
# Create the UI
|
|
ui = UI.new
|
|
|
|
def run(*cmd)
|
|
$stderr.puts " $ " + cmd.join(" ")
|
|
# TODO: better introspection to allow the app to know it is running in a
|
|
# simulated environment, and dry-run in simulator.
|
|
system(*cmd)
|
|
end
|
|
|
|
# TODO: wait ~0.3s for the animation before doing the button actions.
|
|
# Otherwise, it looks jarring.
|
|
|
|
# Default tab
|
|
|
|
ui.button("Reboot to bootloader").tap do |btn|
|
|
btn.event_handler = ->(event) do
|
|
case event
|
|
when LVGL::EVENT::CLICKED
|
|
run("reboot bootloader")
|
|
end
|
|
end
|
|
end
|
|
|
|
ui.button("Reboot to recovery").tap do |btn|
|
|
btn.event_handler = ->(event) do
|
|
case event
|
|
when LVGL::EVENT::CLICKED
|
|
run("reboot recovery")
|
|
end
|
|
end
|
|
end
|
|
|
|
ui.button("Reboot to system").tap do |btn|
|
|
btn.event_handler = ->(event) do
|
|
case event
|
|
when LVGL::EVENT::CLICKED
|
|
run("reboot")
|
|
end
|
|
end
|
|
end
|
|
|
|
ui.button("Power off").tap do |btn|
|
|
btn.event_handler = ->(event) do
|
|
case event
|
|
when LVGL::EVENT::CLICKED
|
|
run("poweroff")
|
|
end
|
|
end
|
|
end
|
|
|
|
# Generations tab
|
|
|
|
if File.exist?(SELECTIONS)
|
|
JSON.parse(File.read(SELECTIONS)).each do |selection|
|
|
ui.button(selection["name"], page: :generations).tap do |btn|
|
|
btn.event_handler = ->(event) do
|
|
case event
|
|
when LVGL::EVENT::CLICKED
|
|
File.write("/run/boot/choice", selection["id"])
|
|
exit 0
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Main loop
|
|
while true
|
|
LVGL::Hacks::LVTask.handle_tasks
|
|
sleep(1.0/REFRESH_RATE)
|
|
# TODO : Allow exiting!
|
|
end
|
|
|
|
# Put back the console on the framebuffer
|
|
VTConsole.map_console(1)
|