• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] UI - FrameFlow

FrameFlow itself is no Frame, it is a resource that manages the positions of Frames on a Frame, if they are added over FrameFlow. Frames using and being added by FrameFlow need a Size and this Size is respected. Therefore Frames that don't fit into the Frame, managed by FrameFlow, anymore will be hidden. One can change the starting Frame to show such later Frames, FrameFlow can add a slider when wanted. This Resource does not have a custom TOC or fdf.

FrameFlow works like in this Image:
Structur.jpg
1, 2, and 3 fit in one row. The 4. Frame would be to big to be in the same row hence it starts a new row but it respects the size of 2 and 3 which take more ySize then 1 hence there is a gap between 1 and 4.
7 is added after 6. Frame 7 looks like it could fit somewhere else but FrameFlow doesn't do such things. It adds new Frames after the previous one as said above or it starts a new row when that is needed.

The 6.Frame won't be displayed cause he would take space outside of the ParentFrame. But 7 would still be next to that hidden Frame resulting into be a single Frame in the center.

System Code
Lua:
--[[
    FrameFlow V0.9b by Tasyen
    FrameFlow is a system that automatic fills a Frame from TopLeft with frames. Further frame try to be placed next to the previous one, if that fails FrameFlow starts a new row.
    This new added Frame is the previous for the next frame regardless if it started a new Row or not.
    The Frame using FrameFlow and the frames added need a size for this system to work.
 

    function FrameFlow.create(frame[, addSlider, marginX, marginY])
        creates a new FrameFlow for frame returns frameFlowTable.
        Having marginX space between 2 frames in 1 row and marginY space between 2 rows
        addSlider(true) creats a slider so the user can alter the frame shown at topLeft with that moves all Content.
        The slider will take over the size of the frame on creation, when the size changes you or was not right on that moment use FrameFlow.updateSlider.

    function FrameFlow.add(frameFlowTable, frame[, noUpdate])
        adds frame to the frameFlowTable. noUpdate (true) does not fit the new Frame into the Frame managed by FrameFlow.
        frame becomes a children of the Frame managed by frameFlowTable

    function FrameFlow.remove(frameFlowTable, [frame, noUpdate])
        removes frame (can be a number) from the frameFlowTable.
        calling this will also call FrameFlow.fit(frameFlowTable, 1).
        noUpdate (true) prevents calling FrameFlow.fit. Recommented when used multiple times in a row.
        returns true if the frameFlowTable still contains frames

    function FrameFlow.fit(frameFlowTable[, startingIndex])
        update the position and visibility of all content of frameFlowTable.
        starting with Content[startingIndex] at TopLeft.
        Frames with a lower index are hidden.
        no startingIndex is the same as 1.
        Use this function to scroll or after the size of the parentFrame or one of the content changed.
        This is also used after an FrameFlow

    function FrameFlow.updateSlider(frameFlowTable)
        updates the Size of the slider to the current Size of the Frame managed by this frameFlowTable
--]]

