1use std::sync::Arc;
7
8use crate::server_client::RagClient;
9use ratatui::Frame;
10use ratatui::crossterm::event::{MouseButton, MouseEventKind};
11use ratatui::layout::{Constraint, Direction, Layout, Position, Rect};
12
13use crate::components::Component;
14use crate::components::activity_rail::{ActivityRail, RAIL_WIDTH, RailCaps};
15use crate::components::ask_thread::ThreadPanel;
16use crate::components::attachment_view::AttachmentView;
17use crate::components::drawer::{DrawerHost, DrawerView};
18use crate::components::event_state::EventState;
19use crate::components::events::{AppTx, InputEvent};
20use crate::components::panel::{PanelKind, panel_block};
21use crate::components::query_panel::QueryPanel;
22use crate::components::sidebar::SidebarComponent;
23use crate::components::text_editor::TextEditorComponent;
24use crate::settings::themes::Theme;
25
26const DEFAULT_DRAWER_WIDTH: u16 = 34;
29const MIN_DRAWER_WIDTH: u16 = 20;
31const MIN_EDITOR_WIDTH: u16 = 20;
33
34struct Slot {
35 kind: PanelKind,
36 visible: bool,
37}
38
39pub struct PanelOrder {
44 slots: Vec<Slot>,
45 focus: usize,
46}
47
48impl PanelOrder {
49 pub fn new() -> Self {
51 let slots = vec![
52 Slot {
53 kind: PanelKind::Rail,
54 visible: true,
55 },
56 Slot {
57 kind: PanelKind::Drawer,
58 visible: true,
59 },
60 Slot {
61 kind: PanelKind::Editor,
62 visible: true,
63 },
64 ];
65 let focus = slots
66 .iter()
67 .position(|s| s.kind == PanelKind::Editor)
68 .expect("editor slot present");
69 Self { slots, focus }
70 }
71
72 pub fn focused(&self) -> PanelKind {
74 self.slots[self.focus].kind
75 }
76
77 pub fn prev_kind(&self) -> Option<PanelKind> {
79 self.step(|i, n| (i + n - 1) % n)
80 }
81
82 pub fn next_kind(&self) -> Option<PanelKind> {
84 self.step(|i, n| (i + 1) % n)
85 }
86
87 fn step(&self, advance: impl Fn(usize, usize) -> usize) -> Option<PanelKind> {
89 let visible = self.visible_in_order();
90 let n = visible.len();
91 if n < 2 {
92 return None;
93 }
94 let i = visible.iter().position(|&k| k == self.focused())?;
95 Some(visible[advance(i, n)])
96 }
97
98 pub fn focus(&mut self, kind: PanelKind) {
100 if let Some(i) = self.slots.iter().position(|s| s.kind == kind) {
101 self.focus = i;
102 }
103 }
104
105 pub fn is_visible(&self, kind: PanelKind) -> bool {
107 self.slots
108 .iter()
109 .find(|s| s.kind == kind)
110 .is_some_and(|s| s.visible)
111 }
112
113 pub fn show(&mut self, kind: PanelKind) {
115 if let Some(s) = self.slots.iter_mut().find(|s| s.kind == kind) {
116 s.visible = true;
117 }
118 }
119
120 pub fn hide(&mut self, kind: PanelKind) {
123 if kind == PanelKind::Editor || kind == PanelKind::Rail {
124 return;
125 }
126 if let Some(s) = self.slots.iter_mut().find(|s| s.kind == kind) {
127 s.visible = false;
128 }
129 if !self.slots[self.focus].visible {
132 self.focus = self.nearest_visible(self.focus);
133 }
134 }
135
136 pub fn visible_in_order(&self) -> Vec<PanelKind> {
139 self.slots
140 .iter()
141 .filter(|s| s.visible)
142 .map(|s| s.kind)
143 .collect()
144 }
145
146 fn nearest_visible(&self, from: usize) -> usize {
150 let n = self.slots.len();
151 (1..n)
152 .flat_map(|d| [from.checked_sub(d), Some(from + d).filter(|&i| i < n)])
153 .flatten()
154 .find(|&i| self.slots[i].visible)
155 .unwrap_or(from)
156 }
157}
158
159impl Default for PanelOrder {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165fn panel_column(kind: PanelKind, drawer_width: u16) -> Constraint {
168 match kind {
169 PanelKind::Rail => Constraint::Length(RAIL_WIDTH),
170 PanelKind::Drawer => Constraint::Length(drawer_width),
171 PanelKind::Editor => Constraint::Min(0),
172 }
173}
174
175fn layout_columns(visible: &[PanelKind], area: Rect, drawer_width: u16) -> Vec<(PanelKind, Rect)> {
179 let constraints: Vec<Constraint> = visible
180 .iter()
181 .map(|k| panel_column(*k, drawer_width))
182 .collect();
183 let columns = Layout::default()
184 .direction(Direction::Horizontal)
185 .constraints(constraints)
186 .split(area);
187 visible
188 .iter()
189 .copied()
190 .zip(columns.iter().copied())
191 .collect()
192}
193
194fn kind_at(columns: &[(PanelKind, Rect)], column: u16, row: u16) -> Option<PanelKind> {
196 columns
197 .iter()
198 .find(|(_, rect)| rect.contains(Position::new(column, row)))
199 .map(|(kind, _)| *kind)
200}
201
202enum EditorAreaContent {
212 Note,
213 Attachment(Box<AttachmentView>),
217 Ask,
220}
221
222pub struct PanelSet {
228 order: PanelOrder,
229 rail: ActivityRail,
230 drawer: DrawerHost,
231 editor: TextEditorComponent,
235 ask: ThreadPanel,
241 content: EditorAreaContent,
245 drawer_width: u16,
247 dragging_divider: bool,
249 column_rects: Vec<(PanelKind, Rect)>,
253}
254
255impl PanelSet {
256 pub fn from_panels(
257 drawer: DrawerHost,
258 editor: TextEditorComponent,
259 icons: crate::settings::icons::Icons,
260 key_bindings: crate::keys::KeyBindings,
261 rail_caps: RailCaps,
262 ) -> Self {
263 let mut ask = ThreadPanel::new();
264 ask.set_icons(icons.clone());
265 Self {
266 order: PanelOrder::new(),
267 rail: ActivityRail::new(key_bindings, icons, rail_caps),
268 drawer,
269 editor,
270 ask,
271 content: EditorAreaContent::Note,
272 drawer_width: DEFAULT_DRAWER_WIDTH,
273 dragging_divider: false,
274 column_rects: Vec::new(),
275 }
276 }
277
278 pub fn rebuild_rail(
283 &mut self,
284 key_bindings: crate::keys::KeyBindings,
285 icons: crate::settings::icons::Icons,
286 rail_caps: RailCaps,
287 ) {
288 let active = self.drawer.active_view();
289 self.ask.set_icons(icons.clone());
290 self.rail = ActivityRail::new(key_bindings, icons, rail_caps);
291 self.rail.set_cursor(active);
292 }
293
294 #[cfg(test)]
296 pub fn rail_shows(&self, view: DrawerView) -> bool {
297 self.rail.shows(view)
298 }
299
300 pub fn set_ask_client(&mut self, client: Option<Arc<RagClient>>) {
306 self.ask.set_client(client);
307 }
308
309 pub fn focused(&self) -> PanelKind {
312 self.order.focused()
313 }
314
315 pub fn focused_label(&self) -> &'static str {
318 match self.order.focused() {
319 PanelKind::Drawer => self.drawer.active_view().label(),
320 PanelKind::Editor if self.is_showing_ask() => DrawerView::Ask.label(),
323 kind => kind.label(),
324 }
325 }
326
327 pub fn prev_kind(&self) -> Option<PanelKind> {
328 self.order.prev_kind()
329 }
330
331 pub fn next_kind(&self) -> Option<PanelKind> {
332 self.order.next_kind()
333 }
334
335 pub fn is_visible(&self, kind: PanelKind) -> bool {
336 self.order.is_visible(kind)
337 }
338
339 pub fn show(&mut self, kind: PanelKind) {
340 self.order.show(kind);
341 }
342
343 pub fn hide(&mut self, kind: PanelKind) {
344 self.order.hide(kind);
345 }
346
347 pub fn focus(&mut self, kind: PanelKind) {
350 if kind != PanelKind::Editor {
351 self.editor.close_autocomplete();
352 }
353 self.order.focus(kind);
354 }
355
356 pub fn active_drawer_view(&self) -> DrawerView {
359 self.drawer.active_view()
360 }
361
362 pub fn drawer_is_text_input(&self) -> bool {
365 self.drawer.is_text_input()
366 }
367
368 pub fn adjust_drawer_width(&mut self, delta: i16) {
371 let Some(right) = self.column_rects.iter().map(|(_, r)| r.right()).max() else {
374 return;
375 };
376 let new = self.drawer_width.saturating_add_signed(delta);
377 let max_width = right.saturating_sub(RAIL_WIDTH + MIN_EDITOR_WIDTH);
378 self.drawer_width = new.clamp(MIN_DRAWER_WIDTH, max_width.max(MIN_DRAWER_WIDTH));
379 }
380
381 pub fn switch_drawer_view(&mut self, view: DrawerView) {
385 self.drawer.set_view(view);
386 self.rail.set_cursor(view);
387 }
388
389 pub fn open_drawer_view(&mut self, view: DrawerView) {
392 self.switch_drawer_view(view);
393 self.order.show(PanelKind::Drawer);
394 }
395
396 pub fn sidebar(&self) -> &SidebarComponent {
399 self.drawer.sidebar()
400 }
401 pub fn sidebar_mut(&mut self) -> &mut SidebarComponent {
402 self.drawer.sidebar_mut()
403 }
404 pub fn editor(&self) -> Option<&TextEditorComponent> {
408 matches!(self.content, EditorAreaContent::Note).then_some(&self.editor)
409 }
410 pub fn editor_mut(&mut self) -> Option<&mut TextEditorComponent> {
411 if !matches!(self.content, EditorAreaContent::Note) {
412 return None;
413 }
414 Some(&mut self.editor)
415 }
416
417 pub fn show_attachment(&mut self, view: AttachmentView) {
421 self.editor.close_autocomplete();
422 self.content = EditorAreaContent::Attachment(Box::new(view));
423 self.order.focus(PanelKind::Editor);
424 }
425
426 pub fn clear_attachment(&mut self) {
432 if matches!(self.content, EditorAreaContent::Attachment(_)) {
433 self.content = EditorAreaContent::Note;
434 }
435 }
436
437 pub fn is_showing_attachment(&self) -> bool {
439 matches!(self.content, EditorAreaContent::Attachment(_))
440 }
441
442 pub fn attachment_path(&self) -> Option<&kimun_core::nfs::VaultPath> {
445 match &self.content {
446 EditorAreaContent::Attachment(view) => Some(view.path()),
447 _ => None,
448 }
449 }
450
451 pub fn show_ask(&mut self) {
456 self.editor.close_autocomplete();
457 self.content = EditorAreaContent::Ask;
458 self.order.focus(PanelKind::Editor);
459 }
460
461 pub fn hide_ask(&mut self) {
465 if matches!(self.content, EditorAreaContent::Ask) {
466 self.content = EditorAreaContent::Note;
467 }
468 }
469
470 pub fn ask(&self) -> &ThreadPanel {
473 &self.ask
474 }
475 pub fn ask_mut(&mut self) -> &mut ThreadPanel {
477 &mut self.ask
478 }
479
480 pub fn is_showing_ask(&self) -> bool {
482 matches!(self.content, EditorAreaContent::Ask)
483 }
484
485 fn editor_area(&self) -> &dyn Component {
491 match &self.content {
492 EditorAreaContent::Attachment(view) => view.as_ref(),
493 EditorAreaContent::Ask => &self.ask,
494 EditorAreaContent::Note => &self.editor,
495 }
496 }
497 pub fn query(&self) -> &QueryPanel {
498 self.drawer.query()
499 }
500 pub fn query_mut(&mut self) -> &mut QueryPanel {
501 self.drawer.query_mut()
502 }
503 pub fn semantic_mut(&mut self) -> &mut crate::components::semantic_search::SemanticPanel {
504 self.drawer.semantic_mut()
505 }
506 pub fn ask_sources_mut(&mut self) -> &mut crate::components::ask_sources::SourcesPanel {
507 self.drawer.ask_sources_mut()
508 }
509 pub fn tags_mut(&mut self) -> &mut crate::components::drawer_views::TagsPanel {
510 self.drawer.tags_mut()
511 }
512 pub fn links_mut(&mut self) -> &mut crate::components::drawer_views::LinksPanel {
513 self.drawer.links_mut()
514 }
515 pub fn outline_mut(&mut self) -> &mut crate::components::drawer_views::OutlinePanel {
516 self.drawer.outline_mut()
517 }
518 pub fn sync_outline(&mut self, note: &kimun_core::nfs::VaultPath, tx: &AppTx) {
524 let editor = matches!(self.content, EditorAreaContent::Note).then_some(&self.editor);
525 let revision = editor.map(|e| e.content_revision());
526 self.drawer.outline_mut().sync(
527 note,
528 revision,
529 || editor.map(|e| e.get_text()).unwrap_or_default(),
530 tx,
531 );
532 }
533 pub fn drawer_set_config_info(&mut self, info: crate::components::drawer::ConfigInfo) {
534 self.drawer.set_config_info(info);
535 }
536
537 pub fn focused_hints(&self) -> Vec<(String, String)> {
541 match self.order.focused() {
542 PanelKind::Rail => self.rail.hint_shortcuts(),
543 PanelKind::Drawer => self.drawer.hint_shortcuts(),
544 PanelKind::Editor => self.editor_area().hint_shortcuts(),
545 }
546 }
547
548 pub fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
550 match self.order.focused() {
551 PanelKind::Rail => self.rail.handle_input(event, tx),
552 PanelKind::Drawer => self.drawer.handle_input(event, tx),
553 PanelKind::Editor => self.editor_area_handle_input(event, tx),
554 }
555 }
556
557 fn editor_area_handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
562 match &mut self.content {
563 EditorAreaContent::Ask => self.ask.handle_input(event, tx),
564 EditorAreaContent::Attachment(view) => view.handle_input(event, tx),
565 EditorAreaContent::Note => self.editor.handle_input(event, tx),
566 }
567 }
568
569 pub fn is_note_buffer_cell(&self, column: u16, row: u16) -> bool {
594 matches!(self.content, EditorAreaContent::Note)
595 && self.editor.mouse_drives_cursor()
596 && self.editor.covers(column, row)
597 }
598
599 pub fn pointer_on_editor(&self, column: u16, row: u16) -> bool {
603 !self.dragging_divider
604 && kind_at(&self.column_rects, column, row) == Some(PanelKind::Editor)
605 }
606
607 fn on_divider(&self, column: u16, row: u16) -> bool {
610 self.column_rects
611 .iter()
612 .find(|(kind, _)| *kind == PanelKind::Drawer)
613 .is_some_and(|(_, rect)| {
614 rect.height > 0
615 && column == rect.right().saturating_sub(1)
616 && row >= rect.y
617 && row < rect.bottom()
618 })
619 }
620
621 fn drag_divider_to(&mut self, column: u16) {
624 let Some((_, drawer_rect)) = self
625 .column_rects
626 .iter()
627 .find(|(kind, _)| *kind == PanelKind::Drawer)
628 else {
629 return;
630 };
631 let total_right = self
632 .column_rects
633 .iter()
634 .map(|(_, r)| r.right())
635 .max()
636 .unwrap_or(drawer_rect.right());
637 let max_width = total_right
638 .saturating_sub(drawer_rect.x)
639 .saturating_sub(MIN_EDITOR_WIDTH);
640 let new_width = column.saturating_sub(drawer_rect.x).saturating_add(1);
641 self.drawer_width = new_width.clamp(MIN_DRAWER_WIDTH, max_width.max(MIN_DRAWER_WIDTH));
642 }
643
644 pub fn handle_mouse(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
652 let InputEvent::Mouse(mouse) = event else {
653 return EventState::NotConsumed;
654 };
655
656 match mouse.kind {
658 MouseEventKind::Down(MouseButton::Left) if self.on_divider(mouse.column, mouse.row) => {
659 self.dragging_divider = true;
660 return EventState::Consumed;
661 }
662 MouseEventKind::Drag(MouseButton::Left) if self.dragging_divider => {
663 self.drag_divider_to(mouse.column);
664 return EventState::Consumed;
665 }
666 MouseEventKind::Up(MouseButton::Left) if self.dragging_divider => {
667 self.dragging_divider = false;
668 return EventState::Consumed;
669 }
670 _ => {}
671 }
672
673 let Some(kind) = kind_at(&self.column_rects, mouse.column, mouse.row) else {
674 return EventState::NotConsumed;
675 };
676 if matches!(mouse.kind, MouseEventKind::Down(_)) {
677 self.focus(kind);
678 }
679 match kind {
680 PanelKind::Rail => {
681 self.rail.handle_input(event, tx);
682 }
683 PanelKind::Drawer => {
684 self.drawer.handle_mouse(event, tx);
685 }
686 PanelKind::Editor => {
687 self.editor_area_handle_input(event, tx);
688 }
689 }
690 EventState::Consumed
691 }
692
693 pub fn render(&mut self, f: &mut Frame, area: Rect, theme: &Theme, show_focus: bool) {
697 let visible = self.order.visible_in_order();
698 if visible.is_empty() {
699 return;
700 }
701 let columns = layout_columns(&visible, area, self.drawer_width);
702 self.column_rects = columns.clone();
703
704 let focused = self.order.focused();
705 let drawer_view = self
706 .is_visible(PanelKind::Drawer)
707 .then(|| self.drawer.active_view());
708 for (kind, rect) in &columns {
709 let is_focused = show_focus && *kind == focused;
710 let rect = *rect;
711 match kind {
712 PanelKind::Rail => self.rail.render(f, rect, theme, is_focused, drawer_view),
713 PanelKind::Drawer => self.drawer.render(f, rect, theme, is_focused),
714 PanelKind::Editor => {
715 match &mut self.content {
720 EditorAreaContent::Attachment(view) => {
721 let block = panel_block("Attachment", theme, is_focused);
722 let inner = block.inner(rect);
723 f.render_widget(block, rect);
724 view.render(f, inner, theme, is_focused);
725 }
726 EditorAreaContent::Ask => {
727 let block = panel_block("Ask", theme, is_focused);
728 let inner = block.inner(rect);
729 f.render_widget(block, rect);
730 self.ask.render(f, inner, theme, is_focused);
731 }
732 EditorAreaContent::Note => {
733 let title = if self.editor.is_dirty() {
734 "Editor [+]"
735 } else {
736 "Editor"
737 };
738 let block = panel_block(title, theme, is_focused);
739 let inner = block.inner(rect);
740 f.render_widget(block, rect);
741 self.editor.render(f, inner, theme, is_focused);
742 }
743 }
744 }
745 }
746 }
747 }
748}
749
750#[cfg(test)]
751mod tests {
752 use super::*;
753 use crate::settings::AppSettings;
754 use crate::test_support::{mouse_down_at, temp_vault};
755 use ratatui::crossterm::event::{KeyModifiers, MouseEvent, MouseEventKind};
756 use tokio::sync::mpsc::unbounded_channel;
757
758 async fn make_panel_set() -> PanelSet {
759 let vault = temp_vault("panelset").await;
760 vault.validate_and_init().await.unwrap();
761 let settings = AppSettings::default();
762 let sidebar = SidebarComponent::new(
763 settings.key_bindings.clone(),
764 vault.clone(),
765 settings.icons(),
766 &settings,
767 );
768 let editor = TextEditorComponent::new(settings.key_bindings.clone(), &settings);
769 let query = QueryPanel::new(
770 vault.clone(),
771 settings.key_bindings.clone(),
772 settings.icons(),
773 );
774 let tags = crate::components::drawer_views::TagsPanel::new(
775 vault.clone(),
776 settings.icons(),
777 settings.yank_combos(),
778 );
779 let links = crate::components::drawer_views::LinksPanel::new(
780 vault.clone(),
781 settings.icons(),
782 settings.yank_combos(),
783 );
784 let semantic = crate::components::semantic_search::SemanticPanel::new(
785 vault.clone(),
786 std::sync::Arc::new(std::sync::RwLock::new(settings.clone())),
787 settings.icons(),
788 settings.yank_combos(),
789 );
790 let outline = crate::components::drawer_views::OutlinePanel::new(
791 settings.icons(),
792 settings.yank_combos(),
793 );
794 let drawer = DrawerHost::new(
795 vault,
796 &settings.key_bindings,
797 sidebar,
798 query,
799 semantic,
800 tags,
801 links,
802 outline,
803 );
804 PanelSet::from_panels(
805 drawer,
806 editor,
807 settings.icons(),
808 settings.key_bindings,
809 RailCaps {
810 semantic: true,
811 ask: true,
812 },
813 )
814 }
815
816 fn lay_out(panels: &mut PanelSet) {
823 let area = Rect::new(0, 0, 120, 40);
824 let theme = crate::settings::themes::Theme::default();
825 let mut term =
826 ratatui::Terminal::new(ratatui::backend::TestBackend::new(area.width, area.height))
827 .unwrap();
828 term.draw(|f| panels.render(f, area, &theme, true)).unwrap();
829 }
830
831 fn editor_cell(panels: &PanelSet) -> (u16, u16) {
833 let (_, rect) = panels
834 .column_rects
835 .iter()
836 .find(|(kind, _)| *kind == PanelKind::Editor)
837 .expect("the editor is always visible");
838 (rect.x + rect.width / 2, rect.y + rect.height / 2)
839 }
840
841 #[tokio::test]
846 async fn the_note_buffer_gate_tracks_what_the_editor_area_shows() {
847 let mut panels = make_panel_set().await;
848 lay_out(&mut panels);
849 let (col, row) = editor_cell(&panels);
850 assert!(
851 panels.is_note_buffer_cell(col, row),
852 "the note buffer is what the gate is for"
853 );
854
855 panels.show_ask();
856 assert!(
857 !panels.is_note_buffer_cell(col, row),
858 "the Ask workspace has no cursor to point at"
859 );
860
861 panels.hide_ask();
862 assert!(panels.is_note_buffer_cell(col, row), "and back again");
863 }
864
865 #[tokio::test]
868 async fn the_note_buffer_gate_rejects_the_divider_and_other_columns() {
869 let mut panels = make_panel_set().await;
870 lay_out(&mut panels);
871 let (_, drawer) = panels
872 .column_rects
873 .iter()
874 .find(|(kind, _)| *kind == PanelKind::Drawer)
875 .expect("the drawer is visible by default");
876 let (divider_col, row) = (drawer.right().saturating_sub(1), drawer.y + 1);
877 assert!(panels.on_divider(divider_col, row), "precondition");
878 assert!(!panels.is_note_buffer_cell(divider_col, row));
879 assert!(
880 !panels.is_note_buffer_cell(drawer.x + 1, row),
881 "and the drawer is not the editor"
882 );
883 }
884
885 #[tokio::test]
892 async fn the_note_buffer_gate_rejects_the_editor_frame() {
893 let mut panels = make_panel_set().await;
894 lay_out(&mut panels);
895 let (_, rect) = panels
896 .column_rects
897 .iter()
898 .find(|(kind, _)| *kind == PanelKind::Editor)
899 .expect("the editor is always visible");
900 let rect = *rect;
901 let (mid_col, mid_row) = (rect.x + rect.width / 2, rect.y + rect.height / 2);
902 assert!(
903 panels.is_note_buffer_cell(mid_col, mid_row),
904 "precondition: the interior still counts"
905 );
906 assert!(
907 !panels.is_note_buffer_cell(mid_col, rect.y),
908 "the title row is frame"
909 );
910 assert!(
911 !panels.is_note_buffer_cell(mid_col, rect.bottom() - 1),
912 "so is the bottom border"
913 );
914 assert!(
915 !panels.is_note_buffer_cell(rect.x, mid_row),
916 "so is the left border"
917 );
918 assert!(
919 !panels.is_note_buffer_cell(rect.right() - 1, mid_row),
920 "so is the right border"
921 );
922 }
923
924 fn scroll_at(col: u16, row: u16) -> InputEvent {
925 InputEvent::Mouse(MouseEvent {
926 kind: MouseEventKind::ScrollDown,
927 column: col,
928 row,
929 modifiers: KeyModifiers::NONE,
930 })
931 }
932
933 fn drag_at(col: u16, row: u16) -> InputEvent {
934 InputEvent::Mouse(MouseEvent {
935 kind: MouseEventKind::Drag(MouseButton::Left),
936 column: col,
937 row,
938 modifiers: KeyModifiers::NONE,
939 })
940 }
941
942 fn up_at(col: u16, row: u16) -> InputEvent {
943 InputEvent::Mouse(MouseEvent {
944 kind: MouseEventKind::Up(MouseButton::Left),
945 column: col,
946 row,
947 modifiers: KeyModifiers::NONE,
948 })
949 }
950
951 #[tokio::test]
954 async fn click_focuses_panel_under_cursor() {
955 let mut panels = make_panel_set().await;
956 lay_out(&mut panels);
957 let (tx, _rx) = unbounded_channel();
958
959 assert_eq!(panels.focused(), PanelKind::Editor);
960 assert_eq!(
962 panels.handle_mouse(&mouse_down_at(3, 5), &tx),
963 EventState::Consumed
964 );
965 assert_eq!(panels.focused(), PanelKind::Rail);
966 assert_eq!(
967 panels.handle_mouse(&mouse_down_at(20, 5), &tx),
968 EventState::Consumed
969 );
970 assert_eq!(panels.focused(), PanelKind::Drawer);
971 assert_eq!(
974 panels.handle_mouse(&mouse_down_at(60, 5), &tx),
975 EventState::Consumed
976 );
977 assert_eq!(panels.focused(), PanelKind::Editor);
978 }
979
980 #[tokio::test]
983 async fn click_outside_panels_changes_nothing() {
984 let mut panels = make_panel_set().await;
985 let (tx, _rx) = unbounded_channel();
986
987 assert_eq!(
989 panels.handle_mouse(&mouse_down_at(10, 10), &tx),
990 EventState::NotConsumed
991 );
992 assert_eq!(panels.focused(), PanelKind::Editor);
993
994 lay_out(&mut panels);
995 assert_eq!(
997 panels.handle_mouse(&mouse_down_at(10, 50), &tx),
998 EventState::NotConsumed
999 );
1000 assert_eq!(panels.focused(), PanelKind::Editor);
1001 }
1002
1003 #[tokio::test]
1005 async fn scroll_does_not_change_focus() {
1006 let mut panels = make_panel_set().await;
1007 lay_out(&mut panels);
1008 let (tx, _rx) = unbounded_channel();
1009
1010 assert_eq!(
1011 panels.handle_mouse(&scroll_at(20, 5), &tx),
1012 EventState::Consumed
1013 );
1014 assert_eq!(panels.focused(), PanelKind::Editor);
1015 }
1016
1017 #[tokio::test]
1020 async fn divider_drag_resizes_drawer() {
1021 let mut panels = make_panel_set().await;
1022 lay_out(&mut panels);
1023 let (tx, _rx) = unbounded_channel();
1024
1025 assert_eq!(
1027 panels.handle_mouse(&mouse_down_at(40, 5), &tx),
1028 EventState::Consumed
1029 );
1030 assert_eq!(
1031 panels.handle_mouse(&drag_at(60, 5), &tx),
1032 EventState::Consumed
1033 );
1034 assert_eq!(panels.drawer_width, 54); assert_eq!(
1037 panels.handle_mouse(&drag_at(0, 5), &tx),
1038 EventState::Consumed
1039 );
1040 assert_eq!(panels.drawer_width, MIN_DRAWER_WIDTH);
1041 assert_eq!(
1043 panels.handle_mouse(&drag_at(200, 5), &tx),
1044 EventState::Consumed
1045 );
1046 assert_eq!(panels.drawer_width, 120 - 7 - MIN_EDITOR_WIDTH);
1047 assert_eq!(
1049 panels.handle_mouse(&up_at(80, 5), &tx),
1050 EventState::Consumed
1051 );
1052 let width_before = panels.drawer_width;
1053 panels.handle_mouse(&drag_at(50, 5), &tx);
1054 assert_eq!(panels.drawer_width, width_before);
1055 }
1056
1057 #[test]
1058 fn default_focus_is_editor() {
1059 let order = PanelOrder::new();
1060 assert_eq!(order.focused(), PanelKind::Editor);
1061 }
1062
1063 #[test]
1064 fn focus_cycle_wraps_over_visible_panels() {
1065 let mut order = PanelOrder::new();
1066 assert_eq!(order.next_kind(), Some(PanelKind::Rail));
1068 assert_eq!(order.prev_kind(), Some(PanelKind::Drawer));
1069
1070 order.focus(PanelKind::Rail);
1071 assert_eq!(order.prev_kind(), Some(PanelKind::Editor)); assert_eq!(order.next_kind(), Some(PanelKind::Drawer));
1073 }
1074
1075 #[test]
1076 fn focus_cycle_skips_hidden_drawer() {
1077 let mut order = PanelOrder::new();
1078 order.hide(PanelKind::Drawer);
1079 assert_eq!(order.next_kind(), Some(PanelKind::Rail));
1080 order.focus(PanelKind::Rail);
1081 assert_eq!(order.next_kind(), Some(PanelKind::Editor));
1082 }
1083
1084 #[test]
1085 fn show_hide_toggles_visibility_except_rail_and_editor() {
1086 let mut order = PanelOrder::new();
1087 assert!(order.is_visible(PanelKind::Drawer));
1088
1089 order.hide(PanelKind::Drawer);
1090 assert!(!order.is_visible(PanelKind::Drawer));
1091 order.show(PanelKind::Drawer);
1092 assert!(order.is_visible(PanelKind::Drawer));
1093
1094 order.hide(PanelKind::Editor);
1096 assert!(order.is_visible(PanelKind::Editor));
1097 order.hide(PanelKind::Rail);
1098 assert!(order.is_visible(PanelKind::Rail));
1099 }
1100
1101 #[test]
1102 fn hiding_focused_panel_moves_focus_to_visible() {
1103 let mut order = PanelOrder::new();
1104 order.focus(PanelKind::Drawer);
1105 order.hide(PanelKind::Drawer);
1106 assert!(order.is_visible(order.focused()));
1108 }
1109
1110 #[test]
1111 fn layout_columns_splits_area_in_panel_order() {
1112 let area = Rect::new(0, 0, 120, 40);
1113 let visible = [PanelKind::Rail, PanelKind::Drawer, PanelKind::Editor];
1114 let columns = layout_columns(&visible, area, DEFAULT_DRAWER_WIDTH);
1115
1116 assert_eq!(columns.len(), 3);
1117 assert_eq!(columns[0].0, PanelKind::Rail);
1119 assert_eq!(columns[0].1.width, RAIL_WIDTH);
1120 assert_eq!(columns[1].0, PanelKind::Drawer);
1121 assert_eq!(columns[1].1.width, DEFAULT_DRAWER_WIDTH);
1122 assert_eq!(columns[2].0, PanelKind::Editor);
1123 assert_eq!(columns[2].1.width, 120 - RAIL_WIDTH - DEFAULT_DRAWER_WIDTH);
1124 assert_eq!(columns[0].1.x, 0);
1126 assert_eq!(columns[1].1.x, RAIL_WIDTH);
1127 assert_eq!(columns[2].1.x, RAIL_WIDTH + DEFAULT_DRAWER_WIDTH);
1128 }
1129
1130 #[test]
1131 fn hidden_drawer_gives_width_to_editor() {
1132 let area = Rect::new(0, 0, 120, 40);
1133 let visible = [PanelKind::Rail, PanelKind::Editor];
1134 let columns = layout_columns(&visible, area, DEFAULT_DRAWER_WIDTH);
1135
1136 assert_eq!(columns.len(), 2);
1137 assert_eq!(columns[1].0, PanelKind::Editor);
1138 assert_eq!(columns[1].1.width, 120 - RAIL_WIDTH);
1139 }
1140
1141 #[test]
1142 fn kind_at_hit_tests_panel_columns() {
1143 let area = Rect::new(0, 0, 120, 40);
1144 let visible = [PanelKind::Rail, PanelKind::Drawer, PanelKind::Editor];
1145 let columns = layout_columns(&visible, area, DEFAULT_DRAWER_WIDTH);
1146
1147 assert_eq!(kind_at(&columns, 0, 0), Some(PanelKind::Rail));
1148 assert_eq!(kind_at(&columns, 6, 10), Some(PanelKind::Rail));
1149 assert_eq!(kind_at(&columns, 7, 10), Some(PanelKind::Drawer));
1150 assert_eq!(kind_at(&columns, 40, 39), Some(PanelKind::Drawer));
1151 assert_eq!(kind_at(&columns, 41, 0), Some(PanelKind::Editor));
1152 assert_eq!(kind_at(&columns, 119, 39), Some(PanelKind::Editor));
1153 assert_eq!(kind_at(&columns, 120, 10), None);
1155 assert_eq!(kind_at(&columns, 10, 40), None);
1156 assert_eq!(kind_at(&[], 10, 10), None);
1158 }
1159
1160 #[test]
1161 fn open_drawer_view_reveals_and_switches() {
1162 let mut order = PanelOrder::new();
1163 order.hide(PanelKind::Drawer);
1164 assert!(!order.is_visible(PanelKind::Drawer));
1165 order.show(PanelKind::Drawer);
1166 assert!(order.is_visible(PanelKind::Drawer));
1167 }
1168
1169 fn test_attachment_view() -> AttachmentView {
1170 let details = kimun_core::AttachmentDetails {
1171 path: kimun_core::nfs::VaultPath::new("a.png"),
1172 size: 0,
1173 modified_secs: 0,
1174 extension: Some("png".to_string()),
1175 content: kimun_core::AttachmentContent::Binary,
1176 };
1177 AttachmentView::new(
1178 details,
1179 crate::settings::icons::Icons::new(false),
1180 crate::keys::KeyBindings::empty(),
1181 )
1182 }
1183
1184 #[tokio::test]
1185 async fn ask_content_hides_the_editor_and_leaving_restores_it() {
1186 let mut ps = make_panel_set().await;
1187 assert!(ps.editor().is_some());
1188 ps.show_ask();
1189 assert!(ps.editor().is_none());
1190 assert!(ps.is_showing_ask());
1191 ps.hide_ask();
1193 assert!(!ps.is_showing_ask());
1194 assert!(ps.editor().is_some());
1195 }
1196
1197 #[tokio::test]
1201 async fn set_ask_client_drives_the_resident_panels_client() {
1202 let mut ps = make_panel_set().await;
1203 ps.set_ask_client(None);
1204 assert!(!ps.ask().has_client());
1205 let client = std::sync::Arc::new(crate::server_client::RagClient::new(
1208 "http://localhost:0".to_string(),
1209 None,
1210 "vault".to_string(),
1211 ));
1212 ps.set_ask_client(Some(client));
1213 assert!(ps.ask().has_client());
1214 }
1215
1216 #[tokio::test]
1217 async fn showing_an_attachment_replaces_ask() {
1218 let mut ps = make_panel_set().await;
1219 ps.show_ask();
1220 ps.show_attachment(test_attachment_view());
1221 assert!(!ps.is_showing_ask());
1222 assert!(ps.is_showing_attachment());
1223 }
1224
1225 #[test]
1226 fn visible_in_order_skips_hidden() {
1227 let mut order = PanelOrder::new();
1228 assert_eq!(
1229 order.visible_in_order(),
1230 vec![PanelKind::Rail, PanelKind::Drawer, PanelKind::Editor]
1231 );
1232 order.hide(PanelKind::Drawer);
1233 assert_eq!(
1234 order.visible_in_order(),
1235 vec![PanelKind::Rail, PanelKind::Editor]
1236 );
1237 }
1238}