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 for layout calculations to avoid recomputation each frame
13pub struct LayoutCache {
14    cached: Option<(u16, u16, u16, u16, u16, Vec<Rect>)>,
15}
16
17impl LayoutCache {
18    fn new() -> Self {
19        Self { cached: None }
20    }
21
22    pub fn get_main_layout(
23        &mut self,
24        area: Rect,
25        input_height: u16,
26        status_line_height: u16,
27        attachment_height: u16,
28    ) -> Vec<Rect> {
29        if let Some((w, h, ih, sh, ah, ref rects)) = self.cached
30            && w == area.width
31            && h == area.height
32            && ih == input_height
33            && sh == status_line_height
34            && ah == attachment_height
35        {
36            return rects.clone();
37        }
38
39        let layout = Layout::default()
40            .direction(Direction::Vertical)
41            .margin(0)
42            .spacing(0)
43            .flex(Flex::Start)
44            .constraints([
45                Constraint::Min(10),
46                Constraint::Length(status_line_height),
47                Constraint::Length(attachment_height),
48                Constraint::Length(input_height),
49                Constraint::Length(2),
50            ])
51            .split(area);
52
53        let layout_vec = layout.to_vec();
54        self.cached = Some((
55            area.width,
56            area.height,
57            input_height,
58            status_line_height,
59            attachment_height,
60            layout_vec.clone(),
61        ));
62        layout_vec
63    }
64}
65
66/// UI state - visual presentation and widget states
67pub struct UIState {
68    /// Chat widget state (scroll, scrolling flag)
69    pub chat_state: ChatState,
70    /// Input widget state (cursor position for display)
71    pub input_state: InputState,
72    /// UI theme
73    pub theme: Theme,
74    /// Selected message index (for navigation)
75    pub selected_message: Option<usize>,
76    /// Whether focus is in the attachment area (above input)
77    pub attachment_focused: bool,
78    /// Which attachment is selected when attachment_focused is true
79    pub selected_attachment: usize,
80    /// Attachment area rect from last render (for Ctrl+Click detection)
81    pub attachment_area_y: Option<u16>,
82    /// Layout cache (avoids recomputation each frame)
83    pub layout_cache: LayoutCache,
84    /// Cached parsed markdown per message: (message_index, content_hash) -> parsed lines
85    pub markdown_cache: FxHashMap<u64, Vec<Line<'static>>>,
86}
87
88impl UIState {
89    /// Create a new UIState with default values
90    pub fn new() -> Self {
91        Self {
92            chat_state: ChatState::default(),
93            input_state: InputState::default(),
94            theme: Theme::dark(),
95            selected_message: None,
96            attachment_focused: false,
97            selected_attachment: 0,
98            attachment_area_y: None,
99            layout_cache: LayoutCache::new(),
100            markdown_cache: FxHashMap::default(),
101        }
102    }
103}
104
105impl Default for UIState {
106    fn default() -> Self {
107        Self::new()
108    }
109}