Present Servo BGRA hardware surfaces
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
use cocoa::base::id;
|
||||
use cocoa::foundation::NSRange;
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
|
||||
/// The `cocoa` crate does not define NSAttributedString (and related Cocoa classes),
|
||||
/// which are needed for copying rich text (that is, text intermingled with images)
|
||||
/// to the clipboard. This adds access to those APIs.
|
||||
#[allow(non_snake_case)]
|
||||
pub trait NSAttributedString: Sized {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSAttributedString), alloc]
|
||||
}
|
||||
|
||||
unsafe fn init_attributed_string(self, string: id) -> id;
|
||||
unsafe fn appendAttributedString_(self, attr_string: id);
|
||||
unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id;
|
||||
unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id;
|
||||
unsafe fn string(self) -> id;
|
||||
}
|
||||
|
||||
impl NSAttributedString for id {
|
||||
unsafe fn init_attributed_string(self, string: id) -> id {
|
||||
msg_send![self, initWithString: string]
|
||||
}
|
||||
|
||||
unsafe fn appendAttributedString_(self, attr_string: id) {
|
||||
let _: () = msg_send![self, appendAttributedString: attr_string];
|
||||
}
|
||||
|
||||
unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id {
|
||||
msg_send![self, RTFDFromRange: range documentAttributes: attrs]
|
||||
}
|
||||
|
||||
unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id {
|
||||
msg_send![self, RTFFromRange: range documentAttributes: attrs]
|
||||
}
|
||||
|
||||
unsafe fn string(self) -> id {
|
||||
msg_send![self, string]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NSMutableAttributedString: NSAttributedString {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSMutableAttributedString), alloc]
|
||||
}
|
||||
}
|
||||
|
||||
impl NSMutableAttributedString for id {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cocoa::appkit::NSImage;
|
||||
use cocoa::base::nil;
|
||||
use cocoa::foundation::NSString;
|
||||
#[test]
|
||||
#[ignore] // This was SIGSEGV-ing on CI but not locally; need to investigate https://github.com/zed-industries/zed/actions/runs/10362363230/job/28684225486?pr=15782#step:4:1348
|
||||
fn test_nsattributed_string() {
|
||||
// TODO move these to parent module once it's actually ready to be used
|
||||
#[allow(non_snake_case)]
|
||||
pub trait NSTextAttachment: Sized {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSTextAttachment), alloc]
|
||||
}
|
||||
}
|
||||
|
||||
impl NSTextAttachment for id {}
|
||||
|
||||
unsafe {
|
||||
let image: id = msg_send![class!(NSImage), alloc];
|
||||
image.initWithContentsOfFile_(NSString::alloc(nil).init_str("test.jpeg"));
|
||||
let _size = image.size();
|
||||
|
||||
let string = NSString::alloc(nil).init_str("Test String");
|
||||
let attr_string = NSMutableAttributedString::alloc(nil).init_attributed_string(string);
|
||||
let hello_string = NSString::alloc(nil).init_str("Hello World");
|
||||
let hello_attr_string =
|
||||
NSAttributedString::alloc(nil).init_attributed_string(hello_string);
|
||||
attr_string.appendAttributedString_(hello_attr_string);
|
||||
|
||||
let attachment = NSTextAttachment::alloc(nil);
|
||||
let _: () = msg_send![attachment, setImage: image];
|
||||
let image_attr_string =
|
||||
msg_send![class!(NSAttributedString), attributedStringWithAttachment: attachment];
|
||||
attr_string.appendAttributedString_(image_attr_string);
|
||||
|
||||
let another_string = NSString::alloc(nil).init_str("Another String");
|
||||
let another_attr_string =
|
||||
NSAttributedString::alloc(nil).init_attributed_string(another_string);
|
||||
attr_string.appendAttributedString_(another_attr_string);
|
||||
|
||||
let _len: cocoa::foundation::NSUInteger = msg_send![attr_string, length];
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// pasteboard.clearContents();
|
||||
|
||||
let rtfd_data = attr_string.RTFDFromRange_documentAttributes_(
|
||||
NSRange::new(0, msg_send![attr_string, length]),
|
||||
nil,
|
||||
);
|
||||
assert_ne!(rtfd_data, nil);
|
||||
// if rtfd_data != nil {
|
||||
// pasteboard.setData_forType(rtfd_data, NSPasteboardTypeRTFD);
|
||||
// }
|
||||
|
||||
// let rtf_data = attributed_string.RTFFromRange_documentAttributes_(
|
||||
// NSRange::new(0, attributed_string.length()),
|
||||
// nil,
|
||||
// );
|
||||
// if rtf_data != nil {
|
||||
// pasteboard.setData_forType(rtf_data, NSPasteboardTypeRTF);
|
||||
// }
|
||||
|
||||
// let plain_text = attributed_string.string();
|
||||
// pasteboard.setString_forType(plain_text, NSPasteboardTypeString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <dispatch/source.h>
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::{PlatformDispatcher, TaskLabel};
|
||||
use async_task::Runnable;
|
||||
use objc::{
|
||||
class, msg_send,
|
||||
runtime::{BOOL, YES},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
ptr::{NonNull, addr_of},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// All items in the generated file are marked as pub, so we're gonna wrap it in a separate mod to prevent
|
||||
/// these pub items from leaking into public API.
|
||||
pub(crate) mod dispatch_sys {
|
||||
include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
|
||||
}
|
||||
|
||||
use dispatch_sys::*;
|
||||
pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t {
|
||||
addr_of!(_dispatch_main_q) as *const _ as dispatch_queue_t
|
||||
}
|
||||
|
||||
pub(crate) struct MacDispatcher;
|
||||
|
||||
impl PlatformDispatcher for MacDispatcher {
|
||||
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: Runnable, _: Option<TaskLabel>) {
|
||||
unsafe {
|
||||
dispatch_async_f(
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0),
|
||||
runnable.into_raw().as_ptr() as *mut c_void,
|
||||
Some(trampoline),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: Runnable) {
|
||||
unsafe {
|
||||
dispatch_async_f(
|
||||
dispatch_get_main_queue(),
|
||||
runnable.into_raw().as_ptr() as *mut c_void,
|
||||
Some(trampoline),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
|
||||
unsafe {
|
||||
let queue =
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0);
|
||||
let when = dispatch_time(DISPATCH_TIME_NOW as u64, duration.as_nanos() as i64);
|
||||
dispatch_after_f(
|
||||
when,
|
||||
queue,
|
||||
runnable.into_raw().as_ptr() as *mut c_void,
|
||||
Some(trampoline),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn trampoline(runnable: *mut c_void) {
|
||||
let task = unsafe { Runnable::<()>::from_raw(NonNull::new_unchecked(runnable as *mut ())) };
|
||||
task.run();
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
use crate::{Bounds, DisplayId, Pixels, PlatformDisplay, px, size};
|
||||
use anyhow::Result;
|
||||
use cocoa::{
|
||||
appkit::NSScreen,
|
||||
base::{id, nil},
|
||||
foundation::{NSDictionary, NSString},
|
||||
};
|
||||
use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef};
|
||||
use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList};
|
||||
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 = NSString::alloc(nil).init_str("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(self.0)
|
||||
}
|
||||
|
||||
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) };
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
use crate::{
|
||||
dispatch_get_main_queue,
|
||||
dispatch_sys::{
|
||||
_dispatch_source_type_data_add, dispatch_resume, dispatch_set_context,
|
||||
dispatch_source_cancel, dispatch_source_create, dispatch_source_merge_data,
|
||||
dispatch_source_set_event_handler_f, dispatch_source_t, dispatch_suspend,
|
||||
},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use core_graphics::display::CGDirectDisplayID;
|
||||
use std::ffi::c_void;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct DisplayLink {
|
||||
display_link: Option<sys::DisplayLink>,
|
||||
frame_requests: dispatch_source_t,
|
||||
}
|
||||
|
||||
impl DisplayLink {
|
||||
pub fn new(
|
||||
display_id: CGDirectDisplayID,
|
||||
data: *mut c_void,
|
||||
callback: unsafe 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 dispatch_source_t;
|
||||
dispatch_source_merge_data(frame_requests, 1);
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let frame_requests = dispatch_source_create(
|
||||
&_dispatch_source_type_data_add,
|
||||
0,
|
||||
0,
|
||||
dispatch_get_main_queue(),
|
||||
);
|
||||
dispatch_set_context(
|
||||
crate::dispatch_sys::dispatch_object_t {
|
||||
_ds: frame_requests,
|
||||
},
|
||||
data,
|
||||
);
|
||||
dispatch_source_set_event_handler_f(frame_requests, Some(callback));
|
||||
|
||||
let display_link = sys::DisplayLink::new(
|
||||
display_id,
|
||||
display_link_callback,
|
||||
frame_requests as *mut c_void,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
display_link: Some(display_link),
|
||||
frame_requests,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) -> Result<()> {
|
||||
unsafe {
|
||||
dispatch_resume(crate::dispatch_sys::dispatch_object_t {
|
||||
_ds: self.frame_requests,
|
||||
});
|
||||
self.display_link.as_mut().unwrap().start()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) -> Result<()> {
|
||||
unsafe {
|
||||
dispatch_suspend(crate::dispatch_sys::dispatch_object_t {
|
||||
_ds: self.frame_requests,
|
||||
});
|
||||
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());
|
||||
unsafe {
|
||||
dispatch_source_cancel(self.frame_requests);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+533
@@ -0,0 +1,533 @@
|
||||
use crate::{
|
||||
Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
|
||||
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||
platform::mac::{
|
||||
LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource,
|
||||
TISGetInputSourceProperty, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData,
|
||||
},
|
||||
point, px,
|
||||
};
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformInput {
|
||||
pub(crate) unsafe fn from_native(
|
||||
native_event: id,
|
||||
window_height: Option<Pixels>,
|
||||
) -> Option<Self> {
|
||||
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(Self::ModifiersChanged(ModifiersChangedEvent {
|
||||
modifiers: read_modifiers(native_event),
|
||||
capslock: Capslock {
|
||||
on: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSAlphaShiftKeyMask),
|
||||
},
|
||||
}))
|
||||
}
|
||||
NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent {
|
||||
keystroke: parse_keystroke(native_event),
|
||||
is_held: native_event.isARepeat() == YES,
|
||||
})),
|
||||
NSEventType::NSKeyUp => Some(Self::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| {
|
||||
Self::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| {
|
||||
Self::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,
|
||||
})
|
||||
})
|
||||
}
|
||||
// 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| {
|
||||
Self::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::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)
|
||||
};
|
||||
|
||||
Self::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| {
|
||||
Self::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| {
|
||||
Self::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| {
|
||||
Self::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 mut 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()
|
||||
}
|
||||
+1500
File diff suppressed because it is too large
Load Diff
+281
@@ -0,0 +1,281 @@
|
||||
use crate::{
|
||||
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
|
||||
Point, Size, platform::AtlasTextureList,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::FxHashMap;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use etagere::BucketedAtlasAllocator;
|
||||
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) -> Self {
|
||||
MetalAtlas(Mutex::new(MetalAtlasState {
|
||||
device: AssertSend(device),
|
||||
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>,
|
||||
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.clone()))
|
||||
} 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.clone());
|
||||
Ok(Some(tile))
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&self, key: &AtlasKey) {
|
||||
let mut lock = self.0.lock();
|
||||
let Some(id) = lock.tiles_by_key.get(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,
|
||||
};
|
||||
|
||||
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);
|
||||
lock.tiles_by_key.remove(key);
|
||||
} 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,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
texture_descriptor.set_pixel_format(pixel_format);
|
||||
texture_descriptor.set_usage(usage);
|
||||
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,
|
||||
};
|
||||
|
||||
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.into()),
|
||||
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 {
|
||||
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
|
||||
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
|
||||
};
|
||||
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.into())?;
|
||||
let tile = AtlasTile {
|
||||
texture_id: self.id,
|
||||
tile_id: allocation.id.into(),
|
||||
bounds: Bounds {
|
||||
origin: allocation.rectangle.min.into(),
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size<DevicePixels>> for etagere::Size {
|
||||
fn from(size: Size<DevicePixels>) -> Self {
|
||||
etagere::Size::new(size.width.into(), size.height.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Point> for Point<DevicePixels> {
|
||||
fn from(value: etagere::Point) -> Self {
|
||||
Point {
|
||||
x: DevicePixels::from(value.x),
|
||||
y: DevicePixels::from(value.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Size> for Size<DevicePixels> {
|
||||
fn from(size: etagere::Size) -> Self {
|
||||
Size {
|
||||
width: DevicePixels::from(size.width),
|
||||
height: DevicePixels::from(size.height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Rectangle> for Bounds<DevicePixels> {
|
||||
fn from(rectangle: etagere::Rectangle) -> Self {
|
||||
Bounds {
|
||||
origin: rectangle.min.into(),
|
||||
size: rectangle.size().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut)]
|
||||
struct AssertSend<T>(T);
|
||||
|
||||
unsafe impl<T> Send for AssertSend<T> {}
|
||||
+1390
File diff suppressed because it is too large
Load Diff
+147
@@ -0,0 +1,147 @@
|
||||
#![allow(unused, non_upper_case_globals)]
|
||||
|
||||
use crate::{FontFallbacks, FontFeatures};
|
||||
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 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,
|
||||
);
|
||||
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;
|
||||
}
|
||||
+1709
File diff suppressed because it is too large
Load Diff
+334
@@ -0,0 +1,334 @@
|
||||
use crate::{
|
||||
DevicePixels, ForegroundExecutor, SharedString, SourceMetadata,
|
||||
platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream},
|
||||
size,
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
base::{YES, id, nil},
|
||||
foundation::{NSArray, NSString},
|
||||
};
|
||||
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 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 super::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];
|
||||
|
||||
let (mut 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];
|
||||
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 message: id = msg_send![error, localizedDescription];
|
||||
Err(anyhow!("failed to stop 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 { NSString::alloc(nil).init_str("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 (mut 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(mut 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1246
File diff suppressed because it is too large
Load Diff
+388
@@ -0,0 +1,388 @@
|
||||
use crate::{
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
platform::{
|
||||
self,
|
||||
mac::{platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer},
|
||||
Event, FontSystem, WindowBounds,
|
||||
},
|
||||
Scene,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSScreen, NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow},
|
||||
base::{id, nil, YES},
|
||||
foundation::{NSPoint, NSRect, NSSize},
|
||||
};
|
||||
use ctor::ctor;
|
||||
use foreign_types::ForeignTypeRef;
|
||||
use objc::{
|
||||
class,
|
||||
declare::ClassDecl,
|
||||
msg_send,
|
||||
rc::StrongPtr,
|
||||
runtime::{Class, Object, Protocol, Sel},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ffi::c_void,
|
||||
ptr,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use super::screen::Screen;
|
||||
|
||||
static mut VIEW_CLASS: *const Class = ptr::null();
|
||||
const STATE_IVAR: &str = "state";
|
||||
|
||||
#[ctor]
|
||||
unsafe fn build_classes() {
|
||||
VIEW_CLASS = {
|
||||
let mut decl = ClassDecl::new("GPUIStatusItemView", class!(NSView)).unwrap();
|
||||
decl.add_ivar::<*mut c_void>(STATE_IVAR);
|
||||
|
||||
decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));
|
||||
|
||||
decl.add_method(
|
||||
sel!(mouseDown:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(mouseUp:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(rightMouseDown:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(rightMouseUp:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(otherMouseDown:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(otherMouseUp:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(mouseMoved:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(mouseDragged:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(scrollWheel:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(flagsChanged:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(makeBackingLayer),
|
||||
make_backing_layer as extern "C" fn(&Object, Sel) -> id,
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(viewDidChangeEffectiveAppearance),
|
||||
view_did_change_effective_appearance as extern "C" fn(&Object, Sel),
|
||||
);
|
||||
|
||||
decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
|
||||
decl.add_method(
|
||||
sel!(displayLayer:),
|
||||
display_layer as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
|
||||
decl.register()
|
||||
};
|
||||
}
|
||||
|
||||
pub struct StatusItem(Rc<RefCell<StatusItemState>>);
|
||||
|
||||
struct StatusItemState {
|
||||
native_item: StrongPtr,
|
||||
native_view: StrongPtr,
|
||||
renderer: Renderer,
|
||||
scene: Option<Scene>,
|
||||
event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
|
||||
appearance_changed_callback: Option<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
impl StatusItem {
|
||||
pub fn add(fonts: Arc<dyn FontSystem>) -> Self {
|
||||
unsafe {
|
||||
let renderer = Renderer::new(false, fonts);
|
||||
let status_bar = NSStatusBar::systemStatusBar(nil);
|
||||
let native_item =
|
||||
StrongPtr::retain(status_bar.statusItemWithLength_(NSSquareStatusItemLength));
|
||||
|
||||
let button = native_item.button();
|
||||
let _: () = msg_send![button, setHidden: YES];
|
||||
|
||||
let native_view = msg_send![VIEW_CLASS, alloc];
|
||||
let state = Rc::new(RefCell::new(StatusItemState {
|
||||
native_item,
|
||||
native_view: StrongPtr::new(native_view),
|
||||
renderer,
|
||||
scene: None,
|
||||
event_callback: None,
|
||||
appearance_changed_callback: None,
|
||||
}));
|
||||
|
||||
let parent_view = button.superview().superview();
|
||||
NSView::initWithFrame_(
|
||||
native_view,
|
||||
NSRect::new(NSPoint::new(0., 0.), NSView::frame(parent_view).size),
|
||||
);
|
||||
(*native_view).set_ivar(
|
||||
STATE_IVAR,
|
||||
Weak::into_raw(Rc::downgrade(&state)) as *const c_void,
|
||||
);
|
||||
native_view.setWantsBestResolutionOpenGLSurface_(YES);
|
||||
native_view.setWantsLayer(YES);
|
||||
let _: () = msg_send![
|
||||
native_view,
|
||||
setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
|
||||
];
|
||||
|
||||
parent_view.addSubview_(native_view);
|
||||
|
||||
{
|
||||
let state = state.borrow();
|
||||
let layer = state.renderer.layer();
|
||||
let scale_factor = state.scale_factor();
|
||||
let size = state.content_size() * scale_factor;
|
||||
layer.set_contents_scale(scale_factor.into());
|
||||
layer.set_drawable_size(metal::CGSize::new(size.x().into(), size.y().into()));
|
||||
}
|
||||
|
||||
Self(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::Window for StatusItem {
|
||||
fn bounds(&self) -> WindowBounds {
|
||||
self.0.borrow().bounds()
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Vector2F {
|
||||
self.0.borrow().content_size()
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.0.borrow().scale_factor()
|
||||
}
|
||||
|
||||
fn appearance(&self) -> platform::Appearance {
|
||||
unsafe {
|
||||
let appearance: id =
|
||||
msg_send![self.0.borrow().native_item.button(), effectiveAppearance];
|
||||
platform::Appearance::from_native(appearance)
|
||||
}
|
||||
}
|
||||
|
||||
fn screen(&self) -> Rc<dyn platform::Screen> {
|
||||
unsafe {
|
||||
Rc::new(Screen {
|
||||
native_screen: self.0.borrow().native_window().screen(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Vector2F {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, _: Box<dyn platform::InputHandler>) {}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_: crate::platform::PromptLevel,
|
||||
_: &str,
|
||||
_: &[&str],
|
||||
) -> postage::oneshot::Receiver<usize> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_title(&mut self, _: &str) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_edited(&mut self, _: bool) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn show_character_palette(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn zoom(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn present_scene(&mut self, scene: Scene) {
|
||||
self.0.borrow_mut().scene = Some(scene);
|
||||
unsafe {
|
||||
let _: () = msg_send![*self.0.borrow().native_view, setNeedsDisplay: YES];
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_fullscreen(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_event(&mut self, callback: Box<dyn FnMut(platform::Event) -> bool>) {
|
||||
self.0.borrow_mut().event_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_active_status_change(&mut self, _: Box<dyn FnMut(bool)>) {}
|
||||
|
||||
fn on_resize(&mut self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_fullscreen(&mut self, _: Box<dyn FnMut(bool)>) {}
|
||||
|
||||
fn on_moved(&mut self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_should_close(&mut self, _: Box<dyn FnMut() -> bool>) {}
|
||||
|
||||
fn on_close(&mut self, _: Box<dyn FnOnce()>) {}
|
||||
|
||||
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
|
||||
self.0.borrow_mut().appearance_changed_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn is_topmost_for_position(&self, _: Vector2F) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemState {
|
||||
fn bounds(&self) -> WindowBounds {
|
||||
unsafe {
|
||||
let window: id = self.native_window();
|
||||
let screen_frame = window.screen().visibleFrame();
|
||||
let window_frame = NSWindow::frame(window);
|
||||
let origin = vec2f(
|
||||
window_frame.origin.x as f32,
|
||||
(window_frame.origin.y - screen_frame.size.height - window_frame.size.height)
|
||||
as f32,
|
||||
);
|
||||
let size = vec2f(
|
||||
window_frame.size.width as f32,
|
||||
window_frame.size.height as f32,
|
||||
);
|
||||
WindowBounds::Fixed(RectF::new(origin, size))
|
||||
}
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Vector2F {
|
||||
unsafe {
|
||||
let NSSize { width, height, .. } =
|
||||
NSView::frame(self.native_item.button().superview().superview()).size;
|
||||
vec2f(width as f32, height as f32)
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
unsafe {
|
||||
let window: id = msg_send![self.native_item.button(), window];
|
||||
NSScreen::backingScaleFactor(window.screen()) as f32
|
||||
}
|
||||
}
|
||||
|
||||
pub fn native_window(&self) -> id {
|
||||
unsafe { msg_send![self.native_item.button(), window] }
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn dealloc_view(this: &Object, _: Sel) {
|
||||
unsafe {
|
||||
drop_state(this);
|
||||
|
||||
let _: () = msg_send![super(this, class!(NSView)), dealloc];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||||
unsafe {
|
||||
if let Some(state) = get_state(this).upgrade() {
|
||||
let mut state_borrow = state.as_ref().borrow_mut();
|
||||
if let Some(event) =
|
||||
Event::from_native(native_event, Some(state_borrow.content_size().y()))
|
||||
{
|
||||
if let Some(mut callback) = state_borrow.event_callback.take() {
|
||||
drop(state_borrow);
|
||||
callback(event);
|
||||
state.borrow_mut().event_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
|
||||
if let Some(state) = unsafe { get_state(this).upgrade() } {
|
||||
let state = state.borrow();
|
||||
state.renderer.layer().as_ptr() as id
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
|
||||
unsafe {
|
||||
if let Some(state) = get_state(this).upgrade() {
|
||||
let mut state = state.borrow_mut();
|
||||
if let Some(scene) = state.scene.take() {
|
||||
state.renderer.render(&scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
|
||||
unsafe {
|
||||
if let Some(state) = get_state(this).upgrade() {
|
||||
let mut state_borrow = state.as_ref().borrow_mut();
|
||||
if let Some(mut callback) = state_borrow.appearance_changed_callback.take() {
|
||||
drop(state_borrow);
|
||||
callback();
|
||||
state.borrow_mut().appearance_changed_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_state(object: &Object) -> Weak<RefCell<StatusItemState>> {
|
||||
let raw: *mut c_void = *object.get_ivar(STATE_IVAR);
|
||||
let weak1 = Weak::from_raw(raw as *mut RefCell<StatusItemState>);
|
||||
let weak2 = weak1.clone();
|
||||
let _ = Weak::into_raw(weak1);
|
||||
weak2
|
||||
}
|
||||
|
||||
unsafe fn drop_state(object: &Object) {
|
||||
let raw: *const c_void = *object.get_ivar(STATE_IVAR);
|
||||
Weak::from_raw(raw as *const RefCell<StatusItemState>);
|
||||
}
|
||||
+846
@@ -0,0 +1,846 @@
|
||||
use crate::{
|
||||
Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun,
|
||||
FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point,
|
||||
RenderGlyphParams, Result, SUBPIXEL_VARIANTS_X, ShapedGlyph, ShapedRun, SharedString, Size,
|
||||
point, px, size, swap_rgba_pa_to_bgra,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use cocoa::appkit::CGFloat;
|
||||
use collections::HashMap;
|
||||
use core_foundation::{
|
||||
attributed_string::CFMutableAttributedString,
|
||||
base::{CFRange, 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_descriptor::{
|
||||
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 parking_lot::{RwLock, RwLockUpgradableReadGuard};
|
||||
use pathfinder_geometry::{
|
||||
rect::{RectF, RectI},
|
||||
transform2d::Transform2F,
|
||||
vector::{Vector2F, Vector2I},
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{borrow::Cow, char, convert::TryFrom, sync::Arc};
|
||||
|
||||
use super::open_type::apply_features_and_fallbacks;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const kCGImageAlphaOnly: u32 = 7;
|
||||
|
||||
pub(crate) 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>,
|
||||
/// UTF-16 indices of ZWNJS
|
||||
zwnjs_scratch_space: Vec<usize>,
|
||||
}
|
||||
|
||||
impl MacTextSystem {
|
||||
pub(crate) 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(),
|
||||
zwnjs_scratch_space: Vec::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
let Some(descriptors) = collection.get_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: font.style.into(),
|
||||
weight: font.weight.into(),
|
||||
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 {
|
||||
self.0.read().fonts[font_id.0].metrics().into()
|
||||
}
|
||||
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
|
||||
Ok(self.0.read().fonts[font_id.0]
|
||||
.typographic_bounds(glyph_id.0)?
|
||||
.into())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 = crate::text_system::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(self.fonts[font_id.0].advance(glyph_id.0)?.into())
|
||||
}
|
||||
|
||||
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);
|
||||
Ok(font
|
||||
.raster_bounds(
|
||||
params.glyph_id.0,
|
||||
params.font_size.into(),
|
||||
scale,
|
||||
HintingOptions::None,
|
||||
font_kit::canvas::RasterizationOptions::GrayscaleAa,
|
||||
)?
|
||||
.into())
|
||||
}
|
||||
|
||||
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_gray_fill_color(0.0, 1.0);
|
||||
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);
|
||||
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 {
|
||||
const ZWNJ: char = '\u{200C}';
|
||||
const ZWNJ_STR: &str = "\u{200C}";
|
||||
const ZWNJ_SIZE_16: usize = ZWNJ.len_utf16();
|
||||
|
||||
self.zwnjs_scratch_space.clear();
|
||||
// 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 ix_converter = StringIndexConverter::new(&text);
|
||||
let mut last_font_run = None;
|
||||
for run in font_runs {
|
||||
let text = &text[ix_converter.utf8_ix..][..run.len];
|
||||
// if the fonts are the same, we need to disconnect the text with a ZWNJ
|
||||
// to prevent core text from forming ligatures between them
|
||||
let needs_zwnj = last_font_run.replace(run.font_id) == Some(run.font_id);
|
||||
|
||||
let utf16_start = string.char_len(); // insert at end of string
|
||||
ix_converter.advance_to_utf8_ix(ix_converter.utf8_ix + run.len);
|
||||
|
||||
// note: replace_str may silently ignore codepoints it dislikes (e.g., BOM at start of string)
|
||||
string.replace_str(&CFString::new(text), CFRange::init(utf16_start, 0));
|
||||
if needs_zwnj {
|
||||
let zwnjs_pos = string.char_len();
|
||||
self.zwnjs_scratch_space.push(zwnjs_pos as usize);
|
||||
string.replace_str(
|
||||
&CFString::from_static_string(ZWNJ_STR),
|
||||
CFRange::init(zwnjs_pos, 0),
|
||||
);
|
||||
}
|
||||
let utf16_end = string.char_len();
|
||||
|
||||
let cf_range = CFRange::init(utf16_start, utf16_end - utf16_start);
|
||||
let font = &self.fonts[run.font_id.0];
|
||||
|
||||
let font_metrics = font.metrics();
|
||||
let font_scale = font_size.0 / 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);
|
||||
|
||||
unsafe {
|
||||
string.set_attribute(
|
||||
cf_range,
|
||||
kCTFontAttributeName,
|
||||
&font.native_font().clone_with_font_size(font_size.into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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 mut 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 mut glyph_utf16_ix = usize::try_from(glyph_utf16_ix).unwrap();
|
||||
let r = self
|
||||
.zwnjs_scratch_space
|
||||
.binary_search_by(|&it| it.cmp(&glyph_utf16_ix));
|
||||
match r {
|
||||
// this glyph is a ZWNJ, skip it
|
||||
Ok(_) => continue,
|
||||
// adjust the index to account for the ZWNJs we've inserted
|
||||
Err(idx) => glyph_utf16_ix -= idx * ZWNJ_SIZE_16,
|
||||
}
|
||||
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_utf8_ix(&mut self, utf8_target: usize) {
|
||||
for (ix, c) in self.text[self.utf8_ix..].char_indices() {
|
||||
if self.utf8_ix + ix >= utf8_target {
|
||||
self.utf8_ix += ix;
|
||||
return;
|
||||
}
|
||||
self.utf16_ix += c.len_utf16();
|
||||
}
|
||||
self.utf8_ix = self.text.len();
|
||||
}
|
||||
|
||||
fn advance_to_utf16_ix(&mut self, utf16_target: usize) {
|
||||
for (ix, c) in self.text[self.utf8_ix..].char_indices() {
|
||||
if self.utf16_ix >= utf16_target {
|
||||
self.utf8_ix += ix;
|
||||
return;
|
||||
}
|
||||
self.utf16_ix += c.len_utf16();
|
||||
}
|
||||
self.utf8_ix = self.text.len();
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Metrics> for FontMetrics {
|
||||
fn from(metrics: Metrics) -> Self {
|
||||
FontMetrics {
|
||||
units_per_em: metrics.units_per_em,
|
||||
ascent: metrics.ascent,
|
||||
descent: metrics.descent,
|
||||
line_gap: metrics.line_gap,
|
||||
underline_position: metrics.underline_position,
|
||||
underline_thickness: metrics.underline_thickness,
|
||||
cap_height: metrics.cap_height,
|
||||
x_height: metrics.x_height,
|
||||
bounding_box: metrics.bounding_box.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RectF> for Bounds<f32> {
|
||||
fn from(rect: RectF) -> Self {
|
||||
Bounds {
|
||||
origin: point(rect.origin_x(), rect.origin_y()),
|
||||
size: size(rect.width(), rect.height()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RectI> for Bounds<DevicePixels> {
|
||||
fn from(rect: RectI) -> Self {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vector2F> for Size<f32> {
|
||||
fn from(vec: Vector2F) -> Self {
|
||||
size(vec.x(), vec.y())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FontWeight> for FontkitWeight {
|
||||
fn from(value: FontWeight) -> Self {
|
||||
FontkitWeight(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FontStyle> for FontkitStyle {
|
||||
fn from(style: FontStyle) -> Self {
|
||||
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::{FontRun, GlyphId, MacTextSystem, 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());
|
||||
}
|
||||
}
|
||||
+2647
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
use crate::WindowAppearance;
|
||||
use cocoa::{
|
||||
appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight},
|
||||
base::id,
|
||||
foundation::NSString,
|
||||
};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use std::ffi::CStr;
|
||||
|
||||
impl WindowAppearance {
|
||||
pub(crate) unsafe fn from_native(appearance: id) -> Self {
|
||||
let name: id = msg_send![appearance, name];
|
||||
unsafe {
|
||||
if name == NSAppearanceNameVibrantLight {
|
||||
Self::VibrantLight
|
||||
} else if name == NSAppearanceNameVibrantDark {
|
||||
Self::VibrantDark
|
||||
} else if name == NSAppearanceNameAqua {
|
||||
Self::Light
|
||||
} else if name == NSAppearanceNameDarkAqua {
|
||||
Self::Dark
|
||||
} else {
|
||||
println!(
|
||||
"unknown appearance: {:?}",
|
||||
CStr::from_ptr(name.UTF8String())
|
||||
);
|
||||
Self::Light
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "AppKit", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
pub static NSAppearanceNameAqua: id;
|
||||
pub static NSAppearanceNameDarkAqua: id;
|
||||
}
|
||||
Reference in New Issue
Block a user