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