<commit_before>//
// Verbum - words, fonts, and tasteful shadows
// Copyright © 2014-2015 Three Rings Design, Inc.

package tripleplay.game;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import pythagoras.f.IDimension;

import react.Closeable;
import react.Signal;
import react.Slot;
import react.UnitSignal;
import react.UnitSlot;

import playn.core.Clock;
import playn.core.Game;
import playn.core.Platform;
import playn.scene.GroupLayer;
import playn.scene.Pointer;

import tripleplay.ui.Interface;
import tripleplay.ui.Root;
import tripleplay.util.Interpolator;

/**
 * Maintains a 2D layout of {@link Screen}s. New screens can be introduced in a direction, and the
 * view is scrolled in that direction to focus on the new screen. The user can then slide the view
 * back toward the previous screen (in the opposite direction that it was introduced). If they
 * release their slide with the old screen sufficiently visible, it will be restored to focus.
 */
public class ScreenSpace implements Iterable<ScreenSpace.Screen>
{
    /** The directions in which a new screen can be added. */
    public static abstract class Dir implements Cloneable {

        /** Returns the horizontal motion of this direction: 1, 0 or -1. */
        public int horizComp () { return 0; }
        /** Returns the vertical motion of this direction: 1, 0 or -1. */
        public int vertComp () { return 0; }

        /** Returns whether this direction can be manually "untransitioned". */
        public boolean canUntrans () {
            return horizComp() != 0 || vertComp() != 0;
        }
        /** Returns the direction to use when untransing from this dir. */
        public abstract Dir untransDir ();

        /** Prepares {@code oscreen} and {@code nscreen} to be transitioned. {@code oscreen} is the
          * currently visible screen and {@code nscreen} is the screen transitioning into view. */
        public void init (Screen oscreen, Screen nscreen) {
            oscreen.setTransiting(true);
            nscreen.setTransiting(true);
        }

        /** Updates the position of {@code oscreen} and {@code nscreen} based on {@code pct}.
          * @param pct a value ranged {@code [0,1]} indicating degree of completeness. */
        public abstract void update (Screen oscreen, Screen nscreen, float pct);

        /** Cleans up after a transition. {@link update} will have been called with {@code pct}
          * equal to one immediately prior to this call, so this method is only needed when actual
          * cleanup is needed, like the removal of custom shaders, etc.
          *
          * <p>Note also that the old screen's layer will have been made non-visible prior to this
          * call. This call should not restore that visibility. */
        public void finish (Screen oscreen, Screen nscreen) {
            oscreen.setTransiting(false);
            nscreen.setTransiting(false);
        }

        /** Returns the duration of this transition (in millis). */
        public float transitionTime () { return 500; }
    }

    public static final Dir UP = new Dir() {
        public int vertComp () { return -1; }
        public Dir untransDir () { return DOWN; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            float oheight = oscreen.size().height();
            float ostart = 0, nstart = oheight, range = -oheight;
            float offset = pct * range;
            oscreen.layer.setTy(ostart + offset);
            nscreen.layer.setTy(nstart + offset);
        }
    };

    public static final Dir DOWN = new Dir() {
        public int vertComp () { return 1; }
        public Dir untransDir () { return UP; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            float nheight = nscreen.size().height();
            float ostart = 0, nstart = -nheight, range = nheight;
            float offset = pct * range;
            oscreen.layer.setTy(ostart + offset);
            nscreen.layer.setTy(nstart + offset);
        }
    };

    public static final Dir LEFT = new Dir() {
        public int horizComp () { return -1; }
        public Dir untransDir () { return RIGHT; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            float owidth = oscreen.size().width();
            float ostart = 0, nstart = owidth, range = -owidth;
            float offset = pct * range;
            oscreen.layer.setTx(ostart + offset);
            nscreen.layer.setTx(nstart + offset);
        }
    };

