platform.rs

  1mod keystroke;
  2#[cfg(target_os = "macos")]
  3mod mac;
  4#[cfg(any(test, feature = "test-support"))]
  5mod test;
  6
  7use crate::{
  8    point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
  9    FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout,
 10    Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene,
 11    SharedString, Size, TaskLabel,
 12};
 13use anyhow::{anyhow, bail};
 14use async_task::Runnable;
 15use futures::channel::oneshot;
 16use parking::Unparker;
 17use seahash::SeaHasher;
 18use serde::{Deserialize, Serialize};
 19use sqlez::bindable::{Bind, Column, StaticColumnCount};
 20use sqlez::statement::Statement;
 21use std::borrow::Cow;
 22use std::hash::{Hash, Hasher};
 23use std::time::Duration;
 24use std::{
 25    any::Any,
 26    fmt::{self, Debug, Display},
 27    ops::Range,
 28    path::{Path, PathBuf},
 29    rc::Rc,
 30    str::FromStr,
 31    sync::Arc,
 32};
 33use uuid::Uuid;
 34
 35pub use keystroke::*;
 36#[cfg(target_os = "macos")]
 37pub use mac::*;
 38#[cfg(any(test, feature = "test-support"))]
 39pub use test::*;
 40pub use time::UtcOffset;
 41
 42#[cfg(target_os = "macos")]
 43pub(crate) fn current_platform() -> Rc<dyn Platform> {
 44    Rc::new(MacPlatform::new())
 45}
 46
 47pub trait Platform: 'static {
 48    fn background_executor(&self) -> BackgroundExecutor;
 49    fn foreground_executor(&self) -> ForegroundExecutor;
 50    fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
 51
 52    fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
 53    fn quit(&self);
 54    fn restart(&self);
 55    fn activate(&self, ignoring_other_apps: bool);
 56    fn hide(&self);
 57    fn hide_other_apps(&self);
 58    fn unhide_other_apps(&self);
 59
 60    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
 61    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
 62    fn main_window(&self) -> Option<AnyWindowHandle>;
 63    fn open_window(
 64        &self,
 65        handle: AnyWindowHandle,
 66        options: WindowOptions,
 67    ) -> Box<dyn PlatformWindow>;
 68
 69    fn set_display_link_output_callback(
 70        &self,
 71        display_id: DisplayId,
 72        callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp) + Send>,
 73    );
 74    fn start_display_link(&self, display_id: DisplayId);
 75    fn stop_display_link(&self, display_id: DisplayId);
 76    // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn PlatformWindow>;
 77
 78    fn open_url(&self, url: &str);
 79    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
 80    fn prompt_for_paths(
 81        &self,
 82        options: PathPromptOptions,
 83    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
 84    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
 85    fn reveal_path(&self, path: &Path);
 86
 87    fn on_become_active(&self, callback: Box<dyn FnMut()>);
 88    fn on_resign_active(&self, callback: Box<dyn FnMut()>);
 89    fn on_quit(&self, callback: Box<dyn FnMut()>);
 90    fn on_reopen(&self, callback: Box<dyn FnMut()>);
 91    fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
 92
 93    fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
 94    fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
 95    fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
 96
 97    fn os_name(&self) -> &'static str;
 98    fn os_version(&self) -> Result<SemanticVersion>;
 99    fn app_version(&self) -> Result<SemanticVersion>;
100    fn app_path(&self) -> Result<PathBuf>;
101    fn local_timezone(&self) -> UtcOffset;
102    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
103
104    fn set_cursor_style(&self, style: CursorStyle);
105    fn should_auto_hide_scrollbars(&self) -> bool;
106
107    fn write_to_clipboard(&self, item: ClipboardItem);
108    fn read_from_clipboard(&self) -> Option<ClipboardItem>;
109
110    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
111    fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
112    fn delete_credentials(&self, url: &str) -> Result<()>;
113}
114
115pub trait PlatformDisplay: Send + Sync + Debug {
116    fn id(&self) -> DisplayId;
117    /// Returns a stable identifier for this display that can be persisted and used
118    /// across system restarts.
119    fn uuid(&self) -> Result<Uuid>;
120    fn as_any(&self) -> &dyn Any;
121    fn bounds(&self) -> Bounds<GlobalPixels>;
122}
123
124#[derive(PartialEq, Eq, Hash, Copy, Clone)]
125pub struct DisplayId(pub(crate) u32);
126
127impl Debug for DisplayId {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        write!(f, "DisplayId({})", self.0)
130    }
131}
132
133unsafe impl Send for DisplayId {}
134
135pub trait PlatformWindow {
136    fn bounds(&self) -> WindowBounds;
137    fn content_size(&self) -> Size<Pixels>;
138    fn scale_factor(&self) -> f32;
139    fn titlebar_height(&self) -> Pixels;
140    fn appearance(&self) -> WindowAppearance;
141    fn display(&self) -> Rc<dyn PlatformDisplay>;
142    fn mouse_position(&self) -> Point<Pixels>;
143    fn as_any_mut(&mut self) -> &mut dyn Any;
144    fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
145    fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
146    fn activate(&self);
147    fn set_title(&mut self, title: &str);
148    fn set_edited(&mut self, edited: bool);
149    fn show_character_palette(&self);
150    fn minimize(&self);
151    fn zoom(&self);
152    fn toggle_full_screen(&self);
153    fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
154    fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
155    fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
156    fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>);
157    fn on_moved(&self, callback: Box<dyn FnMut()>);
158    fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
159    fn on_close(&self, callback: Box<dyn FnOnce()>);
160    fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
161    fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
162    fn draw(&self, scene: Scene);
163
164    fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
165
166    #[cfg(any(test, feature = "test-support"))]
167    fn as_test(&mut self) -> Option<&mut TestWindow> {
168        None
169    }
170}
171
172pub trait PlatformDispatcher: Send + Sync {
173    fn is_main_thread(&self) -> bool;
174    fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
175    fn dispatch_on_main_thread(&self, runnable: Runnable);
176    fn dispatch_after(&self, duration: Duration, runnable: Runnable);
177    fn tick(&self, background_only: bool) -> bool;
178    fn park(&self);
179    fn unparker(&self) -> Unparker;
180
181    #[cfg(any(test, feature = "test-support"))]
182    fn as_test(&self) -> Option<&TestDispatcher> {
183        None
184    }
185}
186
187pub trait PlatformTextSystem: Send + Sync {
188    fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()>;
189    fn all_font_families(&self) -> Vec<String>;
190    fn font_id(&self, descriptor: &Font) -> Result<FontId>;
191    fn font_metrics(&self, font_id: FontId) -> FontMetrics;
192    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
193    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
194    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
195    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
196    fn rasterize_glyph(
197        &self,
198        params: &RenderGlyphParams,
199        raster_bounds: Bounds<DevicePixels>,
200    ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
201    fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
202    fn wrap_line(
203        &self,
204        text: &str,
205        font_id: FontId,
206        font_size: Pixels,
207        width: Pixels,
208    ) -> Vec<usize>;
209}
210
211#[derive(Clone, Debug)]
212pub struct AppMetadata {
213    pub os_name: &'static str,
214    pub os_version: Option<SemanticVersion>,
215    pub app_version: Option<SemanticVersion>,
216}
217
218#[derive(PartialEq, Eq, Hash, Clone)]
219pub enum AtlasKey {
220    Glyph(RenderGlyphParams),
221    Svg(RenderSvgParams),
222    Image(RenderImageParams),
223}
224
225impl AtlasKey {
226    pub(crate) fn texture_kind(&self) -> AtlasTextureKind {
227        match self {
228            AtlasKey::Glyph(params) => {
229                if params.is_emoji {
230                    AtlasTextureKind::Polychrome
231                } else {
232                    AtlasTextureKind::Monochrome
233                }
234            }
235            AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
236            AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
237        }
238    }
239}
240
241impl From<RenderGlyphParams> for AtlasKey {
242    fn from(params: RenderGlyphParams) -> Self {
243        Self::Glyph(params)
244    }
245}
246
247impl From<RenderSvgParams> for AtlasKey {
248    fn from(params: RenderSvgParams) -> Self {
249        Self::Svg(params)
250    }
251}
252
253impl From<RenderImageParams> for AtlasKey {
254    fn from(params: RenderImageParams) -> Self {
255        Self::Image(params)
256    }
257}
258
259pub trait PlatformAtlas: Send + Sync {
260    fn get_or_insert_with<'a>(
261        &self,
262        key: &AtlasKey,
263        build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
264    ) -> Result<AtlasTile>;
265
266    fn clear(&self);
267}
268
269#[derive(Clone, Debug, PartialEq, Eq)]
270#[repr(C)]
271pub struct AtlasTile {
272    pub(crate) texture_id: AtlasTextureId,
273    pub(crate) tile_id: TileId,
274    pub(crate) bounds: Bounds<DevicePixels>,
275}
276
277#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
278#[repr(C)]
279pub(crate) struct AtlasTextureId {
280    // We use u32 instead of usize for Metal Shader Language compatibility
281    pub(crate) index: u32,
282    pub(crate) kind: AtlasTextureKind,
283}
284
285#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
286#[repr(C)]
287pub(crate) enum AtlasTextureKind {
288    Monochrome = 0,
289    Polychrome = 1,
290    Path = 2,
291}
292
293#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
294#[repr(C)]
295pub(crate) struct TileId(pub(crate) u32);
296
297impl From<etagere::AllocId> for TileId {
298    fn from(id: etagere::AllocId) -> Self {
299        Self(id.serialize())
300    }
301}
302
303impl From<TileId> for etagere::AllocId {
304    fn from(id: TileId) -> Self {
305        Self::deserialize(id.0)
306    }
307}
308
309pub trait PlatformInputHandler: 'static {
310    fn selected_text_range(&mut self) -> Option<Range<usize>>;
311    fn marked_text_range(&mut self) -> Option<Range<usize>>;
312    fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String>;
313    fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str);
314    fn replace_and_mark_text_in_range(
315        &mut self,
316        range_utf16: Option<Range<usize>>,
317        new_text: &str,
318        new_selected_range: Option<Range<usize>>,
319    );
320    fn unmark_text(&mut self);
321    fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>>;
322}
323
324#[derive(Debug)]
325pub struct WindowOptions {
326    pub bounds: WindowBounds,
327    pub titlebar: Option<TitlebarOptions>,
328    pub center: bool,
329    pub focus: bool,
330    pub show: bool,
331    pub kind: WindowKind,
332    pub is_movable: bool,
333    pub display_id: Option<DisplayId>,
334}
335
336impl Default for WindowOptions {
337    fn default() -> Self {
338        Self {
339            bounds: WindowBounds::default(),
340            titlebar: Some(TitlebarOptions {
341                title: Default::default(),
342                appears_transparent: Default::default(),
343                traffic_light_position: Default::default(),
344            }),
345            center: false,
346            focus: true,
347            show: true,
348            kind: WindowKind::Normal,
349            is_movable: true,
350            display_id: None,
351        }
352    }
353}
354
355#[derive(Debug, Default)]
356pub struct TitlebarOptions {
357    pub title: Option<SharedString>,
358    pub appears_transparent: bool,
359    pub traffic_light_position: Option<Point<Pixels>>,
360}
361
362#[derive(Copy, Clone, Debug)]
363pub enum Appearance {
364    Light,
365    VibrantLight,
366    Dark,
367    VibrantDark,
368}
369
370impl Default for Appearance {
371    fn default() -> Self {
372        Self::Light
373    }
374}
375
376#[derive(Copy, Clone, Debug, PartialEq, Eq)]
377pub enum WindowKind {
378    Normal,
379    PopUp,
380}
381
382#[derive(Copy, Clone, Debug, PartialEq, Default)]
383pub enum WindowBounds {
384    Fullscreen,
385    #[default]
386    Maximized,
387    Fixed(Bounds<GlobalPixels>),
388}
389
390impl StaticColumnCount for WindowBounds {
391    fn column_count() -> usize {
392        5
393    }
394}
395
396impl Bind for WindowBounds {
397    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
398        let (region, next_index) = match self {
399            WindowBounds::Fullscreen => {
400                let next_index = statement.bind(&"Fullscreen", start_index)?;
401                (None, next_index)
402            }
403            WindowBounds::Maximized => {
404                let next_index = statement.bind(&"Maximized", start_index)?;
405                (None, next_index)
406            }
407            WindowBounds::Fixed(region) => {
408                let next_index = statement.bind(&"Fixed", start_index)?;
409                (Some(*region), next_index)
410            }
411        };
412
413        statement.bind(
414            &region.map(|region| {
415                (
416                    region.origin.x,
417                    region.origin.y,
418                    region.size.width,
419                    region.size.height,
420                )
421            }),
422            next_index,
423        )
424    }
425}
426
427impl Column for WindowBounds {
428    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
429        let (window_state, next_index) = String::column(statement, start_index)?;
430        let bounds = match window_state.as_str() {
431            "Fullscreen" => WindowBounds::Fullscreen,
432            "Maximized" => WindowBounds::Maximized,
433            "Fixed" => {
434                let ((x, y, width, height), _) = Column::column(statement, next_index)?;
435                let x: f64 = x;
436                let y: f64 = y;
437                let width: f64 = width;
438                let height: f64 = height;
439                WindowBounds::Fixed(Bounds {
440                    origin: point(x.into(), y.into()),
441                    size: size(width.into(), height.into()),
442                })
443            }
444            _ => bail!("Window State did not have a valid string"),
445        };
446
447        Ok((bounds, next_index + 4))
448    }
449}
450
451#[derive(Copy, Clone, Debug)]
452pub enum WindowAppearance {
453    Light,
454    VibrantLight,
455    Dark,
456    VibrantDark,
457}
458
459impl Default for WindowAppearance {
460    fn default() -> Self {
461        Self::Light
462    }
463}
464
465#[derive(Copy, Clone, Debug)]
466pub struct PathPromptOptions {
467    pub files: bool,
468    pub directories: bool,
469    pub multiple: bool,
470}
471
472#[derive(Copy, Clone, Debug)]
473pub enum PromptLevel {
474    Info,
475    Warning,
476    Critical,
477}
478
479/// The style of the cursor (pointer)
480#[derive(Copy, Clone, Debug)]
481pub enum CursorStyle {
482    Arrow,
483    IBeam,
484    Crosshair,
485    ClosedHand,
486    OpenHand,
487    PointingHand,
488    ResizeLeft,
489    ResizeRight,
490    ResizeLeftRight,
491    ResizeUp,
492    ResizeDown,
493    ResizeUpDown,
494    DisappearingItem,
495    IBeamCursorForVerticalLayout,
496    OperationNotAllowed,
497    DragLink,
498    DragCopy,
499    ContextualMenu,
500}
501
502impl Default for CursorStyle {
503    fn default() -> Self {
504        Self::Arrow
505    }
506}
507
508#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
509pub struct SemanticVersion {
510    major: usize,
511    minor: usize,
512    patch: usize,
513}
514
515impl FromStr for SemanticVersion {
516    type Err = anyhow::Error;
517
518    fn from_str(s: &str) -> Result<Self> {
519        let mut components = s.trim().split('.');
520        let major = components
521            .next()
522            .ok_or_else(|| anyhow!("missing major version number"))?
523            .parse()?;
524        let minor = components
525            .next()
526            .ok_or_else(|| anyhow!("missing minor version number"))?
527            .parse()?;
528        let patch = components
529            .next()
530            .ok_or_else(|| anyhow!("missing patch version number"))?
531            .parse()?;
532        Ok(Self {
533            major,
534            minor,
535            patch,
536        })
537    }
538}
539
540impl Display for SemanticVersion {
541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
543    }
544}
545
546#[derive(Clone, Debug, Eq, PartialEq)]
547pub struct ClipboardItem {
548    pub(crate) text: String,
549    pub(crate) metadata: Option<String>,
550}
551
552impl ClipboardItem {
553    pub fn new(text: String) -> Self {
554        Self {
555            text,
556            metadata: None,
557        }
558    }
559
560    pub fn with_metadata<T: Serialize>(mut self, metadata: T) -> Self {
561        self.metadata = Some(serde_json::to_string(&metadata).unwrap());
562        self
563    }
564
565    pub fn text(&self) -> &String {
566        &self.text
567    }
568
569    pub fn metadata<T>(&self) -> Option<T>
570    where
571        T: for<'a> Deserialize<'a>,
572    {
573        self.metadata
574            .as_ref()
575            .and_then(|m| serde_json::from_str(m).ok())
576    }
577
578    pub(crate) fn text_hash(text: &str) -> u64 {
579        let mut hasher = SeaHasher::new();
580        text.hash(&mut hasher);
581        hasher.finish()
582    }
583}