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

jzombie / term-wm / 20801154955

08 Jan 2026 12:29AM UTC coverage: 37.626% (+1.7%) from 35.923%
20801154955

Pull #5

github

web-flow
Merge d644661ce into bbd85351a
Pull Request #5: Add splash screen, debug logging, and unify scroll support

753 of 1934 new or added lines in 19 files covered. (38.93%)

10 existing lines in 6 files now uncovered.

3167 of 8417 relevant lines covered (37.63%)

3.55 hits per line

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

68.63
/src/components/scroll_view.rs
1
use crossterm::event::{Event, MouseEvent, MouseEventKind};
2
use ratatui::prelude::Rect;
3
use ratatui::widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState};
4

5
use crate::components::Component;
6
use crate::ui::UiFrame;
7
use crate::window::ScrollState;
8

9
#[derive(Debug, Default, Clone)]
10
pub struct ScrollbarDrag {
11
    dragging: bool,
12
}
13

14
pub struct ScrollbarDragResponse {
15
    pub handled: bool,
16
    pub offset: Option<usize>,
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 {
32✔
27
        Self { dragging: false }
32✔
28
    }
32✔
29

30
    pub fn handle_mouse(
7✔
31
        &mut self,
7✔
32
        mouse: &MouseEvent,
7✔
33
        area: Rect,
7✔
34
        total: usize,
7✔
35
        view: usize,
7✔
36
        axis: ScrollbarAxis,
7✔
37
    ) -> ScrollbarDragResponse {
7✔
38
        let axis_empty = match axis {
7✔
39
            ScrollbarAxis::Vertical => area.height == 0,
4✔
40
            ScrollbarAxis::Horizontal => area.width == 0,
3✔
41
        };
42
        if total <= view || view == 0 || axis_empty {
7✔
43
            self.dragging = false;
×
44
            return ScrollbarDragResponse {
×
45
                handled: false,
×
46
                offset: None,
×
47
            };
×
48
        }
7✔
49
        let on_scrollbar = match axis {
7✔
50
            ScrollbarAxis::Vertical => {
51
                let scrollbar_x = area.x.saturating_add(area.width.saturating_sub(1));
4✔
52
                rect_contains(area, mouse.column, mouse.row) && mouse.column == scrollbar_x
4✔
53
            }
54
            ScrollbarAxis::Horizontal => {
55
                let scrollbar_y = area.y.saturating_add(area.height.saturating_sub(1));
3✔
56
                rect_contains(area, mouse.column, mouse.row) && mouse.row == scrollbar_y
3✔
57
            }
58
        };
59
        match mouse.kind {
2✔
60
            MouseEventKind::Down(_) if on_scrollbar => {
2✔
61
                self.dragging = true;
2✔
62
                ScrollbarDragResponse {
63
                    handled: true,
64
                    offset: Some(match axis {
2✔
65
                        ScrollbarAxis::Vertical => {
66
                            scrollbar_offset_from_row(mouse.row, area, total, view)
1✔
67
                        }
68
                        ScrollbarAxis::Horizontal => {
69
                            scrollbar_offset_from_col(mouse.column, area, total, view)
1✔
70
                        }
71
                    }),
72
                }
73
            }
74
            MouseEventKind::Drag(_) if self.dragging => ScrollbarDragResponse {
2✔
75
                handled: true,
76
                offset: Some(match axis {
2✔
77
                    ScrollbarAxis::Vertical => {
78
                        scrollbar_offset_from_row(mouse.row, area, total, view)
1✔
79
                    }
80
                    ScrollbarAxis::Horizontal => {
81
                        scrollbar_offset_from_col(mouse.column, area, total, view)
1✔
82
                    }
83
                }),
84
            },
85
            MouseEventKind::Up(_) if self.dragging => {
2✔
86
                self.dragging = false;
2✔
87
                ScrollbarDragResponse {
2✔
88
                    handled: true,
2✔
89
                    offset: None,
2✔
90
                }
2✔
91
            }
92
            _ => ScrollbarDragResponse {
1✔
93
                handled: false,
1✔
94
                offset: None,
1✔
95
            },
1✔
96
        }
97
    }
7✔
98
}
99

