Skip to main content

kimun_notes/components/dialogs/
mod.rs

1pub use create_note_dialog::CreateNoteDialog;
2pub use delete_dialog::DeleteConfirmDialog;
3pub use file_ops_menu::FileOpsMenuDialog;
4pub use help_dialog::HelpDialog;
5pub use move_dialog::MoveDialog;
6pub use quick_note_modal::QuickNoteModal;
7pub use rename_dialog::RenameDialog;
8pub use workspace_switcher::WorkspaceSwitcherModal;
9
10use ratatui::Frame;
11use ratatui::layout::{Constraint, Direction, Layout, Rect};
12use ratatui::style::{Color, Modifier, Style};
13use ratatui::widgets::{Block, Borders, Paragraph, Widget};
14
15use crate::components::Component;
16use crate::components::event_state::EventState;
17use crate::components::events::{AppTx, InputEvent};
18use crate::settings::themes::Theme;
19
20// ---------------------------------------------------------------------------
21// ValidationState — shared by RenameDialog and MoveDialog
22// ---------------------------------------------------------------------------
23
24/// Tracks the current state of an async name / destination availability check.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ValidationState {
27    /// No check has been triggered yet (initial state).
28    Idle,
29    /// A check is in progress.
30    Pending,
31    /// The name / destination is available (does not already exist).
32    Available,
33    /// The name / destination is already taken.
34    Taken,
35}
36
37pub mod create_note_dialog;
38pub mod delete_dialog;
39pub mod file_ops_menu;
40pub mod help_dialog;
41pub mod move_dialog;
42pub mod quick_note_modal;
43pub mod rename_dialog;
44pub mod workspace_switcher;
45
46pub enum ActiveDialog {
47    Menu(FileOpsMenuDialog),
48    Delete(DeleteConfirmDialog),
49    Rename(RenameDialog),
50    Move(MoveDialog),
51    CreateNote(CreateNoteDialog),
52    Help(HelpDialog),
53    QuickNote(QuickNoteModal),
54    WorkspaceSwitcher(WorkspaceSwitcherModal),
55}
56
57impl ActiveDialog {
58    pub fn set_error(&mut self, msg: String) {
59        match self {
60            ActiveDialog::Menu(_) => {} // menu has no error state
61            ActiveDialog::Delete(d) => d.error = Some(msg),
62            ActiveDialog::Rename(d) => d.error = Some(msg),
63            ActiveDialog::Move(d) => d.error = Some(msg),
64            ActiveDialog::CreateNote(d) => d.error = Some(msg),
65            ActiveDialog::Help(_) => {}
66            ActiveDialog::QuickNote(d) => d.error = Some(msg),
67            ActiveDialog::WorkspaceSwitcher(_) => {} // no error state
68        }
69    }
70}
71
72impl Component for ActiveDialog {
73    fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
74        let InputEvent::Key(key) = event else {
75            return EventState::NotConsumed;
76        };
77        match self {
78            ActiveDialog::Menu(d) => d.handle_key(*key, tx),
79            ActiveDialog::Delete(d) => d.handle_key(*key, tx),
80            ActiveDialog::Rename(d) => d.handle_key(*key, tx),
81            ActiveDialog::Move(d) => d.handle_key(*key, tx),
82            ActiveDialog::CreateNote(d) => d.handle_key(*key, tx),
83            ActiveDialog::Help(d) => d.handle_key(*key, tx),
84            ActiveDialog::QuickNote(d) => d.handle_key(*key, tx),
85            ActiveDialog::WorkspaceSwitcher(d) => d.handle_key(*key, tx),
86        }
87    }
88
89    fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
90        match self {
91            ActiveDialog::Menu(d) => d.render(f, rect, theme, focused),
92            ActiveDialog::Delete(d) => d.render(f, rect, theme, focused),
93            ActiveDialog::Rename(d) => d.render(f, rect, theme, focused),
94            ActiveDialog::Move(d) => d.render(f, rect, theme, focused),
95            ActiveDialog::CreateNote(d) => d.render(f, rect, theme, focused),
96            ActiveDialog::Help(d) => d.render(f, rect, theme, focused),
97            ActiveDialog::QuickNote(d) => d.render(f, rect, theme, focused),
98            ActiveDialog::WorkspaceSwitcher(d) => d.render(f, rect, theme, focused),
99        }
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Shared render helpers
105// ---------------------------------------------------------------------------
106
107/// Renders a pre-computed path string (should already include leading spaces).
108pub(super) fn render_path_row(f: &mut Frame, rect: Rect, path: &str, fg: Color, bg: Color) {
109    f.render_widget(
110        Paragraph::new(path).style(Style::default().fg(fg).bg(bg)),
111        rect,
112    );
113}
114
115/// Renders a single-line horizontal rule (TOP border only).
116pub(super) fn render_separator(f: &mut Frame, rect: Rect, fg_muted: Color, bg: Color) {
117    Block::default()
118        .borders(Borders::TOP)
119        .border_style(Style::default().fg(fg_muted))
120        .style(Style::default().bg(bg))
121        .render(rect, f.buffer_mut());
122}
123
124/// Renders `  Error: {msg}` in red.
125pub(super) fn render_error_row(f: &mut Frame, rect: Rect, msg: &str, bg: Color) {
126    f.render_widget(
127        Paragraph::new(format!("  Error: {msg}")).style(Style::default().fg(Color::Red).bg(bg)),
128        rect,
129    );
130}
131
132/// Renders `{enter_text}  [Esc] Cancel` split into two horizontal columns.
133/// The Enter part is dimmed when `enter_active` is `false`.
134pub(super) fn render_confirm_hint(
135    f: &mut Frame,
136    rect: Rect,
137    enter_text: &str,
138    enter_active: bool,
139    fg: Color,
140    fg_muted: Color,
141    bg: Color,
142) {
143    let enter_style = if enter_active {
144        Style::default().fg(fg).bg(bg)
145    } else {
146        Style::default()
147            .fg(fg_muted)
148            .bg(bg)
149            .add_modifier(Modifier::DIM)
150    };
151    let chunks = Layout::default()
152        .direction(Direction::Horizontal)
153        .constraints([
154            Constraint::Length(enter_text.len() as u16 + 1),
155            Constraint::Min(1),
156        ])
157        .split(rect);
158    f.render_widget(Paragraph::new(enter_text).style(enter_style), chunks[0]);
159    f.render_widget(
160        Paragraph::new("  [Esc] Cancel").style(Style::default().fg(fg_muted).bg(bg)),
161        chunks[1],
162    );
163}
164
165// ---------------------------------------------------------------------------
166// Layout helper
167// ---------------------------------------------------------------------------
168
169pub(super) fn centered_rect(
170    percent_x: u16,
171    percent_y: u16,
172    area: ratatui::layout::Rect,
173) -> ratatui::layout::Rect {
174    let popup_height = (area.height as u32 * percent_y as u32 / 100) as u16;
175    let popup_width = (area.width as u32 * percent_x as u32 / 100) as u16;
176    ratatui::layout::Rect {
177        x: area.x + (area.width.saturating_sub(popup_width)) / 2,
178        y: area.y + (area.height.saturating_sub(popup_height)) / 2,
179        width: popup_width,
180        height: popup_height,
181    }
182}
183
184/// Centre a dialog of exactly `width` × `height` characters.
185pub(super) fn fixed_centered_rect(
186    width: u16,
187    height: u16,
188    area: ratatui::layout::Rect,
189) -> ratatui::layout::Rect {
190    let w = width.min(area.width);
191    let h = height.min(area.height);
192    ratatui::layout::Rect {
193        x: area.x + (area.width.saturating_sub(w)) / 2,
194        y: area.y + (area.height.saturating_sub(h)) / 2,
195        width: w,
196        height: h,
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::keys::KeyBindings;
204
205    #[test]
206    fn active_dialog_help_variant_compiles() {
207        let dialog = HelpDialog::new(&KeyBindings::empty());
208        let _active: ActiveDialog = ActiveDialog::Help(dialog);
209    }
210}