249 lines
7.7 KiB
Rust
249 lines
7.7 KiB
Rust
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<usize>,
|
|
) {
|
|
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<F>(
|
|
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);
|
|
})
|
|
});
|
|
}
|
|
|