Layout and workspace

Control catalog · Layout contract · Workspace input

Examples use the language fragment contexts. Add xui\adaptive_layout.hpp for Grid, Wrap, and AdaptiveLayout.

Stack

Use Stack for a horizontal row or vertical column. A positive flex value gives a child a share of remaining main-axis space. Stacks measure their children naturally unless an explicit preferred size disables automatic sizing. set_preferred_size requests the outer size, including padding. Parent bounds and minimum and maximum sizes still apply. set_auto_size(true) restores natural measurement without clearing the stored preference or size limits. set_auto_size(false) restores an explicit preference. Without an explicit preference, the Stack keeps natural sizing. Flex and cross-axis stretching still control arrangement inside the parent's allocation.

namespace ControlExamples;
component WorkspaceRow {
    view {
        HStack(spacing: 8, padding: 12) {
            Text("Workspace");
            TextInput("Workspace name", flex: 1);
        }
    }
}
var row = window.Stack(Axis.Horizontal).Spacing(8).Padding(12);
row.Add(window.Label("Workspace"));
row.Add(window.TextInput("Workspace name"), 1);
root.Add(row);
let row = window.stack(Axis::Horizontal)?;
row.spacing(8.0)?;
row.padding(12.0)?;
row.add(&*window.label("Workspace")?, 0.0)?;
row.add(&*window.text_input("Workspace name")?, 1.0)?;
root.add(&row, 0.0)?;
auto row = std::make_shared<xui::Stack>(xui::Axis::horizontal);
row->set_spacing(8);
row->set_padding({12, 12, 12, 12});
row->add(std::make_shared<xui::Label>(L"Workspace"));
row->add(std::make_shared<xui::TextInput>(L"Workspace name"), 1);
root->add(row);

set_surface requests a surface. set_material(SurfaceMaterial::acrylic) paints an in-app acrylic surface instead. C# uses Stack.Material(SurfaceMaterial.Acrylic). Rust and .xui cannot select the material. set_separator_after requests a separator after the content. The style target is stack. Explicit padding and spacing override style values, including zero. Passive layout roots reject foreground text color. Child text uses the child style.

ContentHost

ContentHost retains its layout identity while one scoped root changes inside it. It uses native XUI controls and input, not a separate preview window. Controls outside the host retain their native peers, focus, selection, and undo history.

C# creates the host with Window.CreateContentHost. The .xui shell accepts that element through a parameter and places it with Content(...). The scoped content contract contains the C# example, C ABI declarations, and ownership rules. C++ uses Window::replace_content. Rust does not yet expose a typed wrapper.

Live replacement runs in a deferred UI-thread action, outside native input callbacks. Ordinary topology changes remain restricted to initial construction. Disposing a committed update clears its content if that update remains current. This boundary does not isolate untrusted code or prevent native materialization failures.

Reveal

Reveal retains one child inside a native clip. The default fixed layout reserves full height at entry start and releases that height after exit. RevealLayout::expand changes the reserved extent on each frame so neighboring content resizes with the reveal. RevealDirection selects bottom, top, left, or right entry. The duration defaults to zero, so applications select motion explicitly. The host follows the Windows animation preference and retains native input.

The language guide contains a complete .xui example. C# uses Window.Reveal(content, name), with Open, Duration, Layout, and Direction properties. Rust uses Window::reveal(content, name). C++ uses xui::Reveal from xui/reveal.hpp. The animation contract describes reversal, focus, lifetime, and performance limits.

Grid

Use Grid for aligned rows and columns. Use DataGrid for virtual data rows, sorting, and selection. Grid retains each child and does not virtualize cells.

namespace ControlExamples;
component NameForm {
    style FormSpacing for Grid { rowGap: 8; columnGap: 12; }
    view {
        VStack() {
            Grid("Name form", style: FormSpacing,
                rows: [new(global::Xui.TrackSizing.Automatic), new(global::Xui.TrackSizing.Automatic)],
                columns: [new(global::Xui.TrackSizing.Fixed, 140), new(global::Xui.TrackSizing.Star, 1)]) {
                Text("Name", row: 0, column: 0);
                TextInput("Name", row: 0, column: 1);
                Button("Apply", row: 1, column: 0, columnSpan: 2);
            }
        }
    }
}

