mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 11:41:56 +03:00
GraphEditor Unit Tests (#3352)
[ci no changelog needed] This PR adds a few simple unit tests for GraphEditor, that can be used as an example of native Unit Tests. Covered: 1. Creating nodes - By internal API - By using a TAB shortcut - By using (+) button - By dropping edge 2. Connecting two nodes with an edge Some APIs were extended to allow their testing. Usage of `glyph::System` in `text/component/area` was disabled by conditional compilation, as this code can't be used in native code due to JS dependencies.
This commit is contained in:
parent
fbd80ad4a3
commit
e9f3b2327e
@ -969,3 +969,53 @@ impl display::Object for Node {
|
||||
&self.model.display_object
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// === Test Utils ===
|
||||
// ==================
|
||||
|
||||
/// Test-specific API.
|
||||
pub mod test_utils {
|
||||
use super::*;
|
||||
|
||||
/// Addional [`NodeModel`] API for tests.
|
||||
pub trait NodeModelExt {
|
||||
/// Return the `SinglePortView` of the first output port of the node.
|
||||
///
|
||||
/// Returns `None`:
|
||||
/// 1. If there are no output ports.
|
||||
/// 2. If the port does not have a `PortShapeView`. Some port models does not initialize
|
||||
/// the `PortShapeView`, see [`output::port::Model::init_shape`].
|
||||
/// 3. If the output port is [`MultiPortView`].
|
||||
fn output_port_shape(&self) -> Option<output::port::SinglePortView>;
|
||||
|
||||
/// Return the `Shape` of the first input port of the node.
|
||||
///
|
||||
/// Returns `None`:
|
||||
/// 1. If there are no input ports.
|
||||
/// 2. If the port does not have a `Shape`. Some port models does not initialize the
|
||||
/// `Shape`, see [`input::port::Model::init_shape`].
|
||||
fn input_port_shape(&self) -> Option<input::port::Shape>;
|
||||
}
|
||||
|
||||
impl NodeModelExt for NodeModel {
|
||||
fn output_port_shape(&self) -> Option<output::port::SinglePortView> {
|
||||
let ports = self.output.model.ports();
|
||||
let port = ports.first()?;
|
||||
let shape = port.shape.as_ref()?;
|
||||
use output::port::PortShapeView::Single;
|
||||
match shape {
|
||||
Single(shape) => Some(shape.clone_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn input_port_shape(&self) -> Option<input::port::Shape> {
|
||||
let ports = self.input.model.ports();
|
||||
let port = ports.first()?;
|
||||
port.shape.as_ref().map(CloneRef::clone_ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -286,6 +286,15 @@ impl Model {
|
||||
self
|
||||
}
|
||||
|
||||
/// Return a list of Node's input ports.
|
||||
pub fn ports(&self) -> Vec<port::Model> {
|
||||
let expression = self.expression.borrow();
|
||||
let mut ports = Vec::new();
|
||||
expression.span_tree.root_ref().dfs(|n| ports.push(n.payload.clone()));
|
||||
ports
|
||||
}
|
||||
|
||||
|
||||
fn set_label_layer(&self, layer: &display::scene::Layer) {
|
||||
self.label.add_to_scene_layer(layer);
|
||||
}
|
||||
@ -334,8 +343,8 @@ fn select_color(styles: &StyleWatch, tp: Option<&Type>) -> color::Lcha {
|
||||
#[derive(Clone, CloneRef, Debug)]
|
||||
pub struct Area {
|
||||
#[allow(missing_docs)]
|
||||
pub frp: Frp,
|
||||
model: Rc<Model>,
|
||||
pub frp: Frp,
|
||||
pub(crate) model: Rc<Model>,
|
||||
}
|
||||
|
||||
impl Deref for Area {
|
||||
|
@ -133,8 +133,6 @@ ensogl::define_endpoints! {
|
||||
/// `set_expression` instead. In case the usage type is set to None, ports still may be
|
||||
/// colored if the definition type was present.
|
||||
set_expression_usage_type (Crumbs,Option<Type>),
|
||||
/// Trigger `on_port_hover` output for testing purposes.
|
||||
test_port_hover (),
|
||||
}
|
||||
|
||||
Output {
|
||||
@ -218,6 +216,18 @@ impl Model {
|
||||
self
|
||||
}
|
||||
|
||||
/// Return a list of Node's output ports.
|
||||
pub fn ports(&self) -> Vec<port::Model> {
|
||||
let port_count = self.port_count.get();
|
||||
let mut ports = Vec::with_capacity(port_count);
|
||||
self.traverse_borrowed_expression(|is_a_port, node, _| {
|
||||
if is_a_port {
|
||||
ports.push(node.payload.clone());
|
||||
}
|
||||
});
|
||||
ports
|
||||
}
|
||||
|
||||
fn set_label_layer(&self, layer: &display::scene::Layer) {
|
||||
self.label.add_to_scene_layer(layer);
|
||||
}
|
||||
@ -351,8 +361,6 @@ impl Model {
|
||||
frp::extend! { port_network
|
||||
self.frp.source.on_port_hover <+ port_frp.on_hover.map
|
||||
(f!([crumbs](t) Switch::new(crumbs.clone(),*t)));
|
||||
self.frp.source.on_port_hover <+ self.frp.test_port_hover.map
|
||||
(f_!([crumbs] Switch::new(crumbs.clone(),true)));
|
||||
self.frp.source.on_port_press <+ port_frp.on_press.constant(crumbs.clone());
|
||||
|
||||
port_frp.set_size_multiplier <+ self.frp.port_size_multiplier;
|
||||
@ -425,10 +433,10 @@ impl Model {
|
||||
/// Please note that the origin of the node is on its left side, centered vertically. To learn more
|
||||
/// about this design decision, please read the docs for the [`node::Node`].
|
||||
#[derive(Clone, CloneRef, Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Area {
|
||||
#[allow(missing_docs)]
|
||||
pub frp: Frp,
|
||||
model: Rc<Model>,
|
||||
pub frp: Frp,
|
||||
pub model: Rc<Model>,
|
||||
}
|
||||
|
||||
impl Deref for Area {
|
||||
|
@ -2393,6 +2393,18 @@ pub struct GraphEditor {
|
||||
pub frp: Frp,
|
||||
}
|
||||
|
||||
impl GraphEditor {
|
||||
/// Graph editor nodes.
|
||||
pub fn nodes(&self) -> &Nodes {
|
||||
&self.model.nodes
|
||||
}
|
||||
|
||||
/// Graph editor edges.
|
||||
pub fn edges(&self) -> &Edges {
|
||||
&self.model.edges
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for GraphEditor {
|
||||
type Target = Frp;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@ -2735,9 +2747,6 @@ fn new_graph_editor(app: &Application) -> GraphEditor {
|
||||
create_edge_from_output <- node_output_touch.down.gate_not(&has_detached_edge_on_output_down);
|
||||
create_edge_from_input <- node_input_touch.down.map(|value| value.clone());
|
||||
|
||||
|
||||
// === Edge creation ===
|
||||
|
||||
on_new_edge <- any(&output_down,&input_down);
|
||||
let selection_mode = selection::get_mode(network,inputs);
|
||||
keep_selection <- selection_mode.map(|t| *t != selection::Mode::Normal);
|
||||
@ -3635,3 +3644,172 @@ impl display::Object for GraphEditor {
|
||||
self.model.display_object()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Tests ===
|
||||
// =============
|
||||
|
||||
#[cfg(test)]
|
||||
mod graph_editor_tests {
|
||||
use super::*;
|
||||
use application::test_utils::ApplicationExt;
|
||||
use ensogl::control::io::mouse::PrimaryButton;
|
||||
use ensogl::display::scene::test_utils::MouseExt;
|
||||
use node::test_utils::NodeModelExt;
|
||||
|
||||
#[test]
|
||||
fn test_adding_node_by_internal_api() {
|
||||
let (_, graph_editor) = init();
|
||||
assert_eq!(graph_editor.nodes().len(), 0);
|
||||
graph_editor.add_node();
|
||||
assert_eq!(graph_editor.nodes().len(), 1);
|
||||
graph_editor.assert(Case { node_source: None, should_edit: false });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adding_node_by_shortcut() {
|
||||
let add_node = |editor: &GraphEditor| editor.start_node_creation();
|
||||
test_adding_node(add_node);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adding_node_by_adding_node_button() {
|
||||
let add_node = |editor: &GraphEditor| {
|
||||
let adding_node_button = &editor.model.add_node_button;
|
||||
adding_node_button.click();
|
||||
};
|
||||
test_adding_node(add_node);
|
||||
}
|
||||
|
||||
fn test_adding_node(add_node: impl Fn(&GraphEditor)) {
|
||||
let (app, graph_editor) = init();
|
||||
assert_eq!(graph_editor.nodes().len(), 0);
|
||||
|
||||
// Adding first node.
|
||||
let (node_1_id, node_1) = graph_editor.add_node_by(&add_node);
|
||||
graph_editor.assert(Case { node_source: None, should_edit: true });
|
||||
graph_editor.stop_editing();
|
||||
assert_eq!(graph_editor.nodes().len(), 1);
|
||||
|
||||
// First node is created in the center of the screen.
|
||||
let node_1_pos = node_1.position();
|
||||
let screen_center = app.display.default_scene.screen_to_scene_coordinates(Vector3::zeros());
|
||||
assert_eq!(node_1_pos.xy(), screen_center.xy());
|
||||
|
||||
// Adding second node with the first node selected.
|
||||
graph_editor.nodes().select(node_1_id);
|
||||
let (_, node_2) = graph_editor.add_node_by(&add_node);
|
||||
graph_editor.assert(Case { node_source: Some(node_1_id), should_edit: true });
|
||||
assert_eq!(graph_editor.nodes().len(), 2);
|
||||
|
||||
// Second node is below the first and left-aligned to it.
|
||||
let node_2_pos = node_2.position();
|
||||
assert!(node_2_pos.y < node_1_pos.y);
|
||||
assert_eq!(node_2_pos.x, node_1_pos.x);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adding_node_by_dropping_edge() {
|
||||
let (app, graph_editor) = init();
|
||||
assert_eq!(graph_editor.nodes().len(), 0);
|
||||
// Adding a new node.
|
||||
let (node_1_id, node_1) = graph_editor.add_node_by_api();
|
||||
graph_editor.stop_editing();
|
||||
// Creating edge.
|
||||
let port = node_1.model.output_port_shape().expect("No output port.");
|
||||
port.events.mouse_down.emit(PrimaryButton);
|
||||
port.events.mouse_up.emit(PrimaryButton);
|
||||
assert_eq!(graph_editor.edges().len(), 1);
|
||||
// Dropping edge.
|
||||
let mouse = &app.display.default_scene.mouse;
|
||||
let click_pos = Vector2(300.0, 300.0);
|
||||
mouse.frp.position.emit(click_pos);
|
||||
let click_on_background = |_: &GraphEditor| mouse.click_on_background();
|
||||
let (_, node_2) = graph_editor.add_node_by(&click_on_background);
|
||||
graph_editor.assert(Case { node_source: Some(node_1_id), should_edit: true });
|
||||
let node_pos = node_2.position();
|
||||
assert_eq!(node_pos.xy(), click_pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connecting_two_nodes() {
|
||||
let (_, ref graph_editor) = init();
|
||||
let edges = graph_editor.edges();
|
||||
assert!(graph_editor.nodes().is_empty());
|
||||
assert!(edges.is_empty());
|
||||
// Adding two nodes.
|
||||
let (node_id_1, node_1) = graph_editor.add_node_by_api();
|
||||
graph_editor.stop_editing();
|
||||
let (node_id_2, node_2) = graph_editor.add_node_by_api();
|
||||
graph_editor.stop_editing();
|
||||
// Creating edge.
|
||||
let port = node_1.model.output_port_shape().expect("No output port.");
|
||||
port.events.mouse_down.emit(PrimaryButton);
|
||||
port.events.mouse_up.emit(PrimaryButton);
|
||||
let edge_id = graph_editor.on_edge_add.value();
|
||||
let edge = edges.get_cloned_ref(&edge_id).expect("Edge was not added.");
|
||||
assert_eq!(edge.source().map(|e| e.node_id), Some(node_id_1));
|
||||
assert_eq!(edge.target().map(|e| e.node_id), None);
|
||||
assert_eq!(edges.len(), 1);
|
||||
// Connecting edge.
|
||||
// We need to enable ports. Normally it is done by hovering the node.
|
||||
node_2.model.input.frp.set_ports_active(true, None);
|
||||
let port = node_2.model.input_port_shape().expect("No input port.");
|
||||
port.hover.events.mouse_down.emit(PrimaryButton);
|
||||
port.hover.events.mouse_up.emit(PrimaryButton);
|
||||
assert_eq!(edge.source().map(|e| e.node_id), Some(node_id_1));
|
||||
assert_eq!(edge.target().map(|e| e.node_id), Some(node_id_2));
|
||||
}
|
||||
|
||||
|
||||
// === Test utilities ===
|
||||
|
||||
/// An assertion case used when adding new nodes. See [`GraphEditor::assert`] below.
|
||||
struct Case {
|
||||
/// A source node of the added node.
|
||||
node_source: Option<NodeId>,
|
||||
/// Should we start the node editing immediately after adding it?
|
||||
should_edit: bool,
|
||||
}
|
||||
|
||||
impl GraphEditor {
|
||||
fn add_node_by<F: Fn(&GraphEditor)>(&self, add_node: &F) -> (NodeId, Node) {
|
||||
add_node(self);
|
||||
let (node_id, ..) = self.node_added.value();
|
||||
let node = self.nodes().get_cloned_ref(&node_id).expect("Node was not added.");
|
||||
node.set_expression(node::Expression::new_plain("some_not_empty_expression"));
|
||||
(node_id, node)
|
||||
}
|
||||
|
||||
fn add_node_by_api(&self) -> (NodeId, Node) {
|
||||
let add_node = |editor: &GraphEditor| editor.add_node();
|
||||
self.add_node_by(&add_node)
|
||||
}
|
||||
|
||||
fn assert(&self, case: Case) {
|
||||
let (added_node, node_source, should_edit) = self.node_added.value();
|
||||
let node_being_edited = self.node_being_edited.value();
|
||||
assert_eq!(
|
||||
should_edit, case.should_edit,
|
||||
"Node editing state does not match expected."
|
||||
);
|
||||
assert_eq!(should_edit, node_being_edited.is_some());
|
||||
if let Some(node_being_edited) = node_being_edited {
|
||||
assert_eq!(node_being_edited, added_node, "Edited node does not match added one.");
|
||||
}
|
||||
let node_source = node_source.map(|source| source.node);
|
||||
assert_eq!(node_source, case.node_source, "Source node does not match expected.");
|
||||
}
|
||||
}
|
||||
|
||||
fn init() -> (Application, GraphEditor) {
|
||||
let app = Application::new("root");
|
||||
app.set_screen_size_for_tests();
|
||||
let graph_editor = new_graph_editor(&app);
|
||||
let mouse = &app.display.default_scene.mouse;
|
||||
mouse.frp.position.emit(Vector2::zeros());
|
||||
(app, graph_editor)
|
||||
}
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ impl Model {
|
||||
}
|
||||
|
||||
fn searcher_left_top_position_when_under_node(&self, node_id: NodeId) -> Vector2<f32> {
|
||||
if let Some(node) = self.graph_editor.model.nodes.get_cloned_ref(&node_id) {
|
||||
if let Some(node) = self.graph_editor.nodes().get_cloned_ref(&node_id) {
|
||||
Self::searcher_left_top_position_when_under_node_at(node.position().xy())
|
||||
} else {
|
||||
error!(self.logger, "Trying to show searcher under nonexisting node");
|
||||
@ -252,7 +252,7 @@ impl Model {
|
||||
}
|
||||
|
||||
fn show_fullscreen_visualization(&self, node_id: NodeId) {
|
||||
let node = self.graph_editor.model.model.nodes.all.get_cloned_ref(&node_id);
|
||||
let node = self.graph_editor.nodes().get_cloned_ref(&node_id);
|
||||
if let Some(node) = node {
|
||||
let visualization =
|
||||
node.view.model.visualization.fullscreen_visualization().clone_ref();
|
||||
|
@ -26,7 +26,7 @@ use enso_gui::executor::web::EventLoopExecutor;
|
||||
use enso_gui::initializer::setup_global_executor;
|
||||
use enso_gui::Ide;
|
||||
use enso_web::HtmlDivElement;
|
||||
use ensogl::application::Application;
|
||||
use ensogl::application::test_utils::ApplicationExt;
|
||||
|
||||
|
||||
|
||||
@ -57,8 +57,6 @@ pub struct IntegrationTest {
|
||||
}
|
||||
|
||||
impl IntegrationTest {
|
||||
const SCREEN_SIZE: (f32, f32) = (1920.0, 1000.0);
|
||||
|
||||
/// Initializes the executor and `Ide` structure and returns new Fixture.
|
||||
pub async fn setup() -> Self {
|
||||
let executor = setup_global_executor();
|
||||
@ -69,16 +67,9 @@ impl IntegrationTest {
|
||||
|
||||
let initializer = enso_gui::ide::Initializer::new(default());
|
||||
let ide = initializer.start().await.expect("Failed to initialize the application.");
|
||||
Self::set_screen_size(&ide.ensogl_app);
|
||||
ide.ensogl_app.set_screen_size_for_tests();
|
||||
Self { executor, ide, root_div }
|
||||
}
|
||||
|
||||
fn set_screen_size(app: &Application) {
|
||||
let (screen_width, screen_height) = Self::SCREEN_SIZE;
|
||||
app.display.default_scene.layers.iter_sublayers_and_masks_nested(|layer| {
|
||||
layer.camera().set_screen(screen_width, screen_height)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for IntegrationTest {
|
||||
|
@ -5,6 +5,7 @@
|
||||
use enso_integration_test::prelude::*;
|
||||
|
||||
use approx::assert_abs_diff_eq;
|
||||
use enso_gui::view::graph_editor::component::node::test_utils::NodeModelExt;
|
||||
use enso_gui::view::graph_editor::component::node::Expression;
|
||||
use enso_gui::view::graph_editor::GraphEditor;
|
||||
use enso_gui::view::graph_editor::Node;
|
||||
@ -24,15 +25,15 @@ async fn create_new_project_and_add_nodes() {
|
||||
let test = IntegrationTestOnNewProject::setup().await;
|
||||
let graph_editor = test.graph_editor();
|
||||
|
||||
assert_eq!(graph_editor.model.nodes.all.len(), 2);
|
||||
assert_eq!(graph_editor.nodes().all.len(), 2);
|
||||
let expect_node_added = graph_editor.node_added.next_event();
|
||||
graph_editor.add_node();
|
||||
let (added_node_id, source_node, _) = expect_node_added.expect();
|
||||
assert_eq!(source_node, None);
|
||||
assert_eq!(graph_editor.model.nodes.all.len(), 3);
|
||||
assert_eq!(graph_editor.nodes().all.len(), 3);
|
||||
|
||||
let added_node =
|
||||
graph_editor.model.nodes.get_cloned_ref(&added_node_id).expect("Added node is not added");
|
||||
graph_editor.nodes().get_cloned_ref(&added_node_id).expect("Added node is not added");
|
||||
assert_eq!(added_node.view.expression.value().to_string(), "");
|
||||
}
|
||||
|
||||
@ -118,7 +119,7 @@ async fn adding_node_with_add_node_button() {
|
||||
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 = graph_editor.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 =
|
||||
@ -127,7 +128,7 @@ async fn adding_node_with_add_node_button() {
|
||||
// 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);
|
||||
assert_eq!(graph_editor.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!(
|
||||
@ -136,21 +137,21 @@ async fn adding_node_with_add_node_button() {
|
||||
);
|
||||
|
||||
// Selected node is used as a `source` node.
|
||||
graph_editor.model.nodes.deselect_all();
|
||||
graph_editor.model.nodes.select(first_node_id);
|
||||
graph_editor.nodes().deselect_all();
|
||||
graph_editor.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);
|
||||
assert_eq!(graph_editor.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();
|
||||
graph_editor.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);
|
||||
assert_eq!(graph_editor.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);
|
||||
@ -163,9 +164,9 @@ async fn adding_node_by_clicking_on_the_output_port() {
|
||||
let graph_editor = test.graph_editor();
|
||||
let (node_1_id, _, node_1) = add_node_with_internal_api(&graph_editor, "1 + 1");
|
||||
|
||||
let output = &node_1.model.output;
|
||||
let method = |editor: &GraphEditor| {
|
||||
output.test_port_hover();
|
||||
let port = node_1.model.output_port_shape().expect("No output port");
|
||||
port.events.mouse_over.emit(());
|
||||
editor.start_node_creation_from_port();
|
||||
};
|
||||
let (_, source, node_2) = add_node(&graph_editor, "+ 1", method);
|
||||
@ -182,7 +183,7 @@ fn add_node(
|
||||
let node_added = graph_editor.node_added.next_event();
|
||||
method(graph_editor);
|
||||
let (node_id, source_node, _) = node_added.expect();
|
||||
let node = graph_editor.model.nodes.get_cloned_ref(&node_id).expect("Node was not added");
|
||||
let node = graph_editor.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, node)
|
||||
|
@ -11,9 +11,12 @@ use crate::buffer::Text;
|
||||
use crate::buffer::Transform;
|
||||
use crate::component::selection;
|
||||
use crate::component::Selection;
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_imports))]
|
||||
use crate::typeface;
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_imports))]
|
||||
use crate::typeface::glyph;
|
||||
use crate::typeface::glyph::Glyph;
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_imports))]
|
||||
use crate::typeface::pen;
|
||||
|
||||
use enso_frp as frp;
|
||||
@ -47,6 +50,7 @@ const LINE_VERTICAL_OFFSET: f32 = 4.0; // Set manually. May depend on font. To b
|
||||
|
||||
/// Mapping between selection id, `Selection`, and text location.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
pub struct SelectionMap {
|
||||
id_map: HashMap<usize, Selection>,
|
||||
location_map: HashMap<usize, HashMap<Column, usize>>,
|
||||
@ -64,6 +68,7 @@ pub struct SelectionMap {
|
||||
/// The `divs` and `centers` are kept as vectors for performance reasons. Especially, when clicking
|
||||
/// inside of the text area, it allows us to binary search the place of the mouse pointer.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
pub struct Line {
|
||||
display_object: display::object::Instance,
|
||||
glyphs: Vec<Glyph>,
|
||||
@ -82,6 +87,7 @@ impl Line {
|
||||
}
|
||||
|
||||
/// Set the division points (offsets between letters). Also updates center points.
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
fn set_divs(&mut self, divs: Vec<f32>) {
|
||||
let div_iter = divs.iter();
|
||||
let div_iter_skipped = divs.iter().skip(1);
|
||||
@ -93,11 +99,13 @@ impl Line {
|
||||
self.centers.binary_search_by(|t| t.partial_cmp(&offset).unwrap()).unwrap_both()
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
fn div_by_column(&self, column: Column) -> f32 {
|
||||
let ix = column.as_usize().min(self.divs.len() - 1);
|
||||
self.divs[ix]
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
fn resize_with(&mut self, size: usize, cons: impl Fn() -> Glyph) {
|
||||
let display_object = self.display_object().clone_ref();
|
||||
self.glyphs.resize_with(size, move || {
|
||||
@ -530,6 +538,12 @@ impl Area {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn symbols(&self) -> SmallVec<[display::Symbol; 1]> {
|
||||
default()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn symbols(&self) -> SmallVec<[display::Symbol; 1]> {
|
||||
let text_symbol = self.data.glyph_system.sprite_system().symbol.clone_ref();
|
||||
let shapes = &self.data.app.display.default_scene.shapes;
|
||||
@ -558,6 +572,7 @@ pub struct AreaModel {
|
||||
frp_endpoints: FrpEndpoints,
|
||||
buffer: buffer::View,
|
||||
display_object: display::object::Instance,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
glyph_system: glyph::System,
|
||||
lines: Lines,
|
||||
single_line: Rc<Cell<bool>>,
|
||||
@ -571,15 +586,19 @@ impl AreaModel {
|
||||
let scene = &app.display.default_scene;
|
||||
let logger = Logger::new("text_area");
|
||||
let selection_map = default();
|
||||
let fonts = scene.extension::<typeface::font::Registry>();
|
||||
let font = fonts.load("DejaVuSansMono");
|
||||
let glyph_system = typeface::glyph::System::new(&scene, font);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let glyph_system = {
|
||||
let fonts = scene.extension::<typeface::font::Registry>();
|
||||
let font = fonts.load("DejaVuSansMono");
|
||||
typeface::glyph::System::new(&scene, font)
|
||||
};
|
||||
let display_object = display::object::Instance::new(&logger);
|
||||
let buffer = default();
|
||||
let lines = default();
|
||||
let single_line = default();
|
||||
let camera = Rc::new(CloneRefCell::new(scene.camera().clone_ref()));
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
display_object.add_child(&glyph_system);
|
||||
|
||||
// FIXME[WD]: These settings should be managed wiser. They should be set up during
|
||||
@ -603,6 +622,7 @@ impl AreaModel {
|
||||
frp_endpoints,
|
||||
buffer,
|
||||
display_object,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
glyph_system,
|
||||
lines,
|
||||
single_line,
|
||||
@ -611,6 +631,10 @@ impl AreaModel {
|
||||
.init()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn on_modified_selection(&self, _: &buffer::selection::Group, _: f32, _: bool) {}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn on_modified_selection(
|
||||
&self,
|
||||
selections: &buffer::selection::Group,
|
||||
@ -750,6 +774,12 @@ impl AreaModel {
|
||||
self.lines.len() as f32 * LINE_HEIGHT
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn redraw_line(&self, _: usize, _: String) -> f32 {
|
||||
0.0
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn redraw_line(&self, view_line_index: usize, content: String) -> f32 {
|
||||
let cursor_map = self
|
||||
.selection_map
|
||||
@ -881,6 +911,7 @@ impl AreaModel {
|
||||
}
|
||||
|
||||
/// Constrain the selection to values fitting inside of the current text buffer.
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
fn snap_selection(
|
||||
&self,
|
||||
selection: buffer::selection::Selection,
|
||||
|
@ -18,6 +18,7 @@ use ensogl_core::DEPRECATED_Animation;
|
||||
const CURSOR_PADDING: f32 = 4.0;
|
||||
const CURSOR_WIDTH: f32 = 2.0;
|
||||
const CURSOR_ALPHA: f32 = 0.8;
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
const CURSORS_SPACING: f32 = 1.0;
|
||||
const SELECTION_ALPHA: f32 = 0.3;
|
||||
const SELECTION_CORNER_RADIUS: f32 = 2.0;
|
||||
@ -130,6 +131,7 @@ impl Deref for Selection {
|
||||
|
||||
impl Selection {
|
||||
/// Constructor.
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
pub fn new(logger: impl AnyLogger, edit_mode: bool) -> Self {
|
||||
let logger = Logger::new_sub(logger, "selection");
|
||||
let display_object = display::object::Instance::new(&logger);
|
||||
@ -160,6 +162,7 @@ impl Selection {
|
||||
.init()
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
fn init(self) -> Self {
|
||||
let network = &self.network;
|
||||
let view = &self.shape_view;
|
||||
@ -189,6 +192,7 @@ impl Selection {
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
pub fn flip_sides(&self) {
|
||||
let width = self.width.target_value();
|
||||
self.position.set_value(self.position.value() + Vector2(width, 0.0));
|
||||
|
@ -83,6 +83,39 @@ impl AsRef<theme::Manager> for Application {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// === Test Utils ===
|
||||
// ==================
|
||||
|
||||
/// Test-specific API.
|
||||
pub mod test_utils {
|
||||
use super::*;
|
||||
|
||||
/// Screen size for unit and integration tests.
|
||||
const TEST_SCREEN_SIZE: (f32, f32) = (1920.0, 1080.0);
|
||||
|
||||
/// Extended API for tests.
|
||||
pub trait ApplicationExt {
|
||||
/// Set "fake" screen dimensions for unit and integration tests. This is important for a lot
|
||||
/// of position and screen size related computations in the IDE.
|
||||
fn set_screen_size_for_tests(&self);
|
||||
}
|
||||
|
||||
impl ApplicationExt for Application {
|
||||
fn set_screen_size_for_tests(&self) {
|
||||
let (screen_width, screen_height) = TEST_SCREEN_SIZE;
|
||||
let scene = &self.display.default_scene;
|
||||
scene.layers.iter_sublayers_and_masks_nested(|layer| {
|
||||
let camera = layer.camera();
|
||||
camera.set_screen(screen_width, screen_height);
|
||||
camera.reset_zoom();
|
||||
camera.update(scene);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============
|
||||
// === Tests ===
|
||||
// =============
|
||||
|
@ -1145,3 +1145,28 @@ impl<'t> DomPath for &'t str {
|
||||
web::document.get_html_element_by_id(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// === Test Utils ===
|
||||
// ==================
|
||||
|
||||
/// Extended API for tests.
|
||||
pub mod test_utils {
|
||||
use super::*;
|
||||
|
||||
pub trait MouseExt {
|
||||
/// Emulate click on background for testing purposes.
|
||||
fn click_on_background(&self);
|
||||
}
|
||||
|
||||
impl MouseExt for Mouse {
|
||||
fn click_on_background(&self) {
|
||||
self.target.set(PointerTargetId::Background);
|
||||
let left_mouse_button = frp::io::mouse::Button::Button0;
|
||||
self.frp.down.emit(left_mouse_button);
|
||||
self.frp.up.emit(left_mouse_button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,3 @@
|
||||
// === Standard Linter Configuration ===
|
||||
#![deny(non_ascii_idents)]
|
||||
#![warn(unsafe_code)]
|
||||
//! Interface to profile data.
|
||||
//!
|
||||
//! # Overview
|
||||
@ -91,21 +88,29 @@
|
||||
//! store_and_retrieve_metadata();
|
||||
//! ```
|
||||
|
||||
// === 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 as profiler;
|
||||
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub mod parse;
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user