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 6899-6902)
6717fn form_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6718    let body = section_with_min_viewport(ui, parent, "forms", "Forms", UiSize::new(390.0, 0.0));
6719    let section = widgets::form_section(
6720        ui,
6721        body,
6722        "forms.profile",
6723        Some("Profile".to_string()),
6724        widgets::FormSectionOptions::default().with_layout(
6725            LayoutStyle::column()
6726                .with_width_percent(1.0)
6727                .with_padding(12.0)
6728                .with_gap(10.0),
6729        ),
6730    );
6731    let status_row = wrapping_row(ui, section.root, "forms.profile.status_flags", 6.0);
6732    form_status_chip(
6733        ui,
6734        status_row,
6735        "forms.profile.status.dirty",
6736        "dirty",
6737        state.form.dirty,
6738    );
6739    form_status_chip(
6740        ui,
6741        status_row,
6742        "forms.profile.status.pending",
6743        "pending",
6744        state.form.pending,
6745    );
6746    form_status_chip(
6747        ui,
6748        status_row,
6749        "forms.profile.status.submitted",
6750        "submitted",
6751        state.form.submitted,
6752    );
6753
6754    let mut name_options = widgets::FormRowOptions::default().required();
6755    if state.form_name_text.text().trim().is_empty() {
6756        name_options = name_options.invalid("Name is required");
6757    }
6758    let name = widgets::form_row(ui, section.root, "forms.profile.name", name_options);
6759    widgets::field_label(
6760        ui,
6761        name,
6762        "forms.profile.name.label",
6763        "Name",
6764        widgets::FieldLabelOptions::default().required(),
6765    );
6766    form_text_field(
6767        ui,
6768        name,
6769        "forms.profile.name.input",
6770        &state.form_name_text,
6771        FocusedTextInput::FormName,
6772        state,
6773    );
6774    if state.form_name_text.text().trim().is_empty() {
6775        widgets::field_validation_message(
6776            ui,
6777            name,
6778            "forms.profile.name.validation",
6779            ValidationMessage::error("Name is required"),
6780            widgets::ValidationMessageOptions::default(),
6781        );
6782    } else {
6783        widgets::field_help_text(
6784            ui,
6785            name,
6786            "forms.profile.name.help",
6787            "Shown in window titles and project lists.",
6788            widgets::FieldHelpOptions::default(),
6789        );
6790    }
6791
6792    let mut email_options = widgets::FormRowOptions::default().required();
6793    if !profile_email_valid(state.form_email_text.text()) {
6794        email_options = email_options.invalid("Use a complete email address");
6795    }
6796    let email = widgets::form_row(ui, section.root, "forms.profile.email", email_options);
6797    widgets::field_label(
6798        ui,
6799        email,
6800        "forms.profile.email.label",
6801        "Email",
6802        widgets::FieldLabelOptions::default().required(),
6803    );
6804    form_text_field(
6805        ui,
6806        email,
6807        "forms.profile.email.input",
6808        &state.form_email_text,
6809        FocusedTextInput::FormEmail,
6810        state,
6811    );
6812    if profile_email_valid(state.form_email_text.text()) {
6813        widgets::field_help_text(
6814            ui,
6815            email,
6816            "forms.profile.email.help",
6817            "Used for workspace invites and notifications.",
6818            widgets::FieldHelpOptions::default(),
6819        );
6820    } else {
6821        widgets::field_validation_message(
6822            ui,
6823            email,
6824            "forms.profile.email.validation",
6825            ValidationMessage::error("Use a complete email address"),
6826            widgets::ValidationMessageOptions::default(),
6827        );
6828    }
6829
6830    let role = widgets::form_row(
6831        ui,
6832        section.root,
6833        "forms.profile.role",
6834        widgets::FormRowOptions::default(),
6835    );
6836    widgets::field_label(
6837        ui,
6838        role,
6839        "forms.profile.role.label",
6840        "Role",
6841        widgets::FieldLabelOptions::default(),
6842    );
6843    form_text_field(
6844        ui,
6845        role,
6846        "forms.profile.role.input",
6847        &state.form_role_text,
6848        FocusedTextInput::FormRole,
6849        state,
6850    );
6851    widgets::field_validation_message(
6852        ui,
6853        role,
6854        "forms.profile.role.help",
6855        if state.form_role_text.text().trim().is_empty() {
6856            ValidationMessage::warning("Role can be added later")
6857        } else {
6858            ValidationMessage::info(
6859                "Form rows compose labels, controls, help, and validation text.",
6860            )
6861        },
6862        widgets::ValidationMessageOptions::default(),
6863    );
6864
6865    let newsletter = widgets::form_row(
6866        ui,
6867        section.root,
6868        "forms.profile.newsletter",
6869        widgets::FormRowOptions::default().with_accessibility_label("Newsletter preference"),
6870    );
6871    let mut newsletter_options =
6872        widgets::CheckboxOptions::default().with_action("forms.profile.newsletter.toggle");
6873    newsletter_options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
6874    newsletter_options.text_style = text(12.0, color(220, 228, 238));
6875    widgets::checkbox(
6876        ui,
6877        newsletter,
6878        "forms.profile.newsletter.input",
6879        "Send release notes",
6880        state.form_newsletter,
6881        newsletter_options,
6882    );
6883    widgets::field_help_text(
6884        ui,
6885        newsletter,
6886        "forms.profile.newsletter.help",
6887        "Checkboxes participate in the same form state as text fields.",
6888        widgets::FieldHelpOptions::default(),
6889    );
6890
6891    widgets::form_error_summary(
6892        ui,
6893        section.root,
6894        "forms.profile.errors",
6895        &state.form,
6896        widgets::FormErrorSummaryOptions::default(),
6897    );
6898    let action_layout = Layout::row()
6899        .size(LayoutSize::new(
6900            LayoutDimension::percent(1.0),
6901            LayoutDimension::Auto,
6902        ))
6903        .gap(LayoutGap::points(8.0, 8.0))
6904        .flex_wrap(LayoutFlexWrap::Wrap)
6905        .to_layout_style();
6906    widgets::form_action_buttons(
6907        ui,
6908        section.root,
6909        "forms.profile.actions",
6910        &state.form,
6911        widgets::FormActionButtonsOptions::default()
6912            .with_layout(action_layout)
6913            .include_reset(true)
6914            .with_action_prefix("forms.profile"),
6915    );
6916    widgets::label(
6917        ui,
6918        section.root,
6919        "forms.profile.status",
6920        format!("Status: {}", state.form_status),
6921        text(11.0, color(154, 166, 184)),
6922        LayoutStyle::new().with_width_percent(1.0),
6923    );
6924}
6925
6926fn overlay_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6927    let body =
6928        section_with_min_viewport(ui, parent, "overlays", "Overlays", UiSize::new(420.0, 0.0));
6929    let header = widgets::collapsing_header(
6930        ui,
6931        body,
6932        "overlays.collapsing",
6933        "Collapsing header",
6934        widgets::CollapsingHeaderOptions::default()
6935            .expanded(state.overlay_expanded)
6936            .with_toggle_action("overlays.collapsing.toggle"),
6937    );
6938    if let Some(panel) = header.body {
6939        widgets::label(
6940            ui,
6941            panel,
6942            "overlays.collapsing.body",
6943            "Expanded content lives under the header and remains part of normal layout.",
6944            text(12.0, color(196, 210, 230)),
6945            LayoutStyle::new().with_width_percent(1.0),
6946        );
6947    }
6948
6949    let controls = wrapping_row(ui, body, "overlays.controls", 8.0);
6950    button(
6951        ui,
6952        controls,
6953        "overlays.popup.toggle",
6954        if state.overlay_popup_open {
6955            "Close popup"
6956        } else {
6957            "Open popup"
6958        },
6959        "overlays.popup.toggle",
6960        button_visual(48, 112, 184),
6961    );
6962    button(
6963        ui,
6964        controls,
6965        "overlays.modal.open",
6966        "Open modal",
6967        "overlays.modal.open",
6968        button_visual(58, 78, 96),
6969    );
6970
6971    let tooltip = TooltipContent::new("Tooltip")
6972        .body("Tooltip boxes are overlay surfaces with title, body, and shortcut text.")
6973        .shortcut_label("Ctrl+K")
6974        .disabled_reason("Disabled reasons can be announced without changing the trigger.");
6975    let mut tooltip_options = widgets::TooltipBoxOptions::default()
6976        .with_layout(
6977            LayoutStyle::column()
6978                .with_width(280.0)
6979                .with_padding(8.0)
6980                .with_gap(4.0),
6981        )
6982        .with_animation(None);
6983    tooltip_options.layer = UiLayer::AppContent;
6984    tooltip_options.z_index = 0;
6985    widgets::tooltip_box(ui, body, "overlays.tooltip", tooltip, tooltip_options);
6986
6987    let tooltip_anchor = row(ui, body, "overlays.tooltip_anchor", 8.0);
6988    widgets::label(
6989        ui,
6990        tooltip_anchor,
6991        "overlays.tooltip_anchor.label",
6992        "Tooltip placement clamps to its viewport.",
6993        text(12.0, color(166, 176, 190)),
6994        LayoutStyle::new().with_width_percent(1.0),
6995    );
6996    let clamped_rect = widgets::tooltip::tooltip_rect(
6997        UiRect::new(328.0, 12.0, 54.0, 24.0),
6998        UiSize::new(176.0, 58.0),
6999        UiRect::new(0.0, 0.0, 420.0, 190.0),
7000        TooltipPlacement::Right,
7001        8.0,
7002        None,
7003    );
7004    let clamped_preview = ui.add_child(
7005        body,
7006        UiNode::container(
7007            "overlays.tooltip_rect.preview",
7008            LayoutStyle::new()
7009                .with_width_percent(1.0)
7010                .with_height(78.0)
7011                .with_flex_shrink(0.0),
7012        )
7013        .with_visual(UiVisual::panel(
7014            color(12, 16, 22),
7015            Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
7016            4.0,
7017        )),
7018    );
7019    ui.add_child(
7020        clamped_preview,
7021        UiNode::scene(
7022            "overlays.tooltip_rect.scene",
7023            vec![
7024                ScenePrimitive::Rect(
7025                    PaintRect::solid(UiRect::new(328.0, 12.0, 54.0, 24.0), color(48, 112, 184))
7026                        .corner_radii(CornerRadii::uniform(3.0)),
7027                ),
7028                ScenePrimitive::Rect(
7029                    PaintRect::solid(clamped_rect, color(24, 29, 38))
7030                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7031                            color(92, 106, 128),
7032                            1.0,
7033                        )))
7034                        .corner_radii(CornerRadii::uniform(4.0)),
7035                ),
7036            ],
7037            LayoutStyle::new()
7038                .with_width_percent(1.0)
7039                .with_height_percent(1.0),
7040        ),
7041    );
7042
7043    if state.overlay_popup_open {
7044        let popup = ext_widgets::popup_panel(
7045            ui,
7046            parent,
7047            "overlays.popup_panel",
7048            UiRect::new(18.0, 150.0, 220.0, 112.0),
7049            ext_widgets::PopupOptions {
7050                z_index: 20,
7051                portal: UiPortalTarget::Parent,
7052                accessibility: Some(
7053                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup"),
7054                ),
7055                ..Default::default()
7056            },
7057        );
7058        let popup_body = ui.add_child(
7059            popup,
7060            UiNode::container(
7061                "overlays.popup_panel.body",
7062                LayoutStyle::column()
7063                    .with_width_percent(1.0)
7064                    .with_height_percent(1.0)
7065                    .with_padding(10.0)
7066                    .with_gap(6.0),
7067            ),
7068        );
7069        let popup_header = row(ui, popup_body, "overlays.popup_panel.header", 8.0);
7070        widgets::label(
7071            ui,
7072            popup_header,
7073            "overlays.popup_panel.label",
7074            "Popup panel",
7075            text(12.0, color(220, 228, 238)),
7076            LayoutStyle::new().with_width_percent(1.0),
7077        );
7078        let mut close = widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0))
7079            .with_action("overlays.popup.close");
7080        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7081        close.hovered_visual = Some(button_visual(54, 70, 92));
7082        close.text_style = text(12.0, color(220, 228, 238));
7083        widgets::button(ui, popup_header, "overlays.popup_panel.close", "x", close);
7084        widgets::label(
7085            ui,
7086            popup_body,
7087            "overlays.popup_panel.body_text",
7088            "Popup content is conditionally rendered.",
7089            text(11.0, color(196, 210, 230)),
7090            LayoutStyle::new().with_width_percent(1.0),
7091        );
7092    }
7093
7094    if state.overlay_modal_open {
7095        let modal = widgets::modal_dialog(
7096            ui,
7097            parent,
7098            "overlays.modal",
7099            "Modal dialog",
7100            widgets::ModalDialogOptions::default()
7101                .with_size(320.0, 180.0)
7102                .with_close_action("overlays.modal.close")
7103                .with_dismissal(ext_widgets::DialogDismissal::MODAL)
7104                .with_focus_restore(FocusRestoreTarget::Previous),
7105        );
7106        widgets::label(
7107            ui,
7108            modal.body,
7109            "overlays.modal.body.text",
7110            "Modal dialogs are portaled to the application overlay, include a scrim, and trap focus.",
7111            text(12.0, color(220, 228, 238)),
7112            LayoutStyle::new().with_width_percent(1.0),
7113        );
7114        button(
7115            ui,
7116            modal.body,
7117            "overlays.modal.body.close",
7118            "Close modal",
7119            "overlays.modal.close",
7120            button_visual(48, 112, 184),
7121        );
7122    }
7123}
7124
7125fn drag_drop_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7126    let body = section_with_min_viewport(
7127        ui,
7128        parent,
7129        "drag_drop",
7130        "Drag and drop",
7131        UiSize::new(420.0, 0.0),
7132    );
7133    widgets::label(
7134        ui,
7135        body,
7136        "drag_drop.sources.label",
7137        "Drag sources",
7138        text(12.0, color(166, 176, 190)),
7139        LayoutStyle::new().with_width_percent(1.0),
7140    );
7141    let sources = wrapping_row(ui, body, "drag_drop.sources", 8.0);
7142    widgets::dnd_drag_source(
7143        ui,
7144        sources,
7145        "drag_drop.text_source",
7146        "Text payload",
7147        DragPayload::text("Operad payload"),
7148        widgets::DragSourceOptions::default()
7149            .with_layout(drag_source_layout())
7150            .with_kind(DragDropSurfaceKind::ListRow)
7151            .with_allowed_operations([DragOperation::Copy, DragOperation::Move])
7152            .with_action("drag_drop.text_source")
7153            .with_accessibility_hint("Start a text drag operation"),
7154    );
7155    widgets::dnd_drag_source(
7156        ui,
7157        sources,
7158        "drag_drop.file_source",
7159        "File payload",
7160        DragPayload::files(["/tmp/showcase.scene"]),
7161        widgets::DragSourceOptions::default()
7162            .with_layout(drag_source_layout())
7163            .with_kind(DragDropSurfaceKind::Asset)
7164            .with_drag_image_policy(widgets::DragImagePolicy::image_key(
7165                BuiltInIcon::Folder.key(),
7166                UiSize::new(120.0, 36.0),
7167                UiPoint::new(10.0, 10.0),
7168            ))
7169            .with_allowed_operations([DragOperation::Copy])
7170            .with_action("drag_drop.file_source"),
7171    );
7172    widgets::dnd_drag_source(
7173        ui,
7174        sources,
7175        "drag_drop.bytes_source",
7176        "Image bytes",
7177        DragPayload::bytes(DragBytes::new("image/png", vec![137, 80, 78, 71]).name("sprite.png")),
7178        widgets::DragSourceOptions::default()
7179            .with_layout(drag_source_layout())
7180            .with_kind(DragDropSurfaceKind::Asset)
7181            .with_action("drag_drop.bytes_source")
7182            .without_drag_image(),
7183    );
7184
7185    widgets::label(
7186        ui,
7187        body,
7188        "drag_drop.zones.label",
7189        "Drop zones",
7190        text(12.0, color(166, 176, 190)),
7191        LayoutStyle::new().with_width_percent(1.0),
7192    );
7193    let zones = wrapping_row(ui, body, "drag_drop.zones", 8.0);
7194    let accepted_options = widgets::DropZoneOptions::default()
7195        .with_layout(drop_zone_layout())
7196        .with_kind(DragDropSurfaceKind::EditorSurface)
7197        .with_accepted_payload(DropPayloadFilter::empty().text())
7198        .with_accepted_operations([DragOperation::Copy, DragOperation::Move])
7199        .with_action("drag_drop.accept_text")
7200        .with_accessibility_hint("Accepts text payloads");
7201    let accepted = widgets::dnd_drop_zone(
7202        ui,
7203        zones,
7204        "drag_drop.accept_text",
7205        "Text accepted",
7206        accepted_options.clone(),
7207    );
7208    widgets::drag_drop::dnd_apply_drop_zone_preview(
7209        ui,
7210        accepted.root,
7211        &accepted_options,
7212        widgets::drag_drop::DropZonePreviewState::Accepted,
7213    );
7214
7215    let rejected_options = widgets::DropZoneOptions::default()
7216        .with_layout(drop_zone_layout())
7217        .with_kind(DragDropSurfaceKind::Asset)
7218        .with_accepted_payload(DropPayloadFilter::empty().files())
7219        .with_action("drag_drop.files_only");
7220    let rejected = widgets::dnd_drop_zone(
7221        ui,
7222        zones,
7223        "drag_drop.files_only",
7224        "Files only",
7225        rejected_options.clone(),
7226    );
7227    widgets::drag_drop::dnd_apply_drop_zone_preview(
7228        ui,
7229        rejected.root,
7230        &rejected_options,
7231        widgets::drag_drop::DropZonePreviewState::Rejected,
7232    );
7233    let image_options = widgets::DropZoneOptions::default()
7234        .with_layout(drop_zone_layout())
7235        .with_kind(DragDropSurfaceKind::Asset)
7236        .with_accepted_payload(DropPayloadFilter::empty().mime_type("image/*"))
7237        .with_accepted_operations([DragOperation::Copy])
7238        .with_action("drag_drop.image_bytes");
7239    let image_zone = widgets::dnd_drop_zone(
7240        ui,
7241        zones,
7242        "drag_drop.image_bytes",
7243        "Image bytes",
7244        image_options.clone(),
7245    );
7246    widgets::drag_drop::dnd_apply_drop_zone_preview(
7247        ui,
7248        image_zone.root,
7249        &image_options,
7250        widgets::drag_drop::DropZonePreviewState::Hovered,
7251    );
7252
7253    let disabled_options = widgets::DropZoneOptions::default()
7254        .with_layout(drop_zone_layout())
7255        .with_kind(DragDropSurfaceKind::EditorSurface)
7256        .with_accepted_payload(DropPayloadFilter::any())
7257        .with_action("drag_drop.disabled")
7258        .disabled();
7259    let disabled_zone = widgets::dnd_drop_zone(
7260        ui,
7261        zones,
7262        "drag_drop.disabled",
7263        "Disabled",
7264        disabled_options.clone(),
7265    );
7266    widgets::drag_drop::dnd_apply_drop_zone_preview(
7267        ui,
7268        disabled_zone.root,
7269        &disabled_options,
7270        widgets::drag_drop::DropZonePreviewState::Disabled,
7271    );
7272
7273    let operation_row = wrapping_row(ui, body, "drag_drop.operations", 6.0);
7274    dnd_operation_chip(ui, operation_row, "drag_drop.operation.copy", "copy");
7275    dnd_operation_chip(ui, operation_row, "drag_drop.operation.move", "move");
7276    dnd_operation_chip(ui, operation_row, "drag_drop.operation.link", "link");
7277    widgets::label(
7278        ui,
7279        body,
7280        "drag_drop.status",
7281        format!("Status: {}", state.drag_drop_status),
7282        text(11.0, color(154, 166, 184)),
7283        LayoutStyle::new().with_width_percent(1.0),
7284    );
7285}
7286
7287fn media_widgets(ui: &mut UiDocument, parent: UiNodeId) {
7288    let body = section_with_min_viewport(ui, parent, "media", "Media", UiSize::new(430.0, 0.0));
7289    widgets::label(
7290        ui,
7291        body,
7292        "media.icons.label",
7293        "Built-in icons",
7294        text(12.0, color(166, 176, 190)),
7295        LayoutStyle::new().with_width_percent(1.0),
7296    );
7297    let icons = wrapping_row(ui, body, "media.icons", 8.0);
7298    for icon in BuiltInIcon::COMMON {
7299        media_icon_tile(ui, icons, icon);
7300    }
7301
7302    widgets::label(
7303        ui,
7304        body,
7305        "media.variants.label",
7306        "Image variants",
7307        text(12.0, color(166, 176, 190)),
7308        LayoutStyle::new().with_width_percent(1.0),
7309    );
7310    let variants = wrapping_row(ui, body, "media.variants", 10.0);
7311    widgets::image(
7312        ui,
7313        variants,
7314        "media.image.untinted",
7315        icon_image(BuiltInIcon::Play),
7316        widgets::ImageOptions::default()
7317            .with_layout(media_preview_image_layout())
7318            .with_accessibility_label("Untinted play icon"),
7319    );
7320    widgets::image(
7321        ui,
7322        variants,
7323        "media.image.warning",
7324        ImageContent::new(BuiltInIcon::Warning.key()).tinted(color(232, 186, 88)),
7325        widgets::ImageOptions::default()
7326            .with_layout(media_preview_image_layout())
7327            .with_accessibility_label("Tinted warning icon"),
7328    );
7329    widgets::image(
7330        ui,
7331        variants,
7332        "media.image.shader",
7333        ImageContent::new(BuiltInIcon::Grid.key()).tinted(color(118, 183, 255)),
7334        widgets::ImageOptions::default()
7335            .with_layout(media_preview_image_layout())
7336            .with_shader(ShaderEffect::new("media.preview.tint").uniform("amount", 0.5))
7337            .with_accessibility_label("Shader-decorated grid icon"),
7338    );
7339    widgets::label(
7340        ui,
7341        body,
7342        "media.image.note",
7343        "Image widgets reference stable resource keys; the host resolves them to textures, vector assets, tinting, or shader-backed resources.",
7344        text(12.0, color(166, 176, 190)),
7345        LayoutStyle::new().with_width_percent(1.0),
7346    );
7347}
7348
7349fn timeline_ruler(ui: &mut UiDocument, parent: UiNodeId) {
7350    let layout = LayoutStyle::column()
7351        .with_width_percent(1.0)
7352        .with_height(40.0)
7353        .with_flex_shrink(0.0);
7354    let layout = operad::layout::with_min_size(layout, operad::length(0.0), operad::length(0.0));
7355    let body = widgets::scroll_area(ui, parent, "timeline", ScrollAxes::BOTH, layout);
7356    ext_widgets::timeline_ruler(
7357        ui,
7358        body,
7359        "timeline.ruler",
7360        ext_widgets::RulerSpec {
7361            range: ext_widgets::TimelineRange::new(0.0, 12.0),
7362            width: 600.0,
7363            major_step: 2.0,
7364            minor_step: 0.5,
7365            label_every: 1,
7366        },
7367        ext_widgets::TimelineRulerOptions::default(),
7368    );
7369}
7370
7371fn toast_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7372    let body = section(ui, parent, "toasts", "Toasts");
7373    let controls = row(ui, body, "toasts.controls", 10.0);
7374    button(
7375        ui,
7376        controls,
7377        "toasts.show",
7378        "Show toast",
7379        "toast.show",
7380        button_visual(48, 112, 184),
7381    );
7382    button(
7383        ui,
7384        controls,
7385        "toasts.hide",
7386        "Hide",
7387        "toast.hide",
7388        button_visual(58, 78, 96),
7389    );
7390    widgets::label(
7391        ui,
7392        body,
7393        "toasts.status",
7394        if state.toast_visible {
7395            "Toast overlay is visible."
7396        } else {
7397            "Toast overlay is hidden."
7398        },
7399        text(12.0, color(196, 210, 230)),
7400        LayoutStyle::new().with_width_percent(1.0),
7401    );
7402    widgets::label(
7403        ui,
7404        body,
7405        "toasts.action_status",
7406        format!("Action: {}", state.toast_action_status),
7407        text(12.0, color(154, 166, 184)),
7408        LayoutStyle::new().with_width_percent(1.0),
7409    );
7410}
7411
7412fn popup_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7413    let body = section(ui, parent, "popup_panel", "Popup panel");
7414    let controls = row(ui, body, "popup_panel.controls", 8.0);
7415    button(
7416        ui,
7417        controls,
7418        "popup_panel.toggle",
7419        if state.popup_open {
7420            "Close popup"
7421        } else {
7422            "Open popup"
7423        },
7424        "popup.toggle",
7425        button_visual(48, 112, 184),
7426    );
7427    if state.popup_open {
7428        let mut close =
7429            widgets::ButtonOptions::new(LayoutStyle::size(30.0, 30.0)).with_action("popup.close");
7430        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7431        close.hovered_visual = Some(button_visual(54, 70, 92));
7432        close.text_style = text(13.0, color(220, 228, 238));
7433        widgets::button(ui, controls, "popup_panel.inline_close", "x", close);
7434    }
7435    widgets::label(
7436        ui,
7437        body,
7438        "popup_panel.status",
7439        if state.popup_open {
7440            "Popup overlay is open."
7441        } else {
7442            "Popup overlay is closed."
7443        },
7444        text(12.0, color(196, 210, 230)),
7445        LayoutStyle::new().with_width_percent(1.0),
7446    );
7447    if state.popup_open {
7448        let panel = ext_widgets::popup_panel(
7449            ui,
7450            parent,
7451            "popup_panel.inline_preview",
7452            UiRect::new(0.0, 20.0, 160.0, 104.0),
7453            ext_widgets::PopupOptions {
7454                z_index: 4,
7455                portal: UiPortalTarget::Parent,
7456                accessibility: Some(
7457                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup preview"),
7458                ),
7459                ..Default::default()
7460            },
7461        );
7462        let content = ui.add_child(
7463            panel,
7464            UiNode::container(
7465                "popup_panel.inline_preview.body",
7466                LayoutStyle::column()
7467                    .with_width_percent(1.0)
7468                    .with_height_percent(1.0)
7469                    .with_padding(10.0)
7470                    .with_gap(8.0),
7471            ),
7472        );
7473        let header = row(ui, content, "popup_panel.inline_preview.header", 8.0);
7474        widgets::label(
7475            ui,
7476            header,
7477            "popup_panel.inline_preview.title",
7478            "Popup panel",
7479            text(12.0, color(226, 234, 246)),
7480            LayoutStyle::new().with_width_percent(1.0),
7481        );
7482        let mut close =
7483            widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0)).with_action("popup.close");
7484        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7485        close.hovered_visual = Some(button_visual(54, 70, 92));
7486        close.text_style = text(12.0, color(220, 228, 238));
7487        widgets::button(ui, header, "popup_panel.inline_preview.close", "x", close);
7488        widgets::label(
7489            ui,
7490            content,
7491            "popup_panel.inline_preview.text",
7492            "Overlay content",
7493            text(11.0, color(196, 210, 230)),
7494            LayoutStyle::new().with_width_percent(1.0),
7495        );
7496        widgets::spacer(
7497            ui,
7498            body,
7499            "popup_panel.inline_preview.space",
7500            LayoutStyle::new()
7501                .with_width_percent(1.0)
7502                .with_height(112.0)
7503                .with_flex_shrink(0.0),
7504        );
7505    }
7506}
7507
7508fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7509    let body = section(ui, parent, "styling", "Styling");
7510    let grid_layout = operad::layout::with_grid_template_columns(
7511        Layout::grid()
7512            .size(LayoutSize::percent(1.0, 1.0))
7513            .gap(LayoutGap::points(10.0, 10.0))
7514            .to_layout_style(),
7515        [
7516            LayoutGridTrack::points(300.0),
7517            LayoutGridTrack::points(1.0),
7518            LayoutGridTrack::points(210.0),
7519        ],
7520    );
7521    let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
7522    let controls = ui.add_child(
7523        grid,
7524        UiNode::container(
7525            "styling.controls",
7526            LayoutStyle::column()
7527                .with_width(300.0)
7528                .with_height_percent(1.0)
7529                .with_flex_shrink(0.0)
7530                .gap(6.0),
7531        ),
7532    );
7533    style_edge_group(
7534        ui,
7535        controls,
7536        "styling.inner",
7537        "Inner margin",
7538        "styling.inner_same",
7539        state.styling.inner_same,
7540        [
7541            ("Left", "styling.inner", state.styling.inner_margin),
7542            ("Right", "styling.inner_right", state.styling.inner_right),
7543            ("Top", "styling.inner_top", state.styling.inner_top),
7544            ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
7545        ],
7546        0.0..32.0,
7547    );
7548    style_edge_group(
7549        ui,
7550        controls,
7551        "styling.outer",
7552        "Outer margin",
7553        "styling.outer_same",
7554        state.styling.outer_same,
7555        [
7556            ("Left", "styling.outer", state.styling.outer_margin),
7557            ("Right", "styling.outer_right", state.styling.outer_right),
7558            ("Top", "styling.outer_top", state.styling.outer_top),
7559            ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
7560        ],
7561        0.0..40.0,
7562    );
7563    style_edge_group(
7564        ui,
7565        controls,
7566        "styling.radius",
7567        "Corner radius",
7568        "styling.radius_same",
7569        state.styling.radius_same,
7570        [
7571            ("NW", "styling.radius", state.styling.corner_radius),
7572            ("NE", "styling.radius_ne", state.styling.corner_ne),
7573            ("SW", "styling.radius_sw", state.styling.corner_sw),
7574            ("SE", "styling.radius_se", state.styling.corner_se),
7575        ],
7576        0.0..28.0,
7577    );
7578    style_shadow_group(ui, controls, state);
7579    style_color_button_row(
7580        ui,
7581        controls,
7582        "styling.fill_color_button",
7583        "Fill",
7584        state.styling.fill_color(),
7585        "Pick fill color",
7586    );
7587    if state.styling_fill_picker_open {
7588        ext_widgets::color_picker(
7589            ui,
7590            controls,
7591            "styling.fill_picker",
7592            &state.styling_fill_picker,
7593            ext_widgets::ColorPickerOptions::default()
7594                .with_label("Fill")
7595                .with_action_prefix("styling.fill_picker"),
7596        );
7597    }
7598    style_stroke_row(ui, controls, state);
7599    if state.styling_stroke_picker_open {
7600        ext_widgets::color_picker(
7601            ui,
7602            controls,
7603            "styling.stroke_picker",
7604            &state.styling_stroke_picker,
7605            ext_widgets::ColorPickerOptions::default()
7606                .with_label("Stroke color")
7607                .with_action_prefix("styling.stroke_picker"),
7608        );
7609    }
7610    widgets::separator(
7611        ui,
7612        grid,
7613        "styling.preview.separator",
7614        widgets::SeparatorOptions::vertical().with_layout(
7615            LayoutStyle::new()
7616                .with_width(1.0)
7617                .with_height_percent(1.0)
7618                .with_flex_shrink(0.0),
7619        ),
7620    );
7621
7622    let preview = ui.add_child(
7623        grid,
7624        UiNode::container(
7625            "styling.preview",
7626            LayoutStyle::column()
7627                .with_width(210.0)
7628                .with_height_percent(1.0)
7629                .with_flex_shrink(0.0)
7630                .padding(8.0),
7631        )
7632        .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
7633    );
7634    style_preview(ui, preview, state.styling);
7635}
7636
7637#[allow(clippy::too_many_arguments)]
7638fn style_edge_group(
7639    ui: &mut UiDocument,
7640    parent: UiNodeId,
7641    name: &'static str,
7642    title: &'static str,
7643    same_action: &'static str,
7644    same: bool,
7645    values: [(&'static str, &'static str, f32); 4],
7646    range: std::ops::Range<f32>,
7647) {
7648    let group = style_control_group(ui, parent, format!("{name}.group"));
7649    style_group_title(ui, group, format!("{name}.title"), title);
7650    let fields = ui.add_child(
7651        group,
7652        UiNode::container(
7653            format!("{name}.fields"),
7654            LayoutStyle::column()
7655                .with_width(138.0)
7656                .with_flex_shrink(0.0)
7657                .gap(3.0),
7658        ),
7659    );
7660    style_compact_checkbox(ui, fields, same_action, "same", same);
7661    if same {
7662        style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
7663    } else {
7664        for (label, action, value) in values {
7665            style_number_row(ui, fields, action, label, value, range.clone(), 0);
7666        }
7667    }
7668}
7669
7670fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7671    let group = style_control_group(ui, parent, "styling.shadow.group");
7672    style_group_title(ui, group, "styling.shadow.title", "Shadow");
7673    let fields = ui.add_child(
7674        group,
7675        UiNode::container(
7676            "styling.shadow.fields",
7677            LayoutStyle::column()
7678                .with_width(174.0)
7679                .with_flex_shrink(0.0)
7680                .gap(4.0),
7681        ),
7682    );
7683    let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
7684    style_inline_number(
7685        ui,
7686        offsets,
7687        "styling.shadow_x",
7688        "x",
7689        state.styling.shadow_x,
7690        -24.0..24.0,
7691        0,
7692    );
7693    style_inline_number(
7694        ui,
7695        offsets,
7696        "styling.shadow_y",
7697        "y",
7698        state.styling.shadow_y,
7699        -24.0..24.0,
7700        0,
7701    );
7702    let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
7703    style_inline_number(
7704        ui,
7705        spread,
7706        "styling.shadow",
7707        "blur",
7708        state.styling.shadow_blur,
7709        0.0..32.0,
7710        0,
7711    );
7712    style_inline_number(
7713        ui,
7714        spread,
7715        "styling.shadow_spread",
7716        "spread",
7717        state.styling.shadow_spread,
7718        0.0..16.0,
7719        0,
7720    );
7721    style_color_button_row(
7722        ui,
7723        fields,
7724        "styling.shadow_color_button",
7725        "",
7726        state.styling.shadow_color(),
7727        "Pick shadow color",
7728    );
7729    if state.styling_shadow_picker_open {
7730        ext_widgets::color_picker(
7731            ui,
7732            fields,
7733            "styling.shadow_picker",
7734            &state.styling_shadow_picker,
7735            ext_widgets::ColorPickerOptions::default()
7736                .with_label("Shadow color")
7737                .with_action_prefix("styling.shadow_picker"),
7738        );
7739    }
7740}
7741
7742fn style_stroke_row(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7743    let row = row(ui, parent, "styling.stroke.row", 8.0);
7744    widgets::label(
7745        ui,
7746        row,
7747        "styling.stroke.label",
7748        "Stroke",
7749        text(12.0, color(166, 176, 190)),
7750        LayoutStyle::new().with_width(86.0).with_flex_shrink(0.0),
7751    );
7752    style_value_input(
7753        ui,
7754        row,
7755        "styling.stroke",
7756        state.styling.stroke_width,
7757        0.0..4.0,
7758        1,
7759    );
7760    ext_widgets::color_edit_button(
7761        ui,
7762        row,
7763        "styling.stroke_color_button",
7764        state.styling.stroke_color(),
7765        color_mini_button_options("styling.stroke_color_button")
7766            .with_format(ext_widgets::ColorValueFormat::Rgba)
7767            .accessibility_label("Pick stroke color"),
7768    );
7769    let mut options = widgets::SliderOptions::default()
7770        .with_layout(
7771            LayoutStyle::new()
7772                .with_width(60.0)
7773                .with_height(20.0)
7774                .with_flex_shrink(0.0),
7775        )
7776        .with_value_edit_action("styling.stroke");
7777    options.fill_color = color(120, 170, 230);
7778    widgets::slider(
7779        ui,
7780        row,
7781        "styling.stroke.slider",
7782        (state.styling.stroke_width / 4.0).clamp(0.0, 1.0),
7783        0.0..1.0,
7784        options,
7785    );
7786}
7787
7788fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7789    ui.add_child(
7790        parent,
7791        UiNode::container(
7792            name,
7793            LayoutStyle::row()
7794                .with_width_percent(1.0)
7795                .with_flex_shrink(0.0)
7796                .padding(4.0)
7797                .gap(8.0),
7798        )
7799        .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
7800    )
7801}
7802
7803fn style_group_title(
7804    ui: &mut UiDocument,
7805    parent: UiNodeId,
7806    name: impl Into<String>,
7807    label: &'static str,
7808) {
7809    widgets::label(
7810        ui,
7811        parent,
7812        name,
7813        label,
7814        text(12.0, color(166, 176, 190)),
7815        LayoutStyle::new()
7816            .with_width(88.0)
7817            .with_flex_shrink(0.0)
7818            .with_height(22.0),
7819    );
7820}
7821
7822fn style_color_button_row(
7823    ui: &mut UiDocument,
7824    parent: UiNodeId,
7825    action: &'static str,
7826    label: &'static str,
7827    value: ColorRgba,
7828    accessibility_label: &'static str,
7829) {
7830    let row = row(ui, parent, format!("{action}.row"), 8.0);
7831    if !label.is_empty() {
7832        widgets::label(
7833            ui,
7834            row,
7835            format!("{action}.label"),
7836            label,
7837            text(12.0, color(166, 176, 190)),
7838            LayoutStyle::new()
7839                .with_width(86.0)
7840                .with_flex_shrink(0.0)
7841                .with_height(24.0),
7842        );
7843    }
7844    ext_widgets::color_edit_button(
7845        ui,
7846        row,
7847        action,
7848        value,
7849        color_mini_button_options(action)
7850            .with_format(ext_widgets::ColorValueFormat::Rgba)
7851            .accessibility_label(accessibility_label),
7852    );
7853    widgets::label(
7854        ui,
7855        row,
7856        format!("{action}.value"),
7857        ext_widgets::color_picker::format_hex_color(value, value.a < 255),
7858        text(12.0, color(226, 232, 242)),
7859        LayoutStyle::new().with_width(96.0).with_height(24.0),
7860    );
7861}
7862
7863fn style_number_row(
7864    ui: &mut UiDocument,
7865    parent: UiNodeId,
7866    name: &'static str,
7867    label: &'static str,
7868    value: f32,
7869    range: std::ops::Range<f32>,
7870    decimals: u8,
7871) {
7872    let row = row(ui, parent, format!("{name}.row"), 6.0);
7873    widgets::label(
7874        ui,
7875        row,
7876        format!("{name}.label"),
7877        label,
7878        text(12.0, color(166, 176, 190)),
7879        LayoutStyle::new().with_width(48.0).with_height(22.0),
7880    );
7881    style_value_input(ui, row, name, value, range, decimals);
7882}
7883
7884fn style_inline_number(
7885    ui: &mut UiDocument,
7886    parent: UiNodeId,
7887    name: &'static str,
7888    label: &'static str,
7889    value: f32,
7890    range: std::ops::Range<f32>,
7891    decimals: u8,
7892) {
7893    let row = row(ui, parent, format!("{name}.inline"), 3.0);
7894    widgets::label(
7895        ui,
7896        row,
7897        format!("{name}.inline_label"),
7898        format!("{label}:"),
7899        text(12.0, color(166, 176, 190)),
7900        LayoutStyle::new()
7901            .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
7902            .with_height(22.0),
7903    );
7904    style_value_input(ui, row, name, value, range, decimals);
7905}
7906
7907fn style_value_input(
7908    ui: &mut UiDocument,
7909    parent: UiNodeId,
7910    name: &'static str,
7911    value: f32,
7912    range: std::ops::Range<f32>,
7913    decimals: u8,
7914) {
7915    let mut options = widgets::DragValueOptions::default()
7916        .with_layout(
7917            LayoutStyle::new()
7918                .with_width(42.0)
7919                .with_height(22.0)
7920                .with_flex_shrink(0.0),
7921        )
7922        .with_range(ext_widgets::NumericRange::new(
7923            f64::from(range.start),
7924            f64::from(range.end),
7925        ))
7926        .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
7927        .with_action(name);
7928    options.text_style = text(12.0, color(226, 232, 242));
7929    widgets::drag_value_input(ui, parent, name, f64::from(value), options);
7930}
7931
7932fn style_compact_checkbox(
7933    ui: &mut UiDocument,
7934    parent: UiNodeId,
7935    name: &'static str,
7936    label: &'static str,
7937    checked: bool,
7938) {
7939    let mut options = widgets::CheckboxOptions::default().with_action(name);
7940    options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
7941    options.text_style = text(12.0, color(220, 228, 238));
7942    widgets::checkbox(ui, parent, name, label, checked, options);
7943}
7944
7945fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
7946    ext_widgets::ColorButtonOptions::default()
7947        .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
7948        .with_swatch_size(UiSize::new(22.0, 18.0))
7949        .with_action(action)
7950        .show_label(false)
7951}
7952
7953fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
7954    let outer = styling.outer_edges();
7955    let inner = styling.inner_edges();
7956    let frame = UiRect::new(
7957        22.0 + outer[0],
7958        28.0 + outer[2],
7959        108.0 + inner[0] + inner[1],
7960        40.0 + inner[2] + inner[3],
7961    );
7962    let text_rect = UiRect::new(
7963        frame.x + inner[0],
7964        frame.y + inner[2],
7965        (frame.width - inner[0] - inner[1]).max(1.0),
7966        (frame.height - inner[2] - inner[3]).max(1.0),
7967    );
7968    ui.add_child(
7969        parent,
7970        UiNode::scene(
7971            "styling.preview.scene",
7972            vec![
7973                ScenePrimitive::Rect(
7974                    PaintRect::solid(frame, styling.fill_color())
7975                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7976                            styling.stroke_color(),
7977                            styling.stroke_width,
7978                        )))
7979                        .corner_radii(styling.radii())
7980                        .effect(PaintEffect::shadow(
7981                            styling.shadow_color(),
7982                            UiPoint::new(styling.shadow_x, styling.shadow_y),
7983                            styling.shadow_blur,
7984                            styling.shadow_spread,
7985                        )),
7986                ),
7987                ScenePrimitive::Text(
7988                    PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
7989                        .horizontal_align(TextHorizontalAlign::Center)
7990                        .vertical_align(TextVerticalAlign::Center)
7991                        .multiline(false),
7992                ),
7993            ],
7994            LayoutStyle::new()
7995                .with_width_percent(1.0)
7996                .with_height(180.0)
7997                .with_flex_shrink(0.0),
7998        ),
7999    );
8000}
8001
8002fn slider_options(state: &ShowcaseState, width: f32) -> widgets::SliderOptions {
8003    let mut options = widgets::SliderOptions::default().with_layout(
8004        LayoutStyle::new()
8005            .with_width(width)
8006            .with_height(24.0)
8007            .with_flex_shrink(0.0),
8008    );
8009    options.fill_color = if state.slider_trailing_color {
8010        state.slider_trailing_picker.value()
8011    } else {
8012        color(42, 49, 58)
8013    };
8014    options.thumb_shape = match state.slider_thumb_shape {
8015        SliderThumbChoice::Circle => widgets::slider::SliderThumbShape::Circle,
8016        SliderThumbChoice::Square => widgets::slider::SliderThumbShape::Square,
8017        SliderThumbChoice::Rectangle => widgets::slider::SliderThumbShape::Rectangle,
8018    };
8019    options
8020}
8021
8022#[allow(clippy::field_reassign_with_default)]
8023fn slider_number_input(
8024    ui: &mut UiDocument,
8025    parent: UiNodeId,
8026    name: &'static str,
8027    input: &TextInputState,
8028    focused: FocusedTextInput,
8029    state: &ShowcaseState,
8030    width: f32,
8031) {
8032    let mut options = TextInputOptions::default();
8033    options.layout = LayoutStyle::new().with_width(width).with_height(28.0);
8034    options.text_style = text(12.0, color(230, 236, 246));
8035    options.placeholder_style = text(12.0, color(144, 156, 174));
8036    options.edit_action = Some(format!("{name}.edit").into());
8037    options.focused = state.focused_text == Some(focused);
8038    options.caret_visible = caret_visible(state.caret_phase);
8039    widgets::text_input(ui, parent, name, input, options);
8040}
8041
8042fn form_status_chip(
8043    ui: &mut UiDocument,
8044    parent: UiNodeId,
8045    name: &'static str,
8046    label: &'static str,
8047    active: bool,
8048) {
8049    let chip = ui.add_child(
8050        parent,
8051        UiNode::container(
8052            name,
8053            LayoutStyle::new()
8054                .with_width(82.0)
8055                .with_height(24.0)
8056                .with_padding(4.0)
8057                .with_flex_shrink(0.0),
8058        )
8059        .with_visual(UiVisual::panel(
8060            if active {
8061                color(35, 74, 54)
8062            } else {
8063                color(28, 34, 43)
8064            },
8065            Some(StrokeStyle::new(
8066                if active {
8067                    color(90, 160, 112)
8068                } else {
8069                    color(60, 72, 88)
8070                },
8071                1.0,
8072            )),
8073            4.0,
8074        )),
8075    );
8076    widgets::label(
8077        ui,
8078        chip,
8079        format!("{name}.label"),
8080        label,
8081        text(11.0, color(218, 228, 240)),
8082        LayoutStyle::new()
8083            .with_width_percent(1.0)
8084            .with_height_percent(1.0),
8085    );
8086}
8087
8088#[allow(clippy::field_reassign_with_default)]
8089fn form_text_field(
8090    ui: &mut UiDocument,
8091    parent: UiNodeId,
8092    name: &'static str,
8093    input: &TextInputState,
8094    focused: FocusedTextInput,
8095    state: &ShowcaseState,
8096) {
8097    let mut options = TextInputOptions::default();
8098    options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
8099    options.text_style = text(12.0, color(230, 236, 246));
8100    options.placeholder_style = text(12.0, color(144, 156, 174));
8101    options.placeholder = "Required".to_string();
8102    options.edit_action = Some(format!("{name}.edit").into());
8103    options.focused = state.focused_text == Some(focused);
8104    options.caret_visible = caret_visible(state.caret_phase);
8105    widgets::text_input(ui, parent, name, input, options);
8106}
8107
8108fn profile_email_valid(email: &str) -> bool {
8109    let email = email.trim();
8110    let Some((local, domain)) = email.split_once('@') else {
8111        return false;
8112    };
8113    !local.is_empty() && domain.contains('.') && !domain.ends_with('.')
8114}
8115
8116fn drag_source_layout() -> LayoutStyle {
8117    LayoutStyle::row()
8118        .with_width(128.0)
8119        .with_height(40.0)
8120        .with_padding(8.0)
8121        .with_gap(6.0)
8122        .with_flex_shrink(0.0)
8123}
8124
8125fn drop_zone_layout() -> LayoutStyle {
8126    LayoutStyle::column()
8127        .with_width(128.0)
8128        .with_height(78.0)
8129        .with_padding(10.0)
8130        .with_gap(6.0)
8131        .with_flex_shrink(0.0)
8132}
8133
8134fn dnd_operation_chip(
8135    ui: &mut UiDocument,
8136    parent: UiNodeId,
8137    name: &'static str,
8138    label: &'static str,
8139) {
8140    let chip = ui.add_child(
8141        parent,
8142        UiNode::container(
8143            name,
8144            LayoutStyle::new()
8145                .with_width(58.0)
8146                .with_height(22.0)
8147                .with_padding(3.0)
8148                .with_flex_shrink(0.0),
8149        )
8150        .with_visual(UiVisual::panel(
8151            color(26, 32, 42),
8152            Some(StrokeStyle::new(color(62, 76, 94), 1.0)),
8153            3.0,
8154        )),
8155    );
8156    widgets::label(
8157        ui,
8158        chip,
8159        format!("{name}.label"),
8160        label,
8161        text(11.0, color(190, 204, 222)),
8162        LayoutStyle::new()
8163            .with_width_percent(1.0)
8164            .with_height_percent(1.0),
8165    );
8166}
8167
8168fn media_preview_image_layout() -> LayoutStyle {
8169    LayoutStyle::size(46.0, 46.0).with_flex_shrink(0.0)
8170}
8171
8172fn media_icon_tile(ui: &mut UiDocument, parent: UiNodeId, icon: BuiltInIcon) {
8173    let name = icon.key().replace('.', "_").replace('-', "_");
8174    let tile = ui.add_child(
8175        parent,
8176        UiNode::container(
8177            format!("media.icon_tile.{name}"),
8178            LayoutStyle::column()
8179                .with_width(70.0)
8180                .with_height(78.0)
8181                .with_padding(6.0)
8182                .with_gap(4.0)
8183                .with_flex_shrink(0.0),
8184        )
8185        .with_visual(UiVisual::panel(
8186            color(17, 22, 30),
8187            Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
8188            4.0,
8189        )),
8190    );
8191    widgets::image(
8192        ui,
8193        tile,
8194        format!("media.icon.{name}"),
8195        icon_image(icon),
8196        widgets::ImageOptions::default()
8197            .with_layout(LayoutStyle::size(28.0, 28.0))
8198            .with_accessibility_label(icon.label()),
8199    );
8200    widgets::label(
8201        ui,
8202        tile,
8203        format!("media.icon_label.{name}"),
8204        icon.label(),
8205        text(9.0, color(180, 194, 214)),
8206        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
8207    );
8208}
8209
8210fn slider_checkbox(
8211    ui: &mut UiDocument,
8212    parent: UiNodeId,
8213    name: &'static str,
8214    label: &'static str,
8215    checked: bool,
8216) {
8217    slider_checkbox_with_layout(
8218        ui,
8219        parent,
8220        name,
8221        label,
8222        checked,
8223        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
8224    );
8225}
8226
8227fn slider_checkbox_with_layout(
8228    ui: &mut UiDocument,
8229    parent: UiNodeId,
8230    name: &'static str,
8231    label: &'static str,
8232    checked: bool,
8233    layout: LayoutStyle,
8234) {
8235    let mut options = widgets::CheckboxOptions::default().with_action(name);
8236    options.layout = layout;
8237    options.text_style = text(12.0, color(220, 228, 238));
8238    widgets::checkbox(ui, parent, name, label, checked, options);
8239}
8240
8241fn choice_button(
8242    ui: &mut UiDocument,
8243    parent: UiNodeId,
8244    name: &'static str,
8245    label: &'static str,
8246    selected: bool,
8247) {
8248    let mut options =
8249        widgets::ButtonOptions::new(LayoutStyle::new().with_width(78.0).with_height(28.0))
8250            .with_action(name);
8251    options.visual = if selected {
8252        button_visual(48, 112, 184)
8253    } else {
8254        button_visual(38, 46, 58)
8255    };
8256    options.hovered_visual = Some(button_visual(65, 86, 106));
8257    options.pressed_visual = Some(button_visual(34, 54, 84));
8258    options.text_style = text(12.0, color(238, 244, 252));
8259    widgets::button(ui, parent, name, label, options);
8260}
8261
8262fn divider(ui: &mut UiDocument, parent: UiNodeId, name: &'static str) {
8263    ui.add_child(
8264        parent,
8265        UiNode::container(
8266            name,
8267            LayoutStyle::new()
8268                .with_width_percent(1.0)
8269                .with_height(1.0)
8270                .with_flex_shrink(0.0),
8271        )
8272        .with_visual(UiVisual::panel(color(48, 58, 72), None, 0.0)),
8273    );
8274}
8275
8276fn canvas(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8277    let body = section(ui, parent, "canvas", "Canvas");
8278    let mut options = widgets::CanvasOptions::default()
8279        .with_accessibility_label("Shader canvas")
8280        .with_action("canvas.rotate")
8281        .with_aspect_ratio(16.0 / 9.0);
8282    options.layout = LayoutStyle::new()
8283        .with_width_percent(1.0)
8284        .with_height_percent(1.0)
8285        .with_flex_grow(1.0)
8286        .with_flex_shrink(1.0);
8287    options.visual = UiVisual::panel(
8288        color(18, 22, 28),
8289        Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
8290        4.0,
8291    );
8292    widgets::canvas(
8293        ui,
8294        body,
8295        "canvas.shader",
8296        CanvasContent::new("canvas.shader").program(showcase_canvas_program(state.cube)),
8297        options,
8298    );
8299}
8300
8301fn showcase_canvas_program(cube: CanvasCubeState) -> CanvasRenderProgram {
8302    CanvasRenderProgram::wgsl(include_str!("shaders/showcase_canvas.wgsl"))
8303        .label("showcase.canvas")
8304        .constant("CUBE_YAW", cube.yaw as f64)
8305        .constant("CUBE_PITCH", cube.pitch as f64)
8306        .clear_color(Some(color(18, 22, 28)))
8307}
8308
8309fn section(
8310    ui: &mut UiDocument,
8311    parent: UiNodeId,
8312    name: impl Into<String>,
8313    _title: impl Into<String>,
8314) -> UiNodeId {
8315    section_with_min_viewport(ui, parent, name, _title, UiSize::ZERO)
8316}
8317
8318fn section_with_min_viewport(
8319    ui: &mut UiDocument,
8320    parent: UiNodeId,
8321    name: impl Into<String>,
8322    _title: impl Into<String>,
8323    min_viewport_size: UiSize,
8324) -> UiNodeId {
8325    let name = name.into();
8326    let layout = Layout::column()
8327        .size(LayoutSize::percent(1.0, 1.0))
8328        .min_size(LayoutSize::points(
8329            min_viewport_size.width.max(0.0),
8330            min_viewport_size.height.max(0.0),
8331        ))
8332        .gap(LayoutGap::points(10.0, 10.0))
8333        .flex(1.0, 1.0, LayoutDimension::Auto)
8334        .to_layout_style();
8335    widgets::scroll_area(
8336        ui,
8337        parent,
8338        format!("{name}.section_scroll"),
8339        ScrollAxes::BOTH,
8340        layout,
8341    )
8342}
8343
8344fn row(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>, gap: f32) -> UiNodeId {
8345    ui.add_child(
8346        parent,
8347        UiNode::container(
8348            name,
8349            Layout::row()
8350                .size(LayoutSize::new(
8351                    LayoutDimension::percent(1.0),
8352                    LayoutDimension::Auto,
8353                ))
8354                .gap(LayoutGap::points(gap, gap))
8355                .to_layout_style(),
8356        ),
8357    )
8358}
8359
8360fn wrapping_row(
8361    ui: &mut UiDocument,
8362    parent: UiNodeId,
8363    name: impl Into<String>,
8364    gap: f32,
8365) -> UiNodeId {
8366    ui.add_child(
8367        parent,
8368        UiNode::container(
8369            name,
8370            Layout::row()
8371                .size(LayoutSize::new(
8372                    LayoutDimension::percent(1.0),
8373                    LayoutDimension::Auto,
8374                ))
8375                .gap(LayoutGap::points(gap, gap))
8376                .flex_wrap(LayoutFlexWrap::Wrap)
8377                .to_layout_style(),
8378        ),
8379    )
8380}
Source

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

