ratatui_interact/traits/clickable.rs
1//! Clickable trait for mouse interaction
2//!
3//! Provides click region management for components that respond to mouse clicks.
4//! Click regions are registered during rendering and checked during event handling.
5//!
6//! # Example
7//!
8//! ```rust
9//! use ratatui_interact::traits::{ClickRegion, ClickRegionRegistry};
10//! use ratatui::layout::Rect;
11//!
12//! // Create a registry for tracking click regions
13//! let mut registry: ClickRegionRegistry<&str> = ClickRegionRegistry::new();
14//!
15//! // Register click regions during render
16//! registry.register(Rect::new(0, 0, 10, 1), "button1");
17//! registry.register(Rect::new(15, 0, 10, 1), "button2");
18//!
19//! // Check for clicks during event handling
20//! if let Some(action) = registry.handle_click(5, 0) {
21//! assert_eq!(*action, "button1");
22//! }
23//! ```
24
25use ratatui::layout::Rect;
26
27/// A registered click region that responds to mouse clicks.
28///
29/// Associates a rectangular area with user-defined data that is returned
30/// when a click occurs within the region.
31#[derive(Debug, Clone)]
32pub struct ClickRegion<T: Clone> {
33 /// The area that responds to clicks.
34 pub area: Rect,
35 /// User-defined data associated with this region.
36 pub data: T,
37}
38
39impl<T: Clone> ClickRegion<T> {
40 /// Create a new click region.
41 ///
42 /// # Arguments
43 ///
44 /// * `area` - The rectangular area that responds to clicks
45 /// * `data` - Data to return when this region is clicked
46 pub fn new(area: Rect, data: T) -> Self {
47 Self { area, data }
48 }
49
50 /// Check if a point is within this region.
51 ///
52 /// # Arguments
53 ///
54 /// * `col` - The column (x) position
55 /// * `row` - The row (y) position
56 ///
57 /// # Returns
58 ///
59 /// `true` if the point is within the region's bounds.
60 pub fn contains(&self, col: u16, row: u16) -> bool {
61 col >= self.area.x
62 && col < self.area.x + self.area.width
63 && row >= self.area.y
64 && row < self.area.y + self.area.height
65 }
66}
67
68/// Trait for components that respond to mouse clicks.
69///
70/// Implement this trait to make a component clickable with automatic
71/// hit-testing based on registered click regions.
72pub trait Clickable {
73 /// The type of action that a click produces.
74 type ClickAction: Clone;
75
76 /// Returns all click regions for this component.
77 ///
78 /// Called after rendering to get the active regions.
79 fn click_regions(&self) -> &[ClickRegion<Self::ClickAction>];
80
81 /// Handle a click at the given position.
82 ///
83 /// Returns `Some(action)` if the click was within a region,
84 /// `None` otherwise.
85 ///
86 /// Default implementation checks all regions and returns the first match.
87 fn handle_click(&self, col: u16, row: u16) -> Option<Self::ClickAction> {
88 self.click_regions()
89 .iter()
90 .find(|r| r.contains(col, row))
91 .map(|r| r.data.clone())
92 }
93}
94
95/// Registry for managing click regions during render.
96///
97/// Use this to track clickable areas that are populated during rendering
98/// and checked during event handling.
99///
100/// # Example
101///
102/// ```rust
103/// use ratatui_interact::traits::ClickRegionRegistry;
104/// use ratatui::layout::Rect;
105///
106/// #[derive(Clone, PartialEq, Debug)]
107/// enum ButtonId { Save, Cancel }
108///
109/// let mut registry: ClickRegionRegistry<ButtonId> = ClickRegionRegistry::new();
110///
111/// // Clear before each render
112/// registry.clear();
113///
114/// // Register regions during render
115/// registry.register(Rect::new(0, 0, 8, 1), ButtonId::Save);
116/// registry.register(Rect::new(10, 0, 8, 1), ButtonId::Cancel);
117///
118/// // Check clicks during event handling
119/// assert_eq!(registry.handle_click(4, 0), Some(&ButtonId::Save));
120/// assert_eq!(registry.handle_click(14, 0), Some(&ButtonId::Cancel));
121/// assert_eq!(registry.handle_click(9, 0), None); // Gap between buttons
122/// ```
123#[derive(Debug, Clone)]
124pub struct ClickRegionRegistry<T: Clone> {
125 regions: Vec<ClickRegion<T>>,
126}
127
128impl<T: Clone> Default for ClickRegionRegistry<T> {
129 fn default() -> Self {
130 Self::new()
131 }
132}
133
134impl<T: Clone> ClickRegionRegistry<T> {
135 /// Create a new empty registry.
136 pub fn new() -> Self {
137 Self {
138 regions: Vec::new(),
139 }
140 }
141
142 /// Create a new registry with pre-allocated capacity.
143 pub fn with_capacity(capacity: usize) -> Self {
144 Self {
145 regions: Vec::with_capacity(capacity),
146 }
147 }
148
149 /// Clear all registered regions.
150 ///
151 /// Call this at the start of each render to reset the regions.
152 pub fn clear(&mut self) {
153 self.regions.clear();
154 }
155
156 /// Register a new click region.
157 ///
158 /// # Arguments
159 ///
160 /// * `area` - The rectangular area that responds to clicks
161 /// * `data` - Data to return when this region is clicked
162 pub fn register(&mut self, area: Rect, data: T) {
163 self.regions.push(ClickRegion::new(area, data));
164 }
165
166 /// Handle a click at the given position.
167 ///
168 /// Returns a reference to the data if the click was within a region,
169 /// `None` otherwise.
170 ///
171 /// # Arguments
172 ///
173 /// * `col` - The column (x) position
174 /// * `row` - The row (y) position
175 pub fn handle_click(&self, col: u16, row: u16) -> Option<&T> {
176 self.regions
177 .iter()
178 .find(|r| r.contains(col, row))
179 .map(|r| &r.data)
180 }
181
182 /// Get all registered regions.
183 pub fn regions(&self) -> &[ClickRegion<T>] {
184 &self.regions
185 }
186
187 /// Check if any regions are registered.
188 pub fn is_empty(&self) -> bool {
189 self.regions.is_empty()
190 }
191
192 /// Get the number of registered regions.
193 pub fn len(&self) -> usize {
194 self.regions.len()
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn test_click_region_contains() {
204 let region = ClickRegion::new(Rect::new(10, 5, 20, 3), "test");
205
206 // Inside
207 assert!(region.contains(10, 5)); // Top-left corner
208 assert!(region.contains(29, 7)); // Bottom-right corner (exclusive bounds)
209 assert!(region.contains(20, 6)); // Middle
210
211 // Outside
212 assert!(!region.contains(9, 5)); // Left of region
213 assert!(!region.contains(30, 5)); // Right of region
214 assert!(!region.contains(10, 4)); // Above region
215 assert!(!region.contains(10, 8)); // Below region
216 }
217
218 #[test]
219 fn test_click_region_zero_size() {
220 let region = ClickRegion::new(Rect::new(5, 5, 0, 0), "test");
221 assert!(!region.contains(5, 5));
222 }
223
224 #[test]
225 fn test_registry_basic_operations() {
226 let mut registry: ClickRegionRegistry<&str> = ClickRegionRegistry::new();
227
228 assert!(registry.is_empty());
229 assert_eq!(registry.len(), 0);
230
231 registry.register(Rect::new(0, 0, 10, 1), "first");
232 registry.register(Rect::new(15, 0, 10, 1), "second");
233
234 assert!(!registry.is_empty());
235 assert_eq!(registry.len(), 2);
236
237 registry.clear();
238 assert!(registry.is_empty());
239 }
240
241 #[test]
242 fn test_registry_handle_click() {
243 let mut registry: ClickRegionRegistry<i32> = ClickRegionRegistry::new();
244
245 registry.register(Rect::new(0, 0, 10, 1), 1);
246 registry.register(Rect::new(15, 0, 10, 1), 2);
247 registry.register(Rect::new(0, 2, 25, 2), 3);
248
249 // Click on first region
250 assert_eq!(registry.handle_click(5, 0), Some(&1));
251
252 // Click on second region
253 assert_eq!(registry.handle_click(20, 0), Some(&2));
254
255 // Click on third region
256 assert_eq!(registry.handle_click(12, 3), Some(&3));
257
258 // Click in gap
259 assert_eq!(registry.handle_click(12, 0), None);
260
261 // Click outside all regions
262 assert_eq!(registry.handle_click(100, 100), None);
263 }
264
265 #[test]
266 fn test_registry_overlapping_regions() {
267 let mut registry: ClickRegionRegistry<&str> = ClickRegionRegistry::new();
268
269 // Overlapping regions - first registered wins
270 registry.register(Rect::new(0, 0, 20, 2), "back");
271 registry.register(Rect::new(5, 0, 10, 1), "front");
272
273 // Click on overlapping area returns first registered
274 assert_eq!(registry.handle_click(7, 0), Some(&"back"));
275
276 // Click on non-overlapping part of back region
277 assert_eq!(registry.handle_click(2, 1), Some(&"back"));
278 }
279
280 #[test]
281 fn test_clickable_trait() {
282 #[derive(Clone, PartialEq, Debug)]
283 enum Action {
284 Click,
285 }
286
287 struct ClickableWidget {
288 regions: Vec<ClickRegion<Action>>,
289 }
290
291 impl Clickable for ClickableWidget {
292 type ClickAction = Action;
293
294 fn click_regions(&self) -> &[ClickRegion<Self::ClickAction>] {
295 &self.regions
296 }
297 }
298
299 let widget = ClickableWidget {
300 regions: vec![ClickRegion::new(Rect::new(0, 0, 10, 1), Action::Click)],
301 };
302
303 assert_eq!(widget.handle_click(5, 0), Some(Action::Click));
304 assert_eq!(widget.handle_click(15, 0), None);
305 }
306}