turbo-vision 1.0.7

A Rust implementation of the classic Borland Turbo Vision text-mode UI framework
Documentation
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
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
// (C) 2025 - Enzo Lombardi

//! View trait - base interface for all UI components with event handling and drawing.

use crate::core::command::CommandId;
use crate::core::draw::DrawBuffer;
use crate::core::event::Event;
use crate::core::geometry::Rect;
use crate::core::state::{StateFlags, SF_FOCUSED, SF_SHADOW, SHADOW_ATTR, shadow_size};
use crate::terminal::Terminal;
use std::io;
use std::sync::atomic::{AtomicUsize, Ordering};

/// Unique identifier for a view within a Group
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ViewId(usize);

impl ViewId {
    /// Generate a new unique ViewId
    pub(crate) fn new() -> Self {
        static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
        ViewId(NEXT_ID.fetch_add(1, Ordering::Relaxed))
    }
}

/// Owner context for palette remapping
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum OwnerType {
    None,       // Top-level view (Application)
    Window,     // Inside a Window (blue palette)
    CyanWindow, // Inside a Cyan Window (help windows)
    Dialog,     // Inside a Dialog
}

/// 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:**
/// ```ignore
/// 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() {
            let ss = shadow_size();
            bounds.b.x += ss.0;
            bounds.b.y += ss.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)
    }

    /// Draw shadow for this view
    /// Draws a shadow offset dynamically based on terminal cell aspect ratio
    /// Shadow is semi-transparent - darkens the underlying content by 50%
    /// This matches the Borland Turbo Vision behavior more closely
    fn draw_shadow(&self, terminal: &mut Terminal) {
        use crate::core::palette::Attr;

        const SHADOW_FACTOR: f32 = 0.5; // Darken to 50% of original brightness

        let bounds = self.bounds();
        let ss = shadow_size();
        let mut buf = DrawBuffer::new(ss.0 as usize);

        // Draw right edge shadow (ss.0 columns wide, offset by ss.1 vertically)
        // Read existing cells and darken them for semi-transparency
        for y in (bounds.a.y + ss.1)..(bounds.b.y + ss.1) {
            for i in 0..ss.0 {
                let x = bounds.b.x + i;

                // Read the existing cell at this position
                if let Some(existing_cell) = terminal.read_cell(x, y) {
                    // Darken the existing cell's attribute
                    let darkened_attr = existing_cell.attr.darken(SHADOW_FACTOR);
                    buf.put_char(i as usize, existing_cell.ch, darkened_attr);
                } else {
                    // Out of bounds - use default shadow
                    let default_attr = Attr::from_u8(SHADOW_ATTR);
                    buf.put_char(i as usize, ' ', default_attr);
                }
            }
            write_line_to_terminal(terminal, bounds.b.x, y, &buf);
        }

        // Draw bottom edge shadow (offset by ss.0 horizontally, excludes right shadow area to prevent double-darkening)
        let bottom_width = (bounds.b.x - bounds.a.x - ss.0) as usize;
        let mut bottom_buf = DrawBuffer::new(bottom_width);

        let shadow_y = bounds.b.y;
        for i in 0..bottom_width {
            let x = bounds.a.x + ss.0 + i as i16;

            // Read the existing cell at this position
            if let Some(existing_cell) = terminal.read_cell(x, shadow_y) {
                // Darken the existing cell's attribute
                let darkened_attr = existing_cell.attr.darken(SHADOW_FACTOR);
                bottom_buf.put_char(i, existing_cell.ch, darkened_attr);
            } else {
                // Out of bounds - use default shadow
                let default_attr = Attr::from_u8(SHADOW_ATTR);
                bottom_buf.put_char(i, ' ', default_attr);
            }
        }
        write_line_to_terminal(terminal, bounds.a.x + ss.0, bounds.b.y, &bottom_buf);
    }

    /// Get the linked control ViewId for labels
    /// Matches Borland: TLabel::link field
    /// Returns Some(ViewId) 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<ViewId> {
        None // Default: not a label or no link
    }

    /// Initialize internal owner pointers after view is added to parent and won't move
    /// This is called by parent's add() method after the view is in its final position
    /// Views that contain other views by value should override this to set up owner chains
    /// Default implementation does nothing
    fn init_after_add(&mut self) {
        // Default: no action needed
    }

    /// Constrain view bounds to parent/owner bounds
    /// Used after positioning (e.g., centering) to ensure view stays within valid area
    /// Matches Borland: TView::locate() constrains position to owner bounds
    fn constrain_to_parent_bounds(&mut self) {
        // Default: no action needed (only windows need this)
    }

    /// Set the owner (parent) of this view
    /// Matches Borland: TView::owner field
    /// Called by Group when adding a child
    fn set_owner(&mut self, _owner: *const dyn View) {
        // Default: do nothing (views that need owner support will override)
    }

    /// Get the owner (parent) of this view
    /// Matches Borland: TView::owner field
    /// Returns None if this view has no owner or doesn't track it
    fn get_owner(&self) -> Option<*const dyn View> {
        None // Default: no owner
    }

    /// Get the owner type for palette remapping
    /// This allows views to know their context (Window vs Dialog)
    fn get_owner_type(&self) -> OwnerType {
        OwnerType::None // Default: no owner
    }

    /// Set the owner type for palette remapping
    fn set_owner_type(&mut self, _owner_type: OwnerType) {
        // Default: do nothing (views that need context will override)
    }

    /// Get this view's palette for the Borland indirect palette system
    /// Matches Borland: TView::getPalette()
    ///
    /// Returns a Palette that maps this view's logical color indices to the parent's indices.
    /// When resolving colors, the system walks up the owner chain remapping through palettes
    /// until reaching the Application which has actual color attributes.
    ///
    /// # Returns
    /// * `Some(Palette)` - This view has a palette for color remapping
    /// * `None` - This view has no palette (transparent to color mapping)
    fn get_palette(&self) -> Option<crate::core::palette::Palette>;

    /// Map a logical color index to an actual color attribute
    /// Matches Borland: TView::mapColor(uchar index)
    ///
    /// Walks up the owner chain, remapping the color index through each view's palette
    /// until reaching a view with no owner (Application), which provides actual attributes.
    ///
    /// # Arguments
    /// * `color_index` - Logical color index (1-based, 0 = error color)
    ///
    /// # Returns
    /// The final color attribute
    fn map_color(&self, color_index: u8) -> crate::core::palette::Attr {
        use crate::core::palette::{palettes, Attr};

        // Borland's errorAttr = 0xCF (Light Red/Magenta background, White foreground)
        const ERROR_ATTR: u8 = 0xCF;

        if color_index == 0 {
            return Attr::from_u8(ERROR_ATTR);
        }

        let mut color = color_index;

        // Step 1: Remap through this view's own palette
        if let Some(palette) = self.get_palette() {
            if !palette.is_empty() {
                if color as usize > palette.len() {
                    return Attr::from_u8(ERROR_ATTR);
                }
                color = palette.get(color as usize);
                if color == 0 {
                    return Attr::from_u8(ERROR_ATTR);
                }
            }
        }

        // Step 2: Walk up the owner chain, remapping through each owner's palette.
        // Matches Borland: TView::mapColor() traverses owner->getPalette() up to
        // TApplication. Views without a palette (get_palette returns None) are
        // transparent. The chain stops when there's no owner.
        //
        // Safety: owner pointers are refreshed in Window::draw() every frame,
        // so they are always valid during rendering. Views outside a Window
        // (MenuBar, StatusLine, Desktop background) have no owner and skip
        // this step, going directly to the app palette.
        let mut current_owner = self.get_owner();
        while let Some(owner_ptr) = current_owner {
            let owner = unsafe { &*owner_ptr };
            if let Some(palette) = owner.get_palette() {
                if !palette.is_empty() && (color as usize) <= palette.len() {
                    let remapped = palette.get(color as usize);
                    if remapped == 0 {
                        return Attr::from_u8(ERROR_ATTR);
                    }
                    color = remapped;
                }
            }
            current_owner = owner.get_owner();
        }

        // Step 3: Resolve through application palette (1-indexed)
        let app_palette_data = palettes::get_app_palette();
        let app_index = (color as usize).wrapping_sub(1);
        if app_index < app_palette_data.len() {
            let final_color = app_palette_data[app_index];
            if final_color == 0 {
                return Attr::from_u8(ERROR_ATTR);
            }
            Attr::from_u8(final_color)
        } else {
            Attr::from_u8(ERROR_ATTR)
        }
    }
}

