platform.rs

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