rat_widget/
clipper.rs

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