Confused by validateLayout, setGlobalUIScale, and element positioning

Hi, I’m new to jme3 and game programming, but I’m a long time software dev. First off, I wanted to say that I’m pretty impressed with the GUI work you’re doing, and with JME3 in general. Thank you for your effort.

I’m wondering what the state of the validateLayout and setGlobalUIScale apis are right now. I’ve tried a few experiments had have gotten some strange results. Should I report bugs, wait patiently for them to be completed, or figure out what is wrong and contribute a fix?

One thing I noticed is that when creating an element at 0,0 it’s NW corner is the NW corner of its container, unless validateLayout is called in which case the SW corners align.

Also, when I use validateLayout on a panel after adding a single button with position 0.025f, 0.025f and size 0.95f, 0.95f, the button ends up below the panel (it seems to be assuming a NW corner origin but the SW corner is used instead).

Finally, setGlobalUIScale expands the size of ui elements up and to the right, which only seems to make sense if the origin was the SW corner.

My goal is to create a GUI that works well on Android tablets with high density displays of various sizes. Doing layout in terms of pixels isn’t going to work. What I really want to is to provide layout in terms of percents and inches and for the GUI to scale based on DPI.

One approach would be let 1 represent an inch and use setGlobalUIScale(dpix, dpiy). But validateLayout assumes that anything < 1 is a percentage, so that isn’t likely to work out of the box.

Any suggestions on the best approach to take here? What do you expect the final “built in” behavior to be and what would be a good way to extend it?

Thanks,

== Mike ==

Ohhhh… please ignore both!

validateLayout should be deprecated (actually removed, as it shouldn’t have been a public method) as it was from an effort to completely flip the Y axis, which became more problematic than it was worth.

setGlobalUIScale is a work in progress… it semi-works atm, though it hasn’t been thoroughly tested–or even remotely tested =P

I suggest using parent.getWidth()*somePercent for dynamic scaling… such as:

[java]
dimensions.set(screen.getWidth()*0.25f, screen.getHeight()*0.4f); // etc etc
[/java]

This way it scales with the screen dimensions… however, this is still problematic when trying to account for orientation.

OK, thanks for the clarification.

But there is one thing that still doesn’t make sense to me.

In Element.addChild and Screen.addChild you have code that inverts the y position (typically as provided by the constructor) when a child is “initialized.” But any other time calling setY doesn’t invert the y axis. Why the inconstancy? Am I missing something?

Anyway, I was able to come up with (IMO) a simple yet powerful pattern for doing dynamic layout. In use it looks something like the following. Note that the layout is actually done when the state is enabled and can be refreshed at any time. The calls in initialize just create a LayoutHelper control and attach it to the element.

No changes to tonegodGUI are needed to use this pattern.

[java]
private Vector2f dpiScale;
private Panel panel;
private Button button;

@Override
public final void initialize(AppStateManager stateManager, Application app) {

super.initialize(stateManager, app);

panel = new Panel(
	screen, 
	LayoutHelper.autoPosition(), // initialize with new Vector2f(NaN, NaN)
	LayoutHelper.autoSize() // initialize with new Vector2f(NaN, NaN)
);

dpiScale = new Vector2f(); // will be filled before layout is applied

// setup layout that will happen later
ScaleLayout.dimensions(panel, dpiScale, 0.75f, 0.75f);
ScaleLayout.left(panel, dpiScale, 0.05f);
PixelLayout.centerVertical(panel);

button = new ButtonAdapter(
	screen, 
	LayoutHelper.autoPosition(), 
	LayoutHelper.autoSize()
);

// again, setup layout for later
PercentLayout.fill(button, 0.05f);

panel.addChild(button);

}

public void setEnabled(boolean enabled) {

if(enabled) {
	
	screen.addElement(panel);
	
	// fill in the dpi scale that is used in layout
            // deviceState is what my app uses to encapsulate Android dependencies
	deviceState.getDpi(dpiScale);
	
	// apply the layout
	LayoutHelper.layoutForScreen(panel);
	
} else {
	
	screen.removeElement(panel);
	
}

}
[/java]

LayoutHelper looks like this:

[java]
import java.util.logging.Level;
import java.util.logging.Logger;

import tonegod.gui.core.Element;
import com.jme3.math.Vector2f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;

public abstract class LayoutHelper extends AbstractControl {

private final Logger log = Logger.getLogger(this.getClass().getName());
private final static Level LOG_LEVEL = Level.FINE;

private Element element;
		
protected void log(String msg, Object ... params) {
	if(log.isLoggable(LOG_LEVEL)) {
		log.log(LOG_LEVEL, msg + " -&gt; position: " + element.getPosition() + ", dimensions: " + element.getDimensions() + " on " + element, params);
	}
}

@Override
public void setSpatial(Spatial spatial) {
	
	if(!(spatial instanceof Element)) {
		throw new RuntimeException("Wrong type of spatial");
	}
	
	this.element = (Element)spatial;
	
	super.setSpatial(spatial);
	
}

/**
 * Override to update element's layout before the parent's layout has been updated.
 * Use this when the parent's layout depends on the layout of the child (e.g. size
 * panel to fit children).
 */
protected void beforeParent() {};

/**
 * Override to update element's layout to reflect changes in child layout but before
 * the parent's layout is updated.
 */
protected void afterChildren() {}

/**
 * Override to update element's layout after the parent's layout has been updated.
 * Use this when the child's layout depends on the layout of the parent (e.g. size
 * children to fit in panel).
 */
protected void afterParent() {};

/**
 * Width of parent element or screen.
 */
public float getContainerWidth() {
	Element parent = element.getElementParent();
	if(parent != null) {
		return parent.getWidth();
	} else {
		return element.getScreen().getWidth();
	}
}

/**
 * Height of parent element or screen.
 */
public float getContainerHeight() {
	Element parent = element.getElementParent();
	if(parent != null) {
		return parent.getHeight();
	} else {
		return element.getScreen().getHeight();
	}
}

@Override
protected void controlUpdate(float tpf) {
}

@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
}

