Styling - jMonkeyEngine-Contributions/Lemur GitHub Wiki

Styling

Lemur uses an advanced styling system that is similar in concept to web page cascading style sheets. GUI element attributes can be inherited from more general styling information or element-specific styling can override the more general styling.

For a custom GUI element to support styling its simply a matter of annotating the appropriate set methods with StyleAttribute annotations and then making sure to call styles.applyStyles() during construction.

Concepts

An element's style is defined by both its 'element ID' as well as its 'style name'. A default style name can be setup that all new GUI elements will use if a specific style has not been specified. The 'element ID' determines which set of style attributes apply in a 'cascading' or 'inherited' fashion.

CSS-like but different. Cascading style sheets have a similar concept of inheritance or containment but in the case of CSS this is based on page layout. A style can apply to all paragraphs or paragraphs only under a particular class of 'div' and so on. In Lemur, style is applied before the element has even been added to a GUI and so the cascading is based directly on the element ID and its dotted sub-parts.

Note: Lemur's default GUI Elements adopt Swing's concept of "insets" and "margins" rather than CSS's concept of "padding" and "margins". To those familiar with CSS, margins and insets will seem backwards. "Insets" define how much space a GUI element has around it within its parent layout. "Margin" defines how much space a GUI element has inside its own border/background layer.

Element IDs

An element ID defines a GUI elements place in the style hierarchy. Whether it's just a "button" or it's a "slider.thumb.button" or a "megaSlider.thumb.button" or just a "menu.button", each of these things might require different style attributes while inheriting some common attributes from the regular "button" style.

In general, it's best to define element ID's as container.contained.contained where generally the first part is defined by the thing creating the element. In fact, the ElementId class makes this easy by providing a convenient child(childId) method.

The actual attributes that apply for a given ElementID will be determined by the 'selectors' that have been configured.

Selectors

A selector defines a pattern to which a set of style attributes will apply. The simple selectors will be based just on 'style name' or a simple ID like 'button'. More complicated containment-based styles are also supported.

For brevity, this document will use the style language way to specify selectors when presenting examples. The Java code version is not really different but is slightly more verbose.

selector(styleName) The simplest selector is the style name selector. This will apply a particular set of style attributes to any element of the specified style. A null style indicates that the attributes will apply as defaults to all styles.

selector(elementId, styleName) The next simplest selector applies to any element with the specified elementId and styleName. Again, a styleName of null sets up a default for all elements with the elementId, regardless of style. The elementId in this case, is the 'tail' of the match. In other words, selector("button", "glass") would match any of "button", "slider.thumb.button", "menu.button", and so on, if they have the 'glass' style.

selector(parentId, elementId, styleName) This is the most advanced and allows a style to only apply to a particular elementId and styleName if it is 'contained' in a particular parentId. 'Contained' in this case means that the full elementId has the parentId somewhere earlier in its value. In other words, you can think of it like parentId.*.elementId. For example, selector("slider", "button", "glass") would match all of a Slider element's buttons, including: "slider.thumb.button", "slider.up.button", "slider.down.button", "slider.left.button", and so on.

Attributes

When setting up styles, attributes can be any name the caller wishes but when styles are applied the will only match with element properties that have been properly annotated. The particular name of a styled attribute matches the setter name by convention but it need not. It's always best to check the annotations in the javadoc for a particular element's properties when in doubt.

Example javadoc: Panel.setBackground()

setBackground

@StyleAttribute(value="background", lookupDefault=false)
public void setBackground(GuiComponent bg)|

Precedence

This is the tricky part to predict sometimes but generally the styling library tries to have the 'most specific' styling override the more general styling. Meaning that a selector for "button" is more general than a selector for "thumb.button" which is more general than a selector for "slider.thumb.button". It gets more complicated for containment selectors but the general idea holds true. Longer matching segments mean 'more specific', especially if they are in the tail. For really complicated styling needs, it might be necessary to turn on trace logging for the styler just to see how they are applied. Else, just go ahead and be more specific in the styling definition and rely on cascading less in some ambiguous looking case.

Internally, an attribute set for a specific full elementId is assembled by ordering the matching selectors from most specific to least specific and then building up a set of attribute values only if those values haven't already been set.

Example:

selector("glass") {
    fontSize=20
}
selector("button", "glass") {
    color = color(0.5, 0.75, 0.75, 0.85)     
}
selector("slider", "button", "glass") {
    fontSize=10
}

