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