Skip to main content

gpui_motion_kit/
style.rs

1//! Portable style tracks applied to stock Kit's native styles.
2use crate::core::{Color, ColorProperty, StyleMotion, StyleProperty};
3use gpui_kit::{App, Rgba, Window};
4use std::{ops::Deref, time::Duration};
5
6/// Converts the engine's sRGB color to Kit's native RGBA value.
7pub fn color_to_rgba(color: Color) -> Rgba {
8    let [r, g, b, a] = color.channels();
9
10    Rgba { r, g, b, a }
11}
12
13/// A requested style track has no public renderer hook in Kit 0.7.3.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct UnsupportedStyle(StyleProperty);
16
17impl UnsupportedStyle {
18    /// The unsupported scalar property (element blur or backdrop blur).
19    pub fn property(self) -> StyleProperty {
20        self.0
21    }
22}
23
24impl std::fmt::Display for UnsupportedStyle {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(
27            f,
28            "stock GPUI Kit does not support the {:?} style track",
29            self.0
30        )
31    }
32}
33
34impl std::error::Error for UnsupportedStyle {}
35
36/// Retain one style sampler per logical element. Uses Kit's executor clock.
37#[derive(Debug, Default)]
38pub struct StyleMotionState {
39    inner: crate::core::StyleMotionState,
40    clock: crate::Clock,
41}
42
43impl StyleMotionState {
44    /// Samples during rendering and schedules only while a sampled track moves.
45    /// Use `StyleFrame::try_apply` to detect unsupported filter tracks.
46    pub fn frame(&mut self, motion: StyleMotion, window: &mut Window, cx: &App) -> StyleFrame {
47        let now = self.clock.tick(cx);
48
49        let frame = self.sample(motion, now, cx.reduce_motion());
50
51        if frame.is_animating() {
52            window.request_animation_frame();
53        }
54
55        frame
56    }
57
58    /// Samples one explicit epoch for deterministic tests. Do not mix with `frame`.
59    pub fn sample(
60        &mut self,
61        motion: StyleMotion,
62        now: Duration,
63        reduced_motion: bool,
64    ) -> StyleFrame {
65        StyleFrame(self.inner.sample(motion, now, reduced_motion))
66    }
67}
68
69/// Presented style tracks with Kit-native conversion and engine status getters.
70#[derive(Clone, Copy, Debug)]
71pub struct StyleFrame(crate::core::StyleFrame);
72
73impl From<crate::core::StyleFrame> for StyleFrame {
74    fn from(frame: crate::core::StyleFrame) -> Self {
75        Self(frame)
76    }
77}
78
79impl Deref for StyleFrame {
80    type Target = crate::core::StyleFrame;
81
82    fn deref(&self) -> &Self::Target {
83        &self.0
84    }
85}
86
87impl StyleFrame {
88    /// Applies Kit-supported tracks, panicking if a filter track is declared.
89    /// Only introduced properties are replaced; layout properties reflow.
90    ///
91    /// # Panics
92    /// Panics when element or backdrop blur is declared. Use `try_apply` for
93    /// portable declarations that may contain unsupported filter tracks.
94    pub fn apply<E: gpui_kit::Styled>(self, element: E) -> E {
95        self.try_apply(element).expect("unsupported Kit style")
96    }
97
98    /// Applies introduced colors, spacing, borders, corners and one outer shadow.
99    /// Returns `UnsupportedStyle` before applying anything if element/backdrop blur
100    /// is declared, including a zero radius. Kit has no public filter hook.
101    ///
102    /// # Errors
103    /// Returns `UnsupportedStyle` for element/backdrop blur, before mutating the element.
104    pub fn try_apply<E: gpui_kit::Styled>(self, mut element: E) -> Result<E, UnsupportedStyle> {
105        for property in [StyleProperty::Blur, StyleProperty::BackdropBlur] {
106            if self.target().get(property).is_some() {
107                return Err(UnsupportedStyle(property));
108            }
109        }
110
111        use StyleProperty::*;
112        use gpui_kit::px;
113
114        if let Some(v) = self.target().get(RadiusTopLeft) {
115            element.style().corner_radii.top_left = Some(px(v).into());
116        }
117
118        if let Some(v) = self.target().get(RadiusTopRight) {
119            element.style().corner_radii.top_right = Some(px(v).into());
120        }
121
122        if let Some(v) = self.target().get(RadiusBottomRight) {
123            element.style().corner_radii.bottom_right = Some(px(v).into());
124        }
125
126        if let Some(v) = self.target().get(RadiusBottomLeft) {
127            element.style().corner_radii.bottom_left = Some(px(v).into());
128        }
129
130        if let Some(v) = self.target().get(BorderTop) {
131            element.style().border_widths.top = Some(px(v).into());
132        }
133
134        if let Some(v) = self.target().get(BorderRight) {
135            element.style().border_widths.right = Some(px(v).into());
136        }
137
138        if let Some(v) = self.target().get(BorderBottom) {
139            element.style().border_widths.bottom = Some(px(v).into());
140        }
141
142        if let Some(v) = self.target().get(BorderLeft) {
143            element.style().border_widths.left = Some(px(v).into());
144        }
145
146        if let Some(v) = self.target().get(PaddingTop) {
147            element.style().padding.top = Some(px(v).into());
148        }
149
150        if let Some(v) = self.target().get(PaddingRight) {
151            element.style().padding.right = Some(px(v).into());
152        }
153
154        if let Some(v) = self.target().get(PaddingBottom) {
155            element.style().padding.bottom = Some(px(v).into());
156        }
157
158        if let Some(v) = self.target().get(PaddingLeft) {
159            element.style().padding.left = Some(px(v).into());
160        }
161
162        if let Some(v) = self.target().get(MarginTop) {
163            element.style().margin.top = Some(px(v).into());
164        }
165
166        if let Some(v) = self.target().get(MarginRight) {
167            element.style().margin.right = Some(px(v).into());
168        }
169
170        if let Some(v) = self.target().get(MarginBottom) {
171            element.style().margin.bottom = Some(px(v).into());
172        }
173
174        if let Some(v) = self.target().get(MarginLeft) {
175            element.style().margin.left = Some(px(v).into());
176        }
177
178        if let Some(v) = self.target().get(GapX) {
179            element.style().gap.width = Some(px(v).into());
180        }
181
182        if let Some(v) = self.target().get(GapY) {
183            element.style().gap.height = Some(px(v).into());
184        }
185
186        if let Some(color) = self.target().get_color(ColorProperty::Background) {
187            element = element.bg(color_to_rgba(color));
188        }
189
190        if let Some(color) = self.target().get_color(ColorProperty::Text) {
191            element = element.text_color(color_to_rgba(color));
192        }
193
194        if let Some(color) = self.target().get_color(ColorProperty::Border) {
195            element = element.border_color(color_to_rgba(color));
196        }
197
198        if [ShadowX, ShadowY, ShadowBlur, ShadowSpread]
199            .iter()
200            .any(|property| self.target().get(*property).is_some())
201            || self.target().get_color(ColorProperty::Shadow).is_some()
202        {
203            let color = self
204                .target()
205                .get_color(ColorProperty::Shadow)
206                .unwrap_or(Color::rgba(0., 0., 0., 0.25));
207
208            let color = color_to_rgba(color);
209
210            element.style().box_shadow = Some(vec![
211                gpui_kit::BoxShadow::new(
212                    px(self.target().get(ShadowX).unwrap_or(0.)),
213                    px(self.target().get(ShadowY).unwrap_or(0.)),
214                    color.into(),
215                )
216                .blur_radius(px(self.target().get(ShadowBlur).unwrap_or(0.)))
217                .spread_radius(px(self.target().get(ShadowSpread).unwrap_or(0.))),
218            ]);
219        }
220
221        Ok(element)
222    }
223}