Skip to main content

gpui_ce_motion/
style_motion.rs

1//! Declarative native style tracks, separate from positional/presence targets.
2use crate::{Color, ColorState, MotionValue, Transition};
3use std::time::Duration;
4
5/// Scalar style properties in logical pixels. Margins and shadow offsets/spread
6/// may be negative; the other properties require non-negative targets.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8#[repr(usize)]
9pub enum StyleProperty {
10    /// RadiusTopLeft in logical pixels.
11    RadiusTopLeft,
12    /// RadiusTopRight in logical pixels.
13    RadiusTopRight,
14    /// RadiusBottomRight in logical pixels.
15    RadiusBottomRight,
16    /// RadiusBottomLeft in logical pixels.
17    RadiusBottomLeft,
18    /// BorderTop in logical pixels.
19    BorderTop,
20    /// BorderRight in logical pixels.
21    BorderRight,
22    /// BorderBottom in logical pixels.
23    BorderBottom,
24    /// BorderLeft in logical pixels.
25    BorderLeft,
26    /// PaddingTop in logical pixels.
27    PaddingTop,
28    /// PaddingRight in logical pixels.
29    PaddingRight,
30    /// PaddingBottom in logical pixels.
31    PaddingBottom,
32    /// PaddingLeft in logical pixels.
33    PaddingLeft,
34    /// MarginTop in logical pixels.
35    MarginTop,
36    /// MarginRight in logical pixels.
37    MarginRight,
38    /// MarginBottom in logical pixels.
39    MarginBottom,
40    /// MarginLeft in logical pixels.
41    MarginLeft,
42    /// GapX in logical pixels.
43    GapX,
44    /// GapY in logical pixels.
45    GapY,
46    /// ShadowX in logical pixels.
47    ShadowX,
48    /// ShadowY in logical pixels.
49    ShadowY,
50    /// ShadowBlur in logical pixels.
51    ShadowBlur,
52    /// ShadowSpread in logical pixels.
53    ShadowSpread,
54    /// Blur in logical pixels.
55    Blur,
56    /// BackdropBlur in logical pixels.
57    BackdropBlur,
58}
59
60const COUNT: usize = 24;
61
62/// Native color tracks, interpolated in linear light.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64#[repr(usize)]
65pub enum ColorProperty {
66    /// Element background.
67    Background,
68    /// Inherited text color.
69    Text,
70    /// Border paint.
71    Border,
72    /// A single outer shadow's paint.
73    Shadow,
74}
75
76fn signed(index: usize) -> bool {
77    (12..16).contains(&index) || [18, 19, 21].contains(&index)
78}
79
80/// Partial style destination. Omitted properties preserve the element's styles
81/// until introduced; subsequently they retain their last declared destination.
82#[derive(Clone, Copy, Debug, Default, PartialEq)]
83pub struct StyleTarget {
84    values: [Option<f64>; COUNT],
85    colors: [Option<Color>; 4],
86}
87
88impl StyleTarget {
89    /// Sets a scalar. Panics for non-finite or disallowed negative values.
90    pub fn value(mut self, property: StyleProperty, value: f32) -> Self {
91        let index = property as usize;
92
93        assert!(
94            value.is_finite() && (signed(index) || value >= 0.),
95            "invalid style value"
96        );
97        self.values[index] = Some(value.into());
98
99        self
100    }
101
102    /// Sets a color track.
103    pub fn color(mut self, property: ColorProperty, color: Color) -> Self {
104        self.colors[property as usize] = Some(color);
105
106        self
107    }
108
109    /// Reads a scalar presentation, with non-negative properties clamped for drawing.
110    pub fn get(self, property: StyleProperty) -> Option<f32> {
111        let index = property as usize;
112
113        self.values[index].map(|v| {
114            v.clamp(
115                if signed(index) {
116                    f64::from(f32::MIN)
117                } else {
118                    0.
119                },
120                f64::from(f32::MAX),
121            ) as f32
122        })
123    }
124
125    /// Reads a presented color.
126    pub fn get_color(self, property: ColorProperty) -> Option<Color> {
127        self.colors[property as usize]
128    }
129
130    /// Sets corners clockwise from top-left.
131    pub fn radii(mut self, values: [f32; 4]) -> Self {
132        for (property, value) in [
133            StyleProperty::RadiusTopLeft,
134            StyleProperty::RadiusTopRight,
135            StyleProperty::RadiusBottomRight,
136            StyleProperty::RadiusBottomLeft,
137        ]
138        .into_iter()
139        .zip(values)
140        {
141            self = self.value(property, value);
142        }
143
144        self
145    }
146
147    /// Sets border widths in top/right/bottom/left order.
148    pub fn border_widths(mut self, values: [f32; 4]) -> Self {
149        for (property, value) in [
150            StyleProperty::BorderTop,
151            StyleProperty::BorderRight,
152            StyleProperty::BorderBottom,
153            StyleProperty::BorderLeft,
154        ]
155        .into_iter()
156        .zip(values)
157        {
158            self = self.value(property, value);
159        }
160
161        self
162    }
163
164    /// Sets padding in top/right/bottom/left order.
165    pub fn padding(mut self, values: [f32; 4]) -> Self {
166        for (property, value) in [
167            StyleProperty::PaddingTop,
168            StyleProperty::PaddingRight,
169            StyleProperty::PaddingBottom,
170            StyleProperty::PaddingLeft,
171        ]
172        .into_iter()
173        .zip(values)
174        {
175            self = self.value(property, value);
176        }
177
178        self
179    }
180
181    /// Sets margins in top/right/bottom/left order.
182    pub fn margin(mut self, values: [f32; 4]) -> Self {
183        for (property, value) in [
184            StyleProperty::MarginTop,
185            StyleProperty::MarginRight,
186            StyleProperty::MarginBottom,
187            StyleProperty::MarginLeft,
188        ]
189        .into_iter()
190        .zip(values)
191        {
192            self = self.value(property, value);
193        }
194
195        self
196    }
197
198    /// Sets horizontal and vertical gap.
199    pub fn gap(self, x: f32, y: f32) -> Self {
200        self.value(StyleProperty::GapX, x)
201            .value(StyleProperty::GapY, y)
202    }
203
204    /// Sets a single outer shadow. No shadow-list topology interpolation is implied.
205    pub fn shadow(self, offset: [f32; 2], blur: f32, spread: f32, color: Color) -> Self {
206        self.value(StyleProperty::ShadowX, offset[0])
207            .value(StyleProperty::ShadowY, offset[1])
208            .value(StyleProperty::ShadowBlur, blur)
209            .value(StyleProperty::ShadowSpread, spread)
210            .color(ColorProperty::Shadow, color)
211    }
212}
213
214/// Disposable declaration of style destinations and timing.
215#[derive(Clone, Copy, Debug)]
216pub struct StyleMotion {
217    target: StyleTarget,
218    initial: Option<StyleTarget>,
219    transition: Transition,
220    overrides: [Option<Transition>; COUNT],
221    color_overrides: [Option<Transition>; 4],
222}
223
224impl StyleMotion {
225    /// Creates a destination. First mount snaps unless `initial` is supplied.
226    pub fn new(target: StyleTarget) -> Self {
227        Self {
228            target,
229            initial: None,
230            transition: Transition::default(),
231            overrides: [None; COUNT],
232            color_overrides: [None; 4],
233        }
234    }
235
236    /// Chooses the first presentation; omitted channels inherit the destination.
237    pub fn initial(mut self, target: StyleTarget) -> Self {
238        self.initial = Some(target);
239
240        self
241    }
242
243    /// Sets default timing for all style tracks.
244    pub fn transition(mut self, transition: Transition) -> Self {
245        self.transition = transition;
246
247        self
248    }
249
250    /// Overrides timing for one scalar track.
251    pub fn transition_for(mut self, property: StyleProperty, transition: Transition) -> Self {
252        self.overrides[property as usize] = Some(transition);
253
254        self
255    }
256
257    /// Overrides timing for one color track.
258    pub fn color_transition(mut self, property: ColorProperty, transition: Transition) -> Self {
259        self.color_overrides[property as usize] = Some(transition);
260
261        self
262    }
263}
264
265/// Sampled style properties, ready to apply with the native feature.
266#[derive(Clone, Copy, Debug)]
267pub struct StyleFrame {
268    target: StyleTarget,
269    active: bool,
270    completed: bool,
271}
272
273impl StyleFrame {
274    /// Presented style destination, with clamped readers.
275    pub fn target(self) -> StyleTarget {
276        self.target
277    }
278
279    /// Whether another sample is needed.
280    pub fn is_animating(self) -> bool {
281        self.active
282    }
283
284    /// Completion edge when every active scalar/color finishes.
285    pub fn just_completed(self) -> bool {
286        self.completed
287    }
288}
289
290/// Retained allocation-free style sampler. Springs preserve each channel's
291/// momentum; fields omitted after introduction retain their destination.
292#[derive(Debug, Default)]
293pub struct StyleMotionState {
294    values: [Option<MotionValue>; COUNT],
295    colors: [Option<ColorState>; 4],
296    destination: StyleTarget,
297    initialized: bool,
298    active: bool,
299    #[cfg(feature = "gpui")]
300    epoch: Option<std::time::Instant>,
301}
302
303impl StyleMotionState {
304    /// Samples using one monotonic application epoch. Reduced motion snaps.
305    ///
306    /// ```
307    /// use gpui_ce_motion::{StyleMotionState, StyleMotion, StyleTarget, StyleProperty};
308    /// use std::time::Duration;
309    /// let mut state = StyleMotionState::default();
310    /// let frame = state.sample(StyleMotion::new(StyleTarget::default().gap(12., 8.)), Duration::ZERO, false);
311    /// assert_eq!(frame.target().get(StyleProperty::GapX), Some(12.));
312    /// assert!(!frame.is_animating());
313    /// ```
314    pub fn sample(&mut self, motion: StyleMotion, now: Duration, reduced: bool) -> StyleFrame {
315        let mut result = StyleTarget::default();
316        let mut active = false;
317        let mut completed = false;
318
319        for i in 0..COUNT {
320            if let Some(value) = motion.target.values[i] {
321                self.destination.values[i] = Some(value);
322            }
323
324            if let Some(target) = self.destination.values[i] {
325                let initial = if self.initialized {
326                    target
327                } else {
328                    motion.initial.and_then(|v| v.values[i]).unwrap_or(target)
329                };
330
331                let state = self.values[i].get_or_insert_with(|| MotionValue::new(initial));
332
333                state.animate(
334                    target,
335                    motion.overrides[i].unwrap_or(motion.transition),
336                    now,
337                );
338                let frame = state.sample(now, reduced);
339
340                completed |= frame.just_completed();
341                result.values[i] = Some(frame.value());
342                active |= frame.is_animating();
343            }
344        }
345
346        for i in 0..4 {
347            if let Some(color) = motion.target.colors[i] {
348                completed |= self.colors[i].is_some() && self.destination.colors[i] != Some(color);
349                self.destination.colors[i] = Some(color);
350            }
351
352            if let Some(target) = self.destination.colors[i] {
353                let initial = if self.initialized {
354                    target
355                } else {
356                    motion.initial.and_then(|v| v.colors[i]).unwrap_or(target)
357                };
358
359                completed |= !self.initialized && initial != target;
360                let state = self.colors[i].get_or_insert_with(|| ColorState::new(initial));
361
362                let (color, running) = state.sample(
363                    target,
364                    motion.color_overrides[i].unwrap_or(motion.transition),
365                    now,
366                    reduced,
367                );
368
369                result.colors[i] = Some(color);
370                active |= running;
371            }
372        }
373
374        let completed = (self.active || completed) && !active;
375
376        self.active = active;
377        self.initialized = true;
378
379        StyleFrame {
380            target: result,
381            active,
382            completed,
383        }
384    }
385
386    /// Samples and schedules using native time; invoke during rendering.
387    #[cfg(feature = "gpui")]
388    pub fn frame(
389        &mut self,
390        motion: StyleMotion,
391        window: &mut gpui::Window,
392        cx: &gpui::App,
393    ) -> StyleFrame {
394        let now = *self.epoch.get_or_insert_with(std::time::Instant::now);
395        let frame = self.sample(motion, now.elapsed(), cx.reduce_motion());
396
397        if frame.is_animating() {
398            window.request_animation_frame();
399        }
400
401        frame
402    }
403}
404
405#[cfg(feature = "gpui")]
406impl StyleFrame {
407    /// Applies only introduced style tracks. Blur/filter/shadow lists are replaced
408    /// when declared. Layout-affecting tracks reflow; blur requires offscreen paint.
409    pub fn apply<E: gpui::Styled>(self, mut element: E) -> E {
410        use StyleProperty::*;
411        use gpui::px;
412
413        if let Some(v) = self.target.get(RadiusTopLeft) {
414            element.style().corner_radii.top_left = Some(px(v).into());
415        }
416
417        if let Some(v) = self.target.get(RadiusTopRight) {
418            element.style().corner_radii.top_right = Some(px(v).into());
419        }
420
421        if let Some(v) = self.target.get(RadiusBottomRight) {
422            element.style().corner_radii.bottom_right = Some(px(v).into());
423        }
424
425        if let Some(v) = self.target.get(RadiusBottomLeft) {
426            element.style().corner_radii.bottom_left = Some(px(v).into());
427        }
428
429        if let Some(v) = self.target.get(BorderTop) {
430            element.style().border_widths.top = Some(px(v).into());
431        }
432
433        if let Some(v) = self.target.get(BorderRight) {
434            element.style().border_widths.right = Some(px(v).into());
435        }
436
437        if let Some(v) = self.target.get(BorderBottom) {
438            element.style().border_widths.bottom = Some(px(v).into());
439        }
440
441        if let Some(v) = self.target.get(BorderLeft) {
442            element.style().border_widths.left = Some(px(v).into());
443        }
444
445        if let Some(v) = self.target.get(PaddingTop) {
446            element.style().padding.top = Some(px(v).into());
447        }
448
449        if let Some(v) = self.target.get(PaddingRight) {
450            element.style().padding.right = Some(px(v).into());
451        }
452
453        if let Some(v) = self.target.get(PaddingBottom) {
454            element.style().padding.bottom = Some(px(v).into());
455        }
456
457        if let Some(v) = self.target.get(PaddingLeft) {
458            element.style().padding.left = Some(px(v).into());
459        }
460
461        if let Some(v) = self.target.get(MarginTop) {
462            element.style().margin.top = Some(px(v).into());
463        }
464
465        if let Some(v) = self.target.get(MarginRight) {
466            element.style().margin.right = Some(px(v).into());
467        }
468
469        if let Some(v) = self.target.get(MarginBottom) {
470            element.style().margin.bottom = Some(px(v).into());
471        }
472
473        if let Some(v) = self.target.get(MarginLeft) {
474            element.style().margin.left = Some(px(v).into());
475        }
476
477        if let Some(v) = self.target.get(GapX) {
478            element.style().gap.width = Some(px(v).into());
479        }
480
481        if let Some(v) = self.target.get(GapY) {
482            element.style().gap.height = Some(px(v).into());
483        }
484
485        if let Some(color) = self.target.get_color(ColorProperty::Background) {
486            element = element.bg(color.to_rgba());
487        }
488
489        if let Some(color) = self.target.get_color(ColorProperty::Text) {
490            element = element.text_color(color.to_rgba());
491        }
492
493        if let Some(color) = self.target.get_color(ColorProperty::Border) {
494            element = element.border_color(color.to_rgba());
495        }
496
497        if self.target.values[18..22].iter().any(Option::is_some) || self.target.colors[3].is_some()
498        {
499            let color = self
500                .target
501                .get_color(ColorProperty::Shadow)
502                .unwrap_or(Color::rgba(0., 0., 0., 0.25))
503                .to_rgba();
504
505            element.style().box_shadow = Some(vec![
506                gpui::BoxShadow::new(
507                    px(self.target.get(ShadowX).unwrap_or(0.)),
508                    px(self.target.get(ShadowY).unwrap_or(0.)),
509                    gpui::rgb_to_hsla(color),
510                )
511                .blur_radius(px(self.target.get(ShadowBlur).unwrap_or(0.)))
512                .spread_radius(px(self.target.get(ShadowSpread).unwrap_or(0.))),
513            ]);
514        }
515
516        if let Some(radius) = self.target.get(Blur) {
517            element = element.filter(vec![gpui::Filter::Blur(px(radius))]);
518        }
519
520        if let Some(radius) = self.target.get(BackdropBlur) {
521            element = element.backdrop_filter(vec![gpui::Filter::Blur(px(radius))]);
522        }
523
524        element
525    }
526}
527
528impl crate::MotionData<40> for StyleTarget {
529    fn channels(self) -> [f64; 40] {
530        std::array::from_fn(|i| {
531            if i < COUNT {
532                self.values[i].unwrap_or(0.0)
533            } else {
534                self.colors[(i - COUNT) / 4].map_or(0.0, |color| {
535                    crate::MotionData::<4>::channels(color)[(i - COUNT) % 4]
536                })
537            }
538        })
539    }
540
541    fn with_channels(self, values: [f64; 40]) -> Self {
542        Self {
543            values: std::array::from_fn(|i| {
544                self.values[i].map(|_| {
545                    if signed(i) {
546                        values[i]
547                    } else {
548                        values[i].max(0.0)
549                    }
550                })
551            }),
552            colors: std::array::from_fn(|i| {
553                self.colors[i].map(|color| {
554                    crate::MotionData::<4>::with_channels(
555                        color,
556                        std::array::from_fn(|j| values[COUNT + i * 4 + j]),
557                    )
558                })
559            }),
560        }
561    }
562
563    fn compatible(self, other: Self) -> bool {
564        self.values.map(|v| v.is_some()) == other.values.map(|v| v.is_some())
565            && self.colors.map(|v| v.is_some()) == other.colors.map(|v| v.is_some())
566    }
567}