Action Palette: a hold-and-release action picker - #1187
Conversation
68d756a to
de14176
Compare
|
Pushed the next round of nested-palette work (rebased onto current main, plus four new commits). Grid nests
Arc nests
Options
All of it is verified offline by the agreement harness (real secure-snippet plumbing, walks to every child, release resolution). Still needs in-game confirmation:
Known limit, fix in progress: with three or more sub-palettes on one grid the region budget can overflow and drop coverage, which can break a reach or a swap. A follow-up commit will make the budget lossless. |
A hold-to-open radial action wheel: hold a keybind, steer the mouse toward a wedge, release to fire it. Releasing inside the dead zone cancels. Activation is secure and taint-free. Each ring owns one hidden SecureActionButtonTemplate registered for AnyDown/AnyUp, with the ring's keybind routed to it via SetOverrideBindingClick. The DOWN edge opens the wheel and clears "type"; the UP edge writes the hovered slot's action attributes so the same hardware click performs the cast. The button pins useOnKeyDown to false so the acting edge is UP regardless of the ActionButtonUseKeyDown CVar. Override-binding updates are the only protected calls and are deferred to PLAYER_REGEN_ENABLED in combat, guarded by a signature check so the UPDATE_BINDINGS they emit cannot feed itself. Slots hold spells, items, toys, mounts, macros, custom macro text and battle pets. A ring's slots are a dense ordered array, so the wheel auto-sizes to what the user has actually assigned rather than leaving dead gaps. Mounts cast through their summon spell by name, since C_MountJournal.SummonByID is protected and those spells are not in the spellbook. Configuration is an in-page ring preview rather than a full-screen editor. Actions are added by clicking the trailing "+" wedge and choosing from a searchable per-category picker, or by dropping an action from the cursor; wedges reorder by drag and clear by right-click. Keybinds are set with an inline click-to-bind capture that writes the real EUI_RADIAL<n> binding, so it stays consistent with Blizzard's Keybindings page and accepts modified mouse buttons. Registered with the suite roster, profiles, spec overrides, first-install, global search and the packager.
A coverflow alternative to steering by cursor angle. The new layout setting takes RADIAL, FAN_H or FAN_V; the fan modes draw the ring as a horizontal or vertical strip with the selected entry centred at full size and its neighbours shrinking and fading by a fixed per-step ratio. The motive is that the cursor is frozen whenever the right button steers the camera, which is exactly when the wheel gets used. A fan is scrubbed with the mouse wheel instead, and the centred entry is the selection, so there is no hit test, dead zone or needle at all. Scroll is captured by a full-screen frame with EnableMouseWheel and never EnableMouse: a mouse-enabled overlay would sit between the player and the world and would swallow the button presses the secure activation path depends on. An override binding on the wheel is not an option, being protected and therefore unclaimable at open time in combat. Distance from the centre is the integral of the scale curve plus a constant gap rather than a sum of discrete steps, so spacing derives from the sizes it separates and stays defined for the fractional offsets the slide animation produces. Past the knee where the curve meets its floor the spacing goes linear, since scale stops shrinking there and geometric spacing would close the gaps under icons that had stopped getting smaller. Depth is expressed with SetSize rather than SetScale, because SetPoint offsets are read in the widget's own scaled space and scaling would silently multiply the spacing. The logical index moves the instant a scroll tick arrives and only the geometry is interpolated, so a release mid-animation fires what was last scrolled to. The strip opens with nothing selected and the first tick is what enters it, which preserves the radial's contract that releasing without steering cancels. The centre caption hangs on whichever side faces the middle of the screen and justifies to match, so a strip opened near an edge writes inward rather than off the edge. The options preview shares the renderer as before, fitting the strip against the axis it runs along rather than against the ring's radius budget, and raising its scale and alpha floors so every entry stays legible and hittable while dragging.
Four ways to steer a ring, sharing one renderer. Arcs generalise the wheel rather than sitting beside it: the radial layout is now an arc of 360 degrees, and arcSpan below that fans the entries across a sector centred on arcRotation. A full circle divides by the entry count and wraps, while an arc divides by the count minus one, so its first and last entries sit on its ends instead of leaving a step-wide gap belonging to no entry. Off either end of an arc is a miss rather than a clamp, which is where its cancel lives once the dead zone has been left. Resolving the hit angle folds it into a full turn from the arc's start, NOT into a half turn either side: an arc may span up to a full circle, so an offset past 180 degrees is a legitimate position near its end rather than a negative one near its start. The grid lays every entry out at a fixed cell and zooms whichever is nearest the pointer. Pointer travel to the worst entry then grows with the square root of the count rather than linearly, and a fixed arrangement is far easier to build muscle memory against than a position along a line. Falloff is true 2D distance, since a grid has no privileged axis to project onto. Rows are centred individually so a short final row sits under the middle of the one above. A pointer-steered fan is that same layout one entry deep -- FAN_H a single row, FAN_V a single column -- so fanInput CURSOR resolves through the grid instead of a parallel implementation. Only the scroll fan needs geometry of its own, because it cycles a compressed window rather than showing fixed positions. Flick-ahead holds the radial invisible for a moment and then fades it in, so a gesture finished inside that window never summons a menu at all. It delays only the drawing: the frame is shown and its OnUpdate runs throughout, so a fast flick lands on exactly the selection a slow one would have.
The module draws arcs, strips and grids as well as wheels, so the old name described one of four layouts. Folder, files, addon name, frame names, the options page and the suite registrations all move to Action Palette. Two names deliberately do NOT move. The EUI_RADIAL<n> bindings keep their identifiers: WoW stores bindings by name and offers no rename, so changing them would silently unbind every ring, and the strings are internal -- the Keybindings page shows the header text, not these. The /euirw and /euiradial slash commands stay registered as aliases alongside /euiap and /euipalette, because a command that stops existing after a rename reads as the module having been removed. The saved variable does move, so the toc declares BOTH names for this release and OnInitialize adopts the old table before AceDB sees the new one. An undeclared SavedVariable is never read in at all, so declaring only the new name would leave nothing to migrate from and every configured ring would vanish. The whole table is adopted rather than just the profile, which keeps per-character selection and profile structure intact. The second declaration can go a release after this one ships. Grid columns are now derived rather than fixed at four. Near-square is the right default because a grid exists to shorten the WORST pointer travel, and that is minimised when the axes are balanced: nine entries want 3x3, not 4 + 4 + 1. The one refinement on ceil(sqrt) is that a final row holding a single entry reads as a mistake rather than a layout, and widening by one column always absorbs it, so three becomes one row of three and seven becomes 4 + 3. gridColumns still pins the count when gridAutoColumns is turned off.
hubIcon swaps the hub's small additive star for the suite logo, with size and opacity sliders. The path comes from EllesmereUI.MEDIA_PATH rather than a literal, so the standalone packager's rename of the EllesmereUI token reaches it like every other media reference. Radial only, and only where the centre is empty: the fan and grid layouts put a real entry there, so the hub draws no art in them at all. The texture keeps the default blend mode rather than the star's ADD -- this is artwork with its own alpha, and ADD would wash its dark areas out into whatever is behind the wheel. It sits on ARTWORK so the hub's OVERLAY caption still reads on top, and the opacity slider is what keeps that caption legible rather than being decoration. Size is scaled by the factor the view scaled its geometry by, recovered from the icon size Geom returns. The options preview fits the ring to its panel, so a hub drawn at the profile's literal pixel size would swamp a ring shrunk to fit.
The column slider was dead. Auto-sizing arrived with a gridAutoColumns key defaulting to true, but no control was ever added to turn it off, so the auto path always won and the slider moved without changing anything. Adds the Auto Columns toggle and gates the slider on it. Auto columns were also counted from the SHOWN wedge count rather than the real one. An interactive view draws one extra wedge for the trailing "+", so that extra could tip the count and lay the editor's grid out differently from the way the ring is actually played -- six actions previewed as 4 + 3 while the live palette drew 3 + 3. Counting real entries makes the two agree for every ring size; the "+" now appends into that arrangement instead of reshaping it.
An addon may not write a protected frame's attributes during combat, so the insecure PreClick that chose and committed the wedge was blocked as soon as a fight started. Move the choosing into a secure snippet wrapped around OnClick: Lua pushes the ring's actions and the layout geometry onto the button out of combat, and the snippet reads the cursor and picks the entry at release. The mouse wheel moves into a snippet too, for the same reason, and the fan's index is read back out of the catcher's attributes. Also stop spell cooldowns reading secret values: C_Spell.GetSpellCooldown is secret when cooldowns are restricted, so use GetSpellCooldownDuration and feed the duration object straight to the widget. Add "/euiap trace" to report what the snippet decided on the last release, since the restricted environment has no way to print.
FAN_H and FAN_V differed only in which axis the strip ran along: every setting, decay, floor and input model was shared. Replace them with a single FAN layout plus a fanOrientation key, read through one WheelView:FanHoriz predicate. The options page loses a layout entry and gains a Fan Direction dropdown, which is where orientation belongs -- it describes the strip, not a different way to steer one. Saved profiles holding FAN_H or FAN_V convert on load.
Two things the strip got wrong. It opened selecting nothing, with the first wheel tick entering it -- so the entry it opened centred on was the one entry that could not be chosen without scrolling off it and back. The press now seeds the accumulator at 1, and every tick is a plain step from there. That leaves the strip with no cancel, since releasing without a tick was the old one. Take the grid's gesture instead: carry the pointer clear of the strip -- across its axis, more than 1.5 pitches from where the pointer was when the palette opened -- and nothing is selected, so nothing fires. The live view and the release snippet apply the same rule in the same units, so a strip drawing no selection never casts. Unattended closes now clear the accumulator as well as hiding the catcher, or a key-up arriving after one would fire the seeded entry with nothing on screen.
Cancelling only counted movement ACROSS the strip, on the reasoning that travel along its axis reads as steering. That is not how leaving a thing feels: the strip is a shape on the screen, and carrying the pointer off its end is as clearly a departure as carrying it off its side. The cancel is now a box -- a margin across, the drawn strip plus that margin along -- and the strip's half-length comes from one method the frame sizing shares, so the box can never disagree with what is drawn.
The strip now dims as the pointer is carried toward its cancel box, so leaving is something the player watches happen instead of a boundary they cross blind. It holds full brightness through the first third of the travel and eases down to a floor over the rest -- never to nothing, because bringing the pointer back re-selects what the strip is centred on. The box itself is wider than it was: at 1.5 pitches an ordinary scroll gesture could brush the edge of it. Escape now cancels an open palette in every layout, in combat too. It cannot be a key handler: the release is resolved inside a secure snippet, and only secure code may leave that snippet a flag to read once the player is fighting. So the press snippet binds Escape to a button whose own snippet raises the flag, the release reads it before it steers anything, and the binding goes back to the game menu on the way out -- including after an unattended close, which is the one path that never sees a release.
The module stopped being a wheel some time ago, so the vocabulary goes with it: a set of actions is a PALETTE, the angular layout is the ARC it was generalised into, and an entry is an entry rather than a wedge. The layouts now read Arc, Fan, Grid -- alphabetical, with the default first. Only the names move; RADIAL survives as the binding action name, where renaming it would unbind every palette a user has set. Old profiles convert on first touch rather than once at load, because switching profile repoints the profile table mid-session without a reload and a per-spec profile is resolved after OnInitialize has run. Migrating only the load-time profile would have left both reading a default-seeded empty palette while the user's data sat under the old key. Selecting an entry now grows it in place instead of scaling the widget. SetPoint offsets are read in the widget's own scaled space, so scaling also multiplied the radius the arc anchors at: the icon was thrown outward, out from under the cursor that had selected it, and the two states flickered against each other on the edge of every icon. Size carries the same magnification and moves nothing. The fan and grid magnify their selection too, which they never did -- the strip leaves half that width in every offset past the centre so the zoom cannot close the gaps under its neighbours. The options preview fits both axes of its block and may now grow as well as shrink, capped at 2x. Fitting used to be shrink-only, which left a short strip or a small grid as thumbnails in the middle of a 280px block.
The grid already draws no more columns than it has entries, so a column count at the slot cap is a single row for every palette, and stays one as entries are added. The slider stopped at 8 and could not say it. Take it to MAX_SLOTS instead of inventing a 0 or -1 that would have to mean "row" on a control labelled columns, and name both single-file layouts -- column at 1, row at the top -- in the tooltip.
A slot of kind "palette" names another palette, and pointing through it carries on into that palette's own entries -- so a "Utility" palette can hold Mounts, Teleports and Toys rather than twelve loose actions. The children subdivide their PARENT'S sector rather than taking a fixed span of their own. Nothing runs between the press and the release, so the release has only the final cursor position to go on, and parent regions and child regions must therefore partition the plane between them. Two neighbours each claiming a quarter turn would overlap, and the overlap would be unresolvable. A sector too narrow for its children is answered by pushing them outward, where the same angle buys more room; Nest Width = Overflowing trades that away for a tighter ring, at the price of a long flick through the borrowed angles firing a child rather than the entry it points at. They ride the SAME action table the palette's own entries use, appended past the last of them, so the firing end of the snippet needs no idea that nesting exists -- a child is a cell with a higher index. Only the claim geometry that maps an angle onto one of those indices is new, and it is computed once in ChildGeom and read by the drawing, the hit test and the push alike. Arc only so far. The pointer layouts will nest through the cell list, which already searches a flat list of centres and does not care whether they form a grid. Also here: - palettes past the sixth have no <Binding> entry and exist to be nested, so storage now runs to sixteen while binding stays at six - ns.CanNest refuses a palette inside itself and any chain that would close a loop, which would otherwise send every push and every draw round until the client gave out - the editor draws no nests at all: what a nested entry holds is that palette's own business, and drawing every one at once buried the palette actually being arranged - Nest Distance is a GAP between the two rings' icons, not a radius -- measured centre to centre it had to cover both icons' halves before it separated anything, and the default left them overlapping by 4px
The grid and a pointer-steered fan nest through the cell list rather than through geometry of their own. That branch of the snippet already walks a flat list of cell centres and picks the nearest, and it does not know a grid from anything else -- so the nested entries are appended to that same list and the snippet gains no branch at all, only a longer loop. Outside the perimeter is the only space available. Every interior cell already belongs to an entry, and a release is settled by which centre is nearest, so a nest sharing space with an entry could not be resolved. It is also the space these layouts already treat as their cancel, so hosting a nest there costs nothing that was doing anything else. The side is the one NEAREST the parent's cell, in cells, so nothing about it depends on where the palette sits on the screen -- which matters because the push runs long before the open that uses it, and the two have to agree. Nest Side breaks the tie, and a strip is nothing but ties: one entry deep, so both long sides are equally near. Claims are allocated greedily, nearest side first, stepping to the next side or the next rank out rather than ever overlapping -- a point inside two nests at once would have no answer. A nest on a grid's INTERIOR cell is therefore served by a row that does not touch it. That is the honest cost of the rule; the answer is to keep nested entries on a grid's edge rather than to bend the geometry. Scroll-steered fans still do not nest: their entries are a cycling window rather than fixed positions, and the cursor axis a nest would use is the one that cancels. Every nest control is dead there rather than lying. nestBand/nestScale lose their arc prefix, being shared now.
A nested cell was a cell like any other: the release picked whichever
cell centre was nearest, and nested ones sat in the same search as the
palette's own. That made a nest reachable from open space half a screen
away, gave every nest its own row stacked outward from the last, and left
a grid's middle entry served by a row that did not touch it.
Every nested cell now owns a BOX. Inside it, that child; outside every
box, the layout answers as though the nest were not there. Leaving a run
in any direction leaves the nest, which is what a nest should mean, and
boxes may sit over ground the block is using -- which is what the two new
styles need and a nearest-centre rule could never allow.
On a grid, three of them:
Lane one lane just outside the block, SHARED by every nest on that
side. Runs are packed along the perimeter as a single circular
coordinate, so two nests sit side by side instead of stacking
outward, and a run longer than the edge it started on turns the
corner rather than shooting off into space. The corners are
rounded: around a square one the straight-line distance between
two evenly spaced cells is short enough that the icons overlap.
Halo the eight positions around the parent's own cell, the block
faded and shrunk behind them. Drawn tight enough that the
neighbours keep their centres.
Popout the nested palette as a block of its own, set down outside on
the side its parent leans toward, the parent block faded.
A strip ignores the setting: one entry deep, it has only ever the one
answer, which is to break out across itself.
The scroll-steered strip nests too, where before it was the one layout
that did not -- and the default, so choosing the Fan layout got the half
that could not. The wheel says WHICH nest, the cursor only which of its
children, and travel toward a nest no longer counts as leaving the strip:
its children sit past the cancel margin, so measuring them by it closed
the palette on the way to reaching them.
The hub caption hangs opposite the nest and swaps with Nest Side, rather
than sitting on top of it.
Boxes are tested in cell order and the first hit wins, so two that
overlap still have exactly one answer. Disjointness was never the
requirement -- the drawing and the snippet agreeing is, and they walk the
same list in the same order.
A nest was shifted sideways to make room for its neighbour, so a lane with two of them put neither above its own entry. But a nest is reached by going THROUGH its parent: anywhere else to put it is somewhere the user did not aim. So a nest is now always centred on its parent, and what gives instead when two would collide is the number of cells per row. Each takes the lane out to the halfway point with the nearest other nest, and wraps to a second row further out when that is narrower than its run -- a crowded nest becomes a compact block above its own entry rather than a long row shoved off to one side. Rows past the first follow the outward normal, so they stay square on a straight edge and fan around a corner. Nests set down clear of the block -- Popout always, Lane whenever its parent is not on the edge -- had ground in front of them belonging to the block's own entries, and closing on the first step across it put them out of reach of the very gesture that opens them. Each claim now carries a corridor spanning its parent and its children, and stays drawn while the pointer is anywhere inside it. Visibility only: the entries under a corridor fire exactly as they did. The halo ring goes out to two thirds of a pitch, the furthest it can go while leaving the neighbouring entries their own centres, and its parent draws back to six tenths so the ring has somewhere to be. It keeps its full colour -- it is what the ring is about. Nested entries in a halo or on a strip go unlabelled, as the strip's own entries already do: at that spacing the captions of neighbouring icons land on top of each other.
A nest's cells were live from the moment the palette opened, so two nests could fight over ground they both claimed, and a popout was easier to enter than to leave. Selection now requires the cursor to have entered the parent entry first: secure OnEnter/OnLeave gate frames keep one armed claim on the button, the release fires only that claim's cells, and leaving the claim's true ground -- parent, children and the corridor between them, not their bounding box -- disarms and lets the block back out of its dim. Arming is exclusive: switching nests means backing out of one before entering the other. Arc children now hug the ring in concentric rows instead of riding a narrow sector out; the lane splits by palette shape, a compact block over the parent for a single row or column and the perimeter run for a grid; nest captions move to the hub, which already says where the cursor is; the layout list reads Grid, Fan, Arc. /euiap gates prints the armed claim and the gate transcript for the last hold.
Arming no longer waits for an OnEnter edge: the press snippet and the disarm path both test the cursor against the claim geometry directly, so a gate shown under a parked cursor is live at once. Region rects grow an outward grace band so a fast reach that overshoots a child does not drop the nest. Unarmed hover now draws a dim preview instead of a full open, and an armed parent keeps a pale border while the selection is on a child. The lane style is rebuilt as a snug run that hugs the block perimeter, centered on the point nearest its parent, wrapping corners; each side's region is the bounding box of the run plus the parent cell, so a straight reach cannot leave coverage.
Checks now cover press-time pre-arm, leave-path re-arm, grace probes, lane placement against annotated expectations, and straight-line walks to every lane child through the real gate plumbing.
…rc's children Grid: a lane run may use the full perimeter before it stacks a second row, so two nests in adjacent cells no longer collapse into clusters; every other claim's parent cell is carved out of a claim's region coverage, so a glide from one nest's entry straight onto its neighbor's disarms the first and arms the second in place. Arc: the disarm test's ground is now a beam plus a wedge centered on the parent, which closes the dead ring between the parent's icon and the first ring of children that disarmed every reach the moment it left the icon; a claim's children spread along one ring up to Max Nest Span before they spill into a second ring. Options: slot labels now ship off and the center logo on; a palette can be named, and given an icon that its nested entry and the picker both show -- unset keeps today's look.
An ARC section drives the real snippets through press pre-arm, a reach to every child, and a sideways exit; the block checks cover nest-to-nest handoff across a carved parent cell and full-lane runs.
Two slot kinds join the model: raidtarget and worldmarker, stored by icon position with 0 for the entry that clears. Both resolve to macrotext -- /tm, /wm through the engine-order map, /cwm -- so the secure snippet needs no changes: it already clears the macro keys and writes one key per cell. The picker offers them in a Markers category that keeps the star-to-skull order every marker menu in the game shows, through a new per-category keepOrder flag. The Number of Palettes slider is replaced by an Add Palette button. A slider says nothing about what the new palette will hold; the button opens a chooser instead -- an empty palette, or one of the presets: target markers, world markers, hearthstones, teleports, potions, druid forms. Each preset builds from what the character knows or carries when the chooser opens, and a preset with nothing to offer stays out of the list, which is also how the class-specific ones gate themselves. With the slider gone, a Delete Palette button takes over the other direction, and really deletes rather than hiding the way the slider's decrease did: the palettes above close ranks, nested entries repoint or leave with the deleted palette, and each shifted palette's keybind moves down with it.
de14176 to
68d07b7
Compare
|
Update: markers, preset palettes, and add/delete palettes ( Markers in the action picker. A new Markers category offers the eight target markers, the eight world markers, and a clear entry for each set, in the star-to-skull order the game's own marker menus use. They are two new slot kinds that resolve to Preset palettes. The Add Palette chooser offers presets beside the default empty palette: Target Markers, World Markers, Hearthstones, Teleports, Potions, and Druid Forms. Each preset builds from what the character knows or carries at the moment the chooser opens, and a preset with nothing to offer stays out of the list — which is also how the class-specific ones gate themselves. Add and delete instead of the count slider. The Number of Palettes slider is gone. Add Palette opens the chooser above and points the editor at the new palette. Delete Palette (confirmed by a popup) really removes the palette rather than hiding it the way the slider's decrease did: the palettes above close ranks, nested entries repoint or leave with it, and each shifted palette's keybind moves down with it, both keys if two are bound. Deletion is blocked in combat because it touches bindings. Verified offline (syntax, a fresh-context review pass, and the nest agreement harness still reports ALL AGREE) but not yet live-tested in-game. The first things to try live: marking while grouped (both sets, including the clears, in combat), the chooser anchoring under the Add Palette button, and deleting a middle palette with keys bound on either side of it. |
The rename moved the module's home in the central store from addons.EllesmereUIRadialWheel to addons.EllesmereUIActionPalette, but nothing moved the data: the old migration renamed a child SV global that NewDB wipes without reading, so configured rings vanished and the orphan blob rode along in every profile. MigrateLegacySV now walks the central profiles and moves the old key to the new one (current data wins, old key always removed); P()/MigrateNames converts the field names on first touch. Also document the profile contract above DB_DEFAULTS: everything under profile is per-profile and rides export/import through ADDON_DB_MAP, so a new setting needs only a default there. Correct the toc comment whose wrong claim about the legacy global produced the stranding.
No slash command clears every target marker the way /cwm clears the world ones, so the new clearmarkers slot kind takes the battlepet route: no secure action type, fired from PostClick off the Lua-side selection. SetRaidTarget is not protected, so the sweep is plain Lua over every unit the client can name -- the group and its pets, visible nameplates, target and focus. A marked mob nobody can reference keeps its mark, the limit every clear-all macro shares. The row sits directly after the single-target clear it complements, so a palette can carry either or both.
The arc drew every entry at full size and full strength, so the only thing saying where the cursor was is the selection border -- while the grid and both strips have always drawn back from the entry under the pointer. AdvanceArc gives the ring the same cue, measuring nearness as the shortest angle counted in STEPS: every entry on a ring is the same distance out, so a radial measure would say nothing, and steps are what make one falloff setting mean the same thing in every layout. Positions never move -- an entry that slid as it grew would drag itself out from under the cursor that had just reached it. PointerPolar comes out of HitTest so the falloff and the hit test read one copy of the cursor arithmetic: what the arc draws as nearest and what a release fires cannot part company. Both falloffs now sit behind a single Proximity Falloff toggle, on by default, read through FalloffRatios. Switched off it answers 1, 1 -- a no-op wherever it lands, since decay ^ k is 1 at every k and FanOffset's even-spacing branch takes the strip out to full pitch -- so no layout can be left drawing a cue the others have dropped. The two ratio sliders were gated to Fan and Grid; they describe all three layouts now and are gated by the toggle instead.
Two bugs, both older than the falloff work. The strip's settled entry shifted a moment after the rest of it had stopped, against the direction of travel. Every offset past the centre carries half the width the selected entry gains, so magnifying it cannot close the gaps under its neighbours -- but it was switched on the instant k left 0, and the strip approaches its entry continuously. The centre entry therefore sat that half- width out for the whole slide and dropped back only as k reached exactly 0, which is the twitch. Ramped in over the first step instead. At every integer k the ramp is already at full extra, so the settled strip is unchanged. Clear All Target Markers raised ADDON_ACTION_FORBIDDEN on its first call and cleared nothing. It swept the group, the nameplates, target and focus with SetRaidTarget from plain Lua, on the belief that the function is unprotected. It is not -- it is documented AllowedWhenUntainted and refuses any call an addon makes from its own code. The sweep goes through the secure button now, as SECURE_ACTIONS.raidtarget's clear-all branch, which calls RemoveRaidTargets untainted. The action rides in the ordinary key/value slot the snippet already pushes: clear-all is the one branch that never reads "marker", so the single key per cell can be spent on "action" instead. That key is cleared with the rest before each fire, or a sweep would leave the next raidtarget slot clearing the whole group instead of marking one unit. It also clears more than the loop could: RemoveRaidTargets reaches marks on units the client cannot currently name, which no clear-all macro can. The /tm branch, which does work, had the same wrong premise written above it; what makes it legal is the secure button running the macro, and it now says so.
Nearness was measured to an entry's CENTRE, so the entry the cursor was on breathed as the cursor crossed it: largest dead in the middle, drawing back toward the edges. The one thing on screen that should hold still while you settle on it was the one thing moving. FalloffK flattens the step count over the entry's own ground -- half a step each side, which is exactly what the hit tests hand an entry -- so the entry drawn at full size and full alpha is precisely the entry a release would fire. Past a whole step it is the identity, so nothing about the settled drawing moves: a neighbour is still decay ^ 1, a grid diagonal still decay ^ sqrt 2. Only the half-step band between an entry's edge and its neighbour's centre is redrawn, at twice the rate. The grid flattens each axis before combining them rather than flattening the 2D distance: a flat disc would leave the corners of a cell outside it, still breathing, and would pull the diagonal neighbour in off sqrt 2. The strip needs none of this -- its entries come to rest at whole steps, where the curve is the identity -- and its spacing integral is left alone.
…ed for Four costs, all of them borne by users who nest nothing, open nothing, or have the module switched off. None of it changes what anyone sees. A push writes on the order of a thousand attributes per bound palette, and two of its loops walked all twelve possible claim slots whether or not the palette nested anything -- which most do not. Both are bounded by a per-button high-water mark now, held at zero for a palette that has never nested. The mark is MONOTONIC, and that is the whole of its correctness: every snippet that clears, hides or re-shows a gate walks 1..eapGateMax, so an index ever pushed a box for has to stay inside the bound for the rest of the session, where the loops go on nil'ing it exactly as they did at twelve. Lowering it to today's count would strand a live gate at yesterday's rect with nothing left to clear it. The gates themselves were built twelve claims deep at login -- 120 frames and 240 wrapped scripts per bound palette -- for nests that mostly do not exist. The pool grows on demand to the same mark and never shrinks. _EAP_Apply reached that push on every slider tick, and the options panel drags one per frame while the sandbox only ever reads the last. Refresh requests a push now and a short window folds the drag into one, matching the coalescer shape the suite already uses. The preview still tracks the slider: only the pushed geometry waits. A press lands any pending push before it opens, so a key can never fire geometry the palette has stopped drawing, nor carry stale actions into a fight that starts inside the window. The combat-end handler pushes only when a fight actually refused one, the same bargain bindingsDirty already made beside it. OnEnable built the live view and a full-screen secure frame whether or not the feature was on. Nothing is built there now: the view is made by the first push, which only runs for a palette that has a secure button, and bindings build no buttons and no binding owner while the module is off. A session that never enables it creates no frames at all. Held open, the steering pass rewrote every entry's point, size and alpha every frame, though all three passes are pure functions of the cursor, the armed claim, and where the wheel left the strip. A frame bringing none of those in new skips the pass. Not the alpha -- the flick-ahead and cancel fades are time-based and still run every frame. Closing the strip clears fanVisual with fanTarget: left behind, it reads as a settle that can never finish, and one scroll-fan open would cost every later open its skip for the session. The offline agreement harness needed a C_Timer to drive the deferred push with; it fires inline, which leaves Refresh as synchronous as the sweeps need. Its verdict is unchanged, sweep for sweep.
The palette could hold any one mount but not the button every player actually presses. SummonByID(0) is protected, so the entry casts the spell behind the Journal's own Summon Random Favorite Mount button instead, by name, the same route the mount kind already takes. Pinning is a property of the entry rather than of the category, so a list that is otherwise alphabetical can still lead with the row the user came for.
A palette of potions could not say which stack was nearly out, and a palette of charge spells could not say how many were banked -- the one number an action button has always carried. The charge count is a secret whenever cooldowns are restricted, so SlotCount answers whether there is a count separately from what it is: the value only ever reaches SetText, and nothing tests it, not even for nil. The count is shown and hidden rather than written and cleared, because a font string carrying secret text refuses a tainted clear and would leave the previous entry's number standing.
An entry the game would refuse looked exactly like one it would cast. Spells, items and toys now carry the three cues an action button has always given: red out of range, blue short of the resource, gray and desaturated for anything else. Every getter this reads was checked against the generated documentation first -- IsSpellUsable, IsSpellInRange, IsUsableItem, ItemHasRange and IsItemInRange carry no SecretWhenCooldownsRestricted flag, unlike the cooldown and charge getters beside them, so these results may be branched on. Mounts and macros stay untinted: a mount's summon spell is outside the spellbook and reports every mount unusable, and a macro's usability is whatever its body resolves to. The tint multiplies the selection paint rather than replacing it, so an out-of-range entry under the cursor still reads as the selected one.
Releasing on an entry that opens a palette has always fired nothing -- the snippet refuses on eapPal before it reads any action, and the close runs as normal -- but on screen that entry was captioned in the selection color like every other one, so the cancel read as the nested palette being broken rather than as an answer. The hub hint line says it instead. It takes over the line the keybind sits on: nothing about the keybind matters while the cursor is standing on a door, and it is handed straight back when the selection moves.
Three presets that were missing, and the kind one of them needed. Specializations are a new slot kind. There is no secure action type for a spec change and no slash command either, so it fires from PostClick alongside the battle pet -- and that path had to be made honest first: it read the selection the live view was drawing, which is not what the release fires. Escaping out of an open palette leaves an entry selected and fires nothing, and the pet was summoned anyway. It now reads the cell the snippet actually resolved, off the button, and only when the snippet says it chose one. A spec slot banks the specID rather than the index, so a palette carried to an alt points at nothing instead of at somebody else's spec. Stances covers a warrior's three and a paladin's four auras: the same one-of-a-set choice in two costumes. Druid Forms grows Prowl, the two travel forms Travel Form has mostly absorbed, and Cancel Form -- getting out is the half of shapeshifting no form spell covers.
A profile held one layout, so a grid of markers and an arc of cooldowns could not both exist -- and the palettes most worth having are the ones that want different shapes. The settings that describe a palette's SHAPE and PLACE are now per palette: layout and fan direction, where it opens and at what scale, and every knob that only means anything inside one of the three layouts. Everything describing the module's LOOK -- colors, hub art, labels, nesting geometry, flick-ahead -- stays profile-wide, because a suite of palettes that each looked different would read as several addons. Every one is an override rather than a value, so there is nothing to migrate: a palette that has never been given one reads the profile's, which is exactly what the old flat keys already held. Overrides live under palette.appearance rather than flat on the palette, which already carries name, icon and slots. The renderer still reads `p.layout`. What changed is what `p` is: a read-only view with the palette's overrides in front of the profile, handed out by PA and reached through PaletteView:P(). One metatable instead of a fallback at sixty call sites, none of which could have been left out safely. PushPalette measures palettes the view is not laid out for, so it points that accessor at the palette it is pushing for the length of the push. Copy Appearance takes another palette's whole arrangement in one go -- entries untouched. It copies the override table rather than the values it resolves to, so a setting the source inherits stays inherited here as well and both still follow the profile if it is ever changed. Every row that writes an override carries noCapture: these live inside p.palettes[n], and a per-spec override banked against a flat key would rewrite whichever palette happened to be on screen at capture time. The nest harness grows two sweeps that set the profile to one layout and the palette to another. A read that went to the profile on either side would draw one layout and fire from the geometry of a different one, which no existing sweep could produce.
PushPalette measures through the live view, which is laid out for whatever was drawn last and never for the palette being pushed. Two lines make that safe -- appIndex, and reading PA(index) rather than the profile -- and neither had a test: every sweep drives palette 1, where the view's own index already equals the pushed one and both are no-ops. So this fixture pushes two palettes with different appearances at once and holds palette 2's pushed geometry against palette 2's drawn geometry, with the profile set to a third answer so a read that fell back to it is wrong rather than accidentally right. Removing either line now fails it, as does making the view's accessor ignore appIndex; the first version of this fixture caught none of them, because the two palettes did not differ in anything the view measures. Three comments corrected alongside: ResolveAction returns nothing for two kinds now, not one; the appearance views are not bounded by MAX_PALETTES, since a deleted palette's table stays in them; and the charge flag is one line further down than cited.
|
Status: a bug-hunt pass over the module found eight bugs. All are fixed in the nine commits ending at 12daf91. Fixes
Verification so far
Known limits: while a claim is armed, the floor takes hover (not clicks) from the rest of the screen for that hold. The drawn-versus-pushed divergence after an in-combat edit (a refused push) is not fixed; any cheap guard would also suppress correct in-combat fires. |
A world marker entry maps the icon position it stores onto the number the engine uses. That map was Blizzard's WORLD_RAID_MARKER_ORDER copied whole, but the constant lists its eight numbers from SKULL to STAR -- the order its own dropdown draws them in. Read as though it ran star to skull, it gives the marker mirrored about the middle: the star placed the skull, the orange circle placed the red cross, the purple diamond placed the blue square. Reverse the map. Target markers were never affected, because they take the icon position directly. Every palette now has a <Binding> entry of its own, so no palette is reachable only by being nested in another one. MAX_BOUND_PALETTES and the "Nested Only" state it produced are gone. A secure button is built only for a palette that HAS a key, so a profile that binds two of its sixteen pays for two, and an unbound palette costs no attribute writes. Add two cycling marker entries, one per set. Each press places the next marker, star to skull and back to the star -- the whole set in one slot, for a palette with no room for nine. The position advances inside the secure snippet, because an insecure SetAttribute is refused in combat, which is the whole of when marking matters. The release hands the snippet's answer back to the slot, so the icon always draws the marker the next press places. Offered in the picker only, not in the presets. Ask before a keybind takes a key from another action. SetBinding steals one silently, and the displaced binding is often something the user does not miss until they reach for it mid-fight. Split the Markers picker into Target Markers and World Markers. Twenty one rows in one list is more than the eye wants, and which set you are after is decided before the menu opens. A category can now carry a sub-list, and the Back row walks one level at a time. This also carries the settings page overhaul: the page is cut to the rows most users need, and the rest move into four cog popups.
f542d44 — world marker order, per-palette keys, cycling markersWorld markers placed the wrong marker. Every palette can take a key. Cycling marker entries. Two new kinds, Keybind overwrite now asks. Markers picker split. Also carries the settings page overhaul from the previous batch: the page is cut to the rows most users need, with the rest in four cog popups. Verification
With the fix in place the harness reports ALL AGREE. Still to confirm in game
The picker navigation has no offline coverage — the harness does not load the options file. |
What this is
A new child addon,
EllesmereUIActionPalette. You hold a binding, steer to an action, and release to use it. The release resolves inside a secure snippet, so the palette works in combat.Four steering models:
An entry can open another palette (one level of nesting). Grid nests come in three styles: a lane along the palette's perimeter, a halo around the entry, or a popout block beside the palette.
Status: early draft, for eyes only
Nested palettes are still broken in-game. The current design gates a nest behind secure OnEnter/OnLeave frames, and the latest round of fixes has not yet been validated live. Everything else on the list below works in-game today:
Known open items besides nesting: unlock-mode placement is stand-in X/Y sliders, and the override bindings are not yet scoped.
Feedback on the interaction model and the options layout is what we are after at this stage.