• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jzombie / term-wm / 20885819972

10 Jan 2026 11:10PM UTC coverage: 57.056% (+10.0%) from 47.071%
20885819972

Pull #20

github

web-flow
Merge 8345857e8 into bfecf0f75
Pull Request #20: Initial clipboard and offscreen buffer support

2046 of 3183 new or added lines in 26 files covered. (64.28%)

78 existing lines in 15 files now uncovered.

6788 of 11897 relevant lines covered (57.06%)

9.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.53
/src/components/scroll_view.rs
1
use std::cell::RefCell;
2
use std::rc::Rc;
3

4
use crossterm::event::{Event, MouseEvent, MouseEventKind};
5
use ratatui::prelude::Rect;
6
use ratatui::widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState};
7

8
use crate::component_context::{ViewportHandle, ViewportSharedState};
9
use crate::components::{Component, ComponentContext};
10
use crate::ui::UiFrame;
11

12
// --- Scroll Logic Helpers (Public API) ---
13

14
#[derive(Debug, Default, Clone)]
15
pub struct ScrollbarDrag {
16
    pub dragging: bool,
17
}
18

19
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20
pub enum ScrollbarAxis {
21
    Vertical,
22
    Horizontal,
23
}
24

25
impl ScrollbarDrag {
26
    pub fn new() -> Self {
56✔
27
        Self { dragging: false }
56✔
28
    }
56✔
29

30
    /// Returns Some(new_offset) if a drag event occurred.
31
    pub fn handle_mouse(
11✔
32
        &mut self,
11✔
33
        mouse: &MouseEvent,
11✔
34
        area: Rect,
11✔
35
        total: usize,
11✔
36
        view: usize,
11✔
37
        axis: ScrollbarAxis,
11✔
38
    ) -> Option<usize> {
11✔
39
        let axis_empty = match axis {
11✔
40
            ScrollbarAxis::Vertical => area.height == 0,
6✔
41
            ScrollbarAxis::Horizontal => area.width == 0,
5✔
42
        };
43
        if total <= view || view == 0 || axis_empty {
11✔
44
            self.dragging = false;
×
NEW
45
            return None;
×
46
        }
11✔
47

48
        let on_scrollbar = match axis {
11✔
49
            ScrollbarAxis::Vertical => {
50
                let scrollbar_x = area.x.saturating_add(area.width.saturating_sub(1));
6✔
51
                rect_contains(area, mouse.column, mouse.row) && mouse.column == scrollbar_x
6✔
52
            }
53
            ScrollbarAxis::Horizontal => {
54
                let scrollbar_y = area.y.saturating_add(area.height.saturating_sub(1));
5✔
55
                rect_contains(area, mouse.column, mouse.row) && mouse.row == scrollbar_y
5✔
56
            }
57
        };
58

59
        match mouse.kind {
5✔
60
            MouseEventKind::Down(_) if on_scrollbar => {
3✔
61
                self.dragging = true;
3✔
62
                Some(match axis {
3✔
63
                    ScrollbarAxis::Vertical => {
64
                        scrollbar_offset_from_row(mouse.row, area, total, view)
2✔
65
                    }
66
                    ScrollbarAxis::Horizontal => {
67
                        scrollbar_offset_from_col(mouse.column, area, total, view)
1✔
68
                    }
69
                })
70
            }
71
            MouseEventKind::Drag(_) if self.dragging => Some(match axis {
2✔
72
                ScrollbarAxis::Vertical => scrollbar_offset_from_row(mouse.row, area, total, view),
1✔
73
                ScrollbarAxis::Horizontal => {
74
                    scrollbar_offset_from_col(mouse.column, area, total, view)
1✔
75
                }
76
            }),
77
            MouseEventKind::Up(_) if self.dragging => {
2✔
78
                self.dragging = false;
2✔
79
                None
2✔
80
            }
81
            _ => None,
4✔
82
        }
83
    }
11✔
84
}
85

86
// --- Rendering Helpers ---
87

