Skip to main content

gpui_motion_kit/
svg.rs

1//! Native SVG drawing transforms, without transforming a subtree or its hitbox.
2use crate::core::SvgMotion;
3use gpui_kit::{App, Window, px};
4use std::{ops::Deref, time::Duration};
5
6/// Retained SVG transform sampler using Kit's monotonic executor clock.
7#[derive(Debug, Default)]
8pub struct SvgMotionState {
9    inner: crate::core::SvgMotionState,
10    clock: crate::Clock,
11}
12
13impl SvgMotionState {
14    /// Samples while rendering and requests frames only during active motion.
15    pub fn frame(&mut self, motion: SvgMotion, window: &mut Window, cx: &App) -> SvgFrame {
16        let now = self.clock.tick(cx);
17
18        let frame = self.sample(motion, now, cx.reduce_motion());
19
20        if frame.is_animating() {
21            window.request_animation_frame();
22        }
23
24        frame
25    }
26
27    /// Samples an explicit epoch; do not mix this clock with native `frame` calls.
28    pub fn sample(&mut self, motion: SvgMotion, now: Duration, reduced_motion: bool) -> SvgFrame {
29        SvgFrame(self.inner.sample(motion, now, reduced_motion))
30    }
31}
32
33/// A portable SVG presentation with Kit-native transform conversion.
34#[derive(Clone, Copy, Debug)]
35pub struct SvgFrame(crate::core::SvgFrame);
36
37impl From<crate::core::SvgFrame> for SvgFrame {
38    fn from(frame: crate::core::SvgFrame) -> Self {
39        Self(frame)
40    }
41}
42
43impl Deref for SvgFrame {
44    type Target = crate::core::SvgFrame;
45
46    fn deref(&self) -> &Self::Target {
47        &self.0
48    }
49}
50
51impl SvgFrame {
52    /// Converts to Kit's center-origin SVG transform: signed scale, then rotation.
53    /// Only the drawing angle is reduced modulo one turn, preserving the engine's
54    /// unwrapped presentation and velocity. This replaces any existing SVG transform.
55    pub fn transformation(self) -> gpui_kit::Transformation {
56        let [x, y] = self.target().scale_factors();
57
58        let angle = self
59            .target()
60            .rotation_degrees()
61            .rem_euclid(360.0)
62            .to_radians() as f32;
63
64        gpui_kit::Transformation::scale(gpui_kit::size(x, y))
65            .with_rotation(gpui_kit::radians(angle))
66            .with_translation(gpui_kit::point(
67                px(self.target().translation()[0]),
68                px(self.target().translation()[1]),
69            ))
70    }
71
72    /// Converts with a custom pivot expressed as logical pixels from the SVG's
73    /// top-left. Supply its untransformed bounds size. Translation compensates
74    /// for Kit's fixed center origin. Native layout/hitbox remain unchanged.
75    pub fn transformation_about(
76        self,
77        bounds: gpui_kit::Size<gpui_kit::Pixels>,
78        pivot: gpui_kit::Point<gpui_kit::Pixels>,
79    ) -> gpui_kit::Transformation {
80        assert!(
81            [
82                f32::from(bounds.width),
83                f32::from(bounds.height),
84                f32::from(pivot.x),
85                f32::from(pivot.y)
86            ]
87            .iter()
88            .all(|v| v.is_finite())
89                && bounds.width >= px(0.)
90                && bounds.height >= px(0.),
91            "SVG bounds and pivot must be finite with non-negative size"
92        );
93        let [sx, sy] = self.target().scale_factors().map(f64::from);
94
95        let angle = self
96            .target()
97            .rotation_degrees()
98            .rem_euclid(360.)
99            .to_radians();
100
101        let (sin, cos) = angle.sin_cos();
102        let dx = f64::from(pivot.x) - f64::from(bounds.width) * 0.5;
103        let dy = f64::from(pivot.y) - f64::from(bounds.height) * 0.5;
104        let [tx, ty] = self.target().translation().map(f64::from);
105        let finite = |v: f64| px(v.clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32);
106
107        self.transformation().with_translation(gpui_kit::point(
108            finite(tx + dx - (cos * sx * dx - sin * sy * dy)),
109            finite(ty + dy - (sin * sx * dx + cos * sy * dy)),
110        ))
111    }
112
113    /// Applies GPU scale/rotation to one Kit SVG drawing around its bounds center.
114    /// Native layout and hitbox stay unchanged, and clipping follows Kit's existing
115    /// axis-aligned masks. This does not transform surrounding divs, text, or children.
116    /// Use a fixed, sufficiently padded parent as the interactive hit target.
117    ///
118    /// ```
119    /// use gpui_kit::{svg, prelude::*, px};
120    /// use gpui_motion_kit::{SvgMotionState, core::{SvgMotion, SvgTarget}};
121    /// use std::time::Duration;
122    /// let mut state = SvgMotionState::default();
123    /// let frame = state.sample(SvgMotion::new(SvgTarget::default().scale(1.2).rotate_degrees(30.0)), Duration::ZERO, false);
124    /// let element = frame.apply(svg().path("icons/arrow.svg").size(px(24.0)));
125    /// ```
126    pub fn apply(self, element: gpui_kit::Svg) -> gpui_kit::Svg {
127        element.with_transformation(self.transformation())
128    }
129}