Skip to main content

ColorRgba

Struct ColorRgba 

Source
pub struct ColorRgba {
    pub r: u8,
    pub g: u8,
    pub b: u8,
    pub a: u8,
}

Fields§

§r: u8§g: u8§b: u8§a: u8

Implementations§

Source§

impl ColorRgba

Source

pub const WHITE: Self

Source

pub const BLACK: Self

Source

pub const TRANSPARENT: Self

Source

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
Hide additional 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, &registry);
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        &registry,
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}
Source

pub fn composite_over(self, background: Self) -> Self

Source

pub fn relative_luminance(self) -> f32

Source

pub fn contrast_ratio(self, other: Self) -> f32

Source

pub fn meets_contrast_ratio(self, background: Self, minimum_ratio: f32) -> bool

Source

pub fn highest_contrast_against(self, first: Self, second: Self) -> Self

Trait Implementations§

Source§

impl Clone for ColorRgba

Source§

fn clone(&self) -> ColorRgba

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ColorRgba

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl From<ColorRgba> for PaintBrush

Source§

fn from(color: ColorRgba) -> Self

Converts to this type from the input type.
Source§

impl PartialEq for ColorRgba

Source§

fn eq(&self, other: &ColorRgba) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl Copy for ColorRgba

Source§

impl Eq for ColorRgba

Source§

impl StructuralPartialEq for ColorRgba

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast<T> for T

Source§

fn downcast(&self) -> &T

Source§

impl<T> Downcast for T
where T: Any,

Source§

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>

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)

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)

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
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

fn upcast(&self) -> Option<&T>

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

impl<T> WasmNotSend for T
where T: Send,

Source§

impl<T> WasmNotSendSync for T

Source§

impl<T> WasmNotSync for T
where T: Sync,