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