    public static final Dir RIGHT = new Dir() {
        public Dir untransDir () { return LEFT; }
        public int horizComp () { return 1; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            float nwidth = nscreen.size().width();
            float ostart = 0, nstart = -nwidth, range = nwidth;
            float offset = pct * range;
            oscreen.layer.setTx(ostart + offset);
            nscreen.layer.setTx(nstart + offset);
        }
    };

    public static final Dir IN = new Dir() {
        public Dir untransDir () { return OUT; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            oscreen.layer.setAlpha(1-pct);
            nscreen.layer.setAlpha(pct);
            // TODO: scaling
        }
        public void finish (Screen oscreen, Screen nscreen) {
            super.finish(oscreen, nscreen);
            oscreen.layer.setAlpha(1);
        }
    };

    public static final Dir OUT = new Dir() {
        public Dir untransDir () { return IN; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            oscreen.layer.setAlpha(1-pct);
            nscreen.layer.setAlpha(pct);
            // TODO: scaling
        }
        public void finish (Screen oscreen, Screen nscreen) {
            super.finish(oscreen, nscreen);
            oscreen.layer.setAlpha(1);
        }
    };

    public static final Dir FLIP = new Dir() {
        public Dir untransDir () { return FLIP; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            // TODO
        }
    };

    /**
     * A screen that integrates with {@code ScreenSpace}. The screen lifecycle is:
     * {@code init [wake gainedFocus lostFocus sleep]+ dispose}.
     *
     * <p>When the screen has the potential to become visible (due to the user scrolling part of
     * the screen into view) it will have been wakened. If the user selects the screen, it will be
     * animated into position and then {@code gainedFocus} will be called. If the user scrolls a
     * new screen into view, {@code lostFocus} will be called, the screen will be animated away. If
     * the screen is no longer "at risk" of being shown, {@code sleep} will be called. When the
     * screen is finally removed from the screen space, {@code dispose} will be called.
     */
    public static abstract class Screen {

        /** Contains the scene graph root for this screen. */
        public final GroupLayer layer = new GroupLayer();

        /** A signal emitted on every simulation update, while this screen is showing. */
        public final Signal<Clock> update = Signal.create();

        /** A signal emitted on every frame, while this screen is showing. */
        public final Signal<Clock> paint = Signal.create();

        /** Returns the game in which this screen is operating. */
        public abstract Game game ();

        public Screen () {
            layer.setName(Screen.this + " layer");
            layer.absorbHits();
        }

        /** Called when this screen is first added to the screen space. */
        public void init () {
            // nada by default
        }

        /** Returns the size of this screen, for use by transitions.
          * Defaults to the size of the entire view. */
        public IDimension size () { return game().plat.graphics().viewSize; }

        /** Returns true when this screen is awake. */
        public boolean awake () { return (_flags & AWAKE) != 0; }
        /** Returns true when this screen is in-transition. */
        public boolean transiting () { return (_flags & TRANSITING) != 0; }

        /** Called when this screen will potentially be shown.
          * Should create main UI and prepare it for display. */
        public void wake () {
            _flags |= AWAKE;
        }

        /** Called when this screen has become the active screen. */
        public void gainedFocus () {
            assert awake();
        }

        /** Called when some other screen is about to become the active screen. This screen will be
          * animated out of view. This may not be immediately followed by a call to {@link #sleep}
          * because the screen may remain visible due to incidental scrolling by the user. Only
          * when the screen is separated from the focus screen by at least one screen will it be
          * put to sleep. */
        public void lostFocus () {
        }

        /** Called when this screen is no longer at risk of being seen by the user. This should
          * dispose the UI and minimize the screen's memory footprint as much as possible. */
        public void sleep () {
            _flags &= ~AWAKE;
            _closeOnSleep.close();
        }

        /** Called when this screen is removed from the screen space. This will always be preceded
          * by a call to {@link #sleep}, but if there are any resources that the screen retains
          * until it is completely released, this is the place to remove them. */
        public void dispose () {
            assert !awake();
        }

