slint-ui / slint

Slint is a declarative GUI toolkit to build native user interfaces for Rust, C++, or JavaScript apps.
https://slint.dev
Other
17.07k stars 573 forks source link

Wrong transition animation when values indirectly updated right before start #5426

Open Justyna-JustCode opened 3 months ago

Justyna-JustCode commented 3 months ago

Slint 1.6 + Rust

I noticed that values used for a transition animation are wrong if the property value is indirectly updated right before the transition is triggered.

Example code:

    Rectangle {
        property <bool> expanded: false;
        property <length> start-y: 70% * parent.height;

        background: red;
        height: 30%;
        y: self.start-y;

        states [
            expanded when self.expanded: {
                height: 100%;
                y: 0;

                in { animate y, height { duration: 1s; } }
                out { animate y, height { duration: 300ms; } }
            }
        ]

        TouchArea {
            clicked => {
                parent.start-y = 30% * parent.height;
                // parent.y; // <- required for the animation to work properly
                parent.expanded = !parent.expanded;
            }
        }
    }

Initially, the red rectangle is placed at the very bottom of the window. However, when clicked, the position is updated, and the expanded state is enabled (with the transition triggered).

My expectation for the code is that after clicking on the rectangle, it is firstly moved to the proper position (30% * parent.height) and then expanded. In reality, the rectangle is expanded from the initial bottom position.

It seems like the value of y will not yet be updated when the transition starts. If the y property is set directly (not via the start-y property), the problem is gone. Another workaround is to access the y property before triggering state change (see the commented line).

Actual: Expected:
expand-bug-actual expand-bug-expected
ogoffart commented 3 months ago

Thanks for filling a bug.

The thing is that the binding evaluation is lazy, and the animation starts from the last evaluated value.

So in summary, the binding for your property is (pseudo-code):

y: !self.expanded ? self.start-y : 'state-animation-to'(0);

when you change the value of start-y, the value of y is marked as dirty but not re-evaluated yet. If you don't query the value before changing the state, the state animation will start at the last computed value of the property.

You found the work around which is to query that property before the animation start. One change we could do is re-evaluate the property of the previous state before as the start of the animation. But that also might not be correct :-/

Slightly related is https://github.com/slint-ui/slint/issues/4811 in which animation restarts with an outdated value.

Justyna-JustCode commented 3 months ago

Thanks for your reply!

One change we could do is re-evaluate the property of the previous state before as the start of the animation. But that also might not be correct :-/

This is precisely what I would expect. Could you elaborate on why you think it might not be correct? I can't think of any example. If the property is already marked dirty when the transition starts, it was changed before the transition was triggered. In my opinion, the order here implies that the transition should use the updated value.

When I access (query) the value, it gets updated. But the animation also (well, conceptually) queries the value to know what to start from. So, the behavior should be the same.

Am I missing something? 😉

ogoffart commented 3 months ago

The case i'm thinking about is this:

states [
  A when ...: { p: 0; in => { animate p { duration 1s } }
  B when ...: { p: 100; in => { animate p { duration 1s } }
]

When we change back from B to A, the property p is dirty because the time changes. But we don't want to re-evaluate the start position of state B either.

Justyna-JustCode commented 3 months ago

I might be missing or misunderstanding something, and I don't know the implementation details. But:

When we change back from B to A, the property p is dirty because the time changes.

The p property is dirty because it was updated by the B-state transition (which seems good). When we switch back to state A, we should use the latest property value as a start position (right?). So, the dirty state should be taken into consideration, and the property value should be evaluated before the A-state transition starts.

But we don't want to re-evaluate the start position of state B either.

Why do we consider B at all? Shouldn't the B-state animation just stop (be aborted) when we switch back to state A?

ogoffart commented 3 months ago

we should use the latest property value as a start position (right?).

Yes, but the problem is that we didn't evaluate that property before since it is lazy evaluated. We used the last computed property, which is not necessarily the value it should have had when the animation starts. Which is arguably wrong.