diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4171fbf86fa..a8de76f3fe3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,7 @@ Cargo.lock @MichaelMauderer @4e6 @mwu-tow @farmaazon Cargo.toml @MichaelMauderer @4e6 @mwu-tow @farmaazon /lib/rust/ @MichaelMauderer @4e6 @mwu-tow @farmaazon @wdanilo /lib/rust/ensogl/ @MichaelMauderer @wdanilo @farmaazon +/integration-test/ @MichaelMauderer @wdanilo @farmaazon # Scala Libraries /lib/scala/ @4e6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 439b0cfa988..3ae1c3a290a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ #### Visual Environment +- [Nodes can be added to the graph by clicking (+) button on the screen][3278]. + The button is in the bottom-left corner. Node is added at the center or pushed + down if the center is already occupied by nodes. - [Maximum zoom factor is limited to 1.0x if IDE is not in Debug Mode.][3273] - [Debug Mode for Graph Editor can be activated/deactivated using a shortcut.][3264] It allows access to a set of restricted features. See @@ -89,6 +92,7 @@ [3259]: https://github.com/enso-org/enso/pull/3259 [3273]: https://github.com/enso-org/enso/pull/3273 [3276]: https://github.com/enso-org/enso/pull/3276 +[3278]: https://github.com/enso-org/enso/pull/3278 [3283]: https://github.com/enso-org/enso/pull/3283 [3282]: https://github.com/enso-org/enso/pull/3282 [3285]: https://github.com/enso-org/enso/pull/3285 diff --git a/Cargo.lock b/Cargo.lock index 76a7158afb9..31c6bb6fb70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1039,6 +1039,7 @@ dependencies = [ "enso-prelude", "enso-web", "ensogl", + "ordered-float 2.10.0", "wasm-bindgen", "wasm-bindgen-test", ] @@ -1242,10 +1243,19 @@ dependencies = [ "ensogl-text", ] +[[package]] +name = "ensogl-button" +version = "0.1.0" +dependencies = [ + "enso-frp", + "ensogl-core", +] + [[package]] name = "ensogl-component" version = "0.1.0" dependencies = [ + "ensogl-button", "ensogl-drop-down-menu", "ensogl-drop-manager", "ensogl-file-browser", diff --git a/app/gui/src/presenter/graph.rs b/app/gui/src/presenter/graph.rs index bf44de0ab39..7d3c945fd68 100644 --- a/app/gui/src/presenter/graph.rs +++ b/app/gui/src/presenter/graph.rs @@ -507,7 +507,7 @@ impl Graph { view.disable_visualization <+ disable_vis; view.add_node <+ update_data.map(|update| update.count_nodes_to_add()).repeat(); - added_node_update <- view.node_added.filter_map(f!((view_id) + 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()))); diff --git a/app/gui/src/presenter/project.rs b/app/gui/src/presenter/project.rs index 1bdfe91c2a7..9bdb485949e 100644 --- a/app/gui/src/presenter/project.rs +++ b/app/gui/src/presenter/project.rs @@ -9,7 +9,7 @@ use crate::presenter::graph::ViewNodeId; use enso_frp as frp; use ide_view as view; -use ide_view::project::ComponentBrowserOpenReason; +use ide_view::project::SearcherParams; @@ -67,7 +67,7 @@ impl Model { } } - fn setup_searcher_presenter(&self, way_of_opening_searcher: ComponentBrowserOpenReason) { + fn setup_searcher_presenter(&self, params: SearcherParams) { let new_presenter = presenter::Searcher::setup_controller( &self.logger, self.ide_controller.clone_ref(), @@ -75,7 +75,7 @@ impl Model { self.graph_controller.clone_ref(), &self.graph, self.view.clone_ref(), - way_of_opening_searcher, + params, ); match new_presenter { Ok(searcher) => { @@ -189,8 +189,10 @@ impl Project { let graph_view = &model.view.graph().frp; frp::extend! { network - eval view.searcher_opened ((way_of_opening_searcher) { - model.setup_searcher_presenter(*way_of_opening_searcher) + eval view.searcher ([model](params) { + if let Some(params) = params { + model.setup_searcher_presenter(*params) + } }); graph_view.remove_node <+ view.editing_committed.filter_map(f!([model]((node_view, entry)) { diff --git a/app/gui/src/presenter/searcher.rs b/app/gui/src/presenter/searcher.rs index 274e5896421..2c55cd34808 100644 --- a/app/gui/src/presenter/searcher.rs +++ b/app/gui/src/presenter/searcher.rs @@ -13,7 +13,7 @@ use crate::presenter::graph::ViewNodeId; use enso_frp as frp; use ide_view as view; use ide_view::graph_editor::component::node as node_view; -use ide_view::project::ComponentBrowserOpenReason; +use ide_view::project::SearcherParams; // ============== @@ -158,18 +158,18 @@ impl Searcher { graph_controller: controller::ExecutedGraph, graph_presenter: &presenter::Graph, view: view::project::View, - way_of_opening_searcher: ComponentBrowserOpenReason, + parameters: SearcherParams, ) -> FallibleResult { - let id = way_of_opening_searcher.node(); - let ast_node = graph_presenter.ast_node_of_view(id); + let SearcherParams { input, source_node } = parameters; + let ast_node = graph_presenter.ast_node_of_view(input); let mode = match ast_node { Some(node_id) => controller::searcher::Mode::EditNode { node_id }, None => { - let view_data = view.graph().model.nodes.get_cloned_ref(&id); + let view_data = view.graph().model.nodes.get_cloned_ref(&input); let position = view_data.map(|node| node.position().xy()); let position = position.map(|vector| model::module::Position { vector }); let source_node = - Self::source_node_ast_id(&view, graph_presenter, &way_of_opening_searcher); + source_node.and_then(|id| graph_presenter.ast_node_of_view(id.node)); controller::searcher::Mode::NewNode { position, source_node } } }; @@ -180,7 +180,7 @@ impl Searcher { graph_controller, mode, )?; - Ok(Self::new(parent, searcher_controller, view, id)) + Ok(Self::new(parent, searcher_controller, view, input)) } /// Commit editing. @@ -207,23 +207,4 @@ impl Searcher { let entry = controller.actions().list().and_then(|l| l.get_cloned(entry)); entry.map_or(false, |e| matches!(e.action, Example(_))) } - - /// Return the AST id of the source node. Source node is either: - /// 1. The source node of the connection that was dropped to create a node. - /// 2. The first of the selected nodes on the scene. - fn source_node_ast_id( - view: &view::project::View, - graph_presenter: &presenter::Graph, - way_of_opening_searcher: &ComponentBrowserOpenReason, - ) -> Option { - if let Some(edge_id) = way_of_opening_searcher.edge() { - let edge = view.graph().model.edges.get_cloned_ref(&edge_id); - let edge_source = edge.and_then(|edge| edge.source()); - let source_node_id = edge_source.map(|source| source.node_id); - source_node_id.and_then(|id| graph_presenter.ast_node_of_view(id)) - } else { - let selected_views = view.graph().model.nodes.all_selected(); - selected_views.iter().find_map(|view| graph_presenter.ast_node_of_view(*view)) - } - } } diff --git a/app/gui/view/debug_scene/interface/src/lib.rs b/app/gui/view/debug_scene/interface/src/lib.rs index 25a4b109228..73d9e0d1997 100644 --- a/app/gui/view/debug_scene/interface/src/lib.rs +++ b/app/gui/view/debug_scene/interface/src/lib.rs @@ -128,9 +128,9 @@ fn init(app: &Application) { // === Nodes === - let node1_id = graph_editor.add_node(); - let node2_id = graph_editor.add_node(); - let node3_id = graph_editor.add_node(); + let node1_id = graph_editor.model.add_node(); + let node2_id = graph_editor.model.add_node(); + let node3_id = graph_editor.model.add_node(); graph_editor.frp.set_node_position.emit((node1_id, Vector2(-150.0, 50.0))); graph_editor.frp.set_node_position.emit((node2_id, Vector2(50.0, 50.0))); @@ -145,7 +145,7 @@ fn init(app: &Application) { let expression_2 = expression_mock3(); graph_editor.frp.set_node_expression.emit((node2_id, expression_2.clone())); - let expression_3 = expression_mock2(); + let expression_3 = expression_mock3(); graph_editor.frp.set_node_expression.emit((node3_id, expression_3)); let kind = Immutable(graph_editor::component::node::error::Kind::Panic); let message = Rc::new(Some("Runtime Error".to_owned())); @@ -153,10 +153,10 @@ fn init(app: &Application) { let error = graph_editor::component::node::Error { kind, message, propagated }; graph_editor.frp.set_node_error_status.emit((node3_id, Some(error))); - let foo_node = graph_editor.add_node_below(node3_id); + let foo_node = graph_editor.model.add_node_below(node3_id); graph_editor.set_node_expression.emit((foo_node, Expression::new_plain("foo"))); - let baz_node = graph_editor.add_node_below(node3_id); + let baz_node = graph_editor.model.add_node_below(node3_id); graph_editor.set_node_expression.emit((baz_node, Expression::new_plain("baz"))); let (_, baz_position) = graph_editor.node_position_set.value(); let styles = StyleWatch::new(&scene.style_sheet); @@ -165,7 +165,7 @@ fn init(app: &Application) { let gap_for_bar_node = min_spacing + gap_between_nodes + f32::EPSILON; graph_editor.set_node_position((baz_node, baz_position + Vector2(gap_for_bar_node, 0.0))); - let bar_node = graph_editor.add_node_below(node3_id); + let bar_node = graph_editor.model.add_node_below(node3_id); graph_editor.set_node_expression.emit((bar_node, Expression::new_plain("bar"))); @@ -179,9 +179,9 @@ fn init(app: &Application) { // === VCS === - let dummy_node_added_id = graph_editor.add_node(); - let dummy_node_edited_id = graph_editor.add_node(); - let dummy_node_unchanged_id = graph_editor.add_node(); + let dummy_node_added_id = graph_editor.model.add_node(); + let dummy_node_edited_id = graph_editor.model.add_node(); + let dummy_node_unchanged_id = graph_editor.model.add_node(); graph_editor.frp.set_node_position.emit((dummy_node_added_id, Vector2(-450.0, 50.0))); graph_editor.frp.set_node_position.emit((dummy_node_edited_id, Vector2(-450.0, 125.0))); @@ -341,6 +341,7 @@ pub fn expression_mock() -> Expression { Expression { pattern, code, whole_expression_id, input_span_tree, output_span_tree } } +// TODO[ao] This expression mocks results in panic. If you want to use it, please fix it first. pub fn expression_mock2() -> Expression { let pattern = Some("var1".to_string()); let pattern_cr = vec![Seq { right: false }, Or, Or, Build]; diff --git a/app/gui/view/graph-editor/src/component.rs b/app/gui/view/graph-editor/src/component.rs index 8f555995dce..43d2996c3d7 100644 --- a/app/gui/view/graph-editor/src/component.rs +++ b/app/gui/view/graph-editor/src/component.rs @@ -5,6 +5,7 @@ // === Export === // ============== +pub mod add_node_button; pub mod breadcrumbs; pub mod edge; pub mod node; diff --git a/app/gui/view/graph-editor/src/free_place_finder.rs b/app/gui/view/graph-editor/src/free_place_finder.rs new file mode 100644 index 00000000000..909e178aa79 --- /dev/null +++ b/app/gui/view/graph-editor/src/free_place_finder.rs @@ -0,0 +1,257 @@ +//! The module containing an algorithm for searching free space for some point going at the specific +//! direction. +//! +//! This is used in Graph Editor to find unoccupied place for newly created node. + +use crate::prelude::*; + +use ordered_float::OrderedFloat; + + + +// ==================== +// === OccupiedArea === +// ==================== + +/// The structure describing an occupied area. +/// +/// All such areas are rectangles described by x and y ranges. The (x1, x2) and (y1, y2) pairs are +/// not sorted - if you want to get the lesser/greater one, use one of [`left`], [`right`], [`top`], +/// or [`bottom`] methods. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct OccupiedArea { + pub x1: f32, + pub y1: f32, + pub x2: f32, + pub y2: f32, +} + +impl OccupiedArea { + /// Get x position of the left boundary. + pub fn left(&self) -> f32 { + min(self.x1, self.x2) + } + + /// Get x position of the right boundary. + pub fn right(&self) -> f32 { + max(self.x1, self.x2) + } + + /// Get y position of the top boundary. + pub fn top(&self) -> f32 { + max(self.y1, self.y2) + } + + /// Get y position of the bottom boundary. + pub fn bottom(&self) -> f32 { + min(self.y1, self.y2) + } + + /// Check if the rectangle contains given point. + /// + /// The boundaries are open - they are not considered occupied. + pub fn contains(&self, point: Vector2) -> bool { + (self.x1 - point.x) * (self.x2 - point.x) < 0.0 + && (self.y1 - point.y) * (self.y2 - point.y) < 0.0 + } + + /// Return the x position of the left or right boundary, depending on whether the direction + /// points leftwards of rightwards respectively. Returns [`None`] if direction does not point + /// leftwards nor rightwards. + pub fn x_bound_following_direction(&self, direction: Vector2) -> Option { + if direction.x > f32::EPSILON { + Some(self.right()) + } else if direction.x < -f32::EPSILON { + Some(self.left()) + } else { + None + } + } + + /// Return the y position of the top or bottom boundary, depending on whether the direction + /// points toward the top or bottom respectively. Returns [`None`] if direction does not point + /// toward top nor bottom. + pub fn y_bound_following_direction(&self, direction: Vector2) -> Option { + if direction.y > f32::EPSILON { + Some(self.top()) + } else if direction.y < -f32::EPSILON { + Some(self.bottom()) + } else { + None + } + } + + /// Return the point where the ray going from starting point along the `direction` vector will + /// intersect with area boundaries, if there is exactly one such point, otherwise the result is + /// unspecified. + pub fn boundary_intersection(&self, starting_point: Vector2, direction: Vector2) -> Vector2 { + let x_bound = self.x_bound_following_direction(direction); + let dir_x_factor = x_bound.map(|x| (x - starting_point.x) / direction.x); + let y_bound = self.y_bound_following_direction(direction); + let dir_y_factor = y_bound.map(|y| (y - starting_point.y) / direction.y); + let dir_factor = match (dir_x_factor, dir_y_factor) { + (Some(x), Some(y)) => min(x, y), + (Some(x), None) => x, + (None, Some(y)) => y, + _ => default(), + }; + starting_point + direction * dir_factor + } +} + + + +// ======================= +// === find_free_place === +// ======================= + +/// With the list of occupied areas, return the first unoccupied point when going along the ray +/// starting from `starting_point` and parallel to `direction` vector. +/// +/// Returns [`None`] if the `direction` does not go clearly at any direction (both `direction.x` and +/// `direction.y` are smaller than [`f32::EPSILON`]). +pub fn find_free_place( + starting_point: Vector2, + direction: Vector2, + occupied: impl IntoIterator, +) -> Option { + let valid_dir = direction.x.abs() > f32::EPSILON || direction.y.abs() > f32::EPSILON; + valid_dir.as_some_from(move || { + let sorted_areas = occupied.into_iter().sorted_by_key(|area| { + let x = area.x_bound_following_direction(-direction).unwrap_or(0.0); + let y = area.y_bound_following_direction(-direction).unwrap_or(0.0); + OrderedFloat(x * direction.x + y * direction.y) + }); + let mut current_point = starting_point; + for area in sorted_areas { + if area.contains(current_point) { + current_point = area.boundary_intersection(current_point, direction) + } + } + current_point + }) +} + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn zero_direction_is_not_allowed() { + assert!(find_free_place(default(), Vector2(0.0, 0.0), vec![]).is_none()); + } + + #[derive(Clone, Debug)] + struct Case { + starting_point: Vector2, + direction: Vector2, + occupied: Vec, + expected_result: Vector2, + } + + impl Case { + fn run(&self) { + let occupied = self.occupied.iter().cloned(); + let result = find_free_place(self.starting_point, self.direction, occupied).unwrap(); + assert_eq!(result, self.expected_result, "Case {:?} gave wrong result.", self); + } + + fn flip(&self, on_x: bool, on_y: bool) -> Self { + let x_factor = if on_x { -1.0 } else { 1.0 }; + let y_factor = if on_y { -1.0 } else { 1.0 }; + let factor = Vector2(x_factor, y_factor); + Case { + starting_point: self.starting_point.component_mul(&factor), + direction: self.direction.component_mul(&factor), + occupied: self + .occupied + .iter() + .map(|area| OccupiedArea { + x1: area.x1 * factor.x, + x2: area.x2 * factor.x, + y1: area.y1 * factor.y, + y2: area.y2 * factor.y, + }) + .collect(), + expected_result: self.expected_result.component_mul(&factor), + } + } + + fn run_each_flip(&self) { + self.run(); + self.flip(true, false).run(); + self.flip(false, true).run(); + self.flip(true, true).run(); + } + } + + #[test] + fn already_in_unoccupied() { + // s> - starting point moving right. + // X - occupied areas. + // +---+ + // |XXX| + // +-+---+-+ + // |X| |X| + // |X| s>|X| + // |X| |X| + // +-+---+-+ + // |XXX| + // +---+ + let case = Case { + starting_point: Vector2(1.0, -1.0), + direction: Vector2(1.0, 0.0), + occupied: vec![ + OccupiedArea { x1: 2.0, x2: 3.0, y1: 0.0, y2: -2.0 }, + OccupiedArea { x1: -1.0, x2: 0.0, y1: 0.0, y2: -2.0 }, + OccupiedArea { x1: 0.0, x2: 2.0, y1: 1.0, y2: 0.0 }, + OccupiedArea { x1: 0.0, x2: 2.0, y1: -2.0, y2: -3.0 }, + ], + expected_result: Vector2(1.0, -1.0), + }; + case.run_each_flip() + } + + #[test] + fn single_area() { + let orthogonal = Case { + starting_point: Vector2(10.0, 10.0), + direction: Vector2(1.0, 0.0), + occupied: vec![OccupiedArea { x1: 0.0, x2: 20.0, y1: 15.0, y2: 5.0 }], + expected_result: Vector2(20.0, 10.0), + }; + let non_orthogonal = Case { + direction: Vector2(18.0, 15.0), + expected_result: Vector2(16.0, 15.0), + ..orthogonal.clone() + }; + orthogonal.run_each_flip(); + non_orthogonal.run_each_flip(); + } + + #[test] + fn overlapping_areas() { + let case = Case { + starting_point: Vector2(11.0, 0.0), + direction: Vector2(1.0, 0.0), + occupied: vec![ + OccupiedArea { x1: 10.0, x2: 20.0, y1: -1.0, y2: 1.0 }, + OccupiedArea { x1: 15.0, x2: 25.0, y1: -1.0, y2: 1.0 }, + OccupiedArea { x1: 20.0, x2: 30.0, y1: -1.0, y2: 0.0 }, + OccupiedArea { x1: 25.0, x2: 30.0, y1: -1.0, y2: 1.0 }, + ], + expected_result: Vector2(25.0, 0.0), + }; + let reversed_areas_list = + Case { occupied: case.occupied.iter().rev().cloned().collect(), ..case.clone() }; + case.run_each_flip(); + reversed_areas_list.run_each_flip(); + } +} diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index eb89ac5b32e..9fc373cd90e 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -31,6 +31,8 @@ pub mod component; pub mod builtin; pub mod data; #[warn(missing_docs)] +pub mod free_place_finder; +#[warn(missing_docs)] pub mod profiling; #[warn(missing_docs)] pub mod view; @@ -46,6 +48,8 @@ use crate::component::visualization; use crate::component::visualization::instance::PreprocessorConfiguration; use crate::component::visualization::MockDataGenerator3D; use crate::data::enso; +use crate::free_place_finder::find_free_place; +use crate::free_place_finder::OccupiedArea; pub use crate::node::profiling::Status as NodeProfilingStatus; use enso_config::ARGS; @@ -68,7 +72,6 @@ use ensogl::Animation; use ensogl::DEPRECATED_Animation; use ensogl::DEPRECATED_Tween; use ensogl_hardcoded_theme as theme; -use ordered_float::OrderedFloat; @@ -398,6 +401,17 @@ impl SharedHashMap { // === FrpInputs === // ================= +/// The information about data source hinted by node creation process. For example, when creating +/// node by dropping edge, the source port should be a source for newly created node. +/// +/// This is information meant to be sent to searcher, which can, for example, auto- connect the +/// source to "this" port of new node. +#[derive(Clone, CloneRef, Copy, Debug, Default, Eq, PartialEq)] +pub struct NodeSource { + #[allow(missing_docs)] + pub node: NodeId, +} + ensogl::define_endpoints! { Input { // === General === @@ -447,8 +461,10 @@ ensogl::define_endpoints! { toggle_node_inverse_select(), /// Set the node as selected. Ignores selection mode. + // WARNING: not implemented select_node (NodeId), /// Set the node as deselected. Ignores selection mode. + // WARNING: not implemented deselect_node (NodeId), @@ -466,8 +482,17 @@ ensogl::define_endpoints! { /// Add a new node and place it in the origin of the workspace. add_node(), - /// Add a new node and place it at the mouse cursor position. - add_node_at_cursor(), + /// Start Node creation process. + /// + /// This event is the best to be emit in situations, when the user want to create node (in + /// opposition to e.g. loading graph from file). It will create node and put it into edit + /// mode. The node position may vary, depending on what is the best for the UX - for details + /// see [`GraphEditorModel::create_node`] implementation. + start_node_creation(), + + + + /// Remove all selected nodes from the graph. remove_selected_nodes(), /// Remove all nodes from the graph. @@ -621,7 +646,7 @@ ensogl::define_endpoints! { // === Other === // FIXME: To be refactored - node_added (NodeId), + node_added (NodeId, Option, bool), node_removed (NodeId), nodes_collapsed ((Vec,NodeId)), node_hovered (Option>), @@ -1072,7 +1097,7 @@ impl Nodes { impl Nodes { /// Mark node as selected and send FRP event to node about its selection status. - fn select(&self, node_id: impl Into) { + pub fn select(&self, node_id: impl Into) { let node_id = node_id.into(); if let Some(node) = self.get_cloned_ref(&node_id) { // Remove previous instances and add new selection at end of the list, indicating that @@ -1086,7 +1111,7 @@ impl Nodes { } /// Mark node as deselected and send FRP event to node about its selection status. - fn deselect(&self, node_id: impl Into) { + pub fn deselect(&self, node_id: impl Into) { let node_id = node_id.into(); if let Some(node) = self.get_cloned_ref(&node_id) { self.selected.remove_item(&node_id); @@ -1270,25 +1295,155 @@ impl Deref for GraphEditorModelWithNetwork { } } -/// Context data required to create a new node. -#[derive(Debug)] -struct NodeCreationContext<'a> { - pointer_style: &'a frp::Source, - tooltip_update: &'a frp::Source, - output_press: &'a frp::Source, - input_press: &'a frp::Source, - output: &'a FrpEndpoints, -} impl GraphEditorModelWithNetwork { - #[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented. + /// Constructor. pub fn new(app: &Application, cursor: cursor::Cursor, frp: &Frp) -> Self { let network = frp.network.clone_ref(); // FIXME make weak let model = GraphEditorModel::new(app, cursor, frp); Self { model, network } } - fn new_node(&self, ctx: &NodeCreationContext) -> NodeId { + fn is_node_connected_at_input(&self, node_id: NodeId, crumbs: &span_tree::Crumbs) -> bool { + if let Some(node) = self.nodes.get_cloned(&node_id) { + for in_edge_id in node.in_edges.raw.borrow().iter() { + if let Some(edge) = self.edges.get_cloned(in_edge_id) { + if let Some(target) = edge.target() { + if target.node_id == node_id && target.port == crumbs { + return true; + } + } + } + } + } + false + } + + /// Return a position of the node with provided id. + pub fn get_node_position(&self, node_id: NodeId) -> Option> { + self.nodes.get_cloned_ref(&node_id).map(|node| node.position()) + } + + fn create_edge( + &self, + edge_click: &frp::Source, + edge_over: &frp::Source, + edge_out: &frp::Source, + ) -> EdgeId { + let edge = Edge::new(component::Edge::new(&self.app)); + let edge_id = edge.id(); + self.add_child(&edge); + self.edges.insert(edge.clone_ref()); + + let network = &self.network; + + frp::extend! { network + eval_ edge.view.frp.shape_events.mouse_down ( edge_click.emit(edge_id)); + eval_ edge.view.frp.shape_events.mouse_over ( edge_over.emit(edge_id)); + eval_ edge.view.frp.shape_events.mouse_out ( edge_out.emit(edge_id)); + } + + edge_id + } + + fn new_edge_from_output( + &self, + edge_click: &frp::Source, + edge_over: &frp::Source, + edge_out: &frp::Source, + ) -> EdgeId { + let edge_id = self.create_edge(edge_click, edge_over, edge_out); + let first_detached = self.edges.detached_target.is_empty(); + self.edges.detached_target.insert(edge_id); + if first_detached { + self.frp.source.on_some_edges_targets_unset.emit(()); + } + edge_id + } + + fn new_edge_from_input( + &self, + edge_click: &frp::Source, + edge_over: &frp::Source, + edge_out: &frp::Source, + ) -> EdgeId { + let edge_id = self.create_edge(edge_click, edge_over, edge_out); + let first_detached = self.edges.detached_source.is_empty(); + self.edges.detached_source.insert(edge_id); + if first_detached { + self.frp.source.on_some_edges_sources_unset.emit(()); + } + edge_id + } +} + + +// === Node Creation === + +#[derive(Clone, Copy, Debug)] +enum WayOfCreatingNode { + /// "add_node" FRP event was emitted. + AddNodeEvent, + /// "start_node_creation" FRP event was emitted. + StartCreationEvent, + /// add_node_button was clicked. + ClickingButton, + /// The edge was dropped on the stage. + DroppingEdge { edge_id: EdgeId }, +} + +impl Default for WayOfCreatingNode { + fn default() -> Self { + Self::AddNodeEvent + } +} + +/// Context data required to create a new node. +#[derive(Debug)] +struct NodeCreationContext<'a> { + pointer_style: &'a frp::Any<(NodeId, cursor::Style)>, + tooltip_update: &'a frp::Any<(NodeId, tooltip::Style)>, + output_press: &'a frp::Source, + input_press: &'a frp::Source, + output: &'a FrpEndpoints, +} + +impl GraphEditorModelWithNetwork { + fn create_node( + &self, + ctx: &NodeCreationContext, + way: WayOfCreatingNode, + mouse_position: Vector2, + ) -> (NodeId, Option, bool) { + use WayOfCreatingNode::*; + let should_edit = !matches!(way, AddNodeEvent); + let selection = self.nodes.selected.first_cloned(); + let source_node = match way { + AddNodeEvent => None, + StartCreationEvent | ClickingButton => selection, + DroppingEdge { edge_id } => self.edge_source_node_id(edge_id), + }; + let source = source_node.map(|node| NodeSource { node }); + let screen_center = + self.scene().screen_to_object_space(&self.display_object, Vector2(0.0, 0.0)); + let position: Vector2 = match way { + AddNodeEvent => default(), + StartCreationEvent | ClickingButton if selection.is_some() => + self.find_free_place_under(selection.unwrap()), + StartCreationEvent => mouse_position, + ClickingButton => + self.find_free_place_for_node(screen_center, Vector2(0.0, -1.0)).unwrap(), + DroppingEdge { .. } => mouse_position, + }; + let node = self.new_node(ctx); + node.set_position_xy(position); + if should_edit { + node.view.set_expression(node::Expression::default()); + } + (node.id(), source, should_edit) + } + + fn new_node(&self, ctx: &NodeCreationContext) -> Node { let view = component::Node::new(&self.app, self.vis_registry.clone_ref()); let node = Node::new(view); let node_id = node.id(); @@ -1316,8 +1471,8 @@ impl GraphEditorModelWithNetwork { node.set_output_expression_visibility <+ self.frp.nodes_labels_visible; - eval node.frp.tooltip ((tooltip) tooltip_update.emit(tooltip)); - eval node.model.input.frp.pointer_style ((style) pointer_style.emit(style)); + tooltip_update <+ node.frp.tooltip.map(move |tooltip| (node_id, tooltip.clone())); + pointer_style <+ node.model.input.frp.pointer_style.map(move |s| (node_id, s.clone())); eval node.model.output.frp.on_port_press ([output_press](crumbs){ let target = EdgeEndpoint::new(node_id,crumbs.clone()); output_press.emit(target); @@ -1428,80 +1583,8 @@ impl GraphEditorModelWithNetwork { }; metadata.emit(initial_metadata); init.emit(&()); - self.nodes.insert(node_id, node); - node_id - } - - fn is_node_connected_at_input(&self, node_id: NodeId, crumbs: &span_tree::Crumbs) -> bool { - if let Some(node) = self.nodes.get_cloned(&node_id) { - for in_edge_id in node.in_edges.raw.borrow().iter() { - if let Some(edge) = self.edges.get_cloned(in_edge_id) { - if let Some(target) = edge.target() { - if target.node_id == node_id && target.port == crumbs { - return true; - } - } - } - } - } - false - } - - #[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented. - pub fn get_node_position(&self, node_id: NodeId) -> Option> { - self.nodes.get_cloned_ref(&node_id).map(|node| node.position()) - } - - fn create_edge( - &self, - edge_click: &frp::Source, - edge_over: &frp::Source, - edge_out: &frp::Source, - ) -> EdgeId { - let edge = Edge::new(component::Edge::new(&self.app)); - let edge_id = edge.id(); - self.add_child(&edge); - self.edges.insert(edge.clone_ref()); - - let network = &self.network; - - frp::extend! { network - eval_ edge.view.frp.shape_events.mouse_down ( edge_click.emit(edge_id)); - eval_ edge.view.frp.shape_events.mouse_over ( edge_over.emit(edge_id)); - eval_ edge.view.frp.shape_events.mouse_out ( edge_out.emit(edge_id)); - } - - edge_id - } - - fn new_edge_from_output( - &self, - edge_click: &frp::Source, - edge_over: &frp::Source, - edge_out: &frp::Source, - ) -> EdgeId { - let edge_id = self.create_edge(edge_click, edge_over, edge_out); - let first_detached = self.edges.detached_target.is_empty(); - self.edges.detached_target.insert(edge_id); - if first_detached { - self.frp.source.on_some_edges_targets_unset.emit(()); - } - edge_id - } - - fn new_edge_from_input( - &self, - edge_click: &frp::Source, - edge_over: &frp::Source, - edge_out: &frp::Source, - ) -> EdgeId { - let edge_id = self.create_edge(edge_click, edge_over, edge_out); - let first_detached = self.edges.detached_source.is_empty(); - self.edges.detached_source.insert(edge_id); - if first_detached { - self.frp.source.on_some_edges_sources_unset.emit(()); - } - edge_id + self.nodes.insert(node_id, node.clone_ref()); + node } } @@ -1524,6 +1607,7 @@ pub struct GraphEditorModel { pub vis_registry: visualization::Registry, pub drop_manager: ensogl_drop_manager::Manager, pub navigator: Navigator, + pub add_node_button: Rc, // FIXME[MM]: The tooltip should live next to the cursor in `Application`. This does not // currently work, however, because the `Application` lives in enso-core, and the tooltip // requires enso-text, which in turn depends on enso-core, creating a cyclic dependency. @@ -1559,6 +1643,7 @@ impl GraphEditorModel { let tooltip = Tooltip::new(&app); let profiling_statuses = profiling::Statuses::new(); let profiling_button = component::profiling::Button::new(&app); + let add_node_button = Rc::new(component::add_node_button::AddNodeButton::new(&app)); let drop_manager = ensogl_drop_manager::Manager::new(&scene.dom.root); let styles_frp = StyleWatchFrp::new(&scene.style_sheet); let selection_controller = @@ -1581,6 +1666,7 @@ impl GraphEditorModel { navigator, profiling_statuses, profiling_button, + add_node_button, styles_frp, selection_controller, } @@ -1596,6 +1682,7 @@ impl GraphEditorModel { self.breadcrumbs.gap_width(traffic_lights_gap_width()); self.scene().add_child(&self.tooltip); self.add_child(&self.profiling_button); + self.add_child(&*self.add_node_button); self } @@ -1610,6 +1697,70 @@ impl GraphEditorModel { } +// === Add node === +impl GraphEditorModel { + /// Create a new node and return a unique identifier. + pub fn add_node(&self) -> NodeId { + self.frp.add_node.emit(()); + let (node_id, _, _) = self.frp.node_added.value(); + node_id + } + + /// Create a new node and place it at a free place below `above` node. + pub fn add_node_below(&self, above: NodeId) -> NodeId { + let pos = self.find_free_place_under(above); + self.add_node_at(pos) + } + + /// Create a new node and place it at `pos`. + pub fn add_node_at(&self, pos: Vector2) -> NodeId { + let node_id = self.add_node(); + self.frp.set_node_position((node_id, pos)); + node_id + } + + /// Return the first available position for a new node below `node_above` node. + pub fn find_free_place_under(&self, node_above: NodeId) -> Vector2 { + let above_pos = self.node_position(node_above); + let y_gap = self.frp.default_y_gap_between_nodes.value(); + let y_offset = y_gap + node::HEIGHT; + let starting_point = above_pos - Vector2(0.0, y_offset); + let direction = Vector2(-1.0, 0.0); + self.find_free_place_for_node(starting_point, direction).unwrap() + } + + /// Return the first unoccupied point when going along the ray starting from `starting_point` + /// and parallel to `direction` vector. + pub fn find_free_place_for_node( + &self, + starting_from: Vector2, + direction: Vector2, + ) -> Option { + let x_gap = self.frp.default_x_gap_between_nodes.value(); + let y_gap = self.frp.default_y_gap_between_nodes.value(); + // This is how much horizontal space we are looking for. + let min_spacing = self.frp.min_x_spacing_for_new_nodes.value(); + let nodes = self.nodes.all.raw.borrow(); + // The "occupied area" for given node consists of: + // - area taken by node view (obviously); + // - the minimum gap between nodes in all directions, so the new node won't be "glued" to + // another; + // - the new node size measured from origin point at each direction accordingly: because + // `find_free_place` looks for free place for the origin point, and we want to fit not + // only the point, but the whole node. + let node_areas = nodes.values().map(|node| { + let position = node.position(); + let left = position.x - x_gap - min_spacing; + let right = position.x + node.view.model.width() + x_gap; + let top = position.y + node::HEIGHT + y_gap; + let bottom = position.y - node::HEIGHT - y_gap; + OccupiedArea { x1: left, x2: right, y1: top, y2: bottom } + }); + find_free_place(starting_from, direction, node_areas) + } +} + + // === Remove === impl GraphEditorModel { @@ -1743,6 +1894,12 @@ impl GraphEditorModel { // === Connect === impl GraphEditorModel { + fn edge_source_node_id(&self, edge_id: EdgeId) -> Option { + let edge = self.edges.get_cloned_ref(&edge_id)?; + let endpoint = edge.source()?; + Some(endpoint.node_id) + } + fn set_edge_source(&self, edge_id: EdgeId, target: impl Into) { let target = target.into(); if let Some(edge) = self.edges.get_cloned_ref(&edge_id) { @@ -2228,57 +2385,6 @@ impl Deref for GraphEditor { } } -impl GraphEditor { - /// Add a new node and returns its ID. - pub fn add_node(&self) -> NodeId { - self.frp.add_node.emit(()); - self.frp.output.node_added.value() - } - - /// Ads a new node below `above` and returns its ID. If there is not enough space right below - /// `above` then the new node is moved to the right to first gap that is large enough. - pub fn add_node_below(&self, above: NodeId) -> NodeId { - let above_pos = self.model.get_node_position(above).unwrap_or_default(); - let x_gap = self.default_x_gap_between_nodes.value(); - let y_gap = self.default_y_gap_between_nodes.value(); - let y_offset = y_gap + node::HEIGHT; - let mut x = above_pos.x; - let y = above_pos.y - y_offset; - // Push x to the right until we find a position where we have enough space for the new - // node, including a margin of size `x_gap`/`y_gap` on all sides. - { - let nodes = self.model.nodes.all.raw.borrow(); - // `y_offset` is exactly the distance between `parent` and the new node. At this - // distance, `parent` should not count as overlapping with the new node. But we might - // get this wrong in the presence of rounding errors. To avoid this, we use - // `f32::EPSILON` as an error margin. - let maybe_overlapping = nodes - .values() - .filter(|node| (node.position().y - y).abs() < y_offset - f32::EPSILON); - let maybe_overlapping = - maybe_overlapping.sorted_by_key(|n| OrderedFloat(n.position().x)); - // This is how much horizontal space we are looking for. - let min_spacing = self.min_x_spacing_for_new_nodes.value(); - for node in maybe_overlapping { - let node_left = node.position().x - x_gap; - let node_right = node.position().x + node.view.model.width() + x_gap; - if x + min_spacing > node_left { - x = x.max(node_right); - } else { - // Since `maybe_overlapping` is sorted, we know that the if-condition will - // be false for all following `node`s as well. Therefore, we can skip the - // remaining iterations. - break; - } - } - } - let pos = Vector2(x, y); - let node_id = self.add_node(); - self.set_node_position((node_id, pos)); - node_id - } -} - impl application::View for GraphEditor { fn label() -> &'static str { "GraphEditor" @@ -2295,6 +2401,7 @@ impl application::View for GraphEditor { fn default_shortcuts() -> Vec { use shortcut::ActionType::*; (&[ + (Press, "!node_editing", "tab", "start_node_creation"), // === Drag === (Press, "", "left-mouse-button", "node_press"), (Release, "", "left-mouse-button", "node_release"), @@ -2527,12 +2634,12 @@ fn new_graph_editor(app: &Application) -> GraphEditor { } - // === Add Node === + // === Mouse Interactions === frp::extend! { network - node_pointer_style <- source::(); - node_tooltip <- source::(); + node_pointer_style <- any_mut::<(NodeId, cursor::Style)>(); + node_tooltip <- any_mut::<(NodeId, tooltip::Style)>(); let node_input_touch = TouchNetwork::::new(network,mouse); let node_output_touch = TouchNetwork::::new(network,mouse); @@ -2557,49 +2664,6 @@ fn new_graph_editor(app: &Application) -> GraphEditor { } - // === Node Editing === - - frp::extend! { network - // Clicking on background either drops dragged edge or aborts node editing. - let background_selected = &touch.background.selected; - was_edge_detached_when_background_selected <- has_detached_edge.sample(background_selected); - clicked_to_drop_edge <- was_edge_detached_when_background_selected.on_true(); - clicked_to_abort_edit <- was_edge_detached_when_background_selected.on_false(); - - node_in_edit_mode <- out.node_being_edited.map(|n| n.is_some()); - edit_mode <- bool(&inputs.edit_mode_off,&inputs.edit_mode_on); - node_to_edit <- touch.nodes.down.gate(&edit_mode); - edit_node <- any(&node_to_edit,&inputs.edit_node); - stop_edit_on_bg_click <- clicked_to_abort_edit.gate(&node_in_edit_mode); - stop_edit <- any(&stop_edit_on_bg_click,&inputs.stop_editing); - edit_switch <- edit_node.gate(&node_in_edit_mode); - node_being_edited <- out.node_being_edited.map(|n| n.unwrap_or_default()); - - // The "finish" events must be emitted before "start", to properly cover the "switch" case. - out.source.node_editing_finished <+ node_being_edited.sample(&stop_edit); - out.source.node_editing_finished <+ node_being_edited.sample(&edit_switch); - out.source.node_editing_started <+ edit_node; - - out.source.node_being_edited <+ out.node_editing_started.map(|n| Some(*n));; - out.source.node_being_edited <+ out.node_editing_finished.constant(None); - out.source.node_editing <+ out.node_being_edited.map(|t|t.is_some()); - - out.source.node_edit_mode <+ edit_mode; - out.source.nodes_labels_visible <+ out.node_edit_mode || node_in_edit_mode; - - eval out.node_editing_started ([model] (id) { - if let Some(node) = model.nodes.get_cloned_ref(id) { - node.model.input.frp.set_edit_mode(true); - } - }); - eval out.node_editing_finished ([model](id) { - if let Some(node) = model.nodes.get_cloned_ref(id) { - node.model.input.set_edit_mode(false); - } - }); - } - - // === Edge interactions === frp::extend! { network @@ -2697,48 +2761,138 @@ fn new_graph_editor(app: &Application) -> GraphEditor { out.source.on_edge_add <+ new_input_edge; new_edge_target <- new_input_edge.map2(&node_input_touch.down, move |id,target| (*id,target.clone())); out.source.on_edge_target_set <+ new_edge_target; + } + // === Edge Connect === - // ====================== - // === Node Creation === - // ====================== + frp::extend! { network - let add_node_at_cursor = inputs.add_node_at_cursor.clone_ref(); - add_node <- any (inputs.add_node,add_node_at_cursor); - new_node <- add_node.map(f_!([model,node_pointer_style,node_tooltip,out] { - let ctx = NodeCreationContext { - pointer_style : &node_pointer_style, - tooltip_update : &node_tooltip, - output_press : &node_output_touch.down, - input_press : &node_input_touch.down, - output : &out, - }; - model.new_node(&ctx) - })); - out.source.node_added <+ new_node; + // Clicking on background either drops dragged edge or aborts node editing. + let background_selected = &touch.background.selected; + was_edge_detached_when_background_selected <- has_detached_edge.sample(background_selected); + clicked_to_drop_edge <- was_edge_detached_when_background_selected.on_true(); + clicked_to_abort_edit <- was_edge_detached_when_background_selected.on_false(); + + out.source.on_edge_source_set <+ inputs.set_edge_source; + out.source.on_edge_target_set <+ inputs.set_edge_target; + + let endpoints = inputs.connect_nodes.clone_ref(); + edge <- endpoints . map(f_!(model.new_edge_from_output(&edge_mouse_down,&edge_over,&edge_out))); + new_edge_source <- endpoints . _0() . map2(&edge, |t,id| (*id,t.clone())); + new_edge_target <- endpoints . _1() . map2(&edge, |t,id| (*id,t.clone())); + out.source.on_edge_add <+ edge; + out.source.on_edge_source_set <+ new_edge_source; + out.source.on_edge_target_set <+ new_edge_target; + + detached_edges_without_targets <= attach_all_edge_inputs.map(f_!(model.take_edges_with_detached_targets())); + detached_edges_without_sources <= attach_all_edge_outputs.map(f_!(model.take_edges_with_detached_sources())); + + new_edge_target <- detached_edges_without_targets.map2(&attach_all_edge_inputs, |id,t| (*id,t.clone())); + out.source.on_edge_target_set <+ new_edge_target; + new_edge_source <- detached_edges_without_sources.map2(&attach_all_edge_outputs, |id,t| (*id,t.clone())); + out.source.on_edge_source_set <+ new_edge_source; + + on_new_edge_source <- new_edge_source.constant(()); + on_new_edge_target <- new_edge_target.constant(()); + + overlapping_edges <= out.on_edge_target_set._1().map(f!((t) model.overlapping_edges(t))); + out.source.on_edge_drop <+ overlapping_edges; + + drop_on_bg_up <- background_up.gate(&connect_drag_mode); + drop_edges <- any (drop_on_bg_up,clicked_to_drop_edge); + + edge_dropped_to_create_node <= drop_edges.map(f_!(model.edges_with_detached_targets())); + out.source.on_edge_drop_to_create_node <+ edge_dropped_to_create_node; + + remove_all_detached_edges <- any (drop_edges, inputs.drop_dragged_edge); + edge_to_remove_without_targets <= remove_all_detached_edges.map(f_!(model.take_edges_with_detached_targets())); + edge_to_remove_without_sources <= remove_all_detached_edges.map(f_!(model.take_edges_with_detached_sources())); + edge_to_remove <- any(edge_to_remove_without_targets,edge_to_remove_without_sources); + eval edge_to_remove ((id) model.remove_edge(id)); + } + + // === Adding Node === + + frp::extend! { network + let node_added_with_button = model.add_node_button.clicked.clone_ref(); + + input_add_node_way <- inputs.add_node.constant(WayOfCreatingNode::AddNodeEvent); + input_start_creation_way <- inputs.start_node_creation.constant(WayOfCreatingNode::StartCreationEvent); + add_with_button_way <- node_added_with_button.constant(WayOfCreatingNode::ClickingButton); + add_with_edge_drop_way <- edge_dropped_to_create_node.map(|&edge_id| WayOfCreatingNode::DroppingEdge{edge_id}); + add_node_way <- any (input_add_node_way, input_start_creation_way, add_with_button_way, add_with_edge_drop_way); + + new_node <- add_node_way.map2(&cursor_pos_in_scene, f!([model,node_pointer_style,node_tooltip,out](way, mouse_pos) { + let ctx = NodeCreationContext { + pointer_style : &node_pointer_style, + tooltip_update : &node_tooltip, + output_press : &node_output_touch.down, + input_press : &node_input_touch.down, + output : &out, + }; + model.create_node(&ctx, *way, *mouse_pos) + })); + out.source.node_added <+ new_node.map(|&(id, src, should_edit)| (id, src, should_edit)); + node_to_edit_after_adding <- new_node.filter_map(|&(id,_,cond)| cond.as_some(id)); + } + + + // === Node Editing === + + frp::extend! { network + node_in_edit_mode <- out.node_being_edited.map(|n| n.is_some()); + edit_mode <- bool(&inputs.edit_mode_off,&inputs.edit_mode_on); + node_to_edit <- touch.nodes.down.gate(&edit_mode); + edit_node <- any(node_to_edit, node_to_edit_after_adding, inputs.edit_node); + stop_edit_on_bg_click <- clicked_to_abort_edit.gate(&node_in_edit_mode); + stop_edit <- any(&stop_edit_on_bg_click,&inputs.stop_editing); + edit_switch <- edit_node.gate(&node_in_edit_mode); + node_being_edited <- out.node_being_edited.map(|n| n.unwrap_or_default()); + + // The "finish" events must be emitted before "start", to properly cover the "switch" case. + out.source.node_editing_finished <+ node_being_edited.sample(&stop_edit); + out.source.node_editing_finished <+ node_being_edited.sample(&edit_switch); + out.source.node_editing_started <+ edit_node; + + out.source.node_being_edited <+ out.node_editing_started.map(|n| Some(*n));; + out.source.node_being_edited <+ out.node_editing_finished.constant(None); + out.source.node_editing <+ out.node_being_edited.map(|t|t.is_some()); + + out.source.node_edit_mode <+ edit_mode; + out.source.nodes_labels_visible <+ out.node_edit_mode || node_in_edit_mode; + + eval out.node_editing_started ([model] (id) { + if let Some(node) = model.nodes.get_cloned_ref(id) { + node.model.input.frp.set_edit_mode(true); + } + }); + eval out.node_editing_finished ([model](id) { + if let Some(node) = model.nodes.get_cloned_ref(id) { + node.model.input.set_edit_mode(false); + } + }); + } - node_with_position <- add_node_at_cursor.map3(&new_node,&cursor_pos_in_scene,|_,id,pos| (*id,*pos)); - out.source.node_position_set <+ node_with_position; - out.source.node_position_set_batched <+ node_with_position; // === Event Propagation === - // See the docs of `Node` to learn about how the graph - nodes event propagation works. - _eval <- all_with(&out.node_hovered,&edit_mode,f!([model](tgt,e) - if let Some(tgt) = tgt { - model.with_node(tgt.value,|t| t.model.input.set_edit_ready_mode(*e && tgt.is_on())); - } - )); - _eval <- all_with(&out.node_hovered,&out.some_edge_targets_unset,f!([model](tgt,ok) - if let Some(tgt) = tgt { - let node_id = tgt.value; - let edge_tp = model.first_detached_edge_source_type(); - let is_edge_source = model.has_edges_with_detached_targets(node_id); - let is_active = *ok && !is_edge_source && tgt.is_on(); - model.with_node(node_id,|t| t.model.input.set_ports_active(is_active,edge_tp)); - } - )); + // See the docs of `Node` to learn about how the graph - nodes event propagation works. + frp::extend! { network + _eval <- all_with(&out.node_hovered,&edit_mode,f!([model](tgt,e) + if let Some(tgt) = tgt { + model.with_node(tgt.value,|t| t.model.input.set_edit_ready_mode(*e && tgt.is_on())); + } + )); + _eval <- all_with(&out.node_hovered,&out.some_edge_targets_unset,f!([model](tgt,ok) + if let Some(tgt) = tgt { + let node_id = tgt.value; + let edge_tp = model.first_detached_edge_source_type(); + let is_edge_source = model.has_edges_with_detached_targets(node_id); + let is_active = *ok && !is_edge_source && tgt.is_on(); + model.with_node(node_id,|t| t.model.input.set_ports_active(is_active,edge_tp)); + } + )); } @@ -2753,50 +2907,6 @@ fn new_graph_editor(app: &Application) -> GraphEditor { eval freeze_edges (((edge_id,is_frozen)) model.set_edge_freeze(edge_id,*is_frozen) ); } - - // === Edge Connect === - - frp::extend! { network - - out.source.on_edge_source_set <+ inputs.set_edge_source; - out.source.on_edge_target_set <+ inputs.set_edge_target; - - let endpoints = inputs.connect_nodes.clone_ref(); - edge <- endpoints . map(f_!(model.new_edge_from_output(&edge_mouse_down,&edge_over,&edge_out))); - new_edge_source <- endpoints . _0() . map2(&edge, |t,id| (*id,t.clone())); - new_edge_target <- endpoints . _1() . map2(&edge, |t,id| (*id,t.clone())); - out.source.on_edge_add <+ edge; - out.source.on_edge_source_set <+ new_edge_source; - out.source.on_edge_target_set <+ new_edge_target; - - detached_edges_without_targets <= attach_all_edge_inputs.map(f_!(model.take_edges_with_detached_targets())); - detached_edges_without_sources <= attach_all_edge_outputs.map(f_!(model.take_edges_with_detached_sources())); - - new_edge_target <- detached_edges_without_targets.map2(&attach_all_edge_inputs, |id,t| (*id,t.clone())); - out.source.on_edge_target_set <+ new_edge_target; - new_edge_source <- detached_edges_without_sources.map2(&attach_all_edge_outputs, |id,t| (*id,t.clone())); - out.source.on_edge_source_set <+ new_edge_source; - - on_new_edge_source <- new_edge_source.constant(()); - on_new_edge_target <- new_edge_target.constant(()); - - overlapping_edges <= out.on_edge_target_set._1().map(f!((t) model.overlapping_edges(t))); - out.source.on_edge_drop <+ overlapping_edges; - - drop_on_bg_up <- background_up.gate(&connect_drag_mode); - drop_edges <- any (drop_on_bg_up,clicked_to_drop_edge); - - edge_dropped_to_create_node <= drop_edges.map(f_!(model.edges_with_detached_targets())); - out.source.on_edge_drop_to_create_node <+ edge_dropped_to_create_node; - - remove_all_detached_edges <- any (drop_edges, inputs.drop_dragged_edge); - edge_to_remove_without_targets <= remove_all_detached_edges.map(f_!(model.take_edges_with_detached_targets())); - edge_to_remove_without_sources <= remove_all_detached_edges.map(f_!(model.take_edges_with_detached_sources())); - edge_to_remove <- any(edge_to_remove_without_targets,edge_to_remove_without_sources); - eval edge_to_remove ((id) model.remove_edge(id)); - - } - // // // === Disabling self-connections === // @@ -2817,6 +2927,16 @@ fn new_graph_editor(app: &Application) -> GraphEditor { eval nodes_to_remove ((node_id) inputs.remove_all_node_edges.emit(node_id)); out.source.node_removed <+ nodes_to_remove; + + // Removed nodes lost their right to set cursor and tooltip styles. + pointer_style_setter_removed <- out.node_removed.map2(&node_pointer_style, + |removed,(setter, _)| removed == setter + ); + tooltip_setter_removed <- out.node_removed.map2(&node_tooltip, |removed, (setter, _)| + removed == setter + ); + node_pointer_style <+ out.node_removed.gate(&pointer_style_setter_removed).constant(default()); + node_tooltip <+ out.node_removed.gate(&tooltip_setter_removed).constant(default()); } @@ -3345,6 +3465,7 @@ fn new_graph_editor(app: &Application) -> GraphEditor { let breadcrumb_style = model.breadcrumbs.pointer_style.clone_ref(); let selection_style = selection_controller.cursor_style.clone_ref(); + node_pointer_style <- node_pointer_style._1(); pointer_style <- all [ pointer_on_drag , selection_style @@ -3385,7 +3506,7 @@ fn new_graph_editor(app: &Application) -> GraphEditor { frp::extend! { network eval cursor.frp.scene_position ((pos) model.tooltip.frp.set_location(pos.xy()) ); - eval node_tooltip ((tooltip_update) model.tooltip.frp.set_style(tooltip_update) ); + eval node_tooltip (((_,tooltip_update)) model.tooltip.frp.set_style(tooltip_update) ); quick_visualization_preview <- bool(&frp.disable_quick_visualization_preview, &frp.enable_quick_visualization_preview); diff --git a/app/gui/view/src/project.rs b/app/gui/view/src/project.rs index adc016a2995..14588f2c39f 100644 --- a/app/gui/view/src/project.rs +++ b/app/gui/view/src/project.rs @@ -10,7 +10,6 @@ use crate::debug_mode_popup::DEBUG_MODE_SHORTCUT; use crate::graph_editor::component::node; use crate::graph_editor::component::node::Expression; use crate::graph_editor::component::visualization; -use crate::graph_editor::EdgeId; use crate::graph_editor::GraphEditor; use crate::graph_editor::NodeId; use crate::open_dialog::OpenDialog; @@ -27,45 +26,7 @@ use ensogl::system::web::dom; use ensogl::Animation; use ensogl::DEPRECATED_Animation; use ensogl_hardcoded_theme::Theme; - - - -// ================================== -// === ComponentBrowserOpenReason === -// ================================== - -/// An enum describing how the component browser was opened. -#[derive(Clone, CloneRef, Copy, Debug, PartialEq)] -pub enum ComponentBrowserOpenReason { - /// New node was created by opening the component browser or the node is being edited. - NodeEditing(NodeId), - /// New node was created by dropping a dragged connection on the scene. - EdgeDropped(NodeId, EdgeId), -} - -impl ComponentBrowserOpenReason { - /// [`NodeId`] of the created/edited node. - pub fn node(&self) -> NodeId { - match self { - Self::NodeEditing(id) => *id, - Self::EdgeDropped(id, _) => *id, - } - } - - /// [`EdgeId`] of the edge that was dropped to create a node. - pub fn edge(&self) -> Option { - match self { - Self::NodeEditing(_) => None, - Self::EdgeDropped(_, id) => Some(*id), - } - } -} - -impl Default for ComponentBrowserOpenReason { - fn default() -> Self { - Self::NodeEditing(default()) - } -} +use ide_view_graph_editor::NodeSource; @@ -73,10 +34,28 @@ impl Default for ComponentBrowserOpenReason { // === FRP === // =========== +/// The parameters of the displayed searcher. +#[derive(Clone, Copy, Debug, Default)] +pub struct SearcherParams { + /// The node being an Expression Input. + pub input: NodeId, + /// The node being a source for the edited node data - usually it's output shall be a this port + /// for inserted expression. + pub source_node: Option, +} + +impl SearcherParams { + fn new_for_new_node(node_id: NodeId, source_node: Option) -> Self { + Self { input: node_id, source_node } + } + + fn new_for_edited_node(node_id: NodeId) -> Self { + Self { input: node_id, source_node: None } + } +} + ensogl::define_endpoints! { Input { - /// Open the searcher. - open_searcher(), /// Open the Open File or Project Dialog. show_open_dialog(), /// Close the searcher without taking any actions @@ -102,18 +81,18 @@ ensogl::define_endpoints! { } Output { - searcher_opened (ComponentBrowserOpenReason), - adding_new_node (bool), - is_searcher_opened (bool), - old_expression_of_edited_node (Expression), - editing_aborted (NodeId), - editing_committed (NodeId, Option), - open_dialog_shown (bool), - code_editor_shown (bool), - style (Theme), - fullscreen_visualization_shown (bool), - drop_files_enabled (bool), - debug_mode (bool), + searcher (Option), + is_searcher_opened (bool), + adding_new_node (bool), + old_expression_of_edited_node (Expression), + editing_aborted (NodeId), + editing_committed (NodeId, Option), + open_dialog_shown (bool), + code_editor_shown (bool), + style (Theme), + fullscreen_visualization_shown (bool), + drop_files_enabled (bool), + debug_mode (bool), } } @@ -256,14 +235,14 @@ impl Model { /// Update Searcher View - its visibility and position - when edited node changed. fn update_searcher_view( &self, - edited_node: Option, + searcher_parameters: Option, is_searcher_empty: bool, searcher_left_top_position: &DEPRECATED_Animation>, ) { - match edited_node { - Some(id) if !is_searcher_empty => { + match searcher_parameters { + Some(SearcherParams { input, .. }) if !is_searcher_empty => { self.searcher.show(); - let new_position = self.searcher_left_top_position_when_under_node(id); + let new_position = self.searcher_left_top_position_when_under_node(input); searcher_left_top_position.set_target_value(new_position); } _ => { @@ -272,32 +251,6 @@ impl Model { } } - /// Add a new node and start editing it. Place it below `node_above` if provided, otherwise - /// place it under the cursor. - fn add_node_and_edit(&self, node_above: Option) -> NodeId { - let graph_editor_inputs = &self.graph_editor.frp.input; - let node_id = if let Some(node_above) = node_above { - self.graph_editor.add_node_below(node_above) - } else { - graph_editor_inputs.add_node_at_cursor.emit(()); - self.graph_editor.frp.output.node_added.value() - }; - graph_editor_inputs.set_node_expression.emit(&(node_id, Expression::default())); - graph_editor_inputs.edit_node.emit(&node_id); - node_id - } - - fn add_node_by_opening_searcher(&self) -> ComponentBrowserOpenReason { - let node_above = self.graph_editor.model.nodes.selected.first_cloned(); - let node_id = self.add_node_and_edit(node_above); - ComponentBrowserOpenReason::NodeEditing(node_id) - } - - fn add_node_by_dropping_edge(&self, edge: EdgeId) -> ComponentBrowserOpenReason { - let node_id = self.add_node_and_edit(None); - ComponentBrowserOpenReason::EdgeDropped(node_id, edge) - } - fn show_fullscreen_visualization(&self, node_id: NodeId) { let node = self.graph_editor.model.model.nodes.all.get_cloned_ref(&node_id); if let Some(node) = node { @@ -470,29 +423,65 @@ impl View { }); + // === Closing Searcher + + frp.source.is_searcher_opened <+ frp.searcher.map(|s| s.is_some()); + last_searcher <- frp.searcher.filter_map(|&s| s); + + finished_with_searcher <- graph.node_editing_finished.gate(&frp.is_searcher_opened); + frp.source.searcher <+ frp.close_searcher.constant(None); + frp.source.searcher <+ searcher.editing_committed.constant(None); + frp.source.searcher <+ finished_with_searcher.constant(None); + + committed_in_searcher <- + searcher.editing_committed.map2(&last_searcher, |&entry, &s| (s.input, entry)); + aborted_in_searcher <- frp.close_searcher.map2(&last_searcher, |(), &s| s.input); + frp.source.editing_committed <+ committed_in_searcher; + frp.source.editing_committed <+ finished_with_searcher.map(|id| (*id,None)); + frp.source.editing_aborted <+ aborted_in_searcher; + + committed_in_searcher_event <- committed_in_searcher.constant(()); + aborted_in_searcher_event <- aborted_in_searcher.constant(()); + graph.stop_editing <+ any(&committed_in_searcher_event, &aborted_in_searcher_event); + + + // === Adding Node === + + node_added_by_user <- graph.node_added.filter(|(_, _, should_edit)| *should_edit); + searcher_for_adding <- node_added_by_user.map( + |&(node, src, _)| SearcherParams::new_for_new_node(node, src) + ); + frp.source.adding_new_node <+ searcher_for_adding.to_true(); + new_node_edited <- graph.node_editing_started.gate(&frp.adding_new_node); + frp.source.searcher <+ searcher_for_adding.sample(&new_node_edited).map(|&s| Some(s)); + + adding_committed <- frp.editing_committed.gate(&frp.adding_new_node).map(|(id,_)| *id); + adding_aborted <- frp.editing_aborted.gate(&frp.adding_new_node); + adding_finished <- any(adding_committed,adding_aborted); + frp.source.adding_new_node <+ adding_finished.constant(false); + frp.source.searcher <+ adding_finished.constant(None); + + eval adding_committed ([graph](node) { + graph.deselect_all_nodes(); + graph.select_node(node); + }); + eval adding_aborted ((node) graph.remove_node(node)); + + // === Editing === - // The order of instructions below is important to properly distinguish between - // committing and aborting node editing. + existing_node_edited <- graph.node_being_edited.filter_map(|x| *x).gate_not(&frp.adding_new_node); + frp.source.searcher <+ existing_node_edited.map( + |&node| Some(SearcherParams::new_for_edited_node(node)) + ); - frp.source.editing_committed <+ searcher.editing_committed - .map2(&graph.output.node_being_edited, |entry,id| (*id,*entry)) - .filter_map(|(id,entry)| Some(((*id)?, *entry))); - // This node is true when received "abort_node_editing" signal, and should get false - // once processing of "node_being_edited" event from graph is performed. - editing_aborted <- any(...); - editing_aborted <+ frp.close_searcher.constant(true); - editing_commited_in_searcher <- searcher.editing_committed.constant(()); - should_finish_editing_if_any <- any(frp.close_searcher,editing_commited_in_searcher - ,frp.open_searcher,frp.show_open_dialog); - should_finish_editing <- should_finish_editing_if_any.gate(&graph.output.node_editing); - eval should_finish_editing ((()) graph.input.stop_editing.emit(())); + // === Searcher Position and Visibility === - visibility_conditions <- all(&graph.output.node_being_edited,&searcher.is_empty); + visibility_conditions <- all(&frp.searcher,&searcher.is_empty); _eval <- visibility_conditions.map2(&searcher.is_visible, - f!([model,searcher_left_top_position]((node_id,is_searcher_empty),is_visible) { - model.update_searcher_view(*node_id,*is_searcher_empty,&searcher_left_top_position); + f!([model,searcher_left_top_position]((searcher,is_searcher_empty),is_visible) { + model.update_searcher_view(*searcher,*is_searcher_empty,&searcher_left_top_position); if !is_visible { // Do not animate searcher_left_top_position.skip(); @@ -500,46 +489,14 @@ impl View { }) ); - _eval <- graph.output.node_position_set.map2(&graph.output.node_being_edited, - f!([searcher_left_top_position]((node_id,position),edited_node_id) { - if edited_node_id.contains(node_id) { - let new = Model::searcher_left_top_position_when_under_node_at(*position); + _eval <- graph.output.node_position_set.map2(&frp.searcher, + f!([searcher_left_top_position](&(node_id, position), &searcher) { + if searcher.map_or(false, |s| s.input == node_id) { + let new = Model::searcher_left_top_position_when_under_node_at(position); searcher_left_top_position.set_target_value(new); } }) ); - let editing_finished = graph.output.node_editing_finished.clone_ref(); - editing_finished_no_entry <- editing_finished.gate_not(&editing_aborted); - frp.source.editing_committed <+ editing_finished_no_entry.map(|id| (*id,None)); - frp.source.editing_aborted <+ editing_finished.gate(&editing_aborted); - editing_aborted <+ graph.output.node_editing_finished.constant(false); - - frp.source.is_searcher_opened <+ graph.output.node_being_edited.map(|n| n.is_some()); - - - // === Adding Node === - - let adding_by_dropping_edge = graph.output.on_edge_drop_to_create_node.clone_ref(); - let adding_by_opening_searcher = frp.open_searcher.clone_ref(); - adding_by_dropping_edge_bool <- adding_by_dropping_edge.constant(true); - adding_by_opening_searcher_bool <- adding_by_opening_searcher.constant(true); - frp.source.adding_new_node <+ any(adding_by_dropping_edge_bool, adding_by_opening_searcher_bool); - - node_being_edited <- graph.output.node_being_edited.on_change().filter_map(|n| *n); - - frp.source.searcher_opened <+ node_being_edited.map(|id| ComponentBrowserOpenReason::NodeEditing(*id)); - frp.source.searcher_opened <+ adding_by_dropping_edge.map(f!((e) model.add_node_by_dropping_edge(*e))); - frp.source.searcher_opened <+ adding_by_opening_searcher.map(f_!(model.add_node_by_opening_searcher())); - - adding_committed <- frp.editing_committed.gate(&frp.adding_new_node).map(|(id,_)| *id); - adding_aborted <- frp.editing_aborted.gate(&frp.adding_new_node); - frp.source.adding_new_node <+ any(&adding_committed,&adding_aborted).constant(false); - - eval adding_committed ([graph](node) { - graph.deselect_all_nodes(); - graph.select_node(node); - }); - eval adding_aborted ((node) graph.remove_node(node)); // === Opening Open File or Project Dialog === @@ -603,7 +560,7 @@ impl View { let prompt_size = styles.get_number(prompt_size_path); prompt_size <- all(&prompt_size,&init)._0(); - disable_after_opening_searcher <- frp.is_searcher_opened.filter(|v| *v).constant(()); + disable_after_opening_searcher <- frp.searcher.filter_map(|s| s.map(|_| ())); disable <- any(frp.disable_prompt,disable_after_opening_searcher); disabled <- disable.constant(true); show_prompt <- frp.show_prompt.gate_not(&disabled); @@ -663,11 +620,6 @@ impl View { &self.model.searcher } - /// Searcher 2.0 FRP. - pub fn new_searcher_frp(&self) -> &searcher::new::Frp { - self.model.searcher.new_frp() - } - /// Code Editor View. pub fn code_editor(&self) -> &code_editor::View { &self.model.code_editor @@ -712,7 +664,6 @@ impl application::View for View { fn default_shortcuts() -> Vec { use shortcut::ActionType::*; (&[ - (Press, "!is_searcher_opened", "tab", "open_searcher"), (Press, "!is_searcher_opened", "cmd o", "show_open_dialog"), (Press, "is_searcher_opened", "escape", "close_searcher"), (Press, "open_dialog_shown", "escape", "close_open_dialog"), diff --git a/app/gui/view/src/searcher.rs b/app/gui/view/src/searcher.rs index 68cc767c5d7..fdf18a100ac 100644 --- a/app/gui/view/src/searcher.rs +++ b/app/gui/view/src/searcher.rs @@ -23,7 +23,6 @@ use ensogl_component::list_view::ListView; // ============== pub mod icons; -pub mod new; pub use ensogl_component::list_view::entry; @@ -114,7 +113,6 @@ struct Model { logger: Logger, display_object: display::object::Instance, list: ListView, - new_view: new::View, documentation: documentation::View, doc_provider: Rc>, } @@ -126,7 +124,6 @@ impl Model { let logger = Logger::new("SearcherView"); let display_object = display::object::Instance::new(&logger); let list = app.new_view::>(); - let new_view = new::View::new(); let documentation = documentation::View::new(scene); let doc_provider = default(); scene.layers.above_nodes.add_exclusive(&list); @@ -143,7 +140,7 @@ impl Model { list.set_position_x(ACTION_LIST_X); documentation.set_position_x(DOCUMENTATION_X); documentation.set_position_y(-action_list_gap); - Self { app, logger, display_object, list, new_view, documentation, doc_provider } + Self { app, logger, display_object, list, documentation, doc_provider } } fn docs_for(&self, id: Option) -> String { @@ -252,14 +249,6 @@ impl View { source.used_as_suggestion <+ opt_picked_entry.gate(&is_entry_enabled); source.editing_committed <+ model.list.chosen_entry.gate(&is_entry_enabled); - // New searcher - let is_selected = model.new_view.focused.clone_ref(); - selected_id <- model.new_view.highlight.map(|id| id.last().copied()); - opt_picked_entry <- selected_id.sample(&frp.use_as_suggestion); - source.used_as_suggestion <+ opt_picked_entry.gate(&is_selected); - opt_chosen_id <- model.new_view.entry_chosen.map(|id| id.last().copied()); - source.editing_committed <+ opt_chosen_id.gate(&is_selected); - eval displayed_doc ((data) model.documentation.frp.display_documentation(data)); }; @@ -286,11 +275,6 @@ impl View { let provider = Rc::new(list_view::entry::EmptyProvider); self.set_actions(provider); } - - /// The FRP interface of new searcher. - pub fn new_frp(&self) -> &new::Frp { - &self.model.new_view.frp - } } impl display::Object for View { diff --git a/app/gui/view/src/searcher/new.rs b/app/gui/view/src/searcher/new.rs deleted file mode 100644 index a8a348f3de7..00000000000 --- a/app/gui/view/src/searcher/new.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! A stub module with new searcher GUI. - -use crate::prelude::*; - - - -// ============= -// === Entry === -// ============= - -/// A structure describing a single entry in Searcher. -#[allow(missing_docs)] -#[derive(Clone, CloneRef, Debug, Default)] -pub struct Entry { - pub label: ImString, - pub is_folder: Immutable, - pub icon: Icon, -} - -/// The typewrapper for icon name. -#[derive(Clone, CloneRef, Debug, Default)] -pub struct Icon { - name: ImString, -} - -/// Construct icon structure from its name. -#[allow(non_snake_case)] -pub fn Icon(name: impl Into) -> Icon { - let name = name.into(); - Icon { name } -} - - - -// =========== -// === FRP === -// =========== - -/// A type representing path to entry in some column. -pub type EntryPath = Rc>; - -ensogl::define_endpoints! { - Input { - reset(), - directory_content (EntryPath,Entry), - set_highlight (EntryPath), - } - - Output { - list_directory (EntryPath), - highlight (EntryPath), - entry_chosen (EntryPath), - } -} - -/// The Searcher View gui component. -/// -/// Currently it contains only simple mechanism of requesting searcher content and printing it to -/// the console. -#[allow(missing_docs)] -#[derive(Clone, CloneRef, Debug)] -pub struct View { - pub frp: Frp, -} - -impl Deref for View { - type Target = Frp; - - fn deref(&self) -> &Self::Target { - &self.frp - } -} - -impl View { - /// Create new searcher view. - pub fn new() -> Self { - let logger = Logger::new("searcher::new::View"); - let frp = Frp::new(); - let network = &frp.network; - enso_frp::extend! { network - eval frp.directory_content ([logger]((crumbs,entry)) { - let crumbs = crumbs.iter().map(ToString::to_string).join(","); - info!(logger,"New Searcher Entry received: [{crumbs}] -> {entry:?}."); - }); - - frp.source.list_directory <+ frp.reset.constant(Rc::new(vec![])); - frp.source.list_directory <+ frp.directory_content.filter_map(|(crumbs,entry)| { - entry.is_folder.as_some(crumbs.clone_ref()) - }); - } - - Self { frp } - } -} - -impl Default for View { - fn default() -> Self { - Self::new() - } -} diff --git a/app/gui/view/src/status_bar.rs b/app/gui/view/src/status_bar.rs index d84ed4f363e..c2b6b2e1a10 100644 --- a/app/gui/view/src/status_bar.rs +++ b/app/gui/view/src/status_bar.rs @@ -208,27 +208,23 @@ impl Model { fn camera_changed(&self) { let screen = self.camera.screen(); - let x = -screen.width / 2.0 + MARGIN; - let y = -screen.height / 2.0 + MARGIN; - self.root.set_position_x(x.round()); + let y = screen.height / 2.0 - MARGIN; self.root.set_position_y(y.round()); } fn update_layout(&self) { - self.label.set_position_x(PADDING); - self.label.set_position_y(HEIGHT / 2.0 + TEXT_SIZE / 2.0); + let label_width = self.label.width.value(); + self.label.set_position_x(-label_width / 2.0); + self.label.set_position_y(-HEIGHT / 2.0 + TEXT_SIZE / 2.0); - let bg_width = if self.label.width.value() > 0.0 { - PADDING + self.label.width.value() + PADDING + let bg_width = if label_width > 0.0 { + label_width + 2.0 * PADDING + 2.0 * MAGIC_SHADOW_MARGIN } else { 0.0 }; - let bg_height = HEIGHT; - self.background.size.set(Vector2( - bg_width + 2.0 * MAGIC_SHADOW_MARGIN, - bg_height + 2.0 * MAGIC_SHADOW_MARGIN, - )); - self.background.set_position(Vector3(bg_width / 2.0, bg_height / 2.0, 0.0)); + let bg_height = HEIGHT + 2.0 * MAGIC_SHADOW_MARGIN; + self.background.size.set(Vector2(bg_width, bg_height)); + self.background.set_position_y(-HEIGHT / 2.0); } fn add_event(&self, label: &event::Label) -> event::Id { diff --git a/app/gui/view/src/window_control_buttons.rs b/app/gui/view/src/window_control_buttons.rs index d20dbe313b9..2307125c444 100644 --- a/app/gui/view/src/window_control_buttons.rs +++ b/app/gui/view/src/window_control_buttons.rs @@ -10,6 +10,7 @@ use ensogl::data::color; use ensogl::define_shape_system; use ensogl::display; use ensogl::display::object::ObjectOps; +use ensogl_hardcoded_theme::application::window_control_buttons as theme; // ============== @@ -17,7 +18,6 @@ use ensogl::display::object::ObjectOps; // ============== pub mod close; -pub mod common; pub mod fullscreen; @@ -83,7 +83,6 @@ impl LayoutParams { impl LayoutParams> { /// Get layout from theme. Each layout parameter will be an frp sampler. pub fn from_theme(style: &StyleWatchFrp) -> Self { - use ensogl_hardcoded_theme::application::window_control_buttons as theme; let default = LayoutParams::default(); let spacing = style.get_number_or(theme::spacing, default.spacing); let padding_left = style.get_number_or(theme::padding::left, default.padding_left); @@ -178,10 +177,13 @@ impl Model { layout; let close_size = self.close.size.value(); let fullscreen_size = self.fullscreen.size.value(); + let padding_offset = Vector2(padding_left, -padding_top); + let origin_offset = |size: Vector2| Vector2(size.x / 2.0, -size.y / 2.0); - self.close.set_position_xy(Vector2(padding_left, -padding_top)); + self.close.set_position_xy(padding_offset + origin_offset(close_size)); let fullscreen_x = padding_left + close_size.x + spacing; - self.fullscreen.set_position_xy(Vector2(fullscreen_x, -padding_top)); + self.fullscreen + .set_position_xy(Vector2(fullscreen_x, -padding_top) + origin_offset(fullscreen_size)); let width = fullscreen_x + fullscreen_size.x + padding_right; let height = padding_top + max(close_size.y, fullscreen_size.y) + padding_bottom; @@ -237,13 +239,17 @@ impl View { let style = StyleWatchFrp::new(&app.display.default_scene.style_sheet); let style_frp = LayoutParams::from_theme(&style); let layout_style = style_frp.flatten(network); + let radius = style.get_number(theme::radius); frp::extend! { network // Layout - button_resized <- any_(&model.close.size,&model.fullscreen.size); - layout_on_button_change <- sample(&layout_style,&button_resized); - need_relayout <- any(&layout_style,&layout_on_button_change); - frp.source.size <+ need_relayout.map(f!((layout) model.set_layout(*layout))); + button_size <- radius.map(|&r| Vector2(2.0 * r, 2.0 * r)); + model.close.set_size <+ button_size; + model.fullscreen.set_size <+ button_size; + button_resized <- any_(&model.close.size,&model.fullscreen.size); + layout_on_button_change <- sample(&layout_style,&button_resized); + need_relayout <- any(&layout_style,&layout_on_button_change); + frp.source.size <+ need_relayout.map(f!((layout) model.set_layout(*layout))); // Handle the panel-wide hover mouse_near_buttons <- bool(&model.shape.events.mouse_out,&model.shape.events.mouse_over); diff --git a/app/gui/view/src/window_control_buttons/close.rs b/app/gui/view/src/window_control_buttons/close.rs index 5950f84df84..d528d8b160b 100644 --- a/app/gui/view/src/window_control_buttons/close.rs +++ b/app/gui/view/src/window_control_buttons/close.rs @@ -1,6 +1,6 @@ //! The close button in the Top Button panel. -use crate::window_control_buttons::common::prelude::*; +use ensogl_component::button::prelude::*; // ============== @@ -11,8 +11,9 @@ pub use ensogl_hardcoded_theme::application::window_control_buttons::close as th -/// The view component with the close button. -pub type View = common::View; +// ============= +// === Shape === +// ============= /// The shape for "close" button. It places X-lie cross on a circle. pub mod shape { @@ -62,3 +63,16 @@ impl ButtonShape for shape::DynamicShape { &self.icon_color } } + + + +// ============ +// === View === +// ============ + +/// The view component with the close button. +/// +/// The button styled after macOS, i.e. consists of an icon shape placed on top of a circle. +/// The icon is visible when button or its neighborhood (as provided by `mouse_nearby` input) is +/// hovered. +pub type View = ensogl_component::button::View; diff --git a/app/gui/view/src/window_control_buttons/fullscreen.rs b/app/gui/view/src/window_control_buttons/fullscreen.rs index 805ecbf24f3..7272a9c47ff 100644 --- a/app/gui/view/src/window_control_buttons/fullscreen.rs +++ b/app/gui/view/src/window_control_buttons/fullscreen.rs @@ -1,6 +1,6 @@ //! The fullscreen button in the Top Button panel. -use crate::window_control_buttons::common::prelude::*; +use ensogl_component::button::prelude::*; // ============== @@ -11,8 +11,9 @@ pub use ensogl_hardcoded_theme::application::window_control_buttons::fullscreen -/// The view component with the fullscreen button. -pub type View = common::View; +// ============= +// === Shape === +// ============= /// The shape for "fullscreen" button. The icon consists if two triangles ◤◢ centered around single /// point. @@ -61,3 +62,16 @@ impl ButtonShape for shape::DynamicShape { &self.icon_color } } + + + +// ============ +// === View === +// ============ + +/// The view component with the fullscreen button. +/// +/// The button styled after macOS, i.e. consists of an icon shape placed on top of a circle. +/// The icon is visible when button or its neighborhood (as provided by `mouse_nearby` input) is +/// hovered. +pub type View = ensogl_component::button::View; diff --git a/build/enso-formatter/src/main.rs b/build/enso-formatter/src/main.rs index 0ca86098ae4..9de2ccd02d6 100644 --- a/build/enso-formatter/src/main.rs +++ b/build/enso-formatter/src/main.rs @@ -567,6 +567,7 @@ fn main() { // ============= #[test] +#[ignore] fn test_formatting() { let input = r#"//! Module-level documentation //! written in two lines. diff --git a/build/run.js b/build/run.js index 80135a72b61..95a588bedf7 100755 --- a/build/run.js +++ b/build/run.js @@ -255,7 +255,7 @@ commands.test = command(`Run test suites`) commands.test.rust = async function (argv) { if (argv.native) { console.log(`Running Rust test suite.`) - await run_cargo('cargo', ['test']) + await run_cargo('cargo', ['test', '--workspace']) } if (argv.wasm) { @@ -285,7 +285,7 @@ commands['integration-test'].rust = async function (argv) { } try { console.log(`Running Rust WASM test suite.`) - process.env.WASM_BINDGEN_TEST_TIMEOUT = 120 + process.env.WASM_BINDGEN_TEST_TIMEOUT = 180 let args = [ 'test', '--headless', diff --git a/integration-test/Cargo.toml b/integration-test/Cargo.toml index b6c8012e6f5..d737cd9b939 100644 --- a/integration-test/Cargo.toml +++ b/integration-test/Cargo.toml @@ -10,5 +10,6 @@ enso-frp = { path = "../lib/rust/frp" } enso-prelude = { path = "../lib/rust/prelude" } enso-gui = { path = "../app/gui" } enso-web = { path = "../lib/rust/web" } +ordered-float = "2.7.0" wasm-bindgen = { version = "0.2.78" } wasm-bindgen-test = "0.3.8" diff --git a/integration-test/tests/graph_editor.rs b/integration-test/tests/graph_editor.rs index 4e25da8f4c9..e9c10dbf721 100644 --- a/integration-test/tests/graph_editor.rs +++ b/integration-test/tests/graph_editor.rs @@ -5,8 +5,13 @@ use enso_integration_test::prelude::*; use approx::assert_abs_diff_eq; +use enso_gui::view::graph_editor::component::node::Expression; +use enso_gui::view::graph_editor::GraphEditor; +use enso_gui::view::graph_editor::NodeId; +use enso_gui::view::graph_editor::NodeSource; use enso_web::sleep; use ensogl::display::navigation::navigator::ZoomEvent; +use ordered_float::OrderedFloat; use std::time::Duration; @@ -21,7 +26,8 @@ async fn create_new_project_and_add_nodes() { assert_eq!(graph_editor.model.nodes.all.len(), 2); let expect_node_added = graph_editor.node_added.next_event(); graph_editor.add_node(); - let added_node_id = expect_node_added.expect(); + let (added_node_id, source_node, _) = expect_node_added.expect(); + assert_eq!(source_node, None); assert_eq!(graph_editor.model.nodes.all.len(), 3); let added_node = @@ -103,3 +109,63 @@ async fn zooming() { sleep(zoom_duration_ms).await; assert!(camera.zoom() > 1.0, "Camera zoom {} must be greater than 1.0", camera.zoom()); } + +#[wasm_bindgen_test] +async fn adding_node_with_add_node_button() { + const INITIAL_NODE_COUNT: usize = 2; + let test = IntegrationTestOnNewProject::setup().await; + let graph_editor = test.graph_editor(); + let scene = &test.ide.ensogl_app.display.default_scene; + + let nodes = graph_editor.model.nodes.all.keys(); + let nodes_positions = nodes.into_iter().flat_map(|id| graph_editor.model.get_node_position(id)); + let mut sorted_positions = nodes_positions.sorted_by_key(|pos| OrderedFloat(pos.y)); + let bottom_most_pos = + sorted_positions.next().expect("Default project does not contain any nodes"); + + // Node is created below the bottom-most one. + let (first_node_id, node_source) = add_node_with_add_node_button(&graph_editor, "1 + 1"); + assert!(node_source.is_none()); + assert_eq!(graph_editor.model.nodes.all.len(), INITIAL_NODE_COUNT + 1); + let node_position = + graph_editor.model.get_node_position(first_node_id).expect("Node was not added"); + assert!( + node_position.y < bottom_most_pos.y, + "Expected that {node_position}.y < {bottom_most_pos}.y" + ); + + // Selected node is used as a `source` node. + graph_editor.model.nodes.deselect_all(); + graph_editor.model.nodes.select(first_node_id); + let (_, node_source) = add_node_with_add_node_button(&graph_editor, "+ 1"); + assert_eq!(node_source, Some(NodeSource { node: first_node_id })); + assert_eq!(graph_editor.model.nodes.all.len(), INITIAL_NODE_COUNT + 2); + + // If there is a free space, the new node is created in the center of screen. + let camera = scene.layers.main.camera(); + camera.mod_position_xy(|pos| pos + Vector2(1000.0, 1000.0)); + let wait_for_update = Duration::from_millis(500); + sleep(wait_for_update).await; + graph_editor.model.nodes.deselect_all(); + let (node_id, node_source) = add_node_with_add_node_button(&graph_editor, "1"); + assert!(node_source.is_none()); + assert_eq!(graph_editor.model.nodes.all.len(), INITIAL_NODE_COUNT + 3); + let node_position = graph_editor.model.get_node_position(node_id).expect("Node was not added"); + let center_of_screen = scene.screen_to_scene_coordinates(Vector3::zeros()); + assert_abs_diff_eq!(node_position.x, center_of_screen.x, epsilon = 10.0); + assert_abs_diff_eq!(node_position.y, center_of_screen.y, epsilon = 10.0); +} + +fn add_node_with_add_node_button( + graph_editor: &GraphEditor, + expression: &str, +) -> (NodeId, Option) { + let add_node_button = &graph_editor.model.add_node_button; + let node_added = graph_editor.node_added.next_event(); + add_node_button.click(); + let (node_id, source_node, _) = node_added.expect(); + let node = graph_editor.model.nodes.get_cloned_ref(&node_id).expect("Node was not added"); + node.set_expression(Expression::new_plain(expression)); + graph_editor.stop_editing(); + (node_id, source_node) +} diff --git a/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs b/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs index 5ea2a153763..d9a6ce7b6c9 100644 --- a/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs +++ b/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs @@ -489,6 +489,21 @@ define_themes! { [light:0, dark:1] size = 20.0, 20.0; } } + add_node_button { + margin = 14.0, 14.0; + size = 60.0, 60.0; + background = Rgba(1.0, 1.0, 1.0, 1.0), Rgba(0.0, 0.0, 0.0, 1.0); + color = Rgba(0.0, 0.451, 0.859, 1.0), Rgba(0.0, 0.451, 0.859, 1.0); + + hover { + background = Rgba(0.9, 0.9, 1.0, 1.0), Rgba(0.9, 0.9, 1.0, 1.0); + color = Rgba(0.0, 0.451, 0.859, 1.0), Rgba(0.0, 0.451, 0.859, 1.0); + } + click { + background = Rgba(0.62, 0.62, 1.0, 1.0), Rgba(0.62, 0.62, 1.0, 1.0); + color = Rgba(0.0, 0.451, 0.859, 1.0), Rgba(0.0, 0.451, 0.859, 1.0); + } + } } widget { list_view { diff --git a/lib/rust/ensogl/component/Cargo.toml b/lib/rust/ensogl/component/Cargo.toml index e913ba97b7a..8a87c28755d 100644 --- a/lib/rust/ensogl/component/Cargo.toml +++ b/lib/rust/ensogl/component/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Enso Team "] edition = "2021" [dependencies] +ensogl-button = { path = "button" } ensogl-drop-down-menu = { path = "drop-down-menu" } ensogl-drop-manager = { path = "drop-manager" } ensogl-file-browser = { path = "file-browser" } diff --git a/lib/rust/ensogl/component/button/Cargo.toml b/lib/rust/ensogl/component/button/Cargo.toml new file mode 100644 index 00000000000..eb6a32c551a --- /dev/null +++ b/lib/rust/ensogl/component/button/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ensogl-button" +version = "0.1.0" +authors = ["Enso Team "] +edition = "2021" + +[dependencies] +enso-frp = { path = "../../../frp" } +ensogl-core = { path = "../../core" } diff --git a/app/gui/view/src/window_control_buttons/common.rs b/lib/rust/ensogl/component/button/src/lib.rs similarity index 65% rename from app/gui/view/src/window_control_buttons/common.rs rename to lib/rust/ensogl/component/button/src/lib.rs index 2c6e4c177dc..8d2b113bb25 100644 --- a/app/gui/view/src/window_control_buttons/common.rs +++ b/lib/rust/ensogl/component/button/src/lib.rs @@ -1,18 +1,97 @@ -//! The code shared between different buttons living in the Top Buttons panel. +//! The EnsoGL Button Component +//! +//! This crate contains an abstraction of button component, allowing creating custom buttons +//! quickly. +//! +//! # Usage +//! +//! ```ignore +//! // The prelude will import all structures from this crate and EnsoGL core which are needed +//! // for defining custom button. +//! use crate::prelude::*; +//! +//! // First, define our custom button shape. The shape should take two colors as a parameters: +//! // one of the icon, and one of the background. In this example we will create "close" button. +//! +//! pub mod shape { +//! use super::*; +//! +//! ensogl_core::define_shape_system! { +//! (background_color:Vector4, icon_color:Vector4) { +//! let size = Var::canvas_size(); +//! let radius = Min::min(size.x(),size.y()) / 2.0; +//! let angle = Radians::from(45.0.degrees()); +//! let bar_length = &radius * 4.0 / 3.0; +//! let bar_width = &bar_length / 6.5; +//! #[allow(clippy::blacklisted_name)] // The `bar` name here is totally legit. +//! let bar = Rect((bar_length, &bar_width)).corners_radius(bar_width); +//! let cross = (bar.rotate(angle) + bar.rotate(-angle)).into(); +//! shape(background_color, icon_color, cross, radius) +//! } +//! } +//! } +//! +//! // The defined shape should then implement the [`ButtonShape`] trait: +//! +//! impl ButtonShape for shape::DynamicShape { +//! fn debug_name() -> &'static str { +//! "CloseButton" +//! } +//! +//! fn background_color_path(state: State) -> StaticPath { +//! match state { +//! State::Unconcerned => theme::normal::background_color, +//! State::Hovered => theme::hovered::background_color, +//! State::Pressed => theme::pressed::background_color, +//! } +//! } +//! +//! fn icon_color_path(state: State) -> StaticPath { +//! match state { +//! State::Unconcerned => theme::normal::icon_color, +//! State::Hovered => theme::hovered::icon_color, +//! State::Pressed => theme::pressed::icon_color, +//! } +//! } +//! +//! fn background_color(&self) -> &DynamicParam>> { +//! &self.background_color +//! } +//! +//! fn icon_color(&self) -> &DynamicParam>> { +//! &self.icon_color +//! } +//! } +//! +//! // Finally, we can create the full component by aliasing [`View`] structure. +//! +//! pub type View = ensogl_button::View; +//! ``` -use ensogl::display::shape::*; -use prelude::*; +#![recursion_limit = "256"] +// === Standard Linter Configuration === +#![deny(non_ascii_idents)] +#![warn(unsafe_code)] +// === Non-Standard Linter Configuration === +#![warn(missing_copy_implementations)] +#![warn(missing_debug_implementations)] +#![warn(missing_docs)] +#![warn(trivial_casts)] +#![warn(trivial_numeric_casts)] +#![warn(unused_import_braces)] +#![warn(unused_qualifications)] + +use crate::prelude::*; +use ensogl_core::display::shape::*; use enso_frp as frp; -use ensogl::application; -use ensogl::application::Application; -use ensogl::data::color; -use ensogl::data::color::Rgba; -use ensogl::display; -use ensogl::display::object::ObjectOps; -use ensogl::display::style; -use ensogl::display::style::data::DataMatch; -use ensogl::gui::component::ShapeView; +use ensogl_core::application; +use ensogl_core::application::Application; +use ensogl_core::data::color; +use ensogl_core::data::color::Rgba; +use ensogl_core::display; +use ensogl_core::display::object::ObjectOps; +use ensogl_core::gui::component::ShapeView; @@ -20,19 +99,18 @@ use ensogl::gui::component::ShapeView; // === Prelude === // =============== -/// Prelude meant to be used by sibling modules that provide specific button implementations. +/// Prelude meant to be used by the modules defining custom buttons. pub mod prelude { - pub use crate::prelude::*; + pub use ensogl_core::prelude::*; - pub use crate::window_control_buttons::common; - pub use crate::window_control_buttons::common::shape::shape; - pub use crate::window_control_buttons::common::ButtonShape; - pub use crate::window_control_buttons::common::State; + pub use crate::shape::shape; + pub use crate::ButtonShape; + pub use crate::State; - pub use ensogl::display::shape::*; - pub use ensogl::display::style::StaticPath; - pub use ensogl::system::gpu::shader::glsl::traits::IntoGlsl; - pub use ensogl::system::gpu::Attribute; + pub use ensogl_core::display::shape::*; + pub use ensogl_core::display::style::StaticPath; + pub use ensogl_core::system::gpu::shader::glsl::traits::IntoGlsl; + pub use ensogl_core::system::gpu::Attribute; } @@ -41,8 +119,8 @@ pub mod prelude { // === Constants === // ================= -/// Button radius to be used if theme-provided value is not available. -pub const RADIUS_FALLBACK: f32 = 12.0; +/// The default button's shape size. +const DEFAULT_SIZE_XY: (f32, f32) = (12.0, 12.0); @@ -71,13 +149,13 @@ impl Default for State { /// Trait to be defined on a specific button's shape. pub trait ButtonShape: CloneRef + display::object::class::Object + DynamicShapeInternals + 'static { - /// The human readable name of the button, for debug purposes. + /// The human-readable name of the button, for debug purposes. fn debug_name() -> &'static str; - /// Path to the color of circular button background for a specifc button's state. + /// Path to the color of circular button background for a specific button's state. fn background_color_path(state: State) -> StaticPath; - /// Path to the color of an icon for a specifc button's state. + /// Path to the color of an icon for a specific button's state. fn icon_color_path(state: State) -> StaticPath; /// Access the shader parameter for the background color. @@ -118,7 +196,7 @@ pub mod shape { // === Model === // ============= -/// An internal model of Top Buttons Panel button component +/// An internal model of the button component. #[derive(Clone, CloneRef, Debug)] #[clone_ref(bound = "Shape:CloneRef")] #[allow(missing_docs)] @@ -149,26 +227,6 @@ impl Model { pub fn set_icon_color(&self, color: impl Into) { self.shape.icon_color().set(color.into().into()); } - - /// Retrieves circle radius value from an frp sampler event. - fn get_radius(radius: &Option) -> f32 { - radius.as_ref().and_then(DataMatch::number).unwrap_or(RADIUS_FALLBACK) - } - - /// Set radius, updating the shape sizes and position. - pub fn set_radius(&self, radius: &Option) -> Vector2 { - let radius = Self::get_radius(radius); - let size = Self::size_for_radius(radius); - self.shape.size().set(size); - self.shape.set_position_x(radius); - self.shape.set_position_y(-radius); - size - } - - /// Calculate the size of button for a given radius value. - pub fn size_for_radius(radius: f32) -> Vector2 { - Vector2(radius, radius) * 2.0 - } } @@ -177,9 +235,11 @@ impl Model { // === FRP === // =========== -ensogl::define_endpoints! { +ensogl_core::define_endpoints! { Input { + set_size (Vector2), mouse_nearby (bool), + click (), } Output { clicked (), @@ -195,11 +255,7 @@ ensogl::define_endpoints! { // === View === // ============ -/// The Control Button component view. -/// -/// This is a clickable button styled after macOS, i.e. consists of an icon shape placed on top of -/// a circle. The icon is visible when button or its neighborhood (as provided by `mouse_nearby` -/// input) is hovered. +/// The Button component view. /// /// When clicked, it emits `clicked` frp event. The click requires the mouse to be both pressed and /// released on the button. It is allowed to temporarily move mouse out of the button while holding @@ -219,7 +275,7 @@ impl View { /// Constructor. pub fn new(app: &Application) -> Self { let frp = Frp::new(); - let model = Model::new(app); + let model = Model::::new(app); let network = &frp.network; let scene = &app.display.default_scene; let style = StyleWatchFrp::new(&scene.style_sheet); @@ -239,10 +295,6 @@ impl View { background_color.target(color::Lcha::from(default_background_color)); model.set_icon_color(default_background_color); - // Radius initialization - let radius_frp = - style.get(ensogl_hardcoded_theme::application::window_control_buttons::radius); - // Style's relevant color FRP endpoints. let background_unconcerned_color = style.get_color(Shape::background_color_path(State::Unconcerned)); @@ -256,12 +308,12 @@ impl View { let icon_pressed_color = style.get_color(Shape::icon_color_path(State::Pressed)); model.set_background_color(background_unconcerned_color.value()); + model.set_icon_color(icon_unconcerned_color.value()); let events = &model.shape.events; frp::extend! { network - - // Radius - frp.source.size <+ radius_frp.map(f!((radius) model.set_radius(radius))); + eval frp.set_size ((&size) model.shape.size().set(size)); + frp.source.size <+ frp.set_size; // Mouse frp.source.is_hovered <+ bool(&events.mouse_out,&events.mouse_over); @@ -270,10 +322,12 @@ impl View { mouse_released_on_me <- mouse.up_primary.gate(&frp.is_hovered); was_clicked <- tracking_for_release.previous(); frp.source.clicked <+ mouse_released_on_me.gate(&was_clicked); + frp.source.clicked <+ frp.click; state <- all_with3(&frp.is_hovered,&frp.mouse_nearby,&tracking_for_release, |strict_hover,nearby_hover,clicked| { match (strict_hover,nearby_hover,clicked) { (true , _ , true) => State::Pressed, + (true , _ , _ ) => State::Hovered, (_ , true , _ ) => State::Hovered, (_ , _ , true) => State::Hovered, _ => State::Unconcerned, @@ -281,7 +335,6 @@ impl View { }); frp.source.state <+ state; - // Color animations background_color.target <+ all_with4(&frp.source.state,&background_unconcerned_color, &background_hovered_color,&background_pressed_color, @@ -307,7 +360,8 @@ impl View { eval icon_color.value ((color) model.set_icon_color(color)); } - frp.source.size.emit(model.set_radius(&radius_frp.value())); + let (size_x, size_y) = DEFAULT_SIZE_XY; + frp.set_size.emit(Vector2(size_x, size_y)); Self { frp, model, style } } diff --git a/lib/rust/ensogl/component/src/lib.rs b/lib/rust/ensogl/component/src/lib.rs index 8a41a92c4b5..bde7eeda7bd 100644 --- a/lib/rust/ensogl/component/src/lib.rs +++ b/lib/rust/ensogl/component/src/lib.rs @@ -9,6 +9,7 @@ // === Export === // ============== +pub use ensogl_button as button; pub use ensogl_drop_down_menu as drop_down_menu; pub use ensogl_drop_manager as drop_manager; pub use ensogl_file_browser as file_browser; diff --git a/lib/rust/ensogl/core/src/display/scene.rs b/lib/rust/ensogl/core/src/display/scene.rs index 4d7401f2402..7077370ddad 100644 --- a/lib/rust/ensogl/core/src/display/scene.rs +++ b/lib/rust/ensogl/core/src/display/scene.rs @@ -1055,7 +1055,7 @@ impl SceneData { let origin_clip_space = camera.view_projection_matrix() * origin_world_space; let inv_object_matrix = object.transform_matrix().try_inverse().unwrap(); - let shape = self.frp.shape.value(); + let shape = camera.screen(); let clip_space_z = origin_clip_space.z; let clip_space_x = origin_clip_space.w * 2.0 * screen_pos.x / shape.width; let clip_space_y = origin_clip_space.w * 2.0 * screen_pos.y / shape.height; diff --git a/lib/rust/ensogl/example/render-profile/src/lib.rs b/lib/rust/ensogl/example/render-profile/src/lib.rs index 6f9f1fb0e3b..88988ea86b3 100644 --- a/lib/rust/ensogl/example/render-profile/src/lib.rs +++ b/lib/rust/ensogl/example/render-profile/src/lib.rs @@ -1,11 +1,14 @@ //! Renders profiling data, obtained from a file, as a flame graph. +// === Standard Linter Configuration === +#![deny(non_ascii_idents)] +#![warn(unsafe_code)] +// === Non-Standard Linter Configuration === #![warn(missing_copy_implementations)] #![warn(missing_debug_implementations)] #![warn(missing_docs)] #![warn(trivial_casts)] #![warn(trivial_numeric_casts)] -#![warn(unsafe_code)] #![warn(unused_import_braces)] #![warn(unused_qualifications)] diff --git a/lib/rust/profiler/data/src/bin/measurements.rs b/lib/rust/profiler/data/src/bin/measurements.rs index d19c19f739e..0c7f59187b6 100644 --- a/lib/rust/profiler/data/src/bin/measurements.rs +++ b/lib/rust/profiler/data/src/bin/measurements.rs @@ -10,20 +10,25 @@ //! ~/git/enso/data $ cargo run --bin measurements < profile.json | less //! ``` +// === Features === #![feature(test)] +// === Standard Linter Configuration === +#![deny(non_ascii_idents)] +#![warn(unsafe_code)] +// === Non-Standard Linter Configuration === #![deny(unconditional_recursion)] #![warn(missing_copy_implementations)] #![warn(missing_debug_implementations)] #![warn(missing_docs)] #![warn(trivial_casts)] #![warn(trivial_numeric_casts)] -#![warn(unsafe_code)] #![warn(unused_import_braces)] - use enso_profiler_data as profiler_data; use std::io::Read; + + /// Format a [`profiler_data::Interval`] in an easy-to-read way. fn fmt_interval(interval: profiler_data::Interval) -> String { let start = interval.start.into_ms(); diff --git a/lib/rust/profiler/macros/build.rs b/lib/rust/profiler/macros/build.rs index ccfb66e05b4..a68c81a58df 100644 --- a/lib/rust/profiler/macros/build.rs +++ b/lib/rust/profiler/macros/build.rs @@ -3,6 +3,7 @@ //! needs to be made aware that changes to the env can invalidate the result of compiling this //! crate and any dependents. +// === Non-Standard Linter Configuration === #![warn(missing_copy_implementations)] #![warn(missing_debug_implementations)] #![warn(missing_docs)] @@ -12,6 +13,8 @@ #![warn(unused_import_braces)] #![warn(unused_qualifications)] + + fn main() { println!("cargo:rerun-if-env-changed=ENSO_MAX_PROFILING_LEVEL"); // This is a no-op assignment, except it makes cargo aware that the output depends on the env.