Skip to main content

rat_reloc/
lib.rs

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