Skip to main content

rat_widget/
clipper.rs

1//!
2//!
3//! Alternative View widget that renders only visible widgets.
4//!
5//! It uses a [GenericLayout] to find the visible widgets.
6//! It only uses a Buffer big enough to render these.
7//! They may be only partially visible of course.
8//!
9//! This helps with rendering speed and allows rendering more
10//! than u16::MAX lines.
11//!
12//! It works in several phases:
13//!
14//! ```rust no_run
15//!     # use rat_widget::clipper::{Clipper, ClipperState};
16//!     # use rat_widget::checkbox::{Checkbox, CheckboxState};
17//!     # use ratatui_core::layout::Rect;
18//!     # use ratatui_core::buffer::Buffer;
19//!     # use rat_focus::{FocusFlag, HasFocus};
20//!     # use rat_widget::layout::GenericLayout;
21//!     #
22//!     # let l2 = [Rect::ZERO, Rect::ZERO];
23//!     # struct State {
24//!     #      check_states: Vec<CheckboxState>,
25//!     #      clipper: ClipperState<FocusFlag>
26//!     #  }
27//!     # let mut state = State {
28//!     #      clipper: Default::default(),
29//!     #      check_states: Vec::default()
30//!     #  };
31//!     # let mut buf = Buffer::default();
32//!
33//!     /// Create the layout. The layout can be stored long-term
34//!     /// and needs to be rebuilt only if your widget layout changes.
35//!
36//!     let clipper = Clipper::new();
37//!     let layout_size = clipper.layout_size(l2[1], &mut state.clipper);
38//!
39//!     if !state.clipper.valid_layout(layout_size) {
40//!         let mut cl = GenericLayout::new();
41//!         for i in 0..100 {
42//!             cl.add(state.check_states[i].focus(),
43//!                 Rect::new(10, i as u16 *11, 15, 10),
44//!                 None,
45//!                 Rect::default()
46//!             );
47//!         }
48//!         state.clipper.set_layout(cl);
49//!     }
50//!
51//!     /// The given area plus the current scroll offset define the
52//!     /// view area. With the view area a temporary buffer is created
53//!     /// that is big enough to fit all widgets that are at least
54//!     /// partially visible.
55//!
56//!     let mut clip_buf = clipper
57//!         .into_buffer(l2[1], &mut state.clipper);
58//!
59//!     ///
60//!     /// The widgets are rendered to that buffer.
61//!     ///
62//!     for i in 0..100 {
63//!         // refer by handle
64//!         clip_buf.render(
65//!             state.check_states[i].focus(),
66//!             || {
67//!                 Checkbox::new()
68//!                 .text(format!("{:?}", i))
69//!             },
70//!             &mut state.check_states[i],
71//!         );
72//!     }
73//!
74//!     ///
75//!     /// The last step clips and copies the buffer to the frame buffer.
76//!     ///
77//!
78//!     clip_buf.finish(&mut buf, &mut state.clipper);
79//!
80//! ```
81//!
82//! __StatefulWidget__
83//!
84//! For this to work with StatefulWidgets they must cooperate
85//! by implementing the [RelocatableState]
86//! trait. With this trait the widget can clip/hide all areas that
87//! it stores in its state.
88//!
89//! __Form__
90//!
91//! There is an alternative to scrolling through long lists of widgets.
92//! With [Form](crate::form::Form) you can split the layout into pages.
93//! This avoids clipped widgets and allows the extra feature to stretch
94//! some widgets to fill the available vertical space.
95//!
96//! __See__
97//!
98//! [example](https://github.com/thscharler/rat-widget/blob/master/examples/clipper1.rs)
99//!
100
101use crate::_private::NonExhaustive;
102use crate::layout::GenericLayout;
103use rat_event::{ConsumedEvent, HandleEvent, MouseOnly, Outcome, Regular, ct_event};
104use rat_focus::{Focus, FocusBuilder, FocusFlag, HasFocus};
105use rat_reloc::RelocatableState;
106use rat_scrolled::event::ScrollOutcome;
107use rat_scrolled::{Scroll, ScrollArea, ScrollAreaState, ScrollState, ScrollStyle};
108use ratatui_core::buffer::Buffer;
109use ratatui_core::layout::{Alignment, Position, Rect, Size};
110use ratatui_core::style::Style;
111use ratatui_core::text::Line;
112use ratatui_core::widgets::StatefulWidget;
113use ratatui_core::widgets::Widget;
114use ratatui_crossterm::crossterm::event::Event;
115use ratatui_widgets::block::Block;
116use std::borrow::Cow;
117use std::cell::{Ref, RefCell};
118use std::cmp::{max, min};
119use std::hash::Hash;
120use std::mem;
121use std::rc::Rc;
122
123/// This widget allows rendering to a temporary buffer and clips
124/// it to size for the final rendering.
125#[derive(Debug)]
126pub struct Clipper<'a, W = usize>
127where
128    W: Eq + Clone + Hash,
129{
130    layout: Option<GenericLayout<W>>,
131    style: Style,
132    block: Option<Block<'a>>,
133    hscroll: Option<Scroll<'a>>,
134    vscroll: Option<Scroll<'a>>,
135    label_style: Option<Style>,
136    label_alignment: Option<Alignment>,
137    manual_label: bool,
138    buffer_uses_view_size: bool,
139}
140
141/// Second stage: render widgets to the temporary buffer.
142#[derive(Debug)]
143pub struct ClipperBuffer<'a, W>
144where
145    W: Eq + Clone + Hash,
146{
147    layout: Rc<RefCell<GenericLayout<W>>>,
148    manual_label: bool,
149
150    // offset from buffer to scroll area
151    offset: Position,
152    buffer: Buffer,
153
154    // inner area that will finally be rendered.
155    widget_area: Rect,
156
157    style: Style,
158    block: Option<Block<'a>>,
159    hscroll: Option<Scroll<'a>>,
160    vscroll: Option<Scroll<'a>>,
161    label_style: Option<Style>,
162    label_alignment: Option<Alignment>,
163
164    destruct: bool,
165}
166
167/// Clipper styles.
168#[derive(Debug, Clone)]
169pub struct ClipperStyle {
170    pub style: Style,
171    pub block: Option<Block<'static>>,
172    pub border_style: Option<Style>,
173    pub title_style: Option<Style>,
174    pub scroll: Option<ScrollStyle>,
175    pub label_style: Option<Style>,
176    pub label_alignment: Option<Alignment>,
177    pub non_exhaustive: NonExhaustive,
178}
179
180impl Default for ClipperStyle {
181    fn default() -> Self {
182        Self {
183            style: Default::default(),
184            block: Default::default(),
185            border_style: Default::default(),
186            title_style: Default::default(),
187            scroll: Default::default(),
188            label_style: Default::default(),
189            label_alignment: Default::default(),
190            non_exhaustive: NonExhaustive,
191        }
192    }
193}
194
195/// Widget state.
196#[derive(Debug)]
197pub struct ClipperState<W = usize>
198where
199    W: Eq + Clone + Hash,
200{
201    // Full area for the widget.
202    /// __read only__ renewed for each render.
203    pub area: Rect,
204    /// Area inside the border.
205    /// __read only__ renewed for each render.
206    pub widget_area: Rect,
207
208    /// Page layout.
209    /// __read only__ renewed for each render.
210    pub layout: Rc<RefCell<GenericLayout<W>>>,
211
212    /// Horizontal scroll
213    /// __read+write__
214    pub hscroll: ScrollState,
215    /// Vertical scroll
216    /// __read+write__
217    pub vscroll: ScrollState,
218
219    /// This widget has no focus of its own, but this flag
220    /// can be used to set a container state.
221    pub container: FocusFlag,
222
223    /// For the buffer to survive render()
224    buffer: Option<Buffer>,
225
226    /// Only construct with `..Default::default()`.
227    pub non_exhaustive: NonExhaustive,
228}
229
230impl<W> Clone for Clipper<'_, W>
231where
232    W: Eq + Clone + Hash,
233{
234    fn clone(&self) -> Self {
235        Self {
236            style: Default::default(),
237            block: self.block.clone(),
238            layout: self.layout.clone(),
239            hscroll: self.hscroll.clone(),
240            vscroll: self.vscroll.clone(),
241            label_style: self.label_style.clone(),
242            label_alignment: self.label_alignment.clone(),
243            manual_label: self.manual_label,
244            buffer_uses_view_size: self.buffer_uses_view_size,
245        }
246    }
247}
248
249impl<W> Default for Clipper<'_, W>
250where
251    W: Eq + Clone + Hash,
252{
253    fn default() -> Self {
254        Self {
255            style: Default::default(),
256            block: Default::default(),
257            layout: Default::default(),
258            hscroll: Default::default(),
259            vscroll: Default::default(),
260            label_style: Default::default(),
261            label_alignment: Default::default(),
262            manual_label: Default::default(),
263            buffer_uses_view_size: Default::default(),
264        }
265    }
266}
267
268impl<'a, W> Clipper<'a, W>
269where
270    W: Eq + Clone + Hash,
271{
272    /// New Clipper.
273    pub fn new() -> Self {
274        Self::default()
275    }
276
277    /// Set the layout. If no layout is set here the layout is
278    /// taken from the state.
279    pub fn layout(mut self, layout: GenericLayout<W>) -> Self {
280        self.layout = Some(layout);
281        self
282    }
283
284    /// Base style.
285    pub fn style(mut self, style: Style) -> Self {
286        self.style = style;
287        self.block = self.block.map(|v| v.style(style));
288        self
289    }
290
291    /// Render the label automatically when rendering the widget.
292    ///
293    /// Default: true
294    pub fn auto_label(mut self, auto: bool) -> Self {
295        self.manual_label = !auto;
296        self
297    }
298
299    /// Widget labels.
300    pub fn label_style(mut self, style: Style) -> Self {
301        self.label_style = Some(style);
302        self
303    }
304
305    /// Widget labels.
306    pub fn label_alignment(mut self, alignment: Alignment) -> Self {
307        self.label_alignment = Some(alignment);
308        self
309    }
310
311    /// Block for border
312    pub fn block(mut self, block: Block<'a>) -> Self {
313        self.block = Some(block);
314        self
315    }
316
317    /// Scroll support.
318    pub fn scroll(mut self, scroll: Scroll<'a>) -> Self {
319        self.hscroll = Some(scroll.clone().override_horizontal());
320        self.vscroll = Some(scroll.override_vertical());
321        self
322    }
323
324    /// Horizontal scroll support.
325    pub fn hscroll(mut self, scroll: Scroll<'a>) -> Self {
326        self.hscroll = Some(scroll.override_horizontal());
327        self
328    }
329
330    /// Vertical scroll support.
331    pub fn vscroll(mut self, scroll: Scroll<'a>) -> Self {
332        self.vscroll = Some(scroll.override_vertical());
333        self
334    }
335
336    /// Combined style.
337    pub fn styles(mut self, styles: ClipperStyle) -> Self {
338        self.style = styles.style;
339        if styles.block.is_some() {
340            self.block = styles.block;
341        }
342        if let Some(border_style) = styles.border_style {
343            self.block = self.block.map(|v| v.border_style(border_style));
344        }
345        if let Some(title_style) = styles.title_style {
346            self.block = self.block.map(|v| v.title_style(title_style));
347        }
348        self.block = self.block.map(|v| v.style(self.style));
349        if styles.label_style.is_some() {
350            self.label_style = styles.label_style;
351        }
352        if styles.label_alignment.is_some() {
353            self.label_alignment = styles.label_alignment;
354        }
355        if let Some(styles) = styles.scroll {
356            self.hscroll = self.hscroll.map(|v| v.styles(styles.clone()));
357            self.vscroll = self.vscroll.map(|v| v.styles(styles.clone()));
358        }
359        self.block = self.block.map(|v| v.style(styles.style));
360        self
361    }
362
363    /// By default, the buffer is sized according to the maximum extend
364    /// of the visible widgets.
365    ///
366    /// This may not be enough, when you use popups.
367    ///
368    /// This flag indicates, that the buffer should be at least the size
369    /// of the rendered view area.
370    pub fn buffer_uses_view_size(mut self) -> Self {
371        self.buffer_uses_view_size = true;
372        self
373    }
374
375    /// Calculate the layout size.
376    /// Returns the size of the inner area that is available
377    /// for drawing widgets.
378    pub fn layout_size(&self, area: Rect, state: &ClipperState<W>) -> Size {
379        let width = self.inner(area, state).width;
380        Size::new(width, u16::MAX)
381    }
382
383    /// Calculate the view area.
384    fn inner(&self, area: Rect, state: &ClipperState<W>) -> Rect {
385        let sa = ScrollArea::new()
386            .block(self.block.as_ref())
387            .h_scroll(self.hscroll.as_ref())
388            .v_scroll(self.vscroll.as_ref());
389        sa.inner(area, Some(&state.hscroll), Some(&state.vscroll))
390    }
391
392    fn calc_layout(&self, area: Rect, state: &mut ClipperState<W>) -> (Rect, Position) {
393        let layout = state.layout.borrow();
394
395        let view = Rect::new(
396            state.hscroll.offset() as u16,
397            state.vscroll.offset() as u16,
398            area.width,
399            area.height,
400        );
401
402        // maxima for scroll bar max
403        let mut max_pos = Position::default();
404
405        // find the bounding box for the buffer.
406        // convex hull of all visible widgets/labels/blocks.
407        let mut ext_view: Option<Rect> = None;
408        for idx in 0..layout.widget_len() {
409            let area = layout.widget(idx);
410            let label_area = layout.label(idx);
411
412            if view.intersects(area) || view.intersects(label_area) {
413                if !area.is_empty() {
414                    ext_view = ext_view //
415                        .map(|v| v.union(area))
416                        .or(Some(area));
417                }
418                if !label_area.is_empty() {
419                    ext_view = ext_view //
420                        .map(|v| v.union(label_area))
421                        .or(Some(label_area));
422                }
423            }
424
425            max_pos.x = max(max_pos.x, area.right());
426            max_pos.y = max(max_pos.y, area.bottom());
427            max_pos.x = max(max_pos.x, label_area.right());
428            max_pos.y = max(max_pos.y, label_area.bottom());
429        }
430        for idx in 0..layout.block_len() {
431            let block_area = layout.block_area(idx);
432            if view.intersects(block_area) {
433                ext_view = ext_view //
434                    .map(|v| v.union(block_area))
435                    .or(Some(block_area));
436            }
437
438            max_pos.x = max(max_pos.x, block_area.right());
439            max_pos.y = max(max_pos.y, block_area.bottom());
440        }
441
442        let ext_view = ext_view.unwrap_or(view);
443
444        (ext_view, max_pos)
445    }
446
447    /// Calculates the layout and creates a temporary buffer.
448    pub fn into_buffer(mut self, area: Rect, state: &mut ClipperState<W>) -> ClipperBuffer<'a, W> {
449        state.area = area;
450        if let Some(layout) = self.layout.take() {
451            state.layout = Rc::new(RefCell::new(layout));
452        }
453
454        let sa = ScrollArea::new()
455            .block(self.block.as_ref())
456            .h_scroll(self.hscroll.as_ref())
457            .v_scroll(self.vscroll.as_ref());
458        state.widget_area = sa.inner(area, Some(&state.hscroll), Some(&state.vscroll));
459
460        // run the layout
461        let (mut ext_area, max_pos) = self.calc_layout(area, state);
462
463        if self.buffer_uses_view_size {
464            ext_area = ext_area.union(area)
465        }
466
467        // adjust scroll
468        state
469            .vscroll
470            .set_page_len(state.widget_area.height as usize);
471        state
472            .vscroll
473            .set_max_offset(max_pos.y.saturating_sub(state.widget_area.height) as usize);
474        state.hscroll.set_page_len(state.widget_area.width as usize);
475        state
476            .hscroll
477            .set_max_offset(max_pos.x.saturating_sub(state.widget_area.width) as usize);
478
479        let offset = Position::new(state.hscroll.offset as u16, state.vscroll.offset as u16);
480
481        // resize buffer to fit all visible widgets.
482        let buffer_area = ext_area;
483        // resize buffer to fit the layout.
484        let mut buffer = if let Some(mut buffer) = state.buffer.take() {
485            buffer.reset();
486            buffer.resize(buffer_area);
487            buffer
488        } else {
489            Buffer::empty(buffer_area)
490        };
491        buffer.set_style(buffer_area, self.style);
492
493        ClipperBuffer {
494            layout: state.layout.clone(),
495            manual_label: self.manual_label,
496            offset,
497            buffer,
498            widget_area: state.widget_area,
499            style: self.style,
500            block: self.block,
501            hscroll: self.hscroll,
502            vscroll: self.vscroll,
503            label_style: self.label_style,
504            label_alignment: self.label_alignment,
505            destruct: false,
506        }
507    }
508}
509
510impl<'a, W> Drop for ClipperBuffer<'a, W>
511where
512    W: Eq + Hash + Clone,
513{
514    fn drop(&mut self) {
515        if !self.destruct {
516            panic!("ClipperBuffer: Must be used. Call finish(..)");
517        }
518    }
519}
520
521impl<'a, W> ClipperBuffer<'a, W>
522where
523    W: Eq + Hash + Clone,
524{
525    /// Is the widget visible.
526    pub fn is_visible(&self, widget: W) -> bool {
527        let layout = self.layout.borrow();
528        let Some(idx) = layout.try_index_of(widget) else {
529            return false;
530        };
531        let area = layout.widget(idx);
532        self.buffer.area.intersects(area)
533    }
534
535    /// Render the label with the set style and alignment.
536    #[inline(always)]
537    fn render_auto_label(&mut self, idx: usize) -> bool {
538        let layout = self.layout.borrow();
539        let Some(label_area) = self.locate_area(layout.label(idx)) else {
540            return false;
541        };
542        let Some(label_str) = layout.try_label_str(idx) else {
543            return false;
544        };
545
546        let mut label = Line::from(label_str.as_ref());
547        if let Some(style) = self.label_style {
548            label = label.style(style)
549        };
550        if let Some(align) = self.label_alignment {
551            label = label.alignment(align);
552        }
553        label.render(label_area, &mut self.buffer);
554
555        true
556    }
557
558    /// Render all visible blocks.
559    fn render_block(&mut self) {
560        let layout = self.layout.borrow();
561        for (idx, block_area) in layout.block_area_iter().enumerate() {
562            if let Some(block_area) = self.locate_area(*block_area) {
563                if let Some(block) = layout.block(idx) {
564                    block.render(block_area, &mut self.buffer);
565                }
566            }
567        }
568    }
569
570    // q: why is this a render instead of returning a Widget??
571    // a: lifetime of `&Option<Cow<'static, str>>`. Can't use
572    //    this as part of a return value.
573    /// Render the label for the given widget.
574    #[inline(always)]
575    pub fn render_label<FN>(&mut self, widget: W, render_fn: FN) -> bool
576    where
577        FN: FnOnce(&Option<Cow<'static, str>>, Rect, &mut Buffer),
578    {
579        let layout = self.layout.borrow();
580        let Some(idx) = layout.try_index_of(widget) else {
581            return false;
582        };
583        let Some(label_area) = self.locate_area(layout.label(idx)) else {
584            return false;
585        };
586        let label_str = layout.try_label_str(idx);
587        render_fn(label_str, label_area, &mut self.buffer);
588        true
589    }
590
591    /// Render a stateless widget and its label.
592    #[inline(always)]
593    pub fn render_widget<FN, WW>(&mut self, widget: W, render_fn: FN) -> bool
594    where
595        FN: FnOnce() -> WW,
596        WW: Widget,
597    {
598        let Some(idx) = self.layout.borrow().try_index_of(widget) else {
599            return false;
600        };
601        if !self.manual_label {
602            self.render_auto_label(idx);
603        }
604        let Some(widget_area) = self.locate_area(self.layout.borrow().widget(idx)) else {
605            return false;
606        };
607        render_fn().render(widget_area, &mut self.buffer);
608        true
609    }
610
611    /// Render a stateful widget and its label.
612    #[inline(always)]
613    pub fn render<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
614    where
615        FN: FnOnce() -> WW,
616        WW: StatefulWidget<State = SS>,
617        SS: RelocatableState,
618    {
619        let Some(idx) = self.layout.borrow().try_index_of(widget) else {
620            return false;
621        };
622        if !self.manual_label {
623            self.render_auto_label(idx);
624        }
625        let Some(widget_area) = self.locate_area(self.layout.borrow().widget(idx)) else {
626            state.relocate_hidden();
627            return false;
628        };
629        render_fn().render(widget_area, &mut self.buffer, state);
630        state.relocate(self.shift(), self.widget_area);
631        true
632    }
633
634    /// Render a stateful widget and its label.
635    ///
636    /// This expects a pair of StatefulWidgets, of which the first
637    /// will be rendered and the second will be returned.
638    ///
639    /// This is for rendering widgets that split in two parts,
640    /// the main widget and a popup. The popup must be rendered later
641    /// to be 'above' all other widgets.
642    /// Use [render_popup](ClipperBuffer::render_popup) for this.
643    #[inline(always)]
644    pub fn render2<FN, WW, SS, R>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> Option<R>
645    where
646        FN: FnOnce() -> (WW, R),
647        WW: StatefulWidget<State = SS>,
648        SS: RelocatableState,
649    {
650        let Some(idx) = self.layout.borrow().try_index_of(widget) else {
651            return None;
652        };
653        if !self.manual_label {
654            self.render_auto_label(idx);
655        }
656        let Some(widget_area) = self.locate_area(self.layout.borrow().widget(idx)) else {
657            state.relocate_hidden();
658            return None;
659        };
660        let (widget, remainder) = render_fn();
661        widget.render(widget_area, &mut self.buffer, state);
662        state.relocate(self.shift(), self.widget_area);
663
664        Some(remainder)
665    }
666
667    /// Render an additional popup widget for the given main widget.
668    #[inline(always)]
669    pub fn render_popup<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
670    where
671        FN: FnOnce() -> Option<WW>,
672        WW: StatefulWidget<State = SS>,
673        SS: RelocatableState,
674    {
675        let Some(idx) = self.layout.borrow().try_index_of(widget) else {
676            return false;
677        };
678        let Some(widget_area) = self.locate_area(self.layout.borrow().widget(idx)) else {
679            state.relocate_popup_hidden();
680            return false;
681        };
682        let widget = render_fn();
683        if let Some(widget) = widget {
684            widget.render(widget_area, &mut self.buffer, state);
685            state.relocate_popup(self.shift(), self.widget_area);
686            true
687        } else {
688            state.relocate_popup_hidden();
689            false
690        }
691    }
692
693    /// Get the buffer coordinates for the given widget.
694    #[inline]
695    pub fn locate_widget(&self, widget: W) -> Option<Rect> {
696        let layout = self.layout.borrow();
697        let Some(idx) = layout.try_index_of(widget) else {
698            return None;
699        };
700        self.locate_area(layout.widget(idx))
701    }
702
703    /// Get the buffer coordinates for the label of the given widget.
704    #[inline]
705    #[allow(clippy::question_mark)]
706    pub fn locate_label(&self, widget: W) -> Option<Rect> {
707        let layout = self.layout.borrow();
708        let Some(idx) = layout.try_index_of(widget) else {
709            return None;
710        };
711        self.locate_area(layout.label(idx))
712    }
713
714    /// Relocate the area from layout coordinates to buffer coordinates,
715    /// which is a noop as those are aligned.
716    ///
717    /// But this will return None if the given area is outside the buffer.
718    #[inline]
719    pub fn locate_area(&self, area: Rect) -> Option<Rect> {
720        if self.buffer.area.intersects(area) {
721            Some(area)
722        } else {
723            None
724        }
725    }
726
727    /// Calculate the necessary shift from layout to screen.
728    fn shift(&self) -> (i16, i16) {
729        (
730            self.widget_area.x as i16 - self.offset.x as i16,
731            self.widget_area.y as i16 - self.offset.y as i16,
732        )
733    }
734
735    /// After rendering the widget to the buffer it may have
736    /// stored areas in its state. These will be in buffer
737    /// coordinates instead of screen coordinates.
738    ///
739    /// Call this function to correct this after rendering.
740    ///
741    /// __Note__:
742    ///
743    /// This is only necessary if you do some manual rendering
744    /// of stateful widgets. If you use [render](ClipperBuffer::render) this will
745    /// happen automatically
746    ///
747    /// __Parameter__:
748    ///
749    /// widget: The visibility of this widget will determine
750    /// if the areas in state are shifted or hidden altogether.
751    pub fn relocate<S>(&self, widget: W, state: &mut S)
752    where
753        S: RelocatableState,
754    {
755        let Some(idx) = self.layout.borrow().try_index_of(widget) else {
756            return;
757        };
758        if self.locate_area(self.layout.borrow().widget(idx)).is_some() {
759            state.relocate(self.shift(), self.widget_area);
760        } else {
761            state.relocate_hidden();
762        };
763    }
764
765    /// Return a reference to the buffer.
766    #[inline]
767    pub fn buffer(&mut self) -> &mut Buffer {
768        &mut self.buffer
769    }
770
771    pub fn finish(mut self, tgt_buf: &mut Buffer, state: &mut ClipperState<W>) {
772        self.destruct = true;
773
774        self.render_block();
775
776        ScrollArea::new()
777            .style(self.style)
778            .block(self.block.as_ref())
779            .h_scroll(self.hscroll.as_ref())
780            .v_scroll(self.vscroll.as_ref())
781            .render(
782                state.area,
783                tgt_buf,
784                &mut ScrollAreaState::new()
785                    .h_scroll(&mut state.hscroll)
786                    .v_scroll(&mut state.vscroll),
787            );
788
789        let src_area = self.buffer.area;
790        let tgt_area = state.widget_area;
791        let offset = self.offset;
792
793        // extra offset due to buffer starts right of offset.
794        let off_x0 = src_area.x.saturating_sub(offset.x);
795        let off_y0 = src_area.y.saturating_sub(offset.y);
796        // cut source buffer due to start left of offset.
797        let cut_x0 = offset.x.saturating_sub(src_area.x);
798        let cut_y0 = offset.y.saturating_sub(src_area.y);
799
800        // length to copy
801        let len_src = src_area.width.saturating_sub(cut_x0);
802        let len_tgt = tgt_area.width.saturating_sub(off_x0);
803        let len = min(len_src, len_tgt);
804
805        // area height to copy
806        let height_src = src_area.height.saturating_sub(cut_y0);
807        let height_tgt = tgt_area.height.saturating_sub(off_y0);
808        let height = min(height_src, height_tgt);
809
810        // ** slow version **
811        // for y in 0..height {
812        //     for x in 0..len {
813        //         let src_pos = Position::new(src_area.x + cut_x0 + x, src_area.y + cut_y0 + y);
814        //         let src_cell = self.buffer.cell(src_pos).expect("src-cell");
815        //
816        //         let tgt_pos = Position::new(tgt_area.x + off_x0 + x, tgt_area.y + off_y0 + y);
817        //         let tgt_cell = buf.cell_mut(tgt_pos).expect("tgt_cell");
818        //
819        //         *tgt_cell = src_cell.clone();
820        //     }
821        // }
822
823        for y in 0..height {
824            let src_0 = self
825                .buffer
826                .index_of(src_area.x + cut_x0, src_area.y + cut_y0 + y);
827
828            if tgt_buf
829                .area
830                .contains(Position::new(tgt_area.x + off_x0, tgt_area.y + off_y0 + y))
831            {
832                let tgt_0 = tgt_buf.index_of(tgt_area.x + off_x0, tgt_area.y + off_y0 + y);
833
834                let src = &self.buffer.content[src_0..src_0 + len as usize];
835                let tgt = &mut tgt_buf.content[tgt_0..tgt_0 + len as usize];
836                tgt.clone_from_slice(src);
837            }
838        }
839
840        // keep buffer
841        state.buffer = Some(mem::take(&mut self.buffer));
842    }
843}
844
845impl<W> Default for ClipperState<W>
846where
847    W: Eq + Hash + Clone,
848{
849    fn default() -> Self {
850        Self {
851            area: Default::default(),
852            widget_area: Default::default(),
853            layout: Default::default(),
854            hscroll: Default::default(),
855            vscroll: Default::default(),
856            container: Default::default(),
857            buffer: None,
858            non_exhaustive: NonExhaustive,
859        }
860    }
861}
862
863impl<W> Clone for ClipperState<W>
864where
865    W: Eq + Hash + Clone,
866{
867    fn clone(&self) -> Self {
868        Self {
869            area: self.area,
870            widget_area: self.widget_area,
871            layout: self.layout.clone(),
872            hscroll: self.hscroll.clone(),
873            vscroll: self.vscroll.clone(),
874            container: self.container.new_instance(),
875            buffer: None,
876            non_exhaustive: NonExhaustive,
877        }
878    }
879}
880
881impl<W> HasFocus for ClipperState<W>
882where
883    W: Eq + Clone + Hash,
884{
885    fn build(&self, _builder: &mut FocusBuilder) {
886        // not a widget but a container
887    }
888
889    fn focus(&self) -> FocusFlag {
890        self.container.clone()
891    }
892
893    fn area(&self) -> Rect {
894        self.area
895    }
896}
897
898impl<W> RelocatableState for ClipperState<W>
899where
900    W: Eq + Clone + Hash,
901{
902    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
903        self.area.relocate(shift, clip);
904        self.widget_area.relocate(shift, clip);
905        self.hscroll.relocate(shift, clip);
906        self.vscroll.relocate(shift, clip);
907    }
908}
909
910impl<W> ClipperState<W>
911where
912    W: Eq + Clone + Hash,
913{
914    pub fn new() -> Self {
915        Self::default()
916    }
917
918    pub fn named(name: &str) -> Self {
919        let mut z = Self::default();
920        z.container = z.container.with_name(name);
921        z
922    }
923
924    /// Clear the layout data and reset any scroll
925    pub fn clear(&mut self) {
926        self.layout.borrow_mut().clear();
927        self.hscroll.clear();
928        self.vscroll.clear();
929    }
930
931    /// Layout needs to change?
932    pub fn valid_layout(&self, size: Size) -> bool {
933        let layout = self.layout.borrow();
934        !layout.size_changed(size) && !layout.is_empty()
935    }
936
937    /// Set the layout.
938    pub fn set_layout(&mut self, layout: GenericLayout<W>) {
939        self.layout = Rc::new(RefCell::new(layout));
940    }
941
942    /// Layout.
943    pub fn layout(&self) -> Ref<'_, GenericLayout<W>> {
944        self.layout.borrow()
945    }
946
947    /// Scroll to the given widget.
948    pub fn show(&mut self, widget: W) -> bool {
949        let layout = self.layout.borrow();
950        let Some(idx) = layout.try_index_of(widget) else {
951            return false;
952        };
953        let widget_area = layout.widget(idx);
954        let label_area = layout.label(idx);
955
956        let area = if !widget_area.is_empty() {
957            if !label_area.is_empty() {
958                Some(widget_area.union(label_area))
959            } else {
960                Some(widget_area)
961            }
962        } else {
963            if !label_area.is_empty() {
964                Some(label_area)
965            } else {
966                None
967            }
968        };
969
970        if let Some(area) = area {
971            let h = self
972                .hscroll
973                .scroll_to_range(area.left() as usize..area.right() as usize);
974            let v = self
975                .vscroll
976                .scroll_to_range(area.top() as usize..area.bottom() as usize);
977            h || v
978        } else {
979            false
980        }
981    }
982
983    /// Returns the first visible widget.
984    /// This uses insertion order of the widgets, not
985    /// any graphical ordering.
986    pub fn first(&self) -> Option<W> {
987        let layout = self.layout.borrow();
988
989        let area = Rect::new(
990            self.hscroll.offset() as u16,
991            self.vscroll.offset() as u16,
992            self.widget_area.width,
993            self.widget_area.height,
994        );
995
996        for idx in 0..layout.widget_len() {
997            if layout.widget(idx).intersects(area) {
998                return Some(layout.widget_key(idx).clone());
999            }
1000        }
1001
1002        None
1003    }
1004}
1005
1006impl<W> ClipperState<W>
1007where
1008    W: Eq + Clone + Hash,
1009{
1010    pub fn vertical_offset(&self) -> usize {
1011        self.vscroll.offset()
1012    }
1013
1014    pub fn set_vertical_offset(&mut self, offset: usize) -> bool {
1015        let old = self.vscroll.offset();
1016        self.vscroll.set_offset(offset);
1017        old != self.vscroll.offset()
1018    }
1019
1020    pub fn vertical_page_len(&self) -> usize {
1021        self.vscroll.page_len()
1022    }
1023
1024    pub fn horizontal_offset(&self) -> usize {
1025        self.hscroll.offset()
1026    }
1027
1028    pub fn set_horizontal_offset(&mut self, offset: usize) -> bool {
1029        let old = self.hscroll.offset();
1030        self.hscroll.set_offset(offset);
1031        old != self.hscroll.offset()
1032    }
1033
1034    pub fn horizontal_page_len(&self) -> usize {
1035        self.hscroll.page_len()
1036    }
1037
1038    pub fn horizontal_scroll_to(&mut self, pos: usize) -> bool {
1039        self.hscroll.scroll_to_pos(pos)
1040    }
1041
1042    pub fn vertical_scroll_to(&mut self, pos: usize) -> bool {
1043        self.vscroll.scroll_to_pos(pos)
1044    }
1045
1046    /// Scroll the widget to visible.
1047    pub fn scroll_to(&mut self, widget: W) -> bool {
1048        self.show(widget)
1049    }
1050
1051    pub fn scroll_up(&mut self, delta: usize) -> bool {
1052        self.vscroll.scroll_up(delta)
1053    }
1054
1055    pub fn scroll_down(&mut self, delta: usize) -> bool {
1056        self.vscroll.scroll_down(delta)
1057    }
1058
1059    pub fn scroll_left(&mut self, delta: usize) -> bool {
1060        self.hscroll.scroll_left(delta)
1061    }
1062
1063    pub fn scroll_right(&mut self, delta: usize) -> bool {
1064        self.hscroll.scroll_right(delta)
1065    }
1066}
1067
1068impl ClipperState<usize> {
1069    /// Focus the first widget on the active page.
1070    /// This assumes the usize-key is a widget id.
1071    pub fn focus_first(&self, focus: &Focus) -> bool {
1072        if let Some(w) = self.first() {
1073            focus.by_widget_id(w);
1074            true
1075        } else {
1076            false
1077        }
1078    }
1079
1080    /// Show the page with the focused widget.
1081    /// This assumes the usize-key is a widget id.
1082    /// Does nothing if none of the widgets has the focus.
1083    ///
1084    /// You must use the boolean to trigger a repaint.
1085    pub fn show_focused(&mut self, focus: &Focus) -> bool {
1086        let Some(focused) = focus.focused() else {
1087            return false;
1088        };
1089        let focused = focused.widget_id();
1090        self.scroll_to(focused)
1091    }
1092}
1093
1094impl ClipperState<FocusFlag> {
1095    /// Focus the first widget on the active page.
1096    pub fn focus_first(&self, focus: &Focus) -> bool {
1097        if let Some(w) = self.first() {
1098            focus.focus(&w);
1099            true
1100        } else {
1101            false
1102        }
1103    }
1104
1105    /// Show the page with the focused widget.
1106    /// Does nothing if none of the widgets has the focus.
1107    pub fn show_focused(&mut self, focus: &Focus) -> bool {
1108        let Some(focused) = focus.focused() else {
1109            return false;
1110        };
1111        self.scroll_to(focused)
1112    }
1113}
1114
1115impl<W> HandleEvent<Event, Regular, Outcome> for ClipperState<W>
1116where
1117    W: Eq + Clone + Hash,
1118{
1119    fn handle(&mut self, event: &Event, _keymap: Regular) -> Outcome {
1120        let r = if self.container.is_focused() {
1121            match event {
1122                ct_event!(keycode press PageUp) => self.scroll_up(self.vscroll.page_len()).into(),
1123                ct_event!(keycode press PageDown) => {
1124                    self.scroll_down(self.vscroll.page_len()).into()
1125                }
1126                ct_event!(keycode press Home) => self.vertical_scroll_to(0).into(),
1127                ct_event!(keycode press End) => {
1128                    self.vertical_scroll_to(self.vscroll.max_offset()).into()
1129                }
1130                _ => Outcome::Continue,
1131            }
1132        } else {
1133            Outcome::Continue
1134        };
1135
1136        r.or_else(|| self.handle(event, MouseOnly))
1137    }
1138}
1139
1140impl<W> HandleEvent<Event, MouseOnly, Outcome> for ClipperState<W>
1141where
1142    W: Eq + Clone + Hash,
1143{
1144    fn handle(&mut self, event: &Event, _keymap: MouseOnly) -> Outcome {
1145        if self.is_obscured() {
1146            return Outcome::Continue;
1147        }
1148
1149        let mut sas = ScrollAreaState::new()
1150            .area(self.widget_area)
1151            .h_scroll(&mut self.hscroll)
1152            .v_scroll(&mut self.vscroll);
1153        match sas.handle(event, MouseOnly) {
1154            ScrollOutcome::Up(v) => self.scroll_up(v).into(),
1155            ScrollOutcome::Down(v) => self.scroll_down(v).into(),
1156            ScrollOutcome::VPos(v) => self.set_vertical_offset(v).into(),
1157            ScrollOutcome::Left(v) => self.scroll_left(v).into(),
1158            ScrollOutcome::Right(v) => self.scroll_right(v).into(),
1159            ScrollOutcome::HPos(v) => self.set_horizontal_offset(v).into(),
1160            r => r.into(),
1161        }
1162    }
1163}
1164
1165/// Handle all events.
1166/// Text events are only processed if focus is true.
1167/// Mouse events are processed if they are in range.
1168pub fn handle_events<W>(state: &mut ClipperState<W>, _focus: bool, event: &Event) -> Outcome
1169where
1170    W: Eq + Clone + Hash,
1171{
1172    HandleEvent::handle(state, event, Regular)
1173}
1174
1175/// Handle only mouse-events.
1176pub fn handle_mouse_events<W>(state: &mut ClipperState<W>, event: &Event) -> Outcome
1177where
1178    W: Eq + Clone + Hash,
1179{
1180    HandleEvent::handle(state, event, MouseOnly)
1181}