Examples found in repository?
examples/showcase.rs (lines 8328-8331)
8318fn section_with_min_viewport(
8319    ui: &mut UiDocument,
8320    parent: UiNodeId,
8321    name: impl Into<String>,
8322    _title: impl Into<String>,
8323    min_viewport_size: UiSize,
8324) -> UiNodeId {
8325    let name = name.into();
8326    let layout = Layout::column()
8327        .size(LayoutSize::percent(1.0, 1.0))
8328        .min_size(LayoutSize::points(
8329            min_viewport_size.width.max(0.0),
8330            min_viewport_size.height.max(0.0),
8331        ))
8332        .gap(LayoutGap::points(10.0, 10.0))
8333        .flex(1.0, 1.0, LayoutDimension::Auto)
8334        .to_layout_style();
8335    widgets::scroll_area(
8336        ui,
8337        parent,
8338        format!("{name}.section_scroll"),
8339        ScrollAxes::BOTH,
8340        layout,
8341    )
8342}
Source

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

Examples found in repository?
examples/showcase.rs (line 7512)
7508fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7509    let body = section(ui, parent, "styling", "Styling");
7510    let grid_layout = operad::layout::with_grid_template_columns(
7511        Layout::grid()
7512            .size(LayoutSize::percent(1.0, 1.0))
7513            .gap(LayoutGap::points(10.0, 10.0))
7514            .to_layout_style(),
7515        [
7516            LayoutGridTrack::points(300.0),
7517            LayoutGridTrack::points(1.0),
7518            LayoutGridTrack::points(210.0),
7519        ],
7520    );
7521    let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
7522    let controls = ui.add_child(
7523        grid,
7524        UiNode::container(
7525            "styling.controls",
7526            LayoutStyle::column()
7527                .with_width(300.0)
7528                .with_height_percent(1.0)
7529                .with_flex_shrink(0.0)
7530                .gap(6.0),
7531        ),
7532    );
7533    style_edge_group(
7534        ui,
7535        controls,
7536        "styling.inner",
7537        "Inner margin",
7538        "styling.inner_same",
7539        state.styling.inner_same,
7540        [
7541            ("Left", "styling.inner", state.styling.inner_margin),
7542            ("Right", "styling.inner_right", state.styling.inner_right),
7543            ("Top", "styling.inner_top", state.styling.inner_top),
7544            ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
7545        ],
7546        0.0..32.0,
7547    );
7548    style_edge_group(
7549        ui,
7550        controls,
7551        "styling.outer",
7552        "Outer margin",
7553        "styling.outer_same",
7554        state.styling.outer_same,
7555        [
7556            ("Left", "styling.outer", state.styling.outer_margin),
7557            ("Right", "styling.outer_right", state.styling.outer_right),
7558            ("Top", "styling.outer_top", state.styling.outer_top),
7559            ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
7560        ],
7561        0.0..40.0,
7562    );
7563    style_edge_group(
7564        ui,
7565        controls,
7566        "styling.radius",
7567        "Corner radius",
7568        "styling.radius_same",
7569        state.styling.radius_same,
7570        [
7571            ("NW", "styling.radius", state.styling.corner_radius),
7572            ("NE", "styling.radius_ne", state.styling.corner_ne),
7573            ("SW", "styling.radius_sw", state.styling.corner_sw),
7574            ("SE", "styling.radius_se", state.styling.corner_se),
7575        ],
7576        0.0..28.0,
7577    );
7578    style_shadow_group(ui, controls, state);
7579    style_color_button_row(
7580        ui,
7581        controls,
7582        "styling.fill_color_button",
7583        "Fill",
7584        state.styling.fill_color(),
7585        "Pick fill color",
7586    );
7587    if state.styling_fill_picker_open {
7588        ext_widgets::color_picker(
7589            ui,
7590            controls,
7591            "styling.fill_picker",
7592            &state.styling_fill_picker,
7593            ext_widgets::ColorPickerOptions::default()
7594                .with_label("Fill")
7595                .with_action_prefix("styling.fill_picker"),
7596        );
7597    }
7598    style_stroke_row(ui, controls, state);
7599    if state.styling_stroke_picker_open {
7600        ext_widgets::color_picker(
7601            ui,
7602            controls,
7603            "styling.stroke_picker",
7604            &state.styling_stroke_picker,
7605            ext_widgets::ColorPickerOptions::default()
7606                .with_label("Stroke color")
7607                .with_action_prefix("styling.stroke_picker"),
7608        );
7609    }
7610    widgets::separator(
7611        ui,
7612        grid,
7613        "styling.preview.separator",
7614        widgets::SeparatorOptions::vertical().with_layout(
7615            LayoutStyle::new()
7616                .with_width(1.0)
7617                .with_height_percent(1.0)
7618                .with_flex_shrink(0.0),
7619        ),
7620    );
7621
7622    let preview = ui.add_child(
7623        grid,
7624        UiNode::container(
7625            "styling.preview",
7626            LayoutStyle::column()
7627                .with_width(210.0)
7628                .with_height_percent(1.0)
7629                .with_flex_shrink(0.0)
7630                .padding(8.0),
7631        )
7632        .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
7633    );
7634    style_preview(ui, preview, state.styling);
7635}
7636
7637#[allow(clippy::too_many_arguments)]
7638fn style_edge_group(
7639    ui: &mut UiDocument,
7640    parent: UiNodeId,
7641    name: &'static str,
7642    title: &'static str,
7643    same_action: &'static str,
7644    same: bool,
7645    values: [(&'static str, &'static str, f32); 4],
7646    range: std::ops::Range<f32>,
7647) {
7648    let group = style_control_group(ui, parent, format!("{name}.group"));
7649    style_group_title(ui, group, format!("{name}.title"), title);
7650    let fields = ui.add_child(
7651        group,
7652        UiNode::container(
7653            format!("{name}.fields"),
7654            LayoutStyle::column()
7655                .with_width(138.0)
7656                .with_flex_shrink(0.0)
7657                .gap(3.0),
7658        ),
7659    );
7660    style_compact_checkbox(ui, fields, same_action, "same", same);
7661    if same {
7662        style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
7663    } else {
7664        for (label, action, value) in values {
7665            style_number_row(ui, fields, action, label, value, range.clone(), 0);
7666        }
7667    }
7668}
7669
7670fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7671    let group = style_control_group(ui, parent, "styling.shadow.group");
7672    style_group_title(ui, group, "styling.shadow.title", "Shadow");
7673    let fields = ui.add_child(
7674        group,
7675        UiNode::container(
7676            "styling.shadow.fields",
7677            LayoutStyle::column()
7678                .with_width(174.0)
7679                .with_flex_shrink(0.0)
7680                .gap(4.0),
7681        ),
7682    );
7683    let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
7684    style_inline_number(
7685        ui,
7686        offsets,
7687        "styling.shadow_x",
7688        "x",
7689        state.styling.shadow_x,
7690        -24.0..24.0,
7691        0,
7692    );
7693    style_inline_number(
7694        ui,
7695        offsets,
7696        "styling.shadow_y",
7697        "y",
7698        state.styling.shadow_y,
7699        -24.0..24.0,
7700        0,
7701    );
7702    let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
7703    style_inline_number(
7704        ui,
7705        spread,
7706        "styling.shadow",
7707        "blur",
7708        state.styling.shadow_blur,
7709        0.0..32.0,
7710        0,
7711    );
7712    style_inline_number(
7713        ui,
7714        spread,
7715        "styling.shadow_spread",
7716        "spread",
7717        state.styling.shadow_spread,
7718        0.0..16.0,
7719        0,
7720    );
7721    style_color_button_row(
7722        ui,
7723        fields,
7724        "styling.shadow_color_button",
7725        "",
7726        state.styling.shadow_color(),
7727        "Pick shadow color",
7728    );
7729    if state.styling_shadow_picker_open {
7730        ext_widgets::color_picker(
7731            ui,
7732            fields,
7733            "styling.shadow_picker",
7734            &state.styling_shadow_picker,
7735            ext_widgets::ColorPickerOptions::default()
7736                .with_label("Shadow color")
7737                .with_action_prefix("styling.shadow_picker"),
7738        );
7739    }
7740}
7741
7742fn style_stroke_row(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7743    let row = row(ui, parent, "styling.stroke.row", 8.0);
7744    widgets::label(
7745        ui,
7746        row,
7747        "styling.stroke.label",
7748        "Stroke",
7749        text(12.0, color(166, 176, 190)),
7750        LayoutStyle::new().with_width(86.0).with_flex_shrink(0.0),
7751    );
7752    style_value_input(
7753        ui,
7754        row,
7755        "styling.stroke",
7756        state.styling.stroke_width,
7757        0.0..4.0,
7758        1,
7759    );
7760    ext_widgets::color_edit_button(
7761        ui,
7762        row,
7763        "styling.stroke_color_button",
7764        state.styling.stroke_color(),
7765        color_mini_button_options("styling.stroke_color_button")
7766            .with_format(ext_widgets::ColorValueFormat::Rgba)
7767            .accessibility_label("Pick stroke color"),
7768    );
7769    let mut options = widgets::SliderOptions::default()
7770        .with_layout(
7771            LayoutStyle::new()
7772                .with_width(60.0)
7773                .with_height(20.0)
7774                .with_flex_shrink(0.0),
7775        )
7776        .with_value_edit_action("styling.stroke");
7777    options.fill_color = color(120, 170, 230);
7778    widgets::slider(
7779        ui,
7780        row,
7781        "styling.stroke.slider",
7782        (state.styling.stroke_width / 4.0).clamp(0.0, 1.0),
7783        0.0..1.0,
7784        options,
7785    );
7786}
7787
7788fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7789    ui.add_child(
7790        parent,
7791        UiNode::container(
7792            name,
7793            LayoutStyle::row()
7794                .with_width_percent(1.0)
7795                .with_flex_shrink(0.0)
7796                .padding(4.0)
7797                .gap(8.0),
7798        )
7799        .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
7800    )
7801}
7802
7803fn style_group_title(
7804    ui: &mut UiDocument,
7805    parent: UiNodeId,
7806    name: impl Into<String>,
7807    label: &'static str,
7808) {
7809    widgets::label(
7810        ui,
7811        parent,
7812        name,
7813        label,
7814        text(12.0, color(166, 176, 190)),
7815        LayoutStyle::new()
7816            .with_width(88.0)
7817            .with_flex_shrink(0.0)
7818            .with_height(22.0),
7819    );
7820}
7821
7822fn style_color_button_row(
7823    ui: &mut UiDocument,
7824    parent: UiNodeId,
7825    action: &'static str,
7826    label: &'static str,
7827    value: ColorRgba,
7828    accessibility_label: &'static str,
7829) {
7830    let row = row(ui, parent, format!("{action}.row"), 8.0);
7831    if !label.is_empty() {
7832        widgets::label(
7833            ui,
7834            row,
7835            format!("{action}.label"),
7836            label,
7837            text(12.0, color(166, 176, 190)),
7838            LayoutStyle::new()
7839                .with_width(86.0)
7840                .with_flex_shrink(0.0)
7841                .with_height(24.0),
7842        );
7843    }
7844    ext_widgets::color_edit_button(
7845        ui,
7846        row,
7847        action,
7848        value,
7849        color_mini_button_options(action)
7850            .with_format(ext_widgets::ColorValueFormat::Rgba)
7851            .accessibility_label(accessibility_label),
7852    );
7853    widgets::label(
7854        ui,
7855        row,
7856        format!("{action}.value"),
7857        ext_widgets::color_picker::format_hex_color(value, value.a < 255),
7858        text(12.0, color(226, 232, 242)),
7859        LayoutStyle::new().with_width(96.0).with_height(24.0),
7860    );
7861}
7862
7863fn style_number_row(
7864    ui: &mut UiDocument,
7865    parent: UiNodeId,
7866    name: &'static str,
7867    label: &'static str,
7868    value: f32,
7869    range: std::ops::Range<f32>,
7870    decimals: u8,
7871) {
7872    let row = row(ui, parent, format!("{name}.row"), 6.0);
7873    widgets::label(
7874        ui,
7875        row,
7876        format!("{name}.label"),
7877        label,
7878        text(12.0, color(166, 176, 190)),
7879        LayoutStyle::new().with_width(48.0).with_height(22.0),
7880    );
7881    style_value_input(ui, row, name, value, range, decimals);
7882}
7883
7884fn style_inline_number(
7885    ui: &mut UiDocument,
7886    parent: UiNodeId,
7887    name: &'static str,
7888    label: &'static str,
7889    value: f32,
7890    range: std::ops::Range<f32>,
7891    decimals: u8,
7892) {
7893    let row = row(ui, parent, format!("{name}.inline"), 3.0);
7894    widgets::label(
7895        ui,
7896        row,
7897        format!("{name}.inline_label"),
7898        format!("{label}:"),
7899        text(12.0, color(166, 176, 190)),
7900        LayoutStyle::new()
7901            .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
7902            .with_height(22.0),
7903    );
7904    style_value_input(ui, row, name, value, range, decimals);
7905}
7906
7907fn style_value_input(
7908    ui: &mut UiDocument,
7909    parent: UiNodeId,
7910    name: &'static str,
7911    value: f32,
7912    range: std::ops::Range<f32>,
7913    decimals: u8,
7914) {
7915    let mut options = widgets::DragValueOptions::default()
7916        .with_layout(
7917            LayoutStyle::new()
7918                .with_width(42.0)
7919                .with_height(22.0)
7920                .with_flex_shrink(0.0),
7921        )
7922        .with_range(ext_widgets::NumericRange::new(
7923            f64::from(range.start),
7924            f64::from(range.end),
7925        ))
7926        .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
7927        .with_action(name);
7928    options.text_style = text(12.0, color(226, 232, 242));
7929    widgets::drag_value_input(ui, parent, name, f64::from(value), options);
7930}
7931
7932fn style_compact_checkbox(
7933    ui: &mut UiDocument,
7934    parent: UiNodeId,
7935    name: &'static str,
7936    label: &'static str,
7937    checked: bool,
7938) {
7939    let mut options = widgets::CheckboxOptions::default().with_action(name);
7940    options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
7941    options.text_style = text(12.0, color(220, 228, 238));
7942    widgets::checkbox(ui, parent, name, label, checked, options);
7943}
7944
7945fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
7946    ext_widgets::ColorButtonOptions::default()
7947        .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
7948        .with_swatch_size(UiSize::new(22.0, 18.0))
7949        .with_action(action)
7950        .show_label(false)
7951}
7952
7953fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
7954    let outer = styling.outer_edges();
7955    let inner = styling.inner_edges();
7956    let frame = UiRect::new(
7957        22.0 + outer[0],
7958        28.0 + outer[2],
7959        108.0 + inner[0] + inner[1],
7960        40.0 + inner[2] + inner[3],
7961    );
7962    let text_rect = UiRect::new(
7963        frame.x + inner[0],
7964        frame.y + inner[2],
7965        (frame.width - inner[0] - inner[1]).max(1.0),
7966        (frame.height - inner[2] - inner[3]).max(1.0),
7967    );
7968    ui.add_child(
7969        parent,
7970        UiNode::scene(
7971            "styling.preview.scene",
7972            vec![
7973                ScenePrimitive::Rect(
7974                    PaintRect::solid(frame, styling.fill_color())
7975                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7976                            styling.stroke_color(),
7977                            styling.stroke_width,
7978                        )))
7979                        .corner_radii(styling.radii())
7980                        .effect(PaintEffect::shadow(
7981                            styling.shadow_color(),
7982                            UiPoint::new(styling.shadow_x, styling.shadow_y),
7983                            styling.shadow_blur,
7984                            styling.shadow_spread,
7985                        )),
7986                ),
7987                ScenePrimitive::Text(
7988                    PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
7989                        .horizontal_align(TextHorizontalAlign::Center)
7990                        .vertical_align(TextVerticalAlign::Center)
7991                        .multiline(false),
7992                ),
7993            ],
7994            LayoutStyle::new()
7995                .with_width_percent(1.0)
7996                .with_height(180.0)
7997                .with_flex_shrink(0.0),
7998        ),
7999    );
8000}
8001
8002fn slider_options(state: &ShowcaseState, width: f32) -> widgets::SliderOptions {
8003    let mut options = widgets::SliderOptions::default().with_layout(
8004        LayoutStyle::new()
8005            .with_width(width)
8006            .with_height(24.0)
8007            .with_flex_shrink(0.0),
8008    );
8009    options.fill_color = if state.slider_trailing_color {
8010        state.slider_trailing_picker.value()
8011    } else {
8012        color(42, 49, 58)
8013    };
8014    options.thumb_shape = match state.slider_thumb_shape {
8015        SliderThumbChoice::Circle => widgets::slider::SliderThumbShape::Circle,
8016        SliderThumbChoice::Square => widgets::slider::SliderThumbShape::Square,
8017        SliderThumbChoice::Rectangle => widgets::slider::SliderThumbShape::Rectangle,
8018    };
8019    options
8020}
8021
8022#[allow(clippy::field_reassign_with_default)]
8023fn slider_number_input(
8024    ui: &mut UiDocument,
8025    parent: UiNodeId,
8026    name: &'static str,
8027    input: &TextInputState,
8028    focused: FocusedTextInput,
8029    state: &ShowcaseState,
8030    width: f32,
8031) {
8032    let mut options = TextInputOptions::default();
8033    options.layout = LayoutStyle::new().with_width(width).with_height(28.0);
8034    options.text_style = text(12.0, color(230, 236, 246));
8035    options.placeholder_style = text(12.0, color(144, 156, 174));
8036    options.edit_action = Some(format!("{name}.edit").into());
8037    options.focused = state.focused_text == Some(focused);
8038    options.caret_visible = caret_visible(state.caret_phase);
8039    widgets::text_input(ui, parent, name, input, options);
8040}
8041
8042fn form_status_chip(
8043    ui: &mut UiDocument,
8044    parent: UiNodeId,
8045    name: &'static str,
8046    label: &'static str,
8047    active: bool,
8048) {
8049    let chip = ui.add_child(
8050        parent,
8051        UiNode::container(
8052            name,
8053            LayoutStyle::new()
8054                .with_width(82.0)
8055                .with_height(24.0)
8056                .with_padding(4.0)
8057                .with_flex_shrink(0.0),
8058        )
8059        .with_visual(UiVisual::panel(
8060            if active {
8061                color(35, 74, 54)
8062            } else {
8063                color(28, 34, 43)
8064            },
8065            Some(StrokeStyle::new(
8066                if active {
8067                    color(90, 160, 112)
8068                } else {
8069                    color(60, 72, 88)
8070                },
8071                1.0,
8072            )),
8073            4.0,
8074        )),
8075    );
8076    widgets::label(
8077        ui,
8078        chip,
8079        format!("{name}.label"),
8080        label,
8081        text(11.0, color(218, 228, 240)),
8082        LayoutStyle::new()
8083            .with_width_percent(1.0)
8084            .with_height_percent(1.0),
8085    );
8086}
8087
8088#[allow(clippy::field_reassign_with_default)]
8089fn form_text_field(
8090    ui: &mut UiDocument,
8091    parent: UiNodeId,
8092    name: &'static str,
8093    input: &TextInputState,
8094    focused: FocusedTextInput,
8095    state: &ShowcaseState,
8096) {
8097    let mut options = TextInputOptions::default();
8098    options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
8099    options.text_style = text(12.0, color(230, 236, 246));
8100    options.placeholder_style = text(12.0, color(144, 156, 174));
8101    options.placeholder = "Required".to_string();
8102    options.edit_action = Some(format!("{name}.edit").into());
8103    options.focused = state.focused_text == Some(focused);
8104    options.caret_visible = caret_visible(state.caret_phase);
8105    widgets::text_input(ui, parent, name, input, options);
8106}
8107
8108fn profile_email_valid(email: &str) -> bool {
8109    let email = email.trim();
8110    let Some((local, domain)) = email.split_once('@') else {
8111        return false;
8112    };
8113    !local.is_empty() && domain.contains('.') && !domain.ends_with('.')
8114}
8115
8116fn drag_source_layout() -> LayoutStyle {
8117    LayoutStyle::row()
8118        .with_width(128.0)
8119        .with_height(40.0)
8120        .with_padding(8.0)
8121        .with_gap(6.0)
8122        .with_flex_shrink(0.0)
8123}
8124
8125fn drop_zone_layout() -> LayoutStyle {
8126    LayoutStyle::column()
8127        .with_width(128.0)
8128        .with_height(78.0)
8129        .with_padding(10.0)
8130        .with_gap(6.0)
8131        .with_flex_shrink(0.0)
8132}
8133
8134fn dnd_operation_chip(
8135    ui: &mut UiDocument,
8136    parent: UiNodeId,
8137    name: &'static str,
8138    label: &'static str,
8139) {
8140    let chip = ui.add_child(
8141        parent,
8142        UiNode::container(
8143            name,
8144            LayoutStyle::new()
8145                .with_width(58.0)
8146                .with_height(22.0)
8147                .with_padding(3.0)
8148                .with_flex_shrink(0.0),
8149        )
8150        .with_visual(UiVisual::panel(
8151            color(26, 32, 42),
8152            Some(StrokeStyle::new(color(62, 76, 94), 1.0)),
8153            3.0,
8154        )),
8155    );
8156    widgets::label(
8157        ui,
8158        chip,
8159        format!("{name}.label"),
8160        label,
8161        text(11.0, color(190, 204, 222)),
8162        LayoutStyle::new()
8163            .with_width_percent(1.0)
8164            .with_height_percent(1.0),
8165    );
8166}
8167
8168fn media_preview_image_layout() -> LayoutStyle {
8169    LayoutStyle::size(46.0, 46.0).with_flex_shrink(0.0)
8170}
8171
8172fn media_icon_tile(ui: &mut UiDocument, parent: UiNodeId, icon: BuiltInIcon) {
8173    let name = icon.key().replace('.', "_").replace('-', "_");
8174    let tile = ui.add_child(
8175        parent,
8176        UiNode::container(
8177            format!("media.icon_tile.{name}"),
8178            LayoutStyle::column()
8179                .with_width(70.0)
8180                .with_height(78.0)
8181                .with_padding(6.0)
8182                .with_gap(4.0)
8183                .with_flex_shrink(0.0),
8184        )
8185        .with_visual(UiVisual::panel(
8186            color(17, 22, 30),
8187            Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
8188            4.0,
8189        )),
8190    );
8191    widgets::image(
8192        ui,
8193        tile,
8194        format!("media.icon.{name}"),
8195        icon_image(icon),
8196        widgets::ImageOptions::default()
8197            .with_layout(LayoutStyle::size(28.0, 28.0))
8198            .with_accessibility_label(icon.label()),
8199    );
8200    widgets::label(
8201        ui,
8202        tile,
8203        format!("media.icon_label.{name}"),
8204        icon.label(),
8205        text(9.0, color(180, 194, 214)),
8206        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
8207    );
8208}
8209
8210fn slider_checkbox(
8211    ui: &mut UiDocument,
8212    parent: UiNodeId,
8213    name: &'static str,
8214    label: &'static str,
8215    checked: bool,
8216) {
8217    slider_checkbox_with_layout(
8218        ui,
8219        parent,
8220        name,
8221        label,
8222        checked,
8223        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
8224    );
8225}
8226
8227fn slider_checkbox_with_layout(
8228    ui: &mut UiDocument,
8229    parent: UiNodeId,
8230    name: &'static str,
8231    label: &'static str,
8232    checked: bool,
8233    layout: LayoutStyle,
8234) {
8235    let mut options = widgets::CheckboxOptions::default().with_action(name);
8236    options.layout = layout;
8237    options.text_style = text(12.0, color(220, 228, 238));
8238    widgets::checkbox(ui, parent, name, label, checked, options);
8239}
8240
8241fn choice_button(
8242    ui: &mut UiDocument,
8243    parent: UiNodeId,
8244    name: &'static str,
8245    label: &'static str,
8246    selected: bool,
8247) {
8248    let mut options =
8249        widgets::ButtonOptions::new(LayoutStyle::new().with_width(78.0).with_height(28.0))
8250            .with_action(name);
8251    options.visual = if selected {
8252        button_visual(48, 112, 184)
8253    } else {
8254        button_visual(38, 46, 58)
8255    };
8256    options.hovered_visual = Some(button_visual(65, 86, 106));
8257    options.pressed_visual = Some(button_visual(34, 54, 84));
8258    options.text_style = text(12.0, color(238, 244, 252));
8259    widgets::button(ui, parent, name, label, options);
8260}
8261
8262fn divider(ui: &mut UiDocument, parent: UiNodeId, name: &'static str) {
8263    ui.add_child(
8264        parent,
8265        UiNode::container(
8266            name,
8267            LayoutStyle::new()
8268                .with_width_percent(1.0)
8269                .with_height(1.0)
8270                .with_flex_shrink(0.0),
8271        )
8272        .with_visual(UiVisual::panel(color(48, 58, 72), None, 0.0)),
8273    );
8274}
8275
8276fn canvas(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8277    let body = section(ui, parent, "canvas", "Canvas");
8278    let mut options = widgets::CanvasOptions::default()
8279        .with_accessibility_label("Shader canvas")
8280        .with_action("canvas.rotate")
8281        .with_aspect_ratio(16.0 / 9.0);
8282    options.layout = LayoutStyle::new()
8283        .with_width_percent(1.0)
8284        .with_height_percent(1.0)
8285        .with_flex_grow(1.0)
8286        .with_flex_shrink(1.0);
8287    options.visual = UiVisual::panel(
8288        color(18, 22, 28),
8289        Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
8290        4.0,
8291    );
8292    widgets::canvas(
8293        ui,
8294        body,
8295        "canvas.shader",
8296        CanvasContent::new("canvas.shader").program(showcase_canvas_program(state.cube)),
8297        options,
8298    );
8299}
8300
8301fn showcase_canvas_program(cube: CanvasCubeState) -> CanvasRenderProgram {
8302    CanvasRenderProgram::wgsl(include_str!("shaders/showcase_canvas.wgsl"))
8303        .label("showcase.canvas")
8304        .constant("CUBE_YAW", cube.yaw as f64)
8305        .constant("CUBE_PITCH", cube.pitch as f64)
8306        .clear_color(Some(color(18, 22, 28)))
8307}
8308
8309fn section(
8310    ui: &mut UiDocument,
8311    parent: UiNodeId,
8312    name: impl Into<String>,
8313    _title: impl Into<String>,
8314) -> UiNodeId {
8315    section_with_min_viewport(ui, parent, name, _title, UiSize::ZERO)
8316}
8317
8318fn section_with_min_viewport(
8319    ui: &mut UiDocument,
8320    parent: UiNodeId,
8321    name: impl Into<String>,
8322    _title: impl Into<String>,
8323    min_viewport_size: UiSize,
8324) -> UiNodeId {
8325    let name = name.into();
8326    let layout = Layout::column()
8327        .size(LayoutSize::percent(1.0, 1.0))
8328        .min_size(LayoutSize::points(
8329            min_viewport_size.width.max(0.0),
8330            min_viewport_size.height.max(0.0),
8331        ))
8332        .gap(LayoutGap::points(10.0, 10.0))
8333        .flex(1.0, 1.0, LayoutDimension::Auto)
8334        .to_layout_style();
8335    widgets::scroll_area(
8336        ui,
8337        parent,
8338        format!("{name}.section_scroll"),
8339        ScrollAxes::BOTH,
8340        layout,
8341    )
8342}
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 + Send + Sync>

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

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

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

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

fn in_current_span(self) -> Instrumented<Self>

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

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

Source§

fn into(self) -> U

Calls U::from(self).

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

Source§

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

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

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

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

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

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

Source§

type Error = Infallible

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

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

Performs the conversion.
Source§

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

Source§

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

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

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

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

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

Source§

impl<T> WithSubscriber for T

Source§

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

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

fn with_current_subscriber(self) -> WithDispatch<Self>

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

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

Source§

impl<T> WasmNotSendSync for T

Source§

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