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 (lines 5180-5188)
5178fn animation_panel_primitives(offset: UiPoint) -> Vec<ScenePrimitive> {
5179    vec![ScenePrimitive::Rect(
5180        PaintRect::solid(
5181            UiRect::new(
5182                offset.x,
5183                offset.y,
5184                ANIMATION_PANEL_WIDTH,
5185                ANIMATION_PANEL_HEIGHT,
5186            ),
5187            color(232, 186, 88),
5188        )
5189        .stroke(AlignedStroke::inside(StrokeStyle::new(
5190            color(255, 226, 154),
5191            1.0,
5192        )))
5193        .corner_radii(CornerRadii::uniform(6.0)),
5194    )]
5195}
5196
5197fn list_and_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5198    let body = section(ui, parent, "lists_tables", "Lists and tables");
5199
5200    let scroll_shell = row(ui, body, "lists_tables.scroll_area.shell", 8.0);
5201    let nested_scroll = widgets::scroll_area(
5202        ui,
5203        scroll_shell,
5204        "lists_tables.scroll_area",
5205        ScrollAxes::VERTICAL,
5206        LayoutStyle::column()
5207            .with_width(0.0)
5208            .with_flex_grow(1.0)
5209            .with_height(92.0),
5210    );
5211    ui.node_mut(nested_scroll)
5212        .set_action("lists_tables.scroll_area.scroll");
5213    if let Some(scroll) = ui.node_mut(nested_scroll).scroll_mut() {
5214        scroll.set_offset(UiPoint::new(0.0, state.list_scroll));
5215    }
5216    for index in 0..6 {
5217        widgets::label(
5218            ui,
5219            nested_scroll,
5220            format!("lists_tables.scroll_area.row.{index}"),
5221            format!("Scroll row {}", index + 1),
5222            text(12.0, color(200, 212, 228)),
5223            LayoutStyle::new()
5224                .with_width_percent(1.0)
5225                .with_height(26.0)
5226                .with_flex_shrink(0.0),
5227        );
5228    }
5229    scrollbar_widgets::scrollbar(
5230        ui,
5231        scroll_shell,
5232        "lists_tables.scroll_area.scrollbar",
5233        scroll_state(state.list_scroll, 92.0, 6.0 * 26.0),
5234        scrollbar_widgets::ScrollAxis::Vertical,
5235        scrollbar_widgets::ScrollbarOptions::default()
5236            .with_layout(LayoutStyle::size(8.0, 92.0))
5237            .with_track_size(UiSize::new(8.0, 92.0))
5238            .with_action("lists_tables.scroll_area.scrollbar"),
5239    );
5240
5241    widgets::table_header(ui, body, "lists_tables.table_header", &table_columns());
5242
5243    let virtual_shell = row(ui, body, "lists_tables.virtual_list.shell", 8.0);
5244    let virtual_list = widgets::virtual_list(
5245        ui,
5246        virtual_shell,
5247        "lists_tables.virtual_list",
5248        widgets::VirtualListSpec {
5249            row_count: 24,
5250            row_height: 28.0,
5251            viewport_height: 112.0,
5252            scroll_offset: state.virtual_scroll,
5253            overscan: 1,
5254        },
5255        |ui, row_parent, row| {
5256            widgets::label(
5257                ui,
5258                row_parent,
5259                format!("lists_tables.virtual_list.row.{row}"),
5260                format!("Virtual row {}", row + 1),
5261                text(12.0, color(214, 224, 238)),
5262                LayoutStyle::new()
5263                    .with_width_percent(1.0)
5264                    .with_height(28.0)
5265                    .with_flex_shrink(0.0),
5266            );
5267        },
5268    );
5269    ui.node_mut(virtual_list)
5270        .set_action("lists_tables.virtual_list.scroll");
5271    scrollbar_widgets::scrollbar(
5272        ui,
5273        virtual_shell,
5274        "lists_tables.virtual_list.scrollbar",
5275        scroll_state(state.virtual_scroll, 112.0, 24.0 * 28.0),
5276        scrollbar_widgets::ScrollAxis::Vertical,
5277        scrollbar_widgets::ScrollbarOptions::default()
5278            .with_layout(LayoutStyle::size(8.0, 112.0))
5279            .with_track_size(UiSize::new(8.0, 112.0))
5280            .with_action("lists_tables.virtual_list.scrollbar"),
5281    );
5282
5283    let table_shell = row(ui, body, "lists_tables.data_table.shell", 8.0);
5284    let table_scroll = widgets::scroll_area(
5285        ui,
5286        table_shell,
5287        "lists_tables.data_table",
5288        ScrollAxes::VERTICAL,
5289        LayoutStyle::column()
5290            .with_width(0.0)
5291            .with_flex_grow(1.0)
5292            .with_height(128.0),
5293    );
5294    ui.node_mut(table_scroll)
5295        .set_action("lists_tables.data_table.scroll");
5296    if let Some(scroll) = ui.node_mut(table_scroll).scroll_mut() {
5297        scroll.set_offset(UiPoint::new(0.0, state.table_scroll));
5298    }
5299    for row_index in 0..16 {
5300        data_table_row(ui, table_scroll, row_index, state);
5301    }
5302    scrollbar_widgets::scrollbar(
5303        ui,
5304        table_shell,
5305        "lists_tables.data_table.scrollbar",
5306        scroll_state(state.table_scroll, 128.0, 16.0 * 28.0),
5307        scrollbar_widgets::ScrollAxis::Vertical,
5308        scrollbar_widgets::ScrollbarOptions::default()
5309            .with_layout(LayoutStyle::size(8.0, 128.0))
5310            .with_track_size(UiSize::new(8.0, 128.0))
5311            .with_action("lists_tables.data_table.scrollbar"),
5312    );
5313
5314    let virtual_controls = wrapping_row(ui, body, "lists_tables.virtualized_table.controls", 8.0);
5315    button(
5316        ui,
5317        virtual_controls,
5318        "lists_tables.virtualized_table.sort.name",
5319        if state.virtual_table_descending {
5320            "Name desc"
5321        } else {
5322            "Name asc"
5323        },
5324        "lists_tables.virtualized_table.sort.name",
5325        button_visual(38, 52, 70),
5326    );
5327    button(
5328        ui,
5329        virtual_controls,
5330        "lists_tables.virtualized_table.filter.status",
5331        if state.virtual_table_ready_only {
5332            "Ready only"
5333        } else {
5334            "All status"
5335        },
5336        "lists_tables.virtualized_table.filter.status",
5337        button_visual(38, 52, 70),
5338    );
5339    button(
5340        ui,
5341        virtual_controls,
5342        "lists_tables.virtualized_table.resize.reset",
5343        "Reset width",
5344        "lists_tables.virtualized_table.resize.reset",
5345        button_visual(38, 52, 70),
5346    );
5347
5348    let columns = virtual_table_columns(state);
5349    let visible_rows = virtual_table_visible_rows(state);
5350    let mut table_options = ext_widgets::DataTableOptions::default()
5351        .with_row_action_prefix("lists_tables.virtualized_table")
5352        .with_cell_action_prefix("lists_tables.virtualized_table")
5353        .with_scroll_action("lists_tables.virtualized_table.scroll");
5354    table_options.layout = LayoutStyle::column()
5355        .with_width(0.0)
5356        .with_flex_grow(1.0)
5357        .with_flex_shrink(1.0);
5358    table_options.selection = state.table_selection.clone();
5359    let virtual_shell = row(ui, body, "lists_tables.virtualized_table.shell", 8.0);
5360    ext_widgets::virtualized_data_table(
5361        ui,
5362        virtual_shell,
5363        "lists_tables.virtualized_table",
5364        &columns,
5365        ext_widgets::VirtualDataTableSpec {
5366            row_count: visible_rows.len(),
5367            row_height: 28.0,
5368            viewport_width: 420.0,
5369            viewport_height: 128.0,
5370            scroll_offset: UiPoint::new(0.0, state.virtual_table_scroll),
5371            overscan_rows: 1,
5372        },
5373        table_options,
5374        |ui, cell_parent, cell| {
5375            let source_row = visible_rows.get(cell.row).copied().unwrap_or(cell.row);
5376            let value = virtual_table_cell_value(source_row, cell.column);
5377            widgets::label(
5378                ui,
5379                cell_parent,
5380                format!(
5381                    "lists_tables.virtualized_table.cell.{}.{}.label",
5382                    cell.row, cell.column
5383                ),
5384                value,
5385                text(12.0, color(220, 228, 238)),
5386                LayoutStyle::new().with_width_percent(1.0),
5387            );
5388        },
5389    );
5390    scrollbar_widgets::scrollbar(
5391        ui,
5392        virtual_shell,
5393        "lists_tables.virtualized_table.scrollbar",
5394        scroll_state(
5395            state.virtual_table_scroll,
5396            128.0,
5397            visible_rows.len() as f32 * 28.0,
5398        ),
5399        scrollbar_widgets::ScrollAxis::Vertical,
5400        scrollbar_widgets::ScrollbarOptions::default()
5401            .with_layout(LayoutStyle::size(8.0, 158.0))
5402            .with_track_size(UiSize::new(8.0, 158.0))
5403            .with_action("lists_tables.virtualized_table.scrollbar"),
5404    );
5405}
5406
5407fn data_table_row(ui: &mut UiDocument, parent: UiNodeId, row_index: usize, state: &ShowcaseState) {
5408    let selected = state.table_selection.contains_row(row_index);
5409    let row = ui.add_child(
5410        parent,
5411        UiNode::container(
5412            format!("lists_tables.data_table.row.{row_index}"),
5413            LayoutStyle::row()
5414                .with_width_percent(1.0)
5415                .with_height(28.0)
5416                .with_flex_shrink(0.0),
5417        )
5418        .with_input(operad::InputBehavior::BUTTON)
5419        .with_action(format!("lists_tables.data_table.row.{row_index}"))
5420        .with_visual(if selected {
5421            UiVisual::panel(color(45, 73, 109), None, 0.0)
5422        } else {
5423            UiVisual::TRANSPARENT
5424        }),
5425    );
5426    let values = [
5427        format!("Item {}", row_index + 1),
5428        if row_index % 2 == 0 {
5429            "Ready".to_string()
5430        } else {
5431            "Pending".to_string()
5432        },
5433        format!("{}%", 40 + row_index * 3),
5434    ];
5435    let widths = [0.42, 0.33, 0.25];
5436    for (column, value) in values.into_iter().enumerate() {
5437        let cell = ui.add_child(
5438            row,
5439            UiNode::container(
5440                format!("lists_tables.data_table.cell.{row_index}.{column}"),
5441                LayoutStyle::new()
5442                    .with_width_percent(widths[column])
5443                    .with_height_percent(1.0)
5444                    .padding(6.0),
5445            )
5446            .with_input(operad::InputBehavior::BUTTON)
5447            .with_action(format!("lists_tables.data_table.cell.{row_index}.{column}")),
5448        );
5449        widgets::label(
5450            ui,
5451            cell,
5452            format!("lists_tables.data_table.cell.{row_index}.{column}.label"),
5453            value,
5454            text(12.0, color(222, 230, 240)),
5455            LayoutStyle::new().with_width_percent(1.0),
5456        );
5457    }
5458}
5459
5460#[allow(clippy::field_reassign_with_default)]
5461fn property_inspector(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5462    let body = section(ui, parent, "property_inspector", "Property inspector");
5463    widgets::label(
5464        ui,
5465        body,
5466        "property_inspector.target",
5467        "Inspecting: Styling preview",
5468        text(12.0, color(196, 210, 230)),
5469        LayoutStyle::new().with_width_percent(1.0),
5470    );
5471    let mut options = ext_widgets::PropertyInspectorOptions::default();
5472    options.selected_index = Some(0);
5473    options.label_width = 120.0;
5474    options.row_height = 30.0;
5475    ext_widgets::property_inspector_grid(
5476        ui,
5477        body,
5478        "property_inspector.grid",
5479        &[
5480            ext_widgets::PropertyGridRow::new("target", "Widget", "Button preview").read_only(),
5481            ext_widgets::PropertyGridRow::new(
5482                "inner",
5483                "Inner margin",
5484                format!("{:.0}px", state.styling.inner_margin),
5485            )
5486            .with_kind(ext_widgets::PropertyValueKind::Number),
5487            ext_widgets::PropertyGridRow::new(
5488                "outer",
5489                "Outer margin",
5490                format!("{:.0}px", state.styling.outer_margin),
5491            )
5492            .with_kind(ext_widgets::PropertyValueKind::Number),
5493            ext_widgets::PropertyGridRow::new(
5494                "radius",
5495                "Corner radius",
5496                format!("{:.0}px", state.styling.corner_radius),
5497            )
5498            .with_kind(ext_widgets::PropertyValueKind::Number),
5499            ext_widgets::PropertyGridRow::new(
5500                "stroke",
5501                "Stroke",
5502                format!("{:.1}px", state.styling.stroke_width),
5503            )
5504            .with_kind(ext_widgets::PropertyValueKind::Number)
5505            .changed(),
5506            ext_widgets::PropertyGridRow::new("state", "Source", "Styling widget").read_only(),
5507        ],
5508        options,
5509    );
5510}
5511
5512fn diagnostics_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5513    let body = section(ui, parent, "diagnostics", "Diagnostics");
5514
5515    widgets::label(
5516        ui,
5517        body,
5518        "diagnostics.layout.title",
5519        "Layout and animation inspector",
5520        text(14.0, color(222, 230, 240)),
5521        LayoutStyle::new().with_width_percent(1.0),
5522    );
5523    let debug_snapshot = &state.diagnostics_snapshot;
5524    ext_widgets::debug_inspector_panel(
5525        ui,
5526        body,
5527        "diagnostics.inspector",
5528        debug_snapshot,
5529        ext_widgets::DebugInspectorPanelOptions {
5530            selected_node: Some("diagnostics.sample.preview".to_owned()),
5531            label_width: 104.0,
5532            max_layout_rows: 5,
5533            max_animation_rows: 1,
5534            show_animation: false,
5535            ..Default::default()
5536        },
5537    );
5538    ext_widgets::animation_state_graph_panel(
5539        ui,
5540        body,
5541        "diagnostics.animation.graph",
5542        debug_snapshot.animation("diagnostics.sample.preview"),
5543        ext_widgets::AnimationStateGraphPanelOptions {
5544            state_width: 72.0,
5545            state_height: 28.0,
5546            edge_row_height: 22.0,
5547            max_edges: 2,
5548            action_prefix: Some("diagnostics.animation.graph".to_owned()),
5549            ..Default::default()
5550        },
5551    );
5552    ext_widgets::animation_inspector_controls_panel(
5553        ui,
5554        body,
5555        "diagnostics.animation.controls",
5556        debug_snapshot.animation("diagnostics.sample.preview"),
5557        ext_widgets::AnimationInspectorControlsOptions {
5558            max_inputs: 3,
5559            paused: state.diagnostics_animation_paused,
5560            scrub_progress: Some(state.diagnostics_animation_scrub),
5561            action_prefix: Some("diagnostics.animation.controls".to_owned()),
5562            ..Default::default()
5563        },
5564    );
5565    widgets::label(
5566        ui,
5567        body,
5568        "diagnostics.animation.controls.status",
5569        format!(
5570            "scrub {:.0}%  hover {:.0}%  pulses {}",
5571            state.diagnostics_animation_scrub * 100.0,
5572            state.diagnostics_animation_hover * 100.0,
5573            state.diagnostics_animation_pulse_count
5574        ),
5575        text(12.0, color(166, 180, 198)),
5576        LayoutStyle::new().with_width_percent(1.0),
5577    );
5578
5579    widgets::label(
5580        ui,
5581        body,
5582        "diagnostics.a11y.title",
5583        "Accessibility overlay",
5584        text(14.0, color(222, 230, 240)),
5585        LayoutStyle::new().with_width_percent(1.0),
5586    );
5587    let mut overlay_preview_style = UiNodeStyle::from(
5588        LayoutStyle::new()
5589            .with_width(320.0)
5590            .with_height(140.0)
5591            .with_flex_shrink(0.0),
5592    );
5593    overlay_preview_style.set_clip(ClipBehavior::Clip);
5594    let overlay_preview = ui.add_child(
5595        body,
5596        UiNode::container("diagnostics.a11y.preview", overlay_preview_style).with_visual(
5597            UiVisual::panel(
5598                color(12, 17, 24),
5599                Some(StrokeStyle::new(color(47, 62, 82), 1.0)),
5600                4.0,
5601            ),
5602        ),
5603    );
5604    let mut overlay_options = ext_widgets::AccessibilityDebugOverlayOptions {
5605        action_prefix: Some("diagnostics.a11y.visual".to_owned()),
5606        ..Default::default()
5607    };
5608    overlay_options.show_labels = false;
5609    ext_widgets::accessibility_debug_overlay(
5610        ui,
5611        overlay_preview,
5612        "diagnostics.a11y.visual",
5613        &debug_snapshot,
5614        overlay_options,
5615    );
5616    ext_widgets::accessibility_overlay_panel(
5617        ui,
5618        body,
5619        "diagnostics.a11y",
5620        &debug_snapshot,
5621        ext_widgets::AccessibilityOverlayPanelOptions {
5622            label_width: 118.0,
5623            max_rows: 1,
5624            action_prefix: Some("diagnostics.a11y".to_owned()),
5625            ..Default::default()
5626        },
5627    );
5628
5629    let diagnostic_columns = ui.add_child(
5630        body,
5631        UiNode::container(
5632            "diagnostics.columns",
5633            LayoutStyle::column()
5634                .with_width_percent(1.0)
5635                .with_flex_shrink(0.0)
5636                .gap(10.0),
5637        ),
5638    );
5639    let command_column = ui.add_child(
5640        diagnostic_columns,
5641        UiNode::container(
5642            "diagnostics.commands.column",
5643            LayoutStyle::column()
5644                .with_width_percent(1.0)
5645                .with_flex_shrink(0.0)
5646                .gap(8.0),
5647        ),
5648    );
5649    let theme_column = ui.add_child(
5650        diagnostic_columns,
5651        UiNode::container(
5652            "diagnostics.theme.column",
5653            LayoutStyle::column()
5654                .with_width_percent(1.0)
5655                .with_flex_shrink(0.0)
5656                .gap(8.0),
5657        ),
5658    );
5659
5660    widgets::label(
5661        ui,
5662        command_column,
5663        "diagnostics.commands.title",
5664        "Command registry",
5665        text(14.0, color(222, 230, 240)),
5666        LayoutStyle::new().with_width_percent(1.0),
5667    );
5668    let registry = diagnostics_command_registry();
5669    ext_widgets::command_diagnostics_panel(
5670        ui,
5671        command_column,
5672        "diagnostics.commands",
5673        &registry,
5674        &[CommandScope::Global, CommandScope::Panel],
5675        &ShortcutFormatter::default(),
5676        ext_widgets::CommandDiagnosticsPanelOptions {
5677            label_width: 92.0,
5678            max_command_rows: 3,
5679            max_conflict_rows: 1,
5680            action_prefix: Some("diagnostics.commands".to_owned()),
5681            ..Default::default()
5682        },
5683    );
5684
5685    widgets::label(
5686        ui,
5687        theme_column,
5688        "diagnostics.theme.title",
5689        "Theme editor",
5690        text(14.0, color(222, 230, 240)),
5691        LayoutStyle::new().with_width_percent(1.0),
5692    );
5693    let theme_snapshot = DebugThemeSnapshot::from_theme(&Theme::dark());
5694    ext_widgets::theme_editor_panel(
5695        ui,
5696        theme_column,
5697        "diagnostics.theme",
5698        &theme_snapshot,
5699        ext_widgets::ThemeEditorPanelOptions {
5700            label_width: 92.0,
5701            max_token_rows: 1,
5702            max_component_rows: 1,
5703            action_prefix: Some("diagnostics.theme".to_owned()),
5704            ..Default::default()
5705        },
5706    );
5707}
5708
5709fn diagnostics_sample_snapshot(state: &ShowcaseState) -> DebugInspectorSnapshot {
5710    diagnostics_sample_snapshot_for(
5711        state.diagnostics_animation_hover,
5712        state.diagnostics_animation_active,
5713    )
5714}
5715
5716fn diagnostics_sample_snapshot_for(hover: f32, active: bool) -> DebugInspectorSnapshot {
5717    let mut sample = UiDocument::new(root_style(320.0, 180.0));
5718    let card = sample.add_child(
5719        sample.root(),
5720        UiNode::container(
5721            "diagnostics.sample.card",
5722            LayoutStyle::column()
5723                .with_width_percent(1.0)
5724                .with_height(120.0)
5725                .padding(12.0)
5726                .gap(8.0),
5727        )
5728        .with_visual(UiVisual::panel(
5729            color(16, 22, 30),
5730            Some(StrokeStyle::new(color(62, 77, 98), 1.0)),
5731            6.0,
5732        ))
5733        .with_accessibility(
5734            AccessibilityMeta::new(AccessibilityRole::Group).label("Diagnostics sample"),
5735        ),
5736    );
5737    sample.add_child(
5738        card,
5739        UiNode::container(
5740            "diagnostics.sample.preview",
5741            LayoutStyle::new().with_width(160.0).with_height(38.0),
5742        )
5743        .with_input(InputBehavior::BUTTON)
5744        .with_visual(UiVisual::panel(
5745            color(52, 112, 180),
5746            Some(StrokeStyle::new(color(116, 183, 255), 1.0)),
5747            5.0,
5748        ))
5749        .with_accessibility(
5750            AccessibilityMeta::new(AccessibilityRole::Button)
5751                .label("Preview action")
5752                .focusable(),
5753        )
5754        .with_animation(
5755            AnimationMachine::new(
5756                vec![
5757                    AnimationState::new(
5758                        "idle",
5759                        AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0),
5760                    ),
5761                    AnimationState::new(
5762                        "hot",
5763                        AnimatedValues::new(0.92, UiPoint::new(18.0, 0.0), 1.08),
5764                    ),
5765                ],
5766                vec![AnimationTransition::when(
5767                    "idle",
5768                    "hot",
5769                    AnimationCondition::bool("active", true),
5770                    0.18,
5771                )],
5772                "idle",
5773            )
5774            .expect("sample animation")
5775            .with_number_input("hover", hover)
5776            .with_blend_binding(AnimationBlendBinding::new("hover", "idle", "hot"))
5777            .with_bool_input("active", active)
5778            .with_trigger_input("pulse"),
5779        ),
5780    );
5781    widgets::label(
5782        &mut sample,
5783        card,
5784        "diagnostics.sample.label",
5785        "Sample node",
5786        text(12.0, color(198, 210, 226)),
5787        LayoutStyle::new().with_width_percent(1.0),
5788    );
5789    sample
5790        .compute_layout(UiSize::new(320.0, 180.0), &mut ApproxTextMeasurer)
5791        .expect("sample layout");
5792    DebugInspectorSnapshot::from_document(&sample, &mut ApproxTextMeasurer)
5793}
5794
5795fn diagnostics_command_registry() -> CommandRegistry {
5796    let mut registry = CommandRegistry::new();
5797    registry
5798        .register(
5799            CommandMeta::new("diagnostics.palette", "Open command palette")
5800                .description("Show command search")
5801                .category("Debug"),
5802        )
5803        .expect("command");
5804    registry
5805        .register(
5806            CommandMeta::new("diagnostics.inspect", "Inspect selected node")
5807                .description("Focus the layout inspector")
5808                .category("Debug"),
5809        )
5810        .expect("command");
5811    registry
5812        .register(
5813            CommandMeta::new("diagnostics.record", "Start interaction recording")
5814                .description("Capture replay steps")
5815                .category("Testing"),
5816        )
5817        .expect("command");
5818    registry
5819        .register(CommandMeta::new(
5820            "diagnostics.export_theme",
5821            "Export theme patch",
5822        ))
5823        .expect("command");
5824    registry
5825        .bind_shortcut(
5826            CommandScope::Global,
5827            Shortcut::ctrl('k'),
5828            "diagnostics.palette",
5829        )
5830        .expect("shortcut");
5831    registry
5832        .bind_shortcut(
5833            CommandScope::Panel,
5834            Shortcut::ctrl('i'),
5835            "diagnostics.inspect",
5836        )
5837        .expect("shortcut");
5838    registry
5839        .bind_shortcut(
5840            CommandScope::Panel,
5841            Shortcut::ctrl('r'),
5842            "diagnostics.record",
5843        )
5844        .expect("shortcut");
5845    registry
5846        .disable("diagnostics.export_theme", "No changes to export")
5847        .expect("disable");
5848    registry
5849}
5850
5851fn tree_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5852    let body = section(ui, parent, "trees", "Tree view");
5853    ext_widgets::tree_view(
5854        ui,
5855        body,
5856        "trees.tree_view",
5857        &tree_items(),
5858        &state.tree,
5859        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.tree"),
5860    );
5861    ext_widgets::outliner(
5862        ui,
5863        body,
5864        "trees.outliner",
5865        &tree_items(),
5866        &state.outliner,
5867        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.outliner"),
5868    );
5869    let virtual_state = ext_widgets::TreeViewState::expanded(["root"]);
5870    let virtual_nodes = ext_widgets::virtualized_tree_view(
5871        ui,
5872        body,
5873        "trees.virtual",
5874        &virtual_tree_items(),
5875        &virtual_state,
5876        ext_widgets::VirtualTreeViewSpec::new(24.0, 112.0)
5877            .scroll_offset(state.tree_virtual_scroll)
5878            .overscan_rows(1),
5879        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.virtual"),
5880    );
5881    ui.node_mut(virtual_nodes.body)
5882        .set_action("trees.virtual.scroll");
5883    tree_table_widgets(ui, body, state);
5884}
5885
5886fn tree_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5887    let tree_state = ext_widgets::TreeViewState::expanded(["root", "branch-a"]);
5888    let rows = tree_state.visible_items(&tree_table_items());
5889    let columns = [
5890        ext_widgets::DataTableColumn::new("name", "Name", 220.0),
5891        ext_widgets::DataTableColumn::new("kind", "Kind", 84.0),
5892        ext_widgets::DataTableColumn::new("status", "Status", 92.0),
5893    ];
5894    let mut options = ext_widgets::DataTableOptions::default()
5895        .with_row_action_prefix("trees.table")
5896        .with_cell_action_prefix("trees.table");
5897    options.layout = LayoutStyle::column()
5898        .with_width_percent(1.0)
5899        .with_height(132.0)
5900        .with_flex_shrink(0.0);
5901    ext_widgets::virtualized_data_table(
5902        ui,
5903        parent,
5904        "trees.table",
5905        &columns,
5906        ext_widgets::VirtualDataTableSpec {
5907            row_count: rows.len(),
5908            row_height: 24.0,
5909            viewport_width: 396.0,
5910            viewport_height: 96.0,
5911            scroll_offset: UiPoint::new(0.0, state.tree_virtual_scroll),
5912            overscan_rows: 1,
5913        },
5914        options,
5915        |ui, cell_parent, cell| {
5916            let value = rows
5917                .get(cell.row)
5918                .map(|item| tree_table_cell_value(item, cell.column))
5919                .unwrap_or_default();
5920            widgets::label(
5921                ui,
5922                cell_parent,
5923                format!("trees.table.cell.{}.{}.label", cell.row, cell.column),
5924                value,
5925                text(12.0, color(220, 228, 238)),
5926                LayoutStyle::new().with_width_percent(1.0),
5927            );
5928        },
5929    );
5930}
5931
5932fn tree_table_cell_value(item: &ext_widgets::TreeVisibleItem, column: usize) -> String {
5933    match column {
5934        0 => format!("{}{}", "  ".repeat(item.depth), item.label),
5935        1 => {
5936            if item.has_children() {
5937                "Folder".to_owned()
5938            } else {
5939                "File".to_owned()
5940            }
5941        }
5942        _ => {
5943            if item.disabled {
5944                "Locked".to_owned()
5945            } else if item.expanded {
5946                "Expanded".to_owned()
5947            } else {
5948                "Ready".to_owned()
5949            }
5950        }
5951    }
5952}
5953
5954fn tab_split_dock_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5955    let body = section_with_min_viewport(
5956        ui,
5957        parent,
5958        "layout_widgets",
5959        "Dock workspace",
5960        UiSize::new(546.0, 360.0),
5961    );
5962    let shell = ui.add_child(
5963        body,
5964        UiNode::container(
5965            "layout_widgets.dock_shell",
5966            LayoutStyle::column()
5967                .with_width_percent(1.0)
5968                .with_height(360.0)
5969                .with_flex_shrink(0.0),
5970        )
5971        .with_visual(UiVisual::panel(
5972            color(13, 17, 23),
5973            Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
5974            0.0,
5975        )),
5976    );
5977
5978    let mut panels = base_layout_dock_panels();
5979    state.layout_dock.apply_order_to_panels(&mut panels);
5980    state.layout_dock.apply_visibility_to_panels(&mut panels);
5981
5982    let mut drawer_options = ext_widgets::DockDrawerRailOptions::default();
5983    drawer_options.layout = LayoutStyle::row()
5984        .with_width_percent(1.0)
5985        .with_height(34.0)
5986        .with_padding(4.0)
5987        .with_gap(4.0);
5988    ext_widgets::dock_drawer_rail(
5989        ui,
5990        shell,
5991        "layout_widgets.dock.drawers",
5992        &[
5993            ext_widgets::DockDrawerDescriptor::new(
5994                "inspector",
5995                "Inspector",
5996                "inspector",
5997                ext_widgets::DockSide::Left,
5998            )
5999            .open(!state.layout_dock.is_hidden("inspector"))
6000            .with_action("layout_widgets.drawer.inspector"),
6001            ext_widgets::DockDrawerDescriptor::new(
6002                "assets",
6003                "Assets",
6004                "assets",
6005                ext_widgets::DockSide::Right,
6006            )
6007            .open(!state.layout_dock.is_hidden("assets"))
6008            .with_action("layout_widgets.drawer.assets"),
6009        ],
6010        drawer_options,
6011    );
6012
6013    let mut options = ext_widgets::DockWorkspaceOptions::default();
6014    options.layout = LayoutStyle::column()
6015        .with_width_percent(1.0)
6016        .with_height(0.0)
6017        .with_flex_grow(1.0);
6018    options.show_titles = false;
6019    options.panel_visual = UiVisual::panel(
6020        color(18, 22, 29),
6021        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
6022        0.0,
6023    );
6024    options.center_visual = UiVisual::panel(
6025        color(15, 19, 25),
6026        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
6027        0.0,
6028    );
6029
6030    ext_widgets::dock_workspace(
6031        ui,
6032        shell,
6033        "layout_widgets.dock",
6034        &panels,
6035        options,
6036        |ui, parent, panel| match panel.id.as_str() {
6037            "inspector" => egui_panel_contents(
6038                ui,
6039                parent,
6040                "layout.inspector",
6041                "Inspector",
6042                state.layout_inspector_scroll,
6043            ),
6044            "assets" => egui_panel_contents(
6045                ui,
6046                parent,
6047                "layout.assets",
6048                "Assets",
6049                state.layout_assets_scroll,
6050            ),
6051            _ => dock_document_panel(ui, parent, state),
6052        },
6053    );
6054
6055    if let Some(floating) = state.layout_dock.floating_panel("inspector") {
6056        let floating_panel = ui.add_child(
6057            shell,
6058            UiNode::container(
6059                "layout_widgets.floating.inspector",
6060                operad::layout::absolute(
6061                    floating.rect.x,
6062                    floating.rect.y,
6063                    floating.rect.width,
6064                    floating.rect.height,
6065                ),
6066            )
6067            .with_visual(UiVisual::panel(
6068                color(18, 22, 29),
6069                Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
6070                4.0,
6071            )),
6072        );
6073        egui_panel_contents(
6074            ui,
6075            floating_panel,
6076            "layout.inspector_floating",
6077            "Inspector",
6078            state.layout_inspector_scroll,
6079        );
6080    }
6081}
6082
6083fn base_layout_dock_panels() -> Vec<ext_widgets::DockPanelDescriptor> {
6084    vec![
6085        ext_widgets::DockPanelDescriptor::new(
6086            "inspector",
6087            "Inspector",
6088            ext_widgets::DockSide::Left,
6089            120.0,
6090        )
6091        .with_min_size(104.0)
6092        .resizable(true),
6093        ext_widgets::DockPanelDescriptor::center("document", "Document"),
6094        ext_widgets::DockPanelDescriptor::new(
6095            "assets",
6096            "Assets",
6097            ext_widgets::DockSide::Right,
6098            104.0,
6099        )
6100        .with_min_size(94.0)
6101        .resizable(true),
6102    ]
6103}
6104
6105fn dock_document_panel(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6106    let content = ui.add_child(
6107        parent,
6108        UiNode::container(
6109            "layout_widgets.document.content",
6110            LayoutStyle::column()
6111                .with_width_percent(1.0)
6112                .with_height_percent(1.0)
6113                .with_padding(8.0)
6114                .with_gap(8.0),
6115        ),
6116    );
6117
6118    let controls = wrapping_row(ui, content, "layout_widgets.dock.controls", 8.0);
6119    let (action, label) = if state.layout_dock.is_floating("inspector") {
6120        ("layout_widgets.dock_inspector", "Dock inspector")
6121    } else {
6122        ("layout_widgets.float_inspector", "Float inspector")
6123    };
6124    let mut float_button = widgets::ButtonOptions::new(
6125        LayoutStyle::new()
6126            .with_width(132.0)
6127            .with_height(28.0)
6128            .with_flex_shrink(0.0),
6129    )
6130    .with_action(action);
6131    float_button.visual = button_visual(40, 52, 68);
6132    float_button.hovered_visual = Some(button_visual(54, 70, 92));
6133    float_button.text_style = text(12.0, color(232, 238, 248));
6134    widgets::button(
6135        ui,
6136        controls,
6137        "layout_widgets.dock.float_inspector",
6138        label,
6139        float_button,
6140    );
6141
6142    let mut before_button = widgets::ButtonOptions::new(
6143        LayoutStyle::new()
6144            .with_width(136.0)
6145            .with_height(28.0)
6146            .with_flex_shrink(0.0),
6147    )
6148    .with_action("layout_widgets.reorder.assets.before.inspector");
6149    before_button.visual = button_visual(34, 44, 58);
6150    before_button.hovered_visual = Some(button_visual(48, 64, 84));
6151    before_button.text_style = text(12.0, color(232, 238, 248));
6152    widgets::button(
6153        ui,
6154        controls,
6155        "layout_widgets.dock.assets_before_inspector",
6156        "Assets before",
6157        before_button,
6158    );
6159
6160    let mut after_button = widgets::ButtonOptions::new(
6161        LayoutStyle::new()
6162            .with_width(126.0)
6163            .with_height(28.0)
6164            .with_flex_shrink(0.0),
6165    )
6166    .with_action("layout_widgets.reorder.assets.after.inspector");
6167    after_button.visual = button_visual(34, 44, 58);
6168    after_button.hovered_visual = Some(button_visual(48, 64, 84));
6169    after_button.text_style = text(12.0, color(232, 238, 248));
6170    widgets::button(
6171        ui,
6172        controls,
6173        "layout_widgets.dock.assets_after_inspector",
6174        "Assets after",
6175        after_button,
6176    );
6177
6178    let zones = ext_widgets::dock_workspace::dock_workspace_drop_zones(
6179        "layout_widgets.dock",
6180        UiRect::new(0.0, 0.0, 520.0, 340.0),
6181        ext_widgets::DockWorkspaceDragOptions::default()
6182            .allowed_sides([
6183                ext_widgets::DockSide::Left,
6184                ext_widgets::DockSide::Right,
6185                ext_widgets::DockSide::Center,
6186            ])
6187            .edge_thickness(44.0),
6188    );
6189    let targets = wrapping_row(ui, content, "layout_widgets.dock.targets", 6.0);
6190    for zone in zones {
6191        dock_drop_target_chip(ui, targets, &zone);
6192    }
6193
6194    let mut panels = base_layout_dock_panels();
6195    state.layout_dock.apply_order_to_panels(&mut panels);
6196    let reorder_targets: Vec<_> = [
6197        ext_widgets::DockSide::Left,
6198        ext_widgets::DockSide::Right,
6199        ext_widgets::DockSide::Center,
6200    ]
6201    .into_iter()
6202    .flat_map(|side| {
6203        ext_widgets::dock_workspace::dock_panel_reorder_drop_targets(
6204            "layout_widgets.dock",
6205            &panels,
6206            side,
6207            UiRect::new(0.0, 0.0, 180.0, 120.0),
6208            ext_widgets::DockWorkspaceReorderOptions::default().target_thickness(20.0),
6209        )
6210    })
6211    .collect();
6212    let reorder_row = wrapping_row(ui, content, "layout_widgets.dock.reorder_targets", 6.0);
6213    for target in reorder_targets {
6214        dock_reorder_target_chip(ui, reorder_row, &target);
6215    }
6216
6217    let tabs = [
6218        ext_widgets::TabItem::new("preview", "Preview"),
6219        ext_widgets::TabItem::new("log", "Output").dirty(),
6220        ext_widgets::TabItem::new("settings", "Settings").closable(),
6221    ];
6222    let mut tab_options = ext_widgets::TabGroupOptions::default();
6223    tab_options.layout = LayoutStyle::column()
6224        .with_width_percent(1.0)
6225        .with_height(0.0)
6226        .with_flex_grow(1.0);
6227    tab_options.tab_strip_height = 30.0;
6228    tab_options.min_tab_width = 92.0;
6229    tab_options.text_style = text(12.0, color(226, 234, 246));
6230    tab_options.muted_text_style = text(12.0, color(150, 162, 178));
6231    ext_widgets::tab_group(
6232        ui,
6233        content,
6234        "layout_widgets.document.tabs",
6235        &tabs,
6236        ext_widgets::TabGroupState::selected(0),
6237        tab_options,
6238        |ui, panel, _index| {
6239            widgets::label(
6240                ui,
6241                panel,
6242                "layout_widgets.document.tabs.preview.body",
6243                "Workspace preview",
6244                text(12.0, color(190, 202, 218)),
6245                LayoutStyle::new().with_width_percent(1.0).with_height(26.0),
6246            );
6247        },
6248    );
6249}
6250
6251fn dock_drop_target_chip(
6252    ui: &mut UiDocument,
6253    parent: UiNodeId,
6254    zone: &ext_widgets::DockWorkspaceDropZone,
6255) -> UiNodeId {
6256    let chip = ui.add_child(
6257        parent,
6258        UiNode::container(
6259            format!("{}.chip", zone.target.id.as_str()),
6260            LayoutStyle::row()
6261                .with_width(78.0)
6262                .with_height(26.0)
6263                .with_padding(6.0)
6264                .with_flex_shrink(0.0),
6265        )
6266        .with_input(InputBehavior::BUTTON)
6267        .with_visual(UiVisual::panel(
6268            color(24, 32, 42),
6269            Some(StrokeStyle::new(color(78, 94, 116), 1.0)),
6270            4.0,
6271        ))
6272        .with_accessibility(zone.target.accessibility_meta()),
6273    );
6274    widgets::label(
6275        ui,
6276        chip,
6277        format!("{}.label", zone.target.id.as_str()),
6278        dock_drop_target_short_label(zone.placement),
6279        text(11.0, color(206, 216, 230)),
6280        LayoutStyle::new().with_width_percent(1.0),
6281    );
6282    chip
6283}
6284
6285fn dock_reorder_target_chip(
6286    ui: &mut UiDocument,
6287    parent: UiNodeId,
6288    target: &ext_widgets::DockPanelReorderTarget,
6289) -> UiNodeId {
6290    let chip = ui.add_child(
6291        parent,
6292        UiNode::container(
6293            format!("{}.chip", target.target.id.as_str()),
6294            LayoutStyle::row()
6295                .with_width(104.0)
6296                .with_height(26.0)
6297                .with_padding(6.0)
6298                .with_flex_shrink(0.0),
6299        )
6300        .with_input(InputBehavior::BUTTON)
6301        .with_visual(UiVisual::panel(
6302            color(22, 34, 42),
6303            Some(StrokeStyle::new(color(80, 112, 128), 1.0)),
6304            4.0,
6305        ))
6306        .with_accessibility(target.target.accessibility_meta()),
6307    );
6308    widgets::label(
6309        ui,
6310        chip,
6311        format!("{}.label", target.target.id.as_str()),
6312        dock_reorder_target_short_label(target),
6313        text(11.0, color(206, 216, 230)),
6314        LayoutStyle::new().with_width_percent(1.0),
6315    );
6316    chip
6317}
6318
6319fn dock_drop_target_short_label(placement: ext_widgets::DockDropPlacement) -> &'static str {
6320    match placement {
6321        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Left) => "Left",
6322        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Right) => "Right",
6323        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Center) => "Center",
6324        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Top) => "Top",
6325        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Bottom) => "Bottom",
6326        ext_widgets::DockDropPlacement::Floating => "Float",
6327    }
6328}
6329
6330fn dock_reorder_target_short_label(target: &ext_widgets::DockPanelReorderTarget) -> String {
6331    let placement = match target.placement {
6332        ext_widgets::DockPanelReorderPlacement::Before => "Before",
6333        ext_widgets::DockPanelReorderPlacement::After => "After",
6334    };
6335    format!("{placement} {}", target.panel_id)
6336}
6337
6338fn container_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6339    let body = section(ui, parent, "containers", "Containers");
6340
6341    let frame = widgets::frame(
6342        ui,
6343        body,
6344        "containers.frame",
6345        widgets::FrameOptions::default().with_layout(
6346            LayoutStyle::column()
6347                .with_width_percent(1.0)
6348                .with_height(64.0)
6349                .with_padding(8.0)
6350                .with_gap(6.0),
6351        ),
6352    );
6353    widgets::strong_label(
6354        ui,
6355        frame,
6356        "containers.frame.title",
6357        "Frame",
6358        LayoutStyle::new().with_width_percent(1.0),
6359    );
6360    widgets::weak_label(
6361        ui,
6362        frame,
6363        "containers.frame.body",
6364        "Default framed surface with padding, stroke, and clipping.",
6365        LayoutStyle::new().with_width_percent(1.0),
6366    );
6367
6368    let group = widgets::group(ui, body, "containers.group");
6369    widgets::label(
6370        ui,
6371        group,
6372        "containers.group.label",
6373        "Group helper",
6374        text(12.0, color(220, 228, 238)),
6375        LayoutStyle::new().with_width_percent(1.0),
6376    );
6377    let generic_panel = widgets::panel(
6378        ui,
6379        body,
6380        "containers.panel",
6381        widgets::PanelOptions::group().with_layout(
6382            LayoutStyle::column()
6383                .with_width_percent(1.0)
6384                .with_height(44.0)
6385                .with_padding(8.0),
6386        ),
6387    );
6388    widgets::label(
6389        ui,
6390        generic_panel,
6391        "containers.panel.label",
6392        "Generic panel",
6393        text(12.0, color(220, 228, 238)),
6394        LayoutStyle::new().with_width_percent(1.0),
6395    );
6396    let group_panel = widgets::group_panel(ui, body, "containers.group_panel");
6397    widgets::label(
6398        ui,
6399        group_panel,
6400        "containers.group_panel.label",
6401        "Group panel",
6402        text(12.0, color(220, 228, 238)),
6403        LayoutStyle::new().with_width_percent(1.0),
6404    );
6405
6406    widgets::separator(
6407        ui,
6408        body,
6409        "containers.separator",
6410        widgets::SeparatorOptions::default(),
6411    );
6412    widgets::spacer(
6413        ui,
6414        body,
6415        "containers.spacer",
6416        LayoutStyle::new()
6417            .with_width_percent(1.0)
6418            .with_height(8.0)
6419            .with_flex_shrink(0.0),
6420    );
6421
6422    let grid = widgets::grid::grid(
6423        ui,
6424        body,
6425        "containers.grid",
6426        widgets::grid::GridOptions::default().with_layout(
6427            LayoutStyle::column()
6428                .with_width_percent(1.0)
6429                .with_height(78.0)
6430                .with_gap(4.0),
6431        ),
6432    );
6433    for row_index in 0..2 {
6434        let row = widgets::grid::grid_row(
6435            ui,
6436            grid,
6437            format!("containers.grid.row.{row_index}"),
6438            widgets::grid::GridRowOptions::default(),
6439        );
6440        for column_index in 0..3 {
6441            widgets::grid::grid_text_cell(
6442                ui,
6443                row,
6444                format!("containers.grid.row.{row_index}.cell.{column_index}"),
6445                format!("R{} C{}", row_index + 1, column_index + 1),
6446                widgets::grid::GridCellOptions {
6447                    text_style: text(12.0, color(214, 224, 238)),
6448                    ..Default::default()
6449                },
6450            );
6451        }
6452    }
6453
6454    widgets::sides(
6455        ui,
6456        body,
6457        "containers.sides",
6458        widgets::SidesOptions::default()
6459            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
6460            .with_gap(8.0)
6461            .with_visual(UiVisual::panel(
6462                color(20, 25, 32),
6463                Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
6464                4.0,
6465            )),
6466        |ui, left| {
6467            widgets::label(
6468                ui,
6469                left,
6470                "containers.sides.left.label",
6471                "Left side",
6472                text(12.0, color(220, 228, 238)),
6473                LayoutStyle::new().with_width_percent(1.0),
6474            );
6475        },
6476        |ui, right| {
6477            widgets::label(
6478                ui,
6479                right,
6480                "containers.sides.right.label",
6481                "Right side",
6482                text(12.0, color(220, 228, 238)),
6483                LayoutStyle::new().with_width_percent(1.0),
6484            );
6485        },
6486    );
6487
6488    widgets::columns(
6489        ui,
6490        body,
6491        "containers.columns",
6492        3,
6493        widgets::ColumnsOptions::default()
6494            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
6495            .with_gap(8.0),
6496        |ui, column, index| {
6497            widgets::label(
6498                ui,
6499                column,
6500                format!("containers.columns.{index}.label"),
6501                format!("Column {}", index + 1),
6502                text(12.0, color(220, 228, 238)),
6503                LayoutStyle::new().with_width_percent(1.0),
6504            );
6505        },
6506    );
6507
6508    let indented = widgets::indented_section(
6509        ui,
6510        body,
6511        "containers.indented",
6512        widgets::IndentOptions::default().with_amount(24.0),
6513    );
6514    widgets::label(
6515        ui,
6516        indented,
6517        "containers.indented.label",
6518        "Indented section",
6519        text(12.0, color(196, 210, 230)),
6520        LayoutStyle::new().with_width_percent(1.0),
6521    );
6522
6523    widgets::resize_container(
6524        ui,
6525        body,
6526        "containers.resize_container",
6527        widgets::ResizeContainerOptions::default().with_layout(
6528            LayoutStyle::column()
6529                .with_width_percent(1.0)
6530                .with_height(92.0)
6531                .with_flex_shrink(0.0),
6532        ),
6533        |ui, content| {
6534            widgets::label(
6535                ui,
6536                content,
6537                "containers.resize_container.label",
6538                "Resize container",
6539                text(12.0, color(220, 228, 238)),
6540                LayoutStyle::new().with_width_percent(1.0),
6541            );
6542        },
6543    );
6544    widgets::container::resize_handle(
6545        ui,
6546        body,
6547        "containers.resize_handle",
6548        widgets::container::ResizeHandleOptions::default()
6549            .with_layout(LayoutStyle::size(20.0, 20.0))
6550            .accessibility_label("Inline resize handle"),
6551    );
6552
6553    widgets::scene(
6554        ui,
6555        body,
6556        "containers.scene",
6557        vec![
6558            ScenePrimitive::Rect(
6559                PaintRect::solid(UiRect::new(8.0, 12.0, 108.0, 46.0), color(48, 112, 184))
6560                    .stroke(AlignedStroke::inside(StrokeStyle::new(
6561                        color(132, 174, 222),
6562                        1.0,
6563                    )))
6564                    .corner_radii(CornerRadii::uniform(6.0)),
6565            ),
6566            ScenePrimitive::Circle {
6567                center: UiPoint::new(150.0, 35.0),
6568                radius: 22.0,
6569                fill: color(111, 203, 159),
6570                stroke: Some(StrokeStyle::new(color(176, 236, 206), 1.0)),
6571            },
6572            ScenePrimitive::Line {
6573                from: UiPoint::new(188.0, 18.0),
6574                to: UiPoint::new(238.0, 52.0),
6575                stroke: StrokeStyle::new(color(232, 186, 88), 3.0),
6576            },
6577        ],
6578        widgets::SceneOptions::default()
6579            .with_layout(LayoutStyle::new().with_width(260.0).with_height(70.0))
6580            .accessibility_label("Scene primitives"),
6581    );
6582
6583    let panel_shell = widgets::frame(
6584        ui,
6585        body,
6586        "containers.panels",
6587        widgets::FrameOptions::default().with_layout(
6588            LayoutStyle::column()
6589                .with_width_percent(1.0)
6590                .with_height(160.0)
6591                .with_padding(0.0)
6592                .with_gap(0.0),
6593        ),
6594    );
6595    let top = widgets::top_panel(ui, panel_shell, "containers.panels.top", 28.0);
6596    widgets::label(
6597        ui,
6598        top,
6599        "containers.panels.top.label",
6600        "Top panel",
6601        text(12.0, color(220, 228, 238)),
6602        LayoutStyle::new().with_width_percent(1.0),
6603    );
6604    let middle = row(ui, panel_shell, "containers.panels.middle", 0.0);
6605    let left = widgets::side_panel(
6606        ui,
6607        middle,
6608        "containers.panels.side",
6609        widgets::SidePanelSide::Left,
6610        90.0,
6611    );
6612    widgets::label(
6613        ui,
6614        left,
6615        "containers.panels.side.label",
6616        "Side",
6617        text(12.0, color(220, 228, 238)),
6618        LayoutStyle::new().with_width_percent(1.0),
6619    );
6620    let left = widgets::left_panel(ui, middle, "containers.panels.left", 90.0);
6621    widgets::label(
6622        ui,
6623        left,
6624        "containers.panels.left.label",
6625        "Left",
6626        text(12.0, color(220, 228, 238)),
6627        LayoutStyle::new().with_width_percent(1.0),
6628    );
6629    let center = widgets::central_panel(ui, middle, "containers.panels.center");
6630    widgets::label(
6631        ui,
6632        center,
6633        "containers.panels.center.label",
6634        "Central panel",
6635        text(12.0, color(220, 228, 238)),
6636        LayoutStyle::new().with_width_percent(1.0),
6637    );
6638    let right = widgets::right_panel(ui, middle, "containers.panels.right", 110.0);
6639    widgets::label(
6640        ui,
6641        right,
6642        "containers.panels.right.label",
6643        "Right",
6644        text(12.0, color(220, 228, 238)),
6645        LayoutStyle::new().with_width_percent(1.0),
6646    );
6647    let bottom = widgets::bottom_panel(ui, panel_shell, "containers.panels.bottom", 28.0);
6648    widgets::label(
6649        ui,
6650        bottom,
6651        "containers.panels.bottom.label",
6652        "Bottom panel",
6653        text(12.0, color(220, 228, 238)),
6654        LayoutStyle::new().with_width_percent(1.0),
6655    );
6656
6657    widgets::scroll_container(
6658        ui,
6659        body,
6660        "containers.scroll_area_with_bars",
6661        state.containers_scroll,
6662        widgets::ScrollContainerOptions::default()
6663            .with_axes(ScrollAxes::BOTH)
6664            .with_layout(LayoutStyle::column().with_width(300.0).with_height(116.0)),
6665        |ui, viewport| {
6666            for index in 0..5 {
6667                widgets::label(
6668                    ui,
6669                    viewport,
6670                    format!("containers.scroll_area_with_bars.row.{index}"),
6671                    format!("Scrollable row {}", index + 1),
6672                    text(12.0, color(200, 212, 228)),
6673                    LayoutStyle::new()
6674                        .with_width(420.0)
6675                        .with_height(28.0)
6676                        .with_flex_shrink(0.0),
6677                );
6678            }
6679        },
6680    );
6681
6682    let area_host = ui.add_child(
6683        body,
6684        UiNode::container(
6685            "containers.area.host",
6686            LayoutStyle::new()
6687                .with_width_percent(1.0)
6688                .with_height(82.0)
6689                .with_flex_shrink(0.0),
6690        )
6691        .with_visual(UiVisual::panel(
6692            color(17, 20, 25),
6693            Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
6694            4.0,
6695        )),
6696    );
6697    widgets::container::area(
6698        ui,
6699        area_host,
6700        "containers.area",
6701        widgets::container::AreaOptions::new(UiRect::new(14.0, 14.0, 180.0, 44.0))
6702            .with_visual(UiVisual::panel(color(39, 72, 109), None, 4.0))
6703            .accessibility_label("Absolute positioned area"),
6704        |ui, area| {
6705            widgets::label(
6706                ui,
6707                area,
6708                "containers.area.label",
6709                "Area",
6710                text(12.0, color(238, 244, 252)),
6711                LayoutStyle::new().with_width_percent(1.0),
6712            );
6713        },
6714    );
6715}
6716
6717fn form_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6718    let body = section_with_min_viewport(ui, parent, "forms", "Forms", UiSize::new(390.0, 0.0));
6719    let section = widgets::form_section(
6720        ui,
6721        body,
6722        "forms.profile",
6723        Some("Profile".to_string()),
6724        widgets::FormSectionOptions::default().with_layout(
6725            LayoutStyle::column()
6726                .with_width_percent(1.0)
6727                .with_padding(12.0)
6728                .with_gap(10.0),
6729        ),
6730    );
6731    let status_row = wrapping_row(ui, section.root, "forms.profile.status_flags", 6.0);
6732    form_status_chip(
6733        ui,
6734        status_row,
6735        "forms.profile.status.dirty",
6736        "dirty",
6737        state.form.dirty,
6738    );
6739    form_status_chip(
6740        ui,
6741        status_row,
6742        "forms.profile.status.pending",
6743        "pending",
6744        state.form.pending,
6745    );
6746    form_status_chip(
6747        ui,
6748        status_row,
6749        "forms.profile.status.submitted",
6750        "submitted",
6751        state.form.submitted,
6752    );
6753
6754    let mut name_options = widgets::FormRowOptions::default().required();
6755    if state.form_name_text.text().trim().is_empty() {
6756        name_options = name_options.invalid("Name is required");
6757    }
6758    let name = widgets::form_row(ui, section.root, "forms.profile.name", name_options);
6759    widgets::field_label(
6760        ui,
6761        name,
6762        "forms.profile.name.label",
6763        "Name",
6764        widgets::FieldLabelOptions::default().required(),
6765    );
6766    form_text_field(
6767        ui,
6768        name,
6769        "forms.profile.name.input",
6770        &state.form_name_text,
6771        FocusedTextInput::FormName,
6772        state,
6773    );
6774    if state.form_name_text.text().trim().is_empty() {
6775        widgets::field_validation_message(
6776            ui,
6777            name,
6778            "forms.profile.name.validation",
6779            ValidationMessage::error("Name is required"),
6780            widgets::ValidationMessageOptions::default(),
6781        );
6782    } else {
6783        widgets::field_help_text(
6784            ui,
6785            name,
6786            "forms.profile.name.help",
6787            "Shown in window titles and project lists.",
6788            widgets::FieldHelpOptions::default(),
6789        );
6790    }
6791
6792    let mut email_options = widgets::FormRowOptions::default().required();
6793    if !profile_email_valid(state.form_email_text.text()) {
6794        email_options = email_options.invalid("Use a complete email address");
6795    }
6796    let email = widgets::form_row(ui, section.root, "forms.profile.email", email_options);
6797    widgets::field_label(
6798        ui,
6799        email,
6800        "forms.profile.email.label",
6801        "Email",
6802        widgets::FieldLabelOptions::default().required(),
6803    );
6804    form_text_field(
6805        ui,
6806        email,
6807        "forms.profile.email.input",
6808        &state.form_email_text,
6809        FocusedTextInput::FormEmail,
6810        state,
6811    );
6812    if profile_email_valid(state.form_email_text.text()) {
6813        widgets::field_help_text(
6814            ui,
6815            email,
6816            "forms.profile.email.help",
6817            "Used for workspace invites and notifications.",
6818            widgets::FieldHelpOptions::default(),
6819        );
6820    } else {
6821        widgets::field_validation_message(
6822            ui,
6823            email,
6824            "forms.profile.email.validation",
6825            ValidationMessage::error("Use a complete email address"),
6826            widgets::ValidationMessageOptions::default(),
6827        );
6828    }
6829
6830    let role = widgets::form_row(
6831        ui,
6832        section.root,
6833        "forms.profile.role",
6834        widgets::FormRowOptions::default(),
6835    );
6836    widgets::field_label(
6837        ui,
6838        role,
6839        "forms.profile.role.label",
6840        "Role",
6841        widgets::FieldLabelOptions::default(),
6842    );
6843    form_text_field(
6844        ui,
6845        role,
6846        "forms.profile.role.input",
6847        &state.form_role_text,
6848        FocusedTextInput::FormRole,
6849        state,
6850    );
6851    widgets::field_validation_message(
6852        ui,
6853        role,
6854        "forms.profile.role.help",
6855        if state.form_role_text.text().trim().is_empty() {
6856            ValidationMessage::warning("Role can be added later")
6857        } else {
6858            ValidationMessage::info(
6859                "Form rows compose labels, controls, help, and validation text.",
6860            )
6861        },
6862        widgets::ValidationMessageOptions::default(),
6863    );
6864
6865    let newsletter = widgets::form_row(
6866        ui,
6867        section.root,
6868        "forms.profile.newsletter",
6869        widgets::FormRowOptions::default().with_accessibility_label("Newsletter preference"),
6870    );
6871    let mut newsletter_options =
6872        widgets::CheckboxOptions::default().with_action("forms.profile.newsletter.toggle");
6873    newsletter_options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
6874    newsletter_options.text_style = text(12.0, color(220, 228, 238));
6875    widgets::checkbox(
6876        ui,
6877        newsletter,
6878        "forms.profile.newsletter.input",
6879        "Send release notes",
6880        state.form_newsletter,
6881        newsletter_options,
6882    );
6883    widgets::field_help_text(
6884        ui,
6885        newsletter,
6886        "forms.profile.newsletter.help",
6887        "Checkboxes participate in the same form state as text fields.",
6888        widgets::FieldHelpOptions::default(),
6889    );
6890
6891    widgets::form_error_summary(
6892        ui,
6893        section.root,
6894        "forms.profile.errors",
6895        &state.form,
6896        widgets::FormErrorSummaryOptions::default(),
6897    );
6898    let action_layout = Layout::row()
6899        .size(LayoutSize::new(
6900            LayoutDimension::percent(1.0),
6901            LayoutDimension::Auto,
6902        ))
6903        .gap(LayoutGap::points(8.0, 8.0))
6904        .flex_wrap(LayoutFlexWrap::Wrap)
6905        .to_layout_style();
6906    widgets::form_action_buttons(
6907        ui,
6908        section.root,
6909        "forms.profile.actions",
6910        &state.form,
6911        widgets::FormActionButtonsOptions::default()
6912            .with_layout(action_layout)
6913            .include_reset(true)
6914            .with_action_prefix("forms.profile"),
6915    );
6916    widgets::label(
6917        ui,
6918        section.root,
6919        "forms.profile.status",
6920        format!("Status: {}", state.form_status),
6921        text(11.0, color(154, 166, 184)),
6922        LayoutStyle::new().with_width_percent(1.0),
6923    );
6924}
6925
6926fn overlay_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6927    let body =
6928        section_with_min_viewport(ui, parent, "overlays", "Overlays", UiSize::new(420.0, 0.0));
6929    let header = widgets::collapsing_header(
6930        ui,
6931        body,
6932        "overlays.collapsing",
6933        "Collapsing header",
6934        widgets::CollapsingHeaderOptions::default()
6935            .expanded(state.overlay_expanded)
6936            .with_toggle_action("overlays.collapsing.toggle"),
6937    );
6938    if let Some(panel) = header.body {
6939        widgets::label(
6940            ui,
6941            panel,
6942            "overlays.collapsing.body",
6943            "Expanded content lives under the header and remains part of normal layout.",
6944            text(12.0, color(196, 210, 230)),
6945            LayoutStyle::new().with_width_percent(1.0),
6946        );
6947    }
6948
6949    let controls = wrapping_row(ui, body, "overlays.controls", 8.0);
6950    button(
6951        ui,
6952        controls,
6953        "overlays.popup.toggle",
6954        if state.overlay_popup_open {
6955            "Close popup"
6956        } else {
6957            "Open popup"
6958        },
6959        "overlays.popup.toggle",
6960        button_visual(48, 112, 184),
6961    );
6962    button(
6963        ui,
6964        controls,
6965        "overlays.modal.open",
6966        "Open modal",
6967        "overlays.modal.open",
6968        button_visual(58, 78, 96),
6969    );
6970
6971    let tooltip = TooltipContent::new("Tooltip")
6972        .body("Tooltip boxes are overlay surfaces with title, body, and shortcut text.")
6973        .shortcut_label("Ctrl+K")
6974        .disabled_reason("Disabled reasons can be announced without changing the trigger.");
6975    let mut tooltip_options = widgets::TooltipBoxOptions::default()
6976        .with_layout(
6977            LayoutStyle::column()
6978                .with_width(280.0)
6979                .with_padding(8.0)
6980                .with_gap(4.0),
6981        )
6982        .with_animation(None);
6983    tooltip_options.layer = UiLayer::AppContent;
6984    tooltip_options.z_index = 0;
6985    widgets::tooltip_box(ui, body, "overlays.tooltip", tooltip, tooltip_options);
6986
6987    let tooltip_anchor = row(ui, body, "overlays.tooltip_anchor", 8.0);
6988    widgets::label(
6989        ui,
6990        tooltip_anchor,
6991        "overlays.tooltip_anchor.label",
6992        "Tooltip placement clamps to its viewport.",
6993        text(12.0, color(166, 176, 190)),
6994        LayoutStyle::new().with_width_percent(1.0),
6995    );
6996    let clamped_rect = widgets::tooltip::tooltip_rect(
6997        UiRect::new(328.0, 12.0, 54.0, 24.0),
6998        UiSize::new(176.0, 58.0),
6999        UiRect::new(0.0, 0.0, 420.0, 190.0),
7000        TooltipPlacement::Right,
7001        8.0,
7002        None,
7003    );
7004    let clamped_preview = ui.add_child(
7005        body,
7006        UiNode::container(
7007            "overlays.tooltip_rect.preview",
7008            LayoutStyle::new()
7009                .with_width_percent(1.0)
7010                .with_height(78.0)
7011                .with_flex_shrink(0.0),
7012        )
7013        .with_visual(UiVisual::panel(
7014            color(12, 16, 22),
7015            Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
7016            4.0,
7017        )),
7018    );
7019    ui.add_child(
7020        clamped_preview,
7021        UiNode::scene(
7022            "overlays.tooltip_rect.scene",
7023            vec![
7024                ScenePrimitive::Rect(
7025                    PaintRect::solid(UiRect::new(328.0, 12.0, 54.0, 24.0), color(48, 112, 184))
7026                        .corner_radii(CornerRadii::uniform(3.0)),
7027                ),
7028                ScenePrimitive::Rect(
7029                    PaintRect::solid(clamped_rect, color(24, 29, 38))
7030                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7031                            color(92, 106, 128),
7032                            1.0,
7033                        )))
7034                        .corner_radii(CornerRadii::uniform(4.0)),
7035                ),
7036            ],
7037            LayoutStyle::new()
7038                .with_width_percent(1.0)
7039                .with_height_percent(1.0),
7040        ),
7041    );
7042
7043    if state.overlay_popup_open {
7044        let popup = ext_widgets::popup_panel(
7045            ui,
7046            parent,
7047            "overlays.popup_panel",
7048            UiRect::new(18.0, 150.0, 220.0, 112.0),
7049            ext_widgets::PopupOptions {
7050                z_index: 20,
7051                portal: UiPortalTarget::Parent,
7052                accessibility: Some(
7053                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup"),
7054                ),
7055                ..Default::default()
7056            },
7057        );
7058        let popup_body = ui.add_child(
7059            popup,
7060            UiNode::container(
7061                "overlays.popup_panel.body",
7062                LayoutStyle::column()
7063                    .with_width_percent(1.0)
7064                    .with_height_percent(1.0)
7065                    .with_padding(10.0)
7066                    .with_gap(6.0),
7067            ),
7068        );
7069        let popup_header = row(ui, popup_body, "overlays.popup_panel.header", 8.0);
7070        widgets::label(
7071            ui,
7072            popup_header,
7073            "overlays.popup_panel.label",
7074            "Popup panel",
7075            text(12.0, color(220, 228, 238)),
7076            LayoutStyle::new().with_width_percent(1.0),
7077        );
7078        let mut close = widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0))
7079            .with_action("overlays.popup.close");
7080        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7081        close.hovered_visual = Some(button_visual(54, 70, 92));
7082        close.text_style = text(12.0, color(220, 228, 238));
7083        widgets::button(ui, popup_header, "overlays.popup_panel.close", "x", close);
7084        widgets::label(
7085            ui,
7086            popup_body,
7087            "overlays.popup_panel.body_text",
7088            "Popup content is conditionally rendered.",
7089            text(11.0, color(196, 210, 230)),
7090            LayoutStyle::new().with_width_percent(1.0),
7091        );
7092    }
7093
7094    if state.overlay_modal_open {
7095        let modal = widgets::modal_dialog(
7096            ui,
7097            parent,
7098            "overlays.modal",
7099            "Modal dialog",
7100            widgets::ModalDialogOptions::default()
7101                .with_size(320.0, 180.0)
7102                .with_close_action("overlays.modal.close")
7103                .with_dismissal(ext_widgets::DialogDismissal::MODAL)
7104                .with_focus_restore(FocusRestoreTarget::Previous),
7105        );
7106        widgets::label(
7107            ui,
7108            modal.body,
7109            "overlays.modal.body.text",
7110            "Modal dialogs are portaled to the application overlay, include a scrim, and trap focus.",
7111            text(12.0, color(220, 228, 238)),
7112            LayoutStyle::new().with_width_percent(1.0),
7113        );
7114        button(
7115            ui,
7116            modal.body,
7117            "overlays.modal.body.close",
7118            "Close modal",
7119            "overlays.modal.close",
7120            button_visual(48, 112, 184),
7121        );
7122    }
7123}
7124
7125fn drag_drop_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7126    let body = section_with_min_viewport(
7127        ui,
7128        parent,
7129        "drag_drop",
7130        "Drag and drop",
7131        UiSize::new(420.0, 0.0),
7132    );
7133    widgets::label(
7134        ui,
7135        body,
7136        "drag_drop.sources.label",
7137        "Drag sources",
7138        text(12.0, color(166, 176, 190)),
7139        LayoutStyle::new().with_width_percent(1.0),
7140    );
7141    let sources = wrapping_row(ui, body, "drag_drop.sources", 8.0);
7142    widgets::dnd_drag_source(
7143        ui,
7144        sources,
7145        "drag_drop.text_source",
7146        "Text payload",
7147        DragPayload::text("Operad payload"),
7148        widgets::DragSourceOptions::default()
7149            .with_layout(drag_source_layout())
7150            .with_kind(DragDropSurfaceKind::ListRow)
7151            .with_allowed_operations([DragOperation::Copy, DragOperation::Move])
7152            .with_action("drag_drop.text_source")
7153            .with_accessibility_hint("Start a text drag operation"),
7154    );
7155    widgets::dnd_drag_source(
7156        ui,
7157        sources,
7158        "drag_drop.file_source",
7159        "File payload",
7160        DragPayload::files(["/tmp/showcase.scene"]),
7161        widgets::DragSourceOptions::default()
7162            .with_layout(drag_source_layout())
7163            .with_kind(DragDropSurfaceKind::Asset)
7164            .with_drag_image_policy(widgets::DragImagePolicy::image_key(
7165                BuiltInIcon::Folder.key(),
7166                UiSize::new(120.0, 36.0),
7167                UiPoint::new(10.0, 10.0),
7168            ))
7169            .with_allowed_operations([DragOperation::Copy])
7170            .with_action("drag_drop.file_source"),
7171    );
7172    widgets::dnd_drag_source(
7173        ui,
7174        sources,
7175        "drag_drop.bytes_source",
7176        "Image bytes",
7177        DragPayload::bytes(DragBytes::new("image/png", vec![137, 80, 78, 71]).name("sprite.png")),
7178        widgets::DragSourceOptions::default()
7179            .with_layout(drag_source_layout())
7180            .with_kind(DragDropSurfaceKind::Asset)
7181            .with_action("drag_drop.bytes_source")
7182            .without_drag_image(),
7183    );
7184
7185    widgets::label(
7186        ui,
7187        body,
7188        "drag_drop.zones.label",
7189        "Drop zones",
7190        text(12.0, color(166, 176, 190)),
7191        LayoutStyle::new().with_width_percent(1.0),
7192    );
7193    let zones = wrapping_row(ui, body, "drag_drop.zones", 8.0);
7194    let accepted_options = widgets::DropZoneOptions::default()
7195        .with_layout(drop_zone_layout())
7196        .with_kind(DragDropSurfaceKind::EditorSurface)
7197        .with_accepted_payload(DropPayloadFilter::empty().text())
7198        .with_accepted_operations([DragOperation::Copy, DragOperation::Move])
7199        .with_action("drag_drop.accept_text")
7200        .with_accessibility_hint("Accepts text payloads");
7201    let accepted = widgets::dnd_drop_zone(
7202        ui,
7203        zones,
7204        "drag_drop.accept_text",
7205        "Text accepted",
7206        accepted_options.clone(),
7207    );
7208    widgets::drag_drop::dnd_apply_drop_zone_preview(
7209        ui,
7210        accepted.root,
7211        &accepted_options,
7212        widgets::drag_drop::DropZonePreviewState::Accepted,
7213    );
7214
7215    let rejected_options = widgets::DropZoneOptions::default()
7216        .with_layout(drop_zone_layout())
7217        .with_kind(DragDropSurfaceKind::Asset)
7218        .with_accepted_payload(DropPayloadFilter::empty().files())
7219        .with_action("drag_drop.files_only");
7220    let rejected = widgets::dnd_drop_zone(
7221        ui,
7222        zones,
7223        "drag_drop.files_only",
7224        "Files only",
7225        rejected_options.clone(),
7226    );
7227    widgets::drag_drop::dnd_apply_drop_zone_preview(
7228        ui,
7229        rejected.root,
7230        &rejected_options,
7231        widgets::drag_drop::DropZonePreviewState::Rejected,
7232    );
7233    let image_options = widgets::DropZoneOptions::default()
7234        .with_layout(drop_zone_layout())
7235        .with_kind(DragDropSurfaceKind::Asset)
7236        .with_accepted_payload(DropPayloadFilter::empty().mime_type("image/*"))
7237        .with_accepted_operations([DragOperation::Copy])
7238        .with_action("drag_drop.image_bytes");
7239    let image_zone = widgets::dnd_drop_zone(
7240        ui,
7241        zones,
7242        "drag_drop.image_bytes",
7243        "Image bytes",
7244        image_options.clone(),
7245    );
7246    widgets::drag_drop::dnd_apply_drop_zone_preview(
7247        ui,
7248        image_zone.root,
7249        &image_options,
7250        widgets::drag_drop::DropZonePreviewState::Hovered,
7251    );
7252
7253    let disabled_options = widgets::DropZoneOptions::default()
7254        .with_layout(drop_zone_layout())
7255        .with_kind(DragDropSurfaceKind::EditorSurface)
7256        .with_accepted_payload(DropPayloadFilter::any())
7257        .with_action("drag_drop.disabled")
7258        .disabled();
7259    let disabled_zone = widgets::dnd_drop_zone(
7260        ui,
7261        zones,
7262        "drag_drop.disabled",
7263        "Disabled",
7264        disabled_options.clone(),
7265    );
7266    widgets::drag_drop::dnd_apply_drop_zone_preview(
7267        ui,
7268        disabled_zone.root,
7269        &disabled_options,
7270        widgets::drag_drop::DropZonePreviewState::Disabled,
7271    );
7272
7273    let operation_row = wrapping_row(ui, body, "drag_drop.operations", 6.0);
7274    dnd_operation_chip(ui, operation_row, "drag_drop.operation.copy", "copy");
7275    dnd_operation_chip(ui, operation_row, "drag_drop.operation.move", "move");
7276    dnd_operation_chip(ui, operation_row, "drag_drop.operation.link", "link");
7277    widgets::label(
7278        ui,
7279        body,
7280        "drag_drop.status",
7281        format!("Status: {}", state.drag_drop_status),
7282        text(11.0, color(154, 166, 184)),
7283        LayoutStyle::new().with_width_percent(1.0),
7284    );
7285}
7286
7287fn media_widgets(ui: &mut UiDocument, parent: UiNodeId) {
7288    let body = section_with_min_viewport(ui, parent, "media", "Media", UiSize::new(430.0, 0.0));
7289    widgets::label(
7290        ui,
7291        body,
7292        "media.icons.label",
7293        "Built-in icons",
7294        text(12.0, color(166, 176, 190)),
7295        LayoutStyle::new().with_width_percent(1.0),
7296    );
7297    let icons = wrapping_row(ui, body, "media.icons", 8.0);
7298    for icon in BuiltInIcon::COMMON {
7299        media_icon_tile(ui, icons, icon);
7300    }
7301
7302    widgets::label(
7303        ui,
7304        body,
7305        "media.variants.label",
7306        "Image variants",
7307        text(12.0, color(166, 176, 190)),
7308        LayoutStyle::new().with_width_percent(1.0),
7309    );
7310    let variants = wrapping_row(ui, body, "media.variants", 10.0);
7311    widgets::image(
7312        ui,
7313        variants,
7314        "media.image.untinted",
7315        icon_image(BuiltInIcon::Play),
7316        widgets::ImageOptions::default()
7317            .with_layout(media_preview_image_layout())
7318            .with_accessibility_label("Untinted play icon"),
7319    );
7320    widgets::image(
7321        ui,
7322        variants,
7323        "media.image.warning",
7324        ImageContent::new(BuiltInIcon::Warning.key()).tinted(color(232, 186, 88)),
7325        widgets::ImageOptions::default()
7326            .with_layout(media_preview_image_layout())
7327            .with_accessibility_label("Tinted warning icon"),
7328    );
7329    widgets::image(
7330        ui,
7331        variants,
7332        "media.image.shader",
7333        ImageContent::new(BuiltInIcon::Grid.key()).tinted(color(118, 183, 255)),
7334        widgets::ImageOptions::default()
7335            .with_layout(media_preview_image_layout())
7336            .with_shader(ShaderEffect::new("media.preview.tint").uniform("amount", 0.5))
7337            .with_accessibility_label("Shader-decorated grid icon"),
7338    );
7339    widgets::label(
7340        ui,
7341        body,
7342        "media.image.note",
7343        "Image widgets reference stable resource keys; the host resolves them to textures, vector assets, tinting, or shader-backed resources.",
7344        text(12.0, color(166, 176, 190)),
7345        LayoutStyle::new().with_width_percent(1.0),
7346    );
7347}
7348
7349fn timeline_ruler(ui: &mut UiDocument, parent: UiNodeId) {
7350    let layout = LayoutStyle::column()
7351        .with_width_percent(1.0)
7352        .with_height(40.0)
7353        .with_flex_shrink(0.0);
7354    let layout = operad::layout::with_min_size(layout, operad::length(0.0), operad::length(0.0));
7355    let body = widgets::scroll_area(ui, parent, "timeline", ScrollAxes::BOTH, layout);
7356    ext_widgets::timeline_ruler(
7357        ui,
7358        body,
7359        "timeline.ruler",
7360        ext_widgets::RulerSpec {
7361            range: ext_widgets::TimelineRange::new(0.0, 12.0),
7362            width: 600.0,
7363            major_step: 2.0,
7364            minor_step: 0.5,
7365            label_every: 1,
7366        },
7367        ext_widgets::TimelineRulerOptions::default(),
7368    );
7369}
7370
7371fn toast_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7372    let body = section(ui, parent, "toasts", "Toasts");
7373    let controls = row(ui, body, "toasts.controls", 10.0);
7374    button(
7375        ui,
7376        controls,
7377        "toasts.show",
7378        "Show toast",
7379        "toast.show",
7380        button_visual(48, 112, 184),
7381    );
7382    button(
7383        ui,
7384        controls,
7385        "toasts.hide",
7386        "Hide",
7387        "toast.hide",
7388        button_visual(58, 78, 96),
7389    );
7390    widgets::label(
7391        ui,
7392        body,
7393        "toasts.status",
7394        if state.toast_visible {
7395            "Toast overlay is visible."
7396        } else {
7397            "Toast overlay is hidden."
7398        },
7399        text(12.0, color(196, 210, 230)),
7400        LayoutStyle::new().with_width_percent(1.0),
7401    );
7402    widgets::label(
7403        ui,
7404        body,
7405        "toasts.action_status",
7406        format!("Action: {}", state.toast_action_status),
7407        text(12.0, color(154, 166, 184)),
7408        LayoutStyle::new().with_width_percent(1.0),
7409    );
7410}
7411
7412fn popup_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7413    let body = section(ui, parent, "popup_panel", "Popup panel");
7414    let controls = row(ui, body, "popup_panel.controls", 8.0);
7415    button(
7416        ui,
7417        controls,
7418        "popup_panel.toggle",
7419        if state.popup_open {
7420            "Close popup"
7421        } else {
7422            "Open popup"
7423        },
7424        "popup.toggle",
7425        button_visual(48, 112, 184),
7426    );
7427    if state.popup_open {
7428        let mut close =
7429            widgets::ButtonOptions::new(LayoutStyle::size(30.0, 30.0)).with_action("popup.close");
7430        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7431        close.hovered_visual = Some(button_visual(54, 70, 92));
7432        close.text_style = text(13.0, color(220, 228, 238));
7433        widgets::button(ui, controls, "popup_panel.inline_close", "x", close);
7434    }
7435    widgets::label(
7436        ui,
7437        body,
7438        "popup_panel.status",
7439        if state.popup_open {
7440            "Popup overlay is open."
7441        } else {
7442            "Popup overlay is closed."
7443        },
7444        text(12.0, color(196, 210, 230)),
7445        LayoutStyle::new().with_width_percent(1.0),
7446    );
7447    if state.popup_open {
7448        let panel = ext_widgets::popup_panel(
7449            ui,
7450            parent,
7451            "popup_panel.inline_preview",
7452            UiRect::new(0.0, 20.0, 160.0, 104.0),
7453            ext_widgets::PopupOptions {
7454                z_index: 4,
7455                portal: UiPortalTarget::Parent,
7456                accessibility: Some(
7457                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup preview"),
7458                ),
7459                ..Default::default()
7460            },
7461        );
7462        let content = ui.add_child(
7463            panel,
7464            UiNode::container(
7465                "popup_panel.inline_preview.body",
7466                LayoutStyle::column()
7467                    .with_width_percent(1.0)
7468                    .with_height_percent(1.0)
7469                    .with_padding(10.0)
7470                    .with_gap(8.0),
7471            ),
7472        );
7473        let header = row(ui, content, "popup_panel.inline_preview.header", 8.0);
7474        widgets::label(
7475            ui,
7476            header,
7477            "popup_panel.inline_preview.title",
7478            "Popup panel",
7479            text(12.0, color(226, 234, 246)),
7480            LayoutStyle::new().with_width_percent(1.0),
7481        );
7482        let mut close =
7483            widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0)).with_action("popup.close");
7484        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7485        close.hovered_visual = Some(button_visual(54, 70, 92));
7486        close.text_style = text(12.0, color(220, 228, 238));
7487        widgets::button(ui, header, "popup_panel.inline_preview.close", "x", close);
7488        widgets::label(
7489            ui,
7490            content,
7491            "popup_panel.inline_preview.text",
7492            "Overlay content",
7493            text(11.0, color(196, 210, 230)),
7494            LayoutStyle::new().with_width_percent(1.0),
7495        );
7496        widgets::spacer(
7497            ui,
7498            body,
7499            "popup_panel.inline_preview.space",
7500            LayoutStyle::new()
7501                .with_width_percent(1.0)
7502                .with_height(112.0)
7503                .with_flex_shrink(0.0),
7504        );
7505    }
7506}
7507
7508fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7509    let body = section(ui, parent, "styling", "Styling");
7510    let grid_layout = operad::layout::with_grid_template_columns(
7511        Layout::grid()
7512            .size(LayoutSize::percent(1.0, 1.0))
7513            .gap(LayoutGap::points(10.0, 10.0))
7514            .to_layout_style(),
7515        [
7516            LayoutGridTrack::points(300.0),
7517            LayoutGridTrack::points(1.0),
7518            LayoutGridTrack::points(210.0),
7519        ],
7520    );
7521    let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
7522    let controls = ui.add_child(
7523        grid,
7524        UiNode::container(
7525            "styling.controls",
7526            LayoutStyle::column()
7527                .with_width(300.0)
7528                .with_height_percent(1.0)
7529                .with_flex_shrink(0.0)
7530                .gap(6.0),
7531        ),
7532    );
7533    style_edge_group(
7534        ui,
7535        controls,
7536        "styling.inner",
7537        "Inner margin",
7538        "styling.inner_same",
7539        state.styling.inner_same,
7540        [
7541            ("Left", "styling.inner", state.styling.inner_margin),
7542            ("Right", "styling.inner_right", state.styling.inner_right),
7543            ("Top", "styling.inner_top", state.styling.inner_top),
7544            ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
7545        ],
7546        0.0..32.0,
7547    );
7548    style_edge_group(
7549        ui,
7550        controls,
7551        "styling.outer",
7552        "Outer margin",
7553        "styling.outer_same",
7554        state.styling.outer_same,
7555        [
7556            ("Left", "styling.outer", state.styling.outer_margin),
7557            ("Right", "styling.outer_right", state.styling.outer_right),
7558            ("Top", "styling.outer_top", state.styling.outer_top),
7559            ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
7560        ],
7561        0.0..40.0,
7562    );
7563    style_edge_group(
7564        ui,
7565        controls,
7566        "styling.radius",
7567        "Corner radius",
7568        "styling.radius_same",
7569        state.styling.radius_same,
7570        [
7571            ("NW", "styling.radius", state.styling.corner_radius),
7572            ("NE", "styling.radius_ne", state.styling.corner_ne),
7573            ("SW", "styling.radius_sw", state.styling.corner_sw),
7574            ("SE", "styling.radius_se", state.styling.corner_se),
7575        ],
7576        0.0..28.0,
7577    );
7578    style_shadow_group(ui, controls, state);
7579    style_color_button_row(
7580        ui,
7581        controls,
7582        "styling.fill_color_button",
7583        "Fill",
7584        state.styling.fill_color(),
7585        "Pick fill color",
7586    );
7587    if state.styling_fill_picker_open {
7588        ext_widgets::color_picker(
7589            ui,
7590            controls,
7591            "styling.fill_picker",
7592            &state.styling_fill_picker,
7593            ext_widgets::ColorPickerOptions::default()
7594                .with_label("Fill")
7595                .with_action_prefix("styling.fill_picker"),
7596        );
7597    }
7598    style_stroke_row(ui, controls, state);
7599    if state.styling_stroke_picker_open {
7600        ext_widgets::color_picker(
7601            ui,
7602            controls,
7603            "styling.stroke_picker",
7604            &state.styling_stroke_picker,
7605            ext_widgets::ColorPickerOptions::default()
7606                .with_label("Stroke color")
7607                .with_action_prefix("styling.stroke_picker"),
7608        );
7609    }
7610    widgets::separator(
7611        ui,
7612        grid,
7613        "styling.preview.separator",
7614        widgets::SeparatorOptions::vertical().with_layout(
7615            LayoutStyle::new()
7616                .with_width(1.0)
7617                .with_height_percent(1.0)
7618                .with_flex_shrink(0.0),
7619        ),
7620    );
7621
7622    let preview = ui.add_child(
7623        grid,
7624        UiNode::container(
7625            "styling.preview",
7626            LayoutStyle::column()
7627                .with_width(210.0)
7628                .with_height_percent(1.0)
7629                .with_flex_shrink(0.0)
7630                .padding(8.0),
7631        )
7632        .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
7633    );
7634    style_preview(ui, preview, state.styling);
7635}
7636
7637#[allow(clippy::too_many_arguments)]
7638fn style_edge_group(
7639    ui: &mut UiDocument,
7640    parent: UiNodeId,
7641    name: &'static str,
7642    title: &'static str,
7643    same_action: &'static str,
7644    same: bool,
7645    values: [(&'static str, &'static str, f32); 4],
7646    range: std::ops::Range<f32>,
7647) {
7648    let group = style_control_group(ui, parent, format!("{name}.group"));
7649    style_group_title(ui, group, format!("{name}.title"), title);
7650    let fields = ui.add_child(
7651        group,
7652        UiNode::container(
7653            format!("{name}.fields"),
7654            LayoutStyle::column()
7655                .with_width(138.0)
7656                .with_flex_shrink(0.0)
7657                .gap(3.0),
7658        ),
7659    );
7660    style_compact_checkbox(ui, fields, same_action, "same", same);
7661    if same {
7662        style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
7663    } else {
7664        for (label, action, value) in values {
7665            style_number_row(ui, fields, action, label, value, range.clone(), 0);
7666        }
7667    }
7668}
7669
7670fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7671    let group = style_control_group(ui, parent, "styling.shadow.group");
7672    style_group_title(ui, group, "styling.shadow.title", "Shadow");
7673    let fields = ui.add_child(
7674        group,
7675        UiNode::container(
7676            "styling.shadow.fields",
7677            LayoutStyle::column()
7678                .with_width(174.0)
7679                .with_flex_shrink(0.0)
7680                .gap(4.0),
7681        ),
7682    );
7683    let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
7684    style_inline_number(
7685        ui,
7686        offsets,
7687        "styling.shadow_x",
7688        "x",
7689        state.styling.shadow_x,
7690        -24.0..24.0,
7691        0,
7692    );
7693    style_inline_number(
7694        ui,
7695        offsets,
7696        "styling.shadow_y",
7697        "y",
7698        state.styling.shadow_y,
7699        -24.0..24.0,
7700        0,
7701    );
7702    let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
7703    style_inline_number(
7704        ui,
7705        spread,
7706        "styling.shadow",
7707        "blur",
7708        state.styling.shadow_blur,
7709        0.0..32.0,
7710        0,
7711    );
7712    style_inline_number(
7713        ui,
7714        spread,
7715        "styling.shadow_spread",
7716        "spread",
7717        state.styling.shadow_spread,
7718        0.0..16.0,
7719        0,
7720    );
7721    style_color_button_row(
7722        ui,
7723        fields,
7724        "styling.shadow_color_button",
7725        "",
7726        state.styling.shadow_color(),
7727        "Pick shadow color",
7728    );
7729    if state.styling_shadow_picker_open {
7730        ext_widgets::color_picker(
7731            ui,
7732            fields,
7733            "styling.shadow_picker",
7734            &state.styling_shadow_picker,
7735            ext_widgets::ColorPickerOptions::default()
7736                .with_label("Shadow color")
7737                .with_action_prefix("styling.shadow_picker"),
7738        );
7739    }
7740}
7741
7742fn style_stroke_row(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7743    let row = row(ui, parent, "styling.stroke.row", 8.0);
7744    widgets::label(
7745        ui,
7746        row,
7747        "styling.stroke.label",
7748        "Stroke",
7749        text(12.0, color(166, 176, 190)),
7750        LayoutStyle::new().with_width(86.0).with_flex_shrink(0.0),
7751    );
7752    style_value_input(
7753        ui,
7754        row,
7755        "styling.stroke",
7756        state.styling.stroke_width,
7757        0.0..4.0,
7758        1,
7759    );
7760    ext_widgets::color_edit_button(
7761        ui,
7762        row,
7763        "styling.stroke_color_button",
7764        state.styling.stroke_color(),
7765        color_mini_button_options("styling.stroke_color_button")
7766            .with_format(ext_widgets::ColorValueFormat::Rgba)
7767            .accessibility_label("Pick stroke color"),
7768    );
7769    let mut options = widgets::SliderOptions::default()
7770        .with_layout(
7771            LayoutStyle::new()
7772                .with_width(60.0)
7773                .with_height(20.0)
7774                .with_flex_shrink(0.0),
7775        )
7776        .with_value_edit_action("styling.stroke");
7777    options.fill_color = color(120, 170, 230);
7778    widgets::slider(
7779        ui,
7780        row,
7781        "styling.stroke.slider",
7782        (state.styling.stroke_width / 4.0).clamp(0.0, 1.0),
7783        0.0..1.0,
7784        options,
7785    );
7786}
7787
7788fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7789    ui.add_child(
7790        parent,
7791        UiNode::container(
7792            name,
7793            LayoutStyle::row()
7794                .with_width_percent(1.0)
7795                .with_flex_shrink(0.0)
7796                .padding(4.0)
7797                .gap(8.0),
7798        )
7799        .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
7800    )
7801}
7802
7803fn style_group_title(
7804    ui: &mut UiDocument,
7805    parent: UiNodeId,
7806    name: impl Into<String>,
7807    label: &'static str,
7808) {
7809    widgets::label(
7810        ui,
7811        parent,
7812        name,
7813        label,
7814        text(12.0, color(166, 176, 190)),
7815        LayoutStyle::new()
7816            .with_width(88.0)
7817            .with_flex_shrink(0.0)
7818            .with_height(22.0),
7819    );
7820}
7821
7822fn style_color_button_row(
7823    ui: &mut UiDocument,
7824    parent: UiNodeId,
7825    action: &'static str,
7826    label: &'static str,
7827    value: ColorRgba,
7828    accessibility_label: &'static str,
7829) {
7830    let row = row(ui, parent, format!("{action}.row"), 8.0);
7831    if !label.is_empty() {
7832        widgets::label(
7833            ui,
7834            row,
7835            format!("{action}.label"),
7836            label,
7837            text(12.0, color(166, 176, 190)),
7838            LayoutStyle::new()
7839                .with_width(86.0)
7840                .with_flex_shrink(0.0)
7841                .with_height(24.0),
7842        );
7843    }
7844    ext_widgets::color_edit_button(
7845        ui,
7846        row,
7847        action,
7848        value,
7849        color_mini_button_options(action)
7850            .with_format(ext_widgets::ColorValueFormat::Rgba)
7851            .accessibility_label(accessibility_label),
7852    );
7853    widgets::label(
7854        ui,
7855        row,
7856        format!("{action}.value"),
7857        ext_widgets::color_picker::format_hex_color(value, value.a < 255),
7858        text(12.0, color(226, 232, 242)),
7859        LayoutStyle::new().with_width(96.0).with_height(24.0),
7860    );
7861}
7862
7863fn style_number_row(
7864    ui: &mut UiDocument,
7865    parent: UiNodeId,
7866    name: &'static str,
7867    label: &'static str,
7868    value: f32,
7869    range: std::ops::Range<f32>,
7870    decimals: u8,
7871) {
7872    let row = row(ui, parent, format!("{name}.row"), 6.0);
7873    widgets::label(
7874        ui,
7875        row,
7876        format!("{name}.label"),
7877        label,
7878        text(12.0, color(166, 176, 190)),
7879        LayoutStyle::new().with_width(48.0).with_height(22.0),
7880    );
7881    style_value_input(ui, row, name, value, range, decimals);
7882}
7883
7884fn style_inline_number(
7885    ui: &mut UiDocument,
7886    parent: UiNodeId,
7887    name: &'static str,
7888    label: &'static str,
7889    value: f32,
7890    range: std::ops::Range<f32>,
7891    decimals: u8,
7892) {
7893    let row = row(ui, parent, format!("{name}.inline"), 3.0);
7894    widgets::label(
7895        ui,
7896        row,
7897        format!("{name}.inline_label"),
7898        format!("{label}:"),
7899        text(12.0, color(166, 176, 190)),
7900        LayoutStyle::new()
7901            .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
7902            .with_height(22.0),
7903    );
7904    style_value_input(ui, row, name, value, range, decimals);
7905}
7906
7907fn style_value_input(
7908    ui: &mut UiDocument,
7909    parent: UiNodeId,
7910    name: &'static str,
7911    value: f32,
7912    range: std::ops::Range<f32>,
7913    decimals: u8,
7914) {
7915    let mut options = widgets::DragValueOptions::default()
7916        .with_layout(
7917            LayoutStyle::new()
7918                .with_width(42.0)
7919                .with_height(22.0)
7920                .with_flex_shrink(0.0),
7921        )
7922        .with_range(ext_widgets::NumericRange::new(
7923            f64::from(range.start),
7924            f64::from(range.end),
7925        ))
7926        .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
7927        .with_action(name);
7928    options.text_style = text(12.0, color(226, 232, 242));
7929    widgets::drag_value_input(ui, parent, name, f64::from(value), options);
7930}
7931
7932fn style_compact_checkbox(
7933    ui: &mut UiDocument,
7934    parent: UiNodeId,
7935    name: &'static str,
7936    label: &'static str,
7937    checked: bool,
7938) {
7939    let mut options = widgets::CheckboxOptions::default().with_action(name);
7940    options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
7941    options.text_style = text(12.0, color(220, 228, 238));
7942    widgets::checkbox(ui, parent, name, label, checked, options);
7943}
7944
7945fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
7946    ext_widgets::ColorButtonOptions::default()
7947        .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
7948        .with_swatch_size(UiSize::new(22.0, 18.0))
7949        .with_action(action)
7950        .show_label(false)
7951}
7952
7953fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
7954    let outer = styling.outer_edges();
7955    let inner = styling.inner_edges();
7956    let frame = UiRect::new(
7957        22.0 + outer[0],
7958        28.0 + outer[2],
7959        108.0 + inner[0] + inner[1],
7960        40.0 + inner[2] + inner[3],
7961    );
7962    let text_rect = UiRect::new(
7963        frame.x + inner[0],
7964        frame.y + inner[2],
7965        (frame.width - inner[0] - inner[1]).max(1.0),
7966        (frame.height - inner[2] - inner[3]).max(1.0),
7967    );
7968    ui.add_child(
7969        parent,
7970        UiNode::scene(
7971            "styling.preview.scene",
7972            vec![
7973                ScenePrimitive::Rect(
7974                    PaintRect::solid(frame, styling.fill_color())
7975                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7976                            styling.stroke_color(),
7977                            styling.stroke_width,
7978                        )))
7979                        .corner_radii(styling.radii())
7980                        .effect(PaintEffect::shadow(
7981                            styling.shadow_color(),
7982                            UiPoint::new(styling.shadow_x, styling.shadow_y),
7983                            styling.shadow_blur,
7984                            styling.shadow_spread,
7985                        )),
7986                ),
7987                ScenePrimitive::Text(
7988                    PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
7989                        .horizontal_align(TextHorizontalAlign::Center)
7990                        .vertical_align(TextVerticalAlign::Center)
7991                        .multiline(false),
7992                ),
7993            ],
7994            LayoutStyle::new()
7995                .with_width_percent(1.0)
7996                .with_height(180.0)
7997                .with_flex_shrink(0.0),
7998        ),
7999    );
8000}
Source

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