The binding supplies gaps through style values, not a structural SetGap setter.

var form = window.Grid("Name form");
form.SetTracks(
    [new(TrackSizing.Automatic), new(TrackSizing.Automatic)],
    [new(TrackSizing.Fixed, 140), new(TrackSizing.Star, 1)]);
form.SetControlStyleValues(StylePart.Root, new PartStyleValues { ColumnGap = 12, RowGap = 8 });
form.Add(window.Label("Name"), 0, 0);
form.Add(window.TextInput("Name"), 0, 1);
form.Add(window.Button("Apply"), 1, 0, 1, 2);
root.Add(form);

The binding supplies gaps through style values, not a structural gap setter.

let form = window.grid("Name form")?;
form.set_tracks(
    &[GridTrack { sizing: TrackSizing::Automatic, ..Default::default() }; 2],
    &[
        GridTrack { sizing: TrackSizing::Fixed, value: 140.0, ..Default::default() },
        GridTrack::default(),
    ],
)?;
form.set_control_style_values(StylePart::Root, PartStyleValues {
    column_gap: Some(12.0), row_gap: Some(8.0), ..Default::default()
})?;
form.add(&*window.label("Name")?, 0, 0, 1, 1)?;
form.add(&*window.text_input("Name")?, 0, 1, 1, 1)?;
form.add(&*window.button("Apply")?, 1, 0, 1, 2)?;
root.add(&form, 0.0)?;
auto form = std::make_shared<xui::Grid>();
form->set_tracks(
    {{xui::TrackSizing::automatic}, {xui::TrackSizing::automatic}},
    {{xui::TrackSizing::fixed, 140}, {xui::TrackSizing::star, 1}});
form->set_gap(12, 8);
form->add(std::make_shared<xui::Label>(L"Name"), 0, 0);
form->add(std::make_shared<xui::TextInput>(L"Name"), 0, 1);
form->add(std::make_shared<xui::Button>(L"Apply"), 1, 0, 1, 2);
root->add(form);

GridTrack selects fixed, automatic, or weighted space. The final two add arguments specify row and column spans. The style target is grid. Track definitions, placement, and explicit layout setters remain structural properties.

Wrap

Use Wrap for a small collection of retained controls that changes its column count. Use ItemsView tiles for a large source.

Wrap has no markup constructor. This component requires a C# Wrap from the same window, without an existing parent.

namespace ControlExamples;
component FolderCards {
    param global::Xui.Wrap Cards;
    view {
        VStack() {
            Content(Cards);
        }
    }
}

C# constructs the cards and passes them to the component:

var cards = window.Wrap("Folders").SetItemWidth(180);
cards.SetControlStyleValues(StylePart.Root, new PartStyleValues { Spacing = 12 });
cards.Add(window.Button("Documents"));
cards.Add(window.Button("Pictures"));
cards.Add(window.Button("Downloads"));
var view = new ControlExamples.FolderCards(window, cards);

The binding supplies spacing through style values.

var cards = window.Wrap("Folders").SetItemWidth(180);
cards.SetControlStyleValues(StylePart.Root, new PartStyleValues { Spacing = 12 });
cards.Add(window.Button("Documents"));
cards.Add(window.Button("Pictures"));
cards.Add(window.Button("Downloads"));
root.Add(cards);

The binding supplies spacing through style values.

