add toast
This commit is contained in:
@@ -179,11 +179,7 @@ impl RenderOnce for Button {
|
|||||||
.text_color(foreground)
|
.text_color(foreground)
|
||||||
.when_some(a11y_label, |button, label| button.aria_label(label))
|
.when_some(a11y_label, |button, label| button.aria_label(label))
|
||||||
.when_some(self.icon, |button, role| {
|
.when_some(self.icon, |button, role| {
|
||||||
button.child(
|
button.child(icon(role).size(metrics.icon_size).color(foreground))
|
||||||
icon(role)
|
|
||||||
.size(f32::from(metrics.icon_size))
|
|
||||||
.color(foreground),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.when_some(self.label, |button, label| button.child(label))
|
.when_some(self.label, |button, label| button.child(label))
|
||||||
.when(self.disabled, |button| button.opacity(0.5))
|
.when(self.disabled, |button| button.opacity(0.5))
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ pub mod popover;
|
|||||||
#[cfg(feature = "scroll_area")]
|
#[cfg(feature = "scroll_area")]
|
||||||
pub mod scroll_area;
|
pub mod scroll_area;
|
||||||
|
|
||||||
|
#[cfg(feature = "toast")]
|
||||||
|
pub mod toast;
|
||||||
|
|
||||||
#[cfg(feature = "tooltip")]
|
#[cfg(feature = "tooltip")]
|
||||||
pub mod tooltip;
|
pub mod tooltip;
|
||||||
|
|
||||||
|
|||||||
@@ -495,17 +495,13 @@ fn render_item(
|
|||||||
row.child(div().flex_none().when(checked, |slot| {
|
row.child(div().flex_none().when(checked, |slot| {
|
||||||
slot.child(
|
slot.child(
|
||||||
icon_glyph(IconRole::Check)
|
icon_glyph(IconRole::Check)
|
||||||
.size(f32::from(chrome.icon_size) * 0.75)
|
.size(chrome.icon_size * 0.75)
|
||||||
.color(label_color),
|
.color(label_color),
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.when_some(icon_role, |row, role| {
|
.when_some(icon_role, |row, role| {
|
||||||
row.child(
|
row.child(icon_glyph(role).size(chrome.icon_size).color(label_color))
|
||||||
icon_glyph(role)
|
|
||||||
.size(f32::from(chrome.icon_size))
|
|
||||||
.color(label_color),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
App, Context, ElementId, Entity, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||||
|
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, Window, deferred, div,
|
||||||
|
px,
|
||||||
|
};
|
||||||
|
use gpui_native_icons::{IconRole, icon};
|
||||||
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
|
|
||||||
|
use crate::common::v_flex;
|
||||||
|
|
||||||
|
/// The dismiss handler of a [`Toast`].
|
||||||
|
pub type DismissHandler = Rc<dyn Fn(&mut App)>;
|
||||||
|
|
||||||
|
/// How alarming a [`Toast`] is; picks the icon and the accent color.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum ToastSeverity {
|
||||||
|
/// The default; uses the primary accent.
|
||||||
|
#[default]
|
||||||
|
Info,
|
||||||
|
/// Uses the success role.
|
||||||
|
Success,
|
||||||
|
/// Uses the warning role.
|
||||||
|
Warning,
|
||||||
|
/// Uses the destructive role.
|
||||||
|
Danger,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toast notification.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Toast {
|
||||||
|
title: SharedString,
|
||||||
|
message: Option<SharedString>,
|
||||||
|
severity: ToastSeverity,
|
||||||
|
/// The painted glyph; `None` derives it from the severity.
|
||||||
|
icon: Option<IconRole>,
|
||||||
|
show_icon: bool,
|
||||||
|
duration: Option<Duration>,
|
||||||
|
on_dismiss: Option<DismissHandler>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Toast {
|
||||||
|
/// How long a toast stays up by default.
|
||||||
|
pub const DURATION: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// Creates an informational toast with this title.
|
||||||
|
pub fn new<L>(title: L) -> Self
|
||||||
|
where
|
||||||
|
L: Into<SharedString>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
title: title.into(),
|
||||||
|
message: None,
|
||||||
|
severity: ToastSeverity::Info,
|
||||||
|
icon: None,
|
||||||
|
show_icon: true,
|
||||||
|
duration: Some(Self::DURATION),
|
||||||
|
on_dismiss: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a secondary line under the title.
|
||||||
|
pub fn message<L>(mut self, message: L) -> Self
|
||||||
|
where
|
||||||
|
L: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.message = Some(message.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets how alarming the toast is.
|
||||||
|
pub fn severity(mut self, severity: ToastSeverity) -> Self {
|
||||||
|
self.severity = severity;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overrides the severity-derived icon with this glyph.
|
||||||
|
pub fn icon(mut self, role: IconRole) -> Self {
|
||||||
|
self.icon = Some(role);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hides the icon entirely.
|
||||||
|
pub fn no_icon(mut self) -> Self {
|
||||||
|
self.show_icon = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the time before the toast dismisses itself.
|
||||||
|
pub fn duration(mut self, duration: Duration) -> Self {
|
||||||
|
self.duration = Some(duration);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs when the toast is dismissed by any means.
|
||||||
|
pub fn on_dismiss<F>(mut self, handler: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut App) + 'static,
|
||||||
|
{
|
||||||
|
self.on_dismiss = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One raised toast, with its identity in the store.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ToastEntry {
|
||||||
|
pub id: usize,
|
||||||
|
pub toast: Toast,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The store of raised toasts.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Toasts {
|
||||||
|
entries: Vec<ToastEntry>,
|
||||||
|
next_id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Toasts {
|
||||||
|
/// Raises a toast, starting its auto-dismiss timer. Returns its id.
|
||||||
|
pub fn push(&mut self, toast: Toast, cx: &mut Context<Self>) -> usize {
|
||||||
|
let id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
|
||||||
|
let duration = toast.duration;
|
||||||
|
|
||||||
|
self.entries.push(ToastEntry { id, toast });
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
if let Some(duration) = duration.filter(|duration| !duration.is_zero()) {
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor().timer(duration).await;
|
||||||
|
this.update(cx, |toasts, cx| toasts.dismiss(id, cx)).ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dismisses one toast by id, running its dismiss handler.
|
||||||
|
pub fn dismiss(&mut self, id: usize, cx: &mut Context<Self>) {
|
||||||
|
if let Some(position) = self.entries.iter().position(|entry| entry.id == id) {
|
||||||
|
let entry = self.entries.remove(position);
|
||||||
|
cx.notify();
|
||||||
|
if let Some(on_dismiss) = &entry.toast.on_dismiss {
|
||||||
|
on_dismiss(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dismisses every toast.
|
||||||
|
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if !self.entries.is_empty() {
|
||||||
|
self.entries.clear();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The raised toasts, oldest first.
|
||||||
|
pub fn entries(&self) -> &[ToastEntry] {
|
||||||
|
&self.entries
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many toasts are raised.
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.entries.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ToastsView {
|
||||||
|
toasts: Entity<Toasts>,
|
||||||
|
id: ElementId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToastsView {
|
||||||
|
/// Binds the view to a toast store.
|
||||||
|
pub fn new<I>(id: I, toasts: Entity<Toasts>) -> Self
|
||||||
|
where
|
||||||
|
I: Into<ElementId>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
toasts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ToastsView {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let theme = cx.theme();
|
||||||
|
let surface = theme.colors.surface;
|
||||||
|
let border = theme.colors.border;
|
||||||
|
let foreground = theme.colors.foreground;
|
||||||
|
let muted_foreground = theme.colors.muted_foreground;
|
||||||
|
let muted = theme.colors.muted;
|
||||||
|
let radius = theme.radius.overlay;
|
||||||
|
let shadows = theme.elevations.popover.shadows();
|
||||||
|
let icon_size = theme.sizes.small.icon_size;
|
||||||
|
|
||||||
|
let entries: Vec<ToastEntry> = self.toasts.read(cx).entries().to_vec();
|
||||||
|
|
||||||
|
deferred(
|
||||||
|
div()
|
||||||
|
.id(self.id)
|
||||||
|
.absolute()
|
||||||
|
.top_4()
|
||||||
|
.right_4()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.items_end()
|
||||||
|
.gap_2()
|
||||||
|
.w_80()
|
||||||
|
.children(entries.into_iter().map(|entry| {
|
||||||
|
let toast = &entry.toast;
|
||||||
|
let toasts = self.toasts.clone();
|
||||||
|
|
||||||
|
let accent = match toast.severity {
|
||||||
|
ToastSeverity::Info => theme.ramps.primary.base,
|
||||||
|
ToastSeverity::Success => theme.ramps.success.base,
|
||||||
|
ToastSeverity::Warning => theme.ramps.warning.base,
|
||||||
|
ToastSeverity::Danger => theme.ramps.destructive.base,
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon_role = toast.icon.unwrap_or(match toast.severity {
|
||||||
|
ToastSeverity::Info => IconRole::Info,
|
||||||
|
ToastSeverity::Success => IconRole::Check,
|
||||||
|
ToastSeverity::Warning | ToastSeverity::Danger => IconRole::Warning,
|
||||||
|
});
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id(("toast", entry.id))
|
||||||
|
.flex()
|
||||||
|
.items_start()
|
||||||
|
.gap_2()
|
||||||
|
.py_2()
|
||||||
|
.px_3()
|
||||||
|
.w_full()
|
||||||
|
.bg(surface)
|
||||||
|
.border_1()
|
||||||
|
.border_color(border)
|
||||||
|
.rounded(radius)
|
||||||
|
.shadow(shadows.clone())
|
||||||
|
.when(toast.show_icon, |toast| {
|
||||||
|
toast.child(
|
||||||
|
div()
|
||||||
|
.flex_none()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.h(window.line_height())
|
||||||
|
.child(icon(icon_role).size(icon_size).color(accent)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.flex_1()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(theme.sizes.small.text_size)
|
||||||
|
.text_color(foreground)
|
||||||
|
.when(toast.message.is_some(), |toast| {
|
||||||
|
toast.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||||
|
})
|
||||||
|
.child(toast.title.clone()),
|
||||||
|
)
|
||||||
|
.when_some(toast.message.clone(), |toast, message| {
|
||||||
|
toast.child(
|
||||||
|
div()
|
||||||
|
.text_size(theme.sizes.small.text_size)
|
||||||
|
.text_color(muted_foreground)
|
||||||
|
.child(message),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id(("toast-close", entry.id))
|
||||||
|
.flex_none()
|
||||||
|
.cursor_pointer()
|
||||||
|
.p_0p5()
|
||||||
|
.rounded(px(4.))
|
||||||
|
.hover(|style| style.bg(muted))
|
||||||
|
.child(
|
||||||
|
icon(IconRole::Close)
|
||||||
|
.size(icon_size * 0.75)
|
||||||
|
.color(foreground.alpha(0.55)),
|
||||||
|
)
|
||||||
|
.on_click(move |_, _window, cx| {
|
||||||
|
toasts.update(cx, |toasts, cx| {
|
||||||
|
toasts.dismiss(entry.id, cx);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gpui::{AppContext as _, TestAppContext};
|
||||||
|
use gpui_native_core::toast::{Toast, ToastSeverity, Toasts};
|
||||||
|
use gpui_native_theme::Theme;
|
||||||
|
|
||||||
|
fn toasts_store() -> (TestAppContext, gpui::Entity<Toasts>) {
|
||||||
|
let cx = TestAppContext::single();
|
||||||
|
cx.update(Theme::init);
|
||||||
|
let toasts = cx.update(|cx| cx.new(|_| Toasts::default()));
|
||||||
|
(cx, toasts)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn push_raises_a_toast_and_the_timer_dismisses_it() {
|
||||||
|
let (cx, toasts) = toasts_store();
|
||||||
|
|
||||||
|
let id = cx.update(|cx| {
|
||||||
|
toasts.update(cx, |toasts, cx| {
|
||||||
|
toasts.push(Toast::new("Saved").severity(ToastSeverity::Success), cx)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 1);
|
||||||
|
|
||||||
|
// The default lifetime is five seconds.
|
||||||
|
cx.executor().advance_clock(Toast::DURATION);
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 0);
|
||||||
|
let _ = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sticky_toasts_ignore_the_timer_until_dismissed() {
|
||||||
|
let (cx, toasts) = toasts_store();
|
||||||
|
|
||||||
|
let id = cx.update(|cx| {
|
||||||
|
toasts.update(cx, |toasts, cx| {
|
||||||
|
toasts.push(Toast::new("Sticky").duration(Duration::ZERO), cx)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.executor().advance_clock(Toast::DURATION * 4);
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 1);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
toasts.update(cx, |toasts, cx| toasts.dismiss(id, cx));
|
||||||
|
});
|
||||||
|
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dismiss_runs_the_handler_and_clear_removes_everything() {
|
||||||
|
let (cx, toasts) = toasts_store();
|
||||||
|
let dismissed = Rc::new(std::cell::RefCell::new(Vec::<&'static str>::new()));
|
||||||
|
|
||||||
|
let ids = cx.update(|cx| {
|
||||||
|
toasts.update(cx, |toasts, cx| {
|
||||||
|
let first = dismissed.clone();
|
||||||
|
let second = dismissed.clone();
|
||||||
|
vec![
|
||||||
|
toasts.push(
|
||||||
|
Toast::new("first").on_dismiss(move |_| record(&first, "first")),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
toasts.push(
|
||||||
|
Toast::new("second").on_dismiss(move |_| record(&second, "second")),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
toasts.update(cx, |toasts, cx| toasts.dismiss(ids[0], cx));
|
||||||
|
});
|
||||||
|
assert_eq!(*dismissed.borrow(), vec!["first"]);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
toasts.update(cx, |toasts, cx| toasts.clear(cx));
|
||||||
|
});
|
||||||
|
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 0);
|
||||||
|
|
||||||
|
fn record(sink: &Rc<RefCell<Vec<&'static str>>>, what: &'static str) {
|
||||||
|
sink.borrow_mut().push(what);
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-17
@@ -7,12 +7,11 @@
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AssetSource, Hsla, IntoElement, ParentElement as _, RenderOnce, SharedString, Styled as _,
|
AssetSource, Hsla, IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString,
|
||||||
Window, div, px, svg,
|
Styled as _, Window, div, px, svg,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The semantic glyphs UI components need. Grows one role at a time as
|
/// The semantic glyphs UI components need.
|
||||||
/// components call for it.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum IconRole {
|
pub enum IconRole {
|
||||||
Check,
|
Check,
|
||||||
@@ -65,12 +64,11 @@ impl IconRole {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An icon element. Unpainted unless a color is set (gpui paints SVGs from
|
/// An icon element. Unpainted unless a color is set.
|
||||||
/// the element's text color only).
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Icon {
|
pub struct Icon {
|
||||||
role: IconRole,
|
role: IconRole,
|
||||||
size: f32,
|
size: Pixels,
|
||||||
color: Option<Hsla>,
|
color: Option<Hsla>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,13 +76,13 @@ pub struct Icon {
|
|||||||
pub fn icon(role: IconRole) -> Icon {
|
pub fn icon(role: IconRole) -> Icon {
|
||||||
Icon {
|
Icon {
|
||||||
role,
|
role,
|
||||||
size: 16.0,
|
size: px(16.0),
|
||||||
color: None,
|
color: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Icon {
|
impl Icon {
|
||||||
pub fn size(mut self, size: f32) -> Self {
|
pub fn size(mut self, size: Pixels) -> Self {
|
||||||
self.size = size;
|
self.size = size;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -97,7 +95,7 @@ impl Icon {
|
|||||||
|
|
||||||
impl RenderOnce for Icon {
|
impl RenderOnce for Icon {
|
||||||
fn render(self, _window: &mut Window, _cx: &mut gpui::App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, _cx: &mut gpui::App) -> impl IntoElement {
|
||||||
div().flex_none().size(px(self.size)).child(
|
div().flex_none().size(self.size).child(
|
||||||
svg()
|
svg()
|
||||||
.size_full()
|
.size_full()
|
||||||
.path(self.role.asset_path())
|
.path(self.role.asset_path())
|
||||||
@@ -106,8 +104,7 @@ impl RenderOnce for Icon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The asset source serving this crate's bundled glyphs. Register it on the
|
/// The asset source serving this crate's bundled glyphs.
|
||||||
/// application: `application().with_assets(IconAssets)`.
|
|
||||||
pub struct IconAssets;
|
pub struct IconAssets;
|
||||||
|
|
||||||
fn asset_bytes(path: &str) -> Option<&'static [u8]> {
|
fn asset_bytes(path: &str) -> Option<&'static [u8]> {
|
||||||
@@ -158,9 +155,4 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unknown_assets_report_none() {
|
|
||||||
assert_eq!(asset_bytes("icons/nope.svg"), None);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ pub struct ColorTokens {
|
|||||||
pub accent_foreground: Hsla,
|
pub accent_foreground: Hsla,
|
||||||
pub destructive: Hsla,
|
pub destructive: Hsla,
|
||||||
pub destructive_foreground: Hsla,
|
pub destructive_foreground: Hsla,
|
||||||
|
pub success: Hsla,
|
||||||
|
pub success_foreground: Hsla,
|
||||||
|
pub warning: Hsla,
|
||||||
|
pub warning_foreground: Hsla,
|
||||||
pub border: Hsla,
|
pub border: Hsla,
|
||||||
pub input: Hsla,
|
pub input: Hsla,
|
||||||
pub ring: Hsla,
|
pub ring: Hsla,
|
||||||
@@ -57,6 +61,10 @@ impl ColorTokens {
|
|||||||
accent_foreground: hex(0xffffff),
|
accent_foreground: hex(0xffffff),
|
||||||
destructive: hex(0xdc2626),
|
destructive: hex(0xdc2626),
|
||||||
destructive_foreground: hex(0xffffff),
|
destructive_foreground: hex(0xffffff),
|
||||||
|
success: hex(0x16a34a),
|
||||||
|
success_foreground: hex(0xffffff),
|
||||||
|
warning: hex(0xd97706),
|
||||||
|
warning_foreground: hex(0xffffff),
|
||||||
border: hex(0xe4e4e7),
|
border: hex(0xe4e4e7),
|
||||||
input: hex(0xe4e4e7),
|
input: hex(0xe4e4e7),
|
||||||
ring: hex(0x93c5fd),
|
ring: hex(0x93c5fd),
|
||||||
@@ -84,6 +92,10 @@ impl ColorTokens {
|
|||||||
accent_foreground: hex(0xffffff),
|
accent_foreground: hex(0xffffff),
|
||||||
destructive: hex(0xef4444),
|
destructive: hex(0xef4444),
|
||||||
destructive_foreground: hex(0xffffff),
|
destructive_foreground: hex(0xffffff),
|
||||||
|
success: hex(0x22c55e),
|
||||||
|
success_foreground: hex(0x052e16),
|
||||||
|
warning: hex(0xf59e0b),
|
||||||
|
warning_foreground: hex(0x451a03),
|
||||||
border: hex(0x27272a),
|
border: hex(0x27272a),
|
||||||
input: hex(0x27272a),
|
input: hex(0x27272a),
|
||||||
ring: hex(0x1d4ed8),
|
ring: hex(0x1d4ed8),
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ pub struct ThemeDefinition {
|
|||||||
pub muted_foreground: Option<String>,
|
pub muted_foreground: Option<String>,
|
||||||
pub destructive: Option<String>,
|
pub destructive: Option<String>,
|
||||||
pub destructive_foreground: Option<String>,
|
pub destructive_foreground: Option<String>,
|
||||||
|
pub success: Option<String>,
|
||||||
|
pub success_foreground: Option<String>,
|
||||||
|
pub warning: Option<String>,
|
||||||
|
pub warning_foreground: Option<String>,
|
||||||
pub border: Option<String>,
|
pub border: Option<String>,
|
||||||
pub input: Option<String>,
|
pub input: Option<String>,
|
||||||
pub ring: Option<String>,
|
pub ring: Option<String>,
|
||||||
@@ -94,6 +98,22 @@ impl ThemeDefinition {
|
|||||||
parse_hex(self.destructive_foreground.as_deref()?)
|
parse_hex(self.destructive_foreground.as_deref()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn success(&self) -> Option<Hsla> {
|
||||||
|
parse_hex(self.success.as_deref()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn success_foreground(&self) -> Option<Hsla> {
|
||||||
|
parse_hex(self.success_foreground.as_deref()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warning(&self) -> Option<Hsla> {
|
||||||
|
parse_hex(self.warning.as_deref()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warning_foreground(&self) -> Option<Hsla> {
|
||||||
|
parse_hex(self.warning_foreground.as_deref()?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn border(&self) -> Option<Hsla> {
|
pub fn border(&self) -> Option<Hsla> {
|
||||||
parse_hex(self.border.as_deref()?)
|
parse_hex(self.border.as_deref()?)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ pub struct Ramps {
|
|||||||
pub primary: AccentShades,
|
pub primary: AccentShades,
|
||||||
pub secondary: AccentShades,
|
pub secondary: AccentShades,
|
||||||
pub destructive: AccentShades,
|
pub destructive: AccentShades,
|
||||||
|
pub success: AccentShades,
|
||||||
|
pub warning: AccentShades,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Everything a component paints from, after the overlay chain.
|
/// Everything a component paints from, after the overlay chain.
|
||||||
@@ -58,6 +60,16 @@ pub(crate) fn resolve(
|
|||||||
system_palette.destructive,
|
system_palette.destructive,
|
||||||
base.destructive,
|
base.destructive,
|
||||||
);
|
);
|
||||||
|
let success = pick(
|
||||||
|
def_color(|d| d.success()),
|
||||||
|
system_palette.success,
|
||||||
|
base.success,
|
||||||
|
);
|
||||||
|
let warning = pick(
|
||||||
|
def_color(|d| d.warning()),
|
||||||
|
system_palette.warning,
|
||||||
|
base.warning,
|
||||||
|
);
|
||||||
|
|
||||||
let mut colors = ColorTokens {
|
let mut colors = ColorTokens {
|
||||||
background: pick(
|
background: pick(
|
||||||
@@ -114,6 +126,18 @@ pub(crate) fn resolve(
|
|||||||
None,
|
None,
|
||||||
contrast_foreground(destructive),
|
contrast_foreground(destructive),
|
||||||
),
|
),
|
||||||
|
success: pick(def_color(|d| d.success()), None, base.success),
|
||||||
|
success_foreground: pick(
|
||||||
|
def_color(|d| d.success_foreground()),
|
||||||
|
None,
|
||||||
|
contrast_foreground(success),
|
||||||
|
),
|
||||||
|
warning: pick(def_color(|d| d.warning()), None, base.warning),
|
||||||
|
warning_foreground: pick(
|
||||||
|
def_color(|d| d.warning_foreground()),
|
||||||
|
None,
|
||||||
|
contrast_foreground(warning),
|
||||||
|
),
|
||||||
border: pick(
|
border: pick(
|
||||||
def_color(|d| d.border()),
|
def_color(|d| d.border()),
|
||||||
system_palette.border,
|
system_palette.border,
|
||||||
@@ -182,6 +206,8 @@ pub(crate) fn resolve(
|
|||||||
colors.background,
|
colors.background,
|
||||||
variant == ThemeVariant::Dark,
|
variant == ThemeVariant::Dark,
|
||||||
),
|
),
|
||||||
|
success: derive_shades(success, colors.background, variant == ThemeVariant::Dark),
|
||||||
|
warning: derive_shades(warning, colors.background, variant == ThemeVariant::Dark),
|
||||||
},
|
},
|
||||||
colors,
|
colors,
|
||||||
radius,
|
radius,
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ impl MacosSource {
|
|||||||
muted: None,
|
muted: None,
|
||||||
muted_foreground: Self::color_to_hsla(&NSColor::secondaryLabelColor()),
|
muted_foreground: Self::color_to_hsla(&NSColor::secondaryLabelColor()),
|
||||||
destructive: Self::color_to_hsla(&NSColor::systemRedColor()),
|
destructive: Self::color_to_hsla(&NSColor::systemRedColor()),
|
||||||
|
success: None,
|
||||||
|
warning: None,
|
||||||
border: Self::color_to_hsla(&NSColor::separatorColor()),
|
border: Self::color_to_hsla(&NSColor::separatorColor()),
|
||||||
input: Self::color_to_hsla(&NSColor::controlBackgroundColor()),
|
input: Self::color_to_hsla(&NSColor::controlBackgroundColor()),
|
||||||
ring: Self::color_to_hsla(&NSColor::keyboardFocusIndicatorColor()),
|
ring: Self::color_to_hsla(&NSColor::keyboardFocusIndicatorColor()),
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ pub struct PartialPalette {
|
|||||||
pub muted: Option<Hsla>,
|
pub muted: Option<Hsla>,
|
||||||
pub muted_foreground: Option<Hsla>,
|
pub muted_foreground: Option<Hsla>,
|
||||||
pub destructive: Option<Hsla>,
|
pub destructive: Option<Hsla>,
|
||||||
|
/// No platform states severity colors; presets override them.
|
||||||
|
pub success: Option<Hsla>,
|
||||||
|
pub warning: Option<Hsla>,
|
||||||
pub border: Option<Hsla>,
|
pub border: Option<Hsla>,
|
||||||
pub input: Option<Hsla>,
|
pub input: Option<Hsla>,
|
||||||
pub ring: Option<Hsla>,
|
pub ring: Option<Hsla>,
|
||||||
|
|||||||
+2
-2
@@ -152,7 +152,7 @@ The set stays role-minimal and grows one concept at a time, driven by the unit t
|
|||||||
|
|
||||||
- ~~**Overlay scrim + shadow/elevation tokens**~~ — ✅ landed with unit 11 (2026-10-29): `colors.overlay` (translucent-black scrim, denser in dark; TOML-overridable; no platform states one), `Radius.overlay` (10 pt), `Elevations.popover`/`dialog` (transcribed NSPopover-class shadows, builder → `Vec<BoxShadow>`), and `Motion.overlay` (200 ms enter transition). Popover consumes elevation + motion; the scrim waits for Dialog.
|
- ~~**Overlay scrim + shadow/elevation tokens**~~ — ✅ landed with unit 11 (2026-10-29): `colors.overlay` (translucent-black scrim, denser in dark; TOML-overridable; no platform states one), `Radius.overlay` (10 pt), `Elevations.popover`/`dialog` (transcribed NSPopover-class shadows, builder → `Vec<BoxShadow>`), and `Motion.overlay` (200 ms enter transition). Popover consumes elevation + motion; the scrim waits for Dialog.
|
||||||
- ~~**Scrollbar thumb token + metrics**~~ — ✅ landed with the ScrollArea unit (2026-10-29): `colors.scrollbar_thumb` (translucent over content: black in light, light gray in dark; TOML-overridable) and `Theme.scrollbar` (`width` 8 pt, `inset` 2 pt, `min_thumb` 24 pt).
|
- ~~**Scrollbar thumb token + metrics**~~ — ✅ landed with the ScrollArea unit (2026-10-29): `colors.scrollbar_thumb` (translucent over content: black in light, light gray in dark; TOML-overridable) and `Theme.scrollbar` (`width` 8 pt, `inset` 2 pt, `min_thumb` 24 pt).
|
||||||
- **`success` / `warning` roles** — with the first severity-bearing component (Toast, badges, validation); the Omarchy mapping above already anticipates them.
|
- ~~**`success` / `warning` roles**~~ — ✅ landed with Toast (2026-10-29): `colors.success`/`warning` (+ foregrounds; light and dark variants; TOML-overridable; no platform states them) and derived `Ramps.success`/`warning` via the OKLCH engine, matching the destructive treatment.
|
||||||
- **`knob` / control-fill role** — only if Slider (unit 7) confirms the Switch's borrowed roles (`primary_foreground` thumb, `border` off-track) as a second consumer; until then no expansion.
|
- **`knob` / control-fill role** — only if Slider (unit 7) confirms the Switch's borrowed roles (`primary_foreground` thumb, `border` off-track) as a second consumer; until then no expansion.
|
||||||
|
|
||||||
Controls through unit 10 paint from existing roles and ramps.
|
Controls through unit 10 paint from existing roles and ramps.
|
||||||
@@ -212,7 +212,7 @@ Each row is one review unit: build it, show it in the gallery, review, then move
|
|||||||
| 11.5 | core | ScrollArea + Scrollbar | ✅ Landed (2026-10-29). Inserted before the remaining overlays — Menus, Lists and Tables all scroll. gpui owns the mechanics (`overflow_*_scroll` + caller-held `ScrollHandle`); the component paints an overlay thumb (`scrollbar_thumb` token, `Scrollbar` metrics) with drag and geometry unit-tested. Wheel input is gpui's; the area stops wheel propagation so nested areas don't co-scroll. Horizontal thumb deferred; auto-hide on idle deferred |
|
| 11.5 | core | ScrollArea + Scrollbar | ✅ Landed (2026-10-29). Inserted before the remaining overlays — Menus, Lists and Tables all scroll. gpui owns the mechanics (`overflow_*_scroll` + caller-held `ScrollHandle`); the component paints an overlay thumb (`scrollbar_thumb` token, `Scrollbar` metrics) with drag and geometry unit-tested. Wheel input is gpui's; the area stops wheel propagation so nested areas don't co-scroll. Horizontal thumb deferred; auto-hide on idle deferred |
|
||||||
| 12 | core | Menu | ✅ Landed (2026-10-29). Controlled `open`/`on_change` + `trigger`/`items` (MenuItem: label, icon, shortcut hint, disabled, destructive, checked, separator). Popover shell patterns duplicated per D16 (promote at Dialog); panel = surface + overlay radius + popover elevation, role Menu; claims focus while open; keyboard nav — arrows/home/end over selectable items (wrapping, skipping separators + disabled), enter/space activates (closes menu before the action), escape dismisses; hover selects, click activates; trigger clicks toggle (popover-owned); selection resets on each open; long menus scroll via the ScrollArea once a height is styled. Deferred: submenus, type-ahead, scroll-selected-item-into-view, real shortcut handling |
|
| 12 | core | Menu | ✅ Landed (2026-10-29). Controlled `open`/`on_change` + `trigger`/`items` (MenuItem: label, icon, shortcut hint, disabled, destructive, checked, separator). Popover shell patterns duplicated per D16 (promote at Dialog); panel = surface + overlay radius + popover elevation, role Menu; claims focus while open; keyboard nav — arrows/home/end over selectable items (wrapping, skipping separators + disabled), enter/space activates (closes menu before the action), escape dismisses; hover selects, click activates; trigger clicks toggle (popover-owned); selection resets on each open; long menus scroll via the ScrollArea once a height is styled. Deferred: submenus, type-ahead, scroll-selected-item-into-view, real shortcut handling |
|
||||||
| 11 | core | Popover | ✅ Landed (2026-10-29). First overlay. Controlled `open` + `on_change` (Rc-shared handler); `trigger`/`content` element closures; panel = surface + `Radius.overlay` + `Elevations.popover` + border, role Dialog with label; `anchored()` + `deferred()` (window-overflow flip for free); enter fade via gpui `with_animation` at `Motion.overlay` (respects reduce-motion); claims keyboard focus while open; escape and click-outside dismiss (outside clicks swallowed). No scrim — native macOS popover model; the overlay token waits for Dialog. Known v1 gaps: no exit animation, trigger aria-expanded left to the app |
|
| 11 | core | Popover | ✅ Landed (2026-10-29). First overlay. Controlled `open` + `on_change` (Rc-shared handler); `trigger`/`content` element closures; panel = surface + `Radius.overlay` + `Elevations.popover` + border, role Dialog with label; `anchored()` + `deferred()` (window-overflow flip for free); enter fade via gpui `with_animation` at `Motion.overlay` (respects reduce-motion); claims keyboard focus while open; escape and click-outside dismiss (outside clicks swallowed). No scrim — native macOS popover model; the overlay token waits for Dialog. Known v1 gaps: no exit animation, trigger aria-expanded left to the app |
|
||||||
| 11+ | core | Tooltip ✅ (2026-10-29), Dialog + AlertDialog ✅ (2026-10-29), Toast | One unit each, same gate. Tooltip rides gpui's own `.tooltip()` lifecycle (hover delay, anchoring, dismissal — all framework-owned); ours is the styled view (`surface` + border + `Radius.sm`, small text) plus a `Tooltip::text` closure and a Button passthrough. Dialog + AlertDialog (one component, `alert(true)` switches semantics): scrim (`colors.overlay`, occluding) + centered panel (surface, `Radius.overlay`, `Elevations.dialog`), role Dialog/AlertDialog, focus claimed while open; modal kind closes on escape + scrim click, alert kind is inert to both (the user answers through the actions); actions footer is caller-built elements closing through owner state. Sheet dropped from the roadmap. Toast lands `success`/`warning` tokens when it comes |
|
| 11+ | core | Tooltip ✅ (2026-10-29), Dialog + AlertDialog ✅ (2026-10-29), Toast ✅ (2026-10-29) | One unit each, same gate. Tooltip rides gpui's own `.tooltip()` lifecycle (hover delay, anchoring, dismissal — all framework-owned); ours is the styled view (`surface` + border + `Radius.sm`, small text) plus a `Tooltip::text` closure and a Button passthrough. Dialog + AlertDialog (one component, `alert(true)` switches semantics): scrim (`colors.overlay`, occluding) + centered panel (surface, `Radius.overlay`, `Elevations.dialog`), role Dialog/AlertDialog, focus claimed while open; modal kind closes on escape + scrim click, alert kind is inert to both (the user answers through the actions); actions footer is caller-built elements closing through owner state. Toast: `Toasts` store entity (push/dismiss/clear + auto-dismiss timers on the GPUI executor, `Toast::DURATION` 5 s, zero = sticky) + `ToastsView` deferred stack (top-right, `Radius.overlay` + popover elevation, severity icon from the new success/warning ramps, close button, `on_dismiss` windowless handler). Toast landed the deferred `success`/`warning` tokens (§5.2). Sheet dropped from the roadmap |
|
||||||
| then | data (new) | List, Table, Tree, Tabs, Sidebar | Created on demand, same gate. |
|
| then | data (new) | List, Table, Tree, Tabs, Sidebar | Created on demand, same gate. |
|
||||||
| then | pickers or data | Calendar/DatePicker, TimeField, ColorPicker | Decide at unit time. |
|
| then | pickers or data | Calendar/DatePicker, TimeField, ColorPicker | Decide at unit time. |
|
||||||
| editor | editor (new) | Text/code editor, syntax highlighting (D12) | Same gate. |
|
| editor | editor (new) | Text/code editor, syntax highlighting (D12) | Same gate. |
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, Bounds, ClickEvent, Context, Entity, InteractiveElement as _,
|
App, AppContext as _, Bounds, ClickEvent, Context, Entity, InteractiveElement as _,
|
||||||
@@ -18,6 +20,7 @@ use gpui_native_core::radio::{Radio, RadioGroup};
|
|||||||
use gpui_native_core::scroll_area::ScrollArea;
|
use gpui_native_core::scroll_area::ScrollArea;
|
||||||
use gpui_native_core::slider::Slider;
|
use gpui_native_core::slider::Slider;
|
||||||
use gpui_native_core::switch::Switch;
|
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::toggle::Toggle;
|
||||||
use gpui_native_core::tooltip::Tooltip;
|
use gpui_native_core::tooltip::Tooltip;
|
||||||
use gpui_native_icons::{IconAssets, IconRole, icon};
|
use gpui_native_icons::{IconAssets, IconRole, icon};
|
||||||
@@ -36,6 +39,7 @@ struct Gallery {
|
|||||||
menu_open: bool,
|
menu_open: bool,
|
||||||
dialog_open: bool,
|
dialog_open: bool,
|
||||||
alert_open: bool,
|
alert_open: bool,
|
||||||
|
toasts: Entity<Toasts>,
|
||||||
scroll: gpui::ScrollHandle,
|
scroll: gpui::ScrollHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +339,68 @@ impl Render for Gallery {
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.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(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
@@ -673,7 +739,7 @@ impl Render for Gallery {
|
|||||||
div().flex().items_center().gap_4().children(
|
div().flex().items_center().gap_4().children(
|
||||||
IconRole::all()
|
IconRole::all()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|role| icon(*role).size(20.0).color(colors.foreground)),
|
.map(|role| icon(*role).size(px(20.)).color(colors.foreground)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -793,6 +859,7 @@ impl Render for Gallery {
|
|||||||
)
|
)
|
||||||
.child(dialog)
|
.child(dialog)
|
||||||
.child(alert)
|
.child(alert)
|
||||||
|
.child(ToastsView::new("gallery-toasts", self.toasts.clone()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -872,19 +939,25 @@ fn main() {
|
|||||||
cx.refresh_windows();
|
cx.refresh_windows();
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.new(|_| Gallery {
|
let toasts = cx.new(|_| Toasts::default());
|
||||||
button_clicks: 0,
|
cx.new(|cx| {
|
||||||
checkbox_checked: false,
|
// Toast pushes and dismissals repaint the stack.
|
||||||
switch_checked: true,
|
cx.observe(&toasts, |_, _, cx| cx.notify()).detach();
|
||||||
plan: SharedString::from("starter"),
|
Gallery {
|
||||||
toggle_pressed: false,
|
button_clicks: 0,
|
||||||
volume: 40.0,
|
checkbox_checked: false,
|
||||||
input_state,
|
switch_checked: true,
|
||||||
popover_open: false,
|
plan: SharedString::from("starter"),
|
||||||
menu_open: false,
|
toggle_pressed: false,
|
||||||
dialog_open: false,
|
volume: 40.0,
|
||||||
alert_open: false,
|
input_state,
|
||||||
scroll: gpui::ScrollHandle::new(),
|
popover_open: false,
|
||||||
|
menu_open: false,
|
||||||
|
dialog_open: false,
|
||||||
|
alert_open: false,
|
||||||
|
toasts,
|
||||||
|
scroll: gpui::ScrollHandle::new(),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user