use std::collections::HashMap;
use windows::Win32::Foundation::{HWND, LPARAM};
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GW_OWNER, GWL_STYLE, GetWindow, GetWindowLongW, WINDOW_STYLE, WS_CHILD,
};
use crate::common::{InvisibleBounds, Rect};
use crate::config::types::{WindowRule, WindowRulesConfig};
use super::classification;
use super::types::{FloatingState, IgnoredReason, TilingState, VirtualSlot, Window, WindowState};
use super::win32;
pub struct WindowRegistry {
windows: HashMap<isize, Window>,
focused: Option<isize>,
pipeline: classification::ClassificationPipeline,
}
impl WindowRegistry {
#[must_use]
pub fn new(user_rules: &WindowRulesConfig, default_rules: &WindowRulesConfig) -> Self {
Self {
windows: HashMap::new(),
focused: None,
pipeline: classification::ClassificationPipeline::new(
user_rules.clone(),
default_rules.clone(),
),
}
}
pub fn scan_existing_windows(&mut self) -> Result<(), String> {
match enum_toplevel_windows() {
Ok(hwnds) => {
for hwnd in hwnds {
if !win32::is_window_visible(hwnd) {
continue;
}
let title = win32::get_window_text(hwnd).unwrap_or_default();
if title.is_empty() {
continue;
}
if !win32::is_alt_tab_visible(hwnd) {
log::debug!("init: skipping {:?} — not Alt+Tab visible", hwnd);
continue;
}
if win32::is_iconic(hwnd) {
log::debug!("init: skipping {:?} — iconic (minimized)", hwnd);
continue;
}
if has_owner(hwnd) || has_parent(hwnd) {
continue;
}
match win32::get_window_info(hwnd) {
Ok(info) => {
self.register_window_from_info(&info);
log::debug!("init: registered {} ({:?})", info.exe, hwnd);
}
Err(e) => {
log::warn!("init: skipping {:?}: {e}", hwnd);
}
}
}
log::info!("registry initialized with {} window(s)", self.windows.len());
}
Err(e) => {
log::warn!(
"scan_existing_windows: EnumWindows failed ({e}), relying on event hooks"
);
}
}
Ok(())
}
pub fn register_window_from_info(&mut self, info: &win32::WindowInfo) {
let key = hwnd_key(info.hwnd);
if self.windows.contains_key(&key) {
return;
}
let candidate = classification::WindowCandidate {
exe: info.exe.clone(),
title: info.title.clone(),
class: info.class.clone(),
process_path: info.process_path.clone(),
};
let state = classification::classify_with_state_pipeline(
&candidate,
info.is_maximized,
info.is_fullscreen,
&self.pipeline,
);
let invisible_bounds = win32::get_invisible_bounds(info.hwnd);
let window = Window::new(
info.hwnd,
info.exe.clone(),
info.title.clone(),
info.class.clone(),
std::path::PathBuf::from(&info.process_path),
info.rect,
state,
invisible_bounds,
);
self.windows.insert(key, window);
log::info!(
"registered window: {:?} ({}) class={:?} title={:?}",
info.hwnd,
info.exe,
info.class,
info.title,
);
}
pub fn remove_window(&mut self, hwnd_val: isize) {
if let Some(window) = self.windows.remove(&hwnd_val) {
log::info!("removed window: (isize={hwnd_val}) ({})", window.exe);
if self.focused == Some(hwnd_val) {
self.focused = None;
}
} else {
log::debug!("remove_window: isize={hwnd_val} not in registry");
}
}
pub fn set_focused(&mut self, hwnd_val: isize) {
if self.windows.contains_key(&hwnd_val) {
self.focused = Some(hwnd_val);
log::debug!("focus changed: isize={hwnd_val}");
} else {
log::debug!("set_focused: isize={hwnd_val} not in registry, ignoring");
}
}
#[must_use]
pub fn focused(&self) -> Option<crate::common::WindowId> {
self.focused.map(crate::common::WindowId)
}
pub fn clear_focused(&mut self) {
if self.focused.take().is_some() {
log::debug!("focus cleared");
}
}
pub fn set_learned_rules(&mut self, rules: Vec<WindowRule>) {
self.pipeline.set_learned_rules(rules);
}
pub fn set_user_rules(&mut self, user_rules: WindowRulesConfig) {
self.pipeline.set_user_rules(user_rules);
}
pub fn minimize_window(&mut self, hwnd_val: isize) {
if let Some(window) = self.windows.get_mut(&hwnd_val) {
let new_state = match &window.state {
WindowState::Tiling(TilingState::Active { col, row }) => {
let slot = VirtualSlot {
col: *col,
row: *row,
};
window.last_virtual_slot = Some(slot);
WindowState::Tiling(TilingState::Minimized)
}
WindowState::Floating(FloatingState::Active { .. }) => {
WindowState::Floating(FloatingState::Minimized)
}
_ => return, };
window.state = new_state;
log::info!("minimized window: isize={hwnd_val}");
}
}
pub fn restore_window(&mut self, hwnd_val: isize) {
if let Some(window) = self.windows.get_mut(&hwnd_val) {
let new_state = match &window.state {
WindowState::Tiling(TilingState::Minimized) => {
let (col, row) = window
.last_virtual_slot
.as_ref()
.map(|s| (s.col, s.row))
.unwrap_or((0, 0));
WindowState::Tiling(TilingState::Active { col, row })
}
WindowState::Floating(FloatingState::Minimized) => {
let rect = window.pre_manage_rect;
WindowState::Floating(FloatingState::Active { rect })
}
_ => return, };
window.state = new_state;
log::info!("restored window: isize={hwnd_val}");
}
}
#[must_use]
pub fn reconcile_visibility(&mut self, hwnd_val: isize) -> super::types::VisibilityChange {
use super::types::VisibilityChange;
let h = HWND(hwnd_val as *mut _);
let user_visible =
win32::is_window_visible(h) && !win32::is_cloaked(h) && !win32::is_iconic(h);
let Some(window) = self.windows.get_mut(&hwnd_val) else {
return VisibilityChange::Unchanged;
};
if !user_visible {
let new_state = match &window.state {
WindowState::Tiling(TilingState::Active { col, row }) => {
let slot = VirtualSlot {
col: *col,
row: *row,
};
window.last_virtual_slot = Some(slot);
WindowState::Tiling(TilingState::Hidden)
}
WindowState::Floating(FloatingState::Active { .. }) => {
WindowState::Floating(FloatingState::Hidden)
}
_ => return VisibilityChange::Unchanged,
};
window.state = new_state;
log::info!("reconcile_visibility: window isize={hwnd_val} → Hidden");
VisibilityChange::Hidden
} else {
let new_state = match &window.state {
WindowState::Tiling(TilingState::Hidden) => {
let (col, row) = window
.last_virtual_slot
.as_ref()
.map(|s| (s.col, s.row))
.unwrap_or((0, 0));
WindowState::Tiling(TilingState::Active { col, row })
}
WindowState::Floating(FloatingState::Hidden) => {
let rect = window.pre_manage_rect;
WindowState::Floating(FloatingState::Active { rect })
}
_ => return VisibilityChange::Unchanged,
};
window.state = new_state;
log::info!("reconcile_visibility: window isize={hwnd_val} → Shown");
VisibilityChange::Shown
}
}
#[must_use]
pub fn get_window(&self, hwnd: HWND) -> Option<&Window> {
self.windows.get(&hwnd_key(hwnd))
}
#[must_use]
pub fn get_window_mut(&mut self, hwnd: HWND) -> Option<&mut Window> {
self.windows.get_mut(&hwnd_key(hwnd))
}
pub fn windows(&self) -> impl Iterator<Item = &Window> {
self.windows.values()
}
#[must_use]
pub fn len(&self) -> usize {
self.windows.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.windows.is_empty()
}
#[must_use]
pub fn to_json_value(&self, viewport_offset: i32) -> serde_json::Value {
let windows_json: Vec<serde_json::Value> = self
.windows
.values()
.map(|w| {
let tiled_rect_json = w.tiled_rect.map(|r| {
serde_json::json!({
"x": r.x,
"y": r.y,
"width": r.width,
"height": r.height,
})
});
let window_rect_json = win32::get_window_rect(w.hwnd).ok().map(|r| {
serde_json::json!({
"x": r.x,
"y": r.y,
"width": r.width,
"height": r.height,
})
});
let visible_rect_json = window_rect_json.as_ref().and_then(|wr| {
let wr_obj = wr.as_object()?;
let x = wr_obj.get("x")?.as_i64()?;
let y = wr_obj.get("y")?.as_i64()?;
let width = wr_obj.get("width")?.as_i64()?;
let height = wr_obj.get("height")?.as_i64()?;
let visible = w.invisible_bounds.window_to_visible(Rect {
x: x as i32,
y: y as i32,
width: width as i32,
height: height as i32,
});
Some(serde_json::json!({
"x": visible.x,
"y": visible.y,
"width": visible.width,
"height": visible.height,
}))
});
serde_json::json!({
"hwnd": w.hwnd.0 as isize,
"exe": w.exe,
"title": w.title,
"class": w.class,
"process_path": w.process_path.to_string_lossy(),
"state": state_to_json(&w.state),
"tiled_rect": tiled_rect_json,
"window_rect": window_rect_json,
"visible_rect": visible_rect_json,
"invisible_bounds": serde_json::json!({
"left": w.invisible_bounds.left,
"top": w.invisible_bounds.top,
"right": w.invisible_bounds.right,
"bottom": w.invisible_bounds.bottom,
}),
"pre_manage_rect": serde_json::json!({
"x": w.pre_manage_rect.x,
"y": w.pre_manage_rect.y,
"width": w.pre_manage_rect.width,
"height": w.pre_manage_rect.height,
}),
})
})
.collect();
serde_json::json!({
"viewport_offset": viewport_offset,
"windows": windows_json,
"focused": self.focused,
"count": self.windows.len(),
})
}
#[must_use]
pub fn tiling_window_ids(&self) -> Vec<crate::common::WindowId> {
self.windows
.iter()
.filter_map(|(key, w)| match &w.state {
WindowState::Tiling(TilingState::Active { .. }) => {
Some(crate::common::WindowId(*key))
}
_ => None,
})
.collect()
}
#[must_use]
pub fn tiling_window_ids_sorted_by_x(&self) -> Vec<crate::common::WindowId> {
let mut ids: Vec<(crate::common::WindowId, i32)> = self
.windows
.iter()
.filter_map(|(key, w)| match &w.state {
WindowState::Tiling(TilingState::Active { .. }) => {
Some((crate::common::WindowId(*key), w.pre_manage_rect.x))
}
_ => None,
})
.collect();
ids.sort_by_key(|(_, x)| *x);
ids.into_iter().map(|(id, _)| id).collect()
}
#[must_use]
pub fn tiling_window_ids_with_widths_sorted_by_x(&self) -> Vec<(crate::common::WindowId, u32)> {
let mut entries: Vec<(crate::common::WindowId, i32, u32)> = self
.windows
.iter()
.filter_map(|(key, w)| match &w.state {
WindowState::Tiling(TilingState::Active { .. }) => {
let width = w.pre_manage_rect.width.max(0) as u32;
Some((crate::common::WindowId(*key), w.pre_manage_rect.x, width))
}
_ => None,
})
.collect();
entries.sort_by_key(|(_, x, _)| *x);
entries.into_iter().map(|(id, _, w)| (id, w)).collect()
}
#[must_use]
pub fn restorable_windows(&self) -> Vec<(isize, Rect, InvisibleBounds)> {
self.windows
.iter()
.filter_map(|(key, w)| match &w.state {
WindowState::Tiling(TilingState::Active { .. })
| WindowState::Floating(FloatingState::Active { .. }) => {
Some((*key, w.pre_manage_rect, w.invisible_bounds))
}
_ => None,
})
.collect()
}
pub fn update_tiling_slots_from_layout(
&mut self,
virtual_layout: &crate::layout::types::VirtualLayout,
) {
for (col_idx, column) in virtual_layout.columns.iter().enumerate() {
for (row_idx, row) in column.rows.iter().enumerate() {
if let Some(window) = self.windows.get_mut(&row.window_id.0) {
window.state = WindowState::Tiling(TilingState::Active {
col: col_idx,
row: row_idx,
});
}
}
}
}
pub fn update_tiled_rects(&mut self, actual_layout: &crate::layout::types::ActualLayout) {
for entry in &actual_layout.entries {
if let Some(window) = self.windows.get_mut(&entry.window_id.0) {
window.tiled_rect = Some(entry.rect);
}
}
}
#[must_use]
pub fn is_tiling(&self, hwnd_val: isize) -> bool {
self.windows
.get(&hwnd_val)
.map(|w| matches!(w.state, WindowState::Tiling(_)))
.unwrap_or(false)
}
#[must_use]
pub fn is_tiling_active(&self, hwnd_val: isize) -> bool {
match self.windows.get(&hwnd_val) {
Some(window) => {
matches!(
window.state,
WindowState::Tiling(TilingState::Active { .. })
)
}
None => false,
}
}
#[must_use]
pub fn is_tracked(&self, hwnd_val: isize) -> bool {
self.windows.contains_key(&hwnd_val)
}
#[must_use]
pub fn reclassify_os_state(&mut self, hwnd_val: isize) -> super::types::ReclassifyResult {
use super::types::ReclassifyResult;
let (was_maximized, was_fullscreen) = match self.windows.get(&hwnd_val) {
None => return ReclassifyResult::Untracked,
Some(window) => {
let mx = matches!(window.state, WindowState::Ignored(IgnoredReason::Maximized));
let fs = matches!(
window.state,
WindowState::Ignored(IgnoredReason::Fullscreen)
);
if !mx && !fs {
return ReclassifyResult::NotApplicable;
}
(mx, fs)
}
};
let hwnd = HWND(hwnd_val as *mut _);
let is_maximized = win32::is_zoomed(hwnd);
let is_fullscreen = win32::is_fullscreen(hwnd).unwrap_or(false);
if (was_maximized && is_maximized) || (was_fullscreen && is_fullscreen) {
return ReclassifyResult::Unchanged;
}
let new_state = match self.windows.get(&hwnd_val) {
Some(window) => {
let candidate = classification::WindowCandidate {
exe: window.exe.clone(),
title: window.title.clone(),
class: window.class.clone(),
process_path: window.process_path.to_string_lossy().into_owned(),
};
classification::classify_with_state_pipeline(
&candidate,
is_maximized,
is_fullscreen,
&self.pipeline,
)
}
None => return ReclassifyResult::Untracked,
};
let now_tiling = matches!(new_state, WindowState::Tiling(_));
if let Some(window) = self.windows.get_mut(&hwnd_val) {
window.state = new_state;
}
log::info!(
"reclassify_os_state: window isize={hwnd_val} recovered \
(is_maximized={is_maximized}, is_fullscreen={is_fullscreen}, now_tiling={now_tiling})"
);
ReclassifyResult::Recovered { now_tiling }
}
pub fn handle_created(&mut self, hwnd_val: isize) -> Option<crate::common::WindowId> {
let hwnd = HWND(hwnd_val as *mut _);
if self.windows.contains_key(&hwnd_val) {
log::debug!("handle_created: {hwnd:?} already tracked — skipping");
return None;
}
if !win32::is_window_visible(hwnd) {
log::debug!("handle_created: skipping {hwnd:?} — not visible");
return None;
}
let title = win32::get_window_text(hwnd).unwrap_or_default();
if title.is_empty() {
log::debug!("handle_created: skipping {hwnd:?} — empty title");
return None;
}
if !win32::is_alt_tab_visible(hwnd) {
log::debug!("handle_created: skipping {hwnd:?} — not Alt+Tab visible");
return None;
}
if win32::is_iconic(hwnd) {
log::debug!("handle_created: skipping {hwnd:?} — iconic (minimized)");
return None;
}
if has_owner(hwnd) || has_parent(hwnd) {
log::debug!(
"handle_created: skipping {hwnd:?} — has owner or parent (dialog/popup/child)"
);
return None;
}
match win32::get_window_info(hwnd) {
Ok(info) => {
self.register_window_from_info(&info);
}
Err(e) => {
log::warn!("handle_created: failed to get info for {hwnd:?}: {e}");
return None;
}
}
if let Some(window) = self.windows.get(&hwnd_val) {
match &window.state {
WindowState::Tiling(_) => Some(crate::common::WindowId(hwnd_val)),
other => {
log::debug!("handle_created: {hwnd:?} registered as non-tiling ({other:?})");
None
}
}
} else {
None
}
}
}
fn hwnd_key(hwnd: HWND) -> isize {
hwnd.0 as isize
}
fn state_to_json(state: &WindowState) -> serde_json::Value {
match state {
WindowState::Tiling(TilingState::Active { col, row }) => {
serde_json::json!({"Tiling": {"Active": {"col": col, "row": row}}})
}
WindowState::Tiling(TilingState::Minimized) => {
serde_json::json!("Tiling::Minimized")
}
WindowState::Tiling(TilingState::Hidden) => {
serde_json::json!("Tiling::Hidden")
}
WindowState::Floating(FloatingState::Active { rect }) => serde_json::json!({
"Floating": {"Active": {"rect": {"x": rect.x, "y": rect.y, "width": rect.width, "height": rect.height}}}
}),
WindowState::Floating(FloatingState::Minimized) => {
serde_json::json!("Floating::Minimized")
}
WindowState::Floating(FloatingState::Hidden) => {
serde_json::json!("Floating::Hidden")
}
WindowState::Ignored(IgnoredReason::Maximized) => {
serde_json::json!("Ignored::Maximized")
}
WindowState::Ignored(IgnoredReason::Fullscreen) => {
serde_json::json!("Ignored::Fullscreen")
}
WindowState::Ignored(IgnoredReason::ExplicitRule) => {
serde_json::json!("Ignored::ExplicitRule")
}
}
}
fn has_owner(hwnd: HWND) -> bool {
match unsafe { GetWindow(hwnd, GW_OWNER) } {
Ok(owner) => !owner.is_invalid(),
Err(_) => false,
}
}
fn has_parent(hwnd: HWND) -> bool {
let style = unsafe { GetWindowLongW(hwnd, GWL_STYLE) };
let style = WINDOW_STYLE(style as u32);
style & WS_CHILD != WINDOW_STYLE(0)
}
fn enum_toplevel_windows() -> Result<Vec<HWND>, String> {
let mut collected: Vec<HWND> = Vec::new();
let ptr = &mut collected as *mut Vec<HWND>;
let result = unsafe { EnumWindows(Some(enum_windows_callback), LPARAM(ptr as isize)) };
result.map_err(|e| format!("EnumWindows failed: {e}"))?;
Ok(collected)
}
unsafe extern "system" fn enum_windows_callback(
hwnd: HWND,
l_param: LPARAM,
) -> windows::core::BOOL {
let vec = unsafe { &mut *(l_param.0 as *mut Vec<HWND>) };
vec.push(hwnd);
windows::core::BOOL(1) }
#[cfg(test)]
mod tests {
use super::super::types::ReclassifyResult;
use super::*;
use crate::config::types::{WindowAction, WindowRulesConfig};
fn default_rules() -> (WindowRulesConfig, WindowRulesConfig) {
(WindowRulesConfig::default(), WindowRulesConfig::default())
}
#[test]
fn new_registry_is_empty() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
assert!(reg.is_empty());
assert_eq!(reg.len(), 0);
assert!(reg.focused.is_none());
}
#[test]
fn to_json_value_empty_registry() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
let json = reg.to_json_value(0);
assert_eq!(json["count"], 0);
assert!(json["windows"].as_array().unwrap().is_empty());
assert!(json["focused"].is_null());
assert_eq!(json["viewport_offset"], 0);
}
#[test]
fn to_json_value_has_correct_structure() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
let json = reg.to_json_value(0);
assert!(json.get("windows").is_some());
assert!(json.get("focused").is_some());
assert!(json.get("count").is_some());
assert!(json.get("viewport_offset").is_some());
}
#[test]
fn to_json_value_includes_window_rect_field_for_windows() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 12345isize;
insert_test_window(
&mut reg,
hwnd_val,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
let json = reg.to_json_value(0);
let windows = json["windows"]
.as_array()
.expect("windows should be an array");
assert_eq!(windows.len(), 1);
let w = &windows[0];
assert!(
w.get("window_rect").is_some(),
"window_rect field must be present in JSON output"
);
assert!(
w["window_rect"].is_null(),
"window_rect should be null for invalid HWND (GetWindowRect fails)"
);
}
#[test]
fn to_json_value_includes_tiled_rect_and_window_rect() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 54321isize;
let hwnd = HWND(hwnd_val as *mut _);
let window = Window::new(
hwnd,
"test.exe".into(),
"Test".into(),
"TestClass".into(),
std::path::PathBuf::from("C:\\test.exe"),
crate::common::Rect {
x: 10,
y: 20,
width: 800,
height: 600,
},
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
crate::common::InvisibleBounds::zero(),
);
let mut window = window;
window.tiled_rect = Some(crate::common::Rect {
x: 4,
y: 0,
width: 952,
height: 1080,
});
reg.windows.insert(hwnd_val, window);
let json = reg.to_json_value(0);
let windows = json["windows"]
.as_array()
.expect("windows should be an array");
let w = &windows[0];
let tiled = w["tiled_rect"]
.as_object()
.expect("tiled_rect should be an object");
assert_eq!(tiled["x"], 4);
assert_eq!(tiled["width"], 952);
assert!(
w.get("window_rect").is_some(),
"window_rect field must be present alongside tiled_rect"
);
let ib = w["invisible_bounds"]
.as_object()
.expect("invisible_bounds should be an object");
assert_eq!(ib["left"], 0);
assert_eq!(ib["top"], 0);
assert_eq!(ib["right"], 0);
assert_eq!(ib["bottom"], 0);
}
#[test]
fn to_json_value_window_rect_null_format() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
let json = reg.to_json_value(0);
assert!(json["windows"].is_array());
}
#[test]
fn to_json_value_includes_visible_rect_and_invisible_bounds_fields() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 99999isize;
let hwnd = HWND(hwnd_val as *mut _);
let window = Window::new(
hwnd,
"visrect.exe".into(),
"VisRect".into(),
"VisClass".into(),
std::path::PathBuf::from("C:\\visrect.exe"),
crate::common::Rect {
x: 10,
y: 20,
width: 800,
height: 600,
},
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
crate::common::InvisibleBounds::zero(),
);
reg.windows.insert(hwnd_val, window);
let json = reg.to_json_value(0);
let w = &json["windows"].as_array().unwrap()[0];
assert!(
w.get("invisible_bounds").is_some(),
"invisible_bounds field must be present in JSON output"
);
let ib = w["invisible_bounds"].as_object().unwrap();
assert_eq!(ib["left"], 0);
assert_eq!(ib["top"], 0);
assert_eq!(ib["right"], 0);
assert_eq!(ib["bottom"], 0);
assert!(
w.get("visible_rect").is_some(),
"visible_rect field must be present in JSON output"
);
assert!(
w["visible_rect"].is_null(),
"visible_rect should be null when window_rect is null (invalid HWND)"
);
}
#[test]
fn to_json_value_visible_rect_null_when_window_rect_null() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 11111isize;
insert_test_window(
&mut reg,
hwnd_val,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
let json = reg.to_json_value(0);
let w = &json["windows"].as_array().unwrap()[0];
assert!(
w["window_rect"].is_null(),
"window_rect should be null for test window with invalid HWND"
);
assert!(
w["visible_rect"].is_null(),
"visible_rect should be null when window_rect is null"
);
}
#[test]
fn register_window_from_info_stores_invisible_bounds() {
let user = WindowRulesConfig {
default_action: WindowAction::Tile,
rules: vec![],
};
let default = WindowRulesConfig::default();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 44444isize;
let info = win32::WindowInfo {
hwnd: HWND(hwnd_val as *mut _),
title: "BoundsTest".to_owned(),
class: "BoundsClass".to_owned(),
rect: crate::common::Rect {
x: 0,
y: 0,
width: 640,
height: 480,
},
exe: "boundstest.exe".to_owned(),
process_path: "C:\\boundstest.exe".to_owned(),
is_visible: true,
is_maximized: false,
is_fullscreen: false,
};
reg.register_window_from_info(&info);
let w = reg.get_window(HWND(hwnd_val as *mut _)).unwrap();
assert_eq!(
w.invisible_bounds,
crate::common::InvisibleBounds::zero(),
"invisible_bounds should be stored as zero for invalid HWND"
);
}
#[test]
fn remove_window_clears_focus() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 12345isize;
let hwnd = HWND(hwnd_val as *mut _);
let window = Window::new(
hwnd,
"test.exe".into(),
"Test".into(),
"TestClass".into(),
std::path::PathBuf::from("C:\\test.exe"),
crate::common::Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
crate::common::InvisibleBounds::zero(),
);
reg.windows.insert(hwnd_val, window);
reg.focused = Some(hwnd_val);
assert_eq!(reg.len(), 1);
assert_eq!(reg.focused, Some(hwnd_val));
reg.remove_window(hwnd_val);
assert!(reg.is_empty());
assert!(reg.focused.is_none());
}
#[test]
fn minimize_and_restore_tiling_window() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 99999isize;
let hwnd = HWND(hwnd_val as *mut _);
let window = Window::new(
hwnd,
"app.exe".into(),
"App".into(),
"AppClass".into(),
std::path::PathBuf::from("C:\\app.exe"),
crate::common::Rect {
x: 10,
y: 20,
width: 800,
height: 600,
},
WindowState::Tiling(TilingState::Active { col: 2, row: 1 }),
crate::common::InvisibleBounds::zero(),
);
reg.windows.insert(hwnd_val, window);
reg.minimize_window(hwnd_val);
let w = reg.windows.get(&hwnd_val).unwrap();
assert!(matches!(
w.state,
WindowState::Tiling(TilingState::Minimized)
));
assert_eq!(
w.last_virtual_slot.as_ref().map(|s| (s.col, s.row)),
Some((2, 1))
);
reg.restore_window(hwnd_val);
let w = reg.windows.get(&hwnd_val).unwrap();
assert!(matches!(
w.state,
WindowState::Tiling(TilingState::Active { col: 2, row: 1 })
));
}
fn insert_test_window(reg: &mut WindowRegistry, hwnd_val: isize, state: WindowState) {
insert_test_window_with_rect(reg, hwnd_val, state, 0);
}
fn insert_test_window_with_rect(
reg: &mut WindowRegistry,
hwnd_val: isize,
state: WindowState,
x: i32,
) {
let hwnd = HWND(hwnd_val as *mut _);
let window = Window::new(
hwnd,
"test.exe".into(),
format!("Test-{hwnd_val}"),
"TestClass".into(),
std::path::PathBuf::from("C:\\test.exe"),
crate::common::Rect {
x,
y: 0,
width: 100,
height: 100,
},
state,
crate::common::InvisibleBounds::zero(),
);
reg.windows.insert(hwnd_val, window);
}
fn insert_test_window_with_rect_and_width(
reg: &mut WindowRegistry,
hwnd_val: isize,
state: WindowState,
x: i32,
width: i32,
) {
let hwnd = HWND(hwnd_val as *mut _);
let window = Window::new(
hwnd,
"test.exe".into(),
format!("Test-{hwnd_val}"),
"TestClass".into(),
std::path::PathBuf::from("C:\\test.exe"),
crate::common::Rect {
x,
y: 0,
width,
height: 100,
},
state,
crate::common::InvisibleBounds::zero(),
);
reg.windows.insert(hwnd_val, window);
}
#[test]
fn set_focused_on_tracked_window() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 42isize;
insert_test_window(
&mut reg,
hwnd_val,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
assert!(reg.focused.is_none());
reg.set_focused(hwnd_val);
assert_eq!(reg.focused, Some(hwnd_val));
}
#[test]
fn set_focused_ignores_untracked_window() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
reg.set_focused(99999);
assert!(reg.focused.is_none());
}
#[test]
fn set_focused_changes_between_windows() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
10,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
insert_test_window(
&mut reg,
20,
WindowState::Tiling(TilingState::Active { col: 1, row: 0 }),
);
reg.set_focused(10);
assert_eq!(reg.focused, Some(10));
reg.set_focused(20);
assert_eq!(reg.focused, Some(20));
}
#[test]
fn focused_returns_none_by_default() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
assert!(reg.focused().is_none());
}
#[test]
fn focused_returns_set_value() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
reg.set_focused(42);
assert_eq!(reg.focused(), Some(crate::common::WindowId(42)));
}
#[test]
fn focused_returns_none_after_clear() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
reg.set_focused(42);
assert_eq!(reg.focused(), Some(crate::common::WindowId(42)));
reg.remove_window(42);
assert!(reg.focused().is_none());
}
#[test]
fn clear_focused_drops_tracked_focus() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
reg.set_focused(42);
assert_eq!(reg.focused(), Some(crate::common::WindowId(42)));
reg.clear_focused();
assert!(reg.focused.is_none(), "private field must be cleared");
assert!(reg.focused().is_none(), "public getter must report None");
}
#[test]
fn clear_focused_is_safe_noop_when_already_none() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
assert!(reg.focused.is_none());
reg.clear_focused();
assert!(reg.focused.is_none());
assert!(reg.focused().is_none());
insert_test_window(
&mut reg,
7,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
reg.clear_focused();
assert!(reg.focused.is_none());
}
#[test]
fn get_window_mut_returns_some_and_allows_mutation_for_existing() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
let window = reg
.get_window_mut(HWND(42 as *mut _))
.expect("existing window should be Some");
window.state = WindowState::Floating(FloatingState::Active {
rect: crate::common::Rect {
x: 10,
y: 10,
width: 800,
height: 600,
},
});
let after = reg.get_window(HWND(42 as *mut _)).unwrap();
assert!(
matches!(
after.state,
WindowState::Floating(FloatingState::Active { .. })
),
"state mutation via get_window_mut should persist"
);
}
#[test]
fn get_window_mut_returns_none_for_absent_hwnd() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
assert!(reg.get_window_mut(HWND(99999 as *mut _)).is_none());
insert_test_window(
&mut reg,
10,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
assert!(reg.get_window_mut(HWND(77777 as *mut _)).is_none());
reg.remove_window(10);
assert!(reg.get_window_mut(HWND(10 as *mut _)).is_none());
}
#[test]
fn register_window_from_info_inserts_new_window() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 5555isize;
let info = win32::WindowInfo {
hwnd: HWND(hwnd_val as *mut _),
title: "MyApp".to_owned(),
class: "AppClass".to_owned(),
rect: crate::common::Rect {
x: 0,
y: 0,
width: 800,
height: 600,
},
exe: "myapp.exe".to_owned(),
process_path: "C:\\myapp.exe".to_owned(),
is_visible: true,
is_maximized: false,
is_fullscreen: false,
};
assert!(reg.is_empty());
reg.register_window_from_info(&info);
assert_eq!(reg.len(), 1);
let w = reg.get_window(HWND(hwnd_val as *mut _)).unwrap();
assert_eq!(w.exe, "myapp.exe");
assert_eq!(w.title, "MyApp");
}
#[test]
fn register_window_from_info_is_noop_for_existing() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 7777isize;
let info = win32::WindowInfo {
hwnd: HWND(hwnd_val as *mut _),
title: "Original".to_owned(),
class: "TestClass".to_owned(),
rect: crate::common::Rect {
x: 0,
y: 0,
width: 640,
height: 480,
},
exe: "app.exe".to_owned(),
process_path: "C:\\app.exe".to_owned(),
is_visible: true,
is_maximized: false,
is_fullscreen: false,
};
reg.register_window_from_info(&info);
assert_eq!(reg.len(), 1);
let info2 = win32::WindowInfo {
title: "Changed".to_owned(),
..info.clone()
};
reg.register_window_from_info(&info2);
assert_eq!(reg.len(), 1);
let w = reg.get_window(HWND(hwnd_val as *mut _)).unwrap();
assert_eq!(w.title, "Original");
}
#[test]
fn register_window_from_info_maximized_becomes_ignored() {
let user = WindowRulesConfig {
default_action: WindowAction::Tile,
rules: vec![],
};
let default = WindowRulesConfig::default();
let mut reg = WindowRegistry::new(&user, &default);
let info = win32::WindowInfo {
hwnd: HWND(8888 as *mut _),
title: "MaxApp".to_owned(),
class: "MaxClass".to_owned(),
rect: crate::common::Rect {
x: 0,
y: 0,
width: 1920,
height: 1080,
},
exe: "maxapp.exe".to_owned(),
process_path: "C:\\maxapp.exe".to_owned(),
is_visible: true,
is_maximized: true,
is_fullscreen: false,
};
reg.register_window_from_info(&info);
let w = reg.get_window(HWND(8888 as *mut _)).unwrap();
assert!(matches!(
w.state,
WindowState::Ignored(IgnoredReason::Maximized)
));
}
#[test]
fn tiling_window_ids_returns_active_tiling_only() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
100,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
insert_test_window(&mut reg, 200, WindowState::Tiling(TilingState::Minimized));
insert_test_window(
&mut reg,
300,
WindowState::Floating(FloatingState::Active {
rect: crate::common::Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
}),
);
insert_test_window(
&mut reg,
400,
WindowState::Ignored(IgnoredReason::Maximized),
);
let ids = reg.tiling_window_ids();
assert_eq!(ids.len(), 1);
assert!(ids.contains(&crate::common::WindowId(100)));
}
#[test]
fn tiling_window_ids_empty_registry() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
assert!(reg.tiling_window_ids().is_empty());
}
#[test]
fn tiling_window_ids_multiple_active() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
10,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
insert_test_window(
&mut reg,
20,
WindowState::Tiling(TilingState::Active { col: 1, row: 0 }),
);
insert_test_window(
&mut reg,
30,
WindowState::Tiling(TilingState::Active { col: 2, row: 0 }),
);
let ids = reg.tiling_window_ids();
assert_eq!(ids.len(), 3);
}
#[test]
fn is_tiling_true_for_active() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
assert!(reg.is_tiling(42));
}
#[test]
fn is_tiling_true_for_minimized() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(&mut reg, 42, WindowState::Tiling(TilingState::Minimized));
assert!(reg.is_tiling(42));
}
#[test]
fn is_tiling_false_for_floating() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Floating(FloatingState::Active {
rect: crate::common::Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
}),
);
assert!(!reg.is_tiling(42));
}
#[test]
fn is_tiling_false_for_ignored() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(&mut reg, 42, WindowState::Ignored(IgnoredReason::Maximized));
assert!(!reg.is_tiling(42));
}
#[test]
fn is_tiling_false_for_unknown() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
assert!(!reg.is_tiling(99999));
}
#[test]
fn handle_created_returns_none_for_nonexistent_hwnd() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let result = reg.handle_created(0);
assert!(result.is_none());
}
#[test]
fn handle_created_skips_already_tracked_tiling_window() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
assert_eq!(reg.len(), 1);
let result = reg.handle_created(42);
assert!(
result.is_none(),
"handle_created must no-op on an already-tracked HWND"
);
assert_eq!(
reg.len(),
1,
"registry size must not change on duplicate handle_created"
);
let w = reg.get_window(HWND(42 as *mut _)).unwrap();
assert!(matches!(
w.state,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 })
));
}
#[test]
fn handle_created_skips_already_tracked_non_tiling_window() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
10,
WindowState::Floating(FloatingState::Active {
rect: crate::common::Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
}),
);
insert_test_window(&mut reg, 20, WindowState::Ignored(IgnoredReason::Maximized));
assert_eq!(reg.len(), 2);
assert!(reg.handle_created(10).is_none());
assert!(reg.handle_created(20).is_none());
assert_eq!(
reg.len(),
2,
"non-tiling tracked windows must not be churned"
);
assert!(matches!(
reg.get_window(HWND(10 as *mut _)).unwrap().state,
WindowState::Floating(_)
));
assert!(matches!(
reg.get_window(HWND(20 as *mut _)).unwrap().state,
WindowState::Ignored(IgnoredReason::Maximized)
));
}
#[test]
fn sorted_by_x_returns_ascending_order() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window_with_rect(
&mut reg,
300,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
300,
);
insert_test_window_with_rect(
&mut reg,
100,
WindowState::Tiling(TilingState::Active { col: 1, row: 0 }),
100,
);
insert_test_window_with_rect(
&mut reg,
500,
WindowState::Tiling(TilingState::Active { col: 2, row: 0 }),
500,
);
let sorted = reg.tiling_window_ids_sorted_by_x();
assert_eq!(sorted.len(), 3);
assert_eq!(sorted[0], crate::common::WindowId(100));
assert_eq!(sorted[1], crate::common::WindowId(300));
assert_eq!(sorted[2], crate::common::WindowId(500));
}
#[test]
fn sorted_by_x_empty_registry() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
let sorted = reg.tiling_window_ids_sorted_by_x();
assert!(sorted.is_empty());
}
#[test]
fn sorted_by_x_single_window() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
let sorted = reg.tiling_window_ids_sorted_by_x();
assert_eq!(sorted.len(), 1);
assert_eq!(sorted[0], crate::common::WindowId(42));
}
#[test]
fn sorted_by_x_excludes_non_tiling_windows() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window_with_rect(
&mut reg,
10,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
500,
);
insert_test_window_with_rect(
&mut reg,
20,
WindowState::Floating(FloatingState::Active {
rect: crate::common::Rect {
x: 100,
y: 0,
width: 200,
height: 200,
},
}),
100,
);
insert_test_window_with_rect(
&mut reg,
30,
WindowState::Ignored(IgnoredReason::Maximized),
0,
);
let sorted = reg.tiling_window_ids_sorted_by_x();
assert_eq!(sorted.len(), 1);
assert_eq!(sorted[0], crate::common::WindowId(10));
}
#[test]
fn sorted_by_x_handles_equal_x_positions() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window_with_rect(
&mut reg,
10,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
200,
);
insert_test_window_with_rect(
&mut reg,
20,
WindowState::Tiling(TilingState::Active { col: 1, row: 0 }),
200,
);
let sorted = reg.tiling_window_ids_sorted_by_x();
assert_eq!(sorted.len(), 2);
assert!(sorted.contains(&crate::common::WindowId(10)));
assert!(sorted.contains(&crate::common::WindowId(20)));
}
#[test]
fn sorted_with_widths_returns_ids_and_widths() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window_with_rect_and_width(
&mut reg,
300,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
300,
800,
);
insert_test_window_with_rect_and_width(
&mut reg,
100,
WindowState::Tiling(TilingState::Active { col: 1, row: 0 }),
100,
1200,
);
insert_test_window_with_rect_and_width(
&mut reg,
500,
WindowState::Tiling(TilingState::Active { col: 2, row: 0 }),
500,
600,
);
let result = reg.tiling_window_ids_with_widths_sorted_by_x();
assert_eq!(result.len(), 3);
assert_eq!(result[0].0, crate::common::WindowId(100));
assert_eq!(result[0].1, 1200);
assert_eq!(result[1].0, crate::common::WindowId(300));
assert_eq!(result[1].1, 800);
assert_eq!(result[2].0, crate::common::WindowId(500));
assert_eq!(result[2].1, 600);
}
#[test]
fn sorted_with_widths_empty_registry() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
let result = reg.tiling_window_ids_with_widths_sorted_by_x();
assert!(result.is_empty());
}
#[test]
fn sorted_with_widths_single_window() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window_with_rect_and_width(
&mut reg,
42,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
100,
800,
);
let result = reg.tiling_window_ids_with_widths_sorted_by_x();
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, crate::common::WindowId(42));
assert_eq!(result[0].1, 800);
}
#[test]
fn sorted_with_widths_excludes_non_tiling_windows() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window_with_rect_and_width(
&mut reg,
10,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
500,
800,
);
insert_test_window_with_rect_and_width(
&mut reg,
20,
WindowState::Floating(FloatingState::Active {
rect: crate::common::Rect {
x: 100,
y: 0,
width: 200,
height: 200,
},
}),
100,
1200,
);
insert_test_window_with_rect_and_width(
&mut reg,
30,
WindowState::Ignored(IgnoredReason::Maximized),
0,
600,
);
let result = reg.tiling_window_ids_with_widths_sorted_by_x();
assert_eq!(result.len(), 1, "only the tiling-active window must appear");
assert_eq!(result[0].0, crate::common::WindowId(10));
assert_eq!(result[0].1, 800);
}
#[test]
fn sorted_with_widths_clamps_negative_width_to_zero() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window_with_rect_and_width(
&mut reg,
10,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
100,
-200,
);
let result = reg.tiling_window_ids_with_widths_sorted_by_x();
assert_eq!(result.len(), 1);
assert_eq!(
result[0].1, 0,
"negative width must clamp to 0, not wrap to a huge u32"
);
}
#[test]
fn direct_destroy_handler_works() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 33isize;
insert_test_window(
&mut reg,
hwnd_val,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
reg.remove_window(hwnd_val);
assert!(reg.is_empty());
}
#[test]
fn direct_foreground_handler_works() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let hwnd_val = 44isize;
insert_test_window(
&mut reg,
hwnd_val,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
reg.set_focused(hwnd_val);
assert_eq!(reg.focused, Some(hwnd_val));
}
#[test]
fn is_tiling_active_true_for_tiling_active() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
assert!(reg.is_tiling_active(42));
}
#[test]
fn is_tiling_active_false_for_tiling_minimized() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(&mut reg, 42, WindowState::Tiling(TilingState::Minimized));
assert!(!reg.is_tiling_active(42));
}
#[test]
fn is_tiling_active_false_for_tiling_hidden() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(&mut reg, 42, WindowState::Tiling(TilingState::Hidden));
assert!(!reg.is_tiling_active(42));
}
#[test]
fn is_tiling_active_false_for_untracked() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
assert!(!reg.is_tiling_active(99999));
}
#[test]
fn is_tiling_active_false_for_floating() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Floating(FloatingState::Active {
rect: crate::common::Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
}),
);
assert!(!reg.is_tiling_active(42));
}
#[test]
fn visibility_change_variants_are_comparable() {
use super::super::types::VisibilityChange;
assert_eq!(VisibilityChange::Hidden, VisibilityChange::Hidden);
assert_eq!(VisibilityChange::Shown, VisibilityChange::Shown);
assert_eq!(VisibilityChange::Unchanged, VisibilityChange::Unchanged);
assert_ne!(VisibilityChange::Hidden, VisibilityChange::Shown);
assert_ne!(VisibilityChange::Shown, VisibilityChange::Unchanged);
assert_ne!(VisibilityChange::Hidden, VisibilityChange::Unchanged);
}
#[test]
fn reconcile_visibility_untracked_returns_unchanged() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let result = reg.reconcile_visibility(12345);
assert_eq!(result, super::super::types::VisibilityChange::Unchanged);
}
#[test]
fn reconcile_visibility_signature_compiles() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let _result: super::super::types::VisibilityChange = reg.reconcile_visibility(0);
}
#[test]
fn is_tracked_true_for_registered_window() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
assert!(reg.is_tracked(42));
}
#[test]
fn is_tracked_false_for_unknown_hwnd() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
assert!(!reg.is_tracked(99999));
}
#[test]
fn is_tracked_false_after_removal() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
77,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
assert!(reg.is_tracked(77));
reg.remove_window(77);
assert!(!reg.is_tracked(77));
}
#[test]
fn is_tracked_works_for_all_window_states() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
10,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
insert_test_window(
&mut reg,
20,
WindowState::Floating(FloatingState::Active {
rect: crate::common::Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
}),
);
insert_test_window(&mut reg, 30, WindowState::Ignored(IgnoredReason::Maximized));
assert!(reg.is_tracked(10), "tiling window should be tracked");
assert!(reg.is_tracked(20), "floating window should be tracked");
assert!(reg.is_tracked(30), "ignored window should be tracked");
}
#[test]
fn reclassify_os_state_untracked_for_unknown_hwnd() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let result = reg.reclassify_os_state(99999);
assert_eq!(result, ReclassifyResult::Untracked);
}
#[test]
fn reclassify_os_state_untracked_after_removal() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(&mut reg, 55, WindowState::Ignored(IgnoredReason::Maximized));
reg.remove_window(55);
let result = reg.reclassify_os_state(55);
assert_eq!(result, ReclassifyResult::Untracked);
}
#[test]
fn reclassify_os_state_not_applicable_for_tiling_window() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
42,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
let result = reg.reclassify_os_state(42);
assert_eq!(result, ReclassifyResult::NotApplicable);
}
#[test]
fn reclassify_os_state_not_applicable_for_floating_window() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
88,
WindowState::Floating(FloatingState::Active {
rect: crate::common::Rect {
x: 0,
y: 0,
width: 200,
height: 200,
},
}),
);
let result = reg.reclassify_os_state(88);
assert_eq!(result, ReclassifyResult::NotApplicable);
}
#[test]
fn reclassify_os_state_not_applicable_for_explicit_rule_ignored() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
33,
WindowState::Ignored(IgnoredReason::ExplicitRule),
);
let result = reg.reclassify_os_state(33);
assert_eq!(result, ReclassifyResult::NotApplicable);
}
#[test]
fn reclassify_os_state_not_applicable_for_tiling_minimized() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(&mut reg, 44, WindowState::Tiling(TilingState::Minimized));
let result = reg.reclassify_os_state(44);
assert_eq!(result, ReclassifyResult::NotApplicable);
}
#[test]
fn reclassify_result_variants_are_comparable() {
assert_eq!(ReclassifyResult::Untracked, ReclassifyResult::Untracked);
assert_eq!(
ReclassifyResult::NotApplicable,
ReclassifyResult::NotApplicable
);
assert_eq!(ReclassifyResult::Unchanged, ReclassifyResult::Unchanged);
assert_eq!(
ReclassifyResult::Recovered { now_tiling: true },
ReclassifyResult::Recovered { now_tiling: true }
);
assert_eq!(
ReclassifyResult::Recovered { now_tiling: false },
ReclassifyResult::Recovered { now_tiling: false }
);
assert_ne!(ReclassifyResult::Untracked, ReclassifyResult::NotApplicable);
assert_ne!(ReclassifyResult::NotApplicable, ReclassifyResult::Unchanged);
assert_ne!(
ReclassifyResult::Unchanged,
ReclassifyResult::Recovered { now_tiling: true }
);
assert_ne!(
ReclassifyResult::Recovered { now_tiling: true },
ReclassifyResult::Recovered { now_tiling: false }
);
}
fn collect_restorable(
reg: &WindowRegistry,
) -> std::collections::HashMap<isize, crate::common::Rect> {
reg.restorable_windows()
.into_iter()
.map(|(k, r, _)| (k, r))
.collect()
}
#[test]
fn restorable_windows_empty_registry() {
let (user, default) = default_rules();
let reg = WindowRegistry::new(&user, &default);
assert!(reg.restorable_windows().is_empty());
}
#[test]
fn restorable_windows_includes_only_active_tiling() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window_with_rect(
&mut reg,
101,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
10,
);
insert_test_window_with_rect(
&mut reg,
102,
WindowState::Tiling(TilingState::Minimized),
20,
);
insert_test_window_with_rect(&mut reg, 103, WindowState::Tiling(TilingState::Hidden), 30);
let map = collect_restorable(®);
assert_eq!(map.len(), 1);
assert!(map.contains_key(&101));
assert!(!map.contains_key(&102), "Minimized must not be restorable");
assert!(!map.contains_key(&103), "Hidden must not be restorable");
}
#[test]
fn restorable_windows_includes_only_active_floating() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window_with_rect(
&mut reg,
201,
WindowState::Floating(FloatingState::Active {
rect: crate::common::Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
}),
10,
);
insert_test_window_with_rect(
&mut reg,
202,
WindowState::Floating(FloatingState::Minimized),
20,
);
insert_test_window_with_rect(
&mut reg,
203,
WindowState::Floating(FloatingState::Hidden),
30,
);
let map = collect_restorable(®);
assert_eq!(map.len(), 1);
assert!(map.contains_key(&201));
assert!(!map.contains_key(&202), "Minimized must not be restorable");
assert!(!map.contains_key(&203), "Hidden must not be restorable");
}
#[test]
fn restorable_windows_excludes_all_ignored_variants() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
301,
WindowState::Ignored(IgnoredReason::Maximized),
);
insert_test_window(
&mut reg,
302,
WindowState::Ignored(IgnoredReason::Fullscreen),
);
insert_test_window(
&mut reg,
303,
WindowState::Ignored(IgnoredReason::ExplicitRule),
);
insert_test_window(
&mut reg,
304,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
let map = collect_restorable(®);
assert_eq!(map.len(), 1);
assert!(map.contains_key(&304));
assert!(!map.contains_key(&301));
assert!(!map.contains_key(&302));
assert!(!map.contains_key(&303));
}
#[test]
fn restorable_windows_returns_pre_manage_rect_as_anchor() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let anchor_x = 4242;
insert_test_window_with_rect(
&mut reg,
401,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
anchor_x,
);
let map = collect_restorable(®);
let rect = map
.get(&401)
.expect("controlled window should be present in restorable set");
assert_eq!(rect.x, anchor_x);
assert_eq!(rect.y, 0);
assert_eq!(rect.width, 100);
assert_eq!(rect.height, 100);
}
#[test]
fn restorable_windows_returns_pre_manage_rect_not_tiled_rect() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let pre_manage = crate::common::Rect {
x: 100,
y: 100,
width: 800,
height: 600,
};
let tiled = crate::common::Rect {
x: 5000,
y: 5000,
width: 400,
height: 300,
};
let hwnd_val = 4242isize;
let mut window = Window::new(
HWND(hwnd_val as *mut _),
"anchor.exe".into(),
"Anchor".into(),
"AnchorClass".into(),
std::path::PathBuf::from("C:\\anchor.exe"),
pre_manage,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
crate::common::InvisibleBounds::zero(),
);
window.tiled_rect = Some(tiled);
reg.windows.insert(hwnd_val, window);
let map = collect_restorable(®);
let returned = map
.get(&hwnd_val)
.expect("controlled window should be present in restorable set");
assert_eq!(
*returned, pre_manage,
"rescue anchor must be pre_manage_rect, not tiled_rect"
);
assert_ne!(
*returned, tiled,
"rescue anchor must NOT be the current (off-screen) tiled rect"
);
}
#[test]
fn restorable_windows_returns_invisible_bounds() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
let bounds = crate::common::InvisibleBounds {
left: 7,
top: 0,
right: 7,
bottom: 7,
};
let hwnd_val = 5001isize;
let window = Window::new(
HWND(hwnd_val as *mut _),
"bounded.exe".into(),
"Bounded".into(),
"BoundedClass".into(),
std::path::PathBuf::from("C:\\bounded.exe"),
crate::common::Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
bounds,
);
reg.windows.insert(hwnd_val, window);
let got = reg.restorable_windows();
assert_eq!(got.len(), 1);
assert_eq!(got[0].0, hwnd_val);
assert_eq!(got[0].2, bounds, "invisible_bounds must round-trip");
}
#[test]
fn restorable_windows_mixed_registry_filters_correctly() {
let (user, default) = default_rules();
let mut reg = WindowRegistry::new(&user, &default);
insert_test_window(
&mut reg,
1,
WindowState::Tiling(TilingState::Active { col: 0, row: 0 }),
);
insert_test_window(&mut reg, 2, WindowState::Tiling(TilingState::Minimized));
insert_test_window(
&mut reg,
3,
WindowState::Floating(FloatingState::Active {
rect: crate::common::Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
}),
);
insert_test_window(&mut reg, 4, WindowState::Floating(FloatingState::Hidden));
insert_test_window(&mut reg, 5, WindowState::Ignored(IgnoredReason::Maximized));
insert_test_window(
&mut reg,
6,
WindowState::Ignored(IgnoredReason::ExplicitRule),
);
let map = collect_restorable(®);
assert_eq!(map.len(), 2);
for key in [1, 3] {
assert!(map.contains_key(&key), "active hwnd {key} missing");
}
for key in [2, 4, 5, 6] {
assert!(!map.contains_key(&key), "non-active hwnd {key} leaked");
}
}
#[test]
fn set_user_rules_delegates_to_the_classification_pipeline() {
use crate::config::types::MatchRule;
use classification::WindowCandidate;
let user = WindowRulesConfig {
default_action: WindowAction::Tile,
rules: vec![WindowRule {
match_: MatchRule {
exe: Some("pinned.exe".into()),
..Default::default()
},
action: WindowAction::Ignore,
initial_width_px: None,
override_persist: false,
}],
};
let default = WindowRulesConfig::default();
let mut reg = WindowRegistry::new(&user, &default);
let pinned = WindowCandidate {
exe: "pinned.exe".into(),
title: String::new(),
class: String::new(),
process_path: String::new(),
};
let other = WindowCandidate {
exe: "other.exe".into(),
title: String::new(),
class: String::new(),
process_path: String::new(),
};
assert_eq!(reg.pipeline.classify(&pinned), WindowAction::Ignore);
assert_eq!(reg.pipeline.classify(&other), WindowAction::Tile);
reg.set_user_rules(WindowRulesConfig {
default_action: WindowAction::Float,
rules: vec![],
});
assert_eq!(
reg.pipeline.classify(&pinned),
WindowAction::Float,
"registry.set_user_rules must drop old rules via delegation"
);
assert_eq!(
reg.pipeline.classify(&other),
WindowAction::Float,
"registry.set_user_rules must refresh default_action via delegation"
);
}
use has_parent_test_helpers::*;
mod has_parent_test_helpers {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::Graphics::Gdi::HBRUSH;
use windows::Win32::UI::WindowsAndMessaging::{
CS_HREDRAW, CS_VREDRAW, CreateWindowExW, DefWindowProcW, DestroyWindow, HCURSOR, HICON,
RegisterClassExW, WINDOW_EX_STYLE, WNDCLASSEXW, WS_CHILD, WS_OVERLAPPEDWINDOW,
};
use windows::core::PCWSTR;
pub unsafe extern "system" fn test_wnd_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
pub fn wide(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
pub struct RealTestWindows {
pub parent: HWND,
pub child: HWND,
}
impl RealTestWindows {
pub fn create() -> Option<Self> {
let class_name = wide("FlowHasParentTestClass");
let wnd_class = WNDCLASSEXW {
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(test_wnd_proc),
cbClsExtra: 0,
cbWndExtra: 0,
hInstance: HINSTANCE::default(),
hIcon: HICON::default(),
hCursor: HCURSOR::default(),
hbrBackground: HBRUSH::default(),
lpszMenuName: PCWSTR::null(),
lpszClassName: PCWSTR(class_name.as_ptr()),
hIconSm: HICON::default(),
};
unsafe {
let _ = RegisterClassExW(&wnd_class);
}
let parent = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
PCWSTR(class_name.as_ptr()),
PCWSTR(wide("FlowHasParentTopLevel").as_ptr()),
WS_OVERLAPPEDWINDOW,
0,
0,
100,
100,
None,
None,
None,
None,
)
}
.ok()?;
let child = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
PCWSTR(class_name.as_ptr()),
PCWSTR(wide("FlowHasParentChild").as_ptr()),
WS_CHILD,
0,
0,
50,
50,
Some(parent),
None,
None,
None,
)
}
.ok()?;
Some(Self { parent, child })
}
}
impl Drop for RealTestWindows {
fn drop(&mut self) {
unsafe {
let _ = DestroyWindow(self.child);
let _ = DestroyWindow(self.parent);
}
}
}
}
#[test]
fn has_parent_returns_false_for_toplevel_window() {
let windows = match RealTestWindows::create() {
Some(w) => w,
None => {
eprintln!("skipping: real window creation failed (headless test environment?)");
return;
}
};
assert!(
!has_parent(windows.parent),
"top-level window should not be flagged as WS_CHILD"
);
}
#[test]
fn has_parent_returns_true_for_child_window() {
let windows = match RealTestWindows::create() {
Some(w) => w,
None => {
eprintln!("skipping: real window creation failed (headless test environment?)");
return;
}
};
assert!(
has_parent(windows.child),
"WS_CHILD window should be flagged as having a parent"
);
assert!(
!has_owner(windows.child),
"WS_CHILD controls should have no GW_OWNER — that's why has_parent exists"
);
}
}