1extern crate alloc;
23
24use core::f32::consts::{FRAC_PI_2, PI, TAU};
25#[cfg(feature = "gpu")]
26use core::fmt;
27use core::time::Duration;
28#[cfg(feature = "gpu")]
29use num_traits::ToPrimitive;
30#[cfg(feature = "gpu")]
31use std::time::Instant;
32
33#[cfg(feature = "gpu")]
34use nami::Signal as _;
35use nami::{Computed, SignalExt as _, signal::IntoComputed};
36#[cfg(feature = "gpu")]
37use shaderloom::CompiledShader;
38#[cfg(feature = "gpu")]
39use waterui_core::reactive::watcher::BoxWatcherGuard;
40use waterui_core::{Environment, View, easing::EasingCurve, metadata::MetadataKey};
41use waterui_graphics::color::Color;
42#[cfg(feature = "gpu")]
43use waterui_graphics::{
44 GpuContext, GpuFrame, GpuSurface, GpuView, reactive_color::ReactiveColor,
45 single_bind_group_render_stages,
46};
47
48#[cfg(feature = "gpu")]
49const MORPH_SHADER: CompiledShader = include!(concat!(env!("OUT_DIR"), "/morph.rs"));
50
51#[derive(Debug, Clone, Copy, PartialEq)]
60pub enum PathCommand {
61 MoveTo {
63 x: f32,
65 y: f32,
67 },
68
69 LineTo {
71 x: f32,
73 y: f32,
75 },
76
77 QuadTo {
79 cx: f32,
81 cy: f32,
83 x: f32,
85 y: f32,
87 },
88
89 CubicTo {
91 c1x: f32,
93 c1y: f32,
95 c2x: f32,
97 c2y: f32,
99 x: f32,
101 y: f32,
103 },
104
105 Arc {
107 cx: f32,
109 cy: f32,
111 rx: f32,
113 ry: f32,
115 start: f32,
117 sweep: f32,
119 },
120
121 Close,
123}
124
125#[inline]
126const fn clamp_radius(value: f32) -> f32 {
127 if value.is_finite() {
128 value.clamp(0.0, 0.5)
129 } else {
130 0.0
131 }
132}
133
134#[derive(Debug, Clone, Copy)]
135struct CornerRadii {
136 top_left: f32,
137 top_right: f32,
138 bottom_right: f32,
139 bottom_left: f32,
140}
141
142impl CornerRadii {
143 #[inline]
144 fn sanitized(mut self) -> Self {
145 self.top_left = clamp_radius(self.top_left);
146 self.top_right = clamp_radius(self.top_right);
147 self.bottom_right = clamp_radius(self.bottom_right);
148 self.bottom_left = clamp_radius(self.bottom_left);
149
150 let mut scale = 1.0f32;
152 let pairs = [
153 self.top_left + self.top_right,
154 self.bottom_left + self.bottom_right,
155 self.top_left + self.bottom_left,
156 self.top_right + self.bottom_right,
157 ];
158 for sum in pairs {
159 if sum > 1.0 {
160 scale = scale.min(1.0 / sum);
161 }
162 }
163 if scale < 1.0 {
164 self.top_left *= scale;
165 self.top_right *= scale;
166 self.bottom_right *= scale;
167 self.bottom_left *= scale;
168 }
169 self
170 }
171}
172
173pub trait Shape {
182 type Iter: IntoIterator<Item = PathCommand>;
184
185 fn path(&self) -> Self::Iter;
187
188 fn shape_kind(&self) -> ShapeKind {
196 ShapeKind::CustomPath
197 }
198}
199
200#[derive(Debug, Clone, Copy, Default)]
206pub struct Circle;
207
208impl Shape for Circle {
209 type Iter = [PathCommand; 1];
210
211 fn path(&self) -> Self::Iter {
212 [PathCommand::Arc {
213 cx: 0.5,
214 cy: 0.5,
215 rx: 0.5,
216 ry: 0.5,
217 start: 0.0,
218 sweep: TAU,
219 }]
220 }
221
222 fn shape_kind(&self) -> ShapeKind {
223 ShapeKind::Circle
224 }
225}
226
227#[derive(Debug, Clone, Copy, Default)]
229pub struct Ellipse;
230
231impl Shape for Ellipse {
232 type Iter = [PathCommand; 1];
233
234 fn path(&self) -> Self::Iter {
235 [PathCommand::Arc {
236 cx: 0.5,
237 cy: 0.5,
238 rx: 0.5,
239 ry: 0.5,
240 start: 0.0,
241 sweep: TAU,
242 }]
243 }
244
245 fn shape_kind(&self) -> ShapeKind {
246 ShapeKind::Ellipse
247 }
248}
249
250#[derive(Debug, Clone, Copy, Default)]
252pub struct Capsule;
253
254impl Shape for Capsule {
255 type Iter = [PathCommand; 4];
256
257 fn path(&self) -> Self::Iter {
264 [
265 PathCommand::MoveTo { x: 0.5, y: 0.0 },
266 PathCommand::Arc {
267 cx: 0.5,
268 cy: 0.5,
269 rx: 0.5,
270 ry: 0.5,
271 start: -FRAC_PI_2,
272 sweep: PI,
273 },
274 PathCommand::Arc {
275 cx: 0.5,
276 cy: 0.5,
277 rx: 0.5,
278 ry: 0.5,
279 start: FRAC_PI_2,
280 sweep: PI,
281 },
282 PathCommand::Close,
283 ]
284 }
285
286 fn shape_kind(&self) -> ShapeKind {
287 ShapeKind::Capsule
288 }
289}
290
291#[derive(Debug, Clone, Copy)]
293pub struct RoundedRectangle {
294 pub corner_radius: f32,
296}
297
298impl RoundedRectangle {
299 #[must_use]
310 pub const fn new(corner_radius: f32) -> Self {
311 Self { corner_radius }
312 }
313}
314
315impl Shape for RoundedRectangle {
316 type Iter = [PathCommand; 10];
317
318 fn path(&self) -> Self::Iter {
319 let r = CornerRadii {
320 top_left: self.corner_radius,
321 top_right: self.corner_radius,
322 bottom_right: self.corner_radius,
323 bottom_left: self.corner_radius,
324 }
325 .sanitized()
326 .top_left;
327 [
328 PathCommand::MoveTo { x: r, y: 0.0 },
329 PathCommand::LineTo { x: 1.0 - r, y: 0.0 },
330 PathCommand::Arc {
331 cx: 1.0 - r,
332 cy: r,
333 rx: r,
334 ry: r,
335 start: -FRAC_PI_2,
336 sweep: FRAC_PI_2,
337 },
338 PathCommand::LineTo { x: 1.0, y: 1.0 - r },
339 PathCommand::Arc {
340 cx: 1.0 - r,
341 cy: 1.0 - r,
342 rx: r,
343 ry: r,
344 start: 0.0,
345 sweep: FRAC_PI_2,
346 },
347 PathCommand::LineTo { x: r, y: 1.0 },
348 PathCommand::Arc {
349 cx: r,
350 cy: 1.0 - r,
351 rx: r,
352 ry: r,
353 start: FRAC_PI_2,
354 sweep: FRAC_PI_2,
355 },
356 PathCommand::LineTo { x: 0.0, y: r },
357 PathCommand::Arc {
358 cx: r,
359 cy: r,
360 rx: r,
361 ry: r,
362 start: PI,
363 sweep: FRAC_PI_2,
364 },
365 PathCommand::Close,
366 ]
367 }
368
369 fn shape_kind(&self) -> ShapeKind {
370 let r = CornerRadii {
371 top_left: self.corner_radius,
372 top_right: self.corner_radius,
373 bottom_right: self.corner_radius,
374 bottom_left: self.corner_radius,
375 }
376 .sanitized()
377 .top_left;
378 ShapeKind::RoundedRect { corner_radius: r }
379 }
380}
381
382#[derive(Debug, Clone, Copy)]
384pub struct UnevenRoundedRectangle {
385 pub top_leading: f32,
387 pub top_trailing: f32,
389 pub bottom_leading: f32,
391 pub bottom_trailing: f32,
393}
394
395impl UnevenRoundedRectangle {
396 #[must_use]
398 pub const fn new(
399 top_leading: f32,
400 top_trailing: f32,
401 bottom_leading: f32,
402 bottom_trailing: f32,
403 ) -> Self {
404 Self {
405 top_leading,
406 top_trailing,
407 bottom_leading,
408 bottom_trailing,
409 }
410 }
411}
412
413impl Shape for UnevenRoundedRectangle {
414 type Iter = [PathCommand; 10];
415
416 fn path(&self) -> Self::Iter {
417 let corners = CornerRadii {
418 top_left: self.top_leading,
419 top_right: self.top_trailing,
420 bottom_right: self.bottom_trailing,
421 bottom_left: self.bottom_leading,
422 }
423 .sanitized();
424 let tl = corners.top_left;
425 let tr = corners.top_right;
426 let bl = corners.bottom_left;
427 let br = corners.bottom_right;
428 [
429 PathCommand::MoveTo { x: tl, y: 0.0 },
430 PathCommand::LineTo {
431 x: 1.0 - tr,
432 y: 0.0,
433 },
434 PathCommand::Arc {
435 cx: 1.0 - tr,
436 cy: tr,
437 rx: tr,
438 ry: tr,
439 start: -FRAC_PI_2,
440 sweep: FRAC_PI_2,
441 },
442 PathCommand::LineTo {
443 x: 1.0,
444 y: 1.0 - br,
445 },
446 PathCommand::Arc {
447 cx: 1.0 - br,
448 cy: 1.0 - br,
449 rx: br,
450 ry: br,
451 start: 0.0,
452 sweep: FRAC_PI_2,
453 },
454 PathCommand::LineTo { x: bl, y: 1.0 },
455 PathCommand::Arc {
456 cx: bl,
457 cy: 1.0 - bl,
458 rx: bl,
459 ry: bl,
460 start: FRAC_PI_2,
461 sweep: FRAC_PI_2,
462 },
463 PathCommand::LineTo { x: 0.0, y: tl },
464 PathCommand::Arc {
465 cx: tl,
466 cy: tl,
467 rx: tl,
468 ry: tl,
469 start: PI,
470 sweep: FRAC_PI_2,
471 },
472 PathCommand::Close,
473 ]
474 }
475
476 fn shape_kind(&self) -> ShapeKind {
477 let corners = CornerRadii {
478 top_left: self.top_leading,
479 top_right: self.top_trailing,
480 bottom_right: self.bottom_trailing,
481 bottom_left: self.bottom_leading,
482 }
483 .sanitized();
484 ShapeKind::UnevenRoundedRect {
485 top_left: corners.top_left,
486 top_right: corners.top_right,
487 bottom_left: corners.bottom_left,
488 bottom_right: corners.bottom_right,
489 }
490 }
491}
492
493#[derive(Debug, Clone, Copy, Default)]
495pub struct Rectangle;
496
497impl Shape for Rectangle {
498 type Iter = [PathCommand; 5];
499
500 fn path(&self) -> Self::Iter {
501 [
502 PathCommand::MoveTo { x: 0.0, y: 0.0 },
503 PathCommand::LineTo { x: 1.0, y: 0.0 },
504 PathCommand::LineTo { x: 1.0, y: 1.0 },
505 PathCommand::LineTo { x: 0.0, y: 1.0 },
506 PathCommand::Close,
507 ]
508 }
509
510 fn shape_kind(&self) -> ShapeKind {
511 ShapeKind::Rect
512 }
513}
514
515#[derive(Debug, Clone, Default)]
521pub struct Path {
522 commands: Vec<PathCommand>,
523}
524
525impl Path {
526 #[must_use]
528 pub fn new() -> Self {
529 Self::default()
530 }
531
532 #[must_use]
534 pub fn move_to(mut self, x: f32, y: f32) -> Self {
535 self.commands.push(PathCommand::MoveTo { x, y });
536 self
537 }
538
539 #[must_use]
541 pub fn line_to(mut self, x: f32, y: f32) -> Self {
542 self.commands.push(PathCommand::LineTo { x, y });
543 self
544 }
545
546 #[must_use]
548 pub fn quad_to(mut self, cx: f32, cy: f32, x: f32, y: f32) -> Self {
549 self.commands.push(PathCommand::QuadTo { cx, cy, x, y });
550 self
551 }
552
553 #[must_use]
555 pub fn cubic_to(mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) -> Self {
556 self.commands.push(PathCommand::CubicTo {
557 c1x,
558 c1y,
559 c2x,
560 c2y,
561 x,
562 y,
563 });
564 self
565 }
566
567 #[must_use]
569 pub fn arc(mut self, cx: f32, cy: f32, rx: f32, ry: f32, start: f32, sweep: f32) -> Self {
570 self.commands.push(PathCommand::Arc {
571 cx,
572 cy,
573 rx,
574 ry,
575 start,
576 sweep,
577 });
578 self
579 }
580
581 #[must_use]
583 pub fn close(mut self) -> Self {
584 self.commands.push(PathCommand::Close);
585 self
586 }
587}
588
589impl Shape for Path {
590 type Iter = alloc::vec::IntoIter<PathCommand>;
591
592 fn path(&self) -> Self::Iter {
593 self.commands.clone().into_iter()
594 }
595
596 fn shape_kind(&self) -> ShapeKind {
597 ShapeKind::CustomPath
598 }
599}
600
601#[derive(Debug)]
615pub struct ClipShape {
616 kind: ShapeKind,
617 commands: Vec<PathCommand>,
618}
619
620impl ClipShape {
621 #[allow(clippy::needless_pass_by_value)]
623 pub fn new(shape: impl Shape) -> Self {
624 Self {
625 kind: shape.shape_kind(),
626 commands: shape.path().into_iter().collect(),
627 }
628 }
629
630 #[must_use]
633 pub const fn kind(&self) -> ShapeKind {
634 self.kind
635 }
636
637 #[must_use]
639 pub fn commands(&self) -> &[PathCommand] {
640 &self.commands
641 }
642}
643
644impl MetadataKey for ClipShape {}
645
646#[derive(Debug, Clone, Copy, Default)]
652pub enum ShapeKind {
653 #[default]
655 Rect,
656 Circle,
658 Ellipse,
660 RoundedRect {
662 corner_radius: f32,
664 },
665 UnevenRoundedRect {
667 top_left: f32,
669 top_right: f32,
671 bottom_left: f32,
673 bottom_right: f32,
675 },
676 Capsule,
678 CustomPath,
680}
681
682#[derive(Debug, Clone)]
684pub struct ResolvedShape {
685 pub kind: ShapeKind,
687 pub commands: Vec<PathCommand>,
689 pub fill: Computed<waterui_graphics::ResolvedColor>,
691}
692
693waterui_core::raw_view!(ResolvedShape, waterui_core::layout::StretchAxis::Both);
694
695#[derive(Debug, Clone)]
697pub struct ResolvedMorphShape {
698 pub from: ShapeKind,
700 pub to: ShapeKind,
702 pub fill: Computed<waterui_graphics::ResolvedColor>,
704 pub animation: MorphAnimation,
706 pub progress: Option<Computed<f32>>,
708}
709
710impl waterui_core::NativeView for ResolvedMorphShape {
711 fn stretch_axis(&self) -> waterui_core::layout::StretchAxis {
712 waterui_core::layout::StretchAxis::Both
713 }
714}
715
716#[derive(Debug)]
722pub struct FilledShape {
723 kind: ShapeKind,
724 commands: Vec<PathCommand>,
725 fill: Color,
726}
727
728impl FilledShape {
729 #[allow(clippy::needless_pass_by_value)]
731 pub fn new(shape: impl Shape, fill: impl Into<Color>) -> Self {
732 Self {
733 kind: ShapeKind::CustomPath,
734 commands: shape.path().into_iter().collect(),
735 fill: fill.into(),
736 }
737 }
738
739 #[allow(clippy::needless_pass_by_value)]
740 fn with_kind(kind: ShapeKind, shape: impl Shape, fill: impl Into<Color>) -> Self {
741 Self {
742 kind,
743 commands: shape.path().into_iter().collect(),
744 fill: fill.into(),
745 }
746 }
747
748 #[must_use]
750 pub fn commands(&self) -> &[PathCommand] {
751 &self.commands
752 }
753
754 #[must_use]
756 pub const fn fill(&self) -> &Color {
757 &self.fill
758 }
759
760 #[must_use]
762 pub const fn kind(&self) -> ShapeKind {
763 self.kind
764 }
765
766 #[must_use]
771 #[allow(clippy::needless_pass_by_value)]
772 pub fn morph_to(self, target: impl ShapeExt) -> MorphShape {
773 MorphShape::new(self.kind, target.shape_kind(), self.fill)
774 }
775}
776
777#[derive(Debug, Clone, Copy, PartialEq)]
779pub struct MorphAnimation {
780 pub duration: Duration,
782 pub easing: EasingCurve,
784 pub repeat: bool,
786 pub autoreverse: bool,
788}
789
790impl Default for MorphAnimation {
791 fn default() -> Self {
792 Self {
793 duration: Duration::from_millis(900),
794 easing: EasingCurve::EASE_IN_OUT,
795 repeat: true,
796 autoreverse: true,
797 }
798 }
799}
800
801impl MorphAnimation {
802 #[must_use]
804 pub const fn once(duration: Duration, easing: EasingCurve) -> Self {
805 Self {
806 duration,
807 easing,
808 repeat: false,
809 autoreverse: false,
810 }
811 }
812
813 #[cfg(feature = "gpu")]
814 #[must_use]
815 fn sample(self, elapsed: Duration) -> f32 {
816 if self.duration.is_zero() {
817 return 1.0;
818 }
819 let raw = elapsed.as_secs_f32() / self.duration.as_secs_f32();
820 let cycle = if self.repeat {
821 let base = raw.fract();
822 let index = raw
823 .floor()
824 .to_u64()
825 .expect("MorphAnimation::sample: cycle index must fit into u64");
826 if self.autoreverse && index % 2 == 1 {
827 1.0 - base
828 } else {
829 base
830 }
831 } else {
832 raw.clamp(0.0, 1.0)
833 };
834 self.easing.ease(cycle).clamp(0.0, 1.0)
835 }
836}
837
838#[derive(Debug, Clone)]
840pub struct MorphShape {
841 from: ShapeKind,
842 to: ShapeKind,
843 fill: Color,
844 animation: MorphAnimation,
845 progress: Option<Computed<f32>>,
846}
847
848impl MorphShape {
849 fn new(from: ShapeKind, to: ShapeKind, fill: Color) -> Self {
850 Self {
851 from,
852 to,
853 fill,
854 animation: MorphAnimation::default(),
855 progress: None,
856 }
857 }
858
859 #[must_use]
861 pub const fn animation(mut self, animation: MorphAnimation) -> Self {
862 self.animation = animation;
863 self
864 }
865
866 #[must_use]
868 pub const fn duration(mut self, duration: Duration) -> Self {
869 self.animation.duration = duration;
870 self
871 }
872
873 #[must_use]
875 pub const fn easing(mut self, easing: EasingCurve) -> Self {
876 self.animation.easing = easing;
877 self
878 }
879
880 #[must_use]
882 pub const fn repeat(mut self, repeat: bool) -> Self {
883 self.animation.repeat = repeat;
884 self
885 }
886
887 #[must_use]
889 pub const fn autoreverse(mut self, autoreverse: bool) -> Self {
890 self.animation.autoreverse = autoreverse;
891 self
892 }
893
894 #[must_use]
898 pub fn progress(mut self, progress: impl IntoComputed<f32>) -> Self {
899 self.progress = Some(progress.into_computed());
900 self
901 }
902}
903
904impl View for FilledShape {
905 fn body(self, env: &Environment) -> impl View {
906 ResolvedShape {
907 kind: self.kind,
908 commands: self.commands,
909 fill: self.fill.resolve(env).computed(),
910 }
911 }
912}
913
914impl View for MorphShape {
915 fn body(self, env: &Environment) -> impl View {
916 let resolved = self.fill.resolve(env).computed();
917 #[cfg(feature = "gpu")]
920 let progress_for_gpu = self.progress.clone();
921 let native = waterui_core::Native::new(ResolvedMorphShape {
922 from: self.from,
923 to: self.to,
924 fill: resolved,
925 animation: self.animation,
926 progress: self.progress,
927 });
928 #[cfg(feature = "gpu")]
929 let native = native.with_fallback(GpuSurface::new(MorphShapeRenderer::new(
930 kind_to_morph_shape(self.from)
931 .expect("morph source shape must be a built-in morphable shape"),
932 kind_to_morph_shape(self.to)
933 .expect("morph target shape must be a built-in morphable shape"),
934 ReactiveColor::new(&Computed::constant(self.fill), env),
935 self.animation,
936 progress_for_gpu,
937 )));
938 native
939 }
940}
941
942#[cfg(feature = "gpu")]
947#[derive(Debug, Clone, Copy)]
948struct MorphSdfShape {
949 shape_type: u32,
950 radii: [f32; 4],
951}
952
953#[cfg(feature = "gpu")]
954fn kind_to_morph_shape(kind: ShapeKind) -> Option<MorphSdfShape> {
955 match kind {
956 ShapeKind::Rect => Some(MorphSdfShape {
957 shape_type: 0,
958 radii: [0.0; 4],
959 }),
960 ShapeKind::Circle => Some(MorphSdfShape {
961 shape_type: 1,
962 radii: [0.0; 4],
963 }),
964 ShapeKind::Ellipse => Some(MorphSdfShape {
965 shape_type: 2,
966 radii: [0.0; 4],
967 }),
968 ShapeKind::RoundedRect { corner_radius } => Some(MorphSdfShape {
969 shape_type: 3,
970 radii: [clamp_radius(corner_radius); 4],
971 }),
972 ShapeKind::UnevenRoundedRect {
973 top_left,
974 top_right,
975 bottom_left,
976 bottom_right,
977 } => {
978 let corners = CornerRadii {
979 top_left,
980 top_right,
981 bottom_right,
982 bottom_left,
983 }
984 .sanitized();
985 Some(MorphSdfShape {
986 shape_type: 3,
987 radii: [
988 corners.top_left,
989 corners.top_right,
990 corners.bottom_right,
991 corners.bottom_left,
992 ],
993 })
994 }
995 ShapeKind::Capsule => Some(MorphSdfShape {
996 shape_type: 4,
997 radii: [0.0; 4],
998 }),
999 ShapeKind::CustomPath => None,
1000 }
1001}
1002
1003#[cfg(feature = "gpu")]
1004#[repr(C)]
1005#[derive(Debug, Clone, Copy, Default, bytemuck::Pod, bytemuck::Zeroable)]
1006struct MorphUniforms {
1007 color: [f32; 4],
1008 dimensions_and_progress: [f32; 4], shape_types: [f32; 4], from_radii: [f32; 4], to_radii: [f32; 4], }
1013
1014#[cfg(feature = "gpu")]
1015struct MorphShapeRenderer {
1016 from: MorphSdfShape,
1017 to: MorphSdfShape,
1018 fill_color: ReactiveColor,
1019 animation: MorphAnimation,
1020 progress: Option<Computed<f32>>,
1021 progress_guard: Option<BoxWatcherGuard>,
1022 start_time: Instant,
1023 pipeline: Option<wgpu::RenderPipeline>,
1024 uniform_buffer: Option<wgpu::Buffer>,
1025 bind_group: Option<wgpu::BindGroup>,
1026 pipeline_format: Option<wgpu::TextureFormat>,
1027}
1028
1029#[cfg(feature = "gpu")]
1030impl fmt::Debug for MorphShapeRenderer {
1031 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1032 f.debug_struct("MorphShapeRenderer")
1033 .field("from", &self.from)
1034 .field("to", &self.to)
1035 .finish_non_exhaustive()
1036 }
1037}
1038
1039#[cfg(feature = "gpu")]
1040impl MorphShapeRenderer {
1041 fn new(
1042 from: MorphSdfShape,
1043 to: MorphSdfShape,
1044 fill_color: ReactiveColor,
1045 animation: MorphAnimation,
1046 progress: Option<Computed<f32>>,
1047 ) -> Self {
1048 Self {
1049 from,
1050 to,
1051 fill_color,
1052 animation,
1053 progress,
1054 progress_guard: None,
1055 start_time: Instant::now(),
1056 pipeline: None,
1057 uniform_buffer: None,
1058 bind_group: None,
1059 pipeline_format: None,
1060 }
1061 }
1062}
1063
1064#[cfg(feature = "gpu")]
1065impl GpuView for MorphShapeRenderer {
1066 fn setup(
1067 &mut self,
1068 ctx: &GpuContext<'_>,
1069 _env: &mut waterui_core::Environment,
1070 ) -> impl core::future::Future<Output = ()> {
1071 self.fill_color.install(&ctx.redraw_handle);
1072 if let Some(progress) = &self.progress {
1073 let redraw = ctx.redraw_handle.clone();
1074 self.progress_guard = Some(progress.watch(move |_| redraw.request_redraw()));
1075 }
1076
1077 let (vertex_shader, fragment_shader, bind_group_layout) = single_bind_group_render_stages(
1078 &MORPH_SHADER,
1079 ctx.device,
1080 "the morph shape shader",
1081 "vs_main",
1082 "fs_main",
1083 );
1084
1085 let uniform_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
1086 label: Some("Morph Shape Uniforms"),
1087 size: core::mem::size_of::<MorphUniforms>() as u64,
1088 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1089 mapped_at_creation: false,
1090 });
1091
1092 let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
1093 label: Some("Morph Shape Bind Group"),
1094 layout: &bind_group_layout,
1095 entries: &[wgpu::BindGroupEntry {
1096 binding: 0,
1097 resource: uniform_buffer.as_entire_binding(),
1098 }],
1099 });
1100
1101 let pipeline_layout = ctx
1102 .device
1103 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1104 label: Some("Morph Shape Pipeline Layout"),
1105 bind_group_layouts: &[Some(&bind_group_layout)],
1106 immediate_size: 0,
1107 });
1108
1109 let blend = ctx.alpha_blend_state();
1110
1111 let pipeline = ctx
1112 .device
1113 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1114 label: Some("Morph Shape Pipeline"),
1115 layout: Some(&pipeline_layout),
1116 vertex: wgpu::VertexState {
1117 module: vertex_shader.module(),
1118 entry_point: Some(vertex_shader.entry_point()),
1119 buffers: &[],
1120 compilation_options: wgpu::PipelineCompilationOptions::default(),
1121 },
1122 fragment: Some(wgpu::FragmentState {
1123 module: fragment_shader.module(),
1124 entry_point: Some(fragment_shader.entry_point()),
1125 targets: &[Some(wgpu::ColorTargetState {
1126 format: ctx.surface_format,
1127 blend,
1128 write_mask: wgpu::ColorWrites::ALL,
1129 })],
1130 compilation_options: wgpu::PipelineCompilationOptions::default(),
1131 }),
1132 primitive: wgpu::PrimitiveState {
1133 topology: wgpu::PrimitiveTopology::TriangleList,
1134 ..Default::default()
1135 },
1136 depth_stencil: None,
1137 multisample: wgpu::MultisampleState::default(),
1138 multiview_mask: None,
1139 cache: None,
1140 });
1141
1142 self.pipeline = Some(pipeline);
1143 self.uniform_buffer = Some(uniform_buffer);
1144 self.bind_group = Some(bind_group);
1145 self.pipeline_format = Some(ctx.surface_format);
1146 self.start_time = Instant::now();
1147 core::future::ready(())
1148 }
1149
1150 fn render(&mut self, frame: &mut GpuFrame) {
1151 assert_eq!(
1152 self.pipeline_format,
1153 Some(frame.format),
1154 "MorphShape target format changed after setup"
1155 );
1156 let pipeline = self
1157 .pipeline
1158 .as_ref()
1159 .expect("MorphShape render called before setup");
1160 let uniform_buffer = self
1161 .uniform_buffer
1162 .as_ref()
1163 .expect("MorphShape render called before setup");
1164 let bind_group = self
1165 .bind_group
1166 .as_ref()
1167 .expect("MorphShape render called before setup");
1168
1169 let progress = if let Some(signal) = &self.progress {
1170 let value = signal.get();
1171 assert!(value.is_finite(), "MorphShape progress must be finite");
1172 value.clamp(0.0, 1.0)
1173 } else {
1174 self.animation.sample(self.start_time.elapsed())
1175 };
1176
1177 let fill_color = self.fill_color.get();
1178 let [r, g, b] = fill_color.linear_with_headroom();
1179 let uniforms = MorphUniforms {
1180 color: [r, g, b, fill_color.opacity],
1181 dimensions_and_progress: [
1182 u32_to_f32(frame.width),
1183 u32_to_f32(frame.height),
1184 progress,
1185 0.0,
1186 ],
1187 shape_types: [
1188 u32_to_f32(self.from.shape_type),
1189 u32_to_f32(self.to.shape_type),
1190 0.0,
1191 0.0,
1192 ],
1193 from_radii: self.from.radii,
1194 to_radii: self.to.radii,
1195 };
1196 frame
1197 .queue
1198 .write_buffer(uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
1199
1200 let mut encoder = frame
1201 .device
1202 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1203 label: Some("Morph Shape Encoder"),
1204 });
1205
1206 {
1207 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1208 label: Some("Morph Shape Render Pass"),
1209 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1210 view: &frame.view,
1211 depth_slice: None,
1212 resolve_target: None,
1213 ops: wgpu::Operations {
1214 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1215 store: wgpu::StoreOp::Store,
1216 },
1217 })],
1218 depth_stencil_attachment: None,
1219 timestamp_writes: None,
1220 occlusion_query_set: None,
1221 multiview_mask: None,
1222 });
1223
1224 render_pass.set_pipeline(pipeline);
1225 render_pass.set_bind_group(0, bind_group, &[]);
1226 render_pass.draw(0..6, 0..1);
1227 }
1228
1229 frame.queue.submit(core::iter::once(encoder.finish()));
1230
1231 let animation_active = self.progress.is_none()
1233 && (self.animation.repeat || self.start_time.elapsed() < self.animation.duration);
1234 if animation_active {
1235 frame.request_redraw();
1236 }
1237 }
1238}
1239
1240#[cfg(feature = "gpu")]
1241fn u32_to_f32(value: u32) -> f32 {
1242 value
1243 .to_f32()
1244 .expect("shape dimensions must be representable as f32")
1245}
1246
1247pub trait ShapeExt: Shape + Sized {
1253 fn fill(self, color: impl Into<Color>) -> FilledShape {
1255 FilledShape::with_kind(self.shape_kind(), self, color)
1256 }
1257
1258 fn morph_to(self, target: impl ShapeExt, fill: impl Into<Color>) -> MorphShape {
1263 MorphShape::new(self.shape_kind(), target.shape_kind(), fill.into())
1264 }
1265}
1266
1267impl ShapeExt for Circle {}
1268
1269impl ShapeExt for Ellipse {}
1270
1271impl ShapeExt for Capsule {}
1272
1273impl ShapeExt for Rectangle {}
1274
1275impl ShapeExt for RoundedRectangle {}
1276
1277impl ShapeExt for UnevenRoundedRectangle {}
1278
1279impl ShapeExt for Path {}
1280
1281#[cfg(test)]
1282mod tests {
1283 use super::*;
1284
1285 #[test]
1286 fn rounded_rectangle_radius_is_clamped() {
1287 let kind = RoundedRectangle::new(9.0).shape_kind();
1288 match kind {
1289 ShapeKind::RoundedRect { corner_radius } => {
1290 assert!((corner_radius - 0.5).abs() < 1e-6);
1291 }
1292 _ => panic!("unexpected kind"),
1293 }
1294 }
1295
1296 #[test]
1297 fn uneven_radii_are_normalized_when_edges_overlap() {
1298 let kind = UnevenRoundedRectangle::new(0.8, 0.8, 0.8, 0.8).shape_kind();
1299 match kind {
1300 ShapeKind::UnevenRoundedRect {
1301 top_left,
1302 top_right,
1303 bottom_left,
1304 bottom_right,
1305 } => {
1306 assert!((top_left - 0.5).abs() < 1e-6);
1307 assert!((top_right - 0.5).abs() < 1e-6);
1308 assert!((bottom_left - 0.5).abs() < 1e-6);
1309 assert!((bottom_right - 0.5).abs() < 1e-6);
1310 }
1311 _ => panic!("unexpected kind"),
1312 }
1313 }
1314
1315 #[cfg(feature = "gpu")]
1316 #[test]
1317 fn one_shot_animation_reaches_end() {
1318 let animation = MorphAnimation::once(Duration::from_millis(200), EasingCurve::LINEAR);
1319 assert!((animation.sample(Duration::ZERO) - 0.0).abs() < 1e-6);
1320 assert!((animation.sample(Duration::from_millis(100)) - 0.5).abs() < 1e-3);
1321 assert!((animation.sample(Duration::from_secs(1)) - 1.0).abs() < 1e-6);
1322 }
1323}