feat(launcher): launch button + dep-conflict modal

Detail-panel bottom-right Launch button: switches to the selected
module via engine.switch_module if dep-check is clean, otherwise
opens a modal listing conflicts with Cancel + 'Launch anyway'. The
latter is greyed out unless engine.module.switch_module_supports_fallback
is exposed (separate engine slice). FSM adds STATE_MODAL_CONFLICT
with transitions to/from STATE_LIST via cancel/launch_anyway/esc.

Generic panels/modal.lua takes a def-table (title/body/buttons) and
returns the clicked button id from handle_click; reusable for future
modals beyond the conflict case.
This commit is contained in:
Calic
2026-06-11 02:54:55 +02:00
parent 10d0ab6529
commit 89b7b060d1
4 changed files with 142 additions and 15 deletions

View File

@@ -115,10 +115,22 @@ function M.render(ctx)
if r and r.kind == "error" then ctx.list_tint = "error"
elseif r and r.kind == "warn" then ctx.list_tint = "warn"
end
-- L.7: Launch button at the bottom-right of the detail panel. Label
-- runs through ctx.t() so L.8's strings.lua picks it up once wired.
local label = ctx.t("launch_module")
local lw, lh = engine.render.measure_text(label, 18)
local btn_w = lw + 32
local btn_h = lh + 16
local btn_x = p.x + p.w - PADDING - btn_w
local btn_y = p.y + p.h - PADDING - btn_h
engine.render.draw_rect(btn_x, btn_y, btn_w, btn_h, 0x405080FF)
engine.render.draw_text(label, btn_x + 16, btn_y + 8, 18, 0xFFFFFFFF)
ctx._launch_btn = { x = btn_x, y = btn_y, w = btn_w, h = btn_h }
end
function M.handle_click(ctx, mx, my)
-- L.5: carousel arrow hit-test. L.7 will add launch-button.
-- L.5: carousel arrow hit-test. L.7 adds the launch-button.
if not ctx.selected_entry or ctx.selected_entry == "news" then
return false
end
@@ -128,6 +140,34 @@ function M.handle_click(ctx, mx, my)
if s and ctx.carousel_mod and ctx.carousel_mod.handle_click(s, mx, my) then
return true
end
-- L.7: Launch button → either switch directly (clean deps) or open
-- the conflict modal (warn/error deps). The "Launch anyway" path
-- requires engine.module.switch_module_supports_fallback (separate
-- engine slice); until that ships the button stays disabled.
if ctx._launch_btn and engine.spatial.aabb_contains_point(ctx._launch_btn, mx, my) then
local r = entry.status.dep_check_result
if r and r.ok then
engine.switch_module(entry.id)
else
ctx.modal_def = {
title = ctx.t("conflict_title"),
body = {
ctx.t("conflict_body_line1"),
string.format(ctx.t("conflict_body_line2_fmt"),
r and #r.missing or 0),
},
buttons = {
{ id = "cancel", label = ctx.t("cancel") },
{ id = "launch_anyway", label = ctx.t("launch_anyway"),
disabled = not engine.module.switch_module_supports_fallback },
},
module_id = entry.id,
}
ctx.modal_open = "conflict"
end
return true
end
return false
end