Should result in the following test-cases:

ElementId("label")

  • fontSize=20

ElementId("button")

  • fontSize=20
  • color=color(0.5, 0.75, 0.75, 0.85)

ElementId("slider.thumb.button")

  • fontSize=10
  • color=color(0.5, 0.75, 0.75, 0.85)

..and hopefully you can start to see the flexibility.

Setting Up Styles in Code

This section really assumes that you are already familiar with the concepts described above. If you've managed to skip that then there is much of this that won't make any sense and it's recommended that you skim it at least.

To setup styles in code, first obtain the Attributes object for a particular selector pattern. Once the Attributes object is obtained, simply set any attribute values. Cloneable values will automatically be cloned when applied to a particular target. This allows more complex objects like GuiComponents to be set as style attributes without worry about state being shared.

The following example mirrors the simplified example in the Precedence section with a few enhancements to show more advanced objects like fonts and components:

Styles styles = GuiGlobals.getInstance().getStyles();
Attributes attrs;
attrs = styles.getSelector("glass");
attrs.set("fontSize", 14);

QuadBackgroundComponent bg = new QuadBackgroundComponent(new ColorRGBA(0, 1, 0, 1));
attrs = styles.getSelector("button", "glass");
attrs.set("color", new ColorRGBA(0.5f, 0.75f, 0.75f, 0.85f));
attrs.set("background", bg);

BitmapFont font = assetManager.loadFont("myFont.fnt");
attrs = styles.getSelector("slider", "button", "glass");
attrs.set("fontSize", 10);
attrs.set("font", font);

Using the Style Language

If the groovy jar (groovy-all-version.jar) is in the dependencies for the project then an application can use the built-in styling language. This styling language extends groovy to provide an experience more like CSS and includes some nice built-in functions as well.

Following is a styling example that mirrors the one in the Setting Up Styles in Code section:

import com.simsilica.component.*;

selector('glass') {
    fontSize = 20
}
selector('button', 'glass') {
    background = new QuadBackgroundComponent(color(0, 1, 0, 1))
    color = color(0.5, 0.75, 0.75, 0.85)
}
selector('slider', 'button', 'glass') {
    fontSize = 10
    font = font('myFont.fnt')
}

Clearly it's much less verbose and to the point. Also, by using the styling language it allows an application to easily take advantage of the support for auto-loading Style Resources.

Built-in Functions

This list is enhanced all the time but here are the current set of built-in functions:

  • font(name): loads a font file from the AssetManager.
  • color(r, g, b, a): creates a new ColorRGBA using the specified values.
  • texture(name): loads a texture from the AssetManager.
  • texture(args): loads a texture from the AssetManager and applies any specified attributes post-loading. The built in parameters are 'name' and 'generateMips' that are used to actually load the texture. After that, any standard JME texture property can be set through these args. For example:
    texture([name:"textures/myTexture.png", generateMips:false, wrap:WrapMode.Repeat])
  • vec3(x,y,z): creates a Vector3f object from the specified x,y,z values.
  • vec2(x,y): creates a Vector2f object from the specified x,y values.

Usage

To directly load your own style, use one of the loadStyle() methods on StyleLoader.

new StyleLoader().loadStyle(styleFile.toString(), new FileReader(styleFile));

...or set your style up as a class resource (or asset) and use the BaseStyles class to load it.

BaseStyles.loadStyleResources("styles/myCoolStyle.groovy");

Style Resources

Within the style library is a way to easily load and extend a style using class resource files. Applications can easily embed their style files directly in their jar or in their assets directory and then load them with the BaseStyles class the same way Lemur supports its own default built-in styles. All instances of a particular style resource are loaded and combined, thus it is easy to provide a base style in a library and extend or change just parts of it for a particular application.

The built-in styles use this same mechanism, so it's easy to extend even the built in styles.

For example:

BaseStyles.loadGlassStyle();

...is really just shorthand for:

BaseStyles.loadStyleResources("com/simsilica/lemur/style/base/glass-styles.groovy");

If an application creates their own com/simsilica/lemur/style/base/glass-styles.groovy file (note: path must match exactly) then that file will be loaded, too, and augment/replace anything in the Lemur default styling. (In fact, this is what the LemurProto library does to add additional styling for the prototype GUI elements.)

This is generally the best/easiest way to define a style and load it.