diff --git a/crates/benilla-app/assets/ui/MapCanvas.lua b/crates/benilla-app/assets/ui/MapCanvas.lua index 9b392708..4af321d0 100644 --- a/crates/benilla-app/assets/ui/MapCanvas.lua +++ b/crates/benilla-app/assets/ui/MapCanvas.lua @@ -68,6 +68,9 @@ local SCALE_RATE = 24 -- A drag under this many pixels is still a click, which is what `WouldCursorPositionBeClick` -- answers; Era's own threshold. local CLICK_SLOP = 5 +-- The stratum `WorldMapFrame.xml` declares for itself, re-asserted on every show; see +-- `SeatLevels`. +local MAP_STRATA = "FULLSCREEN" -------------------------------------------------------------------------------- -- PinFrameLevelsManager @@ -605,6 +608,14 @@ function MapCanvasScrollControllerMixin:InstantPanAndZoom(scale, x, y) self.currentScale = scale self.targetScale = scale self.zoomTarget = scale + -- Pulled onto the ladder BEFORE the child is scaled, which the ease path below already does + -- by construction (it applies `self.currentScale`) and this one did not. The child's scale is + -- what the player SEES; `GetCanvasScale` -- which clamps, in `ClampZoomToLevels` -- is what + -- every scroll extent, pan clamp and pin placement is computed FROM. Setting the raw argument + -- let the two disagree: past the ladder's ceiling the art drew at one scale while the whole + -- framework believed another, so the extents were computed for the wrong canvas size and the + -- view would not pan where it should. + scale = self:GetCanvasScale() if self.Child then self.Child:SetScale(scale) end @@ -2066,7 +2077,26 @@ local function BuildCanvas() -- Re-asserted on every show, not seated once: `WorldMapFrame` is level 0 while this builds -- and 1 once `ShowUIPanel` has raised it, so a level computed at build time ties the moment -- the window is first opened. + -- ...and the window's own stratum with them, because a reparent takes it away. + -- + -- `WorldMapFrame.xml:5` declares `frameStrata="FULLSCREEN"`, which is what puts 1.12's map + -- over every other window. The realm's own `WorldMapFrame_Minimize` + -- (`WorldMapFrame.lua:183`, reached from `WorldMapFrame_OnEvent`) calls + -- `WorldMapFrame:SetParent(UIParent)`, and a reparent forces the moved frame into its new + -- parent's stratum (`0x76ab5a`, pinned by `script::tests::setparent`) -- so on every open the + -- window fell into `UIParent`'s MEDIUM at level 0, underneath every pfUI frame, pfUI's unit + -- frames being MEDIUM themselves (`pfUI/api/unitframes.lua:413`). Measured on grom's install: + -- FULLSCREEN with no addons loaded, MEDIUM/0 with his. grom, 2026-11-05: *"the map needs to be + -- on a higher level as I can see my unitframe over it."* + -- + -- Re-asserted rather than fixed in the reparent, because the reparent is right: the force is + -- read off the binary, and the realm's minimize is the thing that spends it. Here it is one + -- window's own declared stratum restored after the code that drops it, on the same hook that + -- already re-seats the levels for the same reason. local function SeatLevels() + if map:GetFrameStrata() ~= MAP_STRATA then + map:SetFrameStrata(MAP_STRATA) + end local base = map:GetFrameLevel() or 0 sc:SetFrameLevel(base + 1) if canvas:GetFrameLevel() <= sc:GetFrameLevel() then @@ -2078,6 +2108,14 @@ local function BuildCanvas() end SeatLevels() map:HookScript("OnShow", SeatLevels) + -- The minimize is where the stratum goes, so re-seat on it rather than waiting for the next + -- show: it runs from `WorldMapFrame_OnEvent`, which fires while the window is already up. + if WorldMapFrame_Minimize then + hooksecurefunc("WorldMapFrame_Minimize", SeatLevels) + end + if WorldMapFrame_Maximize then + hooksecurefunc("WorldMapFrame_Maximize", SeatLevels) + end sc:SetScript("OnMouseDown", function() sc:OnMouseDown(arg1) end) @@ -2095,6 +2133,146 @@ local function BuildCanvas() sc.mapID = map.mapID sc:OnCanvasSizeChanged() + -- THE PLAYER ARROW, THE ONE PIECE OF 1.12'S MAP CONTENT THAT IS NOT IN THE CANVAS + -- + -- `WorldMapFrame_OnLoad` builds it with `CreateWorldMapArrowFrame(WorldMapFrame)` + -- (WorldMapFrame.lua:19), so its parent is the WINDOW, while everything it is anchored + -- against was reparented into the canvas above. `SetPoint`'s offsets are in the positioned + -- frame's OWN units, so the offsets `WorldMapButton_OnUpdate` computes -- + -- `playerX * WorldMapDetailFrame:GetWidth()`, times `WorldMapDetailFrame:GetScale()`, which + -- `SeatOnCanvas` pins at 1 because the zoom lives one level up on `ScrollContainer.Child` -- + -- are short by exactly the canvas zoom. grom, 2026-11-05: *"my player arrow goes to the wrong + -- spot at any zoom level, this needs to be accurate all of the time."* Measured on a 1600x900 + -- harness with the view centred on the player: exact at zoom 1, 250.5 px right and 501.0 px + -- down at zoom 2, so the error is the zoom and nothing else. + -- + -- Note that `WorldMapPlayer` -- the invisible mouseover rect for the same spot, set from the + -- same two FrameXML lines -- has always been right, because 1.12's XML nests it under + -- `WorldMapButton`, which IS in the canvas. That is what makes it the oracle the test uses + -- instead of a recomputed expectation. + -- + -- Why not reparent the arrow as well: it is a size-less `Model` whose rect is the arrow + -- file's bounding box, so inside the canvas it would grow with the zoom, and there is no + -- frame scale that fixes the size without undoing the position -- both read the same + -- effective scale. Outside the canvas it keeps one apparent size at every zoom, which is what + -- the player pin does on every later client. + -- + -- The cost of staying outside is clipping: the container crops its own subtree and the arrow + -- is not in it. So the bounds test below, which is a hide the stock code never needed because + -- a map that cannot pan cannot put the player off-screen. + local arrow + for _, child in ipairs({ map:GetChildren() }) do + if child.GetObjectType and child:GetObjectType() == "Model" then + arrow = child + break + end + end + + -- Runs every frame the map is open, so nothing in it may force a layout resolve. Two rules + -- follow from that, and both are why this reads the frames it reads: + -- + -- * the ART'S SIZE comes off `WorldMapButton`, not off `WorldMapDetailFrame`. The detail + -- frame is `SetAllPoints` to the canvas, so its width is DERIVED and asking for it + -- resolves the tree; the button carries the same rect as an explicit `SetWidth`, for the + -- reason `SeatOnCanvas` states (pfUI reads it). + -- * the BOUNDS TEST is arithmetic on the controller's own normalized scroll, not a rect + -- comparison. `GetLeft` on a dirty tree measured 30-370 us against 0.42 us settled + -- (`benilla-getleft-forces-a-layout-resolve`), and a pan writes the scroll every frame, + -- so the tree is dirty every frame this runs — six of those is a millisecond of nothing. + local function SeatPlayerArrow() + if not map:IsVisible() then + return + end + local px, py = GetPlayerMapPosition("player") + if not px or not py or (px == 0 and py == 0) then + return -- the stock update has already hidden it + end + local de = detail:GetEffectiveScale() + local ae = arrow and arrow:GetEffectiveScale() or map:GetEffectiveScale() + if not de or not ae or de == 0 or ae == 0 then + return + end + local aw = button and button:GetWidth() or 0 + local ah = button and button:GetHeight() or 0 + if aw <= 0 or ah <= 0 then + return + end + -- In the detail frame's own units, which is where the art and every 1.12 icon live... + -- ...and out again into the arrow's, which is the whole conversion that was missing. + local k = de / ae + PositionWorldMapArrowFrame("CENTER", "WorldMapDetailFrame", "TOPLEFT", + px * aw * k, -py * ah * k) + + -- Hidden where the container would have cropped it, which it cannot do for a frame that + -- is not in the container. The view is the box of half-width `halfU` around the scroll + -- centre, in the canvas's own normalized space -- the same numbers + -- `CalculateScrollExtentsAtScale` works in, which is why they are asked for rather than + -- recomputed. + local scale = sc:GetCanvasScale() + if scale and scale > 0 then + local halfU = sc:NormalizeHorizontalSize(sc:GetWidth() * 0.5 / scale) + local halfV = sc:NormalizeVerticalSize(sc:GetHeight() * 0.5 / scale) + local cu = sc:GetNormalizedHorizontalScroll() or 0.5 + local cv = sc:GetNormalizedVerticalScroll() or 0.5 + if math.abs(px - cu) > halfU or math.abs(py - cv) > halfV then + ShowWorldMapArrowFrame(nil) + end + end + end + + -- THE MAP FADES WHILE THE PLAYER IS MOVING + -- + -- grom, 2026-11-05: *"It would also be nice if the whole map window went to 80% + -- transparentcy when the player is moving."* `mapFadeWhileMoving` is the percent, 0 off, with + -- a row on the Options window's Interface page; 1.12 has no such behaviour at all. + -- + -- Written as a FACTOR of whatever alpha the window already has, not as an absolute, because + -- the window's alpha belongs to somebody else: pfUI sets it from its own saved config and + -- offers shift-wheel over the map to change it (`pfUI/modules/map.lua:67`). So the base is + -- re-read whenever the live alpha is not the one this last wrote — that is the signature of + -- another writer, and it makes their setting the thing that gets faded rather than the thing + -- that gets overwritten. + -- + -- `GetSpeed()` is the movement test: 1.12 has no "is the player moving" verb, and this one is + -- ours, always present, and 0 standing still by construction (`compat/superwow.rs`) — the same + -- move flags the engine answers `IsSwimming` and `IsMounted` from, so it cannot disagree with + -- them about what the player is doing this frame. + local fadeBase, fadeApplied + local function FadeWhileMoving() + if not map:IsVisible() then + return + end + local pct = tonumber(GetCVar("mapFadeWhileMoving")) or 0 + local now = map:GetAlpha() or 1 + if not fadeApplied or math.abs(now - fadeApplied) > 0.001 then + fadeBase = now -- somebody else wrote it: that is the alpha to fade FROM + end + local want = fadeBase or 1 + if pct > 0 and GetSpeed and GetSpeed() > 0 then + want = want * (1 - pct / 100) + end + if math.abs(now - want) > 0.001 then + map:SetAlpha(want) + end + fadeApplied = want + end + + if WorldMapButton_OnUpdate then + -- Hooked rather than replaced, and hooked on the function that places it, so if anything + -- ever suppresses the stock update this stops running with it rather than fighting it. + hooksecurefunc("WorldMapButton_OnUpdate", function() + SeatPlayerArrow() + FadeWhileMoving() + end) + end + -- A map closed while faded must not come back faded: the base is whatever it was before. + map:HookScript("OnHide", function() + if fadeApplied and fadeBase and math.abs((map:GetAlpha() or 1) - fadeApplied) <= 0.001 then + map:SetAlpha(fadeBase) + end + fadeApplied = nil + end) + map:HookScript("OnShow", function() WorldMapFrame:SyncFromSelection() WorldMapFrame:RefreshAllDataProviders(true) diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index e283e8e4..31dbb614 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -479,6 +479,14 @@ .. "In Windowed (Fullscreen) the window already fills the screen, so this sets the " .. "size it returns to in Windowed. Takes effect on Apply." + -- 1.12 has no such setting: its map does not fade, because its map cannot be read while + -- you walk either way (it covers the screen and takes the keyboard). + BENILLA_TOOLTIP_MAP_FADE_WHILE_MOVING = + "How far the world map fades out while you are moving, so you can keep it open and " + .. "still see where you are going.\n\n" + .. "At 0 the map does not fade. The map's own alpha is whatever you or your interface " + .. "addon set it to; this fades from there, and restores it the moment you stop." + -- 1.12's GAMMA text points at gray-bar art this page does not have. BENILLA_TOOLTIP_BRIGHTNESS = "Lightens or darkens the whole picture — nights, caves and interiors are what it is " @@ -1573,6 +1581,8 @@ "RowInstantQuestText", "RowBuffDurations", "RowAutoQuestWatch", "RowProfanityFilter", "RowShowTutorials", "RowTipOfTheDay", "RowNewbieTips", + -- Appended, as this list's own rule says: the tests assert positional indices. + "RowMapFadeWhileMoving", }, -- 1.12's chat box (UIOptionsFrame.xml:751-840). Not shipped: Simple Chat, which needs -- FCF_Set_SimpleChat's re-layout, Lock Chat Settings and Guild Recruitment. @@ -1924,6 +1934,15 @@ if n <= 0 then return BENILLA_ADVGFX.everyFrame end return string.format(BENILLA_ADVGFX.hz, n) end + -- A row whose stored value ALREADY IS a percent, where the fallback below would + -- multiply by 100 and print 8000%. 0 reads "Off" rather than "0%", on the precedent + -- the anisotropy groove sets below: a zero that means "not at all" is a word, because + -- a number next to the next stop up reads as the weakest setting rather than as none. + if row.fmt == "pct" then + local n = math.floor(value + 0.5) + if n <= 0 then return "Off" end + return string.format("%d%%", n) + end -- The anisotropy groove. 1 is not "1x", it is the feature OFF (`tex_filter.rs`: -- the reference writes filter mode 5 only at 2 and above), and a player reading "1x" -- next to "2x" would take it for the weakest setting rather than for none. @@ -5043,6 +5062,20 @@ "Show Detailed Tooltips", "OPTION_TOOLTIP_SHOW_NEWBIE_TIPS") + + + + + + + + OptionsRow_OnLoad(self, "mapFadeWhileMoving", + "Fade World Map While Moving", + "BENILLA_TOOLTIP_MAP_FADE_WHILE_MOVING") + OptionsSliderRow_Setup(self, 0, 90, 10, "pct") + +