Examples found in repository?
examples/showcase.rs (lines 5189-5192)
5178fn animation_panel_primitives(offset: UiPoint) -> Vec<ScenePrimitive> {
5179    vec![ScenePrimitive::Rect(
5180        PaintRect::solid(
5181            UiRect::new(
5182                offset.x,
5183                offset.y,
5184                ANIMATION_PANEL_WIDTH,
5185                ANIMATION_PANEL_HEIGHT,
5186            ),
5187            color(232, 186, 88),
5188        )
5189        .stroke(AlignedStroke::inside(StrokeStyle::new(
5190            color(255, 226, 154),
5191            1.0,
5192        )))
5193        .corner_radii(CornerRadii::uniform(6.0)),
5194    )]
5195}
5196
5197fn list_and_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5198    let body = section(ui, parent, "lists_tables", "Lists and tables");
5199
5200    let scroll_shell = row(ui, body, "lists_tables.scroll_area.shell", 8.0);
5201    let nested_scroll = widgets::scroll_area(
5202        ui,
5203        scroll_shell,
5204        "lists_tables.scroll_area",
5205        ScrollAxes::VERTICAL,
5206        LayoutStyle::column()
5207            .with_width(0.0)
5208            .with_flex_grow(1.0)
5209            .with_height(92.0),
5210    );
5211    ui.node_mut(nested_scroll)
5212        .set_action("lists_tables.scroll_area.scroll");
5213    if let Some(scroll) = ui.node_mut(nested_scroll).scroll_mut() {
5214        scroll.set_offset(UiPoint::new(0.0, state.list_scroll));
5215    }
5216    for index in 0..6 {
5217        widgets::label(
5218            ui,
5219            nested_scroll,
5220            format!("lists_tables.scroll_area.row.{index}"),
5221            format!("Scroll row {}", index + 1),
5222            text(12.0, color(200, 212, 228)),
5223            LayoutStyle::new()
5224                .with_width_percent(1.0)
5225                .with_height(26.0)
5226                .with_flex_shrink(0.0),
5227        );
5228    }
5229    scrollbar_widgets::scrollbar(
5230        ui,
5231        scroll_shell,
5232        "lists_tables.scroll_area.scrollbar",
5233        scroll_state(state.list_scroll, 92.0, 6.0 * 26.0),
5234        scrollbar_widgets::ScrollAxis::Vertical,
5235        scrollbar_widgets::ScrollbarOptions::default()
5236            .with_layout(LayoutStyle::size(8.0, 92.0))
5237            .with_track_size(UiSize::new(8.0, 92.0))
5238            .with_action("lists_tables.scroll_area.scrollbar"),
5239    );
5240
5241    widgets::table_header(ui, body, "lists_tables.table_header", &table_columns());
5242
5243    let virtual_shell = row(ui, body, "lists_tables.virtual_list.shell", 8.0);
5244    let virtual_list = widgets::virtual_list(
5245        ui,
5246        virtual_shell,
5247        "lists_tables.virtual_list",
5248        widgets::VirtualListSpec {
5249            row_count: 24,
5250            row_height: 28.0,
5251            viewport_height: 112.0,
5252            scroll_offset: state.virtual_scroll,
5253            overscan: 1,
5254        },
5255        |ui, row_parent, row| {
5256            widgets::label(
5257                ui,
5258                row_parent,
5259                format!("lists_tables.virtual_list.row.{row}"),
5260                format!("Virtual row {}", row + 1),
5261                text(12.0, color(214, 224, 238)),
5262                LayoutStyle::new()
5263                    .with_width_percent(1.0)
5264                    .with_height(28.0)
5265                    .with_flex_shrink(0.0),
5266            );
5267        },
5268    );
5269    ui.node_mut(virtual_list)
5270        .set_action("lists_tables.virtual_list.scroll");
5271    scrollbar_widgets::scrollbar(
5272        ui,
5273        virtual_shell,
5274        "lists_tables.virtual_list.scrollbar",
5275        scroll_state(state.virtual_scroll, 112.0, 24.0 * 28.0),
5276        scrollbar_widgets::ScrollAxis::Vertical,
5277        scrollbar_widgets::ScrollbarOptions::default()
5278            .with_layout(LayoutStyle::size(8.0, 112.0))
5279            .with_track_size(UiSize::new(8.0, 112.0))
5280            .with_action("lists_tables.virtual_list.scrollbar"),
5281    );
5282
5283    let table_shell = row(ui, body, "lists_tables.data_table.shell", 8.0);
5284    let table_scroll = widgets::scroll_area(
5285        ui,
5286        table_shell,
5287        "lists_tables.data_table",
5288        ScrollAxes::VERTICAL,
5289        LayoutStyle::column()
5290            .with_width(0.0)
5291            .with_flex_grow(1.0)
5292            .with_height(128.0),
5293    );
5294    ui.node_mut(table_scroll)
5295        .set_action("lists_tables.data_table.scroll");
5296    if let Some(scroll) = ui.node_mut(table_scroll).scroll_mut() {
5297        scroll.set_offset(UiPoint::new(0.0, state.table_scroll));
5298    }
5299    for row_index in 0..16 {
5300        data_table_row(ui, table_scroll, row_index, state);
5301    }
5302    scrollbar_widgets::scrollbar(
5303        ui,
5304        table_shell,
5305        "lists_tables.data_table.scrollbar",
5306        scroll_state(state.table_scroll, 128.0, 16.0 * 28.0),
5307        scrollbar_widgets::ScrollAxis::Vertical,
5308        scrollbar_widgets::ScrollbarOptions::default()
5309            .with_layout(LayoutStyle::size(8.0, 128.0))
5310            .with_track_size(UiSize::new(8.0, 128.0))
5311            .with_action("lists_tables.data_table.scrollbar"),
5312    );
5313
5314    let virtual_controls = wrapping_row(ui, body, "lists_tables.virtualized_table.controls", 8.0);
5315    button(
5316        ui,
5317        virtual_controls,
5318        "lists_tables.virtualized_table.sort.name",
5319        if state.virtual_table_descending {
5320            "Name desc"
5321        } else {
5322            "Name asc"
5323        },
5324        "lists_tables.virtualized_table.sort.name",
5325        button_visual(38, 52, 70),
5326    );
5327    button(
5328        ui,
5329        virtual_controls,
5330        "lists_tables.virtualized_table.filter.status",
5331        if state.virtual_table_ready_only {
5332            "Ready only"
5333        } else {
5334            "All status"
5335        },
5336        "lists_tables.virtualized_table.filter.status",
5337        button_visual(38, 52, 70),
5338    );
5339    button(
5340        ui,
5341        virtual_controls,
5342        "lists_tables.virtualized_table.resize.reset",
5343        "Reset width",
5344        "lists_tables.virtualized_table.resize.reset",
5345        button_visual(38, 52, 70),
5346    );
5347
5348    let columns = virtual_table_columns(state);
5349    let visible_rows = virtual_table_visible_rows(state);
5350    let mut table_options = ext_widgets::DataTableOptions::default()
5351        .with_row_action_prefix("lists_tables.virtualized_table")
5352        .with_cell_action_prefix("lists_tables.virtualized_table")
5353        .with_scroll_action("lists_tables.virtualized_table.scroll");
5354    table_options.layout = LayoutStyle::column()
5355        .with_width(0.0)
5356        .with_flex_grow(1.0)
5357        .with_flex_shrink(1.0);
5358    table_options.selection = state.table_selection.clone();
5359    let virtual_shell = row(ui, body, "lists_tables.virtualized_table.shell", 8.0);
5360    ext_widgets::virtualized_data_table(
5361        ui,
5362        virtual_shell,
5363        "lists_tables.virtualized_table",
5364        &columns,
5365        ext_widgets::VirtualDataTableSpec {
5366            row_count: visible_rows.len(),
5367            row_height: 28.0,
5368            viewport_width: 420.0,
5369            viewport_height: 128.0,
5370            scroll_offset: UiPoint::new(0.0, state.virtual_table_scroll),
5371            overscan_rows: 1,
5372        },
5373        table_options,
5374        |ui, cell_parent, cell| {
5375            let source_row = visible_rows.get(cell.row).copied().unwrap_or(cell.row);
5376            let value = virtual_table_cell_value(source_row, cell.column);
5377            widgets::label(
5378                ui,
5379                cell_parent,
5380                format!(
5381                    "lists_tables.virtualized_table.cell.{}.{}.label",
5382                    cell.row, cell.column
5383                ),
5384                value,
5385                text(12.0, color(220, 228, 238)),
5386                LayoutStyle::new().with_width_percent(1.0),
5387            );
5388        },
5389    );
5390    scrollbar_widgets::scrollbar(
5391        ui,
5392        virtual_shell,
5393        "lists_tables.virtualized_table.scrollbar",
5394        scroll_state(
5395            state.virtual_table_scroll,
5396            128.0,
5397            visible_rows.len() as f32 * 28.0,
5398        ),
5399        scrollbar_widgets::ScrollAxis::Vertical,
5400        scrollbar_widgets::ScrollbarOptions::default()
5401            .with_layout(LayoutStyle::size(8.0, 158.0))
5402            .with_track_size(UiSize::new(8.0, 158.0))
5403            .with_action("lists_tables.virtualized_table.scrollbar"),
5404    );
5405}
5406
5407fn data_table_row(ui: &mut UiDocument, parent: UiNodeId, row_index: usize, state: &ShowcaseState) {
5408    let selected = state.table_selection.contains_row(row_index);
5409    let row = ui.add_child(
5410        parent,
5411        UiNode::container(
5412            format!("lists_tables.data_table.row.{row_index}"),
5413            LayoutStyle::row()
5414                .with_width_percent(1.0)
5415                .with_height(28.0)
5416                .with_flex_shrink(0.0),
5417        )
5418        .with_input(operad::InputBehavior::BUTTON)
5419        .with_action(format!("lists_tables.data_table.row.{row_index}"))
5420        .with_visual(if selected {
5421            UiVisual::panel(color(45, 73, 109), None, 0.0)
5422        } else {
5423            UiVisual::TRANSPARENT
5424        }),
5425    );
5426    let values = [
5427        format!("Item {}", row_index + 1),
5428        if row_index % 2 == 0 {
5429            "Ready".to_string()
5430        } else {
5431            "Pending".to_string()
5432        },
5433        format!("{}%", 40 + row_index * 3),
5434    ];
5435    let widths = [0.42, 0.33, 0.25];
5436    for (column, value) in values.into_iter().enumerate() {
5437        let cell = ui.add_child(
5438            row,
5439            UiNode::container(
5440                format!("lists_tables.data_table.cell.{row_index}.{column}"),
5441                LayoutStyle::new()
5442                    .with_width_percent(widths[column])
5443                    .with_height_percent(1.0)
5444                    .padding(6.0),
5445            )
5446            .with_input(operad::InputBehavior::BUTTON)
5447            .with_action(format!("lists_tables.data_table.cell.{row_index}.{column}")),
5448        );
5449        widgets::label(
5450            ui,
5451            cell,
5452            format!("lists_tables.data_table.cell.{row_index}.{column}.label"),
5453            value,
5454            text(12.0, color(222, 230, 240)),
5455            LayoutStyle::new().with_width_percent(1.0),
5456        );
5457    }
5458}
5459
5460#[allow(clippy::field_reassign_with_default)]
5461fn property_inspector(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5462    let body = section(ui, parent, "property_inspector", "Property inspector");
5463    widgets::label(
5464        ui,
5465        body,
5466        "property_inspector.target",
5467        "Inspecting: Styling preview",
5468        text(12.0, color(196, 210, 230)),
5469        LayoutStyle::new().with_width_percent(1.0),
5470    );
5471    let mut options = ext_widgets::PropertyInspectorOptions::default();
5472    options.selected_index = Some(0);
5473    options.label_width = 120.0;
5474    options.row_height = 30.0;
5475    ext_widgets::property_inspector_grid(
5476        ui,
5477        body,
5478        "property_inspector.grid",
5479        &[
5480            ext_widgets::PropertyGridRow::new("target", "Widget", "Button preview").read_only(),
5481            ext_widgets::PropertyGridRow::new(
5482                "inner",
5483                "Inner margin",
5484                format!("{:.0}px", state.styling.inner_margin),
5485            )
5486            .with_kind(ext_widgets::PropertyValueKind::Number),
5487            ext_widgets::PropertyGridRow::new(
5488                "outer",
5489                "Outer margin",
5490                format!("{:.0}px", state.styling.outer_margin),
5491            )
5492            .with_kind(ext_widgets::PropertyValueKind::Number),
5493            ext_widgets::PropertyGridRow::new(
5494                "radius",
5495                "Corner radius",
5496                format!("{:.0}px", state.styling.corner_radius),
5497            )
5498            .with_kind(ext_widgets::PropertyValueKind::Number),
5499            ext_widgets::PropertyGridRow::new(
5500                "stroke",
5501                "Stroke",
5502                format!("{:.1}px", state.styling.stroke_width),
5503            )
5504            .with_kind(ext_widgets::PropertyValueKind::Number)
5505            .changed(),
5506            ext_widgets::PropertyGridRow::new("state", "Source", "Styling widget").read_only(),
5507        ],
5508        options,
5509    );
5510}
5511
5512fn diagnostics_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5513    let body = section(ui, parent, "diagnostics", "Diagnostics");
5514
5515    widgets::label(
5516        ui,
5517        body,
5518        "diagnostics.layout.title",
5519        "Layout and animation inspector",
5520        text(14.0, color(222, 230, 240)),
5521        LayoutStyle::new().with_width_percent(1.0),
5522    );
5523    let debug_snapshot = &state.diagnostics_snapshot;
5524    ext_widgets::debug_inspector_panel(
5525        ui,
5526        body,
5527        "diagnostics.inspector",
5528        debug_snapshot,
5529        ext_widgets::DebugInspectorPanelOptions {
5530            selected_node: Some("diagnostics.sample.preview".to_owned()),
5531            label_width: 104.0,
5532            max_layout_rows: 5,
5533            max_animation_rows: 1,
5534            show_animation: false,
5535            ..Default::default()
5536        },
5537    );
5538    ext_widgets::animation_state_graph_panel(
5539        ui,
5540        body,
5541        "diagnostics.animation.graph",
5542        debug_snapshot.animation("diagnostics.sample.preview"),
5543        ext_widgets::AnimationStateGraphPanelOptions {
5544            state_width: 72.0,
5545            state_height: 28.0,
5546            edge_row_height: 22.0,
5547            max_edges: 2,
5548            action_prefix: Some("diagnostics.animation.graph".to_owned()),
5549            ..Default::default()
5550        },
5551    );
5552    ext_widgets::animation_inspector_controls_panel(
5553        ui,
5554        body,
5555        "diagnostics.animation.controls",
5556        debug_snapshot.animation("diagnostics.sample.preview"),
5557        ext_widgets::AnimationInspectorControlsOptions {
5558            max_inputs: 3,
5559            paused: state.diagnostics_animation_paused,
5560            scrub_progress: Some(state.diagnostics_animation_scrub),
5561            action_prefix: Some("diagnostics.animation.controls".to_owned()),
5562            ..Default::default()
5563        },
5564    );
5565    widgets::label(
5566        ui,
5567        body,
5568        "diagnostics.animation.controls.status",
5569        format!(
5570            "scrub {:.0}%  hover {:.0}%  pulses {}",
5571            state.diagnostics_animation_scrub * 100.0,
5572            state.diagnostics_animation_hover * 100.0,
5573            state.diagnostics_animation_pulse_count
5574        ),
5575        text(12.0, color(166, 180, 198)),
5576        LayoutStyle::new().with_width_percent(1.0),
5577    );
5578
5579    widgets::label(
5580        ui,
5581        body,
5582        "diagnostics.a11y.title",
5583        "Accessibility overlay",
5584        text(14.0, color(222, 230, 240)),
5585        LayoutStyle::new().with_width_percent(1.0),
5586    );
5587    let mut overlay_preview_style = UiNodeStyle::from(
5588        LayoutStyle::new()
5589            .with_width(320.0)
5590            .with_height(140.0)
5591            .with_flex_shrink(0.0),
5592    );
5593    overlay_preview_style.set_clip(ClipBehavior::Clip);
5594    let overlay_preview = ui.add_child(
5595        body,
5596        UiNode::container("diagnostics.a11y.preview", overlay_preview_style).with_visual(
5597            UiVisual::panel(
5598                color(12, 17, 24),
5599                Some(StrokeStyle::new(color(47, 62, 82), 1.0)),
5600                4.0,
5601            ),
5602        ),
5603    );
5604    let mut overlay_options = ext_widgets::AccessibilityDebugOverlayOptions {
5605        action_prefix: Some("diagnostics.a11y.visual".to_owned()),
5606        ..Default::default()
5607    };
5608    overlay_options.show_labels = false;
5609    ext_widgets::accessibility_debug_overlay(
5610        ui,
5611        overlay_preview,
5612        "diagnostics.a11y.visual",
5613        &debug_snapshot,
5614        overlay_options,
5615    );
5616    ext_widgets::accessibility_overlay_panel(
5617        ui,
5618        body,
5619        "diagnostics.a11y",
5620        &debug_snapshot,
5621        ext_widgets::AccessibilityOverlayPanelOptions {
5622            label_width: 118.0,
5623            max_rows: 1,
5624            action_prefix: Some("diagnostics.a11y".to_owned()),
5625            ..Default::default()
5626        },
5627    );
5628
5629    let diagnostic_columns = ui.add_child(
5630        body,
5631        UiNode::container(
5632            "diagnostics.columns",
5633            LayoutStyle::column()
5634                .with_width_percent(1.0)
5635                .with_flex_shrink(0.0)
5636                .gap(10.0),
5637        ),
5638    );
5639    let command_column = ui.add_child(
5640        diagnostic_columns,
5641        UiNode::container(
5642            "diagnostics.commands.column",
5643            LayoutStyle::column()
5644                .with_width_percent(1.0)
5645                .with_flex_shrink(0.0)
5646                .gap(8.0),
5647        ),
5648    );
5649    let theme_column = ui.add_child(
5650        diagnostic_columns,
5651        UiNode::container(
5652            "diagnostics.theme.column",
5653            LayoutStyle::column()
5654                .with_width_percent(1.0)
5655                .with_flex_shrink(0.0)
5656                .gap(8.0),
5657        ),
5658    );
5659
5660    widgets::label(
5661        ui,
5662        command_column,
5663        "diagnostics.commands.title",
5664        "Command registry",
5665        text(14.0, color(222, 230, 240)),
5666        LayoutStyle::new().with_width_percent(1.0),
5667    );
5668    let registry = diagnostics_command_registry();
5669    ext_widgets::command_diagnostics_panel(
5670        ui,
5671        command_column,
5672        "diagnostics.commands",
5673        &registry,
5674        &[CommandScope::Global, CommandScope::Panel],
5675        &ShortcutFormatter::default(),
5676        ext_widgets::CommandDiagnosticsPanelOptions {
5677            label_width: 92.0,
5678            max_command_rows: 3,
5679            max_conflict_rows: 1,
5680            action_prefix: Some("diagnostics.commands".to_owned()),
5681            ..Default::default()
5682        },
5683    );
5684
5685    widgets::label(
5686        ui,
5687        theme_column,
5688        "diagnostics.theme.title",
5689        "Theme editor",
5690        text(14.0, color(222, 230, 240)),
5691        LayoutStyle::new().with_width_percent(1.0),
5692    );
5693    let theme_snapshot = DebugThemeSnapshot::from_theme(&Theme::dark());
5694    ext_widgets::theme_editor_panel(
5695        ui,
5696        theme_column,
5697        "diagnostics.theme",
5698        &theme_snapshot,
5699        ext_widgets::ThemeEditorPanelOptions {
5700            label_width: 92.0,
5701            max_token_rows: 1,
5702            max_component_rows: 1,
5703            action_prefix: Some("diagnostics.theme".to_owned()),
5704            ..Default::default()
5705        },
5706    );
5707}
5708
5709fn diagnostics_sample_snapshot(state: &ShowcaseState) -> DebugInspectorSnapshot {
5710    diagnostics_sample_snapshot_for(
5711        state.diagnostics_animation_hover,
5712        state.diagnostics_animation_active,
5713    )
5714}
5715
5716fn diagnostics_sample_snapshot_for(hover: f32, active: bool) -> DebugInspectorSnapshot {
5717    let mut sample = UiDocument::new(root_style(320.0, 180.0));
5718    let card = sample.add_child(
5719        sample.root(),
5720        UiNode::container(
5721            "diagnostics.sample.card",
5722            LayoutStyle::column()
5723                .with_width_percent(1.0)
5724                .with_height(120.0)
5725                .padding(12.0)
5726                .gap(8.0),
5727        )
5728        .with_visual(UiVisual::panel(
5729            color(16, 22, 30),
5730            Some(StrokeStyle::new(color(62, 77, 98), 1.0)),
5731            6.0,
5732        ))
5733        .with_accessibility(
5734            AccessibilityMeta::new(AccessibilityRole::Group).label("Diagnostics sample"),
5735        ),
5736    );
5737    sample.add_child(
5738        card,
5739        UiNode::container(
5740            "diagnostics.sample.preview",
5741            LayoutStyle::new().with_width(160.0).with_height(38.0),
5742        )
5743        .with_input(InputBehavior::BUTTON)
5744        .with_visual(UiVisual::panel(
5745            color(52, 112, 180),
5746            Some(StrokeStyle::new(color(116, 183, 255), 1.0)),
5747            5.0,
5748        ))
5749        .with_accessibility(
5750            AccessibilityMeta::new(AccessibilityRole::Button)
5751                .label("Preview action")
5752                .focusable(),
5753        )
5754        .with_animation(
5755            AnimationMachine::new(
5756                vec![
5757                    AnimationState::new(
5758                        "idle",
5759                        AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0),
5760                    ),
5761                    AnimationState::new(
5762                        "hot",
5763                        AnimatedValues::new(0.92, UiPoint::new(18.0, 0.0), 1.08),
5764                    ),
5765                ],
5766                vec![AnimationTransition::when(
5767                    "idle",
5768                    "hot",
5769                    AnimationCondition::bool("active", true),
5770                    0.18,
5771                )],
5772                "idle",
5773            )
5774            .expect("sample animation")
5775            .with_number_input("hover", hover)
5776            .with_blend_binding(AnimationBlendBinding::new("hover", "idle", "hot"))
5777            .with_bool_input("active", active)
5778            .with_trigger_input("pulse"),
5779        ),
5780    );
5781    widgets::label(
5782        &mut sample,
5783        card,
5784        "diagnostics.sample.label",
5785        "Sample node",
5786        text(12.0, color(198, 210, 226)),
5787        LayoutStyle::new().with_width_percent(1.0),
5788    );
5789    sample
5790        .compute_layout(UiSize::new(320.0, 180.0), &mut ApproxTextMeasurer)
5791        .expect("sample layout");
5792    DebugInspectorSnapshot::from_document(&sample, &mut ApproxTextMeasurer)
5793}
5794
5795fn diagnostics_command_registry() -> CommandRegistry {
5796    let mut registry = CommandRegistry::new();
5797    registry
5798        .register(
5799            CommandMeta::new("diagnostics.palette", "Open command palette")
5800                .description("Show command search")
5801                .category("Debug"),
5802        )
5803        .expect("command");
5804    registry
5805        .register(
5806            CommandMeta::new("diagnostics.inspect", "Inspect selected node")
5807                .description("Focus the layout inspector")
5808                .category("Debug"),
5809        )
5810        .expect("command");
5811    registry
5812        .register(
5813            CommandMeta::new("diagnostics.record", "Start interaction recording")
5814                .description("Capture replay steps")
5815                .category("Testing"),
5816        )
5817        .expect("command");
5818    registry
5819        .register(CommandMeta::new(
5820            "diagnostics.export_theme",
5821            "Export theme patch",
5822        ))
5823        .expect("command");
5824    registry
5825        .bind_shortcut(
5826            CommandScope::Global,
5827            Shortcut::ctrl('k'),
5828            "diagnostics.palette",
5829        )
5830        .expect("shortcut");
5831    registry
5832        .bind_shortcut(
5833            CommandScope::Panel,
5834            Shortcut::ctrl('i'),
5835            "diagnostics.inspect",
5836        )
5837        .expect("shortcut");
5838    registry
5839        .bind_shortcut(
5840            CommandScope::Panel,
5841            Shortcut::ctrl('r'),
5842            "diagnostics.record",
5843        )
5844        .expect("shortcut");
5845    registry
5846        .disable("diagnostics.export_theme", "No changes to export")
5847        .expect("disable");
5848    registry
5849}
5850
5851fn tree_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5852    let body = section(ui, parent, "trees", "Tree view");
5853    ext_widgets::tree_view(
5854        ui,
5855        body,
5856        "trees.tree_view",
5857        &tree_items(),
5858        &state.tree,
5859        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.tree"),
5860    );
5861    ext_widgets::outliner(
5862        ui,
5863        body,
5864        "trees.outliner",
5865        &tree_items(),
5866        &state.outliner,
5867        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.outliner"),
5868    );
5869    let virtual_state = ext_widgets::TreeViewState::expanded(["root"]);
5870    let virtual_nodes = ext_widgets::virtualized_tree_view(
5871        ui,
5872        body,
5873        "trees.virtual",
5874        &virtual_tree_items(),
5875        &virtual_state,
5876        ext_widgets::VirtualTreeViewSpec::new(24.0, 112.0)
5877            .scroll_offset(state.tree_virtual_scroll)
5878            .overscan_rows(1),
5879        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.virtual"),
5880    );
5881    ui.node_mut(virtual_nodes.body)
5882        .set_action("trees.virtual.scroll");
5883    tree_table_widgets(ui, body, state);
5884}
5885
5886fn tree_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5887    let tree_state = ext_widgets::TreeViewState::expanded(["root", "branch-a"]);
5888    let rows = tree_state.visible_items(&tree_table_items());
5889    let columns = [
5890        ext_widgets::DataTableColumn::new("name", "Name", 220.0),
5891        ext_widgets::DataTableColumn::new("kind", "Kind", 84.0),
5892        ext_widgets::DataTableColumn::new("status", "Status", 92.0),
5893    ];
5894    let mut options = ext_widgets::DataTableOptions::default()
5895        .with_row_action_prefix("trees.table")
5896        .with_cell_action_prefix("trees.table");
5897    options.layout = LayoutStyle::column()
5898        .with_width_percent(1.0)
5899        .with_height(132.0)
5900        .with_flex_shrink(0.0);
5901    ext_widgets::virtualized_data_table(
5902        ui,
5903        parent,
5904        "trees.table",
5905        &columns,
5906        ext_widgets::VirtualDataTableSpec {
5907            row_count: rows.len(),
5908            row_height: 24.0,
5909            viewport_width: 396.0,
5910            viewport_height: 96.0,
5911            scroll_offset: UiPoint::new(0.0, state.tree_virtual_scroll),
5912            overscan_rows: 1,
5913        },
5914        options,
5915        |ui, cell_parent, cell| {
5916            let value = rows
5917                .get(cell.row)
5918                .map(|item| tree_table_cell_value(item, cell.column))
5919                .unwrap_or_default();
5920            widgets::label(
5921                ui,
5922                cell_parent,
5923                format!("trees.table.cell.{}.{}.label", cell.row, cell.column),
5924                value,
5925                text(12.0, color(220, 228, 238)),
5926                LayoutStyle::new().with_width_percent(1.0),
5927            );
5928        },
5929    );
5930}
5931
5932fn tree_table_cell_value(item: &ext_widgets::TreeVisibleItem, column: usize) -> String {
5933    match column {
5934        0 => format!("{}{}", "  ".repeat(item.depth), item.label),
5935        1 => {
5936            if item.has_children() {
5937                "Folder".to_owned()
5938            } else {
5939                "File".to_owned()
5940            }
5941        }
5942        _ => {
5943            if item.disabled {
5944                "Locked".to_owned()
5945            } else if item.expanded {
5946                "Expanded".to_owned()
5947            } else {
5948                "Ready".to_owned()
5949            }
5950        }
5951    }
5952}
5953
5954fn tab_split_dock_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5955    let body = section_with_min_viewport(
5956        ui,
5957        parent,
5958        "layout_widgets",
5959        "Dock workspace",
5960        UiSize::new(546.0, 360.0),
5961    );
5962    let shell = ui.add_child(
5963        body,
5964        UiNode::container(
5965            "layout_widgets.dock_shell",
5966            LayoutStyle::column()
5967                .with_width_percent(1.0)
5968                .with_height(360.0)
5969                .with_flex_shrink(0.0),
5970        )
5971        .with_visual(UiVisual::panel(
5972            color(13, 17, 23),
5973            Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
5974            0.0,
5975        )),
5976    );
5977
5978    let mut panels = base_layout_dock_panels();
5979    state.layout_dock.apply_order_to_panels(&mut panels);
5980    state.layout_dock.apply_visibility_to_panels(&mut panels);
5981
5982    let mut drawer_options = ext_widgets::DockDrawerRailOptions::default();
5983    drawer_options.layout = LayoutStyle::row()
5984        .with_width_percent(1.0)
5985        .with_height(34.0)
5986        .with_padding(4.0)
5987        .with_gap(4.0);
5988    ext_widgets::dock_drawer_rail(
5989        ui,
5990        shell,
5991        "layout_widgets.dock.drawers",
5992        &[
5993            ext_widgets::DockDrawerDescriptor::new(
5994                "inspector",
5995                "Inspector",
5996                "inspector",
5997                ext_widgets::DockSide::Left,
5998            )
5999            .open(!state.layout_dock.is_hidden("inspector"))
6000            .with_action("layout_widgets.drawer.inspector"),
6001            ext_widgets::DockDrawerDescriptor::new(
6002                "assets",
6003                "Assets",
6004                "assets",
6005                ext_widgets::DockSide::Right,
6006            )
6007            .open(!state.layout_dock.is_hidden("assets"))
6008            .with_action("layout_widgets.drawer.assets"),
6009        ],
6010        drawer_options,
6011    );
6012
6013    let mut options = ext_widgets::DockWorkspaceOptions::default();
6014    options.layout = LayoutStyle::column()
6015        .with_width_percent(1.0)
6016        .with_height(0.0)
6017        .with_flex_grow(1.0);
6018    options.show_titles = false;
6019    options.panel_visual = UiVisual::panel(
6020        color(18, 22, 29),
6021        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
6022        0.0,
6023    );
6024    options.center_visual = UiVisual::panel(
6025        color(15, 19, 25),
6026        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
6027        0.0,
6028    );
6029
6030    ext_widgets::dock_workspace(
6031        ui,
6032        shell,
6033        "layout_widgets.dock",
6034        &panels,
6035        options,
6036        |ui, parent, panel| match panel.id.as_str() {
6037            "inspector" => egui_panel_contents(
6038                ui,
6039                parent,
6040                "layout.inspector",
6041                "Inspector",
6042                state.layout_inspector_scroll,
6043            ),
6044            "assets" => egui_panel_contents(
6045                ui,
6046                parent,
6047                "layout.assets",
6048                "Assets",
6049                state.layout_assets_scroll,
6050            ),
6051            _ => dock_document_panel(ui, parent, state),
6052        },
6053    );
6054
6055    if let Some(floating) = state.layout_dock.floating_panel("inspector") {
6056        let floating_panel = ui.add_child(
6057            shell,
6058            UiNode::container(
6059                "layout_widgets.floating.inspector",
6060                operad::layout::absolute(
6061                    floating.rect.x,
6062                    floating.rect.y,
6063                    floating.rect.width,
6064                    floating.rect.height,
6065                ),
6066            )
6067            .with_visual(UiVisual::panel(
6068                color(18, 22, 29),
6069                Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
6070                4.0,
6071            )),
6072        );
6073        egui_panel_contents(
6074            ui,
6075            floating_panel,
6076            "layout.inspector_floating",
6077            "Inspector",
6078            state.layout_inspector_scroll,
6079        );
6080    }
6081}
6082
6083fn base_layout_dock_panels() -> Vec<ext_widgets::DockPanelDescriptor> {
6084    vec![
6085        ext_widgets::DockPanelDescriptor::new(
6086            "inspector",
6087            "Inspector",
6088            ext_widgets::DockSide::Left,
6089            120.0,
6090        )
6091        .with_min_size(104.0)
6092        .resizable(true),
6093        ext_widgets::DockPanelDescriptor::center("document", "Document"),
6094        ext_widgets::DockPanelDescriptor::new(
6095            "assets",
6096            "Assets",
6097            ext_widgets::DockSide::Right,
6098            104.0,
6099        )
6100        .with_min_size(94.0)
6101        .resizable(true),
6102    ]
6103}
6104
6105fn dock_document_panel(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6106    let content = ui.add_child(
6107        parent,
6108        UiNode::container(
6109            "layout_widgets.document.content",
6110            LayoutStyle::column()
6111                .with_width_percent(1.0)
6112                .with_height_percent(1.0)
6113                .with_padding(8.0)
6114                .with_gap(8.0),
6115        ),
6116    );
6117
6118    let controls = wrapping_row(ui, content, "layout_widgets.dock.controls", 8.0);
6119    let (action, label) = if state.layout_dock.is_floating("inspector") {
6120        ("layout_widgets.dock_inspector", "Dock inspector")
6121    } else {
6122        ("layout_widgets.float_inspector", "Float inspector")
6123    };
6124    let mut float_button = widgets::ButtonOptions::new(
6125        LayoutStyle::new()
6126            .with_width(132.0)
6127            .with_height(28.0)
6128            .with_flex_shrink(0.0),
6129    )
6130    .with_action(action);
6131    float_button.visual = button_visual(40, 52, 68);
6132    float_button.hovered_visual = Some(button_visual(54, 70, 92));
6133    float_button.text_style = text(12.0, color(232, 238, 248));
6134    widgets::button(
6135        ui,
6136        controls,
6137        "layout_widgets.dock.float_inspector",
6138        label,
6139        float_button,
6140    );
6141
6142    let mut before_button = widgets::ButtonOptions::new(
6143        LayoutStyle::new()
6144            .with_width(136.0)
6145            .with_height(28.0)
6146            .with_flex_shrink(0.0),
6147    )
6148    .with_action("layout_widgets.reorder.assets.before.inspector");
6149    before_button.visual = button_visual(34, 44, 58);
6150    before_button.hovered_visual = Some(button_visual(48, 64, 84));
6151    before_button.text_style = text(12.0, color(232, 238, 248));
6152    widgets::button(
6153        ui,
6154        controls,
6155        "layout_widgets.dock.assets_before_inspector",
6156        "Assets before",
6157        before_button,
6158    );
6159
6160    let mut after_button = widgets::ButtonOptions::new(
6161        LayoutStyle::new()
6162            .with_width(126.0)
6163            .with_height(28.0)
6164            .with_flex_shrink(0.0),
6165    )
6166    .with_action("layout_widgets.reorder.assets.after.inspector");
6167    after_button.visual = button_visual(34, 44, 58);
6168    after_button.hovered_visual = Some(button_visual(48, 64, 84));
6169    after_button.text_style = text(12.0, color(232, 238, 248));
6170    widgets::button(
6171        ui,
6172        controls,
6173        "layout_widgets.dock.assets_after_inspector",
6174        "Assets after",
6175        after_button,
6176    );
6177
6178    let zones = ext_widgets::dock_workspace::dock_workspace_drop_zones(
6179        "layout_widgets.dock",
6180        UiRect::new(0.0, 0.0, 520.0, 340.0),
6181        ext_widgets::DockWorkspaceDragOptions::default()
6182            .allowed_sides([
6183                ext_widgets::DockSide::Left,
6184                ext_widgets::DockSide::Right,
6185                ext_widgets::DockSide::Center,
6186            ])
6187            .edge_thickness(44.0),
6188    );
6189    let targets = wrapping_row(ui, content, "layout_widgets.dock.targets", 6.0);
6190    for zone in zones {
6191        dock_drop_target_chip(ui, targets, &zone);
6192    }
6193
6194    let mut panels = base_layout_dock_panels();
6195    state.layout_dock.apply_order_to_panels(&mut panels);
6196    let reorder_targets: Vec<_> = [
6197        ext_widgets::DockSide::Left,
6198        ext_widgets::DockSide::Right,
6199        ext_widgets::DockSide::Center,
6200    ]
6201    .into_iter()
6202    .flat_map(|side| {
6203        ext_widgets::dock_workspace::dock_panel_reorder_drop_targets(
6204            "layout_widgets.dock",
6205            &panels,
6206            side,
6207            UiRect::new(0.0, 0.0, 180.0, 120.0),
6208            ext_widgets::DockWorkspaceReorderOptions::default().target_thickness(20.0),
6209        )
6210    })
6211    .collect();
6212    let reorder_row = wrapping_row(ui, content, "layout_widgets.dock.reorder_targets", 6.0);
6213    for target in reorder_targets {
6214        dock_reorder_target_chip(ui, reorder_row, &target);
6215    }
6216
6217    let tabs = [
6218        ext_widgets::TabItem::new("preview", "Preview"),
6219        ext_widgets::TabItem::new("log", "Output").dirty(),
6220        ext_widgets::TabItem::new("settings", "Settings").closable(),
6221    ];
6222    let mut tab_options = ext_widgets::TabGroupOptions::default();
6223    tab_options.layout = LayoutStyle::column()
6224        .with_width_percent(1.0)
6225        .with_height(0.0)
6226        .with_flex_grow(1.0);
6227    tab_options.tab_strip_height = 30.0;
6228    tab_options.min_tab_width = 92.0;
6229    tab_options.text_style = text(12.0, color(226, 234, 246));
6230    tab_options.muted_text_style = text(12.0, color(150, 162, 178));
6231    ext_widgets::tab_group(
6232        ui,
6233        content,
6234        "layout_widgets.document.tabs",
6235        &tabs,
6236        ext_widgets::TabGroupState::selected(0),
6237        tab_options,
6238        |ui, panel, _index| {
6239            widgets::label(
6240                ui,
6241                panel,
6242                "layout_widgets.document.tabs.preview.body",
6243                "Workspace preview",
6244                text(12.0, color(190, 202, 218)),
6245                LayoutStyle::new().with_width_percent(1.0).with_height(26.0),
6246            );
6247        },
6248    );
6249}
6250
6251fn dock_drop_target_chip(
6252    ui: &mut UiDocument,
6253    parent: UiNodeId,
6254    zone: &ext_widgets::DockWorkspaceDropZone,
6255) -> UiNodeId {
6256    let chip = ui.add_child(
6257        parent,
6258        UiNode::container(
6259            format!("{}.chip", zone.target.id.as_str()),
6260            LayoutStyle::row()
6261                .with_width(78.0)
6262                .with_height(26.0)
6263                .with_padding(6.0)
6264                .with_flex_shrink(0.0),
6265        )
6266        .with_input(InputBehavior::BUTTON)
6267        .with_visual(UiVisual::panel(
6268            color(24, 32, 42),
6269            Some(StrokeStyle::new(color(78, 94, 116), 1.0)),
6270            4.0,
6271        ))
6272        .with_accessibility(zone.target.accessibility_meta()),
6273    );
6274    widgets::label(
6275        ui,
6276        chip,
6277        format!("{}.label", zone.target.id.as_str()),
6278        dock_drop_target_short_label(zone.placement),
6279        text(11.0, color(206, 216, 230)),
6280        LayoutStyle::new().with_width_percent(1.0),
6281    );
6282    chip
6283}
6284
6285fn dock_reorder_target_chip(
6286    ui: &mut UiDocument,
6287    parent: UiNodeId,
6288    target: &ext_widgets::DockPanelReorderTarget,
6289) -> UiNodeId {
6290    let chip = ui.add_child(
6291        parent,
6292        UiNode::container(
6293            format!("{}.chip", target.target.id.as_str()),
6294            LayoutStyle::row()
6295                .with_width(104.0)
6296                .with_height(26.0)
6297                .with_padding(6.0)
6298                .with_flex_shrink(0.0),
6299        )
6300        .with_input(InputBehavior::BUTTON)
6301        .with_visual(UiVisual::panel(
6302            color(22, 34, 42),
6303            Some(StrokeStyle::new(color(80, 112, 128), 1.0)),
6304            4.0,
6305        ))
6306        .with_accessibility(target.target.accessibility_meta()),
6307    );
6308    widgets::label(
6309        ui,
6310        chip,
6311        format!("{}.label", target.target.id.as_str()),
6312        dock_reorder_target_short_label(target),
6313        text(11.0, color(206, 216, 230)),
6314        LayoutStyle::new().with_width_percent(1.0),
6315    );
6316    chip
6317}
6318
6319fn dock_drop_target_short_label(placement: ext_widgets::DockDropPlacement) -> &'static str {
6320    match placement {
6321        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Left) => "Left",
6322        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Right) => "Right",
6323        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Center) => "Center",
6324        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Top) => "Top",
6325        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Bottom) => "Bottom",
6326        ext_widgets::DockDropPlacement::Floating => "Float",
6327    }
6328}
6329
6330fn dock_reorder_target_short_label(target: &ext_widgets::DockPanelReorderTarget) -> String {
6331    let placement = match target.placement {
6332        ext_widgets::DockPanelReorderPlacement::Before => "Before",
6333        ext_widgets::DockPanelReorderPlacement::After => "After",
6334    };
6335    format!("{placement} {}", target.panel_id)
6336}
6337
6338fn container_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6339    let body = section(ui, parent, "containers", "Containers");
6340
6341    let frame = widgets::frame(
6342        ui,
6343        body,
6344        "containers.frame",
6345        widgets::FrameOptions::default().with_layout(
6346            LayoutStyle::column()
6347                .with_width_percent(1.0)
6348                .with_height(64.0)
6349                .with_padding(8.0)
6350                .with_gap(6.0),
6351        ),
6352    );
6353    widgets::strong_label(
6354        ui,
6355        frame,
6356        "containers.frame.title",
6357        "Frame",
6358        LayoutStyle::new().with_width_percent(1.0),
6359    );
6360    widgets::weak_label(
6361        ui,
6362        frame,
6363        "containers.frame.body",
6364        "Default framed surface with padding, stroke, and clipping.",
6365        LayoutStyle::new().with_width_percent(1.0),
6366    );
6367
6368    let group = widgets::group(ui, body, "containers.group");
6369    widgets::label(
6370        ui,
6371        group,
6372        "containers.group.label",
6373        "Group helper",
6374        text(12.0, color(220, 228, 238)),
6375        LayoutStyle::new().with_width_percent(1.0),
6376    );
6377    let generic_panel = widgets::panel(
6378        ui,
6379        body,
6380        "containers.panel",
6381        widgets::PanelOptions::group().with_layout(
6382            LayoutStyle::column()
6383                .with_width_percent(1.0)
6384                .with_height(44.0)
6385                .with_padding(8.0),
6386        ),
6387    );
6388    widgets::label(
6389        ui,
6390        generic_panel,
6391        "containers.panel.label",
6392        "Generic panel",
6393        text(12.0, color(220, 228, 238)),
6394        LayoutStyle::new().with_width_percent(1.0),
6395    );
6396    let group_panel = widgets::group_panel(ui, body, "containers.group_panel");
6397    widgets::label(
6398        ui,
6399        group_panel,
6400        "containers.group_panel.label",
6401        "Group panel",
6402        text(12.0, color(220, 228, 238)),
6403        LayoutStyle::new().with_width_percent(1.0),
6404    );
6405
6406    widgets::separator(
6407        ui,
6408        body,
6409        "containers.separator",
6410        widgets::SeparatorOptions::default(),
6411    );
6412    widgets::spacer(
6413        ui,
6414        body,
6415        "containers.spacer",
6416        LayoutStyle::new()
6417            .with_width_percent(1.0)
6418            .with_height(8.0)
6419            .with_flex_shrink(0.0),
6420    );
6421
6422    let grid = widgets::grid::grid(
6423        ui,
6424        body,
6425        "containers.grid",
6426        widgets::grid::GridOptions::default().with_layout(
6427            LayoutStyle::column()
6428                .with_width_percent(1.0)
6429                .with_height(78.0)
6430                .with_gap(4.0),
6431        ),
6432    );
6433    for row_index in 0..2 {
6434        let row = widgets::grid::grid_row(
6435            ui,
6436            grid,
6437            format!("containers.grid.row.{row_index}"),
6438            widgets::grid::GridRowOptions::default(),
6439        );
6440        for column_index in 0..3 {
6441            widgets::grid::grid_text_cell(
6442                ui,
6443                row,
6444                format!("containers.grid.row.{row_index}.cell.{column_index}"),
6445                format!("R{} C{}", row_index + 1, column_index + 1),
6446                widgets::grid::GridCellOptions {
6447                    text_style: text(12.0, color(214, 224, 238)),
6448                    ..Default::default()
6449                },
6450            );
6451        }
6452    }
6453
6454    widgets::sides(
6455        ui,
6456        body,
6457        "containers.sides",
6458        widgets::SidesOptions::default()
6459            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
6460            .with_gap(8.0)
6461            .with_visual(UiVisual::panel(
6462                color(20, 25, 32),
6463                Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
6464                4.0,
6465            )),
6466        |ui, left| {
6467            widgets::label(
6468                ui,
6469                left,
6470                "containers.sides.left.label",
6471                "Left side",
6472                text(12.0, color(220, 228, 238)),
6473                LayoutStyle::new().with_width_percent(1.0),
6474            );
6475        },
6476        |ui, right| {
6477            widgets::label(
6478                ui,
6479                right,
6480                "containers.sides.right.label",
6481                "Right side",
6482                text(12.0, color(220, 228, 238)),
6483                LayoutStyle::new().with_width_percent(1.0),
6484            );
6485        },
6486    );
6487
6488    widgets::columns(
6489        ui,
6490        body,
6491        "containers.columns",
6492        3,
6493        widgets::ColumnsOptions::default()
6494            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
6495            .with_gap(8.0),
6496        |ui, column, index| {
6497            widgets::label(
6498                ui,
6499                column,
6500                format!("containers.columns.{index}.label"),
6501                format!("Column {}", index + 1),
6502                text(12.0, color(220, 228, 238)),
6503                LayoutStyle::new().with_width_percent(1.0),
6504            );
6505        },
6506    );
6507
6508    let indented = widgets::indented_section(
6509        ui,
6510        body,
6511        "containers.indented",
6512        widgets::IndentOptions::default().with_amount(24.0),
6513    );
6514    widgets::label(
6515        ui,
6516        indented,
6517        "containers.indented.label",
6518        "Indented section",
6519        text(12.0, color(196, 210, 230)),
6520        LayoutStyle::new().with_width_percent(1.0),
6521    );
6522
6523    widgets::resize_container(
6524        ui,
6525        body,
6526        "containers.resize_container",
6527        widgets::ResizeContainerOptions::default().with_layout(
6528            LayoutStyle::column()
6529                .with_width_percent(1.0)
6530                .with_height(92.0)
6531                .with_flex_shrink(0.0),
6532        ),
6533        |ui, content| {
6534            widgets::label(
6535                ui,
6536                content,
6537                "containers.resize_container.label",
6538                "Resize container",
6539                text(12.0, color(220, 228, 238)),
6540                LayoutStyle::new().with_width_percent(1.0),
6541            );
6542        },
6543    );
6544    widgets::container::resize_handle(
6545        ui,
6546        body,
6547        "containers.resize_handle",
6548        widgets::container::ResizeHandleOptions::default()
6549            .with_layout(LayoutStyle::size(20.0, 20.0))
6550            .accessibility_label("Inline resize handle"),
6551    );
6552
6553    widgets::scene(
6554        ui,
6555        body,
6556        "containers.scene",
6557        vec![
6558            ScenePrimitive::Rect(
6559                PaintRect::solid(UiRect::new(8.0, 12.0, 108.0, 46.0), color(48, 112, 184))
6560                    .stroke(AlignedStroke::inside(StrokeStyle::new(
6561                        color(132, 174, 222),
6562                        1.0,
6563                    )))
6564                    .corner_radii(CornerRadii::uniform(6.0)),
6565            ),
6566            ScenePrimitive::Circle {
6567                center: UiPoint::new(150.0, 35.0),
6568                radius: 22.0,
6569                fill: color(111, 203, 159),
6570                stroke: Some(StrokeStyle::new(color(176, 236, 206), 1.0)),
6571            },
6572            ScenePrimitive::Line {
6573                from: UiPoint::new(188.0, 18.0),
6574                to: UiPoint::new(238.0, 52.0),
6575                stroke: StrokeStyle::new(color(232, 186, 88), 3.0),
6576            },
6577        ],
6578        widgets::SceneOptions::default()
6579            .with_layout(LayoutStyle::new().with_width(260.0).with_height(70.0))
6580            .accessibility_label("Scene primitives"),
6581    );
6582
6583    let panel_shell = widgets::frame(
6584        ui,
6585        body,
6586        "containers.panels",
6587        widgets::FrameOptions::default().with_layout(
6588            LayoutStyle::column()
6589                .with_width_percent(1.0)
6590                .with_height(160.0)
6591                .with_padding(0.0)
6592                .with_gap(0.0),
6593        ),
6594    );
6595    let top = widgets::top_panel(ui, panel_shell, "containers.panels.top", 28.0);
6596    widgets::label(
6597        ui,
6598        top,
6599        "containers.panels.top.label",
6600        "Top panel",
6601        text(12.0, color(220, 228, 238)),
6602        LayoutStyle::new().with_width_percent(1.0),
6603    );
6604    let middle = row(ui, panel_shell, "containers.panels.middle", 0.0);
6605    let left = widgets::side_panel(
6606        ui,
6607        middle,
6608        "containers.panels.side",
6609        widgets::SidePanelSide::Left,
6610        90.0,
6611    );
6612    widgets::label(
6613        ui,
6614        left,
6615        "containers.panels.side.label",
6616        "Side",
6617        text(12.0, color(220, 228, 238)),
6618        LayoutStyle::new().with_width_percent(1.0),
6619    );
6620    let left = widgets::left_panel(ui, middle, "containers.panels.left", 90.0);
6621    widgets::label(
6622        ui,
6623        left,
6624        "containers.panels.left.label",
6625        "Left",
6626        text(12.0, color(220, 228, 238)),
6627        LayoutStyle::new().with_width_percent(1.0),
6628    );
6629    let center = widgets::central_panel(ui, middle, "containers.panels.center");
6630    widgets::label(
6631        ui,
6632        center,
6633        "containers.panels.center.label",
6634        "Central panel",
6635        text(12.0, color(220, 228, 238)),
6636        LayoutStyle::new().with_width_percent(1.0),
6637    );
6638    let right = widgets::right_panel(ui, middle, "containers.panels.right", 110.0);
6639    widgets::label(
6640        ui,
6641        right,
6642        "containers.panels.right.label",
6643        "Right",
6644        text(12.0, color(220, 228, 238)),
6645        LayoutStyle::new().with_width_percent(1.0),
6646    );
6647    let bottom = widgets::bottom_panel(ui, panel_shell, "containers.panels.bottom", 28.0);
6648    widgets::label(
6649        ui,
6650        bottom,
6651        "containers.panels.bottom.label",
6652        "Bottom panel",
6653        text(12.0, color(220, 228, 238)),
6654        LayoutStyle::new().with_width_percent(1.0),
6655    );
6656
6657    widgets::scroll_container(
6658        ui,
6659        body,
6660        "containers.scroll_area_with_bars",
6661        state.containers_scroll,
6662        widgets::ScrollContainerOptions::default()
6663            .with_axes(ScrollAxes::BOTH)
6664            .with_layout(LayoutStyle::column().with_width(300.0).with_height(116.0)),
6665        |ui, viewport| {
6666            for index in 0..5 {
6667                widgets::label(
6668                    ui,
6669                    viewport,
6670                    format!("containers.scroll_area_with_bars.row.{index}"),
6671                    format!("Scrollable row {}", index + 1),
6672                    text(12.0, color(200, 212, 228)),
6673                    LayoutStyle::new()
6674                        .with_width(420.0)
6675                        .with_height(28.0)
6676                        .with_flex_shrink(0.0),
6677                );
6678            }
6679        },
6680    );
6681
6682    let area_host = ui.add_child(
6683        body,
6684        UiNode::container(
6685            "containers.area.host",
6686            LayoutStyle::new()
6687                .with_width_percent(1.0)
6688                .with_height(82.0)
6689                .with_flex_shrink(0.0),
6690        )
6691        .with_visual(UiVisual::panel(
6692            color(17, 20, 25),
6693            Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
6694            4.0,
6695        )),
6696    );
6697    widgets::container::area(
6698        ui,
6699        area_host,
6700        "containers.area",
6701        widgets::container::AreaOptions::new(UiRect::new(14.0, 14.0, 180.0, 44.0))
6702            .with_visual(UiVisual::panel(color(39, 72, 109), None, 4.0))
6703            .accessibility_label("Absolute positioned area"),
6704        |ui, area| {
6705            widgets::label(
6706                ui,
6707                area,
6708                "containers.area.label",
6709                "Area",
6710                text(12.0, color(238, 244, 252)),
6711                LayoutStyle::new().with_width_percent(1.0),
6712            );
6713        },
6714    );
6715}
6716
6717fn form_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6718    let body = section_with_min_viewport(ui, parent, "forms", "Forms", UiSize::new(390.0, 0.0));
6719    let section = widgets::form_section(
6720        ui,
6721        body,
6722        "forms.profile",
6723        Some("Profile".to_string()),
6724        widgets::FormSectionOptions::default().with_layout(
6725            LayoutStyle::column()
6726                .with_width_percent(1.0)
6727                .with_padding(12.0)
6728                .with_gap(10.0),
6729        ),
6730    );
6731    let status_row = wrapping_row(ui, section.root, "forms.profile.status_flags", 6.0);
6732    form_status_chip(
6733        ui,
6734        status_row,
6735        "forms.profile.status.dirty",
6736        "dirty",
6737        state.form.dirty,
6738    );
6739    form_status_chip(
6740        ui,
6741        status_row,
6742        "forms.profile.status.pending",
6743        "pending",
6744        state.form.pending,
6745    );
6746    form_status_chip(
6747        ui,
6748        status_row,
6749        "forms.profile.status.submitted",
6750        "submitted",
6751        state.form.submitted,
6752    );
6753
6754    let mut name_options = widgets::FormRowOptions::default().required();
6755    if state.form_name_text.text().trim().is_empty() {
6756        name_options = name_options.invalid("Name is required");
6757    }
6758    let name = widgets::form_row(ui, section.root, "forms.profile.name", name_options);
6759    widgets::field_label(
6760        ui,
6761        name,
6762        "forms.profile.name.label",
6763        "Name",
6764        widgets::FieldLabelOptions::default().required(),
6765    );
6766    form_text_field(
6767        ui,
6768        name,
6769        "forms.profile.name.input",
6770        &state.form_name_text,
6771        FocusedTextInput::FormName,
6772        state,
6773    );
6774    if state.form_name_text.text().trim().is_empty() {
6775        widgets::field_validation_message(
6776            ui,
6777            name,
6778            "forms.profile.name.validation",
6779            ValidationMessage::error("Name is required"),
6780            widgets::ValidationMessageOptions::default(),
6781        );
6782    } else {
6783        widgets::field_help_text(
6784            ui,
6785            name,
6786            "forms.profile.name.help",
6787            "Shown in window titles and project lists.",
6788            widgets::FieldHelpOptions::default(),
6789        );
6790    }
6791
6792    let mut email_options = widgets::FormRowOptions::default().required();
6793    if !profile_email_valid(state.form_email_text.text()) {
6794        email_options = email_options.invalid("Use a complete email address");
6795    }
6796    let email = widgets::form_row(ui, section.root, "forms.profile.email", email_options);
6797    widgets::field_label(
6798        ui,
6799        email,
6800        "forms.profile.email.label",
6801        "Email",
6802        widgets::FieldLabelOptions::default().required(),
6803    );
6804    form_text_field(
6805        ui,
6806        email,
6807        "forms.profile.email.input",
6808        &state.form_email_text,
6809        FocusedTextInput::FormEmail,
6810        state,
6811    );
6812    if profile_email_valid(state.form_email_text.text()) {
6813        widgets::field_help_text(
6814            ui,
6815            email,
6816            "forms.profile.email.help",
6817            "Used for workspace invites and notifications.",
6818            widgets::FieldHelpOptions::default(),
6819        );
6820    } else {
6821        widgets::field_validation_message(
6822            ui,
6823            email,
6824            "forms.profile.email.validation",
6825            ValidationMessage::error("Use a complete email address"),
6826            widgets::ValidationMessageOptions::default(),
6827        );
6828    }
6829
6830    let role = widgets::form_row(
6831        ui,
6832        section.root,
6833        "forms.profile.role",
6834        widgets::FormRowOptions::default(),
6835    );
6836    widgets::field_label(
6837        ui,
6838        role,
6839        "forms.profile.role.label",
6840        "Role",
6841        widgets::FieldLabelOptions::default(),
6842    );
6843    form_text_field(
6844        ui,
6845        role,
6846        "forms.profile.role.input",
6847        &state.form_role_text,
6848        FocusedTextInput::FormRole,
6849        state,
6850    );
6851    widgets::field_validation_message(
6852        ui,
6853        role,
6854        "forms.profile.role.help",
6855        if state.form_role_text.text().trim().is_empty() {
6856            ValidationMessage::warning("Role can be added later")
6857        } else {
6858            ValidationMessage::info(
6859                "Form rows compose labels, controls, help, and validation text.",
6860            )
6861        },
6862        widgets::ValidationMessageOptions::default(),
6863    );
6864
6865    let newsletter = widgets::form_row(
6866        ui,
6867        section.root,
6868        "forms.profile.newsletter",
6869        widgets::FormRowOptions::default().with_accessibility_label("Newsletter preference"),
6870    );
6871    let mut newsletter_options =
6872        widgets::CheckboxOptions::default().with_action("forms.profile.newsletter.toggle");
6873    newsletter_options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
6874    newsletter_options.text_style = text(12.0, color(220, 228, 238));
6875    widgets::checkbox(
6876        ui,
6877        newsletter,
6878        "forms.profile.newsletter.input",
6879        "Send release notes",
6880        state.form_newsletter,
6881        newsletter_options,
6882    );
6883    widgets::field_help_text(
6884        ui,
6885        newsletter,
6886        "forms.profile.newsletter.help",
6887        "Checkboxes participate in the same form state as text fields.",
6888        widgets::FieldHelpOptions::default(),
6889    );
6890
6891    widgets::form_error_summary(
6892        ui,
6893        section.root,
6894        "forms.profile.errors",
6895        &state.form,
6896        widgets::FormErrorSummaryOptions::default(),
6897    );
6898    let action_layout = Layout::row()
6899        .size(LayoutSize::new(
6900            LayoutDimension::percent(1.0),
6901            LayoutDimension::Auto,
6902        ))
6903        .gap(LayoutGap::points(8.0, 8.0))
6904        .flex_wrap(LayoutFlexWrap::Wrap)
6905        .to_layout_style();
6906    widgets::form_action_buttons(
6907        ui,
6908        section.root,
6909        "forms.profile.actions",
6910        &state.form,
6911        widgets::FormActionButtonsOptions::default()
6912            .with_layout(action_layout)
6913            .include_reset(true)
6914            .with_action_prefix("forms.profile"),
6915    );
6916    widgets::label(
6917        ui,
6918        section.root,
6919        "forms.profile.status",
6920        format!("Status: {}", state.form_status),
6921        text(11.0, color(154, 166, 184)),
6922        LayoutStyle::new().with_width_percent(1.0),
6923    );
6924}
6925
6926fn overlay_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6927    let body =
6928        section_with_min_viewport(ui, parent, "overlays", "Overlays", UiSize::new(420.0, 0.0));
6929    let header = widgets::collapsing_header(
6930        ui,
6931        body,
6932        "overlays.collapsing",
6933        "Collapsing header",
6934        widgets::CollapsingHeaderOptions::default()
6935            .expanded(state.overlay_expanded)
6936            .with_toggle_action("overlays.collapsing.toggle"),
6937    );
6938    if let Some(panel) = header.body {
6939        widgets::label(
6940            ui,
6941            panel,
6942            "overlays.collapsing.body",
6943            "Expanded content lives under the header and remains part of normal layout.",
6944            text(12.0, color(196, 210, 230)),
6945            LayoutStyle::new().with_width_percent(1.0),
6946        );
6947    }
6948
6949    let controls = wrapping_row(ui, body, "overlays.controls", 8.0);
6950    button(
6951        ui,
6952        controls,
6953        "overlays.popup.toggle",
6954        if state.overlay_popup_open {
6955            "Close popup"
6956        } else {
6957            "Open popup"
6958        },
6959        "overlays.popup.toggle",
6960        button_visual(48, 112, 184),
6961    );
6962    button(
6963        ui,
6964        controls,
6965        "overlays.modal.open",
6966        "Open modal",
6967        "overlays.modal.open",
6968        button_visual(58, 78, 96),
6969    );
6970
6971    let tooltip = TooltipContent::new("Tooltip")
6972        .body("Tooltip boxes are overlay surfaces with title, body, and shortcut text.")
6973        .shortcut_label("Ctrl+K")
6974        .disabled_reason("Disabled reasons can be announced without changing the trigger.");
6975    let mut tooltip_options = widgets::TooltipBoxOptions::default()
6976        .with_layout(
6977            LayoutStyle::column()
6978                .with_width(280.0)
6979                .with_padding(8.0)
6980                .with_gap(4.0),
6981        )
6982        .with_animation(None);
6983    tooltip_options.layer = UiLayer::AppContent;
6984    tooltip_options.z_index = 0;
6985    widgets::tooltip_box(ui, body, "overlays.tooltip", tooltip, tooltip_options);
6986
6987    let tooltip_anchor = row(ui, body, "overlays.tooltip_anchor", 8.0);
6988    widgets::label(
6989        ui,
6990        tooltip_anchor,
6991        "overlays.tooltip_anchor.label",
6992        "Tooltip placement clamps to its viewport.",
6993        text(12.0, color(166, 176, 190)),
6994        LayoutStyle::new().with_width_percent(1.0),
6995    );
6996    let clamped_rect = widgets::tooltip::tooltip_rect(
6997        UiRect::new(328.0, 12.0, 54.0, 24.0),
6998        UiSize::new(176.0, 58.0),
6999        UiRect::new(0.0, 0.0, 420.0, 190.0),
7000        TooltipPlacement::Right,
7001        8.0,
7002        None,
7003    );
7004    let clamped_preview = ui.add_child(
7005        body,
7006        UiNode::container(
7007            "overlays.tooltip_rect.preview",
7008            LayoutStyle::new()
7009                .with_width_percent(1.0)
7010                .with_height(78.0)
7011                .with_flex_shrink(0.0),
7012        )
7013        .with_visual(UiVisual::panel(
7014            color(12, 16, 22),
7015            Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
7016            4.0,
7017        )),
7018    );
7019    ui.add_child(
7020        clamped_preview,
7021        UiNode::scene(
7022            "overlays.tooltip_rect.scene",
7023            vec![
7024                ScenePrimitive::Rect(
7025                    PaintRect::solid(UiRect::new(328.0, 12.0, 54.0, 24.0), color(48, 112, 184))
7026                        .corner_radii(CornerRadii::uniform(3.0)),
7027                ),
7028                ScenePrimitive::Rect(
7029                    PaintRect::solid(clamped_rect, color(24, 29, 38))
7030                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7031                            color(92, 106, 128),
7032                            1.0,
7033                        )))
7034                        .corner_radii(CornerRadii::uniform(4.0)),
7035                ),
7036            ],
7037            LayoutStyle::new()
7038                .with_width_percent(1.0)
7039                .with_height_percent(1.0),
7040        ),
7041    );
7042
7043    if state.overlay_popup_open {
7044        let popup = ext_widgets::popup_panel(
7045            ui,
7046            parent,
7047            "overlays.popup_panel",
7048            UiRect::new(18.0, 150.0, 220.0, 112.0),
7049            ext_widgets::PopupOptions {
7050                z_index: 20,
7051                portal: UiPortalTarget::Parent,
7052                accessibility: Some(
7053                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup"),
7054                ),
7055                ..Default::default()
7056            },
7057        );
7058        let popup_body = ui.add_child(
7059            popup,
7060            UiNode::container(
7061                "overlays.popup_panel.body",
7062                LayoutStyle::column()
7063                    .with_width_percent(1.0)
7064                    .with_height_percent(1.0)
7065                    .with_padding(10.0)
7066                    .with_gap(6.0),
7067            ),
7068        );
7069        let popup_header = row(ui, popup_body, "overlays.popup_panel.header", 8.0);
7070        widgets::label(
7071            ui,
7072            popup_header,
7073            "overlays.popup_panel.label",
7074            "Popup panel",
7075            text(12.0, color(220, 228, 238)),
7076            LayoutStyle::new().with_width_percent(1.0),
7077        );
7078        let mut close = widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0))
7079            .with_action("overlays.popup.close");
7080        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7081        close.hovered_visual = Some(button_visual(54, 70, 92));
7082        close.text_style = text(12.0, color(220, 228, 238));
7083        widgets::button(ui, popup_header, "overlays.popup_panel.close", "x", close);
7084        widgets::label(
7085            ui,
7086            popup_body,
7087            "overlays.popup_panel.body_text",
7088            "Popup content is conditionally rendered.",
7089            text(11.0, color(196, 210, 230)),
7090            LayoutStyle::new().with_width_percent(1.0),
7091        );
7092    }
7093
7094    if state.overlay_modal_open {
7095        let modal = widgets::modal_dialog(
7096            ui,
7097            parent,
7098            "overlays.modal",
7099            "Modal dialog",
7100            widgets::ModalDialogOptions::default()
7101                .with_size(320.0, 180.0)
7102                .with_close_action("overlays.modal.close")
7103                .with_dismissal(ext_widgets::DialogDismissal::MODAL)
7104                .with_focus_restore(FocusRestoreTarget::Previous),
7105        );
7106        widgets::label(
7107            ui,
7108            modal.body,
7109            "overlays.modal.body.text",
7110            "Modal dialogs are portaled to the application overlay, include a scrim, and trap focus.",
7111            text(12.0, color(220, 228, 238)),
7112            LayoutStyle::new().with_width_percent(1.0),
7113        );
7114        button(
7115            ui,
7116            modal.body,
7117            "overlays.modal.body.close",
7118            "Close modal",
7119            "overlays.modal.close",
7120            button_visual(48, 112, 184),
7121        );
7122    }
7123}
7124
7125fn drag_drop_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7126    let body = section_with_min_viewport(
7127        ui,
7128        parent,
7129        "drag_drop",
7130        "Drag and drop",
7131        UiSize::new(420.0, 0.0),
7132    );
7133    widgets::label(
7134        ui,
7135        body,
7136        "drag_drop.sources.label",
7137        "Drag sources",
7138        text(12.0, color(166, 176, 190)),
7139        LayoutStyle::new().with_width_percent(1.0),
7140    );
7141    let sources = wrapping_row(ui, body, "drag_drop.sources", 8.0);
7142    widgets::dnd_drag_source(
7143        ui,
7144        sources,
7145        "drag_drop.text_source",
7146        "Text payload",
7147        DragPayload::text("Operad payload"),
7148        widgets::DragSourceOptions::default()
7149            .with_layout(drag_source_layout())
7150            .with_kind(DragDropSurfaceKind::ListRow)
7151            .with_allowed_operations([DragOperation::Copy, DragOperation::Move])
7152            .with_action("drag_drop.text_source")
7153            .with_accessibility_hint("Start a text drag operation"),
7154    );
7155    widgets::dnd_drag_source(
7156        ui,
7157        sources,
7158        "drag_drop.file_source",
7159        "File payload",
7160        DragPayload::files(["/tmp/showcase.scene"]),
7161        widgets::DragSourceOptions::default()
7162            .with_layout(drag_source_layout())
7163            .with_kind(DragDropSurfaceKind::Asset)
7164            .with_drag_image_policy(widgets::DragImagePolicy::image_key(
7165                BuiltInIcon::Folder.key(),
7166                UiSize::new(120.0, 36.0),
7167                UiPoint::new(10.0, 10.0),
7168            ))
7169            .with_allowed_operations([DragOperation::Copy])
7170            .with_action("drag_drop.file_source"),
7171    );
7172    widgets::dnd_drag_source(
7173        ui,
7174        sources,
7175        "drag_drop.bytes_source",
7176        "Image bytes",
7177        DragPayload::bytes(DragBytes::new("image/png", vec![137, 80, 78, 71]).name("sprite.png")),
7178        widgets::DragSourceOptions::default()
7179            .with_layout(drag_source_layout())
7180            .with_kind(DragDropSurfaceKind::Asset)
7181            .with_action("drag_drop.bytes_source")
7182            .without_drag_image(),
7183    );
7184
7185    widgets::label(
7186        ui,
7187        body,
7188        "drag_drop.zones.label",
7189        "Drop zones",
7190        text(12.0, color(166, 176, 190)),
7191        LayoutStyle::new().with_width_percent(1.0),
7192    );
7193    let zones = wrapping_row(ui, body, "drag_drop.zones", 8.0);
7194    let accepted_options = widgets::DropZoneOptions::default()
7195        .with_layout(drop_zone_layout())
7196        .with_kind(DragDropSurfaceKind::EditorSurface)
7197        .with_accepted_payload(DropPayloadFilter::empty().text())
7198        .with_accepted_operations([DragOperation::Copy, DragOperation::Move])
7199        .with_action("drag_drop.accept_text")
7200        .with_accessibility_hint("Accepts text payloads");
7201    let accepted = widgets::dnd_drop_zone(
7202        ui,
7203        zones,
7204        "drag_drop.accept_text",
7205        "Text accepted",
7206        accepted_options.clone(),
7207    );
7208    widgets::drag_drop::dnd_apply_drop_zone_preview(
7209        ui,
7210        accepted.root,
7211        &accepted_options,
7212        widgets::drag_drop::DropZonePreviewState::Accepted,
7213    );
7214
7215    let rejected_options = widgets::DropZoneOptions::default()
7216        .with_layout(drop_zone_layout())
7217        .with_kind(DragDropSurfaceKind::Asset)
7218        .with_accepted_payload(DropPayloadFilter::empty().files())
7219        .with_action("drag_drop.files_only");
7220    let rejected = widgets::dnd_drop_zone(
7221        ui,
7222        zones,
7223        "drag_drop.files_only",
7224        "Files only",
7225        rejected_options.clone(),
7226    );
7227    widgets::drag_drop::dnd_apply_drop_zone_preview(
7228        ui,
7229        rejected.root,
7230        &rejected_options,
7231        widgets::drag_drop::DropZonePreviewState::Rejected,
7232    );
7233    let image_options = widgets::DropZoneOptions::default()
7234        .with_layout(drop_zone_layout())
7235        .with_kind(DragDropSurfaceKind::Asset)
7236        .with_accepted_payload(DropPayloadFilter::empty().mime_type("image/*"))
7237        .with_accepted_operations([DragOperation::Copy])
7238        .with_action("drag_drop.image_bytes");
7239    let image_zone = widgets::dnd_drop_zone(
7240        ui,
7241        zones,
7242        "drag_drop.image_bytes",
7243        "Image bytes",
7244        image_options.clone(),
7245    );
7246    widgets::drag_drop::dnd_apply_drop_zone_preview(
7247        ui,
7248        image_zone.root,
7249        &image_options,
7250        widgets::drag_drop::DropZonePreviewState::Hovered,
7251    );
7252
7253    let disabled_options = widgets::DropZoneOptions::default()
7254        .with_layout(drop_zone_layout())
7255        .with_kind(DragDropSurfaceKind::EditorSurface)
7256        .with_accepted_payload(DropPayloadFilter::any())
7257        .with_action("drag_drop.disabled")
7258        .disabled();
7259    let disabled_zone = widgets::dnd_drop_zone(
7260        ui,
7261        zones,
7262        "drag_drop.disabled",
7263        "Disabled",
7264        disabled_options.clone(),
7265    );
7266    widgets::drag_drop::dnd_apply_drop_zone_preview(
7267        ui,
7268        disabled_zone.root,
7269        &disabled_options,
7270        widgets::drag_drop::DropZonePreviewState::Disabled,
7271    );
7272
7273    let operation_row = wrapping_row(ui, body, "drag_drop.operations", 6.0);
7274    dnd_operation_chip(ui, operation_row, "drag_drop.operation.copy", "copy");
7275    dnd_operation_chip(ui, operation_row, "drag_drop.operation.move", "move");
7276    dnd_operation_chip(ui, operation_row, "drag_drop.operation.link", "link");
7277    widgets::label(
7278        ui,
7279        body,
7280        "drag_drop.status",
7281        format!("Status: {}", state.drag_drop_status),
7282        text(11.0, color(154, 166, 184)),
7283        LayoutStyle::new().with_width_percent(1.0),
7284    );
7285}
7286
7287fn media_widgets(ui: &mut UiDocument, parent: UiNodeId) {
7288    let body = section_with_min_viewport(ui, parent, "media", "Media", UiSize::new(430.0, 0.0));
7289    widgets::label(
7290        ui,
7291        body,
7292        "media.icons.label",
7293        "Built-in icons",
7294        text(12.0, color(166, 176, 190)),
7295        LayoutStyle::new().with_width_percent(1.0),
7296    );
7297    let icons = wrapping_row(ui, body, "media.icons", 8.0);
7298    for icon in BuiltInIcon::COMMON {
7299        media_icon_tile(ui, icons, icon);
7300    }
7301
7302    widgets::label(
7303        ui,
7304        body,
7305        "media.variants.label",
7306        "Image variants",
7307        text(12.0, color(166, 176, 190)),
7308        LayoutStyle::new().with_width_percent(1.0),
7309    );
7310    let variants = wrapping_row(ui, body, "media.variants", 10.0);
7311    widgets::image(
7312        ui,
7313        variants,
7314        "media.image.untinted",
7315        icon_image(BuiltInIcon::Play),
7316        widgets::ImageOptions::default()
7317            .with_layout(media_preview_image_layout())
7318            .with_accessibility_label("Untinted play icon"),
7319    );
7320    widgets::image(
7321        ui,
7322        variants,
7323        "media.image.warning",
7324        ImageContent::new(BuiltInIcon::Warning.key()).tinted(color(232, 186, 88)),
7325        widgets::ImageOptions::default()
7326            .with_layout(media_preview_image_layout())
7327            .with_accessibility_label("Tinted warning icon"),
7328    );
7329    widgets::image(
7330        ui,
7331        variants,
7332        "media.image.shader",
7333        ImageContent::new(BuiltInIcon::Grid.key()).tinted(color(118, 183, 255)),
7334        widgets::ImageOptions::default()
7335            .with_layout(media_preview_image_layout())
7336            .with_shader(ShaderEffect::new("media.preview.tint").uniform("amount", 0.5))
7337            .with_accessibility_label("Shader-decorated grid icon"),
7338    );
7339    widgets::label(
7340        ui,
7341        body,
7342        "media.image.note",
7343        "Image widgets reference stable resource keys; the host resolves them to textures, vector assets, tinting, or shader-backed resources.",
7344        text(12.0, color(166, 176, 190)),
7345        LayoutStyle::new().with_width_percent(1.0),
7346    );
7347}
7348
7349fn timeline_ruler(ui: &mut UiDocument, parent: UiNodeId) {
7350    let layout = LayoutStyle::column()
7351        .with_width_percent(1.0)
7352        .with_height(40.0)
7353        .with_flex_shrink(0.0);
7354    let layout = operad::layout::with_min_size(layout, operad::length(0.0), operad::length(0.0));
7355    let body = widgets::scroll_area(ui, parent, "timeline", ScrollAxes::BOTH, layout);
7356    ext_widgets::timeline_ruler(
7357        ui,
7358        body,
7359        "timeline.ruler",
7360        ext_widgets::RulerSpec {
7361            range: ext_widgets::TimelineRange::new(0.0, 12.0),
7362            width: 600.0,
7363            major_step: 2.0,
7364            minor_step: 0.5,
7365            label_every: 1,
7366        },
7367        ext_widgets::TimelineRulerOptions::default(),
7368    );
7369}
7370
7371fn toast_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7372    let body = section(ui, parent, "toasts", "Toasts");
7373    let controls = row(ui, body, "toasts.controls", 10.0);
7374    button(
7375        ui,
7376        controls,
7377        "toasts.show",
7378        "Show toast",
7379        "toast.show",
7380        button_visual(48, 112, 184),
7381    );
7382    button(
7383        ui,
7384        controls,
7385        "toasts.hide",
7386        "Hide",
7387        "toast.hide",
7388        button_visual(58, 78, 96),
7389    );
7390    widgets::label(
7391        ui,
7392        body,
7393        "toasts.status",
7394        if state.toast_visible {
7395            "Toast overlay is visible."
7396        } else {
7397            "Toast overlay is hidden."
7398        },
7399        text(12.0, color(196, 210, 230)),
7400        LayoutStyle::new().with_width_percent(1.0),
7401    );
7402    widgets::label(
7403        ui,
7404        body,
7405        "toasts.action_status",
7406        format!("Action: {}", state.toast_action_status),
7407        text(12.0, color(154, 166, 184)),
7408        LayoutStyle::new().with_width_percent(1.0),
7409    );
7410}
7411
7412fn popup_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7413    let body = section(ui, parent, "popup_panel", "Popup panel");
7414    let controls = row(ui, body, "popup_panel.controls", 8.0);
7415    button(
7416        ui,
7417        controls,
7418        "popup_panel.toggle",
7419        if state.popup_open {
7420            "Close popup"
7421        } else {
7422            "Open popup"
7423        },
7424        "popup.toggle",
7425        button_visual(48, 112, 184),
7426    );
7427    if state.popup_open {
7428        let mut close =
7429            widgets::ButtonOptions::new(LayoutStyle::size(30.0, 30.0)).with_action("popup.close");
7430        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7431        close.hovered_visual = Some(button_visual(54, 70, 92));
7432        close.text_style = text(13.0, color(220, 228, 238));
7433        widgets::button(ui, controls, "popup_panel.inline_close", "x", close);
7434    }
7435    widgets::label(
7436        ui,
7437        body,
7438        "popup_panel.status",
7439        if state.popup_open {
7440            "Popup overlay is open."
7441        } else {
7442            "Popup overlay is closed."
7443        },
7444        text(12.0, color(196, 210, 230)),
7445        LayoutStyle::new().with_width_percent(1.0),
7446    );
7447    if state.popup_open {
7448        let panel = ext_widgets::popup_panel(
7449            ui,
7450            parent,
7451            "popup_panel.inline_preview",
7452            UiRect::new(0.0, 20.0, 160.0, 104.0),
7453            ext_widgets::PopupOptions {
7454                z_index: 4,
7455                portal: UiPortalTarget::Parent,
7456                accessibility: Some(
7457                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup preview"),
7458                ),
7459                ..Default::default()
7460            },
7461        );
7462        let content = ui.add_child(
7463            panel,
7464            UiNode::container(
7465                "popup_panel.inline_preview.body",
7466                LayoutStyle::column()
7467                    .with_width_percent(1.0)
7468                    .with_height_percent(1.0)
7469                    .with_padding(10.0)
7470                    .with_gap(8.0),
7471            ),
7472        );
7473        let header = row(ui, content, "popup_panel.inline_preview.header", 8.0);
7474        widgets::label(
7475            ui,
7476            header,
7477            "popup_panel.inline_preview.title",
7478            "Popup panel",
7479            text(12.0, color(226, 234, 246)),
7480            LayoutStyle::new().with_width_percent(1.0),
7481        );
7482        let mut close =
7483            widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0)).with_action("popup.close");
7484        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7485        close.hovered_visual = Some(button_visual(54, 70, 92));
7486        close.text_style = text(12.0, color(220, 228, 238));
7487        widgets::button(ui, header, "popup_panel.inline_preview.close", "x", close);
7488        widgets::label(
7489            ui,
7490            content,
7491            "popup_panel.inline_preview.text",
7492            "Overlay content",
7493            text(11.0, color(196, 210, 230)),
7494            LayoutStyle::new().with_width_percent(1.0),
7495        );
7496        widgets::spacer(
7497            ui,
7498            body,
7499            "popup_panel.inline_preview.space",
7500            LayoutStyle::new()
7501                .with_width_percent(1.0)
7502                .with_height(112.0)
7503                .with_flex_shrink(0.0),
7504        );
7505    }
7506}
7507
7508fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7509    let body = section(ui, parent, "styling", "Styling");
7510    let grid_layout = operad::layout::with_grid_template_columns(
7511        Layout::grid()
7512            .size(LayoutSize::percent(1.0, 1.0))
7513            .gap(LayoutGap::points(10.0, 10.0))
7514            .to_layout_style(),
7515        [
7516            LayoutGridTrack::points(300.0),
7517            LayoutGridTrack::points(1.0),
7518            LayoutGridTrack::points(210.0),
7519        ],
7520    );
7521    let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
7522    let controls = ui.add_child(
7523        grid,
7524        UiNode::container(
7525            "styling.controls",
7526            LayoutStyle::column()
7527                .with_width(300.0)
7528                .with_height_percent(1.0)
7529                .with_flex_shrink(0.0)
7530                .gap(6.0),
7531        ),
7532    );
7533    style_edge_group(
7534        ui,
7535        controls,
7536        "styling.inner",
7537        "Inner margin",
7538        "styling.inner_same",
7539        state.styling.inner_same,
7540        [
7541            ("Left", "styling.inner", state.styling.inner_margin),
7542            ("Right", "styling.inner_right", state.styling.inner_right),
7543            ("Top", "styling.inner_top", state.styling.inner_top),
7544            ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
7545        ],
7546        0.0..32.0,
7547    );
7548    style_edge_group(
7549        ui,
7550        controls,
7551        "styling.outer",
7552        "Outer margin",
7553        "styling.outer_same",
7554        state.styling.outer_same,
7555        [
7556            ("Left", "styling.outer", state.styling.outer_margin),
7557            ("Right", "styling.outer_right", state.styling.outer_right),
7558            ("Top", "styling.outer_top", state.styling.outer_top),
7559            ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
7560        ],
7561        0.0..40.0,
7562    );
7563    style_edge_group(
7564        ui,
7565        controls,
7566        "styling.radius",
7567        "Corner radius",
7568        "styling.radius_same",
7569        state.styling.radius_same,
7570        [
7571            ("NW", "styling.radius", state.styling.corner_radius),
7572            ("NE", "styling.radius_ne", state.styling.corner_ne),
7573            ("SW", "styling.radius_sw", state.styling.corner_sw),
7574            ("SE", "styling.radius_se", state.styling.corner_se),
7575        ],
7576        0.0..28.0,
7577    );
7578    style_shadow_group(ui, controls, state);
7579    style_color_button_row(
7580        ui,
7581        controls,
7582        "styling.fill_color_button",
7583        "Fill",
7584        state.styling.fill_color(),
7585        "Pick fill color",
7586    );
7587    if state.styling_fill_picker_open {
7588        ext_widgets::color_picker(
7589            ui,
7590            controls,
7591            "styling.fill_picker",
7592            &state.styling_fill_picker,
7593            ext_widgets::ColorPickerOptions::default()
7594                .with_label("Fill")
7595                .with_action_prefix("styling.fill_picker"),
7596        );
7597    }
7598    style_stroke_row(ui, controls, state);
7599    if state.styling_stroke_picker_open {
7600        ext_widgets::color_picker(
7601            ui,
7602            controls,
7603            "styling.stroke_picker",
7604            &state.styling_stroke_picker,
7605            ext_widgets::ColorPickerOptions::default()
7606                .with_label("Stroke color")
7607                .with_action_prefix("styling.stroke_picker"),
7608        );
7609    }
7610    widgets::separator(
7611        ui,
7612        grid,
7613        "styling.preview.separator",
7614        widgets::SeparatorOptions::vertical().with_layout(
7615            LayoutStyle::new()
7616                .with_width(1.0)
7617                .with_height_percent(1.0)
7618                .with_flex_shrink(0.0),
7619        ),
7620    );
7621
7622    let preview = ui.add_child(
7623        grid,
7624        UiNode::container(
7625            "styling.preview",
7626            LayoutStyle::column()
7627                .with_width(210.0)
7628                .with_height_percent(1.0)
7629                .with_flex_shrink(0.0)
7630                .padding(8.0),
7631        )
7632        .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
7633    );
7634    style_preview(ui, preview, state.styling);
7635}
7636
7637#[allow(clippy::too_many_arguments)]
7638fn style_edge_group(
7639    ui: &mut UiDocument,
7640    parent: UiNodeId,
7641    name: &'static str,
7642    title: &'static str,
7643    same_action: &'static str,
7644    same: bool,
7645    values: [(&'static str, &'static str, f32); 4],
7646    range: std::ops::Range<f32>,
7647) {
7648    let group = style_control_group(ui, parent, format!("{name}.group"));
7649    style_group_title(ui, group, format!("{name}.title"), title);
7650    let fields = ui.add_child(
7651        group,
7652        UiNode::container(
7653            format!("{name}.fields"),
7654            LayoutStyle::column()
7655                .with_width(138.0)
7656                .with_flex_shrink(0.0)
7657                .gap(3.0),
7658        ),
7659    );
7660    style_compact_checkbox(ui, fields, same_action, "same", same);
7661    if same {
7662        style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
7663    } else {
7664        for (label, action, value) in values {
7665            style_number_row(ui, fields, action, label, value, range.clone(), 0);
7666        }
7667    }
7668}
7669
7670fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7671    let group = style_control_group(ui, parent, "styling.shadow.group");
7672    style_group_title(ui, group, "styling.shadow.title", "Shadow");
7673    let fields = ui.add_child(
7674        group,
7675        UiNode::container(
7676            "styling.shadow.fields",
7677            LayoutStyle::column()
7678                .with_width(174.0)
7679                .with_flex_shrink(0.0)
7680                .gap(4.0),
7681        ),
7682    );
7683    let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
7684    style_inline_number(
7685        ui,
7686        offsets,
7687        "styling.shadow_x",
7688        "x",
7689        state.styling.shadow_x,
7690        -24.0..24.0,
7691        0,
7692    );
7693    style_inline_number(
7694        ui,
7695        offsets,
7696        "styling.shadow_y",
7697        "y",
7698        state.styling.shadow_y,
7699        -24.0..24.0,
7700        0,
7701    );
7702    let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
7703    style_inline_number(
7704        ui,
7705        spread,
7706        "styling.shadow",
7707        "blur",
7708        state.styling.shadow_blur,
7709        0.0..32.0,
7710        0,
7711    );
7712    style_inline_number(
7713        ui,
7714        spread,
7715        "styling.shadow_spread",
7716        "spread",
7717        state.styling.shadow_spread,
7718        0.0..16.0,
7719        0,
7720    );
7721    style_color_button_row(
7722        ui,
7723        fields,
7724        "styling.shadow_color_button",
7725        "",
7726        state.styling.shadow_color(),
7727        "Pick shadow color",
7728    );
7729    if state.styling_shadow_picker_open {
7730        ext_widgets::color_picker(
7731            ui,
7732            fields,
7733            "styling.shadow_picker",
7734            &state.styling_shadow_picker,
7735            ext_widgets::ColorPickerOptions::default()
7736                .with_label("Shadow color")
7737                .with_action_prefix("styling.shadow_picker"),
7738        );
7739    }
7740}
7741
7742fn style_stroke_row(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7743    let row = row(ui, parent, "styling.stroke.row", 8.0);
7744    widgets::label(
7745        ui,
7746        row,
7747        "styling.stroke.label",
7748        "Stroke",
7749        text(12.0, color(166, 176, 190)),
7750        LayoutStyle::new().with_width(86.0).with_flex_shrink(0.0),
7751    );
7752    style_value_input(
7753        ui,
7754        row,
7755        "styling.stroke",
7756        state.styling.stroke_width,
7757        0.0..4.0,
7758        1,
7759    );
7760    ext_widgets::color_edit_button(
7761        ui,
7762        row,
7763        "styling.stroke_color_button",
7764        state.styling.stroke_color(),
7765        color_mini_button_options("styling.stroke_color_button")
7766            .with_format(ext_widgets::ColorValueFormat::Rgba)
7767            .accessibility_label("Pick stroke color"),
7768    );
7769    let mut options = widgets::SliderOptions::default()
7770        .with_layout(
7771            LayoutStyle::new()
7772                .with_width(60.0)
7773                .with_height(20.0)
7774                .with_flex_shrink(0.0),
7775        )
7776        .with_value_edit_action("styling.stroke");
7777    options.fill_color = color(120, 170, 230);
7778    widgets::slider(
7779        ui,
7780        row,
7781        "styling.stroke.slider",
7782        (state.styling.stroke_width / 4.0).clamp(0.0, 1.0),
7783        0.0..1.0,
7784        options,
7785    );
7786}
7787
7788fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7789    ui.add_child(
7790        parent,
7791        UiNode::container(
7792            name,
7793            LayoutStyle::row()
7794                .with_width_percent(1.0)
7795                .with_flex_shrink(0.0)
7796                .padding(4.0)
7797                .gap(8.0),
7798        )
7799        .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
7800    )
7801}
7802
7803fn style_group_title(
7804    ui: &mut UiDocument,
7805    parent: UiNodeId,
7806    name: impl Into<String>,
7807    label: &'static str,
7808) {
7809    widgets::label(
7810        ui,
7811        parent,
7812        name,
7813        label,
7814        text(12.0, color(166, 176, 190)),
7815        LayoutStyle::new()
7816            .with_width(88.0)
7817            .with_flex_shrink(0.0)
7818            .with_height(22.0),
7819    );
7820}
7821
7822fn style_color_button_row(
7823    ui: &mut UiDocument,
7824    parent: UiNodeId,
7825    action: &'static str,
7826    label: &'static str,
7827    value: ColorRgba,
7828    accessibility_label: &'static str,
7829) {
7830    let row = row(ui, parent, format!("{action}.row"), 8.0);
7831    if !label.is_empty() {
7832        widgets::label(
7833            ui,
7834            row,
7835            format!("{action}.label"),
7836            label,
7837            text(12.0, color(166, 176, 190)),
7838            LayoutStyle::new()
7839                .with_width(86.0)
7840                .with_flex_shrink(0.0)
7841                .with_height(24.0),
7842        );
7843    }
7844    ext_widgets::color_edit_button(
7845        ui,
7846        row,
7847        action,
7848        value,
7849        color_mini_button_options(action)
7850            .with_format(ext_widgets::ColorValueFormat::Rgba)
7851            .accessibility_label(accessibility_label),
7852    );
7853    widgets::label(
7854        ui,
7855        row,
7856        format!("{action}.value"),
7857        ext_widgets::color_picker::format_hex_color(value, value.a < 255),
7858        text(12.0, color(226, 232, 242)),
7859        LayoutStyle::new().with_width(96.0).with_height(24.0),
7860    );
7861}
7862
7863fn style_number_row(
7864    ui: &mut UiDocument,
7865    parent: UiNodeId,
7866    name: &'static str,
7867    label: &'static str,
7868    value: f32,
7869    range: std::ops::Range<f32>,
7870    decimals: u8,
7871) {
7872    let row = row(ui, parent, format!("{name}.row"), 6.0);
7873    widgets::label(
7874        ui,
7875        row,
7876        format!("{name}.label"),
7877        label,
7878        text(12.0, color(166, 176, 190)),
7879        LayoutStyle::new().with_width(48.0).with_height(22.0),
7880    );
7881    style_value_input(ui, row, name, value, range, decimals);
7882}
7883
7884fn style_inline_number(
7885    ui: &mut UiDocument,
7886    parent: UiNodeId,
7887    name: &'static str,
7888    label: &'static str,
7889    value: f32,
7890    range: std::ops::Range<f32>,
7891    decimals: u8,
7892) {
7893    let row = row(ui, parent, format!("{name}.inline"), 3.0);
7894    widgets::label(
7895        ui,
7896        row,
7897        format!("{name}.inline_label"),
7898        format!("{label}:"),
7899        text(12.0, color(166, 176, 190)),
7900        LayoutStyle::new()
7901            .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
7902            .with_height(22.0),
7903    );
7904    style_value_input(ui, row, name, value, range, decimals);
7905}
7906
7907fn style_value_input(
7908    ui: &mut UiDocument,
7909    parent: UiNodeId,
7910    name: &'static str,
7911    value: f32,
7912    range: std::ops::Range<f32>,
7913    decimals: u8,
7914) {
7915    let mut options = widgets::DragValueOptions::default()
7916        .with_layout(
7917            LayoutStyle::new()
7918                .with_width(42.0)
7919                .with_height(22.0)
7920                .with_flex_shrink(0.0),
7921        )
7922        .with_range(ext_widgets::NumericRange::new(
7923            f64::from(range.start),
7924            f64::from(range.end),
7925        ))
7926        .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
7927        .with_action(name);
7928    options.text_style = text(12.0, color(226, 232, 242));
7929    widgets::drag_value_input(ui, parent, name, f64::from(value), options);
7930}
7931
7932fn style_compact_checkbox(
7933    ui: &mut UiDocument,
7934    parent: UiNodeId,
7935    name: &'static str,
7936    label: &'static str,
7937    checked: bool,
7938) {
7939    let mut options = widgets::CheckboxOptions::default().with_action(name);
7940    options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
7941    options.text_style = text(12.0, color(220, 228, 238));
7942    widgets::checkbox(ui, parent, name, label, checked, options);
7943}
7944
7945fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
7946    ext_widgets::ColorButtonOptions::default()
7947        .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
7948        .with_swatch_size(UiSize::new(22.0, 18.0))
7949        .with_action(action)
7950        .show_label(false)
7951}
7952
7953fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
7954    let outer = styling.outer_edges();
7955    let inner = styling.inner_edges();
7956    let frame = UiRect::new(
7957        22.0 + outer[0],
7958        28.0 + outer[2],
7959        108.0 + inner[0] + inner[1],
7960        40.0 + inner[2] + inner[3],
7961    );
7962    let text_rect = UiRect::new(
7963        frame.x + inner[0],
7964        frame.y + inner[2],
7965        (frame.width - inner[0] - inner[1]).max(1.0),
7966        (frame.height - inner[2] - inner[3]).max(1.0),
7967    );
7968    ui.add_child(
7969        parent,
7970        UiNode::scene(
7971            "styling.preview.scene",
7972            vec![
7973                ScenePrimitive::Rect(
7974                    PaintRect::solid(frame, styling.fill_color())
7975                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7976                            styling.stroke_color(),
7977                            styling.stroke_width,
7978                        )))
7979                        .corner_radii(styling.radii())
7980                        .effect(PaintEffect::shadow(
7981                            styling.shadow_color(),
7982                            UiPoint::new(styling.shadow_x, styling.shadow_y),
7983                            styling.shadow_blur,
7984                            styling.shadow_spread,
7985                        )),
7986                ),
7987                ScenePrimitive::Text(
7988                    PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
7989                        .horizontal_align(TextHorizontalAlign::Center)
7990                        .vertical_align(TextVerticalAlign::Center)
7991                        .multiline(false),
7992                ),
7993            ],
7994            LayoutStyle::new()
7995                .with_width_percent(1.0)
7996                .with_height(180.0)
7997                .with_flex_shrink(0.0),
7998        ),
7999    );
8000}
Source

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

