From 83dae46ec6d646dca233dc770ca3dfdb3121122d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 20 Sep 2023 10:17:29 -0600 Subject: [PATCH] Checkpoint --- Cargo.lock | 44 +- crates/gpui/src/platform/mac/platform.rs | 6 +- crates/gpui3/Cargo.toml | 1 + crates/gpui3/src/app.rs | 21 +- crates/gpui3/src/executor.rs | 2 +- crates/gpui3/src/fonts.rs | 6 +- crates/gpui3/src/geometry.rs | 6 + crates/gpui3/src/gpui3.rs | 1 + crates/gpui3/src/platform.rs | 189 +- crates/gpui3/src/platform/mac.rs | 18 +- crates/gpui3/src/platform/mac/open_type.rs | 395 ++++ crates/gpui3/src/platform/mac/platform.rs | 2232 +++++++++--------- crates/gpui3/src/platform/mac/text_system.rs | 757 ++++++ crates/gpui3/src/platform/test.rs | 142 +- crates/gpui3/src/scene.rs | 32 +- crates/gpui3/src/text.rs | 36 +- crates/storybook/Cargo.toml | 6 - 17 files changed, 2701 insertions(+), 1193 deletions(-) create mode 100644 crates/gpui3/src/platform/mac/open_type.rs create mode 100644 crates/gpui3/src/platform/mac/text_system.rs diff --git a/Cargo.lock b/Cargo.lock index 5ea28ebacd..090ab493bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -983,7 +983,7 @@ dependencies = [ "collections", "editor", "gpui", - "itertools 0.10.5", + "itertools", "language", "outline", "project", @@ -1960,7 +1960,7 @@ dependencies = [ "cranelift-codegen", "cranelift-entity", "cranelift-frontend", - "itertools 0.10.5", + "itertools", "log", "smallvec", "wasmparser", @@ -2392,7 +2392,7 @@ dependencies = [ "git", "gpui", "indoc", - "itertools 0.10.5", + "itertools", "language", "lazy_static", "log", @@ -3273,7 +3273,7 @@ dependencies = [ "futures 0.3.28", "gpui_macros", "image", - "itertools 0.10.5", + "itertools", "lazy_static", "log", "media", @@ -3365,7 +3365,7 @@ dependencies = [ "futures 0.3.28", "gpui_macros", "image", - "itertools 0.10.5", + "itertools", "lazy_static", "log", "media", @@ -3374,6 +3374,7 @@ dependencies = [ "ordered-float", "parking", "parking_lot 0.11.2", + "pathfinder_geometry", "plane-split", "png", "postage", @@ -3971,15 +3972,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.9" @@ -5772,7 +5764,7 @@ dependencies = [ "globset", "gpui", "ignore", - "itertools 0.10.5", + "itertools", "language", "lazy_static", "log", @@ -5896,7 +5888,7 @@ checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ "bytes 1.4.0", "heck 0.3.3", - "itertools 0.10.5", + "itertools", "lazy_static", "log", "multimap", @@ -5915,7 +5907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "600d2f334aa05acb02a755e217ef1ab6dea4d51b58b7846588b747edec04efba" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools", "proc-macro2", "quote", "syn 1.0.109", @@ -5928,7 +5920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools", "proc-macro2", "quote", "syn 1.0.109", @@ -7568,7 +7560,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" dependencies = [ - "itertools 0.10.5", + "itertools", "nom", "unicode_categories", ] @@ -7692,22 +7684,16 @@ name = "storybook" version = "0.1.0" dependencies = [ "anyhow", - "bytemuck", "derive_more", "gpui2", - "itertools 0.11.0", "log", - "plane-split", - "raw-window-handle", "refineable", "rust-embed", "serde", "settings", "simplelog", - "slotmap", "theme", "util", - "wgpu", ] [[package]] @@ -7977,7 +7963,7 @@ dependencies = [ "dirs 4.0.0", "futures 0.3.28", "gpui", - "itertools 0.10.5", + "itertools", "lazy_static", "libc", "mio-extras", @@ -8008,7 +7994,7 @@ dependencies = [ "editor", "futures 0.3.28", "gpui", - "itertools 0.10.5", + "itertools", "language", "lazy_static", "libc", @@ -9190,7 +9176,7 @@ dependencies = [ "futures 0.3.28", "gpui", "indoc", - "itertools 0.10.5", + "itertools", "language", "language_selector", "log", @@ -10137,7 +10123,7 @@ dependencies = [ "gpui", "indoc", "install_cli", - "itertools 0.10.5", + "itertools", "language", "lazy_static", "log", diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 656f2e4475..2f85b23fc9 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1,6 +1,6 @@ use super::{ event::key_to_native, screen::Screen, status_item::StatusItem, BoolExt as _, Dispatcher, - FontSystem, MacWindow, + MacWindow, TextSystem, }; use crate::{ executor, @@ -488,7 +488,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { pub struct MacPlatform { dispatcher: Arc, - fonts: Arc, + fonts: Arc, pasteboard: id, text_hash_pasteboard_type: id, metadata_pasteboard_type: id, @@ -498,7 +498,7 @@ impl MacPlatform { pub fn new() -> Self { Self { dispatcher: Arc::new(Dispatcher), - fonts: Arc::new(FontSystem::new()), + fonts: Arc::new(TextSystem::new()), pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) }, text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") }, metadata_pasteboard_type: unsafe { ns_string("zed-metadata") }, diff --git a/crates/gpui3/Cargo.toml b/crates/gpui3/Cargo.toml index 7d7450c096..339b5b76fd 100644 --- a/crates/gpui3/Cargo.toml +++ b/crates/gpui3/Cargo.toml @@ -35,6 +35,7 @@ num_cpus = "1.13" ordered-float.workspace = true parking = "2.0.0" parking_lot.workspace = true +pathfinder_geometry = "0.5" postage.workspace = true rand.workspace = true refineable.workspace = true diff --git a/crates/gpui3/src/app.rs b/crates/gpui3/src/app.rs index a80564423d..8ce158f106 100644 --- a/crates/gpui3/src/app.rs +++ b/crates/gpui3/src/app.rs @@ -1,14 +1,19 @@ +use crate::{ + Context, FontCache, LayoutId, Platform, Reference, View, Window, WindowContext, WindowHandle, + WindowId, +}; use anyhow::{anyhow, Result}; use slotmap::SlotMap; -use std::{any::Any, marker::PhantomData, rc::Rc, sync::Arc}; +use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc, sync::Arc}; -use crate::FontCache; +#[derive(Clone)] +pub struct App(Rc>); -use super::{ - platform::Platform, - window::{Window, WindowHandle, WindowId}, - Context, LayoutId, Reference, View, WindowContext, -}; +impl App { + pub fn new(platform: Rc) -> Self { + Self(Rc::new(RefCell::new(AppContext::new(platform)))) + } +} pub struct AppContext { platform: Rc, @@ -21,7 +26,7 @@ pub struct AppContext { impl AppContext { pub fn new(platform: Rc) -> Self { - let font_cache = Arc::new(FontCache::new(platform.font_system())); + let font_cache = Arc::new(FontCache::new(platform.text_system())); AppContext { platform, font_cache, diff --git a/crates/gpui3/src/executor.rs b/crates/gpui3/src/executor.rs index f465bd3c3d..253ddf5e36 100644 --- a/crates/gpui3/src/executor.rs +++ b/crates/gpui3/src/executor.rs @@ -619,7 +619,7 @@ impl ExecutorEvent { } impl ForegroundExecutor { - pub fn platform(dispatcher: Arc) -> Result { + pub fn new(dispatcher: Arc) -> Result { if dispatcher.is_main_thread() { Ok(Self::Platform { dispatcher, diff --git a/crates/gpui3/src/fonts.rs b/crates/gpui3/src/fonts.rs index 36d29c7eea..9a3c579f8c 100644 --- a/crates/gpui3/src/fonts.rs +++ b/crates/gpui3/src/fonts.rs @@ -245,7 +245,7 @@ impl FontCache { .typographic_bounds(font_id, glyph_id) .unwrap(); } - bounds.size.width * self.em_size(font_id, font_size) + self.em_size(font_id, font_size) * bounds.size.width } pub fn em_advance(&self, font_id: FontId, font_size: Pixels) -> Pixels { @@ -256,7 +256,7 @@ impl FontCache { glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap(); advance = state.font_system.advance(font_id, glyph_id).unwrap(); } - advance.x * self.em_size(font_id, font_size) + self.em_size(font_id, font_size) * advance.width } pub fn line_height(&self, font_size: Pixels) -> Pixels { @@ -356,7 +356,7 @@ mod tests { #[test] fn test_select_font() { let platform = TestPlatform::new(); - let fonts = FontCache::new(platform.font_system()); + let fonts = FontCache::new(platform.text_system()); let arial = fonts .load_family( &["Arial"], diff --git a/crates/gpui3/src/geometry.rs b/crates/gpui3/src/geometry.rs index ebc7120cca..b226ea3be7 100644 --- a/crates/gpui3/src/geometry.rs +++ b/crates/gpui3/src/geometry.rs @@ -246,6 +246,12 @@ impl std::hash::Hash for Pixels { } } +impl From for Pixels { + fn from(val: f64) -> Self { + Pixels(val as f32) + } +} + unsafe impl bytemuck::Pod for Pixels {} unsafe impl bytemuck::Zeroable for Pixels {} diff --git a/crates/gpui3/src/gpui3.rs b/crates/gpui3/src/gpui3.rs index fd2f97f51a..2c665cc334 100644 --- a/crates/gpui3/src/gpui3.rs +++ b/crates/gpui3/src/gpui3.rs @@ -34,6 +34,7 @@ pub use styled::*; pub use taffy::LayoutId; use taffy::TaffyLayoutEngine; use text::*; +pub use text::{Glyph, GlyphId}; pub use util::arc_cow::ArcCow; pub use window::*; diff --git a/crates/gpui3/src/platform.rs b/crates/gpui3/src/platform.rs index 5298a8fe49..93a8f577bd 100644 --- a/crates/gpui3/src/platform.rs +++ b/crates/gpui3/src/platform.rs @@ -6,13 +6,26 @@ mod mac; mod test; use crate::{ - AnyWindowHandle, Bounds, FontFeatures, FontId, FontMetrics, FontStyle, FontWeight, GlyphId, - LineLayout, Pixels, Point, RunStyle, SharedString, Size, + AnyWindowHandle, Bounds, FontFeatures, FontId, FontMetrics, FontStyle, FontWeight, + ForegroundExecutor, GlyphId, LineLayout, Pixels, Point, Result, RunStyle, SharedString, Size, }; +use anyhow::anyhow; use async_task::Runnable; use futures::channel::oneshot; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; -use std::{any::Any, fmt::Debug, ops::Range, rc::Rc, sync::Arc}; +use seahash::SeaHasher; +use serde::{Deserialize, Serialize}; +use std::hash::{Hash, Hasher}; +use std::{ + any::Any, + fmt::{self, Debug, Display}, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + str::FromStr, + sync::Arc, +}; +pub use time::UtcOffset; use uuid::Uuid; pub use events::*; @@ -22,15 +35,64 @@ pub use mac::*; #[cfg(any(test, feature = "test"))] pub use test::*; -pub trait Platform { - fn dispatcher(&self) -> Arc; - fn font_system(&self) -> Arc; +// #[cfg(target_os = "macos")] +// pub fn current() -> Rc { +// MacPlatform +// } +pub trait Platform { + fn executor(&self) -> Rc; + fn text_system(&self) -> Arc; + + fn run(&self, on_finish_launching: Box); + fn quit(&self); + fn restart(&self); + fn activate(&self, ignoring_other_apps: bool); + fn hide(&self); + fn hide_other_apps(&self); + fn unhide_other_apps(&self); + + fn screens(&self) -> Vec>; + fn screen_by_id(&self, id: uuid::Uuid) -> Option>; + fn main_window(&self) -> Option; fn open_window( &self, handle: AnyWindowHandle, options: WindowOptions, ) -> Box; + // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box; + + fn open_url(&self, url: &str); + fn on_open_urls(&self, callback: Box)>); + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>>; + fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>; + fn reveal_path(&self, path: &Path); + + fn on_become_active(&self, callback: Box); + fn on_resign_active(&self, callback: Box); + fn on_quit(&self, callback: Box); + fn on_reopen(&self, callback: Box); + fn on_event(&self, callback: Box bool>); + + fn os_name(&self) -> &'static str; + fn os_version(&self) -> Result; + fn app_version(&self) -> Result; + fn app_path(&self) -> Result; + fn local_timezone(&self) -> UtcOffset; + fn path_for_auxiliary_executable(&self, name: &str) -> Result; + + fn set_cursor_style(&self, style: CursorStyle); + fn should_auto_hide_scrollbars(&self) -> bool; + + fn write_to_clipboard(&self, item: ClipboardItem); + fn read_from_clipboard(&self) -> Option; + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>; + fn read_credentials(&self, url: &str) -> Result)>>; + fn delete_credentials(&self, url: &str) -> Result<()>; } pub trait PlatformScreen: Debug { @@ -90,12 +152,9 @@ pub trait PlatformTextSystem: Send + Sync { style: FontStyle, ) -> anyhow::Result; fn font_metrics(&self, font_id: FontId) -> FontMetrics; - fn typographic_bounds( - &self, - font_id: FontId, - glyph_id: GlyphId, - ) -> anyhow::Result>; - fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result>; + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) + -> anyhow::Result>; + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result>; fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; fn rasterize_glyph( &self, @@ -224,3 +283,109 @@ pub enum WindowPromptLevel { Warning, Critical, } + +#[derive(Copy, Clone, Debug)] +pub struct PathPromptOptions { + pub files: bool, + pub directories: bool, + pub multiple: bool, +} + +#[derive(Copy, Clone, Debug)] +pub enum PromptLevel { + Info, + Warning, + Critical, +} + +#[derive(Copy, Clone, Debug)] +pub enum CursorStyle { + Arrow, + ResizeLeftRight, + ResizeUpDown, + PointingHand, + IBeam, +} + +impl Default for CursorStyle { + fn default() -> Self { + Self::Arrow + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct SemanticVersion { + major: usize, + minor: usize, + patch: usize, +} + +impl FromStr for SemanticVersion { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut components = s.trim().split('.'); + let major = components + .next() + .ok_or_else(|| anyhow!("missing major version number"))? + .parse()?; + let minor = components + .next() + .ok_or_else(|| anyhow!("missing minor version number"))? + .parse()?; + let patch = components + .next() + .ok_or_else(|| anyhow!("missing patch version number"))? + .parse()?; + Ok(Self { + major, + minor, + patch, + }) + } +} + +impl Display for SemanticVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardItem { + pub(crate) text: String, + pub(crate) metadata: Option, +} + +impl ClipboardItem { + pub fn new(text: String) -> Self { + Self { + text, + metadata: None, + } + } + + pub fn with_metadata(mut self, metadata: T) -> Self { + self.metadata = Some(serde_json::to_string(&metadata).unwrap()); + self + } + + pub fn text(&self) -> &String { + &self.text + } + + pub fn metadata(&self) -> Option + where + T: for<'a> Deserialize<'a>, + { + self.metadata + .as_ref() + .and_then(|m| serde_json::from_str(m).ok()) + } + + pub(crate) fn text_hash(text: &str) -> u64 { + let mut hasher = SeaHasher::new(); + text.hash(&mut hasher); + hasher.finish() + } +} diff --git a/crates/gpui3/src/platform/mac.rs b/crates/gpui3/src/platform/mac.rs index 7a18255435..e44cf1a03b 100644 --- a/crates/gpui3/src/platform/mac.rs +++ b/crates/gpui3/src/platform/mac.rs @@ -2,18 +2,13 @@ ///! an origin at the bottom left of the main display. mod dispatcher; mod events; +mod open_type; mod platform; mod screen; +mod text_system; mod window; mod window_appearence; -use std::{ - ffi::{c_char, CStr, OsStr}, - ops::Range, - os::unix::prelude::OsStrExt, - path::PathBuf, -}; - use crate::{px, size, Pixels, Size}; use anyhow::anyhow; use cocoa::{ @@ -25,8 +20,17 @@ use objc::{ runtime::{BOOL, NO, YES}, sel, sel_impl, }; +use std::{ + ffi::{c_char, CStr, OsStr}, + ops::Range, + os::unix::prelude::OsStrExt, + path::PathBuf, +}; + +pub use dispatcher::*; pub use platform::*; pub use screen::*; +pub use text_system::*; pub use window::*; trait BoolExt { diff --git a/crates/gpui3/src/platform/mac/open_type.rs b/crates/gpui3/src/platform/mac/open_type.rs new file mode 100644 index 0000000000..9da38a28ac --- /dev/null +++ b/crates/gpui3/src/platform/mac/open_type.rs @@ -0,0 +1,395 @@ +#![allow(unused, non_upper_case_globals)] + +use std::ptr; + +use crate::FontFeatures; +use cocoa::appkit::CGFloat; +use core_foundation::{base::TCFType, number::CFNumber}; +use core_graphics::geometry::CGAffineTransform; +use core_text::{ + font::{CTFont, CTFontRef}, + font_descriptor::{ + CTFontDescriptor, CTFontDescriptorCreateCopyWithFeature, CTFontDescriptorRef, + }, +}; +use font_kit::font::Font; + +const kCaseSensitiveLayoutOffSelector: i32 = 1; +const kCaseSensitiveLayoutOnSelector: i32 = 0; +const kCaseSensitiveLayoutType: i32 = 33; +const kCaseSensitiveSpacingOffSelector: i32 = 3; +const kCaseSensitiveSpacingOnSelector: i32 = 2; +const kCharacterAlternativesType: i32 = 17; +const kCommonLigaturesOffSelector: i32 = 3; +const kCommonLigaturesOnSelector: i32 = 2; +const kContextualAlternatesOffSelector: i32 = 1; +const kContextualAlternatesOnSelector: i32 = 0; +const kContextualAlternatesType: i32 = 36; +const kContextualLigaturesOffSelector: i32 = 19; +const kContextualLigaturesOnSelector: i32 = 18; +const kContextualSwashAlternatesOffSelector: i32 = 5; +const kContextualSwashAlternatesOnSelector: i32 = 4; +const kDefaultLowerCaseSelector: i32 = 0; +const kDefaultUpperCaseSelector: i32 = 0; +const kDiagonalFractionsSelector: i32 = 2; +const kFractionsType: i32 = 11; +const kHistoricalLigaturesOffSelector: i32 = 21; +const kHistoricalLigaturesOnSelector: i32 = 20; +const kHojoCharactersSelector: i32 = 12; +const kInferiorsSelector: i32 = 2; +const kJIS2004CharactersSelector: i32 = 11; +const kLigaturesType: i32 = 1; +const kLowerCasePetiteCapsSelector: i32 = 2; +const kLowerCaseSmallCapsSelector: i32 = 1; +const kLowerCaseType: i32 = 37; +const kLowerCaseNumbersSelector: i32 = 0; +const kMathematicalGreekOffSelector: i32 = 11; +const kMathematicalGreekOnSelector: i32 = 10; +const kMonospacedNumbersSelector: i32 = 0; +const kNLCCharactersSelector: i32 = 13; +const kNoFractionsSelector: i32 = 0; +const kNormalPositionSelector: i32 = 0; +const kNoStyleOptionsSelector: i32 = 0; +const kNumberCaseType: i32 = 21; +const kNumberSpacingType: i32 = 6; +const kOrdinalsSelector: i32 = 3; +const kProportionalNumbersSelector: i32 = 1; +const kQuarterWidthTextSelector: i32 = 4; +const kScientificInferiorsSelector: i32 = 4; +const kSlashedZeroOffSelector: i32 = 5; +const kSlashedZeroOnSelector: i32 = 4; +const kStyleOptionsType: i32 = 19; +const kStylisticAltEighteenOffSelector: i32 = 37; +const kStylisticAltEighteenOnSelector: i32 = 36; +const kStylisticAltEightOffSelector: i32 = 17; +const kStylisticAltEightOnSelector: i32 = 16; +const kStylisticAltElevenOffSelector: i32 = 23; +const kStylisticAltElevenOnSelector: i32 = 22; +const kStylisticAlternativesType: i32 = 35; +const kStylisticAltFifteenOffSelector: i32 = 31; +const kStylisticAltFifteenOnSelector: i32 = 30; +const kStylisticAltFiveOffSelector: i32 = 11; +const kStylisticAltFiveOnSelector: i32 = 10; +const kStylisticAltFourOffSelector: i32 = 9; +const kStylisticAltFourOnSelector: i32 = 8; +const kStylisticAltFourteenOffSelector: i32 = 29; +const kStylisticAltFourteenOnSelector: i32 = 28; +const kStylisticAltNineOffSelector: i32 = 19; +const kStylisticAltNineOnSelector: i32 = 18; +const kStylisticAltNineteenOffSelector: i32 = 39; +const kStylisticAltNineteenOnSelector: i32 = 38; +const kStylisticAltOneOffSelector: i32 = 3; +const kStylisticAltOneOnSelector: i32 = 2; +const kStylisticAltSevenOffSelector: i32 = 15; +const kStylisticAltSevenOnSelector: i32 = 14; +const kStylisticAltSeventeenOffSelector: i32 = 35; +const kStylisticAltSeventeenOnSelector: i32 = 34; +const kStylisticAltSixOffSelector: i32 = 13; +const kStylisticAltSixOnSelector: i32 = 12; +const kStylisticAltSixteenOffSelector: i32 = 33; +const kStylisticAltSixteenOnSelector: i32 = 32; +const kStylisticAltTenOffSelector: i32 = 21; +const kStylisticAltTenOnSelector: i32 = 20; +const kStylisticAltThirteenOffSelector: i32 = 27; +const kStylisticAltThirteenOnSelector: i32 = 26; +const kStylisticAltThreeOffSelector: i32 = 7; +const kStylisticAltThreeOnSelector: i32 = 6; +const kStylisticAltTwelveOffSelector: i32 = 25; +const kStylisticAltTwelveOnSelector: i32 = 24; +const kStylisticAltTwentyOffSelector: i32 = 41; +const kStylisticAltTwentyOnSelector: i32 = 40; +const kStylisticAltTwoOffSelector: i32 = 5; +const kStylisticAltTwoOnSelector: i32 = 4; +const kSuperiorsSelector: i32 = 1; +const kSwashAlternatesOffSelector: i32 = 3; +const kSwashAlternatesOnSelector: i32 = 2; +const kTitlingCapsSelector: i32 = 4; +const kTypographicExtrasType: i32 = 14; +const kVerticalFractionsSelector: i32 = 1; +const kVerticalPositionType: i32 = 10; + +pub fn apply_features(font: &mut Font, features: &FontFeatures) { + // See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc + // for a reference implementation. + toggle_open_type_feature( + font, + features.calt, + kContextualAlternatesType, + kContextualAlternatesOnSelector, + kContextualAlternatesOffSelector, + ); + toggle_open_type_feature( + font, + features.case, + kCaseSensitiveLayoutType, + kCaseSensitiveLayoutOnSelector, + kCaseSensitiveLayoutOffSelector, + ); + toggle_open_type_feature( + font, + features.cpsp, + kCaseSensitiveLayoutType, + kCaseSensitiveSpacingOnSelector, + kCaseSensitiveSpacingOffSelector, + ); + toggle_open_type_feature( + font, + features.frac, + kFractionsType, + kDiagonalFractionsSelector, + kNoFractionsSelector, + ); + toggle_open_type_feature( + font, + features.liga, + kLigaturesType, + kCommonLigaturesOnSelector, + kCommonLigaturesOffSelector, + ); + toggle_open_type_feature( + font, + features.onum, + kNumberCaseType, + kLowerCaseNumbersSelector, + 2, + ); + toggle_open_type_feature( + font, + features.ordn, + kVerticalPositionType, + kOrdinalsSelector, + kNormalPositionSelector, + ); + toggle_open_type_feature( + font, + features.pnum, + kNumberSpacingType, + kProportionalNumbersSelector, + 4, + ); + toggle_open_type_feature( + font, + features.ss01, + kStylisticAlternativesType, + kStylisticAltOneOnSelector, + kStylisticAltOneOffSelector, + ); + toggle_open_type_feature( + font, + features.ss02, + kStylisticAlternativesType, + kStylisticAltTwoOnSelector, + kStylisticAltTwoOffSelector, + ); + toggle_open_type_feature( + font, + features.ss03, + kStylisticAlternativesType, + kStylisticAltThreeOnSelector, + kStylisticAltThreeOffSelector, + ); + toggle_open_type_feature( + font, + features.ss04, + kStylisticAlternativesType, + kStylisticAltFourOnSelector, + kStylisticAltFourOffSelector, + ); + toggle_open_type_feature( + font, + features.ss05, + kStylisticAlternativesType, + kStylisticAltFiveOnSelector, + kStylisticAltFiveOffSelector, + ); + toggle_open_type_feature( + font, + features.ss06, + kStylisticAlternativesType, + kStylisticAltSixOnSelector, + kStylisticAltSixOffSelector, + ); + toggle_open_type_feature( + font, + features.ss07, + kStylisticAlternativesType, + kStylisticAltSevenOnSelector, + kStylisticAltSevenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss08, + kStylisticAlternativesType, + kStylisticAltEightOnSelector, + kStylisticAltEightOffSelector, + ); + toggle_open_type_feature( + font, + features.ss09, + kStylisticAlternativesType, + kStylisticAltNineOnSelector, + kStylisticAltNineOffSelector, + ); + toggle_open_type_feature( + font, + features.ss10, + kStylisticAlternativesType, + kStylisticAltTenOnSelector, + kStylisticAltTenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss11, + kStylisticAlternativesType, + kStylisticAltElevenOnSelector, + kStylisticAltElevenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss12, + kStylisticAlternativesType, + kStylisticAltTwelveOnSelector, + kStylisticAltTwelveOffSelector, + ); + toggle_open_type_feature( + font, + features.ss13, + kStylisticAlternativesType, + kStylisticAltThirteenOnSelector, + kStylisticAltThirteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss14, + kStylisticAlternativesType, + kStylisticAltFourteenOnSelector, + kStylisticAltFourteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss15, + kStylisticAlternativesType, + kStylisticAltFifteenOnSelector, + kStylisticAltFifteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss16, + kStylisticAlternativesType, + kStylisticAltSixteenOnSelector, + kStylisticAltSixteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss17, + kStylisticAlternativesType, + kStylisticAltSeventeenOnSelector, + kStylisticAltSeventeenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss18, + kStylisticAlternativesType, + kStylisticAltEighteenOnSelector, + kStylisticAltEighteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss19, + kStylisticAlternativesType, + kStylisticAltNineteenOnSelector, + kStylisticAltNineteenOffSelector, + ); + toggle_open_type_feature( + font, + features.ss20, + kStylisticAlternativesType, + kStylisticAltTwentyOnSelector, + kStylisticAltTwentyOffSelector, + ); + toggle_open_type_feature( + font, + features.subs, + kVerticalPositionType, + kInferiorsSelector, + kNormalPositionSelector, + ); + toggle_open_type_feature( + font, + features.sups, + kVerticalPositionType, + kSuperiorsSelector, + kNormalPositionSelector, + ); + toggle_open_type_feature( + font, + features.swsh, + kContextualAlternatesType, + kSwashAlternatesOnSelector, + kSwashAlternatesOffSelector, + ); + toggle_open_type_feature( + font, + features.titl, + kStyleOptionsType, + kTitlingCapsSelector, + kNoStyleOptionsSelector, + ); + toggle_open_type_feature( + font, + features.tnum, + kNumberSpacingType, + kMonospacedNumbersSelector, + 4, + ); + toggle_open_type_feature( + font, + features.zero, + kTypographicExtrasType, + kSlashedZeroOnSelector, + kSlashedZeroOffSelector, + ); +} + +fn toggle_open_type_feature( + font: &mut Font, + enabled: Option, + type_identifier: i32, + on_selector_identifier: i32, + off_selector_identifier: i32, +) { + if let Some(enabled) = enabled { + let native_font = font.native_font(); + unsafe { + let selector_identifier = if enabled { + on_selector_identifier + } else { + off_selector_identifier + }; + let new_descriptor = CTFontDescriptorCreateCopyWithFeature( + native_font.copy_descriptor().as_concrete_TypeRef(), + CFNumber::from(type_identifier).as_concrete_TypeRef(), + CFNumber::from(selector_identifier).as_concrete_TypeRef(), + ); + let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor); + let new_font = CTFontCreateCopyWithAttributes( + font.native_font().as_concrete_TypeRef(), + 0.0, + ptr::null(), + new_descriptor.as_concrete_TypeRef(), + ); + let new_font = CTFont::wrap_under_create_rule(new_font); + *font = Font::from_native_font(new_font); + } + } +} + +#[link(name = "CoreText", kind = "framework")] +extern "C" { + fn CTFontCreateCopyWithAttributes( + font: CTFontRef, + size: CGFloat, + matrix: *const CGAffineTransform, + attributes: CTFontDescriptorRef, + ) -> CTFontRef; +} diff --git a/crates/gpui3/src/platform/mac/platform.rs b/crates/gpui3/src/platform/mac/platform.rs index 39a9f70566..490301eafd 100644 --- a/crates/gpui3/src/platform/mac/platform.rs +++ b/crates/gpui3/src/platform/mac/platform.rs @@ -1,1102 +1,1130 @@ -// use anyhow::{anyhow, Result}; -// use block::ConcreteBlock; -// use cocoa::{ -// appkit::{ -// NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, -// NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, -// NSSavePanel, NSWindow, -// }, -// base::{id, nil, selector, BOOL, YES}, -// foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSUInteger, NSURL}, -// }; -// use core_foundation::{ -// base::{CFTypeRef, OSStatus}, -// dictionary::CFDictionaryRef, -// string::CFStringRef, -// }; -// use ctor::ctor; -// use objc::{ -// class, -// declare::ClassDecl, -// msg_send, -// runtime::{Class, Object, Sel}, -// sel, sel_impl, -// }; - -// use postage::oneshot; -// use ptr::null_mut; -// use std::{ -// cell::{Cell, RefCell}, -// convert::TryInto, -// ffi::{c_void, CStr, OsStr}, -// os::{raw::c_char, unix::ffi::OsStrExt}, -// path::{Path, PathBuf}, -// process::Command, -// ptr, -// rc::Rc, -// slice, str, -// sync::Arc, -// }; - -// use crate::Event; - -// #[allow(non_upper_case_globals)] -// const NSUTF8StringEncoding: NSUInteger = 4; - -// const MAC_PLATFORM_IVAR: &str = "platform"; -// static mut APP_CLASS: *const Class = ptr::null(); -// static mut APP_DELEGATE_CLASS: *const Class = ptr::null(); - -// #[ctor] -// unsafe fn build_classes() { -// APP_CLASS = { -// let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap(); -// decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); -// decl.add_method( -// sel!(sendEvent:), -// send_event as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.register() -// }; - -// APP_DELEGATE_CLASS = { -// let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap(); -// decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); -// decl.add_method( -// sel!(applicationDidFinishLaunching:), -// did_finish_launching as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.add_method( -// sel!(applicationShouldHandleReopen:hasVisibleWindows:), -// should_handle_reopen as extern "C" fn(&mut Object, Sel, id, bool), -// ); -// decl.add_method( -// sel!(applicationDidBecomeActive:), -// did_become_active as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.add_method( -// sel!(applicationDidResignActive:), -// did_resign_active as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.add_method( -// sel!(applicationWillTerminate:), -// will_terminate as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.add_method( -// sel!(handleGPUIMenuItem:), -// handle_menu_item as extern "C" fn(&mut Object, Sel, id), -// ); -// // Add menu item handlers so that OS save panels have the correct key commands -// decl.add_method( -// sel!(cut:), -// handle_menu_item as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.add_method( -// sel!(copy:), -// handle_menu_item as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.add_method( -// sel!(paste:), -// handle_menu_item as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.add_method( -// sel!(selectAll:), -// handle_menu_item as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.add_method( -// sel!(undo:), -// handle_menu_item as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.add_method( -// sel!(redo:), -// handle_menu_item as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.add_method( -// sel!(validateMenuItem:), -// validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool, -// ); -// decl.add_method( -// sel!(menuWillOpen:), -// menu_will_open as extern "C" fn(&mut Object, Sel, id), -// ); -// decl.add_method( -// sel!(application:openURLs:), -// open_urls as extern "C" fn(&mut Object, Sel, id, id), -// ); -// decl.register() -// } -// } - -// pub struct MacForegroundPlatformState { -// dispatcher: Arc, -// fonts: Arc, -// pasteboard: id, -// text_hash_pasteboard_type: id, -// metadata_pasteboard_type: id, -// become_active: Option>, -// resign_active: Option>, -// reopen: Option>, -// quit: Option>, -// event: Option bool>>, -// // menu_command: Option>, -// validate_menu_command: Option bool>>, -// will_open_menu: Option>, -// open_urls: Option)>>, -// finish_launching: Option>, -// // menu_actions: Vec>, -// // foreground: Rc, -// } - -// impl MacForegroundPlatform { -// pub fn new(foreground: Rc) -> Self { -// Self(RefCell::new(MacForegroundPlatformState { -// become_active: None, -// resign_active: None, -// reopen: None, -// quit: None, -// event: None, -// // menu_command: None, -// validate_menu_command: None, -// will_open_menu: None, -// open_urls: None, -// finish_launching: None, -// menu_actions: Default::default(), -// foreground, -// })) -// } - -// unsafe fn create_menu_bar( -// &self, -// menus: Vec, -// delegate: id, -// actions: &mut Vec>, -// keystroke_matcher: &KeymapMatcher, -// ) -> id { -// let application_menu = NSMenu::new(nil).autorelease(); -// application_menu.setDelegate_(delegate); - -// for menu_config in menus { -// let menu = NSMenu::new(nil).autorelease(); -// menu.setTitle_(ns_string(menu_config.name)); -// menu.setDelegate_(delegate); - -// for item_config in menu_config.items { -// menu.addItem_(self.create_menu_item( -// item_config, -// delegate, -// actions, -// keystroke_matcher, -// )); -// } - -// let menu_item = NSMenuItem::new(nil).autorelease(); -// menu_item.setSubmenu_(menu); -// application_menu.addItem_(menu_item); - -// if menu_config.name == "Window" { -// let app: id = msg_send![APP_CLASS, sharedApplication]; -// app.setWindowsMenu_(menu); -// } -// } - -// application_menu -// } - -// // unsafe fn create_menu_item( -// // &self, -// // item: MenuItem, -// // delegate: id, -// // actions: &mut Vec>, -// // keystroke_matcher: &KeymapMatcher, -// // ) -> id { -// // match item { -// // MenuItem::Separator => NSMenuItem::separatorItem(nil), -// // MenuItem::Action { -// // name, -// // action, -// // os_action, -// // } => { -// // // TODO -// // let keystrokes = keystroke_matcher -// // .bindings_for_action(action.id()) -// // .find(|binding| binding.action().eq(action.as_ref())) -// // .map(|binding| binding.keystrokes()); -// // let selector = match os_action { -// // Some(crate::OsAction::Cut) => selector("cut:"), -// // Some(crate::OsAction::Copy) => selector("copy:"), -// // Some(crate::OsAction::Paste) => selector("paste:"), -// // Some(crate::OsAction::SelectAll) => selector("selectAll:"), -// // Some(crate::OsAction::Undo) => selector("undo:"), -// // Some(crate::OsAction::Redo) => selector("redo:"), -// // None => selector("handleGPUIMenuItem:"), -// // }; - -// // let item; -// // if let Some(keystrokes) = keystrokes { -// // if keystrokes.len() == 1 { -// // let keystroke = &keystrokes[0]; -// // let mut mask = NSEventModifierFlags::empty(); -// // for (modifier, flag) in &[ -// // (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), -// // (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), -// // (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), -// // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), -// // ] { -// // if *modifier { -// // mask |= *flag; -// // } -// // } - -// // item = NSMenuItem::alloc(nil) -// // .initWithTitle_action_keyEquivalent_( -// // ns_string(name), -// // selector, -// // ns_string(key_to_native(&keystroke.key).as_ref()), -// // ) -// // .autorelease(); -// // item.setKeyEquivalentModifierMask_(mask); -// // } -// // // For multi-keystroke bindings, render the keystroke as part of the title. -// // else { -// // use std::fmt::Write; - -// // let mut name = format!("{name} ["); -// // for (i, keystroke) in keystrokes.iter().enumerate() { -// // if i > 0 { -// // name.push(' '); -// // } -// // write!(&mut name, "{}", keystroke).unwrap(); -// // } -// // name.push(']'); - -// // item = NSMenuItem::alloc(nil) -// // .initWithTitle_action_keyEquivalent_( -// // ns_string(&name), -// // selector, -// // ns_string(""), -// // ) -// // .autorelease(); -// // } -// // } else { -// // item = NSMenuItem::alloc(nil) -// // .initWithTitle_action_keyEquivalent_( -// // ns_string(name), -// // selector, -// // ns_string(""), -// // ) -// // .autorelease(); -// // } - -// // let tag = actions.len() as NSInteger; -// // let _: () = msg_send![item, setTag: tag]; -// // actions.push(action); -// // item -// // } -// // MenuItem::Submenu(Menu { name, items }) => { -// // let item = NSMenuItem::new(nil).autorelease(); -// // let submenu = NSMenu::new(nil).autorelease(); -// // submenu.setDelegate_(delegate); -// // for item in items { -// // submenu.addItem_(self.create_menu_item( -// // item, -// // delegate, -// // actions, -// // keystroke_matcher, -// // )); -// // } -// // item.setSubmenu_(submenu); -// // item.setTitle_(ns_string(name)); -// // item -// // } -// // } -// // } - -// unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> { -// let data = self.pasteboard.dataForType(kind); -// if data == nil { -// None -// } else { -// Some(slice::from_raw_parts( -// data.bytes() as *mut u8, -// data.length() as usize, -// )) -// } -// } -// } - -// // impl platform::ForegroundPlatform for MacForegroundPlatform { -// // fn on_become_active(&self, callback: Box) { -// // self.0.borrow_mut().become_active = Some(callback); -// // } - -// // fn on_resign_active(&self, callback: Box) { -// // self.0.borrow_mut().resign_active = Some(callback); -// // } - -// // fn on_quit(&self, callback: Box) { -// // self.0.borrow_mut().quit = Some(callback); -// // } - -// // fn on_reopen(&self, callback: Box) { -// // self.0.borrow_mut().reopen = Some(callback); -// // } - -// // fn on_event(&self, callback: Box bool>) { -// // self.0.borrow_mut().event = Some(callback); -// // } - -// // fn on_open_urls(&self, callback: Box)>) { -// // self.0.borrow_mut().open_urls = Some(callback); -// // } - -// // fn run(&self, on_finish_launching: Box) { -// // self.0.borrow_mut().finish_launching = Some(on_finish_launching); - -// // unsafe { -// // let app: id = msg_send![APP_CLASS, sharedApplication]; -// // let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new]; -// // app.setDelegate_(app_delegate); - -// // let self_ptr = self as *const Self as *const c_void; -// // (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr); -// // (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr); - -// // let pool = NSAutoreleasePool::new(nil); -// // app.run(); -// // pool.drain(); - -// // (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); -// // (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); -// // } -// // } - -// // fn on_menu_command(&self, callback: Box) { -// // self.0.borrow_mut().menu_command = Some(callback); -// // } - -// // fn on_will_open_menu(&self, callback: Box) { -// // self.0.borrow_mut().will_open_menu = Some(callback); -// // } - -// // fn on_validate_menu_command(&self, callback: Box bool>) { -// // self.0.borrow_mut().validate_menu_command = Some(callback); -// // } - -// // fn set_menus(&self, menus: Vec, keystroke_matcher: &KeymapMatcher) { -// // unsafe { -// // let app: id = msg_send![APP_CLASS, sharedApplication]; -// // let mut state = self.0.borrow_mut(); -// // let actions = &mut state.menu_actions; -// // app.setMainMenu_(self.create_menu_bar( -// // menus, -// // app.delegate(), -// // actions, -// // keystroke_matcher, -// // )); -// // } -// // } - -// // fn prompt_for_paths( -// // &self, -// // options: platform::PathPromptOptions, -// // ) -> oneshot::Receiver>> { -// // unsafe { -// // let panel = NSOpenPanel::openPanel(nil); -// // panel.setCanChooseDirectories_(options.directories.to_objc()); -// // panel.setCanChooseFiles_(options.files.to_objc()); -// // panel.setAllowsMultipleSelection_(options.multiple.to_objc()); -// // panel.setResolvesAliases_(false.to_objc()); -// // let (done_tx, done_rx) = oneshot::channel(); -// // let done_tx = Cell::new(Some(done_tx)); -// // let block = ConcreteBlock::new(move |response: NSModalResponse| { -// // let result = if response == NSModalResponse::NSModalResponseOk { -// // let mut result = Vec::new(); -// // let urls = panel.URLs(); -// // for i in 0..urls.count() { -// // let url = urls.objectAtIndex(i); -// // if url.isFileURL() == YES { -// // if let Ok(path) = ns_url_to_path(url) { -// // result.push(path) -// // } -// // } -// // } -// // Some(result) -// // } else { -// // None -// // }; - -// // if let Some(mut done_tx) = done_tx.take() { -// // let _ = postage::sink::Sink::try_send(&mut done_tx, result); -// // } -// // }); -// // let block = block.copy(); -// // let _: () = msg_send![panel, beginWithCompletionHandler: block]; -// // done_rx -// // } -// // } - -// // fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { -// // unsafe { -// // let panel = NSSavePanel::savePanel(nil); -// // let path = ns_string(directory.to_string_lossy().as_ref()); -// // let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); -// // panel.setDirectoryURL(url); - -// // let (done_tx, done_rx) = oneshot::channel(); -// // let done_tx = Cell::new(Some(done_tx)); -// // let block = ConcreteBlock::new(move |response: NSModalResponse| { -// // let mut result = None; -// // if response == NSModalResponse::NSModalResponseOk { -// // let url = panel.URL(); -// // if url.isFileURL() == YES { -// // result = ns_url_to_path(panel.URL()).ok() -// // } -// // } - -// // if let Some(mut done_tx) = done_tx.take() { -// // let _ = postage::sink::Sink::try_send(&mut done_tx, result); -// // } -// // }); -// // let block = block.copy(); -// // let _: () = msg_send![panel, beginWithCompletionHandler: block]; -// // done_rx -// // } -// // } - -// // fn reveal_path(&self, path: &Path) { -// // unsafe { -// // let path = path.to_path_buf(); -// // self.0 -// // .borrow() -// // .foreground -// // .spawn(async move { -// // let full_path = ns_string(path.to_str().unwrap_or("")); -// // let root_full_path = ns_string(""); -// // let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; -// // let _: BOOL = msg_send![ -// // workspace, -// // selectFile: full_path -// // inFileViewerRootedAtPath: root_full_path -// // ]; -// // }) -// // .detach(); -// // } -// // } -// // } - -// // impl Platform for MacPlatform { -// // fn dispatcher(&self) -> Arc { -// // self.dispatcher.clone() -// // } - -// // fn fonts(&self) -> Arc { -// // self.fonts.clone() -// // } - -// // fn activate(&self, ignoring_other_apps: bool) { -// // unsafe { -// // let app = NSApplication::sharedApplication(nil); -// // app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc()); -// // } -// // } - -// // fn hide(&self) { -// // unsafe { -// // let app = NSApplication::sharedApplication(nil); -// // let _: () = msg_send![app, hide: nil]; -// // } -// // } - -// // fn hide_other_apps(&self) { -// // unsafe { -// // let app = NSApplication::sharedApplication(nil); -// // let _: () = msg_send![app, hideOtherApplications: nil]; -// // } -// // } - -// // fn unhide_other_apps(&self) { -// // unsafe { -// // let app = NSApplication::sharedApplication(nil); -// // let _: () = msg_send![app, unhideAllApplications: nil]; -// // } -// // } - -// // fn quit(&self) { -// // // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks -// // // synchronously before this method terminates. If we call `Platform::quit` while holding a -// // // borrow of the app state (which most of the time we will do), we will end up -// // // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve -// // // this, we make quitting the application asynchronous so that we aren't holding borrows to -// // // the app state on the stack when we actually terminate the app. - -// // use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue}; - -// // unsafe { -// // dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit)); -// // } - -// // unsafe extern "C" fn quit(_: *mut c_void) { -// // let app = NSApplication::sharedApplication(nil); -// // let _: () = msg_send![app, terminate: nil]; -// // } -// // } - -// // fn screen_by_id(&self, id: uuid::Uuid) -> Option> { -// // Screen::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>) -// // } - -// // fn screens(&self) -> Vec> { -// // Screen::all() -// // .into_iter() -// // .map(|screen| Rc::new(screen) as Rc<_>) -// // .collect() -// // } - -// // fn open_window( -// // &self, -// // handle: AnyWindowHandle, -// // options: platform::WindowOptions, -// // executor: Rc, -// // ) -> Box { -// // Box::new(MacWindow::open(handle, options, executor, self.fonts())) -// // } - -// // fn main_window(&self) -> Option { -// // MacWindow::main_window() -// // } - -// // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box { -// // Box::new(StatusItem::add(self.fonts())) -// // } - -// // fn write_to_clipboard(&self, item: ClipboardItem) { -// // unsafe { -// // self.pasteboard.clearContents(); - -// // let text_bytes = NSData::dataWithBytes_length_( -// // nil, -// // item.text.as_ptr() as *const c_void, -// // item.text.len() as u64, -// // ); -// // self.pasteboard -// // .setData_forType(text_bytes, NSPasteboardTypeString); - -// // if let Some(metadata) = item.metadata.as_ref() { -// // let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes(); -// // let hash_bytes = NSData::dataWithBytes_length_( -// // nil, -// // hash_bytes.as_ptr() as *const c_void, -// // hash_bytes.len() as u64, -// // ); -// // self.pasteboard -// // .setData_forType(hash_bytes, self.text_hash_pasteboard_type); - -// // let metadata_bytes = NSData::dataWithBytes_length_( -// // nil, -// // metadata.as_ptr() as *const c_void, -// // metadata.len() as u64, -// // ); -// // self.pasteboard -// // .setData_forType(metadata_bytes, self.metadata_pasteboard_type); -// // } -// // } -// // } - -// // fn read_from_clipboard(&self) -> Option { -// // unsafe { -// // if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) { -// // let text = String::from_utf8_lossy(text_bytes).to_string(); -// // let hash_bytes = self -// // .read_from_pasteboard(self.text_hash_pasteboard_type) -// // .and_then(|bytes| bytes.try_into().ok()) -// // .map(u64::from_be_bytes); -// // let metadata_bytes = self -// // .read_from_pasteboard(self.metadata_pasteboard_type) -// // .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok()); - -// // if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) { -// // if hash == ClipboardItem::text_hash(&text) { -// // Some(ClipboardItem { -// // text, -// // metadata: Some(metadata), -// // }) -// // } else { -// // Some(ClipboardItem { -// // text, -// // metadata: None, -// // }) -// // } -// // } else { -// // Some(ClipboardItem { -// // text, -// // metadata: None, -// // }) -// // } -// // } else { -// // None -// // } -// // } -// // } - -// // fn open_url(&self, url: &str) { -// // unsafe { -// // let url = NSURL::alloc(nil) -// // .initWithString_(ns_string(url)) -// // .autorelease(); -// // let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; -// // msg_send![workspace, openURL: url] -// // } -// // } - -// // fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> { -// // let url = CFString::from(url); -// // let username = CFString::from(username); -// // let password = CFData::from_buffer(password); - -// // unsafe { -// // use security::*; - -// // // First, check if there are already credentials for the given server. If so, then -// // // update the username and password. -// // let mut verb = "updating"; -// // let mut query_attrs = CFMutableDictionary::with_capacity(2); -// // query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); -// // query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); - -// // let mut attrs = CFMutableDictionary::with_capacity(4); -// // attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); -// // attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); -// // attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef()); -// // attrs.set(kSecValueData as *const _, password.as_CFTypeRef()); - -// // let mut status = SecItemUpdate( -// // query_attrs.as_concrete_TypeRef(), -// // attrs.as_concrete_TypeRef(), -// // ); - -// // // If there were no existing credentials for the given server, then create them. -// // if status == errSecItemNotFound { -// // verb = "creating"; -// // status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut()); -// // } - -// // if status != errSecSuccess { -// // return Err(anyhow!("{} password failed: {}", verb, status)); -// // } -// // } -// // Ok(()) -// // } - -// // fn read_credentials(&self, url: &str) -> Result)>> { -// // let url = CFString::from(url); -// // let cf_true = CFBoolean::true_value().as_CFTypeRef(); - -// // unsafe { -// // use security::*; - -// // // Find any credentials for the given server URL. -// // let mut attrs = CFMutableDictionary::with_capacity(5); -// // attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); -// // attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); -// // attrs.set(kSecReturnAttributes as *const _, cf_true); -// // attrs.set(kSecReturnData as *const _, cf_true); - -// // let mut result = CFTypeRef::from(ptr::null()); -// // let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result); -// // match status { -// // security::errSecSuccess => {} -// // security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None), -// // _ => return Err(anyhow!("reading password failed: {}", status)), -// // } - -// // let result = CFType::wrap_under_create_rule(result) -// // .downcast::() -// // .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?; -// // let username = result -// // .find(kSecAttrAccount as *const _) -// // .ok_or_else(|| anyhow!("account was missing from keychain item"))?; -// // let username = CFType::wrap_under_get_rule(*username) -// // .downcast::() -// // .ok_or_else(|| anyhow!("account was not a string"))?; -// // let password = result -// // .find(kSecValueData as *const _) -// // .ok_or_else(|| anyhow!("password was missing from keychain item"))?; -// // let password = CFType::wrap_under_get_rule(*password) -// // .downcast::() -// // .ok_or_else(|| anyhow!("password was not a string"))?; - -// // Ok(Some((username.to_string(), password.bytes().to_vec()))) -// // } -// // } - -// // fn delete_credentials(&self, url: &str) -> Result<()> { -// // let url = CFString::from(url); - -// // unsafe { -// // use security::*; - -// // let mut query_attrs = CFMutableDictionary::with_capacity(2); -// // query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); -// // query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); - -// // let status = SecItemDelete(query_attrs.as_concrete_TypeRef()); - -// // if status != errSecSuccess { -// // return Err(anyhow!("delete password failed: {}", status)); -// // } -// // } -// // Ok(()) -// // } - -// // fn set_cursor_style(&self, style: CursorStyle) { -// // unsafe { -// // let new_cursor: id = match style { -// // CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], -// // CursorStyle::ResizeLeftRight => { -// // msg_send![class!(NSCursor), resizeLeftRightCursor] -// // } -// // CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor], -// // CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], -// // CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor], -// // }; - -// // let old_cursor: id = msg_send![class!(NSCursor), currentCursor]; -// // if new_cursor != old_cursor { -// // let _: () = msg_send![new_cursor, set]; -// // } -// // } -// // } - -// // fn should_auto_hide_scrollbars(&self) -> bool { -// // #[allow(non_upper_case_globals)] -// // const NSScrollerStyleOverlay: NSInteger = 1; - -// // unsafe { -// // let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle]; -// // style == NSScrollerStyleOverlay -// // } -// // } - -// // fn local_timezone(&self) -> UtcOffset { -// // unsafe { -// // let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone]; -// // let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT]; -// // UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap() -// // } -// // } - -// // fn path_for_auxiliary_executable(&self, name: &str) -> Result { -// // unsafe { -// // let bundle: id = NSBundle::mainBundle(); -// // if bundle.is_null() { -// // Err(anyhow!("app is not running inside a bundle")) -// // } else { -// // let name = ns_string(name); -// // let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name]; -// // if url.is_null() { -// // Err(anyhow!("resource not found")) -// // } else { -// // ns_url_to_path(url) -// // } -// // } -// // } -// // } - -// // fn app_path(&self) -> Result { -// // unsafe { -// // let bundle: id = NSBundle::mainBundle(); -// // if bundle.is_null() { -// // Err(anyhow!("app is not running inside a bundle")) -// // } else { -// // Ok(path_from_objc(msg_send![bundle, bundlePath])) -// // } -// // } -// // } - -// // fn app_version(&self) -> Result { -// // unsafe { -// // let bundle: id = NSBundle::mainBundle(); -// // if bundle.is_null() { -// // Err(anyhow!("app is not running inside a bundle")) -// // } else { -// // let version: id = msg_send![bundle, objectForInfoDictionaryKey: ns_string("CFBundleShortVersionString")]; -// // let len = msg_send![version, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; -// // let bytes = version.UTF8String() as *const u8; -// // let version = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); -// // version.parse() -// // } -// // } -// // } - -// // fn os_name(&self) -> &'static str { -// // "macOS" -// // } - -// // fn os_version(&self) -> Result { -// // unsafe { -// // let process_info = NSProcessInfo::processInfo(nil); -// // let version = process_info.operatingSystemVersion(); -// // Ok(AppVersion { -// // major: version.majorVersion as usize, -// // minor: version.minorVersion as usize, -// // patch: version.patchVersion as usize, -// // }) -// // } -// // } - -// // fn restart(&self) { -// // use std::os::unix::process::CommandExt as _; - -// // let app_pid = std::process::id().to_string(); -// // let app_path = self -// // .app_path() -// // .ok() -// // // When the app is not bundled, `app_path` returns the -// // // directory containing the executable. Disregard this -// // // and get the path to the executable itself. -// // .and_then(|path| (path.extension()?.to_str()? == "app").then_some(path)) -// // .unwrap_or_else(|| std::env::current_exe().unwrap()); - -// // // Wait until this process has exited and then re-open this path. -// // let script = r#" -// // while kill -0 $0 2> /dev/null; do -// // sleep 0.1 -// // done -// // open "$1" -// // "#; - -// // let restart_process = Command::new("/bin/bash") -// // .arg("-c") -// // .arg(script) -// // .arg(app_pid) -// // .arg(app_path) -// // .process_group(0) -// // .spawn(); - -// // match restart_process { -// // Ok(_) => self.quit(), -// // Err(e) => log::error!("failed to spawn restart script: {:?}", e), -// // } -// // } -// // } - -// unsafe fn path_from_objc(path: id) -> PathBuf { -// let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; -// let bytes = path.UTF8String() as *const u8; -// let path = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); -// PathBuf::from(path) -// } - -// unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform { -// let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR); -// assert!(!platform_ptr.is_null()); -// &*(platform_ptr as *const MacForegroundPlatform) -// } - -// extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) { -// unsafe { -// if let Some(event) = Event::from_native(native_event, None) { -// let platform = get_foreground_platform(this); -// if let Some(callback) = platform.0.borrow_mut().event.as_mut() { -// if callback(event) { -// return; -// } -// } -// } -// msg_send![super(this, class!(NSApplication)), sendEvent: native_event] -// } -// } - -// extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { -// unsafe { -// let app: id = msg_send![APP_CLASS, sharedApplication]; -// app.setActivationPolicy_(NSApplicationActivationPolicyRegular); - -// let platform = get_foreground_platform(this); -// let callback = platform.0.borrow_mut().finish_launching.take(); -// if let Some(callback) = callback { -// callback(); -// } -// } -// } - -// extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) { -// if !has_open_windows { -// let platform = unsafe { get_foreground_platform(this) }; -// if let Some(callback) = platform.0.borrow_mut().reopen.as_mut() { -// callback(); -// } -// } -// } - -// extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) { -// let platform = unsafe { get_foreground_platform(this) }; -// if let Some(callback) = platform.0.borrow_mut().become_active.as_mut() { -// callback(); -// } -// } - -// extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) { -// let platform = unsafe { get_foreground_platform(this) }; -// if let Some(callback) = platform.0.borrow_mut().resign_active.as_mut() { -// callback(); -// } -// } - -// extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { -// let platform = unsafe { get_foreground_platform(this) }; -// if let Some(callback) = platform.0.borrow_mut().quit.as_mut() { -// callback(); -// } -// } - -// extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { -// let urls = unsafe { -// (0..urls.count()) -// .into_iter() -// .filter_map(|i| { -// let url = urls.objectAtIndex(i); -// match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() { -// Ok(string) => Some(string.to_string()), -// Err(err) => { -// log::error!("error converting path to string: {}", err); -// None -// } -// } -// }) -// .collect::>() -// }; -// let platform = unsafe { get_foreground_platform(this) }; -// if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() { -// callback(urls); -// } -// } - -// extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { -// unsafe { -// let platform = get_foreground_platform(this); -// let mut platform = platform.0.borrow_mut(); -// if let Some(mut callback) = platform.menu_command.take() { -// let tag: NSInteger = msg_send![item, tag]; -// let index = tag as usize; -// if let Some(action) = platform.menu_actions.get(index) { -// callback(action.as_ref()); -// } -// platform.menu_command = Some(callback); -// } -// } -// } - -// extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { -// unsafe { -// let mut result = false; -// let platform = get_foreground_platform(this); -// let mut platform = platform.0.borrow_mut(); -// if let Some(mut callback) = platform.validate_menu_command.take() { -// let tag: NSInteger = msg_send![item, tag]; -// let index = tag as usize; -// if let Some(action) = platform.menu_actions.get(index) { -// result = callback(action.as_ref()); -// } -// platform.validate_menu_command = Some(callback); -// } -// result -// } -// } - -// extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) { -// unsafe { -// let platform = get_foreground_platform(this); -// let mut platform = platform.0.borrow_mut(); -// if let Some(mut callback) = platform.will_open_menu.take() { -// callback(); -// platform.will_open_menu = Some(callback); -// } -// } -// } - -// unsafe fn ns_string(string: &str) -> id { -// NSString::alloc(nil).init_str(string).autorelease() -// } - -// unsafe fn ns_url_to_path(url: id) -> Result { -// let path: *mut c_char = msg_send![url, fileSystemRepresentation]; -// if path.is_null() { -// Err(anyhow!( -// "url is not a file path: {}", -// CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy() -// )) -// } else { -// Ok(PathBuf::from(OsStr::from_bytes( -// CStr::from_ptr(path).to_bytes(), -// ))) -// } -// } - -// mod security { -// #![allow(non_upper_case_globals)] -// use super::*; - -// #[link(name = "Security", kind = "framework")] -// extern "C" { -// pub static kSecClass: CFStringRef; -// pub static kSecClassInternetPassword: CFStringRef; -// pub static kSecAttrServer: CFStringRef; -// pub static kSecAttrAccount: CFStringRef; -// pub static kSecValueData: CFStringRef; -// pub static kSecReturnAttributes: CFStringRef; -// pub static kSecReturnData: CFStringRef; - -// pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; -// pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus; -// pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus; -// pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; -// } - -// pub const errSecSuccess: OSStatus = 0; -// pub const errSecUserCanceled: OSStatus = -128; -// pub const errSecItemNotFound: OSStatus = -25300; -// } - -// #[cfg(test)] -// mod tests { -// use crate::platform::Platform; - -// use super::*; - -// #[test] -// fn test_clipboard() { -// let platform = build_platform(); -// assert_eq!(platform.read_from_clipboard(), None); - -// let item = ClipboardItem::new("1".to_string()); -// platform.write_to_clipboard(item.clone()); -// assert_eq!(platform.read_from_clipboard(), Some(item)); - -// let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]); -// platform.write_to_clipboard(item.clone()); -// assert_eq!(platform.read_from_clipboard(), Some(item)); - -// let text_from_other_app = "text from other app"; -// unsafe { -// let bytes = NSData::dataWithBytes_length_( -// nil, -// text_from_other_app.as_ptr() as *const c_void, -// text_from_other_app.len() as u64, -// ); -// platform -// .pasteboard -// .setData_forType(bytes, NSPasteboardTypeString); -// } -// assert_eq!( -// platform.read_from_clipboard(), -// Some(ClipboardItem::new(text_from_other_app.to_string())) -// ); -// } - -// fn build_platform() -> MacPlatform { -// let mut platform = MacPlatform::new(); -// platform.pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) }; -// platform -// } -// } +use super::BoolExt; +use crate::{ + AnyWindowHandle, ClipboardItem, CursorStyle, Event, ForegroundExecutor, MacDispatcher, + MacScreen, MacTextSystem, MacWindow, PathPromptOptions, Platform, PlatformScreen, + PlatformTextSystem, PlatformWindow, Result, SemanticVersion, WindowOptions, +}; +use anyhow::anyhow; +use block::ConcreteBlock; +use cocoa::{ + appkit::{ + NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, + NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, NSSavePanel, NSWindow, + }, + base::{id, nil, BOOL, YES}, + foundation::{ + NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString, + NSUInteger, NSURL, + }, +}; +use core_foundation::{ + base::{CFType, CFTypeRef, OSStatus, TCFType as _}, + boolean::CFBoolean, + data::CFData, + dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary}, + string::{CFString, CFStringRef}, +}; +use ctor::ctor; +use futures::channel::oneshot; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use ptr::null_mut; +use std::{ + cell::{Cell, RefCell}, + convert::TryInto, + ffi::{c_void, CStr, OsStr}, + os::{raw::c_char, unix::ffi::OsStrExt}, + path::{Path, PathBuf}, + process::Command, + ptr, + rc::Rc, + slice, str, + sync::Arc, +}; +use time::UtcOffset; + +#[allow(non_upper_case_globals)] +const NSUTF8StringEncoding: NSUInteger = 4; + +#[allow(non_upper_case_globals)] +pub const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2; + +const MAC_PLATFORM_IVAR: &str = "platform"; +static mut APP_CLASS: *const Class = ptr::null(); +static mut APP_DELEGATE_CLASS: *const Class = ptr::null(); + +#[ctor] +unsafe fn build_classes() { + APP_CLASS = { + let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap(); + decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); + decl.add_method( + sel!(sendEvent:), + send_event as extern "C" fn(&mut Object, Sel, id), + ); + decl.register() + }; + + APP_DELEGATE_CLASS = { + let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap(); + decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR); + decl.add_method( + sel!(applicationDidFinishLaunching:), + did_finish_launching as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationShouldHandleReopen:hasVisibleWindows:), + should_handle_reopen as extern "C" fn(&mut Object, Sel, id, bool), + ); + decl.add_method( + sel!(applicationDidBecomeActive:), + did_become_active as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationDidResignActive:), + did_resign_active as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(applicationWillTerminate:), + will_terminate as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(handleGPUIMenuItem:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + // Add menu item handlers so that OS save panels have the correct key commands + decl.add_method( + sel!(cut:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(copy:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(paste:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(selectAll:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(undo:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(redo:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(validateMenuItem:), + validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool, + ); + decl.add_method( + sel!(menuWillOpen:), + menu_will_open as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(application:openURLs:), + open_urls as extern "C" fn(&mut Object, Sel, id, id), + ); + decl.register() + } +} + +pub struct MacPlatform(RefCell); + +pub struct MacPlatformState { + executor: Rc, + text_system: Arc, + pasteboard: id, + text_hash_pasteboard_type: id, + metadata_pasteboard_type: id, + become_active: Option>, + resign_active: Option>, + reopen: Option>, + quit: Option>, + event: Option bool>>, + // menu_command: Option>, + // validate_menu_command: Option bool>>, + will_open_menu: Option>, + open_urls: Option)>>, + finish_launching: Option>, + // menu_actions: Vec>, +} + +impl MacPlatform { + pub fn new() -> Self { + Self(RefCell::new(MacPlatformState { + executor: Rc::new(ForegroundExecutor::new(Arc::new(MacDispatcher)).unwrap()), + text_system: Arc::new(MacTextSystem::new()), + pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) }, + text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") }, + metadata_pasteboard_type: unsafe { ns_string("zed-metadata") }, + become_active: None, + resign_active: None, + reopen: None, + quit: None, + event: None, + will_open_menu: None, + open_urls: None, + finish_launching: None, + // menu_command: None, + // validate_menu_command: None, + // menu_actions: Default::default(), + })) + } + + unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> { + let pasteboard = self.0.borrow().pasteboard; + let data = pasteboard.dataForType(kind); + if data == nil { + None + } else { + Some(slice::from_raw_parts( + data.bytes() as *mut u8, + data.length() as usize, + )) + } + } + + // unsafe fn create_menu_bar( + // &self, + // menus: Vec, + // delegate: id, + // actions: &mut Vec>, + // keystroke_matcher: &KeymapMatcher, + // ) -> id { + // let application_menu = NSMenu::new(nil).autorelease(); + // application_menu.setDelegate_(delegate); + + // for menu_config in menus { + // let menu = NSMenu::new(nil).autorelease(); + // menu.setTitle_(ns_string(menu_config.name)); + // menu.setDelegate_(delegate); + + // for item_config in menu_config.items { + // menu.addItem_(self.create_menu_item( + // item_config, + // delegate, + // actions, + // keystroke_matcher, + // )); + // } + + // let menu_item = NSMenuItem::new(nil).autorelease(); + // menu_item.setSubmenu_(menu); + // application_menu.addItem_(menu_item); + + // if menu_config.name == "Window" { + // let app: id = msg_send![APP_CLASS, sharedApplication]; + // app.setWindowsMenu_(menu); + // } + // } + + // application_menu + // } + + // unsafe fn create_menu_item( + // &self, + // item: MenuItem, + // delegate: id, + // actions: &mut Vec>, + // keystroke_matcher: &KeymapMatcher, + // ) -> id { + // match item { + // MenuItem::Separator => NSMenuItem::separatorItem(nil), + // MenuItem::Action { + // name, + // action, + // os_action, + // } => { + // // TODO + // let keystrokes = keystroke_matcher + // .bindings_for_action(action.id()) + // .find(|binding| binding.action().eq(action.as_ref())) + // .map(|binding| binding.keystrokes()); + // let selector = match os_action { + // Some(crate::OsAction::Cut) => selector("cut:"), + // Some(crate::OsAction::Copy) => selector("copy:"), + // Some(crate::OsAction::Paste) => selector("paste:"), + // Some(crate::OsAction::SelectAll) => selector("selectAll:"), + // Some(crate::OsAction::Undo) => selector("undo:"), + // Some(crate::OsAction::Redo) => selector("redo:"), + // None => selector("handleGPUIMenuItem:"), + // }; + + // let item; + // if let Some(keystrokes) = keystrokes { + // if keystrokes.len() == 1 { + // let keystroke = &keystrokes[0]; + // let mut mask = NSEventModifierFlags::empty(); + // for (modifier, flag) in &[ + // (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), + // (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), + // (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), + // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), + // ] { + // if *modifier { + // mask |= *flag; + // } + // } + + // item = NSMenuItem::alloc(nil) + // .initWithTitle_action_keyEquivalent_( + // ns_string(name), + // selector, + // ns_string(key_to_native(&keystroke.key).as_ref()), + // ) + // .autorelease(); + // item.setKeyEquivalentModifierMask_(mask); + // } + // // For multi-keystroke bindings, render the keystroke as part of the title. + // else { + // use std::fmt::Write; + + // let mut name = format!("{name} ["); + // for (i, keystroke) in keystrokes.iter().enumerate() { + // if i > 0 { + // name.push(' '); + // } + // write!(&mut name, "{}", keystroke).unwrap(); + // } + // name.push(']'); + + // item = NSMenuItem::alloc(nil) + // .initWithTitle_action_keyEquivalent_( + // ns_string(&name), + // selector, + // ns_string(""), + // ) + // .autorelease(); + // } + // } else { + // item = NSMenuItem::alloc(nil) + // .initWithTitle_action_keyEquivalent_( + // ns_string(name), + // selector, + // ns_string(""), + // ) + // .autorelease(); + // } + + // let tag = actions.len() as NSInteger; + // let _: () = msg_send![item, setTag: tag]; + // actions.push(action); + // item + // } + // MenuItem::Submenu(Menu { name, items }) => { + // let item = NSMenuItem::new(nil).autorelease(); + // let submenu = NSMenu::new(nil).autorelease(); + // submenu.setDelegate_(delegate); + // for item in items { + // submenu.addItem_(self.create_menu_item( + // item, + // delegate, + // actions, + // keystroke_matcher, + // )); + // } + // item.setSubmenu_(submenu); + // item.setTitle_(ns_string(name)); + // item + // } + // } + // } +} + +impl Platform for MacPlatform { + fn executor(&self) -> Rc { + self.0.borrow().executor.clone() + } + + fn text_system(&self) -> Arc { + self.0.borrow().text_system.clone() + } + + fn run(&self, on_finish_launching: Box) { + self.0.borrow_mut().finish_launching = Some(on_finish_launching); + + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new]; + app.setDelegate_(app_delegate); + + let self_ptr = self as *const Self as *const c_void; + (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr); + (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr); + + let pool = NSAutoreleasePool::new(nil); + app.run(); + pool.drain(); + + (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); + (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::()); + } + } + + fn quit(&self) { + // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks + // synchronously before this method terminates. If we call `Platform::quit` while holding a + // borrow of the app state (which most of the time we will do), we will end up + // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve + // this, we make quitting the application asynchronous so that we aren't holding borrows to + // the app state on the stack when we actually terminate the app. + + use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue}; + + unsafe { + dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit)); + } + + unsafe extern "C" fn quit(_: *mut c_void) { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, terminate: nil]; + } + } + + fn restart(&self) { + use std::os::unix::process::CommandExt as _; + + let app_pid = std::process::id().to_string(); + let app_path = self + .app_path() + .ok() + // When the app is not bundled, `app_path` returns the + // directory containing the executable. Disregard this + // and get the path to the executable itself. + .and_then(|path| (path.extension()?.to_str()? == "app").then_some(path)) + .unwrap_or_else(|| std::env::current_exe().unwrap()); + + // Wait until this process has exited and then re-open this path. + let script = r#" + while kill -0 $0 2> /dev/null; do + sleep 0.1 + done + open "$1" + "#; + + let restart_process = Command::new("/bin/bash") + .arg("-c") + .arg(script) + .arg(app_pid) + .arg(app_path) + .process_group(0) + .spawn(); + + match restart_process { + Ok(_) => self.quit(), + Err(e) => log::error!("failed to spawn restart script: {:?}", e), + } + } + + fn activate(&self, ignoring_other_apps: bool) { + unsafe { + let app = NSApplication::sharedApplication(nil); + app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc()); + } + } + + fn hide(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, hide: nil]; + } + } + + fn hide_other_apps(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, hideOtherApplications: nil]; + } + } + + fn unhide_other_apps(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let _: () = msg_send![app, unhideAllApplications: nil]; + } + } + + fn screens(&self) -> Vec> { + MacScreen::all() + .into_iter() + .map(|screen| Rc::new(screen) as Rc<_>) + .collect() + } + + fn screen_by_id(&self, id: uuid::Uuid) -> Option> { + MacScreen::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>) + } + + fn main_window(&self) -> Option { + MacWindow::main_window() + } + + // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box { + // Box::new(StatusItem::add(self.fonts())) + // } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowOptions, + ) -> Box { + Box::new(MacWindow::open( + handle, + options, + self.executor(), + self.text_system(), + )) + } + + fn open_url(&self, url: &str) { + unsafe { + let url = NSURL::alloc(nil) + .initWithString_(ns_string(url)) + .autorelease(); + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + msg_send![workspace, openURL: url] + } + } + + fn on_open_urls(&self, callback: Box)>) { + self.0.borrow_mut().open_urls = Some(callback); + } + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>> { + unsafe { + let panel = NSOpenPanel::openPanel(nil); + panel.setCanChooseDirectories_(options.directories.to_objc()); + panel.setCanChooseFiles_(options.files.to_objc()); + panel.setAllowsMultipleSelection_(options.multiple.to_objc()); + panel.setResolvesAliases_(false.to_objc()); + let (done_tx, done_rx) = oneshot::channel(); + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |response: NSModalResponse| { + let result = if response == NSModalResponse::NSModalResponseOk { + let mut result = Vec::new(); + let urls = panel.URLs(); + for i in 0..urls.count() { + let url = urls.objectAtIndex(i); + if url.isFileURL() == YES { + if let Ok(path) = ns_url_to_path(url) { + result.push(path) + } + } + } + Some(result) + } else { + None + }; + + if let Some(mut done_tx) = done_tx.take() { + let _ = done_tx.send(result); + } + }); + let block = block.copy(); + let _: () = msg_send![panel, beginWithCompletionHandler: block]; + done_rx + } + } + + fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { + unsafe { + let panel = NSSavePanel::savePanel(nil); + let path = ns_string(directory.to_string_lossy().as_ref()); + let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); + panel.setDirectoryURL(url); + + let (done_tx, done_rx) = oneshot::channel(); + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |response: NSModalResponse| { + let mut result = None; + if response == NSModalResponse::NSModalResponseOk { + let url = panel.URL(); + if url.isFileURL() == YES { + result = ns_url_to_path(panel.URL()).ok() + } + } + + if let Some(mut done_tx) = done_tx.take() { + let _ = done_tx.send(result); + } + }); + let block = block.copy(); + let _: () = msg_send![panel, beginWithCompletionHandler: block]; + done_rx + } + } + + fn reveal_path(&self, path: &Path) { + unsafe { + let path = path.to_path_buf(); + self.0 + .borrow() + .executor + .spawn(async move { + let full_path = ns_string(path.to_str().unwrap_or("")); + let root_full_path = ns_string(""); + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + let _: BOOL = msg_send![ + workspace, + selectFile: full_path + inFileViewerRootedAtPath: root_full_path + ]; + }) + .detach(); + } + } + + fn on_become_active(&self, callback: Box) { + self.0.borrow_mut().become_active = Some(callback); + } + + fn on_resign_active(&self, callback: Box) { + self.0.borrow_mut().resign_active = Some(callback); + } + + fn on_quit(&self, callback: Box) { + self.0.borrow_mut().quit = Some(callback); + } + + fn on_reopen(&self, callback: Box) { + self.0.borrow_mut().reopen = Some(callback); + } + + fn on_event(&self, callback: Box bool>) { + self.0.borrow_mut().event = Some(callback); + } + + fn os_name(&self) -> &'static str { + "macOS" + } + + fn os_version(&self) -> Result { + unsafe { + let process_info = NSProcessInfo::processInfo(nil); + let version = process_info.operatingSystemVersion(); + Ok(SemanticVersion { + major: version.majorVersion as usize, + minor: version.minorVersion as usize, + patch: version.patchVersion as usize, + }) + } + } + + fn app_version(&self) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + if bundle.is_null() { + Err(anyhow!("app is not running inside a bundle")) + } else { + let version: id = msg_send![bundle, objectForInfoDictionaryKey: ns_string("CFBundleShortVersionString")]; + let len = msg_send![version, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; + let bytes = version.UTF8String() as *const u8; + let version = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); + version.parse() + } + } + } + + fn app_path(&self) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + if bundle.is_null() { + Err(anyhow!("app is not running inside a bundle")) + } else { + Ok(path_from_objc(msg_send![bundle, bundlePath])) + } + } + } + + fn local_timezone(&self) -> UtcOffset { + unsafe { + let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone]; + let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT]; + UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap() + } + } + + fn path_for_auxiliary_executable(&self, name: &str) -> Result { + unsafe { + let bundle: id = NSBundle::mainBundle(); + if bundle.is_null() { + Err(anyhow!("app is not running inside a bundle")) + } else { + let name = ns_string(name); + let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name]; + if url.is_null() { + Err(anyhow!("resource not found")) + } else { + ns_url_to_path(url) + } + } + } + } + + fn set_cursor_style(&self, style: CursorStyle) { + unsafe { + let new_cursor: id = match style { + CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], + CursorStyle::ResizeLeftRight => { + msg_send![class!(NSCursor), resizeLeftRightCursor] + } + CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor], + CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], + CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor], + }; + + let old_cursor: id = msg_send![class!(NSCursor), currentCursor]; + if new_cursor != old_cursor { + let _: () = msg_send![new_cursor, set]; + } + } + } + + fn should_auto_hide_scrollbars(&self) -> bool { + #[allow(non_upper_case_globals)] + const NSScrollerStyleOverlay: NSInteger = 1; + + unsafe { + let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle]; + style == NSScrollerStyleOverlay + } + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + let state = self.0.borrow(); + unsafe { + state.pasteboard.clearContents(); + + let text_bytes = NSData::dataWithBytes_length_( + nil, + item.text.as_ptr() as *const c_void, + item.text.len() as u64, + ); + state + .pasteboard + .setData_forType(text_bytes, NSPasteboardTypeString); + + if let Some(metadata) = item.metadata.as_ref() { + let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes(); + let hash_bytes = NSData::dataWithBytes_length_( + nil, + hash_bytes.as_ptr() as *const c_void, + hash_bytes.len() as u64, + ); + state + .pasteboard + .setData_forType(hash_bytes, state.text_hash_pasteboard_type); + + let metadata_bytes = NSData::dataWithBytes_length_( + nil, + metadata.as_ptr() as *const c_void, + metadata.len() as u64, + ); + state + .pasteboard + .setData_forType(metadata_bytes, state.metadata_pasteboard_type); + } + } + } + + fn read_from_clipboard(&self) -> Option { + let state = self.0.borrow(); + unsafe { + if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) { + let text = String::from_utf8_lossy(text_bytes).to_string(); + let hash_bytes = self + .read_from_pasteboard(state.text_hash_pasteboard_type) + .and_then(|bytes| bytes.try_into().ok()) + .map(u64::from_be_bytes); + let metadata_bytes = self + .read_from_pasteboard(state.metadata_pasteboard_type) + .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok()); + + if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) { + if hash == ClipboardItem::text_hash(&text) { + Some(ClipboardItem { + text, + metadata: Some(metadata), + }) + } else { + Some(ClipboardItem { + text, + metadata: None, + }) + } + } else { + Some(ClipboardItem { + text, + metadata: None, + }) + } + } else { + None + } + } + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> { + let url = CFString::from(url); + let username = CFString::from(username); + let password = CFData::from_buffer(password); + + unsafe { + use security::*; + + // First, check if there are already credentials for the given server. If so, then + // update the username and password. + let mut verb = "updating"; + let mut query_attrs = CFMutableDictionary::with_capacity(2); + query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + + let mut attrs = CFMutableDictionary::with_capacity(4); + attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef()); + attrs.set(kSecValueData as *const _, password.as_CFTypeRef()); + + let mut status = SecItemUpdate( + query_attrs.as_concrete_TypeRef(), + attrs.as_concrete_TypeRef(), + ); + + // If there were no existing credentials for the given server, then create them. + if status == errSecItemNotFound { + verb = "creating"; + status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut()); + } + + if status != errSecSuccess { + return Err(anyhow!("{} password failed: {}", verb, status)); + } + } + Ok(()) + } + + // fn on_menu_command(&self, callback: Box) { + // self.0.borrow_mut().menu_command = Some(callback); + // } + + // fn on_will_open_menu(&self, callback: Box) { + // self.0.borrow_mut().will_open_menu = Some(callback); + // } + + // fn on_validate_menu_command(&self, callback: Box bool>) { + // self.0.borrow_mut().validate_menu_command = Some(callback); + // } + + // fn set_menus(&self, menus: Vec, keystroke_matcher: &KeymapMatcher) { + // unsafe { + // let app: id = msg_send![APP_CLASS, sharedApplication]; + // let mut state = self.0.borrow_mut(); + // let actions = &mut state.menu_actions; + // app.setMainMenu_(self.create_menu_bar( + // menus, + // app.delegate(), + // actions, + // keystroke_matcher, + // )); + // } + // } + + fn read_credentials(&self, url: &str) -> Result)>> { + let url = CFString::from(url); + let cf_true = CFBoolean::true_value().as_CFTypeRef(); + + unsafe { + use security::*; + + // Find any credentials for the given server URL. + let mut attrs = CFMutableDictionary::with_capacity(5); + attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + attrs.set(kSecReturnAttributes as *const _, cf_true); + attrs.set(kSecReturnData as *const _, cf_true); + + let mut result = CFTypeRef::from(ptr::null()); + let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result); + match status { + security::errSecSuccess => {} + security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None), + _ => return Err(anyhow!("reading password failed: {}", status)), + } + + let result = CFType::wrap_under_create_rule(result) + .downcast::() + .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?; + let username = result + .find(kSecAttrAccount as *const _) + .ok_or_else(|| anyhow!("account was missing from keychain item"))?; + let username = CFType::wrap_under_get_rule(*username) + .downcast::() + .ok_or_else(|| anyhow!("account was not a string"))?; + let password = result + .find(kSecValueData as *const _) + .ok_or_else(|| anyhow!("password was missing from keychain item"))?; + let password = CFType::wrap_under_get_rule(*password) + .downcast::() + .ok_or_else(|| anyhow!("password was not a string"))?; + + Ok(Some((username.to_string(), password.bytes().to_vec()))) + } + } + + fn delete_credentials(&self, url: &str) -> Result<()> { + let url = CFString::from(url); + + unsafe { + use security::*; + + let mut query_attrs = CFMutableDictionary::with_capacity(2); + query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _); + query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef()); + + let status = SecItemDelete(query_attrs.as_concrete_TypeRef()); + + if status != errSecSuccess { + return Err(anyhow!("delete password failed: {}", status)); + } + } + Ok(()) + } +} + +unsafe fn path_from_objc(path: id) -> PathBuf { + let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; + let bytes = path.UTF8String() as *const u8; + let path = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); + PathBuf::from(path) +} + +unsafe fn get_foreground_platform(object: &mut Object) -> &MacPlatform { + let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR); + assert!(!platform_ptr.is_null()); + &*(platform_ptr as *const MacPlatform) +} + +extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) { + unsafe { + if let Some(event) = Event::from_native(native_event, None) { + let platform = get_foreground_platform(this); + if let Some(callback) = platform.0.borrow_mut().event.as_mut() { + if callback(event) { + return; + } + } + } + msg_send![super(this, class!(NSApplication)), sendEvent: native_event] + } +} + +extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setActivationPolicy_(NSApplicationActivationPolicyRegular); + + let platform = get_foreground_platform(this); + let callback = platform.0.borrow_mut().finish_launching.take(); + if let Some(callback) = callback { + callback(); + } + } +} + +extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) { + if !has_open_windows { + let platform = unsafe { get_foreground_platform(this) }; + if let Some(callback) = platform.0.borrow_mut().reopen.as_mut() { + callback(); + } + } +} + +extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) { + let platform = unsafe { get_foreground_platform(this) }; + if let Some(callback) = platform.0.borrow_mut().become_active.as_mut() { + callback(); + } +} + +extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) { + let platform = unsafe { get_foreground_platform(this) }; + if let Some(callback) = platform.0.borrow_mut().resign_active.as_mut() { + callback(); + } +} + +extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { + let platform = unsafe { get_foreground_platform(this) }; + if let Some(callback) = platform.0.borrow_mut().quit.as_mut() { + callback(); + } +} + +extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { + let urls = unsafe { + (0..urls.count()) + .into_iter() + .filter_map(|i| { + let url = urls.objectAtIndex(i); + match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() { + Ok(string) => Some(string.to_string()), + Err(err) => { + log::error!("error converting path to string: {}", err); + None + } + } + }) + .collect::>() + }; + let platform = unsafe { get_foreground_platform(this) }; + if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() { + callback(urls); + } +} + +extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { + todo!() + // unsafe { + // let platform = get_foreground_platform(this); + // let mut platform = platform.0.borrow_mut(); + // if let Some(mut callback) = platform.menu_command.take() { + // let tag: NSInteger = msg_send![item, tag]; + // let index = tag as usize; + // if let Some(action) = platform.menu_actions.get(index) { + // callback(action.as_ref()); + // } + // platform.menu_command = Some(callback); + // } + // } +} + +extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { + todo!() + // unsafe { + // let mut result = false; + // let platform = get_foreground_platform(this); + // let mut platform = platform.0.borrow_mut(); + // if let Some(mut callback) = platform.validate_menu_command.take() { + // let tag: NSInteger = msg_send![item, tag]; + // let index = tag as usize; + // if let Some(action) = platform.menu_actions.get(index) { + // result = callback(action.as_ref()); + // } + // platform.validate_menu_command = Some(callback); + // } + // result + // } +} + +extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) { + unsafe { + let platform = get_foreground_platform(this); + let mut platform = platform.0.borrow_mut(); + if let Some(mut callback) = platform.will_open_menu.take() { + callback(); + platform.will_open_menu = Some(callback); + } + } +} + +unsafe fn ns_string(string: &str) -> id { + NSString::alloc(nil).init_str(string).autorelease() +} + +unsafe fn ns_url_to_path(url: id) -> Result { + let path: *mut c_char = msg_send![url, fileSystemRepresentation]; + if path.is_null() { + Err(anyhow!( + "url is not a file path: {}", + CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy() + )) + } else { + Ok(PathBuf::from(OsStr::from_bytes( + CStr::from_ptr(path).to_bytes(), + ))) + } +} + +mod security { + #![allow(non_upper_case_globals)] + use super::*; + + #[link(name = "Security", kind = "framework")] + extern "C" { + pub static kSecClass: CFStringRef; + pub static kSecClassInternetPassword: CFStringRef; + pub static kSecAttrServer: CFStringRef; + pub static kSecAttrAccount: CFStringRef; + pub static kSecValueData: CFStringRef; + pub static kSecReturnAttributes: CFStringRef; + pub static kSecReturnData: CFStringRef; + + pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; + pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus; + pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus; + pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; + } + + pub const errSecSuccess: OSStatus = 0; + pub const errSecUserCanceled: OSStatus = -128; + pub const errSecItemNotFound: OSStatus = -25300; +} + +#[cfg(test)] +mod tests { + use crate::ClipboardItem; + + use super::*; + + #[test] + fn test_clipboard() { + let platform = build_platform(); + assert_eq!(platform.read_from_clipboard(), None); + + let item = ClipboardItem::new("1".to_string()); + platform.write_to_clipboard(item.clone()); + assert_eq!(platform.read_from_clipboard(), Some(item)); + + let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]); + platform.write_to_clipboard(item.clone()); + assert_eq!(platform.read_from_clipboard(), Some(item)); + + let text_from_other_app = "text from other app"; + unsafe { + let bytes = NSData::dataWithBytes_length_( + nil, + text_from_other_app.as_ptr() as *const c_void, + text_from_other_app.len() as u64, + ); + platform + .0 + .borrow_mut() + .pasteboard + .setData_forType(bytes, NSPasteboardTypeString); + } + assert_eq!( + platform.read_from_clipboard(), + Some(ClipboardItem::new(text_from_other_app.to_string())) + ); + } + + fn build_platform() -> MacPlatform { + let mut platform = MacPlatform::new(); + platform.0.borrow_mut().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) }; + platform + } +} diff --git a/crates/gpui3/src/platform/mac/text_system.rs b/crates/gpui3/src/platform/mac/text_system.rs new file mode 100644 index 0000000000..d572b4461a --- /dev/null +++ b/crates/gpui3/src/platform/mac/text_system.rs @@ -0,0 +1,757 @@ +use crate::{ + point, px, size, Bounds, FontFeatures, FontId, FontMetrics, FontStyle, FontWeight, Glyph, + GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RasterizationOptions, Run, RunStyle, + Size, +}; +use cocoa::appkit::{CGFloat, CGPoint}; +use collections::HashMap; +use core_foundation::{ + array::CFIndex, + attributed_string::{CFAttributedStringRef, CFMutableAttributedString}, + base::{CFRange, TCFType}, + string::CFString, +}; +use core_graphics::{ + base::{kCGImageAlphaPremultipliedLast, CGGlyph}, + color_space::CGColorSpace, + context::CGContext, +}; +use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName}; +use font_kit::{ + handle::Handle, hinting::HintingOptions, metrics::Metrics, source::SystemSource, + sources::mem::MemSource, +}; +use parking_lot::RwLock; +use pathfinder_geometry::{ + rect::{RectF, RectI}, + transform2d::Transform2F, + vector::{Vector2F, Vector2I}, +}; +use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void, sync::Arc}; + +use super::open_type; + +#[allow(non_upper_case_globals)] +const kCGImageAlphaOnly: u32 = 7; + +pub struct MacTextSystem(RwLock); + +struct TextSystemState { + memory_source: MemSource, + system_source: SystemSource, + fonts: Vec, + font_ids_by_postscript_name: HashMap, + postscript_names_by_font_id: HashMap, +} + +impl MacTextSystem { + pub fn new() -> Self { + Self(RwLock::new(TextSystemState { + memory_source: MemSource::empty(), + system_source: SystemSource::new(), + fonts: Vec::new(), + font_ids_by_postscript_name: Default::default(), + postscript_names_by_font_id: Default::default(), + })) + } +} + +impl Default for MacTextSystem { + fn default() -> Self { + Self::new() + } +} + +impl PlatformTextSystem for MacTextSystem { + fn add_fonts(&self, fonts: &[Arc>]) -> anyhow::Result<()> { + self.0.write().add_fonts(fonts) + } + + fn all_families(&self) -> Vec { + self.0 + .read() + .system_source + .all_families() + .expect("core text should never return an error") + } + + fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result> { + self.0.write().load_family(name, features) + } + + fn select_font( + &self, + font_ids: &[FontId], + weight: FontWeight, + style: FontStyle, + ) -> anyhow::Result { + self.0.read().select_font(font_ids, weight, style) + } + + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + self.0.read().font_metrics(font_id) + } + + fn typographic_bounds( + &self, + font_id: FontId, + glyph_id: GlyphId, + ) -> anyhow::Result> { + self.0.read().typographic_bounds(font_id, glyph_id) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result> { + self.0.read().advance(font_id, glyph_id) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.0.read().glyph_for_char(font_id, ch) + } + + fn rasterize_glyph( + &self, + font_id: FontId, + font_size: f32, + glyph_id: GlyphId, + subpixel_shift: Point, + scale_factor: f32, + options: RasterizationOptions, + ) -> Option<(Bounds, Vec)> { + self.0.read().rasterize_glyph( + font_id, + font_size, + glyph_id, + subpixel_shift, + scale_factor, + options, + ) + } + + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[(usize, RunStyle)]) -> LineLayout { + self.0.write().layout_line(text, font_size, runs) + } + + fn wrap_line( + &self, + text: &str, + font_id: FontId, + font_size: Pixels, + width: Pixels, + ) -> Vec { + self.0.read().wrap_line(text, font_id, font_size, width) + } +} + +impl TextSystemState { + fn add_fonts(&mut self, fonts: &[Arc>]) -> anyhow::Result<()> { + self.memory_source.add_fonts( + fonts + .iter() + .map(|bytes| Handle::from_memory(bytes.clone(), 0)), + )?; + Ok(()) + } + + fn load_family(&mut self, name: &str, features: &FontFeatures) -> anyhow::Result> { + let mut font_ids = Vec::new(); + + let family = self + .memory_source + .select_family_by_name(name) + .or_else(|_| self.system_source.select_family_by_name(name))?; + for font in family.fonts() { + let mut font = font.load()?; + open_type::apply_features(&mut font, features); + let font_id = FontId(self.fonts.len()); + font_ids.push(font_id); + let postscript_name = font.postscript_name().unwrap(); + self.font_ids_by_postscript_name + .insert(postscript_name.clone(), font_id); + self.postscript_names_by_font_id + .insert(font_id, postscript_name); + self.fonts.push(font); + } + Ok(font_ids) + } + + fn select_font( + &self, + font_ids: &[FontId], + weight: FontWeight, + style: FontStyle, + ) -> anyhow::Result { + let candidates = font_ids + .iter() + .map(|font_id| self.fonts[font_id.0].properties()) + .collect::>(); + let idx = font_kit::matching::find_best_match( + &candidates, + &font_kit::properties::Properties { + style, + weight, + stretch: Default::default(), + }, + )?; + Ok(font_ids[idx]) + } + + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + self.fonts[font_id.0].metrics().into() + } + + fn typographic_bounds( + &self, + font_id: FontId, + glyph_id: GlyphId, + ) -> anyhow::Result> { + Ok(self.fonts[font_id.0] + .typographic_bounds(glyph_id.into())? + .into()) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result> { + Ok(self.fonts[font_id.0].advance(glyph_id.into())?.into()) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.fonts[font_id.0].glyph_for_char(ch).map(Into::into) + } + + fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId { + let postscript_name = requested_font.postscript_name(); + if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) { + *font_id + } else { + let font_id = FontId(self.fonts.len()); + self.font_ids_by_postscript_name + .insert(postscript_name.clone(), font_id); + self.postscript_names_by_font_id + .insert(font_id, postscript_name); + self.fonts + .push(font_kit::font::Font::from_core_graphics_font( + requested_font.copy_to_CGFont(), + )); + font_id + } + } + + fn is_emoji(&self, font_id: FontId) -> bool { + self.postscript_names_by_font_id + .get(&font_id) + .map_or(false, |postscript_name| { + postscript_name == "AppleColorEmoji" + }) + } + + fn rasterize_glyph( + &self, + font_id: FontId, + font_size: f32, + glyph_id: GlyphId, + subpixel_shift: Point, + scale_factor: f32, + options: RasterizationOptions, + ) -> Option<(Bounds, Vec)> { + let font = &self.fonts[font_id.0]; + let scale = Transform2F::from_scale(scale_factor); + let glyph_bounds = font + .raster_bounds( + glyph_id.into(), + font_size, + scale, + HintingOptions::None, + font_kit::canvas::RasterizationOptions::GrayscaleAa, + ) + .ok()?; + + if glyph_bounds.width() == 0 || glyph_bounds.height() == 0 { + None + } else { + // Make room for subpixel variants. + let subpixel_padding = subpixel_shift.map(|v| f32::from(v).ceil() as u32); + let cx_bounds = RectI::new( + glyph_bounds.origin(), + glyph_bounds.size() + Vector2I::from(subpixel_padding), + ); + + let mut bytes; + let cx; + match options { + RasterizationOptions::Alpha => { + bytes = vec![0; cx_bounds.width() as usize * cx_bounds.height() as usize]; + cx = CGContext::create_bitmap_context( + Some(bytes.as_mut_ptr() as *mut _), + cx_bounds.width() as usize, + cx_bounds.height() as usize, + 8, + cx_bounds.width() as usize, + &CGColorSpace::create_device_gray(), + kCGImageAlphaOnly, + ); + } + RasterizationOptions::Bgra => { + bytes = vec![0; cx_bounds.width() as usize * 4 * cx_bounds.height() as usize]; + cx = CGContext::create_bitmap_context( + Some(bytes.as_mut_ptr() as *mut _), + cx_bounds.width() as usize, + cx_bounds.height() as usize, + 8, + cx_bounds.width() as usize * 4, + &CGColorSpace::create_device_rgb(), + kCGImageAlphaPremultipliedLast, + ); + } + } + + // Move the origin to bottom left and account for scaling, this + // makes drawing text consistent with the font-kit's raster_bounds. + cx.translate( + -glyph_bounds.origin_x() as CGFloat, + (glyph_bounds.origin_y() + glyph_bounds.height()) as CGFloat, + ); + cx.scale(scale_factor as CGFloat, scale_factor as CGFloat); + + cx.set_allows_font_subpixel_positioning(true); + cx.set_should_subpixel_position_fonts(true); + cx.set_allows_font_subpixel_quantization(false); + cx.set_should_subpixel_quantize_fonts(false); + font.native_font() + .clone_with_font_size(font_size as CGFloat) + .draw_glyphs( + &[u32::from(glyph_id) as CGGlyph], + &[CGPoint::new( + (f32::from(subpixel_shift.x) / scale_factor) as CGFloat, + (f32::from(subpixel_shift.y) / scale_factor) as CGFloat, + )], + cx, + ); + + if let RasterizationOptions::Bgra = options { + // Convert from RGBA with premultiplied alpha to BGRA with straight alpha. + for pixel in bytes.chunks_exact_mut(4) { + pixel.swap(0, 2); + let a = pixel[3] as f32 / 255.; + pixel[0] = (pixel[0] as f32 / a) as u8; + pixel[1] = (pixel[1] as f32 / a) as u8; + pixel[2] = (pixel[2] as f32 / a) as u8; + } + } + + Some((cx_bounds.into(), bytes)) + } + } + + fn layout_line( + &mut self, + text: &str, + font_size: Pixels, + runs: &[(usize, RunStyle)], + ) -> LineLayout { + // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. + let mut string = CFMutableAttributedString::new(); + { + string.replace_str(&CFString::new(text), CFRange::init(0, 0)); + let utf16_line_len = string.char_len() as usize; + + let last_run: RefCell> = Default::default(); + let font_runs = runs + .iter() + .filter_map(|(len, style)| { + let mut last_run = last_run.borrow_mut(); + if let Some((last_len, last_font_id)) = last_run.as_mut() { + if style.font_id == *last_font_id { + *last_len += *len; + None + } else { + let result = (*last_len, *last_font_id); + *last_len = *len; + *last_font_id = style.font_id; + Some(result) + } + } else { + *last_run = Some((*len, style.font_id)); + None + } + }) + .chain(std::iter::from_fn(|| last_run.borrow_mut().take())); + + let mut ix_converter = StringIndexConverter::new(text); + for (run_len, font_id) in font_runs { + let utf8_end = ix_converter.utf8_ix + run_len; + let utf16_start = ix_converter.utf16_ix; + + if utf16_start >= utf16_line_len { + break; + } + + ix_converter.advance_to_utf8_ix(utf8_end); + let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len); + + let cf_range = + CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize); + let font = &self.fonts[font_id.0]; + unsafe { + string.set_attribute( + cf_range, + kCTFontAttributeName, + &font.native_font().clone_with_font_size(font_size.into()), + ); + } + + if utf16_end == utf16_line_len { + break; + } + } + } + + // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets. + let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef()); + + let mut runs = Vec::new(); + for run in line.glyph_runs().into_iter() { + let attributes = run.attributes().unwrap(); + let font = unsafe { + attributes + .get(kCTFontAttributeName) + .downcast::() + .unwrap() + }; + let font_id = self.id_for_native_font(font); + + let mut ix_converter = StringIndexConverter::new(text); + let mut glyphs = Vec::new(); + for ((glyph_id, position), glyph_utf16_ix) in run + .glyphs() + .iter() + .zip(run.positions().iter()) + .zip(run.string_indices().iter()) + { + let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap(); + ix_converter.advance_to_utf16_ix(glyph_utf16_ix); + glyphs.push(Glyph { + id: (*glyph_id).into(), + position: point(position.x as f32, position.y as f32).map(px), + index: ix_converter.utf8_ix, + is_emoji: self.is_emoji(font_id), + }); + } + + runs.push(Run { font_id, glyphs }) + } + + let typographic_bounds = line.get_typographic_bounds(); + LineLayout { + width: typographic_bounds.width.into(), + ascent: typographic_bounds.ascent.into(), + descent: typographic_bounds.descent.into(), + runs, + font_size, + len: text.len(), + } + } + + fn wrap_line( + &self, + text: &str, + font_id: FontId, + font_size: Pixels, + width: Pixels, + ) -> Vec { + let mut string = CFMutableAttributedString::new(); + string.replace_str(&CFString::new(text), CFRange::init(0, 0)); + let cf_range = CFRange::init(0, text.encode_utf16().count() as isize); + let font = &self.fonts[font_id.0]; + unsafe { + string.set_attribute( + cf_range, + kCTFontAttributeName, + &font.native_font().clone_with_font_size(font_size.into()), + ); + + let typesetter = CTTypesetterCreateWithAttributedString(string.as_concrete_TypeRef()); + let mut ix_converter = StringIndexConverter::new(text); + let mut break_indices = Vec::new(); + while ix_converter.utf8_ix < text.len() { + let utf16_len = CTTypesetterSuggestLineBreak( + typesetter, + ix_converter.utf16_ix as isize, + width.into(), + ) as usize; + ix_converter.advance_to_utf16_ix(ix_converter.utf16_ix + utf16_len); + if ix_converter.utf8_ix >= text.len() { + break; + } + break_indices.push(ix_converter.utf8_ix as usize); + } + break_indices + } + } +} + +#[derive(Clone)] +struct StringIndexConverter<'a> { + text: &'a str, + utf8_ix: usize, + utf16_ix: usize, +} + +impl<'a> StringIndexConverter<'a> { + fn new(text: &'a str) -> Self { + Self { + text, + utf8_ix: 0, + utf16_ix: 0, + } + } + + fn advance_to_utf8_ix(&mut self, utf8_target: usize) { + for (ix, c) in self.text[self.utf8_ix..].char_indices() { + if self.utf8_ix + ix >= utf8_target { + self.utf8_ix += ix; + return; + } + self.utf16_ix += c.len_utf16(); + } + self.utf8_ix = self.text.len(); + } + + fn advance_to_utf16_ix(&mut self, utf16_target: usize) { + for (ix, c) in self.text[self.utf8_ix..].char_indices() { + if self.utf16_ix >= utf16_target { + self.utf8_ix += ix; + return; + } + self.utf16_ix += c.len_utf16(); + } + self.utf8_ix = self.text.len(); + } +} + +#[repr(C)] +pub struct __CFTypesetter(c_void); + +pub type CTTypesetterRef = *const __CFTypesetter; + +#[link(name = "CoreText", kind = "framework")] +extern "C" { + fn CTTypesetterCreateWithAttributedString(string: CFAttributedStringRef) -> CTTypesetterRef; + + fn CTTypesetterSuggestLineBreak( + typesetter: CTTypesetterRef, + start_index: CFIndex, + width: f64, + ) -> CFIndex; +} + +impl From for FontMetrics { + fn from(metrics: Metrics) -> Self { + FontMetrics { + units_per_em: metrics.units_per_em, + ascent: metrics.ascent, + descent: metrics.descent, + line_gap: metrics.line_gap, + underline_position: metrics.underline_position, + underline_thickness: metrics.underline_thickness, + cap_height: metrics.cap_height, + x_height: metrics.x_height, + bounding_box: metrics.bounding_box.into(), + } + } +} + +impl From for Bounds { + fn from(rect: RectF) -> Self { + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } + } +} + +impl From for Bounds { + fn from(rect: RectI) -> Self { + Bounds { + origin: point(rect.origin_x() as u32, rect.origin_y() as u32), + size: size(rect.width() as u32, rect.height() as u32), + } + } +} + +impl From> for Vector2I { + fn from(size: Point) -> Self { + Vector2I::new(size.x as i32, size.y as i32) + } +} + +impl From for Size { + fn from(vec: Vector2F) -> Self { + size(vec.x(), vec.y()) + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::AppContext; +// use font_kit::properties::{Style, Weight}; +// use platform::FontSystem as _; + +// #[crate::test(self, retries = 5)] +// fn test_layout_str(_: &mut AppContext) { +// // This is failing intermittently on CI and we don't have time to figure it out +// let fonts = FontSystem::new(); +// let menlo = fonts.load_family("Menlo", &Default::default()).unwrap(); +// let menlo_regular = RunStyle { +// font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(), +// color: Default::default(), +// underline: Default::default(), +// }; +// let menlo_italic = RunStyle { +// font_id: fonts +// .select_font(&menlo, Properties::new().style(Style::Italic)) +// .unwrap(), +// color: Default::default(), +// underline: Default::default(), +// }; +// let menlo_bold = RunStyle { +// font_id: fonts +// .select_font(&menlo, Properties::new().weight(Weight::BOLD)) +// .unwrap(), +// color: Default::default(), +// underline: Default::default(), +// }; +// assert_ne!(menlo_regular, menlo_italic); +// assert_ne!(menlo_regular, menlo_bold); +// assert_ne!(menlo_italic, menlo_bold); + +// let line = fonts.layout_line( +// "hello world", +// 16.0, +// &[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)], +// ); +// assert_eq!(line.runs.len(), 3); +// assert_eq!(line.runs[0].font_id, menlo_bold.font_id); +// assert_eq!(line.runs[0].glyphs.len(), 2); +// assert_eq!(line.runs[1].font_id, menlo_italic.font_id); +// assert_eq!(line.runs[1].glyphs.len(), 4); +// assert_eq!(line.runs[2].font_id, menlo_regular.font_id); +// assert_eq!(line.runs[2].glyphs.len(), 5); +// } + +// #[test] +// fn test_glyph_offsets() -> anyhow::Result<()> { +// let fonts = FontSystem::new(); +// let zapfino = fonts.load_family("Zapfino", &Default::default())?; +// let zapfino_regular = RunStyle { +// font_id: fonts.select_font(&zapfino, &Properties::new())?, +// color: Default::default(), +// underline: Default::default(), +// }; +// let menlo = fonts.load_family("Menlo", &Default::default())?; +// let menlo_regular = RunStyle { +// font_id: fonts.select_font(&menlo, &Properties::new())?, +// color: Default::default(), +// underline: Default::default(), +// }; + +// let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈"; +// let line = fonts.layout_line( +// text, +// 16.0, +// &[ +// (9, zapfino_regular), +// (13, menlo_regular), +// (text.len() - 22, zapfino_regular), +// ], +// ); +// assert_eq!( +// line.runs +// .iter() +// .flat_map(|r| r.glyphs.iter()) +// .map(|g| g.index) +// .collect::>(), +// vec![0, 2, 4, 5, 7, 8, 9, 10, 14, 15, 16, 17, 21, 22, 23, 24, 26, 27, 28, 29, 36, 37], +// ); +// Ok(()) +// } + +// #[test] +// #[ignore] +// fn test_rasterize_glyph() { +// use std::{fs::File, io::BufWriter, path::Path}; + +// let fonts = FontSystem::new(); +// let font_ids = fonts.load_family("Fira Code", &Default::default()).unwrap(); +// let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap(); +// let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap(); + +// const VARIANTS: usize = 1; +// for i in 0..VARIANTS { +// let variant = i as f32 / VARIANTS as f32; +// let (bounds, bytes) = fonts +// .rasterize_glyph( +// font_id, +// 16.0, +// glyph_id, +// vec2f(variant, variant), +// 2., +// RasterizationOptions::Alpha, +// ) +// .unwrap(); + +// let name = format!("/Users/as-cii/Desktop/twog-{}.png", i); +// let path = Path::new(&name); +// let file = File::create(path).unwrap(); +// let w = &mut BufWriter::new(file); + +// let mut encoder = png::Encoder::new(w, bounds.width() as u32, bounds.height() as u32); +// encoder.set_color(png::ColorType::Grayscale); +// encoder.set_depth(png::BitDepth::Eight); +// let mut writer = encoder.write_header().unwrap(); +// writer.write_image_data(&bytes).unwrap(); +// } +// } + +// #[test] +// fn test_wrap_line() { +// let fonts = FontSystem::new(); +// let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap(); +// let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap(); + +// let line = "one two three four five\n"; +// let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0); +// assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]); + +// let line = "aaa Ξ±Ξ±Ξ± βœ‹βœ‹βœ‹ πŸŽ‰πŸŽ‰πŸŽ‰\n"; +// let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0); +// assert_eq!( +// wrap_boundaries, +// &["aaa Ξ±Ξ±Ξ± ".len(), "aaa Ξ±Ξ±Ξ± βœ‹βœ‹βœ‹ ".len(),] +// ); +// } + +// #[test] +// fn test_layout_line_bom_char() { +// let fonts = FontSystem::new(); +// let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap(); +// let style = RunStyle { +// font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(), +// color: Default::default(), +// underline: Default::default(), +// }; + +// let line = "\u{feff}"; +// let layout = fonts.layout_line(line, 16., &[(line.len(), style)]); +// assert_eq!(layout.len, line.len()); +// assert!(layout.runs.is_empty()); + +// let line = "a\u{feff}b"; +// let layout = fonts.layout_line(line, 16., &[(line.len(), style)]); +// assert_eq!(layout.len, line.len()); +// assert_eq!(layout.runs.len(), 1); +// assert_eq!(layout.runs[0].glyphs.len(), 2); +// assert_eq!(layout.runs[0].glyphs[0].id, 68); // a +// // There's no glyph for \u{feff} +// assert_eq!(layout.runs[0].glyphs[1].id, 69); // b +// } +// } diff --git a/crates/gpui3/src/platform/test.rs b/crates/gpui3/src/platform/test.rs index f052032591..cdfde5a56d 100644 --- a/crates/gpui3/src/platform/test.rs +++ b/crates/gpui3/src/platform/test.rs @@ -9,7 +9,51 @@ impl TestPlatform { } impl Platform for TestPlatform { - fn font_system(&self) -> std::sync::Arc { + fn executor(&self) -> std::rc::Rc { + todo!() + } + + fn text_system(&self) -> std::sync::Arc { + todo!() + } + + fn run(&self, on_finish_launching: Box) { + todo!() + } + + fn quit(&self) { + todo!() + } + + fn restart(&self) { + todo!() + } + + fn activate(&self, ignoring_other_apps: bool) { + todo!() + } + + fn hide(&self) { + todo!() + } + + fn hide_other_apps(&self) { + todo!() + } + + fn unhide_other_apps(&self) { + todo!() + } + + fn screens(&self) -> Vec> { + todo!() + } + + fn screen_by_id(&self, id: uuid::Uuid) -> Option> { + todo!() + } + + fn main_window(&self) -> Option { todo!() } @@ -21,7 +65,101 @@ impl Platform for TestPlatform { todo!() } - fn dispatcher(&self) -> std::sync::Arc { + fn open_url(&self, url: &str) { + todo!() + } + + fn on_open_urls(&self, callback: Box)>) { + todo!() + } + + fn prompt_for_paths( + &self, + options: crate::PathPromptOptions, + ) -> futures::channel::oneshot::Receiver>> { + todo!() + } + + fn prompt_for_new_path( + &self, + directory: &std::path::Path, + ) -> futures::channel::oneshot::Receiver> { + todo!() + } + + fn reveal_path(&self, path: &std::path::Path) { + todo!() + } + + fn on_become_active(&self, callback: Box) { + todo!() + } + + fn on_resign_active(&self, callback: Box) { + todo!() + } + + fn on_quit(&self, callback: Box) { + todo!() + } + + fn on_reopen(&self, callback: Box) { + todo!() + } + + fn on_event(&self, callback: Box bool>) { + todo!() + } + + fn os_name(&self) -> &'static str { + todo!() + } + + fn os_version(&self) -> anyhow::Result { + todo!() + } + + fn app_version(&self) -> anyhow::Result { + todo!() + } + + fn app_path(&self) -> anyhow::Result { + todo!() + } + + fn local_timezone(&self) -> time::UtcOffset { + todo!() + } + + fn path_for_auxiliary_executable(&self, name: &str) -> anyhow::Result { + todo!() + } + + fn set_cursor_style(&self, style: crate::CursorStyle) { + todo!() + } + + fn should_auto_hide_scrollbars(&self) -> bool { + todo!() + } + + fn write_to_clipboard(&self, item: crate::ClipboardItem) { + todo!() + } + + fn read_from_clipboard(&self) -> Option { + todo!() + } + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> anyhow::Result<()> { + todo!() + } + + fn read_credentials(&self, url: &str) -> anyhow::Result)>> { + todo!() + } + + fn delete_credentials(&self, url: &str) -> anyhow::Result<()> { todo!() } } diff --git a/crates/gpui3/src/scene.rs b/crates/gpui3/src/scene.rs index 4614e0f670..b25322a5e0 100644 --- a/crates/gpui3/src/scene.rs +++ b/crates/gpui3/src/scene.rs @@ -1,3 +1,5 @@ +use crate::{text::GlyphId, FontId}; + use super::{Bounds, Hsla, Pixels, Point}; use bytemuck::{Pod, Zeroable}; use plane_split::BspSplitter; @@ -39,7 +41,7 @@ impl Scene { #[derive(Clone, Debug)] pub enum Primitive { Quad(Quad), - Glyph(Glyph), + Glyph(RenderedGlyph), Underline(Underline), } @@ -58,7 +60,7 @@ impl Primitive { #[derive(Default)] pub struct PrimitiveBatch { pub quads: Vec, - pub glyphs: Vec, + pub glyphs: Vec, pub underlines: Vec, } @@ -96,26 +98,24 @@ unsafe impl Zeroable for Quad {} unsafe impl Pod for Quad {} -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub struct GlyphId(u32); - -#[derive(Clone, Debug)] -pub struct Glyph { - pub id: GlyphId, - pub position: Point, - pub color: Hsla, - pub index: usize, - pub is_emoji: bool, -} - impl From for Primitive { fn from(quad: Quad) -> Self { Primitive::Quad(quad) } } -impl From for Primitive { - fn from(glyph: Glyph) -> Self { +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct RenderedGlyph { + pub font_id: FontId, + pub font_size: f32, + pub id: GlyphId, + pub origin: Point, + pub color: Hsla, +} + +impl From for Primitive { + fn from(glyph: RenderedGlyph) -> Self { Primitive::Glyph(glyph) } } diff --git a/crates/gpui3/src/text.rs b/crates/gpui3/src/text.rs index e5a8da3c72..c088cdfd68 100644 --- a/crates/gpui3/src/text.rs +++ b/crates/gpui3/src/text.rs @@ -1,8 +1,7 @@ use crate::{black, px}; use super::{ - point, Bounds, FontId, Glyph, Hsla, Pixels, PlatformTextSystem, Point, UnderlineStyle, - WindowContext, + point, Bounds, FontId, Hsla, Pixels, PlatformTextSystem, Point, UnderlineStyle, WindowContext, }; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use smallvec::SmallVec; @@ -27,6 +26,35 @@ pub struct RunStyle { pub underline: Option, } +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct GlyphId(u32); + +impl From for u32 { + fn from(value: GlyphId) -> Self { + value.0 + } +} + +impl From for GlyphId { + fn from(num: u16) -> Self { + GlyphId(num as u32) + } +} + +impl From for GlyphId { + fn from(num: u32) -> Self { + GlyphId(num) + } +} + +#[derive(Clone, Debug)] +pub struct Glyph { + pub id: GlyphId, + pub position: Point, + pub index: usize, + pub is_emoji: bool, +} + impl TextLayoutCache { pub fn new(fonts: Arc) -> Self { Self { @@ -726,7 +754,7 @@ mod tests { let cx = AppContext::test(); let font_cache = cx.font_cache().clone(); - let font_system = cx.platform().font_system(); + let font_system = cx.platform().text_system(); let family = font_cache .load_family(&["Courier"], &Default::default()) .unwrap(); @@ -793,7 +821,7 @@ mod tests { fn test_wrap_shaped_line() { let cx = AppContext::test(); let font_cache = cx.font_cache().clone(); - let font_system = cx.platform().font_system(); + let font_system = cx.platform().text_system(); let text_layout_cache = TextLayoutCache::new(font_system.clone()); let family = font_cache diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index 5f9c38a5fb..fa66fd3701 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -13,22 +13,16 @@ test-support = [] [dependencies] anyhow.workspace = true -bytemuck = "1.14.0" derive_more.workspace = true gpui2 = { path = "../gpui2" } -itertools = "0.11.0" log.workspace = true -plane-split = "0.18.0" -raw-window-handle = "0.5.2" refineable = { path = "../refineable" } rust-embed.workspace = true serde.workspace = true settings = { path = "../settings" } simplelog = "0.9" -slotmap = "1.0.6" theme = { path = "../theme" } util = { path = "../util" } -wgpu = "0.17.0" [dev-dependencies] gpui2 = { path = "../gpui2", features = ["test-support"] }