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(
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::{ConsumedEvent, HandleEvent, MouseOnly, Outcome, Regular, ct_event};
55use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
56use rat_reloc::RelocatableState;
57use rat_scrolled::{Scroll, ScrollArea, ScrollAreaState, ScrollState, ScrollStyle};
58use ratatui::buffer::Buffer;
59use ratatui::layout::{Position, Rect, Size};
60use ratatui::style::Style;
61use ratatui::widgets::Block;
62use ratatui::widgets::{StatefulWidget, Widget};
63
64/// Configure the view.
65#[derive(Debug, Default, Clone)]
66pub struct View<'a> {
67    layout: Rect,
68    view_size: Option<Size>,
69    style: Style,
70    block: Option<Block<'a>>,
71    hscroll: Option<Scroll<'a>>,
72    vscroll: Option<Scroll<'a>>,
73}
74
75/// Render to the temp buffer.
76///
77/// * It maps your widget area from layout coordinates
78///   to screen coordinates before rendering.
79/// * It helps with cleanup of the widget state if your
80///   widget is currently invisible.
81#[derive(Debug)]
82pub struct ViewBuffer<'a> {
83    // page layout
84    layout: Rect,
85
86    // Scroll offset into the view.
87    offset: Position,
88    buffer: Buffer,
89
90    // inner area that will finally be rendered.
91    widget_area: Rect,
92
93    style: Style,
94    block: Option<Block<'a>>,
95    hscroll: Option<Scroll<'a>>,
96    vscroll: Option<Scroll<'a>>,
97}
98
99/// Clips and copies the temp buffer to the frame buffer.
100#[derive(Debug)]
101pub struct ViewWidget<'a> {
102    // Scroll offset into the view.
103    offset: Position,
104    buffer: Buffer,
105
106    style: Style,
107    block: Option<Block<'a>>,
108    hscroll: Option<Scroll<'a>>,
109    vscroll: Option<Scroll<'a>>,
110}
111
112/// All styles for a view.
113#[derive(Debug)]
114pub struct ViewStyle {
115    pub style: Style,
116    pub block: Option<Block<'static>>,
117    pub scroll: Option<ScrollStyle>,
118    pub non_exhaustive: NonExhaustive,
119}
120
121/// View state.
122#[derive(Debug, Default, Clone)]
123pub struct ViewState {
124    /// Full area for the widget.
125    /// __read only__ renewed for each render.
126    pub area: Rect,
127    /// Area inside the border.
128    /// __read only__ renewed for each render.
129    pub widget_area: Rect,
130
131    /// The layout of the temp buffer uses.
132    /// __read only__ renewed for each render.
133    pub layout: Rect,
134
135    /// Horizontal scroll
136    /// __read+write__
137    pub hscroll: ScrollState,
138    /// Vertical scroll
139    /// __read+write__
140    pub vscroll: ScrollState,
141
142    /// Current focus state.
143    /// __read+write__
144    pub focus: FocusFlag,
145
146    /// For the buffer to survive render()
147    buffer: Option<Buffer>,
148}
149
150impl<'a> View<'a> {
151    /// New View.
152    pub fn new() -> Self {
153        Self::default()
154    }
155
156    /// Area for the temp buffer.
157    pub fn layout(mut self, area: Rect) -> Self {
158        self.layout = area;
159        self
160    }
161
162    /// Area used for the scrollbars. Maybe bigger then the layout area.
163    /// Uses the area if not set.
164    pub fn view_size(mut self, view: Size) -> Self {
165        self.view_size = Some(view);
166        self
167    }
168
169    /// Base style.
170    pub fn style(mut self, style: Style) -> Self {
171        self.style = style;
172        self.block = self.block.map(|v| v.style(style));
173        self
174    }
175
176    /// Block for border
177    pub fn block(mut self, block: Block<'a>) -> Self {
178        self.block = Some(block);
179        self
180    }
181
182    /// Scroll support.
183    pub fn scroll(mut self, scroll: Scroll<'a>) -> Self {
184        self.hscroll = Some(scroll.clone().override_horizontal());
185        self.vscroll = Some(scroll.override_vertical());
186        self
187    }
188
189    /// Horizontal scroll support.
190    pub fn hscroll(mut self, scroll: Scroll<'a>) -> Self {
191        self.hscroll = Some(scroll.override_horizontal());
192        self
193    }
194
195    /// Vertical scroll support.
196    pub fn vscroll(mut self, scroll: Scroll<'a>) -> Self {
197        self.vscroll = Some(scroll.override_vertical());
198        self
199    }
200
201    /// Combined style.
202    pub fn styles(mut self, styles: ViewStyle) -> Self {
203        self.style = styles.style;
204        if styles.block.is_some() {
205            self.block = styles.block;
206        }
207        if let Some(styles) = styles.scroll {
208            self.hscroll = self.hscroll.map(|v| v.styles(styles.clone()));
209            self.vscroll = self.vscroll.map(|v| v.styles(styles.clone()));
210        }
211        self.block = self.block.map(|v| v.style(styles.style));
212        self
213    }
214
215    /// Calculate the layout width.
216    pub fn layout_width(&self, area: Rect, state: &ViewState) -> u16 {
217        self.inner(area, state).width
218    }
219
220    /// Calculate the view area.
221    pub fn inner(&self, area: Rect, state: &ViewState) -> Rect {
222        let sa = ScrollArea::new()
223            .block(self.block.as_ref())
224            .h_scroll(self.hscroll.as_ref())
225            .v_scroll(self.vscroll.as_ref());
226        sa.inner(area, Some(&state.hscroll), Some(&state.vscroll))
227    }
228
229    /// Calculates the layout and creates a temporary buffer.
230    pub fn into_buffer(self, area: Rect, state: &mut ViewState) -> ViewBuffer<'a> {
231        state.area = area;
232        state.layout = self.layout;
233
234        let sa = ScrollArea::new()
235            .block(self.block.as_ref())
236            .h_scroll(self.hscroll.as_ref())
237            .v_scroll(self.vscroll.as_ref());
238        state.widget_area = sa.inner(area, Some(&state.hscroll), Some(&state.vscroll));
239
240        let max_x = if let Some(view_size) = self.view_size {
241            max(state.layout.right(), view_size.width)
242        } else {
243            state.layout.right()
244        };
245        let max_y = if let Some(view_size) = self.view_size {
246            max(state.layout.bottom(), view_size.height)
247        } else {
248            state.layout.bottom()
249        };
250
251        state
252            .hscroll
253            .set_max_offset(max_x.saturating_sub(state.widget_area.width) as usize);
254        state.hscroll.set_page_len(state.widget_area.width as usize);
255        state
256            .vscroll
257            .set_page_len(state.widget_area.height as usize);
258        state
259            .vscroll
260            .set_max_offset(max_y.saturating_sub(state.widget_area.height) as usize);
261
262        // offset is in layout coordinates.
263        // internal buffer starts at (view.x,view.y)
264        let offset = Position::new(state.hscroll.offset as u16, state.vscroll.offset as u16);
265
266        // resize buffer to fit the layout.
267        let buffer_area = state.layout;
268        let mut buffer = if let Some(mut buffer) = state.buffer.take() {
269            buffer.reset();
270            buffer.resize(buffer_area);
271            buffer
272        } else {
273            Buffer::empty(buffer_area)
274        };
275        buffer.set_style(buffer_area, self.style);
276
277        ViewBuffer {
278            layout: self.layout,
279            offset,
280            buffer,
281            widget_area: state.widget_area,
282            style: self.style,
283            block: self.block,
284            hscroll: self.hscroll,
285            vscroll: self.vscroll,
286        }
287    }
288}
289
290impl<'a> ViewBuffer<'a> {
291    /// Render a widget to the temp buffer.
292    #[inline(always)]
293    pub fn render_widget<W>(&mut self, widget: W, area: Rect)
294    where
295        W: Widget,
296    {
297        if area.intersects(self.buffer.area) {
298            // render the actual widget.
299            widget.render(area, self.buffer());
300        }
301    }
302
303    /// Render a widget to the temp buffer.
304    /// This expects that the state is a [RelocatableState].
305    #[inline(always)]
306    #[allow(deprecated)]
307    pub fn render<W, S>(&mut self, widget: W, area: Rect, state: &mut S)
308    where
309        W: StatefulWidget<State = S>,
310        S: RelocatableState,
311    {
312        if area.intersects(self.buffer.area) {
313            // render the actual widget.
314            widget.render(area, self.buffer(), state);
315            // shift and clip the output areas.
316            state.relocate(self.shift(), self.widget_area);
317        } else {
318            state.relocate_hidden();
319        }
320    }
321
322    /// Return the buffer layout.
323    pub fn layout(&self) -> Rect {
324        self.layout
325    }
326
327    /// Is this area inside the buffer area.
328    pub fn is_visible_area(&self, area: Rect) -> bool {
329        area.intersects(self.buffer.area)
330    }
331
332    /// Calculate the necessary shift from view to screen.
333    #[deprecated(
334        since = "2.0.0",
335        note = "should not be public. use relocate2() instead."
336    )]
337    pub fn shift(&self) -> (i16, i16) {
338        (
339            self.widget_area.x as i16 - self.offset.x as i16,
340            self.widget_area.y as i16 - self.offset.y as i16,
341        )
342    }
343
344    /// Does nothing for view.
345    /// Only exists to match [Clipper](crate::clipper::Clipper).
346    #[deprecated(
347        since = "2.0.0",
348        note = "wrong api, use is_visible_area() or locate_area2()"
349    )]
350    pub fn locate_area(&self, area: Rect) -> Rect {
351        area
352    }
353
354    /// Validates that this area is inside the buffer area.
355    #[deprecated(
356        since = "2.0.0",
357        note = "wrong api, use is_visible_area() or locate_area2()"
358    )]
359    pub fn locate_area2(&self, area: Rect) -> Option<Rect> {
360        if area.intersects(self.buffer.area) {
361            Some(area)
362        } else {
363            None
364        }
365    }
366
367    /// After rendering the widget to the buffer it may have
368    /// stored areas in its state. These will be in buffer
369    /// coordinates instead of screen coordinates.
370    ///
371    /// Call this function to correct this after rendering.
372    #[deprecated(since = "2.0.0", note = "wrong api, use relocate2() instead")]
373    #[allow(deprecated)]
374    pub fn relocate<S>(&self, state: &mut S)
375    where
376        S: RelocatableState,
377    {
378        state.relocate(self.shift(), self.widget_area);
379    }
380
381    /// After rendering the widget to the buffer it may have
382    /// stored areas in its state. These will be in buffer
383    /// coordinates instead of screen coordinates.
384    ///
385    /// Call this function to correct this after rendering.
386    ///
387    ///
388    ///
389    #[deprecated(since = "2.0.0", note = "wrong api, use relocate2() instead")]
390    #[allow(deprecated)]
391    pub fn relocate2<S>(&self, area: Rect, state: &mut S)
392    where
393        S: RelocatableState,
394    {
395        if self.is_visible_area(area) {
396            state.relocate(self.shift(), self.widget_area);
397        } else {
398            state.relocate_hidden();
399        }
400    }
401
402    /// If a widget is not rendered because it is out of
403    /// the buffer area, it may still have left over areas
404    /// in its state.
405    ///
406    /// This uses [relocate_hidden](RelocatableState::relocate_hidden) to zero them out.
407    #[deprecated(since = "2.0.0", note = "bad api, use relocate2() instead")]
408    pub fn hidden<S>(&self, state: &mut S)
409    where
410        S: RelocatableState,
411    {
412        state.relocate_hidden();
413    }
414
415    /// Access the temporary buffer.
416    ///
417    /// __Note__
418    /// Use of render_widget is preferred.
419    pub fn buffer(&mut self) -> &mut Buffer {
420        &mut self.buffer
421    }
422
423    /// Rendering the content is finished.
424    ///
425    /// Convert to the output widget that can be rendered in the target area.
426    pub fn into_widget(self) -> ViewWidget<'a> {
427        ViewWidget {
428            block: self.block,
429            hscroll: self.hscroll,
430            vscroll: self.vscroll,
431            offset: self.offset,
432            buffer: self.buffer,
433            style: self.style,
434        }
435    }
436}
437
438impl StatefulWidget for ViewWidget<'_> {
439    type State = ViewState;
440
441    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
442        assert_eq!(area, state.area);
443
444        ScrollArea::new()
445            .style(self.style)
446            .block(self.block.as_ref())
447            .h_scroll(self.hscroll.as_ref())
448            .v_scroll(self.vscroll.as_ref())
449            .render(
450                area,
451                buf,
452                &mut ScrollAreaState::new()
453                    .h_scroll(&mut state.hscroll)
454                    .v_scroll(&mut state.vscroll),
455            );
456
457        let src_area = self.buffer.area;
458        let tgt_area = state.widget_area;
459        let offset = self.offset;
460
461        // extra offset due to buffer starts right of offset.
462        let off_x0 = src_area.x.saturating_sub(offset.x);
463        let off_y0 = src_area.y.saturating_sub(offset.y);
464        // cut source buffer due to start left of offset.
465        let cut_x0 = offset.x.saturating_sub(src_area.x);
466        let cut_y0 = offset.y.saturating_sub(src_area.y);
467
468        // length to copy
469        let len_src = src_area.width.saturating_sub(cut_x0);
470        let len_tgt = tgt_area.width.saturating_sub(off_x0);
471        let len = min(len_src, len_tgt);
472
473        // area height to copy
474        let height_src = src_area.height.saturating_sub(cut_y0);
475        let height_tgt = tgt_area.height.saturating_sub(off_y0);
476        let height = min(height_src, height_tgt);
477
478        // ** slow version **
479        // for y in 0..height {
480        //     for x in 0..len {
481        //         let src_pos = Position::new(src_area.x + cut_x0 + x, src_area.y + cut_y0 + y);
482        //         let src_cell = self.buffer.cell(src_pos).expect("src-cell");
483        //
484        //         let tgt_pos = Position::new(tgt_area.x + off_x0 + x, tgt_area.y + off_y0 + y);
485        //         let tgt_cell = buf.cell_mut(tgt_pos).expect("tgt_cell");
486        //
487        //         *tgt_cell = src_cell.clone();
488        //     }
489        // }
490
491        for y in 0..height {
492            let src_0 = self
493                .buffer
494                .index_of(src_area.x + cut_x0, src_area.y + cut_y0 + y);
495            let tgt_0 = buf.index_of(tgt_area.x + off_x0, tgt_area.y + off_y0 + y);
496
497            let src = &self.buffer.content[src_0..src_0 + len as usize];
498            let tgt = &mut buf.content[tgt_0..tgt_0 + len as usize];
499            tgt.clone_from_slice(src);
500        }
501
502        // keep buffer
503        state.buffer = Some(self.buffer);
504    }
505}
506
507impl Default for ViewStyle {
508    fn default() -> Self {
509        Self {
510            style: Default::default(),
511            block: None,
512            scroll: None,
513            non_exhaustive: NonExhaustive,
514        }
515    }
516}
517
518impl HasFocus for ViewState {
519    fn build(&self, builder: &mut FocusBuilder) {
520        builder.leaf_widget(self);
521    }
522
523    fn focus(&self) -> FocusFlag {
524        self.focus.clone()
525    }
526
527    fn area(&self) -> Rect {
528        self.area
529    }
530}
531
532impl ViewState {
533    pub fn new() -> Self {
534        Self::default()
535    }
536
537    /// Show this rect.
538    pub fn show_area(&mut self, area: Rect) {
539        self.hscroll.scroll_to_pos(area.x as usize);
540        self.vscroll.scroll_to_pos(area.y as usize);
541    }
542}
543
544impl ViewState {
545    pub fn vertical_offset(&self) -> usize {
546        self.vscroll.offset()
547    }
548
549    pub fn set_vertical_offset(&mut self, offset: usize) -> bool {
550        let old = self.vscroll.offset();
551        self.vscroll.set_offset(offset);
552        old != self.vscroll.offset()
553    }
554
555    pub fn vertical_page_len(&self) -> usize {
556        self.vscroll.page_len()
557    }
558
559    pub fn horizontal_offset(&self) -> usize {
560        self.hscroll.offset()
561    }
562
563    pub fn set_horizontal_offset(&mut self, offset: usize) -> bool {
564        let old = self.hscroll.offset();
565        self.hscroll.set_offset(offset);
566        old != self.hscroll.offset()
567    }
568
569    pub fn horizontal_page_len(&self) -> usize {
570        self.hscroll.page_len()
571    }
572
573    pub fn horizontal_scroll_to(&mut self, pos: usize) -> bool {
574        self.hscroll.scroll_to_pos(pos)
575    }
576
577    pub fn vertical_scroll_to(&mut self, pos: usize) -> bool {
578        self.vscroll.scroll_to_pos(pos)
579    }
580
581    pub fn scroll_up(&mut self, delta: usize) -> bool {
582        self.vscroll.scroll_up(delta)
583    }
584
585    pub fn scroll_down(&mut self, delta: usize) -> bool {
586        self.vscroll.scroll_down(delta)
587    }
588
589    pub fn scroll_left(&mut self, delta: usize) -> bool {
590        self.hscroll.scroll_left(delta)
591    }
592
593    pub fn scroll_right(&mut self, delta: usize) -> bool {
594        self.hscroll.scroll_right(delta)
595    }
596}
597
598impl HandleEvent<crossterm::event::Event, Regular, Outcome> for ViewState {
599    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> Outcome {
600        let r = if self.is_focused() {
601            match event {
602                ct_event!(keycode press Left) => self.scroll_left(self.hscroll.scroll_by()).into(),
603                ct_event!(keycode press Right) => {
604                    self.scroll_right(self.hscroll.scroll_by()).into()
605                }
606                ct_event!(keycode press Up) => self.scroll_up(self.vscroll.scroll_by()).into(),
607                ct_event!(keycode press Down) => self.scroll_down(self.vscroll.scroll_by()).into(),
608
609                ct_event!(keycode press PageUp) => self.scroll_up(self.vscroll.page_len()).into(),
610                ct_event!(keycode press PageDown) => {
611                    self.scroll_down(self.vscroll.page_len()).into()
612                }
613                ct_event!(keycode press Home) => self.vertical_scroll_to(0).into(),
614                ct_event!(keycode press End) => {
615                    self.vertical_scroll_to(self.vscroll.max_offset()).into()
616                }
617
618                ct_event!(keycode press ALT-PageUp) => {
619                    self.scroll_left(self.hscroll.page_len()).into()
620                }
621                ct_event!(keycode press ALT-PageDown) => {
622                    self.scroll_right(self.hscroll.page_len()).into()
623                }
624                ct_event!(keycode press ALT-Home) => self.horizontal_scroll_to(0).into(),
625                ct_event!(keycode press ALT-End) => {
626                    self.horizontal_scroll_to(self.hscroll.max_offset()).into()
627                }
628                _ => Outcome::Continue,
629            }
630        } else {
631            Outcome::Continue
632        };
633
634        r.or_else(|| self.handle(event, MouseOnly))
635    }
636}
637
638impl HandleEvent<crossterm::event::Event, MouseOnly, Outcome> for ViewState {
639    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> Outcome {
640        let mut sas = ScrollAreaState::new()
641            .area(self.widget_area)
642            .h_scroll(&mut self.hscroll)
643            .v_scroll(&mut self.vscroll);
644        match sas.handle(event, MouseOnly) {
645            ScrollOutcome::Up(v) => self.scroll_up(v).into(),
646            ScrollOutcome::Down(v) => self.scroll_down(v).into(),
647            ScrollOutcome::VPos(v) => self.set_vertical_offset(v).into(),
648            ScrollOutcome::Left(v) => self.scroll_left(v).into(),
649            ScrollOutcome::Right(v) => self.scroll_right(v).into(),
650            ScrollOutcome::HPos(v) => self.set_horizontal_offset(v).into(),
651            r => r.into(),
652        }
653    }
654}
655
656/// Handle all events.
657/// Text events are only processed if focus is true.
658/// Mouse events are processed if they are in range.
659pub fn handle_events(
660    state: &mut ViewState,
661    focus: bool,
662    event: &crossterm::event::Event,
663) -> Outcome {
664    state.focus.set(focus);
665    HandleEvent::handle(state, event, Regular)
666}
667
668/// Handle only mouse-events.
669pub fn handle_mouse_events(state: &mut ViewState, event: &crossterm::event::Event) -> Outcome {
670    HandleEvent::handle(state, event, MouseOnly)
671}