The styling support in Lemur is pretty powerful but can be a little intimidating to approach do to the extreme lack of documentation. I’ve had a few people ask me about how to make their GUIs look a bit nicer so I thought it was worth going over. At the end, I’ll even paste some of the “glass” style that I’ve been working on (which will someday be a default available style right in Lemur)
The Basics
Styles are a way to supply a default set of attribute values to a component based on its style and its element ID. When I wrote this, borrowed heavily from CSS. Some concepts may feel familiar if you are already familiar with CSS but there are enough differences to make it tricky either way.
Note: an important take-away is that anything you can do with styles you can also do by directly setting properties on the GUI elements, and vice-versa.
If you look at a GUI element like Panel, you can see that some of the setter methods are annotated with StyleAttribute annotations. This means that those methods can be set through a style.
For example:
@StyleAttribute(value="background", lookupDefault=false)
public void setBackground( GuiComponent bg ) {
This means that “background” is a style attribute for any Panel GUI element, or any element that extends Panel (which is currently all of the default Lemur GUI elements).
Setting up Styles Through Code
The entry point for setting up all style attributes is the Styles object. This can be obtained from the GuiGlobals instance and will contain all of the loaded or configured style attributes.
Styles styles = GuiGlobals.getInstance().getStyles();
Setting style attributes is then done through a “selector”. A selector is like a rule or pattern for how to match a particular style or element ID. The attributes associated with that selector will be applied to any matching GUI elements.
For example:
Attributes attrs = styles.getSelector("panel", null);
attrs.set("background", new QuadBackgroundComponent(ColorRGBA.Red);
…will set a red background for all components with the element ID “panel”. (presuming there isn’t a more specific style that overrides that)
It just so happens that Panel’s default element ID is “panel”. It’s generally safer to use the constants defined on each GUI element rather than refer to them directly as strings, but either way works. Here is an alternate version of the above:
Attributes attrs = styles.getSelector(Panel.ELEMENT_ID, null);
attrs.set(“background”, new QuadBackgroundComponent(ColorRGBA.Red);
The second parameter in those particular selector calls was the “style” ID. ‘null’ is the default style that elements get when they have no specific style defined.
The following will setup the font and font size for all Labels of the “crazy” style:
Attributes attrs = styles.getSelector(Label.ELEMENT_ID, “crazy”);
attrs.set(“font”, assetManager.loadFont(“Interface/ComicSans.fnt”);
attrs.set("fontSize, 64);
Note: if you leave out both the style and the element ID then you setup a rule that matches any GUI element. There is a method for easily getting the ‘wild card’ selector for a style. We could continue the “crazy” style by making every GUI element have a yellow background:
Attributes attrs = styles.getSelector(“crazy”);
attrs.set(“background”, new QuadBackgroundComponent(ColorRGBA.Yellow));
Resolving Selectors and ElementId Nesting
Above I implied in passing that there is a precedence order in how styles are applied. Mostly this is straight forward: the more specific the ‘selector’ is, the more precedence it has.
With the above, where I matched all “crazy” components and gave them a yellow background, it would be enough to single out an ElementId to override this behavior. For example, the following will make sure that “crazy” buttons have red backgrounds instead:
Attributes attrs = styles.getSelector(Button.ELEMENT_ID, “crazy”);
attrs.set(“background”, new QuadBackgroundComponent(ColorRGBA.Red));
Where it can get tricky is with nesting or wild-carding.
In CSS, it’s possible to have attributes apply to an element only when contained within another element. For example, on a web page you could have all paragraph elements turn their font red when underneath something with a ‘warning’ class ID.
Lemur supports a similar idea but it is confined to the element ID. (Styles are applied at creation time in Lemur and so it has no idea what the hierarchy is that it will be placed in and it may change anyway.) For composite elements, this is done using ‘dot notation’.
The Slider is the best example of this. Slider is just a composition or several other Lemur components with some custom code thrown in. The up/down/left/right buttons are just regular Lemur Button elements. The thumb is also just a regular Lemur Button element. However, it’s is extremely likely that you do not want these buttons to be styled like every other button… so they are given more specific ElementIds.
Here is how they break down:
Left button = “slider.left.button”
Right button = “slider.right.button”
Up button = “slider.up.button”
Down button = “slider.down.button”
Thumb button = “slider.thumb.button”
And finally, the panel between the up/down/left/right buttons that holds the thumb uses element ID “slider.range”.
Setting the individual attributes for all of these buttons for every style would be a pain. This is why Lemur supports a sort of wild-carding. In fact, we’ve already kind of seen it in action but we our element IDs have been simple up to now.
Following shows how to get wild-card or containment selectors for different parts of the Slider for our “crazy” style.
Attributes attrs = styles.getSelector(Slider.ELEMENT_ID, “button”, “crazy”);
attrs.set(“font”, assetManager.loadFont(“Interface/wingdings.fnt”));
attrs.set(“fontSize”, 12);
attrs.set(“background”, ColorRGBA.Green); // crazy
attrs = styles.getSelector(Slider.ELEMENT_ID, “thumb.button”, “crazy”);
attrs.set(“text”, “[]”);
attrs.set(“background”, ColorRGBA.Blue);
So, with that code, we’ve setup a crazy font for all buttons in the slider. We’ve also overridden this with a more specific style for the thumb button. It’s more specific because more of the pattern matches for that style than the other. “slider.thumb.button” would have been even more specific.
In general, what takes precedence is kind of logical. The longer the exact matches of a pattern or the closer to the end of the pattern they are, the more likely it is that they will override some other style. For example, (“slider.thumb”, “button”) would have also matched over simply (“slider”, “button”) because it is a more specific rule.
For the most part, you won’t have to worry about these patterns until you start creating your own composite GUI elements. In that case, it’s really nice to expose as much styling as possible by picking useful element ID nestings.
Setting Styles Through Style Files
Settings styles in Java can be a bit tedious. There is a lot of redundant boiler plate code. Lemur includes support for a style language based on the Groovy scripting language. To use this style language, you only need to have the groovy-all.jar in your projects dependencies.
Here is an example of loading a style file:
new StyleLoader(styles).loadStyleResource("Interface/crazy-style.groovy");
Within this file, attribute setting can use appropriate short-hand:
import com.simsilica.lemur.;
import com.simsilica.lemur.component.;style("crazy") {
background=new QuadBackgroundComponent(ColorRGBA.Yellow)
}
style(Label.ELEMENT_ID, "crazy") {
font = font("Interface/ComicSans.fnt")
fontSize = 64
}
…and so on.
Someday I would like to get rid of the requirement for importing those Lemur packages but it is nice that it clearly illustrates how one might easily import their own.
Glass Style
As part of the marching cubes demo and SimArboreal, I’ve been creating a nice “glass” style that I like. This will likely be the first style bundled into Lemur and available by default. Here is that style file in total, though note that it includes styling for non-core GUI elements like rollup panels, property panels, etc… I think it’s a nice example of a what a style file might look like in practice and is likely to generate lots of nice questions.
import com.simsilica.lemur.*;
import com.simsilica.lemur.Button.ButtonAction;
import com.simsilica.lemur.component.*;
def gradient = TbtQuadBackgroundComponent.create(
texture( name:"/com/simsilica/lemur/icons/bordered-gradient.png",
generateMips:false ),
1, 1, 1, 126, 126,
1f, false );
def bevel = TbtQuadBackgroundComponent.create(
texture( name:"/com/simsilica/lemur/icons/bevel-quad.png",
generateMips:false ),
0.125f, 8, 8, 119, 119,
1f, false );
def border = TbtQuadBackgroundComponent.create(
texture( name:"/com/simsilica/lemur/icons/border.png",
generateMips:false ),
1, 1, 1, 6, 6,
1f, false );
def border2 = TbtQuadBackgroundComponent.create(
texture( name:"/com/simsilica/lemur/icons/border.png",
generateMips:false ),
1, 2, 2, 6, 6,
1f, false );
def doubleGradient = new QuadBackgroundComponent( color(0.5, 0.75, 0.85, 0.5) );
doubleGradient.texture = texture( name:"/com/simsilica/lemur/icons/double-gradient-128.png",
generateMips:false )
selector( "glass" ) {
fontSize = 14
}
selector( "label", "glass" ) {
insets = new Insets3f( 2, 2, 0, 2 );
color = color(0.5, 0.75, 0.75, 0.85)
}
selector( "container", "glass" ) {
background = gradient.clone()
background.setColor(color(0.25, 0.5, 0.5, 0.5))
}
selector( "nestedProperties.container", "glass" ) {
background = border2.clone();
background.setColor(color(0.0, 0.0, 0.0, 0.5))
}
selector( "stats", "glass" ) {
background = gradient.clone()
background.setColor(color(0.25, 0.5, 0.5, 0.5))
}
selector( "slider", "glass" ) {
background = gradient.clone()
background.setColor(color(0.25, 0.5, 0.5, 0.5))
}
def pressedCommand = new Command<Button>() {
public void execute( Button source ) {
if( source.isPressed() ) {
source.move(1, -1, 0);
} else {
source.move(-1, 1, 0);
}
}
};
def stdButtonCommands = [
(ButtonAction.Down):[pressedCommand],
(ButtonAction.Up):[pressedCommand]
];
selector( "title", "glass" ) {
color = color(0.8, 0.9, 1, 0.85f)
highlightColor = color(1, 0.8, 1, 0.85f)
shadowColor = color(0, 0, 0, 0.75f)
shadowOffset = new com.jme3.math.Vector3f(2, -2, 1);
background = new QuadBackgroundComponent( color(0.5, 0.75, 0.85, 0.5) );
background.texture = texture( name:"/com/simsilica/lemur/icons/double-gradient-128.png",
generateMips:false )
insets = new Insets3f( 2, 2, 2, 2 );
buttonCommands = stdButtonCommands;
}
selector( "button", "glass" ) {
background = gradient.clone()
color = color(0.8, 0.9, 1, 0.85f)
background.setColor(color(0, 0.75, 0.75, 0.5))
insets = new Insets3f( 2, 2, 2, 2 );
buttonCommands = stdButtonCommands;
}
selector( "slider", "glass" ) {
insets = new Insets3f( 1, 3, 1, 2 );
}
selector( "slider", "button", "glass" ) {
background = doubleGradient.clone()
background.setColor(color(0.5, 0.75, 0.75, 0.5))
insets = new Insets3f( 0, 0, 0, 0 );
}
selector( "slider.thumb.button", "glass" ) {
text = "[]"
color = color(0.6, 0.8, 0.8, 0.85)
}
selector( "slider.left.button", "glass" ) {
text = "-"
background = doubleGradient.clone()
background.setColor(color(0.5, 0.75, 0.75, 0.5))
background.setMargin(5, 0);
color = color(0.6, 0.8, 0.8, 0.85)
}
selector( "slider.right.button", "glass" ) {
text = "+"
background = doubleGradient.clone()
background.setColor(color(0.5, 0.75, 0.75, 0.5))
background.setMargin(4, 0);
color = color(0.6, 0.8, 0.8, 0.85)
}
selector( "checkbox", "glass" ) {
def on = new IconComponent( "/com/simsilica/lemur/icons/Glass-check-on.png", 1f,
0, 0, 1f, false );
on.setColor(color(0.5, 0.9, 0.9, 0.9))
on.setMargin(5, 0);
def off = new IconComponent( "/com/simsilica/lemur/icons/Glass-check-off.png", 1f,
0, 0, 1f, false );
off.setColor(color(0.6, 0.8, 0.8, 0.8))
off.setMargin(5, 0);
onView = on;
offView = off;
color = color(0.8, 0.9, 1, 0.85f)
}
selector( "value", "label", "glass" ) {
insets = new Insets3f( 1, 2, 0, 2 );
textHAlignment = HAlignment.Right;
background = border.clone();
background.color = color(0.5, 0.75, 0.75, 0.25)
color = color(0.6, 0.8, 0.8, 0.85)
}
selector( "rollup", "glass" ) {
background = gradient.clone()
background.setColor(color(0.25, 0.5, 0.5, 0.5))
}
selector( "window", "glass" ) {
background = gradient.clone()
background.setColor(color(0.25, 0.5, 0.5, 0.5))
}
selector( "tabbedPanel", "glass" ) {
activationColor = color(0.8, 0.9, 1, 0.85f)
}
selector( "tabbedPanel.container", "glass" ) {
background = null
}
selector( "tab.button", "glass" ) {
background = gradient.clone()
background.setColor(color(0.25, 0.5, 0.5, 0.5))
color = color(0.4, 0.45, 0.5, 0.85f)
insets = new Insets3f( 4, 2, 0, 2 );
buttonCommands = stdButtonCommands;
}
Composite GUI Element IDs Strategy
Before I go, I thought I would mention a few more things. The first is just a general discussion on how one might go about naming the ElementIDs of any composite components that they create.
If you are creating a composite GUI element, it’s worth thinking for a minute how you might want styles applied. This will give you greater freedom in updating the look-and-feel later. My general advice, is to leave the base element IDs on the children at the end of their ID. So all buttons will still have “.button” on the end, all labels will still have “.label” on the end, etc… This gives you some latitude in how you might move around and rename things later.
Here is sort of a contrived example to illustrate my point. Let’s say you will be rolling together a “window” panel. You would like it to have a title, a place for other components to be placed, maybe some buttons at the bottom, etc… So you start out by extending Panel and giving it a BorderLayout. In the top you stick a Label with the elementID of just “title”.
Well, later you decide you want to give the title area its own background and stick some other buttons in it… now you are stuck. You will have to give your title label a new ID and go back and fix any of your styling to use the new name. If the Label had instead been given “title.label” as its ElementID then you’d be all set. You could easily create “title.button” or set the “title” background separately and so on. More over, if you decided to change the font of your whole GUI then “title.label” will already match any default changes to “label”. Furthermore, you might really want to have given it the element ID of “window.title.label” perhaps to distinguish it from other “title.label” objects you might create later.
The ElementId class that is used for the actual element IDs provides a convenient way to create these child IDs. Strangely enough, this method is called child(). It’s useful because if you were creating a Window class, you might also want to let the caller define their own ElementId… now you can conveniently do that:
this.title = new Label(“Hello, world”, elementId.child(“title”).child(“label”);
this.closeButton = new Button(“X”, elementId.child(“title”).child(“button”);
SimArboreal’s GUI code is available online and it’s the latest/best example of creating custom composite elements. In fact, the RollupPanel will likely be moved to Lemur core soon and I will probably adapt the PropertyPanel as a nice extension.
Using Styles for your Own Objects
I felt I couldn’t leave without at least mentioning this. The Styles API is 100% decoupled from Lemur itself. It is possible to use these classes to apply ‘styles’ to any Java object. As long as the object has the right StyleAttribute annotations, styles can be applied.
Details about this are off topic for this post but I couldn’t leave it unsaid.