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 drawer_set_config_info(&mut self, info: crate::components::drawer::ConfigInfo) {
519 self.drawer.set_config_info(info);
520 }
521
522 pub fn focused_hints(&self) -> Vec<(String, String)> {
526 match self.order.focused() {
527 PanelKind::Rail => self.rail.hint_shortcuts(),
528 PanelKind::Drawer => self.drawer.hint_shortcuts(),
529 PanelKind::Editor => self.editor_area().hint_shortcuts(),
530 }
531 }
532
533 pub fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
535 match self.order.focused() {
536 PanelKind::Rail => self.rail.handle_input(event, tx),
537 PanelKind::Drawer => self.drawer.handle_input(event, tx),
538 PanelKind::Editor => self.editor_area_handle_input(event, tx),
539 }
540 }
541
542 fn editor_area_handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
547 match &mut self.content {
548 EditorAreaContent::Ask => self.ask.handle_input(event, tx),
549 EditorAreaContent::Attachment(view) => view.handle_input(event, tx),
550 EditorAreaContent::Note => self.editor.handle_input(event, tx),
551 }
552 }
553
554 pub fn is_note_buffer_cell(&self, column: u16, row: u16) -> bool {
579 matches!(self.content, EditorAreaContent::Note)
580 && self.editor.mouse_drives_cursor()
581 && self.editor.covers(column, row)
582 }
583
584 fn on_divider(&self, column: u16, row: u16) -> bool {
587 self.column_rects
588 .iter()
589 .find(|(kind, _)| *kind == PanelKind::Drawer)
590 .is_some_and(|(_, rect)| {
591 rect.height > 0
592 && column == rect.right().saturating_sub(1)
593 && row >= rect.y
594 && row < rect.bottom()
595 })
596 }
597
598 fn drag_divider_to(&mut self, column: u16) {
601 let Some((_, drawer_rect)) = self
602 .column_rects
603 .iter()
604 .find(|(kind, _)| *kind == PanelKind::Drawer)
605 else {
606 return;
607 };
608 let total_right = self
609 .column_rects
610 .iter()
611 .map(|(_, r)| r.right())
612 .max()
613 .unwrap_or(drawer_rect.right());
614 let max_width = total_right
615 .saturating_sub(drawer_rect.x)
616 .saturating_sub(MIN_EDITOR_WIDTH);
617 let new_width = column.saturating_sub(drawer_rect.x).saturating_add(1);
618 self.drawer_width = new_width.clamp(MIN_DRAWER_WIDTH, max_width.max(MIN_DRAWER_WIDTH));
619 }
620
621 pub fn handle_mouse(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
629 let InputEvent::Mouse(mouse) = event else {
630 return EventState::NotConsumed;
631 };
632
633 match mouse.kind {
635 MouseEventKind::Down(MouseButton::Left) if self.on_divider(mouse.column, mouse.row) => {
636 self.dragging_divider = true;
637 return EventState::Consumed;
638 }
639 MouseEventKind::Drag(MouseButton::Left) if self.dragging_divider => {
640 self.drag_divider_to(mouse.column);
641 return EventState::Consumed;
642 }
643 MouseEventKind::Up(MouseButton::Left) if self.dragging_divider => {
644 self.dragging_divider = false;
645 return EventState::Consumed;
646 }
647 _ => {}
648 }
649
650 let Some(kind) = kind_at(&self.column_rects, mouse.column, mouse.row) else {
651 return EventState::NotConsumed;
652 };
653 if matches!(mouse.kind, MouseEventKind::Down(_)) {
654 self.focus(kind);
655 }
656 match kind {
657 PanelKind::Rail => {
658 self.rail.handle_input(event, tx);
659 }
660 PanelKind::Drawer => {
661 self.drawer.handle_mouse(event, tx);
662 }
663 PanelKind::Editor => {
664 self.editor_area_handle_input(event, tx);
665 }
666 }
667 EventState::Consumed
668 }
669
670 pub fn render(&mut self, f: &mut Frame, area: Rect, theme: &Theme, show_focus: bool) {
674 let visible = self.order.visible_in_order();
675 if visible.is_empty() {
676 return;
677 }
678 let columns = layout_columns(&visible, area, self.drawer_width);
679 self.column_rects = columns.clone();
680
681 let focused = self.order.focused();
682 let drawer_view = self
683 .is_visible(PanelKind::Drawer)
684 .then(|| self.drawer.active_view());
685 for (kind, rect) in &columns {
686 let is_focused = show_focus && *kind == focused;
687 let rect = *rect;
688 match kind {
689 PanelKind::Rail => self.rail.render(f, rect, theme, is_focused, drawer_view),
690 PanelKind::Drawer => self.drawer.render(f, rect, theme, is_focused),
691 PanelKind::Editor => {
692 match &mut self.content {
697 EditorAreaContent::Attachment(view) => {
698 let block = panel_block("Attachment", theme, is_focused);
699 let inner = block.inner(rect);
700 f.render_widget(block, rect);
701 view.render(f, inner, theme, is_focused);
702 }
703 EditorAreaContent::Ask => {
704 let block = panel_block("Ask", theme, is_focused);
705 let inner = block.inner(rect);
706 f.render_widget(block, rect);
707 self.ask.render(f, inner, theme, is_focused);
708 }
709 EditorAreaContent::Note => {
710 let title = if self.editor.is_dirty() {
711 "Editor [+]"
712 } else {
713 "Editor"
714 };
715 let block = panel_block(title, theme, is_focused);
716 let inner = block.inner(rect);
717 f.render_widget(block, rect);
718 self.editor.render(f, inner, theme, is_focused);
719 }
720 }
721 }
722 }
723 }
724 }
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730 use crate::settings::AppSettings;
731 use crate::test_support::{mouse_down_at, temp_vault};
732 use ratatui::crossterm::event::{KeyModifiers, MouseEvent, MouseEventKind};
733 use tokio::sync::mpsc::unbounded_channel;
734
735 async fn make_panel_set() -> PanelSet {
736 let vault = temp_vault("panelset").await;
737 vault.validate_and_init().await.unwrap();
738 let settings = AppSettings::default();
739 let sidebar = SidebarComponent::new(
740 settings.key_bindings.clone(),
741 vault.clone(),
742 settings.icons(),
743 &settings,
744 );
745 let editor = TextEditorComponent::new(settings.key_bindings.clone(), &settings);
746 let query = QueryPanel::new(
747 vault.clone(),
748 settings.key_bindings.clone(),
749 settings.icons(),
750 );
751 let tags = crate::components::drawer_views::TagsPanel::new(
752 vault.clone(),
753 settings.icons(),
754 settings.yank_combos(),
755 );
756 let links = crate::components::drawer_views::LinksPanel::new(
757 vault.clone(),
758 settings.icons(),
759 settings.yank_combos(),
760 );
761 let semantic = crate::components::semantic_search::SemanticPanel::new(
762 vault.clone(),
763 std::sync::Arc::new(std::sync::RwLock::new(settings.clone())),
764 settings.icons(),
765 settings.yank_combos(),
766 );
767 let outline = crate::components::drawer_views::OutlinePanel::new(
768 vault.clone(),
769 settings.icons(),
770 settings.yank_combos(),
771 );
772 let drawer = DrawerHost::new(
773 vault,
774 &settings.key_bindings,
775 sidebar,
776 query,
777 semantic,
778 tags,
779 links,
780 outline,
781 );
782 PanelSet::from_panels(
783 drawer,
784 editor,
785 settings.icons(),
786 settings.key_bindings,
787 RailCaps {
788 semantic: true,
789 ask: true,
790 },
791 )
792 }
793
794 fn lay_out(panels: &mut PanelSet) {
801 let area = Rect::new(0, 0, 120, 40);
802 let theme = crate::settings::themes::Theme::default();
803 let mut term =
804 ratatui::Terminal::new(ratatui::backend::TestBackend::new(area.width, area.height))
805 .unwrap();
806 term.draw(|f| panels.render(f, area, &theme, true)).unwrap();
807 }
808
809 fn editor_cell(panels: &PanelSet) -> (u16, u16) {
811 let (_, rect) = panels
812 .column_rects
813 .iter()
814 .find(|(kind, _)| *kind == PanelKind::Editor)
815 .expect("the editor is always visible");
816 (rect.x + rect.width / 2, rect.y + rect.height / 2)
817 }
818
819 #[tokio::test]
824 async fn the_note_buffer_gate_tracks_what_the_editor_area_shows() {
825 let mut panels = make_panel_set().await;
826 lay_out(&mut panels);
827 let (col, row) = editor_cell(&panels);
828 assert!(
829 panels.is_note_buffer_cell(col, row),
830 "the note buffer is what the gate is for"
831 );
832
833 panels.show_ask();
834 assert!(
835 !panels.is_note_buffer_cell(col, row),
836 "the Ask workspace has no cursor to point at"
837 );
838
839 panels.hide_ask();
840 assert!(panels.is_note_buffer_cell(col, row), "and back again");
841 }
842
843 #[tokio::test]
846 async fn the_note_buffer_gate_rejects_the_divider_and_other_columns() {
847 let mut panels = make_panel_set().await;
848 lay_out(&mut panels);
849 let (_, drawer) = panels
850 .column_rects
851 .iter()
852 .find(|(kind, _)| *kind == PanelKind::Drawer)
853 .expect("the drawer is visible by default");
854 let (divider_col, row) = (drawer.right().saturating_sub(1), drawer.y + 1);
855 assert!(panels.on_divider(divider_col, row), "precondition");
856 assert!(!panels.is_note_buffer_cell(divider_col, row));
857 assert!(
858 !panels.is_note_buffer_cell(drawer.x + 1, row),
859 "and the drawer is not the editor"
860 );
861 }
862
863 #[tokio::test]
870 async fn the_note_buffer_gate_rejects_the_editor_frame() {
871 let mut panels = make_panel_set().await;
872 lay_out(&mut panels);
873 let (_, rect) = panels
874 .column_rects
875 .iter()
876 .find(|(kind, _)| *kind == PanelKind::Editor)
877 .expect("the editor is always visible");
878 let rect = *rect;
879 let (mid_col, mid_row) = (rect.x + rect.width / 2, rect.y + rect.height / 2);
880 assert!(
881 panels.is_note_buffer_cell(mid_col, mid_row),
882 "precondition: the interior still counts"
883 );
884 assert!(
885 !panels.is_note_buffer_cell(mid_col, rect.y),
886 "the title row is frame"
887 );
888 assert!(
889 !panels.is_note_buffer_cell(mid_col, rect.bottom() - 1),
890 "so is the bottom border"
891 );
892 assert!(
893 !panels.is_note_buffer_cell(rect.x, mid_row),
894 "so is the left border"
895 );
896 assert!(
897 !panels.is_note_buffer_cell(rect.right() - 1, mid_row),
898 "so is the right border"
899 );
900 }
901
902 fn scroll_at(col: u16, row: u16) -> InputEvent {
903 InputEvent::Mouse(MouseEvent {
904 kind: MouseEventKind::ScrollDown,
905 column: col,
906 row,
907 modifiers: KeyModifiers::NONE,
908 })
909 }
910
911 fn drag_at(col: u16, row: u16) -> InputEvent {
912 InputEvent::Mouse(MouseEvent {
913 kind: MouseEventKind::Drag(MouseButton::Left),
914 column: col,
915 row,
916 modifiers: KeyModifiers::NONE,
917 })
918 }
919
920 fn up_at(col: u16, row: u16) -> InputEvent {
921 InputEvent::Mouse(MouseEvent {
922 kind: MouseEventKind::Up(MouseButton::Left),
923 column: col,
924 row,
925 modifiers: KeyModifiers::NONE,
926 })
927 }
928
929 #[tokio::test]
932 async fn click_focuses_panel_under_cursor() {
933 let mut panels = make_panel_set().await;
934 lay_out(&mut panels);
935 let (tx, _rx) = unbounded_channel();
936
937 assert_eq!(panels.focused(), PanelKind::Editor);
938 assert_eq!(
940 panels.handle_mouse(&mouse_down_at(3, 5), &tx),
941 EventState::Consumed
942 );
943 assert_eq!(panels.focused(), PanelKind::Rail);
944 assert_eq!(
945 panels.handle_mouse(&mouse_down_at(20, 5), &tx),
946 EventState::Consumed
947 );
948 assert_eq!(panels.focused(), PanelKind::Drawer);
949 assert_eq!(
952 panels.handle_mouse(&mouse_down_at(60, 5), &tx),
953 EventState::Consumed
954 );
955 assert_eq!(panels.focused(), PanelKind::Editor);
956 }
957
958 #[tokio::test]
961 async fn click_outside_panels_changes_nothing() {
962 let mut panels = make_panel_set().await;
963 let (tx, _rx) = unbounded_channel();
964
965 assert_eq!(
967 panels.handle_mouse(&mouse_down_at(10, 10), &tx),
968 EventState::NotConsumed
969 );
970 assert_eq!(panels.focused(), PanelKind::Editor);
971
972 lay_out(&mut panels);
973 assert_eq!(
975 panels.handle_mouse(&mouse_down_at(10, 50), &tx),
976 EventState::NotConsumed
977 );
978 assert_eq!(panels.focused(), PanelKind::Editor);
979 }
980
981 #[tokio::test]
983 async fn scroll_does_not_change_focus() {
984 let mut panels = make_panel_set().await;
985 lay_out(&mut panels);
986 let (tx, _rx) = unbounded_channel();
987
988 assert_eq!(
989 panels.handle_mouse(&scroll_at(20, 5), &tx),
990 EventState::Consumed
991 );
992 assert_eq!(panels.focused(), PanelKind::Editor);
993 }
994
995 #[tokio::test]
998 async fn divider_drag_resizes_drawer() {
999 let mut panels = make_panel_set().await;
1000 lay_out(&mut panels);
1001 let (tx, _rx) = unbounded_channel();
1002
1003 assert_eq!(
1005 panels.handle_mouse(&mouse_down_at(40, 5), &tx),
1006 EventState::Consumed
1007 );
1008 assert_eq!(
1009 panels.handle_mouse(&drag_at(60, 5), &tx),
1010 EventState::Consumed
1011 );
1012 assert_eq!(panels.drawer_width, 54); assert_eq!(
1015 panels.handle_mouse(&drag_at(0, 5), &tx),
1016 EventState::Consumed
1017 );
1018 assert_eq!(panels.drawer_width, MIN_DRAWER_WIDTH);
1019 assert_eq!(
1021 panels.handle_mouse(&drag_at(200, 5), &tx),
1022 EventState::Consumed
1023 );
1024 assert_eq!(panels.drawer_width, 120 - 7 - MIN_EDITOR_WIDTH);
1025 assert_eq!(
1027 panels.handle_mouse(&up_at(80, 5), &tx),
1028 EventState::Consumed
1029 );
1030 let width_before = panels.drawer_width;
1031 panels.handle_mouse(&drag_at(50, 5), &tx);
1032 assert_eq!(panels.drawer_width, width_before);
1033 }
1034
1035 #[test]
1036 fn default_focus_is_editor() {
1037 let order = PanelOrder::new();
1038 assert_eq!(order.focused(), PanelKind::Editor);
1039 }
1040
1041 #[test]
1042 fn focus_cycle_wraps_over_visible_panels() {
1043 let mut order = PanelOrder::new();
1044 assert_eq!(order.next_kind(), Some(PanelKind::Rail));
1046 assert_eq!(order.prev_kind(), Some(PanelKind::Drawer));
1047
1048 order.focus(PanelKind::Rail);
1049 assert_eq!(order.prev_kind(), Some(PanelKind::Editor)); assert_eq!(order.next_kind(), Some(PanelKind::Drawer));
1051 }
1052
1053 #[test]
1054 fn focus_cycle_skips_hidden_drawer() {
1055 let mut order = PanelOrder::new();
1056 order.hide(PanelKind::Drawer);
1057 assert_eq!(order.next_kind(), Some(PanelKind::Rail));
1058 order.focus(PanelKind::Rail);
1059 assert_eq!(order.next_kind(), Some(PanelKind::Editor));
1060 }
1061
1062 #[test]
1063 fn show_hide_toggles_visibility_except_rail_and_editor() {
1064 let mut order = PanelOrder::new();
1065 assert!(order.is_visible(PanelKind::Drawer));
1066
1067 order.hide(PanelKind::Drawer);
1068 assert!(!order.is_visible(PanelKind::Drawer));
1069 order.show(PanelKind::Drawer);
1070 assert!(order.is_visible(PanelKind::Drawer));
1071
1072 order.hide(PanelKind::Editor);
1074 assert!(order.is_visible(PanelKind::Editor));
1075 order.hide(PanelKind::Rail);
1076 assert!(order.is_visible(PanelKind::Rail));
1077 }
1078
1079 #[test]
1080 fn hiding_focused_panel_moves_focus_to_visible() {
1081 let mut order = PanelOrder::new();
1082 order.focus(PanelKind::Drawer);
1083 order.hide(PanelKind::Drawer);
1084 assert!(order.is_visible(order.focused()));
1086 }
1087
1088 #[test]
1089 fn layout_columns_splits_area_in_panel_order() {
1090 let area = Rect::new(0, 0, 120, 40);
1091 let visible = [PanelKind::Rail, PanelKind::Drawer, PanelKind::Editor];
1092 let columns = layout_columns(&visible, area, DEFAULT_DRAWER_WIDTH);
1093
1094 assert_eq!(columns.len(), 3);
1095 assert_eq!(columns[0].0, PanelKind::Rail);
1097 assert_eq!(columns[0].1.width, RAIL_WIDTH);
1098 assert_eq!(columns[1].0, PanelKind::Drawer);
1099 assert_eq!(columns[1].1.width, DEFAULT_DRAWER_WIDTH);
1100 assert_eq!(columns[2].0, PanelKind::Editor);
1101 assert_eq!(columns[2].1.width, 120 - RAIL_WIDTH - DEFAULT_DRAWER_WIDTH);
1102 assert_eq!(columns[0].1.x, 0);
1104 assert_eq!(columns[1].1.x, RAIL_WIDTH);
1105 assert_eq!(columns[2].1.x, RAIL_WIDTH + DEFAULT_DRAWER_WIDTH);
1106 }
1107
1108 #[test]
1109 fn hidden_drawer_gives_width_to_editor() {
1110 let area = Rect::new(0, 0, 120, 40);
1111 let visible = [PanelKind::Rail, PanelKind::Editor];
1112 let columns = layout_columns(&visible, area, DEFAULT_DRAWER_WIDTH);
1113
1114 assert_eq!(columns.len(), 2);
1115 assert_eq!(columns[1].0, PanelKind::Editor);
1116 assert_eq!(columns[1].1.width, 120 - RAIL_WIDTH);
1117 }
1118
1119 #[test]
1120 fn kind_at_hit_tests_panel_columns() {
1121 let area = Rect::new(0, 0, 120, 40);
1122 let visible = [PanelKind::Rail, PanelKind::Drawer, PanelKind::Editor];
1123 let columns = layout_columns(&visible, area, DEFAULT_DRAWER_WIDTH);
1124
1125 assert_eq!(kind_at(&columns, 0, 0), Some(PanelKind::Rail));
1126 assert_eq!(kind_at(&columns, 6, 10), Some(PanelKind::Rail));
1127 assert_eq!(kind_at(&columns, 7, 10), Some(PanelKind::Drawer));
1128 assert_eq!(kind_at(&columns, 40, 39), Some(PanelKind::Drawer));
1129 assert_eq!(kind_at(&columns, 41, 0), Some(PanelKind::Editor));
1130 assert_eq!(kind_at(&columns, 119, 39), Some(PanelKind::Editor));
1131 assert_eq!(kind_at(&columns, 120, 10), None);
1133 assert_eq!(kind_at(&columns, 10, 40), None);
1134 assert_eq!(kind_at(&[], 10, 10), None);
1136 }
1137
1138 #[test]
1139 fn open_drawer_view_reveals_and_switches() {
1140 let mut order = PanelOrder::new();
1141 order.hide(PanelKind::Drawer);
1142 assert!(!order.is_visible(PanelKind::Drawer));
1143 order.show(PanelKind::Drawer);
1144 assert!(order.is_visible(PanelKind::Drawer));
1145 }
1146
1147 fn test_attachment_view() -> AttachmentView {
1148 let details = kimun_core::AttachmentDetails {
1149 path: kimun_core::nfs::VaultPath::new("a.png"),
1150 size: 0,
1151 modified_secs: 0,
1152 extension: Some("png".to_string()),
1153 content: kimun_core::AttachmentContent::Binary,
1154 };
1155 AttachmentView::new(
1156 details,
1157 crate::settings::icons::Icons::new(false),
1158 crate::keys::KeyBindings::empty(),
1159 )
1160 }
1161
1162 #[tokio::test]
1163 async fn ask_content_hides_the_editor_and_leaving_restores_it() {
1164 let mut ps = make_panel_set().await;
1165 assert!(ps.editor().is_some());
1166 ps.show_ask();
1167 assert!(ps.editor().is_none());
1168 assert!(ps.is_showing_ask());
1169 ps.hide_ask();
1171 assert!(!ps.is_showing_ask());
1172 assert!(ps.editor().is_some());
1173 }
1174
1175 #[tokio::test]
1179 async fn set_ask_client_drives_the_resident_panels_client() {
1180 let mut ps = make_panel_set().await;
1181 ps.set_ask_client(None);
1182 assert!(!ps.ask().has_client());
1183 let client = std::sync::Arc::new(crate::server_client::RagClient::new(
1186 "http://localhost:0".to_string(),
1187 None,
1188 "vault".to_string(),
1189 ));
1190 ps.set_ask_client(Some(client));
1191 assert!(ps.ask().has_client());
1192 }
1193
1194 #[tokio::test]
1195 async fn showing_an_attachment_replaces_ask() {
1196 let mut ps = make_panel_set().await;
1197 ps.show_ask();
1198 ps.show_attachment(test_attachment_view());
1199 assert!(!ps.is_showing_ask());
1200 assert!(ps.is_showing_attachment());
1201 }
1202
1203 #[test]
1204 fn visible_in_order_skips_hidden() {
1205 let mut order = PanelOrder::new();
1206 assert_eq!(
1207 order.visible_in_order(),
1208 vec![PanelKind::Rail, PanelKind::Drawer, PanelKind::Editor]
1209 );
1210 order.hide(PanelKind::Drawer);
1211 assert_eq!(
1212 order.visible_in_order(),
1213 vec![PanelKind::Rail, PanelKind::Editor]
1214 );
1215 }
1216}