platform.rs

  1use crate::{
  2    AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
  3    DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
  4    PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
  5    PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata,
  6    Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size,
  7};
  8use anyhow::Result;
  9use collections::VecDeque;
 10use futures::channel::oneshot;
 11use parking_lot::Mutex;
 12use std::{
 13    cell::RefCell,
 14    path::{Path, PathBuf},
 15    rc::{Rc, Weak},
 16    sync::Arc,
 17};
 18
 19/// TestPlatform implements the Platform trait for use in tests.
 20pub(crate) struct TestPlatform {
 21    background_executor: BackgroundExecutor,
 22    foreground_executor: ForegroundExecutor,
 23
 24    pub(crate) active_window: RefCell<Option<TestWindow>>,
 25    active_display: Rc<dyn PlatformDisplay>,
 26    active_cursor: Mutex<CursorStyle>,
 27    current_clipboard_item: Mutex<Option<ClipboardItem>>,
 28    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
 29    current_primary_item: Mutex<Option<ClipboardItem>>,
 30    #[cfg(target_os = "macos")]
 31    current_find_pasteboard_item: Mutex<Option<ClipboardItem>>,
 32    pub(crate) prompts: RefCell<TestPrompts>,
 33    screen_capture_sources: RefCell<Vec<TestScreenCaptureSource>>,
 34    pub opened_url: RefCell<Option<String>>,
 35    pub text_system: Arc<dyn PlatformTextSystem>,
 36    pub expect_restart: RefCell<Option<oneshot::Sender<Option<PathBuf>>>>,
 37    headless_renderer_factory: Option<Box<dyn Fn() -> Option<Box<dyn PlatformHeadlessRenderer>>>>,
 38    weak: Weak<Self>,
 39}
 40
 41#[derive(Clone)]
 42/// A fake screen capture source, used for testing.
 43pub struct TestScreenCaptureSource {}
 44
 45/// A fake screen capture stream, used for testing.
 46pub struct TestScreenCaptureStream {}
 47
 48impl ScreenCaptureSource for TestScreenCaptureSource {
 49    fn metadata(&self) -> Result<SourceMetadata> {
 50        Ok(SourceMetadata {
 51            id: 0,
 52            is_main: None,
 53            label: None,
 54            resolution: size(DevicePixels(1), DevicePixels(1)),
 55        })
 56    }
 57
 58    fn stream(
 59        &self,
 60        _foreground_executor: &ForegroundExecutor,
 61        _frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
 62    ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
 63        let (mut tx, rx) = oneshot::channel();
 64        let stream = TestScreenCaptureStream {};
 65        tx.send(Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>))
 66            .ok();
 67        rx
 68    }
 69}
 70
 71impl ScreenCaptureStream for TestScreenCaptureStream {
 72    fn metadata(&self) -> Result<SourceMetadata> {
 73        TestScreenCaptureSource {}.metadata()
 74    }
 75}
 76
 77struct TestPrompt {
 78    msg: String,
 79    detail: Option<String>,
 80    answers: Vec<String>,
 81    tx: oneshot::Sender<usize>,
 82}
 83
 84#[derive(Default)]
 85pub(crate) struct TestPrompts {
 86    multiple_choice: VecDeque<TestPrompt>,
 87    new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
 88    paths: VecDeque<(
 89        crate::PathPromptOptions,
 90        oneshot::Sender<Result<Option<Vec<PathBuf>>>>,
 91    )>,
 92}
 93
 94impl TestPlatform {
 95    pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc<Self> {
 96        Self::with_platform(
 97            executor,
 98            foreground_executor,
 99            Arc::new(NoopTextSystem),
100            None,
101        )
102    }
103
104    pub fn with_text_system(
105        executor: BackgroundExecutor,
106        foreground_executor: ForegroundExecutor,
107        text_system: Arc<dyn PlatformTextSystem>,
108    ) -> Rc<Self> {
109        Self::with_platform(executor, foreground_executor, text_system, None)
110    }
111
112    pub fn with_platform(
113        executor: BackgroundExecutor,
114        foreground_executor: ForegroundExecutor,
115        text_system: Arc<dyn PlatformTextSystem>,
116        headless_renderer_factory: Option<
117            Box<dyn Fn() -> Option<Box<dyn PlatformHeadlessRenderer>>>,
118        >,
119    ) -> Rc<Self> {
120        Rc::new_cyclic(|weak| TestPlatform {
121            background_executor: executor,
122            foreground_executor,
123            prompts: Default::default(),
124            screen_capture_sources: Default::default(),
125            active_cursor: Default::default(),
126            active_display: Rc::new(TestDisplay::new()),
127            active_window: Default::default(),
128            expect_restart: Default::default(),
129            current_clipboard_item: Mutex::new(None),
130            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
131            current_primary_item: Mutex::new(None),
132            #[cfg(target_os = "macos")]
133            current_find_pasteboard_item: Mutex::new(None),
134            weak: weak.clone(),
135            opened_url: Default::default(),
136            text_system,
137            headless_renderer_factory,
138        })
139    }
140
141    pub(crate) fn simulate_new_path_selection(
142        &self,
143        select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
144    ) {
145        let (path, tx) = self
146            .prompts
147            .borrow_mut()
148            .new_path
149            .pop_front()
150            .expect("no pending new path prompt");
151        tx.send(Ok(select_path(&path))).ok();
152    }
153
154    #[track_caller]
155    pub(crate) fn simulate_prompt_answer(&self, response: &str) {
156        let prompt = self
157            .prompts
158            .borrow_mut()
159            .multiple_choice
160            .pop_front()
161            .expect("no pending multiple choice prompt");
162        let Some(ix) = prompt.answers.iter().position(|a| a == response) else {
163            panic!(
164                "PROMPT: {}\n{:?}\n{:?}\nCannot respond with {}",
165                prompt.msg, prompt.detail, prompt.answers, response
166            )
167        };
168        prompt.tx.send(ix).ok();
169    }
170
171    pub(crate) fn has_pending_prompt(&self) -> bool {
172        !self.prompts.borrow().multiple_choice.is_empty()
173    }
174
175    pub(crate) fn pending_prompt(&self) -> Option<(String, String)> {
176        let prompts = self.prompts.borrow();
177        let prompt = prompts.multiple_choice.front()?;
178        Some((
179            prompt.msg.clone(),
180            prompt.detail.clone().unwrap_or_default(),
181        ))
182    }
183
184    pub(crate) fn set_screen_capture_sources(&self, sources: Vec<TestScreenCaptureSource>) {
185        *self.screen_capture_sources.borrow_mut() = sources;
186    }
187
188    pub(crate) fn prompt(
189        &self,
190        msg: &str,
191        detail: Option<&str>,
192        answers: &[PromptButton],
193    ) -> oneshot::Receiver<usize> {
194        let (tx, rx) = oneshot::channel();
195        let answers: Vec<String> = answers.iter().map(|s| s.label().to_string()).collect();
196        self.prompts
197            .borrow_mut()
198            .multiple_choice
199            .push_back(TestPrompt {
200                msg: msg.to_string(),
201                detail: detail.map(|s| s.to_string()),
202                answers,
203                tx,
204            });
205        rx
206    }
207
208    pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
209        let executor = self.foreground_executor();
210        let previous_window = self.active_window.borrow_mut().take();
211        self.active_window.borrow_mut().clone_from(&window);
212
213        executor
214            .spawn(async move {
215                if let Some(previous_window) = previous_window {
216                    if let Some(window) = window.as_ref()
217                        && Rc::ptr_eq(&previous_window.0, &window.0)
218                    {
219                        return;
220                    }
221                    previous_window.simulate_active_status_change(false);
222                }
223                if let Some(window) = window {
224                    window.simulate_active_status_change(true);
225                }
226            })
227            .detach();
228    }
229
230    pub(crate) fn did_prompt_for_new_path(&self) -> bool {
231        !self.prompts.borrow().new_path.is_empty()
232    }
233
234    pub(crate) fn did_prompt_for_paths(&self) -> bool {
235        !self.prompts.borrow().paths.is_empty()
236    }
237
238    pub(crate) fn simulate_paths_selection(
239        &self,
240        select_paths: impl FnOnce(&crate::PathPromptOptions) -> Option<Vec<PathBuf>>,
241    ) {
242        let (options, tx) = self
243            .prompts
244            .borrow_mut()
245            .paths
246            .pop_front()
247            .expect("no pending paths prompt");
248        tx.send(Ok(select_paths(&options))).ok();
249    }
250}
251
252impl Platform for TestPlatform {
253    fn background_executor(&self) -> BackgroundExecutor {
254        self.background_executor.clone()
255    }
256
257    fn foreground_executor(&self) -> ForegroundExecutor {
258        self.foreground_executor.clone()
259    }
260
261    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
262        self.text_system.clone()
263    }
264
265    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
266        Box::new(TestKeyboardLayout)
267    }
268
269    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
270        Rc::new(DummyKeyboardMapper)
271    }
272
273    fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
274
275    fn on_thermal_state_change(&self, _: Box<dyn FnMut()>) {}
276
277    fn thermal_state(&self) -> ThermalState {
278        ThermalState::Nominal
279    }
280
281    fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
282        unimplemented!()
283    }
284
285    fn quit(&self) {}
286
287    fn restart(&self, path: Option<PathBuf>) {
288        if let Some(tx) = self.expect_restart.take() {
289            tx.send(path).unwrap();
290        }
291    }
292
293    fn activate(&self, _ignoring_other_apps: bool) {
294        //
295    }
296
297    fn hide(&self) {
298        unimplemented!()
299    }
300
301    fn hide_other_apps(&self) {
302        unimplemented!()
303    }
304
305    fn unhide_other_apps(&self) {
306        unimplemented!()
307    }
308
309    fn displays(&self) -> Vec<std::rc::Rc<dyn crate::PlatformDisplay>> {
310        vec![self.active_display.clone()]
311    }
312
313    fn primary_display(&self) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
314        Some(self.active_display.clone())
315    }
316
317    fn is_screen_capture_supported(&self) -> bool {
318        true
319    }
320
321    fn screen_capture_sources(
322        &self,
323    ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
324        let (mut tx, rx) = oneshot::channel();
325        tx.send(Ok(self
326            .screen_capture_sources
327            .borrow()
328            .iter()
329            .map(|source| Rc::new(source.clone()) as Rc<dyn ScreenCaptureSource>)
330            .collect()))
331            .ok();
332        rx
333    }
334
335    fn active_window(&self) -> Option<crate::AnyWindowHandle> {
336        self.active_window
337            .borrow()
338            .as_ref()
339            .map(|window| window.0.lock().handle)
340    }
341
342    fn open_window(
343        &self,
344        handle: AnyWindowHandle,
345        params: WindowParams,
346    ) -> anyhow::Result<Box<dyn crate::PlatformWindow>> {
347        let renderer = self.headless_renderer_factory.as_ref().and_then(|f| f());
348        let window = TestWindow::new(
349            handle,
350            params,
351            self.weak.clone(),
352            self.active_display.clone(),
353            renderer,
354        );
355        Ok(Box::new(window))
356    }
357
358    fn window_appearance(&self) -> WindowAppearance {
359        WindowAppearance::Light
360    }
361
362    fn open_url(&self, url: &str) {
363        *self.opened_url.borrow_mut() = Some(url.to_string())
364    }
365
366    fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {
367        unimplemented!()
368    }
369
370    fn prompt_for_paths(
371        &self,
372        options: crate::PathPromptOptions,
373    ) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
374        let (tx, rx) = oneshot::channel();
375        self.prompts.borrow_mut().paths.push_back((options, tx));
376        rx
377    }
378
379    fn prompt_for_new_path(
380        &self,
381        directory: &std::path::Path,
382        _suggested_name: Option<&str>,
383    ) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
384        let (tx, rx) = oneshot::channel();
385        self.prompts
386            .borrow_mut()
387            .new_path
388            .push_back((directory.to_path_buf(), tx));
389        rx
390    }
391
392    fn can_select_mixed_files_and_dirs(&self) -> bool {
393        true
394    }
395
396    fn reveal_path(&self, _path: &std::path::Path) {
397        unimplemented!()
398    }
399
400    fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
401
402    fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
403        unimplemented!()
404    }
405
406    fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
407    fn set_dock_menu(&self, _menu: Vec<crate::MenuItem>, _keymap: &Keymap) {}
408
409    fn add_recent_document(&self, _paths: &Path) {}
410
411    fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
412
413    fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
414
415    fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
416
417    fn app_path(&self) -> Result<std::path::PathBuf> {
418        unimplemented!()
419    }
420
421    fn path_for_auxiliary_executable(&self, _name: &str) -> Result<std::path::PathBuf> {
422        unimplemented!()
423    }
424
425    fn set_cursor_style(&self, style: crate::CursorStyle) {
426        *self.active_cursor.lock() = style;
427    }
428
429    fn should_auto_hide_scrollbars(&self) -> bool {
430        false
431    }
432
433    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
434        self.current_clipboard_item.lock().clone()
435    }
436
437    fn write_to_clipboard(&self, item: ClipboardItem) {
438        *self.current_clipboard_item.lock() = Some(item);
439    }
440
441    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
442    fn read_from_primary(&self) -> Option<ClipboardItem> {
443        self.current_primary_item.lock().clone()
444    }
445
446    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
447    fn write_to_primary(&self, item: ClipboardItem) {
448        *self.current_primary_item.lock() = Some(item);
449    }
450
451    #[cfg(target_os = "macos")]
452    fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
453        self.current_find_pasteboard_item.lock().clone()
454    }
455
456    #[cfg(target_os = "macos")]
457    fn write_to_find_pasteboard(&self, item: ClipboardItem) {
458        *self.current_find_pasteboard_item.lock() = Some(item);
459    }
460
461    fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
462        Task::ready(Ok(()))
463    }
464
465    fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
466        Task::ready(Ok(None))
467    }
468
469    fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
470        Task::ready(Ok(()))
471    }
472
473    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
474        unimplemented!()
475    }
476
477    fn open_with_system(&self, _path: &Path) {
478        unimplemented!()
479    }
480}
481
482impl TestScreenCaptureSource {
483    /// Create a fake screen capture source, for testing.
484    pub fn new() -> Self {
485        Self {}
486    }
487}
488
489struct TestKeyboardLayout;
490
491impl PlatformKeyboardLayout for TestKeyboardLayout {
492    fn id(&self) -> &str {
493        "zed.keyboard.example"
494    }
495
496    fn name(&self) -> &str {
497        "zed.keyboard.example"
498    }
499}