public static void layoutForScreen(Element element) {
	
	if(element.getElementParent() != null) {
		throw new RuntimeException("Not a root element");
	}

	// call afterParent (the parent is the screen) for current element
	for(int i = 0; i &lt; element.getNumControls(); ++i) {
		Control control = element.getControl(i);
		if(control instanceof LayoutHelper) {
			((LayoutHelper)control).afterParent();
		}
	}
	
	// call beforeParent and afterChildren for child elements recursively
	for(Spatial spatial : element.getChildren()) {
		if(spatial instanceof Element) {
			visitBeforeParent((Element)spatial);
		}
	}
	
	// call afterChildren for current element
	for(int i = 0; i &lt; element.getNumControls(); ++i) {
		Control control = element.getControl(i);
		if(control instanceof LayoutHelper) {
			((LayoutHelper)control).afterChildren();
		}
	}
	
	// call afterParent for child elements recursively
	for(Spatial spatial : element.getChildren()) {
		if(spatial instanceof Element) {
			visitAfterParent((Element)spatial);
		}
	}
	
}

private static void visitBeforeParent(Element element) {

	// beforeParent for current element
	for(int i = 0; i &lt; element.getNumControls(); ++i) {
		Control control = element.getControl(i);
		if(control instanceof LayoutHelper) {
			((LayoutHelper)control).beforeParent();
		}
	}
	
	// call beforeParent and afterChildren for child elements recursively
	for(Spatial spatial : element.getChildren()) {
		if(spatial instanceof Element) {
			visitBeforeParent((Element)spatial);
		}
	}
			
	// call afterChildren for current element
	for(int i = 0; i &lt; element.getNumControls(); ++i) {
		Control control = element.getControl(i);
		if(control instanceof LayoutHelper) {
			((LayoutHelper)control).afterChildren();
		}
	}
	
}	

private static void visitAfterParent(Element element) {
	
	// afterParent for current element
	for(int i = 0; i &lt; element.getNumControls(); ++i) {
		Control control = element.getControl(i);
		if(control instanceof LayoutHelper) {
			((LayoutHelper)control).afterParent();
		}
	}
			
	// afterParent for child elements recursively
	for(Spatial spatial : element.getChildren()) {
		if(spatial instanceof Element) {
			visitAfterParent((Element)spatial);
		}
	}
			
}

public static Vector2f autoPosition() {
	return new Vector2f(Float.NaN, Float.NaN);
}

public static Vector2f autoSize() {
	return new Vector2f(Float.NaN, Float.NaN);
}

}
[/java]

The PixelLayout, ScaleLayout, and PercentLayout classes are:

[java]
import tonegod.gui.core.Element;

public class PixelLayout {

public static void position(final Element element, final float left, final float bottom) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(left);
				element.setY(bottom);
				log("position - left: {0}, bottom: {1}", left, bottom);
			}
		}
	);
}

public static void left(final Element element, final float left) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(left);
				log("left: {0}", left);
			}
		}
	);
}

public static void bottom(final Element element, final float bottom) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setY(bottom);
				log("bottom: {0}", bottom);
			}
		}
	);
}

public static void dimensions(final Element element, final float width, final float height) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setWidth(width);
				element.setHeight(height);
				log("dimensions - width: {0}, height: {1}", width, height);
			}
		}
	);
}

public static void width(final Element element, final float width) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setWidth(width);
				log("width: {0}", width);
			}
		}
	);
}

public static void height(final Element element, final float height) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(height);
				log("height: {0}", height);
			}
		}
	);
}

public static void fillWidth(final Element element, final float leftMargin, final float rightMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(leftMargin);
				element.setWidth(getContainerWidth() - (leftMargin + rightMargin));
				log("fillWidth - leftMargin: {0}, rightMargin: {1}", leftMargin, rightMargin);
			}
		}
	);
}

public static void fillWidth(final Element element, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(margin);
				element.setWidth(getContainerWidth() - (2 * margin));
				log("fillWidth - margin: {0}", margin);
			}
		}
	);
}

public static void fillHeight(final Element element, final float bottomMargin, final float topMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setY(bottomMargin);
				element.setHeight(getContainerHeight() - (bottomMargin + topMargin));
				log("fillHeight - bottomMargin: {0}, topMaring: {1}", bottomMargin, topMargin);
			}
		}
	);
}

public static void fillHeight(final Element element, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setY(margin);
				element.setHeight(getContainerHeight() - (2 * margin));
				log("fillHeight - margin: {0}", margin);
			}
		}
	);
}

public static void fill(final Element element, final float leftMargin, final float bottomMargin, final float rightMargin, final float topMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(leftMargin);
				element.setY(bottomMargin);
				element.setWidth(getContainerWidth() - (leftMargin + rightMargin));
				element.setHeight(getContainerHeight() - (bottomMargin + topMargin));
				log("fill - leftMargin: {0}, bottomMargin: {1}, rightMargin: {2}, topMargin: {3}", leftMargin, bottomMargin, rightMargin, topMargin);
			}
		}
	);
}

public static void fill(final Element element, final float horizontalMargin, final float verticalMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(horizontalMargin);
				element.setY(verticalMargin);
				element.setWidth(getContainerWidth() - (2 * horizontalMargin));
				element.setHeight(getContainerHeight() - (2 * verticalMargin));
				log("fill - horizontalMargin: {0}, verticalMargin: {1}", horizontalMargin, verticalMargin);
			}
		}
	);
}