let cards = window.wrap("Folders")?;
cards.set_item_width(180.0)?;
cards.set_control_style_values(StylePart::Root, PartStyleValues {
    spacing: Some(12.0), ..Default::default()
})?;
cards.add(&*window.button("Documents")?)?;
cards.add(&*window.button("Pictures")?)?;
cards.add(&*window.button("Downloads")?)?;
root.add(&cards, 0.0)?;
auto cards = std::make_shared<xui::Wrap>();
cards->set_item_width(180);
cards->set_spacing(12);
cards->add(std::make_shared<xui::Button>(L"Documents"));
cards->add(std::make_shared<xui::Button>(L"Pictures"));
cards->add(std::make_shared<xui::Button>(L"Downloads"));
root->add(cards);

columns() reports the current layout. Every child remains retained. The style target is wrap, with frame, separator, padding, and spacing.

AdaptiveLayout

Use AdaptiveLayout for navigation and content that share a row at wide widths. Compact mode can stack the same children or show a navigation overlay.

SetPresentation(AdaptivePresentation.InlinePane) pins navigation beside the content. AdaptivePresentation.Overlay overlays it at any width; Responsive retains the default breakpoint behavior. SetNavigationSide(NavigationSide.Right) changes the inline/overlay edge from left to right. Opt into focus-based auto-hide with SetDismissOnFocusOutside(true). Descendants and popups opened from within navigation keep the overlay open. NavigationOpen is readable and writable; it does not hide an inline pane. See the adaptive layout contract for focus and native occlusion details.

C# SetContentSized(true) uses the measured child widths for the breakpoint and the navigation extent. C++ and Rust expose set_content_sized(true). The C ABI property is XUI_F_CONTENT_SIZED (55). The default is false, which retains the configured breakpoint and extent. This mode suits content-sized buttons and labels, not children that request all available width. AutoSize(true) also makes the layout report its measured size to its parent.

The first child is navigation; the second is content. Both are retained.

namespace ControlExamples;
component AdaptiveWorkspace {
    view {
        VStack() {
            Button("Open navigation", click: OpenNavigation);
            AdaptiveLayout("Workspace", ref: Layout, flex: 1,
                breakpoint: 640, navigationExtent: 220,
                compactNavigation: global::Xui.CompactNavigation.Overlay, navigationOpen: false) {
                VStack() { Button("Home"); }
                VStack() { Text("Home page"); }
            }
        }
    }
    code csharp {
        void OpenNavigation() => Layout.SetNavigationOpen(true);
    }
}
var navigation = window.Stack().Add(window.Button("Home"));
var content = window.Stack().Add(window.Label("Home page"));
var adaptive = window.AdaptiveLayout("Workspace", navigation, content)
    .SetBreakpoint(640).SetNavigationExtent(220)
    .SetCompactNavigation(CompactNavigation.Overlay).SetNavigationOpen(false);
anchor.Click += () => adaptive.NavigationOpen = true;
root.Add(adaptive, 1);
let navigation = window.stack(Axis::Vertical)?;
navigation.add(&*window.button("Home")?, 0.0)?;
let content = window.stack(Axis::Vertical)?;
content.add(&*window.label("Home page")?, 0.0)?;
let adaptive = window.adaptive_layout("Workspace", &navigation, &content)?;
adaptive.set_breakpoint(640.0)?;
adaptive.set_navigation_extent(220.0)?;
adaptive.set_compact_navigation(CompactNavigation::Overlay)?;
adaptive.set_navigation_open(false)?;
let layout = adaptive.weak();
anchor.on_event(move |event| {
    let Some(layout) = layout.upgrade() else { return Ok(()); };
    if event.kind == 1 {
        layout.set_navigation_open(true).map_err(|error| {
            eprintln!("Navigation callback: {error}");
            error
        })?;
    }
    Ok(())
})?;
root.add(&adaptive, 1.0)?;
auto navigation = std::make_shared<xui::Stack>(xui::Axis::vertical);
navigation->add(std::make_shared<xui::Button>(L"Home"));
auto content = std::make_shared<xui::Stack>(xui::Axis::vertical);
content->add(std::make_shared<xui::Label>(L"Home page"));
auto adaptive = std::make_shared<xui::AdaptiveLayout>(navigation, content);
adaptive->set_breakpoint(640);
adaptive->set_navigation_extent(220);
adaptive->set_compact_navigation(xui::CompactNavigation::overlay);
adaptive->set_navigation_open(false);
anchor->on_click([adaptive] { adaptive->set_navigation_open(true); });
root->add(adaptive, 1);

