Add: windows mvp - transparent bugs not fixed
This commit is contained in:
252
crates/sprimo-platform/src/lib.rs
Normal file
252
crates/sprimo-platform/src/lib.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use sprimo_protocol::v1::CapabilityFlags;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PlatformError {
|
||||
#[error("operation unsupported on this platform")]
|
||||
Unsupported,
|
||||
#[error("platform operation failed: {0}")]
|
||||
Message(String),
|
||||
}
|
||||
|
||||
pub trait PlatformAdapter: Send + Sync {
|
||||
fn capabilities(&self) -> CapabilityFlags;
|
||||
fn attach_window_handle(&self, handle: isize) -> Result<(), PlatformError>;
|
||||
fn set_click_through(&self, enabled: bool) -> Result<(), PlatformError>;
|
||||
fn set_always_on_top(&self, enabled: bool) -> Result<(), PlatformError>;
|
||||
fn set_visible(&self, visible: bool) -> Result<(), PlatformError>;
|
||||
fn set_window_position(&self, x: f32, y: f32) -> Result<(), PlatformError>;
|
||||
}
|
||||
|
||||
pub fn create_adapter() -> Box<dyn PlatformAdapter> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
return Box::new(windows::WindowsAdapter::default());
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Box::new(NoopAdapter::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NoopAdapter {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl PlatformAdapter for NoopAdapter {
|
||||
fn capabilities(&self) -> CapabilityFlags {
|
||||
CapabilityFlags::default()
|
||||
}
|
||||
|
||||
fn attach_window_handle(&self, _handle: isize) -> Result<(), PlatformError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_click_through(&self, _enabled: bool) -> Result<(), PlatformError> {
|
||||
Err(PlatformError::Unsupported)
|
||||
}
|
||||
|
||||
fn set_always_on_top(&self, _enabled: bool) -> Result<(), PlatformError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_visible(&self, _visible: bool) -> Result<(), PlatformError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_window_position(&self, _x: f32, _y: f32) -> Result<(), PlatformError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[allow(unsafe_code)]
|
||||
mod windows {
|
||||
use crate::{PlatformAdapter, PlatformError};
|
||||
use sprimo_protocol::v1::CapabilityFlags;
|
||||
use std::ffi::c_void;
|
||||
use std::sync::Mutex;
|
||||
use windows::Win32::Foundation::{COLORREF, GetLastError, HWND};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetWindowLongPtrW, SetLayeredWindowAttributes, SetWindowLongPtrW, SetWindowPos,
|
||||
ShowWindow, GWL_EXSTYLE, HWND_NOTOPMOST, HWND_TOPMOST, LWA_COLORKEY, SW_HIDE, SW_SHOW,
|
||||
SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WINDOW_EX_STYLE, WS_EX_LAYERED,
|
||||
WS_EX_TRANSPARENT,
|
||||
};
|
||||
|
||||
const COLOR_KEY_RGB: [u8; 3] = [255, 0, 255];
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WindowsAdapter {
|
||||
hwnd: Mutex<Option<isize>>,
|
||||
}
|
||||
|
||||
impl WindowsAdapter {
|
||||
fn enable_color_key_transparency(hwnd: HWND) -> Result<(), PlatformError> {
|
||||
// SAFETY: `hwnd` comes from a real native window handle attached during startup.
|
||||
let current_bits = unsafe { GetWindowLongPtrW(hwnd, GWL_EXSTYLE) };
|
||||
let mut style = WINDOW_EX_STYLE(current_bits as u32);
|
||||
style |= WS_EX_LAYERED;
|
||||
|
||||
// SAFETY: We pass the same valid window handle and write a computed style bitmask.
|
||||
let previous = unsafe { SetWindowLongPtrW(hwnd, GWL_EXSTYLE, style.0 as isize) };
|
||||
if previous == 0 {
|
||||
// SAFETY: Calling `GetLastError` immediately after failed Win32 API is valid.
|
||||
let code = unsafe { GetLastError().0 };
|
||||
if code != 0 {
|
||||
return Err(PlatformError::Message(format!(
|
||||
"SetWindowLongPtrW(layered) failed: {}",
|
||||
code
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let key = COLORREF(
|
||||
u32::from(COLOR_KEY_RGB[0])
|
||||
| (u32::from(COLOR_KEY_RGB[1]) << 8)
|
||||
| (u32::from(COLOR_KEY_RGB[2]) << 16),
|
||||
);
|
||||
// SAFETY: `hwnd` is valid and colorkey transparency is configured on a layered window.
|
||||
let result = unsafe { SetLayeredWindowAttributes(hwnd, key, 0, LWA_COLORKEY) };
|
||||
if let Err(err) = result {
|
||||
// SAFETY: Calling `GetLastError` immediately after failed Win32 API is valid.
|
||||
let code = unsafe { GetLastError().0 };
|
||||
return Err(PlatformError::Message(format!(
|
||||
"SetLayeredWindowAttributes(colorkey) failed: {} ({})",
|
||||
code, err
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_click_through_style(
|
||||
hwnd: HWND,
|
||||
click_through: Option<bool>,
|
||||
) -> Result<(), PlatformError> {
|
||||
// SAFETY: `hwnd` comes from a real native window handle attached during startup.
|
||||
let current_bits = unsafe { GetWindowLongPtrW(hwnd, GWL_EXSTYLE) };
|
||||
let mut style = WINDOW_EX_STYLE(current_bits as u32);
|
||||
if let Some(enabled) = click_through {
|
||||
if enabled {
|
||||
style |= WS_EX_TRANSPARENT;
|
||||
} else {
|
||||
style &= !WS_EX_TRANSPARENT;
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: We pass the same valid window handle and write a computed style bitmask.
|
||||
let previous = unsafe { SetWindowLongPtrW(hwnd, GWL_EXSTYLE, style.0 as isize) };
|
||||
if previous == 0 {
|
||||
// SAFETY: Calling `GetLastError` immediately after failed Win32 API is valid.
|
||||
let code = unsafe { GetLastError().0 };
|
||||
if code != 0 {
|
||||
return Err(PlatformError::Message(format!(
|
||||
"SetWindowLongPtrW failed: {}",
|
||||
code
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn with_hwnd<F>(&self, callback: F) -> Result<(), PlatformError>
|
||||
where
|
||||
F: FnOnce(HWND) -> Result<(), PlatformError>,
|
||||
{
|
||||
let guard = self
|
||||
.hwnd
|
||||
.lock()
|
||||
.map_err(|_| PlatformError::Message("window handle lock poisoned".to_string()))?;
|
||||
let hwnd = guard.ok_or_else(|| {
|
||||
PlatformError::Message("window handle not attached".to_string())
|
||||
})?;
|
||||
callback(HWND(hwnd as *mut c_void))
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformAdapter for WindowsAdapter {
|
||||
fn capabilities(&self) -> CapabilityFlags {
|
||||
CapabilityFlags {
|
||||
supports_click_through: true,
|
||||
supports_transparency: true,
|
||||
supports_tray: false,
|
||||
supports_global_hotkey: true,
|
||||
supports_skip_taskbar: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_window_handle(&self, handle: isize) -> Result<(), PlatformError> {
|
||||
{
|
||||
let mut guard = self.hwnd.lock().map_err(|_| {
|
||||
PlatformError::Message("window handle lock poisoned".to_string())
|
||||
})?;
|
||||
*guard = Some(handle);
|
||||
}
|
||||
self.with_hwnd(Self::enable_color_key_transparency)
|
||||
}
|
||||
|
||||
fn set_click_through(&self, enabled: bool) -> Result<(), PlatformError> {
|
||||
self.with_hwnd(|hwnd| Self::apply_click_through_style(hwnd, Some(enabled)))
|
||||
}
|
||||
|
||||
fn set_always_on_top(&self, enabled: bool) -> Result<(), PlatformError> {
|
||||
self.with_hwnd(|hwnd| {
|
||||
let insert_after = if enabled { HWND_TOPMOST } else { HWND_NOTOPMOST };
|
||||
// SAFETY: `hwnd` is valid and flags request only z-order update.
|
||||
let result = unsafe {
|
||||
SetWindowPos(hwnd, insert_after, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE)
|
||||
};
|
||||
if let Err(err) = result {
|
||||
// SAFETY: Calling `GetLastError` immediately after failed Win32 API is valid.
|
||||
let code = unsafe { GetLastError().0 };
|
||||
return Err(PlatformError::Message(format!(
|
||||
"SetWindowPos(topmost) failed: {} ({})",
|
||||
code, err
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn set_visible(&self, visible: bool) -> Result<(), PlatformError> {
|
||||
self.with_hwnd(|hwnd| {
|
||||
let command = if visible { SW_SHOW } else { SW_HIDE };
|
||||
// SAFETY: `hwnd` is valid and `ShowWindow` is safe with these standard commands.
|
||||
unsafe {
|
||||
let _ = ShowWindow(hwnd, command);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn set_window_position(&self, x: f32, y: f32) -> Result<(), PlatformError> {
|
||||
self.with_hwnd(|hwnd| {
|
||||
// SAFETY: `hwnd` is valid and this call updates only position.
|
||||
let result = unsafe {
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
HWND(std::ptr::null_mut()),
|
||||
x.round() as i32,
|
||||
y.round() as i32,
|
||||
0,
|
||||
0,
|
||||
SWP_NOSIZE | SWP_NOZORDER,
|
||||
)
|
||||
};
|
||||
if let Err(err) = result {
|
||||
// SAFETY: Calling `GetLastError` immediately after failed Win32 API is valid.
|
||||
let code = unsafe { GetLastError().0 };
|
||||
return Err(PlatformError::Message(format!(
|
||||
"SetWindowPos(move) failed: {} ({})",
|
||||
code, err
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user