mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 22:21:40 +03:00
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:
parent
286950b2a2
commit
11dfd7bfc9
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -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
|
||||
|
@ -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
10
Cargo.lock
generated
@ -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",
|
||||
|
@ -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())));
|
||||
|
@ -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)) {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -5,6 +5,7 @@
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub mod add_node_button;
|
||||
pub mod breadcrumbs;
|
||||
pub mod edge;
|
||||
pub mod node;
|
||||
|
257
app/gui/view/graph-editor/src/free_place_finder.rs
Normal file
257
app/gui/view/graph-editor/src/free_place_finder.rs
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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"),
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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>;
|
||||
|
@ -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>;
|
||||
|
@ -567,6 +567,7 @@ fn main() {
|
||||
// =============
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_formatting() {
|
||||
let input = r#"//! Module-level documentation
|
||||
//! written in two lines.
|
||||
|
@ -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',
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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" }
|
||||
|
9
lib/rust/ensogl/component/button/Cargo.toml
Normal file
9
lib/rust/ensogl/component/button/Cargo.toml
Normal 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" }
|
@ -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 }
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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)]
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user