public static void fill(final Element element, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(margin);
				element.setY(margin);
				element.setWidth(getContainerWidth() - (2 * margin));
				element.setHeight(getContainerHeight() - (2 * margin));
				log("fill - margin: {0}", margin);
			}
		}
	);
}

public static void extendUp(final Element element, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(getContainerHeight() - (element.getY() + margin));
				log("extendUp - margin: {0}", margin);
			}
		}
	);
}

public static void extendUpTo(final Element element, final float margin, final Element target) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(target.getY() - element.getY() - margin);
				log("extendUpTo - margin: {0}, target: {1}", margin, target);
			}
		}
	);
}

public static void extendRight(final Element element, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setWidth(getContainerWidth() - (element.getX() + margin));
				log("extendRight - margin: {0}", margin);
			}
		}
	);
}

public static void extendRightTo(final Element element, final float margin, final Element target) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(target.getX() - element.getX() - margin);
				log("extendRightTo - margin: {0}, target: {1}", margin, target);
			}
		}
	);
}

public static void centerVertical(final Element element) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setY((getContainerHeight() / 2) - (element.getHeight() / 2));
				log("centerVertical");
			}
		}
	);
}

public static void centerHorizontal(final Element element) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX((getContainerWidth() / 2) - (element.getWidth() / 2));
				log("centerVertical");
			}
		}
	);
}

}
[/java]

[java]
import com.jme3.math.Vector2f;

import tonegod.gui.core.Element;

public class ScaleLayout {

public static void position(final Element element, final Vector2f scale, final float left, final float bottom) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(left * scale.x);
				element.setY(bottom * scale.y);
				log("position - scale: {0}, left: {1}, bottom: {2}", left, bottom);
			}
		}
	);
}

public static void left(final Element element, final Vector2f scale, final float left) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(left * scale.x);
				log("left - scale: {0}, left: {1}", scale, left);
			}
		}
	);
}

public static void bottom(final Element element, final Vector2f scale, final float bottom) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setY(bottom * scale.y);
				log("bottom - scale: {0}, bottom: {1}", scale, bottom);
			}
		}
	);
}

public static void dimensions(final Element element, final Vector2f scale, final float width, final float height) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setWidth(width * scale.x);
				element.setHeight(height * scale.y);
				log("dimensions - scale: {0}, width: {1}, height: {2}", scale, width, height);
			}
		}
	);
}

public static void width(final Element element, final Vector2f scale, final float width) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setWidth(width * scale.x);
				log("width - scale: {0}, width: {1}", scale, width);
			}
		}
	);
}

public static void height(final Element element, final Vector2f scale, final float height) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(height * scale.y);
				log("height - scale: {0}, height: {1}", scale, height);
			}
		}
	);
}

public static void fillWidth(final Element element, final Vector2f scale, final float leftMargin, final float rightMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(leftMargin * scale.x);
				element.setWidth(getContainerWidth() - ((leftMargin + rightMargin) * scale.x));
				log("fillWidth - scale: {0}, leftMargin: {1}, rightMargin: {2}", scale, leftMargin, rightMargin);
			}
		}
	);
}

public static void fillWidth(final Element element, final Vector2f scale, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(margin * scale.x);
				element.setWidth(getContainerWidth() - (2 * margin * scale.x));
				log("fillWidth - scale: {0}, margin: {1}", scale, margin);
			}
		}
	);
}

public static void fillHeight(final Element element, final Vector2f scale, final float bottomMargin, final float topMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setY(bottomMargin * scale.y);
				element.setHeight(getContainerHeight() - ((bottomMargin + topMargin) * scale.y));
				log("fillHeight - scale: {0}, bottomMargin: {1}, topMaring: {2}", scale, bottomMargin, topMargin);
			}
		}
	);
}

public static void fillHeight(final Element element, final Vector2f scale, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setY(margin * scale.y);
				element.setHeight(getContainerHeight() - (2 * margin * scale.y));
				log("fillHeight - scale: {0}, margin: {1}", scale, margin);
			}
		}
	);
}

public static void fill(final Element element, final Vector2f scale, final float leftMargin, final float bottomMargin, final float rightMargin, final float topMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(leftMargin * scale.x);
				element.setY(bottomMargin * scale.y);
				element.setWidth(getContainerWidth() - (leftMargin + rightMargin) * scale.x);
				element.setHeight(getContainerHeight() - (bottomMargin + topMargin) * scale.y);
				log("fill - scale: {0}, leftMargin: {1}, bottomMargin: {2}, rightMargin: {3}, topMargin: {4}", scale, leftMargin, bottomMargin, rightMargin, topMargin);
			}
		}
	);
}

public static void fill(final Element element, final Vector2f scale, final float horizontalMargin, final float verticalMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(horizontalMargin * scale.x);
				element.setY(verticalMargin * scale.y);
				element.setWidth(getContainerWidth() - (2 * horizontalMargin * scale.x));
				element.setHeight(getContainerHeight() - (2 * verticalMargin * scale.y));
				log("fill - scale: {0}, horizontalMargin: {1}, verticalMargin: {2}", scale, horizontalMargin, verticalMargin);
			}
		}
	);
}

public static void fill(final Element element, final Vector2f scale, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(margin * scale.x);
				element.setY(margin * scale.y);
				element.setWidth(getContainerWidth() - (2 * margin * scale.x));
				element.setHeight(getContainerHeight() - (2 * margin * scale.y));
				log("fill - scale: {0}, margin: {1}", scale, margin);
			}
		}
	);
}