The overlay is nonmodal. Escape from navigation closes it. Transitions retain child identities instead of a replacement tree. Native runtime hosts have a separate overlay and unload boundary.

The style target is adaptive_layout. Its compact and expanded states follow layout and navigation state. Styles do not create a breakpoint or change ownership.

ContentView

Use ContentView to clip a retained subtree without a scrollbar. Use ScrollView to let users reach content outside the viewport.

Unavailable as a standalone host. There is no C# ContentView factory or markup constructor. Content(existingElement) embeds an existing element but does not create a clipping host. Use ScrollView for a viewport with scrolling, or SplitView for retained pane hosts.

Unavailable as a standalone host. The binding has no ContentView factory. The ScrollView and SplitView examples provide supported viewport compositions with different behavior.

Unavailable as a standalone host. The binding has no ContentView factory. The ScrollView and SplitView examples provide supported viewport compositions with different behavior.

auto content = std::make_shared<xui::Stack>(xui::Axis::vertical);
content->add(std::make_shared<xui::TextInput>(L"Pane filter"));
auto pane = std::make_shared<xui::ContentView>(content, L"Filter pane");
root->add(pane, 1);

The host supplies a native parent for child controls. It does not add an independent renderer or message loop. The style target is content_view. The frame and content alignment do not style the subtree recursively. C# and Rust have no standalone ContentView factory.

ScrollView

Use ScrollView for a retained form that needs vertical scrolling. It retains every child and is not a replacement for virtual collections.

namespace ControlExamples;
component PreferencesViewport {
    view {
        VStack() {
            ScrollView("Preferences", preferredSize: (360, 160), flex: 1) {
                VStack(spacing: 12) {
                    TextInput("Name");
                    TextInput("Description");
                    TextInput("Owner");
                }
            }
        }
    }
}
var form = window.Stack().Spacing(12);
form.Add(window.TextInput("Name"));
form.Add(window.TextInput("Description"));
form.Add(window.TextInput("Owner"));
var viewport = window.ScrollView(form, "Preferences").PreferredSize(360, 160);
root.Add(viewport, 1);
let form = window.stack(Axis::Vertical)?;
form.spacing(12.0)?;
form.add(&*window.text_input("Name")?, 0.0)?;
form.add(&*window.text_input("Description")?, 0.0)?;
form.add(&*window.text_input("Owner")?, 0.0)?;
let viewport = window.scroll_view(&form, "Preferences")?;
window.update(&[Property {
    a: 360.0, b: 160.0, ..Property::new(&viewport, PropertyKind::PreferredSize)
}])?;
root.add(&viewport, 1.0)?;
auto form = std::make_shared<xui::Stack>(xui::Axis::vertical);
form->set_spacing(12);
form->add(std::make_shared<xui::TextInput>(L"Name"));
form->add(std::make_shared<xui::TextInput>(L"Description"));
form->add(std::make_shared<xui::TextInput>(L"Owner"));
auto viewport = std::make_shared<xui::ScrollView>(form, L"Preferences");
viewport->set_preferred_size({360, 160});
root->add(viewport, 1);

set_offset, scroll_by, and reveal use DIPs and clamp to the content range. Focus traversal reveals nested controls. Native editors retain their editing keys and IME path. UIA supplies Scroll and descendant ScrollItem behavior.

The style target is scroll_view. Scrollbar metrics affect layout, hit testing, and accessible bounds together. The viewport root rejects foreground. set_overlay_scrollbar and set_passthrough select explicit viewport behavior, not style-only variants.

SplitView

Use SplitView for two panes with a user-adjustable divider. Use AdaptiveLayout for a navigation breakpoint without divider interaction.