        /** Returns whether or not an untransition gesture may be initiated via {@code dir}.
          *
          * <p>By default this requires that the screen be at its origin in x or y depending on the
          * orientation of {@code dir}. If a screen uses a {@code Flicker} to scroll vertically,
          * this will automatically do the right thing. If there are other circumstances in which a
          * screen wishes to prevent the user from initiating an untransition gesture, this is the
          * place to put 'em.
          */
        public boolean canUntrans (Dir dir) {
            if (dir.horizComp() != 0) return layer.tx() == 0;
            if (dir.vertComp() != 0) return layer.ty() == 0;
            return true;
        }

        /**
         * Adds {@code ac} to a list of closeables which will be closed when this screen goes to
         * sleep.
         */
        public void closeOnSleep (AutoCloseable ac) {
            _closeOnSleep.add(ac);
        }

        @Override public String toString () {
            String name = getClass().getName();
            return name.substring(name.lastIndexOf(".")+1).intern();
        }

        void setTransiting (boolean transiting) {
            if (transiting) _flags |= TRANSITING;
            else            _flags &= ~TRANSITING;
        }

        void setActive (boolean active) {
            _scons.close();
            if (active) _scons = Closeable.Util.join(
                game().update.connect(update.slot()),
                game().paint.connect(paint.slot()));
            else _scons = Closeable.Util.NOOP;
            layer.setVisible(active);
        }
        boolean isActive () { return _scons != Closeable.Util.NOOP; }

        /** Flag: whether this screen is currently awake. */
        protected static final int AWAKE = 1 << 0;
        /** Flag: whether this screen is currently transitioning. */
        protected static final int TRANSITING = 1 << 1;

        protected int _flags;
        protected Closeable _scons = Closeable.Util.NOOP;
        protected final Closeable.Set _closeOnSleep = new Closeable.Set();
    }

    /** A {@link Screen} that takes care of basic UI setup for you. */
    public static abstract class UIScreen extends Screen {

        /** Manages the main UI elements for this screen. */
        public final Interface iface;

        @Override public void wake () {
            super.wake();
            createUI();
        }

        @Override public void sleep () {
            super.sleep();
            iface.disposeRoots();
            // a screen is completely cleared and recreated between sleep/wake calls, so clear the
            // animator after disposeing the root so that unprocessed anims don't hold onto memory
            iface.anim.clear();
        }

        protected UIScreen (Platform plat) {
            iface = new Interface(plat, paint);
        }

        /** Creates the main UI for this screen. Create one or more {@link Root} instances using
          * {@link #iface} and they will be disposed in {@link #sleep}. */
        protected abstract void createUI ();
    }

    /** Creates a screen space which will manage screens for {@code game}. */
    public ScreenSpace (Game game, GroupLayer rootLayer) {
        _game = game;
        _rootLayer = rootLayer;
    }

    @Override public Iterator<Screen> iterator () {
        return new Iterator<Screen>() {
            private int _idx = 0;
            public boolean hasNext () { return _idx < screenCount(); }
            public Screen next () { return screen(_idx++); }
            public void remove () { throw new UnsupportedOperationException(); }
        };
    }

    /** Returns the number of screens in the space. */
    public int screenCount () { return _screens.size(); }
    /** Returns the screen at {@code index}. */
    public Screen screen (int index) { return _screens.get(index).screen; }
    /** Returns the currently focused screen. */
    public Screen focus () { return (_current == null) ? null : _current.screen; }
    /** Returns true if we're transitioning between two screens at this instant. This may either be
      * an animation driven transition, or a manual transition in progress due to a user drag. */
    public boolean isTransiting () { return _target != null; }
    /** Returns the degree of completeness ({@code [0,1]}) of any in-progress transition, or 0. */
    public float transPct () { return _transPct; }
    /** Returns the target screen in the current transition, or null. */
    public Screen target () { return _target == null ? null : _target.screen; }

