Add component NotificationFrame & CaptureAudio parts for testing (#36081)

Adds component NotificationFrame. It implements a subset of MessageNotification as a Component and refactors MessageNotification to use NotificationFrame. Having some notification UI Component is nice as it allows us to easily build new types of notifications.

Uses the new NotificationFrame component for CaptureAudioNotification. 

Adds a CaptureAudio action in the dev namespace (not meant for
end-users). It records 10 seconds of audio and saves that to a wav file.

Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
David Kleingeld 2025-08-15 12:10:52 +02:00 committed by GitHub
parent a3dcc76687
commit 4f0b00b0d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 448 additions and 127 deletions

View file

@ -6,6 +6,7 @@ use gpui::{
Task, svg,
};
use parking_lot::Mutex;
use std::ops::Deref;
use std::sync::{Arc, LazyLock};
use std::{any::TypeId, time::Duration};
@ -189,6 +190,7 @@ impl Workspace {
cx.notify();
}
/// Hide all notifications matching the given ID
pub fn suppress_notification(&mut self, id: &NotificationId, cx: &mut Context<Self>) {
self.dismiss_notification(id, cx);
self.suppressed_notifications.insert(id.clone());
@ -462,16 +464,144 @@ impl EventEmitter<SuppressEvent> for ErrorMessagePrompt {}
impl Notification for ErrorMessagePrompt {}
#[derive(IntoElement, RegisterComponent)]
pub struct NotificationFrame {
title: Option<SharedString>,
show_suppress_button: bool,
show_close_button: bool,
close: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
contents: Option<AnyElement>,
suffix: Option<AnyElement>,
}
impl NotificationFrame {
pub fn new() -> Self {
Self {
title: None,
contents: None,
suffix: None,
show_suppress_button: true,
show_close_button: true,
close: None,
}
}
pub fn with_title(mut self, title: Option<impl Into<SharedString>>) -> Self {
self.title = title.map(Into::into);
self
}
pub fn with_content(self, content: impl IntoElement) -> Self {
Self {
contents: Some(content.into_any_element()),
..self
}
}
/// Determines whether the given notification ID should be suppressible
/// Suppressed motifications will not be shown anymore
pub fn show_suppress_button(mut self, show: bool) -> Self {
self.show_suppress_button = show;
self
}
pub fn show_close_button(mut self, show: bool) -> Self {
self.show_close_button = show;
self
}
pub fn on_close(self, on_close: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
Self {
close: Some(Box::new(on_close)),
..self
}
}
pub fn with_suffix(mut self, suffix: impl IntoElement) -> Self {
self.suffix = Some(suffix.into_any_element());
self
}
}
impl RenderOnce for NotificationFrame {
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let entity = window.current_view();
let show_suppress_button = self.show_suppress_button;
let suppress = show_suppress_button && window.modifiers().shift;
let (close_id, close_icon) = if suppress {
("suppress", IconName::Minimize)
} else {
("close", IconName::Close)
};
v_flex()
.occlude()
.p_3()
.gap_2()
.elevation_3(cx)
.child(
h_flex()
.gap_4()
.justify_between()
.items_start()
.child(
v_flex()
.gap_0p5()
.when_some(self.title.clone(), |div, title| {
div.child(Label::new(title))
})
.child(div().max_w_96().children(self.contents)),
)
.when(self.show_close_button, |this| {
this.on_modifiers_changed(move |_, _, cx| cx.notify(entity))
.child(
IconButton::new(close_id, close_icon)
.tooltip(move |window, cx| {
if suppress {
Tooltip::for_action(
"Suppress.\nClose with click.",
&SuppressNotification,
window,
cx,
)
} else if show_suppress_button {
Tooltip::for_action(
"Close.\nSuppress with shift-click.",
&menu::Cancel,
window,
cx,
)
} else {
Tooltip::for_action("Close", &menu::Cancel, window, cx)
}
})
.on_click({
let close = self.close.take();
move |_, window, cx| {
if let Some(close) = &close {
close(&suppress, window, cx)
}
}
}),
)
}),
)
.children(self.suffix)
}
}
impl Component for NotificationFrame {}
pub mod simple_message_notification {
use std::sync::Arc;
use gpui::{
AnyElement, ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement,
Render, SharedString, Styled, div,
AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render,
SharedString, Styled,
};
use ui::{Tooltip, prelude::*};
use ui::prelude::*;
use crate::SuppressNotification;
use crate::notifications::NotificationFrame;
use super::{Notification, SuppressEvent};
@ -631,6 +761,8 @@ pub mod simple_message_notification {
self
}
/// Determines whether the given notification ID should be supressable
/// Suppressed motifications will not be shown anymor
pub fn show_suppress_button(mut self, show: bool) -> Self {
self.show_suppress_button = show;
self
@ -647,71 +779,19 @@ pub mod simple_message_notification {
impl Render for MessageNotification {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let show_suppress_button = self.show_suppress_button;
let suppress = show_suppress_button && window.modifiers().shift;
let (close_id, close_icon) = if suppress {
("suppress", IconName::Minimize)
} else {
("close", IconName::Close)
};
v_flex()
.occlude()
.p_3()
.gap_2()
.elevation_3(cx)
.child(
h_flex()
.gap_4()
.justify_between()
.items_start()
.child(
v_flex()
.gap_0p5()
.when_some(self.title.clone(), |element, title| {
element.child(Label::new(title))
})
.child(div().max_w_96().child((self.build_content)(window, cx))),
)
.when(self.show_close_button, |this| {
this.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
.child(
IconButton::new(close_id, close_icon)
.tooltip(move |window, cx| {
if suppress {
Tooltip::for_action(
"Suppress.\nClose with click.",
&SuppressNotification,
window,
cx,
)
} else if show_suppress_button {
Tooltip::for_action(
"Close.\nSuppress with shift-click.",
&menu::Cancel,
window,
cx,
)
} else {
Tooltip::for_action(
"Close",
&menu::Cancel,
window,
cx,
)
}
})
.on_click(cx.listener(move |_, _: &ClickEvent, _, cx| {
if suppress {
cx.emit(SuppressEvent);
} else {
cx.emit(DismissEvent);
}
})),
)
}),
)
.child(
NotificationFrame::new()
.with_title(self.title.clone())
.with_content((self.build_content)(window, cx))
.show_close_button(self.show_close_button)
.show_suppress_button(self.show_suppress_button)
.on_close(cx.listener(|_, suppress, _, cx| {
if *suppress {
cx.emit(SuppressEvent);
} else {
cx.emit(DismissEvent);
}
}))
.with_suffix(
h_flex()
.gap_1()
.children(self.primary_message.iter().map(|message| {

View file

@ -15,6 +15,8 @@ mod toast_layer;
mod toolbar;
mod workspace_settings;
pub use crate::notifications::NotificationFrame;
pub use dock::Panel;
pub use toast_layer::{ToastAction, ToastLayer, ToastView};
use anyhow::{Context as _, Result, anyhow};
@ -24,7 +26,6 @@ use client::{
proto::{self, ErrorCode, PanelId, PeerId},
};
use collections::{HashMap, HashSet, hash_map};
pub use dock::Panel;
use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
use futures::{
Future, FutureExt, StreamExt,