88
pub fn render_scrollbar(
×
89
    frame: &mut UiFrame<'_>,
×
90
    area: Rect,
×
91
    total: usize,
×
92
    view: usize,
×
93
    offset: usize,
×
94
) {
×
95
    if total <= view || view == 0 || area.height == 0 {
×
96
        return;
×
97
    }
×
98
    let content_len = total.saturating_sub(view).saturating_add(1).max(1);
×
99
    let mut state = ScrollbarState::new(content_len)
×
100
        .position(offset.min(content_len.saturating_sub(1)))
×
101
        .viewport_content_length(view.max(1));
×
102
    let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
×
103
    frame.render_stateful_widget(scrollbar, area, &mut state);
×
104
}
×
105

106
pub fn render_scrollbar_oriented(
8✔
107
    frame: &mut UiFrame<'_>,
8✔
108
    area: Rect,
8✔
109
    total: usize,
8✔
110
    view: usize,
8✔
111
    offset: usize,
8✔
112
    orientation: ScrollbarOrientation,
8✔
113
) {
8✔
114
    if total <= view || view == 0 || area.width == 0 || area.height == 0 {
8✔
UNCOV
115
        return;
×
116
    }
8✔
117
    let content_len = total.saturating_sub(view).saturating_add(1).max(1);
8✔
118
    let mut state = ScrollbarState::new(content_len)
8✔
119
        .position(offset.min(content_len.saturating_sub(1)))
8✔
120
        .viewport_content_length(view.max(1));
8✔
121
    let scrollbar = Scrollbar::new(orientation);
8✔
122
    frame.render_stateful_widget(scrollbar, area, &mut state);
8✔
123
}
8✔
124

125
// --- Internal Math ---
126

127
fn scrollbar_offset_from_row(row: u16, area: Rect, total: usize, view: usize) -> usize {
5✔
128
    let content_len = total.saturating_sub(view).saturating_add(1).max(1);
5✔
129
    let max_offset = content_len.saturating_sub(1);
5✔
130
    if max_offset == 0 || area.height <= 1 {
5✔
131
        return 0;
×
132
    }
5✔
133
    let rel = row
5✔
134
        .saturating_sub(area.y)
5✔
135
        .min(area.height.saturating_sub(1));
5✔
136
    let ratio = rel as f64 / (area.height.saturating_sub(1)) as f64;
5✔
137
    (ratio * max_offset as f64).round() as usize
5✔
138
}
5✔
139

140
fn scrollbar_offset_from_col(col: u16, area: Rect, total: usize, view: usize) -> usize {
2✔
141
    let content_len = total.saturating_sub(view).saturating_add(1).max(1);
2✔
142
    let max_offset = content_len.saturating_sub(1);
2✔
143
    if max_offset == 0 || area.width <= 1 {
2✔
144
        return 0;
×
145
    }
2✔
146
    let rel = col.saturating_sub(area.x).min(area.width.saturating_sub(1));
2✔
147
    let ratio = rel as f64 / (area.width.saturating_sub(1)) as f64;
2✔
148
    (ratio * max_offset as f64).round() as usize
2✔
149
}
2✔
150

151
fn rect_contains(rect: Rect, column: u16, row: u16) -> bool {
14✔
152
    if rect.width == 0 || rect.height == 0 {
14✔
153
        return false;
1✔
154
    }
13✔
155
    let max_x = rect.x.saturating_add(rect.width);
13✔
156
    let max_y = rect.y.saturating_add(rect.height);
13✔
157
    column >= rect.x && column < max_x && row >= rect.y && row < max_y
13✔
158
}
14✔
159

160
// --- ScrollView Component Wrapper ---
161

162
#[derive(Debug)]
163
pub struct ScrollViewComponent<C> {
164
    pub content: C,
165
    shared_state: Rc<RefCell<ViewportSharedState>>,
166
    v_drag: ScrollbarDrag,
167
    h_drag: ScrollbarDrag,
168
    pub viewport_area: Rect,
169
    keyboard_enabled: bool,
170
}
171

