A graphic element (BlElement) has properties such as its color, a transformation matrix, a border value, layout rules and so on. If the graphic element is a text, it can have a font-name, font-size, ... A graphic element can be animated, i.e. its properties can be modified with time based events. A graphic element can have event handlers that enable the element to react according to events (e.g. adding a new element inside the graphic element, clicking on it with the mouse, etc.). A graphic element can contain other graphic elements. To display a graphic element on the screen, it must be added to a space (BlSpace).
A space contains a frame which manages the various stages of drawing and event/animation management in Bloc. A frame is made up of different phases. When the "space" receives a pulse, the frame calls all the #runOn: methods of each phase on the space in turn. The phases are :
- Idle: Do nothing. This is a waiting phase when the space has nothing to do.
- Host Validation: Creates a new window when Pharo is launched. This mechanism is not currently used. I suppose it's complicated to implement with external libraries such as Cairo, which need to create a new handler each time the graphical environment is started.
- Task: Triggers time-based events. The animations for example.
- Event: Updates the focus of elements in the space, then retrieves events from the window (mouse, keyboard, zoom, etc.) and sends them to the various elements in the space.
- Drawing Validation: Checks whether the window needs to redraw elements. For example, if the window has changed size.
- Layout: Computes the layout of the various elements.
- Drawing: Orders the renderer to draw the various elements.
Toplo will add 2 new phases to this list, between the Drawing Validation and Layout phases. These two phases are :
- Skin Installer: Checks if a new skin should be installed on graphic elements. If so, it will recalculate the layout, uninstall the old skin and install the new one.
- Skin State Application: Creates look events based on the state of graphic elements.
These two phases are added to a space when an element requests the installation of a new skin for the first time.
Each graphic element has a skin manager. The role of this manager is to install skins on the graphic element, i.e. to update the element's properties according to its state. Graphic element states are divided into three categories:
- Management states: These states manage skin installation and uninstallation, as well as element enblement.
- Intrinsic states: These are states that are inseparable from the object, for example when the object is "focus", or "checked" in the case of a "checkbox button".
- Transient states: These are non-persistent states. For example, if the element is hovered, or if it is pressed, or if it is dragged, etc.
States are generated by an event handler (ToSkinStateGenerator) associated with the graphic element. Only elements with this event handler can receive skins.
When a new skin is installed, the skin manager adds an event handler to the graphic element. This event handler is a ToStyleSheetSkin. This event handler manages look events (ToElementLookEvent). It uses the element's writers to update the properties of the element.
Look events are events created during the SkinStateApplication phase based on the current state of the graphic element. There are 3 categories of look events: management events (skin installation and uninstallation), intrasic events (focus/unfocus, checked/unchecked, etc.) and transient events (pressed, drag, hover, etc.).
Element's writers are ToLookEventListener and are described in the skin theme. They correspond to the set of style rules for the graphic element.
There are two ways to install a new skin for a graphic element. It's possible to apply a new theme to all the graphic elements in a space. And it's also possible to update an element's tokens or stamps, in which case the skin of the children's element will also be replaced by an updated version.
Each graphic element stores its tokens and stamps in a store (ToStyleStore). Tokens and stamps are key-value associations. They can be accessed by the element's writers. There are two differences between tokens and stamps:
- When a graphic element is asked if it contains a token (using its key), it will look for it in its own store. If its store doesn't contain the token, it will ask its parent's store (and so on, all the way back to the root element). If it doesn't found it, it will raised an exception. When a graphical element is asked if it contains a stamp, it will only look in its own store.
- A stamp, unlike a token, can be used as a selector by a theme's style rules.
A theme can be applied to a space to quickly modify the appearance of the graphic elements it contains. A theme contains a variant, a list of editable properties and style rules. Editable properties are a list of properties that the theme allows you to modify; an editable property is not necessarily a property of a graphic element. There are two types of editable property:
- Feature: when the property has the same name in the theme as in the API of the graphic element (e.g. width, height, layout, label, geometry, etc.). In this case, the writer uses the graphic element's API to update the graphic appearance.
- Pseudo: When the property in the theme does not correspond to a property in the graphic element (e.g. background-color, border-with-builder, label-text-background, etc.). Pseudos can be used to modify a widget's child elements directly at widget level, or to access properties not present in the graphical element API in the form of a facade (e.g. change text color).
Theme variants allow you to change the values of certain tokens in the space's root element. When a theme is applied to a space, the root element of the space receives from the theme a list of all tokens used in the theme. Theme variants can be used to change the value of some theme tokens, for example the tokens used for the colors, making it easy to create a light theme and a dark theme.
A style rule is an association between a selector and one or more writers. A rule can only be applied to a graphic element if the rule's selector corresponds to the graphic element. There are 5 types of selector, and they can be combined with different operators. The five selector types are:
- Any: Selects any element.
- Action: Evaluates a block closure with the graphic element as argument. If the block returns true, then the element is selected, otherwise it is not.
- Type: Used to test whether the graphic element is an object of a given class. For example, it can check that the graphic element is a ToButton.
- Id: Selects graphic elements according to their id. The id is a BlElement's property.
- Stamp: Checks that the graphic element has a stamp with a given key.
Selectors can be combined with each other using different operators. There are 6 different operators:
- Not: Negates the selection.
- And: Boolean combination of and between two selectors.
- Or: Boolean or combination between two selectors.
- Child: Used to apply a selector to the graphic element's children. It is possible to set a child depth level.
- Parent: Allows you to apply a selector to the graphic element's parent. It is possible to set a parent depth level.
- Sibling: Allows you to apply a selector to graphic elements having the same parent as the graphic element.
All selectors and combinations are interpreted by ToSelectorInterpreter each time a theme is installed on a space. If the selector "matches" with one of the graphic elements, the associated writers are installed on the element. By default, only the last writter per editable property and look event is installed for a given graphic element. You can force a rule to be applied using the supplements variable.
Writers are ToLookEventListeners. They respond to look events. Writers modify the editable properties of each graphic element. Style rules and their associated writers are built by a scripter (TToStyleRuleScripter). To create a style rule, use the #select:style: method, with a selector for the rule as the first argument and a block closure that executes the associated writers as the second argument. The scripter has 8 differents methods for creating writers:
- #do: - Executes a block with the element selected when the skin is installed as argument.
- #when:do: - Executes a block with the selected element as argument for a given look event.
- #write:with: - Modifies an editable property with the given value.
- #write:whitAll: - Modifies a list of editables properties with the given values.
- #when:write:with: - Modifies an editable property with the given value for a given look event.
- #when:write:with:animation: - Modifies an editable property with the given value and associated animation for a given look event.
- #supplement:with: - Forces the modification of an editable property with the given value.
- #when:supplement:with: - Forces the modification of an editable property with the given value for a given look event.