1use super::{
9 catalog::ShapeKind,
10 morph::Morph,
11 polygon::{Cubic, RoundedPolygon},
12};
13use crate::{
14 color::ColorRole,
15 motion::{self, MotionToken},
16 theme::color,
17 tokens::typography::rem,
18};
19use gpui::{prelude::*, *};
20
21pub const DEFAULT_SIZE: f32 = 48.;
23
24pub(crate) struct PathCache<K> {
29 key: Option<K>,
30 path: Option<Path<Pixels>>,
31}
32
33impl<K> Default for PathCache<K> {
34 fn default() -> Self {
35 Self {
36 key: None,
37 path: None,
38 }
39 }
40}
41
42impl<K: PartialEq> PathCache<K> {
43 pub(crate) fn path(
45 &mut self,
46 key: K,
47 build: impl FnOnce() -> Option<Path<Pixels>>,
48 ) -> Option<Path<Pixels>> {
49 if self.key.as_ref() != Some(&key) {
50 self.key = Some(key);
51 self.path = None;
52 return build();
53 }
54 if self.path.is_none() {
55 self.path = build();
56 }
57 self.path.clone()
58 }
59}
60
61fn outline_path(
64 each: impl Fn(&mut dyn FnMut(&Cubic)),
65 center: Point<Pixels>,
66 side: f32,
67 rotation_degrees: f32,
68) -> Option<Path<Pixels>> {
69 let mut bounds = [f32::MAX, f32::MAX, f32::MIN, f32::MIN];
70 each(&mut |cubic| {
71 for [x, y] in cubic.points.as_chunks::<2>().0 {
72 bounds[0] = bounds[0].min(*x);
73 bounds[1] = bounds[1].min(*y);
74 bounds[2] = bounds[2].max(*x);
75 bounds[3] = bounds[3].max(*y);
76 }
77 });
78 let (middle_x, middle_y) = ((bounds[0] + bounds[2]) / 2., (bounds[1] + bounds[3]) / 2.);
79 let (sin, cos) = rotation_degrees.to_radians().sin_cos();
80 let at = |x: f32, y: f32| {
81 let (x, y) = ((x - middle_x) * side, (y - middle_y) * side);
82 point(
83 center.x + px(x * cos - y * sin),
84 center.y + px(x * sin + y * cos),
85 )
86 };
87 let mut path = PathBuilder::fill();
88 let mut first = true;
89 each(&mut |cubic| {
90 let [a0x, a0y, c0x, c0y, c1x, c1y, a1x, a1y] = cubic.points;
91 if first {
92 path.move_to(at(a0x, a0y));
93 first = false;
94 }
95 path.cubic_bezier_to(at(a1x, a1y), at(c0x, c0y), at(c1x, c1y));
96 });
97 if first {
98 return None;
99 }
100 path.close();
101 path.build().ok()
102}
103
104pub fn polygon_path(
106 polygon: &RoundedPolygon,
107 center: Point<Pixels>,
108 side: f32,
109 rotation_degrees: f32,
110) -> Option<Path<Pixels>> {
111 outline_path(
112 |visit| polygon.cubics().iter().for_each(&mut *visit),
113 center,
114 side,
115 rotation_degrees,
116 )
117}
118
119pub fn morph_path(
123 morph: &Morph,
124 progress: f32,
125 center: Point<Pixels>,
126 side: f32,
127 rotation_degrees: f32,
128) -> Option<Path<Pixels>> {
129 outline_path(
130 |visit| morph.for_each_cubic(progress, &mut *visit),
131 center,
132 side,
133 rotation_degrees,
134 )
135}
136
137#[derive(Clone, Copy, Debug, PartialEq)]
140pub struct ShapeFrame {
141 pub from: ShapeKind,
143 pub to: ShapeKind,
145 pub progress: f32,
147 pub rotation_degrees: f32,
149}
150
151impl ShapeFrame {
152 pub const fn still(shape: ShapeKind) -> Self {
154 Self::morph(shape, shape, 1.)
155 }
156
157 pub const fn morph(from: ShapeKind, to: ShapeKind, progress: f32) -> Self {
160 Self {
161 from,
162 to,
163 progress: if progress.is_finite() { progress } else { 0. },
164 rotation_degrees: 0.,
165 }
166 }
167
168 pub fn path(self, center: Point<Pixels>, side: f32) -> Option<Path<Pixels>> {
170 if self.from == self.to {
171 polygon_path(self.to.polygon(), center, side, self.rotation_degrees)
172 } else {
173 morph_path(
174 self.from.morph_to(self.to),
175 self.progress,
176 center,
177 side,
178 self.rotation_degrees,
179 )
180 }
181 }
182}
183
184const HISTORY: usize = 8;
187
188struct ShapeHistory {
190 kinds: [ShapeKind; HISTORY],
191 generation: usize,
192}
193
194impl ShapeHistory {
195 fn new(kind: ShapeKind) -> Self {
196 Self {
197 kinds: [kind; HISTORY],
198 generation: 0,
199 }
200 }
201
202 fn current(&self) -> ShapeKind {
203 self.kinds[self.generation % HISTORY]
204 }
205
206 fn advance(&mut self, kind: ShapeKind) -> usize {
208 if self.current() != kind {
209 self.generation += 1;
210 self.kinds[self.generation % HISTORY] = kind;
211 }
212 self.generation
213 }
214
215 fn frame(&self, shown: f32) -> ShapeFrame {
218 if self.generation == 0 {
219 return ShapeFrame::still(self.current());
220 }
221 let oldest = self.generation.saturating_sub(HISTORY - 1);
222 let last = (shown.ceil().max(0.) as usize).clamp(oldest + 1, self.generation);
223 let progress = shown - (last - 1) as f32;
224 ShapeFrame::morph(
225 self.kinds[(last - 1) % HISTORY],
226 self.kinds[last % HISTORY],
227 if last == oldest + 1 {
229 progress.max(0.)
230 } else {
231 progress
232 },
233 )
234 }
235}
236
237fn animated_frame(
239 id: &ElementId,
240 kind: ShapeKind,
241 token: MotionToken,
242 window: &mut Window,
243 cx: &mut App,
244) -> ShapeFrame {
245 let history = window.use_keyed_state((id.clone(), "material-shape-history"), cx, |_, _| {
246 ShapeHistory::new(kind)
247 });
248 let generation = history.update(cx, |history, _| history.advance(kind));
249 let shown = motion::token_value(
250 (id.clone(), "material-shape-morph"),
251 generation as f32,
252 token,
253 window,
254 cx,
255 );
256 history.read(cx).frame(shown)
257}
258
259enum Source {
260 Frame(ShapeFrame),
261 Animated(ShapeKind),
262}
263
264#[derive(gpui::IntoElement)]
272pub struct MaterialShape {
273 id: ElementId,
274 source: Source,
275 role: ColorRole,
276 size: f32,
277 rotation_degrees: f32,
278 label: Option<SharedString>,
279 token: MotionToken,
280}
281
282impl MaterialShape {
283 pub fn new(id: impl Into<ElementId>, kind: ShapeKind) -> Self {
285 Self {
286 id: id.into(),
287 source: Source::Frame(ShapeFrame::still(kind)),
288 role: ColorRole::PRIMARY,
289 size: DEFAULT_SIZE,
290 rotation_degrees: 0.,
291 label: None,
292 token: MotionToken::DefaultSpatial,
293 }
294 }
295
296 pub fn frame(mut self, frame: ShapeFrame) -> Self {
298 self.rotation_degrees = frame.rotation_degrees;
299 self.source = Source::Frame(frame);
300 self
301 }
302
303 pub fn animated(mut self, animated: bool) -> Self {
306 let kind = match self.source {
307 Source::Frame(frame) => frame.to,
308 Source::Animated(kind) => kind,
309 };
310 self.source = if animated {
311 Source::Animated(kind)
312 } else {
313 Source::Frame(ShapeFrame::still(kind))
314 };
315 self
316 }
317
318 pub fn motion(mut self, token: MotionToken) -> Self {
321 self.token = token;
322 self
323 }
324
325 pub fn color(mut self, role: ColorRole) -> Self {
327 self.role = role;
328 self
329 }
330
331 pub fn size(mut self, size: f32) -> Self {
334 if size.is_finite() && size > 0. {
335 self.size = size;
336 }
337 self
338 }
339
340 pub fn rotation(mut self, degrees: f32) -> Self {
342 if degrees.is_finite() {
343 self.rotation_degrees = degrees;
344 }
345 self
346 }
347
348 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
350 self.label = Some(label.into());
351 self
352 }
353}
354
355impl RenderOnce for MaterialShape {
356 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
357 let mut frame = match self.source {
358 Source::Frame(frame) => frame,
359 Source::Animated(kind) => animated_frame(&self.id, kind, self.token, window, cx),
360 };
361 frame.rotation_degrees = self.rotation_degrees;
362 let ink = color(self.role);
363 let paths = window.use_keyed_state((self.id.clone(), "material-shape-path"), cx, |_, _| {
364 PathCache::<(ShapeFrame, f32, Point<Pixels>)>::default()
365 });
366 div()
367 .id(self.id)
368 .flex_shrink_0()
369 .size(rem(self.size))
370 .when_some(self.label, |node, label| {
371 node.role(Role::Image).aria_label(label)
372 })
373 .child(
374 canvas(
375 |_, _, _| {},
376 move |bounds, _, window, cx| {
377 let side = f32::from(bounds.size.width.min(bounds.size.height));
378 let center = bounds.center();
379 let path = paths.update(cx, |paths, _| {
380 paths.path((frame, side, center), || frame.path(center, side))
381 });
382 if let Some(path) = path {
383 window.paint_path(path, ink);
384 }
385 },
386 )
387 .size_full(),
388 )
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use core::prelude::v1::test;
396
397 #[test]
398 fn the_history_walks_through_each_shape_in_turn() {
399 let mut history = ShapeHistory::new(ShapeKind::Circle);
400 assert_eq!(history.frame(0.), ShapeFrame::still(ShapeKind::Circle));
401 assert_eq!(
402 history.advance(ShapeKind::Circle),
403 0,
404 "same shape: no morph"
405 );
406 assert_eq!(history.advance(ShapeKind::Heart), 1);
407 assert_eq!(history.advance(ShapeKind::Bun), 2);
408 let early = history.frame(0.33);
410 assert_eq!(
411 (early.from, early.to),
412 (ShapeKind::Circle, ShapeKind::Heart)
413 );
414 assert!((early.progress - 0.33).abs() < 1e-6);
415 let late = history.frame(1.25);
417 assert_eq!((late.from, late.to), (ShapeKind::Heart, ShapeKind::Bun));
418 assert!((late.progress - 0.25).abs() < 1e-6);
419 let over = history.frame(2.1);
421 assert_eq!((over.from, over.to), (ShapeKind::Heart, ShapeKind::Bun));
422 assert!((over.progress - 1.1).abs() < 1e-5);
423 assert_eq!(history.frame(2.).progress, 1.);
424 }
425
426 #[test]
427 fn long_bursts_of_changes_stay_inside_the_history() {
428 let mut history = ShapeHistory::new(ShapeKind::Circle);
429 for i in 0..100 {
430 history.advance(ShapeKind::ALL[1 + i % 34]);
431 }
432 let stale = history.frame(0.);
434 assert_eq!(stale.progress, 0.);
435 assert_eq!(
436 stale.from,
437 history.kinds[(history.generation - 7) % HISTORY]
438 );
439 assert!(history.frame(f32::NAN).progress.is_finite());
441 }
442
443 #[test]
444 fn frames_build_paths_and_refuse_bad_progress() {
445 let center = point(px(24.), px(24.));
446 for kind in ShapeKind::ALL {
447 assert!(
448 ShapeFrame::still(kind).path(center, 48.).is_some(),
449 "{kind:?}"
450 );
451 }
452 let frame = ShapeFrame::morph(ShapeKind::Circle, ShapeKind::Boom, f32::NAN);
453 assert_eq!(frame.progress, 0.);
454 assert!(frame.path(center, 48.).is_some());
455 let _ = frame.path(center, 0.);
457 }
458
459 #[test]
460 fn the_path_cache_keeps_only_a_key_seen_twice_in_a_row() {
461 let mut cache = PathCache::<u32>::default();
462 let builds = std::cell::Cell::new(0);
463 let build = || {
464 builds.set(builds.get() + 1);
465 ShapeFrame::still(ShapeKind::Bun).path(point(px(24.), px(24.)), 48.)
466 };
467 assert!(cache.path(1, build).is_some());
469 assert!(cache.path(2, build).is_some());
470 assert_eq!(builds.get(), 2);
471 assert!(cache.path.is_none());
472 assert!(cache.path(2, build).is_some());
474 assert!(cache.path(2, build).is_some());
475 assert!(cache.path(2, build).is_some());
476 assert_eq!(builds.get(), 3);
477 assert!(cache.path(3, build).is_some());
479 assert!(cache.path.is_none());
480 assert_eq!(builds.get(), 4);
481 }
482
483 struct Harness {
484 kind: ShapeKind,
485 drawn: Option<ShapeFrame>,
486 }
487
488 impl Render for Harness {
489 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
490 window.set_rem_size(px(14.));
491 self.drawn = Some(animated_frame(
492 &ElementId::from("probe"),
493 self.kind,
494 MotionToken::DefaultSpatial,
495 window,
496 cx,
497 ));
498 div()
499 .flex()
500 .items_start()
501 .child(
502 div().debug_selector(|| "shape".into()).child(
503 MaterialShape::new("shape", self.kind)
504 .label("Favorite")
505 .size(96.)
506 .animated(true),
507 ),
508 )
509 .child(MaterialShape::new("decorative", ShapeKind::Bun))
510 }
511 }
512
513 fn draw(cx: &mut VisualTestContext) {
514 cx.update(|window, cx| {
515 window.refresh();
516 window.draw(cx).clear(cx);
517 });
518 }
519
520 #[gpui::test]
521 fn a_shape_fills_its_rem_sized_square(cx: &mut gpui::TestAppContext) {
522 let (_, cx) = cx.add_window_view(|_, _| Harness {
523 kind: ShapeKind::Heart,
524 drawn: None,
525 });
526 draw(cx);
527 assert_eq!(
528 cx.debug_bounds("shape").expect("rendered").size,
529 size(px(96.), px(96.))
530 );
531 }
532
533 fn change_shape(cx: &mut gpui::TestAppContext, reduced: bool) -> ShapeFrame {
535 cx.update(|cx| cx.set_reduce_motion(reduced));
536 let (view, cx) = cx.add_window_view(|_, _| Harness {
537 kind: ShapeKind::Heart,
538 drawn: None,
539 });
540 draw(cx);
541 let first = view.read_with(cx, |view, _| view.drawn).expect("drawn");
542 assert_eq!(
543 first,
544 ShapeFrame::still(ShapeKind::Heart),
545 "no morph on mount"
546 );
547 view.update(cx, |view, cx| {
548 view.kind = ShapeKind::Bun;
549 cx.notify();
550 });
551 draw(cx);
552 view.read_with(cx, |view, _| view.drawn).expect("drawn")
553 }
554
555 #[gpui::test]
556 fn a_new_shape_morphs_from_the_last_one(cx: &mut gpui::TestAppContext) {
557 let frame = change_shape(cx, false);
558 assert_eq!((frame.from, frame.to), (ShapeKind::Heart, ShapeKind::Bun));
559 assert!(frame.progress < 1., "still on its way: {}", frame.progress);
560 }
561
562 #[gpui::test]
563 fn reduced_motion_shows_a_new_shape_at_once(cx: &mut gpui::TestAppContext) {
564 let frame = change_shape(cx, true);
565 assert_eq!((frame.to, frame.progress), (ShapeKind::Bun, 1.));
566 }
567}