ratatui_kit/components/scroll_view/
mod.rs1use crate::{AnyElement, Component, layout_style::LayoutStyle};
2use crate::{Hook, State, UseEffect, UseState};
3use ratatui::{
4 buffer::Buffer,
5 layout::{Constraint, Direction, Flex, Layout, Margin, Offset, Rect},
6 widgets::StatefulWidgetRef,
7};
8use ratatui_kit_macros::Props;
9mod state;
10pub use state::ScrollViewState;
11mod scrollbars;
12pub use scrollbars::{ScrollBars, ScrollbarVisibility};
13
14#[derive(Default, Props)]
15pub struct ScrollViewProps<'a> {
16 pub flex_direction: Direction,
17 pub justify_content: Flex,
18 pub gap: i32,
19 pub margin: Margin,
20 pub offset: Offset,
21 pub width: Constraint,
22 pub height: Constraint,
23 pub children: Vec<AnyElement<'a>>,
24 pub scroll_bars: ScrollBars<'static>,
25 pub scroll_view_state: ScrollViewState,
26}
27
28pub struct ScrollView {
29 scroll_bars: ScrollBars<'static>,
30}
31
32impl<'a> From<&ScrollViewProps<'a>> for LayoutStyle {
33 fn from(props: &ScrollViewProps) -> Self {
34 LayoutStyle {
35 flex_direction: props.flex_direction,
36 justify_content: props.justify_content,
37 gap: props.gap,
38 margin: props.margin,
39 offset: props.offset,
40 width: props.width,
41 height: props.height,
42 }
43 }
44}
45
46impl Component for ScrollView {
47 type Props<'a> = ScrollViewProps<'a>;
48
49 fn new(props: &Self::Props<'_>) -> Self {
50 Self {
51 scroll_bars: props.scroll_bars.clone(),
52 }
53 }
54
55 fn update(
56 &mut self,
57 props: &mut Self::Props<'_>,
58 mut hooks: crate::Hooks,
59 updater: &mut crate::ComponentUpdater,
60 ) {
61 let layout_style = LayoutStyle::from(&*props);
62
63 let scroll_view_state = hooks.use_state(|| props.scroll_view_state);
64
65 let scrollbars = hooks.use_state(|| props.scroll_bars.clone());
66
67 hooks.use_effect(
68 || {
69 *scrollbars.write() = props.scroll_bars.clone();
70 },
71 props.scroll_bars.clone(),
72 );
73
74 hooks.use_effect(
75 || {
76 *scroll_view_state.write() = props.scroll_view_state;
77 },
78 props.scroll_view_state,
79 );
80
81 hooks.use_hook(|| UseScrollImpl {
82 scroll_view_state,
83 scrollbars,
84 area: None,
85 });
86
87 self.scroll_bars = props.scroll_bars.clone();
88
89 updater.set_layout_style(layout_style);
90 updater.update_children(&mut props.children, None);
91 }
92
93 fn update_children_areas(
94 &mut self,
95 children: &crate::Components,
96 layout_style: &LayoutStyle,
97 drawer: &mut crate::ComponentDrawer<'_, '_>,
98 ) {
99 let constraint_sum = |d: Direction, len: u16| {
100 children
101 .get_constraints(d)
102 .iter()
103 .map(|c| match c {
104 Constraint::Length(h) => *h,
105 Constraint::Percentage(p) => len * *p / 100,
106 Constraint::Ratio(r, n) => {
107 if *n != 0 {
108 len * (*r as u16) / (*n as u16)
109 } else {
110 0
111 }
112 }
113 Constraint::Min(min) => *min,
114 Constraint::Max(max) => *max,
115 Constraint::Fill(i) => len * i,
116 })
117 .collect::<Vec<_>>()
118 };
119
120 let old_width_height = {
121 let area = drawer.area;
122 match layout_style.flex_direction {
123 Direction::Horizontal => {
124 let sum_w = constraint_sum(Direction::Horizontal, area.width);
125 let sum_count = sum_w.len();
126 let sum_w = sum_w.iter().sum::<u16>()
127 + ((sum_count as i32 - 1) * layout_style.gap) as u16;
128 let sum_h = constraint_sum(Direction::Vertical, area.height)
129 .into_iter()
130 .max()
131 .unwrap_or_default();
132 (sum_w, sum_h)
133 }
134 Direction::Vertical => {
135 let sum_h = constraint_sum(Direction::Vertical, area.height);
136 let sum_count = sum_h.len();
137 let sum_h = sum_h.iter().sum::<u16>()
138 + ((sum_count as i32 - 1) * layout_style.gap) as u16;
139 let sum_w = constraint_sum(Direction::Horizontal, area.width)
140 .into_iter()
141 .max()
142 .unwrap_or_default();
143 (sum_w, sum_h)
144 }
145 }
146 };
147
148 let horizontal_space = drawer.area.width as i32 - old_width_height.0 as i32 + 1;
149 let vertical_space = drawer.area.height as i32 - old_width_height.1 as i32 + 1;
150 let (show_horizontal, show_vertical) = self
151 .scroll_bars
152 .visible_scrollbars(horizontal_space, vertical_space);
153
154 let (width, height, justify_constraints, align_constraints) = {
155 let mut area = drawer.area;
156 if show_horizontal {
157 area.height -= 1;
158 }
159 if show_vertical {
160 area.width -= 1;
161 }
162 match layout_style.flex_direction {
163 Direction::Horizontal => {
164 let widths = constraint_sum(Direction::Horizontal, area.width);
165 let sum_count = widths.len();
166
167 let justify_constraints = widths
168 .iter()
169 .map(|c| Constraint::Length(*c))
170 .collect::<Vec<Constraint>>();
171
172 let sum_w = widths.iter().sum::<u16>()
173 + ((sum_count as i32 - 1) * layout_style.gap) as u16;
174
175 let heights = constraint_sum(Direction::Vertical, area.height);
176 let sum_h = heights.iter().max().copied().unwrap_or_default();
177
178 let align_constraints = heights
179 .iter()
180 .map(|c| Constraint::Length(*c))
181 .collect::<Vec<Constraint>>();
182
183 (sum_w, sum_h, justify_constraints, align_constraints)
184 }
185 Direction::Vertical => {
186 let heights = constraint_sum(Direction::Vertical, area.height);
187 let sum_count = heights.len();
188
189 let justify_constraints = heights
190 .iter()
191 .map(|c| Constraint::Length(*c))
192 .collect::<Vec<Constraint>>();
193
194 let sum_h = heights.iter().sum::<u16>()
195 + ((sum_count as i32 - 1) * layout_style.gap) as u16;
196
197 let widths = constraint_sum(Direction::Horizontal, area.width);
198 let sum_w = widths.iter().max().copied().unwrap_or_default();
199
200 let align_constraints = widths
201 .iter()
202 .map(|c| Constraint::Length(*c))
203 .collect::<Vec<Constraint>>();
204
205 (sum_w, sum_h, justify_constraints, align_constraints)
206 }
207 }
208 };
209
210 let rect = Rect::new(0, 0, width, height);
211 drawer.scroll_buffer = Some(Buffer::empty(rect));
212
213 drawer.area = drawer.buffer_mut().area;
214
215 let layout = layout_style.get_layout().constraints(justify_constraints);
217 let areas = layout.split(drawer.area);
218
219 let mut new_areas: Vec<ratatui::prelude::Rect> = vec![];
220
221 let rev_direction = match layout_style.flex_direction {
222 Direction::Horizontal => Direction::Vertical,
223 Direction::Vertical => Direction::Horizontal,
224 };
225 for (area, constraint) in areas.iter().zip(align_constraints.iter()) {
226 let area = Layout::new(rev_direction, [constraint]).split(*area)[0];
227 new_areas.push(area);
228 }
229
230 drawer.children_areas = new_areas;
231 }
232}
233
234pub struct UseScrollImpl {
235 scroll_view_state: State<ScrollViewState>,
236 scrollbars: State<ScrollBars<'static>>,
237 area: Option<ratatui::layout::Rect>,
238}
239
240impl Hook for UseScrollImpl {
241 fn pre_component_draw(&mut self, drawer: &mut crate::ComponentDrawer) {
242 self.area = Some(drawer.area);
243 }
244 fn post_component_draw(&mut self, drawer: &mut crate::ComponentDrawer) {
245 let buffer = drawer.scroll_buffer.take().unwrap();
246 let scrollbars = self.scrollbars.read();
247 scrollbars.render_ref(
248 self.area.unwrap_or_default(),
249 drawer.buffer_mut(),
250 &mut (*self.scroll_view_state.write(), buffer),
251 );
252 }
253}