1use std::collections::VecDeque;
19
20use color_eyre::Result;
21use ratatui::{
22 Frame,
23 layout::{Constraint, Layout, Rect},
24 style::{Color, Modifier, Style},
25 text::{Line, Span},
26 widgets::{Block, Borders, Paragraph},
27};
28
29use super::Component;
30use crate::action::Action;
31use crate::log_capture::{LogCapture, LogEntry};
32use crate::state::{LOG_PANE_MAX_HEIGHT, LOG_PANE_MIN_HEIGHT};
33use crate::theme;
34
35const BEE_TAB_RING_CAPACITY: usize = 500;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum LogTab {
44 Errors,
45 Warning,
46 Info,
47 Debug,
48 BeeHttp,
49 SelfHttp,
50}
51
52impl LogTab {
53 pub const ALL: [LogTab; 6] = [
54 LogTab::Errors,
55 LogTab::Warning,
56 LogTab::Info,
57 LogTab::Debug,
58 LogTab::BeeHttp,
59 LogTab::SelfHttp,
60 ];
61
62 pub fn from_kebab(s: &str) -> Self {
66 match s {
67 "errors" => Self::Errors,
68 "warning" => Self::Warning,
69 "info" => Self::Info,
70 "debug" => Self::Debug,
71 "bee-http" => Self::BeeHttp,
72 "self-http" => Self::SelfHttp,
73 _ => Self::SelfHttp,
74 }
75 }
76
77 pub fn to_kebab(self) -> &'static str {
78 match self {
79 Self::Errors => "errors",
80 Self::Warning => "warning",
81 Self::Info => "info",
82 Self::Debug => "debug",
83 Self::BeeHttp => "bee-http",
84 Self::SelfHttp => "self-http",
85 }
86 }
87
88 pub fn label(self) -> &'static str {
90 match self {
91 Self::Errors => "Errors",
92 Self::Warning => "Warn",
93 Self::Info => "Info",
94 Self::Debug => "Debug",
95 Self::BeeHttp => "Bee HTTP",
96 Self::SelfHttp => "bee::http",
97 }
98 }
99
100 fn index(self) -> usize {
102 Self::ALL.iter().position(|t| *t == self).unwrap_or(5)
103 }
104
105 fn from_index(i: usize) -> Self {
106 Self::ALL.get(i).copied().unwrap_or(Self::SelfHttp)
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct BeeLogLine {
116 pub timestamp: String,
118 pub logger: String,
120 pub message: String,
122}
123
124pub enum LogRow<'a> {
127 Self_(&'a LogEntry),
128 Bee(&'a BeeLogLine),
129}
130
131#[derive(Debug, Default)]
134pub struct BeeLogBuffers {
135 pub errors: VecDeque<BeeLogLine>,
136 pub warning: VecDeque<BeeLogLine>,
137 pub info: VecDeque<BeeLogLine>,
138 pub debug: VecDeque<BeeLogLine>,
139 pub bee_http: VecDeque<BeeLogLine>,
140}
141
142impl BeeLogBuffers {
143 fn buffer_for(&self, tab: LogTab) -> Option<&VecDeque<BeeLogLine>> {
144 match tab {
145 LogTab::Errors => Some(&self.errors),
146 LogTab::Warning => Some(&self.warning),
147 LogTab::Info => Some(&self.info),
148 LogTab::Debug => Some(&self.debug),
149 LogTab::BeeHttp => Some(&self.bee_http),
150 LogTab::SelfHttp => None,
151 }
152 }
153
154 pub fn count(&self, tab: LogTab) -> usize {
157 self.buffer_for(tab).map(|b| b.len()).unwrap_or(0)
158 }
159}
160
161pub struct LogPane {
165 capture: Option<LogCapture>,
166 self_http_entries: Vec<LogEntry>,
167 bee_buffers: BeeLogBuffers,
168 active_tab: LogTab,
169 height: u16,
171 spawn_active: bool,
175 scroll_offset: usize,
180 h_scroll_offset: u16,
184}
185
186impl LogPane {
187 pub fn new(capture: Option<LogCapture>, initial_tab: LogTab, initial_height: u16) -> Self {
188 Self {
189 capture,
190 self_http_entries: Vec::new(),
191 bee_buffers: BeeLogBuffers::default(),
192 active_tab: initial_tab,
193 height: initial_height.clamp(LOG_PANE_MIN_HEIGHT, LOG_PANE_MAX_HEIGHT),
194 spawn_active: false,
195 scroll_offset: 0,
196 h_scroll_offset: 0,
197 }
198 }
199
200 pub fn active_tab(&self) -> LogTab {
201 self.active_tab
202 }
203
204 pub fn height(&self) -> u16 {
205 self.height
206 }
207
208 pub fn set_spawn_active(&mut self, active: bool) {
212 self.spawn_active = active;
213 }
214
215 pub fn next_tab(&mut self) -> LogTab {
220 let i = (self.active_tab.index() + 1) % LogTab::ALL.len();
221 self.active_tab = LogTab::from_index(i);
222 self.scroll_offset = 0;
223 self.h_scroll_offset = 0;
224 self.active_tab
225 }
226
227 pub fn prev_tab(&mut self) -> LogTab {
229 let len = LogTab::ALL.len();
230 let i = (self.active_tab.index() + len - 1) % len;
231 self.active_tab = LogTab::from_index(i);
232 self.scroll_offset = 0;
233 self.h_scroll_offset = 0;
234 self.active_tab
235 }
236
237 pub fn scroll_up(&mut self, lines: usize) {
242 self.scroll_offset = self.scroll_offset.saturating_add(lines);
243 }
244
245 pub fn scroll_down(&mut self, lines: usize) {
248 self.scroll_offset = self.scroll_offset.saturating_sub(lines);
249 }
250
251 pub fn resume_tail(&mut self) {
256 self.scroll_offset = 0;
257 self.h_scroll_offset = 0;
258 }
259
260 pub fn scroll_right(&mut self, cols: u16) {
265 self.h_scroll_offset = self.h_scroll_offset.saturating_add(cols);
266 }
267
268 pub fn scroll_left(&mut self, cols: u16) {
271 self.h_scroll_offset = self.h_scroll_offset.saturating_sub(cols);
272 }
273
274 pub fn reset_h_scroll(&mut self) {
278 self.h_scroll_offset = 0;
279 }
280
281 pub fn h_scroll_offset(&self) -> u16 {
284 self.h_scroll_offset
285 }
286
287 pub fn is_tailing(&self) -> bool {
289 self.scroll_offset == 0
290 }
291
292 pub fn scroll_offset(&self) -> usize {
295 self.scroll_offset
296 }
297
298 pub fn grow(&mut self) -> u16 {
301 self.height = (self.height + 1).min(LOG_PANE_MAX_HEIGHT);
302 self.height
303 }
304
305 pub fn shrink(&mut self) -> u16 {
308 self.height = self.height.saturating_sub(1).max(LOG_PANE_MIN_HEIGHT);
309 self.height
310 }
311
312 pub fn push_bee(&mut self, tab: LogTab, line: BeeLogLine) {
322 let buf = match tab {
323 LogTab::Errors => &mut self.bee_buffers.errors,
324 LogTab::Warning => &mut self.bee_buffers.warning,
325 LogTab::Info => &mut self.bee_buffers.info,
326 LogTab::Debug => &mut self.bee_buffers.debug,
327 LogTab::BeeHttp => &mut self.bee_buffers.bee_http,
328 LogTab::SelfHttp => return, };
330 let was_full = buf.len() == BEE_TAB_RING_CAPACITY;
331 if was_full {
332 buf.pop_front();
333 }
334 buf.push_back(line);
335 if tab == self.active_tab && self.scroll_offset > 0 && !was_full {
340 self.scroll_offset = self.scroll_offset.saturating_add(1);
341 }
342 }
343
344 fn pull_self_http(&mut self) {
345 if let Some(c) = &self.capture {
346 let new = c.snapshot();
347 if self.active_tab == LogTab::SelfHttp && self.scroll_offset > 0 {
352 let delta = new.len().saturating_sub(self.self_http_entries.len());
353 if delta > 0 {
354 self.scroll_offset = self.scroll_offset.saturating_add(delta);
355 }
356 }
357 self.self_http_entries = new;
358 }
359 }
360}
361
362impl Component for LogPane {
363 fn update(&mut self, action: Action) -> Result<Option<Action>> {
364 if matches!(action, Action::Tick) {
365 self.pull_self_http();
366 }
367 Ok(None)
368 }
369
370 fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
371 let t = theme::active();
372 let active = self.active_tab;
373
374 let content_h = (area.height as usize).saturating_sub(2);
378 let total_lines = self.active_tab_total_lines();
379 let max_offset = total_lines.saturating_sub(content_h);
380 if self.scroll_offset > max_offset {
381 self.scroll_offset = max_offset;
382 }
383
384 let block = Block::default().borders(Borders::ALL).title(tab_title_line(
385 active,
386 &self.bee_buffers,
387 self.scroll_offset,
388 self.h_scroll_offset,
389 t,
390 ));
391 let inner = block.inner(area);
392 frame.render_widget(block, area);
393
394 let content_area = Layout::vertical([Constraint::Min(0)])
395 .split(inner)
396 .first()
397 .copied()
398 .unwrap_or(inner);
399
400 let lines: Vec<Line> = match active {
401 LogTab::SelfHttp => render_self_http(&self.self_http_entries, t),
402 tab => render_bee_tab(&self.bee_buffers, tab, self.spawn_active, t),
403 };
404
405 let render_h = content_area.height as usize;
410 let visible: Vec<Line> = if lines.len() > render_h {
411 let end = lines.len().saturating_sub(self.scroll_offset);
412 let start = end.saturating_sub(render_h);
413 lines.into_iter().skip(start).take(end - start).collect()
414 } else {
415 lines
416 };
417
418 frame.render_widget(
425 Paragraph::new(visible).scroll((0, self.h_scroll_offset)),
426 content_area,
427 );
428 Ok(())
429 }
430}
431
432impl LogPane {
433 pub fn active_tab_total_lines(&self) -> usize {
436 match self.active_tab {
437 LogTab::SelfHttp => self.self_http_entries.len(),
438 tab => self.bee_buffers.count(tab),
439 }
440 }
441}
442
443fn tab_title_line<'a>(
448 active: LogTab,
449 bufs: &BeeLogBuffers,
450 scroll_offset: usize,
451 h_scroll_offset: u16,
452 t: &theme::Theme,
453) -> Line<'a> {
454 let mut spans: Vec<Span> = Vec::with_capacity(LogTab::ALL.len() * 2 + 2);
455 spans.push(Span::raw(" "));
456 for tab in LogTab::ALL {
457 let count = bufs.count(tab);
458 let label = if count == 0 || tab == LogTab::SelfHttp {
459 format!(" {} ", tab.label())
460 } else {
461 format!(" {} {} ", tab.label(), human_count(count))
462 };
463 let style = if tab == active {
464 Style::default()
465 .fg(t.tab_active_fg)
466 .bg(t.tab_active_bg)
467 .add_modifier(Modifier::BOLD)
468 } else {
469 tab_severity_color(tab, t)
470 };
471 spans.push(Span::styled(label, style));
472 spans.push(Span::raw(" "));
473 }
474 if scroll_offset > 0 {
478 spans.push(Span::styled(
479 format!(" paused {scroll_offset} ↑ "),
480 Style::default().fg(t.warn).add_modifier(Modifier::BOLD),
481 ));
482 }
483 if h_scroll_offset > 0 {
484 spans.push(Span::styled(
488 format!(" → {h_scroll_offset} "),
489 Style::default().fg(t.warn).add_modifier(Modifier::BOLD),
490 ));
491 }
492 Line::from(spans)
493}
494
495fn tab_severity_color(tab: LogTab, t: &theme::Theme) -> Style {
498 match tab {
499 LogTab::Errors => Style::default().fg(t.fail),
500 LogTab::Warning => Style::default().fg(t.warn),
501 LogTab::Info => Style::default().fg(t.info),
502 LogTab::Debug => Style::default().fg(t.dim),
503 LogTab::BeeHttp => Style::default().fg(t.accent),
504 LogTab::SelfHttp => Style::default().fg(t.dim),
505 }
506}
507
508fn human_count(n: usize) -> String {
509 if n < 1_000 {
510 n.to_string()
511 } else if n < 1_000_000 {
512 format!("{:.1}k", n as f64 / 1000.0)
513 } else {
514 format!("{:.1}m", n as f64 / 1_000_000.0)
515 }
516}
517
518fn render_self_http<'a>(entries: &'a [LogEntry], t: &theme::Theme) -> Vec<Line<'a>> {
519 if entries.is_empty() {
520 return vec![Line::from(Span::styled(
521 " (waiting for first request…)",
522 Style::default().fg(t.dim).add_modifier(Modifier::ITALIC),
523 ))];
524 }
525 entries.iter().map(|e| self_http_line(e, t)).collect()
526}
527
528fn render_bee_tab<'a>(
529 bufs: &'a BeeLogBuffers,
530 tab: LogTab,
531 spawn_active: bool,
532 t: &theme::Theme,
533) -> Vec<Line<'a>> {
534 let buf = match bufs.buffer_for(tab) {
535 Some(b) => b,
536 None => return Vec::new(),
537 };
538 if buf.is_empty() {
539 let msg = if spawn_active {
540 " (awaiting bee log entries on this severity…)"
541 } else {
542 " (no bee child — set [bee] in config or pass --bee-bin / --bee-config)"
543 };
544 return vec![Line::from(Span::styled(
545 msg,
546 Style::default().fg(t.dim).add_modifier(Modifier::ITALIC),
547 ))];
548 }
549 buf.iter()
550 .map(|line| {
551 Line::from(vec![
552 Span::styled(format!("{} ", line.timestamp), Style::default().fg(t.dim)),
553 Span::styled(
554 format!("{:<22}", line.logger),
555 Style::default().fg(t.accent).add_modifier(Modifier::BOLD),
556 ),
557 Span::raw(" "),
558 Span::raw(line.message.clone()),
559 ])
560 })
561 .collect()
562}
563
564fn self_http_line<'a>(e: &'a LogEntry, t: &theme::Theme) -> Line<'a> {
565 let status_style = match e.status {
566 Some(s) if (200..300).contains(&s) => Style::default().fg(t.pass),
567 Some(s) if (300..400).contains(&s) => Style::default().fg(t.info),
568 Some(s) if (400..500).contains(&s) => Style::default().fg(t.warn),
569 Some(_) => Style::default().fg(t.fail),
570 None => Style::default().fg(t.dim),
571 };
572 let method_style = Style::default()
573 .fg(method_color(&e.method))
574 .add_modifier(Modifier::BOLD);
575 let elapsed = e
576 .elapsed_ms
577 .map(|ms| format!("{ms:>4}ms"))
578 .unwrap_or_else(|| " —".into());
579 let path = path_only(&e.url);
580 Line::from(vec![
581 Span::styled(format!("{} ", e.ts), Style::default().fg(t.dim)),
582 Span::styled(format!("{:<5}", e.method), method_style),
583 Span::raw(" "),
584 Span::raw(path),
585 Span::raw(" "),
586 Span::styled(
587 e.status
588 .map(|s| s.to_string())
589 .unwrap_or_else(|| "—".into()),
590 status_style,
591 ),
592 Span::raw(" "),
593 Span::styled(elapsed, Style::default().fg(t.dim)),
594 ])
595}
596
597fn method_color(method: &str) -> Color {
601 match method {
602 "GET" => Color::Blue,
603 "POST" => Color::Green,
604 "PUT" => Color::Yellow,
605 "DELETE" => Color::Red,
606 "PATCH" => Color::Magenta,
607 "HEAD" => Color::Cyan,
608 _ => Color::White,
609 }
610}
611
612fn path_only(url: &str) -> String {
616 if let Some(rest) = url.split_once("//").and_then(|(_, r)| r.split_once('/')) {
617 format!("/{}", rest.1)
618 } else {
619 url.to_string()
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 #[test]
628 fn next_tab_wraps() {
629 let mut pane = LogPane::new(None, LogTab::Errors, 10);
630 for expected in [
631 LogTab::Warning,
632 LogTab::Info,
633 LogTab::Debug,
634 LogTab::BeeHttp,
635 LogTab::SelfHttp,
636 LogTab::Errors,
637 ] {
638 assert_eq!(pane.next_tab(), expected);
639 }
640 }
641
642 #[test]
643 fn prev_tab_wraps() {
644 let mut pane = LogPane::new(None, LogTab::Errors, 10);
645 for expected in [
646 LogTab::SelfHttp,
647 LogTab::BeeHttp,
648 LogTab::Debug,
649 LogTab::Info,
650 LogTab::Warning,
651 LogTab::Errors,
652 ] {
653 assert_eq!(pane.prev_tab(), expected);
654 }
655 }
656
657 #[test]
658 fn grow_clamps_at_max() {
659 let mut pane = LogPane::new(None, LogTab::SelfHttp, LOG_PANE_MAX_HEIGHT - 1);
660 pane.grow();
661 assert_eq!(pane.height(), LOG_PANE_MAX_HEIGHT);
662 pane.grow();
664 pane.grow();
665 assert_eq!(pane.height(), LOG_PANE_MAX_HEIGHT);
666 }
667
668 #[test]
669 fn shrink_clamps_at_min() {
670 let mut pane = LogPane::new(None, LogTab::SelfHttp, LOG_PANE_MIN_HEIGHT + 1);
671 pane.shrink();
672 assert_eq!(pane.height(), LOG_PANE_MIN_HEIGHT);
673 pane.shrink();
674 pane.shrink();
675 assert_eq!(pane.height(), LOG_PANE_MIN_HEIGHT);
676 }
677
678 #[test]
679 fn fresh_pane_is_tailing() {
680 let pane = LogPane::new(None, LogTab::Errors, 10);
681 assert!(pane.is_tailing());
682 assert_eq!(pane.scroll_offset(), 0);
683 }
684
685 #[test]
686 fn scroll_up_disables_tail_and_remembers_offset() {
687 let mut pane = LogPane::new(None, LogTab::Errors, 10);
688 pane.scroll_up(3);
689 assert!(!pane.is_tailing());
690 assert_eq!(pane.scroll_offset(), 3);
691 pane.scroll_up(2);
692 assert_eq!(pane.scroll_offset(), 5);
693 }
694
695 #[test]
696 fn scroll_down_eventually_resumes_tail() {
697 let mut pane = LogPane::new(None, LogTab::Errors, 10);
698 pane.scroll_up(5);
699 pane.scroll_down(2);
700 assert_eq!(pane.scroll_offset(), 3);
701 pane.scroll_down(100);
703 assert_eq!(pane.scroll_offset(), 0);
704 assert!(pane.is_tailing());
705 }
706
707 #[test]
708 fn resume_tail_resets_offset() {
709 let mut pane = LogPane::new(None, LogTab::Errors, 10);
710 pane.scroll_up(7);
711 pane.resume_tail();
712 assert!(pane.is_tailing());
713 }
714
715 #[test]
716 fn tab_switch_resets_scroll_offset() {
717 let mut pane = LogPane::new(None, LogTab::Errors, 10);
720 pane.scroll_up(4);
721 pane.next_tab();
722 assert_eq!(pane.scroll_offset(), 0);
723 assert!(pane.is_tailing());
724 pane.scroll_up(4);
726 pane.prev_tab();
727 assert_eq!(pane.scroll_offset(), 0);
728 }
729
730 #[test]
731 fn push_bee_bumps_offset_for_active_tab_when_scrolled() {
732 let mut pane = LogPane::new(None, LogTab::Errors, 10);
736 pane.push_bee(LogTab::Errors, line("err1"));
737 pane.push_bee(LogTab::Errors, line("err2"));
738 pane.scroll_up(2);
739 pane.push_bee(LogTab::Errors, line("err3"));
740 assert_eq!(pane.scroll_offset(), 3);
743 }
744
745 #[test]
746 fn push_bee_doesnt_bump_offset_when_tailing() {
747 let mut pane = LogPane::new(None, LogTab::Errors, 10);
750 for i in 0..5 {
751 pane.push_bee(LogTab::Errors, line(&format!("e{i}")));
752 }
753 assert_eq!(pane.scroll_offset(), 0);
754 assert!(pane.is_tailing());
755 }
756
757 #[test]
758 fn push_bee_doesnt_bump_offset_for_inactive_tab() {
759 let mut pane = LogPane::new(None, LogTab::Errors, 10);
762 pane.push_bee(LogTab::Errors, line("err1"));
763 pane.scroll_up(1);
764 let before = pane.scroll_offset();
765 pane.push_bee(LogTab::Debug, line("dbg1"));
766 assert_eq!(pane.scroll_offset(), before);
767 }
768
769 fn line(msg: &str) -> BeeLogLine {
770 BeeLogLine {
771 timestamp: "t".into(),
772 logger: "node/test".into(),
773 message: msg.into(),
774 }
775 }
776
777 #[test]
778 fn ring_capacity_is_enforced() {
779 let mut pane = LogPane::new(None, LogTab::Debug, 10);
780 for i in 0..(BEE_TAB_RING_CAPACITY + 100) {
782 pane.push_bee(
783 LogTab::Debug,
784 BeeLogLine {
785 timestamp: format!("t{i}"),
786 logger: "node/test".into(),
787 message: format!("msg {i}"),
788 },
789 );
790 }
791 assert_eq!(pane.bee_buffers.debug.len(), BEE_TAB_RING_CAPACITY);
792 assert_eq!(pane.bee_buffers.debug.front().unwrap().timestamp, "t100");
793 assert_eq!(
794 pane.bee_buffers.debug.back().unwrap().timestamp,
795 format!("t{}", BEE_TAB_RING_CAPACITY + 99)
796 );
797 }
798
799 #[test]
800 fn push_bee_to_self_http_is_noop() {
801 let mut pane = LogPane::new(None, LogTab::SelfHttp, 10);
805 pane.push_bee(
806 LogTab::SelfHttp,
807 BeeLogLine {
808 timestamp: "t".into(),
809 logger: "x".into(),
810 message: "m".into(),
811 },
812 );
813 for tab in LogTab::ALL {
814 assert_eq!(pane.bee_buffers.count(tab), 0, "tab {tab:?} got an entry");
815 }
816 }
817
818 #[test]
819 fn human_count_formats_thousands() {
820 assert_eq!(human_count(0), "0");
821 assert_eq!(human_count(42), "42");
822 assert_eq!(human_count(999), "999");
823 assert_eq!(human_count(1000), "1.0k");
824 assert_eq!(human_count(1234), "1.2k");
825 assert_eq!(human_count(999_999), "1000.0k");
826 assert_eq!(human_count(1_000_000), "1.0m");
827 }
828
829 #[test]
830 fn from_kebab_unknown_falls_back_to_self_http() {
831 assert_eq!(LogTab::from_kebab("future-tab"), LogTab::SelfHttp);
834 assert_eq!(LogTab::from_kebab(""), LogTab::SelfHttp);
835 }
836
837 #[test]
838 fn kebab_round_trips() {
839 for tab in LogTab::ALL {
840 assert_eq!(LogTab::from_kebab(tab.to_kebab()), tab);
841 }
842 }
843
844 #[test]
845 fn path_only_strips_scheme_and_host() {
846 assert_eq!(path_only("http://localhost:1633/status"), "/status");
847 assert_eq!(
848 path_only("https://bee.example.com:1633/stamps/abc"),
849 "/stamps/abc"
850 );
851 }
852
853 #[test]
854 fn path_only_handles_root_only() {
855 assert_eq!(path_only("http://localhost:1633"), "http://localhost:1633");
856 }
857
858 #[test]
859 fn h_scroll_starts_at_zero() {
860 let pane = LogPane::new(None, LogTab::SelfHttp, LOG_PANE_MIN_HEIGHT);
861 assert_eq!(pane.h_scroll_offset(), 0);
862 }
863
864 #[test]
865 fn scroll_right_then_left_returns_to_zero() {
866 let mut pane = LogPane::new(None, LogTab::SelfHttp, LOG_PANE_MIN_HEIGHT);
867 pane.scroll_right(8);
868 pane.scroll_right(8);
869 assert_eq!(pane.h_scroll_offset(), 16);
870 pane.scroll_left(16);
871 assert_eq!(pane.h_scroll_offset(), 0);
872 }
873
874 #[test]
875 fn scroll_left_saturates_at_zero() {
876 let mut pane = LogPane::new(None, LogTab::SelfHttp, LOG_PANE_MIN_HEIGHT);
877 pane.scroll_left(100);
878 assert_eq!(pane.h_scroll_offset(), 0);
879 }
880
881 #[test]
882 fn switching_tabs_resets_h_scroll() {
883 let mut pane = LogPane::new(None, LogTab::Errors, LOG_PANE_MIN_HEIGHT);
884 pane.scroll_right(40);
885 assert_eq!(pane.h_scroll_offset(), 40);
886 pane.next_tab();
887 assert_eq!(pane.h_scroll_offset(), 0);
888 pane.scroll_right(20);
889 pane.prev_tab();
890 assert_eq!(pane.h_scroll_offset(), 0);
891 }
892
893 #[test]
894 fn resume_tail_resets_both_axes() {
895 let mut pane = LogPane::new(None, LogTab::SelfHttp, LOG_PANE_MIN_HEIGHT);
896 pane.scroll_up(5);
897 pane.scroll_right(24);
898 pane.resume_tail();
899 assert_eq!(pane.scroll_offset(), 0);
900 assert_eq!(pane.h_scroll_offset(), 0);
901 }
902
903 #[test]
904 fn reset_h_scroll_only_touches_horizontal() {
905 let mut pane = LogPane::new(None, LogTab::SelfHttp, LOG_PANE_MIN_HEIGHT);
906 pane.scroll_up(7);
907 pane.scroll_right(16);
908 pane.reset_h_scroll();
909 assert_eq!(pane.scroll_offset(), 7);
910 assert_eq!(pane.h_scroll_offset(), 0);
911 }
912}