100
pub struct ScrollEvent {
101
    pub handled: bool,
102
    pub v_offset: Option<usize>,
103
    pub h_offset: Option<usize>,
104
}
105

106
#[derive(Debug)]
107
pub struct ScrollViewComponent {
108
    v_state: ScrollState,
109
    v_drag: ScrollbarDrag,
110
    h_state: ScrollState,
111
    h_drag: ScrollbarDrag,
112
    area: Rect,
113
    total: usize,
114
    view: usize,
115
    /// Horizontal total (content width in columns)
116
    h_total: usize,
117
    /// Horizontal viewport width (in columns)
118
    h_view: usize,
119
    fixed_height: Option<u16>,
120
    keyboard_enabled: bool,
121
}
122

123
impl Component for ScrollViewComponent {
NEW
124
    fn resize(&mut self, mut area: Rect) {
×
NEW
125
        if let Some(height) = self.fixed_height {
×
NEW
126
            area.height = area.height.min(height);
×
NEW
127
        }
×
NEW
128
        self.area = area;
×
NEW
129
        self.view = self.view.min(self.area.height as usize);
×
NEW
130
        self.v_state.apply(self.total, self.view);
×
NEW
131
    }
×
132

NEW
133
    fn render(&mut self, frame: &mut UiFrame<'_>, area: Rect, _focused: bool) {
×
NEW
134
        self.resize(area);
×
NEW
135
        ScrollViewComponent::render(self, frame);
×
NEW
136
    }
×
137

NEW
138
    fn handle_event(&mut self, event: &Event) -> bool {
×
NEW
139
        ScrollViewComponent::handle_event(self, event).handled
×
NEW
140
    }
×
141
}
142

