ratatui_kit/components/scroll_view/
mod.rs

1//! ScrollView 组件:可滚动视图容器,支持横向/纵向滚动条,适合长列表、文档阅读等场景。
2//!
3//! ## 用法示例
4//!
5//! ### 自动管理滚动状态(推荐)
6//! ```rust
7//! element!(ScrollView(
8//!     scroll_bars: ScrollBars::default(),
9//! ){
10//!     // 子内容
11//! })
12//! ```
13//!
14//! ### 手动管理滚动状态
15//! ```rust
16//! let scroll_state = hooks.use_state(ScrollViewState::default);
17//!
18//! hooks.use_local_events(move |event| {
19//!     scroll_state.write().handle_event(&event);
20//! });
21//!
22//! element!(ScrollView(
23//!     scroll_view_state: scroll_state,
24//!     scroll_bars: ScrollBars::default(),
25//! ){
26//!     // 子内容
27//! })
28//! ```
29//!
30//! ScrollView 支持两种使用方式:
31//! 1. 不传递 `scroll_view_state` 参数,组件会自动管理滚动状态
32//! 2. 传递由 `use_state` 创建的 `scroll_view_state` 参数,手动管理滚动状态
33//!
34//! 当需要对滚动行为进行精确控制时(如程序化滚动、与其他状态联动等),建议使用手动管理模式。
35
36use 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)]
52/// ScrollView 组件属性。
53pub struct ScrollViewProps<'a> {
54    /// 子元素列表。
55    pub children: Vec<AnyElement<'a>>,
56    /// 滚动条配置。
57    pub scroll_bars: ScrollBars<'static>,
58    /// 滚动状态。
59    pub scroll_view_state: Option<State<ScrollViewState>>,
60}
61
62/// ScrollView 组件实现。
63pub 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        // flex layout
250        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}