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

2489 lines
92 KiB
Rust

use std::time::Duration;
use gpui::prelude::FluentBuilder as _;
use gpui::{
App, AppContext as _, Bounds, Context, Entity, FontWeight, IntoElement, ParentElement as _,
Pixels, Render, SharedString, Styled as _, WeakEntity, 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 _, 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::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::segmented::{Segmented, SegmentedOption};
use gpui_native_core::select::{Select, SelectOption};
use gpui_native_core::slider::Slider;
use gpui_native_core::switch::Switch;
use gpui_native_core::toast::{Toast, ToastAction, Toasts, ToastsView};
use gpui_native_core::toggle::Toggle;
use gpui_native_core::tooltip::Tooltip;
use gpui_native_data::list::{List, ListItem};
use gpui_native_data::sidebar::{Sidebar, SidebarEntry};
use gpui_native_data::tabs::{Tab, Tabs, TabsVariant};
use gpui_native_data::tree::{Tree, TreeItem};
use gpui_native_icons::{IconAssets, IconRole, icon};
use gpui_native_layout::dock::{Dock, 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"),
("checkbox", "Checkbox"),
("radio", "Radio"),
("switch", "Switch"),
("toggle", "Toggle"),
("slider", "Slider"),
("segmented", "Segmented"),
("select", "Select"),
("link", "Link"),
],
),
(
"Data",
&[
("badge", "Badge"),
("avatar", "Avatar"),
("progress", "Progress"),
("list", "List"),
("tree", "Tree"),
("tabs", "Tabs"),
],
),
(
"Feedback",
&[
("alert", "Alert"),
("toast", "Toast"),
("dialog", "Dialog"),
("menu", "Menu"),
("context-menu", "Context menu"),
("popover", "Popover"),
("tooltip", "Tooltip"),
("empty-state", "Empty state"),
("scroll-area", "Scroll area"),
],
),
(
"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"];
struct Gallery {
layout_state: Entity<LayoutState>,
dock_state: Entity<DockState>,
alerts: Entity<Alerts>,
toasts: Entity<Toasts>,
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>,
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("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("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", {
move |_, cx| {
cx.new(|_| DialogDemo {
gallery: gallery.clone(),
})
.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("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();
// 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")
.bg(theme.colors.background)
.text_color(theme.colors.foreground)
.child(self.render_shell(cx))
.child(dialog)
.child(alert_dialog)
.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>,
}
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));
Self {
text,
password,
notes,
}
}
}
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 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 fall back to initials",
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")
.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()),
))
}
}
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 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; the ring on \"Syncing\" is the Spinner placeholder"
)),
),
)
}
}
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 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)),
),
)),
)
}
}
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() {
application().with_assets(IconAssets).run(|cx: &mut App| {
gpui_native_core::init(cx);
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(),
dialog_open: false,
alert_open: false,
}
});
dock_state.update(cx, |dock, cx| {
Gallery::register_panels(
dock,
layout_state.clone(),
alerts.clone(),
toasts.clone(),
gallery.downgrade(),
cx,
);
dock.open_panel("welcome", window, cx);
});
gallery
},
) {
eprintln!("failed to open gallery window: {err}");
}
cx.activate(true);
});
}