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