Return creating node with (+) button & fix a regression (#3338)

* Creating a new node with the (+) button (#3278)

[The Task](https://www.pivotaltracker.com/story/show/180887253)

A new (+) button on the left-bottom corner appeared. It may be clicked to open searcher in the middle of the scene, as an alternative to tab key.

https://user-images.githubusercontent.com/3919101/154514279-7972ed6a-0203-47cb-9a09-82dba948cf2f.mp4

* The window_control_buttons::common was extracted to separate crate `ensogl-component-button` almost without change.
* This includes a severe refactoring of adding nodes in general in the Graph Editor. The whole responsibility of adding new nodes (and starting their editing) was moved to Graph Editor - the Project View only reacts for GE events to show searcher properly.
* The status bar was moved from the bottom-left corner to the middle-top of the scene. It does not collide with (+) button, and plays "notification" role anyway.
* The `interface` debug scene was buggy. The problem was with one expression's span-tree. When I replaced it, the scene works.
* I've removed "new searcher" API, as it is completely outdated.
* I've changed code owners of integration tests to GUI team, as it is the team writing mostly the integration tests (int rust)

* Fix regression #181528359

* Add docs & remove unused function

* Fix & enable native Rust tests

* Fix formatting

Co-authored-by: Adam Obuchowicz <adam.obuchowicz@enso.org>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Ilya Bogdanov 2022-03-16 21:02:47 +03:00 committed by GitHub
parent 286950b2a2
commit 11dfd7bfc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1072 additions and 670 deletions

1
.github/CODEOWNERS vendored
View File

@ -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

View File

@ -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

10
Cargo.lock generated
View File

@ -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",

View File

@ -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())));

View File

@ -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)) {

View File

@ -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<Self> {
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<Uuid> {
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))
}
}
}

View File

@ -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];

View File

@ -5,6 +5,7 @@
// === Export ===
// ==============
pub mod add_node_button;
pub mod breadcrumbs;
pub mod edge;
pub mod node;

View File

@ -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<f32> {
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<f32> {
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<Item = OccupiedArea>,
) -> Option<Vector2> {
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<OccupiedArea>,
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();
}
}

View File

@ -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<K, V, S> SharedHashMap<K, V, S> {
// === 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<NodeSource>, bool),
node_removed (NodeId),
nodes_collapsed ((Vec<NodeId>,NodeId)),
node_hovered (Option<Switch<NodeId>>),
@ -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<NodeId>) {
pub fn select(&self, node_id: impl Into<NodeId>) {
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<NodeId>) {
pub fn deselect(&self, node_id: impl Into<NodeId>) {
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<cursor::Style>,
tooltip_update: &'a frp::Source<tooltip::Style>,
output_press: &'a frp::Source<EdgeEndpoint>,
input_press: &'a frp::Source<EdgeEndpoint>,
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<Vector3<f32>> {
self.nodes.get_cloned_ref(&node_id).map(|node| node.position())
}
fn create_edge(
&self,
edge_click: &frp::Source<EdgeId>,
edge_over: &frp::Source<EdgeId>,
edge_out: &frp::Source<EdgeId>,
) -> 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<EdgeId>,
edge_over: &frp::Source<EdgeId>,
edge_out: &frp::Source<EdgeId>,
) -> 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<EdgeId>,
edge_over: &frp::Source<EdgeId>,
edge_out: &frp::Source<EdgeId>,
) -> 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<EdgeEndpoint>,
input_press: &'a frp::Source<EdgeEndpoint>,
output: &'a FrpEndpoints,
}
impl GraphEditorModelWithNetwork {
fn create_node(
&self,
ctx: &NodeCreationContext,
way: WayOfCreatingNode,
mouse_position: Vector2,
) -> (NodeId, Option<NodeSource>, 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<Vector3<f32>> {
self.nodes.get_cloned_ref(&node_id).map(|node| node.position())
}
fn create_edge(
&self,
edge_click: &frp::Source<EdgeId>,
edge_over: &frp::Source<EdgeId>,
edge_out: &frp::Source<EdgeId>,
) -> 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<EdgeId>,
edge_over: &frp::Source<EdgeId>,
edge_out: &frp::Source<EdgeId>,
) -> 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<EdgeId>,
edge_over: &frp::Source<EdgeId>,
edge_out: &frp::Source<EdgeId>,
) -> 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<component::add_node_button::AddNodeButton>,
// 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<Vector2> {
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<NodeId> {
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<EdgeEndpoint>) {
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<application::shortcut::Shortcut> {
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::<cursor::Style>();
node_tooltip <- source::<tooltip::Style>();
node_pointer_style <- any_mut::<(NodeId, cursor::Style)>();
node_tooltip <- any_mut::<(NodeId, tooltip::Style)>();
let node_input_touch = TouchNetwork::<EdgeEndpoint>::new(network,mouse);
let node_output_touch = TouchNetwork::<EdgeEndpoint>::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);

View File

@ -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<EdgeId> {
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<NodeSource>,
}
impl SearcherParams {
fn new_for_new_node(node_id: NodeId, source_node: Option<NodeSource>) -> 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<searcher::entry::Id>),
open_dialog_shown (bool),
code_editor_shown (bool),
style (Theme),
fullscreen_visualization_shown (bool),
drop_files_enabled (bool),
debug_mode (bool),
searcher (Option<SearcherParams>),
is_searcher_opened (bool),
adding_new_node (bool),
old_expression_of_edited_node (Expression),
editing_aborted (NodeId),
editing_committed (NodeId, Option<searcher::entry::Id>),
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<NodeId>,
searcher_parameters: Option<SearcherParams>,
is_searcher_empty: bool,
searcher_left_top_position: &DEPRECATED_Animation<Vector2<f32>>,
) {
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>) -> 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<usize> {
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<application::shortcut::Shortcut> {
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"),

View File

@ -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<Entry>,
new_view: new::View<usize>,
documentation: documentation::View,
doc_provider: Rc<CloneRefCell<AnyDocumentationProvider>>,
}
@ -126,7 +124,6 @@ impl Model {
let logger = Logger::new("SearcherView");
let display_object = display::object::Instance::new(&logger);
let list = app.new_view::<ListView<Entry>>();
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<entry::Id>) -> 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<usize> {
&self.model.new_view.frp
}
}
impl display::Object for View {

View File

@ -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<bool>,
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<ImString>) -> Icon {
let name = name.into();
Icon { name }
}
// ===========
// === FRP ===
// ===========
/// A type representing path to entry in some column.
pub type EntryPath<Id> = Rc<Vec<Id>>;
ensogl::define_endpoints! { <Id:(Debug+Clone+'static)>
Input {
reset(),
directory_content (EntryPath<Id>,Entry),
set_highlight (EntryPath<Id>),
}
Output {
list_directory (EntryPath<Id>),
highlight (EntryPath<Id>),
entry_chosen (EntryPath<Id>),
}
}
/// 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<Id: Debug + Clone + 'static> {
pub frp: Frp<Id>,
}
impl<Id: Debug + Clone + 'static> Deref for View<Id> {
type Target = Frp<Id>;
fn deref(&self) -> &Self::Target {
&self.frp
}
}
impl<ID: ToString + Debug + Clone + 'static> View<ID> {
/// 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<Id: ToString + Debug + Clone + 'static> Default for View<Id> {
fn default() -> Self {
Self::new()
}
}

View File

@ -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 {

View File

@ -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<T> LayoutParams<T> {
impl LayoutParams<frp::Sampler<f32>> {
/// 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);

View File

@ -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::DynamicShape>;
// =============
// === 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<shape::DynamicShape>;

View File

@ -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::DynamicShape>;
// =============
// === 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<shape::DynamicShape>;

View File

@ -567,6 +567,7 @@ fn main() {
// =============
#[test]
#[ignore]
fn test_formatting() {
let input = r#"//! Module-level documentation
//! written in two lines.

View File

@ -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',

View File

@ -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"

View File

@ -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<NodeSource>) {
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)
}

View File

@ -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 {

View File

@ -5,6 +5,7 @@ authors = ["Enso Team <contact@enso.org>"]
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" }

View File

@ -0,0 +1,9 @@
[package]
name = "ensogl-button"
version = "0.1.0"
authors = ["Enso Team <contact@enso.org>"]
edition = "2021"
[dependencies]
enso-frp = { path = "../../../frp" }
ensogl-core = { path = "../../core" }

View File

@ -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<f32>, icon_color:Vector4<f32>) {
//! 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<Attribute<Vector4<f32>>> {
//! &self.background_color
//! }
//!
//! fn icon_color(&self) -> &DynamicParam<Attribute<Vector4<f32>>> {
//! &self.icon_color
//! }
//! }
//!
//! // Finally, we can create the full component by aliasing [`View`] structure.
//!
//! pub type View = ensogl_button::View<shape::DynamicShape>;
//! ```
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<Shape: ButtonShape> Model<Shape> {
pub fn set_icon_color(&self, color: impl Into<Rgba>) {
self.shape.icon_color().set(color.into().into());
}
/// Retrieves circle radius value from an frp sampler event.
fn get_radius(radius: &Option<style::data::Data>) -> 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<style::data::Data>) -> Vector2<f32> {
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<f32> {
Vector2(radius, radius) * 2.0
}
}
@ -177,9 +235,11 @@ impl<Shape: ButtonShape> Model<Shape> {
// === 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<Shape: ButtonShape> View<Shape> {
/// Constructor.
pub fn new(app: &Application) -> Self {
let frp = Frp::new();
let model = Model::new(app);
let model = Model::<Shape>::new(app);
let network = &frp.network;
let scene = &app.display.default_scene;
let style = StyleWatchFrp::new(&scene.style_sheet);
@ -239,10 +295,6 @@ impl<Shape: ButtonShape> View<Shape> {
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<Shape: ButtonShape> View<Shape> {
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<Shape: ButtonShape> View<Shape> {
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<Shape: ButtonShape> View<Shape> {
});
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<Shape: ButtonShape> View<Shape> {
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 }
}

View File

@ -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;

View File

@ -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;

View File

@ -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)]

View File

@ -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();

View File

@ -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.