1use crate::{Color, ColorState, MotionValue, Transition};
3use std::time::Duration;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8#[repr(usize)]
9pub enum StyleProperty {
10 RadiusTopLeft,
12 RadiusTopRight,
14 RadiusBottomRight,
16 RadiusBottomLeft,
18 BorderTop,
20 BorderRight,
22 BorderBottom,
24 BorderLeft,
26 PaddingTop,
28 PaddingRight,
30 PaddingBottom,
32 PaddingLeft,
34 MarginTop,
36 MarginRight,
38 MarginBottom,
40 MarginLeft,
42 GapX,
44 GapY,
46 ShadowX,
48 ShadowY,
50 ShadowBlur,
52 ShadowSpread,
54 Blur,
56 BackdropBlur,
58}
59
60const COUNT: usize = 24;
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64#[repr(usize)]
65pub enum ColorProperty {
66 Background,
68 Text,
70 Border,
72 Shadow,
74}
75
76fn signed(index: usize) -> bool {
77 (12..16).contains(&index) || [18, 19, 21].contains(&index)
78}
79
80#[derive(Clone, Copy, Debug, Default, PartialEq)]
83pub struct StyleTarget {
84 values: [Option<f64>; COUNT],
85 colors: [Option<Color>; 4],
86}
87
88impl StyleTarget {
89 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 pub fn color(mut self, property: ColorProperty, color: Color) -> Self {
104 self.colors[property as usize] = Some(color);
105
106 self
107 }
108
109 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 pub fn get_color(self, property: ColorProperty) -> Option<Color> {
127 self.colors[property as usize]
128 }
129
130 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 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 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 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 pub fn gap(self, x: f32, y: f32) -> Self {
200 self.value(StyleProperty::GapX, x)
201 .value(StyleProperty::GapY, y)
202 }
203
204 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#[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 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 pub fn initial(mut self, target: StyleTarget) -> Self {
238 self.initial = Some(target);
239
240 self
241 }
242
243 pub fn transition(mut self, transition: Transition) -> Self {
245 self.transition = transition;
246
247 self
248 }
249
250 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 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#[derive(Clone, Copy, Debug)]
267pub struct StyleFrame {
268 target: StyleTarget,
269 active: bool,
270 completed: bool,
271}
272
273impl StyleFrame {
274 pub fn target(self) -> StyleTarget {
276 self.target
277 }
278
279 pub fn is_animating(self) -> bool {
281 self.active
282 }
283
284 pub fn just_completed(self) -> bool {
286 self.completed
287 }
288}
289
290#[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 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 #[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 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}