1930 lines
85 KiB
Rust
1930 lines
85 KiB
Rust
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<InputState>,
|
|
password_state: Entity<InputState>,
|
|
notes_state: Entity<InputState>,
|
|
split_width: Pixels,
|
|
layout_state: Entity<LayoutState>,
|
|
dock_state: Entity<DockState>,
|
|
shell_selection: Option<usize>,
|
|
popover_open: bool,
|
|
menu_open: bool,
|
|
strategy: Option<Strategy>,
|
|
explore_filter: ExploreFilter,
|
|
context_action: SharedString,
|
|
dialog_open: bool,
|
|
alert_open: bool,
|
|
update_alert_dismissed: bool,
|
|
alerts: Entity<Alerts>,
|
|
toasts: Entity<Toasts>,
|
|
scroll: gpui::ScrollHandle,
|
|
list_selected: Option<usize>,
|
|
tree_nodes: Vec<GalleryNode>,
|
|
tree_selected: Option<usize>,
|
|
tab_selected: usize,
|
|
card_tabs: Vec<SharedString>,
|
|
card_tab_selected: usize,
|
|
sidebar_selected: Option<usize>,
|
|
}
|
|
|
|
/// 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<IconRole>,
|
|
expanded: bool,
|
|
children: Vec<GalleryNode>,
|
|
}
|
|
|
|
fn flatten_tree(
|
|
nodes: &[GalleryNode],
|
|
depth: usize,
|
|
prefix: &[usize],
|
|
items: &mut Vec<TreeItem>,
|
|
paths: &mut Vec<Vec<usize>>,
|
|
) {
|
|
for (index, node) in nodes.iter().enumerate() {
|
|
let mut path = prefix.to_vec();
|
|
path.push(index);
|
|
|
|
let mut item = TreeItem::new(node.label.clone())
|
|
.depth(depth)
|
|
.expandable(!node.children.is_empty())
|
|
.expanded(node.expanded);
|
|
if let Some(role) = node.icon {
|
|
item = item.icon(role);
|
|
}
|
|
items.push(item);
|
|
paths.push(path.clone());
|
|
|
|
if node.expanded {
|
|
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<Vec<usize>> {
|
|
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<Self>, 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<DockState>, 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<Self>) -> 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<Self>) -> 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<gpui_native_theme::ThemeVariant>, 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<F>(
|
|
id: String,
|
|
label: SharedString,
|
|
active: bool,
|
|
colors: gpui_native_theme::ColorTokens,
|
|
radius: gpui_native_theme::Radius,
|
|
on_click: F,
|
|
) -> impl IntoElement + use<F>
|
|
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);
|
|
});
|
|
}
|