ratatui_kit/components/scroll_view/
scrollbars.rs

1//! ScrollBars 组件:滚动视图的滚动条配置与渲染,支持横向/纵向滚动条、可见性控制、自定义样式。
2//!
3//! ## 用法示例
4//! ```rust
5//! element!(ScrollView(
6//!     scroll_bars: ScrollBars {
7//!         vertical_scrollbar_visibility: ScrollbarVisibility::Always,
8//!         horizontal_scrollbar_visibility: ScrollbarVisibility::Automatic,
9//!         ..Default::default()
10//!     },
11//!     // ...
12//! ))
13//! ```
14//! 可灵活控制滚动条的显示策略和样式,适合长列表、表格、文档等场景。
15
16use super::ScrollViewState;
17use ratatui::{
18    buffer::Buffer,
19    layout::{Rect, Size},
20    widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget},
21};
22use ratatui_kit_macros::Props;
23
24#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
25/// 滚动条可见性枚举。
26pub enum ScrollbarVisibility {
27    /// 仅在需要时渲染滚动条。
28    #[default]
29    Automatic,
30    /// 始终渲染滚动条。
31    Always,
32    /// 从不渲染滚动条(隐藏)。
33    Never,
34}
35
36#[derive(Props, Clone, Hash)]
37/// 滚动条配置。
38pub struct ScrollBars<'a> {
39    /// 纵向滚动条可见性。
40    pub vertical_scrollbar_visibility: ScrollbarVisibility,
41    /// 横向滚动条可见性。
42    pub horizontal_scrollbar_visibility: ScrollbarVisibility,
43    /// 纵向滚动条样式。
44    pub vertical_scrollbar: Scrollbar<'a>,
45    /// 横向滚动条样式。
46    pub horizontal_scrollbar: Scrollbar<'a>,
47}
48
49impl Default for ScrollBars<'_> {
50    fn default() -> Self {
51        Self {
52            vertical_scrollbar_visibility: ScrollbarVisibility::Automatic,
53            horizontal_scrollbar_visibility: ScrollbarVisibility::Automatic,
54            vertical_scrollbar: Scrollbar::new(ScrollbarOrientation::VerticalRight),
55            horizontal_scrollbar: Scrollbar::new(ScrollbarOrientation::HorizontalBottom),
56        }
57    }
58}
59
60impl ScrollBars<'_> {
61    fn render_visible_area(
62        &self,
63        area: Rect,
64        buf: &mut Buffer,
65        visible_area: Rect,
66        scroll_buffer: &Buffer,
67    ) {
68        // TODO: 这里可能有更高效的实现方式
69        for (src_row, dst_row) in visible_area.rows().zip(area.rows()) {
70            for (src_col, dst_col) in src_row.columns().zip(dst_row.columns()) {
71                buf[dst_col] = scroll_buffer[src_col].clone();
72            }
73        }
74    }
75
76    fn render_vertical_scrollbar(
77        &self,
78        area: Rect,
79        buf: &mut Buffer,
80        state: &ScrollViewState,
81        scroll_size: Size,
82    ) {
83        let scrollbar_height = scroll_size.height.saturating_sub(area.height);
84        let mut scrollbar_state =
85            ScrollbarState::new(scrollbar_height as usize).position(state.offset.y as usize);
86
87        self.vertical_scrollbar
88            .clone()
89            .render(area, buf, &mut scrollbar_state);
90    }
91
92    fn render_horizontal_scrollbar(
93        &self,
94        area: Rect,
95        buf: &mut Buffer,
96        state: &ScrollViewState,
97        scroll_size: Size,
98    ) {
99        let scrollbar_width = scroll_size.width.saturating_sub(area.width);
100
101        let mut scrollbar_state =
102            ScrollbarState::new(scrollbar_width as usize).position(state.offset.x as usize);
103        self.horizontal_scrollbar
104            .clone()
105            .render(area, buf, &mut scrollbar_state);
106    }
107
108    pub fn visible_scrollbars(&self, horizontal_space: i32, vertical_space: i32) -> (bool, bool) {
109        type V = ScrollbarVisibility;
110
111        match (
112            self.horizontal_scrollbar_visibility,
113            self.vertical_scrollbar_visibility,
114        ) {
115            // 直接渲染,无需检查适配值
116            (V::Always, V::Always) => (true, true),
117            (V::Never, V::Never) => (false, false),
118            (V::Always, V::Never) => (true, false),
119            (V::Never, V::Always) => (false, true),
120
121            // Auto => 仅在不适配时渲染滚动条
122            (V::Automatic, V::Never) => (horizontal_space < 0, false),
123            (V::Never, V::Automatic) => (false, vertical_space < 0),
124
125            // Auto => 渲染滚动条如果:
126            //   不适配;或
127            //   完全适配(另一个滚动条占用一行导致触发)
128            (V::Always, V::Automatic) => (true, vertical_space <= 0),
129            (V::Automatic, V::Always) => (horizontal_space <= 0, true),
130
131            // 仅依赖适配值
132            (V::Automatic, V::Automatic) => {
133                if horizontal_space >= 0 && vertical_space >= 0 {
134                    // 两个方向都有足够空间
135                    (false, false)
136                } else if horizontal_space < 0 && vertical_space < 0 {
137                    // 两个方向都没有足够空间
138                    (true, true)
139                } else if horizontal_space > 0 && vertical_space < 0 {
140                    // 水平适配,垂直不适配
141                    (false, true)
142                } else if horizontal_space < 0 && vertical_space > 0 {
143                    // 垂直适配,水平不适配
144                    (true, false)
145                } else {
146                    // 一个方向完全适配,另一个方向不适配,导致两个滚动条都可见,因为另一个滚动条会占用缓冲区的一行
147                    (true, true)
148                }
149            }
150        }
151    }
152
153    fn render_scrollbars(
154        &self,
155        area: Rect,
156        buf: &mut Buffer,
157        state: &mut ScrollViewState,
158        scroll_buffer: &Buffer,
159    ) -> Rect {
160        let size: ratatui::prelude::Size = scroll_buffer.area.as_size();
161        // 每个方向的适配值
162        //   > 0 => 适配
163        //  == 0 => 完全适配
164        //   < 0 => 不适配
165        let horizontal_space = area.width as i32 - size.width as i32;
166        let vertical_space = area.height as i32 - size.height as i32;
167
168        // 如果该方向适配,则重置状态
169        if horizontal_space > 0 {
170            state.offset.x = 0;
171        }
172        if vertical_space > 0 {
173            state.offset.y = 0;
174        }
175
176        let (show_horizontal, show_vertical) =
177            self.visible_scrollbars(horizontal_space, vertical_space);
178
179        let new_height = if show_horizontal {
180            // 如果两个滚动条都渲染,避免角落重叠
181            let width = area.width.saturating_sub(show_vertical as u16);
182            let render_area = Rect { width, ..area };
183            // 渲染滚动条,更新可用空间
184            self.render_horizontal_scrollbar(render_area, buf, state, size);
185            area.height.saturating_sub(1)
186        } else {
187            area.height
188        };
189
190        let new_width = if show_vertical {
191            // 如果两个滚动条都渲染,避免角落重叠
192            let height = area.height.saturating_sub(show_horizontal as u16);
193            let render_area = Rect { height, ..area };
194            // 渲染滚动条,更新可用空间
195            self.render_vertical_scrollbar(render_area, buf, state, size);
196            area.width.saturating_sub(1)
197        } else {
198            area.width
199        };
200
201        Rect::new(state.offset.x, state.offset.y, new_width, new_height)
202    }
203
204    pub fn render_ref(
205        &self,
206        area: Rect,
207        buf: &mut Buffer,
208        state: &mut ScrollViewState,
209        scroll_buffer: &Buffer,
210    ) {
211        let (mut x, mut y) = state.offset.into();
212        // 确保不会在任一方向上滚动超过缓冲区末尾
213        let max_x_offset = scroll_buffer.area.width.saturating_sub(area.width);
214        let max_y_offset = scroll_buffer.area.height.saturating_sub(area.height);
215
216        x = x.min(max_x_offset);
217        y = y.min(max_y_offset);
218        state.offset = (x, y).into();
219        state.size = Some(scroll_buffer.area.as_size());
220        state.page_size = Some(area.into());
221        let visible_area = self
222            .render_scrollbars(area, buf, state, scroll_buffer)
223            .intersection(scroll_buffer.area);
224        self.render_visible_area(area, buf, visible_area, scroll_buffer);
225    }
226}