public static void extendUp(final Element element, final Vector2f scale, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(getContainerHeight() - (element.getY() + margin * scale.y));
				log("extendUp - scale: {0}, margin: {1}", scale, margin);
			}
		}
	);
}

public static void extendUpTo(final Element element, final Vector2f scale, final float margin, final Element target) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(target.getY() - element.getY() - margin * scale.y);
				log("extendUpTo - scale: {0}, margin: {1}, target: {2}", scale, margin, target);
			}
		}
	);
}

public static void extendRight(final Element element, final Vector2f scale, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setWidth(getContainerWidth() - (element.getX() + margin * scale.x));
				log("extendRight - scale: {0}, margin: {1}", scale, margin);
			}
		}
	);
}

public static void extendRightTo(final Element element, final Vector2f scale, final float margin, final Element target) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(target.getX() - element.getX() - margin * scale.x);
				log("extendRightTo - scale: {0}, margin: {1}, target: {2}", scale, margin, target);
			}
		}
	);
}

}
[/java]

[java]
import tonegod.gui.core.Element;

public class PercentLayout {

public static void position(final Element element, final float left, final float bottom) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(left * getContainerWidth());
				element.setY(bottom * getContainerHeight());
				log("position - left: {0}, bottom: {1}", left, bottom);
			}
		}
	);
}

public static void left(final Element element, final float left) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(left * getContainerWidth());
				log("left: {0}", left);
			}
		}
	);
}

public static void bottom(final Element element, final float bottom) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setY(bottom * getContainerHeight());
				log("bottom: {0}", bottom);
			}
		}
	);
}

public static void dimensions(final Element element, final float width, final float height) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setWidth(width * getContainerWidth());
				element.setHeight(height * getContainerHeight());
				log("dimensions - width: {0}, height: {1}", width, height);
			}
		}
	);
}

public static void width(final Element element, final float width) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setWidth(width * getContainerWidth());
				log("width: {0}", width);
			}
		}
	);
}

public static void height(final Element element, final float height) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(height * getContainerHeight());
				log("height: {0}", height);
			}
		}
	);
}

public static void fillWidth(final Element element, final float leftMargin, final float rightMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(leftMargin * getContainerWidth());
				element.setWidth(getContainerWidth() - (leftMargin + rightMargin) * getContainerWidth());
				log("fillWidth - leftMargin: {0}, rightMargin: {1}", leftMargin, rightMargin);
			}
		}
	);
}

public static void fillWidth(final Element element, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(margin * getContainerWidth());
				element.setWidth(getContainerWidth() - (2 * margin * getContainerWidth()));
				log("fillWidth - margin: {0}", margin);
			}
		}
	);
}

public static void fillHeight(final Element element, final float bottomMargin, final float topMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setY(bottomMargin * getContainerHeight());
				element.setHeight(getContainerHeight() - (bottomMargin + topMargin) * getContainerHeight());
				log("fillHeight - bottomMargin: {0}, topMaring: {1}", bottomMargin, topMargin);
			}
		}
	);
}

public static void fillHeight(final Element element, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setY(margin * getContainerHeight());
				element.setHeight(getContainerHeight() - (2 * margin * getContainerHeight()));
				log("fillHeight - margin: {0}", margin);
			}
		}
	);
}

public static void fill(final Element element, final float leftMargin, final float bottomMargin, final float rightMargin, final float topMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(leftMargin * getContainerWidth());
				element.setY(bottomMargin * getContainerHeight());
				element.setWidth(getContainerWidth() - (leftMargin + rightMargin) * getContainerWidth());
				element.setHeight(getContainerHeight() - (bottomMargin + topMargin) * getContainerHeight());
				log("fill - leftMargin: {0}, bottomMargin: {1}, rightMargin: {2}, topMargin: {3}", leftMargin, bottomMargin, rightMargin, topMargin);
			}
		}
	);
}

public static void fill(final Element element, final float horizontalMargin, final float verticalMargin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(horizontalMargin * getContainerWidth());
				element.setY(verticalMargin * getContainerHeight());
				element.setWidth(getContainerWidth() - (2 * horizontalMargin * getContainerWidth()));
				element.setHeight(getContainerHeight() - (2 * verticalMargin * getContainerHeight()));
				log("fill - horizontalMargin: {0}, verticalMargin: {1}", horizontalMargin, verticalMargin);
			}
		}
	);
}

public static void fill(final Element element, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setX(margin * getContainerWidth());
				element.setY(margin * getContainerHeight());
				element.setWidth(getContainerWidth() - (2 * margin * getContainerWidth()));
				element.setHeight(getContainerHeight() - (2 * margin * getContainerHeight()));
				log("fill - margin: {0}", margin);
			}
		}
	);
}

public static void extendUp(final Element element, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(getContainerHeight() - (element.getY() + margin * getContainerHeight()));
				log("extendUp - margin: {0}", margin);
			}
		}
	);
}

public static void extendUpTo(final Element element, final float margin, final Element target) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(target.getY() - element.getY() - margin * getContainerHeight());
				log("extendUpTo - margin: {0}, target: {1}", margin, target);
			}
		}
	);
}

public static void extendRight(final Element element, final float margin) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setWidth(getContainerWidth() - (element.getX() + margin * getContainerWidth()));
				log("extendRight - margin: {0}", margin);
			}
		}
	);
}

public static void extendRightTo(final Element element, final float margin, final Element target) {
	element.addControl(
		new LayoutHelper() {
			@Override
			public void afterParent() {
				element.setHeight(target.getX() - element.getX() - margin * getContainerWidth());
				log("extendRightTo - margin: {0}, target: {1}", margin, target);
			}
		}
	);
}

}
[/java]

1 Like

Wow! This is really nice! Anxious to play around with it tomorrow!

