ratatui_kit/components/scroll_view/
mod.rs

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