rat_scrolled/
scroll_area.rs

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