platform.rs

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