From 41b28ff37d33e527355191529f98aa9453740d07 Mon Sep 17 00:00:00 2001 From: Calic Date: Thu, 11 Jun 2026 01:41:11 +0200 Subject: [PATCH] feat(launcher): add list + detail panel stubs (master-detail L.2) Splits rendering into panels/list.lua and panels/detail.lua. List implements row hit-test, scroll-state, whole-list bg-tint hook for the dep-conflict UX (L-Q6). Detail renders an empty-state placeholder until L.4-L.7 fill in title, description, deps, carousel, and launch button. --- panels/detail.lua | 37 +++++++++++++++++++++ panels/list.lua | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 panels/detail.lua create mode 100644 panels/list.lua diff --git a/panels/detail.lua b/panels/detail.lua new file mode 100644 index 0000000..99dc181 --- /dev/null +++ b/panels/detail.lua @@ -0,0 +1,37 @@ +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 + +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 + 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) + 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) +end + +function M.handle_click(ctx, mx, my) + -- Stub: real hit-tests for launch button + carousel arrows in L.5/L.7. + return false +end + +return M diff --git a/panels/list.lua b/panels/list.lua new file mode 100644 index 0000000..8d423fc --- /dev/null +++ b/panels/list.lua @@ -0,0 +1,82 @@ +local M = {} + +local PADDING = 16 +local ROW_HEIGHT = 56 +local ROW_GAP = 4 +local COLOR_BG = 0x202020FF +local COLOR_ROW = 0x303030FF +local COLOR_ROW_HOVER = 0x404040FF +local COLOR_ROW_ACTIVE = 0x505080FF +local COLOR_TEXT = 0xE0E0E0FF +local COLOR_DIM = 0xA0A0A0FF +local FONT = 18 + +-- Returns the rect rect-table {x,y,w,h} of the row for entry index i. +function M.row_rect(ctx, i) + local p = ctx.list_panel_rect + return { + x = p.x + PADDING, + y = p.y + PADDING + (i - 1) * (ROW_HEIGHT + ROW_GAP) - ctx.scroll_y, + w = p.w - 2 * PADDING, + h = ROW_HEIGHT, + } +end + +-- Pick the display label per spec L-Q8: summary || name || id. +local function label_for(entry) + return entry.summary or entry.name or entry.id +end + +function M.render(ctx) + local p = ctx.list_panel_rect + -- Apply whole-list bg tint per L-Q6 if conflicts exist. + local bg = COLOR_BG + if ctx.list_tint == "warn" then bg = 0x4A3A1AFF end + if ctx.list_tint == "error" then bg = 0x4A1A1AFF end + engine.render.draw_rect(p.x, p.y, p.w, p.h, bg) + + -- News row pinned on top. + if ctx.news_entry then + local rect = { + x = p.x + PADDING, y = p.y + PADDING - ctx.scroll_y, + w = p.w - 2 * PADDING, h = ROW_HEIGHT, + } + local col = (ctx.selected_entry == "news") and COLOR_ROW_ACTIVE or COLOR_ROW + engine.render.draw_rect(rect.x, rect.y, rect.w, rect.h, col) + engine.render.draw_text(ctx.news_entry.title or "News", + rect.x + PADDING, rect.y + PADDING, FONT, COLOR_TEXT) + ctx.news_entry.rect = rect + end + + for i, entry in ipairs(ctx.module_entries) do + local rect = M.row_rect(ctx, i + (ctx.news_entry and 1 or 0)) + if rect.y + rect.h >= p.y and rect.y <= p.y + p.h then + local col = COLOR_ROW + if ctx.selected_entry == entry.id then col = COLOR_ROW_ACTIVE end + engine.render.draw_rect(rect.x, rect.y, rect.w, rect.h, col) + engine.render.draw_text(label_for(entry), + rect.x + PADDING, rect.y + PADDING, FONT, COLOR_TEXT) + end + entry.rect = rect + end +end + +function M.handle_click(ctx, mx, my) + if ctx.news_entry and engine.spatial.aabb_contains_point(ctx.news_entry.rect, mx, my) then + ctx.selected_entry = "news" + return true + end + for _, entry in ipairs(ctx.module_entries) do + if entry.rect and engine.spatial.aabb_contains_point(entry.rect, mx, my) then + ctx.selected_entry = entry.id + return true + end + end + return false +end + +function M.handle_scroll(ctx, dy) + ctx.scroll_y = math.max(0, ctx.scroll_y - dy * 24) +end + +return M