kimun_notes/components/
mod.rs1pub mod activity_rail;
2pub mod ask_sources;
3pub mod ask_thread;
4pub mod attachment_view;
5pub mod autocomplete;
6pub mod autosave_timer;
7pub mod command_palette;
8pub mod config_panel;
9pub mod dialogs;
10pub mod dir_browser;
11pub mod drawer;
12pub mod drawer_views;
13pub mod event_state;
14pub mod events;
15pub mod file_list;
16pub mod footer_bar;
17pub mod hints;
18pub mod indexing;
19pub mod markdown_lines;
20pub mod note_browser;
21pub mod overlay;
22pub mod panel;
23pub mod preferences;
24pub mod preview_highlight;
25pub mod preview_pane;
26pub mod query_highlight;
27pub mod query_list_panel;
28pub mod query_panel;
29pub mod query_vars;
30pub mod rich_row;
31pub mod saved_search_breadcrumb;
32pub mod saved_searches_modal;
33pub mod search_list;
34pub mod semantic_search;
35pub mod sidebar;
36pub mod single_line_input;
37pub mod text_editor;
38pub mod which_key;
39
40use ratatui::Frame;
41use ratatui::layout::Rect;
42
43use crate::components::event_state::EventState;
44use crate::components::events::{AppEvent, AppTx, InputEvent};
45use crate::settings::themes::Theme;
46
47pub fn yank(text: String, done_msg: impl Into<String>, tx: &AppTx) {
58 let msg = match arboard::Clipboard::new().and_then(|mut c| c.set_text(text)) {
59 Ok(()) => done_msg.into(),
60 Err(e) => format!("clipboard: {e}"),
61 };
62 tx.send(AppEvent::FlashMessage(msg)).ok();
63}
64
65pub fn fixed_centered_rect(width: u16, height: u16, r: Rect) -> Rect {
69 let width = width.min(r.width);
70 let height = height.min(r.height);
71 Rect {
72 x: r.x + (r.width - width) / 2,
73 y: r.y + (r.height - height) / 2,
74 width,
75 height,
76 }
77}
78
79pub fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
80 let popup_height = (area.height as u32 * percent_y as u32 / 100) as u16;
81 let popup_width = (area.width as u32 * percent_x as u32 / 100) as u16;
82 Rect {
83 x: area.x + (area.width.saturating_sub(popup_width)) / 2,
84 y: area.y + (area.height.saturating_sub(popup_height)) / 2,
85 width: popup_width,
86 height: popup_height,
87 }
88}
89
90pub trait Component {
91 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
94 let _ = (event, tx);
95 EventState::NotConsumed
96 }
97
98 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool);
99
100 fn hint_shortcuts(&self) -> Vec<(String, String)> {
103 vec![]
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn yank_flashes_outcome_on_tx() {
113 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
117 yank("hello".to_string(), "hello copied", &tx);
118 let ev = rx.try_recv().expect("yank sends exactly one event");
119 match ev {
120 AppEvent::FlashMessage(msg) => {
121 assert!(
122 msg == "hello copied" || msg.starts_with("clipboard: "),
123 "unexpected flash message: {msg}"
124 );
125 }
126 other => panic!("expected FlashMessage, got {other:?}"),
127 }
128 }
129
130 #[test]
131 fn centered_rect_is_centered() {
132 let area = Rect {
133 x: 0,
134 y: 0,
135 width: 100,
136 height: 40,
137 };
138 let r = centered_rect(80, 75, area);
139 assert_eq!(r.width, 80);
140 assert_eq!(r.height, 30);
141 assert_eq!(r.x, 10); assert_eq!(r.y, 5); }
144
145 #[test]
146 fn centered_rect_does_not_underflow() {
147 let area = Rect {
149 x: 0,
150 y: 0,
151 width: 5,
152 height: 5,
153 };
154 let _ = centered_rect(80, 75, area);
155 }
156}