143
impl ScrollViewComponent {
144
    pub fn new() -> Self {
15✔
145
        Self {
15✔
146
            v_state: ScrollState::default(),
15✔
147
            v_drag: ScrollbarDrag::new(),
15✔
148
            h_state: ScrollState::default(),
15✔
149
            h_drag: ScrollbarDrag::new(),
15✔
150
            area: Rect::default(),
15✔
151
            total: 0,
15✔
152
            view: 0,
15✔
153
            h_total: 0,
15✔
154
            h_view: 0,
15✔
155
            fixed_height: None,
15✔
156
            keyboard_enabled: false,
15✔
157
        }
15✔
158
    }
15✔
159

160
    /// Enable or disable default keyboard handling for this ScrollViewComponent.
161
    /// When disabled (default), callers must programmatically control scrolling.
162
    pub fn set_keyboard_enabled(&mut self, enabled: bool) {
3✔
163
        self.keyboard_enabled = enabled;
3✔
164
    }
3✔
165

166
    pub fn set_fixed_height(&mut self, height: Option<u16>) {
1✔
167
        self.fixed_height = height;
1✔
168
    }
1✔
169

170
    pub fn update(&mut self, area: Rect, total: usize, view: usize) {
5✔
171
        let mut area = area;
5✔
172
        if let Some(height) = self.fixed_height {
5✔
173
            area.height = area.height.min(height);
1✔
174
        }
4✔
175
        self.area = area;
5✔
176
        self.total = total;
5✔
177
        self.view = view.min(area.height as usize);
5✔
178
        self.v_state.apply(total, view);
5✔
179
    }
5✔
180

181
    pub fn set_total_view(&mut self, total: usize, view: usize) {
×
182
        self.total = total;
×
183
        self.view = view.min(self.area.height as usize);
×
NEW
184
        self.v_state.apply(total, view);
×
185
    }
×
186

187
    pub fn offset(&self) -> usize {
12✔
188
        self.v_state.offset
12✔
189
    }
12✔
190

191
    pub fn set_offset(&mut self, offset: usize) {
6✔
192
        self.v_state.offset = offset.min(self.max_offset());
6✔
193
    }
6✔
194

195
    pub fn bump(&mut self, delta: isize) {
×
NEW
196
        self.v_state.bump(delta);
×
NEW
197
        self.v_state.apply(self.total, self.view);
×
UNCOV
198
    }
×
199

200
    pub fn reset(&mut self) {
×
NEW
201
        self.v_state.reset();
×
NEW
202
        self.h_state.reset();
×
UNCOV
203
    }
×
204

205
    pub fn view(&self) -> usize {
×
206
        self.view
×
207
    }
×
208

NEW
209
    pub fn h_view(&self) -> usize {
×
NEW
210
        self.h_view
×
NEW
211
    }
×
212

213
    pub fn h_offset(&self) -> usize {
3✔
214
        self.h_state.offset
3✔
215
    }
3✔
216

NEW
217
    pub fn set_h_offset(&mut self, offset: usize) {
×
NEW
218
        self.h_state.offset = offset.min(self.max_h_offset());
×
NEW
219
    }
×
220

NEW
221
    pub fn bump_h(&mut self, delta: isize) {
×
NEW
222
        self.h_state.bump(delta);
×
NEW
223
        self.h_state.apply(self.h_total, self.h_view);
×
NEW
224
    }
×
225

226
    pub fn render(&self, frame: &mut UiFrame<'_>) {
3✔
227
        // Render vertical scrollbar on the right if needed
228
        render_scrollbar_oriented(
3✔
229
            frame,
3✔
230
            self.area,
3✔
231
            self.total,
3✔
232
            self.view,
3✔
233
            self.v_state.offset,
3✔
234
            ScrollbarOrientation::VerticalRight,
3✔
235
        );
236
        // Render horizontal scrollbar on the bottom if needed
237
        render_scrollbar_oriented(
3✔
238
            frame,
3✔
239
            self.area,
3✔
240
            self.h_total,
3✔
241
            self.h_view,
3✔
242
            self.h_state.offset,
3✔
243
            ScrollbarOrientation::HorizontalBottom,
3✔
244
        );
245
    }
3✔
246

247
    pub fn handle_event(&mut self, event: &Event) -> ScrollEvent {
1✔
248
        if self.total == 0 || self.view == 0 {
1✔
249
            return ScrollEvent {
×
250
                handled: false,
×
NEW
251
                v_offset: None,
×
NEW
252
                h_offset: None,
×
253
            };
×
254
        }
1✔
255
        let Event::Mouse(mouse) = event else {
1✔
256
            return ScrollEvent {
×
257
                handled: false,
×
NEW
258
                v_offset: None,
×
NEW
259
                h_offset: None,
×
UNCOV
260
            };
×
261
        };
262
        // First, let vertical scrollbar drag clicks/drags handle the event if applicable.
263
        let response = self.v_drag.handle_mouse(
1✔
264
            mouse,
1✔
265
            self.area,
1✔
266
            self.total,
1✔
267
            self.view,
1✔
268
            ScrollbarAxis::Vertical,
1✔
269
        );
270
        if let Some(offset) = response.offset {
1✔
271
            self.set_offset(offset);
×
272
        }
1✔
273
        if response.handled {
1✔
NEW
274
            return ScrollEvent {
×
NEW
275
                handled: true,
×
NEW
276
                v_offset: response.offset,
×
NEW
277
                h_offset: None,
×
NEW
278
            };
×
279
        }
1✔
280

281
        // Handle mouse wheel scrolling as a fallback.
282
        use crossterm::event::MouseEventKind::*;
283
        let mouse_scroll_resp = match mouse.kind {
1✔
284
            ScrollUp => {
NEW
285
                let off = self.offset().saturating_sub(3);
×
NEW
286
                self.set_offset(off);
×
NEW
287
                ScrollEvent {
×
NEW
288
                    handled: true,
×
NEW
289
                    v_offset: Some(off),
×
NEW
290
                    h_offset: None,
×
NEW
291
                }
×
292
            }
293
            ScrollDown => {
294
                let off = (self.offset().saturating_add(3)).min(self.max_offset());
1✔
295
                self.set_offset(off);
1✔
296
                ScrollEvent {
1✔
297
                    handled: true,
1✔
298
                    v_offset: Some(off),
1✔
299
                    h_offset: None,
1✔
300
                }
1✔
301
            }
NEW
302
            _ => ScrollEvent {
×
NEW
303
                handled: false,
×
NEW
304
                v_offset: None,
×
NEW
305
                h_offset: None,
×
NEW
306
            },
×
307
        };
308
        if mouse_scroll_resp.handled {
1✔
309
            return mouse_scroll_resp;
1✔
NEW
310
        }
×
NEW
311
        if self.h_total > self.h_view && self.h_view > 0 {
×
NEW
312
            let response = self.h_drag.handle_mouse(
×
NEW
313
                mouse,
×
NEW
314
                self.area,
×
NEW
315
                self.h_total,
×
NEW
316
                self.h_view,
×
NEW
317
                ScrollbarAxis::Horizontal,
×
318
            );
NEW
319
            if let Some(offset) = response.offset {
×
NEW
320
                self.set_h_offset(offset);
×
NEW
321
            }
×
NEW
322
            if response.handled {
×
NEW
323
                return ScrollEvent {
×
NEW
324
                    handled: true,
×
NEW
325
                    v_offset: None,
×
NEW
326
                    h_offset: response.offset,
×
NEW
327
                };
×
NEW
328
            }
×
NEW
329
        }
×
330
        ScrollEvent {
×
NEW
331
            handled: false,
×
NEW
332
            v_offset: None,
×
NEW
333
            h_offset: None,
×
NEW
334
        }
×
335
    }
1✔
336

NEW
337
    fn max_h_offset(&self) -> usize {
×
NEW
338
        self.h_total.saturating_sub(self.h_view)
×
NEW
339
    }
×
340

341
    // Handle common keyboard scrolling keys when `keyboard_enabled` is true.
342
    // Returns true if the key was handled and caused a scroll change.
343
    // By default keyboard handling is disabled to avoid hijacking character input;
344
    // enable selectively with `set_keyboard_enabled(true)` when appropriate.
345
    pub fn handle_key_event(&mut self, key: &crossterm::event::KeyEvent) -> bool {
1✔
346
        if !self.keyboard_enabled || self.total == 0 || self.view == 0 {
1✔
NEW
347
            return false;
×
348
        }
1✔
349
        let max_offset = self.total.saturating_sub(self.view);
1✔
350
        match key.code {
1✔
351
            crossterm::event::KeyCode::PageUp => {
352
                let page = self.view.max(1);
1✔
353
                let off = self.v_state.offset.saturating_sub(page);
1✔
354
                self.v_state.offset = off;
1✔
355
                self.v_state.apply(self.total, self.view);
1✔
356
                true
1✔
357
            }
358
            crossterm::event::KeyCode::PageDown => {
NEW
359
                let page = self.view.max(1);
×
NEW
360
                let off = (self.v_state.offset.saturating_add(page)).min(max_offset);
×
NEW
361
                self.v_state.offset = off;
×
NEW
362
                self.v_state.apply(self.total, self.view);
×
NEW
363
                true
×
364
            }
365
            crossterm::event::KeyCode::Home => {
NEW
366
                self.v_state.offset = 0;
×
NEW
367
                self.v_state.apply(self.total, self.view);
×
NEW
368
                true
×
369
            }
370
            crossterm::event::KeyCode::End => {
NEW
371
                self.v_state.offset = max_offset;
×
NEW
372
                self.v_state.apply(self.total, self.view);
×
NEW
373
                true
×
374
            }
375
            crossterm::event::KeyCode::Up => {
NEW
376
                let off = self.v_state.offset.saturating_sub(1);
×
NEW
377
                self.v_state.offset = off;
×
NEW
378
                self.v_state.apply(self.total, self.view);
×
NEW
379
                true
×
380
            }
381
            crossterm::event::KeyCode::Down => {
NEW
382
                let off = (self.v_state.offset.saturating_add(1)).min(max_offset);
×
NEW
383
                self.v_state.offset = off;
×
NEW
384
                self.v_state.apply(self.total, self.view);
×
NEW
385
                true
×
386
            }
NEW
387
            _ => false,
×
388
        }
389
    }
1✔
390

391
    fn max_offset(&self) -> usize {
7✔
392
        self.total.saturating_sub(self.view)
7✔
393
    }
7✔
394
}
395

