feat(launcher): wire panels into master-detail layout
Replaces the centered-button list with a 1/3 list + 2/3 detail split driven by panels/list.lua and panels/detail.lua. The shared ctx_panels table threads UI state (selected_entry, scroll_y, modal_open, etc.) through both panels. Existing dep + update checks remain on the module entries; FSM and ESC-quit flow unchanged. Detail-panel content is still a stub until L.4-L.7.
This commit is contained in:
103
init.lua
103
init.lua
@@ -8,10 +8,17 @@ local MODULE_DIR = engine.module.dir_of("lib-management.launcher")
|
|||||||
local fsm = dofile(MODULE_DIR .. "/fsm.lua")
|
local fsm = dofile(MODULE_DIR .. "/fsm.lua")
|
||||||
local icons = dofile(MODULE_DIR .. "/icons.lua")
|
local icons = dofile(MODULE_DIR .. "/icons.lua")
|
||||||
|
|
||||||
|
local function require_panel(name)
|
||||||
|
return dofile(MODULE_DIR .. "/panels/" .. name .. ".lua")
|
||||||
|
end
|
||||||
|
|
||||||
|
local list_panel = require_panel("list")
|
||||||
|
local detail_panel = require_panel("detail")
|
||||||
|
|
||||||
local STATE_LIST, STATE_QUIT, EXIT = fsm.STATE_LIST, fsm.STATE_QUIT_CONFIRM, fsm.EXIT
|
local STATE_LIST, STATE_QUIT, EXIT = fsm.STATE_LIST, fsm.STATE_QUIT_CONFIRM, fsm.EXIT
|
||||||
local state = STATE_LIST
|
local state = STATE_LIST
|
||||||
|
|
||||||
local modules = {} -- per-entry: see init() below
|
local ctx_panels = nil -- built in init(); shared state for both panels
|
||||||
local quit_buttons = {} -- {yes = rect, no = rect}
|
local quit_buttons = {} -- {yes = rect, no = rect}
|
||||||
local screen_w, screen_h
|
local screen_w, screen_h
|
||||||
local gitea_base = nil -- resolved in init() from engine.config
|
local gitea_base = nil -- resolved in init() from engine.config
|
||||||
@@ -229,27 +236,36 @@ function init(ctx)
|
|||||||
git.set_token(cfg_token)
|
git.set_token(cfg_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
local list = engine.module.list()
|
ctx_panels = {
|
||||||
|
-- Persistent UI context, threaded through the panels.
|
||||||
|
module_entries = {}, -- filled below from engine.module.list
|
||||||
|
news_entry = { title = "News", body_path = "news.md" },
|
||||||
|
selected_entry = nil,
|
||||||
|
scroll_y = 0,
|
||||||
|
list_tint = nil, -- nil | "warn" | "error" — set in L.4
|
||||||
|
list_panel_rect = nil, -- set below from window size
|
||||||
|
detail_panel_rect = nil,
|
||||||
|
bg_texture = nil, -- L.10 loads it
|
||||||
|
default_teaser_texture = nil, -- L.10 loads it
|
||||||
|
t = function(k) return k end, -- L.8 swaps in real t()
|
||||||
|
modal_open = nil, -- L.7 sets to "quit" | "conflict"
|
||||||
|
carousel_state = {}, -- per-module-id state, L.5 populates
|
||||||
|
manifest_cache = {}, -- L.3 populates
|
||||||
|
}
|
||||||
|
|
||||||
-- Uniform button width: max label + padding + reserved icon strip.
|
-- Window split: 1/3 list, 2/3 detail. Margin 12px between panels.
|
||||||
local max_label_w = 0
|
local SW, SH = screen_w, screen_h
|
||||||
for _, m in ipairs(list) do
|
ctx_panels.list_panel_rect = { x = 0, y = 0, w = SW / 3, h = SH }
|
||||||
local label = m.name or m.id
|
ctx_panels.detail_panel_rect = { x = SW / 3 + 12, y = 0, w = SW * 2/3 - 12, h = SH }
|
||||||
local lw = engine.render.measure_text(label, FONT_BUTTON)
|
|
||||||
if lw > max_label_w then max_label_w = lw end
|
|
||||||
end
|
|
||||||
local _, label_h = engine.render.measure_text("Ay", FONT_BUTTON)
|
|
||||||
local btn_w = max_label_w + 2 * PAD_X + ICON_STRIP_W
|
|
||||||
local btn_h = label_h + 2 * PAD_Y
|
|
||||||
|
|
||||||
local y = 80
|
local raw_list = engine.module.list() or {}
|
||||||
for i, m in ipairs(list) do
|
for i, m in ipairs(raw_list) do
|
||||||
modules[i] = {
|
ctx_panels.module_entries[i] = {
|
||||||
id = m.id,
|
id = m.id,
|
||||||
version = m.version,
|
version = m.version,
|
||||||
name = m.name or m.id,
|
name = m.name or m.id,
|
||||||
rect = { x = (screen_w - btn_w) / 2, y = y,
|
summary = nil, -- L.3 fills from manifest.launcher.md
|
||||||
w = btn_w, h = btn_h },
|
rect = nil,
|
||||||
status = {
|
status = {
|
||||||
update_check_handle = nil,
|
update_check_handle = nil,
|
||||||
update_check_result = nil,
|
update_check_result = nil,
|
||||||
@@ -261,7 +277,6 @@ function init(ctx)
|
|||||||
local_rect = nil,
|
local_rect = nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
y = y + btn_h + 8
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local wy, hy = engine.render.measure_text(YES_LABEL, FONT_BUTTON)
|
local wy, hy = engine.render.measure_text(YES_LABEL, FONT_BUTTON)
|
||||||
@@ -279,16 +294,17 @@ function init(ctx)
|
|||||||
-- Kick off async update-checks (one ls-remote per module). Network
|
-- Kick off async update-checks (one ls-remote per module). Network
|
||||||
-- calls; gated off in CI to keep milestone-check offline.
|
-- calls; gated off in CI to keep milestone-check offline.
|
||||||
if os.getenv("SPOREL_CI") ~= "1" then
|
if os.getenv("SPOREL_CI") ~= "1" then
|
||||||
for _, m in ipairs(modules) do start_update_check(m) end
|
for _, m in ipairs(ctx_panels.module_entries) do start_update_check(m) end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Pre-emptive dep-check per module. Sync today (plan §7.6 future
|
-- Pre-emptive dep-check per module. Sync today (plan §7.6 future
|
||||||
-- work: async). In a packaged install with closures already
|
-- work: async). In a packaged install with closures already
|
||||||
-- present, this is filesystem-only and fast; the slow path
|
-- present, this is filesystem-only and fast; the slow path
|
||||||
-- (missing libs → clone) only fires on first launch / new module.
|
-- (missing libs → clone) only fires on first launch / new module.
|
||||||
for _, m in ipairs(modules) do run_dep_check(m) end
|
for _, m in ipairs(ctx_panels.module_entries) do run_dep_check(m) end
|
||||||
|
|
||||||
engine.print(string.format("launcher: init ok, modules=%d", #modules))
|
engine.print(string.format("launcher: init ok, modules=%d",
|
||||||
|
#ctx_panels.module_entries))
|
||||||
end
|
end
|
||||||
|
|
||||||
function update(ctx, dt)
|
function update(ctx, dt)
|
||||||
@@ -299,7 +315,7 @@ function update(ctx, dt)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Poll async update-check handles.
|
-- Poll async update-check handles.
|
||||||
for _, m in ipairs(modules) do
|
for _, m in ipairs(ctx_panels.module_entries) do
|
||||||
local h = m.status.update_check_handle
|
local h = m.status.update_check_handle
|
||||||
if h and git.is_done(h) then
|
if h and git.is_done(h) then
|
||||||
consume_update_check(m)
|
consume_update_check(m)
|
||||||
@@ -312,23 +328,8 @@ function update(ctx, dt)
|
|||||||
if input.was_action_pressed("ui_back") then
|
if input.was_action_pressed("ui_back") then
|
||||||
state = fsm.next(state, "esc")
|
state = fsm.next(state, "esc")
|
||||||
elseif input.was_action_pressed("ui_click") then
|
elseif input.was_action_pressed("ui_click") then
|
||||||
for _, m in ipairs(modules) do
|
if not list_panel.handle_click(ctx_panels, mx, my) then
|
||||||
-- Badge first — its hit-rect lives inside the button,
|
detail_panel.handle_click(ctx_panels, mx, my)
|
||||||
-- so without prioritising it the launch path would
|
|
||||||
-- always win.
|
|
||||||
if m.status.badge_rect and hit(m.status.badge_rect, mx, my) then
|
|
||||||
apply_update(m)
|
|
||||||
return
|
|
||||||
elseif hit(m.rect, mx, my) then
|
|
||||||
local r = m.status.dep_check_result
|
|
||||||
if r and r.ok then
|
|
||||||
engine.switch_module(m.id)
|
|
||||||
else
|
|
||||||
engine.print("launcher: " .. m.id
|
|
||||||
.. " blocked by dep-check")
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif state == STATE_QUIT then
|
elseif state == STATE_QUIT then
|
||||||
@@ -411,29 +412,9 @@ function render(ctx)
|
|||||||
local mx, my = engine.input.get_mouse_pos()
|
local mx, my = engine.input.get_mouse_pos()
|
||||||
|
|
||||||
engine.render.clear_color(COLOR_BG)
|
engine.render.clear_color(COLOR_BG)
|
||||||
local tw = engine.render.measure_text(TITLE, FONT_TITLE)
|
|
||||||
engine.render.draw_text(TITLE, (screen_w - tw) / 2, 24,
|
|
||||||
FONT_TITLE, COLOR_TEXT)
|
|
||||||
|
|
||||||
if #modules == 0 then
|
list_panel.render(ctx_panels)
|
||||||
local ew = engine.render.measure_text(EMPTY_LABEL, FONT_BUTTON)
|
detail_panel.render(ctx_panels)
|
||||||
engine.render.draw_text(EMPTY_LABEL, (screen_w - ew) / 2,
|
|
||||||
screen_h / 2, FONT_BUTTON, COLOR_TEXT)
|
|
||||||
else
|
|
||||||
for _, m in ipairs(modules) do
|
|
||||||
draw_module_row(m)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Tooltip: detected after all rows have populated their rects.
|
|
||||||
local tip = nil
|
|
||||||
for _, m in ipairs(modules) do
|
|
||||||
tip = tooltip_for(m, mx, my)
|
|
||||||
if tip then break end
|
|
||||||
end
|
|
||||||
if tip then
|
|
||||||
icons.draw_tooltip(mx, my, tip, screen_w, screen_h)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if state == STATE_QUIT then
|
if state == STATE_QUIT then
|
||||||
engine.render.draw_rect(0, 0, screen_w, screen_h, COLOR_DIM)
|
engine.render.draw_rect(0, 0, screen_w, screen_h, COLOR_DIM)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "lib-management.launcher",
|
"id": "lib-management.launcher",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"kind": "module",
|
"kind": "module",
|
||||||
"api": "^0.1",
|
"api": "^0.1",
|
||||||
"ci_frames": 10,
|
"ci_frames": 10,
|
||||||
|
|||||||
Reference in New Issue
Block a user