diff --git a/crates/data/Cargo.toml b/crates/data/Cargo.toml index 6359e65..9077d5d 100644 --- a/crates/data/Cargo.toml +++ b/crates/data/Cargo.toml @@ -18,7 +18,10 @@ gpui = { workspace = true, features = ["test-support"] } default = ["full"] full = ["list", "sidebar", "tabs", "tree"] list = [] -sidebar = [] +sidebar = [ + "dep:gpui-native-core", + "gpui-native-core/scroll_area", +] tabs = [ "dep:gpui-native-core", "gpui-native-core/button", diff --git a/crates/data/src/sidebar.rs b/crates/data/src/sidebar.rs index 9b7016d..4a82026 100644 --- a/crates/data/src/sidebar.rs +++ b/crates/data/src/sidebar.rs @@ -9,6 +9,7 @@ use gpui::{ ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, div, }; +use gpui_native_core::scroll_area::ScrollArea; use gpui_native_icons::{IconRole, icon}; use gpui_native_theme::ActiveTheme as _; use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color}; @@ -168,6 +169,11 @@ impl RenderOnce for Sidebar { .collect(); let focus = gpui_native_utils::focus_handle(&self.id, window, cx); + let scroll = window.with_element_namespace(self.id.clone(), |window| { + window + .use_keyed_state("scroll", cx, |_, _| gpui::ScrollHandle::new()) + .clone() + }); let theme = cx.theme(); let label = self @@ -176,6 +182,7 @@ impl RenderOnce for Sidebar { .unwrap_or_else(|| SharedString::from(self.id.to_string())); let id_text = self.id.to_string(); + let scroll_area_id = (self.id.clone(), "entries"); // Entry paint reads the theme through this borrow. let children: Vec = entries @@ -197,9 +204,9 @@ impl RenderOnce for Sidebar { div() .id(self.id) .debug_selector(move || format!("sidebar-{id_text}")) + .size_full() .flex() .flex_col() - .gap_0p5() .text_size(theme.sizes.medium.text_size) .track_focus(&focus) .focusable() @@ -219,7 +226,13 @@ impl RenderOnce for Sidebar { } } }) - .children(children) + .child( + ScrollArea::new(scroll_area_id) + .handle(scroll.read(cx).clone()) + .h_full() + .gap_0p5() + .children(children), + ) } } diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 9c3da20..c187b64 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -2,16 +2,16 @@ use std::time::Duration; use gpui::prelude::FluentBuilder as _; use gpui::{ - App, AppContext as _, Bounds, ClickEvent, Context, Entity, InteractiveElement as _, - IntoElement, ParentElement as _, Pixels, Render, SharedString, StatefulInteractiveElement as _, - Styled as _, Window, WindowBounds, WindowOptions, div, px, size, + App, AppContext as _, Bounds, Context, Entity, FontWeight, IntoElement, ParentElement as _, + Pixels, Render, SharedString, Styled as _, WeakEntity, Window, WindowBounds, WindowOptions, + div, px, size, }; use gpui_native_core::alert::{Alert, AlertSeverity, Alerts, AlertsView}; use gpui_native_core::avatar::Avatar; use gpui_native_core::badge::{Badge, BadgeVariant}; use gpui_native_core::button::{Button, ButtonVariant}; use gpui_native_core::checkbox::Checkbox; -use gpui_native_core::common::{Sizable as _, h_flex, v_flex}; +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; @@ -21,7 +21,7 @@ use gpui_native_core::menu::{Menu, MenuItem}; use gpui_native_core::popover::Popover; use gpui_native_core::progress::Progress; use gpui_native_core::radio::{Radio, RadioGroup}; -use gpui_native_core::scroll_area::ScrollArea; +use gpui_native_core::scroll_area::{ScrollArea, ScrollAxes}; use gpui_native_core::segmented::{Segmented, SegmentedOption}; use gpui_native_core::select::{Select, SelectOption}; use gpui_native_core::slider::Slider; @@ -37,51 +37,73 @@ 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 _, Size, Theme}; +use gpui_native_theme::{ + ActiveTheme as _, Theme, ThemeVariant, active_preset, mode_override, set_mode_override, + set_preset, +}; use gpui_platform::application; -struct Gallery { - button_clicks: usize, - checkbox_checked: bool, - switch_checked: bool, - plan: SharedString, - toggle_pressed: bool, - volume: f32, - input_state: Entity, - password_state: Entity, - notes_state: Entity, - split_width: Pixels, - layout_state: Entity, - dock_state: Entity, - shell_selection: Option, - popover_open: bool, - menu_open: bool, - strategy: Option, - explore_filter: ExploreFilter, - context_action: SharedString, - dialog_open: bool, - alert_open: bool, - update_alert_dismissed: bool, - alerts: Entity, - toasts: Entity, - scroll: gpui::ScrollHandle, - list_selected: Option, - tree_nodes: Vec, - tree_selected: Option, - tab_selected: usize, - card_tabs: Vec, - card_tab_selected: usize, - sidebar_selected: Option, -} +/// The component catalog: (group, [(panel id, sidebar label)]). +const CATALOG: &[(&str, &[(&str, &str)])] = &[ + ( + "Controls", + &[ + ("button", "Button"), + ("input", "Input"), + ("checkbox", "Checkbox"), + ("radio", "Radio"), + ("switch", "Switch"), + ("toggle", "Toggle"), + ("slider", "Slider"), + ("segmented", "Segmented"), + ("select", "Select"), + ("link", "Link"), + ], + ), + ( + "Data", + &[ + ("badge", "Badge"), + ("avatar", "Avatar"), + ("progress", "Progress"), + ("list", "List"), + ("tree", "Tree"), + ("tabs", "Tabs"), + ], + ), + ( + "Feedback", + &[ + ("alert", "Alert"), + ("toast", "Toast"), + ("dialog", "Dialog"), + ("menu", "Menu"), + ("context-menu", "Context menu"), + ("popover", "Popover"), + ("tooltip", "Tooltip"), + ("empty-state", "Empty state"), + ("scroll-area", "Scroll area"), + ], + ), + ( + "Layout", + &[ + ("welcome", "Welcome"), + ("sidebar", "Sidebar"), + ("resize-handle", "Resize handle"), + ], + ), + ("Theme", &[("theme", "Theme")]), +]; -/// The select section's demo value. +/// The select demo's value. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Strategy { Curated, Uncensored, } -/// The segmented section's demo value. +/// The segmented demo's value. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum ExploreFilter { #[default] @@ -90,196 +112,581 @@ enum ExploreFilter { Recent, } -/// The context menu section's demo actions. +/// The theme demo's mode choice, flattening `Option`. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +enum ModeChoice { + #[default] + System, + Light, + Dark, +} + +impl ModeChoice { + fn of(variant: Option) -> Self { + match variant { + None => Self::System, + Some(ThemeVariant::Light) => Self::Light, + Some(ThemeVariant::Dark) => Self::Dark, + } + } + + fn variant(self) -> Option { + 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"]; -/// A demo model for the tree section: the app owns the real tree and its -/// expansion set, and flattens the visible nodes for the component. -#[derive(Clone)] -struct GalleryNode { - label: SharedString, - icon: Option, - expanded: bool, - children: Vec, -} - -fn flatten_tree( - nodes: &[GalleryNode], - depth: usize, - prefix: &[usize], - items: &mut Vec, - paths: &mut Vec>, -) { - for (index, node) in nodes.iter().enumerate() { - let mut path = prefix.to_vec(); - path.push(index); - - let mut item = TreeItem::new(node.label.clone()) - .depth(depth) - .expandable(!node.children.is_empty()) - .expanded(node.expanded); - if let Some(role) = node.icon { - item = item.icon(role); - } - items.push(item); - paths.push(path.clone()); - - if node.expanded { - flatten_tree(&node.children, depth + 1, &path, items, paths); - } - } -} - -fn toggle_tree_path(nodes: &mut [GalleryNode], path: &[usize]) { - let Some(index) = path.first() else { return }; - let Some(node) = nodes.get_mut(*index) else { - return; - }; - if path.len() == 1 { - node.expanded = !node.expanded; - } else { - toggle_tree_path(&mut node.children, &path[1..]); - } -} - -/// The visible-row paths of the current tree state, matching the flat index -/// the tree component reports. -fn tree_paths(nodes: &[GalleryNode]) -> Vec> { - let mut items = Vec::new(); - let mut paths = Vec::new(); - flatten_tree(nodes, 0, &[], &mut items, &mut paths); - paths +struct Gallery { + layout_state: Entity, + dock_state: Entity, + alerts: Entity, + toasts: Entity, + dialog_open: bool, + alert_open: bool, } impl Gallery { - /// The app shell demo: sidebars around a center region, toggles in the - /// center demonstrating the `LayoutState` API. - fn render_app_shell(&self, gallery: &Entity, cx: &App) -> impl IntoElement + use<> { - let state = self.layout_state.clone(); - let state_for_toggle = state.clone(); + /// 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> { + CATALOG + .iter() + .flat_map(|(_, panels)| { + let mut ids = vec![None]; + ids.extend(panels.iter().map(|(id, _)| Some(*id))); + ids + }) + .collect() + } - let toggle = |side: &'static str| { - let state = state_for_toggle.clone(); - Button::new(SharedString::from(format!("gallery-shell-toggle-{side}"))) - .label(format!("Toggle {}", side)) - .variant(ButtonVariant::Secondary) - .small() - .on_click(move |_, _, cx| { - state.update(cx, |state, cx| match side { - "left" => { - let visible = !state.is_left_sidebar_visible(); - state.set_left_sidebar_visible(visible, cx); - } - _ => { - let visible = !state.is_right_sidebar_visible(); - state.set_right_sidebar_visible(visible, cx); - } - }); - }) - }; + 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-shell-sidebar") - .selected(self.shell_selection) + let left_sidebar = Sidebar::new("gallery-sidebar") + .selected(selected) + .accessibility_label("Component catalog") .entries(|_, _| { - vec![ - SidebarEntry::header("Repositories"), - SidebarEntry::row("gpui-native-kit").icon(IconRole::Folder), - SidebarEntry::row("signed").icon(IconRole::Folder), - SidebarEntry::header("Explore"), - SidebarEntry::row("Popular"), - SidebarEntry::row("Recent"), - ] + 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 gallery = gallery.clone(); - move |selection, _, cx| { - gallery.update(cx, |gallery, _| gallery.shell_selection = selection); + 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 right_sidebar = - DockSidebar::new("gallery-dock-sidebar", self.dock_state.clone()).into_any_element(); + 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 open_panel = |dock: &Entity, id: &'static str, label: &'static str| { - let dock = dock.clone(); - Button::new(SharedString::from(format!("gallery-dock-open-{id}"))) - .label(label) - .variant(ButtonVariant::Secondary) - .small() - .on_click(move |_, window, cx| { - dock.update(cx, |dock, cx| dock.open_panel(id, window, cx)); - }) - }; + let right_sidebar = DockSidebar::new("gallery-dock-sidebar", self.dock_state.clone()); - let center = v_flex() - .size_full() - .gap_2() - .child( - h_flex() - .flex_none() - .flex_wrap() - .gap_2() - .child(toggle("left")) - .child(toggle("right")) - .child(open_panel(&self.dock_state, "inbox", "Open inbox")) - .child(open_panel(&self.dock_state, "settings", "Open settings")) - .child({ - let dock = self.dock_state.clone(); - Button::new("gallery-dock-split") - .label("Split active right") - .variant(ButtonVariant::Secondary) - .small() - .on_click(move |_, _, cx| { - dock.update(cx, |dock, cx| { - if let Some(active) = dock.active_panel() { - dock.split_panel(&active, SplitDirection::Right, cx); - } - }); - }) - }), - ) - .child( - div().flex_1().min_h_0().overflow_hidden().child( - 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(!state.read(cx).is_right_sidebar_visible()) - .traffic_light_pad(!state.read(cx).is_left_sidebar_visible()), - ), - ); - - Shell::new("gallery-shell", state) + 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) -> 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) -> 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, + alerts: Entity, + toasts: Entity, + gallery: WeakEntity, + cx: &mut Context, + ) { + let dock_state = cx.entity(); + + dock.register_panel( + PanelDescriptor::new("welcome", "Welcome", { + let layout_state = layout_state.clone(); + let dock_state = dock_state.clone(); + move |_, cx| { + cx.new(|_| WelcomePanel { + layout_state: layout_state.clone(), + dock_state: dock_state.clone(), + }) + .into() + } + }) + .right_sidebar(|_, cx| { + cx.new(|_| RightDetails { + title: "Welcome", + body: "Panels can declare this view; activating one opens the sidebar.", + }) + .into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("button", "Button", |_, cx| { + cx.new(|_| ButtonDemo { clicks: 0 }).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("input", "Input", |_, cx| cx.new(InputDemo::new).into()), + cx, + ); + dock.register_panel( + PanelDescriptor::new("checkbox", "Checkbox", |_, cx| { + cx.new(|_| CheckboxDemo { checked: false }).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("radio", "Radio", |_, cx| { + cx.new(|_| RadioDemo { + plan: SharedString::from("starter"), + }) + .into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("switch", "Switch", |_, cx| { + cx.new(|_| SwitchDemo { checked: true }).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("toggle", "Toggle", |_, cx| { + cx.new(|_| ToggleDemo { pressed: false }).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("slider", "Slider", |_, cx| { + cx.new(|_| SliderDemo { volume: 40.0 }).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("segmented", "Segmented", |_, cx| { + cx.new(|_| SegmentedDemo { + filter: ExploreFilter::default(), + }) + .into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("select", "Select", |_, cx| { + cx.new(|_| SelectDemo { + strategy: Some(Strategy::Curated), + }) + .into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("link", "Link", |_, cx| { + cx.new(|_| LinkDemo { clicks: 0 }).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("badge", "Badge", |_, cx| cx.new(|_| BadgeDemo).into()), + cx, + ); + dock.register_panel( + PanelDescriptor::new("avatar", "Avatar", |_, cx| cx.new(|_| AvatarDemo).into()), + cx, + ); + dock.register_panel( + PanelDescriptor::new("progress", "Progress", |_, cx| { + cx.new(|_| ProgressDemo).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("list", "List", |_, cx| { + cx.new(|_| ListDemo { selected: None }).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("tree", "Tree", |_, cx| { + cx.new(|_| TreeDemo { + nodes: TreeDemo::demo(), + selected: None, + }) + .into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("tabs", "Tabs", |_, cx| { + cx.new(|_| TabsDemo { + selected: 0, + pill_tabs: vec![ + SharedString::from("Explorer"), + SharedString::from("Syncing"), + SharedString::from("Blame"), + ], + pill_selected: 0, + }) + .into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("alert", "Alert", { + let alerts = alerts.clone(); + move |_, cx| { + cx.new(|_| AlertDemo { + alerts: alerts.clone(), + dismissed: false, + }) + .into() + } + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("toast", "Toast", { + let toasts = toasts.clone(); + move |_, cx| { + cx.new(|_| ToastDemo { + toasts: toasts.clone(), + }) + .into() + } + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("dialog", "Dialog", { + move |_, cx| { + cx.new(|_| DialogDemo { + gallery: gallery.clone(), + }) + .into() + } + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("menu", "Menu", |_, cx| { + cx.new(|_| MenuDemo { open: false }).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("context-menu", "Context menu", |_, cx| { + cx.new(|_| ContextMenuDemo { + action: SharedString::from("Right-click the card"), + }) + .into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("popover", "Popover", |_, cx| { + cx.new(|_| PopoverDemo { open: false }).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("tooltip", "Tooltip", |_, cx| cx.new(|_| TooltipDemo).into()), + cx, + ); + dock.register_panel( + PanelDescriptor::new("empty-state", "Empty state", |_, cx| { + cx.new(|_| EmptyStateDemo).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("scroll-area", "Scroll area", |_, cx| { + cx.new(|_| ScrollAreaDemo).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("sidebar", "Sidebar", |_, cx| { + cx.new(|_| SidebarDemo { selected: None }).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("resize-handle", "Resize handle", |_, cx| { + cx.new(|_| ResizeDemo { width: px(200.) }).into() + }), + cx, + ); + dock.register_panel( + PanelDescriptor::new("theme", "Theme", |_, cx| cx.new(|_| ThemeDemo).into()), + cx, + ); + } } -/// A stand-in panel view for the dock demo. -struct DemoPanel { - title: &'static str, - body: &'static str, +impl Render for Gallery { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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())) + } } -impl Render for DemoPanel { +/// The dock introduction: how to drive the center dock, plus the shell's +/// sidebar toggles. +struct WelcomePanel { + layout_state: Entity, + dock_state: Entity, +} + +impl Render for WelcomePanel { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let colors = cx.theme().colors; - v_flex() + 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() - .gap_1() .p_4() - .bg(colors.background) - .child(div().child(self.title)) .child( - div() - .text_sm() - .text_color(colors.muted_foreground) - .child(self.body), + 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 demo panel declares. +/// The right sidebar view a panel declares. struct RightDetails { title: &'static str, body: &'static str, @@ -293,11 +700,7 @@ impl Render for RightDetails { .gap_1() .p_2() .bg(colors.background) - .child( - div() - .font_weight(gpui::FontWeight::MEDIUM) - .child(self.title), - ) + .child(div().font_weight(FontWeight::MEDIUM).child(self.title)) .child( div() .text_sm() @@ -307,16 +710,1555 @@ impl Render for RightDetails { } } -impl Render for Gallery { +struct ButtonDemo { + clicks: usize, +} + +impl Render for ButtonDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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, + password: Entity, + notes: Entity, +} + +impl InputDemo { + fn new(cx: &mut Context) -> 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) -> impl IntoElement { + let colors = cx.theme().colors; + + ScrollArea::new("input-demo").size_full().p_4().child( + v_flex() + .gap_3() + .child(Input::new("input-demo-text", self.text.clone()).placeholder("Type here…")) + .child( + Input::new("input-demo-password", self.password.clone()) + .placeholder("Password"), + ) + .child( + Input::new("input-demo-notes", self.notes.clone()) + .placeholder("Notes — Enter breaks the line, cmd-Enter submits") + .rows(4), + ) + .child( + div() + .text_xs() + .text_color(colors.muted_foreground) + .child(format!("value: {}", self.text.read(cx).value())), + ), + ) + } +} + +struct CheckboxDemo { + checked: bool, +} + +impl Render for CheckboxDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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, +} + +impl Render for SelectDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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) -> 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) -> 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) -> impl IntoElement { + let colors = cx.theme().colors; + + ScrollArea::new("avatar-demo") + .size_full() + .p_4() + .child(demo_section( + "pictures fall back to initials", + colors, + h_flex() + .flex_wrap() + .gap_3() + .child( + Avatar::new("reya@reya.info") + .name("Reya Adele") + .picture(Some("https://github.com/reya.png")), + ) + .child( + Avatar::new("ada@reya.info") + .name("Ada Lovelace") + .picture(Some("https://example.com/missing.png")), + ) + .child(Avatar::new("grace@reya.info").name("Grace Hopper").small()) + .child(Avatar::new("desk@reya.info").name("Help Desk").large()), + )) + } +} + +struct ProgressDemo; + +impl Render for ProgressDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.theme().colors; + + ScrollArea::new("progress-demo") + .size_full() + .p_4() + .child(demo_section( + "determinite and indeterminate", + colors, + v_flex() + .gap_2() + .child(Progress::new("progress-demo-25", 0.25).accessibility_label("Quarter")) + .child(Progress::new("progress-demo-60", 0.6).accessibility_label("Most")) + .child(Progress::new("progress-demo-90", 0.9).accessibility_label("Nearly")) + .child( + Progress::new("progress-demo-indeterminate", 0.0) + .indeterminate(true) + .accessibility_label("Working"), + ), + )) + } +} + +struct ListDemo { + selected: Option, +} + +impl Render for ListDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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, + expanded: bool, + children: Vec, +} + +struct TreeDemo { + nodes: Vec, + selected: Option, +} + +impl TreeDemo { + fn demo() -> Vec { + 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, + paths: &mut Vec>, + ) { + 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> { + 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) -> 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, + pill_selected: usize, +} + +impl Render for TabsDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let view = cx.entity(); + let view_pill = view.clone(); + let view_close = view.clone(); + let colors = cx.theme().colors; + let pill_count = self.pill_tabs.len(); + + ScrollArea::new("tabs-demo") + .size_full() + .p_4() + .child( + v_flex().gap_3() + .child(demo_section( + "underline", + colors, + Tabs::new("tabs-demo-underline") + .selected(self.selected) + .on_change(move |selected, _, cx| { + view.update(cx, |demo, cx| { + demo.selected = selected; + cx.notify(); + }); + }) + .tabs(|_, _| { + vec![ + Tab::new("Overview"), + Tab::new("Details"), + Tab::new("Settings"), + Tab::new("Locked").disabled(true), + ] + }), + )) + .child(match self.selected { + 0 => div() + .child( + "The overview panel is the app's business — the strip only reports.", + ) + .into_any_element(), + 1 => div() + .child("Details: rows, lists and trees all live under their own tabs.") + .into_any_element(), + _ => div() + .child("Settings: keyboard focus rides the strip; arrows switch tabs.") + .into_any_element(), + }) + .child(demo_section( + "pill", + colors, + Tabs::new("tabs-demo-pill") + .variant(TabsVariant::Pill) + .selected(self.pill_selected) + .on_change(move |selected, _, cx| { + view_pill.update(cx, |demo, cx| { + demo.pill_selected = selected; + cx.notify(); + }); + }) + .on_close(move |index, _, cx| { + view_close.update(cx, |demo, cx| { + if index < demo.pill_tabs.len() { + demo.pill_tabs.remove(index); + demo.pill_selected = demo + .pill_selected + .min(demo.pill_tabs.len().saturating_sub(1)); + cx.notify(); + } + }); + }) + .tabs({ + let labels = self.pill_tabs.clone(); + move |_, _| { + labels + .iter() + .map(|label| { + Tab::new(label.clone()) + .closeable(true) + .busy(label == "Syncing") + }) + .collect() + } + }), + )) + .child( + div() + .text_xs() + .text_color(colors.muted_foreground) + .child(format!( + "{pill_count} tabs left; the ring on \"Syncing\" is the Spinner placeholder" + )), + ), + ) + } +} + +struct AlertDemo { + alerts: Entity, + dismissed: bool, +} + +impl Render for AlertDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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, +} + +impl Render for ToastDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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, +} + +impl Render for DialogDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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) -> 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) -> 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) -> 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) -> 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) -> impl IntoElement { let theme = cx.theme(); let colors = theme.colors; let radius = theme.radius; - let variant = theme.variant; - let source = gpui_native_theme::detected_source(cx); - let mode_override = gpui_native_theme::mode_override(cx); - let active_preset = gpui_native_theme::active_preset(cx); - let gallery = cx.entity(); + + 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) -> 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, +} + +impl Render for SidebarDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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) -> 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) -> 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), @@ -345,1233 +2287,41 @@ impl Render for Gallery { ("subtle", theme.ramps.primary.subtle), ]; - let modes: [(SharedString, Option, bool); 3] = [ - ("System".into(), None, mode_override.is_none()), - ( - "Light".into(), - Some(gpui_native_theme::ThemeVariant::Light), - mode_override == Some(gpui_native_theme::ThemeVariant::Light), - ), - ( - "Dark".into(), - Some(gpui_native_theme::ThemeVariant::Dark), - mode_override == Some(gpui_native_theme::ThemeVariant::Dark), - ), - ]; - let presets: [(SharedString, Option<&'static str>, bool); 2] = [ - ("Follow system".into(), None, active_preset.is_none()), - ( - "Tokyo Night".into(), - Some("tokyo-night"), - active_preset == Some("tokyo-night"), - ), - ]; - - let button_variants = [ - ("primary", ButtonVariant::Primary), - ("secondary", ButtonVariant::Secondary), - ("ghost", ButtonVariant::Ghost), - ("destructive", ButtonVariant::Destructive), - ]; - - let content = div() - .font_family(".SystemUIFont") - .bg(colors.background) - .text_color(colors.foreground) - .p_6() - .flex() - .flex_col() - .gap_5() - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_xl() - .font_weight(gpui::FontWeight::SEMIBOLD) - .child("gpui-native-kit"), - ) - .child( - div() - .text_sm() - .text_color(colors.muted_foreground) - .child(format!( - "phase C — button · source: {source} · variant: {variant:?} · control radius {}px", - f32::from(theme.radius.control) - )), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(section_label("mode", colors)) - .child( - div() - .flex() - .gap_2() - .children(modes.map(|(label, value, active)| { - chip( - format!("mode-{label}"), - label, - active, - colors, - radius, - move |_, _, cx| gpui_native_theme::set_mode_override(value, cx), - ) - })), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(section_label("preset", colors)) - .child( - div() - .flex() - .gap_2() - .children(presets.map(|(label, value, active)| { - chip( - format!("preset-{label}"), - label, - active, - colors, - radius, - move |_, _, cx| { - let _ = gpui_native_theme::set_preset(value, cx); - }, - ) - })), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "button · native refs — NSButton, WinUI Button, Adwaita, Breeze", - colors, - )) - .child( - h_flex() - .flex_wrap() - .gap_2() - .children(button_variants.map(|(name, variant)| { - let gallery = gallery.clone(); - let mut button = Button::new(SharedString::from(format!( - "gallery-button-{name}" - ))) - .label(button_label(variant)) - .variant(variant) - .on_click(move |_, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.button_clicks += 1; - cx.notify(); - }); - }); - if variant == ButtonVariant::Destructive { - button = - button.tooltip(Tooltip::text("Removes the selected item")); - } - button - })) - .child( - Button::new("gallery-button-disabled") - .label("Disabled") - .disabled(true), - ) - .child( - Button::new("gallery-button-icon") - .label("Open") - .icon(IconRole::Folder) - .tooltip(Tooltip::text("Opens the selected file")), - ) - .child( - Button::new("gallery-button-icon-only") - .icon(IconRole::Folder) - .variant(ButtonVariant::Ghost) - .accessibility_label("Open the selected file") - .tooltip(Tooltip::text("Opens the selected file")), - ), - ) - .child( - h_flex() - .items_center() - .gap_2() - .child(Button::new("gallery-button-sm").label("Small").small()) - .child(Button::new("gallery-button-md").label("Medium")) - .child(Button::new("gallery-button-lg").label("Large").large()), - ) - .child( - div() - .text_xs() - .text_color(colors.muted_foreground) - .child(format!("clicked {} times", self.button_clicks)), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "input · native refs — NSTextField, WinUI TextBox, Adwaita Entry, Breeze LineEdit", - colors, - )) - .child( - Input::new("gallery-input", self.input_state.clone()) - .placeholder("Type here…"), - ) - .child( - Input::new("gallery-password", self.password_state.clone()) - .placeholder("Password"), - ) - .child( - Input::new("gallery-notes", self.notes_state.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.input_state.read(cx).value() - )), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "layout · resize handle \u{2014} NSSplitView divider, WinUI GridSplitter, GtkPaned handle", - colors, - )) - .child( - h_flex() - .h(px(112.)) - .rounded(radius.control) - .border_1() - .border_color(colors.border) - .overflow_hidden() - .child( - div() - .w(self.split_width) - .h_full() - .flex() - .items_center() - .justify_center() - .bg(colors.muted) - .child("Sidebar"), - ) - .child( - ResizeHandle::new("gallery-resize") - .on_resize({ - let gallery = gallery.clone(); - move |delta, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.split_width = (gallery.split_width - + delta) - .clamp(px(96.), px(320.)); - cx.notify(); - }); - } - }) - .on_reset({ - let gallery = gallery.clone(); - move |_, cx| { - gallery.update(cx, |gallery, cx| { - gallery.split_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 \u{2014} drag the seam, double-click resets", - f32::from(self.split_width) - )), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "layout · app shell \u{2014} NSWindow sidebar + inspector, WinUI SplitView shell", - colors, - )) - .child( - div() - .h(px(320.)) - .rounded(radius.control) - .border_1() - .border_color(colors.border) - .overflow_hidden() - .child(self.render_app_shell(&gallery, cx)), - ) - .child( - div() - .text_xs() - .text_color(colors.muted_foreground) - .child(format!( - "left: {} · right: {} · dock: {} \u{2014} buttons toggle, seams resize, tabs close and split", - if self.layout_state.read(cx).is_left_sidebar_visible() { - "on" - } else { - "off" - }, - if self.layout_state.read(cx).is_right_sidebar_visible() { - "on" - } else { - "off" - }, - if self.dock_state.read(cx).active_panel().is_some() { - "open" - } else { - "empty" - }, - )), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "checkbox · native refs — NSButton (checkbox), WinUI CheckBox, Adwaita CheckButton, Breeze", - colors, - )) - .child( - v_flex().gap_2().child({ - let gallery = gallery.clone(); - Checkbox::new("gallery-checkbox") - .label("Enable telemetry") - .checked(self.checkbox_checked) - .on_change(move |checked, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.checkbox_checked = checked; - cx.notify(); - }); - }) - }) - .child( - Checkbox::new("gallery-checkbox-disabled-off") - .label("Disabled unchecked") - .disabled(true), - ) - .child( - Checkbox::new("gallery-checkbox-disabled-on") - .label("Disabled checked") - .checked(true) - .disabled(true), - ), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "menu · native ref — NSMenu, WinUI MenuFlyout, Adwaita popover menu", - colors, - )) - .child({ - let gallery = gallery.clone(); - Menu::new("gallery-menu") - .open(self.menu_open) - .on_change(move |open, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.menu_open = open; - cx.notify(); - }); - }) - .trigger(|_, _| { - Button::new("gallery-menu-trigger") - .label("Actions") - .variant(ButtonVariant::Secondary) - .into_any_element() - }) - .items(|_, _| { - vec![ - MenuItem::new("Cut") - .shortcut("cmd-x") - .handler(|_, cx| { - eprintln!("menu: cut"); - let _ = cx; - }), - MenuItem::new("Copy").shortcut("cmd-c"), - MenuItem::new("Paste").shortcut("cmd-v"), - MenuItem::separator(), - MenuItem::new("Use quiet mode").checked(true), - MenuItem::new("Show hidden files"), - MenuItem::separator(), - MenuItem::new("Unavailable").disabled(true), - MenuItem::new("Delete").destructive(true), - ] - }) - }), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(section_label( - "select · native refs — NSPopUpButton, WinUI ComboBox, Adwaita Dropdown", - colors, - )) - .child({ - let gallery = gallery.clone(); - div().w(gpui::px(280.)).flex().flex_col().gap_3().child( - Select::new("gallery-strategy") - .options(vec![ - SelectOption::new(Strategy::Curated, "Curated") - .icon(IconRole::Check), - SelectOption::new(Strategy::Uncensored, "Uncensored") - .icon(IconRole::Warning), - ]) - .selected(self.strategy) - .placeholder("Fetching strategy") - .on_change(move |next, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.strategy = next; - cx.notify(); - }); - }), - ) - }) - .child({ - div().w(gpui::px(280.)).child( - Select::new("gallery-size") - .options(vec![ - ("small", "Small"), - ("medium", "Medium"), - ("large", "Large"), - ]) - .selected("medium") - .placeholder("Control size") - .small(), - ) - }), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(section_label( - "context menu · native refs — NSMenu (context), WinUI MenuFlyout, Adwaita context popover", - colors, - )) - .child({ - let gallery = gallery.clone(); - ContextMenu::new("gallery-context") - .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 gallery = gallery.clone(); - MenuItem::new(*action).handler(move |_, cx| { - gallery.update(cx, |gallery, cx| { - gallery.context_action = - SharedString::from(*action); - cx.notify(); - }); - }) - }) - .collect() - }) - .accessibility_label("Card actions") - }) - .child( - div() - .text_sm() - .text_color(colors.muted_foreground) - .child(self.context_action.clone()), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(section_label( - "empty state · native refs — AdwStatusPage, Kirigami PlaceHolderMessage, macOS placeholder views", - colors, - )) - .child( - div() - .h(px(180.)) - .rounded(theme.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-clear") - .label("Clear filters") - .variant(ButtonVariant::Secondary), - ), - ), - ) - .child( - div() - .h(px(120.)) - .rounded(theme.radius.overlay) - .border_1() - .border_color(colors.border) - .flex() - .child(EmptyState::new("Nothing here yet")), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(section_label( - "segmented · native refs — NSSegmentedControl, WinUI Segmented, Adwaita AdwToggleGroup", - colors, - )) - .child({ - let gallery = gallery.clone(); - div() - .flex() - .flex_col() - .gap_2() - .child( - Segmented::new("gallery-explore", self.explore_filter) - .options(vec![ - (ExploreFilter::All, "All"), - (ExploreFilter::Popular, "Popular"), - (ExploreFilter::Recent, "Recent"), - ]) - .on_change(move |next, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.explore_filter = next; - cx.notify(); - }); - }), - ) - .child( - Segmented::new("gallery-explore-medium", self.explore_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), - ) - .child( - Segmented::new("gallery-explore-large", self.explore_filter) - .options(vec![ - (ExploreFilter::All, "All"), - (ExploreFilter::Popular, "Popular"), - (ExploreFilter::Recent, "Recent"), - ]) - .large(), - ) - }), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "list · native refs — NSTableView (list), WinUI ListView, Adwaita ListBox, Breeze", - colors, - )) - .child({ - let gallery = gallery.clone(); - List::new("gallery-list") - .selected(self.list_selected) - .on_change(move |selected, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.list_selected = selected; - cx.notify(); - }); - }) - .items(|_, _| { - vec![ - ListItem::new("Documents").icon(IconRole::Folder), - ListItem::new("Pictures") - .icon(IconRole::Folder) - .detail("24 items"), - ListItem::new("Music").icon(IconRole::Folder), - ListItem::new("Archive").disabled(true), - ListItem::new("Trash").detail("emptied weekly"), - ] - }) - }) - .child( - div() - .text_xs() - .text_color(colors.muted_foreground) - .child(format!("selected: {:?}", self.list_selected)), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "tree · native refs — NSOutlineView, WinUI TreeView, Adwaita expander rows, Breeze", - colors, - )) - .child({ - let gallery = gallery.clone(); - let disclosure_gallery = gallery.clone(); - let mut items = Vec::new(); - let mut paths = Vec::new(); - flatten_tree(&self.tree_nodes, 0, &[], &mut items, &mut paths); - - Tree::new("gallery-tree") - .selected(self.tree_selected) - .on_change(move |selected, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.tree_selected = selected; - cx.notify(); - }); - }) - .on_disclosure(move |index, _, cx| { - disclosure_gallery.update(cx, |gallery, cx| { - let paths = tree_paths(&gallery.tree_nodes); - if let Some(path) = paths.get(index) { - toggle_tree_path(&mut gallery.tree_nodes, path); - } - cx.notify(); - }); - }) - .items(move |_, _| items.clone()) - }), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "sidebar · native refs — NSOutlineView (source list), WinUI NavigationView, Adwaita Sidebar", - colors, - )) - .child(div().w(px(280.)).child({ - let gallery = gallery.clone(); - Sidebar::new("gallery-sidebar") - .selected(self.sidebar_selected) - .on_change(move |selected, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.sidebar_selected = selected; - cx.notify(); - }); - }) - .entries(|_, _| { - vec![ - SidebarEntry::header("Favorites"), - SidebarEntry::row("Documents").icon(IconRole::Folder), - SidebarEntry::row("Pictures") - .icon(IconRole::Folder) - .detail("24"), - SidebarEntry::row("Archive").disabled(true), - SidebarEntry::header("Locations"), - SidebarEntry::row("iCloud").icon(IconRole::Folder), - SidebarEntry::row("Trash"), - ] - }) - })), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "tabs · native refs — NSTabView, WinUI TabView, Adwaita TabBar, Breeze", - colors, - )) - .child({ - let gallery = gallery.clone(); - Tabs::new("gallery-tabs") - .selected(self.tab_selected) - .on_change(move |selected, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.tab_selected = selected; - cx.notify(); - }); - }) - .tabs(|_, _| { - vec![ - Tab::new("Overview"), - Tab::new("Details"), - Tab::new("Settings"), - Tab::new("Locked").disabled(true), - ] - }) - }) - .child(match self.tab_selected { - 0 => div() - .child("The overview panel is the app's business — the strip only reports.") - .into_any_element(), - 1 => div() - .child("Details: rows, lists and trees all live under their own tabs.") - .into_any_element(), - _ => div() - .child("Settings: keyboard focus rides the strip; arrows switch tabs.") - .into_any_element(), - }) - .child({ - let gallery = gallery.clone(); - let gallery_for_close = gallery.clone(); - Tabs::new("gallery-pill-tabs") - .variant(TabsVariant::Pill) - .selected(self.card_tab_selected) - .on_change(move |selected, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.card_tab_selected = selected; - cx.notify(); - }); - }) - .on_close(move |index, _, cx| { - gallery_for_close.update(cx, |gallery, cx| { - if index < gallery.card_tabs.len() { - gallery.card_tabs.remove(index); - gallery.card_tab_selected = gallery - .card_tab_selected - .min(gallery.card_tabs.len().saturating_sub(1)); - cx.notify(); - } - }); - }) - .tabs({ - let labels = self.card_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 variant (the signed dock tab) — {} tabs left; the ring on \"Syncing\" is the Spinner placeholder", - self.card_tabs.len() - )), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "alert · static callout — Adwaita InfoBar, WinUI InfoBar", - colors, - )) - .child( - v_flex() - .gap_2() - .child( - Alert::new("gallery-alert-update").title("Update available") - .message("Version 2.4.0 is ready to install.") - .severity(AlertSeverity::Info), - ) - .when(!self.update_alert_dismissed, |section| { - section.child( - Alert::new("gallery-alert-sync") - .title("Sync failed") - .message("Will retry when you're back online.") - .severity(AlertSeverity::Danger) - .on_dismiss({ - let gallery = gallery.clone(); - move |cx| { - gallery.update(cx, |gallery, cx| { - gallery.update_alert_dismissed = true; - cx.notify(); - }); - } - }), - ) - }), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "alert stack · pushed, persistent — macOS notification stack", - colors, - )) - .child( - div().flex().gap_2().children([ - { - let alerts = self.alerts.clone(); - Button::new("gallery-alert-push-info") - .label("Push info") - .variant(ButtonVariant::Secondary) - .on_click(move |_, _, cx| { - alerts.update(cx, |alerts, cx| { - alerts.push( - Alert::new("pushed-info") - .title("Backed up") - .message("Everything is in sync.") - .severity(AlertSeverity::Info), - cx, - ); - }); - }) - }, - { - let alerts = self.alerts.clone(); - Button::new("gallery-alert-push-warning") - .label("Push warning") - .variant(ButtonVariant::Secondary) - .on_click(move |_, _, cx| { - alerts.update(cx, |alerts, cx| { - alerts.push( - Alert::new("pushed-warning") - .title("Storage almost full") - .message("1.2 GB remaining on this device.") - .severity(AlertSeverity::Warning), - cx, - ); - }); - }) - }, - { - let alerts = self.alerts.clone(); - Button::new("gallery-alert-clear") - .label("Clear all") - .variant(ButtonVariant::Secondary) - .on_click(move |_, _, cx| { - alerts.update(cx, |alerts, cx| alerts.clear(cx)); - }) - }, - ]), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "toast · temporary message, one at a time — Adwaita Toast, macOS banner", - colors, - )) - .child( - div().flex().gap_2().children([ - { - let toasts = self.toasts.clone(); - Button::new("gallery-toast-undo") - .label("Deleted (with undo)") - .variant(ButtonVariant::Secondary) - .on_click(move |_, _, cx| { - toasts.update(cx, |toasts, cx| { - toasts.push( - Toast::new("Draft deleted") - .action(ToastAction::new("Undo", |cx| { - // A real app restores the draft here. - let _ = cx; - })), - cx, - ); - }); - }) - }, - { - let toasts = self.toasts.clone(); - Button::new("gallery-toast-saved") - .label("Saved (with icon)") - .variant(ButtonVariant::Secondary) - .on_click(move |_, _, cx| { - toasts.update(cx, |toasts, cx| { - toasts.push( - Toast::new("Workspace saved") - .icon(IconRole::Check), - cx, - ); - }); - }) - }, - { - let toasts = self.toasts.clone(); - Button::new("gallery-toast-connection") - .label("Connection lost (sticky)") - .variant(ButtonVariant::Secondary) - .on_click(move |_, _, cx| { - toasts.update(cx, |toasts, cx| { - toasts.push( - Toast::new("Connection lost") - .icon(IconRole::Warning) - .duration(Duration::ZERO), - cx, - ); - }); - }) - }, - ]), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "dialog · alert · native refs — NSAlert, WinUI ContentDialog, Adwaita MessageDialog", - colors, - )) - .child( - div().flex().gap_2().children([ - { - let gallery = gallery.clone(); - Button::new("gallery-dialog-open") - .label("Delete file…") - .variant(ButtonVariant::Secondary) - .on_click(move |_, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.dialog_open = true; - cx.notify(); - }); - }) - }, - { - let gallery = gallery.clone(); - Button::new("gallery-alert-open") - .label("Sign out…") - .variant(ButtonVariant::Secondary) - .on_click(move |_, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.alert_open = true; - cx.notify(); - }); - }) - }, - ]), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "popover · native ref — NSPopover, WinUI Flyout, Adwaita Popover", - colors, - )) - .child({ - let gallery = gallery.clone(); - Popover::new("gallery-popover") - .open(self.popover_open) - .on_change(move |open, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.popover_open = open; - cx.notify(); - }); - }) - .trigger(|_, _| { - Button::new("gallery-popover-trigger") - .label("Options") - .variant(ButtonVariant::Secondary) - .into_any_element() - }) - .content(move |_, _| { - v_flex() - .gap_1() - .w(px(240.)) - .p_2() - .child(section_label("appearance", colors)) - .child( - Checkbox::new("gallery-popover-telemetry") - .label("Enable telemetry"), - ) - .child( - Checkbox::new("gallery-popover-crash") - .label("Send crash reports"), - ) - .into_any_element() - }) - }), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "switch · native refs — NSSwitch, WinUI ToggleSwitch, Adwaita Switch, Breeze Switch", - colors, - )) - .child( - v_flex().gap_2().child({ - let gallery = gallery.clone(); - Switch::new("gallery-switch") - .label("Wi-Fi") - .checked(self.switch_checked) - .on_change(move |checked, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.switch_checked = checked; - cx.notify(); - }); - }) - }) - .child( - Switch::new("gallery-switch-disabled-off") - .label("Disabled off") - .disabled(true), - ) - .child( - Switch::new("gallery-switch-disabled-on") - .label("Disabled on") - .checked(true) - .disabled(true), - ), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "radio · native refs — NSButton (radio), WinUI RadioButton, Adwaita CheckButton (radio), Breeze", - colors, - )) - .child({ - let gallery = gallery.clone(); - RadioGroup::new("gallery-radio-group") - .selected(Some(self.plan.clone())) - .on_change(move |plan, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.plan = plan; - cx.notify(); - }); - }) - .child(Radio::new("radio-starter", SharedString::from("starter")).label("Starter")) - .child(Radio::new("radio-pro", SharedString::from("pro")).label("Pro")) - .child( - Radio::new("radio-team", SharedString::from("team")) - .label("Team") - .disabled(true), - ) - }), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "toggle · native refs — NSButton (toggle), WinUI ToggleButton, Adwaita ToggleButton, Breeze", - colors, - )) - .child( - h_flex().gap_2().child({ - let gallery = gallery.clone(); - Toggle::new("gallery-toggle") - .label("Bold") - .pressed(self.toggle_pressed) - .on_change(move |pressed, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.toggle_pressed = pressed; - cx.notify(); - }); - }) - }) - .child( - Toggle::new("gallery-toggle-disabled") - .label("Disabled") - .pressed(true) - .disabled(true), - ), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "link · native refs — NSTextView links, WinUI HyperlinkButton, Adwaita LinkButton, Breeze", - colors, - )) - .child( - h_flex() - .gap_4() - .child( - Link::new("gallery-link-github") - .label("gpui on GitHub") - .href("https://github.com/zed-industries/zed"), - ) - .child({ - let gallery = gallery.clone(); - Link::new("gallery-link-override") - .label("On-click override") - .href("https://example.com/never-opened") - .on_click(move |_, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.button_clicks += 1; - cx.notify(); - }); - }) - }) - .child( - Link::new("gallery-link-disabled") - .label("Disabled") - .href("https://example.com/disabled") - .disabled(true), - ), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "slider · native refs — NSSlider, WinUI Slider, Adwaita Scale, Breeze Slider", - colors, - )) - .child({ - let gallery = gallery.clone(); - Slider::new("gallery-slider", self.volume) - .min(0.0) - .max(100.0) - .step(5.0) - .accessibility_label("Volume") - .on_change(move |volume, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.volume = volume; - cx.notify(); - }); - }) - }) - .child( - div() - .text_xs() - .text_color(colors.muted_foreground) - .child(format!("volume: {}", self.volume)), - ), - ) - .child( - v_flex() - .gap_2() - .child(section_label( - "progress · native refs — NSProgressIndicator, WinUI ProgressBar, Adwaita ProgressBar, Breeze", - colors, - )) - .child(Progress::new("progress-25", 0.25).accessibility_label("Quarter")) - .child(Progress::new("progress-60", 0.6).accessibility_label("Most")) - .child(Progress::new("progress-90", 0.9).accessibility_label("Nearly")) - .child( - Progress::new("progress-indeterminate", 0.0) - .indeterminate(true) - .accessibility_label("Working"), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(section_label( - "avatar · native refs — NSImage (contact), WinUI PersonPicture, Adwaita Avatar, Breeze", - colors, - )) - .child( - h_flex() - .gap_3() - .child( - Avatar::new("reya@reya.info") - .name("Reya Adele") - .picture(Some("https://github.com/reya.png")), - ) - .child( - Avatar::new("ada@reya.info") - .name("Ada Lovelace") - .picture(Some("https://example.com/missing.png")), - ) - .child(Avatar::new("grace@reya.info").name("Grace Hopper").small()) - .child(Avatar::new("desk@reya.info").name("Help Desk").large()), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(section_label( - "badge · native refs — WinUI InfoBadge, macOS app badge, GTK count pill", - colors, - )) - .child( - h_flex() - .gap_3() - .child(Badge::new("3")) - .child(Badge::new("new").variant(BadgeVariant::Accent)) - .child(Badge::new("synced").variant(BadgeVariant::Success)) - .child(Badge::new("drift").variant(BadgeVariant::Warning)) - .child(Badge::new("12").variant(BadgeVariant::Danger)) - .child(Badge::new("small").small()) - .child(Badge::new("large").large()), - ), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(section_label("accent shades (derived, OKLCH)", colors)) - .child(div().flex().gap_2().children(shades.map(|(name, color)| { - div() - .flex() - .items_center() - .gap_2() - .child( - div() - .size_6() - .rounded(radius.full) - .border_1() - .border_color(colors.border) - .bg(color), - ) - .child( - div() - .text_xs() - .text_color(colors.muted_foreground) - .child(name), - ) - }))), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(section_label("color tokens", colors)) - .child(div().flex().flex_wrap().gap_2().children(swatches.map( - |(name, color)| { - div() - .flex() - .items_center() + 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() @@ -1587,144 +2337,44 @@ impl Render for Gallery { .text_color(colors.muted_foreground) .child(name), ) - }, - ))), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child(section_label( - "icons (Lucide fallback; platform resolvers land later)", - colors, - )) - .child( - div().flex().items_center().gap_4().children( - IconRole::all() - .iter() - .map(|role| icon(*role).size(px(20.)).color(colors.foreground)), - ), + })), + )) + .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)), ), - ); - - // The whole gallery scrolls, demonstrating the scroll area at the - // page level: the thumb floats over the window's content. The - // dialogs render outside it, as the last children of the window, so - // their overlay layer paints over everything. - let dialog_gallery = gallery.clone(); - let dialog = Dialog::new("gallery-dialog") - .open(self.dialog_open) - .title("Delete file?") - .on_change({ - let gallery = dialog_gallery.clone(); - move |open, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.dialog_open = open; - cx.notify(); - }); - } - }) - .content(|_, _| { - div() - .child("\"report-final.pdf\" will be removed permanently.") - .into_any_element() - }) - .actions(move |_, _| { - let gallery = dialog_gallery.clone(); - vec![ - Button::new("gallery-dialog-cancel") - .label("Cancel") - .variant(ButtonVariant::Secondary) - .on_click({ - let gallery = gallery.clone(); - move |_, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.dialog_open = false; - cx.notify(); - }); - } - }) - .into_any_element(), - Button::new("gallery-dialog-delete") - .label("Delete") - .variant(ButtonVariant::Destructive) - .on_click({ - let gallery = gallery.clone(); - move |_, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.dialog_open = false; - cx.notify(); - }); - } - }) - .into_any_element(), - ] - }); - - let alert_gallery = gallery.clone(); - let alert = Dialog::new("gallery-alert") - .open(self.alert_open) - .alert(true) - .title("Sign out of the workspace?") - .on_change({ - let gallery = alert_gallery.clone(); - move |open, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.alert_open = open; - cx.notify(); - }); - } - }) - .content(|_, _| { - div() - .child("Any running agents will keep working in the background.") - .into_any_element() - }) - .actions(move |_, _| { - let gallery = alert_gallery.clone(); - vec![ - Button::new("gallery-alert-stay") - .label("Stay") - .variant(ButtonVariant::Secondary) - .on_click({ - let gallery = gallery.clone(); - move |_, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.alert_open = false; - cx.notify(); - }); - } - }) - .into_any_element(), - Button::new("gallery-alert-signout") - .label("Sign out") - .variant(ButtonVariant::Primary) - .on_click({ - let gallery = gallery.clone(); - move |_, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.alert_open = false; - cx.notify(); - }); - } - }) - .into_any_element(), - ] - }); - - div() - .size_full() - .child( - ScrollArea::new("gallery-scroll") - .handle(self.scroll.clone()) - .size_full() - .child(content), - ) - .child(dialog) - .child(alert) - .child(AlertsView::new("gallery-alerts", self.alerts.clone())) - .child(ToastsView::new("gallery-toasts", self.toasts.clone())) + )), + ) } } @@ -1740,53 +2390,28 @@ fn button_label(variant: ButtonVariant) -> &'static str { fn section_label(text: &str, colors: gpui_native_theme::ColorTokens) -> impl IntoElement + use<> { div() .text_sm() - .font_weight(gpui::FontWeight::MEDIUM) + .font_weight(FontWeight::MEDIUM) .text_color(colors.muted_foreground) .child(text.to_string()) } -fn chip( - id: String, - label: SharedString, - active: bool, +fn demo_section( + label: &str, colors: gpui_native_theme::ColorTokens, - radius: gpui_native_theme::Radius, - on_click: F, -) -> impl IntoElement + use + body: B, +) -> impl IntoElement + use where - F: Fn(&ClickEvent, &mut Window, &mut App) + 'static, + B: IntoElement, { - div() - .id(SharedString::from(id)) - .px_3() - .py_1() - .rounded(radius.full) - .border_1() - .text_xs() - .cursor_pointer() - .when(active, |el| { - el.bg(colors.primary) - .border_color(colors.primary) - .text_color(colors.primary_foreground) - }) - .when(!active, |el| { - el.bg(colors.secondary) - .border_color(colors.border) - .text_color(colors.secondary_foreground) - .hover(|el| el.bg(colors.muted)) - }) - .on_click(move |event, window, cx| on_click(event, window, cx)) - .child(label) + v_flex() + .gap_2() + .child(section_label(label, colors)) + .child(body) } fn main() { application().with_assets(IconAssets).run(|cx: &mut App| { gpui_native_core::init(cx); - let input_state = InputState::new(cx); - let password_state = InputState::new(cx); - password_state.update(cx, |state, cx| state.set_password(true, cx)); - let notes_state = InputState::new(cx); - notes_state.update(cx, |state, cx| state.set_multiline(true, 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 @@ -1799,58 +2424,15 @@ fn main() { }); }); }); - dock_state.update(cx, |dock, cx| { - dock.register_panel( - PanelDescriptor::new("overview", "Overview", |_, cx| { - cx.new(|_| DemoPanel { - title: "Overview", - body: "View state survives tab switches and pane moves.", - }) - .into() - }) - .right_sidebar(|_, cx| { - cx.new(|_| RightDetails { - title: "Overview", - body: "Details follow the active panel.", - }) - .into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("inbox", "Inbox", |_, cx| { - cx.new(|_| DemoPanel { - title: "Inbox", - body: "Right-click the tab bar for Close, Close Others, Close Pane.", - }) - .into() - }) - .right_sidebar(|_, cx| { - cx.new(|_| RightDetails { - title: "Inbox", - body: "Sync status of the selected account.", - }) - .into() - }), - cx, - ); - dock.register_panel( - PanelDescriptor::new("settings", "Settings", |_, cx| { - cx.new(|_| DemoPanel { - title: "Settings", - body: "Drag the seam between panes; double-click it to reset.", - }) - .into() - }), - cx, - ); - }); - let bounds = Bounds::centered(None, size(px(720.0), px(560.0)), 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() }), ..Default::default() @@ -1866,108 +2448,34 @@ fn main() { cx.refresh_windows(); }) .detach(); - dock_state.update(cx, |dock, cx| dock.open_panel("overview", window, cx)); let toasts = cx.new(|_| Toasts::default()); let alerts = cx.new(|_| Alerts::default()); - cx.new(|cx| { + 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 { - button_clicks: 0, - checkbox_checked: false, - switch_checked: true, - plan: SharedString::from("starter"), - toggle_pressed: false, - volume: 40.0, - input_state, - password_state, - notes_state, - split_width: px(200.), - layout_state, - dock_state, - shell_selection: None, - popover_open: false, - menu_open: false, - strategy: Some(Strategy::Curated), - explore_filter: ExploreFilter::All, - context_action: "Right-click the card".into(), + layout_state: layout_state.clone(), + dock_state: dock_state.clone(), + alerts: alerts.clone(), + toasts: toasts.clone(), dialog_open: false, alert_open: false, - update_alert_dismissed: false, - alerts, - toasts, - scroll: gpui::ScrollHandle::new(), - list_selected: None, - tree_selected: None, - tab_selected: 0, - card_tabs: vec![ - SharedString::from("Explorer"), - SharedString::from("Syncing"), - SharedString::from("Blame"), - ], - card_tab_selected: 0, - sidebar_selected: None, - tree_nodes: vec![ - GalleryNode { - label: "Workspace".into(), - icon: Some(IconRole::Folder), - expanded: true, - children: vec![GalleryNode { - label: "gpui-native-kit".into(), - icon: Some(IconRole::Folder), - expanded: true, - children: vec![ - GalleryNode { - label: "crates".into(), - icon: Some(IconRole::Folder), - expanded: true, - children: vec![ - GalleryNode { - label: "theme".into(), - icon: None, - expanded: false, - children: Vec::new(), - }, - GalleryNode { - label: "core".into(), - icon: None, - expanded: false, - children: Vec::new(), - }, - GalleryNode { - label: "data".into(), - icon: None, - expanded: false, - children: Vec::new(), - }, - ], - }, - GalleryNode { - label: "docs".into(), - icon: Some(IconRole::Folder), - expanded: false, - children: Vec::new(), - }, - GalleryNode { - label: "Cargo.toml".into(), - icon: None, - expanded: false, - children: Vec::new(), - }, - ], - }], - }, - GalleryNode { - label: "Archive.zip".into(), - icon: None, - expanded: false, - children: Vec::new(), - }, - ], } - }) + }); + 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}");