FrameFlow = {}
-- creates a new FrameFlow filling frame
function FrameFlow.create(frame, addSlider, marginX, marginY)
    local frameFlowTable = {}
    frameFlowTable.Frame = frame --the frame filled
    frameFlowTable.Content = {} -- array of frames

    if addSlider then
        frameFlowTable.Slider = BlzCreateFrameByType("SLIDER", "FrameFlowSlider", frame, "QuestMainListScrollBar", 0)
    --  frameFlowTable.Slider = BlzCreateFrame("QuestMainListScrollBar", frame, "", 0)
        FrameFlow[frameFlowTable.Slider] = frameFlowTable
        BlzFrameClearAllPoints(frameFlowTable.Slider)
        BlzFrameSetStepSize(frameFlowTable.Slider, 1)
        BlzFrameSetSize(frameFlowTable.Slider, 0.012, BlzFrameGetHeight(frame))
        BlzFrameSetPoint(frameFlowTable.Slider, FRAMEPOINT_RIGHT, frame, FRAMEPOINT_RIGHT, 0, 0)
        BlzFrameSetVisible(frameFlowTable.Slider, true)
        BlzFrameSetMinMaxValue(frameFlowTable.Slider, 1, 1)
        frameFlowTable.SliderTrigger = CreateTrigger()
        frameFlowTable.SliderTriggerAction = TriggerAddAction(frameFlowTable.SliderTrigger, FrameFlow.SliderAction)
        BlzTriggerRegisterFrameEvent(frameFlowTable.SliderTrigger, frameFlowTable.Slider , FRAMEEVENT_SLIDER_VALUE_CHANGED)
        BlzTriggerRegisterFrameEvent(frameFlowTable.SliderTrigger, frameFlowTable.Slider , FRAMEEVENT_MOUSE_WHEEL)
    end
    if not marginX then marginX = 0 end
    frameFlowTable.MarginX = marginX --space between 2 frames in one row
    if not marginY then marginY = 0 end
    frameFlowTable.MarginY = marginY --additional space between 2 rows
    return frameFlowTable
end

function FrameFlow.calc(frameFlowTable, frame, prevFrame)
    local rowSizeX = frameFlowTable.CurrentRowRemainX
    local rowSizeY = frameFlowTable.CurrentRowSizeY
    local offsetY = frameFlowTable.CurrentOffsetY
    local parentframeSizeX = BlzFrameGetWidth(frameFlowTable.Frame)
    if frameFlowTable.Slider then --if there is an Slider reduce the used Space
        parentframeSizeX = parentframeSizeX - BlzFrameGetWidth(frameFlowTable.Slider)
    end
    local parentframeSizeY = BlzFrameGetHeight(frameFlowTable.Frame)
    rowSizeX = rowSizeX - BlzFrameGetWidth(frame) - frameFlowTable.MarginX
    BlzFrameClearAllPoints(frame)
 

    if rowSizeX >= 0 then
        BlzFrameSetPoint(frame, FRAMEPOINT_TOPLEFT, prevFrame, FRAMEPOINT_TOPRIGHT, frameFlowTable.MarginX, 0)
        rowSizeY = math.max( rowSizeY, BlzFrameGetHeight(frame))
    elseif rowSizeX < 0 then
        offsetY = offsetY + rowSizeY + frameFlowTable.MarginY
        rowSizeX = parentframeSizeX - BlzFrameGetWidth(frame)
        BlzFrameSetPoint(frame, FRAMEPOINT_TOPLEFT, frameFlowTable.Frame, FRAMEPOINT_TOPLEFT, 0, -offsetY)
        rowSizeY = BlzFrameGetHeight(frame)
    end
    BlzFrameSetVisible(frame, offsetY + BlzFrameGetHeight(frame) <= parentframeSizeY)
    prevFrame = frame

    --save this values to simple down adding 1 frame
    frameFlowTable.CurrentRowRemainX = rowSizeX
    frameFlowTable.CurrentRowSizeY = rowSizeY
    frameFlowTable.CurrentOffsetY = offsetY
end

--refits all frames, startingIndex is the index of frameFlowTable.Content being placed at TopLeft of the parent Frame
function FrameFlow.fit(frameFlowTable, startingIndex)
    if not frameFlowTable.Content or #frameFlowTable.Content == 0 then return end
    if not startingIndex then startingIndex = 1 end
 
    --hide frames before the starting Index
    for index = 1, startingIndex - 1, 1
    do
        BlzFrameSetVisible(frameFlowTable.Content[index], false)
    end
 
    local frame = frameFlowTable.Content[startingIndex]
    BlzFrameClearAllPoints(frame)
    BlzFrameSetVisible(frame, true)
    BlzFrameSetPoint(frame, FRAMEPOINT_TOPLEFT, frameFlowTable.Frame, FRAMEPOINT_TOPLEFT, 0, 0)
 
    local prevFrame = frame

    --reset values
    frameFlowTable.CurrentRowRemainX = BlzFrameGetWidth(frameFlowTable.Frame) - BlzFrameGetWidth(frame)
    if frameFlowTable.Slider then --if there is an Slider reduce the used Space
        frameFlowTable.CurrentRowRemainX = frameFlowTable.CurrentRowRemainX - BlzFrameGetWidth(frameFlowTable.Slider)
    end
    frameFlowTable.CurrentRowSizeY = BlzFrameGetHeight(frame)
    frameFlowTable.CurrentOffsetY = 0

    for index = startingIndex + 1, #frameFlowTable.Content, 1
    do
        local frame = frameFlowTable.Content[index]
        FrameFlow.calc(frameFlowTable, frame, prevFrame)
 
        prevFrame = frame
    end
