Files
gpui-native-kit/examples/gallery/src/main.rs
T
2026-11-02 16:24:20 +07:00

1930 lines
85 KiB
Rust

use std::time::Duration;
use gpui::prelude::FluentBuilder as _;
use gpui::{
App, AppContext as _, Bounds, ClickEvent, Context, Entity, InteractiveElement as _,
IntoElement, ParentElement as _, Pixels, Render, SharedString, StatefulInteractiveElement as _,
Styled as _, Window, WindowBounds, WindowOptions, div, px, size,
};
use gpui_native_core::alert::{Alert, AlertSeverity, Alerts, AlertsView};
use gpui_native_core::avatar::Avatar;
use gpui_native_core::badge::{Badge, BadgeVariant};
use gpui_native_core::button::{Button, ButtonVariant};
use gpui_native_core::checkbox::Checkbox;
use gpui_native_core::common::{Sizable as _, h_flex, v_flex};
use gpui_native_core::context_menu::ContextMenu;
use gpui_native_core::dialog::Dialog;
use gpui_native_core::empty_state::EmptyState;
use gpui_native_core::input::{Input, InputState};
use gpui_native_core::link::Link;
use gpui_native_core::menu::{Menu, MenuItem};
use gpui_native_core::popover::Popover;
use gpui_native_core::progress::Progress;
use gpui_native_core::radio::{Radio, RadioGroup};
use gpui_native_core::scroll_area::ScrollArea;
use gpui_native_core::segmented::{Segmented, SegmentedOption};
use gpui_native_core::select::{Select, SelectOption};
use gpui_native_core::slider::Slider;
use gpui_native_core::switch::Switch;
use gpui_native_core::toast::{Toast, ToastAction, Toasts, ToastsView};
use gpui_native_core::toggle::Toggle;
use gpui_native_core::tooltip::Tooltip;
use gpui_native_data::list::{List, ListItem};
use gpui_native_data::sidebar::{Sidebar, SidebarEntry};
use gpui_native_data::tabs::{Tab, Tabs, TabsVariant};
use gpui_native_data::tree::{Tree, TreeItem};
use gpui_native_icons::{IconAssets, IconRole, icon};
use gpui_native_layout::dock::{Dock, DockState, PanelDescriptor, SplitDirection};
use gpui_native_layout::resize_handle::ResizeHandle;
use gpui_native_layout::shell::{LayoutState, Shell};
use gpui_native_theme::{ActiveTheme as _, Size, Theme};
use gpui_platform::application;
struct Gallery {
button_clicks: usize,
checkbox_checked: bool,
switch_checked: bool,
plan: SharedString,
toggle_pressed: bool,
volume: f32,
input_state: Entity<InputState>,
password_state: Entity<InputState>,
notes_state: Entity<InputState>,
split_width: Pixels,
layout_state: Entity<LayoutState>,
dock_state: Entity<DockState>,
shell_selection: Option<usize>,
popover_open: bool,
menu_open: bool,
strategy: Option<Strategy>,
explore_filter: ExploreFilter,
context_action: SharedString,
dialog_open: bool,
alert_open: bool,
update_alert_dismissed: bool,
alerts: Entity<Alerts>,
toasts: Entity<Toasts>,
scroll: gpui::ScrollHandle,
list_selected: Option<usize>,
tree_nodes: Vec<GalleryNode>,
tree_selected: Option<usize>,
tab_selected: usize,
card_tabs: Vec<SharedString>,
card_tab_selected: usize,
sidebar_selected: Option<usize>,
}
/// The select section's demo value.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Strategy {
Curated,
Uncensored,
}
/// The segmented section's demo value.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum ExploreFilter {
#[default]
All,
Popular,
Recent,
}
/// The context menu section's demo actions.
const CONTEXT_ACTIONS: [&str; 3] = ["Rename", "Duplicate", "Archive"];
/// A demo model for the tree section: the app owns the real tree and its
/// expansion set, and flattens the visible nodes for the component.
#[derive(Clone)]
struct GalleryNode {
label: SharedString,
icon: Option<IconRole>,
expanded: bool,
children: Vec<GalleryNode>,
}
fn flatten_tree(
nodes: &[GalleryNode],
depth: usize,
prefix: &[usize],
items: &mut Vec<TreeItem>,
paths: &mut Vec<Vec<usize>>,
) {
for (index, node) in nodes.iter().enumerate() {
let mut path = prefix.to_vec();
path.push(index);
let mut item = TreeItem::new(node.label.clone())
.depth(depth)
.expandable(!node.children.is_empty())
.expanded(node.expanded);
if let Some(role) = node.icon {
item = item.icon(role);
}
items.push(item);
paths.push(path.clone());
if node.expanded {
flatten_tree(&node.children, depth + 1, &path, items, paths);
}
}
}
fn toggle_tree_path(nodes: &mut [GalleryNode], path: &[usize]) {
let Some(index) = path.first() else { return };
let Some(node) = nodes.get_mut(*index) else {
return;
};
if path.len() == 1 {
node.expanded = !node.expanded;
} else {
toggle_tree_path(&mut node.children, &path[1..]);
}
}
/// The visible-row paths of the current tree state, matching the flat index
/// the tree component reports.
fn tree_paths(nodes: &[GalleryNode]) -> Vec<Vec<usize>> {
let mut items = Vec::new();
let mut paths = Vec::new();
flatten_tree(nodes, 0, &[], &mut items, &mut paths);
paths
}
impl Gallery {
/// The app shell demo: sidebars around a center region, toggles in the
/// center demonstrating the `LayoutState` API.
fn render_app_shell(&self, gallery: &Entity<Self>, cx: &App) -> impl IntoElement + use<> {
let colors = cx.theme().colors;
let state = self.layout_state.clone();
let state_for_toggle = state.clone();
let toggle = |side: &'static str| {
let state = state_for_toggle.clone();
Button::new(SharedString::from(format!("gallery-shell-toggle-{side}")))
.label(format!("Toggle {}", side))
.variant(ButtonVariant::Secondary)
.small()
.on_click(move |_, _, cx| {
state.update(cx, |state, cx| match side {
"left" => {
let visible = !state.is_left_sidebar_visible();
state.set_left_sidebar_visible(visible, cx);
}
_ => {
let visible = !state.is_right_sidebar_visible();
state.set_right_sidebar_visible(visible, cx);
}
});
})
};
let left_sidebar = Sidebar::new("gallery-shell-sidebar")
.selected(self.shell_selection)
.entries(|_, _| {
vec![
SidebarEntry::header("Repositories"),
SidebarEntry::row("gpui-native-kit").icon(IconRole::Folder),
SidebarEntry::row("signed").icon(IconRole::Folder),
SidebarEntry::header("Explore"),
SidebarEntry::row("Popular"),
SidebarEntry::row("Recent"),
]
})
.on_change({
let gallery = gallery.clone();
move |selection, _, cx| {
gallery.update(cx, |gallery, _| gallery.shell_selection = selection);
}
});
let right_sidebar = v_flex()
.gap_1()
.p_2()
.child("Details")
.child("Signed by the author")
.text_sm()
.text_color(colors.muted_foreground);
let open_panel = |dock: &Entity<DockState>, id: &'static str, label: &'static str| {
let dock = dock.clone();
Button::new(SharedString::from(format!("gallery-dock-open-{id}")))
.label(label)
.variant(ButtonVariant::Secondary)
.small()
.on_click(move |_, window, cx| {
dock.update(cx, |dock, cx| dock.open_panel(id, window, cx));
})
};
let center = v_flex()
.size_full()
.gap_2()
.child(
h_flex()
.flex_none()
.flex_wrap()
.gap_2()
.child(toggle("left"))
.child(toggle("right"))
.child(open_panel(&self.dock_state, "inbox", "Open inbox"))
.child(open_panel(&self.dock_state, "settings", "Open settings"))
.child({
let dock = self.dock_state.clone();
Button::new("gallery-dock-split")
.label("Split active right")
.variant(ButtonVariant::Secondary)
.small()
.on_click(move |_, _, cx| {
dock.update(cx, |dock, cx| {
if let Some(active) = dock.active_panel() {
dock.split_panel(&active, SplitDirection::Right, cx);
}
});
})
}),
)
.child(
div()
.flex_1()
.min_h_0()
.overflow_hidden()
.child(Dock::new("gallery-dock", self.dock_state.clone())),
);
Shell::new("gallery-shell", state)
.left_sidebar(left_sidebar)
.center(center)
.right_sidebar(right_sidebar)
}
}
/// A stand-in panel view for the dock demo.
struct DemoPanel {
title: &'static str,
body: &'static str,
}
impl Render for DemoPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
v_flex()
.size_full()
.gap_1()
.p_4()
.bg(colors.background)
.child(div().child(self.title))
.child(
div()
.text_sm()
.text_color(colors.muted_foreground)
.child(self.body),
)
}
}
impl Render for Gallery {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors;
let radius = theme.radius;
let variant = theme.variant;
let source = gpui_native_theme::detected_source(cx);
let mode_override = gpui_native_theme::mode_override(cx);
let active_preset = gpui_native_theme::active_preset(cx);
let gallery = cx.entity();
let swatches: [(&str, gpui::Hsla); 18] = [
("background", colors.background),
("foreground", colors.foreground),
("surface", colors.surface),
("surface_foreground", colors.surface_foreground),
("primary", colors.primary),
("primary_foreground", colors.primary_foreground),
("secondary", colors.secondary),
("secondary_foreground", colors.secondary_foreground),
("muted", colors.muted),
("muted_foreground", colors.muted_foreground),
("accent", colors.accent),
("accent_foreground", colors.accent_foreground),
("destructive", colors.destructive),
("destructive_foreground", colors.destructive_foreground),
("border", colors.border),
("input", colors.input),
("ring", colors.ring),
("selection", colors.selection),
];
let shades = [
("base", theme.ramps.primary.base),
("hover", theme.ramps.primary.hover),
("pressed", theme.ramps.primary.pressed),
("subtle", theme.ramps.primary.subtle),
];
let modes: [(SharedString, Option<gpui_native_theme::ThemeVariant>, bool); 3] = [
("System".into(), None, mode_override.is_none()),
(
"Light".into(),
Some(gpui_native_theme::ThemeVariant::Light),
mode_override == Some(gpui_native_theme::ThemeVariant::Light),
),
(
"Dark".into(),
Some(gpui_native_theme::ThemeVariant::Dark),
mode_override == Some(gpui_native_theme::ThemeVariant::Dark),
),
];
let presets: [(SharedString, Option<&'static str>, bool); 2] = [
("Follow system".into(), None, active_preset.is_none()),
(
"Tokyo Night".into(),
Some("tokyo-night"),
active_preset == Some("tokyo-night"),
),
];
let button_variants = [
("primary", ButtonVariant::Primary),
("secondary", ButtonVariant::Secondary),
("ghost", ButtonVariant::Ghost),
("destructive", ButtonVariant::Destructive),
];
let content = div()
.font_family(".SystemUIFont")
.bg(colors.background)
.text_color(colors.foreground)
.p_6()
.flex()
.flex_col()
.gap_5()
.child(
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_xl()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child("gpui-native-kit"),
)
.child(
div()
.text_sm()
.text_color(colors.muted_foreground)
.child(format!(
"phase C — button · source: {source} · variant: {variant:?} · control radius {}px",
f32::from(theme.radius.control)
)),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(section_label("mode", colors))
.child(
div()
.flex()
.gap_2()
.children(modes.map(|(label, value, active)| {
chip(
format!("mode-{label}"),
label,
active,
colors,
radius,
move |_, _, cx| gpui_native_theme::set_mode_override(value, cx),
)
})),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(section_label("preset", colors))
.child(
div()
.flex()
.gap_2()
.children(presets.map(|(label, value, active)| {
chip(
format!("preset-{label}"),
label,
active,
colors,
radius,
move |_, _, cx| {
let _ = gpui_native_theme::set_preset(value, cx);
},
)
})),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"button · native refs — NSButton, WinUI Button, Adwaita, Breeze",
colors,
))
.child(
h_flex()
.flex_wrap()
.gap_2()
.children(button_variants.map(|(name, variant)| {
let gallery = gallery.clone();
let mut button = Button::new(SharedString::from(format!(
"gallery-button-{name}"
)))
.label(button_label(variant))
.variant(variant)
.on_click(move |_, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.button_clicks += 1;
cx.notify();
});
});
if variant == ButtonVariant::Destructive {
button =
button.tooltip(Tooltip::text("Removes the selected item"));
}
button
}))
.child(
Button::new("gallery-button-disabled")
.label("Disabled")
.disabled(true),
)
.child(
Button::new("gallery-button-icon")
.label("Open")
.icon(IconRole::Folder)
.tooltip(Tooltip::text("Opens the selected file")),
)
.child(
Button::new("gallery-button-icon-only")
.icon(IconRole::Folder)
.variant(ButtonVariant::Ghost)
.accessibility_label("Open the selected file")
.tooltip(Tooltip::text("Opens the selected file")),
),
)
.child(
h_flex()
.items_center()
.gap_2()
.child(Button::new("gallery-button-sm").label("Small").small())
.child(Button::new("gallery-button-md").label("Medium"))
.child(Button::new("gallery-button-lg").label("Large").large()),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("clicked {} times", self.button_clicks)),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"input · native refs — NSTextField, WinUI TextBox, Adwaita Entry, Breeze LineEdit",
colors,
))
.child(
Input::new("gallery-input", self.input_state.clone())
.placeholder("Type here…"),
)
.child(
Input::new("gallery-password", self.password_state.clone())
.placeholder("Password"),
)
.child(
Input::new("gallery-notes", self.notes_state.clone())
.placeholder("Notes — Enter breaks the line, cmd-Enter submits")
.rows(4),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!(
"value: {}",
self.input_state.read(cx).value()
)),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"layout · resize handle \u{2014} NSSplitView divider, WinUI GridSplitter, GtkPaned handle",
colors,
))
.child(
h_flex()
.h(px(112.))
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(
div()
.w(self.split_width)
.h_full()
.flex()
.items_center()
.justify_center()
.bg(colors.muted)
.child("Sidebar"),
)
.child(
ResizeHandle::new("gallery-resize")
.on_resize({
let gallery = gallery.clone();
move |delta, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.split_width = (gallery.split_width
+ delta)
.clamp(px(96.), px(320.));
cx.notify();
});
}
})
.on_reset({
let gallery = gallery.clone();
move |_, cx| {
gallery.update(cx, |gallery, cx| {
gallery.split_width = px(200.);
cx.notify();
});
}
}),
)
.child(
div()
.flex_1()
.h_full()
.flex()
.items_center()
.justify_center()
.bg(colors.input)
.child("Editor"),
),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!(
"width: {} px \u{2014} drag the seam, double-click resets",
f32::from(self.split_width)
)),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"layout · app shell \u{2014} NSWindow sidebar + inspector, WinUI SplitView shell",
colors,
))
.child(
div()
.h(px(320.))
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(self.render_app_shell(&gallery, cx)),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!(
"left: {} · right: {} · dock: {} \u{2014} buttons toggle, seams resize, tabs close and split",
if self.layout_state.read(cx).is_left_sidebar_visible() {
"on"
} else {
"off"
},
if self.layout_state.read(cx).is_right_sidebar_visible() {
"on"
} else {
"off"
},
if self.dock_state.read(cx).active_panel().is_some() {
"open"
} else {
"empty"
},
)),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"checkbox · native refs — NSButton (checkbox), WinUI CheckBox, Adwaita CheckButton, Breeze",
colors,
))
.child(
v_flex().gap_2().child({
let gallery = gallery.clone();
Checkbox::new("gallery-checkbox")
.label("Enable telemetry")
.checked(self.checkbox_checked)
.on_change(move |checked, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.checkbox_checked = checked;
cx.notify();
});
})
})
.child(
Checkbox::new("gallery-checkbox-disabled-off")
.label("Disabled unchecked")
.disabled(true),
)
.child(
Checkbox::new("gallery-checkbox-disabled-on")
.label("Disabled checked")
.checked(true)
.disabled(true),
),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"menu · native ref — NSMenu, WinUI MenuFlyout, Adwaita popover menu",
colors,
))
.child({
let gallery = gallery.clone();
Menu::new("gallery-menu")
.open(self.menu_open)
.on_change(move |open, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.menu_open = open;
cx.notify();
});
})
.trigger(|_, _| {
Button::new("gallery-menu-trigger")
.label("Actions")
.variant(ButtonVariant::Secondary)
.into_any_element()
})
.items(|_, _| {
vec![
MenuItem::new("Cut")
.shortcut("cmd-x")
.handler(|_, cx| {
eprintln!("menu: cut");
let _ = cx;
}),
MenuItem::new("Copy").shortcut("cmd-c"),
MenuItem::new("Paste").shortcut("cmd-v"),
MenuItem::separator(),
MenuItem::new("Use quiet mode").checked(true),
MenuItem::new("Show hidden files"),
MenuItem::separator(),
MenuItem::new("Unavailable").disabled(true),
MenuItem::new("Delete").destructive(true),
]
})
}),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(section_label(
"select · native refs — NSPopUpButton, WinUI ComboBox, Adwaita Dropdown",
colors,
))
.child({
let gallery = gallery.clone();
div().w(gpui::px(280.)).flex().flex_col().gap_3().child(
Select::new("gallery-strategy")
.options(vec![
SelectOption::new(Strategy::Curated, "Curated")
.icon(IconRole::Check),
SelectOption::new(Strategy::Uncensored, "Uncensored")
.icon(IconRole::Warning),
])
.selected(self.strategy)
.placeholder("Fetching strategy")
.on_change(move |next, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.strategy = next;
cx.notify();
});
}),
)
})
.child({
div().w(gpui::px(280.)).child(
Select::new("gallery-size")
.options(vec![
("small", "Small"),
("medium", "Medium"),
("large", "Large"),
])
.selected("medium")
.placeholder("Control size")
.small(),
)
}),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(section_label(
"context menu · native refs — NSMenu (context), WinUI MenuFlyout, Adwaita context popover",
colors,
))
.child({
let gallery = gallery.clone();
ContextMenu::new("gallery-context")
.target(move |_, cx| {
let theme = cx.theme();
div()
.px_3()
.py_2()
.rounded(theme.radius.overlay)
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface)
.text_size(theme.sizes.medium.text_size)
.child("Right-click this card")
.into_any_element()
})
.items(move |_, _| {
CONTEXT_ACTIONS
.iter()
.map(|action| {
let gallery = gallery.clone();
MenuItem::new(*action).handler(move |_, cx| {
gallery.update(cx, |gallery, cx| {
gallery.context_action =
SharedString::from(*action);
cx.notify();
});
})
})
.collect()
})
.accessibility_label("Card actions")
})
.child(
div()
.text_sm()
.text_color(colors.muted_foreground)
.child(self.context_action.clone()),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(section_label(
"empty state · native refs — AdwStatusPage, Kirigami PlaceHolderMessage, macOS placeholder views",
colors,
))
.child(
div()
.h(px(180.))
.rounded(theme.radius.overlay)
.border_1()
.border_color(colors.border)
.flex()
.child(
EmptyState::new("No repositories found")
.icon(IconRole::Search)
.hint("Try a different name, or publish one first.")
.action(
Button::new("empty-state-clear")
.label("Clear filters")
.variant(ButtonVariant::Secondary),
),
),
)
.child(
div()
.h(px(120.))
.rounded(theme.radius.overlay)
.border_1()
.border_color(colors.border)
.flex()
.child(EmptyState::new("Nothing here yet")),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(section_label(
"segmented · native refs — NSSegmentedControl, WinUI Segmented, Adwaita AdwToggleGroup",
colors,
))
.child({
let gallery = gallery.clone();
div()
.flex()
.flex_col()
.gap_2()
.child(
Segmented::new("gallery-explore", self.explore_filter)
.options(vec![
(ExploreFilter::All, "All"),
(ExploreFilter::Popular, "Popular"),
(ExploreFilter::Recent, "Recent"),
])
.on_change(move |next, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.explore_filter = next;
cx.notify();
});
}),
)
.child(
Segmented::new("gallery-explore-medium", self.explore_filter)
.options(vec![
SegmentedOption::new(ExploreFilter::All, "All")
.icon(IconRole::Check),
SegmentedOption::new(ExploreFilter::Popular, "Popular")
.icon(IconRole::Search),
SegmentedOption::new(ExploreFilter::Recent, "Recent")
.icon(IconRole::Info),
])
.with_size(Size::Medium),
)
.child(
Segmented::new("gallery-explore-large", self.explore_filter)
.options(vec![
(ExploreFilter::All, "All"),
(ExploreFilter::Popular, "Popular"),
(ExploreFilter::Recent, "Recent"),
])
.large(),
)
}),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"list · native refs — NSTableView (list), WinUI ListView, Adwaita ListBox, Breeze",
colors,
))
.child({
let gallery = gallery.clone();
List::new("gallery-list")
.selected(self.list_selected)
.on_change(move |selected, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.list_selected = selected;
cx.notify();
});
})
.items(|_, _| {
vec![
ListItem::new("Documents").icon(IconRole::Folder),
ListItem::new("Pictures")
.icon(IconRole::Folder)
.detail("24 items"),
ListItem::new("Music").icon(IconRole::Folder),
ListItem::new("Archive").disabled(true),
ListItem::new("Trash").detail("emptied weekly"),
]
})
})
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("selected: {:?}", self.list_selected)),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"tree · native refs — NSOutlineView, WinUI TreeView, Adwaita expander rows, Breeze",
colors,
))
.child({
let gallery = gallery.clone();
let disclosure_gallery = gallery.clone();
let mut items = Vec::new();
let mut paths = Vec::new();
flatten_tree(&self.tree_nodes, 0, &[], &mut items, &mut paths);
Tree::new("gallery-tree")
.selected(self.tree_selected)
.on_change(move |selected, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.tree_selected = selected;
cx.notify();
});
})
.on_disclosure(move |index, _, cx| {
disclosure_gallery.update(cx, |gallery, cx| {
let paths = tree_paths(&gallery.tree_nodes);
if let Some(path) = paths.get(index) {
toggle_tree_path(&mut gallery.tree_nodes, path);
}
cx.notify();
});
})
.items(move |_, _| items.clone())
}),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"sidebar · native refs — NSOutlineView (source list), WinUI NavigationView, Adwaita Sidebar",
colors,
))
.child(div().w(px(280.)).child({
let gallery = gallery.clone();
Sidebar::new("gallery-sidebar")
.selected(self.sidebar_selected)
.on_change(move |selected, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.sidebar_selected = selected;
cx.notify();
});
})
.entries(|_, _| {
vec![
SidebarEntry::header("Favorites"),
SidebarEntry::row("Documents").icon(IconRole::Folder),
SidebarEntry::row("Pictures")
.icon(IconRole::Folder)
.detail("24"),
SidebarEntry::row("Archive").disabled(true),
SidebarEntry::header("Locations"),
SidebarEntry::row("iCloud").icon(IconRole::Folder),
SidebarEntry::row("Trash"),
]
})
})),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"tabs · native refs — NSTabView, WinUI TabView, Adwaita TabBar, Breeze",
colors,
))
.child({
let gallery = gallery.clone();
Tabs::new("gallery-tabs")
.selected(self.tab_selected)
.on_change(move |selected, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.tab_selected = selected;
cx.notify();
});
})
.tabs(|_, _| {
vec![
Tab::new("Overview"),
Tab::new("Details"),
Tab::new("Settings"),
Tab::new("Locked").disabled(true),
]
})
})
.child(match self.tab_selected {
0 => div()
.child("The overview panel is the app's business — the strip only reports.")
.into_any_element(),
1 => div()
.child("Details: rows, lists and trees all live under their own tabs.")
.into_any_element(),
_ => div()
.child("Settings: keyboard focus rides the strip; arrows switch tabs.")
.into_any_element(),
})
.child({
let gallery = gallery.clone();
let gallery_for_close = gallery.clone();
Tabs::new("gallery-pill-tabs")
.variant(TabsVariant::Pill)
.selected(self.card_tab_selected)
.on_change(move |selected, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.card_tab_selected = selected;
cx.notify();
});
})
.on_close(move |index, _, cx| {
gallery_for_close.update(cx, |gallery, cx| {
if index < gallery.card_tabs.len() {
gallery.card_tabs.remove(index);
gallery.card_tab_selected = gallery
.card_tab_selected
.min(gallery.card_tabs.len().saturating_sub(1));
cx.notify();
}
});
})
.tabs({
let labels = self.card_tabs.clone();
move |_, _| {
labels
.iter()
.map(|label| {
Tab::new(label.clone())
.closeable(true)
.busy(label == "Syncing")
})
.collect()
}
})
})
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!(
"pill variant (the signed dock tab) — {} tabs left; the ring on \"Syncing\" is the Spinner placeholder",
self.card_tabs.len()
)),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"alert · static callout — Adwaita InfoBar, WinUI InfoBar",
colors,
))
.child(
v_flex()
.gap_2()
.child(
Alert::new("gallery-alert-update").title("Update available")
.message("Version 2.4.0 is ready to install.")
.severity(AlertSeverity::Info),
)
.when(!self.update_alert_dismissed, |section| {
section.child(
Alert::new("gallery-alert-sync")
.title("Sync failed")
.message("Will retry when you're back online.")
.severity(AlertSeverity::Danger)
.on_dismiss({
let gallery = gallery.clone();
move |cx| {
gallery.update(cx, |gallery, cx| {
gallery.update_alert_dismissed = true;
cx.notify();
});
}
}),
)
}),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"alert stack · pushed, persistent — macOS notification stack",
colors,
))
.child(
div().flex().gap_2().children([
{
let alerts = self.alerts.clone();
Button::new("gallery-alert-push-info")
.label("Push info")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
alerts.update(cx, |alerts, cx| {
alerts.push(
Alert::new("pushed-info")
.title("Backed up")
.message("Everything is in sync.")
.severity(AlertSeverity::Info),
cx,
);
});
})
},
{
let alerts = self.alerts.clone();
Button::new("gallery-alert-push-warning")
.label("Push warning")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
alerts.update(cx, |alerts, cx| {
alerts.push(
Alert::new("pushed-warning")
.title("Storage almost full")
.message("1.2 GB remaining on this device.")
.severity(AlertSeverity::Warning),
cx,
);
});
})
},
{
let alerts = self.alerts.clone();
Button::new("gallery-alert-clear")
.label("Clear all")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
alerts.update(cx, |alerts, cx| alerts.clear(cx));
})
},
]),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"toast · temporary message, one at a time — Adwaita Toast, macOS banner",
colors,
))
.child(
div().flex().gap_2().children([
{
let toasts = self.toasts.clone();
Button::new("gallery-toast-undo")
.label("Deleted (with undo)")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
toasts.update(cx, |toasts, cx| {
toasts.push(
Toast::new("Draft deleted")
.action(ToastAction::new("Undo", |cx| {
// A real app restores the draft here.
let _ = cx;
})),
cx,
);
});
})
},
{
let toasts = self.toasts.clone();
Button::new("gallery-toast-saved")
.label("Saved (with icon)")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
toasts.update(cx, |toasts, cx| {
toasts.push(
Toast::new("Workspace saved")
.icon(IconRole::Check),
cx,
);
});
})
},
{
let toasts = self.toasts.clone();
Button::new("gallery-toast-connection")
.label("Connection lost (sticky)")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
toasts.update(cx, |toasts, cx| {
toasts.push(
Toast::new("Connection lost")
.icon(IconRole::Warning)
.duration(Duration::ZERO),
cx,
);
});
})
},
]),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"dialog · alert · native refs — NSAlert, WinUI ContentDialog, Adwaita MessageDialog",
colors,
))
.child(
div().flex().gap_2().children([
{
let gallery = gallery.clone();
Button::new("gallery-dialog-open")
.label("Delete file…")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.dialog_open = true;
cx.notify();
});
})
},
{
let gallery = gallery.clone();
Button::new("gallery-alert-open")
.label("Sign out…")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.alert_open = true;
cx.notify();
});
})
},
]),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"popover · native ref — NSPopover, WinUI Flyout, Adwaita Popover",
colors,
))
.child({
let gallery = gallery.clone();
Popover::new("gallery-popover")
.open(self.popover_open)
.on_change(move |open, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.popover_open = open;
cx.notify();
});
})
.trigger(|_, _| {
Button::new("gallery-popover-trigger")
.label("Options")
.variant(ButtonVariant::Secondary)
.into_any_element()
})
.content(move |_, _| {
v_flex()
.gap_1()
.w(px(240.))
.p_2()
.child(section_label("appearance", colors))
.child(
Checkbox::new("gallery-popover-telemetry")
.label("Enable telemetry"),
)
.child(
Checkbox::new("gallery-popover-crash")
.label("Send crash reports"),
)
.into_any_element()
})
}),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"switch · native refs — NSSwitch, WinUI ToggleSwitch, Adwaita Switch, Breeze Switch",
colors,
))
.child(
v_flex().gap_2().child({
let gallery = gallery.clone();
Switch::new("gallery-switch")
.label("Wi-Fi")
.checked(self.switch_checked)
.on_change(move |checked, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.switch_checked = checked;
cx.notify();
});
})
})
.child(
Switch::new("gallery-switch-disabled-off")
.label("Disabled off")
.disabled(true),
)
.child(
Switch::new("gallery-switch-disabled-on")
.label("Disabled on")
.checked(true)
.disabled(true),
),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"radio · native refs — NSButton (radio), WinUI RadioButton, Adwaita CheckButton (radio), Breeze",
colors,
))
.child({
let gallery = gallery.clone();
RadioGroup::new("gallery-radio-group")
.selected(Some(self.plan.clone()))
.on_change(move |plan, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.plan = plan;
cx.notify();
});
})
.child(Radio::new("radio-starter", SharedString::from("starter")).label("Starter"))
.child(Radio::new("radio-pro", SharedString::from("pro")).label("Pro"))
.child(
Radio::new("radio-team", SharedString::from("team"))
.label("Team")
.disabled(true),
)
}),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"toggle · native refs — NSButton (toggle), WinUI ToggleButton, Adwaita ToggleButton, Breeze",
colors,
))
.child(
h_flex().gap_2().child({
let gallery = gallery.clone();
Toggle::new("gallery-toggle")
.label("Bold")
.pressed(self.toggle_pressed)
.on_change(move |pressed, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.toggle_pressed = pressed;
cx.notify();
});
})
})
.child(
Toggle::new("gallery-toggle-disabled")
.label("Disabled")
.pressed(true)
.disabled(true),
),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"link · native refs — NSTextView links, WinUI HyperlinkButton, Adwaita LinkButton, Breeze",
colors,
))
.child(
h_flex()
.gap_4()
.child(
Link::new("gallery-link-github")
.label("gpui on GitHub")
.href("https://github.com/zed-industries/zed"),
)
.child({
let gallery = gallery.clone();
Link::new("gallery-link-override")
.label("On-click override")
.href("https://example.com/never-opened")
.on_click(move |_, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.button_clicks += 1;
cx.notify();
});
})
})
.child(
Link::new("gallery-link-disabled")
.label("Disabled")
.href("https://example.com/disabled")
.disabled(true),
),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"slider · native refs — NSSlider, WinUI Slider, Adwaita Scale, Breeze Slider",
colors,
))
.child({
let gallery = gallery.clone();
Slider::new("gallery-slider", self.volume)
.min(0.0)
.max(100.0)
.step(5.0)
.accessibility_label("Volume")
.on_change(move |volume, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.volume = volume;
cx.notify();
});
})
})
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("volume: {}", self.volume)),
),
)
.child(
v_flex()
.gap_2()
.child(section_label(
"progress · native refs — NSProgressIndicator, WinUI ProgressBar, Adwaita ProgressBar, Breeze",
colors,
))
.child(Progress::new("progress-25", 0.25).accessibility_label("Quarter"))
.child(Progress::new("progress-60", 0.6).accessibility_label("Most"))
.child(Progress::new("progress-90", 0.9).accessibility_label("Nearly"))
.child(
Progress::new("progress-indeterminate", 0.0)
.indeterminate(true)
.accessibility_label("Working"),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(section_label(
"avatar · native refs — NSImage (contact), WinUI PersonPicture, Adwaita Avatar, Breeze",
colors,
))
.child(
h_flex()
.gap_3()
.child(
Avatar::new("reya@reya.info")
.name("Reya Adele")
.picture(Some("https://github.com/reya.png")),
)
.child(
Avatar::new("ada@reya.info")
.name("Ada Lovelace")
.picture(Some("https://example.com/missing.png")),
)
.child(Avatar::new("grace@reya.info").name("Grace Hopper").small())
.child(Avatar::new("desk@reya.info").name("Help Desk").large()),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(section_label(
"badge · native refs — WinUI InfoBadge, macOS app badge, GTK count pill",
colors,
))
.child(
h_flex()
.gap_3()
.child(Badge::new("3"))
.child(Badge::new("new").variant(BadgeVariant::Accent))
.child(Badge::new("synced").variant(BadgeVariant::Success))
.child(Badge::new("drift").variant(BadgeVariant::Warning))
.child(Badge::new("12").variant(BadgeVariant::Danger))
.child(Badge::new("small").small())
.child(Badge::new("large").large()),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(section_label("accent shades (derived, OKLCH)", colors))
.child(div().flex().gap_2().children(shades.map(|(name, color)| {
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.size_6()
.rounded(radius.full)
.border_1()
.border_color(colors.border)
.bg(color),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(name),
)
}))),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(section_label("color tokens", colors))
.child(div().flex().flex_wrap().gap_2().children(swatches.map(
|(name, color)| {
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.size_6()
.rounded(radius.full)
.border_1()
.border_color(colors.border)
.bg(color),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(name),
)
},
))),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(section_label(
"icons (Lucide fallback; platform resolvers land later)",
colors,
))
.child(
div().flex().items_center().gap_4().children(
IconRole::all()
.iter()
.map(|role| icon(*role).size(px(20.)).color(colors.foreground)),
),
),
);
// The whole gallery scrolls, demonstrating the scroll area at the
// page level: the thumb floats over the window's content. The
// dialogs render outside it, as the last children of the window, so
// their overlay layer paints over everything.
let dialog_gallery = gallery.clone();
let dialog = Dialog::new("gallery-dialog")
.open(self.dialog_open)
.title("Delete file?")
.on_change({
let gallery = dialog_gallery.clone();
move |open, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.dialog_open = open;
cx.notify();
});
}
})
.content(|_, _| {
div()
.child("\"report-final.pdf\" will be removed permanently.")
.into_any_element()
})
.actions(move |_, _| {
let gallery = dialog_gallery.clone();
vec![
Button::new("gallery-dialog-cancel")
.label("Cancel")
.variant(ButtonVariant::Secondary)
.on_click({
let gallery = gallery.clone();
move |_, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.dialog_open = false;
cx.notify();
});
}
})
.into_any_element(),
Button::new("gallery-dialog-delete")
.label("Delete")
.variant(ButtonVariant::Destructive)
.on_click({
let gallery = gallery.clone();
move |_, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.dialog_open = false;
cx.notify();
});
}
})
.into_any_element(),
]
});
let alert_gallery = gallery.clone();
let alert = Dialog::new("gallery-alert")
.open(self.alert_open)
.alert(true)
.title("Sign out of the workspace?")
.on_change({
let gallery = alert_gallery.clone();
move |open, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.alert_open = open;
cx.notify();
});
}
})
.content(|_, _| {
div()
.child("Any running agents will keep working in the background.")
.into_any_element()
})
.actions(move |_, _| {
let gallery = alert_gallery.clone();
vec![
Button::new("gallery-alert-stay")
.label("Stay")
.variant(ButtonVariant::Secondary)
.on_click({
let gallery = gallery.clone();
move |_, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.alert_open = false;
cx.notify();
});
}
})
.into_any_element(),
Button::new("gallery-alert-signout")
.label("Sign out")
.variant(ButtonVariant::Primary)
.on_click({
let gallery = gallery.clone();
move |_, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.alert_open = false;
cx.notify();
});
}
})
.into_any_element(),
]
});
div()
.size_full()
.child(
ScrollArea::new("gallery-scroll")
.handle(self.scroll.clone())
.size_full()
.child(content),
)
.child(dialog)
.child(alert)
.child(AlertsView::new("gallery-alerts", self.alerts.clone()))
.child(ToastsView::new("gallery-toasts", self.toasts.clone()))
}
}
fn button_label(variant: ButtonVariant) -> &'static str {
match variant {
ButtonVariant::Primary => "Primary",
ButtonVariant::Secondary => "Secondary",
ButtonVariant::Ghost => "Ghost",
ButtonVariant::Destructive => "Delete",
}
}
fn section_label(text: &str, colors: gpui_native_theme::ColorTokens) -> impl IntoElement + use<> {
div()
.text_sm()
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(colors.muted_foreground)
.child(text.to_string())
}
fn chip<F>(
id: String,
label: SharedString,
active: bool,
colors: gpui_native_theme::ColorTokens,
radius: gpui_native_theme::Radius,
on_click: F,
) -> impl IntoElement + use<F>
where
F: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
{
div()
.id(SharedString::from(id))
.px_3()
.py_1()
.rounded(radius.full)
.border_1()
.text_xs()
.cursor_pointer()
.when(active, |el| {
el.bg(colors.primary)
.border_color(colors.primary)
.text_color(colors.primary_foreground)
})
.when(!active, |el| {
el.bg(colors.secondary)
.border_color(colors.border)
.text_color(colors.secondary_foreground)
.hover(|el| el.bg(colors.muted))
})
.on_click(move |event, window, cx| on_click(event, window, cx))
.child(label)
}
fn main() {
application().with_assets(IconAssets).run(|cx: &mut App| {
gpui_native_core::init(cx);
let input_state = InputState::new(cx);
let password_state = InputState::new(cx);
password_state.update(cx, |state, cx| state.set_password(true, cx));
let notes_state = InputState::new(cx);
notes_state.update(cx, |state, cx| state.set_multiline(true, cx));
let layout_state = LayoutState::new(cx);
let dock_state = DockState::new(cx);
dock_state.update(cx, |dock, cx| {
dock.register_panel(
PanelDescriptor::new("overview", "Overview", |_, cx| {
cx.new(|_| DemoPanel {
title: "Overview",
body: "View state survives tab switches and pane moves.",
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("inbox", "Inbox", |_, cx| {
cx.new(|_| DemoPanel {
title: "Inbox",
body: "Right-click the tab bar for Close, Close Others, Close Pane.",
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("settings", "Settings", |_, cx| {
cx.new(|_| DemoPanel {
title: "Settings",
body: "Drag the seam between panes; double-click it to reset.",
})
.into()
}),
cx,
);
});
let bounds = Bounds::centered(None, size(px(720.0), px(560.0)), cx);
if let Err(err) = cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(gpui::TitlebarOptions {
title: Some("gpui-native-kit gallery".into()),
..Default::default()
}),
..Default::default()
},
|window, cx| {
Theme::sync_from_window(window, cx);
Theme::sync_window_rem_size(window, cx);
// Detached on purpose: the subscription lives as long as the window.
window
.observe_window_appearance(|window, cx| {
Theme::sync_from_window(window, cx);
Theme::sync_window_rem_size(window, cx);
cx.refresh_windows();
})
.detach();
dock_state.update(cx, |dock, cx| dock.open_panel("overview", window, cx));
let toasts = cx.new(|_| Toasts::default());
let alerts = cx.new(|_| Alerts::default());
cx.new(|cx| {
// Toast pushes and dismissals repaint the stack.
cx.observe(&toasts, |_, _, cx| cx.notify()).detach();
// Alert pushes and dismissals repaint it.
cx.observe(&alerts, |_, _, cx| cx.notify()).detach();
Gallery {
button_clicks: 0,
checkbox_checked: false,
switch_checked: true,
plan: SharedString::from("starter"),
toggle_pressed: false,
volume: 40.0,
input_state,
password_state,
notes_state,
split_width: px(200.),
layout_state,
dock_state,
shell_selection: None,
popover_open: false,
menu_open: false,
strategy: Some(Strategy::Curated),
explore_filter: ExploreFilter::All,
context_action: "Right-click the card".into(),
dialog_open: false,
alert_open: false,
update_alert_dismissed: false,
alerts,
toasts,
scroll: gpui::ScrollHandle::new(),
list_selected: None,
tree_selected: None,
tab_selected: 0,
card_tabs: vec![
SharedString::from("Explorer"),
SharedString::from("Syncing"),
SharedString::from("Blame"),
],
card_tab_selected: 0,
sidebar_selected: None,
tree_nodes: vec![
GalleryNode {
label: "Workspace".into(),
icon: Some(IconRole::Folder),
expanded: true,
children: vec![GalleryNode {
label: "gpui-native-kit".into(),
icon: Some(IconRole::Folder),
expanded: true,
children: vec![
GalleryNode {
label: "crates".into(),
icon: Some(IconRole::Folder),
expanded: true,
children: vec![
GalleryNode {
label: "theme".into(),
icon: None,
expanded: false,
children: Vec::new(),
},
GalleryNode {
label: "core".into(),
icon: None,
expanded: false,
children: Vec::new(),
},
GalleryNode {
label: "data".into(),
icon: None,
expanded: false,
children: Vec::new(),
},
],
},
GalleryNode {
label: "docs".into(),
icon: Some(IconRole::Folder),
expanded: false,
children: Vec::new(),
},
GalleryNode {
label: "Cargo.toml".into(),
icon: None,
expanded: false,
children: Vec::new(),
},
],
}],
},
GalleryNode {
label: "Archive.zip".into(),
icon: None,
expanded: false,
children: Vec::new(),
},
],
}
})
},
) {
eprintln!("failed to open gallery window: {err}");
}
cx.activate(true);
});
}