ratatui_kit/components/scroll_view/
mod.rs1use crate::{AnyElement, Component, layout_style::LayoutStyle};
37use crate::{Hook, State, UseEffect, UseEvents, UseState};
38use ratatui::widgets::Block;
39use ratatui::{
40 buffer::Buffer,
41 layout::{Constraint, Direction, Layout, Rect},
42};
43use ratatui_kit_macros::{Props, with_layout_style};
44mod state;
45pub use state::ScrollViewState;
46mod scrollbars;
47pub use scrollbars::{ScrollBars, ScrollbarVisibility};
48
49#[with_layout_style]
50#[derive(Default, Props)]
51pub struct ScrollViewProps<'a> {
53 pub children: Vec<AnyElement<'a>>,
55 pub scroll_bars: ScrollBars<'static>,
57 pub scroll_view_state: Option<State<ScrollViewState>>,
59
60 pub block: Option<Block<'static>>,
61
62 pub disabled: bool,
63}
64
65pub struct ScrollView {
67 scroll_bars: ScrollBars<'static>,
68 block: Option<Block<'static>>,
69}
70
71impl Component for ScrollView {
72 type Props<'a> = ScrollViewProps<'a>;
73
74 fn new(props: &Self::Props<'_>) -> Self {
75 Self {
76 scroll_bars: props.scroll_bars.clone(),
77 block: props.block.clone(),
78 }
79 }
80
81 fn update(
82 &mut self,
83 props: &mut Self::Props<'_>,
84 mut hooks: crate::Hooks,
85 updater: &mut crate::ComponentUpdater,
86 ) {
87 let layout_style = props.layout_style();
88
89 let scrollbars = hooks.use_state(|| props.scroll_bars.clone());
90
91 let this_scroll_view_state = hooks.use_state(ScrollViewState::default);
92
93 let disabled = props.disabled;
94 self.block = props.block.clone();
95
96 hooks.use_effect(
97 || {
98 *scrollbars.write() = props.scroll_bars.clone();
99 },
100 props.scroll_bars.clone(),
101 );
102
103 hooks.use_hook(|| UseScrollImpl {
104 scroll_view_state: props.scroll_view_state.unwrap_or(this_scroll_view_state),
105 scrollbars,
106 area: None,
107 has_block: props.block.is_some(),
108 });
109
110 hooks.use_local_events({
111 let props_scroll_view_state = props.scroll_view_state;
112 move |event| {
113 if props_scroll_view_state.is_none() && !disabled {
114 this_scroll_view_state.write().handle_event(&event);
115 }
116 }
117 });
118
119 self.scroll_bars = props.scroll_bars.clone();
120
121 updater.set_layout_style(layout_style);
122 updater.update_children(&mut props.children, None);
123 }
124
125 fn calc_children_areas(
126 &self,
127 children: &crate::Components,
128 layout_style: &LayoutStyle,
129 drawer: &mut crate::ComponentDrawer<'_, '_>,
130 ) -> Vec<ratatui::prelude::Rect> {
131 let constraint_sum = |d: Direction, len: u16| {
132 children
133 .get_constraints(d)
134 .iter()
135 .map(|c| match c {
136 Constraint::Length(h) => *h,
137 Constraint::Percentage(p) => len * *p / 100,
138 Constraint::Ratio(r, n) => {
139 if *n != 0 {
140 len * (*r as u16) / (*n as u16)
141 } else {
142 0
143 }
144 }
145 Constraint::Min(min) => *min,
146 Constraint::Max(max) => *max,
147 Constraint::Fill(i) => len * i,
148 })
149 .collect::<Vec<_>>()
150 };
151
152 let old_width_height = {
153 let area = drawer.area;
154 match layout_style.flex_direction {
155 Direction::Horizontal => {
156 let sum_w = constraint_sum(Direction::Horizontal, area.width);
157 let sum_count = sum_w.len();
158 let sum_w = sum_w.iter().sum::<u16>()
159 + ((sum_count as i32 - 1) * layout_style.gap) as u16;
160 let sum_h = constraint_sum(Direction::Vertical, area.height)
161 .into_iter()
162 .max()
163 .unwrap_or_default();
164 (sum_w, sum_h)
165 }
166 Direction::Vertical => {
167 let sum_h = constraint_sum(Direction::Vertical, area.height);
168 let sum_count = sum_h.len();
169 let sum_h = sum_h.iter().sum::<u16>()
170 + ((sum_count as i32 - 1) * layout_style.gap) as u16;
171 let sum_w = constraint_sum(Direction::Horizontal, area.width)
172 .into_iter()
173 .max()
174 .unwrap_or_default();
175 (sum_w, sum_h)
176 }
177 }
178 };
179
180 let horizontal_space = drawer.area.width as i32 - old_width_height.0 as i32 + 1;
181 let vertical_space = drawer.area.height as i32 - old_width_height.1 as i32 + 1;
182 let (show_horizontal, show_vertical) = self
183 .scroll_bars
184 .visible_scrollbars(horizontal_space, vertical_space);
185
186 let (width, height, justify_constraints, align_constraints) = {
187 let mut area = drawer.area;
188 if show_horizontal {
189 area.height = area.height.saturating_sub(1);
190 }
191 if show_vertical {
192 area.width = area.width.saturating_sub(1);
193 }
194 match layout_style.flex_direction {
195 Direction::Horizontal => {
196 let widths = constraint_sum(Direction::Horizontal, area.width);
197 let sum_count = widths.len();
198
199 let justify_constraints = widths
200 .iter()
201 .map(|c| Constraint::Length(*c))
202 .collect::<Vec<Constraint>>();
203
204 let sum_w = widths.iter().sum::<u16>()
205 + ((sum_count as i32 - 1) * layout_style.gap) as u16;
206
207 let heights = constraint_sum(Direction::Vertical, area.height);
208 let sum_h = heights.iter().max().copied().unwrap_or_default();
209
210 let align_constraints = heights
211 .iter()
212 .map(|c| Constraint::Length(*c))
213 .collect::<Vec<Constraint>>();
214
215 (sum_w, sum_h, justify_constraints, align_constraints)
216 }
217 Direction::Vertical => {
218 let heights = constraint_sum(Direction::Vertical, area.height);
219 let sum_count = heights.len();
220
221 let justify_constraints = heights
222 .iter()
223 .map(|c| Constraint::Length(*c))
224 .collect::<Vec<Constraint>>();
225
226 let sum_h = heights.iter().sum::<u16>()
227 + ((sum_count as i32 - 1) * layout_style.gap) as u16;
228
229 let widths = constraint_sum(Direction::Horizontal, area.width);
230 let sum_w = widths.iter().max().copied().unwrap_or_default();
231
232 let align_constraints = widths
233 .iter()
234 .map(|c| Constraint::Length(*c))
235 .collect::<Vec<Constraint>>();
236
237 (sum_w, sum_h, justify_constraints, align_constraints)
238 }
239 }
240 };
241
242 let rect = Rect::new(0, 0, width, height);
243 drawer.scroll_buffer = Some(Buffer::empty(rect));
244
245 drawer.area = drawer.buffer_mut().area;
246
247 let layout = layout_style.get_layout().constraints(justify_constraints);
249 let areas = layout.split(drawer.area);
250
251 let mut new_areas: Vec<ratatui::prelude::Rect> = vec![];
252
253 let rev_direction = match layout_style.flex_direction {
254 Direction::Horizontal => Direction::Vertical,
255 Direction::Vertical => Direction::Horizontal,
256 };
257 for (area, constraint) in areas.iter().zip(align_constraints.iter()) {
258 let area = Layout::new(rev_direction, [constraint]).split(*area)[0];
259 new_areas.push(area);
260 }
261
262 new_areas
263 }
264
265 fn draw(&mut self, drawer: &mut crate::ComponentDrawer<'_, '_>) {
266 if let Some(block) = &self.block {
267 let inner_area = block.inner(drawer.area);
268 drawer.render_widget(block, drawer.area);
269 drawer.area = inner_area;
270 }
271 }
272}
273
274pub struct UseScrollImpl {
275 scroll_view_state: State<ScrollViewState>,
276 scrollbars: State<ScrollBars<'static>>,
277 area: Option<ratatui::layout::Rect>,
278 has_block: bool,
279}
280
281impl Hook for UseScrollImpl {
282 fn pre_component_draw(&mut self, drawer: &mut crate::ComponentDrawer) {
283 self.area = Some(if self.has_block {
284 Rect {
285 x: drawer.area.x + 1,
286 y: drawer.area.y + 1,
287 width: drawer.area.width.saturating_sub(1),
288 height: drawer.area.height.saturating_sub(2),
289 }
290 } else {
291 drawer.area
292 });
293 }
294 fn post_component_draw(&mut self, drawer: &mut crate::ComponentDrawer) {
295 let buffer = drawer.scroll_buffer.take().unwrap();
296 let scrollbars = self.scrollbars.read();
297
298 scrollbars.render_ref(
299 self.area.unwrap_or_default(),
300 drawer.buffer_mut(),
301 &mut self.scroll_view_state.write_no_update(),
302 &buffer,
303 );
304 }
305}