1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
//! Settings layout for hit testing
//!
//! Tracks the layout of rendered settings UI elements for mouse interaction.
use super::render::ControlLayoutInfo;
use crate::view::ui::point_in_rect;
use ratatui::layout::Rect;
/// Layout information for the entire settings UI
#[derive(Debug, Clone, Default)]
pub struct SettingsLayout {
/// The modal area
pub modal_area: Rect,
/// Category list items (index, area)
pub categories: Vec<(usize, Rect)>,
/// Setting items (index, path, area, control_layout)
pub items: Vec<ItemLayout>,
/// Search result items (page_index, item_index, area)
pub search_results: Vec<SearchResultLayout>,
/// Layer button area
pub layer_button: Option<Rect>,
/// Edit config file button area
pub edit_button: Option<Rect>,
/// Save button area
pub save_button: Option<Rect>,
/// Cancel button area
pub cancel_button: Option<Rect>,
/// Reset button area
pub reset_button: Option<Rect>,
/// Settings panel area (for scroll hit testing)
pub settings_panel_area: Option<Rect>,
/// Scrollbar area (for drag detection)
pub scrollbar_area: Option<Rect>,
}
/// Layout info for a search result
#[derive(Debug, Clone)]
pub struct SearchResultLayout {
/// Page index (category)
pub page_index: usize,
/// Item index within the page
pub item_index: usize,
/// Full area for this result
pub area: Rect,
}
/// Layout info for a setting item
#[derive(Debug, Clone)]
pub struct ItemLayout {
/// Item index within current page
pub index: usize,
/// JSON path for this setting
pub path: String,
/// Full item area (for selection)
pub area: Rect,
/// Control-specific layout info
pub control: ControlLayoutInfo,
}
impl SettingsLayout {
/// Create a new layout for the given modal area
pub fn new(modal_area: Rect) -> Self {
Self {
modal_area,
categories: Vec::new(),
items: Vec::new(),
search_results: Vec::new(),
layer_button: None,
edit_button: None,
save_button: None,
cancel_button: None,
reset_button: None,
settings_panel_area: None,
scrollbar_area: None,
}
}
/// Add a category to the layout
pub fn add_category(&mut self, index: usize, area: Rect) {
self.categories.push((index, area));
}
/// Add a setting item to the layout
pub fn add_item(&mut self, index: usize, path: String, area: Rect, control: ControlLayoutInfo) {
self.items.push(ItemLayout {
index,
path,
area,
control,
});
}
/// Add a search result to the layout
pub fn add_search_result(&mut self, page_index: usize, item_index: usize, area: Rect) {
self.search_results.push(SearchResultLayout {
page_index,
item_index,
area,
});
}
/// Hit test a position and return what was clicked
pub fn hit_test(&self, x: u16, y: u16) -> Option<SettingsHit> {
// Check if outside modal
if !point_in_rect(self.modal_area, x, y) {
return Some(SettingsHit::Outside);
}
// Check footer buttons
if let Some(ref layer) = self.layer_button {
if point_in_rect(*layer, x, y) {
return Some(SettingsHit::LayerButton);
}
}
if let Some(ref edit) = self.edit_button {
if point_in_rect(*edit, x, y) {
return Some(SettingsHit::EditButton);
}
}
if let Some(ref save) = self.save_button {
if point_in_rect(*save, x, y) {
return Some(SettingsHit::SaveButton);
}
}
if let Some(ref cancel) = self.cancel_button {
if point_in_rect(*cancel, x, y) {
return Some(SettingsHit::CancelButton);
}
}
if let Some(ref reset) = self.reset_button {
if point_in_rect(*reset, x, y) {
return Some(SettingsHit::ResetButton);
}
}
// Check categories
for (index, area) in &self.categories {
if point_in_rect(*area, x, y) {
return Some(SettingsHit::Category(*index));
}
}
// Check search results (before regular items, since they replace the item list during search)
for (idx, result) in self.search_results.iter().enumerate() {
if point_in_rect(result.area, x, y) {
return Some(SettingsHit::SearchResult(idx));
}
}
// Check setting items
for item in &self.items {
if point_in_rect(item.area, x, y) {
// Check specific control areas
match &item.control {
ControlLayoutInfo::Toggle(toggle_area) => {
if point_in_rect(*toggle_area, x, y) {
return Some(SettingsHit::ControlToggle(item.index));
}
}
ControlLayoutInfo::Number {
decrement,
increment,
value,
} => {
if point_in_rect(*decrement, x, y) {
return Some(SettingsHit::ControlDecrement(item.index));
}
if point_in_rect(*increment, x, y) {
return Some(SettingsHit::ControlIncrement(item.index));
}
if point_in_rect(*value, x, y) {
return Some(SettingsHit::Item(item.index));
}
}
ControlLayoutInfo::Dropdown {
button_area,
option_areas,
scroll_offset,
} => {
// Check option areas first (when dropdown is open)
for (i, area) in option_areas.iter().enumerate() {
if point_in_rect(*area, x, y) {
return Some(SettingsHit::ControlDropdownOption(
item.index,
scroll_offset + i,
));
}
}
if point_in_rect(*button_area, x, y) {
return Some(SettingsHit::ControlDropdown(item.index));
}
}
ControlLayoutInfo::Text(area) => {
if point_in_rect(*area, x, y) {
return Some(SettingsHit::ControlText(item.index));
}
}
ControlLayoutInfo::TextList { rows } => {
for (row_idx, row_area) in rows.iter().enumerate() {
if point_in_rect(*row_area, x, y) {
return Some(SettingsHit::ControlTextListRow(item.index, row_idx));
}
}
}
ControlLayoutInfo::Map {
entry_rows,
add_row_area,
} => {
// Check click on add-new row first (so it has priority)
if let Some(add_area) = add_row_area {
if point_in_rect(*add_area, x, y) {
return Some(SettingsHit::ControlMapAddNew(item.index));
}
}
for (row_idx, row_area) in entry_rows.iter().enumerate() {
if point_in_rect(*row_area, x, y) {
return Some(SettingsHit::ControlMapRow(item.index, row_idx));
}
}
}
ControlLayoutInfo::ObjectArray { entry_rows } => {
for (row_idx, row_area) in entry_rows.iter().enumerate() {
if point_in_rect(*row_area, x, y) {
return Some(SettingsHit::ControlMapRow(item.index, row_idx));
}
}
}
ControlLayoutInfo::Json { edit_area } => {
if point_in_rect(*edit_area, x, y) {
return Some(SettingsHit::ControlText(item.index));
}
}
ControlLayoutInfo::Complex => {}
}
return Some(SettingsHit::Item(item.index));
}
}
// Check scrollbar area (for drag detection)
if let Some(ref scrollbar) = self.scrollbar_area {
if point_in_rect(*scrollbar, x, y) {
return Some(SettingsHit::Scrollbar);
}
}
// Check settings panel area (for scroll wheel)
if let Some(ref panel) = self.settings_panel_area {
if point_in_rect(*panel, x, y) {
return Some(SettingsHit::SettingsPanel);
}
}
Some(SettingsHit::Background)
}
}
/// Result of a hit test on the settings UI
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingsHit {
/// Click outside the modal
Outside,
/// Click on modal background
Background,
/// Click on a category (index)
Category(usize),
/// Click on a setting item (index)
Item(usize),
/// Click on a search result (index in search_results)
SearchResult(usize),
/// Click on toggle control
ControlToggle(usize),
/// Click on number decrement button
ControlDecrement(usize),
/// Click on number increment button
ControlIncrement(usize),
/// Click on dropdown button
ControlDropdown(usize),
/// Click on dropdown option (item_idx, option_idx)
ControlDropdownOption(usize, usize),
/// Click on text input
ControlText(usize),
/// Click on text list row (item_idx, row_idx)
ControlTextListRow(usize, usize),
/// Click on map row (item_idx, row_idx)
ControlMapRow(usize, usize),
/// Click on map add-new row (item_idx)
ControlMapAddNew(usize),
/// Click on layer button
LayerButton,
/// Click on edit config file button
EditButton,
/// Click on save button
SaveButton,
/// Click on cancel button
CancelButton,
/// Click on reset button
ResetButton,
/// Click on settings panel scrollbar
Scrollbar,
/// Click on settings panel (scrollable area)
SettingsPanel,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_layout_creation() {
let modal = Rect::new(10, 5, 80, 30);
let mut layout = SettingsLayout::new(modal);
layout.add_category(0, Rect::new(11, 6, 20, 1));
layout.add_category(1, Rect::new(11, 7, 20, 1));
assert_eq!(layout.categories.len(), 2);
}
#[test]
fn test_hit_test_outside() {
let modal = Rect::new(10, 5, 80, 30);
let layout = SettingsLayout::new(modal);
assert_eq!(layout.hit_test(0, 0), Some(SettingsHit::Outside));
assert_eq!(layout.hit_test(5, 5), Some(SettingsHit::Outside));
}
#[test]
fn test_hit_test_category() {
let modal = Rect::new(10, 5, 80, 30);
let mut layout = SettingsLayout::new(modal);
layout.add_category(0, Rect::new(11, 6, 20, 1));
layout.add_category(1, Rect::new(11, 7, 20, 1));
assert_eq!(layout.hit_test(15, 6), Some(SettingsHit::Category(0)));
assert_eq!(layout.hit_test(15, 7), Some(SettingsHit::Category(1)));
}
#[test]
fn test_hit_test_buttons() {
let modal = Rect::new(10, 5, 80, 30);
let mut layout = SettingsLayout::new(modal);
layout.save_button = Some(Rect::new(60, 32, 8, 1));
layout.cancel_button = Some(Rect::new(70, 32, 10, 1));
assert_eq!(layout.hit_test(62, 32), Some(SettingsHit::SaveButton));
assert_eq!(layout.hit_test(75, 32), Some(SettingsHit::CancelButton));
}
#[test]
fn test_hit_test_item_with_toggle() {
let modal = Rect::new(10, 5, 80, 30);
let mut layout = SettingsLayout::new(modal);
layout.add_item(
0,
"/test".to_string(),
Rect::new(35, 10, 50, 2),
ControlLayoutInfo::Toggle(Rect::new(37, 11, 15, 1)),
);
// Click on toggle control
assert_eq!(layout.hit_test(40, 11), Some(SettingsHit::ControlToggle(0)));
// Click on item but not on toggle
assert_eq!(layout.hit_test(35, 10), Some(SettingsHit::Item(0)));
}
}