use std::cell::RefCell;
use std::ptr::{null, null_mut};
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, SIZE, WPARAM};
use windows_sys::Win32::Graphics::Gdi::{
SelectObject, AC_SRC_ALPHA, AC_SRC_OVER, BLENDFUNCTION, HGDIOBJ,
};
use windows_sys::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, ShowWindow, UpdateLayeredWindow, SW_HIDE, SW_SHOWNOACTIVATE,
ULW_ALPHA, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_EX_TRANSPARENT,
WS_POPUP,
};
use winspaces_common::WindowRect;
use winspaces_win32::dpi::{px, scale_for_point};
use winspaces_win32::gdi::color::premultiply;
use winspaces_win32::gdi::guard::{DibSection, MemDc, ScreenDc};
use winspaces_win32::module::app_instance;
use winspaces_win32::text::encode_wide;
use winspaces_win32::window_class::register_class;
use crate::space_indicator::geometry::round_rect_coverage;
use crate::theme::preview_accent;
const CLASS_NAME: &str = "WinSpacesTilingPreview";
const RADIUS: i32 = 8;
const BORDER_W: i32 = 2;
const FILL_ALPHA: f32 = 64.0;
const BORDER_ALPHA: f32 = 230.0;
struct Preview {
hwnd: HWND,
mem_dc: Option<MemDc>,
dib: Option<DibSection>,
prev_bmp: HGDIOBJ,
visible: bool,
painted: Option<(RECT, Vec<WindowRect>)>,
}
impl Preview {
const fn new() -> Self {
Self {
hwnd: null_mut(),
mem_dc: None,
dib: None,
prev_bmp: null_mut(),
visible: false,
painted: None,
}
}
fn release_surface(&mut self) {
if let Some(dc) = &self.mem_dc {
if !self.prev_bmp.is_null() {
unsafe {
SelectObject(dc.handle(), self.prev_bmp);
}
}
}
self.prev_bmp = null_mut();
self.dib = None;
self.mem_dc = None;
self.painted = None;
}
}
thread_local! {
static PREVIEW: RefCell<Preview> = const { RefCell::new(Preview::new()) };
}
pub fn show_preview(work: &RECT, rects: &[WindowRect]) {
let width = work.right - work.left;
let height = work.bottom - work.top;
if width <= 0 || height <= 0 || rects.is_empty() {
return;
}
let unchanged = PREVIEW.with(|s| {
let p = s.borrow();
p.visible
&& p.painted.as_ref().is_some_and(|(w, r)| {
w.left == work.left
&& w.top == work.top
&& w.right == work.right
&& w.bottom == work.bottom
&& r == rects
})
});
if unchanged {
return;
}
unsafe {
let hwnd = PREVIEW.with(|s| s.borrow().hwnd);
let hwnd = if hwnd.is_null() {
let created = create_window();
if created.is_null() {
return;
}
PREVIEW.with(|s| s.borrow_mut().hwnd = created);
created
} else {
hwnd
};
let Some(screen_dc) = ScreenDc::new(null_mut()) else {
return;
};
PREVIEW.with(|s| s.borrow_mut().release_surface());
let Some(mem_dc) = MemDc::new(screen_dc.handle()) else {
return;
};
let Some(mut dib) = DibSection::new(screen_dc.handle(), width, height) else {
return;
};
let prev_bmp = SelectObject(mem_dc.handle(), dib.as_raw());
let center = POINT {
x: (work.left + work.right) / 2,
y: (work.top + work.bottom) / 2,
};
paint_rects(&mut dib, work, rects, scale_for_point(center));
let was_visible = PREVIEW.with(|s| {
let mut p = s.borrow_mut();
p.mem_dc = Some(mem_dc);
p.dib = Some(dib);
p.prev_bmp = prev_bmp;
p.painted = Some((*work, rects.to_vec()));
let was = p.visible;
p.visible = true;
was
});
let blend = BLENDFUNCTION {
BlendOp: AC_SRC_OVER as u8,
BlendFlags: 0,
SourceConstantAlpha: 255,
AlphaFormat: AC_SRC_ALPHA as u8,
};
let pos = POINT {
x: work.left,
y: work.top,
};
let size = SIZE {
cx: width,
cy: height,
};
let src = POINT { x: 0, y: 0 };
PREVIEW.with(|s| {
let p = s.borrow();
if let Some(dc) = &p.mem_dc {
UpdateLayeredWindow(
p.hwnd,
null_mut(),
&pos,
&size,
dc.handle(),
&src,
0,
&blend,
ULW_ALPHA,
);
}
});
if !was_visible {
ShowWindow(hwnd, SW_SHOWNOACTIVATE);
}
}
}
pub fn hide_preview() {
let hwnd = PREVIEW.with(|s| {
let mut p = s.borrow_mut();
if !p.visible && p.dib.is_none() {
return null_mut();
}
p.visible = false;
p.release_surface();
p.hwnd
});
if !hwnd.is_null() {
unsafe {
ShowWindow(hwnd, SW_HIDE);
}
}
}
unsafe fn create_window() -> HWND {
register_class(CLASS_NAME, Some(preview_wnd_proc));
let class_name = encode_wide(CLASS_NAME);
CreateWindowExW(
WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
class_name.as_ptr(),
null(),
WS_POPUP,
0,
0,
0,
0,
null_mut(),
null_mut(),
app_instance(),
null_mut(),
)
}
fn paint_rects(dib: &mut DibSection, work: &RECT, rects: &[WindowRect], scale: f32) {
let width = dib.width;
let height = dib.height;
let radius = px(scale, RADIUS) as f32;
let border_w = (px(scale, BORDER_W) as f32).max(1.0);
let accent = preview_accent();
let (ar, ag, ab) = (accent & 0xFF, (accent >> 8) & 0xFF, (accent >> 16) & 0xFF);
let pixels = dib.pixels();
for r in rects {
let left = (r.left - work.left).clamp(0, width);
let top = (r.top - work.top).clamp(0, height);
let right = (r.right - work.left).clamp(0, width);
let bottom = (r.bottom - work.top).clamp(0, height);
let rw = right - left;
let rh = bottom - top;
if rw <= 0 || rh <= 0 {
continue;
}
let band = (radius.ceil().max(border_w.ceil()) as i32 + 1).min(rw.min(rh));
let fill_a = FILL_ALPHA.round() as u32;
let fill_px = (fill_a << 24)
| (premultiply(ar, fill_a) << 16)
| (premultiply(ag, fill_a) << 8)
| premultiply(ab, fill_a);
for ry in 0..rh {
let edge_row = ry < band || ry >= rh - band;
let row_base = (top + ry) * width + left;
for rx in 0..rw {
let idx = (row_base + rx) as usize;
if !edge_row && rx >= band && rx < rw - band {
pixels[idx] = fill_px;
continue;
}
let outer = round_rect_coverage(rx, ry, rw, rh, radius, 0.0);
if outer <= 0.0 {
continue;
}
let inner = round_rect_coverage(rx, ry, rw, rh, radius - border_w, border_w);
let ring = (outer - inner).clamp(0.0, 1.0);
let a = (inner.min(outer) * FILL_ALPHA + ring * BORDER_ALPHA)
.clamp(0.0, 255.0)
.round() as u32;
pixels[idx] = (a << 24)
| (premultiply(ar, a) << 16)
| (premultiply(ag, a) << 8)
| premultiply(ab, a);
}
}
}
}
unsafe extern "system" fn preview_wnd_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}