diff --git a/reaction_systems_gui/Cargo.toml b/reaction_systems_gui/Cargo.toml index 65c18f4..7527355 100644 --- a/reaction_systems_gui/Cargo.toml +++ b/reaction_systems_gui/Cargo.toml @@ -14,6 +14,8 @@ serde = { version = "1", optional = true } colored = "*" lalrpop-util = "*" petgraph = ">=0.8" +nsvg = "0.5.1" +dyn-clone = "*" petgraph-graphml = "*" egui_node_graph2 = { path = "../egui_node_graph2" } getrandom = "0.3" # dependency that has to be specified correctly for wasm diff --git a/reaction_systems_gui/src/app.rs b/reaction_systems_gui/src/app.rs index 9aa7e13..0eddb52 100644 --- a/reaction_systems_gui/src/app.rs +++ b/reaction_systems_gui/src/app.rs @@ -63,6 +63,7 @@ pub enum BasicDataType { PositiveGraph, PositiveAssertFunction, PositiveGroupFunction, + Svg, } /// Should reflect `BasicDataType`'s values, holding the data that will be @@ -175,6 +176,9 @@ pub enum BasicValue { PositiveGroupFunction { value: assert::positive_grouping::PositiveAssert, }, + Svg { + value: super::svg::Svg, + }, } impl Hash for BasicValue { @@ -216,7 +220,8 @@ impl Hash for BasicValue { PositiveContext, PositiveReactions, PositiveAssertFunction, - PositiveGroupFunction + PositiveGroupFunction, + Svg ); match self { @@ -278,6 +283,7 @@ pub enum NodeInstruction { DisplayEdge, ColorNode, ColorEdge, + StringToSvg, // convert basic data types ToPositiveSet, @@ -486,6 +492,7 @@ impl NodeInstruction { ("display edge", DisplayEdge), ], | Self::Sleep => vec![("seconds", PositiveInt)], + | Self::StringToSvg => vec![("value", String)], } .into_iter() .map(|e| (e.0.to_string(), e.1)) @@ -581,6 +588,7 @@ impl NodeInstruction { vec![("out", String)], | Self::PositiveBisimilarityPaigeTarjan => vec![("out", String)], | Self::Sleep => vec![("out", PositiveInt)], + | Self::StringToSvg => vec![("out", Svg)], }; res.into_iter() .map(|res| (res.0.to_string(), res.1)) @@ -684,6 +692,10 @@ impl NodeInstruction { PositiveGroupFunction, assert::positive_grouping::PositiveAssert::default() ), + | BasicDataType::Svg => helper!( + Svg, + super::svg::Svg::default() + ) } } @@ -739,6 +751,8 @@ impl NodeInstruction { helper!(PositiveAssertFunction), | BasicDataType::PositiveGroupFunction => helper!(PositiveGroupFunction), + | BasicDataType::Svg => + helper!(Svg), } } } @@ -935,6 +949,8 @@ impl DataTypeTrait for BasicDataType { egui::Color32::from_rgb(200, 150, 120), | Self::PositiveGroupFunction => egui::Color32::from_rgb(150, 120, 200), + | Self::Svg => + egui::Color32::from_rgb(200, 200, 240), } } @@ -972,6 +988,7 @@ impl DataTypeTrait for BasicDataType { Cow::Borrowed("positive assert function"), | Self::PositiveGroupFunction => Cow::Borrowed("positive group function"), + | Self::Svg => Cow::Borrowed("Svg"), } } } @@ -1069,6 +1086,7 @@ impl NodeTemplateTrait for NodeInstruction { | Self::PositiveBisimilarityPaigeTarjan => "Positive Paige & Torjan", | Self::Sleep => "Sleep", + | Self::StringToSvg => "String to SVG", }) } @@ -1150,7 +1168,8 @@ impl NodeTemplateTrait for NodeInstruction { | Self::PositiveBisimilarityPaigeTarjanNoLabels | Self::PositiveBisimilarityPaigeTarjan => vec!["Positive Graph", "Positive Bisimilarity"], - | Self::Sleep => vec!["General"], + | Self::Sleep + | Self::StringToSvg => vec!["General"], } } @@ -1256,6 +1275,7 @@ impl NodeTemplateIter for AllInstructions { NodeInstruction::PositiveBisimilarityPaigeTarjanNoLabels, NodeInstruction::PositiveBisimilarityPaigeTarjan, NodeInstruction::Sleep, + NodeInstruction::StringToSvg, ] } } @@ -1395,6 +1415,9 @@ impl WidgetValueTrait for BasicValue { | BasicValue::PositiveGroupFunction { value: _ } => { ui.label(param_name); }, + | BasicValue::Svg { value: _ } => { + ui.label(param_name); + } } responses @@ -1523,7 +1546,7 @@ pub struct AppHandle { translator: Arc>, - cached_last_value: Option, + cached_last_value: Option, #[cfg(not(target_arch = "wasm32"))] app_logic_thread: Option>>, @@ -1870,15 +1893,15 @@ impl eframe::App for AppHandle { user_state.display_result }; if display_result { - let mut text = LayoutJob::default(); + let mut content = WidgetLayout::default(); let mut spin = false; if let Some(l_v) = &self.cached_last_value { - text = l_v.clone(); + content = l_v.clone(); } else { #[cfg(not(target_arch = "wasm32"))] { // wasm does not support threads :-( - // ------------------------------------------------------------- + // --------------------------------------------------------- // did we already start a thread? if self.app_logic_thread.is_none() { let thread_join_handle = { @@ -1911,7 +1934,7 @@ impl eframe::App for AppHandle { if let Err(e) = err { let text = get_layout(Err(e), &self.translator.lock().unwrap(), ctx); - self.cached_last_value = Some(text.clone()); + self.cached_last_value = Some(text); } else if let Some(l_b_v) = self.cache.get_last_state() { if let BasicValue::SaveString { path, value } = &l_b_v { use std::io::Write; @@ -1927,8 +1950,8 @@ impl eframe::App for AppHandle { return; } } - text = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx); - self.cached_last_value = Some(text.clone()); + content = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx); + self.cached_last_value = Some(content.clone()); } } else { spin = true; @@ -1945,7 +1968,7 @@ impl eframe::App for AppHandle { ); if let Err(e) = err { let text = get_layout(Err(e), &self.translator.lock().unwrap(), ctx); - self.cached_last_value = Some(text.clone()); + self.cached_last_value = Some(content.clone()); } else if let Some(l_b_v) = self.cache.get_last_state() { if let BasicValue::SaveString { path, value } = &l_b_v { use std::io::Write; @@ -1961,8 +1984,8 @@ impl eframe::App for AppHandle { return; } } - text = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx); - self.cached_last_value = Some(text.clone()); + content = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx); + self.cached_last_value = Some(content.clone()); } spin = false; } @@ -1972,24 +1995,22 @@ impl eframe::App for AppHandle { if spin { window.show(ctx, |ui| { - egui::ScrollArea::vertical().show(ui, |ui| { - use egui::widgets::Widget; - ui.vertical_centered(|ui| { - ui.heading("Result"); - }); + use egui::widgets::Widget; + ui.vertical_centered(|ui| { + ui.heading("Result"); + }); + ui.separator(); + ui.vertical_centered(|ui| { egui::widgets::Spinner::new().ui(ui); }); }); } else { window.show(ctx, |ui| { - egui::ScrollArea::vertical().show(ui, |ui| { - ui.vertical_centered(|ui| { - ui.heading("Result"); - }); - egui::ScrollArea::vertical().show(ui, |ui| { - ui.label(text); - }); + ui.vertical_centered(|ui| { + ui.heading("Result"); }); + ui.separator(); + ui.add(content); }); } } @@ -2044,11 +2065,40 @@ fn create_output( Ok(()) } +#[derive(Clone, Default)] +enum WidgetLayout { + LayoutJob(LayoutJob), + Image(egui::TextureHandle), + + #[default] + Empty, +} + +impl egui::Widget for WidgetLayout { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + match self { + | Self::LayoutJob(lj) => { + let mut response = None; + egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { + response = Some(egui::Label::new(lj).ui(ui)); + }); + response.unwrap() + }, + | Self::Empty => { + egui::Label::new("").ui(ui) + }, + | Self::Image(i) => { + egui::Image::new(&i).max_size(ui.available_size()).ui(ui) + } + } + } +} + fn get_layout( value: anyhow::Result, translator: &rsprocess::translator::Translator, ctx: &egui::Context, -) -> LayoutJob { +) -> WidgetLayout { let mut text = LayoutJob::default(); match value { @@ -2231,6 +2281,9 @@ fn get_layout( 0., Default::default(), ), + | BasicValue::Svg { value } => { + return WidgetLayout::Image(value.get_texture(ctx)); + }, }, | Err(err) => { text.append(&format!("{err:?}"), 0., TextFormat { @@ -2239,5 +2292,5 @@ fn get_layout( }); }, } - text + WidgetLayout::LayoutJob(text) } diff --git a/reaction_systems_gui/src/app_logic.rs b/reaction_systems_gui/src/app_logic.rs index 66001af..9cb913c 100644 --- a/reaction_systems_gui/src/app_logic.rs +++ b/reaction_systems_gui/src/app_logic.rs @@ -2492,7 +2492,27 @@ fn process_template( #[cfg(target_arch = "wasm32")] { anyhow::bail!("Cannot sleep on wams"); } - } + }, + | NodeInstruction::StringToSvg => { + let s = retrieve_from_cache![1]; + let hash_inputs = hash_inputs!(s); + + if let BasicValue::String { value } = s { + let res = match super::svg::Svg::parse_dot_string(&value) { + Ok(svg) => svg, + Err(e) => anyhow::bail!(e), + }; + + let res = BasicValue::Svg { value: res }; + set_cache_output!(( + output_names.first().unwrap(), + res, + hash_inputs + )); + } else { + anyhow::bail!("Not a string"); + } + }, } Ok(None) } diff --git a/reaction_systems_gui/src/lib.rs b/reaction_systems_gui/src/lib.rs index 2e02f9e..7bfd0ba 100644 --- a/reaction_systems_gui/src/lib.rs +++ b/reaction_systems_gui/src/lib.rs @@ -6,6 +6,7 @@ mod app; mod app_logic; mod helper; +mod svg; pub use app::AppHandle; diff --git a/reaction_systems_gui/src/svg.rs b/reaction_systems_gui/src/svg.rs new file mode 100644 index 0000000..38cb37e --- /dev/null +++ b/reaction_systems_gui/src/svg.rs @@ -0,0 +1,112 @@ +use std::{fmt::Debug, hash::Hash, sync::{Arc, Mutex}}; + +use layout::{backends::svg::SVGWriter, gv::{self, GraphBuilder}}; +use eframe::egui; + +#[cfg_attr( + feature = "persistence", + derive(serde::Serialize, serde::Deserialize) +)] +#[derive(Clone, Default)] +pub(crate) struct Svg { + image: egui::ColorImage, + /// original size of the svg + svg_size: egui::Vec2, + + #[cfg_attr(feature = "persistence", serde(skip))] + svg_texture: Arc>>, +} + +impl Svg { + pub(crate) fn parse_dot_string(dot_str: &str) -> Result { + let mut parser = gv::DotParser::new(dot_str); + let g = match parser.process() { + Ok(g) => g, + Err(_) => + // errors are printed to sdtout so we ignore them + return Err("Could not parse dot string.".into()), + }; + + let mut gb = GraphBuilder::new(); + gb.visit_graph(&g); + let mut graph = gb.get(); + let mut svg = SVGWriter::new(); + graph.do_it( + false, + false, + false, + &mut svg, + ); + let content = svg.finalize(); + + let svg = match nsvg::parse_str(&content, nsvg::Units::Pixel, 96.0) { + Ok(svg) => svg, + Err(nsvg_err) => return Err(format!("{}", nsvg_err)), + }; + + let svg_size = egui::vec2(svg.width(), svg.height()); + + let (w, h, data) = match svg.rasterize_to_raw_rgba(1.) { + Ok(o) => o, + Err(e) => return Err(format!("{}", e)), + }; + + let image = egui::ColorImage::from_rgba_unmultiplied([w as _, h as _], &data); + + let svg = Svg { image, svg_size, svg_texture: Arc::new(Mutex::new(None)) }; + + Ok(svg) + } + + pub(crate) fn get_texture(&self, ctx: &egui::Context) -> egui::TextureHandle { + let tx = self.svg_texture.lock().expect("Poisoned"); + if tx.is_some() { + (*tx).clone().unwrap() + } else { + std::mem::drop(tx); + let svg_texture = ctx.load_texture("svg", self.image.clone(), Default::default()); + *self.svg_texture.lock().expect("Poisoned") = Some(svg_texture.clone()); + svg_texture + } + } +} + +impl Debug for Svg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[image: {:?}, svg_size: {:?}, svg_texture: {}", + self.image, + self.svg_size, + if self.svg_texture.lock().expect("Poisoned").is_some() { + "Some(...)" + } else { + "None" + } + ) + } +} + +impl Hash for Svg { + fn hash(&self, state: &mut H) { + macro_rules! hash_float { + ($name:expr) => ( + let bits = if $name.is_nan() { + // "Canonical" NaN. + 0x7fc00000 + } else { + // A trick taken from the `ordered-float` crate: -0.0 + 0.0 == +0.0. + // https://github.com/reem/rust-ordered-float/blob/1841f0541ea0e56779cbac03de2705149e020675/src/lib.rs#L2178-L2181 + ($name + 0.0).to_bits() + }; + bits.hash(state); + ); + } + + hash_float!(self.svg_size.x); + hash_float!(self.svg_size.y); + self.image.pixels.hash(state); + self.image.size.hash(state); + hash_float!(self.image.source_size.x); + hash_float!(self.image.source_size.y); + self.svg_texture.lock().expect("Poisoned").hash(state); + } +}