feat: add background color support, internal element rendering toggle
This commit is contained in:
parent
abdfff92ca
commit
714d00fbb9
3 changed files with 148 additions and 49 deletions
149
src/app.rs
149
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<String> = 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()));
|
||||
|
|
|
|||
10
src/main.rs
10
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()
|
||||
|
|
|
|||
38
src/svg.rs
38
src/svg.rs
|
|
@ -113,6 +113,7 @@ pub struct SvgContent {
|
|||
pub anchors: Vec<Anchor>,
|
||||
pub texts: Vec<TextElement>,
|
||||
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::*;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue