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:
73
crates/gpui_macos/Cargo.toml
Normal file
73
crates/gpui_macos/Cargo.toml
Normal 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"] }
|
||||
1
crates/gpui_macos/LICENSE-APACHE
Symbolic link
1
crates/gpui_macos/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
174
crates/gpui_macos/build.rs
Normal file
174
crates/gpui_macos/build.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
201
crates/gpui_macos/src/dispatcher.rs
Normal file
201
crates/gpui_macos/src/dispatcher.rs
Normal 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);
|
||||
}
|
||||
169
crates/gpui_macos/src/display.rs
Normal file
169
crates/gpui_macos/src/display.rs
Normal 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
|
||||
}
|
||||
}
|
||||
266
crates/gpui_macos/src/display_link.rs
Normal file
266
crates/gpui_macos/src/display_link.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
574
crates/gpui_macos/src/events.rs
Normal file
574
crates/gpui_macos/src/events.rs
Normal 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()
|
||||
}
|
||||
136
crates/gpui_macos/src/gpui_macos.rs
Normal file
136
crates/gpui_macos/src/gpui_macos.rs
Normal 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() }
|
||||
}
|
||||
1502
crates/gpui_macos/src/keyboard.rs
Normal file
1502
crates/gpui_macos/src/keyboard.rs
Normal file
File diff suppressed because it is too large
Load Diff
349
crates/gpui_macos/src/metal_atlas.rs
Normal file
349
crates/gpui_macos/src/metal_atlas.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
1709
crates/gpui_macos/src/metal_renderer.rs
Normal file
1709
crates/gpui_macos/src/metal_renderer.rs
Normal file
File diff suppressed because it is too large
Load Diff
152
crates/gpui_macos/src/open_type.rs
Normal file
152
crates/gpui_macos/src/open_type.rs
Normal 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;
|
||||
}
|
||||
531
crates/gpui_macos/src/pasteboard.rs
Normal file
531
crates/gpui_macos/src/pasteboard.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
1431
crates/gpui_macos/src/platform.rs
Normal file
1431
crates/gpui_macos/src/platform.rs
Normal file
File diff suppressed because it is too large
Load Diff
344
crates/gpui_macos/src/screen_capture.rs
Normal file
344
crates/gpui_macos/src/screen_capture.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
1266
crates/gpui_macos/src/shaders.metal
Normal file
1266
crates/gpui_macos/src/shaders.metal
Normal file
File diff suppressed because it is too large
Load Diff
880
crates/gpui_macos/src/text_system.rs
Normal file
880
crates/gpui_macos/src/text_system.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
2984
crates/gpui_macos/src/window.rs
Normal file
2984
crates/gpui_macos/src/window.rs
Normal file
File diff suppressed because it is too large
Load Diff
35
crates/gpui_macos/src/window_appearance.rs
Normal file
35
crates/gpui_macos/src/window_appearance.rs
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user