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
+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,
@@ -1,10 +1,11 @@
use std::{ffi::c_void, mem::size_of};
use std::ffi::c_void;
use std::mem::size_of;
use thiserror::Error;
use wgpu::rwh;
use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Dwm::{self, DWMWINDOWATTRIBUTE};
use winit::raw_window_handle::HasWindowHandle;
use winit::raw_window_handle::RawWindowHandle;
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
use winit::window::Window as WinitWindow;
#[derive(Debug, Error)]
@@ -1,5 +1,9 @@
use windows::Win32::Foundation::{FALSE, HWND, TRUE};
use windows::Win32::Foundation::{COLORREF, FALSE, HWND, TRUE};
use windows::Win32::Graphics::Dwm::{DwmSetWindowAttribute, DWMWA_CLOAK};
use windows::Win32::UI::WindowsAndMessaging::{
GetWindowLongPtrW, SetLayeredWindowAttributes, SetWindowLongPtrW, GWL_EXSTYLE, LWA_ALPHA,
WS_EX_LAYERED,
};
use windows_core::BOOL;
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
use winit::window::Window;
@@ -16,6 +20,13 @@ pub enum Error {
pub trait WindowExt {
/// "Cloaks" the window. A cloaked window is one that is invisible, but can still be drawn to.
fn set_cloaked(&self, cloaked: bool) -> Result<(), Error>;
/// Sets the window's uniform opacity (`0.0` fully transparent, `1.0` fully
/// opaque) using a layered window. Unlike `set_cloaked` or hiding, this keeps
/// the window in the z-order and does not change focus, which is what the
/// cross-window tab-drag preview relies on while hovering over a target
/// window's tab bar.
fn set_alpha(&self, alpha: f32) -> Result<(), Error>;
}
impl WindowExt for Window {
@@ -39,4 +50,32 @@ impl WindowExt for Window {
Ok(())
}
fn set_alpha(&self, alpha: f32) -> Result<(), Error> {
let Ok(RawWindowHandle::Win32(handle)) = self
.window_handle()
.map(|window_handle| window_handle.as_raw())
else {
return Err(Error::InvalidWindowHandle);
};
let hwnd = HWND(handle.hwnd.get() as _);
let alpha_byte = (alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
// SAFETY: `hwnd` is a valid top-level window handle obtained from winit.
// `SetLayeredWindowAttributes` requires the `WS_EX_LAYERED` extended
// style, so add it if it isn't already present. We intentionally leave
// the style set afterwards: a fully-opaque (alpha 255) layered window
// composites identically on DWM, which avoids the repaint quirks of
// toggling the style off when restoring opacity.
unsafe {
let ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
if ex_style & (WS_EX_LAYERED.0 as isize) == 0 {
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, ex_style | (WS_EX_LAYERED.0 as isize));
}
SetLayeredWindowAttributes(hwnd, COLORREF(0), alpha_byte, LWA_ALPHA)?;
}
Ok(())
}
}