use eframe::egui; use crate::file_tab::FileTab; use crate::types::HighlightRule; pub struct LogViewContext<'a> { pub tab: &'a mut FileTab, pub highlight_rules: &'a [HighlightRule], pub show_all: bool, } pub fn render_log_view(ui: &mut egui::Ui, ctx: LogViewContext) { let LogViewContext { tab, highlight_rules, show_all, } = ctx; let total_lines = if show_all { tab.line_index.total_lines } else { tab.filtered_lines.len() }; if total_lines == 0 { ui.centered_and_justified(|ui| { if show_all { ui.label("Click 'Open File' to load a log file"); } else { ui.label("No matching lines"); } }); return; } let line_height = ui.text_style_height(&egui::TextStyle::Monospace); // Account for: frame inner margins (2.0 vertical) + horizontal layout spacing + frame outer spacing // The actual rendered height is approximately line_height + 6.0 let row_height = line_height + 2.0; let scroll_id_str = if show_all { "main_view_scroll" } else { "filtered_view_scroll" }; let scroll_id = egui::Id::new(scroll_id_str); // Only handle scroll-to-line and page-scroll for the main view if show_all { handle_scroll_to_line(ui, &scroll_id, tab, row_height); handle_page_scroll(ui, &scroll_id, tab, row_height, total_lines); } let mut scroll_area = egui::ScrollArea::both() .auto_shrink([false; 2]) .id_salt(scroll_id); if show_all && tab.force_scroll { scroll_area = scroll_area.vertical_scroll_offset(tab.desired_scroll_offset); tab.force_scroll = false; } let scroll_output = scroll_area.show_rows(ui, row_height, total_lines, |ui, row_range| { render_visible_lines(ui, tab, highlight_rules, show_all, row_range); }); // Update the scroll offset from the actual scroll state if show_all { let actual_offset = scroll_output.state.offset.y; // eprintln!("Actual scroll offset after render: {}", actual_offset); if (actual_offset - tab.desired_scroll_offset).abs() > 1.0 { eprintln!( "SYNC: Updating desired_scroll_offset from {} to {}", tab.desired_scroll_offset, actual_offset ); } tab.desired_scroll_offset = actual_offset; } } fn handle_scroll_to_line( _ui: &mut egui::Ui, _scroll_id: &egui::Id, tab: &mut FileTab, row_height: f32, ) { if tab.scroll_to_main { let target_row = tab.main_scroll_offset; let adgusted_row_height = row_height + 3f32; let scroll_offset = (target_row as f32) * adgusted_row_height; eprintln!("=== SCROLL TO LINE ==="); eprintln!(" main_scroll_offset (selected line): {}", tab.main_scroll_offset); eprintln!(" target_row (with -5 context): {}", target_row); eprintln!(" row_height: {}", row_height); eprintln!(" calculated scroll_offset: {}", scroll_offset); eprintln!(" force_scroll: true"); tab.desired_scroll_offset = scroll_offset; tab.force_scroll = true; tab.scroll_to_main = false; } } fn handle_page_scroll( ui: &mut egui::Ui, _scroll_id: &egui::Id, tab: &mut FileTab, row_height: f32, total_lines: usize, ) { if let Some(direction) = tab.page_scroll_direction.take() { let row_height_offset = row_height + 3f32; let viewport_height = ui.available_height(); let rows_per_page = (viewport_height / row_height_offset).floor().max(1.0); let scroll_delta = direction * rows_per_page * row_height_offset; let max_offset = (total_lines as f32 * row_height_offset - viewport_height).max(0.0); let new_offset = (tab.desired_scroll_offset + scroll_delta).clamp(0.0, max_offset); eprintln!( "Page scroll: current_offset={}, scroll_delta={}, new_offset={}", tab.desired_scroll_offset, scroll_delta, new_offset ); tab.desired_scroll_offset = new_offset; tab.force_scroll = true; } } fn render_visible_lines( ui: &mut egui::Ui, tab: &mut FileTab, highlight_rules: &[HighlightRule], show_all: bool, row_range: std::ops::Range, ) { ui.style_mut().spacing.item_spacing.y = 0.0; for display_idx in row_range { let (line_num, content) = get_line_content(tab, show_all, display_idx); let is_selected = tab.selected_line == Some(line_num); render_line( ui, &content, line_num, is_selected, highlight_rules, |clicked| { if clicked { tab.selected_line = Some(line_num); if !show_all { tab.main_scroll_offset = line_num; tab.scroll_to_main = true; } } }, ); } } fn get_line_content(tab: &mut FileTab, show_all: bool, display_idx: usize) -> (usize, String) { if show_all { // Main view: read line by display index let content = tab .line_index .read_line(&mut tab.file_handle, display_idx) .unwrap_or_default(); (display_idx, content) } else { // Filtered view: get line number from filtered list, then read content on-demand if display_idx < tab.filtered_lines.len() { let line_number = tab.filtered_lines[display_idx].line_number; let content = tab .line_index .read_line(&mut tab.file_handle, line_number) .unwrap_or_default(); (line_number, content) } else { (0, String::new()) } } } fn render_line( ui: &mut egui::Ui, content: &str, line_num: usize, is_selected: bool, highlight_rules: &[HighlightRule], on_click: F, ) where F: FnOnce(bool), { let highlight_color = highlight_rules .iter() .find(|rule| rule.enabled && content.contains(&rule.pattern)) .map(|rule| egui::Color32::from_rgb(rule.color[0], rule.color[1], rule.color[2])); let bg_color = if is_selected { egui::Color32::from_rgb(70, 130, 180) } else if let Some(color) = highlight_color { color } else { egui::Color32::TRANSPARENT }; let frame = egui::Frame::none() .fill(bg_color) .inner_margin(egui::Margin::symmetric(2.0, 1.0)); frame.show(ui, |ui| { ui.horizontal(|ui| { let line_num_text = egui::RichText::new(format!("{:6} ", line_num + 1)) .monospace() .color(if is_selected { egui::Color32::WHITE } else { egui::Color32::DARK_GRAY }); let line_num_response = ui.label(line_num_text); let text = egui::RichText::new(content).monospace().color( if is_selected { egui::Color32::WHITE } else { ui.style().visuals.text_color() }, ); let text_response = ui .scope(|ui| { ui.style_mut().visuals.selection.bg_fill = egui::Color32::from_rgb(255, 180, 50); ui.style_mut().visuals.selection.stroke.color = egui::Color32::from_rgb(200, 140, 30); ui.add(egui::Label::new(text).selectable(true)) }) .inner; let clicked = line_num_response.clicked() || (text_response.clicked() && !text_response.has_focus()); on_click(clicked); }) }); }