    /** Adds {@code screen} to this space but does not activate or wake it. This is intended for
      * prepopulation of a screenstack at app start. This method <em>must not</em> be called once
      * the stack is in normal operation. A series of calls to {@code initAdd} must be followed by
      * one call to {@link #add} with the screen that is actually to be displayed at app start. */
    public void initAdd (Screen screen, Dir dir) {
        assert _screens.isEmpty() || !_screens.get(0).screen.isActive();
        screen.init();
        _screens.add(0, new ActiveScreen(screen, dir));
    }

    /** Adds {@code screen} to this space, positioned {@code dir}-wise from the current screen. For
      * example, using {@code RIGHT} will add the screen to the right of the current screen and
      * will slide the view to the right to reveal the new screen. The user would then manually
      * slide the view left to return to the previous screen. */
    public void add (Screen screen, Dir dir) {
        add(screen, dir, false);
    }

    /** Adds {@code screen} to this space, replacing the current top-level screen. The screen is
      * animated in the same manner as {@link #add} using the same direction in which the current
      * screen was added. This ensures that the user returns to the previous screen in the same way
      * that they would via the to-be-replaced screen. */
    public void replace (Screen screen) {
        if (_screens.isEmpty()) throw new IllegalStateException("No current screen to replace()");
        add(screen, _screens.get(0).dir, true);
    }

    /** Removes {@code screen} from this space. If it is the top-level screen, an animated
      * transition to the previous screen will be performed. Otherwise the screen will simply be
      * removed. */
    public void pop (Screen screen) {
        if (_current.screen == screen) {
            if (_screens.size() > 1) popTrans(0);
            else {
                ActiveScreen oscr = _screens.remove(0);
                takeFocus(oscr);
                oscr.dispose();
                _current = null;
                _onPointer = Closeable.Util.close(_onPointer);
            }
        } else {
            // TODO: this screen may be inside UntransListener.previous so we may need to recreate
            // UntransListener with a new previous screen; or maybe just don't support pulling
            // screens out of the middle of the stack; that's kind of wacky; popTop and popTo may
            // be enough
            int idx = indexOf(screen);
            if (idx >= 0) {
                popAt(idx);
                checkSleep(); // we may need to wake a new screen
            }
        }
    }

    /** Removes all screens from the space until {@code screen} is reached. No transitions will be
      * used, all screens will simply be removed and disposeed until we reach {@code screen}, and
      * that screen will be woken and positioned properly. */
    public void popTo (Screen screen) {
        if (current() == screen) return; // NOOP!
        ActiveScreen top = _screens.get(0);
        while (top.screen != screen) {
            takeFocus(top);
            top.dispose();
            top = _screens.get(0);
        }
        checkSleep(); // wake up the top screen
        top.screen.layer.setTranslation(0, 0); // ensure that it's positioned properly
        giveFocus(top);
    }

    /** Returns the current screen, or {@code null} if this space is empty. */
    public Screen current () {
        return (_current == null) ? null : _current.screen;
    }

    protected int indexOf (Screen screen) {
        for (int ii = 0, ll = _screens.size(); ii < ll; ii++) {
            if (_screens.get(ii).screen == screen) return ii;
        }
        return -1;
    }

