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

jzombie / term-wm / 20770181342

07 Jan 2026 04:02AM UTC coverage: 36.037% (+2.5%) from 33.582%
20770181342

Pull #3

github

web-flow
Merge 4d97c195c into ab0b82880
Pull Request #3: General UI improvements

454 of 2495 new or added lines in 26 files covered. (18.2%)

17 existing lines in 7 files now uncovered.

2515 of 6979 relevant lines covered (36.04%)

2.06 hits per line

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

90.07
/src/ui.rs
1
//! UiFrame: a thin wrapper around `ratatui::Frame` that clamps drawing to the
2
//! visible area and centralizes clipping logic.
3
//!
4
//! Why this exists
5
//! - Components and widgets sometimes compute rectangles that drift partially or
6
//!   fully outside the terminal buffer. Writing out-of-bounds into the underlying
7
//!   `Buffer` can panic or corrupt rendering. `UiFrame` prevents that by
8
//!   clipping all draw calls to the visible area.
9
//!
10
//! Benefits
11
//! - Safety: components can call the familiar `render_widget` /
12
//!   `render_stateful_widget` helpers without needing to guard every draw with
13
//!   manual bounds checks.
14
//! - Simplicity: keeps widget code concise and focused on layout rather than
15
//!   buffer-safety details.
16
//! - Clear handling: by routing `Clear` widget rendering through `UiFrame`, we
17
//!   can safely clear regions without exposing a brittle `clear_rect` helper.
18
//!
19
//! Usage
20
//! - In paint closures, construct a `UiFrame` from a `ratatui::Frame` via
21
//!   `UiFrame::new(&mut frame)`. Use `frame.render_widget(...)` and
22
//!   `frame.render_stateful_widget(...)` as before. To clear an area, render the
23
//!   `Clear` widget through the `UiFrame`.
24
use ratatui::Frame;
25
use ratatui::buffer::Buffer;
26
use ratatui::layout::Rect;
27
use ratatui::style::Style;
28
use ratatui::widgets::{StatefulWidget, Widget};
29

30
/// Wrapper around `ratatui::Frame` that clamps drawing to the visible area.
31
///
32
/// Components render through this type so they can keep calling familiar
33
/// `render_widget` / `render_stateful_widget` helpers while automatically
34
/// clipping any rectangles that drift outside the buffer.
35
pub struct UiFrame<'a> {
36
    area: Rect,
37
    buffer: &'a mut Buffer,
38
}
39

40
impl<'a> UiFrame<'a> {
NEW
41
    pub fn new(frame: &'a mut Frame<'_>) -> Self {
×
NEW
42
        let area = frame.area();
×
NEW
43
        let buffer = frame.buffer_mut();
×
NEW
44
        Self { area, buffer }
×
NEW
45
    }
×
46

47
    /// Test helper: construct a `UiFrame` directly from an area and buffer.
48
    ///
49
    /// This exists to make unit testing of clipping behavior straightforward
50
    /// without constructing a full `ratatui::Frame` in tests.
51
    #[cfg(test)]
52
    fn from_parts(area: Rect, buffer: &'a mut Buffer) -> Self {
2✔
53
        Self { area, buffer }
2✔
54
    }
2✔
55

NEW
56
    pub fn area(&self) -> Rect {
×
NEW
57
        self.area
×
NEW
58
    }
×
59

NEW
60
    pub fn buffer_mut(&mut self) -> &mut Buffer {
×
NEW
61
        self.buffer
×
NEW
62
    }
×
63

64
    fn clip_rect(&self, rect: Rect) -> Option<Rect> {
2✔
65
        let clipped = rect.intersection(self.area);
2✔
66
        if clipped.width == 0 || clipped.height == 0 {
2✔
NEW
67
            None
×
68
        } else {
69
            Some(clipped)
2✔
70
        }
71
    }
2✔
72

73
    pub fn render_widget<W>(&mut self, widget: W, area: Rect)
1✔
74
    where
1✔
75
        W: Widget,
1✔
76
    {
77
        if let Some(clipped) = self.clip_rect(area) {
1✔
78
            widget.render(clipped, self.buffer);
1✔
79
        }
1✔
80
    }
1✔
81

82
    pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
1✔
83
    where
1✔
84
        W: StatefulWidget,
1✔
85
    {
86
        if let Some(clipped) = self.clip_rect(area) {
1✔
87
            widget.render(clipped, self.buffer, state);
1✔
88
        }
1✔
89
    }
1✔
90
}
91

92
pub(crate) fn safe_set_string(
2✔
93
    buffer: &mut Buffer,
2✔
94
    bounds: Rect,
2✔
95
    x: u16,
2✔
96
    y: u16,
2✔
97
    text: &str,
2✔
98
    style: Style,
2✔
99
) {
2✔
100
    if bounds.width == 0 || bounds.height == 0 {
2✔
101
        return;
×
102
    }
2✔
103
    let max_x = bounds.x.saturating_add(bounds.width);
2✔
104
    let max_y = bounds.y.saturating_add(bounds.height);
2✔
105
    if x < bounds.x || x >= max_x || y < bounds.y || y >= max_y {
2✔
106
        return;
1✔
107
    }
1✔
108
    let available = max_x.saturating_sub(x);
1✔
109
    if available == 0 {
1✔
110
        return;
×
111
    }
1✔
112
    let text = truncate_to_width(text, available as usize);
1✔
113
    buffer.set_string(x, y, text, style);
1✔
114
}
2✔
115

