From 1dac14c53a6e8cfe76dfa9a9ed772df14ac60a10 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 26 May 2026 08:59:21 -0400 Subject: [PATCH] Remove CRLF line endings (#57680) Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #ISSUE Release Notes: - N/A or Added/Fixed/Improved ... --- crates/gpui_web/src/dispatcher.rs | 666 ++++---- crates/gpui_web/src/display.rs | 196 +-- crates/gpui_web/src/events.rs | 1364 +++++++-------- crates/gpui_web/src/keyboard.rs | 38 +- crates/gpui_web/src/logging.rs | 74 +- crates/gpui_web/src/platform.rs | 870 +++++----- crates/gpui_web/src/window.rs | 1462 ++++++++--------- crates/gpui_wgpu/src/shaders_subpixel.wgsl | 112 +- crates/install_cli/src/install_cli_binary.rs | 202 +-- crates/install_cli/src/register_zed_scheme.rs | 28 +- crates/multi_buffer/src/transaction.rs | 1034 ++++++------ crates/remote/src/transport/mock.rs | 684 ++++---- 12 files changed, 3365 insertions(+), 3365 deletions(-) diff --git a/crates/gpui_web/src/dispatcher.rs b/crates/gpui_web/src/dispatcher.rs index 5a0911f7ef1..9c45de1b0ef 100644 --- a/crates/gpui_web/src/dispatcher.rs +++ b/crates/gpui_web/src/dispatcher.rs @@ -1,333 +1,333 @@ -use gpui::{ - PlatformDispatcher, Priority, PriorityQueueReceiver, PriorityQueueSender, RunnableVariant, - ThreadTaskTimings, -}; -use std::sync::Arc; -use std::sync::atomic::AtomicI32; -use std::time::Duration; -use wasm_bindgen::prelude::*; -use web_time::Instant; - -#[cfg(feature = "multithreaded")] -const MIN_BACKGROUND_THREADS: usize = 2; - -#[cfg(feature = "multithreaded")] -fn shared_memory_supported() -> bool { - let global = js_sys::global(); - let has_shared_array_buffer = - js_sys::Reflect::has(&global, &JsValue::from_str("SharedArrayBuffer")).unwrap_or(false); - let has_atomics = js_sys::Reflect::has(&global, &JsValue::from_str("Atomics")).unwrap_or(false); - let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory()); - let buffer = memory.buffer(); - let is_shared_buffer = buffer.is_instance_of::(); - has_shared_array_buffer && has_atomics && is_shared_buffer -} - -enum MainThreadItem { - Runnable(RunnableVariant), - Delayed { - runnable: RunnableVariant, - millis: i32, - }, - // TODO-Wasm: Shouldn't these run on their own dedicated thread? - RealtimeFunction(Box), -} - -struct MainThreadMailbox { - sender: PriorityQueueSender, - receiver: parking_lot::Mutex>, - signal: AtomicI32, -} - -impl MainThreadMailbox { - fn new() -> Self { - let (sender, receiver) = PriorityQueueReceiver::new(); - Self { - sender, - receiver: parking_lot::Mutex::new(receiver), - signal: AtomicI32::new(0), - } - } - - fn post(&self, priority: Priority, item: MainThreadItem) { - if self.sender.spin_send(priority, item).is_err() { - log::error!("MainThreadMailbox::send failed: receiver disconnected"); - } - - // TODO-Wasm: Verify this lock-free protocol - let view = self.signal_view(); - js_sys::Atomics::store(&view, 0, 1).ok(); - js_sys::Atomics::notify(&view, 0).ok(); - } - - fn drain(&self, window: &web_sys::Window) { - let mut receiver = self.receiver.lock(); - loop { - // We need these `spin` variants because we can't acquire a lock on the main thread. - // TODO-WASM: Should we do something different? - match receiver.spin_try_pop() { - Ok(Some(item)) => execute_on_main_thread(window, item), - Ok(None) => break, - Err(_) => break, - } - } - } - - fn signal_view(&self) -> js_sys::Int32Array { - let byte_offset = self.signal.as_ptr() as u32; - let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory()); - js_sys::Int32Array::new_with_byte_offset_and_length(&memory.buffer(), byte_offset, 1) - } - - fn run_waker_loop(self: &Arc, window: web_sys::Window) { - if !shared_memory_supported() { - log::warn!("SharedArrayBuffer not available; main thread mailbox waker loop disabled"); - return; - } - - let mailbox = Arc::clone(self); - wasm_bindgen_futures::spawn_local(async move { - let view = mailbox.signal_view(); - loop { - js_sys::Atomics::store(&view, 0, 0).expect("Atomics.store failed"); - - let result = match js_sys::Atomics::wait_async(&view, 0, 0) { - Ok(result) => result, - Err(error) => { - log::error!("Atomics.waitAsync failed: {error:?}"); - break; - } - }; - - let is_async = js_sys::Reflect::get(&result, &JsValue::from_str("async")) - .ok() - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - if !is_async { - log::error!("Atomics.waitAsync returned synchronously; waker loop exiting"); - break; - } - - let promise: js_sys::Promise = - js_sys::Reflect::get(&result, &JsValue::from_str("value")) - .expect("waitAsync result missing 'value'") - .unchecked_into(); - - let _ = wasm_bindgen_futures::JsFuture::from(promise).await; - - mailbox.drain(&window); - } - }); - } -} - -pub struct WebDispatcher { - main_thread_id: std::thread::ThreadId, - browser_window: web_sys::Window, - background_sender: PriorityQueueSender, - main_thread_mailbox: Arc, - supports_threads: bool, - #[cfg(feature = "multithreaded")] - _background_threads: Vec>, -} - -// Safety: `web_sys::Window` is only accessed from the main thread -// All other fields are `Send + Sync` by construction. -unsafe impl Send for WebDispatcher {} -unsafe impl Sync for WebDispatcher {} - -impl WebDispatcher { - pub fn new(browser_window: web_sys::Window, allow_threads: bool) -> Self { - #[cfg(feature = "multithreaded")] - let (background_sender, background_receiver) = PriorityQueueReceiver::new(); - #[cfg(not(feature = "multithreaded"))] - let (background_sender, _) = PriorityQueueReceiver::new(); - - let main_thread_mailbox = Arc::new(MainThreadMailbox::new()); - - #[cfg(feature = "multithreaded")] - let supports_threads = allow_threads && shared_memory_supported(); - #[cfg(not(feature = "multithreaded"))] - let supports_threads = false; - - if supports_threads { - main_thread_mailbox.run_waker_loop(browser_window.clone()); - } else { - log::warn!( - "SharedArrayBuffer not available; falling back to single-threaded dispatcher" - ); - } - - #[cfg(feature = "multithreaded")] - let background_threads = if supports_threads { - let thread_count = browser_window - .navigator() - .hardware_concurrency() - .max(MIN_BACKGROUND_THREADS as f64) as usize; - - // TODO-Wasm: Is it bad to have web workers blocking for a long time like this? - (0..thread_count) - .map(|i| { - let mut receiver = background_receiver.clone(); - wasm_thread::Builder::new() - .name(format!("background-worker-{i}")) - .spawn(move || { - loop { - let runnable: RunnableVariant = match receiver.pop() { - Ok(runnable) => runnable, - Err(_) => { - log::info!( - "background-worker-{i}: channel disconnected, exiting" - ); - break; - } - }; - - runnable.run(); - } - }) - .expect("failed to spawn background worker thread") - }) - .collect::>() - } else { - Vec::new() - }; - - Self { - main_thread_id: std::thread::current().id(), - browser_window, - background_sender, - main_thread_mailbox, - supports_threads, - #[cfg(feature = "multithreaded")] - _background_threads: background_threads, - } - } - - fn on_main_thread(&self) -> bool { - std::thread::current().id() == self.main_thread_id - } -} - -impl PlatformDispatcher for WebDispatcher { - fn get_all_timings(&self) -> Vec { - // TODO-Wasm: should we panic here? - Vec::new() - } - - fn get_current_thread_timings(&self) -> ThreadTaskTimings { - ThreadTaskTimings { - thread_name: None, - thread_id: std::thread::current().id(), - timings: Vec::new(), - total_pushed: 0, - } - } - - fn is_main_thread(&self) -> bool { - self.on_main_thread() - } - - fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { - if !self.supports_threads { - self.dispatch_on_main_thread(runnable, priority); - return; - } - - let result = if self.on_main_thread() { - self.background_sender.spin_send(priority, runnable) - } else { - self.background_sender.send(priority, runnable) - }; - - if let Err(error) = result { - log::error!("dispatch: failed to send to background queue: {error:?}"); - } - } - - fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) { - if self.on_main_thread() { - schedule_runnable(&self.browser_window, runnable, priority); - } else { - self.main_thread_mailbox - .post(priority, MainThreadItem::Runnable(runnable)); - } - } - - fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) { - let millis = duration.as_millis().min(i32::MAX as u128) as i32; - if self.on_main_thread() { - let callback = Closure::once_into_js(move || { - runnable.run(); - }); - self.browser_window - .set_timeout_with_callback_and_timeout_and_arguments_0( - callback.unchecked_ref(), - millis, - ) - .ok(); - } else { - self.main_thread_mailbox - .post(Priority::High, MainThreadItem::Delayed { runnable, millis }); - } - } - - fn spawn_realtime(&self, function: Box) { - if self.on_main_thread() { - let callback = Closure::once_into_js(move || { - function(); - }); - self.browser_window - .queue_microtask(callback.unchecked_ref()); - } else { - self.main_thread_mailbox - .post(Priority::High, MainThreadItem::RealtimeFunction(function)); - } - } - - fn now(&self) -> Instant { - Instant::now() - } -} - -fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) { - match item { - MainThreadItem::Runnable(runnable) => { - runnable.run(); - } - MainThreadItem::Delayed { runnable, millis } => { - let callback = Closure::once_into_js(move || { - runnable.run(); - }); - window - .set_timeout_with_callback_and_timeout_and_arguments_0( - callback.unchecked_ref(), - millis, - ) - .ok(); - } - MainThreadItem::RealtimeFunction(function) => { - function(); - } - } -} - -fn schedule_runnable(window: &web_sys::Window, runnable: RunnableVariant, priority: Priority) { - let callback = Closure::once_into_js(move || { - runnable.run(); - }); - let callback: &js_sys::Function = callback.unchecked_ref(); - - match priority { - Priority::RealtimeAudio => { - window.queue_microtask(callback); - } - _ => { - // TODO-Wasm: this ought to enqueue so we can dequeue with proper priority - window - .set_timeout_with_callback_and_timeout_and_arguments_0(callback, 0) - .ok(); - } - } -} +use gpui::{ + PlatformDispatcher, Priority, PriorityQueueReceiver, PriorityQueueSender, RunnableVariant, + ThreadTaskTimings, +}; +use std::sync::Arc; +use std::sync::atomic::AtomicI32; +use std::time::Duration; +use wasm_bindgen::prelude::*; +use web_time::Instant; + +#[cfg(feature = "multithreaded")] +const MIN_BACKGROUND_THREADS: usize = 2; + +#[cfg(feature = "multithreaded")] +fn shared_memory_supported() -> bool { + let global = js_sys::global(); + let has_shared_array_buffer = + js_sys::Reflect::has(&global, &JsValue::from_str("SharedArrayBuffer")).unwrap_or(false); + let has_atomics = js_sys::Reflect::has(&global, &JsValue::from_str("Atomics")).unwrap_or(false); + let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory()); + let buffer = memory.buffer(); + let is_shared_buffer = buffer.is_instance_of::(); + has_shared_array_buffer && has_atomics && is_shared_buffer +} + +enum MainThreadItem { + Runnable(RunnableVariant), + Delayed { + runnable: RunnableVariant, + millis: i32, + }, + // TODO-Wasm: Shouldn't these run on their own dedicated thread? + RealtimeFunction(Box), +} + +struct MainThreadMailbox { + sender: PriorityQueueSender, + receiver: parking_lot::Mutex>, + signal: AtomicI32, +} + +impl MainThreadMailbox { + fn new() -> Self { + let (sender, receiver) = PriorityQueueReceiver::new(); + Self { + sender, + receiver: parking_lot::Mutex::new(receiver), + signal: AtomicI32::new(0), + } + } + + fn post(&self, priority: Priority, item: MainThreadItem) { + if self.sender.spin_send(priority, item).is_err() { + log::error!("MainThreadMailbox::send failed: receiver disconnected"); + } + + // TODO-Wasm: Verify this lock-free protocol + let view = self.signal_view(); + js_sys::Atomics::store(&view, 0, 1).ok(); + js_sys::Atomics::notify(&view, 0).ok(); + } + + fn drain(&self, window: &web_sys::Window) { + let mut receiver = self.receiver.lock(); + loop { + // We need these `spin` variants because we can't acquire a lock on the main thread. + // TODO-WASM: Should we do something different? + match receiver.spin_try_pop() { + Ok(Some(item)) => execute_on_main_thread(window, item), + Ok(None) => break, + Err(_) => break, + } + } + } + + fn signal_view(&self) -> js_sys::Int32Array { + let byte_offset = self.signal.as_ptr() as u32; + let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory()); + js_sys::Int32Array::new_with_byte_offset_and_length(&memory.buffer(), byte_offset, 1) + } + + fn run_waker_loop(self: &Arc, window: web_sys::Window) { + if !shared_memory_supported() { + log::warn!("SharedArrayBuffer not available; main thread mailbox waker loop disabled"); + return; + } + + let mailbox = Arc::clone(self); + wasm_bindgen_futures::spawn_local(async move { + let view = mailbox.signal_view(); + loop { + js_sys::Atomics::store(&view, 0, 0).expect("Atomics.store failed"); + + let result = match js_sys::Atomics::wait_async(&view, 0, 0) { + Ok(result) => result, + Err(error) => { + log::error!("Atomics.waitAsync failed: {error:?}"); + break; + } + }; + + let is_async = js_sys::Reflect::get(&result, &JsValue::from_str("async")) + .ok() + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + if !is_async { + log::error!("Atomics.waitAsync returned synchronously; waker loop exiting"); + break; + } + + let promise: js_sys::Promise = + js_sys::Reflect::get(&result, &JsValue::from_str("value")) + .expect("waitAsync result missing 'value'") + .unchecked_into(); + + let _ = wasm_bindgen_futures::JsFuture::from(promise).await; + + mailbox.drain(&window); + } + }); + } +} + +pub struct WebDispatcher { + main_thread_id: std::thread::ThreadId, + browser_window: web_sys::Window, + background_sender: PriorityQueueSender, + main_thread_mailbox: Arc, + supports_threads: bool, + #[cfg(feature = "multithreaded")] + _background_threads: Vec>, +} + +// Safety: `web_sys::Window` is only accessed from the main thread +// All other fields are `Send + Sync` by construction. +unsafe impl Send for WebDispatcher {} +unsafe impl Sync for WebDispatcher {} + +impl WebDispatcher { + pub fn new(browser_window: web_sys::Window, allow_threads: bool) -> Self { + #[cfg(feature = "multithreaded")] + let (background_sender, background_receiver) = PriorityQueueReceiver::new(); + #[cfg(not(feature = "multithreaded"))] + let (background_sender, _) = PriorityQueueReceiver::new(); + + let main_thread_mailbox = Arc::new(MainThreadMailbox::new()); + + #[cfg(feature = "multithreaded")] + let supports_threads = allow_threads && shared_memory_supported(); + #[cfg(not(feature = "multithreaded"))] + let supports_threads = false; + + if supports_threads { + main_thread_mailbox.run_waker_loop(browser_window.clone()); + } else { + log::warn!( + "SharedArrayBuffer not available; falling back to single-threaded dispatcher" + ); + } + + #[cfg(feature = "multithreaded")] + let background_threads = if supports_threads { + let thread_count = browser_window + .navigator() + .hardware_concurrency() + .max(MIN_BACKGROUND_THREADS as f64) as usize; + + // TODO-Wasm: Is it bad to have web workers blocking for a long time like this? + (0..thread_count) + .map(|i| { + let mut receiver = background_receiver.clone(); + wasm_thread::Builder::new() + .name(format!("background-worker-{i}")) + .spawn(move || { + loop { + let runnable: RunnableVariant = match receiver.pop() { + Ok(runnable) => runnable, + Err(_) => { + log::info!( + "background-worker-{i}: channel disconnected, exiting" + ); + break; + } + }; + + runnable.run(); + } + }) + .expect("failed to spawn background worker thread") + }) + .collect::>() + } else { + Vec::new() + }; + + Self { + main_thread_id: std::thread::current().id(), + browser_window, + background_sender, + main_thread_mailbox, + supports_threads, + #[cfg(feature = "multithreaded")] + _background_threads: background_threads, + } + } + + fn on_main_thread(&self) -> bool { + std::thread::current().id() == self.main_thread_id + } +} + +impl PlatformDispatcher for WebDispatcher { + fn get_all_timings(&self) -> Vec { + // TODO-Wasm: should we panic here? + Vec::new() + } + + fn get_current_thread_timings(&self) -> ThreadTaskTimings { + ThreadTaskTimings { + thread_name: None, + thread_id: std::thread::current().id(), + timings: Vec::new(), + total_pushed: 0, + } + } + + fn is_main_thread(&self) -> bool { + self.on_main_thread() + } + + fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { + if !self.supports_threads { + self.dispatch_on_main_thread(runnable, priority); + return; + } + + let result = if self.on_main_thread() { + self.background_sender.spin_send(priority, runnable) + } else { + self.background_sender.send(priority, runnable) + }; + + if let Err(error) = result { + log::error!("dispatch: failed to send to background queue: {error:?}"); + } + } + + fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) { + if self.on_main_thread() { + schedule_runnable(&self.browser_window, runnable, priority); + } else { + self.main_thread_mailbox + .post(priority, MainThreadItem::Runnable(runnable)); + } + } + + fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) { + let millis = duration.as_millis().min(i32::MAX as u128) as i32; + if self.on_main_thread() { + let callback = Closure::once_into_js(move || { + runnable.run(); + }); + self.browser_window + .set_timeout_with_callback_and_timeout_and_arguments_0( + callback.unchecked_ref(), + millis, + ) + .ok(); + } else { + self.main_thread_mailbox + .post(Priority::High, MainThreadItem::Delayed { runnable, millis }); + } + } + + fn spawn_realtime(&self, function: Box) { + if self.on_main_thread() { + let callback = Closure::once_into_js(move || { + function(); + }); + self.browser_window + .queue_microtask(callback.unchecked_ref()); + } else { + self.main_thread_mailbox + .post(Priority::High, MainThreadItem::RealtimeFunction(function)); + } + } + + fn now(&self) -> Instant { + Instant::now() + } +} + +fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) { + match item { + MainThreadItem::Runnable(runnable) => { + runnable.run(); + } + MainThreadItem::Delayed { runnable, millis } => { + let callback = Closure::once_into_js(move || { + runnable.run(); + }); + window + .set_timeout_with_callback_and_timeout_and_arguments_0( + callback.unchecked_ref(), + millis, + ) + .ok(); + } + MainThreadItem::RealtimeFunction(function) => { + function(); + } + } +} + +fn schedule_runnable(window: &web_sys::Window, runnable: RunnableVariant, priority: Priority) { + let callback = Closure::once_into_js(move || { + runnable.run(); + }); + let callback: &js_sys::Function = callback.unchecked_ref(); + + match priority { + Priority::RealtimeAudio => { + window.queue_microtask(callback); + } + _ => { + // TODO-Wasm: this ought to enqueue so we can dequeue with proper priority + window + .set_timeout_with_callback_and_timeout_and_arguments_0(callback, 0) + .ok(); + } + } +} diff --git a/crates/gpui_web/src/display.rs b/crates/gpui_web/src/display.rs index 77dd35d9236..5023e7de33e 100644 --- a/crates/gpui_web/src/display.rs +++ b/crates/gpui_web/src/display.rs @@ -1,98 +1,98 @@ -use anyhow::Result; -use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, Size, px}; - -#[derive(Debug)] -pub struct WebDisplay { - id: DisplayId, - uuid: uuid::Uuid, - browser_window: web_sys::Window, -} - -// Safety: WASM is single-threaded — there is no concurrent access to `web_sys::Window`. -unsafe impl Send for WebDisplay {} -unsafe impl Sync for WebDisplay {} - -impl WebDisplay { - pub fn new(browser_window: web_sys::Window) -> Self { - WebDisplay { - id: DisplayId::new(1), - uuid: uuid::Uuid::new_v4(), - browser_window, - } - } - - fn screen_size(&self) -> Size { - let Some(screen) = self.browser_window.screen().ok() else { - return Size { - width: px(1920.), - height: px(1080.), - }; - }; - - let width = screen.width().unwrap_or(1920) as f32; - let height = screen.height().unwrap_or(1080) as f32; - - Size { - width: px(width), - height: px(height), - } - } - - fn viewport_size(&self) -> Size { - let width = self - .browser_window - .inner_width() - .ok() - .and_then(|v| v.as_f64()) - .unwrap_or(1920.0) as f32; - let height = self - .browser_window - .inner_height() - .ok() - .and_then(|v| v.as_f64()) - .unwrap_or(1080.0) as f32; - - Size { - width: px(width), - height: px(height), - } - } -} - -impl PlatformDisplay for WebDisplay { - fn id(&self) -> DisplayId { - self.id - } - - fn uuid(&self) -> Result { - Ok(self.uuid) - } - - fn bounds(&self) -> Bounds { - let size = self.screen_size(); - Bounds { - origin: Point::default(), - size, - } - } - - fn visible_bounds(&self) -> Bounds { - let size = self.viewport_size(); - Bounds { - origin: Point::default(), - size, - } - } - - fn default_bounds(&self) -> Bounds { - let visible = self.visible_bounds(); - let width = visible.size.width * 0.75; - let height = visible.size.height * 0.75; - let origin_x = (visible.size.width - width) / 2.0; - let origin_y = (visible.size.height - height) / 2.0; - Bounds { - origin: Point::new(origin_x, origin_y), - size: Size { width, height }, - } - } -} +use anyhow::Result; +use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, Size, px}; + +#[derive(Debug)] +pub struct WebDisplay { + id: DisplayId, + uuid: uuid::Uuid, + browser_window: web_sys::Window, +} + +// Safety: WASM is single-threaded — there is no concurrent access to `web_sys::Window`. +unsafe impl Send for WebDisplay {} +unsafe impl Sync for WebDisplay {} + +impl WebDisplay { + pub fn new(browser_window: web_sys::Window) -> Self { + WebDisplay { + id: DisplayId::new(1), + uuid: uuid::Uuid::new_v4(), + browser_window, + } + } + + fn screen_size(&self) -> Size { + let Some(screen) = self.browser_window.screen().ok() else { + return Size { + width: px(1920.), + height: px(1080.), + }; + }; + + let width = screen.width().unwrap_or(1920) as f32; + let height = screen.height().unwrap_or(1080) as f32; + + Size { + width: px(width), + height: px(height), + } + } + + fn viewport_size(&self) -> Size { + let width = self + .browser_window + .inner_width() + .ok() + .and_then(|v| v.as_f64()) + .unwrap_or(1920.0) as f32; + let height = self + .browser_window + .inner_height() + .ok() + .and_then(|v| v.as_f64()) + .unwrap_or(1080.0) as f32; + + Size { + width: px(width), + height: px(height), + } + } +} + +impl PlatformDisplay for WebDisplay { + fn id(&self) -> DisplayId { + self.id + } + + fn uuid(&self) -> Result { + Ok(self.uuid) + } + + fn bounds(&self) -> Bounds { + let size = self.screen_size(); + Bounds { + origin: Point::default(), + size, + } + } + + fn visible_bounds(&self) -> Bounds { + let size = self.viewport_size(); + Bounds { + origin: Point::default(), + size, + } + } + + fn default_bounds(&self) -> Bounds { + let visible = self.visible_bounds(); + let width = visible.size.width * 0.75; + let height = visible.size.height * 0.75; + let origin_x = (visible.size.width - width) / 2.0; + let origin_y = (visible.size.height - height) / 2.0; + Bounds { + origin: Point::new(origin_x, origin_y), + size: Size { width, height }, + } + } +} diff --git a/crates/gpui_web/src/events.rs b/crates/gpui_web/src/events.rs index e93534fbe88..46be646cb56 100644 --- a/crates/gpui_web/src/events.rs +++ b/crates/gpui_web/src/events.rs @@ -1,682 +1,682 @@ -use std::rc::Rc; - -use gpui::{ - Capslock, DispatchEventResult, ExternalPaths, FileDropEvent, KeyDownEvent, KeyUpEvent, - Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, - MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformInput, Point, ScrollDelta, - ScrollWheelEvent, TouchPhase, point, px, -}; -use smallvec::smallvec; -use wasm_bindgen::prelude::*; - -use crate::window::WebWindowInner; - -pub struct WebEventListeners { - #[allow(dead_code)] - closures: Vec>, -} - -pub(crate) struct ClickState { - last_position: Point, - last_time: f64, - current_count: usize, -} - -impl Default for ClickState { - fn default() -> Self { - Self { - last_position: Point::default(), - last_time: 0.0, - current_count: 0, - } - } -} - -impl ClickState { - fn register_click(&mut self, position: Point, time: f64) -> usize { - let distance = ((f32::from(position.x) - f32::from(self.last_position.x)).powi(2) - + (f32::from(position.y) - f32::from(self.last_position.y)).powi(2)) - .sqrt(); - - if (time - self.last_time) < 400.0 && distance < 5.0 { - self.current_count += 1; - } else { - self.current_count = 1; - } - - self.last_position = position; - self.last_time = time; - self.current_count - } -} - -impl WebWindowInner { - pub fn register_event_listeners(self: &Rc) -> WebEventListeners { - let mut closures = vec![ - self.register_pointer_down(), - self.register_pointer_up(), - self.register_pointer_move(), - self.register_pointer_leave(), - self.register_wheel(), - self.register_context_menu(), - self.register_dragover(), - self.register_drop(), - self.register_dragleave(), - self.register_key_down(), - self.register_key_up(), - self.register_composition_start(), - self.register_composition_update(), - self.register_composition_end(), - self.register_focus(), - self.register_blur(), - self.register_pointer_enter(), - self.register_pointer_leave_hover(), - ]; - closures.extend(self.register_visibility_change()); - closures.extend(self.register_appearance_change()); - - WebEventListeners { closures } - } - - fn listen( - self: &Rc, - event_name: &str, - handler: impl FnMut(JsValue) + 'static, - ) -> Closure { - let closure = Closure::::new(handler); - self.canvas - .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) - .ok(); - closure - } - - fn listen_input( - self: &Rc, - event_name: &str, - handler: impl FnMut(JsValue) + 'static, - ) -> Closure { - let closure = Closure::::new(handler); - self.input_element - .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) - .ok(); - closure - } - - /// Registers a listener with `{passive: false}` so that `preventDefault()` works. - /// Needed for events like `wheel` which are passive by default in modern browsers. - fn listen_non_passive( - self: &Rc, - event_name: &str, - handler: impl FnMut(JsValue) + 'static, - ) -> Closure { - let closure = Closure::::new(handler); - let canvas_js: &JsValue = self.canvas.as_ref(); - let callback_js: &JsValue = closure.as_ref(); - let options = js_sys::Object::new(); - js_sys::Reflect::set(&options, &"passive".into(), &false.into()).ok(); - if let Ok(add_fn_val) = js_sys::Reflect::get(canvas_js, &"addEventListener".into()) { - if let Ok(add_fn) = add_fn_val.dyn_into::() { - add_fn - .call3(canvas_js, &event_name.into(), callback_js, &options) - .ok(); - } - } - closure - } - - fn dispatch_input(&self, input: PlatformInput) -> Option { - let mut borrowed = self.callbacks.borrow_mut(); - borrowed.input.as_mut().map(|callback| callback(input)) - } - - fn register_pointer_down(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("pointerdown", move |event: JsValue| { - let event: web_sys::PointerEvent = event.unchecked_into(); - event.prevent_default(); - this.input_element.focus().ok(); - - let button = dom_mouse_button_to_gpui(event.button()); - let position = pointer_position_in_element(&event); - let modifiers = modifiers_from_mouse_event(&event, this.is_mac); - let time = js_sys::Date::now(); - - this.pressed_button.set(Some(button)); - let click_count = this.click_state.borrow_mut().register_click(position, time); - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - current_state.modifiers = modifiers; - } - - this.dispatch_input(PlatformInput::MouseDown(MouseDownEvent { - button, - position, - modifiers, - click_count, - first_mouse: false, - })); - }) - } - - fn register_pointer_up(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("pointerup", move |event: JsValue| { - let event: web_sys::PointerEvent = event.unchecked_into(); - event.prevent_default(); - - let button = dom_mouse_button_to_gpui(event.button()); - let position = pointer_position_in_element(&event); - let modifiers = modifiers_from_mouse_event(&event, this.is_mac); - - this.pressed_button.set(None); - let click_count = this.click_state.borrow().current_count; - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - current_state.modifiers = modifiers; - } - - this.dispatch_input(PlatformInput::MouseUp(MouseUpEvent { - button, - position, - modifiers, - click_count, - })); - }) - } - - fn register_pointer_move(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("pointermove", move |event: JsValue| { - let event: web_sys::PointerEvent = event.unchecked_into(); - event.prevent_default(); - - let position = pointer_position_in_element(&event); - let modifiers = modifiers_from_mouse_event(&event, this.is_mac); - let current_pressed = this.pressed_button.get(); - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - current_state.modifiers = modifiers; - } - - this.dispatch_input(PlatformInput::MouseMove(MouseMoveEvent { - position, - pressed_button: current_pressed, - modifiers, - })); - }) - } - - fn register_pointer_leave(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("pointerleave", move |event: JsValue| { - let event: web_sys::PointerEvent = event.unchecked_into(); - - let position = pointer_position_in_element(&event); - let modifiers = modifiers_from_mouse_event(&event, this.is_mac); - let current_pressed = this.pressed_button.get(); - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - current_state.modifiers = modifiers; - } - - this.dispatch_input(PlatformInput::MouseExited(MouseExitEvent { - position, - pressed_button: current_pressed, - modifiers, - })); - }) - } - - fn register_wheel(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen_non_passive("wheel", move |event: JsValue| { - let event: web_sys::WheelEvent = event.unchecked_into(); - event.prevent_default(); - - let mouse_event: &web_sys::MouseEvent = event.as_ref(); - let position = mouse_position_in_element(mouse_event); - let modifiers = modifiers_from_wheel_event(mouse_event, this.is_mac); - - let delta_mode = event.delta_mode(); - let delta = if delta_mode == 1 { - ScrollDelta::Lines(point(-event.delta_x() as f32, -event.delta_y() as f32)) - } else { - ScrollDelta::Pixels(point( - px(-event.delta_x() as f32), - px(-event.delta_y() as f32), - )) - }; - - { - let mut current_state = this.state.borrow_mut(); - current_state.modifiers = modifiers; - } - - this.dispatch_input(PlatformInput::ScrollWheel(ScrollWheelEvent { - position, - delta, - modifiers, - touch_phase: TouchPhase::Moved, - })); - }) - } - - fn register_context_menu(self: &Rc) -> Closure { - self.listen("contextmenu", move |event: JsValue| { - let event: web_sys::Event = event.unchecked_into(); - event.prevent_default(); - }) - } - - fn register_dragover(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("dragover", move |event: JsValue| { - let event: web_sys::DragEvent = event.unchecked_into(); - event.prevent_default(); - - let mouse_event: &web_sys::MouseEvent = event.as_ref(); - let position = mouse_position_in_element(mouse_event); - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - } - - this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); - }) - } - - fn register_drop(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("drop", move |event: JsValue| { - let event: web_sys::DragEvent = event.unchecked_into(); - event.prevent_default(); - - let mouse_event: &web_sys::MouseEvent = event.as_ref(); - let position = mouse_position_in_element(mouse_event); - - { - let mut current_state = this.state.borrow_mut(); - current_state.mouse_position = position; - } - - let paths = extract_file_paths_from_drag(&event); - - this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Entered { - position, - paths: ExternalPaths(paths), - })); - - this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Submit { position })); - }) - } - - fn register_dragleave(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("dragleave", move |_event: JsValue| { - this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Exited)); - }) - } - - fn register_key_down(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen_input("keydown", move |event: JsValue| { - let event: web_sys::KeyboardEvent = event.unchecked_into(); - - let modifiers = modifiers_from_keyboard_event(&event, this.is_mac); - let capslock = capslock_from_keyboard_event(&event); - - { - let mut current_state = this.state.borrow_mut(); - current_state.modifiers = modifiers; - current_state.capslock = capslock; - } - - this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers, - capslock, - })); - - let key = dom_key_to_gpui_key(&event); - - if is_modifier_only_key(&key) { - return; - } - - event.prevent_default(); - - let is_held = event.repeat(); - let key_char = compute_key_char(&event, &key, &modifiers); - - let keystroke = Keystroke { - modifiers, - key, - key_char: key_char.clone(), - }; - - let result = this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent { - keystroke, - is_held, - prefer_character_input: false, - })); - - if let Some(result) = result { - if !result.propagate { - return; - } - } - - if this.is_composing.get() || event.is_composing() { - return; - } - - if modifiers.is_subset_of(&Modifiers::shift()) { - if let Some(text) = key_char { - this.with_input_handler(|handler| { - handler.replace_text_in_range(None, &text); - }); - } - } - }) - } - - fn register_key_up(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen_input("keyup", move |event: JsValue| { - let event: web_sys::KeyboardEvent = event.unchecked_into(); - - let modifiers = modifiers_from_keyboard_event(&event, this.is_mac); - let capslock = capslock_from_keyboard_event(&event); - - { - let mut current_state = this.state.borrow_mut(); - current_state.modifiers = modifiers; - current_state.capslock = capslock; - } - - this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { - modifiers, - capslock, - })); - - let key = dom_key_to_gpui_key(&event); - - if is_modifier_only_key(&key) { - return; - } - - event.prevent_default(); - - let key_char = compute_key_char(&event, &key, &modifiers); - - let keystroke = Keystroke { - modifiers, - key, - key_char, - }; - - this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke })); - }) - } - - fn register_composition_start(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen_input("compositionstart", move |_event: JsValue| { - this.is_composing.set(true); - }) - } - - fn register_composition_update(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen_input("compositionupdate", move |event: JsValue| { - let event: web_sys::CompositionEvent = event.unchecked_into(); - let data = event.data().unwrap_or_default(); - this.is_composing.set(true); - this.with_input_handler(|handler| { - handler.replace_and_mark_text_in_range(None, &data, None); - }); - }) - } - - fn register_composition_end(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen_input("compositionend", move |event: JsValue| { - let event: web_sys::CompositionEvent = event.unchecked_into(); - let data = event.data().unwrap_or_default(); - this.is_composing.set(false); - this.with_input_handler(|handler| { - handler.replace_text_in_range(None, &data); - handler.unmark_text(); - }); - this.input_element.set_value(""); - }) - } - - fn register_focus(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen_input("focus", move |_event: JsValue| { - { - let mut state = this.state.borrow_mut(); - state.is_active = true; - } - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.active_status_change { - callback(true); - } - }) - } - - fn register_blur(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen_input("blur", move |_event: JsValue| { - { - let mut state = this.state.borrow_mut(); - state.is_active = false; - } - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.active_status_change { - callback(false); - } - }) - } - - fn register_pointer_enter(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("pointerenter", move |_event: JsValue| { - { - let mut state = this.state.borrow_mut(); - state.is_hovered = true; - } - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.hover_status_change { - callback(true); - } - }) - } - - fn register_pointer_leave_hover(self: &Rc) -> Closure { - let this = Rc::clone(self); - self.listen("pointerleave", move |_event: JsValue| { - { - let mut state = this.state.borrow_mut(); - state.is_hovered = false; - } - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.hover_status_change { - callback(false); - } - }) - } -} - -fn dom_key_to_gpui_key(event: &web_sys::KeyboardEvent) -> String { - let key = event.key(); - match key.as_str() { - "Enter" => "enter".to_string(), - "Backspace" => "backspace".to_string(), - "Tab" => "tab".to_string(), - "Escape" => "escape".to_string(), - "Delete" => "delete".to_string(), - " " => "space".to_string(), - "ArrowLeft" => "left".to_string(), - "ArrowRight" => "right".to_string(), - "ArrowUp" => "up".to_string(), - "ArrowDown" => "down".to_string(), - "Home" => "home".to_string(), - "End" => "end".to_string(), - "PageUp" => "pageup".to_string(), - "PageDown" => "pagedown".to_string(), - "Insert" => "insert".to_string(), - "Control" => "control".to_string(), - "Alt" => "alt".to_string(), - "Shift" => "shift".to_string(), - "Meta" => "platform".to_string(), - "CapsLock" => "capslock".to_string(), - other => { - if let Some(rest) = other.strip_prefix('F') { - if let Ok(number) = rest.parse::() { - if (1..=35).contains(&number) { - return format!("f{number}"); - } - } - } - other.to_lowercase() - } - } -} - -fn dom_mouse_button_to_gpui(button: i16) -> MouseButton { - match button { - 0 => MouseButton::Left, - 1 => MouseButton::Middle, - 2 => MouseButton::Right, - 3 => MouseButton::Navigate(NavigationDirection::Back), - 4 => MouseButton::Navigate(NavigationDirection::Forward), - _ => MouseButton::Left, - } -} - -fn modifiers_from_keyboard_event(event: &web_sys::KeyboardEvent, _is_mac: bool) -> Modifiers { - Modifiers { - control: event.ctrl_key(), - alt: event.alt_key(), - shift: event.shift_key(), - platform: event.meta_key(), - function: false, - } -} - -fn modifiers_from_mouse_event(event: &web_sys::PointerEvent, _is_mac: bool) -> Modifiers { - let mouse_event: &web_sys::MouseEvent = event.as_ref(); - Modifiers { - control: mouse_event.ctrl_key(), - alt: mouse_event.alt_key(), - shift: mouse_event.shift_key(), - platform: mouse_event.meta_key(), - function: false, - } -} - -fn modifiers_from_wheel_event(event: &web_sys::MouseEvent, _is_mac: bool) -> Modifiers { - Modifiers { - control: event.ctrl_key(), - alt: event.alt_key(), - shift: event.shift_key(), - platform: event.meta_key(), - function: false, - } -} - -fn capslock_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Capslock { - Capslock { - on: event.get_modifier_state("CapsLock"), - } -} - -pub(crate) fn is_mac_platform(browser_window: &web_sys::Window) -> bool { - let navigator = browser_window.navigator(); - - #[allow(deprecated)] - // navigator.platform() is deprecated but navigator.userAgentData is not widely available yet - if let Ok(platform) = navigator.platform() { - if platform.contains("Mac") { - return true; - } - } - - if let Ok(user_agent) = navigator.user_agent() { - return user_agent.contains("Mac"); - } - - false -} - -fn is_modifier_only_key(key: &str) -> bool { - matches!( - key, - "control" | "alt" | "shift" | "platform" | "capslock" | "compose" | "process" - ) -} - -fn compute_key_char( - event: &web_sys::KeyboardEvent, - gpui_key: &str, - modifiers: &Modifiers, -) -> Option { - if modifiers.platform || modifiers.control { - return None; - } - - if is_modifier_only_key(gpui_key) { - return None; - } - - if gpui_key == "space" { - return Some(" ".to_string()); - } - - let raw_key = event.key(); - - if raw_key.len() == 1 { - return Some(raw_key); - } - - None -} - -fn pointer_position_in_element(event: &web_sys::PointerEvent) -> Point { - let mouse_event: &web_sys::MouseEvent = event.as_ref(); - mouse_position_in_element(mouse_event) -} - -fn mouse_position_in_element(event: &web_sys::MouseEvent) -> Point { - // offset_x/offset_y give position relative to the target element's padding edge - point(px(event.offset_x() as f32), px(event.offset_y() as f32)) -} - -fn extract_file_paths_from_drag( - event: &web_sys::DragEvent, -) -> smallvec::SmallVec<[std::path::PathBuf; 2]> { - let mut paths = smallvec![]; - let Some(data_transfer) = event.data_transfer() else { - return paths; - }; - let file_list = data_transfer.files(); - let Some(files) = file_list else { - return paths; - }; - for index in 0..files.length() { - if let Some(file) = files.get(index) { - paths.push(std::path::PathBuf::from(file.name())); - } - } - paths -} +use std::rc::Rc; + +use gpui::{ + Capslock, DispatchEventResult, ExternalPaths, FileDropEvent, KeyDownEvent, KeyUpEvent, + Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, + MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformInput, Point, ScrollDelta, + ScrollWheelEvent, TouchPhase, point, px, +}; +use smallvec::smallvec; +use wasm_bindgen::prelude::*; + +use crate::window::WebWindowInner; + +pub struct WebEventListeners { + #[allow(dead_code)] + closures: Vec>, +} + +pub(crate) struct ClickState { + last_position: Point, + last_time: f64, + current_count: usize, +} + +impl Default for ClickState { + fn default() -> Self { + Self { + last_position: Point::default(), + last_time: 0.0, + current_count: 0, + } + } +} + +impl ClickState { + fn register_click(&mut self, position: Point, time: f64) -> usize { + let distance = ((f32::from(position.x) - f32::from(self.last_position.x)).powi(2) + + (f32::from(position.y) - f32::from(self.last_position.y)).powi(2)) + .sqrt(); + + if (time - self.last_time) < 400.0 && distance < 5.0 { + self.current_count += 1; + } else { + self.current_count = 1; + } + + self.last_position = position; + self.last_time = time; + self.current_count + } +} + +impl WebWindowInner { + pub fn register_event_listeners(self: &Rc) -> WebEventListeners { + let mut closures = vec![ + self.register_pointer_down(), + self.register_pointer_up(), + self.register_pointer_move(), + self.register_pointer_leave(), + self.register_wheel(), + self.register_context_menu(), + self.register_dragover(), + self.register_drop(), + self.register_dragleave(), + self.register_key_down(), + self.register_key_up(), + self.register_composition_start(), + self.register_composition_update(), + self.register_composition_end(), + self.register_focus(), + self.register_blur(), + self.register_pointer_enter(), + self.register_pointer_leave_hover(), + ]; + closures.extend(self.register_visibility_change()); + closures.extend(self.register_appearance_change()); + + WebEventListeners { closures } + } + + fn listen( + self: &Rc, + event_name: &str, + handler: impl FnMut(JsValue) + 'static, + ) -> Closure { + let closure = Closure::::new(handler); + self.canvas + .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) + .ok(); + closure + } + + fn listen_input( + self: &Rc, + event_name: &str, + handler: impl FnMut(JsValue) + 'static, + ) -> Closure { + let closure = Closure::::new(handler); + self.input_element + .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) + .ok(); + closure + } + + /// Registers a listener with `{passive: false}` so that `preventDefault()` works. + /// Needed for events like `wheel` which are passive by default in modern browsers. + fn listen_non_passive( + self: &Rc, + event_name: &str, + handler: impl FnMut(JsValue) + 'static, + ) -> Closure { + let closure = Closure::::new(handler); + let canvas_js: &JsValue = self.canvas.as_ref(); + let callback_js: &JsValue = closure.as_ref(); + let options = js_sys::Object::new(); + js_sys::Reflect::set(&options, &"passive".into(), &false.into()).ok(); + if let Ok(add_fn_val) = js_sys::Reflect::get(canvas_js, &"addEventListener".into()) { + if let Ok(add_fn) = add_fn_val.dyn_into::() { + add_fn + .call3(canvas_js, &event_name.into(), callback_js, &options) + .ok(); + } + } + closure + } + + fn dispatch_input(&self, input: PlatformInput) -> Option { + let mut borrowed = self.callbacks.borrow_mut(); + borrowed.input.as_mut().map(|callback| callback(input)) + } + + fn register_pointer_down(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("pointerdown", move |event: JsValue| { + let event: web_sys::PointerEvent = event.unchecked_into(); + event.prevent_default(); + this.input_element.focus().ok(); + + let button = dom_mouse_button_to_gpui(event.button()); + let position = pointer_position_in_element(&event); + let modifiers = modifiers_from_mouse_event(&event, this.is_mac); + let time = js_sys::Date::now(); + + this.pressed_button.set(Some(button)); + let click_count = this.click_state.borrow_mut().register_click(position, time); + + { + let mut current_state = this.state.borrow_mut(); + current_state.mouse_position = position; + current_state.modifiers = modifiers; + } + + this.dispatch_input(PlatformInput::MouseDown(MouseDownEvent { + button, + position, + modifiers, + click_count, + first_mouse: false, + })); + }) + } + + fn register_pointer_up(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("pointerup", move |event: JsValue| { + let event: web_sys::PointerEvent = event.unchecked_into(); + event.prevent_default(); + + let button = dom_mouse_button_to_gpui(event.button()); + let position = pointer_position_in_element(&event); + let modifiers = modifiers_from_mouse_event(&event, this.is_mac); + + this.pressed_button.set(None); + let click_count = this.click_state.borrow().current_count; + + { + let mut current_state = this.state.borrow_mut(); + current_state.mouse_position = position; + current_state.modifiers = modifiers; + } + + this.dispatch_input(PlatformInput::MouseUp(MouseUpEvent { + button, + position, + modifiers, + click_count, + })); + }) + } + + fn register_pointer_move(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("pointermove", move |event: JsValue| { + let event: web_sys::PointerEvent = event.unchecked_into(); + event.prevent_default(); + + let position = pointer_position_in_element(&event); + let modifiers = modifiers_from_mouse_event(&event, this.is_mac); + let current_pressed = this.pressed_button.get(); + + { + let mut current_state = this.state.borrow_mut(); + current_state.mouse_position = position; + current_state.modifiers = modifiers; + } + + this.dispatch_input(PlatformInput::MouseMove(MouseMoveEvent { + position, + pressed_button: current_pressed, + modifiers, + })); + }) + } + + fn register_pointer_leave(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("pointerleave", move |event: JsValue| { + let event: web_sys::PointerEvent = event.unchecked_into(); + + let position = pointer_position_in_element(&event); + let modifiers = modifiers_from_mouse_event(&event, this.is_mac); + let current_pressed = this.pressed_button.get(); + + { + let mut current_state = this.state.borrow_mut(); + current_state.mouse_position = position; + current_state.modifiers = modifiers; + } + + this.dispatch_input(PlatformInput::MouseExited(MouseExitEvent { + position, + pressed_button: current_pressed, + modifiers, + })); + }) + } + + fn register_wheel(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_non_passive("wheel", move |event: JsValue| { + let event: web_sys::WheelEvent = event.unchecked_into(); + event.prevent_default(); + + let mouse_event: &web_sys::MouseEvent = event.as_ref(); + let position = mouse_position_in_element(mouse_event); + let modifiers = modifiers_from_wheel_event(mouse_event, this.is_mac); + + let delta_mode = event.delta_mode(); + let delta = if delta_mode == 1 { + ScrollDelta::Lines(point(-event.delta_x() as f32, -event.delta_y() as f32)) + } else { + ScrollDelta::Pixels(point( + px(-event.delta_x() as f32), + px(-event.delta_y() as f32), + )) + }; + + { + let mut current_state = this.state.borrow_mut(); + current_state.modifiers = modifiers; + } + + this.dispatch_input(PlatformInput::ScrollWheel(ScrollWheelEvent { + position, + delta, + modifiers, + touch_phase: TouchPhase::Moved, + })); + }) + } + + fn register_context_menu(self: &Rc) -> Closure { + self.listen("contextmenu", move |event: JsValue| { + let event: web_sys::Event = event.unchecked_into(); + event.prevent_default(); + }) + } + + fn register_dragover(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("dragover", move |event: JsValue| { + let event: web_sys::DragEvent = event.unchecked_into(); + event.prevent_default(); + + let mouse_event: &web_sys::MouseEvent = event.as_ref(); + let position = mouse_position_in_element(mouse_event); + + { + let mut current_state = this.state.borrow_mut(); + current_state.mouse_position = position; + } + + this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); + }) + } + + fn register_drop(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("drop", move |event: JsValue| { + let event: web_sys::DragEvent = event.unchecked_into(); + event.prevent_default(); + + let mouse_event: &web_sys::MouseEvent = event.as_ref(); + let position = mouse_position_in_element(mouse_event); + + { + let mut current_state = this.state.borrow_mut(); + current_state.mouse_position = position; + } + + let paths = extract_file_paths_from_drag(&event); + + this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Entered { + position, + paths: ExternalPaths(paths), + })); + + this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Submit { position })); + }) + } + + fn register_dragleave(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("dragleave", move |_event: JsValue| { + this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Exited)); + }) + } + + fn register_key_down(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("keydown", move |event: JsValue| { + let event: web_sys::KeyboardEvent = event.unchecked_into(); + + let modifiers = modifiers_from_keyboard_event(&event, this.is_mac); + let capslock = capslock_from_keyboard_event(&event); + + { + let mut current_state = this.state.borrow_mut(); + current_state.modifiers = modifiers; + current_state.capslock = capslock; + } + + this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers, + capslock, + })); + + let key = dom_key_to_gpui_key(&event); + + if is_modifier_only_key(&key) { + return; + } + + event.prevent_default(); + + let is_held = event.repeat(); + let key_char = compute_key_char(&event, &key, &modifiers); + + let keystroke = Keystroke { + modifiers, + key, + key_char: key_char.clone(), + }; + + let result = this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held, + prefer_character_input: false, + })); + + if let Some(result) = result { + if !result.propagate { + return; + } + } + + if this.is_composing.get() || event.is_composing() { + return; + } + + if modifiers.is_subset_of(&Modifiers::shift()) { + if let Some(text) = key_char { + this.with_input_handler(|handler| { + handler.replace_text_in_range(None, &text); + }); + } + } + }) + } + + fn register_key_up(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("keyup", move |event: JsValue| { + let event: web_sys::KeyboardEvent = event.unchecked_into(); + + let modifiers = modifiers_from_keyboard_event(&event, this.is_mac); + let capslock = capslock_from_keyboard_event(&event); + + { + let mut current_state = this.state.borrow_mut(); + current_state.modifiers = modifiers; + current_state.capslock = capslock; + } + + this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { + modifiers, + capslock, + })); + + let key = dom_key_to_gpui_key(&event); + + if is_modifier_only_key(&key) { + return; + } + + event.prevent_default(); + + let key_char = compute_key_char(&event, &key, &modifiers); + + let keystroke = Keystroke { + modifiers, + key, + key_char, + }; + + this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke })); + }) + } + + fn register_composition_start(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("compositionstart", move |_event: JsValue| { + this.is_composing.set(true); + }) + } + + fn register_composition_update(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("compositionupdate", move |event: JsValue| { + let event: web_sys::CompositionEvent = event.unchecked_into(); + let data = event.data().unwrap_or_default(); + this.is_composing.set(true); + this.with_input_handler(|handler| { + handler.replace_and_mark_text_in_range(None, &data, None); + }); + }) + } + + fn register_composition_end(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("compositionend", move |event: JsValue| { + let event: web_sys::CompositionEvent = event.unchecked_into(); + let data = event.data().unwrap_or_default(); + this.is_composing.set(false); + this.with_input_handler(|handler| { + handler.replace_text_in_range(None, &data); + handler.unmark_text(); + }); + this.input_element.set_value(""); + }) + } + + fn register_focus(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("focus", move |_event: JsValue| { + { + let mut state = this.state.borrow_mut(); + state.is_active = true; + } + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.active_status_change { + callback(true); + } + }) + } + + fn register_blur(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("blur", move |_event: JsValue| { + { + let mut state = this.state.borrow_mut(); + state.is_active = false; + } + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.active_status_change { + callback(false); + } + }) + } + + fn register_pointer_enter(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("pointerenter", move |_event: JsValue| { + { + let mut state = this.state.borrow_mut(); + state.is_hovered = true; + } + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.hover_status_change { + callback(true); + } + }) + } + + fn register_pointer_leave_hover(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen("pointerleave", move |_event: JsValue| { + { + let mut state = this.state.borrow_mut(); + state.is_hovered = false; + } + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.hover_status_change { + callback(false); + } + }) + } +} + +fn dom_key_to_gpui_key(event: &web_sys::KeyboardEvent) -> String { + let key = event.key(); + match key.as_str() { + "Enter" => "enter".to_string(), + "Backspace" => "backspace".to_string(), + "Tab" => "tab".to_string(), + "Escape" => "escape".to_string(), + "Delete" => "delete".to_string(), + " " => "space".to_string(), + "ArrowLeft" => "left".to_string(), + "ArrowRight" => "right".to_string(), + "ArrowUp" => "up".to_string(), + "ArrowDown" => "down".to_string(), + "Home" => "home".to_string(), + "End" => "end".to_string(), + "PageUp" => "pageup".to_string(), + "PageDown" => "pagedown".to_string(), + "Insert" => "insert".to_string(), + "Control" => "control".to_string(), + "Alt" => "alt".to_string(), + "Shift" => "shift".to_string(), + "Meta" => "platform".to_string(), + "CapsLock" => "capslock".to_string(), + other => { + if let Some(rest) = other.strip_prefix('F') { + if let Ok(number) = rest.parse::() { + if (1..=35).contains(&number) { + return format!("f{number}"); + } + } + } + other.to_lowercase() + } + } +} + +fn dom_mouse_button_to_gpui(button: i16) -> MouseButton { + match button { + 0 => MouseButton::Left, + 1 => MouseButton::Middle, + 2 => MouseButton::Right, + 3 => MouseButton::Navigate(NavigationDirection::Back), + 4 => MouseButton::Navigate(NavigationDirection::Forward), + _ => MouseButton::Left, + } +} + +fn modifiers_from_keyboard_event(event: &web_sys::KeyboardEvent, _is_mac: bool) -> Modifiers { + Modifiers { + control: event.ctrl_key(), + alt: event.alt_key(), + shift: event.shift_key(), + platform: event.meta_key(), + function: false, + } +} + +fn modifiers_from_mouse_event(event: &web_sys::PointerEvent, _is_mac: bool) -> Modifiers { + let mouse_event: &web_sys::MouseEvent = event.as_ref(); + Modifiers { + control: mouse_event.ctrl_key(), + alt: mouse_event.alt_key(), + shift: mouse_event.shift_key(), + platform: mouse_event.meta_key(), + function: false, + } +} + +fn modifiers_from_wheel_event(event: &web_sys::MouseEvent, _is_mac: bool) -> Modifiers { + Modifiers { + control: event.ctrl_key(), + alt: event.alt_key(), + shift: event.shift_key(), + platform: event.meta_key(), + function: false, + } +} + +fn capslock_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Capslock { + Capslock { + on: event.get_modifier_state("CapsLock"), + } +} + +pub(crate) fn is_mac_platform(browser_window: &web_sys::Window) -> bool { + let navigator = browser_window.navigator(); + + #[allow(deprecated)] + // navigator.platform() is deprecated but navigator.userAgentData is not widely available yet + if let Ok(platform) = navigator.platform() { + if platform.contains("Mac") { + return true; + } + } + + if let Ok(user_agent) = navigator.user_agent() { + return user_agent.contains("Mac"); + } + + false +} + +fn is_modifier_only_key(key: &str) -> bool { + matches!( + key, + "control" | "alt" | "shift" | "platform" | "capslock" | "compose" | "process" + ) +} + +fn compute_key_char( + event: &web_sys::KeyboardEvent, + gpui_key: &str, + modifiers: &Modifiers, +) -> Option { + if modifiers.platform || modifiers.control { + return None; + } + + if is_modifier_only_key(gpui_key) { + return None; + } + + if gpui_key == "space" { + return Some(" ".to_string()); + } + + let raw_key = event.key(); + + if raw_key.len() == 1 { + return Some(raw_key); + } + + None +} + +fn pointer_position_in_element(event: &web_sys::PointerEvent) -> Point { + let mouse_event: &web_sys::MouseEvent = event.as_ref(); + mouse_position_in_element(mouse_event) +} + +fn mouse_position_in_element(event: &web_sys::MouseEvent) -> Point { + // offset_x/offset_y give position relative to the target element's padding edge + point(px(event.offset_x() as f32), px(event.offset_y() as f32)) +} + +fn extract_file_paths_from_drag( + event: &web_sys::DragEvent, +) -> smallvec::SmallVec<[std::path::PathBuf; 2]> { + let mut paths = smallvec![]; + let Some(data_transfer) = event.data_transfer() else { + return paths; + }; + let file_list = data_transfer.files(); + let Some(files) = file_list else { + return paths; + }; + for index in 0..files.length() { + if let Some(file) = files.get(index) { + paths.push(std::path::PathBuf::from(file.name())); + } + } + paths +} diff --git a/crates/gpui_web/src/keyboard.rs b/crates/gpui_web/src/keyboard.rs index 3c1c97a01ee..0ab4f7aa4ac 100644 --- a/crates/gpui_web/src/keyboard.rs +++ b/crates/gpui_web/src/keyboard.rs @@ -1,19 +1,19 @@ -use gpui::PlatformKeyboardLayout; - -pub struct WebKeyboardLayout; - -impl WebKeyboardLayout { - pub fn new() -> Self { - WebKeyboardLayout - } -} - -impl PlatformKeyboardLayout for WebKeyboardLayout { - fn id(&self) -> &str { - "us" - } - - fn name(&self) -> &str { - "US" - } -} +use gpui::PlatformKeyboardLayout; + +pub struct WebKeyboardLayout; + +impl WebKeyboardLayout { + pub fn new() -> Self { + WebKeyboardLayout + } +} + +impl PlatformKeyboardLayout for WebKeyboardLayout { + fn id(&self) -> &str { + "us" + } + + fn name(&self) -> &str { + "US" + } +} diff --git a/crates/gpui_web/src/logging.rs b/crates/gpui_web/src/logging.rs index 9e76201b194..773118eeb25 100644 --- a/crates/gpui_web/src/logging.rs +++ b/crates/gpui_web/src/logging.rs @@ -1,37 +1,37 @@ -use log::{Level, Log, Metadata, Record}; - -struct ConsoleLogger; - -impl Log for ConsoleLogger { - fn enabled(&self, _metadata: &Metadata) -> bool { - true - } - - fn log(&self, record: &Record) { - if !self.enabled(record.metadata()) { - return; - } - - let message = format!( - "[{}] {}: {}", - record.level(), - record.target(), - record.args() - ); - let js_string = wasm_bindgen::JsValue::from_str(&message); - - match record.level() { - Level::Error => web_sys::console::error_1(&js_string), - Level::Warn => web_sys::console::warn_1(&js_string), - Level::Info => web_sys::console::info_1(&js_string), - Level::Debug | Level::Trace => web_sys::console::log_1(&js_string), - } - } - - fn flush(&self) {} -} - -pub fn init_logging() { - log::set_logger(&ConsoleLogger).ok(); - log::set_max_level(log::LevelFilter::Info); -} +use log::{Level, Log, Metadata, Record}; + +struct ConsoleLogger; + +impl Log for ConsoleLogger { + fn enabled(&self, _metadata: &Metadata) -> bool { + true + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + + let message = format!( + "[{}] {}: {}", + record.level(), + record.target(), + record.args() + ); + let js_string = wasm_bindgen::JsValue::from_str(&message); + + match record.level() { + Level::Error => web_sys::console::error_1(&js_string), + Level::Warn => web_sys::console::warn_1(&js_string), + Level::Info => web_sys::console::info_1(&js_string), + Level::Debug | Level::Trace => web_sys::console::log_1(&js_string), + } + } + + fn flush(&self) {} +} + +pub fn init_logging() { + log::set_logger(&ConsoleLogger).ok(); + log::set_max_level(log::LevelFilter::Info); +} diff --git a/crates/gpui_web/src/platform.rs b/crates/gpui_web/src/platform.rs index 290ef33e5f1..ace27bef481 100644 --- a/crates/gpui_web/src/platform.rs +++ b/crates/gpui_web/src/platform.rs @@ -1,435 +1,435 @@ -use crate::dispatcher::WebDispatcher; -use crate::display::WebDisplay; -use crate::keyboard::WebKeyboardLayout; -use crate::window::WebWindow; -use anyhow::Result; -use futures::channel::oneshot; -use gpui::{ - Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper, - ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, - PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task, - ThermalState, WindowAppearance, WindowParams, -}; -use gpui_wgpu::WgpuContext; -use std::{ - borrow::Cow, - cell::{Cell, RefCell}, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, -}; -use wasm_bindgen::prelude::*; - -static BUNDLED_FONTS: &[&[u8]] = &[ - include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"), - include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"), - include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"), - include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"), - include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"), - include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"), - include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"), - include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"), -]; - -pub struct WebPlatform { - browser_window: web_sys::Window, - background_executor: BackgroundExecutor, - foreground_executor: ForegroundExecutor, - text_system: Arc, - active_window: RefCell>, - active_display: Rc, - callbacks: RefCell, - wgpu_context: Rc>>, - cursor_visible: Rc>, - last_cursor_css: Rc>, - _cursor_restore_listeners: Vec, -} - -#[derive(Default)] -struct WebPlatformCallbacks { - open_urls: Option)>>, - quit: Option>, - reopen: Option>, - app_menu_action: Option>, - will_open_app_menu: Option>, - validate_app_menu_command: Option bool>>, - keyboard_layout_change: Option>, - thermal_state_change: Option>, -} - -impl WebPlatform { - pub fn new(allow_multi_threading: bool) -> Self { - let browser_window = - web_sys::window().expect("must be running in a browser window context"); - let dispatcher = Arc::new(WebDispatcher::new( - browser_window.clone(), - allow_multi_threading, - )); - let background_executor = BackgroundExecutor::new(dispatcher.clone()); - let foreground_executor = ForegroundExecutor::new(dispatcher); - let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts( - "IBM Plex Sans", - )); - let fonts = BUNDLED_FONTS - .iter() - .map(|bytes| Cow::Borrowed(*bytes)) - .collect(); - if let Err(error) = text_system.add_fonts(fonts) { - log::error!("failed to load bundled fonts: {error:#}"); - } - let text_system: Arc = text_system; - let active_display: Rc = - Rc::new(WebDisplay::new(browser_window.clone())); - - let cursor_visible = Rc::new(Cell::new(true)); - let last_cursor_css = Rc::new(Cell::new("default")); - let cursor_restore_listeners = cursor_restore_listeners( - &browser_window, - cursor_visible.clone(), - last_cursor_css.clone(), - ); - - Self { - browser_window, - background_executor, - foreground_executor, - text_system, - active_window: RefCell::new(None), - active_display, - callbacks: RefCell::new(WebPlatformCallbacks::default()), - wgpu_context: Rc::new(RefCell::new(None)), - cursor_visible, - last_cursor_css, - _cursor_restore_listeners: cursor_restore_listeners, - } - } -} - -impl Platform for WebPlatform { - fn background_executor(&self) -> BackgroundExecutor { - self.background_executor.clone() - } - - fn foreground_executor(&self) -> ForegroundExecutor { - self.foreground_executor.clone() - } - - fn text_system(&self) -> Arc { - self.text_system.clone() - } - - fn run(&self, on_finish_launching: Box) { - let wgpu_context = self.wgpu_context.clone(); - wasm_bindgen_futures::spawn_local(async move { - match WgpuContext::new_web().await { - Ok(context) => { - log::info!("WebGPU context initialized successfully"); - *wgpu_context.borrow_mut() = Some(context); - on_finish_launching(); - } - Err(err) => { - log::error!("Failed to initialize WebGPU context: {err:#}"); - on_finish_launching(); - } - } - }); - } - - fn quit(&self) { - log::warn!("WebPlatform::quit called, but quitting is not supported in the browser ."); - } - - fn restart(&self, _binary_path: Option) {} - - fn activate(&self, _ignoring_other_apps: bool) {} - - fn hide(&self) {} - - fn hide_other_apps(&self) {} - - fn unhide_other_apps(&self) {} - - fn displays(&self) -> Vec> { - vec![self.active_display.clone()] - } - - fn primary_display(&self) -> Option> { - Some(self.active_display.clone()) - } - - fn active_window(&self) -> Option { - *self.active_window.borrow() - } - - fn open_window( - &self, - handle: AnyWindowHandle, - params: WindowParams, - ) -> anyhow::Result> { - let context_ref = self.wgpu_context.borrow(); - let context = context_ref.as_ref().ok_or_else(|| { - anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?") - })?; - - let window = WebWindow::new(handle, params, context, self.browser_window.clone())?; - *self.active_window.borrow_mut() = Some(handle); - Ok(Box::new(window)) - } - - fn window_appearance(&self) -> WindowAppearance { - let Ok(Some(media_query)) = self - .browser_window - .match_media("(prefers-color-scheme: dark)") - else { - return WindowAppearance::Light; - }; - if media_query.matches() { - WindowAppearance::Dark - } else { - WindowAppearance::Light - } - } - - fn open_url(&self, url: &str) { - if let Err(error) = self.browser_window.open_with_url(url) { - log::warn!("Failed to open URL '{url}': {error:?}"); - } - } - - fn on_open_urls(&self, callback: Box)>) { - self.callbacks.borrow_mut().open_urls = Some(callback); - } - - fn register_url_scheme(&self, _url: &str) -> Task> { - Task::ready(Ok(())) - } - - fn prompt_for_paths( - &self, - _options: PathPromptOptions, - ) -> oneshot::Receiver>>> { - let (tx, rx) = oneshot::channel(); - tx.send(Err(anyhow::anyhow!( - "prompt_for_paths is not supported on the web" - ))) - .ok(); - rx - } - - fn prompt_for_new_path( - &self, - _directory: &Path, - _suggested_name: Option<&str>, - ) -> oneshot::Receiver>> { - let (sender, receiver) = oneshot::channel(); - sender - .send(Err(anyhow::anyhow!( - "prompt_for_new_path is not supported on the web" - ))) - .ok(); - receiver - } - - fn can_select_mixed_files_and_dirs(&self) -> bool { - false - } - - fn reveal_path(&self, _path: &Path) {} - - fn open_with_system(&self, _path: &Path) {} - - fn on_quit(&self, callback: Box) { - self.callbacks.borrow_mut().quit = Some(callback); - } - - fn on_reopen(&self, callback: Box) { - self.callbacks.borrow_mut().reopen = Some(callback); - } - - fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} - - fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} - - fn on_app_menu_action(&self, callback: Box) { - self.callbacks.borrow_mut().app_menu_action = Some(callback); - } - - fn on_will_open_app_menu(&self, callback: Box) { - self.callbacks.borrow_mut().will_open_app_menu = Some(callback); - } - - fn on_validate_app_menu_command(&self, callback: Box bool>) { - self.callbacks.borrow_mut().validate_app_menu_command = Some(callback); - } - - fn thermal_state(&self) -> ThermalState { - ThermalState::Nominal - } - - fn on_thermal_state_change(&self, callback: Box) { - self.callbacks.borrow_mut().thermal_state_change = Some(callback); - } - - fn compositor_name(&self) -> &'static str { - "Web" - } - - fn app_path(&self) -> Result { - Err(anyhow::anyhow!("app_path is not available on the web")) - } - - fn path_for_auxiliary_executable(&self, _name: &str) -> Result { - Err(anyhow::anyhow!( - "path_for_auxiliary_executable is not available on the web" - )) - } - - fn set_cursor_style(&self, style: CursorStyle) { - let css_cursor = match style { - CursorStyle::Arrow => "default", - CursorStyle::IBeam => "text", - CursorStyle::Crosshair => "crosshair", - CursorStyle::ClosedHand => "grabbing", - CursorStyle::OpenHand => "grab", - CursorStyle::PointingHand => "pointer", - CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => { - "ew-resize" - } - CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => { - "ns-resize" - } - CursorStyle::ResizeUpLeftDownRight => "nesw-resize", - CursorStyle::ResizeUpRightDownLeft => "nwse-resize", - CursorStyle::ResizeColumn => "col-resize", - CursorStyle::ResizeRow => "row-resize", - CursorStyle::IBeamCursorForVerticalLayout => "vertical-text", - CursorStyle::OperationNotAllowed => "not-allowed", - CursorStyle::DragLink => "alias", - CursorStyle::DragCopy => "copy", - CursorStyle::ContextualMenu => "context-menu", - }; - - self.last_cursor_css.set(css_cursor); - if self.cursor_visible.get() { - set_body_cursor(&self.browser_window, css_cursor); - } - } - - fn hide_cursor_until_mouse_moves(&self) { - if !self.cursor_visible.replace(false) { - return; - } - set_body_cursor(&self.browser_window, "none"); - } - - fn is_cursor_visible(&self) -> bool { - self.cursor_visible.get() - } - - fn should_auto_hide_scrollbars(&self) -> bool { - true - } - - fn read_from_clipboard(&self) -> Option { - None - } - - fn write_to_clipboard(&self, _item: ClipboardItem) {} - - fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task> { - Task::ready(Err(anyhow::anyhow!( - "credential storage is not available on the web" - ))) - } - - fn read_credentials(&self, _url: &str) -> Task)>>> { - Task::ready(Ok(None)) - } - - fn delete_credentials(&self, _url: &str) -> Task> { - Task::ready(Err(anyhow::anyhow!( - "credential storage is not available on the web" - ))) - } - - fn keyboard_layout(&self) -> Box { - Box::new(WebKeyboardLayout) - } - - fn keyboard_mapper(&self) -> Rc { - Rc::new(DummyKeyboardMapper) - } - - fn on_keyboard_layout_change(&self, callback: Box) { - self.callbacks.borrow_mut().keyboard_layout_change = Some(callback); - } -} - -struct EventListenerHandle { - target: web_sys::EventTarget, - event_name: &'static str, - closure: Closure, -} - -impl Drop for EventListenerHandle { - fn drop(&mut self) { - self.target - .remove_event_listener_with_callback( - self.event_name, - self.closure.as_ref().unchecked_ref(), - ) - .ok(); - } -} - -fn cursor_restore_listeners( - browser_window: &web_sys::Window, - cursor_visible: Rc>, - last_cursor_css: Rc>, -) -> Vec { - let mut handles = Vec::new(); - let Some(document) = browser_window.document() else { - return handles; - }; - - let make_restore_handler = |browser_window: web_sys::Window| { - let cursor_visible = cursor_visible.clone(); - let last_cursor_css = last_cursor_css.clone(); - Closure::::new(move |_event: JsValue| { - if !cursor_visible.replace(true) { - set_body_cursor(&browser_window, last_cursor_css.get()); - } - }) - }; - - let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| { - let closure = make_restore_handler(browser_window.clone()); - target - .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) - .ok(); - handles.push(EventListenerHandle { - target: target.clone(), - event_name, - closure, - }); - }; - - let document_target: &web_sys::EventTarget = document.as_ref(); - let window_target: &web_sys::EventTarget = browser_window.as_ref(); - - add_listener(document_target, "mousemove"); - add_listener(document_target, "mouseenter"); - add_listener(window_target, "blur"); - add_listener(document_target, "visibilitychange"); - - handles -} - -fn set_body_cursor(browser_window: &web_sys::Window, css_cursor: &str) { - if let Some(document) = browser_window.document() - && let Some(body) = document.body() - && let Err(error) = body.style().set_property("cursor", css_cursor) - { - log::warn!("Failed to set cursor style: {error:?}"); - } -} +use crate::dispatcher::WebDispatcher; +use crate::display::WebDisplay; +use crate::keyboard::WebKeyboardLayout; +use crate::window::WebWindow; +use anyhow::Result; +use futures::channel::oneshot; +use gpui::{ + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper, + ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, + PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task, + ThermalState, WindowAppearance, WindowParams, +}; +use gpui_wgpu::WgpuContext; +use std::{ + borrow::Cow, + cell::{Cell, RefCell}, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; +use wasm_bindgen::prelude::*; + +static BUNDLED_FONTS: &[&[u8]] = &[ + include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"), + include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"), + include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"), + include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"), + include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"), + include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"), + include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"), + include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"), +]; + +pub struct WebPlatform { + browser_window: web_sys::Window, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + text_system: Arc, + active_window: RefCell>, + active_display: Rc, + callbacks: RefCell, + wgpu_context: Rc>>, + cursor_visible: Rc>, + last_cursor_css: Rc>, + _cursor_restore_listeners: Vec, +} + +#[derive(Default)] +struct WebPlatformCallbacks { + open_urls: Option)>>, + quit: Option>, + reopen: Option>, + app_menu_action: Option>, + will_open_app_menu: Option>, + validate_app_menu_command: Option bool>>, + keyboard_layout_change: Option>, + thermal_state_change: Option>, +} + +impl WebPlatform { + pub fn new(allow_multi_threading: bool) -> Self { + let browser_window = + web_sys::window().expect("must be running in a browser window context"); + let dispatcher = Arc::new(WebDispatcher::new( + browser_window.clone(), + allow_multi_threading, + )); + let background_executor = BackgroundExecutor::new(dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(dispatcher); + let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts( + "IBM Plex Sans", + )); + let fonts = BUNDLED_FONTS + .iter() + .map(|bytes| Cow::Borrowed(*bytes)) + .collect(); + if let Err(error) = text_system.add_fonts(fonts) { + log::error!("failed to load bundled fonts: {error:#}"); + } + let text_system: Arc = text_system; + let active_display: Rc = + Rc::new(WebDisplay::new(browser_window.clone())); + + let cursor_visible = Rc::new(Cell::new(true)); + let last_cursor_css = Rc::new(Cell::new("default")); + let cursor_restore_listeners = cursor_restore_listeners( + &browser_window, + cursor_visible.clone(), + last_cursor_css.clone(), + ); + + Self { + browser_window, + background_executor, + foreground_executor, + text_system, + active_window: RefCell::new(None), + active_display, + callbacks: RefCell::new(WebPlatformCallbacks::default()), + wgpu_context: Rc::new(RefCell::new(None)), + cursor_visible, + last_cursor_css, + _cursor_restore_listeners: cursor_restore_listeners, + } + } +} + +impl Platform for WebPlatform { + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } + + fn text_system(&self) -> Arc { + self.text_system.clone() + } + + fn run(&self, on_finish_launching: Box) { + let wgpu_context = self.wgpu_context.clone(); + wasm_bindgen_futures::spawn_local(async move { + match WgpuContext::new_web().await { + Ok(context) => { + log::info!("WebGPU context initialized successfully"); + *wgpu_context.borrow_mut() = Some(context); + on_finish_launching(); + } + Err(err) => { + log::error!("Failed to initialize WebGPU context: {err:#}"); + on_finish_launching(); + } + } + }); + } + + fn quit(&self) { + log::warn!("WebPlatform::quit called, but quitting is not supported in the browser ."); + } + + fn restart(&self, _binary_path: Option) {} + + fn activate(&self, _ignoring_other_apps: bool) {} + + fn hide(&self) {} + + fn hide_other_apps(&self) {} + + fn unhide_other_apps(&self) {} + + fn displays(&self) -> Vec> { + vec![self.active_display.clone()] + } + + fn primary_display(&self) -> Option> { + Some(self.active_display.clone()) + } + + fn active_window(&self) -> Option { + *self.active_window.borrow() + } + + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> anyhow::Result> { + let context_ref = self.wgpu_context.borrow(); + let context = context_ref.as_ref().ok_or_else(|| { + anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?") + })?; + + let window = WebWindow::new(handle, params, context, self.browser_window.clone())?; + *self.active_window.borrow_mut() = Some(handle); + Ok(Box::new(window)) + } + + fn window_appearance(&self) -> WindowAppearance { + let Ok(Some(media_query)) = self + .browser_window + .match_media("(prefers-color-scheme: dark)") + else { + return WindowAppearance::Light; + }; + if media_query.matches() { + WindowAppearance::Dark + } else { + WindowAppearance::Light + } + } + + fn open_url(&self, url: &str) { + if let Err(error) = self.browser_window.open_with_url(url) { + log::warn!("Failed to open URL '{url}': {error:?}"); + } + } + + fn on_open_urls(&self, callback: Box)>) { + self.callbacks.borrow_mut().open_urls = Some(callback); + } + + fn register_url_scheme(&self, _url: &str) -> Task> { + Task::ready(Ok(())) + } + + fn prompt_for_paths( + &self, + _options: PathPromptOptions, + ) -> oneshot::Receiver>>> { + let (tx, rx) = oneshot::channel(); + tx.send(Err(anyhow::anyhow!( + "prompt_for_paths is not supported on the web" + ))) + .ok(); + rx + } + + fn prompt_for_new_path( + &self, + _directory: &Path, + _suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + let (sender, receiver) = oneshot::channel(); + sender + .send(Err(anyhow::anyhow!( + "prompt_for_new_path is not supported on the web" + ))) + .ok(); + receiver + } + + fn can_select_mixed_files_and_dirs(&self) -> bool { + false + } + + fn reveal_path(&self, _path: &Path) {} + + fn open_with_system(&self, _path: &Path) {} + + fn on_quit(&self, callback: Box) { + self.callbacks.borrow_mut().quit = Some(callback); + } + + fn on_reopen(&self, callback: Box) { + self.callbacks.borrow_mut().reopen = Some(callback); + } + + fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} + + fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} + + fn on_app_menu_action(&self, callback: Box) { + self.callbacks.borrow_mut().app_menu_action = Some(callback); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.callbacks.borrow_mut().will_open_app_menu = Some(callback); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.callbacks.borrow_mut().validate_app_menu_command = Some(callback); + } + + fn thermal_state(&self) -> ThermalState { + ThermalState::Nominal + } + + fn on_thermal_state_change(&self, callback: Box) { + self.callbacks.borrow_mut().thermal_state_change = Some(callback); + } + + fn compositor_name(&self) -> &'static str { + "Web" + } + + fn app_path(&self) -> Result { + Err(anyhow::anyhow!("app_path is not available on the web")) + } + + fn path_for_auxiliary_executable(&self, _name: &str) -> Result { + Err(anyhow::anyhow!( + "path_for_auxiliary_executable is not available on the web" + )) + } + + fn set_cursor_style(&self, style: CursorStyle) { + let css_cursor = match style { + CursorStyle::Arrow => "default", + CursorStyle::IBeam => "text", + CursorStyle::Crosshair => "crosshair", + CursorStyle::ClosedHand => "grabbing", + CursorStyle::OpenHand => "grab", + CursorStyle::PointingHand => "pointer", + CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => { + "ew-resize" + } + CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => { + "ns-resize" + } + CursorStyle::ResizeUpLeftDownRight => "nesw-resize", + CursorStyle::ResizeUpRightDownLeft => "nwse-resize", + CursorStyle::ResizeColumn => "col-resize", + CursorStyle::ResizeRow => "row-resize", + CursorStyle::IBeamCursorForVerticalLayout => "vertical-text", + CursorStyle::OperationNotAllowed => "not-allowed", + CursorStyle::DragLink => "alias", + CursorStyle::DragCopy => "copy", + CursorStyle::ContextualMenu => "context-menu", + }; + + self.last_cursor_css.set(css_cursor); + if self.cursor_visible.get() { + set_body_cursor(&self.browser_window, css_cursor); + } + } + + fn hide_cursor_until_mouse_moves(&self) { + if !self.cursor_visible.replace(false) { + return; + } + set_body_cursor(&self.browser_window, "none"); + } + + fn is_cursor_visible(&self) -> bool { + self.cursor_visible.get() + } + + fn should_auto_hide_scrollbars(&self) -> bool { + true + } + + fn read_from_clipboard(&self) -> Option { + None + } + + fn write_to_clipboard(&self, _item: ClipboardItem) {} + + fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task> { + Task::ready(Err(anyhow::anyhow!( + "credential storage is not available on the web" + ))) + } + + fn read_credentials(&self, _url: &str) -> Task)>>> { + Task::ready(Ok(None)) + } + + fn delete_credentials(&self, _url: &str) -> Task> { + Task::ready(Err(anyhow::anyhow!( + "credential storage is not available on the web" + ))) + } + + fn keyboard_layout(&self) -> Box { + Box::new(WebKeyboardLayout) + } + + fn keyboard_mapper(&self) -> Rc { + Rc::new(DummyKeyboardMapper) + } + + fn on_keyboard_layout_change(&self, callback: Box) { + self.callbacks.borrow_mut().keyboard_layout_change = Some(callback); + } +} + +struct EventListenerHandle { + target: web_sys::EventTarget, + event_name: &'static str, + closure: Closure, +} + +impl Drop for EventListenerHandle { + fn drop(&mut self) { + self.target + .remove_event_listener_with_callback( + self.event_name, + self.closure.as_ref().unchecked_ref(), + ) + .ok(); + } +} + +fn cursor_restore_listeners( + browser_window: &web_sys::Window, + cursor_visible: Rc>, + last_cursor_css: Rc>, +) -> Vec { + let mut handles = Vec::new(); + let Some(document) = browser_window.document() else { + return handles; + }; + + let make_restore_handler = |browser_window: web_sys::Window| { + let cursor_visible = cursor_visible.clone(); + let last_cursor_css = last_cursor_css.clone(); + Closure::::new(move |_event: JsValue| { + if !cursor_visible.replace(true) { + set_body_cursor(&browser_window, last_cursor_css.get()); + } + }) + }; + + let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| { + let closure = make_restore_handler(browser_window.clone()); + target + .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) + .ok(); + handles.push(EventListenerHandle { + target: target.clone(), + event_name, + closure, + }); + }; + + let document_target: &web_sys::EventTarget = document.as_ref(); + let window_target: &web_sys::EventTarget = browser_window.as_ref(); + + add_listener(document_target, "mousemove"); + add_listener(document_target, "mouseenter"); + add_listener(window_target, "blur"); + add_listener(document_target, "visibilitychange"); + + handles +} + +fn set_body_cursor(browser_window: &web_sys::Window, css_cursor: &str) { + if let Some(document) = browser_window.document() + && let Some(body) = document.body() + && let Err(error) = body.style().set_property("cursor", css_cursor) + { + log::warn!("Failed to set cursor style: {error:?}"); + } +} diff --git a/crates/gpui_web/src/window.rs b/crates/gpui_web/src/window.rs index 125432c0ae8..b9399d32e63 100644 --- a/crates/gpui_web/src/window.rs +++ b/crates/gpui_web/src/window.rs @@ -1,731 +1,731 @@ -use crate::display::WebDisplay; -use crate::events::{ClickState, WebEventListeners, is_mac_platform}; -use std::sync::Arc; -use std::{cell::Cell, cell::RefCell, rc::Rc}; - -use gpui::{ - AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, DispatchEventResult, GpuSpecs, - Modifiers, MouseButton, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, - PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, - ResizeEdge, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControlArea, WindowControls, WindowDecorations, WindowParams, px, -}; -use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig}; -use wasm_bindgen::prelude::*; - -#[derive(Default)] -pub(crate) struct WebWindowCallbacks { - pub(crate) request_frame: Option>, - pub(crate) input: Option DispatchEventResult>>, - pub(crate) active_status_change: Option>, - pub(crate) hover_status_change: Option>, - pub(crate) resize: Option, f32)>>, - pub(crate) moved: Option>, - pub(crate) should_close: Option bool>>, - pub(crate) close: Option>, - pub(crate) appearance_changed: Option>, - pub(crate) hit_test_window_control: Option Option>>, -} - -pub(crate) struct WebWindowMutableState { - pub(crate) renderer: WgpuRenderer, - pub(crate) bounds: Bounds, - pub(crate) scale_factor: f32, - pub(crate) max_texture_dimension: u32, - pub(crate) title: String, - pub(crate) input_handler: Option, - pub(crate) is_fullscreen: bool, - pub(crate) is_active: bool, - pub(crate) is_hovered: bool, - pub(crate) mouse_position: Point, - pub(crate) modifiers: Modifiers, - pub(crate) capslock: Capslock, -} - -pub(crate) struct WebWindowInner { - pub(crate) browser_window: web_sys::Window, - pub(crate) canvas: web_sys::HtmlCanvasElement, - pub(crate) input_element: web_sys::HtmlInputElement, - pub(crate) has_device_pixel_support: bool, - pub(crate) is_mac: bool, - pub(crate) state: RefCell, - pub(crate) callbacks: RefCell, - pub(crate) click_state: RefCell, - pub(crate) pressed_button: Cell>, - pub(crate) last_physical_size: Cell<(u32, u32)>, - pub(crate) notify_scale: Cell, - pub(crate) is_composing: Cell, - mql_handle: RefCell>, - pending_physical_size: Cell>, -} - -pub struct WebWindow { - inner: Rc, - display: Rc, - #[allow(dead_code)] - handle: AnyWindowHandle, - _raf_closure: Closure, - _resize_observer: Option, - _resize_observer_closure: Closure, - _event_listeners: WebEventListeners, -} - -impl WebWindow { - pub fn new( - handle: AnyWindowHandle, - _params: WindowParams, - context: &WgpuContext, - browser_window: web_sys::Window, - ) -> anyhow::Result { - let document = browser_window - .document() - .ok_or_else(|| anyhow::anyhow!("No `document` found on window"))?; - - let canvas: web_sys::HtmlCanvasElement = document - .create_element("canvas") - .map_err(|e| anyhow::anyhow!("Failed to create canvas element: {e:?}"))? - .dyn_into() - .map_err(|e| anyhow::anyhow!("Created element is not a canvas: {e:?}"))?; - - let dpr = browser_window.device_pixel_ratio() as f32; - let max_texture_dimension = context.device.limits().max_texture_dimension_2d; - let has_device_pixel_support = check_device_pixel_support(); - - canvas.set_tab_index(-1); - - let style = canvas.style(); - style - .set_property("width", "100%") - .map_err(|e| anyhow::anyhow!("Failed to set canvas width style: {e:?}"))?; - style - .set_property("height", "100%") - .map_err(|e| anyhow::anyhow!("Failed to set canvas height style: {e:?}"))?; - style - .set_property("display", "block") - .map_err(|e| anyhow::anyhow!("Failed to set canvas display style: {e:?}"))?; - style - .set_property("outline", "none") - .map_err(|e| anyhow::anyhow!("Failed to set canvas outline style: {e:?}"))?; - style - .set_property("touch-action", "none") - .map_err(|e| anyhow::anyhow!("Failed to set touch-action style: {e:?}"))?; - - let body = document - .body() - .ok_or_else(|| anyhow::anyhow!("No `body` found on document"))?; - body.append_child(&canvas) - .map_err(|e| anyhow::anyhow!("Failed to append canvas to body: {e:?}"))?; - - let input_element: web_sys::HtmlInputElement = document - .create_element("input") - .map_err(|e| anyhow::anyhow!("Failed to create input element: {e:?}"))? - .dyn_into() - .map_err(|e| anyhow::anyhow!("Created element is not an input: {e:?}"))?; - let input_style = input_element.style(); - input_style.set_property("position", "fixed").ok(); - input_style.set_property("top", "0").ok(); - input_style.set_property("left", "0").ok(); - input_style.set_property("width", "1px").ok(); - input_style.set_property("height", "1px").ok(); - input_style.set_property("opacity", "0").ok(); - body.append_child(&input_element) - .map_err(|e| anyhow::anyhow!("Failed to append input to body: {e:?}"))?; - input_element.focus().ok(); - - let device_size = Size { - width: DevicePixels(0), - height: DevicePixels(0), - }; - - let renderer_config = WgpuSurfaceConfig { - size: device_size, - transparent: false, - preferred_present_mode: None, - }; - - let renderer = WgpuRenderer::new_from_canvas(context, &canvas, renderer_config)?; - - let display: Rc = Rc::new(WebDisplay::new(browser_window.clone())); - - let initial_bounds = Bounds { - origin: Point::default(), - size: Size::default(), - }; - - let mutable_state = WebWindowMutableState { - renderer, - bounds: initial_bounds, - scale_factor: dpr, - max_texture_dimension, - title: String::new(), - input_handler: None, - is_fullscreen: false, - is_active: true, - is_hovered: false, - mouse_position: Point::default(), - modifiers: Modifiers::default(), - capslock: Capslock::default(), - }; - - let is_mac = is_mac_platform(&browser_window); - - let inner = Rc::new(WebWindowInner { - browser_window, - canvas, - input_element, - has_device_pixel_support, - is_mac, - state: RefCell::new(mutable_state), - callbacks: RefCell::new(WebWindowCallbacks::default()), - click_state: RefCell::new(ClickState::default()), - pressed_button: Cell::new(None), - last_physical_size: Cell::new((0, 0)), - notify_scale: Cell::new(false), - is_composing: Cell::new(false), - mql_handle: RefCell::new(None), - pending_physical_size: Cell::new(None), - }); - - let raf_closure = inner.create_raf_closure(); - inner.schedule_raf(&raf_closure); - - let resize_observer_closure = Self::create_resize_observer_closure(Rc::clone(&inner)); - let resize_observer = - web_sys::ResizeObserver::new(resize_observer_closure.as_ref().unchecked_ref()).ok(); - - if let Some(ref observer) = resize_observer { - inner.observe_canvas(observer); - inner.watch_dpr_changes(observer); - } - - let event_listeners = inner.register_event_listeners(); - - Ok(Self { - inner, - display, - handle, - _raf_closure: raf_closure, - _resize_observer: resize_observer, - _resize_observer_closure: resize_observer_closure, - _event_listeners: event_listeners, - }) - } - - fn create_resize_observer_closure( - inner: Rc, - ) -> Closure { - Closure::new(move |entries: js_sys::Array| { - let entry: web_sys::ResizeObserverEntry = match entries.get(0).dyn_into().ok() { - Some(entry) => entry, - None => return, - }; - - let dpr = inner.browser_window.device_pixel_ratio(); - let dpr_f32 = dpr as f32; - - let (physical_width, physical_height, logical_width, logical_height) = - if inner.has_device_pixel_support { - let size: web_sys::ResizeObserverSize = entry - .device_pixel_content_box_size() - .get(0) - .unchecked_into(); - let pw = size.inline_size() as u32; - let ph = size.block_size() as u32; - let lw = pw as f64 / dpr; - let lh = ph as f64 / dpr; - (pw, ph, lw as f32, lh as f32) - } else { - // Safari fallback: use contentRect (always CSS px). - let rect = entry.content_rect(); - let lw = rect.width() as f32; - let lh = rect.height() as f32; - let pw = (lw as f64 * dpr).round() as u32; - let ph = (lh as f64 * dpr).round() as u32; - (pw, ph, lw, lh) - }; - - let scale_changed = inner.notify_scale.replace(false); - let prev = inner.last_physical_size.get(); - let size_changed = prev != (physical_width, physical_height); - - if !scale_changed && !size_changed { - return; - } - inner - .last_physical_size - .set((physical_width, physical_height)); - - // Skip rendering to a zero-size canvas (e.g. display:none). - if physical_width == 0 || physical_height == 0 { - let mut s = inner.state.borrow_mut(); - s.bounds.size = Size::default(); - s.scale_factor = dpr_f32; - // Still fire the callback so GPUI knows the window is gone. - drop(s); - let mut cbs = inner.callbacks.borrow_mut(); - if let Some(ref mut callback) = cbs.resize { - callback(Size::default(), dpr_f32); - } - return; - } - - let max_texture_dimension = inner.state.borrow().max_texture_dimension; - let clamped_width = physical_width.min(max_texture_dimension); - let clamped_height = physical_height.min(max_texture_dimension); - - inner - .pending_physical_size - .set(Some((clamped_width, clamped_height))); - - { - let mut s = inner.state.borrow_mut(); - s.bounds.size = Size { - width: px(logical_width), - height: px(logical_height), - }; - s.scale_factor = dpr_f32; - } - - let new_size = Size { - width: px(logical_width), - height: px(logical_height), - }; - - let mut cbs = inner.callbacks.borrow_mut(); - if let Some(ref mut callback) = cbs.resize { - callback(new_size, dpr_f32); - } - }) - } -} - -impl WebWindowInner { - fn create_raf_closure(self: &Rc) -> Closure { - let raf_handle: Rc>> = Rc::new(RefCell::new(None)); - let raf_handle_inner = Rc::clone(&raf_handle); - - let this = Rc::clone(self); - let closure = Closure::new(move || { - { - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.request_frame { - callback(RequestFrameOptions { - require_presentation: true, - force_render: false, - }); - } - } - - // Re-schedule for the next frame - if let Some(ref func) = *raf_handle_inner.borrow() { - this.browser_window.request_animation_frame(func).ok(); - } - }); - - let js_func: js_sys::Function = - closure.as_ref().unchecked_ref::().clone(); - *raf_handle.borrow_mut() = Some(js_func); - - closure - } - - fn schedule_raf(&self, closure: &Closure) { - self.browser_window - .request_animation_frame(closure.as_ref().unchecked_ref()) - .ok(); - } - - fn observe_canvas(&self, observer: &web_sys::ResizeObserver) { - observer.unobserve(&self.canvas); - if self.has_device_pixel_support { - let options = web_sys::ResizeObserverOptions::new(); - options.set_box(web_sys::ResizeObserverBoxOptions::DevicePixelContentBox); - observer.observe_with_options(&self.canvas, &options); - } else { - observer.observe(&self.canvas); - } - } - - fn watch_dpr_changes(self: &Rc, observer: &web_sys::ResizeObserver) { - let current_dpr = self.browser_window.device_pixel_ratio(); - let media_query = - format!("(resolution: {current_dpr}dppx), (-webkit-device-pixel-ratio: {current_dpr})"); - let Some(mql) = self.browser_window.match_media(&media_query).ok().flatten() else { - return; - }; - - let this = Rc::clone(self); - let observer = observer.clone(); - - let closure = Closure::::new(move |_event: JsValue| { - this.notify_scale.set(true); - this.observe_canvas(&observer); - this.watch_dpr_changes(&observer); - }); - - mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref()) - .ok(); - - *self.mql_handle.borrow_mut() = Some(MqlHandle { - mql, - _closure: closure, - }); - } - - pub(crate) fn register_visibility_change( - self: &Rc, - ) -> Option> { - let document = self.browser_window.document()?; - let this = Rc::clone(self); - - let closure = Closure::::new(move |_event: JsValue| { - let is_visible = this - .browser_window - .document() - .map(|doc| { - let state_str: String = js_sys::Reflect::get(&doc, &"visibilityState".into()) - .ok() - .and_then(|v| v.as_string()) - .unwrap_or_default(); - state_str == "visible" - }) - .unwrap_or(true); - - { - let mut state = this.state.borrow_mut(); - state.is_active = is_visible; - } - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.active_status_change { - callback(is_visible); - } - }); - - document - .add_event_listener_with_callback("visibilitychange", closure.as_ref().unchecked_ref()) - .ok(); - - Some(closure) - } - - pub(crate) fn with_input_handler( - &self, - f: impl FnOnce(&mut PlatformInputHandler) -> R, - ) -> Option { - let mut handler = self.state.borrow_mut().input_handler.take()?; - let result = f(&mut handler); - self.state.borrow_mut().input_handler = Some(handler); - Some(result) - } - - pub(crate) fn register_appearance_change( - self: &Rc, - ) -> Option> { - let mql = self - .browser_window - .match_media("(prefers-color-scheme: dark)") - .ok()??; - - let this = Rc::clone(self); - let closure = Closure::::new(move |_event: JsValue| { - let mut callbacks = this.callbacks.borrow_mut(); - if let Some(ref mut callback) = callbacks.appearance_changed { - callback(); - } - }); - - mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref()) - .ok(); - - Some(closure) - } -} - -fn current_appearance(browser_window: &web_sys::Window) -> WindowAppearance { - let is_dark = browser_window - .match_media("(prefers-color-scheme: dark)") - .ok() - .flatten() - .map(|mql| mql.matches()) - .unwrap_or(false); - - if is_dark { - WindowAppearance::Dark - } else { - WindowAppearance::Light - } -} - -struct MqlHandle { - mql: web_sys::MediaQueryList, - _closure: Closure, -} - -impl Drop for MqlHandle { - fn drop(&mut self) { - self.mql - .remove_event_listener_with_callback("change", self._closure.as_ref().unchecked_ref()) - .ok(); - } -} - -// Safari does not support `devicePixelContentBoxSize`, so detect whether it's available. -fn check_device_pixel_support() -> bool { - let global: JsValue = js_sys::global().into(); - let Ok(constructor) = js_sys::Reflect::get(&global, &"ResizeObserverEntry".into()) else { - return false; - }; - let Ok(prototype) = js_sys::Reflect::get(&constructor, &"prototype".into()) else { - return false; - }; - let descriptor = js_sys::Object::get_own_property_descriptor( - &prototype.unchecked_into::(), - &"devicePixelContentBoxSize".into(), - ); - !descriptor.is_undefined() -} - -impl raw_window_handle::HasWindowHandle for WebWindow { - fn window_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - let canvas_ref: &JsValue = self.inner.canvas.as_ref(); - let obj = std::ptr::NonNull::from(canvas_ref).cast::(); - let handle = raw_window_handle::WebCanvasWindowHandle::new(obj); - Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.into()) }) - } -} - -impl raw_window_handle::HasDisplayHandle for WebWindow { - fn display_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - Ok(raw_window_handle::DisplayHandle::web()) - } -} - -impl PlatformWindow for WebWindow { - fn bounds(&self) -> Bounds { - self.inner.state.borrow().bounds - } - - fn is_maximized(&self) -> bool { - false - } - - fn window_bounds(&self) -> WindowBounds { - WindowBounds::Windowed(self.bounds()) - } - - fn content_size(&self) -> Size { - self.inner.state.borrow().bounds.size - } - - fn resize(&mut self, size: Size) { - let style = self.inner.canvas.style(); - style - .set_property("width", &format!("{}px", f32::from(size.width))) - .ok(); - style - .set_property("height", &format!("{}px", f32::from(size.height))) - .ok(); - } - - fn scale_factor(&self) -> f32 { - self.inner.state.borrow().scale_factor - } - - fn appearance(&self) -> WindowAppearance { - current_appearance(&self.inner.browser_window) - } - - fn display(&self) -> Option> { - Some(self.display.clone()) - } - - fn mouse_position(&self) -> Point { - self.inner.state.borrow().mouse_position - } - - fn modifiers(&self) -> Modifiers { - self.inner.state.borrow().modifiers - } - - fn capslock(&self) -> Capslock { - self.inner.state.borrow().capslock - } - - fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.inner.state.borrow_mut().input_handler = Some(input_handler); - } - - fn take_input_handler(&mut self) -> Option { - self.inner.state.borrow_mut().input_handler.take() - } - - fn prompt( - &self, - _level: PromptLevel, - _msg: &str, - _detail: Option<&str>, - _answers: &[PromptButton], - ) -> Option> { - None - } - - fn activate(&self) { - self.inner.state.borrow_mut().is_active = true; - } - - fn is_active(&self) -> bool { - self.inner.state.borrow().is_active - } - - fn is_hovered(&self) -> bool { - self.inner.state.borrow().is_hovered - } - - fn background_appearance(&self) -> WindowBackgroundAppearance { - WindowBackgroundAppearance::Opaque - } - - fn set_title(&mut self, title: &str) { - self.inner.state.borrow_mut().title = title.to_owned(); - if let Some(document) = self.inner.browser_window.document() { - document.set_title(title); - } - } - - fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} - - fn minimize(&self) { - log::warn!("WebWindow::minimize is not supported in the browser"); - } - - fn zoom(&self) { - log::warn!("WebWindow::zoom is not supported in the browser"); - } - - fn toggle_fullscreen(&self) { - let mut state = self.inner.state.borrow_mut(); - state.is_fullscreen = !state.is_fullscreen; - - if state.is_fullscreen { - let canvas: &web_sys::Element = self.inner.canvas.as_ref(); - canvas.request_fullscreen().ok(); - } else { - if let Some(document) = self.inner.browser_window.document() { - document.exit_fullscreen(); - } - } - } - - fn is_fullscreen(&self) -> bool { - self.inner.state.borrow().is_fullscreen - } - - fn on_request_frame(&self, callback: Box) { - self.inner.callbacks.borrow_mut().request_frame = Some(callback); - } - - fn on_input(&self, callback: Box DispatchEventResult>) { - self.inner.callbacks.borrow_mut().input = Some(callback); - } - - fn on_active_status_change(&self, callback: Box) { - self.inner.callbacks.borrow_mut().active_status_change = Some(callback); - } - - fn on_hover_status_change(&self, callback: Box) { - self.inner.callbacks.borrow_mut().hover_status_change = Some(callback); - } - - fn on_resize(&self, callback: Box, f32)>) { - self.inner.callbacks.borrow_mut().resize = Some(callback); - } - - fn on_moved(&self, callback: Box) { - self.inner.callbacks.borrow_mut().moved = Some(callback); - } - - fn on_should_close(&self, callback: Box bool>) { - self.inner.callbacks.borrow_mut().should_close = Some(callback); - } - - fn on_close(&self, callback: Box) { - self.inner.callbacks.borrow_mut().close = Some(callback); - } - - fn on_hit_test_window_control(&self, callback: Box Option>) { - self.inner.callbacks.borrow_mut().hit_test_window_control = Some(callback); - } - - fn on_appearance_changed(&self, callback: Box) { - self.inner.callbacks.borrow_mut().appearance_changed = Some(callback); - } - - fn draw(&self, scene: &Scene) { - if let Some((width, height)) = self.inner.pending_physical_size.take() { - if self.inner.canvas.width() != width || self.inner.canvas.height() != height { - self.inner.canvas.set_width(width); - self.inner.canvas.set_height(height); - } - - let mut state = self.inner.state.borrow_mut(); - state.renderer.update_drawable_size(Size { - width: DevicePixels(width as i32), - height: DevicePixels(height as i32), - }); - drop(state); - } - - self.inner.state.borrow_mut().renderer.draw(scene); - } - - fn completed_frame(&self) { - // On web, presentation happens automatically via wgpu surface present - } - - fn sprite_atlas(&self) -> Arc { - self.inner.state.borrow().renderer.sprite_atlas().clone() - } - - fn is_subpixel_rendering_supported(&self) -> bool { - self.inner - .state - .borrow() - .renderer - .supports_dual_source_blending() - } - - fn gpu_specs(&self) -> Option { - Some(self.inner.state.borrow().renderer.gpu_specs()) - } - - fn update_ime_position(&self, _bounds: Bounds) {} - - fn request_decorations(&self, _decorations: WindowDecorations) {} - - fn show_window_menu(&self, _position: Point) {} - - fn start_window_move(&self) {} - - fn start_window_resize(&self, _edge: ResizeEdge) {} - - fn window_decorations(&self) -> Decorations { - Decorations::Server - } - - fn set_app_id(&mut self, _app_id: &str) {} - - fn window_controls(&self) -> WindowControls { - WindowControls { - fullscreen: true, - maximize: false, - minimize: false, - window_menu: false, - } - } - - fn set_client_inset(&self, _inset: Pixels) {} -} +use crate::display::WebDisplay; +use crate::events::{ClickState, WebEventListeners, is_mac_platform}; +use std::sync::Arc; +use std::{cell::Cell, cell::RefCell, rc::Rc}; + +use gpui::{ + AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, DispatchEventResult, GpuSpecs, + Modifiers, MouseButton, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, + PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, + ResizeEdge, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, + WindowControlArea, WindowControls, WindowDecorations, WindowParams, px, +}; +use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig}; +use wasm_bindgen::prelude::*; + +#[derive(Default)] +pub(crate) struct WebWindowCallbacks { + pub(crate) request_frame: Option>, + pub(crate) input: Option DispatchEventResult>>, + pub(crate) active_status_change: Option>, + pub(crate) hover_status_change: Option>, + pub(crate) resize: Option, f32)>>, + pub(crate) moved: Option>, + pub(crate) should_close: Option bool>>, + pub(crate) close: Option>, + pub(crate) appearance_changed: Option>, + pub(crate) hit_test_window_control: Option Option>>, +} + +pub(crate) struct WebWindowMutableState { + pub(crate) renderer: WgpuRenderer, + pub(crate) bounds: Bounds, + pub(crate) scale_factor: f32, + pub(crate) max_texture_dimension: u32, + pub(crate) title: String, + pub(crate) input_handler: Option, + pub(crate) is_fullscreen: bool, + pub(crate) is_active: bool, + pub(crate) is_hovered: bool, + pub(crate) mouse_position: Point, + pub(crate) modifiers: Modifiers, + pub(crate) capslock: Capslock, +} + +pub(crate) struct WebWindowInner { + pub(crate) browser_window: web_sys::Window, + pub(crate) canvas: web_sys::HtmlCanvasElement, + pub(crate) input_element: web_sys::HtmlInputElement, + pub(crate) has_device_pixel_support: bool, + pub(crate) is_mac: bool, + pub(crate) state: RefCell, + pub(crate) callbacks: RefCell, + pub(crate) click_state: RefCell, + pub(crate) pressed_button: Cell>, + pub(crate) last_physical_size: Cell<(u32, u32)>, + pub(crate) notify_scale: Cell, + pub(crate) is_composing: Cell, + mql_handle: RefCell>, + pending_physical_size: Cell>, +} + +pub struct WebWindow { + inner: Rc, + display: Rc, + #[allow(dead_code)] + handle: AnyWindowHandle, + _raf_closure: Closure, + _resize_observer: Option, + _resize_observer_closure: Closure, + _event_listeners: WebEventListeners, +} + +impl WebWindow { + pub fn new( + handle: AnyWindowHandle, + _params: WindowParams, + context: &WgpuContext, + browser_window: web_sys::Window, + ) -> anyhow::Result { + let document = browser_window + .document() + .ok_or_else(|| anyhow::anyhow!("No `document` found on window"))?; + + let canvas: web_sys::HtmlCanvasElement = document + .create_element("canvas") + .map_err(|e| anyhow::anyhow!("Failed to create canvas element: {e:?}"))? + .dyn_into() + .map_err(|e| anyhow::anyhow!("Created element is not a canvas: {e:?}"))?; + + let dpr = browser_window.device_pixel_ratio() as f32; + let max_texture_dimension = context.device.limits().max_texture_dimension_2d; + let has_device_pixel_support = check_device_pixel_support(); + + canvas.set_tab_index(-1); + + let style = canvas.style(); + style + .set_property("width", "100%") + .map_err(|e| anyhow::anyhow!("Failed to set canvas width style: {e:?}"))?; + style + .set_property("height", "100%") + .map_err(|e| anyhow::anyhow!("Failed to set canvas height style: {e:?}"))?; + style + .set_property("display", "block") + .map_err(|e| anyhow::anyhow!("Failed to set canvas display style: {e:?}"))?; + style + .set_property("outline", "none") + .map_err(|e| anyhow::anyhow!("Failed to set canvas outline style: {e:?}"))?; + style + .set_property("touch-action", "none") + .map_err(|e| anyhow::anyhow!("Failed to set touch-action style: {e:?}"))?; + + let body = document + .body() + .ok_or_else(|| anyhow::anyhow!("No `body` found on document"))?; + body.append_child(&canvas) + .map_err(|e| anyhow::anyhow!("Failed to append canvas to body: {e:?}"))?; + + let input_element: web_sys::HtmlInputElement = document + .create_element("input") + .map_err(|e| anyhow::anyhow!("Failed to create input element: {e:?}"))? + .dyn_into() + .map_err(|e| anyhow::anyhow!("Created element is not an input: {e:?}"))?; + let input_style = input_element.style(); + input_style.set_property("position", "fixed").ok(); + input_style.set_property("top", "0").ok(); + input_style.set_property("left", "0").ok(); + input_style.set_property("width", "1px").ok(); + input_style.set_property("height", "1px").ok(); + input_style.set_property("opacity", "0").ok(); + body.append_child(&input_element) + .map_err(|e| anyhow::anyhow!("Failed to append input to body: {e:?}"))?; + input_element.focus().ok(); + + let device_size = Size { + width: DevicePixels(0), + height: DevicePixels(0), + }; + + let renderer_config = WgpuSurfaceConfig { + size: device_size, + transparent: false, + preferred_present_mode: None, + }; + + let renderer = WgpuRenderer::new_from_canvas(context, &canvas, renderer_config)?; + + let display: Rc = Rc::new(WebDisplay::new(browser_window.clone())); + + let initial_bounds = Bounds { + origin: Point::default(), + size: Size::default(), + }; + + let mutable_state = WebWindowMutableState { + renderer, + bounds: initial_bounds, + scale_factor: dpr, + max_texture_dimension, + title: String::new(), + input_handler: None, + is_fullscreen: false, + is_active: true, + is_hovered: false, + mouse_position: Point::default(), + modifiers: Modifiers::default(), + capslock: Capslock::default(), + }; + + let is_mac = is_mac_platform(&browser_window); + + let inner = Rc::new(WebWindowInner { + browser_window, + canvas, + input_element, + has_device_pixel_support, + is_mac, + state: RefCell::new(mutable_state), + callbacks: RefCell::new(WebWindowCallbacks::default()), + click_state: RefCell::new(ClickState::default()), + pressed_button: Cell::new(None), + last_physical_size: Cell::new((0, 0)), + notify_scale: Cell::new(false), + is_composing: Cell::new(false), + mql_handle: RefCell::new(None), + pending_physical_size: Cell::new(None), + }); + + let raf_closure = inner.create_raf_closure(); + inner.schedule_raf(&raf_closure); + + let resize_observer_closure = Self::create_resize_observer_closure(Rc::clone(&inner)); + let resize_observer = + web_sys::ResizeObserver::new(resize_observer_closure.as_ref().unchecked_ref()).ok(); + + if let Some(ref observer) = resize_observer { + inner.observe_canvas(observer); + inner.watch_dpr_changes(observer); + } + + let event_listeners = inner.register_event_listeners(); + + Ok(Self { + inner, + display, + handle, + _raf_closure: raf_closure, + _resize_observer: resize_observer, + _resize_observer_closure: resize_observer_closure, + _event_listeners: event_listeners, + }) + } + + fn create_resize_observer_closure( + inner: Rc, + ) -> Closure { + Closure::new(move |entries: js_sys::Array| { + let entry: web_sys::ResizeObserverEntry = match entries.get(0).dyn_into().ok() { + Some(entry) => entry, + None => return, + }; + + let dpr = inner.browser_window.device_pixel_ratio(); + let dpr_f32 = dpr as f32; + + let (physical_width, physical_height, logical_width, logical_height) = + if inner.has_device_pixel_support { + let size: web_sys::ResizeObserverSize = entry + .device_pixel_content_box_size() + .get(0) + .unchecked_into(); + let pw = size.inline_size() as u32; + let ph = size.block_size() as u32; + let lw = pw as f64 / dpr; + let lh = ph as f64 / dpr; + (pw, ph, lw as f32, lh as f32) + } else { + // Safari fallback: use contentRect (always CSS px). + let rect = entry.content_rect(); + let lw = rect.width() as f32; + let lh = rect.height() as f32; + let pw = (lw as f64 * dpr).round() as u32; + let ph = (lh as f64 * dpr).round() as u32; + (pw, ph, lw, lh) + }; + + let scale_changed = inner.notify_scale.replace(false); + let prev = inner.last_physical_size.get(); + let size_changed = prev != (physical_width, physical_height); + + if !scale_changed && !size_changed { + return; + } + inner + .last_physical_size + .set((physical_width, physical_height)); + + // Skip rendering to a zero-size canvas (e.g. display:none). + if physical_width == 0 || physical_height == 0 { + let mut s = inner.state.borrow_mut(); + s.bounds.size = Size::default(); + s.scale_factor = dpr_f32; + // Still fire the callback so GPUI knows the window is gone. + drop(s); + let mut cbs = inner.callbacks.borrow_mut(); + if let Some(ref mut callback) = cbs.resize { + callback(Size::default(), dpr_f32); + } + return; + } + + let max_texture_dimension = inner.state.borrow().max_texture_dimension; + let clamped_width = physical_width.min(max_texture_dimension); + let clamped_height = physical_height.min(max_texture_dimension); + + inner + .pending_physical_size + .set(Some((clamped_width, clamped_height))); + + { + let mut s = inner.state.borrow_mut(); + s.bounds.size = Size { + width: px(logical_width), + height: px(logical_height), + }; + s.scale_factor = dpr_f32; + } + + let new_size = Size { + width: px(logical_width), + height: px(logical_height), + }; + + let mut cbs = inner.callbacks.borrow_mut(); + if let Some(ref mut callback) = cbs.resize { + callback(new_size, dpr_f32); + } + }) + } +} + +impl WebWindowInner { + fn create_raf_closure(self: &Rc) -> Closure { + let raf_handle: Rc>> = Rc::new(RefCell::new(None)); + let raf_handle_inner = Rc::clone(&raf_handle); + + let this = Rc::clone(self); + let closure = Closure::new(move || { + { + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.request_frame { + callback(RequestFrameOptions { + require_presentation: true, + force_render: false, + }); + } + } + + // Re-schedule for the next frame + if let Some(ref func) = *raf_handle_inner.borrow() { + this.browser_window.request_animation_frame(func).ok(); + } + }); + + let js_func: js_sys::Function = + closure.as_ref().unchecked_ref::().clone(); + *raf_handle.borrow_mut() = Some(js_func); + + closure + } + + fn schedule_raf(&self, closure: &Closure) { + self.browser_window + .request_animation_frame(closure.as_ref().unchecked_ref()) + .ok(); + } + + fn observe_canvas(&self, observer: &web_sys::ResizeObserver) { + observer.unobserve(&self.canvas); + if self.has_device_pixel_support { + let options = web_sys::ResizeObserverOptions::new(); + options.set_box(web_sys::ResizeObserverBoxOptions::DevicePixelContentBox); + observer.observe_with_options(&self.canvas, &options); + } else { + observer.observe(&self.canvas); + } + } + + fn watch_dpr_changes(self: &Rc, observer: &web_sys::ResizeObserver) { + let current_dpr = self.browser_window.device_pixel_ratio(); + let media_query = + format!("(resolution: {current_dpr}dppx), (-webkit-device-pixel-ratio: {current_dpr})"); + let Some(mql) = self.browser_window.match_media(&media_query).ok().flatten() else { + return; + }; + + let this = Rc::clone(self); + let observer = observer.clone(); + + let closure = Closure::::new(move |_event: JsValue| { + this.notify_scale.set(true); + this.observe_canvas(&observer); + this.watch_dpr_changes(&observer); + }); + + mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref()) + .ok(); + + *self.mql_handle.borrow_mut() = Some(MqlHandle { + mql, + _closure: closure, + }); + } + + pub(crate) fn register_visibility_change( + self: &Rc, + ) -> Option> { + let document = self.browser_window.document()?; + let this = Rc::clone(self); + + let closure = Closure::::new(move |_event: JsValue| { + let is_visible = this + .browser_window + .document() + .map(|doc| { + let state_str: String = js_sys::Reflect::get(&doc, &"visibilityState".into()) + .ok() + .and_then(|v| v.as_string()) + .unwrap_or_default(); + state_str == "visible" + }) + .unwrap_or(true); + + { + let mut state = this.state.borrow_mut(); + state.is_active = is_visible; + } + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.active_status_change { + callback(is_visible); + } + }); + + document + .add_event_listener_with_callback("visibilitychange", closure.as_ref().unchecked_ref()) + .ok(); + + Some(closure) + } + + pub(crate) fn with_input_handler( + &self, + f: impl FnOnce(&mut PlatformInputHandler) -> R, + ) -> Option { + let mut handler = self.state.borrow_mut().input_handler.take()?; + let result = f(&mut handler); + self.state.borrow_mut().input_handler = Some(handler); + Some(result) + } + + pub(crate) fn register_appearance_change( + self: &Rc, + ) -> Option> { + let mql = self + .browser_window + .match_media("(prefers-color-scheme: dark)") + .ok()??; + + let this = Rc::clone(self); + let closure = Closure::::new(move |_event: JsValue| { + let mut callbacks = this.callbacks.borrow_mut(); + if let Some(ref mut callback) = callbacks.appearance_changed { + callback(); + } + }); + + mql.add_event_listener_with_callback("change", closure.as_ref().unchecked_ref()) + .ok(); + + Some(closure) + } +} + +fn current_appearance(browser_window: &web_sys::Window) -> WindowAppearance { + let is_dark = browser_window + .match_media("(prefers-color-scheme: dark)") + .ok() + .flatten() + .map(|mql| mql.matches()) + .unwrap_or(false); + + if is_dark { + WindowAppearance::Dark + } else { + WindowAppearance::Light + } +} + +struct MqlHandle { + mql: web_sys::MediaQueryList, + _closure: Closure, +} + +impl Drop for MqlHandle { + fn drop(&mut self) { + self.mql + .remove_event_listener_with_callback("change", self._closure.as_ref().unchecked_ref()) + .ok(); + } +} + +// Safari does not support `devicePixelContentBoxSize`, so detect whether it's available. +fn check_device_pixel_support() -> bool { + let global: JsValue = js_sys::global().into(); + let Ok(constructor) = js_sys::Reflect::get(&global, &"ResizeObserverEntry".into()) else { + return false; + }; + let Ok(prototype) = js_sys::Reflect::get(&constructor, &"prototype".into()) else { + return false; + }; + let descriptor = js_sys::Object::get_own_property_descriptor( + &prototype.unchecked_into::(), + &"devicePixelContentBoxSize".into(), + ); + !descriptor.is_undefined() +} + +impl raw_window_handle::HasWindowHandle for WebWindow { + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + let canvas_ref: &JsValue = self.inner.canvas.as_ref(); + let obj = std::ptr::NonNull::from(canvas_ref).cast::(); + let handle = raw_window_handle::WebCanvasWindowHandle::new(obj); + Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.into()) }) + } +} + +impl raw_window_handle::HasDisplayHandle for WebWindow { + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + Ok(raw_window_handle::DisplayHandle::web()) + } +} + +impl PlatformWindow for WebWindow { + fn bounds(&self) -> Bounds { + self.inner.state.borrow().bounds + } + + fn is_maximized(&self) -> bool { + false + } + + fn window_bounds(&self) -> WindowBounds { + WindowBounds::Windowed(self.bounds()) + } + + fn content_size(&self) -> Size { + self.inner.state.borrow().bounds.size + } + + fn resize(&mut self, size: Size) { + let style = self.inner.canvas.style(); + style + .set_property("width", &format!("{}px", f32::from(size.width))) + .ok(); + style + .set_property("height", &format!("{}px", f32::from(size.height))) + .ok(); + } + + fn scale_factor(&self) -> f32 { + self.inner.state.borrow().scale_factor + } + + fn appearance(&self) -> WindowAppearance { + current_appearance(&self.inner.browser_window) + } + + fn display(&self) -> Option> { + Some(self.display.clone()) + } + + fn mouse_position(&self) -> Point { + self.inner.state.borrow().mouse_position + } + + fn modifiers(&self) -> Modifiers { + self.inner.state.borrow().modifiers + } + + fn capslock(&self) -> Capslock { + self.inner.state.borrow().capslock + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.inner.state.borrow_mut().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.inner.state.borrow_mut().input_handler.take() + } + + fn prompt( + &self, + _level: PromptLevel, + _msg: &str, + _detail: Option<&str>, + _answers: &[PromptButton], + ) -> Option> { + None + } + + fn activate(&self) { + self.inner.state.borrow_mut().is_active = true; + } + + fn is_active(&self) -> bool { + self.inner.state.borrow().is_active + } + + fn is_hovered(&self) -> bool { + self.inner.state.borrow().is_hovered + } + + fn background_appearance(&self) -> WindowBackgroundAppearance { + WindowBackgroundAppearance::Opaque + } + + fn set_title(&mut self, title: &str) { + self.inner.state.borrow_mut().title = title.to_owned(); + if let Some(document) = self.inner.browser_window.document() { + document.set_title(title); + } + } + + fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} + + fn minimize(&self) { + log::warn!("WebWindow::minimize is not supported in the browser"); + } + + fn zoom(&self) { + log::warn!("WebWindow::zoom is not supported in the browser"); + } + + fn toggle_fullscreen(&self) { + let mut state = self.inner.state.borrow_mut(); + state.is_fullscreen = !state.is_fullscreen; + + if state.is_fullscreen { + let canvas: &web_sys::Element = self.inner.canvas.as_ref(); + canvas.request_fullscreen().ok(); + } else { + if let Some(document) = self.inner.browser_window.document() { + document.exit_fullscreen(); + } + } + } + + fn is_fullscreen(&self) -> bool { + self.inner.state.borrow().is_fullscreen + } + + fn on_request_frame(&self, callback: Box) { + self.inner.callbacks.borrow_mut().request_frame = Some(callback); + } + + fn on_input(&self, callback: Box DispatchEventResult>) { + self.inner.callbacks.borrow_mut().input = Some(callback); + } + + fn on_active_status_change(&self, callback: Box) { + self.inner.callbacks.borrow_mut().active_status_change = Some(callback); + } + + fn on_hover_status_change(&self, callback: Box) { + self.inner.callbacks.borrow_mut().hover_status_change = Some(callback); + } + + fn on_resize(&self, callback: Box, f32)>) { + self.inner.callbacks.borrow_mut().resize = Some(callback); + } + + fn on_moved(&self, callback: Box) { + self.inner.callbacks.borrow_mut().moved = Some(callback); + } + + fn on_should_close(&self, callback: Box bool>) { + self.inner.callbacks.borrow_mut().should_close = Some(callback); + } + + fn on_close(&self, callback: Box) { + self.inner.callbacks.borrow_mut().close = Some(callback); + } + + fn on_hit_test_window_control(&self, callback: Box Option>) { + self.inner.callbacks.borrow_mut().hit_test_window_control = Some(callback); + } + + fn on_appearance_changed(&self, callback: Box) { + self.inner.callbacks.borrow_mut().appearance_changed = Some(callback); + } + + fn draw(&self, scene: &Scene) { + if let Some((width, height)) = self.inner.pending_physical_size.take() { + if self.inner.canvas.width() != width || self.inner.canvas.height() != height { + self.inner.canvas.set_width(width); + self.inner.canvas.set_height(height); + } + + let mut state = self.inner.state.borrow_mut(); + state.renderer.update_drawable_size(Size { + width: DevicePixels(width as i32), + height: DevicePixels(height as i32), + }); + drop(state); + } + + self.inner.state.borrow_mut().renderer.draw(scene); + } + + fn completed_frame(&self) { + // On web, presentation happens automatically via wgpu surface present + } + + fn sprite_atlas(&self) -> Arc { + self.inner.state.borrow().renderer.sprite_atlas().clone() + } + + fn is_subpixel_rendering_supported(&self) -> bool { + self.inner + .state + .borrow() + .renderer + .supports_dual_source_blending() + } + + fn gpu_specs(&self) -> Option { + Some(self.inner.state.borrow().renderer.gpu_specs()) + } + + fn update_ime_position(&self, _bounds: Bounds) {} + + fn request_decorations(&self, _decorations: WindowDecorations) {} + + fn show_window_menu(&self, _position: Point) {} + + fn start_window_move(&self) {} + + fn start_window_resize(&self, _edge: ResizeEdge) {} + + fn window_decorations(&self) -> Decorations { + Decorations::Server + } + + fn set_app_id(&mut self, _app_id: &str) {} + + fn window_controls(&self) -> WindowControls { + WindowControls { + fullscreen: true, + maximize: false, + minimize: false, + window_menu: false, + } + } + + fn set_client_inset(&self, _inset: Pixels) {} +} diff --git a/crates/gpui_wgpu/src/shaders_subpixel.wgsl b/crates/gpui_wgpu/src/shaders_subpixel.wgsl index 37face0c482..8fd936469dc 100644 --- a/crates/gpui_wgpu/src/shaders_subpixel.wgsl +++ b/crates/gpui_wgpu/src/shaders_subpixel.wgsl @@ -1,56 +1,56 @@ -// --- subpixel sprites --- // - -struct SubpixelSprite { - order: u32, - pad: u32, - bounds: Bounds, - content_mask: Bounds, - color: Hsla, - tile: AtlasTile, - transformation: TransformationMatrix, -} -@group(1) @binding(0) var b_subpixel_sprites: array; - -struct SubpixelSpriteOutput { - @builtin(position) position: vec4, - @location(0) tile_position: vec2, - @location(1) @interpolate(flat) color: vec4, - @location(3) clip_distances: vec4, -} - -struct SubpixelSpriteFragmentOutput { - @location(0) @blend_src(0) foreground: vec4, - @location(0) @blend_src(1) alpha: vec4, -} - -@vertex -fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput { - let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let sprite = b_subpixel_sprites[instance_id]; - - var out = SubpixelSpriteOutput(); - out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); - out.tile_position = to_tile_position(unit_vertex, sprite.tile); - out.color = hsla_to_rgba(sprite.color); - out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); - return out; -} - -@fragment -fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutput { - var sample = textureSample(t_sprite, s_sprite, input.tile_position).rgb; - if (gamma_params.is_bgr != 0u) { - sample = sample.bgr; - } - let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, gamma_params.subpixel_enhanced_contrast, gamma_params.gamma_ratios); - - // Alpha clip after using the derivatives. - if (any(input.clip_distances < vec4(0.0))) { - return SubpixelSpriteFragmentOutput(vec4(0.0), vec4(0.0)); - } - - var out = SubpixelSpriteFragmentOutput(); - out.foreground = vec4(input.color.rgb, 1.0); - out.alpha = vec4(input.color.a * alpha_corrected, 1.0); - return out; -} +// --- subpixel sprites --- // + +struct SubpixelSprite { + order: u32, + pad: u32, + bounds: Bounds, + content_mask: Bounds, + color: Hsla, + tile: AtlasTile, + transformation: TransformationMatrix, +} +@group(1) @binding(0) var b_subpixel_sprites: array; + +struct SubpixelSpriteOutput { + @builtin(position) position: vec4, + @location(0) tile_position: vec2, + @location(1) @interpolate(flat) color: vec4, + @location(3) clip_distances: vec4, +} + +struct SubpixelSpriteFragmentOutput { + @location(0) @blend_src(0) foreground: vec4, + @location(0) @blend_src(1) alpha: vec4, +} + +@vertex +fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput { + let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); + let sprite = b_subpixel_sprites[instance_id]; + + var out = SubpixelSpriteOutput(); + out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); + out.tile_position = to_tile_position(unit_vertex, sprite.tile); + out.color = hsla_to_rgba(sprite.color); + out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); + return out; +} + +@fragment +fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutput { + var sample = textureSample(t_sprite, s_sprite, input.tile_position).rgb; + if (gamma_params.is_bgr != 0u) { + sample = sample.bgr; + } + let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, gamma_params.subpixel_enhanced_contrast, gamma_params.gamma_ratios); + + // Alpha clip after using the derivatives. + if (any(input.clip_distances < vec4(0.0))) { + return SubpixelSpriteFragmentOutput(vec4(0.0), vec4(0.0)); + } + + var out = SubpixelSpriteFragmentOutput(); + out.foreground = vec4(input.color.rgb, 1.0); + out.alpha = vec4(input.color.a * alpha_corrected, 1.0); + return out; +} diff --git a/crates/install_cli/src/install_cli_binary.rs b/crates/install_cli/src/install_cli_binary.rs index 095ed3cd315..4c6d8cde40c 100644 --- a/crates/install_cli/src/install_cli_binary.rs +++ b/crates/install_cli/src/install_cli_binary.rs @@ -1,101 +1,101 @@ -use super::register_zed_scheme; -use anyhow::{Context as _, Result}; -use gpui::{AppContext as _, AsyncApp, Context, PromptLevel, Window, actions}; -use release_channel::ReleaseChannel; -use std::ops::Deref; -use std::path::{Path, PathBuf}; -use util::ResultExt; -use workspace::notifications::{DetachAndPromptErr, NotificationId}; -use workspace::{Toast, Workspace}; - -actions!( - cli, - [ - /// Installs the Zed CLI tool to the system PATH. - InstallCliBinary, - ] -); - -async fn install_script(cx: &AsyncApp) -> Result { - let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))?; - let link_path = Path::new("/usr/local/bin/zed"); - let bin_dir_path = link_path.parent().unwrap(); - - // Don't re-create symlink if it points to the same CLI binary. - if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) { - return Ok(link_path.into()); - } - - // If the symlink is not there or is outdated, first try replacing it - // without escalating. - smol::fs::remove_file(link_path).await.log_err(); - if smol::fs::unix::symlink(&cli_path, link_path) - .await - .log_err() - .is_some() - { - return Ok(link_path.into()); - } - - // The symlink could not be created, so use osascript with admin privileges - // to create it. - let status = smol::process::Command::new("/usr/bin/osascript") - .args([ - "-e", - &format!( - "do shell script \" \ - mkdir -p \'{}\' && \ - ln -sf \'{}\' \'{}\' \ - \" with administrator privileges", - bin_dir_path.to_string_lossy(), - cli_path.to_string_lossy(), - link_path.to_string_lossy(), - ), - ]) - .stdout(smol::process::Stdio::inherit()) - .stderr(smol::process::Stdio::inherit()) - .output() - .await? - .status; - anyhow::ensure!(status.success(), "error running osascript"); - Ok(link_path.into()) -} - -pub fn install_cli_binary(window: &mut Window, cx: &mut Context) { - const LINUX_PROMPT_DETAIL: &str = "If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source like your package manager, then you may need to create an alias/symlink manually.\n\nDepending on your package manager, the CLI might be named zeditor, zedit, zed-editor or something else."; - - cx.spawn_in(window, async move |workspace, cx| { - if cfg!(any(target_os = "linux", target_os = "freebsd")) { - let prompt = cx.prompt( - PromptLevel::Warning, - "CLI should already be installed", - Some(LINUX_PROMPT_DETAIL), - &["Ok"], - ); - cx.background_spawn(prompt).detach(); - return Ok(()); - } - let path = install_script(cx.deref()) - .await - .context("error creating CLI symlink")?; - - workspace.update_in(cx, |workspace, _, cx| { - struct InstalledZedCli; - - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - format!( - "Installed `zed` to {}. You can launch {} from your terminal.", - path.to_string_lossy(), - ReleaseChannel::global(cx).display_name() - ), - ), - cx, - ) - })?; - register_zed_scheme(cx).await.log_err(); - Ok(()) - }) - .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None); -} +use super::register_zed_scheme; +use anyhow::{Context as _, Result}; +use gpui::{AppContext as _, AsyncApp, Context, PromptLevel, Window, actions}; +use release_channel::ReleaseChannel; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use util::ResultExt; +use workspace::notifications::{DetachAndPromptErr, NotificationId}; +use workspace::{Toast, Workspace}; + +actions!( + cli, + [ + /// Installs the Zed CLI tool to the system PATH. + InstallCliBinary, + ] +); + +async fn install_script(cx: &AsyncApp) -> Result { + let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))?; + let link_path = Path::new("/usr/local/bin/zed"); + let bin_dir_path = link_path.parent().unwrap(); + + // Don't re-create symlink if it points to the same CLI binary. + if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) { + return Ok(link_path.into()); + } + + // If the symlink is not there or is outdated, first try replacing it + // without escalating. + smol::fs::remove_file(link_path).await.log_err(); + if smol::fs::unix::symlink(&cli_path, link_path) + .await + .log_err() + .is_some() + { + return Ok(link_path.into()); + } + + // The symlink could not be created, so use osascript with admin privileges + // to create it. + let status = smol::process::Command::new("/usr/bin/osascript") + .args([ + "-e", + &format!( + "do shell script \" \ + mkdir -p \'{}\' && \ + ln -sf \'{}\' \'{}\' \ + \" with administrator privileges", + bin_dir_path.to_string_lossy(), + cli_path.to_string_lossy(), + link_path.to_string_lossy(), + ), + ]) + .stdout(smol::process::Stdio::inherit()) + .stderr(smol::process::Stdio::inherit()) + .output() + .await? + .status; + anyhow::ensure!(status.success(), "error running osascript"); + Ok(link_path.into()) +} + +pub fn install_cli_binary(window: &mut Window, cx: &mut Context) { + const LINUX_PROMPT_DETAIL: &str = "If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source like your package manager, then you may need to create an alias/symlink manually.\n\nDepending on your package manager, the CLI might be named zeditor, zedit, zed-editor or something else."; + + cx.spawn_in(window, async move |workspace, cx| { + if cfg!(any(target_os = "linux", target_os = "freebsd")) { + let prompt = cx.prompt( + PromptLevel::Warning, + "CLI should already be installed", + Some(LINUX_PROMPT_DETAIL), + &["Ok"], + ); + cx.background_spawn(prompt).detach(); + return Ok(()); + } + let path = install_script(cx.deref()) + .await + .context("error creating CLI symlink")?; + + workspace.update_in(cx, |workspace, _, cx| { + struct InstalledZedCli; + + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + format!( + "Installed `zed` to {}. You can launch {} from your terminal.", + path.to_string_lossy(), + ReleaseChannel::global(cx).display_name() + ), + ), + cx, + ) + })?; + register_zed_scheme(cx).await.log_err(); + Ok(()) + }) + .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None); +} diff --git a/crates/install_cli/src/register_zed_scheme.rs b/crates/install_cli/src/register_zed_scheme.rs index 5dac3ef5d8e..048b2dd50ab 100644 --- a/crates/install_cli/src/register_zed_scheme.rs +++ b/crates/install_cli/src/register_zed_scheme.rs @@ -1,14 +1,14 @@ -use client::ZED_URL_SCHEME; -use gpui::{AsyncApp, actions}; - -actions!( - cli, - [ - /// Registers the zed:// URL scheme handler. - RegisterZedScheme - ] -); - -pub async fn register_zed_scheme(cx: &AsyncApp) -> anyhow::Result<()> { - cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME)).await -} +use client::ZED_URL_SCHEME; +use gpui::{AsyncApp, actions}; + +actions!( + cli, + [ + /// Registers the zed:// URL scheme handler. + RegisterZedScheme + ] +); + +pub async fn register_zed_scheme(cx: &AsyncApp) -> anyhow::Result<()> { + cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME)).await +} diff --git a/crates/multi_buffer/src/transaction.rs b/crates/multi_buffer/src/transaction.rs index a3afe55cd69..8161df2b7e4 100644 --- a/crates/multi_buffer/src/transaction.rs +++ b/crates/multi_buffer/src/transaction.rs @@ -1,517 +1,517 @@ -use gpui::{App, Context, Entity}; -use language::{self, Buffer, TransactionId}; -use std::{ - collections::HashMap, - ops::Range, - time::{Duration, Instant}, -}; -use sum_tree::Bias; -use text::BufferId; - -use crate::{Anchor, BufferState, MultiBufferOffset}; - -use super::{Event, MultiBuffer}; - -#[derive(Clone)] -pub(super) struct History { - next_transaction_id: TransactionId, - undo_stack: Vec, - redo_stack: Vec, - transaction_depth: usize, - group_interval: Duration, -} - -impl Default for History { - fn default() -> Self { - History { - next_transaction_id: clock::Lamport::MIN, - undo_stack: Vec::new(), - redo_stack: Vec::new(), - transaction_depth: 0, - group_interval: Duration::from_millis(300), - } - } -} - -#[derive(Clone)] -struct Transaction { - id: TransactionId, - buffer_transactions: HashMap, - first_edit_at: Instant, - last_edit_at: Instant, - suppress_grouping: bool, -} - -impl History { - fn start_transaction(&mut self, now: Instant) -> Option { - self.transaction_depth += 1; - if self.transaction_depth == 1 { - let id = self.next_transaction_id.tick(); - self.undo_stack.push(Transaction { - id, - buffer_transactions: Default::default(), - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }); - Some(id) - } else { - None - } - } - - fn end_transaction( - &mut self, - now: Instant, - buffer_transactions: HashMap, - ) -> bool { - assert_ne!(self.transaction_depth, 0); - self.transaction_depth -= 1; - if self.transaction_depth == 0 { - if buffer_transactions.is_empty() { - self.undo_stack.pop(); - false - } else { - self.redo_stack.clear(); - let transaction = self.undo_stack.last_mut().unwrap(); - transaction.last_edit_at = now; - for (buffer_id, transaction_id) in buffer_transactions { - transaction - .buffer_transactions - .entry(buffer_id) - .or_insert(transaction_id); - } - true - } - } else { - false - } - } - - fn push_transaction<'a, T>( - &mut self, - buffer_transactions: T, - now: Instant, - cx: &Context, - ) where - T: IntoIterator, &'a language::Transaction)>, - { - assert_eq!(self.transaction_depth, 0); - let transaction = Transaction { - id: self.next_transaction_id.tick(), - buffer_transactions: buffer_transactions - .into_iter() - .map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id)) - .collect(), - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }; - if !transaction.buffer_transactions.is_empty() { - self.undo_stack.push(transaction); - self.redo_stack.clear(); - } - } - - fn finalize_last_transaction(&mut self) { - if let Some(transaction) = self.undo_stack.last_mut() { - transaction.suppress_grouping = true; - } - } - - fn forget(&mut self, transaction_id: TransactionId) -> Option { - if let Some(ix) = self - .undo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id) - { - Some(self.undo_stack.remove(ix)) - } else if let Some(ix) = self - .redo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id) - { - Some(self.redo_stack.remove(ix)) - } else { - None - } - } - - fn transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> { - self.undo_stack - .iter() - .find(|transaction| transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter() - .find(|transaction| transaction.id == transaction_id) - }) - } - - fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { - self.undo_stack - .iter_mut() - .find(|transaction| transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter_mut() - .find(|transaction| transaction.id == transaction_id) - }) - } - - fn pop_undo(&mut self) -> Option<&mut Transaction> { - assert_eq!(self.transaction_depth, 0); - if let Some(transaction) = self.undo_stack.pop() { - self.redo_stack.push(transaction); - self.redo_stack.last_mut() - } else { - None - } - } - - fn pop_redo(&mut self) -> Option<&mut Transaction> { - assert_eq!(self.transaction_depth, 0); - if let Some(transaction) = self.redo_stack.pop() { - self.undo_stack.push(transaction); - self.undo_stack.last_mut() - } else { - None - } - } - - fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> { - let ix = self - .undo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id)?; - let transaction = self.undo_stack.remove(ix); - self.redo_stack.push(transaction); - self.redo_stack.last() - } - - fn group(&mut self) -> Option { - let mut count = 0; - let mut transactions = self.undo_stack.iter(); - if let Some(mut transaction) = transactions.next_back() { - while let Some(prev_transaction) = transactions.next_back() { - if !prev_transaction.suppress_grouping - && transaction.first_edit_at - prev_transaction.last_edit_at - <= self.group_interval - { - transaction = prev_transaction; - count += 1; - } else { - break; - } - } - } - self.group_trailing(count) - } - - fn group_until(&mut self, transaction_id: TransactionId) { - let mut count = 0; - for transaction in self.undo_stack.iter().rev() { - if transaction.id == transaction_id { - self.group_trailing(count); - break; - } else if transaction.suppress_grouping { - break; - } else { - count += 1; - } - } - } - - fn group_trailing(&mut self, n: usize) -> Option { - let new_len = self.undo_stack.len() - n; - let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len); - if let Some(last_transaction) = transactions_to_keep.last_mut() { - if let Some(transaction) = transactions_to_merge.last() { - last_transaction.last_edit_at = transaction.last_edit_at; - } - for to_merge in transactions_to_merge { - for (buffer_id, transaction_id) in &to_merge.buffer_transactions { - last_transaction - .buffer_transactions - .entry(*buffer_id) - .or_insert(*transaction_id); - } - } - } - - self.undo_stack.truncate(new_len); - self.undo_stack.last().map(|t| t.id) - } - - pub(super) fn transaction_depth(&self) -> usize { - self.transaction_depth - } - - pub fn set_group_interval(&mut self, group_interval: Duration) { - self.group_interval = group_interval; - } -} - -impl MultiBuffer { - pub fn start_transaction(&mut self, cx: &mut Context) -> Option { - self.start_transaction_at(Instant::now(), cx) - } - - pub fn start_transaction_at( - &mut self, - now: Instant, - cx: &mut Context, - ) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); - } - - for BufferState { buffer, .. } in self.buffers.values() { - buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); - } - self.history.start_transaction(now) - } - - pub fn last_transaction_id(&self, cx: &App) -> Option { - if let Some(buffer) = self.as_singleton() { - buffer - .read(cx) - .peek_undo_stack() - .map(|history_entry| history_entry.transaction_id()) - } else { - let last_transaction = self.history.undo_stack.last()?; - Some(last_transaction.id) - } - } - - pub fn end_transaction(&mut self, cx: &mut Context) -> Option { - self.end_transaction_at(Instant::now(), cx) - } - - pub fn end_transaction_at( - &mut self, - now: Instant, - cx: &mut Context, - ) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)); - } - - let mut buffer_transactions = HashMap::default(); - for BufferState { buffer, .. } in self.buffers.values() { - if let Some(transaction_id) = - buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) - { - buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id); - } - } - - if self.history.end_transaction(now, buffer_transactions) { - let transaction_id = self.history.group().unwrap(); - Some(transaction_id) - } else { - None - } - } - - pub fn edited_ranges_for_transaction( - &self, - transaction_id: TransactionId, - cx: &App, - ) -> Vec> { - let Some(transaction) = self.history.transaction(transaction_id) else { - return Vec::new(); - }; - - let snapshot = self.read(cx); - let mut buffer_anchors = Vec::new(); - - for (buffer_id, buffer_transaction) in &transaction.buffer_transactions { - let Some(buffer) = self.buffer(*buffer_id) else { - continue; - }; - let Some(excerpt) = snapshot.first_excerpt_for_buffer(*buffer_id) else { - continue; - }; - let buffer_snapshot = buffer.read(cx).snapshot(); - - for range in buffer - .read(cx) - .edited_ranges_for_transaction_id::(*buffer_transaction) - { - buffer_anchors.push(Anchor::in_buffer( - excerpt.path_key_index, - buffer_snapshot.anchor_at(range.start, Bias::Left), - )); - buffer_anchors.push(Anchor::in_buffer( - excerpt.path_key_index, - buffer_snapshot.anchor_at(range.end, Bias::Right), - )); - } - } - buffer_anchors.sort_unstable_by(|a, b| a.cmp(b, &snapshot)); - - snapshot - .summaries_for_anchors(buffer_anchors.iter()) - .as_chunks::<2>() - .0 - .iter() - .map(|&[s, e]| s..e) - .collect::>() - } - - pub fn merge_transactions( - &mut self, - transaction: TransactionId, - destination: TransactionId, - cx: &mut Context, - ) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.merge_transactions(transaction, destination) - }); - } else if let Some(transaction) = self.history.forget(transaction) - && let Some(destination) = self.history.transaction_mut(destination) - { - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { - if let Some(destination_buffer_transaction_id) = - destination.buffer_transactions.get(&buffer_id) - { - if let Some(state) = self.buffers.get(&buffer_id) { - state.buffer.update(cx, |buffer, _| { - buffer.merge_transactions( - buffer_transaction_id, - *destination_buffer_transaction_id, - ) - }); - } - } else { - destination - .buffer_transactions - .insert(buffer_id, buffer_transaction_id); - } - } - } - } - - pub fn finalize_last_transaction(&mut self, cx: &mut Context) { - self.history.finalize_last_transaction(); - for BufferState { buffer, .. } in self.buffers.values() { - buffer.update(cx, |buffer, _| { - buffer.finalize_last_transaction(); - }); - } - } - - pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &Context) - where - T: IntoIterator, &'a language::Transaction)>, - { - self.history - .push_transaction(buffer_transactions, Instant::now(), cx); - self.history.finalize_last_transaction(); - } - - pub fn group_until_transaction( - &mut self, - transaction_id: TransactionId, - cx: &mut Context, - ) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.group_until_transaction(transaction_id) - }); - } else { - self.history.group_until(transaction_id); - } - } - pub fn undo(&mut self, cx: &mut Context) -> Option { - let mut transaction_id = None; - if let Some(buffer) = self.as_singleton() { - transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx)); - } else { - while let Some(transaction) = self.history.pop_undo() { - let mut undone = false; - for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - undone |= buffer.update(cx, |buffer, cx| { - let undo_to = *buffer_transaction_id; - if let Some(entry) = buffer.peek_undo_stack() { - *buffer_transaction_id = entry.transaction_id(); - } - buffer.undo_to_transaction(undo_to, cx) - }); - } - } - - if undone { - transaction_id = Some(transaction.id); - break; - } - } - } - - if let Some(transaction_id) = transaction_id { - cx.emit(Event::TransactionUndone { transaction_id }); - } - - transaction_id - } - - pub fn redo(&mut self, cx: &mut Context) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| buffer.redo(cx)); - } - - while let Some(transaction) = self.history.pop_redo() { - let mut redone = false; - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions.iter_mut() { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - redone |= buffer.update(cx, |buffer, cx| { - let redo_to = *buffer_transaction_id; - if let Some(entry) = buffer.peek_redo_stack() { - *buffer_transaction_id = entry.transaction_id(); - } - buffer.redo_to_transaction(redo_to, cx) - }); - } - } - - if redone { - return Some(transaction.id); - } - } - - None - } - - pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); - } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) { - for (buffer_id, transaction_id) in &transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.undo_transaction(*transaction_id, cx) - }); - } - } - } - } - - pub fn forget_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.forget_transaction(transaction_id); - }); - } else if let Some(transaction) = self.history.forget(transaction_id) { - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { - if let Some(state) = self.buffers.get_mut(&buffer_id) { - state.buffer.update(cx, |buffer, _| { - buffer.forget_transaction(buffer_transaction_id); - }); - } - } - } - } -} +use gpui::{App, Context, Entity}; +use language::{self, Buffer, TransactionId}; +use std::{ + collections::HashMap, + ops::Range, + time::{Duration, Instant}, +}; +use sum_tree::Bias; +use text::BufferId; + +use crate::{Anchor, BufferState, MultiBufferOffset}; + +use super::{Event, MultiBuffer}; + +#[derive(Clone)] +pub(super) struct History { + next_transaction_id: TransactionId, + undo_stack: Vec, + redo_stack: Vec, + transaction_depth: usize, + group_interval: Duration, +} + +impl Default for History { + fn default() -> Self { + History { + next_transaction_id: clock::Lamport::MIN, + undo_stack: Vec::new(), + redo_stack: Vec::new(), + transaction_depth: 0, + group_interval: Duration::from_millis(300), + } + } +} + +#[derive(Clone)] +struct Transaction { + id: TransactionId, + buffer_transactions: HashMap, + first_edit_at: Instant, + last_edit_at: Instant, + suppress_grouping: bool, +} + +impl History { + fn start_transaction(&mut self, now: Instant) -> Option { + self.transaction_depth += 1; + if self.transaction_depth == 1 { + let id = self.next_transaction_id.tick(); + self.undo_stack.push(Transaction { + id, + buffer_transactions: Default::default(), + first_edit_at: now, + last_edit_at: now, + suppress_grouping: false, + }); + Some(id) + } else { + None + } + } + + fn end_transaction( + &mut self, + now: Instant, + buffer_transactions: HashMap, + ) -> bool { + assert_ne!(self.transaction_depth, 0); + self.transaction_depth -= 1; + if self.transaction_depth == 0 { + if buffer_transactions.is_empty() { + self.undo_stack.pop(); + false + } else { + self.redo_stack.clear(); + let transaction = self.undo_stack.last_mut().unwrap(); + transaction.last_edit_at = now; + for (buffer_id, transaction_id) in buffer_transactions { + transaction + .buffer_transactions + .entry(buffer_id) + .or_insert(transaction_id); + } + true + } + } else { + false + } + } + + fn push_transaction<'a, T>( + &mut self, + buffer_transactions: T, + now: Instant, + cx: &Context, + ) where + T: IntoIterator, &'a language::Transaction)>, + { + assert_eq!(self.transaction_depth, 0); + let transaction = Transaction { + id: self.next_transaction_id.tick(), + buffer_transactions: buffer_transactions + .into_iter() + .map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id)) + .collect(), + first_edit_at: now, + last_edit_at: now, + suppress_grouping: false, + }; + if !transaction.buffer_transactions.is_empty() { + self.undo_stack.push(transaction); + self.redo_stack.clear(); + } + } + + fn finalize_last_transaction(&mut self) { + if let Some(transaction) = self.undo_stack.last_mut() { + transaction.suppress_grouping = true; + } + } + + fn forget(&mut self, transaction_id: TransactionId) -> Option { + if let Some(ix) = self + .undo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id) + { + Some(self.undo_stack.remove(ix)) + } else if let Some(ix) = self + .redo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id) + { + Some(self.redo_stack.remove(ix)) + } else { + None + } + } + + fn transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> { + self.undo_stack + .iter() + .find(|transaction| transaction.id == transaction_id) + .or_else(|| { + self.redo_stack + .iter() + .find(|transaction| transaction.id == transaction_id) + }) + } + + fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { + self.undo_stack + .iter_mut() + .find(|transaction| transaction.id == transaction_id) + .or_else(|| { + self.redo_stack + .iter_mut() + .find(|transaction| transaction.id == transaction_id) + }) + } + + fn pop_undo(&mut self) -> Option<&mut Transaction> { + assert_eq!(self.transaction_depth, 0); + if let Some(transaction) = self.undo_stack.pop() { + self.redo_stack.push(transaction); + self.redo_stack.last_mut() + } else { + None + } + } + + fn pop_redo(&mut self) -> Option<&mut Transaction> { + assert_eq!(self.transaction_depth, 0); + if let Some(transaction) = self.redo_stack.pop() { + self.undo_stack.push(transaction); + self.undo_stack.last_mut() + } else { + None + } + } + + fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> { + let ix = self + .undo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id)?; + let transaction = self.undo_stack.remove(ix); + self.redo_stack.push(transaction); + self.redo_stack.last() + } + + fn group(&mut self) -> Option { + let mut count = 0; + let mut transactions = self.undo_stack.iter(); + if let Some(mut transaction) = transactions.next_back() { + while let Some(prev_transaction) = transactions.next_back() { + if !prev_transaction.suppress_grouping + && transaction.first_edit_at - prev_transaction.last_edit_at + <= self.group_interval + { + transaction = prev_transaction; + count += 1; + } else { + break; + } + } + } + self.group_trailing(count) + } + + fn group_until(&mut self, transaction_id: TransactionId) { + let mut count = 0; + for transaction in self.undo_stack.iter().rev() { + if transaction.id == transaction_id { + self.group_trailing(count); + break; + } else if transaction.suppress_grouping { + break; + } else { + count += 1; + } + } + } + + fn group_trailing(&mut self, n: usize) -> Option { + let new_len = self.undo_stack.len() - n; + let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len); + if let Some(last_transaction) = transactions_to_keep.last_mut() { + if let Some(transaction) = transactions_to_merge.last() { + last_transaction.last_edit_at = transaction.last_edit_at; + } + for to_merge in transactions_to_merge { + for (buffer_id, transaction_id) in &to_merge.buffer_transactions { + last_transaction + .buffer_transactions + .entry(*buffer_id) + .or_insert(*transaction_id); + } + } + } + + self.undo_stack.truncate(new_len); + self.undo_stack.last().map(|t| t.id) + } + + pub(super) fn transaction_depth(&self) -> usize { + self.transaction_depth + } + + pub fn set_group_interval(&mut self, group_interval: Duration) { + self.group_interval = group_interval; + } +} + +impl MultiBuffer { + pub fn start_transaction(&mut self, cx: &mut Context) -> Option { + self.start_transaction_at(Instant::now(), cx) + } + + pub fn start_transaction_at( + &mut self, + now: Instant, + cx: &mut Context, + ) -> Option { + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); + } + + for BufferState { buffer, .. } in self.buffers.values() { + buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); + } + self.history.start_transaction(now) + } + + pub fn last_transaction_id(&self, cx: &App) -> Option { + if let Some(buffer) = self.as_singleton() { + buffer + .read(cx) + .peek_undo_stack() + .map(|history_entry| history_entry.transaction_id()) + } else { + let last_transaction = self.history.undo_stack.last()?; + Some(last_transaction.id) + } + } + + pub fn end_transaction(&mut self, cx: &mut Context) -> Option { + self.end_transaction_at(Instant::now(), cx) + } + + pub fn end_transaction_at( + &mut self, + now: Instant, + cx: &mut Context, + ) -> Option { + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)); + } + + let mut buffer_transactions = HashMap::default(); + for BufferState { buffer, .. } in self.buffers.values() { + if let Some(transaction_id) = + buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) + { + buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id); + } + } + + if self.history.end_transaction(now, buffer_transactions) { + let transaction_id = self.history.group().unwrap(); + Some(transaction_id) + } else { + None + } + } + + pub fn edited_ranges_for_transaction( + &self, + transaction_id: TransactionId, + cx: &App, + ) -> Vec> { + let Some(transaction) = self.history.transaction(transaction_id) else { + return Vec::new(); + }; + + let snapshot = self.read(cx); + let mut buffer_anchors = Vec::new(); + + for (buffer_id, buffer_transaction) in &transaction.buffer_transactions { + let Some(buffer) = self.buffer(*buffer_id) else { + continue; + }; + let Some(excerpt) = snapshot.first_excerpt_for_buffer(*buffer_id) else { + continue; + }; + let buffer_snapshot = buffer.read(cx).snapshot(); + + for range in buffer + .read(cx) + .edited_ranges_for_transaction_id::(*buffer_transaction) + { + buffer_anchors.push(Anchor::in_buffer( + excerpt.path_key_index, + buffer_snapshot.anchor_at(range.start, Bias::Left), + )); + buffer_anchors.push(Anchor::in_buffer( + excerpt.path_key_index, + buffer_snapshot.anchor_at(range.end, Bias::Right), + )); + } + } + buffer_anchors.sort_unstable_by(|a, b| a.cmp(b, &snapshot)); + + snapshot + .summaries_for_anchors(buffer_anchors.iter()) + .as_chunks::<2>() + .0 + .iter() + .map(|&[s, e]| s..e) + .collect::>() + } + + pub fn merge_transactions( + &mut self, + transaction: TransactionId, + destination: TransactionId, + cx: &mut Context, + ) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.merge_transactions(transaction, destination) + }); + } else if let Some(transaction) = self.history.forget(transaction) + && let Some(destination) = self.history.transaction_mut(destination) + { + for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { + if let Some(destination_buffer_transaction_id) = + destination.buffer_transactions.get(&buffer_id) + { + if let Some(state) = self.buffers.get(&buffer_id) { + state.buffer.update(cx, |buffer, _| { + buffer.merge_transactions( + buffer_transaction_id, + *destination_buffer_transaction_id, + ) + }); + } + } else { + destination + .buffer_transactions + .insert(buffer_id, buffer_transaction_id); + } + } + } + } + + pub fn finalize_last_transaction(&mut self, cx: &mut Context) { + self.history.finalize_last_transaction(); + for BufferState { buffer, .. } in self.buffers.values() { + buffer.update(cx, |buffer, _| { + buffer.finalize_last_transaction(); + }); + } + } + + pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &Context) + where + T: IntoIterator, &'a language::Transaction)>, + { + self.history + .push_transaction(buffer_transactions, Instant::now(), cx); + self.history.finalize_last_transaction(); + } + + pub fn group_until_transaction( + &mut self, + transaction_id: TransactionId, + cx: &mut Context, + ) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.group_until_transaction(transaction_id) + }); + } else { + self.history.group_until(transaction_id); + } + } + pub fn undo(&mut self, cx: &mut Context) -> Option { + let mut transaction_id = None; + if let Some(buffer) = self.as_singleton() { + transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx)); + } else { + while let Some(transaction) = self.history.pop_undo() { + let mut undone = false; + for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { + if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { + undone |= buffer.update(cx, |buffer, cx| { + let undo_to = *buffer_transaction_id; + if let Some(entry) = buffer.peek_undo_stack() { + *buffer_transaction_id = entry.transaction_id(); + } + buffer.undo_to_transaction(undo_to, cx) + }); + } + } + + if undone { + transaction_id = Some(transaction.id); + break; + } + } + } + + if let Some(transaction_id) = transaction_id { + cx.emit(Event::TransactionUndone { transaction_id }); + } + + transaction_id + } + + pub fn redo(&mut self, cx: &mut Context) -> Option { + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, cx| buffer.redo(cx)); + } + + while let Some(transaction) = self.history.pop_redo() { + let mut redone = false; + for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions.iter_mut() { + if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { + redone |= buffer.update(cx, |buffer, cx| { + let redo_to = *buffer_transaction_id; + if let Some(entry) = buffer.peek_redo_stack() { + *buffer_transaction_id = entry.transaction_id(); + } + buffer.redo_to_transaction(redo_to, cx) + }); + } + } + + if redone { + return Some(transaction.id); + } + } + + None + } + + pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); + } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) { + for (buffer_id, transaction_id) in &transaction.buffer_transactions { + if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { + buffer.update(cx, |buffer, cx| { + buffer.undo_transaction(*transaction_id, cx) + }); + } + } + } + } + + pub fn forget_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.forget_transaction(transaction_id); + }); + } else if let Some(transaction) = self.history.forget(transaction_id) { + for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { + if let Some(state) = self.buffers.get_mut(&buffer_id) { + state.buffer.update(cx, |buffer, _| { + buffer.forget_transaction(buffer_transaction_id); + }); + } + } + } + } +} diff --git a/crates/remote/src/transport/mock.rs b/crates/remote/src/transport/mock.rs index f567d24eb12..01f7579ea56 100644 --- a/crates/remote/src/transport/mock.rs +++ b/crates/remote/src/transport/mock.rs @@ -1,342 +1,342 @@ -//! Mock transport for testing remote connections. -//! -//! This module provides a mock implementation of the `RemoteConnection` trait -//! that allows testing remote editing functionality without actual SSH/WSL/Docker -//! connections. -//! -//! # Usage -//! -//! ```rust,ignore -//! use remote::{MockConnection, RemoteClient}; -//! -//! #[gpui::test] -//! async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { -//! let (opts, server_session) = MockConnection::new(cx, server_cx); -//! -//! // Create the headless project (server side) -//! server_cx.update(HeadlessProject::init); -//! let _headless = server_cx.new(|cx| { -//! HeadlessProject::new( -//! HeadlessAppState { session: server_session, /* ... */ }, -//! false, -//! cx, -//! ) -//! }); -//! -//! // Create the client using the helper -//! let (client, server_client) = RemoteClient::new_mock(cx, server_cx).await; -//! // ... test logic ... -//! } -//! ``` - -use crate::remote_client::{ - ChannelClient, CommandTemplate, Interactive, RemoteClientDelegate, RemoteConnection, - RemoteConnectionOptions, -}; -use anyhow::Result; -use async_trait::async_trait; -use collections::HashMap; -use futures::{ - FutureExt, SinkExt, StreamExt, - channel::{ - mpsc::{self, Sender}, - oneshot, - }, - select_biased, -}; -use gpui::{App, AppContext as _, AsyncApp, Global, Task, TestAppContext}; -use rpc::{AnyProtoClient, proto::Envelope}; -use std::{ - path::PathBuf, - sync::{ - Arc, - atomic::{AtomicU64, Ordering}, - }, -}; -use util::paths::{PathStyle, RemotePathBuf}; - -/// Unique identifier for a mock connection. -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] -pub struct MockConnectionOptions { - pub id: u64, -} - -/// A mock implementation of `RemoteConnection` for testing. -pub struct MockRemoteConnection { - options: MockConnectionOptions, - server_channel: Arc, - server_cx: SendableCx, -} - -/// Wrapper to pass `AsyncApp` across thread boundaries in tests. -/// -/// # Safety -/// -/// This is safe because in test mode, GPUI is always single-threaded and so -/// having access to one async app means being on the same main thread. -pub(crate) struct SendableCx(AsyncApp); - -impl SendableCx { - pub(crate) fn new(cx: &TestAppContext) -> Self { - Self(cx.to_async()) - } - - pub(crate) fn get(&self, _: &AsyncApp) -> AsyncApp { - self.0.clone() - } -} - -// SAFETY: In test mode, GPUI is always single-threaded, and SendableCx -// is only accessed from the main thread via the get() method which -// requires a valid AsyncApp reference. -unsafe impl Send for SendableCx {} -unsafe impl Sync for SendableCx {} - -/// Global registry that holds pre-created mock connections. -/// -/// When `ConnectionPool::connect` is called with `MockConnectionOptions`, -/// it retrieves the connection from this registry. -#[derive(Default)] -pub struct MockConnectionRegistry { - pending: HashMap, Arc)>, -} - -impl Global for MockConnectionRegistry {} - -impl MockConnectionRegistry { - /// Called by `ConnectionPool::connect` to retrieve a pre-registered mock connection. - pub fn take( - &mut self, - opts: &MockConnectionOptions, - ) -> Option> + use<>> { - let (guard, con) = self.pending.remove(&opts.id)?; - Some(async move { - _ = guard.await; - con - }) - } -} - -/// Helper for creating mock connection pairs in tests. -pub struct MockConnection; - -pub type ConnectGuard = oneshot::Sender<()>; - -impl MockConnection { - /// Creates a new mock connection pair for testing. - /// - /// This function: - /// 1. Creates a unique `MockConnectionOptions` identifier - /// 2. Sets up the server-side channel (returned as `AnyProtoClient`) - /// 3. Creates a `MockRemoteConnection` and registers it in the global registry - /// 4. The connection will be retrieved from the registry when `ConnectionPool::connect` is called - /// - /// Returns: - /// - `MockConnectionOptions` to pass to `remote::connect()` or `RemoteClient` creation - /// - `AnyProtoClient` to pass to `HeadlessProject::new()` as the session - /// - /// # Arguments - /// - `client_cx`: The test context for the client side - /// - `server_cx`: The test context for the server/headless side - pub(crate) fn new( - client_cx: &mut TestAppContext, - server_cx: &mut TestAppContext, - ) -> (MockConnectionOptions, AnyProtoClient, ConnectGuard) { - static NEXT_ID: AtomicU64 = AtomicU64::new(0); - let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); - let opts = MockConnectionOptions { id }; - let (server_client, connect_guard) = - Self::new_with_opts(opts.clone(), client_cx, server_cx); - (opts, server_client, connect_guard) - } - - /// Creates a mock connection pair for existing `MockConnectionOptions`. - /// - /// This is useful when simulating reconnection: after a connection is torn - /// down, register a new mock server under the same options so the next - /// `ConnectionPool::connect` call finds it. - pub(crate) fn new_with_opts( - opts: MockConnectionOptions, - client_cx: &mut TestAppContext, - server_cx: &mut TestAppContext, - ) -> (AnyProtoClient, ConnectGuard) { - let (outgoing_tx, _) = mpsc::unbounded::(); - let (_, incoming_rx) = mpsc::unbounded::(); - let server_client = server_cx - .update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "mock-server", false)); - - let connection = Arc::new(MockRemoteConnection { - options: opts.clone(), - server_channel: server_client.clone(), - server_cx: SendableCx::new(server_cx), - }); - - let (tx, rx) = oneshot::channel(); - - client_cx.update(|cx| { - cx.default_global::() - .pending - .insert(opts.id, (rx, connection)); - }); - - (server_client.into(), tx) - } -} - -#[async_trait(?Send)] -impl RemoteConnection for MockRemoteConnection { - async fn kill(&self) -> Result<()> { - Ok(()) - } - - fn has_been_killed(&self) -> bool { - false - } - - fn build_command( - &self, - program: Option, - args: &[String], - env: &HashMap, - _working_dir: Option, - _port_forward: Option<(u16, String, u16)>, - _interactive: Interactive, - ) -> Result { - let shell_program = program.unwrap_or_else(|| "sh".to_string()); - let mut shell_args = Vec::new(); - shell_args.push(shell_program); - shell_args.extend(args.iter().cloned()); - Ok(CommandTemplate { - program: "mock".into(), - args: shell_args, - env: env.clone(), - }) - } - - fn build_forward_ports_command( - &self, - forwards: Vec<(u16, String, u16)>, - ) -> Result { - Ok(CommandTemplate { - program: "mock".into(), - args: std::iter::once("-N".to_owned()) - .chain(forwards.into_iter().map(|(local_port, host, remote_port)| { - format!("{local_port}:{host}:{remote_port}") - })) - .collect(), - env: Default::default(), - }) - } - - fn upload_directory( - &self, - _src_path: PathBuf, - _dest_path: RemotePathBuf, - _cx: &App, - ) -> Task> { - Task::ready(Ok(())) - } - - fn connection_options(&self) -> RemoteConnectionOptions { - RemoteConnectionOptions::Mock(self.options.clone()) - } - - fn simulate_disconnect(&self, cx: &AsyncApp) { - let (outgoing_tx, _) = mpsc::unbounded::(); - let (_, incoming_rx) = mpsc::unbounded::(); - self.server_channel - .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx)); - } - - fn start_proxy( - &self, - _unique_identifier: String, - _reconnect: bool, - mut client_incoming_tx: mpsc::UnboundedSender, - mut client_outgoing_rx: mpsc::UnboundedReceiver, - mut connection_activity_tx: Sender<()>, - _delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::(); - let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::(); - - self.server_channel.reconnect( - server_incoming_rx, - server_outgoing_tx, - &self.server_cx.get(cx), - ); - - cx.background_spawn(async move { - loop { - select_biased! { - server_to_client = server_outgoing_rx.next().fuse() => { - let Some(server_to_client) = server_to_client else { - return Ok(1) - }; - connection_activity_tx.try_send(()).ok(); - client_incoming_tx.send(server_to_client).await.ok(); - } - client_to_server = client_outgoing_rx.next().fuse() => { - let Some(client_to_server) = client_to_server else { - return Ok(1) - }; - server_incoming_tx.send(client_to_server).await.ok(); - } - } - } - }) - } - - fn path_style(&self) -> PathStyle { - PathStyle::local() - } - - fn shell(&self) -> String { - "sh".to_owned() - } - - fn default_system_shell(&self) -> String { - "sh".to_owned() - } - - fn has_wsl_interop(&self) -> bool { - false - } -} - -/// Mock delegate for tests that don't need delegate functionality. -pub struct MockDelegate; - -impl RemoteClientDelegate for MockDelegate { - fn ask_password( - &self, - _prompt: String, - _sender: futures::channel::oneshot::Sender, - _cx: &mut AsyncApp, - ) { - unreachable!("MockDelegate::ask_password should not be called in tests") - } - - fn download_server_binary_locally( - &self, - _platform: crate::RemotePlatform, - _release_channel: release_channel::ReleaseChannel, - _version: Option, - _cx: &mut AsyncApp, - ) -> Task> { - unreachable!("MockDelegate::download_server_binary_locally should not be called in tests") - } - - fn get_download_url( - &self, - _platform: crate::RemotePlatform, - _release_channel: release_channel::ReleaseChannel, - _version: Option, - _cx: &mut AsyncApp, - ) -> Task>> { - unreachable!("MockDelegate::get_download_url should not be called in tests") - } - - fn set_status(&self, _status: Option<&str>, _cx: &mut AsyncApp) {} -} +//! Mock transport for testing remote connections. +//! +//! This module provides a mock implementation of the `RemoteConnection` trait +//! that allows testing remote editing functionality without actual SSH/WSL/Docker +//! connections. +//! +//! # Usage +//! +//! ```rust,ignore +//! use remote::{MockConnection, RemoteClient}; +//! +//! #[gpui::test] +//! async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { +//! let (opts, server_session) = MockConnection::new(cx, server_cx); +//! +//! // Create the headless project (server side) +//! server_cx.update(HeadlessProject::init); +//! let _headless = server_cx.new(|cx| { +//! HeadlessProject::new( +//! HeadlessAppState { session: server_session, /* ... */ }, +//! false, +//! cx, +//! ) +//! }); +//! +//! // Create the client using the helper +//! let (client, server_client) = RemoteClient::new_mock(cx, server_cx).await; +//! // ... test logic ... +//! } +//! ``` + +use crate::remote_client::{ + ChannelClient, CommandTemplate, Interactive, RemoteClientDelegate, RemoteConnection, + RemoteConnectionOptions, +}; +use anyhow::Result; +use async_trait::async_trait; +use collections::HashMap; +use futures::{ + FutureExt, SinkExt, StreamExt, + channel::{ + mpsc::{self, Sender}, + oneshot, + }, + select_biased, +}; +use gpui::{App, AppContext as _, AsyncApp, Global, Task, TestAppContext}; +use rpc::{AnyProtoClient, proto::Envelope}; +use std::{ + path::PathBuf, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; +use util::paths::{PathStyle, RemotePathBuf}; + +/// Unique identifier for a mock connection. +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub struct MockConnectionOptions { + pub id: u64, +} + +/// A mock implementation of `RemoteConnection` for testing. +pub struct MockRemoteConnection { + options: MockConnectionOptions, + server_channel: Arc, + server_cx: SendableCx, +} + +/// Wrapper to pass `AsyncApp` across thread boundaries in tests. +/// +/// # Safety +/// +/// This is safe because in test mode, GPUI is always single-threaded and so +/// having access to one async app means being on the same main thread. +pub(crate) struct SendableCx(AsyncApp); + +impl SendableCx { + pub(crate) fn new(cx: &TestAppContext) -> Self { + Self(cx.to_async()) + } + + pub(crate) fn get(&self, _: &AsyncApp) -> AsyncApp { + self.0.clone() + } +} + +// SAFETY: In test mode, GPUI is always single-threaded, and SendableCx +// is only accessed from the main thread via the get() method which +// requires a valid AsyncApp reference. +unsafe impl Send for SendableCx {} +unsafe impl Sync for SendableCx {} + +/// Global registry that holds pre-created mock connections. +/// +/// When `ConnectionPool::connect` is called with `MockConnectionOptions`, +/// it retrieves the connection from this registry. +#[derive(Default)] +pub struct MockConnectionRegistry { + pending: HashMap, Arc)>, +} + +impl Global for MockConnectionRegistry {} + +impl MockConnectionRegistry { + /// Called by `ConnectionPool::connect` to retrieve a pre-registered mock connection. + pub fn take( + &mut self, + opts: &MockConnectionOptions, + ) -> Option> + use<>> { + let (guard, con) = self.pending.remove(&opts.id)?; + Some(async move { + _ = guard.await; + con + }) + } +} + +/// Helper for creating mock connection pairs in tests. +pub struct MockConnection; + +pub type ConnectGuard = oneshot::Sender<()>; + +impl MockConnection { + /// Creates a new mock connection pair for testing. + /// + /// This function: + /// 1. Creates a unique `MockConnectionOptions` identifier + /// 2. Sets up the server-side channel (returned as `AnyProtoClient`) + /// 3. Creates a `MockRemoteConnection` and registers it in the global registry + /// 4. The connection will be retrieved from the registry when `ConnectionPool::connect` is called + /// + /// Returns: + /// - `MockConnectionOptions` to pass to `remote::connect()` or `RemoteClient` creation + /// - `AnyProtoClient` to pass to `HeadlessProject::new()` as the session + /// + /// # Arguments + /// - `client_cx`: The test context for the client side + /// - `server_cx`: The test context for the server/headless side + pub(crate) fn new( + client_cx: &mut TestAppContext, + server_cx: &mut TestAppContext, + ) -> (MockConnectionOptions, AnyProtoClient, ConnectGuard) { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); + let opts = MockConnectionOptions { id }; + let (server_client, connect_guard) = + Self::new_with_opts(opts.clone(), client_cx, server_cx); + (opts, server_client, connect_guard) + } + + /// Creates a mock connection pair for existing `MockConnectionOptions`. + /// + /// This is useful when simulating reconnection: after a connection is torn + /// down, register a new mock server under the same options so the next + /// `ConnectionPool::connect` call finds it. + pub(crate) fn new_with_opts( + opts: MockConnectionOptions, + client_cx: &mut TestAppContext, + server_cx: &mut TestAppContext, + ) -> (AnyProtoClient, ConnectGuard) { + let (outgoing_tx, _) = mpsc::unbounded::(); + let (_, incoming_rx) = mpsc::unbounded::(); + let server_client = server_cx + .update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "mock-server", false)); + + let connection = Arc::new(MockRemoteConnection { + options: opts.clone(), + server_channel: server_client.clone(), + server_cx: SendableCx::new(server_cx), + }); + + let (tx, rx) = oneshot::channel(); + + client_cx.update(|cx| { + cx.default_global::() + .pending + .insert(opts.id, (rx, connection)); + }); + + (server_client.into(), tx) + } +} + +#[async_trait(?Send)] +impl RemoteConnection for MockRemoteConnection { + async fn kill(&self) -> Result<()> { + Ok(()) + } + + fn has_been_killed(&self) -> bool { + false + } + + fn build_command( + &self, + program: Option, + args: &[String], + env: &HashMap, + _working_dir: Option, + _port_forward: Option<(u16, String, u16)>, + _interactive: Interactive, + ) -> Result { + let shell_program = program.unwrap_or_else(|| "sh".to_string()); + let mut shell_args = Vec::new(); + shell_args.push(shell_program); + shell_args.extend(args.iter().cloned()); + Ok(CommandTemplate { + program: "mock".into(), + args: shell_args, + env: env.clone(), + }) + } + + fn build_forward_ports_command( + &self, + forwards: Vec<(u16, String, u16)>, + ) -> Result { + Ok(CommandTemplate { + program: "mock".into(), + args: std::iter::once("-N".to_owned()) + .chain(forwards.into_iter().map(|(local_port, host, remote_port)| { + format!("{local_port}:{host}:{remote_port}") + })) + .collect(), + env: Default::default(), + }) + } + + fn upload_directory( + &self, + _src_path: PathBuf, + _dest_path: RemotePathBuf, + _cx: &App, + ) -> Task> { + Task::ready(Ok(())) + } + + fn connection_options(&self) -> RemoteConnectionOptions { + RemoteConnectionOptions::Mock(self.options.clone()) + } + + fn simulate_disconnect(&self, cx: &AsyncApp) { + let (outgoing_tx, _) = mpsc::unbounded::(); + let (_, incoming_rx) = mpsc::unbounded::(); + self.server_channel + .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx)); + } + + fn start_proxy( + &self, + _unique_identifier: String, + _reconnect: bool, + mut client_incoming_tx: mpsc::UnboundedSender, + mut client_outgoing_rx: mpsc::UnboundedReceiver, + mut connection_activity_tx: Sender<()>, + _delegate: Arc, + cx: &mut AsyncApp, + ) -> Task> { + let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::(); + let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::(); + + self.server_channel.reconnect( + server_incoming_rx, + server_outgoing_tx, + &self.server_cx.get(cx), + ); + + cx.background_spawn(async move { + loop { + select_biased! { + server_to_client = server_outgoing_rx.next().fuse() => { + let Some(server_to_client) = server_to_client else { + return Ok(1) + }; + connection_activity_tx.try_send(()).ok(); + client_incoming_tx.send(server_to_client).await.ok(); + } + client_to_server = client_outgoing_rx.next().fuse() => { + let Some(client_to_server) = client_to_server else { + return Ok(1) + }; + server_incoming_tx.send(client_to_server).await.ok(); + } + } + } + }) + } + + fn path_style(&self) -> PathStyle { + PathStyle::local() + } + + fn shell(&self) -> String { + "sh".to_owned() + } + + fn default_system_shell(&self) -> String { + "sh".to_owned() + } + + fn has_wsl_interop(&self) -> bool { + false + } +} + +/// Mock delegate for tests that don't need delegate functionality. +pub struct MockDelegate; + +impl RemoteClientDelegate for MockDelegate { + fn ask_password( + &self, + _prompt: String, + _sender: futures::channel::oneshot::Sender, + _cx: &mut AsyncApp, + ) { + unreachable!("MockDelegate::ask_password should not be called in tests") + } + + fn download_server_binary_locally( + &self, + _platform: crate::RemotePlatform, + _release_channel: release_channel::ReleaseChannel, + _version: Option, + _cx: &mut AsyncApp, + ) -> Task> { + unreachable!("MockDelegate::download_server_binary_locally should not be called in tests") + } + + fn get_download_url( + &self, + _platform: crate::RemotePlatform, + _release_channel: release_channel::ReleaseChannel, + _version: Option, + _cx: &mut AsyncApp, + ) -> Task>> { + unreachable!("MockDelegate::get_download_url should not be called in tests") + } + + fn set_status(&self, _status: Option<&str>, _cx: &mut AsyncApp) {} +}