# Text Style Propagation: Strawman Proposal A concrete proposal for implementing inheritable text styles. ## Data types This design incorporates the following types: * Resources * `DefaultTextStyle` * Components * `TextStyle` * `TextColor` * `ComputedTextStyle` * `TextStyleRoot` * Structs * `FontFace` * `FontSize` * `FontWeight` * `FontSlant` * `FontStretch` ### DefaultTextStyle ```rust #[derive(Resource)] struct DefaultTextStyle { pub face: FontFace, pub size: FontSize, pub weight: FontWeight, pub stretch: FontStretch, pub line_height: FontSize, } ``` ### TextStyle Defines a text style for an entity. The entity may or may not be a text entity. Rather than having a separate component for each different text style property (which was an earlier idea), there are some advantages to making it a single struct with optional fields, where `None` means "inherits". Otherwise we end up using structs like `FontSize` as both ECS components and field types - or even embedding components inside resources, which is very weird. A third alternative is to make a newtype wrapper component for every style attribute, but that gets wordy fast. ```rust /// Optional fields are inherited from parent entity /// or from `DefaultTextStyle`. #[derive(Component)] struct TextStyle { pub face: Option<FontFace>, pub size: Option<FontSize>, pub weight: Option<FontWeight>, pub stretch: Option<FontStretch>, pub line_height: Option<FontSize>, } ``` ### FontSize ```rust enum FontSize { /// Pixel units Px(f32), /// Relative to the font size of the parent entity, or /// the default font size if there is no parent. Em(f32), /// Relative to the default font size as specified in /// `DefaultTextStyle` which is considered the "root". Rem(f32), /// Font Size relative to the size of the viewport /// width. Vw(f32), /// Font Size relative to the size of the viewport /// height. Vh(f32), /// Font Size relative to the smaller of the viewport /// width and viewport height. VMin(f32), /// Font Size relative to the larger of the viewport /// width and viewport height. VMax(f32), } impl FontSize { /// Return this FontSize scaled by a multipicative factor. pub fn scale(&self, factor: f32) -> Self; /// Return the computed font size, taking the parent's /// computed size into account. This removes `em` units /// and translates them into absolute units. pub fn compose(&self, parent: &Self) -> Self; } ``` ### FontWeight ```rust #[derive(Debug, Clone, PartialEq)] pub enum FontWeight { Thin, // 100 ExtraLight, // 200 Light, // 300 Normal, // 400 Medium, // 500 SemiBold, // 600 Bold, // 700 ExtraBold, // 800 Black, // 900 Numeric(u16), // Custom numeric weight } ``` ### FontSlant ```rust pub enum FontSlant { Normal, Italic, Oblique(f32), // Angle } ``` ### FontStretch ```rust pub enum FontStretch { UltraCondensed, // 50% ExtraCondensed, // 62.5% Condensed, // 75% SemiCondensed, // 87.5% Normal, // 100% SemiExpanded, // 112.5% Expanded, // 125% ExtraExpanded, // 150% UltraExpanded, // 200% Percentage(f32), // Custom percentage } ``` ### ComputedTextStyle An automatically-generated component which collects all the inherited text styles for an entity, which may or may not be a text entity. To make it easier to calculate `em`-sizes during propagation, the font size is split into two separate fields (explanation below). ```rust /// Algorithm for computing sizes: #[derive(Component)] struct ComputedTextStyle { /// The selected font face pub face: FontFace, /// Adjusted font size of this entity. If the size /// of this entity is in `em` units, the adjusted /// size will be the parent entity's size multiplied /// by the em-unit scaling factor. pub size: FontSize, /// Font weight pub weight: FontWeight, /// Font stretch pub stretch: FontStretch, /// Line height pub line_height: FontSize, /// Color pub color: Color, } ``` **Font size Example**: if the parent's font size is `Vh(10.0)` and this node's size is `Em(0.5)` then the adjusted size is `Vh(10.0 * 0.5)` or `Vh(5.0)`. In effect, `Em` units always scale the inherited size of the parent, while other units are copied verbatim. ### TextStyleRoot Marks this node as being a root for purposes of computing inherited styles. See subsequent section on propagation. ```rust #[derive(Component)] struct TextStyleRoot; ``` ### FontFace ```rust enum FontFace { /// A reference to a font face by registered name Alias(FontAlias), /// A reference to a font by its handle. Note that /// when this is the effective value, the `FontWeight`, /// `FontSlant` and other font style attributes are /// ignored. Handle(Handle<Font>), } ``` ### TextColor ```rust struct TextColor(pub Color); ``` ## Propagation Algorithm The following algorithm is intended to provide a way to propagate style changes to the individual text nodes in the tree, while avoiding adding additional overhead to entities that don't care about fonts or text. In other words, it's meant to be "opt-in" in terms of performance. In particular, we want to avoid two kinds of expense: * Having to search all the way up the ancestor chain to the root in order to get the style because we didn't find any stype components lower down. * Having to insert a housekeeping component on every node in the entity hierarchy. The `TextStyleRoot` marker component defines a sub-tree of the entity hierarchy which is used for computing text style inheritance. All descendants of the `TextStyleRoot` entity participate in style propagation and inheritance. Any entity that does not have a `TextStyleRoot` ancestor is not considered in the inheritance computation. The entity marked with `TextStyleRoot` is considered to be the "root" with respect to style inheritance. Any style properties that are not defined at the root are inherited from the global `DefaultTextStyles` resource. All descendants of the `TextStyleRoot` root, along with the root itself, will have a `ComputedTextStyle` component inserted. The `ComputedTextStyle` has a number of responsibilities: * It serves as a compilation of all the inherited styles. * Observers that look for changes to the hierarchy can check for the existence of this component to determine if they need to do any work. Hooks and systems will maintain the following invariants: * An entity having `TextStyleRoot` will also have a `ComputedTextStyle`. * An entity whos parent has a `ComputedTextStyle` will also have a `ComputedTextStyle`. * The fields of a `ComputedTextStyle` are derived from: * The fields of the `TextStyle` and `TextColor` components of the entity, if present. * The `ComputedTextStyle` of the parent, if present. * Otherwise, the `DefaultTextStyle`. So for example, if a text node has a `TextStyle` that has a `weight` field that is not `None`, then the `ComputedTextStyle`'s weight will be set to that value. If the field is `None`, or the `TextStyle` component is missing, then it checks to see if has a parent within the scope. If it does, then it uses the weight from the parent's computed text style; otherwise it uses the default computed text style. **Impact**: this means that a container widget that is a text style scope will cause all of its descendants to have a `ComputedTextStyle` inserted, whether or not they are text. However, this might not be too bad, for a couple of reasons: * `ComputedTextStyle` is a fairly small component, containing mostly simple enums. The two largest fields are the color, and the smolstr for the font alias name. * We should try and design the widgets so that only containers that are meant to have text children should have this. So for example, a scrolling list box widget will not be a text style scope, but the individual rows within the list might be. In this design, we don't try and optimize things by omitting `ComputedTextStyle` for non-text entities. Reasons: * We can use the presence of `ComputedTextStyle` during hierarchy changes to detect whether styles further down the tree need to be recomputed. * It allows local updates to be faster: we only need to look at the style of the immediate parent, instead of walking up to find the root. ### Orphan Text nodes A text node which is not contained within a `TextStyleRoot` can still be rendered, but it will not inherit any styles from it's parent. The effective style will be derived from: * The fields of the `TextStyle` and `TextColor` components, if present. * The `DefaultTextStyle`. It will not have a `ComputedTextStyle` (because the presence of this component tells the system to look for inherited values). This means that if there are any text rendering attributes that *must* be present on text nodes, they will have to be stored in a different component. ### Nested scopes allowed Scopes can be nested. This makes the propagation more complex and harder to reason about, but in practice there's no way to avoid this: we can't prevent a user from nesting widgets this way. To make this simple, we simply say that entities with a `TextStyleRoot` component never inherit from their parent entities, even if those parents have a `ComputedTextStyle` - each `TextStyleRoot` effectively does a reset of the styles back to the default. ### Alternate Idea If the impact is a concern, we can introduce an additional ZST marker component, `TextStyleInheritor`, that is used to indicate that an entity has a `TextStyleRoot` ancestor. This would allow `ComputedTextStyle` to be represented more sparsely, saving some memory, at the cost of sometimes having to search farther up the ancestor chain.