[Solved] Lemur Button Z Order

I’m writing a window manager, and it has a z-order manager. When a window is clicked the window is brought to the front - and this works fine except for button text. For some reason the button text has a z-index +1 above the button itself (the background?).

If you notice in the first image, the button of the “Test Dialog” window should be below and everything is except for the button text.

I’ve searched the commandmaps and everything else I could think if, but I can’t find anywhere when the z-order is changed - or if any internal code does change it.

But if we look at the second image, the second window will cover the text. So it appears to me that the button text has +1’d the z-order. This will probably result in the window above being on the same z-index, but the window above that window is above - If that makes sense.

Is there any place where the z-index of the button text is manipulated?

The background will likely include a z-offset that is used to push anything else forward. Text shadows will also push things forward based on the shadow offset, I think.

In the end, if you want to stack Lemur windows then you are going to have to ask them how thick they are… because Lemur components are 3D. If you setup the GuiComponent properly you could slam an animated Jaimie right in there next to your buttons/labels. A smart z-order manager will take this into consideration.

2 Likes

I see. I was hoping I could restrict the thickness to 1 unit, but thinking about it, the end user probably won’t like that and your Jaime model usecase does reveal a big issue. I can do as you say and take the thickness into account. Sounds like a more malleable workflow anyway tbh.

Much appreciated.

1 Like

I was sure your answer was right, but I wanted to write the code first. I’ve marked it as solved, but with a slight caveat.

If I check the Z-Extent of the BoundingVolume of anything in the GUI node it always returns 0.0f. I guess this is because the GUI node does some magic flattening trickery. I can solve it by using the Lemur getPreferredSize() method, though - which does report the correct size.

Either way, it works! I also added a small margin between each window so the “back” of one window isn’t “touching” the “front” of the next window.

That’s weird. The PopupState uses the z extent of the gui node to figure out where to place popups.

The evil hacks that squash the Z of the gui node happen in the Gui bucket code down in the renderer. It shouldn’t affect the Z extents of things in the GUI node.

Did you maybe scale z to zero in an earlier attempt to fix the layering issues?

Not that I’m aware of. I just create the window as normal (normal being that I don’t manipulate any z-order, I just add them using the .addChild method) and add it to the window list.

package com.jayfella.window;

import com.jme3.bounding.BoundingBox;

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

/**
 * Organizes the z-order of windows automatically.
 * Each window added is assigned a z-order.
 */
public class WindowList {

    private final List<JmeWindow> list = new ArrayList<>();

    /**
     * Creates a z-organized window list.
     */
    public WindowList() {

    }

    public int getWindowCount() {
        return list.size();
    }

    public void add(JmeWindow window) {

        list.add(window);
        reorganize();
    }

    public boolean remove(JmeWindow window) {
        if (list.remove(window)) {
            reorganize();
            return true;
        }

        return false;
    }

    JmeWindow get(String id) {

        return list.stream()
                .filter(element -> {

                    String elemId = element.getWindowContainer().getUserData(JmeWindow.WINDOW_ID);

                    if (elemId != null) {
                        return elemId.compareTo(id) == 0;
                    }

                    return false;
                })
                .findFirst()
                .orElse(null);

    }

    public void bringToFront(JmeWindow window) {

        if (list.contains(window)) {

            list.remove(window);
            list.add(window);

            reorganize();
        }
    }

    public void sendToBack(JmeWindow window) {

        if (list.contains(window)) {

            list.remove(window);
            list.add(0, window);

            reorganize();
        }
    }

    float margin = .1f;

    private void reorganize() {

        for (int i = 0; i < list.size(); i++) {
            JmeWindow element = list.get(i);

            float z;

            if (i == 0) {
                z = 0;
            }
            else {

                JmeWindow previous = list.get(i - 1);

                // this works
                // z = previous.getWindowContainer().getLocalTranslation().z;
                // float zExtent = previous.getWindowContainer().getPreferredSize().z;

                // this doesn't
                BoundingBox bb = (BoundingBox)previous.getWindowContainer().getWorldBound();
                float zExtent = bb.getCenter().z + bb.getZExtent();

                z = zExtent;
                //z+= margin;

            }

            element.getWindowContainer().setLocalTranslation(
                    element.getWindowContainer().getLocalTranslation().x,
                    element.getWindowContainer().getLocalTranslation().y,
                    z);

        }
    }

    void executeWindowUpdateLoops(float tpf) {
        list.forEach(window -> window.update(tpf));
    }

}

But When I query the size of the bounding volume, it’s always zero. The meat of it all is in the reorganize method.

Are the other extents also 0?

Maybe it’s being queried before the first time it’s rendered and so GuiComponent hasn’t actually set the size (created the geometry, etc.) yet.

