2848 lines
106 KiB
Rust
2848 lines
106 KiB
Rust
use std::time::Duration;
|
|
|
|
use gpui::prelude::FluentBuilder as _;
|
|
use gpui::{
|
|
App, AppContext as _, Bounds, Context, Entity, FontWeight, IntoElement, ParentElement as _,
|
|
Pixels, Render, SharedString, Styled as _, WeakEntity, Window, WindowBounds, WindowOptions,
|
|
div, px, size,
|
|
};
|
|
use gpui_native_core::alert::{Alert, AlertSeverity, Alerts, AlertsView};
|
|
use gpui_native_core::avatar::Avatar;
|
|
use gpui_native_core::badge::{Badge, BadgeVariant};
|
|
use gpui_native_core::button::{Button, ButtonVariant};
|
|
use gpui_native_core::checkbox::Checkbox;
|
|
use gpui_native_core::combobox::{Combobox, ComboboxOption, ComboboxState};
|
|
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, InputStateEvent};
|
|
use gpui_native_core::link::Link;
|
|
use gpui_native_core::menu::{Menu, MenuItem};
|
|
use gpui_native_core::popover::Popover;
|
|
use gpui_native_core::progress::Progress;
|
|
use gpui_native_core::radio::{Radio, RadioGroup};
|
|
use gpui_native_core::scroll_area::{ScrollArea, ScrollAxes};
|
|
use gpui_native_core::segmented::{Segmented, SegmentedOption};
|
|
use gpui_native_core::select::{Select, SelectOption};
|
|
use gpui_native_core::slider::Slider;
|
|
use gpui_native_core::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_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"),
|
|
],
|
|
),
|
|
(
|
|
"Feedback",
|
|
&[
|
|
("alert", "Alert"),
|
|
("toast", "Toast"),
|
|
("dialog", "Dialog"),
|
|
("menu", "Menu"),
|
|
("context-menu", "Context menu"),
|
|
("popover", "Popover"),
|
|
("tooltip", "Tooltip"),
|
|
("empty-state", "Empty state"),
|
|
("scroll-area", "Scroll area"),
|
|
],
|
|
),
|
|
(
|
|
"Layout",
|
|
&[
|
|
("welcome", "Welcome"),
|
|
("sidebar", "Sidebar"),
|
|
("resize-handle", "Resize handle"),
|
|
],
|
|
),
|
|
("Theme", &[("theme", "Theme")]),
|
|
];
|
|
|
|
/// The select demo's value.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum Strategy {
|
|
Curated,
|
|
Uncensored,
|
|
}
|
|
|
|
/// The segmented demo's value.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
enum ExploreFilter {
|
|
#[default]
|
|
All,
|
|
Popular,
|
|
Recent,
|
|
}
|
|
|
|
/// The theme demo's mode choice, flattening `Option<ThemeVariant>`.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
enum ModeChoice {
|
|
#[default]
|
|
System,
|
|
Light,
|
|
Dark,
|
|
}
|
|
|
|
impl ModeChoice {
|
|
fn of(variant: Option<ThemeVariant>) -> Self {
|
|
match variant {
|
|
None => Self::System,
|
|
Some(ThemeVariant::Light) => Self::Light,
|
|
Some(ThemeVariant::Dark) => Self::Dark,
|
|
}
|
|
}
|
|
|
|
fn variant(self) -> Option<ThemeVariant> {
|
|
match self {
|
|
Self::System => None,
|
|
Self::Light => Some(ThemeVariant::Light),
|
|
Self::Dark => Some(ThemeVariant::Dark),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The theme demo's preset choice.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
enum PresetChoice {
|
|
#[default]
|
|
FollowSystem,
|
|
TokyoNight,
|
|
}
|
|
|
|
impl PresetChoice {
|
|
fn of(name: Option<&str>) -> Self {
|
|
match name {
|
|
Some("tokyo-night") => Self::TokyoNight,
|
|
_ => Self::FollowSystem,
|
|
}
|
|
}
|
|
|
|
fn name(self) -> Option<&'static str> {
|
|
match self {
|
|
Self::FollowSystem => None,
|
|
Self::TokyoNight => Some("tokyo-night"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The context menu demo's actions.
|
|
const CONTEXT_ACTIONS: [&str; 3] = ["Rename", "Duplicate", "Archive"];
|
|
|
|
struct Gallery {
|
|
layout_state: Entity<LayoutState>,
|
|
dock_state: Entity<DockState>,
|
|
alerts: Entity<Alerts>,
|
|
toasts: Entity<Toasts>,
|
|
dialog_open: bool,
|
|
alert_open: bool,
|
|
}
|
|
|
|
impl Gallery {
|
|
/// Every sidebar entry's panel id, in order. Headers carry `None`: the
|
|
/// sidebar's selection index counts them like any entry.
|
|
fn panel_ids() -> Vec<Option<&'static str>> {
|
|
CATALOG
|
|
.iter()
|
|
.flat_map(|(_, panels)| {
|
|
let mut ids = vec![None];
|
|
ids.extend(panels.iter().map(|(id, _)| Some(*id)));
|
|
ids
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn render_shell(&self, cx: &App) -> Shell {
|
|
let panel_ids = Self::panel_ids();
|
|
let selected = self
|
|
.dock_state
|
|
.read(cx)
|
|
.active_panel()
|
|
.as_deref()
|
|
.and_then(|active| {
|
|
panel_ids
|
|
.iter()
|
|
.position(|id| id.as_deref() == Some(active))
|
|
});
|
|
|
|
let left_sidebar = Sidebar::new("gallery-sidebar")
|
|
.selected(selected)
|
|
.accessibility_label("Component catalog")
|
|
.entries(|_, _| {
|
|
CATALOG
|
|
.iter()
|
|
.flat_map(|(group, panels)| {
|
|
let mut entries = vec![SidebarEntry::header(*group)];
|
|
entries.extend(panels.iter().map(|(_, label)| SidebarEntry::row(*label)));
|
|
entries
|
|
})
|
|
.collect()
|
|
})
|
|
.on_change({
|
|
let dock_state = self.dock_state.clone();
|
|
move |index, window, cx| {
|
|
if let Some(Some(id)) = index.and_then(|index| panel_ids.get(index)) {
|
|
dock_state.update(cx, |dock, cx| dock.open_panel(id, window, cx));
|
|
}
|
|
}
|
|
});
|
|
|
|
let center = Dock::new("gallery-dock", self.dock_state.clone())
|
|
// The pill bar is the titlebar strip whenever no right sidebar
|
|
// sits to its right.
|
|
.window_controls(!self.layout_state.read(cx).is_right_sidebar_visible())
|
|
.traffic_light_pad(!self.layout_state.read(cx).is_left_sidebar_visible());
|
|
|
|
let right_sidebar = DockSidebar::new("gallery-dock-sidebar", self.dock_state.clone());
|
|
|
|
Shell::new("gallery-shell", self.layout_state.clone())
|
|
.left_sidebar(left_sidebar)
|
|
.center(center)
|
|
.right_sidebar(right_sidebar)
|
|
}
|
|
|
|
fn render_dialog(open: bool, gallery: WeakEntity<Self>) -> Dialog {
|
|
let gallery_change = gallery.clone();
|
|
Dialog::new("gallery-dialog")
|
|
.open(open)
|
|
.title("Delete file?")
|
|
.on_change(move |open, _, cx| {
|
|
gallery_change
|
|
.update(cx, |gallery, cx| {
|
|
gallery.dialog_open = open;
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
})
|
|
.content(|_, _| {
|
|
div()
|
|
.child("\"report-final.pdf\" will be removed permanently.")
|
|
.into_any_element()
|
|
})
|
|
.actions(move |_, _| {
|
|
let gallery_cancel = gallery.clone();
|
|
let gallery_delete = gallery.clone();
|
|
vec![
|
|
Button::new("gallery-dialog-cancel")
|
|
.label("Cancel")
|
|
.variant(ButtonVariant::Secondary)
|
|
.on_click(move |_, _, cx| {
|
|
gallery_cancel
|
|
.update(cx, |gallery, cx| {
|
|
gallery.dialog_open = false;
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
})
|
|
.into_any_element(),
|
|
Button::new("gallery-dialog-delete")
|
|
.label("Delete")
|
|
.variant(ButtonVariant::Destructive)
|
|
.on_click(move |_, _, cx| {
|
|
gallery_delete
|
|
.update(cx, |gallery, cx| {
|
|
gallery.dialog_open = false;
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
})
|
|
.into_any_element(),
|
|
]
|
|
})
|
|
}
|
|
|
|
fn render_alert_dialog(open: bool, gallery: WeakEntity<Self>) -> Dialog {
|
|
let gallery_change = gallery.clone();
|
|
Dialog::new("gallery-alert")
|
|
.open(open)
|
|
.alert(true)
|
|
.title("Sign out of the workspace?")
|
|
.on_change(move |open, _, cx| {
|
|
gallery_change
|
|
.update(cx, |gallery, cx| {
|
|
gallery.alert_open = open;
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
})
|
|
.content(|_, _| {
|
|
div()
|
|
.child("Any running agents will keep working in the background.")
|
|
.into_any_element()
|
|
})
|
|
.actions(move |_, _| {
|
|
let gallery_stay = gallery.clone();
|
|
let gallery_signout = gallery.clone();
|
|
vec![
|
|
Button::new("gallery-alert-stay")
|
|
.label("Stay")
|
|
.variant(ButtonVariant::Secondary)
|
|
.on_click(move |_, _, cx| {
|
|
gallery_stay
|
|
.update(cx, |gallery, cx| {
|
|
gallery.alert_open = false;
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
})
|
|
.into_any_element(),
|
|
Button::new("gallery-alert-signout")
|
|
.label("Sign out")
|
|
.variant(ButtonVariant::Primary)
|
|
.on_click(move |_, _, cx| {
|
|
gallery_signout
|
|
.update(cx, |gallery, cx| {
|
|
gallery.alert_open = false;
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
})
|
|
.into_any_element(),
|
|
]
|
|
})
|
|
}
|
|
|
|
fn register_panels(
|
|
dock: &mut DockState,
|
|
layout_state: Entity<LayoutState>,
|
|
alerts: Entity<Alerts>,
|
|
toasts: Entity<Toasts>,
|
|
gallery: WeakEntity<Self>,
|
|
cx: &mut Context<DockState>,
|
|
) {
|
|
let dock_state = cx.entity();
|
|
|
|
dock.register_panel(
|
|
PanelDescriptor::new("welcome", "Welcome", {
|
|
let layout_state = layout_state.clone();
|
|
let dock_state = dock_state.clone();
|
|
move |_, cx| {
|
|
cx.new(|_| WelcomePanel {
|
|
layout_state: layout_state.clone(),
|
|
dock_state: dock_state.clone(),
|
|
})
|
|
.into()
|
|
}
|
|
})
|
|
.right_sidebar(|_, cx| {
|
|
cx.new(|_| RightDetails {
|
|
title: "Welcome",
|
|
body: "Panels can declare this view; activating one opens the sidebar.",
|
|
})
|
|
.into()
|
|
}),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("button", "Button", |_, cx| {
|
|
cx.new(|_| ButtonDemo { clicks: 0 }).into()
|
|
}),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("input", "Input", |_, cx| cx.new(InputDemo::new).into()),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("field", "Field", |_, cx| cx.new(FieldDemo::new).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", {
|
|
move |_, cx| {
|
|
cx.new(|_| DialogDemo {
|
|
gallery: gallery.clone(),
|
|
})
|
|
.into()
|
|
}
|
|
}),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("menu", "Menu", |_, cx| {
|
|
cx.new(|_| MenuDemo { open: false }).into()
|
|
}),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("context-menu", "Context menu", |_, cx| {
|
|
cx.new(|_| ContextMenuDemo {
|
|
action: SharedString::from("Right-click the card"),
|
|
})
|
|
.into()
|
|
}),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("popover", "Popover", |_, cx| {
|
|
cx.new(|_| PopoverDemo { open: false }).into()
|
|
}),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("tooltip", "Tooltip", |_, cx| cx.new(|_| TooltipDemo).into()),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("empty-state", "Empty state", |_, cx| {
|
|
cx.new(|_| EmptyStateDemo).into()
|
|
}),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("scroll-area", "Scroll area", |_, cx| {
|
|
cx.new(|_| ScrollAreaDemo).into()
|
|
}),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("sidebar", "Sidebar", |_, cx| {
|
|
cx.new(|_| SidebarDemo { selected: None }).into()
|
|
}),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("resize-handle", "Resize handle", |_, cx| {
|
|
cx.new(|_| ResizeDemo { width: px(200.) }).into()
|
|
}),
|
|
cx,
|
|
);
|
|
dock.register_panel(
|
|
PanelDescriptor::new("theme", "Theme", |_, cx| cx.new(|_| ThemeDemo).into()),
|
|
cx,
|
|
);
|
|
}
|
|
}
|
|
|
|
impl Render for Gallery {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let theme = cx.theme();
|
|
let gallery = cx.entity().downgrade();
|
|
|
|
// Dialogs are window-modal, so they mount at the window root; the
|
|
// Dialog demo panel only flips these flags.
|
|
let dialog = Self::render_dialog(self.dialog_open, gallery.clone());
|
|
let alert_dialog = Self::render_alert_dialog(self.alert_open, gallery);
|
|
|
|
div()
|
|
.size_full()
|
|
.font_family(".SystemUIFont")
|
|
.bg(theme.colors.background)
|
|
.text_color(theme.colors.foreground)
|
|
.child(self.render_shell(cx))
|
|
.child(dialog)
|
|
.child(alert_dialog)
|
|
.child(AlertsView::new("gallery-alerts", self.alerts.clone()))
|
|
.child(ToastsView::new("gallery-toasts", self.toasts.clone()))
|
|
}
|
|
}
|
|
|
|
/// The dock introduction: how to drive the center dock, plus the shell's
|
|
/// sidebar toggles.
|
|
struct WelcomePanel {
|
|
layout_state: Entity<LayoutState>,
|
|
dock_state: Entity<DockState>,
|
|
}
|
|
|
|
impl Render for WelcomePanel {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let colors = cx.theme().colors;
|
|
let layout_left = self.layout_state.clone();
|
|
let layout_right = self.layout_state.clone();
|
|
let dock_state = self.dock_state.clone();
|
|
|
|
ScrollArea::new("welcome-demo")
|
|
.size_full()
|
|
.p_4()
|
|
.child(
|
|
v_flex()
|
|
.gap_4()
|
|
.max_w(px(560.))
|
|
.child(
|
|
div()
|
|
.text_lg()
|
|
.font_weight(FontWeight::SEMIBOLD)
|
|
.child("gpui-native-kit gallery"),
|
|
)
|
|
.child(
|
|
v_flex()
|
|
.gap_1()
|
|
.text_sm()
|
|
.text_color(colors.muted_foreground)
|
|
.child(div().child("Pick a component in the sidebar — it opens here as a tab."))
|
|
.child(div().child(
|
|
"Drag a tab to another pane's edge to split, or onto a tab bar to move it.",
|
|
))
|
|
.child(div().child(
|
|
"Right-click the tab bar to close a tab, close others, or close the pane.",
|
|
))
|
|
.child(div().child("Seams between panes resize; double-click a seam to reset.")),
|
|
)
|
|
.child(demo_section(
|
|
"layout state",
|
|
colors,
|
|
h_flex()
|
|
.flex_wrap()
|
|
.gap_2()
|
|
.child(
|
|
Button::new("welcome-toggle-left")
|
|
.label("Toggle left sidebar")
|
|
.variant(ButtonVariant::Secondary)
|
|
.small()
|
|
.on_click(move |_, _, cx| {
|
|
layout_left.update(cx, |state, cx| {
|
|
let visible = !state.is_left_sidebar_visible();
|
|
state.set_left_sidebar_visible(visible, cx);
|
|
});
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("welcome-toggle-right")
|
|
.label("Toggle right sidebar")
|
|
.variant(ButtonVariant::Secondary)
|
|
.small()
|
|
.on_click(move |_, _, cx| {
|
|
layout_right.update(cx, |state, cx| {
|
|
let visible = !state.is_right_sidebar_visible();
|
|
state.set_right_sidebar_visible(visible, cx);
|
|
});
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("welcome-split")
|
|
.label("Split active right")
|
|
.variant(ButtonVariant::Secondary)
|
|
.small()
|
|
.on_click(move |_, _, cx| {
|
|
dock_state.update(cx, |dock, cx| {
|
|
if let Some(active) = dock.active_panel() {
|
|
dock.split_panel(&active, SplitDirection::Right, cx);
|
|
}
|
|
});
|
|
}),
|
|
),
|
|
)),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// The right sidebar view a panel declares.
|
|
struct RightDetails {
|
|
title: &'static str,
|
|
body: &'static str,
|
|
}
|
|
|
|
impl Render for RightDetails {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let colors = cx.theme().colors;
|
|
v_flex()
|
|
.size_full()
|
|
.gap_1()
|
|
.p_2()
|
|
.bg(colors.background)
|
|
.child(div().font_weight(FontWeight::MEDIUM).child(self.title))
|
|
.child(
|
|
div()
|
|
.text_sm()
|
|
.text_color(colors.muted_foreground)
|
|
.child(self.body),
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ButtonDemo {
|
|
clicks: usize,
|
|
}
|
|
|
|
impl Render for ButtonDemo {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let view = cx.entity();
|
|
let colors = cx.theme().colors;
|
|
let variants: [(&str, ButtonVariant); 4] = [
|
|
("primary", ButtonVariant::Primary),
|
|
("secondary", ButtonVariant::Secondary),
|
|
("ghost", ButtonVariant::Ghost),
|
|
("destructive", ButtonVariant::Destructive),
|
|
];
|
|
|
|
ScrollArea::new("button-demo").size_full().p_4().child(
|
|
v_flex()
|
|
.gap_4()
|
|
.child(demo_section(
|
|
"variants",
|
|
colors,
|
|
h_flex()
|
|
.flex_wrap()
|
|
.gap_2()
|
|
.children(variants.map(|(name, variant)| {
|
|
let view = view.clone();
|
|
let mut button =
|
|
Button::new(SharedString::from(format!("button-demo-{name}")))
|
|
.label(button_label(variant))
|
|
.variant(variant)
|
|
.on_click(move |_, _, cx| {
|
|
view.update(cx, |demo, cx| {
|
|
demo.clicks += 1;
|
|
cx.notify();
|
|
});
|
|
});
|
|
if variant == ButtonVariant::Destructive {
|
|
button = button.tooltip(Tooltip::text("Removes the selected item"));
|
|
}
|
|
button
|
|
}))
|
|
.child(
|
|
Button::new("button-demo-disabled")
|
|
.label("Disabled")
|
|
.disabled(true),
|
|
)
|
|
.child(
|
|
Button::new("button-demo-icon")
|
|
.label("Open")
|
|
.icon(IconRole::Folder)
|
|
.tooltip(Tooltip::text("Opens the selected file")),
|
|
)
|
|
.child(
|
|
Button::new("button-demo-icon-only")
|
|
.icon(IconRole::Folder)
|
|
.variant(ButtonVariant::Ghost)
|
|
.accessibility_label("Open the selected file")
|
|
.tooltip(Tooltip::text("Opens the selected file")),
|
|
),
|
|
))
|
|
.child(demo_section(
|
|
"sizes",
|
|
colors,
|
|
h_flex()
|
|
.items_center()
|
|
.gap_2()
|
|
.child(Button::new("button-demo-small").label("Small").small())
|
|
.child(Button::new("button-demo-medium").label("Medium"))
|
|
.child(Button::new("button-demo-large").label("Large").large()),
|
|
))
|
|
.child(
|
|
div()
|
|
.text_xs()
|
|
.text_color(colors.muted_foreground)
|
|
.child(format!("clicked {} times", self.clicks)),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
struct InputDemo {
|
|
text: Entity<InputState>,
|
|
password: Entity<InputState>,
|
|
notes: Entity<InputState>,
|
|
}
|
|
|
|
impl InputDemo {
|
|
fn new(cx: &mut Context<Self>) -> Self {
|
|
let text = InputState::new(cx);
|
|
let password = InputState::new(cx);
|
|
password.update(cx, |state, cx| state.set_password(true, cx));
|
|
let notes = InputState::new(cx);
|
|
notes.update(cx, |state, cx| state.set_multiline(true, cx));
|
|
Self {
|
|
text,
|
|
password,
|
|
notes,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Render for InputDemo {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let colors = cx.theme().colors;
|
|
|
|
ScrollArea::new("input-demo").size_full().p_4().child(
|
|
v_flex()
|
|
.gap_3()
|
|
.child(Input::new("input-demo-text", self.text.clone()).placeholder("Type here…"))
|
|
.child(
|
|
Input::new("input-demo-password", self.password.clone())
|
|
.placeholder("Password"),
|
|
)
|
|
.child(
|
|
Input::new("input-demo-notes", self.notes.clone())
|
|
.placeholder("Notes — Enter breaks the line, cmd-Enter submits")
|
|
.rows(4),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_xs()
|
|
.text_color(colors.muted_foreground)
|
|
.child(format!("value: {}", self.text.read(cx).value())),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
struct FieldDemo {
|
|
url: Entity<InputState>,
|
|
name: Entity<InputState>,
|
|
notifications: bool,
|
|
sync_on_open: bool,
|
|
_subscription: gpui::Subscription,
|
|
}
|
|
|
|
impl FieldDemo {
|
|
fn new(cx: &mut Context<Self>) -> Self {
|
|
let url = InputState::new(cx);
|
|
let name = InputState::new_with_text("reya", cx);
|
|
let subscription = cx.subscribe(&url, |_, _, event, cx| {
|
|
if matches!(event, InputStateEvent::TextChanged) {
|
|
cx.notify();
|
|
}
|
|
});
|
|
Self {
|
|
url,
|
|
name,
|
|
notifications: false,
|
|
sync_on_open: true,
|
|
_subscription: subscription,
|
|
}
|
|
}
|
|
}
|
|
|
|
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>,
|
|
}
|
|
|
|
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));
|
|
Self {
|
|
branch,
|
|
label,
|
|
label_created: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
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 MenuDemo {
|
|
open: bool,
|
|
}
|
|
|
|
impl Render for MenuDemo {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let view = cx.entity();
|
|
let colors = cx.theme().colors;
|
|
|
|
ScrollArea::new("menu-demo")
|
|
.size_full()
|
|
.p_4()
|
|
.child(demo_section(
|
|
"trigger and items",
|
|
colors,
|
|
Menu::new("menu-demo-menu")
|
|
.open(self.open)
|
|
.on_change(move |open, _, cx| {
|
|
view.update(cx, |demo, cx| {
|
|
demo.open = open;
|
|
cx.notify();
|
|
});
|
|
})
|
|
.trigger(|_, _| {
|
|
Button::new("menu-demo-trigger")
|
|
.label("Actions")
|
|
.variant(ButtonVariant::Secondary)
|
|
.into_any_element()
|
|
})
|
|
.items(|_, _| {
|
|
vec![
|
|
MenuItem::new("Cut").shortcut("cmd-x").handler(|_, cx| {
|
|
eprintln!("menu: cut");
|
|
let _ = cx;
|
|
}),
|
|
MenuItem::new("Copy").shortcut("cmd-c"),
|
|
MenuItem::new("Paste").shortcut("cmd-v"),
|
|
MenuItem::separator(),
|
|
MenuItem::new("Use quiet mode").checked(true),
|
|
MenuItem::new("Show hidden files"),
|
|
MenuItem::separator(),
|
|
MenuItem::new("Unavailable").disabled(true),
|
|
MenuItem::new("Delete").destructive(true),
|
|
]
|
|
}),
|
|
))
|
|
}
|
|
}
|
|
|
|
struct ContextMenuDemo {
|
|
action: SharedString,
|
|
}
|
|
|
|
impl Render for ContextMenuDemo {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let view = cx.entity();
|
|
let colors = cx.theme().colors;
|
|
|
|
ScrollArea::new("context-menu-demo")
|
|
.size_full()
|
|
.p_4()
|
|
.child(
|
|
v_flex()
|
|
.gap_3()
|
|
.child(demo_section(
|
|
"on any target",
|
|
colors,
|
|
ContextMenu::new("context-menu-demo-menu")
|
|
.target(move |_, cx| {
|
|
let theme = cx.theme();
|
|
div()
|
|
.px_3()
|
|
.py_2()
|
|
.rounded(theme.radius.overlay)
|
|
.border_1()
|
|
.border_color(theme.colors.border)
|
|
.bg(theme.colors.surface)
|
|
.text_size(theme.sizes.medium.text_size)
|
|
.child("Right-click this card")
|
|
.into_any_element()
|
|
})
|
|
.items(move |_, _| {
|
|
CONTEXT_ACTIONS
|
|
.iter()
|
|
.map(|action| {
|
|
let view = view.clone();
|
|
MenuItem::new(*action).handler(move |_, cx| {
|
|
view.update(cx, |demo, cx| {
|
|
demo.action = SharedString::from(*action);
|
|
cx.notify();
|
|
});
|
|
})
|
|
})
|
|
.collect()
|
|
})
|
|
.accessibility_label("Card actions"),
|
|
))
|
|
.child(
|
|
div()
|
|
.text_sm()
|
|
.text_color(colors.muted_foreground)
|
|
.child(self.action.clone()),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
struct PopoverDemo {
|
|
open: bool,
|
|
}
|
|
|
|
impl Render for PopoverDemo {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let view = cx.entity();
|
|
let colors = cx.theme().colors;
|
|
|
|
ScrollArea::new("popover-demo")
|
|
.size_full()
|
|
.p_4()
|
|
.child(demo_section(
|
|
"anchored to its trigger",
|
|
colors,
|
|
Popover::new("popover-demo-popover")
|
|
.open(self.open)
|
|
.on_change(move |open, _, cx| {
|
|
view.update(cx, |demo, cx| {
|
|
demo.open = open;
|
|
cx.notify();
|
|
});
|
|
})
|
|
.trigger(|_, _| {
|
|
Button::new("popover-demo-trigger")
|
|
.label("Options")
|
|
.variant(ButtonVariant::Secondary)
|
|
.into_any_element()
|
|
})
|
|
.content(|_, _| {
|
|
v_flex()
|
|
.gap_1()
|
|
.w(px(240.))
|
|
.p_2()
|
|
.child(
|
|
Checkbox::new("popover-demo-telemetry").label("Enable telemetry"),
|
|
)
|
|
.child(Checkbox::new("popover-demo-crash").label("Send crash reports"))
|
|
.into_any_element()
|
|
}),
|
|
))
|
|
}
|
|
}
|
|
|
|
struct TooltipDemo;
|
|
|
|
impl Render for TooltipDemo {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let colors = cx.theme().colors;
|
|
|
|
ScrollArea::new("tooltip-demo")
|
|
.size_full()
|
|
.p_4()
|
|
.child(demo_section(
|
|
"hover the buttons",
|
|
colors,
|
|
h_flex()
|
|
.gap_2()
|
|
.child(
|
|
Button::new("tooltip-demo-destructive")
|
|
.label("Delete")
|
|
.variant(ButtonVariant::Destructive)
|
|
.tooltip(Tooltip::text("Removes the selected item")),
|
|
)
|
|
.child(
|
|
Button::new("tooltip-demo-icon")
|
|
.label("Open")
|
|
.icon(IconRole::Folder)
|
|
.tooltip(Tooltip::text("Opens the selected file")),
|
|
)
|
|
.child(
|
|
Button::new("tooltip-demo-icon-only")
|
|
.icon(IconRole::Folder)
|
|
.variant(ButtonVariant::Ghost)
|
|
.accessibility_label("Open the selected file")
|
|
.tooltip(Tooltip::text("Opens the selected file")),
|
|
),
|
|
))
|
|
}
|
|
}
|
|
|
|
struct EmptyStateDemo;
|
|
|
|
impl Render for EmptyStateDemo {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let theme = cx.theme();
|
|
let colors = theme.colors;
|
|
let radius = theme.radius;
|
|
|
|
ScrollArea::new("empty-state-demo").size_full().p_4().child(
|
|
v_flex()
|
|
.gap_3()
|
|
.child(demo_section(
|
|
"with action",
|
|
colors,
|
|
div()
|
|
.h(px(180.))
|
|
.rounded(radius.overlay)
|
|
.border_1()
|
|
.border_color(colors.border)
|
|
.flex()
|
|
.child(
|
|
EmptyState::new("No repositories found")
|
|
.icon(IconRole::Search)
|
|
.hint("Try a different name, or publish one first.")
|
|
.action(
|
|
Button::new("empty-state-demo-clear")
|
|
.label("Clear filters")
|
|
.variant(ButtonVariant::Secondary),
|
|
),
|
|
),
|
|
))
|
|
.child(demo_section(
|
|
"bare",
|
|
colors,
|
|
div()
|
|
.h(px(120.))
|
|
.rounded(radius.overlay)
|
|
.border_1()
|
|
.border_color(colors.border)
|
|
.flex()
|
|
.child(EmptyState::new("Nothing here yet")),
|
|
)),
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ScrollAreaDemo;
|
|
|
|
impl Render for ScrollAreaDemo {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let theme = cx.theme();
|
|
let colors = theme.colors;
|
|
let radius = theme.radius;
|
|
let rows = (0..24).map(|row| {
|
|
div()
|
|
.px_3()
|
|
.py_1()
|
|
.border_b_1()
|
|
.border_color(colors.border)
|
|
.child(format!("Row {row} — scroll down to reach it"))
|
|
});
|
|
let wide_rows = (0..12).map(|row| {
|
|
div()
|
|
.flex_none()
|
|
.w(px(760.))
|
|
.px_3()
|
|
.py_1()
|
|
.border_b_1()
|
|
.border_color(colors.border)
|
|
.child(format!("Wide row {row} — scroll right"))
|
|
});
|
|
|
|
ScrollArea::new("scroll-area-demo").size_full().p_4().child(
|
|
v_flex()
|
|
.gap_3()
|
|
.child(demo_section(
|
|
"vertical",
|
|
colors,
|
|
div()
|
|
.h(px(200.))
|
|
.rounded(radius.control)
|
|
.border_1()
|
|
.border_color(colors.border)
|
|
.overflow_hidden()
|
|
.child(
|
|
ScrollArea::new("scroll-area-demo-vertical")
|
|
.size_full()
|
|
.children(rows),
|
|
),
|
|
))
|
|
.child(demo_section(
|
|
"both axes",
|
|
colors,
|
|
div()
|
|
.h(px(160.))
|
|
.rounded(radius.control)
|
|
.border_1()
|
|
.border_color(colors.border)
|
|
.overflow_hidden()
|
|
.child(
|
|
ScrollArea::new("scroll-area-demo-both")
|
|
.size_full()
|
|
.axes(ScrollAxes::Both)
|
|
.child(v_flex().children(wide_rows)),
|
|
),
|
|
)),
|
|
)
|
|
}
|
|
}
|
|
|
|
struct SidebarDemo {
|
|
selected: Option<usize>,
|
|
}
|
|
|
|
impl Render for SidebarDemo {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let view = cx.entity();
|
|
let theme = cx.theme();
|
|
let colors = theme.colors;
|
|
let radius = theme.radius;
|
|
|
|
ScrollArea::new("sidebar-demo").size_full().p_4().child(
|
|
v_flex()
|
|
.gap_3()
|
|
.child(demo_section(
|
|
"standalone; the app's own left sidebar is this component",
|
|
colors,
|
|
div()
|
|
.w(px(280.))
|
|
.h(px(360.))
|
|
.rounded(radius.control)
|
|
.border_1()
|
|
.border_color(colors.border)
|
|
.overflow_hidden()
|
|
.child(
|
|
Sidebar::new("sidebar-demo-list")
|
|
.selected(self.selected)
|
|
.on_change(move |selected, _, cx| {
|
|
view.update(cx, |demo, cx| {
|
|
demo.selected = selected;
|
|
cx.notify();
|
|
});
|
|
})
|
|
.entries(|_, _| {
|
|
vec![
|
|
SidebarEntry::header("Favorites"),
|
|
SidebarEntry::row("Documents").icon(IconRole::Folder),
|
|
SidebarEntry::row("Pictures")
|
|
.icon(IconRole::Folder)
|
|
.detail("24"),
|
|
SidebarEntry::row("Archive").disabled(true),
|
|
SidebarEntry::header("Locations"),
|
|
SidebarEntry::row("iCloud").icon(IconRole::Folder),
|
|
SidebarEntry::row("Trash"),
|
|
]
|
|
}),
|
|
),
|
|
))
|
|
.child(
|
|
div()
|
|
.text_xs()
|
|
.text_color(colors.muted_foreground)
|
|
.child(format!("selected: {:?}", self.selected)),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ResizeDemo {
|
|
width: Pixels,
|
|
}
|
|
|
|
impl Render for ResizeDemo {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let view = cx.entity();
|
|
let view_reset = view.clone();
|
|
let theme = cx.theme();
|
|
let colors = theme.colors;
|
|
let radius = theme.radius;
|
|
let width = self.width;
|
|
|
|
ScrollArea::new("resize-demo").size_full().p_4().child(
|
|
v_flex()
|
|
.gap_3()
|
|
.child(demo_section(
|
|
"seam",
|
|
colors,
|
|
h_flex()
|
|
.h(px(112.))
|
|
.rounded(radius.control)
|
|
.border_1()
|
|
.border_color(colors.border)
|
|
.overflow_hidden()
|
|
.child(
|
|
div()
|
|
.w(width)
|
|
.h_full()
|
|
.flex()
|
|
.items_center()
|
|
.justify_center()
|
|
.bg(colors.muted)
|
|
.child("Sidebar"),
|
|
)
|
|
.child(
|
|
ResizeHandle::new("resize-demo-handle")
|
|
.on_resize(move |delta, _, cx| {
|
|
view.update(cx, |demo, cx| {
|
|
demo.width = (demo.width + delta).clamp(px(96.), px(320.));
|
|
cx.notify();
|
|
});
|
|
})
|
|
.on_reset(move |_, cx| {
|
|
view_reset.update(cx, |demo, cx| {
|
|
demo.width = px(200.);
|
|
cx.notify();
|
|
});
|
|
}),
|
|
)
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.h_full()
|
|
.flex()
|
|
.items_center()
|
|
.justify_center()
|
|
.bg(colors.input)
|
|
.child("Editor"),
|
|
),
|
|
))
|
|
.child(
|
|
div()
|
|
.text_xs()
|
|
.text_color(colors.muted_foreground)
|
|
.child(format!(
|
|
"width: {} px — drag the seam, double-click resets",
|
|
f32::from(width)
|
|
)),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ThemeDemo;
|
|
|
|
impl Render for ThemeDemo {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let theme = cx.theme();
|
|
let colors = theme.colors;
|
|
let radius = theme.radius;
|
|
let mode = ModeChoice::of(mode_override(cx));
|
|
let preset = PresetChoice::of(active_preset(cx));
|
|
|
|
let swatches: [(&str, gpui::Hsla); 18] = [
|
|
("background", colors.background),
|
|
("foreground", colors.foreground),
|
|
("surface", colors.surface),
|
|
("surface_foreground", colors.surface_foreground),
|
|
("primary", colors.primary),
|
|
("primary_foreground", colors.primary_foreground),
|
|
("secondary", colors.secondary),
|
|
("secondary_foreground", colors.secondary_foreground),
|
|
("muted", colors.muted),
|
|
("muted_foreground", colors.muted_foreground),
|
|
("accent", colors.accent),
|
|
("accent_foreground", colors.accent_foreground),
|
|
("destructive", colors.destructive),
|
|
("destructive_foreground", colors.destructive_foreground),
|
|
("border", colors.border),
|
|
("input", colors.input),
|
|
("ring", colors.ring),
|
|
("selection", colors.selection),
|
|
];
|
|
let shades = [
|
|
("base", theme.ramps.primary.base),
|
|
("hover", theme.ramps.primary.hover),
|
|
("pressed", theme.ramps.primary.pressed),
|
|
("subtle", theme.ramps.primary.subtle),
|
|
];
|
|
|
|
ScrollArea::new("theme-demo").size_full().p_4().child(
|
|
v_flex()
|
|
.gap_4()
|
|
.child(demo_section(
|
|
"mode",
|
|
colors,
|
|
Segmented::new("theme-demo-mode", mode)
|
|
.options(vec![
|
|
(ModeChoice::System, "System"),
|
|
(ModeChoice::Light, "Light"),
|
|
(ModeChoice::Dark, "Dark"),
|
|
])
|
|
.on_change(|next, _, cx| set_mode_override(next.variant(), cx)),
|
|
))
|
|
.child(demo_section(
|
|
"preset",
|
|
colors,
|
|
Segmented::new("theme-demo-preset", preset)
|
|
.options(vec![
|
|
(PresetChoice::FollowSystem, "Follow system"),
|
|
(PresetChoice::TokyoNight, "Tokyo Night"),
|
|
])
|
|
.on_change(|next, _, cx| {
|
|
set_preset(next.name(), cx).ok();
|
|
}),
|
|
))
|
|
.child(demo_section(
|
|
"accent shades (derived, OKLCH)",
|
|
colors,
|
|
div()
|
|
.flex()
|
|
.flex_wrap()
|
|
.gap_2()
|
|
.children(shades.map(|(name, color)| {
|
|
h_flex()
|
|
.gap_2()
|
|
.child(
|
|
div()
|
|
.size_6()
|
|
.rounded(radius.full)
|
|
.border_1()
|
|
.border_color(colors.border)
|
|
.bg(color),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_xs()
|
|
.text_color(colors.muted_foreground)
|
|
.child(name),
|
|
)
|
|
})),
|
|
))
|
|
.child(demo_section(
|
|
"color tokens",
|
|
colors,
|
|
div()
|
|
.flex()
|
|
.flex_wrap()
|
|
.gap_2()
|
|
.children(swatches.map(|(name, color)| {
|
|
h_flex()
|
|
.gap_2()
|
|
.child(
|
|
div()
|
|
.size_6()
|
|
.rounded(radius.full)
|
|
.border_1()
|
|
.border_color(colors.border)
|
|
.bg(color),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_xs()
|
|
.text_color(colors.muted_foreground)
|
|
.child(name),
|
|
)
|
|
})),
|
|
))
|
|
.child(demo_section(
|
|
"icons (Lucide fallback; platform resolvers land later)",
|
|
colors,
|
|
div().flex().flex_wrap().items_center().gap_4().children(
|
|
IconRole::all()
|
|
.iter()
|
|
.map(|role| icon(*role).size(px(20.)).color(colors.foreground)),
|
|
),
|
|
)),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn button_label(variant: ButtonVariant) -> &'static str {
|
|
match variant {
|
|
ButtonVariant::Primary => "Primary",
|
|
ButtonVariant::Secondary => "Secondary",
|
|
ButtonVariant::Ghost => "Ghost",
|
|
ButtonVariant::Destructive => "Delete",
|
|
}
|
|
}
|
|
|
|
fn section_label(text: &str, colors: gpui_native_theme::ColorTokens) -> impl IntoElement + use<> {
|
|
div()
|
|
.text_sm()
|
|
.font_weight(FontWeight::MEDIUM)
|
|
.text_color(colors.muted_foreground)
|
|
.child(text.to_string())
|
|
}
|
|
|
|
fn demo_section<B>(
|
|
label: &str,
|
|
colors: gpui_native_theme::ColorTokens,
|
|
body: B,
|
|
) -> impl IntoElement + use<B>
|
|
where
|
|
B: IntoElement,
|
|
{
|
|
v_flex()
|
|
.gap_2()
|
|
.child(section_label(label, colors))
|
|
.child(body)
|
|
}
|
|
|
|
fn main() {
|
|
// 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);
|
|
let layout_state = LayoutState::new(cx);
|
|
let dock_state = DockState::new(cx);
|
|
// A panel that declares a right sidebar view asks for it to open on
|
|
// activation; visibility itself stays on the shell's LayoutState.
|
|
let layout_for_dock = layout_state.clone();
|
|
dock_state.update(cx, |dock, _| {
|
|
dock.on_open_right_sidebar(move |cx| {
|
|
layout_for_dock.update(cx, |state, cx| {
|
|
state.set_right_sidebar_visible(true, cx);
|
|
});
|
|
});
|
|
});
|
|
let bounds = Bounds::centered(None, size(px(960.0), px(640.0)), cx);
|
|
if let Err(err) = cx.open_window(
|
|
WindowOptions {
|
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
|
titlebar: Some(gpui::TitlebarOptions {
|
|
title: Some("gpui-native-kit gallery".into()),
|
|
// The shell's strips are the titlebar; the native one only
|
|
// hosts the traffic lights on macOS.
|
|
appears_transparent: true,
|
|
..Default::default()
|
|
}),
|
|
// The strips move the window themselves, so a tab dragged inside
|
|
// the titlebar band never drags the window with it.
|
|
app_owns_titlebar_drag: true,
|
|
..Default::default()
|
|
},
|
|
|window, cx| {
|
|
Theme::sync_from_window(window, cx);
|
|
Theme::sync_window_rem_size(window, cx);
|
|
// Detached on purpose: the subscription lives as long as the window.
|
|
window
|
|
.observe_window_appearance(|window, cx| {
|
|
Theme::sync_from_window(window, cx);
|
|
Theme::sync_window_rem_size(window, cx);
|
|
cx.refresh_windows();
|
|
})
|
|
.detach();
|
|
let toasts = cx.new(|_| Toasts::default());
|
|
let alerts = cx.new(|_| Alerts::default());
|
|
let gallery = cx.new(|cx| {
|
|
// Toast pushes and dismissals repaint the stack.
|
|
cx.observe(&toasts, |_, _, cx| cx.notify()).detach();
|
|
// Alert pushes and dismissals repaint it.
|
|
cx.observe(&alerts, |_, _, cx| cx.notify()).detach();
|
|
Gallery {
|
|
layout_state: layout_state.clone(),
|
|
dock_state: dock_state.clone(),
|
|
alerts: alerts.clone(),
|
|
toasts: toasts.clone(),
|
|
dialog_open: false,
|
|
alert_open: false,
|
|
}
|
|
});
|
|
dock_state.update(cx, |dock, cx| {
|
|
Gallery::register_panels(
|
|
dock,
|
|
layout_state.clone(),
|
|
alerts.clone(),
|
|
toasts.clone(),
|
|
gallery.downgrade(),
|
|
cx,
|
|
);
|
|
dock.open_panel("welcome", window, cx);
|
|
});
|
|
gallery
|
|
},
|
|
) {
|
|
eprintln!("failed to open gallery window: {err}");
|
|
}
|
|
cx.activate(true);
|
|
});
|
|
}
|