EDIT: As for the flipped Y axis… initially, I was planning for top down throughout, however, this became more and more impossible as things progressed. I left it in place for initialization to make layouts easier… people always have to build ui components once… they don’t always modify them after the fact. It nags at me now and then… but works after the initial culture shock.

Well… the discrepancy had me confused for a while… :-?

As for the code, it hasn’t been well tested and I’m sure there are some cut and paste errors in there. Also, the layout functions I implemented are pretty basic. But I think that someone could build a number of different sophisticated layout systems using LayoutHelper as the foundation.

== Mike ==

It looks like we have similar goals. I also have a bunch of tonegodgui extensions to help with layout (long and boring post here). Mine is too much code to just paste in the forums, so it has it’s own home linked to in that post.

I started off as you did, with a load of static helper classes to do layout, but it got unweildy quickly. So this kind of morphed into a Swing like LayoutManager system.

Btw, one of the layout manager implementations (the brilliant MigLayout) can take measurements in points, taking the DPI into account.

Perhaps something common can be hammered out, there is obviously a need for it.

RR

My goal isn’t to create a new layout system. As you allude to, there are plenty of them out there already.

I just wanted to find a good way to implement my app’s layout on tonegodGUI. In fact, I probably wouldn’t buy into a complicated layout system even if it existed. I agree that all the static methods I put together may be going to far in that direction…

What I like about the LayoutHelper based controller approach is that something like that can support and integrate many different layout systems. Apps can pick and choose between simple, complex, and custom layout behaviors.

Personally I feel that the most usable apps make use of a few simple layout principles and apply them consistently. I expect my app to have it’s own specialized/focused/limited system and I wouldn’t be surprised if many apps follow the same pattern.

So, to put this all another way, my need was for a framework for implementing custom layouts rather than for a full featured layout system itself.

@eyeree said:

My goal isn’t to create a new layout system. As you allude to, there are plenty of them out there already.

I just wanted to find a good way to implement my app’s layout on tonegodGUI. In fact, I probably wouldn’t buy into a complicated layout system even if it existed. I agree that all the static methods I put together may be going to far in that direction….

I understand and agree. I ported my GUI from Nifty which has layout features, I just wanted something that worked and didn’t feel like a step backwards (this area is the only thing is dislike about tonegodgui). Had your solution existed at the time, I probably would have used it :wink:

So, to put this all another way, my need was for a framework for implementing custom layouts rather than for a full featured layout system itself.

At the end of the day this is what I want too really. If there right hooks were available in tonegodgui (and I do like your “Control” approach), I would totally use it and you wouldn’t hear another peep from me :slight_smile:

I’m really liking this and would like to include it in the library as a standard layout manager (if you are okay with that…). Just let me know!

–Yep - I posted this to both layout manager suggestions :wink: Because I like both and would like to offer both as options to people. Let me know if you are ok aywith this as well…

The other is a wrapper that is pretty easy to use, however… this is very robust and offers quite a bit of options without hiding the underpinning UI components.

Since you both are posting to this thread… this is the one I am going to reference in atm

This is @rockfire and @eyeree

Unless you all are interested in collaborating to design a single “best of both worlds” solution, I think offering both would be quite useful.

Sure, it’s all yours. I’ve pasted a version below that is slightly improved. The order that the LayoutHelper methods is called is consistent between the top level object and it’s children and unnecessary restriction on only laying out screen level elements has been removed. It also checks for remaining NaN values after layout is complete.

I’m probably not going to have much to offer as far as putting together a complete layout system however. I just don’t have the time or, really, the interest. You and/or rockfire are welcome to apply what I’ve posted in whatever way you see fit.

To be honest, over the last couple days I’ve put together a generic version of LayoutHelper (not dependent on Element, but using Spatial and Node). I also created a very light weight texture atlas geometry and material class, inspired by the core of your API, but without any inherent concept of layout or input handling. I wanted to figure out how all this stuff really worked (I’m new to graphics programming), and your code helped a lot.

But I’m also inspired by how JME3’s control objects allow all the different features needed for a gui to be factored out into independent classes. Controls for layout, controls for input, controls for dragging and resizing, etc. That way an app can pay (in terms of resources used) only for what they really need, pick and choose between lots of different behaviors provided by lots of different developers, and easily add their own customizations.

I can post this code too, but I really don’t want to be seen as trying to promote a competing framework or anything like that. I’m just doing what I need for my app, which doesn’t include much in the way of traditional layout or controls, but which can benefit from a lightweight, composable, and customizable control based framework for gui development.

[java]
import java.util.logging.Level;
import java.util.logging.Logger;

import tonegod.gui.core.Element;

import com.jme3.math.Vector2f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;

public abstract class LayoutHelper extends AbstractControl {

private final Logger log = Logger.getLogger(this.getClass().getName());
private final static Level LOG_LEVEL = Level.FINE;

private Element element;
		
/**
 * Override to update element's layout before the parent's layout has been updated.
 * Use this when the parent's layout depends on the layout of the child (e.g. size
 * panel to fit children).
 */
protected void beforeParent() {};

/**
 * Override to update element's layout to reflect changes in child layout but before
 * the parent's layout is updated.
 */
protected void afterChildren() {}

/**
 * Override to update element's layout after the parent's layout has been updated.
 * Use this when the child's layout depends on the layout of the parent (e.g. size
 * children to fit in panel).
 */
protected void afterParent() {};

/**
 * Width of parent element or screen.
 */
public float getContainerWidth() {
	Element parent = element.getElementParent();
	if(parent != null) {
		return parent.getWidth();
	} else {
		return element.getScreen().getWidth();
	}
}

/**
 * Height of parent element or screen.
 */
public float getContainerHeight() {
	Element parent = element.getElementParent();
	if(parent != null) {
		return parent.getHeight();
	} else {
		return element.getScreen().getHeight();
	}
}

/**
 * Write a log message that includes the element's position, dimension, and id.
 */
protected void log(String msg, Object ... params) {
	if(log.isLoggable(LOG_LEVEL)) {
		log.log(LOG_LEVEL, msg + " -&gt; position: " + element.getPosition() + ", dimensions: " + element.getDimensions() + " on " + element, params);
	}
}

@Override
public void setSpatial(Spatial spatial) {
	
	if(!(spatial instanceof Element)) {
		throw new RuntimeException("Wrong type of spatial");
	}
	
	this.element = (Element)spatial;
	
	super.setSpatial(spatial);
	
}

@Override
protected void controlUpdate(float tpf) {
}

@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
}

