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