116
pub(crate) fn truncate_to_width(value: &str, width: usize) -> String {
3✔
117
    if value.chars().count() <= width {
3✔
118
        return value.to_string();
2✔
119
    }
1✔
120
    value.chars().take(width).collect()
1✔
121
}
3✔
122

123
#[cfg(test)]
124
mod tests {
125
    use super::*;
126
    use ratatui::buffer::Buffer;
127
    use ratatui::style::Style;
128

129
    #[test]
130
    fn truncate_to_width_short_and_long() {
1✔
131
        assert_eq!(truncate_to_width("abc", 5), "abc");
1✔
132
        assert_eq!(truncate_to_width("abcdef", 3), "abc");
1✔
133
    }
1✔
134

135
    #[test]
136
    fn safe_set_string_writes_within_bounds() {
1✔
137
        let bounds = ratatui::layout::Rect {
1✔
138
            x: 0,
1✔
139
            y: 0,
1✔
140
            width: 10,
1✔
141
            height: 2,
1✔
142
        };
1✔
143
        let mut buf = Buffer::empty(bounds);
1✔
144
        safe_set_string(&mut buf, bounds, 1, 0, "hello", Style::default());
1✔
145
        let cell = buf.cell_mut((1, 0)).expect("cell present");
1✔
146
        let first = cell.symbol().chars().next().unwrap();
1✔
147
        assert_eq!(first, 'h');
1✔
148

149
        // outside bounds should be ignored (no panic)
150
        safe_set_string(&mut buf, bounds, 100, 0, "x", Style::default());
1✔
151
    }
1✔
152

153
    #[test]
154
    fn render_widget_clips_to_frame_area() {
1✔
155
        use ratatui::layout::Rect;
156

157
        let area = Rect {
1✔
158
            x: 0,
1✔
159
            y: 0,
1✔
160
            width: 5,
1✔
161
            height: 3,
1✔
162
        };
1✔
163
        let mut buf = Buffer::empty(area);
1✔
164
        let mut ui = UiFrame::from_parts(area, &mut buf);
1✔
165

166
        struct FillWidget;
167
        impl Widget for FillWidget {
168
            fn render(self, area: Rect, buf: &mut Buffer) {
1✔
169
                for y in area.y..area.y.saturating_add(area.height) {
2✔
170
                    for x in area.x..area.x.saturating_add(area.width) {
4✔
171
                        if let Some(cell) = buf.cell_mut((x, y)) {
4✔
172
                            cell.set_symbol("A");
4✔
173
                        }
4✔
174
                    }
175
                }
176
            }
1✔
177
        }
178

179
        // Request an area that partially lies outside the right edge.
180
        ui.render_widget(
1✔
181
            FillWidget,
1✔
182
            Rect {
1✔
183
                x: 3,
1✔
184
                y: 1,
1✔
185
                width: 5,
1✔
186
                height: 2,
1✔
187
            },
1✔
188
        );
189

190
        // Inside clipped region
191
        let inside = buf.cell_mut((3, 1)).expect("cell present");
1✔
192
        assert!(inside.symbol().starts_with('A'));
1✔
193

194
        // Outside clipped region (left of the filled area)
195
        let outside = buf.cell_mut((2, 1)).expect("cell present");
1✔
196
        assert!(!outside.symbol().starts_with('A'));
1✔
197
    }
1✔
198

199
    #[test]
200
    fn render_stateful_widget_clips_to_frame_area() {
1✔
201
        use ratatui::layout::Rect;
202

203
        let area = Rect {
1✔
204
            x: 0,
1✔
205
            y: 0,
1✔
206
            width: 6,
1✔
207
            height: 4,
1✔
208
        };
1✔
209
        let mut buf = Buffer::empty(area);
1✔
210
        let mut ui = UiFrame::from_parts(area, &mut buf);
1✔
211

212
        struct FillStateful;
213
        impl StatefulWidget for FillStateful {
214
            type State = usize;
215
            fn render(self, area: Rect, buf: &mut Buffer, _state: &mut Self::State) {
1✔
216
                for y in area.y..area.y.saturating_add(area.height) {
2✔
217
                    for x in area.x..area.x.saturating_add(area.width) {
8✔
218
                        if let Some(cell) = buf.cell_mut((x, y)) {
8✔
219
                            cell.set_symbol("S");
8✔
220
                        }
8✔
221
                    }
222
                }
223
            }
1✔
224
        }
225

226
        // Request an area that exceeds bottom edge.
227
        let mut state = 0usize;
1✔
228
        ui.render_stateful_widget(
1✔
229
            FillStateful,
1✔
230
            Rect {
1✔
231
                x: 1,
1✔
232
                y: 2,
1✔
233
                width: 4,
1✔
234
                height: 4,
1✔
235
            },
1✔
236
            &mut state,
1✔
237
        );
238

239
        // Inside clipped region
240
        let inside = buf.cell_mut((1, 2)).expect("cell present");
1✔
241
        assert!(inside.symbol().starts_with('S'));
1✔
242

243
        // Outside clipped region (below buffer)
244
        // Coordinates (1, 6) are outside; ensure we don't panic by checking a nearby in-bounds cell
245
        let near = buf.cell_mut((1, 3)).expect("cell present");
1✔
246
        assert!(near.symbol().starts_with('S'));
1✔
247
    }
1✔
248
}
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