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