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