rat_scrolled/
scroll_area.rs

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