Edit: and in that case getPreferredSize() is the proper way anyway.

No. You can see in the debug window at the bottom of the screen that X and Y of the boundingVolume are sized, and the center is set (because they are centered by default to avoid people getting confused when they add a window and can’t see it).

Either way getPreferredSize works. It’s more of an educational thing at this point :stuck_out_tongue:

It would be interesting to see JmeWindow since I suspect something in there is squashing the Z.

Sure.

package com.jayfella.window;

import com.jayfella.window.exception.WindowNotInitializedException;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.simsilica.lemur.*;
import com.simsilica.lemur.component.IconComponent;
import com.simsilica.lemur.component.SpringGridLayout;
import com.simsilica.lemur.event.CursorEventControl;
import com.simsilica.lemur.event.CursorListener;
import com.simsilica.lemur.event.MouseEventControl;
import com.simsilica.lemur.style.ElementId;
import org.jetbrains.annotations.NotNull;

import java.util.UUID;

public class JmeWindow {

    public static final String WINDOW_ID = "Window_ID";

    public static final String ELEMENT_ID_TITLE_BAR = "window-title-bar";
    public static final String ELEMENT_ID_TITLE_LABEL = "window-title-label";

    public static final String ELEMENT_ID_WINDOW_CONTENT_OUTER = "window-content-outer";
    public static final String ELEMENT_ID_WINDOW_CONTENT_INNER = "window-content-inner";

    private static final String ELEMENT_ID_BUTTON_MINIMIZE = "window-button-minimize";
    private static final String ELEMENT_ID_BUTTON_MAXIMIZE = "window-button-maximize";
    private static final String ELEMENT_ID_BUTTON_CLOSE = "window-button-close";

    private final Container windowContainer;
    private final Container titleContainer;
    private final Container contentParent;
    private final Container contentContainer;

    private final Label titleLabel;
    private final Button minButton;
    private final Button maxButton;
    private final Button closeButton;

    private CursorListener dragHandler;
    private CursorListener clickHandler;

    private final String id;

    private WindowManager windowManager;

    private final ButtonHighlighter minButtonHighlighter;
    private final ButtonHighlighter maxButtonHighlighter;
    private final ButtonHighlighter closeButtonHighlighter;

    public JmeWindow() {
        this(null, null);
    }

    public JmeWindow(String title) {
        this(title, null);
    }

    public JmeWindow(String title, Panel content) {

        id = UUID.randomUUID().toString();

        windowContainer = new Container("null");
        windowContainer.setUserData(WINDOW_ID, id);

        // top bar (title, min, max, close)
        titleContainer = windowContainer.addChild(
                new Container(new SpringGridLayout(Axis.Y, Axis.X, FillMode.First, FillMode.First),
                        new ElementId(ELEMENT_ID_TITLE_BAR)));

        // set the title if one has been given.
        if (title == null) title = "";
        titleLabel = titleContainer.addChild(new Label(title, new ElementId(ELEMENT_ID_TITLE_LABEL)), 0, 0);

        minButton = titleContainer.addChild(new Button("", new ElementId(ELEMENT_ID_BUTTON_MINIMIZE)), 0, 1);
        maxButton = titleContainer.addChild(new Button("", new ElementId(ELEMENT_ID_BUTTON_MAXIMIZE)), 0, 2);
        closeButton = titleContainer.addChild(new Button("", new ElementId(ELEMENT_ID_BUTTON_CLOSE)), 0, 3);

        contentParent = windowContainer.addChild(new Container(
                new SpringGridLayout(Axis.Y, Axis.X, FillMode.First, FillMode.First),
                new ElementId(ELEMENT_ID_WINDOW_CONTENT_OUTER)));

        contentContainer = contentParent.addChild(new Container(
                new SpringGridLayout(),
                new ElementId(ELEMENT_ID_WINDOW_CONTENT_INNER)
        ));

        if (content != null) {
            contentContainer.addChild(content);
        }

        // button actions
        minButton.addCommands(Button.ButtonAction.Click, source -> contentParent.removeFromParent());
        maxButton.addCommands(Button.ButtonAction.Click, source -> windowContainer.addChild(contentParent));

        closeButton.addCommands(Button.ButtonAction.Click, source -> {
            if (windowClosing()) {

                if (windowManager == null) {
                    throw new WindowNotInitializedException("You must add the window to the WindowManager!");
                }

                windowManager.remove(this);
            }
        });

        // MouseEventControl.addListenersToSpatial(minButton, new ButtonHighlighter((IconComponent) minButton.getIcon()));
        this.minButtonHighlighter = new ButtonHighlighter((IconComponent) minButton.getIcon());
        this.maxButtonHighlighter = new ButtonHighlighter((IconComponent) maxButton.getIcon());
        this.closeButtonHighlighter = new ButtonHighlighter((IconComponent) closeButton.getIcon());
    }

