pub struct ColorRgba {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}Fields§
§r: u8§g: u8§b: u8§a: u8Implementations§
Source§impl ColorRgba
impl ColorRgba
pub const WHITE: Self
pub const BLACK: Self
pub const TRANSPARENT: Self
Sourcepub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self
Examples found in repository?
examples/simple_form.rs (line 142)
126fn app_panel(
127 ui: &mut UiDocument,
128 name: &str,
129 viewport: UiSize,
130 width: f32,
131 height: f32,
132) -> UiNodeId {
133 ui.add_child(
134 ui.root(),
135 UiNode::container(
136 name,
137 LayoutStyle::column()
138 .with_size(width.min(viewport.width.max(1.0)), height)
139 .with_padding(16.0)
140 .with_gap(10.0),
141 )
142 .with_visual(UiVisual::panel(ColorRgba::new(24, 29, 36, 255), None, 6.0)),
143 )
144}
145
146fn heading() -> TextStyle {
147 TextStyle {
148 font_size: 22.0,
149 line_height: 30.0,
150 color: ColorRgba::WHITE,
151 ..TextStyle::default()
152 }
153}
154
155fn muted() -> TextStyle {
156 TextStyle {
157 color: ColorRgba::new(166, 178, 196, 255),
158 ..TextStyle::default()
159 }
160}More examples
examples/showcase.rs (line 436)
415 fn default() -> Self {
416 Self {
417 inner_same: true,
418 inner_margin: 12.0,
419 inner_right: 12.0,
420 inner_top: 12.0,
421 inner_bottom: 12.0,
422 outer_same: true,
423 outer_margin: 24.0,
424 outer_right: 24.0,
425 outer_top: 24.0,
426 outer_bottom: 24.0,
427 radius_same: true,
428 corner_radius: 12.0,
429 corner_ne: 12.0,
430 corner_sw: 12.0,
431 corner_se: 12.0,
432 shadow_x: 8.0,
433 shadow_y: 12.0,
434 shadow_blur: 16.0,
435 shadow_spread: 0.0,
436 shadow: ColorRgba::new(0, 0, 0, 140),
437 stroke_width: 1.0,
438 stroke: ColorRgba::new(198, 198, 205, 255),
439 fill: ColorRgba::new(100, 55, 205, 255),
440 }
441 }
442}
443
444impl StylingState {
445 fn inner_edges(self) -> [f32; 4] {
446 if self.inner_same {
447 [self.inner_margin; 4]
448 } else {
449 [
450 self.inner_margin,
451 self.inner_right,
452 self.inner_top,
453 self.inner_bottom,
454 ]
455 }
456 }
457
458 fn outer_edges(self) -> [f32; 4] {
459 if self.outer_same {
460 [self.outer_margin; 4]
461 } else {
462 [
463 self.outer_margin,
464 self.outer_right,
465 self.outer_top,
466 self.outer_bottom,
467 ]
468 }
469 }
470
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}
12243
12244fn theme_demo_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState, theme: &Theme) {
12245 let body = section(ui, parent, "theme", "Theme");
12246 widgets::label(
12247 ui,
12248 body,
12249 "theme.current",
12250 format!("Current theme: {}", theme.name),
12251 themed_text(theme, 14.0),
12252 LayoutStyle::new().with_width_percent(1.0),
12253 );
12254
12255 let choices = wrapping_row(ui, body, "theme.choices", 8.0);
12256 for choice in [
12257 ShowcaseThemeChoice::Light,
12258 ShowcaseThemeChoice::Dark,
12259 ShowcaseThemeChoice::Bubblegum,
12260 ] {
12261 theme_choice_button(
12262 ui,
12263 choices,
12264 choice,
12265 state.showcase_theme == choice,
12266 choice.theme(),
12267 );
12268 }
12269
12270 let swatches = wrapping_row(ui, body, "theme.swatches", 8.0);
12271 theme_swatch(
12272 ui,
12273 swatches,
12274 "theme.swatch.canvas",
12275 "Canvas",
12276 theme.colors.canvas,
12277 theme,
12278 );
12279 theme_swatch(
12280 ui,
12281 swatches,
12282 "theme.swatch.surface",
12283 "Surface",
12284 theme.colors.surface,
12285 theme,
12286 );
12287 theme_swatch(
12288 ui,
12289 swatches,
12290 "theme.swatch.accent",
12291 "Accent",
12292 theme.colors.accent,
12293 theme,
12294 );
12295 theme_swatch(
12296 ui,
12297 swatches,
12298 "theme.swatch.selected",
12299 "Selected",
12300 theme.colors.selected,
12301 theme,
12302 );
12303
12304 let preview = ui.add_child(
12305 body,
12306 UiNode::container(
12307 "theme.preview",
12308 LayoutStyle::column()
12309 .with_width_percent(1.0)
12310 .with_padding(12.0)
12311 .with_gap(10.0)
12312 .with_flex_shrink(0.0),
12313 )
12314 .with_visual(UiVisual::panel(
12315 theme.colors.surface,
12316 Some(theme.stroke.surface),
12317 theme.radius.md,
12318 ))
12319 .with_accessibility(
12320 AccessibilityMeta::new(AccessibilityRole::Group).label("Theme preview"),
12321 ),
12322 );
12323 widgets::label(
12324 ui,
12325 preview,
12326 "theme.preview.title",
12327 "Preview controls",
12328 themed_text(theme, 13.0),
12329 LayoutStyle::new().with_width_percent(1.0),
12330 );
12331 let preview_row = row(ui, preview, "theme.preview.controls", 8.0);
12332 let mut primary = themed_button_options(
12333 theme,
12334 "theme.preview.primary",
12335 ComponentState::ACTIVE,
12336 LayoutStyle::new().with_height(34.0),
12337 );
12338 primary.accessibility_label = Some("Primary preview button".to_owned());
12339 widgets::button(ui, preview_row, "theme.preview.primary", "Primary", primary);
12340 let mut secondary = themed_button_options(
12341 theme,
12342 "theme.preview.secondary",
12343 ComponentState::NORMAL,
12344 LayoutStyle::new().with_height(34.0),
12345 );
12346 secondary.accessibility_label = Some("Secondary preview button".to_owned());
12347 widgets::button(
12348 ui,
12349 preview_row,
12350 "theme.preview.secondary",
12351 "Secondary",
12352 secondary,
12353 );
12354 let mut help = themed_muted_text(theme, 12.0);
12355 help.wrap = TextWrap::WordOrGlyph;
12356 widgets::label(
12357 ui,
12358 preview,
12359 "theme.preview.copy",
12360 "The selected theme drives the app background, right panel, floating windows, and this preview.",
12361 help,
12362 LayoutStyle::new().with_width_percent(1.0),
12363 );
12364}
12365
12366fn theme_choice_button(
12367 ui: &mut UiDocument,
12368 parent: UiNodeId,
12369 choice: ShowcaseThemeChoice,
12370 selected: bool,
12371 preview_theme: Theme,
12372) {
12373 let mut options = themed_button_options(
12374 &preview_theme,
12375 choice.action(),
12376 if selected {
12377 ComponentState::SELECTED
12378 } else {
12379 ComponentState::NORMAL
12380 },
12381 LayoutStyle::new()
12382 .with_width(116.0)
12383 .with_height(34.0)
12384 .with_flex_shrink(0.0),
12385 )
12386 .with_action(choice.action());
12387 options.accessibility_label = Some(format!("Use {} theme", choice.label()));
12388 widgets::button(
12389 ui,
12390 parent,
12391 format!("theme.choice.{}", choice.label().to_ascii_lowercase()),
12392 choice.label(),
12393 options,
12394 );
12395}
12396
12397fn theme_swatch(
12398 ui: &mut UiDocument,
12399 parent: UiNodeId,
12400 name: &'static str,
12401 label: &'static str,
12402 swatch_color: ColorRgba,
12403 theme: &Theme,
12404) {
12405 let tile = ui.add_child(
12406 parent,
12407 UiNode::container(
12408 name,
12409 LayoutStyle::column()
12410 .with_width(92.0)
12411 .with_height(76.0)
12412 .with_padding(8.0)
12413 .with_gap(6.0)
12414 .with_flex_shrink(0.0),
12415 )
12416 .with_visual(UiVisual::panel(
12417 theme.colors.surface_muted,
12418 Some(theme.stroke.surface),
12419 4.0,
12420 ))
12421 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(label)),
12422 );
12423 ui.add_child(
12424 tile,
12425 UiNode::container(
12426 format!("{name}.color"),
12427 LayoutStyle::new()
12428 .with_width_percent(1.0)
12429 .with_height(26.0)
12430 .with_flex_shrink(0.0),
12431 )
12432 .with_visual(UiVisual::panel(
12433 swatch_color,
12434 Some(StrokeStyle::new(theme.colors.border_strong, 1.0)),
12435 4.0,
12436 )),
12437 );
12438 widgets::label(
12439 ui,
12440 tile,
12441 format!("{name}.label"),
12442 label,
12443 themed_muted_text(theme, 11.0),
12444 LayoutStyle::new().with_width_percent(1.0),
12445 );
12446}
12447
12448fn themed_button_options(
12449 theme: &Theme,
12450 action: impl Into<String>,
12451 state: ComponentState,
12452 layout: LayoutStyle,
12453) -> widgets::ButtonOptions {
12454 let mut options = widgets::ButtonOptions::new(layout).with_action(action.into());
12455 options.visual = theme.resolve_visual(ComponentRole::Button, state);
12456 options.hovered_visual =
12457 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::HOVERED));
12458 options.pressed_visual =
12459 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
12460 options.pressed_hovered_visual =
12461 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
12462 options.focused_visual =
12463 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::FOCUSED));
12464 options.disabled_visual =
12465 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::DISABLED));
12466 options.text_style = theme.resolve_text(ComponentRole::Button, state);
12467 options
12468}
12469
12470fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12471 let preview_scene_size = style_preview_scene_size(state.styling);
12472 let preview_min_width = preview_scene_size.width + 16.0;
12473 let preview_min_height = preview_scene_size.height + 16.0;
12474 let body_min_width = STYLING_CONTROLS_WIDTH + 1.0 + preview_min_width + 20.0;
12475 let body = section_with_min_viewport(
12476 ui,
12477 parent,
12478 "styling",
12479 "Styling",
12480 UiSize::new(body_min_width, preview_min_height),
12481 );
12482 let grid_layout = operad::layout::with_grid_template_columns(
12483 Layout::grid()
12484 .size(LayoutSize::percent(1.0, 1.0))
12485 .gap(LayoutGap::points(10.0, 10.0))
12486 .to_layout_style(),
12487 [
12488 LayoutGridTrack::points(STYLING_CONTROLS_WIDTH),
12489 LayoutGridTrack::points(1.0),
12490 LayoutGridTrack::minmax_points_fraction(preview_min_width, 1.0),
12491 ],
12492 );
12493 let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
12494 let controls = ui.add_child(
12495 grid,
12496 UiNode::container(
12497 "styling.controls",
12498 LayoutStyle::column()
12499 .with_width(STYLING_CONTROLS_WIDTH)
12500 .with_height_percent(1.0)
12501 .with_flex_shrink(0.0)
12502 .gap(6.0),
12503 ),
12504 );
12505 style_edge_group(
12506 ui,
12507 controls,
12508 "styling.inner",
12509 "Inner margin",
12510 "styling.inner_same",
12511 state.styling.inner_same,
12512 [
12513 ("Left", "styling.inner", state.styling.inner_margin),
12514 ("Right", "styling.inner_right", state.styling.inner_right),
12515 ("Top", "styling.inner_top", state.styling.inner_top),
12516 ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
12517 ],
12518 0.0..32.0,
12519 );
12520 style_edge_group(
12521 ui,
12522 controls,
12523 "styling.outer",
12524 "Outer margin",
12525 "styling.outer_same",
12526 state.styling.outer_same,
12527 [
12528 ("Left", "styling.outer", state.styling.outer_margin),
12529 ("Right", "styling.outer_right", state.styling.outer_right),
12530 ("Top", "styling.outer_top", state.styling.outer_top),
12531 ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
12532 ],
12533 0.0..40.0,
12534 );
12535 style_edge_group(
12536 ui,
12537 controls,
12538 "styling.radius",
12539 "Corner radius",
12540 "styling.radius_same",
12541 state.styling.radius_same,
12542 [
12543 ("NW", "styling.radius", state.styling.corner_radius),
12544 ("NE", "styling.radius_ne", state.styling.corner_ne),
12545 ("SW", "styling.radius_sw", state.styling.corner_sw),
12546 ("SE", "styling.radius_se", state.styling.corner_se),
12547 ],
12548 0.0..28.0,
12549 );
12550 style_fill_group(ui, controls, state);
12551 style_stroke_group(ui, controls, state);
12552 style_shadow_group(ui, controls, state);
12553 widgets::separator(
12554 ui,
12555 grid,
12556 "styling.preview.separator",
12557 widgets::SeparatorOptions::vertical().with_layout(
12558 LayoutStyle::new()
12559 .with_width(1.0)
12560 .with_height_percent(1.0)
12561 .with_flex_shrink(0.0),
12562 ),
12563 );
12564
12565 let preview = ui.add_child(
12566 grid,
12567 UiNode::container(
12568 "styling.preview",
12569 operad::layout::with_min_size(
12570 LayoutStyle::column()
12571 .with_width_percent(1.0)
12572 .with_height_percent(1.0)
12573 .with_flex_shrink(0.0)
12574 .padding(8.0),
12575 operad::layout::px(preview_min_width),
12576 operad::layout::px(preview_min_height),
12577 ),
12578 )
12579 .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
12580 );
12581 style_preview(ui, preview, state.styling);
12582}
12583
12584#[allow(clippy::too_many_arguments)]
12585fn style_edge_group(
12586 ui: &mut UiDocument,
12587 parent: UiNodeId,
12588 name: &'static str,
12589 title: &'static str,
12590 same_action: &'static str,
12591 same: bool,
12592 values: [(&'static str, &'static str, f32); 4],
12593 range: std::ops::Range<f32>,
12594) {
12595 let group = style_control_group(ui, parent, format!("{name}.group"));
12596 style_group_title(ui, group, format!("{name}.title"), title);
12597 let fields = ui.add_child(
12598 group,
12599 UiNode::container(
12600 format!("{name}.fields"),
12601 LayoutStyle::column()
12602 .with_width(138.0)
12603 .with_flex_shrink(0.0)
12604 .gap(3.0),
12605 ),
12606 );
12607 style_compact_checkbox(ui, fields, same_action, "same", same);
12608 if same {
12609 style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
12610 } else {
12611 for (label, action, value) in values {
12612 style_number_row(ui, fields, action, label, value, range.clone(), 0);
12613 }
12614 }
12615}
12616
12617fn style_fill_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12618 let group = style_control_group(ui, parent, "styling.fill.group");
12619 style_group_title(ui, group, "styling.fill.title", "Fill");
12620 let fields = style_group_fields(
12621 ui,
12622 group,
12623 "styling.fill.fields",
12624 STYLING_WIDE_FIELDS_WIDTH,
12625 4.0,
12626 );
12627 style_color_button_row(
12628 ui,
12629 fields,
12630 "styling.fill_color_button",
12631 "",
12632 state.styling.fill_color(),
12633 "Pick fill color",
12634 );
12635 if state.styling_fill_picker_open {
12636 ext_widgets::color_picker(
12637 ui,
12638 fields,
12639 "styling.fill_picker",
12640 &state.styling_fill_picker,
12641 ext_widgets::ColorPickerOptions::default()
12642 .with_label("Fill")
12643 .with_action_prefix("styling.fill_picker"),
12644 );
12645 }
12646}
12647
12648fn style_stroke_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12649 let group = style_control_group(ui, parent, "styling.stroke.group");
12650 style_group_title(ui, group, "styling.stroke.title", "Stroke");
12651 let fields = style_group_fields(
12652 ui,
12653 group,
12654 "styling.stroke.fields",
12655 STYLING_WIDE_FIELDS_WIDTH,
12656 4.0,
12657 );
12658 let width_row = row(ui, fields, "styling.stroke.row", 6.0);
12659 style_inline_number(
12660 ui,
12661 width_row,
12662 "styling.stroke",
12663 "width",
12664 state.styling.stroke_width,
12665 0.0..STYLING_STROKE_MAX,
12666 1,
12667 );
12668 let mut options = widgets::SliderOptions::default()
12669 .with_layout(
12670 LayoutStyle::new()
12671 .with_width(60.0)
12672 .with_height(20.0)
12673 .with_flex_shrink(0.0),
12674 )
12675 .with_value_edit_action("styling.stroke");
12676 options.fill_color = color(120, 170, 230);
12677 widgets::slider(
12678 ui,
12679 width_row,
12680 "styling.stroke.slider",
12681 (state.styling.stroke_width / STYLING_STROKE_MAX).clamp(0.0, 1.0),
12682 0.0..1.0,
12683 options,
12684 );
12685 style_color_button_row(
12686 ui,
12687 fields,
12688 "styling.stroke_color_button",
12689 "",
12690 state.styling.stroke_color(),
12691 "Pick stroke color",
12692 );
12693 if state.styling_stroke_picker_open {
12694 ext_widgets::color_picker(
12695 ui,
12696 fields,
12697 "styling.stroke_picker",
12698 &state.styling_stroke_picker,
12699 ext_widgets::ColorPickerOptions::default()
12700 .with_label("Stroke color")
12701 .with_action_prefix("styling.stroke_picker"),
12702 );
12703 }
12704}
12705
12706fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12707 let group = style_control_group(ui, parent, "styling.shadow.group");
12708 style_group_title(ui, group, "styling.shadow.title", "Shadow");
12709 let fields = style_group_fields(
12710 ui,
12711 group,
12712 "styling.shadow.fields",
12713 STYLING_WIDE_FIELDS_WIDTH,
12714 4.0,
12715 );
12716 let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
12717 style_inline_number(
12718 ui,
12719 offsets,
12720 "styling.shadow_x",
12721 "x",
12722 state.styling.shadow_x,
12723 -24.0..24.0,
12724 0,
12725 );
12726 style_inline_number(
12727 ui,
12728 offsets,
12729 "styling.shadow_y",
12730 "y",
12731 state.styling.shadow_y,
12732 -24.0..24.0,
12733 0,
12734 );
12735 let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
12736 style_inline_number(
12737 ui,
12738 spread,
12739 "styling.shadow",
12740 "blur",
12741 state.styling.shadow_blur,
12742 0.0..32.0,
12743 0,
12744 );
12745 style_inline_number(
12746 ui,
12747 spread,
12748 "styling.shadow_spread",
12749 "spread",
12750 state.styling.shadow_spread,
12751 0.0..16.0,
12752 0,
12753 );
12754 style_color_button_row(
12755 ui,
12756 fields,
12757 "styling.shadow_color_button",
12758 "",
12759 state.styling.shadow_color(),
12760 "Pick shadow color",
12761 );
12762 if state.styling_shadow_picker_open {
12763 ext_widgets::color_picker(
12764 ui,
12765 fields,
12766 "styling.shadow_picker",
12767 &state.styling_shadow_picker,
12768 ext_widgets::ColorPickerOptions::default()
12769 .with_label("Shadow color")
12770 .with_action_prefix("styling.shadow_picker"),
12771 );
12772 }
12773}
12774
12775fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
12776 ui.add_child(
12777 parent,
12778 UiNode::container(
12779 name,
12780 LayoutStyle::row()
12781 .with_width_percent(1.0)
12782 .with_flex_shrink(0.0)
12783 .padding(4.0)
12784 .gap(8.0),
12785 )
12786 .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
12787 )
12788}
12789
12790fn style_group_fields(
12791 ui: &mut UiDocument,
12792 parent: UiNodeId,
12793 name: impl Into<String>,
12794 width: f32,
12795 gap: f32,
12796) -> UiNodeId {
12797 ui.add_child(
12798 parent,
12799 UiNode::container(
12800 name,
12801 LayoutStyle::column()
12802 .with_width(width)
12803 .with_flex_shrink(0.0)
12804 .gap(gap),
12805 ),
12806 )
12807}
12808
12809fn style_group_title(
12810 ui: &mut UiDocument,
12811 parent: UiNodeId,
12812 name: impl Into<String>,
12813 label: &'static str,
12814) {
12815 widgets::label(
12816 ui,
12817 parent,
12818 name,
12819 label,
12820 text(12.0, color(166, 176, 190)),
12821 LayoutStyle::new()
12822 .with_width(88.0)
12823 .with_flex_shrink(0.0)
12824 .with_height(22.0),
12825 );
12826}
12827
12828fn style_color_button_row(
12829 ui: &mut UiDocument,
12830 parent: UiNodeId,
12831 action: &'static str,
12832 label: &'static str,
12833 value: ColorRgba,
12834 accessibility_label: &'static str,
12835) {
12836 let row = row(ui, parent, format!("{action}.row"), 8.0);
12837 if !label.is_empty() {
12838 widgets::label(
12839 ui,
12840 row,
12841 format!("{action}.label"),
12842 label,
12843 text(12.0, color(166, 176, 190)),
12844 LayoutStyle::new()
12845 .with_width(86.0)
12846 .with_flex_shrink(0.0)
12847 .with_height(24.0),
12848 );
12849 }
12850 ext_widgets::color_edit_button(
12851 ui,
12852 row,
12853 action,
12854 value,
12855 color_mini_button_options(action)
12856 .with_format(ext_widgets::ColorValueFormat::Rgba)
12857 .accessibility_label(accessibility_label),
12858 );
12859 widgets::label(
12860 ui,
12861 row,
12862 format!("{action}.value"),
12863 ext_widgets::color_picker::format_hex_color(value, value.a < 255),
12864 text(12.0, color(226, 232, 242)),
12865 LayoutStyle::new().with_width(96.0).with_height(24.0),
12866 );
12867}
12868
12869fn style_number_row(
12870 ui: &mut UiDocument,
12871 parent: UiNodeId,
12872 name: &'static str,
12873 label: &'static str,
12874 value: f32,
12875 range: std::ops::Range<f32>,
12876 decimals: u8,
12877) {
12878 let row = row(ui, parent, format!("{name}.row"), 6.0);
12879 widgets::label(
12880 ui,
12881 row,
12882 format!("{name}.label"),
12883 label,
12884 text(12.0, color(166, 176, 190)),
12885 LayoutStyle::new().with_width(48.0).with_height(22.0),
12886 );
12887 style_value_input(ui, row, name, value, range, decimals);
12888}
12889
12890fn style_inline_number(
12891 ui: &mut UiDocument,
12892 parent: UiNodeId,
12893 name: &'static str,
12894 label: &'static str,
12895 value: f32,
12896 range: std::ops::Range<f32>,
12897 decimals: u8,
12898) {
12899 let row = compact_row(ui, parent, format!("{name}.inline"), 3.0);
12900 widgets::label(
12901 ui,
12902 row,
12903 format!("{name}.inline_label"),
12904 format!("{label}:"),
12905 text(12.0, color(166, 176, 190)),
12906 LayoutStyle::new()
12907 .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
12908 .with_height(22.0),
12909 );
12910 style_value_input(ui, row, name, value, range, decimals);
12911}
12912
12913fn style_value_input(
12914 ui: &mut UiDocument,
12915 parent: UiNodeId,
12916 name: &'static str,
12917 value: f32,
12918 range: std::ops::Range<f32>,
12919 decimals: u8,
12920) {
12921 let mut options = widgets::DragValueOptions::default()
12922 .with_layout(
12923 LayoutStyle::row()
12924 .with_width(STYLING_VALUE_INPUT_WIDTH)
12925 .with_height(22.0)
12926 .with_flex_shrink(0.0)
12927 .with_align_items(taffy::prelude::AlignItems::Center)
12928 .with_justify_content(taffy::prelude::JustifyContent::Center)
12929 .with_padding(4.0),
12930 )
12931 .with_range(ext_widgets::NumericRange::new(
12932 f64::from(range.start),
12933 f64::from(range.end),
12934 ))
12935 .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
12936 .with_action(name);
12937 options.text_style = text(12.0, color(226, 232, 242));
12938 widgets::drag_value_input(ui, parent, name, f64::from(value), options);
12939}
12940
12941fn style_compact_checkbox(
12942 ui: &mut UiDocument,
12943 parent: UiNodeId,
12944 name: &'static str,
12945 label: &'static str,
12946 checked: bool,
12947) {
12948 let mut options = widgets::CheckboxOptions::default().with_action(name);
12949 options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
12950 options.text_style = text(12.0, color(220, 228, 238));
12951 widgets::checkbox(ui, parent, name, label, checked, options);
12952}
12953
12954fn compact_row(
12955 ui: &mut UiDocument,
12956 parent: UiNodeId,
12957 name: impl Into<String>,
12958 gap: f32,
12959) -> UiNodeId {
12960 ui.add_child(
12961 parent,
12962 UiNode::container(
12963 name,
12964 LayoutStyle::row()
12965 .with_height(22.0)
12966 .with_flex_shrink(0.0)
12967 .with_align_items(taffy::prelude::AlignItems::Center)
12968 .gap(gap),
12969 ),
12970 )
12971}
12972
12973fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
12974 ext_widgets::ColorButtonOptions::default()
12975 .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
12976 .with_swatch_size(UiSize::new(22.0, 18.0))
12977 .with_action(action)
12978 .show_label(false)
12979}
12980
12981fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
12982 let (frame, text_rect) = style_preview_rects(styling);
12983 let scene_size = style_preview_scene_size(styling);
12984 ui.add_child(
12985 parent,
12986 UiNode::scene(
12987 "styling.preview.scene",
12988 vec![
12989 ScenePrimitive::Rect(
12990 PaintRect::solid(frame, styling.fill_color())
12991 .stroke(AlignedStroke::inside(StrokeStyle::new(
12992 styling.stroke_color(),
12993 styling.stroke_width,
12994 )))
12995 .corner_radii(styling.radii())
12996 .effect(PaintEffect::shadow(
12997 styling.shadow_color(),
12998 UiPoint::new(styling.shadow_x, styling.shadow_y),
12999 styling.shadow_blur,
13000 styling.shadow_spread,
13001 )),
13002 ),
13003 ScenePrimitive::Text(
13004 PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
13005 .horizontal_align(TextHorizontalAlign::Center)
13006 .vertical_align(TextVerticalAlign::Center)
13007 .multiline(false),
13008 ),
13009 ],
13010 operad::layout::with_min_size(
13011 LayoutStyle::new()
13012 .with_width_percent(1.0)
13013 .with_height(180.0)
13014 .with_flex_shrink(0.0),
13015 operad::layout::px(scene_size.width),
13016 operad::layout::px(scene_size.height),
13017 ),
13018 ),
13019 );
13020}
13021
13022fn style_preview_rects(styling: StylingState) -> (UiRect, UiRect) {
13023 let outer = styling.outer_edges();
13024 let inner = styling.inner_edges();
13025 let frame = UiRect::new(
13026 22.0 + outer[0],
13027 28.0 + outer[2],
13028 108.0 + inner[0] + inner[1],
13029 40.0 + inner[2] + inner[3],
13030 );
13031 let text_rect = UiRect::new(
13032 frame.x + inner[0],
13033 frame.y + inner[2],
13034 (frame.width - inner[0] - inner[1]).max(1.0),
13035 (frame.height - inner[2] - inner[3]).max(1.0),
13036 );
13037 (frame, text_rect)
13038}
13039
13040fn style_preview_scene_size(styling: StylingState) -> UiSize {
13041 let (frame, text_rect) = style_preview_rects(styling);
13042 let shadow_outset = styling.shadow_blur.max(0.0) + styling.shadow_spread.max(0.0);
13043 let shadow_bounds = UiRect::new(
13044 frame.x + styling.shadow_x - shadow_outset,
13045 frame.y + styling.shadow_y - shadow_outset,
13046 frame.width + shadow_outset * 2.0,
13047 frame.height + shadow_outset * 2.0,
13048 );
13049 let right = frame
13050 .right()
13051 .max(text_rect.right())
13052 .max(shadow_bounds.right());
13053 let bottom = frame
13054 .bottom()
13055 .max(text_rect.bottom())
13056 .max(shadow_bounds.bottom())
13057 .max(180.0);
13058 UiSize::new(right.ceil().max(1.0), bottom.ceil().max(1.0))
13059}
13060
13061fn slider_options(state: &ShowcaseState, width: f32) -> widgets::SliderOptions {
13062 let mut options = widgets::SliderOptions::default().with_layout(
13063 LayoutStyle::new()
13064 .with_width(width)
13065 .with_height(24.0)
13066 .with_flex_shrink(0.0),
13067 );
13068 options.fill_color = if state.slider_trailing_color {
13069 state.slider_trailing_picker.value()
13070 } else {
13071 color(42, 49, 58)
13072 };
13073 options.thumb_shape = match state.slider_thumb_shape {
13074 SliderThumbChoice::Circle => widgets::slider::SliderThumbShape::Circle,
13075 SliderThumbChoice::Square => widgets::slider::SliderThumbShape::Square,
13076 SliderThumbChoice::Rectangle => widgets::slider::SliderThumbShape::Rectangle,
13077 };
13078 options.thumb_visual = UiVisual::panel(
13079 state.slider_thumb_picker.value(),
13080 Some(StrokeStyle::new(color(79, 93, 113), 1.0)),
13081 6.0,
13082 );
13083 options
13084}
13085
13086#[allow(clippy::field_reassign_with_default)]
13087fn slider_number_input(
13088 ui: &mut UiDocument,
13089 parent: UiNodeId,
13090 name: &'static str,
13091 input: &TextInputState,
13092 focused: FocusedTextInput,
13093 state: &ShowcaseState,
13094 width: f32,
13095) {
13096 let mut options = TextInputOptions::default();
13097 options.layout = LayoutStyle::new().with_width(width).with_height(28.0);
13098 options.text_style = text(12.0, color(230, 236, 246));
13099 options.placeholder_style = text(12.0, color(144, 156, 174));
13100 options.edit_action = Some(format!("{name}.edit").into());
13101 options.focused = state.focused_text == Some(focused);
13102 options.caret_visible = caret_visible(state.caret_phase);
13103 widgets::text_input(ui, parent, name, input, options);
13104}
13105
13106fn form_status_chip(
13107 ui: &mut UiDocument,
13108 parent: UiNodeId,
13109 name: &'static str,
13110 label: &'static str,
13111 active: bool,
13112) {
13113 let chip = ui.add_child(
13114 parent,
13115 UiNode::container(
13116 name,
13117 LayoutStyle::new()
13118 .with_width(82.0)
13119 .with_height(24.0)
13120 .with_padding(4.0)
13121 .with_flex_shrink(0.0),
13122 )
13123 .with_visual(UiVisual::panel(
13124 if active {
13125 color(35, 74, 54)
13126 } else {
13127 color(28, 34, 43)
13128 },
13129 Some(StrokeStyle::new(
13130 if active {
13131 color(90, 160, 112)
13132 } else {
13133 color(60, 72, 88)
13134 },
13135 1.0,
13136 )),
13137 4.0,
13138 )),
13139 );
13140 widgets::label(
13141 ui,
13142 chip,
13143 format!("{name}.label"),
13144 label,
13145 text(11.0, color(218, 228, 240)),
13146 LayoutStyle::new()
13147 .with_width_percent(1.0)
13148 .with_height_percent(1.0),
13149 );
13150}
13151
13152fn profile_form_summary(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13153 let has_errors = widgets::form_has_errors(&state.form);
13154 let title = profile_form_summary_title(state, has_errors);
13155 let detail = format!(
13156 "{} | {} | {}",
13157 profile_summary_value(state.form_name_text.text(), "No name"),
13158 profile_summary_value(state.form_email_text.text(), "No email"),
13159 profile_summary_value(state.form_role_text.text(), "No role"),
13160 );
13161 let hint = profile_form_summary_hint(state, has_errors);
13162 let stroke = if has_errors {
13163 color(196, 94, 104)
13164 } else if state.form.dirty {
13165 color(205, 160, 71)
13166 } else if state.form.submitted {
13167 color(91, 164, 119)
13168 } else {
13169 color(60, 72, 88)
13170 };
13171 let summary = ui.add_child(
13172 parent,
13173 UiNode::container(
13174 "forms.profile.summary",
13175 LayoutStyle::column()
13176 .with_width_percent(1.0)
13177 .with_padding(10.0)
13178 .with_gap(4.0)
13179 .with_flex_shrink(0.0),
13180 )
13181 .with_visual(UiVisual::panel(
13182 color(20, 25, 32),
13183 Some(StrokeStyle::new(stroke, 1.0)),
13184 4.0,
13185 ))
13186 .with_accessibility(
13187 AccessibilityMeta::new(AccessibilityRole::Group)
13188 .label("Live profile summary")
13189 .value(format!("{title}. {detail}. {hint}")),
13190 ),
13191 );
13192 widgets::label(
13193 ui,
13194 summary,
13195 "forms.profile.summary.title",
13196 title,
13197 text(13.0, color(232, 240, 250)),
13198 LayoutStyle::new().with_width_percent(1.0),
13199 );
13200 widgets::label(
13201 ui,
13202 summary,
13203 "forms.profile.summary.detail",
13204 detail,
13205 text(12.0, color(186, 198, 216)),
13206 LayoutStyle::new().with_width_percent(1.0),
13207 );
13208 widgets::label(
13209 ui,
13210 summary,
13211 "forms.profile.summary.hint",
13212 hint,
13213 text(11.0, color(154, 166, 184)),
13214 LayoutStyle::new().with_width_percent(1.0),
13215 );
13216}
13217
13218fn profile_form_summary_title(state: &ShowcaseState, has_errors: bool) -> &'static str {
13219 if has_errors {
13220 "Profile needs fixes"
13221 } else if state.form.submitted {
13222 "Profile submitted"
13223 } else if state.form.dirty {
13224 "Profile draft"
13225 } else {
13226 "Profile saved"
13227 }
13228}
13229
13230fn profile_form_summary_hint(state: &ShowcaseState, has_errors: bool) -> &'static str {
13231 if has_errors {
13232 "Fix validation errors before applying or submitting."
13233 } else if state.form.dirty {
13234 "Apply saves the draft; Submit saves and marks it submitted."
13235 } else if state.form.submitted {
13236 "Submission completed. Apply stays disabled until something changes."
13237 } else {
13238 "No pending changes. Submit marks the saved profile submitted."
13239 }
13240}
13241
13242fn profile_summary_value<'a>(value: &'a str, empty: &'static str) -> &'a str {
13243 let value = value.trim();
13244 if value.is_empty() {
13245 empty
13246 } else {
13247 value
13248 }
13249}
13250
13251#[allow(clippy::field_reassign_with_default)]
13252fn form_text_field(
13253 ui: &mut UiDocument,
13254 parent: UiNodeId,
13255 name: &'static str,
13256 input: &TextInputState,
13257 focused: FocusedTextInput,
13258 state: &ShowcaseState,
13259) {
13260 let mut options = TextInputOptions::default();
13261 options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
13262 options.text_style = text(12.0, color(230, 236, 246));
13263 options.placeholder_style = text(12.0, color(144, 156, 174));
13264 options.placeholder = "Required".to_string();
13265 options.edit_action = Some(format!("{name}.edit").into());
13266 options.focused = state.focused_text == Some(focused);
13267 options.caret_visible = caret_visible(state.caret_phase);
13268 widgets::text_input(ui, parent, name, input, options);
13269}
13270
13271fn profile_email_valid(email: &str) -> bool {
13272 let email = email.trim();
13273 let Some((local, domain)) = email.split_once('@') else {
13274 return false;
13275 };
13276 !local.is_empty() && domain.contains('.') && !domain.ends_with('.')
13277}
13278
13279fn drag_source_layout() -> LayoutStyle {
13280 LayoutStyle::row()
13281 .with_width(128.0)
13282 .with_height(40.0)
13283 .with_padding(8.0)
13284 .with_gap(6.0)
13285 .with_flex_shrink(0.0)
13286}
13287
13288fn drop_zone_layout() -> LayoutStyle {
13289 LayoutStyle::column()
13290 .with_width(128.0)
13291 .with_height(78.0)
13292 .with_padding(10.0)
13293 .with_gap(6.0)
13294 .with_flex_shrink(0.0)
13295}
13296
13297fn dnd_operation_chip(
13298 ui: &mut UiDocument,
13299 parent: UiNodeId,
13300 name: &'static str,
13301 label: &'static str,
13302) {
13303 let chip = ui.add_child(
13304 parent,
13305 UiNode::container(
13306 name,
13307 LayoutStyle::new()
13308 .with_width(58.0)
13309 .with_height(22.0)
13310 .with_padding(3.0)
13311 .with_flex_shrink(0.0),
13312 )
13313 .with_visual(UiVisual::panel(
13314 color(26, 32, 42),
13315 Some(StrokeStyle::new(color(62, 76, 94), 1.0)),
13316 3.0,
13317 )),
13318 );
13319 widgets::label(
13320 ui,
13321 chip,
13322 format!("{name}.label"),
13323 label,
13324 text(11.0, color(190, 204, 222)),
13325 LayoutStyle::new()
13326 .with_width_percent(1.0)
13327 .with_height_percent(1.0),
13328 );
13329}
13330
13331fn media_preview_image_layout() -> LayoutStyle {
13332 LayoutStyle::size(46.0, 46.0).with_flex_shrink(0.0)
13333}
13334
13335fn media_icon_columns(state: &ShowcaseState) -> usize {
13336 let theme = state.app_theme();
13337 let options = showcase_desktop_options(state.last_desktop_size, &theme);
13338 let window_width = state
13339 .desktop
13340 .size("media", default_window_size("media"))
13341 .width;
13342 let content_width = (window_width - options.content_padding * 2.0).max(MEDIA_ICON_TILE_WIDTH);
13343 let pitch = MEDIA_ICON_TILE_WIDTH + MEDIA_ICON_GRID_GAP;
13344 (((content_width + MEDIA_ICON_GRID_GAP) / pitch).floor() as usize).clamp(1, MEDIA_ICON_COLUMNS)
13345}
13346
13347fn media_icon_grid_width(columns: usize) -> f32 {
13348 let columns = columns.max(1);
13349 columns as f32 * MEDIA_ICON_TILE_WIDTH + columns.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13350}
13351
13352fn media_icon_grid_height(columns: usize, item_count: usize) -> f32 {
13353 let columns = columns.max(1);
13354 let rows = item_count.div_ceil(columns).max(1);
13355 rows as f32 * MEDIA_ICON_TILE_HEIGHT + rows.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13356}
13357
13358fn media_icon_grid(
13359 ui: &mut UiDocument,
13360 parent: UiNodeId,
13361 name: impl Into<String>,
13362 columns: usize,
13363 item_count: usize,
13364) -> UiNodeId {
13365 let columns = columns.clamp(1, MEDIA_ICON_COLUMNS);
13366 let rows = item_count.div_ceil(columns).max(1);
13367 let width = media_icon_grid_width(columns);
13368 let height = media_icon_grid_height(columns, item_count);
13369 let layout = operad::layout::with_grid_template_rows(
13370 operad::layout::with_grid_template_columns(
13371 Layout::grid()
13372 .size(LayoutSize::points(width, height))
13373 .gap(LayoutGap::points(MEDIA_ICON_GRID_GAP, MEDIA_ICON_GRID_GAP))
13374 .flex(0.0, 0.0, LayoutDimension::Auto)
13375 .to_layout_style(),
13376 (0..columns).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_WIDTH)),
13377 ),
13378 (0..rows).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_HEIGHT)),
13379 );
13380 ui.add_child(parent, UiNode::container(name, layout))
13381}
13382
13383fn media_icon_tile(ui: &mut UiDocument, parent: UiNodeId, icon: BuiltInIcon) {
13384 let name = icon.key().replace('.', "_").replace('-', "_");
13385 let tile = ui.add_child(
13386 parent,
13387 UiNode::container(
13388 format!("media.icon_tile.{name}"),
13389 LayoutStyle::column()
13390 .with_width(MEDIA_ICON_TILE_WIDTH)
13391 .with_height(MEDIA_ICON_TILE_HEIGHT)
13392 .with_padding(6.0)
13393 .with_gap(4.0)
13394 .with_flex_shrink(0.0),
13395 )
13396 .with_visual(UiVisual::panel(
13397 color(17, 22, 30),
13398 Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
13399 4.0,
13400 )),
13401 );
13402 widgets::image(
13403 ui,
13404 tile,
13405 format!("media.icon.{name}"),
13406 icon_image(icon),
13407 widgets::ImageOptions::default()
13408 .with_layout(LayoutStyle::size(28.0, 28.0))
13409 .with_accessibility_label(icon.label()),
13410 );
13411 widgets::label(
13412 ui,
13413 tile,
13414 format!("media.icon_label.{name}"),
13415 icon.label(),
13416 text(9.0, color(180, 194, 214)),
13417 LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13418 );
13419}
13420
13421fn slider_checkbox(
13422 ui: &mut UiDocument,
13423 parent: UiNodeId,
13424 name: &'static str,
13425 label: &'static str,
13426 checked: bool,
13427) {
13428 slider_checkbox_with_layout(
13429 ui,
13430 parent,
13431 name,
13432 label,
13433 checked,
13434 LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13435 );
13436}
13437
13438fn slider_checkbox_with_layout(
13439 ui: &mut UiDocument,
13440 parent: UiNodeId,
13441 name: &'static str,
13442 label: &'static str,
13443 checked: bool,
13444 layout: LayoutStyle,
13445) {
13446 let mut options = widgets::CheckboxOptions::default().with_action(name);
13447 options.layout = layout;
13448 options.text_style = text(12.0, color(220, 228, 238));
13449 widgets::checkbox(ui, parent, name, label, checked, options);
13450}
13451
13452fn choice_button(
13453 ui: &mut UiDocument,
13454 parent: UiNodeId,
13455 name: &'static str,
13456 label: &'static str,
13457 selected: bool,
13458) {
13459 let mut options =
13460 widgets::ButtonOptions::new(LayoutStyle::new().with_width(78.0).with_height(28.0))
13461 .with_action(name);
13462 options.visual = if selected {
13463 button_visual(48, 112, 184)
13464 } else {
13465 button_visual(38, 46, 58)
13466 };
13467 options.hovered_visual = Some(button_visual(65, 86, 106));
13468 options.pressed_visual = Some(button_visual(34, 54, 84));
13469 options.text_style = text(12.0, color(238, 244, 252));
13470 widgets::button(ui, parent, name, label, options);
13471}
13472
13473fn divider(ui: &mut UiDocument, parent: UiNodeId, name: &'static str) {
13474 ui.add_child(
13475 parent,
13476 UiNode::container(
13477 name,
13478 LayoutStyle::new()
13479 .with_width_percent(1.0)
13480 .with_height(1.0)
13481 .with_flex_shrink(0.0),
13482 )
13483 .with_visual(UiVisual::panel(color(48, 58, 72), None, 0.0)),
13484 );
13485}
13486
13487fn canvas(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13488 let canvas_intrinsic = UiSize::new(720.0, 405.0);
13489 let body = section_with_min_viewport(ui, parent, "canvas", "Canvas", UiSize::new(720.0, 458.0));
13490 let controls = wrapping_row(ui, body, "canvas.options", 10.0);
13491 canvas_option_checkbox(
13492 ui,
13493 controls,
13494 "canvas.grow_horizontal",
13495 "Grow width",
13496 state.canvas_grow_horizontal,
13497 );
13498 canvas_option_checkbox(
13499 ui,
13500 controls,
13501 "canvas.grow_vertical",
13502 "Grow height",
13503 state.canvas_grow_vertical,
13504 );
13505 canvas_option_checkbox(
13506 ui,
13507 controls,
13508 "canvas.keep_aspect_ratio",
13509 "Keep aspect ratio",
13510 state.canvas_keep_aspect_ratio,
13511 );
13512
13513 let mut options = widgets::CanvasOptions::default()
13514 .with_accessibility_label("Shader canvas")
13515 .with_action("canvas.rotate")
13516 .with_intrinsic_size(canvas_intrinsic);
13517 options.action_mode = WidgetActionMode::Drag;
13518 if state.canvas_keep_aspect_ratio {
13519 options = options.with_aspect_ratio(16.0 / 9.0);
13520 }
13521 let canvas_width = if state.canvas_grow_horizontal {
13522 LayoutDimension::percent(1.0)
13523 } else {
13524 LayoutDimension::points(canvas_intrinsic.width)
13525 };
13526 let canvas_height = if state.canvas_grow_vertical {
13527 LayoutDimension::percent(1.0)
13528 } else {
13529 LayoutDimension::points(canvas_intrinsic.height)
13530 };
13531 options.layout = Layout::new()
13532 .size(LayoutSize::new(canvas_width, canvas_height))
13533 .min_size(LayoutSize::points(
13534 canvas_intrinsic.width,
13535 canvas_intrinsic.height,
13536 ))
13537 .flex(
13538 if state.canvas_grow_vertical { 1.0 } else { 0.0 },
13539 1.0,
13540 LayoutDimension::Auto,
13541 )
13542 .to_layout_style();
13543 options.visual = UiVisual::panel(
13544 color(18, 22, 28),
13545 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
13546 4.0,
13547 );
13548 widgets::canvas(
13549 ui,
13550 body,
13551 "canvas.shader",
13552 CanvasContent::new("canvas.shader").program(showcase_canvas_program(state.cube)),
13553 options,
13554 );
13555}
13556
13557fn canvas_option_checkbox(
13558 ui: &mut UiDocument,
13559 parent: UiNodeId,
13560 name: &'static str,
13561 label: &'static str,
13562 checked: bool,
13563) {
13564 let mut options = widgets::CheckboxOptions::default()
13565 .with_action(name)
13566 .with_text_style(text(12.0, color(220, 228, 238)));
13567 options.layout = LayoutStyle::new().with_height(28.0).with_flex_shrink(0.0);
13568 widgets::checkbox(ui, parent, name, label, checked, options);
13569}
13570
13571fn showcase_canvas_program(cube: CanvasCubeState) -> CanvasRenderProgram {
13572 CanvasRenderProgram::wgsl(include_str!("shaders/showcase_canvas.wgsl"))
13573 .label("showcase.canvas")
13574 .constant("CUBE_YAW", cube.yaw as f64)
13575 .constant("CUBE_PITCH", cube.pitch as f64)
13576 .clear_color(Some(color(18, 22, 28)))
13577}
13578
13579fn section(
13580 ui: &mut UiDocument,
13581 parent: UiNodeId,
13582 name: impl Into<String>,
13583 _title: impl Into<String>,
13584) -> UiNodeId {
13585 section_with_min_viewport(ui, parent, name, _title, UiSize::ZERO)
13586}
13587
13588fn section_with_min_viewport(
13589 ui: &mut UiDocument,
13590 parent: UiNodeId,
13591 name: impl Into<String>,
13592 _title: impl Into<String>,
13593 min_viewport_size: UiSize,
13594) -> UiNodeId {
13595 let name = name.into();
13596 let layout = Layout::column()
13597 .size(LayoutSize::percent(1.0, 1.0))
13598 .min_size(LayoutSize::points(
13599 min_viewport_size.width.max(0.0),
13600 min_viewport_size.height.max(0.0),
13601 ))
13602 .gap(LayoutGap::points(10.0, 10.0))
13603 .flex(1.0, 1.0, LayoutDimension::Auto)
13604 .to_layout_style();
13605 widgets::scroll_area(
13606 ui,
13607 parent,
13608 format!("{name}.section_scroll"),
13609 ScrollAxes::BOTH,
13610 layout,
13611 )
13612}
13613
13614fn row(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>, gap: f32) -> UiNodeId {
13615 ui.add_child(
13616 parent,
13617 UiNode::container(
13618 name,
13619 Layout::row()
13620 .size(LayoutSize::new(
13621 LayoutDimension::percent(1.0),
13622 LayoutDimension::Auto,
13623 ))
13624 .align_items(LayoutAlignment::Center)
13625 .gap(LayoutGap::points(gap, gap))
13626 .flex(0.0, 0.0, LayoutDimension::Auto)
13627 .to_layout_style(),
13628 ),
13629 )
13630}
13631
13632fn wrapping_row(
13633 ui: &mut UiDocument,
13634 parent: UiNodeId,
13635 name: impl Into<String>,
13636 gap: f32,
13637) -> UiNodeId {
13638 ui.add_child(
13639 parent,
13640 UiNode::container(
13641 name,
13642 Layout::row()
13643 .size(LayoutSize::new(
13644 LayoutDimension::percent(1.0),
13645 LayoutDimension::Auto,
13646 ))
13647 .min_size(LayoutSize::points(0.0, 0.0))
13648 .align_items(LayoutAlignment::Center)
13649 .gap(LayoutGap::points(gap, gap))
13650 .flex_wrap(LayoutFlexWrap::Wrap)
13651 .flex(0.0, 0.0, LayoutDimension::Auto)
13652 .to_layout_style(),
13653 ),
13654 )
13655}
13656
13657fn layout_panel_contents(
13658 ui: &mut UiDocument,
13659 parent: UiNodeId,
13660 name: &'static str,
13661 offset_y: f32,
13662 items: &[&'static str],
13663) {
13664 let scroll = widgets::scroll_area(
13665 ui,
13666 parent,
13667 format!("{name}.scroll_area"),
13668 ScrollAxes::VERTICAL,
13669 LayoutStyle::column()
13670 .with_width_percent(1.0)
13671 .with_height(0.0)
13672 .with_flex_grow(1.0)
13673 .with_padding(8.0)
13674 .with_gap(6.0),
13675 );
13676 ui.node_mut(scroll).set_action(format!("{name}.scroll"));
13677 if let Some(scroll_state) = ui.node_mut(scroll).scroll_mut() {
13678 scroll_state.set_offset(UiPoint::new(0.0, offset_y));
13679 }
13680 for (index, item) in items.iter().enumerate() {
13681 let row = ui.add_child(
13682 scroll,
13683 UiNode::container(
13684 format!("{name}.row.{index}"),
13685 LayoutStyle::row()
13686 .with_width_percent(1.0)
13687 .with_height(30.0)
13688 .with_align_items(taffy::prelude::AlignItems::Center)
13689 .with_padding(8.0)
13690 .with_flex_shrink(0.0),
13691 )
13692 .with_visual(UiVisual::panel(
13693 color(20, 26, 34),
13694 Some(StrokeStyle::new(color(45, 56, 72), 1.0)),
13695 4.0,
13696 )),
13697 );
13698 widgets::label(
13699 ui,
13700 row,
13701 format!("{name}.row.{index}.label"),
13702 *item,
13703 text(12.0, color(218, 228, 242)),
13704 LayoutStyle::new().with_width_percent(1.0),
13705 );
13706 }
13707}
13708
13709fn layout_workspace_contents(
13710 ui: &mut UiDocument,
13711 parent: UiNodeId,
13712 name: &'static str,
13713 offset_y: f32,
13714) {
13715 let scroll = widgets::scroll_area(
13716 ui,
13717 parent,
13718 format!("{name}.scroll_area"),
13719 ScrollAxes::VERTICAL,
13720 LayoutStyle::column()
13721 .with_width_percent(1.0)
13722 .with_height(0.0)
13723 .with_flex_grow(1.0)
13724 .with_padding(10.0)
13725 .with_gap(10.0),
13726 );
13727 ui.node_mut(scroll).set_action(format!("{name}.scroll"));
13728 if let Some(scroll_state) = ui.node_mut(scroll).scroll_mut() {
13729 scroll_state.set_offset(UiPoint::new(0.0, offset_y));
13730 }
13731 let row_one = wrapping_row(ui, scroll, "layout.workspace.row.primary", 8.0);
13732 layout_card(
13733 ui,
13734 row_one,
13735 "layout.workspace.card.one",
13736 "Region 1",
13737 "Flexible",
13738 );
13739 layout_card(
13740 ui,
13741 row_one,
13742 "layout.workspace.card.two",
13743 "Region 2",
13744 "Wraps",
13745 );
13746 layout_card(
13747 ui,
13748 row_one,
13749 "layout.workspace.card.three",
13750 "Region 3",
13751 "Grows",
13752 );
13753
13754 let row_two = row(ui, scroll, "layout.workspace.row.secondary", 8.0);
13755 layout_card(
13756 ui,
13757 row_two,
13758 "layout.workspace.card.four",
13759 "Region 4",
13760 "Left",
13761 );
13762 layout_card(
13763 ui,
13764 row_two,
13765 "layout.workspace.card.five",
13766 "Region 5",
13767 "Right",
13768 );
13769}
13770
13771fn layout_card(
13772 ui: &mut UiDocument,
13773 parent: UiNodeId,
13774 name: &'static str,
13775 title: &'static str,
13776 subtitle: &'static str,
13777) -> UiNodeId {
13778 let card = ui.add_child(
13779 parent,
13780 UiNode::container(
13781 name,
13782 LayoutStyle::column()
13783 .with_width(128.0)
13784 .with_height(76.0)
13785 .with_flex_grow(1.0)
13786 .with_flex_shrink(1.0)
13787 .with_padding(8.0)
13788 .with_gap(4.0),
13789 )
13790 .with_visual(UiVisual::panel(
13791 color(21, 28, 38),
13792 Some(StrokeStyle::new(color(55, 68, 86), 1.0)),
13793 5.0,
13794 )),
13795 );
13796 widgets::label(
13797 ui,
13798 card,
13799 format!("{name}.title"),
13800 title,
13801 text(13.0, color(236, 242, 250)),
13802 LayoutStyle::new().with_width_percent(1.0),
13803 );
13804 widgets::label(
13805 ui,
13806 card,
13807 format!("{name}.subtitle"),
13808 subtitle,
13809 text(11.0, color(162, 176, 196)),
13810 LayoutStyle::new().with_width_percent(1.0),
13811 );
13812 card
13813}
13814
13815fn button(
13816 ui: &mut UiDocument,
13817 parent: UiNodeId,
13818 name: impl Into<String>,
13819 label: impl Into<String>,
13820 action: impl Into<String>,
13821 visual: UiVisual,
13822) -> UiNodeId {
13823 let mut options = widgets::ButtonOptions::new(LayoutStyle::new().with_height(32.0))
13824 .with_action(action.into());
13825 options.visual = visual;
13826 options.hovered_visual = Some(readable_button_hover_visual(visual));
13827 options.pressed_visual = Some(adjusted_button_visual(visual, -62));
13828 options.pressed_hovered_visual = Some(adjusted_button_visual(visual, -24));
13829 options.text_style = text(13.0, color(246, 249, 252));
13830 widgets::button(ui, parent, name, label, options)
13831}
13832
13833fn button_visual(r: u8, g: u8, b: u8) -> UiVisual {
13834 UiVisual::panel(
13835 color(r, g, b),
13836 Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
13837 4.0,
13838 )
13839}
13840
13841fn color_square_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
13842 ext_widgets::ColorButtonOptions::default()
13843 .with_layout(LayoutStyle::size(30.0, 30.0).with_flex_shrink(0.0))
13844 .with_swatch_size(UiSize::new(30.0, 30.0))
13845 .with_action(action)
13846 .show_label(false)
13847}
13848
13849fn icon_image(icon: BuiltInIcon) -> ImageContent {
13850 ImageContent::new(icon.key()).tinted(color(220, 228, 238))
13851}
13852
13853fn adjusted_button_visual(visual: UiVisual, delta: i16) -> UiVisual {
13854 UiVisual::panel(
13855 adjust_color(visual.fill, delta),
13856 visual.stroke.map(|stroke| StrokeStyle {
13857 color: adjust_color(stroke.color, delta / 2),
13858 width: stroke.width,
13859 }),
13860 visual.corner_radius,
13861 )
13862}
13863
13864fn readable_button_hover_visual(visual: UiVisual) -> UiVisual {
13865 let hovered = adjusted_button_visual(visual, 18);
13866 if contrast_ratio(hovered.fill, color(246, 249, 252)) >= 4.5 {
13867 hovered
13868 } else {
13869 adjusted_button_visual(visual, -8)
13870 }
13871}
13872
13873fn adjust_color(color: ColorRgba, delta: i16) -> ColorRgba {
13874 let channel = |value: u8| -> u8 { (i16::from(value) + delta).clamp(0, u8::MAX as i16) as u8 };
13875 ColorRgba::new(
13876 channel(color.r),
13877 channel(color.g),
13878 channel(color.b),
13879 color.a,
13880 )
13881}
13882
13883fn contrast_ratio(left: ColorRgba, right: ColorRgba) -> f32 {
13884 let left = relative_luminance(left);
13885 let right = relative_luminance(right);
13886 (left.max(right) + 0.05) / (left.min(right) + 0.05)
13887}
13888
13889fn relative_luminance(color: ColorRgba) -> f32 {
13890 fn channel(value: u8) -> f32 {
13891 let value = f32::from(value) / 255.0;
13892 if value <= 0.04045 {
13893 value / 12.92
13894 } else {
13895 ((value + 0.055) / 1.055).powf(2.4)
13896 }
13897 }
13898 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b)
13899}
13900
13901fn select_options() -> Vec<ext_widgets::SelectOption> {
13902 vec![
13903 ext_widgets::SelectOption::new("label-1", "Label 1"),
13904 ext_widgets::SelectOption::new("label-2", "Label 2"),
13905 ext_widgets::SelectOption::new("label-3", "Label 3"),
13906 ext_widgets::SelectOption::new("disabled", "Disabled").disabled(),
13907 ]
13908}
13909
13910fn select_options_with_images() -> Vec<ext_widgets::SelectOption> {
13911 vec![
13912 ext_widgets::SelectOption::new("label-1", "Label 1").image_key(BuiltInIcon::Check.key()),
13913 ext_widgets::SelectOption::new("label-2", "Label 2").image_key(BuiltInIcon::Folder.key()),
13914 ext_widgets::SelectOption::new("label-3", "Label 3").image_key(BuiltInIcon::Grid.key()),
13915 ext_widgets::SelectOption::new("disabled", "Disabled")
13916 .image_key(BuiltInIcon::Close.key())
13917 .disabled(),
13918 ]
13919}
13920
13921fn label_locale_options() -> Vec<ext_widgets::SelectOption> {
13922 vec![
13923 ext_widgets::SelectOption::new("en-US", "English"),
13924 ext_widgets::SelectOption::new("es-MX", "Español"),
13925 ext_widgets::SelectOption::new("fr-FR", "Français"),
13926 ext_widgets::SelectOption::new("de-DE", "Deutsch"),
13927 ext_widgets::SelectOption::new("it-IT", "Italiano"),
13928 ext_widgets::SelectOption::new("pt-BR", "Português"),
13929 ext_widgets::SelectOption::new("nl-NL", "Nederlands"),
13930 ]
13931}
13932
13933fn localized_label(locale_id: &str) -> &'static str {
13934 match locale_id {
13935 "en-US" => "Interface language: English",
13936 "fr-FR" => "Langue de l'interface : français",
13937 "de-DE" => "Sprache der Oberfläche: Deutsch",
13938 "it-IT" => "Lingua dell'interfaccia: italiano",
13939 "pt-BR" => "Idioma da interface: português",
13940 "nl-NL" => "Interfacetaal: Nederlands",
13941 _ => "Idioma de interfaz: español de México",
13942 }
13943}
13944
13945fn menu_bar_menus(autosave: bool, grid: bool) -> Vec<ext_widgets::MenuBarMenu> {
13946 vec![
13947 ext_widgets::MenuBarMenu::new("file", "File", menu_items(autosave)),
13948 ext_widgets::MenuBarMenu::new(
13949 "edit",
13950 "Edit",
13951 vec![
13952 ext_widgets::MenuItem::command("undo", "Undo").shortcut("Ctrl+Z"),
13953 ext_widgets::MenuItem::command("redo", "Redo").shortcut("Ctrl+Shift+Z"),
13954 ],
13955 ),
13956 ext_widgets::MenuBarMenu::new(
13957 "view",
13958 "View",
13959 vec![ext_widgets::MenuItem::check("grid", "Grid", grid)],
13960 ),
13961 ]
13962}
13963
13964fn menu_items(autosave: bool) -> Vec<ext_widgets::MenuItem> {
13965 vec![
13966 ext_widgets::MenuItem::command("new", "New").shortcut("Ctrl+N"),
13967 ext_widgets::MenuItem::command("open", "Open").shortcut("Ctrl+O"),
13968 ext_widgets::MenuItem::separator(),
13969 ext_widgets::MenuItem::check("autosave", "Autosave", autosave),
13970 ext_widgets::MenuItem::submenu(
13971 "recent",
13972 "Recent",
13973 vec![
13974 ext_widgets::MenuItem::command("recent.one", "demo.rs"),
13975 ext_widgets::MenuItem::command("recent.two", "notes.md"),
13976 ],
13977 ),
13978 ext_widgets::MenuItem::command("delete", "Delete").destructive(),
13979 ext_widgets::MenuItem::command("disabled", "Disabled").disabled(),
13980 ]
13981}
13982
13983fn menu_item_top_offset(items: &[ext_widgets::MenuItem], index: usize) -> f32 {
13984 items
13985 .iter()
13986 .take(index)
13987 .map(|item| menu_item_height(Some(item)))
13988 .sum()
13989}
13990
13991fn menu_item_height(item: Option<&ext_widgets::MenuItem>) -> f32 {
13992 if item.is_some_and(ext_widgets::MenuItem::is_separator) {
13993 8.0
13994 } else {
13995 28.0
13996 }
13997}
13998
13999fn command_palette_items() -> Vec<ext_widgets::CommandPaletteItem> {
14000 vec![
14001 ext_widgets::CommandPaletteItem::new("open", "Open")
14002 .subtitle("Open a document")
14003 .shortcut("Ctrl+O")
14004 .keyword("file"),
14005 ext_widgets::CommandPaletteItem::new("save", "Save")
14006 .subtitle("Write current changes")
14007 .shortcut("Ctrl+S"),
14008 ext_widgets::CommandPaletteItem::new("format", "Format document")
14009 .subtitle("Apply source formatting")
14010 .keyword("code"),
14011 ext_widgets::CommandPaletteItem::new("rename", "Rename symbol")
14012 .subtitle("Change every reference")
14013 .shortcut("F2"),
14014 ext_widgets::CommandPaletteItem::new("toggle_sidebar", "Toggle sidebar")
14015 .subtitle("Show or hide the widget panel")
14016 .shortcut("Ctrl+B"),
14017 ext_widgets::CommandPaletteItem::new("run", "Run current example")
14018 .subtitle("Launch showcase")
14019 .shortcut("Ctrl+R"),
14020 ext_widgets::CommandPaletteItem::new("focus_canvas", "Focus canvas")
14021 .subtitle("Move interaction to the canvas window"),
14022 ext_widgets::CommandPaletteItem::new("reset_layout", "Reset window layout")
14023 .subtitle("Restore the default showcase positions"),
14024 ext_widgets::CommandPaletteItem::new("disabled", "Disabled command").disabled(),
14025 ]
14026}
14027
14028fn command_palette_items_with_history(
14029 history: &ext_widgets::CommandPaletteHistory,
14030) -> Vec<ext_widgets::CommandPaletteItem> {
14031 let mut items = command_palette_items()
14032 .into_iter()
14033 .map(|item| {
14034 let command = CommandId::from(item.id.as_str());
14035 if history.is_recent(&command) {
14036 item.keyword("recent")
14037 } else {
14038 item
14039 }
14040 })
14041 .collect::<Vec<_>>();
14042 items.sort_by(|left, right| {
14043 let left_id = CommandId::from(left.id.as_str());
14044 let right_id = CommandId::from(right.id.as_str());
14045 match (
14046 history.recency_rank(&left_id),
14047 history.recency_rank(&right_id),
14048 ) {
14049 (Some(left_rank), Some(right_rank)) => left_rank.cmp(&right_rank),
14050 (Some(_), None) => std::cmp::Ordering::Less,
14051 (None, Some(_)) => std::cmp::Ordering::Greater,
14052 (None, None) => left.title.cmp(&right.title),
14053 }
14054 });
14055 items
14056}
14057
14058fn virtual_table_columns(state: &ShowcaseState) -> Vec<ext_widgets::DataTableColumn> {
14059 let sort = if state.virtual_table_descending {
14060 ext_widgets::DataTableSortState::descending()
14061 } else {
14062 ext_widgets::DataTableSortState::ascending()
14063 };
14064 let filter = if state.virtual_table_ready_only {
14065 ext_widgets::DataTableFilterState::active("status").with_value("Ready")
14066 } else {
14067 ext_widgets::DataTableFilterState::inactive()
14068 };
14069 vec![
14070 ext_widgets::DataTableColumn::new("name", "Name", 220.0)
14071 .with_sort(sort)
14072 .sortable("lists_tables.virtualized_table.sort.name"),
14073 ext_widgets::DataTableColumn::new("status", "Status", 160.0)
14074 .with_filter(filter)
14075 .filterable("lists_tables.virtualized_table.filter.status"),
14076 ext_widgets::DataTableColumn::new("value", "Value", state.virtual_table_value_width)
14077 .with_min_width(56.0)
14078 .with_alignment(ext_widgets::DataCellAlignment::End)
14079 .resize_command("lists_tables.virtualized_table.resize.value"),
14080 ]
14081}
14082
14083fn virtual_table_visible_rows(state: &ShowcaseState) -> Vec<usize> {
14084 let mut rows = (0..32)
14085 .filter(|row| !state.virtual_table_ready_only || row % 2 == 0)
14086 .collect::<Vec<_>>();
14087 if state.virtual_table_descending {
14088 rows.reverse();
14089 }
14090 rows
14091}
14092
14093fn virtual_table_cell_value(source_row: usize, column: usize) -> String {
14094 match column {
14095 0 => format!("Virtual row {}", source_row + 1),
14096 1 if source_row % 2 == 0 => "Ready".to_string(),
14097 1 => "Pending".to_string(),
14098 _ => format!("{}%", 30 + source_row * 2),
14099 }
14100}
14101
14102fn editable_tree_default_nodes() -> Vec<EditableTreeNode> {
14103 vec![EditableTreeNode::new("root", "root").with_children(vec![
14104 EditableTreeNode::new("child-0", "child #0").with_children(vec![
14105 EditableTreeNode::new("child-0-0", "child #0"),
14106 EditableTreeNode::new("child-0-1", "child #1"),
14107 EditableTreeNode::new("child-0-2", "child #2"),
14108 EditableTreeNode::new("child-0-3", "child #3")
14109 .with_children(vec![EditableTreeNode::new("child-0-3-0", "child #0")]),
14110 ]),
14111 EditableTreeNode::new("child-1", "child #1").with_children(vec![
14112 EditableTreeNode::new("child-1-0", "child #0"),
14113 EditableTreeNode::new("child-1-1", "child #1"),
14114 EditableTreeNode::new("child-1-2", "child #2"),
14115 ]),
14116 ])]
14117}
14118
14119fn editable_tree_items(nodes: &[EditableTreeNode]) -> Vec<ext_widgets::TreeItem> {
14120 nodes
14121 .iter()
14122 .map(|node| editable_tree_item(node, true))
14123 .collect()
14124}
14125
14126fn editable_tree_item(node: &EditableTreeNode, root: bool) -> ext_widgets::TreeItem {
14127 let mut item = ext_widgets::TreeItem::new(node.id.clone(), node.label.clone()).with_children(
14128 node.children
14129 .iter()
14130 .map(|child| editable_tree_item(child, false))
14131 .collect(),
14132 );
14133 if !root {
14134 item =
14135 item.with_row_action(ext_widgets::TreeRowAction::new("delete", "delete").destructive());
14136 }
14137 item.with_row_action(ext_widgets::TreeRowAction::new("add", "+"))
14138}
14139
14140fn find_editable_tree_node_mut<'a>(
14141 nodes: &'a mut [EditableTreeNode],
14142 id: &str,
14143) -> Option<&'a mut EditableTreeNode> {
14144 for node in nodes {
14145 if node.id == id {
14146 return Some(node);
14147 }
14148 if let Some(found) = find_editable_tree_node_mut(&mut node.children, id) {
14149 return Some(found);
14150 }
14151 }
14152 None
14153}
14154
14155fn remove_editable_tree_node(nodes: &mut Vec<EditableTreeNode>, id: &str) -> Option<String> {
14156 if let Some(index) = nodes.iter().position(|node| node.id == id) {
14157 return Some(nodes.remove(index).label);
14158 }
14159 for node in nodes {
14160 if let Some(label) = remove_editable_tree_node(&mut node.children, id) {
14161 return Some(label);
14162 }
14163 }
14164 None
14165}
14166
14167fn tree_items() -> Vec<ext_widgets::TreeItem> {
14168 vec![
14169 ext_widgets::TreeItem::new("root", "Project").with_children(vec![
14170 ext_widgets::TreeItem::new("src", "src").with_children(vec![
14171 ext_widgets::TreeItem::new("lib", "lib.rs"),
14172 ext_widgets::TreeItem::new("widgets", "widgets.rs"),
14173 ]),
14174 ext_widgets::TreeItem::new("assets", "assets").with_children(vec![
14175 ext_widgets::TreeItem::new("shader", "shader.wgsl"),
14176 ext_widgets::TreeItem::new("logo", "logo.png"),
14177 ]),
14178 ext_widgets::TreeItem::new("target", "target").disabled(),
14179 ]),
14180 ]
14181}
14182
14183fn virtual_tree_items() -> Vec<ext_widgets::TreeItem> {
14184 vec![
14185 ext_widgets::TreeItem::new("root", "Large project").with_children(vec![
14186 ext_widgets::TreeItem::new("src", "src").with_children(
14187 (0..32)
14188 .map(|index| {
14189 ext_widgets::TreeItem::new(
14190 format!("src-file-{index:02}"),
14191 format!("module_{index:02}.rs"),
14192 )
14193 })
14194 .collect(),
14195 ),
14196 ext_widgets::TreeItem::new("examples", "examples").with_children(
14197 (0..12)
14198 .map(|index| {
14199 ext_widgets::TreeItem::new(
14200 format!("example-file-{index:02}"),
14201 format!("demo_{index:02}.rs"),
14202 )
14203 })
14204 .collect(),
14205 ),
14206 ext_widgets::TreeItem::new("assets", "assets").with_children(vec![
14207 ext_widgets::TreeItem::new("icon", "icon.png"),
14208 ext_widgets::TreeItem::new("shader", "shader.wgsl"),
14209 ]),
14210 ext_widgets::TreeItem::new("target", "target").disabled(),
14211 ]),
14212 ]
14213}
14214
14215fn tree_table_items() -> Vec<ext_widgets::TreeItem> {
14216 vec![
14217 ext_widgets::TreeItem::new("root", "Workspace").with_children(vec![
14218 ext_widgets::TreeItem::new("branch-a", "Interface").with_children(vec![
14219 ext_widgets::TreeItem::new("widgets", "widgets.rs"),
14220 ext_widgets::TreeItem::new("layout", "layout.rs"),
14221 ]),
14222 ext_widgets::TreeItem::new("branch-b", "Renderer").with_children(vec![
14223 ext_widgets::TreeItem::new("wgpu", "wgpu.rs"),
14224 ext_widgets::TreeItem::new("paint", "paint.rs").disabled(),
14225 ]),
14226 ext_widgets::TreeItem::new("docs", "docs"),
14227 ]),
14228 ]
14229}
14230
14231fn parse_calendar_date(value: &str) -> Option<CalendarDate> {
14232 let mut parts = value.split('-');
14233 let year = parts.next()?.parse().ok()?;
14234 let month = parts.next()?.parse().ok()?;
14235 let day = parts.next()?.parse().ok()?;
14236 CalendarDate::new(year, month, day)
14237}
14238
14239fn parse_table_cell(value: &str) -> Option<ext_widgets::DataTableCellIndex> {
14240 let mut parts = value.split('.');
14241 let row = parts.next()?.parse().ok()?;
14242 let column = parts.next()?.parse().ok()?;
14243 if parts.next().is_some() {
14244 return None;
14245 }
14246 Some(ext_widgets::DataTableCellIndex::new(row, column))
14247}
14248
14249fn unit(value: f32) -> f32 {
14250 value.clamp(0.0, 1.0)
14251}
14252
14253fn smooth_loop(phase: f32, offset: f32) -> f32 {
14254 0.5 - ((phase + offset).cos() * 0.5)
14255}
14256
14257fn profile_form_state() -> FormState {
14258 let mut form = FormState::new("profile")
14259 .with_field("name", "Operad")
14260 .with_field("email", "ada@example.com")
14261 .with_field("role", "Designer")
14262 .with_field("newsletter", "true");
14263 let _ = form.update_field("email", "invalid@example");
14264 let request = form.begin_form_validation();
14265 let _ = form.apply_form_validation(
14266 FormValidationResult::new(request.generation)
14267 .with_field_messages(
14268 "email",
14269 vec![ValidationMessage::error("Use a complete email address")],
14270 )
14271 .with_form_message(ValidationMessage::warning("Unsaved profile changes")),
14272 );
14273 form
14274}
14275
14276fn profile_form_value(form: &FormState, id: &str) -> String {
14277 form.fields
14278 .iter()
14279 .find_map(|(field_id, field)| (field_id.as_str() == id).then(|| field.value.clone()))
14280 .unwrap_or_default()
14281}
14282
14283fn scaled_slider(rect: UiRect, point: UiPoint, min: f32, max: f32) -> f32 {
14284 min + unit(widgets::slider::slider_value_from_control_point(
14285 rect,
14286 point,
14287 0.0..1.0,
14288 )) * (max - min)
14289}
14290
14291fn resize_split_from_pointer(
14292 state: &mut ext_widgets::SplitPaneState,
14293 axis: ext_widgets::SplitAxis,
14294 edit: WidgetPointerEdit,
14295 handle_thickness: f32,
14296) -> bool {
14297 let total_extent = match axis {
14298 ext_widgets::SplitAxis::Horizontal => edit.target_rect.width,
14299 ext_widgets::SplitAxis::Vertical => edit.target_rect.height,
14300 }
14301 .max(1.0);
14302 let sizes = state.resolved_sizes(total_extent, handle_thickness);
14303 let handle_center = match axis {
14304 ext_widgets::SplitAxis::Horizontal => edit.target_rect.x + sizes.first + sizes.handle * 0.5,
14305 ext_widgets::SplitAxis::Vertical => edit.target_rect.y + sizes.first + sizes.handle * 0.5,
14306 };
14307 let pointer = match axis {
14308 ext_widgets::SplitAxis::Horizontal => edit.position.x,
14309 ext_widgets::SplitAxis::Vertical => edit.position.y,
14310 };
14311 state.resize_by(pointer - handle_center, total_extent, handle_thickness)
14312}
14313
14314fn scroll_state(offset_y: f32, viewport_height: f32, content_height: f32) -> operad::ScrollState {
14315 operad::ScrollState::new(ScrollAxes::VERTICAL)
14316 .with_sizes(
14317 UiSize::new(8.0, viewport_height),
14318 UiSize::new(8.0, content_height),
14319 )
14320 .with_offset(UiPoint::new(0.0, offset_y))
14321}
14322
14323fn timeline_scroll_state_for_view(
14324 saved: operad::ScrollState,
14325 viewport_width: f32,
14326) -> operad::ScrollState {
14327 let viewport_width = if viewport_width > f32::EPSILON {
14328 viewport_width
14329 } else if saved.viewport_size().width > f32::EPSILON {
14330 saved.viewport_size().width
14331 } else {
14332 620.0
14333 };
14334 operad::ScrollState::new(ScrollAxes::HORIZONTAL)
14335 .with_sizes(
14336 UiSize::new(viewport_width, TIMELINE_VIEWPORT_HEIGHT),
14337 UiSize::new(TIMELINE_CONTENT_WIDTH, TIMELINE_VIEWPORT_HEIGHT),
14338 )
14339 .with_offset(saved.offset())
14340}
14341
14342fn controls_list_viewport_height(viewport_height: f32) -> f32 {
14343 (viewport_height - 110.0).max(120.0)
14344}
14345
14346fn controls_scroll_state_for_view(
14347 saved: operad::ScrollState,
14348 viewport_height: f32,
14349) -> operad::ScrollState {
14350 let viewport_height = if saved.viewport_size().height > f32::EPSILON {
14351 saved.viewport_size().height
14352 } else {
14353 viewport_height
14354 };
14355 let content_height = if saved.content_size().height > f32::EPSILON {
14356 saved.content_size().height
14357 } else {
14358 controls_list_content_height()
14359 };
14360 scroll_state(saved.offset().y, viewport_height, content_height)
14361}
14362
14363fn controls_list_content_height() -> f32 {
14364 SHOWCASE_WIDGET_WINDOW_IDS.len() as f32 * CONTROLS_WIDGET_ROW_HEIGHT
14365 + (SHOWCASE_WIDGET_WINDOW_IDS.len().saturating_sub(1)) as f32 * CONTROLS_WIDGET_ROW_GAP
14366}
14367
14368fn caret_visible(phase: f32) -> bool {
14369 phase.sin() >= 0.0
14370}
14371
14372fn showcase_text_color(color: ColorRgba) -> ColorRgba {
14373 if active_showcase_theme_choice() == ShowcaseThemeChoice::Dark || color.a == 0 {
14374 return color;
14375 }
14376
14377 let max = color.r.max(color.g).max(color.b);
14378 let min = color.r.min(color.g).min(color.b);
14379 if max.saturating_sub(min) > 36 {
14380 return color;
14381 }
14382
14383 let brightness = (u16::from(color.r) + u16::from(color.g) + u16::from(color.b)) / 3;
14384 let mut mapped = if brightness >= 215 {
14385 active_showcase_colors().text
14386 } else if brightness >= 170 {
14387 active_showcase_colors().text_muted
14388 } else if brightness >= 110 {
14389 active_showcase_colors().text_subtle
14390 } else {
14391 return color;
14392 };
14393 mapped.a = color.a;
14394 mapped
14395}
14396
14397fn text(size: f32, color: ColorRgba) -> TextStyle {
14398 TextStyle {
14399 font_size: size,
14400 line_height: size + 5.0,
14401 color: showcase_text_color(color),
14402 ..Default::default()
14403 }
14404}
14405
14406fn themed_text(theme: &Theme, size: f32) -> TextStyle {
14407 text(size, theme.colors.text)
14408}
14409
14410fn themed_muted_text(theme: &Theme, size: f32) -> TextStyle {
14411 text(size, theme.colors.text_muted)
14412}
14413
14414fn color(r: u8, g: u8, b: u8) -> ColorRgba {
14415 ColorRgba::new(r, g, b, 255)
14416}examples/minimal_native.rs (line 18)
7fn minimal_document(viewport: UiSize) -> UiDocument {
8 let mut ui = UiDocument::new(root_style(viewport.width, viewport.height));
9 let panel = ui.add_child(
10 ui.root(),
11 UiNode::container(
12 "app.panel",
13 LayoutStyle::column()
14 .with_size(360.0, 120.0)
15 .with_padding(20.0)
16 .with_gap(8.0),
17 )
18 .with_visual(UiVisual::panel(ColorRgba::new(24, 29, 36, 255), None, 6.0)),
19 );
20 ui.add_child(
21 panel,
22 UiNode::text(
23 "app.title",
24 "Hello from Operad",
25 TextStyle {
26 font_size: 22.0,
27 line_height: 30.0,
28 color: ColorRgba::WHITE,
29 ..TextStyle::default()
30 },
31 LayoutStyle::size(320.0, 34.0),
32 ),
33 );
34 ui.add_child(
35 panel,
36 UiNode::text(
37 "app.subtitle",
38 "This app uses the default native runtime.",
39 TextStyle::default(),
40 LayoutStyle::size(320.0, 28.0),
41 ),
42 );
43 ui
44}examples/command_palette_hotkeys.rs (line 64)
53 fn view(&self, viewport: UiSize) -> UiDocument {
54 let mut ui = UiDocument::new(root_style(viewport.width, viewport.height));
55 let panel = ui.add_child(
56 ui.root(),
57 UiNode::container(
58 "commands.panel",
59 LayoutStyle::column()
60 .with_size(560.0, 360.0)
61 .with_padding(16.0)
62 .with_gap(10.0),
63 )
64 .with_visual(UiVisual::panel(ColorRgba::new(24, 29, 36, 255), None, 6.0)),
65 );
66 widgets::label(
67 &mut ui,
68 panel,
69 "commands.title",
70 "Command palette and shortcuts",
71 heading(),
72 LayoutStyle::new().with_width_percent(1.0).with_height(32.0),
73 );
74 let mut options =
75 ext_widgets::CommandPaletteOptions::default().with_action_prefix("commands");
76 options.width = 520.0;
77 options.max_visible_rows = 5;
78 ext_widgets::command_palette(
79 &mut ui,
80 panel,
81 "commands.palette",
82 &command_items(),
83 &self.palette,
84 None,
85 options,
86 );
87 widgets::label(
88 &mut ui,
89 panel,
90 "commands.last",
91 format!("Last command: {}", self.last_command),
92 muted(),
93 LayoutStyle::new().with_width_percent(1.0).with_height(28.0),
94 );
95 ui
96 }
97}
98
99fn command_items() -> Vec<CommandPaletteItem> {
100 let mut registry = CommandRegistry::new();
101 registry
102 .register(
103 CommandMeta::new("app.open", "Open project")
104 .description("Open an existing project")
105 .category("File"),
106 )
107 .expect("register command");
108 registry
109 .register(
110 CommandMeta::new("app.save", "Save project")
111 .description("Write current changes")
112 .category("File"),
113 )
114 .expect("register command");
115 registry
116 .register(
117 CommandMeta::new("app.toggle_sidebar", "Toggle sidebar")
118 .description("Show or hide the left navigation")
119 .category("View"),
120 )
121 .expect("register command");
122 registry
123 .bind_shortcut(CommandScope::Global, Shortcut::ctrl('o'), "app.open")
124 .expect("bind shortcut");
125 registry
126 .bind_shortcut(CommandScope::Global, Shortcut::ctrl('s'), "app.save")
127 .expect("bind shortcut");
128 registry
129 .bind_shortcut(
130 CommandScope::Global,
131 Shortcut::ctrl('b'),
132 "app.toggle_sidebar",
133 )
134 .expect("bind shortcut");
135 ext_widgets::command_palette::command_palette_items_from_registry(
136 ®istry,
137 &[CommandScope::Global],
138 &ShortcutFormatter::default(),
139 )
140}
141
142fn heading() -> TextStyle {
143 TextStyle {
144 font_size: 22.0,
145 line_height: 30.0,
146 color: ColorRgba::WHITE,
147 ..TextStyle::default()
148 }
149}
150
151fn muted() -> TextStyle {
152 TextStyle {
153 color: ColorRgba::new(166, 178, 196, 255),
154 ..TextStyle::default()
155 }
156}examples/canvas_app.rs (line 47)
35 fn view(&self, viewport: UiSize) -> UiDocument {
36 let mut ui = UiDocument::new(root_style(viewport.width, viewport.height));
37 let panel = ui.add_child(
38 ui.root(),
39 UiNode::container(
40 "canvas.app",
41 LayoutStyle::column()
42 .with_width_percent(1.0)
43 .with_height_percent(1.0)
44 .with_padding(16.0)
45 .with_gap(10.0),
46 )
47 .with_visual(UiVisual::panel(ColorRgba::new(13, 17, 23, 255), None, 0.0)),
48 );
49 widgets::label(
50 &mut ui,
51 panel,
52 "canvas.title",
53 "WGPU canvas",
54 heading(),
55 LayoutStyle::new().with_width_percent(1.0).with_height(32.0),
56 );
57 let mut options = widgets::CanvasOptions::default()
58 .with_accessibility_label("Animated shader canvas")
59 .with_aspect_ratio(16.0 / 9.0);
60 options.layout = LayoutStyle::new()
61 .with_width_percent(1.0)
62 .with_height(0.0)
63 .with_flex_grow(1.0);
64 options.visual = UiVisual::panel(
65 ColorRgba::new(18, 22, 28, 255),
66 Some(StrokeStyle::new(ColorRgba::new(58, 68, 84, 255), 1.0)),
67 4.0,
68 );
69 widgets::canvas(
70 &mut ui,
71 panel,
72 "canvas.preview",
73 CanvasContent::new("canvas.preview").program(shader(self.phase)),
74 options,
75 );
76 ui
77 }
78}
79
80fn shader(phase: f32) -> CanvasRenderProgram {
81 CanvasRenderProgram::wgsl(
82 r#"
83override PHASE: f32 = 0.0;
84
85struct VertexOutput {
86 @builtin(position) position: vec4<f32>,
87 @location(0) uv: vec2<f32>,
88};
89
90@vertex
91fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
92 let positions = array<vec2<f32>, 3>(
93 vec2<f32>(-1.0, -1.0),
94 vec2<f32>(3.0, -1.0),
95 vec2<f32>(-1.0, 3.0),
96 );
97 let position = positions[vertex_index];
98 var output: VertexOutput;
99 output.position = vec4<f32>(position, 0.0, 1.0);
100 output.uv = position * 0.5 + vec2<f32>(0.5, 0.5);
101 return output;
102}
103
104@fragment
105fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
106 let p = input.uv * 2.0 - vec2<f32>(1.0, 1.0);
107 let wave = 0.5 + 0.5 * sin((p.x + p.y + PHASE * 6.28318) * 5.0);
108 let glow = 1.0 / (1.0 + dot(p, p) * 3.0);
109 let color = vec3<f32>(0.10, 0.32, 0.72) * glow + vec3<f32>(0.22, 0.70, 0.56) * wave * 0.35;
110 return vec4<f32>(color, 1.0);
111}
112"#,
113 )
114 .label("template.canvas")
115 .constant("PHASE", phase as f64)
116 .clear_color(Some(ColorRgba::new(18, 22, 28, 255)))
117}examples/three_consumer_probe.rs (line 49)
32fn build_game_overlay() -> UiDocument {
33 let mut document = UiDocument::new(root_style(800.0, 600.0));
34 let hotbar = document.add_child(
35 document.root(),
36 UiNode::container(
37 "game.hotbar",
38 layout::clipped_node_style(layout::with_margin_bottom(
39 layout::with_auto_horizontal_margin(layout::with_size(
40 layout::centered_row(),
41 layout::px(360.0),
42 layout::px(64.0),
43 )),
44 18.0,
45 ))
46 .with_z_index(10),
47 )
48 .with_visual(UiVisual::panel(
49 ColorRgba::new(20, 24, 31, 230),
50 Some(StrokeStyle::new(ColorRgba::new(96, 113, 139, 255), 1.0)),
51 6.0,
52 )),
53 );
54
55 for slot in 0..8 {
56 document.add_child(
57 hotbar,
58 UiNode::container(
59 format!("game.hotbar.slot.{slot}"),
60 layout::node_style(layout::with_margin_all(layout::fixed(36.0, 36.0), 4.0)),
61 )
62 .with_input(InputBehavior::BUTTON)
63 .with_accessibility(
64 AccessibilityMeta::new(AccessibilityRole::Button)
65 .label(format!("Hotbar slot {}", slot + 1))
66 .focusable()
67 .action(AccessibilityAction::new("activate", "Activate")),
68 )
69 .with_visual(UiVisual::panel(
70 ColorRgba::new(40, 49, 62, 255),
71 Some(StrokeStyle::new(ColorRgba::new(105, 124, 153, 255), 1.0)),
72 4.0,
73 )),
74 );
75 }
76
77 document
78}
79
80fn build_tool_panel() -> UiDocument {
81 let mut document = UiDocument::new(root_style(800.0, 600.0));
82 let panel = document.add_child(
83 document.root(),
84 UiNode::container(
85 "tool.sidebar.modules",
86 layout::clipped_node_style(layout::with_size(
87 layout::column(),
88 layout::px(260.0),
89 layout::px(220.0),
90 )),
91 )
92 .with_scroll(ScrollAxes::VERTICAL)
93 .with_visual(UiVisual::panel(
94 ColorRgba::new(28, 33, 39, 255),
95 Some(StrokeStyle::new(ColorRgba::new(74, 85, 104, 255), 1.0)),
96 4.0,
97 )),
98 );
99
100 for row in 0..12 {
101 let label = format!("Layer module {}", row + 1);
102 document.add_child(
103 panel,
104 UiNode::text(
105 format!("tool.module.{row}"),
106 label.clone(),
107 TextStyle::default(),
108 layout::size(layout::percent(1.0), layout::px(32.0)),
109 )
110 .with_input(InputBehavior::BUTTON)
111 .with_accessibility(
112 AccessibilityMeta::new(AccessibilityRole::Button)
113 .label(label)
114 .focusable()
115 .action(AccessibilityAction::new("activate", "Activate")),
116 ),
117 );
118 }
119
120 document.handle_input(UiInputEvent::wheel(
121 UiPoint::new(12.0, 12.0),
122 UiPoint::new(0.0, 24.0),
123 ));
124
125 document
126}
127
128fn build_timeline_editor() -> UiDocument {
129 let mut document = UiDocument::new(root_style(800.0, 600.0));
130 let shell = document.add_child(
131 document.root(),
132 UiNode::container(
133 "timeline.shell",
134 layout::clipped_node_style(layout::with_size(
135 layout::column(),
136 layout::percent(1.0),
137 layout::percent(1.0),
138 )),
139 ),
140 );
141
142 document.add_child(
143 shell,
144 UiNode::text(
145 "timeline.transport",
146 "Transport",
147 TextStyle::default(),
148 layout::size(layout::percent(1.0), layout::px(32.0)),
149 )
150 .with_visual(UiVisual::panel(
151 ColorRgba::new(30, 36, 44, 255),
152 Some(StrokeStyle::new(ColorRgba::new(84, 96, 115, 255), 1.0)),
153 0.0,
154 )),
155 );
156 document.add_child(
157 shell,
158 UiNode::canvas(
159 "timeline.editor",
160 "timeline.editor.display_list_surface",
161 layout::size(layout::percent(1.0), layout::px(260.0)),
162 )
163 .with_accessibility(
164 AccessibilityMeta::new(AccessibilityRole::EditorSurface)
165 .label("Timeline editor")
166 .focusable(),
167 )
168 .with_visual(UiVisual::panel(
169 ColorRgba::new(16, 19, 23, 255),
170 Some(StrokeStyle::new(ColorRgba::new(64, 75, 92, 255), 1.0)),
171 0.0,
172 )),
173 );
174
175 document
176}Additional examples can be found in:
pub fn composite_over(self, background: Self) -> Self
pub fn relative_luminance(self) -> f32
pub fn contrast_ratio(self, other: Self) -> f32
pub fn meets_contrast_ratio(self, background: Self, minimum_ratio: f32) -> bool
pub fn highest_contrast_against(self, first: Self, second: Self) -> Self
Trait Implementations§
Source§impl From<ColorRgba> for PaintBrush
impl From<ColorRgba> for PaintBrush
impl Copy for ColorRgba
impl Eq for ColorRgba
impl StructuralPartialEq for ColorRgba
Auto Trait Implementations§
impl Freeze for ColorRgba
impl RefUnwindSafe for ColorRgba
impl Send for ColorRgba
impl Sync for ColorRgba
impl Unpin for ColorRgba
impl UnsafeUnpin for ColorRgba
impl UnwindSafe for ColorRgba
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.Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
Compare self to
key and return true if they are equal.