fix: chromeless suppresses phantom close-button + top-tier modal swallows immediately

- chromeless=true now correctly suppresses the close-button hit-test.
  Previously, chromeless widgets had an invisible 26x26 close-zone
  at the top-right that could close the widget on accidental click.
  Affected map-editor.toolbar: the mode-pill at top-right overlapped
  the phantom zone and clicking it closed the toolbar.
- input_block="all" now swallows outside-clicks immediately, not
  just when no other widget claims the event. Previously, modal
  dialogs leaked clicks to lower-tier widgets behind them.
- register docstring lists chromeless opt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Calic
2026-06-15 02:43:46 +02:00
parent 1fcdbd192b
commit 89cd1727f6

View File

@@ -384,10 +384,15 @@ end
--- 1. Context-menu (if open) always wins. --- 1. Context-menu (if open) always wins.
--- 2. Iterate reverse z_tier (top → normal → hud), within tier --- 2. Iterate reverse z_tier (top → normal → hud), within tier
--- reverse-open-order. For each open window: --- reverse-open-order. For each open window:
--- - If input_block="all" AND click is OUTSIDE its bounds: swallow
--- the event IMMEDIATELY (modal owns the screen — do not let
--- lower-tier widgets claim outside-clicks behind it).
--- - input_block="none": skip hit-test entirely (HUD passes through). --- - input_block="none": skip hit-test entirely (HUD passes through).
--- - else: hit-test bounds; on hit route to widget. --- - else: hit-test bounds; on hit route to widget.
--- - chromeless=true suppresses the close-button hit-test (the
--- widget owns its full bounds, including the top-right area).
--- 3. If no window claimed the event AND any open window has --- 3. If no window claimed the event AND any open window has
--- input_block="all": swallow (modal block). --- input_block="all": swallow (modal block).
--- 4. Else drop (no game-routing — module handles its own input). --- 4. Else drop (no game-routing — module handles its own input).
local function _dispatch_event(event) local function _dispatch_event(event)
-- Context-menu always wins if open (consumes the next click). -- Context-menu always wins if open (consumes the next click).
@@ -407,6 +412,17 @@ local function _dispatch_event(event)
if win and win.open and win.opts.z_tier == tier_name then if win and win.open and win.opts.z_tier == tier_name then
if win.opts.input_block == "all" then if win.opts.input_block == "all" then
saw_modal = true saw_modal = true
-- Modal swallow: if the click is outside this modal's
-- bounds, swallow immediately so lower-tier widgets
-- behind it cannot claim it. Inside-bounds clicks fall
-- through to normal routing below.
if event.kind == "click" and event.x and event.y then
local mb = resolve_bounds(win.opts.layout, sw, sh)
if not (event.x >= mb.x and event.x < mb.x + mb.w
and event.y >= mb.y and event.y < mb.y + mb.h) then
return
end
end
end end
if win.opts.input_block ~= "none" then if win.opts.input_block ~= "none" then
local b = resolve_bounds(win.opts.layout, sw, sh) local b = resolve_bounds(win.opts.layout, sw, sh)
@@ -416,7 +432,10 @@ local function _dispatch_event(event)
and event.y >= b.y and event.y < b.y + b.h then and event.y >= b.y and event.y < b.y + b.h then
-- Close-button intercept (top-right X): left-click -- Close-button intercept (top-right X): left-click
-- closes the window before forwarding to the widget. -- closes the window before forwarding to the widget.
if event.button == "left" then -- Chromeless widgets have no chrome (no X), so we
-- must NOT hit-test the phantom close-zone — the
-- widget owns its full bounds.
if event.button == "left" and not win.opts.chromeless then
local cb = win._close_button_rect local cb = win._close_button_rect
if not cb then if not cb then
cb = close_button_rect(b.x, b.y, b.w, cb = close_button_rect(b.x, b.y, b.w,
@@ -518,6 +537,12 @@ end
--- no-op when it's focused (modder must close(id) explicit). --- no-op when it's focused (modder must close(id) explicit).
--- input_block = "none" | "self" | "all" — defaults per tier: --- input_block = "none" | "self" | "all" — defaults per tier:
--- hud → "none", normal → "self", top → "all". --- hud → "none", normal → "self", top → "all".
--- chromeless = false (default) | true — when true, suppresses the
--- panel-lib chrome (title-bar, border, X close-button).
--- Widget's render(ctx) draws into the full bounds
--- without panel-lib decoration. Useful for HUD-style
--- widgets that have their own visual design (status
--- bars, toolbars with their own theme).
function M.register(widget_id, widget_def, opts) function M.register(widget_id, widget_def, opts)
if type(widget_id) ~= "string" then if type(widget_id) ~= "string" then
error("panel.register: widget_id must be a string, got " .. type(widget_id)) error("panel.register: widget_id must be a string, got " .. type(widget_id))