/**
 * Used to initialize the position of an element that will be
 * positioned automatically by LayoutHelper. The returned vector's
 * values are both NaN. The layout method checks for remaining
 * NaN values after layout is complete, so you'll know if you
 * forgot to add a layout handler for an element.
 */
public static Vector2f autoPosition() {
	return new Vector2f(Float.NaN, Float.NaN);
}

/**
 * Used to initialize the dimension of an element that will be
 * positioned automatically by LayoutHelper. The returned vector's
 * values are both NaN. The layout method checks for remaining
 * NaN values after layout is complete, so you'll know if you
 * forgot to add a layout handler for an element.
 */
public static Vector2f autoSize() {
	return new Vector2f(Float.NaN, Float.NaN);
}

/**
 * Layout an element tree by recursively calling the beforeParent, afterChildren,
 * and afterParent methods on all LayoutHelper control objects attached the each 
 * element.
 * &lt;p&gt;
 * The specified element needs to be added it it's container element or to the 
 * screen before calling layout. If the element has a parent element, it should 
 * have its position and dimension set before layout is called.
 * &lt;p&gt;
 * The elements at each level are visited in the order they were added to their
 * container. Each LayoutHelper control is visited in the order the control was
 * added to the element.
 * &lt;p&gt;
 * After afterParent is called for an element the element's position and 
 * dimensions are checked for any NaN values left over from the vectors used to
 * initialize these values via the autoPosition and autoDimension methods.
 */
public static void layout(Element element) {
	
	// call beforeParent and afterChildren for child elements recursively
	for(Spatial spatial : element.getChildren()) {
		if(spatial instanceof Element) {
			visitBeforeParent((Element)spatial);
		}
	}
	
	// call afterChildren for current element
	for(int i = 0; i &lt; element.getNumControls(); ++i) {
		Control control = element.getControl(i);
		if(control instanceof LayoutHelper) {
			((LayoutHelper)control).afterChildren();
		}
	}
	
	// call afterParent (the parent is the screen) for current element
	for(int i = 0; i &lt; element.getNumControls(); ++i) {
		Control control = element.getControl(i);
		if(control instanceof LayoutHelper) {
			((LayoutHelper)control).afterParent();
		}
	}
	
	checkInitialized(element);
	
	// call afterParent for child elements recursively
	for(Spatial spatial : element.getChildren()) {
		if(spatial instanceof Element) {
			visitAfterParent((Element)spatial);
		}
	}
	
}

private static void visitBeforeParent(Element element) {

	// beforeParent for current element
	for(int i = 0; i &lt; element.getNumControls(); ++i) {
		Control control = element.getControl(i);
		if(control instanceof LayoutHelper) {
			((LayoutHelper)control).beforeParent();
		}
	}
	
	// call beforeParent and afterChildren for child elements recursively
	for(Spatial spatial : element.getChildren()) {
		if(spatial instanceof Element) {
			visitBeforeParent((Element)spatial);
		}
	}
			
	// call afterChildren for current element
	for(int i = 0; i &lt; element.getNumControls(); ++i) {
		Control control = element.getControl(i);
		if(control instanceof LayoutHelper) {
			((LayoutHelper)control).afterChildren();
		}
	}
	
}	

private static void visitAfterParent(Element element) {
	
	// afterParent for current element
	for(int i = 0; i &lt; element.getNumControls(); ++i) {
		Control control = element.getControl(i);
		if(control instanceof LayoutHelper) {
			((LayoutHelper)control).afterParent();
		}
	}
	
	checkInitialized(element);
			
	// afterParent for child elements recursively
	for(Spatial spatial : element.getChildren()) {
		if(spatial instanceof Element) {
			visitAfterParent((Element)spatial);
		}
	}
			
}

private static void checkInitialized(Element element) {
	if(
		Float.isNaN(element.getX()) || 
		Float.isNaN(element.getY()) ||
		Float.isNaN(element.getWidth()) ||
		Float.isNaN(element.getHeight())
	) {
		throw new RuntimeException(
			"Element " + element + " has uninitialized layout properties - position: " + element.getPosition() + " dimensions: " + element.getDimensions()
		);
	}
}

protected void checkInitialized(
	Element element, 
	float value,
	String valueDescription, 
	String layoutDescription
) {
	if(Float.isNaN(value)) {
		throw new RuntimeException(
			"Element " + element + " " + valueDescription + " is needed for " + layoutDescription + " but hasn't been initialized."
		);
	}
}

}
[/java]

1 Like

Hi,

Last week I did some experimentation with tonegodgui and layout. After regarding both thread and overlook the contributions from eyeree and rockfire, I try to integrate MigLayout with tonegodgui as a Control.

The implementation is incomplete, but I was able to layout a several Label in the center of the screen with the following code.

