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