feat: v0.4.0 Focus + Drag + Resize
Adds focus/raise/lower/get_focused API with click-to-focus auto-routing (hud-tier never focusable), drag-by-title-bar with 32px screen-clamp, resize via SE-corner handle with min/max_size clamps. Drag and resize move bounds outside the layout-fn via a per-window bounds_override that shadows resolve_bounds across render + dispatch + point_in_any_panel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
465
init.lua
465
init.lua
@@ -1,12 +1,12 @@
|
||||
-- =====================================================================
|
||||
-- lib-core.panel v0.3.0 — Generic overlay-panel framework (Window-Manager)
|
||||
-- lib-core.panel v0.4.0 — Generic overlay-panel framework (Window-Manager)
|
||||
--
|
||||
-- v0.3.0 layers z_tiers (hud → normal → top), persistent windows, and
|
||||
-- input_block routing (none/self/all) on top of v0.2.0's multi-active +
|
||||
-- layout-slot model. Render iterates per-z-tier (hud → normal → top);
|
||||
-- input dispatch iterates reverse (top → hud, within-tier reverse-open-
|
||||
-- order). `persistent=true` registers a window as open-from-register-time
|
||||
-- and exempts it from bw-compat close() (modder must explicit-close).
|
||||
-- v0.4.0 adds the focus API (`focus`/`raise`/`lower`/`get_focused`),
|
||||
-- click-to-focus auto-routing, drag-by-title-bar, and SE-corner resize
|
||||
-- on top of v0.3.0's z_tier + persistent + input_block model. Drag and
|
||||
-- resize fix bounds outside the layout-fn via `window.bounds_override`;
|
||||
-- min/max_size constraints and a 32px-of-title-bar screen-clamp keep
|
||||
-- windows recoverable.
|
||||
--
|
||||
-- Provides:
|
||||
-- - Widget registry + lifecycle (register, open, close, toggle)
|
||||
@@ -15,6 +15,11 @@
|
||||
-- - Z-tier rendering: hud (bottom) → normal → top (drawn last)
|
||||
-- - Persistent windows: open at register-time; ESC-equivalent no-op
|
||||
-- - Input-block routing: none / self / all (modal swallows misses)
|
||||
-- - Focus API: focus(id) / raise(id) / lower(id) / get_focused()
|
||||
-- - Click-to-focus: clicks on non-hud windows auto-focus the hit widget
|
||||
-- - Drag by title-bar (draggable=true opt)
|
||||
-- - Resize by SE-corner handle (resizable=true opt; 12x12 handle)
|
||||
-- - min_size / max_size constraints + 32px-visible screen-clamp on drag
|
||||
-- - panel.point_in_any_panel(x,y): public hit-test for module-side canvas
|
||||
-- - Per-frame input dispatch with reverse-z-tier reverse-open-order hit
|
||||
-- - Context-menu (show, hit-test, auto-close) on top of windows
|
||||
@@ -32,14 +37,14 @@
|
||||
-- - engine.render.measure_text(text, font_size) → w, h
|
||||
--
|
||||
-- Screen-size limitation: engine.render does NOT expose get_screen_size()
|
||||
-- to Lua (GetScreenWidth/GetScreenHeight are C-only). v0.3 still falls
|
||||
-- to Lua (GetScreenWidth/GetScreenHeight are C-only). v0.4 still falls
|
||||
-- back to 1280x720 constants matching the default Sporel window config.
|
||||
--
|
||||
-- DEFERRED (v0.3 non-goals):
|
||||
-- - Focus API (focus / raise / lower) → v0.4
|
||||
-- - Drag + resize → v0.4
|
||||
-- DEFERRED (v0.4 non-goals):
|
||||
-- - Resize corners other than SE (NE/SW/NW + edge-resize)
|
||||
-- - Keyboard navigation within widgets
|
||||
-- - Panel animation (fade in/out)
|
||||
-- - Save/load window-bounds across sessions
|
||||
-- =====================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------
|
||||
@@ -65,6 +70,31 @@ local triggers = {}
|
||||
local last_mouse_left = false -- for edge-detection (was down last frame)
|
||||
local last_mouse_right = false -- for edge-detection (was down last frame)
|
||||
|
||||
-- v0.4.0: drag-state (set by _dispatch_event on title-bar click; cleared by
|
||||
-- M.update on mouse-release). drag_offset_x/y is the click-point relative
|
||||
-- to the window's top-left at drag-start, so the title stays under the
|
||||
-- cursor through the move.
|
||||
local dragging_id = nil
|
||||
local drag_offset_x = 0
|
||||
local drag_offset_y = 0
|
||||
|
||||
-- v0.4.0: resize-state (set by _dispatch_event on SE-corner click; cleared
|
||||
-- by M.update on mouse-release). v0.4.0 supports only the SE corner.
|
||||
local resizing_id = nil
|
||||
local resize_corner = nil -- "SE" only (placeholder for future NE/SW/NW)
|
||||
local resize_start_w = 0
|
||||
local resize_start_h = 0
|
||||
local resize_start_mx = 0
|
||||
local resize_start_my = 0
|
||||
|
||||
-- v0.4.0: resize-handle size (12x12 px hit-zone + visual cue at SE corner)
|
||||
local RESIZE_HANDLE_SIZE = 12
|
||||
|
||||
-- v0.4.0: minimum visible title-bar pixels on screen edges during drag.
|
||||
-- Keeps the drag-handle reachable even if the user pushes the window past
|
||||
-- the screen border.
|
||||
local MIN_VISIBLE_PX = 32
|
||||
|
||||
-- -----------------------------------------------------------------------
|
||||
-- Theme
|
||||
-- -----------------------------------------------------------------------
|
||||
@@ -210,6 +240,16 @@ local function resolve_bounds(layout, sw, sh)
|
||||
type(layout)), 3)
|
||||
end
|
||||
|
||||
--- v0.4.0: returns the bounds-to-use for a window. Drag and resize fix
|
||||
--- bounds outside the layout-fn via win.bounds_override; everything else
|
||||
--- (initial position, screen-resize follow) falls through to resolve_bounds.
|
||||
--- All render + dispatch + point_in_any_panel callsites use this helper
|
||||
--- instead of calling resolve_bounds directly.
|
||||
local function get_bounds_for(win, sw, sh)
|
||||
if win.bounds_override then return win.bounds_override end
|
||||
return resolve_bounds(win.opts.layout, sw, sh)
|
||||
end
|
||||
|
||||
-- -----------------------------------------------------------------------
|
||||
-- Internal: context-menu dispatch + render helpers
|
||||
-- -----------------------------------------------------------------------
|
||||
@@ -257,7 +297,7 @@ end
|
||||
|
||||
local function render_one_window(widget_id, win, sw, sh)
|
||||
local widget_def = win.widget_def
|
||||
local bounds = resolve_bounds(win.opts.layout, sw, sh)
|
||||
local bounds = get_bounds_for(win, sw, sh)
|
||||
local panel_x, panel_y, panel_w, panel_h = bounds.x, bounds.y, bounds.w, bounds.h
|
||||
local padding = theme.padding
|
||||
|
||||
@@ -277,6 +317,16 @@ local function render_one_window(widget_id, win, sw, sh)
|
||||
is_focused = (widget_id == open_order[#open_order]),
|
||||
}
|
||||
widget_def.render(widget_ctx)
|
||||
-- v0.4.0: resize-handle is drawn even on chromeless widgets when
|
||||
-- resizable=true — the modder opted in, and the 12px corner block
|
||||
-- is the only visual cue that the window can be resized.
|
||||
if win.opts.resizable then
|
||||
engine.render.draw_rect(
|
||||
panel_x + panel_w - RESIZE_HANDLE_SIZE,
|
||||
panel_y + panel_h - RESIZE_HANDLE_SIZE,
|
||||
RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE,
|
||||
theme.border_color)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
@@ -305,6 +355,17 @@ local function render_one_window(widget_id, win, sw, sh)
|
||||
-- Stash close-button rect on the window record so hit-test can find it
|
||||
win._close_button_rect = cb
|
||||
|
||||
-- v0.4.0: resize-handle (small filled square at the SE corner) for
|
||||
-- resizable windows. Rendered after the border so it visually sits
|
||||
-- on top of the corner.
|
||||
if win.opts.resizable then
|
||||
engine.render.draw_rect(
|
||||
panel_x + panel_w - RESIZE_HANDLE_SIZE,
|
||||
panel_y + panel_h - RESIZE_HANDLE_SIZE,
|
||||
RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE,
|
||||
theme.border_color)
|
||||
end
|
||||
|
||||
-- Content area (below title bar)
|
||||
local title_area_h = theme.font_size_title + padding * 2
|
||||
win._content_bounds = {
|
||||
@@ -380,7 +441,7 @@ end
|
||||
--- _dispatch_event(event): route a synthetic or real input event.
|
||||
--- event = {kind="click", x, y, button="left"|"right"} or {kind="wheel", dy}
|
||||
---
|
||||
--- v0.3.0 routing:
|
||||
--- v0.4.0 routing:
|
||||
--- 1. Context-menu (if open) always wins.
|
||||
--- 2. Iterate reverse z_tier (top → normal → hud), within tier
|
||||
--- reverse-open-order. For each open window:
|
||||
@@ -388,9 +449,15 @@ end
|
||||
--- 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).
|
||||
--- - 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).
|
||||
--- - else: hit-test bounds; on hit:
|
||||
--- a. v0.4.0: auto-focus the window (no-op for hud-tier).
|
||||
--- b. v0.4.0: drag-start check (left-click in title-bar area of
|
||||
--- a draggable, non-chromeless window). Drag-start consumes
|
||||
--- the click — no forwarding to handle_input.
|
||||
--- c. v0.4.0: resize-start check (left-click in 12x12 SE corner
|
||||
--- of a resizable window). Resize-start consumes the click.
|
||||
--- d. Close-button check (chromeless excluded).
|
||||
--- e. Forward to widget.handle_input.
|
||||
--- 3. If no window claimed the event AND any open window has
|
||||
--- input_block="all": swallow (modal block).
|
||||
--- 4. Else drop (no game-routing — module handles its own input).
|
||||
@@ -417,7 +484,7 @@ local function _dispatch_event(event)
|
||||
-- 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)
|
||||
local mb = get_bounds_for(win, 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
|
||||
@@ -425,11 +492,68 @@ local function _dispatch_event(event)
|
||||
end
|
||||
end
|
||||
if win.opts.input_block ~= "none" then
|
||||
local b = resolve_bounds(win.opts.layout, sw, sh)
|
||||
local b = get_bounds_for(win, sw, sh)
|
||||
if event.kind == "click" then
|
||||
if event.x and event.y
|
||||
and event.x >= b.x and event.x < b.x + b.w
|
||||
and event.y >= b.y and event.y < b.y + b.h then
|
||||
-- v0.4.0: auto-focus on click for non-hud widgets.
|
||||
-- Hud-tier windows are never focusable (M.focus
|
||||
-- is a no-op for them) so calling it unconditionally
|
||||
-- is safe.
|
||||
M.focus(widget_id)
|
||||
|
||||
-- v0.4.0: drag-start check. Left-click in the
|
||||
-- title-bar area of a draggable, non-chromeless
|
||||
-- window starts a drag and consumes the click.
|
||||
-- Chromeless widgets are excluded because their
|
||||
-- title-bar is invisible — there is no defined
|
||||
-- drag-handle.
|
||||
if event.button == "left"
|
||||
and win.opts.draggable
|
||||
and not win.opts.chromeless then
|
||||
local title_bar_h = theme.font_size_title + theme.padding * 2
|
||||
if event.y >= b.y and event.y < b.y + title_bar_h then
|
||||
-- But not if the click is on the close-button.
|
||||
local cb_check = win._close_button_rect
|
||||
or close_button_rect(b.x, b.y, b.w,
|
||||
theme.padding, theme.font_size_title)
|
||||
if not (event.x >= cb_check.x
|
||||
and event.x < cb_check.x + cb_check.w
|
||||
and event.y >= cb_check.y
|
||||
and event.y < cb_check.y + cb_check.h) then
|
||||
dragging_id = widget_id
|
||||
drag_offset_x = event.x - b.x
|
||||
drag_offset_y = event.y - b.y
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- v0.4.0: resize-start check. Left-click in the
|
||||
-- 12x12 SE-corner of a resizable window starts
|
||||
-- a resize and consumes the click. Render order
|
||||
-- gives drag priority on tiny windows where the
|
||||
-- title-bar overlaps the SE corner: drag-check
|
||||
-- runs first above and would already have
|
||||
-- returned. Resize is allowed on chromeless
|
||||
-- widgets (the 12px handle is the visual cue).
|
||||
if event.button == "left" and win.opts.resizable then
|
||||
local hw = RESIZE_HANDLE_SIZE
|
||||
local hx = b.x + b.w - hw
|
||||
local hy = b.y + b.h - hw
|
||||
if event.x >= hx and event.x < hx + hw
|
||||
and event.y >= hy and event.y < hy + hw then
|
||||
resizing_id = widget_id
|
||||
resize_corner = "SE"
|
||||
resize_start_w = b.w
|
||||
resize_start_h = b.h
|
||||
resize_start_mx = event.x
|
||||
resize_start_my = event.y
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Close-button intercept (top-right X): left-click
|
||||
-- closes the window before forwarding to the widget.
|
||||
-- Chromeless widgets have no chrome (no X), so we
|
||||
@@ -543,6 +667,17 @@ end
|
||||
--- without panel-lib decoration. Useful for HUD-style
|
||||
--- widgets that have their own visual design (status
|
||||
--- bars, toolbars with their own theme).
|
||||
--- draggable = false (default) | true — when true, left-click in
|
||||
--- the title-bar area starts a drag. Ignored if the
|
||||
--- widget is chromeless (no defined drag-handle).
|
||||
--- v0.4.0+.
|
||||
--- resizable = false (default) | true — when true, a 12x12 SE-corner
|
||||
--- handle is rendered + hit-tested for left-click resize.
|
||||
--- Works on chromeless widgets too. v0.4.0+.
|
||||
--- min_size = {w=number, h=number} — minimum window size when
|
||||
--- resizing. Defaults to {w=120, h=80}. v0.4.0+.
|
||||
--- max_size = {w=number, h=number} — maximum window size when
|
||||
--- resizing. Defaults to {w=99999, h=99999}. v0.4.0+.
|
||||
function M.register(widget_id, widget_def, opts)
|
||||
if type(widget_id) ~= "string" then
|
||||
error("panel.register: widget_id must be a string, got " .. type(widget_id))
|
||||
@@ -587,6 +722,28 @@ function M.register(widget_id, widget_def, opts)
|
||||
local persistent = opts.persistent == true
|
||||
local chromeless = opts.chromeless == true
|
||||
|
||||
-- v0.4.0: validate drag/resize opts.
|
||||
local draggable = opts.draggable == true
|
||||
local resizable = opts.resizable == true
|
||||
if opts.min_size ~= nil and type(opts.min_size) ~= "table" then
|
||||
error(string.format(
|
||||
"panel.register: min_size must be a table {w=,h=} (got %s)",
|
||||
type(opts.min_size)), 2)
|
||||
end
|
||||
if opts.max_size ~= nil and type(opts.max_size) ~= "table" then
|
||||
error(string.format(
|
||||
"panel.register: max_size must be a table {w=,h=} (got %s)",
|
||||
type(opts.max_size)), 2)
|
||||
end
|
||||
local min_size = {
|
||||
w = (opts.min_size and opts.min_size.w) or 120,
|
||||
h = (opts.min_size and opts.min_size.h) or 80,
|
||||
}
|
||||
local max_size = {
|
||||
w = (opts.max_size and opts.max_size.w) or 99999,
|
||||
h = (opts.max_size and opts.max_size.h) or 99999,
|
||||
}
|
||||
|
||||
widgets[widget_id] = widget_def
|
||||
windows[widget_id] = {
|
||||
widget_def = widget_def,
|
||||
@@ -596,8 +753,16 @@ function M.register(widget_id, widget_def, opts)
|
||||
persistent = persistent,
|
||||
input_block = input_block,
|
||||
chromeless = chromeless,
|
||||
draggable = draggable,
|
||||
resizable = resizable,
|
||||
min_size = min_size,
|
||||
max_size = max_size,
|
||||
},
|
||||
open = false,
|
||||
-- v0.4.0: bounds_override is nil until drag/resize sets it. When
|
||||
-- non-nil it shadows resolve_bounds via get_bounds_for. Cleared
|
||||
-- on close (handled in M.close).
|
||||
bounds_override = nil,
|
||||
}
|
||||
|
||||
-- v0.3.0: persistent windows are open from register-time and pushed
|
||||
@@ -667,6 +832,12 @@ function M.close(widget_id)
|
||||
w.open = false
|
||||
local idx = find_in_array(open_order, widget_id)
|
||||
if idx then table.remove(open_order, idx) end
|
||||
-- v0.4.0: cancel in-flight drag/resize if it references this window.
|
||||
if dragging_id == widget_id then dragging_id = nil end
|
||||
if resizing_id == widget_id then
|
||||
resizing_id = nil
|
||||
resize_corner = nil
|
||||
end
|
||||
-- Context-menu was attached to whatever was on top — clear it if our close
|
||||
-- removed the focused window.
|
||||
if #open_order == 0 then
|
||||
@@ -720,7 +891,7 @@ function M.point_in_any_panel(x, y)
|
||||
for _, id in ipairs(open_order) do
|
||||
local w = windows[id]
|
||||
if w and w.open then
|
||||
local b = resolve_bounds(w.opts.layout, sw, sh)
|
||||
local b = get_bounds_for(w, sw, sh)
|
||||
if x >= b.x and x < b.x + b.w
|
||||
and y >= b.y and y < b.y + b.h then
|
||||
return true
|
||||
@@ -730,6 +901,60 @@ function M.point_in_any_panel(x, y)
|
||||
return false
|
||||
end
|
||||
|
||||
-- -----------------------------------------------------------------------
|
||||
-- v0.4.0 — Focus API
|
||||
-- -----------------------------------------------------------------------
|
||||
|
||||
--- M.focus(widget_id)
|
||||
--- Brings widget_id to the top within its z_tier (within open_order).
|
||||
--- No-op if the widget is not registered, not open, or in hud-tier.
|
||||
--- Hud-tier widgets are by design never "focused" — they are cosmetic
|
||||
--- overlays and click-to-focus auto-routes do not promote them.
|
||||
function M.focus(widget_id)
|
||||
local w = windows[widget_id]
|
||||
if not w or not w.open then return end
|
||||
if w.opts.z_tier == "hud" then return end
|
||||
local idx = find_in_array(open_order, widget_id)
|
||||
if idx then
|
||||
table.remove(open_order, idx)
|
||||
end
|
||||
open_order[#open_order + 1] = widget_id
|
||||
end
|
||||
|
||||
--- M.raise(widget_id) — alias for focus(); reads as "raise to top".
|
||||
M.raise = M.focus
|
||||
|
||||
--- M.lower(widget_id)
|
||||
--- Moves widget_id to the bottom of open_order. No-op if the widget is
|
||||
--- not registered or not open. Unlike focus, lower works for hud-tier
|
||||
--- widgets too (lowering within tier is a layering operation, not focus).
|
||||
function M.lower(widget_id)
|
||||
local w = windows[widget_id]
|
||||
if not w or not w.open then return end
|
||||
local idx = find_in_array(open_order, widget_id)
|
||||
if idx then
|
||||
table.remove(open_order, idx)
|
||||
table.insert(open_order, 1, widget_id)
|
||||
end
|
||||
end
|
||||
|
||||
--- M.get_focused() → widget_id or nil
|
||||
--- Returns the currently-focused widget_id. Focus is the last-opened id
|
||||
--- in the topmost occupied z_tier. If a top-tier window is open it wins
|
||||
--- regardless of normal/hud-tier windows above it in open_order. If no
|
||||
--- top-tier window is open, returns the last-opened id (which may be a
|
||||
--- normal- or hud-tier window).
|
||||
function M.get_focused()
|
||||
if #open_order == 0 then return nil end
|
||||
for i = #open_order, 1, -1 do
|
||||
local id = open_order[i]
|
||||
if windows[id] and windows[id].opts.z_tier == "top" then
|
||||
return id
|
||||
end
|
||||
end
|
||||
return open_order[#open_order]
|
||||
end
|
||||
|
||||
-- -----------------------------------------------------------------------
|
||||
-- Theme
|
||||
-- -----------------------------------------------------------------------
|
||||
@@ -876,6 +1101,69 @@ function M.update(dt)
|
||||
_dispatch_event({ kind = "click", x = mx, y = my, button = "right" })
|
||||
end
|
||||
|
||||
-- v0.4.0: drag update. While dragging and the left mouse button is
|
||||
-- still held, update bounds_override per cur-mouse - drag_offset.
|
||||
-- Screen-clamp keeps at least MIN_VISIBLE_PX of title-bar visible
|
||||
-- so the user can always grab and drag the window back on-screen.
|
||||
if dragging_id then
|
||||
if cur_left then
|
||||
local win = windows[dragging_id]
|
||||
if win and win.open then
|
||||
local sw, sh = get_screen_size()
|
||||
local current_b = get_bounds_for(win, sw, sh)
|
||||
local new_x = mx - drag_offset_x
|
||||
local new_y = my - drag_offset_y
|
||||
-- Clamp x: at least MIN_VISIBLE_PX of width visible on
|
||||
-- both edges. (-current_b.w + MIN_VISIBLE_PX) = the leftmost
|
||||
-- x for which MIN_VISIBLE_PX still pokes out at the right.
|
||||
new_x = math.max(-current_b.w + MIN_VISIBLE_PX,
|
||||
math.min(sw - MIN_VISIBLE_PX, new_x))
|
||||
-- Clamp y: title-bar must stay within the top edge so the
|
||||
-- user can always grab it; allow vertical extent past
|
||||
-- bottom but keep top of title-bar at y >= 0.
|
||||
new_y = math.max(0, math.min(sh - MIN_VISIBLE_PX, new_y))
|
||||
win.bounds_override = {
|
||||
x = new_x, y = new_y, w = current_b.w, h = current_b.h,
|
||||
}
|
||||
else
|
||||
dragging_id = nil
|
||||
end
|
||||
else
|
||||
-- Mouse released: drag ends.
|
||||
dragging_id = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- v0.4.0: resize update. While resizing and the left mouse button is
|
||||
-- still held, update bounds_override w/h per resize_start + delta,
|
||||
-- clamped to min/max_size.
|
||||
if resizing_id then
|
||||
if cur_left then
|
||||
local win = windows[resizing_id]
|
||||
if win and win.open then
|
||||
local sw, sh = get_screen_size()
|
||||
local current_b = get_bounds_for(win, sw, sh)
|
||||
local new_w = resize_start_w + (mx - resize_start_mx)
|
||||
local new_h = resize_start_h + (my - resize_start_my)
|
||||
local min_w = win.opts.min_size.w
|
||||
local min_h = win.opts.min_size.h
|
||||
local max_w = win.opts.max_size.w
|
||||
local max_h = win.opts.max_size.h
|
||||
new_w = math.max(min_w, math.min(max_w, new_w))
|
||||
new_h = math.max(min_h, math.min(max_h, new_h))
|
||||
win.bounds_override = {
|
||||
x = current_b.x, y = current_b.y, w = new_w, h = new_h,
|
||||
}
|
||||
else
|
||||
resizing_id = nil
|
||||
resize_corner = nil
|
||||
end
|
||||
else
|
||||
resizing_id = nil
|
||||
resize_corner = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Wheel dispatch
|
||||
local wheel_amt = engine.input.get_mouse_wheel()
|
||||
if wheel_amt ~= 0 then
|
||||
@@ -927,13 +1215,22 @@ function M._dispatch_event_for_test(event)
|
||||
end
|
||||
|
||||
--- M._test_reset_all() — clears widgets + windows + open_order + ctx_menu.
|
||||
--- Note: theme + triggers preserved across reset; tests that need
|
||||
--- those reset should do it explicitly.
|
||||
--- Also clears v0.4.0 drag/resize state. Note: theme + triggers preserved
|
||||
--- across reset; tests that need those reset should do it explicitly.
|
||||
function M._test_reset_all()
|
||||
widgets = {}
|
||||
windows = {}
|
||||
open_order = {}
|
||||
ctx_menu = nil
|
||||
widgets = {}
|
||||
windows = {}
|
||||
open_order = {}
|
||||
ctx_menu = nil
|
||||
dragging_id = nil
|
||||
drag_offset_x = 0
|
||||
drag_offset_y = 0
|
||||
resizing_id = nil
|
||||
resize_corner = nil
|
||||
resize_start_w = 0
|
||||
resize_start_h = 0
|
||||
resize_start_mx = 0
|
||||
resize_start_my = 0
|
||||
end
|
||||
|
||||
--- M._test_get_open_ids() — returns array of currently-open ids in
|
||||
@@ -951,12 +1248,13 @@ function M._test_get_focused_id()
|
||||
end
|
||||
|
||||
--- M._test_get_window_bounds(widget_id) — resolved bounds for an open
|
||||
--- window, or nil if not open.
|
||||
--- window, or nil if not open. v0.4.0: returns bounds_override when set
|
||||
--- (so drag/resize tests can observe the effective bounds).
|
||||
function M._test_get_window_bounds(widget_id)
|
||||
local w = windows[widget_id]
|
||||
if not w or not w.open then return nil end
|
||||
local sw, sh = get_screen_size()
|
||||
return resolve_bounds(w.opts.layout, sw, sh)
|
||||
return get_bounds_for(w, sw, sh)
|
||||
end
|
||||
|
||||
--- M._test_get_screen_size() — current screen size used by layouts.
|
||||
@@ -996,4 +1294,117 @@ function M._test_get_persistent(widget_id)
|
||||
return w.opts.persistent
|
||||
end
|
||||
|
||||
-- =====================================================================
|
||||
-- v0.4.0 test backdoors (drag + resize simulation without engine.input)
|
||||
-- =====================================================================
|
||||
|
||||
--- M._test_simulate_drag_start(widget_id, mx, my)
|
||||
--- Forces drag-state as if a left-click at (mx, my) had landed in the
|
||||
--- title-bar of widget_id. Bypasses bounds-hit and hit-test gates so
|
||||
--- tests don't have to compute the title-bar y range. Loud-error if
|
||||
--- the widget is not open. v0.4.0.
|
||||
function M._test_simulate_drag_start(widget_id, mx, my)
|
||||
local w = windows[widget_id]
|
||||
if not w or not w.open then
|
||||
error("panel._test_simulate_drag_start: widget '" .. tostring(widget_id)
|
||||
.. "' is not open")
|
||||
end
|
||||
local sw, sh = get_screen_size()
|
||||
local b = get_bounds_for(w, sw, sh)
|
||||
dragging_id = widget_id
|
||||
drag_offset_x = mx - b.x
|
||||
drag_offset_y = my - b.y
|
||||
M.focus(widget_id)
|
||||
end
|
||||
|
||||
--- M._test_simulate_drag_move(mx, my)
|
||||
--- Drives one drag-step at (mx, my) as if M.update saw the mouse there
|
||||
--- with the left button still held. Performs the same clamp + bounds
|
||||
--- math M.update does. No-op if no drag is in progress. v0.4.0.
|
||||
function M._test_simulate_drag_move(mx, my)
|
||||
if not dragging_id then return end
|
||||
local win = windows[dragging_id]
|
||||
if not win or not win.open then
|
||||
dragging_id = nil
|
||||
return
|
||||
end
|
||||
local sw, sh = get_screen_size()
|
||||
local current_b = get_bounds_for(win, sw, sh)
|
||||
local new_x = mx - drag_offset_x
|
||||
local new_y = my - drag_offset_y
|
||||
new_x = math.max(-current_b.w + MIN_VISIBLE_PX,
|
||||
math.min(sw - MIN_VISIBLE_PX, new_x))
|
||||
new_y = math.max(0, math.min(sh - MIN_VISIBLE_PX, new_y))
|
||||
win.bounds_override = {
|
||||
x = new_x, y = new_y, w = current_b.w, h = current_b.h,
|
||||
}
|
||||
end
|
||||
|
||||
--- M._test_simulate_drag_end()
|
||||
--- Releases the drag (as if the mouse button went up). v0.4.0.
|
||||
function M._test_simulate_drag_end()
|
||||
dragging_id = nil
|
||||
end
|
||||
|
||||
--- M._test_simulate_resize_start(widget_id, mx, my)
|
||||
--- Forces resize-state as if a left-click at (mx, my) had landed in the
|
||||
--- SE-corner of widget_id. Loud-error if the widget is not open. v0.4.0.
|
||||
function M._test_simulate_resize_start(widget_id, mx, my)
|
||||
local w = windows[widget_id]
|
||||
if not w or not w.open then
|
||||
error("panel._test_simulate_resize_start: widget '" .. tostring(widget_id)
|
||||
.. "' is not open")
|
||||
end
|
||||
local sw, sh = get_screen_size()
|
||||
local b = get_bounds_for(w, sw, sh)
|
||||
resizing_id = widget_id
|
||||
resize_corner = "SE"
|
||||
resize_start_w = b.w
|
||||
resize_start_h = b.h
|
||||
resize_start_mx = mx
|
||||
resize_start_my = my
|
||||
M.focus(widget_id)
|
||||
end
|
||||
|
||||
--- M._test_simulate_resize_move(mx, my)
|
||||
--- Drives one resize-step at (mx, my) with min/max-size clamping. v0.4.0.
|
||||
function M._test_simulate_resize_move(mx, my)
|
||||
if not resizing_id then return end
|
||||
local win = windows[resizing_id]
|
||||
if not win or not win.open then
|
||||
resizing_id = nil
|
||||
resize_corner = nil
|
||||
return
|
||||
end
|
||||
local sw, sh = get_screen_size()
|
||||
local current_b = get_bounds_for(win, sw, sh)
|
||||
local new_w = resize_start_w + (mx - resize_start_mx)
|
||||
local new_h = resize_start_h + (my - resize_start_my)
|
||||
local min_w = win.opts.min_size.w
|
||||
local min_h = win.opts.min_size.h
|
||||
local max_w = win.opts.max_size.w
|
||||
local max_h = win.opts.max_size.h
|
||||
new_w = math.max(min_w, math.min(max_w, new_w))
|
||||
new_h = math.max(min_h, math.min(max_h, new_h))
|
||||
win.bounds_override = {
|
||||
x = current_b.x, y = current_b.y, w = new_w, h = new_h,
|
||||
}
|
||||
end
|
||||
|
||||
--- M._test_simulate_resize_end() — releases the resize. v0.4.0.
|
||||
function M._test_simulate_resize_end()
|
||||
resizing_id = nil
|
||||
resize_corner = nil
|
||||
end
|
||||
|
||||
--- M._test_get_dragging_id() — currently dragged widget_id, or nil. v0.4.0.
|
||||
function M._test_get_dragging_id()
|
||||
return dragging_id
|
||||
end
|
||||
|
||||
--- M._test_get_resizing_id() — currently resized widget_id, or nil. v0.4.0.
|
||||
function M._test_get_resizing_id()
|
||||
return resizing_id
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user