3 Commits

Author SHA1 Message Date
Calic
7f43f8452f docs: CR-5 — README v0.1.0->v0.3.3, Requires +git/dep-fetcher, Master-Detail-Beschreibung
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:47:57 +00:00
Calic
6917529778 fix(launcher): localize image-missing placeholder + drop dead icons.lua
- markdown.lua: M.render now accepts an optional `t` 6th arg (ctx.t).
  When present, the image-missing fallback string is looked up via the
  new `image_missing_fmt` key in strings.lua. Callers without t (the
  launcher-test bytecopy) get the hardcoded English string verbatim,
  matching previous behavior.
- panels/detail.lua: both md.render call sites now thread ctx.t through.
- strings.lua: add image_missing_fmt = "[image missing: %s]".
- icons.lua: deleted. The v0.2.0 status-glyph helper was replaced by
  dep_glyph_* string keys in v0.3.0; grep confirmed no remaining refs.
- manifest.module: bump 0.3.2 -> 0.3.3.
2026-06-13 13:31:36 +02:00
Calic
a7dc368712 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>
2026-06-11 11:58:43 +00:00
8 changed files with 180 additions and 116 deletions

View File

@@ -1,10 +1,10 @@
# lib-management.launcher
Module-discovery and module-switch entry. Lists available modules from the configured modules-dir and launches the selected one via `engine.switch_module`. Typical entry point: `--module=lib-management.launcher` boots into a chooser screen rather than directly into a game.
Master-detail Launcher-UI und Modul-Einstieg. Listet die installierten Module aus dem modules-dir auf und zeigt sie in einem geteilten Layout: links (1/3) die Modul-Liste mit Genre-Tags, rechts (2/3) ein Detail-Panel mit Hero-Carousel, gerenderter Markdown-Beschreibung und Dependency-Status. Pro Modul wird `manifest.launcher.md` für Präsentation (Summary, Tags, Teaser) geladen. Beim Launch eines Moduls prüft der Launcher dessen Dependencies (via `lib-management.dep-fetcher`) und Update-Stand (via `lib-core.git`); ein Konflikt öffnet ein Modal statt direkt zu wechseln. Typischer Einstieg: `--module=lib-management.launcher` bootet in den Launcher-Screen statt direkt in ein Spiel.
**Version:** 0.1.0
**Version:** 0.3.3
**Module-ID:** lib-management.launcher
**Requires:** lib-core.input v>=0.4.0
**Requires:** lib-core.input v0.4.0, lib-core.git v0.2.0, lib-management.dep-fetcher v0.1.2
**Tags:** launcher, menu, system, navigation
## Topology
@@ -15,6 +15,10 @@ graph LR
this["lib-management.launcher"]
lib_core_input["lib-core.input"]
this --> lib_core_input
lib_core_git["lib-core.git"]
this --> lib_core_git
lib_management_dep_fetcher["lib-management.dep-fetcher"]
this --> lib_management_dep_fetcher
engine["engine.*"]
this --> engine
```
@@ -22,17 +26,22 @@ graph LR
## Controls
- **Mouse-Left**: Click a module-button to launch it; click confirm/cancel in the quit-modal.
- **Enter**: Confirm quit when the modal is open.
- **Escape**: Open the quit-modal from the chooser; cancel the modal when open.
- **Mouse-Left**: Select a module-row in the left list to show its detail; click Launch in the detail panel to launch it; carousel arrows; confirm/cancel in the modals.
- **Mouse-Wheel**: Scroll the panel under the cursor — the module list (left) or the detail content (right). Detail content (description + dependencies) scrolls between the fixed hero strip and the fixed launch button.
- **Enter**: Confirm quit when the quit-modal is open.
- **Escape**: Open the quit-modal from the chooser; cancel the conflict/quit-modal when open.
## Demonstrates
- `engine.module.list` discovery of installed modules from the modules-dir.
- `engine.switch_module` runtime swap from the launcher to the selected module.
- Simple text + rect rendering via `engine.render` for the chooser UI.
- Input-action binding through `lib-core.input` (`ui_click`, `ui_back`, `ui_confirm`).
- Modal-state FSM (chooser <-> quit-modal) factored into a sibling `fsm.lua` loaded via `engine.module.dir_of`.
- **Master-detail Layout** (1/3 Liste, 2/3 Detail): linke Liste zeigt je Modul **Name** + Genre-**Tags**-Subline (aus `manifest.launcher.md`); rechtes Detail-Panel zeigt Hero-Carousel, gerenderte Markdown-Beschreibung und eine Dependency-Status-Liste.
- Pro-Modul-Präsentation aus `manifest.launcher.md`: Summary/Tags für die Liste, `teaser_images` + `carousel_interval_seconds` für das Hero-Carousel (Panel-State je Modul-ID, nur das selektierte Modul wird gepumpt).
- Dependency-Status via `lib-management.dep-fetcher` (`ensure_for_module`): die `manifest.module::deps[]` werden in Load-Order zu einer Status-Liste komponiert (`ok` / `warn` / `error`) und mit Glyphen je Dep gerendert; ein Konflikt-Roll-up tönt die Liste ein und öffnet beim Launch das Konflikt-Modal statt `engine.switch_module` direkt aufzurufen.
- Update-Check via `lib-core.git` (`start_ls_remote_tags` / `take_result`): asynchrones `ls-remote` pro Modul gegen die Gitea-Repo-URL, Vergleich des höchsten `vX.Y.Z`-Tags gegen die lokale Version → Badge `available` / `no-update` / `local-only` / `fail` (Fehler-Kind für Tooltips erhalten). In CI (`SPOREL_CI=1`) deaktiviert, damit der Milestone-Check offline bleibt.
- `engine.module.list` Discovery der installierten Module aus dem modules-dir; `engine.switch_module` für den Laufzeit-Wechsel vom Launcher ins selektierte Modul.
- Scrollbare Panels via `engine.render.begin_view`/`end_view` (scissor-clipped), getrieben durch `engine.input.get_mouse_wheel`, mit Inline-Scrollbars (Liste links, Detail-Content rechts zwischen fixiertem Hero-Strip und Launch-Button).
- Markdown-/News-Rendering via sibling `markdown.lua` + `frontmatter.lua`; launcher-eigene Brand-Assets via `engine.asset.load_texture` (`assets/launcher/`).
- Input-Action-Binding über `lib-core.input` (`ui_click`, `ui_back`, `ui_confirm`).
- Modal-State-FSM (Liste <-> Quit-/Konflikt-Modal) in ein sibling `fsm.lua` ausgelagert, geladen via `engine.module.dir_of`.
## References

View File

@@ -1,84 +0,0 @@
-- icons.lua — inline status-icon primitives for the launcher.
--
-- The engine ships text + rect bindings; no sprites, no draw_circle yet,
-- so every icon is composed from draw_rect + draw_text. Each helper
-- returns the rect it occupied so the caller can stash it for hit-tests
-- (tooltip + update-badge click).
local M = {}
M.SPINNER_COLOR = 0xC0C0C0FF
M.WARN_COLOR = 0xFFD000FF
M.UPDATE_COLOR = 0x4090FFFF
M.ERROR_COLOR = 0xFF4040FF
M.LOCAL_COLOR = 0x707070FF -- muted gray for "local-only" repos
M.ICON_W = 16
M.ICON_H = 16
local SPINNER_GLYPHS = { "|", "/", "-", "\\" }
-- Spinner: rotating ASCII glyph cycled at ~7.5 Hz at 60 fps (every 8
-- frames). Returns its bounding rect so the caller can hit-test for
-- tooltip ("checking for updates...").
function M.draw_spinner(x, y, frame)
local glyph = SPINNER_GLYPHS[(math.floor(frame / 8) % 4) + 1]
engine.render.draw_text(glyph, x + 4, y, 12, M.SPINNER_COLOR)
return { x = x, y = y, w = M.ICON_W, h = M.ICON_H }
end
-- Warn: yellow square with "!" centered.
function M.draw_warn(x, y)
engine.render.draw_rect(x, y, M.ICON_W, M.ICON_H, M.WARN_COLOR)
engine.render.draw_text("!", x + 5, y, 14, 0x000000FF)
return { x = x, y = y, w = M.ICON_W, h = M.ICON_H }
end
-- Update badge: blue pill "^v0.5.5". Width depends on the version
-- string; returned rect carries the dynamic width so callers can do
-- right-to-left layout and click-hit-tests.
function M.draw_update_badge(x, y, version_str)
local label = "^" .. version_str
local tw, _ = engine.render.measure_text(label, 12)
local w = tw + 10
engine.render.draw_rect(x, y, w, M.ICON_H, M.UPDATE_COLOR)
engine.render.draw_text(label, x + 5, y + 1, 12, 0xFFFFFFFF)
return { x = x, y = y, w = w, h = M.ICON_H }
end
-- Error: red square with white "!" — same shape as warn but different
-- color so colorblind users still see the shape difference via the
-- background tint contrast.
function M.draw_error(x, y)
engine.render.draw_rect(x, y, M.ICON_W, M.ICON_H, M.ERROR_COLOR)
engine.render.draw_text("!", x + 5, y, 14, 0xFFFFFFFF)
return { x = x, y = y, w = M.ICON_W, h = M.ICON_H }
end
-- Local-only: muted-gray pill with "L" — module exists in the local
-- install but the remote returns 404 even with credentials, so the
-- update channel doesn't apply. Informational, not a warning.
function M.draw_local(x, y)
engine.render.draw_rect(x, y, M.ICON_W, M.ICON_H, M.LOCAL_COLOR)
engine.render.draw_text("L", x + 5, y, 12, 0xFFFFFFFF)
return { x = x, y = y, w = M.ICON_W, h = M.ICON_H }
end
-- Tooltip: small dark panel anchored to the right of (mx, my).
-- Centered vertically on the cursor. Clamped to screen via the
-- screen-size args.
function M.draw_tooltip(mx, my, text, screen_w, screen_h)
if not text or text == "" then return end
local tw, th = engine.render.measure_text(text, 12)
local pad_x, pad_y = 6, 3
local w, h = tw + 2 * pad_x, th + 2 * pad_y
local x = mx + 14
local y = my - h / 2
if x + w > screen_w then x = mx - w - 8 end
if y < 0 then y = 0 end
if y + h > screen_h then y = screen_h - h end
engine.render.draw_rect(x, y, w, h, 0x202020E0)
engine.render.draw_text(text, x + pad_x, y + pad_y, 12, 0xFFFFFFFF)
end
return M

View File

@@ -199,7 +199,10 @@ function init(ctx)
module_entries = {}, -- filled below from engine.module.list
news_entry = { title = nil, body_path = "news.md" },
selected_entry = nil,
scroll_y = 0,
scroll_y = 0, -- list-panel vertical scroll
detail_scroll_y = 0, -- detail-panel content scroll
detail_content_h = 0, -- set by detail.render each frame
detail_view_h = 0, -- scrollable viewport height, set by detail.render
list_tint = nil, -- nil | "warn" | "error" — set in L.4
list_panel_rect = nil, -- set below from window size
detail_panel_rect = nil,
@@ -263,6 +266,7 @@ 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
entry.tags = meta.tags -- L-fix: list-row subline (genre tags)
-- L.4: also surface manifest.module::deps[] verbatim so the
-- detail-panel can render per-dep status glyphs in load order
@@ -392,6 +396,16 @@ function update(ctx, dt)
end
if state == STATE_LIST then
-- Mouse-wheel scroll: route to whichever panel the cursor is over.
local wheel = engine.input.get_mouse_wheel()
if wheel ~= 0 then
if hit(ctx_panels.detail_panel_rect, mx, my) then
detail_panel.handle_scroll(ctx_panels, wheel)
elseif hit(ctx_panels.list_panel_rect, mx, my) then
list_panel.handle_scroll(ctx_panels, wheel)
end
end
if input.was_action_pressed("ui_back") then
state = fsm.next(state, "esc")
elseif input.was_action_pressed("ui_click") then

View File

@@ -1,6 +1,6 @@
{
"id": "lib-management.launcher",
"version": "0.3.2",
"version": "0.3.3",
"kind": "module",
"api": "^0.1",
"ci_frames": 10,

View File

@@ -176,7 +176,11 @@ local function render_spans(spans, x, y, max_w, size)
return cur_y + line_h
end
function M.render(blocks, x, y, max_w, module_id_for_imgs)
-- `t` (optional, 6th arg) is the launcher's ctx.t string-lookup function.
-- When present, M.render uses it to localize the image-missing placeholder
-- via the `image_missing_fmt` key. Callers that don't have a t (e.g. tests)
-- may pass nil; the hardcoded English fallback then renders verbatim.
function M.render(blocks, x, y, max_w, module_id_for_imgs, t)
local cur_y = y
for _, b in ipairs(blocks) do
if b.type == "h1" then
@@ -216,7 +220,9 @@ function M.render(blocks, x, y, max_w, module_id_for_imgs)
-- assume a 16:9 max_w-wide image for layout.
cur_y = cur_y + math.floor(max_w * 9 / 16) + PARA_GAP
else
engine.render.draw_text("[image missing: " .. b.src .. "]",
local missing_text = t and t("image_missing_fmt", b.src)
or ("[image missing: " .. b.src .. "]")
engine.render.draw_text(missing_text,
x, cur_y, BODY_SIZE, COLOR_DIM)
local _, h = engine.render.measure_text("[image]", BODY_SIZE)
cur_y = cur_y + h + PARA_GAP

View File

@@ -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, ctx.t)
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)
@@ -84,12 +137,11 @@ function M.render(ctx)
-- 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)
p.w - 2 * PADDING, entry.id, ctx.t)
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

View File

@@ -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

View File

@@ -22,4 +22,7 @@ return {
no = "No",
-- News entry default title
news_default_title = "News",
-- Markdown image-missing placeholder (rendered by markdown.lua when
-- engine.module.load_texture returns nil for an inline image source).
image_missing_fmt = "[image missing: %s]",
}