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, 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        // flex layout
196        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}