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
31#[cfg(feature = "gpu")]
32use nami::Signal as _;
33use nami::{Computed, SignalExt as _, signal::IntoComputed};
34#[cfg(feature = "gpu")]
35use shaderloom::CompiledShader;
36#[cfg(feature = "gpu")]
37use waterui_core::reactive::watcher::BoxWatcherGuard;
38use waterui_core::{Environment, View, easing::EasingCurve, metadata::MetadataKey};
39use waterui_graphics::color::Color;
40#[cfg(feature = "gpu")]
41use waterui_graphics::{
42 GpuContext, GpuFrame, GpuSurface, GpuView, reactive_color::ReactiveColor,
43 single_bind_group_render_stages,
44};
45
46#[cfg(feature = "gpu")]
47const MORPH_SHADER: CompiledShader = include!(concat!(env!("OUT_DIR"), "/morph.rs"));
48
49#[derive(Debug, Clone, Copy, PartialEq)]
58pub enum PathCommand {
59 MoveTo {
61 x: f32,
63 y: f32,
65 },
66
67 LineTo {
69 x: f32,
71 y: f32,
73 },
74
75 QuadTo {
77 cx: f32,
79 cy: f32,
81 x: f32,
83 y: f32,
85 },
86
87 CubicTo {
89 c1x: f32,
91 c1y: f32,
93 c2x: f32,
95 c2y: f32,
97 x: f32,
99 y: f32,
101 },
102
103 Arc {
105 cx: f32,
107 cy: f32,
109 rx: f32,
111 ry: f32,
113 start: f32,
115 sweep: f32,
117 },
118
119 Close,
121}
122
123#[inline]
124const fn clamp_radius(value: f32) -> f32 {
125 if value.is_finite() {
126 value.clamp(0.0, 0.5)
127 } else {
128 0.0
129 }
130}
131
132#[derive(Debug, Clone, Copy)]
133struct CornerRadii {
134 top_left: f32,
135 top_right: f32,
136 bottom_right: f32,
137 bottom_left: f32,
138}
139
140impl CornerRadii {
141 #[inline]
142 fn sanitized(mut self) -> Self {
143 self.top_left = clamp_radius(self.top_left);
144 self.top_right = clamp_radius(self.top_right);
145 self.bottom_right = clamp_radius(self.bottom_right);
146 self.bottom_left = clamp_radius(self.bottom_left);
147
148 let mut scale = 1.0f32;
150 let pairs = [
151 self.top_left + self.top_right,
152 self.bottom_left + self.bottom_right,
153 self.top_left + self.bottom_left,
154 self.top_right + self.bottom_right,
155 ];
156 for sum in pairs {
157 if sum > 1.0 {
158 scale = scale.min(1.0 / sum);
159 }
160 }
161 if scale < 1.0 {
162 self.top_left *= scale;
163 self.top_right *= scale;
164 self.bottom_right *= scale;
165 self.bottom_left *= scale;
166 }
167 self
168 }
169}
170
171pub trait Shape {
180 type Iter: IntoIterator<Item = PathCommand>;
182
183 fn path(&self) -> Self::Iter;
185
186 fn shape_kind(&self) -> ShapeKind {
194 ShapeKind::CustomPath
195 }
196}
197
198#[derive(Debug, Clone, Copy, Default)]
204pub struct Circle;
205
206impl Shape for Circle {
207 type Iter = [PathCommand; 1];
208
209 fn path(&self) -> Self::Iter {
210 [PathCommand::Arc {
211 cx: 0.5,
212 cy: 0.5,
213 rx: 0.5,
214 ry: 0.5,
215 start: 0.0,
216 sweep: TAU,
217 }]
218 }
219
220 fn shape_kind(&self) -> ShapeKind {
221 ShapeKind::Circle
222 }
223}
224
225#[derive(Debug, Clone, Copy, Default)]
227pub struct Ellipse;
228
229impl Shape for Ellipse {
230 type Iter = [PathCommand; 1];
231
232 fn path(&self) -> Self::Iter {
233 [PathCommand::Arc {
234 cx: 0.5,
235 cy: 0.5,
236 rx: 0.5,
237 ry: 0.5,
238 start: 0.0,
239 sweep: TAU,
240 }]
241 }
242
243 fn shape_kind(&self) -> ShapeKind {
244 ShapeKind::Ellipse
245 }
246}
247
248#[derive(Debug, Clone, Copy, Default)]
250pub struct Capsule;
251
252impl Shape for Capsule {
253 type Iter = [PathCommand; 4];
254
255 fn path(&self) -> Self::Iter {
262 [
263 PathCommand::MoveTo { x: 0.5, y: 0.0 },
264 PathCommand::Arc {
265 cx: 0.5,
266 cy: 0.5,
267 rx: 0.5,
268 ry: 0.5,
269 start: -FRAC_PI_2,
270 sweep: PI,
271 },
272 PathCommand::Arc {
273 cx: 0.5,
274 cy: 0.5,
275 rx: 0.5,
276 ry: 0.5,
277 start: FRAC_PI_2,
278 sweep: PI,
279 },
280 PathCommand::Close,
281 ]
282 }
283
284 fn shape_kind(&self) -> ShapeKind {
285 ShapeKind::Capsule
286 }
287}
288
289#[derive(Debug, Clone, Copy)]
291pub struct RoundedRectangle {
292 pub corner_radius: f32,
294}
295
296impl RoundedRectangle {
297 #[must_use]
308 pub const fn new(corner_radius: f32) -> Self {
309 Self { corner_radius }
310 }
311}
312
313impl Shape for RoundedRectangle {
314 type Iter = [PathCommand; 10];
315
316 fn path(&self) -> Self::Iter {
317 let r = CornerRadii {
318 top_left: self.corner_radius,
319 top_right: self.corner_radius,
320 bottom_right: self.corner_radius,
321 bottom_left: self.corner_radius,
322 }
323 .sanitized()
324 .top_left;
325 [
326 PathCommand::MoveTo { x: r, y: 0.0 },
327 PathCommand::LineTo { x: 1.0 - r, y: 0.0 },
328 PathCommand::Arc {
329 cx: 1.0 - r,
330 cy: r,
331 rx: r,
332 ry: r,
333 start: -FRAC_PI_2,
334 sweep: FRAC_PI_2,
335 },
336 PathCommand::LineTo { x: 1.0, y: 1.0 - r },
337 PathCommand::Arc {
338 cx: 1.0 - r,
339 cy: 1.0 - r,
340 rx: r,
341 ry: r,
342 start: 0.0,
343 sweep: FRAC_PI_2,
344 },
345 PathCommand::LineTo { x: r, y: 1.0 },
346 PathCommand::Arc {
347 cx: r,
348 cy: 1.0 - r,
349 rx: r,
350 ry: r,
351 start: FRAC_PI_2,
352 sweep: FRAC_PI_2,
353 },
354 PathCommand::LineTo { x: 0.0, y: r },
355 PathCommand::Arc {
356 cx: r,
357 cy: r,
358 rx: r,
359 ry: r,
360 start: PI,
361 sweep: FRAC_PI_2,
362 },
363 PathCommand::Close,
364 ]
365 }
366
367 fn shape_kind(&self) -> ShapeKind {
368 let r = CornerRadii {
369 top_left: self.corner_radius,
370 top_right: self.corner_radius,
371 bottom_right: self.corner_radius,
372 bottom_left: self.corner_radius,
373 }
374 .sanitized()
375 .top_left;
376 ShapeKind::RoundedRect { corner_radius: r }
377 }
378}
379
380#[derive(Debug, Clone, Copy)]
382pub struct UnevenRoundedRectangle {
383 pub top_leading: f32,
385 pub top_trailing: f32,
387 pub bottom_leading: f32,
389 pub bottom_trailing: f32,
391}
392
393impl UnevenRoundedRectangle {
394 #[must_use]
396 pub const fn new(
397 top_leading: f32,
398 top_trailing: f32,
399 bottom_leading: f32,
400 bottom_trailing: f32,
401 ) -> Self {
402 Self {
403 top_leading,
404 top_trailing,
405 bottom_leading,
406 bottom_trailing,
407 }
408 }
409}
410
411impl Shape for UnevenRoundedRectangle {
412 type Iter = [PathCommand; 10];
413
414 fn path(&self) -> Self::Iter {
415 let corners = CornerRadii {
416 top_left: self.top_leading,
417 top_right: self.top_trailing,
418 bottom_right: self.bottom_trailing,
419 bottom_left: self.bottom_leading,
420 }
421 .sanitized();
422 let tl = corners.top_left;
423 let tr = corners.top_right;
424 let bl = corners.bottom_left;
425 let br = corners.bottom_right;
426 [
427 PathCommand::MoveTo { x: tl, y: 0.0 },
428 PathCommand::LineTo {
429 x: 1.0 - tr,
430 y: 0.0,
431 },
432 PathCommand::Arc {
433 cx: 1.0 - tr,
434 cy: tr,
435 rx: tr,
436 ry: tr,
437 start: -FRAC_PI_2,
438 sweep: FRAC_PI_2,
439 },
440 PathCommand::LineTo {
441 x: 1.0,
442 y: 1.0 - br,
443 },
444 PathCommand::Arc {
445 cx: 1.0 - br,
446 cy: 1.0 - br,
447 rx: br,
448 ry: br,
449 start: 0.0,
450 sweep: FRAC_PI_2,
451 },
452 PathCommand::LineTo { x: bl, y: 1.0 },
453 PathCommand::Arc {
454 cx: bl,
455 cy: 1.0 - bl,
456 rx: bl,
457 ry: bl,
458 start: FRAC_PI_2,
459 sweep: FRAC_PI_2,
460 },
461 PathCommand::LineTo { x: 0.0, y: tl },
462 PathCommand::Arc {
463 cx: tl,
464 cy: tl,
465 rx: tl,
466 ry: tl,
467 start: PI,
468 sweep: FRAC_PI_2,
469 },
470 PathCommand::Close,
471 ]
472 }
473
474 fn shape_kind(&self) -> ShapeKind {
475 let corners = CornerRadii {
476 top_left: self.top_leading,
477 top_right: self.top_trailing,
478 bottom_right: self.bottom_trailing,
479 bottom_left: self.bottom_leading,
480 }
481 .sanitized();
482 ShapeKind::UnevenRoundedRect {
483 top_left: corners.top_left,
484 top_right: corners.top_right,
485 bottom_left: corners.bottom_left,
486 bottom_right: corners.bottom_right,
487 }
488 }
489}
490
491#[derive(Debug, Clone, Copy)]
499pub struct FixedRoundedRectangle {
500 pub corner_radius: f32,
502}
503
504impl FixedRoundedRectangle {
505 #[must_use]
511 pub const fn new(corner_radius: f32) -> Self {
512 Self {
513 corner_radius: if corner_radius.is_finite() {
514 corner_radius.max(0.0)
515 } else {
516 0.0
517 },
518 }
519 }
520}
521
522impl Shape for FixedRoundedRectangle {
523 type Iter = [PathCommand; 10];
524
525 fn path(&self) -> Self::Iter {
531 RoundedRectangle::new(0.5).path()
532 }
533
534 fn shape_kind(&self) -> ShapeKind {
535 ShapeKind::FixedRoundedRect {
536 corner_radius: self.corner_radius,
537 }
538 }
539}
540
541#[derive(Debug, Clone, Copy)]
547pub struct FixedUnevenRoundedRectangle {
548 pub top_leading: f32,
550 pub top_trailing: f32,
552 pub bottom_leading: f32,
554 pub bottom_trailing: f32,
556}
557
558impl FixedUnevenRoundedRectangle {
559 #[must_use]
561 pub const fn new(
562 top_leading: f32,
563 top_trailing: f32,
564 bottom_leading: f32,
565 bottom_trailing: f32,
566 ) -> Self {
567 const fn point_radius(radius: f32) -> f32 {
568 if radius.is_finite() {
569 radius.max(0.0)
570 } else {
571 0.0
572 }
573 }
574 Self {
575 top_leading: point_radius(top_leading),
576 top_trailing: point_radius(top_trailing),
577 bottom_leading: point_radius(bottom_leading),
578 bottom_trailing: point_radius(bottom_trailing),
579 }
580 }
581}
582
583impl Shape for FixedUnevenRoundedRectangle {
584 type Iter = [PathCommand; 10];
585
586 fn path(&self) -> Self::Iter {
593 UnevenRoundedRectangle::new(
594 clamp_radius(self.top_leading),
595 clamp_radius(self.top_trailing),
596 clamp_radius(self.bottom_leading),
597 clamp_radius(self.bottom_trailing),
598 )
599 .path()
600 }
601
602 fn shape_kind(&self) -> ShapeKind {
603 ShapeKind::FixedUnevenRoundedRect {
604 top_left: self.top_leading,
605 top_right: self.top_trailing,
606 bottom_left: self.bottom_leading,
607 bottom_right: self.bottom_trailing,
608 }
609 }
610}
611
612#[derive(Debug, Clone, Copy, Default)]
614pub struct Rectangle;
615
616impl Shape for Rectangle {
617 type Iter = [PathCommand; 5];
618
619 fn path(&self) -> Self::Iter {
620 [
621 PathCommand::MoveTo { x: 0.0, y: 0.0 },
622 PathCommand::LineTo { x: 1.0, y: 0.0 },
623 PathCommand::LineTo { x: 1.0, y: 1.0 },
624 PathCommand::LineTo { x: 0.0, y: 1.0 },
625 PathCommand::Close,
626 ]
627 }
628
629 fn shape_kind(&self) -> ShapeKind {
630 ShapeKind::Rect
631 }
632}
633
634#[derive(Debug, Clone, Default)]
640pub struct Path {
641 commands: Vec<PathCommand>,
642}
643
644impl Path {
645 #[must_use]
647 pub fn new() -> Self {
648 Self::default()
649 }
650
651 #[must_use]
653 pub fn move_to(mut self, x: f32, y: f32) -> Self {
654 self.commands.push(PathCommand::MoveTo { x, y });
655 self
656 }
657
658 #[must_use]
660 pub fn line_to(mut self, x: f32, y: f32) -> Self {
661 self.commands.push(PathCommand::LineTo { x, y });
662 self
663 }
664
665 #[must_use]
667 pub fn quad_to(mut self, cx: f32, cy: f32, x: f32, y: f32) -> Self {
668 self.commands.push(PathCommand::QuadTo { cx, cy, x, y });
669 self
670 }
671
672 #[must_use]
674 pub fn cubic_to(mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) -> Self {
675 self.commands.push(PathCommand::CubicTo {
676 c1x,
677 c1y,
678 c2x,
679 c2y,
680 x,
681 y,
682 });
683 self
684 }
685
686 #[must_use]
688 pub fn arc(mut self, cx: f32, cy: f32, rx: f32, ry: f32, start: f32, sweep: f32) -> Self {
689 self.commands.push(PathCommand::Arc {
690 cx,
691 cy,
692 rx,
693 ry,
694 start,
695 sweep,
696 });
697 self
698 }
699
700 #[must_use]
702 pub fn close(mut self) -> Self {
703 self.commands.push(PathCommand::Close);
704 self
705 }
706}
707
708impl Shape for Path {
709 type Iter = alloc::vec::IntoIter<PathCommand>;
710
711 fn path(&self) -> Self::Iter {
712 self.commands.clone().into_iter()
713 }
714
715 fn shape_kind(&self) -> ShapeKind {
716 ShapeKind::CustomPath
717 }
718}
719
720#[derive(Debug)]
734pub struct ClipShape {
735 kind: ShapeKind,
736 commands: Vec<PathCommand>,
737}
738
739impl ClipShape {
740 #[allow(clippy::needless_pass_by_value)]
742 pub fn new(shape: impl Shape) -> Self {
743 Self {
744 kind: shape.shape_kind(),
745 commands: shape.path().into_iter().collect(),
746 }
747 }
748
749 #[must_use]
752 pub const fn kind(&self) -> ShapeKind {
753 self.kind
754 }
755
756 #[must_use]
758 pub fn commands(&self) -> &[PathCommand] {
759 &self.commands
760 }
761}
762
763impl MetadataKey for ClipShape {}
764
765#[derive(Debug, Clone, Copy, Default)]
771pub enum ShapeKind {
772 #[default]
774 Rect,
775 Circle,
777 Ellipse,
779 RoundedRect {
781 corner_radius: f32,
783 },
784 UnevenRoundedRect {
786 top_left: f32,
788 top_right: f32,
790 bottom_left: f32,
792 bottom_right: f32,
794 },
795 Capsule,
797 FixedRoundedRect {
804 corner_radius: f32,
806 },
807 FixedUnevenRoundedRect {
812 top_left: f32,
814 top_right: f32,
816 bottom_left: f32,
818 bottom_right: f32,
820 },
821 CustomPath,
823}
824
825#[derive(Debug, Clone)]
827pub struct ResolvedShape {
828 pub kind: ShapeKind,
830 pub commands: Vec<PathCommand>,
832 pub fill: Computed<waterui_graphics::ResolvedColor>,
834}
835
836waterui_core::raw_view!(ResolvedShape, waterui_core::layout::StretchAxis::Both);
837
838#[derive(Debug, Clone)]
840pub struct ResolvedMorphShape {
841 pub from: ShapeKind,
843 pub to: ShapeKind,
845 pub fill: Computed<waterui_graphics::ResolvedColor>,
847 pub animation: MorphAnimation,
849 pub progress: Option<Computed<f32>>,
851}
852
853impl waterui_core::NativeView for ResolvedMorphShape {
854 fn stretch_axis(&self) -> waterui_core::layout::StretchAxis {
855 waterui_core::layout::StretchAxis::Both
856 }
857}
858
859#[derive(Debug)]
865pub struct FilledShape {
866 kind: ShapeKind,
867 commands: Vec<PathCommand>,
868 fill: Color,
869}
870
871impl FilledShape {
872 #[allow(clippy::needless_pass_by_value)]
874 pub fn new(shape: impl Shape, fill: impl Into<Color>) -> Self {
875 Self {
876 kind: ShapeKind::CustomPath,
877 commands: shape.path().into_iter().collect(),
878 fill: fill.into(),
879 }
880 }
881
882 #[allow(clippy::needless_pass_by_value)]
883 fn with_kind(kind: ShapeKind, shape: impl Shape, fill: impl Into<Color>) -> Self {
884 Self {
885 kind,
886 commands: shape.path().into_iter().collect(),
887 fill: fill.into(),
888 }
889 }
890
891 #[must_use]
893 pub fn commands(&self) -> &[PathCommand] {
894 &self.commands
895 }
896
897 #[must_use]
899 pub const fn fill(&self) -> &Color {
900 &self.fill
901 }
902
903 #[must_use]
905 pub const fn kind(&self) -> ShapeKind {
906 self.kind
907 }
908
909 #[must_use]
914 #[allow(clippy::needless_pass_by_value)]
915 pub fn morph_to(self, target: impl ShapeExt) -> MorphShape {
916 MorphShape::new(self.kind, target.shape_kind(), self.fill)
917 }
918}
919
920#[derive(Debug, Clone, Copy, PartialEq)]
922pub struct MorphAnimation {
923 pub duration: Duration,
925 pub easing: EasingCurve,
927 pub repeat: bool,
929 pub autoreverse: bool,
931}
932
933impl Default for MorphAnimation {
934 fn default() -> Self {
935 Self {
936 duration: Duration::from_millis(900),
937 easing: EasingCurve::EASE_IN_OUT,
938 repeat: true,
939 autoreverse: true,
940 }
941 }
942}
943
944impl MorphAnimation {
945 #[must_use]
947 pub const fn once(duration: Duration, easing: EasingCurve) -> Self {
948 Self {
949 duration,
950 easing,
951 repeat: false,
952 autoreverse: false,
953 }
954 }
955
956 #[cfg(feature = "gpu")]
957 #[must_use]
958 fn sample(self, elapsed: Duration) -> f32 {
959 if self.duration.is_zero() {
960 return 1.0;
961 }
962 let raw = elapsed.as_secs_f32() / self.duration.as_secs_f32();
963 let cycle = if self.repeat {
964 let base = raw.fract();
965 let index = raw
966 .floor()
967 .to_u64()
968 .expect("MorphAnimation::sample: cycle index must fit into u64");
969 if self.autoreverse && index % 2 == 1 {
970 1.0 - base
971 } else {
972 base
973 }
974 } else {
975 raw.clamp(0.0, 1.0)
976 };
977 self.easing.ease(cycle).clamp(0.0, 1.0)
978 }
979}
980
981#[derive(Debug, Clone)]
983pub struct MorphShape {
984 from: ShapeKind,
985 to: ShapeKind,
986 fill: Color,
987 animation: MorphAnimation,
988 progress: Option<Computed<f32>>,
989}
990
991impl MorphShape {
992 fn new(from: ShapeKind, to: ShapeKind, fill: Color) -> Self {
993 Self {
994 from,
995 to,
996 fill,
997 animation: MorphAnimation::default(),
998 progress: None,
999 }
1000 }
1001
1002 #[must_use]
1004 pub const fn animation(mut self, animation: MorphAnimation) -> Self {
1005 self.animation = animation;
1006 self
1007 }
1008
1009 #[must_use]
1011 pub const fn duration(mut self, duration: Duration) -> Self {
1012 self.animation.duration = duration;
1013 self
1014 }
1015
1016 #[must_use]
1018 pub const fn easing(mut self, easing: EasingCurve) -> Self {
1019 self.animation.easing = easing;
1020 self
1021 }
1022
1023 #[must_use]
1025 pub const fn repeat(mut self, repeat: bool) -> Self {
1026 self.animation.repeat = repeat;
1027 self
1028 }
1029
1030 #[must_use]
1032 pub const fn autoreverse(mut self, autoreverse: bool) -> Self {
1033 self.animation.autoreverse = autoreverse;
1034 self
1035 }
1036
1037 #[must_use]
1041 pub fn progress(mut self, progress: impl IntoComputed<f32>) -> Self {
1042 self.progress = Some(progress.into_computed());
1043 self
1044 }
1045}
1046
1047impl View for FilledShape {
1048 fn body(self, env: &Environment) -> impl View {
1049 ResolvedShape {
1050 kind: self.kind,
1051 commands: self.commands,
1052 fill: self.fill.resolve(env).computed(),
1053 }
1054 }
1055
1056 fn stretch_axis(&self) -> waterui_core::layout::StretchAxis {
1058 waterui_core::layout::StretchAxis::Both
1059 }
1060}
1061
1062impl View for MorphShape {
1063 fn body(self, env: &Environment) -> impl View {
1064 let resolved = self.fill.resolve(env).computed();
1065 #[cfg(feature = "gpu")]
1068 let progress_for_gpu = self.progress.clone();
1069 let native = waterui_core::Native::new(ResolvedMorphShape {
1070 from: self.from,
1071 to: self.to,
1072 fill: resolved,
1073 animation: self.animation,
1074 progress: self.progress,
1075 });
1076 #[cfg(feature = "gpu")]
1077 let native = native.with_fallback(GpuSurface::new(MorphShapeRenderer::new(
1078 kind_to_morph_shape(self.from)
1079 .expect("morph source shape must be a built-in morphable shape"),
1080 kind_to_morph_shape(self.to)
1081 .expect("morph target shape must be a built-in morphable shape"),
1082 ReactiveColor::new(&Computed::constant(self.fill), env),
1083 self.animation,
1084 progress_for_gpu,
1085 )));
1086 native
1087 }
1088
1089 fn stretch_axis(&self) -> waterui_core::layout::StretchAxis {
1092 waterui_core::layout::StretchAxis::Both
1093 }
1094}
1095
1096#[cfg(feature = "gpu")]
1101#[derive(Debug, Clone, Copy)]
1102struct MorphSdfShape {
1103 shape_type: u32,
1104 radii: [f32; 4],
1105}
1106
1107#[cfg(feature = "gpu")]
1108fn kind_to_morph_shape(kind: ShapeKind) -> Option<MorphSdfShape> {
1109 match kind {
1110 ShapeKind::Rect => Some(MorphSdfShape {
1111 shape_type: 0,
1112 radii: [0.0; 4],
1113 }),
1114 ShapeKind::Circle => Some(MorphSdfShape {
1115 shape_type: 1,
1116 radii: [0.0; 4],
1117 }),
1118 ShapeKind::Ellipse => Some(MorphSdfShape {
1119 shape_type: 2,
1120 radii: [0.0; 4],
1121 }),
1122 ShapeKind::RoundedRect { corner_radius } => Some(MorphSdfShape {
1123 shape_type: 3,
1124 radii: [clamp_radius(corner_radius); 4],
1125 }),
1126 ShapeKind::UnevenRoundedRect {
1127 top_left,
1128 top_right,
1129 bottom_left,
1130 bottom_right,
1131 } => {
1132 let corners = CornerRadii {
1133 top_left,
1134 top_right,
1135 bottom_right,
1136 bottom_left,
1137 }
1138 .sanitized();
1139 Some(MorphSdfShape {
1140 shape_type: 3,
1141 radii: [
1142 corners.top_left,
1143 corners.top_right,
1144 corners.bottom_right,
1145 corners.bottom_left,
1146 ],
1147 })
1148 }
1149 ShapeKind::Capsule => Some(MorphSdfShape {
1150 shape_type: 4,
1151 radii: [0.0; 4],
1152 }),
1153 ShapeKind::FixedRoundedRect { .. }
1157 | ShapeKind::FixedUnevenRoundedRect { .. }
1158 | ShapeKind::CustomPath => None,
1159 }
1160}
1161
1162#[cfg(feature = "gpu")]
1163#[repr(C)]
1164#[derive(Debug, Clone, Copy, Default, bytemuck::Pod, bytemuck::Zeroable)]
1165struct MorphUniforms {
1166 color: [f32; 4],
1167 dimensions_and_progress: [f32; 4], shape_types: [f32; 4], from_radii: [f32; 4], to_radii: [f32; 4], }
1172
1173#[cfg(feature = "gpu")]
1174struct MorphShapeRenderer {
1175 from: MorphSdfShape,
1176 to: MorphSdfShape,
1177 fill_color: ReactiveColor,
1178 animation: MorphAnimation,
1179 progress: Option<Computed<f32>>,
1180 progress_guard: Option<BoxWatcherGuard>,
1181 start: Option<Duration>,
1182 pipeline: Option<wgpu::RenderPipeline>,
1183 uniform_buffer: Option<wgpu::Buffer>,
1184 bind_group: Option<wgpu::BindGroup>,
1185 pipeline_format: Option<wgpu::TextureFormat>,
1186}
1187
1188#[cfg(feature = "gpu")]
1189impl fmt::Debug for MorphShapeRenderer {
1190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1191 f.debug_struct("MorphShapeRenderer")
1192 .field("from", &self.from)
1193 .field("to", &self.to)
1194 .finish_non_exhaustive()
1195 }
1196}
1197
1198#[cfg(feature = "gpu")]
1199impl MorphShapeRenderer {
1200 fn new(
1201 from: MorphSdfShape,
1202 to: MorphSdfShape,
1203 fill_color: ReactiveColor,
1204 animation: MorphAnimation,
1205 progress: Option<Computed<f32>>,
1206 ) -> Self {
1207 Self {
1208 from,
1209 to,
1210 fill_color,
1211 animation,
1212 progress,
1213 progress_guard: None,
1214 start: None,
1215 pipeline: None,
1216 uniform_buffer: None,
1217 bind_group: None,
1218 pipeline_format: None,
1219 }
1220 }
1221}
1222
1223#[cfg(feature = "gpu")]
1224impl GpuView for MorphShapeRenderer {
1225 fn setup(
1226 &mut self,
1227 ctx: &GpuContext<'_>,
1228 _env: &mut waterui_core::Environment,
1229 ) -> impl core::future::Future<Output = ()> {
1230 self.fill_color.install(&ctx.redraw_handle);
1231 if let Some(progress) = &self.progress {
1232 let redraw = ctx.redraw_handle.clone();
1233 self.progress_guard = Some(progress.watch(move |_| redraw.request_redraw()));
1234 }
1235
1236 let (vertex_shader, fragment_shader, bind_group_layout) = single_bind_group_render_stages(
1237 &MORPH_SHADER,
1238 ctx.device,
1239 "the morph shape shader",
1240 "vs_main",
1241 "fs_main",
1242 );
1243
1244 let uniform_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
1245 label: Some("Morph Shape Uniforms"),
1246 size: core::mem::size_of::<MorphUniforms>() as u64,
1247 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1248 mapped_at_creation: false,
1249 });
1250
1251 let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
1252 label: Some("Morph Shape Bind Group"),
1253 layout: &bind_group_layout,
1254 entries: &[wgpu::BindGroupEntry {
1255 binding: 0,
1256 resource: uniform_buffer.as_entire_binding(),
1257 }],
1258 });
1259
1260 let pipeline_layout = ctx
1261 .device
1262 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1263 label: Some("Morph Shape Pipeline Layout"),
1264 bind_group_layouts: &[Some(&bind_group_layout)],
1265 immediate_size: 0,
1266 });
1267
1268 let blend = ctx.alpha_blend_state();
1269
1270 let pipeline = ctx
1271 .device
1272 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1273 label: Some("Morph Shape Pipeline"),
1274 layout: Some(&pipeline_layout),
1275 vertex: wgpu::VertexState {
1276 module: vertex_shader.module(),
1277 entry_point: Some(vertex_shader.entry_point()),
1278 buffers: &[],
1279 compilation_options: wgpu::PipelineCompilationOptions::default(),
1280 },
1281 fragment: Some(wgpu::FragmentState {
1282 module: fragment_shader.module(),
1283 entry_point: Some(fragment_shader.entry_point()),
1284 targets: &[Some(wgpu::ColorTargetState {
1285 format: ctx.surface_format,
1286 blend,
1287 write_mask: wgpu::ColorWrites::ALL,
1288 })],
1289 compilation_options: wgpu::PipelineCompilationOptions::default(),
1290 }),
1291 primitive: wgpu::PrimitiveState {
1292 topology: wgpu::PrimitiveTopology::TriangleList,
1293 ..Default::default()
1294 },
1295 depth_stencil: None,
1296 multisample: wgpu::MultisampleState::default(),
1297 multiview_mask: None,
1298 cache: None,
1299 });
1300
1301 self.pipeline = Some(pipeline);
1302 self.uniform_buffer = Some(uniform_buffer);
1303 self.bind_group = Some(bind_group);
1304 self.pipeline_format = Some(ctx.surface_format);
1305 self.start = None;
1306 core::future::ready(())
1307 }
1308
1309 fn render(&mut self, frame: &mut GpuFrame) {
1310 assert_eq!(
1311 self.pipeline_format,
1312 Some(frame.format),
1313 "MorphShape target format changed after setup"
1314 );
1315 let pipeline = self
1316 .pipeline
1317 .as_ref()
1318 .expect("MorphShape render called before setup");
1319 let uniform_buffer = self
1320 .uniform_buffer
1321 .as_ref()
1322 .expect("MorphShape render called before setup");
1323 let bind_group = self
1324 .bind_group
1325 .as_ref()
1326 .expect("MorphShape render called before setup");
1327
1328 let start = *self.start.get_or_insert_with(|| frame.elapsed());
1332 let age = frame.elapsed().saturating_sub(start);
1333 let progress = if let Some(signal) = &self.progress {
1334 let value = signal.get();
1335 assert!(value.is_finite(), "MorphShape progress must be finite");
1336 value.clamp(0.0, 1.0)
1337 } else {
1338 self.animation.sample(age)
1339 };
1340
1341 let fill_color = self.fill_color.get();
1342 let [r, g, b] = fill_color.linear_with_headroom();
1343 let uniforms = MorphUniforms {
1344 color: [r, g, b, fill_color.opacity],
1345 dimensions_and_progress: [
1346 u32_to_f32(frame.width),
1347 u32_to_f32(frame.height),
1348 progress,
1349 0.0,
1350 ],
1351 shape_types: [
1352 u32_to_f32(self.from.shape_type),
1353 u32_to_f32(self.to.shape_type),
1354 0.0,
1355 0.0,
1356 ],
1357 from_radii: self.from.radii,
1358 to_radii: self.to.radii,
1359 };
1360 frame
1361 .queue
1362 .write_buffer(uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
1363
1364 let mut encoder = frame
1365 .device
1366 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1367 label: Some("Morph Shape Encoder"),
1368 });
1369
1370 {
1371 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1372 label: Some("Morph Shape Render Pass"),
1373 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1374 view: &frame.view,
1375 depth_slice: None,
1376 resolve_target: None,
1377 ops: wgpu::Operations {
1378 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1379 store: wgpu::StoreOp::Store,
1380 },
1381 })],
1382 depth_stencil_attachment: None,
1383 timestamp_writes: None,
1384 occlusion_query_set: None,
1385 multiview_mask: None,
1386 });
1387
1388 render_pass.set_pipeline(pipeline);
1389 render_pass.set_bind_group(0, bind_group, &[]);
1390 render_pass.draw(0..6, 0..1);
1391 }
1392
1393 frame.queue.submit(core::iter::once(encoder.finish()));
1394
1395 let animation_active =
1397 self.progress.is_none() && (self.animation.repeat || age < self.animation.duration);
1398 if animation_active {
1399 frame.request_redraw();
1400 }
1401 }
1402}
1403
1404#[cfg(feature = "gpu")]
1405fn u32_to_f32(value: u32) -> f32 {
1406 value
1407 .to_f32()
1408 .expect("shape dimensions must be representable as f32")
1409}
1410
1411pub trait ShapeExt: Shape + Sized {
1417 fn fill(self, color: impl Into<Color>) -> FilledShape {
1419 FilledShape::with_kind(self.shape_kind(), self, color)
1420 }
1421
1422 fn morph_to(self, target: impl ShapeExt, fill: impl Into<Color>) -> MorphShape {
1427 MorphShape::new(self.shape_kind(), target.shape_kind(), fill.into())
1428 }
1429}
1430
1431impl ShapeExt for Circle {}
1432
1433impl ShapeExt for Ellipse {}
1434
1435impl ShapeExt for Capsule {}
1436
1437impl ShapeExt for Rectangle {}
1438
1439impl ShapeExt for RoundedRectangle {}
1440
1441impl ShapeExt for UnevenRoundedRectangle {}
1442
1443impl ShapeExt for FixedRoundedRectangle {}
1444
1445impl ShapeExt for FixedUnevenRoundedRectangle {}
1446
1447impl ShapeExt for Path {}
1448
1449#[cfg(test)]
1450mod tests {
1451 use super::*;
1452
1453 #[test]
1454 fn rounded_rectangle_radius_is_clamped() {
1455 let kind = RoundedRectangle::new(9.0).shape_kind();
1456 match kind {
1457 ShapeKind::RoundedRect { corner_radius } => {
1458 assert!((corner_radius - 0.5).abs() < 1e-6);
1459 }
1460 _ => panic!("unexpected kind"),
1461 }
1462 }
1463
1464 #[test]
1465 fn fixed_rounded_rectangle_carries_its_radius_in_points() {
1466 let kind = FixedRoundedRectangle::new(28.0).shape_kind();
1467 match kind {
1468 ShapeKind::FixedRoundedRect { corner_radius } => {
1469 assert!((corner_radius - 28.0).abs() < 1e-6);
1470 }
1471 _ => panic!("unexpected kind"),
1472 }
1473 }
1474
1475 #[test]
1476 fn fixed_uneven_rounded_rectangle_carries_each_corner_in_points() {
1477 let kind = FixedUnevenRoundedRectangle::new(0.0, 16.0, 0.0, 16.0).shape_kind();
1478 match kind {
1479 ShapeKind::FixedUnevenRoundedRect {
1480 top_left,
1481 top_right,
1482 bottom_left,
1483 bottom_right,
1484 } => {
1485 assert!((top_left - 0.0).abs() < 1e-6);
1486 assert!((top_right - 16.0).abs() < 1e-6);
1487 assert!((bottom_left - 0.0).abs() < 1e-6);
1488 assert!((bottom_right - 16.0).abs() < 1e-6);
1489 }
1490 _ => panic!("unexpected kind"),
1491 }
1492 }
1493
1494 #[test]
1495 fn fixed_radii_reject_negative_and_non_finite_values() {
1496 let kind = FixedRoundedRectangle::new(f32::NAN).shape_kind();
1497 match kind {
1498 ShapeKind::FixedRoundedRect { corner_radius } => {
1499 assert!((corner_radius - 0.0).abs() < 1e-6);
1500 }
1501 _ => panic!("unexpected kind"),
1502 }
1503 let kind = FixedRoundedRectangle::new(-4.0).shape_kind();
1504 match kind {
1505 ShapeKind::FixedRoundedRect { corner_radius } => {
1506 assert!((corner_radius - 0.0).abs() < 1e-6);
1507 }
1508 _ => panic!("unexpected kind"),
1509 }
1510 }
1511
1512 #[test]
1513 fn uneven_radii_are_normalized_when_edges_overlap() {
1514 let kind = UnevenRoundedRectangle::new(0.8, 0.8, 0.8, 0.8).shape_kind();
1515 match kind {
1516 ShapeKind::UnevenRoundedRect {
1517 top_left,
1518 top_right,
1519 bottom_left,
1520 bottom_right,
1521 } => {
1522 assert!((top_left - 0.5).abs() < 1e-6);
1523 assert!((top_right - 0.5).abs() < 1e-6);
1524 assert!((bottom_left - 0.5).abs() < 1e-6);
1525 assert!((bottom_right - 0.5).abs() < 1e-6);
1526 }
1527 _ => panic!("unexpected kind"),
1528 }
1529 }
1530
1531 #[cfg(feature = "gpu")]
1532 #[test]
1533 fn one_shot_animation_reaches_end() {
1534 let animation = MorphAnimation::once(Duration::from_millis(200), EasingCurve::LINEAR);
1535 assert!((animation.sample(Duration::ZERO) - 0.0).abs() < 1e-6);
1536 assert!((animation.sample(Duration::from_millis(100)) - 0.5).abs() < 1e-3);
1537 assert!((animation.sample(Duration::from_secs(1)) - 1.0).abs() < 1e-6);
1538 }
1539}