mod cards;
mod geometry;
mod input;
mod render;
pub use geometry::neighbor_slot;
use std::cell::RefCell;
use std::ptr::null_mut;
use std::sync::OnceLock;
use windows_sys::Win32::Foundation::{HWND, POINT, RECT};
use windows_sys::Win32::Graphics::Dwm::DwmUnregisterThumbnail;
use windows_sys::Win32::Graphics::Gdi::{InvalidateRect, HFONT};
use windows_sys::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, GetClientRect, SetForegroundWindow, SetWindowPos, ShowWindow, HICON,
SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOZORDER, SW_HIDE, SW_SHOW, WS_EX_TOOLWINDOW,
WS_EX_TOPMOST, WS_POPUP,
};
use winspaces_common::log_info;
use winspaces_core::spaces::SpaceManager;
use winspaces_win32::display;
use winspaces_win32::dpi;
use winspaces_win32::dwm;
use winspaces_win32::module::app_instance;
use winspaces_win32::text::encode_wide;
use winspaces_win32::window_class::register_class;
const OVERVIEW_CLASS_NAME: &str = "WinSpacesOverview";
const TIMER_DRAG_PAINT: usize = 1;
pub struct OverviewHost {
pub add_space: fn(mon: usize),
pub remove_space: fn(mon: usize, space: usize),
pub reorder_space: fn(mon: usize, from: usize, to: usize),
pub reorder_space_neighbor: fn(mon: usize, delta: i32),
pub switch_space: fn(mon: usize, space: usize),
pub move_window_to_space: fn(hwnd: HWND, mon: usize, space: usize),
pub move_window_to_new_space: fn(hwnd: HWND, mon: usize),
pub close_window: fn(hwnd: HWND),
pub toggle_window_sticky: fn(hwnd: HWND),
}
static HOST: OnceLock<&'static OverviewHost> = OnceLock::new();
pub fn install_host(host: &'static OverviewHost) {
let _ = HOST.set(host);
}
fn host() -> Option<&'static OverviewHost> {
HOST.get().copied()
}
#[derive(Clone)]
pub struct SpaceCard {
pub space_idx: usize,
pub rect: RECT,
pub window_count: usize,
pub is_active: bool,
pub is_tiled: bool,
}
#[derive(Clone)]
pub struct WindowCard {
pub hwnd: HWND,
pub h_thumb: isize,
pub h_icon: HICON,
pub card_rect: RECT,
pub thumb_rect: RECT,
pub title: String,
pub is_sticky: bool,
}
pub struct Overview {
pub hwnd: HWND,
pub is_visible: bool,
pub active_mon_idx: usize,
pub active_space_idx: usize,
pub scale: f32,
pub space_cards: Vec<SpaceCard>,
pub window_cards: Vec<WindowCard>,
pub plus_rect: RECT,
pub plus_visible: bool,
pub plus_label_w: i32,
pub hovered_plus: bool,
pub hovered_space: Option<usize>,
pub hovered_close: Option<usize>,
pub hovered_window: Option<usize>,
pub hovered_window_close: Option<usize>,
pub hovered_window_pin: Option<usize>,
pub pressed_window_close: Option<usize>,
pub pressed_window_pin: Option<usize>,
pub dragging_window: Option<usize>,
pub drag_active: bool,
pub drag_offset: POINT,
pub dragging_space: Option<usize>,
pub drag_space_active: bool,
pub drag_space_current_x: i32,
pub drag_space_target_slot: Option<usize>,
pub drag_paint_tick: u32,
pub drag_frame_ms: u32,
pub h_font_title: HFONT,
pub h_font_card: HFONT,
pub h_font_small: HFONT,
pub h_font_close: HFONT,
pub h_font_glyph: HFONT,
pub h_font_pin: HFONT,
}
thread_local! {
static OVERVIEW_STATE: RefCell<Overview> = const { RefCell::new(Overview::new()) };
}
impl Overview {
pub const fn new() -> Self {
Self {
hwnd: null_mut(),
is_visible: false,
active_mon_idx: 0,
active_space_idx: 0,
scale: 1.0,
space_cards: Vec::new(),
window_cards: Vec::new(),
plus_rect: RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
plus_visible: false,
plus_label_w: 0,
hovered_plus: false,
hovered_space: None,
hovered_close: None,
hovered_window: None,
hovered_window_close: None,
hovered_window_pin: None,
pressed_window_close: None,
pressed_window_pin: None,
dragging_window: None,
drag_active: false,
drag_offset: POINT { x: 0, y: 0 },
dragging_space: None,
drag_space_active: false,
drag_space_current_x: 0,
drag_space_target_slot: None,
drag_paint_tick: 0,
drag_frame_ms: 16,
h_font_title: null_mut(),
h_font_card: null_mut(),
h_font_small: null_mut(),
h_font_close: null_mut(),
h_font_glyph: null_mut(),
h_font_pin: null_mut(),
}
}
}
impl Default for Overview {
fn default() -> Self {
Self::new()
}
}
pub fn init_overview() {
log_info!("Initialized Overview subsystem");
}
pub fn is_overview_active() -> bool {
OVERVIEW_STATE.with(|s| s.borrow().is_visible)
}
pub fn toggle_overview(mgr: &mut SpaceManager) {
if is_overview_active() {
hide_overview();
} else {
show_overview(mgr);
}
}
pub fn show_overview(mgr: &mut SpaceManager) {
mgr.scan_untracked_windows();
unsafe {
let mon_idx = mgr.get_active_monitor_index();
if mon_idx >= mgr.monitors.len() {
return;
}
let space_idx = mgr.monitors[mon_idx].current;
let hmon = mgr.monitors[mon_idx].hmon;
let Some(mon_rect) = display::monitor_rect_of(hmon) else {
return;
};
let width = mon_rect.right - mon_rect.left;
let height = mon_rect.bottom - mon_rect.top;
OVERVIEW_STATE.with(|s| {
let mut mc = s.borrow_mut();
if mc.is_visible {
return;
}
mc.active_mon_idx = mon_idx;
mc.active_space_idx = space_idx;
mc.space_cards.clear();
mc.window_cards.clear();
mc.hovered_space = None;
mc.hovered_window = None;
mc.hovered_window_close = None;
mc.pressed_window_close = None;
mc.dragging_window = None;
mc.drag_active = false;
mc.dragging_space = None;
mc.drag_space_active = false;
mc.drag_space_current_x = 0;
mc.drag_space_target_slot = None;
if mc.hwnd.is_null() {
register_class(OVERVIEW_CLASS_NAME, Some(input::overview_wnd_proc));
let hinst = app_instance();
let class_name = encode_wide(OVERVIEW_CLASS_NAME);
mc.hwnd = CreateWindowExW(
WS_EX_TOPMOST | WS_EX_TOOLWINDOW,
class_name.as_ptr(),
std::ptr::null(),
WS_POPUP,
mon_rect.left,
mon_rect.top,
width,
height,
null_mut(),
null_mut(),
hinst,
null_mut(),
);
if mc.hwnd.is_null() {
log_info!("Failed to create Overview overlay window");
return;
}
dwm::set_dark_mode(mc.hwnd, true);
dwm::set_backdrop(mc.hwnd, dwm::Backdrop::Acrylic);
} else {
SetWindowPos(
mc.hwnd,
null_mut(),
mon_rect.left,
mon_rect.top,
width,
height,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED,
);
}
let scale = dpi::scale_for_window(mc.hwnd);
cards::update_fonts_for_dpi(&mut mc, scale);
cards::rebuild_cards(&mut mc, mgr, mon_idx, space_idx, width, height);
mc.is_visible = true;
ShowWindow(mc.hwnd, SW_SHOW);
SetForegroundWindow(mc.hwnd);
InvalidateRect(mc.hwnd, std::ptr::null(), 1);
log_info!(
"Overview shown on Mon {} (Space {}) with {} window thumbnails",
mon_idx + 1,
space_idx + 1,
mc.window_cards.len()
);
});
}
}
pub fn refresh_overview(mgr: &mut SpaceManager) {
unsafe {
OVERVIEW_STATE.with(|s| {
let mut mc = s.borrow_mut();
if !mc.is_visible || mc.hwnd.is_null() {
return;
}
let mon_idx = mc.active_mon_idx;
if mon_idx >= mgr.monitors.len() {
return;
}
let space_idx = mgr.monitors[mon_idx].current;
mc.active_space_idx = space_idx;
let mut client_rect: RECT = std::mem::zeroed();
GetClientRect(mc.hwnd, &mut client_rect);
let width = client_rect.right - client_rect.left;
let height = client_rect.bottom - client_rect.top;
cards::rebuild_cards(&mut mc, mgr, mon_idx, space_idx, width, height);
mc.pressed_window_close = None;
mc.dragging_window = None;
mc.drag_active = false;
mc.dragging_space = None;
mc.drag_space_active = false;
mc.drag_space_current_x = 0;
mc.drag_space_target_slot = None;
input::resync_hover(&mut mc);
SetForegroundWindow(mc.hwnd);
InvalidateRect(mc.hwnd, std::ptr::null(), 1);
});
}
}
pub fn hide_overview() {
unsafe {
OVERVIEW_STATE.with(|s| {
let mut mc = s.borrow_mut();
if !mc.is_visible {
return;
}
for card in &mc.window_cards {
if card.h_thumb != 0 {
DwmUnregisterThumbnail(card.h_thumb);
}
}
mc.window_cards.clear();
mc.space_cards.clear();
if !mc.hwnd.is_null() {
ShowWindow(mc.hwnd, SW_HIDE);
}
cards::release_fonts(&mut mc);
mc.is_visible = false;
mc.dragging_window = None;
mc.drag_active = false;
mc.dragging_space = None;
mc.drag_space_active = false;
mc.drag_space_current_x = 0;
mc.drag_space_target_slot = None;
mc.hovered_close = None;
mc.hovered_window_close = None;
mc.pressed_window_close = None;
mc.hovered_plus = false;
log_info!("Overview hidden");
});
}
}