ratatui_kit/components/scroll_view/
scrollbars.rs1use super::ScrollViewState;
17use ratatui::{
18 buffer::Buffer,
19 layout::{Rect, Size},
20 widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget},
21};
22use ratatui_kit_macros::Props;
23
24#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
25pub enum ScrollbarVisibility {
27 #[default]
29 Automatic,
30 Always,
32 Never,
34}
35
36#[derive(Props, Clone, Hash)]
37pub struct ScrollBars<'a> {
39 pub vertical_scrollbar_visibility: ScrollbarVisibility,
41 pub horizontal_scrollbar_visibility: ScrollbarVisibility,
43 pub vertical_scrollbar: Scrollbar<'a>,
45 pub horizontal_scrollbar: Scrollbar<'a>,
47}
48
49impl Default for ScrollBars<'_> {
50 fn default() -> Self {
51 Self {
52 vertical_scrollbar_visibility: ScrollbarVisibility::Automatic,
53 horizontal_scrollbar_visibility: ScrollbarVisibility::Automatic,
54 vertical_scrollbar: Scrollbar::new(ScrollbarOrientation::VerticalRight),
55 horizontal_scrollbar: Scrollbar::new(ScrollbarOrientation::HorizontalBottom),
56 }
57 }
58}
59
60pub(crate) struct ScrollbarLayout {
61 pub show_horizontal: bool,
62 pub show_vertical: bool,
63 pub visible_area: Rect,
64}
65
66impl ScrollBars<'_> {
67 pub(crate) fn layout_for(&self, area: Rect, content_size: Size) -> ScrollbarLayout {
68 let horizontal_space = area.width as i32 - content_size.width as i32;
69 let vertical_space = area.height as i32 - content_size.height as i32;
70 let (show_horizontal, show_vertical) =
71 self.visible_scrollbars(horizontal_space, vertical_space);
72
73 ScrollbarLayout {
74 show_horizontal,
75 show_vertical,
76 visible_area: Rect {
77 width: area.width.saturating_sub(show_vertical as u16),
78 height: area.height.saturating_sub(show_horizontal as u16),
79 ..area
80 },
81 }
82 }
83
84 fn render_visible_area(
85 &self,
86 area: Rect,
87 buf: &mut Buffer,
88 visible_area: Rect,
89 scroll_buffer: &Buffer,
90 ) {
91 for (src_row, dst_row) in visible_area.rows().zip(area.rows()) {
93 for (src_col, dst_col) in src_row.columns().zip(dst_row.columns()) {
94 buf[dst_col] = scroll_buffer[src_col].clone();
95 }
96 }
97 }
98
99 fn render_vertical_scrollbar(
100 &self,
101 area: Rect,
102 buf: &mut Buffer,
103 state: &ScrollViewState,
104 scroll_size: Size,
105 ) {
106 let scrollbar_height = scroll_size.height.saturating_sub(area.height);
107 let mut scrollbar_state =
108 ScrollbarState::new(scrollbar_height as usize).position(state.offset.y as usize);
109
110 self.vertical_scrollbar
111 .clone()
112 .render(area, buf, &mut scrollbar_state);
113 }
114
115 fn render_horizontal_scrollbar(
116 &self,
117 area: Rect,
118 buf: &mut Buffer,
119 state: &ScrollViewState,
120 scroll_size: Size,
121 ) {
122 let scrollbar_width = scroll_size.width.saturating_sub(area.width);
123
124 let mut scrollbar_state =
125 ScrollbarState::new(scrollbar_width as usize).position(state.offset.x as usize);
126 self.horizontal_scrollbar
127 .clone()
128 .render(area, buf, &mut scrollbar_state);
129 }
130
131 pub fn visible_scrollbars(&self, horizontal_space: i32, vertical_space: i32) -> (bool, bool) {
132 type V = ScrollbarVisibility;
133
134 match (
135 self.horizontal_scrollbar_visibility,
136 self.vertical_scrollbar_visibility,
137 ) {
138 (V::Always, V::Always) => (true, true),
140 (V::Never, V::Never) => (false, false),
141 (V::Always, V::Never) => (true, false),
142 (V::Never, V::Always) => (false, true),
143
144 (V::Automatic, V::Never) => (horizontal_space < 0, false),
146 (V::Never, V::Automatic) => (false, vertical_space < 0),
147
148 (V::Always, V::Automatic) => (true, vertical_space <= 0),
152 (V::Automatic, V::Always) => (horizontal_space <= 0, true),
153
154 (V::Automatic, V::Automatic) => {
156 if horizontal_space >= 0 && vertical_space >= 0 {
157 (false, false)
159 } else if horizontal_space < 0 && vertical_space < 0 {
160 (true, true)
162 } else if horizontal_space > 0 && vertical_space < 0 {
163 (false, true)
165 } else if horizontal_space < 0 && vertical_space > 0 {
166 (true, false)
168 } else {
169 (true, true)
171 }
172 }
173 }
174 }
175
176 fn render_scrollbars(
177 &self,
178 area: Rect,
179 buf: &mut Buffer,
180 state: &mut ScrollViewState,
181 scroll_buffer: &Buffer,
182 ) -> Rect {
183 let size: ratatui::prelude::Size = scroll_buffer.area.as_size();
184 let layout = self.layout_for(area, size);
185 let horizontal_space = area.width as i32 - size.width as i32;
186 let vertical_space = area.height as i32 - size.height as i32;
187
188 if horizontal_space > 0 {
190 state.offset.x = 0;
191 }
192 if vertical_space > 0 {
193 state.offset.y = 0;
194 }
195
196 if layout.show_horizontal {
197 let width = area.width.saturating_sub(layout.show_vertical as u16);
199 let render_area = Rect { width, ..area };
200 self.render_horizontal_scrollbar(render_area, buf, state, size);
202 }
203
204 if layout.show_vertical {
205 let height = area.height.saturating_sub(layout.show_horizontal as u16);
207 let render_area = Rect { height, ..area };
208 self.render_vertical_scrollbar(render_area, buf, state, size);
210 }
211
212 Rect::new(
213 state.offset.x,
214 state.offset.y,
215 layout.visible_area.width,
216 layout.visible_area.height,
217 )
218 }
219
220 pub fn render_ref(
221 &self,
222 area: Rect,
223 buf: &mut Buffer,
224 state: &mut ScrollViewState,
225 scroll_buffer: &Buffer,
226 ) {
227 let (mut x, mut y) = state.offset.into();
228 let max_x_offset = scroll_buffer.area.width.saturating_sub(area.width);
230 let max_y_offset = scroll_buffer.area.height.saturating_sub(area.height);
231
232 x = x.min(max_x_offset);
233 y = y.min(max_y_offset);
234 state.offset = (x, y).into();
235 state.size = Some(scroll_buffer.area.as_size());
236 state.page_size = Some(area.into());
237 let visible_area = self
238 .render_scrollbars(area, buf, state, scroll_buffer)
239 .intersection(scroll_buffer.area);
240 self.render_visible_area(area, buf, visible_area, scroll_buffer);
241 }
242}