Skip to content

Ultraviolet Style Sheets

Cole Campbell edited this page Mar 3, 2018 · 2 revisions

Ultraviolet Style Sheets (UVSS) is the language used by the Ultraviolet Presentation Foundation to apply visual styles to interface elements.

Styles

The syntax of the UVSS language is designed to be similar to that of Cascading Style Sheets (CSS). A UVSS file consists of a list of styles. Each style consists of a list of selectors which specify which elements the style targets, as well as a list of styling rules which modify the property values of targeted elements.

*
{
    font: #Global:Fonts:DefaultUI;
    foreground: #ffffffff;
}

The above is an example of a UVSS style. The selector here, *, is the universal selector: this style will apply to all elements in the view. Within the curly braces there are two styling rules: one which will set the value of the font property on elements which have it, and another which will set the value of the foreground property. Note that only dependency properties can be styled, and that we refer to them here by their styling names, rather than their standard names.

The values specified for styling rules must be strings which can be resolved to an instance of the property type by the Nucleus object resolver. Therefore they are equivalent to the values used in the XML loaded by data object registries. Note in particular that resources, such as the font specified in the above example, are referenced by their name in the game's content manifest.

In addition to the universal selector, there are a number of other selectors and operators which can be used to target elements based on different attributes that they possess.

A selector beginning with no special symbol targets an element's type. The styling system understands inheritance relationships, so a value of FrameworkElement will apply to elements of type Button, and so on.

ToggleButton { 
   /* targets all ToggleButton elements... 
    * ...INCLUDING all RadioButton and CheckBox elements (which inherit from ToggleButton) */ 
}

A type selector which ends with the ! operator disregards inheritance relationships and will only match an element of the exact type specified.

ToggleButton! { 
   /* targets all ToggleButton elements...
    * ...but NOT RadioButton or CheckBox elements (which inherit from ToggleButton) */ 
}

A selector beginning with the # symbol targets an element's name (the Name property of a FrameworkElement):

#foo { 
   /* targets an element named "foo" */ 
}

A selector beginning with the . symbol targets an element's classes (the Classes property of a UIElement):

.bar { 
   /* targets elements with the "bar" class */ 
}

You can combine these selectors together, just as you can in CSS:

button#foo.bar { 
   /* targets buttons named "foo" with class "bar" */ 
}

Putting a space between the selector parts indicates an ancestor/descendant relationship:

Button #foo .bar { 
   /* targets elements with class "bar" which are... 
    * ...descendants of elements named "foo" which are...
    * ...descendants of Buttons */
 }

You can also use the > operator to require a direct parent/child relationship (following the visual tree):

Button > #foo > .bar { 
   /* targets elements with class "bar" which are...
    * ...the immediate visual children of elements named "foo" which are...
    * ...the immediate visual children of Buttons */ 
}

You can use the >? operator to require a direct parent/child relationship (following the logical tree):

Button >? #foo >? .bar { 
   /* targets elements with class "bar" which are...
    * ...the immediate logical children of elements named "foo" which are...
    * ...the immediate logical children of Buttons */ 
}

You can use the >> operator to require a templated parent/child relationship:

button >> #foo >> .bar { 
   /* targets elements with class "bar" which are...
    * ...the templated children of elements named "foo" which are...
    * ...the templated children of Buttons */ 
}

Finally, you can apply one set of styling rules to multiple targets by separating selectors with commas:

button, #foo, .bar { 
   /* targets all buttons...
    * ...AND all elements named "foo"...
    * ...AND and all elements with the "bar" class /* 
}

Navigation Expressions

Sometimes, you need to style objects which are not elements, but rather are objects exposed by properties on elements. Consider, for example, a Button with an attached DropShadowEffect. You can style the effect's properties using a navigation expression, indicated by the pipe (|) operator combined with the type conversion operator (as).

#mybtn | effect as DropShadowEffect
{
    blur-radius: 2.0;
}

The type conversion operator is necessary because the Effect property is defined with the Effect type, which is the base class of DropShadowEffect. Therefore, we must specify the exact type in order to access the properties of DropShadowEffect.

Storyboards

The UVSS language also allows you to define storyboards which represent animations. A storyboard is declared by prepending the @ symbol to the storyboard's name, like so:

@storyboard name 
{
    /* etc */
}

A storyboard is played against a particular element. It can target various descendants of that root element in order to animate their properties; this list of targets is specified within the storyboard's curly braces.

@storyboard name
{
    target optional_type (selector)
    {
        /* etc */
    }
}

