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        self.render_block(area, buf);
120        self.render_scrollbars(area, buf, state);
121    }
122}
123
124impl<'a> StatefulWidget for &ScrollArea<'a> {
125    type State = ScrollAreaState<'a>;
126
127    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
128        self.render_block(area, buf);
129        self.render_scrollbars(area, buf, state);
130    }
131}
132
133impl<'a> ScrollArea<'a> {
134    /// Only render the Block.
135    pub fn render_block(&self, area: Rect, buf: &mut Buffer) {
136        if let Some(block) = self.block {
137            block.render(area, buf);
138        } else {
139            buf.set_style(area, self.style);
140        }
141    }
142
143    /// Only render the scrollbars.
144    pub fn render_scrollbars(&self, area: Rect, buf: &mut Buffer, state: &mut ScrollAreaState<'_>) {
145        let (_, hscroll_area, vscroll_area) = layout(
146            self.block,
147            self.h_scroll,
148            self.v_scroll,
149            area,
150            state.h_scroll.as_deref(),
151            state.v_scroll.as_deref(),
152        );
153
154        if let Some(h) = self.h_scroll {
155            if let Some(hstate) = &mut state.h_scroll {
156                h.render(hscroll_area, buf, hstate);
157            } else {
158                panic!("no horizontal scroll state");
159            }
160        }
161        if let Some(v) = self.v_scroll {
162            if let Some(vstate) = &mut state.v_scroll {
163                v.render(vscroll_area, buf, vstate)
164            } else {
165                panic!("no vertical scroll state");
166            }
167        }
168    }
169}
170
171/// Calculate the layout for the given scrollbars.
172/// This prevents overlaps in the corners, if both scrollbars are
173/// visible, and tries to fit in the given block.
174///
175/// Returns (inner, h_area, v_area).
176///
177/// __Panic__
178///
179/// Panics if the orientation doesn't match,
180/// - h_scroll doesn't accept ScrollBarOrientation::Vertical* and
181/// - v_scroll doesn't accept ScrollBarOrientation::Horizontal*.
182///
183/// __Panic__
184///
185/// if the state doesn't contain the necessary scroll-states.
186fn layout<'a>(
187    block: Option<&Block<'a>>,
188    hscroll: Option<&Scroll<'a>>,
189    vscroll: Option<&Scroll<'a>>,
190    area: Rect,
191    hscroll_state: Option<&ScrollState>,
192    vscroll_state: Option<&ScrollState>,
193) -> (Rect, Rect, Rect) {
194    let mut inner = area;
195
196    if let Some(block) = block {
197        inner = block.inner(area);
198    }
199
200    if let Some(hscroll) = hscroll {
201        let show = match hscroll.get_policy() {
202            ScrollbarPolicy::Always => true,
203            ScrollbarPolicy::Minimize => true,
204            ScrollbarPolicy::Collapse => {
205                if let Some(hscroll_state) = hscroll_state {
206                    hscroll_state.max_offset > 0
207                } else {
208                    true
209                }
210            }
211        };
212        if show {
213            match hscroll.get_orientation() {
214                ScrollbarOrientation::VerticalRight => {
215                    unimplemented!(
216                        "ScrollbarOrientation::VerticalRight not supported for horizontal scrolling."
217                    );
218                }
219                ScrollbarOrientation::VerticalLeft => {
220                    unimplemented!(
221                        "ScrollbarOrientation::VerticalLeft not supported for horizontal scrolling."
222                    );
223                }
224                ScrollbarOrientation::HorizontalBottom => {
225                    if inner.bottom() == area.bottom() {
226                        inner.height = inner.height.saturating_sub(1);
227                    }
228                }
229                ScrollbarOrientation::HorizontalTop => {
230                    if inner.top() == area.top() {
231                        inner.y += 1;
232                        inner.height = inner.height.saturating_sub(1);
233                    }
234                }
235            }
236        }
237    }
238
239    if let Some(vscroll) = vscroll {
240        let show = match vscroll.get_policy() {
241            ScrollbarPolicy::Always => true,
242            ScrollbarPolicy::Minimize => true,
243            ScrollbarPolicy::Collapse => {
244                if let Some(vscroll_state) = vscroll_state {
245                    vscroll_state.max_offset > 0
246                } else {
247                    true
248                }
249            }
250        };
251        if show {
252            match vscroll.get_orientation() {
253                ScrollbarOrientation::VerticalRight => {
254                    if inner.right() == area.right() {
255                        inner.width = inner.width.saturating_sub(1);
256                    }
257                }
258                ScrollbarOrientation::VerticalLeft => {
259                    if inner.left() == area.left() {
260                        inner.x += 1;
261                        inner.width = inner.width.saturating_sub(1);
262                    }
263                }
264                ScrollbarOrientation::HorizontalBottom => {
265                    unimplemented!(
266                        "ScrollbarOrientation::HorizontalBottom not supported for vertical scrolling."
267                    );
268                }
269                ScrollbarOrientation::HorizontalTop => {
270                    unimplemented!(
271                        "ScrollbarOrientation::HorizontalTop not supported for vertical scrolling."
272                    );
273                }
274            }
275        }
276    }
277
278    // horizontal
279    let h_area = if let Some(hscroll) = hscroll {
280        let show = match hscroll.get_policy() {
281            ScrollbarPolicy::Always => true,
282            ScrollbarPolicy::Minimize => true,
283            ScrollbarPolicy::Collapse => {
284                if let Some(hscroll_state) = hscroll_state {
285                    hscroll_state.max_offset > 0
286                } else {
287                    true
288                }
289            }
290        };
291        if show {
292            match hscroll.get_orientation() {
293                ScrollbarOrientation::HorizontalBottom => Rect::new(
294                    inner.x + hscroll.get_start_margin(),
295                    area.bottom().saturating_sub(1),
296                    inner
297                        .width
298                        .saturating_sub(hscroll.get_start_margin() + hscroll.get_end_margin()),
299                    if area.height > 0 { 1 } else { 0 },
300                ),
301                ScrollbarOrientation::HorizontalTop => Rect::new(
302                    inner.x + hscroll.get_start_margin(),
303                    area.y,
304                    inner
305                        .width
306                        .saturating_sub(hscroll.get_start_margin() + hscroll.get_end_margin()),
307                    if area.height > 0 { 1 } else { 0 },
308                ),
309                _ => unreachable!(),
310            }
311        } else {
312            Rect::new(area.x, area.y, 0, 0)
313        }
314    } else {
315        Rect::new(area.x, area.y, 0, 0)
316    };
317
318    // vertical
319    let v_area = if let Some(vscroll) = vscroll {
320        let show = match vscroll.get_policy() {
321            ScrollbarPolicy::Always => true,
322            ScrollbarPolicy::Minimize => true,
323            ScrollbarPolicy::Collapse => {
324                if let Some(vscroll_state) = vscroll_state {
325                    vscroll_state.max_offset > 0
326                } else {
327                    true
328                }
329            }
330        };
331        if show {
332            match vscroll.get_orientation() {
333                ScrollbarOrientation::VerticalRight => Rect::new(
334                    area.right().saturating_sub(1),
335                    inner.y + vscroll.get_start_margin(),
336                    if area.width > 0 { 1 } else { 0 },
337                    inner
338                        .height
339                        .saturating_sub(vscroll.get_start_margin() + vscroll.get_end_margin()),
340                ),
341                ScrollbarOrientation::VerticalLeft => Rect::new(
342                    area.x,
343                    inner.y + vscroll.get_start_margin(),
344                    if area.width > 0 { 1 } else { 0 },
345                    inner
346                        .height
347                        .saturating_sub(vscroll.get_start_margin() + vscroll.get_end_margin()),
348                ),
349                _ => unreachable!(),
350            }
351        } else {
352            Rect::new(area.x, area.y, 0, 0)
353        }
354    } else {
355        Rect::new(area.x, area.y, 0, 0)
356    };
357
358    (inner, h_area, v_area)
359}
360
361impl<'a> ScrollAreaState<'a> {
362    pub fn new() -> Self {
363        Self::default()
364    }
365
366    pub fn area(mut self, area: Rect) -> Self {
367        self.area = area;
368        self
369    }
370
371    pub fn v_scroll(mut self, v_scroll: &'a mut ScrollState) -> Self {
372        self.v_scroll = Some(v_scroll);
373        self
374    }
375
376    pub fn v_scroll_opt(mut self, v_scroll: Option<&'a mut ScrollState>) -> Self {
377        self.v_scroll = v_scroll;
378        self
379    }
380
381    pub fn h_scroll(mut self, h_scroll: &'a mut ScrollState) -> Self {
382        self.h_scroll = Some(h_scroll);
383        self
384    }
385
386    pub fn h_scroll_opt(mut self, h_scroll: Option<&'a mut ScrollState>) -> Self {
387        self.h_scroll = h_scroll;
388        self
389    }
390}
391
392///
393/// Handle scrolling for the whole area spanned by the two scroll-states.
394///
395impl HandleEvent<crossterm::event::Event, MouseOnly, ScrollOutcome> for ScrollAreaState<'_> {
396    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> ScrollOutcome {
397        if let Some(h_scroll) = &mut self.h_scroll {
398            flow!(match event {
399                // right scroll with ALT down. shift doesn't work?
400                ct_event!(scroll ALT down for column, row) => {
401                    if self.area.contains(Position::new(*column, *row)) {
402                        ScrollOutcome::Right(h_scroll.scroll_by())
403                    } else {
404                        ScrollOutcome::Continue
405                    }
406                }
407                // left scroll with ALT up. shift doesn't work?
408                ct_event!(scroll ALT up for column, row) => {
409                    if self.area.contains(Position::new(*column, *row)) {
410                        ScrollOutcome::Left(h_scroll.scroll_by())
411                    } else {
412                        ScrollOutcome::Continue
413                    }
414                }
415                _ => ScrollOutcome::Continue,
416            });
417            flow!(h_scroll.handle(event, MouseOnly));
418        }
419        if let Some(v_scroll) = &mut self.v_scroll {
420            flow!(match event {
421                ct_event!(scroll down for column, row) => {
422                    if self.area.contains(Position::new(*column, *row)) {
423                        ScrollOutcome::Down(v_scroll.scroll_by())
424                    } else {
425                        ScrollOutcome::Continue
426                    }
427                }
428                ct_event!(scroll up for column, row) => {
429                    if self.area.contains(Position::new(*column, *row)) {
430                        ScrollOutcome::Up(v_scroll.scroll_by())
431                    } else {
432                        ScrollOutcome::Continue
433                    }
434                }
435                _ => ScrollOutcome::Continue,
436            });
437            flow!(v_scroll.handle(event, MouseOnly));
438        }
439
440        ScrollOutcome::Continue
441    }
442}