use log_viewer_core::types::LogLevel; use unicode_width::UnicodeWidthChar; pub(super) struct ViewportEntry { pub(super) wrapped_rows: Vec, pub(super) level: Option, pub(super) visual_height: usize, } pub(super) struct ViewportCache { pub(super) entries: Vec, pub(super) logical_start: usize, pub(super) width: usize, json_format: bool, pub(super) cached_total_visual_rows: Option, } impl ViewportCache { pub(super) fn new() -> Self { Self { entries: Vec::new(), logical_start: 0, width: 0, json_format: false, cached_total_visual_rows: None, } } pub(super) fn invalidate(&mut self) { self.entries.clear(); self.logical_start = 0; self.width = 0; self.cached_total_visual_rows = None; } pub(super) fn needs_recompute(&self, width: usize, json_format: bool) -> bool { self.width != width || self.json_format != json_format } pub(super) fn set_json_format(&mut self, json_format: bool) { self.json_format = json_format; } #[allow(dead_code)] pub(super) fn get_entry(&self, logical_line: usize) -> Option<&ViewportEntry> { if logical_line >= self.logical_start { let idx = logical_line - self.logical_start; self.entries.get(idx) } else { None } } } pub(super) const TRUNCATE_TAB_WIDTH: usize = 4; pub(super) fn gutter_width_for(total_lines: usize, is_loading: bool) -> usize { if total_lines == 0 { return 0; } let line_num_width = total_lines.to_string().len(); let loading_extra = if is_loading { 1 } else { 0 }; line_num_width + loading_extra + 1 + 1 } pub(super) fn truncate_to_columns(s: &str, max_cols: usize) -> String { if max_cols == 0 || s.is_empty() { return String::new(); } let mut out = String::new(); let mut col = 0; for ch in s.chars() { if ch == '\t' { let tab_stop = TRUNCATE_TAB_WIDTH - (col % TRUNCATE_TAB_WIDTH); if col + tab_stop > max_cols { break; } for _ in 0..tab_stop { out.push(' '); } col += tab_stop; } else { let w = if ch.is_control() { 0 } else { ch.width().unwrap_or(0) }; if col + w > max_cols { break; } out.push(ch); col += w; } } out }