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