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 COLOR_TAGS = 0x8090A0FF local FONT = 18 local FONT_TAGS = 13 local SCROLLBAR_W = 6 local COLOR_SCROLL_TRACK = 0x00000040 local COLOR_SCROLL_THUMB = 0x707070FF -- Total scrollable content height for the current entry set (news row + -- all module rows). Used for scroll-clamp + scrollbar sizing. local function content_height(ctx) local rows = #ctx.module_entries + (ctx.news_entry and 1 or 0) if rows == 0 then return 0 end return rows * (ROW_HEIGHT + ROW_GAP) - ROW_GAP + 2 * PADDING end -- Draw a vertical scrollbar inset on the panel's right edge when content -- overflows the viewport. Mirrors detail.lua's scrollbar (kept inline in -- both panels for cognitive locality — see Lib-Cut criterion). local function draw_scrollbar(p, scroll_y, content_h, view_h) if content_h <= view_h then return end local track_x = p.x + p.w - SCROLLBAR_W - 2 engine.render.draw_rect(track_x, p.y, SCROLLBAR_W, view_h, COLOR_SCROLL_TRACK) local thumb_h = math.max(24, view_h * view_h / content_h) local max_scroll = content_h - view_h local frac = (max_scroll > 0) and (scroll_y / max_scroll) or 0 local thumb_y = p.y + frac * (view_h - thumb_h) engine.render.draw_rect(track_x, thumb_y, SCROLLBAR_W, thumb_h, COLOR_SCROLL_THUMB) end -- 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 -- Row title: name only (the long `summary` moved to the detail panel). local function label_for(entry) return entry.name or entry.id end -- Compact genre-tag subline ("Mittelalter · Hightech"), or nil if the -- module's manifest.launcher.md carries no tags. local function tags_line(entry) if not entry.tags or #entry.tags == 0 then return nil end return table.concat(entry.tags, " · ") 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 ctx.t("news_default_title"), 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) -- Module name on top, genre tags small below. engine.render.draw_text(label_for(entry), rect.x + PADDING, rect.y + 8, FONT, COLOR_TEXT) local tags = tags_line(entry) if tags then engine.render.draw_text(tags, rect.x + PADDING, rect.y + 8 + FONT + 6, FONT_TAGS, COLOR_TAGS) end end entry.rect = rect end draw_scrollbar(p, ctx.scroll_y, content_height(ctx), p.h) end -- Selecting a different entry resets the detail panel's scroll so the new -- module's content starts from the top. local function select(ctx, id) if ctx.selected_entry ~= id then ctx.detail_scroll_y = 0 end ctx.selected_entry = id 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 select(ctx, "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 select(ctx, entry.id) return true end end return false end function M.handle_scroll(ctx, dy) local p = ctx.list_panel_rect local max_scroll = math.max(0, content_height(ctx) - p.h) ctx.scroll_y = math.max(0, math.min(max_scroll, ctx.scroll_y - dy * 24)) end return M