diff --git a/init.lua b/init.lua index a0d1049..e85dac6 100644 --- a/init.lua +++ b/init.lua @@ -8,10 +8,17 @@ local MODULE_DIR = engine.module.dir_of("lib-management.launcher") local fsm = dofile(MODULE_DIR .. "/fsm.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 = 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 screen_w, screen_h local gitea_base = nil -- resolved in init() from engine.config @@ -229,27 +236,36 @@ function init(ctx) git.set_token(cfg_token) 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. - local max_label_w = 0 - for _, m in ipairs(list) do - local label = m.name or m.id - 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 + -- Window split: 1/3 list, 2/3 detail. Margin 12px between panels. + local SW, SH = screen_w, screen_h + ctx_panels.list_panel_rect = { x = 0, y = 0, w = SW / 3, h = SH } + ctx_panels.detail_panel_rect = { x = SW / 3 + 12, y = 0, w = SW * 2/3 - 12, h = SH } - local y = 80 - for i, m in ipairs(list) do - modules[i] = { + local raw_list = engine.module.list() or {} + for i, m in ipairs(raw_list) do + ctx_panels.module_entries[i] = { id = m.id, version = m.version, name = m.name or m.id, - rect = { x = (screen_w - btn_w) / 2, y = y, - w = btn_w, h = btn_h }, + summary = nil, -- L.3 fills from manifest.launcher.md + rect = nil, status = { update_check_handle = nil, update_check_result = nil, @@ -261,7 +277,6 @@ function init(ctx) local_rect = nil, }, } - y = y + btn_h + 8 end 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 -- calls; gated off in CI to keep milestone-check offline. 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 -- Pre-emptive dep-check per module. Sync today (plan ยง7.6 future -- work: async). In a packaged install with closures already -- present, this is filesystem-only and fast; the slow path -- (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 function update(ctx, dt) @@ -299,7 +315,7 @@ function update(ctx, dt) end -- 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 if h and git.is_done(h) then consume_update_check(m) @@ -312,23 +328,8 @@ function update(ctx, dt) if input.was_action_pressed("ui_back") then state = fsm.next(state, "esc") elseif input.was_action_pressed("ui_click") then - for _, m in ipairs(modules) do - -- Badge first โ€” its hit-rect lives inside the button, - -- 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 + if not list_panel.handle_click(ctx_panels, mx, my) then + detail_panel.handle_click(ctx_panels, mx, my) end end elseif state == STATE_QUIT then @@ -411,29 +412,9 @@ function render(ctx) local mx, my = engine.input.get_mouse_pos() 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 - local ew = engine.render.measure_text(EMPTY_LABEL, FONT_BUTTON) - 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 + list_panel.render(ctx_panels) + detail_panel.render(ctx_panels) if state == STATE_QUIT then engine.render.draw_rect(0, 0, screen_w, screen_h, COLOR_DIM) diff --git a/manifest.module b/manifest.module index 38552a1..c944611 100644 --- a/manifest.module +++ b/manifest.module @@ -1,6 +1,6 @@ { "id": "lib-management.launcher", - "version": "0.2.0", + "version": "0.3.0", "kind": "module", "api": "^0.1", "ci_frames": 10,