[java]
Element el = new Element(screen, UUID.randomUUID().toString(), new Vector2f(0, 0), new Vector2f(screen.getWidth(), screen.getHeight()), Vector4f.ZERO, null);
LayoutMig container = new LayoutMig();
el.addControl(container);
container.setConstraints("", “push[]push”, “push[]push”);
for (int i = 0; i < 5; i++) {
Label l = new Label(screen, Vector2f.ZERO, Vector2f.ZERO);
l.setText(“label_” + i);
//l.setColorMap(null);
//l.setTextAlign(BitmapFont.Align.Center);
container.add(l, “cell 0 0, flowy, growx”);
}
[/java]

The implementation is available as gist of https://gist.github.com/davidB/fe7c00c390243f672a2c.

This week I’ll play with Lemur (I also try to adapt MigLayout).

  • This is my first post into the forum -
1 Like
@david.bernard.31 said: Hi,

Last week I did some experimentation with tonegodgui and layout. After regarding both thread and overlook the contributions from eyeree and rockfire, I try to integrate MigLayout with tonegodgui as a Control.

The implementation is incomplete, but I was able to layout a several Label in the center of the screen with the following code.

[java]
Element el = new Element(screen, UUID.randomUUID().toString(), new Vector2f(0, 0), new Vector2f(screen.getWidth(), screen.getHeight()), Vector4f.ZERO, null);
LayoutMig container = new LayoutMig();
el.addControl(container);
container.setConstraints(“”, “pushpush”, “pushpush”);
for (int i = 0; i < 5; i++) {
Label l = new Label(screen, Vector2f.ZERO, Vector2f.ZERO);
l.setText(“label_” + i);
//l.setColorMap(null);
//l.setTextAlign(BitmapFont.Align.Center);
container.add(l, “cell 0 0, flowy, growx”);
}
[/java]

The implementation is available as gist of https://gist.github.com/davidB/fe7c00c390243f672a2c.

This week I’ll play with Lemur (I also try to adapt MigLayout).

  • This is my first post into the forum -

Sorry about the layout implementation… they are not overlooked… they are on the todo list… I just haven’t had time to focus on this yet. And thanks for this effort as well. Definitely more food for thought when I’m able to focus on this!! Plus, I’ve been anxious to try out Lemur and tonegodGUI together.

Currently, I’m:

  1. Still in the process of the 2D Framework integration.
  2. Then adding the ToolTip provider update from @rockfire
  3. Looking at the layout implementations that have been shared and hoping that those who worked on them would be willing to decide on a common implementation that makes it as easy as possible for the end user and sstill provides all needed functionality.

I also get a bit sidetracked with issues that come up that need to be resolved, etc and take priority over additions to the library.

And now and then I try and squeeze in some time to work on personal projects (like the FX Builder, Particle System, Deferred Rendering implementation… and an actual game if I get really lucky) just to keep my sanity. =)

I’m looking forward to trying this out (and should be able to sometime tonight or tomorrow!).

@t0neg0d

I provide the code as gist to provide a simple base code (to complete , to adapt) for user that don’t need features of @rockfire 's lib.
I’ll probably use Lemur as my main GUI. It has less doc, less components (widgets, effects) than tonegodgui but after the bootstart I understand more how to create create custom widget.

You do an amazing work (code, support,…), Thanks a lot. I’m in standby about your projects, mainly by Deferred Rendering (I hope you’ll share it).

@david.bernard.31 said: @t0neg0d

I provide the code as gist to provide a simple base code (to complete , to adapt) for user that don’t need features of @rockfire 's lib.
I’ll probably use Lemur as my main GUI. It has less doc, less components (widgets, effects) than tonegodgui but after the bootstart I understand more how to create create custom widget.

You do an amazing work (code, support,…), Thanks a lot. I’m in standby about your projects, mainly by Deferred Rendering (I hope you’ll share it).

Awesome! Do keep everyone updated on how things go with Lemur, as I haven’t seen a lot of activity on this atm and am very interested in how things go.

I also wanted to make some wuick mentions to the above implementation of Layouts as things to keep in mind that should be avoided (ESPECIALLY when developing for Android).

In one of the above posts (and in the code above) the use of controls is HEAVY. This is generally a terrible idea with Android as it will slow down your game loop dramatically (logic and render wise) even if the update methods of the controls are empty. This is the entire reason I avoided using the standard method for event handling (i.e. I don’t register listeners and then blindly broadcast to all listeners, letting the listener decide if it needs the message).

This is something you should keep in mind as you continue with your effort. You may not notice a difference with a simple UI, but as soon as the UI become complex it will have a dramatic impact on your frame rate as the event handling is also propagated on the update loop. This also applies to Control update calls.

For Android, a simple test to see this in action would be:

Test 1:

  1. Create a new project and add 200-400 quads to the gui node.

Test 2:

  1. Create a generic control that does nothing but add a quad to the gui node.
  2. Add 200-400 of these controls (as controls).

Then, take a look at the difference in performance.

As cool as the idea above above compartmentalizing everything sounds. In practical application (keeping mobile dev in mind), it is simply not the best approach (unfortunately).

Anyways… just thought I would share thoughts on this as it might save you from making mistakes that will be hard to undo later.

1 Like
@david.bernard.31 said: Hi,

Last week I did some experimentation with tonegodgui and layout. After regarding both thread and overlook the contributions from eyeree and rockfire, I try to integrate MigLayout with tonegodgui as a Control.

The implementation is incomplete, but I was able to layout a several Label in the center of the screen with the following code.

