Skip to main content

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