feat: add background color support, internal element rendering toggle

This commit is contained in:
Tomáš Mládek 2026-01-24 23:36:06 +01:00
parent abdfff92ca
commit 714d00fbb9
3 changed files with 148 additions and 49 deletions

View file

@ -1,5 +1,5 @@
use crate::svg::{Renderable as _, SvgContent}; 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. /// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
@ -21,6 +21,9 @@ pub struct TemplateApp {
#[serde(skip)] #[serde(skip)]
show_debug: bool, show_debug: bool,
#[serde(skip)]
render_internal_areas: bool,
/// Exponential moving average of frame time for stable FPS display. /// Exponential moving average of frame time for stable FPS display.
#[serde(skip)] #[serde(skip)]
fps_ema: f32, fps_ema: f32,
@ -47,6 +50,7 @@ impl Default for TemplateApp {
zoom: 1.0, zoom: 1.0,
show_menu_bar: false, show_menu_bar: false,
show_debug: false, show_debug: false,
render_internal_areas: false,
fps_ema: 60.0, fps_ema: 60.0,
last_pointer_pos: None, last_pointer_pos: None,
is_dragging: false, is_dragging: false,
@ -196,10 +200,19 @@ impl eframe::App for TemplateApp {
let mut rendered_count = 0u32; 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| { egui::CentralPanel::default().show(ctx, |ui| {
let (response, painter) = let (response, painter) =
ui.allocate_painter(ui.available_size(), egui::Sense::click_and_drag()); ui.allocate_painter(ui.available_size(), egui::Sense::click_and_drag());
let canvas_rect = response.rect; let canvas_rect = response.rect;
painter.rect_filled(canvas_rect, 0.0, background_color);
// Drag handling // Drag handling
if primary_pressed && response.hovered() { if primary_pressed && response.hovered() {
@ -241,9 +254,12 @@ impl eframe::App for TemplateApp {
} }
// Element colors // Element colors
let video_scroll_color = egui::Color32::from_rgb(70, 130, 180); let internal_alpha = (0.3_f32 * 255.0).round() as u8;
let audio_area_color = egui::Color32::from_rgb(60, 179, 113); let video_scroll_color =
let anchor_color = egui::Color32::from_rgb(255, 215, 0); 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); let text_color = egui::Color32::from_rgb(255, 255, 255);
// Initialize text cache (renders in white, color applied as tint) // 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(), (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"); let text_cache = self.text_cache.as_mut().expect("just initialized");
if let Some(ref content) = self.svg_content { if let Some(ref content) = self.svg_content {
// Video scrolls if self.render_internal_areas {
for vs in &content.video_scrolls { let mut hovered_descs: Vec<String> = Vec::new();
let (x, y, w, h) = vs.bounds(); let pointer_svg = pointer_pos.map(screen_to_svg);
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;
}
}
// Audio areas // Video scrolls
for aa in &content.audio_areas { for vs in &content.video_scrolls {
let (x, y, w, h) = aa.bounds(); let (x, y, w, h) = vs.bounds();
let rect = let rect = egui::Rect::from_min_max(
egui::Rect::from_min_max(svg_to_screen(x, y), svg_to_screen(x + w, y + h)); svg_to_screen(x, y),
if rect.intersects(canvas_rect) { svg_to_screen(x + w, y + h),
painter.rect_filled(rect, (w.min(h) * zoom) / 2.0, audio_area_color); );
rendered_count += 1; if rect.intersects(canvas_rect) {
} painter.rect_filled(rect, 0.0, video_scroll_color);
} rendered_count += 1;
}
// Anchors if let Some((svg_x, svg_y)) = pointer_svg {
for anchor in &content.anchors { if svg_x >= x && svg_x <= x + w && svg_y >= y && svg_y <= y + h {
let (x, y, w, h) = anchor.bounds(); hovered_descs.push(format!("Video: {}", vs.desc));
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; // 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; 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 { if let Some(ref cache) = self.text_cache {
ui.separator(); ui.separator();
ui.label(format!("Text cache: {} entries", cache.cache_size())); ui.label(format!("Text cache: {} entries", cache.cache_size()));

View file

@ -1,13 +1,15 @@
#![warn(clippy::all, rust_2018_idioms)] #![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use log::LevelFilter;
// When compiling natively: // When compiling natively:
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result { fn main() -> eframe::Result {
env_logger::Builder::from_env( env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
env_logger::Env::default().default_filter_or("debug"), .filter_level(LevelFilter::Info)
) .filter_module("las", LevelFilter::Debug)
.init(); .init();
let native_options = eframe::NativeOptions { let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()

View file

@ -113,6 +113,7 @@ pub struct SvgContent {
pub anchors: Vec<Anchor>, pub anchors: Vec<Anchor>,
pub texts: Vec<TextElement>, pub texts: Vec<TextElement>,
pub viewbox: Option<(f32, f32, f32, f32)>, // (min_x, min_y, width, height) 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. /// State for tracking current element during parsing.
@ -186,6 +187,13 @@ impl SvgContent {
Some((parts[0], parts[1], parts[2], parts[3])); 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" => { "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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;