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