[java]
Element el = new Element(screen, UUID.randomUUID().toString(), new Vector2f(0, 0), new Vector2f(screen.getWidth(), screen.getHeight()), Vector4f.ZERO, null);
LayoutMig container = new LayoutMig();
el.addControl(container);
container.setConstraints(“”, “pushpush”, “pushpush”);
for (int i = 0; i < 5; i++) {
Label l = new Label(screen, Vector2f.ZERO, Vector2f.ZERO);
l.setText(“label_” + i);
//l.setColorMap(null);
//l.setTextAlign(BitmapFont.Align.Center);
container.add(l, “cell 0 0, flowy, growx”);
}
[/java]

The implementation is available as gist of https://gist.github.com/davidB/fe7c00c390243f672a2c.

This week I’ll play with Lemur (I also try to adapt MigLayout).

  • This is my first post into the forum -

This sounds cool :slight_smile: I’ll try this soon. I have some places it might be more appropriate than my more heavyweight attempt. There isn’t many layout styles Mig can’t do. I’ve seriously considered removing the others I implemented and sticking with that only. Or maybe making wrappers that use Mig underneath.

I’ve been TRYING to get my stuff packaged up as a JME plugin module so it can be added like tonegod itself for easy use, and so far, it’s proving, err, troublesome.

@t0neg0d said: I also wanted to make some wuick mentions to the above implementation of Layouts as things to keep in mind that should be avoided (ESPECIALLY when developing for Android).

In one of the above posts (and in the code above) the use of controls is HEAVY. This is generally a terrible idea with Android as it will slow down your game loop dramatically (logic and render wise) even if the update methods of the controls are empty. This is the entire reason I avoided using the standard method for event handling (i.e. I don’t register listeners and then blindly broadcast to all listeners, letting the listener decide if it needs the message).

This is something you should keep in mind as you continue with your effort. You may not notice a difference with a simple UI, but as soon as the UI become complex it will have a dramatic impact on your frame rate as the event handling is also propagated on the update loop. This also applies to Control update calls.

For Android, a simple test to see this in action would be:

Test 1:

  1. Create a new project and add 200-400 quads to the gui node.

Test 2:

  1. Create a generic control that does nothing but add a quad to the gui node.
  2. Add 200-400 of these controls (as controls).

Then, take a look at the difference in performance.

As cool as the idea above above compartmentalizing everything sounds. In practical application (keeping mobile dev in mind), it is simply not the best approach (unfortunately).

Anyways… just thought I would share thoughts on this as it might save you from making mistakes that will be hard to undo later.

Very interesting info, thanks for sharing.

I admit, I didn’t take care of Android (not in my current target) or low configuration (I come to jme3 after 6+ month working with webgl + dart, due to webgl limitations). But I should take care of performance, so my workaround idea (not tested), if it’s possible try to reduce (or remove call to update), because gui doesn’t need to be updated every frame :

  • find a way to reduce “update” of a subgraph of guiNode
  • or doesn’t store Layout as Control but as UserData and explicitly call layout() (eg: once when component are added).

From my current understanding, Lemur use the Control approach (via GuiControl).

@david.bernard.31 said: Very interesting info, thanks for sharing.

I admit, I didn’t take care of Android (not in my current target) or low configuration (I come to jme3 after 6+ month working with webgl + dart, due to webgl limitations). But I should take care of performance, so my workaround idea (not tested), if it’s possible try to reduce (or remove call to update), because gui doesn’t need to be updated every frame :

  • find a way to reduce “update” of a subgraph of guiNode
  • or doesn’t store Layout as Control but as UserData and explicitly call layout() (eg: once when component are added).

From my current understanding, Lemur use the Control approach (via GuiControl).

This has been my biggest holdback on actually implementing a layout manager… and thought the info would be more useful to someone who is working on it now. Well, this and how to best wrap resize into the layout provider and remove it from Screen. Anyways… hopefully something will strike me soon(ish) that both works well and doesn’t unintentionally tank Android fps.

@david.bernard.31 said: From my current understanding, Lemur use the Control approach (via GuiControl).

Probably off topic, but just a note that this is because in Lemur any spatial can be a GUI element. You can load the Sinbad model, slap a GuiControl on him and now he’s a GuiElement. I mean, resize() won’t affect him without some tweaking but if you give him a calculated preferred size you can plop him right into any layout.

GUIs that predefine a strict class hierarchy probably don’t need a control.

Hi,

I made an implementation that use UserData to store LayoutMig, see <a href="
https://gist.github.com/davidB/b7565de511ae7d2f6edf">code (an other gist) . There some issues :

  • no auto-layout when “resize” children’s element => explicit call of layout()
  • doesn’t support “detach” of the layout from an Element (TODO)
  • it should no longer store the Layout’s info as UserData of children’s Element (TODO)
  • not tested on Android (TODO)
  • not tested with lot of element (TODO)
  • may be lot of more …

[java]
LayoutMig container = LayoutMig.newContainer(screen);
container.setConstraints("", “push[]push”, “push[]push”);
for (int i = 0; i < 5; i++) {
Label l = new Label(screen, Vector2f.ZERO, Vector2f.ZERO);
l.setText(“label_” + i);
//l.setColorMap(null);
//l.setTextAlign(BitmapFont.Align.Center);
container.add(l, “cell 0 0, flowy”).setPreferredDimensions(findDimensionMin(l));
}
container.layout();
screen.addElement(container.element);
[/java]

1 Like
@pspeed said: Probably off topic, but just a note that this is because in Lemur _any_ spatial can be a GUI element. You can load the Sinbad model, slap a GuiControl on him and now he's a GuiElement. I mean, resize() won't affect him without some tweaking but if you give him a calculated preferred size you can plop him right into any layout.

GUIs that predefine a strict class hierarchy probably don’t need a control.

The is possible with tonegodGUI as well now. It still doesn’t have to broadcast events.