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