chore: refactor dialogs to render custom footer buttons

This commit is contained in:
2026-10-08 16:27:39 +07:00
parent fff0f6a753
commit 09fb58c5a4
9 changed files with 170 additions and 141 deletions
+2 -2
View File
@@ -127,7 +127,7 @@ impl ThemeColors {
text: neutral().light().step_12(), text: neutral().light().step_12(),
text_muted: neutral().light().step_11(), text_muted: neutral().light().step_11(),
text_placeholder: neutral().light().step_10(), text_placeholder: neutral().light().step_10(),
text_accent: brand().light().step_9(), text_accent: brand().light().step_11(),
text_danger: danger().light().step_9(), text_danger: danger().light().step_9(),
text_warning: warning().light().step_9(), text_warning: warning().light().step_9(),
@@ -213,7 +213,7 @@ impl ThemeColors {
text: neutral().dark().step_12(), text: neutral().dark().step_12(),
text_muted: neutral().dark().step_11(), text_muted: neutral().dark().step_11(),
text_placeholder: neutral().dark().step_10(), text_placeholder: neutral().dark().step_10(),
text_accent: brand().dark().step_9(), text_accent: brand().dark().step_11(),
text_danger: danger().dark().step_9(), text_danger: danger().dark().step_9(),
text_warning: warning().dark().step_9(), text_warning: warning().dark().step_9(),
+23 -40
View File
@@ -3,16 +3,17 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
Animation, AnimationExt as _, AnyElement, App, BoxShadow, ClickEvent, Div, FocusHandle, Animation, AnimationExt as _, AnyElement, App, BoxShadow, ClickEvent, Div, FocusHandle,
InteractiveElement as _, IntoElement, ParentElement, Pixels, RenderOnce, SharedString, IntoElement, ParentElement, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, Window,
StyleRefinement, Styled, Window, div, hsla, point, px, size, div, hsla, point, px,
}; };
use gpui_base::{DialogBackdrop, DialogPopup, DialogTitle};
use instant::Duration; use instant::Duration;
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::animation::cubic_bezier; use crate::animation::cubic_bezier;
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _}; use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
use crate::scroll::ScrollableElement; use crate::scroll::ScrollableElement;
use crate::{IconName, Root, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; use crate::{IconName, Root, StyledExt, WindowExtension, h_flex, v_flex};
type OnClose = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>; type OnClose = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
type OnOk = Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>>; type OnOk = Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>>;
@@ -68,7 +69,6 @@ pub struct Dialog {
content: Div, content: Div,
width: Pixels, width: Pixels,
max_width: Option<Pixels>, max_width: Option<Pixels>,
margin_top: Option<Pixels>,
on_close: OnClose, on_close: OnClose,
on_ok: OnOk, on_ok: OnOk,
@@ -94,7 +94,6 @@ impl Dialog {
title: None, title: None,
footer: None, footer: None,
content: v_flex(), content: v_flex(),
margin_top: None,
width: px(380.), width: px(380.),
max_width: None, max_width: None,
overlay: true, overlay: true,
@@ -232,6 +231,7 @@ impl RenderOnce for Dialog {
let layer_ix = self.layer_ix; let layer_ix = self.layer_ix;
let is_topmost = layer_ix + 1 == Root::read(window, cx).active_dialogs.len(); let is_topmost = layer_ix + 1 == Root::read(window, cx).active_dialogs.len();
let has_footer = self.footer.is_some(); let has_footer = self.footer.is_some();
let on_close = self.on_close.clone(); let on_close = self.on_close.clone();
let on_ok = self.on_ok.clone(); let on_ok = self.on_ok.clone();
let on_cancel = self.on_cancel.clone(); let on_cancel = self.on_cancel.clone();
@@ -246,8 +246,7 @@ impl RenderOnce for Dialog {
Button::new("ok") Button::new("ok")
.label(ok_text) .label(ok_text)
.with_variant(ok_variant) .with_variant(ok_variant)
.small() .font_semibold()
.flex_1()
.on_click({ .on_click({
let on_ok = on_ok.clone(); let on_ok = on_ok.clone();
let on_close = on_close.clone(); let on_close = on_close.clone();
@@ -280,8 +279,6 @@ impl RenderOnce for Dialog {
Button::new("cancel") Button::new("cancel")
.label(cancel_text) .label(cancel_text)
.with_variant(cancel_variant) .with_variant(cancel_variant)
.small()
.flex_1()
.on_click({ .on_click({
let on_cancel = on_cancel.clone(); let on_cancel = on_cancel.clone();
let on_close = on_close.clone(); let on_close = on_close.clone();
@@ -298,18 +295,6 @@ impl RenderOnce for Dialog {
} }
}); });
let window_paddings = crate::root::window_paddings(window, cx);
let radius = cx.theme().radius_lg;
let view_size = window.viewport_size()
- size(
window_paddings.left + window_paddings.right,
window_paddings.top + window_paddings.bottom,
);
let offset_top = px(layer_ix as f32 * 16.);
let top_offset = self.margin_top.unwrap_or(px(0.)) + offset_top;
let mut padding_right = px(16.); let mut padding_right = px(16.);
let mut padding_left = px(16.); let mut padding_left = px(16.);
@@ -324,32 +309,25 @@ impl RenderOnce for Dialog {
let animation = Animation::new(Duration::from_secs_f64(0.25)) let animation = Animation::new(Duration::from_secs_f64(0.25))
.with_easing(cubic_bezier(0.32, 0.72, 0., 1.)); .with_easing(cubic_bezier(0.32, 0.72, 0., 1.));
let backdrop = div() let backdrop = DialogBackdrop::new()
.absolute() .absolute()
.top(window_paddings.top) .inset_0()
.left(window_paddings.left) .when(self.overlay_visible, |this| this.bg(cx.theme().overlay))
.w(view_size.width)
.h(view_size.height)
.when(self.overlay_visible, |this| {
this.occlude().bg(cx.theme().overlay)
})
.with_animation("fade-in", animation.clone(), move |this, delta| { .with_animation("fade-in", animation.clone(), move |this, delta| {
this.opacity(delta) this.opacity(delta)
}); });
let card = v_flex() let card = DialogPopup::new()
.id(layer_ix) .flex()
.flex_col()
.bg(cx.theme().background) .bg(cx.theme().background)
.border_1() .border_1()
.border_color(cx.theme().border.alpha(0.4)) .border_color(cx.theme().border.alpha(0.4))
.rounded(radius) .rounded(cx.theme().radius_lg)
.when(cx.theme().shadow, |this| this.shadow_xl()) .when(cx.theme().shadow, |this| this.shadow_xl())
.min_h_24() .min_h_24()
.refine_style(&self.style) .refine_style(&self.style)
// There style is high priority, can't be overridden.
.occlude()
.relative() .relative()
.top(top_offset)
.w(self.width) .w(self.width)
.when_some(self.max_width, |this, w| this.max_w(w)) .when_some(self.max_width, |this, w| this.max_w(w))
.child( .child(
@@ -362,7 +340,12 @@ impl RenderOnce for Dialog {
.justify_center() .justify_center()
.relative() .relative()
.when_some(self.title, |this, title| { .when_some(self.title, |this, title| {
this.h_10().font_semibold().text_center().child(title) this.h_10().child(
DialogTitle::new()
.font_semibold()
.text_center()
.child(title),
)
}) })
.when(self.show_close, |this| { .when(self.show_close, |this| {
let on_cancel = on_cancel.clone(); let on_cancel = on_cancel.clone();
@@ -415,7 +398,7 @@ impl RenderOnce for Dialog {
.when_some(self.footer, |this, footer| { .when_some(self.footer, |this, footer| {
this.child( this.child(
h_flex() h_flex()
.gap_2() .gap_1()
.pt(padding_left) .pt(padding_left)
.pr(padding_right) .pr(padding_right)
.pb(padding_left) .pb(padding_left)
@@ -444,11 +427,13 @@ impl RenderOnce for Dialog {
inset: false, inset: false,
}, },
]; ];
this.top(top_offset + y_offset).shadow(shadow) this.top(y_offset).shadow(shadow)
}); });
gpui_base::Dialog::new(cx) gpui_base::Dialog::new(cx)
.layer(layer_ix, is_topmost) .layer(layer_ix, is_topmost)
.backdrop(backdrop)
.popup(card)
.focus_handle(self.focus_handle.clone()) .focus_handle(self.focus_handle.clone())
.close_on_escape(self.keyboard) .close_on_escape(self.keyboard)
.close_on_backdrop_press(self.overlay_closable) .close_on_backdrop_press(self.overlay_closable)
@@ -461,7 +446,5 @@ impl RenderOnce for Dialog {
on_close(event, window, cx); on_close(event, window, cx);
window.close_dialog(cx); window.close_dialog(cx);
}) })
.backdrop(backdrop)
.popup(card)
} }
} }
+1 -24
View File
@@ -3,7 +3,7 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, ElementId, Entity, AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, ElementId, Entity,
FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton, FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement as _, Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle, ParentElement as _, Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle,
Window, canvas, div, point, px, size, Window, canvas, div, point, px, size,
@@ -389,29 +389,6 @@ impl Render for Root {
} }
} }
/// Get the window paddings.
pub(crate) fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
match window.window_decorations() {
Decorations::Server => Edges::all(px(0.0)),
Decorations::Client { tiling } => {
let mut paddings = Edges::all(CLIENT_SIDE_DECORATION_SHADOW);
if tiling.top {
paddings.top = px(0.0);
}
if tiling.bottom {
paddings.bottom = px(0.0);
}
if tiling.left {
paddings.left = px(0.0);
}
if tiling.right {
paddings.right = px(0.0);
}
paddings
}
}
}
/// Get the window resize edge. /// Get the window resize edge.
fn resize_edge( fn resize_edge(
pos: Point<Pixels>, pos: Point<Pixels>,
+4 -12
View File
@@ -10,7 +10,7 @@ use state::{CoopAuthUrlHandler, NostrRegistry, USER_KEYRING};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::input::{Input, InputEvent, InputState}; use ui::input::{Input, InputEvent, InputState};
use ui::{Disableable, StyledExt, WindowExtension, divider, v_flex}; use ui::{Disableable, StyledExt, WindowExtension, v_flex};
pub fn open(window: &mut Window, cx: &mut App) { pub fn open(window: &mut Window, cx: &mut App) {
let import = cx.new(|cx| ImportIdentity::new(window, cx)); let import = cx.new(|cx| ImportIdentity::new(window, cx));
@@ -20,7 +20,7 @@ pub fn open(window: &mut Window, cx: &mut App) {
.show_close(false) .show_close(false)
.overlay_closable(false) .overlay_closable(false)
.keyboard(false) .keyboard(false)
.title("Get Started") .title("Continue with your Nostr Identity")
.child(import.clone()) .child(import.clone())
}); });
} }
@@ -29,20 +29,13 @@ pub fn open(window: &mut Window, cx: &mut App) {
pub struct ImportIdentity { pub struct ImportIdentity {
/// Secret key input /// Secret key input
key_input: Entity<InputState>, key_input: Entity<InputState>,
/// Password input (if required) /// Password input (if required)
pass_input: Entity<InputState>, pass_input: Entity<InputState>,
/// Error message /// Error message
error: Entity<Option<SharedString>>, error: Entity<Option<SharedString>>,
/// Whether the user is currently loading /// Whether the user is currently loading
loading: bool, loading: bool,
/// Async tasks
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
/// Input subscription
_subscription: Option<Subscription>, _subscription: Option<Subscription>,
} }
@@ -230,7 +223,7 @@ impl Render for ImportIdentity {
v_flex() v_flex()
.size_full() .size_full()
.gap_4() .gap_2()
.text_sm() .text_sm()
.child( .child(
v_flex() v_flex()
@@ -279,12 +272,11 @@ impl Render for ImportIdentity {
this.login(window, cx); this.login(window, cx);
})), })),
) )
.child(divider(cx))
.when(!is_wasm, |this| { .when(!is_wasm, |this| {
this.child( this.child(
Button::new("proxy") Button::new("proxy")
.label("Connect via Web Extension (Experimental)") .label("Connect via Web Extension (Experimental)")
.ghost_alt() .ghost()
.loading(self.loading) .loading(self.loading)
.disabled(self.loading) .disabled(self.loading)
.on_click(cx.listener(move |this, _ev, _window, cx| { .on_click(cx.listener(move |this, _ev, _window, cx| {
+49 -9
View File
@@ -1,20 +1,20 @@
use gpui::{App, AppContext, ParentElement, Styled, Window, px}; use std::rc::Rc;
use gpui::{App, AppContext, ClickEvent, ParentElement, Styled, Window, px};
use nip29::{GroupReference, GroupsRegistry}; use nip29::{GroupReference, GroupsRegistry};
use ui::WindowExtension; use ui::button::{Button, ButtonVariants};
use ui::input::{Input, InputState}; use ui::input::{Input, InputState};
use ui::notification::Notification; use ui::notification::Notification;
use ui::{IconName, StyledExt, WindowExtension, h_flex, v_flex};
type SubmitFn = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool>;
pub fn open(window: &mut Window, cx: &mut App) { pub fn open(window: &mut Window, cx: &mut App) {
let input = cx.new(|cx| InputState::new(window, cx).placeholder("naddr1…")); let input = cx.new(|cx| InputState::new(window, cx).placeholder("naddr1…"));
window.open_dialog(cx, move |this, _window, _cx| { let join: SubmitFn = {
let input = input.clone(); let input = input.clone();
Rc::new(move |_, window, cx| {
this.width(px(380.))
.confirm()
.title("Join Group")
.child(Input::new(&input).cleanable(true).flex_1().text_sm())
.on_ok(move |_event, window, cx| {
let groups = GroupsRegistry::global(cx); let groups = GroupsRegistry::global(cx);
let reference = match GroupReference::parse(input.read(cx).value().trim()) { let reference = match GroupReference::parse(input.read(cx).value().trim()) {
@@ -31,5 +31,45 @@ pub fn open(window: &mut Window, cx: &mut App) {
true true
}) })
};
window.open_dialog(cx, move |this, _window, _cx| {
this.width(px(380.))
.title("Join Group")
.on_ok({
let join = join.clone();
move |event, window, cx| join(event, window, cx)
})
.child(
v_flex()
.gap_3()
.child(Input::new(&input).cleanable(true).text_sm())
.child(
h_flex()
.gap_1()
.justify_end()
.child(
Button::new("cancel")
.label("Cancel")
.ghost()
.on_click(|_, window, cx| window.close_dialog(cx)),
)
.child(
Button::new("join")
.icon(IconName::CheckCircle)
.label("Join")
.primary()
.font_semibold()
.on_click({
let join = join.clone();
move |event, window, cx| {
if join(event, window, cx) {
window.close_dialog(cx);
}
}
}),
),
),
)
}); });
} }
+25 -17
View File
@@ -16,11 +16,10 @@ use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dialog::DialogButtonProps;
use ui::dock::{DockArea, DockPlacement, PanelHandle}; use ui::dock::{DockArea, DockPlacement, PanelHandle};
use ui::input::{Input, InputEvent, InputState}; use ui::input::{Input, InputEvent, InputState};
use ui::nav::Nav; use ui::nav::Nav;
use ui::{Disableable, IconName, Sizable, WindowExtension, h_flex, v_flex}; use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
/// Maximum number of members a chat can have. /// Maximum number of members a chat can have.
const MAX_MEMBERS: usize = 3; const MAX_MEMBERS: usize = 3;
@@ -30,21 +29,7 @@ pub fn open(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
window.open_dialog(cx, { window.open_dialog(cx, {
let view = view.clone(); let view = view.clone();
move |this, _window, _cx| { move |this, _window, _cx| this.width(px(420.)).title("New chat").child(view.clone())
this.width(px(420.))
.title("New chat")
.confirm()
.button_props(
DialogButtonProps::default()
.cancel_text("Cancel")
.ok_text("Start"),
)
.on_ok({
let view = view.clone();
move |_, window, cx| view.update(cx, |this, cx| this.start_chat(window, cx))
})
.child(view.clone())
}
}); });
} }
@@ -452,5 +437,28 @@ impl Render for NewChat {
) )
.h_80(), .h_80(),
) )
.child(
h_flex()
.gap_1()
.justify_end()
.child(
Button::new("cancel")
.label("Cancel")
.ghost()
.on_click(|_, window, cx| window.close_dialog(cx)),
)
.child(
Button::new("start")
.icon(IconName::CheckCircle)
.label("Start")
.primary()
.font_semibold()
.on_click(cx.listener(move |this, _event, window, cx| {
if this.start_chat(window, cx) {
window.close_dialog(cx);
}
})),
),
)
} }
} }
+42 -8
View File
@@ -1,24 +1,33 @@
use gpui::{App, AppContext, Div, Entity, ParentElement, Styled, Window, px}; use std::rc::Rc;
use gpui::{App, AppContext, ClickEvent, Div, Entity, ParentElement, Styled, Window, px};
use nip29::{GroupId, GroupKey, GroupMetadata, GroupsRegistry}; use nip29::{GroupId, GroupKey, GroupMetadata, GroupsRegistry};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::input::{Input, InputState}; use ui::input::{Input, InputState};
use ui::notification::Notification; use ui::notification::Notification;
use ui::{WindowExtension, v_flex}; use ui::{IconName, StyledExt, WindowExtension, h_flex, v_flex};
type SubmitFn = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool>;
pub fn open(window: &mut Window, cx: &mut App) { pub fn open(window: &mut Window, cx: &mut App) {
let name = cx.new(|cx| InputState::new(window, cx).placeholder("Group name")); let name = cx.new(|cx| InputState::new(window, cx).placeholder("Group name"));
let id = cx.new(|cx| InputState::new(window, cx).placeholder("Group id")); let id = cx.new(|cx| InputState::new(window, cx).placeholder("Group id"));
let relay = cx.new(|cx| InputState::new(window, cx).placeholder("wss://relay.example.com")); let relay = cx.new(|cx| InputState::new(window, cx).placeholder("wss://relay.example.com"));
window.open_dialog(cx, move |this, _window, cx| { let submit: SubmitFn = {
let name = name.clone(); let (name, id, relay) = (name.clone(), id.clone(), relay.clone());
let id = id.clone(); Rc::new(move |_, window, cx| create(&name, &id, &relay, window, cx))
let relay = relay.clone(); };
window.open_dialog(cx, move |this, _window, cx| {
this.width(px(380.)) this.width(px(380.))
.confirm()
.title("New group") .title("New group")
.on_ok({
let submit = submit.clone();
move |event, window, cx| submit(event, window, cx)
})
.child( .child(
v_flex() v_flex()
.gap_3() .gap_3()
@@ -26,7 +35,32 @@ pub fn open(window: &mut Window, cx: &mut App) {
.child(field("Group id", &id, cx)) .child(field("Group id", &id, cx))
.child(field("Relay", &relay, cx)), .child(field("Relay", &relay, cx)),
) )
.on_ok(move |_event, window, cx| create(&name, &id, &relay, window, cx)) .child(
h_flex()
.gap_1()
.justify_end()
.child(
Button::new("cancel")
.label("Cancel")
.ghost()
.on_click(|_, window, cx| window.close_dialog(cx)),
)
.child(
Button::new("create")
.icon(IconName::CheckCircle)
.label("Create")
.primary()
.font_semibold()
.on_click({
let submit = submit.clone();
move |event, window, cx| {
if submit(event, window, cx) {
window.close_dialog(cx);
}
}
}),
),
)
}); });
} }
+6 -2
View File
@@ -10,7 +10,7 @@ use nostr_sdk::prelude::*;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::input::{Input, InputEvent, InputState}; use ui::input::{Input, InputEvent, InputState};
use ui::{WindowExtension, v_flex}; use ui::{IconName, StyledExt, WindowExtension, h_flex, v_flex};
#[derive(Debug)] #[derive(Debug)]
pub struct RestoreEncryption { pub struct RestoreEncryption {
@@ -99,7 +99,7 @@ impl Render for RestoreEncryption {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.size_full() .size_full()
.gap_2() .gap_3()
.text_sm() .text_sm()
.child( .child(
v_flex() v_flex()
@@ -110,12 +110,16 @@ impl Render for RestoreEncryption {
.child(Input::new(&self.key_input)), .child(Input::new(&self.key_input)),
) )
.child( .child(
h_flex().gap_1().justify_end().child(
Button::new("restore") Button::new("restore")
.icon(IconName::CheckCircle)
.label("Restore") .label("Restore")
.primary() .primary()
.font_semibold()
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.restore(window, cx); this.restore(window, cx);
})), })),
),
) )
.when_some(self.error.read(cx).as_ref(), |this, error| { .when_some(self.error.read(cx).as_ref(), |this, error| {
this.child( this.child(
@@ -22,27 +22,18 @@ pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<
cx.new(|cx| Screening::new(public_key, window, cx)) cx.new(|cx| Screening::new(public_key, window, cx))
} }
/// Screening
pub struct Screening { pub struct Screening {
/// Public Key of the person being screened. /// Public Key of the person being screened.
public_key: PublicKey, public_key: PublicKey,
/// Whether the person's address is verified. /// Whether the person's address is verified.
verified: bool, verified: bool,
/// Whether the person is followed by current user. /// Whether the person is followed by current user.
followed: bool, followed: bool,
/// Last time the person was active. /// Last time the person was active.
last_active: Option<Timestamp>, last_active: Option<Timestamp>,
/// All mutual contacts of the person being screened. /// All mutual contacts of the person being screened.
mutual_contacts: Vec<PublicKey>, mutual_contacts: Vec<PublicKey>,
/// Async tasks
tasks: SmallVec<[Task<()>; 3]>, tasks: SmallVec<[Task<()>; 3]>,
/// Subscriptions
_subscriptions: SmallVec<[Subscription; 1]>, _subscriptions: SmallVec<[Subscription; 1]>,
} }