In the above code, the optional_type argument can be excluded, or it can be the type name of any element class, such as Button or CheckBox or, more broadly, UIElement. The animations specified within this target will only be played against elements of the specified type. If the type is excluded, it defaults to UIElement.

The selector argument is a normal UVSS selector like those described in the previous section; only elements matching this selector will be targeted.

Within each target is a list of animations; each animation specifies the name of the property which it animates and contains a list of keyframes which specify how the property's value evolves with time.

@storyboard name
{
    target optional_type (selector)
    {
        animation propname
        {
            keyframe time_in_ms { value }
        }
    }
}

The value specified in the keyframe can be left blank (i.e. keyframe 0 { }), in which case the property will animate back to its underlying, non-animated value.

Here's an example of an actual storyboard defined in the default style sheet, which is responsible for transitioning a button into its pressed state:

@button-to-pressed
{
    target control (*:storyboard-root)
    {
        animation foreground
        {
            keyframe 0 { #ffacb3bf }
        }
    }
    target image (*:storyboard-root > grid > .button-background)
    {
        animation source-image
        {
            keyframe 0 { #Global:Textures:DefaultUI 256 69 71 33 4 4 4 4 }
        }
    }
}

Note the use of the :storyboard-root pseudo-selector; this selector will only target the root element to which the storyboard as a whole was applied.

The primary use of storyboards is for transitions between visual states. Different elements can define different sets of these states, which are grouped together into visual state groups. The ButtonBase element class, for example, defines normal, hover, and pressed visual states, and moves between them in response to user input. You can specify that a storyboard should be played when an element transitions between states by using the transition keyword in a list of styling rules:

Button!, RepeatButton!, ToggleButton!
{
    transition (common, pressed): button-to-pressed;
}

The above specifies that the button-to-pressed storyboard should be played against a button whenever it moves into its pressed state - which is a part of the common visual state group - from any other visual state within that group. You can further restrain this by specifying that it should only run when a transition occurs between two specific states:

Button!, RepeatButton!, ToggleButton!
{
    transition (common, normal, pressed): button-normal-to-pressed;
}

Triggers

In addition to styles, UVSS can be used to create triggers. A trigger is a collection of actions which are performed in response to some particular event taking place.

Currently, UPF supports two types of triggers: property triggers and event triggers. A property trigger fires whenever a specified list of dependency properties are set to a particular range of values. An event trigger fires whenever a particular routed event is raised against the target element.

You can create a property trigger like this:

ToggleButton!
{
    trigger property checked = { false }, mouse-over = { true }
    {
        set source-image (*:trigger-root > grid > .button-background) { #Global:Textures:DefaultUI 256 35 71 33 4 4 4 4 }	
    }
}

This trigger will fire when every condition in its condition list is satisfied; that is, the checked property of the trigger target must be false, and the mouse-over property of the trigger target must be true.

You can create an event trigger like this:

Button!, RepeatButton!, ToggleButton!
{
    trigger event mouse.mouse-enter
    {
        play-sfx { #Global:SoundEffects:Rollover3 }
    }
}

This trigger will fire whenever the target element processes the mouse-enter attached event.

Event triggers can also specify two optional arguments: handled and set-handled.

  • If the handled argument is specified, then the trigger will fire even if the routed event has already been handled (that is, the Handled property of its event metadata has been set to true).
  • If the set-handled argument is specified, then the trigger will set the Handled property of the event metadata to true after it has fired.

These arguments should be placed inside of parentheses, after the name of the trigger's event name; if both are present, they should be separated by a comma.

trigger event event_name (handled, set-handled)
{
    // etc
}

Each trigger can specify a collection of actions to perform when it is raised; in the above examples, these are set and play-sfx. Currently, Ultraviolet supports the following trigger actions:

  • set

    set propname (optional_selector) { value }

    Sets the value of the specified dependency property on any descendants of the triggered element which match the specified selector. The selector is optional; if none is specified, the triggered element itself is used. If this action is performed by a property trigger, the dependency property will return to its underlying value when the trigger's conditions are no longer satisfied. Note the use of the :trigger-root pseudo-selector in the above example, which is analogous to the :storyboard-root pseudo-selector.

  • play-sfx

    play-sfx { value }

    Plays the sound effect with the specified manifest resource name.

  • play-storyboard

    play-storyboard (optional_selector) { value }

    Plays the storyboard with the specified name on any elements in the view which match the specified selector. The selector is optional; if none is specified, the triggered element itself is used. As with set, the :trigger-root pseudo-selector works similarly to the :storyboard-root pseudo-selector, specifying the triggered element only.

Clone this wiki locally