rat_scrolled/
scroll_area.rs

1use crate::event::ScrollOutcome;
2use crate::{Scroll, ScrollState, ScrollbarPolicy};
3use rat_event::{HandleEvent, MouseOnly, ct_event, flow};
4use ratatui::buffer::Buffer;
5use ratatui::layout::{Position, Rect};
6use ratatui::style::Style;
7use ratatui::widgets::{Block, Padding, ScrollbarOrientation, StatefulWidget, Widget};
8use std::cmp::max;
9
10/// Utility widget for rendering a combination of a Block and
11/// one or two Scroll(bars). Any of these can be None.
12#[derive(Debug, Default, Clone)]
13pub struct ScrollArea<'a> {
14    style: Style,
15    block: Option<&'a Block<'a>>,
16    h_scroll: Option<&'a Scroll<'a>>,
17    v_scroll: Option<&'a Scroll<'a>>,
18}
19
20/// Temporary state for ScrollArea.
21///
22/// This state is not meant to keep, it just repackages the
23/// widget state for use by ScrollArea.
24#[derive(Debug, Default)]
25pub struct ScrollAreaState<'a> {
26    /// This area is only used for event-handling.
27    /// Populate before calling the event-handler.
28    area: Rect,
29    /// Horizontal scroll state.
30    h_scroll: Option<&'a mut ScrollState>,
31    /// Vertical scroll state.
32    v_scroll: Option<&'a mut ScrollState>,
33}
34
35impl<'a> ScrollArea<'a> {
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Set the base style.
41    pub fn style(mut self, style: Style) -> Self {
42        self.style = style;
43        self
44    }
45
46    /// Sets the block.
47    pub fn block(mut self, block: Option<&'a Block<'a>>) -> Self {
48        self.block = block;
49        self
50    }
51
52    /// Sets the horizontal scroll.
53    pub fn h_scroll(mut self, scroll: Option<&'a Scroll<'a>>) -> Self {
54        self.h_scroll = scroll;
55        self
56    }
57
58    /// Sets the vertical scroll.
59    pub fn v_scroll(mut self, scroll: Option<&'a Scroll<'a>>) -> Self {
60        self.v_scroll = scroll;
61        self
62    }
63
64    /// What is the combined Padding.
65    pub fn padding(&self) -> Padding {
66        let mut padding = block_padding(&self.block);
67        if let Some(h_scroll) = self.h_scroll {
68            let scroll_pad = h_scroll.padding();
69            padding.top = max(padding.top, scroll_pad.top);
70            padding.bottom = max(padding.bottom, scroll_pad.bottom);
71        }
72        if let Some(v_scroll) = self.v_scroll {
73            let scroll_pad = v_scroll.padding();
74            padding.left = max(padding.left, scroll_pad.left);
75            padding.right = max(padding.right, scroll_pad.right);
76        }
77        padding
78    }
79
80    /// Calculate the size of the inner area.
81    pub fn inner(
82        &self,
83        area: Rect,
84        hscroll_state: Option<&ScrollState>,
85        vscroll_state: Option<&ScrollState>,
86    ) -> Rect {
87        layout(
88            self.block,
89            self.h_scroll,
90            self.v_scroll,
91            area,
92            hscroll_state,
93            vscroll_state,
94        )
95        .0
96    }
97}
98
99/// Get the padding the block imposes as Padding.
100fn block_padding(block: &Option<&Block<'_>>) -> Padding {
101    let area = Rect::new(0, 0, 20, 20);
102    let inner = if let Some(block) = block {
103        block.inner(area)
104    } else {
105        area
106    };
107    Padding {
108        left: inner.left() - area.left(),
109        right: area.right() - inner.right(),
110        top: inner.top() - area.top(),
111        bottom: area.bottom() - inner.bottom(),
112    }
113}
114
115impl<'a> StatefulWidget for ScrollArea<'a> {
116    type State = ScrollAreaState<'a>;
117
118    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
119        render_scroll_area(&self, area, buf, state);
120    }
121}
122
123impl<'a> StatefulWidget for &ScrollArea<'a> {
124    type State = ScrollAreaState<'a>;
125
126    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
127        render_scroll_area(self, area, buf, state);
128    }
129}
130
131fn render_scroll_area(
132    widget: &ScrollArea<'_>,
133    area: Rect,
134    buf: &mut Buffer,
135    state: &mut ScrollAreaState<'_>,
136) {
137    let (_, hscroll_area, vscroll_area) = layout(
138        widget.block,
139        widget.h_scroll,
140        widget.v_scroll,
141        area,
142        state.h_scroll.as_deref(),
143        state.v_scroll.as_deref(),
144    );
145
146    if let Some(block) = widget.block {
147        block.render(area, buf);
148    } else {
149        buf.set_style(area, widget.style);
150    }
151    if let Some(h) = widget.h_scroll {
152        if let Some(hstate) = &mut state.h_scroll {
153            h.render(hscroll_area, buf, hstate);
154        } else {
155            panic!("no horizontal scroll state");
156        }
157    }
158    if let Some(v) = widget.v_scroll {
159        if let Some(vstate) = &mut state.v_scroll {
160            v.render(vscroll_area, buf, vstate)
161        } else {
162            panic!("no vertical scroll state");
163        }
164    }
165}
166
167/// Calculate the layout for the given scrollbars.
168/// This prevents overlaps in the corners, if both scrollbars are
169/// visible, and tries to fit in the given block.
170///
171/// Returns (inner, h_area, v_area).
172///
173/// __Panic__
174///
175/// Panics if the orientation doesn't match,
176/// - h_scroll doesn't accept ScrollBarOrientation::Vertical* and
177/// - v_scroll doesn't accept ScrollBarOrientation::Horizontal*.
178///
179/// __Panic__
180///
181/// if the state doesn't contain the necessary scroll-states.
182fn layout<'a>(
183    block: Option<&Block<'a>>,
184    hscroll: Option<&Scroll<'a>>,
185    vscroll: Option<&Scroll<'a>>,
186    area: Rect,
187    hscroll_state: Option<&ScrollState>,
188    vscroll_state: Option<&ScrollState>,
189) -> (Rect, Rect, Rect) {
190    let mut inner = area;
191
192    if let Some(block) = block {
193        inner = block.inner(area);
194    }
195
196    if let Some(hscroll) = hscroll {
197        if let Some(hscroll_state) = hscroll_state {
198            let show = match hscroll.get_policy() {
199                ScrollbarPolicy::Always => true,
200                ScrollbarPolicy::Minimize => true,
201                ScrollbarPolicy::Collapse => hscroll_state.max_offset > 0,
202            };
203            if show {
204                match hscroll.get_orientation() {
205                    ScrollbarOrientation::VerticalRight => {
206                        unimplemented!(
207                            "ScrollbarOrientation::VerticalRight not supported for horizontal scrolling."
208                        );
209                    }
210                    ScrollbarOrientation::VerticalLeft => {
211                        unimplemented!(
212                            "ScrollbarOrientation::VerticalLeft not supported for horizontal scrolling."
213                        );
214                    }
215                    ScrollbarOrientation::HorizontalBottom => {
216                        if inner.bottom() == area.bottom() {
217                            inner.height = inner.height.saturating_sub(1);
218                        }
219                    }
220                    ScrollbarOrientation::HorizontalTop => {
221                        if inner.top() == area.top() {
222                            inner.y += 1;
223                            inner.height = inner.height.saturating_sub(1);
224                        }
225                    }
226                }
227            }
228        } else {
229            panic!("no horizontal scroll state");
230        }
231    }
232
233    if let Some(vscroll) = vscroll {
234        if let Some(vscroll_state) = vscroll_state {
235            let show = match vscroll.get_policy() {
236                ScrollbarPolicy::Always => true,
237                ScrollbarPolicy::Minimize => true,
238                ScrollbarPolicy::Collapse => vscroll_state.max_offset > 0,
239            };
240            if show {
241                match vscroll.get_orientation() {
242                    ScrollbarOrientation::VerticalRight => {
243                        if inner.right() == area.right() {
244                            inner.width = inner.width.saturating_sub(1);
245                        }
246                    }
247                    ScrollbarOrientation::VerticalLeft => {
248                        if inner.left() == area.left() {
249                            inner.x += 1;
250                            inner.width = inner.width.saturating_sub(1);
251                        }
252                    }
253                    ScrollbarOrientation::HorizontalBottom => {
254                        unimplemented!(
255                            "ScrollbarOrientation::HorizontalBottom not supported for vertical scrolling."
256                        );
257                    }
258                    ScrollbarOrientation::HorizontalTop => {
259                        unimplemented!(
260                            "ScrollbarOrientation::HorizontalTop not supported for vertical scrolling."
261                        );
262                    }
263                }
264            }
265        } else {
266            panic!("no horizontal scroll state");
267        }
268    }
269
270    // horizontal
271    let h_area = if let Some(hscroll) = hscroll {
272        if let Some(hscroll_state) = hscroll_state {
273            let show = match hscroll.get_policy() {
274                ScrollbarPolicy::Always => true,
275                ScrollbarPolicy::Minimize => true,
276                ScrollbarPolicy::Collapse => hscroll_state.max_offset > 0,
277            };
278            if show {
279                match hscroll.get_orientation() {
280                    ScrollbarOrientation::HorizontalBottom => Rect::new(
281                        inner.x + hscroll.get_start_margin(),
282                        area.bottom().saturating_sub(1),
283                        inner
284                            .width
285                            .saturating_sub(hscroll.get_start_margin() + hscroll.get_end_margin()),
286                        if area.height > 0 { 1 } else { 0 },
287                    ),
288                    ScrollbarOrientation::HorizontalTop => Rect::new(
289                        inner.x + hscroll.get_start_margin(),
290                        area.y,
291                        inner
292                            .width
293                            .saturating_sub(hscroll.get_start_margin() + hscroll.get_end_margin()),
294                        if area.height > 0 { 1 } else { 0 },
295                    ),
296                    _ => unreachable!(),
297                }
298            } else {
299                Rect::new(area.x, area.y, 0, 0)
300            }
301        } else {
302            panic!("no horizontal scroll state");
303        }
304    } else {
305        Rect::new(area.x, area.y, 0, 0)
306    };
307
308    // vertical
309    let v_area = if let Some(vscroll) = vscroll {
310        if let Some(vscroll_state) = vscroll_state {
311            let show = match vscroll.get_policy() {
312                ScrollbarPolicy::Always => true,
313                ScrollbarPolicy::Minimize => true,
314                ScrollbarPolicy::Collapse => vscroll_state.max_offset > 0,
315            };
316            if show {
317                match vscroll.get_orientation() {
318                    ScrollbarOrientation::VerticalRight => Rect::new(
319                        area.right().saturating_sub(1),
320                        inner.y + vscroll.get_start_margin(),
321                        if area.width > 0 { 1 } else { 0 },
322                        inner
323                            .height
324                            .saturating_sub(vscroll.get_start_margin() + vscroll.get_end_margin()),
325                    ),
326                    ScrollbarOrientation::VerticalLeft => Rect::new(
327                        area.x,
328                        inner.y + vscroll.get_start_margin(),
329                        if area.width > 0 { 1 } else { 0 },
330                        inner
331                            .height
332                            .saturating_sub(vscroll.get_start_margin() + vscroll.get_end_margin()),
333                    ),
334                    _ => unreachable!(),
335                }
336            } else {
337                Rect::new(area.x, area.y, 0, 0)
338            }
339        } else {
340            panic!("no horizontal scroll state");
341        }
342    } else {
343        Rect::new(area.x, area.y, 0, 0)
344    };
345
346    (inner, h_area, v_area)
347}
348
349impl<'a> ScrollAreaState<'a> {
350    pub fn new() -> Self {
351        Self::default()
352    }
353
354    pub fn area(mut self, area: Rect) -> Self {
355        self.area = area;
356        self
357    }
358
359    pub fn v_scroll(mut self, v_scroll: &'a mut ScrollState) -> Self {
360        self.v_scroll = Some(v_scroll);
361        self
362    }
363
364    pub fn v_scroll_opt(mut self, v_scroll: Option<&'a mut ScrollState>) -> Self {
365        self.v_scroll = v_scroll;
366        self
367    }
368
369    pub fn h_scroll(mut self, h_scroll: &'a mut ScrollState) -> Self {
370        self.h_scroll = Some(h_scroll);
371        self
372    }
373
374    pub fn h_scroll_opt(mut self, h_scroll: Option<&'a mut ScrollState>) -> Self {
375        self.h_scroll = h_scroll;
376        self
377    }
378}
379
380///
381/// Handle scrolling for the whole area spanned by the two scroll-states.
382///
383impl HandleEvent<crossterm::event::Event, MouseOnly, ScrollOutcome> for ScrollAreaState<'_> {
384    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> ScrollOutcome {
385        if let Some(h_scroll) = &mut self.h_scroll {
386            flow!(match event {
387                // right scroll with ALT down. shift doesn't work?
388                ct_event!(scroll ALT down for column, row) => {
389                    if self.area.contains(Position::new(*column, *row)) {
390                        ScrollOutcome::Right(h_scroll.scroll_by())
391                    } else {
392                        ScrollOutcome::Continue
393                    }
394                }
395                // left scroll with ALT up. shift doesn't work?
396                ct_event!(scroll ALT up for column, row) => {
397                    if self.area.contains(Position::new(*column, *row)) {
398                        ScrollOutcome::Left(h_scroll.scroll_by())
399                    } else {
400                        ScrollOutcome::Continue
401                    }
402                }
403                _ => ScrollOutcome::Continue,
404            });
405            flow!(h_scroll.handle(event, MouseOnly));
406        }
407        if let Some(v_scroll) = &mut self.v_scroll {
408            flow!(match event {
409                ct_event!(scroll down for column, row) => {
410                    if self.area.contains(Position::new(*column, *row)) {
411                        ScrollOutcome::Down(v_scroll.scroll_by())
412                    } else {
413                        ScrollOutcome::Continue
414                    }
415                }
416                ct_event!(scroll up for column, row) => {
417                    if self.area.contains(Position::new(*column, *row)) {
418                        ScrollOutcome::Up(v_scroll.scroll_by())
419                    } else {
420                        ScrollOutcome::Continue
421                    }
422                }
423                _ => ScrollOutcome::Continue,
424            });
425            flow!(v_scroll.handle(event, MouseOnly));
426        }
427
428        ScrollOutcome::Continue
429    }
430}