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