ItemTransitionStyler.java

package swingtree;

import org.jspecify.annotations.Nullable;
import swingtree.animation.*;
import swingtree.api.AnimatedItemStyler;
import swingtree.style.ComponentStyleDelegate;

import javax.swing.JComponent;
import java.awt.event.ActionEvent;
import java.lang.ref.WeakReference;
import java.util.Objects;

/**
 *  Models a transition animation towards the current item of a bound property:
 *  every item change restarts the animation, whose {@link AnimationStatus#progress()}
 *  runs from 0 to 1 over the configured lifetime, while the item itself switches
 *  immediately. This is the machinery behind
 *  {@link UIForAnySwing#withStyle(sprouts.Val, LifeTime, AnimatedItemStyler)}
 *  and the item flavoured sibling of the {@link FlipFlopStyler}. <br>
 *  All state in here is UI thread owned: the item is refreshed through captured
 *  property event values (never by reading the property, which belongs to the
 *  application thread) and the animation status is written by the animation
 *  dispatcher, which also runs on the UI thread.
 *
 * @param <T> The type of the item of the bound property.
 * @param <C> The type of the component which this transition animation is applied to.
 */
final class ItemTransitionStyler<T, C extends JComponent>
{
    private final LifeTime                 _lifetime;
    private final AnimatedItemStyler<T, C> _styler;
    private final WeakReference<C>         _owner;

    private @Nullable T _item;
    private @Nullable AnimationStatus _status = null;
    private FlipFlopStyler.@Nullable DisposableAnimation _animation = null;


    ItemTransitionStyler( @Nullable T initialItem, C owner, LifeTime lifetime, AnimatedItemStyler<T, C> styler ) {
        _item     = initialItem;
        _owner    = new WeakReference<>(Objects.requireNonNull(owner));
        _lifetime = Objects.requireNonNull(lifetime);
        _styler   = Objects.requireNonNull(styler);
    }


    ComponentStyleDelegate<C> style( ComponentStyleDelegate<C> delegate ) throws Exception {
        AnimationStatus status = _status;
        if ( status == null ) // Nothing has changed yet, so the initial item is considered fully settled.
            status = AnimationStatus.endOf(
                            LifeSpan.startingNowWith(_lifetime),
                            Stride.PROGRESSIVE,
                            new ActionEvent(this, 0, null),
                            1
                        );
        _status = status;
        return _styler.style(NullUtil.fakeNonNull(_item), status, delegate);
    }

    void set( final @Nullable T newItem ) {
        if ( Objects.equals(_item, newItem) )
            return; // The same item arrived again; there is nothing to transition to.
        C owner = _owner.get();
        if ( owner == null )
            return;
        _item = newItem; // The item switches immediately, only the progress towards it is animated...
        if ( _animation != null ) {
            _animation.dispose();
            _animation = null;
        }
        _status = AnimationStatus.startOf(
                        LifeSpan.startingNowWith(_lifetime),
                        Stride.PROGRESSIVE,
                        new ActionEvent(this, 0, null)
                    );
        _animation = new FlipFlopStyler.DisposableAnimation(new Animation() {
            @Override public void run( AnimationStatus status )    { _status = status; }
            @Override public void finish( AnimationStatus status ) { _status = status; _animation = null; }
        });
        AnimationDispatcher.animateFor(_lifetime, Stride.PROGRESSIVE, owner).go(_animation);
    }

}