Skip to content

Custom Dynamic Group

Allen Faure edited this page Jan 19, 2023 · 14 revisions

Dynamic Group Custom Functions

WeakAuras 2.12 introduces the ability to write custom functions for a dynamic group's Grow and Sort options, which will allow you to define what your group should look like very precisely.

WeakAuras 2.14 extended it to group and anchor regions by frames.

aura_env

In a dynamic group, aura_env has a slightly different meaning. There is no state information, so aura_env.state is always nil. Additionally, there is no such thing as a cloned group, so aura_env.cloneId is always nil. There is a new value which is provided to you, which can be accessed at aura_env.child_envs. This is a table which contains all of the aura_env tables of the group's children, in dataIndex order.

RegionData

Both custom sort and custom grow will give you some special tables, which are called regionData (for the simple reason that they contain the region and the data). RegionData is structured as follows:

regionData = {
    id = "string", -- name of the child,
    cloneId = value, -- the cloneId. Usually string, but can be any value, including nil. If this child does not clone, then this value will be nil or empty string "".
    dataIndex = 1, -- number which indicates the order the child appears in the options Pane, starting at 1.
    data = {...}, -- table containing data with which to construct the child's aura
    region = {...}, -- the region object which was built from the data for this child.
    xOffset = 0, -- x offset from previous layout function execution. This may be nil on the first layout.
    yOffset = 0, -- y offset from previous layout function execution. This may be nil on the first layout.
    show = true, -- Boolean indicating if the region was allowed to be shown (note that this is not the same as a region being active)
}

Note: regionData objects are intended to be read only, but for performance reasons this is not enforced. Writing to the regionData object will result in undefined behavior.

Custom Sort

Custom Sort is the more straightforward of the two new functions. You are given two regionData objects, and are expected to return true if the second must be sorted before the first:

function(a, b)
    -- this is roughly equivalent to the "None" sort option
    return a.dataIndex <= b.dataIndex
end

Compositional sorting

The underlying code for the builtin sort options uses a compositional system, which can be repurposed for custom code if you wish. In most cases this will lead to code that is much easier to write and maintain, particularly when the values which you are sorting on don’t necessarily exist.

The code of compositional sorting relies on a few concepts:

  • comparator
    • A function which accepts two parameters, and returns true, false, or nil. A result of true indicates that the arguments must be swapped, a result of false indicates that the arguments must not be swapped. A result of nil indicates an indeterminate result, either because the value are equal, or the values cannot be compared using this comparator. Note that the custom sort function itself is a kind of comparator, and WeakAuras will interpret a nil result in this case as being equivalent to false.
    • A comparator can compare any two values, not just regionData objects. This is useful to simplify comparing values deep in the regionData objects, see the documentation on SortRegionData for more information.
  • path
    • A list-like table consisting of strings, used as a locator for values in a regionData object. For example, to access regionData.region.state.index, the appropriate path is {"region", "state", "index"}

... and is supported by a few API functions:

  • WeakAuras.ComposeSorts(...)
    • takes as arguments a variadic list of comparator functions, and returns a new comparator which composes them in the order they were passed in.
    • This new comparator operates the following loop, which produces the effect that earlier comparators have higher "priority":
      1. Get first comparator from variadic list
      2. Run sub-comparator, and check result.
      3. If nil, repeat step 2 with next comparator from variadic list.
      4. If not nil, return result.
  • WeakAuras.SortRegionData(path, valueComparator)
    • takes as first argument a path to retrieve values with from the region data, and as second argument a comparator to sort said values with, and returns a comparator suitable for use as a custom sort function. This helper helps you abstracts away the tedium of accessing necessary information from the regionData, which is a common pain point with traditional sort functions.
  • WeakAuras.InvertSort(comparator)
    • takes as argument a single comparator, and returns a new comparator which has precisely the opposite behavior. Specifically, the new comparator is true exactly when the old one is false, false when it is true, and nil when it is nil.
  • WeakAuras.SortNilFirst
    • A builtin comparator which stably moves nil values first. Returns nil if and only if both parameters are not nil. For this reason, SortNilFirst is useful to place first in a sort composition, in order to guarantee that lower priority comparators receive well-formed data.
  • WeakAuras.SortNilLast
    • Inverted version of WeakAuras.SortNilFirst.
  • WeakAuras.SortGreaterLast
    • A builtin comparator which is effectively the "<" operator.
  • WeakAuras.SortGreaterFirst
    • Inverted Version of WeakAuras.SortGreaterLast
  • WeakAuras.SortAscending(path)
    • Alias for WeakAuras.SortRegionData(path, WeakAuras.ComposeSorts(WeakAuras.SortNilFirst, WeakAuras.SortGreaterLast))
  • WeakAuras.SortDescending(path)
    • Inverted Version of WeakAuras.SortAscending(path).

