Skip to main content

mermaid_cli/tui/state/
ui.rs

1//! UI state management
2//!
3//! Visual presentation and widget states.
4
5use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect};
6use ratatui::text::Line;
7use rustc_hash::FxHashMap;
8
9use crate::tui::theme::Theme;
10use crate::tui::widgets::{ChatState, InputState};
11
12/// Cache key for the main layout — all dimensions that, if changed,
13/// invalidate the cached `Vec<Rect>`. Step 5e added `bottom_height`
14/// (palette grows the bottom region from 2 lines to ~10).
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16struct LayoutKey {
17    width: u16,
18    height: u16,
19    input_height: u16,
20    status_line_height: u16,
21    attachment_height: u16,
22    bottom_height: u16,
23}
24
25/// Cache for layout calculations to avoid recomputation each frame.
26pub struct LayoutCache {
27    cached: Option<(LayoutKey, Vec<Rect>)>,
28}
29
30impl LayoutCache {
31    fn new() -> Self {
32        Self { cached: None }
33    }
34
35    pub fn get_main_layout(
36        &mut self,
37        area: Rect,
38        input_height: u16,
39        status_line_height: u16,
40        attachment_height: u16,
41        bottom_height: u16,
42    ) -> Vec<Rect> {
43        let key = LayoutKey {
44            width: area.width,
45            height: area.height,
46            input_height,
47            status_line_height,
48            attachment_height,
49            bottom_height,
50        };
51        if let Some((cached_key, ref rects)) = self.cached
52            && cached_key == key
53        {
54            return rects.clone();
55        }
56
57        let layout = Layout::default()
58            .direction(Direction::Vertical)
59            .margin(0)
60            .spacing(0)
61            .flex(Flex::Start)
62            .constraints([
63                Constraint::Min(10),
64                Constraint::Length(status_line_height),
65                Constraint::Length(attachment_height),
66                Constraint::Length(input_height),
67                Constraint::Length(bottom_height),
68            ])
69            .split(area);
70
71        let layout_vec = layout.to_vec();
72        self.cached = Some((key, layout_vec.clone()));
73        layout_vec
74    }
75}
76
77/// UI state - visual presentation and widget states
78pub struct UIState {
79    /// Chat widget state (scroll, scrolling flag)
80    pub chat_state: ChatState,
81    /// Input widget state (cursor position for display)
82    pub input_state: InputState,
83    /// UI theme
84    pub theme: Theme,
85    /// Selected message index (for navigation)
86    pub selected_message: Option<usize>,
87    /// Whether focus is in the attachment area (above input)
88    pub attachment_focused: bool,
89    /// Which attachment is selected when attachment_focused is true
90    pub selected_attachment: usize,
91    /// Attachment area rect from last render (for Ctrl+Click detection)
92    pub attachment_area_y: Option<u16>,
93    /// Selected row in the slash-command palette (visible whenever the
94    /// input starts with `/`). Indexes into the FILTERED list, not the
95    /// full registry — reset to 0 whenever the filter changes so a
96    /// shrinking result list can't leave the index out-of-bounds.
97    pub palette_selected_index: usize,
98    /// Layout cache (avoids recomputation each frame)
99    pub layout_cache: LayoutCache,
100    /// Cached parsed markdown per message: (message_index, content_hash) -> parsed lines
101    pub markdown_cache: FxHashMap<u64, Vec<Line<'static>>>,
102}
103
104impl UIState {
105    /// Create a new UIState with default values
106    pub fn new() -> Self {
107        Self {
108            chat_state: ChatState::default(),
109            input_state: InputState::default(),
110            theme: Theme::dark(),
111            selected_message: None,
112            attachment_focused: false,
113            selected_attachment: 0,
114            attachment_area_y: None,
115            palette_selected_index: 0,
116            layout_cache: LayoutCache::new(),
117            markdown_cache: FxHashMap::default(),
118        }
119    }
120}
121
122impl Default for UIState {
123    fn default() -> Self {
124        Self::new()
125    }
126}