ratatui_kit/components/scroll_view/
scrollbars.rs1use super::ScrollViewState;
19use ratatui::{
20 buffer::Buffer,
21 layout::{Position, Rect, Size},
22 widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget},
23};
24use ratatui_kit_macros::Props;
25
26#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
27pub enum ScrollbarVisibility {
29 #[default]
31 Automatic,
32 Always,
34 Never,
36}
37
38#[derive(Props, Clone, Hash)]
39pub struct Scrollbars<'a> {
41 pub vertical_scrollbar_visibility: ScrollbarVisibility,
43 pub horizontal_scrollbar_visibility: ScrollbarVisibility,
45 pub vertical_scrollbar: Scrollbar<'a>,
47 pub horizontal_scrollbar: Scrollbar<'a>,
49 pub over_border: bool,
52}
53
54impl Default for Scrollbars<'_> {
55 fn default() -> Self {
56 Self {
57 vertical_scrollbar_visibility: ScrollbarVisibility::Automatic,
58 horizontal_scrollbar_visibility: ScrollbarVisibility::Automatic,
59 vertical_scrollbar: Scrollbar::new(ScrollbarOrientation::VerticalRight),
60 horizontal_scrollbar: Scrollbar::new(ScrollbarOrientation::HorizontalBottom),
61 over_border: true,
62 }
63 }
64}
65
66impl Scrollbars<'_> {
67 fn axis_show(visibility: ScrollbarVisibility, space: i32) -> bool {
69 match visibility {
70 ScrollbarVisibility::Always => true,
71 ScrollbarVisibility::Never => false,
72 ScrollbarVisibility::Automatic => space < 0,
73 }
74 }
75
76 pub(crate) fn content_area(&self, inner: Rect, content: Size, ring: bool) -> Rect {
79 if ring {
80 return inner;
81 }
82 let horizontal_space = inner.width as i32 - content.width as i32;
83 let vertical_space = inner.height as i32 - content.height as i32;
84 let (show_horizontal, show_vertical) =
85 self.visible_scrollbars(horizontal_space, vertical_space);
86 Rect {
87 width: inner.width.saturating_sub(show_vertical as u16),
88 height: inner.height.saturating_sub(show_horizontal as u16),
89 ..inner
90 }
91 }
92
93 fn render_visible_area(&self, dst: Rect, buf: &mut Buffer, src: Rect, scroll_buffer: &Buffer) {
94 for (src_row, dst_row) in src.rows().zip(dst.rows()) {
95 for (src_col, dst_col) in src_row.columns().zip(dst_row.columns()) {
96 buf[dst_col] = scroll_buffer[src_col].clone();
97 }
98 }
99 }
100
101 fn render_scrollbar(
103 scrollbar: &Scrollbar<'_>,
104 orientation: ScrollbarOrientation,
105 area: Rect,
106 buf: &mut Buffer,
107 position: u16,
108 content_len: u16,
109 viewport_len: u16,
110 ) {
111 let hidden = content_len.saturating_sub(viewport_len);
112 let mut scrollbar_state = ScrollbarState::new(hidden as usize).position(position as usize);
113 scrollbar
114 .clone()
115 .orientation(orientation)
116 .render(area, buf, &mut scrollbar_state);
117 }
118
119 pub fn visible_scrollbars(&self, horizontal_space: i32, vertical_space: i32) -> (bool, bool) {
120 type V = ScrollbarVisibility;
121
122 match (
123 self.horizontal_scrollbar_visibility,
124 self.vertical_scrollbar_visibility,
125 ) {
126 (V::Always, V::Always) => (true, true),
128 (V::Never, V::Never) => (false, false),
129 (V::Always, V::Never) => (true, false),
130 (V::Never, V::Always) => (false, true),
131
132 (V::Automatic, V::Never) => (horizontal_space < 0, false),
134 (V::Never, V::Automatic) => (false, vertical_space < 0),
135
136 (V::Always, V::Automatic) => (true, vertical_space <= 0),
140 (V::Automatic, V::Always) => (horizontal_space <= 0, true),
141
142 (V::Automatic, V::Automatic) => {
144 if horizontal_space >= 0 && vertical_space >= 0 {
145 (false, false)
147 } else if horizontal_space < 0 && vertical_space < 0 {
148 (true, true)
150 } else if horizontal_space > 0 && vertical_space < 0 {
151 (false, true)
153 } else if horizontal_space < 0 && vertical_space > 0 {
154 (true, false)
156 } else {
157 (true, true)
159 }
160 }
161 }
162 }
163
164 pub(crate) fn ring(&self, outer: Rect, inner: Rect) -> bool {
167 self.over_border && inner.right() < outer.right() && inner.bottom() < outer.bottom()
168 }
169
170 pub fn render_ref(
177 &self,
178 outer: Rect,
179 inner: Rect,
180 buf: &mut Buffer,
181 state: &mut ScrollViewState,
182 scroll_buffer: &Buffer,
183 ) {
184 let content = scroll_buffer.area.as_size();
185 let ring = self.ring(outer, inner);
186
187 let horizontal_space = inner.width as i32 - content.width as i32;
188 let vertical_space = inner.height as i32 - content.height as i32;
189 let (show_horizontal, show_vertical) = if ring {
190 (
192 Self::axis_show(self.horizontal_scrollbar_visibility, horizontal_space),
193 Self::axis_show(self.vertical_scrollbar_visibility, vertical_space),
194 )
195 } else {
196 self.visible_scrollbars(horizontal_space, vertical_space)
197 };
198
199 let viewport = if ring {
201 inner.as_size()
202 } else {
203 Size::new(
204 inner.width.saturating_sub(show_vertical as u16),
205 inner.height.saturating_sub(show_horizontal as u16),
206 )
207 };
208
209 let x = state
211 .offset
212 .x
213 .min(content.width.saturating_sub(viewport.width));
214 let y = state
215 .offset
216 .y
217 .min(content.height.saturating_sub(viewport.height));
218 state.offset = Position::new(x, y);
219 state.size = Some(content);
220 state.page_size = Some(viewport);
221
222 let src = Rect::new(x, y, viewport.width, viewport.height).intersection(scroll_buffer.area);
224 let dst = Rect::new(inner.x, inner.y, viewport.width, viewport.height);
225 self.render_visible_area(dst, buf, src, scroll_buffer);
226
227 if show_vertical {
230 let area = Rect::new(inner.x + viewport.width, inner.y, 1, viewport.height);
231 Self::render_scrollbar(
232 &self.vertical_scrollbar,
233 ScrollbarOrientation::VerticalRight,
234 area,
235 buf,
236 y,
237 content.height,
238 viewport.height,
239 );
240 }
241 if show_horizontal {
242 let area = Rect::new(inner.x, inner.y + viewport.height, viewport.width, 1);
243 Self::render_scrollbar(
244 &self.horizontal_scrollbar,
245 ScrollbarOrientation::HorizontalBottom,
246 area,
247 buf,
248 x,
249 content.width,
250 viewport.width,
251 );
252 }
253 }
254}