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