mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
gpui: Implement audible system bell (#47531)
Relates to #5303 and https://github.com/zed-industries/zed/issues/40826#issuecomment-3684556858 although I haven't found anywhere an actual request for `gpui` itself to support a system alert sound. ### What Basically, this PR adds a function that triggers an OS-dependent alert sound, commonly used by terminal applications for `\a` / `BEL`, and GUI applications to indicate an action failed in some small way (e.g. no search results found, unable to move cursor, button disabled). Also updated the `input` example, which now plays the bell if the user presses <kbd>backspace</kbd> with nothing behind the cursor to delete, or <kbd>delete</kbd> with nothing in front of the cursor. Test with `cargo run --example input --features gpui_platform/font-kit`. ### Why If this is merged, I plan to take a second step: - Add a new Zed setting (probably something like `terminal.audible_bell`) - If enabled, `printf '\a'`, `tput bel` etc. would call this new API to play an audible sound This isn't the super-shiny dream of #5303 but it would allow users to more easily configure tasks to notify when done. Plus, any TUI/CLI apps that expect this functionality will work. Also, I think many terminal users expect something like this (WezTerm, iTerm, etc. almost all support this). ### Notes ~I was only able to test on macOS and Windows, so if there are any Linux users who could verify this works for X11 / Wayland that would be a huge help! If not I can try~ Confirmed Wayland + X11 both working when I ran the example on a NixOS desktop Release Notes: - N/A
This commit is contained in:
parent
4087d9f2ca
commit
971775e3b2
11 changed files with 68 additions and 3 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -7601,6 +7601,7 @@ dependencies = [
|
|||
"media",
|
||||
"metal",
|
||||
"objc",
|
||||
"objc2-app-kit",
|
||||
"parking_lot",
|
||||
"pathfinder_geometry",
|
||||
"raw-window-handle",
|
||||
|
|
@ -11211,6 +11212,16 @@ dependencies = [
|
|||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-app-kit"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-audio-toolbox"
|
||||
version = "0.3.2"
|
||||
|
|
|
|||
|
|
@ -604,6 +604,7 @@ nbformat = "1.2.0"
|
|||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
objc = "0.2"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = [ "NSGraphics" ] }
|
||||
objc2-foundation = { version = "=0.3.2", default-features = false, features = [
|
||||
"NSArray",
|
||||
"NSAttributedString",
|
||||
|
|
@ -821,6 +822,7 @@ features = [
|
|||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Console",
|
||||
"Win32_System_Diagnostics_Debug",
|
||||
"Win32_System_DataExchange",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_LibraryLoader",
|
||||
|
|
|
|||
|
|
@ -85,14 +85,24 @@ impl TextInput {
|
|||
|
||||
fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.select_to(self.previous_boundary(self.cursor_offset()), cx)
|
||||
let prev = self.previous_boundary(self.cursor_offset());
|
||||
if self.cursor_offset() == prev {
|
||||
window.play_system_bell();
|
||||
return;
|
||||
}
|
||||
self.select_to(prev, cx)
|
||||
}
|
||||
self.replace_text_in_range(None, "", window, cx)
|
||||
}
|
||||
|
||||
fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.select_to(self.next_boundary(self.cursor_offset()), cx)
|
||||
let next = self.next_boundary(self.cursor_offset());
|
||||
if self.cursor_offset() == next {
|
||||
window.play_system_bell();
|
||||
return;
|
||||
}
|
||||
self.select_to(next, cx)
|
||||
}
|
||||
self.replace_text_in_range(None, "", window, cx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -689,6 +689,8 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
|||
|
||||
fn update_ime_position(&self, _bounds: Bounds<Pixels>);
|
||||
|
||||
fn play_system_bell(&self) {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||
None
|
||||
|
|
|
|||
|
|
@ -5024,6 +5024,12 @@ impl Window {
|
|||
.set_tabbing_identifier(tabbing_identifier)
|
||||
}
|
||||
|
||||
/// Request the OS to play an alert sound. On some platforms this is associated
|
||||
/// with the window, for others it's just a simple global function call.
|
||||
pub fn play_system_bell(&self) {
|
||||
self.platform_window.play_system_bell()
|
||||
}
|
||||
|
||||
/// Toggles the inspector mode on this window.
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
pub fn toggle_inspector(&mut self, cx: &mut App) {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ use wayland_protocols::xdg::decoration::zv1::client::{
|
|||
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
|
||||
};
|
||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
||||
use wayland_protocols::xdg::system_bell::v1::client::xdg_system_bell_v1;
|
||||
use wayland_protocols::{
|
||||
wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1},
|
||||
xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1},
|
||||
|
|
@ -129,6 +130,7 @@ pub struct Globals {
|
|||
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
|
||||
pub gesture_manager: Option<zwp_pointer_gestures_v1::ZwpPointerGesturesV1>,
|
||||
pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
|
||||
pub system_bell: Option<xdg_system_bell_v1::XdgSystemBellV1>,
|
||||
pub executor: ForegroundExecutor,
|
||||
}
|
||||
|
||||
|
|
@ -170,6 +172,7 @@ impl Globals {
|
|||
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
gesture_manager: globals.bind(&qh, 1..=3, ()).ok(),
|
||||
dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(),
|
||||
system_bell: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
executor,
|
||||
qh,
|
||||
}
|
||||
|
|
@ -1069,6 +1072,7 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
|
|||
}
|
||||
|
||||
delegate_noop!(WaylandClientStatePtr: ignore xdg_activation_v1::XdgActivationV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore xdg_system_bell_v1::XdgSystemBellV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1);
|
||||
|
|
|
|||
|
|
@ -1479,6 +1479,18 @@ impl PlatformWindow for WaylandWindow {
|
|||
fn gpu_specs(&self) -> Option<GpuSpecs> {
|
||||
self.borrow().renderer.gpu_specs().into()
|
||||
}
|
||||
|
||||
fn play_system_bell(&self) {
|
||||
let state = self.borrow();
|
||||
let surface = if state.surface_state.toplevel().is_some() {
|
||||
Some(&state.surface)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(bell) = state.globals.system_bell.as_ref() {
|
||||
bell.ring(surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_window(mut state: RefMut<WaylandWindowState>) {
|
||||
|
|
|
|||
|
|
@ -1846,4 +1846,9 @@ impl PlatformWindow for X11Window {
|
|||
fn gpu_specs(&self) -> Option<GpuSpecs> {
|
||||
self.0.state.borrow().renderer.gpu_specs().into()
|
||||
}
|
||||
|
||||
fn play_system_bell(&self) {
|
||||
// Volume 0% means don't increase or decrease from system volume
|
||||
let _ = self.0.xcb.bell(0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ mach2.workspace = true
|
|||
media.workspace = true
|
||||
metal.workspace = true
|
||||
objc.workspace = true
|
||||
objc2-app-kit.workspace = true
|
||||
parking_lot.workspace = true
|
||||
pathfinder_geometry = "0.5"
|
||||
raw-window-handle = "0.6"
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ use objc::{
|
|||
runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use objc2_app_kit::NSBeep;
|
||||
use parking_lot::Mutex;
|
||||
use raw_window_handle as rwh;
|
||||
use smallvec::SmallVec;
|
||||
|
|
@ -1676,6 +1677,10 @@ impl PlatformWindow for MacWindow {
|
|||
}
|
||||
}
|
||||
|
||||
fn play_system_bell(&self) {
|
||||
unsafe { NSBeep() }
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn render_to_image(&self, scene: &gpui::Scene) -> Result<RgbaImage> {
|
||||
let mut this = self.0.lock();
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ use windows::{
|
|||
Foundation::*,
|
||||
Graphics::Dwm::*,
|
||||
Graphics::Gdi::*,
|
||||
System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*},
|
||||
System::{
|
||||
Com::*, Diagnostics::Debug::MessageBeep, LibraryLoader::*, Ole::*, SystemServices::*,
|
||||
},
|
||||
UI::{Controls::*, HiDpi::*, Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
|
||||
},
|
||||
core::*,
|
||||
|
|
@ -950,6 +952,11 @@ impl PlatformWindow for WindowsWindow {
|
|||
|
||||
self.0.update_ime_position(self.0.hwnd, caret_position);
|
||||
}
|
||||
|
||||
fn play_system_bell(&self) {
|
||||
// MB_OK: The sound specified as the Windows Default Beep sound.
|
||||
let _ = unsafe { MessageBeep(MB_OK) };
|
||||
}
|
||||
}
|
||||
|
||||
#[implement(IDropTarget)]
|
||||
|
|
|
|||
Loading…
Reference in a new issue