Files
gpui-native-kit/examples/gallery/src/main.rs
T
2026-11-06 08:01:23 +07:00

3522 lines
131 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::time::Duration;
use gpui::prelude::FluentBuilder as _;
use gpui::{
App, AppContext as _, Bounds, Context, Entity, FontWeight, InteractiveElement as _,
IntoElement, KeyBinding, ListAlignment, ListState, ParentElement as _, Pixels, Render,
SharedString, Styled as _, UniformListScrollHandle, WeakEntity, Window, WindowBounds,
WindowOptions, actions, div, list, px, size, uniform_list,
};
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::combobox::{Combobox, ComboboxOption, ComboboxState};
use gpui_native_core::command_palette::{Command, CommandPalette, CommandPaletteState};
use gpui_native_core::common::{Sizable as _, Size, 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::field::{Field, FieldVariant};
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, ScrollAxes};
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarVisibility};
use gpui_native_core::segmented::{Segmented, SegmentedOption};
use gpui_native_core::select::{Select, SelectOption};
use gpui_native_core::slider::Slider;
use gpui_native_core::spinner::{Spinner, SpinnerVariant};
use gpui_native_core::status_dot::{StatusDot, StatusDotVariant};
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_editor::code_view::CodeViewState;
use gpui_native_editor::diff_view::{DiffRowKind, DiffViewState};
use gpui_native_editor::highlight::Language;
use gpui_native_icons::{IconAssets, IconRole, icon};
use gpui_native_layout::dock::{Dock, DockSidebar, DockState, PanelDescriptor, SplitDirection};
use gpui_native_layout::resize_handle::ResizeHandle;
use gpui_native_layout::shell::{LayoutState, Shell};
use gpui_native_theme::{
ActiveTheme as _, Theme, ThemeVariant, active_preset, mode_override, set_mode_override,
set_preset,
};
use gpui_platform::application;
/// The component catalog: (group, [(panel id, sidebar label)]).
const CATALOG: &[(&str, &[(&str, &str)])] = &[
(
"Controls",
&[
("button", "Button"),
("input", "Input"),
("field", "Field"),
("combobox", "Combobox"),
("status-dot", "Status dot"),
("checkbox", "Checkbox"),
("radio", "Radio"),
("switch", "Switch"),
("toggle", "Toggle"),
("slider", "Slider"),
("segmented", "Segmented"),
("select", "Select"),
("link", "Link"),
],
),
(
"Data",
&[
("badge", "Badge"),
("avatar", "Avatar"),
("progress", "Progress"),
("spinner", "Spinner"),
("list", "List"),
("tree", "Tree"),
("tabs", "Tabs"),
],
),
(
"Editor",
&[("code-view", "Code view"), ("diff-view", "Diff view")],
),
(
"Feedback",
&[
("alert", "Alert"),
("toast", "Toast"),
("dialog", "Dialog"),
("command-palette", "Command palette"),
("menu", "Menu"),
("context-menu", "Context menu"),
("popover", "Popover"),
("tooltip", "Tooltip"),
("empty-state", "Empty state"),
("scroll-area", "Scroll area"),
("scrollbar", "Scrollbar"),
],
),
(
"Layout",
&[
("welcome", "Welcome"),
("sidebar", "Sidebar"),
("resize-handle", "Resize handle"),
],
),
("Theme", &[("theme", "Theme")]),
];
/// The select demo's value.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Strategy {
Curated,
Uncensored,
}
/// The segmented demo's value.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum ExploreFilter {
#[default]
All,
Popular,
Recent,
}
/// The theme demo's mode choice, flattening `Option<ThemeVariant>`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum ModeChoice {
#[default]
System,
Light,
Dark,
}
impl ModeChoice {
fn of(variant: Option<ThemeVariant>) -> Self {
match variant {
None => Self::System,
Some(ThemeVariant::Light) => Self::Light,
Some(ThemeVariant::Dark) => Self::Dark,
}
}
fn variant(self) -> Option<ThemeVariant> {
match self {
Self::System => None,
Self::Light => Some(ThemeVariant::Light),
Self::Dark => Some(ThemeVariant::Dark),
}
}
}
/// The theme demo's preset choice.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum PresetChoice {
#[default]
FollowSystem,
TokyoNight,
}
impl PresetChoice {
fn of(name: Option<&str>) -> Self {
match name {
Some("tokyo-night") => Self::TokyoNight,
_ => Self::FollowSystem,
}
}
fn name(self) -> Option<&'static str> {
match self {
Self::FollowSystem => None,
Self::TokyoNight => Some("tokyo-night"),
}
}
}
/// The context menu demo's actions.
const CONTEXT_ACTIONS: [&str; 3] = ["Rename", "Duplicate", "Archive"];
actions!(gallery, [OpenCommandPalette]);
/// The command palette demo's actions.
fn gallery_commands() -> Vec<Command> {
vec![
Command::new("open-settings", "Open Settings").hint("Cmd+,"),
Command::new("toggle-theme", "Toggle Theme").hint("Shift+Cmd+T"),
Command::new("save-all", "Save All").hint("Cmd+Alt+S"),
Command::new("close-tab", "Close Tab").hint("Cmd+W"),
Command::new("reload-window", "Reload Window"),
]
}
struct Gallery {
layout_state: Entity<LayoutState>,
dock_state: Entity<DockState>,
alerts: Entity<Alerts>,
toasts: Entity<Toasts>,
palette: Option<Entity<CommandPaletteState>>,
palette_runs: Vec<SharedString>,
dialog_open: bool,
alert_open: bool,
}
impl Gallery {
/// Every sidebar entry's panel id, in order. Headers carry `None`: the
/// sidebar's selection index counts them like any entry.
fn panel_ids() -> Vec<Option<&'static str>> {
CATALOG
.iter()
.flat_map(|(_, panels)| {
let mut ids = vec![None];
ids.extend(panels.iter().map(|(id, _)| Some(*id)));
ids
})
.collect()
}
fn render_shell(&self, cx: &App) -> Shell {
let panel_ids = Self::panel_ids();
let selected = self
.dock_state
.read(cx)
.active_panel()
.as_deref()
.and_then(|active| {
panel_ids
.iter()
.position(|id| id.as_deref() == Some(active))
});
let left_sidebar = Sidebar::new("gallery-sidebar")
.selected(selected)
.accessibility_label("Component catalog")
.entries(|_, _| {
CATALOG
.iter()
.flat_map(|(group, panels)| {
let mut entries = vec![SidebarEntry::header(*group)];
entries.extend(panels.iter().map(|(_, label)| SidebarEntry::row(*label)));
entries
})
.collect()
})
.on_change({
let dock_state = self.dock_state.clone();
move |index, window, cx| {
if let Some(Some(id)) = index.and_then(|index| panel_ids.get(index)) {
dock_state.update(cx, |dock, cx| dock.open_panel(id, window, cx));
}
}
});
let center = Dock::new("gallery-dock", self.dock_state.clone())
// The pill bar is the titlebar strip whenever no right sidebar
// sits to its right.
.window_controls(!self.layout_state.read(cx).is_right_sidebar_visible())
.traffic_light_pad(!self.layout_state.read(cx).is_left_sidebar_visible());
let right_sidebar = DockSidebar::new("gallery-dock-sidebar", self.dock_state.clone());
Shell::new("gallery-shell", self.layout_state.clone())
.left_sidebar(left_sidebar)
.center(center)
.right_sidebar(right_sidebar)
}
fn render_dialog(open: bool, gallery: WeakEntity<Self>) -> Dialog {
let gallery_change = gallery.clone();
Dialog::new("gallery-dialog")
.open(open)
.title("Delete file?")
.on_change(move |open, _, cx| {
gallery_change
.update(cx, |gallery, cx| {
gallery.dialog_open = open;
cx.notify();
})
.ok();
})
.content(|_, _| {
div()
.child("\"report-final.pdf\" will be removed permanently.")
.into_any_element()
})
.actions(move |_, _| {
let gallery_cancel = gallery.clone();
let gallery_delete = gallery.clone();
vec![
Button::new("gallery-dialog-cancel")
.label("Cancel")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
gallery_cancel
.update(cx, |gallery, cx| {
gallery.dialog_open = false;
cx.notify();
})
.ok();
})
.into_any_element(),
Button::new("gallery-dialog-delete")
.label("Delete")
.variant(ButtonVariant::Destructive)
.on_click(move |_, _, cx| {
gallery_delete
.update(cx, |gallery, cx| {
gallery.dialog_open = false;
cx.notify();
})
.ok();
})
.into_any_element(),
]
})
}
fn render_alert_dialog(open: bool, gallery: WeakEntity<Self>) -> Dialog {
let gallery_change = gallery.clone();
Dialog::new("gallery-alert")
.open(open)
.alert(true)
.title("Sign out of the workspace?")
.on_change(move |open, _, cx| {
gallery_change
.update(cx, |gallery, cx| {
gallery.alert_open = open;
cx.notify();
})
.ok();
})
.content(|_, _| {
div()
.child("Any running agents will keep working in the background.")
.into_any_element()
})
.actions(move |_, _| {
let gallery_stay = gallery.clone();
let gallery_signout = gallery.clone();
vec![
Button::new("gallery-alert-stay")
.label("Stay")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
gallery_stay
.update(cx, |gallery, cx| {
gallery.alert_open = false;
cx.notify();
})
.ok();
})
.into_any_element(),
Button::new("gallery-alert-signout")
.label("Sign out")
.variant(ButtonVariant::Primary)
.on_click(move |_, _, cx| {
gallery_signout
.update(cx, |gallery, cx| {
gallery.alert_open = false;
cx.notify();
})
.ok();
})
.into_any_element(),
]
})
}
fn register_panels(
dock: &mut DockState,
layout_state: Entity<LayoutState>,
alerts: Entity<Alerts>,
toasts: Entity<Toasts>,
palette: Entity<CommandPaletteState>,
gallery: WeakEntity<Self>,
cx: &mut Context<DockState>,
) {
let dock_state = cx.entity();
dock.register_panel(
PanelDescriptor::new("welcome", "Welcome", {
let layout_state = layout_state.clone();
let dock_state = dock_state.clone();
move |_, cx| {
cx.new(|_| WelcomePanel {
layout_state: layout_state.clone(),
dock_state: dock_state.clone(),
})
.into()
}
})
.right_sidebar(|_, cx| {
cx.new(|_| RightDetails {
title: "Welcome",
body: "Panels can declare this view; activating one opens the sidebar.",
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("button", "Button", |_, cx| {
cx.new(|_| ButtonDemo { clicks: 0 }).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("input", "Input", |_, cx| cx.new(InputDemo::new).into()),
cx,
);
dock.register_panel(
PanelDescriptor::new("field", "Field", |window, cx| {
cx.new(|cx| FieldDemo::new(window, cx)).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("combobox", "Combobox", |window, cx| {
cx.new(|cx| ComboboxDemo::new(window, cx)).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("status-dot", "Status dot", |_, cx| {
cx.new(|_| StatusDotDemo).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("checkbox", "Checkbox", |_, cx| {
cx.new(|_| CheckboxDemo { checked: false }).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("radio", "Radio", |_, cx| {
cx.new(|_| RadioDemo {
plan: SharedString::from("starter"),
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("switch", "Switch", |_, cx| {
cx.new(|_| SwitchDemo { checked: true }).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("toggle", "Toggle", |_, cx| {
cx.new(|_| ToggleDemo { pressed: false }).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("slider", "Slider", |_, cx| {
cx.new(|_| SliderDemo { volume: 40.0 }).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("segmented", "Segmented", |_, cx| {
cx.new(|_| SegmentedDemo {
filter: ExploreFilter::default(),
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("select", "Select", |_, cx| {
cx.new(|_| SelectDemo {
strategy: Some(Strategy::Curated),
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("link", "Link", |_, cx| {
cx.new(|_| LinkDemo { clicks: 0 }).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("badge", "Badge", |_, cx| cx.new(|_| BadgeDemo).into()),
cx,
);
dock.register_panel(
PanelDescriptor::new("avatar", "Avatar", |_, cx| cx.new(|_| AvatarDemo).into()),
cx,
);
dock.register_panel(
PanelDescriptor::new("progress", "Progress", |_, cx| {
cx.new(|_| ProgressDemo).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("spinner", "Spinner", |_, cx| cx.new(|_| SpinnerDemo).into()),
cx,
);
dock.register_panel(
PanelDescriptor::new("list", "List", |_, cx| {
cx.new(|_| ListDemo { selected: None }).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("tree", "Tree", |_, cx| {
cx.new(|_| TreeDemo {
nodes: TreeDemo::demo(),
selected: None,
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("tabs", "Tabs", |_, cx| {
cx.new(|_| TabsDemo {
selected: 0,
pill_tabs: vec![
SharedString::from("Explorer"),
SharedString::from("Syncing"),
SharedString::from("Blame"),
],
pill_selected: 0,
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("alert", "Alert", {
let alerts = alerts.clone();
move |_, cx| {
cx.new(|_| AlertDemo {
alerts: alerts.clone(),
dismissed: false,
})
.into()
}
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("toast", "Toast", {
let toasts = toasts.clone();
move |_, cx| {
cx.new(|_| ToastDemo {
toasts: toasts.clone(),
})
.into()
}
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("dialog", "Dialog", {
let gallery = gallery.clone();
move |_, cx| {
cx.new(|_| DialogDemo {
gallery: gallery.clone(),
})
.into()
}
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("command-palette", "Command palette", {
let palette = palette.clone();
move |_, cx| {
cx.new(|cx| CommandPaletteDemo::new(gallery.clone(), palette.clone(), cx))
.into()
}
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("menu", "Menu", |_, cx| {
cx.new(|_| MenuDemo { open: false }).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("context-menu", "Context menu", |_, cx| {
cx.new(|_| ContextMenuDemo {
action: SharedString::from("Right-click the card"),
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("popover", "Popover", |_, cx| {
cx.new(|_| PopoverDemo { open: false }).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("tooltip", "Tooltip", |_, cx| cx.new(|_| TooltipDemo).into()),
cx,
);
dock.register_panel(
PanelDescriptor::new("empty-state", "Empty state", |_, cx| {
cx.new(|_| EmptyStateDemo).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("scroll-area", "Scroll area", |_, cx| {
cx.new(|_| ScrollAreaDemo).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("scrollbar", "Scrollbar", |_, cx| {
cx.new(|_| ScrollbarDemo::new()).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("code-view", "Code view", |_, cx| {
cx.new(CodeViewDemo::new).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("diff-view", "Diff view", |_, cx| {
cx.new(DiffViewDemo::new).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("sidebar", "Sidebar", |_, cx| {
cx.new(|_| SidebarDemo { selected: None }).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("resize-handle", "Resize handle", |_, cx| {
cx.new(|_| ResizeDemo { width: px(200.) }).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("theme", "Theme", |_, cx| cx.new(|_| ThemeDemo).into()),
cx,
);
}
}
impl Render for Gallery {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
let gallery = cx.entity().downgrade();
let palette_for_action = self.palette.clone();
// Dialogs are window-modal, so they mount at the window root; the
// Dialog demo panel only flips these flags.
let dialog = Self::render_dialog(self.dialog_open, gallery.clone());
let alert_dialog = Self::render_alert_dialog(self.alert_open, gallery);
div()
.size_full()
.font_family(".SystemUIFont")
.on_action(move |_: &OpenCommandPalette, window, cx| {
if let Some(palette) = &palette_for_action {
palette.update(cx, |palette, cx| palette.toggle(window, cx));
}
})
.bg(theme.colors.background)
.text_color(theme.colors.foreground)
.child(self.render_shell(cx))
.child(dialog)
.child(alert_dialog)
.when_some(self.palette.clone(), |root, palette| root.child(palette))
.child(AlertsView::new("gallery-alerts", self.alerts.clone()))
.child(ToastsView::new("gallery-toasts", self.toasts.clone()))
}
}
/// The dock introduction: how to drive the center dock, plus the shell's
/// sidebar toggles.
struct WelcomePanel {
layout_state: Entity<LayoutState>,
dock_state: Entity<DockState>,
}
impl Render for WelcomePanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
let layout_left = self.layout_state.clone();
let layout_right = self.layout_state.clone();
let dock_state = self.dock_state.clone();
ScrollArea::new("welcome-demo")
.size_full()
.p_4()
.child(
v_flex()
.gap_4()
.max_w(px(560.))
.child(
div()
.text_lg()
.font_weight(FontWeight::SEMIBOLD)
.child("gpui-native-kit gallery"),
)
.child(
v_flex()
.gap_1()
.text_sm()
.text_color(colors.muted_foreground)
.child(div().child("Pick a component in the sidebar — it opens here as a tab."))
.child(div().child(
"Drag a tab to another pane's edge to split, or onto a tab bar to move it.",
))
.child(div().child(
"Right-click the tab bar to close a tab, close others, or close the pane.",
))
.child(div().child("Seams between panes resize; double-click a seam to reset.")),
)
.child(demo_section(
"layout state",
colors,
h_flex()
.flex_wrap()
.gap_2()
.child(
Button::new("welcome-toggle-left")
.label("Toggle left sidebar")
.variant(ButtonVariant::Secondary)
.small()
.on_click(move |_, _, cx| {
layout_left.update(cx, |state, cx| {
let visible = !state.is_left_sidebar_visible();
state.set_left_sidebar_visible(visible, cx);
});
}),
)
.child(
Button::new("welcome-toggle-right")
.label("Toggle right sidebar")
.variant(ButtonVariant::Secondary)
.small()
.on_click(move |_, _, cx| {
layout_right.update(cx, |state, cx| {
let visible = !state.is_right_sidebar_visible();
state.set_right_sidebar_visible(visible, cx);
});
}),
)
.child(
Button::new("welcome-split")
.label("Split active right")
.variant(ButtonVariant::Secondary)
.small()
.on_click(move |_, _, cx| {
dock_state.update(cx, |dock, cx| {
if let Some(active) = dock.active_panel() {
dock.split_panel(&active, SplitDirection::Right, cx);
}
});
}),
),
)),
)
}
}
/// The right sidebar view a panel declares.
struct RightDetails {
title: &'static str,
body: &'static str,
}
impl Render for RightDetails {
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_2()
.bg(colors.background)
.child(div().font_weight(FontWeight::MEDIUM).child(self.title))
.child(
div()
.text_sm()
.text_color(colors.muted_foreground)
.child(self.body),
)
}
}
struct ButtonDemo {
clicks: usize,
}
impl Render for ButtonDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
let variants: [(&str, ButtonVariant); 4] = [
("primary", ButtonVariant::Primary),
("secondary", ButtonVariant::Secondary),
("ghost", ButtonVariant::Ghost),
("destructive", ButtonVariant::Destructive),
];
ScrollArea::new("button-demo").size_full().p_4().child(
v_flex()
.gap_4()
.child(demo_section(
"variants",
colors,
h_flex()
.flex_wrap()
.gap_2()
.children(variants.map(|(name, variant)| {
let view = view.clone();
let mut button =
Button::new(SharedString::from(format!("button-demo-{name}")))
.label(button_label(variant))
.variant(variant)
.on_click(move |_, _, cx| {
view.update(cx, |demo, cx| {
demo.clicks += 1;
cx.notify();
});
});
if variant == ButtonVariant::Destructive {
button = button.tooltip(Tooltip::text("Removes the selected item"));
}
button
}))
.child(
Button::new("button-demo-disabled")
.label("Disabled")
.disabled(true),
)
.child(
Button::new("button-demo-icon")
.label("Open")
.icon(IconRole::Folder)
.tooltip(Tooltip::text("Opens the selected file")),
)
.child(
Button::new("button-demo-icon-only")
.icon(IconRole::Folder)
.variant(ButtonVariant::Ghost)
.accessibility_label("Open the selected file")
.tooltip(Tooltip::text("Opens the selected file")),
),
))
.child(demo_section(
"sizes",
colors,
h_flex()
.items_center()
.gap_2()
.child(Button::new("button-demo-small").label("Small").small())
.child(Button::new("button-demo-medium").label("Medium"))
.child(Button::new("button-demo-large").label("Large").large()),
))
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("clicked {} times", self.clicks)),
),
)
}
}
struct InputDemo {
text: Entity<InputState>,
password: Entity<InputState>,
notes: Entity<InputState>,
_subscriptions: Vec<gpui::Subscription>,
}
impl InputDemo {
fn new(cx: &mut Context<Self>) -> Self {
let text = InputState::new(cx);
let password = InputState::new(cx);
password.update(cx, |state, cx| state.set_password(true, cx));
let notes = InputState::new(cx);
notes.update(cx, |state, cx| state.set_multiline(true, cx));
// The panel is a cached view in the dock: the value readouts re-render
// only when the inputs it reads are notified.
let subscriptions = vec![
cx.observe(&text, |_, _, cx| cx.notify()),
cx.observe(&password, |_, _, cx| cx.notify()),
cx.observe(&notes, |_, _, cx| cx.notify()),
];
Self {
text,
password,
notes,
_subscriptions: subscriptions,
}
}
}
impl Render for InputDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
ScrollArea::new("input-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(Input::new("input-demo-text", self.text.clone()).placeholder("Type here…"))
.child(
Input::new("input-demo-password", self.password.clone())
.placeholder("Password"),
)
.child(
Input::new("input-demo-notes", self.notes.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.text.read(cx).value())),
),
)
}
}
struct FieldDemo {
url: Entity<InputState>,
name: Entity<InputState>,
notifications: bool,
sync_on_open: bool,
_subscriptions: (gpui::Subscription, gpui::Subscription),
}
impl FieldDemo {
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
let url = InputState::new(cx);
let name = InputState::new_with_text("reya", cx);
// The panel is a cached view in the dock: it re-renders only when the
// entities it reads are notified. Both inputs are read at render time
// (validation, error text), so every change must bust the cache.
let url_observation = cx.observe(&url, |_, _, cx| cx.notify());
let name_observation = cx.observe(&name, |_, _, cx| cx.notify());
Self {
url,
name,
notifications: false,
sync_on_open: true,
_subscriptions: (url_observation, name_observation),
}
}
}
impl Render for FieldDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
let url_error = self.url.read(cx).value().is_empty().then_some("Required");
let url_error_render = url_error.map(SharedString::from);
ScrollArea::new("field-demo")
.size_full()
.p_4()
.child(demo_section(
"block",
colors,
v_flex()
.gap_3()
.child(
Field::new("field-demo-url", "Server URL")
.description("Where your patches are sent.")
.error(url_error_render)
.child(Input::new("field-demo-url-input", self.url.clone())),
)
.child(
Field::new("field-demo-name", "Display name")
.description("Shown next to your commits.")
.child(Input::new("field-demo-name-input", self.name.clone())),
),
))
.child(demo_section(
"row",
colors,
v_flex()
.gap_3()
.child(
Field::new("field-demo-notifications", "Desktop notifications")
.description("Tell me when a push lands.")
.variant(FieldVariant::Row)
.child(
Switch::new("field-demo-notifications-switch")
.checked(self.notifications)
.on_change({
let view = view.clone();
move |checked, _, cx| {
view.update(cx, |demo, cx| {
demo.notifications = checked;
cx.notify();
});
}
}),
),
)
.child(
Field::new("field-demo-sync", "Sync on open")
.variant(FieldVariant::Row)
.child(
Switch::new("field-demo-sync-switch")
.checked(self.sync_on_open)
.on_change({
let view = view.clone();
move |checked, _, cx| {
view.update(cx, |demo, cx| {
demo.sync_on_open = checked;
cx.notify();
});
}
}),
),
),
))
}
}
struct StatusDotDemo;
impl Render for StatusDotDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
let bead = |id: &'static str, variant: StatusDotVariant| {
StatusDot::new(id).variant(variant).label("Sync state")
};
ScrollArea::new("status-dot-demo")
.size_full()
.p_4()
.child(demo_section(
"variants",
colors,
h_flex()
.gap_3()
.child(bead("dot-demo-neutral", StatusDotVariant::Neutral))
.child(bead("dot-demo-accent", StatusDotVariant::Accent))
.child(bead("dot-demo-success", StatusDotVariant::Success))
.child(bead("dot-demo-warning", StatusDotVariant::Warning))
.child(bead("dot-demo-danger", StatusDotVariant::Danger)),
))
.child(demo_section(
"sizes",
colors,
h_flex()
.items_center()
.gap_3()
.child(StatusDot::new("dot-demo-small").small().label("Sync state"))
.child(StatusDot::new("dot-demo-medium").label("Sync state"))
.child(StatusDot::new("dot-demo-large").large().label("Sync state")),
))
.child(demo_section(
"on rows",
colors,
v_flex()
.gap_2()
.child(
h_flex()
.items_center()
.gap_2()
.child(bead("dot-demo-row-sync", StatusDotVariant::Success))
.child("reya/signed — synced"),
)
.child(
h_flex()
.items_center()
.gap_2()
.child(bead("dot-demo-row-drift", StatusDotVariant::Warning))
.child("reya/gpui-native-kit — 2 commits drifting"),
)
.child(
h_flex()
.items_center()
.gap_2()
.child(bead("dot-demo-row-fail", StatusDotVariant::Danger))
.child("reya/notes — push failed"),
),
))
}
}
fn combo_option(label: &'static str) -> ComboboxOption<SharedString> {
(SharedString::from(label), SharedString::from(label))
}
struct ComboboxDemo {
branch: Entity<ComboboxState<SharedString>>,
label: Entity<ComboboxState<SharedString>>,
label_created: Option<SharedString>,
_subscriptions: Vec<gpui::Subscription>,
}
impl ComboboxDemo {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let view = cx.entity();
let branch = cx.new(|cx| {
ComboboxState::new(
Combobox::new(
"combo-demo-branch",
"Branch",
vec![
combo_option("main"),
combo_option("next"),
combo_option("release/1.0"),
combo_option("release/2.0"),
],
)
.selected(SharedString::from("main"))
.placeholder("Switch branch…")
.keep_unmatched_text(),
window,
cx,
)
});
let label_builder = Combobox::new(
"combo-demo-label",
"Label",
vec![
combo_option("bug"),
combo_option("docs"),
combo_option("enhancement"),
],
)
.placeholder("Pick or type a label…")
.on_create(move |text: SharedString, window, cx| {
// Push the typed text as a real option and choose it: the picker
// never mutates its own list behind the caller's back, so the
// demo does it here.
view.update(cx, |demo, cx| {
demo.label_created = Some(text.clone());
demo.label.update(cx, |label, cx| {
let mut options = label.options_snapshot();
if !options.iter().any(|(value, _)| value == &text) {
options.push((text.clone(), text.clone()));
label.set_options(options, cx);
}
label.set_selected(Some(text), window, cx);
});
});
});
let label = cx.new(|cx| ComboboxState::new(label_builder, window, cx));
// The panel is a cached view in the dock: the value readouts re-render
// only when the picker states it reads are notified.
let subscriptions = vec![
cx.observe(&branch, |_, _, cx| cx.notify()),
cx.observe(&label, |_, _, cx| cx.notify()),
];
Self {
branch,
label,
label_created: None,
_subscriptions: subscriptions,
}
}
}
impl Render for ComboboxDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
let branch_value = self
.branch
.read(cx)
.selected()
.map(|value| value.to_string())
.unwrap_or_else(|| "none".to_string());
let label_value = self
.label_created
.clone()
.unwrap_or_else(|| SharedString::from("none"));
ScrollArea::new("combobox-demo")
.size_full()
.p_4()
.child(demo_section(
"keep unmatched",
colors,
v_flex()
.gap_2()
.child(
Field::new("combo-demo-branch-field", "Branch")
.description("Type a name that is not in the list to keep it.")
.child(self.branch.clone()),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("branch: {branch_value}")),
),
))
.child(demo_section(
"create on close",
colors,
v_flex()
.gap_2()
.child(
Field::new("combo-demo-label-field", "Label")
.description("Typing a new label creates it when the field blurs.")
.child(self.label.clone()),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("label: {label_value}")),
),
))
}
}
struct CheckboxDemo {
checked: bool,
}
impl Render for CheckboxDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("checkbox-demo")
.size_full()
.p_4()
.child(demo_section(
"states",
colors,
v_flex()
.gap_2()
.child(
Checkbox::new("checkbox-demo-telemetry")
.label("Enable telemetry")
.checked(self.checked)
.on_change(move |checked, _, cx| {
view.update(cx, |demo, cx| {
demo.checked = checked;
cx.notify();
});
}),
)
.child(
Checkbox::new("checkbox-demo-disabled-off")
.label("Disabled unchecked")
.disabled(true),
)
.child(
Checkbox::new("checkbox-demo-disabled-on")
.label("Disabled checked")
.checked(true)
.disabled(true),
),
))
}
}
struct RadioDemo {
plan: SharedString,
}
impl Render for RadioDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("radio-demo")
.size_full()
.p_4()
.child(demo_section(
"plans",
colors,
RadioGroup::new("radio-demo-group")
.selected(Some(self.plan.clone()))
.on_change(move |plan, _, cx| {
view.update(cx, |demo, cx| {
demo.plan = plan;
cx.notify();
});
})
.child(
Radio::new("radio-demo-starter", SharedString::from("starter"))
.label("Starter"),
)
.child(Radio::new("radio-demo-pro", SharedString::from("pro")).label("Pro"))
.child(
Radio::new("radio-demo-team", SharedString::from("team"))
.label("Team")
.disabled(true),
),
))
}
}
struct SwitchDemo {
checked: bool,
}
impl Render for SwitchDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("switch-demo")
.size_full()
.p_4()
.child(demo_section(
"states",
colors,
v_flex()
.gap_2()
.child(
Switch::new("switch-demo-wifi")
.label("Wi-Fi")
.checked(self.checked)
.on_change(move |checked, _, cx| {
view.update(cx, |demo, cx| {
demo.checked = checked;
cx.notify();
});
}),
)
.child(
Switch::new("switch-demo-disabled-off")
.label("Disabled off")
.disabled(true),
)
.child(
Switch::new("switch-demo-disabled-on")
.label("Disabled on")
.checked(true)
.disabled(true),
),
))
}
}
struct ToggleDemo {
pressed: bool,
}
impl Render for ToggleDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("toggle-demo")
.size_full()
.p_4()
.child(demo_section(
"states",
colors,
h_flex()
.gap_2()
.child(
Toggle::new("toggle-demo-bold")
.label("Bold")
.pressed(self.pressed)
.on_change(move |pressed, _, cx| {
view.update(cx, |demo, cx| {
demo.pressed = pressed;
cx.notify();
});
}),
)
.child(
Toggle::new("toggle-demo-disabled")
.label("Disabled")
.pressed(true)
.disabled(true),
),
))
}
}
struct SliderDemo {
volume: f32,
}
impl Render for SliderDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("slider-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"volume",
colors,
Slider::new("slider-demo-volume", self.volume)
.min(0.0)
.max(100.0)
.step(5.0)
.accessibility_label("Volume")
.on_change(move |volume, _, cx| {
view.update(cx, |demo, cx| {
demo.volume = volume;
cx.notify();
});
}),
))
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("volume: {}", self.volume)),
),
)
}
}
struct SegmentedDemo {
filter: ExploreFilter,
}
impl Render for SegmentedDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let view_icons = view.clone();
let view_large = view.clone();
let colors = cx.theme().colors;
ScrollArea::new("segmented-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"sizes",
colors,
v_flex()
.gap_2()
.child(
Segmented::new("segmented-demo-small", self.filter)
.options(vec![
(ExploreFilter::All, "All"),
(ExploreFilter::Popular, "Popular"),
(ExploreFilter::Recent, "Recent"),
])
.on_change(move |next, _, cx| {
view.update(cx, |demo, cx| {
demo.filter = next;
cx.notify();
});
}),
)
.child(
Segmented::new("segmented-demo-medium", self.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)
.on_change(move |next, _, cx| {
view_icons.update(cx, |demo, cx| {
demo.filter = next;
cx.notify();
});
}),
)
.child(
Segmented::new("segmented-demo-large", self.filter)
.options(vec![
(ExploreFilter::All, "All"),
(ExploreFilter::Popular, "Popular"),
(ExploreFilter::Recent, "Recent"),
])
.large()
.on_change(move |next, _, cx| {
view_large.update(cx, |demo, cx| {
demo.filter = next;
cx.notify();
});
}),
),
))
.child({
let label = match self.filter {
ExploreFilter::All => "showing everything",
ExploreFilter::Popular => "showing popular",
ExploreFilter::Recent => "showing recent",
};
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(label)
}),
)
}
}
struct SelectDemo {
strategy: Option<Strategy>,
}
impl Render for SelectDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("select-demo")
.size_full()
.p_4()
.child(demo_section(
"options",
colors,
v_flex()
.gap_3()
.child(
Select::new("select-demo-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| {
view.update(cx, |demo, cx| {
demo.strategy = next;
cx.notify();
});
}),
)
.child(
Select::new("select-demo-size")
.options(vec![
("small", "Small"),
("medium", "Medium"),
("large", "Large"),
])
.selected("medium")
.placeholder("Control size")
.small(),
),
))
}
}
struct LinkDemo {
clicks: usize,
}
impl Render for LinkDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("link-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"hrefs",
colors,
h_flex()
.gap_4()
.child(
Link::new("link-demo-github")
.label("gpui on GitHub")
.href("https://github.com/zed-industries/zed"),
)
.child(
Link::new("link-demo-override")
.label("On-click override")
.href("https://example.com/never-opened")
.on_click(move |_, _, cx| {
view.update(cx, |demo, cx| {
demo.clicks += 1;
cx.notify();
});
}),
)
.child(
Link::new("link-demo-disabled")
.label("Disabled")
.href("https://example.com/disabled")
.disabled(true),
),
))
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("override clicked {} times", self.clicks)),
),
)
}
}
struct BadgeDemo;
impl Render for BadgeDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
ScrollArea::new("badge-demo")
.size_full()
.p_4()
.child(demo_section(
"variants",
colors,
h_flex()
.flex_wrap()
.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()),
))
}
}
struct AvatarDemo;
impl Render for AvatarDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
ScrollArea::new("avatar-demo")
.size_full()
.p_4()
.child(demo_section(
"pictures, with a seeded pixel-art fallback",
colors,
h_flex()
.flex_wrap()
.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"))
.child(
Avatar::new("grace@reya.info")
.name("Grace Hopper")
.fully_rounded(true),
)
.child(
Avatar::new("desk@reya.info")
.name("Help Desk")
.fully_rounded(true)
.large(),
),
))
}
}
struct ProgressDemo;
impl Render for ProgressDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
ScrollArea::new("progress-demo")
.size_full()
.p_4()
.child(demo_section(
"determinite and indeterminate",
colors,
v_flex()
.gap_2()
.child(Progress::new("progress-demo-25", 0.25).accessibility_label("Quarter"))
.child(Progress::new("progress-demo-60", 0.6).accessibility_label("Most"))
.child(Progress::new("progress-demo-90", 0.9).accessibility_label("Nearly"))
.child(
Progress::new("progress-demo-indeterminate", 0.0)
.indeterminate(true)
.accessibility_label("Working"),
),
))
}
}
struct SpinnerDemo;
impl Render for SpinnerDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
ScrollArea::new("spinner-demo")
.size_full()
.p_4()
.child(demo_section(
"spinning and determinate",
colors,
h_flex()
.gap_3()
.child(Spinner::new("spinner-demo-spin"))
.child(Spinner::new("spinner-demo-small").small())
.child(Spinner::new("spinner-demo-large").large())
.child(Spinner::new("spinner-demo-quarter").value(0.25))
.child(Spinner::new("spinner-demo-half").value(0.5))
.child(Spinner::new("spinner-demo-full").value(1.0)),
))
.child(demo_section(
"icon variant",
colors,
h_flex()
.gap_3()
.child(Spinner::new("spinner-demo-icon").variant(SpinnerVariant::Icon))
.child(
Spinner::new("spinner-demo-icon-small")
.variant(SpinnerVariant::Icon)
.small(),
)
.child(
Spinner::new("spinner-demo-icon-held")
.variant(SpinnerVariant::Icon)
.value(0.5),
),
))
}
}
struct ListDemo {
selected: Option<usize>,
}
impl Render for ListDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("list-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"rows",
colors,
List::new("list-demo-list")
.selected(self.selected)
.on_change(move |selected, _, cx| {
view.update(cx, |demo, cx| {
demo.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.selected)),
),
)
}
}
/// A demo model for the tree: the view 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>,
}
struct TreeDemo {
nodes: Vec<GalleryNode>,
selected: Option<usize>,
}
impl TreeDemo {
fn demo() -> Vec<GalleryNode> {
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(),
},
]
}
fn flatten(
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 {
Self::flatten(&node.children, depth + 1, &path, items, paths);
}
}
}
fn toggle(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 {
Self::toggle(&mut node.children, &path[1..]);
}
}
/// The visible-row paths of the current tree state, matching the flat
/// index the tree component reports.
fn paths(nodes: &[GalleryNode]) -> Vec<Vec<usize>> {
let mut items = Vec::new();
let mut paths = Vec::new();
Self::flatten(nodes, 0, &[], &mut items, &mut paths);
paths
}
}
impl Render for TreeDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let view_disclosure = view.clone();
let colors = cx.theme().colors;
let mut items = Vec::new();
let mut paths = Vec::new();
Self::flatten(&self.nodes, 0, &[], &mut items, &mut paths);
ScrollArea::new("tree-demo")
.size_full()
.p_4()
.child(demo_section(
"outline",
colors,
Tree::new("tree-demo-tree")
.selected(self.selected)
.on_change(move |selected, _, cx| {
view.update(cx, |demo, cx| {
demo.selected = selected;
cx.notify();
});
})
.on_disclosure(move |index, _, cx| {
view_disclosure.update(cx, |demo, cx| {
let paths = TreeDemo::paths(&demo.nodes);
if let Some(path) = paths.get(index) {
TreeDemo::toggle(&mut demo.nodes, path);
}
cx.notify();
});
})
.items(move |_, _| items.clone()),
))
}
}
struct TabsDemo {
selected: usize,
pill_tabs: Vec<SharedString>,
pill_selected: usize,
}
impl Render for TabsDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let view_pill = view.clone();
let view_close = view.clone();
let colors = cx.theme().colors;
let pill_count = self.pill_tabs.len();
ScrollArea::new("tabs-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"underline",
colors,
Tabs::new("tabs-demo-underline")
.selected(self.selected)
.on_change(move |selected, _, cx| {
view.update(cx, |demo, cx| {
demo.selected = selected;
cx.notify();
});
})
.tabs(|_, _| {
vec![
Tab::new("Overview"),
Tab::new("Details"),
Tab::new("Settings"),
Tab::new("Locked").disabled(true),
]
}),
))
.child(match self.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(demo_section(
"pill",
colors,
Tabs::new("tabs-demo-pill")
.variant(TabsVariant::Pill)
.selected(self.pill_selected)
.on_change(move |selected, _, cx| {
view_pill.update(cx, |demo, cx| {
demo.pill_selected = selected;
cx.notify();
});
})
.on_close(move |index, _, cx| {
view_close.update(cx, |demo, cx| {
if index < demo.pill_tabs.len() {
demo.pill_tabs.remove(index);
demo.pill_selected = demo
.pill_selected
.min(demo.pill_tabs.len().saturating_sub(1));
cx.notify();
}
});
})
.tabs({
let labels = self.pill_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_count} tabs left; \"Syncing\" spins while it loads"
)),
),
)
}
}
struct AlertDemo {
alerts: Entity<Alerts>,
dismissed: bool,
}
impl Render for AlertDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let alerts_info = self.alerts.clone();
let alerts_warning = self.alerts.clone();
let alerts_clear = self.alerts.clone();
let colors = cx.theme().colors;
ScrollArea::new("alert-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"inline callouts",
colors,
v_flex()
.gap_2()
.child(
Alert::new("alert-demo-update")
.title("Update available")
.message("Version 2.4.0 is ready to install.")
.severity(AlertSeverity::Info),
)
.when(!self.dismissed, |section| {
section.child(
Alert::new("alert-demo-sync")
.title("Sync failed")
.message("Will retry when you're back online.")
.severity(AlertSeverity::Danger)
.on_dismiss(move |cx| {
view.update(cx, |demo, cx| {
demo.dismissed = true;
cx.notify();
});
}),
)
}),
))
.child(demo_section(
"pushed to the window stack",
colors,
h_flex()
.flex_wrap()
.gap_2()
.child(
Button::new("alert-demo-push-info")
.label("Push info")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
alerts_info.update(cx, |alerts, cx| {
alerts.push(
Alert::new("alert-demo-pushed-info")
.title("Backed up")
.message("Everything is in sync.")
.severity(AlertSeverity::Info),
cx,
);
});
}),
)
.child(
Button::new("alert-demo-push-warning")
.label("Push warning")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
alerts_warning.update(cx, |alerts, cx| {
alerts.push(
Alert::new("alert-demo-pushed-warning")
.title("Storage almost full")
.message("1.2 GB remaining on this device.")
.severity(AlertSeverity::Warning),
cx,
);
});
}),
)
.child(
Button::new("alert-demo-clear")
.label("Clear all")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
alerts_clear.update(cx, |alerts, cx| alerts.clear(cx));
}),
),
)),
)
}
}
struct ToastDemo {
toasts: Entity<Toasts>,
}
impl Render for ToastDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let toasts_undo = self.toasts.clone();
let toasts_saved = self.toasts.clone();
let toasts_connection = self.toasts.clone();
let colors = cx.theme().colors;
ScrollArea::new("toast-demo")
.size_full()
.p_4()
.child(demo_section(
"temporary, one at a time",
colors,
h_flex()
.flex_wrap()
.gap_2()
.child(
Button::new("toast-demo-undo")
.label("Deleted (with undo)")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
toasts_undo.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,
);
});
}),
)
.child(
Button::new("toast-demo-saved")
.label("Saved (with icon)")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
toasts_saved.update(cx, |toasts, cx| {
toasts.push(
Toast::new("Workspace saved").icon(IconRole::Check),
cx,
);
});
}),
)
.child(
Button::new("toast-demo-connection")
.label("Connection lost (sticky)")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
toasts_connection.update(cx, |toasts, cx| {
toasts.push(
Toast::new("Connection lost")
.icon(IconRole::Warning)
.duration(Duration::ZERO),
cx,
);
});
}),
),
))
}
}
struct DialogDemo {
gallery: WeakEntity<Gallery>,
}
impl Render for DialogDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let gallery_dialog = self.gallery.clone();
let gallery_alert = self.gallery.clone();
let colors = cx.theme().colors;
ScrollArea::new("dialog-demo")
.size_full()
.p_4()
.child(demo_section(
"window-modal, mounted at the window root",
colors,
h_flex()
.gap_2()
.child(
Button::new("dialog-demo-open")
.label("Delete file…")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
gallery_dialog
.update(cx, |gallery, cx| {
gallery.dialog_open = true;
cx.notify();
})
.ok();
}),
)
.child(
Button::new("dialog-demo-alert")
.label("Sign out…")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| {
gallery_alert
.update(cx, |gallery, cx| {
gallery.alert_open = true;
cx.notify();
})
.ok();
}),
),
))
}
}
struct CommandPaletteDemo {
gallery: WeakEntity<Gallery>,
palette: Entity<CommandPaletteState>,
_subscription: gpui::Subscription,
}
impl CommandPaletteDemo {
fn new(
gallery: WeakEntity<Gallery>,
palette: Entity<CommandPaletteState>,
cx: &mut Context<Self>,
) -> Self {
// The panel is a cached view in the dock: the readouts re-render only
// when the palette state they read is notified.
let subscription = cx.observe(&palette, |_, _, cx| cx.notify());
Self {
gallery,
palette,
_subscription: subscription,
}
}
}
impl Render for CommandPaletteDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
let open = self.palette.read(cx).is_open();
let runs = self
.gallery
.upgrade()
.map(|gallery| gallery.read(cx).palette_runs.clone())
.unwrap_or_default();
let runs = if runs.is_empty() {
"none yet".to_string()
} else {
runs.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(", ")
};
let palette_toggle = self.palette.clone();
ScrollArea::new("command-palette-demo")
.size_full()
.p_4()
.child(demo_section(
"window-modal, mounted at the window root",
colors,
v_flex()
.gap_2()
.child(
Button::new("command-palette-demo-open")
.label("Open command palette")
.variant(ButtonVariant::Secondary)
.on_click(move |_, window, cx| {
palette_toggle.update(cx, |palette, cx| palette.toggle(window, cx));
}),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child("Cmd+K (Ctrl+K elsewhere) opens it anywhere in the window."),
),
))
.child(demo_section(
"runs",
colors,
v_flex()
.gap_1()
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(if open { "open" } else { "closed" }),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("executed: {runs}")),
),
))
}
}
struct MenuDemo {
open: bool,
}
impl Render for MenuDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("menu-demo")
.size_full()
.p_4()
.child(demo_section(
"trigger and items",
colors,
Menu::new("menu-demo-menu")
.open(self.open)
.on_change(move |open, _, cx| {
view.update(cx, |demo, cx| {
demo.open = open;
cx.notify();
});
})
.trigger(|_, _| {
Button::new("menu-demo-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),
]
}),
))
}
}
struct ContextMenuDemo {
action: SharedString,
}
impl Render for ContextMenuDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("context-menu-demo")
.size_full()
.p_4()
.child(
v_flex()
.gap_3()
.child(demo_section(
"on any target",
colors,
ContextMenu::new("context-menu-demo-menu")
.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 view = view.clone();
MenuItem::new(*action).handler(move |_, cx| {
view.update(cx, |demo, cx| {
demo.action = SharedString::from(*action);
cx.notify();
});
})
})
.collect()
})
.accessibility_label("Card actions"),
))
.child(
div()
.text_sm()
.text_color(colors.muted_foreground)
.child(self.action.clone()),
),
)
}
}
struct PopoverDemo {
open: bool,
}
impl Render for PopoverDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("popover-demo")
.size_full()
.p_4()
.child(demo_section(
"anchored to its trigger",
colors,
Popover::new("popover-demo-popover")
.open(self.open)
.on_change(move |open, _, cx| {
view.update(cx, |demo, cx| {
demo.open = open;
cx.notify();
});
})
.trigger(|_, _| {
Button::new("popover-demo-trigger")
.label("Options")
.variant(ButtonVariant::Secondary)
.into_any_element()
})
.content(|_, _| {
v_flex()
.gap_1()
.w(px(240.))
.p_2()
.child(
Checkbox::new("popover-demo-telemetry").label("Enable telemetry"),
)
.child(Checkbox::new("popover-demo-crash").label("Send crash reports"))
.into_any_element()
}),
))
}
}
struct TooltipDemo;
impl Render for TooltipDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
ScrollArea::new("tooltip-demo")
.size_full()
.p_4()
.child(demo_section(
"hover the buttons",
colors,
h_flex()
.gap_2()
.child(
Button::new("tooltip-demo-destructive")
.label("Delete")
.variant(ButtonVariant::Destructive)
.tooltip(Tooltip::text("Removes the selected item")),
)
.child(
Button::new("tooltip-demo-icon")
.label("Open")
.icon(IconRole::Folder)
.tooltip(Tooltip::text("Opens the selected file")),
)
.child(
Button::new("tooltip-demo-icon-only")
.icon(IconRole::Folder)
.variant(ButtonVariant::Ghost)
.accessibility_label("Open the selected file")
.tooltip(Tooltip::text("Opens the selected file")),
),
))
}
}
struct EmptyStateDemo;
impl Render for EmptyStateDemo {
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;
ScrollArea::new("empty-state-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"with action",
colors,
div()
.h(px(180.))
.rounded(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-demo-clear")
.label("Clear filters")
.variant(ButtonVariant::Secondary),
),
),
))
.child(demo_section(
"bare",
colors,
div()
.h(px(120.))
.rounded(radius.overlay)
.border_1()
.border_color(colors.border)
.flex()
.child(EmptyState::new("Nothing here yet")),
)),
)
}
}
struct ScrollAreaDemo;
impl Render for ScrollAreaDemo {
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 rows = (0..24).map(|row| {
div()
.px_3()
.py_1()
.border_b_1()
.border_color(colors.border)
.child(format!("Row {row} — scroll down to reach it"))
});
let wide_rows = (0..12).map(|row| {
div()
.flex_none()
.w(px(760.))
.px_3()
.py_1()
.border_b_1()
.border_color(colors.border)
.child(format!("Wide row {row} — scroll right"))
});
ScrollArea::new("scroll-area-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"vertical",
colors,
div()
.h(px(200.))
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(
ScrollArea::new("scroll-area-demo-vertical")
.size_full()
.children(rows),
),
))
.child(demo_section(
"both axes",
colors,
div()
.h(px(160.))
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(
ScrollArea::new("scroll-area-demo-both")
.size_full()
.axes(ScrollAxes::Both)
.child(v_flex().children(wide_rows)),
),
)),
)
}
}
/// The virtualized lists a [Scrollbar] drives: gpui's `uniform_list` and
/// `list`, whose handles the overlay reads and writes.
struct ScrollbarDemo {
uniform: UniformListScrollHandle,
list: ListState,
visibility: ScrollbarVisibility,
}
impl ScrollbarDemo {
fn new() -> Self {
Self {
uniform: UniformListScrollHandle::new(),
list: ListState::new(64, ListAlignment::Top, px(400.)),
visibility: ScrollbarVisibility::Platform,
}
}
}
impl Render for ScrollbarDemo {
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 list_state = self.list.clone();
let visibility = self.visibility;
let view = cx.entity();
ScrollArea::new("scrollbar-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"visibility",
colors,
Segmented::new("scrollbar-demo-visibility", visibility)
.options(vec![
(ScrollbarVisibility::Platform, "Platform"),
(ScrollbarVisibility::Always, "Always"),
(ScrollbarVisibility::Autohide, "Autohide"),
])
.on_change(move |next, _, cx| {
view.update(cx, |demo, cx| {
demo.visibility = next;
cx.notify();
});
}),
))
.child(demo_section(
"uniform_list",
colors,
div()
.relative()
.h(px(240.))
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(
uniform_list("scrollbar-demo-uniform", 120, move |range, _, _| {
range
.map(|row| {
div()
.h(px(24.))
.px_3()
.flex()
.items_center()
.border_b_1()
.border_color(colors.border)
.child(format!("Row {row} of 120"))
})
.collect()
})
.track_scroll(&self.uniform)
.h_full()
.w_full(),
)
.child(Scrollbar::vertical(self.uniform.clone()).visibility(visibility)),
))
.child(demo_section(
"list",
colors,
div()
.relative()
.h(px(240.))
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(
list(list_state, move |row, _, _| {
div()
.px_3()
.py_1()
.border_b_1()
.border_color(colors.border)
.child(format!("Item {row} — {} lines tall", 1 + row % 3))
.h(px(28. + (row % 3) as f32 * 12.))
.into_any_element()
})
.h_full()
.w_full(),
)
.child(Scrollbar::vertical(self.list.clone()).visibility(visibility)),
))
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child("Platform follows the running OS; scroll or grab the rail to reveal an autohidden thumb."),
),
)
}
}
/// The code view demo's Rust sample.
const RUST_SAMPLE: &str = r#"// The palette is the theme's semantic ramps.
struct Application {
name: String,
ports: Vec<u16>,
}
impl Application {
/// Binds every port, one by one.
fn bind_all(&self) -> Result<(), String> {
for port in &self.ports {
if port > &1024 {
continue;
}
return Err(format!("port {} is privileged", port));
}
Ok(())
}
}
fn main() {
let app = Application {
name: "signed".to_string(),
ports: vec![8080, 443, 9090],
};
println!("{} binds on {:?}", app.name, app.ports);
app.bind_all().unwrap_or_else(|err| eprintln!("{}", err));
}
"#;
/// The code view demo's TypeScript sample.
const TYPESCRIPT_SAMPLE: &str = r#"// A relay flips from stale to fresh as commits land.
interface Relay {
name: string;
commits: number;
fresh: boolean;
}
const STALE_AFTER_MS = 60000;
export function refresh(relays: Relay[]): Relay[] {
return relays.map((relay) => ({
...relay,
fresh: Date.now() - STALE_AFTER_MS > 0,
}));
}
console.log(refresh([]).length); // 0
"#;
/// The code view demo's sample choice.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum CodeSample {
#[default]
Rust,
TypeScript,
/// A generated file with thousands of lines, to test scroll performance.
Large,
}
/// The line count of the large-file sample.
const LARGE_SAMPLE_LINES: usize = 5000;
/// Generates a deterministic pseudo-Rust file of roughly `lines` lines, so
/// scrolling the code view can be exercised on realistic content.
fn large_sample(lines: usize) -> String {
let mut source = String::with_capacity(lines * 48);
let mut index = 0usize;
while source.lines().count() < lines {
let i = index;
source.push_str(&format!(
"// Section {i}: generated for the scroll performance test.\n"
));
source.push_str(&format!("struct Section{i} {{\n"));
source.push_str(" id: u64,\n");
source.push_str(" label: String,\n");
source.push_str(" scores: Vec<f64>,\n");
source.push_str("}\n\n");
source.push_str(&format!("impl Section{i} {{\n"));
source.push_str(" /// A one-line summary of this section.\n");
source.push_str(" fn describe(&self) -> String {\n");
source.push_str(&format!(
" format!(\"section {i}: {{}} at {{:?}}\", self.label, self.scores)\n"
));
source.push_str(" }\n");
source.push_str("}\n\n");
index += 1;
}
source
}
struct CodeViewDemo {
code: Entity<CodeViewState>,
sample: CodeSample,
large_source: SharedString,
_subscription: gpui::Subscription,
}
impl CodeViewDemo {
fn new(cx: &mut Context<Self>) -> Self {
let code =
cx.new(|cx| CodeViewState::new("code-view-demo", RUST_SAMPLE, Language::Rust, cx));
// The panel is a cached view in the dock: it re-renders only when
// the code state it renders is notified.
let subscription = cx.observe(&code, |_, _, cx| cx.notify());
Self {
code,
sample: CodeSample::Rust,
large_source: large_sample(LARGE_SAMPLE_LINES).into(),
_subscription: subscription,
}
}
}
impl Render for CodeViewDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
let radius = cx.theme().radius;
let view = cx.entity();
let line_count = self.code.read(cx).code().lines.len();
ScrollArea::new("code-view-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"language",
colors,
Segmented::new("code-view-demo-sample", self.sample)
.options(vec![
(CodeSample::Rust, "Rust"),
(CodeSample::TypeScript, "TypeScript"),
(CodeSample::Large, "Large file"),
])
.on_change(move |next, _, cx| {
view.update(cx, |demo, cx| {
demo.sample = next;
let (source, language) = match next {
CodeSample::Rust => (RUST_SAMPLE.into(), Language::Rust),
CodeSample::TypeScript => {
(TYPESCRIPT_SAMPLE.into(), Language::TypeScript)
}
CodeSample::Large => {
(demo.large_source.clone(), Language::Rust)
}
};
demo.code
.update(cx, |code, cx| code.set_source(source, language, cx));
});
}),
))
.child(demo_section(
"source",
colors,
div()
.h(px(480.))
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(self.code.clone()),
))
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("{line_count} lines — scroll to test performance")),
),
)
}
}
/// The diff view demo's old version.
const DIFF_OLD: &str = r#"// The relay pool keeps every relay warm.
struct RelayPool {
relays: Vec<Relay>,
timeout: u64,
}
impl RelayPool {
fn new(timeout: u64) -> Self {
Self {
relays: Vec::new(),
timeout,
}
}
fn send(&self, message: &str) -> bool {
message.len() < self.timeout as usize
}
}
"#;
/// The diff view demo's new version.
const DIFF_NEW: &str = r#"// The relay pool keeps every relay warm and retrying.
struct RelayPool {
relays: Vec<Relay>,
timeout: u64,
retries: u8,
}
impl RelayPool {
fn new(timeout: u64) -> Self {
Self {
relays: Vec::new(),
timeout,
retries: 3,
}
}
fn send(&self, message: &str) -> bool {
!message.is_empty() && message.len() < self.timeout as usize
}
}
"#;
struct DiffViewDemo {
diff: Entity<DiffViewState>,
scenario: DiffScenario,
large_old: SharedString,
large_new: SharedString,
_subscription: gpui::Subscription,
}
/// The diff view demo's scenario choice.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum DiffScenario {
#[default]
Small,
/// Thousands of rows with changes sprinkled through, to test scroll
/// performance.
Large,
}
/// The line count of each version in the large-diff sample.
const LARGE_DIFF_LINES: usize = 5000;
/// Generates a matched old/new pair of roughly `lines` lines each, with a
/// change every few lines so the diff has spread-out hunks.
fn large_diff_pair(lines: usize) -> (String, String) {
let mut old = String::with_capacity(lines * 48);
let mut new = String::with_capacity(lines * 48);
let mut index = 0usize;
while old.lines().count() < lines {
let i = index;
let comment = format!("// Handler {i} in the generated dispatch table.\n");
old.push_str(&comment);
new.push_str(&comment);
old.push_str(&format!("fn handle_{i}(payload: &str) -> bool {{\n"));
new.push_str(&format!("fn handle_{i}(payload: &Payload) -> bool {{\n"));
old.push_str(&format!(" payload.len() < {i} + 16\n"));
new.push_str(&format!(
" payload.size() < {i} + 16 && !payload.is_empty()\n"
));
if i.is_multiple_of(5) {
old.push_str(&format!(" log::debug(\"handler {i} ran\");\n"));
}
if i.is_multiple_of(7) {
new.push_str(" metrics::count(\"handle\");\n");
}
old.push_str("}\n\n");
new.push_str("}\n\n");
index += 1;
}
(old, new)
}
impl DiffViewDemo {
fn new(cx: &mut Context<Self>) -> Self {
let diff =
cx.new(|_| DiffViewState::new("diff-view-demo", DIFF_OLD, DIFF_NEW, Language::Rust));
// The panel is a cached view in the dock: it re-renders only when
// the diff state it renders is notified.
let subscription = cx.observe(&diff, |_, _, cx| cx.notify());
let (large_old, large_new) = large_diff_pair(LARGE_DIFF_LINES);
Self {
diff,
scenario: DiffScenario::Small,
large_old: large_old.into(),
large_new: large_new.into(),
_subscription: subscription,
}
}
}
impl Render for DiffViewDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
let radius = cx.theme().radius;
let view = cx.entity();
let rows = self.diff.read(cx).rows();
let added = rows
.iter()
.filter(|row| row.kind == DiffRowKind::Added)
.count();
let removed = rows
.iter()
.filter(|row| row.kind == DiffRowKind::Removed)
.count();
ScrollArea::new("diff-view-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"scenario",
colors,
Segmented::new("diff-view-demo-scenario", self.scenario)
.options(vec![(DiffScenario::Small, "Small"), (DiffScenario::Large, "Large file")])
.on_change(move |next, _, cx| {
view.update(cx, |demo, cx| {
demo.scenario = next;
let (old, new) = match next {
DiffScenario::Small => (DIFF_OLD.into(), DIFF_NEW.into()),
DiffScenario::Large => (
demo.large_old.clone(),
demo.large_new.clone(),
),
};
demo.diff.update(cx, |diff, cx| {
diff.set_versions(old, new, Language::Rust, cx)
});
});
}),
))
.child(demo_section(
"unified, highlighted, virtualized",
colors,
div()
.h(px(480.))
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(self.diff.clone()),
))
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!(
"+{added} −{removed} across {} rows — long lines scroll horizontally, scroll to test performance",
rows.len()
)),
),
)
}
}
struct SidebarDemo {
selected: Option<usize>,
}
impl Render for SidebarDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let theme = cx.theme();
let colors = theme.colors;
let radius = theme.radius;
ScrollArea::new("sidebar-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"standalone; the app's own left sidebar is this component",
colors,
div()
.w(px(280.))
.h(px(360.))
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(
Sidebar::new("sidebar-demo-list")
.selected(self.selected)
.on_change(move |selected, _, cx| {
view.update(cx, |demo, cx| {
demo.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(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("selected: {:?}", self.selected)),
),
)
}
}
struct ResizeDemo {
width: Pixels,
}
impl Render for ResizeDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let view_reset = view.clone();
let theme = cx.theme();
let colors = theme.colors;
let radius = theme.radius;
let width = self.width;
ScrollArea::new("resize-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"seam",
colors,
h_flex()
.h(px(112.))
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(
div()
.w(width)
.h_full()
.flex()
.items_center()
.justify_center()
.bg(colors.muted)
.child("Sidebar"),
)
.child(
ResizeHandle::new("resize-demo-handle")
.on_resize(move |delta, _, cx| {
view.update(cx, |demo, cx| {
demo.width = (demo.width + delta).clamp(px(96.), px(320.));
cx.notify();
});
})
.on_reset(move |_, cx| {
view_reset.update(cx, |demo, cx| {
demo.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 — drag the seam, double-click resets",
f32::from(width)
)),
),
)
}
}
struct ThemeDemo;
impl Render for ThemeDemo {
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 mode = ModeChoice::of(mode_override(cx));
let preset = PresetChoice::of(active_preset(cx));
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),
];
ScrollArea::new("theme-demo").size_full().p_4().child(
v_flex()
.gap_4()
.child(demo_section(
"mode",
colors,
Segmented::new("theme-demo-mode", mode)
.options(vec![
(ModeChoice::System, "System"),
(ModeChoice::Light, "Light"),
(ModeChoice::Dark, "Dark"),
])
.on_change(|next, _, cx| set_mode_override(next.variant(), cx)),
))
.child(demo_section(
"preset",
colors,
Segmented::new("theme-demo-preset", preset)
.options(vec![
(PresetChoice::FollowSystem, "Follow system"),
(PresetChoice::TokyoNight, "Tokyo Night"),
])
.on_change(|next, _, cx| {
set_preset(next.name(), cx).ok();
}),
))
.child(demo_section(
"accent shades (derived, OKLCH)",
colors,
div()
.flex()
.flex_wrap()
.gap_2()
.children(shades.map(|(name, color)| {
h_flex()
.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(demo_section(
"color tokens",
colors,
div()
.flex()
.flex_wrap()
.gap_2()
.children(swatches.map(|(name, color)| {
h_flex()
.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(demo_section(
"icons (Lucide fallback; platform resolvers land later)",
colors,
div().flex().flex_wrap().items_center().gap_4().children(
IconRole::all()
.iter()
.map(|role| icon(*role).size(px(20.)).color(colors.foreground)),
),
)),
)
}
}
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(FontWeight::MEDIUM)
.text_color(colors.muted_foreground)
.child(text.to_string())
}
fn demo_section<B>(
label: &str,
colors: gpui_native_theme::ColorTokens,
body: B,
) -> impl IntoElement + use<B>
where
B: IntoElement,
{
v_flex()
.gap_2()
.child(section_label(label, colors))
.child(body)
}
fn main() {
// Without a client gpui installs a NullHttpClient and remote images —
// the avatar pictures — silently never load.
application()
.with_assets(IconAssets)
.with_http_client(std::sync::Arc::new(reqwest_client::ReqwestClient::new()))
.run(|cx: &mut App| {
gpui_native_core::init(cx);
gpui_native_editor::code_view::bind_code_view_keys(cx);
cx.bind_keys([
KeyBinding::new("cmd-k", OpenCommandPalette, None),
KeyBinding::new("ctrl-k", OpenCommandPalette, None),
]);
let layout_state = LayoutState::new(cx);
let dock_state = DockState::new(cx);
// A panel that declares a right sidebar view asks for it to open on
// activation; visibility itself stays on the shell's LayoutState.
let layout_for_dock = layout_state.clone();
dock_state.update(cx, |dock, _| {
dock.on_open_right_sidebar(move |cx| {
layout_for_dock.update(cx, |state, cx| {
state.set_right_sidebar_visible(true, cx);
});
});
});
let bounds = Bounds::centered(None, size(px(960.0), px(640.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()),
// The shell's strips are the titlebar; the native one only
// hosts the traffic lights on macOS.
appears_transparent: true,
..Default::default()
}),
// The strips move the window themselves, so a tab dragged inside
// the titlebar band never drags the window with it.
app_owns_titlebar_drag: true,
..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());
let gallery = 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 {
layout_state: layout_state.clone(),
dock_state: dock_state.clone(),
alerts: alerts.clone(),
toasts: toasts.clone(),
palette: None,
palette_runs: Vec::new(),
dialog_open: false,
alert_open: false,
}
});
let palette = cx.new(|cx| {
let matcher = |query: &str, commands: &[Command]| {
let needle = query.to_lowercase();
commands
.iter()
.filter(|command| command.label.to_lowercase().contains(&needle))
.cloned()
.collect()
};
let gallery_for_execute = gallery.downgrade();
CommandPaletteState::new(
CommandPalette::new("gallery-palette", gallery_commands(), matcher)
.on_execute(move |command: &Command, _, cx| {
gallery_for_execute
.update(cx, |gallery, cx| {
gallery.palette_runs.push(command.id.clone());
cx.notify();
})
.ok();
}),
window,
cx,
)
});
gallery.update(cx, |gallery, cx| {
gallery.palette = Some(palette.clone());
cx.notify();
});
dock_state.update(cx, |dock, cx| {
Gallery::register_panels(
dock,
layout_state.clone(),
alerts.clone(),
toasts.clone(),
palette.clone(),
gallery.downgrade(),
cx,
);
// TEMPORARY perf probe: open the field panel under the
// autoplay env var so the probe types into a real window.
let first = if std::env::var("GPUI_FIELD_AUTOPLAY").is_ok() {
eprintln!("[field-probe] autoplay: opening field panel");
"field"
} else {
"welcome"
};
dock.open_panel(first, window, cx);
});
gallery
},
) {
eprintln!("failed to open gallery window: {err}");
}
cx.activate(true);
});
}