The canvas gave the map no mouse surface of its own, on the reasoning that 1.12's
`WorldMapButton` covers the same rect and sits above it. The button is pinned INSIDE the canvas
child, so it scales with it: measured at the world view with OpenMapClassic driving, at grom's
own 2560x1440, a 23x16 px button inside a 701x468 px window. Every press on the other 99%
reached the next frame that takes the mouse — `WorldMapFrame`, which pfUI makes a
`RegisterForDrag("LeftButton")` handle for the whole window. So dragging the map moved the
window, and a click never reached `ProcessCanvasClickHandlers`, which is the only way a map pin
is ever dropped.
The container takes the mouse and the wheel now and drives the same controller. The frame level
mattered as much: the hit sweep probes one `(strata, level)` plane in link order and a parent is
linked before its children (`0x764aa0`), so a container tied with `WorldMapFrame` still lost
every press to its own parent. It is re-seated to `map:GetFrameLevel() + 1` on every show,
because the window is level 0 while the canvas builds and level 1 once `ShowUIPanel` has raised
it.
With the click arriving, a pin still stored nothing: `Map/Pins.lua:57` trims the note with
`strtrim`, which was deliberately absent. `script/stdlib.rs` still refuses it on the 1.12
surface, for the reason written there; this build claims ClassicAPI, whose client does have it,
so it lands in `compat/classic_api/string_trim.rs` with both spellings. Eight bare call sites in
the corpus, one of them in pfUI's search box on every keystroke.
`SCALE_RATE` 10 -> 24. A 0.7 s half-life was tolerable only while a wheel notch could not
accumulate; now that they add up the glide is what you feel ("scrolls past where I stop
scrolling").
`BenillaMapTextures(uiMapID)` is `GetExploredMapTextures` without the explored filter. Reveal
mode draws unexplored overlays out of a retail FileDataID table this client resolves none of —
captured with the fog default and with reveal, the same view came back pixel-identical, 0 by
`compare -metric AE`. The overlays were never missing, only their names: `Map/RevealFromClient.lua`
in `Addon_Ports/openmap_port` rebuilds the table from this, with paths.
Also in the port, not here: the arrow is placed through OpenMap's own coordinates, because
1.12 measures `GetPlayerMapPosition` against Blizzard's world and continent art and the stitched
map is a different projection.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2087 lines
76 KiB
Lua
2087 lines
76 KiB
Lua
-- MapCanvas.lua — the Era map-canvas framework, bolted onto 1.12's own WorldMapFrame.
|
|
--
|
|
-- WHY THIS FILE EXISTS, AND WHY IT IS NOT A MIGRATION
|
|
--
|
|
-- 1.12's world map is a fixed 1002x668 frame showing one zone at a time: twelve detail tiles, a
|
|
-- hit-testing button over them, POI buttons placed in its coordinates, and two dropdowns. It has
|
|
-- no canvas, no pan, no zoom, no pins and no notion of where one map sits on another. Every map
|
|
-- addon written after 4.0 — a seamless world map, Mapster, TomTom's pin layer, Questie's map —
|
|
-- drives the *canvas* framework instead: a scaled, pannable child frame, pins acquired from pools
|
|
-- at named frame levels, and data providers notified when the map, the view or the scale changes.
|
|
-- There is no stock 1.12 file to migrate from, because 1.12 has no such thing. This is the same
|
|
-- footing as `CompatTemplates.xml`: a FrameXML-era name addons inherit and reach for, which this
|
|
-- client has to provide or the addon simply does not run.
|
|
--
|
|
-- THE ONE CONSTRAINT THAT SHAPED EVERY DECISION HERE
|
|
--
|
|
-- `WorldMapFrame` already has eight consumers on this install (Atlas-CFM, FlightMap,
|
|
-- LevelRange-Turtle, pfQuest, pfUI, pfUI-locplus, SoftSpoken, SoftSpoken_Places), and all eight
|
|
-- are 1.12 addons: they anchor their icons to `WorldMapDetailFrame` and `WorldMapButton` and read
|
|
-- the `(continent, zone)` selection. Replacing the window would break all eight. So nothing here
|
|
-- replaces anything:
|
|
--
|
|
-- * The canvas is ADDED to `WorldMapFrame`, as `ScrollContainer` and `ScrollContainer.Child`.
|
|
-- * `WorldMapDetailFrame` is REPARENTED INTO the canvas and `SetAllPoints`ed to it. At rest the
|
|
-- canvas is exactly the rect the detail frame had, at scale 1 with no pan, so the stock map is
|
|
-- pixel-identical. The payoff is that the 1.12 art and every POI anchored to it now pan and
|
|
-- zoom WITH the canvas for free — a 1.12 quest icon stays on its quest while a canvas addon
|
|
-- moves the view, which no amount of per-addon work could achieve.
|
|
-- * `SetMapID` moves the 1.12 selection too (`BenillaSelectMapID`), so the stock window and the
|
|
-- canvas can never show different places.
|
|
--
|
|
-- WHAT GIVES US CLIPPING
|
|
--
|
|
-- Era's `ScrollContainer` is a plain Frame with `SetClipsChildren(true)`, which this client has
|
|
-- no verb for. Ours is a real `ScrollFrame` with the canvas as its scroll child, because this
|
|
-- client clips a ScrollFrame's scroll child and every descendant of it to the ScrollFrame's rect
|
|
-- — in the draw pass and in the pointer hit-test, from one shared source (`script/clip.rs`), so
|
|
-- drawing and hitting agree. The pan is the scroll offset, which is what a ScrollFrame's offsets
|
|
-- already mean; the zoom is `Child:SetScale`.
|
|
--
|
|
-- DIALECT
|
|
--
|
|
-- 1.12 Lua 5.0: `table.getn` rather than `#`, `arg`/`unpack(arg)` rather than `...` as an
|
|
-- expression, `string.format` reachable as `format`. Written the way the rest of this chain is.
|
|
|
|
-- The world sheet, Era's own id for Azeroth, which `C_Map.GetFallbackWorldMapID` answers.
|
|
local WORLD_MAP_ID = C_Map.GetFallbackWorldMapID()
|
|
|
|
-- Zoom behaviour constants Era publishes and addons read off the container.
|
|
MAP_CANVAS_MOUSE_WHEEL_ZOOM_BEHAVIOR_FULL = 1
|
|
MAP_CANVAS_MOUSE_WHEEL_ZOOM_BEHAVIOR_SMOOTH = 2
|
|
|
|
-- How close to the target the smooth pan/zoom has to be before it snaps and stops. Era's own
|
|
-- values; small enough that the snap is invisible and large enough that the OnUpdate stops.
|
|
local PAN_SNAP = 0.0005
|
|
local SCALE_SNAP = 0.001
|
|
-- How fast the view approaches its target, per second, as a fraction closed: the ease closes
|
|
-- `1 - 0.5^(elapsed * RATE / 7)` of the remaining gap, so the half-life is `7 / RATE` seconds.
|
|
--
|
|
-- The scale's was 10 — a 0.7 s half-life, still visibly moving two seconds after the last wheel
|
|
-- notch. That was tolerable only while a notch could not accumulate, because the target was never
|
|
-- more than one rung away; once notches add up the glide is the thing you feel. grom, 2026-11-04:
|
|
-- *"scroll is better but feels pretty slippery, like scrolls past where I stop scrolling."* At 24
|
|
-- the half-life is 0.29 s and a multi-rung spin lands about where the wheel stopped. The pan's is
|
|
-- left alone: dragging writes the scroll directly and never eases.
|
|
local PAN_RATE = 10
|
|
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
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- PinFrameLevelsManager
|
|
--
|
|
-- Pins draw in named bands rather than at raw frame levels, so an addon can insert its own band
|
|
-- relative to Blizzard's by name and get a range of levels inside it. Era's structure exactly,
|
|
-- because the names are the contract: `InsertFrameLevelAbove("PIN_FRAME_LEVEL_OPENMAP_STITCH",
|
|
-- "PIN_FRAME_LEVEL_MAP_EXPLORATION", n)` has to mean what it means upstream or the art lands in
|
|
-- the wrong band.
|
|
--------------------------------------------------------------------------------
|
|
|
|
PinFrameLevelsManagerMixin = {}
|
|
|
|
function PinFrameLevelsManagerMixin:Initialize()
|
|
self.frameLevelRanges = {}
|
|
self.ordered = {}
|
|
self.currentFrameLevels = {}
|
|
end
|
|
|
|
-- The base level every band is offset from: above the canvas itself, so a pin never hides behind
|
|
-- the art it sits on.
|
|
local PIN_FRAME_LEVEL_BASE = 100
|
|
|
|
function PinFrameLevelsManagerMixin:Reset()
|
|
self.currentFrameLevels = {}
|
|
for i = 1, table.getn(self.ordered) do
|
|
self.currentFrameLevels[self.ordered[i]] = 0
|
|
end
|
|
end
|
|
|
|
-- Append a band with `count` levels in it; the first call for a name wins, as upstream's does.
|
|
function PinFrameLevelsManagerMixin:InsertFrameLevel(frameLevelType, count)
|
|
if self.frameLevelRanges[frameLevelType] then
|
|
return false
|
|
end
|
|
local base = PIN_FRAME_LEVEL_BASE
|
|
local n = table.getn(self.ordered)
|
|
if n > 0 then
|
|
local last = self.frameLevelRanges[self.ordered[n]]
|
|
base = last.base + last.count
|
|
end
|
|
self.frameLevelRanges[frameLevelType] = { base = base, count = count or 1 }
|
|
self.ordered[n + 1] = frameLevelType
|
|
self.currentFrameLevels[frameLevelType] = 0
|
|
return true
|
|
end
|
|
|
|
-- Insert a band immediately above a named one, shifting every band above it up. False when the
|
|
-- reference band does not exist, which is the answer a caller branches on: OpenMapClassic prints
|
|
-- "Blizzard's map layers aren't the expected ones" and carries on.
|
|
function PinFrameLevelsManagerMixin:InsertFrameLevelAbove(frameLevelType, aboveType, count)
|
|
if self.frameLevelRanges[frameLevelType] or not self.frameLevelRanges[aboveType] then
|
|
return false
|
|
end
|
|
count = count or 1
|
|
local at
|
|
for i = 1, table.getn(self.ordered) do
|
|
if self.ordered[i] == aboveType then
|
|
at = i
|
|
break
|
|
end
|
|
end
|
|
if not at then
|
|
return false
|
|
end
|
|
table.insert(self.ordered, at + 1, frameLevelType)
|
|
-- Re-base every band from the bottom, so the ranges stay contiguous and ordered.
|
|
local base = PIN_FRAME_LEVEL_BASE
|
|
for i = 1, table.getn(self.ordered) do
|
|
local name = self.ordered[i]
|
|
local range = self.frameLevelRanges[name]
|
|
if not range then
|
|
range = { count = count }
|
|
self.frameLevelRanges[name] = range
|
|
self.currentFrameLevels[name] = 0
|
|
end
|
|
range.base = base
|
|
base = base + range.count
|
|
end
|
|
return true
|
|
end
|
|
|
|
function PinFrameLevelsManagerMixin:InsertFrameLevelBelow(frameLevelType, belowType, count)
|
|
if not self.frameLevelRanges[belowType] then
|
|
return false
|
|
end
|
|
-- Below `belowType` is above whatever is under it; with nothing under it, the bottom.
|
|
local at
|
|
for i = 1, table.getn(self.ordered) do
|
|
if self.ordered[i] == belowType then
|
|
at = i
|
|
break
|
|
end
|
|
end
|
|
if not at then
|
|
return false
|
|
end
|
|
if at == 1 then
|
|
if self.frameLevelRanges[frameLevelType] then
|
|
return false
|
|
end
|
|
table.insert(self.ordered, 1, frameLevelType)
|
|
self.frameLevelRanges[frameLevelType] = { base = 0, count = count or 1 }
|
|
self.currentFrameLevels[frameLevelType] = 0
|
|
local base = PIN_FRAME_LEVEL_BASE
|
|
for i = 1, table.getn(self.ordered) do
|
|
local range = self.frameLevelRanges[self.ordered[i]]
|
|
range.base = base
|
|
base = base + range.count
|
|
end
|
|
return true
|
|
end
|
|
return self:InsertFrameLevelAbove(frameLevelType, self.ordered[at - 1], count)
|
|
end
|
|
|
|
-- The next free level inside a band, which is what a pin asks for as it is acquired. A band that
|
|
-- runs out keeps handing back its top level rather than spilling into the next band's.
|
|
function PinFrameLevelsManagerMixin:GetValidFrameLevel(frameLevelType)
|
|
local range = self.frameLevelRanges[frameLevelType]
|
|
if not range then
|
|
return PIN_FRAME_LEVEL_BASE
|
|
end
|
|
local used = self.currentFrameLevels[frameLevelType] or 0
|
|
if used < range.count then
|
|
self.currentFrameLevels[frameLevelType] = used + 1
|
|
else
|
|
used = range.count - 1
|
|
end
|
|
return range.base + used
|
|
end
|
|
|
|
function PinFrameLevelsManagerMixin:GetFrameLevelRange(frameLevelType)
|
|
local range = self.frameLevelRanges[frameLevelType]
|
|
if not range then
|
|
return nil
|
|
end
|
|
return range.base, range.base + range.count - 1
|
|
end
|
|
|
|
local function CreatePinFrameLevelsManager()
|
|
local manager = {}
|
|
for k, v in pairs(PinFrameLevelsManagerMixin) do
|
|
manager[k] = v
|
|
end
|
|
manager:Initialize()
|
|
return manager
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- MapCanvasDataProviderMixin — the base every data provider is built from.
|
|
--
|
|
-- Every member is a no-op, deliberately: upstream's is too, and that is the whole contract. A
|
|
-- provider overrides the two or three hooks it cares about and inherits silence for the rest, so
|
|
-- adding a hook here can never break an existing provider.
|
|
--------------------------------------------------------------------------------
|
|
|
|
MapCanvasDataProviderMixin = {}
|
|
|
|
function MapCanvasDataProviderMixin:OnAdded(mapCanvas)
|
|
self.owningMap = mapCanvas
|
|
end
|
|
|
|
function MapCanvasDataProviderMixin:OnRemoved(mapCanvas)
|
|
self.owningMap = nil
|
|
end
|
|
|
|
function MapCanvasDataProviderMixin:GetMap()
|
|
return self.owningMap
|
|
end
|
|
|
|
function MapCanvasDataProviderMixin:RemoveAllData() end
|
|
function MapCanvasDataProviderMixin:RefreshAllData(fromOnShow) end
|
|
function MapCanvasDataProviderMixin:OnShow() end
|
|
function MapCanvasDataProviderMixin:OnHide() end
|
|
function MapCanvasDataProviderMixin:OnEvent(event, arg1, arg2, arg3) end
|
|
function MapCanvasDataProviderMixin:OnMapChanged() end
|
|
function MapCanvasDataProviderMixin:OnMapInsetSizeChanged(insetIndex, expanded) end
|
|
function MapCanvasDataProviderMixin:OnCanvasScaleChanged() end
|
|
function MapCanvasDataProviderMixin:OnCanvasPanChanged() end
|
|
function MapCanvasDataProviderMixin:OnCanvasSizeChanged() end
|
|
function MapCanvasDataProviderMixin:OnGlobalAlphaChanged() end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- MapCanvasPinMixin — the base every pin is built from.
|
|
--
|
|
-- A pin is a frame on the canvas placed by a normalized position rather than by anchors, so it
|
|
-- stays on its place as the canvas pans and scales. `ApplyCurrentPosition` is the one that does
|
|
-- the work and is called on every pan and scale change.
|
|
--------------------------------------------------------------------------------
|
|
|
|
MapCanvasPinMixin = {}
|
|
|
|
function MapCanvasPinMixin:OnLoad()
|
|
self.numActiveAnimations = 0
|
|
self.scaleFactor = 1
|
|
self.startScale = 1
|
|
self.endScale = 1
|
|
self.normalizedX = 0.5
|
|
self.normalizedY = 0.5
|
|
end
|
|
|
|
function MapCanvasPinMixin:GetMap()
|
|
return self.owningMap
|
|
end
|
|
|
|
function MapCanvasPinMixin:OnAcquired() end
|
|
function MapCanvasPinMixin:OnReleased() end
|
|
function MapCanvasPinMixin:OnClick(button) end
|
|
function MapCanvasPinMixin:OnMouseEnter() end
|
|
function MapCanvasPinMixin:OnMouseLeave() end
|
|
function MapCanvasPinMixin:OnMapInsetSizeChanged(insetIndex, expanded) end
|
|
function MapCanvasPinMixin:OnCanvasScaleChanged()
|
|
self:ApplyCurrentScale()
|
|
end
|
|
function MapCanvasPinMixin:OnCanvasPanChanged() end
|
|
function MapCanvasPinMixin:OnCanvasSizeChanged() end
|
|
|
|
function MapCanvasPinMixin:SetPosition(normalizedX, normalizedY, insetIndex)
|
|
self.normalizedX = normalizedX
|
|
self.normalizedY = normalizedY
|
|
self.insetIndex = insetIndex
|
|
self:ApplyCurrentPosition()
|
|
end
|
|
|
|
function MapCanvasPinMixin:GetPosition()
|
|
return self.normalizedX, self.normalizedY
|
|
end
|
|
|
|
function MapCanvasPinMixin:ApplyCurrentPosition()
|
|
local map = self:GetMap()
|
|
if not map then
|
|
return
|
|
end
|
|
local canvas = map:GetCanvas()
|
|
if not canvas then
|
|
return
|
|
end
|
|
self:ClearAllPoints()
|
|
self:SetPoint("CENTER", canvas, "TOPLEFT",
|
|
(self.normalizedX or 0.5) * canvas:GetWidth(),
|
|
-(self.normalizedY or 0.5) * canvas:GetHeight())
|
|
end
|
|
|
|
-- A pin's own scale, held constant on screen as the canvas scales: a quest icon should stay the
|
|
-- same size however far the map is zoomed out, which is `1 / canvasScale` times whatever the pin
|
|
-- asked for. `scaleFactor` 0 opts out and lets the pin scale with the map, which is what an area
|
|
-- outline (a "blob") wants.
|
|
function MapCanvasPinMixin:SetScalingLimits(scaleFactor, startScale, endScale)
|
|
self.scaleFactor = scaleFactor
|
|
self.startScale = startScale
|
|
self.endScale = endScale
|
|
end
|
|
|
|
function MapCanvasPinMixin:ApplyCurrentScale()
|
|
local map = self:GetMap()
|
|
if not map then
|
|
return
|
|
end
|
|
local factor = self.scaleFactor
|
|
if not factor or factor == 0 then
|
|
self:SetScale(1)
|
|
return
|
|
end
|
|
local canvasScale = map:GetCanvasScale()
|
|
if not canvasScale or canvasScale <= 0 then
|
|
return
|
|
end
|
|
local scale = factor / canvasScale
|
|
local lo = self.startScale
|
|
local hi = self.endScale
|
|
if lo and hi and lo > hi then
|
|
lo, hi = hi, lo
|
|
end
|
|
if lo and scale < lo then
|
|
scale = lo
|
|
end
|
|
if hi and scale > hi then
|
|
scale = hi
|
|
end
|
|
self:SetScale(scale)
|
|
end
|
|
|
|
function MapCanvasPinMixin:UseFrameLevelType(pinFrameLevelType, index)
|
|
self.pinFrameLevelType = pinFrameLevelType
|
|
self.pinFrameLevelIndex = index
|
|
end
|
|
|
|
function MapCanvasPinMixin:GetFrameLevelType()
|
|
return self.pinFrameLevelType
|
|
end
|
|
|
|
function MapCanvasPinMixin:ApplyFrameLevel()
|
|
local map = self:GetMap()
|
|
if not map or not self.pinFrameLevelType then
|
|
return
|
|
end
|
|
local manager = map:GetPinFrameLevelsManager()
|
|
if manager then
|
|
self:SetFrameLevel(manager:GetValidFrameLevel(self.pinFrameLevelType))
|
|
end
|
|
end
|
|
|
|
-- Which mouse buttons fall through to the canvas underneath. Era's pins declare this so a
|
|
-- right-drag over an icon still pans the map; a pin with no declaration swallows everything,
|
|
-- which is what a clickable icon wants.
|
|
function MapCanvasPinMixin:SetPassThroughButtons(button1, button2, button3)
|
|
self.passThroughButtons = { button1, button2, button3 }
|
|
end
|
|
|
|
function MapCanvasPinMixin:CheckMouseButtonPassthrough(button)
|
|
local list = self.passThroughButtons
|
|
if not list then
|
|
return false
|
|
end
|
|
for i = 1, table.getn(list) do
|
|
if list[i] == button then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- MapCanvasScrollControllerMixin — the pan and zoom.
|
|
--
|
|
-- The view is held as a NORMALIZED CENTRE (`currentScrollX`, `currentScrollY`: the canvas point
|
|
-- under the middle of the container, 0..1 across the canvas) plus a scale. That is upstream's own
|
|
-- representation, and it has to be, because a canvas addon reads and writes those fields directly
|
|
-- — OpenMapClassic's `SetView` assigns `currentScrollX`, `targetScrollX`, `currentScale`,
|
|
-- `targetScale` and `zoomTarget` by hand (`Map/WorldMap.lua:184-191`) and then calls
|
|
-- `SetNormalizedHorizontalScroll`. So the field names and their meanings are the contract, not an
|
|
-- implementation detail.
|
|
--
|
|
-- The pan is applied as the ScrollFrame's own scroll offset, which is what a ScrollFrame offset
|
|
-- already means and is the cheapest write this client has (`SetVerticalScroll` re-anchors one
|
|
-- node and skips the layout touch).
|
|
--------------------------------------------------------------------------------
|
|
|
|
MapCanvasScrollControllerMixin = {}
|
|
|
|
function MapCanvasScrollControllerMixin:OnLoad()
|
|
self.currentScrollX = 0.5
|
|
self.currentScrollY = 0.5
|
|
self.targetScrollX = 0.5
|
|
self.targetScrollY = 0.5
|
|
self.currentScale = 1
|
|
self.targetScale = 1
|
|
self.zoomTarget = 1
|
|
self.baseScale = 1
|
|
self.zoomLevels = {}
|
|
self.scrollXExtentsMin = 0.5
|
|
self.scrollXExtentsMax = 0.5
|
|
self.scrollYExtentsMin = 0.5
|
|
self.scrollYExtentsMax = 0.5
|
|
self.mouseButtonInfo = {
|
|
LeftButton = { down = false, x = 0, y = 0 },
|
|
RightButton = { down = false, x = 0, y = 0 },
|
|
MiddleButton = { down = false, x = 0, y = 0 },
|
|
}
|
|
self.scalingMode = "SCALING_MODE_TRANSLATE_FASTER_THAN_SCALE"
|
|
self.mouseWheelZoomMode = MAP_CANVAS_MOUSE_WHEEL_ZOOM_BEHAVIOR_FULL
|
|
self.panning = false
|
|
self.viewRectDirty = true
|
|
self.areaTriggersDirty = true
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetCanvasContainer()
|
|
return self
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetCanvas()
|
|
return self.Child
|
|
end
|
|
|
|
-- The ladder's two ends, or nil before there is a ladder.
|
|
function MapCanvasScrollControllerMixin:GetZoomBounds()
|
|
local levels = self.zoomLevels
|
|
local n = levels and table.getn(levels) or 0
|
|
if n == 0 then
|
|
return nil
|
|
end
|
|
return levels[1].scale, levels[n].scale
|
|
end
|
|
|
|
-- Pull every scale the controller holds onto the ladder it currently has.
|
|
--
|
|
-- `CreateZoomLevels` is the documented extension point and a canvas addon replaces it wholesale,
|
|
-- so the ladder can change out from under a scale that was already set -- and `OnLoad` sets one,
|
|
-- 1, before any addon has run. OpenMapClassic's ladder is 27 steps from 0.0125 to 0.899, its
|
|
-- whole range *below* this file's own 1, because its stitched world is reached by zooming out
|
|
-- past a single zone; measured with it loaded, the canvas sat at 1.0, above its own ceiling, and
|
|
-- `ZoomIn`/`ZoomOut` walk the ladder **by value** -- they look for the first step above or below
|
|
-- the current scale -- so a scale outside it cannot be scrolled back onto at either end.
|
|
--
|
|
-- Here rather than in `CreateZoomLevels`, because the addon's replacement is the one that will
|
|
-- not have it.
|
|
function MapCanvasScrollControllerMixin:ClampZoomToLevels()
|
|
local floor, ceil = self:GetZoomBounds()
|
|
if not floor then
|
|
return
|
|
end
|
|
local function clamp(v)
|
|
if not v then
|
|
return v
|
|
elseif v < floor then
|
|
return floor
|
|
elseif v > ceil then
|
|
return ceil
|
|
end
|
|
return v
|
|
end
|
|
self.currentScale = clamp(self.currentScale)
|
|
self.targetScale = clamp(self.targetScale)
|
|
self.zoomTarget = clamp(self.zoomTarget)
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetCanvasScale()
|
|
self:ClampZoomToLevels()
|
|
return self.currentScale or 1
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:SetScalingMode(mode)
|
|
self.scalingMode = mode
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:SetMouseWheelZoomMode(mode)
|
|
self.mouseWheelZoomMode = mode
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:MarkViewRectDirty()
|
|
self.viewRectDirty = true
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:MarkAreaTriggersDirty()
|
|
self.areaTriggersDirty = true
|
|
end
|
|
|
|
-- Canvas px to a fraction of the canvas, the two helpers every extents computation goes through.
|
|
function MapCanvasScrollControllerMixin:NormalizeHorizontalSize(size)
|
|
local w = self.Child and self.Child:GetWidth()
|
|
if not w or w <= 0 then
|
|
return 0
|
|
end
|
|
return size / w
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:NormalizeVerticalSize(size)
|
|
local h = self.Child and self.Child:GetHeight()
|
|
if not h or h <= 0 then
|
|
return 0
|
|
end
|
|
return size / h
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:DenormalizeHorizontalSize(size)
|
|
local w = self.Child and self.Child:GetWidth()
|
|
return size * (w or 0)
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:DenormalizeVerticalSize(size)
|
|
local h = self.Child and self.Child:GetHeight()
|
|
return size * (h or 0)
|
|
end
|
|
|
|
-- The normalized centre may range over this, so the view stays inside the canvas. When the canvas
|
|
-- is smaller than the container on an axis the range collapses to its middle, which centres it.
|
|
function MapCanvasScrollControllerMixin:CalculateScrollExtentsAtScale(scale)
|
|
local halfW = self:NormalizeHorizontalSize(self:GetWidth() * 0.5 / scale)
|
|
local halfH = self:NormalizeVerticalSize(self:GetHeight() * 0.5 / scale)
|
|
local minX, maxX = halfW, 1 - halfW
|
|
local minY, maxY = halfH, 1 - halfH
|
|
if minX > maxX then
|
|
minX, maxX = 0.5, 0.5
|
|
end
|
|
if minY > maxY then
|
|
minY, maxY = 0.5, 0.5
|
|
end
|
|
return minX, maxX, minY, maxY
|
|
end
|
|
|
|
-- Through `CalculateScrollExtentsAtScale`, never duplicating its arithmetic: a canvas addon
|
|
-- replaces that one method to widen the pan (OpenMapClassic pans the whole world, not just the
|
|
-- map in view), and a second copy of the law here would ignore the replacement.
|
|
function MapCanvasScrollControllerMixin:CalculateScrollExtents()
|
|
self.scrollXExtentsMin, self.scrollXExtentsMax, self.scrollYExtentsMin, self.scrollYExtentsMax =
|
|
self:CalculateScrollExtentsAtScale(self:GetCanvasScale())
|
|
end
|
|
|
|
local function clamp(v, lo, hi)
|
|
if lo and v < lo then
|
|
return lo
|
|
end
|
|
if hi and v > hi then
|
|
return hi
|
|
end
|
|
return v
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:ClampScrollToExtents(x, y)
|
|
return clamp(x, self.scrollXExtentsMin, self.scrollXExtentsMax),
|
|
clamp(y, self.scrollYExtentsMin, self.scrollYExtentsMax)
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:SetNormalizedHorizontalScroll(x)
|
|
self.currentScrollX = x
|
|
self:ApplyCurrentScroll()
|
|
self:MarkViewRectDirty()
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:SetNormalizedVerticalScroll(y)
|
|
self.currentScrollY = y
|
|
self:ApplyCurrentScroll()
|
|
self:MarkViewRectDirty()
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetCurrentScrollX()
|
|
return self.currentScrollX
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetCurrentScrollY()
|
|
return self.currentScrollY
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetNormalizedHorizontalScroll()
|
|
return self.currentScrollX
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetNormalizedVerticalScroll()
|
|
return self.currentScrollY
|
|
end
|
|
|
|
-- The whole view at once, no easing: the canvas scaled, the extents recomputed at that scale, and
|
|
-- the centre clamped into them.
|
|
function MapCanvasScrollControllerMixin:InstantPanAndZoom(scale, x, y)
|
|
self.currentScale = scale
|
|
self.targetScale = scale
|
|
self.zoomTarget = scale
|
|
if self.Child then
|
|
self.Child:SetScale(scale)
|
|
end
|
|
self:CalculateScrollExtents()
|
|
x, y = self:ClampScrollToExtents(x, y)
|
|
self.currentScrollX, self.targetScrollX = x, x
|
|
self.currentScrollY, self.targetScrollY = y, y
|
|
self:ApplyCurrentScroll()
|
|
self:MarkViewRectDirty()
|
|
self:MarkAreaTriggersDirty()
|
|
local map = self:GetMapCanvas()
|
|
if map then
|
|
map:OnCanvasScaleChanged()
|
|
map:OnCanvasPanChanged()
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetMapCanvas()
|
|
return self.mapCanvas
|
|
end
|
|
|
|
-- The zoom steps, derived from the map's own art layers: one step per layer, at that layer's
|
|
-- minimum scale, plus a fitted step so the whole map is reachable. A canvas addon replaces this
|
|
-- wholesale (OpenMapClassic builds a geometric ladder from its own zoom option), which is why it
|
|
-- is one method and why `ResetZoom` reads `zoomLevels` rather than recomputing.
|
|
function MapCanvasScrollControllerMixin:CreateZoomLevels()
|
|
local mapID = self.mapID
|
|
local levels = {}
|
|
if mapID then
|
|
local layers = C_Map.GetMapArtLayers(mapID)
|
|
local layer = layers and layers[1]
|
|
if layer then
|
|
-- A geometric ladder from the layer's own bounds, floored at the scale that fits the
|
|
-- whole map in the container. That used to be the constant 1 and the comment here
|
|
-- said why: the canvas is sized to the layer and the container *is* the rect that
|
|
-- layer filled, so 1 was the whole map by construction and a `minScale` below 1 would
|
|
-- open the map zoomed out of its own window. The container is the window's content
|
|
-- area now (`SeatContainer`), which is 1002x668 only when the window is 1.12's own
|
|
-- size, so the fit has to be measured. It still comes out exactly 1 when it is.
|
|
local fit = self:GetFitScale()
|
|
local lo = layer.minScale or 1
|
|
local hi = layer.maxScale or 1
|
|
if lo > fit then
|
|
lo = fit
|
|
end
|
|
if hi < lo then
|
|
hi = lo
|
|
end
|
|
local steps = (layer.additionalZoomSteps or 0) + 1
|
|
if steps < 1 then
|
|
steps = 1
|
|
end
|
|
local ratio = 1
|
|
if steps > 1 and lo > 0 then
|
|
ratio = math.pow(hi / lo, 1 / (steps - 1))
|
|
end
|
|
local scale = lo
|
|
for i = 1, steps do
|
|
levels[i] = { scale = scale, layerIndex = 1 }
|
|
scale = scale * ratio
|
|
end
|
|
end
|
|
end
|
|
if table.getn(levels) == 0 then
|
|
-- No art layer to build a ladder from: one step, at the scale that fits the canvas in
|
|
-- the window. 1 while the two are the same size, which is 1.12's own map.
|
|
levels[1] = { scale = self:GetFitScale(), layerIndex = 1 }
|
|
end
|
|
self.zoomLevels = levels
|
|
self.baseScale = levels[1].scale
|
|
-- A window that grew raises the floor, so a view below it is pulled back on; see
|
|
-- `ClampZoomToLevels`, which is where the rule lives because a canvas addon's own
|
|
-- `CreateZoomLevels` will not have it.
|
|
self:ClampZoomToLevels()
|
|
-- The scale itself is only seeded when there is none: `CreateZoomLevels` is called again on
|
|
-- every map change and on every container resize, and a view the player has zoomed must not
|
|
-- snap back because the window was dragged.
|
|
if not self.currentScale then
|
|
self.currentScale = levels[1].scale
|
|
self.targetScale = levels[1].scale
|
|
self.zoomTarget = levels[1].scale
|
|
end
|
|
end
|
|
|
|
-- The scale at which the whole canvas child fits inside the container, which is the zoom ladder's
|
|
-- floor. 1 whenever the container is the art's own size, which is 1.12's own map.
|
|
function MapCanvasScrollControllerMixin:GetFitScale()
|
|
local child = self.Child
|
|
if not child then
|
|
return 1
|
|
end
|
|
self:GetLeft()
|
|
local cw, ch = child:GetWidth(), child:GetHeight()
|
|
local w, h = self:GetWidth(), self:GetHeight()
|
|
if not cw or cw <= 0 or not ch or ch <= 0 or not w or w <= 0 or not h or h <= 0 then
|
|
return 1
|
|
end
|
|
local fit = math.min(w / cw, h / ch)
|
|
if fit <= 0 then
|
|
return 1
|
|
end
|
|
return fit
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetScaleForMaxZoom()
|
|
local n = table.getn(self.zoomLevels)
|
|
if n == 0 then
|
|
return 1
|
|
end
|
|
return self.zoomLevels[n].scale
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetScaleForMinZoom()
|
|
if table.getn(self.zoomLevels) == 0 then
|
|
return 1
|
|
end
|
|
return self.zoomLevels[1].scale
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsAtMaxZoom()
|
|
return self:GetCanvasScale() >= self:GetScaleForMaxZoom()
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsAtMinZoom()
|
|
return self:GetCanvasScale() <= self:GetScaleForMinZoom()
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsZoomingIn()
|
|
return (self.targetScale or 1) > (self.currentScale or 1)
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsZoomingOut()
|
|
return (self.targetScale or 1) < (self.currentScale or 1)
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsZooming()
|
|
return self:IsZoomingIn() or self:IsZoomingOut()
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:IsGamepadZooming()
|
|
return self.gamepadZoom and self.gamepadZoom ~= 0
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:HasGamepadScrollSpeed()
|
|
return self.gamepadZoomSpeed ~= nil
|
|
end
|
|
|
|
-- The next step up or down the ladder, toward a point if one is given.
|
|
function MapCanvasScrollControllerMixin:SetZoomTarget(scale, x, y)
|
|
self.zoomTarget = scale
|
|
self.targetScale = scale
|
|
if x and y then
|
|
self.targetScrollX, self.targetScrollY = x, y
|
|
end
|
|
end
|
|
|
|
-- Which rung a wheel notch steps FROM: where the view is heading, not where the ease has got
|
|
-- to. `currentScale` closes about 63% of the gap a second (`SCALE_RATE`), so a second notch
|
|
-- arriving a frame after the first re-reads a scale that has barely moved, finds the same rung
|
|
-- under it, and re-sets the target it already has. Measured at the player's own window size
|
|
-- with his addons loaded: four wheel-down notches over the map moved the ladder ONE step
|
|
-- (1.0000 -> 0.9030) and two more moved it none. Stepping from `targetScale` makes notches
|
|
-- accumulate, which is both how it feels in Era and the only way either end of a 27-step
|
|
-- ladder is reachable at all -- OpenMapClassic's runs 0.033 to 2.373, and its bottom rung is
|
|
-- the whole stitched world.
|
|
function MapCanvasScrollControllerMixin:GetZoomStepScale()
|
|
-- Through `GetCanvasScale`, because it clamps every held scale onto the current ladder as a
|
|
-- side effect, and a canvas addon can have replaced the ladder since the last notch.
|
|
local live = self:GetCanvasScale()
|
|
return self.targetScale or live
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:ZoomIn()
|
|
local n = table.getn(self.zoomLevels)
|
|
local current = self:GetZoomStepScale()
|
|
for i = 1, n do
|
|
if self.zoomLevels[i].scale > current + 0.0001 then
|
|
self:SetZoomTarget(self.zoomLevels[i].scale)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:ZoomOut()
|
|
local current = self:GetZoomStepScale()
|
|
for i = table.getn(self.zoomLevels), 1, -1 do
|
|
if self.zoomLevels[i].scale < current - 0.0001 then
|
|
self:SetZoomTarget(self.zoomLevels[i].scale)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:ResetZoom()
|
|
local scale = self:GetScaleForMinZoom()
|
|
self:InstantPanAndZoom(scale, 0.5, 0.5)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- 1002x668 is a contract, not a size
|
|
--
|
|
-- 1.12's `WorldMapDetailFrame` is 1002x668 and every single thing drawn into it is placed in
|
|
-- absolute pixels against that number: the twelve 256x256 tiles, each exploration overlay at the
|
|
-- offset `WorldMapOverlay.dbc` gives it, the hover highlight -- and, far more importantly,
|
|
-- **every addon's map art**, because that frame is where a 1.12 map addon puts its own layers.
|
|
-- pfUI's reveal is the one that reached grom (`pfUI/modules/mapreveal.lua:157`): one
|
|
-- `SetPoint("TOPLEFT", "WorldMapDetailFrame", "TOPLEFT", tile.offsetX, -tile.offsetY)` per tile,
|
|
-- raw, like everyone else's.
|
|
--
|
|
-- So the frame's **own size must stay 1002x668**. Any rescaling this file does to compensate for
|
|
-- a different size can only ever reach the children it knows the names of, and the children it
|
|
-- does not know the names of are the whole point of the exercise.
|
|
--
|
|
-- It did not stay 1002x668, and the reason is a second layout authority. 1.12's windowed map
|
|
-- shrinks its art by putting `WORLDMAP_WINDOWED_SCALE` (0.7) on `WorldMapDetailFrame` *and*
|
|
-- `WorldMapButton` (`WorldMapFrame.lua:205-208`), because there the window is a fixed sheet and
|
|
-- scaling the two frames is the only way to make the art smaller inside it. Under the canvas the
|
|
-- container already carries the window's size, so that 0.7 is counted twice -- and since the
|
|
-- frame is `SetAllPoints` to the canvas, the half that is not wanted comes back as **unit size**:
|
|
-- 1002 / 0.7 = 1431. Measured in grom's own session log, 1431x954 at scale 0.70 inside a 1002x668
|
|
-- canvas at 0.70, the same screen rect described in two different numbers of units.
|
|
--
|
|
-- Everything absolute inside it then lands at 0.7 of where it belongs, measured off his
|
|
-- screenshot before any code was read: pfUI draws "Coldridge Valley" at (360,525) while the base
|
|
-- art draws it at (460,700), and both axes solve to the same origin (127,117) -- the frame's own
|
|
-- top-left -- at k = 1002/1431 = 0.700.
|
|
--
|
|
-- `WorldMapButton` is the same bug wearing a different coat. It keeps its XML 1002x668 and is
|
|
-- anchored to the frame's TOPLEFT, so with 0.7 on it and 1431 units of art beside it, **the map's
|
|
-- mouse surface covered 70% of the map**: click-to-zoom, the hover highlight and every projection
|
|
-- through `WorldMapButton_OnClick`'s own width were all reading a rect the art had outgrown.
|
|
--
|
|
-- `SeatOnCanvas` is the fix and it is one idea: the canvas is the only layout authority here, so
|
|
-- both frames go to scale 1, the frame fills the canvas and the button covers the frame. The
|
|
-- screen rect does not move (1431 x 0.49 and 1002 x 0.70 are the same pixels), so nothing about
|
|
-- the picture changes except that the units inside it are 1.12's again.
|
|
--
|
|
-- `ScaleDetailArt` stays for the one case that cannot be fixed this way: a map with no art layer,
|
|
-- where the canvas falls back to the container's rect and the frame genuinely is not 1002x668. It
|
|
-- is a no-op whenever the contract holds, which is every map that has art.
|
|
--------------------------------------------------------------------------------
|
|
|
|
local DETAIL_BASE_W, DETAIL_BASE_H = 1002, 668
|
|
local DETAIL_TILE, DETAIL_TILES = 256, 12
|
|
|
|
-- Base geometry per overlay texture, as `WorldMapFrame_Update` wrote it: [texture] = {x,y,w,h}.
|
|
local overlayBase = {}
|
|
|
|
-- Scale the stock map art to whatever size the detail frame currently has. `w`,`h` are a
|
|
-- fallback only, for the one caller that has just set a size the layout cannot answer yet.
|
|
local function ScaleDetailArt(w, h)
|
|
local d = WorldMapDetailFrame
|
|
if not d then
|
|
return
|
|
end
|
|
-- The detail frame's OWN size is the authority, because the tiles are its children. With
|
|
-- `SeatOnCanvas` holding the contract that size reads 1002x668 and the whole function is a
|
|
-- no-op; it earns its keep only on a map with no art layer, where the canvas takes the
|
|
-- container's rect and the frame really is some other size.
|
|
--
|
|
-- `GetLeft` first: the size is derived, and without forcing the layout to settle a read taken
|
|
-- straight after a resize answers the size from before it. The caller's `w`,`h` remain the
|
|
-- fallback for the one case that cannot be measured at all.
|
|
d:GetLeft()
|
|
local dw, dh = d:GetWidth(), d:GetHeight()
|
|
if dw and dh and dw > 0 and dh > 0 then
|
|
w, h = dw, dh
|
|
end
|
|
if not w or not h or w <= 0 or h <= 0 then
|
|
return
|
|
end
|
|
local sx, sy = w / DETAIL_BASE_W, h / DETAIL_BASE_H
|
|
for i = 1, DETAIL_TILES do
|
|
local tile = _G["WorldMapDetailTile" .. i]
|
|
if tile then
|
|
tile:SetWidth(DETAIL_TILE * sx)
|
|
tile:SetHeight(DETAIL_TILE * sy)
|
|
end
|
|
end
|
|
for tex, base in pairs(overlayBase) do
|
|
tex:SetWidth(base.w * sx)
|
|
tex:SetHeight(base.h * sy)
|
|
tex:ClearAllPoints()
|
|
tex:SetPoint("TOPLEFT", WorldMapDetailFrame, "TOPLEFT", base.x * sx, base.y * sy)
|
|
end
|
|
end
|
|
|
|
-- After the stock repaint: every overlay it just placed is at its 1002x668 offset and size, which
|
|
-- is the only moment those base values are readable. Captured, then scaled.
|
|
local function CaptureOverlays()
|
|
for tex in pairs(overlayBase) do
|
|
overlayBase[tex] = nil
|
|
end
|
|
local n = NUM_WORLDMAP_OVERLAYS or 0
|
|
for i = 1, n do
|
|
local tex = _G["WorldMapOverlay" .. i]
|
|
if tex and tex:IsShown() then
|
|
local _, _, _, x, y = tex:GetPoint()
|
|
if x then
|
|
overlayBase[tex] = { x = x, y = y, w = tex:GetWidth(), h = tex:GetHeight() }
|
|
end
|
|
end
|
|
end
|
|
ScaleDetailArt()
|
|
end
|
|
|
|
-- The window's content area, the rect 1.12 gives its map art, in both of the two layouts the
|
|
-- stock map has. Taken from `WorldMapFrame_Minimize`/`_Maximize` themselves, as insets rather
|
|
-- than as the one size each of them happens to produce:
|
|
--
|
|
-- windowed `WorldMapDetailFrame:SetPoint("TOPLEFT", 15, -33)` at scale 0.7 inside a 720x521
|
|
-- window — 701x468 of art, so 15 left, 33 top, 4 right, 20 bottom.
|
|
-- maximized `TOPLEFT WorldMapPositioningGuide TOP -502,-69` at scale 1 — the guide is a fixed
|
|
-- **1024x768** frame centred on the window, so 10 left, 69 top, 12 right, 31 bottom,
|
|
-- and a maximized map that fills the screen still draws its sheet at 1024x768 with
|
|
-- `BlackoutWorld` around it, which is 1.12's own full-screen map.
|
|
--
|
|
-- Read as insets, the same two numbers reproduce 1.12 exactly at 1.12's sizes and do the obvious
|
|
-- thing at any other: pfUI resizes the window to `WorldMapButton`'s size plus its own chrome
|
|
-- (`pfUI/modules/map.lua:111`), and the art then fills that instead of sitting at 70% in the
|
|
-- corner of it. Before this the container kept the maximized anchor whatever the mode, so the
|
|
-- maximize button left the art a 1002x668 patch in a full-screen window and the minimize button
|
|
-- could not put it back.
|
|
local WINDOWED_INSET = { l = 15, t = 33, r = 4, b = 20 }
|
|
local MAXIMIZED_INSET = { l = 10, t = 69, r = 12, b = 31 }
|
|
|
|
-- `(host, inset)` for the current mode: the frame the content area is measured in, and how far
|
|
-- inside it the art sits.
|
|
local function ContentHost()
|
|
if WORLDMAP_WINDOWED == 1 then
|
|
return WorldMapFrame, WINDOWED_INSET
|
|
end
|
|
return WorldMapPositioningGuide or WorldMapFrame, MAXIMIZED_INSET
|
|
end
|
|
|
|
-- Put the container on that content area. The size is written explicitly rather than left to a
|
|
-- two-corner anchor, because this client fires `OnSizeChanged` only for an explicit
|
|
-- `SetWidth`/`SetHeight` — and that event is what rebuilds the zoom ladder.
|
|
local function SeatContainer()
|
|
local sc = WorldMapFrame and WorldMapFrame.ScrollContainer
|
|
local host, inset = ContentHost()
|
|
if not sc or not host then
|
|
return
|
|
end
|
|
host:GetLeft()
|
|
local hw, hh = host:GetWidth(), host:GetHeight()
|
|
if not hw or hw <= 0 or not hh or hh <= 0 then
|
|
return
|
|
end
|
|
local w = hw - inset.l - inset.r
|
|
local h = hh - inset.t - inset.b
|
|
if w <= 0 or h <= 0 then
|
|
return
|
|
end
|
|
sc:ClearAllPoints()
|
|
sc:SetPoint("TOPLEFT", host, "TOPLEFT", inset.l, -inset.t)
|
|
if math.abs((sc:GetWidth() or 0) - w) > 0.5 or math.abs((sc:GetHeight() or 0) - h) > 0.5 then
|
|
sc:SetWidth(w)
|
|
sc:SetHeight(h)
|
|
-- Driven rather than waited for: a resize is what rebuilds the zoom ladder and the scroll
|
|
-- extents, and this runs from inside `SeatOnCanvas`, which `OnCanvasSizeChanged` itself
|
|
-- calls -- so leaving it to the `OnSizeChanged` script would land the rebuild after the
|
|
-- ladder had already been built from the old rect. The size test above is the recursion
|
|
-- guard: the re-entry finds the container already the right size and sets nothing.
|
|
sc:OnCanvasSizeChanged()
|
|
end
|
|
end
|
|
|
|
-- The two frames the canvas took over, re-seated on it: both at scale 1, the art filling the
|
|
-- canvas and the mouse surface covering the art. This is what keeps `WorldMapDetailFrame` at
|
|
-- 1002x668 — see "1002x668 is a contract, not a size".
|
|
--
|
|
-- Called again after `WorldMapFrame_Minimize`/`WorldMapFrame_Maximize`, the only two writers of
|
|
-- that scale and the only two that re-anchor the frame (`SetPoint("TOPLEFT", 15, -33)` and the
|
|
-- positioning guide). Both are 1.12 telling a fixed-size window how small to draw its art; here
|
|
-- the container already answers that, so what they write is re-stated rather than argued with.
|
|
-- `w`,`h` are the canvas size a caller has just set and the layout cannot answer yet, as in
|
|
-- `ScaleDetailArt`.
|
|
local function SeatOnCanvas(w, h)
|
|
local canvas = WorldMapFrame and WorldMapFrame.Canvas
|
|
local detail, button = WorldMapDetailFrame, WorldMapButton
|
|
if not canvas or not detail then
|
|
return
|
|
end
|
|
SeatContainer()
|
|
detail:SetScale(1)
|
|
detail:ClearAllPoints()
|
|
detail:SetAllPoints(canvas)
|
|
if button then
|
|
-- 1.12's own anchor, with the art's size written on it. The button is the map's mouse
|
|
-- surface and `WorldMapButton_OnClick` divides the cursor's offset by the button's own
|
|
-- width, so it has to be the art's rect exactly -- but an explicit size rather than
|
|
-- `SetAllPoints`, because `WorldMapButton:GetSize()` is **read by addons**: pfUI sizes
|
|
-- the whole map window from it (`pfUI/modules/map.lua:111`, `wmWidth + 15`), at
|
|
-- `PLAYER_ENTERING_WORLD`, and a derived size answers 0 until the layout settles.
|
|
detail:GetLeft()
|
|
local bw, bh = detail:GetWidth(), detail:GetHeight()
|
|
if not bw or bw <= 0 or not bh or bh <= 0 then
|
|
bw, bh = w or DETAIL_BASE_W, h or DETAIL_BASE_H
|
|
end
|
|
button:SetScale(1)
|
|
button:ClearAllPoints()
|
|
button:SetPoint("TOPLEFT", detail, "TOPLEFT", 0, 0)
|
|
button:SetWidth(bw)
|
|
button:SetHeight(bh)
|
|
end
|
|
ScaleDetailArt(w, h)
|
|
end
|
|
|
|
-- Called when the container or the canvas changes size, and on the first show: the canvas is
|
|
-- sized from the map's own art layer, the zoom ladder rebuilt and the view reset.
|
|
function MapCanvasScrollControllerMixin:OnCanvasSizeChanged()
|
|
local child = self.Child
|
|
if not child then
|
|
return
|
|
end
|
|
local mapID = self.mapID
|
|
local w, h
|
|
if mapID then
|
|
local layers = C_Map.GetMapArtLayers(mapID)
|
|
local layer = layers and layers[1]
|
|
if layer then
|
|
w, h = layer.layerWidth, layer.layerHeight
|
|
end
|
|
end
|
|
-- With no art layer (an instance map, a map the realm has no folder for) the canvas is still
|
|
-- 1.12's own art rect, which is what `WorldMapDetailFrame` and everything pinned into it are
|
|
-- placed in. It used to take the container's size instead, and that was only ever safe while
|
|
-- the container *was* 1002x668: now that the container is the window's content area
|
|
-- (`SeatContainer`), the fallback would hand the whole 1002x668 contract over to whatever
|
|
-- size the player's map window happens to be. The view still fits, because the zoom ladder is
|
|
-- floored at `GetFitScale` rather than at 1.
|
|
if not w or w <= 0 or not h or h <= 0 then
|
|
w, h = DETAIL_BASE_W, DETAIL_BASE_H
|
|
end
|
|
child:SetWidth(w)
|
|
child:SetHeight(h)
|
|
-- The art, its mouse surface and the canvas have to agree on one rect, and this is the only
|
|
-- place the rect changes (see "1002x668 is a contract, not a size"). The size just set is
|
|
-- passed in, because the detail frame will not answer it until the layout pass.
|
|
SeatOnCanvas(w, h)
|
|
self:CreateZoomLevels()
|
|
self:CalculateScrollExtents()
|
|
self.currentScrollX, self.currentScrollY = self:ClampScrollToExtents(
|
|
self.currentScrollX or 0.5, self.currentScrollY or 0.5)
|
|
self.targetScrollX, self.targetScrollY = self.currentScrollX, self.currentScrollY
|
|
child:SetScale(self:GetCanvasScale())
|
|
self:ApplyCurrentScroll()
|
|
self:MarkViewRectDirty()
|
|
local map = self:GetMapCanvas()
|
|
if map then
|
|
map:OnCanvasSizeChanged()
|
|
end
|
|
end
|
|
|
|
-- The cursor in canvas-normalized coordinates, which is what a click handler is handed.
|
|
function MapCanvasScrollControllerMixin:GetCursorPosition()
|
|
local x, y = GetCursorPosition()
|
|
local effective = self:GetEffectiveScale()
|
|
if not effective or effective == 0 then
|
|
return 0.5, 0.5
|
|
end
|
|
return x / effective, y / effective
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:GetNormalizedCursorPosition()
|
|
local cx, cy = self:GetCursorPosition()
|
|
local left, bottom = self:GetLeft(), self:GetBottom()
|
|
local w, h = self:GetWidth(), self:GetHeight()
|
|
if not left or not w or w <= 0 or h <= 0 then
|
|
return 0.5, 0.5
|
|
end
|
|
-- Inside the container first, then through the view onto the canvas: the container's centre is
|
|
-- the normalized scroll, and one container width spans `containerW / (canvasW * scale)` of it.
|
|
local fx = (cx - left) / w
|
|
local fy = 1 - (cy - bottom) / h
|
|
local spanX = self:NormalizeHorizontalSize(w / self:GetCanvasScale())
|
|
local spanY = self:NormalizeVerticalSize(h / self:GetCanvasScale())
|
|
return (self.currentScrollX or 0.5) + (fx - 0.5) * spanX,
|
|
(self.currentScrollY or 0.5) + (fy - 0.5) * spanY
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:WouldCursorPositionBeClick(button, x, y)
|
|
local info = self.mouseButtonInfo and self.mouseButtonInfo[button]
|
|
if not info or not info.down then
|
|
return false
|
|
end
|
|
local dx = (x or 0) - (info.x or 0)
|
|
local dy = (y or 0) - (info.y or 0)
|
|
return dx * dx + dy * dy <= CLICK_SLOP * CLICK_SLOP
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:OnMouseDown(button)
|
|
local info = self.mouseButtonInfo and self.mouseButtonInfo[button]
|
|
if not info then
|
|
return
|
|
end
|
|
local x, y = self:GetCursorPosition()
|
|
info.down = true
|
|
info.x, info.y = x, y
|
|
if button == "LeftButton" then
|
|
self.panning = true
|
|
self.panStartX, self.panStartY = x, y
|
|
self.panStartScrollX = self.currentScrollX
|
|
self.panStartScrollY = self.currentScrollY
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:OnMouseUp(button)
|
|
local info = self.mouseButtonInfo and self.mouseButtonInfo[button]
|
|
if not info then
|
|
return
|
|
end
|
|
local x, y = self:GetCursorPosition()
|
|
local wasClick = self:WouldCursorPositionBeClick(button, x, y)
|
|
info.down = false
|
|
if button == "LeftButton" then
|
|
self.panning = false
|
|
end
|
|
-- Recorded, not just returned: the stock `WorldMapButton`'s own mouse-up is the 1.12
|
|
-- click-to-zoom, and it must not run when a canvas handler took the click or when the gesture
|
|
-- was a drag. The wrapper in `BuildCanvas` reads this.
|
|
self.lastClickConsumed = true
|
|
local map = self:GetMapCanvas()
|
|
if wasClick and map then
|
|
local x, y = self:GetNormalizedCursorPosition()
|
|
self.lastClickConsumed = map:ProcessCanvasClickHandlers(button, x, y) and true or false
|
|
end
|
|
end
|
|
|
|
function MapCanvasScrollControllerMixin:OnMouseWheel(delta)
|
|
if delta > 0 then
|
|
self:ZoomIn()
|
|
else
|
|
self:ZoomOut()
|
|
end
|
|
end
|
|
|
|
-- The drag, and the ease toward the pan and zoom targets. One place, so a drag during a zoom
|
|
-- keeps working, and it stops writing once both have arrived.
|
|
function MapCanvasScrollControllerMixin:OnUpdate(elapsed)
|
|
local moved = false
|
|
if self.panning then
|
|
local x, y = self:GetCursorPosition()
|
|
local scale = self:GetCanvasScale()
|
|
if scale > 0 then
|
|
local dx = self:NormalizeHorizontalSize((self.panStartX - x) / scale)
|
|
local dy = self:NormalizeVerticalSize((y - self.panStartY) / scale)
|
|
local nx, ny = self:ClampScrollToExtents(
|
|
(self.panStartScrollX or 0.5) + dx, (self.panStartScrollY or 0.5) + dy)
|
|
if nx ~= self.currentScrollX or ny ~= self.currentScrollY then
|
|
self.currentScrollX, self.currentScrollY = nx, ny
|
|
self.targetScrollX, self.targetScrollY = nx, ny
|
|
moved = true
|
|
end
|
|
end
|
|
else
|
|
local rate = 1 - math.pow(0.5, (elapsed or 0) * PAN_RATE / 7)
|
|
local tx, ty = self.targetScrollX or 0.5, self.targetScrollY or 0.5
|
|
local cx, cy = self.currentScrollX or 0.5, self.currentScrollY or 0.5
|
|
if math.abs(tx - cx) > PAN_SNAP or math.abs(ty - cy) > PAN_SNAP then
|
|
self.currentScrollX = cx + (tx - cx) * rate
|
|
self.currentScrollY = cy + (ty - cy) * rate
|
|
moved = true
|
|
elseif cx ~= tx or cy ~= ty then
|
|
self.currentScrollX, self.currentScrollY = tx, ty
|
|
moved = true
|
|
end
|
|
end
|
|
|
|
local scaled = false
|
|
local ts, cs = self.targetScale or 1, self.currentScale or 1
|
|
if math.abs(ts - cs) > SCALE_SNAP then
|
|
local rate = 1 - math.pow(0.5, (elapsed or 0) * SCALE_RATE / 7)
|
|
self.currentScale = cs + (ts - cs) * rate
|
|
scaled = true
|
|
elseif cs ~= ts then
|
|
self.currentScale = ts
|
|
scaled = true
|
|
end
|
|
|
|
if scaled then
|
|
if self.Child then
|
|
self.Child:SetScale(self.currentScale)
|
|
end
|
|
self:CalculateScrollExtents()
|
|
self.currentScrollX, self.currentScrollY = self:ClampScrollToExtents(
|
|
self.currentScrollX, self.currentScrollY)
|
|
moved = true
|
|
end
|
|
if moved then
|
|
self:ApplyCurrentScroll()
|
|
self:MarkViewRectDirty()
|
|
end
|
|
local map = self:GetMapCanvas()
|
|
if map then
|
|
if scaled then
|
|
map:OnCanvasScaleChanged()
|
|
end
|
|
if moved then
|
|
map:OnCanvasPanChanged()
|
|
end
|
|
end
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- MapCanvasMixin — the map itself.
|
|
--------------------------------------------------------------------------------
|
|
|
|
MapCanvasMixin = {}
|
|
|
|
-- What a global pin mouse-action handler is told happened. The values are opaque to a caller,
|
|
-- which compares against these names (`MapCanvasMixin.MouseAction.Click`), so only the identity
|
|
-- matters.
|
|
MapCanvasMixin.MouseAction = { Click = 1, Up = 2, Down = 3 }
|
|
|
|
function MapCanvasMixin:OnLoad()
|
|
-- A SET, keyed by the provider with `true` as the value, because that is upstream's shape and
|
|
-- callers iterate it: `for provider in pairs(map.dataProviders) do` binds the *key*, so an
|
|
-- array here hands the loop the number 1 and the next line indexes a number. Measured: it
|
|
-- raised at `Map/Extras.lua:281`, inside the addon's own `OnEnable`, which caught it, printed
|
|
-- it and left the whole addon disabled — the same trap `pools.rs` names for `activeObjects`,
|
|
-- where a "tidier" array would break `for icon in pool:EnumerateActive()` silently.
|
|
--
|
|
-- `dataProviderOrder` is ours, beside it: `pairs` order is undefined, and a map whose
|
|
-- providers are notified in a different order each session is a bug nobody can reproduce. So
|
|
-- the set is the shape addons read and the list is the order we walk.
|
|
self.dataProviders = {}
|
|
self.dataProviderOrder = {}
|
|
self.dataProviderEventsCount = {}
|
|
self.pinPools = {}
|
|
self.pinTemplateTypes = {}
|
|
self.activePins = {}
|
|
self.canvasClickHandlers = {}
|
|
self.globalPinMouseActionHandlers = {}
|
|
self.cursorHandlers = {}
|
|
self.pinFrameLevelsManager = CreatePinFrameLevelsManager()
|
|
self.mapID = nil
|
|
self.globalPinScale = 1
|
|
end
|
|
|
|
function MapCanvasMixin:GetMapID()
|
|
return self.mapID
|
|
end
|
|
|
|
function MapCanvasMixin:GetCanvas()
|
|
return self.ScrollContainer and self.ScrollContainer.Child
|
|
end
|
|
|
|
function MapCanvasMixin:GetCanvasContainer()
|
|
return self.ScrollContainer
|
|
end
|
|
|
|
function MapCanvasMixin:GetCanvasScale()
|
|
return self.ScrollContainer and self.ScrollContainer:GetCanvasScale() or 1
|
|
end
|
|
|
|
function MapCanvasMixin:GetGlobalPinScale()
|
|
return self.globalPinScale or 1
|
|
end
|
|
|
|
function MapCanvasMixin:SetGlobalPinScale(scale)
|
|
self.globalPinScale = scale
|
|
self:OnCanvasScaleChanged()
|
|
end
|
|
|
|
function MapCanvasMixin:GetPinFrameLevelsManager()
|
|
return self.pinFrameLevelsManager
|
|
end
|
|
|
|
-- The view in canvas-normalized coordinates: left, right, top, bottom.
|
|
function MapCanvasMixin:GetViewRect()
|
|
local sc = self.ScrollContainer
|
|
if not sc then
|
|
return 0, 1, 0, 1
|
|
end
|
|
local scale = sc:GetCanvasScale()
|
|
if scale <= 0 then
|
|
return 0, 1, 0, 1
|
|
end
|
|
local halfW = sc:NormalizeHorizontalSize(sc:GetWidth() * 0.5 / scale)
|
|
local halfH = sc:NormalizeVerticalSize(sc:GetHeight() * 0.5 / scale)
|
|
local x, y = sc.currentScrollX or 0.5, sc.currentScrollY or 0.5
|
|
return x - halfW, x + halfW, y - halfH, y + halfH
|
|
end
|
|
|
|
function MapCanvasMixin:NormalizeHorizontalSize(size)
|
|
return self.ScrollContainer and self.ScrollContainer:NormalizeHorizontalSize(size) or 0
|
|
end
|
|
|
|
function MapCanvasMixin:NormalizeVerticalSize(size)
|
|
return self.ScrollContainer and self.ScrollContainer:NormalizeVerticalSize(size) or 0
|
|
end
|
|
|
|
function MapCanvasMixin:SetPinNudgingDirty(dirty)
|
|
self.pinNudgingDirty = dirty
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Data providers
|
|
--------------------------------------------------------------------------------
|
|
|
|
function MapCanvasMixin:AddDataProvider(dataProvider)
|
|
if not dataProvider or self.dataProviders[dataProvider] then
|
|
return
|
|
end
|
|
self.dataProviders[dataProvider] = true
|
|
self.dataProviderOrder[table.getn(self.dataProviderOrder) + 1] = dataProvider
|
|
if dataProvider.OnAdded then
|
|
dataProvider:OnAdded(self)
|
|
end
|
|
if self:IsShown() and dataProvider.RefreshAllData then
|
|
dataProvider:RefreshAllData(false)
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:RemoveDataProvider(dataProvider)
|
|
if not dataProvider or not self.dataProviders[dataProvider] then
|
|
return
|
|
end
|
|
self.dataProviders[dataProvider] = nil
|
|
for i = 1, table.getn(self.dataProviderOrder) do
|
|
if self.dataProviderOrder[i] == dataProvider then
|
|
table.remove(self.dataProviderOrder, i)
|
|
break
|
|
end
|
|
end
|
|
if dataProvider.RemoveAllData then
|
|
dataProvider:RemoveAllData()
|
|
end
|
|
if dataProvider.OnRemoved then
|
|
dataProvider:OnRemoved(self)
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:RemoveAllDataProviders()
|
|
while table.getn(self.dataProviderOrder) > 0 do
|
|
self:RemoveDataProvider(self.dataProviderOrder[1])
|
|
end
|
|
end
|
|
|
|
-- One walk, one guard: a provider that raises must not stop the providers after it, because a
|
|
-- third-party provider is ordinary here and a half-notified map draws a half-built view.
|
|
function MapCanvasMixin:CallDataProviders(method, a, b, c)
|
|
local providers = self.dataProviderOrder
|
|
for i = 1, table.getn(providers) do
|
|
local provider = providers[i]
|
|
local fn = provider[method]
|
|
if fn then
|
|
local ok, err = pcall(fn, provider, a, b, c)
|
|
if not ok then
|
|
geterrorhandler()(err)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:RefreshAllDataProviders(fromOnShow)
|
|
self:CallDataProviders("RefreshAllData", fromOnShow)
|
|
end
|
|
|
|
function MapCanvasMixin:OnMapChanged()
|
|
self:CallDataProviders("OnMapChanged")
|
|
end
|
|
|
|
function MapCanvasMixin:OnCanvasScaleChanged()
|
|
local pins = self.activePins
|
|
for i = 1, table.getn(pins) do
|
|
local pin = pins[i]
|
|
if pin.OnCanvasScaleChanged then
|
|
pin:OnCanvasScaleChanged()
|
|
end
|
|
end
|
|
self:CallDataProviders("OnCanvasScaleChanged")
|
|
end
|
|
|
|
function MapCanvasMixin:OnCanvasPanChanged()
|
|
local pins = self.activePins
|
|
for i = 1, table.getn(pins) do
|
|
local pin = pins[i]
|
|
if pin.OnCanvasPanChanged then
|
|
pin:OnCanvasPanChanged()
|
|
end
|
|
end
|
|
self:CallDataProviders("OnCanvasPanChanged")
|
|
end
|
|
|
|
function MapCanvasMixin:OnCanvasSizeChanged()
|
|
local pins = self.activePins
|
|
for i = 1, table.getn(pins) do
|
|
local pin = pins[i]
|
|
if pin.ApplyCurrentPosition then
|
|
pin:ApplyCurrentPosition()
|
|
end
|
|
end
|
|
self:CallDataProviders("OnCanvasSizeChanged")
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Pins
|
|
--------------------------------------------------------------------------------
|
|
|
|
function MapCanvasMixin:GetPinPool(pinTemplate)
|
|
local pool = self.pinPools[pinTemplate]
|
|
if not pool then
|
|
local frameType = self.pinTemplateTypes[pinTemplate] or "Frame"
|
|
pool = CreateFramePool(frameType, self:GetCanvas(), pinTemplate, function(_, pin)
|
|
pin:Hide()
|
|
pin:ClearAllPoints()
|
|
if pin.OnReleased then
|
|
pin:OnReleased()
|
|
end
|
|
end)
|
|
self.pinPools[pinTemplate] = pool
|
|
end
|
|
return pool
|
|
end
|
|
|
|
-- Declare the frame type a template needs, for the templates that are not plain Frames. Era's own
|
|
-- `SetPinTemplateType`; without it a Button-shaped pin would be created as a Frame and its
|
|
-- `SetScript("OnClick")` would be nil.
|
|
function MapCanvasMixin:SetPinTemplateType(pinTemplate, pinFrameType)
|
|
self.pinTemplateTypes[pinTemplate] = pinFrameType
|
|
end
|
|
|
|
-- Declared vararg so `arg` is this function's own table: `LUA_COMPAT_VARARG` hands it over to a
|
|
-- vararg function that does not otherwise mention `...` (`script/lua50.rs`), and without the `...`
|
|
-- `arg` would resolve to the *global* one a script handler leaves behind.
|
|
function MapCanvasMixin:AcquirePin(pinTemplate, ...)
|
|
local extra = arg
|
|
local pool = self:GetPinPool(pinTemplate)
|
|
local pin, isNew = pool:Acquire()
|
|
pin.pinTemplate = pinTemplate
|
|
pin.owningMap = self
|
|
if isNew then
|
|
for k, v in pairs(MapCanvasPinMixin) do
|
|
if pin[k] == nil then
|
|
pin[k] = v
|
|
end
|
|
end
|
|
if pin.OnLoad then
|
|
pin:OnLoad()
|
|
end
|
|
end
|
|
self.activePins[table.getn(self.activePins) + 1] = pin
|
|
pin:Show()
|
|
pin:ApplyFrameLevel()
|
|
if pin.OnAcquired then
|
|
if extra and table.getn(extra) > 0 then
|
|
pin:OnAcquired(unpack(extra))
|
|
else
|
|
pin:OnAcquired()
|
|
end
|
|
end
|
|
pin:ApplyCurrentScale()
|
|
return pin
|
|
end
|
|
|
|
function MapCanvasMixin:RemovePin(pin)
|
|
for i = 1, table.getn(self.activePins) do
|
|
if self.activePins[i] == pin then
|
|
table.remove(self.activePins, i)
|
|
break
|
|
end
|
|
end
|
|
local pool = pin.pinTemplate and self.pinPools[pin.pinTemplate]
|
|
if pool then
|
|
pool:Release(pin)
|
|
else
|
|
pin:Hide()
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:RemoveAllPinsByTemplate(pinTemplate)
|
|
local i = 1
|
|
while i <= table.getn(self.activePins) do
|
|
local pin = self.activePins[i]
|
|
if pin.pinTemplate == pinTemplate then
|
|
table.remove(self.activePins, i)
|
|
local pool = self.pinPools[pinTemplate]
|
|
if pool then
|
|
pool:Release(pin)
|
|
else
|
|
pin:Hide()
|
|
end
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:RemoveAllPins()
|
|
while table.getn(self.activePins) > 0 do
|
|
self:RemovePin(self.activePins[1])
|
|
end
|
|
end
|
|
|
|
-- An iterator, because that is what every caller writes: `for pin in
|
|
-- map:EnumeratePinsByTemplate(t) do`. A template with no pins answers an iterator that stops
|
|
-- immediately, never nil, so the loop is always safe.
|
|
function MapCanvasMixin:EnumeratePinsByTemplate(pinTemplate)
|
|
local pins = self.activePins
|
|
local i = 0
|
|
return function()
|
|
while true do
|
|
i = i + 1
|
|
local pin = pins[i]
|
|
if not pin then
|
|
return nil
|
|
end
|
|
if pin.pinTemplate == pinTemplate then
|
|
return pin
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:EnumerateAllPins()
|
|
local pins = self.activePins
|
|
local i = 0
|
|
return function()
|
|
i = i + 1
|
|
return pins[i]
|
|
end
|
|
end
|
|
|
|
function MapCanvasMixin:GetNumActivePinsByTemplate(pinTemplate)
|
|
local n = 0
|
|
for i = 1, table.getn(self.activePins) do
|
|
if self.activePins[i].pinTemplate == pinTemplate then
|
|
n = n + 1
|
|
end
|
|
end
|
|
return n
|
|
end
|
|
|
|
function MapCanvasMixin:GetHoveredPin()
|
|
for i = 1, table.getn(self.activePins) do
|
|
local pin = self.activePins[i]
|
|
if pin:IsShown() and pin:IsMouseOver() then
|
|
return pin
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Handlers: canvas clicks, global pin mouse actions, the cursor
|
|
--
|
|
-- Each is a priority-sorted list, highest first, and the first handler that returns true consumes
|
|
-- the event. That ordering is the contract — a canvas addon registers at priority 1000 precisely
|
|
-- so it sees a click before Blizzard's own handlers.
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function InsertByPriority(list, handler, priority)
|
|
local entry = { handler = handler, priority = priority or 0 }
|
|
local n = table.getn(list)
|
|
local at = n + 1
|
|
for i = 1, n do
|
|
if (list[i].priority or 0) < entry.priority then
|
|
at = i
|
|
break
|
|
end
|
|
end
|
|
table.insert(list, at, entry)
|
|
end
|
|
|
|
function MapCanvasMixin:AddCanvasClickHandler(handler, priority)
|
|
InsertByPriority(self.canvasClickHandlers, handler, priority)
|
|
end
|
|
|
|
function MapCanvasMixin:AddGlobalPinMouseActionHandler(handler, priority)
|
|
InsertByPriority(self.globalPinMouseActionHandlers, handler, priority)
|
|
end
|
|
|
|
function MapCanvasMixin:AddCursorHandler(handler, priority)
|
|
InsertByPriority(self.cursorHandlers, handler, priority)
|
|
end
|
|
|
|
function MapCanvasMixin:ProcessCanvasClickHandlers(button, cursorX, cursorY)
|
|
local list = self.canvasClickHandlers
|
|
for i = 1, table.getn(list) do
|
|
local ok, consumed = pcall(list[i].handler, self, button, cursorX, cursorY)
|
|
if not ok then
|
|
geterrorhandler()(consumed)
|
|
elseif consumed then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function MapCanvasMixin:ProcessGlobalPinMouseActionHandlers(mouseAction, button)
|
|
local list = self.globalPinMouseActionHandlers
|
|
for i = 1, table.getn(list) do
|
|
local ok, consumed = pcall(list[i].handler, self, mouseAction, button)
|
|
if not ok then
|
|
geterrorhandler()(consumed)
|
|
elseif consumed then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function MapCanvasMixin:ProcessCursorHandlers()
|
|
local list = self.cursorHandlers
|
|
for i = 1, table.getn(list) do
|
|
local ok, cursor = pcall(list[i].handler, self)
|
|
if not ok then
|
|
geterrorhandler()(cursor)
|
|
elseif cursor then
|
|
return cursor
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Navigation
|
|
--------------------------------------------------------------------------------
|
|
|
|
function MapCanvasMixin:NavigateToMap(mapID)
|
|
self:SetMapID(mapID)
|
|
end
|
|
|
|
function MapCanvasMixin:NavigateToParentMap()
|
|
local info = self.mapID and C_Map.GetMapInfo(self.mapID)
|
|
if info and info.parentMapID and info.parentMapID ~= 0 then
|
|
self:SetMapID(info.parentMapID)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
function MapCanvasMixin:NavigateToCursor()
|
|
local sc = self.ScrollContainer
|
|
if not sc or not self.mapID then
|
|
return false
|
|
end
|
|
local x, y = sc:GetNormalizedCursorPosition()
|
|
local info = C_Map.GetMapInfoAtPosition(self.mapID, x, y)
|
|
if info and info.mapID ~= self.mapID then
|
|
self:SetMapID(info.mapID)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- The map change
|
|
--------------------------------------------------------------------------------
|
|
|
|
function MapCanvasMixin:SetMapID(mapID)
|
|
if not mapID or self.mapID == mapID then
|
|
return
|
|
end
|
|
self.mapID = mapID
|
|
if self.ScrollContainer then
|
|
self.ScrollContainer.mapID = mapID
|
|
end
|
|
-- The stock window follows, so the 1.12 detail art under this canvas and every 1.12 addon's
|
|
-- icons on it are the same place the canvas shows. The engine's own selection is the authority
|
|
-- for both; see `BenillaSelectMapID`.
|
|
BenillaSelectMapID(mapID)
|
|
self:RefreshDetailLayers()
|
|
if self.ScrollContainer then
|
|
self.ScrollContainer:OnCanvasSizeChanged()
|
|
self.ScrollContainer:ResetZoom()
|
|
end
|
|
self:OnMapChanged()
|
|
self:RefreshAllDataProviders(false)
|
|
end
|
|
|
|
-- Follow the stock window when something else moved it (its dropdowns, `SetMapToCurrentZone`,
|
|
-- another addon's `SetMapZoom`), without calling back into it.
|
|
function MapCanvasMixin:SyncFromSelection()
|
|
local mapID = BenillaMapIDForSelection()
|
|
if not mapID or mapID == self.mapID then
|
|
return
|
|
end
|
|
self.mapID = mapID
|
|
if self.ScrollContainer then
|
|
self.ScrollContainer.mapID = mapID
|
|
end
|
|
self:RefreshDetailLayers()
|
|
if self.ScrollContainer then
|
|
self.ScrollContainer:OnCanvasSizeChanged()
|
|
self.ScrollContainer:ResetZoom()
|
|
end
|
|
self:OnMapChanged()
|
|
self:RefreshAllDataProviders(false)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- The detail layers
|
|
--
|
|
-- Era's canvas builds its base art from a pool of layer frames. Ours has exactly one layer and it
|
|
-- is 1.12's own `WorldMapDetailFrame`, reparented into the canvas — so the base art is whatever
|
|
-- the stock window draws, from the same selection, and there is no second copy of the tiling law
|
|
-- to disagree with it. The pool exists because a canvas addon enumerates it to hide the base art
|
|
-- while it draws its own (`for layer in map.detailLayerPool:EnumerateActive() do`), and that is
|
|
-- the only thing anyone does with it.
|
|
--------------------------------------------------------------------------------
|
|
|
|
local DetailLayerPoolMixin = {}
|
|
|
|
function DetailLayerPoolMixin:EnumerateActive()
|
|
local layers = self.layers
|
|
local i = 0
|
|
return function()
|
|
i = i + 1
|
|
return layers[i]
|
|
end
|
|
end
|
|
|
|
function DetailLayerPoolMixin:GetNumActive()
|
|
return table.getn(self.layers)
|
|
end
|
|
|
|
function MapCanvasMixin:RefreshDetailLayers()
|
|
-- `WorldMapFrame_Update` is the stock repaint, driven by `WORLD_MAP_UPDATE`; nothing to do
|
|
-- here but let a hook see the call, which is what a canvas addon hooks it for.
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- MapUtil
|
|
--------------------------------------------------------------------------------
|
|
|
|
MapUtil = {}
|
|
|
|
function MapUtil.GetDisplayableMapForPlayer()
|
|
local mapID = C_Map.GetBestMapForUnit("player")
|
|
if mapID then
|
|
return mapID
|
|
end
|
|
return C_Map.GetFallbackWorldMapID()
|
|
end
|
|
|
|
-- The name of the smallest thing under the cursor: a sub-area's if its highlight claims the
|
|
-- point, else the child map's, else nothing. Over `C_Map`, so the name is the one the stock
|
|
-- window's own hover would show for that point.
|
|
function MapUtil.FindBestAreaNameAtMouse(mapID, x, y)
|
|
if not mapID or not x or not y then
|
|
return nil
|
|
end
|
|
local info = C_Map.GetMapInfoAtPosition(mapID, x, y)
|
|
if info and info.mapID ~= mapID then
|
|
return info.name
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function MapUtil.ShouldMapTypeShowQuests()
|
|
return true
|
|
end
|
|
|
|
function MapUtil.IsMapTypeAllowed(mapType)
|
|
return mapType == Enum.UIMapType.Continent or mapType == Enum.UIMapType.Zone
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Bolting the canvas onto 1.12's WorldMapFrame
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function Mix(target, mixin)
|
|
for k, v in pairs(mixin) do
|
|
target[k] = v
|
|
end
|
|
end
|
|
|
|
local function BuildDefaultFrameLevels(manager)
|
|
-- Blizzard's own band names and order, bottom to top. The names are the contract: a canvas
|
|
-- addon inserts its own band relative to one of these by name, so a missing or renamed band is
|
|
-- art landing in the wrong place.
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_TERRAIN", 4)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_MAP_EXPLORATION", 4)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_MAP_HIGHLIGHT", 4)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_AREA_POI", 8)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_FLIGHT_POINT", 8)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_WORLD_QUEST", 16)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_STORY_LINE", 8)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_GROUP_MEMBER", 8)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_CORPSE", 4)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_UNIT", 8)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_MAP_LINK", 4)
|
|
manager:InsertFrameLevel("PIN_FRAME_LEVEL_DEATH_RELEASE", 4)
|
|
end
|
|
|
|
-- The pan mechanism: the canvas's own TOPLEFT against the container's, which is Blizzard's own
|
|
-- way and the one whose signs this file controls. A ScrollFrame's scroll offsets would do it with
|
|
-- no layout touch, but their sign convention is the engine's rather than this file's, and a pan
|
|
-- already dirties the layout of everything on the canvas.
|
|
function MapCanvasScrollControllerMixin:ApplyCurrentScroll()
|
|
local child = self.Child
|
|
if not child then
|
|
return
|
|
end
|
|
local scale = self:GetCanvasScale()
|
|
local cw = child:GetWidth() * scale
|
|
local ch = child:GetHeight() * scale
|
|
-- The canvas point under the container's middle is `currentScroll`; place the canvas so that
|
|
-- point lands there. Divided by the scale because the offsets are in the canvas's own units,
|
|
-- which the scale multiplies.
|
|
local offsetX = ((self.currentScrollX or 0.5) * cw - self:GetWidth() * 0.5) / scale
|
|
local offsetY = ((self.currentScrollY or 0.5) * ch - self:GetHeight() * 0.5) / scale
|
|
child:ClearAllPoints()
|
|
child:SetPoint("TOPLEFT", self, "TOPLEFT", -offsetX, offsetY)
|
|
end
|
|
|
|
local function BuildCanvas()
|
|
local map = WorldMapFrame
|
|
local detail = WorldMapDetailFrame
|
|
local button = WorldMapButton
|
|
if not map or not detail or not button or map.ScrollContainer then
|
|
return
|
|
end
|
|
|
|
Mix(map, MapCanvasMixin)
|
|
map:OnLoad()
|
|
|
|
-- The container takes the detail frame's own rect, so at rest the canvas is exactly where the
|
|
-- stock art was and the window is pixel-identical. A real ScrollFrame, for the clipping its
|
|
-- scroll child and every descendant of it get (see this file's header).
|
|
local sc = CreateFrame("ScrollFrame", "WorldMapScrollContainer", map)
|
|
sc:SetWidth(detail:GetWidth())
|
|
sc:SetHeight(detail:GetHeight())
|
|
-- The detail frame's **own** XML anchor, copied rather than referenced: once it is reparented
|
|
-- into the canvas below, anchoring the container to it would be a cycle — container to detail
|
|
-- to canvas to container — and a cycle resolves to nothing, which showed up as
|
|
-- `WorldMapButton:GetCenter()` answering nil inside the stock hover pass. The measured offsets
|
|
-- against `WorldMapFrame` are the fallback for a chain whose map window is laid out some other
|
|
-- way.
|
|
local guide = WorldMapPositioningGuide
|
|
if guide then
|
|
sc:SetPoint("TOPLEFT", guide, "TOP", -502, -69)
|
|
else
|
|
local l, t = detail:GetLeft(), detail:GetTop()
|
|
local ml, mt = map:GetLeft(), map:GetTop()
|
|
if l and t and ml and mt then
|
|
sc:SetPoint("TOPLEFT", map, "TOPLEFT", l - ml, t - mt)
|
|
else
|
|
sc:SetPoint("TOPLEFT", map, "TOPLEFT", 0, 0)
|
|
end
|
|
end
|
|
-- The container takes the mouse as well, UNDER 1.12's `WorldMapButton`.
|
|
--
|
|
-- The button was the whole mouse surface here, on the reasoning that it covers the same rect
|
|
-- and sits above this in the tree. That holds at rest and stops holding the moment a canvas
|
|
-- addon scales the canvas: the button is pinned to the canvas child, so at a scale below 1 it
|
|
-- shrinks away from the container and leaves the rest of the visible map with no mouse
|
|
-- surface at all. A press there reaches the next frame that takes the mouse, which with pfUI
|
|
-- loaded is `WorldMapFrame` itself -- `modules/map.lua:45` makes the whole window a
|
|
-- `RegisterForDrag("LeftButton")` handle -- so dragging the map moved the window instead of
|
|
-- panning, and whether it did depended on the zoom and where you grabbed. grom, 2026-11-04:
|
|
-- *"sometimes its panning the map as it should and sometimes its grabbing and moving the
|
|
-- whole window, this should only happen on the borders."*
|
|
--
|
|
-- The button still wins wherever it reaches, being deeper; this only catches what used to
|
|
-- fall through the floor, and it drives the same controller, so a pan begun on either is one
|
|
-- gesture. It takes no clicks (`RegisterForClicks` is never called), so it cannot swallow a
|
|
-- click the map's own surface would have had.
|
|
sc:EnableMouse(true)
|
|
|
|
-- ...and the container crops everything below it as well, which the ScrollFrame alone does
|
|
-- not: that crops the scroll child's subtree, and an addon is free to parent a layer to the
|
|
-- CONTAINER beside the canvas. Era's own container is a plain Frame with nothing but this, so
|
|
-- that is where such a layer expects to be cropped -- OpenMapClassic's pin layer sits exactly
|
|
-- there and asks for `SetClipsChildren` itself, behind an `if` that was false on this client
|
|
-- until the verb existed. Both mechanisms now hold, and they intersect.
|
|
sc:SetClipsChildren(true)
|
|
|
|
local canvas = CreateFrame("Frame", "WorldMapCanvas", sc)
|
|
canvas:SetWidth(detail:GetWidth())
|
|
canvas:SetHeight(detail:GetHeight())
|
|
sc:SetScrollChild(canvas)
|
|
|
|
Mix(sc, MapCanvasScrollControllerMixin)
|
|
sc.Child = canvas
|
|
sc.mapCanvas = map
|
|
sc:OnLoad()
|
|
|
|
map.ScrollContainer = sc
|
|
map.Canvas = canvas
|
|
|
|
-- 1.12's art and its mouse surface become the canvas's content: the twelve detail tiles, the
|
|
-- exploration overlays, the hover highlight, the POI buttons, the player arrow and the party
|
|
-- and raid pins — and with them every 1.12 addon's icons, which are all anchored to or
|
|
-- parented into these two frames. They now pan, scale and clip with the canvas, which is the
|
|
-- whole reason the canvas is built around them instead of beside them: a 1.12 quest icon stays
|
|
-- on its quest while a canvas addon moves the view.
|
|
detail:SetParent(canvas)
|
|
button:SetParent(canvas)
|
|
-- ...and the canvas becomes their only layout authority, which is a change of owner and not
|
|
-- just of parent: see "1002x668 is a contract, not a size".
|
|
SeatOnCanvas()
|
|
if WorldMapFrame_Minimize then
|
|
hooksecurefunc("WorldMapFrame_Minimize", SeatOnCanvas)
|
|
end
|
|
if WorldMapFrame_Maximize then
|
|
hooksecurefunc("WorldMapFrame_Maximize", SeatOnCanvas)
|
|
end
|
|
|
|
-- The stock repaint places the exploration overlays in 1002x668 pixels and is the only moment
|
|
-- those base offsets are readable; after it, they are scaled to whatever size the canvas has
|
|
-- given the detail frame (see "The stock art, at a canvas size that is not 1.12's").
|
|
if WorldMapFrame_Update then
|
|
hooksecurefunc("WorldMapFrame_Update", CaptureOverlays)
|
|
end
|
|
CaptureOverlays()
|
|
|
|
-- The container resizing IS a canvas size change, and until now nothing said so: the canvas
|
|
-- was re-sized only when the map changed (`SetMapID`, `SyncFromSelection`), so a window
|
|
-- resized while the map stayed put left the canvas at the old size. openmap resizes this
|
|
-- container explicitly, which is the case that reached grom.
|
|
--
|
|
-- Only an explicit `SetWidth`/`SetHeight` is covered, because that is all this client fires
|
|
-- `OnSizeChanged` for — measured: a child `SetAllPoints` to a parent that is then resized
|
|
-- gets no event at all. That is why the art is rescaled inside `OnCanvasSizeChanged` rather
|
|
-- than hung off the detail frame's own resize, which would never run.
|
|
sc:SetScript("OnSizeChanged", function(self)
|
|
self:OnCanvasSizeChanged()
|
|
end)
|
|
|
|
-- The window resizing moves the content area, so the container has to follow it. pfUI's map
|
|
-- module is the case that reaches a player: it sizes `WorldMapFrame` from
|
|
-- `WorldMapButton:GetSize()` at `PLAYER_ENTERING_WORLD`, long after this ran. Same limit as
|
|
-- the container's own hook — only an explicit `SetWidth`/`SetHeight` fires it, which is what
|
|
-- `WorldMapFrame_Minimize` and pfUI both do; the maximize leg has no size of its own
|
|
-- (`SetAllPoints`) and is covered by the `WorldMapFrame_Maximize` hook below.
|
|
map:HookScript("OnSizeChanged", function()
|
|
SeatOnCanvas()
|
|
end)
|
|
|
|
-- Everything the button carries is map content except one frame: `WorldMapFrameAreaFrame`,
|
|
-- the hovered-zone name. 1.12's XML hangs it off the mouse surface (TOP, 400x128) because
|
|
-- that is what covers the art, but it is chrome — it names what the cursor is over and sits
|
|
-- at the top of the window, at the size 1.12 drew it. Its label is a **102 px THICKOUTLINE**
|
|
-- FontString, so inside the canvas it scales with the zoom: at zoom 2.5 that is a 261 px
|
|
-- glyph per letter, and a zone name's worth of those fills the whole glyph sheet. Measured in
|
|
-- grom's 2026-11-02 run — `ui_text: a 261.10907 logical-px request ... past the 256 ceiling`
|
|
-- and then 20 `glyph sheet full ... resetting at the frame boundary`, one per hover, each
|
|
-- costing every string on screen a frame. So it goes back on the window, anchored to the
|
|
-- container's top, which is where the art's top edge is now; at rest that is the pixel it was
|
|
-- already at.
|
|
local area = WorldMapFrameAreaFrame
|
|
if area then
|
|
area:SetParent(map)
|
|
area:ClearAllPoints()
|
|
area:SetPoint("TOP", sc, "TOP", 0, -10)
|
|
end
|
|
|
|
-- The base-art layer a canvas addon hides while it draws its own. It is a proxy rather than the
|
|
-- detail frame itself, and that distinction is the point: `Hide()` on the frame would take the
|
|
-- POI buttons and every addon's icons down with it, because they are its children and its
|
|
-- siblings' children. Switching off its BACKGROUND and ARTWORK **draw layers** removes exactly
|
|
-- the stock tiles, the exploration overlays and the hover highlight — regions, all three —
|
|
-- and leaves every child frame drawing.
|
|
local baseLayer = {
|
|
SetShown = function(self, shown)
|
|
if shown then
|
|
detail:EnableDrawLayer("BACKGROUND")
|
|
detail:EnableDrawLayer("ARTWORK")
|
|
else
|
|
detail:DisableDrawLayer("BACKGROUND")
|
|
detail:DisableDrawLayer("ARTWORK")
|
|
end
|
|
self.shown = shown and true or false
|
|
end,
|
|
Show = function(self)
|
|
self:SetShown(true)
|
|
end,
|
|
Hide = function(self)
|
|
self:SetShown(false)
|
|
end,
|
|
IsShown = function(self)
|
|
return self.shown ~= false
|
|
end,
|
|
SetAlpha = function() end,
|
|
GetAlpha = function()
|
|
return 1
|
|
end,
|
|
}
|
|
map.detailLayerPool = { layers = { baseLayer } }
|
|
Mix(map.detailLayerPool, DetailLayerPoolMixin)
|
|
|
|
BuildDefaultFrameLevels(map.pinFrameLevelsManager)
|
|
|
|
-- The controller's input, off 1.12's own map button.
|
|
--
|
|
-- Its `<OnMouseUp>` *is* the 1.12 click-to-zoom (`WorldMapButton_OnClick`), so that one is
|
|
-- replaced rather than hooked: a canvas click handler that consumes the click must stop the
|
|
-- stock zoom, and a drag that ended over the map is not a click at all. Both of those are
|
|
-- decisions the controller has already made by the time the wrapper reads
|
|
-- `lastClickConsumed`, and neither can be expressed by a hook, which cannot refuse.
|
|
local stockMouseUp = button:GetScript("OnMouseUp")
|
|
button:SetScript("OnMouseUp", function()
|
|
sc:OnMouseUp(arg1)
|
|
if not sc.lastClickConsumed and stockMouseUp then
|
|
stockMouseUp()
|
|
end
|
|
end)
|
|
button:SetScript("OnMouseDown", function()
|
|
sc:OnMouseDown(arg1)
|
|
end)
|
|
button:SetScript("OnMouseWheel", function()
|
|
sc:OnMouseWheel(arg1)
|
|
end)
|
|
button:HookScript("OnUpdate", function()
|
|
sc:OnUpdate(arg1)
|
|
end)
|
|
button:EnableMouseWheel(true)
|
|
button:RegisterForClicks("LeftButtonUp", "RightButtonUp", "MiddleButtonUp")
|
|
|
|
-- ...and the same three off the container, for the part of the window the button has shrunk
|
|
-- away from. `OnUpdate` is the button's alone: the controller eases once a frame, and two
|
|
-- callers would ease it twice.
|
|
--
|
|
-- The container must out-level the map window, and the button must out-level the container.
|
|
--
|
|
-- The hit sweep probes one `(strata, level)` plane in **link** order, and a parent is linked
|
|
-- before its children (`0x764aa0`; `TargetFrame.lua:33` relies on it), so a frame tied with
|
|
-- its own parent loses every press to it. Tied with `WorldMapFrame` the container took
|
|
-- nothing, which is a mouse-enabled container that still handed the drag to pfUI.
|
|
--
|
|
-- 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.
|
|
local function SeatLevels()
|
|
local base = map:GetFrameLevel() or 0
|
|
sc:SetFrameLevel(base + 1)
|
|
if canvas:GetFrameLevel() <= sc:GetFrameLevel() then
|
|
canvas:SetFrameLevel(sc:GetFrameLevel() + 1)
|
|
end
|
|
if button:GetFrameLevel() <= canvas:GetFrameLevel() then
|
|
button:SetFrameLevel(canvas:GetFrameLevel() + 1)
|
|
end
|
|
end
|
|
SeatLevels()
|
|
map:HookScript("OnShow", SeatLevels)
|
|
sc:SetScript("OnMouseDown", function()
|
|
sc:OnMouseDown(arg1)
|
|
end)
|
|
sc:SetScript("OnMouseUp", function()
|
|
sc:OnMouseUp(arg1)
|
|
end)
|
|
sc:SetScript("OnMouseWheel", function()
|
|
sc:OnMouseWheel(arg1)
|
|
end)
|
|
sc:EnableMouseWheel(true)
|
|
|
|
-- Seat the first map before anything reads `GetMapID()`: the stock window has already resolved
|
|
-- the player's zone by the time this runs, so the canvas starts where the window is.
|
|
map.mapID = BenillaMapIDForSelection() or WORLD_MAP_ID
|
|
sc.mapID = map.mapID
|
|
sc:OnCanvasSizeChanged()
|
|
|
|
map:HookScript("OnShow", function()
|
|
WorldMapFrame:SyncFromSelection()
|
|
WorldMapFrame:RefreshAllDataProviders(true)
|
|
WorldMapFrame:CallDataProviders("OnShow")
|
|
end)
|
|
map:HookScript("OnHide", function()
|
|
WorldMapFrame:CallDataProviders("OnHide")
|
|
end)
|
|
end
|
|
|
|
local loader = CreateFrame("Frame", "BenillaMapCanvasLoader")
|
|
loader:RegisterEvent("PLAYER_LOGIN")
|
|
loader:RegisterEvent("WORLD_MAP_UPDATE")
|
|
loader:SetScript("OnEvent", function()
|
|
if event == "PLAYER_LOGIN" then
|
|
BuildCanvas()
|
|
elseif event == "WORLD_MAP_UPDATE" and WorldMapFrame and WorldMapFrame.SyncFromSelection then
|
|
-- Something moved the stock window: its dropdowns, `SetMapToCurrentZone`, another addon.
|
|
-- The canvas follows without calling back, so the two cannot fight over the selection.
|
|
WorldMapFrame:SyncFromSelection()
|
|
end
|
|
end)
|