pub struct CornerRadii {
pub top_left: f32,
pub top_right: f32,
pub bottom_right: f32,
pub bottom_left: f32,
}Fields§
§top_left: f32§top_right: f32§bottom_right: f32§bottom_left: f32Implementations§
Source§impl CornerRadii
impl CornerRadii
pub const ZERO: Self
Sourcepub const fn uniform(radius: f32) -> Self
pub const fn uniform(radius: f32) -> Self
Examples found in repository?
examples/showcase.rs (line 473)
471 fn radii(self) -> CornerRadii {
472 if self.radius_same {
473 CornerRadii::uniform(self.corner_radius)
474 } else {
475 CornerRadii::new(
476 self.corner_radius,
477 self.corner_ne,
478 self.corner_se,
479 self.corner_sw,
480 )
481 }
482 }
483
484 fn stroke_color(self) -> ColorRgba {
485 self.stroke
486 }
487
488 fn fill_color(self) -> ColorRgba {
489 self.fill
490 }
491
492 fn shadow_color(self) -> ColorRgba {
493 self.shadow
494 }
495}
496
497#[derive(Clone, Copy, Debug, PartialEq, Eq)]
498enum FocusedTextInput {
499 Editable,
500 Selectable,
501 Singleline,
502 Multiline,
503 TextArea,
504 CodeEditor,
505 Search,
506 Password,
507 FormName,
508 FormEmail,
509 FormRole,
510 NumericValue,
511 NumericRangeMin,
512 NumericRangeMax,
513 SliderValue,
514 SliderRangeLeft,
515 SliderRangeRight,
516 SliderStep,
517 ShaderLabSource,
518}
519
520impl FocusedTextInput {
521 const fn is_read_only(self) -> bool {
522 matches!(self, Self::Selectable)
523 }
524
525 const fn is_multiline(self) -> bool {
526 matches!(
527 self,
528 Self::Multiline | Self::TextArea | Self::CodeEditor | Self::ShaderLabSource
529 )
530 }
531}
532
533#[derive(Clone, Copy, Debug, PartialEq, Eq)]
534enum SliderThumbChoice {
535 Circle,
536 Square,
537 Rectangle,
538}
539
540#[derive(Clone, Copy, Debug, PartialEq, Eq)]
541enum DragDropDemoPayload {
542 Text,
543 File,
544 ImageBytes,
545}
546
547#[derive(Clone, Copy, Debug, PartialEq, Eq)]
548enum DragDropDemoTarget {
549 Text,
550 FilesOnly,
551 ImageBytes,
552 Disabled,
553}
554
555#[derive(Clone, Copy, Debug, PartialEq, Eq)]
556enum ShaderLabTarget {
557 Canvas,
558 Frame,
559 Button,
560}
561
562impl ShaderLabTarget {
563 const ALL: [Self; 3] = [Self::Canvas, Self::Frame, Self::Button];
564
565 const fn id(self) -> &'static str {
566 match self {
567 Self::Canvas => "canvas",
568 Self::Frame => "frame",
569 Self::Button => "button",
570 }
571 }
572
573 const fn label(self) -> &'static str {
574 match self {
575 Self::Canvas => "Canvas",
576 Self::Frame => "Frame",
577 Self::Button => "Button",
578 }
579 }
580
581 fn from_id(id: &str) -> Option<Self> {
582 match id {
583 "canvas" => Some(Self::Canvas),
584 "frame" => Some(Self::Frame),
585 "button" => Some(Self::Button),
586 _ => None,
587 }
588 }
589}
590
591#[derive(Clone, Copy, Debug, PartialEq, Eq)]
592enum ShaderLabPreset {
593 Plasma,
594 Rings,
595 Grid,
596 VertexWarp,
597}
598
599impl ShaderLabPreset {
600 const ALL: [Self; 4] = [Self::Plasma, Self::Rings, Self::Grid, Self::VertexWarp];
601
602 const fn id(self) -> &'static str {
603 match self {
604 Self::Plasma => "plasma",
605 Self::Rings => "rings",
606 Self::Grid => "grid",
607 Self::VertexWarp => "vertex_warp",
608 }
609 }
610
611 const fn label(self) -> &'static str {
612 match self {
613 Self::Plasma => "Plasma",
614 Self::Rings => "Rings",
615 Self::Grid => "Grid",
616 Self::VertexWarp => "Vertex warp",
617 }
618 }
619
620 const fn source(self) -> &'static str {
621 match self {
622 Self::Plasma => SHADER_LAB_PLASMA_WGSL,
623 Self::Rings => SHADER_LAB_RINGS_WGSL,
624 Self::Grid => SHADER_LAB_GRID_WGSL,
625 Self::VertexWarp => SHADER_LAB_VERTEX_WARP_WGSL,
626 }
627 }
628
629 fn from_id(id: &str) -> Option<Self> {
630 match id {
631 "plasma" => Some(Self::Plasma),
632 "rings" => Some(Self::Rings),
633 "grid" => Some(Self::Grid),
634 "vertex_warp" => Some(Self::VertexWarp),
635 _ => None,
636 }
637 }
638}
639
640#[derive(Clone, Copy, Debug, PartialEq, Eq)]
641enum ShaderLabMaterialShader {
642 None,
643 Tint,
644 Shine,
645 Glow,
646 Plasma,
647 Rings,
648 Grid,
649}
650
651impl ShaderLabMaterialShader {
652 const ALL: [Self; 7] = [
653 Self::None,
654 Self::Tint,
655 Self::Shine,
656 Self::Glow,
657 Self::Plasma,
658 Self::Rings,
659 Self::Grid,
660 ];
661
662 const fn id(self) -> &'static str {
663 match self {
664 Self::None => "none",
665 Self::Tint => "tint",
666 Self::Shine => "shine",
667 Self::Glow => "glow",
668 Self::Plasma => "plasma",
669 Self::Rings => "rings",
670 Self::Grid => "grid",
671 }
672 }
673
674 const fn label(self) -> &'static str {
675 match self {
676 Self::None => "None",
677 Self::Tint => "Tint",
678 Self::Shine => "Shine",
679 Self::Glow => "Glow",
680 Self::Plasma => "Plasma",
681 Self::Rings => "Rings",
682 Self::Grid => "Grid",
683 }
684 }
685
686 fn from_id(id: &str) -> Option<Self> {
687 match id {
688 "none" => Some(Self::None),
689 "tint" => Some(Self::Tint),
690 "shine" => Some(Self::Shine),
691 "glow" => Some(Self::Glow),
692 "plasma" => Some(Self::Plasma),
693 "rings" => Some(Self::Rings),
694 "grid" => Some(Self::Grid),
695 _ => None,
696 }
697 }
698}
699
700#[derive(Clone, Copy, Debug, PartialEq, Eq)]
701enum ShaderLabMaterialShape {
702 Rect,
703 Rounded,
704 Circle,
705 Hexagon,
706}
707
708impl ShaderLabMaterialShape {
709 const ALL: [Self; 4] = [Self::Rect, Self::Rounded, Self::Circle, Self::Hexagon];
710
711 const fn id(self) -> &'static str {
712 match self {
713 Self::Rect => "rect",
714 Self::Rounded => "rounded",
715 Self::Circle => "circle",
716 Self::Hexagon => "hexagon",
717 }
718 }
719
720 const fn label(self) -> &'static str {
721 match self {
722 Self::Rect => "Rectangle",
723 Self::Rounded => "Rounded",
724 Self::Circle => "Circle",
725 Self::Hexagon => "Hexagon",
726 }
727 }
728
729 fn from_id(id: &str) -> Option<Self> {
730 match id {
731 "rect" => Some(Self::Rect),
732 "rounded" => Some(Self::Rounded),
733 "circle" => Some(Self::Circle),
734 "hexagon" => Some(Self::Hexagon),
735 _ => None,
736 }
737 }
738
739 fn shape(self) -> ElementShape {
740 match self {
741 Self::Rect => ElementShape::rect(),
742 Self::Rounded => ElementShape::rounded_rect(16.0),
743 Self::Circle => ElementShape::circle(),
744 Self::Hexagon => ElementShape::normalized_polygon(vec![
745 UiPoint::new(0.50, 0.00),
746 UiPoint::new(0.95, 0.25),
747 UiPoint::new(0.95, 0.75),
748 UiPoint::new(0.50, 1.00),
749 UiPoint::new(0.05, 0.75),
750 UiPoint::new(0.05, 0.25),
751 ]),
752 }
753 }
754
755 const fn visual_radius(self) -> f32 {
756 match self {
757 Self::Rect | Self::Hexagon => 4.0,
758 Self::Rounded => 16.0,
759 Self::Circle => 999.0,
760 }
761 }
762}
763
764#[derive(Clone, Copy, Debug, PartialEq, Eq)]
765enum ShaderLabMaterialGeometry {
766 None,
767 PulseScale,
768 Skew,
769 Wave,
770}
771
772impl ShaderLabMaterialGeometry {
773 const ALL: [Self; 4] = [Self::None, Self::PulseScale, Self::Skew, Self::Wave];
774
775 const fn id(self) -> &'static str {
776 match self {
777 Self::None => "none",
778 Self::PulseScale => "pulse_scale",
779 Self::Skew => "skew",
780 Self::Wave => "wave",
781 }
782 }
783
784 const fn label(self) -> &'static str {
785 match self {
786 Self::None => "None",
787 Self::PulseScale => "Pulse scale",
788 Self::Skew => "Skew",
789 Self::Wave => "Wave",
790 }
791 }
792
793 fn from_id(id: &str) -> Option<Self> {
794 match id {
795 "none" => Some(Self::None),
796 "pulse_scale" => Some(Self::PulseScale),
797 "skew" => Some(Self::Skew),
798 "wave" => Some(Self::Wave),
799 _ => None,
800 }
801 }
802
803 const fn effect(self) -> GeometryEffect {
804 match self {
805 Self::None => GeometryEffect::None,
806 Self::PulseScale => GeometryEffect::PulseScale { max_scale: 1.18 },
807 Self::Skew => GeometryEffect::Skew { x: 0.12, y: 0.0 },
808 Self::Wave => GeometryEffect::Wave { amplitude: 10.0 },
809 }
810 }
811}
812
813fn shader_lab_target_options() -> Vec<ext_widgets::SelectOption> {
814 ShaderLabTarget::ALL
815 .into_iter()
816 .map(|target| ext_widgets::SelectOption::new(target.id(), target.label()))
817 .collect()
818}
819
820fn shader_lab_preset_options() -> Vec<ext_widgets::SelectOption> {
821 ShaderLabPreset::ALL
822 .into_iter()
823 .map(|preset| ext_widgets::SelectOption::new(preset.id(), preset.label()))
824 .collect()
825}
826
827fn shader_lab_material_shader_options() -> Vec<ext_widgets::SelectOption> {
828 ShaderLabMaterialShader::ALL
829 .into_iter()
830 .map(|shader| ext_widgets::SelectOption::new(shader.id(), shader.label()))
831 .collect()
832}
833
834fn shader_lab_material_shape_options() -> Vec<ext_widgets::SelectOption> {
835 ShaderLabMaterialShape::ALL
836 .into_iter()
837 .map(|shape| ext_widgets::SelectOption::new(shape.id(), shape.label()))
838 .collect()
839}
840
841fn shader_lab_material_geometry_options() -> Vec<ext_widgets::SelectOption> {
842 ShaderLabMaterialGeometry::ALL
843 .into_iter()
844 .map(|geometry| ext_widgets::SelectOption::new(geometry.id(), geometry.label()))
845 .collect()
846}
847
848const SHADER_LAB_PLASMA_WGSL: &str = r#"override TIME: f32 = 0.0;
849
850struct VertexOutput {
851 @builtin(position) position: vec4<f32>,
852 @location(0) uv: vec2<f32>,
853};
854
855@vertex
856fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
857 let positions = array<vec2<f32>, 3>(
858 vec2<f32>(-1.0, -1.0),
859 vec2<f32>(3.0, -1.0),
860 vec2<f32>(-1.0, 3.0),
861 );
862 let position = positions[vertex_index];
863 var output: VertexOutput;
864 output.position = vec4<f32>(position, 0.0, 1.0);
865 output.uv = position * 0.5 + vec2<f32>(0.5, 0.5);
866 return output;
867}
868
869@fragment
870fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
871 let p = input.uv * 2.0 - vec2<f32>(1.0, 1.0);
872 let a = sin((p.x * 7.0 + TIME * 2.4));
873 let b = sin((p.y * 8.0 - TIME * 1.8));
874 let c = sin((length(p) * 12.0 - TIME * 3.2));
875 let value = (a + b + c) / 3.0;
876 let cold = vec3<f32>(0.05, 0.16, 0.42);
877 let hot = vec3<f32>(0.10, 0.78, 0.92);
878 let flare = vec3<f32>(0.95, 0.55, 0.20) * pow(max(value, 0.0), 2.0);
879 return vec4<f32>(mix(cold, hot, value * 0.5 + 0.5) + flare, 1.0);
880}
881"#;
882
883const SHADER_LAB_RINGS_WGSL: &str = r#"override TIME: f32 = 0.0;
884
885struct VertexOutput {
886 @builtin(position) position: vec4<f32>,
887 @location(0) uv: vec2<f32>,
888};
889
890@vertex
891fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
892 let positions = array<vec2<f32>, 3>(
893 vec2<f32>(-1.0, -1.0),
894 vec2<f32>(3.0, -1.0),
895 vec2<f32>(-1.0, 3.0),
896 );
897 let position = positions[vertex_index];
898 var output: VertexOutput;
899 output.position = vec4<f32>(position, 0.0, 1.0);
900 output.uv = position * 0.5 + vec2<f32>(0.5, 0.5);
901 return output;
902}
903
904@fragment
905fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
906 let center = vec2<f32>(0.5 + sin(TIME * 1.1) * 0.08, 0.5 + cos(TIME * 0.9) * 0.08);
907 let p = input.uv - center;
908 let d = length(p);
909 let ring = 0.5 + 0.5 * cos((d * 18.0 - TIME * 2.0) * 6.28318);
910 let fade = 1.0 - smoothstep(0.15, 0.74, d);
911 let base = vec3<f32>(0.08, 0.06, 0.15);
912 let color = base + vec3<f32>(1.0, 0.55, 0.18) * ring * fade;
913 return vec4<f32>(color, 1.0);
914}
915"#;
916
917const SHADER_LAB_GRID_WGSL: &str = r#"override TIME: f32 = 0.0;
918
919struct VertexOutput {
920 @builtin(position) position: vec4<f32>,
921 @location(0) uv: vec2<f32>,
922};
923
924@vertex
925fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
926 let positions = array<vec2<f32>, 3>(
927 vec2<f32>(-1.0, -1.0),
928 vec2<f32>(3.0, -1.0),
929 vec2<f32>(-1.0, 3.0),
930 );
931 let position = positions[vertex_index];
932 var output: VertexOutput;
933 output.position = vec4<f32>(position, 0.0, 1.0);
934 output.uv = position * 0.5 + vec2<f32>(0.5, 0.5);
935 return output;
936}
937
938fn grid_line(value: f32) -> f32 {
939 let cell = abs(fract(value) - 0.5);
940 return 1.0 - smoothstep(0.46, 0.50, cell);
941}
942
943@fragment
944fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
945 let uv = input.uv + vec2<f32>(TIME * 0.04, TIME * -0.03);
946 let major = max(grid_line(uv.x * 8.0), grid_line(uv.y * 8.0));
947 let minor = max(grid_line(uv.x * 24.0), grid_line(uv.y * 24.0)) * 0.28;
948 let glow = max(major, minor);
949 let base = vec3<f32>(0.03, 0.04, 0.07);
950 let color = base + vec3<f32>(0.54, 0.38, 1.0) * glow;
951 return vec4<f32>(color, 1.0);
952}
953"#;
954
955const SHADER_LAB_VERTEX_WARP_WGSL: &str = r#"override TIME: f32 = 0.0;
956
957struct VertexOutput {
958 @builtin(position) position: vec4<f32>,
959 @location(0) uv: vec2<f32>,
960};
961
962@vertex
963fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
964 let uv_points = array<vec2<f32>, 3>(
965 vec2<f32>(0.06, 0.12),
966 vec2<f32>(0.96, 0.18),
967 vec2<f32>(0.16, 0.94),
968 );
969 let uv = uv_points[vertex_index];
970 let p = uv * 2.0 - vec2<f32>(1.0, 1.0);
971 let wave = sin(TIME * 2.2 + f32(vertex_index) * 2.1);
972 let bend = vec2<f32>(
973 0.12 * wave,
974 0.10 * cos(TIME * 1.7 + f32(vertex_index) * 1.6),
975 );
976 var output: VertexOutput;
977 output.position = vec4<f32>(p + bend, 0.0, 1.0);
978 output.uv = uv;
979 return output;
980}
981
982@fragment
983fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
984 let stripes = 0.5 + 0.5 * sin((input.uv.x + input.uv.y) * 24.0 - TIME * 4.0);
985 let edge = smoothstep(0.02, 0.12, min(min(input.uv.x, input.uv.y), 1.0 - max(input.uv.x, input.uv.y)));
986 let base = vec3<f32>(0.18, 0.10, 0.42);
987 let hot = vec3<f32>(0.98, 0.65, 0.20);
988 let color = mix(base, hot, stripes) + vec3<f32>(0.10, 0.32, 0.70) * input.uv.x;
989 return vec4<f32>(color * (0.72 + edge * 0.28), 1.0);
990}
991"#;
992
993const SHADER_LAB_ERROR_WGSL: &str = r#"override TIME: f32 = 0.0;
994
995struct VertexOutput {
996 @builtin(position) position: vec4<f32>,
997 @location(0) uv: vec2<f32>,
998};
999
1000@vertex
1001fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
1002 let positions = array<vec2<f32>, 3>(
1003 vec2<f32>(-1.0, -1.0),
1004 vec2<f32>(3.0, -1.0),
1005 vec2<f32>(-1.0, 3.0),
1006 );
1007 let position = positions[vertex_index];
1008 var output: VertexOutput;
1009 output.position = vec4<f32>(position, 0.0, 1.0);
1010 output.uv = position * 0.5 + vec2<f32>(0.5, 0.5);
1011 return output;
1012}
1013
1014@fragment
1015fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
1016 let stripe = step(0.5, fract((input.uv.x + input.uv.y + TIME * 0.04) * 14.0));
1017 let dark = vec3<f32>(0.10, 0.02, 0.04);
1018 let hot = vec3<f32>(0.58, 0.05, 0.12);
1019 return vec4<f32>(mix(dark, hot, stripe), 1.0);
1020}
1021"#;
1022
1023fn drag_drop_preview_status(
1024 payload: DragDropDemoPayload,
1025 target: DragDropDemoTarget,
1026) -> &'static str {
1027 match (payload, target) {
1028 (_, DragDropDemoTarget::Disabled) => "Drop target disabled",
1029 (DragDropDemoPayload::Text, DragDropDemoTarget::Text) => "Text payload can be dropped",
1030 (DragDropDemoPayload::Text, DragDropDemoTarget::FilesOnly) => {
1031 "Text payload rejected: files only"
1032 }
1033 (DragDropDemoPayload::Text, DragDropDemoTarget::ImageBytes) => {
1034 "Text payload rejected: image bytes only"
1035 }
1036 (DragDropDemoPayload::File, DragDropDemoTarget::FilesOnly) => "File payload can be dropped",
1037 (DragDropDemoPayload::File, DragDropDemoTarget::Text) => "File payload rejected: text only",
1038 (DragDropDemoPayload::File, DragDropDemoTarget::ImageBytes) => {
1039 "File payload rejected: image bytes only"
1040 }
1041 (DragDropDemoPayload::ImageBytes, DragDropDemoTarget::ImageBytes) => {
1042 "Image bytes can be dropped"
1043 }
1044 (DragDropDemoPayload::ImageBytes, DragDropDemoTarget::Text) => {
1045 "Image bytes rejected: text only"
1046 }
1047 (DragDropDemoPayload::ImageBytes, DragDropDemoTarget::FilesOnly) => {
1048 "Image bytes rejected: files only"
1049 }
1050 }
1051}
1052
1053fn drag_drop_drop_status(payload: DragDropDemoPayload, target: DragDropDemoTarget) -> &'static str {
1054 match (payload, target) {
1055 (_, DragDropDemoTarget::Disabled) => "Drop failed: target disabled",
1056 (DragDropDemoPayload::Text, DragDropDemoTarget::Text) => "Text payload accepted",
1057 (DragDropDemoPayload::Text, _) => "Text drag failed",
1058 (DragDropDemoPayload::File, DragDropDemoTarget::FilesOnly) => "File payload accepted",
1059 (DragDropDemoPayload::File, _) => "File drag failed",
1060 (DragDropDemoPayload::ImageBytes, DragDropDemoTarget::ImageBytes) => "Image bytes accepted",
1061 (DragDropDemoPayload::ImageBytes, _) => "Image byte drag failed",
1062 }
1063}
1064
1065#[derive(Clone, Debug)]
1066struct EditableTreeNode {
1067 id: String,
1068 label: String,
1069 children: Vec<EditableTreeNode>,
1070}
1071
1072impl EditableTreeNode {
1073 fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
1074 Self {
1075 id: id.into(),
1076 label: label.into(),
1077 children: Vec::new(),
1078 }
1079 }
1080
1081 fn with_children(mut self, children: Vec<EditableTreeNode>) -> Self {
1082 self.children = children;
1083 self
1084 }
1085}
1086
1087#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1088enum DateDemoMode {
1089 Single,
1090 Range,
1091 Week,
1092}
1093
1094#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1095enum ShowcaseThemeChoice {
1096 Light,
1097 Dark,
1098 Bubblegum,
1099}
1100
1101impl ShowcaseThemeChoice {
1102 const fn label(self) -> &'static str {
1103 match self {
1104 Self::Light => "Light",
1105 Self::Dark => "Dark",
1106 Self::Bubblegum => "Bubblegum",
1107 }
1108 }
1109
1110 const fn action(self) -> &'static str {
1111 match self {
1112 Self::Light => "theme.demo.light",
1113 Self::Dark => "theme.demo.dark",
1114 Self::Bubblegum => "theme.demo.bubblegum",
1115 }
1116 }
1117
1118 fn theme(self) -> Theme {
1119 match self {
1120 Self::Light => Theme::light(),
1121 Self::Dark => Theme::dark(),
1122 Self::Bubblegum => Theme::bubblegum(),
1123 }
1124 }
1125}
1126
1127thread_local! {
1128 static SHOWCASE_ACTIVE_THEME: std::cell::Cell<ShowcaseThemeChoice> =
1129 std::cell::Cell::new(ShowcaseThemeChoice::Dark);
1130}
1131
1132fn set_showcase_active_theme(choice: ShowcaseThemeChoice) {
1133 SHOWCASE_ACTIVE_THEME.with(|active| active.set(choice));
1134}
1135
1136fn active_showcase_theme_choice() -> ShowcaseThemeChoice {
1137 SHOWCASE_ACTIVE_THEME.with(std::cell::Cell::get)
1138}
1139
1140fn active_showcase_colors() -> ColorTokens {
1141 match active_showcase_theme_choice() {
1142 ShowcaseThemeChoice::Light => ColorTokens::light(),
1143 ShowcaseThemeChoice::Dark => ColorTokens::dark(),
1144 ShowcaseThemeChoice::Bubblegum => ColorTokens::bubblegum(),
1145 }
1146}
1147
1148#[derive(Clone, Copy)]
1149struct CanvasCubeState {
1150 yaw: f32,
1151 pitch: f32,
1152 drag_origin_yaw: f32,
1153 drag_origin_pitch: f32,
1154}
1155
1156impl Default for CanvasCubeState {
1157 fn default() -> Self {
1158 Self {
1159 yaw: 0.82,
1160 pitch: 0.52,
1161 drag_origin_yaw: 0.82,
1162 drag_origin_pitch: 0.52,
1163 }
1164 }
1165}
1166
1167impl CanvasCubeState {
1168 fn apply_drag(&mut self, drag: WidgetDrag) {
1169 match drag.phase {
1170 WidgetDragPhase::Begin => {
1171 self.drag_origin_yaw = self.yaw;
1172 self.drag_origin_pitch = self.pitch;
1173 self.apply_drag_delta(drag.total_delta);
1174 }
1175 WidgetDragPhase::Update | WidgetDragPhase::Commit => {
1176 self.apply_drag_delta(drag.total_delta);
1177 }
1178 WidgetDragPhase::Cancel => {
1179 self.yaw = self.drag_origin_yaw;
1180 self.pitch = self.drag_origin_pitch;
1181 }
1182 }
1183 }
1184
1185 fn apply_drag_delta(&mut self, total_delta: UiPoint) {
1186 self.yaw = self.drag_origin_yaw + total_delta.x * 0.012;
1187 self.pitch = (self.drag_origin_pitch + total_delta.y * 0.012).clamp(-1.25, 1.25);
1188 }
1189}
1190
1191impl Default for ShowcaseState {
1192 fn default() -> Self {
1193 let text = TextInputState::new("Editable text");
1194 let mut selectable_text = TextInputState::new("Selectable read-only text");
1195 selectable_text.set_selection(0, "Selectable".len());
1196 let form = profile_form_state();
1197 let form_name_text = TextInputState::new(profile_form_value(&form, "name"));
1198 let form_email_text = TextInputState::new(profile_form_value(&form, "email"));
1199 let form_role_text = TextInputState::new(profile_form_value(&form, "role"));
1200 let initial_select_options = select_options();
1201 let initial_image_select_options = select_options_with_images();
1202 let windows = ShowcaseWindows::default();
1203 let mut desktop = ext_widgets::FloatingDesktopState::with_visible_order(
1204 SHOWCASE_WIDGET_WINDOW_IDS
1205 .into_iter()
1206 .filter(|id| windows.is_visible(id))
1207 .map(str::to_string),
1208 showcase_window_z_policy(),
1209 );
1210 for id in SHOWCASE_WIDGET_WINDOW_IDS
1211 .into_iter()
1212 .filter(|id| windows.is_visible(id))
1213 {
1214 desktop.ensure_window(id, window_defaults(id));
1215 }
1216
1217 Self {
1218 checked: true,
1219 checkbox_indeterminate: widgets::CheckboxState::Indeterminate,
1220 slider: 10.0,
1221 slider_left: 1.0,
1222 slider_right: 10000.0,
1223 slider_value_text: TextInputState::new("10"),
1224 slider_left_text: TextInputState::new("1"),
1225 slider_right_text: TextInputState::new("10000"),
1226 slider_step_value: 10.0,
1227 slider_step_text: TextInputState::new("10"),
1228 slider_trailing_color: true,
1229 slider_trailing_picker: ext_widgets::ColorPickerState::new(color(120, 170, 230)),
1230 slider_trailing_picker_open: false,
1231 slider_thumb_picker: ext_widgets::ColorPickerState::new(color(235, 240, 247)),
1232 slider_thumb_picker_open: false,
1233 slider_thumb_shape: SliderThumbChoice::Circle,
1234 slider_use_steps: false,
1235 slider_logarithmic: true,
1236 slider_clamping: widgets::SliderClamping::Always,
1237 slider_smart_aim: true,
1238 label_locale: ext_widgets::SelectMenuState::with_selected(1),
1239 label_link_visited: false,
1240 label_hyperlink_visited: false,
1241 label_link_status: "No link action yet",
1242 color: ext_widgets::ColorPickerState::new(color(118, 183, 255)),
1243 date: ext_widgets::DatePickerModel::builder()
1244 .selected(CalendarDate::new(2026, 5, 12))
1245 .today(CalendarDate::new(2026, 5, 12))
1246 .build(),
1247 date_range: ext_widgets::DateRangePickerModel::builder()
1248 .range(Some(ext_widgets::CalendarDateRange::new(
1249 CalendarDate::new(2026, 5, 12).expect("demo range start"),
1250 CalendarDate::new(2026, 5, 18).expect("demo range end"),
1251 )))
1252 .today(CalendarDate::new(2026, 5, 12))
1253 .build(),
1254 date_mode: DateDemoMode::Single,
1255 radio_choice: "foo",
1256 switch_enabled: true,
1257 mixed_switch: ext_widgets::ToggleValue::Mixed,
1258 theme_preference: widgets::ThemePreference::Dark,
1259 showcase_theme: ShowcaseThemeChoice::Dark,
1260 numeric_value: 42.0,
1261 numeric_text: TextInputState::new("42.0"),
1262 numeric_range_min: 0.0,
1263 numeric_range_max: 100.0,
1264 numeric_range_min_text: TextInputState::new("0.0"),
1265 numeric_range_max_text: TextInputState::new("100.0"),
1266 numeric_sensitivity: 1.0,
1267 numeric_unit: ext_widgets::SelectMenuState::with_selected(0),
1268 numeric_drag_start: None,
1269 dropdown: ext_widgets::SelectMenuState::with_selected(1),
1270 select_menu: ext_widgets::SelectMenuState::with_selected(0)
1271 .with_open(&initial_select_options)
1272 .with_active(&initial_select_options, 2),
1273 image_select_menu: ext_widgets::SelectMenuState::with_selected(0)
1274 .with_open(&initial_image_select_options)
1275 .with_active(&initial_image_select_options, 1),
1276 text,
1277 selectable_text,
1278 singleline_text: TextInputState::new("Single line"),
1279 multiline_text: TextInputState::new("First line\nSecond line").multiline(true),
1280 text_area_text: TextInputState::new("Text area content").multiline(true),
1281 code_editor_text: TextInputState::new("fn main() {\n println!(\"showcase\");\n}")
1282 .multiline(true),
1283 search_text: TextInputState::new("widgets"),
1284 password_text: TextInputState::new("correct horse"),
1285 focused_text: None,
1286 platform: PlatformServiceClient::new(),
1287 clipboard_text: String::new(),
1288 pending_clipboard_paste: None,
1289 last_button: "None",
1290 toggle_button: false,
1291 table_selection: ext_widgets::DataTableSelection::single_row(2)
1292 .with_active_cell(ext_widgets::DataTableCellIndex::new(2, 1)),
1293 tree: ext_widgets::TreeViewState::expanded(["root", "child-0", "child-0-3", "child-1"]),
1294 editable_tree: editable_tree_default_nodes(),
1295 editable_tree_next_id: 100,
1296 editable_tree_status: "Use row buttons to add or delete children".to_owned(),
1297 outliner: ext_widgets::TreeViewState::expanded(["root", "assets"]),
1298 tree_virtual: ext_widgets::TreeViewState::expanded(["root", "src"]),
1299 tree_virtual_scroll: 0.0,
1300 tree_table: ext_widgets::TreeViewState::expanded(["root", "branch-a"]),
1301 tree_table_scroll: 0.0,
1302 toast_visible: false,
1303 toast_action_status: "No toast action",
1304 progress_phase: 0.0,
1305 progress_loading_elapsed: 0.0,
1306 progress_logs_scroll: operad::ScrollState::new(ScrollAxes::VERTICAL),
1307 progress_logs_follow_tail: true,
1308 animation_scrub: 0.0,
1309 animation_open: false,
1310 animation_timed_expanded: true,
1311 animation_scrub_expanded: true,
1312 animation_state_expanded: true,
1313 animation_interaction_expanded: true,
1314 easing_in: ext_widgets::SelectMenuState::with_selected(1),
1315 easing_out: ext_widgets::SelectMenuState::with_selected(1),
1316 caret_phase: 0.0,
1317 command_palette: ext_widgets::CommandPaletteState::new()
1318 .with_max_results(24)
1319 .with_first_active_match(&command_palette_items()),
1320 command_palette_open: false,
1321 command_history: ext_widgets::CommandPaletteHistory::with_capacity(4),
1322 last_command: "None".to_string(),
1323 list_scroll: 0.0,
1324 virtual_scroll: 0.0,
1325 table_scroll: 0.0,
1326 virtual_table_scroll: 0.0,
1327 virtual_table_descending: false,
1328 virtual_table_ready_only: false,
1329 virtual_table_value_width: 120.0,
1330 virtual_table_resize: None,
1331 layout_panel_a_scroll: 0.0,
1332 layout_panel_b_scroll: 0.0,
1333 layout_workspace_scroll: 0.0,
1334 scrollbars: scrollbar_widgets::ScrollbarControllerState::new(),
1335 layout_tab: 0,
1336 styling: StylingState::default(),
1337 styling_stroke_picker: ext_widgets::ColorPickerState::new(
1338 StylingState::default().stroke_color(),
1339 ),
1340 styling_stroke_picker_open: false,
1341 styling_fill_picker: ext_widgets::ColorPickerState::new(
1342 StylingState::default().fill_color(),
1343 ),
1344 styling_fill_picker_open: false,
1345 styling_shadow_picker: ext_widgets::ColorPickerState::new(
1346 StylingState::default().shadow_color(),
1347 ),
1348 styling_shadow_picker_open: false,
1349 cube: CanvasCubeState::default(),
1350 canvas_grow_horizontal: true,
1351 canvas_grow_vertical: true,
1352 canvas_keep_aspect_ratio: true,
1353 menu_bar: ext_widgets::MenuBarState {
1354 open_menu: Some(0),
1355 active_item: Some(0),
1356 },
1357 menu_button: ext_widgets::MenuButtonState::new(),
1358 image_text_menu_button: ext_widgets::MenuButtonState::new(),
1359 image_menu_button: ext_widgets::MenuButtonState::new(),
1360 context_menu: ext_widgets::ContextMenuState::closed(),
1361 menu_autosave: true,
1362 menu_grid: true,
1363 form,
1364 form_name_text,
1365 form_email_text,
1366 form_role_text,
1367 form_newsletter: true,
1368 form_status: "Unsaved profile changes".to_string(),
1369 overlay_expanded: true,
1370 overlay_popup_open: false,
1371 overlay_modal_open: false,
1372 color_picker_button_open: false,
1373 drag_drop_active_payload: None,
1374 drag_drop_status: "Idle",
1375 drag_drop_cursor_shape: CursorShape::Default,
1376 shader_lab_split: ext_widgets::SplitPaneState::new(0.52)
1377 .with_min_sizes(SHADER_LAB_PREVIEW_MIN_WIDTH, SHADER_LAB_EDITOR_MIN_WIDTH),
1378 shader_lab_editor_scroll: UiPoint::new(0.0, 0.0),
1379 shader_lab_show_frame_text: true,
1380 shader_lab_show_button_text: true,
1381 shader_lab_surface_stroke_width: 1.0,
1382 shader_lab_surface_radius: 8.0,
1383 shader_lab_target: ShaderLabTarget::Canvas,
1384 shader_lab_target_menu: ext_widgets::SelectMenuState::with_selected(0),
1385 shader_lab_preset: ShaderLabPreset::Plasma,
1386 shader_lab_preset_menu: ext_widgets::SelectMenuState::with_selected(0),
1387 shader_lab_material_shader: ShaderLabMaterialShader::Glow,
1388 shader_lab_material_shader_menu: ext_widgets::SelectMenuState::with_selected(3),
1389 shader_lab_material_shape: ShaderLabMaterialShape::Rounded,
1390 shader_lab_material_shape_menu: ext_widgets::SelectMenuState::with_selected(1),
1391 shader_lab_material_geometry: ShaderLabMaterialGeometry::Wave,
1392 shader_lab_material_geometry_menu: ext_widgets::SelectMenuState::with_selected(3),
1393 shader_lab_material_outset: SHADER_LAB_MATERIAL_OUTSET,
1394 shader_lab_source: TextInputState::new(ShaderLabPreset::Plasma.source())
1395 .multiline(true),
1396 shader_lab_source_error: None,
1397 timeline_scroll: operad::ScrollState::new(ScrollAxes::HORIZONTAL).with_sizes(
1398 UiSize::new(620.0, TIMELINE_VIEWPORT_HEIGHT),
1399 UiSize::new(TIMELINE_CONTENT_WIDTH, TIMELINE_VIEWPORT_HEIGHT),
1400 ),
1401 panels_top_split: ext_widgets::SplitPaneState::new(0.18).with_min_sizes(46.0, 150.0),
1402 panels_bottom_split: ext_widgets::SplitPaneState::new(0.74).with_min_sizes(120.0, 46.0),
1403 panels_left_split: ext_widgets::SplitPaneState::new(0.22).with_min_sizes(76.0, 180.0),
1404 panels_right_split: ext_widgets::SplitPaneState::new(0.74).with_min_sizes(120.0, 76.0),
1405 layout_split: ext_widgets::SplitPaneState::new(0.44).with_min_sizes(80.0, 80.0),
1406 layout_dock: ext_widgets::DockWorkspaceState::new(),
1407 diagnostics_animation_paused: false,
1408 diagnostics_animation_scrub: 0.35,
1409 diagnostics_animation_active: true,
1410 diagnostics_animation_hover: 0.35,
1411 diagnostics_animation_pulse_count: 0,
1412 diagnostics_snapshot: diagnostics_sample_snapshot_for(0.35, true),
1413 containers_scroll: operad::ScrollState::new(ScrollAxes::VERTICAL)
1414 .with_sizes(UiSize::new(260.0, 82.0), UiSize::new(260.0, 180.0))
1415 .with_offset(UiPoint::new(0.0, 18.0)),
1416 controls_scroll: operad::ScrollState::new(ScrollAxes::VERTICAL),
1417 color_copied_hex: None,
1418 fps_last_sample: Instant::now(),
1419 fps_frames: 0,
1420 fps: 0.0,
1421 last_desktop_size: desktop_size_for_viewport(UiSize::new(900.0, 760.0)),
1422 initial_organize_pending: true,
1423 windows,
1424 desktop,
1425 user_image_update: showcase_user_image_update(),
1426 }
1427 }
1428}
1429
1430struct ShowcaseWindows {
1431 labels: bool,
1432 buttons: bool,
1433 checkbox: bool,
1434 toggles: bool,
1435 slider: bool,
1436 numeric: bool,
1437 text_input: bool,
1438 selection: bool,
1439 menus: bool,
1440 command_palette: bool,
1441 date_picker: bool,
1442 color_picker: bool,
1443 progress: bool,
1444 animation: bool,
1445 easing: bool,
1446 lists_tables: bool,
1447 property_inspector: bool,
1448 diagnostics: bool,
1449 trees: bool,
1450 layout_widgets: bool,
1451 containers: bool,
1452 panels: bool,
1453 forms: bool,
1454 overlays: bool,
1455 drag_drop: bool,
1456 media: bool,
1457 shaders: bool,
1458 shader_lab: bool,
1459 timeline: bool,
1460 canvas: bool,
1461 theme: bool,
1462 styling: bool,
1463}
1464
1465impl Default for ShowcaseWindows {
1466 fn default() -> Self {
1467 Self {
1468 labels: true,
1469 buttons: true,
1470 checkbox: false,
1471 toggles: false,
1472 slider: false,
1473 numeric: false,
1474 text_input: false,
1475 selection: false,
1476 menus: false,
1477 command_palette: false,
1478 date_picker: false,
1479 color_picker: true,
1480 progress: false,
1481 animation: false,
1482 easing: false,
1483 lists_tables: false,
1484 property_inspector: false,
1485 diagnostics: false,
1486 trees: false,
1487 layout_widgets: false,
1488 containers: false,
1489 panels: false,
1490 forms: false,
1491 overlays: false,
1492 drag_drop: false,
1493 media: false,
1494 shaders: false,
1495 shader_lab: false,
1496 timeline: false,
1497 canvas: true,
1498 theme: false,
1499 styling: false,
1500 }
1501 }
1502}
1503
1504impl ShowcaseWindows {
1505 fn is_visible(&self, id: &str) -> bool {
1506 match id {
1507 "labels" => self.labels,
1508 "buttons" => self.buttons,
1509 "checkbox" => self.checkbox,
1510 "toggles" => self.toggles,
1511 "slider" => self.slider,
1512 "numeric" => self.numeric,
1513 "text_input" => self.text_input,
1514 "selection" => self.selection,
1515 "menus" => self.menus,
1516 "command_palette" => self.command_palette,
1517 "date_picker" => self.date_picker,
1518 "color_picker" => self.color_picker,
1519 "progress" => self.progress,
1520 "animation" => self.animation,
1521 "easing" => self.easing,
1522 "lists_tables" => self.lists_tables,
1523 "property_inspector" => self.property_inspector,
1524 "diagnostics" => self.diagnostics,
1525 "trees" => self.trees,
1526 "layout_widgets" => self.layout_widgets,
1527 "containers" => self.containers,
1528 "panels" => self.panels,
1529 "forms" => self.forms,
1530 "overlays" => self.overlays,
1531 "drag_drop" => self.drag_drop,
1532 "media" => self.media,
1533 "shaders" => self.shaders,
1534 "shader_lab" => self.shader_lab,
1535 "timeline" => self.timeline,
1536 "canvas" => self.canvas,
1537 "theme" => self.theme,
1538 "styling" => self.styling,
1539 _ => false,
1540 }
1541 }
1542
1543 fn slot_mut(&mut self, id: &str) -> Option<&mut bool> {
1544 match id {
1545 "labels" => Some(&mut self.labels),
1546 "buttons" => Some(&mut self.buttons),
1547 "checkbox" => Some(&mut self.checkbox),
1548 "toggles" => Some(&mut self.toggles),
1549 "slider" => Some(&mut self.slider),
1550 "numeric" => Some(&mut self.numeric),
1551 "text_input" => Some(&mut self.text_input),
1552 "selection" => Some(&mut self.selection),
1553 "menus" => Some(&mut self.menus),
1554 "command_palette" => Some(&mut self.command_palette),
1555 "date_picker" => Some(&mut self.date_picker),
1556 "color_picker" => Some(&mut self.color_picker),
1557 "progress" => Some(&mut self.progress),
1558 "animation" => Some(&mut self.animation),
1559 "easing" => Some(&mut self.easing),
1560 "lists_tables" => Some(&mut self.lists_tables),
1561 "property_inspector" => Some(&mut self.property_inspector),
1562 "diagnostics" => Some(&mut self.diagnostics),
1563 "trees" => Some(&mut self.trees),
1564 "layout_widgets" => Some(&mut self.layout_widgets),
1565 "containers" => Some(&mut self.containers),
1566 "panels" => Some(&mut self.panels),
1567 "forms" => Some(&mut self.forms),
1568 "overlays" => Some(&mut self.overlays),
1569 "drag_drop" => Some(&mut self.drag_drop),
1570 "media" => Some(&mut self.media),
1571 "shaders" => Some(&mut self.shaders),
1572 "shader_lab" => Some(&mut self.shader_lab),
1573 "timeline" => Some(&mut self.timeline),
1574 "canvas" => Some(&mut self.canvas),
1575 "theme" => Some(&mut self.theme),
1576 "styling" => Some(&mut self.styling),
1577 _ => None,
1578 }
1579 }
1580
1581 fn toggle(&mut self, id: &str) -> Option<bool> {
1582 if let Some(visible) = self.slot_mut(id) {
1583 *visible = !*visible;
1584 return Some(*visible);
1585 }
1586 None
1587 }
1588
1589 fn close(&mut self, id: &str) {
1590 if let Some(visible) = self.slot_mut(id) {
1591 *visible = false;
1592 }
1593 }
1594
1595 fn clear_all(&mut self) {
1596 for id in SHOWCASE_WIDGET_WINDOW_IDS {
1597 if let Some(visible) = self.slot_mut(id) {
1598 *visible = false;
1599 }
1600 }
1601 }
1602
1603 fn open_all(&mut self) {
1604 for id in SHOWCASE_WIDGET_WINDOW_IDS {
1605 if let Some(visible) = self.slot_mut(id) {
1606 *visible = true;
1607 }
1608 }
1609 }
1610}
1611
1612fn showcase_window_z_policy() -> ext_widgets::FloatingDesktopZPolicy {
1613 ext_widgets::FloatingDesktopZPolicy::new(
1614 SHOWCASE_WINDOW_Z_BASE,
1615 SHOWCASE_WINDOW_Z_STRIDE,
1616 SHOWCASE_WINDOW_Z_MAX,
1617 )
1618}
1619
1620fn window_defaults(id: &str) -> ext_widgets::FloatingWindowDefaults {
1621 ext_widgets::FloatingWindowDefaults::new(
1622 default_window_position(id),
1623 default_window_size(id),
1624 default_window_state_min_size(id),
1625 )
1626}
1627
1628fn desktop_size_for_viewport(viewport: UiSize) -> UiSize {
1629 UiSize::new(
1630 (viewport.width - RIGHT_PANEL_WIDTH).max(360.0),
1631 viewport.height,
1632 )
1633}
1634
1635fn showcase_desktop_options(
1636 desktop_size: UiSize,
1637 theme: &Theme,
1638) -> ext_widgets::FloatingDesktopOptions {
1639 let mut options = ext_widgets::FloatingDesktopOptions::new(desktop_size).with_layout(
1640 LayoutStyle::new()
1641 .with_width_percent(1.0)
1642 .with_height_percent(1.0),
1643 );
1644 options = options.with_bounds_rect(UiRect::new(
1645 0.0,
1646 SHOWCASE_ORGANIZE_BUTTON_RESERVED_HEIGHT,
1647 desktop_size.width,
1648 (desktop_size.height - SHOWCASE_ORGANIZE_BUTTON_RESERVED_HEIGHT).max(0.0),
1649 ));
1650 options.base_z_index = SHOWCASE_WINDOW_Z_BASE;
1651 options.window_z_stride = SHOWCASE_WINDOW_Z_STRIDE;
1652 options.margin = 18.0;
1653 options.gap = 14.0;
1654 options.window_visual = UiVisual::panel(theme.colors.surface, Some(theme.stroke.surface), 0.0);
1655 options.title_bar_visual =
1656 UiVisual::panel(theme.colors.surface_muted, Some(theme.stroke.surface), 0.0);
1657 options.content_visual = UiVisual::panel(theme.colors.surface, None, 0.0);
1658 options.title_style = themed_text(theme, 13.0);
1659 options.close_button_text_style = themed_text(theme, 14.0);
1660 options.close_button_visual = UiVisual::panel(ColorRgba::TRANSPARENT, None, 3.0);
1661 options.close_button_hovered_visual =
1662 theme.resolve_visual(ComponentRole::Button, ComponentState::HOVERED);
1663 options.close_button_pressed_visual =
1664 theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED);
1665 options
1666}
1667
1668impl ShowcaseState {
1669 fn prepare_frame(&mut self, viewport: UiSize) {
1670 self.last_desktop_size = desktop_size_for_viewport(viewport);
1671 if self.initial_organize_pending {
1672 self.organize_open_windows();
1673 self.initial_organize_pending = false;
1674 }
1675 self.record_frame();
1676 }
1677
1678 fn record_frame(&mut self) {
1679 self.fps_frames = self.fps_frames.saturating_add(1);
1680 let now = Instant::now();
1681 let elapsed = now
1682 .checked_duration_since(self.fps_last_sample)
1683 .unwrap_or(Duration::ZERO);
1684 if elapsed < SHOWCASE_FPS_SAMPLE_INTERVAL {
1685 return;
1686 }
1687 let seconds = elapsed.as_secs_f32().max(f32::EPSILON);
1688 self.fps = self.fps_frames as f32 / seconds;
1689 self.fps_frames = 0;
1690 self.fps_last_sample = now;
1691 }
1692
1693 fn request_drag_drop_cursor(&mut self, shape: CursorShape) {
1694 if self.drag_drop_cursor_shape == shape {
1695 return;
1696 }
1697 self.drag_drop_cursor_shape = shape;
1698 self.platform
1699 .request(PlatformRequest::Cursor(CursorRequest::SetShape(shape)));
1700 }
1701
1702 fn apply_drag_drop_source_action(
1703 &mut self,
1704 payload: DragDropDemoPayload,
1705 started: &'static str,
1706 dragging: &'static str,
1707 finished: &'static str,
1708 canceled: &'static str,
1709 kind: &WidgetActionKind,
1710 ) {
1711 match kind {
1712 WidgetActionKind::Drag(drag) => match drag.phase {
1713 WidgetDragPhase::Begin => {
1714 self.drag_drop_active_payload = Some(payload);
1715 self.drag_drop_status = started;
1716 self.request_drag_drop_cursor(CursorShape::Grabbing);
1717 }
1718 WidgetDragPhase::Update => {
1719 self.drag_drop_active_payload = Some(payload);
1720 self.drag_drop_status = dragging;
1721 self.request_drag_drop_cursor(CursorShape::Grabbing);
1722 }
1723 WidgetDragPhase::Commit => {
1724 self.drag_drop_status = finished;
1725 self.request_drag_drop_cursor(CursorShape::Default);
1726 }
1727 WidgetDragPhase::Cancel => {
1728 self.drag_drop_active_payload = None;
1729 self.drag_drop_status = canceled;
1730 self.request_drag_drop_cursor(CursorShape::Default);
1731 }
1732 },
1733 WidgetActionKind::Activate(_) => {
1734 self.drag_drop_active_payload = None;
1735 self.drag_drop_status = canceled;
1736 self.request_drag_drop_cursor(CursorShape::Default);
1737 }
1738 _ => {}
1739 }
1740 }
1741
1742 fn apply_drag_drop_target_action(
1743 &mut self,
1744 target: DragDropDemoTarget,
1745 kind: &WidgetActionKind,
1746 ) {
1747 let WidgetActionKind::Drag(drag) = kind else {
1748 return;
1749 };
1750 let Some(payload) = self.drag_drop_active_payload else {
1751 return;
1752 };
1753 self.drag_drop_status = match drag.phase {
1754 WidgetDragPhase::Begin | WidgetDragPhase::Update => {
1755 drag_drop_preview_status(payload, target)
1756 }
1757 WidgetDragPhase::Commit => drag_drop_drop_status(payload, target),
1758 WidgetDragPhase::Cancel => "Drop canceled",
1759 };
1760 if matches!(
1761 drag.phase,
1762 WidgetDragPhase::Commit | WidgetDragPhase::Cancel
1763 ) {
1764 self.drag_drop_active_payload = None;
1765 self.request_drag_drop_cursor(CursorShape::Default);
1766 }
1767 }
1768
1769 fn organize_open_windows(&mut self) {
1770 let desktop_size = self.last_desktop_size;
1771 let theme = self.app_theme();
1772 let options = showcase_desktop_options(desktop_size, &theme);
1773 let arrange_rect = UiRect::new(
1774 0.0,
1775 SHOWCASE_ORGANIZE_BUTTON_RESERVED_HEIGHT,
1776 desktop_size.width,
1777 (desktop_size.height - SHOWCASE_ORGANIZE_BUTTON_RESERVED_HEIGHT).max(0.0),
1778 );
1779 let measured_sizes = self.measured_open_window_sizes(desktop_size);
1780 let windows = SHOWCASE_WIDGET_WINDOW_IDS
1781 .into_iter()
1782 .filter(|id| self.windows.is_visible(id))
1783 .map(|id| {
1784 let mut defaults = window_defaults(id);
1785 let mut collapsed_size =
1786 UiSize::new(defaults.min_size.width, options.title_bar_height);
1787 if let Some(measurement) = measured_sizes
1788 .iter()
1789 .find(|measurement| measurement.id == id)
1790 {
1791 defaults.size = UiSize::new(
1792 measurement.size.width.max(defaults.size.width),
1793 measurement.size.height.max(defaults.size.height),
1794 );
1795 defaults.min_size = UiSize::new(
1796 defaults.min_size.width.max(measurement.min_size.width),
1797 defaults.min_size.height.max(measurement.min_size.height),
1798 );
1799 collapsed_size = UiSize::new(
1800 collapsed_size.width.max(measurement.collapsed_size.width),
1801 collapsed_size.height.max(measurement.collapsed_size.height),
1802 );
1803 }
1804 ext_widgets::FloatingWindowOrganizeSpec::new(id, defaults)
1805 .with_collapsed_size(collapsed_size)
1806 });
1807 let _outcome = self
1808 .desktop
1809 .organize_window_specs_in_rect(windows, arrange_rect, &options);
1810 }
1811
1812 fn measured_open_window_sizes(&self, desktop_size: UiSize) -> Vec<ShowcaseWindowMeasurement> {
1813 let measure_height = desktop_size.height.max(SHOWCASE_ORGANIZE_MEASURE_HEIGHT);
1814 let viewport = UiSize::new(desktop_size.width + RIGHT_PANEL_WIDTH, measure_height);
1815 let mut document = self.window_measurement_view(viewport);
1816 #[cfg(feature = "text-cosmic")]
1817 let mut measurer = CosmicTextMeasurer::new();
1818 #[cfg(not(feature = "text-cosmic"))]
1819 let mut measurer = ApproxTextMeasurer;
1820 if document.compute_layout(viewport, &mut measurer).is_err() {
1821 return Vec::new();
1822 }
1823 let theme = self.app_theme();
1824 let options = showcase_desktop_options(desktop_size, &theme);
1825 let mut measurements = SHOWCASE_WIDGET_WINDOW_IDS
1826 .into_iter()
1827 .filter(|id| self.windows.is_visible(id))
1828 .filter_map(|id| {
1829 let name = format!("showcase.windows.window.{id}");
1830 let collapsed_size = showcase_collapsed_window_size(id, &options);
1831 document
1832 .nodes()
1833 .iter()
1834 .find(|node| node.name() == name)
1835 .map(|node| {
1836 let min_size = node.style().layout_style().min_size();
1837 ShowcaseWindowMeasurement {
1838 id: id.to_string(),
1839 size: UiSize::new(node.layout().rect.width, node.layout().rect.height),
1840 min_size: UiSize::new(
1841 min_size
1842 .and_then(|size| size.width.points_value())
1843 .unwrap_or(node.layout().rect.width),
1844 min_size
1845 .and_then(|size| size.height.points_value())
1846 .unwrap_or(node.layout().rect.height),
1847 ),
1848 collapsed_size,
1849 }
1850 })
1851 })
1852 .collect::<Vec<_>>();
1853
1854 let mut compact_desktop = self.desktop.clone();
1855 compact_desktop.collapsed.clear();
1856 for measurement in &measurements {
1857 compact_desktop.sizes.insert(
1858 measurement.id.clone(),
1859 UiSize::new(
1860 measurement.min_size.width,
1861 default_window_state_min_size(&measurement.id).height,
1862 ),
1863 );
1864 compact_desktop.user_sized.insert(measurement.id.clone());
1865 }
1866 let mut compact_document =
1867 self.window_measurement_view_with_desktop(viewport, compact_desktop);
1868 #[cfg(feature = "text-cosmic")]
1869 let mut compact_measurer = CosmicTextMeasurer::new();
1870 #[cfg(not(feature = "text-cosmic"))]
1871 let mut compact_measurer = ApproxTextMeasurer;
1872 if compact_document
1873 .compute_layout(viewport, &mut compact_measurer)
1874 .is_ok()
1875 {
1876 for measurement in &mut measurements {
1877 let name = format!("showcase.windows.window.{}", measurement.id);
1878 if let Some(node) = compact_document
1879 .nodes()
1880 .iter()
1881 .find(|node| node.name() == name)
1882 {
1883 measurement.min_size.width =
1884 measurement.min_size.width.max(node.layout().rect.width);
1885 measurement.min_size.height =
1886 measurement.min_size.height.max(node.layout().rect.height);
1887 }
1888 }
1889 }
1890
1891 measurements
1892 }
1893
1894 fn window_measurement_view(&self, viewport: UiSize) -> UiDocument {
1895 self.window_measurement_view_with_desktop(viewport, self.desktop.clone())
1896 }
1897
1898 fn window_measurement_view_with_desktop(
1899 &self,
1900 viewport: UiSize,
1901 mut measurement_desktop: ext_widgets::FloatingDesktopState,
1902 ) -> UiDocument {
1903 set_showcase_active_theme(self.showcase_theme);
1904 let theme = self.app_theme();
1905 let desktop_size = desktop_size_for_viewport(viewport);
1906 measurement_desktop.collapsed.clear();
1907
1908 let mut ui = UiDocument::with_capacity(
1909 root_style(viewport.width, viewport.height),
1910 SHOWCASE_DOCUMENT_NODE_CAPACITY,
1911 );
1912 let root = ui.root();
1913 let desktop = ui.add_child(
1914 root,
1915 UiNode::container(
1916 "showcase.desktop.measurement",
1917 LayoutStyle::new()
1918 .with_width(desktop_size.width)
1919 .with_height(viewport.height),
1920 ),
1921 );
1922 showcase_windows_with_desktop_state(
1923 &mut ui,
1924 desktop,
1925 self,
1926 &measurement_desktop,
1927 desktop_size,
1928 &theme,
1929 );
1930 ui
1931 }
1932
1933 fn update(&mut self, action: WidgetAction) {
1934 let WidgetAction { binding, kind, .. } = action;
1935 let WidgetActionBinding::Action(action_id) = binding else {
1936 return;
1937 };
1938 let action_id = action_id.as_str();
1939
1940 let color_outcome = self.color.apply_action(
1941 action_id,
1942 kind.clone(),
1943 ext_widgets::ColorPickerActionOptions::new("color").copy_hex("color.copy_hex"),
1944 );
1945 if color_outcome.update.is_some()
1946 || color_outcome.effect.is_some()
1947 || color_outcome.mode_changed
1948 {
1949 if let Some(ext_widgets::ColorPickerEffect::CopyHex(hex)) = color_outcome.effect {
1950 self.copy_text_to_clipboard(&hex);
1951 self.color_copied_hex = Some(hex);
1952 }
1953 return;
1954 }
1955 let color_button_picker_outcome = self.color.apply_action(
1956 action_id,
1957 kind.clone(),
1958 ext_widgets::ColorPickerActionOptions::new("color.button_picker"),
1959 );
1960 if color_button_picker_outcome.update.is_some() || color_button_picker_outcome.mode_changed
1961 {
1962 return;
1963 }
1964 let slider_color_outcome = self.slider_trailing_picker.apply_action(
1965 action_id,
1966 kind.clone(),
1967 ext_widgets::ColorPickerActionOptions::new("slider.trailing_picker"),
1968 );
1969 if slider_color_outcome.update.is_some() || slider_color_outcome.mode_changed {
1970 return;
1971 }
1972 let slider_thumb_outcome = self.slider_thumb_picker.apply_action(
1973 action_id,
1974 kind.clone(),
1975 ext_widgets::ColorPickerActionOptions::new("slider.thumb_picker"),
1976 );
1977 if slider_thumb_outcome.update.is_some() || slider_thumb_outcome.mode_changed {
1978 return;
1979 }
1980 let styling_stroke_outcome = self.styling_stroke_picker.apply_action(
1981 action_id,
1982 kind.clone(),
1983 ext_widgets::ColorPickerActionOptions::new("styling.stroke_picker"),
1984 );
1985 if styling_stroke_outcome.update.is_some() || styling_stroke_outcome.mode_changed {
1986 self.styling.stroke = self.styling_stroke_picker.value();
1987 return;
1988 }
1989 let styling_fill_outcome = self.styling_fill_picker.apply_action(
1990 action_id,
1991 kind.clone(),
1992 ext_widgets::ColorPickerActionOptions::new("styling.fill_picker"),
1993 );
1994 if styling_fill_outcome.update.is_some() || styling_fill_outcome.mode_changed {
1995 self.styling.fill = self.styling_fill_picker.value();
1996 return;
1997 }
1998 let styling_shadow_outcome = self.styling_shadow_picker.apply_action(
1999 action_id,
2000 kind.clone(),
2001 ext_widgets::ColorPickerActionOptions::new("styling.shadow_picker"),
2002 );
2003 if styling_shadow_outcome.update.is_some() || styling_shadow_outcome.mode_changed {
2004 self.styling.shadow = self.styling_shadow_picker.value();
2005 return;
2006 }
2007
2008 if action_id == "window.clear_all" {
2009 self.windows.clear_all();
2010 for id in SHOWCASE_WIDGET_WINDOW_IDS {
2011 self.desktop.close(id);
2012 }
2013 self.command_palette_open = false;
2014 return;
2015 }
2016 if action_id == "window.add_all" {
2017 self.windows.open_all();
2018 for id in SHOWCASE_WIDGET_WINDOW_IDS {
2019 self.desktop.ensure_window(id, window_defaults(id));
2020 self.desktop.bring_to_front(id);
2021 }
2022 self.reset_progress_loading();
2023 self.organize_open_windows();
2024 self.initial_organize_pending = false;
2025 return;
2026 }
2027 if action_id == "window.organize_open" {
2028 self.organize_open_windows();
2029 self.initial_organize_pending = false;
2030 return;
2031 }
2032 if let Some(id) = action_id.strip_prefix("window.toggle.") {
2033 let visible = self.windows.toggle(id).unwrap_or(false);
2034 if visible {
2035 self.desktop.ensure_window(id, window_defaults(id));
2036 self.desktop.bring_to_front(id);
2037 if id == "progress" {
2038 self.reset_progress_loading();
2039 }
2040 } else {
2041 self.desktop.close(id);
2042 }
2043 if id == "command_palette" {
2044 self.command_palette_open = visible;
2045 }
2046 return;
2047 }
2048 if let Some(id) = action_id.strip_prefix("window.close.") {
2049 self.windows.close(id);
2050 self.desktop.close(id);
2051 if id == "command_palette" {
2052 self.command_palette_open = false;
2053 }
2054 return;
2055 }
2056 if let Some(id) = action_id.strip_prefix("window.activate.") {
2057 self.desktop.bring_to_front(id);
2058 return;
2059 }
2060 if let Some(id) = action_id.strip_prefix("window.drag.") {
2061 if let WidgetActionKind::PointerEdit(edit) = kind {
2062 self.desktop
2063 .apply_drag(id, edit, default_window_position(id));
2064 }
2065 return;
2066 }
2067 if let Some(id) = action_id.strip_prefix("window.resize.") {
2068 if let WidgetActionKind::PointerEdit(edit) = kind {
2069 self.desktop.apply_resize(id, edit, window_defaults(id));
2070 }
2071 return;
2072 }
2073 if let Some(id) = action_id.strip_prefix("window.collapse.") {
2074 self.desktop.toggle_collapsed(id);
2075 return;
2076 }
2077 if let Some(id) = window_for_action(action_id) {
2078 self.desktop.bring_to_front(id);
2079 }
2080 if action_id == "runtime.tick" {
2081 self.progress_phase += SHOWCASE_PROGRESS_RADIANS_PER_SECOND / SHOWCASE_TICK_RATE_HZ;
2082 if self.windows.progress {
2083 self.progress_loading_elapsed = (self.progress_loading_elapsed
2084 + 1.0 / SHOWCASE_TICK_RATE_HZ)
2085 .min(PROGRESS_LOGGED_DURATION_SECONDS);
2086 }
2087 self.caret_phase = (self.caret_phase
2088 + std::f32::consts::TAU * TEXT_CARET_BLINK_HZ / SHOWCASE_TICK_RATE_HZ)
2089 % std::f32::consts::TAU;
2090 return;
2091 }
2092 if action_id == "progress.logged.reset" {
2093 self.reset_progress_loading();
2094 return;
2095 }
2096 if action_id == "command_palette.search" {
2097 if let WidgetActionKind::TextEdit(edit) = kind {
2098 self.apply_command_palette_event(edit.event);
2099 }
2100 return;
2101 }
2102 if action_id == "command_palette.open" {
2103 let items = command_palette_items_with_history(&self.command_history);
2104 self.command_palette.refresh_active_match(&items);
2105 self.command_palette_open = true;
2106 return;
2107 }
2108 if action_id == "command_palette.close" {
2109 self.command_palette_open = false;
2110 return;
2111 }
2112 if let Some(id) = action_id.strip_prefix("command_palette.item.") {
2113 self.select_command_palette_item(id);
2114 return;
2115 }
2116 if let Some(input) = focused_text_for_action(action_id) {
2117 match kind {
2118 WidgetActionKind::TextEdit(edit) => self.apply_text_edit(input, edit),
2119 WidgetActionKind::Focus(change) => self.apply_text_focus(input, change.focused),
2120 _ => {}
2121 }
2122 return;
2123 }
2124 if matches!(kind, WidgetActionKind::Focus(_)) {
2125 return;
2126 }
2127
2128 match action_id {
2129 "labels.link" => {
2130 self.label_link_visited = true;
2131 self.label_link_status = "Internal link activated";
2132 return;
2133 }
2134 "labels.hyperlink" => {
2135 self.label_hyperlink_visited = true;
2136 self.label_link_status = "Opened docs.rs/operad";
2137 self.platform.open_url("https://docs.rs/operad");
2138 return;
2139 }
2140 "button.default" => self.last_button = "Default",
2141 "button.primary" => self.last_button = "Primary",
2142 "button.secondary" => self.last_button = "Secondary",
2143 "button.destructive" => self.last_button = "Destructive",
2144 "button.small" => self.last_button = "Small",
2145 "button.icon" => self.last_button = "Settings",
2146 "button.image" => self.last_button = "Folder",
2147 "button.reset" => {
2148 self.toggle_button = false;
2149 self.last_button = "Reset";
2150 }
2151 "button.toggle" => {
2152 self.toggle_button = !self.toggle_button;
2153 self.last_button = "Toggle";
2154 }
2155 "checkbox.enabled"
2156 | "checkbox.large"
2157 | "checkbox.custom_color"
2158 | "checkbox.image_check"
2159 | "checkbox.compact_gap" => self.checked = !self.checked,
2160 "checkbox.indeterminate" => {
2161 self.checkbox_indeterminate = self.checkbox_indeterminate.next(true);
2162 return;
2163 }
2164 "labels.locale.toggle" => {
2165 self.label_locale.toggle(&label_locale_options());
2166 return;
2167 }
2168 "toggles.switch" => self.switch_enabled = !self.switch_enabled,
2169 "toggles.mixed" => self.mixed_switch = self.mixed_switch.toggled(),
2170 "toggles.radio.foo" => self.radio_choice = "foo",
2171 "toggles.radio.bar" => self.radio_choice = "bar",
2172 "toggles.radio.baz" => self.radio_choice = "baz",
2173 "toggles.theme.system" => {
2174 self.theme_preference = widgets::ThemePreference::System;
2175 return;
2176 }
2177 "toggles.theme.light" => {
2178 self.theme_preference = widgets::ThemePreference::Light;
2179 return;
2180 }
2181 "toggles.theme.dark" => {
2182 self.theme_preference = widgets::ThemePreference::Dark;
2183 return;
2184 }
2185 "theme.preference.dark" => {
2186 self.theme_preference = if self.theme_preference.is_dark() {
2187 widgets::ThemePreference::Light
2188 } else {
2189 widgets::ThemePreference::Dark
2190 };
2191 return;
2192 }
2193 "theme.demo.light" => {
2194 self.showcase_theme = ShowcaseThemeChoice::Light;
2195 return;
2196 }
2197 "theme.demo.dark" => {
2198 self.showcase_theme = ShowcaseThemeChoice::Dark;
2199 return;
2200 }
2201 "theme.demo.bubblegum" => {
2202 self.showcase_theme = ShowcaseThemeChoice::Bubblegum;
2203 return;
2204 }
2205 "selection.dropdown.toggle" => {
2206 self.dropdown.toggle(&select_options());
2207 return;
2208 }
2209 "numeric.unit.toggle" => {
2210 self.numeric_unit.toggle(&numeric_unit_options());
2211 return;
2212 }
2213 "menus.menu_button" => {
2214 let button_items = menu_items(self.menu_autosave);
2215 let outcome = self.menu_button.toggle(&button_items);
2216 if outcome.opened {
2217 self.image_text_menu_button.close();
2218 self.image_menu_button.close();
2219 self.context_menu.close();
2220 }
2221 return;
2222 }
2223 "menus.image_text_menu_button" => {
2224 let button_items = menu_items(self.menu_autosave);
2225 let outcome = self.image_text_menu_button.toggle(&button_items);
2226 if outcome.opened {
2227 self.menu_button.close();
2228 self.image_menu_button.close();
2229 self.context_menu.close();
2230 }
2231 return;
2232 }
2233 "menus.image_menu_button" => {
2234 let button_items = menu_items(self.menu_autosave);
2235 let outcome = self.image_menu_button.toggle(&button_items);
2236 if outcome.opened {
2237 self.menu_button.close();
2238 self.image_text_menu_button.close();
2239 self.context_menu.close();
2240 }
2241 return;
2242 }
2243 "menus.context.open" => {
2244 self.context_menu
2245 .open_with_items(menu_demo_context_anchor(), &menu_items(self.menu_autosave));
2246 self.menu_bar.close();
2247 self.menu_button.close();
2248 self.image_text_menu_button.close();
2249 self.image_menu_button.close();
2250 return;
2251 }
2252 "menus.context.close" => {
2253 self.context_menu.close();
2254 return;
2255 }
2256 "menus.bar.file" => {
2257 self.menu_bar
2258 .open(&menu_bar_menus(self.menu_autosave, self.menu_grid), 0);
2259 return;
2260 }
2261 "menus.bar.edit" => {
2262 self.menu_bar
2263 .open(&menu_bar_menus(self.menu_autosave, self.menu_grid), 1);
2264 return;
2265 }
2266 "menus.bar.view" => {
2267 self.menu_bar
2268 .open(&menu_bar_menus(self.menu_autosave, self.menu_grid), 2);
2269 return;
2270 }
2271 "date.previous" => {
2272 self.date.show_previous_month();
2273 self.date_range.show_previous_month();
2274 }
2275 "date.next" => {
2276 self.date.show_next_month();
2277 self.date_range.show_next_month();
2278 }
2279 "date.week.sunday" => {
2280 self.date.first_weekday = ext_widgets::Weekday::Sunday;
2281 self.date_range.first_weekday = ext_widgets::Weekday::Sunday;
2282 self.refresh_date_week_range();
2283 return;
2284 }
2285 "date.week.monday" => {
2286 self.date.first_weekday = ext_widgets::Weekday::Monday;
2287 self.date_range.first_weekday = ext_widgets::Weekday::Monday;
2288 self.refresh_date_week_range();
2289 return;
2290 }
2291 "date.mode.single" => {
2292 self.date_mode = DateDemoMode::Single;
2293 return;
2294 }
2295 "date.mode.range" => {
2296 self.date_mode = DateDemoMode::Range;
2297 self.date_range.mode = ext_widgets::DateRangeSelectionMode::Custom;
2298 return;
2299 }
2300 "date.mode.week" => {
2301 self.date_mode = DateDemoMode::Week;
2302 self.date_range.mode = ext_widgets::DateRangeSelectionMode::Week;
2303 self.refresh_date_week_range();
2304 return;
2305 }
2306 "date.clear" => {
2307 self.date.selected = None;
2308 self.date_range.clear();
2309 return;
2310 }
2311 "date.bounds.toggle" | "date.range.toggle" => {
2312 if self.date.min.is_some() || self.date.max.is_some() {
2313 self.date.min = None;
2314 self.date.max = None;
2315 self.date_range.min = None;
2316 self.date_range.max = None;
2317 } else {
2318 self.date.min = CalendarDate::new(2026, 5, 4);
2319 self.date.max = CalendarDate::new(2026, 5, 29);
2320 self.date_range.min = CalendarDate::new(2026, 5, 4);
2321 self.date_range.max = CalendarDate::new(2026, 5, 29);
2322 }
2323 return;
2324 }
2325 "toast.show" => {
2326 self.toast_visible = true;
2327 return;
2328 }
2329 "toast.hide" => {
2330 self.toast_visible = false;
2331 return;
2332 }
2333 id if id.starts_with("toast.dismiss.") => {
2334 self.toast_visible = false;
2335 return;
2336 }
2337 "toast.action.1.undo" => {
2338 self.toast_action_status = "Undo requested";
2339 return;
2340 }
2341 "layout.tab.preview" => {
2342 self.layout_tab = 0;
2343 return;
2344 }
2345 "layout.tab.settings" => {
2346 self.layout_tab = 1;
2347 return;
2348 }
2349 "forms.profile.submit" => {
2350 self.form.submit();
2351 self.form.apply();
2352 self.form.submitted = true;
2353 self.form_status =
2354 "Submitted profile; changes are saved and the submission flag is set."
2355 .to_string();
2356 return;
2357 }
2358 "forms.profile.apply" => {
2359 self.form.apply();
2360 self.form.submitted = false;
2361 self.form_status =
2362 "Applied changes; draft is saved but the profile is not submitted.".to_string();
2363 return;
2364 }
2365 "forms.profile.cancel" => {
2366 self.form.cancel();
2367 self.sync_profile_form_text_fields();
2368 self.form_status = "Cancelled".to_string();
2369 return;
2370 }
2371 "forms.profile.reset" => {
2372 self.form = profile_form_state();
2373 self.form_newsletter = true;
2374 self.sync_profile_form_text_fields();
2375 self.form_status = "Reset".to_string();
2376 return;
2377 }
2378 "forms.profile.newsletter.toggle" => {
2379 self.form_newsletter = !self.form_newsletter;
2380 let _ = self.form.update_field(
2381 "newsletter",
2382 if self.form_newsletter {
2383 "true"
2384 } else {
2385 "false"
2386 },
2387 );
2388 self.validate_profile_form();
2389 self.form_status = "Editing profile".to_string();
2390 return;
2391 }
2392 "overlays.collapsing.toggle" => {
2393 self.overlay_expanded = !self.overlay_expanded;
2394 return;
2395 }
2396 "overlays.popup.toggle" => {
2397 self.overlay_popup_open = !self.overlay_popup_open;
2398 return;
2399 }
2400 "overlays.popup.close" => {
2401 self.overlay_popup_open = false;
2402 return;
2403 }
2404 "overlays.modal.open" => {
2405 self.overlay_modal_open = true;
2406 return;
2407 }
2408 "overlays.modal.close" => {
2409 self.overlay_modal_open = false;
2410 return;
2411 }
2412 "drag_drop.text_source" => {
2413 self.apply_drag_drop_source_action(
2414 DragDropDemoPayload::Text,
2415 "Text drag started",
2416 "Text dragging",
2417 "Text drag finished",
2418 "Text drag canceled",
2419 &kind,
2420 );
2421 return;
2422 }
2423 "drag_drop.file_source" => {
2424 self.apply_drag_drop_source_action(
2425 DragDropDemoPayload::File,
2426 "File drag started",
2427 "File dragging",
2428 "File drag finished",
2429 "File drag canceled",
2430 &kind,
2431 );
2432 return;
2433 }
2434 "drag_drop.bytes_source" => {
2435 self.apply_drag_drop_source_action(
2436 DragDropDemoPayload::ImageBytes,
2437 "Image byte drag started",
2438 "Image bytes dragging",
2439 "Image byte drag finished",
2440 "Image byte drag canceled",
2441 &kind,
2442 );
2443 return;
2444 }
2445 "drag_drop.accept_text" => {
2446 self.apply_drag_drop_target_action(DragDropDemoTarget::Text, &kind);
2447 return;
2448 }
2449 "drag_drop.files_only" => {
2450 self.apply_drag_drop_target_action(DragDropDemoTarget::FilesOnly, &kind);
2451 return;
2452 }
2453 "drag_drop.image_bytes" => {
2454 self.apply_drag_drop_target_action(DragDropDemoTarget::ImageBytes, &kind);
2455 return;
2456 }
2457 "drag_drop.disabled" => {
2458 self.apply_drag_drop_target_action(DragDropDemoTarget::Disabled, &kind);
2459 return;
2460 }
2461 "slider.trailing" => {
2462 self.slider_trailing_color = !self.slider_trailing_color;
2463 return;
2464 }
2465 "slider.trailing_color_button" => {
2466 self.slider_trailing_picker_open = !self.slider_trailing_picker_open;
2467 return;
2468 }
2469 "slider.thumb_color_button" => {
2470 self.slider_thumb_picker_open = !self.slider_thumb_picker_open;
2471 return;
2472 }
2473 "slider.thumb.circle" => {
2474 self.slider_thumb_shape = SliderThumbChoice::Circle;
2475 return;
2476 }
2477 "slider.thumb.square" => {
2478 self.slider_thumb_shape = SliderThumbChoice::Square;
2479 return;
2480 }
2481 "slider.thumb.rectangle" => {
2482 self.slider_thumb_shape = SliderThumbChoice::Rectangle;
2483 return;
2484 }
2485 "slider.steps" => {
2486 self.slider_use_steps = !self.slider_use_steps;
2487 if self.slider_use_steps {
2488 self.set_slider_value(widgets::slider::round_slider_to_step(
2489 self.slider,
2490 self.slider_step(),
2491 ));
2492 }
2493 return;
2494 }
2495 "slider.logarithmic" => {
2496 self.slider_logarithmic = !self.slider_logarithmic;
2497 return;
2498 }
2499 "slider.clamping.never" => {
2500 self.slider_clamping = widgets::SliderClamping::Never;
2501 return;
2502 }
2503 "slider.clamping.edits" => {
2504 self.slider_clamping = widgets::SliderClamping::Edits;
2505 return;
2506 }
2507 "slider.clamping.always" => {
2508 self.slider_clamping = widgets::SliderClamping::Always;
2509 self.clamp_slider_to_range();
2510 return;
2511 }
2512 "slider.smart_aim" => {
2513 self.slider_smart_aim = !self.slider_smart_aim;
2514 return;
2515 }
2516 "animation.open" => {
2517 self.animation_open = !self.animation_open;
2518 return;
2519 }
2520 "animation.timed.toggle" => {
2521 self.animation_timed_expanded = !self.animation_timed_expanded;
2522 return;
2523 }
2524 "easing.in.dropdown.toggle" => {
2525 self.easing_in.toggle(&easing_options(EaseDirection::In));
2526 return;
2527 }
2528 "easing.out.dropdown.toggle" => {
2529 self.easing_out.toggle(&easing_options(EaseDirection::Out));
2530 return;
2531 }
2532 "animation.scrub.toggle" => {
2533 self.animation_scrub_expanded = !self.animation_scrub_expanded;
2534 return;
2535 }
2536 "animation.state.toggle" => {
2537 self.animation_state_expanded = !self.animation_state_expanded;
2538 return;
2539 }
2540 "animation.interaction.toggle" => {
2541 self.animation_interaction_expanded = !self.animation_interaction_expanded;
2542 return;
2543 }
2544 "shader_lab.target.toggle" => {
2545 self.shader_lab_target_menu
2546 .toggle(&shader_lab_target_options());
2547 return;
2548 }
2549 "shader_lab.preset.toggle" => {
2550 self.shader_lab_preset_menu
2551 .toggle(&shader_lab_preset_options());
2552 return;
2553 }
2554 "shader_lab.material.shader.toggle" => {
2555 self.shader_lab_material_shader_menu
2556 .toggle(&shader_lab_material_shader_options());
2557 return;
2558 }
2559 "shader_lab.material.shape.toggle" => {
2560 self.shader_lab_material_shape_menu
2561 .toggle(&shader_lab_material_shape_options());
2562 return;
2563 }
2564 "shader_lab.material.geometry.toggle" => {
2565 self.shader_lab_material_geometry_menu
2566 .toggle(&shader_lab_material_geometry_options());
2567 return;
2568 }
2569 "shader_lab.target.canvas" => {
2570 self.set_shader_lab_target(ShaderLabTarget::Canvas);
2571 return;
2572 }
2573 "shader_lab.target.frame" => {
2574 self.set_shader_lab_target(ShaderLabTarget::Frame);
2575 return;
2576 }
2577 "shader_lab.target.button" => {
2578 self.set_shader_lab_target(ShaderLabTarget::Button);
2579 return;
2580 }
2581 "shader_lab.preset.plasma" => {
2582 self.set_shader_lab_preset(ShaderLabPreset::Plasma);
2583 return;
2584 }
2585 "shader_lab.preset.rings" => {
2586 self.set_shader_lab_preset(ShaderLabPreset::Rings);
2587 return;
2588 }
2589 "shader_lab.preset.grid" => {
2590 self.set_shader_lab_preset(ShaderLabPreset::Grid);
2591 return;
2592 }
2593 "shader_lab.preset.vertex_warp" => {
2594 self.set_shader_lab_preset(ShaderLabPreset::VertexWarp);
2595 return;
2596 }
2597 "shader_lab.frame_text.toggle" => {
2598 self.shader_lab_show_frame_text = !self.shader_lab_show_frame_text;
2599 return;
2600 }
2601 "shader_lab.button_text.toggle" => {
2602 self.shader_lab_show_button_text = !self.shader_lab_show_button_text;
2603 return;
2604 }
2605 "shader_lab.surface.stroke" => {
2606 if let WidgetActionKind::PointerEdit(edit) = kind {
2607 self.shader_lab_surface_stroke_width = scaled_slider(
2608 edit.target_rect,
2609 edit.position,
2610 0.0,
2611 SHADER_LAB_SURFACE_STROKE_MAX,
2612 );
2613 }
2614 return;
2615 }
2616 "shader_lab.material.outset" => {
2617 if let WidgetActionKind::PointerEdit(edit) = kind {
2618 self.shader_lab_material_outset = scaled_slider(
2619 edit.target_rect,
2620 edit.position,
2621 0.0,
2622 SHADER_LAB_MATERIAL_OUTSET_MAX,
2623 );
2624 }
2625 return;
2626 }
2627 "shader_lab.surface.radius" => {
2628 if let WidgetActionKind::PointerEdit(edit) = kind {
2629 self.shader_lab_surface_radius = scaled_slider(
2630 edit.target_rect,
2631 edit.position,
2632 0.0,
2633 SHADER_LAB_SURFACE_RADIUS_MAX,
2634 );
2635 }
2636 return;
2637 }
2638 "animation.scrub" => {
2639 if let WidgetActionKind::PointerEdit(edit) = kind {
2640 self.animation_scrub = scaled_slider(edit.target_rect, edit.position, 0.0, 1.0);
2641 }
2642 return;
2643 }
2644 "diagnostics.animation.controls.transport.pause_toggle" => {
2645 self.diagnostics_animation_paused = !self.diagnostics_animation_paused;
2646 return;
2647 }
2648 "diagnostics.animation.controls.transport.step" => {
2649 self.diagnostics_animation_paused = true;
2650 self.diagnostics_animation_scrub =
2651 (self.diagnostics_animation_scrub + 1.0 / 12.0).min(1.0);
2652 return;
2653 }
2654 "diagnostics.animation.controls.transport.scrub" => {
2655 if let WidgetActionKind::PointerEdit(edit) = kind {
2656 self.diagnostics_animation_scrub =
2657 scaled_slider(edit.target_rect, edit.position, 0.0, 1.0);
2658 }
2659 return;
2660 }
2661 "diagnostics.animation.controls.input.active.toggle" => {
2662 self.diagnostics_animation_active = !self.diagnostics_animation_active;
2663 self.refresh_diagnostics_snapshot();
2664 return;
2665 }
2666 "diagnostics.animation.controls.input.hover.set" => {
2667 if let WidgetActionKind::PointerEdit(edit) = kind {
2668 self.diagnostics_animation_hover =
2669 scaled_slider(edit.target_rect, edit.position, 0.0, 1.0);
2670 self.refresh_diagnostics_snapshot();
2671 }
2672 return;
2673 }
2674 "diagnostics.animation.controls.input.pulse.fire" => {
2675 self.diagnostics_animation_pulse_count =
2676 self.diagnostics_animation_pulse_count.saturating_add(1);
2677 return;
2678 }
2679 "layout_widgets.float_panel_a" => {
2680 let panel = ext_widgets::DockPanelDescriptor::new(
2681 "panel_a",
2682 "Panel A",
2683 ext_widgets::DockSide::Left,
2684 200.0,
2685 );
2686 self.layout_dock
2687 .float_panel(&panel, UiRect::new(20.0, 58.0, 236.0, 210.0));
2688 return;
2689 }
2690 "layout_widgets.dock_panel_a" => {
2691 let panel = ext_widgets::DockPanelDescriptor::new(
2692 "panel_a",
2693 "Panel A",
2694 ext_widgets::DockSide::Left,
2695 200.0,
2696 );
2697 self.layout_dock
2698 .dock_panel(&panel, ext_widgets::DockSide::Left);
2699 return;
2700 }
2701 "layout_widgets.drawer.panel_a" => {
2702 self.layout_dock.toggle_panel_hidden("panel_a");
2703 return;
2704 }
2705 "layout_widgets.drawer.panel_b" => {
2706 self.layout_dock.toggle_panel_hidden("panel_b");
2707 return;
2708 }
2709 "layout_widgets.reorder.panel_b.before.panel_a" => {
2710 let mut panels = base_layout_dock_panels();
2711 self.layout_dock.apply_order_to_panels(&mut panels);
2712 let payload = ext_widgets::dock_workspace::dock_panel_drag_payload("panel_b");
2713 self.layout_dock.apply_reorder_to_panels(
2714 &mut panels,
2715 &payload,
2716 "panel_a",
2717 ext_widgets::DockPanelReorderPlacement::Before,
2718 );
2719 return;
2720 }
2721 "layout_widgets.reorder.panel_b.after.panel_a" => {
2722 let mut panels = base_layout_dock_panels();
2723 self.layout_dock.apply_order_to_panels(&mut panels);
2724 let payload = ext_widgets::dock_workspace::dock_panel_drag_payload("panel_b");
2725 self.layout_dock.apply_reorder_to_panels(
2726 &mut panels,
2727 &payload,
2728 "panel_a",
2729 ext_widgets::DockPanelReorderPlacement::After,
2730 );
2731 return;
2732 }
2733 "styling.stroke_color_button" => {
2734 self.styling_stroke_picker_open = !self.styling_stroke_picker_open;
2735 return;
2736 }
2737 "styling.fill_color_button" => {
2738 self.styling_fill_picker_open = !self.styling_fill_picker_open;
2739 return;
2740 }
2741 "styling.shadow_color_button" => {
2742 self.styling_shadow_picker_open = !self.styling_shadow_picker_open;
2743 return;
2744 }
2745 "styling.inner_same" => {
2746 self.styling.inner_same = !self.styling.inner_same;
2747 return;
2748 }
2749 "styling.outer_same" => {
2750 self.styling.outer_same = !self.styling.outer_same;
2751 return;
2752 }
2753 "styling.radius_same" => {
2754 self.styling.radius_same = !self.styling.radius_same;
2755 return;
2756 }
2757 "canvas.grow_horizontal" => {
2758 self.canvas_grow_horizontal = !self.canvas_grow_horizontal;
2759 return;
2760 }
2761 "canvas.grow_vertical" => {
2762 self.canvas_grow_vertical = !self.canvas_grow_vertical;
2763 return;
2764 }
2765 "canvas.keep_aspect_ratio" => {
2766 self.canvas_keep_aspect_ratio = !self.canvas_keep_aspect_ratio;
2767 return;
2768 }
2769 _ => {}
2770 }
2771
2772 if action_id == "canvas.rotate" {
2773 if let WidgetActionKind::Drag(drag) = kind {
2774 self.cube.apply_drag(drag);
2775 }
2776 return;
2777 }
2778 if let WidgetActionKind::Scroll(scroll) = &kind {
2779 match action_id {
2780 "lists_tables.scroll_area.scroll" => self.list_scroll = scroll.offset().y,
2781 "lists_tables.virtual_list.scroll" => self.virtual_scroll = scroll.offset().y,
2782 "lists_tables.data_table.scroll" => self.table_scroll = scroll.offset().y,
2783 "lists_tables.virtualized_table.scroll" => {
2784 self.virtual_table_scroll = scroll.offset().y
2785 }
2786 "layout.panel_a.scroll" => self.layout_panel_a_scroll = scroll.offset().y,
2787 "layout.panel_b.scroll" => self.layout_panel_b_scroll = scroll.offset().y,
2788 "layout.workspace.scroll" => self.layout_workspace_scroll = scroll.offset().y,
2789 "trees.virtual.scroll" => self.tree_virtual_scroll = scroll.offset().y,
2790 "trees.table.scroll" => self.tree_table_scroll = scroll.offset().y,
2791 "containers.scroll_area_with_bars.scroll" => {
2792 self.containers_scroll.set_offset(scroll.offset());
2793 }
2794 "progress.logged.logs.scroll" => {
2795 self.progress_logs_scroll = *scroll;
2796 self.progress_logs_scroll.set_offset(scroll.offset());
2797 self.progress_logs_follow_tail =
2798 scroll.offset().y >= scroll.max_offset().y - 0.5;
2799 }
2800 "timeline.scroll" => {
2801 self.timeline_scroll = *scroll;
2802 self.timeline_scroll.set_offset(scroll.offset());
2803 }
2804 "shader_lab.editor.scroll" => self.shader_lab_editor_scroll = scroll.offset(),
2805 "controls.widget_list.scroll" => {
2806 self.controls_scroll = *scroll;
2807 self.controls_scroll.set_offset(scroll.offset());
2808 }
2809 _ => {}
2810 }
2811 return;
2812 }
2813
2814 if let Some(date) = action_id
2815 .strip_prefix("date.day.")
2816 .and_then(parse_calendar_date)
2817 {
2818 match self.date_mode {
2819 DateDemoMode::Single => {
2820 self.date.select(date);
2821 self.date_range.show_month(date.month());
2822 }
2823 DateDemoMode::Range | DateDemoMode::Week => {
2824 self.date_range.select(date);
2825 self.date.show_month(date.month());
2826 }
2827 }
2828 return;
2829 }
2830
2831 if let Some(option_id) = action_id.strip_prefix("labels.locale.option.") {
2832 self.label_locale
2833 .select_id_and_close(&label_locale_options(), option_id);
2834 return;
2835 }
2836 if let Some(option_id) = action_id.strip_prefix("selection.dropdown.option.") {
2837 self.dropdown
2838 .select_id_and_close(&select_options(), option_id);
2839 return;
2840 }
2841 if let Some(option_id) = action_id.strip_prefix("selection.menu.option.") {
2842 self.select_menu.select_id(&select_options(), option_id);
2843 return;
2844 }
2845 if let Some(option_id) = action_id.strip_prefix("selection.image_menu.option.") {
2846 self.image_select_menu
2847 .select_id(&select_options_with_images(), option_id);
2848 return;
2849 }
2850 if let Some(option_id) = action_id.strip_prefix("numeric.unit.option.") {
2851 self.numeric_unit
2852 .select_id_and_close(&numeric_unit_options(), option_id);
2853 self.reset_numeric_range_for_unit();
2854 self.set_numeric_value(self.numeric_value, true);
2855 return;
2856 }
2857 if let Some(option_id) = action_id.strip_prefix("easing.in.dropdown.option.") {
2858 self.easing_in
2859 .select_id_and_close(&easing_options(EaseDirection::In), option_id);
2860 return;
2861 }
2862 if let Some(option_id) = action_id.strip_prefix("easing.out.dropdown.option.") {
2863 self.easing_out
2864 .select_id_and_close(&easing_options(EaseDirection::Out), option_id);
2865 return;
2866 }
2867 if let Some(option_id) = action_id.strip_prefix("shader_lab.target.option.") {
2868 if let Some(target) = ShaderLabTarget::from_id(option_id) {
2869 self.shader_lab_target_menu
2870 .select_id_and_close(&shader_lab_target_options(), option_id);
2871 self.shader_lab_target = target;
2872 }
2873 return;
2874 }
2875 if let Some(option_id) = action_id.strip_prefix("shader_lab.preset.option.") {
2876 if let Some(preset) = ShaderLabPreset::from_id(option_id) {
2877 self.shader_lab_preset_menu
2878 .select_id_and_close(&shader_lab_preset_options(), option_id);
2879 self.shader_lab_preset = preset;
2880 self.shader_lab_source.set_text(preset.source());
2881 self.refresh_shader_lab_validation();
2882 }
2883 return;
2884 }
2885 if let Some(option_id) = action_id.strip_prefix("shader_lab.material.shader.option.") {
2886 if let Some(shader) = ShaderLabMaterialShader::from_id(option_id) {
2887 self.shader_lab_material_shader_menu
2888 .select_id_and_close(&shader_lab_material_shader_options(), option_id);
2889 self.shader_lab_material_shader = shader;
2890 }
2891 return;
2892 }
2893 if let Some(option_id) = action_id.strip_prefix("shader_lab.material.shape.option.") {
2894 if let Some(shape) = ShaderLabMaterialShape::from_id(option_id) {
2895 self.shader_lab_material_shape_menu
2896 .select_id_and_close(&shader_lab_material_shape_options(), option_id);
2897 self.shader_lab_material_shape = shape;
2898 }
2899 return;
2900 }
2901 if let Some(option_id) = action_id.strip_prefix("shader_lab.material.geometry.option.") {
2902 if let Some(geometry) = ShaderLabMaterialGeometry::from_id(option_id) {
2903 self.shader_lab_material_geometry_menu
2904 .select_id_and_close(&shader_lab_material_geometry_options(), option_id);
2905 self.shader_lab_material_geometry = geometry;
2906 }
2907 return;
2908 }
2909 if let Some(menu_id) = action_id.strip_prefix("menus.item.") {
2910 self.apply_menu_item(menu_id);
2911 return;
2912 }
2913 if let Some(menu_id) = action_id.strip_prefix("menus.context.") {
2914 self.apply_menu_item(menu_id);
2915 self.context_menu.close();
2916 return;
2917 }
2918 if action_id == "color.button.open" {
2919 self.color_picker_button_open = !self.color_picker_button_open;
2920 return;
2921 }
2922 if let Some(row) = action_id
2923 .strip_prefix("lists_tables.data_table.row.")
2924 .and_then(|row| row.parse::<usize>().ok())
2925 {
2926 self.table_selection = ext_widgets::DataTableSelection::single_row(row)
2927 .with_active_cell(ext_widgets::DataTableCellIndex::new(row, 0));
2928 return;
2929 }
2930 if let Some(cell) = action_id
2931 .strip_prefix("lists_tables.data_table.cell.")
2932 .and_then(parse_table_cell)
2933 {
2934 self.table_selection =
2935 ext_widgets::DataTableSelection::single_row(cell.row).with_active_cell(cell);
2936 return;
2937 }
2938 match action_id {
2939 "lists_tables.virtualized_table.sort.name" => {
2940 self.virtual_table_descending = !self.virtual_table_descending;
2941 return;
2942 }
2943 "lists_tables.virtualized_table.filter.status" => {
2944 self.virtual_table_ready_only = !self.virtual_table_ready_only;
2945 self.virtual_table_scroll = 0.0;
2946 return;
2947 }
2948 "lists_tables.virtualized_table.resize.reset" => {
2949 self.virtual_table_value_width = 120.0;
2950 self.virtual_table_resize = None;
2951 return;
2952 }
2953 _ => {}
2954 }
2955 if let Some(row) = action_id
2956 .strip_prefix("lists_tables.virtualized_table.row.")
2957 .and_then(|row| row.parse::<usize>().ok())
2958 {
2959 self.table_selection = ext_widgets::DataTableSelection::single_row(row)
2960 .with_active_cell(ext_widgets::DataTableCellIndex::new(row, 0));
2961 return;
2962 }
2963 if let Some(cell) = action_id
2964 .strip_prefix("lists_tables.virtualized_table.cell.")
2965 .and_then(parse_table_cell)
2966 {
2967 self.table_selection =
2968 ext_widgets::DataTableSelection::single_row(cell.row).with_active_cell(cell);
2969 return;
2970 }
2971 if let Some(rest) = action_id.strip_prefix("trees.tree.action.") {
2972 if let Some((id, action)) = rest.rsplit_once('.') {
2973 self.apply_editable_tree_action(id, action);
2974 }
2975 return;
2976 }
2977 if let Some(id) = action_id.strip_prefix("trees.tree.row.") {
2978 self.apply_tree_row(id, false);
2979 return;
2980 }
2981 if let Some(id) = action_id.strip_prefix("trees.outliner.row.") {
2982 self.apply_tree_row(id, true);
2983 return;
2984 }
2985 if let Some(id) = action_id.strip_prefix("trees.virtual.row.") {
2986 self.apply_virtual_tree_row(id);
2987 return;
2988 }
2989 if let Some(row) = action_id
2990 .strip_prefix("trees.table.row.")
2991 .and_then(|row| row.parse::<usize>().ok())
2992 {
2993 self.apply_tree_table_row(row);
2994 return;
2995 }
2996 if let Some(cell) = action_id
2997 .strip_prefix("trees.table.cell.")
2998 .and_then(parse_table_cell)
2999 {
3000 self.apply_tree_table_row(cell.row);
3001 return;
3002 }
3003
3004 let WidgetActionKind::PointerEdit(edit) = kind else {
3005 return;
3006 };
3007 match action_id {
3008 "numeric.value.drag" => {
3009 self.apply_numeric_drag(edit);
3010 }
3011 "numeric.range_min" => {
3012 let domain = self.numeric_unit_domain();
3013 let min = domain.min as f32;
3014 let max = domain.max as f32;
3015 let span = self.numeric_minimum_span();
3016 self.set_numeric_range_min(scaled_slider(
3017 edit.target_rect,
3018 edit.position,
3019 min,
3020 (max - span).max(min),
3021 ));
3022 }
3023 "numeric.range_max" => {
3024 let domain = self.numeric_unit_domain();
3025 let min = domain.min as f32;
3026 let max = domain.max as f32;
3027 let span = self.numeric_minimum_span();
3028 self.set_numeric_range_max(scaled_slider(
3029 edit.target_rect,
3030 edit.position,
3031 (min + span).min(max),
3032 max,
3033 ));
3034 }
3035 "numeric.sensitivity" => {
3036 self.numeric_sensitivity =
3037 scaled_slider(edit.target_rect, edit.position, 0.25, 4.0);
3038 }
3039 "layout_widgets.split_pane.handle" => {
3040 let total_extent = self
3041 .desktop
3042 .size("layout_widgets", default_window_size("layout_widgets"))
3043 .width
3044 - 48.0;
3045 let total_extent = total_extent.max(1.0);
3046 let handle_center = edit.target_rect.x + edit.target_rect.width * 0.5;
3047 self.layout_split
3048 .resize_by(edit.position.x - handle_center, total_extent, 6.0);
3049 }
3050 "shader_lab.workspace.resize" => {
3051 resize_split_from_pointer(
3052 &mut self.shader_lab_split,
3053 ext_widgets::SplitAxis::Horizontal,
3054 edit,
3055 SHADER_LAB_SPLIT_HANDLE_THICKNESS,
3056 );
3057 }
3058 "panels.resize.top" => {
3059 resize_split_from_pointer(
3060 &mut self.panels_top_split,
3061 ext_widgets::SplitAxis::Vertical,
3062 edit,
3063 PANELS_SPLIT_HANDLE_THICKNESS,
3064 );
3065 }
3066 "panels.resize.bottom" => {
3067 resize_split_from_pointer(
3068 &mut self.panels_bottom_split,
3069 ext_widgets::SplitAxis::Vertical,
3070 edit,
3071 PANELS_SPLIT_HANDLE_THICKNESS,
3072 );
3073 }
3074 "panels.resize.left" => {
3075 resize_split_from_pointer(
3076 &mut self.panels_left_split,
3077 ext_widgets::SplitAxis::Horizontal,
3078 edit,
3079 PANELS_SPLIT_HANDLE_THICKNESS,
3080 );
3081 }
3082 "panels.resize.right" => {
3083 resize_split_from_pointer(
3084 &mut self.panels_right_split,
3085 ext_widgets::SplitAxis::Horizontal,
3086 edit,
3087 PANELS_SPLIT_HANDLE_THICKNESS,
3088 );
3089 }
3090 "slider.value" => {
3091 self.set_slider_value(
3092 self.slider_value_spec()
3093 .value_from_control_point(edit.target_rect, edit.position),
3094 );
3095 }
3096 "slider.range_left" => {
3097 let value = widgets::slider::SliderValueSpec::new(0.0, self.slider_right.max(1.0))
3098 .value_from_control_point(edit.target_rect, edit.position);
3099 self.set_slider_left(value.min(self.slider_right - 1.0));
3100 }
3101 "slider.range_right" => {
3102 let value = widgets::slider::SliderValueSpec::new(self.slider_left + 1.0, 10000.0)
3103 .value_from_control_point(edit.target_rect, edit.position);
3104 self.set_slider_right(value.max(self.slider_left + 1.0));
3105 }
3106 "lists_tables.virtualized_table.resize.value" => match edit.phase.edit_phase() {
3107 EditPhase::Preview => {}
3108 EditPhase::BeginEdit => {
3109 self.virtual_table_resize =
3110 Some((self.virtual_table_value_width, edit.position.x));
3111 }
3112 EditPhase::UpdateEdit | EditPhase::CommitEdit => {
3113 let (origin_width, origin_x) = self
3114 .virtual_table_resize
3115 .unwrap_or((self.virtual_table_value_width, edit.position.x));
3116 self.virtual_table_value_width =
3117 (origin_width + edit.position.x - origin_x).clamp(56.0, 180.0);
3118 if edit.phase.edit_phase() == EditPhase::CommitEdit {
3119 self.virtual_table_resize = None;
3120 }
3121 }
3122 EditPhase::CancelEdit => {
3123 if let Some((origin_width, _)) = self.virtual_table_resize.take() {
3124 self.virtual_table_value_width = origin_width;
3125 }
3126 }
3127 },
3128 "containers.scroll_area_with_bars.vertical-scrollbar" => {
3129 let offset = self.scrollbars.apply_drag_for_target_rect(
3130 "containers.vertical",
3131 self.containers_scroll,
3132 scrollbar_widgets::ScrollAxis::Vertical,
3133 edit,
3134 );
3135 self.containers_scroll.set_offset(offset);
3136 }
3137 "containers.scroll_area_with_bars.horizontal-scrollbar" => {
3138 let offset = self.scrollbars.apply_drag_for_target_rect(
3139 "containers.horizontal",
3140 self.containers_scroll,
3141 scrollbar_widgets::ScrollAxis::Horizontal,
3142 edit,
3143 );
3144 self.containers_scroll.set_offset(offset);
3145 }
3146 "timeline.horizontal-scrollbar" => {
3147 let mut scroll =
3148 timeline_scroll_state_for_view(self.timeline_scroll, edit.target_rect.width);
3149 let offset = self.scrollbars.apply_drag_for_target_rect(
3150 "timeline",
3151 scroll,
3152 scrollbar_widgets::ScrollAxis::Horizontal,
3153 edit,
3154 );
3155 scroll.set_offset(offset);
3156 self.timeline_scroll = scroll;
3157 }
3158 "controls.widget_list.scrollbar" => {
3159 let mut scroll =
3160 controls_scroll_state_for_view(self.controls_scroll, edit.target_rect.height);
3161 let offset = self.scrollbars.apply_drag_for_target_rect(
3162 "controls.widget_list",
3163 scroll,
3164 scrollbar_widgets::ScrollAxis::Vertical,
3165 edit,
3166 );
3167 scroll.set_offset(offset);
3168 self.controls_scroll = scroll;
3169 }
3170 "styling.inner" => {
3171 self.styling.inner_margin =
3172 scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
3173 if self.styling.inner_same {
3174 self.styling.inner_right = self.styling.inner_margin;
3175 self.styling.inner_top = self.styling.inner_margin;
3176 self.styling.inner_bottom = self.styling.inner_margin;
3177 }
3178 }
3179 "styling.inner_right" => {
3180 self.styling.inner_right =
3181 scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
3182 }
3183 "styling.inner_top" => {
3184 self.styling.inner_top = scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
3185 }
3186 "styling.inner_bottom" => {
3187 self.styling.inner_bottom =
3188 scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
3189 }
3190 "styling.outer" => {
3191 self.styling.outer_margin =
3192 scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
3193 if self.styling.outer_same {
3194 self.styling.outer_right = self.styling.outer_margin;
3195 self.styling.outer_top = self.styling.outer_margin;
3196 self.styling.outer_bottom = self.styling.outer_margin;
3197 }
3198 }
3199 "styling.outer_right" => {
3200 self.styling.outer_right =
3201 scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
3202 }
3203 "styling.outer_top" => {
3204 self.styling.outer_top = scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
3205 }
3206 "styling.outer_bottom" => {
3207 self.styling.outer_bottom =
3208 scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
3209 }
3210 "styling.radius" => {
3211 self.styling.corner_radius =
3212 scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
3213 if self.styling.radius_same {
3214 self.styling.corner_ne = self.styling.corner_radius;
3215 self.styling.corner_sw = self.styling.corner_radius;
3216 self.styling.corner_se = self.styling.corner_radius;
3217 }
3218 }
3219 "styling.radius_ne" => {
3220 self.styling.corner_ne = scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
3221 }
3222 "styling.radius_sw" => {
3223 self.styling.corner_sw = scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
3224 }
3225 "styling.radius_se" => {
3226 self.styling.corner_se = scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
3227 }
3228 "styling.shadow_x" => {
3229 self.styling.shadow_x = scaled_slider(edit.target_rect, edit.position, -24.0, 24.0);
3230 }
3231 "styling.shadow_y" => {
3232 self.styling.shadow_y = scaled_slider(edit.target_rect, edit.position, -24.0, 24.0);
3233 }
3234 "styling.shadow" => {
3235 self.styling.shadow_blur =
3236 scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
3237 }
3238 "styling.shadow_spread" => {
3239 self.styling.shadow_spread =
3240 scaled_slider(edit.target_rect, edit.position, 0.0, 16.0);
3241 }
3242 "styling.stroke" => {
3243 self.styling.stroke_width =
3244 scaled_slider(edit.target_rect, edit.position, 0.0, STYLING_STROKE_MAX);
3245 }
3246 _ => {}
3247 }
3248 }
3249
3250 fn apply_command_palette_event(&mut self, event: operad::UiInputEvent) {
3251 let items = command_palette_items_with_history(&self.command_history);
3252 let outcome = self.command_palette.handle_event(&items, &event);
3253 if let Some(selection) = outcome.selected {
3254 self.select_command_palette_item(&selection.id);
3255 }
3256 }
3257
3258 fn select_command_palette_item(&mut self, id: &str) {
3259 if let Some(item) = command_palette_items_with_history(&self.command_history)
3260 .into_iter()
3261 .find(|item| item.id == id && item.enabled)
3262 {
3263 self.command_history.record(item.id.as_str());
3264 self.last_command = item.title;
3265 let items = command_palette_items_with_history(&self.command_history);
3266 self.command_palette.set_query("", &items);
3267 self.command_palette_open = false;
3268 }
3269 }
3270
3271 fn text_edit_options(&self, input: FocusedTextInput) -> TextInputOptions {
3272 let mut options = TextInputOptions::default();
3273 options.focused = self.focused_text == Some(input);
3274 options.caret_visible = caret_visible(self.caret_phase);
3275 match input {
3276 FocusedTextInput::Editable => {
3277 options.layout = LayoutStyle::new().with_width(300.0).with_height(36.0);
3278 options.text_style = text(13.0, color(230, 236, 246));
3279 options.placeholder_style = text(13.0, color(144, 156, 174));
3280 options.placeholder = "Type here".to_string();
3281 }
3282 FocusedTextInput::Selectable => {
3283 options.layout = LayoutStyle::new().with_width(360.0).with_height(36.0);
3284 options.text_style = text(13.0, color(196, 210, 230));
3285 options.read_only = true;
3286 options.selectable = true;
3287 }
3288 FocusedTextInput::Singleline => {
3289 options.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
3290 options.text_style = text(13.0, color(230, 236, 246));
3291 options.placeholder = "Single line".to_string();
3292 }
3293 FocusedTextInput::Multiline => {
3294 options.layout = LayoutStyle::new().with_width(360.0).with_height(72.0);
3295 options.text_style = text(13.0, color(230, 236, 246));
3296 }
3297 FocusedTextInput::TextArea => {
3298 options.layout = LayoutStyle::new().with_width(360.0).with_height(66.0);
3299 options.text_style = text(13.0, color(230, 236, 246));
3300 }
3301 FocusedTextInput::CodeEditor => {
3302 options.layout = LayoutStyle::new().with_width(360.0).with_height(88.0);
3303 options.text_style = widgets::code_text_style();
3304 }
3305 FocusedTextInput::Search => {
3306 options.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
3307 options.text_style = text(13.0, color(230, 236, 246));
3308 options.placeholder = "Search".to_string();
3309 }
3310 FocusedTextInput::Password => {
3311 options.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
3312 options.text_style = text(13.0, color(230, 236, 246));
3313 options.placeholder = "Password".to_string();
3314 }
3315 FocusedTextInput::FormName
3316 | FocusedTextInput::FormEmail
3317 | FocusedTextInput::FormRole => {
3318 options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
3319 options.text_style = text(12.0, color(230, 236, 246));
3320 options.placeholder_style = text(12.0, color(144, 156, 174));
3321 options.placeholder = "Required".to_string();
3322 }
3323 FocusedTextInput::NumericValue => {
3324 options.layout = LayoutStyle::new()
3325 .with_width(112.0)
3326 .with_height(30.0)
3327 .with_flex_shrink(0.0);
3328 options.text_style = text(12.0, color(230, 236, 246));
3329 options.placeholder_style = text(12.0, color(144, 156, 174));
3330 options.accessibility_label = Some("Numeric value".to_string());
3331 }
3332 FocusedTextInput::NumericRangeMin | FocusedTextInput::NumericRangeMax => {
3333 options.layout = LayoutStyle::new()
3334 .with_width(70.0)
3335 .with_height(28.0)
3336 .with_flex_shrink(0.0);
3337 options.text_style = text(12.0, color(230, 236, 246));
3338 options.placeholder_style = text(12.0, color(144, 156, 174));
3339 options.accessibility_label = Some(
3340 match input {
3341 FocusedTextInput::NumericRangeMin => "Numeric range minimum",
3342 _ => "Numeric range maximum",
3343 }
3344 .to_string(),
3345 );
3346 }
3347 FocusedTextInput::SliderValue | FocusedTextInput::SliderStep => {
3348 options.layout = LayoutStyle::new().with_width(86.0).with_height(28.0);
3349 options.text_style = text(12.0, color(230, 236, 246));
3350 options.placeholder_style = text(12.0, color(144, 156, 174));
3351 }
3352 FocusedTextInput::SliderRangeLeft | FocusedTextInput::SliderRangeRight => {
3353 options.layout = LayoutStyle::new().with_width(96.0).with_height(28.0);
3354 options.text_style = text(12.0, color(230, 236, 246));
3355 options.placeholder_style = text(12.0, color(144, 156, 174));
3356 }
3357 FocusedTextInput::ShaderLabSource => {
3358 let editor_content_size =
3359 shader_lab_editor_content_size(self.shader_lab_source.text());
3360 options.layout = LayoutStyle::new()
3361 .with_width(editor_content_size.width)
3362 .with_height(editor_content_size.height)
3363 .with_flex_shrink(0.0);
3364 options.text_style = widgets::code_text_style();
3365 options.placeholder_style = text(12.0, color(144, 156, 174));
3366 options.placeholder = "WGSL shader source".to_string();
3367 options.accessibility_label = Some("Shader source editor".to_string());
3368 }
3369 }
3370 options
3371 }
3372
3373 fn apply_text_edit(&mut self, input: FocusedTextInput, edit: WidgetTextEdit) {
3374 self.set_focused_text(Some(input));
3375 let options = self.text_edit_options(input);
3376 let outcome = self.text_state_mut(input).map(|state| {
3377 state.set_multiline(input.is_multiline());
3378 state.apply_widget_text_edit(&edit, &options)
3379 });
3380 if let Some(outcome) = outcome {
3381 self.sync_text_input_value(input, outcome.committed, outcome.canceled);
3382 self.apply_text_clipboard_outcome(input, outcome);
3383 if input == FocusedTextInput::ShaderLabSource {
3384 self.refresh_shader_lab_validation();
3385 }
3386 }
3387 }
3388
3389 fn apply_text_focus(&mut self, input: FocusedTextInput, focused: bool) {
3390 if focused {
3391 self.set_focused_text(Some(input));
3392 } else if self.focused_text == Some(input) {
3393 self.sync_text_input_value(input, true, false);
3394 self.set_focused_text(None);
3395 }
3396 }
3397
3398 fn set_focused_text(&mut self, next: Option<FocusedTextInput>) {
3399 if self.focused_text != next {
3400 if let Some(previous) = self.focused_text {
3401 if let Some(state) = self.text_state_mut(previous) {
3402 state.clear_selection();
3403 }
3404 }
3405 }
3406 self.focused_text = next;
3407 }
3408
3409 fn apply_text_clipboard_outcome(
3410 &mut self,
3411 input: FocusedTextInput,
3412 outcome: widgets::text_input::TextInputOutcome,
3413 ) {
3414 match outcome.clipboard {
3415 Some(widgets::text_input::TextInputClipboardAction::Copy(text))
3416 | Some(widgets::text_input::TextInputClipboardAction::Cut(text)) => {
3417 self.copy_text_to_clipboard(&text);
3418 }
3419 Some(widgets::text_input::TextInputClipboardAction::Paste) => {
3420 self.pending_clipboard_paste = Some(input);
3421 self.platform.read_clipboard_text();
3422 }
3423 None => {}
3424 }
3425 }
3426
3427 fn text_state_mut(&mut self, input: FocusedTextInput) -> Option<&mut TextInputState> {
3428 match input {
3429 FocusedTextInput::Editable => Some(&mut self.text),
3430 FocusedTextInput::Selectable => Some(&mut self.selectable_text),
3431 FocusedTextInput::Singleline => Some(&mut self.singleline_text),
3432 FocusedTextInput::Multiline => Some(&mut self.multiline_text),
3433 FocusedTextInput::TextArea => Some(&mut self.text_area_text),
3434 FocusedTextInput::CodeEditor => Some(&mut self.code_editor_text),
3435 FocusedTextInput::Search => Some(&mut self.search_text),
3436 FocusedTextInput::Password => Some(&mut self.password_text),
3437 FocusedTextInput::FormName => Some(&mut self.form_name_text),
3438 FocusedTextInput::FormEmail => Some(&mut self.form_email_text),
3439 FocusedTextInput::FormRole => Some(&mut self.form_role_text),
3440 FocusedTextInput::NumericValue => Some(&mut self.numeric_text),
3441 FocusedTextInput::NumericRangeMin => Some(&mut self.numeric_range_min_text),
3442 FocusedTextInput::NumericRangeMax => Some(&mut self.numeric_range_max_text),
3443 FocusedTextInput::SliderValue => Some(&mut self.slider_value_text),
3444 FocusedTextInput::SliderRangeLeft => Some(&mut self.slider_left_text),
3445 FocusedTextInput::SliderRangeRight => Some(&mut self.slider_right_text),
3446 FocusedTextInput::SliderStep => Some(&mut self.slider_step_text),
3447 FocusedTextInput::ShaderLabSource => Some(&mut self.shader_lab_source),
3448 }
3449 }
3450
3451 fn sync_text_input_value(&mut self, input: FocusedTextInput, committed: bool, canceled: bool) {
3452 match input {
3453 FocusedTextInput::SliderValue => {
3454 if let Ok(value) = self.slider_value_text.text().parse::<f32>() {
3455 self.apply_slider_value_from_text(value);
3456 }
3457 }
3458 FocusedTextInput::SliderRangeLeft => {
3459 if let Ok(value) = self.slider_left_text.text().parse::<f32>() {
3460 self.apply_slider_left_from_text(value);
3461 }
3462 }
3463 FocusedTextInput::SliderRangeRight => {
3464 if let Ok(value) = self.slider_right_text.text().parse::<f32>() {
3465 self.apply_slider_right_from_text(value);
3466 }
3467 }
3468 FocusedTextInput::SliderStep => {
3469 if let Ok(value) = self.slider_step_text.text().parse::<f32>() {
3470 self.slider_step_value = value.abs().max(0.0001);
3471 if self.slider_use_steps {
3472 self.set_slider_value(widgets::slider::round_slider_to_step(
3473 self.slider,
3474 self.slider_step(),
3475 ));
3476 }
3477 }
3478 }
3479 FocusedTextInput::NumericValue => {
3480 self.sync_numeric_value_from_text(committed, canceled);
3481 }
3482 FocusedTextInput::NumericRangeMin => {
3483 self.sync_numeric_range_min_from_text(committed, canceled);
3484 }
3485 FocusedTextInput::NumericRangeMax => {
3486 self.sync_numeric_range_max_from_text(committed, canceled);
3487 }
3488 FocusedTextInput::FormName => {
3489 self.update_profile_form_field("name", self.form_name_text.text().to_string());
3490 }
3491 FocusedTextInput::FormEmail => {
3492 self.update_profile_form_field("email", self.form_email_text.text().to_string());
3493 }
3494 FocusedTextInput::FormRole => {
3495 self.update_profile_form_field("role", self.form_role_text.text().to_string());
3496 }
3497 _ => {}
3498 }
3499 }
3500
3501 fn numeric_precision(&self) -> ext_widgets::NumericPrecision {
3502 match numeric_unit_id(&self.numeric_unit) {
3503 "turn" => ext_widgets::NumericPrecision::decimals(3).with_step(0.001),
3504 _ => ext_widgets::NumericPrecision::decimals(1).with_step(0.1),
3505 }
3506 }
3507
3508 fn numeric_range(&self) -> ext_widgets::NumericRange {
3509 let span = self.numeric_minimum_span();
3510 ext_widgets::NumericRange::new(
3511 f64::from(self.numeric_range_min),
3512 f64::from(self.numeric_range_max.max(self.numeric_range_min + span)),
3513 )
3514 }
3515
3516 fn formatted_numeric_value(&self) -> String {
3517 self.numeric_precision()
3518 .format(f64::from(self.numeric_value))
3519 }
3520
3521 fn numeric_unit_domain(&self) -> ext_widgets::NumericRange {
3522 numeric_unit_default_range(numeric_unit_id(&self.numeric_unit))
3523 }
3524
3525 fn numeric_minimum_span(&self) -> f32 {
3526 self.numeric_precision().step as f32
3527 }
3528
3529 fn format_numeric_range_bound(&self, value: f32) -> String {
3530 self.numeric_precision().format(f64::from(value))
3531 }
3532
3533 fn reset_progress_loading(&mut self) {
3534 self.progress_loading_elapsed = 0.0;
3535 self.progress_logs_follow_tail = true;
3536 self.progress_logs_scroll = progress_log_scroll_state(0.0, 0, true);
3537 }
3538
3539 fn set_shader_lab_target(&mut self, target: ShaderLabTarget) {
3540 self.shader_lab_target = target;
3541 self.shader_lab_target_menu
3542 .select_id(&shader_lab_target_options(), target.id());
3543 }
3544
3545 fn set_shader_lab_preset(&mut self, preset: ShaderLabPreset) {
3546 self.shader_lab_preset = preset;
3547 self.shader_lab_preset_menu
3548 .select_id(&shader_lab_preset_options(), preset.id());
3549 self.shader_lab_source.set_text(preset.source());
3550 self.refresh_shader_lab_validation();
3551 }
3552
3553 fn refresh_shader_lab_validation(&mut self) {
3554 self.shader_lab_source_error = shader_lab_source_error(self.shader_lab_source.text());
3555 }
3556
3557 fn set_numeric_value(&mut self, value: f32, sync_text: bool) {
3558 let range = self.numeric_range();
3559 self.numeric_value = self
3560 .numeric_precision()
3561 .quantize(range.clamp(f64::from(value))) as f32;
3562 if sync_text {
3563 self.sync_numeric_text_to_value();
3564 }
3565 }
3566
3567 fn set_numeric_range_min(&mut self, value: f32) {
3568 let domain = self.numeric_unit_domain();
3569 let min_domain = domain.min as f32;
3570 let max_domain = domain.max as f32;
3571 let span = self.numeric_minimum_span();
3572 let max_allowed =
3573 (self.numeric_range_max - span).clamp(min_domain, (max_domain - span).max(min_domain));
3574 self.numeric_range_min = value.clamp(min_domain, max_allowed);
3575 self.numeric_range_min_text
3576 .set_text(self.format_numeric_range_bound(self.numeric_range_min));
3577 self.set_numeric_value(self.numeric_value, true);
3578 }
3579
3580 fn set_numeric_range_max(&mut self, value: f32) {
3581 let domain = self.numeric_unit_domain();
3582 let min_domain = domain.min as f32;
3583 let max_domain = domain.max as f32;
3584 let span = self.numeric_minimum_span();
3585 let min_allowed = (self.numeric_range_min + span).clamp(min_domain + span, max_domain);
3586 self.numeric_range_max = value.clamp(min_allowed, max_domain);
3587 self.numeric_range_max_text
3588 .set_text(self.format_numeric_range_bound(self.numeric_range_max));
3589 self.set_numeric_value(self.numeric_value, true);
3590 }
3591
3592 fn reset_numeric_range_for_unit(&mut self) {
3593 let range = self.numeric_unit_domain();
3594 self.numeric_range_min = range.min as f32;
3595 self.numeric_range_max = range.max as f32;
3596 self.numeric_range_min_text
3597 .set_text(self.format_numeric_range_bound(self.numeric_range_min));
3598 self.numeric_range_max_text
3599 .set_text(self.format_numeric_range_bound(self.numeric_range_max));
3600 }
3601
3602 fn sync_numeric_text_to_value(&mut self) {
3603 self.numeric_text.set_text(self.formatted_numeric_value());
3604 }
3605
3606 fn sync_numeric_value_from_text(&mut self, committed: bool, canceled: bool) {
3607 if canceled {
3608 self.sync_numeric_text_to_value();
3609 return;
3610 }
3611 if let Some(value) = parse_numeric_edit_text(
3612 self.numeric_text.text(),
3613 &numeric_unit_format(&self.numeric_unit),
3614 ) {
3615 self.set_numeric_value(value, false);
3616 }
3617 if committed {
3618 self.sync_numeric_text_to_value();
3619 }
3620 }
3621
3622 fn sync_numeric_range_min_from_text(&mut self, committed: bool, canceled: bool) {
3623 if canceled {
3624 self.numeric_range_min_text
3625 .set_text(self.format_numeric_range_bound(self.numeric_range_min));
3626 return;
3627 }
3628 if let Ok(value) = self.numeric_range_min_text.text().trim().parse::<f32>() {
3629 let raw = self.numeric_range_min_text.text().to_string();
3630 self.set_numeric_range_min(value);
3631 if !committed {
3632 self.numeric_range_min_text.set_text(raw);
3633 }
3634 }
3635 if committed {
3636 self.numeric_range_min_text
3637 .set_text(self.format_numeric_range_bound(self.numeric_range_min));
3638 }
3639 }
3640
3641 fn sync_numeric_range_max_from_text(&mut self, committed: bool, canceled: bool) {
3642 if canceled {
3643 self.numeric_range_max_text
3644 .set_text(self.format_numeric_range_bound(self.numeric_range_max));
3645 return;
3646 }
3647 if let Ok(value) = self.numeric_range_max_text.text().trim().parse::<f32>() {
3648 let raw = self.numeric_range_max_text.text().to_string();
3649 self.set_numeric_range_max(value);
3650 if !committed {
3651 self.numeric_range_max_text.set_text(raw);
3652 }
3653 }
3654 if committed {
3655 self.numeric_range_max_text
3656 .set_text(self.format_numeric_range_bound(self.numeric_range_max));
3657 }
3658 }
3659
3660 fn apply_numeric_drag(&mut self, edit: WidgetPointerEdit) {
3661 let phase = edit.phase.edit_phase();
3662 if phase == EditPhase::CommitEdit && self.numeric_drag_start.is_none() {
3663 self.set_focused_text(Some(FocusedTextInput::NumericValue));
3664 self.sync_numeric_text_to_value();
3665 return;
3666 }
3667 if phase == EditPhase::BeginEdit {
3668 self.numeric_drag_start = Some((self.numeric_value, edit.position.x));
3669 return;
3670 }
3671 let Some((start_value, start_x)) = self.numeric_drag_start else {
3672 return;
3673 };
3674 let precision = self.numeric_precision();
3675 let range = Some(self.numeric_range());
3676 let sensitivity = self.numeric_sensitivity.clamp(0.25, 4.0);
3677 let drag = ext_widgets::NumericDragSpec {
3678 pixels_per_step: (8.0 / sensitivity).clamp(1.0, 64.0),
3679 ..ext_widgets::NumericDragSpec::default()
3680 };
3681 let delta = edit.position.x - start_x;
3682 if phase == EditPhase::CommitEdit && delta.abs() < 1.0 {
3683 self.set_focused_text(Some(FocusedTextInput::NumericValue));
3684 self.sync_numeric_text_to_value();
3685 self.numeric_drag_start = None;
3686 return;
3687 }
3688 let value = ext_widgets::drag_value(
3689 f64::from(start_value),
3690 delta,
3691 precision,
3692 range,
3693 drag,
3694 ext_widgets::NumericDragSpeed::Normal,
3695 ) as f32;
3696 self.set_numeric_value(value, true);
3697 if matches!(phase, EditPhase::CommitEdit | EditPhase::CancelEdit) {
3698 if phase == EditPhase::CancelEdit {
3699 self.set_numeric_value(start_value, true);
3700 }
3701 self.numeric_drag_start = None;
3702 }
3703 }
3704
3705 fn update_profile_form_field(&mut self, id: &'static str, value: String) {
3706 let _ = self.form.update_field(id, value);
3707 self.validate_profile_form();
3708 self.form_status = "Editing profile".to_string();
3709 }
3710
3711 fn sync_profile_form_text_fields(&mut self) {
3712 self.form_name_text = TextInputState::new(profile_form_value(&self.form, "name"));
3713 self.form_email_text = TextInputState::new(profile_form_value(&self.form, "email"));
3714 self.form_role_text = TextInputState::new(profile_form_value(&self.form, "role"));
3715 }
3716
3717 fn validate_profile_form(&mut self) {
3718 let request = self.form.begin_form_validation();
3719 let values = request.values.clone();
3720 let mut result = FormValidationResult::new(request.generation);
3721 let field_value = |id: &str| {
3722 values
3723 .iter()
3724 .find_map(|(field_id, value)| (field_id.as_str() == id).then_some(value.as_str()))
3725 .unwrap_or_default()
3726 };
3727 let name = field_value("name").trim();
3728 let email = field_value("email").trim();
3729 let role = field_value("role").trim();
3730
3731 if name.is_empty() {
3732 result = result
3733 .with_field_messages("name", vec![ValidationMessage::error("Name is required")]);
3734 }
3735 if !profile_email_valid(email) {
3736 result = result.with_field_messages(
3737 "email",
3738 vec![ValidationMessage::error("Use a complete email address")],
3739 );
3740 }
3741 if role.is_empty() {
3742 result = result.with_field_messages(
3743 "role",
3744 vec![ValidationMessage::warning("Role can be added later")],
3745 );
3746 }
3747 if self.form.dirty {
3748 result =
3749 result.with_form_message(ValidationMessage::warning("Unsaved profile changes"));
3750 }
3751 let _ = self.form.apply_form_validation(result);
3752 }
3753
3754 fn copy_text_to_clipboard(&mut self, text: &str) {
3755 self.clipboard_text = text.to_string();
3756 self.platform.write_clipboard_text(text);
3757 }
3758
3759 fn apply_platform_responses(&mut self, responses: &[PlatformServiceResponse]) {
3760 self.platform.record_responses(responses.iter().cloned());
3761 for response in responses {
3762 match &response.response {
3763 PlatformResponse::Clipboard(ClipboardResponse::Text(text)) => {
3764 let pasted = text
3765 .as_deref()
3766 .filter(|text| !text.is_empty())
3767 .unwrap_or(&self.clipboard_text)
3768 .to_string();
3769 self.apply_pending_clipboard_paste(&pasted);
3770 }
3771 PlatformResponse::Clipboard(ClipboardResponse::Unsupported)
3772 | PlatformResponse::Clipboard(ClipboardResponse::Error(_)) => {
3773 let pasted = self.clipboard_text.clone();
3774 self.apply_pending_clipboard_paste(&pasted);
3775 }
3776 _ => {}
3777 }
3778 }
3779 }
3780
3781 fn apply_pending_clipboard_paste(&mut self, pasted: &str) {
3782 let Some(input) = self.pending_clipboard_paste.take() else {
3783 return;
3784 };
3785 if input.is_read_only() {
3786 return;
3787 }
3788 if let Some(state) = self.text_state_mut(input) {
3789 state.paste_text(pasted);
3790 }
3791 self.sync_text_input_value(input, false, false);
3792 }
3793
3794 fn apply_menu_item(&mut self, id: &str) {
3795 let menus = menu_bar_menus(self.menu_autosave, self.menu_grid);
3796 self.menu_bar.set_active_item_by_id(&menus, id);
3797 if id == "autosave" {
3798 self.menu_autosave = !self.menu_autosave;
3799 } else if id == "grid" {
3800 self.menu_grid = !self.menu_grid;
3801 }
3802 self.menu_button.close();
3803 self.image_text_menu_button.close();
3804 self.image_menu_button.close();
3805 }
3806
3807 fn apply_tree_row(&mut self, id: &str, outliner: bool) {
3808 let roots = if outliner {
3809 tree_items()
3810 } else {
3811 editable_tree_items(&self.editable_tree)
3812 };
3813 let state = if outliner {
3814 &mut self.outliner
3815 } else {
3816 &mut self.tree
3817 };
3818 state.activate_visible_item_id(&roots, id);
3819 }
3820
3821 fn apply_editable_tree_action(&mut self, id: &str, action: &str) {
3822 match action {
3823 "add" => {
3824 let new_id = format!("editable-{}", self.editable_tree_next_id);
3825 self.editable_tree_next_id += 1;
3826 if let Some(parent) = find_editable_tree_node_mut(&mut self.editable_tree, id) {
3827 let label = format!("child #{}", parent.children.len());
3828 parent
3829 .children
3830 .push(EditableTreeNode::new(new_id.clone(), label.clone()));
3831 self.tree.set_expanded(id.to_owned(), true);
3832 self.editable_tree_status = format!("Added {label} under {}", parent.label);
3833 }
3834 }
3835 "delete" => {
3836 if id == "root" {
3837 return;
3838 }
3839 if let Some(label) = remove_editable_tree_node(&mut self.editable_tree, id) {
3840 self.tree.select(None);
3841 self.editable_tree_status = format!("Deleted {label}");
3842 }
3843 }
3844 _ => {}
3845 }
3846 }
3847
3848 fn apply_virtual_tree_row(&mut self, id: &str) {
3849 let roots = virtual_tree_items();
3850 if self
3851 .tree_virtual
3852 .activate_visible_item_id(&roots, id)
3853 .is_some_and(|item| item.has_children())
3854 {
3855 self.tree_virtual_scroll = 0.0;
3856 }
3857 }
3858
3859 fn apply_tree_table_row(&mut self, row: usize) {
3860 let roots = tree_table_items();
3861 let Some(item) = self.tree_table.visible_items(&roots).get(row).cloned() else {
3862 return;
3863 };
3864 if item.disabled {
3865 return;
3866 }
3867 self.tree_table.select(Some(item.index));
3868 if item.has_children() {
3869 self.tree_table.toggle_expanded(item.id);
3870 self.tree_table_scroll = 0.0;
3871 }
3872 }
3873
3874 fn slider_value_spec(&self) -> widgets::slider::SliderValueSpec {
3875 let mut spec = widgets::slider::SliderValueSpec::new(self.slider_left, self.slider_right)
3876 .logarithmic(self.slider_logarithmic)
3877 .clamping(self.slider_clamping)
3878 .smart_aim(self.slider_smart_aim);
3879 if self.slider_use_steps {
3880 spec = spec.step(self.slider_step());
3881 }
3882 spec
3883 }
3884
3885 fn set_slider_value(&mut self, value: f32) {
3886 let value = self.slider_value_spec().adjust_value(value);
3887 self.slider = value;
3888 self.slider_value_text
3889 .set_text(widgets::slider::format_slider_value(value));
3890 }
3891
3892 fn apply_slider_value_from_text(&mut self, value: f32) {
3893 if self.slider_clamping == widgets::SliderClamping::Always {
3894 self.set_slider_value(value);
3895 } else {
3896 self.slider = value;
3897 }
3898 }
3899
3900 fn set_slider_left(&mut self, value: f32) {
3901 self.slider_left = value.min(self.slider_right - 1.0).max(0.0);
3902 self.slider_left_text
3903 .set_text(widgets::slider::format_slider_value(self.slider_left));
3904 if self.slider_clamping == widgets::SliderClamping::Always {
3905 self.clamp_slider_to_range();
3906 }
3907 }
3908
3909 fn apply_slider_left_from_text(&mut self, value: f32) {
3910 if value < self.slider_right {
3911 if self.slider_clamping == widgets::SliderClamping::Always {
3912 self.set_slider_left(value);
3913 return;
3914 }
3915 self.slider_left = value.max(0.0);
3916 if self.slider_clamping == widgets::SliderClamping::Always {
3917 self.slider = self.slider.clamp(self.slider_left, self.slider_right);
3918 }
3919 }
3920 }
3921
3922 fn set_slider_right(&mut self, value: f32) {
3923 self.slider_right = value.max(self.slider_left + 1.0).min(10000.0);
3924 self.slider_right_text
3925 .set_text(widgets::slider::format_slider_value(self.slider_right));
3926 if self.slider_clamping == widgets::SliderClamping::Always {
3927 self.clamp_slider_to_range();
3928 }
3929 }
3930
3931 fn apply_slider_right_from_text(&mut self, value: f32) {
3932 if value > self.slider_left {
3933 if self.slider_clamping == widgets::SliderClamping::Always {
3934 self.set_slider_right(value);
3935 return;
3936 }
3937 self.slider_right = value.min(10000.0);
3938 if self.slider_clamping == widgets::SliderClamping::Always {
3939 self.slider = self.slider.clamp(self.slider_left, self.slider_right);
3940 }
3941 }
3942 }
3943
3944 fn clamp_slider_to_range(&mut self) {
3945 self.set_slider_value(self.slider.clamp(self.slider_left, self.slider_right));
3946 }
3947
3948 fn slider_step(&self) -> f32 {
3949 self.slider_step_value.abs().max(0.0001)
3950 }
3951
3952 fn refresh_diagnostics_snapshot(&mut self) {
3953 self.diagnostics_snapshot = diagnostics_sample_snapshot(self);
3954 }
3955
3956 fn refresh_date_week_range(&mut self) {
3957 if self.date_mode != DateDemoMode::Week {
3958 return;
3959 }
3960 let anchor = self
3961 .date_range
3962 .range
3963 .map(|range| range.start)
3964 .or(self.date.selected)
3965 .or(self.date_range.today);
3966 if let Some(anchor) = anchor {
3967 self.date_range.mode = ext_widgets::DateRangeSelectionMode::Week;
3968 self.date_range.select(anchor);
3969 }
3970 }
3971
3972 fn app_theme(&self) -> Theme {
3973 self.showcase_theme.theme()
3974 }
3975
3976 fn view(&self, viewport: UiSize) -> UiDocument {
3977 set_showcase_active_theme(self.showcase_theme);
3978 let theme = self.app_theme();
3979 let mut ui = UiDocument::with_capacity(
3980 root_style(viewport.width, viewport.height),
3981 SHOWCASE_DOCUMENT_NODE_CAPACITY,
3982 );
3983 if let Some(update) = self.user_image_update.clone() {
3984 ui.add_resource_update(update);
3985 }
3986 ui.node_mut(ui.root())
3987 .set_visual(UiVisual::panel(theme.colors.canvas, None, 0.0));
3988
3989 let root = ui.root();
3990 let shell = ui.add_child(
3991 root,
3992 UiNode::container(
3993 "showcase.shell",
3994 LayoutStyle::row().with_size(viewport.width, viewport.height),
3995 ),
3996 );
3997 let desktop_size = desktop_size_for_viewport(viewport);
3998 let desktop_width = desktop_size.width;
3999 let desktop = ui.add_child(
4000 shell,
4001 UiNode::container(
4002 "showcase.desktop",
4003 LayoutStyle::new()
4004 .with_width(desktop_width)
4005 .with_height(viewport.height)
4006 .with_flex_shrink(1.0),
4007 )
4008 .with_visual(UiVisual::panel(theme.colors.canvas_subtle, None, 0.0)),
4009 );
4010 let controls = ui.add_child(
4011 shell,
4012 UiNode::container(
4013 "showcase.controls",
4014 LayoutStyle::column()
4015 .with_width(RIGHT_PANEL_WIDTH)
4016 .with_height(viewport.height)
4017 .with_flex_shrink(0.0)
4018 .padding(12.0)
4019 .gap(4.0),
4020 )
4021 .with_visual(UiVisual::panel(
4022 theme.colors.surface,
4023 Some(theme.stroke.surface),
4024 0.0,
4025 )),
4026 );
4027
4028 showcase_windows(&mut ui, desktop, self, desktop_size, &theme);
4029 organize_windows_button(&mut ui, desktop, &theme);
4030 fps_counter(&mut ui, desktop, self, viewport.height, &theme);
4031 control_panel(&mut ui, controls, self, viewport.height, &theme);
4032
4033 ui
4034 }
4035}
4036
4037fn organize_windows_button(ui: &mut UiDocument, desktop: UiNodeId, theme: &Theme) {
4038 let mut options =
4039 widgets::ButtonOptions::new(operad::layout::absolute(12.0, 12.0, 104.0, 28.0))
4040 .with_action("window.organize_open")
4041 .with_accessibility_label("Organize open windows");
4042 options.visual = theme.resolve_visual(ComponentRole::Button, ComponentState::NORMAL);
4043 options.hovered_visual =
4044 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::HOVERED));
4045 options.pressed_visual =
4046 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
4047 options.pressed_hovered_visual =
4048 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
4049 options.text_style = themed_text(theme, 12.0);
4050 let button = widgets::button(
4051 ui,
4052 desktop,
4053 "showcase.organize_windows",
4054 "Organize",
4055 options,
4056 );
4057 ui.node_mut(button)
4058 .style_mut()
4059 .set_z_index(SHOWCASE_WINDOW_Z_MAX.saturating_add(20));
4060}
4061
4062fn fps_counter(
4063 ui: &mut UiDocument,
4064 desktop: UiNodeId,
4065 state: &ShowcaseState,
4066 viewport_height: f32,
4067 theme: &Theme,
4068) {
4069 let mut counter_style = UiNodeStyle::from(operad::layout::absolute(
4070 12.0,
4071 (viewport_height - 34.0).max(12.0),
4072 92.0,
4073 24.0,
4074 ));
4075 counter_style.set_z_index(SHOWCASE_WINDOW_Z_MAX.saturating_add(16));
4076 let counter = ui.add_child(
4077 desktop,
4078 UiNode::container("showcase.fps", counter_style)
4079 .with_visual(UiVisual::panel(
4080 theme.colors.surface_overlay,
4081 Some(theme.stroke.surface),
4082 4.0,
4083 ))
4084 .with_accessibility(
4085 AccessibilityMeta::new(AccessibilityRole::Label).label("FPS counter"),
4086 ),
4087 );
4088 let fps = if state.fps > 0.0 {
4089 format!("{:.0} FPS", state.fps)
4090 } else {
4091 "-- FPS".to_string()
4092 };
4093 widgets::label(
4094 ui,
4095 counter,
4096 "showcase.fps.label",
4097 fps,
4098 themed_text(theme, 11.0),
4099 LayoutStyle::new()
4100 .with_width_percent(1.0)
4101 .with_height_percent(1.0)
4102 .padding(5.0),
4103 );
4104}
4105
4106fn showcase_windows(
4107 ui: &mut UiDocument,
4108 desktop: UiNodeId,
4109 state: &ShowcaseState,
4110 desktop_size: UiSize,
4111 theme: &Theme,
4112) {
4113 showcase_windows_with_desktop_state(ui, desktop, state, &state.desktop, desktop_size, theme);
4114}
4115
4116fn showcase_windows_with_desktop_state(
4117 ui: &mut UiDocument,
4118 desktop: UiNodeId,
4119 state: &ShowcaseState,
4120 desktop_state: &ext_widgets::FloatingDesktopState,
4121 desktop_size: UiSize,
4122 theme: &Theme,
4123) {
4124 let windows = showcase_window_descriptors(state, desktop_state, desktop_size);
4125 let options = showcase_desktop_options(desktop_size, theme);
4126 ext_widgets::floating_desktop(
4127 ui,
4128 desktop,
4129 "showcase.windows",
4130 &windows,
4131 options,
4132 |ui, window, descriptor| match descriptor.id.as_str() {
4133 "labels" => labels(ui, window, state),
4134 "buttons" => buttons(ui, window, state),
4135 "checkbox" => checkbox(ui, window, state),
4136 "toggles" => toggles(ui, window, state),
4137 "slider" => slider(ui, window, state),
4138 "numeric" => numeric_inputs(ui, window, state),
4139 "text_input" => text_input(ui, window, state),
4140 "selection" => selection_widgets(ui, window, state),
4141 "menus" => menu_widgets(ui, window, state),
4142 "command_palette" => command_palette(ui, window, state),
4143 "date_picker" => date_picker(ui, window, state),
4144 "color_picker" => color_picker(ui, window, state),
4145 "progress" => progress_indicator(ui, window, state),
4146 "animation" => animation_widgets(ui, window, state),
4147 "easing" => easing_widgets(ui, window, state),
4148 "lists_tables" => list_and_table_widgets(ui, window, state),
4149 "property_inspector" => property_inspector(ui, window, state),
4150 "diagnostics" => diagnostics_widgets(ui, window, state),
4151 "trees" => tree_widgets(ui, window, state),
4152 "layout_widgets" => tab_split_dock_widgets(ui, window, state),
4153 "containers" => container_widgets(ui, window, state),
4154 "panels" => panel_widgets(ui, window, state),
4155 "forms" => form_widgets(ui, window, state),
4156 "overlays" => overlay_widgets(ui, window, state),
4157 "drag_drop" => drag_drop_widgets(ui, window, state),
4158 "media" => media_widgets(ui, window, state),
4159 "shaders" => shader_effect_widgets(ui, window, state),
4160 "shader_lab" => shader_lab_widgets(ui, window, state),
4161 "timeline" => timeline_ruler(ui, window, state),
4162 "canvas" => canvas(ui, window, state),
4163 "theme" => theme_demo_widgets(ui, window, state, theme),
4164 "styling" => styling_widgets(ui, window, state),
4165 _ => {}
4166 },
4167 );
4168 showcase_overlays(ui, desktop, state, desktop_size);
4169}
4170
4171#[allow(clippy::field_reassign_with_default)]
4172fn showcase_overlays(
4173 ui: &mut UiDocument,
4174 desktop: UiNodeId,
4175 state: &ShowcaseState,
4176 desktop_size: UiSize,
4177) {
4178 if state.toast_visible {
4179 let overlay_width = 320.0;
4180 let mut overlay_style = UiNodeStyle::from(operad::layout::absolute(
4181 (desktop_size.width - overlay_width - 18.0).max(18.0),
4182 18.0,
4183 overlay_width,
4184 180.0,
4185 ));
4186 overlay_style.set_clip(ClipBehavior::None);
4187 overlay_style.set_z_index(6000);
4188 let overlay = ui.add_child(
4189 desktop,
4190 UiNode::container("showcase.toast_overlay", overlay_style),
4191 );
4192 let mut stack = ext_widgets::ToastStack::new(3);
4193 stack.push_toast(
4194 ext_widgets::Toast::new(
4195 ext_widgets::ToastId::new(1),
4196 ext_widgets::ToastSeverity::Success,
4197 "Saved",
4198 Some("All changes are written".to_string()),
4199 None,
4200 )
4201 .with_action(ext_widgets::ToastAction::new("undo", "Undo")),
4202 );
4203 stack.push(
4204 ext_widgets::ToastSeverity::Warning,
4205 "Autosave paused",
4206 Some("Changes are kept locally".to_string()),
4207 None,
4208 );
4209 let mut options = ext_widgets::ToastStackOptions::default();
4210 options.z_index = 6100;
4211 ext_widgets::toast_stack(ui, overlay, "showcase.toast_overlay.stack", &stack, options);
4212 }
4213}
4214
4215fn showcase_window_descriptors(
4216 state: &ShowcaseState,
4217 desktop_state: &ext_widgets::FloatingDesktopState,
4218 desktop_size: UiSize,
4219) -> Vec<ext_widgets::FloatingWindowDescriptor> {
4220 let wide = (desktop_size.width - 36.0).clamp(320.0, 720.0);
4221 let medium = (desktop_size.width - 36.0).clamp(300.0, 604.0);
4222 let buttons_width = medium.min(620.0);
4223 let mut windows = Vec::new();
4224 push_window(
4225 &mut windows,
4226 state.windows.labels,
4227 "labels",
4228 "Labels",
4229 UiSize::new(380.0, 460.0),
4230 );
4231 push_window(
4232 &mut windows,
4233 state.windows.buttons,
4234 "buttons",
4235 "Buttons",
4236 UiSize::new(buttons_width, 220.0),
4237 );
4238 push_window(
4239 &mut windows,
4240 state.windows.checkbox,
4241 "checkbox",
4242 "Checkbox",
4243 UiSize::new(250.0, 72.0),
4244 );
4245 push_window(
4246 &mut windows,
4247 state.windows.toggles,
4248 "toggles",
4249 "Radio and toggles",
4250 UiSize::new(360.0, 320.0),
4251 );
4252 push_window(
4253 &mut windows,
4254 state.windows.slider,
4255 "slider",
4256 "Slider",
4257 UiSize::new(430.0, 560.0),
4258 );
4259 push_window(
4260 &mut windows,
4261 state.windows.numeric,
4262 "numeric",
4263 "Numeric input",
4264 UiSize::new(360.0, 180.0),
4265 );
4266 push_window(
4267 &mut windows,
4268 state.windows.text_input,
4269 "text_input",
4270 "Text input",
4271 UiSize::new(520.0, 560.0),
4272 );
4273 push_window(
4274 &mut windows,
4275 state.windows.selection,
4276 "selection",
4277 "Select controls",
4278 UiSize::new(300.0, 430.0),
4279 );
4280 push_window(
4281 &mut windows,
4282 state.windows.menus,
4283 "menus",
4284 "Menu controls",
4285 UiSize::new(wide, 520.0),
4286 );
4287 push_window(
4288 &mut windows,
4289 state.windows.command_palette,
4290 "command_palette",
4291 "Command palette",
4292 UiSize::new(280.0, 130.0),
4293 );
4294 push_window(
4295 &mut windows,
4296 state.windows.date_picker,
4297 "date_picker",
4298 "Date picker",
4299 UiSize::new(430.0, 390.0),
4300 );
4301 push_window(
4302 &mut windows,
4303 state.windows.color_picker,
4304 "color_picker",
4305 "Color picker",
4306 UiSize::new(340.0, 390.0),
4307 );
4308 push_window(
4309 &mut windows,
4310 state.windows.progress,
4311 "progress",
4312 "Progress indicator",
4313 UiSize::new(500.0, 168.0),
4314 );
4315 push_window(
4316 &mut windows,
4317 state.windows.animation,
4318 "animation",
4319 "Animation",
4320 UiSize::new(520.0, 430.0),
4321 );
4322 push_window(
4323 &mut windows,
4324 state.windows.easing,
4325 "easing",
4326 "Easing",
4327 UiSize::new(520.0, 450.0),
4328 );
4329 push_window(
4330 &mut windows,
4331 state.windows.lists_tables,
4332 "lists_tables",
4333 "Lists and tables",
4334 UiSize::new(600.0, 500.0),
4335 );
4336 push_window(
4337 &mut windows,
4338 state.windows.property_inspector,
4339 "property_inspector",
4340 "Property inspector",
4341 UiSize::new(330.0, 250.0),
4342 );
4343 push_window(
4344 &mut windows,
4345 state.windows.diagnostics,
4346 "diagnostics",
4347 "Diagnostics",
4348 UiSize::new(640.0, 760.0),
4349 );
4350 push_window(
4351 &mut windows,
4352 state.windows.trees,
4353 "trees",
4354 "Trees",
4355 UiSize::new(430.0, 390.0),
4356 );
4357 push_window(
4358 &mut windows,
4359 state.windows.layout_widgets,
4360 "layout_widgets",
4361 "Layout widgets",
4362 UiSize::new(wide.min(700.0), 430.0),
4363 );
4364 push_window(
4365 &mut windows,
4366 state.windows.containers,
4367 "containers",
4368 "Containers",
4369 UiSize::new(380.0, 520.0),
4370 );
4371 push_window(
4372 &mut windows,
4373 state.windows.panels,
4374 "panels",
4375 "Panels",
4376 UiSize::new(460.0, 280.0),
4377 );
4378 push_window(
4379 &mut windows,
4380 state.windows.forms,
4381 "forms",
4382 "Forms",
4383 UiSize::new(520.0, 620.0),
4384 );
4385 push_window(
4386 &mut windows,
4387 state.windows.overlays,
4388 "overlays",
4389 "Overlays",
4390 UiSize::new(620.0, 680.0),
4391 );
4392 push_window(
4393 &mut windows,
4394 state.windows.drag_drop,
4395 "drag_drop",
4396 "Drag and drop",
4397 UiSize::new(500.0, 460.0),
4398 );
4399 push_window(
4400 &mut windows,
4401 state.windows.media,
4402 "media",
4403 "Media",
4404 UiSize::new(520.0, 430.0),
4405 );
4406 push_window(
4407 &mut windows,
4408 state.windows.shaders,
4409 "shaders",
4410 "Shader effects",
4411 UiSize::new(500.0, 410.0),
4412 );
4413 push_window(
4414 &mut windows,
4415 state.windows.shader_lab,
4416 "shader_lab",
4417 "Shader lab",
4418 UiSize::new(1000.0, 700.0),
4419 );
4420 push_window(
4421 &mut windows,
4422 state.windows.timeline,
4423 "timeline",
4424 "Timeline",
4425 UiSize::new(600.0, 120.0),
4426 );
4427 push_window(
4428 &mut windows,
4429 state.windows.canvas,
4430 "canvas",
4431 "Canvas",
4432 UiSize::new(760.0, 500.0),
4433 );
4434 push_window(
4435 &mut windows,
4436 state.windows.theme,
4437 "theme",
4438 "Theme",
4439 UiSize::new(430.0, 360.0),
4440 );
4441 push_window(
4442 &mut windows,
4443 state.windows.styling,
4444 "styling",
4445 "Styling",
4446 UiSize::new(540.0, 440.0),
4447 );
4448 for window in &mut windows {
4449 window.drag_action = Some(WidgetActionBinding::action(format!(
4450 "window.drag.{}",
4451 window.id
4452 )));
4453 window.collapse_action = Some(WidgetActionBinding::action(format!(
4454 "window.collapse.{}",
4455 window.id
4456 )));
4457 window.resize_action = Some(WidgetActionBinding::action(format!(
4458 "window.resize.{}",
4459 window.id
4460 )));
4461 desktop_state.apply_to_descriptor(window, window_defaults(window.id.as_str()));
4462 }
4463 windows
4464}
4465
4466fn push_window(
4467 windows: &mut Vec<ext_widgets::FloatingWindowDescriptor>,
4468 visible: bool,
4469 id: &'static str,
4470 title: &'static str,
4471 preferred_size: UiSize,
4472) {
4473 if visible {
4474 let mut window = ext_widgets::FloatingWindowDescriptor::new(id, title, preferred_size)
4475 .with_min_size(default_window_state_min_size(id))
4476 .with_auto_size_to_content(true)
4477 .with_activate_action(format!("window.activate.{id}"))
4478 .with_close_action(format!("window.close.{id}"));
4479 if id == "animation" {
4480 window = window.with_content_min_size(UiSize::new(
4481 ANIMATION_STAGE_MIN_WIDTH,
4482 ANIMATION_CONTENT_MIN_HEIGHT,
4483 ));
4484 } else if id == "easing" {
4485 window = window.with_content_min_size(UiSize::new(
4486 EASING_STAGE_MIN_WIDTH,
4487 EASING_CONTENT_MIN_HEIGHT,
4488 ));
4489 } else if id == "layout_widgets" {
4490 window = window.with_content_min_size(UiSize::new(640.0, 360.0));
4491 } else if id == "canvas" {
4492 window = window.with_content_min_size(UiSize::new(720.0, 440.0));
4493 } else if id == "shader_lab" {
4494 window = window.with_content_min_size(UiSize::new(
4495 SHADER_LAB_CONTENT_MIN_WIDTH,
4496 SHADER_LAB_CONTENT_MIN_HEIGHT,
4497 ));
4498 }
4499 windows.push(window);
4500 }
4501}
4502
4503fn default_window_size(id: &str) -> UiSize {
4504 match id {
4505 "labels" => UiSize::new(380.0, 460.0),
4506 "buttons" => UiSize::new(604.0, 220.0),
4507 "checkbox" => UiSize::new(380.0, 360.0),
4508 "toggles" => UiSize::new(400.0, 430.0),
4509 "slider" => UiSize::new(430.0, 560.0),
4510 "numeric" => UiSize::new(430.0, 260.0),
4511 "text_input" => UiSize::new(520.0, 640.0),
4512 "selection" => UiSize::new(300.0, 430.0),
4513 "menus" => UiSize::new(640.0, 640.0),
4514 "command_palette" => UiSize::new(280.0, 130.0),
4515 "date_picker" => UiSize::new(304.0, 470.0),
4516 "color_picker" => UiSize::new(340.0, 390.0),
4517 "progress" => UiSize::new(500.0, 300.0),
4518 "animation" => UiSize::new(520.0, 430.0),
4519 "easing" => UiSize::new(520.0, 450.0),
4520 "lists_tables" => UiSize::new(600.0, 500.0),
4521 "property_inspector" => UiSize::new(330.0, 250.0),
4522 "diagnostics" => UiSize::new(640.0, 760.0),
4523 "trees" => UiSize::new(430.0, 450.0),
4524 "layout_widgets" => UiSize::new(700.0, 430.0),
4525 "containers" => UiSize::new(380.0, 520.0),
4526 "panels" => UiSize::new(640.0, 440.0),
4527 "forms" => UiSize::new(520.0, 620.0),
4528 "overlays" => UiSize::new(620.0, 680.0),
4529 "drag_drop" => UiSize::new(500.0, 460.0),
4530 "media" => UiSize::new(430.0, 560.0),
4531 "shaders" => UiSize::new(500.0, 410.0),
4532 "shader_lab" => UiSize::new(1000.0, 700.0),
4533 "timeline" => UiSize::new(760.0, 280.0),
4534 "canvas" => UiSize::new(760.0, 500.0),
4535 "theme" => UiSize::new(430.0, 360.0),
4536 "styling" => UiSize::new(640.0, 560.0),
4537 _ => UiSize::new(300.0, 180.0),
4538 }
4539}
4540
4541fn default_window_state_min_size(_id: &str) -> UiSize {
4542 UiSize::new(160.0, 96.0)
4543}
4544
4545fn showcase_window_title(id: &str) -> &'static str {
4546 match id {
4547 "labels" => "Labels",
4548 "buttons" => "Buttons",
4549 "checkbox" => "Checkbox",
4550 "toggles" => "Radio and toggles",
4551 "slider" => "Slider",
4552 "numeric" => "Numeric input",
4553 "text_input" => "Text input",
4554 "selection" => "Select controls",
4555 "menus" => "Menu controls",
4556 "command_palette" => "Command palette",
4557 "date_picker" => "Date picker",
4558 "color_picker" => "Color picker",
4559 "progress" => "Progress indicator",
4560 "animation" => "Animation",
4561 "easing" => "Easing",
4562 "lists_tables" => "Lists and tables",
4563 "property_inspector" => "Property inspector",
4564 "diagnostics" => "Diagnostics",
4565 "trees" => "Trees",
4566 "layout_widgets" => "Layout widgets",
4567 "containers" => "Containers",
4568 "panels" => "Panels",
4569 "forms" => "Forms",
4570 "overlays" => "Overlays",
4571 "drag_drop" => "Drag and drop",
4572 "media" => "Media",
4573 "shaders" => "Shader effects",
4574 "shader_lab" => "Shader lab",
4575 "timeline" => "Timeline",
4576 "canvas" => "Canvas",
4577 "theme" => "Theme",
4578 "styling" => "Styling",
4579 _ => "Window",
4580 }
4581}
4582
4583fn showcase_collapsed_window_size(
4584 id: &str,
4585 options: &ext_widgets::FloatingDesktopOptions,
4586) -> UiSize {
4587 let min_size = default_window_state_min_size(id);
4588 let padding = options.content_padding.max(0.0);
4589 let button = options.close_button_size.max(1.0);
4590 let control_width = (button + 8.0) * 2.0;
4591 let font_size = options.title_style.font_size.max(1.0);
4592 let title_width =
4593 (showcase_window_title(id).chars().count() as f32 * font_size * 0.55).max(font_size);
4594 UiSize::new(
4595 min_size
4596 .width
4597 .max(padding * 2.0 + control_width + title_width),
4598 options.title_bar_height.max(1.0),
4599 )
4600}
4601
4602fn default_window_position(id: &str) -> UiPoint {
4603 match id {
4604 "labels" => UiPoint::new(18.0, 18.0),
4605 "buttons" => UiPoint::new(420.0, 18.0),
4606 "checkbox" => UiPoint::new(360.0, 18.0),
4607 "toggles" => UiPoint::new(360.0, 110.0),
4608 "slider" => UiPoint::new(360.0, 110.0),
4609 "numeric" => UiPoint::new(360.0, 260.0),
4610 "text_input" => UiPoint::new(360.0, 18.0),
4611 "selection" => UiPoint::new(360.0, 404.0),
4612 "menus" => UiPoint::new(18.0, 18.0),
4613 "command_palette" => UiPoint::new(68.0, 88.0),
4614 "date_picker" => UiPoint::new(300.0, 170.0),
4615 "color_picker" => UiPoint::new(18.0, 560.0),
4616 "progress" => UiPoint::new(72.0, 540.0),
4617 "animation" => UiPoint::new(180.0, 170.0),
4618 "easing" => UiPoint::new(220.0, 210.0),
4619 "lists_tables" => UiPoint::new(18.0, 90.0),
4620 "property_inspector" => UiPoint::new(300.0, 420.0),
4621 "diagnostics" => UiPoint::new(640.0, 70.0),
4622 "trees" => UiPoint::new(36.0, 220.0),
4623 "layout_widgets" => UiPoint::new(18.0, 18.0),
4624 "containers" => UiPoint::new(48.0, 120.0),
4625 "panels" => UiPoint::new(140.0, 120.0),
4626 "forms" => UiPoint::new(120.0, 160.0),
4627 "overlays" => UiPoint::new(80.0, 110.0),
4628 "drag_drop" => UiPoint::new(210.0, 250.0),
4629 "media" => UiPoint::new(120.0, 360.0),
4630 "shaders" => UiPoint::new(180.0, 260.0),
4631 "shader_lab" => UiPoint::new(120.0, 170.0),
4632 "timeline" => UiPoint::new(18.0, 620.0),
4633 "canvas" => UiPoint::new(280.0, 390.0),
4634 "theme" => UiPoint::new(120.0, 120.0),
4635 "styling" => UiPoint::new(86.0, 118.0),
4636 _ => UiPoint::new(18.0, 18.0),
4637 }
4638}
4639
4640fn window_for_action(action_id: &str) -> Option<&'static str> {
4641 match action_id {
4642 id if id.starts_with("labels.") => Some("labels"),
4643 id if id.starts_with("button.") => Some("buttons"),
4644 id if id.starts_with("checkbox.") => Some("checkbox"),
4645 id if id.starts_with("toggles.") => Some("toggles"),
4646 id if id.starts_with("theme.preference.") => Some("toggles"),
4647 id if id.starts_with("slider.") => Some("slider"),
4648 id if id.starts_with("numeric.") => Some("numeric"),
4649 id if id.starts_with("text.") => Some("text_input"),
4650 id if id.starts_with("selection.dropdown.")
4651 || id.starts_with("selection.menu.")
4652 || id.starts_with("selection.image_menu.") =>
4653 {
4654 Some("selection")
4655 }
4656 id if id.starts_with("menus.") => Some("menus"),
4657 id if id.starts_with("command_palette.") => Some("command_palette"),
4658 id if id.starts_with("date.") => Some("date_picker"),
4659 id if id.starts_with("color.") => Some("color_picker"),
4660 id if id.starts_with("progress.") => Some("progress"),
4661 id if id.starts_with("animation.") => Some("animation"),
4662 id if id.starts_with("easing.") => Some("easing"),
4663 id if id.starts_with("lists_tables.") => Some("lists_tables"),
4664 id if id.starts_with("property_inspector.") => Some("property_inspector"),
4665 id if id.starts_with("diagnostics.") => Some("diagnostics"),
4666 id if id.starts_with("trees.") => Some("trees"),
4667 id if id.starts_with("layout.") || id.starts_with("layout_widgets.") => {
4668 Some("layout_widgets")
4669 }
4670 id if id.starts_with("containers.") => Some("containers"),
4671 id if id.starts_with("panels.") => Some("panels"),
4672 id if id.starts_with("forms.") => Some("forms"),
4673 id if id.starts_with("overlays.") => Some("overlays"),
4674 id if id.starts_with("drag_drop.") => Some("drag_drop"),
4675 id if id.starts_with("media.") => Some("media"),
4676 id if id.starts_with("shaders.") => Some("shaders"),
4677 id if id.starts_with("shader_lab.") => Some("shader_lab"),
4678 id if id.starts_with("toast.") => Some("overlays"),
4679 id if id.starts_with("canvas.") => Some("canvas"),
4680 id if id.starts_with("theme.") => Some("theme"),
4681 id if id.starts_with("styling.") => Some("styling"),
4682 _ => None,
4683 }
4684}
4685
4686fn focused_text_for_action(action_id: &str) -> Option<FocusedTextInput> {
4687 Some(match action_id {
4688 "text.input.edit" => FocusedTextInput::Editable,
4689 "text.selectable.edit" => FocusedTextInput::Selectable,
4690 "text.singleline.edit" => FocusedTextInput::Singleline,
4691 "text.multiline.edit" => FocusedTextInput::Multiline,
4692 "text.area.edit" => FocusedTextInput::TextArea,
4693 "text.code_editor.edit" => FocusedTextInput::CodeEditor,
4694 "text.search.edit" => FocusedTextInput::Search,
4695 "text.password.edit" => FocusedTextInput::Password,
4696 "forms.profile.name.input.edit" => FocusedTextInput::FormName,
4697 "forms.profile.email.input.edit" => FocusedTextInput::FormEmail,
4698 "forms.profile.role.input.edit" => FocusedTextInput::FormRole,
4699 "numeric.value.edit" => FocusedTextInput::NumericValue,
4700 "numeric.range_min.value.edit" => FocusedTextInput::NumericRangeMin,
4701 "numeric.range_max.value.edit" => FocusedTextInput::NumericRangeMax,
4702 "slider.value_text.edit" => FocusedTextInput::SliderValue,
4703 "slider.left_text.edit" => FocusedTextInput::SliderRangeLeft,
4704 "slider.right_text.edit" => FocusedTextInput::SliderRangeRight,
4705 "slider.step_text.edit" => FocusedTextInput::SliderStep,
4706 "shader_lab.editor.edit" => FocusedTextInput::ShaderLabSource,
4707 _ => return None,
4708 })
4709}
4710
4711fn control_panel(
4712 ui: &mut UiDocument,
4713 parent: UiNodeId,
4714 state: &ShowcaseState,
4715 viewport_height: f32,
4716 theme: &Theme,
4717) {
4718 widgets::label(
4719 ui,
4720 parent,
4721 "controls.title",
4722 "Widgets",
4723 themed_text(theme, 16.0),
4724 LayoutStyle::new().with_width_percent(1.0),
4725 );
4726 let list_viewport_height = controls_list_viewport_height(viewport_height);
4727 let controls_scroll =
4728 controls_scroll_state_for_view(state.controls_scroll, list_viewport_height);
4729 let list_nodes = scroll_area_widgets::scroll_container_shell(
4730 ui,
4731 parent,
4732 "controls.widget_list",
4733 controls_scroll,
4734 widgets::ScrollContainerOptions::default()
4735 .with_layout(
4736 LayoutStyle::column()
4737 .with_width_percent(1.0)
4738 .with_height(list_viewport_height)
4739 .with_flex_grow(1.0)
4740 .with_flex_shrink(1.0),
4741 )
4742 .with_viewport_layout(
4743 LayoutStyle::column()
4744 .with_width(0.0)
4745 .with_height_percent(1.0)
4746 .with_flex_grow(1.0)
4747 .with_flex_shrink(1.0)
4748 .gap(CONTROLS_WIDGET_ROW_GAP),
4749 )
4750 .with_axes(ScrollAxes::VERTICAL)
4751 .with_scrollbar_thickness(8.0)
4752 .with_gap(2.0)
4753 .with_action_prefix("controls.widget_list")
4754 .with_vertical_scrollbar(
4755 scrollbar_widgets::ScrollbarOptions::default()
4756 .with_action("controls.widget_list.scrollbar"),
4757 ),
4758 );
4759 let list = list_nodes.viewport;
4760
4761 window_toggle(ui, list, "labels", "Labels", state.windows.labels, theme);
4762 window_toggle(ui, list, "buttons", "Buttons", state.windows.buttons, theme);
4763 window_toggle(
4764 ui,
4765 list,
4766 "checkbox",
4767 "Checkbox",
4768 state.windows.checkbox,
4769 theme,
4770 );
4771 window_toggle(
4772 ui,
4773 list,
4774 "toggles",
4775 "Radio and toggles",
4776 state.windows.toggles,
4777 theme,
4778 );
4779 window_toggle(ui, list, "slider", "Slider", state.windows.slider, theme);
4780 window_toggle(
4781 ui,
4782 list,
4783 "numeric",
4784 "Numeric input",
4785 state.windows.numeric,
4786 theme,
4787 );
4788 window_toggle(
4789 ui,
4790 list,
4791 "text_input",
4792 "Text input",
4793 state.windows.text_input,
4794 theme,
4795 );
4796 window_toggle(
4797 ui,
4798 list,
4799 "selection",
4800 "Select controls",
4801 state.windows.selection,
4802 theme,
4803 );
4804 window_toggle(
4805 ui,
4806 list,
4807 "menus",
4808 "Menu controls",
4809 state.windows.menus,
4810 theme,
4811 );
4812 window_toggle(
4813 ui,
4814 list,
4815 "command_palette",
4816 "Command palette",
4817 state.windows.command_palette,
4818 theme,
4819 );
4820 window_toggle(
4821 ui,
4822 list,
4823 "date_picker",
4824 "Date picker",
4825 state.windows.date_picker,
4826 theme,
4827 );
4828 window_toggle(
4829 ui,
4830 list,
4831 "color_picker",
4832 "Color picker",
4833 state.windows.color_picker,
4834 theme,
4835 );
4836 window_toggle(
4837 ui,
4838 list,
4839 "progress",
4840 "Progress indicator",
4841 state.windows.progress,
4842 theme,
4843 );
4844 window_toggle(
4845 ui,
4846 list,
4847 "animation",
4848 "Animation",
4849 state.windows.animation,
4850 theme,
4851 );
4852 window_toggle(ui, list, "easing", "Easing", state.windows.easing, theme);
4853 window_toggle(
4854 ui,
4855 list,
4856 "lists_tables",
4857 "Lists and tables",
4858 state.windows.lists_tables,
4859 theme,
4860 );
4861 window_toggle(
4862 ui,
4863 list,
4864 "property_inspector",
4865 "Property inspector",
4866 state.windows.property_inspector,
4867 theme,
4868 );
4869 window_toggle(
4870 ui,
4871 list,
4872 "diagnostics",
4873 "Diagnostics",
4874 state.windows.diagnostics,
4875 theme,
4876 );
4877 window_toggle(ui, list, "trees", "Trees", state.windows.trees, theme);
4878 window_toggle(
4879 ui,
4880 list,
4881 "layout_widgets",
4882 "Layout widgets",
4883 state.windows.layout_widgets,
4884 theme,
4885 );
4886 window_toggle(
4887 ui,
4888 list,
4889 "containers",
4890 "Containers",
4891 state.windows.containers,
4892 theme,
4893 );
4894 window_toggle(ui, list, "panels", "Panels", state.windows.panels, theme);
4895 window_toggle(ui, list, "forms", "Forms", state.windows.forms, theme);
4896 window_toggle(
4897 ui,
4898 list,
4899 "overlays",
4900 "Overlays, popups, and toasts",
4901 state.windows.overlays,
4902 theme,
4903 );
4904 window_toggle(
4905 ui,
4906 list,
4907 "drag_drop",
4908 "Drag and drop",
4909 state.windows.drag_drop,
4910 theme,
4911 );
4912 window_toggle(ui, list, "media", "Media", state.windows.media, theme);
4913 window_toggle(
4914 ui,
4915 list,
4916 "shaders",
4917 "Shader effects",
4918 state.windows.shaders,
4919 theme,
4920 );
4921 window_toggle(
4922 ui,
4923 list,
4924 "shader_lab",
4925 "Shader lab",
4926 state.windows.shader_lab,
4927 theme,
4928 );
4929 window_toggle(
4930 ui,
4931 list,
4932 "timeline",
4933 "Timeline",
4934 state.windows.timeline,
4935 theme,
4936 );
4937 window_toggle(ui, list, "canvas", "Canvas", state.windows.canvas, theme);
4938 window_toggle(ui, list, "theme", "Theme", state.windows.theme, theme);
4939 window_toggle(ui, list, "styling", "Styling", state.windows.styling, theme);
4940
4941 ui.add_child(
4942 parent,
4943 UiNode::container(
4944 "controls.clear_all.spacer",
4945 LayoutStyle::new()
4946 .with_width_percent(1.0)
4947 .with_height(1.0)
4948 .with_flex_grow(1.0)
4949 .with_flex_shrink(1.0),
4950 ),
4951 );
4952 let actions = ui.add_child(
4953 parent,
4954 UiNode::container(
4955 "controls.bulk_actions",
4956 LayoutStyle::row()
4957 .with_width_percent(1.0)
4958 .with_height(30.0)
4959 .with_flex_shrink(0.0)
4960 .gap(8.0),
4961 ),
4962 );
4963 control_action_button(
4964 ui,
4965 actions,
4966 "controls.add_all",
4967 "Add all",
4968 "window.add_all",
4969 "Add all widgets",
4970 theme,
4971 );
4972 control_action_button(
4973 ui,
4974 actions,
4975 "controls.clear_all",
4976 "Clear all",
4977 "window.clear_all",
4978 "Clear all widgets",
4979 theme,
4980 );
4981}
4982
4983fn control_action_button(
4984 ui: &mut UiDocument,
4985 parent: UiNodeId,
4986 name: &'static str,
4987 label: &'static str,
4988 action: &'static str,
4989 accessibility_label: &'static str,
4990 theme: &Theme,
4991) {
4992 let mut options = themed_button_options(
4993 theme,
4994 action,
4995 ComponentState::NORMAL,
4996 LayoutStyle::new()
4997 .with_width(0.0)
4998 .with_height_percent(1.0)
4999 .with_flex_grow(1.0)
5000 .with_flex_shrink(1.0),
5001 );
5002 options.text_style = themed_text(theme, 12.0);
5003 options.accessibility_label = Some(accessibility_label.to_string());
5004 widgets::button(ui, parent, name, label, options);
5005}
5006
5007fn window_toggle(
5008 ui: &mut UiDocument,
5009 parent: UiNodeId,
5010 id: &'static str,
5011 label: &'static str,
5012 checked: bool,
5013 theme: &Theme,
5014) {
5015 let mut options =
5016 widgets::CheckboxOptions::default().with_action(format!("window.toggle.{id}"));
5017 options.layout = LayoutStyle::new()
5018 .with_width_percent(1.0)
5019 .with_height(CONTROLS_WIDGET_ROW_HEIGHT);
5020 options.text_style = themed_text(theme, 12.0);
5021 options.box_visual = UiVisual::panel(
5022 theme.colors.surface_sunken,
5023 Some(StrokeStyle::new(theme.colors.border_strong, 1.0)),
5024 3.0,
5025 );
5026 options.checked_box_visual = Some(UiVisual::panel(
5027 theme.colors.accent,
5028 Some(theme.stroke.focus),
5029 3.0,
5030 ));
5031 options.check_color = theme.colors.accent_text;
5032 widgets::checkbox(
5033 ui,
5034 parent,
5035 format!("controls.{id}"),
5036 label,
5037 checked,
5038 options,
5039 );
5040}
5041
5042#[allow(clippy::field_reassign_with_default)]
5043fn labels(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5044 let body = section(ui, parent, "labels", "Labels");
5045 ui.set_node_style(
5046 body,
5047 LayoutStyle::column()
5048 .with_width_percent(1.0)
5049 .with_height_percent(1.0)
5050 .with_flex_grow(1.0)
5051 .gap(10.0),
5052 );
5053 widgets::label(
5054 ui,
5055 body,
5056 "labels.plain",
5057 "Plain label",
5058 text(13.0, color(226, 232, 242)),
5059 LayoutStyle::new().with_width_percent(1.0),
5060 );
5061 let locale_items = label_locale_options();
5062 let locale_id = state
5063 .label_locale
5064 .selected_id(&locale_items)
5065 .unwrap_or("es-MX");
5066 let localization =
5067 LocalizationPolicy::new(LocaleId::new(locale_id).unwrap_or_else(|_| LocaleId::default()));
5068 let locale_row = ui.add_child(
5069 body,
5070 UiNode::container(
5071 "labels.locale.row",
5072 LayoutStyle::row()
5073 .with_width_percent(1.0)
5074 .with_align_items(taffy::prelude::AlignItems::Center)
5075 .gap(10.0),
5076 ),
5077 );
5078 let locale_label_width = 270.0;
5079 let locale_dropdown_width = 148.0;
5080 let locale_gap = 10.0;
5081 widgets::localized_label(
5082 ui,
5083 locale_row,
5084 "labels.localized",
5085 DynamicLabelMeta::keyed("showcase.localized.greeting", localized_label(locale_id)),
5086 Some(&localization),
5087 text(13.0, color(170, 202, 255)),
5088 LayoutStyle::new().with_width(locale_label_width),
5089 );
5090 let mut locale_options = ext_widgets::DropdownSelectOptions::default();
5091 locale_options.trigger_layout = LayoutStyle::row()
5092 .with_width(locale_dropdown_width)
5093 .with_height(30.0)
5094 .with_align_items(taffy::prelude::AlignItems::Center)
5095 .with_justify_content(taffy::prelude::JustifyContent::FlexStart)
5096 .gap(6.0)
5097 .padding(6.0);
5098 locale_options.text_style = text(13.0, color(226, 232, 242));
5099 locale_options.accessibility_label = Some("Locale".to_string());
5100 locale_options.menu =
5101 ext_widgets::SelectMenuOptions::default().with_action_prefix("labels.locale");
5102 locale_options.menu.width = locale_dropdown_width;
5103 locale_options.menu.row_height = 30.0;
5104 locale_options.menu.max_visible_rows = locale_items.len();
5105 locale_options.menu.text_style = text(13.0, color(226, 232, 242));
5106 locale_options.menu.portal = UiPortalTarget::Parent;
5107 let locale_nodes = ext_widgets::dropdown_select(
5108 ui,
5109 locale_row,
5110 "labels.locale",
5111 &locale_items,
5112 &state.label_locale,
5113 Some(ext_widgets::AnchoredPopup::new(
5114 UiRect::new(
5115 locale_label_width + locale_gap,
5116 0.0,
5117 locale_dropdown_width,
5118 30.0,
5119 ),
5120 UiRect::new(0.0, 0.0, 460.0, 260.0),
5121 ext_widgets::PopupPlacement::default()
5122 .with_offset(0.0)
5123 .with_viewport_margin(0.0),
5124 )),
5125 locale_options,
5126 );
5127 ui.node_mut(locale_nodes.trigger)
5128 .set_action("labels.locale.toggle");
5129 widgets::label(
5130 ui,
5131 body,
5132 "labels.muted",
5133 "Muted helper label",
5134 text(12.0, color(154, 166, 184)),
5135 LayoutStyle::new().with_width_percent(1.0),
5136 );
5137
5138 let sizes = ui.add_child(
5139 body,
5140 UiNode::container(
5141 "labels.sizes",
5142 LayoutStyle::row()
5143 .with_width_percent(1.0)
5144 .with_align_items(taffy::prelude::AlignItems::FlexEnd)
5145 .gap(12.0),
5146 ),
5147 );
5148 widgets::label(
5149 ui,
5150 sizes,
5151 "labels.size.small",
5152 "12px",
5153 text(12.0, color(226, 232, 242)),
5154 LayoutStyle::new(),
5155 );
5156 widgets::label(
5157 ui,
5158 sizes,
5159 "labels.size.default",
5160 "13px",
5161 text(13.0, color(226, 232, 242)),
5162 LayoutStyle::new(),
5163 );
5164 widgets::label(
5165 ui,
5166 sizes,
5167 "labels.size.large",
5168 "18px",
5169 text(18.0, color(246, 249, 252)),
5170 LayoutStyle::new(),
5171 );
5172 widgets::label(
5173 ui,
5174 sizes,
5175 "labels.size.display",
5176 "24px",
5177 text(24.0, color(246, 249, 252)),
5178 LayoutStyle::new(),
5179 );
5180
5181 let style_row = row(ui, body, "labels.styles", 12.0);
5182 let mut bold = text(13.0, color(246, 249, 252));
5183 bold.weight = FontWeight::BOLD;
5184 widgets::label(
5185 ui,
5186 style_row,
5187 "labels.style.bold",
5188 "Bold",
5189 bold,
5190 LayoutStyle::new(),
5191 );
5192 widgets::label(
5193 ui,
5194 style_row,
5195 "labels.style.weak",
5196 "Muted",
5197 text(13.0, color(154, 166, 184)),
5198 LayoutStyle::new(),
5199 );
5200
5201 let font_row = row(ui, body, "labels.fonts", 12.0);
5202 let mut serif = text(13.0, color(226, 232, 242));
5203 serif.family = FontFamily::Serif;
5204 widgets::label(
5205 ui,
5206 font_row,
5207 "labels.font.serif",
5208 "Serif",
5209 serif,
5210 LayoutStyle::new(),
5211 );
5212 let mut mono = text(13.0, color(226, 232, 242));
5213 mono.family = FontFamily::Monospace;
5214 widgets::label(
5215 ui,
5216 font_row,
5217 "labels.font.mono",
5218 "Monospace",
5219 mono,
5220 LayoutStyle::new(),
5221 );
5222
5223 let code_panel = ui.add_child(
5224 body,
5225 UiNode::container(
5226 "labels.code.panel",
5227 LayoutStyle::new()
5228 .with_width_percent(1.0)
5229 .padding(8.0)
5230 .with_height(36.0),
5231 )
5232 .with_visual(UiVisual::panel(
5233 color(10, 14, 20),
5234 Some(StrokeStyle::new(color(47, 59, 74), 1.0)),
5235 4.0,
5236 )),
5237 );
5238 widgets::code_label(
5239 ui,
5240 code_panel,
5241 "labels.code",
5242 "let label = widgets::label(...);",
5243 LayoutStyle::new().with_width_percent(1.0),
5244 );
5245
5246 let colors = row(ui, body, "labels.colors", 14.0);
5247 widgets::colored_label(
5248 ui,
5249 colors,
5250 "labels.color.green",
5251 "Green",
5252 color(111, 203, 159),
5253 LayoutStyle::new(),
5254 );
5255 widgets::colored_label(
5256 ui,
5257 colors,
5258 "labels.color.yellow",
5259 "Yellow",
5260 color(232, 196, 101),
5261 LayoutStyle::new(),
5262 );
5263 widgets::colored_label(
5264 ui,
5265 colors,
5266 "labels.color.red",
5267 "Red",
5268 color(244, 118, 118),
5269 LayoutStyle::new(),
5270 );
5271
5272 let wrap_row = wrapping_row(ui, body, "labels.wrap.row", 10.0);
5273 let wrap_word = ui.add_child(
5274 wrap_row,
5275 UiNode::container(
5276 "labels.wrap.word.panel",
5277 LayoutStyle::column().with_width(172.0).padding(8.0),
5278 )
5279 .with_visual(UiVisual::panel(
5280 color(18, 23, 31),
5281 Some(StrokeStyle::new(color(47, 59, 74), 1.0)),
5282 4.0,
5283 )),
5284 );
5285 widgets::wrapped_label(
5286 ui,
5287 wrap_word,
5288 "labels.wrap.word",
5289 "Word wrapping keeps this sentence readable in a narrow box.",
5290 TextWrap::Word,
5291 LayoutStyle::new().with_width_percent(1.0),
5292 );
5293 let wrap_glyph = ui.add_child(
5294 wrap_row,
5295 UiNode::container(
5296 "labels.wrap.glyph.panel",
5297 LayoutStyle::column().with_width(172.0).padding(8.0),
5298 )
5299 .with_visual(UiVisual::panel(
5300 color(18, 23, 31),
5301 Some(StrokeStyle::new(color(47, 59, 74), 1.0)),
5302 4.0,
5303 )),
5304 );
5305 widgets::wrapped_label(
5306 ui,
5307 wrap_glyph,
5308 "labels.wrap.glyph",
5309 "LongIdentifierWithoutSpaces",
5310 TextWrap::Glyph,
5311 LayoutStyle::new().with_width_percent(1.0),
5312 );
5313
5314 let links = wrapping_row(ui, body, "labels.links", 12.0);
5315 widgets::link(
5316 ui,
5317 links,
5318 "labels.link",
5319 "Internal action",
5320 widgets::LinkOptions::default()
5321 .visited(state.label_link_visited)
5322 .with_action("labels.link"),
5323 );
5324 widgets::hyperlink(
5325 ui,
5326 links,
5327 "labels.hyperlink",
5328 "Open docs.rs",
5329 "https://docs.rs/operad",
5330 widgets::LinkOptions::default()
5331 .visited(state.label_hyperlink_visited)
5332 .with_action("labels.hyperlink"),
5333 );
5334 if state.label_link_status != "No link action yet" {
5335 widgets::label(
5336 ui,
5337 body,
5338 "labels.status",
5339 format!("Last action: {}", state.label_link_status),
5340 text(12.0, color(154, 166, 184)),
5341 LayoutStyle::new().with_width_percent(1.0),
5342 );
5343 }
5344}
5345
5346fn buttons(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5347 let body = section(ui, parent, "buttons", "Buttons");
5348 let primary_row = wrapping_row(ui, body, "buttons.row", 10.0);
5349 button(
5350 ui,
5351 primary_row,
5352 "button.default",
5353 "Default",
5354 "button.default",
5355 button_visual(38, 46, 58),
5356 );
5357 button(
5358 ui,
5359 primary_row,
5360 "button.primary",
5361 "Primary",
5362 "button.primary",
5363 button_visual(48, 112, 184),
5364 );
5365 button(
5366 ui,
5367 primary_row,
5368 "button.secondary",
5369 "Secondary",
5370 "button.secondary",
5371 button_visual(58, 78, 96),
5372 );
5373 button(
5374 ui,
5375 primary_row,
5376 "button.destructive",
5377 "Destructive",
5378 "button.destructive",
5379 button_visual(157, 65, 73),
5380 );
5381 let mut disabled = widgets::ButtonOptions::new(LayoutStyle::size(92.0, 32.0));
5382 disabled.enabled = false;
5383 disabled.visual = button_visual(40, 44, 52);
5384 disabled.text_style = text(13.0, color(138, 146, 158));
5385 widgets::button(ui, primary_row, "button.disabled", "Disabled", disabled);
5386 let second_row = wrapping_row(ui, body, "buttons.row.options", 10.0);
5387 button(
5388 ui,
5389 second_row,
5390 "button.momentary",
5391 "Press only",
5392 "button.default",
5393 button_visual(42, 50, 62),
5394 );
5395 let toggle_visual = if state.toggle_button {
5396 button_visual(48, 112, 184)
5397 } else {
5398 button_visual(42, 50, 62)
5399 };
5400 let mut toggle =
5401 widgets::ButtonOptions::new(LayoutStyle::size(112.0, 32.0)).with_action("button.toggle");
5402 toggle.visual = toggle_visual;
5403 toggle.hovered_visual = Some(readable_button_hover_visual(toggle_visual));
5404 toggle.pressed_visual = Some(adjusted_button_visual(toggle_visual, -34));
5405 toggle.pressed_hovered_visual = Some(adjusted_button_visual(toggle_visual, -18));
5406 toggle.accessibility_label = Some("Toggle button state".to_owned());
5407 toggle.text_style = text(13.0, color(246, 249, 252));
5408 let toggle_button = widgets::button(
5409 ui,
5410 second_row,
5411 "button.toggle",
5412 if state.toggle_button {
5413 "Toggle on"
5414 } else {
5415 "Toggle off"
5416 },
5417 toggle,
5418 );
5419 mark_as_toggle_button(ui, toggle_button, state.toggle_button);
5420 let mut forced_pressed = widgets::ButtonOptions::new(LayoutStyle::size(112.0, 32.0));
5421 forced_pressed.pressed = true;
5422 forced_pressed.visual = button_visual(42, 50, 62);
5423 forced_pressed.hovered_visual = Some(button_visual(62, 74, 92));
5424 forced_pressed.pressed_visual = Some(button_visual(38, 82, 136));
5425 forced_pressed.pressed_hovered_visual = Some(button_visual(62, 126, 196));
5426 forced_pressed.text_style = text(13.0, color(246, 249, 252));
5427 widgets::button(
5428 ui,
5429 second_row,
5430 "button.state.pressed",
5431 "Pressed",
5432 forced_pressed,
5433 );
5434 let helper_row = wrapping_row(ui, body, "buttons.row.helpers", 10.0);
5435 widgets::small_button(
5436 ui,
5437 helper_row,
5438 "button.small",
5439 "Small",
5440 widgets::ButtonOptions::default().with_action("button.small"),
5441 );
5442 widgets::icon_button(
5443 ui,
5444 helper_row,
5445 "button.icon",
5446 icon_image(BuiltInIcon::Settings),
5447 "Settings",
5448 widgets::ButtonOptions::default().with_action("button.icon"),
5449 );
5450 widgets::image_button(
5451 ui,
5452 helper_row,
5453 "button.image",
5454 icon_image(BuiltInIcon::Folder),
5455 "Folder",
5456 widgets::ButtonOptions::default().with_action("button.image"),
5457 );
5458 widgets::reset_button(
5459 ui,
5460 helper_row,
5461 "button.reset",
5462 state.toggle_button,
5463 widgets::ButtonOptions::default().with_action("button.reset"),
5464 );
5465 widgets::toggle_button(
5466 ui,
5467 helper_row,
5468 "button.toggle_helper",
5469 "Toggle helper",
5470 state.toggle_button,
5471 widgets::ButtonOptions::default().with_action("button.toggle"),
5472 );
5473 widgets::label(
5474 ui,
5475 body,
5476 "buttons.last",
5477 format!("Last pressed: {}", state.last_button),
5478 text(12.0, color(154, 166, 184)),
5479 LayoutStyle::new().with_width_percent(1.0),
5480 );
5481}
5482
5483fn mark_as_toggle_button(ui: &mut UiDocument, button: UiNodeId, pressed: bool) {
5484 if let Some(accessibility) = ui.node_mut(button).accessibility_mut() {
5485 accessibility.role = AccessibilityRole::ToggleButton;
5486 accessibility.pressed = Some(pressed);
5487 }
5488}
5489
5490fn checkbox(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5491 let body =
5492 section_with_min_viewport(ui, parent, "checkbox", "Checkbox", UiSize::new(300.0, 0.0));
5493 widgets::label(
5494 ui,
5495 body,
5496 "checkbox.states.title",
5497 "States",
5498 text(12.0, color(166, 176, 190)),
5499 LayoutStyle::new().with_width_percent(1.0),
5500 );
5501 let options = widgets::CheckboxOptions::default()
5502 .with_action("checkbox.enabled")
5503 .with_text_style(text(13.0, color(222, 228, 238)))
5504 .with_accessibility_hint("Toggle the shared checkbox demo value");
5505 widgets::checkbox(
5506 ui,
5507 body,
5508 "checkbox.enabled",
5509 "Toggle me",
5510 state.checked,
5511 options,
5512 );
5513 widgets::checkbox(
5514 ui,
5515 body,
5516 "checkbox.unchecked_sample",
5517 "Unchecked",
5518 false,
5519 widgets::CheckboxOptions::default().with_text_style(text(13.0, color(222, 228, 238))),
5520 );
5521 widgets::checkbox(
5522 ui,
5523 body,
5524 "checkbox.checked_sample",
5525 "Checked",
5526 true,
5527 widgets::CheckboxOptions::default().with_text_style(text(13.0, color(222, 228, 238))),
5528 );
5529 widgets::checkbox_with_state(
5530 ui,
5531 body,
5532 "checkbox.indeterminate_sample",
5533 "Indeterminate",
5534 widgets::CheckboxState::Indeterminate,
5535 widgets::CheckboxOptions::default()
5536 .with_indeterminate_support(true)
5537 .with_text_style(text(13.0, color(222, 228, 238))),
5538 );
5539 widgets::checkbox(
5540 ui,
5541 body,
5542 "checkbox.disabled",
5543 "Disabled",
5544 true,
5545 widgets::CheckboxOptions::default()
5546 .disabled()
5547 .with_text_style(text(13.0, color(128, 138, 154))),
5548 );
5549
5550 widgets::separator(
5551 ui,
5552 body,
5553 "checkbox.options.separator",
5554 widgets::SeparatorOptions::default(),
5555 );
5556 widgets::label(
5557 ui,
5558 body,
5559 "checkbox.options.title",
5560 "Options",
5561 text(12.0, color(166, 176, 190)),
5562 LayoutStyle::new().with_width_percent(1.0),
5563 );
5564 widgets::checkbox(
5565 ui,
5566 body,
5567 "checkbox.large",
5568 "Larger box and hit target",
5569 state.checked,
5570 widgets::CheckboxOptions::default()
5571 .with_action("checkbox.large")
5572 .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(36.0))
5573 .with_box_size(UiSize::new(22.0, 22.0))
5574 .with_gap(10.0)
5575 .with_text_style(text(13.0, color(222, 228, 238))),
5576 );
5577 widgets::checkbox(
5578 ui,
5579 body,
5580 "checkbox.custom_color",
5581 "Custom check color",
5582 state.checked,
5583 widgets::CheckboxOptions::default()
5584 .with_action("checkbox.custom_color")
5585 .with_check_color(color(111, 203, 159))
5586 .with_checked_box_visual(UiVisual::panel(
5587 color(29, 68, 50),
5588 Some(StrokeStyle::new(color(111, 203, 159), 1.0)),
5589 3.0,
5590 ))
5591 .with_text_style(text(13.0, color(222, 228, 238))),
5592 );
5593 widgets::checkbox(
5594 ui,
5595 body,
5596 "checkbox.image_check",
5597 "Operad logo PNG check image",
5598 state.checked,
5599 widgets::CheckboxOptions::default()
5600 .with_action("checkbox.image_check")
5601 .with_box_size(UiSize::new(72.0, 72.0))
5602 .with_gap(14.0)
5603 .with_check_image(ImageContent::from(ImageHandle::app(
5604 SHOWCASE_USER_IMAGE_KEY,
5605 )))
5606 .with_checked_box_visual(UiVisual::panel(
5607 color(47, 39, 90),
5608 Some(StrokeStyle::new(color(156, 124, 255), 1.0)),
5609 3.0,
5610 ))
5611 .with_text_style(text(13.0, color(222, 228, 238))),
5612 );
5613 widgets::checkbox(
5614 ui,
5615 body,
5616 "checkbox.compact_gap",
5617 "Compact gap",
5618 state.checked,
5619 widgets::CheckboxOptions::default()
5620 .with_action("checkbox.compact_gap")
5621 .with_gap(4.0)
5622 .with_text_style(text(13.0, color(222, 228, 238))),
5623 );
5624 widgets::checkbox_with_state(
5625 ui,
5626 body,
5627 "checkbox.indeterminate",
5628 "Tri-state cycle",
5629 state.checkbox_indeterminate,
5630 widgets::CheckboxOptions::default()
5631 .with_action("checkbox.indeterminate")
5632 .with_indeterminate_support(true)
5633 .with_box_size(UiSize::new(22.0, 22.0))
5634 .with_checked_box_visual(UiVisual::panel(
5635 color(42, 53, 70),
5636 Some(StrokeStyle::new(color(108, 180, 255), 1.0)),
5637 3.0,
5638 ))
5639 .with_text_style(text(13.0, color(222, 228, 238))),
5640 );
5641}
5642
5643fn toggles(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5644 let body = section_with_min_viewport(
5645 ui,
5646 parent,
5647 "toggles",
5648 "Radio and toggles",
5649 UiSize::new(390.0, 0.0),
5650 );
5651 widgets::label(
5652 ui,
5653 body,
5654 "toggles.radio.title",
5655 "Radio",
5656 text(12.0, color(166, 176, 190)),
5657 LayoutStyle::new().with_width_percent(1.0),
5658 );
5659 let radio_button_options = widgets::RadioButtonOptions::default()
5660 .with_text_style(text(13.0, color(222, 228, 238)))
5661 .with_outer_size(UiSize::new(18.0, 18.0))
5662 .with_dot_radius(4.5);
5663 let radio_options = [
5664 widgets::RadioOption::new("foo", "Foo").with_action("toggles.radio.foo"),
5665 widgets::RadioOption::new("bar", "Bar").with_action("toggles.radio.bar"),
5666 widgets::RadioOption::new("baz", "Baz").with_action("toggles.radio.baz"),
5667 widgets::RadioOption::new("disabled", "Disabled").enabled(false),
5668 ];
5669 let mut radio_group_options = widgets::RadioGroupOptions::default();
5670 radio_group_options.button_options = radio_button_options;
5671 widgets::radio_group(
5672 ui,
5673 body,
5674 "toggles.radio_group",
5675 &radio_options,
5676 Some(state.radio_choice),
5677 radio_group_options,
5678 );
5679 widgets::label(
5680 ui,
5681 body,
5682 "toggles.radio.options_note",
5683 "Custom indicator and no-label option",
5684 text(12.0, color(166, 176, 190)),
5685 LayoutStyle::new().with_width_percent(1.0),
5686 );
5687 let radio_examples = wrapping_row(ui, body, "toggles.radio_examples", 10.0);
5688 widgets::radio_button(
5689 ui,
5690 radio_examples,
5691 "toggles.radio_custom",
5692 "Foo",
5693 true,
5694 widgets::RadioButtonOptions::default()
5695 .with_action("toggles.radio.foo")
5696 .with_outer_visual(UiVisual::panel(
5697 color(33, 38, 48),
5698 Some(StrokeStyle::new(color(162, 128, 255), 1.0)),
5699 6.0,
5700 ))
5701 .with_selected_outer_visual(UiVisual::panel(
5702 color(55, 38, 112),
5703 Some(StrokeStyle::new(color(183, 148, 255), 1.0)),
5704 6.0,
5705 ))
5706 .with_dot_color(color(255, 206, 99))
5707 .with_outer_size(UiSize::new(18.0, 18.0))
5708 .with_dot_radius(4.0)
5709 .with_text_style(text(13.0, color(222, 228, 238))),
5710 );
5711 widgets::radio_button(
5712 ui,
5713 radio_examples,
5714 "toggles.radio_no_label",
5715 "",
5716 state.radio_choice == "bar",
5717 widgets::RadioButtonOptions::default()
5718 .with_action("toggles.radio.bar")
5719 .accessibility_label("No-label radio option")
5720 .with_outer_size(UiSize::new(24.0, 24.0))
5721 .with_dot_radius(6.0),
5722 );
5723
5724 widgets::separator(
5725 ui,
5726 body,
5727 "toggles.switch.separator",
5728 widgets::SeparatorOptions::default(),
5729 );
5730 widgets::label(
5731 ui,
5732 body,
5733 "toggles.switch.title",
5734 "Switches",
5735 text(12.0, color(166, 176, 190)),
5736 LayoutStyle::new().with_width_percent(1.0),
5737 );
5738 widgets::toggle_switch(
5739 ui,
5740 body,
5741 "toggles.switch",
5742 "Label 1",
5743 ext_widgets::ToggleValue::from(state.switch_enabled),
5744 widgets::ToggleSwitchOptions::default()
5745 .with_action("toggles.switch")
5746 .with_text_style(text(13.0, color(222, 228, 238))),
5747 );
5748 widgets::toggle_switch(
5749 ui,
5750 body,
5751 "toggles.mixed",
5752 "Label 2",
5753 state.mixed_switch,
5754 widgets::ToggleSwitchOptions::default()
5755 .with_action("toggles.mixed")
5756 .with_text_style(text(13.0, color(222, 228, 238))),
5757 );
5758 widgets::label(
5759 ui,
5760 body,
5761 "toggles.switch.options_note",
5762 "Track color, thumb shape, length, disabled, and no-label variants",
5763 text(12.0, color(166, 176, 190)),
5764 LayoutStyle::new().with_width_percent(1.0),
5765 );
5766 let switch_examples = wrapping_row(ui, body, "toggles.switch_examples", 10.0);
5767 widgets::toggle_switch(
5768 ui,
5769 switch_examples,
5770 "toggles.switch_custom",
5771 "Label 3",
5772 ext_widgets::ToggleValue::from(state.switch_enabled),
5773 widgets::ToggleSwitchOptions::default()
5774 .with_action("toggles.switch")
5775 .with_track_size(UiSize::new(74.0, 24.0))
5776 .with_thumb_size(UiSize::new(28.0, 18.0))
5777 .with_track_visual(UiVisual::panel(color(47, 53, 66), None, 4.0))
5778 .with_on_track_visual(UiVisual::panel(color(91, 65, 158), None, 4.0))
5779 .with_thumb_visual(UiVisual::panel(
5780 color(255, 205, 90),
5781 Some(StrokeStyle::new(color(255, 236, 171), 1.0)),
5782 3.0,
5783 ))
5784 .with_text_style(text(13.0, color(222, 228, 238))),
5785 );
5786 widgets::toggle_switch(
5787 ui,
5788 switch_examples,
5789 "toggles.switch_no_label",
5790 "",
5791 ext_widgets::ToggleValue::from(state.switch_enabled),
5792 widgets::ToggleSwitchOptions::default()
5793 .with_action("toggles.switch")
5794 .accessibility_label("No-label switch")
5795 .with_track_size(UiSize::new(54.0, 26.0))
5796 .with_thumb_size(UiSize::new(22.0, 22.0))
5797 .with_on_track_visual(UiVisual::panel(color(30, 106, 84), None, 13.0)),
5798 );
5799 widgets::toggle_switch(
5800 ui,
5801 switch_examples,
5802 "toggles.switch_disabled",
5803 "Disabled",
5804 ext_widgets::ToggleValue::Off,
5805 widgets::ToggleSwitchOptions::default()
5806 .enabled(false)
5807 .with_text_style(text(13.0, color(128, 138, 154))),
5808 );
5809
5810 widgets::separator(
5811 ui,
5812 body,
5813 "toggles.theme.separator",
5814 widgets::SeparatorOptions::default(),
5815 );
5816 widgets::label(
5817 ui,
5818 body,
5819 "toggles.theme.title",
5820 "Theme",
5821 text(12.0, color(166, 176, 190)),
5822 LayoutStyle::new().with_width_percent(1.0),
5823 );
5824 widgets::theme_preference_buttons(
5825 ui,
5826 body,
5827 "toggles.theme_buttons",
5828 state.theme_preference,
5829 widgets::ThemePreferenceButtonsOptions::default().with_action_prefix("toggles.theme"),
5830 );
5831 widgets::theme_preference_switch(
5832 ui,
5833 body,
5834 "toggles.theme_switch",
5835 state.theme_preference,
5836 widgets::ThemePreferenceSwitchOptions::default().with_action("theme.preference.dark"),
5837 );
5838}
5839
5840fn slider(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5841 let body = section(ui, parent, "slider", "Slider");
5842 widgets::label(
5843 ui,
5844 body,
5845 "slider.note",
5846 "Click a slider value to edit it with the keyboard.",
5847 text(12.0, color(166, 176, 190)),
5848 LayoutStyle::new().with_width_percent(1.0),
5849 );
5850
5851 let value_row = row(ui, body, "slider.value.row", 10.0);
5852 let options = slider_options(state, 180.0).with_value_edit_action("slider.value");
5853 let slider_unit = state.slider_value_spec().normalize(state.slider);
5854 widgets::slider(
5855 ui,
5856 value_row,
5857 "slider.value",
5858 slider_unit,
5859 0.0..1.0,
5860 options.clone(),
5861 );
5862 slider_number_input(
5863 ui,
5864 value_row,
5865 "slider.value_text",
5866 &state.slider_value_text,
5867 FocusedTextInput::SliderValue,
5868 state,
5869 86.0,
5870 );
5871 widgets::label(
5872 ui,
5873 value_row,
5874 "slider.value.label",
5875 "f64 demo slider",
5876 text(12.0, color(186, 198, 216)),
5877 LayoutStyle::new().with_width_percent(1.0),
5878 );
5879
5880 widgets::label(
5881 ui,
5882 body,
5883 "slider.precision",
5884 format!(
5885 "Displayed value: {} Full precision: {:.6}",
5886 widgets::slider::format_slider_value(state.slider),
5887 state.slider
5888 ),
5889 text(11.0, color(154, 166, 184)),
5890 LayoutStyle::new().with_width_percent(1.0),
5891 );
5892
5893 divider(ui, body, "slider.divider.range");
5894 widgets::label(
5895 ui,
5896 body,
5897 "slider.range.label",
5898 "Slider range",
5899 text(12.0, color(220, 228, 238)),
5900 LayoutStyle::new().with_width_percent(1.0),
5901 );
5902 let left_row = row(ui, body, "slider.range.left.row", 10.0);
5903 let left_options = widgets::SliderOptions::default()
5904 .with_layout(
5905 LayoutStyle::new()
5906 .with_width(180.0)
5907 .with_height(24.0)
5908 .with_flex_shrink(0.0),
5909 )
5910 .with_value_edit_action("slider.range_left");
5911 widgets::slider(
5912 ui,
5913 left_row,
5914 "slider.range_left",
5915 state.slider_left,
5916 0.0..state.slider_right.max(1.0),
5917 left_options,
5918 );
5919 slider_number_input(
5920 ui,
5921 left_row,
5922 "slider.left_text",
5923 &state.slider_left_text,
5924 FocusedTextInput::SliderRangeLeft,
5925 state,
5926 96.0,
5927 );
5928 widgets::label(
5929 ui,
5930 left_row,
5931 "slider.range.left.label",
5932 "left",
5933 text(12.0, color(186, 198, 216)),
5934 LayoutStyle::new().with_width(46.0),
5935 );
5936 let right_row = row(ui, body, "slider.range.right.row", 10.0);
5937 let right_options = widgets::SliderOptions::default()
5938 .with_layout(
5939 LayoutStyle::new()
5940 .with_width(180.0)
5941 .with_height(24.0)
5942 .with_flex_shrink(0.0),
5943 )
5944 .with_value_edit_action("slider.range_right");
5945 widgets::slider(
5946 ui,
5947 right_row,
5948 "slider.range_right",
5949 state.slider_right,
5950 (state.slider_left + 1.0)..10000.0,
5951 right_options,
5952 );
5953 slider_number_input(
5954 ui,
5955 right_row,
5956 "slider.right_text",
5957 &state.slider_right_text,
5958 FocusedTextInput::SliderRangeRight,
5959 state,
5960 96.0,
5961 );
5962 widgets::label(
5963 ui,
5964 right_row,
5965 "slider.range.right.label",
5966 "right",
5967 text(12.0, color(186, 198, 216)),
5968 LayoutStyle::new().with_width(46.0),
5969 );
5970
5971 divider(ui, body, "slider.divider.trailing");
5972 let trailing_row = row(ui, body, "slider.trailing.row", 8.0);
5973 slider_checkbox_with_layout(
5974 ui,
5975 trailing_row,
5976 "slider.trailing",
5977 "Trailing color",
5978 state.slider_trailing_color,
5979 LayoutStyle::new()
5980 .with_width(142.0)
5981 .with_height(30.0)
5982 .with_flex_shrink(0.0),
5983 );
5984 ext_widgets::color_edit_button(
5985 ui,
5986 trailing_row,
5987 "slider.trailing_color_button",
5988 state.slider_trailing_picker.value(),
5989 color_square_button_options("slider.trailing_color_button")
5990 .with_format(ext_widgets::ColorValueFormat::Rgb)
5991 .accessibility_label("Pick trailing slider color"),
5992 );
5993 widgets::label(
5994 ui,
5995 trailing_row,
5996 "slider.trailing_color_button.label",
5997 "Track color",
5998 text(12.0, color(186, 198, 216)),
5999 LayoutStyle::new().with_width(78.0),
6000 );
6001 if state.slider_trailing_picker_open {
6002 ext_widgets::color_picker(
6003 ui,
6004 body,
6005 "slider.trailing_picker",
6006 &state.slider_trailing_picker,
6007 ext_widgets::ColorPickerOptions::default()
6008 .with_label("Trailing slider color")
6009 .with_action_prefix("slider.trailing_picker"),
6010 );
6011 }
6012 let thumb_color_row = row(ui, body, "slider.thumb_color.row", 8.0);
6013 widgets::label(
6014 ui,
6015 thumb_color_row,
6016 "slider.thumb_color.label",
6017 "Thumb color",
6018 text(12.0, color(166, 176, 190)),
6019 LayoutStyle::new().with_width(142.0),
6020 );
6021 ext_widgets::color_edit_button(
6022 ui,
6023 thumb_color_row,
6024 "slider.thumb_color_button",
6025 state.slider_thumb_picker.value(),
6026 color_square_button_options("slider.thumb_color_button")
6027 .with_format(ext_widgets::ColorValueFormat::Rgb)
6028 .accessibility_label("Pick slider thumb color"),
6029 );
6030 if state.slider_thumb_picker_open {
6031 ext_widgets::color_picker(
6032 ui,
6033 body,
6034 "slider.thumb_picker",
6035 &state.slider_thumb_picker,
6036 ext_widgets::ColorPickerOptions::default()
6037 .with_label("Slider thumb color")
6038 .with_action_prefix("slider.thumb_picker"),
6039 );
6040 }
6041 let thumb_row = row(ui, body, "slider.thumb.row", 8.0);
6042 widgets::label(
6043 ui,
6044 thumb_row,
6045 "slider.thumb.label",
6046 "Thumb",
6047 text(12.0, color(166, 176, 190)),
6048 LayoutStyle::new().with_width(64.0),
6049 );
6050 choice_button(
6051 ui,
6052 thumb_row,
6053 "slider.thumb.circle",
6054 "Circle",
6055 state.slider_thumb_shape == SliderThumbChoice::Circle,
6056 );
6057 choice_button(
6058 ui,
6059 thumb_row,
6060 "slider.thumb.square",
6061 "Square",
6062 state.slider_thumb_shape == SliderThumbChoice::Square,
6063 );
6064 choice_button(
6065 ui,
6066 thumb_row,
6067 "slider.thumb.rectangle",
6068 "Rectangle",
6069 state.slider_thumb_shape == SliderThumbChoice::Rectangle,
6070 );
6071 slider_checkbox(
6072 ui,
6073 body,
6074 "slider.steps",
6075 "Use steps",
6076 state.slider_use_steps,
6077 );
6078 let step_row = row(ui, body, "slider.step.row", 10.0);
6079 widgets::label(
6080 ui,
6081 step_row,
6082 "slider.step.label",
6083 "Step value",
6084 text(12.0, color(166, 176, 190)),
6085 LayoutStyle::new().with_width(74.0),
6086 );
6087 slider_number_input(
6088 ui,
6089 step_row,
6090 "slider.step_text",
6091 &state.slider_step_text,
6092 FocusedTextInput::SliderStep,
6093 state,
6094 86.0,
6095 );
6096 slider_checkbox(
6097 ui,
6098 body,
6099 "slider.logarithmic",
6100 "Logarithmic",
6101 state.slider_logarithmic,
6102 );
6103 let clamp_row = row(ui, body, "slider.clamping.row", 8.0);
6104 widgets::label(
6105 ui,
6106 clamp_row,
6107 "slider.clamping.label",
6108 "Clamping",
6109 text(12.0, color(166, 176, 190)),
6110 LayoutStyle::new().with_width(74.0),
6111 );
6112 choice_button(
6113 ui,
6114 clamp_row,
6115 "slider.clamping.never",
6116 "Never",
6117 state.slider_clamping == widgets::SliderClamping::Never,
6118 );
6119 choice_button(
6120 ui,
6121 clamp_row,
6122 "slider.clamping.edits",
6123 "Edits",
6124 state.slider_clamping == widgets::SliderClamping::Edits,
6125 );
6126 choice_button(
6127 ui,
6128 clamp_row,
6129 "slider.clamping.always",
6130 "Always",
6131 state.slider_clamping == widgets::SliderClamping::Always,
6132 );
6133 slider_checkbox(
6134 ui,
6135 body,
6136 "slider.smart_aim",
6137 "Smart aim",
6138 state.slider_smart_aim,
6139 );
6140}
6141
6142fn numeric_inputs(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6143 let body = section(ui, parent, "numeric", "Numeric input");
6144 let unit_domain = state.numeric_unit_domain();
6145 let unit_min = unit_domain.min as f32;
6146 let unit_max = unit_domain.max as f32;
6147 let unit_span = state.numeric_minimum_span();
6148
6149 let value_row = row(ui, body, "numeric.value_row", 10.0);
6150 widgets::label(
6151 ui,
6152 value_row,
6153 "numeric.value_label",
6154 "Value",
6155 text(12.0, color(186, 198, 216)),
6156 LayoutStyle::new()
6157 .with_width(72.0)
6158 .with_height(30.0)
6159 .with_flex_shrink(0.0),
6160 );
6161 numeric_value_editor(ui, value_row, state);
6162
6163 let unit_width = 102.0;
6164 let unit_anchor = ui.add_child(
6165 value_row,
6166 UiNode::container(
6167 "numeric.unit.anchor",
6168 LayoutStyle::new()
6169 .with_width(unit_width)
6170 .with_height(30.0)
6171 .with_flex_shrink(0.0),
6172 ),
6173 );
6174 let unit_options = numeric_unit_options();
6175 let unit_nodes = ext_widgets::dropdown_select(
6176 ui,
6177 unit_anchor,
6178 "numeric.unit",
6179 &unit_options,
6180 &state.numeric_unit,
6181 Some(select_popup(
6182 UiRect::new(0.0, 0.0, unit_width, 30.0),
6183 UiRect::new(0.0, 0.0, 320.0, 260.0),
6184 )),
6185 dropdown_select_options(unit_width, "numeric.unit", "Unit", "Numeric unit"),
6186 );
6187 ui.node_mut(unit_nodes.trigger)
6188 .set_action("numeric.unit.toggle");
6189
6190 divider(ui, body, "numeric.range.divider");
6191 numeric_slider_row(
6192 ui,
6193 body,
6194 "numeric.range_min",
6195 "Min",
6196 state.numeric_range_min,
6197 unit_min..(unit_max - unit_span).max(unit_min),
6198 Some(&state.numeric_range_min_text),
6199 Some(FocusedTextInput::NumericRangeMin),
6200 state,
6201 );
6202 numeric_slider_row(
6203 ui,
6204 body,
6205 "numeric.range_max",
6206 "Max",
6207 state.numeric_range_max,
6208 (unit_min + unit_span).min(unit_max)..unit_max,
6209 Some(&state.numeric_range_max_text),
6210 Some(FocusedTextInput::NumericRangeMax),
6211 state,
6212 );
6213 numeric_slider_row(
6214 ui,
6215 body,
6216 "numeric.sensitivity",
6217 "Drag speed",
6218 state.numeric_sensitivity,
6219 0.25..4.0,
6220 None,
6221 None,
6222 state,
6223 );
6224
6225 widgets::label(
6226 ui,
6227 body,
6228 "numeric.note",
6229 format!(
6230 "Range: {} to {} {}",
6231 state.format_numeric_range_bound(state.numeric_range_min),
6232 state.format_numeric_range_bound(state.numeric_range_max),
6233 numeric_unit_label(&state.numeric_unit)
6234 ),
6235 text(12.0, color(166, 176, 190)),
6236 LayoutStyle::new().with_width_percent(1.0),
6237 );
6238}
6239
6240fn numeric_value_editor(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) -> UiNodeId {
6241 if state.focused_text == Some(FocusedTextInput::NumericValue) {
6242 let mut options = state.text_edit_options(FocusedTextInput::NumericValue);
6243 options.layout = LayoutStyle::new()
6244 .with_width(150.0)
6245 .with_height(30.0)
6246 .with_flex_shrink(0.0);
6247 options.edit_action = Some("numeric.value.edit".into());
6248 return widgets::text_input(ui, parent, "numeric.value", &state.numeric_text, options);
6249 }
6250
6251 let mut options = widgets::DragValueOptions::default()
6252 .with_layout(LayoutStyle::new().with_width(150.0).with_height(30.0))
6253 .with_precision(state.numeric_precision())
6254 .with_range(state.numeric_range())
6255 .with_unit(numeric_unit_format(&state.numeric_unit))
6256 .with_action("numeric.value.drag");
6257 options.text_style = text(12.0, color(230, 236, 246));
6258 options.accessibility_label = Some("Numeric value".to_string());
6259 options.accessibility_hint = Some("Click to edit, or drag horizontally to adjust.".to_string());
6260 widgets::drag_value_input(
6261 ui,
6262 parent,
6263 "numeric.value",
6264 f64::from(state.numeric_value),
6265 options,
6266 )
6267}
6268
6269fn numeric_slider_row(
6270 ui: &mut UiDocument,
6271 parent: UiNodeId,
6272 name: &'static str,
6273 label: &'static str,
6274 value: f32,
6275 range: std::ops::Range<f32>,
6276 input: Option<&TextInputState>,
6277 focused: Option<FocusedTextInput>,
6278 state: &ShowcaseState,
6279) {
6280 let row = row(ui, parent, format!("{name}.row"), 10.0);
6281 widgets::label(
6282 ui,
6283 row,
6284 format!("{name}.label"),
6285 label,
6286 text(12.0, color(186, 198, 216)),
6287 LayoutStyle::new()
6288 .with_width(72.0)
6289 .with_height(28.0)
6290 .with_flex_shrink(0.0),
6291 );
6292 let mut options = widgets::SliderOptions::default()
6293 .with_layout(
6294 LayoutStyle::new()
6295 .with_width(190.0)
6296 .with_height(24.0)
6297 .with_flex_shrink(0.0),
6298 )
6299 .with_value_edit_action(name);
6300 options.accessibility_label = Some(label.to_string());
6301 widgets::slider(ui, row, name, value, range, options);
6302 if let (Some(input), Some(focused)) = (input, focused) {
6303 let mut options = state.text_edit_options(focused);
6304 options.layout = LayoutStyle::new()
6305 .with_width(70.0)
6306 .with_height(28.0)
6307 .with_flex_shrink(0.0);
6308 options.edit_action = Some(format!("{name}.value.edit").into());
6309 widgets::text_input(ui, row, format!("{name}.value"), input, options);
6310 } else {
6311 widgets::label(
6312 ui,
6313 row,
6314 format!("{name}.value"),
6315 format!("{:.2}x", value),
6316 text(12.0, color(230, 236, 246)),
6317 LayoutStyle::new()
6318 .with_width(64.0)
6319 .with_height(28.0)
6320 .with_flex_shrink(0.0),
6321 );
6322 }
6323}
6324
6325fn numeric_unit_options() -> Vec<ext_widgets::SelectOption> {
6326 vec![
6327 ext_widgets::SelectOption::new("px", "Pixels"),
6328 ext_widgets::SelectOption::new("deg", "Degrees"),
6329 ext_widgets::SelectOption::new("turn", "Turns"),
6330 ext_widgets::SelectOption::new("percent", "Percent"),
6331 ]
6332}
6333
6334fn numeric_unit_id(state: &ext_widgets::SelectMenuState) -> &'static str {
6335 match state.selected_index().unwrap_or(0) {
6336 1 => "deg",
6337 2 => "turn",
6338 3 => "percent",
6339 _ => "px",
6340 }
6341}
6342
6343fn numeric_unit_default_range(unit_id: &str) -> ext_widgets::NumericRange {
6344 match unit_id {
6345 "deg" => ext_widgets::NumericRange::new(0.0, 360.0),
6346 "turn" => ext_widgets::NumericRange::new(0.0, 1.0),
6347 "percent" => ext_widgets::NumericRange::new(0.0, 100.0),
6348 _ => ext_widgets::NumericRange::new(0.0, 100.0),
6349 }
6350}
6351
6352fn numeric_unit_label(state: &ext_widgets::SelectMenuState) -> &'static str {
6353 match numeric_unit_id(state) {
6354 "deg" => "deg",
6355 "turn" => "turn",
6356 "percent" => "%",
6357 _ => "px",
6358 }
6359}
6360
6361fn numeric_unit_format(state: &ext_widgets::SelectMenuState) -> ext_widgets::NumericUnitFormat {
6362 match numeric_unit_id(state) {
6363 "deg" => ext_widgets::NumericUnitFormat::default().suffix(" deg"),
6364 "turn" => ext_widgets::NumericUnitFormat::default().suffix(" turn"),
6365 "percent" => ext_widgets::NumericUnitFormat::default().suffix("%"),
6366 _ => ext_widgets::NumericUnitFormat::default().suffix(" px"),
6367 }
6368}
6369
6370fn parse_numeric_edit_text(text: &str, unit: &ext_widgets::NumericUnitFormat) -> Option<f32> {
6371 let value = unit.strip_affixes(text).parse::<f32>().ok()?;
6372 value.is_finite().then_some(value)
6373}
6374
6375fn selection_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6376 let body = section_with_min_viewport(
6377 ui,
6378 parent,
6379 "selection",
6380 "Select controls",
6381 UiSize::new(250.0, 0.0),
6382 );
6383 let select_width = 220.0;
6384 let select_options = select_options();
6385
6386 widgets::label(
6387 ui,
6388 body,
6389 "selection.dropdown.title",
6390 "Dropdown select",
6391 text(12.0, color(166, 176, 190)),
6392 LayoutStyle::new().with_width_percent(1.0),
6393 );
6394 let dropdown_anchor = ui.add_child(
6395 body,
6396 UiNode::container(
6397 "selection.dropdown.anchor",
6398 LayoutStyle::new()
6399 .with_width(select_width)
6400 .with_height(30.0),
6401 ),
6402 );
6403 let dropdown_nodes = ext_widgets::dropdown_select(
6404 ui,
6405 dropdown_anchor,
6406 "selection.dropdown",
6407 &select_options,
6408 &state.dropdown,
6409 Some(select_popup(
6410 UiRect::new(0.0, 0.0, select_width, 30.0),
6411 UiRect::new(0.0, 0.0, 320.0, 260.0),
6412 )),
6413 dropdown_select_options(
6414 select_width,
6415 "selection.dropdown",
6416 "Select option",
6417 "Dropdown select",
6418 ),
6419 );
6420 ui.node_mut(dropdown_nodes.trigger)
6421 .set_action("selection.dropdown.toggle");
6422
6423 widgets::label(
6424 ui,
6425 body,
6426 "selection.menu.label",
6427 "Open menu",
6428 text(12.0, color(166, 176, 190)),
6429 LayoutStyle::new().with_width_percent(1.0),
6430 );
6431 ext_widgets::select_menu(
6432 ui,
6433 body,
6434 "selection.select_menu",
6435 &select_options,
6436 &state.select_menu,
6437 select_menu_options(select_width)
6438 .with_action_prefix("selection.menu")
6439 .with_row_height(30.0)
6440 .with_max_visible_rows(4)
6441 .with_selected_visual(UiVisual::panel(color(42, 62, 87), None, 2.0))
6442 .with_active_visual(UiVisual::panel(color(58, 87, 126), None, 2.0)),
6443 );
6444
6445 widgets::label(
6446 ui,
6447 body,
6448 "selection.images.label",
6449 "Image options",
6450 text(12.0, color(166, 176, 190)),
6451 LayoutStyle::new().with_width_percent(1.0),
6452 );
6453 let image_options = select_options_with_images();
6454 ext_widgets::select_menu(
6455 ui,
6456 body,
6457 "selection.image_menu",
6458 &image_options,
6459 &state.image_select_menu,
6460 select_menu_options(select_width)
6461 .with_action_prefix("selection.image_menu")
6462 .with_row_height(32.0)
6463 .with_max_visible_rows(4)
6464 .with_image_size(UiSize::new(16.0, 16.0))
6465 .with_menu_visual(UiVisual::panel(
6466 color(20, 25, 32),
6467 Some(StrokeStyle::new(color(77, 90, 111), 1.0)),
6468 4.0,
6469 ))
6470 .with_active_visual(UiVisual::panel(color(59, 70, 94), None, 2.0))
6471 .with_selected_visual(UiVisual::panel(color(36, 74, 91), None, 2.0)),
6472 );
6473}
6474
6475fn select_menu_options(width: f32) -> ext_widgets::SelectMenuOptions {
6476 ext_widgets::SelectMenuOptions::default()
6477 .with_width(width)
6478 .with_portal(UiPortalTarget::Parent)
6479 .with_text_style(text(13.0, color(226, 232, 242)))
6480 .with_disabled_text_style(text(13.0, color(138, 148, 164)))
6481}
6482
6483fn dropdown_select_options(
6484 width: f32,
6485 action_prefix: &str,
6486 placeholder: &str,
6487 accessibility_label: &str,
6488) -> ext_widgets::DropdownSelectOptions {
6489 ext_widgets::DropdownSelectOptions::default()
6490 .with_trigger_layout(
6491 LayoutStyle::row()
6492 .with_width(width)
6493 .with_height(30.0)
6494 .with_align_items(taffy::prelude::AlignItems::Center)
6495 .with_justify_content(taffy::prelude::JustifyContent::FlexStart)
6496 .gap(6.0)
6497 .padding(6.0),
6498 )
6499 .with_text_style(text(13.0, color(226, 232, 242)))
6500 .with_placeholder(placeholder)
6501 .with_accessibility_label(accessibility_label)
6502 .with_menu(select_menu_options(width).with_action_prefix(action_prefix))
6503}
6504
6505fn select_popup(anchor: UiRect, viewport: UiRect) -> ext_widgets::AnchoredPopup {
6506 ext_widgets::AnchoredPopup::new(
6507 anchor,
6508 viewport,
6509 ext_widgets::PopupPlacement::default()
6510 .with_offset(0.0)
6511 .with_viewport_margin(0.0),
6512 )
6513}
6514
6515#[allow(clippy::field_reassign_with_default)]
6516fn text_input(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6517 let body = section(ui, parent, "text_input", "Text input");
6518 let mut options = TextInputOptions::default();
6519 options.placeholder = "Type here".to_string();
6520 options.layout = LayoutStyle::new().with_width(300.0).with_height(36.0);
6521 options.text_style = text(13.0, color(230, 236, 246));
6522 options.placeholder_style = text(13.0, color(144, 156, 174));
6523 options.edit_action = Some("text.input.edit".into());
6524 options.focused = state.focused_text == Some(FocusedTextInput::Editable);
6525 options.caret_visible = caret_visible(state.caret_phase);
6526 widgets::text_input(ui, body, "text.input", &state.text, options);
6527
6528 let mut selectable_options = TextInputOptions::default();
6529 selectable_options.layout = LayoutStyle::new().with_width(360.0).with_height(36.0);
6530 selectable_options.text_style = text(13.0, color(196, 210, 230));
6531 selectable_options.read_only = true;
6532 selectable_options.selectable = true;
6533 selectable_options.focused = state.focused_text == Some(FocusedTextInput::Selectable);
6534 selectable_options.edit_action = Some("text.selectable.edit".into());
6535 selectable_options.caret_visible = caret_visible(state.caret_phase);
6536 widgets::text_input(
6537 ui,
6538 body,
6539 "text.selectable",
6540 &state.selectable_text,
6541 selectable_options,
6542 );
6543
6544 let mut singleline = TextInputOptions::default();
6545 singleline.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
6546 singleline.text_style = text(13.0, color(230, 236, 246));
6547 singleline.placeholder = "Single line".to_string();
6548 singleline.edit_action = Some("text.singleline.edit".into());
6549 singleline.focused = state.focused_text == Some(FocusedTextInput::Singleline);
6550 singleline.caret_visible = caret_visible(state.caret_phase);
6551 widgets::singleline_text_input(
6552 ui,
6553 body,
6554 "text.singleline",
6555 &state.singleline_text,
6556 singleline,
6557 );
6558
6559 let mut multiline = TextInputOptions::default();
6560 multiline.layout = LayoutStyle::new().with_width(360.0).with_height(72.0);
6561 multiline.text_style = text(13.0, color(230, 236, 246));
6562 multiline.edit_action = Some("text.multiline.edit".into());
6563 multiline.focused = state.focused_text == Some(FocusedTextInput::Multiline);
6564 multiline.caret_visible = caret_visible(state.caret_phase);
6565 widgets::multiline_text_input(ui, body, "text.multiline", &state.multiline_text, multiline);
6566
6567 let mut area = TextInputOptions::default();
6568 area.layout = LayoutStyle::new().with_width(360.0).with_height(66.0);
6569 area.text_style = text(13.0, color(230, 236, 246));
6570 area.edit_action = Some("text.area.edit".into());
6571 area.focused = state.focused_text == Some(FocusedTextInput::TextArea);
6572 area.caret_visible = caret_visible(state.caret_phase);
6573 widgets::text_area(ui, body, "text.area", &state.text_area_text, area);
6574
6575 let mut code = TextInputOptions::default();
6576 code.layout = LayoutStyle::new().with_width(360.0).with_height(88.0);
6577 code.edit_action = Some("text.code_editor.edit".into());
6578 code.focused = state.focused_text == Some(FocusedTextInput::CodeEditor);
6579 code.caret_visible = caret_visible(state.caret_phase);
6580 widgets::code_editor(ui, body, "text.code_editor", &state.code_editor_text, code);
6581
6582 let mut search = TextInputOptions::default();
6583 search.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
6584 search.text_style = text(13.0, color(230, 236, 246));
6585 search.edit_action = Some("text.search.edit".into());
6586 search.focused = state.focused_text == Some(FocusedTextInput::Search);
6587 search.caret_visible = caret_visible(state.caret_phase);
6588 widgets::search_input(ui, body, "text.search", &state.search_text, search);
6589
6590 let mut password = TextInputOptions::default();
6591 password.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
6592 password.text_style = text(13.0, color(230, 236, 246));
6593 password.edit_action = Some("text.password.edit".into());
6594 password.focused = state.focused_text == Some(FocusedTextInput::Password);
6595 password.caret_visible = caret_visible(state.caret_phase);
6596 widgets::password_input(ui, body, "text.password", &state.password_text, password);
6597}
6598
6599fn date_picker(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6600 let body =
6601 section_with_min_viewport(ui, parent, "date", "Date picker", UiSize::new(284.0, 0.0));
6602 let mode = wrapping_row(ui, body, "date.mode", 8.0);
6603 choice_button(
6604 ui,
6605 mode,
6606 "date.mode.single",
6607 "Single",
6608 state.date_mode == DateDemoMode::Single,
6609 );
6610 choice_button(
6611 ui,
6612 mode,
6613 "date.mode.range",
6614 "Range",
6615 state.date_mode == DateDemoMode::Range,
6616 );
6617 choice_button(
6618 ui,
6619 mode,
6620 "date.mode.week",
6621 "Week",
6622 state.date_mode == DateDemoMode::Week,
6623 );
6624
6625 let controls = wrapping_row(ui, body, "date.options", 8.0);
6626 choice_button(
6627 ui,
6628 controls,
6629 "date.week.sunday",
6630 "Sun first",
6631 state.date.first_weekday == ext_widgets::Weekday::Sunday,
6632 );
6633 choice_button(
6634 ui,
6635 controls,
6636 "date.week.monday",
6637 "Mon first",
6638 state.date.first_weekday == ext_widgets::Weekday::Monday,
6639 );
6640 let bounds = row(ui, body, "date.bounds_options", 8.0);
6641 let mut bounds_button =
6642 widgets::ButtonOptions::new(LayoutStyle::new().with_width(92.0).with_height(28.0))
6643 .with_action("date.bounds.toggle");
6644 bounds_button.visual = if state.date.min.is_some() || state.date.max.is_some() {
6645 button_visual(48, 112, 184)
6646 } else {
6647 button_visual(38, 46, 58)
6648 };
6649 bounds_button.hovered_visual = Some(button_visual(65, 86, 106));
6650 bounds_button.text_style = text(12.0, color(238, 244, 252));
6651 widgets::button(
6652 ui,
6653 bounds,
6654 "date.bounds.toggle",
6655 "May bounds",
6656 bounds_button,
6657 );
6658 let mut clear_options =
6659 widgets::ButtonOptions::new(LayoutStyle::new().with_width(64.0).with_height(28.0))
6660 .with_action("date.clear");
6661 clear_options.visual = button_visual(38, 46, 58);
6662 clear_options.hovered_visual = Some(button_visual(65, 86, 106));
6663 clear_options.text_style = text(12.0, color(238, 244, 252));
6664 widgets::button(ui, bounds, "date.clear", "Clear", clear_options);
6665
6666 match state.date_mode {
6667 DateDemoMode::Single => {
6668 ext_widgets::date_picker(
6669 ui,
6670 body,
6671 "date.picker",
6672 &state.date,
6673 ext_widgets::DatePickerOptions::default().with_action_prefix("date"),
6674 );
6675 }
6676 DateDemoMode::Range | DateDemoMode::Week => {
6677 ext_widgets::date_range_picker(
6678 ui,
6679 body,
6680 "date.picker",
6681 &state.date_range,
6682 ext_widgets::DateRangePickerOptions::default().with_action_prefix("date"),
6683 );
6684 }
6685 }
6686 widgets::label(
6687 ui,
6688 body,
6689 "date.mode_status",
6690 date_mode_status(state),
6691 text(11.0, color(154, 166, 184)),
6692 LayoutStyle::new().with_width_percent(1.0),
6693 );
6694 widgets::label(
6695 ui,
6696 body,
6697 "date.selected",
6698 format!("Selected: {}", date_selection_summary(state)),
6699 text(11.0, color(154, 166, 184)),
6700 LayoutStyle::new().with_width_percent(1.0),
6701 );
6702}
6703
6704fn date_selection_summary(state: &ShowcaseState) -> String {
6705 match state.date_mode {
6706 DateDemoMode::Single => state
6707 .date
6708 .selected
6709 .map_or_else(|| "None".to_string(), CalendarDate::iso_string),
6710 DateDemoMode::Range | DateDemoMode::Week => state.date_range.range.map_or_else(
6711 || "None".to_string(),
6712 ext_widgets::CalendarDateRange::iso_string,
6713 ),
6714 }
6715}
6716
6717fn date_mode_status(state: &ShowcaseState) -> String {
6718 match state.date_mode {
6719 DateDemoMode::Single => "Single date".to_string(),
6720 DateDemoMode::Range => match state.date_range.pending_start {
6721 Some(start) => format!("Range start: {}", start.iso_string()),
6722 None => "Custom date range".to_string(),
6723 },
6724 DateDemoMode::Week => "Whole-week range".to_string(),
6725 }
6726}
6727
6728fn color_picker(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6729 let body = section(ui, parent, "color", "Color picker");
6730 let button_row = row(ui, body, "color.button.row", 8.0);
6731 widgets::label(
6732 ui,
6733 button_row,
6734 "color.button.label",
6735 "Button opens color picker",
6736 text(12.0, color(196, 210, 230)),
6737 LayoutStyle::new()
6738 .with_width(0.0)
6739 .with_flex_grow(1.0)
6740 .with_flex_shrink(1.0),
6741 );
6742 ext_widgets::color_swatch_button(
6743 ui,
6744 button_row,
6745 "color.button.open",
6746 state.color.value(),
6747 color_square_button_options("color.button.open").accessibility_label("Open color picker"),
6748 );
6749 if state.color_picker_button_open {
6750 ext_widgets::color_picker(
6751 ui,
6752 body,
6753 "color.button_picker",
6754 &state.color,
6755 ext_widgets::ColorPickerOptions::default()
6756 .with_label("Button color")
6757 .with_action_prefix("color.button_picker"),
6758 );
6759 divider(ui, body, "color.button.divider");
6760 }
6761 ext_widgets::color_picker(
6762 ui,
6763 body,
6764 "color.picker",
6765 &state.color,
6766 ext_widgets::ColorPickerOptions::default()
6767 .with_action_prefix("color")
6768 .with_copy_hex_action("color.copy_hex")
6769 .with_copy_hex_label("Copy"),
6770 );
6771 if let Some(hex) = &state.color_copied_hex {
6772 widgets::label(
6773 ui,
6774 body,
6775 "color.copied",
6776 format!("Copied {hex}"),
6777 text(11.0, color(154, 166, 184)),
6778 LayoutStyle::new().with_width_percent(1.0),
6779 );
6780 }
6781}
6782
6783fn menu_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6784 let body = section_with_min_viewport(
6785 ui,
6786 parent,
6787 "menus",
6788 "Menu controls",
6789 UiSize::new(320.0, 0.0),
6790 );
6791 let menus = menu_bar_menus(state.menu_autosave, state.menu_grid);
6792 let active_items = state
6793 .menu_bar
6794 .open_menu
6795 .and_then(|index| menus.get(index))
6796 .map(|menu| menu.items.clone())
6797 .unwrap_or_default();
6798 widgets::label(
6799 ui,
6800 body,
6801 "menus.menu_bar.title",
6802 "Menu bar",
6803 text(12.0, color(166, 176, 190)),
6804 LayoutStyle::new().with_width_percent(1.0),
6805 );
6806 ext_widgets::menu_bar(
6807 ui,
6808 body,
6809 "menus.menu_bar",
6810 &menus,
6811 &state.menu_bar,
6812 None,
6813 ext_widgets::MenuBarOptions::default().with_action_prefix("menus.bar"),
6814 );
6815
6816 if !active_items.is_empty() {
6817 let menu_columns = ui.add_child(
6818 body,
6819 UiNode::container(
6820 "menus.menu_columns",
6821 Layout::row()
6822 .size(LayoutSize::new(
6823 LayoutDimension::Auto,
6824 LayoutDimension::Auto,
6825 ))
6826 .align_items(LayoutAlignment::Start)
6827 .gap(LayoutGap::points(4.0, 4.0))
6828 .flex(0.0, 0.0, LayoutDimension::Auto)
6829 .to_layout_style(),
6830 ),
6831 );
6832 ext_widgets::menu_list(
6833 ui,
6834 menu_columns,
6835 "menus.menu_list",
6836 &active_items,
6837 state.menu_bar.active_item,
6838 ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
6839 );
6840 if let Some(active_item) = state.menu_bar.active_item {
6841 if let Some(children) = active_items
6842 .get(active_item)
6843 .and_then(|item| item.children())
6844 {
6845 let submenu_column = ui.add_child(
6846 menu_columns,
6847 UiNode::container(
6848 "menus.submenu_column",
6849 Layout::column()
6850 .size(LayoutSize::new(
6851 LayoutDimension::Auto,
6852 LayoutDimension::Auto,
6853 ))
6854 .gap(LayoutGap::points(0.0, 0.0))
6855 .flex(0.0, 0.0, LayoutDimension::Auto)
6856 .to_layout_style(),
6857 ),
6858 );
6859 let offset = menu_item_top_offset(&active_items, active_item);
6860 if offset > 0.0 {
6861 widgets::spacer(
6862 ui,
6863 submenu_column,
6864 "menus.submenu_spacer",
6865 LayoutStyle::new().with_width(1.0).with_height(offset),
6866 );
6867 }
6868 ext_widgets::menu_list(
6869 ui,
6870 submenu_column,
6871 "menus.submenu",
6872 children,
6873 Some(0),
6874 ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
6875 );
6876 }
6877 }
6878 }
6879 divider(ui, body, "menus.divider.buttons");
6880 widgets::label(
6881 ui,
6882 body,
6883 "menus.buttons.title",
6884 "Menu buttons",
6885 text(12.0, color(166, 176, 190)),
6886 LayoutStyle::new().with_width_percent(1.0),
6887 );
6888 let button_row = row(ui, body, "menus.buttons", 10.0);
6889 let button_items = menu_items(state.menu_autosave);
6890 ext_widgets::menu_button(
6891 ui,
6892 button_row,
6893 "menus.menu_button",
6894 "Menu button",
6895 &button_items,
6896 &state.menu_button,
6897 None,
6898 ext_widgets::MenuButtonOptions::default().with_action("menus.menu_button"),
6899 );
6900 ext_widgets::image_text_menu_button(
6901 ui,
6902 button_row,
6903 "menus.image_text_menu_button",
6904 "Image text",
6905 icon_image(BuiltInIcon::Folder),
6906 &button_items,
6907 &state.image_text_menu_button,
6908 None,
6909 ext_widgets::MenuButtonOptions::default().with_action("menus.image_text_menu_button"),
6910 );
6911 ext_widgets::image_menu_button(
6912 ui,
6913 button_row,
6914 "menus.image_menu_button",
6915 icon_image(BuiltInIcon::Settings),
6916 &button_items,
6917 &state.image_menu_button,
6918 None,
6919 ext_widgets::MenuButtonOptions::default().with_action("menus.image_menu_button"),
6920 );
6921 if state.menu_button.open || state.image_text_menu_button.open || state.image_menu_button.open {
6922 let active = state
6923 .menu_button
6924 .navigation
6925 .active_path
6926 .first()
6927 .copied()
6928 .or_else(|| {
6929 state
6930 .image_text_menu_button
6931 .navigation
6932 .active_path
6933 .first()
6934 .copied()
6935 })
6936 .or_else(|| {
6937 state
6938 .image_menu_button
6939 .navigation
6940 .active_path
6941 .first()
6942 .copied()
6943 });
6944 ext_widgets::menu_list(
6945 ui,
6946 body,
6947 "menus.button_menu",
6948 &button_items,
6949 active,
6950 ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
6951 );
6952 }
6953
6954 divider(ui, body, "menus.divider.context");
6955 widgets::label(
6956 ui,
6957 body,
6958 "menus.context.title",
6959 "Context menu",
6960 text(12.0, color(166, 176, 190)),
6961 LayoutStyle::new().with_width_percent(1.0),
6962 );
6963 let context_row = row(ui, body, "menus.context.controls", 8.0);
6964 button(
6965 ui,
6966 context_row,
6967 "menus.context.open",
6968 "Open context",
6969 "menus.context.open",
6970 button_visual(48, 112, 184),
6971 );
6972 button(
6973 ui,
6974 context_row,
6975 "menus.context.close",
6976 "Close",
6977 "menus.context.close",
6978 button_visual(58, 78, 96),
6979 );
6980 let mut context_options =
6981 ext_widgets::MenuListOptions::default().with_action_prefix("menus.context");
6982 context_options.width = 240.0;
6983 context_options.max_visible_rows = 6;
6984 let _ = ext_widgets::context_menu(
6985 ui,
6986 parent,
6987 "menus.context_menu",
6988 &button_items,
6989 &state.context_menu,
6990 UiRect::new(0.0, 0.0, 560.0, 460.0),
6991 ext_widgets::PopupPlacement::default(),
6992 context_options,
6993 );
6994}
6995
6996fn menu_demo_context_anchor() -> UiPoint {
6997 UiPoint::new(30.0, 390.0)
6998}
6999
7000fn command_palette(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7001 let body = section_with_min_viewport(
7002 ui,
7003 parent,
7004 "command_palette",
7005 "Command palette",
7006 UiSize::new(240.0, 72.0),
7007 );
7008 let items = command_palette_items_with_history(&state.command_history);
7009 let mut trigger_options =
7010 widgets::ButtonOptions::new(LayoutStyle::new().with_width(150.0).with_height(32.0))
7011 .with_action(if state.command_palette_open {
7012 "command_palette.close"
7013 } else {
7014 "command_palette.open"
7015 })
7016 .with_accessibility_label(if state.command_palette_open {
7017 "Close command palette"
7018 } else {
7019 "Open command palette"
7020 });
7021 trigger_options.visual = button_visual(48, 112, 184);
7022 trigger_options.hovered_visual = Some(button_visual(62, 126, 196));
7023 trigger_options.pressed_visual = Some(button_visual(38, 82, 136));
7024 trigger_options.text_style = text(13.0, color(246, 249, 252));
7025 widgets::button(
7026 ui,
7027 body,
7028 "command_palette.open",
7029 if state.command_palette_open {
7030 "Close palette"
7031 } else {
7032 "Open palette"
7033 },
7034 trigger_options,
7035 );
7036 widgets::label(
7037 ui,
7038 body,
7039 "command_palette.last",
7040 format!("Last command: {}", state.last_command),
7041 text(12.0, color(154, 166, 184)),
7042 LayoutStyle::new().with_width_percent(1.0),
7043 );
7044 if state.command_palette_open {
7045 let palette_width = command_palette_popup_width(state.last_desktop_size);
7046 let mut options =
7047 ext_widgets::CommandPaletteOptions::default().with_action_prefix("command_palette");
7048 options.width = palette_width;
7049 options.row_height = 44.0;
7050 options.max_visible_rows = 5;
7051 options.text_style = text(13.0, color(238, 244, 252));
7052 options.muted_text_style = text(12.0, color(166, 178, 196));
7053 options.z_index = SHOWCASE_WINDOW_Z_MAX.saturating_add(40);
7054 ext_widgets::command_palette(
7055 ui,
7056 body,
7057 "command_palette.panel",
7058 &items,
7059 &state.command_palette,
7060 Some(command_palette_popup(
7061 state.last_desktop_size,
7062 palette_width,
7063 )),
7064 options,
7065 );
7066 }
7067}
7068
7069fn command_palette_popup_width(desktop_size: UiSize) -> f32 {
7070 (desktop_size.width - 48.0).clamp(320.0, 560.0)
7071}
7072
7073fn command_palette_popup(desktop_size: UiSize, width: f32) -> ext_widgets::AnchoredPopup {
7074 let viewport = UiRect::new(0.0, 0.0, desktop_size.width, desktop_size.height);
7075 let x = ((desktop_size.width - width) * 0.5).max(12.0);
7076 let y = (desktop_size.height * 0.12).clamp(48.0, 96.0);
7077 ext_widgets::AnchoredPopup::new(
7078 UiRect::new(x, y, width, 0.0),
7079 viewport,
7080 ext_widgets::PopupPlacement::new(
7081 ext_widgets::PopupSide::Bottom,
7082 ext_widgets::PopupAlign::Center,
7083 )
7084 .with_offset(0.0)
7085 .with_flip(false)
7086 .with_viewport_margin(12.0),
7087 )
7088}
7089
7090#[allow(clippy::field_reassign_with_default)]
7091fn progress_indicator(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7092 let body = section(ui, parent, "progress", "Progress indicator");
7093 let animated = smooth_loop(state.progress_phase * 0.85, 0.0) * 100.0;
7094 let mut progress = ext_widgets::ProgressIndicatorOptions::default();
7095 progress.layout = LayoutStyle::new().with_width_percent(1.0).with_height(10.0);
7096 progress.accessibility_label = Some("Progress".to_string());
7097 ext_widgets::progress_indicator(
7098 ui,
7099 body,
7100 "progress.primary",
7101 ext_widgets::ProgressIndicatorValue::percent(animated),
7102 progress,
7103 );
7104 let compact_value = smooth_loop(state.progress_phase * 1.15, 0.7) * 100.0;
7105 let mut compact = ext_widgets::ProgressIndicatorOptions::default();
7106 compact.layout = LayoutStyle::new().with_width_percent(1.0).with_height(6.0);
7107 compact.fill_visual = UiVisual::panel(color(111, 203, 159), None, 3.0);
7108 ext_widgets::progress_indicator(
7109 ui,
7110 body,
7111 "progress.compact",
7112 ext_widgets::ProgressIndicatorValue::percent(compact_value),
7113 compact,
7114 );
7115 let warning_value = smooth_loop(state.progress_phase * 0.65, 1.4) * 100.0;
7116 let mut warning = ext_widgets::ProgressIndicatorOptions::default();
7117 warning.layout = LayoutStyle::new().with_width_percent(1.0).with_height(14.0);
7118 warning.fill_visual = UiVisual::panel(color(232, 186, 88), None, 4.0);
7119 ext_widgets::progress_indicator(
7120 ui,
7121 body,
7122 "progress.warning",
7123 ext_widgets::ProgressIndicatorValue::percent(warning_value),
7124 warning,
7125 );
7126 let logged_value =
7127 (state.progress_loading_elapsed / PROGRESS_LOGGED_DURATION_SECONDS * 100.0).min(100.0);
7128 let logged_entries = progress_demo_logs(logged_value);
7129 progress_loading_panel(
7130 ui,
7131 body,
7132 "progress.logged",
7133 logged_value,
7134 &logged_entries,
7135 state,
7136 );
7137 let spinner_row = row(ui, body, "progress.spinner.row", 8.0);
7138 widgets::spinner(
7139 ui,
7140 spinner_row,
7141 "progress.spinner",
7142 widgets::SpinnerOptions::default()
7143 .with_phase(state.progress_phase)
7144 .with_accessibility_label("Loading spinner"),
7145 );
7146 widgets::label(
7147 ui,
7148 spinner_row,
7149 "progress.spinner.label",
7150 "Spinner",
7151 text(12.0, color(196, 210, 230)),
7152 LayoutStyle::new().with_width_percent(1.0),
7153 );
7154}
7155
7156fn progress_loading_panel(
7157 ui: &mut UiDocument,
7158 parent: UiNodeId,
7159 name: &'static str,
7160 progress_value: f32,
7161 logs: &[ext_widgets::ProgressLogEntry],
7162 state: &ShowcaseState,
7163) {
7164 let panel = ui.add_child(
7165 parent,
7166 UiNode::container(
7167 name,
7168 LayoutStyle::column()
7169 .with_width_percent(1.0)
7170 .with_padding(10.0)
7171 .with_gap(8.0)
7172 .with_flex_shrink(0.0),
7173 )
7174 .with_visual(UiVisual::panel(
7175 color(17, 21, 27),
7176 Some(StrokeStyle::new(color(70, 82, 101), 1.0)),
7177 4.0,
7178 ))
7179 .with_accessibility(
7180 AccessibilityMeta::new(AccessibilityRole::Group).label("Loading progress with logs"),
7181 ),
7182 );
7183
7184 let progress_row = row(ui, panel, "progress.logged.progress_row", 8.0);
7185 let progress_slot = ui.add_child(
7186 progress_row,
7187 UiNode::container(
7188 "progress.logged.progress_slot",
7189 LayoutStyle::new()
7190 .with_width(0.0)
7191 .with_height(30.0)
7192 .with_flex_grow(1.0)
7193 .with_flex_shrink(1.0),
7194 ),
7195 );
7196 let mut progress = ext_widgets::ProgressIndicatorOptions::default();
7197 progress.layout = LayoutStyle::new()
7198 .with_width_percent(1.0)
7199 .with_height(10.0)
7200 .with_flex_grow(1.0)
7201 .with_flex_shrink(1.0);
7202 progress.fill_visual = UiVisual::panel(color(111, 203, 159), None, 3.0);
7203 progress.accessibility_label = Some("Logged loading progress".to_string());
7204 ext_widgets::progress_indicator(
7205 ui,
7206 progress_slot,
7207 "progress.logged.progress",
7208 ext_widgets::ProgressIndicatorValue::percent(progress_value),
7209 progress,
7210 );
7211 let mut reset = widgets::ButtonOptions::new(
7212 LayoutStyle::new()
7213 .with_width(76.0)
7214 .with_height(30.0)
7215 .with_flex_shrink(0.0),
7216 )
7217 .with_action("progress.logged.reset");
7218 reset.visual = button_visual(38, 46, 58);
7219 reset.hovered_visual = Some(button_visual(65, 86, 106));
7220 reset.pressed_visual = Some(button_visual(34, 54, 84));
7221 reset.text_style = text(12.0, color(238, 244, 252));
7222 widgets::button(ui, progress_row, "progress.logged.reset", "Reset", reset);
7223
7224 let log_scroll = progress_log_scroll_state(
7225 state.progress_logs_scroll.offset().y,
7226 logs.len(),
7227 state.progress_logs_follow_tail,
7228 );
7229 let logs_node = ui.add_child(
7230 panel,
7231 UiNode::container(
7232 "progress.logged.logs",
7233 LayoutStyle::column()
7234 .with_width_percent(1.0)
7235 .with_height(PROGRESS_LOG_VIEWPORT_HEIGHT)
7236 .with_flex_shrink(0.0),
7237 )
7238 .with_visual(UiVisual::panel(
7239 color(11, 15, 21),
7240 Some(StrokeStyle::new(color(45, 57, 73), 1.0)),
7241 3.0,
7242 ))
7243 .with_scroll(ScrollAxes::VERTICAL)
7244 .with_accessibility(
7245 AccessibilityMeta::new(AccessibilityRole::List)
7246 .label("Loading logs")
7247 .value(format!("{} entries", logs.len())),
7248 ),
7249 );
7250 {
7251 let node = ui.node_mut(logs_node);
7252 node.set_action("progress.logged.logs.scroll");
7253 node.set_scroll(log_scroll);
7254 }
7255
7256 if logs.is_empty() {
7257 ui.add_child(
7258 logs_node,
7259 UiNode::text(
7260 "progress.logged.logs.empty",
7261 "Waiting for log output...",
7262 text(12.0, color(154, 166, 184)),
7263 LayoutStyle::new()
7264 .with_width_percent(1.0)
7265 .with_height(PROGRESS_LOG_ROW_HEIGHT)
7266 .with_padding(4.0)
7267 .with_flex_shrink(0.0),
7268 )
7269 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Status).label("No logs")),
7270 );
7271 } else {
7272 for (index, entry) in logs.iter().enumerate() {
7273 let mut text_style = text(12.0, entry.level.color());
7274 text_style.line_height = 18.0;
7275 ui.add_child(
7276 logs_node,
7277 UiNode::text(
7278 format!("{name}.logs.row.{index}"),
7279 format!("[{}] {}", entry.level.as_str(), entry.message),
7280 text_style,
7281 LayoutStyle::new()
7282 .with_width_percent(1.0)
7283 .with_height(PROGRESS_LOG_ROW_HEIGHT)
7284 .with_padding(4.0)
7285 .with_flex_shrink(0.0),
7286 )
7287 .with_accessibility(
7288 AccessibilityMeta::new(AccessibilityRole::ListItem).label(format!(
7289 "{}: {}",
7290 entry.level.as_str(),
7291 entry.message
7292 )),
7293 ),
7294 );
7295 }
7296 }
7297}
7298
7299fn progress_log_scroll_state(
7300 saved_offset_y: f32,
7301 log_count: usize,
7302 follow_tail: bool,
7303) -> operad::ScrollState {
7304 let content_height = log_count.max(1) as f32 * PROGRESS_LOG_ROW_HEIGHT;
7305 let max_offset = (content_height - PROGRESS_LOG_VIEWPORT_HEIGHT).max(0.0);
7306 let offset_y = if follow_tail {
7307 max_offset
7308 } else {
7309 saved_offset_y.min(max_offset)
7310 };
7311 operad::ScrollState::new(ScrollAxes::VERTICAL)
7312 .with_sizes(
7313 UiSize::new(8.0, PROGRESS_LOG_VIEWPORT_HEIGHT),
7314 UiSize::new(8.0, content_height),
7315 )
7316 .with_offset(UiPoint::new(0.0, offset_y))
7317}
7318
7319fn progress_demo_logs(progress: f32) -> Vec<ext_widgets::ProgressLogEntry> {
7320 let mut logs = vec![
7321 ext_widgets::ProgressLogEntry::info("Initializing renderer"),
7322 ext_widgets::ProgressLogEntry::info("Mounting content archive"),
7323 ];
7324 if progress >= 24.0 {
7325 logs.push(ext_widgets::ProgressLogEntry::success(
7326 "Compiled material shaders",
7327 ));
7328 }
7329 if progress >= 48.0 {
7330 logs.push(ext_widgets::ProgressLogEntry::info("Decoded texture atlas"));
7331 }
7332 if progress >= 72.0 {
7333 logs.push(ext_widgets::ProgressLogEntry::warning(
7334 "Optional cloud profile is still pending",
7335 ));
7336 }
7337 if progress >= 96.0 {
7338 logs.push(ext_widgets::ProgressLogEntry::success("Ready"));
7339 }
7340 logs
7341}
7342
7343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7344enum EaseCurveKind {
7345 Quad,
7346 Cubic,
7347 Quart,
7348 Expo,
7349 Back,
7350 Elastic,
7351 Bounce,
7352}
7353
7354impl EaseCurveKind {
7355 fn id(self) -> &'static str {
7356 match self {
7357 Self::Quad => "quad",
7358 Self::Cubic => "cubic",
7359 Self::Quart => "quart",
7360 Self::Expo => "expo",
7361 Self::Back => "back",
7362 Self::Elastic => "elastic",
7363 Self::Bounce => "bounce",
7364 }
7365 }
7366
7367 fn base_label(self) -> &'static str {
7368 match self {
7369 Self::Quad => "quad",
7370 Self::Cubic => "cubic",
7371 Self::Quart => "quart",
7372 Self::Expo => "expo",
7373 Self::Back => "back",
7374 Self::Elastic => "elastic",
7375 Self::Bounce => "bounce",
7376 }
7377 }
7378
7379 fn sample_out(self, progress: f32) -> f32 {
7380 let t = unit(progress);
7381 match self {
7382 Self::Quad => 1.0 - (1.0 - t).powi(2),
7383 Self::Cubic => 1.0 - (1.0 - t).powi(3),
7384 Self::Quart => 1.0 - (1.0 - t).powi(4),
7385 Self::Expo => {
7386 if t >= 1.0 {
7387 1.0
7388 } else {
7389 1.0 - 2.0_f32.powf(-10.0 * t)
7390 }
7391 }
7392 Self::Back => {
7393 let c1 = 1.70158;
7394 let c3 = c1 + 1.0;
7395 1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
7396 }
7397 Self::Elastic => {
7398 if t <= 0.0 {
7399 0.0
7400 } else if t >= 1.0 {
7401 1.0
7402 } else {
7403 let period = (2.0 * std::f32::consts::PI) / 3.0;
7404 2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * period).sin() + 1.0
7405 }
7406 }
7407 Self::Bounce => ease_out_bounce(t),
7408 }
7409 }
7410}
7411
7412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7413enum EaseDirection {
7414 In,
7415 Out,
7416}
7417
7418impl EaseDirection {
7419 fn label_prefix(self) -> &'static str {
7420 match self {
7421 Self::In => "Ease in",
7422 Self::Out => "Ease out",
7423 }
7424 }
7425}
7426
7427#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7428struct EasingFunction {
7429 direction: EaseDirection,
7430 kind: EaseCurveKind,
7431}
7432
7433impl EasingFunction {
7434 const fn new(direction: EaseDirection, kind: EaseCurveKind) -> Self {
7435 Self { direction, kind }
7436 }
7437
7438 fn label(self) -> String {
7439 format!(
7440 "{} {}",
7441 self.direction.label_prefix(),
7442 self.kind.base_label()
7443 )
7444 }
7445
7446 fn sample(self, progress: f32) -> f32 {
7447 let t = unit(progress);
7448 match self.direction {
7449 EaseDirection::In => 1.0 - self.kind.sample_out(1.0 - t),
7450 EaseDirection::Out => self.kind.sample_out(t),
7451 }
7452 }
7453}
7454
7455fn ease_out_bounce(t: f32) -> f32 {
7456 let n1 = 7.5625;
7457 let d1 = 2.75;
7458 if t < 1.0 / d1 {
7459 n1 * t * t
7460 } else if t < 2.0 / d1 {
7461 let t = t - 1.5 / d1;
7462 n1 * t * t + 0.75
7463 } else if t < 2.5 / d1 {
7464 let t = t - 2.25 / d1;
7465 n1 * t * t + 0.9375
7466 } else {
7467 let t = t - 2.625 / d1;
7468 n1 * t * t + 0.984375
7469 }
7470}
7471
7472fn easing_options(direction: EaseDirection) -> Vec<ext_widgets::SelectOption> {
7473 [
7474 EaseCurveKind::Quad,
7475 EaseCurveKind::Cubic,
7476 EaseCurveKind::Quart,
7477 EaseCurveKind::Expo,
7478 EaseCurveKind::Back,
7479 EaseCurveKind::Elastic,
7480 EaseCurveKind::Bounce,
7481 ]
7482 .into_iter()
7483 .map(|kind| {
7484 ext_widgets::SelectOption::new(kind.id(), EasingFunction::new(direction, kind).label())
7485 })
7486 .collect()
7487}
7488
7489fn selected_easing(
7490 state: &ext_widgets::SelectMenuState,
7491 direction: EaseDirection,
7492) -> EasingFunction {
7493 let options = easing_options(direction);
7494 let kind = match state.selected_id(&options) {
7495 Some("quad") => EaseCurveKind::Quad,
7496 Some("quart") => EaseCurveKind::Quart,
7497 Some("expo") => EaseCurveKind::Expo,
7498 Some("back") => EaseCurveKind::Back,
7499 Some("elastic") => EaseCurveKind::Elastic,
7500 Some("bounce") => EaseCurveKind::Bounce,
7501 _ => EaseCurveKind::Cubic,
7502 };
7503 EasingFunction::new(direction, kind)
7504}
7505
7506fn animation_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7507 let body = section(ui, parent, "animation", "Animation");
7508
7509 if let Some(section) = animation_section(
7510 ui,
7511 body,
7512 "animation.timed",
7513 "Timed playback",
7514 state.animation_timed_expanded,
7515 ) {
7516 let live_stage = animation_stage(ui, section, "animation.live.stage");
7517 let live_amount = smooth_loop(state.progress_phase * 1.65, 0.0);
7518 let live_values = animation_blend_machine(
7519 ANIMATION_INPUT_PROGRESS,
7520 live_amount,
7521 UiPoint::new(220.0, 0.0),
7522 0.88,
7523 1.10,
7524 1.0,
7525 )
7526 .with_bool_input("looping", true)
7527 .values();
7528 ui.add_child(
7529 live_stage,
7530 UiNode::scene(
7531 "animation.live.orb",
7532 animation_orb_primitives(
7533 color(108, 180, 255),
7534 ANIMATION_ORB_SIZE * live_values.scale,
7535 UiPoint::new(
7536 28.0 + live_values.translate.x,
7537 37.0 + live_values.translate.y,
7538 ),
7539 ),
7540 animation_scene_layout(),
7541 )
7542 .with_accessibility(
7543 AccessibilityMeta::new(AccessibilityRole::Image).label("Looping orb"),
7544 ),
7545 );
7546 }
7547
7548 if let Some(section) = animation_section(
7549 ui,
7550 body,
7551 "animation.scrub",
7552 "Scrubbed input",
7553 state.animation_scrub_expanded,
7554 ) {
7555 let scrub_row = row(ui, section, "animation.scrub.row", 10.0);
7556 widgets::slider(
7557 ui,
7558 scrub_row,
7559 "animation.scrub.slider",
7560 state.animation_scrub,
7561 0.0..1.0,
7562 widgets::SliderOptions::default()
7563 .with_layout(
7564 LayoutStyle::new()
7565 .with_width(200.0)
7566 .with_height(28.0)
7567 .with_flex_shrink(0.0),
7568 )
7569 .with_value_edit_action("animation.scrub"),
7570 );
7571 widgets::label(
7572 ui,
7573 scrub_row,
7574 "animation.scrub.value",
7575 format!("{:.0}%", state.animation_scrub * 100.0),
7576 text(12.0, color(186, 198, 216)),
7577 LayoutStyle::new().with_width_percent(1.0),
7578 );
7579 let scrub_stage = animation_stage(ui, section, "animation.scrub.stage");
7580 let scrub_values = animation_blend_machine(
7581 ANIMATION_INPUT_SCRUB,
7582 state.animation_scrub,
7583 UiPoint::new(220.0, 0.0),
7584 0.82,
7585 1.14,
7586 1.0,
7587 )
7588 .values();
7589 ui.add_child(
7590 scrub_stage,
7591 UiNode::scene(
7592 "animation.scrub.shape",
7593 animation_morph_shape_primitives(
7594 color(111, 203, 159),
7595 ANIMATION_SHAPE_SIZE * scrub_values.scale,
7596 UiPoint::new(
7597 28.0 + scrub_values.translate.x,
7598 37.0 + scrub_values.translate.y,
7599 ),
7600 scrub_values.morph,
7601 ),
7602 animation_scene_layout(),
7603 )
7604 .with_accessibility(
7605 AccessibilityMeta::new(AccessibilityRole::Image).label("Scrubbed morphing shape"),
7606 ),
7607 );
7608 }
7609
7610 if let Some(section) = animation_section(
7611 ui,
7612 body,
7613 "animation.state",
7614 "Boolean input transition",
7615 state.animation_state_expanded,
7616 ) {
7617 let state_row = row(ui, section, "animation.state.row", 10.0);
7618 let mut open = widgets::ButtonOptions::new(
7619 LayoutStyle::new()
7620 .with_width(92.0)
7621 .with_height(30.0)
7622 .with_flex_shrink(0.0),
7623 )
7624 .with_action("animation.open");
7625 open.visual = if state.animation_open {
7626 button_visual(48, 112, 184)
7627 } else {
7628 button_visual(38, 46, 58)
7629 };
7630 open.hovered_visual = Some(button_visual(65, 86, 106));
7631 open.pressed_visual = Some(button_visual(34, 54, 84));
7632 open.text_style = text(12.0, color(238, 244, 252));
7633 widgets::button(
7634 ui,
7635 state_row,
7636 "animation.open",
7637 if state.animation_open {
7638 "Close"
7639 } else {
7640 "Open"
7641 },
7642 open,
7643 );
7644 let open_stage = animation_stage(ui, section, "animation.state.stage");
7645 let panel_offset = if state.animation_open {
7646 UiPoint::new(
7647 ANIMATION_STAGE_MIN_WIDTH - ANIMATION_PANEL_WIDTH - ANIMATION_PANEL_INSET_X,
7648 ANIMATION_PANEL_Y,
7649 )
7650 } else {
7651 UiPoint::new(ANIMATION_PANEL_INSET_X, ANIMATION_PANEL_Y)
7652 };
7653 ui.add_child(
7654 open_stage,
7655 UiNode::scene(
7656 "animation.state.panel",
7657 animation_panel_primitives(panel_offset),
7658 animation_scene_layout(),
7659 )
7660 .with_animation(animation_open_machine(state.animation_open))
7661 .with_accessibility(
7662 AccessibilityMeta::new(AccessibilityRole::Image).label("Open state panel"),
7663 ),
7664 );
7665 }
7666
7667 if let Some(section) = animation_section(
7668 ui,
7669 body,
7670 "animation.interaction",
7671 "Interaction inputs",
7672 state.animation_interaction_expanded,
7673 ) {
7674 let interaction_stage = animation_stage(ui, section, "animation.interaction.stage");
7675 ui.add_child(
7676 interaction_stage,
7677 UiNode::scene(
7678 "animation.interaction.target",
7679 animation_interaction_primitives(
7680 color(176, 126, 230),
7681 ANIMATION_ORB_SIZE,
7682 UiPoint::new(40.0, 37.0),
7683 ),
7684 animation_scene_layout(),
7685 )
7686 .with_input(InputBehavior::BUTTON)
7687 .with_animation(animation_interaction_machine())
7688 .with_accessibility(
7689 AccessibilityMeta::new(AccessibilityRole::Button)
7690 .label("Interaction animation target")
7691 .focusable(),
7692 ),
7693 );
7694 }
7695}
7696
7697fn easing_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7698 let body = section(ui, parent, "easing", "Easing");
7699 let linear_progress = (state.progress_phase * 0.25).rem_euclid(1.0);
7700 easing_curve_demo(
7701 ui,
7702 body,
7703 "easing.in",
7704 "Ease-in functions",
7705 EaseDirection::In,
7706 &state.easing_in,
7707 linear_progress,
7708 );
7709 divider(ui, body, "easing.divider");
7710 easing_curve_demo(
7711 ui,
7712 body,
7713 "easing.out",
7714 "Ease-out functions",
7715 EaseDirection::Out,
7716 &state.easing_out,
7717 linear_progress,
7718 );
7719}
7720
7721fn easing_curve_demo(
7722 ui: &mut UiDocument,
7723 parent: UiNodeId,
7724 name: &'static str,
7725 title: &'static str,
7726 direction: EaseDirection,
7727 state: &ext_widgets::SelectMenuState,
7728 linear_progress: f32,
7729) {
7730 widgets::label(
7731 ui,
7732 parent,
7733 format!("{name}.title"),
7734 title,
7735 text(12.0, color(186, 198, 216)),
7736 LayoutStyle::new().with_width_percent(1.0),
7737 );
7738
7739 let options = easing_options(direction);
7740 let selected = selected_easing(state, direction);
7741 let eased_progress = selected.sample(linear_progress);
7742 let controls = row(ui, parent, format!("{name}.controls"), 10.0);
7743 let dropdown_width = 184.0;
7744 let dropdown_name = format!("{name}.dropdown");
7745 let dropdown_anchor = ui.add_child(
7746 controls,
7747 UiNode::container(
7748 format!("{name}.dropdown.anchor"),
7749 LayoutStyle::new()
7750 .with_width(dropdown_width)
7751 .with_height(30.0)
7752 .with_flex_shrink(0.0),
7753 ),
7754 );
7755 let dropdown_nodes = ext_widgets::dropdown_select(
7756 ui,
7757 dropdown_anchor,
7758 dropdown_name.clone(),
7759 &options,
7760 state,
7761 Some(select_popup(
7762 UiRect::new(0.0, 0.0, dropdown_width, 30.0),
7763 UiRect::new(0.0, 0.0, EASING_STAGE_MIN_WIDTH, 260.0),
7764 )),
7765 dropdown_select_options(
7766 dropdown_width,
7767 dropdown_name.as_str(),
7768 "Ease function",
7769 title,
7770 ),
7771 );
7772 ui.node_mut(dropdown_nodes.trigger)
7773 .set_action(format!("{name}.dropdown.toggle"));
7774 widgets::label(
7775 ui,
7776 controls,
7777 format!("{name}.value"),
7778 format!(
7779 "{:.0}% -> {:.0}%",
7780 linear_progress * 100.0,
7781 eased_progress * 100.0
7782 ),
7783 text(12.0, color(186, 198, 216)),
7784 LayoutStyle::new().with_width_percent(1.0),
7785 );
7786
7787 let stage = easing_stage(ui, parent, format!("{name}.stage"));
7788 ui.add_child(
7789 stage,
7790 UiNode::scene(
7791 format!("{name}.graph"),
7792 easing_curve_primitives(selected, linear_progress),
7793 animation_scene_layout(),
7794 )
7795 .with_accessibility(
7796 AccessibilityMeta::new(AccessibilityRole::Image)
7797 .label(format!("{} curve and looping marker", selected.label())),
7798 ),
7799 );
7800}
7801
7802fn animation_section(
7803 ui: &mut UiDocument,
7804 parent: UiNodeId,
7805 name: &'static str,
7806 title: &'static str,
7807 expanded: bool,
7808) -> Option<UiNodeId> {
7809 let mut options = widgets::CollapsingHeaderOptions::default()
7810 .expanded(expanded)
7811 .with_toggle_action(format!("{name}.toggle"));
7812 options.text_style = text(12.0, color(220, 228, 238));
7813 options.indicator_text_style = text(12.0, color(186, 198, 216));
7814 options.root_visual = UiVisual::panel(
7815 color(17, 22, 29),
7816 Some(StrokeStyle::new(color(48, 58, 72), 1.0)),
7817 6.0,
7818 );
7819 options.header_visual = UiVisual::panel(color(21, 26, 33), None, 0.0);
7820 options.hovered_visual = UiVisual::panel(color(38, 48, 61), None, 0.0);
7821 options.pressed_visual = UiVisual::panel(color(27, 36, 48), None, 0.0);
7822 options.body_layout = LayoutStyle::column()
7823 .with_width_percent(1.0)
7824 .with_padding(10.0)
7825 .with_gap(10.0);
7826 widgets::collapsing_header(ui, parent, name, title, options).body
7827}
7828
7829fn animation_stage(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7830 let layout = LayoutStyle::row()
7831 .with_width_percent(1.0)
7832 .with_height(ANIMATION_STAGE_HEIGHT)
7833 .with_align_items(taffy::prelude::AlignItems::Center)
7834 .with_flex_shrink(0.0);
7835 let layout = operad::layout::with_min_size(
7836 layout,
7837 operad::length(ANIMATION_STAGE_MIN_WIDTH),
7838 operad::length(ANIMATION_STAGE_HEIGHT),
7839 );
7840 ui.add_child(
7841 parent,
7842 UiNode::container(name, layout).with_visual(UiVisual::panel(
7843 color(16, 21, 28),
7844 Some(StrokeStyle::new(color(48, 58, 72), 1.0)),
7845 6.0,
7846 )),
7847 )
7848}
7849
7850fn easing_stage(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7851 let layout = LayoutStyle::row()
7852 .with_width_percent(1.0)
7853 .with_height(EASING_STAGE_HEIGHT)
7854 .with_align_items(taffy::prelude::AlignItems::Center)
7855 .with_flex_shrink(0.0);
7856 let layout = operad::layout::with_min_size(
7857 layout,
7858 operad::length(EASING_STAGE_MIN_WIDTH),
7859 operad::length(EASING_STAGE_HEIGHT),
7860 );
7861 ui.add_child(
7862 parent,
7863 UiNode::container(name, layout).with_visual(UiVisual::panel(
7864 color(16, 21, 28),
7865 Some(StrokeStyle::new(color(48, 58, 72), 1.0)),
7866 6.0,
7867 )),
7868 )
7869}
7870
7871fn animation_scene_layout() -> LayoutStyle {
7872 let layout = LayoutStyle::new()
7873 .with_width_percent(1.0)
7874 .with_height_percent(1.0)
7875 .with_flex_grow(1.0)
7876 .with_flex_shrink(1.0);
7877 operad::layout::with_min_size(layout, operad::length(0.0), operad::length(0.0))
7878}
7879
7880fn easing_curve_primitives(function: EasingFunction, linear_progress: f32) -> Vec<ScenePrimitive> {
7881 let mut primitives = Vec::new();
7882 let graph = UiRect::new(24.0, 24.0, 172.0, 112.0);
7883 primitives.push(ScenePrimitive::Rect(
7884 PaintRect::solid(graph, color(12, 17, 24))
7885 .stroke(AlignedStroke::inside(StrokeStyle::new(
7886 color(58, 70, 88),
7887 1.0,
7888 )))
7889 .corner_radii(CornerRadii::uniform(4.0)),
7890 ));
7891 for index in 1..4 {
7892 let fraction = index as f32 / 4.0;
7893 let x = graph.x + graph.width * fraction;
7894 let y = graph.y + graph.height * fraction;
7895 primitives.push(ScenePrimitive::Line {
7896 from: UiPoint::new(x, graph.y),
7897 to: UiPoint::new(x, graph.y + graph.height),
7898 stroke: StrokeStyle::new(color(29, 38, 50), 1.0),
7899 });
7900 primitives.push(ScenePrimitive::Line {
7901 from: UiPoint::new(graph.x, y),
7902 to: UiPoint::new(graph.x + graph.width, y),
7903 stroke: StrokeStyle::new(color(29, 38, 50), 1.0),
7904 });
7905 }
7906 primitives.push(ScenePrimitive::Line {
7907 from: UiPoint::new(graph.x, graph.y + graph.height),
7908 to: UiPoint::new(graph.x + graph.width, graph.y + graph.height),
7909 stroke: StrokeStyle::new(color(118, 136, 162), 1.0),
7910 });
7911 primitives.push(ScenePrimitive::Line {
7912 from: UiPoint::new(graph.x, graph.y),
7913 to: UiPoint::new(graph.x, graph.y + graph.height),
7914 stroke: StrokeStyle::new(color(118, 136, 162), 1.0),
7915 });
7916
7917 let samples = 40;
7918 let mut previous = None;
7919 for index in 0..=samples {
7920 let t = index as f32 / samples as f32;
7921 let eased = function.sample(t);
7922 let point = UiPoint::new(
7923 graph.x + graph.width * t,
7924 graph.y + graph.height - graph.height * eased.clamp(-0.16, 1.16),
7925 );
7926 if let Some(from) = previous {
7927 primitives.push(ScenePrimitive::Line {
7928 from,
7929 to: point,
7930 stroke: StrokeStyle::new(color(112, 181, 255), 2.0),
7931 });
7932 }
7933 previous = Some(point);
7934 }
7935
7936 let eased_progress = function.sample(linear_progress);
7937 let graph_marker = UiPoint::new(
7938 graph.x + graph.width * linear_progress,
7939 graph.y + graph.height - graph.height * eased_progress.clamp(-0.16, 1.16),
7940 );
7941 primitives.push(ScenePrimitive::Circle {
7942 center: graph_marker,
7943 radius: 5.5,
7944 fill: color(248, 252, 255),
7945 stroke: Some(StrokeStyle::new(color(112, 181, 255), 2.0)),
7946 });
7947
7948 let track = UiRect::new(232.0, 64.0, 96.0, 12.0);
7949 let marker_progress = eased_progress.clamp(-0.10, 1.10);
7950 primitives.push(ScenePrimitive::Rect(
7951 PaintRect::solid(track, color(37, 46, 58)).corner_radii(CornerRadii::uniform(6.0)),
7952 ));
7953 primitives.push(ScenePrimitive::Rect(
7954 PaintRect::solid(
7955 UiRect::new(
7956 track.x,
7957 track.y,
7958 track.width * eased_progress.clamp(0.0, 1.0),
7959 track.height,
7960 ),
7961 color(108, 180, 255),
7962 )
7963 .corner_radii(CornerRadii::uniform(6.0)),
7964 ));
7965 primitives.push(ScenePrimitive::Circle {
7966 center: UiPoint::new(
7967 track.x + track.width * marker_progress,
7968 track.y + track.height * 0.5,
7969 ),
7970 radius: 14.0,
7971 fill: color(112, 181, 255),
7972 stroke: Some(StrokeStyle::new(color(232, 242, 255), 2.0)),
7973 });
7974 primitives.push(ScenePrimitive::Text(
7975 PaintText::new(
7976 function.label(),
7977 UiRect::new(222.0, 98.0, 120.0, 20.0),
7978 text(10.0, color(186, 198, 216)),
7979 )
7980 .horizontal_align(TextHorizontalAlign::Center)
7981 .multiline(false),
7982 ));
7983 primitives
7984}
7985
7986fn animation_blend_machine(
7987 input: &'static str,
7988 value: f32,
7989 translate: UiPoint,
7990 start_scale: f32,
7991 end_scale: f32,
7992 end_opacity: f32,
7993) -> AnimationMachine {
7994 let start_values = AnimatedValues::new(0.45, UiPoint::new(0.0, 0.0), start_scale);
7995 let end_values = AnimatedValues::new(end_opacity, translate, end_scale).with_morph(1.0);
7996 AnimationMachine::new(
7997 vec![
7998 AnimationState::new("start", start_values),
7999 AnimationState::new("end", end_values),
8000 ],
8001 Vec::new(),
8002 "start",
8003 )
8004 .unwrap_or_else(|_| AnimationMachine::single_state("start", start_values))
8005 .with_number_input(input, value)
8006 .with_blend_binding(AnimationBlendBinding::new(input, "start", "end"))
8007}
8008
8009fn animation_open_machine(open: bool) -> AnimationMachine {
8010 let closed_values = AnimatedValues::new(0.35, UiPoint::new(0.0, 0.0), 1.0);
8011 let open_values = AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0);
8012 let fallback_values = if open { open_values } else { closed_values };
8013 AnimationMachine::new(
8014 vec![
8015 AnimationState::new("closed", closed_values),
8016 AnimationState::new("open", open_values),
8017 ],
8018 vec![
8019 AnimationTransition::when(
8020 "closed",
8021 "open",
8022 AnimationCondition::bool(ANIMATION_INPUT_OPEN, true),
8023 0.18,
8024 ),
8025 AnimationTransition::when(
8026 "open",
8027 "closed",
8028 AnimationCondition::bool(ANIMATION_INPUT_OPEN, false),
8029 0.14,
8030 ),
8031 ],
8032 "closed",
8033 )
8034 .unwrap_or_else(|_| AnimationMachine::single_state("closed", fallback_values))
8035 .with_bool_input(ANIMATION_INPUT_OPEN, open)
8036}
8037
8038fn animation_interaction_machine() -> AnimationMachine {
8039 let rest_values = AnimatedValues::new(0.72, UiPoint::new(0.0, 0.0), 1.0);
8040 let right_values = AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0).with_morph(1.0);
8041 AnimationMachine::new(
8042 vec![
8043 AnimationState::new("rest", rest_values),
8044 AnimationState::new("right", right_values),
8045 ],
8046 Vec::new(),
8047 "rest",
8048 )
8049 .unwrap_or_else(|_| AnimationMachine::single_state("rest", rest_values))
8050 .with_number_input(ANIMATION_INPUT_POINTER_NORM_X, 0.0)
8051 .with_blend_binding(AnimationBlendBinding::new(
8052 ANIMATION_INPUT_POINTER_NORM_X,
8053 "rest",
8054 "right",
8055 ))
8056}
8057
8058fn animation_interaction_primitives(
8059 fill: ColorRgba,
8060 size: f32,
8061 offset: UiPoint,
8062) -> Vec<ScenePrimitive> {
8063 vec![
8064 ScenePrimitive::MorphPolygon {
8065 from_points: animation_square_points(size, offset),
8066 to_points: animation_pentagon_points(size, offset),
8067 amount: 0.0,
8068 fill,
8069 stroke: Some(StrokeStyle::new(color(236, 244, 255), 1.0)),
8070 },
8071 ScenePrimitive::Circle {
8072 center: UiPoint::new(offset.x + size * 0.34, offset.y + size * 0.30),
8073 radius: size * 0.10,
8074 fill: color(244, 248, 255),
8075 stroke: None,
8076 },
8077 ]
8078}
8079
8080fn animation_orb_primitives(fill: ColorRgba, size: f32, offset: UiPoint) -> Vec<ScenePrimitive> {
8081 let center = size * 0.5;
8082 let radius = size * 0.44;
8083 vec![
8084 ScenePrimitive::Circle {
8085 center: UiPoint::new(offset.x + center, offset.y + center),
8086 radius,
8087 fill,
8088 stroke: Some(StrokeStyle::new(color(236, 244, 255), 1.0)),
8089 },
8090 ScenePrimitive::Circle {
8091 center: UiPoint::new(offset.x + size * 0.34, offset.y + size * 0.30),
8092 radius: size * 0.12,
8093 fill: color(244, 248, 255),
8094 stroke: None,
8095 },
8096 ]
8097}
8098
8099fn animation_morph_shape_primitives(
8100 fill: ColorRgba,
8101 size: f32,
8102 offset: UiPoint,
8103 amount: f32,
8104) -> Vec<ScenePrimitive> {
8105 vec![ScenePrimitive::MorphPolygon {
8106 from_points: animation_square_points(size, offset),
8107 to_points: animation_pentagon_points(size, offset),
8108 amount,
8109 fill,
8110 stroke: Some(StrokeStyle::new(color(226, 246, 236), 1.0)),
8111 }]
8112}
8113
8114fn animation_square_points(size: f32, offset: UiPoint) -> Vec<UiPoint> {
8115 let inset = size * 0.08;
8116 let left = offset.x + inset;
8117 let top = offset.y + inset;
8118 let right = offset.x + size - inset;
8119 let bottom = offset.y + size - inset;
8120 let center_x = offset.x + size * 0.5;
8121 vec![
8122 UiPoint::new(center_x, top),
8123 UiPoint::new(right, top),
8124 UiPoint::new(right, bottom),
8125 UiPoint::new(left, bottom),
8126 UiPoint::new(left, top),
8127 ]
8128}
8129
8130fn animation_pentagon_points(size: f32, offset: UiPoint) -> Vec<UiPoint> {
8131 let center = size * 0.5;
8132 let radius = size * 0.46;
8133 (0..5)
8134 .map(|index| {
8135 let angle = -std::f32::consts::FRAC_PI_2 + index as f32 * std::f32::consts::TAU / 5.0;
8136 UiPoint::new(
8137 offset.x + center + angle.cos() * radius,
8138 offset.y + center + angle.sin() * radius,
8139 )
8140 })
8141 .collect()
8142}
8143
8144fn animation_panel_primitives(offset: UiPoint) -> Vec<ScenePrimitive> {
8145 vec![ScenePrimitive::Rect(
8146 PaintRect::solid(
8147 UiRect::new(
8148 offset.x,
8149 offset.y,
8150 ANIMATION_PANEL_WIDTH,
8151 ANIMATION_PANEL_HEIGHT,
8152 ),
8153 color(232, 186, 88),
8154 )
8155 .stroke(AlignedStroke::inside(StrokeStyle::new(
8156 color(255, 226, 154),
8157 1.0,
8158 )))
8159 .corner_radii(CornerRadii::uniform(6.0)),
8160 )]
8161}
8162
8163fn list_and_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8164 let body = section_with_min_viewport(
8165 ui,
8166 parent,
8167 "lists_tables",
8168 "Lists and tables",
8169 UiSize::new(520.0, 0.0),
8170 );
8171
8172 let list_row = ui.add_child(
8173 body,
8174 UiNode::container(
8175 "lists_tables.list_row",
8176 Layout::row()
8177 .size(LayoutSize::new(
8178 LayoutDimension::percent(1.0),
8179 LayoutDimension::Auto,
8180 ))
8181 .gap(LayoutGap::points(10.0, 10.0))
8182 .flex_wrap(LayoutFlexWrap::Wrap)
8183 .to_layout_style(),
8184 ),
8185 );
8186 let scroll_column = ui.add_child(
8187 list_row,
8188 UiNode::container(
8189 "lists_tables.scroll_area.column",
8190 Layout::column()
8191 .min_size(LayoutSize::points(220.0, 0.0))
8192 .gap(LayoutGap::points(6.0, 6.0))
8193 .flex(1.0, 1.0, LayoutDimension::points(245.0))
8194 .to_layout_style(),
8195 ),
8196 );
8197 widgets::label(
8198 ui,
8199 scroll_column,
8200 "lists_tables.scroll_area.title",
8201 "Scrollable list",
8202 text(12.0, color(166, 176, 190)),
8203 LayoutStyle::new().with_width_percent(1.0),
8204 );
8205 let nested_scroll = widgets::scroll_area(
8206 ui,
8207 scroll_column,
8208 "lists_tables.scroll_area",
8209 ScrollAxes::VERTICAL,
8210 LayoutStyle::column()
8211 .with_width_percent(1.0)
8212 .with_height(104.0),
8213 );
8214 ui.node_mut(nested_scroll)
8215 .set_action("lists_tables.scroll_area.scroll");
8216 if let Some(scroll) = ui.node_mut(nested_scroll).scroll_mut() {
8217 scroll.set_offset(UiPoint::new(0.0, state.list_scroll));
8218 }
8219 for index in 0..6 {
8220 widgets::label(
8221 ui,
8222 nested_scroll,
8223 format!("lists_tables.scroll_area.row.{index}"),
8224 format!("Scroll row {}", index + 1),
8225 text(12.0, color(200, 212, 228)),
8226 LayoutStyle::new()
8227 .with_width_percent(1.0)
8228 .with_height(26.0)
8229 .with_flex_shrink(0.0),
8230 );
8231 }
8232
8233 let virtual_list_column = ui.add_child(
8234 list_row,
8235 UiNode::container(
8236 "lists_tables.virtual_list.column",
8237 Layout::column()
8238 .min_size(LayoutSize::points(220.0, 0.0))
8239 .gap(LayoutGap::points(6.0, 6.0))
8240 .flex(1.0, 1.0, LayoutDimension::points(245.0))
8241 .to_layout_style(),
8242 ),
8243 );
8244
8245 widgets::label(
8246 ui,
8247 virtual_list_column,
8248 "lists_tables.virtual_list.title",
8249 "Virtualized list",
8250 text(12.0, color(166, 176, 190)),
8251 LayoutStyle::new().with_width_percent(1.0),
8252 );
8253 let virtual_list = widgets::virtual_list(
8254 ui,
8255 virtual_list_column,
8256 "lists_tables.virtual_list",
8257 widgets::VirtualListSpec {
8258 row_count: 24,
8259 row_height: 28.0,
8260 viewport_height: 104.0,
8261 scroll_offset: state.virtual_scroll,
8262 overscan: 1,
8263 },
8264 |ui, row_parent, row| {
8265 widgets::label(
8266 ui,
8267 row_parent,
8268 format!("lists_tables.virtual_list.row.{row}"),
8269 format!("Virtual row {}", row + 1),
8270 text(12.0, color(214, 224, 238)),
8271 LayoutStyle::new()
8272 .with_width_percent(1.0)
8273 .with_height(28.0)
8274 .with_flex_shrink(0.0),
8275 );
8276 },
8277 );
8278 ui.node_mut(virtual_list)
8279 .set_action("lists_tables.virtual_list.scroll");
8280
8281 widgets::separator(
8282 ui,
8283 body,
8284 "lists_tables.virtualized_table.separator",
8285 widgets::SeparatorOptions::default(),
8286 );
8287 widgets::label(
8288 ui,
8289 body,
8290 "lists_tables.data_table.title",
8291 "Virtualized selectable table",
8292 text(12.0, color(166, 176, 190)),
8293 LayoutStyle::new().with_width_percent(1.0),
8294 );
8295 let virtual_controls = wrapping_row(ui, body, "lists_tables.virtualized_table.controls", 8.0);
8296 button(
8297 ui,
8298 virtual_controls,
8299 "lists_tables.virtualized_table.sort.name",
8300 if state.virtual_table_descending {
8301 "Name desc"
8302 } else {
8303 "Name asc"
8304 },
8305 "lists_tables.virtualized_table.sort.name",
8306 button_visual(38, 52, 70),
8307 );
8308 button(
8309 ui,
8310 virtual_controls,
8311 "lists_tables.virtualized_table.filter.status",
8312 if state.virtual_table_ready_only {
8313 "Ready only"
8314 } else {
8315 "All status"
8316 },
8317 "lists_tables.virtualized_table.filter.status",
8318 button_visual(38, 52, 70),
8319 );
8320 button(
8321 ui,
8322 virtual_controls,
8323 "lists_tables.virtualized_table.resize.reset",
8324 "Reset width",
8325 "lists_tables.virtualized_table.resize.reset",
8326 button_visual(38, 52, 70),
8327 );
8328
8329 let columns = virtual_table_columns(state);
8330 let visible_rows = virtual_table_visible_rows(state);
8331 let mut table_options = ext_widgets::DataTableOptions::default()
8332 .with_row_action_prefix("lists_tables.virtualized_table")
8333 .with_cell_action_prefix("lists_tables.virtualized_table")
8334 .with_scroll_action("lists_tables.virtualized_table.scroll");
8335 table_options.layout = LayoutStyle::column()
8336 .with_width_percent(1.0)
8337 .with_flex_shrink(0.0);
8338 table_options.header_visual = UiVisual::panel(
8339 color(34, 41, 50),
8340 Some(StrokeStyle::new(color(67, 78, 95), 1.0)),
8341 0.0,
8342 );
8343 table_options.header_text_style = text(12.0, color(222, 230, 240));
8344 table_options.selection = state.table_selection.clone();
8345 ext_widgets::virtualized_data_table(
8346 ui,
8347 body,
8348 "lists_tables.virtualized_table",
8349 &columns,
8350 ext_widgets::VirtualDataTableSpec {
8351 row_count: visible_rows.len(),
8352 row_height: 28.0,
8353 viewport_width: 520.0,
8354 viewport_height: 156.0,
8355 scroll_offset: UiPoint::new(0.0, state.virtual_table_scroll),
8356 overscan_rows: 1,
8357 },
8358 table_options,
8359 |ui, cell_parent, cell| {
8360 let source_row = visible_rows.get(cell.row).copied().unwrap_or(cell.row);
8361 let value = virtual_table_cell_value(source_row, cell.column);
8362 widgets::label(
8363 ui,
8364 cell_parent,
8365 format!(
8366 "lists_tables.virtualized_table.cell.{}.{}.label",
8367 cell.row, cell.column
8368 ),
8369 value,
8370 text(12.0, color(220, 228, 238)),
8371 LayoutStyle::new().with_width_percent(1.0),
8372 );
8373 },
8374 );
8375}
8376
8377#[allow(clippy::field_reassign_with_default)]
8378fn property_inspector(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8379 let body = section(ui, parent, "property_inspector", "Property inspector");
8380 widgets::label(
8381 ui,
8382 body,
8383 "property_inspector.target",
8384 "Inspecting: Styling preview",
8385 text(12.0, color(196, 210, 230)),
8386 LayoutStyle::new().with_width_percent(1.0),
8387 );
8388 let mut options = ext_widgets::PropertyInspectorOptions::default();
8389 options.selected_index = Some(0);
8390 options.label_width = 120.0;
8391 options.row_height = 30.0;
8392 ext_widgets::property_inspector_grid(
8393 ui,
8394 body,
8395 "property_inspector.grid",
8396 &[
8397 ext_widgets::PropertyGridRow::new("target", "Widget", "Button preview").read_only(),
8398 ext_widgets::PropertyGridRow::new(
8399 "inner",
8400 "Inner margin",
8401 format!("{:.0}px", state.styling.inner_margin),
8402 )
8403 .with_kind(ext_widgets::PropertyValueKind::Number),
8404 ext_widgets::PropertyGridRow::new(
8405 "outer",
8406 "Outer margin",
8407 format!("{:.0}px", state.styling.outer_margin),
8408 )
8409 .with_kind(ext_widgets::PropertyValueKind::Number),
8410 ext_widgets::PropertyGridRow::new(
8411 "radius",
8412 "Corner radius",
8413 format!("{:.0}px", state.styling.corner_radius),
8414 )
8415 .with_kind(ext_widgets::PropertyValueKind::Number),
8416 ext_widgets::PropertyGridRow::new(
8417 "stroke",
8418 "Stroke",
8419 format!("{:.1}px", state.styling.stroke_width),
8420 )
8421 .with_kind(ext_widgets::PropertyValueKind::Number)
8422 .changed(),
8423 ext_widgets::PropertyGridRow::new("state", "Source", "Styling widget").read_only(),
8424 ],
8425 options,
8426 );
8427}
8428
8429fn diagnostics_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8430 let body = section(ui, parent, "diagnostics", "Diagnostics");
8431 let debug_snapshot = &state.diagnostics_snapshot;
8432
8433 diagnostics_selected_node_panel(ui, body, debug_snapshot);
8434 diagnostics_animation_panel(ui, body, state, debug_snapshot);
8435
8436 widgets::label(
8437 ui,
8438 body,
8439 "diagnostics.a11y.title",
8440 "Accessibility",
8441 text(14.0, color(222, 230, 240)),
8442 LayoutStyle::new().with_width_percent(1.0),
8443 );
8444 let mut overlay_preview_style = UiNodeStyle::from(
8445 LayoutStyle::new()
8446 .with_width(320.0)
8447 .with_height(140.0)
8448 .with_flex_shrink(0.0),
8449 );
8450 overlay_preview_style.set_clip(ClipBehavior::Clip);
8451 let overlay_preview = ui.add_child(
8452 body,
8453 UiNode::container("diagnostics.a11y.preview", overlay_preview_style).with_visual(
8454 UiVisual::panel(
8455 color(12, 17, 24),
8456 Some(StrokeStyle::new(color(47, 62, 82), 1.0)),
8457 4.0,
8458 ),
8459 ),
8460 );
8461 let mut overlay_options = ext_widgets::AccessibilityDebugOverlayOptions {
8462 action_prefix: Some("diagnostics.a11y.visual".to_owned()),
8463 ..Default::default()
8464 };
8465 overlay_options.show_labels = false;
8466 ext_widgets::accessibility_debug_overlay(
8467 ui,
8468 overlay_preview,
8469 "diagnostics.a11y.visual",
8470 &debug_snapshot,
8471 overlay_options,
8472 );
8473 diagnostics_accessibility_details(ui, body, debug_snapshot);
8474
8475 let diagnostic_columns = ui.add_child(
8476 body,
8477 UiNode::container(
8478 "diagnostics.columns",
8479 LayoutStyle::column()
8480 .with_width_percent(1.0)
8481 .with_flex_shrink(0.0)
8482 .gap(10.0),
8483 ),
8484 );
8485 let command_column = ui.add_child(
8486 diagnostic_columns,
8487 UiNode::container(
8488 "diagnostics.commands.column",
8489 LayoutStyle::column()
8490 .with_width_percent(1.0)
8491 .with_flex_shrink(0.0)
8492 .gap(8.0),
8493 ),
8494 );
8495 let theme_column = ui.add_child(
8496 diagnostic_columns,
8497 UiNode::container(
8498 "diagnostics.theme.column",
8499 LayoutStyle::column()
8500 .with_width_percent(1.0)
8501 .with_flex_shrink(0.0)
8502 .gap(8.0),
8503 ),
8504 );
8505
8506 let registry = diagnostics_command_registry();
8507 diagnostics_commands_panel(ui, command_column, ®istry);
8508
8509 let theme_snapshot = DebugThemeSnapshot::from_theme(&Theme::dark());
8510 diagnostics_theme_panel(ui, theme_column, &theme_snapshot);
8511}
8512
8513fn diagnostics_selected_node_panel(
8514 ui: &mut UiDocument,
8515 parent: UiNodeId,
8516 snapshot: &DebugInspectorSnapshot,
8517) {
8518 let panel = diagnostics_panel(ui, parent, "diagnostics.inspector", "Selected node");
8519 let rows = snapshot
8520 .node("diagnostics.sample.preview")
8521 .map(|node| {
8522 vec![
8523 ext_widgets::PropertyGridRow::new("name", "Node", "Preview action").read_only(),
8524 ext_widgets::PropertyGridRow::new("role", "Role", "Button").read_only(),
8525 ext_widgets::PropertyGridRow::new(
8526 "bounds",
8527 "Bounds",
8528 format!(
8529 "{:.0}, {:.0}; {:.0} x {:.0}",
8530 node.rect.x, node.rect.y, node.rect.width, node.rect.height
8531 ),
8532 )
8533 .with_kind(ext_widgets::PropertyValueKind::Number)
8534 .read_only(),
8535 ext_widgets::PropertyGridRow::new(
8536 "clip",
8537 "Clip",
8538 format!("{:.0} x {:.0}", node.clip_rect.width, node.clip_rect.height),
8539 )
8540 .with_kind(ext_widgets::PropertyValueKind::Number)
8541 .read_only(),
8542 ext_widgets::PropertyGridRow::new(
8543 "input",
8544 "Input",
8545 if node.input.pointer {
8546 "Receives pointer input"
8547 } else {
8548 "Passive"
8549 },
8550 )
8551 .read_only(),
8552 ]
8553 })
8554 .unwrap_or_else(|| {
8555 vec![
8556 ext_widgets::PropertyGridRow::new("missing", "Selected node", "No node selected")
8557 .read_only(),
8558 ]
8559 });
8560 ext_widgets::property_inspector_grid(
8561 ui,
8562 panel,
8563 "diagnostics.inspector.rows",
8564 &rows,
8565 diagnostics_grid_options("Selected node details"),
8566 );
8567}
8568
8569fn diagnostics_animation_panel(
8570 ui: &mut UiDocument,
8571 parent: UiNodeId,
8572 state: &ShowcaseState,
8573 snapshot: &DebugInspectorSnapshot,
8574) {
8575 let graph_panel =
8576 diagnostics_panel(ui, parent, "diagnostics.animation.graph", "Animation state");
8577 if let Some(animation) = snapshot.animation("diagnostics.sample.preview") {
8578 let state_row = row(ui, graph_panel, "diagnostics.animation.graph.states", 8.0);
8579 for state_name in ["idle", "hot"] {
8580 diagnostic_chip(
8581 ui,
8582 state_row,
8583 format!("diagnostics.animation.graph.state.{state_name}"),
8584 state_name,
8585 animation.current_state == state_name,
8586 );
8587 }
8588
8589 let graph = animation.state_graph();
8590 for (index, edge) in graph.edges.iter().take(2).enumerate() {
8591 let value = if edge.kind == DebugAnimationGraphEdgeKind::Blend {
8592 "Input blend"
8593 } else {
8594 "State change"
8595 };
8596 let detail = if edge.label.is_empty() {
8597 if edge.active { "Active" } else { "Inactive" }.to_owned()
8598 } else if edge.active {
8599 format!("{}; active", edge.label)
8600 } else {
8601 edge.label.clone()
8602 };
8603 diagnostic_value_row(
8604 ui,
8605 graph_panel,
8606 format!("diagnostics.animation.graph.edge.{index}"),
8607 value,
8608 format!("{} -> {}", edge.from, edge.to),
8609 );
8610 diagnostic_muted_label(
8611 ui,
8612 graph_panel,
8613 format!("diagnostics.animation.graph.edge.{index}.detail"),
8614 detail,
8615 );
8616 }
8617 } else {
8618 diagnostic_muted_label(
8619 ui,
8620 graph_panel,
8621 "diagnostics.animation.graph.empty",
8622 "No animation state machine",
8623 );
8624 }
8625
8626 let controls_panel = diagnostics_panel(
8627 ui,
8628 parent,
8629 "diagnostics.animation.controls",
8630 "Animation controls",
8631 );
8632 let transport = row(
8633 ui,
8634 controls_panel,
8635 "diagnostics.animation.controls.transport",
8636 8.0,
8637 );
8638 diagnostic_button(
8639 ui,
8640 transport,
8641 "diagnostics.animation.controls.transport.pause_toggle",
8642 if state.diagnostics_animation_paused {
8643 "Resume"
8644 } else {
8645 "Pause"
8646 },
8647 state.diagnostics_animation_paused,
8648 );
8649 diagnostic_button(
8650 ui,
8651 transport,
8652 "diagnostics.animation.controls.transport.step",
8653 "Step",
8654 false,
8655 );
8656 diagnostic_slider_row(
8657 ui,
8658 controls_panel,
8659 "diagnostics.animation.controls.transport.scrub",
8660 "Scrub progress",
8661 state.diagnostics_animation_scrub,
8662 "diagnostics.animation.controls.transport.scrub",
8663 );
8664 diagnostic_button(
8665 ui,
8666 controls_panel,
8667 "diagnostics.animation.controls.input.active.toggle",
8668 if state.diagnostics_animation_active {
8669 "Active input: true"
8670 } else {
8671 "Active input: false"
8672 },
8673 state.diagnostics_animation_active,
8674 );
8675 diagnostic_slider_row(
8676 ui,
8677 controls_panel,
8678 "diagnostics.animation.controls.input.hover.set",
8679 "Hover blend",
8680 state.diagnostics_animation_hover,
8681 "diagnostics.animation.controls.input.hover.set",
8682 );
8683 diagnostic_button(
8684 ui,
8685 controls_panel,
8686 "diagnostics.animation.controls.input.pulse.fire",
8687 "Fire pulse",
8688 false,
8689 );
8690 widgets::label(
8691 ui,
8692 controls_panel,
8693 "diagnostics.animation.controls.status",
8694 format!(
8695 "Scrub {:.0}% Hover {:.0}% Pulses {}",
8696 state.diagnostics_animation_scrub * 100.0,
8697 state.diagnostics_animation_hover * 100.0,
8698 state.diagnostics_animation_pulse_count
8699 ),
8700 text(12.0, color(166, 180, 198)),
8701 LayoutStyle::new().with_width_percent(1.0),
8702 );
8703}
8704
8705fn diagnostics_accessibility_details(
8706 ui: &mut UiDocument,
8707 parent: UiNodeId,
8708 snapshot: &DebugInspectorSnapshot,
8709) {
8710 let rows = snapshot
8711 .accessibility_overlay
8712 .iter()
8713 .find(|node| node.name == "diagnostics.sample.preview")
8714 .map(|node| {
8715 let accessibility = node.accessibility.as_ref();
8716 vec![
8717 ext_widgets::PropertyGridRow::new("role", "Role", "Button").read_only(),
8718 ext_widgets::PropertyGridRow::new(
8719 "label",
8720 "Label",
8721 accessibility
8722 .and_then(|meta| meta.label.clone())
8723 .unwrap_or_else(|| "Preview action".to_owned()),
8724 )
8725 .read_only(),
8726 ext_widgets::PropertyGridRow::new(
8727 "focus",
8728 "Focus order",
8729 node.focus_index
8730 .map(|index| format!("#{}", index + 1))
8731 .unwrap_or_else(|| "Not focusable".to_owned()),
8732 )
8733 .read_only(),
8734 ext_widgets::PropertyGridRow::new(
8735 "warnings",
8736 "Warnings",
8737 if node.warnings.is_empty() {
8738 "None"
8739 } else {
8740 "Needs review"
8741 },
8742 )
8743 .read_only(),
8744 ]
8745 })
8746 .unwrap_or_else(|| {
8747 vec![
8748 ext_widgets::PropertyGridRow::new("missing", "Accessibility", "No metadata")
8749 .read_only(),
8750 ]
8751 });
8752 ext_widgets::property_inspector_grid(
8753 ui,
8754 parent,
8755 "diagnostics.a11y",
8756 &rows,
8757 diagnostics_grid_options("Accessibility metadata"),
8758 );
8759}
8760
8761fn diagnostics_commands_panel(ui: &mut UiDocument, parent: UiNodeId, registry: &CommandRegistry) {
8762 let panel = diagnostics_panel(ui, parent, "diagnostics.commands", "Commands");
8763 let formatter = ShortcutFormatter::default();
8764 for command_id in [
8765 "diagnostics.palette",
8766 "diagnostics.inspect",
8767 "diagnostics.record",
8768 "diagnostics.export_theme",
8769 ] {
8770 if let Some(command) = registry.command(command_id) {
8771 let shortcut = registry
8772 .command_bindings(command.meta.id.clone())
8773 .first()
8774 .map(|binding| formatter.format(binding.shortcut))
8775 .unwrap_or_else(|| "Unbound".to_owned());
8776 let status = if command.enabled {
8777 command
8778 .meta
8779 .category
8780 .clone()
8781 .unwrap_or_else(|| "General".to_owned())
8782 } else {
8783 command
8784 .disabled_reason
8785 .clone()
8786 .unwrap_or_else(|| "Disabled".to_owned())
8787 };
8788 diagnostic_command_row(
8789 ui,
8790 panel,
8791 format!(
8792 "diagnostics.commands.row.{}",
8793 command.meta.id.as_str().replace('.', "_")
8794 ),
8795 &command.meta.label,
8796 &shortcut,
8797 &status,
8798 );
8799 }
8800 }
8801 diagnostic_value_row(
8802 ui,
8803 panel,
8804 "diagnostics.commands.conflicts",
8805 "Shortcut conflicts",
8806 if registry.conflicts().is_empty() {
8807 "None"
8808 } else {
8809 "Needs review"
8810 },
8811 );
8812}
8813
8814fn diagnostics_theme_panel(ui: &mut UiDocument, parent: UiNodeId, snapshot: &DebugThemeSnapshot) {
8815 let panel = diagnostics_panel(ui, parent, "diagnostics.theme", "Theme tokens");
8816 diagnostic_value_row(
8817 ui,
8818 panel,
8819 "diagnostics.theme.name",
8820 "Theme",
8821 snapshot.name.as_str(),
8822 );
8823 for token_path in ["colors.accent", "colors.surface", "typography.body"] {
8824 if let Some(token) = snapshot.token(token_path) {
8825 diagnostic_value_row(
8826 ui,
8827 panel,
8828 format!("diagnostics.theme.token.{}", token_path.replace('.', "_")),
8829 token_path,
8830 token.value.as_str(),
8831 );
8832 }
8833 }
8834 if let Some(component) = snapshot.component_states.first() {
8835 diagnostic_value_row(
8836 ui,
8837 panel,
8838 "diagnostics.theme.component.button",
8839 "Button normal",
8840 format!(
8841 "{:.0} x {:.0}, padding {:.0}",
8842 component.min_width, component.min_height, component.padding_x
8843 ),
8844 );
8845 }
8846}
8847
8848fn diagnostics_panel(
8849 ui: &mut UiDocument,
8850 parent: UiNodeId,
8851 name: impl Into<String>,
8852 title: impl Into<String>,
8853) -> UiNodeId {
8854 let name = name.into();
8855 let title = title.into();
8856 let panel = ui.add_child(
8857 parent,
8858 UiNode::container(
8859 name.clone(),
8860 LayoutStyle::column()
8861 .with_width_percent(1.0)
8862 .with_padding(10.0)
8863 .with_gap(8.0)
8864 .with_flex_shrink(0.0),
8865 )
8866 .with_visual(UiVisual::panel(
8867 color(15, 20, 28),
8868 Some(StrokeStyle::new(color(52, 65, 84), 1.0)),
8869 4.0,
8870 ))
8871 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(title.clone())),
8872 );
8873 widgets::label(
8874 ui,
8875 panel,
8876 format!("{name}.title"),
8877 title,
8878 text(13.0, color(222, 230, 240)),
8879 LayoutStyle::new().with_width_percent(1.0),
8880 );
8881 panel
8882}
8883
8884fn diagnostics_grid_options(label: impl Into<String>) -> ext_widgets::PropertyInspectorOptions {
8885 ext_widgets::PropertyInspectorOptions {
8886 label_width: 112.0,
8887 row_height: 28.0,
8888 accessibility_label: Some(label.into()),
8889 ..Default::default()
8890 }
8891}
8892
8893fn diagnostic_value_row(
8894 ui: &mut UiDocument,
8895 parent: UiNodeId,
8896 name: impl Into<String>,
8897 label: impl Into<String>,
8898 value: impl Into<String>,
8899) -> UiNodeId {
8900 let name = name.into();
8901 let row = row(ui, parent, name.clone(), 8.0);
8902 widgets::label(
8903 ui,
8904 row,
8905 format!("{name}.label"),
8906 label.into(),
8907 text(12.0, color(166, 180, 198)),
8908 LayoutStyle::new().with_width(136.0).with_flex_shrink(0.0),
8909 );
8910 widgets::label(
8911 ui,
8912 row,
8913 format!("{name}.value"),
8914 value.into(),
8915 text(12.0, color(226, 234, 244)),
8916 LayoutStyle::new().with_width_percent(1.0),
8917 );
8918 row
8919}
8920
8921fn diagnostic_muted_label(
8922 ui: &mut UiDocument,
8923 parent: UiNodeId,
8924 name: impl Into<String>,
8925 label: impl Into<String>,
8926) -> UiNodeId {
8927 let mut style = text(12.0, color(166, 180, 198));
8928 style.wrap = TextWrap::WordOrGlyph;
8929 widgets::label(
8930 ui,
8931 parent,
8932 name,
8933 label.into(),
8934 style,
8935 LayoutStyle::new().with_width_percent(1.0),
8936 )
8937}
8938
8939fn diagnostic_command_row(
8940 ui: &mut UiDocument,
8941 parent: UiNodeId,
8942 name: impl Into<String>,
8943 label: &str,
8944 shortcut: &str,
8945 status: &str,
8946) -> UiNodeId {
8947 let name = name.into();
8948 let row = row(ui, parent, name.clone(), 8.0);
8949 widgets::label(
8950 ui,
8951 row,
8952 format!("{name}.label"),
8953 label,
8954 text(12.0, color(226, 234, 244)),
8955 LayoutStyle::new()
8956 .with_width_percent(1.0)
8957 .with_flex_grow(1.0),
8958 );
8959 widgets::label(
8960 ui,
8961 row,
8962 format!("{name}.shortcut"),
8963 shortcut,
8964 text(12.0, color(166, 180, 198)),
8965 LayoutStyle::new().with_width(78.0).with_flex_shrink(0.0),
8966 );
8967 widgets::label(
8968 ui,
8969 row,
8970 format!("{name}.status"),
8971 status,
8972 text(12.0, color(166, 180, 198)),
8973 LayoutStyle::new().with_width(140.0).with_flex_shrink(0.0),
8974 );
8975 row
8976}
8977
8978fn diagnostic_slider_row(
8979 ui: &mut UiDocument,
8980 parent: UiNodeId,
8981 name: impl Into<String>,
8982 label: impl Into<String>,
8983 value: f32,
8984 action: impl Into<String>,
8985) -> UiNodeId {
8986 let name = name.into();
8987 let label = label.into();
8988 let row = row(ui, parent, format!("{name}.row"), 8.0);
8989 widgets::label(
8990 ui,
8991 row,
8992 format!("{name}.label"),
8993 label.clone(),
8994 text(12.0, color(166, 180, 198)),
8995 LayoutStyle::new().with_width(136.0).with_flex_shrink(0.0),
8996 );
8997 let slider_name = if name.ends_with(".set") {
8998 format!("{name}.slider")
8999 } else {
9000 name.clone()
9001 };
9002 let mut options = widgets::SliderOptions::default()
9003 .with_layout(LayoutStyle::new().with_width(160.0).with_height(24.0))
9004 .with_value_edit_action(action.into());
9005 options.accessibility_label = Some(label);
9006 widgets::slider(ui, row, slider_name, value, 0.0..1.0, options);
9007 widgets::label(
9008 ui,
9009 row,
9010 format!("{name}.percent"),
9011 format!("{:.0}%", value * 100.0),
9012 text(12.0, color(226, 234, 244)),
9013 LayoutStyle::new().with_width(46.0).with_flex_shrink(0.0),
9014 );
9015 row
9016}
9017
9018fn diagnostic_button(
9019 ui: &mut UiDocument,
9020 parent: UiNodeId,
9021 name: impl Into<String>,
9022 label: impl Into<String>,
9023 active: bool,
9024) -> UiNodeId {
9025 let name = name.into();
9026 let mut options = widgets::ButtonOptions::default()
9027 .with_layout(LayoutStyle::new().with_height(32.0))
9028 .with_action(name.clone())
9029 .pressed(active);
9030 if active {
9031 options.visual = UiVisual::panel(
9032 color(47, 94, 150),
9033 Some(StrokeStyle::new(color(103, 164, 224), 1.0)),
9034 4.0,
9035 );
9036 }
9037 widgets::button(ui, parent, name, label, options)
9038}
9039
9040fn diagnostic_chip(
9041 ui: &mut UiDocument,
9042 parent: UiNodeId,
9043 name: impl Into<String>,
9044 label: impl Into<String>,
9045 active: bool,
9046) -> UiNodeId {
9047 let name = name.into();
9048 let chip = ui.add_child(
9049 parent,
9050 UiNode::container(
9051 name.clone(),
9052 LayoutStyle::new()
9053 .with_width(82.0)
9054 .with_height(28.0)
9055 .with_padding(4.0)
9056 .with_flex_shrink(0.0),
9057 )
9058 .with_visual(if active {
9059 UiVisual::panel(
9060 color(47, 94, 150),
9061 Some(StrokeStyle::new(color(103, 164, 224), 1.0)),
9062 4.0,
9063 )
9064 } else {
9065 UiVisual::panel(
9066 color(31, 39, 50),
9067 Some(StrokeStyle::new(color(62, 76, 96), 1.0)),
9068 4.0,
9069 )
9070 }),
9071 );
9072 widgets::label(
9073 ui,
9074 chip,
9075 format!("{name}.label"),
9076 label.into(),
9077 text(12.0, color(226, 234, 244)),
9078 LayoutStyle::new().with_width_percent(1.0),
9079 );
9080 chip
9081}
9082
9083fn diagnostics_sample_snapshot(state: &ShowcaseState) -> DebugInspectorSnapshot {
9084 diagnostics_sample_snapshot_for(
9085 state.diagnostics_animation_hover,
9086 state.diagnostics_animation_active,
9087 )
9088}
9089
9090fn diagnostics_sample_snapshot_for(hover: f32, active: bool) -> DebugInspectorSnapshot {
9091 let mut sample = UiDocument::new(root_style(320.0, 180.0));
9092 let card = sample.add_child(
9093 sample.root(),
9094 UiNode::container(
9095 "diagnostics.sample.card",
9096 LayoutStyle::column()
9097 .with_width_percent(1.0)
9098 .with_height(120.0)
9099 .padding(12.0)
9100 .gap(8.0),
9101 )
9102 .with_visual(UiVisual::panel(
9103 color(16, 22, 30),
9104 Some(StrokeStyle::new(color(62, 77, 98), 1.0)),
9105 6.0,
9106 ))
9107 .with_accessibility(
9108 AccessibilityMeta::new(AccessibilityRole::Group).label("Diagnostics sample"),
9109 ),
9110 );
9111 sample.add_child(
9112 card,
9113 UiNode::container(
9114 "diagnostics.sample.preview",
9115 LayoutStyle::new().with_width(160.0).with_height(38.0),
9116 )
9117 .with_input(InputBehavior::BUTTON)
9118 .with_visual(UiVisual::panel(
9119 color(52, 112, 180),
9120 Some(StrokeStyle::new(color(116, 183, 255), 1.0)),
9121 5.0,
9122 ))
9123 .with_accessibility(
9124 AccessibilityMeta::new(AccessibilityRole::Button)
9125 .label("Preview action")
9126 .focusable(),
9127 )
9128 .with_animation(
9129 AnimationMachine::new(
9130 vec![
9131 AnimationState::new(
9132 "idle",
9133 AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0),
9134 ),
9135 AnimationState::new(
9136 "hot",
9137 AnimatedValues::new(0.92, UiPoint::new(18.0, 0.0), 1.08),
9138 ),
9139 ],
9140 vec![AnimationTransition::when(
9141 "idle",
9142 "hot",
9143 AnimationCondition::bool("active", true),
9144 0.18,
9145 )],
9146 "idle",
9147 )
9148 .expect("sample animation")
9149 .with_number_input("hover", hover)
9150 .with_blend_binding(AnimationBlendBinding::new("hover", "idle", "hot"))
9151 .with_bool_input("active", active)
9152 .with_trigger_input("pulse"),
9153 ),
9154 );
9155 widgets::label(
9156 &mut sample,
9157 card,
9158 "diagnostics.sample.label",
9159 "Sample node",
9160 text(12.0, color(198, 210, 226)),
9161 LayoutStyle::new().with_width_percent(1.0),
9162 );
9163 sample
9164 .compute_layout(UiSize::new(320.0, 180.0), &mut ApproxTextMeasurer)
9165 .expect("sample layout");
9166 DebugInspectorSnapshot::from_document(&sample, &mut ApproxTextMeasurer)
9167}
9168
9169fn diagnostics_command_registry() -> CommandRegistry {
9170 let mut registry = CommandRegistry::new();
9171 registry
9172 .register(
9173 CommandMeta::new("diagnostics.palette", "Open command palette")
9174 .description("Show command search")
9175 .category("Debug"),
9176 )
9177 .expect("command");
9178 registry
9179 .register(
9180 CommandMeta::new("diagnostics.inspect", "Inspect selected node")
9181 .description("Focus the layout inspector")
9182 .category("Debug"),
9183 )
9184 .expect("command");
9185 registry
9186 .register(
9187 CommandMeta::new("diagnostics.record", "Start interaction recording")
9188 .description("Capture replay steps")
9189 .category("Testing"),
9190 )
9191 .expect("command");
9192 registry
9193 .register(CommandMeta::new(
9194 "diagnostics.export_theme",
9195 "Export theme patch",
9196 ))
9197 .expect("command");
9198 registry
9199 .bind_shortcut(
9200 CommandScope::Global,
9201 Shortcut::ctrl('k'),
9202 "diagnostics.palette",
9203 )
9204 .expect("shortcut");
9205 registry
9206 .bind_shortcut(
9207 CommandScope::Panel,
9208 Shortcut::ctrl('i'),
9209 "diagnostics.inspect",
9210 )
9211 .expect("shortcut");
9212 registry
9213 .bind_shortcut(
9214 CommandScope::Panel,
9215 Shortcut::ctrl('r'),
9216 "diagnostics.record",
9217 )
9218 .expect("shortcut");
9219 registry
9220 .disable("diagnostics.export_theme", "No changes to export")
9221 .expect("disable");
9222 registry
9223}
9224
9225fn tree_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9226 let body = section(ui, parent, "trees", "Tree view");
9227 widgets::label(
9228 ui,
9229 body,
9230 "trees.tree_view.title",
9231 "Editable tree",
9232 text(12.0, color(166, 176, 190)),
9233 LayoutStyle::new().with_width_percent(1.0),
9234 );
9235 ext_widgets::tree_view(
9236 ui,
9237 body,
9238 "trees.tree_view",
9239 &editable_tree_items(&state.editable_tree),
9240 &state.tree,
9241 ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.tree"),
9242 );
9243 widgets::label(
9244 ui,
9245 body,
9246 "trees.editable.status",
9247 &state.editable_tree_status,
9248 text(12.0, color(154, 166, 184)),
9249 LayoutStyle::new().with_width_percent(1.0),
9250 );
9251 widgets::label(
9252 ui,
9253 body,
9254 "trees.virtual.title",
9255 "Virtualized tree",
9256 text(12.0, color(166, 176, 190)),
9257 LayoutStyle::new().with_width_percent(1.0),
9258 );
9259 let virtual_nodes = ext_widgets::virtualized_tree_view(
9260 ui,
9261 body,
9262 "trees.virtual",
9263 &virtual_tree_items(),
9264 &state.tree_virtual,
9265 ext_widgets::VirtualTreeViewSpec::new(24.0, 112.0)
9266 .scroll_offset(state.tree_virtual_scroll)
9267 .overscan_rows(1),
9268 ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.virtual"),
9269 );
9270 ui.node_mut(virtual_nodes.body)
9271 .set_action("trees.virtual.scroll");
9272 widgets::label(
9273 ui,
9274 body,
9275 "trees.table.title",
9276 "Tree table",
9277 text(12.0, color(166, 176, 190)),
9278 LayoutStyle::new().with_width_percent(1.0),
9279 );
9280 tree_table_widgets(ui, body, state);
9281}
9282
9283fn tree_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9284 let rows = state.tree_table.visible_items(&tree_table_items());
9285 let columns = [
9286 ext_widgets::DataTableColumn::new("name", "Name", 220.0),
9287 ext_widgets::DataTableColumn::new("kind", "Kind", 84.0),
9288 ext_widgets::DataTableColumn::new("status", "Status", 92.0),
9289 ];
9290 let mut options = ext_widgets::DataTableOptions::default()
9291 .with_row_action_prefix("trees.table")
9292 .with_cell_action_prefix("trees.table")
9293 .with_scroll_action("trees.table.scroll");
9294 options.selection = state
9295 .tree_table
9296 .selected_index()
9297 .map(ext_widgets::DataTableSelection::single_row)
9298 .unwrap_or_default();
9299 options.layout = LayoutStyle::column()
9300 .with_width_percent(1.0)
9301 .with_height(132.0)
9302 .with_flex_shrink(0.0);
9303 ext_widgets::virtualized_data_table(
9304 ui,
9305 parent,
9306 "trees.table",
9307 &columns,
9308 ext_widgets::VirtualDataTableSpec {
9309 row_count: rows.len(),
9310 row_height: 24.0,
9311 viewport_width: 396.0,
9312 viewport_height: 96.0,
9313 scroll_offset: UiPoint::new(0.0, state.tree_table_scroll),
9314 overscan_rows: 1,
9315 },
9316 options,
9317 |ui, cell_parent, cell| {
9318 let Some(item) = rows.get(cell.row) else {
9319 return;
9320 };
9321 if cell.column == 0 {
9322 tree_table_name_cell(ui, cell_parent, cell.row, item);
9323 } else {
9324 widgets::label(
9325 ui,
9326 cell_parent,
9327 format!("trees.table.cell.{}.{}.label", cell.row, cell.column),
9328 tree_table_cell_value(item, cell.column),
9329 text(12.0, color(220, 228, 238)),
9330 LayoutStyle::new().with_width_percent(1.0),
9331 );
9332 }
9333 },
9334 );
9335}
9336
9337fn tree_table_name_cell(
9338 ui: &mut UiDocument,
9339 parent: UiNodeId,
9340 row: usize,
9341 item: &ext_widgets::TreeVisibleItem,
9342) {
9343 if item.depth > 0 {
9344 ui.add_child(
9345 parent,
9346 UiNode::container(
9347 format!("trees.table.row.{}.indent", item.id),
9348 LayoutStyle::new()
9349 .with_width(item.depth as f32 * 16.0)
9350 .with_height_percent(1.0)
9351 .with_flex_shrink(0.0),
9352 ),
9353 );
9354 }
9355 widgets::label(
9356 ui,
9357 parent,
9358 format!("trees.table.row.{}.disclosure", item.id),
9359 if item.has_children() {
9360 if item.expanded {
9361 "v"
9362 } else {
9363 ">"
9364 }
9365 } else {
9366 ""
9367 },
9368 text(12.0, color(166, 176, 190)),
9369 LayoutStyle::new()
9370 .with_width(18.0)
9371 .with_height_percent(1.0)
9372 .with_flex_shrink(0.0),
9373 );
9374 widgets::label(
9375 ui,
9376 parent,
9377 format!("trees.table.cell.{row}.0.label"),
9378 item.label.clone(),
9379 if item.disabled {
9380 text(12.0, color(154, 166, 184))
9381 } else {
9382 text(12.0, color(220, 228, 238))
9383 },
9384 LayoutStyle::new().with_width_percent(1.0),
9385 );
9386}
9387
9388fn tree_table_cell_value(item: &ext_widgets::TreeVisibleItem, column: usize) -> String {
9389 match column {
9390 0 => item.label.clone(),
9391 1 => {
9392 if item.has_children() {
9393 "Folder".to_owned()
9394 } else {
9395 "File".to_owned()
9396 }
9397 }
9398 _ => {
9399 if item.disabled {
9400 "Locked".to_owned()
9401 } else if item.has_children() && item.expanded {
9402 "Expanded".to_owned()
9403 } else if item.has_children() {
9404 "Collapsed".to_owned()
9405 } else if item.expanded {
9406 "Expanded".to_owned()
9407 } else {
9408 "Ready".to_owned()
9409 }
9410 }
9411 }
9412}
9413
9414fn tab_split_dock_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9415 let body = section_with_min_viewport(
9416 ui,
9417 parent,
9418 "layout_widgets",
9419 "Layout widgets",
9420 UiSize::new(640.0, 360.0),
9421 );
9422 let shell = ui.add_child(
9423 body,
9424 UiNode::container(
9425 "layout_widgets.dock_shell",
9426 LayoutStyle::column()
9427 .with_width_percent(1.0)
9428 .with_height(360.0)
9429 .with_flex_shrink(0.0),
9430 )
9431 .with_visual(UiVisual::panel(
9432 color(13, 17, 23),
9433 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9434 0.0,
9435 )),
9436 );
9437
9438 let mut panels = base_layout_dock_panels();
9439 state.layout_dock.apply_order_to_panels(&mut panels);
9440 state.layout_dock.apply_visibility_to_panels(&mut panels);
9441
9442 let mut drawer_options = ext_widgets::DockDrawerRailOptions::default();
9443 drawer_options.layout = LayoutStyle::row()
9444 .with_width_percent(1.0)
9445 .with_height(34.0)
9446 .with_padding(4.0)
9447 .with_gap(4.0);
9448 ext_widgets::dock_drawer_rail(
9449 ui,
9450 shell,
9451 "layout_widgets.dock.drawers",
9452 &[
9453 ext_widgets::DockDrawerDescriptor::new(
9454 "panel_a",
9455 "Panel A",
9456 "panel_a",
9457 ext_widgets::DockSide::Left,
9458 )
9459 .open(!state.layout_dock.is_hidden("panel_a"))
9460 .with_action("layout_widgets.drawer.panel_a"),
9461 ext_widgets::DockDrawerDescriptor::new(
9462 "panel_b",
9463 "Panel B",
9464 "panel_b",
9465 ext_widgets::DockSide::Right,
9466 )
9467 .open(!state.layout_dock.is_hidden("panel_b"))
9468 .with_action("layout_widgets.drawer.panel_b"),
9469 ],
9470 drawer_options,
9471 );
9472
9473 let mut options = ext_widgets::DockWorkspaceOptions::default();
9474 options.layout = LayoutStyle::column()
9475 .with_width_percent(1.0)
9476 .with_height(0.0)
9477 .with_flex_grow(1.0);
9478 options.show_titles = true;
9479 options.handle_thickness = 2.0;
9480 options.panel_visual = UiVisual::panel(
9481 color(18, 22, 29),
9482 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9483 0.0,
9484 );
9485 options.center_visual = UiVisual::panel(
9486 color(15, 19, 25),
9487 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9488 0.0,
9489 );
9490 options.resize_handle_visual = UiVisual::panel(color(65, 78, 96), None, 0.0);
9491
9492 ext_widgets::dock_workspace(
9493 ui,
9494 shell,
9495 "layout_widgets.dock",
9496 &panels,
9497 options,
9498 |ui, parent, panel| match panel.id.as_str() {
9499 "panel_a" => layout_panel_contents(
9500 ui,
9501 parent,
9502 "layout.panel_a",
9503 state.layout_panel_a_scroll,
9504 &["Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6"],
9505 ),
9506 "workspace" => layout_workspace_contents(
9507 ui,
9508 parent,
9509 "layout.workspace",
9510 state.layout_workspace_scroll,
9511 ),
9512 "panel_b" => layout_panel_contents(
9513 ui,
9514 parent,
9515 "layout.panel_b",
9516 state.layout_panel_b_scroll,
9517 &[
9518 "Value A", "Value B", "Value C", "Value D", "Value E", "Value F",
9519 ],
9520 ),
9521 _ => {}
9522 },
9523 );
9524
9525 if let Some(floating) = state.layout_dock.floating_panel("panel_a") {
9526 let floating_panel = ui.add_child(
9527 shell,
9528 UiNode::container(
9529 "layout_widgets.floating.panel_a",
9530 operad::layout::absolute(
9531 floating.rect.x,
9532 floating.rect.y,
9533 floating.rect.width,
9534 floating.rect.height,
9535 ),
9536 )
9537 .with_visual(UiVisual::panel(
9538 color(18, 22, 29),
9539 Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
9540 4.0,
9541 )),
9542 );
9543 layout_panel_contents(
9544 ui,
9545 floating_panel,
9546 "layout.panel_a_floating",
9547 state.layout_panel_a_scroll,
9548 &["Item 1", "Item 2", "Item 3", "Item 4"],
9549 );
9550 }
9551}
9552
9553fn base_layout_dock_panels() -> Vec<ext_widgets::DockPanelDescriptor> {
9554 vec![
9555 ext_widgets::DockPanelDescriptor::new(
9556 "panel_a",
9557 "Panel A",
9558 ext_widgets::DockSide::Left,
9559 200.0,
9560 )
9561 .with_min_size(150.0)
9562 .resizable(true),
9563 ext_widgets::DockPanelDescriptor::center("workspace", "Workspace").with_min_size(220.0),
9564 ext_widgets::DockPanelDescriptor::new(
9565 "panel_b",
9566 "Panel B",
9567 ext_widgets::DockSide::Right,
9568 200.0,
9569 )
9570 .with_min_size(150.0)
9571 .resizable(true),
9572 ]
9573}
9574
9575fn container_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9576 let body = section_with_min_viewport(
9577 ui,
9578 parent,
9579 "containers",
9580 "Containers",
9581 UiSize::new(420.0, 0.0),
9582 );
9583
9584 let frame = widgets::frame(
9585 ui,
9586 body,
9587 "containers.frame",
9588 widgets::FrameOptions::default().with_layout(
9589 LayoutStyle::column()
9590 .with_width_percent(1.0)
9591 .with_height(64.0)
9592 .with_padding(8.0)
9593 .with_gap(6.0),
9594 ),
9595 );
9596 widgets::strong_label(
9597 ui,
9598 frame,
9599 "containers.frame.title",
9600 "Frame",
9601 LayoutStyle::new().with_width_percent(1.0),
9602 );
9603 widgets::weak_label(
9604 ui,
9605 frame,
9606 "containers.frame.body",
9607 "Framed surface with padding.",
9608 LayoutStyle::new().with_width_percent(1.0),
9609 );
9610
9611 let group = widgets::group(ui, body, "containers.group");
9612 widgets::label(
9613 ui,
9614 group,
9615 "containers.group.label",
9616 "Group helper",
9617 text(12.0, color(220, 228, 238)),
9618 LayoutStyle::new().with_width_percent(1.0),
9619 );
9620 let generic_panel = widgets::panel(
9621 ui,
9622 body,
9623 "containers.panel",
9624 widgets::PanelOptions::group().with_layout(
9625 LayoutStyle::column()
9626 .with_width_percent(1.0)
9627 .with_height(44.0)
9628 .with_padding(8.0),
9629 ),
9630 );
9631 widgets::label(
9632 ui,
9633 generic_panel,
9634 "containers.panel.label",
9635 "Generic panel",
9636 text(12.0, color(220, 228, 238)),
9637 LayoutStyle::new().with_width_percent(1.0),
9638 );
9639 let group_panel = widgets::group_panel(ui, body, "containers.group_panel");
9640 widgets::label(
9641 ui,
9642 group_panel,
9643 "containers.group_panel.label",
9644 "Group panel",
9645 text(12.0, color(220, 228, 238)),
9646 LayoutStyle::new().with_width_percent(1.0),
9647 );
9648
9649 widgets::separator(
9650 ui,
9651 body,
9652 "containers.separator",
9653 widgets::SeparatorOptions::default(),
9654 );
9655 widgets::spacer(
9656 ui,
9657 body,
9658 "containers.spacer",
9659 LayoutStyle::new()
9660 .with_width_percent(1.0)
9661 .with_height(8.0)
9662 .with_flex_shrink(0.0),
9663 );
9664
9665 let grid = widgets::grid::grid(
9666 ui,
9667 body,
9668 "containers.grid",
9669 widgets::grid::GridOptions::default().with_layout(
9670 LayoutStyle::column()
9671 .with_width_percent(1.0)
9672 .with_height(78.0)
9673 .with_gap(4.0),
9674 ),
9675 );
9676 for row_index in 0..2 {
9677 let row = widgets::grid::grid_row(
9678 ui,
9679 grid,
9680 format!("containers.grid.row.{row_index}"),
9681 widgets::grid::GridRowOptions::default(),
9682 );
9683 for column_index in 0..3 {
9684 widgets::grid::grid_text_cell(
9685 ui,
9686 row,
9687 format!("containers.grid.row.{row_index}.cell.{column_index}"),
9688 format!("R{} C{}", row_index + 1, column_index + 1),
9689 widgets::grid::GridCellOptions {
9690 text_style: text(12.0, color(214, 224, 238)),
9691 ..Default::default()
9692 },
9693 );
9694 }
9695 }
9696
9697 widgets::sides(
9698 ui,
9699 body,
9700 "containers.sides",
9701 widgets::SidesOptions::default()
9702 .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
9703 .with_gap(8.0)
9704 .with_visual(UiVisual::panel(
9705 color(20, 25, 32),
9706 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
9707 4.0,
9708 )),
9709 |ui, left| {
9710 widgets::label(
9711 ui,
9712 left,
9713 "containers.sides.left.label",
9714 "Left side",
9715 text(12.0, color(220, 228, 238)),
9716 LayoutStyle::new().with_width_percent(1.0),
9717 );
9718 },
9719 |ui, right| {
9720 widgets::label(
9721 ui,
9722 right,
9723 "containers.sides.right.label",
9724 "Right side",
9725 text(12.0, color(220, 228, 238)),
9726 LayoutStyle::new().with_width_percent(1.0),
9727 );
9728 },
9729 );
9730
9731 widgets::columns(
9732 ui,
9733 body,
9734 "containers.columns",
9735 3,
9736 widgets::ColumnsOptions::default()
9737 .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
9738 .with_gap(8.0),
9739 |ui, column, index| {
9740 widgets::label(
9741 ui,
9742 column,
9743 format!("containers.columns.{index}.label"),
9744 format!("Column {}", index + 1),
9745 text(12.0, color(220, 228, 238)),
9746 LayoutStyle::new().with_width_percent(1.0),
9747 );
9748 },
9749 );
9750
9751 let indented = widgets::indented_section(
9752 ui,
9753 body,
9754 "containers.indented",
9755 widgets::IndentOptions::default().with_amount(24.0),
9756 );
9757 widgets::label(
9758 ui,
9759 indented,
9760 "containers.indented.label",
9761 "Indented section",
9762 text(12.0, color(196, 210, 230)),
9763 LayoutStyle::new().with_width_percent(1.0),
9764 );
9765
9766 widgets::resize_container(
9767 ui,
9768 body,
9769 "containers.resize_container",
9770 widgets::ResizeContainerOptions::default().with_layout(
9771 LayoutStyle::column()
9772 .with_width_percent(1.0)
9773 .with_height(92.0)
9774 .with_flex_shrink(0.0),
9775 ),
9776 |ui, content| {
9777 widgets::label(
9778 ui,
9779 content,
9780 "containers.resize_container.label",
9781 "Resize container",
9782 text(12.0, color(220, 228, 238)),
9783 LayoutStyle::new().with_width_percent(1.0),
9784 );
9785 },
9786 );
9787
9788 widgets::scene(
9789 ui,
9790 body,
9791 "containers.scene",
9792 vec![
9793 ScenePrimitive::Rect(
9794 PaintRect::solid(UiRect::new(8.0, 12.0, 108.0, 46.0), color(48, 112, 184))
9795 .stroke(AlignedStroke::inside(StrokeStyle::new(
9796 color(132, 174, 222),
9797 1.0,
9798 )))
9799 .corner_radii(CornerRadii::uniform(6.0)),
9800 ),
9801 ScenePrimitive::Circle {
9802 center: UiPoint::new(150.0, 35.0),
9803 radius: 22.0,
9804 fill: color(111, 203, 159),
9805 stroke: Some(StrokeStyle::new(color(176, 236, 206), 1.0)),
9806 },
9807 ScenePrimitive::Line {
9808 from: UiPoint::new(188.0, 18.0),
9809 to: UiPoint::new(238.0, 52.0),
9810 stroke: StrokeStyle::new(color(232, 186, 88), 3.0),
9811 },
9812 ],
9813 widgets::SceneOptions::default()
9814 .with_layout(LayoutStyle::new().with_width(260.0).with_height(70.0))
9815 .accessibility_label("Scene primitives"),
9816 );
9817
9818 widgets::scroll_container(
9819 ui,
9820 body,
9821 "containers.scroll_area_with_bars",
9822 state.containers_scroll,
9823 widgets::ScrollContainerOptions::default()
9824 .with_axes(ScrollAxes::VERTICAL)
9825 .with_layout(LayoutStyle::column().with_width(260.0).with_height(116.0))
9826 .with_viewport_layout(
9827 LayoutStyle::column()
9828 .with_width(0.0)
9829 .with_height_percent(1.0)
9830 .with_flex_grow(1.0)
9831 .with_flex_shrink(1.0),
9832 ),
9833 |ui, viewport| {
9834 for index in 0..5 {
9835 widgets::label(
9836 ui,
9837 viewport,
9838 format!("containers.scroll_area_with_bars.row.{index}"),
9839 format!("Scrollable row {}", index + 1),
9840 text(12.0, color(200, 212, 228)),
9841 LayoutStyle::new()
9842 .with_width(232.0)
9843 .with_height(28.0)
9844 .with_flex_shrink(0.0),
9845 );
9846 }
9847 },
9848 );
9849
9850 widgets::label(
9851 ui,
9852 body,
9853 "containers.area.title",
9854 "Absolute area",
9855 text(12.0, color(166, 176, 190)),
9856 LayoutStyle::new().with_width_percent(1.0),
9857 );
9858 let area_host = ui.add_child(
9859 body,
9860 UiNode::container(
9861 "containers.area.host",
9862 LayoutStyle::new()
9863 .with_width_percent(1.0)
9864 .with_height(82.0)
9865 .with_flex_shrink(0.0),
9866 )
9867 .with_visual(UiVisual::panel(
9868 color(17, 20, 25),
9869 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
9870 4.0,
9871 )),
9872 );
9873 widgets::container::area(
9874 ui,
9875 area_host,
9876 "containers.area",
9877 widgets::container::AreaOptions::new(UiRect::new(14.0, 14.0, 180.0, 44.0))
9878 .with_visual(UiVisual::panel(color(39, 72, 109), None, 4.0))
9879 .accessibility_label("Absolute positioned area"),
9880 |ui, area| {
9881 widgets::label(
9882 ui,
9883 area,
9884 "containers.area.label",
9885 "Area",
9886 text(12.0, color(238, 244, 252)),
9887 LayoutStyle::new().with_width_percent(1.0),
9888 );
9889 },
9890 );
9891}
9892
9893fn panel_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9894 let body = section_with_min_viewport(ui, parent, "panels", "Panels", UiSize::new(520.0, 320.0));
9895 widgets::label(
9896 ui,
9897 body,
9898 "panels.title",
9899 "Drag the split bars to resize the docked panels.",
9900 text(12.0, color(166, 176, 190)),
9901 LayoutStyle::new().with_width_percent(1.0),
9902 );
9903 let shell = widgets::frame(
9904 ui,
9905 body,
9906 "panels.shell",
9907 widgets::FrameOptions::default().with_layout(
9908 LayoutStyle::column()
9909 .with_width_percent(1.0)
9910 .with_height(260.0)
9911 .with_flex_grow(1.0)
9912 .with_padding(0.0)
9913 .with_gap(0.0),
9914 ),
9915 );
9916 ext_widgets::split_pane(
9917 ui,
9918 shell,
9919 "panels.top_split",
9920 ext_widgets::SplitAxis::Vertical,
9921 state.panels_top_split,
9922 panel_split_options("panels.resize.top"),
9923 |ui, top| {
9924 panel_region(
9925 ui,
9926 top,
9927 "panels.top",
9928 widgets::PanelKind::Top,
9929 "Top",
9930 "Header controls",
9931 );
9932 },
9933 |ui, lower| {
9934 ext_widgets::split_pane(
9935 ui,
9936 lower,
9937 "panels.bottom_split",
9938 ext_widgets::SplitAxis::Vertical,
9939 state.panels_bottom_split,
9940 panel_split_options("panels.resize.bottom"),
9941 |ui, middle| {
9942 ext_widgets::split_pane(
9943 ui,
9944 middle,
9945 "panels.left_split",
9946 ext_widgets::SplitAxis::Horizontal,
9947 state.panels_left_split,
9948 panel_split_options("panels.resize.left"),
9949 |ui, left| {
9950 panel_region(
9951 ui,
9952 left,
9953 "panels.left",
9954 widgets::PanelKind::Left,
9955 "Left",
9956 "Navigation",
9957 );
9958 },
9959 |ui, center_and_right| {
9960 ext_widgets::split_pane(
9961 ui,
9962 center_and_right,
9963 "panels.right_split",
9964 ext_widgets::SplitAxis::Horizontal,
9965 state.panels_right_split,
9966 panel_split_options("panels.resize.right"),
9967 |ui, center| {
9968 panel_region(
9969 ui,
9970 center,
9971 "panels.center",
9972 widgets::PanelKind::Central,
9973 "Central",
9974 "Primary workspace",
9975 );
9976 },
9977 |ui, right| {
9978 panel_region(
9979 ui,
9980 right,
9981 "panels.right",
9982 widgets::PanelKind::Right,
9983 "Right",
9984 "Inspector",
9985 );
9986 },
9987 );
9988 },
9989 );
9990 },
9991 |ui, bottom| {
9992 panel_region(
9993 ui,
9994 bottom,
9995 "panels.bottom",
9996 widgets::PanelKind::Bottom,
9997 "Bottom",
9998 "Status and output",
9999 );
10000 },
10001 );
10002 },
10003 );
10004}
10005
10006fn panel_split_options(action: &'static str) -> ext_widgets::SplitPaneOptions {
10007 let mut options = ext_widgets::SplitPaneOptions::default().with_handle_action(action);
10008 options.handle_thickness = PANELS_SPLIT_HANDLE_THICKNESS;
10009 options.handle_visual = UiVisual::panel(color(58, 70, 88), None, 0.0);
10010 options.handle_hover_visual = Some(UiVisual::panel(color(100, 172, 244), None, 0.0));
10011 options
10012}
10013
10014fn panel_region(
10015 ui: &mut UiDocument,
10016 parent: UiNodeId,
10017 name: &'static str,
10018 kind: widgets::PanelKind,
10019 title: &'static str,
10020 detail: &'static str,
10021) -> UiNodeId {
10022 let panel = widgets::panel(
10023 ui,
10024 parent,
10025 name,
10026 widgets::PanelOptions {
10027 kind,
10028 layout: LayoutStyle::column()
10029 .with_width_percent(1.0)
10030 .with_height_percent(1.0)
10031 .with_padding(10.0)
10032 .with_gap(6.0),
10033 visual: UiVisual::panel(
10034 color(18, 23, 31),
10035 Some(StrokeStyle::new(color(66, 80, 98), 1.0)),
10036 0.0,
10037 ),
10038 accessibility_label: Some(title.to_string()),
10039 ..Default::default()
10040 },
10041 );
10042 widgets::label(
10043 ui,
10044 panel,
10045 format!("{name}.label"),
10046 title,
10047 text(13.0, color(232, 240, 250)),
10048 LayoutStyle::new().with_width_percent(1.0),
10049 );
10050 widgets::label(
10051 ui,
10052 panel,
10053 format!("{name}.detail"),
10054 detail,
10055 text(11.0, color(154, 166, 184)),
10056 LayoutStyle::new().with_width_percent(1.0),
10057 );
10058 panel
10059}
10060
10061fn form_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10062 let body = section_with_min_viewport(ui, parent, "forms", "Forms", UiSize::new(390.0, 0.0));
10063 let section = widgets::form_section(
10064 ui,
10065 body,
10066 "forms.profile",
10067 None::<String>,
10068 widgets::FormSectionOptions::default().with_layout(
10069 LayoutStyle::column()
10070 .with_width_percent(1.0)
10071 .with_padding(12.0)
10072 .with_gap(10.0),
10073 ),
10074 );
10075 profile_form_summary(ui, section.root, state);
10076
10077 let status_row = wrapping_row(ui, section.root, "forms.profile.status_flags", 6.0);
10078 form_status_chip(
10079 ui,
10080 status_row,
10081 "forms.profile.status.dirty",
10082 "dirty",
10083 state.form.dirty,
10084 );
10085 form_status_chip(
10086 ui,
10087 status_row,
10088 "forms.profile.status.pending",
10089 "pending",
10090 state.form.pending,
10091 );
10092 form_status_chip(
10093 ui,
10094 status_row,
10095 "forms.profile.status.submitted",
10096 "submitted",
10097 state.form.submitted,
10098 );
10099
10100 let mut name_options = widgets::FormRowOptions::default().required();
10101 if state.form_name_text.text().trim().is_empty() {
10102 name_options = name_options.invalid("Name is required");
10103 }
10104 let name = widgets::form_row(ui, section.root, "forms.profile.name", name_options);
10105 widgets::field_label(
10106 ui,
10107 name,
10108 "forms.profile.name.label",
10109 "Name",
10110 widgets::FieldLabelOptions::default().required(),
10111 );
10112 form_text_field(
10113 ui,
10114 name,
10115 "forms.profile.name.input",
10116 &state.form_name_text,
10117 FocusedTextInput::FormName,
10118 state,
10119 );
10120 if state.form_name_text.text().trim().is_empty() {
10121 widgets::field_validation_message(
10122 ui,
10123 name,
10124 "forms.profile.name.validation",
10125 ValidationMessage::error("Name is required"),
10126 widgets::ValidationMessageOptions::default(),
10127 );
10128 } else {
10129 widgets::field_help_text(
10130 ui,
10131 name,
10132 "forms.profile.name.help",
10133 "Shown in window titles and project lists.",
10134 widgets::FieldHelpOptions::default(),
10135 );
10136 }
10137
10138 let mut email_options = widgets::FormRowOptions::default().required();
10139 if !profile_email_valid(state.form_email_text.text()) {
10140 email_options = email_options.invalid("Use a complete email address");
10141 }
10142 let email = widgets::form_row(ui, section.root, "forms.profile.email", email_options);
10143 widgets::field_label(
10144 ui,
10145 email,
10146 "forms.profile.email.label",
10147 "Email",
10148 widgets::FieldLabelOptions::default().required(),
10149 );
10150 form_text_field(
10151 ui,
10152 email,
10153 "forms.profile.email.input",
10154 &state.form_email_text,
10155 FocusedTextInput::FormEmail,
10156 state,
10157 );
10158 if profile_email_valid(state.form_email_text.text()) {
10159 widgets::field_help_text(
10160 ui,
10161 email,
10162 "forms.profile.email.help",
10163 "Used for workspace invites and notifications.",
10164 widgets::FieldHelpOptions::default(),
10165 );
10166 } else {
10167 widgets::field_validation_message(
10168 ui,
10169 email,
10170 "forms.profile.email.validation",
10171 ValidationMessage::error("Use a complete email address"),
10172 widgets::ValidationMessageOptions::default(),
10173 );
10174 }
10175
10176 let role = widgets::form_row(
10177 ui,
10178 section.root,
10179 "forms.profile.role",
10180 widgets::FormRowOptions::default(),
10181 );
10182 widgets::field_label(
10183 ui,
10184 role,
10185 "forms.profile.role.label",
10186 "Role",
10187 widgets::FieldLabelOptions::default(),
10188 );
10189 form_text_field(
10190 ui,
10191 role,
10192 "forms.profile.role.input",
10193 &state.form_role_text,
10194 FocusedTextInput::FormRole,
10195 state,
10196 );
10197 widgets::field_validation_message(
10198 ui,
10199 role,
10200 "forms.profile.role.help",
10201 if state.form_role_text.text().trim().is_empty() {
10202 ValidationMessage::warning("Role can be added later")
10203 } else {
10204 ValidationMessage::info(
10205 "Form rows compose labels, controls, help, and validation text.",
10206 )
10207 },
10208 widgets::ValidationMessageOptions::default(),
10209 );
10210
10211 let newsletter = widgets::form_row(
10212 ui,
10213 section.root,
10214 "forms.profile.newsletter",
10215 widgets::FormRowOptions::default().with_accessibility_label("Newsletter preference"),
10216 );
10217 let mut newsletter_options =
10218 widgets::CheckboxOptions::default().with_action("forms.profile.newsletter.toggle");
10219 newsletter_options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
10220 newsletter_options.text_style = text(12.0, color(220, 228, 238));
10221 widgets::checkbox(
10222 ui,
10223 newsletter,
10224 "forms.profile.newsletter.input",
10225 "Send release notes",
10226 state.form_newsletter,
10227 newsletter_options,
10228 );
10229 widgets::field_help_text(
10230 ui,
10231 newsletter,
10232 "forms.profile.newsletter.help",
10233 "Checkboxes participate in the same form state as text fields.",
10234 widgets::FieldHelpOptions::default(),
10235 );
10236
10237 widgets::form_error_summary(
10238 ui,
10239 section.root,
10240 "forms.profile.errors",
10241 &state.form,
10242 widgets::FormErrorSummaryOptions::default(),
10243 );
10244 widgets::label(
10245 ui,
10246 section.root,
10247 "forms.profile.action_help",
10248 "Apply changes saves this draft and keeps editing. Submit profile saves and marks it submitted.",
10249 text(11.0, color(154, 166, 184)),
10250 LayoutStyle::new().with_width_percent(1.0),
10251 );
10252 let action_layout = Layout::row()
10253 .size(LayoutSize::new(
10254 LayoutDimension::percent(1.0),
10255 LayoutDimension::Auto,
10256 ))
10257 .gap(LayoutGap::points(8.0, 8.0))
10258 .flex_wrap(LayoutFlexWrap::Wrap)
10259 .to_layout_style();
10260 widgets::form_action_buttons(
10261 ui,
10262 section.root,
10263 "forms.profile.actions",
10264 &state.form,
10265 widgets::FormActionButtonsOptions::default()
10266 .with_layout(action_layout)
10267 .with_labels(widgets::FormActionLabels {
10268 submit: "Submit profile".to_string(),
10269 apply: "Apply changes".to_string(),
10270 cancel: "Cancel".to_string(),
10271 reset: "Reset".to_string(),
10272 })
10273 .include_reset(true)
10274 .with_action_prefix("forms.profile"),
10275 );
10276 widgets::label(
10277 ui,
10278 section.root,
10279 "forms.profile.status",
10280 format!("Status: {}", state.form_status),
10281 text(11.0, color(154, 166, 184)),
10282 LayoutStyle::new().with_width_percent(1.0),
10283 );
10284}
10285
10286fn overlay_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10287 let body =
10288 section_with_min_viewport(ui, parent, "overlays", "Overlays", UiSize::new(420.0, 0.0));
10289 let header = widgets::collapsing_header(
10290 ui,
10291 body,
10292 "overlays.collapsing",
10293 "Collapsing header",
10294 widgets::CollapsingHeaderOptions::default()
10295 .expanded(state.overlay_expanded)
10296 .with_toggle_action("overlays.collapsing.toggle"),
10297 );
10298 if let Some(panel) = header.body {
10299 widgets::label(
10300 ui,
10301 panel,
10302 "overlays.collapsing.body_text",
10303 "Expanded content lives under the header and remains part of normal layout.",
10304 text(12.0, color(196, 210, 230)),
10305 LayoutStyle::new().with_width_percent(1.0),
10306 );
10307 }
10308
10309 let controls = wrapping_row(ui, body, "overlays.controls", 8.0);
10310 let tooltip_visual = button_visual(58, 78, 96);
10311 let mut tooltip_options = widgets::ButtonOptions::new(LayoutStyle::new().with_height(32.0));
10312 tooltip_options.visual = tooltip_visual;
10313 tooltip_options.hovered_visual = Some(readable_button_hover_visual(tooltip_visual));
10314 tooltip_options.pressed_visual = Some(adjusted_button_visual(tooltip_visual, -62));
10315 tooltip_options.pressed_hovered_visual = Some(adjusted_button_visual(tooltip_visual, -24));
10316 tooltip_options.text_style = text(13.0, color(246, 249, 252));
10317 let tooltip_target = widgets::button(
10318 ui,
10319 controls,
10320 "overlays.tooltip_target",
10321 "Tooltip target",
10322 tooltip_options,
10323 );
10324 ui.node_mut(tooltip_target).set_tooltip(
10325 TooltipContent::new("Tooltip")
10326 .body("Tooltips render as overlay surfaces anchored to a target.")
10327 .shortcut_label("Ctrl+K")
10328 .disabled_reason("Disabled reasons can be announced without changing the trigger."),
10329 );
10330 ui.node_mut(tooltip_target)
10331 .set_tooltip_placement(TooltipPlacement::Below);
10332 ui.node_mut(tooltip_target)
10333 .set_tooltip_size(UiSize::new(240.0, 148.0));
10334 button(
10335 ui,
10336 controls,
10337 "overlays.popup.toggle",
10338 if state.overlay_popup_open {
10339 "Close popup"
10340 } else {
10341 "Open popup"
10342 },
10343 "overlays.popup.toggle",
10344 button_visual(48, 112, 184),
10345 );
10346 button(
10347 ui,
10348 controls,
10349 "overlays.modal.open",
10350 "Open modal",
10351 "overlays.modal.open",
10352 button_visual(58, 78, 96),
10353 );
10354
10355 widgets::label(
10356 ui,
10357 body,
10358 "overlays.tooltip_rect.label",
10359 "A right-edge target keeps its tooltip inside the preview.",
10360 text(12.0, color(166, 176, 190)),
10361 LayoutStyle::new().with_width_percent(1.0),
10362 );
10363 let preview_viewport = UiRect::new(0.0, 0.0, 420.0, 112.0);
10364 let tooltip_target = UiRect::new(328.0, 42.0, 64.0, 28.0);
10365 let tooltip_size = UiSize::new(176.0, 58.0);
10366 let placed_tooltip = widgets::tooltip::tooltip_rect(
10367 tooltip_target,
10368 tooltip_size,
10369 preview_viewport,
10370 TooltipPlacement::Right,
10371 8.0,
10372 None,
10373 );
10374 let clamped_preview = ui.add_child(
10375 body,
10376 UiNode::container(
10377 "overlays.tooltip_rect.preview",
10378 LayoutStyle::new()
10379 .with_width_percent(1.0)
10380 .with_height(112.0)
10381 .with_flex_shrink(0.0),
10382 )
10383 .with_visual(UiVisual::panel(
10384 color(12, 16, 22),
10385 Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
10386 4.0,
10387 )),
10388 );
10389 ui.add_child(
10390 clamped_preview,
10391 UiNode::scene(
10392 "overlays.tooltip_rect.scene",
10393 vec![
10394 ScenePrimitive::Line {
10395 from: UiPoint::new(placed_tooltip.right() + 2.0, placed_tooltip.y + 29.0),
10396 to: UiPoint::new(tooltip_target.x - 2.0, tooltip_target.y + 14.0),
10397 stroke: StrokeStyle::new(color(92, 106, 128), 1.0),
10398 },
10399 ScenePrimitive::Rect(
10400 PaintRect::solid(placed_tooltip, color(24, 29, 38))
10401 .stroke(AlignedStroke::inside(StrokeStyle::new(
10402 color(92, 106, 128),
10403 1.0,
10404 )))
10405 .corner_radii(CornerRadii::uniform(4.0)),
10406 ),
10407 ScenePrimitive::Text(
10408 PaintText::new(
10409 "Tooltip",
10410 UiRect::new(
10411 placed_tooltip.x + 12.0,
10412 placed_tooltip.y + 9.0,
10413 placed_tooltip.width - 24.0,
10414 18.0,
10415 ),
10416 text(12.0, color(225, 233, 244)),
10417 )
10418 .multiline(false),
10419 ),
10420 ScenePrimitive::Text(
10421 PaintText::new(
10422 "Placed inside",
10423 UiRect::new(
10424 placed_tooltip.x + 12.0,
10425 placed_tooltip.y + 31.0,
10426 placed_tooltip.width - 24.0,
10427 18.0,
10428 ),
10429 text(10.0, color(156, 170, 190)),
10430 )
10431 .multiline(false),
10432 ),
10433 ScenePrimitive::Rect(
10434 PaintRect::solid(tooltip_target, color(48, 112, 184))
10435 .stroke(AlignedStroke::inside(StrokeStyle::new(
10436 color(132, 190, 255),
10437 1.0,
10438 )))
10439 .corner_radii(CornerRadii::uniform(3.0)),
10440 ),
10441 ScenePrimitive::Text(
10442 PaintText::new("Target", tooltip_target, text(10.0, color(240, 247, 255)))
10443 .horizontal_align(TextHorizontalAlign::Center)
10444 .vertical_align(TextVerticalAlign::Center)
10445 .multiline(false),
10446 ),
10447 ],
10448 LayoutStyle::new()
10449 .with_width_percent(1.0)
10450 .with_height_percent(1.0),
10451 ),
10452 );
10453
10454 widgets::label(
10455 ui,
10456 body,
10457 "overlays.popup.label",
10458 "Popup panel",
10459 text(12.0, color(166, 176, 190)),
10460 LayoutStyle::new().with_width_percent(1.0),
10461 );
10462 widgets::label(
10463 ui,
10464 body,
10465 "overlays.popup.status",
10466 if state.overlay_popup_open {
10467 "Popup overlay is open."
10468 } else {
10469 "Popup overlay is closed."
10470 },
10471 text(12.0, color(196, 210, 230)),
10472 LayoutStyle::new().with_width_percent(1.0),
10473 );
10474 let popup_host_layout = if state.overlay_popup_open {
10475 LayoutStyle::column()
10476 .with_width_percent(1.0)
10477 .with_height(128.0)
10478 .with_flex_shrink(0.0)
10479 } else {
10480 LayoutStyle::column()
10481 .with_width_percent(1.0)
10482 .with_padding(10.0)
10483 .with_flex_shrink(0.0)
10484 };
10485 let popup_host = ui.add_child(
10486 body,
10487 UiNode::container("overlays.popup.host", popup_host_layout).with_visual(UiVisual::panel(
10488 color(12, 16, 22),
10489 Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
10490 4.0,
10491 )),
10492 );
10493 if state.overlay_popup_open {
10494 let popup = ext_widgets::popup_panel(
10495 ui,
10496 popup_host,
10497 "overlays.popup_panel",
10498 UiRect::new(10.0, 10.0, 220.0, 104.0),
10499 ext_widgets::PopupOptions {
10500 z_index: 4,
10501 portal: UiPortalTarget::Parent,
10502 accessibility: Some(
10503 AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup preview"),
10504 ),
10505 ..Default::default()
10506 },
10507 );
10508 let popup_body = ui.add_child(
10509 popup,
10510 UiNode::container(
10511 "overlays.popup_panel.body",
10512 LayoutStyle::column()
10513 .with_width_percent(1.0)
10514 .with_height_percent(1.0)
10515 .with_padding(10.0)
10516 .with_gap(6.0),
10517 ),
10518 );
10519 let popup_header = row(ui, popup_body, "overlays.popup_panel.header", 8.0);
10520 widgets::label(
10521 ui,
10522 popup_header,
10523 "overlays.popup_panel.label",
10524 "Popup panel",
10525 text(12.0, color(220, 228, 238)),
10526 LayoutStyle::new().with_width_percent(1.0),
10527 );
10528 let mut close = widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0))
10529 .with_action("overlays.popup.close");
10530 close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
10531 close.hovered_visual = Some(button_visual(54, 70, 92));
10532 close.text_style = text(12.0, color(220, 228, 238));
10533 widgets::button(ui, popup_header, "overlays.popup_panel.close", "x", close);
10534 widgets::label(
10535 ui,
10536 popup_body,
10537 "overlays.popup_panel.body_text",
10538 "Popup content is conditionally rendered.",
10539 text(11.0, color(196, 210, 230)),
10540 LayoutStyle::new().with_width_percent(1.0),
10541 );
10542 } else {
10543 widgets::label(
10544 ui,
10545 popup_host,
10546 "overlays.popup.empty",
10547 "Open the popup to render an overlay inside this host.",
10548 text(12.0, color(154, 166, 184)),
10549 LayoutStyle::new().with_width_percent(1.0),
10550 );
10551 }
10552
10553 widgets::label(
10554 ui,
10555 body,
10556 "overlays.toasts.label",
10557 "Toasts",
10558 text(12.0, color(166, 176, 190)),
10559 LayoutStyle::new().with_width_percent(1.0),
10560 );
10561 let toast_controls = row(ui, body, "overlays.toasts.controls", 10.0);
10562 button(
10563 ui,
10564 toast_controls,
10565 "overlays.toasts.show",
10566 "Show toast",
10567 "toast.show",
10568 button_visual(48, 112, 184),
10569 );
10570 button(
10571 ui,
10572 toast_controls,
10573 "overlays.toasts.hide",
10574 "Hide",
10575 "toast.hide",
10576 button_visual(58, 78, 96),
10577 );
10578 widgets::label(
10579 ui,
10580 body,
10581 "overlays.toasts.status",
10582 if state.toast_visible {
10583 "Toast overlay is visible."
10584 } else {
10585 "Toast overlay is hidden."
10586 },
10587 text(12.0, color(196, 210, 230)),
10588 LayoutStyle::new().with_width_percent(1.0),
10589 );
10590 widgets::label(
10591 ui,
10592 body,
10593 "overlays.toasts.action_status",
10594 format!("Action: {}", state.toast_action_status),
10595 text(12.0, color(154, 166, 184)),
10596 LayoutStyle::new().with_width_percent(1.0),
10597 );
10598
10599 if state.overlay_modal_open {
10600 let modal = widgets::modal_dialog(
10601 ui,
10602 parent,
10603 "overlays.modal",
10604 "Modal dialog",
10605 widgets::ModalDialogOptions::default()
10606 .with_size(320.0, 180.0)
10607 .with_close_action("overlays.modal.close")
10608 .with_dismissal(ext_widgets::DialogDismissal::MODAL)
10609 .with_focus_restore(FocusRestoreTarget::Previous),
10610 );
10611 widgets::label(
10612 ui,
10613 modal.body,
10614 "overlays.modal.body.text",
10615 "Modal dialogs are portaled to the application overlay, include a scrim, and trap focus.",
10616 text(12.0, color(220, 228, 238)),
10617 LayoutStyle::new().with_width_percent(1.0),
10618 );
10619 button(
10620 ui,
10621 modal.body,
10622 "overlays.modal.body.close",
10623 "Close modal",
10624 "overlays.modal.close",
10625 button_visual(48, 112, 184),
10626 );
10627 }
10628}
10629
10630fn drag_drop_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10631 let body = section_with_min_viewport(
10632 ui,
10633 parent,
10634 "drag_drop",
10635 "Drag and drop",
10636 UiSize::new(420.0, 0.0),
10637 );
10638 widgets::label(
10639 ui,
10640 body,
10641 "drag_drop.sources.label",
10642 "Drag sources",
10643 text(12.0, color(166, 176, 190)),
10644 LayoutStyle::new().with_width_percent(1.0),
10645 );
10646 let sources = wrapping_row(ui, body, "drag_drop.sources", 8.0);
10647 widgets::dnd_drag_source(
10648 ui,
10649 sources,
10650 "drag_drop.text_source",
10651 "Text payload",
10652 DragPayload::text("Operad payload"),
10653 widgets::DragSourceOptions::default()
10654 .with_layout(drag_source_layout())
10655 .with_kind(DragDropSurfaceKind::ListRow)
10656 .with_allowed_operations([DragOperation::Copy, DragOperation::Move])
10657 .with_action("drag_drop.text_source")
10658 .with_accessibility_hint("Start a text drag operation"),
10659 );
10660 widgets::dnd_drag_source(
10661 ui,
10662 sources,
10663 "drag_drop.file_source",
10664 "File payload",
10665 DragPayload::files(["/tmp/showcase.scene"]),
10666 widgets::DragSourceOptions::default()
10667 .with_layout(drag_source_layout())
10668 .with_kind(DragDropSurfaceKind::Asset)
10669 .with_drag_image_policy(widgets::DragImagePolicy::image_key(
10670 BuiltInIcon::Folder.key(),
10671 UiSize::new(120.0, 36.0),
10672 UiPoint::new(10.0, 10.0),
10673 ))
10674 .with_allowed_operations([DragOperation::Copy])
10675 .with_action("drag_drop.file_source"),
10676 );
10677 widgets::dnd_drag_source(
10678 ui,
10679 sources,
10680 "drag_drop.bytes_source",
10681 "Image bytes",
10682 DragPayload::bytes(DragBytes::new("image/png", vec![137, 80, 78, 71]).name("sprite.png")),
10683 widgets::DragSourceOptions::default()
10684 .with_layout(drag_source_layout())
10685 .with_kind(DragDropSurfaceKind::Asset)
10686 .with_action("drag_drop.bytes_source")
10687 .without_drag_image(),
10688 );
10689
10690 widgets::label(
10691 ui,
10692 body,
10693 "drag_drop.zones.label",
10694 "Drop zones",
10695 text(12.0, color(166, 176, 190)),
10696 LayoutStyle::new().with_width_percent(1.0),
10697 );
10698 let zones = wrapping_row(ui, body, "drag_drop.zones", 8.0);
10699 let accepted_options = widgets::DropZoneOptions::default()
10700 .with_layout(drop_zone_layout())
10701 .with_kind(DragDropSurfaceKind::EditorSurface)
10702 .with_accepted_payload(DropPayloadFilter::empty().text())
10703 .with_accepted_operations([DragOperation::Copy, DragOperation::Move])
10704 .with_action("drag_drop.accept_text")
10705 .with_accessibility_hint("Accepts text payloads");
10706 let accepted = widgets::dnd_drop_zone(
10707 ui,
10708 zones,
10709 "drag_drop.accept_text",
10710 "Text accepted",
10711 accepted_options.clone(),
10712 );
10713 widgets::drag_drop::dnd_apply_drop_zone_preview(
10714 ui,
10715 accepted.root,
10716 &accepted_options,
10717 widgets::drag_drop::DropZonePreviewState::Accepted,
10718 );
10719
10720 let rejected_options = widgets::DropZoneOptions::default()
10721 .with_layout(drop_zone_layout())
10722 .with_kind(DragDropSurfaceKind::Asset)
10723 .with_accepted_payload(DropPayloadFilter::empty().files())
10724 .with_action("drag_drop.files_only");
10725 let rejected = widgets::dnd_drop_zone(
10726 ui,
10727 zones,
10728 "drag_drop.files_only",
10729 "Files only",
10730 rejected_options.clone(),
10731 );
10732 widgets::drag_drop::dnd_apply_drop_zone_preview(
10733 ui,
10734 rejected.root,
10735 &rejected_options,
10736 widgets::drag_drop::DropZonePreviewState::Rejected,
10737 );
10738 let image_options = widgets::DropZoneOptions::default()
10739 .with_layout(drop_zone_layout())
10740 .with_kind(DragDropSurfaceKind::Asset)
10741 .with_accepted_payload(DropPayloadFilter::empty().mime_type("image/*"))
10742 .with_accepted_operations([DragOperation::Copy])
10743 .with_action("drag_drop.image_bytes");
10744 let image_zone = widgets::dnd_drop_zone(
10745 ui,
10746 zones,
10747 "drag_drop.image_bytes",
10748 "Image bytes",
10749 image_options.clone(),
10750 );
10751 widgets::drag_drop::dnd_apply_drop_zone_preview(
10752 ui,
10753 image_zone.root,
10754 &image_options,
10755 widgets::drag_drop::DropZonePreviewState::Hovered,
10756 );
10757
10758 let disabled_options = widgets::DropZoneOptions::default()
10759 .with_layout(drop_zone_layout())
10760 .with_kind(DragDropSurfaceKind::EditorSurface)
10761 .with_accepted_payload(DropPayloadFilter::any())
10762 .with_action("drag_drop.disabled")
10763 .disabled();
10764 let disabled_zone = widgets::dnd_drop_zone(
10765 ui,
10766 zones,
10767 "drag_drop.disabled",
10768 "Disabled",
10769 disabled_options.clone(),
10770 );
10771 widgets::drag_drop::dnd_apply_drop_zone_preview(
10772 ui,
10773 disabled_zone.root,
10774 &disabled_options,
10775 widgets::drag_drop::DropZonePreviewState::Disabled,
10776 );
10777
10778 let operation_row = wrapping_row(ui, body, "drag_drop.operations", 6.0);
10779 dnd_operation_chip(ui, operation_row, "drag_drop.operation.copy", "copy");
10780 dnd_operation_chip(ui, operation_row, "drag_drop.operation.move", "move");
10781 dnd_operation_chip(ui, operation_row, "drag_drop.operation.link", "link");
10782 widgets::label(
10783 ui,
10784 body,
10785 "drag_drop.status",
10786 format!("Status: {}", state.drag_drop_status),
10787 text(11.0, color(154, 166, 184)),
10788 LayoutStyle::new().with_width_percent(1.0),
10789 );
10790}
10791
10792fn media_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10793 let body = section_with_min_viewport(
10794 ui,
10795 parent,
10796 "media",
10797 "Media",
10798 UiSize::new(MEDIA_ICON_TILE_WIDTH, 0.0),
10799 );
10800 widgets::label(
10801 ui,
10802 body,
10803 "media.icons.label",
10804 "Built-in icons",
10805 text(12.0, color(166, 176, 190)),
10806 LayoutStyle::new().with_width_percent(1.0),
10807 );
10808 let icon_columns = media_icon_columns(state);
10809 let icons = media_icon_grid(
10810 ui,
10811 body,
10812 "media.icons",
10813 icon_columns,
10814 BuiltInIcon::COMMON.len(),
10815 );
10816 for icon in BuiltInIcon::COMMON {
10817 media_icon_tile(ui, icons, icon);
10818 }
10819
10820 widgets::label(
10821 ui,
10822 body,
10823 "media.variants.label",
10824 "Image variants",
10825 text(12.0, color(166, 176, 190)),
10826 LayoutStyle::new().with_width_percent(1.0),
10827 );
10828 let variants = wrapping_row(ui, body, "media.variants", 10.0);
10829 widgets::image(
10830 ui,
10831 variants,
10832 "media.image.user_png",
10833 ImageContent::from(ImageHandle::app(SHOWCASE_USER_IMAGE_KEY)),
10834 widgets::ImageOptions::default()
10835 .with_layout(media_preview_image_layout())
10836 .with_accessibility_label("User supplied PNG image"),
10837 );
10838 widgets::image(
10839 ui,
10840 variants,
10841 "media.image.untinted",
10842 icon_image(BuiltInIcon::Play),
10843 widgets::ImageOptions::default()
10844 .with_layout(media_preview_image_layout())
10845 .with_accessibility_label("Untinted play icon"),
10846 );
10847 widgets::image(
10848 ui,
10849 variants,
10850 "media.image.warning",
10851 ImageContent::new(BuiltInIcon::Warning.key()).tinted(color(232, 186, 88)),
10852 widgets::ImageOptions::default()
10853 .with_layout(media_preview_image_layout())
10854 .with_accessibility_label("Tinted warning icon"),
10855 );
10856 widgets::image(
10857 ui,
10858 variants,
10859 "media.image.shader",
10860 ImageContent::new(BuiltInIcon::Grid.key()).tinted(color(118, 183, 255)),
10861 widgets::ImageOptions::default()
10862 .with_layout(media_preview_image_layout())
10863 .with_shader(ShaderEffect::tint(color(169, 119, 255), 0.65))
10864 .with_accessibility_label("Shader-decorated grid icon"),
10865 );
10866}
10867
10868fn shader_effect_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10869 let body = section_with_min_viewport(
10870 ui,
10871 parent,
10872 "shaders",
10873 "Shader effects",
10874 UiSize::new(420.0, 280.0),
10875 );
10876 widgets::label(
10877 ui,
10878 body,
10879 "shaders.effects.label",
10880 "Built-in effects",
10881 text(12.0, color(166, 176, 190)),
10882 LayoutStyle::new().with_width_percent(1.0),
10883 );
10884 let phase = (state.progress_phase / std::f32::consts::TAU).fract();
10885 let previews = wrapping_row(ui, body, "shaders.effects", 10.0);
10886 shader_effect_preview_card(ui, previews, "base", "Base", None);
10887 shader_effect_preview_card(
10888 ui,
10889 previews,
10890 "tint",
10891 "Tint",
10892 Some(ShaderEffect::tint(color(252, 186, 90), 0.72)),
10893 );
10894 shader_effect_preview_card(
10895 ui,
10896 previews,
10897 "shine",
10898 "Shine",
10899 Some(ShaderEffect::shine(phase, 0.55).uniform("width", 0.14)),
10900 );
10901 shader_effect_preview_card(
10902 ui,
10903 previews,
10904 "glow",
10905 "Glow",
10906 Some(ShaderEffect::glow(color(118, 183, 255), 0.9, 7.0)),
10907 );
10908
10909 widgets::label(
10910 ui,
10911 body,
10912 "shaders.widgets.label",
10913 "Applied to widgets",
10914 text(12.0, color(166, 176, 190)),
10915 LayoutStyle::new().with_width_percent(1.0),
10916 );
10917 let panel = ui.add_child(
10918 body,
10919 UiNode::container(
10920 "shaders.widgets",
10921 LayoutStyle::column()
10922 .with_width_percent(1.0)
10923 .with_padding(10.0)
10924 .with_gap(10.0)
10925 .with_flex_shrink(0.0),
10926 )
10927 .with_visual(UiVisual::panel(
10928 color(13, 18, 25),
10929 Some(StrokeStyle::new(color(48, 61, 78), 1.0)),
10930 4.0,
10931 )),
10932 );
10933 let control_row = wrapping_row(ui, panel, "shaders.widgets.controls", 10.0);
10934 let mut shine_button = widgets::ButtonOptions::new(
10935 LayoutStyle::new()
10936 .with_width(150.0)
10937 .with_height(34.0)
10938 .with_flex_shrink(0.0),
10939 );
10940 shine_button.leading_image = Some(icon_image(BuiltInIcon::Settings));
10941 shine_button.image_shader = Some(ShaderEffect::tint(color(111, 203, 159), 0.85));
10942 shine_button.shader = Some(ShaderEffect::shine(phase, 0.45).uniform("width", 0.12));
10943 widgets::button(
10944 ui,
10945 control_row,
10946 "shaders.widgets.button",
10947 "Shine button",
10948 shine_button,
10949 );
10950
10951 widgets::checkbox_with_state(
10952 ui,
10953 control_row,
10954 "shaders.widgets.checkbox",
10955 "Glow check",
10956 widgets::CheckboxState::Checked,
10957 widgets::CheckboxOptions::default()
10958 .with_check_color(color(118, 183, 255))
10959 .with_check_shader(ShaderEffect::glow(color(118, 183, 255), 1.0, 4.0)),
10960 );
10961
10962 let progress_value = smooth_loop(state.progress_phase * 0.8, 0.2) * 100.0;
10963 let mut progress = ext_widgets::ProgressIndicatorOptions::default();
10964 progress.layout = LayoutStyle::new().with_width_percent(1.0).with_height(12.0);
10965 progress.fill_visual = UiVisual::panel(color(111, 203, 159), None, 4.0);
10966 progress.fill_shader = Some(ShaderEffect::shine(phase, 0.5).uniform("width", 0.16));
10967 progress.accessibility_label = Some("Shadered progress fill".to_string());
10968 ext_widgets::progress_indicator(
10969 ui,
10970 panel,
10971 "shaders.widgets.progress",
10972 ext_widgets::ProgressIndicatorValue::percent(progress_value),
10973 progress,
10974 );
10975
10976 let mut slider_options = widgets::SliderOptions::default();
10977 slider_options.layout = LayoutStyle::new()
10978 .with_width_percent(1.0)
10979 .with_height(28.0)
10980 .with_flex_shrink(0.0);
10981 slider_options.fill_shader = Some(ShaderEffect::tint(color(169, 119, 255), 0.55));
10982 slider_options.thumb_shader = Some(ShaderEffect::glow(color(252, 186, 90), 0.9, 4.0));
10983 slider_options.accessibility_label = Some("Shadered slider".to_string());
10984 widgets::slider(
10985 ui,
10986 panel,
10987 "shaders.widgets.slider",
10988 smooth_loop(state.progress_phase * 0.6, 0.4),
10989 0.0..1.0,
10990 slider_options,
10991 );
10992}
10993
10994fn shader_effect_preview_card(
10995 ui: &mut UiDocument,
10996 parent: UiNodeId,
10997 name: &'static str,
10998 label: &'static str,
10999 shader: Option<ShaderEffect>,
11000) {
11001 let tile = ui.add_child(
11002 parent,
11003 UiNode::container(
11004 format!("shaders.effect_tile.{name}"),
11005 LayoutStyle::column()
11006 .with_width(96.0)
11007 .with_height(104.0)
11008 .with_padding(8.0)
11009 .with_gap(8.0)
11010 .with_flex_shrink(0.0),
11011 )
11012 .with_visual(UiVisual::panel(
11013 color(17, 22, 30),
11014 Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
11015 4.0,
11016 ))
11017 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(label)),
11018 );
11019 let mut swatch = UiNode::container(
11020 format!("shaders.effect.{name}.swatch"),
11021 LayoutStyle::new()
11022 .with_width_percent(1.0)
11023 .with_height(50.0)
11024 .with_flex_shrink(0.0),
11025 )
11026 .with_visual(UiVisual::panel(
11027 color(64, 109, 194),
11028 Some(StrokeStyle::new(color(138, 164, 194), 1.0)),
11029 8.0,
11030 ));
11031 if let Some(shader) = shader {
11032 swatch = swatch.with_shader(shader);
11033 }
11034 ui.add_child(tile, swatch);
11035 widgets::label(
11036 ui,
11037 tile,
11038 format!("shaders.effect.{name}.label"),
11039 label,
11040 text(11.0, color(204, 216, 232)),
11041 LayoutStyle::new().with_width_percent(1.0),
11042 );
11043}
11044
11045fn shader_lab_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11046 let body = section_with_min_viewport(
11047 ui,
11048 parent,
11049 "shader_lab",
11050 "Shader lab",
11051 UiSize::new(SHADER_LAB_CONTENT_MIN_WIDTH, SHADER_LAB_CONTENT_MIN_HEIGHT),
11052 );
11053 let source_error = state.shader_lab_source_error.as_deref();
11054 let source_valid = source_error.is_none();
11055
11056 let mut split_options = ext_widgets::SplitPaneOptions::default()
11057 .with_handle_action("shader_lab.workspace.resize")
11058 .with_handle_hover_visual(UiVisual::panel(color(96, 166, 238), None, 2.0));
11059 split_options.layout = Layout::row()
11060 .size(LayoutSize::new(
11061 LayoutDimension::percent(1.0),
11062 LayoutDimension::points(SHADER_LAB_WORKSPACE_HEIGHT),
11063 ))
11064 .min_size(LayoutSize::points(
11065 SHADER_LAB_CONTENT_MIN_WIDTH,
11066 SHADER_LAB_WORKSPACE_HEIGHT,
11067 ))
11068 .flex(0.0, 0.0, LayoutDimension::Auto)
11069 .to_layout_style();
11070 split_options.handle_thickness = SHADER_LAB_SPLIT_HANDLE_THICKNESS;
11071 split_options.handle_visual = UiVisual::panel(color(48, 61, 78), None, 2.0);
11072
11073 ext_widgets::split_pane(
11074 ui,
11075 body,
11076 "shader_lab.workspace",
11077 ext_widgets::SplitAxis::Horizontal,
11078 state.shader_lab_split,
11079 split_options,
11080 |ui, preview_pane| {
11081 shader_lab_preview_column(ui, preview_pane, state, source_error, source_valid);
11082 },
11083 |ui, editor_pane| {
11084 shader_lab_editor_column(ui, editor_pane, state, source_error);
11085 },
11086 );
11087}
11088
11089fn shader_lab_preview_column(
11090 ui: &mut UiDocument,
11091 parent: UiNodeId,
11092 state: &ShowcaseState,
11093 source_error: Option<&str>,
11094 source_valid: bool,
11095) {
11096 let preview_column = ui.add_child(
11097 parent,
11098 UiNode::container(
11099 "shader_lab.preview.column",
11100 LayoutStyle::column()
11101 .with_width_percent(1.0)
11102 .with_height_percent(1.0)
11103 .with_gap(8.0)
11104 .with_flex_shrink(1.0),
11105 ),
11106 );
11107 let target_row = row(ui, preview_column, "shader_lab.target.row", 8.0);
11108 widgets::label(
11109 ui,
11110 target_row,
11111 "shader_lab.target.caption",
11112 "Preview",
11113 text(12.0, color(166, 176, 190)),
11114 LayoutStyle::new()
11115 .with_width(58.0)
11116 .with_height(30.0)
11117 .with_flex_shrink(0.0),
11118 );
11119 shader_lab_dropdown_select(
11120 ui,
11121 target_row,
11122 "shader_lab.target",
11123 &shader_lab_target_options(),
11124 &state.shader_lab_target_menu,
11125 160.0,
11126 "Preview target",
11127 "Shader lab preview target",
11128 );
11129 shader_lab_preview_controls(ui, preview_column, state);
11130
11131 let preview = ui.add_child(
11132 preview_column,
11133 UiNode::container(
11134 "shader_lab.preview.surface",
11135 LayoutStyle::column()
11136 .with_width_percent(1.0)
11137 .with_height(0.0)
11138 .with_flex_grow(1.0)
11139 .with_padding(18.0)
11140 .with_gap(8.0)
11141 .with_align_items(taffy::prelude::AlignItems::Center)
11142 .with_justify_content(taffy::prelude::JustifyContent::Center)
11143 .with_flex_shrink(0.0),
11144 )
11145 .with_visual(UiVisual::panel(
11146 color(8, 12, 18),
11147 Some(StrokeStyle::new(color(48, 61, 78), 1.0)),
11148 4.0,
11149 )),
11150 );
11151 shader_lab_preview(ui, preview, state, source_valid);
11152
11153 if let Some(status) = shader_lab_status_label(state, source_error) {
11154 widgets::label(
11155 ui,
11156 preview_column,
11157 "shader_lab.preview.status",
11158 status,
11159 text(11.0, color(166, 176, 190)),
11160 LayoutStyle::new().with_width_percent(1.0),
11161 );
11162 }
11163 shader_lab_material_contract_demo(ui, preview_column, state);
11164}
11165
11166fn shader_lab_preview_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11167 let controls = ui.add_child(
11168 parent,
11169 UiNode::container(
11170 "shader_lab.preview.controls",
11171 LayoutStyle::column()
11172 .with_width_percent(1.0)
11173 .with_gap(6.0)
11174 .with_flex_shrink(0.0),
11175 ),
11176 );
11177 let text_row = wrapping_row(ui, controls, "shader_lab.preview.text_controls", 8.0);
11178 shader_lab_option_checkbox(
11179 ui,
11180 text_row,
11181 "shader_lab.frame_text.toggle",
11182 "Frame",
11183 state.shader_lab_show_frame_text,
11184 );
11185 shader_lab_option_checkbox(
11186 ui,
11187 text_row,
11188 "shader_lab.button_text.toggle",
11189 "Button",
11190 state.shader_lab_show_button_text,
11191 );
11192
11193 let style_row = wrapping_row(ui, controls, "shader_lab.preview.style_controls", 8.0);
11194 shader_lab_slider_control(
11195 ui,
11196 style_row,
11197 "shader_lab.surface.stroke",
11198 "Border",
11199 state.shader_lab_surface_stroke_width,
11200 SHADER_LAB_SURFACE_STROKE_MAX,
11201 1,
11202 );
11203 shader_lab_slider_control(
11204 ui,
11205 style_row,
11206 "shader_lab.surface.radius",
11207 "Radius",
11208 state.shader_lab_surface_radius,
11209 SHADER_LAB_SURFACE_RADIUS_MAX,
11210 0,
11211 );
11212}
11213
11214fn shader_lab_material_contract_demo(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11215 let panel = ui.add_child(
11216 parent,
11217 UiNode::container(
11218 "shader_lab.material",
11219 LayoutStyle::column()
11220 .with_width_percent(1.0)
11221 .with_gap(6.0)
11222 .with_flex_shrink(0.0),
11223 ),
11224 );
11225 widgets::label(
11226 ui,
11227 panel,
11228 "shader_lab.material.title",
11229 "Material",
11230 text(12.0, color(186, 198, 216)),
11231 LayoutStyle::new().with_width_percent(1.0),
11232 );
11233 let controls = wrapping_row(ui, panel, "shader_lab.material.controls", 8.0);
11234 shader_lab_labeled_dropdown(
11235 ui,
11236 controls,
11237 "shader_lab.material.shader",
11238 "Shader",
11239 &shader_lab_material_shader_options(),
11240 &state.shader_lab_material_shader_menu,
11241 132.0,
11242 );
11243 shader_lab_labeled_dropdown(
11244 ui,
11245 controls,
11246 "shader_lab.material.shape",
11247 "Shape",
11248 &shader_lab_material_shape_options(),
11249 &state.shader_lab_material_shape_menu,
11250 132.0,
11251 );
11252 shader_lab_labeled_dropdown(
11253 ui,
11254 controls,
11255 "shader_lab.material.geometry",
11256 "Geometry",
11257 &shader_lab_material_geometry_options(),
11258 &state.shader_lab_material_geometry_menu,
11259 140.0,
11260 );
11261 shader_lab_slider_control(
11262 ui,
11263 controls,
11264 "shader_lab.material.outset",
11265 "Outset",
11266 state.shader_lab_material_outset,
11267 SHADER_LAB_MATERIAL_OUTSET_MAX,
11268 0,
11269 );
11270
11271 let contracts_layout = Layout::column()
11272 .size(LayoutSize::new(
11273 LayoutDimension::percent(1.0),
11274 LayoutDimension::Auto,
11275 ))
11276 .padding(LayoutSpacing::new(
11277 LayoutLength::points(4.0),
11278 LayoutLength::points(4.0),
11279 LayoutLength::points(SHADER_LAB_MATERIAL_OUTSET_MAX),
11280 LayoutLength::points(8.0),
11281 ))
11282 .flex(0.0, 0.0, LayoutDimension::Auto)
11283 .to_layout_style();
11284 let contracts_shell = ui.add_child(
11285 panel,
11286 UiNode::container("shader_lab.material.contracts.shell", contracts_layout),
11287 );
11288 let row = wrapping_row(ui, contracts_shell, "shader_lab.material.contracts", 8.0);
11289 shader_lab_material_chip(
11290 ui,
11291 row,
11292 "shader_lab.material.current",
11293 "Selected",
11294 shader_lab_selected_material(state),
11295 shader_lab_material_visual(state.shader_lab_material_shape),
11296 );
11297 shader_lab_material_chip(
11298 ui,
11299 row,
11300 "shader_lab.material.outset",
11301 "Glow",
11302 ElementMaterial::shader(ShaderEffect::glow(
11303 color(100, 180, 255),
11304 0.95,
11305 SHADER_LAB_MATERIAL_OUTSET,
11306 ))
11307 .with_paint_outset(LayoutInsets::points(
11308 state
11309 .shader_lab_material_outset
11310 .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11311 )),
11312 UiVisual::panel(color(32, 64, 96), None, 8.0),
11313 );
11314 shader_lab_material_chip(
11315 ui,
11316 row,
11317 "shader_lab.material.circle_hit",
11318 "Circle",
11319 ElementMaterial::new()
11320 .with_clip_shape(ElementShape::circle())
11321 .with_hit_shape(ElementShape::circle()),
11322 UiVisual::panel(
11323 color(74, 133, 198),
11324 Some(StrokeStyle::new(color(212, 232, 255), 1.0)),
11325 999.0,
11326 ),
11327 );
11328 shader_lab_material_chip(
11329 ui,
11330 row,
11331 "shader_lab.material.geometry_chip",
11332 "Warp",
11333 ElementMaterial::new()
11334 .with_paint_outset(LayoutInsets::points(8.0))
11335 .with_geometry_effect(GeometryEffect::wave(8.0)),
11336 UiVisual::panel(
11337 color(101, 70, 170),
11338 Some(StrokeStyle::new(color(214, 196, 255), 1.0)),
11339 6.0,
11340 ),
11341 );
11342}
11343
11344fn shader_lab_labeled_dropdown(
11345 ui: &mut UiDocument,
11346 parent: UiNodeId,
11347 name: &'static str,
11348 label: &'static str,
11349 options: &[ext_widgets::SelectOption],
11350 state: &ext_widgets::SelectMenuState,
11351 width: f32,
11352) {
11353 let control = ui.add_child(
11354 parent,
11355 UiNode::container(
11356 format!("{name}.control"),
11357 LayoutStyle::row()
11358 .with_width(width + 74.0)
11359 .with_height(30.0)
11360 .with_gap(6.0)
11361 .with_align_items(taffy::prelude::AlignItems::Center)
11362 .with_flex_shrink(0.0),
11363 ),
11364 );
11365 widgets::label(
11366 ui,
11367 control,
11368 format!("{name}.caption"),
11369 label,
11370 text(12.0, color(166, 176, 190)),
11371 LayoutStyle::new()
11372 .with_width(66.0)
11373 .with_height(30.0)
11374 .with_flex_shrink(0.0),
11375 );
11376 shader_lab_dropdown_select(ui, control, name, options, state, width, label, label);
11377}
11378
11379fn shader_lab_dropdown_select(
11380 ui: &mut UiDocument,
11381 parent: UiNodeId,
11382 name: &'static str,
11383 options: &[ext_widgets::SelectOption],
11384 state: &ext_widgets::SelectMenuState,
11385 width: f32,
11386 placeholder: &'static str,
11387 accessibility_label: &'static str,
11388) {
11389 let anchor = ui.add_child(
11390 parent,
11391 UiNode::container(
11392 format!("{name}.anchor"),
11393 LayoutStyle::new()
11394 .with_width(width)
11395 .with_height(30.0)
11396 .with_flex_shrink(0.0),
11397 ),
11398 );
11399 let nodes = ext_widgets::dropdown_select(
11400 ui,
11401 anchor,
11402 name,
11403 options,
11404 state,
11405 Some(select_popup(
11406 UiRect::new(0.0, 0.0, width, 30.0),
11407 UiRect::new(0.0, 0.0, width + 48.0, 240.0),
11408 )),
11409 dropdown_select_options(width, name, placeholder, accessibility_label),
11410 );
11411 ui.node_mut(nodes.trigger)
11412 .set_action(format!("{name}.toggle"));
11413}
11414
11415fn shader_lab_selected_material(state: &ShowcaseState) -> ElementMaterial {
11416 let shape = state.shader_lab_material_shape.shape();
11417 let mut material = ElementMaterial::new()
11418 .with_paint_outset(LayoutInsets::points(
11419 state
11420 .shader_lab_material_outset
11421 .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11422 ))
11423 .with_clip_shape(shape.clone())
11424 .with_hit_shape(shape)
11425 .with_geometry_effect(state.shader_lab_material_geometry.effect());
11426 if let Some(shader) = shader_lab_material_shader_effect(state) {
11427 material = material.with_shader(shader);
11428 }
11429 material
11430}
11431
11432fn shader_lab_material_shader_effect(state: &ShowcaseState) -> Option<ShaderEffect> {
11433 let phase = state.progress_phase.rem_euclid(1.0);
11434 match state.shader_lab_material_shader {
11435 ShaderLabMaterialShader::None => None,
11436 ShaderLabMaterialShader::Tint => Some(ShaderEffect::tint(color(255, 196, 92), 0.62)),
11437 ShaderLabMaterialShader::Shine => Some(ShaderEffect::shine(phase, 0.92)),
11438 ShaderLabMaterialShader::Glow => Some(ShaderEffect::glow(
11439 color(100, 180, 255),
11440 0.95,
11441 state
11442 .shader_lab_material_outset
11443 .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11444 )),
11445 ShaderLabMaterialShader::Plasma => {
11446 Some(ShaderEffect::plasma(phase, color(82, 190, 255), 0.75, 12.0))
11447 }
11448 ShaderLabMaterialShader::Rings => {
11449 Some(ShaderEffect::rings(phase, color(232, 170, 88), 0.78, 11.0))
11450 }
11451 ShaderLabMaterialShader::Grid => {
11452 Some(ShaderEffect::grid(phase, color(156, 132, 255), 0.85, 9.0))
11453 }
11454 }
11455}
11456
11457fn shader_lab_material_visual(shape: ShaderLabMaterialShape) -> UiVisual {
11458 UiVisual::panel(
11459 color(39, 71, 114),
11460 Some(StrokeStyle::new(color(168, 205, 255), 1.0)),
11461 shape.visual_radius(),
11462 )
11463}
11464
11465fn shader_lab_material_chip(
11466 ui: &mut UiDocument,
11467 parent: UiNodeId,
11468 name: &'static str,
11469 label: &'static str,
11470 material: ElementMaterial,
11471 visual: UiVisual,
11472) {
11473 let chip = ui.add_child(
11474 parent,
11475 UiNode::container(
11476 name,
11477 LayoutStyle::row()
11478 .with_width(156.0)
11479 .with_height(44.0)
11480 .with_padding(8.0)
11481 .with_align_items(taffy::prelude::AlignItems::Center)
11482 .with_justify_content(taffy::prelude::JustifyContent::Center)
11483 .with_flex_shrink(0.0),
11484 )
11485 .with_visual(visual)
11486 .with_material(material)
11487 .with_accessibility(
11488 AccessibilityMeta::new(AccessibilityRole::Image).label(format!("{label} material")),
11489 ),
11490 );
11491 widgets::label(
11492 ui,
11493 chip,
11494 format!("{name}.text"),
11495 label,
11496 text(11.0, color(246, 249, 252)),
11497 LayoutStyle::new().with_flex_shrink(0.0),
11498 );
11499}
11500
11501fn shader_lab_option_checkbox(
11502 ui: &mut UiDocument,
11503 parent: UiNodeId,
11504 name: &'static str,
11505 label: &'static str,
11506 checked: bool,
11507) {
11508 let mut options = widgets::CheckboxOptions::default()
11509 .with_action(name)
11510 .with_text_style(text(12.0, color(220, 228, 238)));
11511 options.layout = LayoutStyle::new()
11512 .with_width(112.0)
11513 .with_height(24.0)
11514 .with_flex_shrink(0.0);
11515 widgets::checkbox(ui, parent, name, label, checked, options);
11516}
11517
11518fn shader_lab_slider_control(
11519 ui: &mut UiDocument,
11520 parent: UiNodeId,
11521 name: &'static str,
11522 label: &'static str,
11523 value: f32,
11524 max: f32,
11525 decimals: usize,
11526) {
11527 let control = ui.add_child(
11528 parent,
11529 UiNode::container(
11530 format!("{name}.control"),
11531 Layout::row()
11532 .size(LayoutSize::new(
11533 LayoutDimension::points(214.0),
11534 LayoutDimension::Auto,
11535 ))
11536 .align_items(LayoutAlignment::Center)
11537 .gap(LayoutGap::points(6.0, 6.0))
11538 .flex(0.0, 0.0, LayoutDimension::Auto)
11539 .to_layout_style(),
11540 ),
11541 );
11542 widgets::label(
11543 ui,
11544 control,
11545 format!("{name}.label"),
11546 label,
11547 text(12.0, color(166, 176, 190)),
11548 LayoutStyle::new()
11549 .with_width(46.0)
11550 .with_height(22.0)
11551 .with_flex_shrink(0.0),
11552 );
11553 let mut options = widgets::SliderOptions::default()
11554 .with_layout(
11555 LayoutStyle::new()
11556 .with_width(96.0)
11557 .with_height(22.0)
11558 .with_flex_shrink(0.0),
11559 )
11560 .with_value_edit_action(name);
11561 options.accessibility_label = Some(format!("Shader lab {label}"));
11562 widgets::slider(
11563 ui,
11564 control,
11565 format!("{name}.slider"),
11566 (value / max.max(f32::EPSILON)).clamp(0.0, 1.0),
11567 0.0..1.0,
11568 options,
11569 );
11570 widgets::label(
11571 ui,
11572 control,
11573 format!("{name}.value"),
11574 format!("{value:.decimals$}px"),
11575 text(12.0, color(226, 232, 242)),
11576 LayoutStyle::new()
11577 .with_width(48.0)
11578 .with_height(22.0)
11579 .with_flex_shrink(0.0),
11580 );
11581}
11582
11583fn shader_lab_surface_stroke(state: &ShowcaseState) -> Option<StrokeStyle> {
11584 (state.shader_lab_surface_stroke_width > f32::EPSILON).then(|| {
11585 StrokeStyle::new(
11586 color(150, 180, 235),
11587 state
11588 .shader_lab_surface_stroke_width
11589 .clamp(0.0, SHADER_LAB_SURFACE_STROKE_MAX),
11590 )
11591 })
11592}
11593
11594fn shader_lab_surface_radius(state: &ShowcaseState) -> f32 {
11595 state
11596 .shader_lab_surface_radius
11597 .clamp(0.0, SHADER_LAB_SURFACE_RADIUS_MAX)
11598}
11599
11600fn shader_lab_editor_column(
11601 ui: &mut UiDocument,
11602 parent: UiNodeId,
11603 state: &ShowcaseState,
11604 source_error: Option<&str>,
11605) {
11606 let editor_column = ui.add_child(
11607 parent,
11608 UiNode::container(
11609 "shader_lab.editor.column",
11610 LayoutStyle::column()
11611 .with_width_percent(1.0)
11612 .with_height_percent(1.0)
11613 .with_gap(8.0)
11614 .with_flex_shrink(1.0),
11615 ),
11616 );
11617 let preset_row = row(ui, editor_column, "shader_lab.preset.row", 8.0);
11618 widgets::label(
11619 ui,
11620 preset_row,
11621 "shader_lab.preset.caption",
11622 "Program",
11623 text(12.0, color(166, 176, 190)),
11624 LayoutStyle::new()
11625 .with_width(62.0)
11626 .with_height(30.0)
11627 .with_flex_shrink(0.0),
11628 );
11629 shader_lab_dropdown_select(
11630 ui,
11631 preset_row,
11632 "shader_lab.preset",
11633 &shader_lab_preset_options(),
11634 &state.shader_lab_preset_menu,
11635 180.0,
11636 "WGSL preset",
11637 "Shader lab WGSL preset",
11638 );
11639
11640 let editor_frame = ui.add_child(
11641 editor_column,
11642 UiNode::container(
11643 "shader_lab.editor.frame",
11644 Layout::column()
11645 .size(LayoutSize::new(
11646 LayoutDimension::percent(1.0),
11647 LayoutDimension::points(0.0),
11648 ))
11649 .min_size(LayoutSize::points(0.0, SHADER_LAB_EDITOR_HEIGHT))
11650 .flex(1.0, 1.0, LayoutDimension::points(0.0))
11651 .to_layout_style(),
11652 )
11653 .with_visual(UiVisual::panel(
11654 color(18, 22, 28),
11655 Some(StrokeStyle::new(color(72, 84, 104), 1.0)),
11656 4.0,
11657 )),
11658 );
11659 let editor_scroll = widgets::scroll_area(
11660 ui,
11661 editor_frame,
11662 "shader_lab.editor.scroll",
11663 ScrollAxes::BOTH,
11664 LayoutStyle::column()
11665 .with_width_percent(1.0)
11666 .with_height_percent(1.0),
11667 );
11668 ui.node_mut(editor_scroll)
11669 .set_action("shader_lab.editor.scroll");
11670 if let Some(scroll) = ui.node_mut(editor_scroll).scroll_mut() {
11671 scroll.set_offset(state.shader_lab_editor_scroll);
11672 }
11673
11674 let mut code_options = state.text_edit_options(FocusedTextInput::ShaderLabSource);
11675 code_options.edit_action = Some("shader_lab.editor.edit".into());
11676 code_options.visual = UiVisual::TRANSPARENT;
11677 code_options.focused_visual = Some(UiVisual::TRANSPARENT);
11678 code_options.disabled_visual = Some(UiVisual::TRANSPARENT);
11679 widgets::code_editor(
11680 ui,
11681 editor_scroll,
11682 "shader_lab.editor",
11683 &state.shader_lab_source,
11684 code_options,
11685 );
11686 let (validation_text, validation_color) = shader_lab_validation_label(source_error);
11687 widgets::label(
11688 ui,
11689 editor_column,
11690 "shader_lab.validation",
11691 validation_text,
11692 text(11.0, validation_color),
11693 LayoutStyle::new().with_width_percent(1.0),
11694 );
11695}
11696
11697fn shader_lab_editor_content_size(source: &str) -> UiSize {
11698 let style = widgets::code_text_style();
11699 let line_count = source.lines().count().max(1) as f32;
11700 let longest_line = source
11701 .lines()
11702 .map(|line| line.chars().count())
11703 .max()
11704 .unwrap_or(0)
11705 .max(48) as f32;
11706 UiSize::new(
11707 (longest_line * style.font_size * 0.56 + 24.0).max(SHADER_LAB_EDITOR_WIDTH),
11708 (line_count * style.line_height + 18.0).max(SHADER_LAB_EDITOR_HEIGHT),
11709 )
11710}
11711
11712fn shader_lab_preview(
11713 ui: &mut UiDocument,
11714 parent: UiNodeId,
11715 state: &ShowcaseState,
11716 source_valid: bool,
11717) {
11718 match state.shader_lab_target {
11719 ShaderLabTarget::Canvas => {
11720 let mut options = widgets::CanvasOptions::default()
11721 .with_layout(
11722 LayoutStyle::new()
11723 .with_width_percent(1.0)
11724 .with_height_percent(1.0)
11725 .with_flex_grow(1.0),
11726 )
11727 .with_intrinsic_size(UiSize::new(
11728 SHADER_LAB_PREVIEW_WIDTH - 20.0,
11729 SHADER_LAB_PREVIEW_HEIGHT,
11730 ))
11731 .with_accessibility_label("Shader lab canvas preview");
11732 options.visual = UiVisual::panel(color(8, 12, 18), None, 4.0);
11733 widgets::canvas(
11734 ui,
11735 parent,
11736 "shader_lab.preview.canvas",
11737 CanvasContent::new("shader_lab.preview.canvas")
11738 .program(shader_lab_canvas_program(state, source_valid)),
11739 options,
11740 );
11741 }
11742 ShaderLabTarget::Frame => {
11743 let mut frame_node = UiNode::container(
11744 "shader_lab.preview.frame",
11745 operad::layout::with_min_size(
11746 LayoutStyle::column()
11747 .with_width_percent(0.82)
11748 .with_height_percent(0.62)
11749 .with_padding(14.0)
11750 .with_align_items(taffy::prelude::AlignItems::Center)
11751 .with_justify_content(taffy::prelude::JustifyContent::Center)
11752 .with_flex_shrink(0.0),
11753 operad::layout::px(SHADER_LAB_FRAME_MIN_WIDTH),
11754 operad::layout::px(SHADER_LAB_FRAME_MIN_HEIGHT),
11755 ),
11756 )
11757 .with_visual(UiVisual::panel(
11758 ColorRgba::new(0, 0, 0, 0),
11759 shader_lab_surface_stroke(state),
11760 shader_lab_surface_radius(state),
11761 ));
11762 frame_node.style_mut().set_clip(ClipBehavior::Clip);
11763 let frame = ui.add_child(parent, frame_node);
11764 shader_lab_canvas_layer_fill(
11765 ui,
11766 frame,
11767 "shader_lab.preview.frame.shader",
11768 state,
11769 source_valid,
11770 );
11771 if state.shader_lab_show_frame_text {
11772 let label = widgets::label(
11773 ui,
11774 frame,
11775 "shader_lab.preview.frame.label",
11776 "WGSL frame",
11777 text(14.0, color(246, 249, 252)),
11778 LayoutStyle::new().with_flex_shrink(0.0),
11779 );
11780 ui.node_mut(label).style_mut().set_z_index(1);
11781 }
11782 }
11783 ShaderLabTarget::Button => {
11784 let mut shell_node = UiNode::container(
11785 "shader_lab.preview.button.shell",
11786 LayoutStyle::column()
11787 .with_width(SHADER_LAB_BUTTON_WIDTH)
11788 .with_height(SHADER_LAB_BUTTON_HEIGHT)
11789 .with_align_items(taffy::prelude::AlignItems::Center)
11790 .with_justify_content(taffy::prelude::JustifyContent::Center),
11791 )
11792 .with_visual(UiVisual::TRANSPARENT);
11793 shell_node.style_mut().set_clip(ClipBehavior::Clip);
11794 let shell = ui.add_child(parent, shell_node);
11795 shader_lab_canvas_layer_fill(
11796 ui,
11797 shell,
11798 "shader_lab.preview.button.shader",
11799 state,
11800 source_valid,
11801 );
11802 let mut options = widgets::ButtonOptions::new(
11803 LayoutStyle::new()
11804 .with_width(SHADER_LAB_BUTTON_WIDTH)
11805 .with_height(SHADER_LAB_BUTTON_HEIGHT),
11806 )
11807 .with_action("shader_lab.preview.button")
11808 .with_accessibility_label("Shader button preview");
11809 options.visual = UiVisual::panel(
11810 ColorRgba::new(0, 0, 0, 0),
11811 shader_lab_surface_stroke(state),
11812 shader_lab_surface_radius(state),
11813 );
11814 options.hovered_visual = Some(UiVisual::panel(
11815 ColorRgba::new(255, 255, 255, 28),
11816 shader_lab_surface_stroke(state),
11817 shader_lab_surface_radius(state),
11818 ));
11819 options.pressed_visual = Some(UiVisual::panel(
11820 ColorRgba::new(0, 0, 0, 48),
11821 shader_lab_surface_stroke(state),
11822 shader_lab_surface_radius(state),
11823 ));
11824 options.text_style = text(14.0, color(246, 249, 252));
11825 let button = widgets::button(
11826 ui,
11827 shell,
11828 "shader_lab.preview.button",
11829 if state.shader_lab_show_button_text {
11830 "Shader button"
11831 } else {
11832 ""
11833 },
11834 options,
11835 );
11836 ui.node_mut(button).style_mut().set_z_index(1);
11837 }
11838 }
11839}
11840
11841fn shader_lab_canvas_layer_fill(
11842 ui: &mut UiDocument,
11843 parent: UiNodeId,
11844 name: &'static str,
11845 state: &ShowcaseState,
11846 source_valid: bool,
11847) -> UiNodeId {
11848 let layout = operad::layout::with_absolute_position(
11849 LayoutStyle::new()
11850 .with_width_percent(1.0)
11851 .with_height_percent(1.0),
11852 0.0,
11853 0.0,
11854 );
11855 let mut options = widgets::CanvasOptions::default()
11856 .with_layout(layout)
11857 .with_intrinsic_size(UiSize::new(
11858 SHADER_LAB_PREVIEW_WIDTH,
11859 SHADER_LAB_PREVIEW_HEIGHT,
11860 ))
11861 .with_accessibility_label(format!("{name} shader preview"));
11862 options.input = InputBehavior::NONE;
11863 options.visual = UiVisual::TRANSPARENT;
11864 let canvas = widgets::canvas(
11865 ui,
11866 parent,
11867 name,
11868 CanvasContent::new(name).program(shader_lab_canvas_program(state, source_valid)),
11869 options,
11870 );
11871 ui.node_mut(canvas).style_mut().set_z_index(0);
11872 canvas
11873}
11874
11875fn shader_lab_status_label(state: &ShowcaseState, source_error: Option<&str>) -> Option<String> {
11876 if source_error.is_some() {
11877 Some(format!(
11878 "{} fallback until WGSL validates",
11879 state.shader_lab_target.label()
11880 ))
11881 } else {
11882 None
11883 }
11884}
11885
11886fn shader_lab_validation_label(source_error: Option<&str>) -> (String, ColorRgba) {
11887 if let Some(error) = source_error {
11888 (
11889 format!("WGSL error: {}", compact_shader_error(error, 160)),
11890 color(255, 139, 128),
11891 )
11892 } else {
11893 ("WGSL valid".to_string(), color(112, 221, 160))
11894 }
11895}
11896
11897fn compact_shader_error(error: &str, max_chars: usize) -> String {
11898 let mut compact = error.split_whitespace().collect::<Vec<_>>().join(" ");
11899 if compact.chars().count() > max_chars {
11900 compact = compact.chars().take(max_chars.saturating_sub(3)).collect();
11901 compact.push_str("...");
11902 }
11903 compact
11904}
11905
11906fn shader_lab_canvas_program(state: &ShowcaseState, source_valid: bool) -> CanvasRenderProgram {
11907 let source = if source_valid {
11908 state.shader_lab_source.text().to_string()
11909 } else {
11910 SHADER_LAB_ERROR_WGSL.to_string()
11911 };
11912 CanvasRenderProgram::wgsl(source)
11913 .label("showcase.shader_lab.canvas")
11914 .constant("TIME", state.progress_phase as f64)
11915 .clear_color(Some(color(8, 12, 18)))
11916}
11917
11918fn shader_lab_source_error(source: &str) -> Option<String> {
11919 if !shader_lab_source_has_entry_points(source) {
11920 return Some("source must define @vertex fn vs_main and @fragment fn fs_main".to_string());
11921 }
11922
11923 shader_lab_compile_error(source)
11924}
11925
11926#[cfg(feature = "wgpu")]
11927fn shader_lab_compile_error(source: &str) -> Option<String> {
11928 let module = match naga::front::wgsl::parse_str(source) {
11929 Ok(module) => module,
11930 Err(error) => return Some(error.emit_to_string(source)),
11931 };
11932 let mut validator = naga::valid::Validator::new(
11933 naga::valid::ValidationFlags::all(),
11934 naga::valid::Capabilities::empty(),
11935 );
11936 validator
11937 .validate(&module)
11938 .err()
11939 .map(|error| error.to_string())
11940}
11941
11942#[cfg(not(feature = "wgpu"))]
11943fn shader_lab_compile_error(_source: &str) -> Option<String> {
11944 None
11945}
11946
11947fn shader_lab_source_has_entry_points(source: &str) -> bool {
11948 source.contains("@vertex")
11949 && source.contains("fn vs_main")
11950 && source.contains("@fragment")
11951 && source.contains("fn fs_main")
11952}
11953
11954fn timeline_ruler(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11955 let body =
11956 section_with_min_viewport(ui, parent, "timeline", "Timeline", UiSize::new(560.0, 0.0));
11957 widgets::label(
11958 ui,
11959 body,
11960 "timeline.label",
11961 "Clip timeline",
11962 text(12.0, color(166, 176, 190)),
11963 LayoutStyle::new().with_width_percent(1.0),
11964 );
11965 widgets::label(
11966 ui,
11967 body,
11968 "timeline.description",
11969 "The ruler maps time to tracks, clips, markers, and the current playhead.",
11970 text(12.0, color(196, 210, 230)),
11971 LayoutStyle::new().with_width_percent(1.0),
11972 );
11973
11974 let editor = row(ui, body, "timeline.editor", 0.0);
11975 let labels = ui.add_child(
11976 editor,
11977 UiNode::container(
11978 "timeline.lane_labels",
11979 LayoutStyle::column()
11980 .with_width(96.0)
11981 .with_height(172.0)
11982 .with_flex_shrink(0.0),
11983 ),
11984 );
11985 for (name, label, height) in [
11986 ("timeline.lane_labels.header", "Tracks", 40.0),
11987 ("timeline.lane_labels.video", "Video", 44.0),
11988 ("timeline.lane_labels.audio", "Audio", 44.0),
11989 ("timeline.lane_labels.notes", "Notes", 44.0),
11990 ] {
11991 widgets::label(
11992 ui,
11993 labels,
11994 name,
11995 label,
11996 text(11.0, color(166, 176, 190)),
11997 LayoutStyle::new()
11998 .with_width_percent(1.0)
11999 .with_height(height)
12000 .with_flex_shrink(0.0),
12001 );
12002 }
12003
12004 let timeline_scroll = timeline_scroll_state_for_view(
12005 state.timeline_scroll,
12006 state.timeline_scroll.viewport_size().width,
12007 );
12008 let range = ext_widgets::TimelineRange::new(0.0, 48.0);
12009 let nodes = scroll_area_widgets::scroll_container_shell(
12010 ui,
12011 editor,
12012 "timeline",
12013 timeline_scroll,
12014 widgets::ScrollContainerOptions::default()
12015 .with_axes(ScrollAxes::HORIZONTAL)
12016 .with_action_prefix("timeline")
12017 .with_gap(4.0)
12018 .with_scrollbar_thickness(TIMELINE_SCROLLBAR_HEIGHT)
12019 .with_layout(
12020 LayoutStyle::column()
12021 .with_width(0.0)
12022 .with_flex_grow(1.0)
12023 .with_height(TIMELINE_SCROLL_CONTAINER_HEIGHT)
12024 .with_flex_shrink(0.0),
12025 )
12026 .with_viewport_layout(
12027 LayoutStyle::column()
12028 .with_width(0.0)
12029 .with_flex_grow(1.0)
12030 .with_height(TIMELINE_VIEWPORT_HEIGHT)
12031 .with_flex_shrink(1.0),
12032 )
12033 .with_horizontal_scrollbar(
12034 scrollbar_widgets::ScrollbarOptions::default()
12035 .with_action("timeline.horizontal-scrollbar"),
12036 )
12037 .with_accessibility_label("Timeline horizontal scroller"),
12038 );
12039 let content = ui.add_child(
12040 nodes.viewport,
12041 UiNode::container(
12042 "timeline.content",
12043 LayoutStyle::column()
12044 .with_width(TIMELINE_CONTENT_WIDTH)
12045 .with_height(TIMELINE_VIEWPORT_HEIGHT)
12046 .with_flex_shrink(0.0),
12047 ),
12048 );
12049 let mut ruler_options = ext_widgets::TimelineRulerOptions::default();
12050 ruler_options.height = 40.0;
12051 ruler_options.layout = LayoutStyle::new()
12052 .with_width(TIMELINE_CONTENT_WIDTH)
12053 .with_height(40.0)
12054 .with_flex_shrink(0.0);
12055 ruler_options.accessibility_label = Some("Editing timeline ruler".to_string());
12056 ruler_options.accessibility_hint =
12057 Some("Shows seconds for the visible timeline clips".to_string());
12058 ext_widgets::timeline_ruler(
12059 ui,
12060 content,
12061 "timeline.ruler",
12062 ext_widgets::RulerSpec {
12063 range,
12064 width: TIMELINE_CONTENT_WIDTH,
12065 major_step: 4.0,
12066 minor_step: 1.0,
12067 label_every: 1,
12068 },
12069 ruler_options,
12070 );
12071 ui.add_child(
12072 content,
12073 UiNode::scene(
12074 "timeline.tracks",
12075 timeline_track_primitives(range, TIMELINE_CONTENT_WIDTH),
12076 LayoutStyle::new()
12077 .with_width(TIMELINE_CONTENT_WIDTH)
12078 .with_height(132.0)
12079 .with_flex_shrink(0.0),
12080 ),
12081 );
12082}
12083
12084fn timeline_track_primitives(range: ext_widgets::TimelineRange, width: f32) -> Vec<ScenePrimitive> {
12085 let mut primitives = Vec::new();
12086 let lane_height = 36.0;
12087 let lane_gap = 8.0;
12088 let lanes = [
12089 ("Video", 0.0, color(16, 22, 30)),
12090 ("Audio", lane_height + lane_gap, color(13, 20, 27)),
12091 ("Notes", (lane_height + lane_gap) * 2.0, color(16, 22, 30)),
12092 ];
12093
12094 for (label, y, fill) in lanes {
12095 primitives.push(ScenePrimitive::Rect(
12096 PaintRect::solid(UiRect::new(0.0, y, width, lane_height), fill)
12097 .stroke(AlignedStroke::inside(StrokeStyle::new(
12098 color(38, 49, 64),
12099 1.0,
12100 )))
12101 .corner_radii(CornerRadii::uniform(2.0)),
12102 ));
12103 primitives.push(ScenePrimitive::Text(
12104 PaintText::new(
12105 label,
12106 UiRect::new(8.0, y + 8.0, 72.0, 18.0),
12107 text(10.0, color(116, 128, 145)),
12108 )
12109 .multiline(false),
12110 ));
12111 }
12112
12113 for second in (0..=48).step_by(4) {
12114 let x = range.value_to_x(second as f64, width);
12115 primitives.push(ScenePrimitive::Line {
12116 from: UiPoint::new(x, 0.0),
12117 to: UiPoint::new(x, 124.0),
12118 stroke: StrokeStyle::new(color(34, 44, 58), 1.0),
12119 });
12120 }
12121
12122 push_timeline_clip(
12123 &mut primitives,
12124 range,
12125 width,
12126 "Intro",
12127 2.0,
12128 10.0,
12129 0.0,
12130 color(57, 126, 207),
12131 );
12132 push_timeline_clip(
12133 &mut primitives,
12134 range,
12135 width,
12136 "Cutaway",
12137 12.0,
12138 24.0,
12139 0.0,
12140 color(95, 107, 212),
12141 );
12142 push_timeline_clip(
12143 &mut primitives,
12144 range,
12145 width,
12146 "Final",
12147 28.0,
12148 44.0,
12149 0.0,
12150 color(68, 153, 122),
12151 );
12152 push_timeline_clip(
12153 &mut primitives,
12154 range,
12155 width,
12156 "Music bed",
12157 0.0,
12158 48.0,
12159 lane_height + lane_gap,
12160 color(205, 160, 71),
12161 );
12162 push_timeline_clip(
12163 &mut primitives,
12164 range,
12165 width,
12166 "Voiceover",
12167 8.0,
12168 18.0,
12169 lane_height + lane_gap,
12170 color(183, 107, 185),
12171 );
12172
12173 for (second, label) in [(6.0, "Beat"), (21.0, "Cut"), (37.0, "Cue")] {
12174 let x = range.value_to_x(second, width);
12175 let y = (lane_height + lane_gap) * 2.0 + 8.0;
12176 primitives.push(ScenePrimitive::Polygon {
12177 points: vec![
12178 UiPoint::new(x, y),
12179 UiPoint::new(x + 8.0, y + 8.0),
12180 UiPoint::new(x, y + 16.0),
12181 UiPoint::new(x - 8.0, y + 8.0),
12182 ],
12183 fill: color(245, 198, 83),
12184 stroke: Some(StrokeStyle::new(color(255, 234, 178), 1.0)),
12185 });
12186 primitives.push(ScenePrimitive::Text(
12187 PaintText::new(
12188 label,
12189 UiRect::new(x + 12.0, y - 1.0, 72.0, 18.0),
12190 text(10.0, color(225, 233, 244)),
12191 )
12192 .multiline(false),
12193 ));
12194 }
12195
12196 let playhead_x = range.value_to_x(18.5, width);
12197 primitives.push(ScenePrimitive::Line {
12198 from: UiPoint::new(playhead_x, 0.0),
12199 to: UiPoint::new(playhead_x, 124.0),
12200 stroke: StrokeStyle::new(ColorRgba::new(255, 120, 96, 255), 2.0),
12201 });
12202 primitives.push(ScenePrimitive::Text(
12203 PaintText::new(
12204 "Playhead 18.5s",
12205 UiRect::new(playhead_x + 8.0, 106.0, 120.0, 18.0),
12206 text(10.0, ColorRgba::new(255, 172, 154, 255)),
12207 )
12208 .multiline(false),
12209 ));
12210
12211 primitives
12212}
12213
12214#[allow(clippy::too_many_arguments)]
12215fn push_timeline_clip(
12216 primitives: &mut Vec<ScenePrimitive>,
12217 range: ext_widgets::TimelineRange,
12218 width: f32,
12219 label: &'static str,
12220 start: f64,
12221 end: f64,
12222 lane_y: f32,
12223 fill: ColorRgba,
12224) {
12225 let x = range.value_to_x(start, width);
12226 let right = range.value_to_x(end, width);
12227 let rect = UiRect::new(x, lane_y + 6.0, (right - x).max(1.0), 24.0);
12228 primitives.push(ScenePrimitive::Rect(
12229 PaintRect::solid(rect, fill)
12230 .stroke(AlignedStroke::inside(StrokeStyle::new(
12231 ColorRgba::new(230, 240, 255, 96),
12232 1.0,
12233 )))
12234 .corner_radii(CornerRadii::uniform(4.0)),
12235 ));
12236 primitives.push(ScenePrimitive::Text(
12237 PaintText::new(label, rect, text(10.0, color(245, 248, 252)))
12238 .horizontal_align(TextHorizontalAlign::Center)
12239 .vertical_align(TextVerticalAlign::Center)
12240 .multiline(false),
12241 ));
12242}Sourcepub const fn new(
top_left: f32,
top_right: f32,
bottom_right: f32,
bottom_left: f32,
) -> Self
pub const fn new( top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32, ) -> Self
Examples found in repository?
examples/showcase.rs (lines 475-480)
471 fn radii(self) -> CornerRadii {
472 if self.radius_same {
473 CornerRadii::uniform(self.corner_radius)
474 } else {
475 CornerRadii::new(
476 self.corner_radius,
477 self.corner_ne,
478 self.corner_se,
479 self.corner_sw,
480 )
481 }
482 }pub fn max_radius(self) -> f32
Trait Implementations§
Source§impl Clone for CornerRadii
impl Clone for CornerRadii
Source§fn clone(&self) -> CornerRadii
fn clone(&self) -> CornerRadii
Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
Performs copy-assignment from
source. Read moreSource§impl Debug for CornerRadii
impl Debug for CornerRadii
Source§impl Default for CornerRadii
impl Default for CornerRadii
Source§impl PartialEq for CornerRadii
impl PartialEq for CornerRadii
Source§fn eq(&self, other: &CornerRadii) -> bool
fn eq(&self, other: &CornerRadii) -> bool
Tests for
self and other values to be equal, and is used by ==.impl Copy for CornerRadii
impl StructuralPartialEq for CornerRadii
Auto Trait Implementations§
impl Freeze for CornerRadii
impl RefUnwindSafe for CornerRadii
impl Send for CornerRadii
impl Sync for CornerRadii
impl Unpin for CornerRadii
impl UnsafeUnpin for CornerRadii
impl UnwindSafe for CornerRadii
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Convert
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Convert
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
Convert
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
Convert
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.