396
impl Default for ScrollViewComponent {
397
    fn default() -> Self {
×
398
        Self::new()
×
399
    }
×
400
}
401

402
impl ScrollViewComponent {
403
    /// Set horizontal totals (content width in columns and viewport columns).
404
    pub fn set_horizontal_total_view(&mut self, total: usize, view: usize) {
3✔
405
        self.h_total = total;
3✔
406
        self.h_view = view.min(self.area.width as usize);
3✔
407
        self.h_state.apply(total, view);
3✔
408
    }
3✔
409
}
410

411
pub fn render_scrollbar(
×
412
    frame: &mut UiFrame<'_>,
×
413
    area: Rect,
×
414
    total: usize,
×
415
    view: usize,
×
416
    offset: usize,
×
417
) {
×
418
    if total <= view || view == 0 || area.height == 0 {
×
419
        return;
×
420
    }
×
421
    let content_len = total.saturating_sub(view).saturating_add(1).max(1);
×
422
    let mut state = ScrollbarState::new(content_len)
×
423
        .position(offset.min(content_len.saturating_sub(1)))
×
424
        .viewport_content_length(view.max(1));
×
425
    let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
×
426
    frame.render_stateful_widget(scrollbar, area, &mut state);
×
427
}
×
428

429
pub fn render_scrollbar_oriented(
6✔
430
    frame: &mut UiFrame<'_>,
6✔
431
    area: Rect,
6✔
432
    total: usize,
6✔
433
    view: usize,
6✔
434
    offset: usize,
6✔
435
    orientation: ScrollbarOrientation,
6✔
436
) {
6✔
437
    if total <= view || view == 0 || area.width == 0 || area.height == 0 {
6✔
438
        return;
3✔
439
    }
3✔
440
    let content_len = total.saturating_sub(view).saturating_add(1).max(1);
3✔
441
    let mut state = ScrollbarState::new(content_len)
3✔
442
        .position(offset.min(content_len.saturating_sub(1)))
3✔
443
        .viewport_content_length(view.max(1));
3✔
444
    let scrollbar = Scrollbar::new(orientation);
3✔
445
    frame.render_stateful_widget(scrollbar, area, &mut state);
3✔
446
}
6✔
447

