Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
use std::ops::Not;
|
||||
|
||||
use arboard::{self, Clipboard as WindowsClipboardInner};
|
||||
|
||||
use crate::{clipboard::ClipboardContent, Clipboard};
|
||||
|
||||
pub struct WindowsClipboard {
|
||||
inner: WindowsClipboardInner,
|
||||
}
|
||||
|
||||
impl WindowsClipboard {
|
||||
pub fn new() -> Result<Self, arboard::Error> {
|
||||
Ok(Self {
|
||||
inner: WindowsClipboardInner::new()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Clipboard for WindowsClipboard {
|
||||
fn write(&mut self, contents: ClipboardContent) {
|
||||
let set_result = if let Some(html) = &contents.html {
|
||||
self.inner.set().html(html, Some(&contents.plain_text))
|
||||
} else {
|
||||
self.inner.set().text(&contents.plain_text)
|
||||
};
|
||||
|
||||
if let Err(err) = set_result {
|
||||
if contents.html.is_some() {
|
||||
log::warn!("Unable to set clipboard HTML: {err:?}");
|
||||
} else {
|
||||
log::warn!("Unable to set clipboard text: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&mut self) -> ClipboardContent {
|
||||
let mut content = ClipboardContent {
|
||||
plain_text: self.inner.get().text().unwrap_or_default(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Try to get HTML content
|
||||
if let Ok(html) = self.inner.get().html() {
|
||||
content.html = html.is_empty().not().then_some(html);
|
||||
}
|
||||
|
||||
// Some environments provide HTML but do not provide a plaintext representation.
|
||||
// If that happens, derive a best-effort plaintext fallback from the HTML.
|
||||
if content.plain_text.trim().is_empty() {
|
||||
if let Some(html) = content.html.as_ref() {
|
||||
let derived = crate::clipboard_utils::strip_html_to_plain_text(html);
|
||||
if !derived.trim().is_empty() {
|
||||
content.plain_text = derived;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get file paths.
|
||||
content.paths = self.inner.get().file_list().ok().map(|list| {
|
||||
list.into_iter()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.collect()
|
||||
});
|
||||
|
||||
// Try to get image content from clipboard
|
||||
content.images = crate::clipboard_utils::read_images_from_clipboard(
|
||||
&mut self.inner,
|
||||
&content.html,
|
||||
&content.plain_text,
|
||||
);
|
||||
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "clipboard_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,90 @@
|
||||
/// Windows-specific clipboard tests.
|
||||
///
|
||||
/// Note: Most image processing functionality is tested in ui/src/clipboard_utils_tests.rs
|
||||
/// to avoid duplication. These tests focus on Windows-specific clipboard behavior.
|
||||
#[cfg(target_os = "windows")]
|
||||
mod clipboard_tests {
|
||||
use crate::windowing::winit::windows::clipboard::WindowsClipboard;
|
||||
use crate::{clipboard::ClipboardContent, Clipboard};
|
||||
|
||||
fn create_test_clipboard() -> Option<WindowsClipboard> {
|
||||
WindowsClipboard::new().ok()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_round_trip() {
|
||||
let mut clipboard = match create_test_clipboard() {
|
||||
Some(clipboard) => clipboard,
|
||||
None => {
|
||||
eprintln!("Skipping test - no clipboard available (headless environment)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let test_content = ClipboardContent::plain_text("Windows clipboard test".to_string());
|
||||
|
||||
// Write content
|
||||
clipboard.write(test_content.clone());
|
||||
|
||||
// Read it back
|
||||
let read_content = clipboard.read();
|
||||
|
||||
// Should get the same text back (in environments where clipboard works)
|
||||
if !read_content.plain_text.is_empty() {
|
||||
assert_eq!(read_content.plain_text, test_content.plain_text);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_html_content_handling() {
|
||||
let mut clipboard = match create_test_clipboard() {
|
||||
Some(clipboard) => clipboard,
|
||||
None => {
|
||||
eprintln!("Skipping test - no clipboard available (headless environment)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let test_content = ClipboardContent {
|
||||
plain_text: "Test text".to_string(),
|
||||
html: Some("<div>Test HTML</div>".to_string()),
|
||||
images: None,
|
||||
paths: None,
|
||||
};
|
||||
|
||||
// Write HTML content
|
||||
clipboard.write(test_content.clone());
|
||||
|
||||
// Read it back
|
||||
let read_content = clipboard.read();
|
||||
|
||||
// In environments where clipboard works, we should get content back
|
||||
// (the exact HTML may not be preserved depending on the system)
|
||||
if !read_content.is_empty() {
|
||||
assert!(!read_content.plain_text.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_content_handling() {
|
||||
let mut clipboard = match create_test_clipboard() {
|
||||
Some(clipboard) => clipboard,
|
||||
None => {
|
||||
eprintln!("Skipping test - no clipboard available (headless environment)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let empty_content = ClipboardContent::plain_text("".to_string());
|
||||
|
||||
// Writing empty content should not panic
|
||||
clipboard.write(empty_content);
|
||||
|
||||
// Reading should return valid ClipboardContent (may be empty or have previous content)
|
||||
let read_content = clipboard.read();
|
||||
|
||||
// Should always return a valid ClipboardContent struct
|
||||
// Test that the structure itself is valid, not the content
|
||||
assert!(matches!(read_content.images, None | Some(_)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
pub mod clipboard;
|
||||
mod network;
|
||||
mod registry;
|
||||
mod system_caption_buttons;
|
||||
mod window_attribute;
|
||||
mod window_ext;
|
||||
|
||||
pub use clipboard::*;
|
||||
pub use network::*;
|
||||
pub use registry::*;
|
||||
pub use system_caption_buttons::*;
|
||||
pub use window_attribute::*;
|
||||
pub use window_ext::WindowExt;
|
||||
@@ -0,0 +1,108 @@
|
||||
use crate::windowing::winit::app::CustomEvent;
|
||||
use anyhow::Context;
|
||||
use windows::core::{implement, Interface};
|
||||
use windows::Win32::Networking::NetworkListManager::{
|
||||
INetworkListManager, INetworkListManagerEvents, INetworkListManagerEvents_Impl,
|
||||
NetworkListManager, NLM_CONNECTIVITY, NLM_CONNECTIVITY_DISCONNECTED,
|
||||
NLM_CONNECTIVITY_IPV4_INTERNET, NLM_CONNECTIVITY_IPV6_INTERNET,
|
||||
};
|
||||
use windows::Win32::System::Com::{
|
||||
CoCreateInstance, CoInitializeEx, IConnectionPoint, IConnectionPointContainer, CLSCTX_ALL,
|
||||
COINIT_APARTMENTTHREADED,
|
||||
};
|
||||
|
||||
/// Implements the INetworkListManagerEvents trait so we can pass along connectivity events from Windows
|
||||
/// OS to our winit event loop.
|
||||
#[implement(INetworkListManagerEvents)]
|
||||
#[allow(non_camel_case_types)]
|
||||
struct WindowsNetworkListener {
|
||||
event_loop: winit::event_loop::EventLoopProxy<CustomEvent>,
|
||||
}
|
||||
|
||||
impl WindowsNetworkListener {
|
||||
fn new(event_loop: winit::event_loop::EventLoopProxy<CustomEvent>) -> Self {
|
||||
Self { event_loop }
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
impl INetworkListManagerEvents_Impl for WindowsNetworkListener_Impl {
|
||||
fn ConnectivityChanged(&self, new_connectivity: NLM_CONNECTIVITY) -> windows::core::Result<()> {
|
||||
// The NLM_CONNECTIVITY parameter is a bitmap. When it contains NLM_CONNECTIVITY_IPV4_INTERNET
|
||||
// or NLM_CONNECTIVITY_IPV6_INTERNET, there's a connection. When it's equal to
|
||||
// NLM_CONNECTIVITY_DISCONNECTED, it's a disconnection. Other arbitrary network events are ignored.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/netlistmgr/ne-netlistmgr-nlm_connectivity#syntax
|
||||
// let connected = new_connectivity.eq(&NLM_CONNECTIVITY_IPV6_INTERNET) || new_connectivity.eq(&NLM_CONNECTIVITY_IPV4_INTERNET);
|
||||
let connected = (new_connectivity.0
|
||||
& (NLM_CONNECTIVITY_IPV6_INTERNET.0 | NLM_CONNECTIVITY_IPV4_INTERNET.0))
|
||||
!= 0;
|
||||
let disconnected = new_connectivity.eq(&NLM_CONNECTIVITY_DISCONNECTED);
|
||||
|
||||
if connected {
|
||||
let _ = self.event_loop.send_event(CustomEvent::InternetConnected);
|
||||
} else if disconnected {
|
||||
let _ = self
|
||||
.event_loop
|
||||
.send_event(CustomEvent::InternetDisconnected);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WindowsNetworkConnectionPoint {
|
||||
connection_point: IConnectionPoint,
|
||||
cookie: u32,
|
||||
|
||||
#[allow(unused)]
|
||||
/// We keep the events interface around for the duration of the program because
|
||||
/// we're not sure we don't need it to keep living.
|
||||
events_interface: INetworkListManagerEvents,
|
||||
}
|
||||
|
||||
impl WindowsNetworkConnectionPoint {
|
||||
pub fn clean_up(&self) {
|
||||
unsafe {
|
||||
if let Err(e) = self.connection_point.Unadvise(self.cookie) {
|
||||
log::warn!("Failed to clean up network connection point: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_network_connection_listener(
|
||||
event_loop_proxy: winit::event_loop::EventLoopProxy<CustomEvent>,
|
||||
) -> anyhow::Result<WindowsNetworkConnectionPoint> {
|
||||
let network_listener = {
|
||||
unsafe {
|
||||
// This invocation matches winit exactly. We want to make sure we don't modify any winit invariants in the case that
|
||||
// winit also calls CoInitializeEx.
|
||||
// https://github.com/rust-windowing/winit/blob/953d9b426886749e2f88250f420c87db58080c97/src/platform_impl/windows/window.rs#L1386
|
||||
CoInitializeEx(None, COINIT_APARTMENTTHREADED)
|
||||
.ok()
|
||||
.context("Failed to initialize COM")?;
|
||||
|
||||
let events_interface: INetworkListManagerEvents =
|
||||
WindowsNetworkListener::new(event_loop_proxy).into();
|
||||
|
||||
let connection_point_container: IConnectionPointContainer =
|
||||
CoCreateInstance(&NetworkListManager, None, CLSCTX_ALL)
|
||||
.and_then(|network_manager: INetworkListManager| network_manager.cast())
|
||||
.context("Failed to construct IConnectionPointContainer")?;
|
||||
|
||||
let connection_point: IConnectionPoint = connection_point_container
|
||||
.FindConnectionPoint(&INetworkListManagerEvents::IID)
|
||||
.context("Failed to construct IConnectionPoint")?;
|
||||
|
||||
let cookie = connection_point
|
||||
.Advise(&events_interface)
|
||||
.context("Failed to attach point and sink")?;
|
||||
|
||||
WindowsNetworkConnectionPoint {
|
||||
connection_point,
|
||||
cookie,
|
||||
events_interface,
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(network_listener)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::platform::SystemTheme;
|
||||
use winreg::enums::HKEY_CURRENT_USER;
|
||||
use winreg::RegKey;
|
||||
|
||||
const SYSTEM_THEME_SUBKEY_PATH: &str =
|
||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
const LIGHT_MODE_SUBKEY_NAME: &str = "AppsUseLightTheme";
|
||||
|
||||
/// Retrieves the system theme from the Windows Registry.
|
||||
/// https://github.com/wez/wezterm/blob/b8f94c474ce48ac195b51c1aeacf41ae049b774e/window/src/os/windows/connection.rs#L42
|
||||
pub fn get_system_theme() -> Result<SystemTheme, std::io::Error> {
|
||||
let theme_subkey = RegKey::predef(HKEY_CURRENT_USER).open_subkey(SYSTEM_THEME_SUBKEY_PATH)?;
|
||||
let theme_value = theme_subkey.get_value::<u32, _>(LIGHT_MODE_SUBKEY_NAME)?;
|
||||
match theme_value {
|
||||
1 => Ok(SystemTheme::Light),
|
||||
0 => Ok(SystemTheme::Dark),
|
||||
_ => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("System theme value {theme_value:?} was invalid"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SystemCaptionButtonData {
|
||||
bounds: RECT,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SystemCaptionButtonSide {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl SystemCaptionButtonData {
|
||||
pub fn total_width(&self) -> i32 {
|
||||
self.bounds.right - self.bounds.left
|
||||
}
|
||||
|
||||
pub fn side(&self) -> SystemCaptionButtonSide {
|
||||
if self.bounds.left == 0 {
|
||||
SystemCaptionButtonSide::Left
|
||||
} else {
|
||||
SystemCaptionButtonSide::Right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the system caption button's bounds using the window's
|
||||
/// CAPTION_BUTTON_BOUNDS attribute.
|
||||
pub fn get_system_caption_button_bounds(
|
||||
window: &WinitWindow,
|
||||
) -> Result<SystemCaptionButtonData, WindowAttributeErr> {
|
||||
let caption_button_bounds = get_window_attribute(window, Dwm::DWMWA_CAPTION_BUTTON_BOUNDS)?;
|
||||
|
||||
Ok(SystemCaptionButtonData {
|
||||
bounds: caption_button_bounds,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use std::{ffi::c_void, 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::window::Window as WinitWindow;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WindowAttributeErr {
|
||||
#[error(transparent)]
|
||||
HandleError(#[from] rwh::HandleError),
|
||||
#[error(transparent)]
|
||||
Win32Error(#[from] windows::core::Error),
|
||||
}
|
||||
|
||||
/// Uses the `windows` crate to fetch a specific window attribute.
|
||||
/// First, we translate the Winit window object to a native Windows HWND handle.
|
||||
/// Then, we invoke the Device Window Manager (DWM)'s `DwmGetWindowAttribute`
|
||||
/// function for the attribute in question.
|
||||
pub fn get_window_attribute<T>(
|
||||
window: &WinitWindow,
|
||||
attribute_name: DWMWINDOWATTRIBUTE,
|
||||
) -> Result<T, WindowAttributeErr>
|
||||
where
|
||||
T: Default,
|
||||
{
|
||||
let hwnd_handle = to_hwnd(window)?;
|
||||
let mut result_destination: T = T::default();
|
||||
let window_attribute_result = unsafe {
|
||||
let result_address = core::ptr::addr_of_mut!(result_destination);
|
||||
Dwm::DwmGetWindowAttribute(
|
||||
hwnd_handle,
|
||||
attribute_name,
|
||||
result_address as *mut c_void,
|
||||
size_of::<T>().try_into().unwrap(),
|
||||
)
|
||||
};
|
||||
Ok(window_attribute_result.map(|_| result_destination)?)
|
||||
}
|
||||
|
||||
/// Uses the `windows` crate to set a specific window attribute.
|
||||
/// First, we translate the Winit window object to a native Windows HWND handle.
|
||||
/// Then, we invoke the Device Window Manager (DWM)'s `DwmSetWindowAttribute`
|
||||
/// function for the attribute in question.
|
||||
pub fn set_window_attribute<T>(
|
||||
window: &WinitWindow,
|
||||
attribute_name: DWMWINDOWATTRIBUTE,
|
||||
value: T,
|
||||
) -> Result<(), WindowAttributeErr> {
|
||||
let hwnd_handle = to_hwnd(window)?;
|
||||
let window_attribute_result = unsafe {
|
||||
Dwm::DwmSetWindowAttribute(
|
||||
hwnd_handle,
|
||||
attribute_name,
|
||||
core::ptr::addr_of!(value) as *const c_void,
|
||||
size_of::<T>().try_into().unwrap(),
|
||||
)
|
||||
};
|
||||
Ok(window_attribute_result?)
|
||||
}
|
||||
|
||||
fn to_hwnd(window: &WinitWindow) -> Result<HWND, rwh::HandleError> {
|
||||
window
|
||||
.window_handle()
|
||||
.and_then(|handle| match handle.as_raw() {
|
||||
RawWindowHandle::Win32(handle) => Ok(handle),
|
||||
_ => Err(rwh::HandleError::NotSupported),
|
||||
})
|
||||
.map(|handle| HWND(handle.hwnd.get() as *mut c_void))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use windows::Win32::Foundation::{FALSE, HWND, TRUE};
|
||||
use windows::Win32::Graphics::Dwm::{DwmSetWindowAttribute, DWMWA_CLOAK};
|
||||
use windows_core::BOOL;
|
||||
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
use winit::window::Window;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Invalid WindowHandle")]
|
||||
InvalidWindowHandle,
|
||||
#[error("Unknown error")]
|
||||
Other(#[from] windows::core::Error),
|
||||
}
|
||||
|
||||
/// Extension trait for Windows specific logic on a [`winit::window::Window`].
|
||||
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>;
|
||||
}
|
||||
|
||||
impl WindowExt for Window {
|
||||
fn set_cloaked(&self, cloaked: bool) -> Result<(), Error> {
|
||||
let Ok(RawWindowHandle::Win32(handle)) = self
|
||||
.window_handle()
|
||||
.map(|window_handle| window_handle.as_raw())
|
||||
else {
|
||||
return Err(Error::InvalidWindowHandle);
|
||||
};
|
||||
|
||||
let value = if cloaked { TRUE } else { FALSE };
|
||||
unsafe {
|
||||
DwmSetWindowAttribute(
|
||||
HWND(handle.hwnd.get() as _),
|
||||
DWMWA_CLOAK,
|
||||
&value as *const BOOL as *const _,
|
||||
size_of::<BOOL>() as u32,
|
||||
)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user