ratatui_kit/components/scroll_view/
mod.rs

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