C# sets the ratio through the generated reference.

namespace ControlExamples;
component PreviewPanes {
    view {
        VStack() {
            SplitView("Preview divider", ref: Split, secondVisible: true, flex: 1) {
                VStack() { Text("Primary pane"); }
                VStack() { Text("Preview pane"); }
            }
        }
    }
}
var view = new ControlExamples.PreviewPanes(window);
view.Split.SetRatio(0.6);
var first = window.Stack().Add(window.Label("Primary pane"));
var second = window.Stack().Add(window.Label("Preview pane"));
var split = window.SplitView("Preview divider", first, second)
    .SetRatio(0.6).SetSecondVisible(true);
root.Add(split, 1);
let first = window.stack(Axis::Vertical)?;
first.add(&*window.label("Primary pane")?, 0.0)?;
let second = window.stack(Axis::Vertical)?;
second.add(&*window.label("Preview pane")?, 0.0)?;
let split = window.split_view("Preview divider", &first, &second)?;
split.set_ratio(0.6)?;
split.set_second_visible(true)?;
root.add(&split, 1.0)?;
auto first = std::make_shared<xui::Stack>(xui::Axis::vertical);
first->add(std::make_shared<xui::Label>(L"Primary pane"));
auto second = std::make_shared<xui::Stack>(xui::Axis::vertical);
second->add(std::make_shared<xui::Label>(L"Preview pane"));
auto split = std::make_shared<xui::SplitView>(first, second, L"Preview divider");
split->set_ratio(0.6f);
split->set_secondary_visible(true);
root->add(split, 1);

first() and second() return retained ContentView hosts. Narrow widths collapse the second pane. on_expanded reports logical visibility and available layout, not animation completion. The divider supports keyboard resizing and cancellation. An explicit transition duration animates secondary-pane entry and exit. C# uses TransitionDuration. C++ and Rust use set_transition_duration. The .xui argument is duration. The default is zero, so existing panes remain immediate. The animation contract defines full-width content, reversal, and reduced motion.

The style target is split_view. Pane frames, divider, and grip do not replace either ContentView. Divider metrics preserve the shared drawing and hit-test geometry.

PageView

Use PageView for retained pages with one active page. It does not supply its own page selector.

PageView has no markup constructor. This component requires a C# page view from the same window, without an existing parent.

namespace ControlExamples;
component PreferencePages {
    param global::Xui.PageView Pages;
    view {
        VStack() {
            Button("Show advanced preferences", click: ShowAdvanced);
            Content(Pages, flex: 1);
        }
    }
    code csharp {
        void ShowAdvanced() => Pages.SetSelectedPage(1);
    }
}

C# constructs the pages. The component supplies the page-selection button:

var pages = window.PageView("Preferences");
pages.Add(window.TextInput("General preferences"));
pages.Add(window.TextInput("Advanced preferences"));
pages.SetSelectedPage(0);
var view = new ControlExamples.PreferencePages(window, pages);
var pages = window.PageView("Preferences");
pages.Add(window.TextInput("General preferences"));
pages.Add(window.TextInput("Advanced preferences"));
pages.SetSelectedPage(0);
anchor.Click += () => pages.SelectedPage = 1;
root.Add(pages, 1);
let pages = window.page_view("Preferences")?;
pages.add(&*window.text_input("General preferences")?)?;
pages.add(&*window.text_input("Advanced preferences")?)?;
pages.set_selected_page(0)?;
let selected_pages = pages.weak();
anchor.on_event(move |event| {
    let Some(selected_pages) = selected_pages.upgrade() else { return Ok(()); };
    if event.kind == 1 {
        selected_pages.set_selected_page(1).map_err(|error| {
            eprintln!("Page callback: {error}");
            error
        })?;
    }
    Ok(())
})?;
root.add(&pages, 1.0)?;
auto pages = std::make_shared<xui::PageView>();
pages->add_page(std::make_shared<xui::TextInput>(L"General preferences"));
pages->add_page(std::make_shared<xui::TextInput>(L"Advanced preferences"));
pages->select(0);
anchor->on_click([pages] { pages->select(1); });
root->add(pages, 1);

