rat_reloc/
lib.rs

1#![doc = include_str!("../readme.md")]
2
3use ratatui::layout::{Position, Rect};
4
5/// Widgets can be rendered to a temporary Buffer that will
6/// be dumped to the main render Buffer at a later point.
7///
8/// This temporary Buffer can have its own coordinate system
9/// and output in the main render Buffer can happen anywhere.
10///
11/// Most rat-widgets store one or more areas during rendering
12/// for later mouse interactions. All these areas need to
13/// be adjusted whenever such a temporary Buffer is used.
14///
15/// This trait provides the entry point for such an adjustment.
16pub trait RelocatableState {
17    /// Relocate the areas in this widgets state.
18    /// - shift: positions are moved by (x,y)
19    /// - clip: areas must be clipped to the given Rect.
20    fn relocate(&mut self, shift: (i16, i16), clip: Rect);
21
22    /// Relocate only popup areas.
23    /// As rendering the popups is a separate render,
24    /// this has to be separate too.
25    #[allow(unused_variables)]
26    fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {}
27
28    /// Relocate all popup areas to a clip-rect (0,0+0x0).
29    fn relocate_popup_hidden(&mut self) {
30        self.relocate_popup((0, 0), Rect::default())
31    }
32
33    /// Relocate all areas to a clip-rect (0,0+0x0).
34    fn relocate_hidden(&mut self) {
35        self.relocate((0, 0), Rect::default())
36    }
37}
38
39/// Create the implementation of RelocatableState for the
40/// given list of struct members.
41#[macro_export]
42macro_rules! impl_relocatable_state {
43    ($($n:ident),* for $ty:ty) => {
44        impl $crate::RelocatableState for $ty {
45            fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
46                $(self.$n.relocate(shift, clip);)*
47            }
48        }
49    };
50}
51
52impl RelocatableState for Rect {
53    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
54        *self = relocate_area(*self, shift, clip);
55    }
56}
57
58impl RelocatableState for [Rect] {
59    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
60        for rect in self {
61            rect.relocate(shift, clip);
62        }
63    }
64}
65
66impl RelocatableState for () {
67    fn relocate(&mut self, _shift: (i16, i16), _clip: Rect) {}
68}
69
70/// Shift the area by offset and clip it.
71pub fn relocate_areas(area: &mut [Rect], shift: (i16, i16), clip: Rect) {
72    for a in area {
73        *a = relocate_area(*a, shift, clip)
74    }
75}
76
77/// Shift the area by offset and clip it.
78pub fn relocate_area(area: Rect, shift: (i16, i16), clip: Rect) -> Rect {
79    let relocated = relocate(area, shift);
80    let clipped = clipped(relocated, clip);
81
82    if !clipped.is_empty() {
83        clipped
84    } else {
85        // default on empty
86        Rect::default()
87    }
88}
89
90/// Shift the position by offset and clip it.
91/// Clipped positions are replaced with (0,0) for this function.
92pub fn relocate_positions(pos: &mut [Position], shift: (i16, i16), clip: Rect) {
93    for p in pos {
94        *p = relocate_position(*p, shift, clip).unwrap_or_default()
95    }
96}
97
98/// Shift the position by offset and clip it.
99/// Returns None if the position is clipped.
100pub fn relocate_position(pos: Position, shift: (i16, i16), clip: Rect) -> Option<Position> {
101    let reloc = relocate(Rect::new(pos.x, pos.y, 0, 0), shift);
102    let reloc_pos = Position::new(reloc.x, reloc.y);
103
104    if clip.contains(reloc_pos) {
105        Some(reloc_pos)
106    } else {
107        None
108    }
109}
110
111/// Shift the position by offset and clip it.
112/// Returns None if the position is clipped.
113pub fn relocate_pos_tuple(pos: (u16, u16), shift: (i16, i16), clip: Rect) -> Option<(u16, u16)> {
114    let reloc = relocate(Rect::new(pos.0, pos.1, 0, 0), shift);
115    let reloc_pos = Position::new(reloc.x, reloc.y);
116
117    if clip.contains(reloc_pos) {
118        Some((reloc_pos.x, reloc_pos.y))
119    } else {
120        None
121    }
122}
123
124/// Clipping might introduce another offset by cutting away
125/// part of an area that a widgets internal offset refers to.
126///
127/// This calculates this dark, extra offset.
128pub fn relocate_dark_offset(area: Rect, shift: (i16, i16), clip: Rect) -> (u16, u16) {
129    let relocated = relocate(area, shift);
130    let clipped = clipped(relocated, clip);
131
132    (clipped.x - relocated.x, clipped.y - relocated.y)
133}
134
135#[inline]
136fn relocate(area: Rect, shift: (i16, i16)) -> Rect {
137    let x0 = area.left().saturating_add_signed(shift.0);
138    let x1 = area.right().saturating_add_signed(shift.0);
139    let y0 = area.top().saturating_add_signed(shift.1);
140    let y1 = area.bottom().saturating_add_signed(shift.1);
141
142    Rect::new(x0, y0, x1 - x0, y1 - y0)
143}
144
145#[inline]
146fn clipped(area: Rect, clip: Rect) -> Rect {
147    area.intersection(clip)
148}