From 714d00fbb9f3fbefb66368774f2a194d119763fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Ml=C3=A1dek?= Date: Sat, 24 Jan 2026 23:36:06 +0100 Subject: [PATCH] feat: add background color support, internal element rendering toggle --- src/app.rs | 149 ++++++++++++++++++++++++++++++++++++---------------- src/main.rs | 10 ++-- src/svg.rs | 38 ++++++++++++++ 3 files changed, 148 insertions(+), 49 deletions(-) diff --git a/src/app.rs b/src/app.rs index f32ff92..1e521c2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,5 @@ use crate::svg::{Renderable as _, SvgContent}; -use crate::text_cache::{RENDER_SCALE, TextCache}; +use crate::text_cache::{TextCache, RENDER_SCALE}; /// We derive Deserialize/Serialize so we can persist app state on shutdown. #[derive(serde::Deserialize, serde::Serialize)] @@ -21,6 +21,9 @@ pub struct TemplateApp { #[serde(skip)] show_debug: bool, + #[serde(skip)] + render_internal_areas: bool, + /// Exponential moving average of frame time for stable FPS display. #[serde(skip)] fps_ema: f32, @@ -47,6 +50,7 @@ impl Default for TemplateApp { zoom: 1.0, show_menu_bar: false, show_debug: false, + render_internal_areas: false, fps_ema: 60.0, last_pointer_pos: None, is_dragging: false, @@ -196,10 +200,19 @@ impl eframe::App for TemplateApp { let mut rendered_count = 0u32; + let background_rgb = self + .svg_content + .as_ref() + .and_then(|content| content.background_color) + .unwrap_or([0, 0, 0]); + let background_color = + egui::Color32::from_rgb(background_rgb[0], background_rgb[1], background_rgb[2]); + egui::CentralPanel::default().show(ctx, |ui| { let (response, painter) = ui.allocate_painter(ui.available_size(), egui::Sense::click_and_drag()); let canvas_rect = response.rect; + painter.rect_filled(canvas_rect, 0.0, background_color); // Drag handling if primary_pressed && response.hovered() { @@ -241,9 +254,12 @@ impl eframe::App for TemplateApp { } // Element colors - let video_scroll_color = egui::Color32::from_rgb(70, 130, 180); - let audio_area_color = egui::Color32::from_rgb(60, 179, 113); - let anchor_color = egui::Color32::from_rgb(255, 215, 0); + let internal_alpha = (0.3_f32 * 255.0).round() as u8; + let video_scroll_color = + egui::Color32::from_rgba_unmultiplied(255, 0, 0, internal_alpha); + let audio_area_color = + egui::Color32::from_rgba_unmultiplied(0, 0, 255, internal_alpha); + let anchor_color = egui::Color32::from_rgba_unmultiplied(64, 255, 64, internal_alpha); let text_color = egui::Color32::from_rgb(255, 255, 255); // Initialize text cache (renders in white, color applied as tint) @@ -262,40 +278,93 @@ impl eframe::App for TemplateApp { (y + pan_y) * zoom + canvas_rect.top(), ) }; + let screen_to_svg = |pos: egui::Pos2| -> (f32, f32) { + ( + (pos.x - canvas_rect.left()) / zoom - pan_x, + (pos.y - canvas_rect.top()) / zoom - pan_y, + ) + }; let text_cache = self.text_cache.as_mut().expect("just initialized"); if let Some(ref content) = self.svg_content { - // Video scrolls - for vs in &content.video_scrolls { - let (x, y, w, h) = vs.bounds(); - let rect = - egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h)); - if rect.intersects(canvas_rect) { - painter.rect_filled(rect, 0.0, video_scroll_color); - rendered_count += 1; - } - } + if self.render_internal_areas { + let mut hovered_descs: Vec = Vec::new(); + let pointer_svg = pointer_pos.map(screen_to_svg); - // Audio areas - for aa in &content.audio_areas { - let (x, y, w, h) = aa.bounds(); - let rect = - egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h)); - if rect.intersects(canvas_rect) { - painter.rect_filled(rect, (w.min(h) * zoom) / 2.0, audio_area_color); - rendered_count += 1; - } - } + // Video scrolls + for vs in &content.video_scrolls { + let (x, y, w, h) = vs.bounds(); + let rect = egui::Rect::from_min_max( + svg_to_screen(x, y), + svg_to_screen(x + w, y + h), + ); + if rect.intersects(canvas_rect) { + painter.rect_filled(rect, 0.0, video_scroll_color); + rendered_count += 1; + } - // Anchors - for anchor in &content.anchors { - let (x, y, w, h) = anchor.bounds(); - let rect = - egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h)); - if rect.intersects(canvas_rect) { - painter.rect_filled(rect, 0.0, anchor_color); - rendered_count += 1; + if let Some((svg_x, svg_y)) = pointer_svg { + if svg_x >= x && svg_x <= x + w && svg_y >= y && svg_y <= y + h { + hovered_descs.push(format!("Video: {}", vs.desc)); + } + } + } + + // Audio areas + for aa in &content.audio_areas { + let center = svg_to_screen(aa.cx, aa.cy); + let radius = aa.radius * zoom; + let rect = egui::Rect::from_min_max( + egui::pos2(center.x - radius, center.y - radius), + egui::pos2(center.x + radius, center.y + radius), + ); + if rect.intersects(canvas_rect) { + painter.circle_filled(center, radius, audio_area_color); + rendered_count += 1; + } + + if let Some((svg_x, svg_y)) = pointer_svg { + let dx = svg_x - aa.cx; + let dy = svg_y - aa.cy; + if dx * dx + dy * dy <= aa.radius * aa.radius { + hovered_descs.push(format!("Audio: {}", aa.desc)); + } + } + } + + // Anchors + for anchor in &content.anchors { + let (x, y, w, h) = anchor.bounds(); + let rect = egui::Rect::from_min_max( + svg_to_screen(x, y), + svg_to_screen(x + w, y + h), + ); + if rect.intersects(canvas_rect) { + painter.rect_filled(rect, 0.0, anchor_color); + rendered_count += 1; + } + + if let Some((svg_x, svg_y)) = pointer_svg { + if svg_x >= x && svg_x <= x + w && svg_y >= y && svg_y <= y + h { + hovered_descs.push(format!("Anchor: {}", anchor.id)); + } + } + } + + if !hovered_descs.is_empty() { + egui::Tooltip::always_open( + ctx.clone(), + ui.layer_id(), + egui::Id::new("internal-area-desc"), + egui::PopupAnchor::Pointer, + ) + .gap(12.0) + .show(|ui| { + for desc in hovered_descs { + ui.label(desc); + } + }); } } @@ -342,19 +411,6 @@ impl eframe::App for TemplateApp { rendered_count += 1; } - - // Viewbox border - if let Some((vb_x, vb_y, vb_w, vb_h)) = content.viewbox { - painter.rect_stroke( - egui::Rect::from_min_max( - svg_to_screen(vb_x, vb_y), - svg_to_screen(vb_x + vb_w, vb_y + vb_h), - ), - 0.0, - egui::Stroke::new(1.0, egui::Color32::from_gray(100)), - egui::StrokeKind::Inside, - ); - } } }); @@ -385,6 +441,9 @@ impl eframe::App for TemplateApp { )); } + ui.separator(); + ui.checkbox(&mut self.render_internal_areas, "Render internal areas"); + if let Some(ref cache) = self.text_cache { ui.separator(); ui.label(format!("Text cache: {} entries", cache.cache_size())); diff --git a/src/main.rs b/src/main.rs index b223c4b..1031007 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,15 @@ #![warn(clippy::all, rust_2018_idioms)] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +use log::LevelFilter; + // When compiling natively: #[cfg(not(target_arch = "wasm32"))] fn main() -> eframe::Result { - env_logger::Builder::from_env( - env_logger::Env::default().default_filter_or("debug"), - ) - .init(); + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .filter_level(LevelFilter::Info) + .filter_module("las", LevelFilter::Debug) + .init(); let native_options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() diff --git a/src/svg.rs b/src/svg.rs index 5fbfe79..8a00006 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -113,6 +113,7 @@ pub struct SvgContent { pub anchors: Vec, pub texts: Vec, pub viewbox: Option<(f32, f32, f32, f32)>, // (min_x, min_y, width, height) + pub background_color: Option<[u8; 3]>, } /// State for tracking current element during parsing. @@ -186,6 +187,13 @@ impl SvgContent { Some((parts[0], parts[1], parts[2], parts[3])); } } + if attr.key.as_ref() == b"style" { + let value = String::from_utf8_lossy(&attr.value); + if svg_content.background_color.is_none() { + svg_content.background_color = + parse_background_color(&value); + } + } } } "image" => { @@ -465,6 +473,36 @@ impl SvgContent { } } +fn parse_background_color(style: &str) -> Option<[u8; 3]> { + for entry in style.split(';') { + let entry = entry.trim(); + if let Some(value) = entry.strip_prefix("background-color:") { + let value = value.trim(); + return parse_hex_color(value); + } + } + None +} + +fn parse_hex_color(value: &str) -> Option<[u8; 3]> { + let hex = value.strip_prefix('#')?.trim(); + match hex.len() { + 3 => { + let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?; + let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?; + let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?; + Some([r, g, b]) + } + 6 => { + let r = u8::from_str_radix(&hex[0..2], 16).ok()?; + let g = u8::from_str_radix(&hex[2..4], 16).ok()?; + let b = u8::from_str_radix(&hex[4..6], 16).ok()?; + Some([r, g, b]) + } + _ => None, + } +} + #[cfg(test)] mod tests { use super::*;