rat_widget/
view.rs

1//! A view allows scrolling of on or more widgets without builtin
2//! support for scrolling.
3//!
4//! ```rust
5//! # use rat_scrolled::Scroll;
6//! use rat_widget::paragraph::{Paragraph, ParagraphState};
7//! # use rat_widget::view::{View, ViewState};
8//! # use ratatui::prelude::*;
9//! #
10//! # let l2 = [Rect::ZERO, Rect::ZERO];
11//! # struct State {
12//! #      view: ViewState,
13//! #      first: ParagraphState,
14//! #  }
15//! # let mut state = State {
16//! #     view: Default::default(),
17//! #     first: Default::default(),
18//! # };
19//! # let mut buf = Buffer::default();
20//!
21//! ///
22//! /// Create the view and set the layout area
23//! /// for the buffer.
24//! ///
25//!
26//! let mut view_buf = View::new()
27//!     .layout(Rect::new(0, 0, 400, 400))
28//!     .vscroll(Scroll::new())
29//!     .hscroll(Scroll::new())
30//!     .into_buffer(l2[1], &mut state.view);
31//!
32//! ///
33//! /// Render the widgets to the view buffer.
34//! ///
35//! view_buf.render_stateful(
36//!     Paragraph::new("Paragraph\nParagraph\n..."),
37//!     Rect::new(0, 0, 40, 15),
38//!     &mut state.first,
39//! );
40//!
41//! ///
42//! /// Render the finished buffer.
43//! ///
44//! view_buf
45//!     .into_widget()
46//!     .render(l2[1], &mut buf, &mut state.view);
47//!
48//! ```
49
50use std::cmp::{max, min};
51
52use crate::_private::NonExhaustive;
53use crate::event::ScrollOutcome;
54use rat_event::{HandleEvent, MouseOnly, Outcome, Regular};
55use rat_reloc::RelocatableState;
56use rat_scrolled::{Scroll, ScrollArea, ScrollAreaState, ScrollState, ScrollStyle};
57use ratatui::buffer::Buffer;
58use ratatui::layout::{Position, Rect, Size};
59use ratatui::style::Style;
60use ratatui::widgets::Block;
61use ratatui::widgets::{StatefulWidget, Widget};
62
63/// Configure the view.
64#[derive(Debug, Default, Clone)]
65pub struct View<'a> {
66    layout: Rect,
67    view_size: Option<Size>,
68    style: Style,
69    block: Option<Block<'a>>,
70    hscroll: Option<Scroll<'a>>,
71    vscroll: Option<Scroll<'a>>,
72}
73
74/// Render to the temp buffer.
75///
76/// * It maps your widget area from layout coordinates
77///   to screen coordinates before rendering.
78/// * It helps with cleanup of the widget state if your
79///   widget is currently invisible.
80#[derive(Debug)]
81pub struct ViewBuffer<'a> {
82    // page layout
83    layout: Rect,
84
85    // Scroll offset into the view.
86    offset: Position,
87    buffer: Buffer,
88
89    // inner area that will finally be rendered.
90    widget_area: Rect,
91
92    style: Style,
93    block: Option<Block<'a>>,
94    hscroll: Option<Scroll<'a>>,
95    vscroll: Option<Scroll<'a>>,
96}
97
98/// Clips and copies the temp buffer to the frame buffer.
99#[derive(Debug)]
100pub struct ViewWidget<'a> {
101    // Scroll offset into the view.
102    offset: Position,
103    buffer: Buffer,
104
105    style: Style,
106    block: Option<Block<'a>>,
107    hscroll: Option<Scroll<'a>>,
108    vscroll: Option<Scroll<'a>>,
109}
110
111/// All styles for a view.
112#[derive(Debug)]
113pub struct ViewStyle {
114    pub style: Style,
115    pub block: Option<Block<'static>>,
116    pub scroll: Option<ScrollStyle>,
117    pub non_exhaustive: NonExhaustive,
118}
119
120/// View state.
121#[derive(Debug, Default, Clone)]
122pub struct ViewState {
123    /// Full area for the widget.
124    /// __read only__ renewed for each render.
125    pub area: Rect,
126    /// Area inside the border.
127    /// __read only__ renewed for each render.
128    pub widget_area: Rect,
129
130    /// The layout of the temp buffer uses.
131    /// __read only__ renewed for each render.
132    pub layout: Rect,
133
134    /// Horizontal scroll
135    /// __read+write__
136    pub hscroll: ScrollState,
137    /// Vertical scroll
138    /// __read+write__
139    pub vscroll: ScrollState,
140
141    /// For the buffer to survive render()
142    buffer: Option<Buffer>,
143}
144
145impl<'a> View<'a> {
146    /// New View.
147    pub fn new() -> Self {
148        Self::default()
149    }
150
151    /// Area for the temp buffer.
152    pub fn layout(mut self, area: Rect) -> Self {
153        self.layout = area;
154        self
155    }
156
157    /// Area used for the scrollbars. Maybe bigger then the layout area.
158    /// Uses the area if not set.
159    pub fn view_size(mut self, view: Size) -> Self {
160        self.view_size = Some(view);
161        self
162    }
163
164    /// Base style.
165    pub fn style(mut self, style: Style) -> Self {
166        self.style = style;
167        self.block = self.block.map(|v| v.style(style));
168        self
169    }
170
171    /// Block for border
172    pub fn block(mut self, block: Block<'a>) -> Self {
173        self.block = Some(block);
174        self
175    }
176
177    /// Scroll support.
178    pub fn scroll(mut self, scroll: Scroll<'a>) -> Self {
179        self.hscroll = Some(scroll.clone().override_horizontal());
180        self.vscroll = Some(scroll.override_vertical());
181        self
182    }
183
184    /// Horizontal scroll support.
185    pub fn hscroll(mut self, scroll: Scroll<'a>) -> Self {
186        self.hscroll = Some(scroll.override_horizontal());
187        self
188    }
189
190    /// Vertical scroll support.
191    pub fn vscroll(mut self, scroll: Scroll<'a>) -> Self {
192        self.vscroll = Some(scroll.override_vertical());
193        self
194    }
195
196    /// Combined style.
197    pub fn styles(mut self, styles: ViewStyle) -> Self {
198        self.style = styles.style;
199        if styles.block.is_some() {
200            self.block = styles.block;
201        }
202        if let Some(styles) = styles.scroll {
203            self.hscroll = self.hscroll.map(|v| v.styles(styles.clone()));
204            self.vscroll = self.vscroll.map(|v| v.styles(styles.clone()));
205        }
206        self.block = self.block.map(|v| v.style(styles.style));
207        self
208    }
209
210    /// Calculate the layout width.
211    pub fn layout_width(&self, area: Rect, state: &ViewState) -> u16 {
212        self.inner(area, state).width
213    }
214
215    /// Calculate the view area.
216    pub fn inner(&self, area: Rect, state: &ViewState) -> Rect {
217        let sa = ScrollArea::new()
218            .block(self.block.as_ref())
219            .h_scroll(self.hscroll.as_ref())
220            .v_scroll(self.vscroll.as_ref());
221        sa.inner(area, Some(&state.hscroll), Some(&state.vscroll))
222    }
223
224    /// Calculates the layout and creates a temporary buffer.
225    pub fn into_buffer(self, area: Rect, state: &mut ViewState) -> ViewBuffer<'a> {
226        state.area = area;
227        state.layout = self.layout;
228
229        let sa = ScrollArea::new()
230            .block(self.block.as_ref())
231            .h_scroll(self.hscroll.as_ref())
232            .v_scroll(self.vscroll.as_ref());
233        state.widget_area = sa.inner(area, Some(&state.hscroll), Some(&state.vscroll));
234
235        let max_x = if let Some(view_size) = self.view_size {
236            max(state.layout.right(), view_size.width)
237        } else {
238            state.layout.right()
239        };
240        let max_y = if let Some(view_size) = self.view_size {
241            max(state.layout.bottom(), view_size.height)
242        } else {
243            state.layout.bottom()
244        };
245
246        state
247            .hscroll
248            .set_max_offset(max_x.saturating_sub(state.widget_area.width) as usize);
249        state.hscroll.set_page_len(state.widget_area.width as usize);
250        state
251            .vscroll
252            .set_page_len(state.widget_area.height as usize);
253        state
254            .vscroll
255            .set_max_offset(max_y.saturating_sub(state.widget_area.height) as usize);
256
257        // offset is in layout coordinates.
258        // internal buffer starts at (view.x,view.y)
259        let offset = Position::new(state.hscroll.offset as u16, state.vscroll.offset as u16);
260
261        // resize buffer to fit the layout.
262        let buffer_area = state.layout;
263        let mut buffer = if let Some(mut buffer) = state.buffer.take() {
264            buffer.reset();
265            buffer.resize(buffer_area);
266            buffer
267        } else {
268            Buffer::empty(buffer_area)
269        };
270        buffer.set_style(buffer_area, self.style);
271
272        ViewBuffer {
273            layout: self.layout,
274            offset,
275            buffer,
276            widget_area: state.widget_area,
277            style: self.style,
278            block: self.block,
279            hscroll: self.hscroll,
280            vscroll: self.vscroll,
281        }
282    }
283}
284
285impl<'a> ViewBuffer<'a> {
286    /// Render a widget to the temp buffer.
287    #[inline(always)]
288    pub fn render_widget<W>(&mut self, widget: W, area: Rect)
289    where
290        W: Widget,
291    {
292        if area.intersects(self.buffer.area) {
293            // render the actual widget.
294            widget.render(area, self.buffer());
295        }
296    }
297
298    /// Render a widget to the temp buffer.
299    /// This expects that the state is a [RelocatableState].
300    #[inline(always)]
301    pub fn render_stateful<W, S>(&mut self, widget: W, area: Rect, state: &mut S)
302    where
303        W: StatefulWidget<State = S>,
304        S: RelocatableState,
305    {
306        if area.intersects(self.buffer.area) {
307            // render the actual widget.
308            widget.render(area, self.buffer(), state);
309            // shift and clip the output areas.
310            self.relocate(state);
311        } else {
312            self.hidden(state);
313        }
314    }
315
316    /// Return the buffer layout.
317    pub fn layout(&self) -> Rect {
318        self.layout
319    }
320
321    /// Is the given area visible?
322    pub fn is_visible_area(&self, area: Rect) -> bool {
323        area.intersects(self.buffer.area)
324    }
325
326    /// Calculate the necessary shift from view to screen.
327    pub fn shift(&self) -> (i16, i16) {
328        (
329            self.widget_area.x as i16 - self.offset.x as i16,
330            self.widget_area.y as i16 - self.offset.y as i16,
331        )
332    }
333
334    /// Does nothing for view.
335    /// Only exists to match [Clipper](crate::clipper::Clipper).
336    pub fn locate_area(&self, area: Rect) -> Rect {
337        area
338    }
339
340    /// After rendering the widget to the buffer it may have
341    /// stored areas in its state. These will be in buffer
342    /// coordinates instead of screen coordinates.
343    ///
344    /// Call this function to correct this after rendering.
345    pub fn relocate<S>(&self, state: &mut S)
346    where
347        S: RelocatableState,
348    {
349        state.relocate(self.shift(), self.widget_area);
350    }
351
352    /// If a widget is not rendered because it is out of
353    /// the buffer area, it may still have left over areas
354    /// in its state.
355    ///
356    /// This uses the mechanism for [relocate](Self::relocate) to zero them out.
357    pub fn hidden<S>(&self, state: &mut S)
358    where
359        S: RelocatableState,
360    {
361        state.relocate((0, 0), Rect::default())
362    }
363
364    /// Access the temporary buffer.
365    ///
366    /// __Note__
367    /// Use of render_widget is preferred.
368    pub fn buffer(&mut self) -> &mut Buffer {
369        &mut self.buffer
370    }
371
372    /// Rendering the content is finished.
373    ///
374    /// Convert to the output widget that can be rendered in the target area.
375    pub fn into_widget(self) -> ViewWidget<'a> {
376        ViewWidget {
377            block: self.block,
378            hscroll: self.hscroll,
379            vscroll: self.vscroll,
380            offset: self.offset,
381            buffer: self.buffer,
382            style: self.style,
383        }
384    }
385}
386
387impl StatefulWidget for ViewWidget<'_> {
388    type State = ViewState;
389
390    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
391        assert_eq!(area, state.area);
392
393        ScrollArea::new()
394            .style(self.style)
395            .block(self.block.as_ref())
396            .h_scroll(self.hscroll.as_ref())
397            .v_scroll(self.vscroll.as_ref())
398            .render(
399                area,
400                buf,
401                &mut ScrollAreaState::new()
402                    .h_scroll(&mut state.hscroll)
403                    .v_scroll(&mut state.vscroll),
404            );
405
406        let src_area = self.buffer.area;
407        let tgt_area = state.widget_area;
408        let offset = self.offset;
409
410        // extra offset due to buffer starts right of offset.
411        let off_x0 = src_area.x.saturating_sub(offset.x);
412        let off_y0 = src_area.y.saturating_sub(offset.y);
413        // cut source buffer due to start left of offset.
414        let cut_x0 = offset.x.saturating_sub(src_area.x);
415        let cut_y0 = offset.y.saturating_sub(src_area.y);
416
417        // length to copy
418        let len_src = src_area.width.saturating_sub(cut_x0);
419        let len_tgt = tgt_area.width.saturating_sub(off_x0);
420        let len = min(len_src, len_tgt);
421
422        // area height to copy
423        let height_src = src_area.height.saturating_sub(cut_y0);
424        let height_tgt = tgt_area.height.saturating_sub(off_y0);
425        let height = min(height_src, height_tgt);
426
427        // ** slow version **
428        // for y in 0..height {
429        //     for x in 0..len {
430        //         let src_pos = Position::new(src_area.x + cut_x0 + x, src_area.y + cut_y0 + y);
431        //         let src_cell = self.buffer.cell(src_pos).expect("src-cell");
432        //
433        //         let tgt_pos = Position::new(tgt_area.x + off_x0 + x, tgt_area.y + off_y0 + y);
434        //         let tgt_cell = buf.cell_mut(tgt_pos).expect("tgt_cell");
435        //
436        //         *tgt_cell = src_cell.clone();
437        //     }
438        // }
439
440        for y in 0..height {
441            let src_0 = self
442                .buffer
443                .index_of(src_area.x + cut_x0, src_area.y + cut_y0 + y);
444            let tgt_0 = buf.index_of(tgt_area.x + off_x0, tgt_area.y + off_y0 + y);
445
446            let src = &self.buffer.content[src_0..src_0 + len as usize];
447            let tgt = &mut buf.content[tgt_0..tgt_0 + len as usize];
448            tgt.clone_from_slice(src);
449        }
450
451        // keep buffer
452        state.buffer = Some(self.buffer);
453    }
454}
455
456impl Default for ViewStyle {
457    fn default() -> Self {
458        Self {
459            style: Default::default(),
460            block: None,
461            scroll: None,
462            non_exhaustive: NonExhaustive,
463        }
464    }
465}
466
467impl ViewState {
468    pub fn new() -> Self {
469        Self::default()
470    }
471
472    /// Show this rect.
473    pub fn show_area(&mut self, area: Rect) {
474        self.hscroll.scroll_to_pos(area.x as usize);
475        self.vscroll.scroll_to_pos(area.y as usize);
476    }
477}
478
479impl ViewState {
480    pub fn vertical_offset(&self) -> usize {
481        self.vscroll.offset()
482    }
483
484    pub fn set_vertical_offset(&mut self, offset: usize) -> bool {
485        let old = self.vscroll.offset();
486        self.vscroll.set_offset(offset);
487        old != self.vscroll.offset()
488    }
489
490    pub fn vertical_page_len(&self) -> usize {
491        self.vscroll.page_len()
492    }
493
494    pub fn horizontal_offset(&self) -> usize {
495        self.hscroll.offset()
496    }
497
498    pub fn set_horizontal_offset(&mut self, offset: usize) -> bool {
499        let old = self.hscroll.offset();
500        self.hscroll.set_offset(offset);
501        old != self.hscroll.offset()
502    }
503
504    pub fn horizontal_page_len(&self) -> usize {
505        self.hscroll.page_len()
506    }
507
508    pub fn horizontal_scroll_to(&mut self, pos: usize) -> bool {
509        self.hscroll.scroll_to_pos(pos)
510    }
511
512    pub fn vertical_scroll_to(&mut self, pos: usize) -> bool {
513        self.vscroll.scroll_to_pos(pos)
514    }
515
516    pub fn scroll_up(&mut self, delta: usize) -> bool {
517        self.vscroll.scroll_up(delta)
518    }
519
520    pub fn scroll_down(&mut self, delta: usize) -> bool {
521        self.vscroll.scroll_down(delta)
522    }
523
524    pub fn scroll_left(&mut self, delta: usize) -> bool {
525        self.hscroll.scroll_left(delta)
526    }
527
528    pub fn scroll_right(&mut self, delta: usize) -> bool {
529        self.hscroll.scroll_right(delta)
530    }
531}
532
533impl HandleEvent<crossterm::event::Event, Regular, Outcome> for ViewState {
534    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> Outcome {
535        self.handle(event, MouseOnly)
536    }
537}
538
539impl HandleEvent<crossterm::event::Event, MouseOnly, Outcome> for ViewState {
540    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> Outcome {
541        let mut sas = ScrollAreaState::new()
542            .area(self.widget_area)
543            .h_scroll(&mut self.hscroll)
544            .v_scroll(&mut self.vscroll);
545        match sas.handle(event, MouseOnly) {
546            ScrollOutcome::Up(v) => self.scroll_up(v).into(),
547            ScrollOutcome::Down(v) => self.scroll_down(v).into(),
548            ScrollOutcome::VPos(v) => self.set_vertical_offset(v).into(),
549            ScrollOutcome::Left(v) => self.scroll_left(v).into(),
550            ScrollOutcome::Right(v) => self.scroll_right(v).into(),
551            ScrollOutcome::HPos(v) => self.set_horizontal_offset(v).into(),
552            r => r.into(),
553        }
554    }
555}