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