Here are a few examples:

sort by stacks, descending
WeakAuras.SortDescending {"region", "state", "stacks"}
sort by stacks ascending, then by expiration descending
WeakAuras.ComposeSorts(
  WeakAuras.SortAscending{"region", "state", "stacks"},
  WeakAuras.SortDescending{"region", "state", "expirationTime"}
)
sort by stacks descending, but put “0” stacks at the top
WeakAuras.SortRegionData(
  {"region", "state", "stacks"},
  WeakAuras.ComposeSorts(
    WeakAuras.SortNilFirst,
    function(a, b)
      if a ~= 0 and b ~= 0 then
        -- both belong in the "nonzero" section, return nil to let next comparator run
        return nil
      elseif a == 0 then
        -- a belongs in the "zero" section and is already fist, so don't swap to avoid creating an unstable sort
        return false
      else
        -- b == 0 and a ~= 0, a swap is needed
        return true
      end
    end,
    WeakAuras.SortGreaterFirst
  )
)

Custom Grow

Custom Grow is slightly more involved, but still straightforward. You are given 2 parameters. The first is an empty table which is expected to be filled with positioning data, and the second's is a table containing the regionData of all active children, in sorted order (note that positioning always occurs after sorting). You are not expected to return anything, and anything you do return from this function are ignored.

function(newPositions, activeRegions)
    -- this function will produce a parabola shape
    local mid = #activeRegions / 2
    for i = 1, #activeRegions do
        newPositions[i] = {
            40 * (i - mid),
            0.5 * (i - mid)^2
        }
    end
end

If a child is not given any position data, then it is hidden, and moved to position 0, 0. You may hide a child at a particular spot other than 0, 0 (useful if you use the animated expand/collapse option) by setting the third value in the position data to false.

Since WeakAuras 2.14 Custom Grow function can also be used to group and anchor children per frame

function(newPositions, activeRegions)
    -- make a list of regionData for each frame
    local frames = {}
    for _, regionData in ipairs(activeRegions) do
        local unit = regionData.region.state and regionData.region.state.unit
        if unit then
            local frame = C_NamePlate.GetNamePlateForUnit(unit)
            if frame then
                frames[frame] = frames[frame] or {}
                tinsert(frames[frame], regionData)
            end
        end
    end
    for frame, regionsData in pairs(frames) do
        local totalWidth = #regionsData - 1
        for _, regionData in ipairs(regionsData) do
            totalWidth = totalWidth + (regionData.data.width or regionData.region.width)
        end
        local x, y = - totalWidth/2, - (#regionsData - 1)/2
        newPositions[frame] = {}
        for i, regionData in ipairs(regionsData) do
            x = x + (regionData.data.width or regionData.region.width) / 2
            newPositions[frame][regionData] = { x, y }
            x = x + (regionData.data.width or regionData.region.width) / 2
        end
    end
end

Lazy Evaluation

With WeakAuras 5.3.6 as a measure to improve performance, dynamic groups are now required to declare which parts of state they depend on to function correctly. You will see this as a new input box above a custom grow/sort codebox, labeled "Run On...". Write into this box a comma-separated list of every state key your custom layout consumes.

For example, if you have a custom sort like this:

WeakAuras.SortAscending{"region", "state", "myKey"}

then you should add myKey to the "Run On..." box in order to guarantee your sorting continues to function correctly. As an escape hatch, the special key changed acts as a wildcard and causes WeakAuras to always trigger your custom sort & grow functions. We recommend you avoid this if possible, however, since it eliminates any opportunity for WeakAuras to optimize its performance.

Group by Frame

The Group by Frame option use pre-built growers functions to group and anchor regions to frames. Nameplates to nameplates found from state.unit. Unit Frames to unit frames found from state.unit. Custom Frames give you control over which frame each region should be anchored to.

function(frames, activeRegions)
    for _, regionData in ipairs(activeRegions) do
        local unit = regionData.region.state and regionData.region.state.destUnit
        if unit then
            local frame = C_NamePlate.GetNamePlateForUnit(unit)
            if frame then
                frames[frame] = frames[frame] or {}
                tinsert(frames[frame], regionData)
            end
        end
    end
end
Clone this wiki locally