/// Trait for views that need idle processing (animations, timers, etc.)
/// These views have their idle() method called periodically even during modal dialogs,
/// matching Borland's TProgram::idle() behavior which continues running during execView().
///
/// # Examples
///
/// ```ignore
/// use turbo_vision::views::{View, IdleView};
/// use turbo_vision::terminal::Terminal;
/// use std::time::Instant;
///
/// struct AnimatedWidget {
///     position: usize,
///     last_update: Instant,
///     // ... other View fields
/// }
///
/// impl IdleView for AnimatedWidget {
///     fn idle(&mut self) {
///         if self.last_update.elapsed().as_millis() > 100 {
///             self.position = (self.position + 1) % 10;
///             self.last_update = Instant::now();
///         }
///     }
/// }
/// ```
pub trait IdleView: View {
    /// Called periodically to update animation state, timers, etc.
    /// Matches Borland: TProgram::idle() continues running even during modal dialogs
    fn idle(&mut self);
}

/// 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 {
        return;
    }
    terminal.write_line(x.max(0) as u16, y as u16, &buf.data);
}

/// Draw shadow for arbitrary bounds (for non-view elements like temporary dropdowns)
///
/// Note: Views should use the `draw_shadow()` trait method instead, which gets bounds
/// from `self.bounds()` following the principle "bounds should not be passed down".
/// This standalone function is only for special cases where you're drawing shadows
/// for elements that aren't views (e.g., temporary dropdowns).
pub fn draw_shadow_bounds(terminal: &mut Terminal, bounds: Rect) {
    use crate::core::palette::Attr;

    const SHADOW_FACTOR: f32 = 0.5; // Darken to 50% of original brightness

    let ss = shadow_size();
    let mut buf = DrawBuffer::new(ss.0 as usize);

    // Draw right edge shadow (ss.0 columns wide, offset by ss.1 vertically)
    // Read existing cells and darken them for semi-transparency
    for y in (bounds.a.y + ss.1)..(bounds.b.y + ss.1) {
        for i in 0..ss.0 {
            let x = bounds.b.x + i;

            // Read the existing cell at this position
            if let Some(existing_cell) = terminal.read_cell(x, y) {
                // Darken the existing cell's attribute
                let darkened_attr = existing_cell.attr.darken(SHADOW_FACTOR);
                buf.put_char(i as usize, existing_cell.ch, darkened_attr);
            } else {
                // Out of bounds - use default shadow
                let default_attr = Attr::from_u8(SHADOW_ATTR);
                buf.put_char(i as usize, ' ', default_attr);
            }
        }
        write_line_to_terminal(terminal, bounds.b.x, y, &buf);
    }

    // Draw bottom edge shadow (offset by ss.0 horizontally, excludes right shadow area to prevent double-darkening)
    let bottom_width = (bounds.b.x - bounds.a.x - ss.0) as usize;
    let mut bottom_buf = DrawBuffer::new(bottom_width);

    let shadow_y = bounds.b.y;
    for i in 0..bottom_width {
        let x = bounds.a.x + ss.0 + i as i16;

        // Read the existing cell at this position
        if let Some(existing_cell) = terminal.read_cell(x, shadow_y) {
            // Darken the existing cell's attribute
            let darkened_attr = existing_cell.attr.darken(SHADOW_FACTOR);
            bottom_buf.put_char(i, existing_cell.ch, darkened_attr);
        } else {
            // Out of bounds - use default shadow
            let default_attr = Attr::from_u8(SHADOW_ATTR);
            bottom_buf.put_char(i, ' ', default_attr);
        }
    }
    write_line_to_terminal(terminal, bounds.a.x + ss.0, bounds.b.y, &bottom_buf);
}