172
impl<C: Component> ScrollViewComponent<C> {
173
    pub fn new(content: C) -> Self {
27✔
174
        Self {
27✔
175
            content,
27✔
176
            shared_state: Rc::new(RefCell::new(ViewportSharedState::default())),
27✔
177
            v_drag: ScrollbarDrag::new(),
27✔
178
            h_drag: ScrollbarDrag::new(),
27✔
179
            viewport_area: Rect::default(),
27✔
180
            keyboard_enabled: true,
27✔
181
        }
27✔
182
    }
27✔
183

184
    pub fn set_keyboard_enabled(&mut self, enabled: bool) {
6✔
185
        self.keyboard_enabled = enabled;
6✔
186
    }
6✔
187

188
    pub fn viewport_handle(&self) -> ViewportHandle {
38✔
189
        ViewportHandle {
38✔
190
            shared: self.shared_state.clone(),
38✔
191
        }
38✔
192
    }
38✔
193

194
    fn compute_layout(&mut self, area: Rect) -> Rect {
21✔
195
        // Simple reservation strategy:
196
        // Use previous frame's content size to decide on scrollbars.
197
        let state = self.shared_state.borrow();
21✔
198
        let content_w = state.content_width;
21✔
199
        let content_h = state.content_height;
21✔
200
        drop(state); // Drop borrow
21✔
201

202
        // If we don't know content size yet, give full area.
203
        if content_w == 0 && content_h == 0 {
21✔
204
            return area;
8✔
205
        }
13✔
206

207
        let mut view_w = area.width;
13✔
208
        let mut view_h = area.height;
13✔
209

210
        let needs_v = content_h > view_h as usize;
13✔
211
        if needs_v && view_w > 0 {
13✔
212
            view_w = view_w.saturating_sub(1);
12✔
213
        }
12✔
214

215
        let needs_h = content_w > view_w as usize;
13✔
216
        if needs_h && view_h > 0 {
13✔
217
            view_h = view_h.saturating_sub(1);
6✔
218
        }
7✔
219

220
        // Re-check V if H reduced height?
221
        // (If reducing height makes V needed unexpectedly? Unlikely if content_h check used original height.
222
        // But if content_h matched exactly, reducing height might trigger wrap... but we use content size from CHILD which is usually absolute).
223

224
        Rect {
13✔
225
            x: area.x,
13✔
226
            y: area.y,
13✔
227
            width: view_w,
13✔
228
            height: view_h,
13✔
229
        }
13✔
230
    }
21✔
231
}
232