    protected void add (Screen screen, Dir dir, final boolean replace) {
        screen.init();
        ActiveScreen otop = _screens.isEmpty() ? null : _screens.get(0);
        final ActiveScreen ntop = new ActiveScreen(screen, dir);
        _screens.add(0, ntop);
        ntop.check(true); // wake up the to-be-added screen
        if (otop == null || !otop.screen.isActive()) {
            ntop.screen.setActive(true);
            giveFocus(ntop);
        }
        else transition(otop, ntop, ntop.dir, 0).onComplete.connect(new UnitSlot() {
            public void onEmit () {
                giveFocus(ntop);<commit_msg>Fix issues with popTo().<commit_after>//
// Verbum - words, fonts, and tasteful shadows
// Copyright © 2014-2015 Three Rings Design, Inc.

package tripleplay.game;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import pythagoras.f.IDimension;

import react.Closeable;
import react.Signal;
import react.Slot;
import react.UnitSignal;
import react.UnitSlot;

import playn.core.Clock;
import playn.core.Game;
import playn.core.Platform;
import playn.scene.GroupLayer;
import playn.scene.Pointer;

import tripleplay.ui.Interface;
import tripleplay.ui.Root;
import tripleplay.util.Interpolator;

/**
 * Maintains a 2D layout of {@link Screen}s. New screens can be introduced in a direction, and the
 * view is scrolled in that direction to focus on the new screen. The user can then slide the view
 * back toward the previous screen (in the opposite direction that it was introduced). If they
 * release their slide with the old screen sufficiently visible, it will be restored to focus.
 */
public class ScreenSpace implements Iterable<ScreenSpace.Screen>
{
    /** The directions in which a new screen can be added. */
    public static abstract class Dir implements Cloneable {

        /** Returns the horizontal motion of this direction: 1, 0 or -1. */
        public int horizComp () { return 0; }
        /** Returns the vertical motion of this direction: 1, 0 or -1. */
        public int vertComp () { return 0; }

        /** Returns whether this direction can be manually "untransitioned". */
        public boolean canUntrans () {
            return horizComp() != 0 || vertComp() != 0;
        }
        /** Returns the direction to use when untransing from this dir. */
        public abstract Dir untransDir ();

        /** Prepares {@code oscreen} and {@code nscreen} to be transitioned. {@code oscreen} is the
          * currently visible screen and {@code nscreen} is the screen transitioning into view. */
        public void init (Screen oscreen, Screen nscreen) {
            oscreen.setTransiting(true);
            nscreen.setTransiting(true);
        }

        /** Updates the position of {@code oscreen} and {@code nscreen} based on {@code pct}.
          * @param pct a value ranged {@code [0,1]} indicating degree of completeness. */
        public abstract void update (Screen oscreen, Screen nscreen, float pct);

        /** Cleans up after a transition. {@link update} will have been called with {@code pct}
          * equal to one immediately prior to this call, so this method is only needed when actual
          * cleanup is needed, like the removal of custom shaders, etc.
          *
          * <p>Note also that the old screen's layer will have been made non-visible prior to this
          * call. This call should not restore that visibility. */
        public void finish (Screen oscreen, Screen nscreen) {
            oscreen.setTransiting(false);
            nscreen.setTransiting(false);
        }

        /** Returns the duration of this transition (in millis). */
        public float transitionTime () { return 500; }
    }

    public static final Dir UP = new Dir() {
        public int vertComp () { return -1; }
        public Dir untransDir () { return DOWN; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            float oheight = oscreen.size().height();
            float ostart = 0, nstart = oheight, range = -oheight;
            float offset = pct * range;
            oscreen.layer.setTy(ostart + offset);
            nscreen.layer.setTy(nstart + offset);
        }
    };

    public static final Dir DOWN = new Dir() {
        public int vertComp () { return 1; }
        public Dir untransDir () { return UP; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            float nheight = nscreen.size().height();
            float ostart = 0, nstart = -nheight, range = nheight;
            float offset = pct * range;
            oscreen.layer.setTy(ostart + offset);
            nscreen.layer.setTy(nstart + offset);
        }
    };

    public static final Dir LEFT = new Dir() {
        public int horizComp () { return -1; }
        public Dir untransDir () { return RIGHT; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            float owidth = oscreen.size().width();
            float ostart = 0, nstart = owidth, range = -owidth;
            float offset = pct * range;
            oscreen.layer.setTx(ostart + offset);
            nscreen.layer.setTx(nstart + offset);
        }
    };

    public static final Dir RIGHT = new Dir() {
        public Dir untransDir () { return LEFT; }
        public int horizComp () { return 1; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            float nwidth = nscreen.size().width();
            float ostart = 0, nstart = -nwidth, range = nwidth;
            float offset = pct * range;
            oscreen.layer.setTx(ostart + offset);
            nscreen.layer.setTx(nstart + offset);
        }
    };

    public static final Dir IN = new Dir() {
        public Dir untransDir () { return OUT; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            oscreen.layer.setAlpha(1-pct);
            nscreen.layer.setAlpha(pct);
            // TODO: scaling
        }
        public void finish (Screen oscreen, Screen nscreen) {
            super.finish(oscreen, nscreen);
            oscreen.layer.setAlpha(1);
        }
    };

    public static final Dir OUT = new Dir() {
        public Dir untransDir () { return IN; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            oscreen.layer.setAlpha(1-pct);
            nscreen.layer.setAlpha(pct);
            // TODO: scaling
        }
        public void finish (Screen oscreen, Screen nscreen) {
            super.finish(oscreen, nscreen);
            oscreen.layer.setAlpha(1);
        }
    };

    public static final Dir FLIP = new Dir() {
        public Dir untransDir () { return FLIP; }
        public void update (Screen oscreen, Screen nscreen, float pct) {
            // TODO
        }
    };

    /**
     * A screen that integrates with {@code ScreenSpace}. The screen lifecycle is:
     * {@code init [wake gainedFocus lostFocus sleep]+ dispose}.
     *
     * <p>When the screen has the potential to become visible (due to the user scrolling part of
     * the screen into view) it will have been wakened. If the user selects the screen, it will be
     * animated into position and then {@code gainedFocus} will be called. If the user scrolls a
     * new screen into view, {@code lostFocus} will be called, the screen will be animated away. If
     * the screen is no longer "at risk" of being shown, {@code sleep} will be called. When the
     * screen is finally removed from the screen space, {@code dispose} will be called.
     */
    public static abstract class Screen {

        /** Contains the scene graph root for this screen. */
        public final GroupLayer layer = new GroupLayer();

        /** A signal emitted on every simulation update, while this screen is showing. */
        public final Signal<Clock> update = Signal.create();

        /** A signal emitted on every frame, while this screen is showing. */
        public final Signal<Clock> paint = Signal.create();

        /** Returns the game in which this screen is operating. */
        public abstract Game game ();

        public Screen () {
            layer.setName(Screen.this + " layer");
            layer.absorbHits();
        }

        /** Called when this screen is first added to the screen space. */
        public void init () {
            // nada by default
        }

        /** Returns the size of this screen, for use by transitions.
          * Defaults to the size of the entire view. */
        public IDimension size () { return game().plat.graphics().viewSize; }

        /** Returns true when this screen is awake. */
        public boolean awake () { return (_flags & AWAKE) != 0; }
        /** Returns true when this screen is in-transition. */
        public boolean transiting () { return (_flags & TRANSITING) != 0; }

        /** Called when this screen will potentially be shown.
          * Should create main UI and prepare it for display. */
        public void wake () {
            _flags |= AWAKE;
        }

        /** Called when this screen has become the active screen. */
        public void gainedFocus () {
            assert awake();
        }

        /** Called when some other screen is about to become the active screen. This screen will be
          * animated out of view. This may not be immediately followed by a call to {@link #sleep}
          * because the screen may remain visible due to incidental scrolling by the user. Only
          * when the screen is separated from the focus screen by at least one screen will it be
          * put to sleep. */
        public void lostFocus () {
        }

        /** Called when this screen is no longer at risk of being seen by the user. This should
          * dispose the UI and minimize the screen's memory footprint as much as possible. */
        public void sleep () {
            _flags &= ~AWAKE;
            _closeOnSleep.close();
        }

        /** Called when this screen is removed from the screen space. This will always be preceded
          * by a call to {@link #sleep}, but if there are any resources that the screen retains
          * until it is completely released, this is the place to remove them. */
        public void dispose () {
            assert !awake();
        }

        /** Returns whether or not an untransition gesture may be initiated via {@code dir}.
          *
          * <p>By default this requires that the screen be at its origin in x or y depending on the
          * orientation of {@code dir}. If a screen uses a {@code Flicker} to scroll vertically,
          * this will automatically do the right thing. If there are other circumstances in which a
          * screen wishes to prevent the user from initiating an untransition gesture, this is the
          * place to put 'em.
          */
        public boolean canUntrans (Dir dir) {
            if (dir.horizComp() != 0) return layer.tx() == 0;
            if (dir.vertComp() != 0) return layer.ty() == 0;
            return true;
        }

        /**
         * Adds {@code ac} to a list of closeables which will be closed when this screen goes to
         * sleep.
         */
        public void closeOnSleep (AutoCloseable ac) {
            _closeOnSleep.add(ac);
        }

        @Override public String toString () {
            String name = getClass().getName();
            return name.substring(name.lastIndexOf(".")+1).intern();
        }

        void setTransiting (boolean transiting) {
            if (transiting) _flags |= TRANSITING;
            else            _flags &= ~TRANSITING;
        }

        void setActive (boolean active) {
            _scons.close();
            if (active) _scons = Closeable.Util.join(
                game().update.connect(update.slot()),
                game().paint.connect(paint.slot()));
            else _scons = Closeable.Util.NOOP;
            layer.setVisible(active);
        }
        boolean isActive () { return _scons != Closeable.Util.NOOP; }

        /** Flag: whether this screen is currently awake. */
        protected static final int AWAKE = 1 << 0;
        /** Flag: whether this screen is currently transitioning. */
        protected static final int TRANSITING = 1 << 1;

        protected int _flags;
        protected Closeable _scons = Closeable.Util.NOOP;
        protected final Closeable.Set _closeOnSleep = new Closeable.Set();
    }

    /** A {@link Screen} that takes care of basic UI setup for you. */
    public static abstract class UIScreen extends Screen {

        /** Manages the main UI elements for this screen. */
        public final Interface iface;

        @Override public void wake () {
            super.wake();
            createUI();
        }

        @Override public void sleep () {
            super.sleep();
            iface.disposeRoots();
            // a screen is completely cleared and recreated between sleep/wake calls, so clear the
            // animator after disposeing the root so that unprocessed anims don't hold onto memory
            iface.anim.clear();
        }

        protected UIScreen (Platform plat) {
            iface = new Interface(plat, paint);
        }

        /** Creates the main UI for this screen. Create one or more {@link Root} instances using
          * {@link #iface} and they will be disposed in {@link #sleep}. */
        protected abstract void createUI ();
    }

    /** Creates a screen space which will manage screens for {@code game}. */
    public ScreenSpace (Game game, GroupLayer rootLayer) {
        _game = game;
        _rootLayer = rootLayer;
    }

    @Override public Iterator<Screen> iterator () {
        return new Iterator<Screen>() {
            private int _idx = 0;
            public boolean hasNext () { return _idx < screenCount(); }
            public Screen next () { return screen(_idx++); }
            public void remove () { throw new UnsupportedOperationException(); }
        };
    }

    /** Returns the number of screens in the space. */
    public int screenCount () { return _screens.size(); }
    /** Returns the screen at {@code index}. */
    public Screen screen (int index) { return _screens.get(index).screen; }
    /** Returns the currently focused screen. */
    public Screen focus () { return (_current == null) ? null : _current.screen; }
    /** Returns true if we're transitioning between two screens at this instant. This may either be
      * an animation driven transition, or a manual transition in progress due to a user drag. */
    public boolean isTransiting () { return _target != null; }
    /** Returns the degree of completeness ({@code [0,1]}) of any in-progress transition, or 0. */
    public float transPct () { return _transPct; }
    /** Returns the target screen in the current transition, or null. */
    public Screen target () { return _target == null ? null : _target.screen; }

    /** Adds {@code screen} to this space but does not activate or wake it. This is intended for
      * prepopulation of a screenstack at app start. This method <em>must not</em> be called once
      * the stack is in normal operation. A series of calls to {@code initAdd} must be followed by
      * one call to {@link #add} with the screen that is actually to be displayed at app start. */
    public void initAdd (Screen screen, Dir dir) {
        assert _screens.isEmpty() || !_screens.get(0).screen.isActive();
        screen.init();
        _screens.add(0, new ActiveScreen(screen, dir));
    }

    /** Adds {@code screen} to this space, positioned {@code dir}-wise from the current screen. For
      * example, using {@code RIGHT} will add the screen to the right of the current screen and
      * will slide the view to the right to reveal the new screen. The user would then manually
      * slide the view left to return to the previous screen. */
    public void add (Screen screen, Dir dir) {
        add(screen, dir, false);
    }

    /** Adds {@code screen} to this space, replacing the current top-level screen. The screen is
      * animated in the same manner as {@link #add} using the same direction in which the current
      * screen was added. This ensures that the user returns to the previous screen in the same way
      * that they would via the to-be-replaced screen. */
    public void replace (Screen screen) {
        if (_screens.isEmpty()) throw new IllegalStateException("No current screen to replace()");
        add(screen, _screens.get(0).dir, true);
    }

    /** Removes {@code screen} from this space. If it is the top-level screen, an animated
      * transition to the previous screen will be performed. Otherwise the screen will simply be
      * removed. */
    public void pop (Screen screen) {
        if (_current.screen == screen) {
            if (_screens.size() > 1) popTrans(0);
            else {
                ActiveScreen oscr = _screens.remove(0);
                takeFocus(oscr);
                oscr.dispose();
                _current = null;
                _onPointer = Closeable.Util.close(_onPointer);
            }
        } else {
            // TODO: this screen may be inside UntransListener.previous so we may need to recreate
            // UntransListener with a new previous screen; or maybe just don't support pulling
            // screens out of the middle of the stack; that's kind of wacky; popTop and popTo may
            // be enough
            int idx = indexOf(screen);
            if (idx >= 0) {
                popAt(idx);
                checkSleep(); // we may need to wake a new screen
            }
        }
    }

    /** Removes all screens from the space until {@code screen} is reached. No transitions will be
      * used, all screens will simply be removed and disposed until we reach {@code screen}, and
      * that screen will be woken and positioned properly. */
    public void popTo (Screen screen) {
        if (current() == screen) return; // NOOP!
        ActiveScreen top = _screens.get(0);
        while (top.screen != screen) {
            _screens.remove(0);
            takeFocus(top);
            top.dispose();
            top = _screens.get(0);
        }
        checkSleep(); // wake up the top screen
        top.screen.layer.setTranslation(0, 0); // ensure that it's positioned properly
        top.screen.setActive(true); // mark the screen as active
        giveFocus(top);
    }

    /** Returns the current screen, or {@code null} if this space is empty. */
    public Screen current () {
        return (_current == null) ? null : _current.screen;
    }

    protected int indexOf (Screen screen) {
        for (int ii = 0, ll = _screens.size(); ii < ll; ii++) {
            if (_screens.get(ii).screen == screen) return ii;
        }
        return -1;
    }

    protected void add (Screen screen, Dir dir, final boolean replace) {
        screen.init();
        ActiveScreen otop = _screens.isEmpty() ? null : _screens.get(0);
        final ActiveScreen ntop = new ActiveScreen(screen, dir);
        _screens.add(0, ntop);
        ntop.check(true); // wake up the to-be-added screen
        if (otop == null || !otop.screen.isActive()) {
            ntop.screen.setActive(true);
            giveFocus(ntop);
        }
        else transition(otop, ntop, ntop.dir, 0).onComplete.connect(new UnitSlot() {
            public void onEmit () {
                giveFocus(ntop);
{'fraction_non_alphanumeric': 0.09387918389118549, 'fraction_numerical': 0.0036004800640085347, 'mean_word_length': 2.1514540258867036, 'pattern_counts': {'":': 0, '<': 35, '<?xml version=': 0, '>': 27, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5521070', 'n_tokens_mistral': 9854, 'n_tokens_neox': 9521, 'n_words': 4045}