use windows_sys::Win32::Foundation::{POINT, RECT};
use winspaces_win32::dpi;
pub(crate) fn pt_in_rect(rect: &RECT, pt: POINT) -> bool {
pt.x >= rect.left && pt.x <= rect.right && pt.y >= rect.top && pt.y <= rect.bottom
}
pub(crate) struct SpacesBarMetrics {
pub(crate) card_w: i32,
pub(crate) card_h: i32,
pub(crate) gap: i32,
pub(crate) start_x: i32,
pub(crate) top_y: i32,
pub(crate) plus_rect: RECT,
}
pub(crate) fn spaces_bar_metrics(
count: usize,
has_plus: bool,
width: i32,
scale: f32,
plus_label_w: i32,
) -> SpacesBarMetrics {
let px = |val: i32| dpi::px(scale, val);
let count = count.max(1) as i32;
let card_h = px(100);
let gap = px(16);
let top_y = px(28);
let plus_w = px(132).max(plus_label_w + px(24));
let margin = px(60);
let plus_total = if has_plus { plus_w + gap } else { 0 };
let avail = width - 2 * margin;
let mut card_w = px(210);
if count * card_w + (count - 1) * gap + plus_total > avail {
card_w = ((avail - plus_total - (count - 1) * gap) / count).max(px(120));
}
let total_w = count * card_w + (count - 1) * gap + plus_total;
let start_x = (width - total_w) / 2;
let plus_left = start_x + count * (card_w + gap);
let plus_rect = if has_plus {
RECT {
left: plus_left,
top: top_y,
right: plus_left + plus_w,
bottom: top_y + card_h,
}
} else {
RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
}
};
SpacesBarMetrics {
card_w,
card_h,
gap,
start_x,
top_y,
plus_rect,
}
}
pub(crate) fn target_slot_for_center(
center_x: i32,
start_x: i32,
card_w: i32,
gap: i32,
count: usize,
) -> usize {
if count <= 1 {
return 0;
}
let pitch = card_w + gap;
if pitch <= 0 {
return 0;
}
let rel_x = center_x - start_x;
let slot = (rel_x + gap / 2) / pitch;
slot.clamp(0, (count - 1) as i32) as usize
}
pub(crate) fn spaces_bar_strip_rect(bar: &SpacesBarMetrics, width: i32, scale: f32) -> RECT {
let pad = (8.0 * scale).round() as i32;
RECT {
left: 0,
top: bar.top_y - pad,
right: width,
bottom: bar.top_y + bar.card_h + pad,
}
}
pub(crate) fn floating_card_left(
card_left: i32,
delta_x: i32,
card_w: i32,
width: i32,
scale: f32,
) -> i32 {
let margin = dpi::px(scale, 20);
let max_left = width - card_w - margin;
(card_left + delta_x).min(max_left).max(margin)
}
pub fn neighbor_slot(current: usize, delta: i32, count: usize) -> Option<usize> {
let target = current as i32 + delta;
if target < 0 || target >= count as i32 {
return None;
}
Some(target as usize)
}
pub(crate) fn close_button_rect(card: &RECT, scale: f32) -> RECT {
let px = |val: i32| dpi::px(scale, val);
let d = px(20);
let pad = px(6);
RECT {
left: card.right - pad - d,
top: card.top + pad,
right: card.right - pad,
bottom: card.top + pad + d,
}
}
pub(crate) fn window_close_button_rect(card: &RECT, scale: f32) -> RECT {
let px = |val: i32| dpi::px(scale, val);
let d = px(20);
let pad = px(8);
RECT {
left: card.right - pad - d,
top: card.top + pad,
right: card.right - pad,
bottom: card.top + pad + d,
}
}
pub(crate) fn window_pin_button_rect(card: &RECT, scale: f32) -> RECT {
let px = |val: i32| dpi::px(scale, val);
let d = px(20);
let pad = px(8);
let gap = px(6);
let right = card.right - pad - d - gap;
RECT {
left: right - d,
top: card.top + pad,
right,
bottom: card.top + pad + d,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn window_pin_button_rect_sits_to_left_of_close_button() {
let card = RECT {
left: 100,
top: 100,
right: 400,
bottom: 300,
};
let close = window_close_button_rect(&card, 1.0);
let pin = window_pin_button_rect(&card, 1.0);
assert_eq!(pin.right, close.left - 6);
assert_eq!(pin.top, close.top);
assert_eq!(pin.bottom, close.bottom);
assert_eq!(pin.right - pin.left, 20);
}
fn rect_w(r: &RECT) -> i32 {
r.right - r.left
}
#[test]
fn spaces_bar_fits_four_cards_at_natural_width() {
let m = spaces_bar_metrics(4, true, 1920, 1.0, 0);
assert_eq!(m.card_w, 210);
assert!(m.start_x > 0);
assert_eq!(m.plus_rect.left, m.start_x + 4 * (m.card_w + m.gap));
assert_eq!(rect_w(&m.plus_rect), 132);
}
#[test]
fn plus_tile_grows_with_a_long_label() {
let short = spaces_bar_metrics(4, true, 1920, 1.0, 60);
assert_eq!(rect_w(&short.plus_rect), 132);
let long = spaces_bar_metrics(4, true, 1920, 1.0, 160);
assert_eq!(rect_w(&long.plus_rect), 184);
}
#[test]
fn spaces_bar_shrinks_cards_instead_of_overflowing() {
let m = spaces_bar_metrics(9, false, 1920, 1.0, 0);
assert!(m.card_w < 210);
assert!(m.card_w >= 120);
assert!(m.start_x >= 0);
let total = 9 * m.card_w + 8 * m.gap;
assert!(total <= 1920 - 2 * 60);
}
#[test]
fn spaces_bar_clamp_accounts_for_the_plus_tile() {
let without = spaces_bar_metrics(8, false, 1600, 1.0, 0);
let with = spaces_bar_metrics(8, true, 1600, 1.0, 0);
assert!(with.card_w <= without.card_w);
assert!(with.plus_rect.right <= 1600 - 60);
}
#[test]
fn spaces_bar_at_max_count_has_no_plus_rect() {
let m = spaces_bar_metrics(9, false, 3840, 1.5, 0);
assert_eq!(rect_w(&m.plus_rect), 0);
}
#[test]
fn close_button_sits_inside_the_card_corner() {
let card = RECT {
left: 100,
top: 28,
right: 310,
bottom: 128,
};
let cb = close_button_rect(&card, 1.0);
assert!(cb.left > card.left && cb.right <= card.right);
assert!(cb.top >= card.top && cb.bottom < card.bottom);
let cb2 = close_button_rect(&card, 2.0);
assert_eq!(cb2.bottom - cb2.top, 2 * (cb.bottom - cb.top));
}
#[test]
fn target_slot_for_center_resolves_correct_slots_and_transitions_at_midpoints() {
let start_x = 100;
let card_w = 200;
let gap = 20;
let count = 4;
assert_eq!(target_slot_for_center(200, start_x, card_w, gap, count), 0);
assert_eq!(target_slot_for_center(-100, start_x, card_w, gap, count), 0);
assert_eq!(target_slot_for_center(309, start_x, card_w, gap, count), 0);
assert_eq!(target_slot_for_center(310, start_x, card_w, gap, count), 1);
assert_eq!(target_slot_for_center(420, start_x, card_w, gap, count), 1);
assert_eq!(target_slot_for_center(529, start_x, card_w, gap, count), 1);
assert_eq!(target_slot_for_center(530, start_x, card_w, gap, count), 2);
assert_eq!(target_slot_for_center(860, start_x, card_w, gap, count), 3);
assert_eq!(target_slot_for_center(2000, start_x, card_w, gap, count), 3);
}
#[test]
fn bar_strip_spans_the_width_and_covers_the_lifted_card() {
let bar = spaces_bar_metrics(4, true, 1920, 1.0, 0);
let strip = spaces_bar_strip_rect(&bar, 1920, 1.0);
assert_eq!(strip.left, 0);
assert_eq!(strip.right, 1920);
assert!(strip.top <= bar.top_y - 4);
assert!(strip.bottom >= bar.top_y + bar.card_h);
let bar2 = spaces_bar_metrics(4, true, 3840, 2.0, 0);
let strip2 = spaces_bar_strip_rect(&bar2, 3840, 2.0);
assert_eq!(bar2.top_y - strip2.top, 2 * (bar.top_y - strip.top));
}
#[test]
fn floating_card_stays_inside_the_overlay_margins() {
assert_eq!(floating_card_left(100, 50, 210, 1920, 1.0), 150);
assert_eq!(floating_card_left(100, -500, 210, 1920, 1.0), 20);
assert_eq!(floating_card_left(1600, 500, 210, 1920, 1.0), 1690);
assert_eq!(floating_card_left(0, -500, 210, 1920, 2.0), 40);
assert_eq!(floating_card_left(0, 999, 210, 100, 1.0), 20);
}
#[test]
fn neighbor_slot_stops_at_both_ends() {
assert_eq!(neighbor_slot(0, 1, 4), Some(1));
assert_eq!(neighbor_slot(3, -1, 4), Some(2));
assert_eq!(neighbor_slot(0, -1, 4), None);
assert_eq!(neighbor_slot(3, 1, 4), None);
assert_eq!(neighbor_slot(0, -1, 1), None);
assert_eq!(neighbor_slot(0, 1, 1), None);
}
#[test]
fn window_close_button_rect_sits_in_top_right() {
let card = RECT {
left: 100,
top: 200,
right: 400,
bottom: 450,
};
let close = window_close_button_rect(&card, 1.0);
assert_eq!(close.right, 400 - 8);
assert_eq!(close.left, 400 - 8 - 20);
assert_eq!(close.top, 200 + 8);
assert_eq!(close.bottom, 200 + 8 + 20);
}
}