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