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, }; 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::context_menu::ContextMenu; use gpui_native_core::dialog::Dialog; use gpui_native_core::empty_state::EmptyState; use gpui_native_core::input::{Input, InputState}; use gpui_native_core::link::Link; use gpui_native_core::menu::{Menu, MenuItem}; use gpui_native_core::popover::Popover; use gpui_native_core::progress::Progress; use gpui_native_core::radio::{Radio, RadioGroup}; use gpui_native_core::scroll_area::ScrollArea; use gpui_native_core::segmented::{Segmented, SegmentedOption}; use gpui_native_core::select::{Select, SelectOption}; use gpui_native_core::slider::Slider; use gpui_native_core::switch::Switch; use gpui_native_core::toast::{Toast, ToastAction, Toasts, ToastsView}; use gpui_native_core::toggle::Toggle; use gpui_native_core::tooltip::Tooltip; use gpui_native_data::list::{List, ListItem}; use gpui_native_data::sidebar::{Sidebar, SidebarEntry}; use gpui_native_data::tabs::{Tab, Tabs, TabsVariant}; use gpui_native_data::tree::{Tree, TreeItem}; use gpui_native_icons::{IconAssets, IconRole, icon}; use gpui_native_layout::dock::{Dock, 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_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 select section's demo value. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Strategy { Curated, Uncensored, } /// The segmented section's demo value. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum ExploreFilter { #[default] All, Popular, Recent, } /// The context menu section's demo 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 } 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 colors = cx.theme().colors; let state = self.layout_state.clone(); let state_for_toggle = state.clone(); 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); } }); }) }; let left_sidebar = Sidebar::new("gallery-shell-sidebar") .selected(self.shell_selection) .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"), ] }) .on_change({ let gallery = gallery.clone(); move |selection, _, cx| { gallery.update(cx, |gallery, _| gallery.shell_selection = selection); } }); let right_sidebar = v_flex() .gap_1() .p_2() .child("Details") .child("Signed by the author") .text_sm() .text_color(colors.muted_foreground); 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 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())), ); Shell::new("gallery-shell", state) .left_sidebar(left_sidebar) .center(center) .right_sidebar(right_sidebar) } } /// A stand-in panel view for the dock demo. struct DemoPanel { title: &'static str, body: &'static str, } impl Render for DemoPanel { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let colors = cx.theme().colors; v_flex() .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), ) } } impl Render for Gallery { 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(); let swatches: [(&str, gpui::Hsla); 18] = [ ("background", colors.background), ("foreground", colors.foreground), ("surface", colors.surface), ("surface_foreground", colors.surface_foreground), ("primary", colors.primary), ("primary_foreground", colors.primary_foreground), ("secondary", colors.secondary), ("secondary_foreground", colors.secondary_foreground), ("muted", colors.muted), ("muted_foreground", colors.muted_foreground), ("accent", colors.accent), ("accent_foreground", colors.accent_foreground), ("destructive", colors.destructive), ("destructive_foreground", colors.destructive_foreground), ("border", colors.border), ("input", colors.input), ("ring", colors.ring), ("selection", colors.selection), ]; let shades = [ ("base", theme.ramps.primary.base), ("hover", theme.ramps.primary.hover), ("pressed", theme.ramps.primary.pressed), ("subtle", theme.ramps.primary.subtle), ]; 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() .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( "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)), ), ), ); // 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())) } } fn button_label(variant: ButtonVariant) -> &'static str { match variant { ButtonVariant::Primary => "Primary", ButtonVariant::Secondary => "Secondary", ButtonVariant::Ghost => "Ghost", ButtonVariant::Destructive => "Delete", } } fn section_label(text: &str, colors: gpui_native_theme::ColorTokens) -> impl IntoElement + use<> { div() .text_sm() .font_weight(gpui::FontWeight::MEDIUM) .text_color(colors.muted_foreground) .child(text.to_string()) } fn chip( id: String, label: SharedString, active: bool, colors: gpui_native_theme::ColorTokens, radius: gpui_native_theme::Radius, on_click: F, ) -> impl IntoElement + use where F: Fn(&ClickEvent, &mut Window, &mut App) + 'static, { 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) } 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); 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() }), 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() }), 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); 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()), ..Default::default() }), ..Default::default() }, |window, cx| { Theme::sync_from_window(window, cx); Theme::sync_window_rem_size(window, cx); // Detached on purpose: the subscription lives as long as the window. window .observe_window_appearance(|window, cx| { Theme::sync_from_window(window, cx); Theme::sync_window_rem_size(window, cx); cx.refresh_windows(); }) .detach(); 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| { // 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(), 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(), }, ], } }) }, ) { eprintln!("failed to open gallery window: {err}"); } cx.activate(true); }); }