From 567ddd701c0e631ccf1a2ad2bb81a65c3deb5dce Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Wed, 15 Dec 2021 11:40:14 +0100 Subject: [PATCH] Developers should find the ide-gui controller codebase logical and easy to work with. (#3188) --- Cargo.lock | 7 + app/gui/config/src/lib.rs | 1 + app/gui/src/controller/graph.rs | 5 + app/gui/src/ide.rs | 23 +- app/gui/src/lib.rs | 2 + app/gui/src/presenter.rs | 148 +++++ app/gui/src/presenter/graph.rs | 401 ++++++++++++ app/gui/src/presenter/graph/state.rs | 756 +++++++++++++++++++++++ app/gui/src/presenter/project.rs | 64 ++ app/gui/view/Cargo.toml | 1 + app/gui/view/graph-editor/src/lib.rs | 2 +- app/ide-desktop/lib/content/src/index.ts | 4 + lib/rust/frp/src/nodes.rs | 52 ++ 13 files changed, 1461 insertions(+), 5 deletions(-) create mode 100644 app/gui/src/presenter.rs create mode 100644 app/gui/src/presenter/graph.rs create mode 100644 app/gui/src/presenter/graph/state.rs create mode 100644 app/gui/src/presenter/project.rs diff --git a/Cargo.lock b/Cargo.lock index 04628ca87e2..5b0c16ba9d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2059,6 +2059,7 @@ dependencies = [ "ensogl-text-msdf-sys", "ide-view-graph-editor", "js-sys", + "multi-map", "nalgebra 0.26.2", "ordered-float 2.8.0", "parser", @@ -2524,6 +2525,12 @@ dependencies = [ "syn", ] +[[package]] +name = "multi-map" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba551d6d795f74a01767577ea8339560bf0a65354e0417b7e915ed608443d46" + [[package]] name = "nalgebra" version = "0.21.1" diff --git a/app/gui/config/src/lib.rs b/app/gui/config/src/lib.rs index 9d93aea797a..243b00281cb 100644 --- a/app/gui/config/src/lib.rs +++ b/app/gui/config/src/lib.rs @@ -56,5 +56,6 @@ ensogl::read_args! { authentication_enabled : bool, email : String, application_config_url : String, + rust_new_presentation_layer : bool, } } diff --git a/app/gui/src/controller/graph.rs b/app/gui/src/controller/graph.rs index bc66f11b0c0..47bf67fc8e1 100644 --- a/app/gui/src/controller/graph.rs +++ b/app/gui/src/controller/graph.rs @@ -93,6 +93,11 @@ pub struct Node { } impl Node { + /// Get the node's id. + pub fn id(&self) -> double_representation::node::Id { + self.main_line.id() + } + /// Get the node's position. pub fn position(&self) -> Option { self.metadata.as_ref().and_then(|m| m.position) diff --git a/app/gui/src/ide.rs b/app/gui/src/ide.rs index a78789514ce..39bbeb68f07 100644 --- a/app/gui/src/ide.rs +++ b/app/gui/src/ide.rs @@ -2,10 +2,12 @@ pub mod initializer; pub mod integration; +pub use initializer::Initializer; + use crate::prelude::*; use crate::controller::project::INITIAL_MODULE_NAME; -use crate::ide::integration::Integration; +use crate::presenter::Presenter; use analytics::AnonymousData; use enso_frp as frp; @@ -13,8 +15,6 @@ use ensogl::application::Application; use ensogl::system::web::sleep; use std::time::Duration; -pub use initializer::Initializer; - // ================= @@ -33,6 +33,17 @@ const ALIVE_LOG_INTERVAL_SEC: u64 = 60; // === Ide === // =========== +/// One of the integration implementations. +/// +/// The new, refactored integration is called "Presenter", but it is not yet fully implemented. +/// To test it, run IDE with `--rust-new-presentation-layer` option. By default, the old integration +/// is used. +#[derive(Debug)] +enum Integration { + Old(integration::Integration), + New(Presenter), +} + /// The main Ide structure. /// /// This structure is a root of all objects in our application. It includes both layers: @@ -54,7 +65,11 @@ impl Ide { view: ide_view::root::View, controller: controller::Ide, ) -> Self { - let integration = integration::Integration::new(controller, view); + let integration = if enso_config::ARGS.rust_new_presentation_layer.unwrap_or(false) { + Integration::New(Presenter::new(controller, view)) + } else { + Integration::Old(integration::Integration::new(controller, view)) + }; let network = frp::Network::new("Ide"); Ide { application, integration, network }.init() } diff --git a/app/gui/src/lib.rs b/app/gui/src/lib.rs index 890a34433fc..f22b3a568ff 100644 --- a/app/gui/src/lib.rs +++ b/app/gui/src/lib.rs @@ -16,6 +16,7 @@ #![feature(map_try_insert)] #![feature(assert_matches)] #![feature(cell_filter_map)] +#![feature(hash_drain_filter)] #![recursion_limit = "512"] #![warn(missing_docs)] #![warn(trivial_casts)] @@ -33,6 +34,7 @@ pub mod executor; pub mod ide; pub mod model; pub mod notification; +pub mod presenter; pub mod sync; pub mod test; pub mod transport; diff --git a/app/gui/src/presenter.rs b/app/gui/src/presenter.rs new file mode 100644 index 00000000000..92a413d45cf --- /dev/null +++ b/app/gui/src/presenter.rs @@ -0,0 +1,148 @@ +//! The Presenter is a layer between logical part of the IDE (controllers, engine models) and the +//! views (the P letter in MVP pattern). The presenter reacts to changes in the controllers, and +//! actively updates the view. It also passes all user interactions from view to controllers. +//! +//! **The presenters are not fully implemented**. Therefore, the old integration defined in +//! [`crate::integration`] is used by default. The presenters may be tested by passing +//! `--rust-new-presentation-layer` commandline argument. + +pub mod graph; +pub mod project; + +pub use graph::Graph; +pub use project::Project; + +use crate::prelude::*; + +use crate::controller::ide::StatusNotification; +use crate::executor::global::spawn_stream_handler; +use crate::presenter; + +use ide_view as view; +use ide_view::graph_editor::SharedHashMap; + + + +// ============= +// === Model === +// ============= + +#[derive(Debug)] +struct Model { + logger: Logger, + current_project: RefCell>, + controller: controller::Ide, + view: view::root::View, +} + +impl Model { + /// Instantiate a new project presenter, which will display current project in the view. + fn setup_and_display_new_project(self: Rc) { + // Remove the old integration first. We want to be sure the old and new integrations will + // not race for the view. + *self.current_project.borrow_mut() = None; + + if let Some(project_model) = self.controller.current_project() { + // We know the name of new project before it loads. We set it right now to avoid + // displaying placeholder on the scene during loading. + let project_view = self.view.project(); + let breadcrumbs = &project_view.graph().model.breadcrumbs; + breadcrumbs.project_name(project_model.name().to_string()); + + let status_notifications = self.controller.status_notifications().clone_ref(); + let project_controller = + controller::Project::new(project_model, status_notifications.clone_ref()); + + executor::global::spawn(async move { + match presenter::Project::initialize(project_controller, project_view).await { + Ok(project) => { + *self.current_project.borrow_mut() = Some(project); + } + Err(err) => { + let err_msg = format!("Failed to initialize project: {}", err); + error!(self.logger, "{err_msg}"); + status_notifications.publish_event(err_msg) + } + } + }); + } + } +} + + + +// ================= +// === Presenter === +// ================= + +/// The root presenter, handling the synchronization between IDE controller and root view. +/// +/// See [`crate::presenter`] docs for information about presenters in general. +#[derive(Clone, CloneRef, Debug)] +pub struct Presenter { + model: Rc, +} + +impl Presenter { + /// Create new root presenter. + /// + /// The returned presenter is working and does not require any initialization. The current + /// project will be displayed (if any). + pub fn new(controller: controller::Ide, view: ide_view::root::View) -> Self { + let logger = Logger::new("Presenter"); + let current_project = default(); + let model = Rc::new(Model { logger, controller, view, current_project }); + Self { model }.init() + } + + fn init(self) -> Self { + self.setup_status_bar_notification_handler(); + self.setup_controller_notification_handler(); + self.model.clone_ref().setup_and_display_new_project(); + self + } + + fn setup_status_bar_notification_handler(&self) { + use controller::ide::BackgroundTaskHandle as ControllerHandle; + use ide_view::status_bar::process::Id as ViewHandle; + + let logger = self.model.logger.clone_ref(); + let process_map = SharedHashMap::::new(); + let status_bar = self.model.view.status_bar().clone_ref(); + let status_notifications = self.model.controller.status_notifications().subscribe(); + let weak = Rc::downgrade(&self.model); + spawn_stream_handler(weak, status_notifications, move |notification, _| { + match notification { + StatusNotification::Event { label } => { + status_bar.add_event(ide_view::status_bar::event::Label::new(label)); + } + StatusNotification::BackgroundTaskStarted { label, handle } => { + status_bar.add_process(ide_view::status_bar::process::Label::new(label)); + let view_handle = status_bar.last_process.value(); + process_map.insert(handle, view_handle); + } + StatusNotification::BackgroundTaskFinished { handle } => { + if let Some(view_handle) = process_map.remove(&handle) { + status_bar.finish_process(view_handle); + } else { + warning!(logger, "Controllers finished process not displayed in view"); + } + } + } + futures::future::ready(()) + }); + } + + fn setup_controller_notification_handler(&self) { + let stream = self.model.controller.subscribe(); + let weak = Rc::downgrade(&self.model); + spawn_stream_handler(weak, stream, move |notification, model| { + match notification { + controller::ide::Notification::NewProjectCreated + | controller::ide::Notification::ProjectOpened => + model.setup_and_display_new_project(), + } + futures::future::ready(()) + }); + } +} diff --git a/app/gui/src/presenter/graph.rs b/app/gui/src/presenter/graph.rs new file mode 100644 index 00000000000..94ed8cd6a98 --- /dev/null +++ b/app/gui/src/presenter/graph.rs @@ -0,0 +1,401 @@ +//! The module with the [`Graph`] presenter. See [`crate::presenter`] documentation to know more +//! about presenters in general. + +mod state; + +use crate::prelude::*; + +use crate::executor::global::spawn_stream_handler; + +use enso_frp as frp; +use ide_view as view; +use ide_view::graph_editor::component::node as node_view; +use ide_view::graph_editor::EdgeEndpoint; + + + +// =============== +// === Aliases === +// =============== + +type ViewNodeId = view::graph_editor::NodeId; +type AstNodeId = ast::Id; +type ViewConnection = view::graph_editor::EdgeId; +type AstConnection = controller::graph::Connection; + + + +// ============= +// === Model === +// ============= + +#[derive(Clone, Debug)] +struct Model { + logger: Logger, + controller: controller::ExecutedGraph, + view: view::graph_editor::GraphEditor, + state: Rc, +} + +impl Model { + pub fn new( + controller: controller::ExecutedGraph, + view: view::graph_editor::GraphEditor, + ) -> Self { + let logger = Logger::new("presenter::Graph"); + let state = default(); + Self { logger, controller, view, state } + } + + /// Node position was changed in view. + fn node_position_changed(&self, id: ViewNodeId, position: Vector2) { + self.update_ast( + || { + let ast_id = self.state.update_from_view().set_node_position(id, position)?; + Some(self.controller.graph().set_node_position(ast_id, position)) + }, + "update node position", + ); + } + + /// Node was removed in view. + fn node_removed(&self, id: ViewNodeId) { + self.update_ast( + || { + let ast_id = self.state.update_from_view().remove_node(id)?; + Some(self.controller.graph().remove_node(ast_id)) + }, + "remove node", + ) + } + + /// Connection was created in view. + fn new_connection_created(&self, id: ViewConnection) { + self.update_ast( + || { + let connection = self.view.model.edges.get_cloned_ref(&id)?; + let ast_to_create = self.state.update_from_view().create_connection(connection)?; + Some(self.controller.connect(&ast_to_create)) + }, + "create connection", + ); + } + + /// Connection was removed in view. + fn connection_removed(&self, id: ViewConnection) { + self.update_ast( + || { + let ast_to_remove = self.state.update_from_view().remove_connection(id)?; + Some(self.controller.disconnect(&ast_to_remove)) + }, + "delete connection", + ); + } + + fn update_ast(&self, f: F, action: &str) + where F: FnOnce() -> Option { + if let Some(Err(err)) = f() { + error!(self.logger, "Failed to {action} in AST: {err}"); + } + } + + /// Extract all types for subexpressions in node expressions, update the state, + /// and return the events for graph editor FRP input setting all of those types. + /// + /// The result includes the types not changed according to the state. That's because this + /// function is used after node expression change, and we need to reset all the types in view. + fn all_types_of_node( + &self, + node: ViewNodeId, + ) -> Vec<(ViewNodeId, ast::Id, Option)> { + let subexpressions = self.state.expressions_of_node(node); + subexpressions + .iter() + .map(|id| { + let a_type = self.expression_type(*id); + self.state.update_from_controller().set_expression_type(*id, a_type.clone()); + (node, *id, a_type) + }) + .collect() + } + + /// Extract all method pointers for subexpressions, update the state, and return events updating + /// view for expressions where method pointer actually changed. + fn all_method_pointers_of_node( + &self, + node: ViewNodeId, + ) -> Vec<(ast::Id, Option)> { + let subexpressions = self.state.expressions_of_node(node); + subexpressions.iter().filter_map(|id| self.refresh_expression_method_pointer(*id)).collect() + } + + /// Refresh type of the given expression. + /// + /// If the view update is required, the GraphEditor's FRP input event is returned. + fn refresh_expression_type( + &self, + id: ast::Id, + ) -> Option<(ViewNodeId, ast::Id, Option)> { + let a_type = self.expression_type(id); + let node_view = + self.state.update_from_controller().set_expression_type(id, a_type.clone())?; + Some((node_view, id, a_type)) + } + + /// Refresh method pointer of the given expression. + /// + /// If the view update is required, the GraphEditor's FRP input event is returned. + fn refresh_expression_method_pointer( + &self, + id: ast::Id, + ) -> Option<(ast::Id, Option)> { + let method_pointer = self.expression_method(id); + self.state + .update_from_controller() + .set_expression_method_pointer(id, method_pointer.clone())?; + Some((id, method_pointer)) + } + + /// Extract the expression's current type from controllers. + fn expression_type(&self, id: ast::Id) -> Option { + let registry = self.controller.computed_value_info_registry(); + let info = registry.get(&id)?; + Some(view::graph_editor::Type(info.typename.as_ref()?.clone_ref())) + } + + /// Extract the expression's current method pointer from controllers. + fn expression_method(&self, id: ast::Id) -> Option { + let registry = self.controller.computed_value_info_registry(); + let method_id = registry.get(&id)?.method_call?; + let suggestion_db = self.controller.graph().suggestion_db.clone_ref(); + let method = suggestion_db.lookup_method_ptr(method_id).ok()?; + Some(view::graph_editor::MethodPointer(Rc::new(method))) + } +} + + + +// ================== +// === ViewUpdate === +// ================== + +/// Structure handling view update after graph invalidation. +/// +/// Because updating various graph elements (nodes, connections, types) bases on the same data +/// extracted from controllers, the data are cached in this structure. +#[derive(Clone, Debug, Default)] +struct ViewUpdate { + state: Rc, + nodes: Vec, + trees: HashMap, + connections: HashSet, +} + +impl ViewUpdate { + /// Create ViewUpdate information from Graph Presenter's model. + fn new(model: &Model) -> FallibleResult { + let displayed = model.state.clone_ref(); + let nodes = model.controller.graph().nodes()?; + let connections_and_trees = model.controller.connections()?; + let connections = connections_and_trees.connections.into_iter().collect(); + let trees = connections_and_trees.trees; + Ok(Self { state: displayed, nodes, trees, connections }) + } + + /// Remove nodes from the state and return node views to be removed. + fn remove_nodes(&self) -> Vec { + self.state.update_from_controller().retain_nodes(&self.node_ids().collect()) + } + + /// Returns number of nodes view should create. + fn count_nodes_to_add(&self) -> usize { + self.node_ids().filter(|n| self.state.view_id_of_ast_node(*n).is_none()).count() + } + + /// Set the nodes expressions in state, and return the events to be passed to Graph Editor FRP + /// input for nodes where expression changed. + /// + /// The nodes not having views are also updated in the state. + fn set_node_expressions(&self) -> Vec<(ViewNodeId, node_view::Expression)> { + self.nodes + .iter() + .filter_map(|node| { + let id = node.main_line.id(); + let trees = self.trees.get(&id).cloned().unwrap_or_default(); + self.state.update_from_controller().set_node_expression(node, trees) + }) + .collect() + } + + /// Set the nodes position in state, and return the events to be passed to GraphEditor FRP + /// input for nodes where position changed. + /// + /// The nodes not having views are also updated in the state. + fn set_node_positions(&self) -> Vec<(ViewNodeId, Vector2)> { + self.nodes + .iter() + .filter_map(|node| { + let id = node.main_line.id(); + let position = node.position()?.vector; + let view_id = + self.state.update_from_controller().set_node_position(id, position)?; + Some((view_id, position)) + }) + .collect() + } + + /// Remove connections from the state and return views to be removed. + fn remove_connections(&self) -> Vec { + self.state.update_from_controller().retain_connections(&self.connections) + } + + /// Add connections to the state and return endpoints of connections to be created in views. + fn add_connections(&self) -> Vec<(EdgeEndpoint, EdgeEndpoint)> { + let ast_conns = self.connections.iter(); + ast_conns + .filter_map(|connection| { + self.state.update_from_controller().set_connection(connection.clone()) + }) + .collect() + } + + fn node_ids(&self) -> impl Iterator + '_ { + self.nodes.iter().map(controller::graph::Node::id) + } +} + + + +// ============= +// === Graph === +// ============= + +/// The Graph Presenter, synchronizing graph state between graph controller and view. +/// +/// This presenter focuses on the graph structure: nodes, their expressions and types, and +/// connections between them. It does not integrate Searcher nor Breadcrumbs - integration of +/// these is still to-be-delivered. +#[derive(Debug)] +pub struct Graph { + network: frp::Network, + model: Rc, +} + +impl Graph { + /// Create graph presenter. The returned structure is working and does not require any + /// initialization. + pub fn new( + controller: controller::ExecutedGraph, + view: view::graph_editor::GraphEditor, + ) -> Self { + let network = frp::Network::new("presenter::Graph"); + let model = Rc::new(Model::new(controller, view)); + Self { network, model }.init() + } + + fn init(self) -> Self { + let logger = &self.model.logger; + let network = &self.network; + let model = &self.model; + let view = &self.model.view.frp; + frp::extend! { network + update_view <- source::<()>(); + update_data <- update_view.map( + f_!([logger,model] match ViewUpdate::new(&*model) { + Ok(update) => Rc::new(update), + Err(err) => { + error!(logger,"Failed to update view: {err:?}"); + Rc::new(default()) + } + }) + ); + + + // === Refreshing Nodes === + + remove_node <= update_data.map(|update| update.remove_nodes()); + update_node_expression <= update_data.map(|update| update.set_node_expressions()); + set_node_position <= update_data.map(|update| update.set_node_positions()); + view.remove_node <+ remove_node; + view.set_node_expression <+ update_node_expression; + view.set_node_position <+ set_node_position; + + view.add_node <+ update_data.map(|update| update.count_nodes_to_add()).repeat(); + added_node_update <- view.node_added.filter_map(f!((view_id) + model.state.assign_node_view(*view_id) + )); + init_node_expression <- added_node_update.filter_map(|update| Some((update.view_id?, update.expression.clone()))); + view.set_node_expression <+ init_node_expression; + view.set_node_position <+ added_node_update.filter_map(|update| Some((update.view_id?, update.position))); + + + // === Refreshing Connections === + + remove_connection <= update_data.map(|update| update.remove_connections()); + add_connection <= update_data.map(|update| update.add_connections()); + view.remove_edge <+ remove_connection; + view.connect_nodes <+ add_connection; + + + // === Refreshing Expressions === + + reset_node_types <- any(update_node_expression, init_node_expression)._0(); + set_expression_type <= reset_node_types.map(f!((view_id) model.all_types_of_node(*view_id))); + set_method_pointer <= reset_node_types.map(f!((view_id) model.all_method_pointers_of_node(*view_id))); + view.set_expression_usage_type <+ set_expression_type; + view.set_method_pointer <+ set_method_pointer; + + update_expressions <- source::>(); + update_expression <= update_expressions; + view.set_expression_usage_type <+ update_expression.filter_map(f!((id) model.refresh_expression_type(*id))); + view.set_method_pointer <+ update_expression.filter_map(f!((id) model.refresh_expression_method_pointer(*id))); + + + // === Changes from the View === + + eval view.node_position_set_batched(((node_id, position)) model.node_position_changed(*node_id, *position)); + eval view.node_removed((node_id) model.node_removed(*node_id)); + eval view.on_edge_endpoints_set((edge_id) model.new_connection_created(*edge_id)); + eval view.on_edge_endpoint_unset(((edge_id,_)) model.connection_removed(*edge_id)); + } + + update_view.emit(()); + self.setup_controller_notification_handlers(update_view, update_expressions); + + self + } + + fn setup_controller_notification_handlers( + &self, + update_view: frp::Source<()>, + update_expressions: frp::Source>, + ) { + use crate::controller::graph::executed; + use crate::controller::graph::Notification; + let graph_notifications = self.model.controller.subscribe(); + self.spawn_sync_stream_handler(graph_notifications, move |notification, model| { + info!(model.logger, "Received controller notification {notification:?}"); + match notification { + executed::Notification::Graph(graph) => match graph { + Notification::Invalidate => update_view.emit(()), + Notification::PortsUpdate => update_view.emit(()), + }, + executed::Notification::ComputedValueInfo(expressions) => + update_expressions.emit(expressions), + executed::Notification::EnteredNode(_) => {} + executed::Notification::SteppedOutOfNode(_) => {} + } + }) + } + + fn spawn_sync_stream_handler(&self, stream: Stream, handler: Function) + where + Stream: StreamExt + Unpin + 'static, + Function: Fn(Stream::Item, Rc) + 'static, { + let model = Rc::downgrade(&self.model); + spawn_stream_handler(model, stream, move |item, model| { + handler(item, model); + futures::future::ready(()) + }) + } +} diff --git a/app/gui/src/presenter/graph/state.rs b/app/gui/src/presenter/graph/state.rs new file mode 100644 index 00000000000..539d65c24fa --- /dev/null +++ b/app/gui/src/presenter/graph/state.rs @@ -0,0 +1,756 @@ +//! The module containing the Graph Presenter [`State`] + +use crate::prelude::*; + +use crate::presenter::graph::AstConnection; +use crate::presenter::graph::AstNodeId; +use crate::presenter::graph::ViewConnection; +use crate::presenter::graph::ViewNodeId; + +use bimap::BiMap; +use bimap::Overwritten; +use ide_view as view; +use ide_view::graph_editor::component::node as node_view; +use ide_view::graph_editor::EdgeEndpoint; + + + +// ============= +// === Nodes === +// ============= + +/// A single node data. +#[allow(missing_docs)] +#[derive(Clone, Debug, Default)] +pub struct Node { + pub view_id: Option, + pub position: Vector2, + pub expression: node_view::Expression, +} + +/// The set of node states. +/// +/// This structure allows to access data of any node by Ast id, or view id. It also keeps list +/// of the AST nodes with no view assigned, and allows to assign View Id to the next one. +#[derive(Clone, Debug, Default)] +pub struct Nodes { + // Each operation in this structure should keep the following constraints: + // * Each `nodes_without_view` entry has an entry in `nodes` with `view_id` being `None`. + // * All values in `ast_node_by_view_id` has corresponding element in `nodes` with `view_id` + // being equal to key of the value. + nodes: HashMap, + nodes_without_view: Vec, + ast_node_by_view_id: HashMap, +} + +impl Nodes { + /// Get the state of the node by Ast id. + pub fn get(&self, id: AstNodeId) -> Option<&Node> { + self.nodes.get(&id) + } + + /// Get mutable reference of the node's state by Ast id. + pub fn get_mut(&mut self, id: AstNodeId) -> Option<&mut Node> { + self.nodes.get_mut(&id) + } + + /// Get the mutable reference, creating an default entry without view if it's missing. + /// + /// The entry will be also present on the "nodes without view" list and may have view assigned + /// using [`assign_newly_created_node`] method. + pub fn get_mut_or_create(&mut self, id: AstNodeId) -> &mut Node { + let nodes_without_view = &mut self.nodes_without_view; + self.nodes.entry(id).or_insert_with(|| { + nodes_without_view.push(id); + default() + }) + } + + /// Get the AST id of the node represented by given view. Returns None, if the node view does + /// not represent any AST node. + pub fn ast_id_of_view(&self, view_id: ViewNodeId) -> Option { + self.ast_node_by_view_id.get(&view_id).copied() + } + + /// Assign a node view to the one of AST nodes without view. If there is any of such nodes, + /// `None` is returned. Otherwise, returns the node state - the newly created view must be + /// refreshed with the data from the state. + pub fn assign_newly_created_node(&mut self, view_id: ViewNodeId) -> Option<&mut Node> { + let ast_node = self.nodes_without_view.pop()?; + let mut opt_displayed = self.nodes.get_mut(&ast_node); + if let Some(displayed) = &mut opt_displayed { + displayed.view_id = Some(view_id); + self.ast_node_by_view_id.insert(view_id, ast_node); + } + opt_displayed + } + + /// Update the state retaining given set of nodes. Returns the list of removed nodes' views. + pub fn retain_nodes(&mut self, nodes: &HashSet) -> Vec { + self.nodes_without_view.drain_filter(|id| !nodes.contains(id)); + let removed = self.nodes.drain_filter(|id, _| !nodes.contains(id)); + let removed_views = removed.filter_map(|(_, data)| data.view_id).collect(); + for view_id in &removed_views { + self.ast_node_by_view_id.remove(view_id); + } + removed_views + } + + /// Remove node represented by given view (if any) and return it's AST id. + pub fn remove_node(&mut self, node: ViewNodeId) -> Option { + let ast_id = self.ast_node_by_view_id.remove(&node)?; + self.nodes.remove(&ast_id); + Some(ast_id) + } +} + + + +// =================== +// === Connections === +// =================== + +/// A structure keeping pairs of AST connections with their views (and list of AST connections +/// without view). +#[derive(Clone, Debug, Default)] +pub struct Connections { + connections: BiMap, + connections_without_view: HashSet, +} + +impl Connections { + /// Remove all connections not belonging to the given set. + /// + /// Returns the views of removed connections. + pub fn retain_connections( + &mut self, + connections: &HashSet, + ) -> Vec { + self.connections_without_view.retain(|x| connections.contains(x)); + let to_remove = self.connections.iter().filter(|(con, _)| !connections.contains(con)); + let to_remove_vec = to_remove.map(|(_, edge_id)| *edge_id).collect_vec(); + self.connections.retain(|con, _| connections.contains(con)); + to_remove_vec + } + + /// Add a new AST connection without view. + pub fn add_ast_connection(&mut self, connection: AstConnection) -> bool { + if !self.connections.contains_left(&connection) { + self.connections_without_view.insert(connection) + } else { + false + } + } + + /// Add a connection with view. + /// + /// Returns `true` if the new connection was added, and `false` if it already existed. In the + /// latter case, the new `view` is assigned to it (replacing possible previous view). + pub fn add_connection_view(&mut self, connection: AstConnection, view: ViewConnection) -> bool { + let existed_without_view = self.connections_without_view.remove(&connection); + match self.connections.insert(connection, view) { + Overwritten::Neither => !existed_without_view, + Overwritten::Left(_, _) => false, + Overwritten::Right(previous, _) => { + self.connections_without_view.insert(previous); + !existed_without_view + } + Overwritten::Pair(_, _) => false, + Overwritten::Both(_, (previous, _)) => { + self.connections_without_view.insert(previous); + false + } + } + } + + /// Remove the connection by view (if any), and return it. + pub fn remove_connection(&mut self, connection: ViewConnection) -> Option { + let (ast_connection, _) = self.connections.remove_by_right(&connection)?; + Some(ast_connection) + } +} + + + +// =================== +// === Expressions === +// =================== + +/// A single expression data. +#[derive(Clone, Debug, Default)] +pub struct Expression { + pub node: AstNodeId, + pub expression_type: Option, + pub method_pointer: Option, +} + +/// The data of node's expressions. +/// +/// The expressions are all AST nodes of the line representing the node in the code. +#[derive(Clone, Debug, Default)] +pub struct Expressions { + expressions: HashMap, + expressions_of_node: HashMap>, +} + +impl Expressions { + /// Remove all expressions not belonging to the any of the `nodes`. + pub fn retain_expression_of_nodes(&mut self, nodes: &HashSet) { + let nodes_to_remove = + self.expressions_of_node.drain_filter(|node_id, _| !nodes.contains(node_id)); + let expr_to_remove = nodes_to_remove.map(|(_, exprs)| exprs).flatten(); + for expression_id in expr_to_remove { + self.expressions.remove(&expression_id); + } + } + + /// Update information about node expressions. + /// + /// New node's expressions are added, and those which stopped to be part of the node are + /// removed. + pub fn update_node_expressions(&mut self, node: AstNodeId, expressions: Vec) { + let new_set: HashSet = expressions.iter().copied().collect(); + let old_set = self.expressions_of_node.insert(node, expressions).unwrap_or_default(); + for old_expression in &old_set { + if !new_set.contains(old_expression) { + self.expressions.remove(old_expression); + } + } + for new_expression in new_set { + if !old_set.contains(&new_expression) { + self.expressions.insert(new_expression, Expression { node, ..default() }); + } + } + } + + /// Get mutable reference to given expression data. + pub fn get_mut(&mut self, id: ast::Id) -> Option<&mut Expression> { + self.expressions.get_mut(&id) + } + + /// Get the list of all expressions of the given node. + pub fn expressions_of_node(&self, id: AstNodeId) -> &[ast::Id] { + self.expressions_of_node.get(&id).map_or(&[], |v| v.as_slice()) + } +} + + + +// ============= +// === State === +// ============= + +/// The Graph Presenter State. +/// +/// This structure keeps the information how the particular graph elements received from controllers +/// are represented in the view. It also handles updates from the controllers and +/// the view in `update_from_controller` and `update_from_view` respectively. +#[derive(Clone, Debug, Default)] +pub struct State { + nodes: RefCell, + connections: RefCell, + expressions: RefCell, +} + +impl State { + /// Get node's view id by the AST id. + pub fn view_id_of_ast_node(&self, node: AstNodeId) -> Option { + self.nodes.borrow().get(node).and_then(|n| n.view_id) + } + + /// Convert the AST connection to pair of [`EdgeEndpoint`]s. + pub fn view_edge_targets_of_ast_connection( + &self, + connection: AstConnection, + ) -> Option<(EdgeEndpoint, EdgeEndpoint)> { + let nodes = self.nodes.borrow(); + let src_node = nodes.get(connection.source.node)?.view_id?; + let dst_node = nodes.get(connection.destination.node)?.view_id?; + let src = EdgeEndpoint::new(src_node, connection.source.port); + let data = EdgeEndpoint::new(dst_node, connection.destination.port); + Some((src, data)) + } + + /// Convert the pair of [`EdgeEndpoint`]s to AST connection. + pub fn ast_connection_from_view_edge_targets( + &self, + source: EdgeEndpoint, + target: EdgeEndpoint, + ) -> Option { + let nodes = self.nodes.borrow(); + let src_node = nodes.ast_id_of_view(source.node_id)?; + let dst_node = nodes.ast_id_of_view(target.node_id)?; + Some(controller::graph::Connection { + source: controller::graph::Endpoint::new(src_node, source.port), + destination: controller::graph::Endpoint::new(dst_node, target.port), + }) + } + + /// Get id of all node's expressions (ids of the all corresponding line AST nodes). + pub fn expressions_of_node(&self, node: ViewNodeId) -> Vec { + let ast_node = self.nodes.borrow().ast_id_of_view(node); + ast_node.map_or_default(|id| self.expressions.borrow().expressions_of_node(id).to_owned()) + } + + /// Apply the update from controller. + pub fn update_from_controller(&self) -> ControllerChange { + ControllerChange { state: self } + } + + /// Apply the update from the view. + pub fn update_from_view(&self) -> ViewChange { + ViewChange { state: self } + } + + /// Assign a node view to the one of AST nodes without view. If there is any of such nodes, + /// `None` is returned. Otherwise, returns the node state - the newly created view must be + /// refreshed with the data from the state. + pub fn assign_node_view(&self, view_id: ViewNodeId) -> Option { + self.nodes.borrow_mut().assign_newly_created_node(view_id).cloned() + } +} + +// ======================== +// === ControllerChange === +// ======================== + +/// The wrapper for [`State`] reference providing the API to be called when presenter is notified +/// by controllers about graph change. +/// +/// All of its operations updates the [`State`] to synchronize it with the graph in AST, and returns +/// the information how to update yje view, to have the view synchronized with the state. +/// +/// In the particular case, when the graph was changed due to user interations with the view, these +/// method should discover that no change in state is needed (because it was updated already by +/// [`ViewChange`]), and so the view's. This way we avoid an infinite synchronization cycle. +#[derive(Deref, DerefMut, Debug)] +pub struct ControllerChange<'a> { + state: &'a State, +} + + +// === Nodes === + +impl<'a> ControllerChange<'a> { + /// Remove all nodes not belonging to the given set. Returns the list of to-be-removed views. + pub fn retain_nodes(&self, nodes: &HashSet) -> Vec { + self.expressions.borrow_mut().retain_expression_of_nodes(nodes); + self.nodes.borrow_mut().retain_nodes(nodes) + } + + /// Set the new node position. If the node position actually changed, the to-be-updated view + /// is returned. + pub fn set_node_position(&self, node: AstNodeId, position: Vector2) -> Option { + let mut nodes = self.nodes.borrow_mut(); + let mut displayed = nodes.get_mut_or_create(node); + if displayed.position != position { + displayed.position = position; + displayed.view_id + } else { + None + } + } + + /// Set the new node expression. If the expression actually changed, the to-be-updated view + /// is returned with the new expression to set. + pub fn set_node_expression( + &self, + node: &controller::graph::Node, + trees: controller::graph::NodeTrees, + ) -> Option<(ViewNodeId, node_view::Expression)> { + let ast_id = node.main_line.id(); + let new_displayed_expr = node_view::Expression { + pattern: node.info.pattern().map(|t| t.repr()), + code: node.info.expression().repr(), + whole_expression_id: node.info.expression().id, + input_span_tree: trees.inputs, + output_span_tree: trees.outputs.unwrap_or_else(default), + }; + let mut nodes = self.nodes.borrow_mut(); + let displayed = nodes.get_mut_or_create(ast_id); + if displayed.expression != new_displayed_expr { + displayed.expression = new_displayed_expr.clone(); + let new_expressions = + node.info.ast().iter_recursive().filter_map(|ast| ast.id).collect(); + self.expressions.borrow_mut().update_node_expressions(ast_id, new_expressions); + Some((displayed.view_id?, new_displayed_expr)) + } else { + None + } + } +} + + +// === Connections === + +impl<'a> ControllerChange<'a> { + /// If given connection does not exists yet, add it and return the endpoints of the + /// to-be-created edge. + pub fn set_connection( + &self, + connection: AstConnection, + ) -> Option<(EdgeEndpoint, EdgeEndpoint)> { + self.connections + .borrow_mut() + .add_ast_connection(connection.clone()) + .and_option_from(move || self.view_edge_targets_of_ast_connection(connection)) + } + + /// Remove all connection not belonging to the given set. Returns the list of to-be-removed + /// views. + pub fn retain_connections(&self, connections: &HashSet) -> Vec { + self.connections.borrow_mut().retain_connections(connections) + } +} + + +// === Expressions === + +impl<'a> ControllerChange<'a> { + /// Set the new type of expression. If the type actually changes, the to-be-updated view is + /// returned. + pub fn set_expression_type( + &self, + id: ast::Id, + new_type: Option, + ) -> Option { + let mut expressions = self.expressions.borrow_mut(); + let to_update = expressions.get_mut(id).filter(|d| d.expression_type != new_type); + if let Some(displayed) = to_update { + displayed.expression_type = new_type; + self.nodes.borrow().get(displayed.node).and_then(|node| node.view_id) + } else { + None + } + } + + /// Set the new expression's method pointer. If the method pointer actually changes, the + /// to-be-updated view is returned. + pub fn set_expression_method_pointer( + &self, + id: ast::Id, + method_ptr: Option, + ) -> Option { + let mut expressions = self.expressions.borrow_mut(); + let to_update = expressions.get_mut(id).filter(|d| d.method_pointer != method_ptr); + if let Some(displayed) = to_update { + displayed.method_pointer = method_ptr; + self.nodes.borrow().get(displayed.node).and_then(|node| node.view_id) + } else { + None + } + } +} + + + +// ================== +// === ViewChange === +// ================== + +/// The wrapper for [`State`] reference providing the API to be called when presenter is notified +/// about view change. +/// +/// All of its operations updates the [`State`] to synchronize it with the graph view, and returns +/// the information how to update the AST graph, to have the AST synchronized with the state. +/// +/// In particular case, when the view was changed due to change in controller, these method should +/// discover that no change in state is needed (because it was updated already by +/// [`ControllerChange`]), and so the AST graph's. This way we avoid an infinite synchronization +/// cycle. +#[derive(Deref, DerefMut, Debug)] +pub struct ViewChange<'a> { + state: &'a State, +} + + +// === Nodes === + +impl<'a> ViewChange<'a> { + /// Set the new node position. If the node position actually changed, the AST node to-be-updated + /// id is returned. + pub fn set_node_position(&self, id: ViewNodeId, new_position: Vector2) -> Option { + let mut nodes = self.nodes.borrow_mut(); + let ast_id = nodes.ast_id_of_view(id)?; + let displayed = nodes.get_mut(ast_id)?; + if displayed.position != new_position { + displayed.position = new_position; + Some(ast_id) + } else { + None + } + } + + /// Remove the node, and returns its AST id. + pub fn remove_node(&self, id: ViewNodeId) -> Option { + self.nodes.borrow_mut().remove_node(id) + } +} + + +// === Connections === + +impl<'a> ViewChange<'a> { + /// If the connections does not already exist, it is created and corresponding to-be-created + /// Ast connection is returned. + pub fn create_connection(&self, connection: view::graph_editor::Edge) -> Option { + let source = connection.source()?; + let target = connection.target()?; + self.create_connection_from_endpoints(connection.id(), source, target) + } + + /// If the connections with provided endpoints does not already exist, it is created and + /// corresponding to-be-created Ast connection is returned. + pub fn create_connection_from_endpoints( + &self, + connection: ViewConnection, + source: EdgeEndpoint, + target: EdgeEndpoint, + ) -> Option { + let ast_connection = self.ast_connection_from_view_edge_targets(source, target)?; + let mut connections = self.connections.borrow_mut(); + let should_update_controllers = + connections.add_connection_view(ast_connection.clone(), connection); + should_update_controllers.then_some(ast_connection) + } + + /// Remove the connection and return the corresponding AST connection which should be removed. + pub fn remove_connection(&self, id: ViewConnection) -> Option { + self.connections.borrow_mut().remove_connection(id) + } +} + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod tests { + use super::*; + use engine_protocol::language_server::MethodPointer; + use parser::Parser; + + fn create_test_node(expression: &str) -> controller::graph::Node { + let parser = Parser::new_or_panic(); + let ast = parser.parse_line_ast(expression).unwrap(); + controller::graph::Node { + info: double_representation::node::NodeInfo { + documentation: None, + main_line: double_representation::node::MainLine::from_ast(&ast).unwrap(), + }, + metadata: None, + } + } + + fn node_trees_of(node: &controller::graph::Node) -> controller::graph::NodeTrees { + controller::graph::NodeTrees::new(&node.info, &span_tree::generate::context::Empty).unwrap() + } + + struct TestNode { + node: controller::graph::Node, + view: ViewNodeId, + } + + struct Fixture { + state: State, + nodes: Vec, + } + + impl Fixture { + fn setup_nodes(expressions: impl IntoIterator>) -> Self { + let nodes = expressions.into_iter().map(|expr| create_test_node(expr.as_ref())); + let state = State::default(); + let displayed_nodes = nodes + .enumerate() + .map(|(i, node)| { + let view = ensogl::display::object::Id::from(i).into(); + state.update_from_controller().set_node_expression(&node, node_trees_of(&node)); + state.assign_node_view(view); + TestNode { node, view } + }) + .collect(); + Fixture { state, nodes: displayed_nodes } + } + } + + #[wasm_bindgen_test] + fn adding_and_removing_nodes() { + let state = State::default(); + let node1 = create_test_node("node1 = 2 + 2"); + let node2 = create_test_node("node2 = node1 + 2"); + let node_view_1 = ensogl::display::object::Id::from(1).into(); + let node_view_2 = ensogl::display::object::Id::from(2).into(); + let from_controller = state.update_from_controller(); + let from_view = state.update_from_view(); + + assert_eq!(from_controller.set_node_expression(&node1, node_trees_of(&node1)), None); + assert_eq!(from_controller.set_node_expression(&node2, node_trees_of(&node2)), None); + + assert_eq!(state.view_id_of_ast_node(node1.id()), None); + assert_eq!(state.view_id_of_ast_node(node2.id()), None); + + let assigned = state.assign_node_view(node_view_2); + assert_eq!(assigned.map(|node| node.expression.code), Some("node1 + 2".to_owned())); + let assigned = state.assign_node_view(node_view_1); + assert_eq!(assigned.map(|node| node.expression.code), Some("2 + 2".to_owned())); + + assert_eq!(state.view_id_of_ast_node(node1.id()), Some(node_view_1)); + assert_eq!(state.view_id_of_ast_node(node2.id()), Some(node_view_2)); + + let node1_exprs = + node1.info.main_line.ast().iter_recursive().filter_map(|a| a.id).collect_vec(); + assert_eq!(state.expressions_of_node(node_view_1), node1_exprs); + let node2_exprs = + node2.info.main_line.ast().iter_recursive().filter_map(|a| a.id).collect_vec(); + assert_eq!(state.expressions_of_node(node_view_2), node2_exprs); + + let views_to_remove = from_controller.retain_nodes(&[node1.id()].iter().copied().collect()); + assert_eq!(views_to_remove, vec![node_view_2]); + + assert_eq!(state.view_id_of_ast_node(node1.id()), Some(node_view_1)); + assert_eq!(state.view_id_of_ast_node(node2.id()), None); + + assert_eq!(from_view.remove_node(node_view_1), Some(node1.id())); + assert_eq!(state.view_id_of_ast_node(node1.id()), None) + } + + #[wasm_bindgen_test] + fn adding_and_removing_connections() { + use controller::graph::Endpoint; + let Fixture { state, nodes } = Fixture::setup_nodes(&["node1 = 2", "node1 + node1"]); + let src = Endpoint { + node: nodes[0].node.id(), + port: default(), + var_crumbs: default(), + }; + let dest1 = Endpoint { + node: nodes[1].node.id(), + port: span_tree::Crumbs::new(vec![0]), + var_crumbs: default(), + }; + let dest2 = Endpoint { + node: nodes[1].node.id(), + port: span_tree::Crumbs::new(vec![2]), + var_crumbs: default(), + }; + let ast_con1 = AstConnection { source: src.clone(), destination: dest1.clone() }; + let ast_con2 = AstConnection { source: src.clone(), destination: dest2.clone() }; + let view_con1 = ensogl::display::object::Id::from(1).into(); + let view_con2 = ensogl::display::object::Id::from(2).into(); + let view_src = EdgeEndpoint { node_id: nodes[0].view, port: src.port.clone() }; + let view_tgt1 = EdgeEndpoint { node_id: nodes[1].view, port: dest1.port.clone() }; + let view_tgt2 = EdgeEndpoint { node_id: nodes[1].view, port: dest2.port.clone() }; + let view_pair1 = (view_src.clone(), view_tgt1.clone()); + + let from_controller = state.update_from_controller(); + let from_view = state.update_from_view(); + + assert_eq!(from_controller.set_connection(ast_con1.clone()), Some(view_pair1.clone())); + + assert_eq!( + from_view.create_connection_from_endpoints(view_con1, view_src.clone(), view_tgt1), + None + ); + assert_eq!( + from_view.create_connection_from_endpoints(view_con2, view_src.clone(), view_tgt2), + Some(ast_con2.clone()) + ); + + let all_connections = [ast_con1.clone(), ast_con2.clone()].into_iter().collect(); + assert_eq!(from_controller.retain_connections(&all_connections), vec![]); + assert_eq!( + from_controller.retain_connections(&[ast_con2.clone()].into_iter().collect()), + vec![view_con1] + ); + + assert_eq!(from_view.remove_connection(view_con2), Some(ast_con2.clone())); + } + + #[wasm_bindgen_test] + fn refreshing_node_expression() { + let Fixture { state, nodes } = Fixture::setup_nodes(&["foo bar"]); + let node_id = nodes[0].node.id(); + let new_ast = Parser::new_or_panic().parse_line_ast("foo baz").unwrap().with_id(node_id); + let new_node = controller::graph::Node { + info: double_representation::node::NodeInfo { + documentation: None, + main_line: double_representation::node::MainLine::from_ast(&new_ast).unwrap(), + }, + metadata: None, + }; + let new_subexpressions = new_ast.iter_recursive().filter_map(|ast| ast.id).collect_vec(); + let new_trees = node_trees_of(&new_node); + let view = nodes[0].view; + let expected_new_expression = view::graph_editor::component::node::Expression { + pattern: None, + code: "foo baz".to_string(), + whole_expression_id: Some(node_id), + input_span_tree: new_trees.inputs.clone(), + output_span_tree: default(), + }; + let updater = state.update_from_controller(); + assert_eq!( + updater.set_node_expression(&new_node, new_trees.clone()), + Some((view, expected_new_expression)) + ); + assert_eq!(updater.set_node_expression(&new_node, new_trees), None); + assert_eq!(state.expressions_of_node(view), new_subexpressions); + } + + #[wasm_bindgen_test] + fn updating_node_position() { + let Fixture { state, nodes } = Fixture::setup_nodes(&["foo"]); + let node_id = nodes[0].node.id(); + let view_id = nodes[0].view; + let position_from_ast = Vector2(1.0, 2.0); + let position_from_view = Vector2(3.0, 4.0); + let from_controller = state.update_from_controller(); + let from_view = state.update_from_view(); + + assert_eq!(from_controller.set_node_position(node_id, position_from_ast), Some(view_id)); + assert_eq!(from_view.set_node_position(view_id, position_from_ast), None); + assert_eq!(from_view.set_node_position(view_id, position_from_view), Some(node_id)); + assert_eq!(from_controller.set_node_position(node_id, position_from_view), None); + } + + #[wasm_bindgen_test] + fn refreshing_expression_types() { + use ast::crumbs::InfixCrumb; + let Fixture { state, nodes } = Fixture::setup_nodes(&["2 + 3"]); + let view = nodes[0].view; + let node_ast = nodes[0].node.main_line.expression(); + let left_operand = node_ast.get(&InfixCrumb::LeftOperand.into()).unwrap().id.unwrap(); + let right_operand = node_ast.get(&InfixCrumb::RightOperand.into()).unwrap().id.unwrap(); + let updater = state.update_from_controller(); + + let number_type = Some(view::graph_editor::Type::from("Number".to_owned())); + assert_eq!(updater.set_expression_type(left_operand, number_type.clone()), Some(view)); + assert_eq!(updater.set_expression_type(right_operand, number_type.clone()), Some(view)); + + assert_eq!(updater.set_expression_type(left_operand, number_type.clone()), None); + assert_eq!(updater.set_expression_type(right_operand, number_type), None); + + assert_eq!(updater.set_expression_type(left_operand, None), Some(view)); + assert_eq!(updater.set_expression_type(right_operand, None), Some(view)); + } + + #[wasm_bindgen_test] + fn refreshing_expression_method_pointers() { + let Fixture { state, nodes } = Fixture::setup_nodes(&["foo bar"]); + let view = nodes[0].view; + let expr = nodes[0].node.id(); + let updater = state.update_from_controller(); + + let method_ptr = MethodPointer { + module: "Foo".to_string(), + defined_on_type: "Foo".to_string(), + name: "foo".to_string(), + }; + let method_ptr = Some(view::graph_editor::MethodPointer::from(method_ptr)); + assert_eq!(updater.set_expression_method_pointer(expr, method_ptr.clone()), Some(view)); + assert_eq!(updater.set_expression_method_pointer(expr, method_ptr), None); + assert_eq!(updater.set_expression_method_pointer(expr, None), Some(view)); + } +} diff --git a/app/gui/src/presenter/project.rs b/app/gui/src/presenter/project.rs new file mode 100644 index 00000000000..06a48738c55 --- /dev/null +++ b/app/gui/src/presenter/project.rs @@ -0,0 +1,64 @@ +//! The module with the [`Project`] presenter. See [`crate::presenter`] documentation to know more +//! about presenters in general. + +use crate::prelude::*; + +use crate::presenter; + +use ide_view as view; + + + +// ============= +// === Model === +// ============= + +// Those fields will be probably used when Searcher and Breadcrumbs integration will be implemented. +#[allow(unused)] +#[derive(Debug)] +struct Model { + controller: controller::Project, + view: view::project::View, + graph: presenter::Graph, +} + + + +// =============== +// === Project === +// =============== + +/// The Project Presenter, synchronizing state between project controller and project view. +#[derive(Clone, CloneRef, Debug)] +pub struct Project { + model: Rc, +} + +impl Project { + /// Construct new project presenter, basing of the project initialization result. + /// + /// The returned presenter will be already working: it will display the initial main graph, and + /// react to all notifications. + pub fn new( + controller: controller::Project, + init_result: controller::project::InitializationResult, + view: view::project::View, + ) -> Self { + let graph_controller = init_result.main_graph; + let graph = presenter::Graph::new(graph_controller, view.graph().clone_ref()); + let model = Model { controller, view, graph }; + Self { model: Rc::new(model) } + } + + /// Initialize project and return working presenter. + /// + /// This calls the [`controller::Project::initialize`] method and use the initialization result + /// to construct working presenter. + pub async fn initialize( + controller: controller::Project, + view: view::project::View, + ) -> FallibleResult { + let init_result = controller.initialize().await?; + Ok(Self::new(controller, init_result, view)) + } +} diff --git a/app/gui/view/Cargo.toml b/app/gui/view/Cargo.toml index 682809908a7..97bda2a2589 100644 --- a/app/gui/view/Cargo.toml +++ b/app/gui/view/Cargo.toml @@ -24,6 +24,7 @@ ide-view-graph-editor = { path = "graph-editor" } parser = { path = "../language/parser" } span-tree = { path = "../language/span-tree" } js-sys = { version = "0.3.28" } +multi-map = { version = "1.3.0" } nalgebra = { version = "0.26.1", features = ["serde-serialize"] } ordered-float = { version = "2.7.0" } serde_json = { version = "1.0" } diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index d2ac4f2fedc..5818879600f 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -857,7 +857,7 @@ pub struct LocalCall { // === EdgeEndpoint === // ================== -#[derive(Clone, CloneRef, Debug, Default)] +#[derive(Clone, CloneRef, Debug, Default, Eq, PartialEq)] pub struct EdgeEndpoint { pub node_id: NodeId, pub port: span_tree::Crumbs, diff --git a/app/ide-desktop/lib/content/src/index.ts b/app/ide-desktop/lib/content/src/index.ts index daee37ff938..c4e47a4ed22 100644 --- a/app/ide-desktop/lib/content/src/index.ts +++ b/app/ide-desktop/lib/content/src/index.ts @@ -816,6 +816,7 @@ class Config { public authentication_enabled: boolean public email: string public application_config_url: string + public rust_new_presentation_layer: boolean static default() { let config = new Config() @@ -878,6 +879,9 @@ class Config { this.application_config_url = ok(other.application_config_url) ? tryAsString(other.application_config_url) : this.application_config_url + this.rust_new_presentation_layer = ok(other.rust_new_presentation_layer) + ? tryAsBoolean(other.rust_new_presentation_layer) + : this.rust_new_presentation_layer } } diff --git a/lib/rust/frp/src/nodes.rs b/lib/rust/frp/src/nodes.rs index 5e8d3841ddd..52b34e7dc29 100644 --- a/lib/rust/frp/src/nodes.rs +++ b/lib/rust/frp/src/nodes.rs @@ -892,6 +892,16 @@ impl Network { { self.register(OwnedAllWith8::new(label, t1, t2, t3, t4, t5, t6, t7, t8, f)) } + + + // === Repeat === + + /// Repeat node listens for input events of type [`usize`] and emits events in number equal to + /// the input event value. + pub fn repeat(&self, label: Label, src: &T) -> Stream<()> + where T: EventOutput { + self.register(OwnedRepeat::new(label, src)) + } } @@ -3839,3 +3849,45 @@ impl Debug for AllWith8Data { + #[allow(dead_code)] + /// This is not accessed in this implementation but it needs to be kept so the source struct + /// stays alive at least as long as this struct. + event: T, + // behavior: watch::Ref, +} +pub type OwnedRepeat = stream::Node>; +pub type Repeat = stream::WeakNode>; + +impl HasOutput for RepeatData { + type Output = (); +} + +impl OwnedRepeat +where T: EventOutput +{ + /// Constructor. + pub fn new(label: Label, src: &T) -> Self { + let event = src.clone_ref(); + let definition = RepeatData { event }; + Self::construct_and_connect(label, src, definition) + } +} + +impl stream::EventConsumer for OwnedRepeat +where T: EventOutput +{ + fn on_event(&self, stack: CallStack, event: &Output) { + for _ in 0..*event { + self.emit_event(stack, &()) + } + } +}