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