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