3522 lines
131 KiB
Rust
3522 lines
131 KiB
Rust
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(¬es, |_, _, 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);
|
||
});
|
||
}
|