Files
gpui-native-kit/examples/gallery/src/main.rs
T

1409 lines
62 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 _, 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::dialog::Dialog;
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::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};
use gpui_native_data::tree::{Tree, TreeItem};
use gpui_native_icons::{IconAssets, IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, 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>,
popover_open: bool,
menu_open: bool,
strategy: Option<Strategy>,
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,
sidebar_selected: Option<usize>,
}
/// The select section's demo value.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Strategy {
Curated,
Uncensored,
}
/// 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 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(
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(
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(
"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(
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(
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 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();
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,
popover_open: false,
menu_open: false,
strategy: Some(Strategy::Curated),
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,
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);
});
}