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 all areas to a clip-rect (0,0+0x0).
23    fn relocate_hidden(&mut self) {
24        self.relocate((0, 0), Rect::default())
25    }
26}
27
28/// Create the implementation of RelocatableState for the
29/// given list of struct members.
30#[macro_export]
31macro_rules! impl_relocatable_state {
32    ($($n:ident),* for $ty:ty) => {
33        impl $crate::RelocatableState for $ty {
34            fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
35                $(self.$n.relocate(shift, clip);)*
36            }
37        }
38    };
39}
40
41impl RelocatableState for Rect {
42    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
43        *self = relocate_area(*self, shift, clip);
44    }
45}
46
47impl RelocatableState for [Rect] {
48    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
49        for rect in self {
50            rect.relocate(shift, clip);
51        }
52    }
53}
54
55impl RelocatableState for () {
56    fn relocate(&mut self, _shift: (i16, i16), _clip: Rect) {}
57}
58
59/// Shift the area by offset and clip it.
60pub fn relocate_areas(area: &mut [Rect], shift: (i16, i16), clip: Rect) {
61    for a in area {
62        *a = relocate_area(*a, shift, clip)
63    }
64}
65
66/// Shift the area by offset and clip it.
67pub fn relocate_area(area: Rect, shift: (i16, i16), clip: Rect) -> Rect {
68    let relocated = relocate(area, shift);
69    let clipped = clipped(relocated, clip);
70
71    if !clipped.is_empty() {
72        clipped
73    } else {
74        // default on empty
75        Rect::default()
76    }
77}
78
79/// Shift the position by offset and clip it.
80/// Clipped positions are replaced with (0,0) for this function.
81pub fn relocate_positions(pos: &mut [Position], shift: (i16, i16), clip: Rect) {
82    for p in pos {
83        *p = relocate_position(*p, shift, clip).unwrap_or_default()
84    }
85}
86
87/// Shift the position by offset and clip it.
88/// Returns None if the position is clipped.
89pub fn relocate_position(pos: Position, shift: (i16, i16), clip: Rect) -> Option<Position> {
90    let reloc = relocate(Rect::new(pos.x, pos.y, 0, 0), shift);
91    let reloc_pos = Position::new(reloc.x, reloc.y);
92
93    if clip.contains(reloc_pos) {
94        Some(reloc_pos)
95    } else {
96        None
97    }
98}
99
100/// Shift the position by offset and clip it.
101/// Returns None if the position is clipped.
102pub fn relocate_pos_tuple(pos: (u16, u16), shift: (i16, i16), clip: Rect) -> Option<(u16, u16)> {
103    let reloc = relocate(Rect::new(pos.0, pos.1, 0, 0), shift);
104    let reloc_pos = Position::new(reloc.x, reloc.y);
105
106    if clip.contains(reloc_pos) {
107        Some((reloc_pos.x, reloc_pos.y))
108    } else {
109        None
110    }
111}
112
113/// Clipping might introduce another offset by cutting away
114/// part of an area that a widgets internal offset refers to.
115///
116/// This calculates this dark, extra offset.
117pub fn relocate_dark_offset(area: Rect, shift: (i16, i16), clip: Rect) -> (u16, u16) {
118    let relocated = relocate(area, shift);
119    let clipped = clipped(relocated, clip);
120
121    (clipped.x - relocated.x, clipped.y - relocated.y)
122}
123
124#[inline]
125fn relocate(area: Rect, shift: (i16, i16)) -> Rect {
126    let x0 = area.left().saturating_add_signed(shift.0);
127    let x1 = area.right().saturating_add_signed(shift.0);
128    let y0 = area.top().saturating_add_signed(shift.1);
129    let y1 = area.bottom().saturating_add_signed(shift.1);
130
131    Rect::new(x0, y0, x1 - x0, y1 - y0)
132}
133
134#[inline]
135fn clipped(area: Rect, clip: Rect) -> Rect {
136    area.intersection(clip)
137}