Skip to main content

PaintRect

Struct PaintRect 

Source
pub struct PaintRect {
    pub rect: UiRect,
    pub fill: PaintBrush,
    pub stroke: Option<AlignedStroke>,
    pub corner_radii: CornerRadii,
    pub effects: Vec<PaintEffect>,
}

Fields§

§rect: UiRect§fill: PaintBrush§stroke: Option<AlignedStroke>§corner_radii: CornerRadii§effects: Vec<PaintEffect>

Implementations§

Source§

impl PaintRect

Source

pub fn new(rect: UiRect, fill: impl Into<PaintBrush>) -> Self

Source

pub fn solid(rect: UiRect, fill: ColorRgba) -> Self

Examples found in repository?
examples/showcase.rs (line 7884)
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}
Source

pub fn stroke(self, stroke: impl Into<AlignedStroke>) -> Self

Examples found in repository?
examples/showcase.rs (lines 7885-7888)
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}
Source

pub const fn corner_radii(self, corner_radii: CornerRadii) -> Self

Examples found in repository?
examples/showcase.rs (line 7889)
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}
Source

pub fn effect(self, effect: PaintEffect) -> Self

Examples found in repository?
examples/showcase.rs (lines 12996-13001)
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}
Source

pub fn translated(self, offset: UiPoint) -> Self

Source

pub fn pixel_snapped(self, policy: PixelSnapPolicy) -> Self

Trait Implementations§

Source§

impl Clone for PaintRect

Source§

fn clone(&self) -> PaintRect

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 PaintRect

Source§

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

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

impl PartialEq for PaintRect

Source§

fn eq(&self, other: &PaintRect) -> 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 StructuralPartialEq for PaintRect

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<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,