Skip to main content

boxmux_lib/components/
renderable_content.rs

1use crate::Bounds;
2
3/// Universal event system for RenderableContent interactions
4/// Replaces direct method calls with flexible event-based architecture
5#[derive(Debug, Clone)]
6pub struct ContentEvent {
7    /// Type of event that occurred
8    pub event_type: EventType,
9    /// Screen coordinates where event occurred (relative to content area)
10    pub position: Option<(usize, usize)>,
11    /// Identifier of the content zone that was targeted
12    pub zone_id: Option<String>,
13    /// Additional event-specific data
14    pub data: EventData,
15    /// Timestamp when event occurred
16    pub timestamp: std::time::SystemTime,
17}
18
19/// Types of events that can occur on content
20#[derive(Debug, Clone, PartialEq)]
21pub enum EventType {
22    /// Mouse click event
23    Click,
24    /// Mouse double-click event
25    DoubleClick,
26    /// Mouse hover event (entering/leaving sensitive zones)
27    Hover,
28    /// Mouse move event (continuous movement tracking)
29    MouseMove,
30    /// Keyboard key press event
31    KeyPress,
32    /// Focus gained event
33    Focus,
34    /// Focus lost event
35    Blur,
36    /// Content scroll event
37    Scroll,
38    /// Content area resize event
39    Resize,
40    /// Box resize event (interactive resizing)
41    BoxResize,
42    /// Box title change event
43    TitleChange,
44    /// Custom application-specific event
45    Custom(String),
46}
47
48/// Event-specific data payload
49#[derive(Debug, Clone, Default)]
50pub struct EventData {
51    /// Mouse button for click events
52    pub mouse_button: Option<MouseButton>,
53    /// Key information for keyboard events
54    pub key: Option<KeyInfo>,
55    /// Scroll direction and amount
56    pub scroll: Option<ScrollInfo>,
57    /// Size information for resize events
58    pub size: Option<(usize, usize)>,
59    /// Mouse movement information
60    pub mouse_move: Option<MouseMoveInfo>,
61    /// Hover state information
62    pub hover: Option<HoverInfo>,
63    /// Box resize information
64    pub box_resize: Option<BoxResizeInfo>,
65    /// Title change information
66    pub title_change: Option<TitleChangeInfo>,
67    /// Custom event data
68    pub custom_data: Option<String>,
69}
70
71/// Mouse button types
72#[derive(Debug, Clone, PartialEq)]
73pub enum MouseButton {
74    Left,
75    Right,
76    Middle,
77    WheelUp,
78    WheelDown,
79}
80
81/// Keyboard key information
82#[derive(Debug, Clone)]
83pub struct KeyInfo {
84    /// Key code or character
85    pub key: String,
86    /// Modifier keys pressed
87    pub modifiers: Vec<KeyModifier>,
88}
89
90/// Keyboard modifier keys
91#[derive(Debug, Clone, PartialEq)]
92pub enum KeyModifier {
93    Ctrl,
94    Alt,
95    Shift,
96    Meta,
97}
98
99/// Scroll event information
100#[derive(Debug, Clone)]
101pub struct ScrollInfo {
102    /// Scroll direction
103    pub direction: ScrollDirection,
104    /// Amount to scroll
105    pub amount: i32,
106}
107
108/// Scroll directions
109#[derive(Debug, Clone, PartialEq)]
110pub enum ScrollDirection {
111    Up,
112    Down,
113    Left,
114    Right,
115}
116
117/// Mouse movement information
118#[derive(Debug, Clone)]
119pub struct MouseMoveInfo {
120    /// Previous mouse position (if available)
121    pub from_position: Option<(usize, usize)>,
122    /// Current mouse position
123    pub to_position: (usize, usize),
124    /// Movement delta (dx, dy)
125    pub delta: (i32, i32),
126    /// Whether mouse is being dragged (button held)
127    pub is_dragging: bool,
128    /// Button being held during drag (if dragging)
129    pub drag_button: Option<MouseButton>,
130}
131
132/// Hover state information
133#[derive(Debug, Clone)]
134pub struct HoverInfo {
135    /// Whether entering or leaving hover state
136    pub state: HoverState,
137    /// Previous zone that was hovered (for leave events)
138    pub previous_zone: Option<String>,
139    /// Current zone being hovered (for enter events)
140    pub current_zone: Option<String>,
141    /// Duration mouse has been hovering (for tooltip timing)
142    pub hover_duration: Option<std::time::Duration>,
143}
144
145/// Hover state transitions
146#[derive(Debug, Clone, PartialEq)]
147pub enum HoverState {
148    /// Mouse entered sensitive zone
149    Enter,
150    /// Mouse left sensitive zone  
151    Leave,
152    /// Mouse moved within same zone
153    Move,
154}
155
156/// Box resize information
157#[derive(Debug, Clone)]
158pub struct BoxResizeInfo {
159    /// Type of resize operation
160    pub resize_type: BoxResizeType,
161    /// Original box bounds before resize
162    pub original_bounds: (usize, usize, usize, usize), // x, y, width, height
163    /// New box bounds after resize
164    pub new_bounds: (usize, usize, usize, usize), // x, y, width, height
165    /// Resize anchor point (corner/edge being dragged)
166    pub anchor: ResizeAnchor,
167    /// Whether resize is in progress or completed
168    pub state: ResizeState,
169}
170
171/// Types of box resize operations
172#[derive(Debug, Clone, PartialEq)]
173pub enum BoxResizeType {
174    /// Interactive resize via mouse drag
175    Interactive,
176    /// Programmatic resize via API
177    Programmatic,
178    /// Resize due to terminal window changes
179    Terminal,
180}
181
182/// Resize anchor points
183#[derive(Debug, Clone, PartialEq)]
184pub enum ResizeAnchor {
185    /// Top-left corner
186    TopLeft,
187    /// Top-right corner
188    TopRight,
189    /// Bottom-left corner
190    BottomLeft,
191    /// Bottom-right corner
192    BottomRight,
193    /// Top edge
194    Top,
195    /// Bottom edge
196    Bottom,
197    /// Left edge
198    Left,
199    /// Right edge
200    Right,
201}
202
203/// Resize operation state
204#[derive(Debug, Clone, PartialEq)]
205pub enum ResizeState {
206    /// Resize operation started
207    Started,
208    /// Resize in progress
209    InProgress,
210    /// Resize completed
211    Completed,
212    /// Resize cancelled
213    Cancelled,
214}
215
216/// Title change information
217#[derive(Debug, Clone)]
218pub struct TitleChangeInfo {
219    /// Previous title (if any)
220    pub old_title: Option<String>,
221    /// New title
222    pub new_title: String,
223    /// Source of title change
224    pub source: TitleChangeSource,
225    /// Whether change should be persisted to YAML
226    pub persist: bool,
227}
228
229/// Sources of title changes
230#[derive(Debug, Clone, PartialEq)]
231pub enum TitleChangeSource {
232    /// User interaction (editing)
233    User,
234    /// PTY program (terminal title sequences)
235    PTY,
236    /// Script execution result
237    Script,
238    /// API call
239    API,
240    /// System event
241    System,
242}
243
244/// Result of event handling
245#[derive(Debug, Clone, PartialEq)]
246pub enum EventResult {
247    /// Event was handled and should not propagate
248    Handled,
249    /// Event was not handled, continue propagation
250    NotHandled,
251    /// Event was handled but should continue propagation
252    HandledContinue,
253    /// Event caused a state change that requires re-rendering
254    StateChanged,
255}
256
257impl ContentEvent {
258    /// Create a new content event with current timestamp
259    pub fn new(
260        event_type: EventType,
261        position: Option<(usize, usize)>,
262        zone_id: Option<String>,
263    ) -> Self {
264        Self {
265            event_type,
266            position,
267            zone_id,
268            data: EventData::default(),
269            timestamp: std::time::SystemTime::now(),
270        }
271    }
272
273    /// Create a click event
274    pub fn new_click(position: Option<(usize, usize)>, zone_id: Option<String>) -> Self {
275        let mut event = Self::new(EventType::Click, position, zone_id);
276        event.data.mouse_button = Some(MouseButton::Left);
277        event
278    }
279
280    /// Create a click event with specific mouse button
281    pub fn new_click_with_button(
282        position: Option<(usize, usize)>,
283        zone_id: Option<String>,
284        button: MouseButton,
285    ) -> Self {
286        let mut event = Self::new(EventType::Click, position, zone_id);
287        event.data.mouse_button = Some(button);
288        event
289    }
290
291    /// Create a hover event
292    pub fn new_hover(position: (usize, usize), zone_id: Option<String>) -> Self {
293        Self::new(EventType::Hover, Some(position), zone_id)
294    }
295
296    /// Create a key press event
297    pub fn new_keypress(key: String, modifiers: Vec<KeyModifier>, zone_id: Option<String>) -> Self {
298        let mut event = Self::new(EventType::KeyPress, None, zone_id);
299        event.data.key = Some(KeyInfo { key, modifiers });
300        event
301    }
302
303    /// Create a scroll event
304    pub fn new_scroll(direction: ScrollDirection, amount: i32, zone_id: Option<String>) -> Self {
305        let mut event = Self::new(EventType::Scroll, None, zone_id);
306        event.data.scroll = Some(ScrollInfo { direction, amount });
307        event
308    }
309
310    /// Create a focus event
311    pub fn new_focus(zone_id: Option<String>) -> Self {
312        Self::new(EventType::Focus, None, zone_id)
313    }
314
315    /// Create a blur event
316    pub fn new_blur(zone_id: Option<String>) -> Self {
317        Self::new(EventType::Blur, None, zone_id)
318    }
319
320    /// Create a resize event
321    pub fn new_resize(new_size: (usize, usize)) -> Self {
322        let mut event = Self::new(EventType::Resize, None, None);
323        event.data.size = Some(new_size);
324        event
325    }
326
327    /// Create a custom event
328    pub fn new_custom(name: String, data: Option<String>, zone_id: Option<String>) -> Self {
329        let mut event = Self::new(EventType::Custom(name), None, zone_id);
330        event.data.custom_data = data;
331        event
332    }
333
334    /// Create a mouse move event
335    pub fn new_mouse_move(
336        from_pos: Option<(usize, usize)>,
337        to_pos: (usize, usize),
338        zone_id: Option<String>,
339    ) -> Self {
340        let mut event = Self::new(EventType::MouseMove, Some(to_pos), zone_id);
341        let delta = if let Some(from) = from_pos {
342            (
343                to_pos.0 as i32 - from.0 as i32,
344                to_pos.1 as i32 - from.1 as i32,
345            )
346        } else {
347            (0, 0)
348        };
349        event.data.mouse_move = Some(MouseMoveInfo {
350            from_position: from_pos,
351            to_position: to_pos,
352            delta,
353            is_dragging: false,
354            drag_button: None,
355        });
356        event
357    }
358
359    /// Create a mouse drag event
360    pub fn new_mouse_drag(
361        from_pos: (usize, usize),
362        to_pos: (usize, usize),
363        button: MouseButton,
364        zone_id: Option<String>,
365    ) -> Self {
366        let mut event = Self::new(EventType::MouseMove, Some(to_pos), zone_id);
367        let delta = (
368            to_pos.0 as i32 - from_pos.0 as i32,
369            to_pos.1 as i32 - from_pos.1 as i32,
370        );
371        event.data.mouse_move = Some(MouseMoveInfo {
372            from_position: Some(from_pos),
373            to_position: to_pos,
374            delta,
375            is_dragging: true,
376            drag_button: Some(button),
377        });
378        event
379    }
380
381    /// Create a hover enter event
382    pub fn new_hover_enter(
383        position: (usize, usize),
384        zone_id: String,
385        previous_zone: Option<String>,
386    ) -> Self {
387        let mut event = Self::new(EventType::Hover, Some(position), Some(zone_id.clone()));
388        event.data.hover = Some(HoverInfo {
389            state: HoverState::Enter,
390            previous_zone,
391            current_zone: Some(zone_id),
392            hover_duration: None,
393        });
394        event
395    }
396
397    /// Create a hover leave event
398    pub fn new_hover_leave(
399        position: (usize, usize),
400        zone_id: String,
401        new_zone: Option<String>,
402    ) -> Self {
403        let mut event = Self::new(EventType::Hover, Some(position), Some(zone_id.clone()));
404        event.data.hover = Some(HoverInfo {
405            state: HoverState::Leave,
406            previous_zone: Some(zone_id),
407            current_zone: new_zone,
408            hover_duration: None,
409        });
410        event
411    }
412
413    /// Create a hover move event (within same zone)
414    pub fn new_hover_move(
415        position: (usize, usize),
416        zone_id: String,
417        duration: std::time::Duration,
418    ) -> Self {
419        let mut event = Self::new(EventType::Hover, Some(position), Some(zone_id.clone()));
420        event.data.hover = Some(HoverInfo {
421            state: HoverState::Move,
422            previous_zone: None,
423            current_zone: Some(zone_id),
424            hover_duration: Some(duration),
425        });
426        event
427    }
428
429    /// Create a box resize event
430    pub fn new_box_resize(
431        resize_type: BoxResizeType,
432        original_bounds: (usize, usize, usize, usize),
433        new_bounds: (usize, usize, usize, usize),
434        anchor: ResizeAnchor,
435        state: ResizeState,
436    ) -> Self {
437        let mut event = Self::new(EventType::BoxResize, None, None);
438        event.data.box_resize = Some(BoxResizeInfo {
439            resize_type,
440            original_bounds,
441            new_bounds,
442            anchor,
443            state,
444        });
445        event
446    }
447
448    /// Create a title change event
449    pub fn new_title_change(
450        old_title: Option<String>,
451        new_title: String,
452        source: TitleChangeSource,
453        persist: bool,
454    ) -> Self {
455        let mut event = Self::new(EventType::TitleChange, None, None);
456        event.data.title_change = Some(TitleChangeInfo {
457            old_title,
458            new_title,
459            source,
460            persist,
461        });
462        event
463    }
464
465    /// Get the mouse button for click events
466    pub fn mouse_button(&self) -> Option<&MouseButton> {
467        self.data.mouse_button.as_ref()
468    }
469
470    /// Get the key information for key events
471    pub fn key_info(&self) -> Option<&KeyInfo> {
472        self.data.key.as_ref()
473    }
474
475    /// Get the scroll information for scroll events
476    pub fn scroll_info(&self) -> Option<&ScrollInfo> {
477        self.data.scroll.as_ref()
478    }
479
480    /// Check if this is a click event
481    pub fn is_click(&self) -> bool {
482        self.event_type == EventType::Click
483    }
484
485    /// Check if this is a double-click event
486    pub fn is_double_click(&self) -> bool {
487        self.event_type == EventType::DoubleClick
488    }
489
490    /// Check if this is a keyboard event
491    pub fn is_keyboard(&self) -> bool {
492        self.event_type == EventType::KeyPress
493    }
494
495    /// Check if this is a mouse move event
496    pub fn is_mouse_move(&self) -> bool {
497        self.event_type == EventType::MouseMove
498    }
499
500    /// Check if this is a box resize event
501    pub fn is_box_resize(&self) -> bool {
502        self.event_type == EventType::BoxResize
503    }
504
505    /// Check if this is a title change event
506    pub fn is_title_change(&self) -> bool {
507        self.event_type == EventType::TitleChange
508    }
509
510    /// Get the mouse move information
511    pub fn mouse_move_info(&self) -> Option<&MouseMoveInfo> {
512        self.data.mouse_move.as_ref()
513    }
514
515    /// Get the hover information
516    pub fn hover_info(&self) -> Option<&HoverInfo> {
517        self.data.hover.as_ref()
518    }
519
520    /// Get the box resize information
521    pub fn box_resize_info(&self) -> Option<&BoxResizeInfo> {
522        self.data.box_resize.as_ref()
523    }
524
525    /// Get the title change information
526    pub fn title_change_info(&self) -> Option<&TitleChangeInfo> {
527        self.data.title_change.as_ref()
528    }
529
530    /// Check if this is a drag event
531    pub fn is_drag(&self) -> bool {
532        if let Some(mouse_move) = &self.data.mouse_move {
533            mouse_move.is_dragging
534        } else {
535            false
536        }
537    }
538
539    /// Check if this is a hover enter event
540    pub fn is_hover_enter(&self) -> bool {
541        if let Some(hover) = &self.data.hover {
542            hover.state == HoverState::Enter
543        } else {
544            false
545        }
546    }
547
548    /// Check if this is a hover leave event
549    pub fn is_hover_leave(&self) -> bool {
550        if let Some(hover) = &self.data.hover {
551            hover.state == HoverState::Leave
552        } else {
553            false
554        }
555    }
556
557    /// Get the movement delta for mouse move/drag events
558    pub fn movement_delta(&self) -> Option<(i32, i32)> {
559        self.data.mouse_move.as_ref().map(|m| m.delta)
560    }
561}
562
563/// Universal content interface for all content types in BoxMux
564/// Enables unified overflow handling for text, choices, charts, and any future content types
565pub trait RenderableContent {
566    /// Get the raw content dimensions before any transformations
567    /// Returns (width, height) in character cells
568    fn get_dimensions(&self) -> (usize, usize);
569
570    /// Get the raw content as a string - for backward compatibility with existing text rendering
571    fn get_raw_content(&self) -> String;
572
573    /// Get sensitive zones in box-relative coordinates (0,0 = top-left of content area, ignoring borders)
574    /// BoxRenderer will translate these to absolute screen coordinates accounting for scroll/wrap/centering
575    fn get_box_relative_sensitive_zones(&self) -> Vec<SensitiveZone>;
576
577    /// Handle a content event (click, hover, keypress, etc.)
578    /// Event contains zone_id, position, and event-specific data
579    /// Returns EventResult indicating how the event was processed
580    fn handle_event(&mut self, event: &ContentEvent) -> EventResult;
581}
582
583/// Sensitive zone representing an interactive area on screen
584#[derive(Debug, Clone)]
585pub struct SensitiveZone {
586    /// Screen rectangle bounds for this sensitive area
587    pub bounds: Bounds,
588    /// Identifier for the content item (choice ID, link target, etc.)
589    pub content_id: String,
590    /// Type of content this zone represents
591    pub content_type: ContentType,
592    /// Additional metadata for this sensitive zone
593    pub metadata: SensitiveMetadata,
594}
595
596/// Type of sensitive content
597#[derive(Debug, Clone, PartialEq)]
598pub enum ContentType {
599    /// Menu choice item
600    Choice,
601    /// Text link or hyperlink
602    Link,
603    /// Interactive button
604    Button,
605    /// Chart element (bar, line, data point)
606    ChartElement,
607    /// Tab in tab bar
608    Tab,
609    /// Generic interactive element
610    Interactive,
611}
612
613/// Additional metadata for sensitive zones
614#[derive(Debug, Clone, Default)]
615pub struct SensitiveMetadata {
616    /// Display text for this zone
617    pub display_text: Option<String>,
618    /// Tooltip or help text
619    pub tooltip: Option<String>,
620    /// Whether this zone is currently selected/focused
621    pub selected: bool,
622    /// Whether this zone is enabled for interaction
623    pub enabled: bool,
624    /// Line number within the original content (for wrapped content)
625    pub original_line: Option<usize>,
626    /// Character range within the original line
627    pub char_range: Option<(usize, usize)>,
628}
629
630impl SensitiveZone {
631    /// Create a new sensitive zone
632    pub fn new(bounds: Bounds, content_id: String, content_type: ContentType) -> Self {
633        Self {
634            bounds,
635            content_id,
636            content_type,
637            metadata: SensitiveMetadata::default(),
638        }
639    }
640
641    /// Create a sensitive zone with metadata
642    pub fn with_metadata(
643        bounds: Bounds,
644        content_id: String,
645        content_type: ContentType,
646        metadata: SensitiveMetadata,
647    ) -> Self {
648        Self {
649            bounds,
650            content_id,
651            content_type,
652            metadata,
653        }
654    }
655
656    /// Check if a point is within this sensitive zone (inclusive of edges, matching
657    /// the hit-testing convention used everywhere else).
658    pub fn contains(&self, x: usize, y: usize) -> bool {
659        self.bounds.contains_point(x, y)
660    }
661
662    /// Get the display text for this zone
663    pub fn display_text(&self) -> String {
664        self.metadata
665            .display_text
666            .clone()
667            .unwrap_or_else(|| self.content_id.clone())
668    }
669}
670
671/// Content dimensions with scroll requirements
672#[derive(Debug, Clone)]
673pub struct ContentDimensions {
674    /// Total content width in characters
675    pub width: usize,
676    /// Total content height in lines
677    pub height: usize,
678    /// Whether horizontal scrolling is needed
679    pub needs_horizontal_scroll: bool,
680    /// Whether vertical scrolling is needed
681    pub needs_vertical_scroll: bool,
682}
683
684impl ContentDimensions {
685    /// Create content dimensions
686    pub fn new(width: usize, height: usize, viewport_width: usize, viewport_height: usize) -> Self {
687        Self {
688            width,
689            height,
690            needs_horizontal_scroll: width > viewport_width,
691            needs_vertical_scroll: height > viewport_height,
692        }
693    }
694
695    /// Check if any scrolling is needed
696    pub fn needs_scrolling(&self) -> bool {
697        self.needs_horizontal_scroll || self.needs_vertical_scroll
698    }
699}