Skip to main content

bern_ir/
style.rs

1//! Style overrides and motion tokens declared with `style!`.
2//!
3//! Standard controls keep their platform look unless the application
4//! declares otherwise. A rule overrides one kind of control, or one named
5//! variant of it, on every platform or on the platforms it names. Colors
6//! are semantic roles or explicit pairs for light and dark; motion refers
7//! to spring tokens by name.
8
9use crate::{SourceLocation, Srgb};
10use serde::{Deserialize, Serialize};
11
12/// Spring tokens every application has: name, period in seconds and
13/// damping ratio. An application may change their values.
14pub const BUILT_IN_MOTION: [(&str, f64, f64); 6] = [
15    ("press", 0.12, 1.0),
16    ("snappy", 0.22, 0.85),
17    ("smooth", 0.45, 1.0),
18    ("bouncy", 0.50, 0.62),
19    ("jelly", 0.42, 0.42),
20    ("hero", 0.48, 0.86),
21];
22
23/// Semantic color roles a style may name. Native targets map them to their
24/// system colors; Bern-drawn targets take them from the theme kernel.
25pub const COLOR_ROLES: &[&str] = &[
26    "surface.canvas",
27    "surface.panel",
28    "surface.raised",
29    "text.primary",
30    "text.secondary",
31    "text.placeholder",
32    "icon.primary",
33    "icon.secondary",
34    "accent",
35    "on_accent",
36    "status.success",
37    "status.warning",
38    "status.error",
39    "control.border",
40    "control.track",
41    "control.thumb",
42    "focus.indicator",
43    "selection.background",
44    "selection.foreground",
45    "disabled.foreground",
46    "disabled.surface",
47    "fill.subtle",
48    "fill.strong",
49];
50
51#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
52pub struct StyleSheet {
53    /// Every motion token: the built-in ones, then the application's own.
54    pub motion: Vec<MotionToken>,
55    pub rules: Vec<StyleRule>,
56    /// Every transition: the built-in ones, then the application's own.
57    #[serde(default)]
58    pub transitions: Vec<TransitionDef>,
59    /// Patterns `Pattern(name:)` tiles over a canvas.
60    #[serde(default)]
61    pub patterns: Vec<PatternDef>,
62    /// Accent colors the application chooses among at run time with
63    /// `WindowSet(accent:)`.
64    #[serde(default)]
65    pub accents: Vec<AccentDef>,
66    /// The weights the application gives text styles, such as a heavy
67    /// `large_title`; the sizes stay the platform's.
68    #[serde(default)]
69    pub text_weights: Vec<TextWeight>,
70    /// Named corner radii (`corner { card: 22 }`) in declaration order:
71    /// the values for every platform, then those of `#[target(…)]` blocks.
72    #[serde(default)]
73    pub corners: Vec<CornerDef>,
74}
75
76/// A named corner radius, for every platform when `targets` is empty and
77/// for the platforms it names otherwise.
78#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
79pub struct CornerDef {
80    pub name: String,
81    pub radius: f64,
82    #[serde(default)]
83    pub targets: Vec<String>,
84}
85
86/// The weight, and the size when the application's design gives one, of
87/// one text style.
88#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
89pub struct TextWeight {
90    /// A text style, such as `large_title`.
91    pub style: String,
92    /// One of [`TEXT_WEIGHTS`].
93    pub weight: String,
94    /// The size in points on desktops; the platform's size of the style
95    /// when absent.
96    #[serde(default)]
97    pub size: Option<f64>,
98    /// The size in points on a television, read from across a room, where
99    /// the design gives one; `size` there otherwise.
100    #[serde(default)]
101    pub tv_size: Option<f64>,
102}
103
104/// The weights a text style can take.
105pub const TEXT_WEIGHTS: &[&str] = &[
106    "ultralight",
107    "thin",
108    "light",
109    "regular",
110    "medium",
111    "semibold",
112    "bold",
113    "heavy",
114    "black",
115];
116
117/// A named accent color.
118#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
119pub struct AccentDef {
120    pub name: String,
121    pub color: ColorRef,
122}
123
124/// A woven check: the bands, repeated across and down, colored where they
125/// cross; with `twill`, fine diagonal lines over it.
126#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
127pub struct PatternDef {
128    pub name: String,
129    pub bands: Vec<Band>,
130    pub twill: bool,
131}
132
133/// A band of a pattern: `width` logical units of `color` at `opacity`, or
134/// a gap of the canvas's own color when `color` is `None`.
135#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
136pub struct Band {
137    pub color: Option<ColorRef>,
138    pub width: f64,
139    pub opacity: f64,
140}
141
142/// How an element appears or disappears, moved by a motion token.
143#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
144pub struct TransitionDef {
145    pub name: String,
146    pub kind: TransitionKind,
147    pub motion: String,
148}
149
150#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
151#[serde(tag = "kind", rename_all = "snake_case")]
152pub enum TransitionKind {
153    /// Fades in or out.
154    Fade,
155    /// Grows from (or shrinks to) `from` of its size while fading.
156    Pop { from: f64 },
157    /// Extends from zero along one axis, keeping `edge` fixed, without fading.
158    Grow { edge: Edge },
159    /// Slides from (or to) `distance` beyond `edge` while fading.
160    Slide { edge: Edge, distance: f64 },
161    /// Moves in from (or out to) beyond `edge`, its whole size away, without
162    /// fading, as a sheet comes up from the bottom of the screen.
163    Move { edge: Edge },
164}
165
166/// Transitions every application has.
167pub fn built_in_transitions() -> Vec<TransitionDef> {
168    vec![
169        TransitionDef {
170            name: "fade".into(),
171            kind: TransitionKind::Fade,
172            motion: "smooth".into(),
173        },
174        TransitionDef {
175            name: "pop".into(),
176            kind: TransitionKind::Pop { from: 0.8 },
177            motion: "bouncy".into(),
178        },
179        TransitionDef {
180            name: "slide".into(),
181            kind: TransitionKind::Slide {
182                edge: Edge::Bottom,
183                distance: 16.0,
184            },
185            motion: "smooth".into(),
186        },
187    ]
188}
189
190impl StyleSheet {
191    pub fn token(&self, name: &str) -> Option<&MotionToken> {
192        self.motion.iter().find(|token| token.name == name)
193    }
194
195    /// The accent named like an enum variant: `Crimson` and `crimson`
196    /// name the same accent.
197    pub fn accent(&self, name: &str) -> Option<&AccentDef> {
198        let wanted = name.replace('_', "").to_lowercase();
199        self.accents
200            .iter()
201            .find(|accent| accent.name.replace('_', "").to_lowercase() == wanted)
202    }
203
204    pub fn pattern(&self, name: &str) -> Option<&PatternDef> {
205        self.patterns.iter().find(|pattern| pattern.name == name)
206    }
207
208    pub fn transition(&self, name: &str) -> Option<&TransitionDef> {
209        self.transitions
210            .iter()
211            .find(|transition| transition.name == name)
212    }
213
214    /// The radius the corner `name` has on `platform`: the last
215    /// declaration that applies there.
216    pub fn corner(&self, name: &str, platform: &str) -> Option<f64> {
217        self.corners
218            .iter()
219            .rev()
220            .find(|corner| {
221                corner.name == name
222                    && (corner.targets.is_empty() || corner.targets.iter().any(|t| t == platform))
223            })
224            .map(|corner| corner.radius)
225    }
226
227    /// The rules for `platform`, in declaration order: later rules override
228    /// earlier ones property by property.
229    pub fn rules_for<'a>(&'a self, platform: &'a str) -> impl Iterator<Item = &'a StyleRule> {
230        self.rules.iter().filter(move |rule| {
231            rule.targets.is_empty() || rule.targets.iter().any(|t| t == platform)
232        })
233    }
234}
235
236/// A spring: undamped period `period` in seconds and damping ratio
237/// `damping`. Stiffness is (2π / period)² and damping 4π·damping / period,
238/// for a mass of 1.
239#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
240pub struct MotionToken {
241    pub name: String,
242    pub period: f64,
243    pub damping: f64,
244}
245
246#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
247pub struct StyleRule {
248    /// The control kind, such as `SidebarItem`.
249    pub component: String,
250    /// A named variant that instances select with `variant:`.
251    pub variant: Option<String>,
252    /// Platforms the rule applies to (`macos`, `ios`, `windows`, `linux`,
253    /// `browser`); empty for all.
254    pub targets: Vec<String>,
255    /// Platforms that keep their default look for effects they cannot show.
256    pub exempt: Vec<String>,
257    /// Effects in every state.
258    pub base: Vec<StyleEffect>,
259    pub states: Vec<StateStyle>,
260    /// Motion back to the resting state.
261    pub settle: Option<String>,
262    pub source: SourceLocation,
263}
264
265#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
266pub struct StateStyle {
267    pub state: StyleState,
268    pub effects: Vec<StyleEffect>,
269}
270
271#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(rename_all = "snake_case")]
273pub enum StyleState {
274    Hover,
275    Pressed,
276    Selected,
277    Focus,
278    Disabled,
279}
280
281impl StyleState {
282    pub const ALL: [StyleState; 5] = [
283        StyleState::Hover,
284        StyleState::Pressed,
285        StyleState::Selected,
286        StyleState::Focus,
287        StyleState::Disabled,
288    ];
289
290    pub fn name(self) -> &'static str {
291        match self {
292            StyleState::Hover => "hover",
293            StyleState::Pressed => "pressed",
294            StyleState::Selected => "selected",
295            StyleState::Focus => "focus",
296            StyleState::Disabled => "disabled",
297        }
298    }
299}
300
301#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
302#[serde(tag = "kind", rename_all = "snake_case")]
303pub enum StyleEffect {
304    Background {
305        color: ColorRef,
306    },
307    Foreground {
308        color: ColorRef,
309    },
310    Border {
311        color: ColorRef,
312        width: f64,
313    },
314    Corner {
315        radius: f64,
316    },
317    Padding {
318        length: f64,
319    },
320    Size {
321        size: ControlSize,
322    },
323    /// A selected item filled with `color`; its text and icons use the
324    /// color's foreground pair.
325    Fill {
326        color: ColorRef,
327    },
328    /// A selected item marked by a bar on one edge.
329    Indicator {
330        color: ColorRef,
331        edge: Edge,
332        width: f64,
333    },
334    Scale {
335        x: f64,
336        y: f64,
337        motion: String,
338    },
339    Offset {
340        x: f64,
341        y: f64,
342        motion: String,
343    },
344    Opacity {
345        value: f64,
346        motion: String,
347    },
348    /// A shadow below the control, as if lifted off the page: `radius` of
349    /// blur, `y` down, black at `opacity`.
350    Shadow {
351        radius: f64,
352        y: f64,
353        opacity: f64,
354    },
355    /// The platform's own look for this state.
356    System,
357    /// No feedback in this state.
358    Nothing,
359    /// A length the element writes, replaced for the platforms the rule
360    /// applies to; `None` drops a written width or height, so the element
361    /// takes its content's.
362    Metric {
363        metric: Metric,
364        value: Option<f64>,
365    },
366}
367
368/// The lengths a style may set in place of those an element writes: a
369/// row's or column's spacing, a container's padding and size, a control's
370/// text or symbol size.
371#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
372#[serde(rename_all = "snake_case")]
373pub enum Metric {
374    Gap,
375    PaddingX,
376    PaddingY,
377    Width,
378    Height,
379    FontSize,
380    RowHeight,
381}
382
383impl Metric {
384    pub const ALL: [Metric; 7] = [
385        Metric::Gap,
386        Metric::PaddingX,
387        Metric::PaddingY,
388        Metric::Width,
389        Metric::Height,
390        Metric::FontSize,
391        Metric::RowHeight,
392    ];
393
394    /// The property's name, in `style!` and on the element.
395    pub fn name(self) -> &'static str {
396        match self {
397            Metric::Gap => "gap",
398            Metric::PaddingX => "padding_x",
399            Metric::PaddingY => "padding_y",
400            Metric::Width => "width",
401            Metric::Height => "height",
402            Metric::FontSize => "font_size",
403            Metric::RowHeight => "row_height",
404        }
405    }
406}
407
408impl StyleEffect {
409    /// The effect's name in `style!` and in target capabilities.
410    pub fn name(&self) -> &'static str {
411        match self {
412            StyleEffect::Background { .. } => "background",
413            StyleEffect::Foreground { .. } => "foreground",
414            StyleEffect::Border { .. } => "border",
415            StyleEffect::Corner { .. } => "corner",
416            StyleEffect::Padding { .. } => "padding",
417            StyleEffect::Size { .. } => "size",
418            StyleEffect::Fill { .. } => "fill",
419            StyleEffect::Indicator { .. } => "indicator",
420            StyleEffect::Scale { .. } => "scale",
421            StyleEffect::Offset { .. } => "offset",
422            StyleEffect::Opacity { .. } => "opacity",
423            StyleEffect::Shadow { .. } => "shadow",
424            StyleEffect::System => "system",
425            StyleEffect::Nothing => "none",
426            StyleEffect::Metric { metric, .. } => metric.name(),
427        }
428    }
429}
430
431#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
432#[serde(tag = "kind", rename_all = "snake_case")]
433pub enum ColorRef {
434    Role { role: String },
435    Fixed { light: Srgb, dark: Srgb },
436}
437
438#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
439#[serde(rename_all = "snake_case")]
440pub enum ControlSize {
441    Small,
442    Regular,
443    Large,
444    ExtraLarge,
445}
446
447#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
448#[serde(rename_all = "snake_case")]
449pub enum Edge {
450    Leading,
451    Trailing,
452    Top,
453    Bottom,
454}