first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -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.;
|
||||
|
||||
@@ -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")] {
|
||||
|
||||
@@ -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(|| {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user