feat(launcher): render detail panel content (title/desc/deps)
panels/detail.lua now renders the selected module's title, version, author, markdown description, and per-dep status list with OK/WARN/ERR glyphs. Dep-list order follows manifest.module::deps[] (L-Q6 load- order, no re-sort). Whole-list bg-tint is bubbled to ctx.list_tint so the list panel picks up a yellow/red ambient cue for warn/error states. Hero strip remains a placeholder until L.5.
This commit is contained in:
54
init.lua
54
init.lua
@@ -123,21 +123,57 @@ local function consume_update_check(m)
|
||||
end
|
||||
end
|
||||
|
||||
-- Composes a per-dep status list keyed by lib_id from the dep-fetcher
|
||||
-- result's `errors` and `warnings` arrays. Each input dep entry is
|
||||
-- `{id, version}` (from manifest.module::deps[]); each output entry
|
||||
-- adds `status = "ok" | "warn" | "error"`. Load-order preserved
|
||||
-- (L-Q6: no re-sort).
|
||||
local function compose_dep_status_list(result, module_deps)
|
||||
local out = {}
|
||||
local err_ids, warn_ids = {}, {}
|
||||
for _, e in ipairs(result.errors or {}) do
|
||||
if e.lib_id then err_ids[e.lib_id] = true end
|
||||
end
|
||||
for _, w in ipairs(result.warnings or {}) do
|
||||
if w.lib_id then warn_ids[w.lib_id] = true end
|
||||
end
|
||||
for _, c in ipairs(result.conflicts or {}) do
|
||||
if c.lib_id then err_ids[c.lib_id] = true end
|
||||
end
|
||||
for _, d in ipairs(module_deps) do
|
||||
local status = "ok"
|
||||
if err_ids[d.id] then status = "error"
|
||||
elseif warn_ids[d.id] then status = "warn"
|
||||
end
|
||||
out[#out + 1] = { id = d.id, version = d.version, status = status }
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- Derives a roll-up kind ("ok" | "warn" | "error") from the dep-fetcher
|
||||
-- result. Drives the list-panel's bg-tint cue via ctx.list_tint.
|
||||
local function derive_result_kind(result)
|
||||
if not result.ok then return "error" end
|
||||
if (result.warnings and #result.warnings > 0) then return "warn" end
|
||||
return "ok"
|
||||
end
|
||||
|
||||
-- Lazy: only re-check dep-fetcher when the consumer flow asks for it
|
||||
-- (init + after-update). Heavy-lift in init() is acceptable per the
|
||||
-- plan; an async dep-check is future work.
|
||||
local function run_dep_check(m)
|
||||
local ok, result = pcall(depf.ensure_for_module, m.id)
|
||||
if ok and type(result) == "table" then
|
||||
m.status.dep_check_result = result
|
||||
else
|
||||
m.status.dep_check_result = {
|
||||
if not (ok and type(result) == "table") then
|
||||
result = {
|
||||
ok = false, conflicts = {}, warnings = {},
|
||||
errors = {{ lib_id = "<launcher>", kind = "internal",
|
||||
message = tostring(result) }},
|
||||
closure = {},
|
||||
}
|
||||
end
|
||||
result.deps = compose_dep_status_list(result, m._raw_deps or {})
|
||||
result.kind = derive_result_kind(result)
|
||||
m.status.dep_check_result = result
|
||||
end
|
||||
|
||||
-- ----- lifecycle -----------------------------------------------------
|
||||
@@ -172,6 +208,7 @@ function init(ctx)
|
||||
modal_open = nil, -- L.7 sets to "quit" | "conflict"
|
||||
carousel_state = {}, -- per-module-id state, L.5 populates
|
||||
manifest_cache = {}, -- L.3 populates
|
||||
launcher_dir = MODULE_DIR, -- L.4: detail.lua dofiles markdown.lua via this
|
||||
}
|
||||
|
||||
-- Window split: 1/3 list, 2/3 detail. Margin 12px between panels.
|
||||
@@ -207,6 +244,15 @@ function init(ctx)
|
||||
local meta, blocks = manifest_loader.load_for(entry.id)
|
||||
ctx_panels.manifest_cache[entry.id] = { meta = meta, blocks = blocks }
|
||||
entry.summary = meta.summary
|
||||
|
||||
-- L.4: also surface manifest.module::deps[] verbatim so the
|
||||
-- detail-panel can render per-dep status glyphs in load order
|
||||
-- (no re-sort, per L-Q6).
|
||||
local raw, _ = engine.module.read_file(entry.id, "manifest.module")
|
||||
if raw then
|
||||
local data = depf._json_decode(raw)
|
||||
if data and data.deps then entry._raw_deps = data.deps end
|
||||
end
|
||||
end
|
||||
|
||||
local wy, hy = engine.render.measure_text(YES_LABEL, FONT_BUTTON)
|
||||
|
||||
@@ -1,36 +1,116 @@
|
||||
local M = {}
|
||||
|
||||
local PADDING = 24
|
||||
local COLOR_BG = 0x282828FF
|
||||
local COLOR_TEXT = 0xE0E0E0FF
|
||||
local COLOR_DIM = 0xA0A0A0FF
|
||||
local FONT_TITLE = 28
|
||||
local FONT_BODY = 14
|
||||
local PADDING = 24
|
||||
local COLOR_BG = 0x282828FF
|
||||
local COLOR_TEXT = 0xE0E0E0FF
|
||||
local COLOR_DIM = 0xA0A0A0FF
|
||||
local COLOR_OK = 0x80C080FF
|
||||
local COLOR_WARN = 0xE0C040FF
|
||||
local COLOR_ERR = 0xE05040FF
|
||||
local FONT_TITLE = 28
|
||||
local FONT_SUB = 16
|
||||
local FONT_BODY = 14
|
||||
local FONT_DEP = 14
|
||||
|
||||
local md -- lazy-loaded markdown renderer
|
||||
|
||||
local function ensure_md(launcher_dir)
|
||||
if not md then md = dofile(launcher_dir .. "/markdown.lua") end
|
||||
end
|
||||
|
||||
local function status_glyph(dep_status)
|
||||
if dep_status == "ok" then return "OK", COLOR_OK end
|
||||
if dep_status == "warn" then return "WARN", COLOR_WARN end
|
||||
if dep_status == "error" then return "ERR", COLOR_ERR end
|
||||
return "?", COLOR_DIM
|
||||
end
|
||||
|
||||
local function find_entry(ctx, id)
|
||||
if id == "news" then return nil end
|
||||
for _, e in ipairs(ctx.module_entries) do
|
||||
if e.id == id then return e end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function M.render(ctx)
|
||||
local p = ctx.detail_panel_rect
|
||||
engine.render.draw_rect(p.x, p.y, p.w, p.h, COLOR_BG)
|
||||
|
||||
if not ctx.selected_entry then
|
||||
-- Empty state: background image + hint text (L.10 ships the image).
|
||||
if ctx.bg_texture then
|
||||
engine.render.draw_texture(ctx.bg_texture, p.x, p.y)
|
||||
end
|
||||
if ctx.bg_texture then engine.render.draw_texture(ctx.bg_texture, p.x, p.y) end
|
||||
local hint = ctx.t("empty_hint")
|
||||
local w, h = engine.render.measure_text(hint, FONT_BODY)
|
||||
engine.render.draw_text(hint,
|
||||
p.x + (p.w - w) / 2, p.y + (p.h - h) / 2,
|
||||
FONT_BODY, COLOR_DIM)
|
||||
engine.render.draw_text(hint, p.x + (p.w - w) / 2, p.y + (p.h - h) / 2,
|
||||
FONT_BODY, COLOR_DIM)
|
||||
return
|
||||
end
|
||||
|
||||
-- Stub for L.4/L.5/L.6/L.7: full content lands later.
|
||||
engine.render.draw_text(tostring(ctx.selected_entry),
|
||||
p.x + PADDING, p.y + PADDING, FONT_TITLE, COLOR_TEXT)
|
||||
-- News: render news.md.
|
||||
if ctx.selected_entry == "news" then
|
||||
ensure_md(ctx.launcher_dir)
|
||||
local body = ctx.news_body_blocks or {}
|
||||
md.render(body, p.x + PADDING, p.y + PADDING, p.w - 2 * PADDING, nil)
|
||||
return
|
||||
end
|
||||
|
||||
local entry = find_entry(ctx, ctx.selected_entry)
|
||||
if not entry then return end
|
||||
local mc = ctx.manifest_cache[entry.id] or { meta = {}, blocks = {} }
|
||||
|
||||
-- Carousel hero placeholder: L.5 swaps this in. For now: a strip.
|
||||
local hero_h = 160
|
||||
if ctx.carousel_state[entry.id] and ctx.carousel_mod then
|
||||
-- L.5 implements this; render call lands later.
|
||||
else
|
||||
engine.render.draw_rect(p.x, p.y, p.w, hero_h, 0x404040FF)
|
||||
end
|
||||
|
||||
local cur_y = p.y + hero_h + PADDING
|
||||
-- Title row.
|
||||
local title = entry.summary or entry.name or entry.id
|
||||
engine.render.draw_text(title, p.x + PADDING, cur_y, FONT_TITLE, COLOR_TEXT)
|
||||
local _, title_h = engine.render.measure_text(title, FONT_TITLE)
|
||||
-- Version + author right-aligned.
|
||||
local sub = string.format("v%s %s", entry.version, mc.meta.author or "")
|
||||
local sub_w, _ = engine.render.measure_text(sub, FONT_SUB)
|
||||
engine.render.draw_text(sub, p.x + p.w - PADDING - sub_w, cur_y + 6,
|
||||
FONT_SUB, COLOR_DIM)
|
||||
cur_y = cur_y + title_h + 12
|
||||
|
||||
-- Description (rendered MD).
|
||||
ensure_md(ctx.launcher_dir)
|
||||
cur_y = md.render(mc.blocks, p.x + PADDING, cur_y,
|
||||
p.w - 2 * PADDING, entry.id)
|
||||
|
||||
cur_y = cur_y + 12
|
||||
|
||||
-- Dep list — load-order from manifest.module per L-Q6.
|
||||
local r = entry.status.dep_check_result
|
||||
local dep_count = (r and r.deps and #r.deps) or 0
|
||||
engine.render.draw_text(
|
||||
string.format("%s (%d)", ctx.t("dependencies"), dep_count),
|
||||
p.x + PADDING, cur_y, FONT_SUB, COLOR_TEXT)
|
||||
cur_y = cur_y + 24
|
||||
|
||||
if r and r.deps then
|
||||
for _, d in ipairs(r.deps) do
|
||||
local glyph, col = status_glyph(d.status)
|
||||
engine.render.draw_text(glyph, p.x + PADDING, cur_y, FONT_DEP, col)
|
||||
engine.render.draw_text(string.format("%s %s", d.id, d.version),
|
||||
p.x + PADDING + 40, cur_y, FONT_DEP, COLOR_TEXT)
|
||||
cur_y = cur_y + 20
|
||||
end
|
||||
end
|
||||
|
||||
-- Whole-list tint hint: bubble up to ctx.list_tint so panels/list.lua picks it up.
|
||||
if r and r.kind == "error" then ctx.list_tint = "error"
|
||||
elseif r and r.kind == "warn" then ctx.list_tint = "warn"
|
||||
end
|
||||
end
|
||||
|
||||
function M.handle_click(ctx, mx, my)
|
||||
-- Stub: real hit-tests for launch button + carousel arrows in L.5/L.7.
|
||||
-- L.5/L.7 add carousel-arrow + launch-button hit-tests.
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user