use ratatui::layout::Rect;
const MIN_VISIBLE_W: u16 = 20;
const MIN_VISIBLE_H: u16 = 5;
#[derive(Default)]
pub struct DragState {
offset: Option<(u16, u16)>,
border_dragged_once: bool,
}
impl DragState {
pub fn new() -> Self {
Self::default()
}
pub fn begin(&mut self, popup: Rect, col: u16, row: u16, on_border: bool, in_drag_area: bool) {
if in_drag_area {
self.offset = Some((col.saturating_sub(popup.x), row.saturating_sub(popup.y)));
if on_border {
self.border_dragged_once = true;
}
}
}
pub fn header_drag_enabled(&self) -> bool {
self.border_dragged_once
}
pub fn update(&self, area: Rect, col: u16, row: u16) -> Option<(u16, u16)> {
let (dx, dy) = self.offset?;
let max_w = area.width.saturating_sub(MIN_VISIBLE_W);
let max_h = area.height.saturating_sub(MIN_VISIBLE_H);
let nx = (col as i16 - dx as i16).clamp(0, max_w as i16) as u16;
let ny = (row as i16 - dy as i16).clamp(0, max_h as i16) as u16;
Some((nx, ny))
}
pub fn end(&mut self) {
self.offset = None;
}
pub fn is_dragging(&self) -> bool {
self.offset.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn begin_sets_offset_when_in_drag_area() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 15, 10, false, true);
assert!(drag.is_dragging());
}
#[test]
fn begin_skips_offset_when_not_in_drag_area() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 15, 10, false, false);
assert!(!drag.is_dragging());
}
#[test]
fn begin_computes_correct_offset() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 25, 15, false, true);
let result = drag.update(Rect::new(0, 0, 100, 50), 25, 15);
assert_eq!(result, Some((10, 5)));
}
#[test]
fn begin_border_drag_enables_header() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
assert!(!drag.header_drag_enabled());
drag.begin(popup, 12, 6, true, true);
assert!(drag.header_drag_enabled());
}
#[test]
fn begin_header_drag_does_not_enable_header() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 15, 7, false, false);
assert!(!drag.header_drag_enabled());
}
#[test]
fn begin_does_not_need_to_be_in_drag_area_to_enable_header() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 12, 6, true, false);
assert!(!drag.header_drag_enabled());
}
#[test]
fn header_drag_enabled_false_by_default() {
let drag = DragState::new();
assert!(!drag.header_drag_enabled());
}
#[test]
fn header_drag_enabled_true_after_border_drag() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 12, 6, true, true);
assert!(drag.header_drag_enabled());
}
#[test]
fn header_drag_enabled_stays_true_after_end() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 12, 6, true, true);
drag.end();
assert!(drag.header_drag_enabled());
}
#[test]
fn header_drag_enabled_stays_true_after_header_drag() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 12, 6, true, true);
drag.begin(popup, 15, 7, false, true);
assert!(drag.header_drag_enabled());
}
#[test]
fn update_returns_none_when_not_dragging() {
let drag = DragState::new();
assert!(drag.update(Rect::new(0, 0, 100, 50), 50, 25).is_none());
}
#[test]
fn update_returns_origin() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 15, 10, false, true);
let result = drag.update(Rect::new(0, 0, 100, 50), 40, 30);
assert_eq!(result, Some((35, 25)));
}
#[test]
fn update_clamps_nx_to_area_width_minus_min_w() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 15, 10, false, true);
let result = drag.update(Rect::new(0, 0, 100, 50), 999, 30);
let (nx, _) = result.unwrap();
assert!(nx <= 80);
}
#[test]
fn update_clamps_ny_to_area_height_minus_min_h() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 15, 10, false, true);
let result = drag.update(Rect::new(0, 0, 100, 50), 30, 999);
let (_, ny) = result.unwrap();
assert!(ny <= 45);
}
#[test]
fn update_keeps_minimum_visible_size() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 15, 10, false, true);
let result = drag.update(Rect::new(0, 0, 100, 50), 999, 999);
let (nx, ny) = result.unwrap();
assert_eq!(nx, 80);
assert_eq!(ny, 45);
}
#[test]
fn update_clamps_to_zero_when_negative() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 15, 10, false, true);
let result = drag.update(Rect::new(0, 0, 100, 50), 0, 0);
assert_eq!(result, Some((0, 0)));
}
#[test]
fn update_allows_drag_below_middle() {
let popup = Rect::new(0, 0, 40, 35);
let mut drag = DragState::new();
drag.begin(popup, 5, 1, false, true);
let result = drag.update(Rect::new(0, 0, 100, 50), 5, 26);
let (_, ny) = result.unwrap();
assert!(ny >= 25, "ny should reach middle of screen, got ny={}", ny);
}
#[test]
fn end_clears_state() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 15, 10, false, true);
assert!(drag.is_dragging());
drag.end();
assert!(!drag.is_dragging());
assert!(drag.update(Rect::new(0, 0, 100, 50), 40, 30).is_none());
}
#[test]
fn is_dragging_false_by_default() {
let drag = DragState::new();
assert!(!drag.is_dragging());
}
#[test]
fn is_dragging_true_after_begin() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 15, 10, false, true);
assert!(drag.is_dragging());
}
#[test]
fn is_dragging_false_after_end() {
let popup = Rect::new(10, 5, 40, 30);
let mut drag = DragState::new();
drag.begin(popup, 15, 10, false, true);
drag.end();
assert!(!drag.is_dragging());
}
}