diff --git a/Cargo.lock b/Cargo.lock index f68704cb6ef68887b102f7f6a1a37c0fe694f662..bfd80726843695dbfcb4baf1db4fe3e6ca9a4682 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index d1271e0166677fb4069b5917f320e57c755263b4..3a393237ab9f5a5a8cd4b02517f6d22382ff51ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index d15d791cd008883506389cc7bb16dbad765969c0..370e27de7d54c317af6683c240f343e750c68698 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -85,14 +85,24 @@ impl TextInput { fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { 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) { 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) } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 806a34040a4ec685c3d5c6ec01f47b5026e349a6..efca26a6b4802037a96490bf81f7d1c5c1d8b298 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -689,6 +689,8 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn update_ime_position(&self, _bounds: Bounds); + fn play_system_bell(&self) {} + #[cfg(any(test, feature = "test-support"))] fn as_test(&mut self) -> Option<&mut TestWindow> { None diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 48c381e5275e950bd6754541fedbab03ae3d64c2..7790480e32149fa33dfd082df7a8cdbb09568134 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -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) { diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index b65a203dd3448ba191b7e2f5ae0f5b6c396545a8..10f4aab0db19978302143519dd6e2a7e4d25ec4d 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -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, pub gesture_manager: Option, pub dialog: Option, + pub system_bell: Option, 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 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); diff --git a/crates/gpui_linux/src/linux/wayland/window.rs b/crates/gpui_linux/src/linux/wayland/window.rs index c4ff55fc80cc4d14069dd510b8e6855c17096773..1e3af66c59858c435ca3da093a1c48056b77667e 100644 --- a/crates/gpui_linux/src/linux/wayland/window.rs +++ b/crates/gpui_linux/src/linux/wayland/window.rs @@ -1479,6 +1479,18 @@ impl PlatformWindow for WaylandWindow { fn gpu_specs(&self) -> Option { 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) { diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index 5e1287976cbb3ba9bc2c1571fa9e215f47fdd615..1974cc0bb28f62da4d7dcb3e9fca92b6324470bb 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/crates/gpui_linux/src/linux/x11/window.rs @@ -1846,4 +1846,9 @@ impl PlatformWindow for X11Window { fn gpu_specs(&self) -> Option { 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); + } } diff --git a/crates/gpui_macos/Cargo.toml b/crates/gpui_macos/Cargo.toml index 06e5d0e7321af523a249f19ec0d5ac50e2da5d3f..3626bbd05e8a7c7fa2ae577f11e5277da995d2f7 100644 --- a/crates/gpui_macos/Cargo.toml +++ b/crates/gpui_macos/Cargo.toml @@ -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" diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 398cf46eab09dc8412ffdda8eb550b8ad4e09b40..ace36d695401ce76949129197dcd05135508f7d3 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -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 { let mut this = self.0.lock(); diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index 3a55100dfb75e961f57b977297bfcd2dc2ae2701..92255f93fd95969931c6b1ae8cb465ff628f82cb 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -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)]