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