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