233
impl<C: Component> Component for ScrollViewComponent<C> {
NEW
234
    fn resize(&mut self, area: Rect, ctx: &ComponentContext) {
×
235
        // Predict layout and resize child
NEW
236
        let inner = self.compute_layout(area);
×
NEW
237
        self.viewport_area = inner;
×
NEW
238
        self.content.resize(inner, ctx);
×
NEW
239
    }
×
240

241
    fn render(&mut self, frame: &mut UiFrame<'_>, area: Rect, ctx: &ComponentContext) {
9✔
242
        if area.width == 0 || area.height == 0 {
9✔
NEW
243
            self.viewport_area = Rect::default();
×
NEW
244
            return;
×
245
        }
9✔
246

247
        let max_attempts = 3;
9✔
248
        let mut attempt = 0;
9✔
249

250
        loop {
251
            // 1. Compute layout (potentially reserving space for scrollbars)
252
            let inner_area = self.compute_layout(area);
21✔
253
            self.viewport_area = inner_area;
21✔
254

255
            // 2. Update Shared State for this frame's Viewport properties
256
            {
257
                let mut state = self.shared_state.borrow_mut();
21✔
258
                state.width = inner_area.width as usize;
21✔
259
                state.height = inner_area.height as usize;
21✔
260

261
                let max_x = state.content_width.saturating_sub(state.width);
21✔
262
                let max_y = state.content_height.saturating_sub(state.height);
21✔
263
                if let Some(off) = state.pending_offset_x.take() {
21✔
NEW
264
                    state.offset_x = off.min(max_x);
×
265
                } else {
21✔
266
                    state.offset_x = state.offset_x.min(max_x);
21✔
267
                }
21✔
268
                if let Some(off) = state.pending_offset_y.take() {
21✔
269
                    state.offset_y = off.min(max_y);
2✔
270
                } else {
19✔
271
                    state.offset_y = state.offset_y.min(max_y);
19✔
272
                }
19✔
273
            }
274

275
            // 3. Create context with ViewportHandle
276
            let handle = self.viewport_handle();
21✔
277
            let info = handle.info();
21✔
278
            let child_ctx = ctx.with_viewport(info, Some(handle));
21✔
279

280
            // 4. Render Child
281
            self.content.render(frame, inner_area, &child_ctx);
21✔
282

283
            // Retrieve updated state (child might have updated content_size during render)
284
            let state = self.shared_state.borrow();
21✔
285
            let content_w = state.content_width;
21✔
286
            let content_h = state.content_height;
21✔
287
            let off_x = state.offset_x;
21✔
288
            let off_y = state.offset_y;
21✔
289
            drop(state);
21✔
290

291
            let needs_vertical = inner_area.height > 0 && content_h > inner_area.height as usize;
21✔
292
            let has_vertical_reserved = inner_area.width < area.width;
21✔
293
            let needs_horizontal = inner_area.width > 0 && content_w > inner_area.width as usize;
21✔
294
            let has_horizontal_reserved = inner_area.height < area.height;
21✔
295

296
            // store final values in loop-local variables (use them directly below)
297

298
            let drop_vertical = has_vertical_reserved && !needs_vertical && area.width > 0;
21✔
299
            let drop_horizontal = has_horizontal_reserved && !needs_horizontal && area.height > 0;
21✔
300
            let retry_vertical =
21✔
301
                (needs_vertical && !has_vertical_reserved && area.width > 0) || drop_vertical;
21✔
302
            let retry_horizontal =
21✔
303
                (needs_horizontal && !has_horizontal_reserved && area.height > 0)
21✔
304
                    || drop_horizontal;
20✔
305

306
            if (retry_vertical || retry_horizontal) && attempt + 1 < max_attempts {
21✔
307
                attempt += 1;
12✔
308
                continue;
12✔
309
            }
9✔
310

311
            // 5. Render Scrollbars with finalized layout
312
            if needs_vertical {
9✔
313
                let sb_area = Rect {
7✔
314
                    x: area.x + area.width.saturating_sub(1),
7✔
315
                    y: area.y,
7✔
316
                    width: 1,
7✔
317
                    height: inner_area.height,
7✔
318
                };
7✔
319
                render_scrollbar_oriented(
7✔
320
                    frame,
7✔
321
                    sb_area,
7✔
322
                    content_h,
7✔
323
                    inner_area.height as usize,
7✔
324
                    off_y,
7✔
325
                    ScrollbarOrientation::VerticalRight,
7✔
326
                );
7✔
327
            }
7✔
328

329
            if needs_horizontal {
9✔
330
                let sb_area = Rect {
1✔
331
                    x: area.x,
1✔
332
                    y: area.y + area.height.saturating_sub(1),
1✔
333
                    width: inner_area.width,
1✔
334
                    height: 1,
1✔
335
                };
1✔
336
                render_scrollbar_oriented(
1✔
337
                    frame,
1✔
338
                    sb_area,
1✔
339
                    content_w,
1✔
340
                    inner_area.width as usize,
1✔
341
                    off_x,
1✔
342
                    ScrollbarOrientation::HorizontalBottom,
1✔
343
                );
1✔
344
            }
8✔
345

346
            break;
9✔
347
        }
348
    }
9✔
349

350
    fn handle_event(&mut self, event: &Event, ctx: &ComponentContext) -> bool {
6✔
351
        // Handle scrollbar drags interactions first
352
        if let Event::Mouse(mouse) = event {
6✔
353
            let state = self.shared_state.borrow();
5✔
354
            let content_h = state.content_height;
5✔
355
            let view_h = state.height;
5✔
356
            let content_w = state.content_width;
5✔
357
            let view_w = state.width;
5✔
358
            drop(state);
5✔
359

360
            // Vertical Scrollbar
361
            if content_h > view_h {
5✔
362
                // Assumes vertical scrollbar is immediately to the right of viewport
363
                let sb_area = Rect {
3✔
364
                    x: self
3✔
365
                        .viewport_area
3✔
366
                        .x
3✔
367
                        .saturating_add(self.viewport_area.width),
3✔
368
                    y: self.viewport_area.y,
3✔
369
                    width: 1,
3✔
370
                    height: self.viewport_area.height,
3✔
371
                };
3✔
372
                if let Some(new_off) = self.v_drag.handle_mouse(
3✔
373
                    mouse,
3✔
374
                    sb_area,
3✔
375
                    content_h,
3✔
376
                    view_h,
3✔
377
                    ScrollbarAxis::Vertical,
3✔
378
                ) {
3✔
379
                    let mut st = self.shared_state.borrow_mut();
1✔
380
                    st.offset_y = new_off;
1✔
381
                    st.pending_offset_y = Some(new_off);
1✔
382
                    return true;
1✔
383
                }
2✔
384
            }
2✔
385

386
            // Horizontal Scrollbar
387
            if content_w > view_w {
4✔
388
                let sb_area = Rect {
2✔
389
                    x: self.viewport_area.x,
2✔
390
                    y: self
2✔
391
                        .viewport_area
2✔
392
                        .y
2✔
393
                        .saturating_add(self.viewport_area.height),
2✔
394
                    width: self.viewport_area.width,
2✔
395
                    height: 1,
2✔
396
                };
2✔
397
                if let Some(new_off) = self.h_drag.handle_mouse(
2✔
398
                    mouse,
2✔
399
                    sb_area,
2✔
400
                    content_w,
2✔
401
                    view_w,
2✔
402
                    ScrollbarAxis::Horizontal,
2✔
403
                ) {
2✔
NEW
404
                    let mut st = self.shared_state.borrow_mut();
×
NEW
405
                    st.offset_x = new_off;
×
NEW
406
                    st.pending_offset_x = Some(new_off);
×
NEW
407
                    return true;
×
408
                }
2✔
409
            }
2✔
410
        }
1✔
411

412
        // Mouse Wheel (Common)
413
        if let Event::Mouse(mouse) = event {
5✔
414
            match mouse.kind {
4✔
415
                MouseEventKind::ScrollUp => {
NEW
416
                    let mut st = self.shared_state.borrow_mut();
×
NEW
417
                    st.offset_y = st.offset_y.saturating_sub(3);
×
NEW
418
                    st.pending_offset_y = Some(st.offset_y);
×
NEW
419
                    return true;
×
420
                }
421
                MouseEventKind::ScrollDown => {
422
                    let mut st = self.shared_state.borrow_mut();
1✔
423
                    let max = st.content_height.saturating_sub(st.height);
1✔
424
                    st.offset_y = (st.offset_y + 3).min(max);
1✔
425
                    st.pending_offset_y = Some(st.offset_y);
1✔
426
                    return true;
1✔
427
                }
428
                _ => {}
3✔
429
            }
430
        }
1✔
431

432
        // Pass to child
433
        // Construct child context
434
        let handle = self.viewport_handle();
4✔
435
        let info = handle.info();
4✔
436
        let child_ctx = ctx.with_viewport(info, Some(handle));
4✔
437

438
        if self.content.handle_event(event, &child_ctx) {
4✔
439
            return true;
3✔
440
        }
1✔
441

442
        if self.keyboard_enabled
1✔
443
            && ctx.focused()
1✔
444
            && let Event::Key(key) = event
1✔
445
            && key.kind == crossterm::event::KeyEventKind::Press
1✔
446
        {
447
            use crossterm::event::KeyCode;
448
            let mut st = self.shared_state.borrow_mut();
1✔
449
            let max_y = st.content_height.saturating_sub(st.height);
1✔
450
            match key.code {
1✔
451
                KeyCode::Up => {
NEW
452
                    st.offset_y = st.offset_y.saturating_sub(1);
×
NEW
453
                    st.pending_offset_y = Some(st.offset_y);
×
NEW
454
                    return true;
×
455
                }
456
                KeyCode::Down => {
NEW
457
                    st.offset_y = (st.offset_y + 1).min(max_y);
×
NEW
458
                    st.pending_offset_y = Some(st.offset_y);
×
NEW
459
                    return true;
×
460
                }
461
                KeyCode::PageUp => {
462
                    st.offset_y = st.offset_y.saturating_sub(st.height);
1✔
463
                    st.pending_offset_y = Some(st.offset_y);
1✔
464
                    return true;
1✔
465
                }
466
                KeyCode::PageDown => {
NEW
467
                    st.offset_y = (st.offset_y + st.height).min(max_y);
×
NEW
468
                    st.pending_offset_y = Some(st.offset_y);
×
NEW
469
                    return true;
×
470
                }
471
                KeyCode::Home => {
NEW
472
                    st.offset_y = 0;
×
NEW
473
                    st.pending_offset_y = Some(st.offset_y);
×
NEW
474
                    return true;
×
475
                }
476
                KeyCode::End => {
NEW
477
                    st.offset_y = max_y;
×
NEW
478
                    st.pending_offset_y = Some(st.offset_y);
×
NEW
479
                    return true;
×
480
                }
NEW
481
                _ => {}
×
482
            }
NEW
483
        }
×
484

NEW
485
        false
×
486
    }
6✔
487
}
488

