1use ratatui::layout::Rect;
2
3const MIN_VISIBLE_W: u16 = 20;
5
6const MIN_VISIBLE_H: u16 = 5;
8
9#[derive(Default)]
20pub struct DragState {
21 offset: Option<(u16, u16)>,
22 border_dragged_once: bool,
23}
24
25impl DragState {
26 pub fn new() -> Self {
27 Self::default()
28 }
29
30 pub fn begin(&mut self, popup: Rect, col: u16, row: u16, on_border: bool, in_drag_area: bool) {
37 if in_drag_area {
38 self.offset = Some((col.saturating_sub(popup.x), row.saturating_sub(popup.y)));
39 if on_border {
40 self.border_dragged_once = true;
41 }
42 }
43 }
44
45 pub fn header_drag_enabled(&self) -> bool {
50 self.border_dragged_once
51 }
52
53 pub fn update(&self, area: Rect, col: u16, row: u16) -> Option<(u16, u16)> {
59 let (dx, dy) = self.offset?;
60 let max_w = area.width.saturating_sub(MIN_VISIBLE_W);
61 let max_h = area.height.saturating_sub(MIN_VISIBLE_H);
62 let nx = (col as i16 - dx as i16).clamp(0, max_w as i16) as u16;
63 let ny = (row as i16 - dy as i16).clamp(0, max_h as i16) as u16;
64 Some((nx, ny))
65 }
66
67 pub fn end(&mut self) {
69 self.offset = None;
70 }
71
72 pub fn is_dragging(&self) -> bool {
74 self.offset.is_some()
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
85 fn begin_sets_offset_when_in_drag_area() {
86 let popup = Rect::new(10, 5, 40, 30);
87 let mut drag = DragState::new();
88 drag.begin(popup, 15, 10, false, true);
89 assert!(drag.is_dragging());
90 }
91
92 #[test]
93 fn begin_skips_offset_when_not_in_drag_area() {
94 let popup = Rect::new(10, 5, 40, 30);
95 let mut drag = DragState::new();
96 drag.begin(popup, 15, 10, false, false);
97 assert!(!drag.is_dragging());
98 }
99
100 #[test]
101 fn begin_computes_correct_offset() {
102 let popup = Rect::new(10, 5, 40, 30);
103 let mut drag = DragState::new();
104 drag.begin(popup, 25, 15, false, true);
105 let result = drag.update(Rect::new(0, 0, 100, 50), 25, 15);
106 assert_eq!(result, Some((10, 5)));
107 }
108
109 #[test]
110 fn begin_border_drag_enables_header() {
111 let popup = Rect::new(10, 5, 40, 30);
112 let mut drag = DragState::new();
113 assert!(!drag.header_drag_enabled());
114 drag.begin(popup, 12, 6, true, true);
115 assert!(drag.header_drag_enabled());
116 }
117
118 #[test]
119 fn begin_header_drag_does_not_enable_header() {
120 let popup = Rect::new(10, 5, 40, 30);
121 let mut drag = DragState::new();
122 drag.begin(popup, 15, 7, false, false);
124 assert!(!drag.header_drag_enabled());
125 }
126
127 #[test]
128 fn begin_does_not_need_to_be_in_drag_area_to_enable_header() {
129 let popup = Rect::new(10, 5, 40, 30);
130 let mut drag = DragState::new();
131 drag.begin(popup, 12, 6, true, false);
134 assert!(!drag.header_drag_enabled());
135 }
136
137 #[test]
140 fn header_drag_enabled_false_by_default() {
141 let drag = DragState::new();
142 assert!(!drag.header_drag_enabled());
143 }
144
145 #[test]
146 fn header_drag_enabled_true_after_border_drag() {
147 let popup = Rect::new(10, 5, 40, 30);
148 let mut drag = DragState::new();
149 drag.begin(popup, 12, 6, true, true);
150 assert!(drag.header_drag_enabled());
151 }
152
153 #[test]
154 fn header_drag_enabled_stays_true_after_end() {
155 let popup = Rect::new(10, 5, 40, 30);
156 let mut drag = DragState::new();
157 drag.begin(popup, 12, 6, true, true);
158 drag.end();
159 assert!(drag.header_drag_enabled());
160 }
161
162 #[test]
163 fn header_drag_enabled_stays_true_after_header_drag() {
164 let popup = Rect::new(10, 5, 40, 30);
165 let mut drag = DragState::new();
166 drag.begin(popup, 12, 6, true, true);
168 drag.begin(popup, 15, 7, false, true);
170 assert!(drag.header_drag_enabled());
171 }
172
173 #[test]
176 fn update_returns_none_when_not_dragging() {
177 let drag = DragState::new();
178 assert!(drag.update(Rect::new(0, 0, 100, 50), 50, 25).is_none());
179 }
180
181 #[test]
182 fn update_returns_origin() {
183 let popup = Rect::new(10, 5, 40, 30);
184 let mut drag = DragState::new();
185 drag.begin(popup, 15, 10, false, true);
186 let result = drag.update(Rect::new(0, 0, 100, 50), 40, 30);
187 assert_eq!(result, Some((35, 25)));
188 }
189
190 #[test]
191 fn update_clamps_nx_to_area_width_minus_min_w() {
192 let popup = Rect::new(10, 5, 40, 30);
193 let mut drag = DragState::new();
194 drag.begin(popup, 15, 10, false, true);
195 let result = drag.update(Rect::new(0, 0, 100, 50), 999, 30);
196 let (nx, _) = result.unwrap();
197 assert!(nx <= 80);
198 }
199
200 #[test]
201 fn update_clamps_ny_to_area_height_minus_min_h() {
202 let popup = Rect::new(10, 5, 40, 30);
203 let mut drag = DragState::new();
204 drag.begin(popup, 15, 10, false, true);
205 let result = drag.update(Rect::new(0, 0, 100, 50), 30, 999);
206 let (_, ny) = result.unwrap();
207 assert!(ny <= 45);
208 }
209
210 #[test]
211 fn update_keeps_minimum_visible_size() {
212 let popup = Rect::new(10, 5, 40, 30);
213 let mut drag = DragState::new();
214 drag.begin(popup, 15, 10, false, true);
215 let result = drag.update(Rect::new(0, 0, 100, 50), 999, 999);
216 let (nx, ny) = result.unwrap();
217 assert_eq!(nx, 80);
218 assert_eq!(ny, 45);
219 }
220
221 #[test]
222 fn update_clamps_to_zero_when_negative() {
223 let popup = Rect::new(10, 5, 40, 30);
224 let mut drag = DragState::new();
225 drag.begin(popup, 15, 10, false, true);
226 let result = drag.update(Rect::new(0, 0, 100, 50), 0, 0);
227 assert_eq!(result, Some((0, 0)));
228 }
229
230 #[test]
231 fn update_allows_drag_below_middle() {
232 let popup = Rect::new(0, 0, 40, 35);
233 let mut drag = DragState::new();
234 drag.begin(popup, 5, 1, false, true);
235 let result = drag.update(Rect::new(0, 0, 100, 50), 5, 26);
236 let (_, ny) = result.unwrap();
237 assert!(ny >= 25, "ny should reach middle of screen, got ny={}", ny);
238 }
239
240 #[test]
243 fn end_clears_state() {
244 let popup = Rect::new(10, 5, 40, 30);
245 let mut drag = DragState::new();
246 drag.begin(popup, 15, 10, false, true);
247 assert!(drag.is_dragging());
248 drag.end();
249 assert!(!drag.is_dragging());
250 assert!(drag.update(Rect::new(0, 0, 100, 50), 40, 30).is_none());
251 }
252
253 #[test]
256 fn is_dragging_false_by_default() {
257 let drag = DragState::new();
258 assert!(!drag.is_dragging());
259 }
260
261 #[test]
262 fn is_dragging_true_after_begin() {
263 let popup = Rect::new(10, 5, 40, 30);
264 let mut drag = DragState::new();
265 drag.begin(popup, 15, 10, false, true);
266 assert!(drag.is_dragging());
267 }
268
269 #[test]
270 fn is_dragging_false_after_end() {
271 let popup = Rect::new(10, 5, 40, 30);
272 let mut drag = DragState::new();
273 drag.begin(popup, 15, 10, false, true);
274 drag.end();
275 assert!(!drag.is_dragging());
276 }
277}