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    #[deprecated(since = "1.2.0", note = "use render() instead")]
302    pub fn render_stateful<W, S>(&mut self, widget: W, area: Rect, state: &mut S)
303    where
304        W: StatefulWidget<State = S>,
305        S: RelocatableState,
306    {
307        self.render(widget, area, state);
308    }
309
310    /// Render a widget to the temp buffer.
311    /// This expects that the state is a [RelocatableState].
312    #[inline(always)]
313    pub fn render<W, S>(&mut self, widget: W, area: Rect, state: &mut S)
314    where
315        W: StatefulWidget<State = S>,
316        S: RelocatableState,
317    {
318        if area.intersects(self.buffer.area) {
319            // render the actual widget.
320            widget.render(area, self.buffer(), state);
321            // shift and clip the output areas.
322            self.relocate(state);
323        } else {
324            self.hidden(state);
325        }
326    }
327
328    /// Return the buffer layout.
329    pub fn layout(&self) -> Rect {
330        self.layout
331    }
332
333    /// Is the given area visible?
334    pub fn is_visible_area(&self, area: Rect) -> bool {
335        area.intersects(self.buffer.area)
336    }
337
338    /// Calculate the necessary shift from view to screen.
339    pub fn shift(&self) -> (i16, i16) {
340        (
341            self.widget_area.x as i16 - self.offset.x as i16,
342            self.widget_area.y as i16 - self.offset.y as i16,
343        )
344    }
345
346    /// Does nothing for view.
347    /// Only exists to match [Clipper](crate::clipper::Clipper).
348    pub fn locate_area(&self, area: Rect) -> Rect {
349        area
350    }
351
352    /// After rendering the widget to the buffer it may have
353    /// stored areas in its state. These will be in buffer
354    /// coordinates instead of screen coordinates.
355    ///
356    /// Call this function to correct this after rendering.
357    pub fn relocate<S>(&self, state: &mut S)
358    where
359        S: RelocatableState,
360    {
361        state.relocate(self.shift(), self.widget_area);
362    }
363
364    /// If a widget is not rendered because it is out of
365    /// the buffer area, it may still have left over areas
366    /// in its state.
367    ///
368    /// This uses the mechanism for [relocate](Self::relocate) to zero them out.
369    pub fn hidden<S>(&self, state: &mut S)
370    where
371        S: RelocatableState,
372    {
373        state.relocate((0, 0), Rect::default())
374    }
375
376    /// Access the temporary buffer.
377    ///
378    /// __Note__
379    /// Use of render_widget is preferred.
380    pub fn buffer(&mut self) -> &mut Buffer {
381        &mut self.buffer
382    }
383
384    /// Rendering the content is finished.
385    ///
386    /// Convert to the output widget that can be rendered in the target area.
387    pub fn into_widget(self) -> ViewWidget<'a> {
388        ViewWidget {
389            block: self.block,
390            hscroll: self.hscroll,
391            vscroll: self.vscroll,
392            offset: self.offset,
393            buffer: self.buffer,
394            style: self.style,
395        }
396    }
397}
398
399impl StatefulWidget for ViewWidget<'_> {
400    type State = ViewState;
401
402    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
403        assert_eq!(area, state.area);
404
405        ScrollArea::new()
406            .style(self.style)
407            .block(self.block.as_ref())
408            .h_scroll(self.hscroll.as_ref())
409            .v_scroll(self.vscroll.as_ref())
410            .render(
411                area,
412                buf,
413                &mut ScrollAreaState::new()
414                    .h_scroll(&mut state.hscroll)
415                    .v_scroll(&mut state.vscroll),
416            );
417
418        let src_area = self.buffer.area;
419        let tgt_area = state.widget_area;
420        let offset = self.offset;
421
422        // extra offset due to buffer starts right of offset.
423        let off_x0 = src_area.x.saturating_sub(offset.x);
424        let off_y0 = src_area.y.saturating_sub(offset.y);
425        // cut source buffer due to start left of offset.
426        let cut_x0 = offset.x.saturating_sub(src_area.x);
427        let cut_y0 = offset.y.saturating_sub(src_area.y);
428
429        // length to copy
430        let len_src = src_area.width.saturating_sub(cut_x0);
431        let len_tgt = tgt_area.width.saturating_sub(off_x0);
432        let len = min(len_src, len_tgt);
433
434        // area height to copy
435        let height_src = src_area.height.saturating_sub(cut_y0);
436        let height_tgt = tgt_area.height.saturating_sub(off_y0);
437        let height = min(height_src, height_tgt);
438
439        // ** slow version **
440        // for y in 0..height {
441        //     for x in 0..len {
442        //         let src_pos = Position::new(src_area.x + cut_x0 + x, src_area.y + cut_y0 + y);
443        //         let src_cell = self.buffer.cell(src_pos).expect("src-cell");
444        //
445        //         let tgt_pos = Position::new(tgt_area.x + off_x0 + x, tgt_area.y + off_y0 + y);
446        //         let tgt_cell = buf.cell_mut(tgt_pos).expect("tgt_cell");
447        //
448        //         *tgt_cell = src_cell.clone();
449        //     }
450        // }
451
452        for y in 0..height {
453            let src_0 = self
454                .buffer
455                .index_of(src_area.x + cut_x0, src_area.y + cut_y0 + y);
456            let tgt_0 = buf.index_of(tgt_area.x + off_x0, tgt_area.y + off_y0 + y);
457
458            let src = &self.buffer.content[src_0..src_0 + len as usize];
459            let tgt = &mut buf.content[tgt_0..tgt_0 + len as usize];
460            tgt.clone_from_slice(src);
461        }
462
463        // keep buffer
464        state.buffer = Some(self.buffer);
465    }
466}
467
468impl Default for ViewStyle {
469    fn default() -> Self {
470        Self {
471            style: Default::default(),
472            block: None,
473            scroll: None,
474            non_exhaustive: NonExhaustive,
475        }
476    }
477}
478
479impl ViewState {
480    pub fn new() -> Self {
481        Self::default()
482    }
483
484    /// Show this rect.
485    pub fn show_area(&mut self, area: Rect) {
486        self.hscroll.scroll_to_pos(area.x as usize);
487        self.vscroll.scroll_to_pos(area.y as usize);
488    }
489}
490
491impl ViewState {
492    pub fn vertical_offset(&self) -> usize {
493        self.vscroll.offset()
494    }
495
496    pub fn set_vertical_offset(&mut self, offset: usize) -> bool {
497        let old = self.vscroll.offset();
498        self.vscroll.set_offset(offset);
499        old != self.vscroll.offset()
500    }
501
502    pub fn vertical_page_len(&self) -> usize {
503        self.vscroll.page_len()
504    }
505
506    pub fn horizontal_offset(&self) -> usize {
507        self.hscroll.offset()
508    }
509
510    pub fn set_horizontal_offset(&mut self, offset: usize) -> bool {
511        let old = self.hscroll.offset();
512        self.hscroll.set_offset(offset);
513        old != self.hscroll.offset()
514    }
515
516    pub fn horizontal_page_len(&self) -> usize {
517        self.hscroll.page_len()
518    }
519
520    pub fn horizontal_scroll_to(&mut self, pos: usize) -> bool {
521        self.hscroll.scroll_to_pos(pos)
522    }
523
524    pub fn vertical_scroll_to(&mut self, pos: usize) -> bool {
525        self.vscroll.scroll_to_pos(pos)
526    }
527
528    pub fn scroll_up(&mut self, delta: usize) -> bool {
529        self.vscroll.scroll_up(delta)
530    }
531
532    pub fn scroll_down(&mut self, delta: usize) -> bool {
533        self.vscroll.scroll_down(delta)
534    }
535
536    pub fn scroll_left(&mut self, delta: usize) -> bool {
537        self.hscroll.scroll_left(delta)
538    }
539
540    pub fn scroll_right(&mut self, delta: usize) -> bool {
541        self.hscroll.scroll_right(delta)
542    }
543}
544
545impl HandleEvent<crossterm::event::Event, Regular, Outcome> for ViewState {
546    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> Outcome {
547        self.handle(event, MouseOnly)
548    }
549}
550
551impl HandleEvent<crossterm::event::Event, MouseOnly, Outcome> for ViewState {
552    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> Outcome {
553        let mut sas = ScrollAreaState::new()
554            .area(self.widget_area)
555            .h_scroll(&mut self.hscroll)
556            .v_scroll(&mut self.vscroll);
557        match sas.handle(event, MouseOnly) {
558            ScrollOutcome::Up(v) => self.scroll_up(v).into(),
559            ScrollOutcome::Down(v) => self.scroll_down(v).into(),
560            ScrollOutcome::VPos(v) => self.set_vertical_offset(v).into(),
561            ScrollOutcome::Left(v) => self.scroll_left(v).into(),
562            ScrollOutcome::Right(v) => self.scroll_right(v).into(),
563            ScrollOutcome::HPos(v) => self.set_horizontal_offset(v).into(),
564            r => r.into(),
565        }
566    }
567}