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:
@@ -11,6 +11,11 @@ local FONT_TITLE = 28
|
||||
local FONT_SUB = 16
|
||||
local FONT_BODY = 14
|
||||
local FONT_DEP = 14
|
||||
local HERO_H = 160 -- fixed hero strip (outside scroll region)
|
||||
local BOTTOM_RESERVE = 70 -- fixed launch-button strip (outside scroll region)
|
||||
local SCROLLBAR_W = 6
|
||||
local COLOR_SCROLL_TRACK = 0x00000040
|
||||
local COLOR_SCROLL_THUMB = 0x707070FF
|
||||
|
||||
local md -- lazy-loaded markdown renderer
|
||||
|
||||
@@ -18,6 +23,38 @@ local function ensure_md(launcher_dir)
|
||||
if not md then md = dofile(launcher_dir .. "/markdown.lua") end
|
||||
end
|
||||
|
||||
-- Camera that shifts content up by `scroll` px and clips to `view` (set up
|
||||
-- via engine.render.begin_view). Drawing inside uses absolute screen-space
|
||||
-- coords (zoom 1, target.x = view-center → x unchanged); only y scrolls.
|
||||
local function scroll_cam(view, scroll)
|
||||
return {
|
||||
target_x = view.x + view.w / 2,
|
||||
target_y = view.y + view.h / 2 + scroll,
|
||||
zoom = 1,
|
||||
}
|
||||
end
|
||||
|
||||
-- Clamp the live scroll offset to [0, content - view] using last frame's
|
||||
-- measured content height. Returns the clamped value (also written back).
|
||||
local function clamp_scroll(ctx)
|
||||
local max_scroll = math.max(0, (ctx.detail_content_h or 0) - (ctx.detail_view_h or 0))
|
||||
ctx.detail_scroll_y = math.max(0, math.min(max_scroll, ctx.detail_scroll_y or 0))
|
||||
return ctx.detail_scroll_y
|
||||
end
|
||||
|
||||
-- Vertical scrollbar on the view's right edge (mirrors list.lua; kept inline
|
||||
-- per the Lib-Cut criterion — cognitive locality over de-duplication).
|
||||
local function draw_scrollbar(view, scroll, content_h)
|
||||
if content_h <= view.h then return end
|
||||
local track_x = view.x + view.w - SCROLLBAR_W - 2
|
||||
engine.render.draw_rect(track_x, view.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 / max_scroll) or 0
|
||||
local thumb_y = view.y + frac * (view.h - thumb_h)
|
||||
engine.render.draw_rect(track_x, thumb_y, SCROLLBAR_W, thumb_h, COLOR_SCROLL_THUMB)
|
||||
end
|
||||
|
||||
local function status_glyph(ctx, dep_status)
|
||||
if dep_status == "ok" then return ctx.t("dep_glyph_ok"), COLOR_OK end
|
||||
if dep_status == "warn" then return ctx.t("dep_glyph_warn"), COLOR_WARN end
|
||||
@@ -46,11 +83,18 @@ function M.render(ctx)
|
||||
return
|
||||
end
|
||||
|
||||
-- News: render news.md.
|
||||
-- News: render news.md (scrollable, full panel — no hero/launch strip).
|
||||
if ctx.selected_entry == "news" then
|
||||
ensure_md(ctx.launcher_dir)
|
||||
local view = { x = p.x, y = p.y, w = p.w, h = p.h }
|
||||
local scroll = clamp_scroll(ctx)
|
||||
engine.render.begin_view(view, scroll_cam(view, scroll))
|
||||
local body = ctx.news_body_blocks or {}
|
||||
md.render(body, p.x + PADDING, p.y + PADDING, p.w - 2 * PADDING, nil)
|
||||
local end_y = md.render(body, p.x + PADDING, p.y + PADDING, p.w - 2 * PADDING, nil)
|
||||
engine.render.end_view()
|
||||
ctx.detail_content_h = (end_y - view.y) + PADDING
|
||||
ctx.detail_view_h = view.h
|
||||
draw_scrollbar(view, scroll, ctx.detail_content_h)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -58,18 +102,27 @@ function M.render(ctx)
|
||||
if not entry then return end
|
||||
local mc = ctx.manifest_cache[entry.id] or { meta = {}, blocks = {} }
|
||||
|
||||
-- Hero carousel (L.5). Renders bg + texture + arrows + dots.
|
||||
-- Zero-teaser modules render the default_teaser_texture (L.10)
|
||||
-- on top of the COLOR_FALLBACK_BG strip.
|
||||
local hero_h = 160
|
||||
-- Hero carousel (L.5) — fixed, above the scroll region. Renders bg +
|
||||
-- texture + arrows + dots. Zero-teaser modules render the
|
||||
-- default_teaser_texture (L.10) on top of the COLOR_FALLBACK_BG strip.
|
||||
local s = ctx.carousel_state[entry.id]
|
||||
if s and ctx.carousel_mod then
|
||||
ctx.carousel_mod.render(s, p.x, p.y, p.w, hero_h, ctx.default_teaser_texture)
|
||||
ctx.carousel_mod.render(s, p.x, p.y, p.w, HERO_H, ctx.default_teaser_texture)
|
||||
else
|
||||
engine.render.draw_rect(p.x, p.y, p.w, hero_h, 0x404040FF)
|
||||
engine.render.draw_rect(p.x, p.y, p.w, HERO_H, 0x404040FF)
|
||||
end
|
||||
|
||||
local cur_y = p.y + hero_h + PADDING
|
||||
local r = entry.status.dep_check_result
|
||||
|
||||
-- Scroll region: everything between hero and the fixed launch strip.
|
||||
local view = {
|
||||
x = p.x, y = p.y + HERO_H,
|
||||
w = p.w, h = p.h - HERO_H - BOTTOM_RESERVE,
|
||||
}
|
||||
local scroll = clamp_scroll(ctx)
|
||||
engine.render.begin_view(view, scroll_cam(view, scroll))
|
||||
|
||||
local cur_y = view.y + 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)
|
||||
@@ -89,7 +142,6 @@ function M.render(ctx)
|
||||
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),
|
||||
@@ -111,6 +163,13 @@ function M.render(ctx)
|
||||
end
|
||||
end
|
||||
|
||||
engine.render.end_view()
|
||||
|
||||
-- Record content/viewport extent for scroll-clamp (next frame) + bar.
|
||||
ctx.detail_content_h = (cur_y - view.y) + PADDING
|
||||
ctx.detail_view_h = view.h
|
||||
draw_scrollbar(view, scroll, ctx.detail_content_h)
|
||||
|
||||
-- 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"
|
||||
@@ -173,4 +232,10 @@ function M.handle_click(ctx, mx, my)
|
||||
return false
|
||||
end
|
||||
|
||||
function M.handle_scroll(ctx, dy)
|
||||
local max_scroll = math.max(0, (ctx.detail_content_h or 0) - (ctx.detail_view_h or 0))
|
||||
ctx.detail_scroll_y = math.max(0,
|
||||
math.min(max_scroll, (ctx.detail_scroll_y or 0) - dy * 24))
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user