end

--fit in a new frame at the end of the table
function FrameFlow.fitNewFrame(frameFlowTable, frame)
    FrameFlow.calc(frameFlowTable, frame, frameFlowTable.Content[#frameFlowTable.Content - 1])
end

function FrameFlow.remove(frameFlowTable, frame, noUpdate)
    local removed = nil
    if not frameFlowTable or #frameFlowTable.Content == 0 then return false end
    if not frame then
        removed = table.remove(frameFlowTable.Content)
    elseif type(frame) == "number" then
        removed = table.remove( frameFlowTable.Content, frame)
    else
        for index, value in ipairs(frameFlowTable.Content)
        do
            if frame == value then
                removed = table.remove(frameFlowTable.Content, index)
                break
            end
        end
    end
    if removed then
        BlzFrameClearAllPoints(removed)
        BlzFrameSetVisible(removed, false)
        BlzFrameSetMinMaxValue(frameFlowTable.Slider, 1, #frameFlowTable.Content)
        if not noUpdate then
            FrameFlow.fit(frameFlowTable, 1)
        end
    end
    return #frameFlowTable.Content
end

function FrameFlow.destroy(frameFlowTable)
    if frameFlowTable.Slider then
        FrameFlow[frameFlowTable.Slider] = nil
        TriggerRemoveAction(frameFlowTable.SliderTrigger, frameFlowTable.SliderTriggerAction)
        DestroyTrigger(frameFlowTable.SliderTrigger)
        BlzDestroyFrame(frameFlowTable.Slider)
        frameFlowTable.Slider = nil
        frameFlowTable.SliderTrigger = nil
        frameFlowTable.SliderTriggerAction = nil
    end
    frameFlowTable.Frame = nil
    frameFlowTable.Content = nil
    frameFlowTable.CurrentRowRemainX = nil
    frameFlowTable.CurrentRowSizeY = nil
    frameFlowTable.CurrentOffsetY = nil
end

function FrameFlow.add(frameFlowTable, frame, noUpdate)
    table.insert(frameFlowTable.Content, frame)
    BlzFrameSetParent(frame, frameFlowTable.Frame)
    BlzFrameSetMinMaxValue(frameFlowTable.Slider, 1, #frameFlowTable.Content)
    if not noUpdate then
        if #frameFlowTable.Content == 1 then
            FrameFlow.fit(frameFlowTable, 1)
        else
            FrameFlow.fitNewFrame(frameFlowTable, frame)
        end
    end
end

function FrameFlow.updateSlider(frameFlowTable)
    if frameFlowTable.Slider then
        BlzFrameSetSize(frameFlowTable.Slider, 0.012, BlzFrameGetHeight(frameFlowTable.Frame))
    end
end

function FrameFlow.SliderAction()
    local frame = BlzGetTriggerFrame()
    if GetLocalPlayer() == GetTriggerPlayer() then
        if BlzGetTriggerFrameEvent() == FRAMEEVENT_MOUSE_WHEEL then
            if BlzGetTriggerFrameValue() > 0 then
                BlzFrameSetValue(frame, BlzFrameGetValue(frame) + 1)
            else
                BlzFrameSetValue(frame, BlzFrameGetValue(frame) - 1)
            end
        end
        FrameFlow.fit(FrameFlow[frame],  BlzFrameGetValue(frame))
    end
end

Example
Lua:
do
    local real = MarkGameStarted
  function MarkGameStarted()
        real()
    xpcall(function()
    local gameUI = BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0)
    local parentFrame = BlzCreateFrameByType("FRAME", "", gameUI, "", 0)
    BlzFrameSetAbsPoint(parentFrame, FRAMEPOINT_CENTER, 0.4, 0.3)
    BlzFrameSetSize(parentFrame, 0.15, 0.22)
    local frameFlowTable = FrameFlow.create(parentFrame, 0.0005, 0.001)


    for index = 1, 11, 1 do

       local icon =  BlzCreateFrameByType("BACKDROP", "MYIcon", gameUI, "", 0)
       BlzFrameSetTexture(icon, "ReplaceableTextures\\CommandButtons\\BTNHeroPaladin",0, false)
       BlzFrameSetSize(icon, 0.03, 0.03)
       FrameFlow.add(frameFlowTable, icon)
    end
    icon =  BlzCreateFrameByType("BACKDROP", "MYIcon", gameUI, "", 0)
    BlzFrameSetTexture(icon, "ReplaceableTextures\\CommandButtons\\BTNHeroArchMage",0, false)
    BlzFrameSetSize(icon, 0.08, 0.08)
    FrameFlow.add(frameFlowTable, icon)

    icon =  BlzCreateFrameByType("BACKDROP", "MYIcon", gameUI, "", 0)
    BlzFrameSetSize(icon, 0.1, 0.1)
    BlzFrameSetTexture(icon, "ReplaceableTextures\\CommandButtons\\BTNHeroMountainKing",0, false)
    FrameFlow.add(frameFlowTable, icon)
    icon =  BlzCreateFrameByType("BACKDROP", "MYIcon", gameUI, "", 0)
    BlzFrameSetTexture(icon, "ReplaceableTextures\\CommandButtons\\BTNHeroBloodElfPrince",0, false)
    BlzFrameSetSize(icon, 0.08, 0.04)
    FrameFlow.add(frameFlowTable, icon)
    end,err)
  end
end

This is the visual result of the example
Frame Flow example 2.jpg

Window + FrameFlow
FrameFlow in Window.jpg

Edited: Replaced selfexecution with a less problematic approach
 
Last edited:
Level 3
Joined
Aug 18, 2016
Messages
42
How do I call this code using a custom script action? I'm not sure what to type.
 
Look again at the code provided After example.

One creates a container Frame using BlzCreateFrameByType.
sets size and position of the container.
Then one creates a new FrameFlow using the container.
after that one adds wanted frames into the FrameFlow: FrameFlow.add(frameFlowTable, icon)
The added Frames should have a size before they are added
 
Uses cases are:
lazy frame placing
wierd scrolling frames containing other frames, the contained frames can be anything (from the Frame-Group, no SimpleFrames) as long they have a size.

One still has to create the frames oneself. With that it supports also more simple static UI-Frames, in which each Frame does only one thing.

Because of that I think it is not suited for an inventory. Normaly the Frames inside an inventory share one way to display them, basicly they change content. That is the opposite of the idea of this system. FrameFlow does not alter the Frames given, it only moves shows/hides them.
Edit: For a simple grid, it is an overkill

My other resource [Lua] UI - FrameList was also created with such a mindset, but it does displays the frames in a row or col. While FrameFlow fills a rect.

But I understand the doubt of the use of this system.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I would definitely prefer [Lua] - UI - FrameList over this in terms of consistency. Unless I'm looking at photographs, I don't think it makes sense to display frames with different sizes like this. I think @Zwiebelchen was onto something with identifying it as a Diablo 3 style inventory, but as you said, that wouldn't work.

If there arises a good use case for this, I can pull it from the graveyard. But I'd rather not let this distract from your other high-quality accomplishments.
 
Top