diff --git a/gui/src/rust/Cargo.lock b/gui/src/rust/Cargo.lock index 9ce044b853b..71ab650c142 100644 --- a/gui/src/rust/Cargo.lock +++ b/gui/src/rust/Cargo.lock @@ -963,8 +963,10 @@ dependencies = [ "enso-frp 0.1.0", "enso-prelude 0.1.0", "ensogl 0.1.0", + "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", "logger 0.1.0", "nalgebra 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "shapely 0.1.0", "span-tree 0.1.0", diff --git a/gui/src/rust/lib/graph-editor/Cargo.toml b/gui/src/rust/lib/graph-editor/Cargo.toml index 9cf4a378636..bb8efd84a3e 100644 --- a/gui/src/rust/lib/graph-editor/Cargo.toml +++ b/gui/src/rust/lib/graph-editor/Cargo.toml @@ -5,17 +5,19 @@ authors = ["Enso Team "] edition = "2018" [dependencies] -ast = { version = "0.1.0" , path = "../../ide/ast/impl" } -span-tree = { version = "0.1.0" , path = "../../ide/span-tree" } -shapely = { version = "0.1.0" , path = "../shapely/impl" } -ensogl = { version = "0.1.0" , path = "../../ensogl" } -enso-prelude = { version = "0.1.0" , path = "../prelude" } -logger = { version = "0.1.0" , path = "../logger" } -enso-frp = { version = "0.1.0" , path = "../frp" } +ast = { version = "0.1.0" , path = "../../ide/ast/impl" } +span-tree = { version = "0.1.0" , path = "../../ide/span-tree" } +shapely = { version = "0.1.0" , path = "../shapely/impl" } +ensogl = { version = "0.1.0" , path = "../../ensogl" } +enso-prelude = { version = "0.1.0" , path = "../prelude" } +logger = { version = "0.1.0" , path = "../logger" } +enso-frp = { version = "0.1.0" , path = "../frp" } -wasm-bindgen = { version = "=0.2.58" , features = ["nightly"] } -nalgebra = { version = "0.19.0" } -serde_json = { version = "1.0" } +wasm-bindgen = { version = "=0.2.58" , features = ["nightly","serde-serialize"] } +nalgebra = { version = "0.19.0" , features = ["serde-serialize"] } +serde_json = { version = "1.0" } +serde = { version = "1.0" , features = ["derive"] } +js-sys = { version = "0.3.28" } [dependencies.web-sys] version = "0.3.4" diff --git a/gui/src/rust/lib/graph-editor/src/component/node.rs b/gui/src/rust/lib/graph-editor/src/component/node.rs index f6b9742de76..eed7cea0a0d 100644 --- a/gui/src/rust/lib/graph-editor/src/component/node.rs +++ b/gui/src/rust/lib/graph-editor/src/component/node.rs @@ -474,7 +474,7 @@ impl Node { let visualization_container = visualization::Container::new(); visualization_container.mod_position(|t| { t.x = 60.0; - t.y = -60.0; + t.y = -120.0; }); display_object.add_child(&visualization_container); diff --git a/gui/src/rust/lib/graph-editor/src/component/visualization.rs b/gui/src/rust/lib/graph-editor/src/component/visualization.rs index a2d5b1c633c..43e165996df 100644 --- a/gui/src/rust/lib/graph-editor/src/component/visualization.rs +++ b/gui/src/rust/lib/graph-editor/src/component/visualization.rs @@ -1,276 +1,29 @@ //! This module defines the visualization widgets and related functionality. //! -//! At the core of this functionality is the `Visualisation` that takes in data and renders an -//! output visualisation which is displayed in a `Container`. The `Container` provides generic UI -//! elements that facilitate generic interactions, for example, visualisation selection. The -//! `Container` also provides the FRP API that allows internal interaction with the -//! `Visualisation`. Data for a visualisation has to be provided wrapped in the `Data` struct. +//! The overall architecture of visualizations consists of three parts: +//! +//! 1. The `DataRenderer` trait provides the functionality to render the actual visualization view +//! that implements `display::Object`. It is provided with data and itself provides frp streams +//! of its output if there is some, for example, if it acts as a widget. +//! +//! 2. The `Visualization` is a struct that wraps the `DataRenderer` and implements the generic +//! tasks that are the same for all visualisations. That is, interfacing with the other UI +//! elements, the visualization registry, as well as propagating frp messages. +//! +//! 3. The `Container` wraps the `Visualisation` and provides the UI elements that facilitate +//! user interactions. For example, selecting a visualisation or connecting it to nodes in the +//! graph editor scene. +//! +//! In addition this module also contains a `Data` struct that provides a dynamically typed way to +//! handle data for visualisations. This allows the `Visualisation` struct to be without type +//! parameters and simplifies the FRP communication and complexity of the node system. -use crate::prelude::*; +pub mod class; +pub mod container; +pub mod renderer; +pub mod data; -use crate::frp; - -use ensogl::display::DomSymbol; -use ensogl::display; -use ensogl::system::web; -use serde_json; -use web::StyleSetter; -use ensogl::display::object::traits::*; - - - -// ============================================ -// === Wrapper for Visualisation Input Data === -// ============================================ - -/// Wrapper for data that can be consumed by a visualisation. -// TODO replace with better typed data wrapper. -#[derive(Clone,CloneRef,Debug)] -#[allow(missing_docs)] -pub enum Data { - JSON { content : Rc }, - Binary, -} - -impl Data { - /// Render the data as JSON. - pub fn as_json(&self) -> String { - match &self { - Data::JSON { content } => content.to_string(), - _ => "{}".to_string(), - } - } -} - - - -// ============================================= -// === Internal Visualisation Representation === -// ============================================= - -/// Inner representation of a visualisation. -#[derive(Clone,CloneRef,Debug)] -#[allow(missing_docs)] -pub struct Visualization { - content : DomSymbol -} - -impl display::Object for Visualization { - fn display_object(&self) -> &display::object::Instance { - &self.content.display_object() - } -} - -impl Visualization { - /// Update the visualisation with the given data. - // TODO remove dummy functionality and use an actual visualisation - pub fn set_data(&self, data:Data){ - self.content.dom().set_inner_html( - &format!(r#" - - - -"#, data.as_json())); - } -} - -impl From for Visualization { - fn from(symbol:DomSymbol) -> Self { - Visualization { content : symbol } - } -} - - -// ========================= -// === Visualization FRP === -// ========================= - -/// Visualization events. -#[derive(Clone,CloneRef,Debug)] -#[allow(missing_docs)] -pub struct ContainerFrp { - pub network : frp::Network, - pub set_visibility : frp::Source, - pub toggle_visibility : frp::Source, - pub set_visualization : frp::Source>, - pub set_data : frp::Source>, -} - -impl Default for ContainerFrp { - fn default() -> Self { - frp::new_network! { visualization_events - def set_visibility = source(); - def toggle_visibility = source(); - def set_visualization = source(); - def set_data = source(); - }; - let network = visualization_events; - Self {network,set_visibility,set_visualization,toggle_visibility,set_data } - } -} - - - -// ================================ -// === Visualizations Container === -// ================================ - -/// Container that wraps a `Visualization` for rendering and interaction in the GUI. -/// -/// The API to interact with the visualisation is exposed through the `ContainerFrp`. -#[derive(Clone,CloneRef,Debug,Shrinkwrap)] -#[allow(missing_docs)] -pub struct Container { - // The internals are split into two structs: `ContainerData` and `ContainerFrp`. The - // `ContainerData` contains the actual data and logic for the `Container`. The `ContainerFrp` - // contains the FRP api and network. This split is required to avoid creating cycles in the FRP - // network: the FRP network holds `Rc`s to the `ContainerData` and thus must not live in the - // same struct. - - #[shrinkwrap(main_field)] - data : Rc, - pub frp : Rc, -} - -/// Internal data of a `Container`. -#[derive(Debug,Clone)] -#[allow(missing_docs)] -pub struct ContainerData { - logger : Logger, - display_object : display::object::Instance, - size : Cell>, - visualization : RefCell>, -} - -impl ContainerData { - /// Set whether the visualisation should be visible or not. - pub fn set_visibility(&self, visibility:bool) { - if let Some(vis) = self.visualization.borrow().as_ref() { - if visibility { self.add_child(&vis) } else { vis.unset_parent() } - } - } - - /// Indicates whether the visualisation is visible. - pub fn is_visible(&self) -> bool { - self.visualization.borrow().as_ref().map(|t| t.has_parent()).unwrap_or(false) - } - - /// Toggle visibility. - pub fn toggle_visibility(&self) { - self.set_visibility(!self.is_visible()) - } - - /// Update the data in the inner visualisation. - pub fn set_data(&self, data:Data) { - self.visualization.borrow().for_each_ref(|vis| vis.set_data(data)); - } - - /// Set the visualization shown in this container.. - pub fn set_visualisation(&self, visualization:Visualization) { - let size = self.size.get(); - visualization.content.set_size(size); - self.display_object.add_child(&visualization); - self.visualization.replace(Some(visualization)); - self.set_visibility(false); - } -} - -impl display::Object for ContainerData { - fn display_object(&self) -> &display::object::Instance { - &self.display_object - } -} - - -impl Container { - /// Constructor. - pub fn new() -> Self { - let logger = Logger::new("visualization_container"); - let visualization = default(); - let size = Cell::new(Vector2::new(100.0, 100.0)); - let display_object = display::object::Instance::new(&logger); - let data = ContainerData {logger,visualization,size,display_object}; - let data = Rc::new(data); - let frp = default(); - Self {data, frp} . init_frp() - } - - fn init_frp(self) -> Self { - let frp = &self.frp; - let network = &self.frp.network; - - frp::extend! { network - - let container_data = &self.data; - - def _set_visibility = frp.set_visibility.map(f!((container_data)(is_visible) { - container_data.set_visibility(*is_visible); - })); - - def _toggle_visibility = frp.toggle_visibility.map(f!((container_data)(_) { - container_data.toggle_visibility() - })); - - def _set_visualization = frp.set_visualization.map(f!((container_data)(visualisation) { - if let Some(visualisation) = visualisation.as_ref() { - container_data.set_visualisation(visualisation.clone_ref()); - } - })); - - def _set_data = frp.set_data.map(f!((container_data)(data) { - if let Some(data) = data.as_ref() { - container_data.set_data(data.clone_ref()); - } - })); - } - self - } -} - -impl Default for Container { - fn default() -> Self { - Container::new() - } -} - -impl display::Object for Container { - fn display_object(&self) -> &display::object::Instance { - &self.data.display_object - } -} - - - -// ================= -// === Mock Data === -// ================= - -/// Dummy content for testing. -// FIXME[mm] remove this when actual content is available. -pub(crate) fn default_content() -> DomSymbol { - let div = web::create_div(); - div.set_style_or_panic("width","100px"); - div.set_style_or_panic("height","100px"); - - let content = web::create_element("div"); - content.set_inner_html( - r#" - - -"#); - content.set_attribute("width","100%").unwrap(); - content.set_attribute("height","100%").unwrap(); - - div.append_child(&content).unwrap(); - - let r = 102_u8; - let g = 153_u8; - let b = 194_u8; - let color = iformat!("rgb({r},{g},{b})"); - div.set_style_or_panic("background-color",color); - - let symbol = DomSymbol::new(&div); - symbol.dom().set_attribute("id","vis").unwrap(); - symbol.dom().style().set_property("overflow","hidden").unwrap(); - symbol -} +pub use class::*; +pub use data::*; +pub use container::*; +pub use renderer::*; diff --git a/gui/src/rust/lib/graph-editor/src/component/visualization/class.rs b/gui/src/rust/lib/graph-editor/src/component/visualization/class.rs new file mode 100644 index 00000000000..d42f123d6e5 --- /dev/null +++ b/gui/src/rust/lib/graph-editor/src/component/visualization/class.rs @@ -0,0 +1,156 @@ +//! This module defines the `Visualization` struct and related functionality. + +use crate::prelude::*; + +use crate::frp; +use crate::visualization::*; + +use ensogl::display; + + + +// ==================== +// === Helper Types === +// ==================== + +/// Type alias for a string containing enso code. +#[derive(Clone,CloneRef,Debug)] +pub struct EnsoCode { + content: Rc +} + +/// Type alias for a string representing an enso type. +#[derive(Clone,CloneRef,Debug)] +pub struct EnsoType { + content: Rc +} + + + +// ========================= +// === Visualization FRP === +// ========================= + +/// Events that are used by the visualization. +#[derive(Clone,CloneRef,Debug)] +#[allow(missing_docs)] +pub struct Frp { + /// Can be sent to set the data of the visualization. + pub set_data : frp::Source>, + /// Will be emitted if the visualization has new data (e.g., through UI interaction). + /// Data is provides encoded as EnsoCode. + pub on_change : frp::Stream>, + /// Will be emitted if the visualization changes it's preprocessor code. + pub on_preprocess_change : frp::Stream>, + /// Will be emitted if the visualization has been provided with invalid data. + pub on_invalid_data : frp::Stream<()>, + + // Internal sources that feed the public streams. + change : frp::Source>, + preprocess_change : frp::Source>, + invalid_data : frp::Source<()>, + +} + +impl Frp { + fn new(network: &frp::Network) -> Self { + frp::extend! { network + def change = source(); + def preprocess_change = source(); + def invalid_data = source(); + def set_data = source(); + + let on_change = change.clone_ref().into(); + let on_preprocess_change = preprocess_change.clone_ref().into(); + let on_invalid_data = invalid_data.clone_ref().into(); + }; + Self { on_change,on_preprocess_change,set_data,on_invalid_data,change + ,preprocess_change,invalid_data} + } +} + + + +// =========================== +// === Visualization Model === +// =========================== + +/// Internal data of Visualization. +#[derive(Clone,CloneRef,Debug)] +#[allow(missing_docs)] +pub struct State { + pub renderer : Rc, + pub preprocessor : Rc>>, +} + +impl display::Object for State { + fn display_object(&self) -> &display::object::Instance { + &self.renderer.display_object() + } +} + + + +// ===================== +// === Visualization === +// ===================== + +/// A visualization that can be rendered and interacted with. Provides an frp API. +#[derive(Clone,CloneRef,Debug)] +#[allow(missing_docs)] +pub struct Visualization { + pub network : frp::Network, + pub frp : Frp, + state : State +} + +impl display::Object for Visualization { + fn display_object(&self) -> &display::object::Instance { + &self.state.display_object() + } +} + +impl Visualization { + /// Create a new `Visualization` with the given `DataRenderer`. + pub fn new(renderer:T) -> Self { + let preprocessor = default(); + let network = default(); + let frp = Frp::new(&network); + let renderer = Rc::new(renderer); + let internal = State {preprocessor,renderer}; + Visualization{frp, state: internal,network}.init() + } + + fn init(self) -> Self { + let network = &self.network; + let visualization = &self.state; + let frp = &self.frp; + frp::extend! { network + def _set_data = self.frp.set_data.map(f!((frp,visualization)(data) { + if let Some(data) = data { + if visualization.renderer.receive_data(data.clone_ref()).is_err() { + frp.invalid_data.emit(()) + } + } + })); + } + + let renderer_frp = self.state.renderer.frp(); + let renderer_network = &renderer_frp.network; + frp::new_bridge_network! { [network,renderer_network] + def _on_changed = renderer_frp.on_change.map(f!((frp)(data) { + frp.change.emit(data) + })); + def _on_preprocess_change = renderer_frp.on_preprocess_change.map(f!((frp)(data) { + frp.preprocess_change.emit(data.as_ref().map(|code|code.clone_ref())) + })); + } + + self + } + + /// Set the viewport size of the visualization. + pub fn set_size(&self, size:Vector2) { + self.state.renderer.set_size(size) + } +} diff --git a/gui/src/rust/lib/graph-editor/src/component/visualization/container.rs b/gui/src/rust/lib/graph-editor/src/component/visualization/container.rs new file mode 100644 index 00000000000..ea99a41d688 --- /dev/null +++ b/gui/src/rust/lib/graph-editor/src/component/visualization/container.rs @@ -0,0 +1,173 @@ +//! This module defines the `Container` struct and related functionality. + +use crate::prelude::*; + +use crate::frp; +use crate::visualization::*; + +use ensogl::display::traits::*; +use ensogl::display; + + + +// =========== +// === FRP === +// =========== + +/// Event system of the `Container`. +#[derive(Clone,CloneRef,Debug)] +#[allow(missing_docs)] +pub struct ContainerFrp { + pub network : frp::Network, + pub set_visibility : frp::Source, + pub toggle_visibility : frp::Source, + pub set_visualization : frp::Source>, + pub set_data : frp::Source>, +} + +impl Default for ContainerFrp { + fn default() -> Self { + frp::new_network! { visualization_events + def set_visibility = source(); + def toggle_visibility = source(); + def set_visualization = source(); + def set_data = source(); + }; + let network = visualization_events; + Self {network,set_visibility,set_visualization,toggle_visibility,set_data } + } +} + + + +// ================================ +// === Visualizations Container === +// ================================ + +/// Container that wraps a `Visualization` for rendering and interaction in the GUI. +/// +/// The API to interact with the visualisation is exposed through the `ContainerFrp`. +#[derive(Clone,CloneRef,Debug,Shrinkwrap)] +#[allow(missing_docs)] +pub struct Container { + // The internals are split into two structs: `ContainerData` and `ContainerFrp`. The + // `ContainerData` contains the actual data and logic for the `Container`. The `ContainerFrp` + // contains the FRP api and network. This split is required to avoid creating cycles in the FRP + // network: the FRP network holds `Rc`s to the `ContainerData` and thus must not live in the + // same struct. + + #[shrinkwrap(main_field)] + data : Rc, + pub frp : ContainerFrp, +} + +/// Internal data of a `Container`. +#[derive(Debug,Clone)] +#[allow(missing_docs)] +pub struct ContainerData { + logger : Logger, + display_object : display::object::Instance, + size : Cell>, + visualization : RefCell>, +} + +impl ContainerData { + /// Set whether the visualisation should be visible or not. + pub fn set_visibility(&self, is_visible:bool) { + if let Some(vis) = self.visualization.borrow().as_ref() { + if is_visible { + vis.display_object().set_parent(&self.display_object); + } else { + vis.display_object().unset_parent(); + } + } + } + + /// Indicates whether the visualisation is visible. + fn is_visible(&self) -> bool { + if let Some(vis) = self.visualization.borrow().as_ref() { + vis.has_parent() + } else { + false + } + } + + /// Toggle visibility. + fn toggle_visibility(&self) { + self.set_visibility(!self.is_visible()) + } + + /// Update the content properties with the values from the `ContainerData`. + /// + /// Needs to called when a visualisation has been set. + fn init_visualisation_properties(&self) { + let size = self.size.get(); + if let Some(vis) = self.visualization.borrow().as_ref() { + vis.set_size(size); + }; + self.set_visibility(true); + } + + /// Set the visualization shown in this container.. + fn set_visualisation(&self, visualization:Visualization) { + visualization.display_object().set_parent(&self.display_object); + self.visualization.replace(Some(visualization)); + self.init_visualisation_properties(); + } +} + +impl Container { + /// Constructor. + pub fn new() -> Self { + let logger = Logger::new("visualization"); + let visualization = default(); + let size = Cell::new(Vector2::new(100.0, 100.0)); + let display_object = display::object::Instance::new(&logger); + let data = ContainerData {logger,visualization,size,display_object}; + let data = Rc::new(data); + let frp = default(); + Self {data,frp} . init_frp() + } + + fn init_frp(self) -> Self { + let frp = &self.frp; + let network = &self.frp.network; + + frp::extend! { network + + let container_data = &self.data; + + def _f_hide = frp.set_visibility.map(f!((container_data)(is_visible) { + container_data.set_visibility(*is_visible); + })); + + def _f_toggle = frp.toggle_visibility.map(f!((container_data)(_) { + container_data.toggle_visibility() + })); + + def _f_set_vis = frp.set_visualization.map(f!((container_data)(visualisation) { + if let Some(visualisation) = visualisation.as_ref() { + container_data.set_visualisation(visualisation.clone()); + } + })); + + def _f_set_data = frp.set_data.map(f!((container_data)(data) { + container_data.visualization.borrow() + .for_each_ref(|vis| vis.frp.set_data.emit(data)); + })); + } + self + } +} + +impl Default for Container { + fn default() -> Self { + Container::new() + } +} + +impl display::Object for Container { + fn display_object(&self) -> &display::object::Instance { + &self.data.display_object + } +} diff --git a/gui/src/rust/lib/graph-editor/src/component/visualization/data.rs b/gui/src/rust/lib/graph-editor/src/component/visualization/data.rs new file mode 100644 index 00000000000..38f7843d306 --- /dev/null +++ b/gui/src/rust/lib/graph-editor/src/component/visualization/data.rs @@ -0,0 +1,108 @@ +//! This module defines the `Data` struct and related functionality. + +use crate::prelude::*; + +use crate::component::visualization::EnsoType; + +use serde::Deserialize; + + + +// ====================================== +// === Wrapper for Visualisation Data === +// ======================================= + +/// Type indicator +pub type DataType = EnsoType; + +/// Wrapper for data that can be consumed by a visualisation. +/// TODO[mm] consider static versus dynamic typing for visualizations and data! +#[derive(Clone,CloneRef,Debug)] +#[allow(missing_docs)] +pub enum Data { + JSON { content : Rc }, + // TODO replace with actual binary data stream. + Binary { content : Rc }, +} + +impl Data { + /// Returns the data as as JSON. If the data cannot be returned as JSON, it will return a + /// `DataError` instead. + pub fn as_json(&self) -> Result, DataError> { + match &self { + Data::JSON { content } => Ok(Rc::clone(content)), + _ => { Err(DataError::InvalidDataType{}) }, + } + } + + /// Returns the wrapped data in Rust format. If the data cannot be returned as rust datatype, a + /// `DataError` is returned instead. + pub fn as_binary(&self) -> Result, DataError> + where for<'de> T:Deserialize<'de> + 'static { + match &self { + Data::JSON { content } => { + // We try to deserialize here. Just in case it works. + // This is useful for simple data types where we don't want to care to much about + // representation, e.g., a list of numbers. + let value : serde_json::Value = content.as_ref().clone(); + if let Ok(result) = serde_json::from_value(value) { + Ok(Rc::new(result)) + } else { + Err(DataError::InvalidDataType) + } + }, + Data::Binary { content } => { Rc::clone(content).downcast() + .or(Err(DataError::InvalidDataType))}, + } + } +} + + + +// ============== +// === Errors === +// ============== + +/// Indicates a problem with the provided data. That is, the data has the wrong format, or maybe +/// violates some other assumption of the visualization. +// TODO[mm] add more information to errors once typing is defined. +#[derive(Copy,Clone,Debug)] +pub enum DataError { + /// Indicates that that the provided data type does not match the expected data type. + InvalidDataType, + /// The data caused an error in the computation of the visualisation. + InternalComputationError, +} + + + +// ============================= +// === Sample Data Generator === +// ============================= +// TODO this will go away once we have real data + +#[derive(Clone,CloneRef,Debug,Default)] +pub(crate) struct MockDataGenerator3D { + counter: Rc> +} + +impl MockDataGenerator3D { + + pub fn generate_data(&self) -> Vec> { + + let current_value = self.counter.get(); + self.counter.set(current_value + 0.1); + + let delta1 = current_value.sin() * 10.0; + let delta2 = current_value.cos() * 10.0; + + vec![ + Vector3::new(25.0, 75.0, 25.0 + delta1), + Vector3::new(25.0, 25.0, 25.0 + delta2), + Vector3::new(75.0 - 12.5, 75.0 + delta1, 5.0 ), + Vector3::new(75.0 + 12.5, 75.0 + delta2, 15.0 ), + Vector3::new(75.0 - 12.5 + delta1, 25.0 + delta2, 5.0 ), + Vector3::new(75.0 + 12.5 + delta2, 25.0 + delta1, 15.0 ), + ] + } +} diff --git a/gui/src/rust/lib/graph-editor/src/component/visualization/renderer.rs b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer.rs new file mode 100644 index 00000000000..51523eda62a --- /dev/null +++ b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer.rs @@ -0,0 +1,9 @@ +//! This module defines the `DataRenderer` trait, related functionality and examples of how to use +//! the `DataRenderer`. + +mod class; +mod js; +pub mod example; + +pub use class::*; +pub use js::*; diff --git a/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/class.rs b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/class.rs new file mode 100644 index 00000000000..3664effab7b --- /dev/null +++ b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/class.rs @@ -0,0 +1,79 @@ +//! This module defines the `DataRenderer` trait and related functionality. + +use crate::prelude::*; +use crate::visualization::*; + +use crate::frp; + +use ensogl::display; + + + +// =========== +// === FRP === +// =========== + +/// FRP api of a `DataRenderer`. +#[derive(Clone,Debug)] +#[allow(missing_docs)] +pub struct DataRendererFrp { + pub network : frp::Network, + /// This is emitted if the state of the renderer has been changed by UI interaction. + /// It contains the output data of this visualisation if there is some. + pub on_change : frp::Stream>, + /// Will be emitted if the visualization changes it's preprocessor. Transmits the new + /// preprocessor code. + pub on_preprocess_change : frp::Stream>, + // Internal sources that feed the public streams. + change : frp::Source>, + preprocess_change : frp::Source>, +} + +impl Default for DataRendererFrp { + fn default() -> Self { + frp::new_network! { renderer_events + def change = source(); + def preprocess_change = source(); + + let on_change = change.clone_ref().into(); + let on_preprocess_change = preprocess_change.clone_ref().into(); + }; + let network = renderer_events; + Self {network,on_change,on_preprocess_change,change,preprocess_change} + } +} + + + +// ==================== +// === DataRenderer === +// ==================== + +/// At the core of the visualization system sits a `DataRenderer`. The DataRenderer is in charge of +/// producing a `display::Object` that will be shown in the scene. It will create FRP events to +/// indicate updates to its output data (e.g., through user interaction). +/// +/// A DataRenderer can indicate what kind of data it can use to create a visualisation through the +/// `valid_input_types` method. This serves as a hint, it will also reject invalid input in the +/// `set_data` method with a `DataError`. The owner of the `DataRenderer` is in charge of producing +/// UI feedback to indicate a problem with the data. +pub trait DataRenderer: display::Object + Debug { + /// Indicate which `DataType`s can be rendered by this visualization. + fn valid_input_types(&self) -> Vec { + // TODO this will need to be implemented by each Renderer once we get to the registry. + unimplemented!() + } + /// Receive the data that should be rendered. If the data is valid, it will return the data as + /// processed by this `DataRenderer`, if the data is of an invalid data type, it violates other + /// assumptions of this `DataRenderer`, a `DataError` is returned. + fn receive_data(&self, data:Data) -> Result<(), DataError>; + /// Set the size of viewport of the visualization. The visualisation must not render outside of + /// this viewport. + fn set_size(&self, size:Vector2); + + /// Return a ref to the internal FRP network. This replaces a potential callback mechanism. + /// + /// Note: the presence of this functions imposes the requirement that a `DataRendererFrp` is + /// owned by whoever implements this trait. + fn frp(&self) -> &DataRendererFrp; +} diff --git a/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/example.rs b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/example.rs new file mode 100644 index 00000000000..81d209f0d57 --- /dev/null +++ b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/example.rs @@ -0,0 +1,5 @@ +//! The `example` modules contains some examples of visualisations. +//! TODO further describe the examples, how they work and how to extend them. + +pub mod native; +pub mod js; diff --git a/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/example/js.rs b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/example/js.rs new file mode 100644 index 00000000000..d5fa10d0946 --- /dev/null +++ b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/example/js.rs @@ -0,0 +1,139 @@ +//! Example of the visualisation JS wrapper API usage +// TODO remove once we have proper visualizations or replace with a nice d3 example. +// These implementations are neither efficient nor pretty, but get the idea across. + +use crate::component::visualization::JsRenderer; + +/// Returns a simple bubble chart implemented in vanilla JS. uses single functions to implement the +/// visualization. +pub fn function_sample_js_bubble_chart() -> JsRenderer { + let fn_set_data = r#"{ + const xmlns = "http://www.w3.org/2000/svg"; + const root = arguments[0]; + while (root.firstChild) { + root.removeChild(root.lastChild); + } + + const svgElem = document.createElementNS(xmlns, "svg"); + svgElem.setAttributeNS(null, "id" , "vis-svg"); + svgElem.setAttributeNS(null, "viewBox", "0 0 " + 100 + " " + 100); + svgElem.setAttributeNS(null, "width" , 100); + svgElem.setAttributeNS(null, "height" , 100); + root.appendChild(svgElem); + + const data = arguments[1]; + data.forEach(data => { + const bubble = document.createElementNS(xmlns,"circle"); + bubble.setAttributeNS(null,"stroke", "black"); + bubble.setAttributeNS(null,"fill" , "red"); + bubble.setAttributeNS(null,"r" , data[2]); + bubble.setAttributeNS(null,"cx" , data[0]); + bubble.setAttributeNS(null,"cy" , data[1]); + svgElem.appendChild(bubble); + }); + } + "#; + + let fn_set_size = r#"{ + const root = arguments[0]; + const width = arguments[1][0]; + const height = arguments[1][1]; + const svgElem = root.firstChild; + svgElem.setAttributeNS(null, "viewBox", "0 0 " + width + " " + height); + svgElem.setAttributeNS(null, "width" , width); + svgElem.setAttributeNS(null, "height" , height); + }"#; + JsRenderer::from_functions(fn_set_data, fn_set_size) +} + +/// Returns a simple bubble chart implemented in vanilla JS. Uses an object to implement the +/// visualization logic. +pub fn object_sample_js_bubble_chart() -> JsRenderer { + let fn_prototype = r#" + (() => { + class BubbleVisualisation { + onDataReceived(root, data) { + const xmlns = "http://www.w3.org/2000/svg"; + while (root.firstChild) { + root.removeChild(root.lastChild); + } + + const svgElem = document.createElementNS(xmlns, "svg"); + svgElem.setAttributeNS(null, "id" , "vis-svg"); + svgElem.setAttributeNS(null, "viewBox", "0 0 " + 100 + " " + 100); + svgElem.setAttributeNS(null, "width" , 100); + svgElem.setAttributeNS(null, "height" , 100); + root.appendChild(svgElem); + + data.forEach(data => { + const bubble = document.createElementNS(xmlns,"circle"); + bubble.setAttributeNS(null,"stroke", "black"); + bubble.setAttributeNS(null,"fill" , "red"); + bubble.setAttributeNS(null,"r" , data[2]); + bubble.setAttributeNS(null,"cx" , data[0]); + bubble.setAttributeNS(null,"cy" , data[1]); + svgElem.appendChild(bubble); + }); + } + + setSize(root, size) { + const width = size[0]; + const height = size[1]; + const svgElem = root.firstChild; + svgElem.setAttributeNS(null, "viewBox", "0 0 " + width + " " + height); + svgElem.setAttributeNS(null, "width" , width); + svgElem.setAttributeNS(null, "height" , height); + } + } + + return new BubbleVisualisation(); + })() + "#; + JsRenderer::from_object(fn_prototype).unwrap() +} + + + +/// Returns a simple bubble chart implemented in vanilla JS. uses single functions to implement the +/// visualization. +pub fn constructor_sample_js_bubble_chart() -> JsRenderer { + let fn_constructor = r#" + class BubbleVisualisation { + onDataReceived(root, data) { + const xmlns = "http://www.w3.org/2000/svg"; + while (root.firstChild) { + root.removeChild(root.lastChild); + } + + const svgElem = document.createElementNS(xmlns, "svg"); + svgElem.setAttributeNS(null, "id" , "vis-svg"); + svgElem.setAttributeNS(null, "viewBox", "0 0 " + 100 + " " + 100); + svgElem.setAttributeNS(null, "width" , 100); + svgElem.setAttributeNS(null, "height" , 100); + root.appendChild(svgElem); + + data.forEach(data => { + const bubble = document.createElementNS(xmlns,"circle"); + bubble.setAttributeNS(null,"stroke", "black"); + bubble.setAttributeNS(null,"fill" , "red"); + bubble.setAttributeNS(null,"r" , data[2]); + bubble.setAttributeNS(null,"cx" , data[0]); + bubble.setAttributeNS(null,"cy" , data[1]); + svgElem.appendChild(bubble); + }); + } + + setSize(root, size) { + const width = size[0]; + const height = size[1]; + const svgElem = root.firstChild; + svgElem.setAttributeNS(null, "viewBox", "0 0 " + width + " " + height); + svgElem.setAttributeNS(null, "width" , width); + svgElem.setAttributeNS(null, "height" , height); + } + } + + return new BubbleVisualisation(); + "#; + JsRenderer::from_constructor(fn_constructor).unwrap() +} diff --git a/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/example/native.rs b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/example/native.rs new file mode 100644 index 00000000000..7926aae00c5 --- /dev/null +++ b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/example/native.rs @@ -0,0 +1,103 @@ +//! Examples of defining visualisation in Rust using web_sys or ensogl. + +use crate::prelude::*; + +use crate::component::visualization::*; + +use ensogl::data::color::Rgba; +use ensogl::display::layout::alignment; +use ensogl::display::scene::Scene; +use ensogl::display; +use ensogl::gui::component; + + + +// ========================== +// === Native BubbleChart === +// ========================== + +/// Bubble shape definition. +pub mod shape { + use super::*; + use ensogl::display::shape::*; + use ensogl::display::scene::Scene; + use ensogl::display::Sprite; + use ensogl::display::Buffer; + use ensogl::display::Attribute; + + ensogl::define_shape_system! { + (position:Vector2,radius:f32) { + let node = Circle(radius); + let node = node.fill(Rgba::new(0.17,0.46,0.15,1.0)); + let node = node.translate(("input_position.x","input_position.y")); + node.into() + } + } +} + +/// Sample implementation of a Bubble Chart using the ensogl shape system. +#[derive(Debug)] +#[allow(missing_docs)] +pub struct BubbleChart { + pub display_object : display::object::Instance, + pub scene : Scene, + frp : DataRendererFrp, + views : RefCell>>, + logger : Logger, + size : Cell>, +} + +#[allow(missing_docs)] +impl BubbleChart { + pub fn new(scene:&Scene) -> Self { + let logger = Logger::new("bubble"); + let display_object = display::object::Instance::new(&logger); + let views = RefCell::new(vec![]); + let frp = default(); + let size = Cell::new(Vector2::zero()); + let scene = scene.clone_ref(); + + BubbleChart { display_object,views,logger,frp,size,scene } + } +} + +impl DataRenderer for BubbleChart { + + fn receive_data(&self, data:Data) -> Result<(),DataError> { + let data_inner: Rc>> = data.as_binary()?; + + // Avoid re-creating views, if we have already created some before. + let mut views = self.views.borrow_mut(); + views.resize_with(data_inner.len(),|| component::ShapeView::new(&self.logger,&self.scene)); + + // TODO[mm] this is somewhat inefficient, as the canvas for each bubble is too large. + // But this ensures that we can get a cropped view area and avoids an issue with the data + // and position not matching up. + views.iter().zip(data_inner.iter()).for_each(|(view,item)| { + + let shape_system = self.scene.shapes.shape_system(PhantomData::); + shape_system.shape_system.set_alignment( + alignment::HorizontalAlignment::Left, alignment::VerticalAlignment::Bottom); + + view.display_object.set_parent(&self.display_object); + view.shape.sprite.size().set(self.size.get()); + view.shape.radius.set(item.z); + view.shape.position.set(Vector2::new(item.x,item.y)); + }); + Ok(()) + } + + fn set_size(&self, size:Vector2) { + self.size.set(size); + } + + fn frp(&self) -> &DataRendererFrp { + &self.frp + } +} + +impl display::Object for BubbleChart { + fn display_object(&self) -> &display::object::Instance { + &self.display_object.display_object() + } +} diff --git a/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/js.rs b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/js.rs new file mode 100644 index 00000000000..72d960cf08f --- /dev/null +++ b/gui/src/rust/lib/graph-editor/src/component/visualization/renderer/js.rs @@ -0,0 +1,228 @@ +//! This module contains functionality that allows the usage of JavaScript to define visualizations. +//! +//! The `JsRenderer` defines a generic way to wrap JS function calls and allow interaction with +//! JS code and the visualisation system. +//! +//! There are at the moment three way to generate a `JsRenderer`: +//! 1. `JsRenderer::from_functions` where the bodies of the required functions are provided as +//! source code. +//! 2. `JsRenderer::from_object` where the a piece of JS code is provided that must evaluate to an +//! object that has the required methods that will be called at runtime. +//! 3. `JsRenderer::from_constructor`where the body of a constructor function needs to be +//! provided. The returned object needs to fulfill the same specification as in (2). +//! +//! Right now the only functions required on the wrapped object are +//! * `onDataReceived(root, data)`, which receives the html element that the visualisation should be +//! appended on, as well as the data that should be rendered. +//! * `setSize(root, size)`, which receives the node that the visualisation should be appended on, +//! as well as the intended size. +//! +//! TODO: refine spec and add functions as needed, e.g., init, callback hooks or type indicators. + +use crate::prelude::*; + +use crate::component::visualization::Data; +use crate::component::visualization::DataError; +use crate::component::visualization::DataRenderer; +use crate::component::visualization::DataRendererFrp; + +use ensogl::display::DomScene; +use ensogl::display::DomSymbol; +use ensogl::display; +use ensogl::system::web::JsValue; +use ensogl::system::web; +use js_sys; + + + +// ============== +// === Errors === +// ============== + +/// Errors that can occur when transforming JS source to a visualization. +#[derive(Clone,Debug)] +#[allow(missing_docs)] +pub enum JsVisualisationError { + NotAnObject { inner:JsValue }, + NotAFunction { inner:JsValue }, + /// An unknown error occurred on the JS side. Inspect the content for more information. . + Unknown { inner:JsValue } +} + +impl From for JsVisualisationError { + fn from(value:JsValue) -> Self { + // TODO add differentiation if we encounter specific errors and return new variants. + JsVisualisationError::Unknown {inner:value} + } +} + + + +// ================== +// === JsRenderer === +// ================== + +/// `JsVisualizationGeneric` allows the use of arbitrary javascript to create visualisations. It +/// takes function definitions as strings and proved those functions with data. +#[derive(Clone,Debug)] +#[allow(missing_docs)] +pub struct JsRenderer { + pub root_node : DomSymbol, + pub logger : Logger, + on_data_received : js_sys::Function, + set_size : js_sys::Function, + frp : DataRendererFrp, +} + +impl JsRenderer { + /// Constructor from single functions. + /// + /// `fn_set_data` and `fn_set_size` need to be strings that contain valid JavaScript code. This + /// code will be executed as the function body of the respective functions. + /// + /// `fn_set_data` will be called with two arguments: the first argument (`root) )will be the + /// root node that the visualisation should use to build its output, the second argument + /// (`data`)will be the data that it should visualise. + /// + /// `fn_set_size` will be called with a tuple of floating point values indicating the desired + /// width and height. This can be used by the visualisation to ensure proper scaling. + /// + /// For a full example see + /// `crate::component::visualization::renderer::example::function_sample_js_bubble_chart` + pub fn from_functions(fn_set_data:&str, fn_set_size:&str) -> Self { + let set_data = js_sys::Function::new_no_args(fn_set_data); + let set_size = js_sys::Function::new_no_args(fn_set_size); + + let logger = Logger::new("JsRendererGeneric"); + let frp = default(); + let div = web::create_div(); + let root_node = DomSymbol::new(&div); + root_node.dom().set_attribute("id","vis").unwrap(); + + JsRenderer { on_data_received: set_data,set_size,root_node,frp,logger } + } + + /// Internal helper that tries to convert a JS object into a `JsRenderer`. + fn from_object_js(object:js_sys::Object) -> Result { + let set_data = js_sys::Reflect::get(&object,&"onDataReceived".into())?; + let set_size = js_sys::Reflect::get(&object,&"setSize".into())?; + if !set_data.is_function() { + return Err(JsVisualisationError::NotAFunction { inner:set_data }) + } + if !set_size.is_function() { + return Err(JsVisualisationError::NotAFunction { inner:set_size }) + } + let set_data:js_sys::Function = set_data.into(); + let set_size:js_sys::Function = set_size.into(); + + let logger = Logger::new("JsRenderer"); + let frp = default(); + let div = web::create_div(); + let root_node = DomSymbol::new(&div); + root_node.dom().set_attribute("id","vis")?; + + Ok(JsRenderer { on_data_received: set_data,set_size,root_node,frp,logger }) + } + + /// Constructor from a source that evaluates to an object with specific methods. + /// + /// Example: + /// -------- + /// + /// ```no_run + /// use graph_editor::component::visualization::JsRenderer; + /// + /// let renderer = JsRenderer::from_object("function() { + /// class Visualization { + /// onDataReceived(root, data) {}; + /// setSize(root, size) {}; + /// } + /// return new Visualisation(); + /// }()").unwrap(); + /// + /// ``` + /// + /// For a full example see + /// `crate::component::visualization::renderer::example::object_sample_js_bubble_chart` + pub fn from_object(source: &str) -> Result { + let object = js_sys::eval(source)?; + if !object.is_object() { + return Err(JsVisualisationError::NotAnObject { inner:object } ) + } + Self::from_object_js(object.into()) + } + + /// Constructor from function body that returns a object with specific functions. + /// + /// Example: + /// -------- + /// + /// ```no_run + /// use graph_editor::component::visualization::JsRenderer; + /// + /// let renderer = JsRenderer::from_constructor(" + /// class Visualization { + /// onDataReceived(root, data) {}; + /// setSize(root, size) {}; + /// } + /// return new Visualisation(); + /// ").unwrap(); + /// + /// ``` + /// For a full example see + /// `crate::component::visualization::renderer::example::constructor_sample_js_bubble_chart` + pub fn from_constructor(source:&str) -> Result { + let context = JsValue::NULL; + let constructor = js_sys::Function::new_no_args(source); + let object = constructor.call0(&context)?; + if !object.is_object() { + return Err(JsVisualisationError::NotAnObject { inner:object } ) + } + Self::from_object_js(object.into()) + } + + /// Hooks the root node into the given scene. + /// + /// MUST be called to make this visualisation visible. + // TODO[mm] find a better mechanism to ensure this. Probably through the registry later on. + pub fn set_dom_layer(&self, scene:&DomScene) { + scene.manage(&self.root_node); + } +} + +impl DataRenderer for JsRenderer { + + fn receive_data(&self, data:Data) -> Result<(),DataError> { + let context = JsValue::NULL; + let data_json = data.as_json()?; + let data_js = match JsValue::from_serde(&data_json) { + Ok(value) => value, + Err(_) => return Err(DataError::InvalidDataType), + }; + if let Err(error) = self.on_data_received.call2(&context, &self.root_node.dom(), &data_js) { + self.logger.warning( + || format!("Failed to set data in {:?} with error: {:?}",self,error)); + return Err(DataError::InternalComputationError) + } + Ok(()) + } + + fn set_size(&self, size:Vector2) { + let context = JsValue::NULL; + let data_json = JsValue::from_serde(&size).unwrap(); + if let Err(error) = self.set_size.call2(&context, &self.root_node.dom(), &data_json) { + self.logger.warning( + || format!("Failed to set size in {:?} with error: {:?}", self, error)); + } + } + + fn frp(&self) -> &DataRendererFrp { + &self.frp + } +} + +impl display::Object for JsRenderer { + fn display_object(&self) -> &display::object::Instance { + &self.root_node.display_object() + } +} diff --git a/gui/src/rust/lib/graph-editor/src/lib.rs b/gui/src/rust/lib/graph-editor/src/lib.rs index 223d84aaa5d..826147d2450 100644 --- a/gui/src/rust/lib/graph-editor/src/lib.rs +++ b/gui/src/rust/lib/graph-editor/src/lib.rs @@ -56,9 +56,9 @@ use ensogl::display::Scene; use crate::component::node::port::Expression; use crate::component::visualization::Visualization; use crate::component::visualization; - -use serde_json::json; - +use crate::component::visualization::example::js::constructor_sample_js_bubble_chart; +use crate::component::visualization::MockDataGenerator3D; +use crate::component::visualization::example::native; // ===================== @@ -241,20 +241,24 @@ ensogl::def_command_api! { Commands toggle_visualization_visibility, /// Set the data for the selected nodes. // TODO only has dummy functionality at the moment. debug_set_data_for_selected_node, + /// Cycle the visualization for the selected nodes. TODO only has dummy functionality at the moment. + debug_cycle_visualisation_for_selected_node, } impl Commands { pub fn new(network:&frp::Network) -> Self { frp::extend! { network - def add_node = source(); - def add_node_at_cursor = source(); - def remove_selected_nodes = source(); - def remove_all_nodes = source(); - def toggle_visualization_visibility = source(); - def debug_set_data_for_selected_node = source(); + def add_node = source(); + def add_node_at_cursor = source(); + def remove_selected_nodes = source(); + def remove_all_nodes = source(); + def toggle_visualization_visibility = source(); + def debug_set_data_for_selected_node = source(); + def debug_cycle_visualisation_for_selected_node = source(); } Self {add_node,add_node_at_cursor,remove_selected_nodes,remove_all_nodes - ,toggle_visualization_visibility,debug_set_data_for_selected_node} + ,toggle_visualization_visibility,debug_set_data_for_selected_node + ,debug_cycle_visualisation_for_selected_node} } } @@ -281,6 +285,8 @@ pub struct FrpInputs { pub set_node_position : frp::Source<(NodeId,Position)>, pub set_visualization_data : frp::Source, pub translate_selected_nodes : frp::Source, + pub cycle_visualization : frp::Source, + pub set_visualization : frp::Source<(NodeId,Option)>, } impl FrpInputs { @@ -299,12 +305,14 @@ impl FrpInputs { def set_node_position = source(); def set_visualization_data = source(); def translate_selected_nodes = source(); + def cycle_visualization = source(); + def set_visualization = source(); } let commands = Commands::new(&network); Self {commands,remove_edge,press_node_port,set_visualization_data ,connect_detached_edges_to_node,connect_edge_source,connect_edge_target ,set_node_position,select_node,translate_selected_nodes,set_node_expression - ,connect_nodes,deselect_all_nodes} + ,connect_nodes,deselect_all_nodes,cycle_visualization,set_visualization} } } @@ -721,14 +729,11 @@ impl GraphEditorModelWithNetwork { })); } - - - - let dummy_content = visualization::default_content(); + let chart = constructor_sample_js_bubble_chart(); let dom_layer = model.scene.dom.layers.front.clone_ref(); - dom_layer.manage(&dummy_content); + chart.set_dom_layer(&dom_layer); - let vis:Visualization = dummy_content.into(); + let vis = Visualization::new(chart); node.view.frp.set_visualization.emit(Some(vis)); @@ -1017,6 +1022,7 @@ impl application::shortcut::DefaultShortcutProvider for GraphEditor { , Self::self_shortcut(&[Key::Backspace] , "remove_selected_nodes") , Self::self_shortcut(&[Key::Character(" ".into())] , "toggle_visualization_visibility") , Self::self_shortcut(&[Key::Character("d".into())] , "debug_set_data_for_selected_node") + , Self::self_shortcut(&[Key::Character("f".into())] , "debug_cycle_visualisation_for_selected_node") ] } } @@ -1200,31 +1206,58 @@ impl application::View for GraphEditor { }) })); + // === Vis Cycling === + def _cycle_vis= inputs.debug_cycle_visualisation_for_selected_node.map(f!((inputs,nodes)(_) { + nodes.selected.for_each(|node| inputs.cycle_visualization.emit(node)); + })); + // === Vis Set === + def _update_vis_data = inputs.set_visualization.map(f!((nodes)((node_id,vis)) { + if let Some(node) = nodes.get_cloned_ref(node_id) { + node.view.visualization_container.frp.set_visualization.emit(vis) + } + })); // === Vis Update Data === // TODO remove this once real data is available. - let dummy_counter = Rc::new(Cell::new(1.0_f32)); - def _update_vis_data = inputs.debug_set_data_for_selected_node.map(f!((nodes)(_) { - let dc = dummy_counter.get(); - dummy_counter.set(dc + 0.1); - let content = Rc::new(json!(format!("{}", 20.0 + 10.0 * dummy_counter.get().sin()))); - let dummy_data = Some(visualization::Data::JSON { content }); + let dummy_switch = Rc::new(Cell::new(false)); + let sample_data_generator = MockDataGenerator3D::default(); + def _set_dumy_data = inputs.debug_set_data_for_selected_node.map(f!((nodes)(_) { nodes.selected.for_each(|node_id| { - if let Some(node) = nodes.get_cloned_ref(node_id) { - node.view.visualization_container.frp.set_data.emit(&dummy_data); + let data = Rc::new(sample_data_generator.generate_data()); + let content = Rc::new(serde_json::to_value(data).unwrap()); + let data = visualization::Data::JSON{ content }; + if let Some(node) = nodes.get_cloned(node_id) { + node.view.visualization_container.frp.set_data.emit(Some(data)); } }) })); + def _set_dumy_data = inputs.cycle_visualization.map(f!((scene,nodes)(node_id) { + // TODO remove dummy cycling once we have the visualization registry. + let dc = dummy_switch.get(); + dummy_switch.set(!dc); + let vis = if dc { + Visualization::new(native::BubbleChart::new(&scene)) + } else { + let chart = constructor_sample_js_bubble_chart(); + let dom_layer = scene.dom.layers.front.clone_ref(); + chart.set_dom_layer(&dom_layer); + Visualization::new(chart) + }; + if let Some(node) = nodes.get_cloned_ref(node_id) { + node.view.visualization_container.frp.set_visualization.emit(Some(vis)); + } + })); + // === Toggle Visualization Visibility === def _toggle_selected = inputs.toggle_visualization_visibility.map(f!((nodes)(_) { nodes.selected.for_each(|node_id| { if let Some(node) = nodes.get_cloned_ref(node_id) { - node.view.visualization_container.toggle_visibility(); + node.view.visualization_container.frp.toggle_visibility.emit(()); } }); }));