feat: add spinner component

This commit is contained in:
2026-11-03 16:39:13 +07:00
parent 978aecb651
commit 7399d92f36
11 changed files with 595 additions and 114 deletions
+125 -82
View File
@@ -25,6 +25,7 @@ use gpui_native_core::scroll_area::{ScrollArea, ScrollAxes};
use gpui_native_core::segmented::{Segmented, SegmentedOption};
use gpui_native_core::select::{Select, SelectOption};
use gpui_native_core::slider::Slider;
use gpui_native_core::spinner::{Spinner, SpinnerVariant};
use gpui_native_core::switch::Switch;
use gpui_native_core::toast::{Toast, ToastAction, Toasts, ToastsView};
use gpui_native_core::toggle::Toggle;
@@ -66,6 +67,7 @@ const CATALOG: &[(&str, &[(&str, &str)])] = &[
("badge", "Badge"),
("avatar", "Avatar"),
("progress", "Progress"),
("spinner", "Spinner"),
("list", "List"),
("tree", "Tree"),
("tabs", "Tabs"),
@@ -451,6 +453,10 @@ impl Gallery {
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("spinner", "Spinner", |_, cx| cx.new(|_| SpinnerDemo).into()),
cx,
);
dock.register_panel(
PanelDescriptor::new("list", "List", |_, cx| {
cx.new(|_| ListDemo { selected: None }).into()
@@ -1305,6 +1311,47 @@ impl Render for ProgressDemo {
}
}
struct SpinnerDemo;
impl Render for SpinnerDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
ScrollArea::new("spinner-demo")
.size_full()
.p_4()
.child(demo_section(
"spinning and determinate",
colors,
h_flex()
.gap_3()
.child(Spinner::new("spinner-demo-spin"))
.child(Spinner::new("spinner-demo-small").small())
.child(Spinner::new("spinner-demo-large").large())
.child(Spinner::new("spinner-demo-quarter").value(0.25))
.child(Spinner::new("spinner-demo-half").value(0.5))
.child(Spinner::new("spinner-demo-full").value(1.0)),
))
.child(demo_section(
"icon variant",
colors,
h_flex()
.gap_3()
.child(Spinner::new("spinner-demo-icon").variant(SpinnerVariant::Icon))
.child(
Spinner::new("spinner-demo-icon-small")
.variant(SpinnerVariant::Icon)
.small(),
)
.child(
Spinner::new("spinner-demo-icon-held")
.variant(SpinnerVariant::Icon)
.value(0.5),
),
))
}
}
struct ListDemo {
selected: Option<usize>,
}
@@ -1526,90 +1573,86 @@ impl Render for TabsDemo {
let colors = cx.theme().colors;
let pill_count = self.pill_tabs.len();
ScrollArea::new("tabs-demo")
.size_full()
.p_4()
.child(
v_flex().gap_3()
.child(demo_section(
"underline",
colors,
Tabs::new("tabs-demo-underline")
.selected(self.selected)
.on_change(move |selected, _, cx| {
view.update(cx, |demo, cx| {
demo.selected = selected;
ScrollArea::new("tabs-demo").size_full().p_4().child(
v_flex()
.gap_3()
.child(demo_section(
"underline",
colors,
Tabs::new("tabs-demo-underline")
.selected(self.selected)
.on_change(move |selected, _, cx| {
view.update(cx, |demo, cx| {
demo.selected = selected;
cx.notify();
});
})
.tabs(|_, _| {
vec![
Tab::new("Overview"),
Tab::new("Details"),
Tab::new("Settings"),
Tab::new("Locked").disabled(true),
]
}),
))
.child(match self.selected {
0 => div()
.child("The overview panel is the app's business — the strip only reports.")
.into_any_element(),
1 => div()
.child("Details: rows, lists and trees all live under their own tabs.")
.into_any_element(),
_ => div()
.child("Settings: keyboard focus rides the strip; arrows switch tabs.")
.into_any_element(),
})
.child(demo_section(
"pill",
colors,
Tabs::new("tabs-demo-pill")
.variant(TabsVariant::Pill)
.selected(self.pill_selected)
.on_change(move |selected, _, cx| {
view_pill.update(cx, |demo, cx| {
demo.pill_selected = selected;
cx.notify();
});
})
.on_close(move |index, _, cx| {
view_close.update(cx, |demo, cx| {
if index < demo.pill_tabs.len() {
demo.pill_tabs.remove(index);
demo.pill_selected = demo
.pill_selected
.min(demo.pill_tabs.len().saturating_sub(1));
cx.notify();
});
})
.tabs(|_, _| {
vec![
Tab::new("Overview"),
Tab::new("Details"),
Tab::new("Settings"),
Tab::new("Locked").disabled(true),
]
}),
))
.child(match self.selected {
0 => div()
.child(
"The overview panel is the app's business — the strip only reports.",
)
.into_any_element(),
1 => div()
.child("Details: rows, lists and trees all live under their own tabs.")
.into_any_element(),
_ => div()
.child("Settings: keyboard focus rides the strip; arrows switch tabs.")
.into_any_element(),
})
.child(demo_section(
"pill",
colors,
Tabs::new("tabs-demo-pill")
.variant(TabsVariant::Pill)
.selected(self.pill_selected)
.on_change(move |selected, _, cx| {
view_pill.update(cx, |demo, cx| {
demo.pill_selected = selected;
cx.notify();
});
})
.on_close(move |index, _, cx| {
view_close.update(cx, |demo, cx| {
if index < demo.pill_tabs.len() {
demo.pill_tabs.remove(index);
demo.pill_selected = demo
.pill_selected
.min(demo.pill_tabs.len().saturating_sub(1));
cx.notify();
}
});
})
.tabs({
let labels = self.pill_tabs.clone();
move |_, _| {
labels
.iter()
.map(|label| {
Tab::new(label.clone())
.closeable(true)
.busy(label == "Syncing")
})
.collect()
}
}),
))
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!(
"{pill_count} tabs left; the ring on \"Syncing\" is the Spinner placeholder"
)),
),
)
});
})
.tabs({
let labels = self.pill_tabs.clone();
move |_, _| {
labels
.iter()
.map(|label| {
Tab::new(label.clone())
.closeable(true)
.busy(label == "Syncing")
})
.collect()
}
}),
))
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!(
"{pill_count} tabs left; \"Syncing\" spins while it loads"
)),
),
)
}
}