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
// (C) 2025 - Enzo Lombardi
//! View trait - base interface for all UI components with event handling and drawing.
use crate::core::geometry::Rect;
use crate::core::event::Event;
use crate::core::draw::DrawBuffer;
use crate::core::state::{StateFlags, SF_SHADOW, SF_FOCUSED, SHADOW_SIZE};
use crate::core::command::CommandId;
use crate::terminal::Terminal;
use std::io;
/// View trait - all UI components implement this
///
/// ## Owner/Parent Communication Pattern
///
/// Unlike Borland's TView which stores an `owner` pointer to the parent TGroup,
/// Rust views communicate with parents through event propagation:
///
/// **Borland Pattern:**
/// ```cpp
/// void TButton::press() {
/// message(owner, evBroadcast, command, this);
/// }
/// ```
///
/// **Rust Pattern:**
/// ```rust
/// fn handle_event(&mut self, event: &mut Event) {
/// // Transform event to send message upward
/// *event = Event::command(self.command);
/// // Event bubbles up through Group::handle_event() call stack
/// }
/// ```
///
/// This achieves the same result (child-to-parent communication) without raw pointers,
/// using Rust's ownership system and the call stack for context.
pub trait View {
fn bounds(&self) -> Rect;
fn set_bounds(&mut self, bounds: Rect);
fn draw(&mut self, terminal: &mut Terminal);
fn handle_event(&mut self, event: &mut Event);
fn can_focus(&self) -> bool { false }
/// Set focus state - default implementation uses SF_FOCUSED flag
/// Views should override only if they need custom focus behavior
fn set_focus(&mut self, focused: bool) {
self.set_state_flag(SF_FOCUSED, focused);
}
/// Check if view is focused - reads SF_FOCUSED flag
fn is_focused(&self) -> bool {
self.get_state_flag(SF_FOCUSED)
}
/// Get view option flags (OF_SELECTABLE, OF_PRE_PROCESS, OF_POST_PROCESS, etc.)
fn options(&self) -> u16 { 0 }
/// Set view option flags
fn set_options(&mut self, _options: u16) {}
/// Get view state flags
fn state(&self) -> StateFlags { 0 }
/// Set view state flags
fn set_state(&mut self, _state: StateFlags) {}
/// Set or clear specific state flag(s)
/// Matches Borland's TView::setState(ushort aState, Boolean enable)
/// If enable is true, sets the flag(s), otherwise clears them
fn set_state_flag(&mut self, flag: StateFlags, enable: bool) {
let current = self.state();
if enable {
self.set_state(current | flag);
} else {
self.set_state(current & !flag);
}
}
/// Check if specific state flag(s) are set
/// Matches Borland's TView::getState(ushort aState)
fn get_state_flag(&self, flag: StateFlags) -> bool {
(self.state() & flag) == flag
}
/// Check if view has shadow enabled
fn has_shadow(&self) -> bool {
(self.state() & SF_SHADOW) != 0
}
/// Get bounds including shadow area
fn shadow_bounds(&self) -> Rect {
let mut bounds = self.bounds();
if self.has_shadow() {
bounds.b.x += SHADOW_SIZE.0;
bounds.b.y += SHADOW_SIZE.1;
}
bounds
}
/// Update cursor state (called after draw)
/// Views that need to show a cursor when focused should override this
fn update_cursor(&self, _terminal: &mut Terminal) {
// Default: do nothing (cursor stays hidden)
}
/// Zoom (maximize/restore) the view with given maximum bounds
/// Matches Borland: TWindow::zoom() toggles between current and max size
/// Default implementation does nothing (only windows support zoom)
fn zoom(&mut self, _max_bounds: Rect) {
// Default: do nothing (only Window implements zoom)
}
/// Validate the view before performing a command (usually closing)
/// Matches Borland: TView::valid(ushort command) - returns Boolean
/// Returns true if the view's state is valid for the given command
/// Used for "Save before closing?" type scenarios and input validation
///
/// # Arguments
/// * `command` - The command being performed (CM_OK, CM_CANCEL, CM_RELEASED_FOCUS, etc.)
///
/// # Returns
/// * `true` - View state is valid, command can proceed
/// * `false` - View state is invalid, command should be blocked
///
/// Default implementation always returns true (no validation)
fn valid(&mut self, _command: crate::core::command::CommandId) -> bool {
true
}
/// Downcast to concrete type (immutable)
/// Allows accessing specific view type methods from trait object
fn as_any(&self) -> &dyn std::any::Any {
panic!("as_any() not implemented for this view type")
}
/// Downcast to concrete type (mutable)
/// Allows accessing specific view type methods from trait object
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
panic!("as_any_mut() not implemented for this view type")
}
/// Dump this view's region of the terminal buffer to an ANSI file for debugging
fn dump_to_file(&self, terminal: &Terminal, path: &str) -> io::Result<()> {
let bounds = self.shadow_bounds();
terminal.dump_region(
bounds.a.x as u16,
bounds.a.y as u16,
(bounds.b.x - bounds.a.x) as u16,
(bounds.b.y - bounds.a.y) as u16,
path,
)
}
/// Check if this view is a default button (for Enter key handling at Dialog level)
/// Corresponds to Borland's TButton::amDefault flag (tbutton.cc line 239)
fn is_default_button(&self) -> bool {
false
}
/// Get the command ID for this button (if it's a button)
/// Returns None if not a button
/// Used by Dialog to activate default button on Enter key
fn button_command(&self) -> Option<u16> {
None
}
/// Set the selection index for listbox views
/// Only implemented by ListBox, other views ignore this
fn set_list_selection(&mut self, _index: usize) {
// Default: do nothing (not a listbox)
}
/// Get the selection index for listbox views
/// Only implemented by ListBox, other views return 0
fn get_list_selection(&self) -> usize {
0
}
/// Get the union rect of previous and current bounds for redrawing
/// Matches Borland: TView::locate() calculates union of old and new bounds
/// Returns None if the view hasn't moved since last redraw
/// Used by Desktop to implement Borland's drawUnderRect pattern
fn get_redraw_union(&self) -> Option<Rect> {
None // Default: no movement tracking
}
/// Clear movement tracking after redrawing
/// Matches Borland: Called after drawUnderRect completes
fn clear_move_tracking(&mut self) {
// Default: do nothing (no movement tracking)
}
/// Get the end state for modal views
/// Matches Borland: TGroup::endState field
/// Returns the command ID that ended modal execution (0 if still running)
fn get_end_state(&self) -> CommandId {
0 // Default: not ended
}
/// Set the end state for modal views
/// Called by end_modal() to signal the modal loop should exit
fn set_end_state(&mut self, _command: CommandId) {
// Default: do nothing (only modal views need this)
}
/// Convert local coordinates to global (screen) coordinates
/// Matches Borland: TView::makeGlobal(TPoint source, TPoint& dest)
///
/// In Borland, makeGlobal traverses the owner chain and accumulates offsets.
/// In this Rust implementation, views store absolute bounds (converted in Group::add()),
/// so we simply add the view's origin to the local coordinates.
///
/// # Arguments
/// * `local_x` - X coordinate relative to view's interior (0,0 = top-left of view)
/// * `local_y` - Y coordinate relative to view's interior
///
/// # Returns
/// Global (screen) coordinates as (x, y) tuple
fn make_global(&self, local_x: i16, local_y: i16) -> (i16, i16) {
let bounds = self.bounds();
(bounds.a.x + local_x, bounds.a.y + local_y)
}
/// Convert global (screen) coordinates to local view coordinates
/// Matches Borland: TView::makeLocal(TPoint source, TPoint& dest)
///
/// In Borland, makeLocal is the inverse of makeGlobal, converting screen
/// coordinates back to view-relative coordinates.
///
/// # Arguments
/// * `global_x` - X coordinate in screen space
/// * `global_y` - Y coordinate in screen space
///
/// # Returns
/// Local coordinates as (x, y) tuple, where (0,0) is the view's top-left
fn make_local(&self, global_x: i16, global_y: i16) -> (i16, i16) {
let bounds = self.bounds();
(global_x - bounds.a.x, global_y - bounds.a.y)
}
/// Get the linked control index for labels
/// Matches Borland: TLabel::link field
/// Returns Some(index) if this is a label with a linked control, None otherwise
/// Used by Group to implement focus transfer when clicking labels
fn label_link(&self) -> Option<usize> {
None // Default: not a label or no link
}
}
/// Helper to draw a line to the terminal
pub fn write_line_to_terminal(terminal: &mut Terminal, x: i16, y: i16, buf: &DrawBuffer) {
if y < 0 || y >= terminal.size().1 as i16 {
return;
}
terminal.write_line(x.max(0) as u16, y as u16, &buf.data);
}
/// Draw shadow for a view
/// Draws a shadow offset by (1, 1) from the view bounds
/// Shadow is the same size as the view, but only the right and bottom edges are visible
pub fn draw_shadow(terminal: &mut Terminal, bounds: Rect, shadow_attr: u8) {
use crate::core::palette::Attr;
let attr = Attr::from_u8(shadow_attr);
let mut buf = DrawBuffer::new(SHADOW_SIZE.0 as usize);
// Draw right edge shadow (2 columns wide, offset by 1 vertically)
// Starts at y+1 and extends to y+height+1
for y in (bounds.a.y + 1)..(bounds.b.y + 1) {
buf.move_char(0, ' ', attr, SHADOW_SIZE.0 as usize);
write_line_to_terminal(terminal, bounds.b.x, y, &buf);
}
// Draw bottom edge shadow (offset by 1 horizontally, includes right shadow area)
// Starts at x+1 and extends to match the right shadow end point
// Width = view_width + (SHADOW_SIZE.0 - 1) because we offset by 1
let bottom_width = (bounds.b.x - bounds.a.x + SHADOW_SIZE.0 - 1) as usize;
let mut bottom_buf = DrawBuffer::new(bottom_width);
bottom_buf.move_char(0, ' ', attr, bottom_width);
write_line_to_terminal(terminal, bounds.a.x + 1, bounds.b.y, &bottom_buf);
}