448
fn scrollbar_offset_from_row(row: u16, area: Rect, total: usize, view: usize) -> usize {
4✔
449
    let content_len = total.saturating_sub(view).saturating_add(1).max(1);
4✔
450
    let max_offset = content_len.saturating_sub(1);
4✔
451
    if max_offset == 0 || area.height <= 1 {
4✔
452
        return 0;
×
453
    }
4✔
454
    let rel = row
4✔
455
        .saturating_sub(area.y)
4✔
456
        .min(area.height.saturating_sub(1));
4✔
457
    let ratio = rel as f64 / (area.height.saturating_sub(1)) as f64;
4✔
458
    (ratio * max_offset as f64).round() as usize
4✔
459
}
4✔
460

461
fn scrollbar_offset_from_col(col: u16, area: Rect, total: usize, view: usize) -> usize {
2✔
462
    // Map a column within area to a horizontal offset
463
    let content_len = total.saturating_sub(view).saturating_add(1).max(1);
2✔
464
    let max_offset = content_len.saturating_sub(1);
2✔
465
    if max_offset == 0 || area.width <= 1 {
2✔
NEW
466
        return 0;
×
467
    }
2✔
468
    let rel = col.saturating_sub(area.x).min(area.width.saturating_sub(1));
2✔
469
    let ratio = rel as f64 / (area.width.saturating_sub(1)) as f64;
2✔
470
    (ratio * max_offset as f64).round() as usize
2✔
471
}
2✔
472

