Skip to main content

turbo_vision/views/
view.rs

1// (C) 2025 - Enzo Lombardi
2
3//! View trait - base interface for all UI components with event handling and drawing.
4
5use crate::core::command::CommandId;
6use crate::core::draw::DrawBuffer;
7use crate::core::event::Event;
8use crate::core::geometry::Rect;
9use crate::core::state::{StateFlags, SF_FOCUSED, SF_SHADOW, SHADOW_ATTR, shadow_size};
10use crate::terminal::Terminal;
11use std::io;
12use std::sync::atomic::{AtomicUsize, Ordering};
13
14/// Unique identifier for a view within a Group
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct ViewId(usize);
17
18impl ViewId {
19    /// Generate a new unique ViewId
20    pub(crate) fn new() -> Self {
21        static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
22        ViewId(NEXT_ID.fetch_add(1, Ordering::Relaxed))
23    }
24
25    /// Get the ViewId as a u16 for embedding in event fields.
26    /// ViewId values are small sequential numbers that fit in u16.
27    #[allow(clippy::cast_possible_truncation)]
28    pub fn as_u16(self) -> u16 {
29        self.0 as u16
30    }
31
32    /// Reconstruct a ViewId from a u16 value.
33    pub fn from_u16(val: u16) -> Self {
34        ViewId(val as usize)
35    }
36}
37
38/// View trait - all UI components implement this
39///
40/// ## Owner/Parent Communication Pattern
41///
42/// Unlike Borland's TView which stores an `owner` pointer to the parent TGroup,
43/// Rust views communicate with parents through event propagation:
44///
45/// **Borland Pattern:**
46/// ```cpp
47/// void TButton::press() {
48///     message(owner, evBroadcast, command, this);
49/// }
50/// ```
51///
52/// **Rust Pattern:**
53/// ```ignore
54/// fn handle_event(&mut self, event: &mut Event) {
55///     // Transform event to send message upward
56///     *event = Event::command(self.command);
57///     // Event bubbles up through Group::handle_event() call stack
58/// }
59/// ```
60///
61/// This achieves the same result (child-to-parent communication) without raw pointers,
62/// using Rust's ownership system and the call stack for context.
63pub trait View {
64    fn bounds(&self) -> Rect;
65    fn set_bounds(&mut self, bounds: Rect);
66    fn draw(&mut self, terminal: &mut Terminal);
67    fn handle_event(&mut self, event: &mut Event);
68    fn can_focus(&self) -> bool {
69        false
70    }
71
72    /// Set focus state - default implementation uses SF_FOCUSED flag
73    /// Views should override only if they need custom focus behavior
74    fn set_focus(&mut self, focused: bool) {
75        self.set_state_flag(SF_FOCUSED, focused);
76    }
77
78    /// Check if view is focused - reads SF_FOCUSED flag
79    fn is_focused(&self) -> bool {
80        self.get_state_flag(SF_FOCUSED)
81    }
82
83    /// Get view option flags (OF_SELECTABLE, OF_PRE_PROCESS, OF_POST_PROCESS, etc.)
84    fn options(&self) -> u16 {
85        0
86    }
87
88    /// Set view option flags
89    fn set_options(&mut self, _options: u16) {}
90
91    /// Get view state flags
92    fn state(&self) -> StateFlags {
93        0
94    }
95
96    /// Set view state flags
97    fn set_state(&mut self, _state: StateFlags) {}
98
99    /// Set or clear specific state flag(s)
100    /// Matches Borland's TView::setState(ushort aState, Boolean enable)
101    /// If enable is true, sets the flag(s), otherwise clears them
102    fn set_state_flag(&mut self, flag: StateFlags, enable: bool) {
103        let current = self.state();
104        if enable {
105            self.set_state(current | flag);
106        } else {
107            self.set_state(current & !flag);
108        }
109    }
110
111    /// Check if specific state flag(s) are set
112    /// Matches Borland's TView::getState(ushort aState)
113    fn get_state_flag(&self, flag: StateFlags) -> bool {
114        (self.state() & flag) == flag
115    }
116
117    /// Check if view has shadow enabled
118    fn has_shadow(&self) -> bool {
119        (self.state() & SF_SHADOW) != 0
120    }
121
122    /// Get bounds including shadow area
123    fn shadow_bounds(&self) -> Rect {
124        let mut bounds = self.bounds();
125        if self.has_shadow() {
126            let ss = shadow_size();
127            bounds.b.x += ss.0;
128            bounds.b.y += ss.1;
129        }
130        bounds
131    }
132
133    /// Update cursor state (called after draw)
134    /// Views that need to show a cursor when focused should override this
135    fn update_cursor(&self, _terminal: &mut Terminal) {
136        // Default: do nothing (cursor stays hidden)
137    }
138
139    /// Zoom (maximize/restore) the view with given maximum bounds
140    /// Matches Borland: TWindow::zoom() toggles between current and max size
141    /// Default implementation does nothing (only windows support zoom)
142    fn zoom(&mut self, _max_bounds: Rect) {
143        // Default: do nothing (only Window implements zoom)
144    }
145
146    /// Validate the view before performing a command (usually closing)
147    /// Matches Borland: TView::valid(ushort command) - returns Boolean
148    /// Returns true if the view's state is valid for the given command
149    /// Used for "Save before closing?" type scenarios and input validation
150    ///
151    /// # Arguments
152    /// * `command` - The command being performed (CM_OK, CM_CANCEL, CM_RELEASED_FOCUS, etc.)
153    ///
154    /// # Returns
155    /// * `true` - View state is valid, command can proceed
156    /// * `false` - View state is invalid, command should be blocked
157    ///
158    /// Default implementation always returns true (no validation)
159    fn valid(&mut self, _command: crate::core::command::CommandId) -> bool {
160        true
161    }
162
163    /// Downcast to concrete type (immutable)
164    /// Allows accessing specific view type methods from trait object
165    fn as_any(&self) -> &dyn std::any::Any {
166        panic!("as_any() not implemented for this view type")
167    }
168
169    /// Downcast to concrete type (mutable)
170    /// Allows accessing specific view type methods from trait object
171    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
172        panic!("as_any_mut() not implemented for this view type")
173    }
174
175    /// Dump this view's region of the terminal buffer to an ANSI file for debugging
176    fn dump_to_file(&self, terminal: &Terminal, path: &str) -> io::Result<()> {
177        let bounds = self.shadow_bounds();
178        terminal.dump_region(
179            bounds.a.x as u16,
180            bounds.a.y as u16,
181            (bounds.b.x - bounds.a.x) as u16,
182            (bounds.b.y - bounds.a.y) as u16,
183            path,
184        )
185    }
186
187    /// Check if this view is a default button (for Enter key handling at Dialog level)
188    /// Corresponds to Borland's TButton::amDefault flag (tbutton.cc line 239)
189    fn is_default_button(&self) -> bool {
190        false
191    }
192
193    /// Get the command ID for this button (if it's a button)
194    /// Returns None if not a button
195    /// Used by Dialog to activate default button on Enter key
196    fn button_command(&self) -> Option<u16> {
197        None
198    }
199
200    /// Set the selection index for listbox views
201    /// Only implemented by ListBox, other views ignore this
202    fn set_list_selection(&mut self, _index: usize) {
203        // Default: do nothing (not a listbox)
204    }
205
206    /// Get the selection index for listbox views
207    /// Only implemented by ListBox, other views return 0
208    fn get_list_selection(&self) -> usize {
209        0
210    }
211
212    /// Get the union rect of previous and current bounds for redrawing
213    /// Matches Borland: TView::locate() calculates union of old and new bounds
214    /// Returns None if the view hasn't moved since last redraw
215    /// Used by Desktop to implement Borland's drawUnderRect pattern
216    fn get_redraw_union(&self) -> Option<Rect> {
217        None // Default: no movement tracking
218    }
219
220    /// Clear movement tracking after redrawing
221    /// Matches Borland: Called after drawUnderRect completes
222    fn clear_move_tracking(&mut self) {
223        // Default: do nothing (no movement tracking)
224    }
225
226    /// Get the end state for modal views
227    /// Matches Borland: TGroup::endState field
228    /// Returns the command ID that ended modal execution (0 if still running)
229    fn get_end_state(&self) -> CommandId {
230        0 // Default: not ended
231    }
232
233    /// Set the end state for modal views
234    /// Called by end_modal() to signal the modal loop should exit
235    fn set_end_state(&mut self, _command: CommandId) {
236        // Default: do nothing (only modal views need this)
237    }
238
239    /// Convert local coordinates to global (screen) coordinates
240    /// Matches Borland: TView::makeGlobal(TPoint source, TPoint& dest)
241    ///
242    /// In Borland, makeGlobal traverses the owner chain and accumulates offsets.
243    /// In this Rust implementation, views store absolute bounds (converted in Group::add()),
244    /// so we simply add the view's origin to the local coordinates.
245    ///
246    /// # Arguments
247    /// * `local_x` - X coordinate relative to view's interior (0,0 = top-left of view)
248    /// * `local_y` - Y coordinate relative to view's interior
249    ///
250    /// # Returns
251    /// Global (screen) coordinates as (x, y) tuple
252    fn make_global(&self, local_x: i16, local_y: i16) -> (i16, i16) {
253        let bounds = self.bounds();
254        (bounds.a.x + local_x, bounds.a.y + local_y)
255    }
256
257    /// Convert global (screen) coordinates to local view coordinates
258    /// Matches Borland: TView::makeLocal(TPoint source, TPoint& dest)
259    ///
260    /// In Borland, makeLocal is the inverse of makeGlobal, converting screen
261    /// coordinates back to view-relative coordinates.
262    ///
263    /// # Arguments
264    /// * `global_x` - X coordinate in screen space
265    /// * `global_y` - Y coordinate in screen space
266    ///
267    /// # Returns
268    /// Local coordinates as (x, y) tuple, where (0,0) is the view's top-left
269    fn make_local(&self, global_x: i16, global_y: i16) -> (i16, i16) {
270        let bounds = self.bounds();
271        (global_x - bounds.a.x, global_y - bounds.a.y)
272    }
273
274    /// Draw shadow for this view
275    /// Draws a shadow offset dynamically based on terminal cell aspect ratio
276    /// Shadow is semi-transparent - darkens the underlying content by 50%
277    /// This matches the Borland Turbo Vision behavior more closely
278    fn draw_shadow(&self, terminal: &mut Terminal) {
279        use crate::core::palette::Attr;
280
281        const SHADOW_FACTOR: f32 = 0.5; // Darken to 50% of original brightness
282
283        let bounds = self.bounds();
284        let ss = shadow_size();
285        let mut buf = DrawBuffer::new(ss.0 as usize);
286
287        // Draw right edge shadow (ss.0 columns wide, offset by ss.1 vertically)
288        // Read existing cells and darken them for semi-transparency
289        for y in (bounds.a.y + ss.1)..(bounds.b.y + ss.1) {
290            for i in 0..ss.0 {
291                let x = bounds.b.x + i;
292
293                // Read the existing cell at this position
294                if let Some(existing_cell) = terminal.read_cell(x, y) {
295                    // Darken the existing cell's attribute
296                    let darkened_attr = existing_cell.attr.darken(SHADOW_FACTOR);
297                    buf.put_char(i as usize, existing_cell.ch, darkened_attr);
298                } else {
299                    // Out of bounds - use default shadow
300                    let default_attr = Attr::from_u8(SHADOW_ATTR);
301                    buf.put_char(i as usize, ' ', default_attr);
302                }
303            }
304            write_line_to_terminal(terminal, bounds.b.x, y, &buf);
305        }
306
307        // Draw bottom edge shadow (offset by ss.0 horizontally, excludes right shadow area to prevent double-darkening)
308        let bottom_width = (bounds.b.x - bounds.a.x - ss.0) as usize;
309        let mut bottom_buf = DrawBuffer::new(bottom_width);
310
311        let shadow_y = bounds.b.y;
312        for i in 0..bottom_width {
313            let x = bounds.a.x + ss.0 + i as i16;
314
315            // Read the existing cell at this position
316            if let Some(existing_cell) = terminal.read_cell(x, shadow_y) {
317                // Darken the existing cell's attribute
318                let darkened_attr = existing_cell.attr.darken(SHADOW_FACTOR);
319                bottom_buf.put_char(i, existing_cell.ch, darkened_attr);
320            } else {
321                // Out of bounds - use default shadow
322                let default_attr = Attr::from_u8(SHADOW_ATTR);
323                bottom_buf.put_char(i, ' ', default_attr);
324            }
325        }
326        write_line_to_terminal(terminal, bounds.a.x + ss.0, bounds.b.y, &bottom_buf);
327    }
328
329    /// Get the linked control ViewId for labels
330    /// Matches Borland: TLabel::link field
331    /// Returns Some(ViewId) if this is a label with a linked control, None otherwise
332    /// Used by Group to implement focus transfer when clicking labels
333    fn label_link(&self) -> Option<ViewId> {
334        None // Default: not a label or no link
335    }
336
337    /// Initialize internal owner pointers after view is added to parent and won't move
338    /// This is called by parent's add() method after the view is in its final position
339    /// Views that contain other views by value should override this to set up owner chains
340    /// Default implementation does nothing
341    fn init_after_add(&mut self) {
342        // Default: no action needed
343    }
344
345    /// Constrain view bounds to parent/owner bounds
346    /// Used after positioning (e.g., centering) to ensure view stays within valid area
347    /// Matches Borland: TView::locate() constrains position to owner bounds
348    fn constrain_to_parent_bounds(&mut self) {
349        // Default: no action needed (only windows need this)
350    }
351
352    /// Set the QCell-based palette chain node for this view.
353    /// Called by parent (Group/Window) during draw to establish the safe owner chain.
354    fn set_palette_chain(&mut self, _node: Option<crate::core::palette_chain::PaletteChainNode>) {
355        // Default: do nothing (views that need palette chain will override)
356    }
357
358    /// Get the QCell-based palette chain node for this view.
359    /// Used by `map_color()` to safely walk the owner chain.
360    fn get_palette_chain(&self) -> Option<&crate::core::palette_chain::PaletteChainNode> {
361        None // Default: no palette chain
362    }
363
364    /// Set the parent's bounds for drag/resize limit resolution.
365    /// Called by Desktop when adding windows.
366    fn set_parent_bounds(&mut self, _bounds: crate::core::geometry::Rect) {
367        // Default: do nothing (only Window needs this)
368    }
369
370    /// Get this view's palette for the Borland indirect palette system
371    /// Matches Borland: TView::getPalette()
372    ///
373    /// Returns a Palette that maps this view's logical color indices to the parent's indices.
374    /// When resolving colors, the system walks up the owner chain remapping through palettes
375    /// until reaching the Application which has actual color attributes.
376    ///
377    /// # Returns
378    /// * `Some(Palette)` - This view has a palette for color remapping
379    /// * `None` - This view has no palette (transparent to color mapping)
380    fn get_palette(&self) -> Option<crate::core::palette::Palette>;
381
382    /// Map a logical color index to an actual color attribute
383    /// Matches Borland: TView::mapColor(uchar index)
384    ///
385    /// Walks up the owner chain, remapping the color index through each view's palette
386    /// until reaching a view with no owner (Application), which provides actual attributes.
387    ///
388    /// # Arguments
389    /// * `color_index` - Logical color index (1-based, 0 = error color)
390    ///
391    /// # Returns
392    /// The final color attribute
393    fn map_color(&self, color_index: u8) -> crate::core::palette::Attr {
394        use crate::core::palette::{palettes, Attr};
395
396        // Borland's errorAttr = 0xCF (Light Red/Magenta background, White foreground)
397        const ERROR_ATTR: u8 = 0xCF;
398
399        if color_index == 0 {
400            return Attr::from_u8(ERROR_ATTR);
401        }
402
403        let mut color = color_index;
404
405        // Step 1: Remap through this view's own palette
406        if let Some(palette) = self.get_palette() {
407            if !palette.is_empty() {
408                if color as usize > palette.len() {
409                    return Attr::from_u8(ERROR_ATTR);
410                }
411                color = palette.get(color as usize);
412                if color == 0 {
413                    return Attr::from_u8(ERROR_ATTR);
414                }
415            }
416        }
417
418        // Step 2: Walk up the owner chain via QCell-based palette chain.
419        // Matches Borland: TView::mapColor() traverses owner->getPalette() up to
420        // TApplication. Views without a palette (get_palette returns None) are
421        // transparent. The chain stops when there's no parent.
422        if let Some(chain_node) = self.get_palette_chain() {
423            color = chain_node.remap_color(color);
424            if color == 0 {
425                return Attr::from_u8(ERROR_ATTR);
426            }
427        }
428        // Views without a palette chain (top-level views like MenuBar, StatusLine)
429        // skip the chain walk and go directly to the app palette.
430
431        // Step 3: Resolve through application palette (1-indexed)
432        let app_palette_data = palettes::get_app_palette();
433        let app_index = (color as usize).wrapping_sub(1);
434        if app_index < app_palette_data.len() {
435            let final_color = app_palette_data[app_index];
436            if final_color == 0 {
437                return Attr::from_u8(ERROR_ATTR);
438            }
439            Attr::from_u8(final_color)
440        } else {
441            Attr::from_u8(ERROR_ATTR)
442        }
443    }
444}
445
446/// Trait for views that need idle processing (animations, timers, etc.)
447/// These views have their idle() method called periodically even during modal dialogs,
448/// matching Borland's TProgram::idle() behavior which continues running during execView().
449///
450/// # Examples
451///
452/// ```ignore
453/// use turbo_vision::views::{View, IdleView};
454/// use turbo_vision::terminal::Terminal;
455/// use std::time::Instant;
456///
457/// struct AnimatedWidget {
458///     position: usize,
459///     last_update: Instant,
460///     // ... other View fields
461/// }
462///
463/// impl IdleView for AnimatedWidget {
464///     fn idle(&mut self) {
465///         if self.last_update.elapsed().as_millis() > 100 {
466///             self.position = (self.position + 1) % 10;
467///             self.last_update = Instant::now();
468///         }
469///     }
470/// }
471/// ```
472pub trait IdleView: View {
473    /// Called periodically to update animation state, timers, etc.
474    /// Matches Borland: TProgram::idle() continues running even during modal dialogs
475    fn idle(&mut self);
476}
477
478/// Helper to draw a line to the terminal
479pub fn write_line_to_terminal(terminal: &mut Terminal, x: i16, y: i16, buf: &DrawBuffer) {
480    if y < 0 || y >= terminal.size().1 {
481        return;
482    }
483    terminal.write_line(x.max(0) as u16, y as u16, &buf.data);
484}
485
486/// Draw shadow for arbitrary bounds (for non-view elements like temporary dropdowns)
487///
488/// Note: Views should use the `draw_shadow()` trait method instead, which gets bounds
489/// from `self.bounds()` following the principle "bounds should not be passed down".
490/// This standalone function is only for special cases where you're drawing shadows
491/// for elements that aren't views (e.g., temporary dropdowns).
492pub fn draw_shadow_bounds(terminal: &mut Terminal, bounds: Rect) {
493    use crate::core::palette::Attr;
494
495    const SHADOW_FACTOR: f32 = 0.5; // Darken to 50% of original brightness
496
497    let ss = shadow_size();
498    let mut buf = DrawBuffer::new(ss.0 as usize);
499
500    // Draw right edge shadow (ss.0 columns wide, offset by ss.1 vertically)
501    // Read existing cells and darken them for semi-transparency
502    for y in (bounds.a.y + ss.1)..(bounds.b.y + ss.1) {
503        for i in 0..ss.0 {
504            let x = bounds.b.x + i;
505
506            // Read the existing cell at this position
507            if let Some(existing_cell) = terminal.read_cell(x, y) {
508                // Darken the existing cell's attribute
509                let darkened_attr = existing_cell.attr.darken(SHADOW_FACTOR);
510                buf.put_char(i as usize, existing_cell.ch, darkened_attr);
511            } else {
512                // Out of bounds - use default shadow
513                let default_attr = Attr::from_u8(SHADOW_ATTR);
514                buf.put_char(i as usize, ' ', default_attr);
515            }
516        }
517        write_line_to_terminal(terminal, bounds.b.x, y, &buf);
518    }
519
520    // Draw bottom edge shadow (offset by ss.0 horizontally, excludes right shadow area to prevent double-darkening)
521    let bottom_width = (bounds.b.x - bounds.a.x - ss.0) as usize;
522    let mut bottom_buf = DrawBuffer::new(bottom_width);
523
524    let shadow_y = bounds.b.y;
525    for i in 0..bottom_width {
526        let x = bounds.a.x + ss.0 + i as i16;
527
528        // Read the existing cell at this position
529        if let Some(existing_cell) = terminal.read_cell(x, shadow_y) {
530            // Darken the existing cell's attribute
531            let darkened_attr = existing_cell.attr.darken(SHADOW_FACTOR);
532            bottom_buf.put_char(i, existing_cell.ch, darkened_attr);
533        } else {
534            // Out of bounds - use default shadow
535            let default_attr = Attr::from_u8(SHADOW_ATTR);
536            bottom_buf.put_char(i, ' ', default_attr);
537        }
538    }
539    write_line_to_terminal(terminal, bounds.a.x + ss.0, bounds.b.y, &bottom_buf);
540}