Michael-48 / Iris

Iris is an Immediate mode GUI Library for Roblox, Based on Dear ImGui
https://michael-48.github.io/Iris/
MIT License
147 stars 22 forks source link

Widget layout order does not update for dynamically visible widgets #58

Closed OverHash closed 1 week ago

OverHash commented 3 weeks ago

Consider the following example:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Iris = require(ReplicatedStorage.Iris)

Iris.Init()

local currentSchedule = {
    { eventName = "Dinner", minutesInDay = 540 },
    { eventName = "BedTime", minutesInDay = 600 },
    { eventName = "SleepTime", minutesInDay = 630 },
}

local callCount = 0
Iris:Connect(function()
    callCount += 1
    Iris.Window({"debug"})

    for _, event in currentSchedule do
        local isCurrentEvent = event.eventName == "Dinner" and callCount >= 200

        if isCurrentEvent then
            Iris.Text({ `{event.eventName} ({event.minutesInDay})` })
        else
            Iris.Text({ `{event.eventName}` })
        end
    end

    Iris.End()
end)

I would expect this to render the items in the Dinner, BedTime, SleepTime order. This is what happens for the first 200 frames:

image

however, after these 200 frames, the Dinner text updates and randomly reorders to be 2nd in the list. This reordering is unexpected (only the text should have changed):

image

This is a similar issue to #48, except we are not conditionally rendering, but merely updating an elements text argument.

OverHash commented 3 weeks ago

Here's some more details when investigating:

If we change our code to be:

-if isCurrentEvent then
-   Iris.Text({ `{event.eventName} ({event.minutesInDay})` })
-else
-   Iris.Text({ `{event.eventName}` })
-end
+Iris.Text({ `{event.eventName} {if isCurrentEvent then "(updated)" else ""}` })

then we get consistent IDs for each call. That is, we will have IDs of:

regardless of if we are calling before or after the 200th frame. We then get stable order, because Iris performs the deep compare between the arguments and makes an update.


Conversely, the original code given in the issue causes a new widget constructed with an ID of +22+238+86:1. Iris then performs an update for both +24+238+86:1 and +24+238+86:2.

The new +22+238+86:1 created does get the correct ZIndex, However, the old +24+238+86:2 (originally 2nd in the order) has an update from Text=Dinner -> Text=BedTime, and so it gets the same ZIndex as +22+238+86:1, and so during the update since this Instance is constructed before the newly created Dinner (updated), it orders before it (since in Roblox on equal LayoutOrder, we break ties by instance creation time).

I can't see a quick fix here, since this is quite a complicated issue with conditional rendering. It does seem rather related to the issue I posted earlier.

SirMallard commented 3 weeks ago

Yeah, this is one of the drawbacks of Iris. It could be possible for us to change the whole ID system, so that it doesn't use the line number and instead relies on the text arguments, but that probably won't fix these issues.

Iris doesn't work that well with changing layout orders because we are lazy and only compute these at first render and don't check. As you said, I can't see a quick fix either, unless we start monitoring when a new widget suddenly appears and then updating everything around it.

The best bet at the moment is probably to use the ID system to push and pop specific IDs for the text. The IDs being the meal would work, because the meal order shouldn't change and the name of the meal is constant:

Iris.PushId(event.eventName)
if isCurrentEvent then
    Iris.Text({ `{event.eventName} ({event.minutesInDay})` })
else
    Iris.Text({ `{event.eventName}` })
end
Iris.PopId(event.EventName)