Stop the hover tooltip from clipping off the left screen edge
The Combo Advisor info popup is right-anchored to the hovered card and grows leftward with no screen clamp (vanilla align_h_popup leaves lr_clamp off), so its width is set by its single widest row -- and several rows had no cap: the fusion "Fuses into X - still needs Y" line, chained copy lines, and the "name: reason" combo / "Clashes - name: blurb" rows, which run well past a single row once a long joker name meets a full 34-char blurb. On a left-of-centre shop card the box ran off the screen and clipped. Enforce one width invariant on every rendered line via a wrap_text helper (word-wrap, never splits a name, keeps continuation indents) and a seg_row for the two-segment lines. The prefixed wrap_names blocks (Looking for:, Offers:, also:, Feeds:, Fusing drops) now budget for their prefix instead of adding it on top. Size the budget to the card's actual gutter rather than a fixed number: the room width is a constant in game units whatever the window size, so the real variable is where the card sits and which side vanilla opens the popup on. row_budget measures the gutter with the game's own text metric (exact, resolution-independent) -- roomy for a far-side card, tight near the 40% flip point so it never clips, and it tracks an ultrawide/room-widening mod for free. Missing geometry falls back to the fixed budget. The one estimate, MAIN_BOX_UNITS (the card's own desc box between us and the card), is sized ~1 unit generous to bias toward wrapping early over clipping; dbg() logs the inputs for later calibration. Tests: real row-builder driven to its widest row and asserted within budget; wrap_text budget/indent/word-integrity; row_budget fallback, monotonicity and clamp. 936 pass on 5.4 and LuaJIT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1108,6 +1108,78 @@ build('Traps last page', function()
|
||||
G.FUNCS.jca_page_Traps{cycle_config = {current_option = 999}}
|
||||
end)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
section('UI: advice rows fit the tooltip gutter')
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- The hover info popup is right-anchored to the card and grows leftward with no
|
||||
-- screen clamp, so a row wider than the shop's left gutter runs off-screen and
|
||||
-- clips (the reported bug). Every rendered line -- summed across its coloured
|
||||
-- segments -- must stay within the budget, however long the name or blurb.
|
||||
local BUDGET = JCA._row_budget
|
||||
|
||||
-- wrap_text: never exceeds the budget, never splits a word, loses no content.
|
||||
do
|
||||
local long = 'Fuses into Obelisk ($12) - still needs Wrathful Joker and Blueprint'
|
||||
local lines = JCA._wrap_text(long, BUDGET)
|
||||
local wide = false
|
||||
for _, l in ipairs(lines) do if #l > BUDGET then wide = true end end
|
||||
ok(not wide, 'wrap_text keeps every line within budget')
|
||||
eq(table.concat(lines, ' '), long, 'wrap_text loses no words')
|
||||
eq(#JCA._wrap_text(string.rep('x', BUDGET + 10), BUDGET), 1,
|
||||
'wrap_text leaves an unbreakable long word on its own line')
|
||||
-- Continuation lines arrive pre-indented (" Baron, Mime"); the indent must
|
||||
-- survive and still count against the budget.
|
||||
local indented = JCA._wrap_text(' ' .. string.rep('a', BUDGET - 2), BUDGET)
|
||||
eq(#indented, 1, 'an indented line at budget does not wrap')
|
||||
ok(indented[1]:sub(1, 2) == ' ', 'wrap_text keeps the leading indent')
|
||||
ok(#indented[1] <= BUDGET, 'indent counts against the budget')
|
||||
end
|
||||
|
||||
-- row_budget sizes each row to the card's actual gutter, using the game's own
|
||||
-- text metric. Window size never enters it (the room is a constant in units);
|
||||
-- only the card's position does. With no geometry it must fall back to the fixed
|
||||
-- budget so headless callers -- and this suite -- keep a stable width.
|
||||
do
|
||||
eq(JCA._row_budget_for({}), BUDGET,
|
||||
'row_budget falls back to the fixed budget with no card geometry')
|
||||
-- Stub the font metric: getWidth in "pixels", scaled to ~0.15 units/char.
|
||||
G.TILESIZE = 0.32 / 0.15
|
||||
G.LANG = {font = {squish = 1, FONTSCALE = 1,
|
||||
FONT = {getWidth = function(_, s) return #s end}}}
|
||||
local roomy = JCA._row_budget_for({T = {x = 17, w = 1}}) -- far right, wide gutter
|
||||
local tight = JCA._row_budget_for({T = {x = 8.1, w = 1}}) -- just past the flip
|
||||
ok(roomy > tight, 'a card with more room gets a wider budget',
|
||||
('roomy=%d tight=%d'):format(roomy, tight))
|
||||
ok(tight >= 20 and roomy <= 64, 'the budget stays within a sane clamp',
|
||||
('roomy=%d tight=%d'):format(roomy, tight))
|
||||
G.TILESIZE, G.LANG = nil, nil
|
||||
end
|
||||
|
||||
-- The widest real row is a clash against a long-named joker carrying a full
|
||||
-- 34-char blurb: "Clashes - <name>: <blurb>" is ~64 chars and must wrap.
|
||||
G.shop_jokers = {}
|
||||
G.jokers = {cards = {{config = {center = {key = 'j_gluttenous_joker', set = 'Joker'}}}}}
|
||||
JCA.register_clash('j_widetest', 'j_gluttenous_joker', string.rep('x', BLURB_MAX))
|
||||
local hovered = {
|
||||
config = {center = {key = 'j_widetest', set = 'Joker'}},
|
||||
ability = {}, area = G.shop_jokers,
|
||||
}
|
||||
local rows = JCA._tooltip_rows(hovered)
|
||||
local widest, saw_clash = 0, false
|
||||
for _, row in ipairs(rows) do
|
||||
local w = 0
|
||||
for _, seg in ipairs(row) do w = w + #((seg.config or {}).text or '') end
|
||||
if w > widest then widest = w end
|
||||
for _, seg in ipairs(row) do
|
||||
if ((seg.config or {}).text or ''):find('Clashes') then saw_clash = true end
|
||||
end
|
||||
end
|
||||
ok(saw_clash, 'the clash line is present to be measured')
|
||||
ok(widest <= BUDGET, 'every advice row fits the budget',
|
||||
('widest row is %d chars (budget %d)'):format(widest, BUDGET))
|
||||
G.jokers, G.shop_jokers = nil, nil
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
print(('\n%d passed, %d failed'):format(pass, fail))
|
||||
os.exit(fail == 0 and 0 or 1)
|
||||
|
||||
Reference in New Issue
Block a user