select uses a zero-based page index. Page changes perform no application I/O. Native editors retain state after their first use. Hidden runtime hosts are an explicit exception: they unload and need another load.

The style target is page_view. Page content keeps its own styles and identity.

TabStrip

Use TabStrip for stable tab IDs, selection, activation, and close requests. Connect it to a PageView or application model explicitly.

TabStrip and PageView have no markup constructors. This component requires C# elements from the same window, without existing parents.

namespace ControlExamples;
component TabbedPreferences {
    param global::Xui.TabStrip Tabs;
    param global::Xui.PageView Pages;
    view {
        VStack() {
            Content(Tabs);
            Content(Pages, flex: 1);
        }
    }
}

C# constructs both elements and connects selection:

var pages = window.PageView("Preferences");
pages.Add(window.Label("General")).Add(window.Label("Advanced"));
var tabs = window.TabStrip("Preference pages");
tabs.SetTabs([new(10, "General"), new(20, "Advanced")], 10);
tabs.Event += e => {
    if (e.Kind == EventKind.Selection) pages.SelectedPage = e.Value == 10 ? 0UL : 1UL;
};
var view = new ControlExamples.TabbedPreferences(window, tabs, pages);
var pages = window.PageView("Preferences");
pages.Add(window.Label("General")).Add(window.Label("Advanced"));
var tabs = window.TabStrip("Preference pages");
tabs.SetTabs([new(10, "General"), new(20, "Advanced")], 10);
tabs.Event += e => {
    if (e.Kind == EventKind.Selection) pages.SelectedPage = e.Value == 10 ? 0UL : 1UL;
};
root.Add(tabs).Add(pages, 1);
let pages = window.page_view("Preferences")?;
pages.add(&*window.label("General")?)?;
pages.add(&*window.label("Advanced")?)?;
let tabs = window.tab_strip("Preference pages")?;
tabs.set_items(&[
    Choice { id: 10, text: "General".into(), enabled: true, version: 0 },
    Choice { id: 20, text: "Advanced".into(), enabled: true, version: 0 },
], Some(10))?;
let selected_pages = pages.weak();
tabs.on_event(move |event| {
    let Some(selected_pages) = selected_pages.upgrade() else { return Ok(()); };
    if event.kind == 5 {
        selected_pages.set_selected_page(if event.value == 10 { 0 } else { 1 })
            .map_err(|error| {
                eprintln!("Tab callback: {error}");
                error
            })?;
    }
    Ok(())
})?;
root.add(&tabs, 0.0)?;
root.add(&pages, 1.0)?;
auto pages = std::make_shared<xui::PageView>();
pages->add_page(std::make_shared<xui::Label>(L"General"));
pages->add_page(std::make_shared<xui::Label>(L"Advanced"));
auto tabs = std::make_shared<xui::TabStrip>(L"Preference pages");
tabs->set_tabs({{10, L"General"}, {20, L"Advanced"}}, 10);
tabs->on_select([pages](std::uint64_t id) {
    pages->select(id == 10 ? 0 : 1);
});
root->add(tabs);
root->add(pages, 1);

on_activate also handles activation of the already-selected tab. on_close enables close requests. The application updates the tab data after acceptance. One retained control supplies virtual tab providers, not one native peer per tab.

The style target is tab_strip. Tab labels, selection markers, and close actions use stable tab identities. Tab icons and drag states are not supported. C++ supplies set_new_tab_button_visible and on_new_tab for an optional New tab button. C# supplies SetNewTabButtonVisible; activation raises EventKind.Action. The typed Rust wrapper does not expose this new-tab configuration. When tabs overflow, the New tab button reserves a stationary slot at the viewport edge. The opt-in tab duration also animates selected-tab reveal within that viewport.