Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions i18n/ar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "تتبع المؤشر"
cursor_tracking_desc: "إبقاء المؤشر في المنتصف أثناء التمرير"
window: "النافذة:"
scroll_to_zoom: "التمرير للتكبير"
scroll_to_zoom_desc: "عجلة الماوس تكبر المخطط مباشرة بدلاً من الإزاحة"
field_names: "أسماء الحقول"
field_normalization: "توحيد الحقول"
field_normalization_desc: "توحيد أسماء القنوات عبر أنواع ECU"
Expand Down
2 changes: 2 additions & 0 deletions i18n/bn.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "কার্সর ট্র্যাকিং"
cursor_tracking_desc: "স্ক্রাবিংয়ের সময় কার্সর কেন্দ্রে রাখুন"
window: "উইন্ডো:"
scroll_to_zoom: "স্ক্রল করে জুম করুন"
scroll_to_zoom_desc: "মাউস হুইল সরাসরি চার্টকে জুম করে প্যানিং এর পরিবর্তে"
field_names: "ফিল্ড নাম"
field_normalization: "ফিল্ড নরমালাইজেশন"
field_normalization_desc: "ECU টাইপ জুড়ে চ্যানেল নাম মানসম্মত করুন"
Expand Down
2 changes: 2 additions & 0 deletions i18n/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "Cursor-Verfolgung"
cursor_tracking_desc: "Cursor beim Scrubben zentriert halten"
window: "Fenster:"
scroll_to_zoom: "Scrollen zum Zoomen"
scroll_to_zoom_desc: "Mausrad zoomt direkt in das Diagramm, anstatt es zu verschieben"
field_names: "Feldnamen"
field_normalization: "Feldnormalisierung"
field_normalization_desc: "Kanalnamen über ECU-Typen hinweg standardisieren"
Expand Down
2 changes: 2 additions & 0 deletions i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "Cursor Tracking"
cursor_tracking_desc: "Keep cursor centered while scrubbing"
window: "Window:"
scroll_to_zoom: "Scroll to Zoom"
scroll_to_zoom_desc: "Scroll wheel zooms the chart directly instead of panning"
field_names: "Field Names"
field_normalization: "Field Normalization"
field_normalization_desc: "Standardize channel names across ECU types"
Expand Down
2 changes: 2 additions & 0 deletions i18n/es.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "Seguimiento del Cursor"
cursor_tracking_desc: "Mantener el cursor centrado al desplazar"
window: "Ventana:"
scroll_to_zoom: "Desplazar para Zoom"
scroll_to_zoom_desc: "La rueda de desplazamiento amplía el gráfico directamente en lugar de desplazarse"
field_names: "Nombres de Campos"
field_normalization: "Normalizacion de Campos"
field_normalization_desc: "Estandarizar nombres de canales entre tipos de ECU"
Expand Down
2 changes: 2 additions & 0 deletions i18n/fr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "Suivi du curseur"
cursor_tracking_desc: "Garder le curseur centre pendant le defilement"
window: "Fenetre :"
scroll_to_zoom: "Scroll pour Zoomer"
scroll_to_zoom_desc: "La molette de la souris zoome le graphique directement au lieu de faire defiler"
field_names: "Noms des champs"
field_normalization: "Normalisation des champs"
field_normalization_desc: "Standardiser les noms de canaux entre les types d'ECU"
Expand Down
2 changes: 2 additions & 0 deletions i18n/hi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "कर्सर ट्रैकिंग"
cursor_tracking_desc: "स्क्रबिंग के दौरान कर्सर को केंद्रित रखें"
window: "विंडो:"
scroll_to_zoom: "स्क्रॉल करके ज़ूम करें"
scroll_to_zoom_desc: "माउस व्हील सीधे चार्ट को ज़ूम करता है, पैन करने के बजाय"
field_names: "फ़ील्ड नाम"
field_normalization: "फ़ील्ड नॉर्मलाइज़ेशन"
field_normalization_desc: "ECU प्रकारों में चैनल नामों को मानकीकृत करें"
Expand Down
2 changes: 2 additions & 0 deletions i18n/id.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "Pelacakan Kursor"
cursor_tracking_desc: "Pertahankan kursor di tengah saat menggulir"
window: "Jendela:"
scroll_to_zoom: "Gulir untuk Zoom"
scroll_to_zoom_desc: "Roda mouse memperbesar grafik secara langsung daripada menggeser"
field_names: "Nama Field"
field_normalization: "Normalisasi Field"
field_normalization_desc: "Standarisasi nama kanal antar jenis ECU"
Expand Down
2 changes: 2 additions & 0 deletions i18n/it.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "Tracciamento Cursore"
cursor_tracking_desc: "Mantieni il cursore centrato durante lo scorrimento"
window: "Finestra:"
scroll_to_zoom: "Scroll per Zoom"
scroll_to_zoom_desc: "La rotella del mouse ingrandisce il grafico direttamente invece di scorrere"
field_names: "Nomi dei Campi"
field_normalization: "Normalizzazione Campi"
field_normalization_desc: "Standardizza i nomi dei canali tra diversi tipi di ECU"
Expand Down
2 changes: 2 additions & 0 deletions i18n/ja.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "カーソル追従"
cursor_tracking_desc: "スクラブ中にカーソルを中央に保持"
window: "ウィンドウ:"
scroll_to_zoom: "スクロールしてズーム"
scroll_to_zoom_desc: "マウスホイールでチャートを直接ズームします(パンの代わりに)"
field_names: "フィールド名"
field_normalization: "フィールド正規化"
field_normalization_desc: "ECUタイプ間でチャンネル名を標準化"
Expand Down
2 changes: 2 additions & 0 deletions i18n/pt-BR.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "Rastreamento do Cursor"
cursor_tracking_desc: "Manter o cursor centralizado durante a navegação"
window: "Janela:"
scroll_to_zoom: "Scroll para Ampliar"
scroll_to_zoom_desc: "A roda do mouse amplia o gráfico diretamente em vez de fazer pan"
field_names: "Nomes dos Campos"
field_normalization: "Normalização de Campos"
field_normalization_desc: "Padronizar nomes de canais entre tipos de ECU"
Expand Down
2 changes: 2 additions & 0 deletions i18n/pt-PT.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "Seguimento do Cursor"
cursor_tracking_desc: "Manter o cursor centrado durante a navegação"
window: "Janela:"
scroll_to_zoom: "Scroll para Ampliação"
scroll_to_zoom_desc: "A roda do rato amplia o gráfico diretamente em vez de fazer pan"
field_names: "Nomes dos Campos"
field_normalization: "Normalização de Campos"
field_normalization_desc: "Uniformizar nomes de canais entre tipos de ECU"
Expand Down
2 changes: 2 additions & 0 deletions i18n/ru.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "Отслеживание курсора"
cursor_tracking_desc: "Удерживать курсор в центре при прокрутке"
window: "Окно:"
scroll_to_zoom: "Прокрутка для увеличения"
scroll_to_zoom_desc: "Колесико мыши увеличивает график напрямую вместо панорамирования"
field_names: "Имена полей"
field_normalization: "Нормализация полей"
field_normalization_desc: "Стандартизировать имена каналов для разных типов ECU"
Expand Down
2 changes: 2 additions & 0 deletions i18n/ur.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ settings:
cursor_tracking: "کرسر ٹریکنگ"
cursor_tracking_desc: "سکربنگ کے دوران کرسر کو مرکز میں رکھیں"
window: "ونڈو:"
scroll_to_zoom: "سکرول کریں تاکہ تقریب ہو"
scroll_to_zoom_desc: "ماؤس وہیل براہ راست چارٹ کو بڑھاتا ہے بجائے پیننگ کے"
field_names: "فیلڈ کے نام"
field_normalization: "فیلڈ نارملائزیشن"
field_normalization_desc: "ECU اقسام میں چینل ناموں کو معیاری بنائیں"
Expand Down
2 changes: 2 additions & 0 deletions i18n/zh-CN.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ settings:
cursor_tracking: "光标跟踪"
cursor_tracking_desc: "拖动时保持光标居中"
window: "窗口:"
scroll_to_zoom: "滚动以缩放"
scroll_to_zoom_desc: "鼠标滚轮直接缩放图表,而不是平移"
field_names: "字段名称"
field_normalization: "字段标准化"
field_normalization_desc: "统一不同 ECU 类型的通道名称"
Expand Down
70 changes: 70 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ pub struct UltraLogApp {
// === Chart View State ===
/// Initial view window in seconds (shown before user interacts with chart)
pub(crate) initial_view_seconds: f64,
/// When true, scroll wheel zooms chart directly instead of panning
pub(crate) scroll_to_zoom: bool,
// === Unit Preferences ===
/// User preferences for display units
pub(crate) unit_preferences: UnitPreferences,
Expand Down Expand Up @@ -176,6 +178,7 @@ impl Default for UltraLogApp {
color_blind_mode: false,
field_normalization: true, // Enabled by default for better readability
initial_view_seconds: 60.0, // Start with 60 second view
scroll_to_zoom: false,
unit_preferences: UnitPreferences::default(),
font_scale: FontScale::default(),
custom_normalizations: HashMap::new(),
Expand Down Expand Up @@ -254,6 +257,7 @@ impl UltraLogApp {
Self {
user_settings: user_settings.clone(),
language: user_settings.language,
scroll_to_zoom: user_settings.scroll_to_zoom,
..Self::default()
}
}
Expand Down Expand Up @@ -1356,6 +1360,12 @@ impl UltraLogApp {
}
}

/// Stop playback and reset the frame timer
fn stop_playback(&mut self) {
self.is_playing = false;
self.last_frame_time = None;
}

// ========================================================================
// Keyboard Shortcuts
// ========================================================================
Expand Down Expand Up @@ -1433,6 +1443,66 @@ impl UltraLogApp {
return;
}

// Arrow Left - step one record backward (Shift = 10 records)
if i.key_pressed(egui::Key::ArrowLeft) {
self.stop_playback();
if let Some(tab_idx) = self.active_tab {
let file_index = self.tabs[tab_idx].file_index;
if file_index < self.files.len() {
let times = self.files[file_index].log.get_times_as_f64();
if !times.is_empty() {
let current = self.get_cursor_record().unwrap_or(0);
let step = if shift { 10 } else { 1 };
let new_record = current.saturating_sub(step);
self.set_cursor_time(Some(times[new_record]));
self.set_cursor_record(Some(new_record));
}
}
}
return;
}

// Arrow Right - step one record forward (Shift = 10 records)
if i.key_pressed(egui::Key::ArrowRight) {
self.stop_playback();
if let Some(tab_idx) = self.active_tab {
let file_index = self.tabs[tab_idx].file_index;
if file_index < self.files.len() {
let times = self.files[file_index].log.get_times_as_f64();
if !times.is_empty() {
let current = self.get_cursor_record().unwrap_or(0);
let step = if shift { 10 } else { 1 };
let new_record = (current + step).min(times.len() - 1);
self.set_cursor_time(Some(times[new_record]));
self.set_cursor_record(Some(new_record));
}
}
}
return;
}

// Home - jump to start of log
if i.key_pressed(egui::Key::Home) {
self.stop_playback();
if let Some((min, _)) = self.get_time_range() {
self.set_cursor_time(Some(min));
let record = self.find_record_at_time(min);
self.set_cursor_record(record);
}
return;
}

// End - jump to end of log
if i.key_pressed(egui::Key::End) {
self.stop_playback();
if let Some((_, max)) = self.get_time_range() {
self.set_cursor_time(Some(max));
let record = self.find_record_at_time(max);
self.set_cursor_record(record);
}
return;
}
Comment on lines 1446 to 1504

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The newly added keyboard shortcuts for ArrowLeft, ArrowRight, Home, and End keys duplicate the logic for setting is_playing to false and last_frame_time to None. This repetition can be refactored into a single helper function or a common block to improve readability and maintainability.

            // Arrow Left - step one record backward (Shift = 10 records)
            if i.key_pressed(egui::Key::ArrowLeft) {
                self.stop_playback_and_reset_frame_time();
                if let Some(tab_idx) = self.active_tab {
                    let file_index = self.tabs[tab_idx].file_index;
                    if file_index < self.files.len() {
                        let times = self.files[file_index].log.get_times_as_f64();
                        if !times.is_empty() {
                            let current = self.get_cursor_record().unwrap_or(0);
                            let step = if shift { 10 } else { 1 };
                            let new_record = current.saturating_sub(step);
                            self.set_cursor_time(Some(times[new_record]));
                            self.set_cursor_record(Some(new_record));
                        }
                    }
                }
                return;
            }

            // Arrow Right - step one record forward (Shift = 10 records)
            if i.key_pressed(egui::Key::ArrowRight) {
                self.stop_playback_and_reset_frame_time();
                if let Some(tab_idx) = self.active_tab {
                    let file_index = self.tabs[tab_idx].file_index;
                    if file_index < self.files.len() {
                        let times = self.files[tab_idx].log.get_times_as_f64();
                        if !times.is_empty() {
                            let current = self.get_cursor_record().unwrap_or(0);
                            let step = if shift { 10 } else { 1 };
                            let new_record = (current + step).min(times.len() - 1);
                            self.set_cursor_time(Some(times[new_record]));
                            self.set_cursor_record(Some(new_record));
                        }
                    }
                }
                return;
            }

            // Home - jump to start of log
            if i.key_pressed(egui::Key::Home) {
                self.stop_playback_and_reset_frame_time();
                if let Some((min, _)) = self.get_time_range() {
                    self.set_cursor_time(Some(min));
                    let record = self.find_record_at_time(min);
                    self.set_cursor_record(record);
                }
                return;
            }

            // End - jump to end of log
            if i.key_pressed(egui::Key::End) {
                self.stop_playback_and_reset_frame_time();
                if let Some((_, max)) = self.get_time_range() {
                    self.set_cursor_time(Some(max));
                    let record = self.find_record_at_time(max);
                    self.set_cursor_record(record);
                }
                return;
            }


// Spacebar to toggle play/pause
if i.key_pressed(egui::Key::Space) {
self.is_playing = !self.is_playing;
Expand Down
4 changes: 4 additions & 0 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ pub struct UserSettings {
/// Selected language
#[serde(default)]
pub language: Language,
/// When true, scroll wheel zooms chart directly instead of panning
#[serde(default)]
pub scroll_to_zoom: bool,
}

fn default_version() -> u32 {
Expand All @@ -27,6 +30,7 @@ impl Default for UserSettings {
Self {
version: 1,
language: Language::default(),
scroll_to_zoom: false,
}
}
}
Expand Down
48 changes: 47 additions & 1 deletion src/ui/chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,30 @@ impl UltraLogApp {
let chart_interacted = self.get_chart_interacted();
let initial_view_seconds = self.initial_view_seconds;
let jump_to_time = self.get_jump_to_time();
let scroll_to_zoom = self.scroll_to_zoom;

// Read scroll input before plot consumes it (for scroll-to-zoom mode)
let scroll_delta_y = if scroll_to_zoom && !cursor_tracking {
ui.input(|i| i.smooth_scroll_delta.y)
} else {
0.0
};

// Fixed Y bounds for normalized data (0-1 with small padding)
const Y_MIN: f64 = -0.05;
const Y_MAX: f64 = 1.05;
/// Sensitivity multiplier for scroll-to-zoom (higher = faster zoom per scroll tick)
const SCROLL_ZOOM_SENSITIVITY: f64 = 0.003;

// Build the plot - X-axis zoom only, Y fixed
// When scroll_to_zoom is enabled, disable scroll-to-pan so we handle scroll as zoom
let plot = Plot::new("log_chart")
.legend(egui_plot::Legend::default())
.y_axis_label("") // Hide Y axis label since values are normalized
.show_axes([true, false]) // Show X axis (time), hide Y axis (normalized 0-1)
.allow_zoom([true, false]) // Only allow X-axis zoom
.allow_drag([!cursor_tracking, false]) // Only allow X-axis drag, never Y
.allow_scroll([!cursor_tracking, false]); // Only allow X-axis scroll, never Y
.allow_scroll([!cursor_tracking && !scroll_to_zoom, false]); // Disable scroll-pan when scroll-to-zoom enabled

let response = plot.show(ui, |plot_ui| {
// Get current bounds
Expand Down Expand Up @@ -170,6 +181,40 @@ impl UltraLogApp {
}
}

// Apply scroll-to-zoom: use scroll delta to zoom centered on pointer
if scroll_to_zoom && scroll_delta_y.abs() > 0.1 {
if let Some((min_t, max_t)) = time_range {
let zoom_factor =
(1.0 - scroll_delta_y as f64 * SCROLL_ZOOM_SENSITIVITY).clamp(0.8, 1.25);
let width = x_max - x_min;
let new_width = (width * zoom_factor).clamp(0.01, max_t - min_t);

// Zoom around pointer position if hovering, otherwise center
let center = plot_ui
.pointer_coordinate()
.map(|p| p.x.clamp(x_min, x_max))
.unwrap_or((x_min + x_max) / 2.0);
let ratio = if width > 0.0 {
(center - x_min) / width
} else {
0.5
};

x_min = center - new_width * ratio;
x_max = center + new_width * (1.0 - ratio);

// Clamp to data range
if x_min < min_t {
x_min = min_t;
x_max = (min_t + new_width).min(max_t);
}
if x_max > max_t {
x_max = max_t;
x_min = (max_t - new_width).max(min_t);
}
}
}

// Always enforce bounds: X clamped to data, Y fixed to normalized range
let new_bounds = PlotBounds::from_min_max([x_min, Y_MIN], [x_max, Y_MAX]);
plot_ui.set_plot_bounds(new_bounds);
Expand Down Expand Up @@ -224,6 +269,7 @@ impl UltraLogApp {
|| response.response.drag_started()
|| ui.input(|i| i.zoom_delta() != 1.0)
|| ui.input(|i| i.smooth_scroll_delta.x != 0.0)
|| (scroll_to_zoom && scroll_delta_y.abs() > 0.1)
{
self.set_chart_interacted(true);
}
Expand Down
20 changes: 20 additions & 0 deletions src/ui/settings_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,26 @@ impl UltraLogApp {
);
});
}

ui.add_space(8.0);

// Scroll to zoom
let old_scroll_to_zoom = self.scroll_to_zoom;
ui.checkbox(
&mut self.scroll_to_zoom,
egui::RichText::new(t!("settings.scroll_to_zoom")).size(font_14),
);
ui.label(
egui::RichText::new(t!("settings.scroll_to_zoom_desc"))
.size(font_12)
.color(egui::Color32::GRAY),
);
if self.scroll_to_zoom != old_scroll_to_zoom {
self.user_settings.scroll_to_zoom = self.scroll_to_zoom;
if let Err(e) = self.user_settings.save() {
self.show_toast_error(&t!("toast.failed_to_save", error = e));
}
}
});
}

Expand Down
3 changes: 3 additions & 0 deletions tests/core/settings_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ fn test_settings_roundtrip() {
let original = UserSettings {
version: 1,
language: Language::Spanish,
scroll_to_zoom: false,
};

let json = serde_json::to_string(&original).unwrap();
Expand All @@ -125,6 +126,7 @@ fn test_settings_roundtrip_all_languages() {
let settings = UserSettings {
version: 1,
language: *lang,
scroll_to_zoom: false,
};

let json = serde_json::to_string(&settings).unwrap();
Expand Down Expand Up @@ -223,6 +225,7 @@ fn test_settings_clone() {
let original = UserSettings {
version: 1,
language: Language::Spanish,
scroll_to_zoom: false,
};

let cloned = original.clone();
Expand Down
Loading