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