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