use std::time::Duration; use gpui::prelude::FluentBuilder as _; use gpui::{ App, AppContext as _, Bounds, ClickEvent, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, Styled as _, Window, WindowBounds, WindowOptions, div, px, size, }; use gpui_native_core::avatar::Avatar; 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::dialog::Dialog; 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::slider::Slider; use gpui_native_core::switch::Switch; use gpui_native_core::toast::{Toast, ToastSeverity, Toasts, ToastsView}; use gpui_native_core::toggle::Toggle; use gpui_native_core::tooltip::Tooltip; use gpui_native_data::list::{List, ListItem}; use gpui_native_icons::{IconAssets, IconRole, icon}; use gpui_native_theme::{ActiveTheme as _, 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, popover_open: bool, menu_open: bool, dialog_open: bool, alert_open: bool, toasts: Entity, scroll: gpui::ScrollHandle, list_selected: Option, } 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( 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( 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( "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( 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( "toast · native ref — NSUserNotification stack, WinUI InfoBar, Adwaita in-app notifications", colors, )) .child( div().flex().gap_2().children([ { let toasts = self.toasts.clone(); Button::new("gallery-toast-success") .label("Success") .variant(ButtonVariant::Secondary) .on_click(move |_, _, cx| { toasts.update(cx, |toasts, cx| { toasts.push( Toast::new("Workspace saved") .message("All changes written to disk.") .severity(ToastSeverity::Success) .no_icon(), cx, ); }); }) }, { let toasts = self.toasts.clone(); Button::new("gallery-toast-warning") .label("Warning") .variant(ButtonVariant::Secondary) .on_click(move |_, _, cx| { toasts.update(cx, |toasts, cx| { toasts.push( Toast::new("Disk almost full") .message("1.2 GB remaining.") .severity(ToastSeverity::Warning), cx, ); }); }) }, { let toasts = self.toasts.clone(); Button::new("gallery-toast-danger") .label("Danger (sticky)") .variant(ButtonVariant::Destructive) .on_click(move |_, _, cx| { toasts.update(cx, |toasts, cx| { toasts.push( Toast::new("Sync failed") .message("Will retry when online.") .severity(ToastSeverity::Danger) .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("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(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 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(); let toasts = cx.new(|_| Toasts::default()); cx.new(|cx| { // Toast pushes and dismissals repaint the stack. cx.observe(&toasts, |_, _, 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, popover_open: false, menu_open: false, dialog_open: false, alert_open: false, toasts, scroll: gpui::ScrollHandle::new(), list_selected: None, } }) }, ) { eprintln!("failed to open gallery window: {err}"); } cx.activate(true); }); }