Skip to main content

gpui_base/
state_style.rs

1use gpui::{Refineable as _, StyleRefinement, Styled, prelude::FluentBuilder};
2
3/// A semantic-state style builder.
4///
5/// Visual modifiers come from GPUI's [`Styled`] trait. Unlike a bare
6/// [`StyleRefinement`], this wrapper also supports [`FluentBuilder`] helpers
7/// such as `when`, `when_some`, and `when_none`.
8#[derive(Default)]
9pub struct StateStyle {
10    refinement: StyleRefinement,
11}
12
13impl StateStyle {
14    pub(crate) fn into_refinement(self) -> StyleRefinement {
15        self.refinement
16    }
17}
18
19impl Styled for StateStyle {
20    fn style(&mut self) -> &mut StyleRefinement {
21        &mut self.refinement
22    }
23}
24
25impl FluentBuilder for StateStyle {}
26
27/// Resolves the final style of a control from its instance style and the
28/// semantic-state styles that are currently active.
29///
30/// The layering order is fixed for every Base control:
31///
32/// 1. the instance style supplied through GPUI's [`Styled`] builder chain,
33/// 2. value states such as `checked`, `pressed`, `selected`, or `focused`,
34/// 3. `disabled`, which is always resolved last.
35///
36/// Semantic states therefore override the builder chain, matching how GPUI
37/// layers `hover`, `active`, and `focus_visible` on top of an element's base
38/// style. Callers pass their active states in the order above; inactive states
39/// are simply omitted from the iterator.
40///
41/// Every control routes through this function so the ordering cannot drift
42/// apart between controls again.
43pub(crate) fn resolve_style<'a>(
44    instance: &StyleRefinement,
45    active_states: impl IntoIterator<Item = &'a StyleRefinement>,
46) -> StyleRefinement {
47    let mut style = StyleRefinement::default();
48    style.refine(instance);
49    for state in active_states {
50        style.refine(state);
51    }
52    style
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    fn state(build: impl FnOnce(StateStyle) -> StateStyle) -> StyleRefinement {
60        build(StateStyle::default()).into_refinement()
61    }
62
63    #[test]
64    fn instance_style_is_the_baseline_when_no_state_is_active() {
65        let instance = state(|style| style.opacity(0.9));
66
67        let resolved = resolve_style(&instance, []);
68
69        assert_eq!(resolved.opacity, Some(0.9));
70    }
71
72    #[test]
73    fn an_active_state_overrides_the_instance_style() {
74        let instance = state(|style| style.opacity(0.9));
75        let checked = state(|style| style.opacity(0.8));
76
77        let resolved = resolve_style(&instance, [&checked]);
78
79        assert_eq!(resolved.opacity, Some(0.8));
80    }
81
82    #[test]
83    fn later_states_override_earlier_states() {
84        let instance = state(|style| style.opacity(0.9));
85        let checked = state(|style| style.opacity(0.8));
86        let disabled = state(|style| style.opacity(0.5));
87
88        let resolved = resolve_style(&instance, [&checked, &disabled]);
89
90        assert_eq!(resolved.opacity, Some(0.5));
91    }
92
93    #[test]
94    fn states_only_override_the_fields_they_set() {
95        let instance = state(|style| style.opacity(0.9).border_1());
96        let disabled = state(|style| style.opacity(0.5));
97
98        let resolved = resolve_style(&instance, [&disabled]);
99
100        assert_eq!(resolved.opacity, Some(0.5));
101        assert_eq!(instance.border_widths, resolved.border_widths);
102    }
103}