473
fn rect_contains(rect: Rect, column: u16, row: u16) -> bool {
10✔
474
    if rect.width == 0 || rect.height == 0 {
10✔
475
        return false;
1✔
476
    }
9✔
477
    let max_x = rect.x.saturating_add(rect.width);
9✔
478
    let max_y = rect.y.saturating_add(rect.height);
9✔
479
    column >= rect.x && column < max_x && row >= rect.y && row < max_y
9✔
480
}
10✔
481

482
#[cfg(test)]
483
mod tests {
484
    use super::*;
485
    use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
486
    use ratatui::prelude::Rect;
487

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

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

531
        let drag_evt = MouseEvent {
1✔
532
            kind: MouseEventKind::Drag(MouseButton::Left),
1✔
533
            column: scrollbar_x,
1✔
534
            row: area.y + 2,
1✔
535
            modifiers: KeyModifiers::NONE,
1✔
536
        };
1✔
537
        let resp2 = drag.handle_mouse(&drag_evt, area, total, view, ScrollbarAxis::Vertical);
1✔
538
        assert!(resp2.handled);
1✔
539
        assert!(resp2.offset.is_some());
1✔
540

541
        let up = MouseEvent {
1✔
542
            kind: MouseEventKind::Up(MouseButton::Left),
1✔
543
            column: scrollbar_x,
1✔
544
            row: area.y + 2,
1✔
545
            modifiers: KeyModifiers::NONE,
1✔
546
        };
1✔
547
        let resp3 = drag.handle_mouse(&up, area, total, view, ScrollbarAxis::Vertical);
1✔
548
        assert!(resp3.handled);
1✔
549
        assert!(resp3.offset.is_none());
1✔
550
    }
1✔
551

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

575
        let drag_evt = MouseEvent {
1✔
576
            kind: MouseEventKind::Drag(MouseButton::Left),
1✔
577
            column: area.x + 4,
1✔
578
            row: scrollbar_y,
1✔
579
            modifiers: KeyModifiers::NONE,
1✔
580
        };
1✔
581
        let resp2 = drag.handle_mouse(&drag_evt, area, total, view, ScrollbarAxis::Horizontal);
1✔
582
        assert!(resp2.handled);
1✔
583
        assert!(resp2.offset.is_some());
1✔
584

585
        let up = MouseEvent {
1✔
586
            kind: MouseEventKind::Up(MouseButton::Left),
1✔
587
            column: area.x + 4,
1✔
588
            row: scrollbar_y,
1✔
589
            modifiers: KeyModifiers::NONE,
1✔
590
        };
1✔
591
        let resp3 = drag.handle_mouse(&up, area, total, view, ScrollbarAxis::Horizontal);
1✔
592
        assert!(resp3.handled);
1✔
593
        assert!(resp3.offset.is_none());
1✔
594
    }
1✔
595

596
    #[test]
597
    fn scroll_view_set_offset_and_max() {
1✔
598
        let mut sv = ScrollViewComponent::new();
1✔
599
        let area = Rect {
1✔
600
            x: 0,
1✔
601
            y: 0,
1✔
602
            width: 3,
1✔
603
            height: 4,
1✔
604
        };
1✔
605
        sv.update(area, 50, 3);
1✔
606
        sv.set_offset(1000);
1✔
607
        assert!(sv.offset() <= sv.total.saturating_sub(sv.view));
1✔
608
        sv.set_offset(0);
1✔
609
        assert_eq!(sv.offset(), 0);
1✔
610
    }
1✔
611

612
    #[test]
613
    fn rect_contains_edge_cases() {
1✔
614
        let r = Rect {
1✔
615
            x: 0,
1✔
616
            y: 0,
1✔
617
            width: 0,
1✔
618
            height: 3,
1✔
619
        };
1✔
620
        assert!(!rect_contains(r, 0, 0));
1✔
621
        let r2 = Rect {
1✔
622
            x: 1,
1✔
623
            y: 1,
1✔
624
            width: 2,
1✔
625
            height: 2,
1✔
626
        };
1✔
627
        assert!(rect_contains(r2, 1, 1));
1✔
628
        assert!(!rect_contains(r2, 3, 1));
1✔
629
    }
1✔
630
}
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