Examples found in repository?
examples/showcase.rs (line 5193)
5178fn animation_panel_primitives(offset: UiPoint) -> Vec<ScenePrimitive> {
5179    vec![ScenePrimitive::Rect(
5180        PaintRect::solid(
5181            UiRect::new(
5182                offset.x,
5183                offset.y,
5184                ANIMATION_PANEL_WIDTH,
5185                ANIMATION_PANEL_HEIGHT,
5186            ),
5187            color(232, 186, 88),
5188        )
5189        .stroke(AlignedStroke::inside(StrokeStyle::new(
5190            color(255, 226, 154),
5191            1.0,
5192        )))
5193        .corner_radii(CornerRadii::uniform(6.0)),
5194    )]
5195}
5196
5197fn list_and_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5198    let body = section(ui, parent, "lists_tables", "Lists and tables");
5199
5200    let scroll_shell = row(ui, body, "lists_tables.scroll_area.shell", 8.0);
5201    let nested_scroll = widgets::scroll_area(
5202        ui,
5203        scroll_shell,
5204        "lists_tables.scroll_area",
5205        ScrollAxes::VERTICAL,
5206        LayoutStyle::column()
5207            .with_width(0.0)
5208            .with_flex_grow(1.0)
5209            .with_height(92.0),
5210    );
5211    ui.node_mut(nested_scroll)
5212        .set_action("lists_tables.scroll_area.scroll");
5213    if let Some(scroll) = ui.node_mut(nested_scroll).scroll_mut() {
5214        scroll.set_offset(UiPoint::new(0.0, state.list_scroll));
5215    }
5216    for index in 0..6 {
5217        widgets::label(
5218            ui,
5219            nested_scroll,
5220            format!("lists_tables.scroll_area.row.{index}"),
5221            format!("Scroll row {}", index + 1),
5222            text(12.0, color(200, 212, 228)),
5223            LayoutStyle::new()
5224                .with_width_percent(1.0)
5225                .with_height(26.0)
5226                .with_flex_shrink(0.0),
5227        );
5228    }
5229    scrollbar_widgets::scrollbar(
5230        ui,
5231        scroll_shell,
5232        "lists_tables.scroll_area.scrollbar",
5233        scroll_state(state.list_scroll, 92.0, 6.0 * 26.0),
5234        scrollbar_widgets::ScrollAxis::Vertical,
5235        scrollbar_widgets::ScrollbarOptions::default()
5236            .with_layout(LayoutStyle::size(8.0, 92.0))
5237            .with_track_size(UiSize::new(8.0, 92.0))
5238            .with_action("lists_tables.scroll_area.scrollbar"),
5239    );
5240
5241    widgets::table_header(ui, body, "lists_tables.table_header", &table_columns());
5242
5243    let virtual_shell = row(ui, body, "lists_tables.virtual_list.shell", 8.0);
5244    let virtual_list = widgets::virtual_list(
5245        ui,
5246        virtual_shell,
5247        "lists_tables.virtual_list",
5248        widgets::VirtualListSpec {
5249            row_count: 24,
5250            row_height: 28.0,
5251            viewport_height: 112.0,
5252            scroll_offset: state.virtual_scroll,
5253            overscan: 1,
5254        },
5255        |ui, row_parent, row| {
5256            widgets::label(
5257                ui,
5258                row_parent,
5259                format!("lists_tables.virtual_list.row.{row}"),
5260                format!("Virtual row {}", row + 1),
5261                text(12.0, color(214, 224, 238)),
5262                LayoutStyle::new()
5263                    .with_width_percent(1.0)
5264                    .with_height(28.0)
5265                    .with_flex_shrink(0.0),
5266            );
5267        },
5268    );
5269    ui.node_mut(virtual_list)
5270        .set_action("lists_tables.virtual_list.scroll");
5271    scrollbar_widgets::scrollbar(
5272        ui,
5273        virtual_shell,
5274        "lists_tables.virtual_list.scrollbar",
5275        scroll_state(state.virtual_scroll, 112.0, 24.0 * 28.0),
5276        scrollbar_widgets::ScrollAxis::Vertical,
5277        scrollbar_widgets::ScrollbarOptions::default()
5278            .with_layout(LayoutStyle::size(8.0, 112.0))
5279            .with_track_size(UiSize::new(8.0, 112.0))
5280            .with_action("lists_tables.virtual_list.scrollbar"),
5281    );
5282
5283    let table_shell = row(ui, body, "lists_tables.data_table.shell", 8.0);
5284    let table_scroll = widgets::scroll_area(
5285        ui,
5286        table_shell,
5287        "lists_tables.data_table",
5288        ScrollAxes::VERTICAL,
5289        LayoutStyle::column()
5290            .with_width(0.0)
5291            .with_flex_grow(1.0)
5292            .with_height(128.0),
5293    );
5294    ui.node_mut(table_scroll)
5295        .set_action("lists_tables.data_table.scroll");
5296    if let Some(scroll) = ui.node_mut(table_scroll).scroll_mut() {
5297        scroll.set_offset(UiPoint::new(0.0, state.table_scroll));
5298    }
5299    for row_index in 0..16 {
5300        data_table_row(ui, table_scroll, row_index, state);
5301    }
5302    scrollbar_widgets::scrollbar(
5303        ui,
5304        table_shell,
5305        "lists_tables.data_table.scrollbar",
5306        scroll_state(state.table_scroll, 128.0, 16.0 * 28.0),
5307        scrollbar_widgets::ScrollAxis::Vertical,
5308        scrollbar_widgets::ScrollbarOptions::default()
5309            .with_layout(LayoutStyle::size(8.0, 128.0))
5310            .with_track_size(UiSize::new(8.0, 128.0))
5311            .with_action("lists_tables.data_table.scrollbar"),
5312    );
5313
5314    let virtual_controls = wrapping_row(ui, body, "lists_tables.virtualized_table.controls", 8.0);
5315    button(
5316        ui,
5317        virtual_controls,
5318        "lists_tables.virtualized_table.sort.name",
5319        if state.virtual_table_descending {
5320            "Name desc"
5321        } else {
5322            "Name asc"
5323        },
5324        "lists_tables.virtualized_table.sort.name",
5325        button_visual(38, 52, 70),
5326    );
5327    button(
5328        ui,
5329        virtual_controls,
5330        "lists_tables.virtualized_table.filter.status",
5331        if state.virtual_table_ready_only {
5332            "Ready only"
5333        } else {
5334            "All status"
5335        },
5336        "lists_tables.virtualized_table.filter.status",
5337        button_visual(38, 52, 70),
5338    );
5339    button(
5340        ui,
5341        virtual_controls,
5342        "lists_tables.virtualized_table.resize.reset",
5343        "Reset width",
5344        "lists_tables.virtualized_table.resize.reset",
5345        button_visual(38, 52, 70),
5346    );
5347
5348    let columns = virtual_table_columns(state);
5349    let visible_rows = virtual_table_visible_rows(state);
5350    let mut table_options = ext_widgets::DataTableOptions::default()
5351        .with_row_action_prefix("lists_tables.virtualized_table")
5352        .with_cell_action_prefix("lists_tables.virtualized_table")
5353        .with_scroll_action("lists_tables.virtualized_table.scroll");
5354    table_options.layout = LayoutStyle::column()
5355        .with_width(0.0)
5356        .with_flex_grow(1.0)
5357        .with_flex_shrink(1.0);
5358    table_options.selection = state.table_selection.clone();
5359    let virtual_shell = row(ui, body, "lists_tables.virtualized_table.shell", 8.0);
5360    ext_widgets::virtualized_data_table(
5361        ui,
5362        virtual_shell,
5363        "lists_tables.virtualized_table",
5364        &columns,
5365        ext_widgets::VirtualDataTableSpec {
5366            row_count: visible_rows.len(),
5367            row_height: 28.0,
5368            viewport_width: 420.0,
5369            viewport_height: 128.0,
5370            scroll_offset: UiPoint::new(0.0, state.virtual_table_scroll),
5371            overscan_rows: 1,
5372        },
5373        table_options,
5374        |ui, cell_parent, cell| {
5375            let source_row = visible_rows.get(cell.row).copied().unwrap_or(cell.row);
5376            let value = virtual_table_cell_value(source_row, cell.column);
5377            widgets::label(
5378                ui,
5379                cell_parent,
5380                format!(
5381                    "lists_tables.virtualized_table.cell.{}.{}.label",
5382                    cell.row, cell.column
5383                ),
5384                value,
5385                text(12.0, color(220, 228, 238)),
5386                LayoutStyle::new().with_width_percent(1.0),
5387            );
5388        },
5389    );
5390    scrollbar_widgets::scrollbar(
5391        ui,
5392        virtual_shell,
5393        "lists_tables.virtualized_table.scrollbar",
5394        scroll_state(
5395            state.virtual_table_scroll,
5396            128.0,
5397            visible_rows.len() as f32 * 28.0,
5398        ),
5399        scrollbar_widgets::ScrollAxis::Vertical,
5400        scrollbar_widgets::ScrollbarOptions::default()
5401            .with_layout(LayoutStyle::size(8.0, 158.0))
5402            .with_track_size(UiSize::new(8.0, 158.0))
5403            .with_action("lists_tables.virtualized_table.scrollbar"),
5404    );
5405}
5406
5407fn data_table_row(ui: &mut UiDocument, parent: UiNodeId, row_index: usize, state: &ShowcaseState) {
5408    let selected = state.table_selection.contains_row(row_index);
5409    let row = ui.add_child(
5410        parent,
5411        UiNode::container(
5412            format!("lists_tables.data_table.row.{row_index}"),
5413            LayoutStyle::row()
5414                .with_width_percent(1.0)
5415                .with_height(28.0)
5416                .with_flex_shrink(0.0),
5417        )
5418        .with_input(operad::InputBehavior::BUTTON)
5419        .with_action(format!("lists_tables.data_table.row.{row_index}"))
5420        .with_visual(if selected {
5421            UiVisual::panel(color(45, 73, 109), None, 0.0)
5422        } else {
5423            UiVisual::TRANSPARENT
5424        }),
5425    );
5426    let values = [
5427        format!("Item {}", row_index + 1),
5428        if row_index % 2 == 0 {
5429            "Ready".to_string()
5430        } else {
5431            "Pending".to_string()
5432        },
5433        format!("{}%", 40 + row_index * 3),
5434    ];
5435    let widths = [0.42, 0.33, 0.25];
5436    for (column, value) in values.into_iter().enumerate() {
5437        let cell = ui.add_child(
5438            row,
5439            UiNode::container(
5440                format!("lists_tables.data_table.cell.{row_index}.{column}"),
5441                LayoutStyle::new()
5442                    .with_width_percent(widths[column])
5443                    .with_height_percent(1.0)
5444                    .padding(6.0),
5445            )
5446            .with_input(operad::InputBehavior::BUTTON)
5447            .with_action(format!("lists_tables.data_table.cell.{row_index}.{column}")),
5448        );
5449        widgets::label(
5450            ui,
5451            cell,
5452            format!("lists_tables.data_table.cell.{row_index}.{column}.label"),
5453            value,
5454            text(12.0, color(222, 230, 240)),
5455            LayoutStyle::new().with_width_percent(1.0),
5456        );
5457    }
5458}
5459
5460#[allow(clippy::field_reassign_with_default)]
5461fn property_inspector(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5462    let body = section(ui, parent, "property_inspector", "Property inspector");
5463    widgets::label(
5464        ui,
5465        body,
5466        "property_inspector.target",
5467        "Inspecting: Styling preview",
5468        text(12.0, color(196, 210, 230)),
5469        LayoutStyle::new().with_width_percent(1.0),
5470    );
5471    let mut options = ext_widgets::PropertyInspectorOptions::default();
5472    options.selected_index = Some(0);
5473    options.label_width = 120.0;
5474    options.row_height = 30.0;
5475    ext_widgets::property_inspector_grid(
5476        ui,
5477        body,
5478        "property_inspector.grid",
5479        &[
5480            ext_widgets::PropertyGridRow::new("target", "Widget", "Button preview").read_only(),
5481            ext_widgets::PropertyGridRow::new(
5482                "inner",
5483                "Inner margin",
5484                format!("{:.0}px", state.styling.inner_margin),
5485            )
5486            .with_kind(ext_widgets::PropertyValueKind::Number),
5487            ext_widgets::PropertyGridRow::new(
5488                "outer",
5489                "Outer margin",
5490                format!("{:.0}px", state.styling.outer_margin),
5491            )
5492            .with_kind(ext_widgets::PropertyValueKind::Number),
5493            ext_widgets::PropertyGridRow::new(
5494                "radius",
5495                "Corner radius",
5496                format!("{:.0}px", state.styling.corner_radius),
5497            )
5498            .with_kind(ext_widgets::PropertyValueKind::Number),
5499            ext_widgets::PropertyGridRow::new(
5500                "stroke",
5501                "Stroke",
5502                format!("{:.1}px", state.styling.stroke_width),
5503            )
5504            .with_kind(ext_widgets::PropertyValueKind::Number)
5505            .changed(),
5506            ext_widgets::PropertyGridRow::new("state", "Source", "Styling widget").read_only(),
5507        ],
5508        options,
5509    );
5510}
5511
5512fn diagnostics_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5513    let body = section(ui, parent, "diagnostics", "Diagnostics");
5514
5515    widgets::label(
5516        ui,
5517        body,
5518        "diagnostics.layout.title",
5519        "Layout and animation inspector",
5520        text(14.0, color(222, 230, 240)),
5521        LayoutStyle::new().with_width_percent(1.0),
5522    );
5523    let debug_snapshot = &state.diagnostics_snapshot;
5524    ext_widgets::debug_inspector_panel(
5525        ui,
5526        body,
5527        "diagnostics.inspector",
5528        debug_snapshot,
5529        ext_widgets::DebugInspectorPanelOptions {
5530            selected_node: Some("diagnostics.sample.preview".to_owned()),
5531            label_width: 104.0,
5532            max_layout_rows: 5,
5533            max_animation_rows: 1,
5534            show_animation: false,
5535            ..Default::default()
5536        },
5537    );
5538    ext_widgets::animation_state_graph_panel(
5539        ui,
5540        body,
5541        "diagnostics.animation.graph",
5542        debug_snapshot.animation("diagnostics.sample.preview"),
5543        ext_widgets::AnimationStateGraphPanelOptions {
5544            state_width: 72.0,
5545            state_height: 28.0,
5546            edge_row_height: 22.0,
5547            max_edges: 2,
5548            action_prefix: Some("diagnostics.animation.graph".to_owned()),
5549            ..Default::default()
5550        },
5551    );
5552    ext_widgets::animation_inspector_controls_panel(
5553        ui,
5554        body,
5555        "diagnostics.animation.controls",
5556        debug_snapshot.animation("diagnostics.sample.preview"),
5557        ext_widgets::AnimationInspectorControlsOptions {
5558            max_inputs: 3,
5559            paused: state.diagnostics_animation_paused,
5560            scrub_progress: Some(state.diagnostics_animation_scrub),
5561            action_prefix: Some("diagnostics.animation.controls".to_owned()),
5562            ..Default::default()
5563        },
5564    );
5565    widgets::label(
5566        ui,
5567        body,
5568        "diagnostics.animation.controls.status",
5569        format!(
5570            "scrub {:.0}%  hover {:.0}%  pulses {}",
5571            state.diagnostics_animation_scrub * 100.0,
5572            state.diagnostics_animation_hover * 100.0,
5573            state.diagnostics_animation_pulse_count
5574        ),
5575        text(12.0, color(166, 180, 198)),
5576        LayoutStyle::new().with_width_percent(1.0),
5577    );
5578
5579    widgets::label(
5580        ui,
5581        body,
5582        "diagnostics.a11y.title",
5583        "Accessibility overlay",
5584        text(14.0, color(222, 230, 240)),
5585        LayoutStyle::new().with_width_percent(1.0),
5586    );
5587    let mut overlay_preview_style = UiNodeStyle::from(
5588        LayoutStyle::new()
5589            .with_width(320.0)
5590            .with_height(140.0)
5591            .with_flex_shrink(0.0),
5592    );
5593    overlay_preview_style.set_clip(ClipBehavior::Clip);
5594    let overlay_preview = ui.add_child(
5595        body,
5596        UiNode::container("diagnostics.a11y.preview", overlay_preview_style).with_visual(
5597            UiVisual::panel(
5598                color(12, 17, 24),
5599                Some(StrokeStyle::new(color(47, 62, 82), 1.0)),
5600                4.0,
5601            ),
5602        ),
5603    );
5604    let mut overlay_options = ext_widgets::AccessibilityDebugOverlayOptions {
5605        action_prefix: Some("diagnostics.a11y.visual".to_owned()),
5606        ..Default::default()
5607    };
5608    overlay_options.show_labels = false;
5609    ext_widgets::accessibility_debug_overlay(
5610        ui,
5611        overlay_preview,
5612        "diagnostics.a11y.visual",
5613        &debug_snapshot,
5614        overlay_options,
5615    );
5616    ext_widgets::accessibility_overlay_panel(
5617        ui,
5618        body,
5619        "diagnostics.a11y",
5620        &debug_snapshot,
5621        ext_widgets::AccessibilityOverlayPanelOptions {
5622            label_width: 118.0,
5623            max_rows: 1,
5624            action_prefix: Some("diagnostics.a11y".to_owned()),
5625            ..Default::default()
5626        },
5627    );
5628
5629    let diagnostic_columns = ui.add_child(
5630        body,
5631        UiNode::container(
5632            "diagnostics.columns",
5633            LayoutStyle::column()
5634                .with_width_percent(1.0)
5635                .with_flex_shrink(0.0)
5636                .gap(10.0),
5637        ),
5638    );
5639    let command_column = ui.add_child(
5640        diagnostic_columns,
5641        UiNode::container(
5642            "diagnostics.commands.column",
5643            LayoutStyle::column()
5644                .with_width_percent(1.0)
5645                .with_flex_shrink(0.0)
5646                .gap(8.0),
5647        ),
5648    );
5649    let theme_column = ui.add_child(
5650        diagnostic_columns,
5651        UiNode::container(
5652            "diagnostics.theme.column",
5653            LayoutStyle::column()
5654                .with_width_percent(1.0)
5655                .with_flex_shrink(0.0)
5656                .gap(8.0),
5657        ),
5658    );
5659
5660    widgets::label(
5661        ui,
5662        command_column,
5663        "diagnostics.commands.title",
5664        "Command registry",
5665        text(14.0, color(222, 230, 240)),
5666        LayoutStyle::new().with_width_percent(1.0),
5667    );
5668    let registry = diagnostics_command_registry();
5669    ext_widgets::command_diagnostics_panel(
5670        ui,
5671        command_column,
5672        "diagnostics.commands",
5673        &registry,
5674        &[CommandScope::Global, CommandScope::Panel],
5675        &ShortcutFormatter::default(),
5676        ext_widgets::CommandDiagnosticsPanelOptions {
5677            label_width: 92.0,
5678            max_command_rows: 3,
5679            max_conflict_rows: 1,
5680            action_prefix: Some("diagnostics.commands".to_owned()),
5681            ..Default::default()
5682        },
5683    );
5684
5685    widgets::label(
5686        ui,
5687        theme_column,
5688        "diagnostics.theme.title",
5689        "Theme editor",
5690        text(14.0, color(222, 230, 240)),
5691        LayoutStyle::new().with_width_percent(1.0),
5692    );
5693    let theme_snapshot = DebugThemeSnapshot::from_theme(&Theme::dark());
5694    ext_widgets::theme_editor_panel(
5695        ui,
5696        theme_column,
5697        "diagnostics.theme",
5698        &theme_snapshot,
5699        ext_widgets::ThemeEditorPanelOptions {
5700            label_width: 92.0,
5701            max_token_rows: 1,
5702            max_component_rows: 1,
5703            action_prefix: Some("diagnostics.theme".to_owned()),
5704            ..Default::default()
5705        },
5706    );
5707}
5708
5709fn diagnostics_sample_snapshot(state: &ShowcaseState) -> DebugInspectorSnapshot {
5710    diagnostics_sample_snapshot_for(
5711        state.diagnostics_animation_hover,
5712        state.diagnostics_animation_active,
5713    )
5714}
5715
5716fn diagnostics_sample_snapshot_for(hover: f32, active: bool) -> DebugInspectorSnapshot {
5717    let mut sample = UiDocument::new(root_style(320.0, 180.0));
5718    let card = sample.add_child(
5719        sample.root(),
5720        UiNode::container(
5721            "diagnostics.sample.card",
5722            LayoutStyle::column()
5723                .with_width_percent(1.0)
5724                .with_height(120.0)
5725                .padding(12.0)
5726                .gap(8.0),
5727        )
5728        .with_visual(UiVisual::panel(
5729            color(16, 22, 30),
5730            Some(StrokeStyle::new(color(62, 77, 98), 1.0)),
5731            6.0,
5732        ))
5733        .with_accessibility(
5734            AccessibilityMeta::new(AccessibilityRole::Group).label("Diagnostics sample"),
5735        ),
5736    );
5737    sample.add_child(
5738        card,
5739        UiNode::container(
5740            "diagnostics.sample.preview",
5741            LayoutStyle::new().with_width(160.0).with_height(38.0),
5742        )
5743        .with_input(InputBehavior::BUTTON)
5744        .with_visual(UiVisual::panel(
5745            color(52, 112, 180),
5746            Some(StrokeStyle::new(color(116, 183, 255), 1.0)),
5747            5.0,
5748        ))
5749        .with_accessibility(
5750            AccessibilityMeta::new(AccessibilityRole::Button)
5751                .label("Preview action")
5752                .focusable(),
5753        )
5754        .with_animation(
5755            AnimationMachine::new(
5756                vec![
5757                    AnimationState::new(
5758                        "idle",
5759                        AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0),
5760                    ),
5761                    AnimationState::new(
5762                        "hot",
5763                        AnimatedValues::new(0.92, UiPoint::new(18.0, 0.0), 1.08),
5764                    ),
5765                ],
5766                vec![AnimationTransition::when(
5767                    "idle",
5768                    "hot",
5769                    AnimationCondition::bool("active", true),
5770                    0.18,
5771                )],
5772                "idle",
5773            )
5774            .expect("sample animation")
5775            .with_number_input("hover", hover)
5776            .with_blend_binding(AnimationBlendBinding::new("hover", "idle", "hot"))
5777            .with_bool_input("active", active)
5778            .with_trigger_input("pulse"),
5779        ),
5780    );
5781    widgets::label(
5782        &mut sample,
5783        card,
5784        "diagnostics.sample.label",
5785        "Sample node",
5786        text(12.0, color(198, 210, 226)),
5787        LayoutStyle::new().with_width_percent(1.0),
5788    );
5789    sample
5790        .compute_layout(UiSize::new(320.0, 180.0), &mut ApproxTextMeasurer)
5791        .expect("sample layout");
5792    DebugInspectorSnapshot::from_document(&sample, &mut ApproxTextMeasurer)
5793}
5794
5795fn diagnostics_command_registry() -> CommandRegistry {
5796    let mut registry = CommandRegistry::new();
5797    registry
5798        .register(
5799            CommandMeta::new("diagnostics.palette", "Open command palette")
5800                .description("Show command search")
5801                .category("Debug"),
5802        )
5803        .expect("command");
5804    registry
5805        .register(
5806            CommandMeta::new("diagnostics.inspect", "Inspect selected node")
5807                .description("Focus the layout inspector")
5808                .category("Debug"),
5809        )
5810        .expect("command");
5811    registry
5812        .register(
5813            CommandMeta::new("diagnostics.record", "Start interaction recording")
5814                .description("Capture replay steps")
5815                .category("Testing"),
5816        )
5817        .expect("command");
5818    registry
5819        .register(CommandMeta::new(
5820            "diagnostics.export_theme",
5821            "Export theme patch",
5822        ))
5823        .expect("command");
5824    registry
5825        .bind_shortcut(
5826            CommandScope::Global,
5827            Shortcut::ctrl('k'),
5828            "diagnostics.palette",
5829        )
5830        .expect("shortcut");
5831    registry
5832        .bind_shortcut(
5833            CommandScope::Panel,
5834            Shortcut::ctrl('i'),
5835            "diagnostics.inspect",
5836        )
5837        .expect("shortcut");
5838    registry
5839        .bind_shortcut(
5840            CommandScope::Panel,
5841            Shortcut::ctrl('r'),
5842            "diagnostics.record",
5843        )
5844        .expect("shortcut");
5845    registry
5846        .disable("diagnostics.export_theme", "No changes to export")
5847        .expect("disable");
5848    registry
5849}
5850
5851fn tree_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5852    let body = section(ui, parent, "trees", "Tree view");
5853    ext_widgets::tree_view(
5854        ui,
5855        body,
5856        "trees.tree_view",
5857        &tree_items(),
5858        &state.tree,
5859        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.tree"),
5860    );
5861    ext_widgets::outliner(
5862        ui,
5863        body,
5864        "trees.outliner",
5865        &tree_items(),
5866        &state.outliner,
5867        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.outliner"),
5868    );
5869    let virtual_state = ext_widgets::TreeViewState::expanded(["root"]);
5870    let virtual_nodes = ext_widgets::virtualized_tree_view(
5871        ui,
5872        body,
5873        "trees.virtual",
5874        &virtual_tree_items(),
5875        &virtual_state,
5876        ext_widgets::VirtualTreeViewSpec::new(24.0, 112.0)
5877            .scroll_offset(state.tree_virtual_scroll)
5878            .overscan_rows(1),
5879        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.virtual"),
5880    );
5881    ui.node_mut(virtual_nodes.body)
5882        .set_action("trees.virtual.scroll");
5883    tree_table_widgets(ui, body, state);
5884}
5885
5886fn tree_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5887    let tree_state = ext_widgets::TreeViewState::expanded(["root", "branch-a"]);
5888    let rows = tree_state.visible_items(&tree_table_items());
5889    let columns = [
5890        ext_widgets::DataTableColumn::new("name", "Name", 220.0),
5891        ext_widgets::DataTableColumn::new("kind", "Kind", 84.0),
5892        ext_widgets::DataTableColumn::new("status", "Status", 92.0),
5893    ];
5894    let mut options = ext_widgets::DataTableOptions::default()
5895        .with_row_action_prefix("trees.table")
5896        .with_cell_action_prefix("trees.table");
5897    options.layout = LayoutStyle::column()
5898        .with_width_percent(1.0)
5899        .with_height(132.0)
5900        .with_flex_shrink(0.0);
5901    ext_widgets::virtualized_data_table(
5902        ui,
5903        parent,
5904        "trees.table",
5905        &columns,
5906        ext_widgets::VirtualDataTableSpec {
5907            row_count: rows.len(),
5908            row_height: 24.0,
5909            viewport_width: 396.0,
5910            viewport_height: 96.0,
5911            scroll_offset: UiPoint::new(0.0, state.tree_virtual_scroll),
5912            overscan_rows: 1,
5913        },
5914        options,
5915        |ui, cell_parent, cell| {
5916            let value = rows
5917                .get(cell.row)
5918                .map(|item| tree_table_cell_value(item, cell.column))
5919                .unwrap_or_default();
5920            widgets::label(
5921                ui,
5922                cell_parent,
5923                format!("trees.table.cell.{}.{}.label", cell.row, cell.column),
5924                value,
5925                text(12.0, color(220, 228, 238)),
5926                LayoutStyle::new().with_width_percent(1.0),
5927            );
5928        },
5929    );
5930}
5931
5932fn tree_table_cell_value(item: &ext_widgets::TreeVisibleItem, column: usize) -> String {
5933    match column {
5934        0 => format!("{}{}", "  ".repeat(item.depth), item.label),
5935        1 => {
5936            if item.has_children() {
5937                "Folder".to_owned()
5938            } else {
5939                "File".to_owned()
5940            }
5941        }
5942        _ => {
5943            if item.disabled {
5944                "Locked".to_owned()
5945            } else if item.expanded {
5946                "Expanded".to_owned()
5947            } else {
5948                "Ready".to_owned()
5949            }
5950        }
5951    }
5952}
5953
5954fn tab_split_dock_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5955    let body = section_with_min_viewport(
5956        ui,
5957        parent,
5958        "layout_widgets",
5959        "Dock workspace",
5960        UiSize::new(546.0, 360.0),
5961    );
5962    let shell = ui.add_child(
5963        body,
5964        UiNode::container(
5965            "layout_widgets.dock_shell",
5966            LayoutStyle::column()
5967                .with_width_percent(1.0)
5968                .with_height(360.0)
5969                .with_flex_shrink(0.0),
5970        )
5971        .with_visual(UiVisual::panel(
5972            color(13, 17, 23),
5973            Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
5974            0.0,
5975        )),
5976    );
5977
5978    let mut panels = base_layout_dock_panels();
5979    state.layout_dock.apply_order_to_panels(&mut panels);
5980    state.layout_dock.apply_visibility_to_panels(&mut panels);
5981
5982    let mut drawer_options = ext_widgets::DockDrawerRailOptions::default();
5983    drawer_options.layout = LayoutStyle::row()
5984        .with_width_percent(1.0)
5985        .with_height(34.0)
5986        .with_padding(4.0)
5987        .with_gap(4.0);
5988    ext_widgets::dock_drawer_rail(
5989        ui,
5990        shell,
5991        "layout_widgets.dock.drawers",
5992        &[
5993            ext_widgets::DockDrawerDescriptor::new(
5994                "inspector",
5995                "Inspector",
5996                "inspector",
5997                ext_widgets::DockSide::Left,
5998            )
5999            .open(!state.layout_dock.is_hidden("inspector"))
6000            .with_action("layout_widgets.drawer.inspector"),
6001            ext_widgets::DockDrawerDescriptor::new(
6002                "assets",
6003                "Assets",
6004                "assets",
6005                ext_widgets::DockSide::Right,
6006            )
6007            .open(!state.layout_dock.is_hidden("assets"))
6008            .with_action("layout_widgets.drawer.assets"),
6009        ],
6010        drawer_options,
6011    );
6012
6013    let mut options = ext_widgets::DockWorkspaceOptions::default();
6014    options.layout = LayoutStyle::column()
6015        .with_width_percent(1.0)
6016        .with_height(0.0)
6017        .with_flex_grow(1.0);
6018    options.show_titles = false;
6019    options.panel_visual = UiVisual::panel(
6020        color(18, 22, 29),
6021        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
6022        0.0,
6023    );
6024    options.center_visual = UiVisual::panel(
6025        color(15, 19, 25),
6026        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
6027        0.0,
6028    );
6029
6030    ext_widgets::dock_workspace(
6031        ui,
6032        shell,
6033        "layout_widgets.dock",
6034        &panels,
6035        options,
6036        |ui, parent, panel| match panel.id.as_str() {
6037            "inspector" => egui_panel_contents(
6038                ui,
6039                parent,
6040                "layout.inspector",
6041                "Inspector",
6042                state.layout_inspector_scroll,
6043            ),
6044            "assets" => egui_panel_contents(
6045                ui,
6046                parent,
6047                "layout.assets",
6048                "Assets",
6049                state.layout_assets_scroll,
6050            ),
6051            _ => dock_document_panel(ui, parent, state),
6052        },
6053    );
6054
6055    if let Some(floating) = state.layout_dock.floating_panel("inspector") {
6056        let floating_panel = ui.add_child(
6057            shell,
6058            UiNode::container(
6059                "layout_widgets.floating.inspector",
6060                operad::layout::absolute(
6061                    floating.rect.x,
6062                    floating.rect.y,
6063                    floating.rect.width,
6064                    floating.rect.height,
6065                ),
6066            )
6067            .with_visual(UiVisual::panel(
6068                color(18, 22, 29),
6069                Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
6070                4.0,
6071            )),
6072        );
6073        egui_panel_contents(
6074            ui,
6075            floating_panel,
6076            "layout.inspector_floating",
6077            "Inspector",
6078            state.layout_inspector_scroll,
6079        );
6080    }
6081}
6082
6083fn base_layout_dock_panels() -> Vec<ext_widgets::DockPanelDescriptor> {
6084    vec![
6085        ext_widgets::DockPanelDescriptor::new(
6086            "inspector",
6087            "Inspector",
6088            ext_widgets::DockSide::Left,
6089            120.0,
6090        )
6091        .with_min_size(104.0)
6092        .resizable(true),
6093        ext_widgets::DockPanelDescriptor::center("document", "Document"),
6094        ext_widgets::DockPanelDescriptor::new(
6095            "assets",
6096            "Assets",
6097            ext_widgets::DockSide::Right,
6098            104.0,
6099        )
6100        .with_min_size(94.0)
6101        .resizable(true),
6102    ]
6103}
6104
6105fn dock_document_panel(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6106    let content = ui.add_child(
6107        parent,
6108        UiNode::container(
6109            "layout_widgets.document.content",
6110            LayoutStyle::column()
6111                .with_width_percent(1.0)
6112                .with_height_percent(1.0)
6113                .with_padding(8.0)
6114                .with_gap(8.0),
6115        ),
6116    );
6117
6118    let controls = wrapping_row(ui, content, "layout_widgets.dock.controls", 8.0);
6119    let (action, label) = if state.layout_dock.is_floating("inspector") {
6120        ("layout_widgets.dock_inspector", "Dock inspector")
6121    } else {
6122        ("layout_widgets.float_inspector", "Float inspector")
6123    };
6124    let mut float_button = widgets::ButtonOptions::new(
6125        LayoutStyle::new()
6126            .with_width(132.0)
6127            .with_height(28.0)
6128            .with_flex_shrink(0.0),
6129    )
6130    .with_action(action);
6131    float_button.visual = button_visual(40, 52, 68);
6132    float_button.hovered_visual = Some(button_visual(54, 70, 92));
6133    float_button.text_style = text(12.0, color(232, 238, 248));
6134    widgets::button(
6135        ui,
6136        controls,
6137        "layout_widgets.dock.float_inspector",
6138        label,
6139        float_button,
6140    );
6141
6142    let mut before_button = widgets::ButtonOptions::new(
6143        LayoutStyle::new()
6144            .with_width(136.0)
6145            .with_height(28.0)
6146            .with_flex_shrink(0.0),
6147    )
6148    .with_action("layout_widgets.reorder.assets.before.inspector");
6149    before_button.visual = button_visual(34, 44, 58);
6150    before_button.hovered_visual = Some(button_visual(48, 64, 84));
6151    before_button.text_style = text(12.0, color(232, 238, 248));
6152    widgets::button(
6153        ui,
6154        controls,
6155        "layout_widgets.dock.assets_before_inspector",
6156        "Assets before",
6157        before_button,
6158    );
6159
6160    let mut after_button = widgets::ButtonOptions::new(
6161        LayoutStyle::new()
6162            .with_width(126.0)
6163            .with_height(28.0)
6164            .with_flex_shrink(0.0),
6165    )
6166    .with_action("layout_widgets.reorder.assets.after.inspector");
6167    after_button.visual = button_visual(34, 44, 58);
6168    after_button.hovered_visual = Some(button_visual(48, 64, 84));
6169    after_button.text_style = text(12.0, color(232, 238, 248));
6170    widgets::button(
6171        ui,
6172        controls,
6173        "layout_widgets.dock.assets_after_inspector",
6174        "Assets after",
6175        after_button,
6176    );
6177
6178    let zones = ext_widgets::dock_workspace::dock_workspace_drop_zones(
6179        "layout_widgets.dock",
6180        UiRect::new(0.0, 0.0, 520.0, 340.0),
6181        ext_widgets::DockWorkspaceDragOptions::default()
6182            .allowed_sides([
6183                ext_widgets::DockSide::Left,
6184                ext_widgets::DockSide::Right,
6185                ext_widgets::DockSide::Center,
6186            ])
6187            .edge_thickness(44.0),
6188    );
6189    let targets = wrapping_row(ui, content, "layout_widgets.dock.targets", 6.0);
6190    for zone in zones {
6191        dock_drop_target_chip(ui, targets, &zone);
6192    }
6193
6194    let mut panels = base_layout_dock_panels();
6195    state.layout_dock.apply_order_to_panels(&mut panels);
6196    let reorder_targets: Vec<_> = [
6197        ext_widgets::DockSide::Left,
6198        ext_widgets::DockSide::Right,
6199        ext_widgets::DockSide::Center,
6200    ]
6201    .into_iter()
6202    .flat_map(|side| {
6203        ext_widgets::dock_workspace::dock_panel_reorder_drop_targets(
6204            "layout_widgets.dock",
6205            &panels,
6206            side,
6207            UiRect::new(0.0, 0.0, 180.0, 120.0),
6208            ext_widgets::DockWorkspaceReorderOptions::default().target_thickness(20.0),
6209        )
6210    })
6211    .collect();
6212    let reorder_row = wrapping_row(ui, content, "layout_widgets.dock.reorder_targets", 6.0);
6213    for target in reorder_targets {
6214        dock_reorder_target_chip(ui, reorder_row, &target);
6215    }
6216
6217    let tabs = [
6218        ext_widgets::TabItem::new("preview", "Preview"),
6219        ext_widgets::TabItem::new("log", "Output").dirty(),
6220        ext_widgets::TabItem::new("settings", "Settings").closable(),
6221    ];
6222    let mut tab_options = ext_widgets::TabGroupOptions::default();
6223    tab_options.layout = LayoutStyle::column()
6224        .with_width_percent(1.0)
6225        .with_height(0.0)
6226        .with_flex_grow(1.0);
6227    tab_options.tab_strip_height = 30.0;
6228    tab_options.min_tab_width = 92.0;
6229    tab_options.text_style = text(12.0, color(226, 234, 246));
6230    tab_options.muted_text_style = text(12.0, color(150, 162, 178));
6231    ext_widgets::tab_group(
6232        ui,
6233        content,
6234        "layout_widgets.document.tabs",
6235        &tabs,
6236        ext_widgets::TabGroupState::selected(0),
6237        tab_options,
6238        |ui, panel, _index| {
6239            widgets::label(
6240                ui,
6241                panel,
6242                "layout_widgets.document.tabs.preview.body",
6243                "Workspace preview",
6244                text(12.0, color(190, 202, 218)),
6245                LayoutStyle::new().with_width_percent(1.0).with_height(26.0),
6246            );
6247        },
6248    );
6249}
6250
6251fn dock_drop_target_chip(
6252    ui: &mut UiDocument,
6253    parent: UiNodeId,
6254    zone: &ext_widgets::DockWorkspaceDropZone,
6255) -> UiNodeId {
6256    let chip = ui.add_child(
6257        parent,
6258        UiNode::container(
6259            format!("{}.chip", zone.target.id.as_str()),
6260            LayoutStyle::row()
6261                .with_width(78.0)
6262                .with_height(26.0)
6263                .with_padding(6.0)
6264                .with_flex_shrink(0.0),
6265        )
6266        .with_input(InputBehavior::BUTTON)
6267        .with_visual(UiVisual::panel(
6268            color(24, 32, 42),
6269            Some(StrokeStyle::new(color(78, 94, 116), 1.0)),
6270            4.0,
6271        ))
6272        .with_accessibility(zone.target.accessibility_meta()),
6273    );
6274    widgets::label(
6275        ui,
6276        chip,
6277        format!("{}.label", zone.target.id.as_str()),
6278        dock_drop_target_short_label(zone.placement),
6279        text(11.0, color(206, 216, 230)),
6280        LayoutStyle::new().with_width_percent(1.0),
6281    );
6282    chip
6283}
6284
6285fn dock_reorder_target_chip(
6286    ui: &mut UiDocument,
6287    parent: UiNodeId,
6288    target: &ext_widgets::DockPanelReorderTarget,
6289) -> UiNodeId {
6290    let chip = ui.add_child(
6291        parent,
6292        UiNode::container(
6293            format!("{}.chip", target.target.id.as_str()),
6294            LayoutStyle::row()
6295                .with_width(104.0)
6296                .with_height(26.0)
6297                .with_padding(6.0)
6298                .with_flex_shrink(0.0),
6299        )
6300        .with_input(InputBehavior::BUTTON)
6301        .with_visual(UiVisual::panel(
6302            color(22, 34, 42),
6303            Some(StrokeStyle::new(color(80, 112, 128), 1.0)),
6304            4.0,
6305        ))
6306        .with_accessibility(target.target.accessibility_meta()),
6307    );
6308    widgets::label(
6309        ui,
6310        chip,
6311        format!("{}.label", target.target.id.as_str()),
6312        dock_reorder_target_short_label(target),
6313        text(11.0, color(206, 216, 230)),
6314        LayoutStyle::new().with_width_percent(1.0),
6315    );
6316    chip
6317}
6318
6319fn dock_drop_target_short_label(placement: ext_widgets::DockDropPlacement) -> &'static str {
6320    match placement {
6321        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Left) => "Left",
6322        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Right) => "Right",
6323        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Center) => "Center",
6324        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Top) => "Top",
6325        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Bottom) => "Bottom",
6326        ext_widgets::DockDropPlacement::Floating => "Float",
6327    }
6328}
6329
6330fn dock_reorder_target_short_label(target: &ext_widgets::DockPanelReorderTarget) -> String {
6331    let placement = match target.placement {
6332        ext_widgets::DockPanelReorderPlacement::Before => "Before",
6333        ext_widgets::DockPanelReorderPlacement::After => "After",
6334    };
6335    format!("{placement} {}", target.panel_id)
6336}
6337
6338fn container_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6339    let body = section(ui, parent, "containers", "Containers");
6340
6341    let frame = widgets::frame(
6342        ui,
6343        body,
6344        "containers.frame",
6345        widgets::FrameOptions::default().with_layout(
6346            LayoutStyle::column()
6347                .with_width_percent(1.0)
6348                .with_height(64.0)
6349                .with_padding(8.0)
6350                .with_gap(6.0),
6351        ),
6352    );
6353    widgets::strong_label(
6354        ui,
6355        frame,
6356        "containers.frame.title",
6357        "Frame",
6358        LayoutStyle::new().with_width_percent(1.0),
6359    );
6360    widgets::weak_label(
6361        ui,
6362        frame,
6363        "containers.frame.body",
6364        "Default framed surface with padding, stroke, and clipping.",
6365        LayoutStyle::new().with_width_percent(1.0),
6366    );
6367
6368    let group = widgets::group(ui, body, "containers.group");
6369    widgets::label(
6370        ui,
6371        group,
6372        "containers.group.label",
6373        "Group helper",
6374        text(12.0, color(220, 228, 238)),
6375        LayoutStyle::new().with_width_percent(1.0),
6376    );
6377    let generic_panel = widgets::panel(
6378        ui,
6379        body,
6380        "containers.panel",
6381        widgets::PanelOptions::group().with_layout(
6382            LayoutStyle::column()
6383                .with_width_percent(1.0)
6384                .with_height(44.0)
6385                .with_padding(8.0),
6386        ),
6387    );
6388    widgets::label(
6389        ui,
6390        generic_panel,
6391        "containers.panel.label",
6392        "Generic panel",
6393        text(12.0, color(220, 228, 238)),
6394        LayoutStyle::new().with_width_percent(1.0),
6395    );
6396    let group_panel = widgets::group_panel(ui, body, "containers.group_panel");
6397    widgets::label(
6398        ui,
6399        group_panel,
6400        "containers.group_panel.label",
6401        "Group panel",
6402        text(12.0, color(220, 228, 238)),
6403        LayoutStyle::new().with_width_percent(1.0),
6404    );
6405
6406    widgets::separator(
6407        ui,
6408        body,
6409        "containers.separator",
6410        widgets::SeparatorOptions::default(),
6411    );
6412    widgets::spacer(
6413        ui,
6414        body,
6415        "containers.spacer",
6416        LayoutStyle::new()
6417            .with_width_percent(1.0)
6418            .with_height(8.0)
6419            .with_flex_shrink(0.0),
6420    );
6421
6422    let grid = widgets::grid::grid(
6423        ui,
6424        body,
6425        "containers.grid",
6426        widgets::grid::GridOptions::default().with_layout(
6427            LayoutStyle::column()
6428                .with_width_percent(1.0)
6429                .with_height(78.0)
6430                .with_gap(4.0),
6431        ),
6432    );
6433    for row_index in 0..2 {
6434        let row = widgets::grid::grid_row(
6435            ui,
6436            grid,
6437            format!("containers.grid.row.{row_index}"),
6438            widgets::grid::GridRowOptions::default(),
6439        );
6440        for column_index in 0..3 {
6441            widgets::grid::grid_text_cell(
6442                ui,
6443                row,
6444                format!("containers.grid.row.{row_index}.cell.{column_index}"),
6445                format!("R{} C{}", row_index + 1, column_index + 1),
6446                widgets::grid::GridCellOptions {
6447                    text_style: text(12.0, color(214, 224, 238)),
6448                    ..Default::default()
6449                },
6450            );
6451        }
6452    }
6453
6454    widgets::sides(
6455        ui,
6456        body,
6457        "containers.sides",
6458        widgets::SidesOptions::default()
6459            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
6460            .with_gap(8.0)
6461            .with_visual(UiVisual::panel(
6462                color(20, 25, 32),
6463                Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
6464                4.0,
6465            )),
6466        |ui, left| {
6467            widgets::label(
6468                ui,
6469                left,
6470                "containers.sides.left.label",
6471                "Left side",
6472                text(12.0, color(220, 228, 238)),
6473                LayoutStyle::new().with_width_percent(1.0),
6474            );
6475        },
6476        |ui, right| {
6477            widgets::label(
6478                ui,
6479                right,
6480                "containers.sides.right.label",
6481                "Right side",
6482                text(12.0, color(220, 228, 238)),
6483                LayoutStyle::new().with_width_percent(1.0),
6484            );
6485        },
6486    );
6487
6488    widgets::columns(
6489        ui,
6490        body,
6491        "containers.columns",
6492        3,
6493        widgets::ColumnsOptions::default()
6494            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
6495            .with_gap(8.0),
6496        |ui, column, index| {
6497            widgets::label(
6498                ui,
6499                column,
6500                format!("containers.columns.{index}.label"),
6501                format!("Column {}", index + 1),
6502                text(12.0, color(220, 228, 238)),
6503                LayoutStyle::new().with_width_percent(1.0),
6504            );
6505        },
6506    );
6507
6508    let indented = widgets::indented_section(
6509        ui,
6510        body,
6511        "containers.indented",
6512        widgets::IndentOptions::default().with_amount(24.0),
6513    );
6514    widgets::label(
6515        ui,
6516        indented,
6517        "containers.indented.label",
6518        "Indented section",
6519        text(12.0, color(196, 210, 230)),
6520        LayoutStyle::new().with_width_percent(1.0),
6521    );
6522
6523    widgets::resize_container(
6524        ui,
6525        body,
6526        "containers.resize_container",
6527        widgets::ResizeContainerOptions::default().with_layout(
6528            LayoutStyle::column()
6529                .with_width_percent(1.0)
6530                .with_height(92.0)
6531                .with_flex_shrink(0.0),
6532        ),
6533        |ui, content| {
6534            widgets::label(
6535                ui,
6536                content,
6537                "containers.resize_container.label",
6538                "Resize container",
6539                text(12.0, color(220, 228, 238)),
6540                LayoutStyle::new().with_width_percent(1.0),
6541            );
6542        },
6543    );
6544    widgets::container::resize_handle(
6545        ui,
6546        body,
6547        "containers.resize_handle",
6548        widgets::container::ResizeHandleOptions::default()
6549            .with_layout(LayoutStyle::size(20.0, 20.0))
6550            .accessibility_label("Inline resize handle"),
6551    );
6552
6553    widgets::scene(
6554        ui,
6555        body,
6556        "containers.scene",
6557        vec![
6558            ScenePrimitive::Rect(
6559                PaintRect::solid(UiRect::new(8.0, 12.0, 108.0, 46.0), color(48, 112, 184))
6560                    .stroke(AlignedStroke::inside(StrokeStyle::new(
6561                        color(132, 174, 222),
6562                        1.0,
6563                    )))
6564                    .corner_radii(CornerRadii::uniform(6.0)),
6565            ),
6566            ScenePrimitive::Circle {
6567                center: UiPoint::new(150.0, 35.0),
6568                radius: 22.0,
6569                fill: color(111, 203, 159),
6570                stroke: Some(StrokeStyle::new(color(176, 236, 206), 1.0)),
6571            },
6572            ScenePrimitive::Line {
6573                from: UiPoint::new(188.0, 18.0),
6574                to: UiPoint::new(238.0, 52.0),
6575                stroke: StrokeStyle::new(color(232, 186, 88), 3.0),
6576            },
6577        ],
6578        widgets::SceneOptions::default()
6579            .with_layout(LayoutStyle::new().with_width(260.0).with_height(70.0))
6580            .accessibility_label("Scene primitives"),
6581    );
6582
6583    let panel_shell = widgets::frame(
6584        ui,
6585        body,
6586        "containers.panels",
6587        widgets::FrameOptions::default().with_layout(
6588            LayoutStyle::column()
6589                .with_width_percent(1.0)
6590                .with_height(160.0)
6591                .with_padding(0.0)
6592                .with_gap(0.0),
6593        ),
6594    );
6595    let top = widgets::top_panel(ui, panel_shell, "containers.panels.top", 28.0);
6596    widgets::label(
6597        ui,
6598        top,
6599        "containers.panels.top.label",
6600        "Top panel",
6601        text(12.0, color(220, 228, 238)),
6602        LayoutStyle::new().with_width_percent(1.0),
6603    );
6604    let middle = row(ui, panel_shell, "containers.panels.middle", 0.0);
6605    let left = widgets::side_panel(
6606        ui,
6607        middle,
6608        "containers.panels.side",
6609        widgets::SidePanelSide::Left,
6610        90.0,
6611    );
6612    widgets::label(
6613        ui,
6614        left,
6615        "containers.panels.side.label",
6616        "Side",
6617        text(12.0, color(220, 228, 238)),
6618        LayoutStyle::new().with_width_percent(1.0),
6619    );
6620    let left = widgets::left_panel(ui, middle, "containers.panels.left", 90.0);
6621    widgets::label(
6622        ui,
6623        left,
6624        "containers.panels.left.label",
6625        "Left",
6626        text(12.0, color(220, 228, 238)),
6627        LayoutStyle::new().with_width_percent(1.0),
6628    );
6629    let center = widgets::central_panel(ui, middle, "containers.panels.center");
6630    widgets::label(
6631        ui,
6632        center,
6633        "containers.panels.center.label",
6634        "Central panel",
6635        text(12.0, color(220, 228, 238)),
6636        LayoutStyle::new().with_width_percent(1.0),
6637    );
6638    let right = widgets::right_panel(ui, middle, "containers.panels.right", 110.0);
6639    widgets::label(
6640        ui,
6641        right,
6642        "containers.panels.right.label",
6643        "Right",
6644        text(12.0, color(220, 228, 238)),
6645        LayoutStyle::new().with_width_percent(1.0),
6646    );
6647    let bottom = widgets::bottom_panel(ui, panel_shell, "containers.panels.bottom", 28.0);
6648    widgets::label(
6649        ui,
6650        bottom,
6651        "containers.panels.bottom.label",
6652        "Bottom panel",
6653        text(12.0, color(220, 228, 238)),
6654        LayoutStyle::new().with_width_percent(1.0),
6655    );
6656
6657    widgets::scroll_container(
6658        ui,
6659        body,
6660        "containers.scroll_area_with_bars",
6661        state.containers_scroll,
6662        widgets::ScrollContainerOptions::default()
6663            .with_axes(ScrollAxes::BOTH)
6664            .with_layout(LayoutStyle::column().with_width(300.0).with_height(116.0)),
6665        |ui, viewport| {
6666            for index in 0..5 {
6667                widgets::label(
6668                    ui,
6669                    viewport,
6670                    format!("containers.scroll_area_with_bars.row.{index}"),
6671                    format!("Scrollable row {}", index + 1),
6672                    text(12.0, color(200, 212, 228)),
6673                    LayoutStyle::new()
6674                        .with_width(420.0)
6675                        .with_height(28.0)
6676                        .with_flex_shrink(0.0),
6677                );
6678            }
6679        },
6680    );
6681
6682    let area_host = ui.add_child(
6683        body,
6684        UiNode::container(
6685            "containers.area.host",
6686            LayoutStyle::new()
6687                .with_width_percent(1.0)
6688                .with_height(82.0)
6689                .with_flex_shrink(0.0),
6690        )
6691        .with_visual(UiVisual::panel(
6692            color(17, 20, 25),
6693            Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
6694            4.0,
6695        )),
6696    );
6697    widgets::container::area(
6698        ui,
6699        area_host,
6700        "containers.area",
6701        widgets::container::AreaOptions::new(UiRect::new(14.0, 14.0, 180.0, 44.0))
6702            .with_visual(UiVisual::panel(color(39, 72, 109), None, 4.0))
6703            .accessibility_label("Absolute positioned area"),
6704        |ui, area| {
6705            widgets::label(
6706                ui,
6707                area,
6708                "containers.area.label",
6709                "Area",
6710                text(12.0, color(238, 244, 252)),
6711                LayoutStyle::new().with_width_percent(1.0),
6712            );
6713        },
6714    );
6715}
6716
6717fn form_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6718    let body = section_with_min_viewport(ui, parent, "forms", "Forms", UiSize::new(390.0, 0.0));
6719    let section = widgets::form_section(
6720        ui,
6721        body,
6722        "forms.profile",
6723        Some("Profile".to_string()),
6724        widgets::FormSectionOptions::default().with_layout(
6725            LayoutStyle::column()
6726                .with_width_percent(1.0)
6727                .with_padding(12.0)
6728                .with_gap(10.0),
6729        ),
6730    );
6731    let status_row = wrapping_row(ui, section.root, "forms.profile.status_flags", 6.0);
6732    form_status_chip(
6733        ui,
6734        status_row,
6735        "forms.profile.status.dirty",
6736        "dirty",
6737        state.form.dirty,
6738    );
6739    form_status_chip(
6740        ui,
6741        status_row,
6742        "forms.profile.status.pending",
6743        "pending",
6744        state.form.pending,
6745    );
6746    form_status_chip(
6747        ui,
6748        status_row,
6749        "forms.profile.status.submitted",
6750        "submitted",
6751        state.form.submitted,
6752    );
6753
6754    let mut name_options = widgets::FormRowOptions::default().required();
6755    if state.form_name_text.text().trim().is_empty() {
6756        name_options = name_options.invalid("Name is required");
6757    }
6758    let name = widgets::form_row(ui, section.root, "forms.profile.name", name_options);
6759    widgets::field_label(
6760        ui,
6761        name,
6762        "forms.profile.name.label",
6763        "Name",
6764        widgets::FieldLabelOptions::default().required(),
6765    );
6766    form_text_field(
6767        ui,
6768        name,
6769        "forms.profile.name.input",
6770        &state.form_name_text,
6771        FocusedTextInput::FormName,
6772        state,
6773    );
6774    if state.form_name_text.text().trim().is_empty() {
6775        widgets::field_validation_message(
6776            ui,
6777            name,
6778            "forms.profile.name.validation",
6779            ValidationMessage::error("Name is required"),
6780            widgets::ValidationMessageOptions::default(),
6781        );
6782    } else {
6783        widgets::field_help_text(
6784            ui,
6785            name,
6786            "forms.profile.name.help",
6787            "Shown in window titles and project lists.",
6788            widgets::FieldHelpOptions::default(),
6789        );
6790    }
6791
6792    let mut email_options = widgets::FormRowOptions::default().required();
6793    if !profile_email_valid(state.form_email_text.text()) {
6794        email_options = email_options.invalid("Use a complete email address");
6795    }
6796    let email = widgets::form_row(ui, section.root, "forms.profile.email", email_options);
6797    widgets::field_label(
6798        ui,
6799        email,
6800        "forms.profile.email.label",
6801        "Email",
6802        widgets::FieldLabelOptions::default().required(),
6803    );
6804    form_text_field(
6805        ui,
6806        email,
6807        "forms.profile.email.input",
6808        &state.form_email_text,
6809        FocusedTextInput::FormEmail,
6810        state,
6811    );
6812    if profile_email_valid(state.form_email_text.text()) {
6813        widgets::field_help_text(
6814            ui,
6815            email,
6816            "forms.profile.email.help",
6817            "Used for workspace invites and notifications.",
6818            widgets::FieldHelpOptions::default(),
6819        );
6820    } else {
6821        widgets::field_validation_message(
6822            ui,
6823            email,
6824            "forms.profile.email.validation",
6825            ValidationMessage::error("Use a complete email address"),
6826            widgets::ValidationMessageOptions::default(),
6827        );
6828    }
6829
6830    let role = widgets::form_row(
6831        ui,
6832        section.root,
6833        "forms.profile.role",
6834        widgets::FormRowOptions::default(),
6835    );
6836    widgets::field_label(
6837        ui,
6838        role,
6839        "forms.profile.role.label",
6840        "Role",
6841        widgets::FieldLabelOptions::default(),
6842    );
6843    form_text_field(
6844        ui,
6845        role,
6846        "forms.profile.role.input",
6847        &state.form_role_text,
6848        FocusedTextInput::FormRole,
6849        state,
6850    );
6851    widgets::field_validation_message(
6852        ui,
6853        role,
6854        "forms.profile.role.help",
6855        if state.form_role_text.text().trim().is_empty() {
6856            ValidationMessage::warning("Role can be added later")
6857        } else {
6858            ValidationMessage::info(
6859                "Form rows compose labels, controls, help, and validation text.",
6860            )
6861        },
6862        widgets::ValidationMessageOptions::default(),
6863    );
6864
6865    let newsletter = widgets::form_row(
6866        ui,
6867        section.root,
6868        "forms.profile.newsletter",
6869        widgets::FormRowOptions::default().with_accessibility_label("Newsletter preference"),
6870    );
6871    let mut newsletter_options =
6872        widgets::CheckboxOptions::default().with_action("forms.profile.newsletter.toggle");
6873    newsletter_options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
6874    newsletter_options.text_style = text(12.0, color(220, 228, 238));
6875    widgets::checkbox(
6876        ui,
6877        newsletter,
6878        "forms.profile.newsletter.input",
6879        "Send release notes",
6880        state.form_newsletter,
6881        newsletter_options,
6882    );
6883    widgets::field_help_text(
6884        ui,
6885        newsletter,
6886        "forms.profile.newsletter.help",
6887        "Checkboxes participate in the same form state as text fields.",
6888        widgets::FieldHelpOptions::default(),
6889    );
6890
6891    widgets::form_error_summary(
6892        ui,
6893        section.root,
6894        "forms.profile.errors",
6895        &state.form,
6896        widgets::FormErrorSummaryOptions::default(),
6897    );
6898    let action_layout = Layout::row()
6899        .size(LayoutSize::new(
6900            LayoutDimension::percent(1.0),
6901            LayoutDimension::Auto,
6902        ))
6903        .gap(LayoutGap::points(8.0, 8.0))
6904        .flex_wrap(LayoutFlexWrap::Wrap)
6905        .to_layout_style();
6906    widgets::form_action_buttons(
6907        ui,
6908        section.root,
6909        "forms.profile.actions",
6910        &state.form,
6911        widgets::FormActionButtonsOptions::default()
6912            .with_layout(action_layout)
6913            .include_reset(true)
6914            .with_action_prefix("forms.profile"),
6915    );
6916    widgets::label(
6917        ui,
6918        section.root,
6919        "forms.profile.status",
6920        format!("Status: {}", state.form_status),
6921        text(11.0, color(154, 166, 184)),
6922        LayoutStyle::new().with_width_percent(1.0),
6923    );
6924}
6925
6926fn overlay_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6927    let body =
6928        section_with_min_viewport(ui, parent, "overlays", "Overlays", UiSize::new(420.0, 0.0));
6929    let header = widgets::collapsing_header(
6930        ui,
6931        body,
6932        "overlays.collapsing",
6933        "Collapsing header",
6934        widgets::CollapsingHeaderOptions::default()
6935            .expanded(state.overlay_expanded)
6936            .with_toggle_action("overlays.collapsing.toggle"),
6937    );
6938    if let Some(panel) = header.body {
6939        widgets::label(
6940            ui,
6941            panel,
6942            "overlays.collapsing.body",
6943            "Expanded content lives under the header and remains part of normal layout.",
6944            text(12.0, color(196, 210, 230)),
6945            LayoutStyle::new().with_width_percent(1.0),
6946        );
6947    }
6948
6949    let controls = wrapping_row(ui, body, "overlays.controls", 8.0);
6950    button(
6951        ui,
6952        controls,
6953        "overlays.popup.toggle",
6954        if state.overlay_popup_open {
6955            "Close popup"
6956        } else {
6957            "Open popup"
6958        },
6959        "overlays.popup.toggle",
6960        button_visual(48, 112, 184),
6961    );
6962    button(
6963        ui,
6964        controls,
6965        "overlays.modal.open",
6966        "Open modal",
6967        "overlays.modal.open",
6968        button_visual(58, 78, 96),
6969    );
6970
6971    let tooltip = TooltipContent::new("Tooltip")
6972        .body("Tooltip boxes are overlay surfaces with title, body, and shortcut text.")
6973        .shortcut_label("Ctrl+K")
6974        .disabled_reason("Disabled reasons can be announced without changing the trigger.");
6975    let mut tooltip_options = widgets::TooltipBoxOptions::default()
6976        .with_layout(
6977            LayoutStyle::column()
6978                .with_width(280.0)
6979                .with_padding(8.0)
6980                .with_gap(4.0),
6981        )
6982        .with_animation(None);
6983    tooltip_options.layer = UiLayer::AppContent;
6984    tooltip_options.z_index = 0;
6985    widgets::tooltip_box(ui, body, "overlays.tooltip", tooltip, tooltip_options);
6986
6987    let tooltip_anchor = row(ui, body, "overlays.tooltip_anchor", 8.0);
6988    widgets::label(
6989        ui,
6990        tooltip_anchor,
6991        "overlays.tooltip_anchor.label",
6992        "Tooltip placement clamps to its viewport.",
6993        text(12.0, color(166, 176, 190)),
6994        LayoutStyle::new().with_width_percent(1.0),
6995    );
6996    let clamped_rect = widgets::tooltip::tooltip_rect(
6997        UiRect::new(328.0, 12.0, 54.0, 24.0),
6998        UiSize::new(176.0, 58.0),
6999        UiRect::new(0.0, 0.0, 420.0, 190.0),
7000        TooltipPlacement::Right,
7001        8.0,
7002        None,
7003    );
7004    let clamped_preview = ui.add_child(
7005        body,
7006        UiNode::container(
7007            "overlays.tooltip_rect.preview",
7008            LayoutStyle::new()
7009                .with_width_percent(1.0)
7010                .with_height(78.0)
7011                .with_flex_shrink(0.0),
7012        )
7013        .with_visual(UiVisual::panel(
7014            color(12, 16, 22),
7015            Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
7016            4.0,
7017        )),
7018    );
7019    ui.add_child(
7020        clamped_preview,
7021        UiNode::scene(
7022            "overlays.tooltip_rect.scene",
7023            vec![
7024                ScenePrimitive::Rect(
7025                    PaintRect::solid(UiRect::new(328.0, 12.0, 54.0, 24.0), color(48, 112, 184))
7026                        .corner_radii(CornerRadii::uniform(3.0)),
7027                ),
7028                ScenePrimitive::Rect(
7029                    PaintRect::solid(clamped_rect, color(24, 29, 38))
7030                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7031                            color(92, 106, 128),
7032                            1.0,
7033                        )))
7034                        .corner_radii(CornerRadii::uniform(4.0)),
7035                ),
7036            ],
7037            LayoutStyle::new()
7038                .with_width_percent(1.0)
7039                .with_height_percent(1.0),
7040        ),
7041    );
7042
7043    if state.overlay_popup_open {
7044        let popup = ext_widgets::popup_panel(
7045            ui,
7046            parent,
7047            "overlays.popup_panel",
7048            UiRect::new(18.0, 150.0, 220.0, 112.0),
7049            ext_widgets::PopupOptions {
7050                z_index: 20,
7051                portal: UiPortalTarget::Parent,
7052                accessibility: Some(
7053                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup"),
7054                ),
7055                ..Default::default()
7056            },
7057        );
7058        let popup_body = ui.add_child(
7059            popup,
7060            UiNode::container(
7061                "overlays.popup_panel.body",
7062                LayoutStyle::column()
7063                    .with_width_percent(1.0)
7064                    .with_height_percent(1.0)
7065                    .with_padding(10.0)
7066                    .with_gap(6.0),
7067            ),
7068        );
7069        let popup_header = row(ui, popup_body, "overlays.popup_panel.header", 8.0);
7070        widgets::label(
7071            ui,
7072            popup_header,
7073            "overlays.popup_panel.label",
7074            "Popup panel",
7075            text(12.0, color(220, 228, 238)),
7076            LayoutStyle::new().with_width_percent(1.0),
7077        );
7078        let mut close = widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0))
7079            .with_action("overlays.popup.close");
7080        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7081        close.hovered_visual = Some(button_visual(54, 70, 92));
7082        close.text_style = text(12.0, color(220, 228, 238));
7083        widgets::button(ui, popup_header, "overlays.popup_panel.close", "x", close);
7084        widgets::label(
7085            ui,
7086            popup_body,
7087            "overlays.popup_panel.body_text",
7088            "Popup content is conditionally rendered.",
7089            text(11.0, color(196, 210, 230)),
7090            LayoutStyle::new().with_width_percent(1.0),
7091        );
7092    }
7093
7094    if state.overlay_modal_open {
7095        let modal = widgets::modal_dialog(
7096            ui,
7097            parent,
7098            "overlays.modal",
7099            "Modal dialog",
7100            widgets::ModalDialogOptions::default()
7101                .with_size(320.0, 180.0)
7102                .with_close_action("overlays.modal.close")
7103                .with_dismissal(ext_widgets::DialogDismissal::MODAL)
7104                .with_focus_restore(FocusRestoreTarget::Previous),
7105        );
7106        widgets::label(
7107            ui,
7108            modal.body,
7109            "overlays.modal.body.text",
7110            "Modal dialogs are portaled to the application overlay, include a scrim, and trap focus.",
7111            text(12.0, color(220, 228, 238)),
7112            LayoutStyle::new().with_width_percent(1.0),
7113        );
7114        button(
7115            ui,
7116            modal.body,
7117            "overlays.modal.body.close",
7118            "Close modal",
7119            "overlays.modal.close",
7120            button_visual(48, 112, 184),
7121        );
7122    }
7123}
7124
7125fn drag_drop_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7126    let body = section_with_min_viewport(
7127        ui,
7128        parent,
7129        "drag_drop",
7130        "Drag and drop",
7131        UiSize::new(420.0, 0.0),
7132    );
7133    widgets::label(
7134        ui,
7135        body,
7136        "drag_drop.sources.label",
7137        "Drag sources",
7138        text(12.0, color(166, 176, 190)),
7139        LayoutStyle::new().with_width_percent(1.0),
7140    );
7141    let sources = wrapping_row(ui, body, "drag_drop.sources", 8.0);
7142    widgets::dnd_drag_source(
7143        ui,
7144        sources,
7145        "drag_drop.text_source",
7146        "Text payload",
7147        DragPayload::text("Operad payload"),
7148        widgets::DragSourceOptions::default()
7149            .with_layout(drag_source_layout())
7150            .with_kind(DragDropSurfaceKind::ListRow)
7151            .with_allowed_operations([DragOperation::Copy, DragOperation::Move])
7152            .with_action("drag_drop.text_source")
7153            .with_accessibility_hint("Start a text drag operation"),
7154    );
7155    widgets::dnd_drag_source(
7156        ui,
7157        sources,
7158        "drag_drop.file_source",
7159        "File payload",
7160        DragPayload::files(["/tmp/showcase.scene"]),
7161        widgets::DragSourceOptions::default()
7162            .with_layout(drag_source_layout())
7163            .with_kind(DragDropSurfaceKind::Asset)
7164            .with_drag_image_policy(widgets::DragImagePolicy::image_key(
7165                BuiltInIcon::Folder.key(),
7166                UiSize::new(120.0, 36.0),
7167                UiPoint::new(10.0, 10.0),
7168            ))
7169            .with_allowed_operations([DragOperation::Copy])
7170            .with_action("drag_drop.file_source"),
7171    );
7172    widgets::dnd_drag_source(
7173        ui,
7174        sources,
7175        "drag_drop.bytes_source",
7176        "Image bytes",
7177        DragPayload::bytes(DragBytes::new("image/png", vec![137, 80, 78, 71]).name("sprite.png")),
7178        widgets::DragSourceOptions::default()
7179            .with_layout(drag_source_layout())
7180            .with_kind(DragDropSurfaceKind::Asset)
7181            .with_action("drag_drop.bytes_source")
7182            .without_drag_image(),
7183    );
7184
7185    widgets::label(
7186        ui,
7187        body,
7188        "drag_drop.zones.label",
7189        "Drop zones",
7190        text(12.0, color(166, 176, 190)),
7191        LayoutStyle::new().with_width_percent(1.0),
7192    );
7193    let zones = wrapping_row(ui, body, "drag_drop.zones", 8.0);
7194    let accepted_options = widgets::DropZoneOptions::default()
7195        .with_layout(drop_zone_layout())
7196        .with_kind(DragDropSurfaceKind::EditorSurface)
7197        .with_accepted_payload(DropPayloadFilter::empty().text())
7198        .with_accepted_operations([DragOperation::Copy, DragOperation::Move])
7199        .with_action("drag_drop.accept_text")
7200        .with_accessibility_hint("Accepts text payloads");
7201    let accepted = widgets::dnd_drop_zone(
7202        ui,
7203        zones,
7204        "drag_drop.accept_text",
7205        "Text accepted",
7206        accepted_options.clone(),
7207    );
7208    widgets::drag_drop::dnd_apply_drop_zone_preview(
7209        ui,
7210        accepted.root,
7211        &accepted_options,
7212        widgets::drag_drop::DropZonePreviewState::Accepted,
7213    );
7214
7215    let rejected_options = widgets::DropZoneOptions::default()
7216        .with_layout(drop_zone_layout())
7217        .with_kind(DragDropSurfaceKind::Asset)
7218        .with_accepted_payload(DropPayloadFilter::empty().files())
7219        .with_action("drag_drop.files_only");
7220    let rejected = widgets::dnd_drop_zone(
7221        ui,
7222        zones,
7223        "drag_drop.files_only",
7224        "Files only",
7225        rejected_options.clone(),
7226    );
7227    widgets::drag_drop::dnd_apply_drop_zone_preview(
7228        ui,
7229        rejected.root,
7230        &rejected_options,
7231        widgets::drag_drop::DropZonePreviewState::Rejected,
7232    );
7233    let image_options = widgets::DropZoneOptions::default()
7234        .with_layout(drop_zone_layout())
7235        .with_kind(DragDropSurfaceKind::Asset)
7236        .with_accepted_payload(DropPayloadFilter::empty().mime_type("image/*"))
7237        .with_accepted_operations([DragOperation::Copy])
7238        .with_action("drag_drop.image_bytes");
7239    let image_zone = widgets::dnd_drop_zone(
7240        ui,
7241        zones,
7242        "drag_drop.image_bytes",
7243        "Image bytes",
7244        image_options.clone(),
7245    );
7246    widgets::drag_drop::dnd_apply_drop_zone_preview(
7247        ui,
7248        image_zone.root,
7249        &image_options,
7250        widgets::drag_drop::DropZonePreviewState::Hovered,
7251    );
7252
7253    let disabled_options = widgets::DropZoneOptions::default()
7254        .with_layout(drop_zone_layout())
7255        .with_kind(DragDropSurfaceKind::EditorSurface)
7256        .with_accepted_payload(DropPayloadFilter::any())
7257        .with_action("drag_drop.disabled")
7258        .disabled();
7259    let disabled_zone = widgets::dnd_drop_zone(
7260        ui,
7261        zones,
7262        "drag_drop.disabled",
7263        "Disabled",
7264        disabled_options.clone(),
7265    );
7266    widgets::drag_drop::dnd_apply_drop_zone_preview(
7267        ui,
7268        disabled_zone.root,
7269        &disabled_options,
7270        widgets::drag_drop::DropZonePreviewState::Disabled,
7271    );
7272
7273    let operation_row = wrapping_row(ui, body, "drag_drop.operations", 6.0);
7274    dnd_operation_chip(ui, operation_row, "drag_drop.operation.copy", "copy");
7275    dnd_operation_chip(ui, operation_row, "drag_drop.operation.move", "move");
7276    dnd_operation_chip(ui, operation_row, "drag_drop.operation.link", "link");
7277    widgets::label(
7278        ui,
7279        body,
7280        "drag_drop.status",
7281        format!("Status: {}", state.drag_drop_status),
7282        text(11.0, color(154, 166, 184)),
7283        LayoutStyle::new().with_width_percent(1.0),
7284    );
7285}
7286
7287fn media_widgets(ui: &mut UiDocument, parent: UiNodeId) {
7288    let body = section_with_min_viewport(ui, parent, "media", "Media", UiSize::new(430.0, 0.0));
7289    widgets::label(
7290        ui,
7291        body,
7292        "media.icons.label",
7293        "Built-in icons",
7294        text(12.0, color(166, 176, 190)),
7295        LayoutStyle::new().with_width_percent(1.0),
7296    );
7297    let icons = wrapping_row(ui, body, "media.icons", 8.0);
7298    for icon in BuiltInIcon::COMMON {
7299        media_icon_tile(ui, icons, icon);
7300    }
7301
7302    widgets::label(
7303        ui,
7304        body,
7305        "media.variants.label",
7306        "Image variants",
7307        text(12.0, color(166, 176, 190)),
7308        LayoutStyle::new().with_width_percent(1.0),
7309    );
7310    let variants = wrapping_row(ui, body, "media.variants", 10.0);
7311    widgets::image(
7312        ui,
7313        variants,
7314        "media.image.untinted",
7315        icon_image(BuiltInIcon::Play),
7316        widgets::ImageOptions::default()
7317            .with_layout(media_preview_image_layout())
7318            .with_accessibility_label("Untinted play icon"),
7319    );
7320    widgets::image(
7321        ui,
7322        variants,
7323        "media.image.warning",
7324        ImageContent::new(BuiltInIcon::Warning.key()).tinted(color(232, 186, 88)),
7325        widgets::ImageOptions::default()
7326            .with_layout(media_preview_image_layout())
7327            .with_accessibility_label("Tinted warning icon"),
7328    );
7329    widgets::image(
7330        ui,
7331        variants,
7332        "media.image.shader",
7333        ImageContent::new(BuiltInIcon::Grid.key()).tinted(color(118, 183, 255)),
7334        widgets::ImageOptions::default()
7335            .with_layout(media_preview_image_layout())
7336            .with_shader(ShaderEffect::new("media.preview.tint").uniform("amount", 0.5))
7337            .with_accessibility_label("Shader-decorated grid icon"),
7338    );
7339    widgets::label(
7340        ui,
7341        body,
7342        "media.image.note",
7343        "Image widgets reference stable resource keys; the host resolves them to textures, vector assets, tinting, or shader-backed resources.",
7344        text(12.0, color(166, 176, 190)),
7345        LayoutStyle::new().with_width_percent(1.0),
7346    );
7347}
7348
7349fn timeline_ruler(ui: &mut UiDocument, parent: UiNodeId) {
7350    let layout = LayoutStyle::column()
7351        .with_width_percent(1.0)
7352        .with_height(40.0)
7353        .with_flex_shrink(0.0);
7354    let layout = operad::layout::with_min_size(layout, operad::length(0.0), operad::length(0.0));
7355    let body = widgets::scroll_area(ui, parent, "timeline", ScrollAxes::BOTH, layout);
7356    ext_widgets::timeline_ruler(
7357        ui,
7358        body,
7359        "timeline.ruler",
7360        ext_widgets::RulerSpec {
7361            range: ext_widgets::TimelineRange::new(0.0, 12.0),
7362            width: 600.0,
7363            major_step: 2.0,
7364            minor_step: 0.5,
7365            label_every: 1,
7366        },
7367        ext_widgets::TimelineRulerOptions::default(),
7368    );
7369}
7370
7371fn toast_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7372    let body = section(ui, parent, "toasts", "Toasts");
7373    let controls = row(ui, body, "toasts.controls", 10.0);
7374    button(
7375        ui,
7376        controls,
7377        "toasts.show",
7378        "Show toast",
7379        "toast.show",
7380        button_visual(48, 112, 184),
7381    );
7382    button(
7383        ui,
7384        controls,
7385        "toasts.hide",
7386        "Hide",
7387        "toast.hide",
7388        button_visual(58, 78, 96),
7389    );
7390    widgets::label(
7391        ui,
7392        body,
7393        "toasts.status",
7394        if state.toast_visible {
7395            "Toast overlay is visible."
7396        } else {
7397            "Toast overlay is hidden."
7398        },
7399        text(12.0, color(196, 210, 230)),
7400        LayoutStyle::new().with_width_percent(1.0),
7401    );
7402    widgets::label(
7403        ui,
7404        body,
7405        "toasts.action_status",
7406        format!("Action: {}", state.toast_action_status),
7407        text(12.0, color(154, 166, 184)),
7408        LayoutStyle::new().with_width_percent(1.0),
7409    );
7410}
7411
7412fn popup_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7413    let body = section(ui, parent, "popup_panel", "Popup panel");
7414    let controls = row(ui, body, "popup_panel.controls", 8.0);
7415    button(
7416        ui,
7417        controls,
7418        "popup_panel.toggle",
7419        if state.popup_open {
7420            "Close popup"
7421        } else {
7422            "Open popup"
7423        },
7424        "popup.toggle",
7425        button_visual(48, 112, 184),
7426    );
7427    if state.popup_open {
7428        let mut close =
7429            widgets::ButtonOptions::new(LayoutStyle::size(30.0, 30.0)).with_action("popup.close");
7430        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7431        close.hovered_visual = Some(button_visual(54, 70, 92));
7432        close.text_style = text(13.0, color(220, 228, 238));
7433        widgets::button(ui, controls, "popup_panel.inline_close", "x", close);
7434    }
7435    widgets::label(
7436        ui,
7437        body,
7438        "popup_panel.status",
7439        if state.popup_open {
7440            "Popup overlay is open."
7441        } else {
7442            "Popup overlay is closed."
7443        },
7444        text(12.0, color(196, 210, 230)),
7445        LayoutStyle::new().with_width_percent(1.0),
7446    );
7447    if state.popup_open {
7448        let panel = ext_widgets::popup_panel(
7449            ui,
7450            parent,
7451            "popup_panel.inline_preview",
7452            UiRect::new(0.0, 20.0, 160.0, 104.0),
7453            ext_widgets::PopupOptions {
7454                z_index: 4,
7455                portal: UiPortalTarget::Parent,
7456                accessibility: Some(
7457                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup preview"),
7458                ),
7459                ..Default::default()
7460            },
7461        );
7462        let content = ui.add_child(
7463            panel,
7464            UiNode::container(
7465                "popup_panel.inline_preview.body",
7466                LayoutStyle::column()
7467                    .with_width_percent(1.0)
7468                    .with_height_percent(1.0)
7469                    .with_padding(10.0)
7470                    .with_gap(8.0),
7471            ),
7472        );
7473        let header = row(ui, content, "popup_panel.inline_preview.header", 8.0);
7474        widgets::label(
7475            ui,
7476            header,
7477            "popup_panel.inline_preview.title",
7478            "Popup panel",
7479            text(12.0, color(226, 234, 246)),
7480            LayoutStyle::new().with_width_percent(1.0),
7481        );
7482        let mut close =
7483            widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0)).with_action("popup.close");
7484        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7485        close.hovered_visual = Some(button_visual(54, 70, 92));
7486        close.text_style = text(12.0, color(220, 228, 238));
7487        widgets::button(ui, header, "popup_panel.inline_preview.close", "x", close);
7488        widgets::label(
7489            ui,
7490            content,
7491            "popup_panel.inline_preview.text",
7492            "Overlay content",
7493            text(11.0, color(196, 210, 230)),
7494            LayoutStyle::new().with_width_percent(1.0),
7495        );
7496        widgets::spacer(
7497            ui,
7498            body,
7499            "popup_panel.inline_preview.space",
7500            LayoutStyle::new()
7501                .with_width_percent(1.0)
7502                .with_height(112.0)
7503                .with_flex_shrink(0.0),
7504        );
7505    }
7506}
7507
7508fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7509    let body = section(ui, parent, "styling", "Styling");
7510    let grid_layout = operad::layout::with_grid_template_columns(
7511        Layout::grid()
7512            .size(LayoutSize::percent(1.0, 1.0))
7513            .gap(LayoutGap::points(10.0, 10.0))
7514            .to_layout_style(),
7515        [
7516            LayoutGridTrack::points(300.0),
7517            LayoutGridTrack::points(1.0),
7518            LayoutGridTrack::points(210.0),
7519        ],
7520    );
7521    let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
7522    let controls = ui.add_child(
7523        grid,
7524        UiNode::container(
7525            "styling.controls",
7526            LayoutStyle::column()
7527                .with_width(300.0)
7528                .with_height_percent(1.0)
7529                .with_flex_shrink(0.0)
7530                .gap(6.0),
7531        ),
7532    );
7533    style_edge_group(
7534        ui,
7535        controls,
7536        "styling.inner",
7537        "Inner margin",
7538        "styling.inner_same",
7539        state.styling.inner_same,
7540        [
7541            ("Left", "styling.inner", state.styling.inner_margin),
7542            ("Right", "styling.inner_right", state.styling.inner_right),
7543            ("Top", "styling.inner_top", state.styling.inner_top),
7544            ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
7545        ],
7546        0.0..32.0,
7547    );
7548    style_edge_group(
7549        ui,
7550        controls,
7551        "styling.outer",
7552        "Outer margin",
7553        "styling.outer_same",
7554        state.styling.outer_same,
7555        [
7556            ("Left", "styling.outer", state.styling.outer_margin),
7557            ("Right", "styling.outer_right", state.styling.outer_right),
7558            ("Top", "styling.outer_top", state.styling.outer_top),
7559            ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
7560        ],
7561        0.0..40.0,
7562    );
7563    style_edge_group(
7564        ui,
7565        controls,
7566        "styling.radius",
7567        "Corner radius",
7568        "styling.radius_same",
7569        state.styling.radius_same,
7570        [
7571            ("NW", "styling.radius", state.styling.corner_radius),
7572            ("NE", "styling.radius_ne", state.styling.corner_ne),
7573            ("SW", "styling.radius_sw", state.styling.corner_sw),
7574            ("SE", "styling.radius_se", state.styling.corner_se),
7575        ],
7576        0.0..28.0,
7577    );
7578    style_shadow_group(ui, controls, state);
7579    style_color_button_row(
7580        ui,
7581        controls,
7582        "styling.fill_color_button",
7583        "Fill",
7584        state.styling.fill_color(),
7585        "Pick fill color",
7586    );
7587    if state.styling_fill_picker_open {
7588        ext_widgets::color_picker(
7589            ui,
7590            controls,
7591            "styling.fill_picker",
7592            &state.styling_fill_picker,
7593            ext_widgets::ColorPickerOptions::default()
7594                .with_label("Fill")
7595                .with_action_prefix("styling.fill_picker"),
7596        );
7597    }
7598    style_stroke_row(ui, controls, state);
7599    if state.styling_stroke_picker_open {
7600        ext_widgets::color_picker(
7601            ui,
7602            controls,
7603            "styling.stroke_picker",
7604            &state.styling_stroke_picker,
7605            ext_widgets::ColorPickerOptions::default()
7606                .with_label("Stroke color")
7607                .with_action_prefix("styling.stroke_picker"),
7608        );
7609    }
7610    widgets::separator(
7611        ui,
7612        grid,
7613        "styling.preview.separator",
7614        widgets::SeparatorOptions::vertical().with_layout(
7615            LayoutStyle::new()
7616                .with_width(1.0)
7617                .with_height_percent(1.0)
7618                .with_flex_shrink(0.0),
7619        ),
7620    );
7621
7622    let preview = ui.add_child(
7623        grid,
7624        UiNode::container(
7625            "styling.preview",
7626            LayoutStyle::column()
7627                .with_width(210.0)
7628                .with_height_percent(1.0)
7629                .with_flex_shrink(0.0)
7630                .padding(8.0),
7631        )
7632        .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
7633    );
7634    style_preview(ui, preview, state.styling);
7635}
7636
7637#[allow(clippy::too_many_arguments)]
7638fn style_edge_group(
7639    ui: &mut UiDocument,
7640    parent: UiNodeId,
7641    name: &'static str,
7642    title: &'static str,
7643    same_action: &'static str,
7644    same: bool,
7645    values: [(&'static str, &'static str, f32); 4],
7646    range: std::ops::Range<f32>,
7647) {
7648    let group = style_control_group(ui, parent, format!("{name}.group"));
7649    style_group_title(ui, group, format!("{name}.title"), title);
7650    let fields = ui.add_child(
7651        group,
7652        UiNode::container(
7653            format!("{name}.fields"),
7654            LayoutStyle::column()
7655                .with_width(138.0)
7656                .with_flex_shrink(0.0)
7657                .gap(3.0),
7658        ),
7659    );
7660    style_compact_checkbox(ui, fields, same_action, "same", same);
7661    if same {
7662        style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
7663    } else {
7664        for (label, action, value) in values {
7665            style_number_row(ui, fields, action, label, value, range.clone(), 0);
7666        }
7667    }
7668}
7669
7670fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7671    let group = style_control_group(ui, parent, "styling.shadow.group");
7672    style_group_title(ui, group, "styling.shadow.title", "Shadow");
7673    let fields = ui.add_child(
7674        group,
7675        UiNode::container(
7676            "styling.shadow.fields",
7677            LayoutStyle::column()
7678                .with_width(174.0)
7679                .with_flex_shrink(0.0)
7680                .gap(4.0),
7681        ),
7682    );
7683    let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
7684    style_inline_number(
7685        ui,
7686        offsets,
7687        "styling.shadow_x",
7688        "x",
7689        state.styling.shadow_x,
7690        -24.0..24.0,
7691        0,
7692    );
7693    style_inline_number(
7694        ui,
7695        offsets,
7696        "styling.shadow_y",
7697        "y",
7698        state.styling.shadow_y,
7699        -24.0..24.0,
7700        0,
7701    );
7702    let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
7703    style_inline_number(
7704        ui,
7705        spread,
7706        "styling.shadow",
7707        "blur",
7708        state.styling.shadow_blur,
7709        0.0..32.0,
7710        0,
7711    );
7712    style_inline_number(
7713        ui,
7714        spread,
7715        "styling.shadow_spread",
7716        "spread",
7717        state.styling.shadow_spread,
7718        0.0..16.0,
7719        0,
7720    );
7721    style_color_button_row(
7722        ui,
7723        fields,
7724        "styling.shadow_color_button",
7725        "",
7726        state.styling.shadow_color(),
7727        "Pick shadow color",
7728    );
7729    if state.styling_shadow_picker_open {
7730        ext_widgets::color_picker(
7731            ui,
7732            fields,
7733            "styling.shadow_picker",
7734            &state.styling_shadow_picker,
7735            ext_widgets::ColorPickerOptions::default()
7736                .with_label("Shadow color")
7737                .with_action_prefix("styling.shadow_picker"),
7738        );
7739    }
7740}
7741
7742fn style_stroke_row(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7743    let row = row(ui, parent, "styling.stroke.row", 8.0);
7744    widgets::label(
7745        ui,
7746        row,
7747        "styling.stroke.label",
7748        "Stroke",
7749        text(12.0, color(166, 176, 190)),
7750        LayoutStyle::new().with_width(86.0).with_flex_shrink(0.0),
7751    );
7752    style_value_input(
7753        ui,
7754        row,
7755        "styling.stroke",
7756        state.styling.stroke_width,
7757        0.0..4.0,
7758        1,
7759    );
7760    ext_widgets::color_edit_button(
7761        ui,
7762        row,
7763        "styling.stroke_color_button",
7764        state.styling.stroke_color(),
7765        color_mini_button_options("styling.stroke_color_button")
7766            .with_format(ext_widgets::ColorValueFormat::Rgba)
7767            .accessibility_label("Pick stroke color"),
7768    );
7769    let mut options = widgets::SliderOptions::default()
7770        .with_layout(
7771            LayoutStyle::new()
7772                .with_width(60.0)
7773                .with_height(20.0)
7774                .with_flex_shrink(0.0),
7775        )
7776        .with_value_edit_action("styling.stroke");
7777    options.fill_color = color(120, 170, 230);
7778    widgets::slider(
7779        ui,
7780        row,
7781        "styling.stroke.slider",
7782        (state.styling.stroke_width / 4.0).clamp(0.0, 1.0),
7783        0.0..1.0,
7784        options,
7785    );
7786}
7787
7788fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7789    ui.add_child(
7790        parent,
7791        UiNode::container(
7792            name,
7793            LayoutStyle::row()
7794                .with_width_percent(1.0)
7795                .with_flex_shrink(0.0)
7796                .padding(4.0)
7797                .gap(8.0),
7798        )
7799        .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
7800    )
7801}
7802
7803fn style_group_title(
7804    ui: &mut UiDocument,
7805    parent: UiNodeId,
7806    name: impl Into<String>,
7807    label: &'static str,
7808) {
7809    widgets::label(
7810        ui,
7811        parent,
7812        name,
7813        label,
7814        text(12.0, color(166, 176, 190)),
7815        LayoutStyle::new()
7816            .with_width(88.0)
7817            .with_flex_shrink(0.0)
7818            .with_height(22.0),
7819    );
7820}
7821
7822fn style_color_button_row(
7823    ui: &mut UiDocument,
7824    parent: UiNodeId,
7825    action: &'static str,
7826    label: &'static str,
7827    value: ColorRgba,
7828    accessibility_label: &'static str,
7829) {
7830    let row = row(ui, parent, format!("{action}.row"), 8.0);
7831    if !label.is_empty() {
7832        widgets::label(
7833            ui,
7834            row,
7835            format!("{action}.label"),
7836            label,
7837            text(12.0, color(166, 176, 190)),
7838            LayoutStyle::new()
7839                .with_width(86.0)
7840                .with_flex_shrink(0.0)
7841                .with_height(24.0),
7842        );
7843    }
7844    ext_widgets::color_edit_button(
7845        ui,
7846        row,
7847        action,
7848        value,
7849        color_mini_button_options(action)
7850            .with_format(ext_widgets::ColorValueFormat::Rgba)
7851            .accessibility_label(accessibility_label),
7852    );
7853    widgets::label(
7854        ui,
7855        row,
7856        format!("{action}.value"),
7857        ext_widgets::color_picker::format_hex_color(value, value.a < 255),
7858        text(12.0, color(226, 232, 242)),
7859        LayoutStyle::new().with_width(96.0).with_height(24.0),
7860    );
7861}
7862
7863fn style_number_row(
7864    ui: &mut UiDocument,
7865    parent: UiNodeId,
7866    name: &'static str,
7867    label: &'static str,
7868    value: f32,
7869    range: std::ops::Range<f32>,
7870    decimals: u8,
7871) {
7872    let row = row(ui, parent, format!("{name}.row"), 6.0);
7873    widgets::label(
7874        ui,
7875        row,
7876        format!("{name}.label"),
7877        label,
7878        text(12.0, color(166, 176, 190)),
7879        LayoutStyle::new().with_width(48.0).with_height(22.0),
7880    );
7881    style_value_input(ui, row, name, value, range, decimals);
7882}
7883
7884fn style_inline_number(
7885    ui: &mut UiDocument,
7886    parent: UiNodeId,
7887    name: &'static str,
7888    label: &'static str,
7889    value: f32,
7890    range: std::ops::Range<f32>,
7891    decimals: u8,
7892) {
7893    let row = row(ui, parent, format!("{name}.inline"), 3.0);
7894    widgets::label(
7895        ui,
7896        row,
7897        format!("{name}.inline_label"),
7898        format!("{label}:"),
7899        text(12.0, color(166, 176, 190)),
7900        LayoutStyle::new()
7901            .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
7902            .with_height(22.0),
7903    );
7904    style_value_input(ui, row, name, value, range, decimals);
7905}
7906
7907fn style_value_input(
7908    ui: &mut UiDocument,
7909    parent: UiNodeId,
7910    name: &'static str,
7911    value: f32,
7912    range: std::ops::Range<f32>,
7913    decimals: u8,
7914) {
7915    let mut options = widgets::DragValueOptions::default()
7916        .with_layout(
7917            LayoutStyle::new()
7918                .with_width(42.0)
7919                .with_height(22.0)
7920                .with_flex_shrink(0.0),
7921        )
7922        .with_range(ext_widgets::NumericRange::new(
7923            f64::from(range.start),
7924            f64::from(range.end),
7925        ))
7926        .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
7927        .with_action(name);
7928    options.text_style = text(12.0, color(226, 232, 242));
7929    widgets::drag_value_input(ui, parent, name, f64::from(value), options);
7930}
7931
7932fn style_compact_checkbox(
7933    ui: &mut UiDocument,
7934    parent: UiNodeId,
7935    name: &'static str,
7936    label: &'static str,
7937    checked: bool,
7938) {
7939    let mut options = widgets::CheckboxOptions::default().with_action(name);
7940    options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
7941    options.text_style = text(12.0, color(220, 228, 238));
7942    widgets::checkbox(ui, parent, name, label, checked, options);
7943}
7944
7945fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
7946    ext_widgets::ColorButtonOptions::default()
7947        .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
7948        .with_swatch_size(UiSize::new(22.0, 18.0))
7949        .with_action(action)
7950        .show_label(false)
7951}
7952
7953fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
7954    let outer = styling.outer_edges();
7955    let inner = styling.inner_edges();
7956    let frame = UiRect::new(
7957        22.0 + outer[0],
7958        28.0 + outer[2],
7959        108.0 + inner[0] + inner[1],
7960        40.0 + inner[2] + inner[3],
7961    );
7962    let text_rect = UiRect::new(
7963        frame.x + inner[0],
7964        frame.y + inner[2],
7965        (frame.width - inner[0] - inner[1]).max(1.0),
7966        (frame.height - inner[2] - inner[3]).max(1.0),
7967    );
7968    ui.add_child(
7969        parent,
7970        UiNode::scene(
7971            "styling.preview.scene",
7972            vec![
7973                ScenePrimitive::Rect(
7974                    PaintRect::solid(frame, styling.fill_color())
7975                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7976                            styling.stroke_color(),
7977                            styling.stroke_width,
7978                        )))
7979                        .corner_radii(styling.radii())
7980                        .effect(PaintEffect::shadow(
7981                            styling.shadow_color(),
7982                            UiPoint::new(styling.shadow_x, styling.shadow_y),
7983                            styling.shadow_blur,
7984                            styling.shadow_spread,
7985                        )),
7986                ),
7987                ScenePrimitive::Text(
7988                    PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
7989                        .horizontal_align(TextHorizontalAlign::Center)
7990                        .vertical_align(TextVerticalAlign::Center)
7991                        .multiline(false),
7992                ),
7993            ],
7994            LayoutStyle::new()
7995                .with_width_percent(1.0)
7996                .with_height(180.0)
7997                .with_flex_shrink(0.0),
7998        ),
7999    );
8000}
Source

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

