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