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
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
// (C) 2025 - Enzo Lombardi
//! Window view - draggable, resizable window with frame and shadow.
use crate::core::geometry::{Rect, Point};
use crate::core::event::{Event, EventType};
use crate::core::command::{CM_CLOSE, CM_CANCEL};
use crate::core::state::{StateFlags, SF_SHADOW, SF_DRAGGING, SF_RESIZING, SF_MODAL, SHADOW_ATTR};
use crate::core::palette::colors;
use crate::terminal::Terminal;
use super::view::{View, draw_shadow};
use super::frame::Frame;
use super::group::Group;
pub struct Window {
bounds: Rect,
frame: Frame,
interior: Group,
state: StateFlags,
options: u16,
/// Drag start position (relative to mouse when drag started)
drag_offset: Option<Point>,
/// Resize start size (size when resize drag started)
resize_start_size: Option<Point>,
/// Minimum window size (matches Borland's minWinSize)
min_size: Point,
/// Saved bounds for zoom/restore (matches Borland's zoomRect)
zoom_rect: Rect,
/// Previous bounds (for calculating union rect for redrawing)
/// Matches Borland: TView::locate() calculates union of old and new bounds
prev_bounds: Option<Rect>,
}
impl Window {
/// Create a new TWindow with blue palette (default Borland TWindow behavior)
/// Matches Borland: TWindow constructor sets palette(wpBlueWindow)
/// For TDialog (gray palette), use new_for_dialog() instead
pub fn new(bounds: Rect, title: &str) -> Self {
Self::new_with_palette(bounds, title, super::frame::FramePaletteType::Editor, colors::EDITOR_NORMAL)
}
/// Create a window for TDialog with gray palette
/// Matches Borland: TDialog overrides TWindow palette to use cpGrayDialog
pub(crate) fn new_for_dialog(bounds: Rect, title: &str) -> Self {
Self::new_with_palette(bounds, title, super::frame::FramePaletteType::Dialog, colors::DIALOG_NORMAL)
}
fn new_with_palette(bounds: Rect, title: &str, frame_palette: super::frame::FramePaletteType, interior_color: crate::core::palette::Attr) -> Self {
use crate::core::state::{OF_SELECTABLE, OF_TOP_SELECT, OF_TILEABLE};
let frame = Frame::with_palette(bounds, title, frame_palette);
// Interior bounds are ABSOLUTE (inset by 1 from window bounds for frame)
let mut interior_bounds = bounds;
interior_bounds.grow(-1, -1);
let interior = Group::with_background(interior_bounds, interior_color);
Self {
bounds,
frame,
interior,
state: SF_SHADOW, // Windows have shadows by default
options: OF_SELECTABLE | OF_TOP_SELECT | OF_TILEABLE, // Matches Borland: TWindow/TEditWindow flags
drag_offset: None,
resize_start_size: None,
min_size: Point::new(16, 6), // Minimum size: 16 wide, 6 tall (matches Borland's minWinSize)
zoom_rect: bounds, // Initialize to current bounds
prev_bounds: None,
}
}
pub fn add(&mut self, view: Box<dyn View>) -> usize {
self.interior.add(view)
}
pub fn set_initial_focus(&mut self) {
self.interior.set_initial_focus();
}
/// Set the window title
/// Matches Borland: TWindow allows title mutation via setTitle()
/// The frame will be redrawn on the next draw() call
pub fn set_title(&mut self, title: &str) {
self.frame.set_title(title);
}
/// Set minimum window size (matches Borland: minWinSize)
/// Prevents window from being resized smaller than these dimensions
pub fn set_min_size(&mut self, min_size: Point) {
self.min_size = min_size;
}
/// Get size limits for this window
/// Matches Borland: TWindow::sizeLimits(TPoint &min, TPoint &max)
/// Returns (min, max) where max is typically the desktop size
pub fn size_limits(&self) -> (Point, Point) {
// Max size would typically be the desktop/owner size
// For now, return a large max (similar to Borland's INT_MAX approach)
let max = Point::new(999, 999);
(self.min_size, max)
}
/// Set the maximum size for zoom operations
/// Typically set to desktop size when added to desktop
pub fn set_max_size(&mut self, _max_size: Point) {
// Store max size as zoom_rect if we want to zoom to it
// For now, we'll calculate it dynamically in zoom()
}
/// Set focus to a specific child by index
/// Matches Borland: owner->setCurrent(this, normalSelect)
pub fn set_focus_to_child(&mut self, index: usize) {
// Clear focus from all children first
self.interior.clear_all_focus();
// Set focus to the specified child (updates both focused index and focus state)
self.interior.set_focus_to(index);
}
/// Get the number of child views in the interior
pub fn child_count(&self) -> usize {
self.interior.len()
}
/// Get a reference to a child view by index
pub fn child_at(&self, index: usize) -> &dyn View {
self.interior.child_at(index)
}
/// Get a mutable reference to a child view by index
pub fn child_at_mut(&mut self, index: usize) -> &mut dyn View {
self.interior.child_at_mut(index)
}
/// Get the union rect of current and previous bounds (for redrawing)
/// Matches Borland: TView::locate() calculates union rect
/// Returns None if window hasn't moved yet
pub fn get_redraw_union(&self) -> Option<Rect> {
self.prev_bounds.map(|prev| {
// Union of old and new bounds, including shadows
let mut union = prev.union(&self.bounds);
// Expand by 1 on right and bottom for shadow
// Matches Borland: TView::shadowSize
union.b.x += 1;
union.b.y += 1;
union
})
}
/// Clear the movement tracking (call after redraw)
pub fn clear_move_tracking(&mut self) {
self.prev_bounds = None;
}
/// Execute a modal event loop
/// Delegates to the interior Group's execute() method
/// Matches Borland: Window and Dialog both inherit TGroup's execute()
pub fn execute(&mut self, app: &mut crate::app::Application) -> crate::core::command::CommandId {
self.interior.execute(app)
}
/// End the modal event loop
/// Delegates to the interior Group's end_modal() method
pub fn end_modal(&mut self, command: crate::core::command::CommandId) {
self.interior.end_modal(command);
}
/// Get the current end_state from the interior Group
/// Used by Dialog to check if the modal loop should end
pub fn get_end_state(&self) -> crate::core::command::CommandId {
self.interior.get_end_state()
}
/// Set the end_state in the interior Group
/// Used by modal dialogs to signal they want to close
pub fn set_end_state(&mut self, command: crate::core::command::CommandId) {
self.interior.set_end_state(command);
}
}
impl View for Window {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
self.frame.set_bounds(bounds);
// Update interior bounds (absolute, inset by 1 for frame)
let mut interior_bounds = bounds;
interior_bounds.grow(-1, -1);
self.interior.set_bounds(interior_bounds);
}
fn draw(&mut self, terminal: &mut Terminal) {
self.frame.draw(terminal);
self.interior.draw(terminal);
// Draw shadow if enabled
if self.has_shadow() {
draw_shadow(terminal, self.bounds, SHADOW_ATTR);
}
}
fn update_cursor(&self, terminal: &mut Terminal) {
// Propagate cursor update to interior group
self.interior.update_cursor(terminal);
}
fn handle_event(&mut self, event: &mut Event) {
// First, let the frame handle the event (for close button clicks, drag start, etc.)
self.frame.handle_event(event);
// Check if frame started dragging or resizing
let frame_dragging = (self.frame.state() & SF_DRAGGING) != 0;
let frame_resizing = (self.frame.state() & SF_RESIZING) != 0;
if frame_dragging && self.drag_offset.is_none() {
// Frame just started dragging - record offset
if event.what == EventType::MouseDown || event.what == EventType::MouseMove {
let mouse_pos = event.mouse.pos;
self.drag_offset = Some(Point::new(
mouse_pos.x - self.bounds.a.x,
mouse_pos.y - self.bounds.a.y,
));
self.state |= SF_DRAGGING;
}
}
if frame_resizing && self.resize_start_size.is_none() {
// Frame just started resizing - record initial size
if event.what == EventType::MouseDown || event.what == EventType::MouseMove {
let mouse_pos = event.mouse.pos;
// Calculate offset from bottom-right corner
// Borland: p = size - event.mouse.where (tview.cc:235)
self.resize_start_size = Some(Point::new(
self.bounds.b.x - mouse_pos.x,
self.bounds.b.y - mouse_pos.y,
));
self.state |= SF_RESIZING;
}
}
// Handle mouse move during drag
if frame_dragging && self.drag_offset.is_some() {
if event.what == EventType::MouseMove {
let mouse_pos = event.mouse.pos;
let offset = self.drag_offset.unwrap();
// Calculate new position
let new_x = mouse_pos.x - offset.x;
let new_y = mouse_pos.y - offset.y;
// Save previous bounds for union rect calculation (Borland's locate pattern)
self.prev_bounds = Some(self.bounds);
// Update bounds (maintaining size)
let width = self.bounds.width();
let height = self.bounds.height();
self.bounds = Rect::new(new_x, new_y, new_x + width, new_y + height);
// Update frame and interior bounds
self.frame.set_bounds(self.bounds);
let mut interior_bounds = self.bounds;
interior_bounds.grow(-1, -1);
self.interior.set_bounds(interior_bounds);
event.clear(); // Mark event as handled
return;
}
}
// Handle mouse move during resize
if frame_resizing && self.resize_start_size.is_some() {
if event.what == EventType::MouseMove {
let mouse_pos = event.mouse.pos;
let offset = self.resize_start_size.unwrap();
// Calculate new size (Borland: event.mouse.where += p, then use as size)
let new_width = (mouse_pos.x + offset.x - self.bounds.a.x) as u16;
let new_height = (mouse_pos.y + offset.y - self.bounds.a.y) as u16;
// Apply size constraints (Borland: sizeLimits)
let (min, max) = self.size_limits();
let final_width = new_width.max(min.x as u16).min(max.x as u16);
let final_height = new_height.max(min.y as u16).min(max.y as u16);
// Save previous bounds for union rect calculation
self.prev_bounds = Some(self.bounds);
// Update bounds (maintaining position, changing size)
self.bounds.b.x = self.bounds.a.x + final_width as i16;
self.bounds.b.y = self.bounds.a.y + final_height as i16;
// Update frame and interior bounds
self.frame.set_bounds(self.bounds);
let mut interior_bounds = self.bounds;
interior_bounds.grow(-1, -1);
self.interior.set_bounds(interior_bounds);
event.clear(); // Mark event as handled
return;
}
}
// Check if frame ended dragging
if !frame_dragging && self.drag_offset.is_some() {
self.drag_offset = None;
self.state &= !SF_DRAGGING;
}
// Check if frame ended resizing
if !frame_resizing && self.resize_start_size.is_some() {
self.resize_start_size = None;
self.state &= !SF_RESIZING;
}
// Handle ESC key for modal windows
// Modal windows should close when ESC or ESC ESC is pressed
if event.what == EventType::Keyboard {
let is_esc = event.key_code == crate::core::event::KB_ESC;
let is_esc_esc = event.key_code == crate::core::event::KB_ESC_ESC;
if (is_esc || is_esc_esc) && (self.state & SF_MODAL) != 0 {
// Modal window: ESC ends the modal loop with CM_CANCEL
self.end_modal(CM_CANCEL);
event.clear();
return;
}
}
// Handle CM_CLOSE command (Borland: twindow.cc lines 104-118)
// Frame generates CM_CLOSE when close button is clicked
// Matches Borland: TWindow::handleEvent calls close(), which calls destroy(this)
if event.what == EventType::Command && event.command == CM_CLOSE {
use crate::core::state::SF_CLOSED;
// Check if this window is modal
if (self.state & SF_MODAL) != 0 {
// Modal window: convert CM_CLOSE to CM_CANCEL
// Borland: event.message.command = cmCancel; putEvent(event);
*event = Event::command(CM_CANCEL);
// Don't clear event - let it propagate to dialog's execute loop
} else {
// Non-modal window: close itself (Borland: TWindow::close() calls destroy(this))
// In Rust, we mark with SF_CLOSED flag and let app.desktop.remove_closed_windows() handle it
// TODO: Add valid(cmClose) support for validation (e.g., "Save before closing?")
self.state |= SF_CLOSED;
event.clear();
}
return;
}
// Then let the interior handle it (if not already handled)
self.interior.handle_event(event);
}
fn can_focus(&self) -> bool {
true
}
fn set_focus(&mut self, focused: bool) {
// Propagate focus to the interior group
// When the window gets focus, set focus on its first focusable child
if focused {
self.interior.set_initial_focus();
} else {
self.interior.clear_all_focus();
}
}
fn state(&self) -> StateFlags {
self.state
}
fn set_state(&mut self, state: StateFlags) {
self.state = state;
}
fn options(&self) -> u16 {
self.options
}
fn set_options(&mut self, options: u16) {
self.options = options;
}
fn get_end_state(&self) -> crate::core::command::CommandId {
self.interior.get_end_state()
}
fn set_end_state(&mut self, command: crate::core::command::CommandId) {
self.interior.set_end_state(command);
}
/// Zoom (maximize) or restore window
/// Matches Borland: TWindow::zoom() toggles between current size and maximum size
/// In Borland, this is called by owner in response to cmZoom command
fn zoom(&mut self, max_bounds: Rect) {
let (_min, _max_size) = self.size_limits();
let current_size = Point::new(self.bounds.width(), self.bounds.height());
// If not at max size, zoom to max
if current_size.x != max_bounds.width() || current_size.y != max_bounds.height() {
// Save current bounds for restore
self.zoom_rect = self.bounds;
// Save previous bounds for redraw union
self.prev_bounds = Some(self.bounds);
// Zoom to max size (typically desktop bounds)
self.bounds = max_bounds;
} else {
// Restore to saved bounds
self.prev_bounds = Some(self.bounds);
self.bounds = self.zoom_rect;
}
// Update frame and interior
self.frame.set_bounds(self.bounds);
let mut interior_bounds = self.bounds;
interior_bounds.grow(-1, -1);
self.interior.set_bounds(interior_bounds);
}
/// Validate window before closing with given command
/// Matches Borland: TWindow inherits TGroup::valid() which validates all children
/// Delegates to interior group to validate all children
fn valid(&mut self, command: crate::core::command::CommandId) -> bool {
self.interior.valid(command)
}
}
/// Builder for creating windows with a fluent API.
///
/// # Examples
///
/// ```
/// use turbo_vision::views::window::WindowBuilder;
/// use turbo_vision::views::button::ButtonBuilder;
/// use turbo_vision::core::geometry::Rect;
/// use turbo_vision::core::command::CM_OK;
///
/// let mut window = WindowBuilder::new()
/// .bounds(Rect::new(10, 5, 60, 20))
/// .title("My Window")
/// .build();
///
/// // Add a button to the window
/// let ok_button = ButtonBuilder::new()
/// .bounds(Rect::new(10, 10, 20, 12))
/// .title("OK")
/// .command(CM_OK)
/// .build();
/// window.add(Box::new(ok_button));
/// ```
pub struct WindowBuilder {
bounds: Option<Rect>,
title: Option<String>,
}
impl WindowBuilder {
/// Creates a new WindowBuilder with default values.
pub fn new() -> Self {
Self {
bounds: None,
title: None,
}
}
/// Sets the window bounds (required).
#[must_use]
pub fn bounds(mut self, bounds: Rect) -> Self {
self.bounds = Some(bounds);
self
}
/// Sets the window title (required).
#[must_use]
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
/// Builds the Window.
///
/// # Panics
///
/// Panics if required fields (bounds, title) are not set.
pub fn build(self) -> Window {
let bounds = self.bounds.expect("Window bounds must be set");
let title = self.title.expect("Window title must be set");
Window::new(bounds, &title)
}
}
impl Default for WindowBuilder {
fn default() -> Self {
Self::new()
}
}