feat(launcher): trim list rows to name+tags, scrollable detail panel
List rows showed the long `summary` (name+id+description fallback chain) and overflowed. Now each row shows the module name with a small genre- tags subline sourced from manifest.launcher.md (meta.tags, threaded onto the entry in init). The list also scrolls properly now (wheel dispatch + upper clamp) — it was wired but dead without a wheel binding. Detail panel: description + dependency list ran off the bottom of the window. The mid-section (title/description/deps) now scrolls inside a begin_view/end_view scissor region between the fixed hero strip and the fixed launch button; news view scrolls too. Both panels get an inline scrollbar. Detail scroll resets on module switch. Depends on engine.input.get_mouse_wheel (sporel-engine). README controls/ demonstrates synced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,34 @@ 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)
|
||||
@@ -22,9 +49,16 @@ function M.row_rect(ctx, i)
|
||||
}
|
||||
end
|
||||
|
||||
-- Pick the display label per spec L-Q8: summary || name || id.
|
||||
-- Row title: name only (the long `summary` moved to the detail panel).
|
||||
local function label_for(entry)
|
||||
return entry.summary or entry.name or entry.id
|
||||
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)
|
||||
@@ -55,21 +89,36 @@ function M.render(ctx)
|
||||
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 + PADDING, FONT, COLOR_TEXT)
|
||||
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
|
||||
ctx.selected_entry = "news"
|
||||
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
|
||||
ctx.selected_entry = entry.id
|
||||
select(ctx, entry.id)
|
||||
return true
|
||||
end
|
||||
end
|
||||
@@ -77,7 +126,9 @@ function M.handle_click(ctx, mx, my)
|
||||
end
|
||||
|
||||
function M.handle_scroll(ctx, dy)
|
||||
ctx.scroll_y = math.max(0, ctx.scroll_y - dy * 24)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user