Skip to main content

LayoutSize

Struct LayoutSize 

Source
pub struct LayoutSize {
    pub width: LayoutDimension,
    pub height: LayoutDimension,
}

Fields§

§width: LayoutDimension§height: LayoutDimension

Implementations§

Source§

impl LayoutSize

Source

pub const AUTO: Self

Source

pub const ZERO: Self

Source

pub const FILL: Self

Source

pub const fn new(width: LayoutDimension, height: LayoutDimension) -> Self

Examples found in repository?
examples/showcase.rs (lines 6822-6825)
6783fn menu_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6784    let body = section_with_min_viewport(
6785        ui,
6786        parent,
6787        "menus",
6788        "Menu controls",
6789        UiSize::new(320.0, 0.0),
6790    );
6791    let menus = menu_bar_menus(state.menu_autosave, state.menu_grid);
6792    let active_items = state
6793        .menu_bar
6794        .open_menu
6795        .and_then(|index| menus.get(index))
6796        .map(|menu| menu.items.clone())
6797        .unwrap_or_default();
6798    widgets::label(
6799        ui,
6800        body,
6801        "menus.menu_bar.title",
6802        "Menu bar",
6803        text(12.0, color(166, 176, 190)),
6804        LayoutStyle::new().with_width_percent(1.0),
6805    );
6806    ext_widgets::menu_bar(
6807        ui,
6808        body,
6809        "menus.menu_bar",
6810        &menus,
6811        &state.menu_bar,
6812        None,
6813        ext_widgets::MenuBarOptions::default().with_action_prefix("menus.bar"),
6814    );
6815
6816    if !active_items.is_empty() {
6817        let menu_columns = ui.add_child(
6818            body,
6819            UiNode::container(
6820                "menus.menu_columns",
6821                Layout::row()
6822                    .size(LayoutSize::new(
6823                        LayoutDimension::Auto,
6824                        LayoutDimension::Auto,
6825                    ))
6826                    .align_items(LayoutAlignment::Start)
6827                    .gap(LayoutGap::points(4.0, 4.0))
6828                    .flex(0.0, 0.0, LayoutDimension::Auto)
6829                    .to_layout_style(),
6830            ),
6831        );
6832        ext_widgets::menu_list(
6833            ui,
6834            menu_columns,
6835            "menus.menu_list",
6836            &active_items,
6837            state.menu_bar.active_item,
6838            ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
6839        );
6840        if let Some(active_item) = state.menu_bar.active_item {
6841            if let Some(children) = active_items
6842                .get(active_item)
6843                .and_then(|item| item.children())
6844            {
6845                let submenu_column = ui.add_child(
6846                    menu_columns,
6847                    UiNode::container(
6848                        "menus.submenu_column",
6849                        Layout::column()
6850                            .size(LayoutSize::new(
6851                                LayoutDimension::Auto,
6852                                LayoutDimension::Auto,
6853                            ))
6854                            .gap(LayoutGap::points(0.0, 0.0))
6855                            .flex(0.0, 0.0, LayoutDimension::Auto)
6856                            .to_layout_style(),
6857                    ),
6858                );
6859                let offset = menu_item_top_offset(&active_items, active_item);
6860                if offset > 0.0 {
6861                    widgets::spacer(
6862                        ui,
6863                        submenu_column,
6864                        "menus.submenu_spacer",
6865                        LayoutStyle::new().with_width(1.0).with_height(offset),
6866                    );
6867                }
6868                ext_widgets::menu_list(
6869                    ui,
6870                    submenu_column,
6871                    "menus.submenu",
6872                    children,
6873                    Some(0),
6874                    ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
6875                );
6876            }
6877        }
6878    }
6879    divider(ui, body, "menus.divider.buttons");
6880    widgets::label(
6881        ui,
6882        body,
6883        "menus.buttons.title",
6884        "Menu buttons",
6885        text(12.0, color(166, 176, 190)),
6886        LayoutStyle::new().with_width_percent(1.0),
6887    );
6888    let button_row = row(ui, body, "menus.buttons", 10.0);
6889    let button_items = menu_items(state.menu_autosave);
6890    ext_widgets::menu_button(
6891        ui,
6892        button_row,
6893        "menus.menu_button",
6894        "Menu button",
6895        &button_items,
6896        &state.menu_button,
6897        None,
6898        ext_widgets::MenuButtonOptions::default().with_action("menus.menu_button"),
6899    );
6900    ext_widgets::image_text_menu_button(
6901        ui,
6902        button_row,
6903        "menus.image_text_menu_button",
6904        "Image text",
6905        icon_image(BuiltInIcon::Folder),
6906        &button_items,
6907        &state.image_text_menu_button,
6908        None,
6909        ext_widgets::MenuButtonOptions::default().with_action("menus.image_text_menu_button"),
6910    );
6911    ext_widgets::image_menu_button(
6912        ui,
6913        button_row,
6914        "menus.image_menu_button",
6915        icon_image(BuiltInIcon::Settings),
6916        &button_items,
6917        &state.image_menu_button,
6918        None,
6919        ext_widgets::MenuButtonOptions::default().with_action("menus.image_menu_button"),
6920    );
6921    if state.menu_button.open || state.image_text_menu_button.open || state.image_menu_button.open {
6922        let active = state
6923            .menu_button
6924            .navigation
6925            .active_path
6926            .first()
6927            .copied()
6928            .or_else(|| {
6929                state
6930                    .image_text_menu_button
6931                    .navigation
6932                    .active_path
6933                    .first()
6934                    .copied()
6935            })
6936            .or_else(|| {
6937                state
6938                    .image_menu_button
6939                    .navigation
6940                    .active_path
6941                    .first()
6942                    .copied()
6943            });
6944        ext_widgets::menu_list(
6945            ui,
6946            body,
6947            "menus.button_menu",
6948            &button_items,
6949            active,
6950            ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
6951        );
6952    }
6953
6954    divider(ui, body, "menus.divider.context");
6955    widgets::label(
6956        ui,
6957        body,
6958        "menus.context.title",
6959        "Context menu",
6960        text(12.0, color(166, 176, 190)),
6961        LayoutStyle::new().with_width_percent(1.0),
6962    );
6963    let context_row = row(ui, body, "menus.context.controls", 8.0);
6964    button(
6965        ui,
6966        context_row,
6967        "menus.context.open",
6968        "Open context",
6969        "menus.context.open",
6970        button_visual(48, 112, 184),
6971    );
6972    button(
6973        ui,
6974        context_row,
6975        "menus.context.close",
6976        "Close",
6977        "menus.context.close",
6978        button_visual(58, 78, 96),
6979    );
6980    let mut context_options =
6981        ext_widgets::MenuListOptions::default().with_action_prefix("menus.context");
6982    context_options.width = 240.0;
6983    context_options.max_visible_rows = 6;
6984    let _ = ext_widgets::context_menu(
6985        ui,
6986        parent,
6987        "menus.context_menu",
6988        &button_items,
6989        &state.context_menu,
6990        UiRect::new(0.0, 0.0, 560.0, 460.0),
6991        ext_widgets::PopupPlacement::default(),
6992        context_options,
6993    );
6994}
6995
6996fn menu_demo_context_anchor() -> UiPoint {
6997    UiPoint::new(30.0, 390.0)
6998}
6999
7000fn command_palette(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7001    let body = section_with_min_viewport(
7002        ui,
7003        parent,
7004        "command_palette",
7005        "Command palette",
7006        UiSize::new(240.0, 72.0),
7007    );
7008    let items = command_palette_items_with_history(&state.command_history);
7009    let mut trigger_options =
7010        widgets::ButtonOptions::new(LayoutStyle::new().with_width(150.0).with_height(32.0))
7011            .with_action(if state.command_palette_open {
7012                "command_palette.close"
7013            } else {
7014                "command_palette.open"
7015            })
7016            .with_accessibility_label(if state.command_palette_open {
7017                "Close command palette"
7018            } else {
7019                "Open command palette"
7020            });
7021    trigger_options.visual = button_visual(48, 112, 184);
7022    trigger_options.hovered_visual = Some(button_visual(62, 126, 196));
7023    trigger_options.pressed_visual = Some(button_visual(38, 82, 136));
7024    trigger_options.text_style = text(13.0, color(246, 249, 252));
7025    widgets::button(
7026        ui,
7027        body,
7028        "command_palette.open",
7029        if state.command_palette_open {
7030            "Close palette"
7031        } else {
7032            "Open palette"
7033        },
7034        trigger_options,
7035    );
7036    widgets::label(
7037        ui,
7038        body,
7039        "command_palette.last",
7040        format!("Last command: {}", state.last_command),
7041        text(12.0, color(154, 166, 184)),
7042        LayoutStyle::new().with_width_percent(1.0),
7043    );
7044    if state.command_palette_open {
7045        let palette_width = command_palette_popup_width(state.last_desktop_size);
7046        let mut options =
7047            ext_widgets::CommandPaletteOptions::default().with_action_prefix("command_palette");
7048        options.width = palette_width;
7049        options.row_height = 44.0;
7050        options.max_visible_rows = 5;
7051        options.text_style = text(13.0, color(238, 244, 252));
7052        options.muted_text_style = text(12.0, color(166, 178, 196));
7053        options.z_index = SHOWCASE_WINDOW_Z_MAX.saturating_add(40);
7054        ext_widgets::command_palette(
7055            ui,
7056            body,
7057            "command_palette.panel",
7058            &items,
7059            &state.command_palette,
7060            Some(command_palette_popup(
7061                state.last_desktop_size,
7062                palette_width,
7063            )),
7064            options,
7065        );
7066    }
7067}
7068
7069fn command_palette_popup_width(desktop_size: UiSize) -> f32 {
7070    (desktop_size.width - 48.0).clamp(320.0, 560.0)
7071}
7072
7073fn command_palette_popup(desktop_size: UiSize, width: f32) -> ext_widgets::AnchoredPopup {
7074    let viewport = UiRect::new(0.0, 0.0, desktop_size.width, desktop_size.height);
7075    let x = ((desktop_size.width - width) * 0.5).max(12.0);
7076    let y = (desktop_size.height * 0.12).clamp(48.0, 96.0);
7077    ext_widgets::AnchoredPopup::new(
7078        UiRect::new(x, y, width, 0.0),
7079        viewport,
7080        ext_widgets::PopupPlacement::new(
7081            ext_widgets::PopupSide::Bottom,
7082            ext_widgets::PopupAlign::Center,
7083        )
7084        .with_offset(0.0)
7085        .with_flip(false)
7086        .with_viewport_margin(12.0),
7087    )
7088}
7089
7090#[allow(clippy::field_reassign_with_default)]
7091fn progress_indicator(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7092    let body = section(ui, parent, "progress", "Progress indicator");
7093    let animated = smooth_loop(state.progress_phase * 0.85, 0.0) * 100.0;
7094    let mut progress = ext_widgets::ProgressIndicatorOptions::default();
7095    progress.layout = LayoutStyle::new().with_width_percent(1.0).with_height(10.0);
7096    progress.accessibility_label = Some("Progress".to_string());
7097    ext_widgets::progress_indicator(
7098        ui,
7099        body,
7100        "progress.primary",
7101        ext_widgets::ProgressIndicatorValue::percent(animated),
7102        progress,
7103    );
7104    let compact_value = smooth_loop(state.progress_phase * 1.15, 0.7) * 100.0;
7105    let mut compact = ext_widgets::ProgressIndicatorOptions::default();
7106    compact.layout = LayoutStyle::new().with_width_percent(1.0).with_height(6.0);
7107    compact.fill_visual = UiVisual::panel(color(111, 203, 159), None, 3.0);
7108    ext_widgets::progress_indicator(
7109        ui,
7110        body,
7111        "progress.compact",
7112        ext_widgets::ProgressIndicatorValue::percent(compact_value),
7113        compact,
7114    );
7115    let warning_value = smooth_loop(state.progress_phase * 0.65, 1.4) * 100.0;
7116    let mut warning = ext_widgets::ProgressIndicatorOptions::default();
7117    warning.layout = LayoutStyle::new().with_width_percent(1.0).with_height(14.0);
7118    warning.fill_visual = UiVisual::panel(color(232, 186, 88), None, 4.0);
7119    ext_widgets::progress_indicator(
7120        ui,
7121        body,
7122        "progress.warning",
7123        ext_widgets::ProgressIndicatorValue::percent(warning_value),
7124        warning,
7125    );
7126    let logged_value =
7127        (state.progress_loading_elapsed / PROGRESS_LOGGED_DURATION_SECONDS * 100.0).min(100.0);
7128    let logged_entries = progress_demo_logs(logged_value);
7129    progress_loading_panel(
7130        ui,
7131        body,
7132        "progress.logged",
7133        logged_value,
7134        &logged_entries,
7135        state,
7136    );
7137    let spinner_row = row(ui, body, "progress.spinner.row", 8.0);
7138    widgets::spinner(
7139        ui,
7140        spinner_row,
7141        "progress.spinner",
7142        widgets::SpinnerOptions::default()
7143            .with_phase(state.progress_phase)
7144            .with_accessibility_label("Loading spinner"),
7145    );
7146    widgets::label(
7147        ui,
7148        spinner_row,
7149        "progress.spinner.label",
7150        "Spinner",
7151        text(12.0, color(196, 210, 230)),
7152        LayoutStyle::new().with_width_percent(1.0),
7153    );
7154}
7155
7156fn progress_loading_panel(
7157    ui: &mut UiDocument,
7158    parent: UiNodeId,
7159    name: &'static str,
7160    progress_value: f32,
7161    logs: &[ext_widgets::ProgressLogEntry],
7162    state: &ShowcaseState,
7163) {
7164    let panel = ui.add_child(
7165        parent,
7166        UiNode::container(
7167            name,
7168            LayoutStyle::column()
7169                .with_width_percent(1.0)
7170                .with_padding(10.0)
7171                .with_gap(8.0)
7172                .with_flex_shrink(0.0),
7173        )
7174        .with_visual(UiVisual::panel(
7175            color(17, 21, 27),
7176            Some(StrokeStyle::new(color(70, 82, 101), 1.0)),
7177            4.0,
7178        ))
7179        .with_accessibility(
7180            AccessibilityMeta::new(AccessibilityRole::Group).label("Loading progress with logs"),
7181        ),
7182    );
7183
7184    let progress_row = row(ui, panel, "progress.logged.progress_row", 8.0);
7185    let progress_slot = ui.add_child(
7186        progress_row,
7187        UiNode::container(
7188            "progress.logged.progress_slot",
7189            LayoutStyle::new()
7190                .with_width(0.0)
7191                .with_height(30.0)
7192                .with_flex_grow(1.0)
7193                .with_flex_shrink(1.0),
7194        ),
7195    );
7196    let mut progress = ext_widgets::ProgressIndicatorOptions::default();
7197    progress.layout = LayoutStyle::new()
7198        .with_width_percent(1.0)
7199        .with_height(10.0)
7200        .with_flex_grow(1.0)
7201        .with_flex_shrink(1.0);
7202    progress.fill_visual = UiVisual::panel(color(111, 203, 159), None, 3.0);
7203    progress.accessibility_label = Some("Logged loading progress".to_string());
7204    ext_widgets::progress_indicator(
7205        ui,
7206        progress_slot,
7207        "progress.logged.progress",
7208        ext_widgets::ProgressIndicatorValue::percent(progress_value),
7209        progress,
7210    );
7211    let mut reset = widgets::ButtonOptions::new(
7212        LayoutStyle::new()
7213            .with_width(76.0)
7214            .with_height(30.0)
7215            .with_flex_shrink(0.0),
7216    )
7217    .with_action("progress.logged.reset");
7218    reset.visual = button_visual(38, 46, 58);
7219    reset.hovered_visual = Some(button_visual(65, 86, 106));
7220    reset.pressed_visual = Some(button_visual(34, 54, 84));
7221    reset.text_style = text(12.0, color(238, 244, 252));
7222    widgets::button(ui, progress_row, "progress.logged.reset", "Reset", reset);
7223
7224    let log_scroll = progress_log_scroll_state(
7225        state.progress_logs_scroll.offset().y,
7226        logs.len(),
7227        state.progress_logs_follow_tail,
7228    );
7229    let logs_node = ui.add_child(
7230        panel,
7231        UiNode::container(
7232            "progress.logged.logs",
7233            LayoutStyle::column()
7234                .with_width_percent(1.0)
7235                .with_height(PROGRESS_LOG_VIEWPORT_HEIGHT)
7236                .with_flex_shrink(0.0),
7237        )
7238        .with_visual(UiVisual::panel(
7239            color(11, 15, 21),
7240            Some(StrokeStyle::new(color(45, 57, 73), 1.0)),
7241            3.0,
7242        ))
7243        .with_scroll(ScrollAxes::VERTICAL)
7244        .with_accessibility(
7245            AccessibilityMeta::new(AccessibilityRole::List)
7246                .label("Loading logs")
7247                .value(format!("{} entries", logs.len())),
7248        ),
7249    );
7250    {
7251        let node = ui.node_mut(logs_node);
7252        node.set_action("progress.logged.logs.scroll");
7253        node.set_scroll(log_scroll);
7254    }
7255
7256    if logs.is_empty() {
7257        ui.add_child(
7258            logs_node,
7259            UiNode::text(
7260                "progress.logged.logs.empty",
7261                "Waiting for log output...",
7262                text(12.0, color(154, 166, 184)),
7263                LayoutStyle::new()
7264                    .with_width_percent(1.0)
7265                    .with_height(PROGRESS_LOG_ROW_HEIGHT)
7266                    .with_padding(4.0)
7267                    .with_flex_shrink(0.0),
7268            )
7269            .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Status).label("No logs")),
7270        );
7271    } else {
7272        for (index, entry) in logs.iter().enumerate() {
7273            let mut text_style = text(12.0, entry.level.color());
7274            text_style.line_height = 18.0;
7275            ui.add_child(
7276                logs_node,
7277                UiNode::text(
7278                    format!("{name}.logs.row.{index}"),
7279                    format!("[{}] {}", entry.level.as_str(), entry.message),
7280                    text_style,
7281                    LayoutStyle::new()
7282                        .with_width_percent(1.0)
7283                        .with_height(PROGRESS_LOG_ROW_HEIGHT)
7284                        .with_padding(4.0)
7285                        .with_flex_shrink(0.0),
7286                )
7287                .with_accessibility(
7288                    AccessibilityMeta::new(AccessibilityRole::ListItem).label(format!(
7289                        "{}: {}",
7290                        entry.level.as_str(),
7291                        entry.message
7292                    )),
7293                ),
7294            );
7295        }
7296    }
7297}
7298
7299fn progress_log_scroll_state(
7300    saved_offset_y: f32,
7301    log_count: usize,
7302    follow_tail: bool,
7303) -> operad::ScrollState {
7304    let content_height = log_count.max(1) as f32 * PROGRESS_LOG_ROW_HEIGHT;
7305    let max_offset = (content_height - PROGRESS_LOG_VIEWPORT_HEIGHT).max(0.0);
7306    let offset_y = if follow_tail {
7307        max_offset
7308    } else {
7309        saved_offset_y.min(max_offset)
7310    };
7311    operad::ScrollState::new(ScrollAxes::VERTICAL)
7312        .with_sizes(
7313            UiSize::new(8.0, PROGRESS_LOG_VIEWPORT_HEIGHT),
7314            UiSize::new(8.0, content_height),
7315        )
7316        .with_offset(UiPoint::new(0.0, offset_y))
7317}
7318
7319fn progress_demo_logs(progress: f32) -> Vec<ext_widgets::ProgressLogEntry> {
7320    let mut logs = vec![
7321        ext_widgets::ProgressLogEntry::info("Initializing renderer"),
7322        ext_widgets::ProgressLogEntry::info("Mounting content archive"),
7323    ];
7324    if progress >= 24.0 {
7325        logs.push(ext_widgets::ProgressLogEntry::success(
7326            "Compiled material shaders",
7327        ));
7328    }
7329    if progress >= 48.0 {
7330        logs.push(ext_widgets::ProgressLogEntry::info("Decoded texture atlas"));
7331    }
7332    if progress >= 72.0 {
7333        logs.push(ext_widgets::ProgressLogEntry::warning(
7334            "Optional cloud profile is still pending",
7335        ));
7336    }
7337    if progress >= 96.0 {
7338        logs.push(ext_widgets::ProgressLogEntry::success("Ready"));
7339    }
7340    logs
7341}
7342
7343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7344enum EaseCurveKind {
7345    Quad,
7346    Cubic,
7347    Quart,
7348    Expo,
7349    Back,
7350    Elastic,
7351    Bounce,
7352}
7353
7354impl EaseCurveKind {
7355    fn id(self) -> &'static str {
7356        match self {
7357            Self::Quad => "quad",
7358            Self::Cubic => "cubic",
7359            Self::Quart => "quart",
7360            Self::Expo => "expo",
7361            Self::Back => "back",
7362            Self::Elastic => "elastic",
7363            Self::Bounce => "bounce",
7364        }
7365    }
7366
7367    fn base_label(self) -> &'static str {
7368        match self {
7369            Self::Quad => "quad",
7370            Self::Cubic => "cubic",
7371            Self::Quart => "quart",
7372            Self::Expo => "expo",
7373            Self::Back => "back",
7374            Self::Elastic => "elastic",
7375            Self::Bounce => "bounce",
7376        }
7377    }
7378
7379    fn sample_out(self, progress: f32) -> f32 {
7380        let t = unit(progress);
7381        match self {
7382            Self::Quad => 1.0 - (1.0 - t).powi(2),
7383            Self::Cubic => 1.0 - (1.0 - t).powi(3),
7384            Self::Quart => 1.0 - (1.0 - t).powi(4),
7385            Self::Expo => {
7386                if t >= 1.0 {
7387                    1.0
7388                } else {
7389                    1.0 - 2.0_f32.powf(-10.0 * t)
7390                }
7391            }
7392            Self::Back => {
7393                let c1 = 1.70158;
7394                let c3 = c1 + 1.0;
7395                1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
7396            }
7397            Self::Elastic => {
7398                if t <= 0.0 {
7399                    0.0
7400                } else if t >= 1.0 {
7401                    1.0
7402                } else {
7403                    let period = (2.0 * std::f32::consts::PI) / 3.0;
7404                    2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * period).sin() + 1.0
7405                }
7406            }
7407            Self::Bounce => ease_out_bounce(t),
7408        }
7409    }
7410}
7411
7412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7413enum EaseDirection {
7414    In,
7415    Out,
7416}
7417
7418impl EaseDirection {
7419    fn label_prefix(self) -> &'static str {
7420        match self {
7421            Self::In => "Ease in",
7422            Self::Out => "Ease out",
7423        }
7424    }
7425}
7426
7427#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7428struct EasingFunction {
7429    direction: EaseDirection,
7430    kind: EaseCurveKind,
7431}
7432
7433impl EasingFunction {
7434    const fn new(direction: EaseDirection, kind: EaseCurveKind) -> Self {
7435        Self { direction, kind }
7436    }
7437
7438    fn label(self) -> String {
7439        format!(
7440            "{} {}",
7441            self.direction.label_prefix(),
7442            self.kind.base_label()
7443        )
7444    }
7445
7446    fn sample(self, progress: f32) -> f32 {
7447        let t = unit(progress);
7448        match self.direction {
7449            EaseDirection::In => 1.0 - self.kind.sample_out(1.0 - t),
7450            EaseDirection::Out => self.kind.sample_out(t),
7451        }
7452    }
7453}
7454
7455fn ease_out_bounce(t: f32) -> f32 {
7456    let n1 = 7.5625;
7457    let d1 = 2.75;
7458    if t < 1.0 / d1 {
7459        n1 * t * t
7460    } else if t < 2.0 / d1 {
7461        let t = t - 1.5 / d1;
7462        n1 * t * t + 0.75
7463    } else if t < 2.5 / d1 {
7464        let t = t - 2.25 / d1;
7465        n1 * t * t + 0.9375
7466    } else {
7467        let t = t - 2.625 / d1;
7468        n1 * t * t + 0.984375
7469    }
7470}
7471
7472fn easing_options(direction: EaseDirection) -> Vec<ext_widgets::SelectOption> {
7473    [
7474        EaseCurveKind::Quad,
7475        EaseCurveKind::Cubic,
7476        EaseCurveKind::Quart,
7477        EaseCurveKind::Expo,
7478        EaseCurveKind::Back,
7479        EaseCurveKind::Elastic,
7480        EaseCurveKind::Bounce,
7481    ]
7482    .into_iter()
7483    .map(|kind| {
7484        ext_widgets::SelectOption::new(kind.id(), EasingFunction::new(direction, kind).label())
7485    })
7486    .collect()
7487}
7488
7489fn selected_easing(
7490    state: &ext_widgets::SelectMenuState,
7491    direction: EaseDirection,
7492) -> EasingFunction {
7493    let options = easing_options(direction);
7494    let kind = match state.selected_id(&options) {
7495        Some("quad") => EaseCurveKind::Quad,
7496        Some("quart") => EaseCurveKind::Quart,
7497        Some("expo") => EaseCurveKind::Expo,
7498        Some("back") => EaseCurveKind::Back,
7499        Some("elastic") => EaseCurveKind::Elastic,
7500        Some("bounce") => EaseCurveKind::Bounce,
7501        _ => EaseCurveKind::Cubic,
7502    };
7503    EasingFunction::new(direction, kind)
7504}
7505
7506fn animation_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7507    let body = section(ui, parent, "animation", "Animation");
7508
7509    if let Some(section) = animation_section(
7510        ui,
7511        body,
7512        "animation.timed",
7513        "Timed playback",
7514        state.animation_timed_expanded,
7515    ) {
7516        let live_stage = animation_stage(ui, section, "animation.live.stage");
7517        let live_amount = smooth_loop(state.progress_phase * 1.65, 0.0);
7518        let live_values = animation_blend_machine(
7519            ANIMATION_INPUT_PROGRESS,
7520            live_amount,
7521            UiPoint::new(220.0, 0.0),
7522            0.88,
7523            1.10,
7524            1.0,
7525        )
7526        .with_bool_input("looping", true)
7527        .values();
7528        ui.add_child(
7529            live_stage,
7530            UiNode::scene(
7531                "animation.live.orb",
7532                animation_orb_primitives(
7533                    color(108, 180, 255),
7534                    ANIMATION_ORB_SIZE * live_values.scale,
7535                    UiPoint::new(
7536                        28.0 + live_values.translate.x,
7537                        37.0 + live_values.translate.y,
7538                    ),
7539                ),
7540                animation_scene_layout(),
7541            )
7542            .with_accessibility(
7543                AccessibilityMeta::new(AccessibilityRole::Image).label("Looping orb"),
7544            ),
7545        );
7546    }
7547
7548    if let Some(section) = animation_section(
7549        ui,
7550        body,
7551        "animation.scrub",
7552        "Scrubbed input",
7553        state.animation_scrub_expanded,
7554    ) {
7555        let scrub_row = row(ui, section, "animation.scrub.row", 10.0);
7556        widgets::slider(
7557            ui,
7558            scrub_row,
7559            "animation.scrub.slider",
7560            state.animation_scrub,
7561            0.0..1.0,
7562            widgets::SliderOptions::default()
7563                .with_layout(
7564                    LayoutStyle::new()
7565                        .with_width(200.0)
7566                        .with_height(28.0)
7567                        .with_flex_shrink(0.0),
7568                )
7569                .with_value_edit_action("animation.scrub"),
7570        );
7571        widgets::label(
7572            ui,
7573            scrub_row,
7574            "animation.scrub.value",
7575            format!("{:.0}%", state.animation_scrub * 100.0),
7576            text(12.0, color(186, 198, 216)),
7577            LayoutStyle::new().with_width_percent(1.0),
7578        );
7579        let scrub_stage = animation_stage(ui, section, "animation.scrub.stage");
7580        let scrub_values = animation_blend_machine(
7581            ANIMATION_INPUT_SCRUB,
7582            state.animation_scrub,
7583            UiPoint::new(220.0, 0.0),
7584            0.82,
7585            1.14,
7586            1.0,
7587        )
7588        .values();
7589        ui.add_child(
7590            scrub_stage,
7591            UiNode::scene(
7592                "animation.scrub.shape",
7593                animation_morph_shape_primitives(
7594                    color(111, 203, 159),
7595                    ANIMATION_SHAPE_SIZE * scrub_values.scale,
7596                    UiPoint::new(
7597                        28.0 + scrub_values.translate.x,
7598                        37.0 + scrub_values.translate.y,
7599                    ),
7600                    scrub_values.morph,
7601                ),
7602                animation_scene_layout(),
7603            )
7604            .with_accessibility(
7605                AccessibilityMeta::new(AccessibilityRole::Image).label("Scrubbed morphing shape"),
7606            ),
7607        );
7608    }
7609
7610    if let Some(section) = animation_section(
7611        ui,
7612        body,
7613        "animation.state",
7614        "Boolean input transition",
7615        state.animation_state_expanded,
7616    ) {
7617        let state_row = row(ui, section, "animation.state.row", 10.0);
7618        let mut open = widgets::ButtonOptions::new(
7619            LayoutStyle::new()
7620                .with_width(92.0)
7621                .with_height(30.0)
7622                .with_flex_shrink(0.0),
7623        )
7624        .with_action("animation.open");
7625        open.visual = if state.animation_open {
7626            button_visual(48, 112, 184)
7627        } else {
7628            button_visual(38, 46, 58)
7629        };
7630        open.hovered_visual = Some(button_visual(65, 86, 106));
7631        open.pressed_visual = Some(button_visual(34, 54, 84));
7632        open.text_style = text(12.0, color(238, 244, 252));
7633        widgets::button(
7634            ui,
7635            state_row,
7636            "animation.open",
7637            if state.animation_open {
7638                "Close"
7639            } else {
7640                "Open"
7641            },
7642            open,
7643        );
7644        let open_stage = animation_stage(ui, section, "animation.state.stage");
7645        let panel_offset = if state.animation_open {
7646            UiPoint::new(
7647                ANIMATION_STAGE_MIN_WIDTH - ANIMATION_PANEL_WIDTH - ANIMATION_PANEL_INSET_X,
7648                ANIMATION_PANEL_Y,
7649            )
7650        } else {
7651            UiPoint::new(ANIMATION_PANEL_INSET_X, ANIMATION_PANEL_Y)
7652        };
7653        ui.add_child(
7654            open_stage,
7655            UiNode::scene(
7656                "animation.state.panel",
7657                animation_panel_primitives(panel_offset),
7658                animation_scene_layout(),
7659            )
7660            .with_animation(animation_open_machine(state.animation_open))
7661            .with_accessibility(
7662                AccessibilityMeta::new(AccessibilityRole::Image).label("Open state panel"),
7663            ),
7664        );
7665    }
7666
7667    if let Some(section) = animation_section(
7668        ui,
7669        body,
7670        "animation.interaction",
7671        "Interaction inputs",
7672        state.animation_interaction_expanded,
7673    ) {
7674        let interaction_stage = animation_stage(ui, section, "animation.interaction.stage");
7675        ui.add_child(
7676            interaction_stage,
7677            UiNode::scene(
7678                "animation.interaction.target",
7679                animation_interaction_primitives(
7680                    color(176, 126, 230),
7681                    ANIMATION_ORB_SIZE,
7682                    UiPoint::new(40.0, 37.0),
7683                ),
7684                animation_scene_layout(),
7685            )
7686            .with_input(InputBehavior::BUTTON)
7687            .with_animation(animation_interaction_machine())
7688            .with_accessibility(
7689                AccessibilityMeta::new(AccessibilityRole::Button)
7690                    .label("Interaction animation target")
7691                    .focusable(),
7692            ),
7693        );
7694    }
7695}
7696
7697fn easing_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7698    let body = section(ui, parent, "easing", "Easing");
7699    let linear_progress = (state.progress_phase * 0.25).rem_euclid(1.0);
7700    easing_curve_demo(
7701        ui,
7702        body,
7703        "easing.in",
7704        "Ease-in functions",
7705        EaseDirection::In,
7706        &state.easing_in,
7707        linear_progress,
7708    );
7709    divider(ui, body, "easing.divider");
7710    easing_curve_demo(
7711        ui,
7712        body,
7713        "easing.out",
7714        "Ease-out functions",
7715        EaseDirection::Out,
7716        &state.easing_out,
7717        linear_progress,
7718    );
7719}
7720
7721fn easing_curve_demo(
7722    ui: &mut UiDocument,
7723    parent: UiNodeId,
7724    name: &'static str,
7725    title: &'static str,
7726    direction: EaseDirection,
7727    state: &ext_widgets::SelectMenuState,
7728    linear_progress: f32,
7729) {
7730    widgets::label(
7731        ui,
7732        parent,
7733        format!("{name}.title"),
7734        title,
7735        text(12.0, color(186, 198, 216)),
7736        LayoutStyle::new().with_width_percent(1.0),
7737    );
7738
7739    let options = easing_options(direction);
7740    let selected = selected_easing(state, direction);
7741    let eased_progress = selected.sample(linear_progress);
7742    let controls = row(ui, parent, format!("{name}.controls"), 10.0);
7743    let dropdown_width = 184.0;
7744    let dropdown_name = format!("{name}.dropdown");
7745    let dropdown_anchor = ui.add_child(
7746        controls,
7747        UiNode::container(
7748            format!("{name}.dropdown.anchor"),
7749            LayoutStyle::new()
7750                .with_width(dropdown_width)
7751                .with_height(30.0)
7752                .with_flex_shrink(0.0),
7753        ),
7754    );
7755    let dropdown_nodes = ext_widgets::dropdown_select(
7756        ui,
7757        dropdown_anchor,
7758        dropdown_name.clone(),
7759        &options,
7760        state,
7761        Some(select_popup(
7762            UiRect::new(0.0, 0.0, dropdown_width, 30.0),
7763            UiRect::new(0.0, 0.0, EASING_STAGE_MIN_WIDTH, 260.0),
7764        )),
7765        dropdown_select_options(
7766            dropdown_width,
7767            dropdown_name.as_str(),
7768            "Ease function",
7769            title,
7770        ),
7771    );
7772    ui.node_mut(dropdown_nodes.trigger)
7773        .set_action(format!("{name}.dropdown.toggle"));
7774    widgets::label(
7775        ui,
7776        controls,
7777        format!("{name}.value"),
7778        format!(
7779            "{:.0}% -> {:.0}%",
7780            linear_progress * 100.0,
7781            eased_progress * 100.0
7782        ),
7783        text(12.0, color(186, 198, 216)),
7784        LayoutStyle::new().with_width_percent(1.0),
7785    );
7786
7787    let stage = easing_stage(ui, parent, format!("{name}.stage"));
7788    ui.add_child(
7789        stage,
7790        UiNode::scene(
7791            format!("{name}.graph"),
7792            easing_curve_primitives(selected, linear_progress),
7793            animation_scene_layout(),
7794        )
7795        .with_accessibility(
7796            AccessibilityMeta::new(AccessibilityRole::Image)
7797                .label(format!("{} curve and looping marker", selected.label())),
7798        ),
7799    );
7800}
7801
7802fn animation_section(
7803    ui: &mut UiDocument,
7804    parent: UiNodeId,
7805    name: &'static str,
7806    title: &'static str,
7807    expanded: bool,
7808) -> Option<UiNodeId> {
7809    let mut options = widgets::CollapsingHeaderOptions::default()
7810        .expanded(expanded)
7811        .with_toggle_action(format!("{name}.toggle"));
7812    options.text_style = text(12.0, color(220, 228, 238));
7813    options.indicator_text_style = text(12.0, color(186, 198, 216));
7814    options.root_visual = UiVisual::panel(
7815        color(17, 22, 29),
7816        Some(StrokeStyle::new(color(48, 58, 72), 1.0)),
7817        6.0,
7818    );
7819    options.header_visual = UiVisual::panel(color(21, 26, 33), None, 0.0);
7820    options.hovered_visual = UiVisual::panel(color(38, 48, 61), None, 0.0);
7821    options.pressed_visual = UiVisual::panel(color(27, 36, 48), None, 0.0);
7822    options.body_layout = LayoutStyle::column()
7823        .with_width_percent(1.0)
7824        .with_padding(10.0)
7825        .with_gap(10.0);
7826    widgets::collapsing_header(ui, parent, name, title, options).body
7827}
7828
7829fn animation_stage(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7830    let layout = LayoutStyle::row()
7831        .with_width_percent(1.0)
7832        .with_height(ANIMATION_STAGE_HEIGHT)
7833        .with_align_items(taffy::prelude::AlignItems::Center)
7834        .with_flex_shrink(0.0);
7835    let layout = operad::layout::with_min_size(
7836        layout,
7837        operad::length(ANIMATION_STAGE_MIN_WIDTH),
7838        operad::length(ANIMATION_STAGE_HEIGHT),
7839    );
7840    ui.add_child(
7841        parent,
7842        UiNode::container(name, layout).with_visual(UiVisual::panel(
7843            color(16, 21, 28),
7844            Some(StrokeStyle::new(color(48, 58, 72), 1.0)),
7845            6.0,
7846        )),
7847    )
7848}
7849
7850fn easing_stage(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7851    let layout = LayoutStyle::row()
7852        .with_width_percent(1.0)
7853        .with_height(EASING_STAGE_HEIGHT)
7854        .with_align_items(taffy::prelude::AlignItems::Center)
7855        .with_flex_shrink(0.0);
7856    let layout = operad::layout::with_min_size(
7857        layout,
7858        operad::length(EASING_STAGE_MIN_WIDTH),
7859        operad::length(EASING_STAGE_HEIGHT),
7860    );
7861    ui.add_child(
7862        parent,
7863        UiNode::container(name, layout).with_visual(UiVisual::panel(
7864            color(16, 21, 28),
7865            Some(StrokeStyle::new(color(48, 58, 72), 1.0)),
7866            6.0,
7867        )),
7868    )
7869}
7870
7871fn animation_scene_layout() -> LayoutStyle {
7872    let layout = LayoutStyle::new()
7873        .with_width_percent(1.0)
7874        .with_height_percent(1.0)
7875        .with_flex_grow(1.0)
7876        .with_flex_shrink(1.0);
7877    operad::layout::with_min_size(layout, operad::length(0.0), operad::length(0.0))
7878}
7879
7880fn easing_curve_primitives(function: EasingFunction, linear_progress: f32) -> Vec<ScenePrimitive> {
7881    let mut primitives = Vec::new();
7882    let graph = UiRect::new(24.0, 24.0, 172.0, 112.0);
7883    primitives.push(ScenePrimitive::Rect(
7884        PaintRect::solid(graph, color(12, 17, 24))
7885            .stroke(AlignedStroke::inside(StrokeStyle::new(
7886                color(58, 70, 88),
7887                1.0,
7888            )))
7889            .corner_radii(CornerRadii::uniform(4.0)),
7890    ));
7891    for index in 1..4 {
7892        let fraction = index as f32 / 4.0;
7893        let x = graph.x + graph.width * fraction;
7894        let y = graph.y + graph.height * fraction;
7895        primitives.push(ScenePrimitive::Line {
7896            from: UiPoint::new(x, graph.y),
7897            to: UiPoint::new(x, graph.y + graph.height),
7898            stroke: StrokeStyle::new(color(29, 38, 50), 1.0),
7899        });
7900        primitives.push(ScenePrimitive::Line {
7901            from: UiPoint::new(graph.x, y),
7902            to: UiPoint::new(graph.x + graph.width, y),
7903            stroke: StrokeStyle::new(color(29, 38, 50), 1.0),
7904        });
7905    }
7906    primitives.push(ScenePrimitive::Line {
7907        from: UiPoint::new(graph.x, graph.y + graph.height),
7908        to: UiPoint::new(graph.x + graph.width, graph.y + graph.height),
7909        stroke: StrokeStyle::new(color(118, 136, 162), 1.0),
7910    });
7911    primitives.push(ScenePrimitive::Line {
7912        from: UiPoint::new(graph.x, graph.y),
7913        to: UiPoint::new(graph.x, graph.y + graph.height),
7914        stroke: StrokeStyle::new(color(118, 136, 162), 1.0),
7915    });
7916
7917    let samples = 40;
7918    let mut previous = None;
7919    for index in 0..=samples {
7920        let t = index as f32 / samples as f32;
7921        let eased = function.sample(t);
7922        let point = UiPoint::new(
7923            graph.x + graph.width * t,
7924            graph.y + graph.height - graph.height * eased.clamp(-0.16, 1.16),
7925        );
7926        if let Some(from) = previous {
7927            primitives.push(ScenePrimitive::Line {
7928                from,
7929                to: point,
7930                stroke: StrokeStyle::new(color(112, 181, 255), 2.0),
7931            });
7932        }
7933        previous = Some(point);
7934    }
7935
7936    let eased_progress = function.sample(linear_progress);
7937    let graph_marker = UiPoint::new(
7938        graph.x + graph.width * linear_progress,
7939        graph.y + graph.height - graph.height * eased_progress.clamp(-0.16, 1.16),
7940    );
7941    primitives.push(ScenePrimitive::Circle {
7942        center: graph_marker,
7943        radius: 5.5,
7944        fill: color(248, 252, 255),
7945        stroke: Some(StrokeStyle::new(color(112, 181, 255), 2.0)),
7946    });
7947
7948    let track = UiRect::new(232.0, 64.0, 96.0, 12.0);
7949    let marker_progress = eased_progress.clamp(-0.10, 1.10);
7950    primitives.push(ScenePrimitive::Rect(
7951        PaintRect::solid(track, color(37, 46, 58)).corner_radii(CornerRadii::uniform(6.0)),
7952    ));
7953    primitives.push(ScenePrimitive::Rect(
7954        PaintRect::solid(
7955            UiRect::new(
7956                track.x,
7957                track.y,
7958                track.width * eased_progress.clamp(0.0, 1.0),
7959                track.height,
7960            ),
7961            color(108, 180, 255),
7962        )
7963        .corner_radii(CornerRadii::uniform(6.0)),
7964    ));
7965    primitives.push(ScenePrimitive::Circle {
7966        center: UiPoint::new(
7967            track.x + track.width * marker_progress,
7968            track.y + track.height * 0.5,
7969        ),
7970        radius: 14.0,
7971        fill: color(112, 181, 255),
7972        stroke: Some(StrokeStyle::new(color(232, 242, 255), 2.0)),
7973    });
7974    primitives.push(ScenePrimitive::Text(
7975        PaintText::new(
7976            function.label(),
7977            UiRect::new(222.0, 98.0, 120.0, 20.0),
7978            text(10.0, color(186, 198, 216)),
7979        )
7980        .horizontal_align(TextHorizontalAlign::Center)
7981        .multiline(false),
7982    ));
7983    primitives
7984}
7985
7986fn animation_blend_machine(
7987    input: &'static str,
7988    value: f32,
7989    translate: UiPoint,
7990    start_scale: f32,
7991    end_scale: f32,
7992    end_opacity: f32,
7993) -> AnimationMachine {
7994    let start_values = AnimatedValues::new(0.45, UiPoint::new(0.0, 0.0), start_scale);
7995    let end_values = AnimatedValues::new(end_opacity, translate, end_scale).with_morph(1.0);
7996    AnimationMachine::new(
7997        vec![
7998            AnimationState::new("start", start_values),
7999            AnimationState::new("end", end_values),
8000        ],
8001        Vec::new(),
8002        "start",
8003    )
8004    .unwrap_or_else(|_| AnimationMachine::single_state("start", start_values))
8005    .with_number_input(input, value)
8006    .with_blend_binding(AnimationBlendBinding::new(input, "start", "end"))
8007}
8008
8009fn animation_open_machine(open: bool) -> AnimationMachine {
8010    let closed_values = AnimatedValues::new(0.35, UiPoint::new(0.0, 0.0), 1.0);
8011    let open_values = AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0);
8012    let fallback_values = if open { open_values } else { closed_values };
8013    AnimationMachine::new(
8014        vec![
8015            AnimationState::new("closed", closed_values),
8016            AnimationState::new("open", open_values),
8017        ],
8018        vec![
8019            AnimationTransition::when(
8020                "closed",
8021                "open",
8022                AnimationCondition::bool(ANIMATION_INPUT_OPEN, true),
8023                0.18,
8024            ),
8025            AnimationTransition::when(
8026                "open",
8027                "closed",
8028                AnimationCondition::bool(ANIMATION_INPUT_OPEN, false),
8029                0.14,
8030            ),
8031        ],
8032        "closed",
8033    )
8034    .unwrap_or_else(|_| AnimationMachine::single_state("closed", fallback_values))
8035    .with_bool_input(ANIMATION_INPUT_OPEN, open)
8036}
8037
8038fn animation_interaction_machine() -> AnimationMachine {
8039    let rest_values = AnimatedValues::new(0.72, UiPoint::new(0.0, 0.0), 1.0);
8040    let right_values = AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0).with_morph(1.0);
8041    AnimationMachine::new(
8042        vec![
8043            AnimationState::new("rest", rest_values),
8044            AnimationState::new("right", right_values),
8045        ],
8046        Vec::new(),
8047        "rest",
8048    )
8049    .unwrap_or_else(|_| AnimationMachine::single_state("rest", rest_values))
8050    .with_number_input(ANIMATION_INPUT_POINTER_NORM_X, 0.0)
8051    .with_blend_binding(AnimationBlendBinding::new(
8052        ANIMATION_INPUT_POINTER_NORM_X,
8053        "rest",
8054        "right",
8055    ))
8056}
8057
8058fn animation_interaction_primitives(
8059    fill: ColorRgba,
8060    size: f32,
8061    offset: UiPoint,
8062) -> Vec<ScenePrimitive> {
8063    vec![
8064        ScenePrimitive::MorphPolygon {
8065            from_points: animation_square_points(size, offset),
8066            to_points: animation_pentagon_points(size, offset),
8067            amount: 0.0,
8068            fill,
8069            stroke: Some(StrokeStyle::new(color(236, 244, 255), 1.0)),
8070        },
8071        ScenePrimitive::Circle {
8072            center: UiPoint::new(offset.x + size * 0.34, offset.y + size * 0.30),
8073            radius: size * 0.10,
8074            fill: color(244, 248, 255),
8075            stroke: None,
8076        },
8077    ]
8078}
8079
8080fn animation_orb_primitives(fill: ColorRgba, size: f32, offset: UiPoint) -> Vec<ScenePrimitive> {
8081    let center = size * 0.5;
8082    let radius = size * 0.44;
8083    vec![
8084        ScenePrimitive::Circle {
8085            center: UiPoint::new(offset.x + center, offset.y + center),
8086            radius,
8087            fill,
8088            stroke: Some(StrokeStyle::new(color(236, 244, 255), 1.0)),
8089        },
8090        ScenePrimitive::Circle {
8091            center: UiPoint::new(offset.x + size * 0.34, offset.y + size * 0.30),
8092            radius: size * 0.12,
8093            fill: color(244, 248, 255),
8094            stroke: None,
8095        },
8096    ]
8097}
8098
8099fn animation_morph_shape_primitives(
8100    fill: ColorRgba,
8101    size: f32,
8102    offset: UiPoint,
8103    amount: f32,
8104) -> Vec<ScenePrimitive> {
8105    vec![ScenePrimitive::MorphPolygon {
8106        from_points: animation_square_points(size, offset),
8107        to_points: animation_pentagon_points(size, offset),
8108        amount,
8109        fill,
8110        stroke: Some(StrokeStyle::new(color(226, 246, 236), 1.0)),
8111    }]
8112}
8113
8114fn animation_square_points(size: f32, offset: UiPoint) -> Vec<UiPoint> {
8115    let inset = size * 0.08;
8116    let left = offset.x + inset;
8117    let top = offset.y + inset;
8118    let right = offset.x + size - inset;
8119    let bottom = offset.y + size - inset;
8120    let center_x = offset.x + size * 0.5;
8121    vec![
8122        UiPoint::new(center_x, top),
8123        UiPoint::new(right, top),
8124        UiPoint::new(right, bottom),
8125        UiPoint::new(left, bottom),
8126        UiPoint::new(left, top),
8127    ]
8128}
8129
8130fn animation_pentagon_points(size: f32, offset: UiPoint) -> Vec<UiPoint> {
8131    let center = size * 0.5;
8132    let radius = size * 0.46;
8133    (0..5)
8134        .map(|index| {
8135            let angle = -std::f32::consts::FRAC_PI_2 + index as f32 * std::f32::consts::TAU / 5.0;
8136            UiPoint::new(
8137                offset.x + center + angle.cos() * radius,
8138                offset.y + center + angle.sin() * radius,
8139            )
8140        })
8141        .collect()
8142}
8143
8144fn animation_panel_primitives(offset: UiPoint) -> Vec<ScenePrimitive> {
8145    vec![ScenePrimitive::Rect(
8146        PaintRect::solid(
8147            UiRect::new(
8148                offset.x,
8149                offset.y,
8150                ANIMATION_PANEL_WIDTH,
8151                ANIMATION_PANEL_HEIGHT,
8152            ),
8153            color(232, 186, 88),
8154        )
8155        .stroke(AlignedStroke::inside(StrokeStyle::new(
8156            color(255, 226, 154),
8157            1.0,
8158        )))
8159        .corner_radii(CornerRadii::uniform(6.0)),
8160    )]
8161}
8162
8163fn list_and_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8164    let body = section_with_min_viewport(
8165        ui,
8166        parent,
8167        "lists_tables",
8168        "Lists and tables",
8169        UiSize::new(520.0, 0.0),
8170    );
8171
8172    let list_row = ui.add_child(
8173        body,
8174        UiNode::container(
8175            "lists_tables.list_row",
8176            Layout::row()
8177                .size(LayoutSize::new(
8178                    LayoutDimension::percent(1.0),
8179                    LayoutDimension::Auto,
8180                ))
8181                .gap(LayoutGap::points(10.0, 10.0))
8182                .flex_wrap(LayoutFlexWrap::Wrap)
8183                .to_layout_style(),
8184        ),
8185    );
8186    let scroll_column = ui.add_child(
8187        list_row,
8188        UiNode::container(
8189            "lists_tables.scroll_area.column",
8190            Layout::column()
8191                .min_size(LayoutSize::points(220.0, 0.0))
8192                .gap(LayoutGap::points(6.0, 6.0))
8193                .flex(1.0, 1.0, LayoutDimension::points(245.0))
8194                .to_layout_style(),
8195        ),
8196    );
8197    widgets::label(
8198        ui,
8199        scroll_column,
8200        "lists_tables.scroll_area.title",
8201        "Scrollable list",
8202        text(12.0, color(166, 176, 190)),
8203        LayoutStyle::new().with_width_percent(1.0),
8204    );
8205    let nested_scroll = widgets::scroll_area(
8206        ui,
8207        scroll_column,
8208        "lists_tables.scroll_area",
8209        ScrollAxes::VERTICAL,
8210        LayoutStyle::column()
8211            .with_width_percent(1.0)
8212            .with_height(104.0),
8213    );
8214    ui.node_mut(nested_scroll)
8215        .set_action("lists_tables.scroll_area.scroll");
8216    if let Some(scroll) = ui.node_mut(nested_scroll).scroll_mut() {
8217        scroll.set_offset(UiPoint::new(0.0, state.list_scroll));
8218    }
8219    for index in 0..6 {
8220        widgets::label(
8221            ui,
8222            nested_scroll,
8223            format!("lists_tables.scroll_area.row.{index}"),
8224            format!("Scroll row {}", index + 1),
8225            text(12.0, color(200, 212, 228)),
8226            LayoutStyle::new()
8227                .with_width_percent(1.0)
8228                .with_height(26.0)
8229                .with_flex_shrink(0.0),
8230        );
8231    }
8232
8233    let virtual_list_column = ui.add_child(
8234        list_row,
8235        UiNode::container(
8236            "lists_tables.virtual_list.column",
8237            Layout::column()
8238                .min_size(LayoutSize::points(220.0, 0.0))
8239                .gap(LayoutGap::points(6.0, 6.0))
8240                .flex(1.0, 1.0, LayoutDimension::points(245.0))
8241                .to_layout_style(),
8242        ),
8243    );
8244
8245    widgets::label(
8246        ui,
8247        virtual_list_column,
8248        "lists_tables.virtual_list.title",
8249        "Virtualized list",
8250        text(12.0, color(166, 176, 190)),
8251        LayoutStyle::new().with_width_percent(1.0),
8252    );
8253    let virtual_list = widgets::virtual_list(
8254        ui,
8255        virtual_list_column,
8256        "lists_tables.virtual_list",
8257        widgets::VirtualListSpec {
8258            row_count: 24,
8259            row_height: 28.0,
8260            viewport_height: 104.0,
8261            scroll_offset: state.virtual_scroll,
8262            overscan: 1,
8263        },
8264        |ui, row_parent, row| {
8265            widgets::label(
8266                ui,
8267                row_parent,
8268                format!("lists_tables.virtual_list.row.{row}"),
8269                format!("Virtual row {}", row + 1),
8270                text(12.0, color(214, 224, 238)),
8271                LayoutStyle::new()
8272                    .with_width_percent(1.0)
8273                    .with_height(28.0)
8274                    .with_flex_shrink(0.0),
8275            );
8276        },
8277    );
8278    ui.node_mut(virtual_list)
8279        .set_action("lists_tables.virtual_list.scroll");
8280
8281    widgets::separator(
8282        ui,
8283        body,
8284        "lists_tables.virtualized_table.separator",
8285        widgets::SeparatorOptions::default(),
8286    );
8287    widgets::label(
8288        ui,
8289        body,
8290        "lists_tables.data_table.title",
8291        "Virtualized selectable table",
8292        text(12.0, color(166, 176, 190)),
8293        LayoutStyle::new().with_width_percent(1.0),
8294    );
8295    let virtual_controls = wrapping_row(ui, body, "lists_tables.virtualized_table.controls", 8.0);
8296    button(
8297        ui,
8298        virtual_controls,
8299        "lists_tables.virtualized_table.sort.name",
8300        if state.virtual_table_descending {
8301            "Name desc"
8302        } else {
8303            "Name asc"
8304        },
8305        "lists_tables.virtualized_table.sort.name",
8306        button_visual(38, 52, 70),
8307    );
8308    button(
8309        ui,
8310        virtual_controls,
8311        "lists_tables.virtualized_table.filter.status",
8312        if state.virtual_table_ready_only {
8313            "Ready only"
8314        } else {
8315            "All status"
8316        },
8317        "lists_tables.virtualized_table.filter.status",
8318        button_visual(38, 52, 70),
8319    );
8320    button(
8321        ui,
8322        virtual_controls,
8323        "lists_tables.virtualized_table.resize.reset",
8324        "Reset width",
8325        "lists_tables.virtualized_table.resize.reset",
8326        button_visual(38, 52, 70),
8327    );
8328
8329    let columns = virtual_table_columns(state);
8330    let visible_rows = virtual_table_visible_rows(state);
8331    let mut table_options = ext_widgets::DataTableOptions::default()
8332        .with_row_action_prefix("lists_tables.virtualized_table")
8333        .with_cell_action_prefix("lists_tables.virtualized_table")
8334        .with_scroll_action("lists_tables.virtualized_table.scroll");
8335    table_options.layout = LayoutStyle::column()
8336        .with_width_percent(1.0)
8337        .with_flex_shrink(0.0);
8338    table_options.header_visual = UiVisual::panel(
8339        color(34, 41, 50),
8340        Some(StrokeStyle::new(color(67, 78, 95), 1.0)),
8341        0.0,
8342    );
8343    table_options.header_text_style = text(12.0, color(222, 230, 240));
8344    table_options.selection = state.table_selection.clone();
8345    ext_widgets::virtualized_data_table(
8346        ui,
8347        body,
8348        "lists_tables.virtualized_table",
8349        &columns,
8350        ext_widgets::VirtualDataTableSpec {
8351            row_count: visible_rows.len(),
8352            row_height: 28.0,
8353            viewport_width: 520.0,
8354            viewport_height: 156.0,
8355            scroll_offset: UiPoint::new(0.0, state.virtual_table_scroll),
8356            overscan_rows: 1,
8357        },
8358        table_options,
8359        |ui, cell_parent, cell| {
8360            let source_row = visible_rows.get(cell.row).copied().unwrap_or(cell.row);
8361            let value = virtual_table_cell_value(source_row, cell.column);
8362            widgets::label(
8363                ui,
8364                cell_parent,
8365                format!(
8366                    "lists_tables.virtualized_table.cell.{}.{}.label",
8367                    cell.row, cell.column
8368                ),
8369                value,
8370                text(12.0, color(220, 228, 238)),
8371                LayoutStyle::new().with_width_percent(1.0),
8372            );
8373        },
8374    );
8375}
8376
8377#[allow(clippy::field_reassign_with_default)]
8378fn property_inspector(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8379    let body = section(ui, parent, "property_inspector", "Property inspector");
8380    widgets::label(
8381        ui,
8382        body,
8383        "property_inspector.target",
8384        "Inspecting: Styling preview",
8385        text(12.0, color(196, 210, 230)),
8386        LayoutStyle::new().with_width_percent(1.0),
8387    );
8388    let mut options = ext_widgets::PropertyInspectorOptions::default();
8389    options.selected_index = Some(0);
8390    options.label_width = 120.0;
8391    options.row_height = 30.0;
8392    ext_widgets::property_inspector_grid(
8393        ui,
8394        body,
8395        "property_inspector.grid",
8396        &[
8397            ext_widgets::PropertyGridRow::new("target", "Widget", "Button preview").read_only(),
8398            ext_widgets::PropertyGridRow::new(
8399                "inner",
8400                "Inner margin",
8401                format!("{:.0}px", state.styling.inner_margin),
8402            )
8403            .with_kind(ext_widgets::PropertyValueKind::Number),
8404            ext_widgets::PropertyGridRow::new(
8405                "outer",
8406                "Outer margin",
8407                format!("{:.0}px", state.styling.outer_margin),
8408            )
8409            .with_kind(ext_widgets::PropertyValueKind::Number),
8410            ext_widgets::PropertyGridRow::new(
8411                "radius",
8412                "Corner radius",
8413                format!("{:.0}px", state.styling.corner_radius),
8414            )
8415            .with_kind(ext_widgets::PropertyValueKind::Number),
8416            ext_widgets::PropertyGridRow::new(
8417                "stroke",
8418                "Stroke",
8419                format!("{:.1}px", state.styling.stroke_width),
8420            )
8421            .with_kind(ext_widgets::PropertyValueKind::Number)
8422            .changed(),
8423            ext_widgets::PropertyGridRow::new("state", "Source", "Styling widget").read_only(),
8424        ],
8425        options,
8426    );
8427}
8428
8429fn diagnostics_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8430    let body = section(ui, parent, "diagnostics", "Diagnostics");
8431    let debug_snapshot = &state.diagnostics_snapshot;
8432
8433    diagnostics_selected_node_panel(ui, body, debug_snapshot);
8434    diagnostics_animation_panel(ui, body, state, debug_snapshot);
8435
8436    widgets::label(
8437        ui,
8438        body,
8439        "diagnostics.a11y.title",
8440        "Accessibility",
8441        text(14.0, color(222, 230, 240)),
8442        LayoutStyle::new().with_width_percent(1.0),
8443    );
8444    let mut overlay_preview_style = UiNodeStyle::from(
8445        LayoutStyle::new()
8446            .with_width(320.0)
8447            .with_height(140.0)
8448            .with_flex_shrink(0.0),
8449    );
8450    overlay_preview_style.set_clip(ClipBehavior::Clip);
8451    let overlay_preview = ui.add_child(
8452        body,
8453        UiNode::container("diagnostics.a11y.preview", overlay_preview_style).with_visual(
8454            UiVisual::panel(
8455                color(12, 17, 24),
8456                Some(StrokeStyle::new(color(47, 62, 82), 1.0)),
8457                4.0,
8458            ),
8459        ),
8460    );
8461    let mut overlay_options = ext_widgets::AccessibilityDebugOverlayOptions {
8462        action_prefix: Some("diagnostics.a11y.visual".to_owned()),
8463        ..Default::default()
8464    };
8465    overlay_options.show_labels = false;
8466    ext_widgets::accessibility_debug_overlay(
8467        ui,
8468        overlay_preview,
8469        "diagnostics.a11y.visual",
8470        &debug_snapshot,
8471        overlay_options,
8472    );
8473    diagnostics_accessibility_details(ui, body, debug_snapshot);
8474
8475    let diagnostic_columns = ui.add_child(
8476        body,
8477        UiNode::container(
8478            "diagnostics.columns",
8479            LayoutStyle::column()
8480                .with_width_percent(1.0)
8481                .with_flex_shrink(0.0)
8482                .gap(10.0),
8483        ),
8484    );
8485    let command_column = ui.add_child(
8486        diagnostic_columns,
8487        UiNode::container(
8488            "diagnostics.commands.column",
8489            LayoutStyle::column()
8490                .with_width_percent(1.0)
8491                .with_flex_shrink(0.0)
8492                .gap(8.0),
8493        ),
8494    );
8495    let theme_column = ui.add_child(
8496        diagnostic_columns,
8497        UiNode::container(
8498            "diagnostics.theme.column",
8499            LayoutStyle::column()
8500                .with_width_percent(1.0)
8501                .with_flex_shrink(0.0)
8502                .gap(8.0),
8503        ),
8504    );
8505
8506    let registry = diagnostics_command_registry();
8507    diagnostics_commands_panel(ui, command_column, &registry);
8508
8509    let theme_snapshot = DebugThemeSnapshot::from_theme(&Theme::dark());
8510    diagnostics_theme_panel(ui, theme_column, &theme_snapshot);
8511}
8512
8513fn diagnostics_selected_node_panel(
8514    ui: &mut UiDocument,
8515    parent: UiNodeId,
8516    snapshot: &DebugInspectorSnapshot,
8517) {
8518    let panel = diagnostics_panel(ui, parent, "diagnostics.inspector", "Selected node");
8519    let rows = snapshot
8520        .node("diagnostics.sample.preview")
8521        .map(|node| {
8522            vec![
8523                ext_widgets::PropertyGridRow::new("name", "Node", "Preview action").read_only(),
8524                ext_widgets::PropertyGridRow::new("role", "Role", "Button").read_only(),
8525                ext_widgets::PropertyGridRow::new(
8526                    "bounds",
8527                    "Bounds",
8528                    format!(
8529                        "{:.0}, {:.0}; {:.0} x {:.0}",
8530                        node.rect.x, node.rect.y, node.rect.width, node.rect.height
8531                    ),
8532                )
8533                .with_kind(ext_widgets::PropertyValueKind::Number)
8534                .read_only(),
8535                ext_widgets::PropertyGridRow::new(
8536                    "clip",
8537                    "Clip",
8538                    format!("{:.0} x {:.0}", node.clip_rect.width, node.clip_rect.height),
8539                )
8540                .with_kind(ext_widgets::PropertyValueKind::Number)
8541                .read_only(),
8542                ext_widgets::PropertyGridRow::new(
8543                    "input",
8544                    "Input",
8545                    if node.input.pointer {
8546                        "Receives pointer input"
8547                    } else {
8548                        "Passive"
8549                    },
8550                )
8551                .read_only(),
8552            ]
8553        })
8554        .unwrap_or_else(|| {
8555            vec![
8556                ext_widgets::PropertyGridRow::new("missing", "Selected node", "No node selected")
8557                    .read_only(),
8558            ]
8559        });
8560    ext_widgets::property_inspector_grid(
8561        ui,
8562        panel,
8563        "diagnostics.inspector.rows",
8564        &rows,
8565        diagnostics_grid_options("Selected node details"),
8566    );
8567}
8568
8569fn diagnostics_animation_panel(
8570    ui: &mut UiDocument,
8571    parent: UiNodeId,
8572    state: &ShowcaseState,
8573    snapshot: &DebugInspectorSnapshot,
8574) {
8575    let graph_panel =
8576        diagnostics_panel(ui, parent, "diagnostics.animation.graph", "Animation state");
8577    if let Some(animation) = snapshot.animation("diagnostics.sample.preview") {
8578        let state_row = row(ui, graph_panel, "diagnostics.animation.graph.states", 8.0);
8579        for state_name in ["idle", "hot"] {
8580            diagnostic_chip(
8581                ui,
8582                state_row,
8583                format!("diagnostics.animation.graph.state.{state_name}"),
8584                state_name,
8585                animation.current_state == state_name,
8586            );
8587        }
8588
8589        let graph = animation.state_graph();
8590        for (index, edge) in graph.edges.iter().take(2).enumerate() {
8591            let value = if edge.kind == DebugAnimationGraphEdgeKind::Blend {
8592                "Input blend"
8593            } else {
8594                "State change"
8595            };
8596            let detail = if edge.label.is_empty() {
8597                if edge.active { "Active" } else { "Inactive" }.to_owned()
8598            } else if edge.active {
8599                format!("{}; active", edge.label)
8600            } else {
8601                edge.label.clone()
8602            };
8603            diagnostic_value_row(
8604                ui,
8605                graph_panel,
8606                format!("diagnostics.animation.graph.edge.{index}"),
8607                value,
8608                format!("{} -> {}", edge.from, edge.to),
8609            );
8610            diagnostic_muted_label(
8611                ui,
8612                graph_panel,
8613                format!("diagnostics.animation.graph.edge.{index}.detail"),
8614                detail,
8615            );
8616        }
8617    } else {
8618        diagnostic_muted_label(
8619            ui,
8620            graph_panel,
8621            "diagnostics.animation.graph.empty",
8622            "No animation state machine",
8623        );
8624    }
8625
8626    let controls_panel = diagnostics_panel(
8627        ui,
8628        parent,
8629        "diagnostics.animation.controls",
8630        "Animation controls",
8631    );
8632    let transport = row(
8633        ui,
8634        controls_panel,
8635        "diagnostics.animation.controls.transport",
8636        8.0,
8637    );
8638    diagnostic_button(
8639        ui,
8640        transport,
8641        "diagnostics.animation.controls.transport.pause_toggle",
8642        if state.diagnostics_animation_paused {
8643            "Resume"
8644        } else {
8645            "Pause"
8646        },
8647        state.diagnostics_animation_paused,
8648    );
8649    diagnostic_button(
8650        ui,
8651        transport,
8652        "diagnostics.animation.controls.transport.step",
8653        "Step",
8654        false,
8655    );
8656    diagnostic_slider_row(
8657        ui,
8658        controls_panel,
8659        "diagnostics.animation.controls.transport.scrub",
8660        "Scrub progress",
8661        state.diagnostics_animation_scrub,
8662        "diagnostics.animation.controls.transport.scrub",
8663    );
8664    diagnostic_button(
8665        ui,
8666        controls_panel,
8667        "diagnostics.animation.controls.input.active.toggle",
8668        if state.diagnostics_animation_active {
8669            "Active input: true"
8670        } else {
8671            "Active input: false"
8672        },
8673        state.diagnostics_animation_active,
8674    );
8675    diagnostic_slider_row(
8676        ui,
8677        controls_panel,
8678        "diagnostics.animation.controls.input.hover.set",
8679        "Hover blend",
8680        state.diagnostics_animation_hover,
8681        "diagnostics.animation.controls.input.hover.set",
8682    );
8683    diagnostic_button(
8684        ui,
8685        controls_panel,
8686        "diagnostics.animation.controls.input.pulse.fire",
8687        "Fire pulse",
8688        false,
8689    );
8690    widgets::label(
8691        ui,
8692        controls_panel,
8693        "diagnostics.animation.controls.status",
8694        format!(
8695            "Scrub {:.0}%   Hover {:.0}%   Pulses {}",
8696            state.diagnostics_animation_scrub * 100.0,
8697            state.diagnostics_animation_hover * 100.0,
8698            state.diagnostics_animation_pulse_count
8699        ),
8700        text(12.0, color(166, 180, 198)),
8701        LayoutStyle::new().with_width_percent(1.0),
8702    );
8703}
8704
8705fn diagnostics_accessibility_details(
8706    ui: &mut UiDocument,
8707    parent: UiNodeId,
8708    snapshot: &DebugInspectorSnapshot,
8709) {
8710    let rows = snapshot
8711        .accessibility_overlay
8712        .iter()
8713        .find(|node| node.name == "diagnostics.sample.preview")
8714        .map(|node| {
8715            let accessibility = node.accessibility.as_ref();
8716            vec![
8717                ext_widgets::PropertyGridRow::new("role", "Role", "Button").read_only(),
8718                ext_widgets::PropertyGridRow::new(
8719                    "label",
8720                    "Label",
8721                    accessibility
8722                        .and_then(|meta| meta.label.clone())
8723                        .unwrap_or_else(|| "Preview action".to_owned()),
8724                )
8725                .read_only(),
8726                ext_widgets::PropertyGridRow::new(
8727                    "focus",
8728                    "Focus order",
8729                    node.focus_index
8730                        .map(|index| format!("#{}", index + 1))
8731                        .unwrap_or_else(|| "Not focusable".to_owned()),
8732                )
8733                .read_only(),
8734                ext_widgets::PropertyGridRow::new(
8735                    "warnings",
8736                    "Warnings",
8737                    if node.warnings.is_empty() {
8738                        "None"
8739                    } else {
8740                        "Needs review"
8741                    },
8742                )
8743                .read_only(),
8744            ]
8745        })
8746        .unwrap_or_else(|| {
8747            vec![
8748                ext_widgets::PropertyGridRow::new("missing", "Accessibility", "No metadata")
8749                    .read_only(),
8750            ]
8751        });
8752    ext_widgets::property_inspector_grid(
8753        ui,
8754        parent,
8755        "diagnostics.a11y",
8756        &rows,
8757        diagnostics_grid_options("Accessibility metadata"),
8758    );
8759}
8760
8761fn diagnostics_commands_panel(ui: &mut UiDocument, parent: UiNodeId, registry: &CommandRegistry) {
8762    let panel = diagnostics_panel(ui, parent, "diagnostics.commands", "Commands");
8763    let formatter = ShortcutFormatter::default();
8764    for command_id in [
8765        "diagnostics.palette",
8766        "diagnostics.inspect",
8767        "diagnostics.record",
8768        "diagnostics.export_theme",
8769    ] {
8770        if let Some(command) = registry.command(command_id) {
8771            let shortcut = registry
8772                .command_bindings(command.meta.id.clone())
8773                .first()
8774                .map(|binding| formatter.format(binding.shortcut))
8775                .unwrap_or_else(|| "Unbound".to_owned());
8776            let status = if command.enabled {
8777                command
8778                    .meta
8779                    .category
8780                    .clone()
8781                    .unwrap_or_else(|| "General".to_owned())
8782            } else {
8783                command
8784                    .disabled_reason
8785                    .clone()
8786                    .unwrap_or_else(|| "Disabled".to_owned())
8787            };
8788            diagnostic_command_row(
8789                ui,
8790                panel,
8791                format!(
8792                    "diagnostics.commands.row.{}",
8793                    command.meta.id.as_str().replace('.', "_")
8794                ),
8795                &command.meta.label,
8796                &shortcut,
8797                &status,
8798            );
8799        }
8800    }
8801    diagnostic_value_row(
8802        ui,
8803        panel,
8804        "diagnostics.commands.conflicts",
8805        "Shortcut conflicts",
8806        if registry.conflicts().is_empty() {
8807            "None"
8808        } else {
8809            "Needs review"
8810        },
8811    );
8812}
8813
8814fn diagnostics_theme_panel(ui: &mut UiDocument, parent: UiNodeId, snapshot: &DebugThemeSnapshot) {
8815    let panel = diagnostics_panel(ui, parent, "diagnostics.theme", "Theme tokens");
8816    diagnostic_value_row(
8817        ui,
8818        panel,
8819        "diagnostics.theme.name",
8820        "Theme",
8821        snapshot.name.as_str(),
8822    );
8823    for token_path in ["colors.accent", "colors.surface", "typography.body"] {
8824        if let Some(token) = snapshot.token(token_path) {
8825            diagnostic_value_row(
8826                ui,
8827                panel,
8828                format!("diagnostics.theme.token.{}", token_path.replace('.', "_")),
8829                token_path,
8830                token.value.as_str(),
8831            );
8832        }
8833    }
8834    if let Some(component) = snapshot.component_states.first() {
8835        diagnostic_value_row(
8836            ui,
8837            panel,
8838            "diagnostics.theme.component.button",
8839            "Button normal",
8840            format!(
8841                "{:.0} x {:.0}, padding {:.0}",
8842                component.min_width, component.min_height, component.padding_x
8843            ),
8844        );
8845    }
8846}
8847
8848fn diagnostics_panel(
8849    ui: &mut UiDocument,
8850    parent: UiNodeId,
8851    name: impl Into<String>,
8852    title: impl Into<String>,
8853) -> UiNodeId {
8854    let name = name.into();
8855    let title = title.into();
8856    let panel = ui.add_child(
8857        parent,
8858        UiNode::container(
8859            name.clone(),
8860            LayoutStyle::column()
8861                .with_width_percent(1.0)
8862                .with_padding(10.0)
8863                .with_gap(8.0)
8864                .with_flex_shrink(0.0),
8865        )
8866        .with_visual(UiVisual::panel(
8867            color(15, 20, 28),
8868            Some(StrokeStyle::new(color(52, 65, 84), 1.0)),
8869            4.0,
8870        ))
8871        .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(title.clone())),
8872    );
8873    widgets::label(
8874        ui,
8875        panel,
8876        format!("{name}.title"),
8877        title,
8878        text(13.0, color(222, 230, 240)),
8879        LayoutStyle::new().with_width_percent(1.0),
8880    );
8881    panel
8882}
8883
8884fn diagnostics_grid_options(label: impl Into<String>) -> ext_widgets::PropertyInspectorOptions {
8885    ext_widgets::PropertyInspectorOptions {
8886        label_width: 112.0,
8887        row_height: 28.0,
8888        accessibility_label: Some(label.into()),
8889        ..Default::default()
8890    }
8891}
8892
8893fn diagnostic_value_row(
8894    ui: &mut UiDocument,
8895    parent: UiNodeId,
8896    name: impl Into<String>,
8897    label: impl Into<String>,
8898    value: impl Into<String>,
8899) -> UiNodeId {
8900    let name = name.into();
8901    let row = row(ui, parent, name.clone(), 8.0);
8902    widgets::label(
8903        ui,
8904        row,
8905        format!("{name}.label"),
8906        label.into(),
8907        text(12.0, color(166, 180, 198)),
8908        LayoutStyle::new().with_width(136.0).with_flex_shrink(0.0),
8909    );
8910    widgets::label(
8911        ui,
8912        row,
8913        format!("{name}.value"),
8914        value.into(),
8915        text(12.0, color(226, 234, 244)),
8916        LayoutStyle::new().with_width_percent(1.0),
8917    );
8918    row
8919}
8920
8921fn diagnostic_muted_label(
8922    ui: &mut UiDocument,
8923    parent: UiNodeId,
8924    name: impl Into<String>,
8925    label: impl Into<String>,
8926) -> UiNodeId {
8927    let mut style = text(12.0, color(166, 180, 198));
8928    style.wrap = TextWrap::WordOrGlyph;
8929    widgets::label(
8930        ui,
8931        parent,
8932        name,
8933        label.into(),
8934        style,
8935        LayoutStyle::new().with_width_percent(1.0),
8936    )
8937}
8938
8939fn diagnostic_command_row(
8940    ui: &mut UiDocument,
8941    parent: UiNodeId,
8942    name: impl Into<String>,
8943    label: &str,
8944    shortcut: &str,
8945    status: &str,
8946) -> UiNodeId {
8947    let name = name.into();
8948    let row = row(ui, parent, name.clone(), 8.0);
8949    widgets::label(
8950        ui,
8951        row,
8952        format!("{name}.label"),
8953        label,
8954        text(12.0, color(226, 234, 244)),
8955        LayoutStyle::new()
8956            .with_width_percent(1.0)
8957            .with_flex_grow(1.0),
8958    );
8959    widgets::label(
8960        ui,
8961        row,
8962        format!("{name}.shortcut"),
8963        shortcut,
8964        text(12.0, color(166, 180, 198)),
8965        LayoutStyle::new().with_width(78.0).with_flex_shrink(0.0),
8966    );
8967    widgets::label(
8968        ui,
8969        row,
8970        format!("{name}.status"),
8971        status,
8972        text(12.0, color(166, 180, 198)),
8973        LayoutStyle::new().with_width(140.0).with_flex_shrink(0.0),
8974    );
8975    row
8976}
8977
8978fn diagnostic_slider_row(
8979    ui: &mut UiDocument,
8980    parent: UiNodeId,
8981    name: impl Into<String>,
8982    label: impl Into<String>,
8983    value: f32,
8984    action: impl Into<String>,
8985) -> UiNodeId {
8986    let name = name.into();
8987    let label = label.into();
8988    let row = row(ui, parent, format!("{name}.row"), 8.0);
8989    widgets::label(
8990        ui,
8991        row,
8992        format!("{name}.label"),
8993        label.clone(),
8994        text(12.0, color(166, 180, 198)),
8995        LayoutStyle::new().with_width(136.0).with_flex_shrink(0.0),
8996    );
8997    let slider_name = if name.ends_with(".set") {
8998        format!("{name}.slider")
8999    } else {
9000        name.clone()
9001    };
9002    let mut options = widgets::SliderOptions::default()
9003        .with_layout(LayoutStyle::new().with_width(160.0).with_height(24.0))
9004        .with_value_edit_action(action.into());
9005    options.accessibility_label = Some(label);
9006    widgets::slider(ui, row, slider_name, value, 0.0..1.0, options);
9007    widgets::label(
9008        ui,
9009        row,
9010        format!("{name}.percent"),
9011        format!("{:.0}%", value * 100.0),
9012        text(12.0, color(226, 234, 244)),
9013        LayoutStyle::new().with_width(46.0).with_flex_shrink(0.0),
9014    );
9015    row
9016}
9017
9018fn diagnostic_button(
9019    ui: &mut UiDocument,
9020    parent: UiNodeId,
9021    name: impl Into<String>,
9022    label: impl Into<String>,
9023    active: bool,
9024) -> UiNodeId {
9025    let name = name.into();
9026    let mut options = widgets::ButtonOptions::default()
9027        .with_layout(LayoutStyle::new().with_height(32.0))
9028        .with_action(name.clone())
9029        .pressed(active);
9030    if active {
9031        options.visual = UiVisual::panel(
9032            color(47, 94, 150),
9033            Some(StrokeStyle::new(color(103, 164, 224), 1.0)),
9034            4.0,
9035        );
9036    }
9037    widgets::button(ui, parent, name, label, options)
9038}
9039
9040fn diagnostic_chip(
9041    ui: &mut UiDocument,
9042    parent: UiNodeId,
9043    name: impl Into<String>,
9044    label: impl Into<String>,
9045    active: bool,
9046) -> UiNodeId {
9047    let name = name.into();
9048    let chip = ui.add_child(
9049        parent,
9050        UiNode::container(
9051            name.clone(),
9052            LayoutStyle::new()
9053                .with_width(82.0)
9054                .with_height(28.0)
9055                .with_padding(4.0)
9056                .with_flex_shrink(0.0),
9057        )
9058        .with_visual(if active {
9059            UiVisual::panel(
9060                color(47, 94, 150),
9061                Some(StrokeStyle::new(color(103, 164, 224), 1.0)),
9062                4.0,
9063            )
9064        } else {
9065            UiVisual::panel(
9066                color(31, 39, 50),
9067                Some(StrokeStyle::new(color(62, 76, 96), 1.0)),
9068                4.0,
9069            )
9070        }),
9071    );
9072    widgets::label(
9073        ui,
9074        chip,
9075        format!("{name}.label"),
9076        label.into(),
9077        text(12.0, color(226, 234, 244)),
9078        LayoutStyle::new().with_width_percent(1.0),
9079    );
9080    chip
9081}
9082
9083fn diagnostics_sample_snapshot(state: &ShowcaseState) -> DebugInspectorSnapshot {
9084    diagnostics_sample_snapshot_for(
9085        state.diagnostics_animation_hover,
9086        state.diagnostics_animation_active,
9087    )
9088}
9089
9090fn diagnostics_sample_snapshot_for(hover: f32, active: bool) -> DebugInspectorSnapshot {
9091    let mut sample = UiDocument::new(root_style(320.0, 180.0));
9092    let card = sample.add_child(
9093        sample.root(),
9094        UiNode::container(
9095            "diagnostics.sample.card",
9096            LayoutStyle::column()
9097                .with_width_percent(1.0)
9098                .with_height(120.0)
9099                .padding(12.0)
9100                .gap(8.0),
9101        )
9102        .with_visual(UiVisual::panel(
9103            color(16, 22, 30),
9104            Some(StrokeStyle::new(color(62, 77, 98), 1.0)),
9105            6.0,
9106        ))
9107        .with_accessibility(
9108            AccessibilityMeta::new(AccessibilityRole::Group).label("Diagnostics sample"),
9109        ),
9110    );
9111    sample.add_child(
9112        card,
9113        UiNode::container(
9114            "diagnostics.sample.preview",
9115            LayoutStyle::new().with_width(160.0).with_height(38.0),
9116        )
9117        .with_input(InputBehavior::BUTTON)
9118        .with_visual(UiVisual::panel(
9119            color(52, 112, 180),
9120            Some(StrokeStyle::new(color(116, 183, 255), 1.0)),
9121            5.0,
9122        ))
9123        .with_accessibility(
9124            AccessibilityMeta::new(AccessibilityRole::Button)
9125                .label("Preview action")
9126                .focusable(),
9127        )
9128        .with_animation(
9129            AnimationMachine::new(
9130                vec![
9131                    AnimationState::new(
9132                        "idle",
9133                        AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0),
9134                    ),
9135                    AnimationState::new(
9136                        "hot",
9137                        AnimatedValues::new(0.92, UiPoint::new(18.0, 0.0), 1.08),
9138                    ),
9139                ],
9140                vec![AnimationTransition::when(
9141                    "idle",
9142                    "hot",
9143                    AnimationCondition::bool("active", true),
9144                    0.18,
9145                )],
9146                "idle",
9147            )
9148            .expect("sample animation")
9149            .with_number_input("hover", hover)
9150            .with_blend_binding(AnimationBlendBinding::new("hover", "idle", "hot"))
9151            .with_bool_input("active", active)
9152            .with_trigger_input("pulse"),
9153        ),
9154    );
9155    widgets::label(
9156        &mut sample,
9157        card,
9158        "diagnostics.sample.label",
9159        "Sample node",
9160        text(12.0, color(198, 210, 226)),
9161        LayoutStyle::new().with_width_percent(1.0),
9162    );
9163    sample
9164        .compute_layout(UiSize::new(320.0, 180.0), &mut ApproxTextMeasurer)
9165        .expect("sample layout");
9166    DebugInspectorSnapshot::from_document(&sample, &mut ApproxTextMeasurer)
9167}
9168
9169fn diagnostics_command_registry() -> CommandRegistry {
9170    let mut registry = CommandRegistry::new();
9171    registry
9172        .register(
9173            CommandMeta::new("diagnostics.palette", "Open command palette")
9174                .description("Show command search")
9175                .category("Debug"),
9176        )
9177        .expect("command");
9178    registry
9179        .register(
9180            CommandMeta::new("diagnostics.inspect", "Inspect selected node")
9181                .description("Focus the layout inspector")
9182                .category("Debug"),
9183        )
9184        .expect("command");
9185    registry
9186        .register(
9187            CommandMeta::new("diagnostics.record", "Start interaction recording")
9188                .description("Capture replay steps")
9189                .category("Testing"),
9190        )
9191        .expect("command");
9192    registry
9193        .register(CommandMeta::new(
9194            "diagnostics.export_theme",
9195            "Export theme patch",
9196        ))
9197        .expect("command");
9198    registry
9199        .bind_shortcut(
9200            CommandScope::Global,
9201            Shortcut::ctrl('k'),
9202            "diagnostics.palette",
9203        )
9204        .expect("shortcut");
9205    registry
9206        .bind_shortcut(
9207            CommandScope::Panel,
9208            Shortcut::ctrl('i'),
9209            "diagnostics.inspect",
9210        )
9211        .expect("shortcut");
9212    registry
9213        .bind_shortcut(
9214            CommandScope::Panel,
9215            Shortcut::ctrl('r'),
9216            "diagnostics.record",
9217        )
9218        .expect("shortcut");
9219    registry
9220        .disable("diagnostics.export_theme", "No changes to export")
9221        .expect("disable");
9222    registry
9223}
9224
9225fn tree_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9226    let body = section(ui, parent, "trees", "Tree view");
9227    widgets::label(
9228        ui,
9229        body,
9230        "trees.tree_view.title",
9231        "Editable tree",
9232        text(12.0, color(166, 176, 190)),
9233        LayoutStyle::new().with_width_percent(1.0),
9234    );
9235    ext_widgets::tree_view(
9236        ui,
9237        body,
9238        "trees.tree_view",
9239        &editable_tree_items(&state.editable_tree),
9240        &state.tree,
9241        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.tree"),
9242    );
9243    widgets::label(
9244        ui,
9245        body,
9246        "trees.editable.status",
9247        &state.editable_tree_status,
9248        text(12.0, color(154, 166, 184)),
9249        LayoutStyle::new().with_width_percent(1.0),
9250    );
9251    widgets::label(
9252        ui,
9253        body,
9254        "trees.virtual.title",
9255        "Virtualized tree",
9256        text(12.0, color(166, 176, 190)),
9257        LayoutStyle::new().with_width_percent(1.0),
9258    );
9259    let virtual_nodes = ext_widgets::virtualized_tree_view(
9260        ui,
9261        body,
9262        "trees.virtual",
9263        &virtual_tree_items(),
9264        &state.tree_virtual,
9265        ext_widgets::VirtualTreeViewSpec::new(24.0, 112.0)
9266            .scroll_offset(state.tree_virtual_scroll)
9267            .overscan_rows(1),
9268        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.virtual"),
9269    );
9270    ui.node_mut(virtual_nodes.body)
9271        .set_action("trees.virtual.scroll");
9272    widgets::label(
9273        ui,
9274        body,
9275        "trees.table.title",
9276        "Tree table",
9277        text(12.0, color(166, 176, 190)),
9278        LayoutStyle::new().with_width_percent(1.0),
9279    );
9280    tree_table_widgets(ui, body, state);
9281}
9282
9283fn tree_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9284    let rows = state.tree_table.visible_items(&tree_table_items());
9285    let columns = [
9286        ext_widgets::DataTableColumn::new("name", "Name", 220.0),
9287        ext_widgets::DataTableColumn::new("kind", "Kind", 84.0),
9288        ext_widgets::DataTableColumn::new("status", "Status", 92.0),
9289    ];
9290    let mut options = ext_widgets::DataTableOptions::default()
9291        .with_row_action_prefix("trees.table")
9292        .with_cell_action_prefix("trees.table")
9293        .with_scroll_action("trees.table.scroll");
9294    options.selection = state
9295        .tree_table
9296        .selected_index()
9297        .map(ext_widgets::DataTableSelection::single_row)
9298        .unwrap_or_default();
9299    options.layout = LayoutStyle::column()
9300        .with_width_percent(1.0)
9301        .with_height(132.0)
9302        .with_flex_shrink(0.0);
9303    ext_widgets::virtualized_data_table(
9304        ui,
9305        parent,
9306        "trees.table",
9307        &columns,
9308        ext_widgets::VirtualDataTableSpec {
9309            row_count: rows.len(),
9310            row_height: 24.0,
9311            viewport_width: 396.0,
9312            viewport_height: 96.0,
9313            scroll_offset: UiPoint::new(0.0, state.tree_table_scroll),
9314            overscan_rows: 1,
9315        },
9316        options,
9317        |ui, cell_parent, cell| {
9318            let Some(item) = rows.get(cell.row) else {
9319                return;
9320            };
9321            if cell.column == 0 {
9322                tree_table_name_cell(ui, cell_parent, cell.row, item);
9323            } else {
9324                widgets::label(
9325                    ui,
9326                    cell_parent,
9327                    format!("trees.table.cell.{}.{}.label", cell.row, cell.column),
9328                    tree_table_cell_value(item, cell.column),
9329                    text(12.0, color(220, 228, 238)),
9330                    LayoutStyle::new().with_width_percent(1.0),
9331                );
9332            }
9333        },
9334    );
9335}
9336
9337fn tree_table_name_cell(
9338    ui: &mut UiDocument,
9339    parent: UiNodeId,
9340    row: usize,
9341    item: &ext_widgets::TreeVisibleItem,
9342) {
9343    if item.depth > 0 {
9344        ui.add_child(
9345            parent,
9346            UiNode::container(
9347                format!("trees.table.row.{}.indent", item.id),
9348                LayoutStyle::new()
9349                    .with_width(item.depth as f32 * 16.0)
9350                    .with_height_percent(1.0)
9351                    .with_flex_shrink(0.0),
9352            ),
9353        );
9354    }
9355    widgets::label(
9356        ui,
9357        parent,
9358        format!("trees.table.row.{}.disclosure", item.id),
9359        if item.has_children() {
9360            if item.expanded {
9361                "v"
9362            } else {
9363                ">"
9364            }
9365        } else {
9366            ""
9367        },
9368        text(12.0, color(166, 176, 190)),
9369        LayoutStyle::new()
9370            .with_width(18.0)
9371            .with_height_percent(1.0)
9372            .with_flex_shrink(0.0),
9373    );
9374    widgets::label(
9375        ui,
9376        parent,
9377        format!("trees.table.cell.{row}.0.label"),
9378        item.label.clone(),
9379        if item.disabled {
9380            text(12.0, color(154, 166, 184))
9381        } else {
9382            text(12.0, color(220, 228, 238))
9383        },
9384        LayoutStyle::new().with_width_percent(1.0),
9385    );
9386}
9387
9388fn tree_table_cell_value(item: &ext_widgets::TreeVisibleItem, column: usize) -> String {
9389    match column {
9390        0 => item.label.clone(),
9391        1 => {
9392            if item.has_children() {
9393                "Folder".to_owned()
9394            } else {
9395                "File".to_owned()
9396            }
9397        }
9398        _ => {
9399            if item.disabled {
9400                "Locked".to_owned()
9401            } else if item.has_children() && item.expanded {
9402                "Expanded".to_owned()
9403            } else if item.has_children() {
9404                "Collapsed".to_owned()
9405            } else if item.expanded {
9406                "Expanded".to_owned()
9407            } else {
9408                "Ready".to_owned()
9409            }
9410        }
9411    }
9412}
9413
9414fn tab_split_dock_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9415    let body = section_with_min_viewport(
9416        ui,
9417        parent,
9418        "layout_widgets",
9419        "Layout widgets",
9420        UiSize::new(640.0, 360.0),
9421    );
9422    let shell = ui.add_child(
9423        body,
9424        UiNode::container(
9425            "layout_widgets.dock_shell",
9426            LayoutStyle::column()
9427                .with_width_percent(1.0)
9428                .with_height(360.0)
9429                .with_flex_shrink(0.0),
9430        )
9431        .with_visual(UiVisual::panel(
9432            color(13, 17, 23),
9433            Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9434            0.0,
9435        )),
9436    );
9437
9438    let mut panels = base_layout_dock_panels();
9439    state.layout_dock.apply_order_to_panels(&mut panels);
9440    state.layout_dock.apply_visibility_to_panels(&mut panels);
9441
9442    let mut drawer_options = ext_widgets::DockDrawerRailOptions::default();
9443    drawer_options.layout = LayoutStyle::row()
9444        .with_width_percent(1.0)
9445        .with_height(34.0)
9446        .with_padding(4.0)
9447        .with_gap(4.0);
9448    ext_widgets::dock_drawer_rail(
9449        ui,
9450        shell,
9451        "layout_widgets.dock.drawers",
9452        &[
9453            ext_widgets::DockDrawerDescriptor::new(
9454                "panel_a",
9455                "Panel A",
9456                "panel_a",
9457                ext_widgets::DockSide::Left,
9458            )
9459            .open(!state.layout_dock.is_hidden("panel_a"))
9460            .with_action("layout_widgets.drawer.panel_a"),
9461            ext_widgets::DockDrawerDescriptor::new(
9462                "panel_b",
9463                "Panel B",
9464                "panel_b",
9465                ext_widgets::DockSide::Right,
9466            )
9467            .open(!state.layout_dock.is_hidden("panel_b"))
9468            .with_action("layout_widgets.drawer.panel_b"),
9469        ],
9470        drawer_options,
9471    );
9472
9473    let mut options = ext_widgets::DockWorkspaceOptions::default();
9474    options.layout = LayoutStyle::column()
9475        .with_width_percent(1.0)
9476        .with_height(0.0)
9477        .with_flex_grow(1.0);
9478    options.show_titles = true;
9479    options.handle_thickness = 2.0;
9480    options.panel_visual = UiVisual::panel(
9481        color(18, 22, 29),
9482        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9483        0.0,
9484    );
9485    options.center_visual = UiVisual::panel(
9486        color(15, 19, 25),
9487        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9488        0.0,
9489    );
9490    options.resize_handle_visual = UiVisual::panel(color(65, 78, 96), None, 0.0);
9491
9492    ext_widgets::dock_workspace(
9493        ui,
9494        shell,
9495        "layout_widgets.dock",
9496        &panels,
9497        options,
9498        |ui, parent, panel| match panel.id.as_str() {
9499            "panel_a" => layout_panel_contents(
9500                ui,
9501                parent,
9502                "layout.panel_a",
9503                state.layout_panel_a_scroll,
9504                &["Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6"],
9505            ),
9506            "workspace" => layout_workspace_contents(
9507                ui,
9508                parent,
9509                "layout.workspace",
9510                state.layout_workspace_scroll,
9511            ),
9512            "panel_b" => layout_panel_contents(
9513                ui,
9514                parent,
9515                "layout.panel_b",
9516                state.layout_panel_b_scroll,
9517                &[
9518                    "Value A", "Value B", "Value C", "Value D", "Value E", "Value F",
9519                ],
9520            ),
9521            _ => {}
9522        },
9523    );
9524
9525    if let Some(floating) = state.layout_dock.floating_panel("panel_a") {
9526        let floating_panel = ui.add_child(
9527            shell,
9528            UiNode::container(
9529                "layout_widgets.floating.panel_a",
9530                operad::layout::absolute(
9531                    floating.rect.x,
9532                    floating.rect.y,
9533                    floating.rect.width,
9534                    floating.rect.height,
9535                ),
9536            )
9537            .with_visual(UiVisual::panel(
9538                color(18, 22, 29),
9539                Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
9540                4.0,
9541            )),
9542        );
9543        layout_panel_contents(
9544            ui,
9545            floating_panel,
9546            "layout.panel_a_floating",
9547            state.layout_panel_a_scroll,
9548            &["Item 1", "Item 2", "Item 3", "Item 4"],
9549        );
9550    }
9551}
9552
9553fn base_layout_dock_panels() -> Vec<ext_widgets::DockPanelDescriptor> {
9554    vec![
9555        ext_widgets::DockPanelDescriptor::new(
9556            "panel_a",
9557            "Panel A",
9558            ext_widgets::DockSide::Left,
9559            200.0,
9560        )
9561        .with_min_size(150.0)
9562        .resizable(true),
9563        ext_widgets::DockPanelDescriptor::center("workspace", "Workspace").with_min_size(220.0),
9564        ext_widgets::DockPanelDescriptor::new(
9565            "panel_b",
9566            "Panel B",
9567            ext_widgets::DockSide::Right,
9568            200.0,
9569        )
9570        .with_min_size(150.0)
9571        .resizable(true),
9572    ]
9573}
9574
9575fn container_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9576    let body = section_with_min_viewport(
9577        ui,
9578        parent,
9579        "containers",
9580        "Containers",
9581        UiSize::new(420.0, 0.0),
9582    );
9583
9584    let frame = widgets::frame(
9585        ui,
9586        body,
9587        "containers.frame",
9588        widgets::FrameOptions::default().with_layout(
9589            LayoutStyle::column()
9590                .with_width_percent(1.0)
9591                .with_height(64.0)
9592                .with_padding(8.0)
9593                .with_gap(6.0),
9594        ),
9595    );
9596    widgets::strong_label(
9597        ui,
9598        frame,
9599        "containers.frame.title",
9600        "Frame",
9601        LayoutStyle::new().with_width_percent(1.0),
9602    );
9603    widgets::weak_label(
9604        ui,
9605        frame,
9606        "containers.frame.body",
9607        "Framed surface with padding.",
9608        LayoutStyle::new().with_width_percent(1.0),
9609    );
9610
9611    let group = widgets::group(ui, body, "containers.group");
9612    widgets::label(
9613        ui,
9614        group,
9615        "containers.group.label",
9616        "Group helper",
9617        text(12.0, color(220, 228, 238)),
9618        LayoutStyle::new().with_width_percent(1.0),
9619    );
9620    let generic_panel = widgets::panel(
9621        ui,
9622        body,
9623        "containers.panel",
9624        widgets::PanelOptions::group().with_layout(
9625            LayoutStyle::column()
9626                .with_width_percent(1.0)
9627                .with_height(44.0)
9628                .with_padding(8.0),
9629        ),
9630    );
9631    widgets::label(
9632        ui,
9633        generic_panel,
9634        "containers.panel.label",
9635        "Generic panel",
9636        text(12.0, color(220, 228, 238)),
9637        LayoutStyle::new().with_width_percent(1.0),
9638    );
9639    let group_panel = widgets::group_panel(ui, body, "containers.group_panel");
9640    widgets::label(
9641        ui,
9642        group_panel,
9643        "containers.group_panel.label",
9644        "Group panel",
9645        text(12.0, color(220, 228, 238)),
9646        LayoutStyle::new().with_width_percent(1.0),
9647    );
9648
9649    widgets::separator(
9650        ui,
9651        body,
9652        "containers.separator",
9653        widgets::SeparatorOptions::default(),
9654    );
9655    widgets::spacer(
9656        ui,
9657        body,
9658        "containers.spacer",
9659        LayoutStyle::new()
9660            .with_width_percent(1.0)
9661            .with_height(8.0)
9662            .with_flex_shrink(0.0),
9663    );
9664
9665    let grid = widgets::grid::grid(
9666        ui,
9667        body,
9668        "containers.grid",
9669        widgets::grid::GridOptions::default().with_layout(
9670            LayoutStyle::column()
9671                .with_width_percent(1.0)
9672                .with_height(78.0)
9673                .with_gap(4.0),
9674        ),
9675    );
9676    for row_index in 0..2 {
9677        let row = widgets::grid::grid_row(
9678            ui,
9679            grid,
9680            format!("containers.grid.row.{row_index}"),
9681            widgets::grid::GridRowOptions::default(),
9682        );
9683        for column_index in 0..3 {
9684            widgets::grid::grid_text_cell(
9685                ui,
9686                row,
9687                format!("containers.grid.row.{row_index}.cell.{column_index}"),
9688                format!("R{} C{}", row_index + 1, column_index + 1),
9689                widgets::grid::GridCellOptions {
9690                    text_style: text(12.0, color(214, 224, 238)),
9691                    ..Default::default()
9692                },
9693            );
9694        }
9695    }
9696
9697    widgets::sides(
9698        ui,
9699        body,
9700        "containers.sides",
9701        widgets::SidesOptions::default()
9702            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
9703            .with_gap(8.0)
9704            .with_visual(UiVisual::panel(
9705                color(20, 25, 32),
9706                Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
9707                4.0,
9708            )),
9709        |ui, left| {
9710            widgets::label(
9711                ui,
9712                left,
9713                "containers.sides.left.label",
9714                "Left side",
9715                text(12.0, color(220, 228, 238)),
9716                LayoutStyle::new().with_width_percent(1.0),
9717            );
9718        },
9719        |ui, right| {
9720            widgets::label(
9721                ui,
9722                right,
9723                "containers.sides.right.label",
9724                "Right side",
9725                text(12.0, color(220, 228, 238)),
9726                LayoutStyle::new().with_width_percent(1.0),
9727            );
9728        },
9729    );
9730
9731    widgets::columns(
9732        ui,
9733        body,
9734        "containers.columns",
9735        3,
9736        widgets::ColumnsOptions::default()
9737            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
9738            .with_gap(8.0),
9739        |ui, column, index| {
9740            widgets::label(
9741                ui,
9742                column,
9743                format!("containers.columns.{index}.label"),
9744                format!("Column {}", index + 1),
9745                text(12.0, color(220, 228, 238)),
9746                LayoutStyle::new().with_width_percent(1.0),
9747            );
9748        },
9749    );
9750
9751    let indented = widgets::indented_section(
9752        ui,
9753        body,
9754        "containers.indented",
9755        widgets::IndentOptions::default().with_amount(24.0),
9756    );
9757    widgets::label(
9758        ui,
9759        indented,
9760        "containers.indented.label",
9761        "Indented section",
9762        text(12.0, color(196, 210, 230)),
9763        LayoutStyle::new().with_width_percent(1.0),
9764    );
9765
9766    widgets::resize_container(
9767        ui,
9768        body,
9769        "containers.resize_container",
9770        widgets::ResizeContainerOptions::default().with_layout(
9771            LayoutStyle::column()
9772                .with_width_percent(1.0)
9773                .with_height(92.0)
9774                .with_flex_shrink(0.0),
9775        ),
9776        |ui, content| {
9777            widgets::label(
9778                ui,
9779                content,
9780                "containers.resize_container.label",
9781                "Resize container",
9782                text(12.0, color(220, 228, 238)),
9783                LayoutStyle::new().with_width_percent(1.0),
9784            );
9785        },
9786    );
9787
9788    widgets::scene(
9789        ui,
9790        body,
9791        "containers.scene",
9792        vec![
9793            ScenePrimitive::Rect(
9794                PaintRect::solid(UiRect::new(8.0, 12.0, 108.0, 46.0), color(48, 112, 184))
9795                    .stroke(AlignedStroke::inside(StrokeStyle::new(
9796                        color(132, 174, 222),
9797                        1.0,
9798                    )))
9799                    .corner_radii(CornerRadii::uniform(6.0)),
9800            ),
9801            ScenePrimitive::Circle {
9802                center: UiPoint::new(150.0, 35.0),
9803                radius: 22.0,
9804                fill: color(111, 203, 159),
9805                stroke: Some(StrokeStyle::new(color(176, 236, 206), 1.0)),
9806            },
9807            ScenePrimitive::Line {
9808                from: UiPoint::new(188.0, 18.0),
9809                to: UiPoint::new(238.0, 52.0),
9810                stroke: StrokeStyle::new(color(232, 186, 88), 3.0),
9811            },
9812        ],
9813        widgets::SceneOptions::default()
9814            .with_layout(LayoutStyle::new().with_width(260.0).with_height(70.0))
9815            .accessibility_label("Scene primitives"),
9816    );
9817
9818    widgets::scroll_container(
9819        ui,
9820        body,
9821        "containers.scroll_area_with_bars",
9822        state.containers_scroll,
9823        widgets::ScrollContainerOptions::default()
9824            .with_axes(ScrollAxes::VERTICAL)
9825            .with_layout(LayoutStyle::column().with_width(260.0).with_height(116.0))
9826            .with_viewport_layout(
9827                LayoutStyle::column()
9828                    .with_width(0.0)
9829                    .with_height_percent(1.0)
9830                    .with_flex_grow(1.0)
9831                    .with_flex_shrink(1.0),
9832            ),
9833        |ui, viewport| {
9834            for index in 0..5 {
9835                widgets::label(
9836                    ui,
9837                    viewport,
9838                    format!("containers.scroll_area_with_bars.row.{index}"),
9839                    format!("Scrollable row {}", index + 1),
9840                    text(12.0, color(200, 212, 228)),
9841                    LayoutStyle::new()
9842                        .with_width(232.0)
9843                        .with_height(28.0)
9844                        .with_flex_shrink(0.0),
9845                );
9846            }
9847        },
9848    );
9849
9850    widgets::label(
9851        ui,
9852        body,
9853        "containers.area.title",
9854        "Absolute area",
9855        text(12.0, color(166, 176, 190)),
9856        LayoutStyle::new().with_width_percent(1.0),
9857    );
9858    let area_host = ui.add_child(
9859        body,
9860        UiNode::container(
9861            "containers.area.host",
9862            LayoutStyle::new()
9863                .with_width_percent(1.0)
9864                .with_height(82.0)
9865                .with_flex_shrink(0.0),
9866        )
9867        .with_visual(UiVisual::panel(
9868            color(17, 20, 25),
9869            Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
9870            4.0,
9871        )),
9872    );
9873    widgets::container::area(
9874        ui,
9875        area_host,
9876        "containers.area",
9877        widgets::container::AreaOptions::new(UiRect::new(14.0, 14.0, 180.0, 44.0))
9878            .with_visual(UiVisual::panel(color(39, 72, 109), None, 4.0))
9879            .accessibility_label("Absolute positioned area"),
9880        |ui, area| {
9881            widgets::label(
9882                ui,
9883                area,
9884                "containers.area.label",
9885                "Area",
9886                text(12.0, color(238, 244, 252)),
9887                LayoutStyle::new().with_width_percent(1.0),
9888            );
9889        },
9890    );
9891}
9892
9893fn panel_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9894    let body = section_with_min_viewport(ui, parent, "panels", "Panels", UiSize::new(520.0, 320.0));
9895    widgets::label(
9896        ui,
9897        body,
9898        "panels.title",
9899        "Drag the split bars to resize the docked panels.",
9900        text(12.0, color(166, 176, 190)),
9901        LayoutStyle::new().with_width_percent(1.0),
9902    );
9903    let shell = widgets::frame(
9904        ui,
9905        body,
9906        "panels.shell",
9907        widgets::FrameOptions::default().with_layout(
9908            LayoutStyle::column()
9909                .with_width_percent(1.0)
9910                .with_height(260.0)
9911                .with_flex_grow(1.0)
9912                .with_padding(0.0)
9913                .with_gap(0.0),
9914        ),
9915    );
9916    ext_widgets::split_pane(
9917        ui,
9918        shell,
9919        "panels.top_split",
9920        ext_widgets::SplitAxis::Vertical,
9921        state.panels_top_split,
9922        panel_split_options("panels.resize.top"),
9923        |ui, top| {
9924            panel_region(
9925                ui,
9926                top,
9927                "panels.top",
9928                widgets::PanelKind::Top,
9929                "Top",
9930                "Header controls",
9931            );
9932        },
9933        |ui, lower| {
9934            ext_widgets::split_pane(
9935                ui,
9936                lower,
9937                "panels.bottom_split",
9938                ext_widgets::SplitAxis::Vertical,
9939                state.panels_bottom_split,
9940                panel_split_options("panels.resize.bottom"),
9941                |ui, middle| {
9942                    ext_widgets::split_pane(
9943                        ui,
9944                        middle,
9945                        "panels.left_split",
9946                        ext_widgets::SplitAxis::Horizontal,
9947                        state.panels_left_split,
9948                        panel_split_options("panels.resize.left"),
9949                        |ui, left| {
9950                            panel_region(
9951                                ui,
9952                                left,
9953                                "panels.left",
9954                                widgets::PanelKind::Left,
9955                                "Left",
9956                                "Navigation",
9957                            );
9958                        },
9959                        |ui, center_and_right| {
9960                            ext_widgets::split_pane(
9961                                ui,
9962                                center_and_right,
9963                                "panels.right_split",
9964                                ext_widgets::SplitAxis::Horizontal,
9965                                state.panels_right_split,
9966                                panel_split_options("panels.resize.right"),
9967                                |ui, center| {
9968                                    panel_region(
9969                                        ui,
9970                                        center,
9971                                        "panels.center",
9972                                        widgets::PanelKind::Central,
9973                                        "Central",
9974                                        "Primary workspace",
9975                                    );
9976                                },
9977                                |ui, right| {
9978                                    panel_region(
9979                                        ui,
9980                                        right,
9981                                        "panels.right",
9982                                        widgets::PanelKind::Right,
9983                                        "Right",
9984                                        "Inspector",
9985                                    );
9986                                },
9987                            );
9988                        },
9989                    );
9990                },
9991                |ui, bottom| {
9992                    panel_region(
9993                        ui,
9994                        bottom,
9995                        "panels.bottom",
9996                        widgets::PanelKind::Bottom,
9997                        "Bottom",
9998                        "Status and output",
9999                    );
10000                },
10001            );
10002        },
10003    );
10004}
10005
10006fn panel_split_options(action: &'static str) -> ext_widgets::SplitPaneOptions {
10007    let mut options = ext_widgets::SplitPaneOptions::default().with_handle_action(action);
10008    options.handle_thickness = PANELS_SPLIT_HANDLE_THICKNESS;
10009    options.handle_visual = UiVisual::panel(color(58, 70, 88), None, 0.0);
10010    options.handle_hover_visual = Some(UiVisual::panel(color(100, 172, 244), None, 0.0));
10011    options
10012}
10013
10014fn panel_region(
10015    ui: &mut UiDocument,
10016    parent: UiNodeId,
10017    name: &'static str,
10018    kind: widgets::PanelKind,
10019    title: &'static str,
10020    detail: &'static str,
10021) -> UiNodeId {
10022    let panel = widgets::panel(
10023        ui,
10024        parent,
10025        name,
10026        widgets::PanelOptions {
10027            kind,
10028            layout: LayoutStyle::column()
10029                .with_width_percent(1.0)
10030                .with_height_percent(1.0)
10031                .with_padding(10.0)
10032                .with_gap(6.0),
10033            visual: UiVisual::panel(
10034                color(18, 23, 31),
10035                Some(StrokeStyle::new(color(66, 80, 98), 1.0)),
10036                0.0,
10037            ),
10038            accessibility_label: Some(title.to_string()),
10039            ..Default::default()
10040        },
10041    );
10042    widgets::label(
10043        ui,
10044        panel,
10045        format!("{name}.label"),
10046        title,
10047        text(13.0, color(232, 240, 250)),
10048        LayoutStyle::new().with_width_percent(1.0),
10049    );
10050    widgets::label(
10051        ui,
10052        panel,
10053        format!("{name}.detail"),
10054        detail,
10055        text(11.0, color(154, 166, 184)),
10056        LayoutStyle::new().with_width_percent(1.0),
10057    );
10058    panel
10059}
10060
10061fn form_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10062    let body = section_with_min_viewport(ui, parent, "forms", "Forms", UiSize::new(390.0, 0.0));
10063    let section = widgets::form_section(
10064        ui,
10065        body,
10066        "forms.profile",
10067        None::<String>,
10068        widgets::FormSectionOptions::default().with_layout(
10069            LayoutStyle::column()
10070                .with_width_percent(1.0)
10071                .with_padding(12.0)
10072                .with_gap(10.0),
10073        ),
10074    );
10075    profile_form_summary(ui, section.root, state);
10076
10077    let status_row = wrapping_row(ui, section.root, "forms.profile.status_flags", 6.0);
10078    form_status_chip(
10079        ui,
10080        status_row,
10081        "forms.profile.status.dirty",
10082        "dirty",
10083        state.form.dirty,
10084    );
10085    form_status_chip(
10086        ui,
10087        status_row,
10088        "forms.profile.status.pending",
10089        "pending",
10090        state.form.pending,
10091    );
10092    form_status_chip(
10093        ui,
10094        status_row,
10095        "forms.profile.status.submitted",
10096        "submitted",
10097        state.form.submitted,
10098    );
10099
10100    let mut name_options = widgets::FormRowOptions::default().required();
10101    if state.form_name_text.text().trim().is_empty() {
10102        name_options = name_options.invalid("Name is required");
10103    }
10104    let name = widgets::form_row(ui, section.root, "forms.profile.name", name_options);
10105    widgets::field_label(
10106        ui,
10107        name,
10108        "forms.profile.name.label",
10109        "Name",
10110        widgets::FieldLabelOptions::default().required(),
10111    );
10112    form_text_field(
10113        ui,
10114        name,
10115        "forms.profile.name.input",
10116        &state.form_name_text,
10117        FocusedTextInput::FormName,
10118        state,
10119    );
10120    if state.form_name_text.text().trim().is_empty() {
10121        widgets::field_validation_message(
10122            ui,
10123            name,
10124            "forms.profile.name.validation",
10125            ValidationMessage::error("Name is required"),
10126            widgets::ValidationMessageOptions::default(),
10127        );
10128    } else {
10129        widgets::field_help_text(
10130            ui,
10131            name,
10132            "forms.profile.name.help",
10133            "Shown in window titles and project lists.",
10134            widgets::FieldHelpOptions::default(),
10135        );
10136    }
10137
10138    let mut email_options = widgets::FormRowOptions::default().required();
10139    if !profile_email_valid(state.form_email_text.text()) {
10140        email_options = email_options.invalid("Use a complete email address");
10141    }
10142    let email = widgets::form_row(ui, section.root, "forms.profile.email", email_options);
10143    widgets::field_label(
10144        ui,
10145        email,
10146        "forms.profile.email.label",
10147        "Email",
10148        widgets::FieldLabelOptions::default().required(),
10149    );
10150    form_text_field(
10151        ui,
10152        email,
10153        "forms.profile.email.input",
10154        &state.form_email_text,
10155        FocusedTextInput::FormEmail,
10156        state,
10157    );
10158    if profile_email_valid(state.form_email_text.text()) {
10159        widgets::field_help_text(
10160            ui,
10161            email,
10162            "forms.profile.email.help",
10163            "Used for workspace invites and notifications.",
10164            widgets::FieldHelpOptions::default(),
10165        );
10166    } else {
10167        widgets::field_validation_message(
10168            ui,
10169            email,
10170            "forms.profile.email.validation",
10171            ValidationMessage::error("Use a complete email address"),
10172            widgets::ValidationMessageOptions::default(),
10173        );
10174    }
10175
10176    let role = widgets::form_row(
10177        ui,
10178        section.root,
10179        "forms.profile.role",
10180        widgets::FormRowOptions::default(),
10181    );
10182    widgets::field_label(
10183        ui,
10184        role,
10185        "forms.profile.role.label",
10186        "Role",
10187        widgets::FieldLabelOptions::default(),
10188    );
10189    form_text_field(
10190        ui,
10191        role,
10192        "forms.profile.role.input",
10193        &state.form_role_text,
10194        FocusedTextInput::FormRole,
10195        state,
10196    );
10197    widgets::field_validation_message(
10198        ui,
10199        role,
10200        "forms.profile.role.help",
10201        if state.form_role_text.text().trim().is_empty() {
10202            ValidationMessage::warning("Role can be added later")
10203        } else {
10204            ValidationMessage::info(
10205                "Form rows compose labels, controls, help, and validation text.",
10206            )
10207        },
10208        widgets::ValidationMessageOptions::default(),
10209    );
10210
10211    let newsletter = widgets::form_row(
10212        ui,
10213        section.root,
10214        "forms.profile.newsletter",
10215        widgets::FormRowOptions::default().with_accessibility_label("Newsletter preference"),
10216    );
10217    let mut newsletter_options =
10218        widgets::CheckboxOptions::default().with_action("forms.profile.newsletter.toggle");
10219    newsletter_options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
10220    newsletter_options.text_style = text(12.0, color(220, 228, 238));
10221    widgets::checkbox(
10222        ui,
10223        newsletter,
10224        "forms.profile.newsletter.input",
10225        "Send release notes",
10226        state.form_newsletter,
10227        newsletter_options,
10228    );
10229    widgets::field_help_text(
10230        ui,
10231        newsletter,
10232        "forms.profile.newsletter.help",
10233        "Checkboxes participate in the same form state as text fields.",
10234        widgets::FieldHelpOptions::default(),
10235    );
10236
10237    widgets::form_error_summary(
10238        ui,
10239        section.root,
10240        "forms.profile.errors",
10241        &state.form,
10242        widgets::FormErrorSummaryOptions::default(),
10243    );
10244    widgets::label(
10245        ui,
10246        section.root,
10247        "forms.profile.action_help",
10248        "Apply changes saves this draft and keeps editing. Submit profile saves and marks it submitted.",
10249        text(11.0, color(154, 166, 184)),
10250        LayoutStyle::new().with_width_percent(1.0),
10251    );
10252    let action_layout = Layout::row()
10253        .size(LayoutSize::new(
10254            LayoutDimension::percent(1.0),
10255            LayoutDimension::Auto,
10256        ))
10257        .gap(LayoutGap::points(8.0, 8.0))
10258        .flex_wrap(LayoutFlexWrap::Wrap)
10259        .to_layout_style();
10260    widgets::form_action_buttons(
10261        ui,
10262        section.root,
10263        "forms.profile.actions",
10264        &state.form,
10265        widgets::FormActionButtonsOptions::default()
10266            .with_layout(action_layout)
10267            .with_labels(widgets::FormActionLabels {
10268                submit: "Submit profile".to_string(),
10269                apply: "Apply changes".to_string(),
10270                cancel: "Cancel".to_string(),
10271                reset: "Reset".to_string(),
10272            })
10273            .include_reset(true)
10274            .with_action_prefix("forms.profile"),
10275    );
10276    widgets::label(
10277        ui,
10278        section.root,
10279        "forms.profile.status",
10280        format!("Status: {}", state.form_status),
10281        text(11.0, color(154, 166, 184)),
10282        LayoutStyle::new().with_width_percent(1.0),
10283    );
10284}
10285
10286fn overlay_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10287    let body =
10288        section_with_min_viewport(ui, parent, "overlays", "Overlays", UiSize::new(420.0, 0.0));
10289    let header = widgets::collapsing_header(
10290        ui,
10291        body,
10292        "overlays.collapsing",
10293        "Collapsing header",
10294        widgets::CollapsingHeaderOptions::default()
10295            .expanded(state.overlay_expanded)
10296            .with_toggle_action("overlays.collapsing.toggle"),
10297    );
10298    if let Some(panel) = header.body {
10299        widgets::label(
10300            ui,
10301            panel,
10302            "overlays.collapsing.body_text",
10303            "Expanded content lives under the header and remains part of normal layout.",
10304            text(12.0, color(196, 210, 230)),
10305            LayoutStyle::new().with_width_percent(1.0),
10306        );
10307    }
10308
10309    let controls = wrapping_row(ui, body, "overlays.controls", 8.0);
10310    let tooltip_visual = button_visual(58, 78, 96);
10311    let mut tooltip_options = widgets::ButtonOptions::new(LayoutStyle::new().with_height(32.0));
10312    tooltip_options.visual = tooltip_visual;
10313    tooltip_options.hovered_visual = Some(readable_button_hover_visual(tooltip_visual));
10314    tooltip_options.pressed_visual = Some(adjusted_button_visual(tooltip_visual, -62));
10315    tooltip_options.pressed_hovered_visual = Some(adjusted_button_visual(tooltip_visual, -24));
10316    tooltip_options.text_style = text(13.0, color(246, 249, 252));
10317    let tooltip_target = widgets::button(
10318        ui,
10319        controls,
10320        "overlays.tooltip_target",
10321        "Tooltip target",
10322        tooltip_options,
10323    );
10324    ui.node_mut(tooltip_target).set_tooltip(
10325        TooltipContent::new("Tooltip")
10326            .body("Tooltips render as overlay surfaces anchored to a target.")
10327            .shortcut_label("Ctrl+K")
10328            .disabled_reason("Disabled reasons can be announced without changing the trigger."),
10329    );
10330    ui.node_mut(tooltip_target)
10331        .set_tooltip_placement(TooltipPlacement::Below);
10332    ui.node_mut(tooltip_target)
10333        .set_tooltip_size(UiSize::new(240.0, 148.0));
10334    button(
10335        ui,
10336        controls,
10337        "overlays.popup.toggle",
10338        if state.overlay_popup_open {
10339            "Close popup"
10340        } else {
10341            "Open popup"
10342        },
10343        "overlays.popup.toggle",
10344        button_visual(48, 112, 184),
10345    );
10346    button(
10347        ui,
10348        controls,
10349        "overlays.modal.open",
10350        "Open modal",
10351        "overlays.modal.open",
10352        button_visual(58, 78, 96),
10353    );
10354
10355    widgets::label(
10356        ui,
10357        body,
10358        "overlays.tooltip_rect.label",
10359        "A right-edge target keeps its tooltip inside the preview.",
10360        text(12.0, color(166, 176, 190)),
10361        LayoutStyle::new().with_width_percent(1.0),
10362    );
10363    let preview_viewport = UiRect::new(0.0, 0.0, 420.0, 112.0);
10364    let tooltip_target = UiRect::new(328.0, 42.0, 64.0, 28.0);
10365    let tooltip_size = UiSize::new(176.0, 58.0);
10366    let placed_tooltip = widgets::tooltip::tooltip_rect(
10367        tooltip_target,
10368        tooltip_size,
10369        preview_viewport,
10370        TooltipPlacement::Right,
10371        8.0,
10372        None,
10373    );
10374    let clamped_preview = ui.add_child(
10375        body,
10376        UiNode::container(
10377            "overlays.tooltip_rect.preview",
10378            LayoutStyle::new()
10379                .with_width_percent(1.0)
10380                .with_height(112.0)
10381                .with_flex_shrink(0.0),
10382        )
10383        .with_visual(UiVisual::panel(
10384            color(12, 16, 22),
10385            Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
10386            4.0,
10387        )),
10388    );
10389    ui.add_child(
10390        clamped_preview,
10391        UiNode::scene(
10392            "overlays.tooltip_rect.scene",
10393            vec![
10394                ScenePrimitive::Line {
10395                    from: UiPoint::new(placed_tooltip.right() + 2.0, placed_tooltip.y + 29.0),
10396                    to: UiPoint::new(tooltip_target.x - 2.0, tooltip_target.y + 14.0),
10397                    stroke: StrokeStyle::new(color(92, 106, 128), 1.0),
10398                },
10399                ScenePrimitive::Rect(
10400                    PaintRect::solid(placed_tooltip, color(24, 29, 38))
10401                        .stroke(AlignedStroke::inside(StrokeStyle::new(
10402                            color(92, 106, 128),
10403                            1.0,
10404                        )))
10405                        .corner_radii(CornerRadii::uniform(4.0)),
10406                ),
10407                ScenePrimitive::Text(
10408                    PaintText::new(
10409                        "Tooltip",
10410                        UiRect::new(
10411                            placed_tooltip.x + 12.0,
10412                            placed_tooltip.y + 9.0,
10413                            placed_tooltip.width - 24.0,
10414                            18.0,
10415                        ),
10416                        text(12.0, color(225, 233, 244)),
10417                    )
10418                    .multiline(false),
10419                ),
10420                ScenePrimitive::Text(
10421                    PaintText::new(
10422                        "Placed inside",
10423                        UiRect::new(
10424                            placed_tooltip.x + 12.0,
10425                            placed_tooltip.y + 31.0,
10426                            placed_tooltip.width - 24.0,
10427                            18.0,
10428                        ),
10429                        text(10.0, color(156, 170, 190)),
10430                    )
10431                    .multiline(false),
10432                ),
10433                ScenePrimitive::Rect(
10434                    PaintRect::solid(tooltip_target, color(48, 112, 184))
10435                        .stroke(AlignedStroke::inside(StrokeStyle::new(
10436                            color(132, 190, 255),
10437                            1.0,
10438                        )))
10439                        .corner_radii(CornerRadii::uniform(3.0)),
10440                ),
10441                ScenePrimitive::Text(
10442                    PaintText::new("Target", tooltip_target, text(10.0, color(240, 247, 255)))
10443                        .horizontal_align(TextHorizontalAlign::Center)
10444                        .vertical_align(TextVerticalAlign::Center)
10445                        .multiline(false),
10446                ),
10447            ],
10448            LayoutStyle::new()
10449                .with_width_percent(1.0)
10450                .with_height_percent(1.0),
10451        ),
10452    );
10453
10454    widgets::label(
10455        ui,
10456        body,
10457        "overlays.popup.label",
10458        "Popup panel",
10459        text(12.0, color(166, 176, 190)),
10460        LayoutStyle::new().with_width_percent(1.0),
10461    );
10462    widgets::label(
10463        ui,
10464        body,
10465        "overlays.popup.status",
10466        if state.overlay_popup_open {
10467            "Popup overlay is open."
10468        } else {
10469            "Popup overlay is closed."
10470        },
10471        text(12.0, color(196, 210, 230)),
10472        LayoutStyle::new().with_width_percent(1.0),
10473    );
10474    let popup_host_layout = if state.overlay_popup_open {
10475        LayoutStyle::column()
10476            .with_width_percent(1.0)
10477            .with_height(128.0)
10478            .with_flex_shrink(0.0)
10479    } else {
10480        LayoutStyle::column()
10481            .with_width_percent(1.0)
10482            .with_padding(10.0)
10483            .with_flex_shrink(0.0)
10484    };
10485    let popup_host = ui.add_child(
10486        body,
10487        UiNode::container("overlays.popup.host", popup_host_layout).with_visual(UiVisual::panel(
10488            color(12, 16, 22),
10489            Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
10490            4.0,
10491        )),
10492    );
10493    if state.overlay_popup_open {
10494        let popup = ext_widgets::popup_panel(
10495            ui,
10496            popup_host,
10497            "overlays.popup_panel",
10498            UiRect::new(10.0, 10.0, 220.0, 104.0),
10499            ext_widgets::PopupOptions {
10500                z_index: 4,
10501                portal: UiPortalTarget::Parent,
10502                accessibility: Some(
10503                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup preview"),
10504                ),
10505                ..Default::default()
10506            },
10507        );
10508        let popup_body = ui.add_child(
10509            popup,
10510            UiNode::container(
10511                "overlays.popup_panel.body",
10512                LayoutStyle::column()
10513                    .with_width_percent(1.0)
10514                    .with_height_percent(1.0)
10515                    .with_padding(10.0)
10516                    .with_gap(6.0),
10517            ),
10518        );
10519        let popup_header = row(ui, popup_body, "overlays.popup_panel.header", 8.0);
10520        widgets::label(
10521            ui,
10522            popup_header,
10523            "overlays.popup_panel.label",
10524            "Popup panel",
10525            text(12.0, color(220, 228, 238)),
10526            LayoutStyle::new().with_width_percent(1.0),
10527        );
10528        let mut close = widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0))
10529            .with_action("overlays.popup.close");
10530        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
10531        close.hovered_visual = Some(button_visual(54, 70, 92));
10532        close.text_style = text(12.0, color(220, 228, 238));
10533        widgets::button(ui, popup_header, "overlays.popup_panel.close", "x", close);
10534        widgets::label(
10535            ui,
10536            popup_body,
10537            "overlays.popup_panel.body_text",
10538            "Popup content is conditionally rendered.",
10539            text(11.0, color(196, 210, 230)),
10540            LayoutStyle::new().with_width_percent(1.0),
10541        );
10542    } else {
10543        widgets::label(
10544            ui,
10545            popup_host,
10546            "overlays.popup.empty",
10547            "Open the popup to render an overlay inside this host.",
10548            text(12.0, color(154, 166, 184)),
10549            LayoutStyle::new().with_width_percent(1.0),
10550        );
10551    }
10552
10553    widgets::label(
10554        ui,
10555        body,
10556        "overlays.toasts.label",
10557        "Toasts",
10558        text(12.0, color(166, 176, 190)),
10559        LayoutStyle::new().with_width_percent(1.0),
10560    );
10561    let toast_controls = row(ui, body, "overlays.toasts.controls", 10.0);
10562    button(
10563        ui,
10564        toast_controls,
10565        "overlays.toasts.show",
10566        "Show toast",
10567        "toast.show",
10568        button_visual(48, 112, 184),
10569    );
10570    button(
10571        ui,
10572        toast_controls,
10573        "overlays.toasts.hide",
10574        "Hide",
10575        "toast.hide",
10576        button_visual(58, 78, 96),
10577    );
10578    widgets::label(
10579        ui,
10580        body,
10581        "overlays.toasts.status",
10582        if state.toast_visible {
10583            "Toast overlay is visible."
10584        } else {
10585            "Toast overlay is hidden."
10586        },
10587        text(12.0, color(196, 210, 230)),
10588        LayoutStyle::new().with_width_percent(1.0),
10589    );
10590    widgets::label(
10591        ui,
10592        body,
10593        "overlays.toasts.action_status",
10594        format!("Action: {}", state.toast_action_status),
10595        text(12.0, color(154, 166, 184)),
10596        LayoutStyle::new().with_width_percent(1.0),
10597    );
10598
10599    if state.overlay_modal_open {
10600        let modal = widgets::modal_dialog(
10601            ui,
10602            parent,
10603            "overlays.modal",
10604            "Modal dialog",
10605            widgets::ModalDialogOptions::default()
10606                .with_size(320.0, 180.0)
10607                .with_close_action("overlays.modal.close")
10608                .with_dismissal(ext_widgets::DialogDismissal::MODAL)
10609                .with_focus_restore(FocusRestoreTarget::Previous),
10610        );
10611        widgets::label(
10612            ui,
10613            modal.body,
10614            "overlays.modal.body.text",
10615            "Modal dialogs are portaled to the application overlay, include a scrim, and trap focus.",
10616            text(12.0, color(220, 228, 238)),
10617            LayoutStyle::new().with_width_percent(1.0),
10618        );
10619        button(
10620            ui,
10621            modal.body,
10622            "overlays.modal.body.close",
10623            "Close modal",
10624            "overlays.modal.close",
10625            button_visual(48, 112, 184),
10626        );
10627    }
10628}
10629
10630fn drag_drop_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10631    let body = section_with_min_viewport(
10632        ui,
10633        parent,
10634        "drag_drop",
10635        "Drag and drop",
10636        UiSize::new(420.0, 0.0),
10637    );
10638    widgets::label(
10639        ui,
10640        body,
10641        "drag_drop.sources.label",
10642        "Drag sources",
10643        text(12.0, color(166, 176, 190)),
10644        LayoutStyle::new().with_width_percent(1.0),
10645    );
10646    let sources = wrapping_row(ui, body, "drag_drop.sources", 8.0);
10647    widgets::dnd_drag_source(
10648        ui,
10649        sources,
10650        "drag_drop.text_source",
10651        "Text payload",
10652        DragPayload::text("Operad payload"),
10653        widgets::DragSourceOptions::default()
10654            .with_layout(drag_source_layout())
10655            .with_kind(DragDropSurfaceKind::ListRow)
10656            .with_allowed_operations([DragOperation::Copy, DragOperation::Move])
10657            .with_action("drag_drop.text_source")
10658            .with_accessibility_hint("Start a text drag operation"),
10659    );
10660    widgets::dnd_drag_source(
10661        ui,
10662        sources,
10663        "drag_drop.file_source",
10664        "File payload",
10665        DragPayload::files(["/tmp/showcase.scene"]),
10666        widgets::DragSourceOptions::default()
10667            .with_layout(drag_source_layout())
10668            .with_kind(DragDropSurfaceKind::Asset)
10669            .with_drag_image_policy(widgets::DragImagePolicy::image_key(
10670                BuiltInIcon::Folder.key(),
10671                UiSize::new(120.0, 36.0),
10672                UiPoint::new(10.0, 10.0),
10673            ))
10674            .with_allowed_operations([DragOperation::Copy])
10675            .with_action("drag_drop.file_source"),
10676    );
10677    widgets::dnd_drag_source(
10678        ui,
10679        sources,
10680        "drag_drop.bytes_source",
10681        "Image bytes",
10682        DragPayload::bytes(DragBytes::new("image/png", vec![137, 80, 78, 71]).name("sprite.png")),
10683        widgets::DragSourceOptions::default()
10684            .with_layout(drag_source_layout())
10685            .with_kind(DragDropSurfaceKind::Asset)
10686            .with_action("drag_drop.bytes_source")
10687            .without_drag_image(),
10688    );
10689
10690    widgets::label(
10691        ui,
10692        body,
10693        "drag_drop.zones.label",
10694        "Drop zones",
10695        text(12.0, color(166, 176, 190)),
10696        LayoutStyle::new().with_width_percent(1.0),
10697    );
10698    let zones = wrapping_row(ui, body, "drag_drop.zones", 8.0);
10699    let accepted_options = widgets::DropZoneOptions::default()
10700        .with_layout(drop_zone_layout())
10701        .with_kind(DragDropSurfaceKind::EditorSurface)
10702        .with_accepted_payload(DropPayloadFilter::empty().text())
10703        .with_accepted_operations([DragOperation::Copy, DragOperation::Move])
10704        .with_action("drag_drop.accept_text")
10705        .with_accessibility_hint("Accepts text payloads");
10706    let accepted = widgets::dnd_drop_zone(
10707        ui,
10708        zones,
10709        "drag_drop.accept_text",
10710        "Text accepted",
10711        accepted_options.clone(),
10712    );
10713    widgets::drag_drop::dnd_apply_drop_zone_preview(
10714        ui,
10715        accepted.root,
10716        &accepted_options,
10717        widgets::drag_drop::DropZonePreviewState::Accepted,
10718    );
10719
10720    let rejected_options = widgets::DropZoneOptions::default()
10721        .with_layout(drop_zone_layout())
10722        .with_kind(DragDropSurfaceKind::Asset)
10723        .with_accepted_payload(DropPayloadFilter::empty().files())
10724        .with_action("drag_drop.files_only");
10725    let rejected = widgets::dnd_drop_zone(
10726        ui,
10727        zones,
10728        "drag_drop.files_only",
10729        "Files only",
10730        rejected_options.clone(),
10731    );
10732    widgets::drag_drop::dnd_apply_drop_zone_preview(
10733        ui,
10734        rejected.root,
10735        &rejected_options,
10736        widgets::drag_drop::DropZonePreviewState::Rejected,
10737    );
10738    let image_options = widgets::DropZoneOptions::default()
10739        .with_layout(drop_zone_layout())
10740        .with_kind(DragDropSurfaceKind::Asset)
10741        .with_accepted_payload(DropPayloadFilter::empty().mime_type("image/*"))
10742        .with_accepted_operations([DragOperation::Copy])
10743        .with_action("drag_drop.image_bytes");
10744    let image_zone = widgets::dnd_drop_zone(
10745        ui,
10746        zones,
10747        "drag_drop.image_bytes",
10748        "Image bytes",
10749        image_options.clone(),
10750    );
10751    widgets::drag_drop::dnd_apply_drop_zone_preview(
10752        ui,
10753        image_zone.root,
10754        &image_options,
10755        widgets::drag_drop::DropZonePreviewState::Hovered,
10756    );
10757
10758    let disabled_options = widgets::DropZoneOptions::default()
10759        .with_layout(drop_zone_layout())
10760        .with_kind(DragDropSurfaceKind::EditorSurface)
10761        .with_accepted_payload(DropPayloadFilter::any())
10762        .with_action("drag_drop.disabled")
10763        .disabled();
10764    let disabled_zone = widgets::dnd_drop_zone(
10765        ui,
10766        zones,
10767        "drag_drop.disabled",
10768        "Disabled",
10769        disabled_options.clone(),
10770    );
10771    widgets::drag_drop::dnd_apply_drop_zone_preview(
10772        ui,
10773        disabled_zone.root,
10774        &disabled_options,
10775        widgets::drag_drop::DropZonePreviewState::Disabled,
10776    );
10777
10778    let operation_row = wrapping_row(ui, body, "drag_drop.operations", 6.0);
10779    dnd_operation_chip(ui, operation_row, "drag_drop.operation.copy", "copy");
10780    dnd_operation_chip(ui, operation_row, "drag_drop.operation.move", "move");
10781    dnd_operation_chip(ui, operation_row, "drag_drop.operation.link", "link");
10782    widgets::label(
10783        ui,
10784        body,
10785        "drag_drop.status",
10786        format!("Status: {}", state.drag_drop_status),
10787        text(11.0, color(154, 166, 184)),
10788        LayoutStyle::new().with_width_percent(1.0),
10789    );
10790}
10791
10792fn media_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10793    let body = section_with_min_viewport(
10794        ui,
10795        parent,
10796        "media",
10797        "Media",
10798        UiSize::new(MEDIA_ICON_TILE_WIDTH, 0.0),
10799    );
10800    widgets::label(
10801        ui,
10802        body,
10803        "media.icons.label",
10804        "Built-in icons",
10805        text(12.0, color(166, 176, 190)),
10806        LayoutStyle::new().with_width_percent(1.0),
10807    );
10808    let icon_columns = media_icon_columns(state);
10809    let icons = media_icon_grid(
10810        ui,
10811        body,
10812        "media.icons",
10813        icon_columns,
10814        BuiltInIcon::COMMON.len(),
10815    );
10816    for icon in BuiltInIcon::COMMON {
10817        media_icon_tile(ui, icons, icon);
10818    }
10819
10820    widgets::label(
10821        ui,
10822        body,
10823        "media.variants.label",
10824        "Image variants",
10825        text(12.0, color(166, 176, 190)),
10826        LayoutStyle::new().with_width_percent(1.0),
10827    );
10828    let variants = wrapping_row(ui, body, "media.variants", 10.0);
10829    widgets::image(
10830        ui,
10831        variants,
10832        "media.image.user_png",
10833        ImageContent::from(ImageHandle::app(SHOWCASE_USER_IMAGE_KEY)),
10834        widgets::ImageOptions::default()
10835            .with_layout(media_preview_image_layout())
10836            .with_accessibility_label("User supplied PNG image"),
10837    );
10838    widgets::image(
10839        ui,
10840        variants,
10841        "media.image.untinted",
10842        icon_image(BuiltInIcon::Play),
10843        widgets::ImageOptions::default()
10844            .with_layout(media_preview_image_layout())
10845            .with_accessibility_label("Untinted play icon"),
10846    );
10847    widgets::image(
10848        ui,
10849        variants,
10850        "media.image.warning",
10851        ImageContent::new(BuiltInIcon::Warning.key()).tinted(color(232, 186, 88)),
10852        widgets::ImageOptions::default()
10853            .with_layout(media_preview_image_layout())
10854            .with_accessibility_label("Tinted warning icon"),
10855    );
10856    widgets::image(
10857        ui,
10858        variants,
10859        "media.image.shader",
10860        ImageContent::new(BuiltInIcon::Grid.key()).tinted(color(118, 183, 255)),
10861        widgets::ImageOptions::default()
10862            .with_layout(media_preview_image_layout())
10863            .with_shader(ShaderEffect::tint(color(169, 119, 255), 0.65))
10864            .with_accessibility_label("Shader-decorated grid icon"),
10865    );
10866}
10867
10868fn shader_effect_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10869    let body = section_with_min_viewport(
10870        ui,
10871        parent,
10872        "shaders",
10873        "Shader effects",
10874        UiSize::new(420.0, 280.0),
10875    );
10876    widgets::label(
10877        ui,
10878        body,
10879        "shaders.effects.label",
10880        "Built-in effects",
10881        text(12.0, color(166, 176, 190)),
10882        LayoutStyle::new().with_width_percent(1.0),
10883    );
10884    let phase = (state.progress_phase / std::f32::consts::TAU).fract();
10885    let previews = wrapping_row(ui, body, "shaders.effects", 10.0);
10886    shader_effect_preview_card(ui, previews, "base", "Base", None);
10887    shader_effect_preview_card(
10888        ui,
10889        previews,
10890        "tint",
10891        "Tint",
10892        Some(ShaderEffect::tint(color(252, 186, 90), 0.72)),
10893    );
10894    shader_effect_preview_card(
10895        ui,
10896        previews,
10897        "shine",
10898        "Shine",
10899        Some(ShaderEffect::shine(phase, 0.55).uniform("width", 0.14)),
10900    );
10901    shader_effect_preview_card(
10902        ui,
10903        previews,
10904        "glow",
10905        "Glow",
10906        Some(ShaderEffect::glow(color(118, 183, 255), 0.9, 7.0)),
10907    );
10908
10909    widgets::label(
10910        ui,
10911        body,
10912        "shaders.widgets.label",
10913        "Applied to widgets",
10914        text(12.0, color(166, 176, 190)),
10915        LayoutStyle::new().with_width_percent(1.0),
10916    );
10917    let panel = ui.add_child(
10918        body,
10919        UiNode::container(
10920            "shaders.widgets",
10921            LayoutStyle::column()
10922                .with_width_percent(1.0)
10923                .with_padding(10.0)
10924                .with_gap(10.0)
10925                .with_flex_shrink(0.0),
10926        )
10927        .with_visual(UiVisual::panel(
10928            color(13, 18, 25),
10929            Some(StrokeStyle::new(color(48, 61, 78), 1.0)),
10930            4.0,
10931        )),
10932    );
10933    let control_row = wrapping_row(ui, panel, "shaders.widgets.controls", 10.0);
10934    let mut shine_button = widgets::ButtonOptions::new(
10935        LayoutStyle::new()
10936            .with_width(150.0)
10937            .with_height(34.0)
10938            .with_flex_shrink(0.0),
10939    );
10940    shine_button.leading_image = Some(icon_image(BuiltInIcon::Settings));
10941    shine_button.image_shader = Some(ShaderEffect::tint(color(111, 203, 159), 0.85));
10942    shine_button.shader = Some(ShaderEffect::shine(phase, 0.45).uniform("width", 0.12));
10943    widgets::button(
10944        ui,
10945        control_row,
10946        "shaders.widgets.button",
10947        "Shine button",
10948        shine_button,
10949    );
10950
10951    widgets::checkbox_with_state(
10952        ui,
10953        control_row,
10954        "shaders.widgets.checkbox",
10955        "Glow check",
10956        widgets::CheckboxState::Checked,
10957        widgets::CheckboxOptions::default()
10958            .with_check_color(color(118, 183, 255))
10959            .with_check_shader(ShaderEffect::glow(color(118, 183, 255), 1.0, 4.0)),
10960    );
10961
10962    let progress_value = smooth_loop(state.progress_phase * 0.8, 0.2) * 100.0;
10963    let mut progress = ext_widgets::ProgressIndicatorOptions::default();
10964    progress.layout = LayoutStyle::new().with_width_percent(1.0).with_height(12.0);
10965    progress.fill_visual = UiVisual::panel(color(111, 203, 159), None, 4.0);
10966    progress.fill_shader = Some(ShaderEffect::shine(phase, 0.5).uniform("width", 0.16));
10967    progress.accessibility_label = Some("Shadered progress fill".to_string());
10968    ext_widgets::progress_indicator(
10969        ui,
10970        panel,
10971        "shaders.widgets.progress",
10972        ext_widgets::ProgressIndicatorValue::percent(progress_value),
10973        progress,
10974    );
10975
10976    let mut slider_options = widgets::SliderOptions::default();
10977    slider_options.layout = LayoutStyle::new()
10978        .with_width_percent(1.0)
10979        .with_height(28.0)
10980        .with_flex_shrink(0.0);
10981    slider_options.fill_shader = Some(ShaderEffect::tint(color(169, 119, 255), 0.55));
10982    slider_options.thumb_shader = Some(ShaderEffect::glow(color(252, 186, 90), 0.9, 4.0));
10983    slider_options.accessibility_label = Some("Shadered slider".to_string());
10984    widgets::slider(
10985        ui,
10986        panel,
10987        "shaders.widgets.slider",
10988        smooth_loop(state.progress_phase * 0.6, 0.4),
10989        0.0..1.0,
10990        slider_options,
10991    );
10992}
10993
10994fn shader_effect_preview_card(
10995    ui: &mut UiDocument,
10996    parent: UiNodeId,
10997    name: &'static str,
10998    label: &'static str,
10999    shader: Option<ShaderEffect>,
11000) {
11001    let tile = ui.add_child(
11002        parent,
11003        UiNode::container(
11004            format!("shaders.effect_tile.{name}"),
11005            LayoutStyle::column()
11006                .with_width(96.0)
11007                .with_height(104.0)
11008                .with_padding(8.0)
11009                .with_gap(8.0)
11010                .with_flex_shrink(0.0),
11011        )
11012        .with_visual(UiVisual::panel(
11013            color(17, 22, 30),
11014            Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
11015            4.0,
11016        ))
11017        .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(label)),
11018    );
11019    let mut swatch = UiNode::container(
11020        format!("shaders.effect.{name}.swatch"),
11021        LayoutStyle::new()
11022            .with_width_percent(1.0)
11023            .with_height(50.0)
11024            .with_flex_shrink(0.0),
11025    )
11026    .with_visual(UiVisual::panel(
11027        color(64, 109, 194),
11028        Some(StrokeStyle::new(color(138, 164, 194), 1.0)),
11029        8.0,
11030    ));
11031    if let Some(shader) = shader {
11032        swatch = swatch.with_shader(shader);
11033    }
11034    ui.add_child(tile, swatch);
11035    widgets::label(
11036        ui,
11037        tile,
11038        format!("shaders.effect.{name}.label"),
11039        label,
11040        text(11.0, color(204, 216, 232)),
11041        LayoutStyle::new().with_width_percent(1.0),
11042    );
11043}
11044
11045fn shader_lab_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11046    let body = section_with_min_viewport(
11047        ui,
11048        parent,
11049        "shader_lab",
11050        "Shader lab",
11051        UiSize::new(SHADER_LAB_CONTENT_MIN_WIDTH, SHADER_LAB_CONTENT_MIN_HEIGHT),
11052    );
11053    let source_error = state.shader_lab_source_error.as_deref();
11054    let source_valid = source_error.is_none();
11055
11056    let mut split_options = ext_widgets::SplitPaneOptions::default()
11057        .with_handle_action("shader_lab.workspace.resize")
11058        .with_handle_hover_visual(UiVisual::panel(color(96, 166, 238), None, 2.0));
11059    split_options.layout = Layout::row()
11060        .size(LayoutSize::new(
11061            LayoutDimension::percent(1.0),
11062            LayoutDimension::points(SHADER_LAB_WORKSPACE_HEIGHT),
11063        ))
11064        .min_size(LayoutSize::points(
11065            SHADER_LAB_CONTENT_MIN_WIDTH,
11066            SHADER_LAB_WORKSPACE_HEIGHT,
11067        ))
11068        .flex(0.0, 0.0, LayoutDimension::Auto)
11069        .to_layout_style();
11070    split_options.handle_thickness = SHADER_LAB_SPLIT_HANDLE_THICKNESS;
11071    split_options.handle_visual = UiVisual::panel(color(48, 61, 78), None, 2.0);
11072
11073    ext_widgets::split_pane(
11074        ui,
11075        body,
11076        "shader_lab.workspace",
11077        ext_widgets::SplitAxis::Horizontal,
11078        state.shader_lab_split,
11079        split_options,
11080        |ui, preview_pane| {
11081            shader_lab_preview_column(ui, preview_pane, state, source_error, source_valid);
11082        },
11083        |ui, editor_pane| {
11084            shader_lab_editor_column(ui, editor_pane, state, source_error);
11085        },
11086    );
11087}
11088
11089fn shader_lab_preview_column(
11090    ui: &mut UiDocument,
11091    parent: UiNodeId,
11092    state: &ShowcaseState,
11093    source_error: Option<&str>,
11094    source_valid: bool,
11095) {
11096    let preview_column = ui.add_child(
11097        parent,
11098        UiNode::container(
11099            "shader_lab.preview.column",
11100            LayoutStyle::column()
11101                .with_width_percent(1.0)
11102                .with_height_percent(1.0)
11103                .with_gap(8.0)
11104                .with_flex_shrink(1.0),
11105        ),
11106    );
11107    let target_row = row(ui, preview_column, "shader_lab.target.row", 8.0);
11108    widgets::label(
11109        ui,
11110        target_row,
11111        "shader_lab.target.caption",
11112        "Preview",
11113        text(12.0, color(166, 176, 190)),
11114        LayoutStyle::new()
11115            .with_width(58.0)
11116            .with_height(30.0)
11117            .with_flex_shrink(0.0),
11118    );
11119    shader_lab_dropdown_select(
11120        ui,
11121        target_row,
11122        "shader_lab.target",
11123        &shader_lab_target_options(),
11124        &state.shader_lab_target_menu,
11125        160.0,
11126        "Preview target",
11127        "Shader lab preview target",
11128    );
11129    shader_lab_preview_controls(ui, preview_column, state);
11130
11131    let preview = ui.add_child(
11132        preview_column,
11133        UiNode::container(
11134            "shader_lab.preview.surface",
11135            LayoutStyle::column()
11136                .with_width_percent(1.0)
11137                .with_height(0.0)
11138                .with_flex_grow(1.0)
11139                .with_padding(18.0)
11140                .with_gap(8.0)
11141                .with_align_items(taffy::prelude::AlignItems::Center)
11142                .with_justify_content(taffy::prelude::JustifyContent::Center)
11143                .with_flex_shrink(0.0),
11144        )
11145        .with_visual(UiVisual::panel(
11146            color(8, 12, 18),
11147            Some(StrokeStyle::new(color(48, 61, 78), 1.0)),
11148            4.0,
11149        )),
11150    );
11151    shader_lab_preview(ui, preview, state, source_valid);
11152
11153    if let Some(status) = shader_lab_status_label(state, source_error) {
11154        widgets::label(
11155            ui,
11156            preview_column,
11157            "shader_lab.preview.status",
11158            status,
11159            text(11.0, color(166, 176, 190)),
11160            LayoutStyle::new().with_width_percent(1.0),
11161        );
11162    }
11163    shader_lab_material_contract_demo(ui, preview_column, state);
11164}
11165
11166fn shader_lab_preview_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11167    let controls = ui.add_child(
11168        parent,
11169        UiNode::container(
11170            "shader_lab.preview.controls",
11171            LayoutStyle::column()
11172                .with_width_percent(1.0)
11173                .with_gap(6.0)
11174                .with_flex_shrink(0.0),
11175        ),
11176    );
11177    let text_row = wrapping_row(ui, controls, "shader_lab.preview.text_controls", 8.0);
11178    shader_lab_option_checkbox(
11179        ui,
11180        text_row,
11181        "shader_lab.frame_text.toggle",
11182        "Frame",
11183        state.shader_lab_show_frame_text,
11184    );
11185    shader_lab_option_checkbox(
11186        ui,
11187        text_row,
11188        "shader_lab.button_text.toggle",
11189        "Button",
11190        state.shader_lab_show_button_text,
11191    );
11192
11193    let style_row = wrapping_row(ui, controls, "shader_lab.preview.style_controls", 8.0);
11194    shader_lab_slider_control(
11195        ui,
11196        style_row,
11197        "shader_lab.surface.stroke",
11198        "Border",
11199        state.shader_lab_surface_stroke_width,
11200        SHADER_LAB_SURFACE_STROKE_MAX,
11201        1,
11202    );
11203    shader_lab_slider_control(
11204        ui,
11205        style_row,
11206        "shader_lab.surface.radius",
11207        "Radius",
11208        state.shader_lab_surface_radius,
11209        SHADER_LAB_SURFACE_RADIUS_MAX,
11210        0,
11211    );
11212}
11213
11214fn shader_lab_material_contract_demo(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11215    let panel = ui.add_child(
11216        parent,
11217        UiNode::container(
11218            "shader_lab.material",
11219            LayoutStyle::column()
11220                .with_width_percent(1.0)
11221                .with_gap(6.0)
11222                .with_flex_shrink(0.0),
11223        ),
11224    );
11225    widgets::label(
11226        ui,
11227        panel,
11228        "shader_lab.material.title",
11229        "Material",
11230        text(12.0, color(186, 198, 216)),
11231        LayoutStyle::new().with_width_percent(1.0),
11232    );
11233    let controls = wrapping_row(ui, panel, "shader_lab.material.controls", 8.0);
11234    shader_lab_labeled_dropdown(
11235        ui,
11236        controls,
11237        "shader_lab.material.shader",
11238        "Shader",
11239        &shader_lab_material_shader_options(),
11240        &state.shader_lab_material_shader_menu,
11241        132.0,
11242    );
11243    shader_lab_labeled_dropdown(
11244        ui,
11245        controls,
11246        "shader_lab.material.shape",
11247        "Shape",
11248        &shader_lab_material_shape_options(),
11249        &state.shader_lab_material_shape_menu,
11250        132.0,
11251    );
11252    shader_lab_labeled_dropdown(
11253        ui,
11254        controls,
11255        "shader_lab.material.geometry",
11256        "Geometry",
11257        &shader_lab_material_geometry_options(),
11258        &state.shader_lab_material_geometry_menu,
11259        140.0,
11260    );
11261    shader_lab_slider_control(
11262        ui,
11263        controls,
11264        "shader_lab.material.outset",
11265        "Outset",
11266        state.shader_lab_material_outset,
11267        SHADER_LAB_MATERIAL_OUTSET_MAX,
11268        0,
11269    );
11270
11271    let contracts_layout = Layout::column()
11272        .size(LayoutSize::new(
11273            LayoutDimension::percent(1.0),
11274            LayoutDimension::Auto,
11275        ))
11276        .padding(LayoutSpacing::new(
11277            LayoutLength::points(4.0),
11278            LayoutLength::points(4.0),
11279            LayoutLength::points(SHADER_LAB_MATERIAL_OUTSET_MAX),
11280            LayoutLength::points(8.0),
11281        ))
11282        .flex(0.0, 0.0, LayoutDimension::Auto)
11283        .to_layout_style();
11284    let contracts_shell = ui.add_child(
11285        panel,
11286        UiNode::container("shader_lab.material.contracts.shell", contracts_layout),
11287    );
11288    let row = wrapping_row(ui, contracts_shell, "shader_lab.material.contracts", 8.0);
11289    shader_lab_material_chip(
11290        ui,
11291        row,
11292        "shader_lab.material.current",
11293        "Selected",
11294        shader_lab_selected_material(state),
11295        shader_lab_material_visual(state.shader_lab_material_shape),
11296    );
11297    shader_lab_material_chip(
11298        ui,
11299        row,
11300        "shader_lab.material.outset",
11301        "Glow",
11302        ElementMaterial::shader(ShaderEffect::glow(
11303            color(100, 180, 255),
11304            0.95,
11305            SHADER_LAB_MATERIAL_OUTSET,
11306        ))
11307        .with_paint_outset(LayoutInsets::points(
11308            state
11309                .shader_lab_material_outset
11310                .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11311        )),
11312        UiVisual::panel(color(32, 64, 96), None, 8.0),
11313    );
11314    shader_lab_material_chip(
11315        ui,
11316        row,
11317        "shader_lab.material.circle_hit",
11318        "Circle",
11319        ElementMaterial::new()
11320            .with_clip_shape(ElementShape::circle())
11321            .with_hit_shape(ElementShape::circle()),
11322        UiVisual::panel(
11323            color(74, 133, 198),
11324            Some(StrokeStyle::new(color(212, 232, 255), 1.0)),
11325            999.0,
11326        ),
11327    );
11328    shader_lab_material_chip(
11329        ui,
11330        row,
11331        "shader_lab.material.geometry_chip",
11332        "Warp",
11333        ElementMaterial::new()
11334            .with_paint_outset(LayoutInsets::points(8.0))
11335            .with_geometry_effect(GeometryEffect::wave(8.0)),
11336        UiVisual::panel(
11337            color(101, 70, 170),
11338            Some(StrokeStyle::new(color(214, 196, 255), 1.0)),
11339            6.0,
11340        ),
11341    );
11342}
11343
11344fn shader_lab_labeled_dropdown(
11345    ui: &mut UiDocument,
11346    parent: UiNodeId,
11347    name: &'static str,
11348    label: &'static str,
11349    options: &[ext_widgets::SelectOption],
11350    state: &ext_widgets::SelectMenuState,
11351    width: f32,
11352) {
11353    let control = ui.add_child(
11354        parent,
11355        UiNode::container(
11356            format!("{name}.control"),
11357            LayoutStyle::row()
11358                .with_width(width + 74.0)
11359                .with_height(30.0)
11360                .with_gap(6.0)
11361                .with_align_items(taffy::prelude::AlignItems::Center)
11362                .with_flex_shrink(0.0),
11363        ),
11364    );
11365    widgets::label(
11366        ui,
11367        control,
11368        format!("{name}.caption"),
11369        label,
11370        text(12.0, color(166, 176, 190)),
11371        LayoutStyle::new()
11372            .with_width(66.0)
11373            .with_height(30.0)
11374            .with_flex_shrink(0.0),
11375    );
11376    shader_lab_dropdown_select(ui, control, name, options, state, width, label, label);
11377}
11378
11379fn shader_lab_dropdown_select(
11380    ui: &mut UiDocument,
11381    parent: UiNodeId,
11382    name: &'static str,
11383    options: &[ext_widgets::SelectOption],
11384    state: &ext_widgets::SelectMenuState,
11385    width: f32,
11386    placeholder: &'static str,
11387    accessibility_label: &'static str,
11388) {
11389    let anchor = ui.add_child(
11390        parent,
11391        UiNode::container(
11392            format!("{name}.anchor"),
11393            LayoutStyle::new()
11394                .with_width(width)
11395                .with_height(30.0)
11396                .with_flex_shrink(0.0),
11397        ),
11398    );
11399    let nodes = ext_widgets::dropdown_select(
11400        ui,
11401        anchor,
11402        name,
11403        options,
11404        state,
11405        Some(select_popup(
11406            UiRect::new(0.0, 0.0, width, 30.0),
11407            UiRect::new(0.0, 0.0, width + 48.0, 240.0),
11408        )),
11409        dropdown_select_options(width, name, placeholder, accessibility_label),
11410    );
11411    ui.node_mut(nodes.trigger)
11412        .set_action(format!("{name}.toggle"));
11413}
11414
11415fn shader_lab_selected_material(state: &ShowcaseState) -> ElementMaterial {
11416    let shape = state.shader_lab_material_shape.shape();
11417    let mut material = ElementMaterial::new()
11418        .with_paint_outset(LayoutInsets::points(
11419            state
11420                .shader_lab_material_outset
11421                .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11422        ))
11423        .with_clip_shape(shape.clone())
11424        .with_hit_shape(shape)
11425        .with_geometry_effect(state.shader_lab_material_geometry.effect());
11426    if let Some(shader) = shader_lab_material_shader_effect(state) {
11427        material = material.with_shader(shader);
11428    }
11429    material
11430}
11431
11432fn shader_lab_material_shader_effect(state: &ShowcaseState) -> Option<ShaderEffect> {
11433    let phase = state.progress_phase.rem_euclid(1.0);
11434    match state.shader_lab_material_shader {
11435        ShaderLabMaterialShader::None => None,
11436        ShaderLabMaterialShader::Tint => Some(ShaderEffect::tint(color(255, 196, 92), 0.62)),
11437        ShaderLabMaterialShader::Shine => Some(ShaderEffect::shine(phase, 0.92)),
11438        ShaderLabMaterialShader::Glow => Some(ShaderEffect::glow(
11439            color(100, 180, 255),
11440            0.95,
11441            state
11442                .shader_lab_material_outset
11443                .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11444        )),
11445        ShaderLabMaterialShader::Plasma => {
11446            Some(ShaderEffect::plasma(phase, color(82, 190, 255), 0.75, 12.0))
11447        }
11448        ShaderLabMaterialShader::Rings => {
11449            Some(ShaderEffect::rings(phase, color(232, 170, 88), 0.78, 11.0))
11450        }
11451        ShaderLabMaterialShader::Grid => {
11452            Some(ShaderEffect::grid(phase, color(156, 132, 255), 0.85, 9.0))
11453        }
11454    }
11455}
11456
11457fn shader_lab_material_visual(shape: ShaderLabMaterialShape) -> UiVisual {
11458    UiVisual::panel(
11459        color(39, 71, 114),
11460        Some(StrokeStyle::new(color(168, 205, 255), 1.0)),
11461        shape.visual_radius(),
11462    )
11463}
11464
11465fn shader_lab_material_chip(
11466    ui: &mut UiDocument,
11467    parent: UiNodeId,
11468    name: &'static str,
11469    label: &'static str,
11470    material: ElementMaterial,
11471    visual: UiVisual,
11472) {
11473    let chip = ui.add_child(
11474        parent,
11475        UiNode::container(
11476            name,
11477            LayoutStyle::row()
11478                .with_width(156.0)
11479                .with_height(44.0)
11480                .with_padding(8.0)
11481                .with_align_items(taffy::prelude::AlignItems::Center)
11482                .with_justify_content(taffy::prelude::JustifyContent::Center)
11483                .with_flex_shrink(0.0),
11484        )
11485        .with_visual(visual)
11486        .with_material(material)
11487        .with_accessibility(
11488            AccessibilityMeta::new(AccessibilityRole::Image).label(format!("{label} material")),
11489        ),
11490    );
11491    widgets::label(
11492        ui,
11493        chip,
11494        format!("{name}.text"),
11495        label,
11496        text(11.0, color(246, 249, 252)),
11497        LayoutStyle::new().with_flex_shrink(0.0),
11498    );
11499}
11500
11501fn shader_lab_option_checkbox(
11502    ui: &mut UiDocument,
11503    parent: UiNodeId,
11504    name: &'static str,
11505    label: &'static str,
11506    checked: bool,
11507) {
11508    let mut options = widgets::CheckboxOptions::default()
11509        .with_action(name)
11510        .with_text_style(text(12.0, color(220, 228, 238)));
11511    options.layout = LayoutStyle::new()
11512        .with_width(112.0)
11513        .with_height(24.0)
11514        .with_flex_shrink(0.0);
11515    widgets::checkbox(ui, parent, name, label, checked, options);
11516}
11517
11518fn shader_lab_slider_control(
11519    ui: &mut UiDocument,
11520    parent: UiNodeId,
11521    name: &'static str,
11522    label: &'static str,
11523    value: f32,
11524    max: f32,
11525    decimals: usize,
11526) {
11527    let control = ui.add_child(
11528        parent,
11529        UiNode::container(
11530            format!("{name}.control"),
11531            Layout::row()
11532                .size(LayoutSize::new(
11533                    LayoutDimension::points(214.0),
11534                    LayoutDimension::Auto,
11535                ))
11536                .align_items(LayoutAlignment::Center)
11537                .gap(LayoutGap::points(6.0, 6.0))
11538                .flex(0.0, 0.0, LayoutDimension::Auto)
11539                .to_layout_style(),
11540        ),
11541    );
11542    widgets::label(
11543        ui,
11544        control,
11545        format!("{name}.label"),
11546        label,
11547        text(12.0, color(166, 176, 190)),
11548        LayoutStyle::new()
11549            .with_width(46.0)
11550            .with_height(22.0)
11551            .with_flex_shrink(0.0),
11552    );
11553    let mut options = widgets::SliderOptions::default()
11554        .with_layout(
11555            LayoutStyle::new()
11556                .with_width(96.0)
11557                .with_height(22.0)
11558                .with_flex_shrink(0.0),
11559        )
11560        .with_value_edit_action(name);
11561    options.accessibility_label = Some(format!("Shader lab {label}"));
11562    widgets::slider(
11563        ui,
11564        control,
11565        format!("{name}.slider"),
11566        (value / max.max(f32::EPSILON)).clamp(0.0, 1.0),
11567        0.0..1.0,
11568        options,
11569    );
11570    widgets::label(
11571        ui,
11572        control,
11573        format!("{name}.value"),
11574        format!("{value:.decimals$}px"),
11575        text(12.0, color(226, 232, 242)),
11576        LayoutStyle::new()
11577            .with_width(48.0)
11578            .with_height(22.0)
11579            .with_flex_shrink(0.0),
11580    );
11581}
11582
11583fn shader_lab_surface_stroke(state: &ShowcaseState) -> Option<StrokeStyle> {
11584    (state.shader_lab_surface_stroke_width > f32::EPSILON).then(|| {
11585        StrokeStyle::new(
11586            color(150, 180, 235),
11587            state
11588                .shader_lab_surface_stroke_width
11589                .clamp(0.0, SHADER_LAB_SURFACE_STROKE_MAX),
11590        )
11591    })
11592}
11593
11594fn shader_lab_surface_radius(state: &ShowcaseState) -> f32 {
11595    state
11596        .shader_lab_surface_radius
11597        .clamp(0.0, SHADER_LAB_SURFACE_RADIUS_MAX)
11598}
11599
11600fn shader_lab_editor_column(
11601    ui: &mut UiDocument,
11602    parent: UiNodeId,
11603    state: &ShowcaseState,
11604    source_error: Option<&str>,
11605) {
11606    let editor_column = ui.add_child(
11607        parent,
11608        UiNode::container(
11609            "shader_lab.editor.column",
11610            LayoutStyle::column()
11611                .with_width_percent(1.0)
11612                .with_height_percent(1.0)
11613                .with_gap(8.0)
11614                .with_flex_shrink(1.0),
11615        ),
11616    );
11617    let preset_row = row(ui, editor_column, "shader_lab.preset.row", 8.0);
11618    widgets::label(
11619        ui,
11620        preset_row,
11621        "shader_lab.preset.caption",
11622        "Program",
11623        text(12.0, color(166, 176, 190)),
11624        LayoutStyle::new()
11625            .with_width(62.0)
11626            .with_height(30.0)
11627            .with_flex_shrink(0.0),
11628    );
11629    shader_lab_dropdown_select(
11630        ui,
11631        preset_row,
11632        "shader_lab.preset",
11633        &shader_lab_preset_options(),
11634        &state.shader_lab_preset_menu,
11635        180.0,
11636        "WGSL preset",
11637        "Shader lab WGSL preset",
11638    );
11639
11640    let editor_frame = ui.add_child(
11641        editor_column,
11642        UiNode::container(
11643            "shader_lab.editor.frame",
11644            Layout::column()
11645                .size(LayoutSize::new(
11646                    LayoutDimension::percent(1.0),
11647                    LayoutDimension::points(0.0),
11648                ))
11649                .min_size(LayoutSize::points(0.0, SHADER_LAB_EDITOR_HEIGHT))
11650                .flex(1.0, 1.0, LayoutDimension::points(0.0))
11651                .to_layout_style(),
11652        )
11653        .with_visual(UiVisual::panel(
11654            color(18, 22, 28),
11655            Some(StrokeStyle::new(color(72, 84, 104), 1.0)),
11656            4.0,
11657        )),
11658    );
11659    let editor_scroll = widgets::scroll_area(
11660        ui,
11661        editor_frame,
11662        "shader_lab.editor.scroll",
11663        ScrollAxes::BOTH,
11664        LayoutStyle::column()
11665            .with_width_percent(1.0)
11666            .with_height_percent(1.0),
11667    );
11668    ui.node_mut(editor_scroll)
11669        .set_action("shader_lab.editor.scroll");
11670    if let Some(scroll) = ui.node_mut(editor_scroll).scroll_mut() {
11671        scroll.set_offset(state.shader_lab_editor_scroll);
11672    }
11673
11674    let mut code_options = state.text_edit_options(FocusedTextInput::ShaderLabSource);
11675    code_options.edit_action = Some("shader_lab.editor.edit".into());
11676    code_options.visual = UiVisual::TRANSPARENT;
11677    code_options.focused_visual = Some(UiVisual::TRANSPARENT);
11678    code_options.disabled_visual = Some(UiVisual::TRANSPARENT);
11679    widgets::code_editor(
11680        ui,
11681        editor_scroll,
11682        "shader_lab.editor",
11683        &state.shader_lab_source,
11684        code_options,
11685    );
11686    let (validation_text, validation_color) = shader_lab_validation_label(source_error);
11687    widgets::label(
11688        ui,
11689        editor_column,
11690        "shader_lab.validation",
11691        validation_text,
11692        text(11.0, validation_color),
11693        LayoutStyle::new().with_width_percent(1.0),
11694    );
11695}
11696
11697fn shader_lab_editor_content_size(source: &str) -> UiSize {
11698    let style = widgets::code_text_style();
11699    let line_count = source.lines().count().max(1) as f32;
11700    let longest_line = source
11701        .lines()
11702        .map(|line| line.chars().count())
11703        .max()
11704        .unwrap_or(0)
11705        .max(48) as f32;
11706    UiSize::new(
11707        (longest_line * style.font_size * 0.56 + 24.0).max(SHADER_LAB_EDITOR_WIDTH),
11708        (line_count * style.line_height + 18.0).max(SHADER_LAB_EDITOR_HEIGHT),
11709    )
11710}
11711
11712fn shader_lab_preview(
11713    ui: &mut UiDocument,
11714    parent: UiNodeId,
11715    state: &ShowcaseState,
11716    source_valid: bool,
11717) {
11718    match state.shader_lab_target {
11719        ShaderLabTarget::Canvas => {
11720            let mut options = widgets::CanvasOptions::default()
11721                .with_layout(
11722                    LayoutStyle::new()
11723                        .with_width_percent(1.0)
11724                        .with_height_percent(1.0)
11725                        .with_flex_grow(1.0),
11726                )
11727                .with_intrinsic_size(UiSize::new(
11728                    SHADER_LAB_PREVIEW_WIDTH - 20.0,
11729                    SHADER_LAB_PREVIEW_HEIGHT,
11730                ))
11731                .with_accessibility_label("Shader lab canvas preview");
11732            options.visual = UiVisual::panel(color(8, 12, 18), None, 4.0);
11733            widgets::canvas(
11734                ui,
11735                parent,
11736                "shader_lab.preview.canvas",
11737                CanvasContent::new("shader_lab.preview.canvas")
11738                    .program(shader_lab_canvas_program(state, source_valid)),
11739                options,
11740            );
11741        }
11742        ShaderLabTarget::Frame => {
11743            let mut frame_node = UiNode::container(
11744                "shader_lab.preview.frame",
11745                operad::layout::with_min_size(
11746                    LayoutStyle::column()
11747                        .with_width_percent(0.82)
11748                        .with_height_percent(0.62)
11749                        .with_padding(14.0)
11750                        .with_align_items(taffy::prelude::AlignItems::Center)
11751                        .with_justify_content(taffy::prelude::JustifyContent::Center)
11752                        .with_flex_shrink(0.0),
11753                    operad::layout::px(SHADER_LAB_FRAME_MIN_WIDTH),
11754                    operad::layout::px(SHADER_LAB_FRAME_MIN_HEIGHT),
11755                ),
11756            )
11757            .with_visual(UiVisual::panel(
11758                ColorRgba::new(0, 0, 0, 0),
11759                shader_lab_surface_stroke(state),
11760                shader_lab_surface_radius(state),
11761            ));
11762            frame_node.style_mut().set_clip(ClipBehavior::Clip);
11763            let frame = ui.add_child(parent, frame_node);
11764            shader_lab_canvas_layer_fill(
11765                ui,
11766                frame,
11767                "shader_lab.preview.frame.shader",
11768                state,
11769                source_valid,
11770            );
11771            if state.shader_lab_show_frame_text {
11772                let label = widgets::label(
11773                    ui,
11774                    frame,
11775                    "shader_lab.preview.frame.label",
11776                    "WGSL frame",
11777                    text(14.0, color(246, 249, 252)),
11778                    LayoutStyle::new().with_flex_shrink(0.0),
11779                );
11780                ui.node_mut(label).style_mut().set_z_index(1);
11781            }
11782        }
11783        ShaderLabTarget::Button => {
11784            let mut shell_node = UiNode::container(
11785                "shader_lab.preview.button.shell",
11786                LayoutStyle::column()
11787                    .with_width(SHADER_LAB_BUTTON_WIDTH)
11788                    .with_height(SHADER_LAB_BUTTON_HEIGHT)
11789                    .with_align_items(taffy::prelude::AlignItems::Center)
11790                    .with_justify_content(taffy::prelude::JustifyContent::Center),
11791            )
11792            .with_visual(UiVisual::TRANSPARENT);
11793            shell_node.style_mut().set_clip(ClipBehavior::Clip);
11794            let shell = ui.add_child(parent, shell_node);
11795            shader_lab_canvas_layer_fill(
11796                ui,
11797                shell,
11798                "shader_lab.preview.button.shader",
11799                state,
11800                source_valid,
11801            );
11802            let mut options = widgets::ButtonOptions::new(
11803                LayoutStyle::new()
11804                    .with_width(SHADER_LAB_BUTTON_WIDTH)
11805                    .with_height(SHADER_LAB_BUTTON_HEIGHT),
11806            )
11807            .with_action("shader_lab.preview.button")
11808            .with_accessibility_label("Shader button preview");
11809            options.visual = UiVisual::panel(
11810                ColorRgba::new(0, 0, 0, 0),
11811                shader_lab_surface_stroke(state),
11812                shader_lab_surface_radius(state),
11813            );
11814            options.hovered_visual = Some(UiVisual::panel(
11815                ColorRgba::new(255, 255, 255, 28),
11816                shader_lab_surface_stroke(state),
11817                shader_lab_surface_radius(state),
11818            ));
11819            options.pressed_visual = Some(UiVisual::panel(
11820                ColorRgba::new(0, 0, 0, 48),
11821                shader_lab_surface_stroke(state),
11822                shader_lab_surface_radius(state),
11823            ));
11824            options.text_style = text(14.0, color(246, 249, 252));
11825            let button = widgets::button(
11826                ui,
11827                shell,
11828                "shader_lab.preview.button",
11829                if state.shader_lab_show_button_text {
11830                    "Shader button"
11831                } else {
11832                    ""
11833                },
11834                options,
11835            );
11836            ui.node_mut(button).style_mut().set_z_index(1);
11837        }
11838    }
11839}
11840
11841fn shader_lab_canvas_layer_fill(
11842    ui: &mut UiDocument,
11843    parent: UiNodeId,
11844    name: &'static str,
11845    state: &ShowcaseState,
11846    source_valid: bool,
11847) -> UiNodeId {
11848    let layout = operad::layout::with_absolute_position(
11849        LayoutStyle::new()
11850            .with_width_percent(1.0)
11851            .with_height_percent(1.0),
11852        0.0,
11853        0.0,
11854    );
11855    let mut options = widgets::CanvasOptions::default()
11856        .with_layout(layout)
11857        .with_intrinsic_size(UiSize::new(
11858            SHADER_LAB_PREVIEW_WIDTH,
11859            SHADER_LAB_PREVIEW_HEIGHT,
11860        ))
11861        .with_accessibility_label(format!("{name} shader preview"));
11862    options.input = InputBehavior::NONE;
11863    options.visual = UiVisual::TRANSPARENT;
11864    let canvas = widgets::canvas(
11865        ui,
11866        parent,
11867        name,
11868        CanvasContent::new(name).program(shader_lab_canvas_program(state, source_valid)),
11869        options,
11870    );
11871    ui.node_mut(canvas).style_mut().set_z_index(0);
11872    canvas
11873}
11874
11875fn shader_lab_status_label(state: &ShowcaseState, source_error: Option<&str>) -> Option<String> {
11876    if source_error.is_some() {
11877        Some(format!(
11878            "{} fallback until WGSL validates",
11879            state.shader_lab_target.label()
11880        ))
11881    } else {
11882        None
11883    }
11884}
11885
11886fn shader_lab_validation_label(source_error: Option<&str>) -> (String, ColorRgba) {
11887    if let Some(error) = source_error {
11888        (
11889            format!("WGSL error: {}", compact_shader_error(error, 160)),
11890            color(255, 139, 128),
11891        )
11892    } else {
11893        ("WGSL valid".to_string(), color(112, 221, 160))
11894    }
11895}
11896
11897fn compact_shader_error(error: &str, max_chars: usize) -> String {
11898    let mut compact = error.split_whitespace().collect::<Vec<_>>().join(" ");
11899    if compact.chars().count() > max_chars {
11900        compact = compact.chars().take(max_chars.saturating_sub(3)).collect();
11901        compact.push_str("...");
11902    }
11903    compact
11904}
11905
11906fn shader_lab_canvas_program(state: &ShowcaseState, source_valid: bool) -> CanvasRenderProgram {
11907    let source = if source_valid {
11908        state.shader_lab_source.text().to_string()
11909    } else {
11910        SHADER_LAB_ERROR_WGSL.to_string()
11911    };
11912    CanvasRenderProgram::wgsl(source)
11913        .label("showcase.shader_lab.canvas")
11914        .constant("TIME", state.progress_phase as f64)
11915        .clear_color(Some(color(8, 12, 18)))
11916}
11917
11918fn shader_lab_source_error(source: &str) -> Option<String> {
11919    if !shader_lab_source_has_entry_points(source) {
11920        return Some("source must define @vertex fn vs_main and @fragment fn fs_main".to_string());
11921    }
11922
11923    shader_lab_compile_error(source)
11924}
11925
11926#[cfg(feature = "wgpu")]
11927fn shader_lab_compile_error(source: &str) -> Option<String> {
11928    let module = match naga::front::wgsl::parse_str(source) {
11929        Ok(module) => module,
11930        Err(error) => return Some(error.emit_to_string(source)),
11931    };
11932    let mut validator = naga::valid::Validator::new(
11933        naga::valid::ValidationFlags::all(),
11934        naga::valid::Capabilities::empty(),
11935    );
11936    validator
11937        .validate(&module)
11938        .err()
11939        .map(|error| error.to_string())
11940}
11941
11942#[cfg(not(feature = "wgpu"))]
11943fn shader_lab_compile_error(_source: &str) -> Option<String> {
11944    None
11945}
11946
11947fn shader_lab_source_has_entry_points(source: &str) -> bool {
11948    source.contains("@vertex")
11949        && source.contains("fn vs_main")
11950        && source.contains("@fragment")
11951        && source.contains("fn fs_main")
11952}
11953
11954fn timeline_ruler(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11955    let body =
11956        section_with_min_viewport(ui, parent, "timeline", "Timeline", UiSize::new(560.0, 0.0));
11957    widgets::label(
11958        ui,
11959        body,
11960        "timeline.label",
11961        "Clip timeline",
11962        text(12.0, color(166, 176, 190)),
11963        LayoutStyle::new().with_width_percent(1.0),
11964    );
11965    widgets::label(
11966        ui,
11967        body,
11968        "timeline.description",
11969        "The ruler maps time to tracks, clips, markers, and the current playhead.",
11970        text(12.0, color(196, 210, 230)),
11971        LayoutStyle::new().with_width_percent(1.0),
11972    );
11973
11974    let editor = row(ui, body, "timeline.editor", 0.0);
11975    let labels = ui.add_child(
11976        editor,
11977        UiNode::container(
11978            "timeline.lane_labels",
11979            LayoutStyle::column()
11980                .with_width(96.0)
11981                .with_height(172.0)
11982                .with_flex_shrink(0.0),
11983        ),
11984    );
11985    for (name, label, height) in [
11986        ("timeline.lane_labels.header", "Tracks", 40.0),
11987        ("timeline.lane_labels.video", "Video", 44.0),
11988        ("timeline.lane_labels.audio", "Audio", 44.0),
11989        ("timeline.lane_labels.notes", "Notes", 44.0),
11990    ] {
11991        widgets::label(
11992            ui,
11993            labels,
11994            name,
11995            label,
11996            text(11.0, color(166, 176, 190)),
11997            LayoutStyle::new()
11998                .with_width_percent(1.0)
11999                .with_height(height)
12000                .with_flex_shrink(0.0),
12001        );
12002    }
12003
12004    let timeline_scroll = timeline_scroll_state_for_view(
12005        state.timeline_scroll,
12006        state.timeline_scroll.viewport_size().width,
12007    );
12008    let range = ext_widgets::TimelineRange::new(0.0, 48.0);
12009    let nodes = scroll_area_widgets::scroll_container_shell(
12010        ui,
12011        editor,
12012        "timeline",
12013        timeline_scroll,
12014        widgets::ScrollContainerOptions::default()
12015            .with_axes(ScrollAxes::HORIZONTAL)
12016            .with_action_prefix("timeline")
12017            .with_gap(4.0)
12018            .with_scrollbar_thickness(TIMELINE_SCROLLBAR_HEIGHT)
12019            .with_layout(
12020                LayoutStyle::column()
12021                    .with_width(0.0)
12022                    .with_flex_grow(1.0)
12023                    .with_height(TIMELINE_SCROLL_CONTAINER_HEIGHT)
12024                    .with_flex_shrink(0.0),
12025            )
12026            .with_viewport_layout(
12027                LayoutStyle::column()
12028                    .with_width(0.0)
12029                    .with_flex_grow(1.0)
12030                    .with_height(TIMELINE_VIEWPORT_HEIGHT)
12031                    .with_flex_shrink(1.0),
12032            )
12033            .with_horizontal_scrollbar(
12034                scrollbar_widgets::ScrollbarOptions::default()
12035                    .with_action("timeline.horizontal-scrollbar"),
12036            )
12037            .with_accessibility_label("Timeline horizontal scroller"),
12038    );
12039    let content = ui.add_child(
12040        nodes.viewport,
12041        UiNode::container(
12042            "timeline.content",
12043            LayoutStyle::column()
12044                .with_width(TIMELINE_CONTENT_WIDTH)
12045                .with_height(TIMELINE_VIEWPORT_HEIGHT)
12046                .with_flex_shrink(0.0),
12047        ),
12048    );
12049    let mut ruler_options = ext_widgets::TimelineRulerOptions::default();
12050    ruler_options.height = 40.0;
12051    ruler_options.layout = LayoutStyle::new()
12052        .with_width(TIMELINE_CONTENT_WIDTH)
12053        .with_height(40.0)
12054        .with_flex_shrink(0.0);
12055    ruler_options.accessibility_label = Some("Editing timeline ruler".to_string());
12056    ruler_options.accessibility_hint =
12057        Some("Shows seconds for the visible timeline clips".to_string());
12058    ext_widgets::timeline_ruler(
12059        ui,
12060        content,
12061        "timeline.ruler",
12062        ext_widgets::RulerSpec {
12063            range,
12064            width: TIMELINE_CONTENT_WIDTH,
12065            major_step: 4.0,
12066            minor_step: 1.0,
12067            label_every: 1,
12068        },
12069        ruler_options,
12070    );
12071    ui.add_child(
12072        content,
12073        UiNode::scene(
12074            "timeline.tracks",
12075            timeline_track_primitives(range, TIMELINE_CONTENT_WIDTH),
12076            LayoutStyle::new()
12077                .with_width(TIMELINE_CONTENT_WIDTH)
12078                .with_height(132.0)
12079                .with_flex_shrink(0.0),
12080        ),
12081    );
12082}
12083
12084fn timeline_track_primitives(range: ext_widgets::TimelineRange, width: f32) -> Vec<ScenePrimitive> {
12085    let mut primitives = Vec::new();
12086    let lane_height = 36.0;
12087    let lane_gap = 8.0;
12088    let lanes = [
12089        ("Video", 0.0, color(16, 22, 30)),
12090        ("Audio", lane_height + lane_gap, color(13, 20, 27)),
12091        ("Notes", (lane_height + lane_gap) * 2.0, color(16, 22, 30)),
12092    ];
12093
12094    for (label, y, fill) in lanes {
12095        primitives.push(ScenePrimitive::Rect(
12096            PaintRect::solid(UiRect::new(0.0, y, width, lane_height), fill)
12097                .stroke(AlignedStroke::inside(StrokeStyle::new(
12098                    color(38, 49, 64),
12099                    1.0,
12100                )))
12101                .corner_radii(CornerRadii::uniform(2.0)),
12102        ));
12103        primitives.push(ScenePrimitive::Text(
12104            PaintText::new(
12105                label,
12106                UiRect::new(8.0, y + 8.0, 72.0, 18.0),
12107                text(10.0, color(116, 128, 145)),
12108            )
12109            .multiline(false),
12110        ));
12111    }
12112
12113    for second in (0..=48).step_by(4) {
12114        let x = range.value_to_x(second as f64, width);
12115        primitives.push(ScenePrimitive::Line {
12116            from: UiPoint::new(x, 0.0),
12117            to: UiPoint::new(x, 124.0),
12118            stroke: StrokeStyle::new(color(34, 44, 58), 1.0),
12119        });
12120    }
12121
12122    push_timeline_clip(
12123        &mut primitives,
12124        range,
12125        width,
12126        "Intro",
12127        2.0,
12128        10.0,
12129        0.0,
12130        color(57, 126, 207),
12131    );
12132    push_timeline_clip(
12133        &mut primitives,
12134        range,
12135        width,
12136        "Cutaway",
12137        12.0,
12138        24.0,
12139        0.0,
12140        color(95, 107, 212),
12141    );
12142    push_timeline_clip(
12143        &mut primitives,
12144        range,
12145        width,
12146        "Final",
12147        28.0,
12148        44.0,
12149        0.0,
12150        color(68, 153, 122),
12151    );
12152    push_timeline_clip(
12153        &mut primitives,
12154        range,
12155        width,
12156        "Music bed",
12157        0.0,
12158        48.0,
12159        lane_height + lane_gap,
12160        color(205, 160, 71),
12161    );
12162    push_timeline_clip(
12163        &mut primitives,
12164        range,
12165        width,
12166        "Voiceover",
12167        8.0,
12168        18.0,
12169        lane_height + lane_gap,
12170        color(183, 107, 185),
12171    );
12172
12173    for (second, label) in [(6.0, "Beat"), (21.0, "Cut"), (37.0, "Cue")] {
12174        let x = range.value_to_x(second, width);
12175        let y = (lane_height + lane_gap) * 2.0 + 8.0;
12176        primitives.push(ScenePrimitive::Polygon {
12177            points: vec![
12178                UiPoint::new(x, y),
12179                UiPoint::new(x + 8.0, y + 8.0),
12180                UiPoint::new(x, y + 16.0),
12181                UiPoint::new(x - 8.0, y + 8.0),
12182            ],
12183            fill: color(245, 198, 83),
12184            stroke: Some(StrokeStyle::new(color(255, 234, 178), 1.0)),
12185        });
12186        primitives.push(ScenePrimitive::Text(
12187            PaintText::new(
12188                label,
12189                UiRect::new(x + 12.0, y - 1.0, 72.0, 18.0),
12190                text(10.0, color(225, 233, 244)),
12191            )
12192            .multiline(false),
12193        ));
12194    }
12195
12196    let playhead_x = range.value_to_x(18.5, width);
12197    primitives.push(ScenePrimitive::Line {
12198        from: UiPoint::new(playhead_x, 0.0),
12199        to: UiPoint::new(playhead_x, 124.0),
12200        stroke: StrokeStyle::new(ColorRgba::new(255, 120, 96, 255), 2.0),
12201    });
12202    primitives.push(ScenePrimitive::Text(
12203        PaintText::new(
12204            "Playhead 18.5s",
12205            UiRect::new(playhead_x + 8.0, 106.0, 120.0, 18.0),
12206            text(10.0, ColorRgba::new(255, 172, 154, 255)),
12207        )
12208        .multiline(false),
12209    ));
12210
12211    primitives
12212}
12213
12214#[allow(clippy::too_many_arguments)]
12215fn push_timeline_clip(
12216    primitives: &mut Vec<ScenePrimitive>,
12217    range: ext_widgets::TimelineRange,
12218    width: f32,
12219    label: &'static str,
12220    start: f64,
12221    end: f64,
12222    lane_y: f32,
12223    fill: ColorRgba,
12224) {
12225    let x = range.value_to_x(start, width);
12226    let right = range.value_to_x(end, width);
12227    let rect = UiRect::new(x, lane_y + 6.0, (right - x).max(1.0), 24.0);
12228    primitives.push(ScenePrimitive::Rect(
12229        PaintRect::solid(rect, fill)
12230            .stroke(AlignedStroke::inside(StrokeStyle::new(
12231                ColorRgba::new(230, 240, 255, 96),
12232                1.0,
12233            )))
12234            .corner_radii(CornerRadii::uniform(4.0)),
12235    ));
12236    primitives.push(ScenePrimitive::Text(
12237        PaintText::new(label, rect, text(10.0, color(245, 248, 252)))
12238            .horizontal_align(TextHorizontalAlign::Center)
12239            .vertical_align(TextVerticalAlign::Center)
12240            .multiline(false),
12241    ));
12242}
12243
12244fn theme_demo_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState, theme: &Theme) {
12245    let body = section(ui, parent, "theme", "Theme");
12246    widgets::label(
12247        ui,
12248        body,
12249        "theme.current",
12250        format!("Current theme: {}", theme.name),
12251        themed_text(theme, 14.0),
12252        LayoutStyle::new().with_width_percent(1.0),
12253    );
12254
12255    let choices = wrapping_row(ui, body, "theme.choices", 8.0);
12256    for choice in [
12257        ShowcaseThemeChoice::Light,
12258        ShowcaseThemeChoice::Dark,
12259        ShowcaseThemeChoice::Bubblegum,
12260    ] {
12261        theme_choice_button(
12262            ui,
12263            choices,
12264            choice,
12265            state.showcase_theme == choice,
12266            choice.theme(),
12267        );
12268    }
12269
12270    let swatches = wrapping_row(ui, body, "theme.swatches", 8.0);
12271    theme_swatch(
12272        ui,
12273        swatches,
12274        "theme.swatch.canvas",
12275        "Canvas",
12276        theme.colors.canvas,
12277        theme,
12278    );
12279    theme_swatch(
12280        ui,
12281        swatches,
12282        "theme.swatch.surface",
12283        "Surface",
12284        theme.colors.surface,
12285        theme,
12286    );
12287    theme_swatch(
12288        ui,
12289        swatches,
12290        "theme.swatch.accent",
12291        "Accent",
12292        theme.colors.accent,
12293        theme,
12294    );
12295    theme_swatch(
12296        ui,
12297        swatches,
12298        "theme.swatch.selected",
12299        "Selected",
12300        theme.colors.selected,
12301        theme,
12302    );
12303
12304    let preview = ui.add_child(
12305        body,
12306        UiNode::container(
12307            "theme.preview",
12308            LayoutStyle::column()
12309                .with_width_percent(1.0)
12310                .with_padding(12.0)
12311                .with_gap(10.0)
12312                .with_flex_shrink(0.0),
12313        )
12314        .with_visual(UiVisual::panel(
12315            theme.colors.surface,
12316            Some(theme.stroke.surface),
12317            theme.radius.md,
12318        ))
12319        .with_accessibility(
12320            AccessibilityMeta::new(AccessibilityRole::Group).label("Theme preview"),
12321        ),
12322    );
12323    widgets::label(
12324        ui,
12325        preview,
12326        "theme.preview.title",
12327        "Preview controls",
12328        themed_text(theme, 13.0),
12329        LayoutStyle::new().with_width_percent(1.0),
12330    );
12331    let preview_row = row(ui, preview, "theme.preview.controls", 8.0);
12332    let mut primary = themed_button_options(
12333        theme,
12334        "theme.preview.primary",
12335        ComponentState::ACTIVE,
12336        LayoutStyle::new().with_height(34.0),
12337    );
12338    primary.accessibility_label = Some("Primary preview button".to_owned());
12339    widgets::button(ui, preview_row, "theme.preview.primary", "Primary", primary);
12340    let mut secondary = themed_button_options(
12341        theme,
12342        "theme.preview.secondary",
12343        ComponentState::NORMAL,
12344        LayoutStyle::new().with_height(34.0),
12345    );
12346    secondary.accessibility_label = Some("Secondary preview button".to_owned());
12347    widgets::button(
12348        ui,
12349        preview_row,
12350        "theme.preview.secondary",
12351        "Secondary",
12352        secondary,
12353    );
12354    let mut help = themed_muted_text(theme, 12.0);
12355    help.wrap = TextWrap::WordOrGlyph;
12356    widgets::label(
12357        ui,
12358        preview,
12359        "theme.preview.copy",
12360        "The selected theme drives the app background, right panel, floating windows, and this preview.",
12361        help,
12362        LayoutStyle::new().with_width_percent(1.0),
12363    );
12364}
12365
12366fn theme_choice_button(
12367    ui: &mut UiDocument,
12368    parent: UiNodeId,
12369    choice: ShowcaseThemeChoice,
12370    selected: bool,
12371    preview_theme: Theme,
12372) {
12373    let mut options = themed_button_options(
12374        &preview_theme,
12375        choice.action(),
12376        if selected {
12377            ComponentState::SELECTED
12378        } else {
12379            ComponentState::NORMAL
12380        },
12381        LayoutStyle::new()
12382            .with_width(116.0)
12383            .with_height(34.0)
12384            .with_flex_shrink(0.0),
12385    )
12386    .with_action(choice.action());
12387    options.accessibility_label = Some(format!("Use {} theme", choice.label()));
12388    widgets::button(
12389        ui,
12390        parent,
12391        format!("theme.choice.{}", choice.label().to_ascii_lowercase()),
12392        choice.label(),
12393        options,
12394    );
12395}
12396
12397fn theme_swatch(
12398    ui: &mut UiDocument,
12399    parent: UiNodeId,
12400    name: &'static str,
12401    label: &'static str,
12402    swatch_color: ColorRgba,
12403    theme: &Theme,
12404) {
12405    let tile = ui.add_child(
12406        parent,
12407        UiNode::container(
12408            name,
12409            LayoutStyle::column()
12410                .with_width(92.0)
12411                .with_height(76.0)
12412                .with_padding(8.0)
12413                .with_gap(6.0)
12414                .with_flex_shrink(0.0),
12415        )
12416        .with_visual(UiVisual::panel(
12417            theme.colors.surface_muted,
12418            Some(theme.stroke.surface),
12419            4.0,
12420        ))
12421        .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(label)),
12422    );
12423    ui.add_child(
12424        tile,
12425        UiNode::container(
12426            format!("{name}.color"),
12427            LayoutStyle::new()
12428                .with_width_percent(1.0)
12429                .with_height(26.0)
12430                .with_flex_shrink(0.0),
12431        )
12432        .with_visual(UiVisual::panel(
12433            swatch_color,
12434            Some(StrokeStyle::new(theme.colors.border_strong, 1.0)),
12435            4.0,
12436        )),
12437    );
12438    widgets::label(
12439        ui,
12440        tile,
12441        format!("{name}.label"),
12442        label,
12443        themed_muted_text(theme, 11.0),
12444        LayoutStyle::new().with_width_percent(1.0),
12445    );
12446}
12447
12448fn themed_button_options(
12449    theme: &Theme,
12450    action: impl Into<String>,
12451    state: ComponentState,
12452    layout: LayoutStyle,
12453) -> widgets::ButtonOptions {
12454    let mut options = widgets::ButtonOptions::new(layout).with_action(action.into());
12455    options.visual = theme.resolve_visual(ComponentRole::Button, state);
12456    options.hovered_visual =
12457        Some(theme.resolve_visual(ComponentRole::Button, ComponentState::HOVERED));
12458    options.pressed_visual =
12459        Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
12460    options.pressed_hovered_visual =
12461        Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
12462    options.focused_visual =
12463        Some(theme.resolve_visual(ComponentRole::Button, ComponentState::FOCUSED));
12464    options.disabled_visual =
12465        Some(theme.resolve_visual(ComponentRole::Button, ComponentState::DISABLED));
12466    options.text_style = theme.resolve_text(ComponentRole::Button, state);
12467    options
12468}
12469
12470fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12471    let preview_scene_size = style_preview_scene_size(state.styling);
12472    let preview_min_width = preview_scene_size.width + 16.0;
12473    let preview_min_height = preview_scene_size.height + 16.0;
12474    let body_min_width = STYLING_CONTROLS_WIDTH + 1.0 + preview_min_width + 20.0;
12475    let body = section_with_min_viewport(
12476        ui,
12477        parent,
12478        "styling",
12479        "Styling",
12480        UiSize::new(body_min_width, preview_min_height),
12481    );
12482    let grid_layout = operad::layout::with_grid_template_columns(
12483        Layout::grid()
12484            .size(LayoutSize::percent(1.0, 1.0))
12485            .gap(LayoutGap::points(10.0, 10.0))
12486            .to_layout_style(),
12487        [
12488            LayoutGridTrack::points(STYLING_CONTROLS_WIDTH),
12489            LayoutGridTrack::points(1.0),
12490            LayoutGridTrack::minmax_points_fraction(preview_min_width, 1.0),
12491        ],
12492    );
12493    let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
12494    let controls = ui.add_child(
12495        grid,
12496        UiNode::container(
12497            "styling.controls",
12498            LayoutStyle::column()
12499                .with_width(STYLING_CONTROLS_WIDTH)
12500                .with_height_percent(1.0)
12501                .with_flex_shrink(0.0)
12502                .gap(6.0),
12503        ),
12504    );
12505    style_edge_group(
12506        ui,
12507        controls,
12508        "styling.inner",
12509        "Inner margin",
12510        "styling.inner_same",
12511        state.styling.inner_same,
12512        [
12513            ("Left", "styling.inner", state.styling.inner_margin),
12514            ("Right", "styling.inner_right", state.styling.inner_right),
12515            ("Top", "styling.inner_top", state.styling.inner_top),
12516            ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
12517        ],
12518        0.0..32.0,
12519    );
12520    style_edge_group(
12521        ui,
12522        controls,
12523        "styling.outer",
12524        "Outer margin",
12525        "styling.outer_same",
12526        state.styling.outer_same,
12527        [
12528            ("Left", "styling.outer", state.styling.outer_margin),
12529            ("Right", "styling.outer_right", state.styling.outer_right),
12530            ("Top", "styling.outer_top", state.styling.outer_top),
12531            ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
12532        ],
12533        0.0..40.0,
12534    );
12535    style_edge_group(
12536        ui,
12537        controls,
12538        "styling.radius",
12539        "Corner radius",
12540        "styling.radius_same",
12541        state.styling.radius_same,
12542        [
12543            ("NW", "styling.radius", state.styling.corner_radius),
12544            ("NE", "styling.radius_ne", state.styling.corner_ne),
12545            ("SW", "styling.radius_sw", state.styling.corner_sw),
12546            ("SE", "styling.radius_se", state.styling.corner_se),
12547        ],
12548        0.0..28.0,
12549    );
12550    style_fill_group(ui, controls, state);
12551    style_stroke_group(ui, controls, state);
12552    style_shadow_group(ui, controls, state);
12553    widgets::separator(
12554        ui,
12555        grid,
12556        "styling.preview.separator",
12557        widgets::SeparatorOptions::vertical().with_layout(
12558            LayoutStyle::new()
12559                .with_width(1.0)
12560                .with_height_percent(1.0)
12561                .with_flex_shrink(0.0),
12562        ),
12563    );
12564
12565    let preview = ui.add_child(
12566        grid,
12567        UiNode::container(
12568            "styling.preview",
12569            operad::layout::with_min_size(
12570                LayoutStyle::column()
12571                    .with_width_percent(1.0)
12572                    .with_height_percent(1.0)
12573                    .with_flex_shrink(0.0)
12574                    .padding(8.0),
12575                operad::layout::px(preview_min_width),
12576                operad::layout::px(preview_min_height),
12577            ),
12578        )
12579        .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
12580    );
12581    style_preview(ui, preview, state.styling);
12582}
12583
12584#[allow(clippy::too_many_arguments)]
12585fn style_edge_group(
12586    ui: &mut UiDocument,
12587    parent: UiNodeId,
12588    name: &'static str,
12589    title: &'static str,
12590    same_action: &'static str,
12591    same: bool,
12592    values: [(&'static str, &'static str, f32); 4],
12593    range: std::ops::Range<f32>,
12594) {
12595    let group = style_control_group(ui, parent, format!("{name}.group"));
12596    style_group_title(ui, group, format!("{name}.title"), title);
12597    let fields = ui.add_child(
12598        group,
12599        UiNode::container(
12600            format!("{name}.fields"),
12601            LayoutStyle::column()
12602                .with_width(138.0)
12603                .with_flex_shrink(0.0)
12604                .gap(3.0),
12605        ),
12606    );
12607    style_compact_checkbox(ui, fields, same_action, "same", same);
12608    if same {
12609        style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
12610    } else {
12611        for (label, action, value) in values {
12612            style_number_row(ui, fields, action, label, value, range.clone(), 0);
12613        }
12614    }
12615}
12616
12617fn style_fill_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12618    let group = style_control_group(ui, parent, "styling.fill.group");
12619    style_group_title(ui, group, "styling.fill.title", "Fill");
12620    let fields = style_group_fields(
12621        ui,
12622        group,
12623        "styling.fill.fields",
12624        STYLING_WIDE_FIELDS_WIDTH,
12625        4.0,
12626    );
12627    style_color_button_row(
12628        ui,
12629        fields,
12630        "styling.fill_color_button",
12631        "",
12632        state.styling.fill_color(),
12633        "Pick fill color",
12634    );
12635    if state.styling_fill_picker_open {
12636        ext_widgets::color_picker(
12637            ui,
12638            fields,
12639            "styling.fill_picker",
12640            &state.styling_fill_picker,
12641            ext_widgets::ColorPickerOptions::default()
12642                .with_label("Fill")
12643                .with_action_prefix("styling.fill_picker"),
12644        );
12645    }
12646}
12647
12648fn style_stroke_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12649    let group = style_control_group(ui, parent, "styling.stroke.group");
12650    style_group_title(ui, group, "styling.stroke.title", "Stroke");
12651    let fields = style_group_fields(
12652        ui,
12653        group,
12654        "styling.stroke.fields",
12655        STYLING_WIDE_FIELDS_WIDTH,
12656        4.0,
12657    );
12658    let width_row = row(ui, fields, "styling.stroke.row", 6.0);
12659    style_inline_number(
12660        ui,
12661        width_row,
12662        "styling.stroke",
12663        "width",
12664        state.styling.stroke_width,
12665        0.0..STYLING_STROKE_MAX,
12666        1,
12667    );
12668    let mut options = widgets::SliderOptions::default()
12669        .with_layout(
12670            LayoutStyle::new()
12671                .with_width(60.0)
12672                .with_height(20.0)
12673                .with_flex_shrink(0.0),
12674        )
12675        .with_value_edit_action("styling.stroke");
12676    options.fill_color = color(120, 170, 230);
12677    widgets::slider(
12678        ui,
12679        width_row,
12680        "styling.stroke.slider",
12681        (state.styling.stroke_width / STYLING_STROKE_MAX).clamp(0.0, 1.0),
12682        0.0..1.0,
12683        options,
12684    );
12685    style_color_button_row(
12686        ui,
12687        fields,
12688        "styling.stroke_color_button",
12689        "",
12690        state.styling.stroke_color(),
12691        "Pick stroke color",
12692    );
12693    if state.styling_stroke_picker_open {
12694        ext_widgets::color_picker(
12695            ui,
12696            fields,
12697            "styling.stroke_picker",
12698            &state.styling_stroke_picker,
12699            ext_widgets::ColorPickerOptions::default()
12700                .with_label("Stroke color")
12701                .with_action_prefix("styling.stroke_picker"),
12702        );
12703    }
12704}
12705
12706fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12707    let group = style_control_group(ui, parent, "styling.shadow.group");
12708    style_group_title(ui, group, "styling.shadow.title", "Shadow");
12709    let fields = style_group_fields(
12710        ui,
12711        group,
12712        "styling.shadow.fields",
12713        STYLING_WIDE_FIELDS_WIDTH,
12714        4.0,
12715    );
12716    let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
12717    style_inline_number(
12718        ui,
12719        offsets,
12720        "styling.shadow_x",
12721        "x",
12722        state.styling.shadow_x,
12723        -24.0..24.0,
12724        0,
12725    );
12726    style_inline_number(
12727        ui,
12728        offsets,
12729        "styling.shadow_y",
12730        "y",
12731        state.styling.shadow_y,
12732        -24.0..24.0,
12733        0,
12734    );
12735    let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
12736    style_inline_number(
12737        ui,
12738        spread,
12739        "styling.shadow",
12740        "blur",
12741        state.styling.shadow_blur,
12742        0.0..32.0,
12743        0,
12744    );
12745    style_inline_number(
12746        ui,
12747        spread,
12748        "styling.shadow_spread",
12749        "spread",
12750        state.styling.shadow_spread,
12751        0.0..16.0,
12752        0,
12753    );
12754    style_color_button_row(
12755        ui,
12756        fields,
12757        "styling.shadow_color_button",
12758        "",
12759        state.styling.shadow_color(),
12760        "Pick shadow color",
12761    );
12762    if state.styling_shadow_picker_open {
12763        ext_widgets::color_picker(
12764            ui,
12765            fields,
12766            "styling.shadow_picker",
12767            &state.styling_shadow_picker,
12768            ext_widgets::ColorPickerOptions::default()
12769                .with_label("Shadow color")
12770                .with_action_prefix("styling.shadow_picker"),
12771        );
12772    }
12773}
12774
12775fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
12776    ui.add_child(
12777        parent,
12778        UiNode::container(
12779            name,
12780            LayoutStyle::row()
12781                .with_width_percent(1.0)
12782                .with_flex_shrink(0.0)
12783                .padding(4.0)
12784                .gap(8.0),
12785        )
12786        .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
12787    )
12788}
12789
12790fn style_group_fields(
12791    ui: &mut UiDocument,
12792    parent: UiNodeId,
12793    name: impl Into<String>,
12794    width: f32,
12795    gap: f32,
12796) -> UiNodeId {
12797    ui.add_child(
12798        parent,
12799        UiNode::container(
12800            name,
12801            LayoutStyle::column()
12802                .with_width(width)
12803                .with_flex_shrink(0.0)
12804                .gap(gap),
12805        ),
12806    )
12807}
12808
12809fn style_group_title(
12810    ui: &mut UiDocument,
12811    parent: UiNodeId,
12812    name: impl Into<String>,
12813    label: &'static str,
12814) {
12815    widgets::label(
12816        ui,
12817        parent,
12818        name,
12819        label,
12820        text(12.0, color(166, 176, 190)),
12821        LayoutStyle::new()
12822            .with_width(88.0)
12823            .with_flex_shrink(0.0)
12824            .with_height(22.0),
12825    );
12826}
12827
12828fn style_color_button_row(
12829    ui: &mut UiDocument,
12830    parent: UiNodeId,
12831    action: &'static str,
12832    label: &'static str,
12833    value: ColorRgba,
12834    accessibility_label: &'static str,
12835) {
12836    let row = row(ui, parent, format!("{action}.row"), 8.0);
12837    if !label.is_empty() {
12838        widgets::label(
12839            ui,
12840            row,
12841            format!("{action}.label"),
12842            label,
12843            text(12.0, color(166, 176, 190)),
12844            LayoutStyle::new()
12845                .with_width(86.0)
12846                .with_flex_shrink(0.0)
12847                .with_height(24.0),
12848        );
12849    }
12850    ext_widgets::color_edit_button(
12851        ui,
12852        row,
12853        action,
12854        value,
12855        color_mini_button_options(action)
12856            .with_format(ext_widgets::ColorValueFormat::Rgba)
12857            .accessibility_label(accessibility_label),
12858    );
12859    widgets::label(
12860        ui,
12861        row,
12862        format!("{action}.value"),
12863        ext_widgets::color_picker::format_hex_color(value, value.a < 255),
12864        text(12.0, color(226, 232, 242)),
12865        LayoutStyle::new().with_width(96.0).with_height(24.0),
12866    );
12867}
12868
12869fn style_number_row(
12870    ui: &mut UiDocument,
12871    parent: UiNodeId,
12872    name: &'static str,
12873    label: &'static str,
12874    value: f32,
12875    range: std::ops::Range<f32>,
12876    decimals: u8,
12877) {
12878    let row = row(ui, parent, format!("{name}.row"), 6.0);
12879    widgets::label(
12880        ui,
12881        row,
12882        format!("{name}.label"),
12883        label,
12884        text(12.0, color(166, 176, 190)),
12885        LayoutStyle::new().with_width(48.0).with_height(22.0),
12886    );
12887    style_value_input(ui, row, name, value, range, decimals);
12888}
12889
12890fn style_inline_number(
12891    ui: &mut UiDocument,
12892    parent: UiNodeId,
12893    name: &'static str,
12894    label: &'static str,
12895    value: f32,
12896    range: std::ops::Range<f32>,
12897    decimals: u8,
12898) {
12899    let row = compact_row(ui, parent, format!("{name}.inline"), 3.0);
12900    widgets::label(
12901        ui,
12902        row,
12903        format!("{name}.inline_label"),
12904        format!("{label}:"),
12905        text(12.0, color(166, 176, 190)),
12906        LayoutStyle::new()
12907            .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
12908            .with_height(22.0),
12909    );
12910    style_value_input(ui, row, name, value, range, decimals);
12911}
12912
12913fn style_value_input(
12914    ui: &mut UiDocument,
12915    parent: UiNodeId,
12916    name: &'static str,
12917    value: f32,
12918    range: std::ops::Range<f32>,
12919    decimals: u8,
12920) {
12921    let mut options = widgets::DragValueOptions::default()
12922        .with_layout(
12923            LayoutStyle::row()
12924                .with_width(STYLING_VALUE_INPUT_WIDTH)
12925                .with_height(22.0)
12926                .with_flex_shrink(0.0)
12927                .with_align_items(taffy::prelude::AlignItems::Center)
12928                .with_justify_content(taffy::prelude::JustifyContent::Center)
12929                .with_padding(4.0),
12930        )
12931        .with_range(ext_widgets::NumericRange::new(
12932            f64::from(range.start),
12933            f64::from(range.end),
12934        ))
12935        .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
12936        .with_action(name);
12937    options.text_style = text(12.0, color(226, 232, 242));
12938    widgets::drag_value_input(ui, parent, name, f64::from(value), options);
12939}
12940
12941fn style_compact_checkbox(
12942    ui: &mut UiDocument,
12943    parent: UiNodeId,
12944    name: &'static str,
12945    label: &'static str,
12946    checked: bool,
12947) {
12948    let mut options = widgets::CheckboxOptions::default().with_action(name);
12949    options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
12950    options.text_style = text(12.0, color(220, 228, 238));
12951    widgets::checkbox(ui, parent, name, label, checked, options);
12952}
12953
12954fn compact_row(
12955    ui: &mut UiDocument,
12956    parent: UiNodeId,
12957    name: impl Into<String>,
12958    gap: f32,
12959) -> UiNodeId {
12960    ui.add_child(
12961        parent,
12962        UiNode::container(
12963            name,
12964            LayoutStyle::row()
12965                .with_height(22.0)
12966                .with_flex_shrink(0.0)
12967                .with_align_items(taffy::prelude::AlignItems::Center)
12968                .gap(gap),
12969        ),
12970    )
12971}
12972
12973fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
12974    ext_widgets::ColorButtonOptions::default()
12975        .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
12976        .with_swatch_size(UiSize::new(22.0, 18.0))
12977        .with_action(action)
12978        .show_label(false)
12979}
12980
12981fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
12982    let (frame, text_rect) = style_preview_rects(styling);
12983    let scene_size = style_preview_scene_size(styling);
12984    ui.add_child(
12985        parent,
12986        UiNode::scene(
12987            "styling.preview.scene",
12988            vec![
12989                ScenePrimitive::Rect(
12990                    PaintRect::solid(frame, styling.fill_color())
12991                        .stroke(AlignedStroke::inside(StrokeStyle::new(
12992                            styling.stroke_color(),
12993                            styling.stroke_width,
12994                        )))
12995                        .corner_radii(styling.radii())
12996                        .effect(PaintEffect::shadow(
12997                            styling.shadow_color(),
12998                            UiPoint::new(styling.shadow_x, styling.shadow_y),
12999                            styling.shadow_blur,
13000                            styling.shadow_spread,
13001                        )),
13002                ),
13003                ScenePrimitive::Text(
13004                    PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
13005                        .horizontal_align(TextHorizontalAlign::Center)
13006                        .vertical_align(TextVerticalAlign::Center)
13007                        .multiline(false),
13008                ),
13009            ],
13010            operad::layout::with_min_size(
13011                LayoutStyle::new()
13012                    .with_width_percent(1.0)
13013                    .with_height(180.0)
13014                    .with_flex_shrink(0.0),
13015                operad::layout::px(scene_size.width),
13016                operad::layout::px(scene_size.height),
13017            ),
13018        ),
13019    );
13020}
13021
13022fn style_preview_rects(styling: StylingState) -> (UiRect, UiRect) {
13023    let outer = styling.outer_edges();
13024    let inner = styling.inner_edges();
13025    let frame = UiRect::new(
13026        22.0 + outer[0],
13027        28.0 + outer[2],
13028        108.0 + inner[0] + inner[1],
13029        40.0 + inner[2] + inner[3],
13030    );
13031    let text_rect = UiRect::new(
13032        frame.x + inner[0],
13033        frame.y + inner[2],
13034        (frame.width - inner[0] - inner[1]).max(1.0),
13035        (frame.height - inner[2] - inner[3]).max(1.0),
13036    );
13037    (frame, text_rect)
13038}
13039
13040fn style_preview_scene_size(styling: StylingState) -> UiSize {
13041    let (frame, text_rect) = style_preview_rects(styling);
13042    let shadow_outset = styling.shadow_blur.max(0.0) + styling.shadow_spread.max(0.0);
13043    let shadow_bounds = UiRect::new(
13044        frame.x + styling.shadow_x - shadow_outset,
13045        frame.y + styling.shadow_y - shadow_outset,
13046        frame.width + shadow_outset * 2.0,
13047        frame.height + shadow_outset * 2.0,
13048    );
13049    let right = frame
13050        .right()
13051        .max(text_rect.right())
13052        .max(shadow_bounds.right());
13053    let bottom = frame
13054        .bottom()
13055        .max(text_rect.bottom())
13056        .max(shadow_bounds.bottom())
13057        .max(180.0);
13058    UiSize::new(right.ceil().max(1.0), bottom.ceil().max(1.0))
13059}
13060
13061fn slider_options(state: &ShowcaseState, width: f32) -> widgets::SliderOptions {
13062    let mut options = widgets::SliderOptions::default().with_layout(
13063        LayoutStyle::new()
13064            .with_width(width)
13065            .with_height(24.0)
13066            .with_flex_shrink(0.0),
13067    );
13068    options.fill_color = if state.slider_trailing_color {
13069        state.slider_trailing_picker.value()
13070    } else {
13071        color(42, 49, 58)
13072    };
13073    options.thumb_shape = match state.slider_thumb_shape {
13074        SliderThumbChoice::Circle => widgets::slider::SliderThumbShape::Circle,
13075        SliderThumbChoice::Square => widgets::slider::SliderThumbShape::Square,
13076        SliderThumbChoice::Rectangle => widgets::slider::SliderThumbShape::Rectangle,
13077    };
13078    options.thumb_visual = UiVisual::panel(
13079        state.slider_thumb_picker.value(),
13080        Some(StrokeStyle::new(color(79, 93, 113), 1.0)),
13081        6.0,
13082    );
13083    options
13084}
13085
13086#[allow(clippy::field_reassign_with_default)]
13087fn slider_number_input(
13088    ui: &mut UiDocument,
13089    parent: UiNodeId,
13090    name: &'static str,
13091    input: &TextInputState,
13092    focused: FocusedTextInput,
13093    state: &ShowcaseState,
13094    width: f32,
13095) {
13096    let mut options = TextInputOptions::default();
13097    options.layout = LayoutStyle::new().with_width(width).with_height(28.0);
13098    options.text_style = text(12.0, color(230, 236, 246));
13099    options.placeholder_style = text(12.0, color(144, 156, 174));
13100    options.edit_action = Some(format!("{name}.edit").into());
13101    options.focused = state.focused_text == Some(focused);
13102    options.caret_visible = caret_visible(state.caret_phase);
13103    widgets::text_input(ui, parent, name, input, options);
13104}
13105
13106fn form_status_chip(
13107    ui: &mut UiDocument,
13108    parent: UiNodeId,
13109    name: &'static str,
13110    label: &'static str,
13111    active: bool,
13112) {
13113    let chip = ui.add_child(
13114        parent,
13115        UiNode::container(
13116            name,
13117            LayoutStyle::new()
13118                .with_width(82.0)
13119                .with_height(24.0)
13120                .with_padding(4.0)
13121                .with_flex_shrink(0.0),
13122        )
13123        .with_visual(UiVisual::panel(
13124            if active {
13125                color(35, 74, 54)
13126            } else {
13127                color(28, 34, 43)
13128            },
13129            Some(StrokeStyle::new(
13130                if active {
13131                    color(90, 160, 112)
13132                } else {
13133                    color(60, 72, 88)
13134                },
13135                1.0,
13136            )),
13137            4.0,
13138        )),
13139    );
13140    widgets::label(
13141        ui,
13142        chip,
13143        format!("{name}.label"),
13144        label,
13145        text(11.0, color(218, 228, 240)),
13146        LayoutStyle::new()
13147            .with_width_percent(1.0)
13148            .with_height_percent(1.0),
13149    );
13150}
13151
13152fn profile_form_summary(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13153    let has_errors = widgets::form_has_errors(&state.form);
13154    let title = profile_form_summary_title(state, has_errors);
13155    let detail = format!(
13156        "{} | {} | {}",
13157        profile_summary_value(state.form_name_text.text(), "No name"),
13158        profile_summary_value(state.form_email_text.text(), "No email"),
13159        profile_summary_value(state.form_role_text.text(), "No role"),
13160    );
13161    let hint = profile_form_summary_hint(state, has_errors);
13162    let stroke = if has_errors {
13163        color(196, 94, 104)
13164    } else if state.form.dirty {
13165        color(205, 160, 71)
13166    } else if state.form.submitted {
13167        color(91, 164, 119)
13168    } else {
13169        color(60, 72, 88)
13170    };
13171    let summary = ui.add_child(
13172        parent,
13173        UiNode::container(
13174            "forms.profile.summary",
13175            LayoutStyle::column()
13176                .with_width_percent(1.0)
13177                .with_padding(10.0)
13178                .with_gap(4.0)
13179                .with_flex_shrink(0.0),
13180        )
13181        .with_visual(UiVisual::panel(
13182            color(20, 25, 32),
13183            Some(StrokeStyle::new(stroke, 1.0)),
13184            4.0,
13185        ))
13186        .with_accessibility(
13187            AccessibilityMeta::new(AccessibilityRole::Group)
13188                .label("Live profile summary")
13189                .value(format!("{title}. {detail}. {hint}")),
13190        ),
13191    );
13192    widgets::label(
13193        ui,
13194        summary,
13195        "forms.profile.summary.title",
13196        title,
13197        text(13.0, color(232, 240, 250)),
13198        LayoutStyle::new().with_width_percent(1.0),
13199    );
13200    widgets::label(
13201        ui,
13202        summary,
13203        "forms.profile.summary.detail",
13204        detail,
13205        text(12.0, color(186, 198, 216)),
13206        LayoutStyle::new().with_width_percent(1.0),
13207    );
13208    widgets::label(
13209        ui,
13210        summary,
13211        "forms.profile.summary.hint",
13212        hint,
13213        text(11.0, color(154, 166, 184)),
13214        LayoutStyle::new().with_width_percent(1.0),
13215    );
13216}
13217
13218fn profile_form_summary_title(state: &ShowcaseState, has_errors: bool) -> &'static str {
13219    if has_errors {
13220        "Profile needs fixes"
13221    } else if state.form.submitted {
13222        "Profile submitted"
13223    } else if state.form.dirty {
13224        "Profile draft"
13225    } else {
13226        "Profile saved"
13227    }
13228}
13229
13230fn profile_form_summary_hint(state: &ShowcaseState, has_errors: bool) -> &'static str {
13231    if has_errors {
13232        "Fix validation errors before applying or submitting."
13233    } else if state.form.dirty {
13234        "Apply saves the draft; Submit saves and marks it submitted."
13235    } else if state.form.submitted {
13236        "Submission completed. Apply stays disabled until something changes."
13237    } else {
13238        "No pending changes. Submit marks the saved profile submitted."
13239    }
13240}
13241
13242fn profile_summary_value<'a>(value: &'a str, empty: &'static str) -> &'a str {
13243    let value = value.trim();
13244    if value.is_empty() {
13245        empty
13246    } else {
13247        value
13248    }
13249}
13250
13251#[allow(clippy::field_reassign_with_default)]
13252fn form_text_field(
13253    ui: &mut UiDocument,
13254    parent: UiNodeId,
13255    name: &'static str,
13256    input: &TextInputState,
13257    focused: FocusedTextInput,
13258    state: &ShowcaseState,
13259) {
13260    let mut options = TextInputOptions::default();
13261    options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
13262    options.text_style = text(12.0, color(230, 236, 246));
13263    options.placeholder_style = text(12.0, color(144, 156, 174));
13264    options.placeholder = "Required".to_string();
13265    options.edit_action = Some(format!("{name}.edit").into());
13266    options.focused = state.focused_text == Some(focused);
13267    options.caret_visible = caret_visible(state.caret_phase);
13268    widgets::text_input(ui, parent, name, input, options);
13269}
13270
13271fn profile_email_valid(email: &str) -> bool {
13272    let email = email.trim();
13273    let Some((local, domain)) = email.split_once('@') else {
13274        return false;
13275    };
13276    !local.is_empty() && domain.contains('.') && !domain.ends_with('.')
13277}
13278
13279fn drag_source_layout() -> LayoutStyle {
13280    LayoutStyle::row()
13281        .with_width(128.0)
13282        .with_height(40.0)
13283        .with_padding(8.0)
13284        .with_gap(6.0)
13285        .with_flex_shrink(0.0)
13286}
13287
13288fn drop_zone_layout() -> LayoutStyle {
13289    LayoutStyle::column()
13290        .with_width(128.0)
13291        .with_height(78.0)
13292        .with_padding(10.0)
13293        .with_gap(6.0)
13294        .with_flex_shrink(0.0)
13295}
13296
13297fn dnd_operation_chip(
13298    ui: &mut UiDocument,
13299    parent: UiNodeId,
13300    name: &'static str,
13301    label: &'static str,
13302) {
13303    let chip = ui.add_child(
13304        parent,
13305        UiNode::container(
13306            name,
13307            LayoutStyle::new()
13308                .with_width(58.0)
13309                .with_height(22.0)
13310                .with_padding(3.0)
13311                .with_flex_shrink(0.0),
13312        )
13313        .with_visual(UiVisual::panel(
13314            color(26, 32, 42),
13315            Some(StrokeStyle::new(color(62, 76, 94), 1.0)),
13316            3.0,
13317        )),
13318    );
13319    widgets::label(
13320        ui,
13321        chip,
13322        format!("{name}.label"),
13323        label,
13324        text(11.0, color(190, 204, 222)),
13325        LayoutStyle::new()
13326            .with_width_percent(1.0)
13327            .with_height_percent(1.0),
13328    );
13329}
13330
13331fn media_preview_image_layout() -> LayoutStyle {
13332    LayoutStyle::size(46.0, 46.0).with_flex_shrink(0.0)
13333}
13334
13335fn media_icon_columns(state: &ShowcaseState) -> usize {
13336    let theme = state.app_theme();
13337    let options = showcase_desktop_options(state.last_desktop_size, &theme);
13338    let window_width = state
13339        .desktop
13340        .size("media", default_window_size("media"))
13341        .width;
13342    let content_width = (window_width - options.content_padding * 2.0).max(MEDIA_ICON_TILE_WIDTH);
13343    let pitch = MEDIA_ICON_TILE_WIDTH + MEDIA_ICON_GRID_GAP;
13344    (((content_width + MEDIA_ICON_GRID_GAP) / pitch).floor() as usize).clamp(1, MEDIA_ICON_COLUMNS)
13345}
13346
13347fn media_icon_grid_width(columns: usize) -> f32 {
13348    let columns = columns.max(1);
13349    columns as f32 * MEDIA_ICON_TILE_WIDTH + columns.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13350}
13351
13352fn media_icon_grid_height(columns: usize, item_count: usize) -> f32 {
13353    let columns = columns.max(1);
13354    let rows = item_count.div_ceil(columns).max(1);
13355    rows as f32 * MEDIA_ICON_TILE_HEIGHT + rows.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13356}
13357
13358fn media_icon_grid(
13359    ui: &mut UiDocument,
13360    parent: UiNodeId,
13361    name: impl Into<String>,
13362    columns: usize,
13363    item_count: usize,
13364) -> UiNodeId {
13365    let columns = columns.clamp(1, MEDIA_ICON_COLUMNS);
13366    let rows = item_count.div_ceil(columns).max(1);
13367    let width = media_icon_grid_width(columns);
13368    let height = media_icon_grid_height(columns, item_count);
13369    let layout = operad::layout::with_grid_template_rows(
13370        operad::layout::with_grid_template_columns(
13371            Layout::grid()
13372                .size(LayoutSize::points(width, height))
13373                .gap(LayoutGap::points(MEDIA_ICON_GRID_GAP, MEDIA_ICON_GRID_GAP))
13374                .flex(0.0, 0.0, LayoutDimension::Auto)
13375                .to_layout_style(),
13376            (0..columns).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_WIDTH)),
13377        ),
13378        (0..rows).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_HEIGHT)),
13379    );
13380    ui.add_child(parent, UiNode::container(name, layout))
13381}
13382
13383fn media_icon_tile(ui: &mut UiDocument, parent: UiNodeId, icon: BuiltInIcon) {
13384    let name = icon.key().replace('.', "_").replace('-', "_");
13385    let tile = ui.add_child(
13386        parent,
13387        UiNode::container(
13388            format!("media.icon_tile.{name}"),
13389            LayoutStyle::column()
13390                .with_width(MEDIA_ICON_TILE_WIDTH)
13391                .with_height(MEDIA_ICON_TILE_HEIGHT)
13392                .with_padding(6.0)
13393                .with_gap(4.0)
13394                .with_flex_shrink(0.0),
13395        )
13396        .with_visual(UiVisual::panel(
13397            color(17, 22, 30),
13398            Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
13399            4.0,
13400        )),
13401    );
13402    widgets::image(
13403        ui,
13404        tile,
13405        format!("media.icon.{name}"),
13406        icon_image(icon),
13407        widgets::ImageOptions::default()
13408            .with_layout(LayoutStyle::size(28.0, 28.0))
13409            .with_accessibility_label(icon.label()),
13410    );
13411    widgets::label(
13412        ui,
13413        tile,
13414        format!("media.icon_label.{name}"),
13415        icon.label(),
13416        text(9.0, color(180, 194, 214)),
13417        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13418    );
13419}
13420
13421fn slider_checkbox(
13422    ui: &mut UiDocument,
13423    parent: UiNodeId,
13424    name: &'static str,
13425    label: &'static str,
13426    checked: bool,
13427) {
13428    slider_checkbox_with_layout(
13429        ui,
13430        parent,
13431        name,
13432        label,
13433        checked,
13434        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13435    );
13436}
13437
13438fn slider_checkbox_with_layout(
13439    ui: &mut UiDocument,
13440    parent: UiNodeId,
13441    name: &'static str,
13442    label: &'static str,
13443    checked: bool,
13444    layout: LayoutStyle,
13445) {
13446    let mut options = widgets::CheckboxOptions::default().with_action(name);
13447    options.layout = layout;
13448    options.text_style = text(12.0, color(220, 228, 238));
13449    widgets::checkbox(ui, parent, name, label, checked, options);
13450}
13451
13452fn choice_button(
13453    ui: &mut UiDocument,
13454    parent: UiNodeId,
13455    name: &'static str,
13456    label: &'static str,
13457    selected: bool,
13458) {
13459    let mut options =
13460        widgets::ButtonOptions::new(LayoutStyle::new().with_width(78.0).with_height(28.0))
13461            .with_action(name);
13462    options.visual = if selected {
13463        button_visual(48, 112, 184)
13464    } else {
13465        button_visual(38, 46, 58)
13466    };
13467    options.hovered_visual = Some(button_visual(65, 86, 106));
13468    options.pressed_visual = Some(button_visual(34, 54, 84));
13469    options.text_style = text(12.0, color(238, 244, 252));
13470    widgets::button(ui, parent, name, label, options);
13471}
13472
13473fn divider(ui: &mut UiDocument, parent: UiNodeId, name: &'static str) {
13474    ui.add_child(
13475        parent,
13476        UiNode::container(
13477            name,
13478            LayoutStyle::new()
13479                .with_width_percent(1.0)
13480                .with_height(1.0)
13481                .with_flex_shrink(0.0),
13482        )
13483        .with_visual(UiVisual::panel(color(48, 58, 72), None, 0.0)),
13484    );
13485}
13486
13487fn canvas(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13488    let canvas_intrinsic = UiSize::new(720.0, 405.0);
13489    let body = section_with_min_viewport(ui, parent, "canvas", "Canvas", UiSize::new(720.0, 458.0));
13490    let controls = wrapping_row(ui, body, "canvas.options", 10.0);
13491    canvas_option_checkbox(
13492        ui,
13493        controls,
13494        "canvas.grow_horizontal",
13495        "Grow width",
13496        state.canvas_grow_horizontal,
13497    );
13498    canvas_option_checkbox(
13499        ui,
13500        controls,
13501        "canvas.grow_vertical",
13502        "Grow height",
13503        state.canvas_grow_vertical,
13504    );
13505    canvas_option_checkbox(
13506        ui,
13507        controls,
13508        "canvas.keep_aspect_ratio",
13509        "Keep aspect ratio",
13510        state.canvas_keep_aspect_ratio,
13511    );
13512
13513    let mut options = widgets::CanvasOptions::default()
13514        .with_accessibility_label("Shader canvas")
13515        .with_action("canvas.rotate")
13516        .with_intrinsic_size(canvas_intrinsic);
13517    options.action_mode = WidgetActionMode::Drag;
13518    if state.canvas_keep_aspect_ratio {
13519        options = options.with_aspect_ratio(16.0 / 9.0);
13520    }
13521    let canvas_width = if state.canvas_grow_horizontal {
13522        LayoutDimension::percent(1.0)
13523    } else {
13524        LayoutDimension::points(canvas_intrinsic.width)
13525    };
13526    let canvas_height = if state.canvas_grow_vertical {
13527        LayoutDimension::percent(1.0)
13528    } else {
13529        LayoutDimension::points(canvas_intrinsic.height)
13530    };
13531    options.layout = Layout::new()
13532        .size(LayoutSize::new(canvas_width, canvas_height))
13533        .min_size(LayoutSize::points(
13534            canvas_intrinsic.width,
13535            canvas_intrinsic.height,
13536        ))
13537        .flex(
13538            if state.canvas_grow_vertical { 1.0 } else { 0.0 },
13539            1.0,
13540            LayoutDimension::Auto,
13541        )
13542        .to_layout_style();
13543    options.visual = UiVisual::panel(
13544        color(18, 22, 28),
13545        Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
13546        4.0,
13547    );
13548    widgets::canvas(
13549        ui,
13550        body,
13551        "canvas.shader",
13552        CanvasContent::new("canvas.shader").program(showcase_canvas_program(state.cube)),
13553        options,
13554    );
13555}
13556
13557fn canvas_option_checkbox(
13558    ui: &mut UiDocument,
13559    parent: UiNodeId,
13560    name: &'static str,
13561    label: &'static str,
13562    checked: bool,
13563) {
13564    let mut options = widgets::CheckboxOptions::default()
13565        .with_action(name)
13566        .with_text_style(text(12.0, color(220, 228, 238)));
13567    options.layout = LayoutStyle::new().with_height(28.0).with_flex_shrink(0.0);
13568    widgets::checkbox(ui, parent, name, label, checked, options);
13569}
13570
13571fn showcase_canvas_program(cube: CanvasCubeState) -> CanvasRenderProgram {
13572    CanvasRenderProgram::wgsl(include_str!("shaders/showcase_canvas.wgsl"))
13573        .label("showcase.canvas")
13574        .constant("CUBE_YAW", cube.yaw as f64)
13575        .constant("CUBE_PITCH", cube.pitch as f64)
13576        .clear_color(Some(color(18, 22, 28)))
13577}
13578
13579fn section(
13580    ui: &mut UiDocument,
13581    parent: UiNodeId,
13582    name: impl Into<String>,
13583    _title: impl Into<String>,
13584) -> UiNodeId {
13585    section_with_min_viewport(ui, parent, name, _title, UiSize::ZERO)
13586}
13587
13588fn section_with_min_viewport(
13589    ui: &mut UiDocument,
13590    parent: UiNodeId,
13591    name: impl Into<String>,
13592    _title: impl Into<String>,
13593    min_viewport_size: UiSize,
13594) -> UiNodeId {
13595    let name = name.into();
13596    let layout = Layout::column()
13597        .size(LayoutSize::percent(1.0, 1.0))
13598        .min_size(LayoutSize::points(
13599            min_viewport_size.width.max(0.0),
13600            min_viewport_size.height.max(0.0),
13601        ))
13602        .gap(LayoutGap::points(10.0, 10.0))
13603        .flex(1.0, 1.0, LayoutDimension::Auto)
13604        .to_layout_style();
13605    widgets::scroll_area(
13606        ui,
13607        parent,
13608        format!("{name}.section_scroll"),
13609        ScrollAxes::BOTH,
13610        layout,
13611    )
13612}
13613
13614fn row(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>, gap: f32) -> UiNodeId {
13615    ui.add_child(
13616        parent,
13617        UiNode::container(
13618            name,
13619            Layout::row()
13620                .size(LayoutSize::new(
13621                    LayoutDimension::percent(1.0),
13622                    LayoutDimension::Auto,
13623                ))
13624                .align_items(LayoutAlignment::Center)
13625                .gap(LayoutGap::points(gap, gap))
13626                .flex(0.0, 0.0, LayoutDimension::Auto)
13627                .to_layout_style(),
13628        ),
13629    )
13630}
13631
13632fn wrapping_row(
13633    ui: &mut UiDocument,
13634    parent: UiNodeId,
13635    name: impl Into<String>,
13636    gap: f32,
13637) -> UiNodeId {
13638    ui.add_child(
13639        parent,
13640        UiNode::container(
13641            name,
13642            Layout::row()
13643                .size(LayoutSize::new(
13644                    LayoutDimension::percent(1.0),
13645                    LayoutDimension::Auto,
13646                ))
13647                .min_size(LayoutSize::points(0.0, 0.0))
13648                .align_items(LayoutAlignment::Center)
13649                .gap(LayoutGap::points(gap, gap))
13650                .flex_wrap(LayoutFlexWrap::Wrap)
13651                .flex(0.0, 0.0, LayoutDimension::Auto)
13652                .to_layout_style(),
13653        ),
13654    )
13655}
Source

pub const fn points(width: f32, height: f32) -> Self

Examples found in repository?
examples/showcase.rs (line 8191)
8163fn list_and_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8164    let body = section_with_min_viewport(
8165        ui,
8166        parent,
8167        "lists_tables",
8168        "Lists and tables",
8169        UiSize::new(520.0, 0.0),
8170    );
8171
8172    let list_row = ui.add_child(
8173        body,
8174        UiNode::container(
8175            "lists_tables.list_row",
8176            Layout::row()
8177                .size(LayoutSize::new(
8178                    LayoutDimension::percent(1.0),
8179                    LayoutDimension::Auto,
8180                ))
8181                .gap(LayoutGap::points(10.0, 10.0))
8182                .flex_wrap(LayoutFlexWrap::Wrap)
8183                .to_layout_style(),
8184        ),
8185    );
8186    let scroll_column = ui.add_child(
8187        list_row,
8188        UiNode::container(
8189            "lists_tables.scroll_area.column",
8190            Layout::column()
8191                .min_size(LayoutSize::points(220.0, 0.0))
8192                .gap(LayoutGap::points(6.0, 6.0))
8193                .flex(1.0, 1.0, LayoutDimension::points(245.0))
8194                .to_layout_style(),
8195        ),
8196    );
8197    widgets::label(
8198        ui,
8199        scroll_column,
8200        "lists_tables.scroll_area.title",
8201        "Scrollable list",
8202        text(12.0, color(166, 176, 190)),
8203        LayoutStyle::new().with_width_percent(1.0),
8204    );
8205    let nested_scroll = widgets::scroll_area(
8206        ui,
8207        scroll_column,
8208        "lists_tables.scroll_area",
8209        ScrollAxes::VERTICAL,
8210        LayoutStyle::column()
8211            .with_width_percent(1.0)
8212            .with_height(104.0),
8213    );
8214    ui.node_mut(nested_scroll)
8215        .set_action("lists_tables.scroll_area.scroll");
8216    if let Some(scroll) = ui.node_mut(nested_scroll).scroll_mut() {
8217        scroll.set_offset(UiPoint::new(0.0, state.list_scroll));
8218    }
8219    for index in 0..6 {
8220        widgets::label(
8221            ui,
8222            nested_scroll,
8223            format!("lists_tables.scroll_area.row.{index}"),
8224            format!("Scroll row {}", index + 1),
8225            text(12.0, color(200, 212, 228)),
8226            LayoutStyle::new()
8227                .with_width_percent(1.0)
8228                .with_height(26.0)
8229                .with_flex_shrink(0.0),
8230        );
8231    }
8232
8233    let virtual_list_column = ui.add_child(
8234        list_row,
8235        UiNode::container(
8236            "lists_tables.virtual_list.column",
8237            Layout::column()
8238                .min_size(LayoutSize::points(220.0, 0.0))
8239                .gap(LayoutGap::points(6.0, 6.0))
8240                .flex(1.0, 1.0, LayoutDimension::points(245.0))
8241                .to_layout_style(),
8242        ),
8243    );
8244
8245    widgets::label(
8246        ui,
8247        virtual_list_column,
8248        "lists_tables.virtual_list.title",
8249        "Virtualized list",
8250        text(12.0, color(166, 176, 190)),
8251        LayoutStyle::new().with_width_percent(1.0),
8252    );
8253    let virtual_list = widgets::virtual_list(
8254        ui,
8255        virtual_list_column,
8256        "lists_tables.virtual_list",
8257        widgets::VirtualListSpec {
8258            row_count: 24,
8259            row_height: 28.0,
8260            viewport_height: 104.0,
8261            scroll_offset: state.virtual_scroll,
8262            overscan: 1,
8263        },
8264        |ui, row_parent, row| {
8265            widgets::label(
8266                ui,
8267                row_parent,
8268                format!("lists_tables.virtual_list.row.{row}"),
8269                format!("Virtual row {}", row + 1),
8270                text(12.0, color(214, 224, 238)),
8271                LayoutStyle::new()
8272                    .with_width_percent(1.0)
8273                    .with_height(28.0)
8274                    .with_flex_shrink(0.0),
8275            );
8276        },
8277    );
8278    ui.node_mut(virtual_list)
8279        .set_action("lists_tables.virtual_list.scroll");
8280
8281    widgets::separator(
8282        ui,
8283        body,
8284        "lists_tables.virtualized_table.separator",
8285        widgets::SeparatorOptions::default(),
8286    );
8287    widgets::label(
8288        ui,
8289        body,
8290        "lists_tables.data_table.title",
8291        "Virtualized selectable table",
8292        text(12.0, color(166, 176, 190)),
8293        LayoutStyle::new().with_width_percent(1.0),
8294    );
8295    let virtual_controls = wrapping_row(ui, body, "lists_tables.virtualized_table.controls", 8.0);
8296    button(
8297        ui,
8298        virtual_controls,
8299        "lists_tables.virtualized_table.sort.name",
8300        if state.virtual_table_descending {
8301            "Name desc"
8302        } else {
8303            "Name asc"
8304        },
8305        "lists_tables.virtualized_table.sort.name",
8306        button_visual(38, 52, 70),
8307    );
8308    button(
8309        ui,
8310        virtual_controls,
8311        "lists_tables.virtualized_table.filter.status",
8312        if state.virtual_table_ready_only {
8313            "Ready only"
8314        } else {
8315            "All status"
8316        },
8317        "lists_tables.virtualized_table.filter.status",
8318        button_visual(38, 52, 70),
8319    );
8320    button(
8321        ui,
8322        virtual_controls,
8323        "lists_tables.virtualized_table.resize.reset",
8324        "Reset width",
8325        "lists_tables.virtualized_table.resize.reset",
8326        button_visual(38, 52, 70),
8327    );
8328
8329    let columns = virtual_table_columns(state);
8330    let visible_rows = virtual_table_visible_rows(state);
8331    let mut table_options = ext_widgets::DataTableOptions::default()
8332        .with_row_action_prefix("lists_tables.virtualized_table")
8333        .with_cell_action_prefix("lists_tables.virtualized_table")
8334        .with_scroll_action("lists_tables.virtualized_table.scroll");
8335    table_options.layout = LayoutStyle::column()
8336        .with_width_percent(1.0)
8337        .with_flex_shrink(0.0);
8338    table_options.header_visual = UiVisual::panel(
8339        color(34, 41, 50),
8340        Some(StrokeStyle::new(color(67, 78, 95), 1.0)),
8341        0.0,
8342    );
8343    table_options.header_text_style = text(12.0, color(222, 230, 240));
8344    table_options.selection = state.table_selection.clone();
8345    ext_widgets::virtualized_data_table(
8346        ui,
8347        body,
8348        "lists_tables.virtualized_table",
8349        &columns,
8350        ext_widgets::VirtualDataTableSpec {
8351            row_count: visible_rows.len(),
8352            row_height: 28.0,
8353            viewport_width: 520.0,
8354            viewport_height: 156.0,
8355            scroll_offset: UiPoint::new(0.0, state.virtual_table_scroll),
8356            overscan_rows: 1,
8357        },
8358        table_options,
8359        |ui, cell_parent, cell| {
8360            let source_row = visible_rows.get(cell.row).copied().unwrap_or(cell.row);
8361            let value = virtual_table_cell_value(source_row, cell.column);
8362            widgets::label(
8363                ui,
8364                cell_parent,
8365                format!(
8366                    "lists_tables.virtualized_table.cell.{}.{}.label",
8367                    cell.row, cell.column
8368                ),
8369                value,
8370                text(12.0, color(220, 228, 238)),
8371                LayoutStyle::new().with_width_percent(1.0),
8372            );
8373        },
8374    );
8375}
8376
8377#[allow(clippy::field_reassign_with_default)]
8378fn property_inspector(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8379    let body = section(ui, parent, "property_inspector", "Property inspector");
8380    widgets::label(
8381        ui,
8382        body,
8383        "property_inspector.target",
8384        "Inspecting: Styling preview",
8385        text(12.0, color(196, 210, 230)),
8386        LayoutStyle::new().with_width_percent(1.0),
8387    );
8388    let mut options = ext_widgets::PropertyInspectorOptions::default();
8389    options.selected_index = Some(0);
8390    options.label_width = 120.0;
8391    options.row_height = 30.0;
8392    ext_widgets::property_inspector_grid(
8393        ui,
8394        body,
8395        "property_inspector.grid",
8396        &[
8397            ext_widgets::PropertyGridRow::new("target", "Widget", "Button preview").read_only(),
8398            ext_widgets::PropertyGridRow::new(
8399                "inner",
8400                "Inner margin",
8401                format!("{:.0}px", state.styling.inner_margin),
8402            )
8403            .with_kind(ext_widgets::PropertyValueKind::Number),
8404            ext_widgets::PropertyGridRow::new(
8405                "outer",
8406                "Outer margin",
8407                format!("{:.0}px", state.styling.outer_margin),
8408            )
8409            .with_kind(ext_widgets::PropertyValueKind::Number),
8410            ext_widgets::PropertyGridRow::new(
8411                "radius",
8412                "Corner radius",
8413                format!("{:.0}px", state.styling.corner_radius),
8414            )
8415            .with_kind(ext_widgets::PropertyValueKind::Number),
8416            ext_widgets::PropertyGridRow::new(
8417                "stroke",
8418                "Stroke",
8419                format!("{:.1}px", state.styling.stroke_width),
8420            )
8421            .with_kind(ext_widgets::PropertyValueKind::Number)
8422            .changed(),
8423            ext_widgets::PropertyGridRow::new("state", "Source", "Styling widget").read_only(),
8424        ],
8425        options,
8426    );
8427}
8428
8429fn diagnostics_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8430    let body = section(ui, parent, "diagnostics", "Diagnostics");
8431    let debug_snapshot = &state.diagnostics_snapshot;
8432
8433    diagnostics_selected_node_panel(ui, body, debug_snapshot);
8434    diagnostics_animation_panel(ui, body, state, debug_snapshot);
8435
8436    widgets::label(
8437        ui,
8438        body,
8439        "diagnostics.a11y.title",
8440        "Accessibility",
8441        text(14.0, color(222, 230, 240)),
8442        LayoutStyle::new().with_width_percent(1.0),
8443    );
8444    let mut overlay_preview_style = UiNodeStyle::from(
8445        LayoutStyle::new()
8446            .with_width(320.0)
8447            .with_height(140.0)
8448            .with_flex_shrink(0.0),
8449    );
8450    overlay_preview_style.set_clip(ClipBehavior::Clip);
8451    let overlay_preview = ui.add_child(
8452        body,
8453        UiNode::container("diagnostics.a11y.preview", overlay_preview_style).with_visual(
8454            UiVisual::panel(
8455                color(12, 17, 24),
8456                Some(StrokeStyle::new(color(47, 62, 82), 1.0)),
8457                4.0,
8458            ),
8459        ),
8460    );
8461    let mut overlay_options = ext_widgets::AccessibilityDebugOverlayOptions {
8462        action_prefix: Some("diagnostics.a11y.visual".to_owned()),
8463        ..Default::default()
8464    };
8465    overlay_options.show_labels = false;
8466    ext_widgets::accessibility_debug_overlay(
8467        ui,
8468        overlay_preview,
8469        "diagnostics.a11y.visual",
8470        &debug_snapshot,
8471        overlay_options,
8472    );
8473    diagnostics_accessibility_details(ui, body, debug_snapshot);
8474
8475    let diagnostic_columns = ui.add_child(
8476        body,
8477        UiNode::container(
8478            "diagnostics.columns",
8479            LayoutStyle::column()
8480                .with_width_percent(1.0)
8481                .with_flex_shrink(0.0)
8482                .gap(10.0),
8483        ),
8484    );
8485    let command_column = ui.add_child(
8486        diagnostic_columns,
8487        UiNode::container(
8488            "diagnostics.commands.column",
8489            LayoutStyle::column()
8490                .with_width_percent(1.0)
8491                .with_flex_shrink(0.0)
8492                .gap(8.0),
8493        ),
8494    );
8495    let theme_column = ui.add_child(
8496        diagnostic_columns,
8497        UiNode::container(
8498            "diagnostics.theme.column",
8499            LayoutStyle::column()
8500                .with_width_percent(1.0)
8501                .with_flex_shrink(0.0)
8502                .gap(8.0),
8503        ),
8504    );
8505
8506    let registry = diagnostics_command_registry();
8507    diagnostics_commands_panel(ui, command_column, &registry);
8508
8509    let theme_snapshot = DebugThemeSnapshot::from_theme(&Theme::dark());
8510    diagnostics_theme_panel(ui, theme_column, &theme_snapshot);
8511}
8512
8513fn diagnostics_selected_node_panel(
8514    ui: &mut UiDocument,
8515    parent: UiNodeId,
8516    snapshot: &DebugInspectorSnapshot,
8517) {
8518    let panel = diagnostics_panel(ui, parent, "diagnostics.inspector", "Selected node");
8519    let rows = snapshot
8520        .node("diagnostics.sample.preview")
8521        .map(|node| {
8522            vec![
8523                ext_widgets::PropertyGridRow::new("name", "Node", "Preview action").read_only(),
8524                ext_widgets::PropertyGridRow::new("role", "Role", "Button").read_only(),
8525                ext_widgets::PropertyGridRow::new(
8526                    "bounds",
8527                    "Bounds",
8528                    format!(
8529                        "{:.0}, {:.0}; {:.0} x {:.0}",
8530                        node.rect.x, node.rect.y, node.rect.width, node.rect.height
8531                    ),
8532                )
8533                .with_kind(ext_widgets::PropertyValueKind::Number)
8534                .read_only(),
8535                ext_widgets::PropertyGridRow::new(
8536                    "clip",
8537                    "Clip",
8538                    format!("{:.0} x {:.0}", node.clip_rect.width, node.clip_rect.height),
8539                )
8540                .with_kind(ext_widgets::PropertyValueKind::Number)
8541                .read_only(),
8542                ext_widgets::PropertyGridRow::new(
8543                    "input",
8544                    "Input",
8545                    if node.input.pointer {
8546                        "Receives pointer input"
8547                    } else {
8548                        "Passive"
8549                    },
8550                )
8551                .read_only(),
8552            ]
8553        })
8554        .unwrap_or_else(|| {
8555            vec![
8556                ext_widgets::PropertyGridRow::new("missing", "Selected node", "No node selected")
8557                    .read_only(),
8558            ]
8559        });
8560    ext_widgets::property_inspector_grid(
8561        ui,
8562        panel,
8563        "diagnostics.inspector.rows",
8564        &rows,
8565        diagnostics_grid_options("Selected node details"),
8566    );
8567}
8568
8569fn diagnostics_animation_panel(
8570    ui: &mut UiDocument,
8571    parent: UiNodeId,
8572    state: &ShowcaseState,
8573    snapshot: &DebugInspectorSnapshot,
8574) {
8575    let graph_panel =
8576        diagnostics_panel(ui, parent, "diagnostics.animation.graph", "Animation state");
8577    if let Some(animation) = snapshot.animation("diagnostics.sample.preview") {
8578        let state_row = row(ui, graph_panel, "diagnostics.animation.graph.states", 8.0);
8579        for state_name in ["idle", "hot"] {
8580            diagnostic_chip(
8581                ui,
8582                state_row,
8583                format!("diagnostics.animation.graph.state.{state_name}"),
8584                state_name,
8585                animation.current_state == state_name,
8586            );
8587        }
8588
8589        let graph = animation.state_graph();
8590        for (index, edge) in graph.edges.iter().take(2).enumerate() {
8591            let value = if edge.kind == DebugAnimationGraphEdgeKind::Blend {
8592                "Input blend"
8593            } else {
8594                "State change"
8595            };
8596            let detail = if edge.label.is_empty() {
8597                if edge.active { "Active" } else { "Inactive" }.to_owned()
8598            } else if edge.active {
8599                format!("{}; active", edge.label)
8600            } else {
8601                edge.label.clone()
8602            };
8603            diagnostic_value_row(
8604                ui,
8605                graph_panel,
8606                format!("diagnostics.animation.graph.edge.{index}"),
8607                value,
8608                format!("{} -> {}", edge.from, edge.to),
8609            );
8610            diagnostic_muted_label(
8611                ui,
8612                graph_panel,
8613                format!("diagnostics.animation.graph.edge.{index}.detail"),
8614                detail,
8615            );
8616        }
8617    } else {
8618        diagnostic_muted_label(
8619            ui,
8620            graph_panel,
8621            "diagnostics.animation.graph.empty",
8622            "No animation state machine",
8623        );
8624    }
8625
8626    let controls_panel = diagnostics_panel(
8627        ui,
8628        parent,
8629        "diagnostics.animation.controls",
8630        "Animation controls",
8631    );
8632    let transport = row(
8633        ui,
8634        controls_panel,
8635        "diagnostics.animation.controls.transport",
8636        8.0,
8637    );
8638    diagnostic_button(
8639        ui,
8640        transport,
8641        "diagnostics.animation.controls.transport.pause_toggle",
8642        if state.diagnostics_animation_paused {
8643            "Resume"
8644        } else {
8645            "Pause"
8646        },
8647        state.diagnostics_animation_paused,
8648    );
8649    diagnostic_button(
8650        ui,
8651        transport,
8652        "diagnostics.animation.controls.transport.step",
8653        "Step",
8654        false,
8655    );
8656    diagnostic_slider_row(
8657        ui,
8658        controls_panel,
8659        "diagnostics.animation.controls.transport.scrub",
8660        "Scrub progress",
8661        state.diagnostics_animation_scrub,
8662        "diagnostics.animation.controls.transport.scrub",
8663    );
8664    diagnostic_button(
8665        ui,
8666        controls_panel,
8667        "diagnostics.animation.controls.input.active.toggle",
8668        if state.diagnostics_animation_active {
8669            "Active input: true"
8670        } else {
8671            "Active input: false"
8672        },
8673        state.diagnostics_animation_active,
8674    );
8675    diagnostic_slider_row(
8676        ui,
8677        controls_panel,
8678        "diagnostics.animation.controls.input.hover.set",
8679        "Hover blend",
8680        state.diagnostics_animation_hover,
8681        "diagnostics.animation.controls.input.hover.set",
8682    );
8683    diagnostic_button(
8684        ui,
8685        controls_panel,
8686        "diagnostics.animation.controls.input.pulse.fire",
8687        "Fire pulse",
8688        false,
8689    );
8690    widgets::label(
8691        ui,
8692        controls_panel,
8693        "diagnostics.animation.controls.status",
8694        format!(
8695            "Scrub {:.0}%   Hover {:.0}%   Pulses {}",
8696            state.diagnostics_animation_scrub * 100.0,
8697            state.diagnostics_animation_hover * 100.0,
8698            state.diagnostics_animation_pulse_count
8699        ),
8700        text(12.0, color(166, 180, 198)),
8701        LayoutStyle::new().with_width_percent(1.0),
8702    );
8703}
8704
8705fn diagnostics_accessibility_details(
8706    ui: &mut UiDocument,
8707    parent: UiNodeId,
8708    snapshot: &DebugInspectorSnapshot,
8709) {
8710    let rows = snapshot
8711        .accessibility_overlay
8712        .iter()
8713        .find(|node| node.name == "diagnostics.sample.preview")
8714        .map(|node| {
8715            let accessibility = node.accessibility.as_ref();
8716            vec![
8717                ext_widgets::PropertyGridRow::new("role", "Role", "Button").read_only(),
8718                ext_widgets::PropertyGridRow::new(
8719                    "label",
8720                    "Label",
8721                    accessibility
8722                        .and_then(|meta| meta.label.clone())
8723                        .unwrap_or_else(|| "Preview action".to_owned()),
8724                )
8725                .read_only(),
8726                ext_widgets::PropertyGridRow::new(
8727                    "focus",
8728                    "Focus order",
8729                    node.focus_index
8730                        .map(|index| format!("#{}", index + 1))
8731                        .unwrap_or_else(|| "Not focusable".to_owned()),
8732                )
8733                .read_only(),
8734                ext_widgets::PropertyGridRow::new(
8735                    "warnings",
8736                    "Warnings",
8737                    if node.warnings.is_empty() {
8738                        "None"
8739                    } else {
8740                        "Needs review"
8741                    },
8742                )
8743                .read_only(),
8744            ]
8745        })
8746        .unwrap_or_else(|| {
8747            vec![
8748                ext_widgets::PropertyGridRow::new("missing", "Accessibility", "No metadata")
8749                    .read_only(),
8750            ]
8751        });
8752    ext_widgets::property_inspector_grid(
8753        ui,
8754        parent,
8755        "diagnostics.a11y",
8756        &rows,
8757        diagnostics_grid_options("Accessibility metadata"),
8758    );
8759}
8760
8761fn diagnostics_commands_panel(ui: &mut UiDocument, parent: UiNodeId, registry: &CommandRegistry) {
8762    let panel = diagnostics_panel(ui, parent, "diagnostics.commands", "Commands");
8763    let formatter = ShortcutFormatter::default();
8764    for command_id in [
8765        "diagnostics.palette",
8766        "diagnostics.inspect",
8767        "diagnostics.record",
8768        "diagnostics.export_theme",
8769    ] {
8770        if let Some(command) = registry.command(command_id) {
8771            let shortcut = registry
8772                .command_bindings(command.meta.id.clone())
8773                .first()
8774                .map(|binding| formatter.format(binding.shortcut))
8775                .unwrap_or_else(|| "Unbound".to_owned());
8776            let status = if command.enabled {
8777                command
8778                    .meta
8779                    .category
8780                    .clone()
8781                    .unwrap_or_else(|| "General".to_owned())
8782            } else {
8783                command
8784                    .disabled_reason
8785                    .clone()
8786                    .unwrap_or_else(|| "Disabled".to_owned())
8787            };
8788            diagnostic_command_row(
8789                ui,
8790                panel,
8791                format!(
8792                    "diagnostics.commands.row.{}",
8793                    command.meta.id.as_str().replace('.', "_")
8794                ),
8795                &command.meta.label,
8796                &shortcut,
8797                &status,
8798            );
8799        }
8800    }
8801    diagnostic_value_row(
8802        ui,
8803        panel,
8804        "diagnostics.commands.conflicts",
8805        "Shortcut conflicts",
8806        if registry.conflicts().is_empty() {
8807            "None"
8808        } else {
8809            "Needs review"
8810        },
8811    );
8812}
8813
8814fn diagnostics_theme_panel(ui: &mut UiDocument, parent: UiNodeId, snapshot: &DebugThemeSnapshot) {
8815    let panel = diagnostics_panel(ui, parent, "diagnostics.theme", "Theme tokens");
8816    diagnostic_value_row(
8817        ui,
8818        panel,
8819        "diagnostics.theme.name",
8820        "Theme",
8821        snapshot.name.as_str(),
8822    );
8823    for token_path in ["colors.accent", "colors.surface", "typography.body"] {
8824        if let Some(token) = snapshot.token(token_path) {
8825            diagnostic_value_row(
8826                ui,
8827                panel,
8828                format!("diagnostics.theme.token.{}", token_path.replace('.', "_")),
8829                token_path,
8830                token.value.as_str(),
8831            );
8832        }
8833    }
8834    if let Some(component) = snapshot.component_states.first() {
8835        diagnostic_value_row(
8836            ui,
8837            panel,
8838            "diagnostics.theme.component.button",
8839            "Button normal",
8840            format!(
8841                "{:.0} x {:.0}, padding {:.0}",
8842                component.min_width, component.min_height, component.padding_x
8843            ),
8844        );
8845    }
8846}
8847
8848fn diagnostics_panel(
8849    ui: &mut UiDocument,
8850    parent: UiNodeId,
8851    name: impl Into<String>,
8852    title: impl Into<String>,
8853) -> UiNodeId {
8854    let name = name.into();
8855    let title = title.into();
8856    let panel = ui.add_child(
8857        parent,
8858        UiNode::container(
8859            name.clone(),
8860            LayoutStyle::column()
8861                .with_width_percent(1.0)
8862                .with_padding(10.0)
8863                .with_gap(8.0)
8864                .with_flex_shrink(0.0),
8865        )
8866        .with_visual(UiVisual::panel(
8867            color(15, 20, 28),
8868            Some(StrokeStyle::new(color(52, 65, 84), 1.0)),
8869            4.0,
8870        ))
8871        .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(title.clone())),
8872    );
8873    widgets::label(
8874        ui,
8875        panel,
8876        format!("{name}.title"),
8877        title,
8878        text(13.0, color(222, 230, 240)),
8879        LayoutStyle::new().with_width_percent(1.0),
8880    );
8881    panel
8882}
8883
8884fn diagnostics_grid_options(label: impl Into<String>) -> ext_widgets::PropertyInspectorOptions {
8885    ext_widgets::PropertyInspectorOptions {
8886        label_width: 112.0,
8887        row_height: 28.0,
8888        accessibility_label: Some(label.into()),
8889        ..Default::default()
8890    }
8891}
8892
8893fn diagnostic_value_row(
8894    ui: &mut UiDocument,
8895    parent: UiNodeId,
8896    name: impl Into<String>,
8897    label: impl Into<String>,
8898    value: impl Into<String>,
8899) -> UiNodeId {
8900    let name = name.into();
8901    let row = row(ui, parent, name.clone(), 8.0);
8902    widgets::label(
8903        ui,
8904        row,
8905        format!("{name}.label"),
8906        label.into(),
8907        text(12.0, color(166, 180, 198)),
8908        LayoutStyle::new().with_width(136.0).with_flex_shrink(0.0),
8909    );
8910    widgets::label(
8911        ui,
8912        row,
8913        format!("{name}.value"),
8914        value.into(),
8915        text(12.0, color(226, 234, 244)),
8916        LayoutStyle::new().with_width_percent(1.0),
8917    );
8918    row
8919}
8920
8921fn diagnostic_muted_label(
8922    ui: &mut UiDocument,
8923    parent: UiNodeId,
8924    name: impl Into<String>,
8925    label: impl Into<String>,
8926) -> UiNodeId {
8927    let mut style = text(12.0, color(166, 180, 198));
8928    style.wrap = TextWrap::WordOrGlyph;
8929    widgets::label(
8930        ui,
8931        parent,
8932        name,
8933        label.into(),
8934        style,
8935        LayoutStyle::new().with_width_percent(1.0),
8936    )
8937}
8938
8939fn diagnostic_command_row(
8940    ui: &mut UiDocument,
8941    parent: UiNodeId,
8942    name: impl Into<String>,
8943    label: &str,
8944    shortcut: &str,
8945    status: &str,
8946) -> UiNodeId {
8947    let name = name.into();
8948    let row = row(ui, parent, name.clone(), 8.0);
8949    widgets::label(
8950        ui,
8951        row,
8952        format!("{name}.label"),
8953        label,
8954        text(12.0, color(226, 234, 244)),
8955        LayoutStyle::new()
8956            .with_width_percent(1.0)
8957            .with_flex_grow(1.0),
8958    );
8959    widgets::label(
8960        ui,
8961        row,
8962        format!("{name}.shortcut"),
8963        shortcut,
8964        text(12.0, color(166, 180, 198)),
8965        LayoutStyle::new().with_width(78.0).with_flex_shrink(0.0),
8966    );
8967    widgets::label(
8968        ui,
8969        row,
8970        format!("{name}.status"),
8971        status,
8972        text(12.0, color(166, 180, 198)),
8973        LayoutStyle::new().with_width(140.0).with_flex_shrink(0.0),
8974    );
8975    row
8976}
8977
8978fn diagnostic_slider_row(
8979    ui: &mut UiDocument,
8980    parent: UiNodeId,
8981    name: impl Into<String>,
8982    label: impl Into<String>,
8983    value: f32,
8984    action: impl Into<String>,
8985) -> UiNodeId {
8986    let name = name.into();
8987    let label = label.into();
8988    let row = row(ui, parent, format!("{name}.row"), 8.0);
8989    widgets::label(
8990        ui,
8991        row,
8992        format!("{name}.label"),
8993        label.clone(),
8994        text(12.0, color(166, 180, 198)),
8995        LayoutStyle::new().with_width(136.0).with_flex_shrink(0.0),
8996    );
8997    let slider_name = if name.ends_with(".set") {
8998        format!("{name}.slider")
8999    } else {
9000        name.clone()
9001    };
9002    let mut options = widgets::SliderOptions::default()
9003        .with_layout(LayoutStyle::new().with_width(160.0).with_height(24.0))
9004        .with_value_edit_action(action.into());
9005    options.accessibility_label = Some(label);
9006    widgets::slider(ui, row, slider_name, value, 0.0..1.0, options);
9007    widgets::label(
9008        ui,
9009        row,
9010        format!("{name}.percent"),
9011        format!("{:.0}%", value * 100.0),
9012        text(12.0, color(226, 234, 244)),
9013        LayoutStyle::new().with_width(46.0).with_flex_shrink(0.0),
9014    );
9015    row
9016}
9017
9018fn diagnostic_button(
9019    ui: &mut UiDocument,
9020    parent: UiNodeId,
9021    name: impl Into<String>,
9022    label: impl Into<String>,
9023    active: bool,
9024) -> UiNodeId {
9025    let name = name.into();
9026    let mut options = widgets::ButtonOptions::default()
9027        .with_layout(LayoutStyle::new().with_height(32.0))
9028        .with_action(name.clone())
9029        .pressed(active);
9030    if active {
9031        options.visual = UiVisual::panel(
9032            color(47, 94, 150),
9033            Some(StrokeStyle::new(color(103, 164, 224), 1.0)),
9034            4.0,
9035        );
9036    }
9037    widgets::button(ui, parent, name, label, options)
9038}
9039
9040fn diagnostic_chip(
9041    ui: &mut UiDocument,
9042    parent: UiNodeId,
9043    name: impl Into<String>,
9044    label: impl Into<String>,
9045    active: bool,
9046) -> UiNodeId {
9047    let name = name.into();
9048    let chip = ui.add_child(
9049        parent,
9050        UiNode::container(
9051            name.clone(),
9052            LayoutStyle::new()
9053                .with_width(82.0)
9054                .with_height(28.0)
9055                .with_padding(4.0)
9056                .with_flex_shrink(0.0),
9057        )
9058        .with_visual(if active {
9059            UiVisual::panel(
9060                color(47, 94, 150),
9061                Some(StrokeStyle::new(color(103, 164, 224), 1.0)),
9062                4.0,
9063            )
9064        } else {
9065            UiVisual::panel(
9066                color(31, 39, 50),
9067                Some(StrokeStyle::new(color(62, 76, 96), 1.0)),
9068                4.0,
9069            )
9070        }),
9071    );
9072    widgets::label(
9073        ui,
9074        chip,
9075        format!("{name}.label"),
9076        label.into(),
9077        text(12.0, color(226, 234, 244)),
9078        LayoutStyle::new().with_width_percent(1.0),
9079    );
9080    chip
9081}
9082
9083fn diagnostics_sample_snapshot(state: &ShowcaseState) -> DebugInspectorSnapshot {
9084    diagnostics_sample_snapshot_for(
9085        state.diagnostics_animation_hover,
9086        state.diagnostics_animation_active,
9087    )
9088}
9089
9090fn diagnostics_sample_snapshot_for(hover: f32, active: bool) -> DebugInspectorSnapshot {
9091    let mut sample = UiDocument::new(root_style(320.0, 180.0));
9092    let card = sample.add_child(
9093        sample.root(),
9094        UiNode::container(
9095            "diagnostics.sample.card",
9096            LayoutStyle::column()
9097                .with_width_percent(1.0)
9098                .with_height(120.0)
9099                .padding(12.0)
9100                .gap(8.0),
9101        )
9102        .with_visual(UiVisual::panel(
9103            color(16, 22, 30),
9104            Some(StrokeStyle::new(color(62, 77, 98), 1.0)),
9105            6.0,
9106        ))
9107        .with_accessibility(
9108            AccessibilityMeta::new(AccessibilityRole::Group).label("Diagnostics sample"),
9109        ),
9110    );
9111    sample.add_child(
9112        card,
9113        UiNode::container(
9114            "diagnostics.sample.preview",
9115            LayoutStyle::new().with_width(160.0).with_height(38.0),
9116        )
9117        .with_input(InputBehavior::BUTTON)
9118        .with_visual(UiVisual::panel(
9119            color(52, 112, 180),
9120            Some(StrokeStyle::new(color(116, 183, 255), 1.0)),
9121            5.0,
9122        ))
9123        .with_accessibility(
9124            AccessibilityMeta::new(AccessibilityRole::Button)
9125                .label("Preview action")
9126                .focusable(),
9127        )
9128        .with_animation(
9129            AnimationMachine::new(
9130                vec![
9131                    AnimationState::new(
9132                        "idle",
9133                        AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0),
9134                    ),
9135                    AnimationState::new(
9136                        "hot",
9137                        AnimatedValues::new(0.92, UiPoint::new(18.0, 0.0), 1.08),
9138                    ),
9139                ],
9140                vec![AnimationTransition::when(
9141                    "idle",
9142                    "hot",
9143                    AnimationCondition::bool("active", true),
9144                    0.18,
9145                )],
9146                "idle",
9147            )
9148            .expect("sample animation")
9149            .with_number_input("hover", hover)
9150            .with_blend_binding(AnimationBlendBinding::new("hover", "idle", "hot"))
9151            .with_bool_input("active", active)
9152            .with_trigger_input("pulse"),
9153        ),
9154    );
9155    widgets::label(
9156        &mut sample,
9157        card,
9158        "diagnostics.sample.label",
9159        "Sample node",
9160        text(12.0, color(198, 210, 226)),
9161        LayoutStyle::new().with_width_percent(1.0),
9162    );
9163    sample
9164        .compute_layout(UiSize::new(320.0, 180.0), &mut ApproxTextMeasurer)
9165        .expect("sample layout");
9166    DebugInspectorSnapshot::from_document(&sample, &mut ApproxTextMeasurer)
9167}
9168
9169fn diagnostics_command_registry() -> CommandRegistry {
9170    let mut registry = CommandRegistry::new();
9171    registry
9172        .register(
9173            CommandMeta::new("diagnostics.palette", "Open command palette")
9174                .description("Show command search")
9175                .category("Debug"),
9176        )
9177        .expect("command");
9178    registry
9179        .register(
9180            CommandMeta::new("diagnostics.inspect", "Inspect selected node")
9181                .description("Focus the layout inspector")
9182                .category("Debug"),
9183        )
9184        .expect("command");
9185    registry
9186        .register(
9187            CommandMeta::new("diagnostics.record", "Start interaction recording")
9188                .description("Capture replay steps")
9189                .category("Testing"),
9190        )
9191        .expect("command");
9192    registry
9193        .register(CommandMeta::new(
9194            "diagnostics.export_theme",
9195            "Export theme patch",
9196        ))
9197        .expect("command");
9198    registry
9199        .bind_shortcut(
9200            CommandScope::Global,
9201            Shortcut::ctrl('k'),
9202            "diagnostics.palette",
9203        )
9204        .expect("shortcut");
9205    registry
9206        .bind_shortcut(
9207            CommandScope::Panel,
9208            Shortcut::ctrl('i'),
9209            "diagnostics.inspect",
9210        )
9211        .expect("shortcut");
9212    registry
9213        .bind_shortcut(
9214            CommandScope::Panel,
9215            Shortcut::ctrl('r'),
9216            "diagnostics.record",
9217        )
9218        .expect("shortcut");
9219    registry
9220        .disable("diagnostics.export_theme", "No changes to export")
9221        .expect("disable");
9222    registry
9223}
9224
9225fn tree_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9226    let body = section(ui, parent, "trees", "Tree view");
9227    widgets::label(
9228        ui,
9229        body,
9230        "trees.tree_view.title",
9231        "Editable tree",
9232        text(12.0, color(166, 176, 190)),
9233        LayoutStyle::new().with_width_percent(1.0),
9234    );
9235    ext_widgets::tree_view(
9236        ui,
9237        body,
9238        "trees.tree_view",
9239        &editable_tree_items(&state.editable_tree),
9240        &state.tree,
9241        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.tree"),
9242    );
9243    widgets::label(
9244        ui,
9245        body,
9246        "trees.editable.status",
9247        &state.editable_tree_status,
9248        text(12.0, color(154, 166, 184)),
9249        LayoutStyle::new().with_width_percent(1.0),
9250    );
9251    widgets::label(
9252        ui,
9253        body,
9254        "trees.virtual.title",
9255        "Virtualized tree",
9256        text(12.0, color(166, 176, 190)),
9257        LayoutStyle::new().with_width_percent(1.0),
9258    );
9259    let virtual_nodes = ext_widgets::virtualized_tree_view(
9260        ui,
9261        body,
9262        "trees.virtual",
9263        &virtual_tree_items(),
9264        &state.tree_virtual,
9265        ext_widgets::VirtualTreeViewSpec::new(24.0, 112.0)
9266            .scroll_offset(state.tree_virtual_scroll)
9267            .overscan_rows(1),
9268        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.virtual"),
9269    );
9270    ui.node_mut(virtual_nodes.body)
9271        .set_action("trees.virtual.scroll");
9272    widgets::label(
9273        ui,
9274        body,
9275        "trees.table.title",
9276        "Tree table",
9277        text(12.0, color(166, 176, 190)),
9278        LayoutStyle::new().with_width_percent(1.0),
9279    );
9280    tree_table_widgets(ui, body, state);
9281}
9282
9283fn tree_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9284    let rows = state.tree_table.visible_items(&tree_table_items());
9285    let columns = [
9286        ext_widgets::DataTableColumn::new("name", "Name", 220.0),
9287        ext_widgets::DataTableColumn::new("kind", "Kind", 84.0),
9288        ext_widgets::DataTableColumn::new("status", "Status", 92.0),
9289    ];
9290    let mut options = ext_widgets::DataTableOptions::default()
9291        .with_row_action_prefix("trees.table")
9292        .with_cell_action_prefix("trees.table")
9293        .with_scroll_action("trees.table.scroll");
9294    options.selection = state
9295        .tree_table
9296        .selected_index()
9297        .map(ext_widgets::DataTableSelection::single_row)
9298        .unwrap_or_default();
9299    options.layout = LayoutStyle::column()
9300        .with_width_percent(1.0)
9301        .with_height(132.0)
9302        .with_flex_shrink(0.0);
9303    ext_widgets::virtualized_data_table(
9304        ui,
9305        parent,
9306        "trees.table",
9307        &columns,
9308        ext_widgets::VirtualDataTableSpec {
9309            row_count: rows.len(),
9310            row_height: 24.0,
9311            viewport_width: 396.0,
9312            viewport_height: 96.0,
9313            scroll_offset: UiPoint::new(0.0, state.tree_table_scroll),
9314            overscan_rows: 1,
9315        },
9316        options,
9317        |ui, cell_parent, cell| {
9318            let Some(item) = rows.get(cell.row) else {
9319                return;
9320            };
9321            if cell.column == 0 {
9322                tree_table_name_cell(ui, cell_parent, cell.row, item);
9323            } else {
9324                widgets::label(
9325                    ui,
9326                    cell_parent,
9327                    format!("trees.table.cell.{}.{}.label", cell.row, cell.column),
9328                    tree_table_cell_value(item, cell.column),
9329                    text(12.0, color(220, 228, 238)),
9330                    LayoutStyle::new().with_width_percent(1.0),
9331                );
9332            }
9333        },
9334    );
9335}
9336
9337fn tree_table_name_cell(
9338    ui: &mut UiDocument,
9339    parent: UiNodeId,
9340    row: usize,
9341    item: &ext_widgets::TreeVisibleItem,
9342) {
9343    if item.depth > 0 {
9344        ui.add_child(
9345            parent,
9346            UiNode::container(
9347                format!("trees.table.row.{}.indent", item.id),
9348                LayoutStyle::new()
9349                    .with_width(item.depth as f32 * 16.0)
9350                    .with_height_percent(1.0)
9351                    .with_flex_shrink(0.0),
9352            ),
9353        );
9354    }
9355    widgets::label(
9356        ui,
9357        parent,
9358        format!("trees.table.row.{}.disclosure", item.id),
9359        if item.has_children() {
9360            if item.expanded {
9361                "v"
9362            } else {
9363                ">"
9364            }
9365        } else {
9366            ""
9367        },
9368        text(12.0, color(166, 176, 190)),
9369        LayoutStyle::new()
9370            .with_width(18.0)
9371            .with_height_percent(1.0)
9372            .with_flex_shrink(0.0),
9373    );
9374    widgets::label(
9375        ui,
9376        parent,
9377        format!("trees.table.cell.{row}.0.label"),
9378        item.label.clone(),
9379        if item.disabled {
9380            text(12.0, color(154, 166, 184))
9381        } else {
9382            text(12.0, color(220, 228, 238))
9383        },
9384        LayoutStyle::new().with_width_percent(1.0),
9385    );
9386}
9387
9388fn tree_table_cell_value(item: &ext_widgets::TreeVisibleItem, column: usize) -> String {
9389    match column {
9390        0 => item.label.clone(),
9391        1 => {
9392            if item.has_children() {
9393                "Folder".to_owned()
9394            } else {
9395                "File".to_owned()
9396            }
9397        }
9398        _ => {
9399            if item.disabled {
9400                "Locked".to_owned()
9401            } else if item.has_children() && item.expanded {
9402                "Expanded".to_owned()
9403            } else if item.has_children() {
9404                "Collapsed".to_owned()
9405            } else if item.expanded {
9406                "Expanded".to_owned()
9407            } else {
9408                "Ready".to_owned()
9409            }
9410        }
9411    }
9412}
9413
9414fn tab_split_dock_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9415    let body = section_with_min_viewport(
9416        ui,
9417        parent,
9418        "layout_widgets",
9419        "Layout widgets",
9420        UiSize::new(640.0, 360.0),
9421    );
9422    let shell = ui.add_child(
9423        body,
9424        UiNode::container(
9425            "layout_widgets.dock_shell",
9426            LayoutStyle::column()
9427                .with_width_percent(1.0)
9428                .with_height(360.0)
9429                .with_flex_shrink(0.0),
9430        )
9431        .with_visual(UiVisual::panel(
9432            color(13, 17, 23),
9433            Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9434            0.0,
9435        )),
9436    );
9437
9438    let mut panels = base_layout_dock_panels();
9439    state.layout_dock.apply_order_to_panels(&mut panels);
9440    state.layout_dock.apply_visibility_to_panels(&mut panels);
9441
9442    let mut drawer_options = ext_widgets::DockDrawerRailOptions::default();
9443    drawer_options.layout = LayoutStyle::row()
9444        .with_width_percent(1.0)
9445        .with_height(34.0)
9446        .with_padding(4.0)
9447        .with_gap(4.0);
9448    ext_widgets::dock_drawer_rail(
9449        ui,
9450        shell,
9451        "layout_widgets.dock.drawers",
9452        &[
9453            ext_widgets::DockDrawerDescriptor::new(
9454                "panel_a",
9455                "Panel A",
9456                "panel_a",
9457                ext_widgets::DockSide::Left,
9458            )
9459            .open(!state.layout_dock.is_hidden("panel_a"))
9460            .with_action("layout_widgets.drawer.panel_a"),
9461            ext_widgets::DockDrawerDescriptor::new(
9462                "panel_b",
9463                "Panel B",
9464                "panel_b",
9465                ext_widgets::DockSide::Right,
9466            )
9467            .open(!state.layout_dock.is_hidden("panel_b"))
9468            .with_action("layout_widgets.drawer.panel_b"),
9469        ],
9470        drawer_options,
9471    );
9472
9473    let mut options = ext_widgets::DockWorkspaceOptions::default();
9474    options.layout = LayoutStyle::column()
9475        .with_width_percent(1.0)
9476        .with_height(0.0)
9477        .with_flex_grow(1.0);
9478    options.show_titles = true;
9479    options.handle_thickness = 2.0;
9480    options.panel_visual = UiVisual::panel(
9481        color(18, 22, 29),
9482        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9483        0.0,
9484    );
9485    options.center_visual = UiVisual::panel(
9486        color(15, 19, 25),
9487        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9488        0.0,
9489    );
9490    options.resize_handle_visual = UiVisual::panel(color(65, 78, 96), None, 0.0);
9491
9492    ext_widgets::dock_workspace(
9493        ui,
9494        shell,
9495        "layout_widgets.dock",
9496        &panels,
9497        options,
9498        |ui, parent, panel| match panel.id.as_str() {
9499            "panel_a" => layout_panel_contents(
9500                ui,
9501                parent,
9502                "layout.panel_a",
9503                state.layout_panel_a_scroll,
9504                &["Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6"],
9505            ),
9506            "workspace" => layout_workspace_contents(
9507                ui,
9508                parent,
9509                "layout.workspace",
9510                state.layout_workspace_scroll,
9511            ),
9512            "panel_b" => layout_panel_contents(
9513                ui,
9514                parent,
9515                "layout.panel_b",
9516                state.layout_panel_b_scroll,
9517                &[
9518                    "Value A", "Value B", "Value C", "Value D", "Value E", "Value F",
9519                ],
9520            ),
9521            _ => {}
9522        },
9523    );
9524
9525    if let Some(floating) = state.layout_dock.floating_panel("panel_a") {
9526        let floating_panel = ui.add_child(
9527            shell,
9528            UiNode::container(
9529                "layout_widgets.floating.panel_a",
9530                operad::layout::absolute(
9531                    floating.rect.x,
9532                    floating.rect.y,
9533                    floating.rect.width,
9534                    floating.rect.height,
9535                ),
9536            )
9537            .with_visual(UiVisual::panel(
9538                color(18, 22, 29),
9539                Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
9540                4.0,
9541            )),
9542        );
9543        layout_panel_contents(
9544            ui,
9545            floating_panel,
9546            "layout.panel_a_floating",
9547            state.layout_panel_a_scroll,
9548            &["Item 1", "Item 2", "Item 3", "Item 4"],
9549        );
9550    }
9551}
9552
9553fn base_layout_dock_panels() -> Vec<ext_widgets::DockPanelDescriptor> {
9554    vec![
9555        ext_widgets::DockPanelDescriptor::new(
9556            "panel_a",
9557            "Panel A",
9558            ext_widgets::DockSide::Left,
9559            200.0,
9560        )
9561        .with_min_size(150.0)
9562        .resizable(true),
9563        ext_widgets::DockPanelDescriptor::center("workspace", "Workspace").with_min_size(220.0),
9564        ext_widgets::DockPanelDescriptor::new(
9565            "panel_b",
9566            "Panel B",
9567            ext_widgets::DockSide::Right,
9568            200.0,
9569        )
9570        .with_min_size(150.0)
9571        .resizable(true),
9572    ]
9573}
9574
9575fn container_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9576    let body = section_with_min_viewport(
9577        ui,
9578        parent,
9579        "containers",
9580        "Containers",
9581        UiSize::new(420.0, 0.0),
9582    );
9583
9584    let frame = widgets::frame(
9585        ui,
9586        body,
9587        "containers.frame",
9588        widgets::FrameOptions::default().with_layout(
9589            LayoutStyle::column()
9590                .with_width_percent(1.0)
9591                .with_height(64.0)
9592                .with_padding(8.0)
9593                .with_gap(6.0),
9594        ),
9595    );
9596    widgets::strong_label(
9597        ui,
9598        frame,
9599        "containers.frame.title",
9600        "Frame",
9601        LayoutStyle::new().with_width_percent(1.0),
9602    );
9603    widgets::weak_label(
9604        ui,
9605        frame,
9606        "containers.frame.body",
9607        "Framed surface with padding.",
9608        LayoutStyle::new().with_width_percent(1.0),
9609    );
9610
9611    let group = widgets::group(ui, body, "containers.group");
9612    widgets::label(
9613        ui,
9614        group,
9615        "containers.group.label",
9616        "Group helper",
9617        text(12.0, color(220, 228, 238)),
9618        LayoutStyle::new().with_width_percent(1.0),
9619    );
9620    let generic_panel = widgets::panel(
9621        ui,
9622        body,
9623        "containers.panel",
9624        widgets::PanelOptions::group().with_layout(
9625            LayoutStyle::column()
9626                .with_width_percent(1.0)
9627                .with_height(44.0)
9628                .with_padding(8.0),
9629        ),
9630    );
9631    widgets::label(
9632        ui,
9633        generic_panel,
9634        "containers.panel.label",
9635        "Generic panel",
9636        text(12.0, color(220, 228, 238)),
9637        LayoutStyle::new().with_width_percent(1.0),
9638    );
9639    let group_panel = widgets::group_panel(ui, body, "containers.group_panel");
9640    widgets::label(
9641        ui,
9642        group_panel,
9643        "containers.group_panel.label",
9644        "Group panel",
9645        text(12.0, color(220, 228, 238)),
9646        LayoutStyle::new().with_width_percent(1.0),
9647    );
9648
9649    widgets::separator(
9650        ui,
9651        body,
9652        "containers.separator",
9653        widgets::SeparatorOptions::default(),
9654    );
9655    widgets::spacer(
9656        ui,
9657        body,
9658        "containers.spacer",
9659        LayoutStyle::new()
9660            .with_width_percent(1.0)
9661            .with_height(8.0)
9662            .with_flex_shrink(0.0),
9663    );
9664
9665    let grid = widgets::grid::grid(
9666        ui,
9667        body,
9668        "containers.grid",
9669        widgets::grid::GridOptions::default().with_layout(
9670            LayoutStyle::column()
9671                .with_width_percent(1.0)
9672                .with_height(78.0)
9673                .with_gap(4.0),
9674        ),
9675    );
9676    for row_index in 0..2 {
9677        let row = widgets::grid::grid_row(
9678            ui,
9679            grid,
9680            format!("containers.grid.row.{row_index}"),
9681            widgets::grid::GridRowOptions::default(),
9682        );
9683        for column_index in 0..3 {
9684            widgets::grid::grid_text_cell(
9685                ui,
9686                row,
9687                format!("containers.grid.row.{row_index}.cell.{column_index}"),
9688                format!("R{} C{}", row_index + 1, column_index + 1),
9689                widgets::grid::GridCellOptions {
9690                    text_style: text(12.0, color(214, 224, 238)),
9691                    ..Default::default()
9692                },
9693            );
9694        }
9695    }
9696
9697    widgets::sides(
9698        ui,
9699        body,
9700        "containers.sides",
9701        widgets::SidesOptions::default()
9702            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
9703            .with_gap(8.0)
9704            .with_visual(UiVisual::panel(
9705                color(20, 25, 32),
9706                Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
9707                4.0,
9708            )),
9709        |ui, left| {
9710            widgets::label(
9711                ui,
9712                left,
9713                "containers.sides.left.label",
9714                "Left side",
9715                text(12.0, color(220, 228, 238)),
9716                LayoutStyle::new().with_width_percent(1.0),
9717            );
9718        },
9719        |ui, right| {
9720            widgets::label(
9721                ui,
9722                right,
9723                "containers.sides.right.label",
9724                "Right side",
9725                text(12.0, color(220, 228, 238)),
9726                LayoutStyle::new().with_width_percent(1.0),
9727            );
9728        },
9729    );
9730
9731    widgets::columns(
9732        ui,
9733        body,
9734        "containers.columns",
9735        3,
9736        widgets::ColumnsOptions::default()
9737            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
9738            .with_gap(8.0),
9739        |ui, column, index| {
9740            widgets::label(
9741                ui,
9742                column,
9743                format!("containers.columns.{index}.label"),
9744                format!("Column {}", index + 1),
9745                text(12.0, color(220, 228, 238)),
9746                LayoutStyle::new().with_width_percent(1.0),
9747            );
9748        },
9749    );
9750
9751    let indented = widgets::indented_section(
9752        ui,
9753        body,
9754        "containers.indented",
9755        widgets::IndentOptions::default().with_amount(24.0),
9756    );
9757    widgets::label(
9758        ui,
9759        indented,
9760        "containers.indented.label",
9761        "Indented section",
9762        text(12.0, color(196, 210, 230)),
9763        LayoutStyle::new().with_width_percent(1.0),
9764    );
9765
9766    widgets::resize_container(
9767        ui,
9768        body,
9769        "containers.resize_container",
9770        widgets::ResizeContainerOptions::default().with_layout(
9771            LayoutStyle::column()
9772                .with_width_percent(1.0)
9773                .with_height(92.0)
9774                .with_flex_shrink(0.0),
9775        ),
9776        |ui, content| {
9777            widgets::label(
9778                ui,
9779                content,
9780                "containers.resize_container.label",
9781                "Resize container",
9782                text(12.0, color(220, 228, 238)),
9783                LayoutStyle::new().with_width_percent(1.0),
9784            );
9785        },
9786    );
9787
9788    widgets::scene(
9789        ui,
9790        body,
9791        "containers.scene",
9792        vec![
9793            ScenePrimitive::Rect(
9794                PaintRect::solid(UiRect::new(8.0, 12.0, 108.0, 46.0), color(48, 112, 184))
9795                    .stroke(AlignedStroke::inside(StrokeStyle::new(
9796                        color(132, 174, 222),
9797                        1.0,
9798                    )))
9799                    .corner_radii(CornerRadii::uniform(6.0)),
9800            ),
9801            ScenePrimitive::Circle {
9802                center: UiPoint::new(150.0, 35.0),
9803                radius: 22.0,
9804                fill: color(111, 203, 159),
9805                stroke: Some(StrokeStyle::new(color(176, 236, 206), 1.0)),
9806            },
9807            ScenePrimitive::Line {
9808                from: UiPoint::new(188.0, 18.0),
9809                to: UiPoint::new(238.0, 52.0),
9810                stroke: StrokeStyle::new(color(232, 186, 88), 3.0),
9811            },
9812        ],
9813        widgets::SceneOptions::default()
9814            .with_layout(LayoutStyle::new().with_width(260.0).with_height(70.0))
9815            .accessibility_label("Scene primitives"),
9816    );
9817
9818    widgets::scroll_container(
9819        ui,
9820        body,
9821        "containers.scroll_area_with_bars",
9822        state.containers_scroll,
9823        widgets::ScrollContainerOptions::default()
9824            .with_axes(ScrollAxes::VERTICAL)
9825            .with_layout(LayoutStyle::column().with_width(260.0).with_height(116.0))
9826            .with_viewport_layout(
9827                LayoutStyle::column()
9828                    .with_width(0.0)
9829                    .with_height_percent(1.0)
9830                    .with_flex_grow(1.0)
9831                    .with_flex_shrink(1.0),
9832            ),
9833        |ui, viewport| {
9834            for index in 0..5 {
9835                widgets::label(
9836                    ui,
9837                    viewport,
9838                    format!("containers.scroll_area_with_bars.row.{index}"),
9839                    format!("Scrollable row {}", index + 1),
9840                    text(12.0, color(200, 212, 228)),
9841                    LayoutStyle::new()
9842                        .with_width(232.0)
9843                        .with_height(28.0)
9844                        .with_flex_shrink(0.0),
9845                );
9846            }
9847        },
9848    );
9849
9850    widgets::label(
9851        ui,
9852        body,
9853        "containers.area.title",
9854        "Absolute area",
9855        text(12.0, color(166, 176, 190)),
9856        LayoutStyle::new().with_width_percent(1.0),
9857    );
9858    let area_host = ui.add_child(
9859        body,
9860        UiNode::container(
9861            "containers.area.host",
9862            LayoutStyle::new()
9863                .with_width_percent(1.0)
9864                .with_height(82.0)
9865                .with_flex_shrink(0.0),
9866        )
9867        .with_visual(UiVisual::panel(
9868            color(17, 20, 25),
9869            Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
9870            4.0,
9871        )),
9872    );
9873    widgets::container::area(
9874        ui,
9875        area_host,
9876        "containers.area",
9877        widgets::container::AreaOptions::new(UiRect::new(14.0, 14.0, 180.0, 44.0))
9878            .with_visual(UiVisual::panel(color(39, 72, 109), None, 4.0))
9879            .accessibility_label("Absolute positioned area"),
9880        |ui, area| {
9881            widgets::label(
9882                ui,
9883                area,
9884                "containers.area.label",
9885                "Area",
9886                text(12.0, color(238, 244, 252)),
9887                LayoutStyle::new().with_width_percent(1.0),
9888            );
9889        },
9890    );
9891}
9892
9893fn panel_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9894    let body = section_with_min_viewport(ui, parent, "panels", "Panels", UiSize::new(520.0, 320.0));
9895    widgets::label(
9896        ui,
9897        body,
9898        "panels.title",
9899        "Drag the split bars to resize the docked panels.",
9900        text(12.0, color(166, 176, 190)),
9901        LayoutStyle::new().with_width_percent(1.0),
9902    );
9903    let shell = widgets::frame(
9904        ui,
9905        body,
9906        "panels.shell",
9907        widgets::FrameOptions::default().with_layout(
9908            LayoutStyle::column()
9909                .with_width_percent(1.0)
9910                .with_height(260.0)
9911                .with_flex_grow(1.0)
9912                .with_padding(0.0)
9913                .with_gap(0.0),
9914        ),
9915    );
9916    ext_widgets::split_pane(
9917        ui,
9918        shell,
9919        "panels.top_split",
9920        ext_widgets::SplitAxis::Vertical,
9921        state.panels_top_split,
9922        panel_split_options("panels.resize.top"),
9923        |ui, top| {
9924            panel_region(
9925                ui,
9926                top,
9927                "panels.top",
9928                widgets::PanelKind::Top,
9929                "Top",
9930                "Header controls",
9931            );
9932        },
9933        |ui, lower| {
9934            ext_widgets::split_pane(
9935                ui,
9936                lower,
9937                "panels.bottom_split",
9938                ext_widgets::SplitAxis::Vertical,
9939                state.panels_bottom_split,
9940                panel_split_options("panels.resize.bottom"),
9941                |ui, middle| {
9942                    ext_widgets::split_pane(
9943                        ui,
9944                        middle,
9945                        "panels.left_split",
9946                        ext_widgets::SplitAxis::Horizontal,
9947                        state.panels_left_split,
9948                        panel_split_options("panels.resize.left"),
9949                        |ui, left| {
9950                            panel_region(
9951                                ui,
9952                                left,
9953                                "panels.left",
9954                                widgets::PanelKind::Left,
9955                                "Left",
9956                                "Navigation",
9957                            );
9958                        },
9959                        |ui, center_and_right| {
9960                            ext_widgets::split_pane(
9961                                ui,
9962                                center_and_right,
9963                                "panels.right_split",
9964                                ext_widgets::SplitAxis::Horizontal,
9965                                state.panels_right_split,
9966                                panel_split_options("panels.resize.right"),
9967                                |ui, center| {
9968                                    panel_region(
9969                                        ui,
9970                                        center,
9971                                        "panels.center",
9972                                        widgets::PanelKind::Central,
9973                                        "Central",
9974                                        "Primary workspace",
9975                                    );
9976                                },
9977                                |ui, right| {
9978                                    panel_region(
9979                                        ui,
9980                                        right,
9981                                        "panels.right",
9982                                        widgets::PanelKind::Right,
9983                                        "Right",
9984                                        "Inspector",
9985                                    );
9986                                },
9987                            );
9988                        },
9989                    );
9990                },
9991                |ui, bottom| {
9992                    panel_region(
9993                        ui,
9994                        bottom,
9995                        "panels.bottom",
9996                        widgets::PanelKind::Bottom,
9997                        "Bottom",
9998                        "Status and output",
9999                    );
10000                },
10001            );
10002        },
10003    );
10004}
10005
10006fn panel_split_options(action: &'static str) -> ext_widgets::SplitPaneOptions {
10007    let mut options = ext_widgets::SplitPaneOptions::default().with_handle_action(action);
10008    options.handle_thickness = PANELS_SPLIT_HANDLE_THICKNESS;
10009    options.handle_visual = UiVisual::panel(color(58, 70, 88), None, 0.0);
10010    options.handle_hover_visual = Some(UiVisual::panel(color(100, 172, 244), None, 0.0));
10011    options
10012}
10013
10014fn panel_region(
10015    ui: &mut UiDocument,
10016    parent: UiNodeId,
10017    name: &'static str,
10018    kind: widgets::PanelKind,
10019    title: &'static str,
10020    detail: &'static str,
10021) -> UiNodeId {
10022    let panel = widgets::panel(
10023        ui,
10024        parent,
10025        name,
10026        widgets::PanelOptions {
10027            kind,
10028            layout: LayoutStyle::column()
10029                .with_width_percent(1.0)
10030                .with_height_percent(1.0)
10031                .with_padding(10.0)
10032                .with_gap(6.0),
10033            visual: UiVisual::panel(
10034                color(18, 23, 31),
10035                Some(StrokeStyle::new(color(66, 80, 98), 1.0)),
10036                0.0,
10037            ),
10038            accessibility_label: Some(title.to_string()),
10039            ..Default::default()
10040        },
10041    );
10042    widgets::label(
10043        ui,
10044        panel,
10045        format!("{name}.label"),
10046        title,
10047        text(13.0, color(232, 240, 250)),
10048        LayoutStyle::new().with_width_percent(1.0),
10049    );
10050    widgets::label(
10051        ui,
10052        panel,
10053        format!("{name}.detail"),
10054        detail,
10055        text(11.0, color(154, 166, 184)),
10056        LayoutStyle::new().with_width_percent(1.0),
10057    );
10058    panel
10059}
10060
10061fn form_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10062    let body = section_with_min_viewport(ui, parent, "forms", "Forms", UiSize::new(390.0, 0.0));
10063    let section = widgets::form_section(
10064        ui,
10065        body,
10066        "forms.profile",
10067        None::<String>,
10068        widgets::FormSectionOptions::default().with_layout(
10069            LayoutStyle::column()
10070                .with_width_percent(1.0)
10071                .with_padding(12.0)
10072                .with_gap(10.0),
10073        ),
10074    );
10075    profile_form_summary(ui, section.root, state);
10076
10077    let status_row = wrapping_row(ui, section.root, "forms.profile.status_flags", 6.0);
10078    form_status_chip(
10079        ui,
10080        status_row,
10081        "forms.profile.status.dirty",
10082        "dirty",
10083        state.form.dirty,
10084    );
10085    form_status_chip(
10086        ui,
10087        status_row,
10088        "forms.profile.status.pending",
10089        "pending",
10090        state.form.pending,
10091    );
10092    form_status_chip(
10093        ui,
10094        status_row,
10095        "forms.profile.status.submitted",
10096        "submitted",
10097        state.form.submitted,
10098    );
10099
10100    let mut name_options = widgets::FormRowOptions::default().required();
10101    if state.form_name_text.text().trim().is_empty() {
10102        name_options = name_options.invalid("Name is required");
10103    }
10104    let name = widgets::form_row(ui, section.root, "forms.profile.name", name_options);
10105    widgets::field_label(
10106        ui,
10107        name,
10108        "forms.profile.name.label",
10109        "Name",
10110        widgets::FieldLabelOptions::default().required(),
10111    );
10112    form_text_field(
10113        ui,
10114        name,
10115        "forms.profile.name.input",
10116        &state.form_name_text,
10117        FocusedTextInput::FormName,
10118        state,
10119    );
10120    if state.form_name_text.text().trim().is_empty() {
10121        widgets::field_validation_message(
10122            ui,
10123            name,
10124            "forms.profile.name.validation",
10125            ValidationMessage::error("Name is required"),
10126            widgets::ValidationMessageOptions::default(),
10127        );
10128    } else {
10129        widgets::field_help_text(
10130            ui,
10131            name,
10132            "forms.profile.name.help",
10133            "Shown in window titles and project lists.",
10134            widgets::FieldHelpOptions::default(),
10135        );
10136    }
10137
10138    let mut email_options = widgets::FormRowOptions::default().required();
10139    if !profile_email_valid(state.form_email_text.text()) {
10140        email_options = email_options.invalid("Use a complete email address");
10141    }
10142    let email = widgets::form_row(ui, section.root, "forms.profile.email", email_options);
10143    widgets::field_label(
10144        ui,
10145        email,
10146        "forms.profile.email.label",
10147        "Email",
10148        widgets::FieldLabelOptions::default().required(),
10149    );
10150    form_text_field(
10151        ui,
10152        email,
10153        "forms.profile.email.input",
10154        &state.form_email_text,
10155        FocusedTextInput::FormEmail,
10156        state,
10157    );
10158    if profile_email_valid(state.form_email_text.text()) {
10159        widgets::field_help_text(
10160            ui,
10161            email,
10162            "forms.profile.email.help",
10163            "Used for workspace invites and notifications.",
10164            widgets::FieldHelpOptions::default(),
10165        );
10166    } else {
10167        widgets::field_validation_message(
10168            ui,
10169            email,
10170            "forms.profile.email.validation",
10171            ValidationMessage::error("Use a complete email address"),
10172            widgets::ValidationMessageOptions::default(),
10173        );
10174    }
10175
10176    let role = widgets::form_row(
10177        ui,
10178        section.root,
10179        "forms.profile.role",
10180        widgets::FormRowOptions::default(),
10181    );
10182    widgets::field_label(
10183        ui,
10184        role,
10185        "forms.profile.role.label",
10186        "Role",
10187        widgets::FieldLabelOptions::default(),
10188    );
10189    form_text_field(
10190        ui,
10191        role,
10192        "forms.profile.role.input",
10193        &state.form_role_text,
10194        FocusedTextInput::FormRole,
10195        state,
10196    );
10197    widgets::field_validation_message(
10198        ui,
10199        role,
10200        "forms.profile.role.help",
10201        if state.form_role_text.text().trim().is_empty() {
10202            ValidationMessage::warning("Role can be added later")
10203        } else {
10204            ValidationMessage::info(
10205                "Form rows compose labels, controls, help, and validation text.",
10206            )
10207        },
10208        widgets::ValidationMessageOptions::default(),
10209    );
10210
10211    let newsletter = widgets::form_row(
10212        ui,
10213        section.root,
10214        "forms.profile.newsletter",
10215        widgets::FormRowOptions::default().with_accessibility_label("Newsletter preference"),
10216    );
10217    let mut newsletter_options =
10218        widgets::CheckboxOptions::default().with_action("forms.profile.newsletter.toggle");
10219    newsletter_options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
10220    newsletter_options.text_style = text(12.0, color(220, 228, 238));
10221    widgets::checkbox(
10222        ui,
10223        newsletter,
10224        "forms.profile.newsletter.input",
10225        "Send release notes",
10226        state.form_newsletter,
10227        newsletter_options,
10228    );
10229    widgets::field_help_text(
10230        ui,
10231        newsletter,
10232        "forms.profile.newsletter.help",
10233        "Checkboxes participate in the same form state as text fields.",
10234        widgets::FieldHelpOptions::default(),
10235    );
10236
10237    widgets::form_error_summary(
10238        ui,
10239        section.root,
10240        "forms.profile.errors",
10241        &state.form,
10242        widgets::FormErrorSummaryOptions::default(),
10243    );
10244    widgets::label(
10245        ui,
10246        section.root,
10247        "forms.profile.action_help",
10248        "Apply changes saves this draft and keeps editing. Submit profile saves and marks it submitted.",
10249        text(11.0, color(154, 166, 184)),
10250        LayoutStyle::new().with_width_percent(1.0),
10251    );
10252    let action_layout = Layout::row()
10253        .size(LayoutSize::new(
10254            LayoutDimension::percent(1.0),
10255            LayoutDimension::Auto,
10256        ))
10257        .gap(LayoutGap::points(8.0, 8.0))
10258        .flex_wrap(LayoutFlexWrap::Wrap)
10259        .to_layout_style();
10260    widgets::form_action_buttons(
10261        ui,
10262        section.root,
10263        "forms.profile.actions",
10264        &state.form,
10265        widgets::FormActionButtonsOptions::default()
10266            .with_layout(action_layout)
10267            .with_labels(widgets::FormActionLabels {
10268                submit: "Submit profile".to_string(),
10269                apply: "Apply changes".to_string(),
10270                cancel: "Cancel".to_string(),
10271                reset: "Reset".to_string(),
10272            })
10273            .include_reset(true)
10274            .with_action_prefix("forms.profile"),
10275    );
10276    widgets::label(
10277        ui,
10278        section.root,
10279        "forms.profile.status",
10280        format!("Status: {}", state.form_status),
10281        text(11.0, color(154, 166, 184)),
10282        LayoutStyle::new().with_width_percent(1.0),
10283    );
10284}
10285
10286fn overlay_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10287    let body =
10288        section_with_min_viewport(ui, parent, "overlays", "Overlays", UiSize::new(420.0, 0.0));
10289    let header = widgets::collapsing_header(
10290        ui,
10291        body,
10292        "overlays.collapsing",
10293        "Collapsing header",
10294        widgets::CollapsingHeaderOptions::default()
10295            .expanded(state.overlay_expanded)
10296            .with_toggle_action("overlays.collapsing.toggle"),
10297    );
10298    if let Some(panel) = header.body {
10299        widgets::label(
10300            ui,
10301            panel,
10302            "overlays.collapsing.body_text",
10303            "Expanded content lives under the header and remains part of normal layout.",
10304            text(12.0, color(196, 210, 230)),
10305            LayoutStyle::new().with_width_percent(1.0),
10306        );
10307    }
10308
10309    let controls = wrapping_row(ui, body, "overlays.controls", 8.0);
10310    let tooltip_visual = button_visual(58, 78, 96);
10311    let mut tooltip_options = widgets::ButtonOptions::new(LayoutStyle::new().with_height(32.0));
10312    tooltip_options.visual = tooltip_visual;
10313    tooltip_options.hovered_visual = Some(readable_button_hover_visual(tooltip_visual));
10314    tooltip_options.pressed_visual = Some(adjusted_button_visual(tooltip_visual, -62));
10315    tooltip_options.pressed_hovered_visual = Some(adjusted_button_visual(tooltip_visual, -24));
10316    tooltip_options.text_style = text(13.0, color(246, 249, 252));
10317    let tooltip_target = widgets::button(
10318        ui,
10319        controls,
10320        "overlays.tooltip_target",
10321        "Tooltip target",
10322        tooltip_options,
10323    );
10324    ui.node_mut(tooltip_target).set_tooltip(
10325        TooltipContent::new("Tooltip")
10326            .body("Tooltips render as overlay surfaces anchored to a target.")
10327            .shortcut_label("Ctrl+K")
10328            .disabled_reason("Disabled reasons can be announced without changing the trigger."),
10329    );
10330    ui.node_mut(tooltip_target)
10331        .set_tooltip_placement(TooltipPlacement::Below);
10332    ui.node_mut(tooltip_target)
10333        .set_tooltip_size(UiSize::new(240.0, 148.0));
10334    button(
10335        ui,
10336        controls,
10337        "overlays.popup.toggle",
10338        if state.overlay_popup_open {
10339            "Close popup"
10340        } else {
10341            "Open popup"
10342        },
10343        "overlays.popup.toggle",
10344        button_visual(48, 112, 184),
10345    );
10346    button(
10347        ui,
10348        controls,
10349        "overlays.modal.open",
10350        "Open modal",
10351        "overlays.modal.open",
10352        button_visual(58, 78, 96),
10353    );
10354
10355    widgets::label(
10356        ui,
10357        body,
10358        "overlays.tooltip_rect.label",
10359        "A right-edge target keeps its tooltip inside the preview.",
10360        text(12.0, color(166, 176, 190)),
10361        LayoutStyle::new().with_width_percent(1.0),
10362    );
10363    let preview_viewport = UiRect::new(0.0, 0.0, 420.0, 112.0);
10364    let tooltip_target = UiRect::new(328.0, 42.0, 64.0, 28.0);
10365    let tooltip_size = UiSize::new(176.0, 58.0);
10366    let placed_tooltip = widgets::tooltip::tooltip_rect(
10367        tooltip_target,
10368        tooltip_size,
10369        preview_viewport,
10370        TooltipPlacement::Right,
10371        8.0,
10372        None,
10373    );
10374    let clamped_preview = ui.add_child(
10375        body,
10376        UiNode::container(
10377            "overlays.tooltip_rect.preview",
10378            LayoutStyle::new()
10379                .with_width_percent(1.0)
10380                .with_height(112.0)
10381                .with_flex_shrink(0.0),
10382        )
10383        .with_visual(UiVisual::panel(
10384            color(12, 16, 22),
10385            Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
10386            4.0,
10387        )),
10388    );
10389    ui.add_child(
10390        clamped_preview,
10391        UiNode::scene(
10392            "overlays.tooltip_rect.scene",
10393            vec![
10394                ScenePrimitive::Line {
10395                    from: UiPoint::new(placed_tooltip.right() + 2.0, placed_tooltip.y + 29.0),
10396                    to: UiPoint::new(tooltip_target.x - 2.0, tooltip_target.y + 14.0),
10397                    stroke: StrokeStyle::new(color(92, 106, 128), 1.0),
10398                },
10399                ScenePrimitive::Rect(
10400                    PaintRect::solid(placed_tooltip, color(24, 29, 38))
10401                        .stroke(AlignedStroke::inside(StrokeStyle::new(
10402                            color(92, 106, 128),
10403                            1.0,
10404                        )))
10405                        .corner_radii(CornerRadii::uniform(4.0)),
10406                ),
10407                ScenePrimitive::Text(
10408                    PaintText::new(
10409                        "Tooltip",
10410                        UiRect::new(
10411                            placed_tooltip.x + 12.0,
10412                            placed_tooltip.y + 9.0,
10413                            placed_tooltip.width - 24.0,
10414                            18.0,
10415                        ),
10416                        text(12.0, color(225, 233, 244)),
10417                    )
10418                    .multiline(false),
10419                ),
10420                ScenePrimitive::Text(
10421                    PaintText::new(
10422                        "Placed inside",
10423                        UiRect::new(
10424                            placed_tooltip.x + 12.0,
10425                            placed_tooltip.y + 31.0,
10426                            placed_tooltip.width - 24.0,
10427                            18.0,
10428                        ),
10429                        text(10.0, color(156, 170, 190)),
10430                    )
10431                    .multiline(false),
10432                ),
10433                ScenePrimitive::Rect(
10434                    PaintRect::solid(tooltip_target, color(48, 112, 184))
10435                        .stroke(AlignedStroke::inside(StrokeStyle::new(
10436                            color(132, 190, 255),
10437                            1.0,
10438                        )))
10439                        .corner_radii(CornerRadii::uniform(3.0)),
10440                ),
10441                ScenePrimitive::Text(
10442                    PaintText::new("Target", tooltip_target, text(10.0, color(240, 247, 255)))
10443                        .horizontal_align(TextHorizontalAlign::Center)
10444                        .vertical_align(TextVerticalAlign::Center)
10445                        .multiline(false),
10446                ),
10447            ],
10448            LayoutStyle::new()
10449                .with_width_percent(1.0)
10450                .with_height_percent(1.0),
10451        ),
10452    );
10453
10454    widgets::label(
10455        ui,
10456        body,
10457        "overlays.popup.label",
10458        "Popup panel",
10459        text(12.0, color(166, 176, 190)),
10460        LayoutStyle::new().with_width_percent(1.0),
10461    );
10462    widgets::label(
10463        ui,
10464        body,
10465        "overlays.popup.status",
10466        if state.overlay_popup_open {
10467            "Popup overlay is open."
10468        } else {
10469            "Popup overlay is closed."
10470        },
10471        text(12.0, color(196, 210, 230)),
10472        LayoutStyle::new().with_width_percent(1.0),
10473    );
10474    let popup_host_layout = if state.overlay_popup_open {
10475        LayoutStyle::column()
10476            .with_width_percent(1.0)
10477            .with_height(128.0)
10478            .with_flex_shrink(0.0)
10479    } else {
10480        LayoutStyle::column()
10481            .with_width_percent(1.0)
10482            .with_padding(10.0)
10483            .with_flex_shrink(0.0)
10484    };
10485    let popup_host = ui.add_child(
10486        body,
10487        UiNode::container("overlays.popup.host", popup_host_layout).with_visual(UiVisual::panel(
10488            color(12, 16, 22),
10489            Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
10490            4.0,
10491        )),
10492    );
10493    if state.overlay_popup_open {
10494        let popup = ext_widgets::popup_panel(
10495            ui,
10496            popup_host,
10497            "overlays.popup_panel",
10498            UiRect::new(10.0, 10.0, 220.0, 104.0),
10499            ext_widgets::PopupOptions {
10500                z_index: 4,
10501                portal: UiPortalTarget::Parent,
10502                accessibility: Some(
10503                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup preview"),
10504                ),
10505                ..Default::default()
10506            },
10507        );
10508        let popup_body = ui.add_child(
10509            popup,
10510            UiNode::container(
10511                "overlays.popup_panel.body",
10512                LayoutStyle::column()
10513                    .with_width_percent(1.0)
10514                    .with_height_percent(1.0)
10515                    .with_padding(10.0)
10516                    .with_gap(6.0),
10517            ),
10518        );
10519        let popup_header = row(ui, popup_body, "overlays.popup_panel.header", 8.0);
10520        widgets::label(
10521            ui,
10522            popup_header,
10523            "overlays.popup_panel.label",
10524            "Popup panel",
10525            text(12.0, color(220, 228, 238)),
10526            LayoutStyle::new().with_width_percent(1.0),
10527        );
10528        let mut close = widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0))
10529            .with_action("overlays.popup.close");
10530        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
10531        close.hovered_visual = Some(button_visual(54, 70, 92));
10532        close.text_style = text(12.0, color(220, 228, 238));
10533        widgets::button(ui, popup_header, "overlays.popup_panel.close", "x", close);
10534        widgets::label(
10535            ui,
10536            popup_body,
10537            "overlays.popup_panel.body_text",
10538            "Popup content is conditionally rendered.",
10539            text(11.0, color(196, 210, 230)),
10540            LayoutStyle::new().with_width_percent(1.0),
10541        );
10542    } else {
10543        widgets::label(
10544            ui,
10545            popup_host,
10546            "overlays.popup.empty",
10547            "Open the popup to render an overlay inside this host.",
10548            text(12.0, color(154, 166, 184)),
10549            LayoutStyle::new().with_width_percent(1.0),
10550        );
10551    }
10552
10553    widgets::label(
10554        ui,
10555        body,
10556        "overlays.toasts.label",
10557        "Toasts",
10558        text(12.0, color(166, 176, 190)),
10559        LayoutStyle::new().with_width_percent(1.0),
10560    );
10561    let toast_controls = row(ui, body, "overlays.toasts.controls", 10.0);
10562    button(
10563        ui,
10564        toast_controls,
10565        "overlays.toasts.show",
10566        "Show toast",
10567        "toast.show",
10568        button_visual(48, 112, 184),
10569    );
10570    button(
10571        ui,
10572        toast_controls,
10573        "overlays.toasts.hide",
10574        "Hide",
10575        "toast.hide",
10576        button_visual(58, 78, 96),
10577    );
10578    widgets::label(
10579        ui,
10580        body,
10581        "overlays.toasts.status",
10582        if state.toast_visible {
10583            "Toast overlay is visible."
10584        } else {
10585            "Toast overlay is hidden."
10586        },
10587        text(12.0, color(196, 210, 230)),
10588        LayoutStyle::new().with_width_percent(1.0),
10589    );
10590    widgets::label(
10591        ui,
10592        body,
10593        "overlays.toasts.action_status",
10594        format!("Action: {}", state.toast_action_status),
10595        text(12.0, color(154, 166, 184)),
10596        LayoutStyle::new().with_width_percent(1.0),
10597    );
10598
10599    if state.overlay_modal_open {
10600        let modal = widgets::modal_dialog(
10601            ui,
10602            parent,
10603            "overlays.modal",
10604            "Modal dialog",
10605            widgets::ModalDialogOptions::default()
10606                .with_size(320.0, 180.0)
10607                .with_close_action("overlays.modal.close")
10608                .with_dismissal(ext_widgets::DialogDismissal::MODAL)
10609                .with_focus_restore(FocusRestoreTarget::Previous),
10610        );
10611        widgets::label(
10612            ui,
10613            modal.body,
10614            "overlays.modal.body.text",
10615            "Modal dialogs are portaled to the application overlay, include a scrim, and trap focus.",
10616            text(12.0, color(220, 228, 238)),
10617            LayoutStyle::new().with_width_percent(1.0),
10618        );
10619        button(
10620            ui,
10621            modal.body,
10622            "overlays.modal.body.close",
10623            "Close modal",
10624            "overlays.modal.close",
10625            button_visual(48, 112, 184),
10626        );
10627    }
10628}
10629
10630fn drag_drop_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10631    let body = section_with_min_viewport(
10632        ui,
10633        parent,
10634        "drag_drop",
10635        "Drag and drop",
10636        UiSize::new(420.0, 0.0),
10637    );
10638    widgets::label(
10639        ui,
10640        body,
10641        "drag_drop.sources.label",
10642        "Drag sources",
10643        text(12.0, color(166, 176, 190)),
10644        LayoutStyle::new().with_width_percent(1.0),
10645    );
10646    let sources = wrapping_row(ui, body, "drag_drop.sources", 8.0);
10647    widgets::dnd_drag_source(
10648        ui,
10649        sources,
10650        "drag_drop.text_source",
10651        "Text payload",
10652        DragPayload::text("Operad payload"),
10653        widgets::DragSourceOptions::default()
10654            .with_layout(drag_source_layout())
10655            .with_kind(DragDropSurfaceKind::ListRow)
10656            .with_allowed_operations([DragOperation::Copy, DragOperation::Move])
10657            .with_action("drag_drop.text_source")
10658            .with_accessibility_hint("Start a text drag operation"),
10659    );
10660    widgets::dnd_drag_source(
10661        ui,
10662        sources,
10663        "drag_drop.file_source",
10664        "File payload",
10665        DragPayload::files(["/tmp/showcase.scene"]),
10666        widgets::DragSourceOptions::default()
10667            .with_layout(drag_source_layout())
10668            .with_kind(DragDropSurfaceKind::Asset)
10669            .with_drag_image_policy(widgets::DragImagePolicy::image_key(
10670                BuiltInIcon::Folder.key(),
10671                UiSize::new(120.0, 36.0),
10672                UiPoint::new(10.0, 10.0),
10673            ))
10674            .with_allowed_operations([DragOperation::Copy])
10675            .with_action("drag_drop.file_source"),
10676    );
10677    widgets::dnd_drag_source(
10678        ui,
10679        sources,
10680        "drag_drop.bytes_source",
10681        "Image bytes",
10682        DragPayload::bytes(DragBytes::new("image/png", vec![137, 80, 78, 71]).name("sprite.png")),
10683        widgets::DragSourceOptions::default()
10684            .with_layout(drag_source_layout())
10685            .with_kind(DragDropSurfaceKind::Asset)
10686            .with_action("drag_drop.bytes_source")
10687            .without_drag_image(),
10688    );
10689
10690    widgets::label(
10691        ui,
10692        body,
10693        "drag_drop.zones.label",
10694        "Drop zones",
10695        text(12.0, color(166, 176, 190)),
10696        LayoutStyle::new().with_width_percent(1.0),
10697    );
10698    let zones = wrapping_row(ui, body, "drag_drop.zones", 8.0);
10699    let accepted_options = widgets::DropZoneOptions::default()
10700        .with_layout(drop_zone_layout())
10701        .with_kind(DragDropSurfaceKind::EditorSurface)
10702        .with_accepted_payload(DropPayloadFilter::empty().text())
10703        .with_accepted_operations([DragOperation::Copy, DragOperation::Move])
10704        .with_action("drag_drop.accept_text")
10705        .with_accessibility_hint("Accepts text payloads");
10706    let accepted = widgets::dnd_drop_zone(
10707        ui,
10708        zones,
10709        "drag_drop.accept_text",
10710        "Text accepted",
10711        accepted_options.clone(),
10712    );
10713    widgets::drag_drop::dnd_apply_drop_zone_preview(
10714        ui,
10715        accepted.root,
10716        &accepted_options,
10717        widgets::drag_drop::DropZonePreviewState::Accepted,
10718    );
10719
10720    let rejected_options = widgets::DropZoneOptions::default()
10721        .with_layout(drop_zone_layout())
10722        .with_kind(DragDropSurfaceKind::Asset)
10723        .with_accepted_payload(DropPayloadFilter::empty().files())
10724        .with_action("drag_drop.files_only");
10725    let rejected = widgets::dnd_drop_zone(
10726        ui,
10727        zones,
10728        "drag_drop.files_only",
10729        "Files only",
10730        rejected_options.clone(),
10731    );
10732    widgets::drag_drop::dnd_apply_drop_zone_preview(
10733        ui,
10734        rejected.root,
10735        &rejected_options,
10736        widgets::drag_drop::DropZonePreviewState::Rejected,
10737    );
10738    let image_options = widgets::DropZoneOptions::default()
10739        .with_layout(drop_zone_layout())
10740        .with_kind(DragDropSurfaceKind::Asset)
10741        .with_accepted_payload(DropPayloadFilter::empty().mime_type("image/*"))
10742        .with_accepted_operations([DragOperation::Copy])
10743        .with_action("drag_drop.image_bytes");
10744    let image_zone = widgets::dnd_drop_zone(
10745        ui,
10746        zones,
10747        "drag_drop.image_bytes",
10748        "Image bytes",
10749        image_options.clone(),
10750    );
10751    widgets::drag_drop::dnd_apply_drop_zone_preview(
10752        ui,
10753        image_zone.root,
10754        &image_options,
10755        widgets::drag_drop::DropZonePreviewState::Hovered,
10756    );
10757
10758    let disabled_options = widgets::DropZoneOptions::default()
10759        .with_layout(drop_zone_layout())
10760        .with_kind(DragDropSurfaceKind::EditorSurface)
10761        .with_accepted_payload(DropPayloadFilter::any())
10762        .with_action("drag_drop.disabled")
10763        .disabled();
10764    let disabled_zone = widgets::dnd_drop_zone(
10765        ui,
10766        zones,
10767        "drag_drop.disabled",
10768        "Disabled",
10769        disabled_options.clone(),
10770    );
10771    widgets::drag_drop::dnd_apply_drop_zone_preview(
10772        ui,
10773        disabled_zone.root,
10774        &disabled_options,
10775        widgets::drag_drop::DropZonePreviewState::Disabled,
10776    );
10777
10778    let operation_row = wrapping_row(ui, body, "drag_drop.operations", 6.0);
10779    dnd_operation_chip(ui, operation_row, "drag_drop.operation.copy", "copy");
10780    dnd_operation_chip(ui, operation_row, "drag_drop.operation.move", "move");
10781    dnd_operation_chip(ui, operation_row, "drag_drop.operation.link", "link");
10782    widgets::label(
10783        ui,
10784        body,
10785        "drag_drop.status",
10786        format!("Status: {}", state.drag_drop_status),
10787        text(11.0, color(154, 166, 184)),
10788        LayoutStyle::new().with_width_percent(1.0),
10789    );
10790}
10791
10792fn media_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10793    let body = section_with_min_viewport(
10794        ui,
10795        parent,
10796        "media",
10797        "Media",
10798        UiSize::new(MEDIA_ICON_TILE_WIDTH, 0.0),
10799    );
10800    widgets::label(
10801        ui,
10802        body,
10803        "media.icons.label",
10804        "Built-in icons",
10805        text(12.0, color(166, 176, 190)),
10806        LayoutStyle::new().with_width_percent(1.0),
10807    );
10808    let icon_columns = media_icon_columns(state);
10809    let icons = media_icon_grid(
10810        ui,
10811        body,
10812        "media.icons",
10813        icon_columns,
10814        BuiltInIcon::COMMON.len(),
10815    );
10816    for icon in BuiltInIcon::COMMON {
10817        media_icon_tile(ui, icons, icon);
10818    }
10819
10820    widgets::label(
10821        ui,
10822        body,
10823        "media.variants.label",
10824        "Image variants",
10825        text(12.0, color(166, 176, 190)),
10826        LayoutStyle::new().with_width_percent(1.0),
10827    );
10828    let variants = wrapping_row(ui, body, "media.variants", 10.0);
10829    widgets::image(
10830        ui,
10831        variants,
10832        "media.image.user_png",
10833        ImageContent::from(ImageHandle::app(SHOWCASE_USER_IMAGE_KEY)),
10834        widgets::ImageOptions::default()
10835            .with_layout(media_preview_image_layout())
10836            .with_accessibility_label("User supplied PNG image"),
10837    );
10838    widgets::image(
10839        ui,
10840        variants,
10841        "media.image.untinted",
10842        icon_image(BuiltInIcon::Play),
10843        widgets::ImageOptions::default()
10844            .with_layout(media_preview_image_layout())
10845            .with_accessibility_label("Untinted play icon"),
10846    );
10847    widgets::image(
10848        ui,
10849        variants,
10850        "media.image.warning",
10851        ImageContent::new(BuiltInIcon::Warning.key()).tinted(color(232, 186, 88)),
10852        widgets::ImageOptions::default()
10853            .with_layout(media_preview_image_layout())
10854            .with_accessibility_label("Tinted warning icon"),
10855    );
10856    widgets::image(
10857        ui,
10858        variants,
10859        "media.image.shader",
10860        ImageContent::new(BuiltInIcon::Grid.key()).tinted(color(118, 183, 255)),
10861        widgets::ImageOptions::default()
10862            .with_layout(media_preview_image_layout())
10863            .with_shader(ShaderEffect::tint(color(169, 119, 255), 0.65))
10864            .with_accessibility_label("Shader-decorated grid icon"),
10865    );
10866}
10867
10868fn shader_effect_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10869    let body = section_with_min_viewport(
10870        ui,
10871        parent,
10872        "shaders",
10873        "Shader effects",
10874        UiSize::new(420.0, 280.0),
10875    );
10876    widgets::label(
10877        ui,
10878        body,
10879        "shaders.effects.label",
10880        "Built-in effects",
10881        text(12.0, color(166, 176, 190)),
10882        LayoutStyle::new().with_width_percent(1.0),
10883    );
10884    let phase = (state.progress_phase / std::f32::consts::TAU).fract();
10885    let previews = wrapping_row(ui, body, "shaders.effects", 10.0);
10886    shader_effect_preview_card(ui, previews, "base", "Base", None);
10887    shader_effect_preview_card(
10888        ui,
10889        previews,
10890        "tint",
10891        "Tint",
10892        Some(ShaderEffect::tint(color(252, 186, 90), 0.72)),
10893    );
10894    shader_effect_preview_card(
10895        ui,
10896        previews,
10897        "shine",
10898        "Shine",
10899        Some(ShaderEffect::shine(phase, 0.55).uniform("width", 0.14)),
10900    );
10901    shader_effect_preview_card(
10902        ui,
10903        previews,
10904        "glow",
10905        "Glow",
10906        Some(ShaderEffect::glow(color(118, 183, 255), 0.9, 7.0)),
10907    );
10908
10909    widgets::label(
10910        ui,
10911        body,
10912        "shaders.widgets.label",
10913        "Applied to widgets",
10914        text(12.0, color(166, 176, 190)),
10915        LayoutStyle::new().with_width_percent(1.0),
10916    );
10917    let panel = ui.add_child(
10918        body,
10919        UiNode::container(
10920            "shaders.widgets",
10921            LayoutStyle::column()
10922                .with_width_percent(1.0)
10923                .with_padding(10.0)
10924                .with_gap(10.0)
10925                .with_flex_shrink(0.0),
10926        )
10927        .with_visual(UiVisual::panel(
10928            color(13, 18, 25),
10929            Some(StrokeStyle::new(color(48, 61, 78), 1.0)),
10930            4.0,
10931        )),
10932    );
10933    let control_row = wrapping_row(ui, panel, "shaders.widgets.controls", 10.0);
10934    let mut shine_button = widgets::ButtonOptions::new(
10935        LayoutStyle::new()
10936            .with_width(150.0)
10937            .with_height(34.0)
10938            .with_flex_shrink(0.0),
10939    );
10940    shine_button.leading_image = Some(icon_image(BuiltInIcon::Settings));
10941    shine_button.image_shader = Some(ShaderEffect::tint(color(111, 203, 159), 0.85));
10942    shine_button.shader = Some(ShaderEffect::shine(phase, 0.45).uniform("width", 0.12));
10943    widgets::button(
10944        ui,
10945        control_row,
10946        "shaders.widgets.button",
10947        "Shine button",
10948        shine_button,
10949    );
10950
10951    widgets::checkbox_with_state(
10952        ui,
10953        control_row,
10954        "shaders.widgets.checkbox",
10955        "Glow check",
10956        widgets::CheckboxState::Checked,
10957        widgets::CheckboxOptions::default()
10958            .with_check_color(color(118, 183, 255))
10959            .with_check_shader(ShaderEffect::glow(color(118, 183, 255), 1.0, 4.0)),
10960    );
10961
10962    let progress_value = smooth_loop(state.progress_phase * 0.8, 0.2) * 100.0;
10963    let mut progress = ext_widgets::ProgressIndicatorOptions::default();
10964    progress.layout = LayoutStyle::new().with_width_percent(1.0).with_height(12.0);
10965    progress.fill_visual = UiVisual::panel(color(111, 203, 159), None, 4.0);
10966    progress.fill_shader = Some(ShaderEffect::shine(phase, 0.5).uniform("width", 0.16));
10967    progress.accessibility_label = Some("Shadered progress fill".to_string());
10968    ext_widgets::progress_indicator(
10969        ui,
10970        panel,
10971        "shaders.widgets.progress",
10972        ext_widgets::ProgressIndicatorValue::percent(progress_value),
10973        progress,
10974    );
10975
10976    let mut slider_options = widgets::SliderOptions::default();
10977    slider_options.layout = LayoutStyle::new()
10978        .with_width_percent(1.0)
10979        .with_height(28.0)
10980        .with_flex_shrink(0.0);
10981    slider_options.fill_shader = Some(ShaderEffect::tint(color(169, 119, 255), 0.55));
10982    slider_options.thumb_shader = Some(ShaderEffect::glow(color(252, 186, 90), 0.9, 4.0));
10983    slider_options.accessibility_label = Some("Shadered slider".to_string());
10984    widgets::slider(
10985        ui,
10986        panel,
10987        "shaders.widgets.slider",
10988        smooth_loop(state.progress_phase * 0.6, 0.4),
10989        0.0..1.0,
10990        slider_options,
10991    );
10992}
10993
10994fn shader_effect_preview_card(
10995    ui: &mut UiDocument,
10996    parent: UiNodeId,
10997    name: &'static str,
10998    label: &'static str,
10999    shader: Option<ShaderEffect>,
11000) {
11001    let tile = ui.add_child(
11002        parent,
11003        UiNode::container(
11004            format!("shaders.effect_tile.{name}"),
11005            LayoutStyle::column()
11006                .with_width(96.0)
11007                .with_height(104.0)
11008                .with_padding(8.0)
11009                .with_gap(8.0)
11010                .with_flex_shrink(0.0),
11011        )
11012        .with_visual(UiVisual::panel(
11013            color(17, 22, 30),
11014            Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
11015            4.0,
11016        ))
11017        .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(label)),
11018    );
11019    let mut swatch = UiNode::container(
11020        format!("shaders.effect.{name}.swatch"),
11021        LayoutStyle::new()
11022            .with_width_percent(1.0)
11023            .with_height(50.0)
11024            .with_flex_shrink(0.0),
11025    )
11026    .with_visual(UiVisual::panel(
11027        color(64, 109, 194),
11028        Some(StrokeStyle::new(color(138, 164, 194), 1.0)),
11029        8.0,
11030    ));
11031    if let Some(shader) = shader {
11032        swatch = swatch.with_shader(shader);
11033    }
11034    ui.add_child(tile, swatch);
11035    widgets::label(
11036        ui,
11037        tile,
11038        format!("shaders.effect.{name}.label"),
11039        label,
11040        text(11.0, color(204, 216, 232)),
11041        LayoutStyle::new().with_width_percent(1.0),
11042    );
11043}
11044
11045fn shader_lab_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11046    let body = section_with_min_viewport(
11047        ui,
11048        parent,
11049        "shader_lab",
11050        "Shader lab",
11051        UiSize::new(SHADER_LAB_CONTENT_MIN_WIDTH, SHADER_LAB_CONTENT_MIN_HEIGHT),
11052    );
11053    let source_error = state.shader_lab_source_error.as_deref();
11054    let source_valid = source_error.is_none();
11055
11056    let mut split_options = ext_widgets::SplitPaneOptions::default()
11057        .with_handle_action("shader_lab.workspace.resize")
11058        .with_handle_hover_visual(UiVisual::panel(color(96, 166, 238), None, 2.0));
11059    split_options.layout = Layout::row()
11060        .size(LayoutSize::new(
11061            LayoutDimension::percent(1.0),
11062            LayoutDimension::points(SHADER_LAB_WORKSPACE_HEIGHT),
11063        ))
11064        .min_size(LayoutSize::points(
11065            SHADER_LAB_CONTENT_MIN_WIDTH,
11066            SHADER_LAB_WORKSPACE_HEIGHT,
11067        ))
11068        .flex(0.0, 0.0, LayoutDimension::Auto)
11069        .to_layout_style();
11070    split_options.handle_thickness = SHADER_LAB_SPLIT_HANDLE_THICKNESS;
11071    split_options.handle_visual = UiVisual::panel(color(48, 61, 78), None, 2.0);
11072
11073    ext_widgets::split_pane(
11074        ui,
11075        body,
11076        "shader_lab.workspace",
11077        ext_widgets::SplitAxis::Horizontal,
11078        state.shader_lab_split,
11079        split_options,
11080        |ui, preview_pane| {
11081            shader_lab_preview_column(ui, preview_pane, state, source_error, source_valid);
11082        },
11083        |ui, editor_pane| {
11084            shader_lab_editor_column(ui, editor_pane, state, source_error);
11085        },
11086    );
11087}
11088
11089fn shader_lab_preview_column(
11090    ui: &mut UiDocument,
11091    parent: UiNodeId,
11092    state: &ShowcaseState,
11093    source_error: Option<&str>,
11094    source_valid: bool,
11095) {
11096    let preview_column = ui.add_child(
11097        parent,
11098        UiNode::container(
11099            "shader_lab.preview.column",
11100            LayoutStyle::column()
11101                .with_width_percent(1.0)
11102                .with_height_percent(1.0)
11103                .with_gap(8.0)
11104                .with_flex_shrink(1.0),
11105        ),
11106    );
11107    let target_row = row(ui, preview_column, "shader_lab.target.row", 8.0);
11108    widgets::label(
11109        ui,
11110        target_row,
11111        "shader_lab.target.caption",
11112        "Preview",
11113        text(12.0, color(166, 176, 190)),
11114        LayoutStyle::new()
11115            .with_width(58.0)
11116            .with_height(30.0)
11117            .with_flex_shrink(0.0),
11118    );
11119    shader_lab_dropdown_select(
11120        ui,
11121        target_row,
11122        "shader_lab.target",
11123        &shader_lab_target_options(),
11124        &state.shader_lab_target_menu,
11125        160.0,
11126        "Preview target",
11127        "Shader lab preview target",
11128    );
11129    shader_lab_preview_controls(ui, preview_column, state);
11130
11131    let preview = ui.add_child(
11132        preview_column,
11133        UiNode::container(
11134            "shader_lab.preview.surface",
11135            LayoutStyle::column()
11136                .with_width_percent(1.0)
11137                .with_height(0.0)
11138                .with_flex_grow(1.0)
11139                .with_padding(18.0)
11140                .with_gap(8.0)
11141                .with_align_items(taffy::prelude::AlignItems::Center)
11142                .with_justify_content(taffy::prelude::JustifyContent::Center)
11143                .with_flex_shrink(0.0),
11144        )
11145        .with_visual(UiVisual::panel(
11146            color(8, 12, 18),
11147            Some(StrokeStyle::new(color(48, 61, 78), 1.0)),
11148            4.0,
11149        )),
11150    );
11151    shader_lab_preview(ui, preview, state, source_valid);
11152
11153    if let Some(status) = shader_lab_status_label(state, source_error) {
11154        widgets::label(
11155            ui,
11156            preview_column,
11157            "shader_lab.preview.status",
11158            status,
11159            text(11.0, color(166, 176, 190)),
11160            LayoutStyle::new().with_width_percent(1.0),
11161        );
11162    }
11163    shader_lab_material_contract_demo(ui, preview_column, state);
11164}
11165
11166fn shader_lab_preview_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11167    let controls = ui.add_child(
11168        parent,
11169        UiNode::container(
11170            "shader_lab.preview.controls",
11171            LayoutStyle::column()
11172                .with_width_percent(1.0)
11173                .with_gap(6.0)
11174                .with_flex_shrink(0.0),
11175        ),
11176    );
11177    let text_row = wrapping_row(ui, controls, "shader_lab.preview.text_controls", 8.0);
11178    shader_lab_option_checkbox(
11179        ui,
11180        text_row,
11181        "shader_lab.frame_text.toggle",
11182        "Frame",
11183        state.shader_lab_show_frame_text,
11184    );
11185    shader_lab_option_checkbox(
11186        ui,
11187        text_row,
11188        "shader_lab.button_text.toggle",
11189        "Button",
11190        state.shader_lab_show_button_text,
11191    );
11192
11193    let style_row = wrapping_row(ui, controls, "shader_lab.preview.style_controls", 8.0);
11194    shader_lab_slider_control(
11195        ui,
11196        style_row,
11197        "shader_lab.surface.stroke",
11198        "Border",
11199        state.shader_lab_surface_stroke_width,
11200        SHADER_LAB_SURFACE_STROKE_MAX,
11201        1,
11202    );
11203    shader_lab_slider_control(
11204        ui,
11205        style_row,
11206        "shader_lab.surface.radius",
11207        "Radius",
11208        state.shader_lab_surface_radius,
11209        SHADER_LAB_SURFACE_RADIUS_MAX,
11210        0,
11211    );
11212}
11213
11214fn shader_lab_material_contract_demo(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11215    let panel = ui.add_child(
11216        parent,
11217        UiNode::container(
11218            "shader_lab.material",
11219            LayoutStyle::column()
11220                .with_width_percent(1.0)
11221                .with_gap(6.0)
11222                .with_flex_shrink(0.0),
11223        ),
11224    );
11225    widgets::label(
11226        ui,
11227        panel,
11228        "shader_lab.material.title",
11229        "Material",
11230        text(12.0, color(186, 198, 216)),
11231        LayoutStyle::new().with_width_percent(1.0),
11232    );
11233    let controls = wrapping_row(ui, panel, "shader_lab.material.controls", 8.0);
11234    shader_lab_labeled_dropdown(
11235        ui,
11236        controls,
11237        "shader_lab.material.shader",
11238        "Shader",
11239        &shader_lab_material_shader_options(),
11240        &state.shader_lab_material_shader_menu,
11241        132.0,
11242    );
11243    shader_lab_labeled_dropdown(
11244        ui,
11245        controls,
11246        "shader_lab.material.shape",
11247        "Shape",
11248        &shader_lab_material_shape_options(),
11249        &state.shader_lab_material_shape_menu,
11250        132.0,
11251    );
11252    shader_lab_labeled_dropdown(
11253        ui,
11254        controls,
11255        "shader_lab.material.geometry",
11256        "Geometry",
11257        &shader_lab_material_geometry_options(),
11258        &state.shader_lab_material_geometry_menu,
11259        140.0,
11260    );
11261    shader_lab_slider_control(
11262        ui,
11263        controls,
11264        "shader_lab.material.outset",
11265        "Outset",
11266        state.shader_lab_material_outset,
11267        SHADER_LAB_MATERIAL_OUTSET_MAX,
11268        0,
11269    );
11270
11271    let contracts_layout = Layout::column()
11272        .size(LayoutSize::new(
11273            LayoutDimension::percent(1.0),
11274            LayoutDimension::Auto,
11275        ))
11276        .padding(LayoutSpacing::new(
11277            LayoutLength::points(4.0),
11278            LayoutLength::points(4.0),
11279            LayoutLength::points(SHADER_LAB_MATERIAL_OUTSET_MAX),
11280            LayoutLength::points(8.0),
11281        ))
11282        .flex(0.0, 0.0, LayoutDimension::Auto)
11283        .to_layout_style();
11284    let contracts_shell = ui.add_child(
11285        panel,
11286        UiNode::container("shader_lab.material.contracts.shell", contracts_layout),
11287    );
11288    let row = wrapping_row(ui, contracts_shell, "shader_lab.material.contracts", 8.0);
11289    shader_lab_material_chip(
11290        ui,
11291        row,
11292        "shader_lab.material.current",
11293        "Selected",
11294        shader_lab_selected_material(state),
11295        shader_lab_material_visual(state.shader_lab_material_shape),
11296    );
11297    shader_lab_material_chip(
11298        ui,
11299        row,
11300        "shader_lab.material.outset",
11301        "Glow",
11302        ElementMaterial::shader(ShaderEffect::glow(
11303            color(100, 180, 255),
11304            0.95,
11305            SHADER_LAB_MATERIAL_OUTSET,
11306        ))
11307        .with_paint_outset(LayoutInsets::points(
11308            state
11309                .shader_lab_material_outset
11310                .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11311        )),
11312        UiVisual::panel(color(32, 64, 96), None, 8.0),
11313    );
11314    shader_lab_material_chip(
11315        ui,
11316        row,
11317        "shader_lab.material.circle_hit",
11318        "Circle",
11319        ElementMaterial::new()
11320            .with_clip_shape(ElementShape::circle())
11321            .with_hit_shape(ElementShape::circle()),
11322        UiVisual::panel(
11323            color(74, 133, 198),
11324            Some(StrokeStyle::new(color(212, 232, 255), 1.0)),
11325            999.0,
11326        ),
11327    );
11328    shader_lab_material_chip(
11329        ui,
11330        row,
11331        "shader_lab.material.geometry_chip",
11332        "Warp",
11333        ElementMaterial::new()
11334            .with_paint_outset(LayoutInsets::points(8.0))
11335            .with_geometry_effect(GeometryEffect::wave(8.0)),
11336        UiVisual::panel(
11337            color(101, 70, 170),
11338            Some(StrokeStyle::new(color(214, 196, 255), 1.0)),
11339            6.0,
11340        ),
11341    );
11342}
11343
11344fn shader_lab_labeled_dropdown(
11345    ui: &mut UiDocument,
11346    parent: UiNodeId,
11347    name: &'static str,
11348    label: &'static str,
11349    options: &[ext_widgets::SelectOption],
11350    state: &ext_widgets::SelectMenuState,
11351    width: f32,
11352) {
11353    let control = ui.add_child(
11354        parent,
11355        UiNode::container(
11356            format!("{name}.control"),
11357            LayoutStyle::row()
11358                .with_width(width + 74.0)
11359                .with_height(30.0)
11360                .with_gap(6.0)
11361                .with_align_items(taffy::prelude::AlignItems::Center)
11362                .with_flex_shrink(0.0),
11363        ),
11364    );
11365    widgets::label(
11366        ui,
11367        control,
11368        format!("{name}.caption"),
11369        label,
11370        text(12.0, color(166, 176, 190)),
11371        LayoutStyle::new()
11372            .with_width(66.0)
11373            .with_height(30.0)
11374            .with_flex_shrink(0.0),
11375    );
11376    shader_lab_dropdown_select(ui, control, name, options, state, width, label, label);
11377}
11378
11379fn shader_lab_dropdown_select(
11380    ui: &mut UiDocument,
11381    parent: UiNodeId,
11382    name: &'static str,
11383    options: &[ext_widgets::SelectOption],
11384    state: &ext_widgets::SelectMenuState,
11385    width: f32,
11386    placeholder: &'static str,
11387    accessibility_label: &'static str,
11388) {
11389    let anchor = ui.add_child(
11390        parent,
11391        UiNode::container(
11392            format!("{name}.anchor"),
11393            LayoutStyle::new()
11394                .with_width(width)
11395                .with_height(30.0)
11396                .with_flex_shrink(0.0),
11397        ),
11398    );
11399    let nodes = ext_widgets::dropdown_select(
11400        ui,
11401        anchor,
11402        name,
11403        options,
11404        state,
11405        Some(select_popup(
11406            UiRect::new(0.0, 0.0, width, 30.0),
11407            UiRect::new(0.0, 0.0, width + 48.0, 240.0),
11408        )),
11409        dropdown_select_options(width, name, placeholder, accessibility_label),
11410    );
11411    ui.node_mut(nodes.trigger)
11412        .set_action(format!("{name}.toggle"));
11413}
11414
11415fn shader_lab_selected_material(state: &ShowcaseState) -> ElementMaterial {
11416    let shape = state.shader_lab_material_shape.shape();
11417    let mut material = ElementMaterial::new()
11418        .with_paint_outset(LayoutInsets::points(
11419            state
11420                .shader_lab_material_outset
11421                .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11422        ))
11423        .with_clip_shape(shape.clone())
11424        .with_hit_shape(shape)
11425        .with_geometry_effect(state.shader_lab_material_geometry.effect());
11426    if let Some(shader) = shader_lab_material_shader_effect(state) {
11427        material = material.with_shader(shader);
11428    }
11429    material
11430}
11431
11432fn shader_lab_material_shader_effect(state: &ShowcaseState) -> Option<ShaderEffect> {
11433    let phase = state.progress_phase.rem_euclid(1.0);
11434    match state.shader_lab_material_shader {
11435        ShaderLabMaterialShader::None => None,
11436        ShaderLabMaterialShader::Tint => Some(ShaderEffect::tint(color(255, 196, 92), 0.62)),
11437        ShaderLabMaterialShader::Shine => Some(ShaderEffect::shine(phase, 0.92)),
11438        ShaderLabMaterialShader::Glow => Some(ShaderEffect::glow(
11439            color(100, 180, 255),
11440            0.95,
11441            state
11442                .shader_lab_material_outset
11443                .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11444        )),
11445        ShaderLabMaterialShader::Plasma => {
11446            Some(ShaderEffect::plasma(phase, color(82, 190, 255), 0.75, 12.0))
11447        }
11448        ShaderLabMaterialShader::Rings => {
11449            Some(ShaderEffect::rings(phase, color(232, 170, 88), 0.78, 11.0))
11450        }
11451        ShaderLabMaterialShader::Grid => {
11452            Some(ShaderEffect::grid(phase, color(156, 132, 255), 0.85, 9.0))
11453        }
11454    }
11455}
11456
11457fn shader_lab_material_visual(shape: ShaderLabMaterialShape) -> UiVisual {
11458    UiVisual::panel(
11459        color(39, 71, 114),
11460        Some(StrokeStyle::new(color(168, 205, 255), 1.0)),
11461        shape.visual_radius(),
11462    )
11463}
11464
11465fn shader_lab_material_chip(
11466    ui: &mut UiDocument,
11467    parent: UiNodeId,
11468    name: &'static str,
11469    label: &'static str,
11470    material: ElementMaterial,
11471    visual: UiVisual,
11472) {
11473    let chip = ui.add_child(
11474        parent,
11475        UiNode::container(
11476            name,
11477            LayoutStyle::row()
11478                .with_width(156.0)
11479                .with_height(44.0)
11480                .with_padding(8.0)
11481                .with_align_items(taffy::prelude::AlignItems::Center)
11482                .with_justify_content(taffy::prelude::JustifyContent::Center)
11483                .with_flex_shrink(0.0),
11484        )
11485        .with_visual(visual)
11486        .with_material(material)
11487        .with_accessibility(
11488            AccessibilityMeta::new(AccessibilityRole::Image).label(format!("{label} material")),
11489        ),
11490    );
11491    widgets::label(
11492        ui,
11493        chip,
11494        format!("{name}.text"),
11495        label,
11496        text(11.0, color(246, 249, 252)),
11497        LayoutStyle::new().with_flex_shrink(0.0),
11498    );
11499}
11500
11501fn shader_lab_option_checkbox(
11502    ui: &mut UiDocument,
11503    parent: UiNodeId,
11504    name: &'static str,
11505    label: &'static str,
11506    checked: bool,
11507) {
11508    let mut options = widgets::CheckboxOptions::default()
11509        .with_action(name)
11510        .with_text_style(text(12.0, color(220, 228, 238)));
11511    options.layout = LayoutStyle::new()
11512        .with_width(112.0)
11513        .with_height(24.0)
11514        .with_flex_shrink(0.0);
11515    widgets::checkbox(ui, parent, name, label, checked, options);
11516}
11517
11518fn shader_lab_slider_control(
11519    ui: &mut UiDocument,
11520    parent: UiNodeId,
11521    name: &'static str,
11522    label: &'static str,
11523    value: f32,
11524    max: f32,
11525    decimals: usize,
11526) {
11527    let control = ui.add_child(
11528        parent,
11529        UiNode::container(
11530            format!("{name}.control"),
11531            Layout::row()
11532                .size(LayoutSize::new(
11533                    LayoutDimension::points(214.0),
11534                    LayoutDimension::Auto,
11535                ))
11536                .align_items(LayoutAlignment::Center)
11537                .gap(LayoutGap::points(6.0, 6.0))
11538                .flex(0.0, 0.0, LayoutDimension::Auto)
11539                .to_layout_style(),
11540        ),
11541    );
11542    widgets::label(
11543        ui,
11544        control,
11545        format!("{name}.label"),
11546        label,
11547        text(12.0, color(166, 176, 190)),
11548        LayoutStyle::new()
11549            .with_width(46.0)
11550            .with_height(22.0)
11551            .with_flex_shrink(0.0),
11552    );
11553    let mut options = widgets::SliderOptions::default()
11554        .with_layout(
11555            LayoutStyle::new()
11556                .with_width(96.0)
11557                .with_height(22.0)
11558                .with_flex_shrink(0.0),
11559        )
11560        .with_value_edit_action(name);
11561    options.accessibility_label = Some(format!("Shader lab {label}"));
11562    widgets::slider(
11563        ui,
11564        control,
11565        format!("{name}.slider"),
11566        (value / max.max(f32::EPSILON)).clamp(0.0, 1.0),
11567        0.0..1.0,
11568        options,
11569    );
11570    widgets::label(
11571        ui,
11572        control,
11573        format!("{name}.value"),
11574        format!("{value:.decimals$}px"),
11575        text(12.0, color(226, 232, 242)),
11576        LayoutStyle::new()
11577            .with_width(48.0)
11578            .with_height(22.0)
11579            .with_flex_shrink(0.0),
11580    );
11581}
11582
11583fn shader_lab_surface_stroke(state: &ShowcaseState) -> Option<StrokeStyle> {
11584    (state.shader_lab_surface_stroke_width > f32::EPSILON).then(|| {
11585        StrokeStyle::new(
11586            color(150, 180, 235),
11587            state
11588                .shader_lab_surface_stroke_width
11589                .clamp(0.0, SHADER_LAB_SURFACE_STROKE_MAX),
11590        )
11591    })
11592}
11593
11594fn shader_lab_surface_radius(state: &ShowcaseState) -> f32 {
11595    state
11596        .shader_lab_surface_radius
11597        .clamp(0.0, SHADER_LAB_SURFACE_RADIUS_MAX)
11598}
11599
11600fn shader_lab_editor_column(
11601    ui: &mut UiDocument,
11602    parent: UiNodeId,
11603    state: &ShowcaseState,
11604    source_error: Option<&str>,
11605) {
11606    let editor_column = ui.add_child(
11607        parent,
11608        UiNode::container(
11609            "shader_lab.editor.column",
11610            LayoutStyle::column()
11611                .with_width_percent(1.0)
11612                .with_height_percent(1.0)
11613                .with_gap(8.0)
11614                .with_flex_shrink(1.0),
11615        ),
11616    );
11617    let preset_row = row(ui, editor_column, "shader_lab.preset.row", 8.0);
11618    widgets::label(
11619        ui,
11620        preset_row,
11621        "shader_lab.preset.caption",
11622        "Program",
11623        text(12.0, color(166, 176, 190)),
11624        LayoutStyle::new()
11625            .with_width(62.0)
11626            .with_height(30.0)
11627            .with_flex_shrink(0.0),
11628    );
11629    shader_lab_dropdown_select(
11630        ui,
11631        preset_row,
11632        "shader_lab.preset",
11633        &shader_lab_preset_options(),
11634        &state.shader_lab_preset_menu,
11635        180.0,
11636        "WGSL preset",
11637        "Shader lab WGSL preset",
11638    );
11639
11640    let editor_frame = ui.add_child(
11641        editor_column,
11642        UiNode::container(
11643            "shader_lab.editor.frame",
11644            Layout::column()
11645                .size(LayoutSize::new(
11646                    LayoutDimension::percent(1.0),
11647                    LayoutDimension::points(0.0),
11648                ))
11649                .min_size(LayoutSize::points(0.0, SHADER_LAB_EDITOR_HEIGHT))
11650                .flex(1.0, 1.0, LayoutDimension::points(0.0))
11651                .to_layout_style(),
11652        )
11653        .with_visual(UiVisual::panel(
11654            color(18, 22, 28),
11655            Some(StrokeStyle::new(color(72, 84, 104), 1.0)),
11656            4.0,
11657        )),
11658    );
11659    let editor_scroll = widgets::scroll_area(
11660        ui,
11661        editor_frame,
11662        "shader_lab.editor.scroll",
11663        ScrollAxes::BOTH,
11664        LayoutStyle::column()
11665            .with_width_percent(1.0)
11666            .with_height_percent(1.0),
11667    );
11668    ui.node_mut(editor_scroll)
11669        .set_action("shader_lab.editor.scroll");
11670    if let Some(scroll) = ui.node_mut(editor_scroll).scroll_mut() {
11671        scroll.set_offset(state.shader_lab_editor_scroll);
11672    }
11673
11674    let mut code_options = state.text_edit_options(FocusedTextInput::ShaderLabSource);
11675    code_options.edit_action = Some("shader_lab.editor.edit".into());
11676    code_options.visual = UiVisual::TRANSPARENT;
11677    code_options.focused_visual = Some(UiVisual::TRANSPARENT);
11678    code_options.disabled_visual = Some(UiVisual::TRANSPARENT);
11679    widgets::code_editor(
11680        ui,
11681        editor_scroll,
11682        "shader_lab.editor",
11683        &state.shader_lab_source,
11684        code_options,
11685    );
11686    let (validation_text, validation_color) = shader_lab_validation_label(source_error);
11687    widgets::label(
11688        ui,
11689        editor_column,
11690        "shader_lab.validation",
11691        validation_text,
11692        text(11.0, validation_color),
11693        LayoutStyle::new().with_width_percent(1.0),
11694    );
11695}
11696
11697fn shader_lab_editor_content_size(source: &str) -> UiSize {
11698    let style = widgets::code_text_style();
11699    let line_count = source.lines().count().max(1) as f32;
11700    let longest_line = source
11701        .lines()
11702        .map(|line| line.chars().count())
11703        .max()
11704        .unwrap_or(0)
11705        .max(48) as f32;
11706    UiSize::new(
11707        (longest_line * style.font_size * 0.56 + 24.0).max(SHADER_LAB_EDITOR_WIDTH),
11708        (line_count * style.line_height + 18.0).max(SHADER_LAB_EDITOR_HEIGHT),
11709    )
11710}
11711
11712fn shader_lab_preview(
11713    ui: &mut UiDocument,
11714    parent: UiNodeId,
11715    state: &ShowcaseState,
11716    source_valid: bool,
11717) {
11718    match state.shader_lab_target {
11719        ShaderLabTarget::Canvas => {
11720            let mut options = widgets::CanvasOptions::default()
11721                .with_layout(
11722                    LayoutStyle::new()
11723                        .with_width_percent(1.0)
11724                        .with_height_percent(1.0)
11725                        .with_flex_grow(1.0),
11726                )
11727                .with_intrinsic_size(UiSize::new(
11728                    SHADER_LAB_PREVIEW_WIDTH - 20.0,
11729                    SHADER_LAB_PREVIEW_HEIGHT,
11730                ))
11731                .with_accessibility_label("Shader lab canvas preview");
11732            options.visual = UiVisual::panel(color(8, 12, 18), None, 4.0);
11733            widgets::canvas(
11734                ui,
11735                parent,
11736                "shader_lab.preview.canvas",
11737                CanvasContent::new("shader_lab.preview.canvas")
11738                    .program(shader_lab_canvas_program(state, source_valid)),
11739                options,
11740            );
11741        }
11742        ShaderLabTarget::Frame => {
11743            let mut frame_node = UiNode::container(
11744                "shader_lab.preview.frame",
11745                operad::layout::with_min_size(
11746                    LayoutStyle::column()
11747                        .with_width_percent(0.82)
11748                        .with_height_percent(0.62)
11749                        .with_padding(14.0)
11750                        .with_align_items(taffy::prelude::AlignItems::Center)
11751                        .with_justify_content(taffy::prelude::JustifyContent::Center)
11752                        .with_flex_shrink(0.0),
11753                    operad::layout::px(SHADER_LAB_FRAME_MIN_WIDTH),
11754                    operad::layout::px(SHADER_LAB_FRAME_MIN_HEIGHT),
11755                ),
11756            )
11757            .with_visual(UiVisual::panel(
11758                ColorRgba::new(0, 0, 0, 0),
11759                shader_lab_surface_stroke(state),
11760                shader_lab_surface_radius(state),
11761            ));
11762            frame_node.style_mut().set_clip(ClipBehavior::Clip);
11763            let frame = ui.add_child(parent, frame_node);
11764            shader_lab_canvas_layer_fill(
11765                ui,
11766                frame,
11767                "shader_lab.preview.frame.shader",
11768                state,
11769                source_valid,
11770            );
11771            if state.shader_lab_show_frame_text {
11772                let label = widgets::label(
11773                    ui,
11774                    frame,
11775                    "shader_lab.preview.frame.label",
11776                    "WGSL frame",
11777                    text(14.0, color(246, 249, 252)),
11778                    LayoutStyle::new().with_flex_shrink(0.0),
11779                );
11780                ui.node_mut(label).style_mut().set_z_index(1);
11781            }
11782        }
11783        ShaderLabTarget::Button => {
11784            let mut shell_node = UiNode::container(
11785                "shader_lab.preview.button.shell",
11786                LayoutStyle::column()
11787                    .with_width(SHADER_LAB_BUTTON_WIDTH)
11788                    .with_height(SHADER_LAB_BUTTON_HEIGHT)
11789                    .with_align_items(taffy::prelude::AlignItems::Center)
11790                    .with_justify_content(taffy::prelude::JustifyContent::Center),
11791            )
11792            .with_visual(UiVisual::TRANSPARENT);
11793            shell_node.style_mut().set_clip(ClipBehavior::Clip);
11794            let shell = ui.add_child(parent, shell_node);
11795            shader_lab_canvas_layer_fill(
11796                ui,
11797                shell,
11798                "shader_lab.preview.button.shader",
11799                state,
11800                source_valid,
11801            );
11802            let mut options = widgets::ButtonOptions::new(
11803                LayoutStyle::new()
11804                    .with_width(SHADER_LAB_BUTTON_WIDTH)
11805                    .with_height(SHADER_LAB_BUTTON_HEIGHT),
11806            )
11807            .with_action("shader_lab.preview.button")
11808            .with_accessibility_label("Shader button preview");
11809            options.visual = UiVisual::panel(
11810                ColorRgba::new(0, 0, 0, 0),
11811                shader_lab_surface_stroke(state),
11812                shader_lab_surface_radius(state),
11813            );
11814            options.hovered_visual = Some(UiVisual::panel(
11815                ColorRgba::new(255, 255, 255, 28),
11816                shader_lab_surface_stroke(state),
11817                shader_lab_surface_radius(state),
11818            ));
11819            options.pressed_visual = Some(UiVisual::panel(
11820                ColorRgba::new(0, 0, 0, 48),
11821                shader_lab_surface_stroke(state),
11822                shader_lab_surface_radius(state),
11823            ));
11824            options.text_style = text(14.0, color(246, 249, 252));
11825            let button = widgets::button(
11826                ui,
11827                shell,
11828                "shader_lab.preview.button",
11829                if state.shader_lab_show_button_text {
11830                    "Shader button"
11831                } else {
11832                    ""
11833                },
11834                options,
11835            );
11836            ui.node_mut(button).style_mut().set_z_index(1);
11837        }
11838    }
11839}
11840
11841fn shader_lab_canvas_layer_fill(
11842    ui: &mut UiDocument,
11843    parent: UiNodeId,
11844    name: &'static str,
11845    state: &ShowcaseState,
11846    source_valid: bool,
11847) -> UiNodeId {
11848    let layout = operad::layout::with_absolute_position(
11849        LayoutStyle::new()
11850            .with_width_percent(1.0)
11851            .with_height_percent(1.0),
11852        0.0,
11853        0.0,
11854    );
11855    let mut options = widgets::CanvasOptions::default()
11856        .with_layout(layout)
11857        .with_intrinsic_size(UiSize::new(
11858            SHADER_LAB_PREVIEW_WIDTH,
11859            SHADER_LAB_PREVIEW_HEIGHT,
11860        ))
11861        .with_accessibility_label(format!("{name} shader preview"));
11862    options.input = InputBehavior::NONE;
11863    options.visual = UiVisual::TRANSPARENT;
11864    let canvas = widgets::canvas(
11865        ui,
11866        parent,
11867        name,
11868        CanvasContent::new(name).program(shader_lab_canvas_program(state, source_valid)),
11869        options,
11870    );
11871    ui.node_mut(canvas).style_mut().set_z_index(0);
11872    canvas
11873}
11874
11875fn shader_lab_status_label(state: &ShowcaseState, source_error: Option<&str>) -> Option<String> {
11876    if source_error.is_some() {
11877        Some(format!(
11878            "{} fallback until WGSL validates",
11879            state.shader_lab_target.label()
11880        ))
11881    } else {
11882        None
11883    }
11884}
11885
11886fn shader_lab_validation_label(source_error: Option<&str>) -> (String, ColorRgba) {
11887    if let Some(error) = source_error {
11888        (
11889            format!("WGSL error: {}", compact_shader_error(error, 160)),
11890            color(255, 139, 128),
11891        )
11892    } else {
11893        ("WGSL valid".to_string(), color(112, 221, 160))
11894    }
11895}
11896
11897fn compact_shader_error(error: &str, max_chars: usize) -> String {
11898    let mut compact = error.split_whitespace().collect::<Vec<_>>().join(" ");
11899    if compact.chars().count() > max_chars {
11900        compact = compact.chars().take(max_chars.saturating_sub(3)).collect();
11901        compact.push_str("...");
11902    }
11903    compact
11904}
11905
11906fn shader_lab_canvas_program(state: &ShowcaseState, source_valid: bool) -> CanvasRenderProgram {
11907    let source = if source_valid {
11908        state.shader_lab_source.text().to_string()
11909    } else {
11910        SHADER_LAB_ERROR_WGSL.to_string()
11911    };
11912    CanvasRenderProgram::wgsl(source)
11913        .label("showcase.shader_lab.canvas")
11914        .constant("TIME", state.progress_phase as f64)
11915        .clear_color(Some(color(8, 12, 18)))
11916}
11917
11918fn shader_lab_source_error(source: &str) -> Option<String> {
11919    if !shader_lab_source_has_entry_points(source) {
11920        return Some("source must define @vertex fn vs_main and @fragment fn fs_main".to_string());
11921    }
11922
11923    shader_lab_compile_error(source)
11924}
11925
11926#[cfg(feature = "wgpu")]
11927fn shader_lab_compile_error(source: &str) -> Option<String> {
11928    let module = match naga::front::wgsl::parse_str(source) {
11929        Ok(module) => module,
11930        Err(error) => return Some(error.emit_to_string(source)),
11931    };
11932    let mut validator = naga::valid::Validator::new(
11933        naga::valid::ValidationFlags::all(),
11934        naga::valid::Capabilities::empty(),
11935    );
11936    validator
11937        .validate(&module)
11938        .err()
11939        .map(|error| error.to_string())
11940}
11941
11942#[cfg(not(feature = "wgpu"))]
11943fn shader_lab_compile_error(_source: &str) -> Option<String> {
11944    None
11945}
11946
11947fn shader_lab_source_has_entry_points(source: &str) -> bool {
11948    source.contains("@vertex")
11949        && source.contains("fn vs_main")
11950        && source.contains("@fragment")
11951        && source.contains("fn fs_main")
11952}
11953
11954fn timeline_ruler(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11955    let body =
11956        section_with_min_viewport(ui, parent, "timeline", "Timeline", UiSize::new(560.0, 0.0));
11957    widgets::label(
11958        ui,
11959        body,
11960        "timeline.label",
11961        "Clip timeline",
11962        text(12.0, color(166, 176, 190)),
11963        LayoutStyle::new().with_width_percent(1.0),
11964    );
11965    widgets::label(
11966        ui,
11967        body,
11968        "timeline.description",
11969        "The ruler maps time to tracks, clips, markers, and the current playhead.",
11970        text(12.0, color(196, 210, 230)),
11971        LayoutStyle::new().with_width_percent(1.0),
11972    );
11973
11974    let editor = row(ui, body, "timeline.editor", 0.0);
11975    let labels = ui.add_child(
11976        editor,
11977        UiNode::container(
11978            "timeline.lane_labels",
11979            LayoutStyle::column()
11980                .with_width(96.0)
11981                .with_height(172.0)
11982                .with_flex_shrink(0.0),
11983        ),
11984    );
11985    for (name, label, height) in [
11986        ("timeline.lane_labels.header", "Tracks", 40.0),
11987        ("timeline.lane_labels.video", "Video", 44.0),
11988        ("timeline.lane_labels.audio", "Audio", 44.0),
11989        ("timeline.lane_labels.notes", "Notes", 44.0),
11990    ] {
11991        widgets::label(
11992            ui,
11993            labels,
11994            name,
11995            label,
11996            text(11.0, color(166, 176, 190)),
11997            LayoutStyle::new()
11998                .with_width_percent(1.0)
11999                .with_height(height)
12000                .with_flex_shrink(0.0),
12001        );
12002    }
12003
12004    let timeline_scroll = timeline_scroll_state_for_view(
12005        state.timeline_scroll,
12006        state.timeline_scroll.viewport_size().width,
12007    );
12008    let range = ext_widgets::TimelineRange::new(0.0, 48.0);
12009    let nodes = scroll_area_widgets::scroll_container_shell(
12010        ui,
12011        editor,
12012        "timeline",
12013        timeline_scroll,
12014        widgets::ScrollContainerOptions::default()
12015            .with_axes(ScrollAxes::HORIZONTAL)
12016            .with_action_prefix("timeline")
12017            .with_gap(4.0)
12018            .with_scrollbar_thickness(TIMELINE_SCROLLBAR_HEIGHT)
12019            .with_layout(
12020                LayoutStyle::column()
12021                    .with_width(0.0)
12022                    .with_flex_grow(1.0)
12023                    .with_height(TIMELINE_SCROLL_CONTAINER_HEIGHT)
12024                    .with_flex_shrink(0.0),
12025            )
12026            .with_viewport_layout(
12027                LayoutStyle::column()
12028                    .with_width(0.0)
12029                    .with_flex_grow(1.0)
12030                    .with_height(TIMELINE_VIEWPORT_HEIGHT)
12031                    .with_flex_shrink(1.0),
12032            )
12033            .with_horizontal_scrollbar(
12034                scrollbar_widgets::ScrollbarOptions::default()
12035                    .with_action("timeline.horizontal-scrollbar"),
12036            )
12037            .with_accessibility_label("Timeline horizontal scroller"),
12038    );
12039    let content = ui.add_child(
12040        nodes.viewport,
12041        UiNode::container(
12042            "timeline.content",
12043            LayoutStyle::column()
12044                .with_width(TIMELINE_CONTENT_WIDTH)
12045                .with_height(TIMELINE_VIEWPORT_HEIGHT)
12046                .with_flex_shrink(0.0),
12047        ),
12048    );
12049    let mut ruler_options = ext_widgets::TimelineRulerOptions::default();
12050    ruler_options.height = 40.0;
12051    ruler_options.layout = LayoutStyle::new()
12052        .with_width(TIMELINE_CONTENT_WIDTH)
12053        .with_height(40.0)
12054        .with_flex_shrink(0.0);
12055    ruler_options.accessibility_label = Some("Editing timeline ruler".to_string());
12056    ruler_options.accessibility_hint =
12057        Some("Shows seconds for the visible timeline clips".to_string());
12058    ext_widgets::timeline_ruler(
12059        ui,
12060        content,
12061        "timeline.ruler",
12062        ext_widgets::RulerSpec {
12063            range,
12064            width: TIMELINE_CONTENT_WIDTH,
12065            major_step: 4.0,
12066            minor_step: 1.0,
12067            label_every: 1,
12068        },
12069        ruler_options,
12070    );
12071    ui.add_child(
12072        content,
12073        UiNode::scene(
12074            "timeline.tracks",
12075            timeline_track_primitives(range, TIMELINE_CONTENT_WIDTH),
12076            LayoutStyle::new()
12077                .with_width(TIMELINE_CONTENT_WIDTH)
12078                .with_height(132.0)
12079                .with_flex_shrink(0.0),
12080        ),
12081    );
12082}
12083
12084fn timeline_track_primitives(range: ext_widgets::TimelineRange, width: f32) -> Vec<ScenePrimitive> {
12085    let mut primitives = Vec::new();
12086    let lane_height = 36.0;
12087    let lane_gap = 8.0;
12088    let lanes = [
12089        ("Video", 0.0, color(16, 22, 30)),
12090        ("Audio", lane_height + lane_gap, color(13, 20, 27)),
12091        ("Notes", (lane_height + lane_gap) * 2.0, color(16, 22, 30)),
12092    ];
12093
12094    for (label, y, fill) in lanes {
12095        primitives.push(ScenePrimitive::Rect(
12096            PaintRect::solid(UiRect::new(0.0, y, width, lane_height), fill)
12097                .stroke(AlignedStroke::inside(StrokeStyle::new(
12098                    color(38, 49, 64),
12099                    1.0,
12100                )))
12101                .corner_radii(CornerRadii::uniform(2.0)),
12102        ));
12103        primitives.push(ScenePrimitive::Text(
12104            PaintText::new(
12105                label,
12106                UiRect::new(8.0, y + 8.0, 72.0, 18.0),
12107                text(10.0, color(116, 128, 145)),
12108            )
12109            .multiline(false),
12110        ));
12111    }
12112
12113    for second in (0..=48).step_by(4) {
12114        let x = range.value_to_x(second as f64, width);
12115        primitives.push(ScenePrimitive::Line {
12116            from: UiPoint::new(x, 0.0),
12117            to: UiPoint::new(x, 124.0),
12118            stroke: StrokeStyle::new(color(34, 44, 58), 1.0),
12119        });
12120    }
12121
12122    push_timeline_clip(
12123        &mut primitives,
12124        range,
12125        width,
12126        "Intro",
12127        2.0,
12128        10.0,
12129        0.0,
12130        color(57, 126, 207),
12131    );
12132    push_timeline_clip(
12133        &mut primitives,
12134        range,
12135        width,
12136        "Cutaway",
12137        12.0,
12138        24.0,
12139        0.0,
12140        color(95, 107, 212),
12141    );
12142    push_timeline_clip(
12143        &mut primitives,
12144        range,
12145        width,
12146        "Final",
12147        28.0,
12148        44.0,
12149        0.0,
12150        color(68, 153, 122),
12151    );
12152    push_timeline_clip(
12153        &mut primitives,
12154        range,
12155        width,
12156        "Music bed",
12157        0.0,
12158        48.0,
12159        lane_height + lane_gap,
12160        color(205, 160, 71),
12161    );
12162    push_timeline_clip(
12163        &mut primitives,
12164        range,
12165        width,
12166        "Voiceover",
12167        8.0,
12168        18.0,
12169        lane_height + lane_gap,
12170        color(183, 107, 185),
12171    );
12172
12173    for (second, label) in [(6.0, "Beat"), (21.0, "Cut"), (37.0, "Cue")] {
12174        let x = range.value_to_x(second, width);
12175        let y = (lane_height + lane_gap) * 2.0 + 8.0;
12176        primitives.push(ScenePrimitive::Polygon {
12177            points: vec![
12178                UiPoint::new(x, y),
12179                UiPoint::new(x + 8.0, y + 8.0),
12180                UiPoint::new(x, y + 16.0),
12181                UiPoint::new(x - 8.0, y + 8.0),
12182            ],
12183            fill: color(245, 198, 83),
12184            stroke: Some(StrokeStyle::new(color(255, 234, 178), 1.0)),
12185        });
12186        primitives.push(ScenePrimitive::Text(
12187            PaintText::new(
12188                label,
12189                UiRect::new(x + 12.0, y - 1.0, 72.0, 18.0),
12190                text(10.0, color(225, 233, 244)),
12191            )
12192            .multiline(false),
12193        ));
12194    }
12195
12196    let playhead_x = range.value_to_x(18.5, width);
12197    primitives.push(ScenePrimitive::Line {
12198        from: UiPoint::new(playhead_x, 0.0),
12199        to: UiPoint::new(playhead_x, 124.0),
12200        stroke: StrokeStyle::new(ColorRgba::new(255, 120, 96, 255), 2.0),
12201    });
12202    primitives.push(ScenePrimitive::Text(
12203        PaintText::new(
12204            "Playhead 18.5s",
12205            UiRect::new(playhead_x + 8.0, 106.0, 120.0, 18.0),
12206            text(10.0, ColorRgba::new(255, 172, 154, 255)),
12207        )
12208        .multiline(false),
12209    ));
12210
12211    primitives
12212}
12213
12214#[allow(clippy::too_many_arguments)]
12215fn push_timeline_clip(
12216    primitives: &mut Vec<ScenePrimitive>,
12217    range: ext_widgets::TimelineRange,
12218    width: f32,
12219    label: &'static str,
12220    start: f64,
12221    end: f64,
12222    lane_y: f32,
12223    fill: ColorRgba,
12224) {
12225    let x = range.value_to_x(start, width);
12226    let right = range.value_to_x(end, width);
12227    let rect = UiRect::new(x, lane_y + 6.0, (right - x).max(1.0), 24.0);
12228    primitives.push(ScenePrimitive::Rect(
12229        PaintRect::solid(rect, fill)
12230            .stroke(AlignedStroke::inside(StrokeStyle::new(
12231                ColorRgba::new(230, 240, 255, 96),
12232                1.0,
12233            )))
12234            .corner_radii(CornerRadii::uniform(4.0)),
12235    ));
12236    primitives.push(ScenePrimitive::Text(
12237        PaintText::new(label, rect, text(10.0, color(245, 248, 252)))
12238            .horizontal_align(TextHorizontalAlign::Center)
12239            .vertical_align(TextVerticalAlign::Center)
12240            .multiline(false),
12241    ));
12242}
12243
12244fn theme_demo_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState, theme: &Theme) {
12245    let body = section(ui, parent, "theme", "Theme");
12246    widgets::label(
12247        ui,
12248        body,
12249        "theme.current",
12250        format!("Current theme: {}", theme.name),
12251        themed_text(theme, 14.0),
12252        LayoutStyle::new().with_width_percent(1.0),
12253    );
12254
12255    let choices = wrapping_row(ui, body, "theme.choices", 8.0);
12256    for choice in [
12257        ShowcaseThemeChoice::Light,
12258        ShowcaseThemeChoice::Dark,
12259        ShowcaseThemeChoice::Bubblegum,
12260    ] {
12261        theme_choice_button(
12262            ui,
12263            choices,
12264            choice,
12265            state.showcase_theme == choice,
12266            choice.theme(),
12267        );
12268    }
12269
12270    let swatches = wrapping_row(ui, body, "theme.swatches", 8.0);
12271    theme_swatch(
12272        ui,
12273        swatches,
12274        "theme.swatch.canvas",
12275        "Canvas",
12276        theme.colors.canvas,
12277        theme,
12278    );
12279    theme_swatch(
12280        ui,
12281        swatches,
12282        "theme.swatch.surface",
12283        "Surface",
12284        theme.colors.surface,
12285        theme,
12286    );
12287    theme_swatch(
12288        ui,
12289        swatches,
12290        "theme.swatch.accent",
12291        "Accent",
12292        theme.colors.accent,
12293        theme,
12294    );
12295    theme_swatch(
12296        ui,
12297        swatches,
12298        "theme.swatch.selected",
12299        "Selected",
12300        theme.colors.selected,
12301        theme,
12302    );
12303
12304    let preview = ui.add_child(
12305        body,
12306        UiNode::container(
12307            "theme.preview",
12308            LayoutStyle::column()
12309                .with_width_percent(1.0)
12310                .with_padding(12.0)
12311                .with_gap(10.0)
12312                .with_flex_shrink(0.0),
12313        )
12314        .with_visual(UiVisual::panel(
12315            theme.colors.surface,
12316            Some(theme.stroke.surface),
12317            theme.radius.md,
12318        ))
12319        .with_accessibility(
12320            AccessibilityMeta::new(AccessibilityRole::Group).label("Theme preview"),
12321        ),
12322    );
12323    widgets::label(
12324        ui,
12325        preview,
12326        "theme.preview.title",
12327        "Preview controls",
12328        themed_text(theme, 13.0),
12329        LayoutStyle::new().with_width_percent(1.0),
12330    );
12331    let preview_row = row(ui, preview, "theme.preview.controls", 8.0);
12332    let mut primary = themed_button_options(
12333        theme,
12334        "theme.preview.primary",
12335        ComponentState::ACTIVE,
12336        LayoutStyle::new().with_height(34.0),
12337    );
12338    primary.accessibility_label = Some("Primary preview button".to_owned());
12339    widgets::button(ui, preview_row, "theme.preview.primary", "Primary", primary);
12340    let mut secondary = themed_button_options(
12341        theme,
12342        "theme.preview.secondary",
12343        ComponentState::NORMAL,
12344        LayoutStyle::new().with_height(34.0),
12345    );
12346    secondary.accessibility_label = Some("Secondary preview button".to_owned());
12347    widgets::button(
12348        ui,
12349        preview_row,
12350        "theme.preview.secondary",
12351        "Secondary",
12352        secondary,
12353    );
12354    let mut help = themed_muted_text(theme, 12.0);
12355    help.wrap = TextWrap::WordOrGlyph;
12356    widgets::label(
12357        ui,
12358        preview,
12359        "theme.preview.copy",
12360        "The selected theme drives the app background, right panel, floating windows, and this preview.",
12361        help,
12362        LayoutStyle::new().with_width_percent(1.0),
12363    );
12364}
12365
12366fn theme_choice_button(
12367    ui: &mut UiDocument,
12368    parent: UiNodeId,
12369    choice: ShowcaseThemeChoice,
12370    selected: bool,
12371    preview_theme: Theme,
12372) {
12373    let mut options = themed_button_options(
12374        &preview_theme,
12375        choice.action(),
12376        if selected {
12377            ComponentState::SELECTED
12378        } else {
12379            ComponentState::NORMAL
12380        },
12381        LayoutStyle::new()
12382            .with_width(116.0)
12383            .with_height(34.0)
12384            .with_flex_shrink(0.0),
12385    )
12386    .with_action(choice.action());
12387    options.accessibility_label = Some(format!("Use {} theme", choice.label()));
12388    widgets::button(
12389        ui,
12390        parent,
12391        format!("theme.choice.{}", choice.label().to_ascii_lowercase()),
12392        choice.label(),
12393        options,
12394    );
12395}
12396
12397fn theme_swatch(
12398    ui: &mut UiDocument,
12399    parent: UiNodeId,
12400    name: &'static str,
12401    label: &'static str,
12402    swatch_color: ColorRgba,
12403    theme: &Theme,
12404) {
12405    let tile = ui.add_child(
12406        parent,
12407        UiNode::container(
12408            name,
12409            LayoutStyle::column()
12410                .with_width(92.0)
12411                .with_height(76.0)
12412                .with_padding(8.0)
12413                .with_gap(6.0)
12414                .with_flex_shrink(0.0),
12415        )
12416        .with_visual(UiVisual::panel(
12417            theme.colors.surface_muted,
12418            Some(theme.stroke.surface),
12419            4.0,
12420        ))
12421        .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(label)),
12422    );
12423    ui.add_child(
12424        tile,
12425        UiNode::container(
12426            format!("{name}.color"),
12427            LayoutStyle::new()
12428                .with_width_percent(1.0)
12429                .with_height(26.0)
12430                .with_flex_shrink(0.0),
12431        )
12432        .with_visual(UiVisual::panel(
12433            swatch_color,
12434            Some(StrokeStyle::new(theme.colors.border_strong, 1.0)),
12435            4.0,
12436        )),
12437    );
12438    widgets::label(
12439        ui,
12440        tile,
12441        format!("{name}.label"),
12442        label,
12443        themed_muted_text(theme, 11.0),
12444        LayoutStyle::new().with_width_percent(1.0),
12445    );
12446}
12447
12448fn themed_button_options(
12449    theme: &Theme,
12450    action: impl Into<String>,
12451    state: ComponentState,
12452    layout: LayoutStyle,
12453) -> widgets::ButtonOptions {
12454    let mut options = widgets::ButtonOptions::new(layout).with_action(action.into());
12455    options.visual = theme.resolve_visual(ComponentRole::Button, state);
12456    options.hovered_visual =
12457        Some(theme.resolve_visual(ComponentRole::Button, ComponentState::HOVERED));
12458    options.pressed_visual =
12459        Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
12460    options.pressed_hovered_visual =
12461        Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
12462    options.focused_visual =
12463        Some(theme.resolve_visual(ComponentRole::Button, ComponentState::FOCUSED));
12464    options.disabled_visual =
12465        Some(theme.resolve_visual(ComponentRole::Button, ComponentState::DISABLED));
12466    options.text_style = theme.resolve_text(ComponentRole::Button, state);
12467    options
12468}
12469
12470fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12471    let preview_scene_size = style_preview_scene_size(state.styling);
12472    let preview_min_width = preview_scene_size.width + 16.0;
12473    let preview_min_height = preview_scene_size.height + 16.0;
12474    let body_min_width = STYLING_CONTROLS_WIDTH + 1.0 + preview_min_width + 20.0;
12475    let body = section_with_min_viewport(
12476        ui,
12477        parent,
12478        "styling",
12479        "Styling",
12480        UiSize::new(body_min_width, preview_min_height),
12481    );
12482    let grid_layout = operad::layout::with_grid_template_columns(
12483        Layout::grid()
12484            .size(LayoutSize::percent(1.0, 1.0))
12485            .gap(LayoutGap::points(10.0, 10.0))
12486            .to_layout_style(),
12487        [
12488            LayoutGridTrack::points(STYLING_CONTROLS_WIDTH),
12489            LayoutGridTrack::points(1.0),
12490            LayoutGridTrack::minmax_points_fraction(preview_min_width, 1.0),
12491        ],
12492    );
12493    let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
12494    let controls = ui.add_child(
12495        grid,
12496        UiNode::container(
12497            "styling.controls",
12498            LayoutStyle::column()
12499                .with_width(STYLING_CONTROLS_WIDTH)
12500                .with_height_percent(1.0)
12501                .with_flex_shrink(0.0)
12502                .gap(6.0),
12503        ),
12504    );
12505    style_edge_group(
12506        ui,
12507        controls,
12508        "styling.inner",
12509        "Inner margin",
12510        "styling.inner_same",
12511        state.styling.inner_same,
12512        [
12513            ("Left", "styling.inner", state.styling.inner_margin),
12514            ("Right", "styling.inner_right", state.styling.inner_right),
12515            ("Top", "styling.inner_top", state.styling.inner_top),
12516            ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
12517        ],
12518        0.0..32.0,
12519    );
12520    style_edge_group(
12521        ui,
12522        controls,
12523        "styling.outer",
12524        "Outer margin",
12525        "styling.outer_same",
12526        state.styling.outer_same,
12527        [
12528            ("Left", "styling.outer", state.styling.outer_margin),
12529            ("Right", "styling.outer_right", state.styling.outer_right),
12530            ("Top", "styling.outer_top", state.styling.outer_top),
12531            ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
12532        ],
12533        0.0..40.0,
12534    );
12535    style_edge_group(
12536        ui,
12537        controls,
12538        "styling.radius",
12539        "Corner radius",
12540        "styling.radius_same",
12541        state.styling.radius_same,
12542        [
12543            ("NW", "styling.radius", state.styling.corner_radius),
12544            ("NE", "styling.radius_ne", state.styling.corner_ne),
12545            ("SW", "styling.radius_sw", state.styling.corner_sw),
12546            ("SE", "styling.radius_se", state.styling.corner_se),
12547        ],
12548        0.0..28.0,
12549    );
12550    style_fill_group(ui, controls, state);
12551    style_stroke_group(ui, controls, state);
12552    style_shadow_group(ui, controls, state);
12553    widgets::separator(
12554        ui,
12555        grid,
12556        "styling.preview.separator",
12557        widgets::SeparatorOptions::vertical().with_layout(
12558            LayoutStyle::new()
12559                .with_width(1.0)
12560                .with_height_percent(1.0)
12561                .with_flex_shrink(0.0),
12562        ),
12563    );
12564
12565    let preview = ui.add_child(
12566        grid,
12567        UiNode::container(
12568            "styling.preview",
12569            operad::layout::with_min_size(
12570                LayoutStyle::column()
12571                    .with_width_percent(1.0)
12572                    .with_height_percent(1.0)
12573                    .with_flex_shrink(0.0)
12574                    .padding(8.0),
12575                operad::layout::px(preview_min_width),
12576                operad::layout::px(preview_min_height),
12577            ),
12578        )
12579        .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
12580    );
12581    style_preview(ui, preview, state.styling);
12582}
12583
12584#[allow(clippy::too_many_arguments)]
12585fn style_edge_group(
12586    ui: &mut UiDocument,
12587    parent: UiNodeId,
12588    name: &'static str,
12589    title: &'static str,
12590    same_action: &'static str,
12591    same: bool,
12592    values: [(&'static str, &'static str, f32); 4],
12593    range: std::ops::Range<f32>,
12594) {
12595    let group = style_control_group(ui, parent, format!("{name}.group"));
12596    style_group_title(ui, group, format!("{name}.title"), title);
12597    let fields = ui.add_child(
12598        group,
12599        UiNode::container(
12600            format!("{name}.fields"),
12601            LayoutStyle::column()
12602                .with_width(138.0)
12603                .with_flex_shrink(0.0)
12604                .gap(3.0),
12605        ),
12606    );
12607    style_compact_checkbox(ui, fields, same_action, "same", same);
12608    if same {
12609        style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
12610    } else {
12611        for (label, action, value) in values {
12612            style_number_row(ui, fields, action, label, value, range.clone(), 0);
12613        }
12614    }
12615}
12616
12617fn style_fill_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12618    let group = style_control_group(ui, parent, "styling.fill.group");
12619    style_group_title(ui, group, "styling.fill.title", "Fill");
12620    let fields = style_group_fields(
12621        ui,
12622        group,
12623        "styling.fill.fields",
12624        STYLING_WIDE_FIELDS_WIDTH,
12625        4.0,
12626    );
12627    style_color_button_row(
12628        ui,
12629        fields,
12630        "styling.fill_color_button",
12631        "",
12632        state.styling.fill_color(),
12633        "Pick fill color",
12634    );
12635    if state.styling_fill_picker_open {
12636        ext_widgets::color_picker(
12637            ui,
12638            fields,
12639            "styling.fill_picker",
12640            &state.styling_fill_picker,
12641            ext_widgets::ColorPickerOptions::default()
12642                .with_label("Fill")
12643                .with_action_prefix("styling.fill_picker"),
12644        );
12645    }
12646}
12647
12648fn style_stroke_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12649    let group = style_control_group(ui, parent, "styling.stroke.group");
12650    style_group_title(ui, group, "styling.stroke.title", "Stroke");
12651    let fields = style_group_fields(
12652        ui,
12653        group,
12654        "styling.stroke.fields",
12655        STYLING_WIDE_FIELDS_WIDTH,
12656        4.0,
12657    );
12658    let width_row = row(ui, fields, "styling.stroke.row", 6.0);
12659    style_inline_number(
12660        ui,
12661        width_row,
12662        "styling.stroke",
12663        "width",
12664        state.styling.stroke_width,
12665        0.0..STYLING_STROKE_MAX,
12666        1,
12667    );
12668    let mut options = widgets::SliderOptions::default()
12669        .with_layout(
12670            LayoutStyle::new()
12671                .with_width(60.0)
12672                .with_height(20.0)
12673                .with_flex_shrink(0.0),
12674        )
12675        .with_value_edit_action("styling.stroke");
12676    options.fill_color = color(120, 170, 230);
12677    widgets::slider(
12678        ui,
12679        width_row,
12680        "styling.stroke.slider",
12681        (state.styling.stroke_width / STYLING_STROKE_MAX).clamp(0.0, 1.0),
12682        0.0..1.0,
12683        options,
12684    );
12685    style_color_button_row(
12686        ui,
12687        fields,
12688        "styling.stroke_color_button",
12689        "",
12690        state.styling.stroke_color(),
12691        "Pick stroke color",
12692    );
12693    if state.styling_stroke_picker_open {
12694        ext_widgets::color_picker(
12695            ui,
12696            fields,
12697            "styling.stroke_picker",
12698            &state.styling_stroke_picker,
12699            ext_widgets::ColorPickerOptions::default()
12700                .with_label("Stroke color")
12701                .with_action_prefix("styling.stroke_picker"),
12702        );
12703    }
12704}
12705
12706fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12707    let group = style_control_group(ui, parent, "styling.shadow.group");
12708    style_group_title(ui, group, "styling.shadow.title", "Shadow");
12709    let fields = style_group_fields(
12710        ui,
12711        group,
12712        "styling.shadow.fields",
12713        STYLING_WIDE_FIELDS_WIDTH,
12714        4.0,
12715    );
12716    let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
12717    style_inline_number(
12718        ui,
12719        offsets,
12720        "styling.shadow_x",
12721        "x",
12722        state.styling.shadow_x,
12723        -24.0..24.0,
12724        0,
12725    );
12726    style_inline_number(
12727        ui,
12728        offsets,
12729        "styling.shadow_y",
12730        "y",
12731        state.styling.shadow_y,
12732        -24.0..24.0,
12733        0,
12734    );
12735    let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
12736    style_inline_number(
12737        ui,
12738        spread,
12739        "styling.shadow",
12740        "blur",
12741        state.styling.shadow_blur,
12742        0.0..32.0,
12743        0,
12744    );
12745    style_inline_number(
12746        ui,
12747        spread,
12748        "styling.shadow_spread",
12749        "spread",
12750        state.styling.shadow_spread,
12751        0.0..16.0,
12752        0,
12753    );
12754    style_color_button_row(
12755        ui,
12756        fields,
12757        "styling.shadow_color_button",
12758        "",
12759        state.styling.shadow_color(),
12760        "Pick shadow color",
12761    );
12762    if state.styling_shadow_picker_open {
12763        ext_widgets::color_picker(
12764            ui,
12765            fields,
12766            "styling.shadow_picker",
12767            &state.styling_shadow_picker,
12768            ext_widgets::ColorPickerOptions::default()
12769                .with_label("Shadow color")
12770                .with_action_prefix("styling.shadow_picker"),
12771        );
12772    }
12773}
12774
12775fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
12776    ui.add_child(
12777        parent,
12778        UiNode::container(
12779            name,
12780            LayoutStyle::row()
12781                .with_width_percent(1.0)
12782                .with_flex_shrink(0.0)
12783                .padding(4.0)
12784                .gap(8.0),
12785        )
12786        .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
12787    )
12788}
12789
12790fn style_group_fields(
12791    ui: &mut UiDocument,
12792    parent: UiNodeId,
12793    name: impl Into<String>,
12794    width: f32,
12795    gap: f32,
12796) -> UiNodeId {
12797    ui.add_child(
12798        parent,
12799        UiNode::container(
12800            name,
12801            LayoutStyle::column()
12802                .with_width(width)
12803                .with_flex_shrink(0.0)
12804                .gap(gap),
12805        ),
12806    )
12807}
12808
12809fn style_group_title(
12810    ui: &mut UiDocument,
12811    parent: UiNodeId,
12812    name: impl Into<String>,
12813    label: &'static str,
12814) {
12815    widgets::label(
12816        ui,
12817        parent,
12818        name,
12819        label,
12820        text(12.0, color(166, 176, 190)),
12821        LayoutStyle::new()
12822            .with_width(88.0)
12823            .with_flex_shrink(0.0)
12824            .with_height(22.0),
12825    );
12826}
12827
12828fn style_color_button_row(
12829    ui: &mut UiDocument,
12830    parent: UiNodeId,
12831    action: &'static str,
12832    label: &'static str,
12833    value: ColorRgba,
12834    accessibility_label: &'static str,
12835) {
12836    let row = row(ui, parent, format!("{action}.row"), 8.0);
12837    if !label.is_empty() {
12838        widgets::label(
12839            ui,
12840            row,
12841            format!("{action}.label"),
12842            label,
12843            text(12.0, color(166, 176, 190)),
12844            LayoutStyle::new()
12845                .with_width(86.0)
12846                .with_flex_shrink(0.0)
12847                .with_height(24.0),
12848        );
12849    }
12850    ext_widgets::color_edit_button(
12851        ui,
12852        row,
12853        action,
12854        value,
12855        color_mini_button_options(action)
12856            .with_format(ext_widgets::ColorValueFormat::Rgba)
12857            .accessibility_label(accessibility_label),
12858    );
12859    widgets::label(
12860        ui,
12861        row,
12862        format!("{action}.value"),
12863        ext_widgets::color_picker::format_hex_color(value, value.a < 255),
12864        text(12.0, color(226, 232, 242)),
12865        LayoutStyle::new().with_width(96.0).with_height(24.0),
12866    );
12867}
12868
12869fn style_number_row(
12870    ui: &mut UiDocument,
12871    parent: UiNodeId,
12872    name: &'static str,
12873    label: &'static str,
12874    value: f32,
12875    range: std::ops::Range<f32>,
12876    decimals: u8,
12877) {
12878    let row = row(ui, parent, format!("{name}.row"), 6.0);
12879    widgets::label(
12880        ui,
12881        row,
12882        format!("{name}.label"),
12883        label,
12884        text(12.0, color(166, 176, 190)),
12885        LayoutStyle::new().with_width(48.0).with_height(22.0),
12886    );
12887    style_value_input(ui, row, name, value, range, decimals);
12888}
12889
12890fn style_inline_number(
12891    ui: &mut UiDocument,
12892    parent: UiNodeId,
12893    name: &'static str,
12894    label: &'static str,
12895    value: f32,
12896    range: std::ops::Range<f32>,
12897    decimals: u8,
12898) {
12899    let row = compact_row(ui, parent, format!("{name}.inline"), 3.0);
12900    widgets::label(
12901        ui,
12902        row,
12903        format!("{name}.inline_label"),
12904        format!("{label}:"),
12905        text(12.0, color(166, 176, 190)),
12906        LayoutStyle::new()
12907            .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
12908            .with_height(22.0),
12909    );
12910    style_value_input(ui, row, name, value, range, decimals);
12911}
12912
12913fn style_value_input(
12914    ui: &mut UiDocument,
12915    parent: UiNodeId,
12916    name: &'static str,
12917    value: f32,
12918    range: std::ops::Range<f32>,
12919    decimals: u8,
12920) {
12921    let mut options = widgets::DragValueOptions::default()
12922        .with_layout(
12923            LayoutStyle::row()
12924                .with_width(STYLING_VALUE_INPUT_WIDTH)
12925                .with_height(22.0)
12926                .with_flex_shrink(0.0)
12927                .with_align_items(taffy::prelude::AlignItems::Center)
12928                .with_justify_content(taffy::prelude::JustifyContent::Center)
12929                .with_padding(4.0),
12930        )
12931        .with_range(ext_widgets::NumericRange::new(
12932            f64::from(range.start),
12933            f64::from(range.end),
12934        ))
12935        .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
12936        .with_action(name);
12937    options.text_style = text(12.0, color(226, 232, 242));
12938    widgets::drag_value_input(ui, parent, name, f64::from(value), options);
12939}
12940
12941fn style_compact_checkbox(
12942    ui: &mut UiDocument,
12943    parent: UiNodeId,
12944    name: &'static str,
12945    label: &'static str,
12946    checked: bool,
12947) {
12948    let mut options = widgets::CheckboxOptions::default().with_action(name);
12949    options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
12950    options.text_style = text(12.0, color(220, 228, 238));
12951    widgets::checkbox(ui, parent, name, label, checked, options);
12952}
12953
12954fn compact_row(
12955    ui: &mut UiDocument,
12956    parent: UiNodeId,
12957    name: impl Into<String>,
12958    gap: f32,
12959) -> UiNodeId {
12960    ui.add_child(
12961        parent,
12962        UiNode::container(
12963            name,
12964            LayoutStyle::row()
12965                .with_height(22.0)
12966                .with_flex_shrink(0.0)
12967                .with_align_items(taffy::prelude::AlignItems::Center)
12968                .gap(gap),
12969        ),
12970    )
12971}
12972
12973fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
12974    ext_widgets::ColorButtonOptions::default()
12975        .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
12976        .with_swatch_size(UiSize::new(22.0, 18.0))
12977        .with_action(action)
12978        .show_label(false)
12979}
12980
12981fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
12982    let (frame, text_rect) = style_preview_rects(styling);
12983    let scene_size = style_preview_scene_size(styling);
12984    ui.add_child(
12985        parent,
12986        UiNode::scene(
12987            "styling.preview.scene",
12988            vec![
12989                ScenePrimitive::Rect(
12990                    PaintRect::solid(frame, styling.fill_color())
12991                        .stroke(AlignedStroke::inside(StrokeStyle::new(
12992                            styling.stroke_color(),
12993                            styling.stroke_width,
12994                        )))
12995                        .corner_radii(styling.radii())
12996                        .effect(PaintEffect::shadow(
12997                            styling.shadow_color(),
12998                            UiPoint::new(styling.shadow_x, styling.shadow_y),
12999                            styling.shadow_blur,
13000                            styling.shadow_spread,
13001                        )),
13002                ),
13003                ScenePrimitive::Text(
13004                    PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
13005                        .horizontal_align(TextHorizontalAlign::Center)
13006                        .vertical_align(TextVerticalAlign::Center)
13007                        .multiline(false),
13008                ),
13009            ],
13010            operad::layout::with_min_size(
13011                LayoutStyle::new()
13012                    .with_width_percent(1.0)
13013                    .with_height(180.0)
13014                    .with_flex_shrink(0.0),
13015                operad::layout::px(scene_size.width),
13016                operad::layout::px(scene_size.height),
13017            ),
13018        ),
13019    );
13020}
13021
13022fn style_preview_rects(styling: StylingState) -> (UiRect, UiRect) {
13023    let outer = styling.outer_edges();
13024    let inner = styling.inner_edges();
13025    let frame = UiRect::new(
13026        22.0 + outer[0],
13027        28.0 + outer[2],
13028        108.0 + inner[0] + inner[1],
13029        40.0 + inner[2] + inner[3],
13030    );
13031    let text_rect = UiRect::new(
13032        frame.x + inner[0],
13033        frame.y + inner[2],
13034        (frame.width - inner[0] - inner[1]).max(1.0),
13035        (frame.height - inner[2] - inner[3]).max(1.0),
13036    );
13037    (frame, text_rect)
13038}
13039
13040fn style_preview_scene_size(styling: StylingState) -> UiSize {
13041    let (frame, text_rect) = style_preview_rects(styling);
13042    let shadow_outset = styling.shadow_blur.max(0.0) + styling.shadow_spread.max(0.0);
13043    let shadow_bounds = UiRect::new(
13044        frame.x + styling.shadow_x - shadow_outset,
13045        frame.y + styling.shadow_y - shadow_outset,
13046        frame.width + shadow_outset * 2.0,
13047        frame.height + shadow_outset * 2.0,
13048    );
13049    let right = frame
13050        .right()
13051        .max(text_rect.right())
13052        .max(shadow_bounds.right());
13053    let bottom = frame
13054        .bottom()
13055        .max(text_rect.bottom())
13056        .max(shadow_bounds.bottom())
13057        .max(180.0);
13058    UiSize::new(right.ceil().max(1.0), bottom.ceil().max(1.0))
13059}
13060
13061fn slider_options(state: &ShowcaseState, width: f32) -> widgets::SliderOptions {
13062    let mut options = widgets::SliderOptions::default().with_layout(
13063        LayoutStyle::new()
13064            .with_width(width)
13065            .with_height(24.0)
13066            .with_flex_shrink(0.0),
13067    );
13068    options.fill_color = if state.slider_trailing_color {
13069        state.slider_trailing_picker.value()
13070    } else {
13071        color(42, 49, 58)
13072    };
13073    options.thumb_shape = match state.slider_thumb_shape {
13074        SliderThumbChoice::Circle => widgets::slider::SliderThumbShape::Circle,
13075        SliderThumbChoice::Square => widgets::slider::SliderThumbShape::Square,
13076        SliderThumbChoice::Rectangle => widgets::slider::SliderThumbShape::Rectangle,
13077    };
13078    options.thumb_visual = UiVisual::panel(
13079        state.slider_thumb_picker.value(),
13080        Some(StrokeStyle::new(color(79, 93, 113), 1.0)),
13081        6.0,
13082    );
13083    options
13084}
13085
13086#[allow(clippy::field_reassign_with_default)]
13087fn slider_number_input(
13088    ui: &mut UiDocument,
13089    parent: UiNodeId,
13090    name: &'static str,
13091    input: &TextInputState,
13092    focused: FocusedTextInput,
13093    state: &ShowcaseState,
13094    width: f32,
13095) {
13096    let mut options = TextInputOptions::default();
13097    options.layout = LayoutStyle::new().with_width(width).with_height(28.0);
13098    options.text_style = text(12.0, color(230, 236, 246));
13099    options.placeholder_style = text(12.0, color(144, 156, 174));
13100    options.edit_action = Some(format!("{name}.edit").into());
13101    options.focused = state.focused_text == Some(focused);
13102    options.caret_visible = caret_visible(state.caret_phase);
13103    widgets::text_input(ui, parent, name, input, options);
13104}
13105
13106fn form_status_chip(
13107    ui: &mut UiDocument,
13108    parent: UiNodeId,
13109    name: &'static str,
13110    label: &'static str,
13111    active: bool,
13112) {
13113    let chip = ui.add_child(
13114        parent,
13115        UiNode::container(
13116            name,
13117            LayoutStyle::new()
13118                .with_width(82.0)
13119                .with_height(24.0)
13120                .with_padding(4.0)
13121                .with_flex_shrink(0.0),
13122        )
13123        .with_visual(UiVisual::panel(
13124            if active {
13125                color(35, 74, 54)
13126            } else {
13127                color(28, 34, 43)
13128            },
13129            Some(StrokeStyle::new(
13130                if active {
13131                    color(90, 160, 112)
13132                } else {
13133                    color(60, 72, 88)
13134                },
13135                1.0,
13136            )),
13137            4.0,
13138        )),
13139    );
13140    widgets::label(
13141        ui,
13142        chip,
13143        format!("{name}.label"),
13144        label,
13145        text(11.0, color(218, 228, 240)),
13146        LayoutStyle::new()
13147            .with_width_percent(1.0)
13148            .with_height_percent(1.0),
13149    );
13150}
13151
13152fn profile_form_summary(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13153    let has_errors = widgets::form_has_errors(&state.form);
13154    let title = profile_form_summary_title(state, has_errors);
13155    let detail = format!(
13156        "{} | {} | {}",
13157        profile_summary_value(state.form_name_text.text(), "No name"),
13158        profile_summary_value(state.form_email_text.text(), "No email"),
13159        profile_summary_value(state.form_role_text.text(), "No role"),
13160    );
13161    let hint = profile_form_summary_hint(state, has_errors);
13162    let stroke = if has_errors {
13163        color(196, 94, 104)
13164    } else if state.form.dirty {
13165        color(205, 160, 71)
13166    } else if state.form.submitted {
13167        color(91, 164, 119)
13168    } else {
13169        color(60, 72, 88)
13170    };
13171    let summary = ui.add_child(
13172        parent,
13173        UiNode::container(
13174            "forms.profile.summary",
13175            LayoutStyle::column()
13176                .with_width_percent(1.0)
13177                .with_padding(10.0)
13178                .with_gap(4.0)
13179                .with_flex_shrink(0.0),
13180        )
13181        .with_visual(UiVisual::panel(
13182            color(20, 25, 32),
13183            Some(StrokeStyle::new(stroke, 1.0)),
13184            4.0,
13185        ))
13186        .with_accessibility(
13187            AccessibilityMeta::new(AccessibilityRole::Group)
13188                .label("Live profile summary")
13189                .value(format!("{title}. {detail}. {hint}")),
13190        ),
13191    );
13192    widgets::label(
13193        ui,
13194        summary,
13195        "forms.profile.summary.title",
13196        title,
13197        text(13.0, color(232, 240, 250)),
13198        LayoutStyle::new().with_width_percent(1.0),
13199    );
13200    widgets::label(
13201        ui,
13202        summary,
13203        "forms.profile.summary.detail",
13204        detail,
13205        text(12.0, color(186, 198, 216)),
13206        LayoutStyle::new().with_width_percent(1.0),
13207    );
13208    widgets::label(
13209        ui,
13210        summary,
13211        "forms.profile.summary.hint",
13212        hint,
13213        text(11.0, color(154, 166, 184)),
13214        LayoutStyle::new().with_width_percent(1.0),
13215    );
13216}
13217
13218fn profile_form_summary_title(state: &ShowcaseState, has_errors: bool) -> &'static str {
13219    if has_errors {
13220        "Profile needs fixes"
13221    } else if state.form.submitted {
13222        "Profile submitted"
13223    } else if state.form.dirty {
13224        "Profile draft"
13225    } else {
13226        "Profile saved"
13227    }
13228}
13229
13230fn profile_form_summary_hint(state: &ShowcaseState, has_errors: bool) -> &'static str {
13231    if has_errors {
13232        "Fix validation errors before applying or submitting."
13233    } else if state.form.dirty {
13234        "Apply saves the draft; Submit saves and marks it submitted."
13235    } else if state.form.submitted {
13236        "Submission completed. Apply stays disabled until something changes."
13237    } else {
13238        "No pending changes. Submit marks the saved profile submitted."
13239    }
13240}
13241
13242fn profile_summary_value<'a>(value: &'a str, empty: &'static str) -> &'a str {
13243    let value = value.trim();
13244    if value.is_empty() {
13245        empty
13246    } else {
13247        value
13248    }
13249}
13250
13251#[allow(clippy::field_reassign_with_default)]
13252fn form_text_field(
13253    ui: &mut UiDocument,
13254    parent: UiNodeId,
13255    name: &'static str,
13256    input: &TextInputState,
13257    focused: FocusedTextInput,
13258    state: &ShowcaseState,
13259) {
13260    let mut options = TextInputOptions::default();
13261    options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
13262    options.text_style = text(12.0, color(230, 236, 246));
13263    options.placeholder_style = text(12.0, color(144, 156, 174));
13264    options.placeholder = "Required".to_string();
13265    options.edit_action = Some(format!("{name}.edit").into());
13266    options.focused = state.focused_text == Some(focused);
13267    options.caret_visible = caret_visible(state.caret_phase);
13268    widgets::text_input(ui, parent, name, input, options);
13269}
13270
13271fn profile_email_valid(email: &str) -> bool {
13272    let email = email.trim();
13273    let Some((local, domain)) = email.split_once('@') else {
13274        return false;
13275    };
13276    !local.is_empty() && domain.contains('.') && !domain.ends_with('.')
13277}
13278
13279fn drag_source_layout() -> LayoutStyle {
13280    LayoutStyle::row()
13281        .with_width(128.0)
13282        .with_height(40.0)
13283        .with_padding(8.0)
13284        .with_gap(6.0)
13285        .with_flex_shrink(0.0)
13286}
13287
13288fn drop_zone_layout() -> LayoutStyle {
13289    LayoutStyle::column()
13290        .with_width(128.0)
13291        .with_height(78.0)
13292        .with_padding(10.0)
13293        .with_gap(6.0)
13294        .with_flex_shrink(0.0)
13295}
13296
13297fn dnd_operation_chip(
13298    ui: &mut UiDocument,
13299    parent: UiNodeId,
13300    name: &'static str,
13301    label: &'static str,
13302) {
13303    let chip = ui.add_child(
13304        parent,
13305        UiNode::container(
13306            name,
13307            LayoutStyle::new()
13308                .with_width(58.0)
13309                .with_height(22.0)
13310                .with_padding(3.0)
13311                .with_flex_shrink(0.0),
13312        )
13313        .with_visual(UiVisual::panel(
13314            color(26, 32, 42),
13315            Some(StrokeStyle::new(color(62, 76, 94), 1.0)),
13316            3.0,
13317        )),
13318    );
13319    widgets::label(
13320        ui,
13321        chip,
13322        format!("{name}.label"),
13323        label,
13324        text(11.0, color(190, 204, 222)),
13325        LayoutStyle::new()
13326            .with_width_percent(1.0)
13327            .with_height_percent(1.0),
13328    );
13329}
13330
13331fn media_preview_image_layout() -> LayoutStyle {
13332    LayoutStyle::size(46.0, 46.0).with_flex_shrink(0.0)
13333}
13334
13335fn media_icon_columns(state: &ShowcaseState) -> usize {
13336    let theme = state.app_theme();
13337    let options = showcase_desktop_options(state.last_desktop_size, &theme);
13338    let window_width = state
13339        .desktop
13340        .size("media", default_window_size("media"))
13341        .width;
13342    let content_width = (window_width - options.content_padding * 2.0).max(MEDIA_ICON_TILE_WIDTH);
13343    let pitch = MEDIA_ICON_TILE_WIDTH + MEDIA_ICON_GRID_GAP;
13344    (((content_width + MEDIA_ICON_GRID_GAP) / pitch).floor() as usize).clamp(1, MEDIA_ICON_COLUMNS)
13345}
13346
13347fn media_icon_grid_width(columns: usize) -> f32 {
13348    let columns = columns.max(1);
13349    columns as f32 * MEDIA_ICON_TILE_WIDTH + columns.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13350}
13351
13352fn media_icon_grid_height(columns: usize, item_count: usize) -> f32 {
13353    let columns = columns.max(1);
13354    let rows = item_count.div_ceil(columns).max(1);
13355    rows as f32 * MEDIA_ICON_TILE_HEIGHT + rows.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13356}
13357
13358fn media_icon_grid(
13359    ui: &mut UiDocument,
13360    parent: UiNodeId,
13361    name: impl Into<String>,
13362    columns: usize,
13363    item_count: usize,
13364) -> UiNodeId {
13365    let columns = columns.clamp(1, MEDIA_ICON_COLUMNS);
13366    let rows = item_count.div_ceil(columns).max(1);
13367    let width = media_icon_grid_width(columns);
13368    let height = media_icon_grid_height(columns, item_count);
13369    let layout = operad::layout::with_grid_template_rows(
13370        operad::layout::with_grid_template_columns(
13371            Layout::grid()
13372                .size(LayoutSize::points(width, height))
13373                .gap(LayoutGap::points(MEDIA_ICON_GRID_GAP, MEDIA_ICON_GRID_GAP))
13374                .flex(0.0, 0.0, LayoutDimension::Auto)
13375                .to_layout_style(),
13376            (0..columns).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_WIDTH)),
13377        ),
13378        (0..rows).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_HEIGHT)),
13379    );
13380    ui.add_child(parent, UiNode::container(name, layout))
13381}
13382
13383fn media_icon_tile(ui: &mut UiDocument, parent: UiNodeId, icon: BuiltInIcon) {
13384    let name = icon.key().replace('.', "_").replace('-', "_");
13385    let tile = ui.add_child(
13386        parent,
13387        UiNode::container(
13388            format!("media.icon_tile.{name}"),
13389            LayoutStyle::column()
13390                .with_width(MEDIA_ICON_TILE_WIDTH)
13391                .with_height(MEDIA_ICON_TILE_HEIGHT)
13392                .with_padding(6.0)
13393                .with_gap(4.0)
13394                .with_flex_shrink(0.0),
13395        )
13396        .with_visual(UiVisual::panel(
13397            color(17, 22, 30),
13398            Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
13399            4.0,
13400        )),
13401    );
13402    widgets::image(
13403        ui,
13404        tile,
13405        format!("media.icon.{name}"),
13406        icon_image(icon),
13407        widgets::ImageOptions::default()
13408            .with_layout(LayoutStyle::size(28.0, 28.0))
13409            .with_accessibility_label(icon.label()),
13410    );
13411    widgets::label(
13412        ui,
13413        tile,
13414        format!("media.icon_label.{name}"),
13415        icon.label(),
13416        text(9.0, color(180, 194, 214)),
13417        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13418    );
13419}
13420
13421fn slider_checkbox(
13422    ui: &mut UiDocument,
13423    parent: UiNodeId,
13424    name: &'static str,
13425    label: &'static str,
13426    checked: bool,
13427) {
13428    slider_checkbox_with_layout(
13429        ui,
13430        parent,
13431        name,
13432        label,
13433        checked,
13434        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13435    );
13436}
13437
13438fn slider_checkbox_with_layout(
13439    ui: &mut UiDocument,
13440    parent: UiNodeId,
13441    name: &'static str,
13442    label: &'static str,
13443    checked: bool,
13444    layout: LayoutStyle,
13445) {
13446    let mut options = widgets::CheckboxOptions::default().with_action(name);
13447    options.layout = layout;
13448    options.text_style = text(12.0, color(220, 228, 238));
13449    widgets::checkbox(ui, parent, name, label, checked, options);
13450}
13451
13452fn choice_button(
13453    ui: &mut UiDocument,
13454    parent: UiNodeId,
13455    name: &'static str,
13456    label: &'static str,
13457    selected: bool,
13458) {
13459    let mut options =
13460        widgets::ButtonOptions::new(LayoutStyle::new().with_width(78.0).with_height(28.0))
13461            .with_action(name);
13462    options.visual = if selected {
13463        button_visual(48, 112, 184)
13464    } else {
13465        button_visual(38, 46, 58)
13466    };
13467    options.hovered_visual = Some(button_visual(65, 86, 106));
13468    options.pressed_visual = Some(button_visual(34, 54, 84));
13469    options.text_style = text(12.0, color(238, 244, 252));
13470    widgets::button(ui, parent, name, label, options);
13471}
13472
13473fn divider(ui: &mut UiDocument, parent: UiNodeId, name: &'static str) {
13474    ui.add_child(
13475        parent,
13476        UiNode::container(
13477            name,
13478            LayoutStyle::new()
13479                .with_width_percent(1.0)
13480                .with_height(1.0)
13481                .with_flex_shrink(0.0),
13482        )
13483        .with_visual(UiVisual::panel(color(48, 58, 72), None, 0.0)),
13484    );
13485}
13486
13487fn canvas(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13488    let canvas_intrinsic = UiSize::new(720.0, 405.0);
13489    let body = section_with_min_viewport(ui, parent, "canvas", "Canvas", UiSize::new(720.0, 458.0));
13490    let controls = wrapping_row(ui, body, "canvas.options", 10.0);
13491    canvas_option_checkbox(
13492        ui,
13493        controls,
13494        "canvas.grow_horizontal",
13495        "Grow width",
13496        state.canvas_grow_horizontal,
13497    );
13498    canvas_option_checkbox(
13499        ui,
13500        controls,
13501        "canvas.grow_vertical",
13502        "Grow height",
13503        state.canvas_grow_vertical,
13504    );
13505    canvas_option_checkbox(
13506        ui,
13507        controls,
13508        "canvas.keep_aspect_ratio",
13509        "Keep aspect ratio",
13510        state.canvas_keep_aspect_ratio,
13511    );
13512
13513    let mut options = widgets::CanvasOptions::default()
13514        .with_accessibility_label("Shader canvas")
13515        .with_action("canvas.rotate")
13516        .with_intrinsic_size(canvas_intrinsic);
13517    options.action_mode = WidgetActionMode::Drag;
13518    if state.canvas_keep_aspect_ratio {
13519        options = options.with_aspect_ratio(16.0 / 9.0);
13520    }
13521    let canvas_width = if state.canvas_grow_horizontal {
13522        LayoutDimension::percent(1.0)
13523    } else {
13524        LayoutDimension::points(canvas_intrinsic.width)
13525    };
13526    let canvas_height = if state.canvas_grow_vertical {
13527        LayoutDimension::percent(1.0)
13528    } else {
13529        LayoutDimension::points(canvas_intrinsic.height)
13530    };
13531    options.layout = Layout::new()
13532        .size(LayoutSize::new(canvas_width, canvas_height))
13533        .min_size(LayoutSize::points(
13534            canvas_intrinsic.width,
13535            canvas_intrinsic.height,
13536        ))
13537        .flex(
13538            if state.canvas_grow_vertical { 1.0 } else { 0.0 },
13539            1.0,
13540            LayoutDimension::Auto,
13541        )
13542        .to_layout_style();
13543    options.visual = UiVisual::panel(
13544        color(18, 22, 28),
13545        Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
13546        4.0,
13547    );
13548    widgets::canvas(
13549        ui,
13550        body,
13551        "canvas.shader",
13552        CanvasContent::new("canvas.shader").program(showcase_canvas_program(state.cube)),
13553        options,
13554    );
13555}
13556
13557fn canvas_option_checkbox(
13558    ui: &mut UiDocument,
13559    parent: UiNodeId,
13560    name: &'static str,
13561    label: &'static str,
13562    checked: bool,
13563) {
13564    let mut options = widgets::CheckboxOptions::default()
13565        .with_action(name)
13566        .with_text_style(text(12.0, color(220, 228, 238)));
13567    options.layout = LayoutStyle::new().with_height(28.0).with_flex_shrink(0.0);
13568    widgets::checkbox(ui, parent, name, label, checked, options);
13569}
13570
13571fn showcase_canvas_program(cube: CanvasCubeState) -> CanvasRenderProgram {
13572    CanvasRenderProgram::wgsl(include_str!("shaders/showcase_canvas.wgsl"))
13573        .label("showcase.canvas")
13574        .constant("CUBE_YAW", cube.yaw as f64)
13575        .constant("CUBE_PITCH", cube.pitch as f64)
13576        .clear_color(Some(color(18, 22, 28)))
13577}
13578
13579fn section(
13580    ui: &mut UiDocument,
13581    parent: UiNodeId,
13582    name: impl Into<String>,
13583    _title: impl Into<String>,
13584) -> UiNodeId {
13585    section_with_min_viewport(ui, parent, name, _title, UiSize::ZERO)
13586}
13587
13588fn section_with_min_viewport(
13589    ui: &mut UiDocument,
13590    parent: UiNodeId,
13591    name: impl Into<String>,
13592    _title: impl Into<String>,
13593    min_viewport_size: UiSize,
13594) -> UiNodeId {
13595    let name = name.into();
13596    let layout = Layout::column()
13597        .size(LayoutSize::percent(1.0, 1.0))
13598        .min_size(LayoutSize::points(
13599            min_viewport_size.width.max(0.0),
13600            min_viewport_size.height.max(0.0),
13601        ))
13602        .gap(LayoutGap::points(10.0, 10.0))
13603        .flex(1.0, 1.0, LayoutDimension::Auto)
13604        .to_layout_style();
13605    widgets::scroll_area(
13606        ui,
13607        parent,
13608        format!("{name}.section_scroll"),
13609        ScrollAxes::BOTH,
13610        layout,
13611    )
13612}
13613
13614fn row(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>, gap: f32) -> UiNodeId {
13615    ui.add_child(
13616        parent,
13617        UiNode::container(
13618            name,
13619            Layout::row()
13620                .size(LayoutSize::new(
13621                    LayoutDimension::percent(1.0),
13622                    LayoutDimension::Auto,
13623                ))
13624                .align_items(LayoutAlignment::Center)
13625                .gap(LayoutGap::points(gap, gap))
13626                .flex(0.0, 0.0, LayoutDimension::Auto)
13627                .to_layout_style(),
13628        ),
13629    )
13630}
13631
13632fn wrapping_row(
13633    ui: &mut UiDocument,
13634    parent: UiNodeId,
13635    name: impl Into<String>,
13636    gap: f32,
13637) -> UiNodeId {
13638    ui.add_child(
13639        parent,
13640        UiNode::container(
13641            name,
13642            Layout::row()
13643                .size(LayoutSize::new(
13644                    LayoutDimension::percent(1.0),
13645                    LayoutDimension::Auto,
13646                ))
13647                .min_size(LayoutSize::points(0.0, 0.0))
13648                .align_items(LayoutAlignment::Center)
13649                .gap(LayoutGap::points(gap, gap))
13650                .flex_wrap(LayoutFlexWrap::Wrap)
13651                .flex(0.0, 0.0, LayoutDimension::Auto)
13652                .to_layout_style(),
13653        ),
13654    )
13655}
Source

pub const fn percent(width: f32, height: f32) -> Self

Examples found in repository?
examples/showcase.rs (line 12484)
12470fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12471    let preview_scene_size = style_preview_scene_size(state.styling);
12472    let preview_min_width = preview_scene_size.width + 16.0;
12473    let preview_min_height = preview_scene_size.height + 16.0;
12474    let body_min_width = STYLING_CONTROLS_WIDTH + 1.0 + preview_min_width + 20.0;
12475    let body = section_with_min_viewport(
12476        ui,
12477        parent,
12478        "styling",
12479        "Styling",
12480        UiSize::new(body_min_width, preview_min_height),
12481    );
12482    let grid_layout = operad::layout::with_grid_template_columns(
12483        Layout::grid()
12484            .size(LayoutSize::percent(1.0, 1.0))
12485            .gap(LayoutGap::points(10.0, 10.0))
12486            .to_layout_style(),
12487        [
12488            LayoutGridTrack::points(STYLING_CONTROLS_WIDTH),
12489            LayoutGridTrack::points(1.0),
12490            LayoutGridTrack::minmax_points_fraction(preview_min_width, 1.0),
12491        ],
12492    );
12493    let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
12494    let controls = ui.add_child(
12495        grid,
12496        UiNode::container(
12497            "styling.controls",
12498            LayoutStyle::column()
12499                .with_width(STYLING_CONTROLS_WIDTH)
12500                .with_height_percent(1.0)
12501                .with_flex_shrink(0.0)
12502                .gap(6.0),
12503        ),
12504    );
12505    style_edge_group(
12506        ui,
12507        controls,
12508        "styling.inner",
12509        "Inner margin",
12510        "styling.inner_same",
12511        state.styling.inner_same,
12512        [
12513            ("Left", "styling.inner", state.styling.inner_margin),
12514            ("Right", "styling.inner_right", state.styling.inner_right),
12515            ("Top", "styling.inner_top", state.styling.inner_top),
12516            ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
12517        ],
12518        0.0..32.0,
12519    );
12520    style_edge_group(
12521        ui,
12522        controls,
12523        "styling.outer",
12524        "Outer margin",
12525        "styling.outer_same",
12526        state.styling.outer_same,
12527        [
12528            ("Left", "styling.outer", state.styling.outer_margin),
12529            ("Right", "styling.outer_right", state.styling.outer_right),
12530            ("Top", "styling.outer_top", state.styling.outer_top),
12531            ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
12532        ],
12533        0.0..40.0,
12534    );
12535    style_edge_group(
12536        ui,
12537        controls,
12538        "styling.radius",
12539        "Corner radius",
12540        "styling.radius_same",
12541        state.styling.radius_same,
12542        [
12543            ("NW", "styling.radius", state.styling.corner_radius),
12544            ("NE", "styling.radius_ne", state.styling.corner_ne),
12545            ("SW", "styling.radius_sw", state.styling.corner_sw),
12546            ("SE", "styling.radius_se", state.styling.corner_se),
12547        ],
12548        0.0..28.0,
12549    );
12550    style_fill_group(ui, controls, state);
12551    style_stroke_group(ui, controls, state);
12552    style_shadow_group(ui, controls, state);
12553    widgets::separator(
12554        ui,
12555        grid,
12556        "styling.preview.separator",
12557        widgets::SeparatorOptions::vertical().with_layout(
12558            LayoutStyle::new()
12559                .with_width(1.0)
12560                .with_height_percent(1.0)
12561                .with_flex_shrink(0.0),
12562        ),
12563    );
12564
12565    let preview = ui.add_child(
12566        grid,
12567        UiNode::container(
12568            "styling.preview",
12569            operad::layout::with_min_size(
12570                LayoutStyle::column()
12571                    .with_width_percent(1.0)
12572                    .with_height_percent(1.0)
12573                    .with_flex_shrink(0.0)
12574                    .padding(8.0),
12575                operad::layout::px(preview_min_width),
12576                operad::layout::px(preview_min_height),
12577            ),
12578        )
12579        .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
12580    );
12581    style_preview(ui, preview, state.styling);
12582}
12583
12584#[allow(clippy::too_many_arguments)]
12585fn style_edge_group(
12586    ui: &mut UiDocument,
12587    parent: UiNodeId,
12588    name: &'static str,
12589    title: &'static str,
12590    same_action: &'static str,
12591    same: bool,
12592    values: [(&'static str, &'static str, f32); 4],
12593    range: std::ops::Range<f32>,
12594) {
12595    let group = style_control_group(ui, parent, format!("{name}.group"));
12596    style_group_title(ui, group, format!("{name}.title"), title);
12597    let fields = ui.add_child(
12598        group,
12599        UiNode::container(
12600            format!("{name}.fields"),
12601            LayoutStyle::column()
12602                .with_width(138.0)
12603                .with_flex_shrink(0.0)
12604                .gap(3.0),
12605        ),
12606    );
12607    style_compact_checkbox(ui, fields, same_action, "same", same);
12608    if same {
12609        style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
12610    } else {
12611        for (label, action, value) in values {
12612            style_number_row(ui, fields, action, label, value, range.clone(), 0);
12613        }
12614    }
12615}
12616
12617fn style_fill_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12618    let group = style_control_group(ui, parent, "styling.fill.group");
12619    style_group_title(ui, group, "styling.fill.title", "Fill");
12620    let fields = style_group_fields(
12621        ui,
12622        group,
12623        "styling.fill.fields",
12624        STYLING_WIDE_FIELDS_WIDTH,
12625        4.0,
12626    );
12627    style_color_button_row(
12628        ui,
12629        fields,
12630        "styling.fill_color_button",
12631        "",
12632        state.styling.fill_color(),
12633        "Pick fill color",
12634    );
12635    if state.styling_fill_picker_open {
12636        ext_widgets::color_picker(
12637            ui,
12638            fields,
12639            "styling.fill_picker",
12640            &state.styling_fill_picker,
12641            ext_widgets::ColorPickerOptions::default()
12642                .with_label("Fill")
12643                .with_action_prefix("styling.fill_picker"),
12644        );
12645    }
12646}
12647
12648fn style_stroke_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12649    let group = style_control_group(ui, parent, "styling.stroke.group");
12650    style_group_title(ui, group, "styling.stroke.title", "Stroke");
12651    let fields = style_group_fields(
12652        ui,
12653        group,
12654        "styling.stroke.fields",
12655        STYLING_WIDE_FIELDS_WIDTH,
12656        4.0,
12657    );
12658    let width_row = row(ui, fields, "styling.stroke.row", 6.0);
12659    style_inline_number(
12660        ui,
12661        width_row,
12662        "styling.stroke",
12663        "width",
12664        state.styling.stroke_width,
12665        0.0..STYLING_STROKE_MAX,
12666        1,
12667    );
12668    let mut options = widgets::SliderOptions::default()
12669        .with_layout(
12670            LayoutStyle::new()
12671                .with_width(60.0)
12672                .with_height(20.0)
12673                .with_flex_shrink(0.0),
12674        )
12675        .with_value_edit_action("styling.stroke");
12676    options.fill_color = color(120, 170, 230);
12677    widgets::slider(
12678        ui,
12679        width_row,
12680        "styling.stroke.slider",
12681        (state.styling.stroke_width / STYLING_STROKE_MAX).clamp(0.0, 1.0),
12682        0.0..1.0,
12683        options,
12684    );
12685    style_color_button_row(
12686        ui,
12687        fields,
12688        "styling.stroke_color_button",
12689        "",
12690        state.styling.stroke_color(),
12691        "Pick stroke color",
12692    );
12693    if state.styling_stroke_picker_open {
12694        ext_widgets::color_picker(
12695            ui,
12696            fields,
12697            "styling.stroke_picker",
12698            &state.styling_stroke_picker,
12699            ext_widgets::ColorPickerOptions::default()
12700                .with_label("Stroke color")
12701                .with_action_prefix("styling.stroke_picker"),
12702        );
12703    }
12704}
12705
12706fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12707    let group = style_control_group(ui, parent, "styling.shadow.group");
12708    style_group_title(ui, group, "styling.shadow.title", "Shadow");
12709    let fields = style_group_fields(
12710        ui,
12711        group,
12712        "styling.shadow.fields",
12713        STYLING_WIDE_FIELDS_WIDTH,
12714        4.0,
12715    );
12716    let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
12717    style_inline_number(
12718        ui,
12719        offsets,
12720        "styling.shadow_x",
12721        "x",
12722        state.styling.shadow_x,
12723        -24.0..24.0,
12724        0,
12725    );
12726    style_inline_number(
12727        ui,
12728        offsets,
12729        "styling.shadow_y",
12730        "y",
12731        state.styling.shadow_y,
12732        -24.0..24.0,
12733        0,
12734    );
12735    let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
12736    style_inline_number(
12737        ui,
12738        spread,
12739        "styling.shadow",
12740        "blur",
12741        state.styling.shadow_blur,
12742        0.0..32.0,
12743        0,
12744    );
12745    style_inline_number(
12746        ui,
12747        spread,
12748        "styling.shadow_spread",
12749        "spread",
12750        state.styling.shadow_spread,
12751        0.0..16.0,
12752        0,
12753    );
12754    style_color_button_row(
12755        ui,
12756        fields,
12757        "styling.shadow_color_button",
12758        "",
12759        state.styling.shadow_color(),
12760        "Pick shadow color",
12761    );
12762    if state.styling_shadow_picker_open {
12763        ext_widgets::color_picker(
12764            ui,
12765            fields,
12766            "styling.shadow_picker",
12767            &state.styling_shadow_picker,
12768            ext_widgets::ColorPickerOptions::default()
12769                .with_label("Shadow color")
12770                .with_action_prefix("styling.shadow_picker"),
12771        );
12772    }
12773}
12774
12775fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
12776    ui.add_child(
12777        parent,
12778        UiNode::container(
12779            name,
12780            LayoutStyle::row()
12781                .with_width_percent(1.0)
12782                .with_flex_shrink(0.0)
12783                .padding(4.0)
12784                .gap(8.0),
12785        )
12786        .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
12787    )
12788}
12789
12790fn style_group_fields(
12791    ui: &mut UiDocument,
12792    parent: UiNodeId,
12793    name: impl Into<String>,
12794    width: f32,
12795    gap: f32,
12796) -> UiNodeId {
12797    ui.add_child(
12798        parent,
12799        UiNode::container(
12800            name,
12801            LayoutStyle::column()
12802                .with_width(width)
12803                .with_flex_shrink(0.0)
12804                .gap(gap),
12805        ),
12806    )
12807}
12808
12809fn style_group_title(
12810    ui: &mut UiDocument,
12811    parent: UiNodeId,
12812    name: impl Into<String>,
12813    label: &'static str,
12814) {
12815    widgets::label(
12816        ui,
12817        parent,
12818        name,
12819        label,
12820        text(12.0, color(166, 176, 190)),
12821        LayoutStyle::new()
12822            .with_width(88.0)
12823            .with_flex_shrink(0.0)
12824            .with_height(22.0),
12825    );
12826}
12827
12828fn style_color_button_row(
12829    ui: &mut UiDocument,
12830    parent: UiNodeId,
12831    action: &'static str,
12832    label: &'static str,
12833    value: ColorRgba,
12834    accessibility_label: &'static str,
12835) {
12836    let row = row(ui, parent, format!("{action}.row"), 8.0);
12837    if !label.is_empty() {
12838        widgets::label(
12839            ui,
12840            row,
12841            format!("{action}.label"),
12842            label,
12843            text(12.0, color(166, 176, 190)),
12844            LayoutStyle::new()
12845                .with_width(86.0)
12846                .with_flex_shrink(0.0)
12847                .with_height(24.0),
12848        );
12849    }
12850    ext_widgets::color_edit_button(
12851        ui,
12852        row,
12853        action,
12854        value,
12855        color_mini_button_options(action)
12856            .with_format(ext_widgets::ColorValueFormat::Rgba)
12857            .accessibility_label(accessibility_label),
12858    );
12859    widgets::label(
12860        ui,
12861        row,
12862        format!("{action}.value"),
12863        ext_widgets::color_picker::format_hex_color(value, value.a < 255),
12864        text(12.0, color(226, 232, 242)),
12865        LayoutStyle::new().with_width(96.0).with_height(24.0),
12866    );
12867}
12868
12869fn style_number_row(
12870    ui: &mut UiDocument,
12871    parent: UiNodeId,
12872    name: &'static str,
12873    label: &'static str,
12874    value: f32,
12875    range: std::ops::Range<f32>,
12876    decimals: u8,
12877) {
12878    let row = row(ui, parent, format!("{name}.row"), 6.0);
12879    widgets::label(
12880        ui,
12881        row,
12882        format!("{name}.label"),
12883        label,
12884        text(12.0, color(166, 176, 190)),
12885        LayoutStyle::new().with_width(48.0).with_height(22.0),
12886    );
12887    style_value_input(ui, row, name, value, range, decimals);
12888}
12889
12890fn style_inline_number(
12891    ui: &mut UiDocument,
12892    parent: UiNodeId,
12893    name: &'static str,
12894    label: &'static str,
12895    value: f32,
12896    range: std::ops::Range<f32>,
12897    decimals: u8,
12898) {
12899    let row = compact_row(ui, parent, format!("{name}.inline"), 3.0);
12900    widgets::label(
12901        ui,
12902        row,
12903        format!("{name}.inline_label"),
12904        format!("{label}:"),
12905        text(12.0, color(166, 176, 190)),
12906        LayoutStyle::new()
12907            .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
12908            .with_height(22.0),
12909    );
12910    style_value_input(ui, row, name, value, range, decimals);
12911}
12912
12913fn style_value_input(
12914    ui: &mut UiDocument,
12915    parent: UiNodeId,
12916    name: &'static str,
12917    value: f32,
12918    range: std::ops::Range<f32>,
12919    decimals: u8,
12920) {
12921    let mut options = widgets::DragValueOptions::default()
12922        .with_layout(
12923            LayoutStyle::row()
12924                .with_width(STYLING_VALUE_INPUT_WIDTH)
12925                .with_height(22.0)
12926                .with_flex_shrink(0.0)
12927                .with_align_items(taffy::prelude::AlignItems::Center)
12928                .with_justify_content(taffy::prelude::JustifyContent::Center)
12929                .with_padding(4.0),
12930        )
12931        .with_range(ext_widgets::NumericRange::new(
12932            f64::from(range.start),
12933            f64::from(range.end),
12934        ))
12935        .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
12936        .with_action(name);
12937    options.text_style = text(12.0, color(226, 232, 242));
12938    widgets::drag_value_input(ui, parent, name, f64::from(value), options);
12939}
12940
12941fn style_compact_checkbox(
12942    ui: &mut UiDocument,
12943    parent: UiNodeId,
12944    name: &'static str,
12945    label: &'static str,
12946    checked: bool,
12947) {
12948    let mut options = widgets::CheckboxOptions::default().with_action(name);
12949    options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
12950    options.text_style = text(12.0, color(220, 228, 238));
12951    widgets::checkbox(ui, parent, name, label, checked, options);
12952}
12953
12954fn compact_row(
12955    ui: &mut UiDocument,
12956    parent: UiNodeId,
12957    name: impl Into<String>,
12958    gap: f32,
12959) -> UiNodeId {
12960    ui.add_child(
12961        parent,
12962        UiNode::container(
12963            name,
12964            LayoutStyle::row()
12965                .with_height(22.0)
12966                .with_flex_shrink(0.0)
12967                .with_align_items(taffy::prelude::AlignItems::Center)
12968                .gap(gap),
12969        ),
12970    )
12971}
12972
12973fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
12974    ext_widgets::ColorButtonOptions::default()
12975        .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
12976        .with_swatch_size(UiSize::new(22.0, 18.0))
12977        .with_action(action)
12978        .show_label(false)
12979}
12980
12981fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
12982    let (frame, text_rect) = style_preview_rects(styling);
12983    let scene_size = style_preview_scene_size(styling);
12984    ui.add_child(
12985        parent,
12986        UiNode::scene(
12987            "styling.preview.scene",
12988            vec![
12989                ScenePrimitive::Rect(
12990                    PaintRect::solid(frame, styling.fill_color())
12991                        .stroke(AlignedStroke::inside(StrokeStyle::new(
12992                            styling.stroke_color(),
12993                            styling.stroke_width,
12994                        )))
12995                        .corner_radii(styling.radii())
12996                        .effect(PaintEffect::shadow(
12997                            styling.shadow_color(),
12998                            UiPoint::new(styling.shadow_x, styling.shadow_y),
12999                            styling.shadow_blur,
13000                            styling.shadow_spread,
13001                        )),
13002                ),
13003                ScenePrimitive::Text(
13004                    PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
13005                        .horizontal_align(TextHorizontalAlign::Center)
13006                        .vertical_align(TextVerticalAlign::Center)
13007                        .multiline(false),
13008                ),
13009            ],
13010            operad::layout::with_min_size(
13011                LayoutStyle::new()
13012                    .with_width_percent(1.0)
13013                    .with_height(180.0)
13014                    .with_flex_shrink(0.0),
13015                operad::layout::px(scene_size.width),
13016                operad::layout::px(scene_size.height),
13017            ),
13018        ),
13019    );
13020}
13021
13022fn style_preview_rects(styling: StylingState) -> (UiRect, UiRect) {
13023    let outer = styling.outer_edges();
13024    let inner = styling.inner_edges();
13025    let frame = UiRect::new(
13026        22.0 + outer[0],
13027        28.0 + outer[2],
13028        108.0 + inner[0] + inner[1],
13029        40.0 + inner[2] + inner[3],
13030    );
13031    let text_rect = UiRect::new(
13032        frame.x + inner[0],
13033        frame.y + inner[2],
13034        (frame.width - inner[0] - inner[1]).max(1.0),
13035        (frame.height - inner[2] - inner[3]).max(1.0),
13036    );
13037    (frame, text_rect)
13038}
13039
13040fn style_preview_scene_size(styling: StylingState) -> UiSize {
13041    let (frame, text_rect) = style_preview_rects(styling);
13042    let shadow_outset = styling.shadow_blur.max(0.0) + styling.shadow_spread.max(0.0);
13043    let shadow_bounds = UiRect::new(
13044        frame.x + styling.shadow_x - shadow_outset,
13045        frame.y + styling.shadow_y - shadow_outset,
13046        frame.width + shadow_outset * 2.0,
13047        frame.height + shadow_outset * 2.0,
13048    );
13049    let right = frame
13050        .right()
13051        .max(text_rect.right())
13052        .max(shadow_bounds.right());
13053    let bottom = frame
13054        .bottom()
13055        .max(text_rect.bottom())
13056        .max(shadow_bounds.bottom())
13057        .max(180.0);
13058    UiSize::new(right.ceil().max(1.0), bottom.ceil().max(1.0))
13059}
13060
13061fn slider_options(state: &ShowcaseState, width: f32) -> widgets::SliderOptions {
13062    let mut options = widgets::SliderOptions::default().with_layout(
13063        LayoutStyle::new()
13064            .with_width(width)
13065            .with_height(24.0)
13066            .with_flex_shrink(0.0),
13067    );
13068    options.fill_color = if state.slider_trailing_color {
13069        state.slider_trailing_picker.value()
13070    } else {
13071        color(42, 49, 58)
13072    };
13073    options.thumb_shape = match state.slider_thumb_shape {
13074        SliderThumbChoice::Circle => widgets::slider::SliderThumbShape::Circle,
13075        SliderThumbChoice::Square => widgets::slider::SliderThumbShape::Square,
13076        SliderThumbChoice::Rectangle => widgets::slider::SliderThumbShape::Rectangle,
13077    };
13078    options.thumb_visual = UiVisual::panel(
13079        state.slider_thumb_picker.value(),
13080        Some(StrokeStyle::new(color(79, 93, 113), 1.0)),
13081        6.0,
13082    );
13083    options
13084}
13085
13086#[allow(clippy::field_reassign_with_default)]
13087fn slider_number_input(
13088    ui: &mut UiDocument,
13089    parent: UiNodeId,
13090    name: &'static str,
13091    input: &TextInputState,
13092    focused: FocusedTextInput,
13093    state: &ShowcaseState,
13094    width: f32,
13095) {
13096    let mut options = TextInputOptions::default();
13097    options.layout = LayoutStyle::new().with_width(width).with_height(28.0);
13098    options.text_style = text(12.0, color(230, 236, 246));
13099    options.placeholder_style = text(12.0, color(144, 156, 174));
13100    options.edit_action = Some(format!("{name}.edit").into());
13101    options.focused = state.focused_text == Some(focused);
13102    options.caret_visible = caret_visible(state.caret_phase);
13103    widgets::text_input(ui, parent, name, input, options);
13104}
13105
13106fn form_status_chip(
13107    ui: &mut UiDocument,
13108    parent: UiNodeId,
13109    name: &'static str,
13110    label: &'static str,
13111    active: bool,
13112) {
13113    let chip = ui.add_child(
13114        parent,
13115        UiNode::container(
13116            name,
13117            LayoutStyle::new()
13118                .with_width(82.0)
13119                .with_height(24.0)
13120                .with_padding(4.0)
13121                .with_flex_shrink(0.0),
13122        )
13123        .with_visual(UiVisual::panel(
13124            if active {
13125                color(35, 74, 54)
13126            } else {
13127                color(28, 34, 43)
13128            },
13129            Some(StrokeStyle::new(
13130                if active {
13131                    color(90, 160, 112)
13132                } else {
13133                    color(60, 72, 88)
13134                },
13135                1.0,
13136            )),
13137            4.0,
13138        )),
13139    );
13140    widgets::label(
13141        ui,
13142        chip,
13143        format!("{name}.label"),
13144        label,
13145        text(11.0, color(218, 228, 240)),
13146        LayoutStyle::new()
13147            .with_width_percent(1.0)
13148            .with_height_percent(1.0),
13149    );
13150}
13151
13152fn profile_form_summary(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13153    let has_errors = widgets::form_has_errors(&state.form);
13154    let title = profile_form_summary_title(state, has_errors);
13155    let detail = format!(
13156        "{} | {} | {}",
13157        profile_summary_value(state.form_name_text.text(), "No name"),
13158        profile_summary_value(state.form_email_text.text(), "No email"),
13159        profile_summary_value(state.form_role_text.text(), "No role"),
13160    );
13161    let hint = profile_form_summary_hint(state, has_errors);
13162    let stroke = if has_errors {
13163        color(196, 94, 104)
13164    } else if state.form.dirty {
13165        color(205, 160, 71)
13166    } else if state.form.submitted {
13167        color(91, 164, 119)
13168    } else {
13169        color(60, 72, 88)
13170    };
13171    let summary = ui.add_child(
13172        parent,
13173        UiNode::container(
13174            "forms.profile.summary",
13175            LayoutStyle::column()
13176                .with_width_percent(1.0)
13177                .with_padding(10.0)
13178                .with_gap(4.0)
13179                .with_flex_shrink(0.0),
13180        )
13181        .with_visual(UiVisual::panel(
13182            color(20, 25, 32),
13183            Some(StrokeStyle::new(stroke, 1.0)),
13184            4.0,
13185        ))
13186        .with_accessibility(
13187            AccessibilityMeta::new(AccessibilityRole::Group)
13188                .label("Live profile summary")
13189                .value(format!("{title}. {detail}. {hint}")),
13190        ),
13191    );
13192    widgets::label(
13193        ui,
13194        summary,
13195        "forms.profile.summary.title",
13196        title,
13197        text(13.0, color(232, 240, 250)),
13198        LayoutStyle::new().with_width_percent(1.0),
13199    );
13200    widgets::label(
13201        ui,
13202        summary,
13203        "forms.profile.summary.detail",
13204        detail,
13205        text(12.0, color(186, 198, 216)),
13206        LayoutStyle::new().with_width_percent(1.0),
13207    );
13208    widgets::label(
13209        ui,
13210        summary,
13211        "forms.profile.summary.hint",
13212        hint,
13213        text(11.0, color(154, 166, 184)),
13214        LayoutStyle::new().with_width_percent(1.0),
13215    );
13216}
13217
13218fn profile_form_summary_title(state: &ShowcaseState, has_errors: bool) -> &'static str {
13219    if has_errors {
13220        "Profile needs fixes"
13221    } else if state.form.submitted {
13222        "Profile submitted"
13223    } else if state.form.dirty {
13224        "Profile draft"
13225    } else {
13226        "Profile saved"
13227    }
13228}
13229
13230fn profile_form_summary_hint(state: &ShowcaseState, has_errors: bool) -> &'static str {
13231    if has_errors {
13232        "Fix validation errors before applying or submitting."
13233    } else if state.form.dirty {
13234        "Apply saves the draft; Submit saves and marks it submitted."
13235    } else if state.form.submitted {
13236        "Submission completed. Apply stays disabled until something changes."
13237    } else {
13238        "No pending changes. Submit marks the saved profile submitted."
13239    }
13240}
13241
13242fn profile_summary_value<'a>(value: &'a str, empty: &'static str) -> &'a str {
13243    let value = value.trim();
13244    if value.is_empty() {
13245        empty
13246    } else {
13247        value
13248    }
13249}
13250
13251#[allow(clippy::field_reassign_with_default)]
13252fn form_text_field(
13253    ui: &mut UiDocument,
13254    parent: UiNodeId,
13255    name: &'static str,
13256    input: &TextInputState,
13257    focused: FocusedTextInput,
13258    state: &ShowcaseState,
13259) {
13260    let mut options = TextInputOptions::default();
13261    options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
13262    options.text_style = text(12.0, color(230, 236, 246));
13263    options.placeholder_style = text(12.0, color(144, 156, 174));
13264    options.placeholder = "Required".to_string();
13265    options.edit_action = Some(format!("{name}.edit").into());
13266    options.focused = state.focused_text == Some(focused);
13267    options.caret_visible = caret_visible(state.caret_phase);
13268    widgets::text_input(ui, parent, name, input, options);
13269}
13270
13271fn profile_email_valid(email: &str) -> bool {
13272    let email = email.trim();
13273    let Some((local, domain)) = email.split_once('@') else {
13274        return false;
13275    };
13276    !local.is_empty() && domain.contains('.') && !domain.ends_with('.')
13277}
13278
13279fn drag_source_layout() -> LayoutStyle {
13280    LayoutStyle::row()
13281        .with_width(128.0)
13282        .with_height(40.0)
13283        .with_padding(8.0)
13284        .with_gap(6.0)
13285        .with_flex_shrink(0.0)
13286}
13287
13288fn drop_zone_layout() -> LayoutStyle {
13289    LayoutStyle::column()
13290        .with_width(128.0)
13291        .with_height(78.0)
13292        .with_padding(10.0)
13293        .with_gap(6.0)
13294        .with_flex_shrink(0.0)
13295}
13296
13297fn dnd_operation_chip(
13298    ui: &mut UiDocument,
13299    parent: UiNodeId,
13300    name: &'static str,
13301    label: &'static str,
13302) {
13303    let chip = ui.add_child(
13304        parent,
13305        UiNode::container(
13306            name,
13307            LayoutStyle::new()
13308                .with_width(58.0)
13309                .with_height(22.0)
13310                .with_padding(3.0)
13311                .with_flex_shrink(0.0),
13312        )
13313        .with_visual(UiVisual::panel(
13314            color(26, 32, 42),
13315            Some(StrokeStyle::new(color(62, 76, 94), 1.0)),
13316            3.0,
13317        )),
13318    );
13319    widgets::label(
13320        ui,
13321        chip,
13322        format!("{name}.label"),
13323        label,
13324        text(11.0, color(190, 204, 222)),
13325        LayoutStyle::new()
13326            .with_width_percent(1.0)
13327            .with_height_percent(1.0),
13328    );
13329}
13330
13331fn media_preview_image_layout() -> LayoutStyle {
13332    LayoutStyle::size(46.0, 46.0).with_flex_shrink(0.0)
13333}
13334
13335fn media_icon_columns(state: &ShowcaseState) -> usize {
13336    let theme = state.app_theme();
13337    let options = showcase_desktop_options(state.last_desktop_size, &theme);
13338    let window_width = state
13339        .desktop
13340        .size("media", default_window_size("media"))
13341        .width;
13342    let content_width = (window_width - options.content_padding * 2.0).max(MEDIA_ICON_TILE_WIDTH);
13343    let pitch = MEDIA_ICON_TILE_WIDTH + MEDIA_ICON_GRID_GAP;
13344    (((content_width + MEDIA_ICON_GRID_GAP) / pitch).floor() as usize).clamp(1, MEDIA_ICON_COLUMNS)
13345}
13346
13347fn media_icon_grid_width(columns: usize) -> f32 {
13348    let columns = columns.max(1);
13349    columns as f32 * MEDIA_ICON_TILE_WIDTH + columns.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13350}
13351
13352fn media_icon_grid_height(columns: usize, item_count: usize) -> f32 {
13353    let columns = columns.max(1);
13354    let rows = item_count.div_ceil(columns).max(1);
13355    rows as f32 * MEDIA_ICON_TILE_HEIGHT + rows.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13356}
13357
13358fn media_icon_grid(
13359    ui: &mut UiDocument,
13360    parent: UiNodeId,
13361    name: impl Into<String>,
13362    columns: usize,
13363    item_count: usize,
13364) -> UiNodeId {
13365    let columns = columns.clamp(1, MEDIA_ICON_COLUMNS);
13366    let rows = item_count.div_ceil(columns).max(1);
13367    let width = media_icon_grid_width(columns);
13368    let height = media_icon_grid_height(columns, item_count);
13369    let layout = operad::layout::with_grid_template_rows(
13370        operad::layout::with_grid_template_columns(
13371            Layout::grid()
13372                .size(LayoutSize::points(width, height))
13373                .gap(LayoutGap::points(MEDIA_ICON_GRID_GAP, MEDIA_ICON_GRID_GAP))
13374                .flex(0.0, 0.0, LayoutDimension::Auto)
13375                .to_layout_style(),
13376            (0..columns).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_WIDTH)),
13377        ),
13378        (0..rows).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_HEIGHT)),
13379    );
13380    ui.add_child(parent, UiNode::container(name, layout))
13381}
13382
13383fn media_icon_tile(ui: &mut UiDocument, parent: UiNodeId, icon: BuiltInIcon) {
13384    let name = icon.key().replace('.', "_").replace('-', "_");
13385    let tile = ui.add_child(
13386        parent,
13387        UiNode::container(
13388            format!("media.icon_tile.{name}"),
13389            LayoutStyle::column()
13390                .with_width(MEDIA_ICON_TILE_WIDTH)
13391                .with_height(MEDIA_ICON_TILE_HEIGHT)
13392                .with_padding(6.0)
13393                .with_gap(4.0)
13394                .with_flex_shrink(0.0),
13395        )
13396        .with_visual(UiVisual::panel(
13397            color(17, 22, 30),
13398            Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
13399            4.0,
13400        )),
13401    );
13402    widgets::image(
13403        ui,
13404        tile,
13405        format!("media.icon.{name}"),
13406        icon_image(icon),
13407        widgets::ImageOptions::default()
13408            .with_layout(LayoutStyle::size(28.0, 28.0))
13409            .with_accessibility_label(icon.label()),
13410    );
13411    widgets::label(
13412        ui,
13413        tile,
13414        format!("media.icon_label.{name}"),
13415        icon.label(),
13416        text(9.0, color(180, 194, 214)),
13417        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13418    );
13419}
13420
13421fn slider_checkbox(
13422    ui: &mut UiDocument,
13423    parent: UiNodeId,
13424    name: &'static str,
13425    label: &'static str,
13426    checked: bool,
13427) {
13428    slider_checkbox_with_layout(
13429        ui,
13430        parent,
13431        name,
13432        label,
13433        checked,
13434        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13435    );
13436}
13437
13438fn slider_checkbox_with_layout(
13439    ui: &mut UiDocument,
13440    parent: UiNodeId,
13441    name: &'static str,
13442    label: &'static str,
13443    checked: bool,
13444    layout: LayoutStyle,
13445) {
13446    let mut options = widgets::CheckboxOptions::default().with_action(name);
13447    options.layout = layout;
13448    options.text_style = text(12.0, color(220, 228, 238));
13449    widgets::checkbox(ui, parent, name, label, checked, options);
13450}
13451
13452fn choice_button(
13453    ui: &mut UiDocument,
13454    parent: UiNodeId,
13455    name: &'static str,
13456    label: &'static str,
13457    selected: bool,
13458) {
13459    let mut options =
13460        widgets::ButtonOptions::new(LayoutStyle::new().with_width(78.0).with_height(28.0))
13461            .with_action(name);
13462    options.visual = if selected {
13463        button_visual(48, 112, 184)
13464    } else {
13465        button_visual(38, 46, 58)
13466    };
13467    options.hovered_visual = Some(button_visual(65, 86, 106));
13468    options.pressed_visual = Some(button_visual(34, 54, 84));
13469    options.text_style = text(12.0, color(238, 244, 252));
13470    widgets::button(ui, parent, name, label, options);
13471}
13472
13473fn divider(ui: &mut UiDocument, parent: UiNodeId, name: &'static str) {
13474    ui.add_child(
13475        parent,
13476        UiNode::container(
13477            name,
13478            LayoutStyle::new()
13479                .with_width_percent(1.0)
13480                .with_height(1.0)
13481                .with_flex_shrink(0.0),
13482        )
13483        .with_visual(UiVisual::panel(color(48, 58, 72), None, 0.0)),
13484    );
13485}
13486
13487fn canvas(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13488    let canvas_intrinsic = UiSize::new(720.0, 405.0);
13489    let body = section_with_min_viewport(ui, parent, "canvas", "Canvas", UiSize::new(720.0, 458.0));
13490    let controls = wrapping_row(ui, body, "canvas.options", 10.0);
13491    canvas_option_checkbox(
13492        ui,
13493        controls,
13494        "canvas.grow_horizontal",
13495        "Grow width",
13496        state.canvas_grow_horizontal,
13497    );
13498    canvas_option_checkbox(
13499        ui,
13500        controls,
13501        "canvas.grow_vertical",
13502        "Grow height",
13503        state.canvas_grow_vertical,
13504    );
13505    canvas_option_checkbox(
13506        ui,
13507        controls,
13508        "canvas.keep_aspect_ratio",
13509        "Keep aspect ratio",
13510        state.canvas_keep_aspect_ratio,
13511    );
13512
13513    let mut options = widgets::CanvasOptions::default()
13514        .with_accessibility_label("Shader canvas")
13515        .with_action("canvas.rotate")
13516        .with_intrinsic_size(canvas_intrinsic);
13517    options.action_mode = WidgetActionMode::Drag;
13518    if state.canvas_keep_aspect_ratio {
13519        options = options.with_aspect_ratio(16.0 / 9.0);
13520    }
13521    let canvas_width = if state.canvas_grow_horizontal {
13522        LayoutDimension::percent(1.0)
13523    } else {
13524        LayoutDimension::points(canvas_intrinsic.width)
13525    };
13526    let canvas_height = if state.canvas_grow_vertical {
13527        LayoutDimension::percent(1.0)
13528    } else {
13529        LayoutDimension::points(canvas_intrinsic.height)
13530    };
13531    options.layout = Layout::new()
13532        .size(LayoutSize::new(canvas_width, canvas_height))
13533        .min_size(LayoutSize::points(
13534            canvas_intrinsic.width,
13535            canvas_intrinsic.height,
13536        ))
13537        .flex(
13538            if state.canvas_grow_vertical { 1.0 } else { 0.0 },
13539            1.0,
13540            LayoutDimension::Auto,
13541        )
13542        .to_layout_style();
13543    options.visual = UiVisual::panel(
13544        color(18, 22, 28),
13545        Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
13546        4.0,
13547    );
13548    widgets::canvas(
13549        ui,
13550        body,
13551        "canvas.shader",
13552        CanvasContent::new("canvas.shader").program(showcase_canvas_program(state.cube)),
13553        options,
13554    );
13555}
13556
13557fn canvas_option_checkbox(
13558    ui: &mut UiDocument,
13559    parent: UiNodeId,
13560    name: &'static str,
13561    label: &'static str,
13562    checked: bool,
13563) {
13564    let mut options = widgets::CheckboxOptions::default()
13565        .with_action(name)
13566        .with_text_style(text(12.0, color(220, 228, 238)));
13567    options.layout = LayoutStyle::new().with_height(28.0).with_flex_shrink(0.0);
13568    widgets::checkbox(ui, parent, name, label, checked, options);
13569}
13570
13571fn showcase_canvas_program(cube: CanvasCubeState) -> CanvasRenderProgram {
13572    CanvasRenderProgram::wgsl(include_str!("shaders/showcase_canvas.wgsl"))
13573        .label("showcase.canvas")
13574        .constant("CUBE_YAW", cube.yaw as f64)
13575        .constant("CUBE_PITCH", cube.pitch as f64)
13576        .clear_color(Some(color(18, 22, 28)))
13577}
13578
13579fn section(
13580    ui: &mut UiDocument,
13581    parent: UiNodeId,
13582    name: impl Into<String>,
13583    _title: impl Into<String>,
13584) -> UiNodeId {
13585    section_with_min_viewport(ui, parent, name, _title, UiSize::ZERO)
13586}
13587
13588fn section_with_min_viewport(
13589    ui: &mut UiDocument,
13590    parent: UiNodeId,
13591    name: impl Into<String>,
13592    _title: impl Into<String>,
13593    min_viewport_size: UiSize,
13594) -> UiNodeId {
13595    let name = name.into();
13596    let layout = Layout::column()
13597        .size(LayoutSize::percent(1.0, 1.0))
13598        .min_size(LayoutSize::points(
13599            min_viewport_size.width.max(0.0),
13600            min_viewport_size.height.max(0.0),
13601        ))
13602        .gap(LayoutGap::points(10.0, 10.0))
13603        .flex(1.0, 1.0, LayoutDimension::Auto)
13604        .to_layout_style();
13605    widgets::scroll_area(
13606        ui,
13607        parent,
13608        format!("{name}.section_scroll"),
13609        ScrollAxes::BOTH,
13610        layout,
13611    )
13612}
Source

pub const fn width(self, width: LayoutDimension) -> Self

Source

pub const fn height(self, height: LayoutDimension) -> Self

Source

pub const fn to_taffy(self) -> TaffySize<Dimension>

Source

pub fn from_taffy(size: TaffySize<Dimension>) -> Option<Self>

Trait Implementations§

Source§

impl Clone for LayoutSize

Source§

fn clone(&self) -> LayoutSize

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 LayoutSize

Source§

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

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

impl From<LayoutSize> for Size<Dimension>

Source§

fn from(value: LayoutSize) -> Self

Converts to this type from the input type.
Source§

impl PartialEq for LayoutSize

Source§

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

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

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

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

impl Copy for LayoutSize

Source§

impl StructuralPartialEq for LayoutSize

Auto Trait Implementations§

Blanket Implementations§

Source§

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

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

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

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

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

Source§

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

Mutably borrows from an owned value. Read more
Source§

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

Source§

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

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

impl<T> Downcast<T> for T

Source§

fn downcast(&self) -> &T

Source§

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

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

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

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

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

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

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

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

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

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

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

fn in_current_span(self) -> Instrumented<Self>

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

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

Source§

fn into(self) -> U

Calls U::from(self).

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

Source§

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

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

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

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

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

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

Source§

type Error = Infallible

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

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

Performs the conversion.
Source§

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

Source§

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

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

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

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

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

Source§

impl<T> WithSubscriber for T

Source§

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

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

fn with_current_subscriber(self) -> WithDispatch<Self>

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

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

Source§

impl<T> WasmNotSendSync for T

Source§

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