1
1
mirror of https://github.com/NixOS/mobile-nixos.git synced 2024-12-17 21:11:34 +03:00
mobile-nixos/boot/gui/main.rb
2020-03-08 17:05:15 -04:00

312 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()
now = Time.now
set_text([
:hour,
:min,
:sec,
].map{|fn| now.send(fn).to_s.rjust(2, "0") }.join(":"))
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(" ")
system(*cmd) unless LVGL::Hacks.simulator?
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"])
# Put back the console on the framebuffer
VTConsole.map_console(1)
exit 0
end
end
end
end
end
# Main loop
while true
LVGL::Hacks::LVTask.handle_tasks
sleep(1.0/REFRESH_RATE)
end