first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,11 +1,8 @@
|
||||
use galaxyui_core::{
|
||||
integration::TestDriver,
|
||||
keymap::{CustomTag, Keystroke},
|
||||
r#async::LocalBoxFuture,
|
||||
AppContext, AssetProvider,
|
||||
};
|
||||
|
||||
use galaxyui_core::integration::TestDriver;
|
||||
use galaxyui_core::keymap::{CustomTag, Keystroke};
|
||||
pub use galaxyui_core::platform::app::*;
|
||||
use galaxyui_core::r#async::LocalBoxFuture;
|
||||
use galaxyui_core::{AppContext, AssetProvider};
|
||||
|
||||
use super::AsInnerMut;
|
||||
|
||||
@@ -83,7 +80,7 @@ impl AppBuilder {
|
||||
/// [`Keystroke`]-based binding using the provided `custom_tag_to_keystroke` function.
|
||||
///
|
||||
/// This can be useful in the cases where an application registers a binding with a
|
||||
/// [`crate::keymap::Trigger::Custom`] for use in a Mac menu, but still wants to to register the
|
||||
/// [`crate::keymap::Trigger::Custom`] for use in a Mac menu, but still wants to register the
|
||||
/// binding with its corresponding `Keystroke` on other platforms that don't support menus.
|
||||
pub fn convert_custom_triggers_to_keystroke_triggers(
|
||||
&mut self,
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
use futures::future::LocalBoxFuture;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use crate::platform::app::TerminationResult;
|
||||
use crate::platform::test::FontDB as TestFontDB;
|
||||
use crate::{
|
||||
integration::TestDriver,
|
||||
platform::{self},
|
||||
AppContext, AssetProvider,
|
||||
};
|
||||
use futures::future::LocalBoxFuture;
|
||||
|
||||
use super::delegate::{self, AppDelegate};
|
||||
use super::event_loop::{self, AppEvent};
|
||||
use super::windowing::WindowManager;
|
||||
use std::sync::mpsc;
|
||||
use crate::integration::TestDriver;
|
||||
use crate::platform::app::TerminationResult;
|
||||
use crate::platform::test::FontDB as TestFontDB;
|
||||
use crate::platform::{self};
|
||||
use crate::{AppContext, AssetProvider};
|
||||
|
||||
pub struct App {
|
||||
callbacks: platform::app::AppCallbacks,
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{
|
||||
clipboard::InMemoryClipboard,
|
||||
notification::{NotificationSendError, RequestPermissionsOutcome},
|
||||
platform::{self, Cursor},
|
||||
};
|
||||
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::thread;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use super::event_loop::AppEvent;
|
||||
use crate::clipboard::InMemoryClipboard;
|
||||
use crate::notification::{NotificationSendError, RequestPermissionsOutcome};
|
||||
use crate::platform::{self, Cursor};
|
||||
|
||||
/// Stores the ID of the application's main thread, which we can reference
|
||||
/// to determine if a given thread is the main thread or not.
|
||||
@@ -44,7 +40,7 @@ impl AppDelegate {
|
||||
|
||||
fn send_event(&self, event: AppEvent) {
|
||||
if self.event_sender.send(event).is_err() {
|
||||
log::warn!("Tried to send event, but event loop is no longer running");
|
||||
log::debug!("Tried to send event, but event loop is no longer running");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,7 +210,7 @@ impl platform::DispatchDelegate for DispatchDelegate {
|
||||
.send(AppEvent::RunTask(ManuallyDrop::new(task)))
|
||||
.is_err()
|
||||
{
|
||||
log::warn!("Tried to send event, but event loop is no longer running");
|
||||
log::debug!("Tried to send event, but event loop is no longer running");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
use crate::{
|
||||
platform::{
|
||||
self,
|
||||
app::{AppCallbackDispatcher, ApproveTerminateResult, TerminationResult},
|
||||
TerminationMode,
|
||||
},
|
||||
AppContext, WindowId,
|
||||
use crate::platform::app::{
|
||||
AppCallbackDispatcher, ApproveTerminateResult, TerminationRequestSource, TerminationResult,
|
||||
};
|
||||
use crate::platform::{self, TerminationMode};
|
||||
use crate::{AppContext, WindowId};
|
||||
|
||||
/// Application events handled on the headless platform's main thread.
|
||||
pub(super) enum AppEvent {
|
||||
@@ -51,7 +48,7 @@ pub(super) fn run(
|
||||
let should_terminate = match termination_mode {
|
||||
TerminationMode::Cancellable => {
|
||||
matches!(
|
||||
callbacks.should_terminate_app(),
|
||||
callbacks.should_terminate_app(TerminationRequestSource::User),
|
||||
ApproveTerminateResult::Terminate
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,5 @@ mod windowing;
|
||||
|
||||
pub use app::App;
|
||||
pub use delegate::AppDelegate;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use windowing::Window;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::mpsc};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
geometry::rect::RectF,
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
platform::{self, WindowOptions},
|
||||
windowing::WindowCallbacks,
|
||||
WindowId,
|
||||
};
|
||||
|
||||
use super::event_loop::AppEvent;
|
||||
use crate::geometry::rect::RectF;
|
||||
use crate::geometry::vector::{vec2f, Vector2F};
|
||||
use crate::platform::{self, WindowOptions};
|
||||
use crate::windowing::WindowCallbacks;
|
||||
use crate::WindowId;
|
||||
|
||||
pub struct WindowManager {
|
||||
windows: HashMap<WindowId, Rc<Window>>,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// Re-export a couple winit types and modules as the concrete implementations
|
||||
// for the linux platform.
|
||||
use super::app::AppBackend;
|
||||
use super::AsInnerMut;
|
||||
pub use crate::windowing::winit::app::App;
|
||||
|
||||
use crate::{
|
||||
windowing::{self, WindowingSystem},
|
||||
AppContext,
|
||||
};
|
||||
|
||||
use super::{app::AppBackend, AsInnerMut};
|
||||
use crate::windowing::{self, WindowingSystem};
|
||||
use crate::AppContext;
|
||||
|
||||
/// An extension trait defining additional configurability for
|
||||
/// applications when running on Linux.
|
||||
@@ -56,11 +53,7 @@ pub fn user_windowing_system() -> WindowingSystem {
|
||||
}
|
||||
|
||||
pub fn is_wsl() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static IS_WSL: OnceLock<bool> = OnceLock::new();
|
||||
IS_WSL
|
||||
.get_or_init(|| std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists())
|
||||
.to_owned()
|
||||
command::wsl::is_wsl()
|
||||
}
|
||||
|
||||
pub fn is_wayland_env_var_set() -> bool {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use objc2_app_kit::{NSAlert, NSApplication, NSModalResponse};
|
||||
|
||||
/// Configures and runs an `NSAlert` modally, returning its response.
|
||||
///
|
||||
/// Exported via `#[no_mangle]` so the AppKit `showModal:modalId:` dispatch path
|
||||
/// in `app.m` can call it by its C symbol; `app.m` invokes it inside a
|
||||
/// main-queue block, so this runs on the main thread and `runModal` stays
|
||||
/// synchronous.
|
||||
#[no_mangle]
|
||||
pub extern "C-unwind" fn configureAndRunModal(
|
||||
alert: &NSAlert,
|
||||
app: &NSApplication,
|
||||
) -> NSModalResponse {
|
||||
alert.setShowsSuppressionButton(true);
|
||||
|
||||
// It is generally frowned-upon to be overly assertive about putting our windows in
|
||||
// the user's face. However, it is reasonable to do this before showing our modal. If
|
||||
// we don't make ourselves the top active app, our modal might show up behind another
|
||||
// app's window.
|
||||
app.activateIgnoringOtherApps(true);
|
||||
|
||||
alert.runModal()
|
||||
}
|
||||
@@ -1,90 +1,43 @@
|
||||
use cocoa::appkit::NSApp;
|
||||
use cocoa::foundation::{NSUInteger, NSURL};
|
||||
use cocoa::{
|
||||
base::{id, nil},
|
||||
foundation::{NSArray, NSAutoreleasePool, NSData, NSString},
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::c_void;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cocoa::base::id;
|
||||
use futures_util::future::LocalBoxFuture;
|
||||
use objc::{
|
||||
class, msg_send,
|
||||
runtime::{Object, Sel, BOOL, NO, YES},
|
||||
sel, sel_impl,
|
||||
use objc::runtime::{Object, Sel, BOOL, NO, YES};
|
||||
use objc2::rc::{autoreleasepool, Retained};
|
||||
use objc2::{msg_send, AnyThread, MainThreadMarker};
|
||||
use objc2_app_kit::{NSAlert, NSApplication, NSImage, NSRunningApplication};
|
||||
use objc2_foundation::{NSArray, NSData, NSString, NSUInteger, NSURL};
|
||||
use galaxyui_core::assets::AssetProvider;
|
||||
use galaxyui_core::integration::TestDriver;
|
||||
use galaxyui_core::keymap::{Keystroke, Trigger};
|
||||
use galaxyui_core::modals::{AlertDialog, ModalId};
|
||||
use galaxyui_core::platform::app::{
|
||||
AppCallbackDispatcher, ApproveTerminateResult, TerminationRequestSource,
|
||||
};
|
||||
use galaxyui_core::platform::menu::{Menu, MenuBar};
|
||||
use galaxyui_core::platform::{self, FilePickerCallback, SaveFilePickerCallback};
|
||||
use galaxyui_core::{AppContext, Event};
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::CStr,
|
||||
os::raw::{c_char, c_void},
|
||||
path::PathBuf,
|
||||
};
|
||||
use super::keycode::{Keycode, CMD_KEY, CONTROL_KEY, OPTION_KEY, SHIFT_KEY};
|
||||
use super::menus::{make_dock_menu, make_main_menu};
|
||||
use super::window::{get_window_state, IntegrationTestWindowManager, Window, WindowManager};
|
||||
use crate::platform::app::{AppBackend, AppBuilder};
|
||||
use crate::platform::AsInnerMut;
|
||||
|
||||
use crate::platform::{
|
||||
app::{AppBackend, AppBuilder},
|
||||
AsInnerMut,
|
||||
};
|
||||
use galaxyui_core::{
|
||||
assets::AssetProvider,
|
||||
integration::TestDriver,
|
||||
keymap::{Keystroke, Trigger},
|
||||
modals::{AlertDialog, ModalId},
|
||||
platform::app::{AppCallbackDispatcher, ApproveTerminateResult},
|
||||
platform::menu::{Menu, MenuBar},
|
||||
platform::SaveFilePickerCallback,
|
||||
platform::{self, FilePickerCallback},
|
||||
AppContext, Event,
|
||||
};
|
||||
|
||||
use super::{
|
||||
keycode::{Keycode, CMD_KEY, CONTROL_KEY, OPTION_KEY, SHIFT_KEY},
|
||||
make_nsstring,
|
||||
menus::{make_dock_menu, make_main_menu},
|
||||
window::{get_window_state, IntegrationTestWindowManager, Window, WindowManager},
|
||||
};
|
||||
|
||||
pub trait NSAlert: Sized {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSAlert), alloc]
|
||||
}
|
||||
|
||||
unsafe fn init(self) -> id;
|
||||
unsafe fn autorelease(self) -> id;
|
||||
unsafe fn set_message_text(self, message_text: id);
|
||||
unsafe fn set_informative_text(self, informative_text: id);
|
||||
unsafe fn add_button_with_title(self, title: id);
|
||||
}
|
||||
|
||||
impl NSAlert for id {
|
||||
unsafe fn init(self) -> id {
|
||||
msg_send![self, init]
|
||||
}
|
||||
|
||||
unsafe fn autorelease(self) -> id {
|
||||
msg_send![self, autorelease]
|
||||
}
|
||||
|
||||
unsafe fn set_message_text(self, message_text: id) {
|
||||
msg_send![self, setMessageText: message_text]
|
||||
}
|
||||
|
||||
unsafe fn set_informative_text(self, informative_text: id) {
|
||||
msg_send![self, setInformativeText: informative_text]
|
||||
}
|
||||
|
||||
unsafe fn add_button_with_title(self, title: id) {
|
||||
msg_send![self, addButtonWithTitle: title]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_native_platform_modal(dialog: AlertDialog) -> id {
|
||||
unsafe {
|
||||
let alert = NSAlert::autorelease(NSAlert::init(NSAlert::alloc(nil)));
|
||||
alert.set_informative_text(make_nsstring(&dialog.info_text));
|
||||
alert.set_message_text(make_nsstring(&dialog.message_text));
|
||||
for title in dialog.buttons {
|
||||
alert.add_button_with_title(make_nsstring(&title));
|
||||
}
|
||||
alert
|
||||
/// Builds a native macOS alert dialog from an [`AlertDialog`].
|
||||
pub fn create_native_platform_modal(dialog: AlertDialog) -> Retained<NSAlert> {
|
||||
// SAFETY: native modals are constructed on the main thread.
|
||||
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
||||
let alert = NSAlert::new(mtm);
|
||||
alert.setInformativeText(&NSString::from_str(&dialog.info_text));
|
||||
alert.setMessageText(&NSString::from_str(&dialog.message_text));
|
||||
for title in dialog.buttons {
|
||||
alert.addButtonWithTitle(&NSString::from_str(&title));
|
||||
}
|
||||
alert
|
||||
}
|
||||
|
||||
const RUST_WRAPPER_IVAR_NAME: &str = "rustWrapper";
|
||||
@@ -110,6 +63,9 @@ pub trait AppExt {
|
||||
|
||||
/// Sets the macOS dock menu constructor function.
|
||||
fn set_dock_menu_builder(&mut self, value: impl FnOnce(&mut AppContext) -> Menu + 'static);
|
||||
|
||||
/// Sets whether the application should show its Dock icon on launch.
|
||||
fn set_show_dock_icon_on_launch(&mut self, value: bool);
|
||||
}
|
||||
|
||||
type MenuBarBuilderFn = Box<dyn FnOnce(&mut AppContext) -> MenuBar>;
|
||||
@@ -121,6 +77,7 @@ pub struct App {
|
||||
callbacks: AppCallbackDispatcher,
|
||||
activate_on_launch: bool,
|
||||
dev_icon: Option<Cow<'static, [u8]>>,
|
||||
show_dock_icon_on_launch: bool,
|
||||
menu_bar_builder: Option<MenuBarBuilderFn>,
|
||||
dock_menu_builder: Option<DockMenuBuilderFn>,
|
||||
init_fn: Option<platform::app::AppInitCallbackFn>,
|
||||
@@ -162,6 +119,7 @@ impl App {
|
||||
callbacks: AppCallbackDispatcher::new(callbacks, ui_app),
|
||||
activate_on_launch: true,
|
||||
dev_icon: None,
|
||||
show_dock_icon_on_launch: true,
|
||||
menu_bar_builder: None,
|
||||
dock_menu_builder: None,
|
||||
init_fn: None,
|
||||
@@ -174,44 +132,47 @@ impl App {
|
||||
) {
|
||||
self.init_fn = Some(Box::new(init_fn));
|
||||
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
|
||||
// The autorelease pool stays open for the whole app lifetime (`run` blocks
|
||||
// until termination).
|
||||
autoreleasepool(|_| {
|
||||
// Get (and create, if necessary) the underlying NSApplication.
|
||||
let app: id = get_warp_app();
|
||||
// SAFETY: `get_warp_app()` returns the warp NSApplication subclass instance.
|
||||
let app_ptr = unsafe { get_warp_app() };
|
||||
let app = unsafe { &*app_ptr.cast::<NSApplication>() };
|
||||
|
||||
let running_app: id = msg_send![class!(NSRunningApplication), currentApplication];
|
||||
let bundle_id: id = msg_send![running_app, bundleIdentifier];
|
||||
let dev_icon = if bundle_id.is_null() {
|
||||
self.dev_icon.as_ref().map(|dev_icon| {
|
||||
let data: id = msg_send![class!(NSData), alloc];
|
||||
let data: id = data.initWithBytes_length_(
|
||||
dev_icon.as_ptr() as *const c_void,
|
||||
dev_icon.len() as u64,
|
||||
);
|
||||
let image: id = msg_send![class!(NSImage), alloc];
|
||||
image.initWithData_(data)
|
||||
// When running without an application bundle (dev builds), install the
|
||||
// provided dev icon as the app icon. This is a dev-only path: if the icon
|
||||
// bytes fail to decode we skip the call below and leave the default icon.
|
||||
let running_app = NSRunningApplication::currentApplication();
|
||||
let dev_icon: Option<Retained<NSImage>> = if running_app.bundleIdentifier().is_none() {
|
||||
self.dev_icon.as_ref().and_then(|dev_icon| {
|
||||
let data = NSData::with_bytes(dev_icon);
|
||||
NSImage::initWithData(NSImage::alloc(), &data)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let app_delegate: id = msg_send![app, delegate];
|
||||
// SAFETY: the app and its delegate are exclusively owned here, so writing
|
||||
// the `rustWrapper` ivar and messaging them is sound.
|
||||
unsafe {
|
||||
let app_delegate = app.delegate().expect("the warp app always has a delegate");
|
||||
|
||||
let self_ptr = Box::into_raw(Box::new(self));
|
||||
(*app).set_ivar(RUST_WRAPPER_IVAR_NAME, self_ptr as *mut c_void);
|
||||
(*app_delegate).set_ivar(RUST_WRAPPER_IVAR_NAME, self_ptr as *mut c_void);
|
||||
let self_ptr = Box::into_raw(Box::new(self));
|
||||
(*app_ptr).set_ivar(RUST_WRAPPER_IVAR_NAME, self_ptr as *mut c_void);
|
||||
(*Retained::as_ptr(&app_delegate).cast::<Object>().cast_mut())
|
||||
.set_ivar(RUST_WRAPPER_IVAR_NAME, self_ptr as *mut c_void);
|
||||
|
||||
if let Some(dev_icon) = dev_icon {
|
||||
let _: () = msg_send![app, setApplicationIconImage: dev_icon];
|
||||
if let Some(dev_icon) = dev_icon {
|
||||
app.setApplicationIconImage(Some(&dev_icon));
|
||||
}
|
||||
|
||||
app.run();
|
||||
|
||||
// App is done running when we get here, so we can reinstantiate the Box and drop it.
|
||||
drop(Box::from_raw(self_ptr));
|
||||
}
|
||||
|
||||
let _: () = msg_send![app, run];
|
||||
let _: () = msg_send![pool, drain];
|
||||
|
||||
// App is done running when we get here, so we can reinstantiate the Box and drop it.
|
||||
drop(Box::from_raw(self_ptr));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +204,13 @@ impl AppExt for AppBuilder {
|
||||
AppBackend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_show_dock_icon_on_launch(&mut self, value: bool) {
|
||||
match self.as_inner_mut() {
|
||||
AppBackend::CurrentPlatform(app) => app.show_dock_icon_on_launch = value,
|
||||
AppBackend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_app(object: &mut Object) -> &mut App {
|
||||
@@ -291,31 +259,47 @@ pub unsafe extern "C-unwind" fn warp_app_will_finish_launching(this: &mut Object
|
||||
|
||||
let app = get_app(this);
|
||||
|
||||
// SAFETY: this delegate callback runs on the main thread.
|
||||
let mtm = MainThreadMarker::new_unchecked();
|
||||
let ns_app = NSApplication::sharedApplication(mtm);
|
||||
|
||||
if app.activate_on_launch {
|
||||
let _: () = msg_send![NSApp(), activateIgnoringOtherApps: YES];
|
||||
ns_app.activateIgnoringOtherApps(true);
|
||||
}
|
||||
|
||||
if let Some(init_fn) = app.init_fn.take() {
|
||||
app.callbacks.initialize_app(init_fn);
|
||||
}
|
||||
|
||||
let app_delegate: id = msg_send![NSApp(), delegate];
|
||||
let app_delegate = ns_app
|
||||
.delegate()
|
||||
.expect("the warp app always has a delegate");
|
||||
|
||||
if app.callbacks.has_internet_reachability_changed_callback() {
|
||||
let _: () = msg_send![app_delegate, setReachabilityListener];
|
||||
// `setReachabilityListener` is a custom warp app-delegate selector.
|
||||
let _: () = msg_send![&*app_delegate, setReachabilityListener];
|
||||
}
|
||||
|
||||
if let Some(menu_bar_builder) = app.menu_bar_builder.take() {
|
||||
let menu_bar = app.callbacks.with_mutable_app_context(menu_bar_builder);
|
||||
let nsmenu = make_main_menu(menu_bar);
|
||||
let () = msg_send![NSApp(), setMainMenu: nsmenu];
|
||||
ns_app.setMainMenu(Some(&nsmenu));
|
||||
}
|
||||
|
||||
if let Some(dock_menu_builder) = app.dock_menu_builder.take() {
|
||||
let dock_menu = app.callbacks.with_mutable_app_context(dock_menu_builder);
|
||||
let nsmenu = make_dock_menu(dock_menu);
|
||||
let _: () = msg_send![app_delegate, setDockMenu: nsmenu];
|
||||
// `setDockMenu:` is a custom warp app-delegate selector.
|
||||
let _: () = msg_send![&*app_delegate, setDockMenu: &*nsmenu];
|
||||
}
|
||||
|
||||
let show_dock_icon = if app.show_dock_icon_on_launch {
|
||||
YES
|
||||
} else {
|
||||
NO
|
||||
};
|
||||
// `setDockIconVisible:` is a custom warp app-delegate selector.
|
||||
let _: BOOL = msg_send![&*app_delegate, setDockIconVisible: show_dock_icon];
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -337,10 +321,18 @@ pub(crate) extern "C-unwind" fn warp_app_internet_reachability_changed(
|
||||
|
||||
/// Returns whether or not we can proceed with termination.
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_should_terminate_app(this: &mut Object) -> BOOL {
|
||||
pub(crate) extern "C-unwind" fn warp_app_should_terminate_app(
|
||||
this: &mut Object,
|
||||
system_initiated: BOOL,
|
||||
) -> BOOL {
|
||||
let app = unsafe { get_app(this) };
|
||||
|
||||
match app.callbacks.should_terminate_app() {
|
||||
let source = if system_initiated != NO {
|
||||
TerminationRequestSource::System
|
||||
} else {
|
||||
TerminationRequestSource::User
|
||||
};
|
||||
match app.callbacks.should_terminate_app(source) {
|
||||
ApproveTerminateResult::Terminate => YES,
|
||||
ApproveTerminateResult::Cancel => NO,
|
||||
}
|
||||
@@ -538,11 +530,13 @@ extern "C-unwind" fn cpu_will_sleep(this: &mut Object) {
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_open_files(this: &mut Object, paths: id) {
|
||||
// SAFETY: `paths` is an `NSArray<NSString>` of file paths.
|
||||
let paths = unsafe {
|
||||
let paths = &*paths.cast::<NSArray<NSString>>();
|
||||
(0..paths.count())
|
||||
.filter_map(|i| {
|
||||
let path = paths.objectAtIndex(i);
|
||||
match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
|
||||
match CStr::from_ptr(path.UTF8String()).to_str() {
|
||||
Ok(string) => Some(PathBuf::from(string)),
|
||||
Err(err) => {
|
||||
log::error!("error converting path to string: {err}");
|
||||
@@ -558,11 +552,13 @@ extern "C-unwind" fn warp_app_open_files(this: &mut Object, paths: id) {
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_open_urls(this: &mut Object, urls: id) {
|
||||
// SAFETY: `urls` is an `NSArray<NSURL>`.
|
||||
let urls = unsafe {
|
||||
let urls = &*urls.cast::<NSArray<NSURL>>();
|
||||
(0..urls.count())
|
||||
.filter_map(|i| {
|
||||
let url = urls.objectAtIndex(i).absoluteString();
|
||||
match CStr::from_ptr(url.UTF8String() as *mut c_char).to_str() {
|
||||
let url = urls.objectAtIndex(i).absoluteString()?;
|
||||
match CStr::from_ptr(url.UTF8String()).to_str() {
|
||||
Ok(string) => Some(string.to_string()),
|
||||
Err(err) => {
|
||||
log::error!("error converting url to string: {err}");
|
||||
@@ -590,16 +586,15 @@ pub(crate) extern "C-unwind" fn warp_open_panel_file_selected(urls: id, callback
|
||||
// avoid the memory leak that would occur if we left it in raw pointer form.
|
||||
let callback = unsafe { Box::from_raw(callback as *mut FilePickerCallback) };
|
||||
|
||||
// SAFETY: `urls` is an `NSArray<NSURL>` of selected files.
|
||||
let paths = unsafe {
|
||||
let urls = &*urls.cast::<NSArray<NSURL>>();
|
||||
(0..urls.count())
|
||||
.map(|i| {
|
||||
let file_url = urls.objectAtIndex(i);
|
||||
let file_path: id = msg_send![file_url, path];
|
||||
let slice = std::slice::from_raw_parts(
|
||||
file_path.UTF8String() as *const std::ffi::c_uchar,
|
||||
file_path.len(),
|
||||
);
|
||||
std::str::from_utf8_unchecked(slice).to_string()
|
||||
urls.objectAtIndex(i)
|
||||
.path()
|
||||
.map(|file_path| file_path.to_string())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
@@ -608,6 +603,7 @@ pub(crate) extern "C-unwind" fn warp_open_panel_file_selected(urls: id, callback
|
||||
log::info!("No file was selected. Dialog was cancelled.")
|
||||
}
|
||||
|
||||
// SAFETY: `get_warp_app()` returns the warp NSApplication subclass instance.
|
||||
let app = unsafe { get_app(&mut *get_warp_app()) };
|
||||
app.callbacks.with_mutable_app_context(move |ctx| {
|
||||
callback(Ok(paths), ctx);
|
||||
@@ -619,23 +615,19 @@ pub(crate) extern "C-unwind" fn warp_open_panel_file_selected(urls: id, callback
|
||||
pub(crate) extern "C-unwind" fn warp_save_panel_file_selected(url: id, callback: *mut c_void) {
|
||||
let callback = unsafe { Box::from_raw(callback as *mut SaveFilePickerCallback) };
|
||||
|
||||
let path = if url.is_null() {
|
||||
None
|
||||
} else {
|
||||
unsafe {
|
||||
let file_path: id = msg_send![url, path];
|
||||
let slice = std::slice::from_raw_parts(
|
||||
file_path.UTF8String() as *const std::ffi::c_uchar,
|
||||
file_path.len(),
|
||||
);
|
||||
Some(std::str::from_utf8_unchecked(slice).to_string())
|
||||
}
|
||||
// SAFETY: `url` is null or a valid `NSURL`.
|
||||
let path = unsafe {
|
||||
url.cast::<NSURL>()
|
||||
.as_ref()
|
||||
.and_then(|url| url.path())
|
||||
.map(|file_path| file_path.to_string())
|
||||
};
|
||||
|
||||
if path.is_none() {
|
||||
log::info!("Save dialog was cancelled.");
|
||||
}
|
||||
|
||||
// SAFETY: `get_warp_app()` returns the warp NSApplication subclass instance.
|
||||
let app = unsafe { get_app(&mut *get_warp_app()) };
|
||||
app.callbacks.with_mutable_app_context(move |ctx| {
|
||||
callback(path, ctx);
|
||||
|
||||
@@ -1,62 +1,54 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use cocoa::appkit::{NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypeString};
|
||||
use cocoa::foundation::{NSArray, NSData};
|
||||
use cocoa::{
|
||||
base::{id, nil},
|
||||
foundation::NSString,
|
||||
};
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::{c_uchar, c_void};
|
||||
use std::os::raw::c_uchar;
|
||||
use std::slice;
|
||||
|
||||
use super::make_nsstring;
|
||||
use anyhow::Result;
|
||||
use cocoa::base::id;
|
||||
use objc2::rc::Retained;
|
||||
use objc2_app_kit::{NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypeString};
|
||||
use objc2_foundation::{ns_string, NSArray, NSData, NSString};
|
||||
use galaxyui_core::clipboard::{ClipboardContent, ImageData};
|
||||
|
||||
extern "C" {
|
||||
fn getFilePathsFromPasteboard() -> id;
|
||||
}
|
||||
|
||||
pub struct Clipboard(id);
|
||||
pub struct Clipboard(Retained<NSPasteboard>);
|
||||
|
||||
unsafe impl Send for Clipboard {}
|
||||
|
||||
impl Clipboard {
|
||||
pub fn new() -> Result<Self> {
|
||||
let pboard = unsafe { NSPasteboard::generalPasteboard(nil) };
|
||||
if pboard.is_null() {
|
||||
Err(anyhow!("NSPasteboard::generalPasteboard returned nil"))
|
||||
} else {
|
||||
Ok(Clipboard(pboard))
|
||||
}
|
||||
// `generalPasteboard` is documented to always return the shared
|
||||
// pasteboard, so objc2 models it as a non-null `Retained`.
|
||||
Ok(Clipboard(NSPasteboard::generalPasteboard()))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn pasteboard_type_for_image_mime_type(mime_type: &str) -> Option<id> {
|
||||
let pasteboard_type = match mime_type {
|
||||
"image/png" => "public.png",
|
||||
"image/jpeg" => "public.jpeg",
|
||||
"image/gif" => "public.gif",
|
||||
"image/webp" => "public.webp",
|
||||
"image/svg+xml" => "public.svg-image",
|
||||
_ => return None,
|
||||
};
|
||||
Some(make_nsstring(pasteboard_type))
|
||||
fn pasteboard_type_for_image_mime_type(mime_type: &str) -> Option<&'static NSString> {
|
||||
match mime_type {
|
||||
"image/png" => Some(ns_string!("public.png")),
|
||||
"image/jpeg" => Some(ns_string!("public.jpeg")),
|
||||
"image/gif" => Some(ns_string!("public.gif")),
|
||||
"image/webp" => Some(ns_string!("public.webp")),
|
||||
"image/svg+xml" => Some(ns_string!("public.svg-image")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Clipboard for Clipboard {
|
||||
fn write(&mut self, contents: ClipboardContent) {
|
||||
unsafe {
|
||||
let nsstr = make_nsstring(&contents.plain_text);
|
||||
let nsstr = NSString::from_str(&contents.plain_text);
|
||||
self.0
|
||||
.declareTypes_owner(NSArray::arrayWithObject(nil, NSPasteboardTypeString), nil);
|
||||
NSPasteboard::setString_forType(self.0, nsstr, NSPasteboardTypeString);
|
||||
.declareTypes_owner(&NSArray::from_slice(&[NSPasteboardTypeString]), None);
|
||||
self.0.setString_forType(&nsstr, NSPasteboardTypeString);
|
||||
|
||||
if let Some(html) = contents.html {
|
||||
let nsstr = make_nsstring(&html);
|
||||
let nsstr = NSString::from_str(&html);
|
||||
self.0
|
||||
.addTypes_owner(NSArray::arrayWithObject(nil, NSPasteboardTypeHTML), nil);
|
||||
NSPasteboard::setString_forType(self.0, nsstr, NSPasteboardTypeHTML);
|
||||
.addTypes_owner(&NSArray::from_slice(&[NSPasteboardTypeHTML]), None);
|
||||
self.0.setString_forType(&nsstr, NSPasteboardTypeHTML);
|
||||
}
|
||||
|
||||
if let Some(images) = contents.images {
|
||||
@@ -66,17 +58,13 @@ impl crate::Clipboard for Clipboard {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let data: id = msg_send![class!(NSData), alloc];
|
||||
let data: id = data.initWithBytes_length_(
|
||||
image.data.as_ptr() as *const c_void,
|
||||
image.data.len() as u64,
|
||||
);
|
||||
// `NSData::with_bytes` copies the image bytes into a +1-retained
|
||||
// NSData. The pasteboard retains it in `setData:forType:`, and the
|
||||
// `Retained` releases our reference when it drops at the loop end.
|
||||
let data = NSData::with_bytes(&image.data);
|
||||
self.0
|
||||
.addTypes_owner(NSArray::arrayWithObject(nil, pasteboard_type), nil);
|
||||
let _: () = msg_send![self.0, setData: data forType: pasteboard_type];
|
||||
// Balance the +1 retain from `[NSData alloc]`. The pasteboard retains
|
||||
// `data` in `setData:forType:`, so the object stays alive as needed.
|
||||
let _: () = msg_send![data, release];
|
||||
.addTypes_owner(&NSArray::from_slice(&[pasteboard_type]), None);
|
||||
self.0.setData_forType(Some(&data), pasteboard_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,11 +74,11 @@ impl crate::Clipboard for Clipboard {
|
||||
unsafe {
|
||||
// Try getting file paths from the clipboard. If we end up with an empty
|
||||
// array of file paths, fallback to getting the string from the pasteboard.
|
||||
let file_paths = getFilePathsFromPasteboard();
|
||||
let file_paths = &*getFilePathsFromPasteboard().cast::<NSArray<NSString>>();
|
||||
let available_paths = file_paths.count();
|
||||
|
||||
let text = NSPasteboard::stringForType(self.0, NSPasteboardTypeString);
|
||||
let mut content = ClipboardContent::plain_text(if text != nil {
|
||||
let text = self.0.stringForType(NSPasteboardTypeString);
|
||||
let mut content = ClipboardContent::plain_text(if let Some(text) = text.as_deref() {
|
||||
CStr::from_ptr(text.UTF8String())
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
@@ -114,8 +102,8 @@ impl crate::Clipboard for Clipboard {
|
||||
);
|
||||
}
|
||||
|
||||
let html = NSPasteboard::stringForType(self.0, NSPasteboardTypeHTML);
|
||||
if html != nil {
|
||||
let html = self.0.stringForType(NSPasteboardTypeHTML);
|
||||
if let Some(html) = html.as_deref() {
|
||||
content.html = Some(
|
||||
CStr::from_ptr(html.UTF8String())
|
||||
.to_str()
|
||||
@@ -143,24 +131,20 @@ impl Clipboard {
|
||||
// macOS pasteboard type identifiers for supported image formats
|
||||
// Ordered by preference for web compatibility
|
||||
let supported_pasteboard_types = [
|
||||
make_nsstring("public.png"),
|
||||
make_nsstring("public.jpeg"),
|
||||
make_nsstring("public.gif"),
|
||||
make_nsstring("public.webp"),
|
||||
make_nsstring("public.svg-image"),
|
||||
make_nsstring("com.compuserve.gif"),
|
||||
ns_string!("public.png"),
|
||||
ns_string!("public.jpeg"),
|
||||
ns_string!("public.gif"),
|
||||
ns_string!("public.webp"),
|
||||
ns_string!("public.svg-image"),
|
||||
ns_string!("com.compuserve.gif"),
|
||||
];
|
||||
|
||||
let mut images = Vec::new();
|
||||
|
||||
for &pasteboard_type in &supported_pasteboard_types {
|
||||
let data = NSPasteboard::dataForType(self.0, pasteboard_type);
|
||||
if data != nil {
|
||||
let length = NSData::length(data);
|
||||
for pasteboard_type in supported_pasteboard_types {
|
||||
if let Some(data) = self.0.dataForType(pasteboard_type) {
|
||||
let length = data.len();
|
||||
if length > 0 {
|
||||
let bytes_ptr = NSData::bytes(data) as *const u8;
|
||||
let bytes = slice::from_raw_parts(bytes_ptr, length as usize);
|
||||
|
||||
let mime_type = match CStr::from_ptr(pasteboard_type.UTF8String())
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
@@ -175,8 +159,8 @@ impl Clipboard {
|
||||
|
||||
// Try to extract filename from HTML content if available
|
||||
let filename = {
|
||||
let html = NSPasteboard::stringForType(self.0, NSPasteboardTypeHTML);
|
||||
if html != nil {
|
||||
let html = self.0.stringForType(NSPasteboardTypeHTML);
|
||||
if let Some(html) = html.as_deref() {
|
||||
let html_str =
|
||||
CStr::from_ptr(html.UTF8String()).to_str().unwrap_or("");
|
||||
if !html_str.is_empty() {
|
||||
@@ -190,7 +174,7 @@ impl Clipboard {
|
||||
};
|
||||
|
||||
images.push(ImageData {
|
||||
data: bytes.to_vec(),
|
||||
data: data.to_vec(),
|
||||
mime_type: mime_type.to_string(),
|
||||
filename,
|
||||
});
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
use super::app::create_native_platform_modal;
|
||||
use super::keycode::{modifier_code, Keycode};
|
||||
use super::utils::nsstring_as_str;
|
||||
use super::{app, make_nsstring, Clipboard, Window};
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use cocoa::base::{BOOL, NO, YES};
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use cocoa::{
|
||||
appkit::{NSApp, NSRequestUserAttentionType},
|
||||
base::{id, nil},
|
||||
};
|
||||
use cocoa::base::{id, nil, BOOL, NO, YES};
|
||||
use objc2::{msg_send, MainThreadMarker};
|
||||
use objc2_app_kit::{NSApplication, NSCursor, NSRequestUserAttentionType};
|
||||
use objc2_av_foundation::{AVAuthorizationStatus, AVCaptureDevice, AVMediaTypeAudio};
|
||||
use objc2_foundation::NSUInteger;
|
||||
use galaxyui_core::accessibility::AccessibilityContent;
|
||||
use galaxyui_core::clipboard::InMemoryClipboard;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::modals::{AlertDialog, ModalId};
|
||||
use galaxyui_core::notification::{NotificationSendError, RequestPermissionsOutcome};
|
||||
use galaxyui_core::notification::{
|
||||
NotificationSendError, RequestPermissionsOutcome, UserNotification,
|
||||
};
|
||||
use galaxyui_core::platform::{
|
||||
Cursor, FilePickerCallback, FilePickerConfiguration, MicrophoneAccessState,
|
||||
SendNotificationErrorCallback, TerminationMode,
|
||||
};
|
||||
use galaxyui_core::ApplicationBundleInfo;
|
||||
use galaxyui_core::{
|
||||
accessibility::AccessibilityContent, notification::UserNotification, platform, WindowId,
|
||||
};
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use galaxyui_core::{platform, ApplicationBundleInfo, WindowId};
|
||||
|
||||
use super::app::create_native_platform_modal;
|
||||
use super::keycode::{modifier_code, Keycode};
|
||||
use super::utils::nsstring_as_str;
|
||||
use super::{app, make_nsstring, Clipboard, Window};
|
||||
|
||||
// Functions implemented in objC files.
|
||||
extern "C" {
|
||||
@@ -198,21 +198,19 @@ impl platform::Delegate for AppDelegate {
|
||||
/// Sets the cursor shape
|
||||
/// See https://developer.apple.com/documentation/appkit/nscursor?language=objc
|
||||
fn set_cursor_shape(&self, cursor: Cursor) {
|
||||
unsafe {
|
||||
let cursor: id = match cursor {
|
||||
Cursor::Arrow => msg_send![class!(NSCursor), arrowCursor],
|
||||
Cursor::IBeam => msg_send![class!(NSCursor), IBeamCursor],
|
||||
Cursor::Crosshair => msg_send![class!(NSCursor), crosshairCursor],
|
||||
Cursor::OpenHand => msg_send![class!(NSCursor), openHandCursor],
|
||||
Cursor::NotAllowed => msg_send![class!(NSCursor), operationNotAllowedCursor],
|
||||
Cursor::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
|
||||
Cursor::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||
Cursor::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||
Cursor::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
|
||||
Cursor::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
|
||||
};
|
||||
let () = msg_send![cursor, set];
|
||||
}
|
||||
let cursor = match cursor {
|
||||
Cursor::Arrow => NSCursor::arrowCursor(),
|
||||
Cursor::IBeam => NSCursor::IBeamCursor(),
|
||||
Cursor::Crosshair => NSCursor::crosshairCursor(),
|
||||
Cursor::OpenHand => NSCursor::openHandCursor(),
|
||||
Cursor::NotAllowed => NSCursor::operationNotAllowedCursor(),
|
||||
Cursor::PointingHand => NSCursor::pointingHandCursor(),
|
||||
Cursor::ResizeLeftRight => NSCursor::resizeLeftRightCursor(),
|
||||
Cursor::ResizeUpDown => NSCursor::resizeUpDownCursor(),
|
||||
Cursor::ClosedHand => NSCursor::closedHandCursor(),
|
||||
Cursor::DragCopy => NSCursor::dragCopyCursor(),
|
||||
};
|
||||
cursor.set();
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-util")]
|
||||
@@ -282,12 +280,14 @@ impl platform::Delegate for AppDelegate {
|
||||
fn open_character_palette(&self) {
|
||||
// Open the character palette in a async task on the main thread to
|
||||
// ensure we don't double-borrow the app.
|
||||
dispatch::Queue::main().exec_async(move || unsafe {
|
||||
dispatch::Queue::main().exec_async(move || {
|
||||
// See https://developer.apple.com/documentation/appkit/nsapplication/1428455-orderfrontcharacterpalette.
|
||||
// If the `sender` argument is nil, the palette is shown relative to the
|
||||
// first responder's cursor location. In our case, that will be the Warp
|
||||
// host view, with a location set via the `active_cursor_position` API.
|
||||
let () = msg_send![NSApp(), orderFrontCharacterPalette: nil];
|
||||
// SAFETY: the closure runs on the main dispatch queue.
|
||||
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
||||
NSApplication::sharedApplication(mtm).orderFrontCharacterPalette(None);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -304,12 +304,10 @@ impl platform::Delegate for AppDelegate {
|
||||
}
|
||||
|
||||
fn request_user_attention(&self, _window_id: WindowId) {
|
||||
unsafe {
|
||||
let () = msg_send![
|
||||
NSApp(),
|
||||
requestUserAttention: NSRequestUserAttentionType::NSInformationalRequest
|
||||
];
|
||||
}
|
||||
// SAFETY: delegate methods run on the main thread.
|
||||
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
||||
let _ = NSApplication::sharedApplication(mtm)
|
||||
.requestUserAttention(NSRequestUserAttentionType::InformationalRequest);
|
||||
}
|
||||
|
||||
fn request_desktop_notification_permissions(
|
||||
@@ -373,8 +371,14 @@ impl platform::Delegate for AppDelegate {
|
||||
|
||||
fn show_native_platform_modal(&self, id: ModalId, modal: AlertDialog) {
|
||||
let alert = create_native_platform_modal(modal);
|
||||
// `ModalId` is `#[repr(transparent)]` over `usize`, matching the
|
||||
// `showModal:modalId:` selector's `NSUInteger` parameter.
|
||||
// SAFETY: `ModalId` and `NSUInteger` share the same representation.
|
||||
let modal_id: NSUInteger = unsafe { std::mem::transmute(id) };
|
||||
// SAFETY: `showModal:modalId:` is a custom warp NSApplication selector.
|
||||
unsafe {
|
||||
let _: () = msg_send![app::get_warp_app(), showModal: alert modalId: id];
|
||||
let app = &*app::get_warp_app().cast::<NSApplication>();
|
||||
let _: () = msg_send![app, showModal: &*alert, modalId: modal_id];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,20 +398,38 @@ impl platform::Delegate for AppDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_dock_icon_visible(&self, visible: bool) {
|
||||
dispatch::Queue::main().exec_async(move || {
|
||||
// SAFETY: the closure runs on the main dispatch queue.
|
||||
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
let app_delegate = app.delegate().expect("the warp app always has a delegate");
|
||||
let value: BOOL = if visible { YES } else { NO };
|
||||
// `setDockIconVisible:` is a custom warp app-delegate selector.
|
||||
// SAFETY: messaging the app delegate on the main thread.
|
||||
let _: BOOL = unsafe { msg_send![&*app_delegate, setDockIconVisible: value] };
|
||||
});
|
||||
}
|
||||
|
||||
fn terminate_app(&self, termination_mode: TerminationMode) {
|
||||
// Execute `[NSApp terminate]` asynchronously on the main thread to
|
||||
// ensure we don't accidentally run into any double-borrow errors.
|
||||
dispatch::Queue::main().exec_async(move || unsafe {
|
||||
dispatch::Queue::main().exec_async(move || {
|
||||
// SAFETY: the closure runs on the main dispatch queue.
|
||||
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
match termination_mode {
|
||||
// ContentTransferred windows have already moved their content to another
|
||||
// window (e.g. during tab drag), so they can close immediately without
|
||||
// prompting the user for confirmation.
|
||||
TerminationMode::ForceTerminate | TerminationMode::ContentTransferred => {
|
||||
let _: () = msg_send![NSApp(), setForceTermination];
|
||||
// `setForceTermination` is a custom warp NSApplication selector.
|
||||
// SAFETY: messaging the shared application.
|
||||
let _: () = unsafe { msg_send![&*app, setForceTermination] };
|
||||
}
|
||||
TerminationMode::Cancellable => {}
|
||||
}
|
||||
let _: () = msg_send![NSApp(), terminate: nil];
|
||||
app.terminate(None);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -416,24 +438,20 @@ impl platform::Delegate for AppDelegate {
|
||||
}
|
||||
|
||||
fn microphone_access_state(&self) -> MicrophoneAccessState {
|
||||
unsafe {
|
||||
let cls = class!(AVCaptureDevice);
|
||||
// "soun" is not a typo, it's the correct constant name.
|
||||
let media_type_audio = make_nsstring("soun");
|
||||
// SAFETY: this is a global static variable, but simply reading it should be safe.
|
||||
let media_type = unsafe { AVMediaTypeAudio };
|
||||
let Some(media_type) = media_type else {
|
||||
return MicrophoneAccessState::NotDetermined;
|
||||
};
|
||||
|
||||
// AVAuthorizationStatus constants:
|
||||
// 0 = AVAuthorizationStatusNotDetermined - User has not yet made a choice
|
||||
// 1 = AVAuthorizationStatusRestricted - Restricted by system settings/parental controls
|
||||
// 2 = AVAuthorizationStatusDenied - User explicitly denied access
|
||||
// 3 = AVAuthorizationStatusAuthorized - User granted access
|
||||
let status: i32 = msg_send![cls, authorizationStatusForMediaType: media_type_audio];
|
||||
match status {
|
||||
0 => MicrophoneAccessState::NotDetermined,
|
||||
1 => MicrophoneAccessState::Restricted,
|
||||
2 => MicrophoneAccessState::Denied,
|
||||
3 => MicrophoneAccessState::Authorized,
|
||||
_ => MicrophoneAccessState::NotDetermined, // fallback
|
||||
}
|
||||
// SAFETY: this can raise an exception if you pass an invalid media type, but we're only
|
||||
// ever passing AVMediaTypeAudio here.
|
||||
let status = unsafe { AVCaptureDevice::authorizationStatusForMediaType(media_type) };
|
||||
match status {
|
||||
AVAuthorizationStatus::Restricted => MicrophoneAccessState::Restricted,
|
||||
AVAuthorizationStatus::Denied => MicrophoneAccessState::Denied,
|
||||
AVAuthorizationStatus::Authorized => MicrophoneAccessState::Authorized,
|
||||
_ => MicrophoneAccessState::NotDetermined, // fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -473,8 +491,7 @@ pub unsafe extern "C-unwind" fn warp_on_notification_send_error(
|
||||
|
||||
impl platform::DispatchDelegate for DispatchDelegate {
|
||||
fn is_main_thread(&self) -> bool {
|
||||
let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] };
|
||||
is_main_thread == YES
|
||||
MainThreadMarker::new().is_some()
|
||||
}
|
||||
|
||||
fn run_on_main_thread(&self, task: async_task::Runnable) {
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use std::{ffi::CStr, os::raw::c_char};
|
||||
use std::ffi::CStr;
|
||||
|
||||
use galaxyui_core::event::{KeyEventDetails, ModifiersState};
|
||||
use galaxyui_core::platform::keyboard::{KeyCode, PhysicalKey};
|
||||
use galaxyui_core::{keymap::Keystroke, Event};
|
||||
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventType},
|
||||
base::{id, YES},
|
||||
foundation::NSString,
|
||||
};
|
||||
use cocoa::base::id;
|
||||
use objc2_app_kit::{NSEvent, NSEventModifierFlags, NSEventType};
|
||||
use objc2_foundation::NSUInteger;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use galaxyui_core::event::{KeyEventDetails, ModifiersState};
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::keyboard::{KeyCode, PhysicalKey};
|
||||
use galaxyui_core::Event;
|
||||
|
||||
use super::{
|
||||
keycode::{scancode_to_physicalkey, Keycode},
|
||||
utils::unicode_char_to_key,
|
||||
};
|
||||
use super::keycode::{scancode_to_physicalkey, Keycode};
|
||||
use super::utils::unicode_char_to_key;
|
||||
|
||||
// Unpublished but widely known and stable flags for distinguishing left/right alt.
|
||||
// Google "NX_DEVICELALTKEYMASK" for more.
|
||||
@@ -24,11 +19,11 @@ const RIGHT_ALT_MASK: NSUInteger = 0x00000040;
|
||||
|
||||
fn modifier_flags_to_state(flags: NSEventModifierFlags) -> ModifiersState {
|
||||
ModifiersState {
|
||||
alt: flags.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
||||
cmd: flags.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
shift: flags.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||
ctrl: flags.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||
func: flags.contains(NSEventModifierFlags::NSFunctionKeyMask),
|
||||
alt: flags.contains(NSEventModifierFlags::Option),
|
||||
cmd: flags.contains(NSEventModifierFlags::Command),
|
||||
shift: flags.contains(NSEventModifierFlags::Shift),
|
||||
ctrl: flags.contains(NSEventModifierFlags::Control),
|
||||
func: flags.contains(NSEventModifierFlags::Function),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,11 +44,12 @@ pub unsafe fn from_native(
|
||||
window_height: Option<f32>,
|
||||
is_first_mouse: bool,
|
||||
) -> Option<Event> {
|
||||
let event_type = native_event.eventType();
|
||||
let native_event = &*native_event.cast::<NSEvent>();
|
||||
let event_type = native_event.r#type();
|
||||
|
||||
// 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 {
|
||||
match event_type.0 as u64 {
|
||||
0 | 21 | 32 | 33 | 35 | 36 | 37 => {
|
||||
return None;
|
||||
}
|
||||
@@ -62,7 +58,7 @@ pub unsafe fn from_native(
|
||||
let modifiers = modifier_flags_to_state(native_event.modifierFlags());
|
||||
|
||||
match event_type {
|
||||
NSEventType::NSKeyDown => {
|
||||
NSEventType::KeyDown => {
|
||||
let native_modifiers = native_event.modifierFlags();
|
||||
|
||||
// Get the base character for this key without any modifiers (including Shift)
|
||||
@@ -76,8 +72,8 @@ pub unsafe fn from_native(
|
||||
right_alt: (native_modifiers.bits() & RIGHT_ALT_MASK) != 0,
|
||||
key_without_modifiers,
|
||||
};
|
||||
let unmodified_chars = native_event.charactersIgnoringModifiers();
|
||||
let unmodified_chars = CStr::from_ptr(unmodified_chars.UTF8String() as *mut c_char)
|
||||
let unmodified_chars = native_event.charactersIgnoringModifiers()?;
|
||||
let unmodified_chars = CStr::from_ptr(unmodified_chars.UTF8String())
|
||||
.to_str()
|
||||
.ok()?;
|
||||
|
||||
@@ -88,24 +84,30 @@ pub unsafe fn from_native(
|
||||
};
|
||||
|
||||
let keystroke = Keystroke {
|
||||
ctrl: native_modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||
alt: native_modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
||||
shift: native_modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||
cmd: native_modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
ctrl: native_modifiers.contains(NSEventModifierFlags::Control),
|
||||
alt: native_modifiers.contains(NSEventModifierFlags::Option),
|
||||
shift: native_modifiers.contains(NSEventModifierFlags::Shift),
|
||||
cmd: native_modifiers.contains(NSEventModifierFlags::Command),
|
||||
meta: false, /* handled separately */
|
||||
key: unmodified_chars.into(),
|
||||
};
|
||||
|
||||
let chars = native_event.characters().UTF8String() as *mut c_char;
|
||||
let chars = if chars.is_null() {
|
||||
// `UTF8String` can return null in some rare cases where the
|
||||
// string isn't valid UTF-8. For example, if the user
|
||||
// enters a UTF-8 surrogate character, e.g. U+DDDD, via the
|
||||
// Unicode Hex Input keyboard, the conversion will produce
|
||||
// null.
|
||||
String::new()
|
||||
} else {
|
||||
CStr::from_ptr(chars).to_str().ok()?.to_owned()
|
||||
let characters = native_event.characters();
|
||||
let chars = match characters.as_deref() {
|
||||
None => String::new(),
|
||||
Some(characters) => {
|
||||
let chars = characters.UTF8String();
|
||||
if chars.is_null() {
|
||||
// `UTF8String` can return null in some rare cases where the
|
||||
// string isn't valid UTF-8. For example, if the user
|
||||
// enters a UTF-8 surrogate character, e.g. U+DDDD, via the
|
||||
// Unicode Hex Input keyboard, the conversion will produce
|
||||
// null.
|
||||
String::new()
|
||||
} else {
|
||||
CStr::from_ptr(chars).to_str().ok()?.to_owned()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(Event::KeyDown {
|
||||
@@ -115,20 +117,20 @@ pub unsafe fn from_native(
|
||||
is_composing: false,
|
||||
})
|
||||
}
|
||||
NSEventType::NSMouseMoved => window_height.map(|window_height| Event::MouseMoved {
|
||||
NSEventType::MouseMoved => window_height.map(|window_height| Event::MouseMoved {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
cmd: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
.contains(NSEventModifierFlags::Command),
|
||||
shift: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||
.contains(NSEventModifierFlags::Shift),
|
||||
is_synthetic: false,
|
||||
}),
|
||||
NSEventType::NSFlagsChanged => {
|
||||
NSEventType::FlagsChanged => {
|
||||
let key_code = native_key_code_to_key_code(native_event.keyCode());
|
||||
|
||||
window_height.map(|window_height| Event::ModifierStateChanged {
|
||||
@@ -140,7 +142,7 @@ pub unsafe fn from_native(
|
||||
key_code,
|
||||
})
|
||||
}
|
||||
NSEventType::NSLeftMouseDown => window_height.map(|window_height| {
|
||||
NSEventType::LeftMouseDown => window_height.map(|window_height| {
|
||||
let position = vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
@@ -165,14 +167,14 @@ pub unsafe fn from_native(
|
||||
}
|
||||
}
|
||||
}),
|
||||
NSEventType::NSLeftMouseUp => window_height.map(|window_height| Event::LeftMouseUp {
|
||||
NSEventType::LeftMouseUp => window_height.map(|window_height| Event::LeftMouseUp {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
modifiers,
|
||||
}),
|
||||
NSEventType::NSLeftMouseDragged => {
|
||||
NSEventType::LeftMouseDragged => {
|
||||
window_height.map(|window_height| Event::LeftMouseDragged {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
@@ -184,7 +186,7 @@ pub unsafe fn from_native(
|
||||
// TODO: This option is deprecated by Apple in favour of NSEventTypeOtherMouseDown
|
||||
// but we'll likely need to update cocoa.
|
||||
// See https://developer.apple.com/documentation/appkit/nsothermousedown.
|
||||
NSEventType::NSOtherMouseDown => {
|
||||
NSEventType::OtherMouseDown => {
|
||||
let window_height = window_height?;
|
||||
let window_location = native_event.locationInWindow();
|
||||
let position = vec2f(
|
||||
@@ -192,8 +194,8 @@ pub unsafe fn from_native(
|
||||
window_height - (window_location.y as f32),
|
||||
);
|
||||
let modifier_flags = native_event.modifierFlags();
|
||||
let cmd = modifier_flags.contains(NSEventModifierFlags::NSCommandKeyMask);
|
||||
let shift = modifier_flags.contains(NSEventModifierFlags::NSShiftKeyMask);
|
||||
let cmd = modifier_flags.contains(NSEventModifierFlags::Command);
|
||||
let shift = modifier_flags.contains(NSEventModifierFlags::Shift);
|
||||
let click_count = native_event.clickCount() as u32;
|
||||
|
||||
match native_event.buttonNumber() {
|
||||
@@ -219,20 +221,20 @@ pub unsafe fn from_native(
|
||||
}
|
||||
}
|
||||
// For trackpads, this event will get triggered by the user's secondary click setting.
|
||||
NSEventType::NSRightMouseDown => window_height.map(|window_height| Event::RightMouseDown {
|
||||
NSEventType::RightMouseDown => window_height.map(|window_height| Event::RightMouseDown {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
cmd: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
.contains(NSEventModifierFlags::Command),
|
||||
shift: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||
.contains(NSEventModifierFlags::Shift),
|
||||
click_count: native_event.clickCount() as u32,
|
||||
}),
|
||||
NSEventType::NSScrollWheel => window_height.map(|window_height| Event::ScrollWheel {
|
||||
NSEventType::ScrollWheel => window_height.map(|window_height| Event::ScrollWheel {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
@@ -241,7 +243,7 @@ pub unsafe fn from_native(
|
||||
native_event.scrollingDeltaX() as f32,
|
||||
native_event.scrollingDeltaY() as f32,
|
||||
),
|
||||
precise: native_event.hasPreciseScrollingDeltas() == YES,
|
||||
precise: native_event.hasPreciseScrollingDeltas(),
|
||||
modifiers,
|
||||
}),
|
||||
_ => None,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use super::text_layout::{layout_line, layout_text};
|
||||
use crate::fonts::font_kit::{properties_to_font_kit, Rasterizer};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use core_foundation::array::{CFArray, CFArrayRef};
|
||||
use core_foundation::base::{CFType, ItemRef, TCFType};
|
||||
@@ -14,7 +18,8 @@ use core_text::font_descriptor::{
|
||||
SymbolicTraitAccessors, TraitAccessors,
|
||||
};
|
||||
use core_text::{font, font_collection, font_descriptor};
|
||||
use dashmap::{mapref::entry::Entry, DashMap};
|
||||
use dashmap::mapref::entry::Entry;
|
||||
use dashmap::DashMap;
|
||||
use font_kit::font::Font;
|
||||
use font_kit::loaders::core_text::NativeFont;
|
||||
use futures::future::BoxFuture;
|
||||
@@ -30,13 +35,9 @@ use itertools::Itertools as _;
|
||||
use ordered_float::OrderedFloat;
|
||||
use pathfinder_geometry::rect::RectI;
|
||||
use pathfinder_geometry::vector::{Vector2F, Vector2I};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use super::text_layout::{layout_line, layout_text};
|
||||
use crate::fonts::font_kit::{properties_to_font_kit, Rasterizer};
|
||||
|
||||
struct FontFamily {
|
||||
name: String,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use cocoa::foundation::{NSPoint, NSRect, NSSize};
|
||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
||||
use objc2_foundation::{NSPoint, NSRect, NSSize};
|
||||
use pathfinder_geometry::rect::RectF;
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
pub trait Vector2FExt {
|
||||
fn to_ns_point(&self) -> NSPoint;
|
||||
fn to_ns_size(&self) -> NSSize;
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use std::slice;
|
||||
|
||||
use cocoa::{
|
||||
base::{id, nil, BOOL},
|
||||
foundation::{NSArray, NSString, NSUInteger},
|
||||
};
|
||||
use cocoa::base::{id, BOOL};
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use objc2::rc::Retained;
|
||||
use objc2_foundation::{NSArray, NSNumber, NSString};
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::keyboard::{KeyCode, NativeKeyCode, PhysicalKey};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
|
||||
use super::make_nsstring;
|
||||
|
||||
// Modifier key mask values for the Carbon API.
|
||||
pub const CMD_KEY: u16 = 256;
|
||||
@@ -32,10 +29,11 @@ impl Keycode {
|
||||
#[allow(clippy::useless_conversion)]
|
||||
let key = keyCodeToChar(self.0 as u64, shift_key_pressed.into());
|
||||
|
||||
if key == nil {
|
||||
if key.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let key = &*key.cast::<NSString>();
|
||||
let cstr = key.UTF8String() as *const u8;
|
||||
std::str::from_utf8(slice::from_raw_parts(cstr, key.len()))
|
||||
.ok()
|
||||
@@ -46,16 +44,21 @@ impl Keycode {
|
||||
// There could have multiple keycodes mapping to one virtual key. Return an iterator
|
||||
// to all possible values of keycode here.
|
||||
pub fn keycodes_from_key_name(key_name: &str) -> impl Iterator<Item = Keycode> {
|
||||
unsafe {
|
||||
let keycodes: id = charToKeyCodes(make_nsstring(key_name));
|
||||
let keycodes_length = keycodes.count();
|
||||
let key_name = NSString::from_str(key_name);
|
||||
// `charToKeyCodes` borrows the string only for the duration of the call
|
||||
// and returns an autoreleased array of NSNumber keycodes.
|
||||
let keycodes: *const NSArray<NSNumber> =
|
||||
unsafe { charToKeyCodes(Retained::as_ptr(&key_name) as id) }.cast();
|
||||
let keycodes_length = if keycodes.is_null() {
|
||||
0
|
||||
} else {
|
||||
unsafe { (*keycodes).count() }
|
||||
};
|
||||
|
||||
(0..keycodes_length).map(move |i| {
|
||||
let keycode: NSUInteger =
|
||||
msg_send![keycodes.objectAtIndex(i), unsignedIntegerValue];
|
||||
Self(keycode as u16)
|
||||
})
|
||||
}
|
||||
(0..keycodes_length).map(move |i| {
|
||||
let keycode = unsafe { (*keycodes).objectAtIndex(i).unsignedIntegerValue() };
|
||||
Self(keycode as u16)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
use cocoa::appkit::{NSApp, NSEventModifierFlags, NSMenu, NSMenuItem};
|
||||
use cocoa::base::selector;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
NSDownArrowFunctionKey, NSEndFunctionKey, NSF10FunctionKey, NSF11FunctionKey,
|
||||
NSF12FunctionKey, NSF13FunctionKey, NSF14FunctionKey, NSF15FunctionKey, NSF16FunctionKey,
|
||||
NSF17FunctionKey, NSF18FunctionKey, NSF19FunctionKey, NSF1FunctionKey, NSF20FunctionKey,
|
||||
NSF2FunctionKey, NSF3FunctionKey, NSF4FunctionKey, NSF5FunctionKey, NSF6FunctionKey,
|
||||
NSF7FunctionKey, NSF8FunctionKey, NSF9FunctionKey, NSHomeFunctionKey, NSInsertFunctionKey,
|
||||
NSLeftArrowFunctionKey, NSPageDownFunctionKey, NSPageUpFunctionKey,
|
||||
NSRightArrowFunctionKey, NSUpArrowFunctionKey,
|
||||
},
|
||||
base::{id, nil},
|
||||
foundation::{NSArray, NSAutoreleasePool, NSInteger},
|
||||
use std::boxed::Box;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::rc::Rc;
|
||||
|
||||
use cocoa::base::{id, nil};
|
||||
use lazy_static::lazy_static;
|
||||
use objc2::rc::{autoreleasepool, Retained};
|
||||
use objc2::runtime::Sel;
|
||||
use objc2::{sel, MainThreadMarker};
|
||||
use objc2_app_kit::{
|
||||
NSApplication, NSControlStateValue, NSDownArrowFunctionKey, NSEndFunctionKey,
|
||||
NSEventModifierFlags, NSF10FunctionKey, NSF11FunctionKey, NSF12FunctionKey, NSF13FunctionKey,
|
||||
NSF14FunctionKey, NSF15FunctionKey, NSF16FunctionKey, NSF17FunctionKey, NSF18FunctionKey,
|
||||
NSF19FunctionKey, NSF1FunctionKey, NSF20FunctionKey, NSF2FunctionKey, NSF3FunctionKey,
|
||||
NSF4FunctionKey, NSF5FunctionKey, NSF6FunctionKey, NSF7FunctionKey, NSF8FunctionKey,
|
||||
NSF9FunctionKey, NSHomeFunctionKey, NSInsertFunctionKey, NSLeftArrowFunctionKey, NSMenu,
|
||||
NSMenuItem, NSPageDownFunctionKey, NSPageUpFunctionKey, NSRightArrowFunctionKey,
|
||||
NSUpArrowFunctionKey,
|
||||
};
|
||||
use objc2_foundation::{ns_string, NSInteger, NSString};
|
||||
use galaxyui_core::actions::StandardAction;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::menu::{
|
||||
ItemTriggeredCallback, Menu, MenuBar, MenuItem, MenuItemProperties, MenuItemPropertyChanges,
|
||||
UpdateMenuItemCallback,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use objc::runtime::{NO, YES};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use std::{boxed::Box, cell::RefCell, collections::HashMap, ffi::c_void, rc::Rc};
|
||||
|
||||
use super::app::callback_dispatcher;
|
||||
use super::make_nsstring;
|
||||
|
||||
lazy_static! {
|
||||
/// A mac-menu-specific map of key names to special characters used for the keyboard shortcuts
|
||||
/// in the mac menus
|
||||
static ref MENU_KEY_EQUIVALENTS: HashMap<&'static str, char> = {
|
||||
fn to_char(key: u16) -> char {
|
||||
char::from_u32(key.into()).unwrap()
|
||||
fn to_char(key: u32) -> char {
|
||||
char::from_u32(key).unwrap()
|
||||
}
|
||||
|
||||
HashMap::from([
|
||||
@@ -161,24 +163,41 @@ extern "C" {
|
||||
}
|
||||
|
||||
struct StandardMenuItemProperties {
|
||||
title: &'static str, // menu item title
|
||||
action: &'static str, // the selector name
|
||||
shortcut: &'static str, // the key equivalent string, or empty for none
|
||||
/// The menu item title.
|
||||
title: &'static NSString,
|
||||
/// The selector to invoke.
|
||||
action: Sel,
|
||||
/// The key equivalent string, or empty for none.
|
||||
shortcut: &'static NSString,
|
||||
modifiers: NSEventModifierFlags,
|
||||
}
|
||||
|
||||
enum KeyEquivalent {
|
||||
Static(&'static NSString),
|
||||
Dynamic(Retained<NSString>),
|
||||
}
|
||||
|
||||
impl KeyEquivalent {
|
||||
fn as_nsstring(&self) -> &NSString {
|
||||
match self {
|
||||
Self::Static(value) => value,
|
||||
Self::Dynamic(value) => value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get properties from a standard action.
|
||||
fn resolve_standard_action(action: StandardAction) -> StandardMenuItemProperties {
|
||||
let cmd = NSEventModifierFlags::NSCommandKeyMask;
|
||||
let option = NSEventModifierFlags::NSAlternateKeyMask;
|
||||
let ctrl = NSEventModifierFlags::NSControlKeyMask;
|
||||
let cmd = NSEventModifierFlags::Command;
|
||||
let option = NSEventModifierFlags::Option;
|
||||
let ctrl = NSEventModifierFlags::Control;
|
||||
let none = NSEventModifierFlags::empty();
|
||||
|
||||
fn make(
|
||||
title: &'static str,
|
||||
action: &'static str,
|
||||
title: &'static NSString,
|
||||
action: Sel,
|
||||
modifiers: NSEventModifierFlags,
|
||||
shortcut: &'static str,
|
||||
shortcut: &'static NSString,
|
||||
) -> StandardMenuItemProperties {
|
||||
StandardMenuItemProperties {
|
||||
title,
|
||||
@@ -189,42 +208,73 @@ fn resolve_standard_action(action: StandardAction) -> StandardMenuItemProperties
|
||||
}
|
||||
|
||||
match action {
|
||||
StandardAction::Close => make("Close Window", "performClose:", none, ""),
|
||||
StandardAction::Quit => make("Quit Galaxy", "terminate:", cmd, "q"),
|
||||
StandardAction::Hide => make("Hide Galaxy", "hide:", cmd, "h"),
|
||||
StandardAction::HideOtherApps => {
|
||||
make("Hide Others", "hideOtherApplications:", cmd | option, "h")
|
||||
}
|
||||
StandardAction::ShowAllApps => make("Show All", "unhideAllApplications:", none, ""),
|
||||
StandardAction::Minimize => make("Minimize", "performMiniaturize:", cmd, "m"),
|
||||
StandardAction::Zoom => make("Zoom", "performZoom:", none, ""),
|
||||
StandardAction::BringAllToFront => make("Bring All to Front", "arrangeInFront:", none, ""),
|
||||
StandardAction::ToggleFullScreen => {
|
||||
make("ToggleFullScreen", "toggleFullScreen:", cmd | ctrl, "f")
|
||||
}
|
||||
StandardAction::Paste => make("Paste", "paste:", none, ""),
|
||||
StandardAction::Close => make(
|
||||
ns_string!("Close Window"),
|
||||
sel!(performClose:),
|
||||
none,
|
||||
ns_string!(""),
|
||||
),
|
||||
StandardAction::Quit => make(
|
||||
ns_string!("Quit Warp"),
|
||||
sel!(terminate:),
|
||||
cmd,
|
||||
ns_string!("q"),
|
||||
),
|
||||
StandardAction::Hide => make(ns_string!("Hide Warp"), sel!(hide:), cmd, ns_string!("h")),
|
||||
StandardAction::HideOtherApps => make(
|
||||
ns_string!("Hide Others"),
|
||||
sel!(hideOtherApplications:),
|
||||
cmd | option,
|
||||
ns_string!("h"),
|
||||
),
|
||||
StandardAction::ShowAllApps => make(
|
||||
ns_string!("Show All"),
|
||||
sel!(unhideAllApplications:),
|
||||
none,
|
||||
ns_string!(""),
|
||||
),
|
||||
StandardAction::Minimize => make(
|
||||
ns_string!("Minimize"),
|
||||
sel!(performMiniaturize:),
|
||||
cmd,
|
||||
ns_string!("m"),
|
||||
),
|
||||
StandardAction::Zoom => make(ns_string!("Zoom"), sel!(performZoom:), none, ns_string!("")),
|
||||
StandardAction::BringAllToFront => make(
|
||||
ns_string!("Bring All to Front"),
|
||||
sel!(arrangeInFront:),
|
||||
none,
|
||||
ns_string!(""),
|
||||
),
|
||||
StandardAction::ToggleFullScreen => make(
|
||||
ns_string!("ToggleFullScreen"),
|
||||
sel!(toggleFullScreen:),
|
||||
cmd | ctrl,
|
||||
ns_string!("f"),
|
||||
),
|
||||
StandardAction::Paste => make(ns_string!("Paste"), sel!(paste:), none, ns_string!("")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine the key equivalent for the given keystroke
|
||||
fn resolve_key_equivalent(keystroke: Option<&Keystroke>) -> (id, NSEventModifierFlags) {
|
||||
fn resolve_key_equivalent(keystroke: Option<&Keystroke>) -> (KeyEquivalent, NSEventModifierFlags) {
|
||||
let mut flags = NSEventModifierFlags::empty();
|
||||
|
||||
let keystroke = match keystroke {
|
||||
Some(value) => value,
|
||||
None => return (make_nsstring(""), flags),
|
||||
None => return (KeyEquivalent::Static(ns_string!("")), flags),
|
||||
};
|
||||
|
||||
let key_equivalent = match MENU_KEY_EQUIVALENTS.get(keystroke.key.as_str()) {
|
||||
Some(c) => make_nsstring(String::from(*c)),
|
||||
None => make_nsstring(&keystroke.key),
|
||||
Some(c) => KeyEquivalent::Dynamic(NSString::from_str(&String::from(*c))),
|
||||
None => KeyEquivalent::Dynamic(NSString::from_str(&keystroke.key)),
|
||||
};
|
||||
|
||||
for (is_set, flag) in [
|
||||
(keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
|
||||
(keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
|
||||
(keystroke.shift, NSEventModifierFlags::NSShiftKeyMask),
|
||||
(keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
|
||||
(keystroke.cmd, NSEventModifierFlags::Command),
|
||||
(keystroke.alt, NSEventModifierFlags::Option),
|
||||
(keystroke.shift, NSEventModifierFlags::Shift),
|
||||
(keystroke.ctrl, NSEventModifierFlags::Control),
|
||||
] {
|
||||
if is_set {
|
||||
flags |= flag
|
||||
@@ -238,39 +288,42 @@ fn resolve_key_equivalent(keystroke: Option<&Keystroke>) -> (id, NSEventModifier
|
||||
unsafe fn apply_changes(changes: MenuItemPropertyChanges, item: id) {
|
||||
// Wrap in a local autorelease pool: AppKit invokes `warp_menu_item_needs_update`
|
||||
// on every menu validation (per menu open and per keystroke for shortcut matching),
|
||||
// so this is a hot path. A local pool bounds peak memory for the NSString temporaries
|
||||
// created here (item title, key equivalent) without relying on the outer AppKit pool.
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
if let Some(name) = changes.name {
|
||||
let _: () = msg_send![item, setTitle: make_nsstring(name)];
|
||||
}
|
||||
if let Some(keystroke) = changes.keystroke {
|
||||
let (key_equivalent, modifiers) = resolve_key_equivalent(keystroke.as_ref());
|
||||
let _: () = msg_send![item, setKeyEquivalent: key_equivalent];
|
||||
let _: () = msg_send![item, setKeyEquivalentModifierMask: modifiers];
|
||||
}
|
||||
if let Some(disabled) = changes.disabled {
|
||||
let enabled = if disabled { NO } else { YES };
|
||||
let _: () = msg_send![item, setEnabled: enabled];
|
||||
}
|
||||
if let Some(checked) = changes.checked {
|
||||
// NSControlStateValue has Off as 0, On as 1, Mixed as -1.
|
||||
let control_state: NSInteger = i64::from(checked);
|
||||
let _: () = msg_send![item, setState: control_state];
|
||||
}
|
||||
if let Some(submenu) = changes.submenu {
|
||||
let nsmenu = submenu
|
||||
.map(|menu_items| make_submenu(menu_items))
|
||||
.unwrap_or(nil);
|
||||
set_menu_item_submenu(item, nsmenu);
|
||||
}
|
||||
pool.drain();
|
||||
// so this is a hot path. A local pool bounds peak memory for the temporaries AppKit
|
||||
// produces here (e.g. inside `setTitle:`/`setKeyEquivalent:`) without relying on the
|
||||
// outer AppKit pool.
|
||||
autoreleasepool(|_| unsafe {
|
||||
let menu_item = &*item.cast::<NSMenuItem>();
|
||||
if let Some(name) = changes.name {
|
||||
menu_item.setTitle(&NSString::from_str(&name));
|
||||
}
|
||||
if let Some(keystroke) = changes.keystroke {
|
||||
let (key_equivalent, modifiers) = resolve_key_equivalent(keystroke.as_ref());
|
||||
menu_item.setKeyEquivalent(key_equivalent.as_nsstring());
|
||||
menu_item.setKeyEquivalentModifierMask(modifiers);
|
||||
}
|
||||
if let Some(disabled) = changes.disabled {
|
||||
menu_item.setEnabled(!disabled);
|
||||
}
|
||||
if let Some(checked) = changes.checked {
|
||||
// NSControlStateValue has Off as 0, On as 1, Mixed as -1.
|
||||
let control_state = i64::from(checked) as NSControlStateValue;
|
||||
menu_item.setState(control_state);
|
||||
}
|
||||
if let Some(submenu) = changes.submenu {
|
||||
let nsmenu = match submenu {
|
||||
Some(menu_items) => make_submenu(menu_items),
|
||||
None => nil,
|
||||
};
|
||||
set_menu_item_submenu(item, nsmenu);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unsafe fn make_submenu(menu_items: Vec<MenuItem>) -> id {
|
||||
let nsmenu = make_delegated_menu(make_nsstring(""));
|
||||
let nsmenu = make_delegated_menu(ns_string!("") as *const NSString as id);
|
||||
let nsmenu_ref = &*nsmenu.cast::<NSMenu>();
|
||||
for menu_item in menu_items {
|
||||
nsmenu.addItem_(make_menu_item(menu_item));
|
||||
nsmenu_ref.addItem(&*make_menu_item(menu_item).cast::<NSMenuItem>());
|
||||
}
|
||||
nsmenu
|
||||
}
|
||||
@@ -296,19 +349,22 @@ unsafe fn make_menu_item(menu_item: MenuItem) -> id {
|
||||
nsmenu_item
|
||||
}
|
||||
MenuItem::Standard(standard_action) => {
|
||||
let mtm = MainThreadMarker::new_unchecked();
|
||||
let properties = resolve_standard_action(standard_action);
|
||||
let nsmenu_item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
make_nsstring(properties.title),
|
||||
selector(properties.action),
|
||||
make_nsstring(properties.shortcut),
|
||||
)
|
||||
.autorelease();
|
||||
nsmenu_item.setKeyEquivalentModifierMask_(properties.modifiers);
|
||||
let _: id = msg_send![nsmenu_item, setTag: standard_action as libc::c_long];
|
||||
nsmenu_item
|
||||
let nsmenu_item = NSMenuItem::initWithTitle_action_keyEquivalent(
|
||||
mtm.alloc(),
|
||||
properties.title,
|
||||
Some(properties.action),
|
||||
properties.shortcut,
|
||||
);
|
||||
nsmenu_item.setKeyEquivalentModifierMask(properties.modifiers);
|
||||
nsmenu_item.setTag(standard_action as NSInteger);
|
||||
Retained::autorelease_ptr(nsmenu_item) as id
|
||||
}
|
||||
MenuItem::Separator => {
|
||||
Retained::autorelease_ptr(NSMenuItem::separatorItem(MainThreadMarker::new_unchecked()))
|
||||
as id
|
||||
}
|
||||
MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
||||
MenuItem::Services => make_services_menu_item(),
|
||||
}
|
||||
}
|
||||
@@ -316,37 +372,41 @@ unsafe fn make_menu_item(menu_item: MenuItem) -> id {
|
||||
/// \return an autoreleased NSMenuItem with a submenu represented by \p menu.
|
||||
// This supports creating the top-level menu bar.
|
||||
unsafe fn make_top_level_menu_item(menu: Menu) -> id {
|
||||
let nsmenu = make_delegated_menu(make_nsstring(&menu.title));
|
||||
let mtm = MainThreadMarker::new_unchecked();
|
||||
let nsmenu = make_delegated_menu(Retained::as_ptr(&NSString::from_str(&menu.title)) as id);
|
||||
let nsmenu = &*nsmenu.cast::<NSMenu>();
|
||||
|
||||
if menu.is_window_menu() {
|
||||
// `setWindowsMenu` gives us all the default window menu items like
|
||||
// 'Enter Full Screen' and 'Tile Window to Left of Screen'.
|
||||
let () = msg_send![NSApp(), setWindowsMenu: nsmenu];
|
||||
NSApplication::sharedApplication(mtm).setWindowsMenu(Some(nsmenu));
|
||||
}
|
||||
|
||||
for menu_item in menu.menu_items {
|
||||
nsmenu.addItem_(make_menu_item(menu_item));
|
||||
nsmenu.addItem(&*make_menu_item(menu_item).cast::<NSMenuItem>());
|
||||
}
|
||||
|
||||
let menuitem = NSMenuItem::alloc(nil).init().autorelease();
|
||||
menuitem.setSubmenu_(nsmenu);
|
||||
menuitem
|
||||
let menuitem = NSMenuItem::new(mtm);
|
||||
menuitem.setSubmenu(Some(nsmenu));
|
||||
Retained::autorelease_ptr(menuitem) as id
|
||||
}
|
||||
|
||||
/// \return an autoreleased NSMenu representing the given menu bar.
|
||||
pub unsafe fn make_main_menu(menubar: MenuBar) -> id {
|
||||
let main_menu = NSMenu::alloc(nil).init().autorelease();
|
||||
/// \return an NSMenu representing the given menu bar.
|
||||
pub unsafe fn make_main_menu(menubar: MenuBar) -> Retained<NSMenu> {
|
||||
let mtm = MainThreadMarker::new_unchecked();
|
||||
let main_menu = NSMenu::new(mtm);
|
||||
for menu in menubar.menus {
|
||||
main_menu.addItem_(make_top_level_menu_item(menu));
|
||||
main_menu.addItem(&*make_top_level_menu_item(menu).cast::<NSMenuItem>());
|
||||
}
|
||||
main_menu
|
||||
}
|
||||
|
||||
/// \return an autoreleased NSMenu representing the given dock menu.
|
||||
pub unsafe fn make_dock_menu(menu: Menu) -> id {
|
||||
let dock_menu = NSMenu::alloc(nil).init().autorelease();
|
||||
/// \return an NSMenu representing the given dock menu.
|
||||
pub unsafe fn make_dock_menu(menu: Menu) -> Retained<NSMenu> {
|
||||
let mtm = MainThreadMarker::new_unchecked();
|
||||
let dock_menu = NSMenu::new(mtm);
|
||||
for item in menu.menu_items {
|
||||
dock_menu.addItem_(make_menu_item(item));
|
||||
dock_menu.addItem(&*make_menu_item(item).cast::<NSMenuItem>());
|
||||
}
|
||||
dock_menu
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
mod alert;
|
||||
mod app;
|
||||
pub mod clipboard;
|
||||
pub mod delegate;
|
||||
@@ -15,28 +16,27 @@ pub mod utils;
|
||||
mod window;
|
||||
|
||||
pub use app::{App, AppExt};
|
||||
use clipboard::*;
|
||||
use cocoa::base::{id, nil};
|
||||
use cocoa::foundation::NSAutoreleasePool;
|
||||
pub use delegate::{AppDelegate, IntegrationTestDelegate};
|
||||
pub use fonts::FontDB;
|
||||
pub use rendering::is_low_power_gpu_available;
|
||||
pub use window::Window;
|
||||
pub use window::WindowExt;
|
||||
|
||||
use clipboard::*;
|
||||
|
||||
use geometry::*;
|
||||
|
||||
use cocoa::{
|
||||
base::{id, nil},
|
||||
foundation::{NSAutoreleasePool, NSString},
|
||||
};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use objc2::rc::Retained;
|
||||
use objc2_foundation::NSString;
|
||||
pub use rendering::is_low_power_gpu_available;
|
||||
pub use window::{Window, WindowExt};
|
||||
|
||||
/// Create an autoreleased NSString from a string reference.
|
||||
pub fn make_nsstring<S>(s: S) -> id
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
unsafe { NSString::alloc(nil).init_str(s.as_ref()).autorelease() }
|
||||
// `NSString::from_str` returns a +1-retained `Retained<NSString>`.
|
||||
// `autorelease_ptr` hands that retain count to the innermost autorelease
|
||||
// pool and returns the raw pointer.
|
||||
Retained::autorelease_ptr(NSString::from_str(s.as_ref())).cast()
|
||||
}
|
||||
|
||||
/// Holds a Cocoa autorelease pool and drains it when the guard is dropped.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::DateTime;
|
||||
use cocoa::base::id;
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use objc2_foundation::NSUInteger;
|
||||
use galaxyui_core::notification::{
|
||||
NotificationResponse, NotificationSendError, RequestPermissionsOutcome,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
: NSObject <NSApplicationDelegate, NSMenuDelegate, UNUserNotificationCenterDelegate>
|
||||
|
||||
@property(strong) NSMenu *dockMenu;
|
||||
- (BOOL)setDockIconVisible:(BOOL)visible;
|
||||
|
||||
@end
|
||||
|
||||
@@ -32,7 +33,7 @@ void warp_app_active_window_changed(id app);
|
||||
void warp_app_notification_clicked(id app, double date, id data);
|
||||
void warp_app_open_urls(id app, id urls);
|
||||
void warp_app_os_appearance_changed(id app);
|
||||
BOOL warp_app_should_terminate_app(id app);
|
||||
BOOL warp_app_should_terminate_app(id app, BOOL systemInitiated);
|
||||
BOOL warp_app_should_close_window(id app, id window);
|
||||
BOOL warp_app_are_key_bindings_disabled_for_window(id app, id window);
|
||||
BOOL warp_app_has_binding_for_keystroke(id app, id event);
|
||||
|
||||
@@ -254,8 +254,30 @@ NSUInteger activeScreenId() {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns YES when the in-flight quit Apple event carries a kAEQuitReason
|
||||
// indicating the system (logout / restart / shutdown / scheduled OS update)
|
||||
// initiated the termination, as opposed to the user quitting the app directly.
|
||||
// Per AERegistry.h, the documented kAEQuitReason values are kAEQuitAll,
|
||||
// kAEShutDown, kAERestart, and kAEReallyLogOut; a missing event or reason
|
||||
// (both accessors return 0 on nil) means the user or another process quit us.
|
||||
static BOOL isSystemInitiatedTermination(void) {
|
||||
NSAppleEventDescriptor *event =
|
||||
[[NSAppleEventManager sharedAppleEventManager] currentAppleEvent];
|
||||
NSAppleEventDescriptor *reason = [event attributeDescriptorForKeyword:kAEQuitReason];
|
||||
switch ([reason typeCodeValue]) {
|
||||
case kAEQuitAll:
|
||||
case kAEShutDown:
|
||||
case kAERestart:
|
||||
case kAEReallyLogOut:
|
||||
return YES;
|
||||
default:
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)application {
|
||||
BOOL okToTerminate = YES;
|
||||
BOOL systemInitiated = isSystemInitiatedTermination();
|
||||
|
||||
// If this is the second termination attempt after we've already hidden the app, we can go ahead
|
||||
// and terminate.
|
||||
@@ -266,10 +288,18 @@ NSUInteger activeScreenId() {
|
||||
if (!forceTermination) {
|
||||
// Make sure the rust app doesn't have any reasons to interrupt quit, e.g. needs to relaunch
|
||||
// for autoupdate, but launching the new process failed.
|
||||
okToTerminate = warp_app_should_terminate_app(application);
|
||||
okToTerminate = warp_app_should_terminate_app(application, systemInitiated);
|
||||
}
|
||||
|
||||
if (okToTerminate) {
|
||||
if (systemInitiated) {
|
||||
// Comply immediately when the system asked us to quit. Anything but
|
||||
// `NSTerminateNow` here (including the hide-then-reterminate dance
|
||||
// below, which returns `NSTerminateCancel`) makes macOS treat Warp
|
||||
// as blocking the logout/shutdown, which can abort a scheduled OS
|
||||
// update and leave the app in a stuck-looking state (#12441).
|
||||
return NSTerminateNow;
|
||||
}
|
||||
// We want to hide the application before we start the teardown
|
||||
// process, to ensure the user isn't affected by any slow teardown
|
||||
// steps. The tricky part here is that a call to `[NSApp hide]` isn't
|
||||
@@ -437,6 +467,11 @@ NSUInteger activeScreenId() {
|
||||
return self.dockMenu;
|
||||
}
|
||||
|
||||
- (BOOL)setDockIconVisible:(BOOL)visible {
|
||||
NSApplicationActivationPolicy policy =
|
||||
visible ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory;
|
||||
return [NSApp setActivationPolicy:policy];
|
||||
}
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||
didReceiveNotificationResponse:(UNNotificationResponse *)response
|
||||
withCompletionHandler:(void (^)(void))completionHandler {
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
enableTitlebarDrag:(BOOL)enableTitlebarDrag
|
||||
testMode:(BOOL)testMode;
|
||||
- (void)setAsyncCallback:(BOOL)shouldAsync;
|
||||
- (void)setPresentsWithTransaction:(BOOL)presentsWithTransaction;
|
||||
- (BOOL)keyDownImpl:(NSEvent *)event;
|
||||
@end
|
||||
|
||||
@@ -55,6 +55,14 @@ void warp_marked_text_cleared(WarpHostView *);
|
||||
|
||||
// Whether we're in the middle of a call to interpretKeyEvents.
|
||||
BOOL interpretingKeyEvents;
|
||||
|
||||
// Whether the IME modified marked text (via setMarkedText: or unmarkText)
|
||||
// during the current interpretKeyEvents: pass. Used to avoid wiping a
|
||||
// freshly-set marked text in the split-commit scenario where an IME
|
||||
// calls insertText: (committing some text) and then setMarkedText: (with
|
||||
// new in-progress text) in the same keystroke. Without this, the trailing
|
||||
// unmarkText in keyDownImpl would clobber that new marked text.
|
||||
BOOL imeTouchedMarkedTextDuringInterpret;
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstResponder {
|
||||
@@ -145,6 +153,10 @@ void warp_marked_text_cleared(WarpHostView *);
|
||||
- (void)setAsyncCallback:(BOOL)shouldAsync {
|
||||
asyncCallback = shouldAsync;
|
||||
}
|
||||
- (void)setPresentsWithTransaction:(BOOL)presentsWithTransaction {
|
||||
CAMetalLayer *layer = (CAMetalLayer *)self.layer;
|
||||
layer.presentsWithTransaction = presentsWithTransaction;
|
||||
}
|
||||
|
||||
- (void)keyDown:(NSEvent *)event {
|
||||
[self keyDownImpl:event];
|
||||
@@ -153,6 +165,7 @@ void warp_marked_text_cleared(WarpHostView *);
|
||||
- (BOOL)keyDownImpl:(NSEvent *)event {
|
||||
BOOL wasComposing = [self hasMarkedText];
|
||||
[textToInsert setString:@""];
|
||||
imeTouchedMarkedTextDuringInterpret = NO;
|
||||
|
||||
// Interpret the key events here so we could check whether user is composing
|
||||
// text within the IME and pass the state down to the KeyDown events.
|
||||
@@ -182,7 +195,14 @@ void warp_marked_text_cleared(WarpHostView *);
|
||||
// Dispatch TypedCharacter event after KeyDown has been dispatched.
|
||||
if ([textToInsert length] > 0 && !handled) {
|
||||
warp_handle_insert_text(self, (NSString *)textToInsert);
|
||||
[self unmarkText];
|
||||
// Only clear marked text if the IME did not touch it during this
|
||||
// interpretKeyEvents pass. Otherwise we'd either fire a redundant
|
||||
// ClearMarkedText (if IME already cleared) or, worse, wipe the new
|
||||
// marked text the IME just set in a split-commit (e.g. Japanese IME
|
||||
// committing a phrase and queuing the next character as marked text).
|
||||
if (!imeTouchedMarkedTextDuringInterpret) {
|
||||
[self unmarkText];
|
||||
}
|
||||
}
|
||||
|
||||
return handled;
|
||||
@@ -259,7 +279,7 @@ void warp_marked_text_cleared(WarpHostView *);
|
||||
layer.allowsNextDrawableTimeout = NO;
|
||||
layer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
|
||||
layer.needsDisplayOnBoundsChange = YES;
|
||||
layer.presentsWithTransaction = YES;
|
||||
layer.presentsWithTransaction = NO;
|
||||
layer.delegate = self;
|
||||
layer.opaque = NO;
|
||||
return layer;
|
||||
@@ -465,6 +485,10 @@ void warp_marked_text_cleared(WarpHostView *);
|
||||
- (void)setMarkedText:(id)string
|
||||
selectedRange:(NSRange)selectedRange
|
||||
replacementRange:(NSRange)replacementRange {
|
||||
if (interpretingKeyEvents) {
|
||||
imeTouchedMarkedTextDuringInterpret = YES;
|
||||
}
|
||||
|
||||
[markedText release];
|
||||
if ([string isKindOfClass:[NSAttributedString class]])
|
||||
markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string];
|
||||
@@ -482,6 +506,9 @@ void warp_marked_text_cleared(WarpHostView *);
|
||||
}
|
||||
|
||||
- (void)unmarkText {
|
||||
if (interpretingKeyEvents) {
|
||||
imeTouchedMarkedTextDuringInterpret = YES;
|
||||
}
|
||||
[[markedText mutableString] setString:@""];
|
||||
if (self.readyForWarp) {
|
||||
warp_update_ime_state(self, NO);
|
||||
|
||||
@@ -21,6 +21,8 @@ NSWindowStyleMask warpWindowMask = NSWindowStyleMaskClosable | NSWindowStyleMask
|
||||
|
||||
// The default macOS titlebar height (in points).
|
||||
static const CGFloat DEFAULT_TITLEBAR_HEIGHT = 28.0;
|
||||
static const NSSize MIN_WINDOW_SIZE = {480.0, 192.0};
|
||||
static const NSSize TEST_MIN_WINDOW_SIZE = {124.0, 34.0};
|
||||
|
||||
// A back-to-front ordered array of windows, identified by their `windowNumber`
|
||||
// property.
|
||||
@@ -141,12 +143,20 @@ NSNumber *previouslyActiveAppPID;
|
||||
// we explicitly force callbacks to be synchronous if it's caused by the user instead
|
||||
// of another system call (such as the active screen changing)
|
||||
[warp_view setAsyncCallback:NO];
|
||||
|
||||
// While the user is dragging to resize the window, we want to present frames
|
||||
// within transactions to ensure the resize is visually smooth and there is no
|
||||
// stuttering resulting from asynchronous presentation.
|
||||
[warp_view setPresentsWithTransaction:YES];
|
||||
}
|
||||
|
||||
- (void)windowDidEndLiveResize:(NSNotification *)notification {
|
||||
WarpWindow *warp_window = notification.object;
|
||||
WarpHostView *warp_view = warp_window.contentView;
|
||||
|
||||
// Reset state changed in `windowWillStartLiveResize`.
|
||||
[warp_view setAsyncCallback:YES];
|
||||
[warp_view setPresentsWithTransaction:NO];
|
||||
}
|
||||
|
||||
- (void)setForceTermination {
|
||||
@@ -285,6 +295,7 @@ static NSLayoutConstraint *configure_titlebar_height(NSWindow *window, CGFloat h
|
||||
void init_warp_nswindow(NSWindow<WarpWindowProtocol> *window, bool testMode, bool hideTitleBar) {
|
||||
window.testMode = testMode;
|
||||
window.hideTitleBar = hideTitleBar;
|
||||
NSSize minWindowSize = testMode ? TEST_MIN_WINDOW_SIZE : MIN_WINDOW_SIZE;
|
||||
|
||||
// Set the background color to clear to support window background transparency. When this is set
|
||||
// to NSColor.clearColor with alpha = 0 and window drop shadows are enabled, MacOS renders a
|
||||
@@ -298,8 +309,22 @@ void init_warp_nswindow(NSWindow<WarpWindowProtocol> *window, bool testMode, boo
|
||||
window.acceptsMouseMovedEvents = YES;
|
||||
window.titlebarAppearsTransparent = hideTitleBar;
|
||||
window.titleVisibility = hideTitleBar ? NSWindowTitleHidden : NSWindowTitleVisible;
|
||||
window.minSize = minWindowSize;
|
||||
window.contentMinSize = minWindowSize;
|
||||
if ([window respondsToSelector:@selector(setMinFullScreenContentSize:)]) {
|
||||
window.minFullScreenContentSize = minWindowSize;
|
||||
}
|
||||
}
|
||||
|
||||
@interface NSWindow (PrivateAPI)
|
||||
- (NSInteger)_resizeDirectionForMouseLocation:(NSPoint)location;
|
||||
@end
|
||||
|
||||
@interface WarpWindow ()
|
||||
- (NSButton *)standardWindowButtonAtEvent:(NSEvent *)event;
|
||||
- (BOOL)eventIsOverResizeEdge:(NSEvent *)event;
|
||||
@end
|
||||
|
||||
@implementation WarpWindow {
|
||||
// The windowState is managed on the Rust side.
|
||||
void *windowState;
|
||||
@@ -316,6 +341,7 @@ void init_warp_nswindow(NSWindow<WarpWindowProtocol> *window, bool testMode, boo
|
||||
// macOS from cascading or clamping the window position while a tab-drag preview window is
|
||||
// being created and positioned under the cursor.
|
||||
BOOL _suppressFrameConstraintsDuringDrag;
|
||||
BOOL _leftMouseDownStartedInNativeWindowChrome;
|
||||
}
|
||||
|
||||
@synthesize testMode;
|
||||
@@ -385,8 +411,50 @@ void init_warp_nswindow(NSWindow<WarpWindowProtocol> *window, bool testMode, boo
|
||||
return [super constrainFrameRect:frameRect toScreen:screen];
|
||||
}
|
||||
|
||||
- (NSButton *)standardWindowButtonAtEvent:(NSEvent *)event {
|
||||
NSWindowButton buttons[] = {
|
||||
NSWindowCloseButton,
|
||||
NSWindowMiniaturizeButton,
|
||||
NSWindowZoomButton,
|
||||
};
|
||||
|
||||
for (NSUInteger i = 0; i < sizeof(buttons) / sizeof(buttons[0]); i++) {
|
||||
NSButton *button = [self standardWindowButton:buttons[i]];
|
||||
if (button && !button.hidden) {
|
||||
NSPoint point = [button convertPoint:event.locationInWindow fromView:nil];
|
||||
if (NSPointInRect(point, button.bounds)) {
|
||||
return button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)eventIsOverResizeEdge:(NSEvent *)event {
|
||||
if ((self.styleMask & NSWindowStyleMaskResizable) == 0) {
|
||||
return NO;
|
||||
}
|
||||
if ([self respondsToSelector:@selector(_resizeDirectionForMouseLocation:)]) {
|
||||
return [self _resizeDirectionForMouseLocation:event.locationInWindow] != -1;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)sendEvent:(NSEvent *)event {
|
||||
switch (event.type) {
|
||||
case NSEventTypeLeftMouseDown: {
|
||||
NSButton *windowButton = [self standardWindowButtonAtEvent:event];
|
||||
if (windowButton) {
|
||||
_leftMouseDownStartedInNativeWindowChrome = NO;
|
||||
[windowButton mouseDown:event];
|
||||
break;
|
||||
}
|
||||
_leftMouseDownStartedInNativeWindowChrome = [self eventIsOverResizeEdge:event];
|
||||
[super sendEvent:event];
|
||||
break;
|
||||
}
|
||||
|
||||
// In some cases, NSWindow's default sendEvent: implementation will dispatch a MouseDown
|
||||
// event and subsequent MouseDragged events to the content view, but then dispatch the
|
||||
// remaining MouseDragged events and MouseUp event elsewhere.
|
||||
@@ -396,10 +464,27 @@ void init_warp_nswindow(NSWindow<WarpWindowProtocol> *window, bool testMode, boo
|
||||
// This breaks drag-and-drop for panes and tabs (see CLD-2581), so we work around it with
|
||||
// custom dispatching.
|
||||
case NSEventTypeLeftMouseUp:
|
||||
[self.contentView mouseUp:event];
|
||||
if (@available(macOS 27, *)) {
|
||||
if (_leftMouseDownStartedInNativeWindowChrome) {
|
||||
[super sendEvent:event];
|
||||
} else {
|
||||
[self.contentView mouseUp:event];
|
||||
}
|
||||
} else {
|
||||
[self.contentView mouseUp:event];
|
||||
}
|
||||
_leftMouseDownStartedInNativeWindowChrome = NO;
|
||||
break;
|
||||
case NSEventTypeLeftMouseDragged:
|
||||
[self.contentView mouseDragged:event];
|
||||
if (@available(macOS 27, *)) {
|
||||
if (_leftMouseDownStartedInNativeWindowChrome) {
|
||||
[super sendEvent:event];
|
||||
} else {
|
||||
[self.contentView mouseDragged:event];
|
||||
}
|
||||
} else {
|
||||
[self.contentView mouseDragged:event];
|
||||
}
|
||||
break;
|
||||
|
||||
// The NSWindow's default sendEvent: implementation does not propagate RightMouseDown events
|
||||
@@ -432,6 +517,15 @@ void init_warp_nswindow(NSWindow<WarpWindowProtocol> *window, bool testMode, boo
|
||||
// We need to bypass the default performKeyEquivalent implementation which, in the case of
|
||||
// having keybinding conflicts with MacOS itself, yields priority to the OS.
|
||||
if ([event type] == NSEventTypeKeyDown) {
|
||||
// Skip the key-equivalent priority path while the IME has marked text. Arrow keys carry
|
||||
// NSEventModifierFlagFunction, so AppKit delivers them here before keyDown:. If we call
|
||||
// keyDownImpl and Rust suppresses the keystroke (composing mode), we return NO, and AppKit
|
||||
// proceeds to call keyDown: — running interpretKeyEvents a second time for the same event.
|
||||
// See #9709.
|
||||
if ([(WarpHostView *)self.contentView hasMarkedText]) {
|
||||
return [super performKeyEquivalent:event];
|
||||
}
|
||||
|
||||
NSApplication *application = [NSApplication sharedApplication];
|
||||
|
||||
// If we are recording a keystroke for an EditableBinding.
|
||||
@@ -456,8 +550,12 @@ void init_warp_nswindow(NSWindow<WarpWindowProtocol> *window, bool testMode, boo
|
||||
WarpWindowDelegate *delegate = self.delegate;
|
||||
if (forceTermination) {
|
||||
[delegate setForceTermination];
|
||||
// Bypass performClose: (which can be deferred or vetoed by the
|
||||
// delegate's shouldClose) and tear the window down right away.
|
||||
[self close];
|
||||
} else {
|
||||
[self performClose:nil];
|
||||
}
|
||||
[self performClose:nil];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1001,6 +1099,15 @@ void hide_window(WarpWindow<WarpWindowProtocol> *window) {
|
||||
[window orderOut:nil];
|
||||
}
|
||||
|
||||
// Sets the per-window opacity. Unlike `hide_window`, this does not change the
|
||||
// window's z-order, key state, or the app's active state — making it a much
|
||||
// cheaper way to visually hide a window (e.g. a tab drag preview) without
|
||||
// triggering AppKit's `orderOut:` machinery or the previous-app activation
|
||||
// dance.
|
||||
void set_window_alpha(WarpWindow<WarpWindowProtocol> *window, double alpha) {
|
||||
[window setAlphaValue:alpha];
|
||||
}
|
||||
|
||||
void set_window_title(id window, NSString *title) {
|
||||
if ([window isKindOfClass:[WarpPanel class]] && [window isVisible]) {
|
||||
// For the hotkey window (which is an NSPanel), we need to explicitly
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
use std::ffi::c_void;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use galaxyui_core::platform::CapturedFrame;
|
||||
use metal::{MTLPixelFormat, MTLStorageMode};
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::ProtocolObject;
|
||||
use objc2_metal::{
|
||||
MTLDevice, MTLOrigin, MTLPixelFormat, MTLRegion, MTLSize, MTLStorageMode, MTLTexture,
|
||||
MTLTextureDescriptor, MTLTextureUsage,
|
||||
};
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -19,12 +27,15 @@ mod tests;
|
||||
/// # Returns
|
||||
/// * `Some(CapturedFrame)` containing the RGBA pixel data if successful
|
||||
/// * `None` if the texture dimensions are invalid
|
||||
pub fn capture_frame(texture: &metal::TextureRef, size: Vector2F) -> Option<CapturedFrame> {
|
||||
pub fn capture_frame(
|
||||
texture: &ProtocolObject<dyn MTLTexture>,
|
||||
size: Vector2F,
|
||||
) -> Option<CapturedFrame> {
|
||||
let width = size.x() as usize;
|
||||
let height = size.y() as usize;
|
||||
|
||||
if width == 0 || height == 0 {
|
||||
log::warn!("Invalid texture dimensions: {}x{}", width, height);
|
||||
log::warn!("Invalid texture dimensions: {width}x{height}");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -33,21 +44,26 @@ pub fn capture_frame(texture: &metal::TextureRef, size: Vector2F) -> Option<Capt
|
||||
|
||||
let mut pixel_data: Vec<u8> = vec![0u8; buffer_size];
|
||||
|
||||
let region = metal::MTLRegion {
|
||||
origin: metal::MTLOrigin { x: 0, y: 0, z: 0 },
|
||||
size: metal::MTLSize {
|
||||
width: width as u64,
|
||||
height: height as u64,
|
||||
let region = MTLRegion {
|
||||
origin: MTLOrigin { x: 0, y: 0, z: 0 },
|
||||
size: MTLSize {
|
||||
width,
|
||||
height,
|
||||
depth: 1,
|
||||
},
|
||||
};
|
||||
|
||||
texture.get_bytes(
|
||||
pixel_data.as_mut_ptr() as *mut std::ffi::c_void,
|
||||
bytes_per_row as u64,
|
||||
region,
|
||||
0,
|
||||
);
|
||||
// SAFETY: `pixel_data` holds `bytes_per_row * height` bytes, matching the requested region and
|
||||
// row stride, so Metal copies the texture contents into a valid buffer.
|
||||
unsafe {
|
||||
texture.getBytes_bytesPerRow_fromRegion_mipmapLevel(
|
||||
NonNull::new(pixel_data.as_mut_ptr() as *mut c_void)
|
||||
.expect("pixel buffer pointer is non-null"),
|
||||
bytes_per_row,
|
||||
region,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
Some(CapturedFrame::new_bgra(
|
||||
width as u32,
|
||||
@@ -79,26 +95,30 @@ pub(crate) fn convert_bgra_to_rgba(data: &mut [u8]) {
|
||||
/// * A new Metal texture that can be rendered to and read back from
|
||||
#[allow(dead_code)]
|
||||
pub fn create_capture_texture(
|
||||
device: &metal::Device,
|
||||
width: u64,
|
||||
height: u64,
|
||||
device: &ProtocolObject<dyn MTLDevice>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
pixel_format: MTLPixelFormat,
|
||||
) -> metal::Texture {
|
||||
let texture_descriptor = metal::TextureDescriptor::new();
|
||||
texture_descriptor.set_pixel_format(pixel_format);
|
||||
texture_descriptor.set_width(width);
|
||||
texture_descriptor.set_height(height);
|
||||
texture_descriptor.set_depth(1);
|
||||
texture_descriptor.set_mipmap_level_count(1);
|
||||
texture_descriptor.set_sample_count(1);
|
||||
texture_descriptor.set_array_length(1);
|
||||
) -> Retained<ProtocolObject<dyn MTLTexture>> {
|
||||
let texture_descriptor = MTLTextureDescriptor::new();
|
||||
texture_descriptor.setPixelFormat(pixel_format);
|
||||
// SAFETY: the dimensions are caller-provided valid texture sizes within Metal limits.
|
||||
unsafe {
|
||||
texture_descriptor.setWidth(width);
|
||||
texture_descriptor.setHeight(height);
|
||||
texture_descriptor.setDepth(1);
|
||||
texture_descriptor.setMipmapLevelCount(1);
|
||||
texture_descriptor.setSampleCount(1);
|
||||
texture_descriptor.setArrayLength(1);
|
||||
}
|
||||
|
||||
// Set usage flags for rendering and reading
|
||||
texture_descriptor
|
||||
.set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
|
||||
texture_descriptor.setUsage(MTLTextureUsage::RenderTarget | MTLTextureUsage::ShaderRead);
|
||||
|
||||
// Use managed storage mode so we can read it back
|
||||
texture_descriptor.set_storage_mode(MTLStorageMode::Managed);
|
||||
texture_descriptor.setStorageMode(MTLStorageMode::Managed);
|
||||
|
||||
device.new_texture(&texture_descriptor)
|
||||
device
|
||||
.newTextureWithDescriptor(&texture_descriptor)
|
||||
.expect("device should create a capture texture")
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use objc2::runtime::ProtocolObject;
|
||||
use objc2_metal::MTLDevice;
|
||||
|
||||
pub mod frame_capture;
|
||||
mod renderer;
|
||||
mod renderer_manager;
|
||||
@@ -8,6 +11,6 @@ pub use renderer_manager::RendererManager;
|
||||
///
|
||||
/// In dual GPU Macs, this is `false` for the discrete high-performance GPU.
|
||||
#[cfg_attr(wgpu, allow(dead_code))]
|
||||
pub fn is_integrated_gpu(device: &metal::Device) -> bool {
|
||||
device.is_low_power() && !device.is_removable()
|
||||
pub fn is_integrated_gpu(device: &ProtocolObject<dyn MTLDevice>) -> bool {
|
||||
device.isLowPower() && !device.isRemovable()
|
||||
}
|
||||
|
||||
@@ -1,54 +1,61 @@
|
||||
use crate::rendering::atlas::{AllocatedRegion, TextureId};
|
||||
use crate::rendering::{get_best_dash_gap, GlyphCache, GlyphRasterBoundsFn, RasterizeGlyphFn};
|
||||
use galaxyui_core::{
|
||||
fonts::{self, SubpixelAlignment},
|
||||
rendering::{self, texture_cache::TextureCache},
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::mem;
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::Once;
|
||||
|
||||
use dispatch2::DispatchData;
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::ProtocolObject;
|
||||
use objc2_foundation::NSString;
|
||||
use objc2_metal::{
|
||||
MTLBlendFactor, MTLBlendOperation, MTLBuffer, MTLClearColor, MTLCommandBuffer,
|
||||
MTLCommandEncoder, MTLCommandQueue, MTLDevice, MTLDrawable, MTLFunction, MTLIndexType,
|
||||
MTLLibrary, MTLLoadAction, MTLOrigin, MTLPixelFormat, MTLPrimitiveType, MTLRegion,
|
||||
MTLRenderCommandEncoder, MTLRenderPassDescriptor, MTLRenderPipelineDescriptor,
|
||||
MTLRenderPipelineState, MTLResourceOptions, MTLScissorRect, MTLSize, MTLStoreAction,
|
||||
MTLTexture, MTLTextureDescriptor, MTLViewport,
|
||||
};
|
||||
use objc2_quartz_core::CAMetalDrawable;
|
||||
use pathfinder_color::{ColorF, ColorU};
|
||||
use pathfinder_geometry::rect::{RectF, RectI};
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use galaxyui_core::fonts::{self, canvas, RasterizedGlyph, SubpixelAlignment};
|
||||
use galaxyui_core::platform::CapturedFrame;
|
||||
use galaxyui_core::rendering::texture_cache::TextureCache;
|
||||
use galaxyui_core::rendering::{self};
|
||||
use galaxyui_core::scene::{CornerRadius, GlyphFade, GlyphKey, Icon, Image, Layer, Scene};
|
||||
|
||||
use super::frame_capture::capture_frame;
|
||||
use crate::platform::mac::rendering::renderer::Device;
|
||||
use crate::platform::mac::window::WindowState;
|
||||
use cocoa::base::id;
|
||||
use galaxyui_core::platform::CapturedFrame;
|
||||
use metal::{
|
||||
Function, MTLBlendFactor, MTLBlendOperation, MTLIndexType, MTLPrimitiveType,
|
||||
MTLResourceOptions, RenderPipelineDescriptor,
|
||||
};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
|
||||
use galaxyui_core::fonts::{canvas, RasterizedGlyph};
|
||||
use galaxyui_core::scene::{CornerRadius, GlyphFade, Icon, Image, Layer, Scene};
|
||||
use pathfinder_color::{ColorF, ColorU};
|
||||
use pathfinder_geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use galaxyui_core::scene::GlyphKey;
|
||||
use pathfinder_geometry::rect::RectI;
|
||||
use std::{fs::File, mem, sync::Once};
|
||||
use std::{io::Write, os::raw::c_void};
|
||||
use crate::rendering::atlas::{AllocatedRegion, TextureId};
|
||||
use crate::rendering::{get_best_dash_gap, GlyphCache, GlyphRasterBoundsFn, RasterizeGlyphFn};
|
||||
|
||||
const METAL_LIB_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
|
||||
static WRITE_LIB_TO_FILE: Once = Once::new();
|
||||
|
||||
/// A structure to help manage a single rendering pass.
|
||||
struct RenderPass<'a> {
|
||||
drawable: &'a metal::MetalDrawableRef,
|
||||
buffer: &'a metal::CommandBufferRef,
|
||||
encoder: &'a metal::RenderCommandEncoderRef,
|
||||
drawable: &'a ProtocolObject<dyn CAMetalDrawable>,
|
||||
buffer: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
|
||||
encoder: Retained<ProtocolObject<dyn MTLRenderCommandEncoder>>,
|
||||
encoding_finished: bool,
|
||||
}
|
||||
|
||||
impl<'a> RenderPass<'a> {
|
||||
fn new(
|
||||
command_queue: &'a mut metal::CommandQueue,
|
||||
drawable: &'a metal::MetalDrawableRef,
|
||||
command_queue: &ProtocolObject<dyn MTLCommandQueue>,
|
||||
drawable: &'a ProtocolObject<dyn CAMetalDrawable>,
|
||||
) -> Self {
|
||||
let buffer = command_queue.new_command_buffer();
|
||||
let encoder = buffer.new_render_command_encoder(Self::create_descriptor(drawable));
|
||||
let buffer = command_queue
|
||||
.commandBuffer()
|
||||
.expect("command queue should always vend a command buffer");
|
||||
let encoder = buffer
|
||||
.renderCommandEncoderWithDescriptor(&Self::create_descriptor(drawable))
|
||||
.expect("command buffer should always vend a render command encoder");
|
||||
Self {
|
||||
drawable,
|
||||
buffer,
|
||||
@@ -67,37 +74,52 @@ impl<'a> RenderPass<'a> {
|
||||
mut self,
|
||||
drawable_size: pathfinder_geometry::vector::Vector2F,
|
||||
should_capture: bool,
|
||||
presents_with_transaction: bool,
|
||||
) -> Option<CapturedFrame> {
|
||||
self.encoder.end_encoding();
|
||||
|
||||
self.encoder.endEncoding();
|
||||
self.encoding_finished = true;
|
||||
|
||||
// If we're able to do asynchronous presentation, do so - it allows us to avoid
|
||||
// blocking on the GPU for the duration of the frame.
|
||||
if !should_capture && !presents_with_transaction {
|
||||
self.buffer
|
||||
.presentDrawable(ProtocolObject::from_ref(self.drawable));
|
||||
self.buffer.commit();
|
||||
return None;
|
||||
}
|
||||
|
||||
// Otherwise, commit the buffer and wait for it to complete before continuing.
|
||||
self.buffer.commit();
|
||||
self.buffer.waitUntilCompleted();
|
||||
|
||||
self.buffer.wait_until_completed();
|
||||
|
||||
let captured = if should_capture {
|
||||
let capture = if should_capture {
|
||||
let texture = self.drawable.texture();
|
||||
capture_frame(texture, drawable_size)
|
||||
capture_frame(&texture, drawable_size)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.drawable.present();
|
||||
captured
|
||||
capture
|
||||
}
|
||||
|
||||
/// Creates a descriptor for a pass that renders into the provided drawable.
|
||||
fn create_descriptor(drawable: &metal::MetalDrawableRef) -> &metal::RenderPassDescriptorRef {
|
||||
let descriptor = metal::RenderPassDescriptor::new();
|
||||
fn create_descriptor(
|
||||
drawable: &ProtocolObject<dyn CAMetalDrawable>,
|
||||
) -> Retained<MTLRenderPassDescriptor> {
|
||||
let descriptor = MTLRenderPassDescriptor::new();
|
||||
|
||||
let color_attachment = descriptor.color_attachments().object_at(0).expect(
|
||||
"should always be able to get a color attachment for a CAMetalLayer's drawable",
|
||||
);
|
||||
color_attachment.set_texture(Some(drawable.texture()));
|
||||
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
|
||||
color_attachment.set_store_action(metal::MTLStoreAction::Store);
|
||||
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 0.));
|
||||
// SAFETY: index 0 is always a valid color attachment slot for a CAMetalLayer's drawable.
|
||||
let color_attachment = unsafe { descriptor.colorAttachments().objectAtIndexedSubscript(0) };
|
||||
color_attachment.setTexture(Some(&drawable.texture()));
|
||||
color_attachment.setLoadAction(MTLLoadAction::Clear);
|
||||
color_attachment.setStoreAction(MTLStoreAction::Store);
|
||||
color_attachment.setClearColor(MTLClearColor {
|
||||
red: 0.,
|
||||
green: 0.,
|
||||
blue: 0.,
|
||||
alpha: 0.,
|
||||
});
|
||||
|
||||
descriptor
|
||||
}
|
||||
@@ -108,33 +130,33 @@ impl Drop for RenderPass<'_> {
|
||||
// Make sure that `end_encoding()` is called, even if a panic occurs
|
||||
// during rendering.
|
||||
if !self.encoding_finished {
|
||||
self.encoder.end_encoding();
|
||||
self.encoder.endEncoding();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of resources necessary for rendering that retain state across frames.
|
||||
struct Resources {
|
||||
draw_rects_pipeline_state: metal::RenderPipelineState,
|
||||
draw_images_pipeline_state: metal::RenderPipelineState,
|
||||
draw_glyphs_pipeline_state: metal::RenderPipelineState,
|
||||
quad_vertices: metal::Buffer,
|
||||
quad_indices: metal::Buffer,
|
||||
glyph_cache: GlyphCache<metal::Texture>,
|
||||
texture_cache: TextureCache<metal::Texture>,
|
||||
draw_rects_pipeline_state: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
|
||||
draw_images_pipeline_state: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
|
||||
draw_glyphs_pipeline_state: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
|
||||
quad_vertices: Retained<ProtocolObject<dyn MTLBuffer>>,
|
||||
quad_indices: Retained<ProtocolObject<dyn MTLBuffer>>,
|
||||
glyph_cache: GlyphCache<Retained<ProtocolObject<dyn MTLTexture>>>,
|
||||
texture_cache: TextureCache<Retained<ProtocolObject<dyn MTLTexture>>>,
|
||||
}
|
||||
|
||||
/// A structure that manages rendering scenes using a particular hardware
|
||||
/// device.
|
||||
pub struct Renderer {
|
||||
resources: Resources,
|
||||
command_queue: metal::CommandQueue,
|
||||
command_queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn new(
|
||||
device: &metal::Device,
|
||||
color_pixel_format: metal::MTLPixelFormat,
|
||||
device: &ProtocolObject<dyn MTLDevice>,
|
||||
color_pixel_format: MTLPixelFormat,
|
||||
glyph_config: rendering::GlyphConfig,
|
||||
) -> Self {
|
||||
let library = if cfg!(feature = "enable-metal-frame-capture") {
|
||||
@@ -143,39 +165,61 @@ impl Renderer {
|
||||
let mut file = File::create(&temp_lib_path).unwrap();
|
||||
file.write_all(METAL_LIB_BYTES).unwrap();
|
||||
});
|
||||
device.new_library_with_file(temp_lib_path).unwrap()
|
||||
let path = NSString::from_str(temp_lib_path.to_str().unwrap());
|
||||
// `newLibraryWithURL:` is the non-deprecated replacement, but we
|
||||
// load the shader library from a file path here.
|
||||
#[allow(deprecated)]
|
||||
let library = device.newLibraryWithFile_error(&path).unwrap();
|
||||
library
|
||||
} else {
|
||||
device.new_library_with_data(METAL_LIB_BYTES).unwrap()
|
||||
let data = DispatchData::from_static_bytes(METAL_LIB_BYTES);
|
||||
device.newLibraryWithData_error(&data).unwrap()
|
||||
};
|
||||
|
||||
let rect_vertex_shader = library.get_function("rect_vertex_shader", None).unwrap();
|
||||
let rect_fragment_shader = library.get_function("rect_fragment_shader", None).unwrap();
|
||||
let rect_vertex_shader = library
|
||||
.newFunctionWithName(&NSString::from_str("rect_vertex_shader"))
|
||||
.unwrap();
|
||||
let rect_fragment_shader = library
|
||||
.newFunctionWithName(&NSString::from_str("rect_fragment_shader"))
|
||||
.unwrap();
|
||||
let rect_pipeline = Self::create_pipeline(
|
||||
"Rects",
|
||||
color_pixel_format,
|
||||
&rect_vertex_shader,
|
||||
&rect_fragment_shader,
|
||||
);
|
||||
let draw_rects_pipeline_state = device.new_render_pipeline_state(&rect_pipeline).unwrap();
|
||||
let draw_rects_pipeline_state = device
|
||||
.newRenderPipelineStateWithDescriptor_error(&rect_pipeline)
|
||||
.unwrap();
|
||||
|
||||
let image_fragment_shader = library.get_function("image_fragment_shader", None).unwrap();
|
||||
let image_fragment_shader = library
|
||||
.newFunctionWithName(&NSString::from_str("image_fragment_shader"))
|
||||
.unwrap();
|
||||
let image_pipeline = Self::create_pipeline(
|
||||
"Images",
|
||||
color_pixel_format,
|
||||
&rect_vertex_shader,
|
||||
&image_fragment_shader,
|
||||
);
|
||||
let draw_images_pipeline_state = device.new_render_pipeline_state(&image_pipeline).unwrap();
|
||||
let draw_images_pipeline_state = device
|
||||
.newRenderPipelineStateWithDescriptor_error(&image_pipeline)
|
||||
.unwrap();
|
||||
|
||||
let glyph_vertex_shader = library.get_function("glyph_vertex_shader", None).unwrap();
|
||||
let glyph_fragment_shader = library.get_function("glyph_fragment_shader", None).unwrap();
|
||||
let glyph_vertex_shader = library
|
||||
.newFunctionWithName(&NSString::from_str("glyph_vertex_shader"))
|
||||
.unwrap();
|
||||
let glyph_fragment_shader = library
|
||||
.newFunctionWithName(&NSString::from_str("glyph_fragment_shader"))
|
||||
.unwrap();
|
||||
let glyph_pipeline = Self::create_pipeline(
|
||||
"Glyphs",
|
||||
color_pixel_format,
|
||||
&glyph_vertex_shader,
|
||||
&glyph_fragment_shader,
|
||||
);
|
||||
let draw_glyphs_pipeline_state = device.new_render_pipeline_state(&glyph_pipeline).unwrap();
|
||||
let draw_glyphs_pipeline_state = device
|
||||
.newRenderPipelineStateWithDescriptor_error(&glyph_pipeline)
|
||||
.unwrap();
|
||||
|
||||
let quad_vertices = new_metal_buffer(
|
||||
device,
|
||||
@@ -206,30 +250,33 @@ impl Renderer {
|
||||
glyph_cache,
|
||||
texture_cache: TextureCache::new(),
|
||||
},
|
||||
command_queue: device.new_command_queue(),
|
||||
command_queue: device
|
||||
.newCommandQueue()
|
||||
.expect("device should always vend a command queue"),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_pipeline(
|
||||
label: &str,
|
||||
color_pixel_format: metal::MTLPixelFormat,
|
||||
vertex_shader: &Function,
|
||||
fragment_shader: &Function,
|
||||
) -> RenderPipelineDescriptor {
|
||||
let pipeline = metal::RenderPipelineDescriptor::new();
|
||||
pipeline.set_label(label);
|
||||
pipeline.set_vertex_function(Some(vertex_shader));
|
||||
pipeline.set_fragment_function(Some(fragment_shader));
|
||||
color_pixel_format: MTLPixelFormat,
|
||||
vertex_shader: &ProtocolObject<dyn MTLFunction>,
|
||||
fragment_shader: &ProtocolObject<dyn MTLFunction>,
|
||||
) -> Retained<MTLRenderPipelineDescriptor> {
|
||||
let pipeline = MTLRenderPipelineDescriptor::new();
|
||||
pipeline.setLabel(Some(&NSString::from_str(label)));
|
||||
pipeline.setVertexFunction(Some(vertex_shader));
|
||||
pipeline.setFragmentFunction(Some(fragment_shader));
|
||||
|
||||
let attachment = pipeline.color_attachments().object_at(0).unwrap();
|
||||
attachment.set_pixel_format(color_pixel_format);
|
||||
attachment.set_blending_enabled(true);
|
||||
attachment.set_rgb_blend_operation(MTLBlendOperation::Add);
|
||||
attachment.set_alpha_blend_operation(MTLBlendOperation::Add);
|
||||
attachment.set_source_rgb_blend_factor(MTLBlendFactor::SourceAlpha);
|
||||
attachment.set_source_alpha_blend_factor(MTLBlendFactor::One);
|
||||
attachment.set_destination_rgb_blend_factor(MTLBlendFactor::OneMinusSourceAlpha);
|
||||
attachment.set_destination_alpha_blend_factor(MTLBlendFactor::OneMinusSourceAlpha);
|
||||
// SAFETY: index 0 is always a valid color attachment slot for a render pipeline.
|
||||
let attachment = unsafe { pipeline.colorAttachments().objectAtIndexedSubscript(0) };
|
||||
attachment.setPixelFormat(color_pixel_format);
|
||||
attachment.setBlendingEnabled(true);
|
||||
attachment.setRgbBlendOperation(MTLBlendOperation::Add);
|
||||
attachment.setAlphaBlendOperation(MTLBlendOperation::Add);
|
||||
attachment.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
|
||||
attachment.setSourceAlphaBlendFactor(MTLBlendFactor::One);
|
||||
attachment.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
|
||||
attachment.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
|
||||
|
||||
pipeline
|
||||
}
|
||||
@@ -239,16 +286,21 @@ impl Renderer {
|
||||
scene: &Scene,
|
||||
ctx: &MetalDrawContext,
|
||||
should_capture: bool,
|
||||
presents_with_transaction: bool,
|
||||
) -> Option<CapturedFrame> {
|
||||
self.resources
|
||||
.glyph_cache
|
||||
.update_config(&scene.rendering_config().glyphs);
|
||||
|
||||
let render_pass = RenderPass::new(&mut self.command_queue, ctx.drawable);
|
||||
let render_pass = RenderPass::new(&self.command_queue, ctx.drawable);
|
||||
|
||||
Frame::new(scene, render_pass.encoder, &mut self.resources, ctx).draw();
|
||||
Frame::new(scene, &render_pass.encoder, &mut self.resources, ctx).draw();
|
||||
|
||||
render_pass.finish_with_capture(ctx.drawable_size, should_capture)
|
||||
render_pass.finish_with_capture(
|
||||
ctx.drawable_size,
|
||||
should_capture,
|
||||
presents_with_transaction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +309,7 @@ impl Renderer {
|
||||
/// image.
|
||||
pub struct Frame<'a> {
|
||||
scene: &'a Scene,
|
||||
command_encoder: &'a metal::RenderCommandEncoderRef,
|
||||
command_encoder: &'a ProtocolObject<dyn MTLRenderCommandEncoder>,
|
||||
resources: &'a mut Resources,
|
||||
ctx: &'a MetalDrawContext<'a>,
|
||||
}
|
||||
@@ -265,7 +317,7 @@ pub struct Frame<'a> {
|
||||
impl<'a> Frame<'a> {
|
||||
fn new(
|
||||
scene: &'a Scene,
|
||||
command_encoder: &'a metal::RenderCommandEncoderRef,
|
||||
command_encoder: &'a ProtocolObject<dyn MTLRenderCommandEncoder>,
|
||||
resources: &'a mut Resources,
|
||||
ctx: &'a MetalDrawContext<'a>,
|
||||
) -> Self {
|
||||
@@ -278,7 +330,7 @@ impl<'a> Frame<'a> {
|
||||
}
|
||||
|
||||
fn draw(&mut self) {
|
||||
self.command_encoder.set_viewport(metal::MTLViewport {
|
||||
self.command_encoder.setViewport(MTLViewport {
|
||||
originX: 0.0,
|
||||
originY: 0.0,
|
||||
width: self.ctx.drawable_size.x() as f64,
|
||||
@@ -296,26 +348,24 @@ impl<'a> Frame<'a> {
|
||||
let device_bounds = RectF::new(Vector2F::zero(), self.ctx.drawable_size);
|
||||
let bounds = (bounds * self.scene.scale_factor()).intersection(device_bounds);
|
||||
if let Some(intersection) = bounds {
|
||||
self.command_encoder
|
||||
.set_scissor_rect(metal::MTLScissorRect {
|
||||
x: intersection.origin_x().round() as u64,
|
||||
y: intersection.origin_y().round() as u64,
|
||||
width: intersection.width().round() as u64,
|
||||
height: intersection.height().round() as u64,
|
||||
});
|
||||
self.command_encoder.setScissorRect(MTLScissorRect {
|
||||
x: intersection.origin_x().round() as usize,
|
||||
y: intersection.origin_y().round() as usize,
|
||||
width: intersection.width().round() as usize,
|
||||
height: intersection.height().round() as usize,
|
||||
});
|
||||
} else {
|
||||
// The layer's clip bounds don't intersect the window bounds
|
||||
// at all; we can skip drawing anything in this layer.
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
self.command_encoder
|
||||
.set_scissor_rect(metal::MTLScissorRect {
|
||||
x: 0_u64,
|
||||
y: 0_u64,
|
||||
width: self.ctx.drawable_size.x() as u64,
|
||||
height: self.ctx.drawable_size.y() as u64,
|
||||
});
|
||||
self.command_encoder.setScissorRect(MTLScissorRect {
|
||||
x: 0_usize,
|
||||
y: 0_usize,
|
||||
width: self.ctx.drawable_size.x() as usize,
|
||||
height: self.ctx.drawable_size.y() as usize,
|
||||
});
|
||||
}
|
||||
self.draw_rects(layer);
|
||||
self.draw_images(layer);
|
||||
@@ -389,64 +439,83 @@ impl<'a> Frame<'a> {
|
||||
MTLResourceOptions::StorageModeManaged,
|
||||
);
|
||||
|
||||
self.command_encoder
|
||||
.set_vertex_buffer(1, Some(per_rect_uniforms_buffer.as_ref()), 0);
|
||||
|
||||
let uniforms = shader::Uniforms::new(self.ctx.drawable_size.into());
|
||||
self.command_encoder.set_vertex_bytes(
|
||||
2,
|
||||
mem::size_of::<shader::Uniforms>() as u64,
|
||||
[uniforms].as_ptr() as *const _,
|
||||
);
|
||||
self.command_encoder.set_fragment_bytes(
|
||||
0,
|
||||
mem::size_of::<shader::Uniforms>() as u64,
|
||||
[uniforms].as_ptr() as *const _,
|
||||
);
|
||||
let uniforms_ptr = NonNull::from(&uniforms).cast::<c_void>();
|
||||
let uniforms_len = mem::size_of::<shader::Uniforms>();
|
||||
|
||||
// SAFETY: the per-rect uniform buffer and `uniforms` value outlive this encoded draw
|
||||
// call, and the bound buffer/byte sizes and indices match the shader bindings.
|
||||
unsafe {
|
||||
self.command_encoder.setVertexBuffer_offset_atIndex(
|
||||
Some(&per_rect_uniforms_buffer),
|
||||
0,
|
||||
1,
|
||||
);
|
||||
self.command_encoder
|
||||
.setVertexBytes_length_atIndex(uniforms_ptr, uniforms_len, 2);
|
||||
self.command_encoder
|
||||
.setFragmentBytes_length_atIndex(uniforms_ptr, uniforms_len, 0);
|
||||
}
|
||||
|
||||
let (_, texture) = self
|
||||
.resources
|
||||
.texture_cache
|
||||
.get_or_insert_by_asset(asset, |asset| {
|
||||
let width = asset.size().x() as u64;
|
||||
let height = asset.size().y() as u64;
|
||||
let width = asset.size().x() as usize;
|
||||
let height = asset.size().y() as usize;
|
||||
|
||||
let texture_descriptor = metal::TextureDescriptor::new();
|
||||
texture_descriptor.set_pixel_format(metal::MTLPixelFormat::RGBA8Unorm);
|
||||
texture_descriptor.set_width(width);
|
||||
texture_descriptor.set_height(height);
|
||||
let texture = self.ctx.device.new_texture(&texture_descriptor);
|
||||
let region = metal::MTLRegion {
|
||||
origin: metal::MTLOrigin { x: 0, y: 0, z: 0 },
|
||||
size: metal::MTLSize {
|
||||
let texture_descriptor = MTLTextureDescriptor::new();
|
||||
texture_descriptor.setPixelFormat(MTLPixelFormat::RGBA8Unorm);
|
||||
// SAFETY: width/height come from a decoded asset and are within Metal limits.
|
||||
unsafe {
|
||||
texture_descriptor.setWidth(width);
|
||||
texture_descriptor.setHeight(height);
|
||||
}
|
||||
let texture = self
|
||||
.ctx
|
||||
.device
|
||||
.newTextureWithDescriptor(&texture_descriptor)
|
||||
.expect("device should create an RGBA8 texture");
|
||||
let region = MTLRegion {
|
||||
origin: MTLOrigin { x: 0, y: 0, z: 0 },
|
||||
size: MTLSize {
|
||||
width,
|
||||
height,
|
||||
depth: 1,
|
||||
},
|
||||
};
|
||||
|
||||
let bytes_per_row: u64 = 4 * width;
|
||||
texture.replace_region(
|
||||
region,
|
||||
0,
|
||||
asset.rgba_bytes().as_ptr() as *const c_void,
|
||||
bytes_per_row,
|
||||
);
|
||||
let bytes_per_row: usize = 4 * width;
|
||||
// SAFETY: rgba_bytes holds width*height*4 bytes laid out to match the region
|
||||
// and row stride.
|
||||
unsafe {
|
||||
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
|
||||
region,
|
||||
0,
|
||||
NonNull::new(asset.rgba_bytes().as_ptr() as *mut c_void)
|
||||
.expect("asset rgba bytes pointer is non-null"),
|
||||
bytes_per_row,
|
||||
);
|
||||
}
|
||||
|
||||
texture
|
||||
});
|
||||
|
||||
self.command_encoder
|
||||
.set_fragment_texture(0, Some(texture.as_ref()));
|
||||
// SAFETY: the bound texture and quad index buffer outlive this encoded draw call.
|
||||
unsafe {
|
||||
self.command_encoder
|
||||
.setFragmentTexture_atIndex(Some(&**texture), 0);
|
||||
|
||||
self.command_encoder.draw_indexed_primitives_instanced(
|
||||
MTLPrimitiveType::Triangle,
|
||||
6,
|
||||
MTLIndexType::UInt16,
|
||||
self.resources.quad_indices.as_ref(),
|
||||
0,
|
||||
per_rect_uniforms.len() as u64,
|
||||
);
|
||||
self.command_encoder
|
||||
.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset_instanceCount(
|
||||
MTLPrimitiveType::Triangle,
|
||||
6,
|
||||
MTLIndexType::UInt16,
|
||||
&self.resources.quad_indices,
|
||||
0,
|
||||
per_rect_uniforms.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_images(&mut self, layer: &Layer) {
|
||||
@@ -456,9 +525,15 @@ impl<'a> Frame<'a> {
|
||||
}
|
||||
|
||||
self.command_encoder
|
||||
.set_render_pipeline_state(&self.resources.draw_images_pipeline_state);
|
||||
self.command_encoder
|
||||
.set_vertex_buffer(0, Some(self.resources.quad_vertices.as_ref()), 0);
|
||||
.setRenderPipelineState(&self.resources.draw_images_pipeline_state);
|
||||
// SAFETY: index 0 binds the shared quad vertex buffer, which outlives the draw calls.
|
||||
unsafe {
|
||||
self.command_encoder.setVertexBuffer_offset_atIndex(
|
||||
Some(&self.resources.quad_vertices),
|
||||
0,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
for image in &layer.images {
|
||||
self.render_image_or_icon(Some(image), None);
|
||||
@@ -477,9 +552,15 @@ impl<'a> Frame<'a> {
|
||||
}
|
||||
|
||||
self.command_encoder
|
||||
.set_render_pipeline_state(&self.resources.draw_rects_pipeline_state);
|
||||
self.command_encoder
|
||||
.set_vertex_buffer(0, Some(self.resources.quad_vertices.as_ref()), 0);
|
||||
.setRenderPipelineState(&self.resources.draw_rects_pipeline_state);
|
||||
// SAFETY: index 0 binds the shared quad vertex buffer, which outlives the draw call.
|
||||
unsafe {
|
||||
self.command_encoder.setVertexBuffer_offset_atIndex(
|
||||
Some(&self.resources.quad_vertices),
|
||||
0,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
let mut per_rect_uniforms = Vec::new();
|
||||
for rect in &layer.rects {
|
||||
@@ -583,29 +664,33 @@ impl<'a> Frame<'a> {
|
||||
MTLResourceOptions::StorageModeManaged,
|
||||
);
|
||||
|
||||
self.command_encoder
|
||||
.set_vertex_buffer(1, Some(per_rect_uniforms_buffer.as_ref()), 0);
|
||||
|
||||
let uniforms = shader::Uniforms::new(self.ctx.drawable_size.into());
|
||||
self.command_encoder.set_vertex_bytes(
|
||||
2,
|
||||
mem::size_of::<shader::Uniforms>() as u64,
|
||||
[uniforms].as_ptr() as *const _,
|
||||
);
|
||||
self.command_encoder.set_fragment_bytes(
|
||||
0,
|
||||
mem::size_of::<shader::Uniforms>() as u64,
|
||||
[uniforms].as_ptr() as *const _,
|
||||
);
|
||||
let uniforms_ptr = NonNull::from(&uniforms).cast::<c_void>();
|
||||
let uniforms_len = mem::size_of::<shader::Uniforms>();
|
||||
|
||||
self.command_encoder.draw_indexed_primitives_instanced(
|
||||
MTLPrimitiveType::Triangle,
|
||||
6,
|
||||
MTLIndexType::UInt16,
|
||||
self.resources.quad_indices.as_ref(),
|
||||
0,
|
||||
per_rect_uniforms.len() as u64,
|
||||
);
|
||||
// SAFETY: the per-rect uniform buffer and `uniforms` value outlive this encoded draw
|
||||
// call, and the bound buffer/byte sizes and indices match the shader bindings.
|
||||
unsafe {
|
||||
self.command_encoder.setVertexBuffer_offset_atIndex(
|
||||
Some(&per_rect_uniforms_buffer),
|
||||
0,
|
||||
1,
|
||||
);
|
||||
self.command_encoder
|
||||
.setVertexBytes_length_atIndex(uniforms_ptr, uniforms_len, 2);
|
||||
self.command_encoder
|
||||
.setFragmentBytes_length_atIndex(uniforms_ptr, uniforms_len, 0);
|
||||
|
||||
self.command_encoder
|
||||
.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset_instanceCount(
|
||||
MTLPrimitiveType::Triangle,
|
||||
6,
|
||||
MTLIndexType::UInt16,
|
||||
&self.resources.quad_indices,
|
||||
0,
|
||||
per_rect_uniforms.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_glyphs(&mut self, layer: &Layer) {
|
||||
@@ -615,9 +700,15 @@ impl<'a> Frame<'a> {
|
||||
}
|
||||
|
||||
self.command_encoder
|
||||
.set_render_pipeline_state(&self.resources.draw_glyphs_pipeline_state);
|
||||
self.command_encoder
|
||||
.set_vertex_buffer(0, Some(self.resources.quad_vertices.as_ref()), 0);
|
||||
.setRenderPipelineState(&self.resources.draw_glyphs_pipeline_state);
|
||||
// SAFETY: index 0 binds the shared quad vertex buffer, which outlives the draw calls.
|
||||
unsafe {
|
||||
self.command_encoder.setVertexBuffer_offset_atIndex(
|
||||
Some(&self.resources.quad_vertices),
|
||||
0,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
let scale_factor = self.scene.scale_factor();
|
||||
|
||||
@@ -703,15 +794,9 @@ impl<'a> Frame<'a> {
|
||||
MTLResourceOptions::StorageModeManaged,
|
||||
);
|
||||
|
||||
self.command_encoder
|
||||
.set_vertex_buffer(1, Some(per_glyph_uniforms_buffer.as_ref()), 0);
|
||||
|
||||
let uniforms = shader::Uniforms::new(self.ctx.drawable_size.into());
|
||||
self.command_encoder.set_vertex_bytes(
|
||||
2,
|
||||
mem::size_of::<shader::Uniforms>() as u64,
|
||||
[uniforms].as_ptr() as *const _,
|
||||
);
|
||||
let uniforms_ptr = NonNull::from(&uniforms).cast::<c_void>();
|
||||
let uniforms_len = mem::size_of::<shader::Uniforms>();
|
||||
|
||||
let texture = self
|
||||
.resources
|
||||
@@ -719,16 +804,29 @@ impl<'a> Frame<'a> {
|
||||
.texture(&texture_id)
|
||||
.expect("texture ID should be in atlas");
|
||||
|
||||
self.command_encoder.set_fragment_texture(0, Some(texture));
|
||||
|
||||
self.command_encoder.draw_indexed_primitives_instanced(
|
||||
MTLPrimitiveType::Triangle,
|
||||
6,
|
||||
MTLIndexType::UInt16,
|
||||
self.resources.quad_indices.as_ref(),
|
||||
0,
|
||||
per_glyph_uniforms.len() as u64,
|
||||
);
|
||||
// SAFETY: the per-glyph uniform buffer, `uniforms` value, bound texture, and quad
|
||||
// index buffer outlive this encoded draw call, and the bound sizes/indices match the
|
||||
// shader bindings.
|
||||
unsafe {
|
||||
self.command_encoder.setVertexBuffer_offset_atIndex(
|
||||
Some(&per_glyph_uniforms_buffer),
|
||||
0,
|
||||
1,
|
||||
);
|
||||
self.command_encoder
|
||||
.setVertexBytes_length_atIndex(uniforms_ptr, uniforms_len, 2);
|
||||
self.command_encoder
|
||||
.setFragmentTexture_atIndex(Some(&**texture), 0);
|
||||
self.command_encoder
|
||||
.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset_instanceCount(
|
||||
MTLPrimitiveType::Triangle,
|
||||
6,
|
||||
MTLIndexType::UInt16,
|
||||
&self.resources.quad_indices,
|
||||
0,
|
||||
per_glyph_uniforms.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -740,15 +838,20 @@ impl Drop for Frame<'_> {
|
||||
}
|
||||
|
||||
fn new_metal_buffer<T>(
|
||||
device: &metal::Device,
|
||||
device: &ProtocolObject<dyn MTLDevice>,
|
||||
data: &[T],
|
||||
options: MTLResourceOptions,
|
||||
) -> metal::Buffer {
|
||||
device.new_buffer_with_data(
|
||||
data.as_ptr() as *const c_void,
|
||||
std::mem::size_of_val(data) as u64,
|
||||
options,
|
||||
)
|
||||
) -> Retained<ProtocolObject<dyn MTLBuffer>> {
|
||||
// SAFETY: `data` points to `size_of_val(data)` initialized bytes; Metal copies them into the
|
||||
// new buffer, so the pointer only needs to be valid for the duration of this call.
|
||||
unsafe {
|
||||
device.newBufferWithBytes_length_options(
|
||||
NonNull::new(data.as_ptr() as *mut c_void).expect("buffer data pointer is non-null"),
|
||||
std::mem::size_of_val(data),
|
||||
options,
|
||||
)
|
||||
}
|
||||
.expect("device should create a buffer")
|
||||
}
|
||||
|
||||
mod shader {
|
||||
@@ -917,8 +1020,8 @@ mod shader {
|
||||
}
|
||||
|
||||
pub(super) struct MetalDrawContext<'a> {
|
||||
pub(super) device: &'a metal::Device,
|
||||
pub(super) drawable: &'a metal::MetalDrawableRef,
|
||||
pub(super) device: &'a ProtocolObject<dyn MTLDevice>,
|
||||
pub(super) drawable: &'a ProtocolObject<dyn CAMetalDrawable>,
|
||||
pub(super) drawable_size: Vector2F,
|
||||
rasterize_glyph_fn: &'a RasterizeGlyphFn<'a>,
|
||||
glyph_raster_bounds_fn: &'a GlyphRasterBoundsFn<'a>,
|
||||
@@ -958,17 +1061,17 @@ impl super::super::Renderer for Renderer {
|
||||
log::error!("Metal renderer called with non-metal device");
|
||||
return;
|
||||
};
|
||||
let metal_device: &ProtocolObject<dyn MTLDevice> = metal_device;
|
||||
|
||||
let drawable = unsafe {
|
||||
let native_view = window.native_view();
|
||||
let layer: id = msg_send![native_view, layer];
|
||||
let drawable: &metal::MetalDrawableRef = msg_send![layer, nextDrawable];
|
||||
drawable
|
||||
};
|
||||
let metal_layer = window.metal_layer();
|
||||
let presents_with_transaction = metal_layer.presentsWithTransaction();
|
||||
let drawable = metal_layer
|
||||
.nextDrawable()
|
||||
.expect("CAMetalLayer with allowsNextDrawableTimeout disabled always vends a drawable");
|
||||
|
||||
let ctx = &MetalDrawContext {
|
||||
device: metal_device,
|
||||
drawable,
|
||||
drawable: &drawable,
|
||||
drawable_size: window.physical_size(),
|
||||
rasterize_glyph_fn: &|glyph_key, scale, subpixel_alignment, glyph_config, format| {
|
||||
font_cache.rasterized_glyph(
|
||||
@@ -986,7 +1089,7 @@ impl super::super::Renderer for Renderer {
|
||||
|
||||
let capture_callback = window.capture_callback.borrow_mut().take();
|
||||
let should_capture = capture_callback.is_some();
|
||||
let captured = Self::render(self, scene, ctx, should_capture);
|
||||
let captured = Self::render(self, scene, ctx, should_capture, presents_with_transaction);
|
||||
if let (Some(frame), Some(callback)) = (captured, capture_callback) {
|
||||
callback(frame);
|
||||
}
|
||||
@@ -1002,35 +1105,48 @@ impl super::super::Renderer for Renderer {
|
||||
fn insert_glyph_into_texture(
|
||||
region: AllocatedRegion,
|
||||
glyph: &RasterizedGlyph,
|
||||
texture: &mut metal::Texture,
|
||||
texture: &mut Retained<ProtocolObject<dyn MTLTexture>>,
|
||||
) {
|
||||
let region = metal::MTLRegion {
|
||||
origin: metal::MTLOrigin {
|
||||
x: region.pixel_region.origin_x() as u64,
|
||||
y: region.pixel_region.origin_y() as u64,
|
||||
let region = MTLRegion {
|
||||
origin: MTLOrigin {
|
||||
x: region.pixel_region.origin_x() as usize,
|
||||
y: region.pixel_region.origin_y() as usize,
|
||||
z: 0,
|
||||
},
|
||||
size: metal::MTLSize {
|
||||
width: region.pixel_region.width() as u64,
|
||||
height: region.pixel_region.height() as u64,
|
||||
size: MTLSize {
|
||||
width: region.pixel_region.width() as usize,
|
||||
height: region.pixel_region.height() as usize,
|
||||
depth: 1,
|
||||
},
|
||||
};
|
||||
|
||||
let bytes_per_row: u64 = 4 * (glyph.canvas.size.x() as u64);
|
||||
texture.replace_region(
|
||||
region,
|
||||
0,
|
||||
glyph.canvas.pixels.as_slice().as_ptr() as *const c_void,
|
||||
bytes_per_row,
|
||||
);
|
||||
let bytes_per_row: usize = 4 * (glyph.canvas.size.x() as usize);
|
||||
// SAFETY: the glyph canvas holds at least `bytes_per_row * region.height` bytes laid out to
|
||||
// match the destination region.
|
||||
unsafe {
|
||||
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
|
||||
region,
|
||||
0,
|
||||
NonNull::new(glyph.canvas.pixels.as_slice().as_ptr() as *mut c_void)
|
||||
.expect("glyph canvas pixel pointer is non-null"),
|
||||
bytes_per_row,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new texture atlas for use in the cache.
|
||||
fn create_new_texture_atlas(atlas_size: usize, device: &metal::Device) -> metal::Texture {
|
||||
let texture_descriptor = metal::TextureDescriptor::new();
|
||||
texture_descriptor.set_pixel_format(metal::MTLPixelFormat::RGBA8Unorm);
|
||||
texture_descriptor.set_width(atlas_size as u64);
|
||||
texture_descriptor.set_height(atlas_size as u64);
|
||||
device.new_texture(&texture_descriptor)
|
||||
fn create_new_texture_atlas(
|
||||
atlas_size: usize,
|
||||
device: &ProtocolObject<dyn MTLDevice>,
|
||||
) -> Retained<ProtocolObject<dyn MTLTexture>> {
|
||||
let texture_descriptor = MTLTextureDescriptor::new();
|
||||
texture_descriptor.setPixelFormat(MTLPixelFormat::RGBA8Unorm);
|
||||
// SAFETY: `atlas_size` is a fixed, valid texture dimension within Metal limits.
|
||||
unsafe {
|
||||
texture_descriptor.setWidth(atlas_size);
|
||||
texture_descriptor.setHeight(atlas_size);
|
||||
}
|
||||
device
|
||||
.newTextureWithDescriptor(&texture_descriptor)
|
||||
.expect("device should create an atlas texture")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use crate::platform::mac::rendering::metal::renderer::Renderer;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use objc2::runtime::ProtocolObject;
|
||||
use objc2_metal::{MTLDevice, MTLPixelFormat};
|
||||
use galaxyui_core::rendering;
|
||||
|
||||
use crate::platform::mac::rendering::metal::renderer::Renderer;
|
||||
|
||||
pub struct RendererManager {
|
||||
/// Maps a device's registry ID to its renderer (collection of state related
|
||||
/// to rendering on a particular device).
|
||||
@@ -16,13 +19,13 @@ impl RendererManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn renderer_for_device(&mut self, device: &metal::Device) -> &mut Renderer {
|
||||
pub fn renderer_for_device(&mut self, device: &ProtocolObject<dyn MTLDevice>) -> &mut Renderer {
|
||||
use std::collections::hash_map::Entry::*;
|
||||
match self.renderers.entry(device.registry_id()) {
|
||||
match self.renderers.entry(device.registryID()) {
|
||||
Occupied(entry) => entry.into_mut(),
|
||||
Vacant(entry) => entry.insert(Renderer::new(
|
||||
device,
|
||||
metal::MTLPixelFormat::BGRA8Unorm,
|
||||
MTLPixelFormat::BGRA8Unorm,
|
||||
rendering::GlyphConfig::default(),
|
||||
)),
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ mod renderer_manager;
|
||||
#[cfg(wgpu)]
|
||||
mod wgpu;
|
||||
|
||||
pub use self::metal::is_integrated_gpu;
|
||||
pub use renderer::{Device, Renderer};
|
||||
pub use renderer::{Device, MetalDevice, Renderer};
|
||||
pub use renderer_manager::RendererManager;
|
||||
|
||||
pub use self::metal::is_integrated_gpu;
|
||||
|
||||
/// Returns `true` if a low power GPU is available for rendering. Typically, this is true for
|
||||
/// machines with two GPUs -- a dedicated discrete high-performance GPU and a lower power
|
||||
/// integrated GPU.
|
||||
@@ -17,12 +18,10 @@ pub fn is_low_power_gpu_available() -> bool {
|
||||
if #[cfg(wgpu)] {
|
||||
crate::r#async::block_on(crate::rendering::wgpu::is_low_power_gpu_available())
|
||||
} else {
|
||||
let devices = ::metal::Device::all();
|
||||
let gpu_count = devices.len();
|
||||
let devices = objc2_metal::MTLCopyAllDevices();
|
||||
let gpu_count = devices.count();
|
||||
gpu_count > 1
|
||||
&& devices
|
||||
.iter()
|
||||
.any(metal::is_integrated_gpu)
|
||||
&& (0..gpu_count).any(|i| metal::is_integrated_gpu(&devices.objectAtIndex(i)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
use crate::platform::mac::rendering::is_integrated_gpu;
|
||||
use crate::platform::mac::window::WindowState;
|
||||
use cocoa::base::id;
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::ProtocolObject;
|
||||
use objc2_app_kit::{NSView, NSWindow};
|
||||
use objc2_metal::MTLDevice;
|
||||
use galaxyui_core::rendering::{
|
||||
GPUBackend, GPUDeviceInfo, GPUDeviceType, GPUPowerPreference, OnGPUDeviceSelected,
|
||||
};
|
||||
use galaxyui_core::{fonts, Scene};
|
||||
|
||||
use crate::platform::mac::rendering::is_integrated_gpu;
|
||||
use crate::platform::mac::window::WindowState;
|
||||
|
||||
/// An owned handle to a Metal device, used to render with the Metal backend.
|
||||
///
|
||||
/// This is the objc2-metal equivalent of the legacy `metal::Device`, and is the
|
||||
/// type the window layer creates (via `MTLCreateSystemDefaultDevice` /
|
||||
/// `MTLCopyAllDevices`) and hands to [`Device::new`].
|
||||
pub type MetalDevice = Retained<ProtocolObject<dyn MTLDevice>>;
|
||||
|
||||
/// Trait to render the [`Scene`] onto the screen using the provided [`WindowState`].
|
||||
pub trait Renderer {
|
||||
fn render(&mut self, scene: &Scene, window: &WindowState, font_cache: &fonts::Cache);
|
||||
@@ -17,15 +28,15 @@ pub trait Renderer {
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum Device {
|
||||
#[allow(dead_code)]
|
||||
Metal(metal::Device),
|
||||
Metal(MetalDevice),
|
||||
#[cfg(wgpu)]
|
||||
WGPU(Box<crate::rendering::wgpu::Resources>),
|
||||
}
|
||||
impl Device {
|
||||
pub fn new(
|
||||
_metal_device: metal::Device,
|
||||
_native_view: id,
|
||||
_native_window: id,
|
||||
_metal_device: MetalDevice,
|
||||
_native_view: &NSView,
|
||||
_native_window: &NSWindow,
|
||||
_gpu_power_preference: GPUPowerPreference,
|
||||
on_gpu_device_info: Box<OnGPUDeviceSelected>,
|
||||
) -> Self {
|
||||
@@ -45,7 +56,7 @@ impl Device {
|
||||
}
|
||||
|
||||
#[cfg_attr(wgpu, allow(dead_code))]
|
||||
fn get_gpu_device_info(device: &metal::Device) -> GPUDeviceInfo {
|
||||
fn get_gpu_device_info(device: &ProtocolObject<dyn MTLDevice>) -> GPUDeviceInfo {
|
||||
let device_type = if is_integrated_gpu(device) {
|
||||
GPUDeviceType::IntegratedGpu
|
||||
} else {
|
||||
@@ -53,7 +64,7 @@ fn get_gpu_device_info(device: &metal::Device) -> GPUDeviceInfo {
|
||||
};
|
||||
GPUDeviceInfo {
|
||||
device_type,
|
||||
device_name: device.name().into(),
|
||||
device_name: device.name().to_string(),
|
||||
// Mimic wgpu by setting the driver name and info to empty strings when
|
||||
// rendering on Metal. See https://github.com/gfx-rs/wgpu/blob/8129897ccbff869ef48a3b53a4cdd8a8a21840f9/wgpu-hal/src/metal/mod.rs#L135.
|
||||
driver_name: String::new(),
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
|
||||
use super::{
|
||||
metal,
|
||||
renderer::{Device, Renderer},
|
||||
};
|
||||
use super::metal;
|
||||
use super::renderer::{Device, Renderer};
|
||||
|
||||
pub struct RendererManager {
|
||||
metal_renderer_manager: metal::RendererManager,
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
mod renderer;
|
||||
mod renderer_manager;
|
||||
|
||||
use crate::rendering::wgpu::Resources;
|
||||
use crate::{platform::mac::rendering::Device, rendering::GPUPowerPreference};
|
||||
use anyhow::{anyhow, Result};
|
||||
pub use renderer_manager::RendererManager;
|
||||
|
||||
use crate::rendering::OnGPUDeviceSelected;
|
||||
use cocoa::{appkit::NSView, base::id};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use anyhow::Result;
|
||||
use objc2_app_kit::NSView;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
pub use renderer_manager::RendererManager;
|
||||
use wgpu::rwh::{
|
||||
AppKitDisplayHandle, AppKitWindowHandle, DisplayHandle, HandleError, HasDisplayHandle,
|
||||
HasWindowHandle, RawDisplayHandle, RawWindowHandle, WindowHandle,
|
||||
};
|
||||
|
||||
use crate::platform::mac::rendering::Device;
|
||||
use crate::rendering::wgpu::Resources;
|
||||
use crate::rendering::{GPUPowerPreference, OnGPUDeviceSelected};
|
||||
|
||||
impl Device {
|
||||
/// Constructs a new [`Device`] to render using WGPU.
|
||||
pub fn new_wgpu(
|
||||
native_view: id,
|
||||
native_view: &NSView,
|
||||
gpu_power_preference: GPUPowerPreference,
|
||||
on_gpu_device_info: Box<OnGPUDeviceSelected>,
|
||||
) -> Result<Device> {
|
||||
let view_frame = unsafe { NSView::frame(native_view) };
|
||||
let view_frame = native_view.frame();
|
||||
let surface_size = vec2f(view_frame.size.width as f32, view_frame.size.height as f32);
|
||||
|
||||
let appkit_window_handle = AppKitWindowHandle::new(
|
||||
NonNull::new(native_view)
|
||||
.ok_or_else(|| anyhow!("Received null NSView pointer"))?
|
||||
.cast(),
|
||||
);
|
||||
let appkit_window_handle = AppKitWindowHandle::new(NonNull::from(native_view).cast());
|
||||
let window_handle =
|
||||
unsafe { WindowHandle::borrow_raw(RawWindowHandle::AppKit(appkit_window_handle)) };
|
||||
let display_handle = unsafe {
|
||||
@@ -61,7 +58,7 @@ impl Device {
|
||||
/// guaranteed that the underlying window won't become invalid while the `WindowHandle` is alive.
|
||||
/// In the case of Warp this _should_ be safe because we ultimately deallocate the native window
|
||||
/// when [`crate::platform::mac::Window`] is deallocated (once a `Window` is deallocated, there
|
||||
/// are no pointers to the native window anymore, which cause it to to be deallocated via the
|
||||
/// are no pointers to the native window anymore, which cause it to be deallocated via the
|
||||
/// `warp_dealloc_window` callback).
|
||||
/// See <https://github.com/rust-windowing/raw-window-handle/pull/73> for more information on the
|
||||
/// safety requirements of implementing the [`HasRawWindowHandle`] trait.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::rendering::wgpu::{Renderer, Resources};
|
||||
use crate::rendering::GlyphConfig;
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use wgpu::Device;
|
||||
|
||||
use crate::rendering::wgpu::{Renderer, Resources};
|
||||
use crate::rendering::GlyphConfig;
|
||||
|
||||
pub struct RendererManager {
|
||||
renderers: HashMap<DeviceID, Renderer>,
|
||||
}
|
||||
|
||||
@@ -1,40 +1,3 @@
|
||||
use block::{Block, ConcreteBlock};
|
||||
use core_foundation::array::CFArray;
|
||||
use core_foundation::attributed_string::CFMutableAttributedStringRef;
|
||||
use core_foundation::base::CFType;
|
||||
use core_foundation::boolean::CFBoolean;
|
||||
use core_foundation::dictionary::CFDictionary;
|
||||
use core_foundation::mach_port::CFIndex;
|
||||
use core_foundation::number::CFNumber;
|
||||
use core_foundation::{
|
||||
attributed_string::CFMutableAttributedString,
|
||||
base::CFTypeID,
|
||||
base::{CFRange, TCFType},
|
||||
declare_TCFType, impl_TCFType,
|
||||
string::CFString,
|
||||
};
|
||||
use core_graphics::base::CGFloat;
|
||||
use core_graphics::color::CGColor;
|
||||
use core_graphics::display::{CGPoint, CGRect, CGSize};
|
||||
use core_graphics::path::CGPath;
|
||||
use core_text::framesetter::CTFramesetter;
|
||||
use core_text::line::CTLineRef;
|
||||
use core_text::run::{CTRun, CTRunRef};
|
||||
use core_text::string_attributes::kCTKernAttributeName;
|
||||
use core_text::{
|
||||
font::CTFont,
|
||||
line::CTLine,
|
||||
string_attributes::{kCTFontAttributeName, kCTParagraphStyleAttributeName},
|
||||
};
|
||||
use galaxyui_core::fonts::GlyphId;
|
||||
use galaxyui_core::platform::LineStyle;
|
||||
use galaxyui_core::text_layout::{
|
||||
CaretPosition, ClipConfig, Glyph, Line, Run, StyleAndFont, TextAlignment, TextBorder,
|
||||
TextFrame, TextStyle,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::c_void;
|
||||
@@ -42,8 +5,40 @@ use std::marker::PhantomData;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use std::slice;
|
||||
|
||||
use block::{Block, ConcreteBlock};
|
||||
use core_foundation::array::CFArray;
|
||||
use core_foundation::attributed_string::{CFMutableAttributedString, CFMutableAttributedStringRef};
|
||||
use core_foundation::base::{CFRange, CFType, CFTypeID, TCFType};
|
||||
use core_foundation::boolean::CFBoolean;
|
||||
use core_foundation::dictionary::CFDictionary;
|
||||
use core_foundation::mach_port::CFIndex;
|
||||
use core_foundation::number::CFNumber;
|
||||
use core_foundation::string::CFString;
|
||||
use core_foundation::{declare_TCFType, impl_TCFType};
|
||||
use core_graphics::base::CGFloat;
|
||||
use core_graphics::color::CGColor;
|
||||
use core_graphics::display::{CGPoint, CGRect, CGSize};
|
||||
use core_graphics::path::CGPath;
|
||||
use core_text::font::CTFont;
|
||||
use core_text::framesetter::CTFramesetter;
|
||||
use core_text::line::{CTLine, CTLineRef};
|
||||
use core_text::run::{CTRun, CTRunRef};
|
||||
use core_text::string_attributes::{
|
||||
kCTFontAttributeName, kCTKernAttributeName, kCTParagraphStyleAttributeName,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use vec1::Vec1;
|
||||
|
||||
use galaxyui_core::fonts::GlyphId;
|
||||
use galaxyui_core::platform::{CapturedFrame, LineStyle};
|
||||
use galaxyui_core::text_layout::{
|
||||
CaretPosition, ClipConfig, Glyph, Line, Run, StyleAndFont, TextAlignment, TextBorder,
|
||||
TextFrame, TextStyle,
|
||||
};
|
||||
|
||||
use super::fonts::FontDB;
|
||||
use super::utils::{cg_color_to_color_u, color_u_to_cg_color};
|
||||
|
||||
@@ -1023,5 +1018,5 @@ fn advances(run: &CTRun) -> Cow<'_, [CGSize]> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "text_layout_test.rs"]
|
||||
#[path = "text_layout_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use super::*;
|
||||
use crate::fonts::Properties;
|
||||
|
||||
use crate::fonts::{collect_glyph_indices, collect_line_caret_position_starts, init_fonts};
|
||||
use crate::platform::FontDB as _;
|
||||
use crate::text_layout::DEFAULT_TOP_BOTTOM_RATIO;
|
||||
|
||||
use anyhow::Result;
|
||||
use rand::random;
|
||||
|
||||
use super::*;
|
||||
use crate::fonts::{
|
||||
collect_glyph_indices, collect_line_caret_position_starts, init_fonts, Properties,
|
||||
};
|
||||
use crate::platform::FontDB as _;
|
||||
use crate::text_layout::DEFAULT_TOP_BOTTOM_RATIO;
|
||||
|
||||
pub(crate) fn collect_line_caret_position_pairs(line: &Line) -> Vec<(usize, usize)> {
|
||||
line.caret_positions
|
||||
.iter()
|
||||
@@ -52,7 +52,7 @@ fn test_char_indices_ligatures() -> Result<()> {
|
||||
// characters get combined to become a single glyph. At a high level, what this is testing
|
||||
// is that after laying out the string, we see some characters get combined into a single
|
||||
// glyph. For example, the text "Zapfino" gets combined into a single glyph, which is why
|
||||
// there is a jump from 23 to 30 in in the list of glyph indices below.
|
||||
// there is a jump from 23 to 30 in the list of glyph indices below.
|
||||
// See https://docs.google.com/drawings/d/18qOKhzA5rWaMuxKVeWFDXh7ebrDjxongarAckkm0qnE/edit
|
||||
// for a full diagram of what's happening here.
|
||||
assert_eq!(
|
||||
|
||||
@@ -0,0 +1,652 @@
|
||||
use anyhow::Result;
|
||||
use rand::random;
|
||||
|
||||
use super::*;
|
||||
use crate::fonts::{
|
||||
collect_glyph_indices, collect_line_caret_position_starts, init_fonts, Properties,
|
||||
};
|
||||
use crate::platform::FontDB as _;
|
||||
use crate::text_layout::DEFAULT_TOP_BOTTOM_RATIO;
|
||||
|
||||
pub(crate) fn collect_line_caret_position_pairs(line: &Line) -> Vec<(usize, usize)> {
|
||||
line.caret_positions
|
||||
.iter()
|
||||
.map(|pos| (pos.start_offset, pos.last_offset))
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_char_indices_ligatures() -> Result<()> {
|
||||
let mut font_db = FontDB::new();
|
||||
let zapfino = font_db.load_from_system("Zapfino")?;
|
||||
let menlo = font_db.load_from_system("Menlo")?;
|
||||
|
||||
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
||||
let line = layout_line(
|
||||
text,
|
||||
LineStyle {
|
||||
font_size: 16.0,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: None,
|
||||
},
|
||||
&[
|
||||
(
|
||||
0..9,
|
||||
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
9..22,
|
||||
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
22..text.encode_utf16().count(),
|
||||
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
],
|
||||
&font_db,
|
||||
ClipConfig::default(),
|
||||
);
|
||||
|
||||
// It's easiest to understand what's happening here by visualizing the text and seeing which
|
||||
// characters get combined to become a single glyph. At a high level, what this is testing
|
||||
// is that after laying out the string, we see some characters get combined into a single
|
||||
// glyph. For example, the text "Zapfino" gets combined into a single glyph, which is why
|
||||
// there is a jump from 23 to 30 in the list of glyph indices below.
|
||||
// See https://docs.google.com/drawings/d/18qOKhzA5rWaMuxKVeWFDXh7ebrDjxongarAckkm0qnE/edit
|
||||
// for a full diagram of what's happening here.
|
||||
assert_eq!(
|
||||
line.runs
|
||||
.iter()
|
||||
.flat_map(|r| r.glyphs.iter())
|
||||
.map(|g| g.index)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![0, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 30, 31]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_caret_positions_ligatures() -> Result<()> {
|
||||
// There's some overlap between caret positions and the character indices we
|
||||
// store in glyphs. However, a single glyph may have multiple caret positions
|
||||
// because characters/graphemes may get combined into a single glyph.
|
||||
|
||||
let mut font_db = FontDB::new();
|
||||
let zapfino = font_db.load_from_system("Zapfino")?;
|
||||
let menlo = font_db.load_from_system("Menlo")?;
|
||||
|
||||
// This string has 32 characters, but 35 UTF-16 code points and 41 UTF-8 code points.
|
||||
// Each '𐍈' character encodes as 2 UTF-16 code points or 4 UTF-8 code points.
|
||||
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
||||
|
||||
let line = layout_line(
|
||||
text,
|
||||
LineStyle {
|
||||
font_size: 16.0,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: None,
|
||||
},
|
||||
&[
|
||||
(
|
||||
0..9,
|
||||
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
9..22,
|
||||
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
22..35,
|
||||
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
],
|
||||
&font_db,
|
||||
ClipConfig::default(),
|
||||
);
|
||||
|
||||
// There are only 23 glyphs because 'Zapfino', 'Th', and 'is' each have ligatures.
|
||||
assert_eq!(
|
||||
line.runs.iter().map(|run| run.glyphs.len()).sum::<usize>(),
|
||||
23
|
||||
);
|
||||
|
||||
// There should be a caret position for each character.
|
||||
assert_eq!(
|
||||
line.caret_positions
|
||||
.iter()
|
||||
.map(|pos| pos.start_offset)
|
||||
.collect::<Vec<_>>(),
|
||||
(0..32).collect::<Vec<usize>>()
|
||||
);
|
||||
|
||||
// There is a caret for the 3rd character at the 3rd position, even though
|
||||
// the first 2 characters are represented with 1 glyph.
|
||||
assert_eq!(
|
||||
line.caret_position_for_index(3),
|
||||
line.caret_positions[3].position_in_line
|
||||
);
|
||||
|
||||
// Likewise for the second 𐍈, even though it (and the previous one) take
|
||||
// multiple code points.
|
||||
assert_eq!(
|
||||
line.caret_position_for_index(15),
|
||||
line.caret_positions[15].position_in_line
|
||||
);
|
||||
|
||||
// This tests hit-testing on a regular character.
|
||||
assert_eq!(line.caret_index_for_x(0.), Some(0));
|
||||
assert_eq!(line.caret_index_for_x(20.), Some(1));
|
||||
|
||||
// This tests hit-testing within a ligature.
|
||||
assert_eq!(line.caret_index_for_x(260.), Some(25));
|
||||
|
||||
// This tests rounding up to the next character.
|
||||
assert_eq!(line.caret_index_for_x(268.), Some(26));
|
||||
|
||||
// This tests a few random positions within the bound and before the last character.
|
||||
let last_caret_pos = line
|
||||
.caret_positions
|
||||
.last()
|
||||
.map_or(0., |p| p.position_in_line);
|
||||
// The bounded and unbounded method should return the same result.
|
||||
for _ in 0..5 {
|
||||
let pos: f32 = random();
|
||||
let index = line.caret_index_for_x(pos * last_caret_pos);
|
||||
assert_eq!(
|
||||
index,
|
||||
Some(line.caret_index_for_x_unbounded(pos * last_caret_pos))
|
||||
);
|
||||
}
|
||||
|
||||
// This tests that the unbounded method returns the first index for out-of-bound position to the left
|
||||
assert_eq!(line.caret_index_for_x_unbounded(-1.), line.first_index());
|
||||
// The bounded method should return `None`
|
||||
assert_eq!(line.caret_index_for_x(-1.), None);
|
||||
|
||||
// This tests that the unbounded method returns the end index for out-of-bound position to the right
|
||||
assert_eq!(
|
||||
line.caret_index_for_x_unbounded(line.width + 0.1),
|
||||
line.end_index()
|
||||
);
|
||||
assert_eq!(line.caret_index_for_x(line.width + 0.1), None);
|
||||
|
||||
// This tests that the unbounded method returns the correct index either before or after the last glyph
|
||||
assert_eq!(
|
||||
line.caret_index_for_x_unbounded(0.9 * last_caret_pos + 0.1 * line.width),
|
||||
line.last_index()
|
||||
);
|
||||
assert_eq!(
|
||||
line.caret_index_for_x_unbounded(0.1 * last_caret_pos + 0.9 * line.width),
|
||||
line.end_index()
|
||||
);
|
||||
// The bounded method should always just return the last index
|
||||
assert_eq!(
|
||||
line.caret_index_for_x(0.9 * last_caret_pos + 0.1 * line.width),
|
||||
Some(line.last_index())
|
||||
);
|
||||
assert_eq!(
|
||||
line.caret_index_for_x(0.1 * last_caret_pos + 0.9 * line.width),
|
||||
Some(line.last_index())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The emojis in this test use font fallback, which means it won't behave
|
||||
/// consistently across platforms.
|
||||
#[test]
|
||||
fn test_emoji_caret_positions() -> Result<()> {
|
||||
let (font_db, font_family) = init_fonts();
|
||||
|
||||
// We're using these emoji specifically because they're represented as multiple
|
||||
// combined characters.
|
||||
let text = "👨👧👧🇨🇦";
|
||||
|
||||
let line = font_db.text_layout_system().layout_line(
|
||||
text,
|
||||
LineStyle {
|
||||
font_size: 16.0,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: None,
|
||||
},
|
||||
&[(
|
||||
0..12,
|
||||
StyleAndFont::new(font_family, Properties::default(), TextStyle::new()),
|
||||
)],
|
||||
10000.0,
|
||||
ClipConfig::default(),
|
||||
);
|
||||
|
||||
// We want the leading edge for caret positions, so the first one is at the
|
||||
// start of the line.
|
||||
assert_eq!(line.caret_positions[0].position_in_line, 0.0);
|
||||
|
||||
assert_eq!(
|
||||
collect_line_caret_position_starts(&line),
|
||||
// CoreText gives us one caret position per visible character.
|
||||
// Each emoji is multiple characters, but one grapheme and therefore one
|
||||
// caret position.
|
||||
vec![0, 5]
|
||||
);
|
||||
|
||||
// The first character is within the first emoji, so its caret position is
|
||||
// at the start of the line.
|
||||
assert_eq!(line.caret_position_for_index(0), 0.0);
|
||||
// Likewise, the start of the next emoji returns its start position.
|
||||
assert_eq!(
|
||||
line.caret_position_for_index(5),
|
||||
line.caret_positions[1].position_in_line
|
||||
);
|
||||
// Subsequent positions within the emoji also resolve to its starting offset.
|
||||
assert_eq!(
|
||||
line.caret_position_for_index(6),
|
||||
line.caret_positions[1].position_in_line
|
||||
);
|
||||
// Past the end of the last emoji, we clamp to the end of the line.
|
||||
assert_eq!(line.caret_position_for_index(7), line.width);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The RTL text and emoji in this test use font fallback, which means
|
||||
/// this test won't behave consistently across platforms.
|
||||
#[test]
|
||||
fn test_bidi_caret_positions() -> Result<()> {
|
||||
let (font_db, font_family) = init_fonts();
|
||||
|
||||
let text = "a שָׁלוֹם 🇨🇦 test";
|
||||
let line = font_db.text_layout_system().layout_line(
|
||||
text,
|
||||
LineStyle {
|
||||
font_size: 16.0,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: None,
|
||||
},
|
||||
&[(
|
||||
0..text.encode_utf16().count(),
|
||||
StyleAndFont::new(font_family, Properties::default(), TextStyle::new()),
|
||||
)],
|
||||
10000.0,
|
||||
ClipConfig::default(),
|
||||
);
|
||||
|
||||
// Caret positions should account for diacritics in the Hebrew text, as well
|
||||
// as the 🇨🇦 emoji consisting of multiple characters. In addition, they should
|
||||
// be sorted by display order.
|
||||
assert_eq!(
|
||||
collect_line_caret_position_pairs(&line),
|
||||
vec![
|
||||
// "a "
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
// שָׁלוֹם
|
||||
(8, 8),
|
||||
(6, 7),
|
||||
(5, 5),
|
||||
(2, 4),
|
||||
// " "
|
||||
(9, 9),
|
||||
// 🇨🇦
|
||||
(10, 11),
|
||||
// " test"
|
||||
(12, 12),
|
||||
(13, 13),
|
||||
(14, 14),
|
||||
(15, 15),
|
||||
(16, 16)
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_text_ligatures() -> Result<()> {
|
||||
let mut font_db = FontDB::new();
|
||||
let zapfino = font_db.load_from_system("Zapfino")?;
|
||||
let menlo = font_db.load_from_system("Menlo")?;
|
||||
|
||||
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
||||
let frame = layout_text(
|
||||
text,
|
||||
LineStyle {
|
||||
font_size: 16.0,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: None,
|
||||
},
|
||||
&[
|
||||
(
|
||||
0..9,
|
||||
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
9..22,
|
||||
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
22..text.encode_utf16().count(),
|
||||
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
],
|
||||
&font_db,
|
||||
125., /* max_width */
|
||||
f32::MAX, /* max_height */
|
||||
Default::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
// The text should contain multiple lines since it can't fit in 125 pixels on the first
|
||||
// line.
|
||||
assert_eq!(frame.lines().len(), 4);
|
||||
|
||||
// The text should be wrapped over 4 lines and look like this:
|
||||
// "This is
|
||||
// m𐍈re or
|
||||
// less,
|
||||
// Zapfino!𐍈"
|
||||
assert_eq!(
|
||||
collect_glyph_indices(&frame),
|
||||
vec![
|
||||
vec![0, 2, 4, 5, 7, 8],
|
||||
vec![9, 10, 11, 12, 13, 14, 15, 16],
|
||||
vec![17, 18, 19, 20, 21, 22,],
|
||||
vec![23, 30, 31]
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_text_first_line_head_indent_ligatures() -> Result<()> {
|
||||
// Similar test to above, except we add in a left head indent (with reduced max width)!
|
||||
let mut font_db = FontDB::new();
|
||||
let zapfino = font_db.load_from_system("Zapfino")?;
|
||||
let menlo = font_db.load_from_system("Menlo")?;
|
||||
|
||||
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
||||
let frame = layout_text(
|
||||
text,
|
||||
LineStyle {
|
||||
font_size: 16.0,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: None,
|
||||
},
|
||||
&[
|
||||
(
|
||||
0..9,
|
||||
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
9..22,
|
||||
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
22..text.encode_utf16().count(),
|
||||
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
],
|
||||
&font_db,
|
||||
80., /* max_width */
|
||||
f32::MAX, /* max_height */
|
||||
Default::default(),
|
||||
Some(50.), /* first_line_head_indent */
|
||||
);
|
||||
|
||||
// The text should contain multiple lines since we have a 50px left head indent on the first
|
||||
// line and then each line only has 80px.
|
||||
assert_eq!(frame.lines().len(), 6);
|
||||
|
||||
assert_eq!(
|
||||
collect_glyph_indices(&frame),
|
||||
vec![
|
||||
vec![0], // left head indent means we don't have much content laid out on this line.
|
||||
vec![1, 2, 4, 5, 7, 8],
|
||||
vec![9, 10, 11, 12, 13, 14, 15, 16],
|
||||
vec![17, 18, 19, 20, 21, 22],
|
||||
vec![23, 24, 25, 27, 28],
|
||||
vec![29, 30, 31],
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tab_stops_affect_line_width() -> Result<()> {
|
||||
let mut font_db = FontDB::new();
|
||||
let menlo = font_db.load_from_system("Menlo")?;
|
||||
|
||||
let font_size = 13.0;
|
||||
let tab_size = 4;
|
||||
|
||||
let font_id = font_db.select_font(menlo, Properties::default());
|
||||
let tab_interval = (font_db
|
||||
.space_advance_width(font_id, font_size)
|
||||
.expect("space width should be measurable")
|
||||
* tab_size as f64) as f32;
|
||||
|
||||
let style = StyleAndFont::new(menlo, Properties::default(), TextStyle::new());
|
||||
let strings = "strings";
|
||||
let tabbed = "\t\t\tstrings";
|
||||
|
||||
let strings_line = layout_line(
|
||||
strings,
|
||||
LineStyle {
|
||||
font_size,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: None,
|
||||
},
|
||||
&[(0..strings.chars().count(), style)],
|
||||
&font_db,
|
||||
ClipConfig::default(),
|
||||
);
|
||||
|
||||
let tabbed_line = layout_line(
|
||||
tabbed,
|
||||
LineStyle {
|
||||
font_size,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: Some(tab_size),
|
||||
},
|
||||
&[(0..tabbed.chars().count(), style)],
|
||||
&font_db,
|
||||
ClipConfig::default(),
|
||||
);
|
||||
|
||||
// Each tab advances to the next stop.
|
||||
let expected_width = (tab_interval * 3.0) + strings_line.width;
|
||||
let error = (tabbed_line.width - expected_width).abs();
|
||||
assert!(
|
||||
error < 1.0,
|
||||
"expected tabbed width ~{}, got {} (error {})",
|
||||
expected_width,
|
||||
tabbed_line.width,
|
||||
error
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tab_stops_do_not_drift_over_long_runs() -> Result<()> {
|
||||
let mut font_db = FontDB::new();
|
||||
let menlo = font_db.load_from_system("Menlo")?;
|
||||
|
||||
let font_size = 13.0;
|
||||
let tab_size = 4;
|
||||
|
||||
let font_id = font_db.select_font(menlo, Properties::default());
|
||||
let tab_interval = (font_db
|
||||
.space_advance_width(font_id, font_size)
|
||||
.expect("space width should be measurable")
|
||||
* tab_size as f64) as f32;
|
||||
|
||||
let style = StyleAndFont::new(menlo, Properties::default(), TextStyle::new());
|
||||
let strings = "strings";
|
||||
|
||||
let strings_line = layout_line(
|
||||
strings,
|
||||
LineStyle {
|
||||
font_size,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: None,
|
||||
},
|
||||
&[(0..strings.chars().count(), style)],
|
||||
&font_db,
|
||||
ClipConfig::default(),
|
||||
);
|
||||
|
||||
let tab_count = 100;
|
||||
let tabbed = format!("{}{}", "\t".repeat(tab_count), strings);
|
||||
|
||||
let tabbed_line = layout_line(
|
||||
&tabbed,
|
||||
LineStyle {
|
||||
font_size,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: Some(tab_size),
|
||||
},
|
||||
&[(0..tabbed.chars().count(), style)],
|
||||
&font_db,
|
||||
ClipConfig::default(),
|
||||
);
|
||||
|
||||
let expected_width = (tab_interval * tab_count as f32) + strings_line.width;
|
||||
let error = (tabbed_line.width - expected_width).abs();
|
||||
assert!(
|
||||
error < 1.0,
|
||||
"expected tabbed width ~{}, got {} (error {})",
|
||||
expected_width,
|
||||
tabbed_line.width,
|
||||
error
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_text_large_first_line_head_indent_ligatures() -> Result<()> {
|
||||
// Similar test to above, except we have a large first line head indent which goes beyond the
|
||||
// max_width of the first line! We expect an empty line at the start to account for this (post-layout).
|
||||
let mut font_db = FontDB::new();
|
||||
let zapfino = font_db.load_from_system("Zapfino")?;
|
||||
let menlo = font_db.load_from_system("Menlo")?;
|
||||
|
||||
let text = "This is, some text, in Zapfino being laid out!";
|
||||
let frame = layout_text(
|
||||
text,
|
||||
LineStyle {
|
||||
font_size: 16.0,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: None,
|
||||
},
|
||||
&[
|
||||
(
|
||||
0..9,
|
||||
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
9..22,
|
||||
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
22..text.encode_utf16().count(),
|
||||
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
],
|
||||
&font_db,
|
||||
80., /* max_width */
|
||||
f32::MAX, /* max_height */
|
||||
Default::default(),
|
||||
Some(80.), /* first_line_head_indent */
|
||||
);
|
||||
|
||||
// We expect 1 empty line at the start and then 7 lines of content.
|
||||
assert_eq!(frame.lines().len(), 8);
|
||||
|
||||
assert_eq!(
|
||||
collect_glyph_indices(&frame),
|
||||
vec![
|
||||
vec![], // first line head indent takes up entire line!
|
||||
vec![0, 2, 4],
|
||||
vec![5, 7, 8, 9, 10, 11, 12, 13],
|
||||
vec![14, 15, 16, 17, 18, 19, 20, 21, 22],
|
||||
vec![23, 24, 25, 27, 28],
|
||||
vec![29, 30, 31, 32, 33, 34, 35, 36],
|
||||
vec![37, 38, 39, 40, 41],
|
||||
vec![42, 44, 45],
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_text_last_line_clipped_ligatures() -> Result<()> {
|
||||
let mut font_db = FontDB::new();
|
||||
let zapfino = font_db.load_from_system("Zapfino")?;
|
||||
let menlo = font_db.load_from_system("Menlo")?;
|
||||
|
||||
let text = "m𐍈re, Zapfino!ll𐍈, qqqq";
|
||||
let max_width = 180.;
|
||||
|
||||
let frame = layout_text(
|
||||
text,
|
||||
LineStyle {
|
||||
font_size: 16.0,
|
||||
line_height_ratio: 1.2,
|
||||
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
|
||||
fixed_width_tab_size: None,
|
||||
},
|
||||
&[
|
||||
(
|
||||
0..5,
|
||||
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
5..13,
|
||||
StyleAndFont::new(zapfino, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
13..16,
|
||||
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
(
|
||||
16..text.encode_utf16().count(),
|
||||
StyleAndFont::new(menlo, Properties::default(), TextStyle::new()),
|
||||
),
|
||||
],
|
||||
&font_db,
|
||||
max_width,
|
||||
70., /* max_height */
|
||||
Default::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
// The text should only fit one line.
|
||||
assert_eq!(frame.lines().len(), 1);
|
||||
|
||||
// The text is one line long and should be clipped like so: "m𐍈re, Zapfin𐍈!l...".
|
||||
// Note that the contents are not clipped, but the width being greater than the max width
|
||||
// indicates that when we paint, this is clipped.
|
||||
assert_eq!(
|
||||
collect_glyph_indices(&frame),
|
||||
vec![[0, 1, 2, 3, 4, 5, 6, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22],]
|
||||
);
|
||||
let first_line = frame.lines().first().unwrap();
|
||||
assert!(first_line.width > max_width);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,32 +1,57 @@
|
||||
use std::slice;
|
||||
use std::str::Utf8Error;
|
||||
|
||||
use core_foundation::base::TCFType;
|
||||
use core_graphics::base::CGFloat;
|
||||
use core_graphics::color::CGColor;
|
||||
use core_graphics::sys::CGColorRef;
|
||||
use objc::runtime::Object;
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use pathfinder_color::ColorU;
|
||||
use std::os::raw::c_char;
|
||||
use std::slice;
|
||||
use std::str::Utf8Error;
|
||||
|
||||
use cocoa::appkit::{
|
||||
NSDeleteFunctionKey as DELETE_KEY, NSDownArrowFunctionKey as ARROW_DOWN_KEY,
|
||||
NSEndFunctionKey as END_KEY, NSF10FunctionKey as F10_FUNCTION_KEY,
|
||||
NSF11FunctionKey as F11_FUNCTION_KEY, NSF12FunctionKey as F12_FUNCTION_KEY,
|
||||
NSF13FunctionKey as F13_FUNCTION_KEY, NSF14FunctionKey as F14_FUNCTION_KEY,
|
||||
NSF15FunctionKey as F15_FUNCTION_KEY, NSF16FunctionKey as F16_FUNCTION_KEY,
|
||||
NSF17FunctionKey as F17_FUNCTION_KEY, NSF18FunctionKey as F18_FUNCTION_KEY,
|
||||
NSF19FunctionKey as F19_FUNCTION_KEY, NSF1FunctionKey as F1_FUNCTION_KEY,
|
||||
NSF20FunctionKey as F20_FUNCTION_KEY, NSF2FunctionKey as F2_FUNCTION_KEY,
|
||||
NSF3FunctionKey as F3_FUNCTION_KEY, NSF4FunctionKey as F4_FUNCTION_KEY,
|
||||
NSF5FunctionKey as F5_FUNCTION_KEY, NSF6FunctionKey as F6_FUNCTION_KEY,
|
||||
NSF7FunctionKey as F7_FUNCTION_KEY, NSF8FunctionKey as F8_FUNCTION_KEY,
|
||||
NSF9FunctionKey as F9_FUNCTION_KEY, NSHelpFunctionKey as HELP_KEY,
|
||||
NSHomeFunctionKey as HOME_KEY, NSInsertFunctionKey as INSERT_KEY,
|
||||
NSLeftArrowFunctionKey as ARROW_LEFT_KEY, NSPageDownFunctionKey as PAGE_DOWN_KEY,
|
||||
NSPageUpFunctionKey as PAGE_UP_KEY, NSRightArrowFunctionKey as ARROW_RIGHT_KEY,
|
||||
NSUpArrowFunctionKey as ARROW_UP_KEY,
|
||||
use objc2_app_kit::{
|
||||
NSDeleteFunctionKey, NSDownArrowFunctionKey, NSEndFunctionKey, NSF10FunctionKey,
|
||||
NSF11FunctionKey, NSF12FunctionKey, NSF13FunctionKey, NSF14FunctionKey, NSF15FunctionKey,
|
||||
NSF16FunctionKey, NSF17FunctionKey, NSF18FunctionKey, NSF19FunctionKey, NSF1FunctionKey,
|
||||
NSF20FunctionKey, NSF2FunctionKey, NSF3FunctionKey, NSF4FunctionKey, NSF5FunctionKey,
|
||||
NSF6FunctionKey, NSF7FunctionKey, NSF8FunctionKey, NSF9FunctionKey, NSHelpFunctionKey,
|
||||
NSHomeFunctionKey, NSInsertFunctionKey, NSLeftArrowFunctionKey, NSPageDownFunctionKey,
|
||||
NSPageUpFunctionKey, NSRightArrowFunctionKey, NSUpArrowFunctionKey,
|
||||
};
|
||||
use objc2_foundation::{NSString, NSUTF8StringEncoding};
|
||||
use pathfinder_color::ColorU;
|
||||
|
||||
// AppKit exposes the function-key Unicode values as `c_uint`, but the lookup
|
||||
// below compares them against a `u16` code unit. Narrow each one to `u16`;
|
||||
// every value lies in the 0xF700..=0xF8FF private-use range and fits losslessly.
|
||||
const ARROW_UP_KEY: u16 = NSUpArrowFunctionKey as u16;
|
||||
const ARROW_DOWN_KEY: u16 = NSDownArrowFunctionKey as u16;
|
||||
const ARROW_LEFT_KEY: u16 = NSLeftArrowFunctionKey as u16;
|
||||
const ARROW_RIGHT_KEY: u16 = NSRightArrowFunctionKey as u16;
|
||||
const HOME_KEY: u16 = NSHomeFunctionKey as u16;
|
||||
const END_KEY: u16 = NSEndFunctionKey as u16;
|
||||
const PAGE_UP_KEY: u16 = NSPageUpFunctionKey as u16;
|
||||
const PAGE_DOWN_KEY: u16 = NSPageDownFunctionKey as u16;
|
||||
const HELP_KEY: u16 = NSHelpFunctionKey as u16;
|
||||
const INSERT_KEY: u16 = NSInsertFunctionKey as u16;
|
||||
const DELETE_KEY: u16 = NSDeleteFunctionKey as u16;
|
||||
const F1_FUNCTION_KEY: u16 = NSF1FunctionKey as u16;
|
||||
const F2_FUNCTION_KEY: u16 = NSF2FunctionKey as u16;
|
||||
const F3_FUNCTION_KEY: u16 = NSF3FunctionKey as u16;
|
||||
const F4_FUNCTION_KEY: u16 = NSF4FunctionKey as u16;
|
||||
const F5_FUNCTION_KEY: u16 = NSF5FunctionKey as u16;
|
||||
const F6_FUNCTION_KEY: u16 = NSF6FunctionKey as u16;
|
||||
const F7_FUNCTION_KEY: u16 = NSF7FunctionKey as u16;
|
||||
const F8_FUNCTION_KEY: u16 = NSF8FunctionKey as u16;
|
||||
const F9_FUNCTION_KEY: u16 = NSF9FunctionKey as u16;
|
||||
const F10_FUNCTION_KEY: u16 = NSF10FunctionKey as u16;
|
||||
const F11_FUNCTION_KEY: u16 = NSF11FunctionKey as u16;
|
||||
const F12_FUNCTION_KEY: u16 = NSF12FunctionKey as u16;
|
||||
const F13_FUNCTION_KEY: u16 = NSF13FunctionKey as u16;
|
||||
const F14_FUNCTION_KEY: u16 = NSF14FunctionKey as u16;
|
||||
const F15_FUNCTION_KEY: u16 = NSF15FunctionKey as u16;
|
||||
const F16_FUNCTION_KEY: u16 = NSF16FunctionKey as u16;
|
||||
const F17_FUNCTION_KEY: u16 = NSF17FunctionKey as u16;
|
||||
const F18_FUNCTION_KEY: u16 = NSF18FunctionKey as u16;
|
||||
const F19_FUNCTION_KEY: u16 = NSF19FunctionKey as u16;
|
||||
const F20_FUNCTION_KEY: u16 = NSF20FunctionKey as u16;
|
||||
|
||||
const BACKSPACE_KEY: u16 = 0x7f;
|
||||
const ENTER_KEY: u16 = 0x0d;
|
||||
@@ -88,11 +113,12 @@ pub fn unicode_char_to_key(char: u16) -> Option<&'static str> {
|
||||
///
|
||||
/// This code is only unsafe since it requires interfacing with platform code.
|
||||
pub unsafe fn nsstring_as_str<'a>(nsstring: *const Object) -> Result<&'a str, Utf8Error> {
|
||||
const UTF8_ENCODING: usize = 4;
|
||||
|
||||
let cstr: *const c_char = msg_send![nsstring, UTF8String];
|
||||
let len: usize = msg_send![nsstring, lengthOfBytesUsingEncoding: UTF8_ENCODING];
|
||||
std::str::from_utf8(slice::from_raw_parts(cstr as *const u8, len))
|
||||
// The caller guarantees `nsstring` points at a live Objective-C string, so
|
||||
// reinterpret it as an `NSString` for typed access to its UTF-8 bytes.
|
||||
let nsstring = &*nsstring.cast::<NSString>();
|
||||
let cstr = nsstring.UTF8String();
|
||||
let len = nsstring.lengthOfBytesUsingEncoding(NSUTF8StringEncoding);
|
||||
std::str::from_utf8(slice::from_raw_parts(cstr.cast::<u8>(), len))
|
||||
}
|
||||
|
||||
pub fn color_u_to_cg_color(color: ColorU) -> CGColor {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
pub mod app;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod mac;
|
||||
@@ -14,7 +14,7 @@ pub mod current {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_family = "wasm")] {
|
||||
pub use super::wasm::*;
|
||||
} else if #[cfg(target_os = "linux")] {
|
||||
} else if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
|
||||
pub use super::linux::*;
|
||||
} else if #[cfg(target_os = "macos")] {
|
||||
pub use super::mac::*;
|
||||
@@ -26,9 +26,8 @@ pub mod current {
|
||||
}
|
||||
}
|
||||
|
||||
pub use galaxyui_core::platform::*;
|
||||
|
||||
pub use app::AppBuilder;
|
||||
pub use galaxyui_core::platform::*;
|
||||
|
||||
/// Returns whether the current device is a mobile device with touch input.
|
||||
///
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
//! on the canvas to reposition the cursor. This is a known limitation of the hidden input
|
||||
//! approach.
|
||||
|
||||
use gloo::events::EventListener;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gloo::events::EventListener;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{HtmlInputElement, InputEvent, KeyboardEvent};
|
||||
|
||||
|
||||
@@ -3,22 +3,20 @@ pub(crate) mod mobile_detection;
|
||||
pub(crate) mod soft_keyboard;
|
||||
|
||||
use gloo::events::{EventListener, EventListenerOptions};
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
use crate::{keymap::Keystroke, windowing::winit::app::CustomEvent};
|
||||
|
||||
pub use hidden_input::{HiddenInput, HiddenInputEvent, InputCallback};
|
||||
pub use mobile_detection::{is_mobile_device, is_mobile_user_agent};
|
||||
pub use soft_keyboard::{SoftKeyboardInput, SoftKeyboardManager, SoftKeyboardState};
|
||||
// Re-export the functions from the core crate.
|
||||
pub use galaxyui_core::platform::wasm::*;
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
use super::KEYS_TO_IGNORE;
|
||||
use crate::keymap::Keystroke;
|
||||
use crate::platform::OperatingSystem;
|
||||
// Re-export a couple winit types and modules as the concrete implementations
|
||||
// for the wasm platform.
|
||||
pub use crate::windowing::winit::app::App;
|
||||
|
||||
// Re-export the functions from the core crate.
|
||||
pub use galaxyui_core::platform::wasm::*;
|
||||
|
||||
use super::KEYS_TO_IGNORE;
|
||||
use crate::windowing::winit::app::CustomEvent;
|
||||
|
||||
fn get_visual_viewport_dimensions() -> Option<(f32, f32)> {
|
||||
let window = gloo::utils::window();
|
||||
@@ -111,7 +109,8 @@ pub(crate) fn add_prevent_default_listener(canvas: &web_sys::HtmlCanvasElement)
|
||||
key: event.key(),
|
||||
};
|
||||
|
||||
let allow_default_event = KEYS_TO_IGNORE.contains(&keystroke);
|
||||
let allow_default_event =
|
||||
KEYS_TO_IGNORE.contains(&keystroke) || is_browser_shortcut(event);
|
||||
if !allow_default_event {
|
||||
event.prevent_default();
|
||||
}
|
||||
@@ -121,6 +120,54 @@ pub(crate) fn add_prevent_default_listener(canvas: &web_sys::HtmlCanvasElement)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_browser_shortcut(event: &web_sys::KeyboardEvent) -> bool {
|
||||
let key = event.key().to_ascii_lowercase();
|
||||
|
||||
if !event.ctrl_key() && !event.alt_key() && !event.meta_key() {
|
||||
return key == "f5";
|
||||
}
|
||||
|
||||
if is_browser_history_shortcut(event, &key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !has_browser_primary_modifier(event) || event.alt_key() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// These are standard browser chrome shortcuts that should keep working in WASM sessions.
|
||||
// This allowlist uses the browser's primary shortcut modifier for the current OS
|
||||
// (Cmd on macOS, Ctrl elsewhere), so the behavior is not macOS-specific.
|
||||
let browser_primary_shortcut_keys = [
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", "=", "+", "[", "]", "l", "n", "r",
|
||||
"t", "tab", "w", "pageup", "pagedown",
|
||||
];
|
||||
let browser_shifted_primary_shortcut_keys = [
|
||||
"-", "=", "+", "[", "]", "{", "}", "n", "r", "t", "tab", "w", "pageup", "pagedown",
|
||||
];
|
||||
|
||||
if event.shift_key() {
|
||||
browser_shifted_primary_shortcut_keys.contains(&key.as_str())
|
||||
} else {
|
||||
browser_primary_shortcut_keys.contains(&key.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
fn has_browser_primary_modifier(event: &web_sys::KeyboardEvent) -> bool {
|
||||
match OperatingSystem::get() {
|
||||
OperatingSystem::Mac => event.meta_key() && !event.ctrl_key(),
|
||||
_ => event.ctrl_key() && !event.meta_key(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_browser_history_shortcut(event: &web_sys::KeyboardEvent, key: &str) -> bool {
|
||||
if event.shift_key() || event.ctrl_key() || event.meta_key() {
|
||||
return false;
|
||||
}
|
||||
|
||||
event.alt_key() && matches!(key, "arrowleft" | "arrowright")
|
||||
}
|
||||
|
||||
pub(crate) fn add_paste_listener(event_loop_proxy: winit::event_loop::EventLoopProxy<CustomEvent>) {
|
||||
EventListener::new(&gloo::utils::document(), "paste", move |event| {
|
||||
let event = event.dyn_ref::<web_sys::ClipboardEvent>().unwrap_throw();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use itertools::Itertools as _;
|
||||
use std::os::windows::ffi::OsStrExt as _;
|
||||
|
||||
use itertools::Itertools as _;
|
||||
|
||||
// Re-export a couple winit types and modules as the concrete implementations
|
||||
// for Windows.
|
||||
pub use crate::windowing::winit::app::App;
|
||||
|
||||
Reference in New Issue
Block a user