    private void addCursorEvents() {
        CursorEventControl.addListenersToSpatial(titleContainer, dragHandler);
        CursorEventControl.addListenersToSpatial(windowContainer, clickHandler);
        CursorEventControl.addListenersToSpatial(titleContainer, clickHandler);

        MouseEventControl.addListenersToSpatial(minButton, minButtonHighlighter);
        MouseEventControl.addListenersToSpatial(maxButton, maxButtonHighlighter);
        MouseEventControl.addListenersToSpatial(closeButton, closeButtonHighlighter);
    }

    protected void removeCursorEvents() {
        CursorEventControl.removeListenersFromSpatial(titleContainer, dragHandler);
        CursorEventControl.removeListenersFromSpatial(windowContainer, clickHandler);
        CursorEventControl.removeListenersFromSpatial(titleContainer, clickHandler);

        MouseEventControl.removeListenersFromSpatial(minButton, minButtonHighlighter);
        MouseEventControl.removeListenersFromSpatial(maxButton, maxButtonHighlighter);
        MouseEventControl.removeListenersFromSpatial(closeButton, closeButtonHighlighter);
    }

    Container getWindowContainer() {
        return windowContainer;
    }

    void setWindowManager(WindowManager windowManager) {
        this.windowManager = windowManager;

        this.dragHandler = new WindowTitleBarDragHandler(true);
        this.clickHandler = new WindowClickZOrderHandler(windowManager);

        addCursorEvents();

        // center it by default. Let the user set another location if they prefer.
        // This way they won't get confused when a window doesn't appear.
        centerOnScreen();
    }

    public WindowManager getWindowManager() {

        if (windowManager == null) {
            throw new WindowNotInitializedException("You must add the window to the WindowManager!");
        }

        return windowManager;
    }

    /**
     * Gets the title of the window.
     * @return the title of the window.
     */
    @NotNull
    String getTitle() {
        return titleLabel.getText();
    }

    public void setTitle(String title) {
        titleLabel.setText(title);
    }

    public void setContent(Panel content) {
        contentContainer.clearChildren();
        contentContainer.addChild(content);
    }

    /**
     * Sets the location of the window.
     *
     * @param location the location of the window you wish to set.
     */
    public void setLocation(Vector2f location) {
        setLocation(location.x, location.y);
    }

    /**
     * Sets the location of the window.
     *
     * @param x the x coordinate.
     * @param y the y coordinate.
     */
    public void setLocation(float x, float y) {
        windowContainer.setLocalTranslation(x, y, windowContainer.getLocalTranslation().z);
    }

    /**
     * Returns the location of the window.
     *
     * @return the location of the window.
     */
    public @NotNull Vector2f getLocation() {
        return new Vector2f(windowContainer.getLocalTranslation().x, windowContainer.getLocalTranslation().y);
    }

    /**
     * Returns the calculated size of the window, or the size that has been set by the user.
     * @return the calculated size of the window, or the size that has been set by the user.
     */
    public @NotNull Vector3f getPreferredSize() {
        return windowContainer.getPreferredSize();
    }

    /**
     * Sets the preferred size of the window, overriding the calculated size.
     * @param preferredSize the preferred size of the window.
     */
    public void setPreferredSize(Vector3f preferredSize) {
        windowContainer.setPreferredSize(preferredSize);
    }

    /**
     * Brings this window in front of all other windows.
     */
    void bringToFront() {

        if (windowManager == null) {
            throw new WindowNotInitializedException("You must add the window to the WindowManager!");
        }

        windowManager.bringToFront(this);
    }

    /**
     * Puts this window behind every window.
     */
    void sendToBack() {

        if (windowManager == null) {
            throw new WindowNotInitializedException("You must add the window to the WindowManager!");
        }

        windowManager.sendToBack(this);
    }

    /**
     * This event is fired when a window is attempting to close, and can be cancelled by returning false.
     *
     * @return whether this close event is allowed to occur.
     */
    public boolean windowClosing() {
        removeCursorEvents();
        return true;
    }

    public void centerOnScreen() {

        if (windowManager == null) {
            throw new WindowNotInitializedException("You must add the window to the WindowManager!");
        }

        setLocation(
                windowManager.getApplication().getCamera().getWidth() * 0.5f - getPreferredSize().x * 0.5f,
                windowManager.getApplication().getCamera().getHeight() * 0.5f + getPreferredSize().y * 0.5f
        );
    }

    /**
     * Provides an update loop.
     * @param tpf time per frame.
     */
    public void update(float tpf) {

    }

}