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