489
#[cfg(test)]
490
mod tests {
491
    use super::*;
492
    use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
493
    use ratatui::prelude::Rect;
494

495
    #[test]
496
    fn scrollbar_offset_from_row_edges() {
1✔
497
        let area = Rect {
1✔
498
            x: 0,
1✔
499
            y: 0,
1✔
500
            width: 5,
1✔
501
            height: 10,
1✔
502
        };
1✔
503
        let total = 100usize;
1✔
504
        let view = 10usize;
1✔
505
        let top = scrollbar_offset_from_row(0, area, total, view);
1✔
506
        let bottom = scrollbar_offset_from_row(area.y + area.height - 1, area, total, view);
1✔
507
        assert_eq!(top, 0);
1✔
508
        let max_offset = total
1✔
509
            .saturating_sub(view)
1✔
510
            .saturating_add(1)
1✔
511
            .saturating_sub(1);
1✔
512
        assert!(bottom <= max_offset);
1✔
513
    }
1✔
514

515
    #[test]
516
    fn drag_handle_mouse_lifecycle() {
1✔
517
        let mut drag = ScrollbarDrag::new();
1✔
518
        let area = Rect {
1✔
519
            x: 0,
1✔
520
            y: 0,
1✔
521
            width: 4,
1✔
522
            height: 6,
1✔
523
        };
1✔
524
        let total = 20usize;
1✔
525
        let view = 5usize;
1✔
526
        let scrollbar_x = area.x.saturating_add(area.width.saturating_sub(1));
1✔
527
        use crossterm::event::KeyModifiers;
528
        let down = MouseEvent {
1✔
529
            kind: MouseEventKind::Down(MouseButton::Left),
1✔
530
            column: scrollbar_x,
1✔
531
            row: area.y + 1,
1✔
532
            modifiers: KeyModifiers::NONE,
1✔
533
        };
1✔
534
        let resp = drag.handle_mouse(&down, area, total, view, ScrollbarAxis::Vertical);
1✔
535
        assert!(resp.is_some());
1✔
536
        assert!(drag.dragging);
1✔
537

538
        let drag_evt = MouseEvent {
1✔
539
            kind: MouseEventKind::Drag(MouseButton::Left),
1✔
540
            column: scrollbar_x,
1✔
541
            row: area.y + 2,
1✔
542
            modifiers: KeyModifiers::NONE,
1✔
543
        };
1✔
544
        let resp2 = drag.handle_mouse(&drag_evt, area, total, view, ScrollbarAxis::Vertical);
1✔
545
        assert!(resp2.is_some());
1✔
546
        assert!(drag.dragging);
1✔
547

548
        let up = MouseEvent {
1✔
549
            kind: MouseEventKind::Up(MouseButton::Left),
1✔
550
            column: scrollbar_x,
1✔
551
            row: area.y + 2,
1✔
552
            modifiers: KeyModifiers::NONE,
1✔
553
        };
1✔
554
        let resp3 = drag.handle_mouse(&up, area, total, view, ScrollbarAxis::Vertical);
1✔
555
        assert!(resp3.is_none());
1✔
556
        assert!(!drag.dragging);
1✔
557
    }
1✔
558

559
    #[test]
560
    fn horizontal_drag_handle_mouse_lifecycle() {
1✔
561
        let mut drag = ScrollbarDrag::new();
1✔
562
        let area = Rect {
1✔
563
            x: 0,
1✔
564
            y: 0,
1✔
565
            width: 8,
1✔
566
            height: 4,
1✔
567
        };
1✔
568
        let total = 40usize;
1✔
569
        let view = 6usize;
1✔
570
        let scrollbar_y = area.y.saturating_add(area.height.saturating_sub(1));
1✔
571
        use crossterm::event::KeyModifiers;
572
        let down = MouseEvent {
1✔
573
            kind: MouseEventKind::Down(MouseButton::Left),
1✔
574
            column: area.x + 2,
1✔
575
            row: scrollbar_y,
1✔
576
            modifiers: KeyModifiers::NONE,
1✔
577
        };
1✔
578
        let resp = drag.handle_mouse(&down, area, total, view, ScrollbarAxis::Horizontal);
1✔
579
        assert!(resp.is_some());
1✔
580
        assert!(drag.dragging);
1✔
581

582
        let drag_evt = MouseEvent {
1✔
583
            kind: MouseEventKind::Drag(MouseButton::Left),
1✔
584
            column: area.x + 4,
1✔
585
            row: scrollbar_y,
1✔
586
            modifiers: KeyModifiers::NONE,
1✔
587
        };
1✔
588
        let resp2 = drag.handle_mouse(&drag_evt, area, total, view, ScrollbarAxis::Horizontal);
1✔
589
        assert!(resp2.is_some());
1✔
590
        assert!(drag.dragging);
1✔
591

592
        let up = MouseEvent {
1✔
593
            kind: MouseEventKind::Up(MouseButton::Left),
1✔
594
            column: area.x + 4,
1✔
595
            row: scrollbar_y,
1✔
596
            modifiers: KeyModifiers::NONE,
1✔
597
        };
1✔
598
        let resp3 = drag.handle_mouse(&up, area, total, view, ScrollbarAxis::Horizontal);
1✔
599
        assert!(resp3.is_none());
1✔
600
        assert!(!drag.dragging);
1✔
601
    }
1✔
602

603
    #[test]
604
    fn rect_contains_edge_cases() {
1✔
605
        let r = Rect {
1✔
606
            x: 0,
1✔
607
            y: 0,
1✔
608
            width: 0,
1✔
609
            height: 3,
1✔
610
        };
1✔
611
        assert!(!rect_contains(r, 0, 0));
1✔
612
        let r2 = Rect {
1✔
613
            x: 1,
1✔
614
            y: 1,
1✔
615
            width: 2,
1✔
616
            height: 2,
1✔
617
        };
1✔
618
        assert!(rect_contains(r2, 1, 1));
1✔
619
        assert!(!rect_contains(r2, 3, 1));
1✔
620
    }
1✔
621
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc