logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
[package]
name = "gpui_macos"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/gpui_macos.rs"
[features]
default = ["gpui/default"]
test-support = ["gpui/test-support"]
runtime_shaders = []
font-kit = ["dep:font-kit"]
screen-capture = ["gpui/screen-capture"]
[dependencies]
gpui.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
anyhow.workspace = true
async-task = "4.7"
block = "0.1"
cocoa.workspace = true
collections.workspace = true
core-foundation.workspace = true
core-foundation-sys.workspace = true
core-graphics = "0.24"
core-text = "21"
core-video.workspace = true
ctor.workspace = true
derive_more.workspace = true
dispatch2 = "0.3.1"
etagere = "0.2"
# WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "94b0f28166665e8fd2f53ff6d268a14955c82269", package = "zed-font-kit", version = "0.14.1-zed", optional = true }
foreign-types = "0.5"
futures.workspace = true
image.workspace = true
itertools.workspace = true
libc.workspace = true
log.workspace = true
mach2.workspace = true
media.workspace = true
metal.workspace = true
objc.workspace = true
objc2-app-kit.workspace = true
parking_lot.workspace = true
pathfinder_geometry = "0.5"
raw-window-handle = "0.6"
semver.workspace = true
smallvec.workspace = true
strum.workspace = true
util.workspace = true
uuid.workspace = true
[target.'cfg(target_os = "macos")'.build-dependencies]
cbindgen = { version = "0.28.0", default-features = false }
gpui.workspace = true
# When this crate is itself being tested (cargo test -p gpui_macos), its own
# cfg(test) flag enables impls of test-only traits like PlatformHeadlessRenderer
# and PlatformWindow::render_to_image. Those traits/methods only exist in gpui
# when gpui's `test-support` feature is on, so we have to turn that feature on
# as a dev-dependency. The `cfg(test)` flag of a dependent crate doesn't
# propagate to its dependencies, but dev-dependencies do, so this is the
# correct way to enable the feature exactly when needed.
[target.'cfg(target_os = "macos")'.dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -0,0 +1 @@
../../LICENSE-APACHE

174
crates/gpui_macos/build.rs Normal file
View File

@@ -0,0 +1,174 @@
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
fn main() {
#[cfg(target_os = "macos")]
macos_build::run();
}
#[cfg(target_os = "macos")]
mod macos_build {
use std::{
env,
path::{Path, PathBuf},
};
use cbindgen::Config;
pub fn run() {
let header_path = generate_shader_bindings();
#[cfg(feature = "runtime_shaders")]
emit_stitched_shaders(&header_path);
#[cfg(not(feature = "runtime_shaders"))]
compile_metal_shaders(&header_path);
}
fn generate_shader_bindings() -> PathBuf {
let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
let gpui_dir = find_gpui_crate_dir();
let mut config = Config {
include_guard: Some("SCENE_H".into()),
language: cbindgen::Language::C,
no_includes: true,
..Default::default()
};
config.export.include.extend([
"Bounds".into(),
"Corners".into(),
"Edges".into(),
"Size".into(),
"Pixels".into(),
"PointF".into(),
"Hsla".into(),
"ContentMask".into(),
"Uniforms".into(),
"AtlasTile".into(),
"PathRasterizationInputIndex".into(),
"PathVertex_ScaledPixels".into(),
"PathRasterizationVertex".into(),
"ShadowInputIndex".into(),
"Shadow".into(),
"QuadInputIndex".into(),
"Underline".into(),
"UnderlineInputIndex".into(),
"Quad".into(),
"BorderStyle".into(),
"SpriteInputIndex".into(),
"MonochromeSprite".into(),
"PolychromeSprite".into(),
"PathSprite".into(),
"SurfaceInputIndex".into(),
"SurfaceBounds".into(),
"TransformationMatrix".into(),
]);
config.no_includes = true;
config.enumeration.prefix_with_name = true;
let mut builder = cbindgen::Builder::new();
let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
// Source files from gpui that define types used in shaders
let gpui_src_paths = [
gpui_dir.join("src/scene.rs"),
gpui_dir.join("src/geometry.rs"),
gpui_dir.join("src/color.rs"),
gpui_dir.join("src/window.rs"),
gpui_dir.join("src/platform.rs"),
];
// Source files from this crate
let local_src_paths = [crate_dir.join("src/metal_renderer.rs")];
for src_path in gpui_src_paths.iter().chain(local_src_paths.iter()) {
println!("cargo:rerun-if-changed={}", src_path.display());
builder = builder.with_src(src_path);
}
builder
.with_config(config)
.generate()
.expect("Unable to generate bindings")
.write_to_file(&output_path);
output_path
}
/// Locate the gpui crate directory relative to this crate.
fn find_gpui_crate_dir() -> PathBuf {
gpui::GPUI_MANIFEST_DIR.into()
}
/// To enable runtime compilation, we need to "stitch" the shaders file with the generated header
/// so that it is self-contained.
#[cfg(feature = "runtime_shaders")]
fn emit_stitched_shaders(header_path: &Path) {
fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result<PathBuf> {
let header_contents = std::fs::read_to_string(header)?;
let shader_contents = std::fs::read_to_string(shader_path)?;
let stitched_contents = format!("{header_contents}\n{shader_contents}");
let out_path =
PathBuf::from(env::var("OUT_DIR").unwrap()).join("stitched_shaders.metal");
std::fs::write(&out_path, stitched_contents)?;
Ok(out_path)
}
let shader_source_path = "./src/shaders.metal";
let shader_path = PathBuf::from(shader_source_path);
stitch_header(header_path, &shader_path).unwrap();
println!("cargo:rerun-if-changed={}", &shader_source_path);
}
#[cfg(not(feature = "runtime_shaders"))]
fn compile_metal_shaders(header_path: &Path) {
use std::process::{self, Command};
let shader_path = "./src/shaders.metal";
let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
let metallib_output_path =
PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
println!("cargo:rerun-if-changed={}", shader_path);
let output = Command::new("xcrun")
.args([
"-sdk",
"macosx",
"metal",
"-gline-tables-only",
"-mmacosx-version-min=10.15.7",
"-MO",
"-c",
shader_path,
"-include",
(header_path.to_str().unwrap()),
"-o",
])
.arg(&air_output_path)
.output()
.unwrap();
if !output.status.success() {
println!(
"cargo::error=metal shader compilation failed:\n{}",
String::from_utf8_lossy(&output.stderr)
);
process::exit(1);
}
let output = Command::new("xcrun")
.args(["-sdk", "macosx", "metallib"])
.arg(air_output_path)
.arg("-o")
.arg(metallib_output_path)
.output()
.unwrap();
if !output.status.success() {
println!(
"cargo::error=metallib compilation failed:\n{}",
String::from_utf8_lossy(&output.stderr)
);
process::exit(1);
}
}
}

View File

@@ -0,0 +1,201 @@
use dispatch2::{DispatchQueue, DispatchQueueGlobalPriority, DispatchTime, GlobalQueueIdentifier};
use gpui::{
GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, RunnableMeta, RunnableVariant, TaskTiming,
ThreadTaskTimings, add_task_timing,
};
use mach2::{
kern_return::KERN_SUCCESS,
mach_time::mach_timebase_info_data_t,
thread_policy::{
THREAD_EXTENDED_POLICY, THREAD_EXTENDED_POLICY_COUNT, THREAD_PRECEDENCE_POLICY,
THREAD_PRECEDENCE_POLICY_COUNT, THREAD_TIME_CONSTRAINT_POLICY,
THREAD_TIME_CONSTRAINT_POLICY_COUNT, thread_extended_policy_data_t,
thread_precedence_policy_data_t, thread_time_constraint_policy_data_t,
},
};
use util::ResultExt;
use async_task::Runnable;
use objc::{
class, msg_send,
runtime::{BOOL, YES},
sel, sel_impl,
};
use std::{
ffi::c_void,
ptr::NonNull,
time::{Duration, Instant},
};
pub(crate) struct MacDispatcher;
impl MacDispatcher {
pub fn new() -> Self {
Self
}
}
impl PlatformDispatcher for MacDispatcher {
fn get_all_timings(&self) -> Vec<ThreadTaskTimings> {
let global_timings = GLOBAL_THREAD_TIMINGS.lock();
ThreadTaskTimings::convert(&global_timings)
}
fn get_current_thread_timings(&self) -> ThreadTaskTimings {
gpui::profiler::get_current_thread_task_timings()
}
fn is_main_thread(&self) -> bool {
let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] };
is_main_thread == YES
}
fn dispatch(&self, runnable: RunnableVariant, priority: Priority) {
let context = runnable.into_raw().as_ptr() as *mut c_void;
let queue_priority = match priority {
Priority::RealtimeAudio => {
panic!("RealtimeAudio priority should use spawn_realtime, not dispatch")
}
Priority::High => DispatchQueueGlobalPriority::High,
Priority::Medium => DispatchQueueGlobalPriority::Default,
Priority::Low => DispatchQueueGlobalPriority::Low,
};
unsafe {
DispatchQueue::global_queue(GlobalQueueIdentifier::Priority(queue_priority))
.exec_async_f(context, trampoline);
}
}
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) {
let context = runnable.into_raw().as_ptr() as *mut c_void;
unsafe {
DispatchQueue::main().exec_async_f(context, trampoline);
}
}
fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) {
let context = runnable.into_raw().as_ptr() as *mut c_void;
let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::Priority(
DispatchQueueGlobalPriority::High,
));
let when = DispatchTime::NOW.time(duration.as_nanos() as i64);
unsafe {
DispatchQueue::exec_after_f(when, &queue, context, trampoline);
}
}
fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>) {
std::thread::spawn(move || {
set_audio_thread_priority().log_err();
f();
});
}
}
fn set_audio_thread_priority() -> anyhow::Result<()> {
// https://chromium.googlesource.com/chromium/chromium/+/master/base/threading/platform_thread_mac.mm#93
// SAFETY: always safe to call
let thread_id = unsafe { libc::pthread_self() };
// SAFETY: thread_id is a valid thread id
let thread_id = unsafe { libc::pthread_mach_thread_np(thread_id) };
// Fixed priority thread
let mut policy = thread_extended_policy_data_t { timeshare: 0 };
// SAFETY: thread_id is a valid thread id
// SAFETY: thread_extended_policy_data_t is passed as THREAD_EXTENDED_POLICY
let result = unsafe {
mach2::thread_policy::thread_policy_set(
thread_id,
THREAD_EXTENDED_POLICY,
&mut policy as *mut _ as *mut _,
THREAD_EXTENDED_POLICY_COUNT,
)
};
if result != KERN_SUCCESS {
anyhow::bail!("failed to set thread extended policy");
}
// relatively high priority
let mut precedence = thread_precedence_policy_data_t { importance: 63 };
// SAFETY: thread_id is a valid thread id
// SAFETY: thread_precedence_policy_data_t is passed as THREAD_PRECEDENCE_POLICY
let result = unsafe {
mach2::thread_policy::thread_policy_set(
thread_id,
THREAD_PRECEDENCE_POLICY,
&mut precedence as *mut _ as *mut _,
THREAD_PRECEDENCE_POLICY_COUNT,
)
};
if result != KERN_SUCCESS {
anyhow::bail!("failed to set thread precedence policy");
}
const GUARANTEED_AUDIO_DUTY_CYCLE: f32 = 0.75;
const MAX_AUDIO_DUTY_CYCLE: f32 = 0.85;
// ~128 frames @ 44.1KHz
const TIME_QUANTUM: f32 = 2.9;
const AUDIO_TIME_NEEDED: f32 = GUARANTEED_AUDIO_DUTY_CYCLE * TIME_QUANTUM;
const MAX_TIME_ALLOWED: f32 = MAX_AUDIO_DUTY_CYCLE * TIME_QUANTUM;
let mut timebase_info = mach_timebase_info_data_t { numer: 0, denom: 0 };
// SAFETY: timebase_info is a valid pointer to a mach_timebase_info_data_t struct
unsafe { mach2::mach_time::mach_timebase_info(&mut timebase_info) };
let ms_to_abs_time = ((timebase_info.denom as f32) / (timebase_info.numer as f32)) * 1000000f32;
let mut time_constraints = thread_time_constraint_policy_data_t {
period: (TIME_QUANTUM * ms_to_abs_time) as u32,
computation: (AUDIO_TIME_NEEDED * ms_to_abs_time) as u32,
constraint: (MAX_TIME_ALLOWED * ms_to_abs_time) as u32,
preemptible: 0,
};
// SAFETY: thread_id is a valid thread id
// SAFETY: thread_precedence_pthread_time_constraint_policy_data_t is passed as THREAD_TIME_CONSTRAINT_POLICY
let result = unsafe {
mach2::thread_policy::thread_policy_set(
thread_id,
THREAD_TIME_CONSTRAINT_POLICY,
&mut time_constraints as *mut _ as *mut _,
THREAD_TIME_CONSTRAINT_POLICY_COUNT,
)
};
if result != KERN_SUCCESS {
anyhow::bail!("failed to set thread time constraint policy");
}
Ok(())
}
extern "C" fn trampoline(context: *mut c_void) {
let runnable =
unsafe { Runnable::<RunnableMeta>::from_raw(NonNull::new_unchecked(context as *mut ())) };
let location = runnable.metadata().location;
let start = Instant::now();
let mut timing = TaskTiming {
location,
start,
end: None,
};
add_task_timing(timing);
runnable.run();
timing.end = Some(Instant::now());
add_task_timing(timing);
}

View File

@@ -0,0 +1,169 @@
use crate::ns_string;
use anyhow::Result;
use cocoa::{
appkit::NSScreen,
base::{id, nil},
foundation::{NSArray, NSDictionary},
};
use core_foundation::base::CFRelease;
use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef};
use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList};
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, point, px, size};
use objc::{msg_send, sel, sel_impl};
use uuid::Uuid;
#[derive(Debug)]
pub(crate) struct MacDisplay(pub(crate) CGDirectDisplayID);
unsafe impl Send for MacDisplay {}
impl MacDisplay {
/// Get the screen with the given [`DisplayId`].
pub fn find_by_id(id: DisplayId) -> Option<Self> {
Self::all().find(|screen| screen.id() == id)
}
/// Get the primary screen - the one with the menu bar, and whose bottom left
/// corner is at the origin of the AppKit coordinate system.
pub fn primary() -> Self {
// Instead of iterating through all active systems displays via `all()` we use the first
// NSScreen and gets its CGDirectDisplayID, because we can't be sure that `CGGetActiveDisplayList`
// will always return a list of active displays (machine might be sleeping).
//
// The following is what Chromium does too:
//
// https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/ui/display/mac/screen_mac.mm#56
unsafe {
let screens = NSScreen::screens(nil);
let screen = cocoa::foundation::NSArray::objectAtIndex(screens, 0);
let device_description = NSScreen::deviceDescription(screen);
let screen_number_key: id = ns_string("NSScreenNumber");
let screen_number = device_description.objectForKey_(screen_number_key);
let screen_number: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue];
Self(screen_number)
}
}
/// Obtains an iterator over all currently active system displays.
pub fn all() -> impl Iterator<Item = Self> {
unsafe {
// We're assuming there aren't more than 32 displays connected to the system.
let mut displays = Vec::with_capacity(32);
let mut display_count = 0;
let result = CGGetActiveDisplayList(
displays.capacity() as u32,
displays.as_mut_ptr(),
&mut display_count,
);
if result == 0 {
displays.set_len(display_count as usize);
displays.into_iter().map(MacDisplay)
} else {
panic!("Failed to get active display list. Result: {result}");
}
}
}
}
#[link(name = "ApplicationServices", kind = "framework")]
unsafe extern "C" {
fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
}
impl PlatformDisplay for MacDisplay {
fn id(&self) -> DisplayId {
DisplayId::new(self.0 as u64)
}
fn uuid(&self) -> Result<Uuid> {
let cfuuid = unsafe { CGDisplayCreateUUIDFromDisplayID(self.0 as CGDirectDisplayID) };
anyhow::ensure!(
!cfuuid.is_null(),
"AppKit returned a null from CGDisplayCreateUUIDFromDisplayID"
);
let bytes = unsafe { CFUUIDGetUUIDBytes(cfuuid) };
unsafe { CFRelease(cfuuid as _) };
Ok(Uuid::from_bytes([
bytes.byte0,
bytes.byte1,
bytes.byte2,
bytes.byte3,
bytes.byte4,
bytes.byte5,
bytes.byte6,
bytes.byte7,
bytes.byte8,
bytes.byte9,
bytes.byte10,
bytes.byte11,
bytes.byte12,
bytes.byte13,
bytes.byte14,
bytes.byte15,
]))
}
fn bounds(&self) -> Bounds<Pixels> {
unsafe {
// CGDisplayBounds is in "global display" coordinates, where 0 is
// the top left of the primary display.
let bounds = CGDisplayBounds(self.0);
Bounds {
origin: Default::default(),
size: size(px(bounds.size.width as f32), px(bounds.size.height as f32)),
}
}
}
fn visible_bounds(&self) -> Bounds<Pixels> {
unsafe {
let dominated_screen = self.get_nsscreen();
if dominated_screen == nil {
return self.bounds();
}
let screen_frame = NSScreen::frame(dominated_screen);
let visible_frame = NSScreen::visibleFrame(dominated_screen);
// Convert from bottom-left origin (AppKit) to top-left origin
let origin_y =
screen_frame.size.height - visible_frame.origin.y - visible_frame.size.height
+ screen_frame.origin.y;
Bounds {
origin: point(
px(visible_frame.origin.x as f32 - screen_frame.origin.x as f32),
px(origin_y as f32),
),
size: size(
px(visible_frame.size.width as f32),
px(visible_frame.size.height as f32),
),
}
}
}
}
impl MacDisplay {
/// Find the NSScreen corresponding to this display
unsafe fn get_nsscreen(&self) -> id {
let screens = unsafe { NSScreen::screens(nil) };
let count = unsafe { NSArray::count(screens) };
let screen_number_key: id = unsafe { ns_string("NSScreenNumber") };
for i in 0..count {
let screen = unsafe { NSArray::objectAtIndex(screens, i) };
let device_description = unsafe { NSScreen::deviceDescription(screen) };
let screen_number = unsafe { device_description.objectForKey_(screen_number_key) };
let screen_id: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue];
if screen_id == self.0 {
return screen;
}
}
nil
}
}

View File

@@ -0,0 +1,266 @@
use anyhow::Result;
use core_graphics::display::CGDirectDisplayID;
use dispatch2::{
_dispatch_source_type_data_add, DispatchObject, DispatchQueue, DispatchRetained, DispatchSource,
};
use std::ffi::c_void;
use util::ResultExt;
pub struct DisplayLink {
display_link: Option<sys::DisplayLink>,
frame_requests: DispatchRetained<DispatchSource>,
}
impl DisplayLink {
pub fn new(
display_id: CGDirectDisplayID,
data: *mut c_void,
callback: extern "C" fn(*mut c_void),
) -> Result<DisplayLink> {
unsafe extern "C" fn display_link_callback(
_display_link_out: *mut sys::CVDisplayLink,
_current_time: *const sys::CVTimeStamp,
_output_time: *const sys::CVTimeStamp,
_flags_in: i64,
_flags_out: *mut i64,
frame_requests: *mut c_void,
) -> i32 {
unsafe {
let frame_requests = &*(frame_requests as *const DispatchSource);
frame_requests.merge_data(1);
0
}
}
unsafe {
let frame_requests = DispatchSource::new(
&raw const _dispatch_source_type_data_add as *mut _,
0,
0,
Some(DispatchQueue::main()),
);
frame_requests.set_context(data);
frame_requests.set_event_handler_f(callback);
frame_requests.resume();
let display_link = sys::DisplayLink::new(
display_id,
display_link_callback,
&*frame_requests as *const DispatchSource as *mut c_void,
)?;
Ok(Self {
display_link: Some(display_link),
frame_requests,
})
}
}
pub fn start(&mut self) -> Result<()> {
unsafe {
self.display_link.as_mut().unwrap().start()?;
}
Ok(())
}
pub fn stop(&mut self) -> Result<()> {
unsafe {
self.display_link.as_mut().unwrap().stop()?;
}
Ok(())
}
}
impl Drop for DisplayLink {
fn drop(&mut self) {
self.stop().log_err();
// We see occasional segfaults on the CVDisplayLink thread.
//
// It seems possible that this happens because CVDisplayLinkRelease releases the CVDisplayLink
// on the main thread immediately, but the background thread that CVDisplayLink uses for timers
// is still accessing it.
//
// We might also want to upgrade to CADisplayLink, but that requires dropping old macOS support.
std::mem::forget(self.display_link.take());
self.frame_requests.cancel();
}
}
mod sys {
//! Derived from display-link crate under the following license:
//! <https://github.com/BrainiumLLC/display-link/blob/master/LICENSE-MIT>
//! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc)
#![allow(dead_code, non_upper_case_globals)]
use anyhow::Result;
use core_graphics::display::CGDirectDisplayID;
use foreign_types::{ForeignType, foreign_type};
use std::{
ffi::c_void,
fmt::{self, Debug, Formatter},
};
#[derive(Debug)]
pub enum CVDisplayLink {}
foreign_type! {
pub unsafe type DisplayLink {
type CType = CVDisplayLink;
fn drop = CVDisplayLinkRelease;
fn clone = CVDisplayLinkRetain;
}
}
impl Debug for DisplayLink {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
formatter
.debug_tuple("DisplayLink")
.field(&self.as_ptr())
.finish()
}
}
#[repr(C)]
#[derive(Clone, Copy)]
pub(crate) struct CVTimeStamp {
pub version: u32,
pub video_time_scale: i32,
pub video_time: i64,
pub host_time: u64,
pub rate_scalar: f64,
pub video_refresh_period: i64,
pub smpte_time: CVSMPTETime,
pub flags: u64,
pub reserved: u64,
}
pub type CVTimeStampFlags = u64;
pub const kCVTimeStampVideoTimeValid: CVTimeStampFlags = 1 << 0;
pub const kCVTimeStampHostTimeValid: CVTimeStampFlags = 1 << 1;
pub const kCVTimeStampSMPTETimeValid: CVTimeStampFlags = 1 << 2;
pub const kCVTimeStampVideoRefreshPeriodValid: CVTimeStampFlags = 1 << 3;
pub const kCVTimeStampRateScalarValid: CVTimeStampFlags = 1 << 4;
pub const kCVTimeStampTopField: CVTimeStampFlags = 1 << 16;
pub const kCVTimeStampBottomField: CVTimeStampFlags = 1 << 17;
pub const kCVTimeStampVideoHostTimeValid: CVTimeStampFlags =
kCVTimeStampVideoTimeValid | kCVTimeStampHostTimeValid;
pub const kCVTimeStampIsInterlaced: CVTimeStampFlags =
kCVTimeStampTopField | kCVTimeStampBottomField;
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub(crate) struct CVSMPTETime {
pub subframes: i16,
pub subframe_divisor: i16,
pub counter: u32,
pub time_type: u32,
pub flags: u32,
pub hours: i16,
pub minutes: i16,
pub seconds: i16,
pub frames: i16,
}
pub type CVSMPTETimeType = u32;
pub const kCVSMPTETimeType24: CVSMPTETimeType = 0;
pub const kCVSMPTETimeType25: CVSMPTETimeType = 1;
pub const kCVSMPTETimeType30Drop: CVSMPTETimeType = 2;
pub const kCVSMPTETimeType30: CVSMPTETimeType = 3;
pub const kCVSMPTETimeType2997: CVSMPTETimeType = 4;
pub const kCVSMPTETimeType2997Drop: CVSMPTETimeType = 5;
pub const kCVSMPTETimeType60: CVSMPTETimeType = 6;
pub const kCVSMPTETimeType5994: CVSMPTETimeType = 7;
pub type CVSMPTETimeFlags = u32;
pub const kCVSMPTETimeValid: CVSMPTETimeFlags = 1 << 0;
pub const kCVSMPTETimeRunning: CVSMPTETimeFlags = 1 << 1;
pub type CVDisplayLinkOutputCallback = unsafe extern "C" fn(
display_link_out: *mut CVDisplayLink,
// A pointer to the current timestamp. This represents the timestamp when the callback is called.
current_time: *const CVTimeStamp,
// A pointer to the output timestamp. This represents the timestamp for when the frame will be displayed.
output_time: *const CVTimeStamp,
// Unused
flags_in: i64,
// Unused
flags_out: *mut i64,
// A pointer to app-defined data.
display_link_context: *mut c_void,
) -> i32;
#[link(name = "CoreFoundation", kind = "framework")]
#[link(name = "CoreVideo", kind = "framework")]
#[allow(improper_ctypes, unknown_lints, clippy::duplicated_attributes)]
unsafe extern "C" {
pub fn CVDisplayLinkCreateWithActiveCGDisplays(
display_link_out: *mut *mut CVDisplayLink,
) -> i32;
pub fn CVDisplayLinkSetCurrentCGDisplay(
display_link: &mut DisplayLinkRef,
display_id: u32,
) -> i32;
pub fn CVDisplayLinkSetOutputCallback(
display_link: &mut DisplayLinkRef,
callback: CVDisplayLinkOutputCallback,
user_info: *mut c_void,
) -> i32;
pub fn CVDisplayLinkStart(display_link: &mut DisplayLinkRef) -> i32;
pub fn CVDisplayLinkStop(display_link: &mut DisplayLinkRef) -> i32;
pub fn CVDisplayLinkRelease(display_link: *mut CVDisplayLink);
pub fn CVDisplayLinkRetain(display_link: *mut CVDisplayLink) -> *mut CVDisplayLink;
}
impl DisplayLink {
/// Apple docs: [CVDisplayLinkCreateWithCGDisplay](https://developer.apple.com/documentation/corevideo/1456981-cvdisplaylinkcreatewithcgdisplay?language=objc)
pub unsafe fn new(
display_id: CGDirectDisplayID,
callback: CVDisplayLinkOutputCallback,
user_info: *mut c_void,
) -> Result<Self> {
unsafe {
let mut display_link: *mut CVDisplayLink = 0 as _;
let code = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link);
anyhow::ensure!(code == 0, "could not create display link, code: {}", code);
let mut display_link = DisplayLink::from_ptr(display_link);
let code = CVDisplayLinkSetOutputCallback(&mut display_link, callback, user_info);
anyhow::ensure!(code == 0, "could not set output callback, code: {}", code);
let code = CVDisplayLinkSetCurrentCGDisplay(&mut display_link, display_id);
anyhow::ensure!(
code == 0,
"could not assign display to display link, code: {}",
code
);
Ok(display_link)
}
}
}
impl DisplayLinkRef {
/// Apple docs: [CVDisplayLinkStart](https://developer.apple.com/documentation/corevideo/1457193-cvdisplaylinkstart?language=objc)
pub unsafe fn start(&mut self) -> Result<()> {
unsafe {
let code = CVDisplayLinkStart(self);
anyhow::ensure!(code == 0, "could not start display link, code: {}", code);
Ok(())
}
}
/// Apple docs: [CVDisplayLinkStop](https://developer.apple.com/documentation/corevideo/1457281-cvdisplaylinkstop?language=objc)
pub unsafe fn stop(&mut self) -> Result<()> {
unsafe {
let code = CVDisplayLinkStop(self);
anyhow::ensure!(code == 0, "could not stop display link, code: {}", code);
Ok(())
}
}
}
}

View File

@@ -0,0 +1,574 @@
use gpui::{
Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent,
NavigationDirection, PinchEvent, Pixels, PlatformInput, PressureStage, ScrollDelta,
ScrollWheelEvent, TouchPhase, point, px,
};
use crate::{
LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty,
UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
base::{YES, id},
};
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
use core_graphics::event::CGKeyCode;
use objc::{msg_send, sel, sel_impl};
use std::{borrow::Cow, ffi::c_void};
const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16;
const ENTER_KEY: u16 = 0x0d;
const NUMPAD_ENTER_KEY: u16 = 0x03;
pub(crate) const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19;
pub fn key_to_native(key: &str) -> Cow<'_, str> {
use cocoa::appkit::*;
let code = match key {
"space" => SPACE_KEY,
"backspace" => BACKSPACE_KEY,
"escape" => ESCAPE_KEY,
"up" => NSUpArrowFunctionKey,
"down" => NSDownArrowFunctionKey,
"left" => NSLeftArrowFunctionKey,
"right" => NSRightArrowFunctionKey,
"pageup" => NSPageUpFunctionKey,
"pagedown" => NSPageDownFunctionKey,
"home" => NSHomeFunctionKey,
"end" => NSEndFunctionKey,
"delete" => NSDeleteFunctionKey,
"insert" => NSHelpFunctionKey,
"f1" => NSF1FunctionKey,
"f2" => NSF2FunctionKey,
"f3" => NSF3FunctionKey,
"f4" => NSF4FunctionKey,
"f5" => NSF5FunctionKey,
"f6" => NSF6FunctionKey,
"f7" => NSF7FunctionKey,
"f8" => NSF8FunctionKey,
"f9" => NSF9FunctionKey,
"f10" => NSF10FunctionKey,
"f11" => NSF11FunctionKey,
"f12" => NSF12FunctionKey,
"f13" => NSF13FunctionKey,
"f14" => NSF14FunctionKey,
"f15" => NSF15FunctionKey,
"f16" => NSF16FunctionKey,
"f17" => NSF17FunctionKey,
"f18" => NSF18FunctionKey,
"f19" => NSF19FunctionKey,
"f20" => NSF20FunctionKey,
"f21" => NSF21FunctionKey,
"f22" => NSF22FunctionKey,
"f23" => NSF23FunctionKey,
"f24" => NSF24FunctionKey,
"f25" => NSF25FunctionKey,
"f26" => NSF26FunctionKey,
"f27" => NSF27FunctionKey,
"f28" => NSF28FunctionKey,
"f29" => NSF29FunctionKey,
"f30" => NSF30FunctionKey,
"f31" => NSF31FunctionKey,
"f32" => NSF32FunctionKey,
"f33" => NSF33FunctionKey,
"f34" => NSF34FunctionKey,
"f35" => NSF35FunctionKey,
_ => return Cow::Borrowed(key),
};
Cow::Owned(String::from_utf16(&[code]).unwrap())
}
unsafe fn read_modifiers(native_event: id) -> Modifiers {
unsafe {
let modifiers = native_event.modifierFlags();
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
Modifiers {
control,
alt,
shift,
platform: command,
function,
}
}
}
pub(crate) unsafe fn platform_input_from_native(
native_event: id,
window_height: Option<Pixels>,
) -> Option<PlatformInput> {
unsafe {
let event_type = native_event.eventType();
// Filter out event types that aren't in the NSEventType enum.
// See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details.
match event_type as u64 {
0 | 21 | 32 | 33 | 35 | 36 | 37 => {
return None;
}
_ => {}
}
match event_type {
NSEventType::NSFlagsChanged => {
Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
modifiers: read_modifiers(native_event),
capslock: Capslock {
on: native_event
.modifierFlags()
.contains(NSEventModifierFlags::NSAlphaShiftKeyMask),
},
}))
}
NSEventType::NSKeyDown => Some(PlatformInput::KeyDown(KeyDownEvent {
keystroke: parse_keystroke(native_event),
is_held: native_event.isARepeat() == YES,
prefer_character_input: false,
})),
NSEventType::NSKeyUp => Some(PlatformInput::KeyUp(KeyUpEvent {
keystroke: parse_keystroke(native_event),
})),
NSEventType::NSLeftMouseDown
| NSEventType::NSRightMouseDown
| NSEventType::NSOtherMouseDown => {
let button = match native_event.buttonNumber() {
0 => MouseButton::Left,
1 => MouseButton::Right,
2 => MouseButton::Middle,
3 => MouseButton::Navigate(NavigationDirection::Back),
4 => MouseButton::Navigate(NavigationDirection::Forward),
// Other mouse buttons aren't tracked currently
_ => return None,
};
window_height.map(|window_height| {
PlatformInput::MouseDown(MouseDownEvent {
button,
position: point(
px(native_event.locationInWindow().x as f32),
// MacOS screen coordinates are relative to bottom left
window_height - px(native_event.locationInWindow().y as f32),
),
modifiers: read_modifiers(native_event),
click_count: native_event.clickCount() as usize,
first_mouse: false,
})
})
}
NSEventType::NSLeftMouseUp
| NSEventType::NSRightMouseUp
| NSEventType::NSOtherMouseUp => {
let button = match native_event.buttonNumber() {
0 => MouseButton::Left,
1 => MouseButton::Right,
2 => MouseButton::Middle,
3 => MouseButton::Navigate(NavigationDirection::Back),
4 => MouseButton::Navigate(NavigationDirection::Forward),
// Other mouse buttons aren't tracked currently
_ => return None,
};
window_height.map(|window_height| {
PlatformInput::MouseUp(MouseUpEvent {
button,
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
modifiers: read_modifiers(native_event),
click_count: native_event.clickCount() as usize,
})
})
}
NSEventType::NSEventTypePressure => {
let stage = native_event.stage();
let pressure = native_event.pressure();
window_height.map(|window_height| {
PlatformInput::MousePressure(MousePressureEvent {
stage: match stage {
1 => PressureStage::Normal,
2 => PressureStage::Force,
_ => PressureStage::Zero,
},
pressure,
modifiers: read_modifiers(native_event),
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
})
})
}
// Some mice (like Logitech MX Master) send navigation buttons as swipe events
NSEventType::NSEventTypeSwipe => {
let navigation_direction = match native_event.phase() {
NSEventPhase::NSEventPhaseEnded => match native_event.deltaX() {
x if x > 0.0 => Some(NavigationDirection::Back),
x if x < 0.0 => Some(NavigationDirection::Forward),
_ => return None,
},
_ => return None,
};
match navigation_direction {
Some(direction) => window_height.map(|window_height| {
PlatformInput::MouseDown(MouseDownEvent {
button: MouseButton::Navigate(direction),
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
modifiers: read_modifiers(native_event),
click_count: 1,
first_mouse: false,
})
}),
_ => None,
}
}
NSEventType::NSEventTypeMagnify => window_height.map(|window_height| {
let phase = match native_event.phase() {
NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
TouchPhase::Started
}
NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended,
_ => TouchPhase::Moved,
};
let magnification = native_event.magnification() as f32;
PlatformInput::Pinch(PinchEvent {
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
delta: magnification,
modifiers: read_modifiers(native_event),
phase,
})
}),
NSEventType::NSScrollWheel => window_height.map(|window_height| {
let phase = match native_event.phase() {
NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
TouchPhase::Started
}
NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended,
_ => TouchPhase::Moved,
};
let raw_data = point(
native_event.scrollingDeltaX() as f32,
native_event.scrollingDeltaY() as f32,
);
let delta = if native_event.hasPreciseScrollingDeltas() == YES {
ScrollDelta::Pixels(raw_data.map(px))
} else {
ScrollDelta::Lines(raw_data)
};
PlatformInput::ScrollWheel(ScrollWheelEvent {
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
delta,
touch_phase: phase,
modifiers: read_modifiers(native_event),
})
}),
NSEventType::NSLeftMouseDragged
| NSEventType::NSRightMouseDragged
| NSEventType::NSOtherMouseDragged => {
let pressed_button = match native_event.buttonNumber() {
0 => MouseButton::Left,
1 => MouseButton::Right,
2 => MouseButton::Middle,
3 => MouseButton::Navigate(NavigationDirection::Back),
4 => MouseButton::Navigate(NavigationDirection::Forward),
// Other mouse buttons aren't tracked currently
_ => return None,
};
window_height.map(|window_height| {
PlatformInput::MouseMove(MouseMoveEvent {
pressed_button: Some(pressed_button),
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
modifiers: read_modifiers(native_event),
})
})
}
NSEventType::NSMouseMoved => window_height.map(|window_height| {
PlatformInput::MouseMove(MouseMoveEvent {
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
pressed_button: None,
modifiers: read_modifiers(native_event),
})
}),
NSEventType::NSMouseExited => window_height.map(|window_height| {
PlatformInput::MouseExited(MouseExitEvent {
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
pressed_button: None,
modifiers: read_modifiers(native_event),
})
}),
_ => None,
}
}
}
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
unsafe {
use cocoa::appkit::*;
let characters = native_event
.charactersIgnoringModifiers()
.to_str()
.to_string();
let mut key_char = None;
let first_char = characters.chars().next().map(|ch| ch as u16);
let modifiers = native_event.modifierFlags();
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask)
&& first_char
.is_none_or(|ch| !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch));
#[allow(non_upper_case_globals)]
let key = match first_char {
Some(SPACE_KEY) => {
key_char = Some(" ".to_string());
"space".to_string()
}
Some(TAB_KEY) => {
key_char = Some("\t".to_string());
"tab".to_string()
}
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => {
key_char = Some("\n".to_string());
"enter".to_string()
}
Some(BACKSPACE_KEY) => "backspace".to_string(),
Some(ESCAPE_KEY) => "escape".to_string(),
Some(SHIFT_TAB_KEY) => "tab".to_string(),
Some(NSUpArrowFunctionKey) => "up".to_string(),
Some(NSDownArrowFunctionKey) => "down".to_string(),
Some(NSLeftArrowFunctionKey) => "left".to_string(),
Some(NSRightArrowFunctionKey) => "right".to_string(),
Some(NSPageUpFunctionKey) => "pageup".to_string(),
Some(NSPageDownFunctionKey) => "pagedown".to_string(),
Some(NSHomeFunctionKey) => "home".to_string(),
Some(NSEndFunctionKey) => "end".to_string(),
Some(NSDeleteFunctionKey) => "delete".to_string(),
// Observed Insert==NSHelpFunctionKey not NSInsertFunctionKey.
Some(NSHelpFunctionKey) => "insert".to_string(),
Some(NSF1FunctionKey) => "f1".to_string(),
Some(NSF2FunctionKey) => "f2".to_string(),
Some(NSF3FunctionKey) => "f3".to_string(),
Some(NSF4FunctionKey) => "f4".to_string(),
Some(NSF5FunctionKey) => "f5".to_string(),
Some(NSF6FunctionKey) => "f6".to_string(),
Some(NSF7FunctionKey) => "f7".to_string(),
Some(NSF8FunctionKey) => "f8".to_string(),
Some(NSF9FunctionKey) => "f9".to_string(),
Some(NSF10FunctionKey) => "f10".to_string(),
Some(NSF11FunctionKey) => "f11".to_string(),
Some(NSF12FunctionKey) => "f12".to_string(),
Some(NSF13FunctionKey) => "f13".to_string(),
Some(NSF14FunctionKey) => "f14".to_string(),
Some(NSF15FunctionKey) => "f15".to_string(),
Some(NSF16FunctionKey) => "f16".to_string(),
Some(NSF17FunctionKey) => "f17".to_string(),
Some(NSF18FunctionKey) => "f18".to_string(),
Some(NSF19FunctionKey) => "f19".to_string(),
Some(NSF20FunctionKey) => "f20".to_string(),
Some(NSF21FunctionKey) => "f21".to_string(),
Some(NSF22FunctionKey) => "f22".to_string(),
Some(NSF23FunctionKey) => "f23".to_string(),
Some(NSF24FunctionKey) => "f24".to_string(),
Some(NSF25FunctionKey) => "f25".to_string(),
Some(NSF26FunctionKey) => "f26".to_string(),
Some(NSF27FunctionKey) => "f27".to_string(),
Some(NSF28FunctionKey) => "f28".to_string(),
Some(NSF29FunctionKey) => "f29".to_string(),
Some(NSF30FunctionKey) => "f30".to_string(),
Some(NSF31FunctionKey) => "f31".to_string(),
Some(NSF32FunctionKey) => "f32".to_string(),
Some(NSF33FunctionKey) => "f33".to_string(),
Some(NSF34FunctionKey) => "f34".to_string(),
Some(NSF35FunctionKey) => "f35".to_string(),
_ => {
// Cases to test when modifying this:
//
// qwerty key | none | cmd | cmd-shift
// * Armenian s | ս | cmd-s | cmd-shift-s (layout is non-ASCII, so we use cmd layout)
// * Dvorak+QWERTY s | o | cmd-s | cmd-shift-s (layout switches on cmd)
// * Ukrainian+QWERTY s | с | cmd-s | cmd-shift-s (macOS reports cmd-s instead of cmd-S)
// * Czech 7 | ý | cmd-ý | cmd-7 (layout has shifted numbers)
// * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/)
// * Russian 7 | 7 | cmd-7 | cmd-& (shift-7 is . but when cmd is down, should use cmd layout)
// * German QWERTZ ; | ö | cmd-ö | cmd-Ö (Zed's shift special case only applies to a-z)
//
let mut chars_ignoring_modifiers =
chars_for_modified_key(native_event.keyCode(), NO_MOD);
let mut chars_with_shift =
chars_for_modified_key(native_event.keyCode(), SHIFT_MOD);
let always_use_cmd_layout = always_use_command_layout();
// Handle Dvorak+QWERTY / Russian / Armenian
if command || always_use_cmd_layout {
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD);
let chars_with_both =
chars_for_modified_key(native_event.keyCode(), CMD_MOD | SHIFT_MOD);
// We don't do this in the case that the shifted command key generates
// the same character as the unshifted command key (Norwegian, e.g.)
if chars_with_both != chars_with_cmd {
chars_with_shift = chars_with_both;
// Handle edge-case where cmd-shift-s reports cmd-s instead of
// cmd-shift-s (Ukrainian, etc.)
} else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd {
chars_with_shift = chars_with_cmd.to_ascii_uppercase();
}
chars_ignoring_modifiers = chars_with_cmd;
}
if !control && !command && !function {
let mut mods = NO_MOD;
if shift {
mods |= SHIFT_MOD;
}
if alt {
mods |= OPTION_MOD;
}
key_char = Some(chars_for_modified_key(native_event.keyCode(), mods));
}
if shift
&& chars_ignoring_modifiers
.chars()
.all(|c| c.is_ascii_lowercase())
{
chars_ignoring_modifiers
} else if shift {
shift = false;
chars_with_shift
} else {
chars_ignoring_modifiers
}
}
};
Keystroke {
modifiers: Modifiers {
control,
alt,
shift,
platform: command,
function,
},
key,
key_char,
}
}
}
fn always_use_command_layout() -> bool {
if chars_for_modified_key(0, NO_MOD).is_ascii() {
return false;
}
chars_for_modified_key(0, CMD_MOD).is_ascii()
}
const NO_MOD: u32 = 0;
const CMD_MOD: u32 = 1;
const SHIFT_MOD: u32 = 2;
const OPTION_MOD: u32 = 8;
fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
// Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
// shifted >> 8 for UCKeyTranslate
const CG_SPACE_KEY: u16 = 49;
// https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
#[allow(non_upper_case_globals)]
const kUCKeyActionDown: u16 = 0;
#[allow(non_upper_case_globals)]
const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
let keyboard_type = unsafe { LMGetKbdType() as u32 };
const BUFFER_SIZE: usize = 4;
let mut dead_key_state = 0;
let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
let mut buffer_size: usize = 0;
let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
if keyboard.is_null() {
return "".to_string();
}
let layout_data = unsafe {
TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
as CFDataRef
};
if layout_data.is_null() {
unsafe {
let _: () = msg_send![keyboard, release];
}
return "".to_string();
}
let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
unsafe {
UCKeyTranslate(
keyboard_layout as *const c_void,
code,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
if dead_key_state != 0 {
UCKeyTranslate(
keyboard_layout as *const c_void,
CG_SPACE_KEY,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
}
let _: () = msg_send![keyboard, release];
}
String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
}

View File

@@ -0,0 +1,136 @@
#![cfg(target_os = "macos")]
//! macOS platform implementation for GPUI.
//!
//! macOS screens have a y axis that goes up from the bottom of the screen and
//! an origin at the bottom left of the main display.
mod dispatcher;
mod display;
mod display_link;
mod events;
mod keyboard;
mod pasteboard;
#[cfg(feature = "screen-capture")]
mod screen_capture;
mod metal_atlas;
pub mod metal_renderer;
use metal_renderer as renderer;
#[cfg(feature = "font-kit")]
mod open_type;
#[cfg(feature = "font-kit")]
mod text_system;
mod platform;
mod window;
mod window_appearance;
use cocoa::{
base::{id, nil},
foundation::{NSAutoreleasePool, NSNotFound, NSString, NSUInteger},
};
use objc::runtime::{BOOL, NO, YES};
use std::{
ffi::{CStr, c_char},
ops::Range,
};
pub(crate) use dispatcher::*;
pub(crate) use display::*;
pub(crate) use display_link::*;
pub(crate) use keyboard::*;
pub(crate) use platform::*;
pub(crate) use window::*;
#[cfg(feature = "font-kit")]
pub(crate) use text_system::*;
pub use platform::MacPlatform;
trait BoolExt {
fn to_objc(self) -> BOOL;
}
impl BoolExt for bool {
fn to_objc(self) -> BOOL {
if self { YES } else { NO }
}
}
trait NSStringExt {
unsafe fn to_str(&self) -> &str;
}
impl NSStringExt for id {
unsafe fn to_str(&self) -> &str {
unsafe {
let cstr = self.UTF8String();
if cstr.is_null() {
""
} else {
CStr::from_ptr(cstr as *mut c_char).to_str().unwrap()
}
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug)]
struct NSRange {
pub location: NSUInteger,
pub length: NSUInteger,
}
impl NSRange {
fn invalid() -> Self {
Self {
location: NSNotFound as NSUInteger,
length: 0,
}
}
fn is_valid(&self) -> bool {
self.location != NSNotFound as NSUInteger
}
fn to_range(self) -> Option<Range<usize>> {
if self.is_valid() {
let start = self.location as usize;
let end = start + self.length as usize;
Some(start..end)
} else {
None
}
}
}
impl From<Range<usize>> for NSRange {
fn from(range: Range<usize>) -> Self {
NSRange {
location: range.start as NSUInteger,
length: range.len() as NSUInteger,
}
}
}
unsafe impl objc::Encode for NSRange {
fn encode() -> objc::Encoding {
let encoding = format!(
"{{NSRange={}{}}}",
NSUInteger::encode().as_str(),
NSUInteger::encode().as_str()
);
unsafe { objc::Encoding::from_str(&encoding) }
}
}
/// Allow NSString::alloc use here because it sets autorelease
#[allow(clippy::disallowed_methods)]
unsafe fn ns_string(string: &str) -> id {
unsafe { NSString::alloc(nil).init_str(string).autorelease() }
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,349 @@
use anyhow::{Context as _, Result};
use collections::FxHashMap;
use derive_more::{Deref, DerefMut};
use etagere::BucketedAtlasAllocator;
use gpui::{
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTextureList, AtlasTile, Bounds, DevicePixels,
PlatformAtlas, Point, Size,
};
use metal::Device;
use parking_lot::Mutex;
use std::borrow::Cow;
pub(crate) struct MetalAtlas(Mutex<MetalAtlasState>);
impl MetalAtlas {
pub(crate) fn new(device: Device, is_apple_gpu: bool) -> Self {
MetalAtlas(Mutex::new(MetalAtlasState {
device: AssertSend(device),
is_apple_gpu,
monochrome_textures: Default::default(),
polychrome_textures: Default::default(),
tiles_by_key: Default::default(),
}))
}
pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> metal::Texture {
self.0.lock().texture(id).metal_texture.clone()
}
}
struct MetalAtlasState {
device: AssertSend<Device>,
is_apple_gpu: bool,
monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
}
impl PlatformAtlas for MetalAtlas {
fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>> {
let mut lock = self.0.lock();
if let Some(tile) = lock.tiles_by_key.get(key) {
Ok(Some(*tile))
} else {
let Some((size, bytes)) = build()? else {
return Ok(None);
};
let tile = lock
.allocate(size, key.texture_kind())
.context("failed to allocate")?;
let texture = lock.texture(tile.texture_id);
texture.upload(tile.bounds, &bytes);
lock.tiles_by_key.insert(key.clone(), tile);
Ok(Some(tile))
}
}
fn remove(&self, key: &AtlasKey) {
let mut lock = self.0.lock();
let Some(id) = lock.tiles_by_key.remove(key).map(|v| v.texture_id) else {
return;
};
let textures = match id.kind {
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
AtlasTextureKind::Subpixel => unreachable!(),
};
let Some(texture_slot) = textures
.textures
.iter_mut()
.find(|texture| texture.as_ref().is_some_and(|v| v.id == id))
else {
return;
};
if let Some(mut texture) = texture_slot.take() {
texture.decrement_ref_count();
if texture.is_unreferenced() {
textures.free_list.push(id.index as usize);
} else {
*texture_slot = Some(texture);
}
}
}
}
impl MetalAtlasState {
fn allocate(
&mut self,
size: Size<DevicePixels>,
texture_kind: AtlasTextureKind,
) -> Option<AtlasTile> {
{
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Subpixel => unreachable!(),
};
if let Some(tile) = textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
{
return Some(tile);
}
}
let texture = self.push_texture(size, texture_kind);
texture.allocate(size)
}
fn push_texture(
&mut self,
min_size: Size<DevicePixels>,
kind: AtlasTextureKind,
) -> &mut MetalAtlasTexture {
const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
width: DevicePixels(1024),
height: DevicePixels(1024),
};
// Max texture size on all modern Apple GPUs. Anything bigger than that crashes in validateWithDevice.
const MAX_ATLAS_SIZE: Size<DevicePixels> = Size {
width: DevicePixels(16384),
height: DevicePixels(16384),
};
let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE);
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_width(size.width.into());
texture_descriptor.set_height(size.height.into());
let pixel_format;
let usage;
match kind {
AtlasTextureKind::Monochrome => {
pixel_format = metal::MTLPixelFormat::A8Unorm;
usage = metal::MTLTextureUsage::ShaderRead;
}
AtlasTextureKind::Polychrome => {
pixel_format = metal::MTLPixelFormat::BGRA8Unorm;
usage = metal::MTLTextureUsage::ShaderRead;
}
AtlasTextureKind::Subpixel => unreachable!(),
}
texture_descriptor.set_pixel_format(pixel_format);
texture_descriptor.set_usage(usage);
// Shared memory mode can be used only on Apple GPU families
// https://developer.apple.com/documentation/metal/mtlresourceoptions/storagemodeshared
texture_descriptor.set_storage_mode(if self.is_apple_gpu {
metal::MTLStorageMode::Shared
} else {
metal::MTLStorageMode::Managed
});
let metal_texture = self.device.new_texture(&texture_descriptor);
let texture_list = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Subpixel => unreachable!(),
};
let index = texture_list.free_list.pop();
let atlas_texture = MetalAtlasTexture {
id: AtlasTextureId {
index: index.unwrap_or(texture_list.textures.len()) as u32,
kind,
},
allocator: etagere::BucketedAtlasAllocator::new(size_to_etagere(size)),
metal_texture: AssertSend(metal_texture),
live_atlas_keys: 0,
};
if let Some(ix) = index {
texture_list.textures[ix] = Some(atlas_texture);
texture_list.textures.get_mut(ix)
} else {
texture_list.textures.push(Some(atlas_texture));
texture_list.textures.last_mut()
}
.unwrap()
.as_mut()
.unwrap()
}
fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture {
let textures = match id.kind {
AtlasTextureKind::Monochrome => &self.monochrome_textures,
AtlasTextureKind::Polychrome => &self.polychrome_textures,
AtlasTextureKind::Subpixel => unreachable!(),
};
textures[id.index as usize].as_ref().unwrap()
}
}
struct MetalAtlasTexture {
id: AtlasTextureId,
allocator: BucketedAtlasAllocator,
metal_texture: AssertSend<metal::Texture>,
live_atlas_keys: u32,
}
impl MetalAtlasTexture {
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
let allocation = self.allocator.allocate(size_to_etagere(size))?;
let tile = AtlasTile {
texture_id: self.id,
tile_id: allocation.id.into(),
bounds: Bounds {
origin: point_from_etagere(allocation.rectangle.min),
size,
},
padding: 0,
};
self.live_atlas_keys += 1;
Some(tile)
}
fn upload(&self, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
let region = metal::MTLRegion::new_2d(
bounds.origin.x.into(),
bounds.origin.y.into(),
bounds.size.width.into(),
bounds.size.height.into(),
);
self.metal_texture.replace_region(
region,
0,
bytes.as_ptr() as *const _,
bounds.size.width.to_bytes(self.bytes_per_pixel()) as u64,
);
}
fn bytes_per_pixel(&self) -> u8 {
use metal::MTLPixelFormat::*;
match self.metal_texture.pixel_format() {
A8Unorm | R8Unorm => 1,
RGBA8Unorm | BGRA8Unorm => 4,
_ => unimplemented!(),
}
}
fn decrement_ref_count(&mut self) {
self.live_atlas_keys -= 1;
}
fn is_unreferenced(&mut self) -> bool {
self.live_atlas_keys == 0
}
}
fn size_to_etagere(size: Size<DevicePixels>) -> etagere::Size {
etagere::Size::new(size.width.into(), size.height.into())
}
fn point_from_etagere(value: etagere::Point) -> Point<DevicePixels> {
Point {
x: DevicePixels::from(value.x),
y: DevicePixels::from(value.y),
}
}
#[derive(Deref, DerefMut)]
struct AssertSend<T>(T);
unsafe impl<T> Send for AssertSend<T> {}
#[cfg(test)]
mod tests {
use super::*;
use gpui::PlatformAtlas;
use std::borrow::Cow;
fn create_atlas() -> Option<MetalAtlas> {
let device = metal::Device::system_default()?;
Some(MetalAtlas::new(device, true))
}
fn make_image_key(image_id: usize, frame_index: usize) -> AtlasKey {
AtlasKey::Image(gpui::RenderImageParams {
image_id: gpui::ImageId(image_id),
frame_index,
})
}
fn insert_tile(atlas: &MetalAtlas, key: &AtlasKey, size: Size<DevicePixels>) -> AtlasTile {
atlas
.get_or_insert_with(key, &mut || {
let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4;
Ok(Some((size, Cow::Owned(vec![0u8; byte_count]))))
})
.expect("allocation should succeed")
.expect("callback returns Some")
}
#[test]
fn test_remove_clears_stale_keys_from_tiles_by_key() {
let Some(atlas) = create_atlas() else {
return;
};
let small = Size {
width: DevicePixels(64),
height: DevicePixels(64),
};
let key_a = make_image_key(1, 0);
let key_b = make_image_key(2, 0);
let key_c = make_image_key(3, 0);
let tile_a = insert_tile(&atlas, &key_a, small);
let tile_b = insert_tile(&atlas, &key_b, small);
let tile_c = insert_tile(&atlas, &key_c, small);
assert_eq!(tile_a.texture_id, tile_b.texture_id);
assert_eq!(tile_b.texture_id, tile_c.texture_id);
// Remove A: texture still has B and C, so it stays.
// The key for A must be removed from tiles_by_key.
atlas.remove(&key_a);
// Remove B: texture still has C.
atlas.remove(&key_b);
// Remove C: texture becomes unreferenced and is deleted.
atlas.remove(&key_c);
// Re-inserting A must allocate a fresh tile on a new texture,
// NOT return a stale tile referencing the deleted texture.
let tile_a2 = insert_tile(&atlas, &key_a, small);
// The texture must actually exist — this would panic before the fix.
let _texture = atlas.metal_texture(tile_a2.texture_id);
}
#[test]
fn test_remove_nonexistent_key_is_noop() {
let Some(atlas) = create_atlas() else {
return;
};
let key = make_image_key(999, 0);
atlas.remove(&key);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
#![allow(unused, non_upper_case_globals)]
use cocoa::appkit::CGFloat;
use core_foundation::{
array::{
CFArray, CFArrayAppendArray, CFArrayAppendValue, CFArrayCreateMutable, CFArrayGetCount,
CFArrayGetValueAtIndex, CFArrayRef, CFMutableArrayRef, kCFTypeArrayCallBacks,
},
base::{CFRelease, TCFType, kCFAllocatorDefault},
dictionary::{
CFDictionaryCreate, kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks,
},
number::CFNumber,
string::{CFString, CFStringRef},
};
use core_foundation_sys::locale::CFLocaleCopyPreferredLanguages;
use core_graphics::{display::CFDictionary, geometry::CGAffineTransform};
use core_text::{
font::{CTFont, CTFontRef, cascade_list_for_languages},
font_descriptor::{
CTFontDescriptor, CTFontDescriptorCopyAttributes, CTFontDescriptorCreateCopyWithFeature,
CTFontDescriptorCreateWithAttributes, CTFontDescriptorCreateWithNameAndSize,
CTFontDescriptorRef, kCTFontCascadeListAttribute, kCTFontFeatureSettingsAttribute,
},
};
use font_kit::font::Font as FontKitFont;
use gpui::{FontFallbacks, FontFeatures};
use std::ptr;
pub fn apply_features_and_fallbacks(
font: &mut FontKitFont,
features: &FontFeatures,
fallbacks: Option<&FontFallbacks>,
) -> anyhow::Result<()> {
unsafe {
let mut keys = vec![kCTFontFeatureSettingsAttribute];
let mut values = vec![generate_feature_array(features)];
if let Some(fallbacks) = fallbacks
&& !fallbacks.fallback_list().is_empty()
{
keys.push(kCTFontCascadeListAttribute);
values.push(generate_fallback_array(
fallbacks,
font.native_font().as_concrete_TypeRef(),
));
}
let attrs = CFDictionaryCreate(
kCFAllocatorDefault,
keys.as_ptr() as _,
values.as_ptr() as _,
keys.len() as isize,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks,
);
for value in &values {
CFRelease(*value as _);
}
let new_descriptor = CTFontDescriptorCreateWithAttributes(attrs);
CFRelease(attrs as _);
let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);
let new_font = CTFontCreateCopyWithAttributes(
font.native_font().as_concrete_TypeRef(),
0.0,
std::ptr::null(),
new_descriptor.as_concrete_TypeRef(),
);
let new_font = CTFont::wrap_under_create_rule(new_font);
*font = font_kit::font::Font::from_native_font(&new_font);
Ok(())
}
}
fn generate_feature_array(features: &FontFeatures) -> CFMutableArrayRef {
unsafe {
let feature_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
for (tag, value) in features.tag_value_list() {
let keys = [kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue];
let values = [
CFString::new(tag).as_CFTypeRef(),
CFNumber::from(*value as i32).as_CFTypeRef(),
];
let dict = CFDictionaryCreate(
kCFAllocatorDefault,
&keys as *const _ as _,
&values as *const _ as _,
2,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks,
);
values.into_iter().for_each(|value| CFRelease(value));
CFArrayAppendValue(feature_array, dict as _);
CFRelease(dict as _);
}
feature_array
}
}
fn generate_fallback_array(fallbacks: &FontFallbacks, font_ref: CTFontRef) -> CFMutableArrayRef {
unsafe {
let fallback_array = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
for user_fallback in fallbacks.fallback_list() {
let name = CFString::from(user_fallback.as_str());
let fallback_desc =
CTFontDescriptorCreateWithNameAndSize(name.as_concrete_TypeRef(), 0.0);
CFArrayAppendValue(fallback_array, fallback_desc as _);
CFRelease(fallback_desc as _);
}
append_system_fallbacks(fallback_array, font_ref);
fallback_array
}
}
fn append_system_fallbacks(fallback_array: CFMutableArrayRef, font_ref: CTFontRef) {
unsafe {
let preferred_languages: CFArray<CFString> =
CFArray::wrap_under_create_rule(CFLocaleCopyPreferredLanguages());
let default_fallbacks = CTFontCopyDefaultCascadeListForLanguages(
font_ref,
preferred_languages.as_concrete_TypeRef(),
);
let default_fallbacks: CFArray<CTFontDescriptor> =
CFArray::wrap_under_create_rule(default_fallbacks);
default_fallbacks
.iter()
.filter(|desc| desc.font_path().is_some())
.map(|desc| {
CFArrayAppendValue(fallback_array, desc.as_concrete_TypeRef() as _);
});
}
}
#[link(name = "CoreText", kind = "framework")]
unsafe extern "C" {
static kCTFontOpenTypeFeatureTag: CFStringRef;
static kCTFontOpenTypeFeatureValue: CFStringRef;
fn CTFontCreateCopyWithAttributes(
font: CTFontRef,
size: CGFloat,
matrix: *const CGAffineTransform,
attributes: CTFontDescriptorRef,
) -> CTFontRef;
fn CTFontCopyDefaultCascadeListForLanguages(
font: CTFontRef,
languagePrefList: CFArrayRef,
) -> CFArrayRef;
}

View File

@@ -0,0 +1,531 @@
use core::slice;
use std::ffi::{CStr, c_void};
use std::path::PathBuf;
use cocoa::{
appkit::{
NSFilenamesPboardType, NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString,
NSPasteboardTypeTIFF,
},
base::{id, nil},
foundation::{NSArray, NSData, NSFastEnumeration, NSString},
};
use objc::{msg_send, runtime::Object, sel, sel_impl};
use smallvec::SmallVec;
use strum::IntoEnumIterator as _;
use crate::ns_string;
use gpui::{
ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash,
};
pub struct Pasteboard {
inner: id,
text_hash_type: id,
metadata_type: id,
}
impl Pasteboard {
pub fn general() -> Self {
unsafe { Self::new(NSPasteboard::generalPasteboard(nil)) }
}
pub fn find() -> Self {
unsafe { Self::new(NSPasteboard::pasteboardWithName(nil, NSPasteboardNameFind)) }
}
#[cfg(test)]
pub fn unique() -> Self {
unsafe { Self::new(NSPasteboard::pasteboardWithUniqueName(nil)) }
}
unsafe fn new(inner: id) -> Self {
Self {
inner,
text_hash_type: unsafe { ns_string("zed-text-hash") },
metadata_type: unsafe { ns_string("zed-metadata") },
}
}
pub fn read(&self) -> Option<ClipboardItem> {
unsafe {
// Check for file paths first
let filenames = NSPasteboard::propertyListForType(self.inner, NSFilenamesPboardType);
if filenames != nil && NSArray::count(filenames) > 0 {
let mut paths = SmallVec::new();
for file in filenames.iter() {
let f = NSString::UTF8String(file);
let path = CStr::from_ptr(f).to_string_lossy().into_owned();
paths.push(PathBuf::from(path));
}
if !paths.is_empty() {
let mut entries = vec![ClipboardEntry::ExternalPaths(ExternalPaths(paths))];
// Also include the string representation so text editors can
// paste the path as text.
if let Some(string_item) = self.read_string_from_pasteboard() {
entries.push(string_item);
}
return Some(ClipboardItem { entries });
}
}
// Next, check for a plain string.
if let Some(string_entry) = self.read_string_from_pasteboard() {
return Some(ClipboardItem {
entries: vec![string_entry],
});
}
// Finally, try the various supported image types.
for format in ImageFormat::iter() {
if let Some(item) = self.read_image(format) {
return Some(item);
}
}
}
None
}
fn read_image(&self, format: ImageFormat) -> Option<ClipboardItem> {
let ut_type: UTType = format.into();
unsafe {
let types: id = self.inner.types();
if msg_send![types, containsObject: ut_type.inner()] {
self.data_for_type(ut_type.inner_mut()).map(|bytes| {
let bytes = bytes.to_vec();
let id = hash(&bytes);
ClipboardItem {
entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
}
})
} else {
None
}
}
}
unsafe fn read_string_from_pasteboard(&self) -> Option<ClipboardEntry> {
unsafe {
let pasteboard_types: id = self.inner.types();
let string_type: id = ns_string("public.utf8-plain-text");
if !msg_send![pasteboard_types, containsObject: string_type] {
return None;
}
let data = self.inner.dataForType(string_type);
let text_bytes: &[u8] = if data == nil {
return None;
} else if data.bytes().is_null() {
// https://developer.apple.com/documentation/foundation/nsdata/1410616-bytes?language=objc
// "If the length of the NSData object is 0, this property returns nil."
&[]
} else {
slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize)
};
let text = String::from_utf8_lossy(text_bytes).to_string();
let metadata = self
.data_for_type(self.text_hash_type)
.and_then(|hash_bytes| {
let hash_bytes = hash_bytes.try_into().ok()?;
let hash = u64::from_be_bytes(hash_bytes);
let metadata = self.data_for_type(self.metadata_type)?;
if hash == ClipboardString::text_hash(&text) {
String::from_utf8(metadata.to_vec()).ok()
} else {
None
}
});
Some(ClipboardEntry::String(ClipboardString { text, metadata }))
}
}
unsafe fn data_for_type(&self, kind: id) -> Option<&[u8]> {
unsafe {
let data = self.inner.dataForType(kind);
if data == nil {
None
} else {
Some(slice::from_raw_parts(
data.bytes() as *mut u8,
data.length() as usize,
))
}
}
}
pub fn write(&self, item: ClipboardItem) {
unsafe {
match item.entries.as_slice() {
[] => {
// Writing an empty list of entries just clears the clipboard.
self.inner.clearContents();
}
[ClipboardEntry::String(string)] => {
self.write_plaintext(string);
}
[ClipboardEntry::Image(image)] => {
self.write_image(image);
}
[ClipboardEntry::ExternalPaths(_)] => {}
_ => {
// Agus NB: We're currently only writing string entries to the clipboard when we have more than one.
//
// This was the existing behavior before I refactored the outer clipboard code:
// https://github.com/zed-industries/zed/blob/65f7412a0265552b06ce122655369d6cc7381dd6/crates/gpui/src/platform/mac/platform.rs#L1060-L1110
//
// Note how `any_images` is always `false`. We should fix that, but that's orthogonal to the refactor.
let mut combined = ClipboardString {
text: String::new(),
metadata: None,
};
for entry in item.entries {
match entry {
ClipboardEntry::String(text) => {
combined.text.push_str(&text.text());
if combined.metadata.is_none() {
combined.metadata = text.metadata;
}
}
_ => {}
}
}
self.write_plaintext(&combined);
}
}
}
}
fn write_plaintext(&self, string: &ClipboardString) {
unsafe {
self.inner.clearContents();
let text_bytes = NSData::dataWithBytes_length_(
nil,
string.text.as_ptr() as *const c_void,
string.text.len() as u64,
);
self.inner
.setData_forType(text_bytes, NSPasteboardTypeString);
if let Some(metadata) = string.metadata.as_ref() {
let hash_bytes = ClipboardString::text_hash(&string.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.inner.setData_forType(hash_bytes, self.text_hash_type);
let metadata_bytes = NSData::dataWithBytes_length_(
nil,
metadata.as_ptr() as *const c_void,
metadata.len() as u64,
);
self.inner
.setData_forType(metadata_bytes, self.metadata_type);
}
}
}
unsafe fn write_image(&self, image: &Image) {
unsafe {
self.inner.clearContents();
let bytes = NSData::dataWithBytes_length_(
nil,
image.bytes.as_ptr() as *const c_void,
image.bytes.len() as u64,
);
self.inner
.setData_forType(bytes, Into::<UTType>::into(image.format).inner_mut());
}
}
}
#[link(name = "AppKit", kind = "framework")]
unsafe extern "C" {
/// [Apple's documentation](https://developer.apple.com/documentation/appkit/nspasteboardnamefind?language=objc)
pub static NSPasteboardNameFind: id;
}
impl From<ImageFormat> for UTType {
fn from(value: ImageFormat) -> Self {
match value {
ImageFormat::Png => Self::png(),
ImageFormat::Jpeg => Self::jpeg(),
ImageFormat::Tiff => Self::tiff(),
ImageFormat::Webp => Self::webp(),
ImageFormat::Gif => Self::gif(),
ImageFormat::Bmp => Self::bmp(),
ImageFormat::Svg => Self::svg(),
ImageFormat::Ico => Self::ico(),
ImageFormat::Pnm => Self::pnm(),
}
}
}
// See https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/
pub struct UTType(id);
impl UTType {
pub fn png() -> Self {
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png
Self(unsafe { NSPasteboardTypePNG }) // This is a rare case where there's a built-in NSPasteboardType
}
pub fn jpeg() -> Self {
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/jpeg
Self(unsafe { ns_string("public.jpeg") })
}
pub fn gif() -> Self {
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/gif
Self(unsafe { ns_string("com.compuserve.gif") })
}
pub fn webp() -> Self {
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/webp
Self(unsafe { ns_string("org.webmproject.webp") })
}
pub fn bmp() -> Self {
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/bmp
Self(unsafe { ns_string("com.microsoft.bmp") })
}
pub fn svg() -> Self {
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/svg
Self(unsafe { ns_string("public.svg-image") })
}
pub fn ico() -> Self {
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico
Self(unsafe { ns_string("com.microsoft.ico") })
}
pub fn tiff() -> Self {
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff
Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType
}
pub fn pnm() -> Self {
//https://en.wikipedia.org/w/index.php?title=Netpbm&oldid=1336679433 under Uniform Type Identifier
Self(unsafe { ns_string("public.pbm") })
}
fn inner(&self) -> *const Object {
self.0
}
pub fn inner_mut(&self) -> *mut Object {
self.0 as *mut _
}
}
#[cfg(test)]
mod tests {
use cocoa::{
appkit::{NSFilenamesPboardType, NSPasteboard, NSPasteboardTypeString},
base::{id, nil},
foundation::{NSArray, NSData},
};
use std::ffi::c_void;
use gpui::{ClipboardEntry, ClipboardItem, ClipboardString, ImageFormat};
use super::*;
unsafe fn simulate_external_file_copy(pasteboard: &Pasteboard, paths: &[&str]) {
unsafe {
let ns_paths: Vec<id> = paths.iter().map(|p| ns_string(p)).collect();
let ns_array = NSArray::arrayWithObjects(nil, &ns_paths);
let mut types = vec![NSFilenamesPboardType];
types.push(NSPasteboardTypeString);
let types_array = NSArray::arrayWithObjects(nil, &types);
pasteboard.inner.declareTypes_owner(types_array, nil);
pasteboard
.inner
.setPropertyList_forType(ns_array, NSFilenamesPboardType);
let joined = paths.join("\n");
let bytes = NSData::dataWithBytes_length_(
nil,
joined.as_ptr() as *const c_void,
joined.len() as u64,
);
pasteboard
.inner
.setData_forType(bytes, NSPasteboardTypeString);
}
}
#[test]
fn test_string() {
let pasteboard = Pasteboard::unique();
assert_eq!(pasteboard.read(), None);
let item = ClipboardItem::new_string("1".to_string());
pasteboard.write(item.clone());
assert_eq!(pasteboard.read(), Some(item));
let item = ClipboardItem {
entries: vec![ClipboardEntry::String(
ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]),
)],
};
pasteboard.write(item.clone());
assert_eq!(pasteboard.read(), 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,
);
pasteboard
.inner
.setData_forType(bytes, NSPasteboardTypeString);
}
assert_eq!(
pasteboard.read(),
Some(ClipboardItem::new_string(text_from_other_app.to_string()))
);
}
#[test]
fn test_read_external_path() {
let pasteboard = Pasteboard::unique();
unsafe {
simulate_external_file_copy(&pasteboard, &["/test.txt"]);
}
let item = pasteboard.read().expect("should read clipboard item");
// Test both ExternalPaths and String entries exist
assert_eq!(item.entries.len(), 2);
// Test first entry is ExternalPaths
match &item.entries[0] {
ClipboardEntry::ExternalPaths(ep) => {
assert_eq!(ep.paths(), &[PathBuf::from("/test.txt")]);
}
other => panic!("expected ExternalPaths, got {:?}", other),
}
// Test second entry is String
match &item.entries[1] {
ClipboardEntry::String(s) => {
assert_eq!(s.text(), "/test.txt");
}
other => panic!("expected String, got {:?}", other),
}
}
#[test]
fn test_read_external_paths_with_spaces() {
let pasteboard = Pasteboard::unique();
let paths = ["/some file with spaces.txt"];
unsafe {
simulate_external_file_copy(&pasteboard, &paths);
}
let item = pasteboard.read().expect("should read clipboard item");
match &item.entries[0] {
ClipboardEntry::ExternalPaths(ep) => {
assert_eq!(ep.paths(), &[PathBuf::from("/some file with spaces.txt")]);
}
other => panic!("expected ExternalPaths, got {:?}", other),
}
}
#[test]
fn test_read_multiple_external_paths() {
let pasteboard = Pasteboard::unique();
let paths = ["/file.txt", "/image.png"];
unsafe {
simulate_external_file_copy(&pasteboard, &paths);
}
let item = pasteboard.read().expect("should read clipboard item");
assert_eq!(item.entries.len(), 2);
// Test both ExternalPaths and String entries exist
match &item.entries[0] {
ClipboardEntry::ExternalPaths(ep) => {
assert_eq!(
ep.paths(),
&[PathBuf::from("/file.txt"), PathBuf::from("/image.png"),]
);
}
other => panic!("expected ExternalPaths, got {:?}", other),
}
match &item.entries[1] {
ClipboardEntry::String(s) => {
assert_eq!(s.text(), "/file.txt\n/image.png");
assert_eq!(s.metadata, None);
}
other => panic!("expected String, got {:?}", other),
}
}
#[test]
fn test_read_image() {
let pasteboard = Pasteboard::unique();
// Smallest valid PNG: 1x1 transparent pixel
let png_bytes: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00,
0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78,
0x9C, 0x62, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE5, 0x27, 0xDE, 0xFC, 0x00, 0x00,
0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
];
unsafe {
let ns_png_type = NSPasteboardTypePNG;
let types_array = NSArray::arrayWithObjects(nil, &[ns_png_type]);
pasteboard.inner.declareTypes_owner(types_array, nil);
let data = NSData::dataWithBytes_length_(
nil,
png_bytes.as_ptr() as *const c_void,
png_bytes.len() as u64,
);
pasteboard.inner.setData_forType(data, ns_png_type);
}
let item = pasteboard.read().expect("should read PNG image");
// Test Image entry exists
assert_eq!(item.entries.len(), 1);
match &item.entries[0] {
ClipboardEntry::Image(img) => {
assert_eq!(img.format, ImageFormat::Png);
assert_eq!(img.bytes, png_bytes);
}
other => panic!("expected Image, got {:?}", other),
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,344 @@
use crate::ns_string;
use anyhow::{Result, anyhow};
use block::ConcreteBlock;
use cocoa::{
base::{YES, id, nil},
foundation::NSArray,
};
use collections::HashMap;
use core_foundation::base::TCFType;
use core_graphics::display::{
CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight,
CGDisplayModeGetPixelWidth, CGDisplayModeRelease,
};
use ctor::ctor;
use futures::channel::oneshot;
use gpui::{
DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
SharedString, SourceMetadata, size,
};
use media::core_media::{CMSampleBuffer, CMSampleBufferRef};
use metal::NSInteger;
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use std::{cell::RefCell, ffi::c_void, mem, ptr, rc::Rc};
use crate::NSStringExt;
#[derive(Clone)]
pub struct MacScreenCaptureSource {
sc_display: id,
meta: Option<ScreenMeta>,
}
pub struct MacScreenCaptureStream {
sc_stream: id,
sc_stream_output: id,
meta: SourceMetadata,
}
static mut DELEGATE_CLASS: *const Class = ptr::null();
static mut OUTPUT_CLASS: *const Class = ptr::null();
const FRAME_CALLBACK_IVAR: &str = "frame_callback";
#[allow(non_upper_case_globals)]
const SCStreamOutputTypeScreen: NSInteger = 0;
impl ScreenCaptureSource for MacScreenCaptureSource {
fn metadata(&self) -> Result<SourceMetadata> {
let (display_id, size) = unsafe {
let display_id: CGDirectDisplayID = msg_send![self.sc_display, displayID];
let display_mode_ref = CGDisplayCopyDisplayMode(display_id);
let width = CGDisplayModeGetPixelWidth(display_mode_ref);
let height = CGDisplayModeGetPixelHeight(display_mode_ref);
CGDisplayModeRelease(display_mode_ref);
(
display_id,
size(DevicePixels(width as i32), DevicePixels(height as i32)),
)
};
let (label, is_main) = self
.meta
.clone()
.map(|meta| (meta.label, meta.is_main))
.unzip();
Ok(SourceMetadata {
id: display_id as u64,
label,
is_main,
resolution: size,
})
}
fn stream(
&self,
_foreground_executor: &ForegroundExecutor,
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
unsafe {
let stream: id = msg_send![class!(SCStream), alloc];
let filter: id = msg_send![class!(SCContentFilter), alloc];
let configuration: id = msg_send![class!(SCStreamConfiguration), alloc];
let delegate: id = msg_send![DELEGATE_CLASS, alloc];
let output: id = msg_send![OUTPUT_CLASS, alloc];
let excluded_windows = NSArray::array(nil);
let filter: id = msg_send![filter, initWithDisplay:self.sc_display excludingWindows:excluded_windows];
let configuration: id = msg_send![configuration, init];
let _: id = msg_send![configuration, setScalesToFit: true];
let _: id = msg_send![configuration, setPixelFormat: 0x42475241];
// let _: id = msg_send![configuration, setShowsCursor: false];
// let _: id = msg_send![configuration, setCaptureResolution: 3];
let delegate: id = msg_send![delegate, init];
let output: id = msg_send![output, init];
output.as_mut().unwrap().set_ivar(
FRAME_CALLBACK_IVAR,
Box::into_raw(Box::new(frame_callback)) as *mut c_void,
);
let meta = self.metadata().unwrap();
let _: id = msg_send![configuration, setWidth: meta.resolution.width.0 as i64];
let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64];
let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate];
// Stream contains filter, configuration, and delegate internally so we release them here
// to prevent a memory leak when steam is dropped
let _: () = msg_send![filter, release];
let _: () = msg_send![configuration, release];
let _: () = msg_send![delegate, release];
let (tx, rx) = oneshot::channel();
let mut error: id = nil;
let _: () = msg_send![stream, addStreamOutput:output type:SCStreamOutputTypeScreen sampleHandlerQueue:0 error:&mut error as *mut id];
if error != nil {
let message: id = msg_send![error, localizedDescription];
let _: () = msg_send![stream, release];
let _: () = msg_send![output, release];
tx.send(Err(anyhow!("failed to add stream output {message:?}")))
.ok();
return rx;
}
let tx = Rc::new(RefCell::new(Some(tx)));
let handler = ConcreteBlock::new({
move |error: id| {
let result = if error == nil {
let stream = MacScreenCaptureStream {
meta: meta.clone(),
sc_stream: stream,
sc_stream_output: output,
};
Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>)
} else {
let _: () = msg_send![stream, release];
let _: () = msg_send![output, release];
let message: id = msg_send![error, localizedDescription];
Err(anyhow!("failed to start screen capture stream {message:?}"))
};
if let Some(tx) = tx.borrow_mut().take() {
tx.send(result).ok();
}
}
});
let handler = handler.copy();
let _: () = msg_send![stream, startCaptureWithCompletionHandler:handler];
rx
}
}
}
impl Drop for MacScreenCaptureSource {
fn drop(&mut self) {
unsafe {
let _: () = msg_send![self.sc_display, release];
}
}
}
impl ScreenCaptureStream for MacScreenCaptureStream {
fn metadata(&self) -> Result<SourceMetadata> {
Ok(self.meta.clone())
}
}
impl Drop for MacScreenCaptureStream {
fn drop(&mut self) {
unsafe {
let mut error: id = nil;
let _: () = msg_send![self.sc_stream, removeStreamOutput:self.sc_stream_output type:SCStreamOutputTypeScreen error:&mut error as *mut _];
if error != nil {
let message: id = msg_send![error, localizedDescription];
log::error!("failed to add stream output {message:?}");
}
let handler = ConcreteBlock::new(move |error: id| {
if error != nil {
let message: id = msg_send![error, localizedDescription];
log::error!("failed to stop screen capture stream {message:?}");
}
});
let block = handler.copy();
let _: () = msg_send![self.sc_stream, stopCaptureWithCompletionHandler:block];
let _: () = msg_send![self.sc_stream, release];
let _: () = msg_send![self.sc_stream_output, release];
}
}
}
#[derive(Clone)]
struct ScreenMeta {
label: SharedString,
// Is this the screen with menu bar?
is_main: bool,
}
unsafe fn screen_id_to_human_label() -> HashMap<CGDirectDisplayID, ScreenMeta> {
let screens: id = msg_send![class!(NSScreen), screens];
let count: usize = msg_send![screens, count];
let mut map = HashMap::default();
let screen_number_key = unsafe { ns_string("NSScreenNumber") };
for i in 0..count {
let screen: id = msg_send![screens, objectAtIndex: i];
let device_desc: id = msg_send![screen, deviceDescription];
if device_desc == nil {
continue;
}
let nsnumber: id = msg_send![device_desc, objectForKey: screen_number_key];
if nsnumber == nil {
continue;
}
let screen_id: u32 = msg_send![nsnumber, unsignedIntValue];
let name: id = msg_send![screen, localizedName];
if name != nil {
let cstr: *const std::os::raw::c_char = msg_send![name, UTF8String];
let rust_str = unsafe {
std::ffi::CStr::from_ptr(cstr)
.to_string_lossy()
.into_owned()
};
map.insert(
screen_id,
ScreenMeta {
label: rust_str.into(),
is_main: i == 0,
},
);
}
}
map
}
pub(crate) fn get_sources() -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
unsafe {
let (tx, rx) = oneshot::channel();
let tx = Rc::new(RefCell::new(Some(tx)));
let screen_id_to_label = screen_id_to_human_label();
let block = ConcreteBlock::new(move |shareable_content: id, error: id| {
let Some(tx) = tx.borrow_mut().take() else {
return;
};
let result = if error == nil {
let displays: id = msg_send![shareable_content, displays];
let mut result = Vec::new();
for i in 0..displays.count() {
let display = displays.objectAtIndex(i);
let id: CGDirectDisplayID = msg_send![display, displayID];
let meta = screen_id_to_label.get(&id).cloned();
let source = MacScreenCaptureSource {
sc_display: msg_send![display, retain],
meta,
};
result.push(Rc::new(source) as Rc<dyn ScreenCaptureSource>);
}
Ok(result)
} else {
let msg: id = msg_send![error, localizedDescription];
Err(anyhow!(
"Screen share failed: {:?}",
NSStringExt::to_str(&msg)
))
};
tx.send(result).ok();
});
let block = block.copy();
let _: () = msg_send![
class!(SCShareableContent),
getShareableContentExcludingDesktopWindows:YES
onScreenWindowsOnly:YES
completionHandler:block];
rx
}
}
#[ctor]
unsafe fn build_classes() {
let mut decl = ClassDecl::new("GPUIStreamDelegate", class!(NSObject)).unwrap();
unsafe {
decl.add_method(
sel!(outputVideoEffectDidStartForStream:),
output_video_effect_did_start_for_stream as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(outputVideoEffectDidStopForStream:),
output_video_effect_did_stop_for_stream as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(stream:didStopWithError:),
stream_did_stop_with_error as extern "C" fn(&Object, Sel, id, id),
);
DELEGATE_CLASS = decl.register();
let mut decl = ClassDecl::new("GPUIStreamOutput", class!(NSObject)).unwrap();
decl.add_method(
sel!(stream:didOutputSampleBuffer:ofType:),
stream_did_output_sample_buffer_of_type
as extern "C" fn(&Object, Sel, id, id, NSInteger),
);
decl.add_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR);
OUTPUT_CLASS = decl.register();
}
}
extern "C" fn output_video_effect_did_start_for_stream(_this: &Object, _: Sel, _stream: id) {}
extern "C" fn output_video_effect_did_stop_for_stream(_this: &Object, _: Sel, _stream: id) {}
extern "C" fn stream_did_stop_with_error(_this: &Object, _: Sel, _stream: id, _error: id) {}
extern "C" fn stream_did_output_sample_buffer_of_type(
this: &Object,
_: Sel,
_stream: id,
sample_buffer: id,
buffer_type: NSInteger,
) {
if buffer_type != SCStreamOutputTypeScreen {
return;
}
unsafe {
let sample_buffer = sample_buffer as CMSampleBufferRef;
let sample_buffer = CMSampleBuffer::wrap_under_get_rule(sample_buffer);
if let Some(buffer) = sample_buffer.image_buffer() {
let callback: Box<Box<dyn Fn(ScreenCaptureFrame)>> =
Box::from_raw(*this.get_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR) as *mut _);
callback(ScreenCaptureFrame(buffer));
mem::forget(callback);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,880 @@
use anyhow::anyhow;
use cocoa::appkit::CGFloat;
use collections::HashMap;
use core_foundation::{
array::{CFArray, CFArrayRef},
attributed_string::CFMutableAttributedString,
base::{CFRange, CFType, TCFType},
number::CFNumber,
string::CFString,
};
use core_graphics::{
base::{CGGlyph, kCGImageAlphaPremultipliedLast},
color_space::CGColorSpace,
context::{CGContext, CGTextDrawingMode},
display::CGPoint,
};
use core_text::{
font::CTFont,
font_collection::CTFontCollectionRef,
font_descriptor::{
CTFontDescriptor, kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait,
kCTFontWidthTrait,
},
line::CTLine,
string_attributes::kCTFontAttributeName,
};
use font_kit::{
font::Font as FontKitFont,
handle::Handle,
hinting::HintingOptions,
metrics::Metrics,
properties::{Style as FontkitStyle, Weight as FontkitWeight},
source::SystemSource,
sources::mem::MemSource,
};
use gpui::{
Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun,
FontStyle, FontWeight, GlyphId, Hsla, LineLayout, Pixels, PlatformTextSystem,
RenderGlyphParams, Result, Rgba, SUBPIXEL_VARIANTS_X, ShapedGlyph, ShapedRun, SharedString,
Size, TextRenderingMode, point, px, size, swap_rgba_pa_to_bgra,
};
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use pathfinder_geometry::{
rect::{RectF, RectI},
transform2d::Transform2F,
vector::Vector2F,
};
use smallvec::SmallVec;
use std::{borrow::Cow, char, convert::TryFrom, sync::Arc, sync::OnceLock};
use crate::open_type::apply_features_and_fallbacks;
#[allow(non_upper_case_globals)]
const kCGImageAlphaOnly: u32 = 7;
/// macOS text system using CoreText for font shaping.
pub struct MacTextSystem(RwLock<MacTextSystemState>);
#[derive(Clone, PartialEq, Eq, Hash)]
struct FontKey {
font_family: SharedString,
font_features: FontFeatures,
font_fallbacks: Option<FontFallbacks>,
}
struct MacTextSystemState {
memory_source: MemSource,
system_source: SystemSource,
fonts: Vec<FontKitFont>,
font_selections: HashMap<Font, FontId>,
font_ids_by_postscript_name: HashMap<String, FontId>,
font_ids_by_font_key: HashMap<FontKey, SmallVec<[FontId; 4]>>,
postscript_names_by_font_id: HashMap<FontId, String>,
}
impl MacTextSystem {
/// Create a new MacTextSystem.
pub fn new() -> Self {
Self(RwLock::new(MacTextSystemState {
memory_source: MemSource::empty(),
system_source: SystemSource::new(),
fonts: Vec::new(),
font_selections: HashMap::default(),
font_ids_by_postscript_name: HashMap::default(),
font_ids_by_font_key: HashMap::default(),
postscript_names_by_font_id: HashMap::default(),
}))
}
}
impl Default for MacTextSystem {
fn default() -> Self {
Self::new()
}
}
impl PlatformTextSystem for MacTextSystem {
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
self.0.write().add_fonts(fonts)
}
fn all_font_names(&self) -> Vec<String> {
let mut names = Vec::new();
let collection = core_text::font_collection::create_for_all_families();
// NOTE: We intentionally avoid using `collection.get_descriptors()` here because
// it has a memory leak bug in core-text v21.0.0. The upstream code uses
// `wrap_under_get_rule` but `CTFontCollectionCreateMatchingFontDescriptors`
// follows the Create Rule (caller owns the result), so it should use
// `wrap_under_create_rule`. We call the function directly with correct memory management.
unsafe extern "C" {
fn CTFontCollectionCreateMatchingFontDescriptors(
collection: CTFontCollectionRef,
) -> CFArrayRef;
}
let descriptors: Option<CFArray<CTFontDescriptor>> = unsafe {
let array_ref =
CTFontCollectionCreateMatchingFontDescriptors(collection.as_concrete_TypeRef());
if array_ref.is_null() {
None
} else {
Some(CFArray::wrap_under_create_rule(array_ref))
}
};
let Some(descriptors) = descriptors else {
return names;
};
for descriptor in descriptors.into_iter() {
names.extend(lenient_font_attributes::family_name(&descriptor));
}
if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() {
names.extend(fonts_in_memory);
}
names
}
fn font_id(&self, font: &Font) -> Result<FontId> {
let lock = self.0.upgradable_read();
if let Some(font_id) = lock.font_selections.get(font) {
Ok(*font_id)
} else {
let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
let font_key = FontKey {
font_family: font.family.clone(),
font_features: font.features.clone(),
font_fallbacks: font.fallbacks.clone(),
};
let candidates = if let Some(font_ids) = lock.font_ids_by_font_key.get(&font_key) {
font_ids.as_slice()
} else {
let font_ids =
lock.load_family(&font.family, &font.features, font.fallbacks.as_ref())?;
lock.font_ids_by_font_key.insert(font_key.clone(), font_ids);
lock.font_ids_by_font_key[&font_key].as_ref()
};
let candidate_properties = candidates
.iter()
.map(|font_id| lock.fonts[font_id.0].properties())
.collect::<SmallVec<[_; 4]>>();
let ix = font_kit::matching::find_best_match(
&candidate_properties,
&font_kit::properties::Properties {
style: fontkit_style(font.style),
weight: fontkit_weight(font.weight),
stretch: Default::default(),
},
)?;
let font_id = candidates[ix];
lock.font_selections.insert(font.clone(), font_id);
Ok(font_id)
}
}
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
font_kit_metrics_to_metrics(self.0.read().fonts[font_id.0].metrics())
}
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
Ok(bounds_from_rect(
self.0.read().fonts[font_id.0].typographic_bounds(glyph_id.0)?,
))
}
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> 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 glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
self.0.read().raster_bounds(params)
}
fn rasterize_glyph(
&self,
glyph_id: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
self.0.read().rasterize_glyph(glyph_id, raster_bounds)
}
fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
self.0.write().layout_line(text, font_size, font_runs)
}
fn recommended_rendering_mode(
&self,
_font_id: FontId,
_font_size: Pixels,
) -> TextRenderingMode {
TextRenderingMode::Grayscale
}
fn glyph_dilation_for_color(&self, color: Hsla) -> u8 {
// When font smoothing is enabled, CoreGraphics thickens glyph strokes by an amount that
// depends on the foreground color's luminance. We replicate the logic used by CoreGraphics
// to select between the different levels of dilation.
if !font_smoothing_allowed_by_user() {
return 0;
}
let rgba: Rgba = color.into();
let luminance = 0.2126 * rgba.r + 0.7152 * rgba.g + 0.0722 * rgba.b;
let level = ((4.0 * luminance) + 0.5).floor() as i32;
level.clamp(0, 4) as u8
}
}
fn font_smoothing_allowed_by_user() -> bool {
static ALLOWED: OnceLock<bool> = OnceLock::new();
*ALLOWED.get_or_init(|| {
use core_foundation_sys::preferences::{
CFPreferencesCopyAppValue, kCFPreferencesCurrentApplication,
};
let key = CFString::new("AppleFontSmoothing");
let value_ref = unsafe {
CFPreferencesCopyAppValue(key.as_concrete_TypeRef(), kCFPreferencesCurrentApplication)
};
if value_ref.is_null() {
return true;
}
let value = unsafe { CFType::wrap_under_create_rule(value_ref) };
let Some(number) = value.downcast_into::<CFNumber>() else {
return true;
};
// Only an explicit value of `0` means that font smoothing is disabled.
number.to_i64() != Some(0)
})
}
impl MacTextSystemState {
fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
let fonts = fonts
.into_iter()
.map(|bytes| match bytes {
Cow::Borrowed(embedded_font) => {
let data_provider = unsafe {
core_graphics::data_provider::CGDataProvider::from_slice(embedded_font)
};
let font = core_graphics::font::CGFont::from_data_provider(data_provider)
.map_err(|()| anyhow!("Could not load an embedded font."))?;
let font = font_kit::loaders::core_text::Font::from_core_graphics_font(font);
Ok(Handle::from_native(&font))
}
Cow::Owned(bytes) => Ok(Handle::from_memory(Arc::new(bytes), 0)),
})
.collect::<Result<Vec<_>>>()?;
self.memory_source.add_fonts(fonts.into_iter())?;
Ok(())
}
fn load_family(
&mut self,
name: &str,
features: &FontFeatures,
fallbacks: Option<&FontFallbacks>,
) -> Result<SmallVec<[FontId; 4]>> {
let name = gpui::font_name_with_fallbacks(name, ".AppleSystemUIFont");
let mut font_ids = SmallVec::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()?;
apply_features_and_fallbacks(&mut font, features, fallbacks)?;
// This block contains a precautionary fix to guard against loading fonts
// that might cause panics due to `.unwrap()`s up the chain.
{
// We use the 'm' character for text measurements in various spots
// (e.g., the editor). However, at time of writing some of those usages
// will panic if the font has no 'm' glyph.
//
// Therefore, we check up front that the font has the necessary glyph.
let has_m_glyph = font.glyph_for_char('m').is_some();
// HACK: The 'Segoe Fluent Icons' font does not have an 'm' glyph,
// but we need to be able to load it for rendering Windows icons in
// the Storybook (on macOS).
let is_segoe_fluent_icons = font.full_name() == "Segoe Fluent Icons";
if !has_m_glyph && !is_segoe_fluent_icons {
// I spent far too long trying to track down why a font missing the 'm'
// character wasn't loading. This log statement will hopefully save
// someone else from suffering the same fate.
log::warn!(
"font '{}' has no 'm' character and was not loaded",
font.full_name()
);
continue;
}
}
// We've seen a number of panics in production caused by calling font.properties()
// which unwraps a downcast to CFNumber. This is an attempt to avoid the panic,
// and to try and identify the incalcitrant font.
let traits = font.native_font().all_traits();
if unsafe {
!(traits
.get(kCTFontSymbolicTrait)
.downcast::<CFNumber>()
.is_some()
&& traits
.get(kCTFontWidthTrait)
.downcast::<CFNumber>()
.is_some()
&& traits
.get(kCTFontWeightTrait)
.downcast::<CFNumber>()
.is_some()
&& traits
.get(kCTFontSlantTrait)
.downcast::<CFNumber>()
.is_some())
} {
log::error!(
"Failed to read traits for font {:?}",
font.postscript_name().unwrap()
);
continue;
}
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 advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
Ok(size_from_vector2f(
self.fonts[font_id.0].advance(glyph_id.0)?,
))
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
self.fonts[font_id.0].glyph_for_char(ch).map(GlyphId)
}
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)
.is_some_and(|postscript_name| {
postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI"
})
}
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let font = &self.fonts[params.font_id.0];
let scale = Transform2F::from_scale(params.scale_factor);
let bounds: Bounds<DevicePixels> = bounds_from_rect_i(font.raster_bounds(
params.glyph_id.0,
params.font_size.into(),
scale,
HintingOptions::None,
font_kit::canvas::RasterizationOptions::GrayscaleAa,
)?);
// Expand the bounds by 1 pixel on each side to give CG room for anti-aliasing.
Ok(bounds.dilate(DevicePixels(1)))
}
fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
glyph_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
anyhow::bail!("glyph bounds are empty");
} else {
// Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing.
let mut bitmap_size = glyph_bounds.size;
if params.subpixel_variant.x > 0 {
bitmap_size.width += DevicePixels(1);
}
if params.subpixel_variant.y > 0 {
bitmap_size.height += DevicePixels(1);
}
let bitmap_size = bitmap_size;
let mut bytes;
let cx;
if params.is_emoji {
bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize];
cx = CGContext::create_bitmap_context(
Some(bytes.as_mut_ptr() as *mut _),
bitmap_size.width.0 as usize,
bitmap_size.height.0 as usize,
8,
bitmap_size.width.0 as usize * 4,
&CGColorSpace::create_device_rgb(),
kCGImageAlphaPremultipliedLast,
);
} else {
bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize];
cx = CGContext::create_bitmap_context(
Some(bytes.as_mut_ptr() as *mut _),
bitmap_size.width.0 as usize,
bitmap_size.height.0 as usize,
8,
bitmap_size.width.0 as usize,
&CGColorSpace::create_device_gray(),
kCGImageAlphaOnly,
);
}
// 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.0 as CGFloat,
(glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat,
);
cx.scale(
params.scale_factor as CGFloat,
params.scale_factor as CGFloat,
);
let subpixel_shift = params
.subpixel_variant
.map(|v| v as f32 / SUBPIXEL_VARIANTS_X as f32);
cx.set_text_drawing_mode(CGTextDrawingMode::CGTextFill);
cx.set_allows_antialiasing(true);
cx.set_should_antialias(true);
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);
if params.dilation > 0 {
let luminance = params.dilation as f64 * 0.25;
cx.set_should_smooth_fonts(true);
cx.set_gray_fill_color(luminance, 1.0);
} else {
cx.set_gray_fill_color(0.0, 1.0);
}
self.fonts[params.font_id.0]
.native_font()
.clone_with_font_size(f32::from(params.font_size) as CGFloat)
.draw_glyphs(
&[params.glyph_id.0 as CGGlyph],
&[CGPoint::new(
(subpixel_shift.x / params.scale_factor) as CGFloat,
(subpixel_shift.y / params.scale_factor) as CGFloat,
)],
cx,
);
if params.is_emoji {
// Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
for pixel in bytes.chunks_exact_mut(4) {
swap_rgba_pa_to_bgra(pixel);
}
}
Ok((bitmap_size, bytes))
}
}
fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
// Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
let mut string = CFMutableAttributedString::new();
let mut max_ascent = 0.0f32;
let mut max_descent = 0.0f32;
{
let mut text = text;
let mut break_ligature = true;
for run in font_runs {
let text_run;
(text_run, text) = text.split_at(run.len);
let utf16_start = string.char_len(); // insert at end of string
// note: replace_str may silently ignore codepoints it dislikes (e.g., BOM at start of string)
string.replace_str(&CFString::new(text_run), CFRange::init(utf16_start, 0));
let utf16_end = string.char_len();
let length = utf16_end - utf16_start;
let cf_range = CFRange::init(utf16_start, length);
let font = &self.fonts[run.font_id.0];
let font_metrics = font.metrics();
let font_scale = f32::from(font_size) / font_metrics.units_per_em as f32;
max_ascent = max_ascent.max(font_metrics.ascent * font_scale);
max_descent = max_descent.max(-font_metrics.descent * font_scale);
let font_size = if break_ligature {
px(f32::from(font_size).next_up())
} else {
font_size
};
unsafe {
string.set_attribute(
cf_range,
kCTFontAttributeName,
&font.native_font().clone_with_font_size(font_size.into()),
);
}
break_ligature = !break_ligature;
}
}
// 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 glyph_runs = line.glyph_runs();
let mut runs = <Vec<ShapedRun>>::with_capacity(glyph_runs.len() as usize);
let mut ix_converter = StringIndexConverter::new(text);
for run in 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 glyphs = match runs.last_mut() {
Some(run) if run.font_id == font_id => &mut run.glyphs,
_ => {
runs.push(ShapedRun {
font_id,
glyphs: Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0)),
});
&mut runs.last_mut().unwrap().glyphs
}
};
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();
if ix_converter.utf16_ix > glyph_utf16_ix {
// We cannot reuse current index converter, as it can only seek forward. Restart the search.
ix_converter = StringIndexConverter::new(text);
}
ix_converter.advance_to_utf16_ix(glyph_utf16_ix);
glyphs.push(ShapedGlyph {
id: GlyphId(glyph_id as u32),
position: point(position.x as f32, position.y as f32).map(px),
index: ix_converter.utf8_ix,
is_emoji: self.is_emoji(font_id),
});
}
}
let typographic_bounds = line.get_typographic_bounds();
LineLayout {
runs,
font_size,
width: typographic_bounds.width.into(),
ascent: max_ascent.into(),
descent: max_descent.into(),
len: text.len(),
}
}
}
#[derive(Debug, Clone)]
struct StringIndexConverter<'a> {
text: &'a str,
/// Index in UTF-8 bytes
utf8_ix: usize,
/// Index in UTF-16 code units
utf16_ix: usize,
}
impl<'a> StringIndexConverter<'a> {
fn new(text: &'a str) -> Self {
Self {
text,
utf8_ix: 0,
utf16_ix: 0,
}
}
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();
}
}
fn font_kit_metrics_to_metrics(metrics: Metrics) -> FontMetrics {
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: bounds_from_rect(metrics.bounding_box),
}
}
fn bounds_from_rect(rect: RectF) -> Bounds<f32> {
Bounds {
origin: point(rect.origin_x(), rect.origin_y()),
size: size(rect.width(), rect.height()),
}
}
fn bounds_from_rect_i(rect: RectI) -> Bounds<DevicePixels> {
Bounds {
origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
}
}
// impl From<Vector2I> for Size<DevicePixels> {
// fn from(value: Vector2I) -> Self {
// size(value.x().into(), value.y().into())
// }
// }
// impl From<RectI> for Bounds<i32> {
// fn from(rect: RectI) -> Self {
// Bounds {
// origin: point(rect.origin_x(), rect.origin_y()),
// size: size(rect.width(), rect.height()),
// }
// }
// }
// impl From<Point<u32>> for Vector2I {
// fn from(size: Point<u32>) -> Self {
// Vector2I::new(size.x as i32, size.y as i32)
// }
// }
fn size_from_vector2f(vec: Vector2F) -> Size<f32> {
size(vec.x(), vec.y())
}
fn fontkit_weight(value: FontWeight) -> FontkitWeight {
FontkitWeight(value.0)
}
fn fontkit_style(style: FontStyle) -> FontkitStyle {
match style {
FontStyle::Normal => FontkitStyle::Normal,
FontStyle::Italic => FontkitStyle::Italic,
FontStyle::Oblique => FontkitStyle::Oblique,
}
}
// Some fonts may have no attributes despite `core_text` requiring them (and panicking).
// This is the same version as `core_text` has without `expect` calls.
mod lenient_font_attributes {
use core_foundation::{
base::{CFRetain, CFType, TCFType},
string::{CFString, CFStringRef},
};
use core_text::font_descriptor::{
CTFontDescriptor, CTFontDescriptorCopyAttribute, kCTFontFamilyNameAttribute,
};
pub fn family_name(descriptor: &CTFontDescriptor) -> Option<String> {
unsafe { get_string_attribute(descriptor, kCTFontFamilyNameAttribute) }
}
fn get_string_attribute(
descriptor: &CTFontDescriptor,
attribute: CFStringRef,
) -> Option<String> {
unsafe {
let value = CTFontDescriptorCopyAttribute(descriptor.as_concrete_TypeRef(), attribute);
if value.is_null() {
return None;
}
let value = CFType::wrap_under_create_rule(value);
assert!(value.instance_of::<CFString>());
let s = wrap_under_get_rule(value.as_CFTypeRef() as CFStringRef);
Some(s.to_string())
}
}
unsafe fn wrap_under_get_rule(reference: CFStringRef) -> CFString {
unsafe {
assert!(!reference.is_null(), "Attempted to create a NULL object.");
let reference = CFRetain(reference as *const ::std::os::raw::c_void) as CFStringRef;
TCFType::wrap_under_create_rule(reference)
}
}
}
#[cfg(test)]
mod tests {
use crate::MacTextSystem;
use gpui::{FontRun, GlyphId, PlatformTextSystem, font, px};
#[test]
fn test_layout_line_bom_char() {
let fonts = MacTextSystem::new();
let font_id = fonts.font_id(&font("Helvetica")).unwrap();
let line = "\u{feff}";
let mut style = FontRun {
font_id,
len: line.len(),
};
let layout = fonts.layout_line(line, px(16.), &[style]);
assert_eq!(layout.len, line.len());
assert!(layout.runs.is_empty());
let line = "a\u{feff}b";
style.len = line.len();
let layout = fonts.layout_line(line, px(16.), &[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, GlyphId(68u32)); // a
// There's no glyph for \u{feff}
assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b
let line = "\u{feff}ab";
let font_runs = &[
FontRun {
len: "\u{feff}".len(),
font_id,
},
FontRun {
len: "ab".len(),
font_id,
},
];
let layout = fonts.layout_line(line, px(16.), font_runs);
assert_eq!(layout.len, line.len());
assert_eq!(layout.runs.len(), 1);
assert_eq!(layout.runs[0].glyphs.len(), 2);
// There's no glyph for \u{feff}
assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a
assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b
}
#[test]
fn test_layout_line_zwnj_insertion() {
let fonts = MacTextSystem::new();
let font_id = fonts.font_id(&font("Helvetica")).unwrap();
let text = "hello world";
let font_runs = &[
FontRun { font_id, len: 5 }, // "hello"
FontRun { font_id, len: 6 }, // " world"
];
let layout = fonts.layout_line(text, px(16.), font_runs);
assert_eq!(layout.len, text.len());
for run in &layout.runs {
for glyph in &run.glyphs {
assert!(
glyph.index < text.len(),
"Glyph index {} is out of bounds for text length {}",
glyph.index,
text.len()
);
}
}
// Test with different font runs - should not insert ZWNJ
let font_id2 = fonts.font_id(&font("Times")).unwrap_or(font_id);
let font_runs_different = &[
FontRun { font_id, len: 5 }, // "hello"
// " world"
FontRun {
font_id: font_id2,
len: 6,
},
];
let layout2 = fonts.layout_line(text, px(16.), font_runs_different);
assert_eq!(layout2.len, text.len());
for run in &layout2.runs {
for glyph in &run.glyphs {
assert!(
glyph.index < text.len(),
"Glyph index {} is out of bounds for text length {}",
glyph.index,
text.len()
);
}
}
}
#[test]
fn test_layout_line_zwnj_edge_cases() {
let fonts = MacTextSystem::new();
let font_id = fonts.font_id(&font("Helvetica")).unwrap();
let text = "hello";
let font_runs = &[FontRun { font_id, len: 5 }];
let layout = fonts.layout_line(text, px(16.), font_runs);
assert_eq!(layout.len, text.len());
let text = "abc";
let font_runs = &[
FontRun { font_id, len: 1 }, // "a"
FontRun { font_id, len: 1 }, // "b"
FontRun { font_id, len: 1 }, // "c"
];
let layout = fonts.layout_line(text, px(16.), font_runs);
assert_eq!(layout.len, text.len());
for run in &layout.runs {
for glyph in &run.glyphs {
assert!(
glyph.index < text.len(),
"Glyph index {} is out of bounds for text length {}",
glyph.index,
text.len()
);
}
}
// Test with empty text
let text = "";
let font_runs = &[];
let layout = fonts.layout_line(text, px(16.), font_runs);
assert_eq!(layout.len, 0);
assert!(layout.runs.is_empty());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
use cocoa::{
appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight},
base::id,
foundation::NSString,
};
use gpui::WindowAppearance;
use objc::{msg_send, sel, sel_impl};
use std::ffi::CStr;
pub(crate) unsafe fn window_appearance_from_native(appearance: id) -> WindowAppearance {
let name: id = msg_send![appearance, name];
unsafe {
if name == NSAppearanceNameVibrantLight {
WindowAppearance::VibrantLight
} else if name == NSAppearanceNameVibrantDark {
WindowAppearance::VibrantDark
} else if name == NSAppearanceNameAqua {
WindowAppearance::Light
} else if name == NSAppearanceNameDarkAqua {
WindowAppearance::Dark
} else {
println!(
"unknown appearance: {:?}",
CStr::from_ptr(name.UTF8String())
);
WindowAppearance::Light
}
}
}
#[link(name = "AppKit", kind = "framework")]
unsafe extern "C" {
pub static NSAppearanceNameAqua: id;
pub static NSAppearanceNameDarkAqua: id;
}