Examples found in repository?
examples/showcase.rs (lines 7980-7985)
7953fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
7954    let outer = styling.outer_edges();
7955    let inner = styling.inner_edges();
7956    let frame = UiRect::new(
7957        22.0 + outer[0],
7958        28.0 + outer[2],
7959        108.0 + inner[0] + inner[1],
7960        40.0 + inner[2] + inner[3],
7961    );
7962    let text_rect = UiRect::new(
7963        frame.x + inner[0],
7964        frame.y + inner[2],
7965        (frame.width - inner[0] - inner[1]).max(1.0),
7966        (frame.height - inner[2] - inner[3]).max(1.0),
7967    );
7968    ui.add_child(
7969        parent,
7970        UiNode::scene(
7971            "styling.preview.scene",
7972            vec![
7973                ScenePrimitive::Rect(
7974                    PaintRect::solid(frame, styling.fill_color())
7975                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7976                            styling.stroke_color(),
7977                            styling.stroke_width,
7978                        )))
7979                        .corner_radii(styling.radii())
7980                        .effect(PaintEffect::shadow(
7981                            styling.shadow_color(),
7982                            UiPoint::new(styling.shadow_x, styling.shadow_y),
7983                            styling.shadow_blur,
7984                            styling.shadow_spread,
7985                        )),
7986                ),
7987                ScenePrimitive::Text(
7988                    PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
7989                        .horizontal_align(TextHorizontalAlign::Center)
7990                        .vertical_align(TextVerticalAlign::Center)
7991                        .multiline(false),
7992                ),
7993            ],
7994            LayoutStyle::new()
7995                .with_width_percent(1.0)
7996                .with_height(180.0)
7997                .with_flex_shrink(0.0),
7998        ),
7999    );
8000}
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 + Send + Sync>

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,