first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+28
View File
@@ -0,0 +1,28 @@
pub fn escape_html_attribute(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'&' => escaped.push_str("&"),
'"' => escaped.push_str("""),
'\'' => escaped.push_str("'"),
'<' => escaped.push_str("&lt;"),
'>' => escaped.push_str("&gt;"),
_ => escaped.push(ch),
}
}
escaped
}
#[cfg(any(target_family = "wasm", test))]
pub(crate) fn safe_browser_open_url(url: &str) -> Option<String> {
let parsed_url = url::Url::parse(url).ok()?;
match parsed_url.scheme() {
"http" | "https" | "mailto" | "warp" | "warppreview" | "warpdev" | "warplocal"
| "warposs" | "warpintegration" => Some(parsed_url.to_string()),
_ => None,
}
}
#[cfg(test)]
#[path = "browser_tests.rs"]
mod tests;
+73
View File
@@ -0,0 +1,73 @@
use super::safe_browser_open_url;
#[test]
fn safe_browser_open_url_accepts_browser_safe_urls() {
assert_eq!(
safe_browser_open_url("https://example.com/path?q=1").as_deref(),
Some("https://example.com/path?q=1")
);
assert_eq!(
safe_browser_open_url("http://example.com/path?q=1").as_deref(),
Some("http://example.com/path?q=1")
);
assert_eq!(
safe_browser_open_url("mailto:support@warp.dev").as_deref(),
Some("mailto:support@warp.dev")
);
}
#[test]
fn safe_browser_open_url_accepts_warp_channel_urls() {
for scheme in [
"warp",
"warppreview",
"warpdev",
"warplocal",
"warposs",
"warpintegration",
] {
let url = format!("{scheme}://action/focus_cloud_mode");
assert_eq!(safe_browser_open_url(&url).as_deref(), Some(url.as_str()));
}
}
#[test]
fn safe_browser_open_url_rejects_script_capable_and_risky_urls() {
for url in [
"javascript:alert(1)",
"JaVaScRiPt:alert(1)",
"data:text/html,<script>alert(1)</script>",
"vbscript:msgbox(1)",
"blob:https://example.com/550e8400-e29b-41d4-a716-446655440000",
"about:blank",
"file:///tmp/payload.html",
"ftp://example.com/file",
] {
assert_eq!(safe_browser_open_url(url), None, "{url}");
}
}
#[test]
fn safe_browser_open_url_rejects_relative_and_malformed_urls() {
for url in [
"/relative/path",
"example.com/path",
"https://",
"http://[::1",
"",
] {
assert_eq!(safe_browser_open_url(url), None, "{url}");
}
}
#[test]
fn safe_browser_open_url_keeps_dangerous_content_as_url_data() {
let url =
"https://example.com/%22%3E%3Cscript%3Ealert(1)%3C/script%3E?next=javascript:alert(1)";
let safe_url = safe_browser_open_url(url).expect("https URL should be allowed");
let parsed_url = url::Url::parse(&safe_url).expect("safe URL should remain parseable");
assert_eq!(parsed_url.scheme(), "https");
assert_eq!(parsed_url.host_str(), Some("example.com"));
assert_eq!(parsed_url.query(), Some("next=javascript:alert(1)"));
}
+4 -6
View File
@@ -3,13 +3,11 @@
pub mod font_kit;
#[cfg(test)]
#[path = "text_layout_test.rs"]
#[path = "text_layout_tests.rs"]
mod text_layout_tests;
pub use galaxyui_core::fonts::*;
#[cfg(test)]
pub(crate) use text_layout_tests::{collect_glyph_indices, init_fonts};
#[cfg(all(test, target_os = "macos"))]
pub(crate) use text_layout_tests::collect_line_caret_position_starts;
#[cfg(test)]
pub(crate) use text_layout_tests::{collect_glyph_indices, init_fonts};
pub use galaxyui_core::fonts::*;
+9 -10
View File
@@ -1,18 +1,16 @@
//! Platform-independent text layout tests.
use crate::elements::DEFAULT_UI_LINE_HEIGHT_RATIO;
use crate::fonts::{FamilyId, Properties, Style, Weight};
use crate::platform::FontDB as _;
use crate::platform::LineStyle;
use crate::text_layout::{
ClipConfig, Line, StyleAndFont, TextAlignment, TextFrame, TextStyle, DEFAULT_TOP_BOTTOM_RATIO,
};
use anyhow::Result;
use itertools::Itertools;
use pathfinder_color::ColorU;
use crate::elements::DEFAULT_UI_LINE_HEIGHT_RATIO;
use crate::fonts::{FamilyId, Properties, Style, Weight};
#[cfg(target_os = "macos")]
use crate::platform::mac::fonts::FontDB;
use crate::platform::{FontDB as _, LineStyle};
use crate::text_layout::{
ClipConfig, Line, StyleAndFont, TextAlignment, TextFrame, TextStyle, DEFAULT_TOP_BOTTOM_RATIO,
};
#[cfg(not(target_os = "macos"))]
use crate::windowing::winit::fonts::FontDB;
@@ -70,7 +68,8 @@ fn test_fixed_width_tab_size_matches_spaces_width() -> Result<()> {
/// Read the bundled Roboto font's bytes from the filesystem.
fn load_roboto_bytes() -> Vec<Vec<u8>> {
use std::{fs::read, path::PathBuf};
use std::fs::read;
use std::path::PathBuf;
let root = env!("CARGO_MANIFEST_DIR");
let typeface_files = ["Roboto-Italic.ttf", "Roboto-Bold.ttf", "Roboto-Regular.ttf"];
typeface_files
@@ -483,7 +482,7 @@ fn test_char_indices() -> 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!(
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1,3 +1,4 @@
pub mod browser;
pub mod fonts;
pub mod platform;
pub mod rendering;
+5 -8
View File
@@ -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,
+7 -9
View File
@@ -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>>,
+5 -12
View File
@@ -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 {
+23
View File
@@ -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()
}
+130 -138
View File
@@ -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);
+48 -64
View File
@@ -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,
});
+82 -65
View File
@@ -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) {
+56 -54
View File
@@ -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,
+11 -10
View File
@@ -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,
+3 -2
View File
@@ -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;
+20 -17
View File
@@ -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)
})
}
}
+166 -106
View File
@@ -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
}
+12 -12
View File
@@ -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,
};
+2 -1
View File
@@ -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);
+36 -1
View File
@@ -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);
+110 -3
View File
@@ -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>,
}
+33 -38
View File
@@ -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(())
}
+54 -28
View File
@@ -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
+3 -4
View File
@@ -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};
+57 -10
View File
@@ -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();
+2 -1
View File
@@ -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;
@@ -1,7 +1,8 @@
use crate::rendering::atlas::{AllocatedRegion, AllocationError};
use pathfinder_geometry::rect::{RectF, RectI};
use pathfinder_geometry::vector::{vec2f, vec2i, Vector2I};
use crate::rendering::atlas::{AllocatedRegion, AllocationError};
/// The number of pixels of padding that should be applied between elements
/// in an atlas row.
const HORIZONTAL_PADDING: i32 = 1;
@@ -1,8 +1,9 @@
use crate::rendering::atlas::allocator::Allocator;
use crate::rendering::atlas::{AllocatedRegion, AllocationError};
use anyhow::Result;
use pathfinder_geometry::vector::Vector2I;
use crate::rendering::atlas::allocator::Allocator;
use crate::rendering::atlas::{AllocatedRegion, AllocationError};
/// Manager that is responsible for allocating areas into a series of textures atlases.
pub(crate) struct Manager {
current_allocator: Allocator,
@@ -2,7 +2,6 @@ mod allocator;
mod manager;
pub(crate) use manager::{Manager, TextureId};
use pathfinder_geometry::rect::{RectF, RectI};
use thiserror::Error;
+9 -9
View File
@@ -1,14 +1,14 @@
use crate::fonts::{canvas, RasterizedGlyph};
use crate::rendering::atlas::{self, AllocatedRegion, TextureId};
use crate::{fonts::SubpixelAlignment, rendering, scene::GlyphKey};
use std::collections::HashMap;
use anyhow::Result;
use ordered_float::OrderedFloat;
use pathfinder_geometry::rect::RectI;
use pathfinder_geometry::{
rect::RectF,
vector::{Vector2F, Vector2I},
};
use std::collections::HashMap;
use pathfinder_geometry::rect::{RectF, RectI};
use pathfinder_geometry::vector::{Vector2F, Vector2I};
use crate::fonts::{canvas, RasterizedGlyph, SubpixelAlignment};
use crate::rendering;
use crate::rendering::atlas::{self, AllocatedRegion, TextureId};
use crate::scene::GlyphKey;
const ATLAS_SIZE: usize = 1024;
+1 -2
View File
@@ -3,11 +3,10 @@ pub(crate) mod glyph_cache;
#[cfg(wgpu)]
pub mod wgpu;
pub(crate) use glyph_cache::{GlyphCache, GlyphRasterBoundsFn, RasterizeGlyphFn};
pub use galaxyui_core::rendering::*;
use galaxyui_core::scene::Dash;
pub(crate) use glyph_cache::{GlyphCache, GlyphRasterBoundsFn, RasterizeGlyphFn};
/// Cache for the result of calling [`is_low_power_gpu_available`], as the
/// check can be expensive.
static LOW_POWER_GPU_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
+4 -4
View File
@@ -5,10 +5,9 @@ mod texture_with_bind_group;
use std::sync::{Arc, LazyLock, Mutex};
use wgpu::wgt::WgpuHasDisplayHandle;
pub use renderer::Renderer;
pub use resources::{adapter_has_rendering_offset_bug, Resources};
use wgpu::wgt::WgpuHasDisplayHandle;
use crate::platform::GraphicsBackend;
#[cfg(not(target_family = "wasm"))]
@@ -78,9 +77,10 @@ pub fn init_wgpu_instance(display_handle: Box<dyn WgpuHasDisplayHandle>) {
}
instance_lock_guard.get_or_insert_with(|| {
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
use crate::windowing::{winit::app::WINDOWING_SYSTEM, WindowingSystem};
use crate::windowing::winit::app::WINDOWING_SYSTEM;
use crate::windowing::WindowingSystem;
// If the user hasn't enabled (and is making use of) native Wayland
// support, due to the fact that we force use of X11 in
// ui/src/windowing/winit/app.rs, we need to make sure wgpu doesn't
@@ -8,15 +8,15 @@ use frame::Frame;
use galaxyui_core::platform::CapturedFrame;
use pathfinder_geometry::vector::Vector2F;
use util::with_error_scope;
use wgpu::wgc::{device::DeviceError, present::SurfaceError};
use wgpu::wgc::device::DeviceError;
use wgpu::wgc::present::SurfaceError;
pub use super::resources::{GetSurfaceTextureError, SurfaceConfigureError};
use crate::r#async::block_on;
use crate::rendering::wgpu::Resources;
use crate::rendering::{GlyphConfig, GlyphRasterBoundsFn, RasterizeGlyphFn};
use crate::Scene;
pub use super::resources::{GetSurfaceTextureError, SurfaceConfigureError};
const ENCODER_DESCRIPTOR: wgpu::CommandEncoderDescriptor = wgpu::CommandEncoderDescriptor {
label: Some("Command encoder"),
};
@@ -1,13 +1,12 @@
use crate::rendering::wgpu::renderer::{glyph, image, rect, WGPUContext};
use crate::rendering::wgpu::Resources;
use crate::scene::Layer;
use crate::Scene;
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::Vector2F;
use wgpu::{CommandEncoder, RenderPass, SurfaceTexture};
use crate::rendering::wgpu::renderer::{glyph, image, rect, WGPUContext};
use crate::rendering::wgpu::Resources;
use crate::scene::Layer;
use crate::Scene;
#[derive(Default)]
struct PerFrameState {
rect: rect::PerFrameState,
@@ -1,3 +1,16 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use pathfinder_geometry::rect::RectF;
use wgpu::util::BufferInitDescriptor;
use wgpu::{
BindGroupLayout, BufferUsages, ColorTargetState, Device, FilterMode, RenderPass,
RenderPipeline, Sampler,
};
use super::util::create_buffer_init;
use crate::fonts::SubpixelAlignment;
use crate::rendering::atlas::TextureId;
use crate::rendering::wgpu::renderer::WGPUContext;
@@ -6,17 +19,6 @@ use crate::rendering::wgpu::{resources, shader_types};
use crate::rendering::{GlyphCache, GlyphConfig};
use crate::scene::{GlyphFade, Layer};
use crate::Scene;
use pathfinder_geometry::rect::RectF;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::{atomic::AtomicBool, Arc};
use wgpu::util::BufferInitDescriptor;
use wgpu::{
BindGroupLayout, BufferUsages, ColorTargetState, Device, FilterMode, RenderPass,
RenderPipeline, Sampler,
};
use super::util::create_buffer_init;
pub(super) struct Pipeline {
glyph_cache: GlyphCache<TextureWithBindGroup>,
@@ -292,9 +294,9 @@ impl Pipeline {
}
mod shaders {
use crate::rendering::wgpu::shader_types::{ColorF, Vector4F};
use pathfinder_color::ColorU;
use pathfinder_geometry::rect::RectF;
use crate::rendering::wgpu::shader_types::{ColorF, Vector4F};
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
@@ -1,10 +1,7 @@
use crate::image_cache::StaticImage;
use crate::rendering::texture_cache::{TextureCache, TextureCacheIndex};
use crate::rendering::wgpu::{resources, shader_types};
use crate::scene::Layer;
use crate::Scene;
use std::borrow::Cow;
use std::sync::{atomic::AtomicBool, Arc};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use wgpu::util::BufferInitDescriptor;
use wgpu::{
BindGroup, BindGroupDescriptor, BindGroupLayout, ColorTargetState, Device, Extent3d,
@@ -13,9 +10,13 @@ use wgpu::{
};
use self::shaders::{ColorModifier, ImageInstanceData};
use super::util::create_buffer_init;
use super::WGPUContext;
use crate::image_cache::StaticImage;
use crate::rendering::texture_cache::{TextureCache, TextureCacheIndex};
use crate::rendering::wgpu::{resources, shader_types};
use crate::scene::Layer;
use crate::Scene;
pub(super) struct Pipeline {
render_pipeline: RenderPipeline,
@@ -302,11 +303,12 @@ impl TextureInfo {
}
mod shaders {
use crate::rendering::wgpu::shader_types::{vec4f, ColorF, Vector4F};
use crate::rendering::CornerRadius;
use pathfinder_color::ColorU;
use pathfinder_geometry::rect::RectF;
use crate::rendering::wgpu::shader_types::{vec4f, ColorF, Vector4F};
use crate::rendering::CornerRadius;
/// Icons support overriding the color, whereas images only allow setting the opacity.
pub(super) enum ColorModifier {
Icon { color: ColorU },
@@ -1,17 +1,19 @@
use std::borrow::Cow;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use pathfinder_color::ColorU;
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::vec2f;
use wgpu::util::BufferInitDescriptor;
use wgpu::{BindGroupLayout, ColorTargetState, Device, RenderPass, RenderPipeline};
use super::util::create_buffer_init;
use crate::rendering::get_best_dash_gap;
use crate::rendering::wgpu::shader_types::BorderWidth;
use crate::rendering::wgpu::{resources, shader_types};
use crate::scene::Layer;
use crate::Scene;
use pathfinder_color::ColorU;
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::vec2f;
use std::borrow::Cow;
use std::sync::{atomic::AtomicBool, Arc};
use wgpu::util::BufferInitDescriptor;
use wgpu::{BindGroupLayout, ColorTargetState, Device, RenderPass, RenderPipeline};
use super::util::create_buffer_init;
pub(super) struct Pipeline {
render_pipeline: RenderPipeline,
@@ -1,12 +1,8 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use wgpu::{
util::BufferInitDescriptor, Buffer, BufferAddress, BufferDescriptor, Device,
COPY_BUFFER_ALIGNMENT,
};
use wgpu::util::BufferInitDescriptor;
use wgpu::{Buffer, BufferAddress, BufferDescriptor, Device, COPY_BUFFER_ALIGNMENT};
use super::Error;
@@ -3,14 +3,9 @@ pub mod uniforms;
use std::cell::RefCell;
use std::collections::HashSet;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::rendering::OnGPUDeviceSelected;
use crate::windowing;
use crate::{r#async::block_on, rendering::GPUPowerPreference};
use anyhow::{anyhow, Result};
use galaxyui_core::rendering::{GPUBackend, GPUDeviceInfo, GPUDeviceType};
use itertools::Itertools;
@@ -23,6 +18,10 @@ use wgpu::{
Queue, Surface, SurfaceConfiguration,
};
use crate::r#async::block_on;
use crate::rendering::{GPUPowerPreference, OnGPUDeviceSelected};
use crate::windowing;
/// A mostly-arbitrary value to use as the height/width of a surface when
/// creating a default surface configuration.
///
@@ -500,6 +499,21 @@ fn is_intel_uhd_620_adapter_on_windows_with_vulkan_backend(
|| adapter_info.name.contains("Intel(R) HD Graphics 620"))
}
/// Returns true if this is an Intel UHD Graphics 770 integrated GPU on Windows, using either the
/// DX12 or Vulkan backend.
///
/// This integrated GPU's drivers introduce severe latency/stutter in the present path.
/// Public reports of latency/stutter bugs on Intel integrated GPU drivers (including the UHD 770):
/// https://github.com/IGCIT/Intel-GPU-Community-Issue-Tracker-IGCIT/issues/1002
/// https://github.com/libsdl-org/SDL/issues/5628
/// See also https://github.com/warpdotdev/warp/issues/4856
fn is_intel_uhd_770_adapter_on_windows(adapter_info: &wgpu::AdapterInfo) -> bool {
cfg!(windows)
&& matches!(adapter_info.backend, Backend::Dx12 | Backend::Vulkan)
&& adapter_info.device_type == DeviceType::IntegratedGpu
&& adapter_info.name.contains("Intel(R) UHD Graphics 770")
}
/// Returns whether the given adapter is known to have a rendering offset bug on Windows.
///
/// Certain Intel integrated GPU drivers using the GL backend render the scene at an offset from
@@ -521,6 +535,7 @@ pub fn adapter_has_rendering_offset_bug(adapter_info: &wgpu::AdapterInfo) -> boo
// Known affected Intel integrated GPU models. This list is based on user reports from
// https://github.com/warpdotdev/Warp/issues/6120.
let affected_models = [
"Intel(R) HD Graphics 2500",
"Intel(R) HD Graphics 4000",
"Intel(R) HD Graphics 4400",
"Intel(R) HD Graphics 4600",
@@ -535,6 +550,20 @@ pub fn adapter_has_rendering_offset_bug(adapter_info: &wgpu::AdapterInfo) -> boo
.any(|model| adapter_info.name.contains(model))
}
/// Returns whether the adapter is the Broadcom V3D Vulkan driver (V3DV).
///
/// The V3D Vulkan driver on Raspberry Pi and similar ARM SBCs have panics and flickering issues.
/// The logs have warnings about these drivers missing the `FULL_DRAW_INDEX_UINT32` downlevel flag
/// but if it's unclear if that is the actual cause. See:
/// https://github.com/warpdotdev/warp/issues/10618
/// https://github.com/warpdotdev/warp/issues/4879
fn is_v3d_vulkan_adapter(adapter_info: &wgpu::AdapterInfo) -> bool {
cfg!(target_os = "linux")
&& adapter_info.backend == wgpu::Backend::Vulkan
&& adapter_info.driver.contains("V3DV")
&& adapter_info.name.starts_with("V3D 4.2.14.")
}
/// Checks whether the provided adapter info describes a lavapipe
/// (Vulkan llvmpipe) adapter that may not work properly with galaxyui.
fn is_older_lavapipe_adapter(adapter_info: &wgpu::AdapterInfo) -> bool {
@@ -715,11 +744,21 @@ fn adapter_stability_sort_func(
return AdapterSupport::Unsupported;
}
if is_v3d_vulkan_adapter(&adapter_info) {
log::warn!("Deprioritizing Vulkan-backed V3D adapter");
return AdapterSupport::Unsupported;
}
if is_intel_uhd_620_adapter_on_windows_with_vulkan_backend(&adapter_info) {
log::warn!("Deprioritizing Vulkan-backed Intel UHD 620 adapter");
return AdapterSupport::SupportedWithIssues;
}
if is_intel_uhd_770_adapter_on_windows(&adapter_info) {
log::warn!("Deprioritizing Intel UHD 770 integrated GPU on Windows");
return AdapterSupport::SupportedWithIssues;
}
if is_older_vulkan_intel_uhd_adapter(&adapter_info) {
log::warn!(
"Deprioritizing Vulkan-backed Intel UHD adapter due to Mesa < {} (unsupported)",
@@ -1,7 +1,5 @@
use wgpu::{
util::{BufferInitDescriptor, DeviceExt},
Buffer, RenderPass,
};
use wgpu::util::{BufferInitDescriptor, DeviceExt};
use wgpu::{Buffer, RenderPass};
use crate::rendering::wgpu::shader_types;
@@ -200,7 +200,6 @@ impl RectData {
}
pub(super) fn desc() -> wgpu::VertexBufferLayout<'static> {
use std::mem;
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<Self>() as wgpu::BufferAddress,
@@ -1,11 +1,11 @@
use crate::fonts::RasterizedGlyph;
use crate::rendering::atlas::AllocatedRegion;
use wgpu::{
BindGroup, BindGroupDescriptor, BindGroupLayout, Extent3d, Queue, Sampler,
TexelCopyBufferLayout, Texture, TextureDescriptor, TextureFormat, TextureUsages,
};
use crate::fonts::RasterizedGlyph;
use crate::rendering::atlas::AllocatedRegion;
/// Helper struct that includes a [`Texture`] and its corresponding [`BindGroup`] for use in the
/// `GlyphCache`.
pub(super) struct TextureWithBindGroup {
+15 -1
View File
@@ -2,5 +2,19 @@
pub mod winit;
pub use galaxyui_core::windowing::*;
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub use winit::WindowingSystem;
/// The minimum width a window can be resized to.
/// TODO(CORE-1891) Instead of being hard-coded, this should be configurable by the user via
/// [`crate::platform::WindowOptions`].
#[cfg(any(test, feature = "integration_tests"))]
pub const MIN_WINDOW_WIDTH: f32 = 124.;
#[cfg(not(any(test, feature = "integration_tests")))]
pub const MIN_WINDOW_WIDTH: f32 = 480.;
/// The minimum height a window can be resized to.
#[cfg(any(test, feature = "integration_tests"))]
pub const MIN_WINDOW_HEIGHT: f32 = 34.;
#[cfg(not(any(test, feature = "integration_tests")))]
pub const MIN_WINDOW_HEIGHT: f32 = 192.;
+21 -27
View File
@@ -1,24 +1,18 @@
use futures_util::future::LocalBoxFuture;
use std::mem::ManuallyDrop;
use crate::{
clipboard::ClipboardContent,
integration::TestDriver,
keymap,
platform::{self, TerminationMode},
AppContext, AssetProvider, WindowId,
};
use derivative::Derivative;
use super::window::{IntegrationTestWindowManager, WindowManager};
use crate::notification::RequestPermissionsOutcome;
use crate::platform::NotificationInfo;
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use std::sync::OnceLock;
#[cfg(target_os = "linux")]
use derivative::Derivative;
use futures_util::future::LocalBoxFuture;
use super::window::{IntegrationTestWindowManager, WindowManager};
use crate::clipboard::ClipboardContent;
use crate::integration::TestDriver;
use crate::notification::RequestPermissionsOutcome;
use crate::platform::{self, NotificationInfo, TerminationMode};
use crate::{keymap, AppContext, AssetProvider, WindowId};
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub static WINDOWING_SYSTEM: OnceLock<WindowingSystem> = OnceLock::new();
pub type RequestPermissionsCallback =
@@ -65,9 +59,9 @@ pub enum CustomEvent {
Clipboard(ClipboardEvent),
SetCursorShape(platform::Cursor),
ActiveCursorPositionUpdated,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[cfg_attr(not(any(target_os = "linux", target_os = "freebsd")), allow(dead_code))]
AboutToSleep,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[cfg_attr(not(any(target_os = "linux", target_os = "freebsd")), allow(dead_code))]
ResumedFromSleep,
/// The application is connected to the internet.
#[cfg_attr(any(target_os = "macos"), allow(dead_code))]
@@ -115,7 +109,7 @@ pub enum ClipboardEvent {
Paste(ClipboardContent),
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
#[derive(Debug, PartialEq)]
pub enum WindowingSystem {
X11,
@@ -127,7 +121,7 @@ pub struct App {
assets: Box<dyn AssetProvider>,
is_integration_test: bool,
window_class: Option<String>,
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
force_x11: bool,
}
@@ -142,7 +136,7 @@ impl App {
assets,
is_integration_test: test_driver.is_some(),
window_class: None,
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
force_x11: false,
}
}
@@ -154,7 +148,7 @@ impl App {
self.window_class = Some(window_class);
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub(crate) fn force_x11(&mut self, force_x11: bool) {
self.force_x11 = force_x11;
}
@@ -168,13 +162,13 @@ impl App {
assets,
is_integration_test,
window_class,
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
force_x11,
} = self;
let mut event_loop_builder = winit::event_loop::EventLoop::with_user_event();
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if force_x11 {
winit::platform::x11::EventLoopBuilderExtX11::with_x11(&mut event_loop_builder);
}
@@ -188,7 +182,7 @@ impl App {
// Perform some platform-specific initialization.
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
super::linux::maybe_register_xlib_error_hook(&event_loop);
super::linux::ensure_cursor_theme();
} else if #[cfg(target_family = "wasm")] {
+71 -58
View File
@@ -3,14 +3,12 @@
#[cfg(not(target_family = "wasm"))]
mod global_hotkey;
use std::cell::RefCell;
use std::collections::HashMap;
use std::mem::ManuallyDrop;
use std::{
cell::RefCell,
collections::HashMap,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
thread::{self, panicking},
};
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use std::thread::{self, panicking};
use anyhow::Result;
use geometry::rect::RectF;
@@ -19,35 +17,27 @@ use parking_lot::Mutex;
use serde::de::IntoDeserializer;
use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
use crate::platform::MicrophoneAccessState;
use crate::platform::{
file_picker::{
FilePickerCallback, FilePickerError, SaveFilePickerCallback, SaveFilePickerConfiguration,
},
Cursor, RequestNotificationPermissionsCallback, SendNotificationErrorCallback,
};
use crate::windowing::winit::app::CustomEvent::UpdateUIApp;
use crate::windowing::WindowManager;
use crate::Effect::Event;
use crate::{
accessibility,
clipboard::{self, ClipboardContent, InMemoryClipboard},
geometry, keymap,
modals::{AlertDialog, ModalId},
notification, platform,
platform::file_picker::{FilePickerConfiguration, FileType},
windowing::{self, WindowCallbacks},
AppContext, ApplicationBundleInfo, Clipboard, DisplayId, DisplayIdx, WindowId,
};
use crate::{
notification::{NotificationSendError, RequestPermissionsOutcome},
platform::TerminationMode,
};
use super::{notifications, CustomEvent};
#[cfg(not(target_family = "wasm"))]
use self::global_hotkey::GlobalHotKeyHandler;
use super::{notifications, CustomEvent};
use crate::clipboard::{self, ClipboardContent, InMemoryClipboard};
use crate::modals::{AlertDialog, ModalId};
use crate::notification::{NotificationSendError, RequestPermissionsOutcome};
use crate::platform::file_picker::{
FilePickerCallback, FilePickerConfiguration, FilePickerError, FileType, SaveFilePickerCallback,
SaveFilePickerConfiguration,
};
use crate::platform::{
Cursor, MicrophoneAccessState, RequestNotificationPermissionsCallback,
SendNotificationErrorCallback, TerminationMode,
};
use crate::windowing::winit::app::CustomEvent::UpdateUIApp;
use crate::windowing::{self, WindowCallbacks, WindowManager};
use crate::Effect::Event;
use crate::{
accessibility, geometry, keymap, notification, platform, AppContext, ApplicationBundleInfo,
Clipboard, DisplayId, DisplayIdx, WindowId,
};
// No-op on WASM since the browser cannot provide this functionality.
#[cfg(target_family = "wasm")]
@@ -67,11 +57,18 @@ static MAIN_THREAD_ID: OnceLock<thread::ThreadId> = OnceLock::new();
pub fn open_url_in_system(url: &str) {
#[cfg(target_family = "wasm")]
if let Some(window) = web_sys::window() {
// Try to open the URL in a new tab.
let _ = window.open_with_url_and_target(url, "_blank");
if let Some(safe_url) = crate::browser::safe_browser_open_url(url) {
let _ = window.open_with_url_and_target_and_features(
&safe_url,
"_blank",
"noopener,noreferrer",
);
} else {
log::warn!("Skipping browser URL open for invalid or unsafe URL");
}
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
// Opening in WSL is complicated for a few reasons
// 1. By default, wsl does not have an awareness of browsers installed in windows.
@@ -80,7 +77,7 @@ pub fn open_url_in_system(url: &str) {
// "native" opening of files is not necessarily going to work.
// We choose to do the following:
// 1. First attempt to open with `wslview`, since that is basically made to open stuff in wsl
// 2. Use `cmd.exe /c start {url}` to open in the user's default windows browser
// 2. Use `rundll32.exe url.dll,FileProtocolHandler {url}` to open in the user's default windows browser
// - If a user does not want this behavior, and wants all opening to go through
// WSL, they can set the env variable WARP_FORCE_WSL_BROWSER.
// 3. Fall back to default linux url opening behavior.
@@ -92,23 +89,35 @@ pub fn open_url_in_system(url: &str) {
),
};
// Attempt to open by
if !use_wsl_browser() {
let mut cmd = command::blocking::Command::new("cmd.exe");
cmd.args(["/c", "start", url]);
// Validate the URL scheme before passing to Windows to prevent injection
// via unrecognized or file-system-targeting schemes (e.g. file:, ms-msdt:,
// search-ms:, javascript:). We use the re-serialized URL from the parser
// rather than the raw input so that characters like `"` are percent-encoded
// (e.g. %22) before they reach explorer.exe's command line.
let safe_url = url::Url::parse(url)
.ok()
.and_then(|u| matches!(u.scheme(), "http" | "https").then(|| u.to_string()));
// Note: Ideally, we would be calling detached like open::that_detached does.
// However, it is probably fine.
match cmd
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
{
Ok(_) => return,
Err(e) => log::info!(
"Failed to open url with cmd.exe {e:?}, falling back to another method"
),
if let Some(safe_url) = safe_url {
let mut cmd = command::blocking::Command::new("rundll32.exe");
cmd.args(["url.dll,FileProtocolHandler", &safe_url]);
// Note: Ideally, we would be calling detached like open::that_detached does.
// However, it is probably fine.
match cmd
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
{
Ok(_) => return,
Err(e) => log::info!(
"Failed to open url with rundll32.exe {e:?}, falling back to another method"
),
}
} else {
log::warn!("Skipping Windows URL open for unrecognized or unsafe URL scheme");
}
}
}
@@ -125,7 +134,7 @@ pub fn open_url_in_system(url: &str) {
}
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn use_wsl_browser() -> bool {
static USE_WSL_BROWSER: OnceLock<bool> = OnceLock::new();
USE_WSL_BROWSER
@@ -214,7 +223,7 @@ impl AppDelegate {
cfg_if::cfg_if! {
if #[cfg(target_family = "wasm")] {
self.clipboard = Box::new(super::wasm::WebClipboard::new());
} else if #[cfg(target_os = "linux")] {
} else if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
match super::linux::LinuxClipboard::new() {
Ok(clipboard) => self.clipboard = Box::new(clipboard),
Err(err) => {
@@ -251,7 +260,7 @@ impl platform::Delegate for AppDelegate {
#[cfg(not(target_family = "wasm"))]
fn system_theme(&self) -> platform::SystemTheme {
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
match super::linux::get_system_theme() {
Ok(system_theme) => {
return system_theme;
@@ -295,7 +304,7 @@ impl platform::Delegate for AppDelegate {
fn open_file_path(&self, path: &Path) {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
let _ = command::blocking::Command::new("xdg-open")
.arg(path)
.spawn();
@@ -304,7 +313,11 @@ impl platform::Delegate for AppDelegate {
if let Some(path) = path.to_str() {
// Try to open the path via a file:// URL.
let url = format!("file://{path}");
let _ = window.open_with_url(&url);
let _ = window.open_with_url_and_target_and_features(
&url,
"_blank",
"noopener,noreferrer",
);
}
}
} else if #[cfg(windows)] {
@@ -1,14 +1,16 @@
use std::{collections::HashMap, rc::Rc, str::FromStr, sync::Arc, thread};
use std::collections::HashMap;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::Arc;
use std::thread;
use crate::keymap;
use crate::windowing::winit::app::CustomEvent;
use global_hotkey::hotkey::{Code, HotKey, Modifiers};
use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState};
use parking_lot::Mutex;
use winit::event_loop::EventLoopProxy;
use global_hotkey::{
hotkey::{Code, HotKey, Modifiers},
GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState,
};
use crate::keymap;
use crate::windowing::winit::app::CustomEvent;
/// Responsible for registering system-wide (global) hotkeys with the platform.
pub struct GlobalHotKeyHandler {
@@ -1,7 +1,9 @@
use super::*;
use std::path::PathBuf;
use winit::window::WindowId as WinitWindowId;
use super::*;
#[test]
fn test_drag_drop_debouncing_single_file() {
// Create a mock event loop structure
@@ -2,7 +2,6 @@ use std::borrow::Cow;
use std::collections::HashMap;
use lazy_static::lazy_static;
use winit::event::ElementState;
#[cfg(windows)]
use winit::keyboard::NativeKey;
@@ -10,10 +9,10 @@ use winit::keyboard::{Key, ModifiersState, NamedKey};
#[cfg(not(target_family = "wasm"))]
use winit::platform::modifier_supplement::KeyEventExtModifierSupplement;
use crate::platform::KEYS_TO_IGNORE;
use crate::{event::KeyEventDetails, keymap::Keystroke};
use super::WindowState;
use crate::event::KeyEventDetails;
use crate::keymap::Keystroke;
use crate::platform::KEYS_TO_IGNORE;
lazy_static! {
/// Mapping between a printable ASCII character and its corresponding control code had `ctrl`
@@ -101,6 +100,25 @@ pub fn convert_keyboard_input_event(
{
input.key_without_modifiers()
}
// On Windows, non-Latin keyboard layouts (Cyrillic, Greek, Arabic, etc.) translate
// the physical key to a non-ASCII character even when Ctrl/Cmd is held. That makes
// bindings like `ctrl-c` / `ctrl-v` fail to match. Fall back to the US-QWERTY
// position so chord shortcuts work regardless of the active layout — same approach
// used by VS Code, JetBrains, and Chromium. Issue #9036.
//
// Right-Alt is excluded because Windows reports AltGr as Ctrl+Alt; without this
// guard, AltGr-produced characters (e.g. `€` on a German layout) would be rewritten
// into a spurious chord and the typed character would be swallowed.
#[cfg(windows)]
Key::Character(c)
if (window_state.modifiers.control_key() || window_state.modifiers.super_key())
&& !window_state.right_alt_pressed
&& !c.is_ascii() =>
{
us_qwerty_fallback_for_chord(&input.physical_key, shift)
.map(|s| Key::Character(s.into()))
.unwrap_or_else(|| input.logical_key.clone())
}
_ => input.logical_key,
};
let input_key = get_input_key(&logical_key, shift);
@@ -253,6 +271,100 @@ fn convert_key(key: Key) -> Option<Cow<'static, str>> {
Some(Cow::Borrowed(value))
}
/// Maps a winit `PhysicalKey` to the US-QWERTY character it would produce. Used on Windows
/// to recover layout-independent chord shortcuts (e.g. `ctrl-c`) when a non-Latin keyboard
/// layout has translated the logical key to a non-ASCII character.
///
/// Returns `None` for keys outside the standard letter/digit/punctuation set (function keys,
/// modifiers, navigation keys, etc.), since those either aren't typically used in chord
/// bindings as character keys or are already handled via `NamedKey` in `convert_key`.
#[cfg_attr(not(windows), allow(dead_code))]
fn us_qwerty_fallback_for_chord(
physical_key: &winit::keyboard::PhysicalKey,
shift: bool,
) -> Option<&'static str> {
use winit::keyboard::{KeyCode, PhysicalKey};
let PhysicalKey::Code(code) = physical_key else {
return None;
};
// Letters always return lowercase here; `get_input_key` applies the uppercase
// transform downstream when shift is held. Digit/punctuation keys must return
// the shifted US-QWERTY symbol up front because `get_input_key` passes
// non-letter characters through unchanged, so bindings like `ctrl-shift-}`
// would otherwise see `ctrl-shift-]` on non-Latin layouts.
Some(match (code, shift) {
(KeyCode::KeyA, _) => "a",
(KeyCode::KeyB, _) => "b",
(KeyCode::KeyC, _) => "c",
(KeyCode::KeyD, _) => "d",
(KeyCode::KeyE, _) => "e",
(KeyCode::KeyF, _) => "f",
(KeyCode::KeyG, _) => "g",
(KeyCode::KeyH, _) => "h",
(KeyCode::KeyI, _) => "i",
(KeyCode::KeyJ, _) => "j",
(KeyCode::KeyK, _) => "k",
(KeyCode::KeyL, _) => "l",
(KeyCode::KeyM, _) => "m",
(KeyCode::KeyN, _) => "n",
(KeyCode::KeyO, _) => "o",
(KeyCode::KeyP, _) => "p",
(KeyCode::KeyQ, _) => "q",
(KeyCode::KeyR, _) => "r",
(KeyCode::KeyS, _) => "s",
(KeyCode::KeyT, _) => "t",
(KeyCode::KeyU, _) => "u",
(KeyCode::KeyV, _) => "v",
(KeyCode::KeyW, _) => "w",
(KeyCode::KeyX, _) => "x",
(KeyCode::KeyY, _) => "y",
(KeyCode::KeyZ, _) => "z",
(KeyCode::Digit1, true) => "!",
(KeyCode::Digit1, false) => "1",
(KeyCode::Digit2, true) => "@",
(KeyCode::Digit2, false) => "2",
(KeyCode::Digit3, true) => "#",
(KeyCode::Digit3, false) => "3",
(KeyCode::Digit4, true) => "$",
(KeyCode::Digit4, false) => "4",
(KeyCode::Digit5, true) => "%",
(KeyCode::Digit5, false) => "5",
(KeyCode::Digit6, true) => "^",
(KeyCode::Digit6, false) => "6",
(KeyCode::Digit7, true) => "&",
(KeyCode::Digit7, false) => "7",
(KeyCode::Digit8, true) => "*",
(KeyCode::Digit8, false) => "8",
(KeyCode::Digit9, true) => "(",
(KeyCode::Digit9, false) => "9",
(KeyCode::Digit0, true) => ")",
(KeyCode::Digit0, false) => "0",
(KeyCode::Minus, true) => "_",
(KeyCode::Minus, false) => "-",
(KeyCode::Equal, true) => "+",
(KeyCode::Equal, false) => "=",
(KeyCode::BracketLeft, true) => "{",
(KeyCode::BracketLeft, false) => "[",
(KeyCode::BracketRight, true) => "}",
(KeyCode::BracketRight, false) => "]",
(KeyCode::Backslash, true) => "|",
(KeyCode::Backslash, false) => "\\",
(KeyCode::Semicolon, true) => ":",
(KeyCode::Semicolon, false) => ";",
(KeyCode::Quote, true) => "\"",
(KeyCode::Quote, false) => "'",
(KeyCode::Comma, true) => "<",
(KeyCode::Comma, false) => ",",
(KeyCode::Period, true) => ">",
(KeyCode::Period, false) => ".",
(KeyCode::Slash, true) => "?",
(KeyCode::Slash, false) => "/",
(KeyCode::Backquote, true) => "~",
(KeyCode::Backquote, false) => "`",
_ => return None,
})
}
#[cfg(test)]
#[path = "key_events_tests.rs"]
mod tests;
@@ -1,5 +1,7 @@
use super::get_input_key;
use winit::keyboard::{Key::Character, SmolStr};
use winit::keyboard::Key::Character;
use winit::keyboard::{KeyCode, NativeKeyCode, PhysicalKey, SmolStr};
use super::{get_input_key, us_qwerty_fallback_for_chord};
#[test]
fn test_get_input_key() {
@@ -48,3 +50,112 @@ fn test_get_input_key() {
}
}
}
#[test]
fn us_qwerty_fallback_maps_letters() {
// Letters return lowercase regardless of shift; `get_input_key` applies the
// uppercase transform downstream.
let cases = [
(KeyCode::KeyA, "a"),
(KeyCode::KeyC, "c"),
(KeyCode::KeyV, "v"),
(KeyCode::KeyZ, "z"),
];
for (code, expected) in cases {
for shift in [false, true] {
assert_eq!(
us_qwerty_fallback_for_chord(&PhysicalKey::Code(code), shift),
Some(expected),
"expected {code:?} -> {expected} (shift={shift})",
);
}
}
}
#[test]
fn us_qwerty_fallback_maps_digits_and_punctuation() {
let cases = [
(KeyCode::Digit0, "0"),
(KeyCode::Digit9, "9"),
(KeyCode::Minus, "-"),
(KeyCode::Equal, "="),
(KeyCode::Slash, "/"),
(KeyCode::Backquote, "`"),
(KeyCode::Semicolon, ";"),
(KeyCode::Comma, ","),
];
for (code, expected) in cases {
assert_eq!(
us_qwerty_fallback_for_chord(&PhysicalKey::Code(code), false),
Some(expected),
"expected {code:?} -> {expected}",
);
}
}
#[test]
fn us_qwerty_fallback_maps_shifted_digits_and_punctuation() {
let cases = [
(KeyCode::Digit1, "!"),
(KeyCode::Digit2, "@"),
(KeyCode::Digit6, "^"),
(KeyCode::Digit9, "("),
(KeyCode::Digit0, ")"),
(KeyCode::Minus, "_"),
(KeyCode::Equal, "+"),
(KeyCode::BracketLeft, "{"),
(KeyCode::BracketRight, "}"),
(KeyCode::Backslash, "|"),
(KeyCode::Semicolon, ":"),
(KeyCode::Quote, "\""),
(KeyCode::Comma, "<"),
(KeyCode::Period, ">"),
(KeyCode::Slash, "?"),
(KeyCode::Backquote, "~"),
];
for (code, expected) in cases {
assert_eq!(
us_qwerty_fallback_for_chord(&PhysicalKey::Code(code), true),
Some(expected),
"expected {code:?} + shift -> {expected}",
);
}
}
#[test]
fn us_qwerty_fallback_returns_none_for_unmapped_keys() {
// Keys outside the chord-shortcut set should fall through so the original
// logical_key is preserved.
let unmapped = [
KeyCode::F1,
KeyCode::F13,
KeyCode::AltLeft,
KeyCode::ShiftRight,
KeyCode::ControlLeft,
KeyCode::Enter,
KeyCode::Escape,
KeyCode::ArrowUp,
KeyCode::Tab,
];
for code in unmapped {
for shift in [false, true] {
assert_eq!(
us_qwerty_fallback_for_chord(&PhysicalKey::Code(code), shift),
None,
"{code:?} should not have a chord fallback (shift={shift})",
);
}
}
}
#[test]
fn us_qwerty_fallback_returns_none_for_unidentified_physical_key() {
let unidentified = PhysicalKey::Unidentified(NativeKeyCode::Unidentified);
for shift in [false, true] {
assert_eq!(
us_qwerty_fallback_for_chord(&unidentified, shift),
None,
"unidentified key should not have a chord fallback (shift={shift})",
);
}
}
@@ -6,33 +6,35 @@ mod drag_drop_tests;
use std::collections::HashMap;
use std::mem::ManuallyDrop;
#[cfg(target_os = "linux")]
use crate::notification::RequestPermissionsOutcome;
use futures_util::future::LocalBoxFuture;
use futures_util::stream::AbortHandle;
use instant::{Duration, Instant};
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::{vec2f, Vector2F};
#[cfg(target_family = "wasm")]
use wasm_bindgen::JsCast;
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition};
use winit::event::Ime as ImeEvent;
use winit::event_loop::EventLoopProxy;
use winit::event::{
ElementState, Event, Ime as ImeEvent, MouseButton, StartCause, Touch, TouchPhase, WindowEvent,
};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
use winit::keyboard::{self, KeyCode};
use winit::window::WindowId as WinitWindowId;
use winit::{
event::{ElementState, Event, MouseButton, StartCause, Touch, TouchPhase, WindowEvent},
event_loop::{ActiveEventLoop, ControlFlow},
};
use self::key_events::convert_keyboard_input_event;
use super::app::ClipboardEvent;
use super::window::DEFAULT_TITLEBAR_HEIGHT;
#[cfg(windows)]
use super::windows::{add_network_connection_listener, WindowsNetworkConnectionPoint};
use super::CustomEvent;
use crate::actions::StandardAction;
use crate::event::ModifiersState;
use crate::platform::NotificationInfo;
use crate::platform::OperatingSystem;
use crate::platform::{
self,
app::{AppCallbackDispatcher, ApproveTerminateResult},
TerminationMode, WindowContext,
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use crate::notification::RequestPermissionsOutcome;
use crate::platform::app::{
AppCallbackDispatcher, ApproveTerminateResult, TerminationRequestSource,
};
use crate::platform::{self, NotificationInfo, OperatingSystem, TerminationMode, WindowContext};
use crate::r#async::Timer;
use crate::rendering::wgpu::renderer;
use crate::windowing::winit::app::RequestPermissionsCallback;
@@ -40,18 +42,6 @@ use crate::windowing::winit::window::MIN_WINDOW_SIZE;
use crate::Event::{ClearMarkedText, SetMarkedText, TypedCharacters};
use crate::{AppContext, WindowId};
#[cfg(target_family = "wasm")]
use wasm_bindgen::JsCast;
use super::app::ClipboardEvent;
use super::window::DEFAULT_TITLEBAR_HEIGHT;
use super::CustomEvent;
#[cfg(windows)]
use super::windows::{add_network_connection_listener, WindowsNetworkConnectionPoint};
use self::key_events::convert_keyboard_input_event;
/// This is the time duration beyond which clicks get treated as separate single clicks instead of
/// double-click, triple-click, etc.
const MULTI_CLICK_INTERVAL: Duration = Duration::from_millis(400);
@@ -544,7 +534,7 @@ impl EventLoop {
match evt {
Event::NewEvents(StartCause::Init) => {
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
let windowing_system =
if winit::platform::x11::ActiveEventLoopExtX11::is_x11(window_target) {
@@ -563,7 +553,7 @@ impl EventLoop {
}
// Start listening for various platform events.
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
super::linux::watch_suspend_resume_changes(
self.proxy.clone(),
@@ -755,13 +745,13 @@ impl EventLoop {
}
}
Event::UserEvent(CustomEvent::AboutToSleep) => {
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
self.prepare_for_sleep_on_linux(window_target);
self.callbacks.cpu_will_sleep();
}
Event::UserEvent(CustomEvent::ResumedFromSleep) => {
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
self.resume_from_sleep_on_linux();
self.callbacks.cpu_awakened();
@@ -987,7 +977,7 @@ impl EventLoop {
let window = downcast_window(window.as_ref());
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if crate::windowing::winit::linux::take_encountered_bad_match_from_dri3_fence_from_fd() {
log::warn!("Encountered a DRI3FenceFromFd error, forcing use of the NVIDIA GPU and recreating resources...");
self.downrank_non_nvidia_vulkan_adapters = true;
@@ -1298,12 +1288,28 @@ impl EventLoop {
}
let event_text = event.text.as_ref().map(|text| text.to_string());
let warp_ui_event =
convert_keyboard_input_event(event, window_state, is_synthetic)?;
Some(ConvertedEvent::KeyDownWithTypedCharacters {
chars: event_text,
event: warp_ui_event,
})
let event_state = event.state;
let is_unidentified_key =
matches!(event.logical_key, keyboard::Key::Unidentified(_));
match convert_keyboard_input_event(event, window_state, is_synthetic) {
Some(warp_ui_event) => Some(ConvertedEvent::KeyDownWithTypedCharacters {
chars: event_text,
event: warp_ui_event,
}),
None if is_unidentified_key
&& !is_synthetic
&& event_state == ElementState::Pressed =>
{
// Fallback for synthetic WM_CHAR messages injected by non-IME input methods
// (e.g. Unikey/EVKey on Windows for Vietnamese Telex/VNI). These input
// methods hook the keyboard at a low level and inject pre-composed
// characters via `SendInput` instead of going through the standard IME
// pipeline. The resulting key event has
// `logical_key == Key::Unidentified(...)`
event_text.map(|chars| ConvertedEvent::Event(TypedCharacters { chars }))
}
None => None,
}
}
WindowEvent::Resized(_) => Some(ConvertedEvent::Resize),
WindowEvent::Focused(is_focused) => {
@@ -1409,7 +1415,7 @@ impl EventLoop {
)
.await;
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
// On Linux, there is no concept of requesting notification permissions. This
// logic is hard-coded to always return an outcome of "Accepted".
@@ -1500,7 +1506,11 @@ impl EventLoop {
return ApproveTerminateResult::Terminate;
}
let approve_terminate_result = self.callbacks.should_terminate_app();
// Winit doesn't tell us why termination was requested, so assume the
// user asked (system-initiated shutdown detection is macOS-only for now).
let approve_terminate_result = self
.callbacks
.should_terminate_app(TerminationRequestSource::User);
if let ApproveTerminateResult::Terminate = approve_terminate_result {}
approve_terminate_result
}
@@ -1804,7 +1814,7 @@ impl EventLoop {
///
/// To work around this, we drop all rendering resources pre-suspend, and
/// re-create them post-resume.
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn prepare_for_sleep_on_linux(&mut self, window_target: &ActiveEventLoop) {
self.ui_app.update(|ctx| {
for window_id in ctx.window_ids() {
@@ -1822,7 +1832,7 @@ impl EventLoop {
///
/// See the [`Self::prepare_for_sleep_on_linux`] documentation for more
/// details.
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn resume_from_sleep_on_linux(&mut self) {
self.ui_app.update(|ctx| {
for window_id in ctx.window_ids() {
@@ -1902,7 +1912,7 @@ impl EventLoop {
/// synchronously during event processing may not work reliably on iOS Safari.
#[cfg(target_family = "wasm")]
fn refocus_canvas() {
use wasm_bindgen::{prelude::Closure, JsCast};
use wasm_bindgen::prelude::Closure;
// Defer focus to next frame to ensure we're outside the current event processing.
let callback = Closure::once(Box::new(|| {
+36 -42
View File
@@ -4,15 +4,11 @@ mod str_index_map;
mod swash_rasterizer;
mod text_layout;
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod linux;
#[cfg(target_os = "windows")]
mod windows;
use galaxyui_core::fonts::{Style, Weight};
#[cfg(target_os = "windows")]
use windows::loader;
use std::any::Any;
use std::collections::HashMap;
use std::ops::{DerefMut, Range};
@@ -22,49 +18,45 @@ use std::sync::Arc;
use anyhow::{anyhow, bail, Result};
use bimap::BiMap;
use pathfinder_geometry::{
rect::{RectF, RectI},
vector::Vector2F,
};
use resvg::usvg::fontdb;
use resvg::usvg::fontdb::Query;
use vec1::Vec1;
use cosmic_text::{
Align, Attrs, AttrsList, BidiParagraphs, LayoutGlyph, LayoutLine, ShapeLine, Shaping, Wrap,
};
use dashmap::{mapref::entry::Entry, DashMap};
use dashmap::mapref::entry::Entry;
use dashmap::DashMap;
use fontdb::Source;
use itertools::Itertools;
use parking_lot::RwLock;
use pathfinder_geometry::vector::{vec2f, vec2i, Vector2I};
use pathfinder_geometry::rect::{RectF, RectI};
use pathfinder_geometry::vector::{vec2f, vec2i, Vector2F, Vector2I};
use resvg::usvg::fontdb;
use resvg::usvg::fontdb::Query;
use vec1::Vec1;
use galaxyui_core::fonts::{Style, Weight};
#[cfg(target_os = "windows")]
use windows::loader;
use self::font_handle::{FontData, FontHandle};
use self::str_index_map::StrIndexMap;
use self::text_layout::{RunBuilder, TextStylesMap};
use crate::fonts::Metrics;
use crate::platform::{self};
use crate::text_layout::{CaretPosition, TextAlignment};
use crate::{
fonts::{
canvas::RasterFormat, FamilyId, FontId, GlyphId, Properties, RasterizedGlyph,
SubpixelAlignment,
},
platform::LineStyle,
rendering::GlyphConfig,
text_layout::{ClipConfig, Line, StyleAndFont, TextFrame},
use crate::fonts::canvas::RasterFormat;
use crate::fonts::{
FamilyId, FontId, GlyphId, Metrics, Properties, RasterizedGlyph, SubpixelAlignment,
};
use crate::platform::{self, LineStyle};
use crate::rendering::GlyphConfig;
use crate::text_layout::{CaretPosition, ClipConfig, Line, StyleAndFont, TextAlignment, TextFrame};
struct FontFamily {
name: String,
fonts: Vec<FontHandle>,
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod loader {
use anyhow::Result;
use super::*;
use crate::windowing::winit::fonts::linux::{Error, FontconfigLoader};
use anyhow::Result;
pub fn load_all_system_fonts() -> LoadedSystemFonts {
let manager = match FontconfigLoader::new() {
@@ -115,9 +107,8 @@ mod loader {
}
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
mod loader {
use super::*;
#[cfg(not(target_family = "wasm"))]
use crate::fonts::FontInfo;
@@ -159,7 +150,7 @@ fn load_font_family_from_bytes(name: &str, font_bytes: Vec<Vec<u8>>) -> Result<F
}
/// Enum indicating whether font validation should enforce that the font supports the english language.
#[cfg(any(target_os = "linux", target_os = "windows"))]
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))]
#[derive(Copy, Clone)]
enum ValidateFontSupportsEn {
Yes,
@@ -586,17 +577,20 @@ impl TextLayoutSystem {
);
total_height += line.height();
// We add 1 to the last caret position here to skip the newline character.
// Since we're working with separate lines within a text frame, there is guaranteed
// to be a newline to skip at the end of each iteration of the loop.
// The only exception is on the last iteration; in that case, we don't use this
// value later anyway.
line_glyph_start_index = line
.caret_positions
.last()
.map(|position| position.last_offset)
.unwrap_or(line_glyph_start_index)
+ 1;
// Only update line_glyph_start_index at paragraph boundaries (when there's a trailing
// newline). For soft-wrapped lines within the same paragraph, glyph.start values from
// cosmic-text are always byte offsets relative to the paragraph start, so
// line_glyph_start_index must stay at the paragraph's starting byte offset in the full
// text.
if has_trailing_newline {
// Convert the last caret's char index back to a byte index, then advance past the
// newline separator to get the next paragraph's byte offset.
line_glyph_start_index = line
.caret_positions
.last()
.and_then(|position| str_index_map.byte_index(position.last_offset + 1))
.unwrap_or(line_glyph_start_index);
}
// TODO(alokedesai): Properly clip multi-line text using the same strategy we use on mac.
// See https://github.com/warpdotdev/warp-internal/blob/91dfe429074c6129a6b5c1c57c55c1daf6d274a9/ui/src/platform/mac/text_layout.rs#L318-L359.
@@ -7,10 +7,11 @@
allow(dead_code)
)]
use owned_ttf_parser::{AsFaceRef, Face, FaceParsingError, OwnedFace};
use std::fs::File;
use std::path::PathBuf;
use owned_ttf_parser::{AsFaceRef, Face, FaceParsingError, OwnedFace};
/// A handle that wraps around a font face.
pub struct FontHandle {
data: FontData,
@@ -85,7 +86,7 @@ impl FontHandle {
}
}
/// Validates the the [`FontHandle`] is a parseable font.
/// Validates the [`FontHandle`] is a parseable font.
pub fn validate_font_data(&self) -> Result<(), Error> {
self.data.validate()
}
@@ -6,14 +6,8 @@
//! Handles can be converted to owned_ttf_parser::OwnedFace objects
//! by loading the fonts into memory.
use std::ffi::c_int;
use std::{collections::HashMap, ffi::CString};
use super::{
font_handle::{Error as FontDataError, FontHandle},
FontFamily, ValidateFontSupportsEn,
};
use crate::fonts::{FontInfo, Properties, Style, Weight};
use std::collections::HashMap;
use std::ffi::{c_int, CString};
use fontconfig::{
list_fonts, sort_fonts, FontSet, Fontconfig, ObjectSet, Pattern, FC_FAMILY, FC_FILE,
@@ -24,6 +18,10 @@ use fontconfig::{
};
use itertools::Itertools;
use super::font_handle::{Error as FontDataError, FontHandle};
use super::{FontFamily, ValidateFontSupportsEn};
use crate::fonts::{FontInfo, Properties, Style, Weight};
/// Manages font detection and handle generation.
///
/// Contains our font loading object, wrapping around fontconfig::FontConfig
@@ -1,14 +1,15 @@
//! Module that rasterizes text using `swash`.
use anyhow::{anyhow, Result};
use cosmic_text::{CacheKey, CacheKeyFlags};
use pathfinder_geometry::rect::RectI;
use pathfinder_geometry::vector::{vec2i, Vector2F, Vector2I};
use crate::fonts::canvas::{Canvas, RasterFormat};
use crate::fonts::{FontId, GlyphId, RasterizedGlyph, SubpixelAlignment};
use crate::platform::FontDB as _;
use crate::rendering::GlyphConfig;
use crate::windowing::winit::fonts::FontDB;
use anyhow::{anyhow, Result};
use cosmic_text::{CacheKey, CacheKeyFlags};
use pathfinder_geometry::rect::RectI;
use pathfinder_geometry::vector::{vec2i, Vector2F, Vector2I};
impl FontDB {
pub(super) fn glyph_raster_bounds(
@@ -1,8 +1,9 @@
use cosmic_text::LayoutGlyph;
use pathfinder_geometry::vector::vec2f;
use super::str_index_map::StrIndexMap;
use crate::fonts::FontId;
use crate::text_layout::{Glyph, Run, TextStyle};
use cosmic_text::LayoutGlyph;
use pathfinder_geometry::vector::vec2f;
/// Helper struct to construct [`Run`]s from a series of shaped glyphs.
pub(super) struct RunBuilder<'a> {
@@ -1,19 +1,18 @@
use super::{
font_handle::FontHandle, FontFamily, LoadedSystemFonts, TextLayoutSystem,
ValidateFontSupportsEn,
};
use crate::fonts::FontId;
use anyhow::Result;
use font_kit::loader::Loader as _;
use font_kit::{
family_name::FamilyName as FKFamilyName, properties::Properties as FKProperties,
properties::Style as FKStyle, properties::Weight as FKWeight, source::SystemSource as FKSource,
};
use itertools::Itertools;
use owned_ttf_parser::OwnedFace;
use std::collections::HashMap;
use std::sync::Arc;
use anyhow::Result;
use font_kit::family_name::FamilyName as FKFamilyName;
use font_kit::loader::Loader as _;
use font_kit::properties::{Properties as FKProperties, Style as FKStyle, Weight as FKWeight};
use font_kit::source::SystemSource as FKSource;
use itertools::Itertools;
use owned_ttf_parser::OwnedFace;
use super::font_handle::FontHandle;
use super::{FontFamily, LoadedSystemFonts, TextLayoutSystem, ValidateFontSupportsEn};
use crate::fonts::FontId;
const EN_US_LOCALE: &str = "en-US";
/// Windows symbol fonts that are used to render window control icons. We specifically do not do any
@@ -21,9 +20,8 @@ const EN_US_LOCALE: &str = "en-US";
const SYMBOL_ICON_FONTS: &[&str] = &["Segoe Fluent Icons", "Segoe MDL2 Assets"];
pub(crate) mod loader {
use crate::fonts::FontInfo;
use super::*;
use crate::fonts::FontInfo;
pub fn load_all_system_fonts() -> LoadedSystemFonts {
let source = font_kit::source::SystemSource::new();
@@ -3,8 +3,9 @@
//! For more information about X11 extensions and request codes/opcodes,
//! see: https://www.x.org/wiki/Development/Documentation/Protocol/OpCodes.
use lazy_static::lazy_static;
use std::sync::{Arc, Mutex};
use lazy_static::lazy_static;
use wgpu::rwh::{HasDisplayHandle, RawDisplayHandle};
use winit::event_loop::EventLoop;
use x11rb::protocol::xproto::ConnectionExt as _;
@@ -5,7 +5,8 @@ use arboard::{
};
use zbus::zvariant::NoneValue;
use crate::{clipboard::ClipboardContent, Clipboard};
use crate::clipboard::ClipboardContent;
use crate::Clipboard;
pub struct LinuxClipboard {
inner: LinuxClipboardInner,
@@ -2,7 +2,7 @@
///
/// Note: Most image processing functionality is tested in ui/src/clipboard_utils_tests.rs
/// to avoid duplication. These tests focus on Linux-specific clipboard behavior.
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod clipboard_tests {
use crate::clipboard::{Clipboard, ClipboardContent};
use crate::windowing::winit::linux::LinuxClipboard;
@@ -1,4 +1,6 @@
use std::{env, path::PathBuf};
use std::env;
use std::path::PathBuf;
use tini::Ini;
static CURSOR_DIR_NAME: &'static &str = &"cursors";
@@ -1,6 +1,7 @@
use super::CursorThemeCrawler;
use ::virtual_fs::{Stub, VirtualFS};
use super::CursorThemeCrawler;
#[test]
fn test_no_themes_found() {
VirtualFS::test("test_no_themes_found", |dirs, mut sandbox| {
@@ -1,8 +1,9 @@
use command::blocking::Command;
use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::process::Stdio;
use std::{env, fs, path};
use command::blocking::Command;
/// Attempt to find a running process that we believe is the window compositor.
///
/// The name comes from `/proc/$pid/comm`, and so it will be truncated to the first 15 chars of the
@@ -8,11 +8,10 @@ use futures::StreamExt as _;
use winit::event_loop::EventLoopProxy;
use zbus::{proxy, zvariant};
use crate::{
platform::SystemTheme,
r#async::{block_on, executor::Background, FutureExt as _},
windowing::winit::app::CustomEvent,
};
use crate::platform::SystemTheme;
use crate::r#async::executor::Background;
use crate::r#async::{block_on, FutureExt as _};
use crate::windowing::winit::app::CustomEvent;
const COLOR_SCHEME_SETTINGS_NAMESPACE: &str = "org.freedesktop.appearance";
const COLOR_SCHEME_SETTINGS_KEY: &str = "color-scheme";
@@ -2,7 +2,8 @@ use futures_lite::StreamExt;
use winit::event_loop::EventLoopProxy;
use zbus::proxy;
use crate::{r#async::executor::Background, windowing::winit::app::CustomEvent};
use crate::r#async::executor::Background;
use crate::windowing::winit::app::CustomEvent;
/// A zbus proxy for receiving network status signals from `NetworkManager`.
#[proxy(
@@ -2,7 +2,8 @@ use futures_lite::StreamExt;
use winit::event_loop::EventLoopProxy;
use zbus::proxy;
use crate::{r#async::executor::Background, windowing::winit::app::CustomEvent};
use crate::r#async::executor::Background;
use crate::windowing::winit::app::CustomEvent;
/// A zbus proxy for receiving PrepareForSleep signals from systemd-logind.
#[proxy(
+3 -3
View File
@@ -2,7 +2,7 @@ pub(crate) mod app;
pub mod delegate;
mod event_loop;
pub(crate) mod fonts;
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub mod linux;
mod notifications;
@@ -15,9 +15,9 @@ mod window;
pub mod windows;
use app::CustomEvent;
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub use app::WindowingSystem;
use event_loop::EventLoop;
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub use window::get_os_window_manager_name;
use window::Window;
@@ -1,9 +1,10 @@
use futures::FutureExt;
use winit::event_loop::EventLoopProxy;
use crate::notification::NotificationSendError;
use crate::windowing::winit::app::CustomEvent;
use crate::windowing::winit::notifications::NotificationInfo;
use crate::WindowId;
use futures::FutureExt;
use winit::event_loop::EventLoopProxy;
pub(super) async fn send_notification(
notification_info: NotificationInfo,
@@ -1,12 +1,14 @@
//! Module to display system desktop notifications through the winit windowing backend.
use crate::platform::NotificationInfo;
use crate::platform::{RequestNotificationPermissionsCallback, SendNotificationErrorCallback};
use crate::windowing::winit::app::CustomEvent;
use crate::{notification, WindowId};
use winit::event_loop::EventLoopProxy;
#[cfg_attr(target_os = "linux", path = "linux.rs")]
use crate::platform::{
NotificationInfo, RequestNotificationPermissionsCallback, SendNotificationErrorCallback,
};
use crate::windowing::winit::app::CustomEvent;
use crate::{notification, WindowId};
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), path = "linux.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
#[cfg_attr(target_family = "wasm", path = "wasm.rs")]
mod imp;
@@ -1,11 +1,11 @@
use crate::notification::NotificationSendError;
use crate::notification::RequestPermissionsOutcome;
use wasm_bindgen_futures::JsFuture;
use winit::event_loop::EventLoopProxy;
use crate::notification::{NotificationSendError, RequestPermissionsOutcome};
use crate::platform::NotificationInfo;
use crate::windowing::winit::app::RequestPermissionsCallback;
use crate::windowing::winit::CustomEvent;
use crate::WindowId;
use wasm_bindgen_futures::JsFuture;
use winit::event_loop::EventLoopProxy;
pub async fn send_notification(
notification_info: NotificationInfo,
@@ -1,9 +1,11 @@
use crate::notification::NotificationSendError;
use crate::windowing::winit::{app::CustomEvent, notifications::NotificationInfo};
use crate::WindowId;
use tauri_winrt_notification::Toast;
use winit::event_loop::EventLoopProxy;
use crate::notification::NotificationSendError;
use crate::windowing::winit::app::CustomEvent;
use crate::windowing::winit::notifications::NotificationInfo;
use crate::WindowId;
pub(super) async fn send_notification(
notification_info: NotificationInfo,
window_id: WindowId,
@@ -1,11 +1,10 @@
use anyhow::Result;
use super::*;
use crate::elements::DEFAULT_UI_LINE_HEIGHT_RATIO;
use crate::fonts::{collect_glyph_indices, init_fonts, Properties};
use crate::platform::FontDB as _;
use crate::{
elements::DEFAULT_UI_LINE_HEIGHT_RATIO,
text_layout::{TextStyle, DEFAULT_TOP_BOTTOM_RATIO},
};
use anyhow::Result;
use crate::text_layout::{TextStyle, DEFAULT_TOP_BOTTOM_RATIO};
const FONT_SIZE: f32 = 16.;
const FRAME_WIDTH: f32 = 80.;
@@ -592,3 +591,146 @@ fn all_lines_bounded(frame: &TextFrame, frame_width: f32) -> bool {
all_bounded && current_bounded
})
}
#[test]
fn test_softwrap_caret_positions_are_contiguous() -> Result<()> {
let (font_db, font_family) = init_fonts();
// A single paragraph (no newlines) long enough to soft-wrap at 200px.
let text = "The quick brown fox jumps over the lazy dog and then keeps running onward";
let frame = font_db.text_layout_system().layout_text(
text,
LineStyle {
font_size: FONT_SIZE,
line_height_ratio: DEFAULT_UI_LINE_HEIGHT_RATIO,
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
fixed_width_tab_size: None,
},
&[(
0..text.chars().count(),
StyleAndFont::new(font_family, Properties::default(), TextStyle::new()),
)],
200.,
f32::MAX,
TextAlignment::Left,
None,
);
// Should wrap onto multiple lines.
assert!(
frame.lines().len() >= 2,
"Expected at least 2 lines but got {}",
frame.lines().len()
);
// Collect all caret position start_offsets across all lines.
let all_caret_starts: Vec<usize> = frame
.lines()
.iter()
.flat_map(|line| line.caret_positions.iter().map(|c| c.start_offset))
.collect();
// The caret positions should be monotonically non-decreasing across all lines.
// Before the fix, the second/third wrapped line's carets would reset to 0.
for window in all_caret_starts.windows(2) {
assert!(
window[0] <= window[1],
"Caret positions are not monotonically non-decreasing: {} > {} (all: {:?})",
window[0],
window[1],
all_caret_starts
);
}
// The first caret should start at 0 and the last should correspond to near the end of the text.
assert_eq!(
*all_caret_starts.first().unwrap(),
0,
"First caret should start at 0"
);
let last_caret = frame
.lines()
.last()
.unwrap()
.caret_positions
.last()
.unwrap();
assert!(
last_caret.last_offset > 0,
"Last caret offset should be > 0"
);
// Each wrapped line's first caret should pick up where the previous line left off.
for i in 1..frame.lines().len() {
let prev_line = &frame.lines()[i - 1];
let curr_line = &frame.lines()[i];
if let (Some(prev_last), Some(curr_first)) = (
prev_line.caret_positions.last(),
curr_line.caret_positions.first(),
) {
assert!(
curr_first.start_offset > prev_last.start_offset,
"Line {}'s first caret ({}) should be after line {}'s last caret ({})",
i,
curr_first.start_offset,
i - 1,
prev_last.start_offset
);
}
}
Ok(())
}
#[test]
fn test_softwrap_caret_positions_multi_paragraph() -> Result<()> {
let (font_db, font_family) = init_fonts();
// Two paragraphs, each long enough to soft-wrap.
let text = "The quick brown fox jumps over the lazy dog repeatedly\nAnother paragraph that \
also wraps around when narrow";
let frame = font_db.text_layout_system().layout_text(
text,
LineStyle {
font_size: FONT_SIZE,
line_height_ratio: DEFAULT_UI_LINE_HEIGHT_RATIO,
baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
fixed_width_tab_size: None,
},
&[(
0..text.chars().count(),
StyleAndFont::new(font_family, Properties::default(), TextStyle::new()),
)],
200.,
f32::MAX,
TextAlignment::Left,
None,
);
// Should have multiple lines from wrapping.
assert!(
frame.lines().len() >= 3,
"Expected at least 3 lines but got {}",
frame.lines().len()
);
// Caret positions should be monotonically non-decreasing across ALL lines (including across
// the paragraph boundary).
let all_caret_starts: Vec<usize> = frame
.lines()
.iter()
.flat_map(|line| line.caret_positions.iter().map(|c| c.start_offset))
.collect();
for window in all_caret_starts.windows(2) {
assert!(
window[0] <= window[1],
"Caret positions are not monotonically non-decreasing across paragraphs: {} > {} (all: {:?})",
window[0],
window[1],
all_caret_starts
);
}
Ok(())
}
+5 -2
View File
@@ -1,8 +1,11 @@
use crate::{clipboard::ClipboardContent, Clipboard};
use js_sys::{Array, Object};
use wasm_bindgen::{self, prelude::*, JsCast};
use wasm_bindgen::prelude::*;
use wasm_bindgen::{self, JsCast};
use web_sys::{Blob, BlobPropertyBag};
use crate::clipboard::ClipboardContent;
use crate::Clipboard;
pub struct WebClipboard {
inner: web_sys::Clipboard,
saved_content: ClipboardContent,
+176 -68
View File
@@ -1,17 +1,15 @@
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod x11;
#[cfg(windows)]
mod windows_wm;
use std::cell::{Cell, OnceCell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
#[cfg(windows)]
use std::sync::LazyLock;
use std::{
cell::{Cell, OnceCell, RefCell},
rc::Rc,
};
use anyhow::{Context as _, Result};
use itertools::Itertools;
@@ -21,42 +19,33 @@ use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::{vec2f, Vector2F};
use wgpu::rwh::HasDisplayHandle;
use wgpu::{AdapterInfo, CompositeAlphaMode};
use winit::dpi::PhysicalPosition;
#[cfg(windows)]
use windows::Win32::Graphics::Dwm;
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size};
use winit::error::ExternalError;
use winit::event_loop::{ActiveEventLoop, EventLoopProxy, OwnedDisplayHandle};
#[cfg(not(target_family = "wasm"))]
use winit::monitor::MonitorHandle;
#[cfg(windows)]
use winit::platform::windows::{BackdropType, WindowExtWindows};
use winit::window::{CursorIcon, ResizeDirection, UserAttentionType, WindowLevel};
use winit::{
dpi::{LogicalPosition, LogicalSize, PhysicalSize, Position, Size},
window::Fullscreen,
};
use winit::window::{CursorIcon, Fullscreen, ResizeDirection, UserAttentionType, WindowLevel};
use super::app::CustomEvent;
#[cfg(windows)]
use super::windows::{get_system_caption_button_bounds, set_window_attribute, WindowAttributeErr};
#[cfg(not(target_family = "wasm"))]
use crate::platform::WindowBounds;
use crate::platform::{
self, Cursor, FullscreenState, GraphicsBackend, TerminationMode, WindowFocusBehavior,
WindowOptions, WindowStyle,
};
use crate::rendering::{
wgpu::{
adapter_has_rendering_offset_bug, from_wgpu_backend, renderer, to_wgpu_backend, Renderer,
Resources,
},
GPUPowerPreference, GlyphConfig, OnGPUDeviceSelected,
use crate::rendering::wgpu::{
adapter_has_rendering_offset_bug, from_wgpu_backend, renderer, to_wgpu_backend, Renderer,
Resources,
};
use crate::rendering::{GPUPowerPreference, GlyphConfig, OnGPUDeviceSelected};
use crate::windowing::WindowCallbacks;
use crate::{fonts, geometry, Scene};
use crate::{DisplayId, DisplayIdx, OptionalPlatformWindow, WindowId};
use super::app::CustomEvent;
#[cfg(windows)]
use super::windows::{get_system_caption_button_bounds, set_window_attribute, WindowAttributeErr};
#[cfg(windows)]
use windows::Win32::Graphics::Dwm;
use crate::{fonts, geometry, DisplayId, DisplayIdx, OptionalPlatformWindow, Scene, WindowId};
/// The inner margin from the edges of the window within which the mouse can drag to resize the
/// window. Note that this value is a logical size, not a physical size. It can be converted to a
@@ -67,20 +56,11 @@ const DRAG_RESIZE_MARGIN: f32 = 4.0;
#[cfg(windows)]
const IDI_ICON: u16 = 0x101;
cfg_if::cfg_if! {
if #[cfg(any(test, feature = "integration_tests"))] {
/// The window cannot be resized smaller than this.
/// TODO(CORE-1891) Instead of being hard-coded, this should be configurable by the user via
/// [`crate::platform::WindowOptions`].
#[cfg_attr(target_family = "wasm", allow(dead_code))]
pub(in crate::windowing::winit) const MIN_WINDOW_SIZE: LogicalSize<f64> =
LogicalSize::new(124., 34.);
} else {
#[cfg_attr(target_family = "wasm", allow(dead_code))]
pub(in crate::windowing::winit) const MIN_WINDOW_SIZE: LogicalSize<f64> =
LogicalSize::new(480., 192.);
}
}
#[cfg_attr(target_family = "wasm", allow(dead_code))]
pub(in crate::windowing::winit) const MIN_WINDOW_SIZE: LogicalSize<f64> = LogicalSize::new(
crate::windowing::MIN_WINDOW_WIDTH as f64,
crate::windowing::MIN_WINDOW_HEIGHT as f64,
);
lazy_static! {
static ref DEFAULT_WINDOW_SIZE: Vector2F = Vector2F::new(1280., 800.);
@@ -89,14 +69,53 @@ lazy_static! {
pub(crate) struct WindowManager {
windows: HashMap<WindowId, Rc<Window>>,
event_loop_proxy: EventLoopProxy<CustomEvent>,
window_ordering: Mutex<WindowOrderingState>,
/// We assume this won't change throughout the life of the Warp process.
os_window_manager_name: OnceCell<Option<String>>,
/// This is a client for talking to the Xorg server directly instead of through winit.
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
x11_manager: Option<x11::X11Manager>,
display_handle: OwnedDisplayHandle,
}
/// Manually-tracked window z-ordering. Winit has no native z-order API, so
/// `WindowManager` keeps this list in sync from its create / focus / hide /
/// remove callbacks and exposes it via `ordered_window_ids()`.
#[derive(Default)]
struct WindowOrderingState {
front_to_back_window_ids: Vec<WindowId>,
window_styles: HashMap<WindowId, WindowStyle>,
}
impl WindowOrderingState {
fn note_window_created(&mut self, window_id: WindowId, style: WindowStyle) {
self.window_styles.insert(window_id, style);
if style != WindowStyle::NotStealFocus {
self.move_to_front(window_id);
}
}
fn move_to_front(&mut self, window_id: WindowId) {
self.front_to_back_window_ids.retain(|id| *id != window_id);
self.front_to_back_window_ids.insert(0, window_id);
}
fn note_window_hidden(&mut self, window_id: WindowId) {
self.front_to_back_window_ids.retain(|id| *id != window_id);
}
fn note_window_removed(&mut self, window_id: WindowId) {
self.note_window_hidden(window_id);
self.window_styles.remove(&window_id);
}
fn has_positioned_no_focus_window(&self) -> bool {
self.front_to_back_window_ids
.iter()
.any(|id| self.window_styles.get(id) == Some(&WindowStyle::PositionedNoFocus))
}
}
impl WindowManager {
pub(crate) fn new(
event_loop_proxy: EventLoopProxy<CustomEvent>,
@@ -105,8 +124,9 @@ impl WindowManager {
Self {
windows: Default::default(),
event_loop_proxy,
window_ordering: Default::default(),
os_window_manager_name: Default::default(),
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
x11_manager: match x11::X11Manager::new() {
Ok(x11_manager) => Some(x11_manager),
Err(err) => {
@@ -125,7 +145,7 @@ impl WindowManager {
/// space. All our app's windows must be on the same screen, and hence will have the same scale
/// factor. For more in-depth explanation:
/// https://github.com/warpdotdev/warp-internal/pull/8431#discussion_r1460629912
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn get_x11_backing_scale_factor(&self) -> f32 {
use crate::platform::WindowContext;
@@ -144,12 +164,16 @@ impl platform::WindowManager for WindowManager {
window_options: WindowOptions,
callbacks: WindowCallbacks,
) -> Result<()> {
let style = window_options.style;
self.event_loop_proxy.send_event(CustomEvent::OpenWindow {
window_id,
window_options,
})?;
self.windows
.insert(window_id, Rc::new(super::window::Window::new(callbacks)));
self.window_ordering
.lock()
.note_window_created(window_id, style);
Ok(())
}
@@ -162,6 +186,7 @@ impl platform::WindowManager for WindowManager {
fn remove_window(&mut self, window_id: WindowId) {
self.windows.remove(&window_id);
self.window_ordering.lock().note_window_removed(window_id);
}
fn active_window_id(&self) -> Option<WindowId> {
@@ -216,6 +241,10 @@ impl platform::WindowManager for WindowManager {
}
}
if let Some(window_id) = next_active_window {
self.window_ordering.lock().move_to_front(window_id);
}
next_active_window
}
@@ -224,6 +253,7 @@ impl platform::WindowManager for WindowManager {
if let Some(window) = self.windows.get(&window_id) {
window.focus();
}
self.window_ordering.lock().move_to_front(window_id);
}
fn hide_app(&self) {
@@ -236,6 +266,7 @@ impl platform::WindowManager for WindowManager {
if let Some(window) = self.windows.get(&window_id) {
window.set_visible(false);
}
self.window_ordering.lock().note_window_hidden(window_id);
}
fn set_window_bounds(&self, window_id: WindowId, bound: RectF) {
@@ -244,6 +275,12 @@ impl platform::WindowManager for WindowManager {
}
}
fn set_window_alpha(&self, window_id: WindowId, alpha: f32) {
if let Some(window) = self.windows.get(&window_id) {
window.set_alpha(alpha);
}
}
fn set_all_windows_background_blur_radius(&self, _blur_radius_pixels: u8) {
// unsupported on Linux and Windows
// https://docs.rs/winit/latest/winit/window/struct.Window.html#method.set_blur
@@ -283,7 +320,7 @@ impl platform::WindowManager for WindowManager {
fn active_display_bounds(&self) -> RectF {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
self.x11_manager
.as_ref()
.and_then(|x11_manager| match x11_manager.get_active_monitor() {
@@ -305,7 +342,7 @@ impl platform::WindowManager for WindowManager {
fn active_display_id(&self) -> DisplayId {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
self.x11_manager
.as_ref()
.and_then(|x11_manager| match x11_manager.get_active_monitor() {
@@ -330,7 +367,7 @@ impl platform::WindowManager for WindowManager {
// never invalidates the cache. We need to drop down to X11 directly to ensure we read a
// fresh value.
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
self.x11_manager
.as_ref()
.and_then(|x11_manager| x11_manager.list_monitor_bounds().ok())
@@ -347,7 +384,7 @@ impl platform::WindowManager for WindowManager {
fn bounds_for_display_idx(&self, display_idx: DisplayIdx) -> Option<RectF> {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
let idx = match display_idx {
DisplayIdx::Primary => 0,
DisplayIdx::External(idx) => idx + 1,
@@ -388,11 +425,34 @@ impl platform::WindowManager for WindowManager {
.unwrap_or(false)
}
fn ordered_window_ids(&self) -> Vec<WindowId> {
let mut window_ordering = self.window_ordering.lock();
// Winit has no native z-order API, so we maintain this list manually.
// Prune windows that have since been hidden.
window_ordering
.front_to_back_window_ids
.retain(|window_id| {
self.windows
.get(window_id)
.is_some_and(|window| window.is_visible())
});
// Keep the list current by promoting the focused window to the front.
// Skip when a PositionedNoFocus (drag preview) window is present: it
// was moved to the front on creation and must stay there so that
// `cross_window_attach_target` can find it at index 0.
if !window_ordering.has_positioned_no_focus_window() {
if let Some(active_window_id) = self.active_window_id() {
window_ordering.move_to_front(active_window_id);
}
}
window_ordering.front_to_back_window_ids.clone()
}
fn os_window_manager_name(&self) -> Option<String> {
self.os_window_manager_name
.get_or_init(|| {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
get_os_window_manager_name_internal(self.x11_manager.as_ref())
} else {
None
@@ -403,12 +463,12 @@ impl platform::WindowManager for WindowManager {
}
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub fn get_os_window_manager_name() -> Option<String> {
get_os_window_manager_name_internal(x11::X11Manager::new().ok().as_ref())
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn get_os_window_manager_name_internal(x11_manager: Option<&x11::X11Manager>) -> Option<String> {
super::linux::look_for_wayland_compositor()
.or_else(|| x11_manager.and_then(|manager| manager.os_window_manager_name().ok()))
@@ -416,7 +476,7 @@ fn get_os_window_manager_name_internal(x11_manager: Option<&x11::X11Manager>) ->
fn is_tiling_window_manager(name: &str) -> bool {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
super::linux::is_tiling_window_manager(name)
} else {
let _ = name;
@@ -428,7 +488,7 @@ fn is_tiling_window_manager(name: &str) -> bool {
/// Some additional state we need to track in memory for integration tests.
struct IntegrationTestAppState {
/// A list of window IDs representing the order of visible windows, with
/// the frontmost window at the end of the list.
/// the frontmost window at the start of the list.
window_id_stack: Vec<WindowId>,
}
@@ -461,12 +521,13 @@ impl platform::WindowManager for IntegrationTestWindowManager {
window_options: WindowOptions,
callbacks: WindowCallbacks,
) -> Result<()> {
let window_will_be_focused = window_options.style != platform::WindowStyle::NotStealFocus;
let window_should_be_tracked = window_options.style != platform::WindowStyle::NotStealFocus;
self.window_manager
.open_window(window_id, window_options, callbacks)?;
if window_will_be_focused {
if window_should_be_tracked {
let mut app_state = self.app_state.lock();
app_state.window_id_stack.push(window_id);
app_state.window_id_stack.retain(|id| *id != window_id);
app_state.window_id_stack.insert(0, window_id);
}
Ok(())
}
@@ -481,7 +542,7 @@ impl platform::WindowManager for IntegrationTestWindowManager {
fn active_window_id(&self) -> Option<WindowId> {
self.app_is_active()
.then(|| self.app_state.lock().window_id_stack.last().cloned())
.then(|| self.app_state.lock().window_id_stack.first().cloned())
.flatten()
}
@@ -507,7 +568,7 @@ impl platform::WindowManager for IntegrationTestWindowManager {
// Move the window to the top of the stack.
app_state.window_id_stack.retain(|id| *id != window_id);
app_state.window_id_stack.push(window_id);
app_state.window_id_stack.insert(0, window_id);
}
fn hide_app(&self) {
@@ -527,6 +588,10 @@ impl platform::WindowManager for IntegrationTestWindowManager {
self.window_manager.set_window_bounds(window_id, bound)
}
fn set_window_alpha(&self, window_id: WindowId, alpha: f32) {
self.window_manager.set_window_alpha(window_id, alpha)
}
fn set_all_windows_background_blur_radius(&self, blur_radius_pixels: u8) {
self.window_manager
.set_all_windows_background_blur_radius(blur_radius_pixels)
@@ -582,6 +647,10 @@ impl platform::WindowManager for IntegrationTestWindowManager {
fn is_tiling_window_manager(&self) -> bool {
self.window_manager.is_tiling_window_manager()
}
fn ordered_window_ids(&self) -> Vec<WindowId> {
self.app_state.lock().window_id_stack.clone()
}
}
fn window_level_for_style(style: WindowStyle) -> WindowLevel {
@@ -614,7 +683,7 @@ struct Inner {
window: Arc<winit::window::Window>,
#[cfg(windows)]
is_cloaked: bool,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[cfg_attr(not(any(target_os = "linux", target_os = "freebsd")), allow(dead_code))]
gpu_power_preference: GPUPowerPreference,
backend_preference: Option<wgpu::Backend>,
rendering_resources: Option<RenderingResources>,
@@ -838,7 +907,7 @@ impl Window {
}
/// Drops the window's renderer and all associated resources.
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[cfg_attr(not(any(target_os = "linux", target_os = "freebsd")), allow(dead_code))]
pub fn drop_renderer(&self, display_handle: Box<dyn wgpu::wgt::WgpuHasDisplayHandle>) {
let mut inner = self.inner.borrow_mut();
let Some(inner) = inner.as_mut() else {
@@ -855,7 +924,7 @@ impl Window {
}
/// Recreates the window's renderer and all associated resources.
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[cfg_attr(not(any(target_os = "linux", target_os = "freebsd")), allow(dead_code))]
pub fn recreate_renderer(&self, downrank_non_nvidia_vulkan_adapters: bool) {
let mut inner = self.inner.borrow_mut();
let Some(inner) = inner.as_mut() else {
@@ -1080,15 +1149,13 @@ impl Window {
pub fn focus(&self) {
if let Some(Inner { window, level, .. }) = self.inner.borrow().as_ref() {
// Winit is a bit quirky here. Trying to focus a window which isn't visible will not
// make it visible. So, call `focus_window` if the window is visible, otherwise make it
// visible.
// make it visible. So, make it visible first if needed, then explicitly focus it.
if window.is_visible().unwrap_or(true) {
window.set_minimized(false);
window.focus_window();
} else {
// Setting visible to `true` will also focus it.
window.set_visible(true);
}
window.focus_window();
window.set_window_level(*level);
}
}
@@ -1153,6 +1220,30 @@ impl Window {
}
}
/// Sets the window's uniform opacity, where `1.0` is fully opaque and `0.0`
/// is fully transparent. Used to cheaply hide the cross-window tab-drag
/// preview while hovering over a target window, without changing the
/// window's z-order or focus. Best-effort: a no-op on platforms / windowing
/// systems that don't support per-window opacity (e.g. Wayland).
fn set_alpha(&self, alpha: f32) {
let inner = self.inner.borrow();
let Some(Inner { window, .. }) = inner.as_ref() else {
return;
};
#[cfg(windows)]
{
if let Err(err) = window.set_alpha(alpha) {
log::warn!("Failed to set window alpha: {err:#?}");
}
}
#[cfg(not(windows))]
{
// No per-window opacity support (e.g. wasm); avoid unused warnings.
let _ = (window, alpha);
}
}
fn set_title(&self, title: &str) {
if let Some(Inner { window, .. }) = self.inner.borrow().as_ref() {
window.set_title(title)
@@ -1186,8 +1277,7 @@ fn create_window(
_window_class: &Option<String>,
_tiling_window_manager: bool,
) -> Result<winit::window::Window> {
use winit::platform::web::WindowAttributesExtWebSys;
use winit::platform::web::WindowExtWebSys;
use winit::platform::web::{WindowAttributesExtWebSys, WindowExtWebSys};
use crate::platform::current::add_prevent_default_listener;
@@ -1338,7 +1428,7 @@ fn create_window(
FullscreenState::Normal => {}
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if let Some(window_class) = _window_class.as_deref() {
use winit::platform::x11::{WindowAttributesExtX11, WindowType};
@@ -1360,6 +1450,18 @@ fn create_window(
.create_window(window_attributes)
.map_err(Into::into);
#[cfg(target_os = "linux")]
if let Ok(window) = created_window.as_ref() {
use wgpu::rwh::RawDisplayHandle;
let is_x11 = matches!(
window_target.display_handle().map(|dh| dh.as_raw()),
Ok(RawDisplayHandle::Xlib(_)) | Ok(RawDisplayHandle::Xcb(_))
);
if is_x11 {
window.set_ime_allowed(true);
}
}
#[cfg(windows)]
{
use super::windows::WindowExt;
@@ -1386,7 +1488,10 @@ fn create_window(
// When launching a window from windows file explorer, it isn't given focus. We're considering
// this a winit quirk and forcing it to be focused.
if window_options.style != WindowStyle::NotStealFocus {
if !matches!(
window_options.style,
WindowStyle::NotStealFocus | WindowStyle::PositionedNoFocus
) {
window.focus_window();
}
@@ -1433,6 +1538,9 @@ fn create_window(
///
/// Returns the vertical difference of the adjustment, or None.
fn maybe_adjust_window_vertically(window: &winit::window::Window) -> Option<i32> {
if window.is_maximized() || window.fullscreen().is_some() {
return None;
}
let window_position = window.outer_position().ok()?;
let window_size = window.outer_size();
let bottom_of_window = window_position.y + window_size.height as i32;
@@ -1,17 +1,22 @@
use crate::platform::WindowManager as _;
use crate::windowing::winit::window::WindowManager;
use crate::{DisplayId, DisplayIdx};
use std::sync::Arc;
use anyhow::Result;
use itertools::Itertools as _;
use pathfinder_geometry::rect::RectF;
use std::sync::Arc;
use windows::Win32::Graphics::Gdi::{MonitorFromWindow, MONITOR_DEFAULTTONEAREST};
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use winit::monitor::MonitorHandle;
use winit::platform::windows::MonitorHandleExtWindows;
use winit::window::Window as WinitWindow;
use super::get_monitor_logical_bounds;
use crate::platform::WindowManager as _;
use crate::windowing::winit::window::WindowManager;
use crate::{DisplayId, DisplayIdx};
impl WindowManager {
/// Returns the active Warp window. This will return an error if a different app's window is
/// active.
fn get_active_window_handle(&self) -> Result<Arc<WinitWindow>> {
let window_id = &self
.active_window_id()
@@ -27,11 +32,42 @@ impl WindowManager {
Ok(winit_window_ref.window.clone())
}
fn get_current_monitor_handle(&self) -> Result<MonitorHandle> {
let winit_window_ref = self.get_active_window_handle()?;
winit_window_ref
.current_monitor()
.ok_or(anyhow::anyhow!("Unable to get current monitor"))
fn get_any_window_handle(&self) -> Result<Arc<WinitWindow>> {
self.windows
.values()
.find_map(|window| {
window
.inner
.try_borrow()
.ok()
.and_then(|borrow| borrow.as_ref().map(|inner| inner.window.clone()))
})
.ok_or_else(|| anyhow::anyhow!("No window handles available"))
}
/// Returns the monitor which contains the focused window ("key window" in MacOS parlance). It's
/// the window that receives and handles the keypress events.
fn get_foreground_monitor(&self) -> Result<MonitorHandle> {
let any_window = self.get_any_window_handle()?;
// Even if no window has foreground focus, MonitorFromWindow with
// MONITOR_DEFAULTTONEAREST will return the nearest/primary monitor.
let fg_hwnd = unsafe { GetForegroundWindow() };
let target_hmonitor = unsafe { MonitorFromWindow(fg_hwnd, MONITOR_DEFAULTTONEAREST) };
any_window
.available_monitors()
.find(|monitor| monitor.hmonitor() == target_hmonitor.0 as isize)
.ok_or_else(|| anyhow::anyhow!("Could not match foreground window's monitor"))
}
fn get_active_monitor(&self) -> Result<MonitorHandle> {
self.get_active_window_handle()
.and_then(|w| {
w.current_monitor()
.ok_or_else(|| anyhow::anyhow!("Unable to get current monitor"))
})
.or_else(|_| self.get_foreground_monitor())
}
pub(super) fn get_monitor_bounds_for_display_idx(&self, idx: DisplayIdx) -> Result<RectF> {
@@ -57,30 +93,30 @@ impl WindowManager {
}
fn get_primary_monitor_handle(&self) -> Result<MonitorHandle> {
let winit_window_ref = self.get_active_window_handle()?;
let winit_window_ref = self.get_any_window_handle()?;
winit_window_ref
.primary_monitor()
.ok_or(anyhow::anyhow!("No primary monitor found"))
}
pub(super) fn get_current_monitor_id(&self) -> Result<DisplayId> {
let active_monitor = self.get_current_monitor_handle()?;
let active_monitor = self.get_active_monitor()?;
let active_monitor_id = active_monitor.hmonitor();
Ok(DisplayId::from(active_monitor_id as usize))
}
fn get_available_monitors(&self) -> Result<Vec<MonitorHandle>> {
let winit_window_ref = self.get_active_window_handle()?;
let winit_window_ref = self.get_any_window_handle()?;
Ok(winit_window_ref.available_monitors().collect_vec())
}
pub(super) fn get_available_monitor_count(&self) -> Result<usize> {
let winit_window_ref = self.get_active_window_handle()?;
let winit_window_ref = self.get_any_window_handle()?;
Ok(winit_window_ref.available_monitors().count())
}
pub(super) fn get_active_monitor_logical_bounds(&self) -> Result<RectF> {
let active_monitor = self.get_current_monitor_handle()?;
let active_monitor = self.get_active_monitor()?;
Ok(get_monitor_logical_bounds(&active_monitor))
}
}
@@ -2,7 +2,8 @@ use std::ops::Not;
use arboard::{self, Clipboard as WindowsClipboardInner};
use crate::{clipboard::ClipboardContent, Clipboard};
use crate::clipboard::ClipboardContent;
use crate::Clipboard;
pub struct WindowsClipboard {
inner: WindowsClipboardInner,
@@ -4,8 +4,9 @@
/// to avoid duplication. These tests focus on Windows-specific clipboard behavior.
#[cfg(target_os = "windows")]
mod clipboard_tests {
use crate::clipboard::ClipboardContent;
use crate::windowing::winit::windows::clipboard::WindowsClipboard;
use crate::{clipboard::ClipboardContent, Clipboard};
use crate::Clipboard;
fn create_test_clipboard() -> Option<WindowsClipboard> {
WindowsClipboard::new().ok()
@@ -1,4 +1,3 @@
use crate::windowing::winit::app::CustomEvent;
use anyhow::Context;
use windows::core::{implement, Interface};
use windows::Win32::Networking::NetworkListManager::{
@@ -11,6 +10,8 @@ use windows::Win32::System::Com::{
COINIT_APARTMENTTHREADED,
};
use crate::windowing::winit::app::CustomEvent;
/// Implements the INetworkListManagerEvents trait so we can pass along connectivity events from Windows
/// OS to our winit event loop.
#[implement(INetworkListManagerEvents)]
@@ -1,7 +1,8 @@
use crate::platform::SystemTheme;
use winreg::enums::HKEY_CURRENT_USER;
use winreg::RegKey;
use crate::platform::SystemTheme;
const SYSTEM_THEME_SUBKEY_PATH: &str =
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
const LIGHT_MODE_SUBKEY_NAME: &str = "AppsUseLightTheme";
@@ -1,9 +1,10 @@
use super::window_attribute::get_window_attribute;
use super::WindowAttributeErr;
use windows::Win32::Foundation::RECT;
use windows::Win32::Graphics::Dwm;
use winit::window::Window as WinitWindow;
use super::window_attribute::get_window_attribute;
use super::WindowAttributeErr;
#[derive(Debug)]
pub struct SystemCaptionButtonData {
bounds: RECT,

Some files were not shown because too many files have changed in this diff Show More