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

jzombie / term-wm / 20828291393

08 Jan 2026 06:58PM UTC coverage: 39.989% (+0.9%) from 39.086%
20828291393

Pull #9

github

web-flow
Merge bd526f376 into dccaa1888
Pull Request #9: Centralize key bindings

209 of 364 new or added lines in 10 files covered. (57.42%)

13 existing lines in 6 files now uncovered.

3601 of 9005 relevant lines covered (39.99%)

4.45 hits per line

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

67.96
/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 {
124
    fn resize(&mut self, mut area: Rect) {
×
125
        if let Some(height) = self.fixed_height {
×
126
            area.height = area.height.min(height);
×
127
        }
×
128
        self.area = area;
×
129
        self.view = self.view.min(self.area.height as usize);
×
130
        self.v_state.apply(self.total, self.view);
×
131
    }
×
132

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

138
    fn handle_event(&mut self, event: &Event) -> bool {
×
139
        ScrollViewComponent::handle_event(self, event).handled
×
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);
×
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) {
×
196
        self.v_state.bump(delta);
×
197
        self.v_state.apply(self.total, self.view);
×
198
    }
×
199

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

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

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

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

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

221
    pub fn bump_h(&mut self, delta: isize) {
×
222
        self.h_state.bump(delta);
×
223
        self.h_state.apply(self.h_total, self.h_view);
×
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,
×
251
                v_offset: None,
×
252
                h_offset: None,
×
253
            };
×
254
        }
1✔
255
        let Event::Mouse(mouse) = event else {
1✔
256
            return ScrollEvent {
×
257
                handled: false,
×
258
                v_offset: None,
×
259
                h_offset: None,
×
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✔
274
            return ScrollEvent {
×
275
                handled: true,
×
276
                v_offset: response.offset,
×
277
                h_offset: None,
×
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 => {
285
                let off = self.offset().saturating_sub(3);
×
286
                self.set_offset(off);
×
287
                ScrollEvent {
×
288
                    handled: true,
×
289
                    v_offset: Some(off),
×
290
                    h_offset: None,
×
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
            }
302
            _ => ScrollEvent {
×
303
                handled: false,
×
304
                v_offset: None,
×
305
                h_offset: None,
×
306
            },
×
307
        };
308
        if mouse_scroll_resp.handled {
1✔
309
            return mouse_scroll_resp;
1✔
310
        }
×
311
        if self.h_total > self.h_view && self.h_view > 0 {
×
312
            let response = self.h_drag.handle_mouse(
×
313
                mouse,
×
314
                self.area,
×
315
                self.h_total,
×
316
                self.h_view,
×
317
                ScrollbarAxis::Horizontal,
×
318
            );
319
            if let Some(offset) = response.offset {
×
320
                self.set_h_offset(offset);
×
321
            }
×
322
            if response.handled {
×
323
                return ScrollEvent {
×
324
                    handled: true,
×
325
                    v_offset: None,
×
326
                    h_offset: response.offset,
×
327
                };
×
328
            }
×
329
        }
×
330
        ScrollEvent {
×
331
            handled: false,
×
332
            v_offset: None,
×
333
            h_offset: None,
×
334
        }
×
335
    }
1✔
336

337
    fn max_h_offset(&self) -> usize {
×
338
        self.h_total.saturating_sub(self.h_view)
×
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✔
347
            return false;
×
348
        }
1✔
349
        let max_offset = self.total.saturating_sub(self.view);
1✔
350
        let kb = crate::keybindings::KeyBindings::default();
1✔
351
        if kb.matches(crate::keybindings::Action::ScrollPageUp, key) {
1✔
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✔
NEW
357
        } else if kb.matches(crate::keybindings::Action::ScrollPageDown, key) {
×
NEW
358
            let page = self.view.max(1);
×
NEW
359
            let off = (self.v_state.offset.saturating_add(page)).min(max_offset);
×
NEW
360
            self.v_state.offset = off;
×
NEW
361
            self.v_state.apply(self.total, self.view);
×
NEW
362
            true
×
NEW
363
        } else if kb.matches(crate::keybindings::Action::ScrollHome, key) {
×
NEW
364
            self.v_state.offset = 0;
×
NEW
365
            self.v_state.apply(self.total, self.view);
×
NEW
366
            true
×
NEW
367
        } else if kb.matches(crate::keybindings::Action::ScrollEnd, key) {
×
NEW
368
            self.v_state.offset = max_offset;
×
NEW
369
            self.v_state.apply(self.total, self.view);
×
NEW
370
            true
×
NEW
371
        } else if kb.matches(crate::keybindings::Action::ScrollUp, key) {
×
NEW
372
            let off = self.v_state.offset.saturating_sub(1);
×
NEW
373
            self.v_state.offset = off;
×
NEW
374
            self.v_state.apply(self.total, self.view);
×
NEW
375
            true
×
NEW
376
        } else if kb.matches(crate::keybindings::Action::ScrollDown, key) {
×
NEW
377
            let off = (self.v_state.offset.saturating_add(1)).min(max_offset);
×
NEW
378
            self.v_state.offset = off;
×
NEW
379
            self.v_state.apply(self.total, self.view);
×
NEW
380
            true
×
381
        } else {
NEW
382
            false
×
383
        }
384
    }
1✔
385

386
    fn max_offset(&self) -> usize {
7✔
387
        self.total.saturating_sub(self.view)
7✔
388
    }
7✔
389
}
390

391
impl Default for ScrollViewComponent {
392
    fn default() -> Self {
×
393
        Self::new()
×
394
    }
×
395
}
396

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

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

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

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

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

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

477
#[cfg(test)]
478
mod tests {
479
    use super::*;
480
    use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
481
    use ratatui::prelude::Rect;
482

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

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

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

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

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

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

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

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

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