init, basic glogg clone

This commit is contained in:
2025-12-02 18:30:12 +01:00
commit a53ced9160
26 changed files with 6512 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
use eframe::egui;
use crate::highlight::HighlightManager;
pub fn render_highlight_editor(ctx: &egui::Context, manager: &mut HighlightManager) -> bool {
if !manager.show_editor {
return false;
}
let mut config_changed = false;
egui::Window::new("Highlight Manager")
.collapsible(false)
.show(ctx, |ui| {
ui.heading("Add New Highlight");
ui.horizontal(|ui| {
ui.label("Pattern:");
ui.text_edit_singleline(&mut manager.new_pattern);
});
ui.horizontal(|ui| {
ui.label("Color:");
ui.color_edit_button_srgb(&mut manager.new_color);
});
if ui.button("Add Highlight").clicked() && manager.add_highlight() {
config_changed = true;
}
ui.separator();
ui.heading("Existing Highlights");
let mut to_delete = None;
for (idx, rule) in manager.rules.iter_mut().enumerate() {
ui.horizontal(|ui| {
if ui.checkbox(&mut rule.enabled, "").changed() {
config_changed = true;
}
ui.label(&rule.pattern);
if ui.color_edit_button_srgb(&mut rule.color).changed() {
config_changed = true;
}
if ui.button("🗑").clicked() {
to_delete = Some(idx);
}
});
}
if let Some(idx) = to_delete {
manager.remove_highlight(idx);
config_changed = true;
}
});
config_changed
}

241
src/ui/log_view.rs Normal file
View File

@@ -0,0 +1,241 @@
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 viewport_height = ui.available_height();
let rows_per_page = (viewport_height / row_height).floor().max(1.0);
let scroll_delta = direction * rows_per_page * row_height;
let max_offset = (total_lines as f32 * row_height - 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 {
let content = tab
.line_index
.read_line(&mut tab.file_handle, display_idx)
.unwrap_or_default();
(display_idx, content)
} else {
if display_idx < tab.filtered_lines.len() {
let filtered = &tab.filtered_lines[display_idx];
(filtered.line_number, filtered.content.clone())
} 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);
})
});
}

11
src/ui/mod.rs Normal file
View File

@@ -0,0 +1,11 @@
pub mod highlight_editor;
pub mod log_view;
pub mod search_panel;
pub mod tabs_panel;
pub mod top_menu;
pub use highlight_editor::render_highlight_editor;
pub use log_view::{render_log_view, LogViewContext};
pub use search_panel::{render_search_panel, SearchPanelState};
pub use tabs_panel::render_tabs_panel;
pub use top_menu::render_top_menu;

99
src/ui/search_panel.rs Normal file
View File

@@ -0,0 +1,99 @@
use eframe::egui;
use crate::search::SearchState;
pub struct SearchPanelState {
pub query: String,
pub case_sensitive: bool,
pub use_regex: bool,
pub history: Vec<String>,
}
pub struct SearchPanelActions {
pub execute_search: bool,
pub clear_search: bool,
pub config_changed: bool,
}
pub fn render_search_panel(
ctx: &egui::Context,
state: &mut SearchPanelState,
search_state: &SearchState,
match_count: usize,
) -> SearchPanelActions {
let mut actions = SearchPanelActions {
execute_search: false,
clear_search: false,
config_changed: false,
};
egui::TopBottomPanel::bottom("search_panel").show(ctx, |ui| {
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label("🔍 Filter:");
let text_edit_width = 200.0;
let text_response = ui.add_sized(
[text_edit_width, 20.0],
egui::TextEdit::singleline(&mut state.query),
);
let enter_pressed =
text_response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
if !state.history.is_empty() {
render_history_dropdown(ui, state);
}
let case_changed = ui
.checkbox(&mut state.case_sensitive, "Case sensitive")
.changed();
let regex_changed = ui.checkbox(&mut state.use_regex, "Regex").changed();
if case_changed || regex_changed {
actions.config_changed = true;
}
if ui.button("Search").clicked() || enter_pressed {
actions.execute_search = true;
actions.config_changed = true;
}
if !state.query.is_empty() {
if ui.button("✖ Clear").clicked() {
actions.clear_search = true;
}
ui.label(format!("({} matches)", match_count));
}
});
if search_state.is_searching() {
let progress = search_state.get_progress();
ui.horizontal(|ui| {
ui.add(
egui::ProgressBar::new(progress)
.text(format!("Searching... {:.0}%", progress * 100.0))
.animate(true),
);
});
}
});
});
actions
}
fn render_history_dropdown(ui: &mut egui::Ui, state: &mut SearchPanelState) {
egui::ComboBox::from_id_salt("search_history_dropdown")
.selected_text("")
.width(30.0)
.show_ui(ui, |ui| {
ui.set_min_width(300.0);
for history_item in &state.history.clone() {
if ui.selectable_label(false, history_item).clicked() {
state.query = history_item.clone();
}
}
});
}

41
src/ui/tabs_panel.rs Normal file
View File

@@ -0,0 +1,41 @@
use eframe::egui;
use crate::file_tab::FileTab;
pub fn render_tabs_panel(
ctx: &egui::Context,
tabs: &[FileTab],
active_tab_index: &mut usize,
on_close_tab: &mut Option<usize>,
) {
if tabs.is_empty() {
return;
}
egui::TopBottomPanel::top("tabs_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
for (idx, tab) in tabs.iter().enumerate() {
let is_active = idx == *active_tab_index;
let button_text = if is_active {
egui::RichText::new(format!("📄 {}", tab.filename())).strong()
} else {
egui::RichText::new(format!("📄 {}", tab.filename()))
};
if ui.selectable_label(is_active, button_text).clicked() {
*active_tab_index = idx;
}
if ui.small_button("").clicked() {
*on_close_tab = Some(idx);
}
ui.separator();
}
if let Some(tab) = tabs.get(*active_tab_index) {
ui.label(format!("({} lines)", tab.line_index.total_lines));
}
});
});
}

49
src/ui/top_menu.rs Normal file
View File

@@ -0,0 +1,49 @@
use eframe::egui;
use crate::highlight::HighlightManager;
use crate::tab_manager::IndexingState;
pub fn render_top_menu(
ctx: &egui::Context,
highlight_manager: &mut HighlightManager,
indexing_state: &IndexingState,
on_open_file: &mut bool,
) {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
if ui.button("📂 Open File").clicked() {
*on_open_file = true;
}
if ui.button("🎨 Highlights").clicked() {
highlight_manager.toggle_editor();
}
ui.separator();
let mut to_toggle = None;
for (idx, rule) in highlight_manager.rules.iter().enumerate() {
let color = egui::Color32::from_rgb(rule.color[0], rule.color[1], rule.color[2]);
let button_text = egui::RichText::new(&rule.pattern)
.background_color(color)
.color(egui::Color32::BLACK);
if ui.selectable_label(rule.enabled, button_text).clicked() {
to_toggle = Some(idx);
}
}
if let Some(idx) = to_toggle {
highlight_manager.toggle_rule(idx);
// Signal that config should be saved
}
ui.separator();
if indexing_state.is_indexing() {
ui.spinner();
ui.label("Indexing...");
}
});
});
}