Detailed changes
@@ -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",
@@ -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<Dispatcher>,
- fonts: Arc<FontSystem>,
+ fonts: Arc<TextSystem>,
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") },
@@ -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
@@ -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<RefCell<AppContext>>);
-use super::{
- platform::Platform,
- window::{Window, WindowHandle, WindowId},
- Context, LayoutId, Reference, View, WindowContext,
-};
+impl App {
+ pub fn new(platform: Rc<dyn Platform>) -> Self {
+ Self(Rc::new(RefCell::new(AppContext::new(platform))))
+ }
+}
pub struct AppContext {
platform: Rc<dyn Platform>,
@@ -21,7 +26,7 @@ pub struct AppContext {
impl AppContext {
pub fn new(platform: Rc<dyn Platform>) -> 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,
@@ -619,7 +619,7 @@ impl ExecutorEvent {
}
impl ForegroundExecutor {
- pub fn platform(dispatcher: Arc<dyn PlatformDispatcher>) -> Result<Self> {
+ pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Result<Self> {
if dispatcher.is_main_thread() {
Ok(Self::Platform {
dispatcher,
@@ -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"],
@@ -246,6 +246,12 @@ impl std::hash::Hash for Pixels {
}
}
+impl From<f64> for Pixels {
+ fn from(val: f64) -> Self {
+ Pixels(val as f32)
+ }
+}
+
unsafe impl bytemuck::Pod for Pixels {}
unsafe impl bytemuck::Zeroable for Pixels {}
@@ -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::*;
@@ -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::*;
+// #[cfg(target_os = "macos")]
+// pub fn current() -> Rc<dyn Platform> {
+// MacPlatform
+// }
+
pub trait Platform {
- fn dispatcher(&self) -> Arc<dyn PlatformDispatcher>;
- fn font_system(&self) -> Arc<dyn PlatformTextSystem>;
+ fn executor(&self) -> Rc<ForegroundExecutor>;
+ fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
+ fn run(&self, on_finish_launching: Box<dyn FnOnce()>);
+ 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<Rc<dyn PlatformScreen>>;
+ fn screen_by_id(&self, id: uuid::Uuid) -> Option<Rc<dyn PlatformScreen>>;
+ fn main_window(&self) -> Option<AnyWindowHandle>;
fn open_window(
&self,
handle: AnyWindowHandle,
options: WindowOptions,
) -> Box<dyn PlatformWindow>;
+ // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn PlatformWindow>;
+
+ fn open_url(&self, url: &str);
+ fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
+ fn prompt_for_paths(
+ &self,
+ options: PathPromptOptions,
+ ) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
+ fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
+ fn reveal_path(&self, path: &Path);
+
+ fn on_become_active(&self, callback: Box<dyn FnMut()>);
+ fn on_resign_active(&self, callback: Box<dyn FnMut()>);
+ fn on_quit(&self, callback: Box<dyn FnMut()>);
+ fn on_reopen(&self, callback: Box<dyn FnMut()>);
+ fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
+
+ fn os_name(&self) -> &'static str;
+ fn os_version(&self) -> Result<SemanticVersion>;
+ fn app_version(&self) -> Result<SemanticVersion>;
+ fn app_path(&self) -> Result<PathBuf>;
+ fn local_timezone(&self) -> UtcOffset;
+ fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
+
+ 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<ClipboardItem>;
+
+ fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
+ fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
+ fn delete_credentials(&self, url: &str) -> Result<()>;
}
pub trait PlatformScreen: Debug {
@@ -90,12 +152,9 @@ pub trait PlatformTextSystem: Send + Sync {
style: FontStyle,
) -> anyhow::Result<FontId>;
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
- fn typographic_bounds(
- &self,
- font_id: FontId,
- glyph_id: GlyphId,
- ) -> anyhow::Result<Bounds<Pixels>>;
- fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<Point<Pixels>>;
+ fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId)
+ -> anyhow::Result<Bounds<f32>>;
+ fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<Size<f32>>;
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
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<Self> {
+ 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<String>,
+}
+
+impl ClipboardItem {
+ pub fn new(text: String) -> Self {
+ Self {
+ text,
+ metadata: None,
+ }
+ }
+
+ pub fn with_metadata<T: Serialize>(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<T>(&self) -> Option<T>
+ 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()
+ }
+}
@@ -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 {
@@ -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<bool>,
+ 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;
+}
@@ -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<MacDispatcher>,
-// fonts: Arc<MacFontSystem>,
-// pasteboard: id,
-// text_hash_pasteboard_type: id,
-// metadata_pasteboard_type: id,
-// become_active: Option<Box<dyn FnMut()>>,
-// resign_active: Option<Box<dyn FnMut()>>,
-// reopen: Option<Box<dyn FnMut()>>,
-// quit: Option<Box<dyn FnMut()>>,
-// event: Option<Box<dyn FnMut(Event) -> bool>>,
-// // menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
-// validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
-// will_open_menu: Option<Box<dyn FnMut()>>,
-// open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
-// finish_launching: Option<Box<dyn FnOnce()>>,
-// // menu_actions: Vec<Box<dyn Action>>,
-// // foreground: Rc<executor::Foreground>,
-// }
-
-// impl MacForegroundPlatform {
-// pub fn new(foreground: Rc<executor::Foreground>) -> 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<Menu>,
-// delegate: id,
-// actions: &mut Vec<Box<dyn Action>>,
-// 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<Box<dyn Action>>,
-// // 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<dyn FnMut()>) {
-// // self.0.borrow_mut().become_active = Some(callback);
-// // }
-
-// // fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
-// // self.0.borrow_mut().resign_active = Some(callback);
-// // }
-
-// // fn on_quit(&self, callback: Box<dyn FnMut()>) {
-// // self.0.borrow_mut().quit = Some(callback);
-// // }
-
-// // fn on_reopen(&self, callback: Box<dyn FnMut()>) {
-// // self.0.borrow_mut().reopen = Some(callback);
-// // }
-
-// // fn on_event(&self, callback: Box<dyn FnMut(platform::Event) -> bool>) {
-// // self.0.borrow_mut().event = Some(callback);
-// // }
-
-// // fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
-// // self.0.borrow_mut().open_urls = Some(callback);
-// // }
-
-// // fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
-// // 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::<c_void>());
-// // (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
-// // }
-// // }
-
-// // fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
-// // self.0.borrow_mut().menu_command = Some(callback);
-// // }
-
-// // fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
-// // self.0.borrow_mut().will_open_menu = Some(callback);
-// // }
-
-// // fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
-// // self.0.borrow_mut().validate_menu_command = Some(callback);
-// // }
-
-// // fn set_menus(&self, menus: Vec<Menu>, 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<Option<Vec<PathBuf>>> {
-// // 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<Option<PathBuf>> {
-// // 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<dyn platform::Dispatcher> {
-// // self.dispatcher.clone()
-// // }
-
-// // fn fonts(&self) -> Arc<dyn platform::FontSystem> {
-// // 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<Rc<dyn platform::Screen>> {
-// // Screen::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>)
-// // }
-
-// // fn screens(&self) -> Vec<Rc<dyn platform::Screen>> {
-// // Screen::all()
-// // .into_iter()
-// // .map(|screen| Rc::new(screen) as Rc<_>)
-// // .collect()
-// // }
-
-// // fn open_window(
-// // &self,
-// // handle: AnyWindowHandle,
-// // options: platform::WindowOptions,
-// // executor: Rc<executor::Foreground>,
-// // ) -> Box<dyn platform::Window> {
-// // Box::new(MacWindow::open(handle, options, executor, self.fonts()))
-// // }
-
-// // fn main_window(&self) -> Option<AnyWindowHandle> {
-// // MacWindow::main_window()
-// // }
-
-// // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn platform::Window> {
-// // 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<ClipboardItem> {
-// // 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<Option<(String, Vec<u8>)>> {
-// // 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::<CFDictionary>()
-// // .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::<CFString>()
-// // .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::<CFData>()
-// // .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<PathBuf> {
-// // 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<PathBuf> {
-// // 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<platform::AppVersion> {
-// // 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<crate::platform::AppVersion> {
-// // 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::<Vec<_>>()
-// };
-// 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<PathBuf> {
-// 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<MacPlatformState>);
+
+pub struct MacPlatformState {
+ executor: Rc<ForegroundExecutor>,
+ text_system: Arc<MacTextSystem>,
+ pasteboard: id,
+ text_hash_pasteboard_type: id,
+ metadata_pasteboard_type: id,
+ become_active: Option<Box<dyn FnMut()>>,
+ resign_active: Option<Box<dyn FnMut()>>,
+ reopen: Option<Box<dyn FnMut()>>,
+ quit: Option<Box<dyn FnMut()>>,
+ event: Option<Box<dyn FnMut(Event) -> bool>>,
+ // menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
+ // validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
+ will_open_menu: Option<Box<dyn FnMut()>>,
+ open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
+ finish_launching: Option<Box<dyn FnOnce()>>,
+ // menu_actions: Vec<Box<dyn Action>>,
+}
+
+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<Menu>,
+ // delegate: id,
+ // actions: &mut Vec<Box<dyn Action>>,
+ // 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<Box<dyn Action>>,
+ // 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<ForegroundExecutor> {
+ self.0.borrow().executor.clone()
+ }
+
+ fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
+ self.0.borrow().text_system.clone()
+ }
+
+ fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
+ 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::<c_void>());
+ (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
+ }
+ }
+
+ 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<Rc<dyn PlatformScreen>> {
+ MacScreen::all()
+ .into_iter()
+ .map(|screen| Rc::new(screen) as Rc<_>)
+ .collect()
+ }
+
+ fn screen_by_id(&self, id: uuid::Uuid) -> Option<Rc<dyn PlatformScreen>> {
+ MacScreen::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>)
+ }
+
+ fn main_window(&self) -> Option<AnyWindowHandle> {
+ MacWindow::main_window()
+ }
+
+ // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn platform::Window> {
+ // Box::new(StatusItem::add(self.fonts()))
+ // }
+
+ fn open_window(
+ &self,
+ handle: AnyWindowHandle,
+ options: WindowOptions,
+ ) -> Box<dyn PlatformWindow> {
+ 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<dyn FnMut(Vec<String>)>) {
+ self.0.borrow_mut().open_urls = Some(callback);
+ }
+
+ fn prompt_for_paths(
+ &self,
+ options: PathPromptOptions,
+ ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+ 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<Option<PathBuf>> {
+ 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<dyn FnMut()>) {
+ self.0.borrow_mut().become_active = Some(callback);
+ }
+
+ fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
+ self.0.borrow_mut().resign_active = Some(callback);
+ }
+
+ fn on_quit(&self, callback: Box<dyn FnMut()>) {
+ self.0.borrow_mut().quit = Some(callback);
+ }
+
+ fn on_reopen(&self, callback: Box<dyn FnMut()>) {
+ self.0.borrow_mut().reopen = Some(callback);
+ }
+
+ fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>) {
+ self.0.borrow_mut().event = Some(callback);
+ }
+
+ fn os_name(&self) -> &'static str {
+ "macOS"
+ }
+
+ fn os_version(&self) -> Result<SemanticVersion> {
+ 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<SemanticVersion> {
+ 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<PathBuf> {
+ 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<PathBuf> {
+ 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<ClipboardItem> {
+ 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<dyn FnMut(&dyn Action)>) {
+ // self.0.borrow_mut().menu_command = Some(callback);
+ // }
+
+ // fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
+ // self.0.borrow_mut().will_open_menu = Some(callback);
+ // }
+
+ // fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
+ // self.0.borrow_mut().validate_menu_command = Some(callback);
+ // }
+
+ // fn set_menus(&self, menus: Vec<Menu>, 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<Option<(String, Vec<u8>)>> {
+ 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::<CFDictionary>()
+ .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::<CFString>()
+ .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::<CFData>()
+ .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::<Vec<_>>()
+ };
+ 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<PathBuf> {
+ 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
+ }
+}
@@ -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<TextSystemState>);
+
+struct TextSystemState {
+ memory_source: MemSource,
+ system_source: SystemSource,
+ fonts: Vec<font_kit::font::Font>,
+ font_ids_by_postscript_name: HashMap<String, FontId>,
+ postscript_names_by_font_id: HashMap<FontId, String>,
+}
+
+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<Vec<u8>>]) -> anyhow::Result<()> {
+ self.0.write().add_fonts(fonts)
+ }
+
+ fn all_families(&self) -> Vec<String> {
+ 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<Vec<FontId>> {
+ self.0.write().load_family(name, features)
+ }
+
+ fn select_font(
+ &self,
+ font_ids: &[FontId],
+ weight: FontWeight,
+ style: FontStyle,
+ ) -> anyhow::Result<FontId> {
+ 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<Bounds<f32>> {
+ self.0.read().typographic_bounds(font_id, glyph_id)
+ }
+
+ fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<Size<f32>> {
+ self.0.read().advance(font_id, glyph_id)
+ }
+
+ fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
+ 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<Pixels>,
+ scale_factor: f32,
+ options: RasterizationOptions,
+ ) -> Option<(Bounds<u32>, Vec<u8>)> {
+ 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<usize> {
+ self.0.read().wrap_line(text, font_id, font_size, width)
+ }
+}
+
+impl TextSystemState {
+ fn add_fonts(&mut self, fonts: &[Arc<Vec<u8>>]) -> 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<Vec<FontId>> {
+ 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<FontId> {
+ let candidates = font_ids
+ .iter()
+ .map(|font_id| self.fonts[font_id.0].properties())
+ .collect::<Vec<_>>();
+ 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<Bounds<f32>> {
+ Ok(self.fonts[font_id.0]
+ .typographic_bounds(glyph_id.into())?
+ .into())
+ }
+
+ fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<Size<f32>> {
+ Ok(self.fonts[font_id.0].advance(glyph_id.into())?.into())
+ }
+
+ fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
+ 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<Pixels>,
+ scale_factor: f32,
+ options: RasterizationOptions,
+ ) -> Option<(Bounds<u32>, Vec<u8>)> {
+ 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<Option<(usize, FontId)>> = 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::<CTFont>()
+ .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<usize> {
+ 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<Metrics> 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<RectF> for Bounds<f32> {
+ fn from(rect: RectF) -> Self {
+ Bounds {
+ origin: point(rect.origin_x(), rect.origin_y()),
+ size: size(rect.width(), rect.height()),
+ }
+ }
+}
+
+impl From<RectI> for Bounds<u32> {
+ 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<Point<u32>> for Vector2I {
+ fn from(size: Point<u32>) -> Self {
+ Vector2I::new(size.x as i32, size.y as i32)
+ }
+}
+
+impl From<Vector2F> for Size<f32> {
+ 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<_>>(),
+// 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
+// }
+// }
@@ -9,7 +9,51 @@ impl TestPlatform {
}
impl Platform for TestPlatform {
- fn font_system(&self) -> std::sync::Arc<dyn crate::PlatformTextSystem> {
+ fn executor(&self) -> std::rc::Rc<crate::ForegroundExecutor> {
+ todo!()
+ }
+
+ fn text_system(&self) -> std::sync::Arc<dyn crate::PlatformTextSystem> {
+ todo!()
+ }
+
+ fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
+ 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<std::rc::Rc<dyn crate::PlatformScreen>> {
+ todo!()
+ }
+
+ fn screen_by_id(&self, id: uuid::Uuid) -> Option<std::rc::Rc<dyn crate::PlatformScreen>> {
+ todo!()
+ }
+
+ fn main_window(&self) -> Option<crate::AnyWindowHandle> {
todo!()
}
@@ -21,7 +65,101 @@ impl Platform for TestPlatform {
todo!()
}
- fn dispatcher(&self) -> std::sync::Arc<dyn crate::PlatformDispatcher> {
+ fn open_url(&self, url: &str) {
+ todo!()
+ }
+
+ fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
+ todo!()
+ }
+
+ fn prompt_for_paths(
+ &self,
+ options: crate::PathPromptOptions,
+ ) -> futures::channel::oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
+ todo!()
+ }
+
+ fn prompt_for_new_path(
+ &self,
+ directory: &std::path::Path,
+ ) -> futures::channel::oneshot::Receiver<Option<std::path::PathBuf>> {
+ todo!()
+ }
+
+ fn reveal_path(&self, path: &std::path::Path) {
+ todo!()
+ }
+
+ fn on_become_active(&self, callback: Box<dyn FnMut()>) {
+ todo!()
+ }
+
+ fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
+ todo!()
+ }
+
+ fn on_quit(&self, callback: Box<dyn FnMut()>) {
+ todo!()
+ }
+
+ fn on_reopen(&self, callback: Box<dyn FnMut()>) {
+ todo!()
+ }
+
+ fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
+ todo!()
+ }
+
+ fn os_name(&self) -> &'static str {
+ todo!()
+ }
+
+ fn os_version(&self) -> anyhow::Result<crate::SemanticVersion> {
+ todo!()
+ }
+
+ fn app_version(&self) -> anyhow::Result<crate::SemanticVersion> {
+ todo!()
+ }
+
+ fn app_path(&self) -> anyhow::Result<std::path::PathBuf> {
+ todo!()
+ }
+
+ fn local_timezone(&self) -> time::UtcOffset {
+ todo!()
+ }
+
+ fn path_for_auxiliary_executable(&self, name: &str) -> anyhow::Result<std::path::PathBuf> {
+ 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<crate::ClipboardItem> {
+ todo!()
+ }
+
+ fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> anyhow::Result<()> {
+ todo!()
+ }
+
+ fn read_credentials(&self, url: &str) -> anyhow::Result<Option<(String, Vec<u8>)>> {
+ todo!()
+ }
+
+ fn delete_credentials(&self, url: &str) -> anyhow::Result<()> {
todo!()
}
}
@@ -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<Quad>,
- pub glyphs: Vec<Glyph>,
+ pub glyphs: Vec<RenderedGlyph>,
pub underlines: Vec<Underline>,
}
@@ -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<Pixels>,
- pub color: Hsla,
- pub index: usize,
- pub is_emoji: bool,
-}
-
impl From<Quad> for Primitive {
fn from(quad: Quad) -> Self {
Primitive::Quad(quad)
}
}
-impl From<Glyph> 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<Pixels>,
+ pub color: Hsla,
+}
+
+impl From<RenderedGlyph> for Primitive {
+ fn from(glyph: RenderedGlyph) -> Self {
Primitive::Glyph(glyph)
}
}
@@ -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<UnderlineStyle>,
}
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+pub struct GlyphId(u32);
+
+impl From<GlyphId> for u32 {
+ fn from(value: GlyphId) -> Self {
+ value.0
+ }
+}
+
+impl From<u16> for GlyphId {
+ fn from(num: u16) -> Self {
+ GlyphId(num as u32)
+ }
+}
+
+impl From<u32> for GlyphId {
+ fn from(num: u32) -> Self {
+ GlyphId(num)
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct Glyph {
+ pub id: GlyphId,
+ pub position: Point<Pixels>,
+ pub index: usize,
+ pub is_emoji: bool,
+}
+
impl TextLayoutCache {
pub fn new(fonts: Arc<dyn PlatformTextSystem>) -> 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
@@ -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"] }