use std::time::{Duration, Instant};
use windows::Win32::Foundation::HWND;
use crate::borders::{BorderState, style_for_state};
use crate::common::{Direction, Rect, WindowId};
use crate::layout::mutations::ensure_column_visible;
use crate::layout::preview::{DropZone, preview_move, resolve_drop_zone};
use crate::layout::projection;
use crate::layout::resize::{
DragKind, ResizeEdge, VerticalEdge, classify_drag, resize_column_boundary_move,
resize_row_boundary_move,
};
use crate::layout::types::AppliedLayout;
use crate::registry::hooks::{clear_dragged_hwnd, set_dragged_hwnd};
use crate::registry::types::{TilingState, WindowState};
use crate::registry::win32 as registry_win32;
use super::borders::float_border_rect;
use super::edge_scroll::{EdgeScrollAction, EdgeScrollScheduler, EdgeScrollTimings};
use super::types::FlowWM;
pub(super) enum DragMode {
Classifying(ClassifyingDrag),
Translate(DragState),
Resize(ResizeDrag),
}
impl DragMode {
#[must_use]
pub(super) fn dragged_id(&self) -> WindowId {
match self {
DragMode::Classifying(c) => c.dragged_id,
DragMode::Translate(t) => t.dragged_id,
DragMode::Resize(r) => r.dragged_id,
}
}
#[must_use]
pub(super) fn dragged_hwnd(&self) -> isize {
match self {
DragMode::Classifying(c) => c.dragged_hwnd,
DragMode::Translate(t) => t.dragged_hwnd,
DragMode::Resize(r) => r.dragged_hwnd,
}
}
#[must_use]
pub(super) fn edge_scroll_deadline(&self) -> Option<Instant> {
match self {
DragMode::Translate(t) => t.edge_scroll_deadline,
DragMode::Classifying(_) | DragMode::Resize(_) => None,
}
}
}
pub(super) struct ClassifyingDrag {
pub(super) dragged_id: WindowId,
pub(super) dragged_hwnd: isize,
pub(super) start_rect: Rect,
}
pub(super) struct DragState {
pub(super) dragged_id: WindowId,
pub(super) dragged_hwnd: isize,
pub(super) current_zone: Option<DropZone>,
pub(super) edge_scroll: EdgeScrollScheduler,
pub(super) edge_scroll_timings: EdgeScrollTimings,
pub(super) edge_scroll_deadline: Option<Instant>,
}
pub(super) struct ResizeDrag {
pub(super) dragged_id: WindowId,
pub(super) dragged_hwnd: isize,
pub(super) grip_h: ResizeEdge,
pub(super) grip_v: VerticalEdge,
pub(super) col: usize,
pub(super) row: usize,
}
impl FlowWM {
pub(super) fn on_drag_start(&mut self, hwnd: isize) {
let hwnd_handle = HWND(hwnd as *mut _);
let Some(window) = self.registry.get_window(hwnd_handle) else {
return;
};
if !matches!(
window.state,
WindowState::Tiling(TilingState::Active { .. })
) {
return;
}
let start_rect = registry_win32::get_window_rect(hwnd_handle).unwrap_or(Rect {
x: 0,
y: 0,
width: 0,
height: 0,
});
self.drag_state = Some(DragMode::Classifying(ClassifyingDrag {
dragged_id: WindowId(hwnd),
dragged_hwnd: hwnd,
start_rect,
}));
set_dragged_hwnd(hwnd);
let focused_style = style_for_state(&self.config.borders, BorderState::Focused);
if let Some(win) = self.registry.get_window_mut(hwnd_handle)
&& let Some(border) = win.border.as_ref()
{
border.set_style(focused_style);
}
log::debug!("drag start: hwnd={hwnd} (classifying)");
}
pub(super) fn on_drag_move(&mut self, hwnd: isize) {
if matches!(self.drag_state.as_ref(), Some(DragMode::Classifying(_))) {
self.classify_and_promote(hwnd);
}
match self.drag_state.as_ref() {
Some(DragMode::Translate(_)) => self.on_translate_move(hwnd),
Some(DragMode::Resize(_)) => self.on_resize_move(hwnd),
_ => {}
}
}
fn classify_and_promote(&mut self, hwnd: isize) {
let classifying = match self.drag_state.take() {
Some(DragMode::Classifying(c)) => c,
other => {
self.drag_state = other;
return;
}
};
let hwnd_handle = HWND(hwnd as *mut _);
let current_rect = match registry_win32::get_window_rect(hwnd_handle) {
Ok(r) => r,
Err(e) => {
log::debug!("classify: GetWindowRect failed for {hwnd}: {e}");
self.drag_state = Some(DragMode::Classifying(classifying));
return;
}
};
match classify_drag(classifying.start_rect, current_rect) {
DragKind::None => {
self.drag_state = Some(DragMode::Classifying(classifying));
}
DragKind::Translate => {
let edge_scroll_timings = self.compute_edge_scroll_timings();
self.drag_state = Some(DragMode::Translate(DragState {
dragged_id: classifying.dragged_id,
dragged_hwnd: classifying.dragged_hwnd,
current_zone: None,
edge_scroll: EdgeScrollScheduler::new(),
edge_scroll_timings,
edge_scroll_deadline: None,
}));
log::debug!("drag classify: hwnd={hwnd} → translate");
}
DragKind::Resize {
horizontal,
vertical,
} => {
let has_h = matches!(horizontal, ResizeEdge::Left | ResizeEdge::Right);
let has_v = matches!(vertical, VerticalEdge::Top | VerticalEdge::Bottom);
if !has_h && !has_v {
self.drag_state = Some(DragMode::Classifying(classifying));
return;
}
let pos = self
.active_scrolling()
.virtual_layout()
.find_window(classifying.dragged_id);
match pos {
Some((col, row)) => {
self.drag_state = Some(DragMode::Resize(ResizeDrag {
dragged_id: classifying.dragged_id,
dragged_hwnd: classifying.dragged_hwnd,
grip_h: horizontal,
grip_v: vertical,
col,
row,
}));
log::debug!(
"drag classify: hwnd={hwnd} → resize grip_h={horizontal:?} grip_v={vertical:?} col={col} row={row}"
);
}
None => {
log::debug!(
"drag classify: hwnd={hwnd} not in layout; staying Classifying"
);
self.drag_state = Some(DragMode::Classifying(classifying));
}
}
}
}
}
fn compute_edge_scroll_timings(&self) -> EdgeScrollTimings {
let drag_cfg = &self.config.drag;
let repeat_ms = drag_cfg.effective_repeat_interval_ms(&self.config.animation);
let initial_ms = drag_cfg.effective_initial_delay_ms(repeat_ms);
EdgeScrollTimings {
initial_delay: Duration::from_millis(u64::from(initial_ms)),
repeat_interval: Duration::from_millis(u64::from(repeat_ms)),
}
}
fn on_resize_move(&mut self, hwnd: isize) {
let (dragged_hwnd, grip_h, grip_v, col, row) = match self.drag_state.as_ref() {
Some(DragMode::Resize(r)) => (r.dragged_hwnd, r.grip_h, r.grip_v, r.col, r.row),
_ => return,
};
let hwnd_handle = HWND(dragged_hwnd as *mut _);
let (visible_rect, border) = match registry_win32::get_window_rect(hwnd_handle) {
Ok(r) => {
let window = self.registry.get_window(hwnd_handle);
let invisible_bounds = window.map(|w| w.invisible_bounds).unwrap_or_default();
let visible_rect = invisible_bounds.window_to_visible(r);
let border = window.and_then(|w| w.border.clone());
(visible_rect, border)
}
Err(e) => {
log::debug!("resize move: GetWindowRect failed for {hwnd}: {e}");
return;
}
};
if let Some(border) = border.as_ref() {
border.set_geometry(float_border_rect(
visible_rect,
self.config.borders.thickness,
self.config.borders.overlap,
));
}
let (vl, config, monitor) = {
let space = self.active_scrolling();
(
space.virtual_layout().clone(),
*space.config(),
*space.monitor(),
)
};
let preview_vl = vl.clone();
let preview_vl = match grip_h {
ResizeEdge::Left | ResizeEdge::Right => {
match resize_column_boundary_move(
&preview_vl,
col,
grip_h,
visible_rect.width,
&config,
) {
Some(v) => v,
None => preview_vl,
}
}
ResizeEdge::None => preview_vl,
};
let preview_vl = match grip_v {
VerticalEdge::Top | VerticalEdge::Bottom => {
match resize_row_boundary_move(
&preview_vl,
col,
row,
grip_v,
visible_rect.height,
&config,
) {
Some(v) => v,
None => preview_vl,
}
}
VerticalEdge::None => preview_vl,
};
if preview_vl == vl {
return;
}
let actual = projection::project(&preview_vl, &monitor, &config.padding);
let applied = AppliedLayout {
virtual_layout: preview_vl,
actual_layout: actual,
};
self.teleport_preview(&applied);
}
fn on_translate_move(&mut self, hwnd: isize) {
let Some(drag) = self.drag_state.as_ref() else {
return;
};
let dragged_id = drag.dragged_id();
let hwnd_handle = HWND(hwnd as *mut _);
let window_rect = match registry_win32::get_window_rect(hwnd_handle) {
Ok(r) => r,
Err(e) => {
log::debug!("drag move: GetWindowRect failed for {hwnd}: {e}");
return;
}
};
{
let Some(window) = self.registry.get_window(hwnd_handle) else {
return;
};
let visible_rect = window.invisible_bounds.window_to_visible(window_rect);
let border_rect = float_border_rect(
visible_rect,
self.config.borders.thickness,
self.config.borders.overlap,
);
if let Some(border) = window.border.as_ref() {
border.set_geometry(border_rect);
}
}
let (cx, cy) = match registry_win32::get_cursor_pos() {
Ok(pos) => pos,
Err(e) => {
log::debug!("drag move: GetCursorPos failed: {e}");
return;
}
};
let (applied, config, monitor) = {
let space = self.active_scrolling();
(
AppliedLayout {
virtual_layout: space.virtual_layout().clone(),
actual_layout: space.actual_layout().clone(),
},
*space.config(),
*space.monitor(),
)
};
let drag_cfg = &self.config.drag;
let new_zone = resolve_drop_zone(
&applied,
&monitor,
dragged_id,
cx,
cy,
drag_cfg.edge_scroll_width,
drag_cfg.col_edge_ratio,
drag_cfg.col_edge_max_px,
);
let prev_zone = match self.drag_state.as_ref() {
Some(DragMode::Translate(t)) => t.current_zone,
_ => None,
};
if let Some(DragMode::Translate(t)) = self.drag_state.as_mut() {
t.current_zone = new_zone;
}
let prev_dir = scroll_direction(prev_zone);
let new_dir = scroll_direction(new_zone);
if new_dir != prev_dir {
if prev_dir.is_some() {
let action = match self.drag_state.as_mut() {
Some(DragMode::Translate(t)) => t.edge_scroll.on_leave(),
_ => EdgeScrollAction::Cancel,
};
self.apply_edge_scroll_action(action);
}
if let Some(dir) = new_dir {
{
if let Some(DragMode::Translate(t)) = self.drag_state.as_mut() {
let _ = t.edge_scroll.on_enter(dir);
}
}
self.scroll_once_and_rearm(dir);
}
}
match new_zone {
None => {
}
Some(DropZone::ScrollLeft | DropZone::ScrollRight) => {}
Some(zone @ (DropZone::Row { .. } | DropZone::Column { .. }))
if should_submit_preview(prev_zone, new_zone) =>
{
match preview_move(&applied.virtual_layout, dragged_id, zone, &config, &monitor) {
Some(preview) => self.animate_preview(&preview),
None => self.animate_preview(&applied),
}
}
Some(DropZone::Row { .. } | DropZone::Column { .. }) => {}
}
if let Some(zone) = new_zone {
log::debug!("drag move: hwnd={hwnd} zone={zone:?}");
}
}
pub(super) fn on_drag_end(&mut self, _hwnd: isize) {
let Some(mut drag) = self.drag_state.take() else {
return;
};
let dragged_hwnd = drag.dragged_hwnd();
if let DragMode::Translate(t) = &mut drag {
let _ = t.edge_scroll.on_drag_end();
}
clear_dragged_hwnd();
if self
.registry
.get_window(HWND(dragged_hwnd as *mut _))
.is_none()
{
log::debug!("drag end: window {dragged_hwnd} destroyed, skipping commit");
return;
}
match drag {
DragMode::Translate(t) => self.on_translate_end(t),
DragMode::Resize(r) => self.on_resize_end(r),
DragMode::Classifying(c) => self.on_classifying_end(c),
}
}
fn on_translate_end(&mut self, drag: DragState) {
let (vl, config, monitor) = {
let space = self.active_scrolling();
(
space.virtual_layout().clone(),
*space.config(),
*space.monitor(),
)
};
let target_vl = match drag.current_zone {
Some(zone @ (DropZone::Row { .. } | DropZone::Column { .. })) => {
preview_move(&vl, drag.dragged_id, zone, &config, &monitor)
.map(|p| p.virtual_layout)
.unwrap_or(vl)
}
_ => vl,
};
let target_vl = match target_vl.find_window(drag.dragged_id) {
Some((col, _)) => ensure_column_visible(&target_vl, col, &config),
None => target_vl,
};
let applied = self.active_scrolling_mut().commit_layout(target_vl);
self.animate_layout(&applied);
self.refresh_border_for(drag.dragged_hwnd);
log::debug!("drag end (translate): hwnd={}", drag.dragged_hwnd);
}
fn on_resize_end(&mut self, drag: ResizeDrag) {
let (vl, config) = {
let space = self.active_scrolling();
(space.virtual_layout().clone(), *space.config())
};
let hwnd_handle = HWND(drag.dragged_hwnd as *mut _);
let (fallback_width, fallback_height) = vl
.columns
.get(drag.col)
.and_then(|c| c.rows.get(drag.row))
.map(|r| (vl.columns[drag.col].width_px, r.height))
.unwrap_or((config.column_width as i32, config.available_height()));
let visible_rect = match registry_win32::get_window_rect(hwnd_handle) {
Ok(r) => {
let invisible_bounds = self
.registry
.get_window(hwnd_handle)
.map(|w| w.invisible_bounds)
.unwrap_or_default();
invisible_bounds.window_to_visible(r)
}
Err(e) => {
log::debug!(
"resize end: GetWindowRect failed for {}: {e}; committing layout as-is",
drag.dragged_hwnd
);
Rect {
x: 0,
y: 0,
width: fallback_width,
height: fallback_height,
}
}
};
let target_vl = vl.clone();
let target_vl = match drag.grip_h {
ResizeEdge::Left | ResizeEdge::Right => resize_column_boundary_move(
&target_vl,
drag.col,
drag.grip_h,
visible_rect.width,
&config,
)
.unwrap_or(target_vl),
ResizeEdge::None => target_vl,
};
let target_vl = match drag.grip_v {
VerticalEdge::Top | VerticalEdge::Bottom => resize_row_boundary_move(
&target_vl,
drag.col,
drag.row,
drag.grip_v,
visible_rect.height,
&config,
)
.unwrap_or(target_vl),
VerticalEdge::None => target_vl,
};
let target_vl = ensure_column_visible(&target_vl, drag.col, &config);
let applied = self.active_scrolling_mut().commit_layout(target_vl);
self.animate_layout(&applied);
self.refresh_border_for(drag.dragged_hwnd);
log::debug!(
"drag end (resize): hwnd={} grip_h={:?} grip_v={:?} col={} row={}",
drag.dragged_hwnd,
drag.grip_h,
drag.grip_v,
drag.col,
drag.row,
);
}
fn on_classifying_end(&mut self, drag: ClassifyingDrag) {
let vl = self.active_scrolling().virtual_layout().clone();
let applied = self.active_scrolling_mut().commit_layout(vl);
self.animate_layout(&applied);
self.refresh_border_for(drag.dragged_hwnd);
log::debug!("drag end (classifying/no-op): hwnd={}", drag.dragged_hwnd);
}
fn perform_edge_scroll(&mut self, direction: Direction) -> bool {
let scrolled = match direction {
Direction::Left => self.active_scrolling_mut().scroll_left(),
Direction::Right => self.active_scrolling_mut().scroll_right(),
Direction::Up | Direction::Down => return false,
};
match scrolled {
Some(applied) => {
self.animate_layout(&applied);
true
}
None => false,
}
}
fn apply_edge_scroll_action(&mut self, action: EdgeScrollAction) {
let Some(drag) = self.drag_state.as_mut() else {
return;
};
let DragMode::Translate(t) = drag else {
return;
};
match action {
EdgeScrollAction::Arm(deadline) => t.edge_scroll_deadline = Some(deadline),
EdgeScrollAction::Cancel => t.edge_scroll_deadline = None,
EdgeScrollAction::Scroll(_) => {}
}
}
fn scroll_once_and_rearm(&mut self, direction: Direction) {
let scrolled = self.perform_edge_scroll(direction);
let (timings, action) = match self.drag_state.as_mut() {
Some(DragMode::Translate(t)) => {
let timings = t.edge_scroll_timings;
let action = t
.edge_scroll
.on_scroll_outcome(scrolled, Instant::now(), &timings);
(timings, action)
}
_ => return,
};
let _ = timings;
self.apply_edge_scroll_action(action);
}
pub(super) fn maybe_fire_edge_scroll(&mut self) {
let deadline = match self.drag_state.as_ref() {
Some(DragMode::Translate(t)) => t.edge_scroll_deadline,
_ => return,
};
let Some(deadline) = deadline else {
return;
};
if Instant::now() < deadline {
return;
}
let dir = match self.drag_state.as_mut() {
Some(DragMode::Translate(t)) => t.edge_scroll.on_timer_fired(),
_ => return,
};
let dir = match dir {
Some(EdgeScrollAction::Scroll(dir)) => dir,
_ => return,
};
self.scroll_once_and_rearm(dir);
}
}
#[must_use]
fn should_submit_preview(prev_zone: Option<DropZone>, new_zone: Option<DropZone>) -> bool {
prev_zone != new_zone
}
#[must_use]
fn scroll_direction(zone: Option<DropZone>) -> Option<Direction> {
match zone {
Some(DropZone::ScrollLeft) => Some(Direction::Left),
Some(DropZone::ScrollRight) => Some(Direction::Right),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn same_zone_is_skipped() {
let z = Some(DropZone::Column { col: 2 });
assert!(!should_submit_preview(z, z));
}
#[test]
fn different_column_zone_submits() {
assert!(should_submit_preview(
Some(DropZone::Column { col: 2 }),
Some(DropZone::Column { col: 3 }),
));
}
#[test]
fn row_vs_column_zone_submits() {
assert!(should_submit_preview(
Some(DropZone::Column { col: 2 }),
Some(DropZone::Row { col: 2, row: 0 }),
));
}
#[test]
fn first_preview_after_idle_submits() {
assert!(should_submit_preview(
None,
Some(DropZone::Column { col: 1 })
));
}
#[test]
fn scroll_band_naturally_invalidates_gate() {
let col = Some(DropZone::Column { col: 2 });
assert!(should_submit_preview(col, Some(DropZone::ScrollLeft)));
assert!(should_submit_preview(Some(DropZone::ScrollLeft), col));
}
#[test]
fn leaving_to_none_and_back_submits() {
let col = Some(DropZone::Column { col: 2 });
assert!(should_submit_preview(col, None));
assert!(should_submit_preview(None, col));
}
#[test]
fn scroll_direction_maps_bands() {
assert_eq!(
scroll_direction(Some(DropZone::ScrollLeft)),
Some(Direction::Left)
);
assert_eq!(
scroll_direction(Some(DropZone::ScrollRight)),
Some(Direction::Right)
);
}
#[test]
fn scroll_direction_off_band_is_none() {
assert_eq!(scroll_direction(Some(DropZone::Column { col: 2 })), None);
assert_eq!(
scroll_direction(Some(DropZone::Row { col: 2, row: 0 })),
None
);
assert_eq!(scroll_direction(None), None);
}
}