From ea01ead2fd4fd65a11be91d00b5da7b354c25789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Mon, 20 Mar 2023 20:07:29 +0100 Subject: [PATCH 01/45] common widget configuration struct --- app/gui/src/controller/graph/widget.rs | 81 +---- .../src/controller/graph/widget/metadata.rs | 66 ++++ .../src/controller/graph/widget/response.rs | 135 ++++++++ app/gui/view/examples/interface/src/lib.rs | 12 +- .../src/component/node/input/widget.rs | 191 ++++++++--- .../src/component/node/input/widget/label.rs | 24 ++ .../node/input/widget/single_choice.rs | 316 ++++++++++++++++++ .../node/input/widget/vector_editor.rs | 22 +- 8 files changed, 717 insertions(+), 130 deletions(-) create mode 100644 app/gui/src/controller/graph/widget/metadata.rs create mode 100644 app/gui/src/controller/graph/widget/response.rs create mode 100644 app/gui/view/graph-editor/src/component/node/input/widget/label.rs create mode 100644 app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs diff --git a/app/gui/src/controller/graph/widget.rs b/app/gui/src/controller/graph/widget.rs index b87648a78a54..0c113a5c28f1 100644 --- a/app/gui/src/controller/graph/widget.rs +++ b/app/gui/src/controller/graph/widget.rs @@ -3,6 +3,11 @@ //! The Widget Controller is responsible for querying the language server for information about //! the node's widget metadata or resolving it from local cache. + + +mod metadata; +mod response; + use crate::prelude::*; use crate::controller::visualization::manager::Manager; @@ -170,7 +175,7 @@ impl Model { ) -> Option<(NodeId, WidgetUpdates)> { let query_data = self.widget_queries.get_mut(&target)?; - let (updates, errors) = VisualizationData::try_deserialize(&data); + let (updates, errors) = metadata::deserialize_widget_update(&data); for error in errors { error!("{:?}", error); @@ -416,77 +421,3 @@ impl QueryData { buffer } } - - - -/// =============================== -/// === WidgetVisualizationData === -/// =============================== - -/// A type representing the data received from the widget visualization for a single widget. -/// -/// The structure of this struct is dictated by the expected widget visualization JSON result shape. -#[derive(Debug, serde::Deserialize)] -struct VisualizationData { - constructor: widget::Kind, - display: VisualizationDataDisplay, - values: Vec, -} - -#[derive(Debug, serde::Deserialize)] -struct VisualizationDataDisplay { - constructor: widget::Display, -} - -#[derive(Debug, serde::Deserialize)] -struct VisualizationDataChoice { - value: String, - label: Option, -} - -impl From<&VisualizationDataChoice> for widget::Entry { - fn from(choice: &VisualizationDataChoice) -> Self { - let value: ImString = (&choice.value).into(); - let label = choice.label.as_ref().map_or_else(|| value.clone(), |label| label.into()); - Self { required_import: None, value, label } - } -} - -impl VisualizationData { - fn into_metadata(self) -> widget::Metadata { - let kind = self.constructor; - let display = self.display.constructor; - let dynamic_entries = self.values.iter().map(Into::into).collect(); - widget::Metadata { kind, display, dynamic_entries } - } - - /// Try to deserialize widget visualization update data. If deserialization fails for only part - /// of the response, the rest of the response is still processed, while errors are returned - /// separately for each failed widget. - fn try_deserialize(data: &VisualizationUpdateData) -> (Vec, Vec) { - let arguments: Vec<(String, serde_json::Value)> = match serde_json::from_slice(data) { - Ok(args) => args, - Err(err) => { - let err = err - .context("Failed to deserialize a list of arguments in widget response") - .into(); - return (default(), vec![err]); - } - }; - - let updates = arguments.into_iter().map( - |(argument_name, meta_json)| -> FallibleResult { - let deserialized = serde_json::from_value(meta_json); - let deserialized: Option = deserialized.map_err(|e| { - let message = - format!("Failed to deserialize widget data for argument '{argument_name}'"); - e.context(message) - })?; - let meta = deserialized.map(VisualizationData::into_metadata); - Ok(WidgetUpdate { argument_name, meta }) - }, - ); - - updates.partition_result() - } -} diff --git a/app/gui/src/controller/graph/widget/metadata.rs b/app/gui/src/controller/graph/widget/metadata.rs new file mode 100644 index 000000000000..43c6cd034557 --- /dev/null +++ b/app/gui/src/controller/graph/widget/metadata.rs @@ -0,0 +1,66 @@ +//! This module contains the mappings of widget visualization responses into metadata structs used +//! by the views. + +use crate::prelude::*; + +use crate::model::execution_context::VisualizationUpdateData; + +use super::response; +use ide_view::graph_editor::component::node::input::widget; +use ide_view::graph_editor::WidgetUpdate; + + + +pub fn deserialize_widget_update( + data: &VisualizationUpdateData, +) -> (Vec, Vec) { + match serde_json::from_slice::(data) { + Ok(response) => { + let updates = response.into_iter().map( + |(argument_name, fallable_widget)| -> FallibleResult { + let widget: Option = fallable_widget.0.map_err(|e| { + e.context(format!( + "Failed to deserialize widget data for argument '{argument_name}'" + )) + })?; + let meta = widget.map(map_metadata); + let argument_name = argument_name.to_owned(); + Ok(WidgetUpdate { argument_name, meta }) + }, + ); + + updates.partition_result() + } + Err(err) => { + let err = + err.context("Failed to deserialize a list of arguments in widget response").into(); + (default(), vec![err]) + } + } +} + +fn map_metadata(resp: response::Widget) -> widget::Metadata { + widget::Metadata { + label: resp.label.map(Into::into), + display: resp.display, + config: map_config(resp.inner), + } +} + +fn map_config(inner: response::WidgetSpecific) -> widget::Config { + match inner { + response::WidgetSpecific::SingleChoice { values } => + widget::single_choice::Config { entries: Rc::new(map_entries(&values)) }.into(), + _ => widget::label::Config.into(), + } +} + +fn map_entries(choices: &[response::Choice]) -> Vec { + choices.iter().map(map_entry).collect() +} + +fn map_entry(choice: &response::Choice) -> widget::Entry { + let value: ImString = (&choice.value).into(); + let label = choice.label.as_ref().map_or_else(|| value.clone(), |label| label.into()); + widget::Entry { required_import: None, value, label } +} diff --git a/app/gui/src/controller/graph/widget/response.rs b/app/gui/src/controller/graph/widget/response.rs new file mode 100644 index 000000000000..a4bbeb50a272 --- /dev/null +++ b/app/gui/src/controller/graph/widget/response.rs @@ -0,0 +1,135 @@ +//! The module containing the types used for deserializing language-server responses containing +//! widget configuration. + +use crate::prelude::*; + +use ide_view::graph_editor::component::node::input::widget; + + + +/// ===================== +/// === WidgetUpdates === +/// ===================== + +/// A complete object received from the widget visualization. Contain widget definitions for all +/// requested arguments. +/// +/// Pairs of argument name and associated widget configuration. +pub(super) type WidgetUpdates<'a> = Vec<(&'a str, FallableWidgetData<'a>)>; + +/// A wrapper type that allows deserialization of a widget to partially fail. Failure message of +/// individual widget data deserialization will be preserved, and it will be allowed to continue +/// deserializing other widgets. +#[derive(Debug)] +pub(super) struct FallableWidgetData<'a>(pub(super) FallibleResult>>); + +impl<'de: 'a, 'a> serde::Deserialize<'de> for FallableWidgetData<'a> { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + let widget = >::deserialize(deserializer) + .map_err(|e| failure::err_msg(e.to_string())); + Ok(Self(widget)) + } +} + + +/// ============== +/// === Widget === +/// ============== + +/// Describes the widget configuration, as provided by the node metadata from the engine. +/// +/// Must be kept in sync with `Widget` type definition in `Standard.Base.Metadata` module. +/// In order to not ruin forward compatibility, only fields that are currently used by the IDE are +/// specified and deserialized. +#[derive(Debug, serde::Deserialize)] +pub(super) struct Widget<'a> { + /// The placeholder text value. By default, the parameter name is used. + #[serde(borrow, default)] + pub label: Option<&'a str>, + /// The display mode for the parameter. + #[serde(default)] + pub display: widget::Display, + #[serde(borrow, flatten)] + pub inner: WidgetSpecific<'a>, +} + +/// Widget type dependant fields. +#[derive(Debug, serde::Deserialize)] +#[serde(tag = "constructor")] +pub(super) enum WidgetSpecific<'a> { + /// Describes a single value widget (dropdown). + #[serde(rename = "Single_Choice")] + SingleChoice { + /// A list of choices to display. + #[serde(borrow, default)] + values: Vec>, + }, + + /// Describes a list editor widget producing a Vector. + /// Items can be dragged around to change the order, or dragged out to be deleted from the + /// Vector. + #[serde(rename = "Vector_Editor")] + VectorEditor { + /// The widget to use for editing the items. + #[serde(borrow)] + item_editor: Box>, + /// The default value for new items inserted when the user clicks the `+` button. + #[serde(borrow)] + item_default: &'a str, + }, + + /// Describes a multi value widget. + #[serde(rename = "Multi_Choice")] + MultipleChoice, + + /// Describe a code parameter. + #[serde(rename = "Code_Input")] + CodeInput, + + /// Describe a boolean parameter. + #[serde(rename = "Boolean_Input")] + BooleanInput, + + /// Describe a numeric parameter. + #[serde(rename = "Numeric_Input")] + NumericInput, + + /// Describes a text widget. + #[serde(rename = "Text_Input")] + TextInput, + + /// Describes a folder chooser. + #[serde(rename = "Folder_Browse")] + FolderBrowse, + + /// Describes a file chooser. + #[serde(rename = "File_Browse")] + FileBrowse, +} + +/// Widget display mode. Determines when the widget should be expanded. +#[derive(serde::Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)] +#[serde(tag = "constructor")] +pub enum Display { + /// The widget should always be in its expanded mode. + #[default] + Always, + /// The widget should only be in its expanded mode when it has non-default value. + #[serde(rename = "When_Modified")] + WhenModified, + /// The widget should only be in its expanded mode whe the whole node is expanded. + #[serde(rename = "Expanded_Only")] + ExpandedOnly, +} + +/// A choice in a single or multiselect widget. +#[derive(Debug, serde::Deserialize)] +pub(super) struct Choice<'a> { + /// The value of the choice. Must be a valid code expression. + pub value: &'a str, + /// Custom label to display in the dropdown. If not provided, IDE will create a label based on + /// value. + #[serde(borrow)] + pub label: Option<&'a str>, +} diff --git a/app/gui/view/examples/interface/src/lib.rs b/app/gui/view/examples/interface/src/lib.rs index ea20b7e01206..ee4b6fe0ed01 100644 --- a/app/gui/view/examples/interface/src/lib.rs +++ b/app/gui/view/examples/interface/src/lib.rs @@ -418,18 +418,18 @@ pub fn expression_mock_trim() -> Expression { tag_values: vec![ TagValue { required_import: None, - expression: "Location.Start".into(), - label: Some("Start".into()), + expression: "Standard.Base.Data.Text.Location.Start".into(), + label: Some("Location.Start".into()), }, TagValue { required_import: None, - expression: "Location.End".into(), - label: Some("End".into()), + expression: "Standard.Base.Data.Text.Location.End".into(), + label: Some("Location.End".into()), }, TagValue { required_import: None, - expression: "Location.Both".into(), - label: Some("Both".into()), + expression: "Standard.Base.Data.Text.Location.Both".into(), + label: Some("Location.Both".into()), }, ], ..default() diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index ee9fadb63e1d..f6af513692a6 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -5,10 +5,7 @@ use crate::prelude::*; use enso_config::ARGS; use enso_frp as frp; use ensogl::application::Application; -use ensogl::data::color; use ensogl::display; -use ensogl::display::object::event; -use ensogl_component::drop_down::Dropdown; use ensogl_component::drop_down::DropdownValue; @@ -20,17 +17,6 @@ pub mod vector_editor; -/// ================= -/// === Constants === -/// ================= - -const ACTIVATION_SHAPE_COLOR: color::Lch = color::Lch::new(0.56708, 0.23249, 0.71372); -const ACTIVATION_SHAPE_Y_OFFSET: f32 = -5.0; -const ACTIVATION_SHAPE_SIZE: Vector2 = Vector2(15.0, 11.0); -/// Distance between the dropdown and the bottom of the port. -const DROPDOWN_Y_OFFSET: f32 = 5.0; - - // =========== // === FRP === // =========== @@ -50,21 +36,136 @@ ensogl::define_endpoints_2! { } } +/// ====================== +/// === Widget modules === +/// ====================== + +/// Common trait for constructing and reconfiguring all widget variants. +pub trait SpanWidget { + type Config: Debug + Clone + PartialEq; + fn new(config: &Self::Config, ctx: ConfigContext<'_>) -> Self; + fn configure(&mut self, config: &Self::Config, ctx: ConfigContext<'_>); +} + +pub struct ConfigContext<'a> { + app: &'a Application, + display_object: &'a display::object::Instance, + frp: &'a SampledFrp, + label: Option, + display: Display, +} + +macro_rules! define_widget_modules( + ($( + $(#[$meta:meta])* + $name:ident $module:ident, + )*) => { + $(pub mod $module;)* + + /// A widget configuration that determines the widget kind. + #[derive(Debug, Clone, PartialEq)] + #[allow(missing_docs)] + pub enum Config { + $($name(<$module::Widget as SpanWidget>::Config),)* + } + + /// A part of widget model that is dependant on the widget kind. + #[derive(Debug)] + #[allow(missing_docs)] + enum DynWidget { + $( + $(#[$meta])* + $name($module::Widget) + ),* + } + + $( + impl From<<$module::Widget as SpanWidget>::Config> for Config { + fn from(config: <$module::Widget as SpanWidget>::Config) -> Self { + Self::$name(config) + } + } + + impl From<$module::Widget> for DynWidget { + fn from(config: $module::Widget) -> Self { + Self::$name(config) + } + } + )* + + impl SpanWidget for DynWidget { + type Config = Config; + fn new(config: &Config, ctx: ConfigContext<'_>) -> Self { + match config { + $( + Config::$name(config) => DynWidget::$name(SpanWidget::new(config, ctx)), + )* + } + } + + fn configure(&mut self, config: &Config, ctx: ConfigContext<'_>) { + match (self, config) { + $((DynWidget::$name(model), Config::$name(config)) => { + SpanWidget::configure(model, config, ctx); + },)* + (this, _) => { + *this = SpanWidget::new(config, ctx); + }, + } + } + } + }; +); + +define_widget_modules! { + /// Default widget that only displays text. + Label label, + /// A widget for selecting a single value from a list of available options. + SingleChoice single_choice, + /// A widget for managing a list of values - adding, removing or reordering them. + VectorEditor vector_editor, +} + +impl Config { + const FALLBACK: Self = Config::Label(label::Config); +} + +/// ================ +/// === Metadata === +/// ================ + /// Widget metadata that comes from an asynchronous visualization. Defines which widget should be /// used and a set of options that it should allow to choose from. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct Metadata { - pub kind: Kind, - pub display: Display, - /// Entries that should be displayed by the widget, as proposed by language server. This list - /// is not exhaustive. The widget implementation might present additional options or allow - /// arbitrary user input. - pub dynamic_entries: Vec, + /// The placeholder text value. By default, the parameter name is used. + pub label: Option, + pub display: Display, + pub config: Config, +} + +impl Metadata { + const FALLBACK: Self = + Metadata { label: None, display: Display::Always, config: Config::FALLBACK }; + + /// Widget metadata for static dropdown, based on the tag values provided by suggestion + /// database. + fn static_dropdown(tag_values: &[span_tree::TagValue]) -> Metadata { + let entries = Rc::new(tag_values.into_iter().map(Entry::from).collect()); + let config = single_choice::Config { entries }.into(); + Self { label: None, display: Display::Always, config } + } + + fn vector_editor() -> Metadata { + let config = vector_editor::Config::default().into(); + Self { label: None, display: Display::Always, config } + } } /// Widget display mode. Determines when the widget should be expanded. #[derive(serde::Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)] +#[serde(tag = "constructor")] pub enum Display { /// The widget should always be in its expanded mode. #[default] @@ -72,7 +173,7 @@ pub enum Display { /// The widget should only be in its expanded mode when it has non-default value. #[serde(rename = "When_Modified")] WhenModified, - /// The widget should only be in its expanded mode whe the whole node is expanded. + /// The widget should only be in its expanded mode when the whole node is expanded. #[serde(rename = "Expanded_Only")] ExpandedOnly, } @@ -216,7 +317,6 @@ impl View { } } - /// ============= /// === Model === /// ============= @@ -225,17 +325,18 @@ impl View { struct Model { app: Application, display_object: display::object::Instance, - kind_model: RefCell>, + inner_widget: RefCell>, } + impl Model { /// Create a new node widget, selecting the appropriate widget type based on the provided /// argument info. fn new(app: &Application) -> Self { let app = app.clone_ref(); let display_object = display::object::Instance::new(); - let kind = default(); - Self { app, display_object, kind_model: kind } + let inner_widget = default(); + Self { app, display_object, inner_widget } } #[profile(Task)] @@ -245,18 +346,24 @@ impl Model { let is_array_type = node_data.tp.as_ref().map_or(false, |tp| tp.contains(VECTOR_TYPE)); let has_tag_values = !node_data.tag_values.is_empty(); let kind_fallback = (is_array_enabled && is_array_type) - .then_some(Kind::VectorEditor) - .or(has_tag_values.then_some(Kind::SingleChoice)); - - let desired_kind = meta.as_ref().map(|m| m.kind).or(kind_fallback); - let current_kind = self.kind_model.borrow().as_ref().map(|m| m.kind()); + .then(|| Metadata::vector_editor()) + .or((meta.is_none() && has_tag_values) + .then(|| Metadata::static_dropdown(&node_data.tag_values))); + let meta = meta.as_ref().or(tag_value_meta.as_ref()).unwrap_or(&Metadata::FALLBACK); + + self.display_object.set_size(node_data.port_size); + + let ctx = ConfigContext { + app: &self.app, + display_object: &self.display_object, + frp, + label: meta.label.clone(), + display: meta.display, + }; - if current_kind != desired_kind { - *self.kind_model.borrow_mut() = desired_kind.map(|desired_kind| { - KindModel::new(&self.app, &self.display_object, desired_kind, frp, meta, node_data) - }); - } else if let Some(model) = self.kind_model.borrow().as_ref() { - model.update(meta, node_data); + match self.inner_widget.borrow_mut().deref_mut() { + Some(inner_widget) => inner_widget.configure(&meta.config, ctx), + model @ None => *model = Some(DynWidget::new(&meta.config, ctx)), } } } @@ -289,9 +396,6 @@ pub enum Kind { /// A widget for selecting a single value from a list of available options. #[serde(rename = "Single_Choice")] SingleChoice, - /// A widget for constructing and modifying vector of various types. - #[serde(rename = "Vector_Editor")] - VectorEditor, } /// A part of widget model that is dependant on the widget kind. @@ -299,8 +403,6 @@ pub enum Kind { pub enum KindModel { /// A widget for selecting a single value from a list of available options. SingleChoice(SingleChoiceModel), - /// A widget for constructing and modifying vector of various types. - VectorEditor(vector_editor::Model), } impl KindModel { @@ -315,8 +417,6 @@ impl KindModel { let this = match kind { Kind::SingleChoice => Self::SingleChoice(SingleChoiceModel::new(app, display_object, frp)), - Kind::VectorEditor => - Self::VectorEditor(vector_editor::Model::new(app, display_object, frp)), }; this.update(meta, node_data); @@ -333,17 +433,12 @@ impl KindModel { inner.set_port_size(node_data.port_size); inner.set_entries(entries); } - KindModel::VectorEditor(inner) => { - warn!("VectorEditor updated with metadata {meta:#?} and node data {node_data:#?}."); - inner.set_port_size.emit(node_data.port_size); - } } } fn kind(&self) -> Kind { match self { Self::SingleChoice(_) => Kind::SingleChoice, - Self::VectorEditor(_) => Kind::VectorEditor, } } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs new file mode 100644 index 000000000000..79f2825e4122 --- /dev/null +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -0,0 +1,24 @@ +//! Definition of static text label widget. + +use crate::prelude::*; + +// ============= +// === Label === +// ============= + +#[derive(Debug, Clone, Copy, PartialEq, Default)] + +/// Label widget configuration options. +pub struct Config; + +#[derive(Clone, Copy, Debug)] +pub struct Widget; + +impl super::SpanWidget for Widget { + type Config = Config; + fn new(config: &Config, ctx: super::ConfigContext<'_>) -> Self { + Self {} + } + + fn configure(&mut self, config: &Config, ctx: super::ConfigContext<'_>) {} +} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs new file mode 100644 index 000000000000..ddfb07770920 --- /dev/null +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -0,0 +1,316 @@ +//! Definition of single choice widget. + +use crate::prelude::*; + +use crate::component::node::input::widget::Entry; + +use enso_frp as frp; +use ensogl::application::Application; +use ensogl::data::color; +use ensogl::display; +use ensogl::display::object::event; +use ensogl_component::drop_down::Dropdown; + + + +/// ================= +/// === Constants === +/// ================= + +const ACTIVATION_SHAPE_COLOR: color::Lch = color::Lch::new(0.56708, 0.23249, 0.71372); +const ACTIVATION_SHAPE_Y_OFFSET: f32 = -5.0; +const ACTIVATION_SHAPE_SIZE: Vector2 = Vector2(15.0, 11.0); +/// Distance between the dropdown and the bottom of the port. +const DROPDOWN_Y_OFFSET: f32 = 5.0; + + + +// ====================== +// === Triangle Shape === +// ====================== + +/// Temporary dropdown activation shape definition. +pub mod triangle { + use super::*; + ensogl::shape! { + above = [ + crate::component::node::background, + crate::component::node::input::port::hover + ]; + (style:Style, color:Vector4) { + let size = Var::canvas_size(); + let radius = 1.0.px(); + let shrink = &radius * 2.0; + let shape = Triangle(size.x() - &shrink, size.y() - &shrink) + .flip_y() + .grow(radius); + shape.fill(color).into() + } + } +} + + + +// ==================== +// === SingleChoice === +// ==================== + +/// SingleChoice widget configuration options. +#[derive(Debug, Clone, PartialEq)] +pub struct Config { + /// Entries that should be displayed by the widget, as proposed by language server. This + /// list is not exhaustive. The widget implementation might present additional + /// options or allow arbitrary user input. + pub entries: Rc>, +} + +/// A widget for selecting a single value from a list of available options. The options can be +/// provided as a static list of strings from argument `tag_values`, or as a dynamic expression. +#[derive(Debug)] +pub struct Widget { + #[allow(dead_code)] + network: frp::Network, + dropdown: Rc>, + /// temporary click handling + activation_shape: triangle::View, +} + +impl super::SpanWidget for Widget { + type Config = Config; + fn new(config: &Config, ctx: super::ConfigContext<'_>) -> Self { + let super::ConfigContext { app, display_object, frp, .. } = ctx; + + let activation_shape = triangle::View::new(); + activation_shape.set_size(ACTIVATION_SHAPE_SIZE); + display_object.add_child(&activation_shape); + + frp::new_network! { network + init <- source_(); + let focus_in = display_object.on_event::(); + let focus_out = display_object.on_event::(); + is_focused <- bool(&focus_out, &focus_in); + is_open <- frp.set_visible && is_focused; + is_open <- is_open.sampler(); + }; + + let set_current_value = frp.set_current_value.clone_ref(); + let dropdown_output = frp.out_value_changed.clone_ref(); + let request_import = frp.out_request_import.clone_ref(); + let dropdown = LazyDropdown::new( + app, + display_object, + set_current_value, + is_open, + dropdown_output, + request_import, + ); + let dropdown = Rc::new(RefCell::new(dropdown)); + + frp::extend! { network + let dot_clicked = activation_shape.events.mouse_down_primary.clone_ref(); + toggle_focus <- dot_clicked.map(f!([display_object](()) !display_object.is_focused())); + set_focused <- any(toggle_focus, frp.set_focused); + eval set_focused([display_object](focus) match focus { + true => display_object.focus(), + false => display_object.blur(), + }); + + set_visible <- all(&frp.set_visible, &init)._0(); + shape_alpha <- set_visible.map(|visible| if *visible { 1.0 } else { 0.0 }); + shape_color <- shape_alpha.map(|a| ACTIVATION_SHAPE_COLOR.with_alpha(*a)); + eval shape_color([activation_shape] (color) { + activation_shape.color.set(color::Rgba::from(color).into()); + }); + + eval focus_in((_) dropdown.borrow_mut().initialize_on_open()); + } + + init.emit(()); + + let mut this = Self { network, dropdown, activation_shape }; + this.configure(config, ctx); + this + } + + fn configure(&mut self, config: &Config, ctx: super::ConfigContext<'_>) { + // TODO: use auto layout to position the activation shape. + let port_size = ctx.display_object.size(); + let port_size_x = port_size.x().as_pixels().expect("Port size is fixed."); + let port_size_y = port_size.y().as_pixels().expect("Port size is fixed."); + self.activation_shape.set_x(port_size_x / 2.0); + self.activation_shape + .set_y(-port_size_y / 2.0 - ACTIVATION_SHAPE_SIZE.y() - ACTIVATION_SHAPE_Y_OFFSET); + let mut dropdown = self.dropdown.borrow_mut(); + dropdown.set_port_size(Vector2(port_size_x, port_size_y)); + dropdown.set_entries(config.entries.clone()); + } +} + +// ==================== +// === LazyDropdown === +// ==================== + +/// A lazy dropdown that is only initialized when it is opened for the first time. This prevents +/// very long initialization time, as dropdown view creation is currently a very slow process. +/// +/// FIXME [PG]: Improve grid-view creation performance, so that this is no longer needed. +/// https://www.pivotaltracker.com/story/show/184223891 +/// +/// Once grid-view creation is reasonably fast, this might be replaced by direct dropdown +/// initialization on widget creation. +#[derive(Debug)] +enum LazyDropdown { + NotInitialized { + app: Application, + display_object: display::object::Instance, + dropdown_y: f32, + entries: Rc>, + set_current_value: frp::Sampler>, + is_open: frp::Sampler, + output_value: frp::Any>, + request_import: frp::Any, + }, + Initialized { + _network: frp::Network, + dropdown: Dropdown, + set_entries: frp::Any>, + }, +} + +impl LazyDropdown { + fn new( + app: &Application, + display_object: &display::object::Instance, + set_current_value: frp::Sampler>, + is_open: frp::Sampler, + output_value: frp::Any>, + request_import: frp::Any, + ) -> Self { + let app = app.clone_ref(); + let display_object = display_object.clone_ref(); + let dropdown_y = default(); + let entries = default(); + LazyDropdown::NotInitialized { + app, + display_object, + dropdown_y, + entries, + set_current_value, + is_open, + output_value, + request_import, + } + } + + fn set_port_size(&mut self, new_port_size: Vector2) { + let y = -new_port_size.y() - DROPDOWN_Y_OFFSET; + match self { + LazyDropdown::Initialized { dropdown, .. } => { + dropdown.set_y(y); + } + LazyDropdown::NotInitialized { dropdown_y, .. } => { + *dropdown_y = y; + } + } + } + + fn set_entries(&mut self, new_entries: Rc>) { + match self { + LazyDropdown::Initialized { set_entries, .. } => { + let new_entries = Rc::try_unwrap(new_entries).unwrap_or_else(|rc| (*rc).clone()); + set_entries.emit(new_entries); + } + LazyDropdown::NotInitialized { entries, .. } => { + *entries = new_entries; + } + } + } + + #[profile(Detail)] + fn initialize_on_open(&mut self) { + match self { + LazyDropdown::Initialized { .. } => {} + LazyDropdown::NotInitialized { + app, + display_object, + dropdown_y, + entries, + is_open, + set_current_value, + output_value, + request_import, + } => { + let dropdown = app.new_view::>(); + display_object.add_child(&dropdown); + app.display.default_scene.layers.above_nodes.add(&dropdown); + dropdown.set_y(*dropdown_y); + dropdown.set_max_open_size(Vector2(300.0, 500.0)); + dropdown.allow_deselect_all(true); + + frp::new_network! { network + init <- source_(); + set_entries <- any(...); + + dropdown.set_all_entries <+ set_entries; + entries_and_value <- all(&set_entries, set_current_value); + entries_and_value <- entries_and_value.debounce(); + + selected_entry <- entries_and_value.map(|(e, v)| entry_for_current_value(e, v)); + dropdown.set_selected_entries <+ selected_entry.map(|e| e.iter().cloned().collect()); + + dropdown_entry <- dropdown.selected_entries.map(|e| e.iter().next().cloned()); + // Emit the output value only after actual user action. This prevents the + // dropdown from emitting its initial value when it is opened, which can + // represent slightly different version of code than actually written. + submitted_entry <- dropdown_entry.sample(&dropdown.user_select_action); + dropdown_out_value <- submitted_entry.map(|e| e.as_ref().map(Entry::value)); + dropdown_out_import <- submitted_entry.map(|e| e.as_ref().and_then(Entry::required_import)); + request_import <+ dropdown_out_import.unwrap(); + output_value <+ dropdown_out_value.sample(&dropdown.user_select_action); + + is_open <- all(is_open, &init)._0(); + dropdown.set_open <+ is_open.on_change(); + + // Close the dropdown after a short delay after selection. Because the dropdown + // value application triggers operations that can introduce a few dropped frames, + // we want to delay the dropdown closing animation after that is handled. + // Otherwise the animation finishes within single frame, which looks bad. + let close_after_selection_timer = frp::io::timer::Timeout::new(&network); + close_after_selection_timer.restart <+ dropdown.user_select_action.constant(1); + eval close_after_selection_timer.on_expired((()) display_object.blur()); + } + + let entries = std::mem::take(Rc::make_mut(entries)); + set_entries.emit(entries); + init.emit(()); + *self = LazyDropdown::Initialized { _network: network, dropdown, set_entries }; + } + } + } +} + +fn entry_for_current_value( + all_entries: &[Entry], + current_value: &Option, +) -> Option { + let current_value = current_value.clone()?; + let found_entry = all_entries.iter().find(|entry| entry.value.as_ref() == current_value); + let with_partial_match = found_entry.or_else(|| { + // Handle parentheses in current value. Entries with parenthesized expressions will match if + // they start with the same expression as the current value. That way it is still matched + // once extra arguments are added to the nested function call. + if current_value.starts_with('(') { + let current_value = current_value.trim_start_matches('(').trim_end_matches(')'); + all_entries.iter().find(|entry| { + let trimmed_value = entry.value.trim_start_matches('(').trim_end_matches(')'); + current_value.starts_with(trimmed_value) + }) + } else { + None + } + }); + + let with_fallback = + with_partial_match.cloned().unwrap_or_else(|| Entry::from_value(current_value.clone())); + Some(with_fallback) +} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs b/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs index 2a66f0dd2390..7654178413fb 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs @@ -44,7 +44,7 @@ pub struct Model { } impl Model { - /// A gap between the `activation_shape` and `elements` view. + /// A gap between the `activation_shape` and `elements` view. const GAP: f32 = 3.0; /// Create Model for Vector Editor widget. @@ -199,3 +199,23 @@ impl Model { opt_iterator.into_iter().flatten() } } + + + +/// ============== +/// === Widget === +/// ============== + +struct Widget {} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +struct Config {} + +impl super::SpanWidget for Widget { + type Config = Config; + fn new(config: &Config, ctx: super::ConfigContext<'_>) -> Self { + Self {} + } + + fn configure(&mut self, config: &Config, ctx: super::ConfigContext<'_>) {} +} From d7211616bbb8c51360692afbb120444f89641ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Fri, 24 Mar 2023 15:05:59 +0100 Subject: [PATCH 02/45] wip widget tree --- Cargo.lock | 1 + app/gui/language/span-tree/src/builder.rs | 25 +- app/gui/language/span-tree/src/generate.rs | 29 +- app/gui/language/span-tree/src/lib.rs | 16 +- app/gui/language/span-tree/src/node.rs | 31 +- .../src/controller/graph/widget/metadata.rs | 2 +- .../src/component/node/input/area.rs | 1294 ++++++++--------- .../src/component/node/input/port.rs | 8 +- .../src/component/node/input/widget.rs | 431 ++++-- .../component/node/input/widget/hierarchy.rs | 75 + .../src/component/node/input/widget/label.rs | 51 +- .../node/input/widget/single_choice.rs | 120 +- .../core/src/display/object/instance.rs | 14 +- .../ensogl/examples/auto-layout/Cargo.toml | 1 + lib/rust/frp/src/nodes.rs | 17 + 15 files changed, 1168 insertions(+), 947 deletions(-) create mode 100644 app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs diff --git a/Cargo.lock b/Cargo.lock index 8a2cc5970d00..f93de70f64d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2765,6 +2765,7 @@ version = "0.1.0" dependencies = [ "ensogl-core", "ensogl-hardcoded-theme", + "ensogl-text", "wasm-bindgen", ] diff --git a/app/gui/language/span-tree/src/builder.rs b/app/gui/language/span-tree/src/builder.rs index 44c57d9dd89c..f7b3a27f43a3 100644 --- a/app/gui/language/span-tree/src/builder.rs +++ b/app/gui/language/span-tree/src/builder.rs @@ -19,6 +19,9 @@ pub trait Builder: Sized { /// Reference to currently built node. fn node_being_built(&mut self) -> &mut Node; + /// The total length of the tree built so far. + fn current_end_offset(&self) -> usize; + /// Add new AST-type child to node. Returns the child's builder which may be used to further /// extend this branch of the tree. fn add_child( @@ -30,7 +33,13 @@ pub trait Builder: Sized { ) -> ChildBuilder { let kind = kind.into(); let node = Node::::new().with_kind(kind).with_size(len.into()); - let child = node::Child { node, offset: offset.into(), ast_crumbs: crumbs.into_crumbs() }; + let sibling_offset = offset.saturating_sub(self.current_end_offset()); + let child = node::Child { + node, + offset: offset.into(), + sibling_offset: sibling_offset.into(), + ast_crumbs: crumbs.into_crumbs(), + }; ChildBuilder { built: child, parent: self } } @@ -47,10 +56,12 @@ pub trait Builder: Sized { /// Add an Empty-type child to node. fn add_empty_child(mut self, offset: usize, kind: impl Into) -> Self { + let sibling_offset = offset.saturating_sub(self.current_end_offset()); let child = node::Child { - node: Node::::new().with_kind(kind), - offset: offset.into(), - ast_crumbs: vec![], + node: Node::::new().with_kind(kind), + offset: offset.into(), + sibling_offset: sibling_offset.into(), + ast_crumbs: vec![], }; self.node_being_built().children.push(child); self @@ -95,6 +106,9 @@ impl Builder for TreeBuilder { fn node_being_built(&mut self) -> &mut Node { &mut self.built } + fn current_end_offset(&self) -> usize { + self.built.size.as_usize() + } } @@ -119,4 +133,7 @@ impl Builder for ChildBuilder { fn node_being_built(&mut self) -> &mut Node { &mut self.built.node } + fn current_end_offset(&self) -> usize { + self.built.offset.as_usize() + self.built.node.size.as_usize() + } } diff --git a/app/gui/language/span-tree/src/generate.rs b/app/gui/language/span-tree/src/generate.rs index ae8b0c210464..37315970918a 100644 --- a/app/gui/language/span-tree/src/generate.rs +++ b/app/gui/language/span-tree/src/generate.rs @@ -93,6 +93,7 @@ impl SpanTreeGenerator for String { #[derivative(Default(bound = ""))] struct ChildGenerator { current_offset: ByteDiff, + sibling_offset: ByteDiff, children: Vec>, } @@ -100,7 +101,9 @@ impl ChildGenerator { /// Add spacing to current generator state. It will be taken into account for the next generated /// children's offsets fn spacing(&mut self, size: usize) { - self.current_offset += (size as i32).byte_diff(); + let offset = (size as i32).byte_diff(); + self.current_offset += offset; + self.sibling_offset += offset; } fn generate_ast_node( @@ -116,18 +119,22 @@ impl ChildGenerator { fn add_node(&mut self, ast_crumbs: ast::Crumbs, node: Node) -> &mut node::Child { let offset = self.current_offset; - let child = node::Child { node, offset, ast_crumbs }; + let sibling_offset = self.sibling_offset; + let child = node::Child { node, offset, sibling_offset, ast_crumbs }; self.current_offset += child.node.size; + self.sibling_offset = 0.byte_diff(); self.children.push(child); self.children.last_mut().unwrap() } fn generate_empty_node(&mut self, insert_type: InsertionPointType) -> &mut node::Child { let child = node::Child { - node: Node::::new().with_kind(insert_type), - offset: self.current_offset, - ast_crumbs: vec![], + node: Node::::new().with_kind(insert_type), + offset: self.current_offset, + sibling_offset: self.sibling_offset, + ast_crumbs: vec![], }; + self.sibling_offset = 0.byte_diff(); self.children.push(child); self.children.last_mut().unwrap() } @@ -812,24 +819,30 @@ fn tree_generate_node( size = ByteDiff::from(leaf_info.len()); } else { let mut offset = ByteDiff::from(0); + let mut sibling_offset = ByteDiff::from(0); for (index, raw_span_info) in tree.span_info.iter().enumerate() { match raw_span_info { - SpanSeed::Space(ast::SpanSeedSpace { space }) => offset += ByteDiff::from(space), + SpanSeed::Space(ast::SpanSeedSpace { space }) => { + offset += ByteDiff::from(space); + sibling_offset += ByteDiff::from(space); + } SpanSeed::Token(ast::SpanSeedToken { token }) => { let kind = node::Kind::Token; let size = ByteDiff::from(token.len()); let ast_crumbs = vec![TreeCrumb { index }.into()]; let node = Node { kind, size, ..default() }; - children.push(node::Child { node, offset, ast_crumbs }); + children.push(node::Child { node, offset, sibling_offset, ast_crumbs }); offset += size; + sibling_offset = 0.byte_diff(); } SpanSeed::Child(ast::SpanSeedChild { node }) => { let kind = node::Kind::argument(); let node = node.generate_node(kind, context)?; let child_size = node.size; let ast_crumbs = vec![TreeCrumb { index }.into()]; - children.push(node::Child { node, offset, ast_crumbs }); + children.push(node::Child { node, offset, sibling_offset, ast_crumbs }); offset += child_size; + sibling_offset = 0.byte_diff(); } } } diff --git a/app/gui/language/span-tree/src/lib.rs b/app/gui/language/span-tree/src/lib.rs index ae14f7815780..11704c80bdf6 100644 --- a/app/gui/language/span-tree/src/lib.rs +++ b/app/gui/language/span-tree/src/lib.rs @@ -261,7 +261,7 @@ impl SpanTree { } let mut buffer = String::new(); - let span_padding = " ".repeat(code.len() + 1); + let span_padding = " ".repeat(code.len() + 2); struct PrintState { indent: String, @@ -271,15 +271,11 @@ impl SpanTree { self.root_ref().dfs_with_layer_data(state, |node, state| { let span = node.span(); let node_code = &code[span]; - buffer.push_str(&span_padding[0..node.span_offset.into()]); - let mut written = node.span_offset.into(); - if node_code.is_empty() { - buffer.push('▲'); - written += 1; - } else { - buffer.push_str(node_code); - written += node_code.len(); - } + buffer.push_str(&span_padding[0..node.span_offset.value]); + buffer.push('▷'); + buffer.push_str(node_code); + buffer.push('◁'); + let written = node.span_offset.value + node_code.len() + 2; buffer.push_str(&span_padding[written..]); let indent = if let Some(index) = node.crumbs.last() { diff --git a/app/gui/language/span-tree/src/node.rs b/app/gui/language/span-tree/src/node.rs index 79e6f179c611..524bd809cfdf 100644 --- a/app/gui/language/span-tree/src/node.rs +++ b/app/gui/language/span-tree/src/node.rs @@ -181,11 +181,13 @@ impl Node { #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Child { /// A child node. - pub node: Node, + pub node: Node, /// An offset counted from the parent node starting index to the start of this node's span. - pub offset: ByteDiff, + pub offset: ByteDiff, + /// The offset counted from the end of previous sibling node. + pub sibling_offset: ByteDiff, /// AST crumbs which lead from parent to child associated AST node. - pub ast_crumbs: ast::Crumbs, + pub ast_crumbs: ast::Crumbs, } impl Child { @@ -194,7 +196,8 @@ impl Child { let node = self.node.map(f); let offset = self.offset; let ast_crumbs = self.ast_crumbs; - Child { node, offset, ast_crumbs } + let sibling_offset = self.sibling_offset; + Child { node, offset, sibling_offset, ast_crumbs } } } @@ -319,15 +322,17 @@ impl InvalidCrumb { #[derivative(Clone(bound = ""))] pub struct Ref<'a, T = ()> { /// The span tree that the node is a part of. - pub span_tree: &'a SpanTree, + pub span_tree: &'a SpanTree, /// The node's ref. - pub node: &'a Node, + pub node: &'a Node, /// Span begin's offset counted from the root expression. - pub span_offset: Byte, + pub span_offset: Byte, + /// The offset counted from the end of previous sibling node. + pub sibling_offset: ByteDiff, /// Crumbs specifying this node position related to root. - pub crumbs: Crumbs, + pub crumbs: Crumbs, /// Ast crumbs locating associated AST node, related to the root's AST node. - pub ast_crumbs: ast::Crumbs, + pub ast_crumbs: ast::Crumbs, } /// A result of `get_subnode_by_ast_crumbs` @@ -343,10 +348,11 @@ impl<'a, T> Ref<'a, T> { /// Constructor. pub fn root(span_tree: &'a SpanTree) -> Self { let span_offset = default(); + let sibling_offset = default(); let crumbs = default(); let ast_crumbs = default(); let node = &span_tree.root; - Self { span_tree, node, span_offset, crumbs, ast_crumbs } + Self { span_tree, node, span_offset, sibling_offset, crumbs, ast_crumbs } } /// Get span of current node. @@ -358,16 +364,17 @@ impl<'a, T> Ref<'a, T> { /// Get the reference to child with given index. Fails if index if out of bounds. pub fn child(self, index: usize) -> FallibleResult { - let Ref { span_tree, node, mut span_offset, crumbs, mut ast_crumbs } = self; + let Ref { span_tree, node, mut span_offset, crumbs, mut ast_crumbs, .. } = self; match node.children.get(index) { None => Err(InvalidCrumb::new(node.children.len(), index, &crumbs).into()), Some(child) => { let node = &child.node; span_offset += child.offset; + let sibling_offset = child.sibling_offset; let crumbs = crumbs.into_sub(index); ast_crumbs.extend_from_slice(&child.ast_crumbs); - Ok(Self { span_tree, node, span_offset, crumbs, ast_crumbs }) + Ok(Self { span_tree, node, span_offset, sibling_offset, crumbs, ast_crumbs }) } } } diff --git a/app/gui/src/controller/graph/widget/metadata.rs b/app/gui/src/controller/graph/widget/metadata.rs index 43c6cd034557..3bdc28da650a 100644 --- a/app/gui/src/controller/graph/widget/metadata.rs +++ b/app/gui/src/controller/graph/widget/metadata.rs @@ -51,7 +51,7 @@ fn map_config(inner: response::WidgetSpecific) -> widget::Config { match inner { response::WidgetSpecific::SingleChoice { values } => widget::single_choice::Config { entries: Rc::new(map_entries(&values)) }.into(), - _ => widget::label::Config.into(), + _ => widget::label::Config::default().into(), } } diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 57116779425f..4f837b161584 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -1,8 +1,7 @@ //! Definition of the node input port component. +use crate::component::node::input::widget::MetadataPointer; use crate::prelude::*; -use enso_text::index::*; -use enso_text::unit::*; use ensogl::display::shape::*; use ensogl::display::traits::*; @@ -36,6 +35,9 @@ use ensogl_hardcoded_theme as theme; /// An offset from the port area position to the text position. pub const TEXT_OFFSET: f32 = 10.0; +/// Total height of the node input area. +pub const NODE_HEIGHT: f32 = 18.0; + /// Width of a single glyph // TODO: avoid using hardcoded value. See https://www.pivotaltracker.com/story/show/183567623. pub const GLYPH_WIDTH: f32 = 7.224_609_4; @@ -47,11 +49,6 @@ pub const DEBUG: bool = false; /// set to `true`. pub const DEBUG_PORT_OFFSET: f32 = 5.0; -/// Skip creating ports on all operations. For example, in expression `foo bar`, `foo` is considered -/// an operation. -const SKIP_OPERATIONS: bool = true; -const PORT_PADDING_X: f32 = 4.0; - /// Text size used for input area text. pub const TEXT_SIZE: f32 = 12.0; @@ -65,7 +62,7 @@ pub use span_tree::Crumb; pub use span_tree::Crumbs; /// Specialized `SpanTree` for the input ports model. -pub type SpanTree = span_tree::SpanTree; +pub type SpanTree = span_tree::SpanTree<()>; /// Mutable reference to port inside of a `SpanTree`. pub type PortRefMut<'a> = span_tree::node::RefMut<'a, port::Model>; @@ -76,31 +73,28 @@ pub type PortRefMut<'a> = span_tree::node::RefMut<'a, port::Model>; // === Expression === // ================== -/// Specialized version of `node::Expression`, containing the port information. +/// Specialized version of `node::Expression`. #[derive(Clone, Default)] #[allow(missing_docs)] -pub struct Expression { - /// Visual code representation. It can contain names of missing arguments, and thus can differ - /// from `code`. - pub viz_code: ImString, +pub struct InputExpression { pub code: ImString, pub span_tree: SpanTree, } -impl Deref for Expression { +impl Deref for InputExpression { type Target = SpanTree; fn deref(&self) -> &Self::Target { &self.span_tree } } -impl DerefMut for Expression { +impl DerefMut for InputExpression { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.span_tree } } -impl Debug for Expression { +impl Debug for InputExpression { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Expression({})", self.code) } @@ -114,7 +108,7 @@ impl Debug for Expression { /// a default `Debug` implementation of `Expression`, so it is hidden behind a separate adapter /// and can be chosen by calling `expression.tree_pretty_printer()`. pub struct ExpressionTreePrettyPrint<'a> { - expression: &'a Expression, + expression: &'a InputExpression, } impl<'a> Debug for ExpressionTreePrettyPrint<'a> { @@ -124,7 +118,7 @@ impl<'a> Debug for ExpressionTreePrettyPrint<'a> { } } -impl Expression { +impl InputExpression { /// Wrap the expression into a pretty-printing adapter that implements `Debug` and prints /// detailed span-tree information. See [`SpanTree::debug_print`] method for more details. /// @@ -138,60 +132,10 @@ impl Expression { // === Conversions === -/// Helper struct used for `Expression` conversions. -#[derive(Debug, Default)] -struct ExprConversion { - prev_tok_local_index: Byte, - /// Index of the last traverse parent node in the `SpanTree`. - last_parent_tok_index: Byte, -} - -impl ExprConversion { - fn new(last_parent_tok_index: Byte) -> Self { - let prev_tok_local_index = default(); - Self { prev_tok_local_index, last_parent_tok_index } - } -} - -impl From for Expression { - /// Traverses the `SpanTree` and constructs `viz_code` based on `code` and the `SpanTree` - /// structure. It also computes `port::Model` values in the `viz_code` representation. +impl From for InputExpression { #[profile(Debug)] fn from(t: node::Expression) -> Self { - // The length difference between `code` and `viz_code` so far. - let mut shift = 0.byte(); - let mut span_tree: SpanTree = t.input_span_tree.map(|()| port::Model::default()); - let mut viz_code = String::new(); - let code = t.code; - span_tree.root_ref_mut().dfs_with_layer_data(ExprConversion::default(), |node, info| { - let is_expected_arg = node.is_expected_argument(); - let span = node.span(); - // TODO: remove unwrap. (https://www.pivotaltracker.com/story/show/183567590) - let mut size = Byte::try_from(span.size()).unwrap(); - let mut index = span.start; - let offset_from_prev_tok = node.offset - info.prev_tok_local_index.to_diff(); - info.prev_tok_local_index = size + node.offset; - viz_code += &" ".repeat(offset_from_prev_tok.as_usize()); - if node.children.is_empty() { - viz_code += &code.as_str()[enso_text::Range::new(index, index + size)]; - } - index += shift; - if is_expected_arg { - if let Some(name) = node.name() { - size = name.len().into(); - index += 1.byte(); - shift += 1.byte() + size; - viz_code += " "; - viz_code += name; - } - } - let port = node.payload_mut(); - port.local_index = index - info.last_parent_tok_index; - port.index = index.into(); - port.length = size.into(); - ExprConversion::new(index) - }); - Self { viz_code: viz_code.into(), code, span_tree } + Self { code: t.code, span_tree: t.input_span_tree } } } @@ -206,19 +150,19 @@ impl From for Expression { pub struct Model { app: Application, display_object: display::object::Instance, - ports: display::object::Instance, - header: display::object::Instance, + // ports: display::object::Instance, + // header: display::object::Instance, /// Text label used for displaying the ports. Contains both expression text and inserted /// argument placeholders. The style is adjusted based on port types. - ports_label: text::Text, + // ports_label: text::Text, /// Text label used during edit mode. Contains only the expression text without any /// modifications. Handles user input in edit mode. edit_mode_label: text::Text, - expression: RefCell, + expression: RefCell, id_crumbs_map: RefCell>, - widgets_map: RefCell>, styles: StyleWatch, styles_frp: StyleWatchFrp, + root_widget: widget::Root, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -231,31 +175,24 @@ impl Model { /// Constructor. #[profile(Debug)] pub fn new(app: &Application) -> Self { - let display_object = display::object::Instance::new(); - let ports = display::object::Instance::new(); - let header = display::object::Instance::new(); let app = app.clone_ref(); + let display_object = display::object::Instance::new(); + display_object.use_auto_layout(); let edit_mode_label = app.new_view::(); - let ports_label = app.new_view::(); let id_crumbs_map = default(); let expression = default(); let styles = StyleWatch::new(&app.display.default_scene.style_sheet); let styles_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet); - let widgets_map = default(); - display_object.add_child(&ports); - ports.add_child(&header); + let root_widget = widget::Root::new(&app); Self { app, display_object, - ports, - header, edit_mode_label, - ports_label, expression, id_crumbs_map, - widgets_map, styles, styles_frp, + root_widget, } .init() } @@ -265,46 +202,44 @@ impl Model { pub fn set_edit_mode(&self, edit_mode_active: bool) { if edit_mode_active { // When transitioning to edit mode, we need to find the code location that corresponds - // to the code at mouse position. First we search for the port at that position, then - // find the right character index within that port. + // to the widget at mouse position. First we search for the widget at that position, + // then find the right character index within that port. let expression = self.expression.borrow(); - let clicked_label_location = self.ports_label.location_at_mouse_position(); - let clicked_char_index = - expression.viz_code.char_indices().nth(clicked_label_location.offset.into()); - let location_to_set = clicked_char_index.and_then(|char_index| { - let loc_offset = char_index.0.byte().to_diff(); - let clicked_port = expression.span_tree.root_ref().leaf_iter().find(|node| { - let range = node.payload.range(); - range.contains(&loc_offset) - })?; - - let byte_offset_within_port = loc_offset - clicked_port.payload.index; - let byte_offset_within_port = byte_offset_within_port.min(clicked_port.size); - let final_code_byte_offset = clicked_port.span_offset + byte_offset_within_port; - - let final_code_column: Column = - expression.code[..final_code_byte_offset.into()].chars().count().into(); - let final_code_location = clicked_label_location.with_offset(final_code_column); - Some(final_code_location) - }); + // let clicked_label_location = self.ports_label.location_at_mouse_position(); + // let clicked_char_index = + // expression.viz_code.char_indices().nth(clicked_label_location.offset.into()); + // let location_to_set = clicked_char_index.and_then(|char_index| { + // let loc_offset = char_index.0.byte().to_diff(); + // let clicked_port = expression.span_tree.root_ref().leaf_iter().find(|node| { + // let range = node.payload.range(); + // range.contains(&loc_offset) + // })?; + + // let byte_offset_within_port = loc_offset - clicked_port.payload.index; + // let byte_offset_within_port = byte_offset_within_port.min(clicked_port.size); + // let final_code_byte_offset = clicked_port.span_offset + byte_offset_within_port; + + // let final_code_column: Column = + // expression.code[..final_code_byte_offset.into()].chars().count().into(); + // let final_code_location = clicked_label_location.with_offset(final_code_column); + // Some(final_code_location) + // }); self.edit_mode_label.set_content(expression.code.clone()); - self.display_object.remove_child(&self.ports); - self.display_object.remove_child(&self.ports_label); + self.display_object.remove_child(&self.root_widget); self.display_object.add_child(&self.edit_mode_label); - if let Some(location) = location_to_set { - self.edit_mode_label.set_cursor(location); - } else { - // If we were unable to find a port under current mouse position, set the edit label - // cursor at the mouse position immediately after setting its content to the raw - // expression code. - self.edit_mode_label.set_cursor_at_mouse_position(); - } + // if let Some(location) = location_to_set { + // self.edit_mode_label.set_cursor(location); + // } else { + // If we were unable to find a port under current mouse position, set the edit label + // cursor at the mouse position immediately after setting its content to the raw + // expression code. + self.edit_mode_label.set_cursor_at_mouse_position(); + // } } else { self.display_object.remove_child(&self.edit_mode_label); - self.display_object.add_child(&self.ports); - self.display_object.add_child(&self.ports_label); + self.display_object.add_child(&self.root_widget); // When we exit the edit mode, clear the label. That way we don't have any extra glyphs // to process during rendering in non-edit mode. self.edit_mode_label.set_content(""); @@ -321,8 +256,8 @@ impl Model { self.set_label_layer(&scene.layers.label); let text_color = self.styles.get_color(theme::graph_editor::node::text); - self.ports_label.set_property_default(text_color); - self.ports_label.set_property_default(text::Size(TEXT_SIZE)); + // self.ports_label.set_property_default(text_color); + // self.ports_label.set_property_default(text::Size(TEXT_SIZE)); self.edit_mode_label.set_single_line_mode(true); self.edit_mode_label.disable_command("cursor_move_up"); @@ -332,10 +267,9 @@ impl Model { self.edit_mode_label.set_property_default(text::Size(TEXT_SIZE)); self.edit_mode_label.remove_all_cursors(); - let ports_origin = Vector2(TEXT_OFFSET, 0.0); + let widgets_origin = Vector2(0.0, -NODE_HEIGHT / 2.0); let label_origin = Vector2(TEXT_OFFSET, TEXT_SIZE / 2.0); - self.ports.set_xy(ports_origin); - self.ports_label.set_xy(label_origin); + self.root_widget.set_xy(widgets_origin); self.edit_mode_label.set_xy(label_origin); self.set_edit_mode(false); @@ -344,539 +278,512 @@ impl Model { /// Return a list of Node's input ports. pub fn ports(&self) -> Vec { - let expression = self.expression.borrow(); - let mut ports = Vec::new(); - expression.span_tree.root_ref().dfs(|n| ports.push(n.payload.clone())); - ports + // let expression = self.expression.borrow(); + // let mut ports = Vec::new(); + // expression.span_tree.root_ref().dfs(|n| ports.push(n.payload.clone())); + // ports + todo!() } fn set_label_layer(&self, layer: &display::scene::Layer) { self.edit_mode_label.add_to_scene_layer(layer); - self.ports_label.add_to_scene_layer(layer); + // self.ports_label.add_to_scene_layer(layer); } /// Run the provided function on the target port if exists. - fn with_port_mut(&self, crumbs: &Crumbs, f: impl FnOnce(PortRefMut)) { - let mut expression = self.expression.borrow_mut(); - if let Ok(node) = expression.span_tree.root_ref_mut().get_descendant(crumbs) { - f(node) - } - } + // fn with_port_mut(&self, crumbs: &Crumbs, f: impl FnOnce(PortRefMut)) { + // let mut expression = self.expression.borrow_mut(); + // if let Ok(node) = expression.span_tree.root_ref_mut().get_descendant(crumbs) { + // f(node) + // } + // } /// Traverse all `SpanTree` leaves of the given port and emit hover style to set their colors. fn set_port_hover(&self, target: &Switch) { - self.with_port_mut(&target.value, |t| t.set_hover(target.is_on())) - } - - /// Update expression type for the particular `ast::Id`. - #[profile(Debug)] - fn set_expression_usage_type(&self, crumbs: &Crumbs, tp: &Option) { - if let Ok(port) = self.expression.borrow().span_tree.root_ref().get_descendant(crumbs) { - port.set_usage_type(tp) - } + // TODO + // self.with_port_mut(&target.value, |t| t.set_hover(target.is_on())) } /// Apply widget updates to widgets in this input area. fn apply_widget_updates(&self, updates: &WidgetUpdates) { - let expression = self.expression.borrow(); - let widgets_map = self.widgets_map.borrow(); let WidgetUpdates { call_id, updates } = updates; for update in updates.iter() { - let argument_name = update.argument_name.to_string(); - let widget_id = WidgetBind { call_id: *call_id, argument_name }; - let crumbs = widgets_map.get(&widget_id); - - let root = expression.span_tree.root_ref(); - let port = crumbs.and_then(|crumbs| root.get_descendant(crumbs).ok()); - let widget = port.and_then(|port| port.payload.widget.clone_ref()); - - // When a widget is found, update it. Failing to find a widget is not an error, as it - // might be a widget that was removed from the expression while the request was pending. - // If it comes back, the widget data will be requested again. - if let Some(widget) = widget { - widget.set_metadata(update.meta.clone()); - } + let argument_name = update.argument_name.clone().into(); + let meta_pointer = MetadataPointer { call_id: *call_id, argument_name }; + self.root_widget.set_metadata(meta_pointer, update.meta.clone()); } + let expr = self.expression.borrow(); + self.root_widget.rebuild_tree_on_metadata_change(&expr.span_tree, &expr.code); } - #[profile(Debug)] - fn set_label_on_new_expression(&self, expression: &Expression) { - self.ports_label.set_content(expression.viz_code.clone()); - } - - #[profile(Debug)] - fn build_port_shapes_on_new_expression( - &self, - expression: &mut Expression, - area_frp: &FrpEndpoints, - call_info: &CallInfoMap, - ) { - let mut is_header = true; - - let mut id_crumbs_map = HashMap::new(); - let mut widgets_map = HashMap::new(); - let builder = PortLayerBuilder::empty(&self.ports); - let code = &expression.viz_code; - - expression.span_tree.root_ref_mut().dfs_with_layer_data(builder, |mut node, builder| { - let skip_opr = if SKIP_OPERATIONS { - node.is_operation() && !is_header - } else { - let crumb = ast::Crumb::Infix(ast::crumbs::InfixCrumb::Operator); - node.ast_crumbs.last().map(|t| t == &crumb) == Some(true) - }; - - let not_a_port = node.is_positional_insertion_point() - || node.is_chained() - || (node.is_root() && !node.children.is_empty()) - || skip_opr - || node.is_token() - || node.is_named_argument() - || builder.parent_parensed; - - if let Some(id) = node.ast_id { - if DEBUG { - debug!("New id mapping: {id} -> {:?}", node.crumbs); - } - id_crumbs_map.insert(id, node.crumbs.clone_ref()); - } - - if DEBUG { - let indent = " ".repeat(4 * builder.depth); - let skipped = if not_a_port { "(skip)" } else { "" }; - debug!( - "{indent}[{},{}] {skipped} {:?} (tp: {:?}) (id: {:?})", - node.payload.index, - node.payload.length, - node.kind.variant_name(), - node.tp(), - node.ast_id - ); - } - - let range_before_start = node.payload.index - node.payload.local_index; - let range_before_end = node.payload.index; - let range_before = enso_text::Range::new(range_before_start, range_before_end); - let local_char_offset = code[range_before].chars().count(); - - let new_parent = if not_a_port { - builder.parent.clone_ref() - } else { - let crumbs = node.crumbs.clone_ref(); - let port = &mut node; - - let index = local_char_offset + builder.shift; - let size = code[port.payload.range()].chars().count(); - let unit = GLYPH_WIDTH; - let width = unit * size as f32; - let width_padded = width + 2.0 * PORT_PADDING_X; - let height = 18.0; - let size = Vector2(width, height); - let padded_size = Vector2(width_padded, height); - let position_x = unit * index as f32; - - let port_shape = port.payload.init_shape(size, node::HEIGHT); - - port_shape.set_x(position_x); - if DEBUG { - port_shape.set_y(DEBUG_PORT_OFFSET); - } - - if is_header { - is_header = false; - self.header.add_child(&port_shape); - } else { - builder.parent.add_child(&port_shape); - } - - // TODO: StyleWatch is unsuitable here, as it was designed as an internal tool for - // shape system. (https://www.pivotaltracker.com/story/show/183567648) - let style_sheet = &self.app.display.default_scene.style_sheet; - let styles = StyleWatch::new(style_sheet); - let styles_frp = &self.styles_frp; - let any_type_sel_color = styles_frp.get_color(theme::code::types::any::selection); - let port_network = &port.network; - - frp::extend! { port_network - - // === Aliases === - - let mouse_over_raw = port_shape.hover.events_deprecated.mouse_over.clone_ref(); - let mouse_out = port_shape.hover.events_deprecated.mouse_out.clone_ref(); - let mouse_down_raw = port_shape.hover.events_deprecated.mouse_down_primary.clone_ref(); - - - // === Body Hover === - - // This is meant to be on top of FRP network. Read more about `Node` docs to - // learn more about the architecture and the importance of the hover - // functionality. - - // Please note, that this is computed first in order to compute `ports_visible` - // when needed, and thus it has to be run before the following lines. - area_frp.source.body_hover <+ bool(&mouse_out,&mouse_over_raw); - - // TODO[WD] for FRP3: Consider the following code. Here, we have to first - // handle `bg_down` and then `mouse_down`. Otherwise, `mouse_down` may - // trigger some events and can change `ports_visible` status, and thus make - // the `bg_down` emitted unnecessarily. For example, after plugging in - // connections to selected port, the `ports_visible` will be set to `false`, - // and `bg_down` will be emitted, causing the node to be selected. This can - // be solved by solving in the FRP engine all children first, and then their - // children (then both `bg_down` and `mouse_down` will be resolved before - // the `ports_visible` changes). - bg_down <- mouse_down_raw.gate_not(&area_frp.ports_visible); - mouse_down <- mouse_down_raw.gate(&area_frp.ports_visible); - mouse_over <- mouse_over_raw.gate(&area_frp.ports_visible); - area_frp.source.on_background_press <+ bg_down; - - - // === Press === - - area_frp.source.on_port_press <+ mouse_down.map(f_!([crumbs] crumbs.clone_ref())); - - // === Hover === - - hovered <- bool(&mouse_out,&mouse_over); - hover <- hovered.map (f!([crumbs](t) Switch::new(crumbs.clone_ref(),*t))); - area_frp.source.on_port_hover <+ hover; - - - // === Pointer Style === - - let port_shape_hover = port_shape.hover.clone_ref(); - pointer_style_out <- mouse_out.map(|_| default()); - - init_color <- source::<()>(); - any_type_sel_color <- all_with(&any_type_sel_color,&init_color, - |c,_| color::Lcha::from(c)); - tp <- all_with(&port.tp,&area_frp.set_ports_active, - |tp,(_,edge_tp)| tp.clone().or_else(||edge_tp.clone())); - tp_color <- tp.map( - f!([styles](tp) tp.map_ref(|tp| type_coloring::compute(tp,&styles)))); - tp_color <- all_with(&tp_color,&any_type_sel_color, - |tp_color,any_type_sel_color| tp_color.unwrap_or(*any_type_sel_color)); - in_profiling_mode <- area_frp.view_mode.map(|m| matches!(m,view::Mode::Profiling)); - pointer_color_over <- in_profiling_mode.switch(&tp_color,&any_type_sel_color); - pointer_style_over <- pointer_color_over.map(move |color| - cursor::Style::new_highlight(&port_shape_hover,padded_size,Some(color)) - ); - pointer_style_over <- pointer_style_over.sample(&mouse_over); - - pointer_style_hover <- any(pointer_style_over,pointer_style_out); - pointer_styles <- all[ - pointer_style_hover, - self.ports_label.pointer_style, - self.edit_mode_label.pointer_style - ]; - pointer_style <- pointer_styles.fold(); - area_frp.source.pointer_style <+ pointer_style; - } - - let port_range = port.span(); - let port_code = &expression.code[port_range]; - if let Some((widget_bind, widget)) = self.init_port_widget(port, size, call_info) { - widgets_map.insert(widget_bind, crumbs.clone_ref()); - widget.set_x(position_x); - builder.parent.add_child(&widget); - if port.is_argument() { - debug!("Setting current value while range is {port_range:?}, code is \"{port_code}\" \ - and full expression is \"{}\".", expression.code); - widget.set_current_value(Some(port_code.into())); - } else { - widget.set_current_value(None); - } - widget.set_visible(true); - - let port_network = &port.network; - frp::extend! { port_network - code_update <- widget.value_changed.map(f!([crumbs](value) { - let expression = value.clone().unwrap_or_default(); - (crumbs.clone_ref(), expression) - })); - area_frp.source.on_port_code_update <+ code_update; - area_frp.source.request_import <+ widget.request_import; - widget.set_read_only <+ area_frp.set_read_only; - } - } - - init_color.emit(()); - - port_shape.display_object().clone_ref() - }; - - if let Some(parent_frp) = &builder.parent_frp { - frp::extend! { port_network - node.frp.set_active <+ parent_frp.set_active; - node.frp.set_hover <+ parent_frp.set_hover; - node.frp.set_parent_connected <+ parent_frp.set_parent_connected; - } - } - let new_parent_frp = Some(node.frp.output.clone_ref()); - let new_shift = if !not_a_port { 0 } else { builder.shift + local_char_offset }; - let parenthesized = node.parenthesized(); - builder.nested(new_parent, new_parent_frp, parenthesized, new_shift) - }); - *self.id_crumbs_map.borrow_mut() = id_crumbs_map; - *self.widgets_map.borrow_mut() = widgets_map; - area_frp.set_view_mode.emit(area_frp.view_mode.value()); - } - - fn init_port_widget( - &self, - port: &mut PortRefMut, - port_size: Vector2, - call_info: &CallInfoMap, - ) -> Option<(WidgetBind, widget::View)> { - let call_id = port.kind.call_id().filter(|id| call_info.has_target(id))?; - let argument_name = port.kind.argument_name()?.to_owned(); - - let widget_bind = WidgetBind { call_id, argument_name }; - - - // Try getting the previous widget by exact target/argument ID first, which is - // necessary when the argument expression was replaced. This lookup can fail - // when the target expression was replaced, but the widget argument expression - // wasn't. In that case, try to reuse the widget from old argument node under - // the same ast ID. - let prev_widgets_map = self.widgets_map.borrow(); - let prev_id_crumbs_map = self.id_crumbs_map.borrow(); - let prev_crumbs = prev_widgets_map - .get(&widget_bind) - .or_else(|| port.ast_id.as_ref().and_then(|id| prev_id_crumbs_map.get(id))); - let prev_widget = prev_crumbs.and_then(|crumbs| { - let prev_expression = self.expression.borrow(); - let prev_root = prev_expression.span_tree.root_ref(); - let prev_node = prev_root.get_descendant(crumbs).ok()?; - let prev_widget = prev_node.payload.widget.as_ref()?.clone_ref(); - Some(prev_widget) - }); - - let widget = match prev_widget { - Some(prev_widget) => port.payload.use_existing_widget(prev_widget), - None => port.payload.init_widget(&self.app), - }; - - let tag_values = port.kind.tag_values().unwrap_or_default().to_vec(); - let tp = port.kind.tp().cloned(); - widget.set_node_data(widget::NodeData { tag_values, port_size, tp }); - - Some((widget_bind, widget)) - } + // #[profile(Debug)] + // fn build_port_shapes_on_new_expression( + // &self, + // expression: &mut Expression, + // area_frp: &FrpEndpoints, + // call_info: &CallInfoMap, + // ) { + // let mut is_header = true; + + // expression.span_tree.root_ref_mut().dfs_with_layer_data(builder, |mut node, builder| { + // let skip_opr = if SKIP_OPERATIONS { + // node.is_operation() && !is_header + // } else { + // let crumb = ast::Crumb::Infix(ast::crumbs::InfixCrumb::Operator); + // node.ast_crumbs.last().map(|t| t == &crumb) == Some(true) + // }; + + // let not_a_port = node.is_positional_insertion_point() + // || node.is_chained() + // || (node.is_root() && !node.children.is_empty()) + // || skip_opr + // || node.is_token() + // || node.is_named_argument() + // || builder.parent_parensed; + + // if let Some(id) = node.ast_id { + // if DEBUG { + // debug!("New id mapping: {id} -> {:?}", node.crumbs); + // } + // id_crumbs_map.insert(id, node.crumbs.clone_ref()); + // } + + // if DEBUG { + // let indent = " ".repeat(4 * builder.depth); + // let skipped = if not_a_port { "(skip)" } else { "" }; + // debug!( + // "{indent}[{},{}] {skipped} {:?} (tp: {:?}) (id: {:?})", + // node.payload.index, + // node.payload.length, + // node.kind.variant_name(), + // node.tp(), + // node.ast_id + // ); + // } + + // let range_before_start = node.payload.index - node.payload.local_index; + // let range_before_end = node.payload.index; + // let range_before = enso_text::Range::new(range_before_start, range_before_end); + // let local_char_offset = code[range_before].chars().count(); + + // let new_parent = if not_a_port { + // builder.parent.clone_ref() + // } else { + // let crumbs = node.crumbs.clone_ref(); + // let port = &mut node; + + // let index = local_char_offset + builder.shift; + // let size = code[port.payload.range()].chars().count(); + // let unit = GLYPH_WIDTH; + // let width = unit * size as f32; + // let width_padded = width + 2.0 * PORT_PADDING_X; + // let height = 18.0; + // let size = Vector2(width, height); + // let padded_size = Vector2(width_padded, height); + // let position_x = unit * index as f32; + + // let port_shape = port.payload.init_shape(size, node::HEIGHT); + + // port_shape.set_x(position_x); + // if DEBUG { + // port_shape.set_y(DEBUG_PORT_OFFSET); + // } + + // if is_header { + // is_header = false; + // self.header.add_child(&port_shape); + // } else { + // builder.parent.add_child(&port_shape); + // } + + // // TODO: StyleWatch is unsuitable here, as it was designed as an internal tool + // for // shape system. (https://www.pivotaltracker.com/story/show/183567648) + // let style_sheet = &self.app.display.default_scene.style_sheet; + // let styles = StyleWatch::new(style_sheet); + // let styles_frp = &self.styles_frp; + // let any_type_sel_color = + // styles_frp.get_color(theme::code::types::any::selection); let + // port_network = &port.network; + + // frp::extend! { port_network + + // // === Aliases === + + // let mouse_over_raw = port_shape.hover.events.mouse_over.clone_ref(); + // let mouse_out = port_shape.hover.events.mouse_out.clone_ref(); + // let mouse_down_raw = port_shape.hover.events.mouse_down_primary.clone_ref(); + + // // === Body Hover === + + // // This is meant to be on top of FRP network. Read more about `Node` docs to + // // learn more about the architecture and the importance of the hover + // // functionality. + + // // Please note, that this is computed first in order to compute + // `ports_visible` // when needed, and thus it has to be run before the + // following lines. area_frp.source.body_hover <+ + // bool(&mouse_out,&mouse_over_raw); + + // // TODO[WD] for FRP3: Consider the following code. Here, we have to first + // // handle `bg_down` and then `mouse_down`. Otherwise, `mouse_down` may + // // trigger some events and can change `ports_visible` status, and thus + // make // the `bg_down` emitted unnecessarily. For example, after + // plugging in // connections to selected port, the `ports_visible` + // will be set to `false`, // and `bg_down` will be emitted, causing + // the node to be selected. This can // be solved by solving in the + // FRP engine all children first, and then their // children (then + // both `bg_down` and `mouse_down` will be resolved before // the + // `ports_visible` changes). bg_down <- + // mouse_down_raw.gate_not(&area_frp.ports_visible); mouse_down <- + // mouse_down_raw.gate(&area_frp.ports_visible); mouse_over <- + // mouse_over_raw.gate(&area_frp.ports_visible); + // area_frp.source.on_background_press <+ bg_down; + + // // === Press === + + // area_frp.source.on_port_press <+ mouse_down.map(f_!([crumbs] + // crumbs.clone_ref())); + + // // === Hover === + + // hovered <- bool(&mouse_out,&mouse_over); + // hover <- hovered.map (f!([crumbs](t) Switch::new(crumbs.clone_ref(),*t))); + // area_frp.source.on_port_hover <+ hover; + + // // === Pointer Style === + + // let port_shape_hover = port_shape.hover.clone_ref(); + // pointer_style_out <- mouse_out.map(|_| default()); + + // init_color <- source::<()>(); + // any_type_sel_color <- all_with(&any_type_sel_color,&init_color, + // |c,_| color::Lcha::from(c)); + // tp <- all_with(&port.tp,&area_frp.set_ports_active, + // |tp,(_,edge_tp)| tp.clone().or_else(||edge_tp.clone())); + // tp_color <- tp.map( + // f!([styles](tp) tp.map_ref(|tp| type_coloring::compute(tp,&styles)))); + // tp_color <- all_with(&tp_color,&any_type_sel_color, + // |tp_color,any_type_sel_color| tp_color.unwrap_or(*any_type_sel_color)); + // in_profiling_mode <- area_frp.view_mode.map(|m| + // matches!(m,view::Mode::Profiling)); pointer_color_over <- + // in_profiling_mode.switch(&tp_color,&any_type_sel_color); + // pointer_style_over <- pointer_color_over.map(move |color| + // cursor::Style::new_highlight(&port_shape_hover,padded_size,Some(color)) + // ); + // pointer_style_over <- pointer_style_over.sample(&mouse_over); + + // pointer_style_hover <- any(pointer_style_over,pointer_style_out); + // pointer_styles <- all[ + // pointer_style_hover, + // self.ports_label.pointer_style, + // self.edit_mode_label.pointer_style + // ]; + // pointer_style <- pointer_styles.fold(); + // area_frp.source.pointer_style <+ pointer_style; + // } + + // if let Some((widget_bind, widget)) = self.init_port_widget(port, size, call_info) + // { widgets_map.insert(widget_bind, crumbs.clone_ref()); + // widget.set_x(position_x); + // builder.parent.add_child(&widget); + + // if port.is_argument() { + // let range = port.span(); + // let code = &expression.code[range]; + // debug!("Setting current value while range is {range:?}, code is + // \"{code}\" \ and full expression is \"{}\".", + // expression.code); widget.set_current_value(Some(code.into())); + // } else { + // widget.set_current_value(None); + // } + // widget.set_visible(true); + + // let port_network = &port.network; + // frp::extend! { port_network + // code_update <- widget.value_changed.map(f!([crumbs](value) { + // let expression = value.clone().unwrap_or_default(); + // (crumbs.clone_ref(), expression) + // })); + // area_frp.source.on_port_code_update <+ code_update; + // area_frp.source.request_import <+ widget.request_import; + // } + // } + + // init_color.emit(()); + + // port_shape.display_object().clone_ref() + // }; + + // if let Some(parent_frp) = &builder.parent_frp { + // frp::extend! { port_network + // node.frp.set_active <+ parent_frp.set_active; + // node.frp.set_hover <+ parent_frp.set_hover; + // node.frp.set_parent_connected <+ parent_frp.set_parent_connected; + // } + // } + // let new_parent_frp = Some(node.frp.output.clone_ref()); + // let new_shift = if !not_a_port { 0 } else { builder.shift + local_char_offset }; + // let parenthesized = node.parenthesized(); + // builder.nested(new_parent, new_parent_frp, parenthesized, new_shift) + // }); + // *self.id_crumbs_map.borrow_mut() = id_crumbs_map; + // *self.widgets_map.borrow_mut() = widgets_map; + // area_frp.set_view_mode.emit(area_frp.view_mode.value()); + // } + + // fn init_port_widget( + // &self, + // port: &mut PortRefMut, + // port_size: Vector2, + // call_info: &CallInfoMap, + // ) -> Option<(WidgetBind, widget::Root)> { + // let call_id = port.kind.call_id().filter(|id| call_info.has_target(id))?; + // let argument_name = port.kind.argument_name()?.to_owned(); + + // let widget_bind = WidgetBind { call_id, argument_name }; + + + // // Try getting the previous widget by exact target/argument ID first, which is + // // necessary when the argument expression was replaced. This lookup can fail + // // when the target expression was replaced, but the widget argument expression + // // wasn't. In that case, try to reuse the widget from old argument node under + // // the same ast ID. + // let prev_widgets_map = self.widgets_map.borrow(); + // let prev_id_crumbs_map = self.id_crumbs_map.borrow(); + // let prev_crumbs = prev_widgets_map + // .get(&widget_bind) + // .or_else(|| port.ast_id.as_ref().and_then(|id| prev_id_crumbs_map.get(id))); + // let prev_widget = prev_crumbs.and_then(|crumbs| { + // let prev_expression = self.expression.borrow(); + // let prev_root = prev_expression.span_tree.root_ref(); + // let prev_node = prev_root.get_descendant(crumbs).ok()?; + // let prev_widget = prev_node.payload.widget.as_ref()?.clone_ref(); + // Some(prev_widget) + // }); + + // let widget = match prev_widget { + // Some(prev_widget) => port.payload.use_existing_widget(prev_widget), + // None => port.payload.init_widget(&self.app), + // }; + + // let tag_values = port.kind.tag_values().unwrap_or_default().to_vec(); + // // widget.set_node_data(widget::NodeData { tag_values, port_size }); + + // Some((widget_bind, widget)) + // } /// Initializes FRP network for every port. Please note that the networks are connected /// hierarchically (children get events from parents), so it is easier to init all networks /// this way, rather than delegate it to every port. + // #[profile(Debug)] + // fn init_port_frp_on_new_expression( + // &self, + // expression: &mut Expression, + // area_frp: &FrpEndpoints, + // ) { + // let model = &self; + + // let parent_tp: Option>> = None; + // expression.root_ref_mut().dfs_with_layer_data(parent_tp, |node, parent_tp| { + // let frp = &node.frp; + // let port_network = &frp.network; + // let is_token = node.is_token(); + // let crumbs = node.crumbs.clone(); + + // // === Type Computation === + // let parent_tp = parent_tp.clone().unwrap_or_else(|| { + // frp::extend! { port_network + // empty_parent_tp <- source::>(); + // } + // empty_parent_tp.into() + // }); + // frp::extend! { port_network + // final_tp <- all_with3(&parent_tp,&frp.set_definition_type,&frp.set_usage_type, + // move |parent_tp,def_tp,usage_tp| { + // usage_tp.clone().or_else(|| + // if is_token {parent_tp.clone()} else {def_tp.clone()} + // ) + // } + // ); + // frp.source.tp <+ final_tp; + + // area_frp.source.on_port_type_change <+ frp.tp.map(move + // |t|(crumbs.clone(),t.clone())); } + + // // === Code Coloring === + + // let styles = model.styles.clone_ref(); + // let styles_frp = model.styles_frp.clone_ref(); + + // if node.children.is_empty() { + // let is_expected_arg = node.is_expected_argument(); + + // use theme::code::syntax; + // let selected_color = styles_frp.get_color(theme::code::types::selected); + // let std_base_color = styles_frp.get_color(syntax::base); + // let std_disabled_color = styles_frp.get_color(syntax::disabled); + // let std_expected_color = styles_frp.get_color(syntax::expected); + // let std_editing_color = styles_frp.get_color(syntax::base); + // let profiled_base_color = styles_frp.get_color(syntax::profiling::base); + // let profiled_disabled_color = styles_frp.get_color(syntax::profiling::disabled); + // let profiled_expected_color = styles_frp.get_color(syntax::profiling::expected); + // let profiled_editing_color = styles_frp.get_color(syntax::profiling::base); + + // frp::extend! { port_network + // in_profiling_mode <- area_frp.view_mode.map(|m| m.is_profiling()); + // finished <- area_frp.set_profiling_status.map(|s| s.is_finished()); + // profiled <- in_profiling_mode && finished; + // selected <- frp.set_hover || frp.set_parent_connected; + + // init_colors <- source::<()>(); + // std_base_color <- all(std_base_color,init_colors)._0(); + // profiled_base_color <- all(profiled_base_color,init_colors)._0(); + + // profiling_color <- finished.switch(&std_base_color,&profiled_base_color); + // normal_color <- frp.tp.map(f!([styles](t) + // color::Rgba::from(type_coloring::compute_for_code(t.as_ref(),&styles)))); + // base_color <- in_profiling_mode.switch(&normal_color,&profiling_color); + + // disabled_color <- + // profiled.switch(&std_disabled_color,&profiled_disabled_color); + // expected_color <- profiled.switch(&std_expected_color,&profiled_expected_color); + // editing_color <- + // profiled.switch(&std_editing_color,&profiled_editing_color); // TODO: + // `label_color` should be animated, when when we can set text colors // more efficiently. (See https://www.pivotaltracker.com/story/show/183567665) + // label_color <- all_with8( + // &area_frp.editing, + // &selected, + // &area_frp.set_disabled, + // &editing_color, + // &selected_color, + // &disabled_color, + // &expected_color, + // &base_color, + // move |&editing, + // &selected, + // &disabled, + // &editing_color, + // &selected_color, + // &disabled_color, + // &expected_color, + // &base_color| { + // if editing { + // color::Lcha::from(editing_color) + // } else if selected { + // color::Lcha::from(selected_color) + // } else if disabled { + // color::Lcha::from(disabled_color) + // } else if is_expected_arg { + // color::Lcha::from(expected_color) + // } else { + // color::Lcha::from(base_color) + // } + // }, + // ); + // } + + // let index = node.payload.index; + // let length = node.payload.length; + // let label = model.ports_label.clone_ref(); + // frp::extend! { port_network + // eval label_color ([label](color) { + // let range = enso_text::Range::new(index, index + length); + // // TODO: remove unwrap. (https://www.pivotaltracker.com/story/show/183567590) + // let range = enso_text::Range::::try_from(range).unwrap(); + // label.set_property(range,color::Rgba::from(color)); + // }); + // } + + // init_colors.emit(()); + // area_frp.set_view_mode(area_frp.view_mode.value()); + // } + + // // === Highlight Coloring === + + // if let Some(port_shape) = &node.payload.shape { + // let viz_color = color::Animation::new(port_network); + // let any_type_sel_color = + // styles_frp.get_color(theme::code::types::any::selection); + + // frp::extend! { port_network + // normal_viz_color <- all_with(&frp.tp,&frp.set_connected, + // f!([styles](port_tp,(_,edge_tp)) { + // let tp = port_tp.as_ref().or(edge_tp.as_ref()); + // select_color(&styles,tp) + // })); + // init_color <- source::<()>(); + // profiling_viz_color <- all_with(&any_type_sel_color,&init_color, + // |c,_| color::Lcha::from(c)); + // profiling <- area_frp.view_mode.map(|m| m.is_profiling()); + // connected_viz_color <- + // profiling.switch(&normal_viz_color,&profiling_viz_color); is_connected + // <- frp.set_connected.map(|(is_connected,_)| *is_connected); transparent + // <- init_color.constant(color::Lcha::transparent()); viz_color_target + // <- is_connected.switch(&transparent,&connected_viz_color); + + // // We need to make sure that the network contains correct values before we + // // connect the `viz_color` animation. The reason is that the animation will + // // start from the first value that it receives, and during initialization of + // the // network, while some nodes are still set to their defaults, this + // first value // would be incorrect, causing the animation in some cases + // to start from black // (the default color) and animating towards the + // color that we really want to // set. + // init_color.emit(()); + + // viz_color.target <+ viz_color_target; + // eval viz_color.value ((t) + // port_shape.viz.color.set(color::Rgba::from(t).into()) + // ); + // } + // } + // Some(frp.tp.clone_ref().into()) + // }); + + // area_frp.set_view_mode(area_frp.view_mode.value()); + // } + + /// Request widgets metadata for all method calls within the expression. #[profile(Debug)] - fn init_port_frp_on_new_expression( - &self, - expression: &mut Expression, - area_frp: &FrpEndpoints, - ) { - let model = &self; - - let parent_tp: Option>> = None; - expression.root_ref_mut().dfs_with_layer_data(parent_tp, |node, parent_tp| { - let frp = &node.frp; - let port_network = &frp.network; - let is_token = node.is_token(); - let crumbs = node.crumbs.clone(); - - - // === Type Computation === - let parent_tp = parent_tp.clone().unwrap_or_else(|| { - frp::extend! { port_network - empty_parent_tp <- source::>(); - } - empty_parent_tp.into() - }); - frp::extend! { port_network - final_tp <- all_with3(&parent_tp,&frp.set_definition_type,&frp.set_usage_type, - move |parent_tp,def_tp,usage_tp| { - usage_tp.clone().or_else(|| - if is_token {parent_tp.clone()} else {def_tp.clone()} - ) - } - ); - frp.source.tp <+ final_tp; - - area_frp.source.on_port_type_change <+ frp.tp.map(move |t|(crumbs.clone(),t.clone())); - } - - - // === Code Coloring === - - let styles = model.styles.clone_ref(); - let styles_frp = model.styles_frp.clone_ref(); - - if node.children.is_empty() { - let is_expected_arg = node.is_expected_argument(); - - use theme::code::syntax; - let selected_color = styles_frp.get_color(theme::code::types::selected); - let std_base_color = styles_frp.get_color(syntax::base); - let std_disabled_color = styles_frp.get_color(syntax::disabled); - let std_expected_color = styles_frp.get_color(syntax::expected); - let std_editing_color = styles_frp.get_color(syntax::base); - let profiled_base_color = styles_frp.get_color(syntax::profiling::base); - let profiled_disabled_color = styles_frp.get_color(syntax::profiling::disabled); - let profiled_expected_color = styles_frp.get_color(syntax::profiling::expected); - let profiled_editing_color = styles_frp.get_color(syntax::profiling::base); - - frp::extend! { port_network - in_profiling_mode <- area_frp.view_mode.map(|m| m.is_profiling()); - finished <- area_frp.set_profiling_status.map(|s| s.is_finished()); - profiled <- in_profiling_mode && finished; - selected <- frp.set_hover || frp.set_parent_connected; - - init_colors <- source::<()>(); - std_base_color <- all(std_base_color,init_colors)._0(); - profiled_base_color <- all(profiled_base_color,init_colors)._0(); - - profiling_color <- finished.switch(&std_base_color,&profiled_base_color); - normal_color <- frp.tp.map(f!([styles](t) - color::Rgba::from(type_coloring::compute_for_code(t.as_ref(),&styles)))); - base_color <- in_profiling_mode.switch(&normal_color,&profiling_color); - - disabled_color <- profiled.switch(&std_disabled_color,&profiled_disabled_color); - expected_color <- profiled.switch(&std_expected_color,&profiled_expected_color); - editing_color <- profiled.switch(&std_editing_color,&profiled_editing_color); - // TODO: `label_color` should be animated, when when we can set text colors - // more efficiently. (See https://www.pivotaltracker.com/story/show/183567665) - label_color <- all_with8( - &area_frp.editing, - &selected, - &area_frp.set_disabled, - &editing_color, - &selected_color, - &disabled_color, - &expected_color, - &base_color, - move |&editing, - &selected, - &disabled, - &editing_color, - &selected_color, - &disabled_color, - &expected_color, - &base_color| { - if editing { - color::Lcha::from(editing_color) - } else if selected { - color::Lcha::from(selected_color) - } else if disabled { - color::Lcha::from(disabled_color) - } else if is_expected_arg { - color::Lcha::from(expected_color) - } else { - color::Lcha::from(base_color) - } - }, - ); - } - - let index = node.payload.index; - let length = node.payload.length; - let label = model.ports_label.clone_ref(); - frp::extend! { port_network - eval label_color ([label](color) { - let range = enso_text::Range::new(index, index + length); - // TODO: remove unwrap. (https://www.pivotaltracker.com/story/show/183567590) - let range = enso_text::Range::::try_from(range).unwrap(); - label.set_property(range,color::Rgba::from(color)); - }); - } - - init_colors.emit(()); - area_frp.set_view_mode(area_frp.view_mode.value()); - } - - - // === Highlight Coloring === - - if let Some(port_shape) = &node.payload.shape { - let viz_color = color::Animation::new(port_network); - let any_type_sel_color = styles_frp.get_color(theme::code::types::any::selection); - - frp::extend! { port_network - normal_viz_color <- all_with(&frp.tp,&frp.set_connected, - f!([styles](port_tp,(_,edge_tp)) { - let tp = port_tp.as_ref().or(edge_tp.as_ref()); - select_color(&styles,tp) - })); - init_color <- source::<()>(); - profiling_viz_color <- all_with(&any_type_sel_color,&init_color, - |c,_| color::Lcha::from(c)); - profiling <- area_frp.view_mode.map(|m| m.is_profiling()); - connected_viz_color <- profiling.switch(&normal_viz_color,&profiling_viz_color); - is_connected <- frp.set_connected.map(|(is_connected,_)| *is_connected); - transparent <- init_color.constant(color::Lcha::transparent()); - viz_color_target <- is_connected.switch(&transparent,&connected_viz_color); - - // We need to make sure that the network contains correct values before we - // connect the `viz_color` animation. The reason is that the animation will - // start from the first value that it receives, and during initialization of the - // network, while some nodes are still set to their defaults, this first value - // would be incorrect, causing the animation in some cases to start from black - // (the default color) and animating towards the color that we really want to - // set. - init_color.emit(()); - - viz_color.target <+ viz_color_target; - eval viz_color.value ((t) - port_shape.viz.color.set(color::Rgba::from(t).into()) - ); - } - } - Some(frp.tp.clone_ref().into()) - }); - - area_frp.set_view_mode(area_frp.view_mode.value()); - } - - /// This function first assigns the new expression to the model and then emits the definition - /// type signals to all port FRP networks. - /// - /// As a design note, it is important to first assign the expression to the model, as the FRP - /// signals can cause other parts of the network to fire, which may query the expression types. - /// For example, firing the `port::set_definition_type` will fire `on_port_type_change`, which - /// may require some edges to re-color, which consequently will require to checking the current - /// expression types. - #[profile(Debug)] - fn init_new_expression( - &self, - expression: Expression, - area_frp: &FrpEndpoints, - call_info: &CallInfoMap, - ) { - *self.expression.borrow_mut() = expression; - let expression = self.expression.borrow(); - expression.root_ref().dfs_with_layer_data((), |node, _| { - node.frp.set_definition_type(node.tp().cloned().map(|t| t.into())); - let call_id = node.kind.call_id(); - let widget_request = - call_id.and_then(|call_id| Some((call_id, call_info.target(&call_id)?))); - if let Some(widget_request) = widget_request { - area_frp.source.requested_widgets.emit(widget_request); + fn request_widgets_metadata(&self, expression: &InputExpression, area_frp: &FrpEndpoints) { + let call_info = CallInfoMap::scan_expression(&expression.span_tree); + for (call_id, info) in call_info.iter() { + if let Some(target_id) = info.target_id { + area_frp.source.requested_widgets.emit((*call_id, target_id)); } - }); + } } /// Set a displayed expression, updating the input ports. `is_editing` indicates whether the /// expression is being edited by the user. #[profile(Debug)] fn set_expression(&self, new_expression: impl Into, area_frp: &FrpEndpoints) { - let mut new_expression = Expression::from(new_expression.into()); - if DEBUG { - debug!("set expression: \n{:?}", new_expression.tree_pretty_printer()); - } - - let call_info = CallInfoMap::scan_expression(&new_expression); - self.set_label_on_new_expression(&new_expression); - self.build_port_shapes_on_new_expression(&mut new_expression, area_frp, &call_info); - self.init_port_frp_on_new_expression(&mut new_expression, area_frp); - self.init_new_expression(new_expression.clone(), area_frp, &call_info); + let new_expression = InputExpression::from(new_expression.into()); + // if DEBUG { + warn!("set expression: \n{:?}", new_expression.tree_pretty_printer()); + // } + + self.root_widget.rebuild_tree(&new_expression.span_tree, &new_expression.code); + + // TODO streams to handle: + // area_frp.source.pointer_style <+ pointer_style; + // area_frp.source.pointer_style + + // pointer_style (cursor::Style), + // width (f32), + // editing (bool), + // ports_visible (bool), + // body_hover (bool), + // on_port_press (Crumbs), + // on_port_hover (Switch), + // on_port_type_change (Crumbs,Option), ?? + // on_background_press (), + // view_mode (view::Mode), + + + // self.init_port_frp_on_new_expression(&mut new_expression, area_frp); + self.request_widgets_metadata(&new_expression, area_frp); + *self.expression.borrow_mut() = new_expression; } } @@ -974,17 +881,17 @@ ensogl::define_endpoints! { /// ## Origin /// 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)] +#[derive(Clone, Deref, CloneRef, Debug)] pub struct Area { #[allow(missing_docs)] + #[deref] pub frp: Frp, pub(crate) model: Rc, } -impl Deref for Area { - type Target = Frp; - fn deref(&self) -> &Self::Target { - &self.frp +impl display::Object for Area { + fn display_object(&self) -> &display::object::Instance { + &self.model.display_object } } @@ -1043,27 +950,31 @@ impl Area { // === Label Hover === label_hovered <- reacts_to_hover && frp.output.body_hover; - not_editing <- set_editing.not(); - model.ports_label.set_hover <+ label_hovered && not_editing; model.edit_mode_label.set_hover <+ label_hovered && set_editing; // === Port Hover === eval frp.on_port_hover ((t) model.set_port_hover(t)); - eval frp.set_connected ([model]((crumbs,edge_tp,is_connected)) { - model.with_port_mut(crumbs,|n|n.set_connected(is_connected,edge_tp)); - model.with_port_mut(crumbs,|n|n.set_parent_connected(is_connected)); - }); + // eval frp.set_connected ([model]((crumbs,edge_tp,is_connected)) { + // model.with_port_mut(crumbs,|n|n.set_connected(is_connected,edge_tp)); + // model.with_port_mut(crumbs,|n|n.set_parent_connected(is_connected)); + // }); // === Properties === + let layout_refresh = ensogl::animation::on_before_animations(); + root_widget_width <- layout_refresh.map(f!([model](_) { + let instance = model.root_widget.display_object(); + instance.computed_size().x() + })); - label_width <- set_editing.switch( - &model.ports_label.width, - &model.edit_mode_label.width + padded_edit_label_width <- model.edit_mode_label.width.map(|t| t + 2.0 * TEXT_OFFSET); + + frp.output.source.width <+ set_editing.switch( + &root_widget_width, + &padded_edit_label_width ); - frp.output.source.width <+ label_width.map(|t| t + 2.0 * TEXT_OFFSET); // === Expression === @@ -1088,10 +999,17 @@ impl Area { (default(), e.into()) }); + widget_code_update <- model.root_widget.value_changed.map(|(crumbs, value)| { + let expression = value.clone().unwrap_or_default(); + (crumbs.clone(), expression) + }); + + frp.output.source.on_port_code_update <+ widget_code_update; + frp.output.source.request_import <+ model.root_widget.request_import; // === Expression Type === - eval frp.set_expression_usage_type (((a,b)) model.set_expression_usage_type(a,b)); + // eval frp.set_expression_usage_type (((a,b)) model.set_expression_usage_type(a,b)); // === Widgets === @@ -1114,12 +1032,12 @@ impl Area { selection_color_rgba <- profiled.switch(&std_selection_color,&profiled_selection_color); selection_color.target <+ selection_color_rgba.map(|c| color::Lcha::from(c)); - model.ports_label.set_selection_color <+ selection_color.value.map(|c| color::Lch::from(c)); + // model.ports_label.set_selection_color <+ selection_color.value.map(|c| color::Lch::from(c)); std_base_color <- all(std_base_color,init)._0(); profiled_base_color <- all(profiled_base_color,init)._0(); base_color <- profiled.switch(&std_base_color,&profiled_base_color); - eval base_color ((color) model.ports_label.set_property_default(color)); + // eval base_color ((color) model.ports_label.set_property_default(color)); } init.emit(()); @@ -1130,21 +1048,24 @@ impl Area { /// An offset from node position to a specific port. pub fn port_offset(&self, crumbs: &[Crumb]) -> Option> { let expr = self.model.expression.borrow(); - expr.root_ref().get_descendant(crumbs).ok().map(|node| { - let unit = GLYPH_WIDTH; - let range_before = enso_text::Range::new(ByteDiff(0), node.payload.index); - let char_offset = expr.viz_code[range_before].chars().count(); - let char_count = expr.viz_code[node.payload.range()].chars().count(); - let width = unit * (char_count as f32); - let x = width / 2.0 + unit * (char_offset as f32); - Vector2::new(TEXT_OFFSET + x, 0.0) - }) + None + // nocheckin + // expr.root_ref().get_descendant(crumbs).ok().map(|node| { + // let unit = GLYPH_WIDTH; + // let range_before = enso_text::Range::new(ByteDiff(0), node.payload.index); + // let char_offset = expr.viz_code[range_before].chars().count(); + // let char_count = expr.viz_code[node.payload.range()].chars().count(); + // let width = unit * (char_count as f32); + // let x = width / 2.0 + unit * (char_offset as f32); + // Vector2::new(TEXT_OFFSET + x, 0.0) + // }) } /// A type of the specified port. - pub fn port_type(&self, crumbs: &Crumbs) -> Option { - let expression = self.model.expression.borrow(); - expression.span_tree.root_ref().get_descendant(crumbs).ok().and_then(|t| t.tp.value()) + pub fn port_type(&self, _crumbs: &Crumbs) -> Option { + // let expression = self.model.expression.borrow(); + // expression.span_tree.root_ref().get_descendant(crumbs).ok().and_then(|t| t.tp.value()) + None } /// A crumb by AST ID. @@ -1160,69 +1081,6 @@ impl Area { -// ========================== -// === Expression Setting === -// ========================== - -/// Helper struct used to keep information about the current expression layer when building visual -/// port representation. A "layer" is a visual layer in terms of span tree. For example, given -/// expression `img.blur (foo (bar baz))`, we've got several layers, like the whole expression, -/// `img.blur`, `foo (bar baz)`, or `(bar baz)`. The layer builder keeps information passed from the -/// parent layer when building the nested one. -#[derive(Clone, Debug)] -struct PortLayerBuilder { - parent_frp: Option, - /// Parent port display object. - parent: display::object::Instance, - /// Information whether the parent port was a parensed expression. - parent_parensed: bool, - /// The number of chars the expression should be shifted. For example, consider - /// `(foo bar)`, where expression `foo bar` does not get its own port, and thus a 1 char - /// shift should be applied when considering its children. - shift: usize, - /// The depth at which the current expression is, where root is at depth 0. - depth: usize, -} - -impl PortLayerBuilder { - /// Constructor. - #[profile(Debug)] - fn new( - parent: impl display::Object, - parent_frp: Option, - parent_parensed: bool, - shift: usize, - depth: usize, - ) -> Self { - let parent = parent.display_object().clone_ref(); - Self { parent_frp, parent, parent_parensed, shift, depth } - } - - fn empty(parent: impl display::Object) -> Self { - Self::new(parent, default(), default(), default(), default()) - } - - /// Create a nested builder with increased depth and updated `parent_frp`. - #[profile(Debug)] - fn nested( - &self, - parent: display::object::Instance, - new_parent_frp: Option, - parent_parensed: bool, - shift: usize, - ) -> Self { - let depth = self.depth + 1; - let parent_frp = new_parent_frp.or_else(|| self.parent_frp.clone()); - Self::new(parent, parent_frp, parent_parensed, shift, depth) - } -} - -impl display::Object for Area { - fn display_object(&self) -> &display::object::Instance { - &self.model.display_object - } -} - /// =================== /// === CallInfoMap === /// =================== diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index 94b88b1bcb80..a18bd60ccb05 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -156,7 +156,7 @@ ensogl::define_endpoints! { pub struct Model { pub frp: Frp, pub shape: Option, - pub widget: Option, + pub widget: Option, pub index: ByteDiff, pub local_index: ByteDiff, pub length: ByteDiff, @@ -188,14 +188,14 @@ impl Model { /// Widget initialization. Only nodes that represent function arguments or argument placeholders /// will have widgets created for them. - pub fn init_widget(&mut self, app: &Application) -> widget::View { - let widget = widget::View::new(app); + pub fn init_widget(&mut self, app: &Application) -> widget::Root { + let widget = widget::Root::new(app); self.widget = Some(widget.clone_ref()); widget } /// Assign an existing widget to this port. - pub fn use_existing_widget(&mut self, widget: widget::View) -> widget::View { + pub fn use_existing_widget(&mut self, widget: widget::Root) -> widget::Root { self.widget = Some(widget.clone_ref()); widget } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index f6af513692a6..cd3ce2d6ed80 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -2,18 +2,17 @@ use crate::prelude::*; +use crate::component::node::input::area::NODE_HEIGHT; +use crate::component::node::input::area::TEXT_OFFSET; use enso_config::ARGS; use enso_frp as frp; +use enso_text as text; use ensogl::application::Application; use ensogl::display; use ensogl_component::drop_down::DropdownValue; - - -// ============== -// === Export === -// ============== - -pub mod vector_editor; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use text::index::Byte; @@ -23,36 +22,54 @@ pub mod vector_editor; ensogl::define_endpoints_2! { Input { - set_metadata (Option), - set_node_data (NodeData), - set_current_value (Option), - set_focused (bool), - set_visible (bool), - set_read_only (bool), + /// Set the widget's metadata that was received from the language server. It overrides + /// widget's configuration, even allowing the widget type to be completely changed. When + /// the metadata is set to `Some` value, the corresponding widget will ignore its span-tree + /// type. + set_metadata (MetadataPointer, Option), } Output { - value_changed(Option), - request_import(ImString), + value_changed (span_tree::Crumbs, Option), + request_import (ImString), } } +/// Information associated with widget metadata which describes which uniquely identifies the +/// widget to reconfigure. +#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] +pub struct MetadataPointer { + pub call_id: ast::Id, + pub argument_name: ImString, +} + + /// ====================== /// === Widget modules === /// ====================== /// Common trait for constructing and reconfiguring all widget variants. pub trait SpanWidget { + /// Configuration associated with specific widget variant. type Config: Debug + Clone + PartialEq; - fn new(config: &Self::Config, ctx: ConfigContext<'_>) -> Self; - fn configure(&mut self, config: &Self::Config, ctx: ConfigContext<'_>); + /// Create a new widget with given configuration. + fn new(config: &Self::Config, ctx: ConfigContext) -> Self; + /// Update configuration for existing widget. + fn configure(&mut self, config: &Self::Config, ctx: ConfigContext); } -pub struct ConfigContext<'a> { - app: &'a Application, - display_object: &'a display::object::Instance, - frp: &'a SampledFrp, - label: Option, - display: Display, +/// Create a widget if it does not exist, or reconfigure if it does. +pub fn create_or_update_widget( + old_widget: Option, + config: &T::Config, + ctx: ConfigContext, +) -> T { + match old_widget { + Some(mut widget) => { + widget.configure(config, ctx); + widget + } + None => T::new(config, ctx), + } } macro_rules! define_widget_modules( @@ -95,7 +112,7 @@ macro_rules! define_widget_modules( impl SpanWidget for DynWidget { type Config = Config; - fn new(config: &Config, ctx: ConfigContext<'_>) -> Self { + fn new(config: &Config, ctx: ConfigContext) -> Self { match config { $( Config::$name(config) => DynWidget::$name(SpanWidget::new(config, ctx)), @@ -103,7 +120,7 @@ macro_rules! define_widget_modules( } } - fn configure(&mut self, config: &Config, ctx: ConfigContext<'_>) { + fn configure(&mut self, config: &Config, ctx: ConfigContext) { match (self, config) { $((DynWidget::$name(model), Config::$name(config)) => { SpanWidget::configure(model, config, ctx); @@ -124,10 +141,14 @@ define_widget_modules! { SingleChoice single_choice, /// A widget for managing a list of values - adding, removing or reordering them. VectorEditor vector_editor, + /// Default span tree traversal widget. + Hierarchy hierarchy, } impl Config { - const FALLBACK: Self = Config::Label(label::Config); + const RAW_EXPRESSION: Self = Config::Label(label::Config { placeholder: false }); + const ARG_PLACEHOLDER: Self = Config::Label(label::Config { placeholder: true }); + const HIERARCHY: Self = Config::Hierarchy(hierarchy::Config); } /// ================ @@ -146,21 +167,50 @@ pub struct Metadata { } impl Metadata { - const FALLBACK: Self = - Metadata { label: None, display: Display::Always, config: Config::FALLBACK }; + const RAW_EXPRESSION: Self = + Metadata { label: None, display: Display::Always, config: Config::RAW_EXPRESSION }; /// Widget metadata for static dropdown, based on the tag values provided by suggestion /// database. - fn static_dropdown(tag_values: &[span_tree::TagValue]) -> Metadata { + fn static_dropdown(label: Option, tag_values: &[span_tree::TagValue]) -> Metadata { let entries = Rc::new(tag_values.into_iter().map(Entry::from).collect()); let config = single_choice::Config { entries }.into(); - Self { label: None, display: Display::Always, config } + Self { label, display: Display::Always, config } } fn vector_editor() -> Metadata { let config = vector_editor::Config::default().into(); Self { label: None, display: Display::Always, config } } + + fn from_kind(kind: &span_tree::node::Kind) -> Self { + use span_tree::node::Kind; + + const VECTOR_TYPE: &str = "Standard.Base.Data.Vector.Vector"; + let is_array_enabled = ARGS.groups.feature_preview.options.vector_editor.value; + + match kind { + Kind::Argument(arg) if !arg.tag_values.is_empty() => + Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values), + Kind::Argument(arg) + if is_array_enabled + && arg.tp.as_ref().map_or(false, |tp| tp.contains(VECTOR_TYPE)) => + Self::vector_editor(), + Kind::InsertionPoint(arg) if arg.kind.is_expected_argument() => + if !arg.tag_values.is_empty() { + Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values) + } else { + Metadata { + label: arg.name.as_ref().map(Into::into), + display: Display::Always, + config: Config::ARG_PLACEHOLDER, + } + }, + Kind::Root | Kind::NamedArgument | Kind::Chained(_) => + Metadata { label: None, display: Display::Always, config: Config::HIERARCHY }, + _ => Self::RAW_EXPRESSION, + } + } } /// Widget display mode. Determines when the widget should be expanded. @@ -224,33 +274,19 @@ impl DropdownValue for Entry { } } -/// The data of node port that this widget is attached to. Available immediately after widget -/// creation. Can be updated later when the node data changes. -#[derive(Debug, Clone, Default, PartialEq)] -#[allow(missing_docs)] -pub struct NodeData { - pub tag_values: Vec, - pub port_size: Vector2, - pub tp: Option, -} - -/// ================== -/// === SampledFrp === -/// ================== +// ================== +// === SampledFrp === +// ================== /// Sampled version of widget FRP endpoints that can be used by widget views that are initialized /// on demand after first interaction. Without samplers, when a widget view would be initialized /// after the endpoints were set, it would not receive previously set endpoint values. #[derive(Debug, Clone, CloneRef)] -pub struct SampledFrp { - set_current_value: frp::Sampler>, - set_visible: frp::Sampler, - set_focused: frp::Sampler, - out_value_changed: frp::Any>, +struct SampledFrp { + out_value_changed: frp::Any<(span_tree::Crumbs, Option)>, out_request_import: frp::Any, - set_read_only: frp::Sampler, } @@ -261,59 +297,54 @@ pub struct SampledFrp { /// The node widget view. Represents one widget of any kind on the node input area. Can change its /// appearance and behavior depending on the widget metadata updates, without being recreated. -#[derive(Debug, Clone, CloneRef)] -pub struct View { - frp: Frp, - model: Rc, +#[derive(Debug, Deref, Clone, CloneRef)] +pub struct Root { + #[deref] + frp: Frp, + sampled_frp: SampledFrp, + model: Rc, +} + +impl display::Object for Root { + fn display_object(&self) -> &display::object::Instance { + &self.model.display_object + } } -impl View { - /// Create a new node widget. The widget is initialized to empty state, waiting for widget - /// metadata to be provided using `set_node_data` and `set_metadata` FRP endpoints. +impl Root { + /// Create a new node widget. The widget is initialized to empty state, waiting for first + /// `rebuild_tree` call to build appropriate view hierarchy. #[profile(Task)] pub fn new(app: &Application) -> Self { let frp = Frp::new(); - let model = Rc::new(Model::new(app)); - Self { frp, model }.init() - } + let model = Rc::new(RootModel::new(app)); + let network = frp.network(); - /// Widget FRP API. Contains all endpoints that can be used to control the widget of any kind. - pub fn frp(&self) -> &Frp { - &self.frp - } + frp::extend! { network + eval frp.input.set_metadata([model]((pointer, meta)) { + model.set_metadata(pointer.clone(), meta.clone()); + }); + } - fn init(self) -> Self { - let model = &self.model; - let frp = &self.frp; - let network = &frp.network; - let input = &frp.input; + let out_value_changed = frp.private.output.value_changed.clone_ref(); + let out_request_import = frp.private.output.request_import.clone_ref(); + let sampled_frp = SampledFrp { out_value_changed, out_request_import }; - frp::extend! { network - metadata_change <- input.set_metadata.on_change(); - node_data_change <- input.set_node_data.on_change(); - widget_data <- all(&metadata_change, &node_data_change).debounce(); - - set_current_value <- input.set_current_value.sampler(); - set_visible <- input.set_visible.sampler(); - set_focused <- input.set_focused.sampler(); - set_read_only <- input.set_read_only.sampler(); - let out_value_changed = frp.private.output.value_changed.clone_ref(); - let out_request_import = frp.private.output.request_import.clone_ref(); - let sampled_frp = SampledFrp { - set_current_value, - set_visible, - set_focused, - out_value_changed, - out_request_import, - set_read_only - }; + Self { frp, sampled_frp, model } + } - eval widget_data([model, sampled_frp]((meta, node_data)) { - model.set_widget_data(&sampled_frp, meta, node_data); - }); + pub fn rebuild_tree_on_metadata_change( + &self, + tree: &span_tree::SpanTree, + node_expression: &str, + ) { + if self.model.metadata_dirty.load(Ordering::Acquire) { + self.rebuild_tree(tree, node_expression); } + } - self + pub fn rebuild_tree(&self, tree: &span_tree::SpanTree, node_expression: &str) { + self.model.rebuild_tree(self.sampled_frp.clone_ref(), tree, node_expression) } } @@ -322,62 +353,208 @@ impl View { /// ============= #[derive(Debug)] -struct Model { +struct RootModel { app: Application, display_object: display::object::Instance, - inner_widget: RefCell>, + widgets_map: RefCell>, + metadata_map: Rc>>, + metadata_dirty: AtomicBool, } -impl Model { +impl RootModel { /// Create a new node widget, selecting the appropriate widget type based on the provided /// argument info. fn new(app: &Application) -> Self { let app = app.clone_ref(); let display_object = display::object::Instance::new(); - let inner_widget = default(); - Self { app, display_object, inner_widget } + display_object.use_auto_layout(); + + display_object.set_size_y(NODE_HEIGHT); + display_object.set_padding_left(TEXT_OFFSET); + display_object.set_padding_right(TEXT_OFFSET); + + let widgets_map = default(); + let metadata_map = default(); + let metadata_dirty = default(); + Self { app, display_object, widgets_map, metadata_map, metadata_dirty } } - #[profile(Task)] - fn set_widget_data(&self, frp: &SampledFrp, meta: &Option, node_data: &NodeData) { - const VECTOR_TYPE: &str = "Standard.Base.Data.Vector.Vector"; - let is_array_enabled = ARGS.groups.feature_preview.options.vector_editor.value; - let is_array_type = node_data.tp.as_ref().map_or(false, |tp| tp.contains(VECTOR_TYPE)); - let has_tag_values = !node_data.tag_values.is_empty(); - let kind_fallback = (is_array_enabled && is_array_type) - .then(|| Metadata::vector_editor()) - .or((meta.is_none() && has_tag_values) - .then(|| Metadata::static_dropdown(&node_data.tag_values))); - let meta = meta.as_ref().or(tag_value_meta.as_ref()).unwrap_or(&Metadata::FALLBACK); - - self.display_object.set_size(node_data.port_size); - - let ctx = ConfigContext { - app: &self.app, - display_object: &self.display_object, - frp, - label: meta.label.clone(), - display: meta.display, + /// Set the metadata for the given node data. + fn set_metadata(&self, pointer: MetadataPointer, meta: Option) { + // TODO: rebuild tree partially after batch updating metadata. + // For now, the tree will be rebuilt completely. + + use std::collections::hash_map::Entry; + let mut map = self.metadata_map.borrow_mut(); + let entry = map.entry(pointer); + let dirty = match (entry, meta) { + (Entry::Occupied(e), None) => { + e.remove(); + true + } + (Entry::Occupied(mut e), Some(meta)) if e.get() != &meta => { + e.insert(meta); + true + } + (Entry::Vacant(e), Some(meta)) => { + e.insert(meta); + true + } + _ => false, }; + self.metadata_dirty.store(dirty, Ordering::Release); + } - match self.inner_widget.borrow_mut().deref_mut() { - Some(inner_widget) => inner_widget.configure(&meta.config, ctx), - model @ None => *model = Some(DynWidget::new(&meta.config, ctx)), - } + #[profile(Task)] + fn rebuild_tree(&self, frp: SampledFrp, tree: &span_tree::SpanTree, node_expression: &str) { + self.metadata_dirty.store(false, Ordering::Release); + let app = self.app.clone(); + let metadata_map = self.metadata_map.borrow(); + let widgets_map = self.widgets_map.take(); + let mut builder = + WidgetTreeBuilder::new(node_expression, app, frp, &*metadata_map, widgets_map); + builder.child_widget(&self.display_object, tree.root_ref(), 0); + self.widgets_map.replace(builder.new_widgets); } } -impl Deref for View { - type Target = Frp; - fn deref(&self) -> &Self::Target { - self.frp() + +pub struct ConfigContext<'a, 'b> { + builder: &'a mut WidgetTreeBuilder<'b>, + parent_instance: &'a display::object::Instance, + label: Option, + display: Display, + span_tree_node: span_tree::node::Ref<'a>, + parent_parenthesized: bool, + depth: usize, +} + +impl<'a, 'b> ConfigContext<'a, 'b> { + fn app(&self) -> &Application { + &self.builder.app + } + + fn frp(&self) -> &SampledFrp { + &self.builder.frp + } + + fn expression_at(&self, range: text::Range) -> &str { + &self.builder.node_expression[range] } } -impl display::Object for View { - fn display_object(&self) -> &display::object::Instance { - &self.model.display_object +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct WidgetTreePointer { + /// The latest set ast::Id in the span tree. + id: Option, + /// Remaining crumbs to the widget, starting from the node with the latest set ast::Id. + crumbs: span_tree::Crumbs, +} + +struct WidgetTreeBuilder<'a> { + app: Application, + frp: SampledFrp, + metadata_map: &'a HashMap, + old_widgets: HashMap, + new_widgets: HashMap, + last_ast_id: Option, + last_ast_id_crumbs: span_tree::Crumbs, + node_expression: &'a str, + parenthesized: bool, +} + +impl<'a> WidgetTreeBuilder<'a> { + fn new( + node_expression: &'a str, + app: Application, + frp: SampledFrp, + metadata_map: &'a HashMap, + old_widgets: HashMap, + ) -> Self { + Self { + app, + frp, + metadata_map, + old_widgets, + new_widgets: default(), + last_ast_id: default(), + last_ast_id_crumbs: default(), + node_expression, + parenthesized: false, + } + } + + pub(self) fn child_widget( + &mut self, + parent_object: &display::object::Instance, + span_tree_node: span_tree::node::Ref<'_>, + depth: usize, + ) -> WidgetTreePointer { + // This call can recurse into itself within the widget configuration logic. We need to save + // the current layer's state, so it can be restored later after visiting the child node. + let mut ast_data_to_restore = None; + let parent_parenthesized = self.parenthesized; + self.parenthesized = span_tree_node.parenthesized; + + // Figure out the widget tree pointer for the current node. That pointer determines the + // widget identity, allowing it to maintain internal state. If the previous tree already + // contained a widget for this pointer, we have to reuse it. + let tree_ptr = match span_tree_node.ast_id { + Some(ast_id) => { + let prev_ast_id = self.last_ast_id.replace(ast_id); + let prev_crumbs = + std::mem::replace(&mut self.last_ast_id_crumbs, span_tree_node.crumbs.clone()); + ast_data_to_restore = Some((prev_ast_id, prev_crumbs)); + WidgetTreePointer { id: Some(ast_id), crumbs: default() } + } + None => { + let this_crumbs = &span_tree_node.crumbs; + // We should always be in a child node since last ast ID. Verify that. + let is_in_ast_subtree = this_crumbs.starts_with(&self.last_ast_id_crumbs); + assert!(is_in_ast_subtree, "Not in AST child node."); + let id = self.last_ast_id; + let crumbs_since_id = &this_crumbs[self.last_ast_id_crumbs.len()..]; + let crumbs = span_tree::Crumbs::new(crumbs_since_id.to_vec()); + WidgetTreePointer { id, crumbs } + } + }; + + let kind = &span_tree_node.kind; + let pointer_data = kind.call_id().zip(kind.argument_name()); + let meta_pointer = pointer_data.map(|(call_id, argument_name)| MetadataPointer { + call_id, + argument_name: argument_name.into(), + }); + let meta_received = meta_pointer.and_then(|ptr| self.metadata_map.get(&ptr)); + let meta_fallback = meta_received.is_none().then(|| Metadata::from_kind(kind)); + let meta = meta_received.or(meta_fallback.as_ref()).expect("Fallback must always exist."); + + let old_widget = self.old_widgets.remove(&tree_ptr); + let widget = { + let ctx = ConfigContext { + builder: &mut *self, + parent_instance: parent_object, + label: meta.label.clone(), + display: meta.display, + span_tree_node, + parent_parenthesized, + depth, + }; + // Widget creation/update can recurse into the builder. All borrows must be dropped + // at this point. + create_or_update_widget(old_widget, &meta.config, ctx) + }; + self.new_widgets.insert(tree_ptr.clone(), widget); + + // After visiting child node, restore previous layer's data. + if let Some((id, crumbs)) = ast_data_to_restore { + self.last_ast_id = id; + self.last_ast_id_crumbs = crumbs; + } + self.parenthesized = parent_parenthesized; + + tree_ptr } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs new file mode 100644 index 000000000000..0d3415df4662 --- /dev/null +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -0,0 +1,75 @@ +//! Definition of default hierarchy widget. This widget expands each child of its span tree into +//! a new widget. + +use crate::prelude::*; + +use ensogl::display::object::Instance; + +// ================= +// === Hierarchy === +// ================ + +#[derive(Debug, Clone, Copy, PartialEq, Default)] + +/// Label widget configuration options. +pub struct Config; + +/// Hierarchy widget. This widget expands each child of its span tree into a new widget. +#[derive(Clone, Debug)] +pub struct Widget { + display_object: Instance, + // shape: debug_shape::View, +} + +/// Width of a single space glyph +// TODO: avoid using hardcoded value. See https://www.pivotaltracker.com/story/show/183567623. +pub const SPACE_GLYPH_WIDTH: f32 = 7.224_609_4; + +impl super::SpanWidget for Widget { + type Config = Config; + fn new(config: &Config, ctx: super::ConfigContext) -> Self { + // TODO: add display object, so we can handle mouse events. + let display_object = ctx.parent_instance.new_child(); + display_object.use_auto_layout(); + // display_object.set_size_y(100.pc()); + display_object.set_children_alignment_left_center(); + // let shape = debug_shape::View::new(); + // display_object.add_child(&shape); + // shape.set_size((0, 1)); + + + let mut this = Self { display_object }; + this.configure(config, ctx); + this + } + + fn configure(&mut self, _config: &Config, ctx: super::ConfigContext) { + self.display_object.set_parent(ctx.parent_instance); + let offset = ctx.span_tree_node.sibling_offset.as_usize() as f32; + self.display_object.set_padding_left(offset * SPACE_GLYPH_WIDTH); + + let preserve_depth = + ctx.span_tree_node.is_chained() || ctx.span_tree_node.is_named_argument(); + let next_depth = if preserve_depth { ctx.depth } else { ctx.depth + 1 }; + + for child in ctx.span_tree_node.children_iter() { + ctx.builder.child_widget(&self.display_object, child, next_depth); + } + } +} + + +/// Temporary dropdown activation shape definition. +pub mod debug_shape { + use super::*; + ensogl::shape! { + above = [ + crate::component::node::background, + crate::component::node::input::port::hover + ]; + (style:Style) { + let color = Var::::from("srgba(1.0,0.0,0.0,0.1)"); + Rect(Var::canvas_size()).fill(color).into() + } + } +} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 79f2825e4122..16be5826a6b4 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -2,6 +2,10 @@ use crate::prelude::*; +use crate::component::node::input::area::TEXT_SIZE; +use ensogl_component::text; + + // ============= // === Label === // ============= @@ -9,16 +13,51 @@ use crate::prelude::*; #[derive(Debug, Clone, Copy, PartialEq, Default)] /// Label widget configuration options. -pub struct Config; +pub struct Config { + /// If true, the label will be displayed in a placeholder style. + pub placeholder: bool, +} -#[derive(Clone, Copy, Debug)] -pub struct Widget; +/// Label widget. Always displays the span tree node's expression as text. +#[derive(Clone, Debug)] +pub struct Widget { + label: text::Text, +} impl super::SpanWidget for Widget { type Config = Config; - fn new(config: &Config, ctx: super::ConfigContext<'_>) -> Self { - Self {} + fn new(config: &Config, ctx: super::ConfigContext) -> Self { + let label = text::Text::new(ctx.app()); + label.set_property_default(text::Size(TEXT_SIZE)); + let text_layer = &ctx.app().display.default_scene.layers.above_nodes_text; + text_layer.add(&label); + + // let label_display = label.display_object(); + // frp::new_network! { network + // text_size <- all(&label.width, &label.height); + // eval text_size((size) { + // label_display.set_size(*size); + // }); + // }; + + let mut this = Self { label }; + this.configure(config, ctx); + this } - fn configure(&mut self, config: &Config, ctx: super::ConfigContext<'_>) {} + fn configure(&mut self, config: &Config, ctx: super::ConfigContext) { + self.label.display_object().set_parent(ctx.parent_instance); + + let content = ctx.expression_at(ctx.span_tree_node.span()); + + // let text_color = self.styles.get_color(theme::graph_editor::node::text); + use ensogl::data::color; + let text_color = if config.placeholder { + color::Lcha::new(0.5, 0.0, 0.0, 1.0) + } else { + color::Lcha::new(0.0, 0.0, 0.0, 1.0) + }; + self.label.set_property_default(text_color); + self.label.set_content(content); + } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index ddfb07770920..3e8cdf2b9dca 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -2,23 +2,23 @@ use crate::prelude::*; -use crate::component::node::input::widget::Entry; - use enso_frp as frp; use ensogl::application::Application; use ensogl::data::color; use ensogl::display; use ensogl::display::object::event; use ensogl_component::drop_down::Dropdown; +use ensogl_component::text; - +use crate::component::node::input::area::TEXT_SIZE; +use crate::component::node::input::widget::Entry; /// ================= /// === Constants === /// ================= const ACTIVATION_SHAPE_COLOR: color::Lch = color::Lch::new(0.56708, 0.23249, 0.71372); -const ACTIVATION_SHAPE_Y_OFFSET: f32 = -5.0; +const ACTIVATION_SHAPE_Y_OFFSET: f32 = 15.0; const ACTIVATION_SHAPE_SIZE: Vector2 = Vector2(15.0, 11.0); /// Distance between the dropdown and the bottom of the port. const DROPDOWN_Y_OFFSET: f32 = 5.0; @@ -69,80 +69,104 @@ pub struct Config { #[derive(Debug)] pub struct Widget { #[allow(dead_code)] - network: frp::Network, - dropdown: Rc>, + network: frp::Network, + set_current_value: frp::Source>, + current_crumbs: frp::Any, + dropdown: Rc>, + display_object: display::object::Instance, + label: text::Text, /// temporary click handling - activation_shape: triangle::View, + _activation_shape: triangle::View, } impl super::SpanWidget for Widget { type Config = Config; - fn new(config: &Config, ctx: super::ConfigContext<'_>) -> Self { - let super::ConfigContext { app, display_object, frp, .. } = ctx; + fn new(config: &Config, ctx: super::ConfigContext) -> Self { + let app = ctx.app(); + let frp = ctx.frp(); + let display_object = display::object::Instance::new(); + display_object.use_auto_layout(); let activation_shape = triangle::View::new(); + let dot_color = color::Rgba::from(ACTIVATION_SHAPE_COLOR.with_alpha(1.0)).into(); + activation_shape.color.set(dot_color); activation_shape.set_size(ACTIVATION_SHAPE_SIZE); + activation_shape.set_alignment_center_x(); + activation_shape.set_y(-ACTIVATION_SHAPE_SIZE.y() - ACTIVATION_SHAPE_Y_OFFSET); + + let label = text::Text::new(app); + label.set_property_default(text::Size(TEXT_SIZE)); + display_object.add_child(&label); display_object.add_child(&activation_shape); frp::new_network! { network init <- source_(); + set_current_value <- source(); + value_changed <- any(...); + let focus_in = display_object.on_event::(); let focus_out = display_object.on_event::(); is_focused <- bool(&focus_out, &focus_in); - is_open <- frp.set_visible && is_focused; - is_open <- is_open.sampler(); + is_open <- is_focused.sampler(); + current_value_sampler <- set_current_value.sampler(); + current_crumbs <- any(...); + frp.out_value_changed <+ value_changed.map2(¤t_crumbs, + move |t: &Option, crumbs: &span_tree::Crumbs| (crumbs.clone(), t.clone()) + ); }; - let set_current_value = frp.set_current_value.clone_ref(); - let dropdown_output = frp.out_value_changed.clone_ref(); let request_import = frp.out_request_import.clone_ref(); let dropdown = LazyDropdown::new( app, - display_object, - set_current_value, + &display_object, + current_value_sampler, is_open, - dropdown_output, + value_changed, request_import, ); let dropdown = Rc::new(RefCell::new(dropdown)); frp::extend! { network let dot_clicked = activation_shape.events.mouse_down_primary.clone_ref(); - toggle_focus <- dot_clicked.map(f!([display_object](()) !display_object.is_focused())); - set_focused <- any(toggle_focus, frp.set_focused); + set_focused <- dot_clicked.map(f!([display_object](()) !display_object.is_focused())); eval set_focused([display_object](focus) match focus { true => display_object.focus(), false => display_object.blur(), }); - set_visible <- all(&frp.set_visible, &init)._0(); - shape_alpha <- set_visible.map(|visible| if *visible { 1.0 } else { 0.0 }); - shape_color <- shape_alpha.map(|a| ACTIVATION_SHAPE_COLOR.with_alpha(*a)); - eval shape_color([activation_shape] (color) { - activation_shape.color.set(color::Rgba::from(color).into()); - }); - eval focus_in((_) dropdown.borrow_mut().initialize_on_open()); } init.emit(()); - let mut this = Self { network, dropdown, activation_shape }; + let mut this = Self { + display_object, + network, + set_current_value, + current_crumbs, + dropdown, + _activation_shape: activation_shape, + label, + }; this.configure(config, ctx); this } - fn configure(&mut self, config: &Config, ctx: super::ConfigContext<'_>) { - // TODO: use auto layout to position the activation shape. - let port_size = ctx.display_object.size(); - let port_size_x = port_size.x().as_pixels().expect("Port size is fixed."); - let port_size_y = port_size.y().as_pixels().expect("Port size is fixed."); - self.activation_shape.set_x(port_size_x / 2.0); - self.activation_shape - .set_y(-port_size_y / 2.0 - ACTIVATION_SHAPE_SIZE.y() - ACTIVATION_SHAPE_Y_OFFSET); + fn configure(&mut self, config: &Config, ctx: super::ConfigContext) { + self.display_object.set_parent(ctx.parent_instance); + let mut dropdown = self.dropdown.borrow_mut(); - dropdown.set_port_size(Vector2(port_size_x, port_size_y)); + let content: ImString = ctx.expression_at(ctx.span_tree_node.span()).into(); dropdown.set_entries(config.entries.clone()); + self.set_current_value.emit(content.clone()); + self.label.set_content(content); + self.current_crumbs.emit(ctx.span_tree_node.crumbs.clone()); + + // Do not increment the depth. If the dropdown is displayed, it should also display + // its arguments. + for child in ctx.span_tree_node.children_iter() { + ctx.builder.child_widget(&self.display_object, child, ctx.depth); + } } } @@ -163,7 +187,6 @@ enum LazyDropdown { NotInitialized { app: Application, display_object: display::object::Instance, - dropdown_y: f32, entries: Rc>, set_current_value: frp::Sampler>, is_open: frp::Sampler, @@ -172,7 +195,7 @@ enum LazyDropdown { }, Initialized { _network: frp::Network, - dropdown: Dropdown, + _dropdown: Dropdown, set_entries: frp::Any>, }, } @@ -188,12 +211,10 @@ impl LazyDropdown { ) -> Self { let app = app.clone_ref(); let display_object = display_object.clone_ref(); - let dropdown_y = default(); let entries = default(); LazyDropdown::NotInitialized { app, display_object, - dropdown_y, entries, set_current_value, is_open, @@ -202,18 +223,6 @@ impl LazyDropdown { } } - fn set_port_size(&mut self, new_port_size: Vector2) { - let y = -new_port_size.y() - DROPDOWN_Y_OFFSET; - match self { - LazyDropdown::Initialized { dropdown, .. } => { - dropdown.set_y(y); - } - LazyDropdown::NotInitialized { dropdown_y, .. } => { - *dropdown_y = y; - } - } - } - fn set_entries(&mut self, new_entries: Rc>) { match self { LazyDropdown::Initialized { set_entries, .. } => { @@ -233,7 +242,6 @@ impl LazyDropdown { LazyDropdown::NotInitialized { app, display_object, - dropdown_y, entries, is_open, set_current_value, @@ -243,7 +251,7 @@ impl LazyDropdown { let dropdown = app.new_view::>(); display_object.add_child(&dropdown); app.display.default_scene.layers.above_nodes.add(&dropdown); - dropdown.set_y(*dropdown_y); + dropdown.set_y(20.0); dropdown.set_max_open_size(Vector2(300.0, 500.0)); dropdown.allow_deselect_all(true); @@ -283,7 +291,11 @@ impl LazyDropdown { let entries = std::mem::take(Rc::make_mut(entries)); set_entries.emit(entries); init.emit(()); - *self = LazyDropdown::Initialized { _network: network, dropdown, set_entries }; + *self = LazyDropdown::Initialized { + _network: network, + _dropdown: dropdown, + set_entries, + }; } } } diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index 2bb60206e77d..5aa449fa752e 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -2021,9 +2021,17 @@ impl InstanceDef { }) } - /// Replaces the parent binding with a new parent. - fn set_parent(&self, parent: &InstanceDef) { - parent.add_child(self); + /// Replaces the parent binding with a new parent. Does nothing if the provided parent is the + /// same as the current one. + pub fn set_parent(&self, parent: &InstanceDef) { + if !parent.is_parent_of(self) { + parent.add_child(self); + } + } + + /// Checks if the provided object is a parent of the current one. + fn is_parent_of(&self, child: &InstanceDef) -> bool { + child.parent_bind.parent_and_child_index().map_or(false, |(parent, _)| &parent.def == self) } /// Removes the current parent binding. diff --git a/lib/rust/ensogl/examples/auto-layout/Cargo.toml b/lib/rust/ensogl/examples/auto-layout/Cargo.toml index fc9a62c17de5..d21e4ad2bb17 100644 --- a/lib/rust/ensogl/examples/auto-layout/Cargo.toml +++ b/lib/rust/ensogl/examples/auto-layout/Cargo.toml @@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] ensogl-core = { path = "../../core" } +ensogl-text = { path = "../../component/text" } wasm-bindgen = { workspace = true } ensogl-hardcoded-theme = { path = "../../../ensogl/app/theme/hardcoded" } diff --git a/lib/rust/frp/src/nodes.rs b/lib/rust/frp/src/nodes.rs index 1d3fc45a8355..b23b57307a6b 100644 --- a/lib/rust/frp/src/nodes.rs +++ b/lib/rust/frp/src/nodes.rs @@ -1779,6 +1779,14 @@ impl OwnedSampler { pub fn value(&self) -> Out { self.value.borrow().clone() } + + /// Perform scoped operation on sampler value without cloning it. The internal value is borrowed + /// for the duration of the passed function scope. Setting the sampler value inside the + /// function scope will cause a panic. + pub fn with(&self, f: impl FnOnce(&Out) -> T) -> T { + let borrow = self.value.borrow(); + f(&*borrow) + } } impl Sampler { @@ -1786,6 +1794,15 @@ impl Sampler { pub fn value(&self) -> Out { self.upgrade().map(|t| t.value.borrow().clone()).unwrap_or_default() } + + /// Perform scoped operation on sampler value without cloning it. The internal value is borrowed + /// for the duration of the passed function scope. Setting the sampler value inside the + /// function scope will cause a panic. + /// + /// If the sampler is already dropped, the function will return `None`. + pub fn with(&self, f: impl FnOnce(&Out) -> T) -> Option { + self.upgrade().map(|t| t.with(f)) + } } impl stream::EventConsumer for OwnedSampler { From 94b3d0608351b4a324237e811f8f69b2a4cbc4b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Sun, 2 Apr 2023 11:45:01 +0200 Subject: [PATCH 03/45] proper alignment for widgets --- app/gui/language/span-tree/src/generate.rs | 3 + app/gui/language/span-tree/src/iter.rs | 4 + app/gui/language/span-tree/src/node.rs | 2 +- .../src/controller/graph/widget/metadata.rs | 13 +- .../src/controller/graph/widget/response.rs | 6 +- .../view/graph-editor/src/component/node.rs | 1 + .../src/component/node/input/area.rs | 43 +- .../src/component/node/input/widget.rs | 139 ++++--- .../src/component/node/input/widget/debug.rs | 50 +++ .../component/node/input/widget/hierarchy.rs | 20 +- .../src/component/node/input/widget/label.rs | 93 +++-- .../node/input/widget/single_choice.rs | 374 +++++++++--------- app/gui/view/graph-editor/src/lib.rs | 1 + build/ci_utils/src/fs.rs | 9 +- .../ensogl/core/src/control/io/mouse/event.rs | 35 ++ lib/rust/frp/src/nodes.rs | 133 +++++++ lib/rust/frp/src/stream.rs | 38 +- 17 files changed, 628 insertions(+), 336 deletions(-) create mode 100644 app/gui/view/graph-editor/src/component/node/input/widget/debug.rs diff --git a/app/gui/language/span-tree/src/generate.rs b/app/gui/language/span-tree/src/generate.rs index 37315970918a..27acced247c1 100644 --- a/app/gui/language/span-tree/src/generate.rs +++ b/app/gui/language/span-tree/src/generate.rs @@ -535,6 +535,9 @@ fn generate_node_for_prefix_chain( context: &impl Context, ) -> FallibleResult> { let app_base = ApplicationBase::from_prefix_chain(this); + + // If actual method arguments are not resolved, we still want to assign correct call ID to all + // argument spans. This is required for correct handling of let fallback_call_id = app_base.call_id; let mut application = app_base.resolve(context); diff --git a/app/gui/language/span-tree/src/iter.rs b/app/gui/language/span-tree/src/iter.rs index 1748b5ebbcb5..34ed63a99272 100644 --- a/app/gui/language/span-tree/src/iter.rs +++ b/app/gui/language/span-tree/src/iter.rs @@ -97,6 +97,10 @@ impl<'a, T> LeafIterator<'a, T> { TreeFragment::ChainAndDirectChildren => current_node.kind.is_chained(), } } + + pub fn into_base(self) -> node::Ref<'a, T> { + self.base_node + } } diff --git a/app/gui/language/span-tree/src/node.rs b/app/gui/language/span-tree/src/node.rs index 524bd809cfdf..1dd3bc0037c8 100644 --- a/app/gui/language/span-tree/src/node.rs +++ b/app/gui/language/span-tree/src/node.rs @@ -409,7 +409,7 @@ impl<'a, T> Ref<'a, T> { /// Iterator over all children of operator/prefix chain starting from this node. See crate's /// documentation for more information about _chaining_. - pub fn chain_children_iter(self) -> impl Iterator> { + pub fn chain_children_iter(self) -> LeafIterator<'a, T> { LeafIterator::new(self, TreeFragment::ChainAndDirectChildren) } diff --git a/app/gui/src/controller/graph/widget/metadata.rs b/app/gui/src/controller/graph/widget/metadata.rs index 3bdc28da650a..896e352624de 100644 --- a/app/gui/src/controller/graph/widget/metadata.rs +++ b/app/gui/src/controller/graph/widget/metadata.rs @@ -40,17 +40,16 @@ pub fn deserialize_widget_update( } fn map_metadata(resp: response::Widget) -> widget::Metadata { - widget::Metadata { - label: resp.label.map(Into::into), - display: resp.display, - config: map_config(resp.inner), - } + widget::Metadata { display: resp.display, config: map_config(resp.inner) } } fn map_config(inner: response::WidgetSpecific) -> widget::Config { match inner { - response::WidgetSpecific::SingleChoice { values } => - widget::single_choice::Config { entries: Rc::new(map_entries(&values)) }.into(), + response::WidgetSpecific::SingleChoice { label, values } => widget::single_choice::Config { + label: label.map(Into::into), + entries: Rc::new(map_entries(&values)), + } + .into(), _ => widget::label::Config::default().into(), } } diff --git a/app/gui/src/controller/graph/widget/response.rs b/app/gui/src/controller/graph/widget/response.rs index a4bbeb50a272..58dc0a587ec1 100644 --- a/app/gui/src/controller/graph/widget/response.rs +++ b/app/gui/src/controller/graph/widget/response.rs @@ -44,9 +44,6 @@ impl<'de: 'a, 'a> serde::Deserialize<'de> for FallableWidgetData<'a> { /// specified and deserialized. #[derive(Debug, serde::Deserialize)] pub(super) struct Widget<'a> { - /// The placeholder text value. By default, the parameter name is used. - #[serde(borrow, default)] - pub label: Option<&'a str>, /// The display mode for the parameter. #[serde(default)] pub display: widget::Display, @@ -61,6 +58,9 @@ pub(super) enum WidgetSpecific<'a> { /// Describes a single value widget (dropdown). #[serde(rename = "Single_Choice")] SingleChoice { + /// The placeholder text value. By default, the parameter name is used. + #[serde(borrow, default)] + label: Option<&'a str>, /// A list of choices to display. #[serde(borrow, default)] values: Vec>, diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index a1e096ff3ebb..b2b39fcdc62d 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -109,6 +109,7 @@ pub mod background { use super::*; ensogl::shape! { + pointer_events = false; alignment = center; (style:Style, bg_color:Vector4) { let bg_color = Var::::from(bg_color); diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 4f837b161584..6cbad7d35a16 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -1,7 +1,7 @@ //! Definition of the node input port component. -use crate::component::node::input::widget::MetadataPointer; use crate::prelude::*; +use enso_text::index::*; use ensogl::display::shape::*; use ensogl::display::traits::*; @@ -14,6 +14,7 @@ use crate::view; use crate::Type; use crate::WidgetUpdates; +use crate::component::node::input::widget::MetadataPointer; use enso_frp as frp; use enso_frp; use ensogl::application::Application; @@ -27,7 +28,6 @@ use ensogl_component::text::FromInContextSnapped; use ensogl_hardcoded_theme as theme; - // ================= // === Constants === // ================= @@ -177,7 +177,7 @@ impl Model { pub fn new(app: &Application) -> Self { let app = app.clone_ref(); let display_object = display::object::Instance::new(); - display_object.use_auto_layout(); + let edit_mode_label = app.new_view::(); let id_crumbs_map = default(); let expression = default(); @@ -201,47 +201,14 @@ impl Model { /// mode. Sets cursor position when entering edit mode. pub fn set_edit_mode(&self, edit_mode_active: bool) { if edit_mode_active { - // When transitioning to edit mode, we need to find the code location that corresponds - // to the widget at mouse position. First we search for the widget at that position, - // then find the right character index within that port. - let expression = self.expression.borrow(); - // let clicked_label_location = self.ports_label.location_at_mouse_position(); - // let clicked_char_index = - // expression.viz_code.char_indices().nth(clicked_label_location.offset.into()); - // let location_to_set = clicked_char_index.and_then(|char_index| { - // let loc_offset = char_index.0.byte().to_diff(); - // let clicked_port = expression.span_tree.root_ref().leaf_iter().find(|node| { - // let range = node.payload.range(); - // range.contains(&loc_offset) - // })?; - - // let byte_offset_within_port = loc_offset - clicked_port.payload.index; - // let byte_offset_within_port = byte_offset_within_port.min(clicked_port.size); - // let final_code_byte_offset = clicked_port.span_offset + byte_offset_within_port; - - // let final_code_column: Column = - // expression.code[..final_code_byte_offset.into()].chars().count().into(); - // let final_code_location = clicked_label_location.with_offset(final_code_column); - // Some(final_code_location) - // }); - self.edit_mode_label.set_content(expression.code.clone()); self.display_object.remove_child(&self.root_widget); self.display_object.add_child(&self.edit_mode_label); - // if let Some(location) = location_to_set { - // self.edit_mode_label.set_cursor(location); - // } else { - // If we were unable to find a port under current mouse position, set the edit label - // cursor at the mouse position immediately after setting its content to the raw - // expression code. self.edit_mode_label.set_cursor_at_mouse_position(); - // } } else { self.display_object.remove_child(&self.edit_mode_label); self.display_object.add_child(&self.root_widget); - // When we exit the edit mode, clear the label. That way we don't have any extra glyphs - // to process during rendering in non-edit mode. self.edit_mode_label.set_content(""); } self.edit_mode_label.deprecated_set_focus(edit_mode_active); @@ -967,7 +934,9 @@ impl Area { root_widget_width <- layout_refresh.map(f!([model](_) { let instance = model.root_widget.display_object(); instance.computed_size().x() - })); + })).on_change(); + + trace root_widget_width; padded_edit_label_width <- model.edit_mode_label.width.map(|t| t + 2.0 * TEXT_OFFSET); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index cd3ce2d6ed80..e57e4f250bdc 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -1,5 +1,7 @@ //! Definition of all hardcoded node widget variants and common widget FRP API. +mod debug; + use crate::prelude::*; use crate::component::node::input::area::NODE_HEIGHT; @@ -38,7 +40,9 @@ ensogl::define_endpoints_2! { /// widget to reconfigure. #[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] pub struct MetadataPointer { + /// The function call associated with the widget. pub call_id: ast::Id, + /// The name of function argument at which the widget is located. pub argument_name: ImString, } @@ -97,13 +101,13 @@ macro_rules! define_widget_modules( } $( - impl From<<$module::Widget as SpanWidget>::Config> for Config { + impl const From<<$module::Widget as SpanWidget>::Config> for Config { fn from(config: <$module::Widget as SpanWidget>::Config) -> Self { Self::$name(config) } } - impl From<$module::Widget> for DynWidget { + impl const From<$module::Widget> for DynWidget { fn from(config: $module::Widget) -> Self { Self::$name(config) } @@ -145,12 +149,6 @@ define_widget_modules! { Hierarchy hierarchy, } -impl Config { - const RAW_EXPRESSION: Self = Config::Label(label::Config { placeholder: false }); - const ARG_PLACEHOLDER: Self = Config::Label(label::Config { placeholder: true }); - const HIERARCHY: Self = Config::Hierarchy(hierarchy::Config); -} - /// ================ /// === Metadata === /// ================ @@ -161,21 +159,21 @@ impl Config { #[allow(missing_docs)] pub struct Metadata { /// The placeholder text value. By default, the parameter name is used. - pub label: Option, pub display: Display, pub config: Config, } impl Metadata { - const RAW_EXPRESSION: Self = - Metadata { label: None, display: Display::Always, config: Config::RAW_EXPRESSION }; + const fn always(config: C) -> Self + where C: ~const Into { + Self { display: Display::Always, config: config.into() } + } /// Widget metadata for static dropdown, based on the tag values provided by suggestion /// database. fn static_dropdown(label: Option, tag_values: &[span_tree::TagValue]) -> Metadata { let entries = Rc::new(tag_values.into_iter().map(Entry::from).collect()); - let config = single_choice::Config { entries }.into(); - Self { label, display: Display::Always, config } + Self::always(single_choice::Config { label, entries }) } fn vector_editor() -> Metadata { @@ -200,15 +198,10 @@ impl Metadata { if !arg.tag_values.is_empty() { Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values) } else { - Metadata { - label: arg.name.as_ref().map(Into::into), - display: Display::Always, - config: Config::ARG_PLACEHOLDER, - } + Self::always(label::Config::default()) }, - Kind::Root | Kind::NamedArgument | Kind::Chained(_) => - Metadata { label: None, display: Display::Always, config: Config::HIERARCHY }, - _ => Self::RAW_EXPRESSION, + Kind::Root | Kind::NamedArgument | Kind::Chained(_) => Self::always(hierarchy::Config), + _ => Self::always(label::Config::default()), } } } @@ -285,8 +278,8 @@ impl DropdownValue for Entry { /// after the endpoints were set, it would not receive previously set endpoint values. #[derive(Debug, Clone, CloneRef)] struct SampledFrp { - out_value_changed: frp::Any<(span_tree::Crumbs, Option)>, - out_request_import: frp::Any, + value_changed: frp::Any<(span_tree::Crumbs, Option)>, + request_import: frp::Any, } @@ -307,7 +300,7 @@ pub struct Root { impl display::Object for Root { fn display_object(&self) -> &display::object::Instance { - &self.model.display_object + &self.model.display_object.outer } } @@ -326,9 +319,9 @@ impl Root { }); } - let out_value_changed = frp.private.output.value_changed.clone_ref(); - let out_request_import = frp.private.output.request_import.clone_ref(); - let sampled_frp = SampledFrp { out_value_changed, out_request_import }; + let value_changed = frp.private.output.value_changed.clone_ref(); + let request_import = frp.private.output.request_import.clone_ref(); + let sampled_frp = SampledFrp { value_changed, request_import }; Self { frp, sampled_frp, model } } @@ -355,24 +348,40 @@ impl Root { #[derive(Debug)] struct RootModel { app: Application, - display_object: display::object::Instance, + display_object: debug::InstanceWithBg, widgets_map: RefCell>, metadata_map: Rc>>, metadata_dirty: AtomicBool, } +mod rectangle { + use super::*; + ensogl::shape! { + alignment = left_bottom; + (style: Style, color: Vector4) { + let color = Var::::from(color); + let width = Var::::from("input_size.x"); + let height = Var::::from("input_size.y"); + let rect = Rect((&width, &height)).corners_radius(5.0.px()); + let shape = rect.fill(color); + shape.into() + } + } +} + + impl RootModel { /// Create a new node widget, selecting the appropriate widget type based on the provided /// argument info. fn new(app: &Application) -> Self { let app = app.clone_ref(); - let display_object = display::object::Instance::new(); - display_object.use_auto_layout(); - - display_object.set_size_y(NODE_HEIGHT); - display_object.set_padding_left(TEXT_OFFSET); - display_object.set_padding_right(TEXT_OFFSET); + let display_object = debug::InstanceWithBg::gray(); + display_object.inner.use_auto_layout(); + display_object.inner.set_children_alignment_left_center().justify_content_center_y(); + display_object.inner.set_size_y(NODE_HEIGHT); + display_object.inner.set_padding_left(TEXT_OFFSET); + display_object.inner.set_padding_right(TEXT_OFFSET); let widgets_map = default(); let metadata_map = default(); @@ -414,16 +423,16 @@ impl RootModel { let widgets_map = self.widgets_map.take(); let mut builder = WidgetTreeBuilder::new(node_expression, app, frp, &*metadata_map, widgets_map); - builder.child_widget(&self.display_object, tree.root_ref(), 0); + builder.child_widget(&self.display_object.inner, tree.root_ref(), 0); self.widgets_map.replace(builder.new_widgets); } } +#[derive(Debug)] pub struct ConfigContext<'a, 'b> { builder: &'a mut WidgetTreeBuilder<'b>, parent_instance: &'a display::object::Instance, - label: Option, display: Display, span_tree_node: span_tree::node::Ref<'a>, parent_parenthesized: bool, @@ -447,11 +456,15 @@ impl<'a, 'b> ConfigContext<'a, 'b> { #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct WidgetTreePointer { /// The latest set ast::Id in the span tree. - id: Option, + id: Option, /// Remaining crumbs to the widget, starting from the node with the latest set ast::Id. - crumbs: span_tree::Crumbs, + crumbs: span_tree::Crumbs, + /// The optional additional identifier of the widget that is attached to the same node as its + /// parent. Used to distinguish between multiple widgets attached to the same node. + nested_key: Option, } +#[derive(Debug)] struct WidgetTreeBuilder<'a> { app: Application, frp: SampledFrp, @@ -490,6 +503,29 @@ impl<'a> WidgetTreeBuilder<'a> { parent_object: &display::object::Instance, span_tree_node: span_tree::node::Ref<'_>, depth: usize, + ) -> WidgetTreePointer { + self.create_child_widget(parent_object, span_tree_node, depth, None, None) + } + + pub(self) fn child_widget_of_type( + &mut self, + parent_object: &display::object::Instance, + span_tree_node: span_tree::node::Ref<'_>, + depth: usize, + meta: Metadata, + key: usize, + ) -> WidgetTreePointer { + self.create_child_widget(parent_object, span_tree_node, depth, Some(meta), Some(key)) + } + + + fn create_child_widget( + &mut self, + parent_object: &display::object::Instance, + span_tree_node: span_tree::node::Ref<'_>, + depth: usize, + set_metadata: Option, + nested_key: Option, ) -> WidgetTreePointer { // This call can recurse into itself within the widget configuration logic. We need to save // the current layer's state, so it can be restored later after visiting the child node. @@ -506,7 +542,7 @@ impl<'a> WidgetTreeBuilder<'a> { let prev_crumbs = std::mem::replace(&mut self.last_ast_id_crumbs, span_tree_node.crumbs.clone()); ast_data_to_restore = Some((prev_ast_id, prev_crumbs)); - WidgetTreePointer { id: Some(ast_id), crumbs: default() } + WidgetTreePointer { id: Some(ast_id), crumbs: default(), nested_key } } None => { let this_crumbs = &span_tree_node.crumbs; @@ -516,26 +552,31 @@ impl<'a> WidgetTreeBuilder<'a> { let id = self.last_ast_id; let crumbs_since_id = &this_crumbs[self.last_ast_id_crumbs.len()..]; let crumbs = span_tree::Crumbs::new(crumbs_since_id.to_vec()); - WidgetTreePointer { id, crumbs } + WidgetTreePointer { id, crumbs, nested_key } } }; + + let mut meta_fallback = None; let kind = &span_tree_node.kind; - let pointer_data = kind.call_id().zip(kind.argument_name()); - let meta_pointer = pointer_data.map(|(call_id, argument_name)| MetadataPointer { - call_id, - argument_name: argument_name.into(), - }); - let meta_received = meta_pointer.and_then(|ptr| self.metadata_map.get(&ptr)); - let meta_fallback = meta_received.is_none().then(|| Metadata::from_kind(kind)); - let meta = meta_received.or(meta_fallback.as_ref()).expect("Fallback must always exist."); + let meta = set_metadata + .as_ref() + .or_else(|| { + let pointer_data = kind.call_id().zip(kind.argument_name()); + let meta_pointer = pointer_data.map(|(call_id, argument_name)| MetadataPointer { + call_id, + argument_name: argument_name.into(), + }); + + meta_pointer.and_then(|ptr| self.metadata_map.get(&ptr)) + }) + .unwrap_or_else(|| meta_fallback.get_or_insert_with(|| Metadata::from_kind(kind))); let old_widget = self.old_widgets.remove(&tree_ptr); let widget = { let ctx = ConfigContext { builder: &mut *self, parent_instance: parent_object, - label: meta.label.clone(), display: meta.display, span_tree_node, parent_parenthesized, diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs b/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs new file mode 100644 index 000000000000..43b517d1a5c8 --- /dev/null +++ b/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs @@ -0,0 +1,50 @@ +use crate::prelude::*; +use ensogl::data::color; +use ensogl::display; + +mod shape { + ensogl::shape! { + above = [ + crate::component::node::background, + crate::component::node::drag_area + ]; + pointer_events = false; + (style: Style, color: Vector4) { + let color = Var::::from(color); + let shape = Rect(Var::canvas_size()).fill(color); + shape.into() + } + } +} + +#[derive(Clone, Debug)] +pub struct InstanceWithBg { + pub bg: shape::View, + pub outer: display::object::Instance, + pub inner: display::object::Instance, +} + +impl InstanceWithBg { + pub fn magenta() -> Self { + Self::with_color(color::Rgba::new(0.5, 0.0, 0.5, 0.15)) + } + + pub fn olive() -> Self { + Self::with_color(color::Rgba::new(0.5, 0.8, 0.0, 0.2)) + } + + pub fn gray() -> Self { + Self::with_color(color::Rgba::new(0.5, 0.5, 0.5, 0.2)) + } + + pub fn with_color(color: color::Rgba) -> Self { + let bg = shape::View::new(); + let outer = bg.display_object().clone(); + let inner = display::object::Instance::new(); + // bg.allow_grow(); + bg.color.set(color.into()); + // outer.add_child(&bg); + outer.add_child(&inner); + Self { bg, outer, inner } + } +} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs index 0d3415df4662..6d418d91567e 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -17,7 +17,7 @@ pub struct Config; /// Hierarchy widget. This widget expands each child of its span tree into a new widget. #[derive(Clone, Debug)] pub struct Widget { - display_object: Instance, + display_object: super::debug::InstanceWithBg, // shape: debug_shape::View, } @@ -28,15 +28,9 @@ pub const SPACE_GLYPH_WIDTH: f32 = 7.224_609_4; impl super::SpanWidget for Widget { type Config = Config; fn new(config: &Config, ctx: super::ConfigContext) -> Self { - // TODO: add display object, so we can handle mouse events. - let display_object = ctx.parent_instance.new_child(); - display_object.use_auto_layout(); - // display_object.set_size_y(100.pc()); - display_object.set_children_alignment_left_center(); - // let shape = debug_shape::View::new(); - // display_object.add_child(&shape); - // shape.set_size((0, 1)); - + let display_object = super::debug::InstanceWithBg::olive(); + display_object.inner.use_auto_layout(); + display_object.inner.set_children_alignment_left_center().justify_content_center_y(); let mut this = Self { display_object }; this.configure(config, ctx); @@ -44,16 +38,16 @@ impl super::SpanWidget for Widget { } fn configure(&mut self, _config: &Config, ctx: super::ConfigContext) { - self.display_object.set_parent(ctx.parent_instance); + self.display_object.outer.set_parent(ctx.parent_instance); let offset = ctx.span_tree_node.sibling_offset.as_usize() as f32; - self.display_object.set_padding_left(offset * SPACE_GLYPH_WIDTH); + self.display_object.inner.set_padding_left(offset * SPACE_GLYPH_WIDTH); let preserve_depth = ctx.span_tree_node.is_chained() || ctx.span_tree_node.is_named_argument(); let next_depth = if preserve_depth { ctx.depth } else { ctx.depth + 1 }; for child in ctx.span_tree_node.children_iter() { - ctx.builder.child_widget(&self.display_object, child, next_depth); + ctx.builder.child_widget(&self.display_object.inner, child, next_depth); } } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 16be5826a6b4..7bf9cbf88e86 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -2,62 +2,109 @@ use crate::prelude::*; +use super::debug::InstanceWithBg; use crate::component::node::input::area::TEXT_SIZE; +use ensogl::data::color; use ensogl_component::text; - // ============= // === Label === // ============= -#[derive(Debug, Clone, Copy, PartialEq, Default)] - /// Label widget configuration options. +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Config { - /// If true, the label will be displayed in a placeholder style. - pub placeholder: bool, + pub ignore_offset: bool, + pub bold: bool, +} + +impl Config { + /// Create a new label widget configuration. + pub const fn expression() -> Self { + Self { ignore_offset: false, bold: false } + } +} + +ensogl::define_endpoints_2! { + Input { + content(ImString), + text_color(color::Lcha), + text_weight(text::Weight), + } } /// Label widget. Always displays the span tree node's expression as text. #[derive(Clone, Debug)] pub struct Widget { + frp: Frp, + root: InstanceWithBg, label: text::Text, } impl super::SpanWidget for Widget { type Config = Config; fn new(config: &Config, ctx: super::ConfigContext) -> Self { + // Embed the label in a vertically centered fixed height container, so that the label's + // baseline is aligned with other labels in the same row. + + let layers = &ctx.app().display.default_scene.layers; + let root = InstanceWithBg::magenta(); + root.inner.set_size_y(TEXT_SIZE); let label = text::Text::new(ctx.app()); label.set_property_default(text::Size(TEXT_SIZE)); - let text_layer = &ctx.app().display.default_scene.layers.above_nodes_text; - text_layer.add(&label); - - // let label_display = label.display_object(); - // frp::new_network! { network - // text_size <- all(&label.width, &label.height); - // eval text_size((size) { - // label_display.set_size(*size); - // }); - // }; - - let mut this = Self { label }; + label.set_y(TEXT_SIZE); + layers.above_nodes_text.add(&label); + root.inner.add_child(&label); + let frp = Frp::new(); + let network = &frp.network; + + frp::extend! { network + content_change <- frp.content.on_change(); + color_change <- frp.text_color.on_change(); + weight_change <- frp.text_weight.on_change(); + eval content_change((content) label.set_content(content)); + eval color_change((color) label.set_property_default(color)); + eval weight_change((weight) label.set_property_default(weight)); + } + + let mut this = Self { frp, root, label }; this.configure(config, ctx); this } fn configure(&mut self, config: &Config, ctx: super::ConfigContext) { - self.label.display_object().set_parent(ctx.parent_instance); + self.root.outer.set_parent(ctx.parent_instance); + let is_placeholder = ctx.span_tree_node.is_expected_argument(); + + let offset = if config.ignore_offset { + 0.0 + } else { + let min_offset = if is_placeholder { 1.0f32 } else { 0.0 }; + min_offset.max(ctx.span_tree_node.sibling_offset.as_usize() as f32) + }; + + self.label.set_x(offset * super::hierarchy::SPACE_GLYPH_WIDTH); + + + let content = if is_placeholder { + ctx.span_tree_node.kind.argument_name().unwrap_or_default() + } else { + ctx.expression_at(ctx.span_tree_node.span()) + }; - let content = ctx.expression_at(ctx.span_tree_node.span()); // let text_color = self.styles.get_color(theme::graph_editor::node::text); - use ensogl::data::color; - let text_color = if config.placeholder { + let text_color = if is_placeholder { color::Lcha::new(0.5, 0.0, 0.0, 1.0) } else { color::Lcha::new(0.0, 0.0, 0.0, 1.0) }; - self.label.set_property_default(text_color); - self.label.set_content(content); + + let weight = if config.bold { text::Weight::Bold } else { text::Weight::Normal }; + + let input = &self.frp.public.input; + input.content.emit(content.clone()); + input.text_color.emit(text_color); + input.text_weight.emit(weight); } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 3e8cdf2b9dca..a05dc978d63c 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -3,15 +3,15 @@ use crate::prelude::*; use enso_frp as frp; -use ensogl::application::Application; +use ensogl::control::io::mouse; use ensogl::data::color; use ensogl::display; use ensogl::display::object::event; use ensogl_component::drop_down::Dropdown; -use ensogl_component::text; -use crate::component::node::input::area::TEXT_SIZE; + use crate::component::node::input::widget::Entry; +use crate::component::node::input::widget::Metadata; /// ================= /// === Constants === @@ -23,7 +23,8 @@ const ACTIVATION_SHAPE_SIZE: Vector2 = Vector2(15.0, 11.0); /// Distance between the dropdown and the bottom of the port. const DROPDOWN_Y_OFFSET: f32 = 5.0; - +const LABEL_CONFIG: Metadata = + Metadata::always(super::label::Config { ignore_offset: true, bold: true }); // ====================== // === Triangle Shape === @@ -35,8 +36,10 @@ pub mod triangle { ensogl::shape! { above = [ crate::component::node::background, + crate::component::node::drag_area, crate::component::node::input::port::hover ]; + alignment = left_bottom; (style:Style, color:Vector4) { let size = Var::canvas_size(); let radius = 1.0.px(); @@ -58,95 +61,166 @@ pub mod triangle { /// SingleChoice widget configuration options. #[derive(Debug, Clone, PartialEq)] pub struct Config { + /// Default label to display when no value is selected. Will use argument name if not provided. + pub label: Option, /// Entries that should be displayed by the widget, as proposed by language server. This /// list is not exhaustive. The widget implementation might present additional /// options or allow arbitrary user input. pub entries: Rc>, } +ensogl::define_endpoints_2! { + Input { + set_entries(Rc>), + content(ImString), + current_value(Option), + current_crumbs(span_tree::Crumbs), + is_open(bool), + } +} + /// A widget for selecting a single value from a list of available options. The options can be /// provided as a static list of strings from argument `tag_values`, or as a dynamic expression. #[derive(Debug)] pub struct Widget { + config_frp: Frp, + display_object: display::object::Instance, + #[allow(dead_code)] + content_wrapper: display::object::Instance, + #[allow(dead_code)] + dropdown_wrapper: display::object::Instance, #[allow(dead_code)] - network: frp::Network, - set_current_value: frp::Source>, - current_crumbs: frp::Any, - dropdown: Rc>, - display_object: display::object::Instance, - label: text::Text, - /// temporary click handling - _activation_shape: triangle::View, + dropdown: Dropdown, + label_wrapper: display::object::Instance, + #[allow(dead_code)] + activation_shape: triangle::View, } impl super::SpanWidget for Widget { type Config = Config; fn new(config: &Config, ctx: super::ConfigContext) -> Self { let app = ctx.app(); - let frp = ctx.frp(); - let display_object = display::object::Instance::new(); - display_object.use_auto_layout(); - let activation_shape = triangle::View::new(); + + // ╭─display_object───────────────╮ + // │╭─content_wrapper────────────╮│ + // ││╭ shape ╮ ╭─label_wrapper──╮││ + // │││ │ │ │││ + // │││ │ │ │││ + // ││╰───────╯ ╰────────────────╯││ + // │╰────────────────────────────╯│ + // ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ + // │ ◎ dropdown_wrapper size=0 │ + // ╰──────────────────────────────╯ + let dot_color = color::Rgba::from(ACTIVATION_SHAPE_COLOR.with_alpha(1.0)).into(); + let activation_shape = triangle::View::new(); activation_shape.color.set(dot_color); activation_shape.set_size(ACTIVATION_SHAPE_SIZE); - activation_shape.set_alignment_center_x(); - activation_shape.set_y(-ACTIVATION_SHAPE_SIZE.y() - ACTIVATION_SHAPE_Y_OFFSET); - - let label = text::Text::new(app); - label.set_property_default(text::Size(TEXT_SIZE)); - display_object.add_child(&label); - display_object.add_child(&activation_shape); - - frp::new_network! { network - init <- source_(); - set_current_value <- source(); - value_changed <- any(...); - - let focus_in = display_object.on_event::(); - let focus_out = display_object.on_event::(); - is_focused <- bool(&focus_out, &focus_in); - is_open <- is_focused.sampler(); - current_value_sampler <- set_current_value.sampler(); - current_crumbs <- any(...); - frp.out_value_changed <+ value_changed.map2(¤t_crumbs, - move |t: &Option, crumbs: &span_tree::Crumbs| (crumbs.clone(), t.clone()) - ); - }; + // activation_shape.set_y(-ACTIVATION_SHAPE_SIZE.y() - ACTIVATION_SHAPE_Y_OFFSET); - let request_import = frp.out_request_import.clone_ref(); - let dropdown = LazyDropdown::new( - app, - &display_object, - current_value_sampler, - is_open, - value_changed, - request_import, - ); - let dropdown = Rc::new(RefCell::new(dropdown)); + + let display_object = display::object::Instance::new(); + display_object + .use_auto_layout() + .set_column_flow() + .set_children_alignment_left_center() + .justify_content_center_y(); + + let content_wrapper = display_object.new_child(); + content_wrapper + .use_auto_layout() + .set_children_alignment_left_center() + .justify_content_center_y(); + content_wrapper.add_child(&activation_shape); + let label_wrapper = content_wrapper.new_child(); + label_wrapper + .use_auto_layout() + .set_children_alignment_left_center() + .justify_content_center_y(); + + let dropdown_wrapper = display_object.new_child(); + dropdown_wrapper.set_size((0.0, 0.0)).set_alignment_left_top(); + + let dropdown = app.new_view::>(); + dropdown_wrapper.add_child(&dropdown); + let layers = &ctx.app().display.default_scene.layers; + layers.above_nodes.add(&dropdown); + dropdown.set_y(-20.0); + dropdown.set_max_open_size(Vector2(300.0, 500.0)); + dropdown.allow_deselect_all(true); + + + let config_frp = Frp::new(); + let network = &config_frp.network; + let input = &config_frp.private.input; + let widgets_frp = ctx.frp(); + + let focus_receiver = display_object.clone_ref(); frp::extend! { network - let dot_clicked = activation_shape.events.mouse_down_primary.clone_ref(); - set_focused <- dot_clicked.map(f!([display_object](()) !display_object.is_focused())); - eval set_focused([display_object](focus) match focus { - true => display_object.focus(), - false => display_object.blur(), + let focus_in = focus_receiver.on_event::(); + let focus_out = focus_receiver.on_event::(); + is_open <- bool(&focus_out, &focus_in); + } + frp::extend! { network + + let dot_mouse_down = activation_shape.on_event::(); + dot_clicked <- dot_mouse_down.filter(mouse::is_primary); + set_focused <- dot_clicked.map(f!([focus_receiver](_) !focus_receiver.is_focused())); + eval set_focused([focus_receiver](focus) match focus { + true => focus_receiver.focus(), + false => focus_receiver.blur(), }); + } + frp::extend! { network + + // Close the dropdown after a short delay after selection. Because the dropdown + // value application triggers operations that can introduce a few dropped frames, + // we want to delay the dropdown closing animation after that is handled. + // Otherwise the animation finishes within single frame, which looks bad. + let close_after_selection_timer = frp::io::timer::Timeout::new(&network); + close_after_selection_timer.restart <+ dropdown.user_select_action.constant(1); + eval close_after_selection_timer.on_expired((()) focus_receiver.blur()); + } + frp::extend! { network + entries_and_value <- all(&input.set_entries, &input.current_value); + entries_and_value <- entries_and_value.debounce(); + entries_and_value <- entries_and_value.buffered_gate(&is_open); + } + frp::extend! { network + dropdown.set_all_entries <+ entries_and_value.map(|(rc, _)| rc.deref().clone()); + dropdown.set_open <+ is_open.on_change(); + } + frp::extend! { network - eval focus_in((_) dropdown.borrow_mut().initialize_on_open()); + selected_entry <- entries_and_value.map(|(e, v)| entry_for_current_value(e, v)); + dropdown.set_selected_entries <+ selected_entry.map(|e| e.iter().cloned().collect()); + + dropdown_entry <- dropdown.selected_entries.map(|e| e.iter().next().cloned()); + // Emit the output value only after actual user action. This prevents the + // dropdown from emitting its initial value when it is opened, which can + // represent slightly different version of code than actually written. + submitted_entry <- dropdown_entry.sample(&dropdown.user_select_action); + dropdown_out_value <- submitted_entry.map(|e| e.as_ref().map(Entry::value)); + dropdown_out_import <- submitted_entry.map(|e| e.as_ref().and_then(Entry::required_import)); } + frp::extend! { network - init.emit(()); + widgets_frp.request_import <+ dropdown_out_import.unwrap(); + widgets_frp.value_changed <+ dropdown_out_value.map2(&input.current_crumbs, + move |t: &Option, crumbs: &span_tree::Crumbs| (crumbs.clone(), t.clone()) + ); + }; let mut this = Self { + config_frp, display_object, - network, - set_current_value, - current_crumbs, + content_wrapper, + dropdown_wrapper, dropdown, - _activation_shape: activation_shape, - label, + activation_shape, + label_wrapper, }; this.configure(config, ctx); this @@ -154,153 +228,59 @@ impl super::SpanWidget for Widget { fn configure(&mut self, config: &Config, ctx: super::ConfigContext) { self.display_object.set_parent(ctx.parent_instance); - - let mut dropdown = self.dropdown.borrow_mut(); - let content: ImString = ctx.expression_at(ctx.span_tree_node.span()).into(); - dropdown.set_entries(config.entries.clone()); - self.set_current_value.emit(content.clone()); - self.label.set_content(content); - self.current_crumbs.emit(ctx.span_tree_node.crumbs.clone()); + let input = &self.config_frp.public.input; + + let is_placeholder = ctx.span_tree_node.is_expected_argument(); + let min_offset = if is_placeholder { 1.0f32 } else { 0.0 }; + let offset = min_offset.max(ctx.span_tree_node.sibling_offset.as_usize() as f32); + self.display_object.set_margin_left(offset * super::hierarchy::SPACE_GLYPH_WIDTH); + + let has_value = !ctx.span_tree_node.is_insertion_point(); + let current_value: Option = + has_value.then(|| ctx.expression_at(ctx.span_tree_node.span()).into()); + let content = current_value + .clone() + .or_else(|| config.label.clone()) + .or_else(|| ctx.span_tree_node.kind.argument_name().map(Into::into)) + .unwrap_or_default(); + + input.current_crumbs(ctx.span_tree_node.crumbs.clone()); + input.current_value(current_value); + input.content(content); + input.set_entries(config.entries.clone()); // Do not increment the depth. If the dropdown is displayed, it should also display // its arguments. - for child in ctx.span_tree_node.children_iter() { - ctx.builder.child_widget(&self.display_object, child, ctx.depth); - } - } -} - -// ==================== -// === LazyDropdown === -// ==================== + let mut chain_children = ctx.span_tree_node.chain_children_iter(); -/// A lazy dropdown that is only initialized when it is opened for the first time. This prevents -/// very long initialization time, as dropdown view creation is currently a very slow process. -/// -/// FIXME [PG]: Improve grid-view creation performance, so that this is no longer needed. -/// https://www.pivotaltracker.com/story/show/184223891 -/// -/// Once grid-view creation is reasonably fast, this might be replaced by direct dropdown -/// initialization on widget creation. -#[derive(Debug)] -enum LazyDropdown { - NotInitialized { - app: Application, - display_object: display::object::Instance, - entries: Rc>, - set_current_value: frp::Sampler>, - is_open: frp::Sampler, - output_value: frp::Any>, - request_import: frp::Any, - }, - Initialized { - _network: frp::Network, - _dropdown: Dropdown, - set_entries: frp::Any>, - }, -} -impl LazyDropdown { - fn new( - app: &Application, - display_object: &display::object::Instance, - set_current_value: frp::Sampler>, - is_open: frp::Sampler, - output_value: frp::Any>, - request_import: frp::Any, - ) -> Self { - let app = app.clone_ref(); - let display_object = display_object.clone_ref(); - let entries = default(); - LazyDropdown::NotInitialized { - app, - display_object, - entries, - set_current_value, - is_open, - output_value, - request_import, - } - } - - fn set_entries(&mut self, new_entries: Rc>) { - match self { - LazyDropdown::Initialized { set_entries, .. } => { - let new_entries = Rc::try_unwrap(new_entries).unwrap_or_else(|rc| (*rc).clone()); - set_entries.emit(new_entries); - } - LazyDropdown::NotInitialized { entries, .. } => { - *entries = new_entries; - } - } - } + if let Some(first_child) = chain_children.next() { + ctx.builder.child_widget_of_type( + &self.label_wrapper, + first_child, + ctx.depth, + LABEL_CONFIG, + 0, + ); - #[profile(Detail)] - fn initialize_on_open(&mut self) { - match self { - LazyDropdown::Initialized { .. } => {} - LazyDropdown::NotInitialized { - app, - display_object, - entries, - is_open, - set_current_value, - output_value, - request_import, - } => { - let dropdown = app.new_view::>(); - display_object.add_child(&dropdown); - app.display.default_scene.layers.above_nodes.add(&dropdown); - dropdown.set_y(20.0); - dropdown.set_max_open_size(Vector2(300.0, 500.0)); - dropdown.allow_deselect_all(true); - - frp::new_network! { network - init <- source_(); - set_entries <- any(...); - - dropdown.set_all_entries <+ set_entries; - entries_and_value <- all(&set_entries, set_current_value); - entries_and_value <- entries_and_value.debounce(); - - selected_entry <- entries_and_value.map(|(e, v)| entry_for_current_value(e, v)); - dropdown.set_selected_entries <+ selected_entry.map(|e| e.iter().cloned().collect()); - - dropdown_entry <- dropdown.selected_entries.map(|e| e.iter().next().cloned()); - // Emit the output value only after actual user action. This prevents the - // dropdown from emitting its initial value when it is opened, which can - // represent slightly different version of code than actually written. - submitted_entry <- dropdown_entry.sample(&dropdown.user_select_action); - dropdown_out_value <- submitted_entry.map(|e| e.as_ref().map(Entry::value)); - dropdown_out_import <- submitted_entry.map(|e| e.as_ref().and_then(Entry::required_import)); - request_import <+ dropdown_out_import.unwrap(); - output_value <+ dropdown_out_value.sample(&dropdown.user_select_action); - - is_open <- all(is_open, &init)._0(); - dropdown.set_open <+ is_open.on_change(); - - // Close the dropdown after a short delay after selection. Because the dropdown - // value application triggers operations that can introduce a few dropped frames, - // we want to delay the dropdown closing animation after that is handled. - // Otherwise the animation finishes within single frame, which looks bad. - let close_after_selection_timer = frp::io::timer::Timeout::new(&network); - close_after_selection_timer.restart <+ dropdown.user_select_action.constant(1); - eval close_after_selection_timer.on_expired((()) display_object.blur()); - } - - let entries = std::mem::take(Rc::make_mut(entries)); - set_entries.emit(entries); - init.emit(()); - *self = LazyDropdown::Initialized { - _network: network, - _dropdown: dropdown, - set_entries, - }; + for child in chain_children { + // No depth change. If the dropdown is displayed, it should also display its + // arguments. + ctx.builder.child_widget(ctx.parent_instance, child, ctx.depth); } + } else { + ctx.builder.child_widget_of_type( + &self.label_wrapper, + chain_children.into_base(), + ctx.depth, + LABEL_CONFIG, + 0, + ); } } } + fn entry_for_current_value( all_entries: &[Entry], current_value: &Option, diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 0acc3081ed31..7858a2fd6f60 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -4,6 +4,7 @@ // === Features === #![feature(associated_type_defaults)] +#![feature(const_trait_impl)] #![feature(drain_filter)] #![feature(entry_insert)] #![feature(fn_traits)] diff --git a/build/ci_utils/src/fs.rs b/build/ci_utils/src/fs.rs index 8b743e8867fa..12393462e855 100644 --- a/build/ci_utils/src/fs.rs +++ b/build/ci_utils/src/fs.rs @@ -85,8 +85,13 @@ pub async fn mirror_directory(source: impl AsRef, destination: impl AsRef< /// Get the size of a file after gzip compression. pub async fn compressed_size(path: impl AsRef) -> Result { - let file = ::tokio::io::BufReader::new(crate::fs::tokio::open(&path).await?); - let encoded_stream = GzipEncoder::with_quality(file, Level::Best); + // Read the file in chunks of 4MB. Our wasm files are usually way bigger than that, so this + // buffer gives very significant speedup over the default 8KB chunks. + const READER_CAPACITY: usize = 4096 * 1024; + + let file = crate::fs::tokio::open(&path).await?; + let buf_file = ::tokio::io::BufReader::with_capacity(READER_CAPACITY, file); + let encoded_stream = GzipEncoder::with_quality(buf_file, Level::Best); crate::io::read_length(encoded_stream).await.map(into) } diff --git a/lib/rust/ensogl/core/src/control/io/mouse/event.rs b/lib/rust/ensogl/core/src/control/io/mouse/event.rs index 094f3c221e9f..ff5dc060ac11 100644 --- a/lib/rust/ensogl/core/src/control/io/mouse/event.rs +++ b/lib/rust/ensogl/core/src/control/io/mouse/event.rs @@ -140,6 +140,20 @@ where JsEvent: AsRef ) } + pub fn is_primary(&self) -> bool { + self.button() == mouse::PrimaryButton + } + + /// Indicates whether the secondary mouse button was pressed when the event was triggered. + pub fn is_secondary(&self) -> bool { + self.button() == mouse::SecondaryButton + } + + /// Indicates whether the middle mouse button was pressed when the event was triggered. + pub fn is_middle(&self) -> bool { + self.button() == mouse::MiddleButton + } + /// Return the position relative to the event handler that was used to catch the event. If the /// event handler does not have a position in the DOM, the returned position will be relative to /// the viewport. This can happen if the event handler is, for example, the window. @@ -186,6 +200,27 @@ where JsEvent: AsRef } } +// =============== +// === Filters === +// =============== + +type FanMouseEvent = crate::event::Event>; + +/// Indicates whether the primary mouse button was pressed when the event was triggered. +pub fn is_primary(event: &FanMouseEvent) -> bool { + event.button() == mouse::PrimaryButton +} + +/// Indicates whether the primary mouse button was pressed when the event was triggered. +pub fn is_middle(event: &FanMouseEvent) -> bool { + event.button() == mouse::MiddleButton +} + +/// Indicates whether the primary mouse button was pressed when the event was triggered. +pub fn is_secondary(event: &FanMouseEvent) -> bool { + event.button() == mouse::SecondaryButton +} + // ============== diff --git a/lib/rust/frp/src/nodes.rs b/lib/rust/frp/src/nodes.rs index b23b57307a6b..ae71e30afd53 100644 --- a/lib/rust/frp/src/nodes.rs +++ b/lib/rust/frp/src/nodes.rs @@ -134,6 +134,27 @@ impl Network { self.register(OwnedGateNot::new(label, event, behavior)) } + /// Passes the incoming event of the first stream only if the value of the second stream is + /// true. If the event is received when condition is false, it is buffered and emitted when + /// the condition becomes true. Only the last received event value is emitted next time the + /// condition becomes true. A single received event will be reemitted at most once. + /// + /// Behavior: T---F---T-----F-------T---T---F---T-- + /// Event: --1--2-----3---4-5-6----------------- + /// Output: --1-----2--3----------6-------------- + pub fn buffered_gate( + &self, + label: Label, + event: &T1, + behavior: &T2, + ) -> Stream> + where + T1: EventOutput, + T2: EventOutput, + { + self.register(OwnedBufferedGate::new(label, event, behavior)) + } + /// Unwraps the value of incoming events and emits the unwrapped values. pub fn unwrap(&self, label: Label, event: &T) -> Stream where @@ -2203,6 +2224,118 @@ where T2: EventOutput +// ==================== +// === BufferedGate === +// ==================== + +#[derive(Debug)] +pub struct BufferedGateData { + #[allow(dead_code)] + /// This is not accessed in this implementation but it needs to be kept so the source struct + /// stays alive at least as long as this struct. + event: watch::Ref, + behavior: T2, + state: Cell, +} +pub type OwnedBufferedGate = stream::Node>; +pub type BufferedGate = stream::WeakNode>; +struct BufferedGateEdgeTrigger { + gate: BufferedGate, +} + +#[derive(Clone, Copy, Debug)] +enum BufferedGateState { + /// Gate is currently open. + Active, + /// Gate is currently closed and haven't received any events since it was closed. + Inactive, + /// Gate is currently closed and have received at least one event since it was closed. + Buffered, +} + +impl HasOutput for BufferedGateData { + type Output = Output; +} + +impl OwnedBufferedGate +where + T1: EventOutput, + T2: EventOutput, +{ + /// Constructor. + pub fn new(label: Label, src: &T1, behavior: &T2) -> Self { + let event = watch_stream(src); + let state = match behavior.value() { + true => BufferedGateState::Active, + false => BufferedGateState::Inactive, + }; + let definition = + BufferedGateData { event, behavior: behavior.clone_ref(), state: Cell::new(state) }; + let this = Self::construct(label, definition); + let weak = this.downgrade(); + let on_edge = BufferedGateEdgeTrigger { gate: weak.clone() }; + behavior.register_target(stream::EventInput::new(Rc::new(on_edge))); + src.register_target(weak.into()); + this + } +} + +impl stream::EventConsumer> for OwnedBufferedGate +where + T1: EventOutput, + T2: EventOutput, +{ + fn on_event(&self, stack: CallStack, event: &Output) { + match self.state.get() { + BufferedGateState::Active => self.emit_event(stack, event), + BufferedGateState::Inactive => { + self.state.set(BufferedGateState::Buffered); + } + BufferedGateState::Buffered => (), + } + } +} + +impl stream::WeakEventConsumer> for BufferedGateEdgeTrigger +where + T1: EventOutput, + T2: EventOutput, +{ + fn is_dropped(&self) -> bool { + self.gate.is_dropped() + } + + fn on_event_if_exists(&self, stack: CallStack, is_active: &bool) -> bool { + if let Some(gate) = self.gate.upgrade() { + let new_state = match is_active { + true => BufferedGateState::Active, + false => BufferedGateState::Inactive, + }; + match gate.state.replace(new_state) { + BufferedGateState::Active => {} + BufferedGateState::Inactive => {} + BufferedGateState::Buffered => { + gate.event.with(|value| gate.emit_event(stack, value)); + } + } + true + } else { + false + } + } +} + + +impl stream::InputBehaviors for BufferedGateData +where T2: EventOutput +{ + fn input_behaviors(&self) -> Vec { + vec![Link::behavior(&self.behavior)] + } +} + + + // ============== // === Unwrap === // ============== diff --git a/lib/rust/frp/src/stream.rs b/lib/rust/frp/src/stream.rs index 975dc68670dd..06bac023455d 100644 --- a/lib/rust/frp/src/stream.rs +++ b/lib/rust/frp/src/stream.rs @@ -163,6 +163,15 @@ pub trait WeakEventConsumer { pub trait ValueProvider: HasOutput { /// The current output value of the FRP node. fn value(&self) -> Self::Output; + + /// Perform scoped operation on current output value of the FRP node without cloning it. The + /// internal value is borrowed for the duration of the passed function scope. Emitting an event + /// on that node within the function scope will cause a panic. + /// + /// If the node has no cached value, the passed closure will not be executed and `None` will be + /// returned. + fn with(&self, f: impl FnOnce(&Self::Output) -> T) -> Option + where Self: Sized; } @@ -316,10 +325,11 @@ impl EventEmitter for NodeData { impl ValueProvider for NodeData { fn value(&self) -> Out { - if !self.use_caching() { - Out::default(); - } - self.value_cache.borrow().clone() + self.with(|t| t.clone()).unwrap_or_default() + } + + fn with(&self, f: impl FnOnce(&Self::Output) -> T) -> Option { + self.use_caching().then(|| f(&self.value_cache.borrow())) } } @@ -623,24 +633,44 @@ impl ValueProvider for OwnedStream { fn value(&self) -> Self::Output { self.data.value() } + + fn with(&self, f: impl FnOnce(&Self::Output) -> T) -> Option + where Self: Sized { + self.data.with(f) + } } impl ValueProvider for Stream { fn value(&self) -> Self::Output { self.upgrade().map(|t| t.value()).unwrap_or_default() } + + fn with(&self, f: impl FnOnce(&Self::Output) -> T) -> Option + where Self: Sized { + self.upgrade().and_then(|t| t.with(f)) + } } impl ValueProvider for Node { fn value(&self) -> Self::Output { self.stream.value() } + + fn with(&self, f: impl FnOnce(&Self::Output) -> T) -> Option + where Self: Sized { + self.stream.with(f) + } } impl ValueProvider for WeakNode { fn value(&self) -> Self::Output { self.stream.value() } + + fn with(&self, f: impl FnOnce(&Self::Output) -> T) -> Option + where Self: Sized { + self.stream.with(f) + } } From 62fd98e12079c2e9a7d3ef3cad8f5be67693065e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 4 Apr 2023 09:31:56 +0200 Subject: [PATCH 04/45] widgets on nodes wip --- app/gui/src/controller/graph/widget.rs | 1 - .../view/graph-editor/src/component/node.rs | 29 +- .../graph-editor/src/component/node/input.rs | 1 - .../src/component/node/input/area.rs | 96 ++---- .../src/component/node/input/port.rs | 209 ------------- .../src/component/node/input/widget.rs | 283 ++++++++++++------ .../component/node/input/widget/hierarchy.rs | 41 +-- .../src/component/node/input/widget/label.rs | 23 +- .../node/input/widget/single_choice.rs | 52 ++-- app/gui/view/graph-editor/src/lib.rs | 43 ++- .../component/text/src/component/text.rs | 12 +- .../core/src/display/object/instance.rs | 45 +-- .../core/src/display/object/transformation.rs | 2 +- 13 files changed, 347 insertions(+), 490 deletions(-) delete mode 100644 app/gui/view/graph-editor/src/component/node/input/port.rs diff --git a/app/gui/src/controller/graph/widget.rs b/app/gui/src/controller/graph/widget.rs index 0c113a5c28f1..e965a78a25d5 100644 --- a/app/gui/src/controller/graph/widget.rs +++ b/app/gui/src/controller/graph/widget.rs @@ -18,7 +18,6 @@ use crate::model::execution_context::VisualizationUpdateData; use engine_protocol::language_server::SuggestionId; use ensogl::define_endpoints_2; -use ide_view::graph_editor::component::node::input::widget; use ide_view::graph_editor::component::visualization; use ide_view::graph_editor::component::visualization::Metadata; use ide_view::graph_editor::data::enso::Code; diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index b2b39fcdc62d..868426ae4d3a 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -289,6 +289,15 @@ impl Default for Crumbs { } } +#[derive(Debug, Clone, Copy, Default)] +pub enum ConnectionStatus { + #[default] + Disconnected, + Connected { + color: color::Lcha, + }, +} + // ============ @@ -303,7 +312,7 @@ ensogl::define_endpoints_2! { disable_visualization (), set_visualization (Option), set_disabled (bool), - set_input_connected (span_tree::Crumbs,Option,bool), + set_input_connected (span_tree::Crumbs,ConnectionStatus), set_expression (Expression), edit_expression (text::Range, ImString), set_skip_macro (bool), @@ -500,9 +509,8 @@ impl NodeModel { background -> drag_area; drag_area -> edge::front::corner; drag_area -> edge::front::line; - edge::front::corner -> input::port::hover; - edge::front::line -> input::port::hover; - input::port::hover -> input::port::viz; + edge::front::corner -> input::widget::port; + edge::front::line -> input::widget::port; } } @@ -640,7 +648,7 @@ impl NodeModel { #[profile(Debug)] fn set_expression_usage_type(&self, crumbs: &Crumbs, tp: &Option) { match crumbs.endpoint { - Endpoint::Input => self.input.set_expression_usage_type(&crumbs.crumbs, tp), + Endpoint::Input => {} //self.input.set_expression_usage_type(&crumbs.crumbs, tp), Endpoint::Output => self.output.set_expression_usage_type(&crumbs.crumbs, tp), } } @@ -1123,7 +1131,7 @@ pub mod test_utils { /// 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; + fn input_port_shape(&self) -> Option; } impl NodeModelExt for NodeModel { @@ -1138,10 +1146,11 @@ pub mod test_utils { } } - fn input_port_shape(&self) -> Option { - let ports = self.input.model.ports(); - let port = ports.first()?; - port.shape.as_ref().map(CloneRef::clone_ref) + fn input_port_shape(&self) -> Option { + // let ports = self.input.model.ports(); + // let port = ports.first()?; + // port.shape.as_ref().map(CloneRef::clone_ref) + None // TODO } } } diff --git a/app/gui/view/graph-editor/src/component/node/input.rs b/app/gui/view/graph-editor/src/component/node/input.rs index c371dc6c0523..2ed65db5af5e 100644 --- a/app/gui/view/graph-editor/src/component/node/input.rs +++ b/app/gui/view/graph-editor/src/component/node/input.rs @@ -6,7 +6,6 @@ // ============== pub mod area; -pub mod port; pub mod widget; pub use area::Area; diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 6cbad7d35a16..e9668e115c3e 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -7,7 +7,6 @@ use ensogl::display::traits::*; use crate::component::type_coloring; use crate::node; -use crate::node::input::port; use crate::node::input::widget; use crate::node::profiling; use crate::view; @@ -60,12 +59,7 @@ pub const TEXT_SIZE: f32 = 12.0; pub use span_tree::Crumb; pub use span_tree::Crumbs; - -/// Specialized `SpanTree` for the input ports model. -pub type SpanTree = span_tree::SpanTree<()>; - -/// Mutable reference to port inside of a `SpanTree`. -pub type PortRefMut<'a> = span_tree::node::RefMut<'a, port::Model>; +pub use span_tree::SpanTree; @@ -150,13 +144,6 @@ impl From for InputExpression { pub struct Model { app: Application, display_object: display::object::Instance, - // ports: display::object::Instance, - // header: display::object::Instance, - /// Text label used for displaying the ports. Contains both expression text and inserted - /// argument placeholders. The style is adjusted based on port types. - // ports_label: text::Text, - /// Text label used during edit mode. Contains only the expression text without any - /// modifications. Handles user input in edit mode. edit_mode_label: text::Text, expression: RefCell, id_crumbs_map: RefCell>, @@ -243,28 +230,17 @@ impl Model { self } - /// Return a list of Node's input ports. - pub fn ports(&self) -> Vec { - // let expression = self.expression.borrow(); - // let mut ports = Vec::new(); - // expression.span_tree.root_ref().dfs(|n| ports.push(n.payload.clone())); - // ports - todo!() - } - - fn set_label_layer(&self, layer: &display::scene::Layer) { self.edit_mode_label.add_to_scene_layer(layer); // self.ports_label.add_to_scene_layer(layer); } - /// Run the provided function on the target port if exists. - // fn with_port_mut(&self, crumbs: &Crumbs, f: impl FnOnce(PortRefMut)) { - // let mut expression = self.expression.borrow_mut(); - // if let Ok(node) = expression.span_tree.root_ref_mut().get_descendant(crumbs) { - // f(node) - // } - // } + /// Set connection status of the given port. + fn set_connected(&self, crumbs: &Crumbs, status: node::ConnectionStatus) { + let expr = self.expression.borrow(); + let port = expr.span_tree.get_node(crumbs).ok(); + port.map(|port| self.root_widget.set_connected(&port, status)); + } /// Traverse all `SpanTree` leaves of the given port and emit hover style to set their colors. fn set_port_hover(&self, target: &Switch) { @@ -791,14 +767,9 @@ ensogl::define_endpoints! { /// Disable the node (aka "skip mode"). set_disabled (bool), - /// Set the connection status of the port indicated by the breadcrumbs. The optional type - /// is the type of the edge that was connected or disconnected if the edge was typed. - set_connected (Crumbs,Option,bool), - - /// Set the expression USAGE type. This is not the definition type, which can be set with - /// `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), + /// Set the connection status of the port indicated by the breadcrumbs. For connected ports, + /// contains the color of connected edge. + set_connected (Crumbs, node::ConnectionStatus), /// Update widget metadata for widgets already present in this input area. update_widgets (WidgetUpdates), @@ -922,11 +893,7 @@ impl Area { // === Port Hover === eval frp.on_port_hover ((t) model.set_port_hover(t)); - - // eval frp.set_connected ([model]((crumbs,edge_tp,is_connected)) { - // model.with_port_mut(crumbs,|n|n.set_connected(is_connected,edge_tp)); - // model.with_port_mut(crumbs,|n|n.set_parent_connected(is_connected)); - // }); + eval frp.set_connected (((crumbs,status)) model.set_connected(crumbs,*status)); // === Properties === @@ -976,10 +943,6 @@ impl Area { frp.output.source.on_port_code_update <+ widget_code_update; frp.output.source.request_import <+ model.root_widget.request_import; - // === Expression Type === - - // eval frp.set_expression_usage_type (((a,b)) model.set_expression_usage_type(a,b)); - // === Widgets === eval frp.update_widgets ((a) model.apply_widget_updates(a)); @@ -1017,24 +980,23 @@ impl Area { /// An offset from node position to a specific port. pub fn port_offset(&self, crumbs: &[Crumb]) -> Option> { let expr = self.model.expression.borrow(); - None - // nocheckin - // expr.root_ref().get_descendant(crumbs).ok().map(|node| { - // let unit = GLYPH_WIDTH; - // let range_before = enso_text::Range::new(ByteDiff(0), node.payload.index); - // let char_offset = expr.viz_code[range_before].chars().count(); - // let char_count = expr.viz_code[node.payload.range()].chars().count(); - // let width = unit * (char_count as f32); - // let x = width / 2.0 + unit * (char_offset as f32); - // Vector2::new(TEXT_OFFSET + x, 0.0) - // }) + let node = expr.get_node(crumbs).ok()?; + let instance = self.model.root_widget.get_port_display_object(&node)?; + let pos = instance.global_position(); + let node_pos = self.model.display_object.global_position(); + let size = instance.computed_size(); + Some(pos.xy() - node_pos.xy() + size * 0.5) } /// A type of the specified port. - pub fn port_type(&self, _crumbs: &Crumbs) -> Option { - // let expression = self.model.expression.borrow(); - // expression.span_tree.root_ref().get_descendant(crumbs).ok().and_then(|t| t.tp.value()) - None + pub fn port_type(&self, crumbs: &Crumbs) -> Option { + let expression = self.model.expression.borrow(); + expression + .span_tree + .root_ref() + .get_descendant(crumbs) + .ok() + .and_then(|t| t.tp().map(|t| Type(t.into()))) } /// A crumb by AST ID. @@ -1081,12 +1043,4 @@ impl CallInfoMap { Self { call_info } } - - fn has_target(&self, call_id: &ast::Id) -> bool { - self.call_info.get(call_id).map_or(false, |info| info.target_id.is_some()) - } - - fn target(&self, call_id: &ast::Id) -> Option { - self.call_info.get(call_id).and_then(|info| info.target_id) - } } diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs deleted file mode 100644 index a18bd60ccb05..000000000000 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ /dev/null @@ -1,209 +0,0 @@ -//! FIXME[everyone] Modules should be documented. - -use crate::prelude::*; -use enso_text::unit::*; -use ensogl::display::shape::*; - -use crate::node::input::area; -use crate::node::input::widget; -use crate::Type; - -use ensogl::application::Application; -use ensogl::data::color; -use ensogl::display; - - - -// ================= -// === Constants === -// ================= - -/// The horizontal padding of ports. It affects how the port hover should extend the target text -/// boundary on both sides. -pub const PADDING_X: f32 = 4.0; - - - -// =================== -// === Hover Shape === -// =================== - -/// Port hover shape definition. -pub mod hover { - use super::*; - ensogl::shape! { - alignment = center; - (style:Style) { - let width : Var = "input_size.x".into(); - let height : Var = "input_size.y".into(); - let shape = Rect((&width,&height)); - if !area::DEBUG { - let color = Var::::from("srgba(1.0,1.0,1.0,0.00001)"); - shape.fill(color).into() - } else { - let shape = shape.corners_radius(6.px()); - let color = Var::::from("srgba(1.0,0.0,0.0,0.1)"); - shape.fill(color).into() - } - } - } -} - - - -// ============= -// === Shape === -// ============= - -/// Port shape definition. -pub mod viz { - use super::*; - ensogl::shape! { - above = [hover]; - pointer_events = false; - alignment = center; - (style:Style, color:Vector4) { - let width : Var = "input_size.x".into(); - let height : Var = "input_size.y".into(); - let shape = Rect((&width,&height)).corners_radius(&height / 2.0); - shape.fill("srgba(input_color)").into() - } - } -} - - - -// ============= -// === Shape === -// ============= - -/// Shapes the port is build from. It consist of the `hover_shape`, which represents a hover area of -/// a full node height, and the `viz_shape`, which is a nice, visual highlight representation. -/// Both shapes are children of the `root` display object: -/// -/// ```text -/// hover_shape -/// ◄──────► -/// ╭───┬────────┬──┄ -/// │ │╭──────╮│▼ viz_shape -/// │ │╰──────╯│▲ (appears after mouse_hover) -/// ╰───┴────────┴──┄ -/// ``` -#[derive(Clone, CloneRef, Debug)] -#[allow(missing_docs)] -pub struct Shape { - pub root: display::object::Instance, - pub hover: hover::View, - pub viz: viz::View, -} - -impl Shape { - /// Constructor. - #[profile(Debug)] - pub fn new(size: Vector2, hover_height: f32) -> Self { - let root = display::object::Instance::new(); - let hover = hover::View::new(); - let viz = viz::View::new(); - - let width_padded = size.x + 2.0 * PADDING_X; - hover.set_size((width_padded, hover_height)); - viz.set_size((width_padded, size.y)); - hover.set_x(size.x / 2.0); - viz.set_x(size.x / 2.0); - viz.color.set(color::Rgba::transparent().into()); - - root.add_child(&hover); - root.add_child(&viz); - - Self { root, hover, viz } - } -} - -impl display::Object for Shape { - fn display_object(&self) -> &display::object::Instance { - self.root.display_object() - } -} - - - -// ============= -// === Model === -// ============= - -ensogl::define_endpoints! { - Input { - set_optional (bool), - set_disabled (bool), - set_active (bool), - set_hover (bool), - set_connected (bool,Option), - set_parent_connected (bool), - set_definition_type (Option), - set_usage_type (Option), - } - - Output { - tp (Option), - new_value (String), - } -} - -/// Input port model. Please note that this is not a component model. It is a `SpanTree` payload -/// model. -#[derive(Clone, Debug, Default)] -#[allow(missing_docs)] -pub struct Model { - pub frp: Frp, - pub shape: Option, - pub widget: Option, - pub index: ByteDiff, - pub local_index: ByteDiff, - pub length: ByteDiff, -} - -impl Deref for Model { - type Target = Frp; - fn deref(&self) -> &Self::Target { - &self.frp - } -} - -impl Model { - /// Constructor. - pub fn new() -> Self { - default() - } - - /// Shape initialization. Please note that not all port models get their shapes initialized, - /// as some are skipped. For example, given the expression `(((foo)))`, the inner parentheses - /// will be skipped, as there is no point in making them ports. The skip algorithm is - /// implemented as part of the port are initialization. - #[profile(Debug)] - pub fn init_shape(&mut self, size: Vector2, hover_height: f32) -> Shape { - let shape = Shape::new(size, hover_height); - self.shape = Some(shape); - self.shape.as_ref().unwrap().clone_ref() - } - - /// Widget initialization. Only nodes that represent function arguments or argument placeholders - /// will have widgets created for them. - pub fn init_widget(&mut self, app: &Application) -> widget::Root { - let widget = widget::Root::new(app); - self.widget = Some(widget.clone_ref()); - widget - } - - /// Assign an existing widget to this port. - pub fn use_existing_widget(&mut self, widget: widget::Root) -> widget::Root { - self.widget = Some(widget.clone_ref()); - widget - } - - /// The range of this port. - pub fn range(&self) -> enso_text::Range { - let start = self.index; - let end = self.index + self.length; - enso_text::Range::new(start, end) - } -} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index e57e4f250bdc..a57ff49324d1 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -6,29 +6,33 @@ use crate::prelude::*; use crate::component::node::input::area::NODE_HEIGHT; use crate::component::node::input::area::TEXT_OFFSET; +use crate::component::node::ConnectionStatus; use enso_config::ARGS; use enso_frp as frp; use enso_text as text; use ensogl::application::Application; +use ensogl::data::color; use ensogl::display; use ensogl_component::drop_down::DropdownValue; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use text::index::Byte; - - // =========== // === FRP === // =========== ensogl::define_endpoints_2! { Input { - /// Set the widget's metadata that was received from the language server. It overrides - /// widget's configuration, even allowing the widget type to be completely changed. When - /// the metadata is set to `Some` value, the corresponding widget will ignore its span-tree - /// type. - set_metadata (MetadataPointer, Option), + // /// Set the widget's metadata that was received from the language server. It overrides + // /// widget's configuration, even allowing the widget type to be completely changed. When + // /// the metadata is set to `Some` value, the corresponding widget will ignore its span-tree + // /// type. + // set_metadata (MetadataPointer, Option), + + // /// Set the connection status of the port indicated by the breadcrumbs. The optional type + // /// is the type of the edge that was connected or disconnected if the edge was typed. + // set_connected (span_tree::Crumbs, ConnectionStatus), } Output { value_changed (span_tree::Crumbs, Option), @@ -55,27 +59,14 @@ pub struct MetadataPointer { pub trait SpanWidget { /// Configuration associated with specific widget variant. type Config: Debug + Clone + PartialEq; + /// Root display object of a widget. It is returned to the parent widget for positioning. + fn root_object(&self) -> &display::object::Instance; /// Create a new widget with given configuration. - fn new(config: &Self::Config, ctx: ConfigContext) -> Self; + fn new(config: &Self::Config, app: &Application, frp: &WidgetsFrp) -> Self; /// Update configuration for existing widget. fn configure(&mut self, config: &Self::Config, ctx: ConfigContext); } -/// Create a widget if it does not exist, or reconfigure if it does. -pub fn create_or_update_widget( - old_widget: Option, - config: &T::Config, - ctx: ConfigContext, -) -> T { - match old_widget { - Some(mut widget) => { - widget.configure(config, ctx); - widget - } - None => T::new(config, ctx), - } -} - macro_rules! define_widget_modules( ($( $(#[$meta:meta])* @@ -116,10 +107,16 @@ macro_rules! define_widget_modules( impl SpanWidget for DynWidget { type Config = Config; - fn new(config: &Config, ctx: ConfigContext) -> Self { + fn root_object(&self) -> &display::object::Instance { + match self { + $(DynWidget::$name(inner) => inner.root_object(),)* + } + } + + fn new(config: &Config, app: &Application, frp: &WidgetsFrp) -> Self { match config { $( - Config::$name(config) => DynWidget::$name(SpanWidget::new(config, ctx)), + Config::$name(config) => DynWidget::$name(SpanWidget::new(config, app, frp)), )* } } @@ -130,7 +127,8 @@ macro_rules! define_widget_modules( SpanWidget::configure(model, config, ctx); },)* (this, _) => { - *this = SpanWidget::new(config, ctx); + *this = SpanWidget::new(config, ctx.app(), ctx.frp()); + this.configure(config, ctx) }, } } @@ -177,11 +175,10 @@ impl Metadata { } fn vector_editor() -> Metadata { - let config = vector_editor::Config::default().into(); - Self { label: None, display: Display::Always, config } + Self::always(vector_editor::Config::default()) } - fn from_kind(kind: &span_tree::node::Kind) -> Self { + fn from_kind(kind: &span_tree::node::Kind, has_children: bool) -> Self { use span_tree::node::Kind; const VECTOR_TYPE: &str = "Standard.Base.Data.Vector.Vector"; @@ -200,7 +197,8 @@ impl Metadata { } else { Self::always(label::Config::default()) }, - Kind::Root | Kind::NamedArgument | Kind::Chained(_) => Self::always(hierarchy::Config), + Kind::Root | Kind::NamedArgument | Kind::Chained(_) if has_children => + Self::always(hierarchy::Config), _ => Self::always(label::Config::default()), } } @@ -269,17 +267,15 @@ impl DropdownValue for Entry { -// ================== -// === SampledFrp === -// ================== +/// ================== +/// === WidgetsFrp === +/// ================== -/// Sampled version of widget FRP endpoints that can be used by widget views that are initialized -/// on demand after first interaction. Without samplers, when a widget view would be initialized -/// after the endpoints were set, it would not receive previously set endpoint values. +/// Widget FRP endpoints that can be used by widget views, and go straight to the root. #[derive(Debug, Clone, CloneRef)] -struct SampledFrp { - value_changed: frp::Any<(span_tree::Crumbs, Option)>, - request_import: frp::Any, +pub struct WidgetsFrp { + pub(self) value_changed: frp::Any<(span_tree::Crumbs, Option)>, + pub(self) request_import: frp::Any, } @@ -294,7 +290,7 @@ struct SampledFrp { pub struct Root { #[deref] frp: Frp, - sampled_frp: SampledFrp, + sampled_frp: WidgetsFrp, model: Rc, } @@ -313,19 +309,23 @@ impl Root { let model = Rc::new(RootModel::new(app)); let network = frp.network(); - frp::extend! { network - eval frp.input.set_metadata([model]((pointer, meta)) { - model.set_metadata(pointer.clone(), meta.clone()); - }); - } - let value_changed = frp.private.output.value_changed.clone_ref(); let request_import = frp.private.output.request_import.clone_ref(); - let sampled_frp = SampledFrp { value_changed, request_import }; + let sampled_frp = WidgetsFrp { value_changed, request_import }; Self { frp, sampled_frp, model } } + pub fn set_metadata(&self, pointer: MetadataPointer, meta: Option) { + self.model.set_metadata(pointer, meta); + } + + pub fn set_connected(&self, tree_node: &span_tree::node::Ref, status: ConnectionStatus) { + self.model + .get_port_widget_pointer(tree_node) + .map(|pointer| self.model.with_port_mut(&pointer, |p| p.set_connected(status))); + } + pub fn rebuild_tree_on_metadata_change( &self, tree: &span_tree::SpanTree, @@ -339,6 +339,14 @@ impl Root { pub fn rebuild_tree(&self, tree: &span_tree::SpanTree, node_expression: &str) { self.model.rebuild_tree(self.sampled_frp.clone_ref(), tree, node_expression) } + + pub fn get_port_display_object( + &self, + tree_node: &span_tree::node::Ref, + ) -> Option { + let pointer = self.model.get_port_widget_pointer(tree_node)?; + self.model.with_port(&pointer, |w| w.display_object().clone()) + } } /// ============= @@ -349,7 +357,7 @@ impl Root { struct RootModel { app: Application, display_object: debug::InstanceWithBg, - widgets_map: RefCell>, + widgets_map: RefCell>, metadata_map: Rc>>, metadata_dirty: AtomicBool, } @@ -416,27 +424,58 @@ impl RootModel { } #[profile(Task)] - fn rebuild_tree(&self, frp: SampledFrp, tree: &span_tree::SpanTree, node_expression: &str) { + fn rebuild_tree(&self, frp: WidgetsFrp, tree: &span_tree::SpanTree, node_expression: &str) { self.metadata_dirty.store(false, Ordering::Release); let app = self.app.clone(); let metadata_map = self.metadata_map.borrow(); let widgets_map = self.widgets_map.take(); let mut builder = WidgetTreeBuilder::new(node_expression, app, frp, &*metadata_map, widgets_map); - builder.child_widget(&self.display_object.inner, tree.root_ref(), 0); + let child = builder.child_widget(tree.root_ref(), 0); + self.display_object.inner.add_child(&child.root); self.widgets_map.replace(builder.new_widgets); } + + pub fn get_port_widget_pointer( + &self, + tree_node: &span_tree::node::Ref, + ) -> Option { + if let Some(id) = tree_node.node.ast_id { + Some(WidgetTreePointer { + id: Some(id), + crumbs: default(), + nested_key: None, + }) + } else { + // TODO + None + } + } + + pub fn with_port( + &self, + pointer: &WidgetTreePointer, + f: impl FnOnce(&Port) -> T, + ) -> Option { + self.widgets_map.borrow().get(pointer).map(f) + } + + pub fn with_port_mut( + &self, + pointer: &WidgetTreePointer, + f: impl FnOnce(&mut Port) -> T, + ) -> Option { + self.widgets_map.borrow_mut().get_mut(pointer).map(f) + } } #[derive(Debug)] pub struct ConfigContext<'a, 'b> { - builder: &'a mut WidgetTreeBuilder<'b>, - parent_instance: &'a display::object::Instance, - display: Display, - span_tree_node: span_tree::node::Ref<'a>, - parent_parenthesized: bool, - depth: usize, + builder: &'a mut WidgetTreeBuilder<'b>, + display: Display, + span_tree_node: span_tree::node::Ref<'a>, + depth: usize, } impl<'a, 'b> ConfigContext<'a, 'b> { @@ -444,7 +483,7 @@ impl<'a, 'b> ConfigContext<'a, 'b> { &self.builder.app } - fn frp(&self) -> &SampledFrp { + fn frp(&self) -> &WidgetsFrp { &self.builder.frp } @@ -453,6 +492,12 @@ impl<'a, 'b> ConfigContext<'a, 'b> { } } +#[derive(Debug)] +struct ChildWidget { + root: display::object::Instance, + pointer: WidgetTreePointer, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct WidgetTreePointer { /// The latest set ast::Id in the span tree. @@ -467,23 +512,22 @@ struct WidgetTreePointer { #[derive(Debug)] struct WidgetTreeBuilder<'a> { app: Application, - frp: SampledFrp, + frp: WidgetsFrp, metadata_map: &'a HashMap, - old_widgets: HashMap, - new_widgets: HashMap, + old_widgets: HashMap, + new_widgets: HashMap, last_ast_id: Option, last_ast_id_crumbs: span_tree::Crumbs, node_expression: &'a str, - parenthesized: bool, } impl<'a> WidgetTreeBuilder<'a> { fn new( node_expression: &'a str, app: Application, - frp: SampledFrp, + frp: WidgetsFrp, metadata_map: &'a HashMap, - old_widgets: HashMap, + old_widgets: HashMap, ) -> Self { Self { app, @@ -494,44 +538,40 @@ impl<'a> WidgetTreeBuilder<'a> { last_ast_id: default(), last_ast_id_crumbs: default(), node_expression, - parenthesized: false, } } + #[must_use] pub(self) fn child_widget( &mut self, - parent_object: &display::object::Instance, span_tree_node: span_tree::node::Ref<'_>, depth: usize, - ) -> WidgetTreePointer { - self.create_child_widget(parent_object, span_tree_node, depth, None, None) + ) -> ChildWidget { + self.create_child_widget(span_tree_node, depth, None, None) } + #[must_use] pub(self) fn child_widget_of_type( &mut self, - parent_object: &display::object::Instance, span_tree_node: span_tree::node::Ref<'_>, depth: usize, meta: Metadata, key: usize, - ) -> WidgetTreePointer { - self.create_child_widget(parent_object, span_tree_node, depth, Some(meta), Some(key)) + ) -> ChildWidget { + self.create_child_widget(span_tree_node, depth, Some(meta), Some(key)) } fn create_child_widget( &mut self, - parent_object: &display::object::Instance, span_tree_node: span_tree::node::Ref<'_>, depth: usize, set_metadata: Option, nested_key: Option, - ) -> WidgetTreePointer { + ) -> ChildWidget { // This call can recurse into itself within the widget configuration logic. We need to save // the current layer's state, so it can be restored later after visiting the child node. let mut ast_data_to_restore = None; - let parent_parenthesized = self.parenthesized; - self.parenthesized = span_tree_node.parenthesized; // Figure out the widget tree pointer for the current node. That pointer determines the // widget identity, allowing it to maintain internal state. If the previous tree already @@ -559,6 +599,7 @@ impl<'a> WidgetTreeBuilder<'a> { let mut meta_fallback = None; let kind = &span_tree_node.kind; + let has_children = !span_tree_node.children.is_empty(); let meta = set_metadata .as_ref() .or_else(|| { @@ -570,32 +611,98 @@ impl<'a> WidgetTreeBuilder<'a> { meta_pointer.and_then(|ptr| self.metadata_map.get(&ptr)) }) - .unwrap_or_else(|| meta_fallback.get_or_insert_with(|| Metadata::from_kind(kind))); + .unwrap_or_else(|| { + meta_fallback.get_or_insert_with(|| Metadata::from_kind(kind, has_children)) + }); let old_widget = self.old_widgets.remove(&tree_ptr); - let widget = { - let ctx = ConfigContext { - builder: &mut *self, - parent_instance: parent_object, - display: meta.display, - span_tree_node, - parent_parenthesized, - depth, - }; + let port = { + let ctx = + ConfigContext { builder: &mut *self, display: meta.display, span_tree_node, depth }; // Widget creation/update can recurse into the builder. All borrows must be dropped // at this point. - create_or_update_widget(old_widget, &meta.config, ctx) + + match old_widget { + Some(mut port) => { + port.widget.configure(&meta.config, ctx); + port + } + None => { + let app = ctx.app(); + let frp = ctx.frp(); + let mut widget = DynWidget::new(&meta.config, app, frp); + widget.configure(&meta.config, ctx); + Port::new(widget) + } + } }; - self.new_widgets.insert(tree_ptr.clone(), widget); + let root = port.display_object().clone(); + self.new_widgets.insert(tree_ptr.clone(), port); // After visiting child node, restore previous layer's data. if let Some((id, crumbs)) = ast_data_to_restore { self.last_ast_id = id; self.last_ast_id_crumbs = crumbs; } - self.parenthesized = parent_parenthesized; + ChildWidget { pointer: tree_ptr, root } + } +} + + +// ================= +// === Constants === +// ================= + +/// The horizontal padding of ports. It affects how the port hover should extend the target text +/// boundary on both sides. +pub const PADDING_X: f32 = 4.0; + + +// ============ +// === Port === +// ============ + +/// Port shape definition. +pub mod port { + use super::*; + ensogl::shape! { + (style:Style, color:Vector4) { + let size = Var::canvas_size(); + let transparent = Var::::from("srgba(1.0,1.0,1.0,0.00001)"); + let shape_color = Var::::from(color); + let hover_shape = Rect(&size).fill(transparent); + let visual_shape = Rect(&size).corners_radius(size.y() / 2.0).fill(shape_color); + hover_shape.union(visual_shape).into() + } + } +} + +#[derive(Debug)] +pub(super) struct Port { + shape: port::View, + widget: DynWidget, +} + +impl Port { + fn new(widget: DynWidget) -> Self { + let shape = port::View::new(); + shape.color.set(color::Rgba::transparent().into()); + shape.add_child(widget.root_object()); + Self { shape, widget } + } + + fn set_connected(&self, status: ConnectionStatus) { + match status { + ConnectionStatus::Connected { color } => + self.shape.color.set(color::Rgba::from(color).into()), + ConnectionStatus::Disconnected => self.shape.color.modify(|c| c.xyz().push(0.0)), + }; + } +} - tree_ptr +impl display::Object for Port { + fn display_object(&self) -> &display::object::Instance { + self.shape.display_object() } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs index 6d418d91567e..1245c9cebf29 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -3,7 +3,8 @@ use crate::prelude::*; -use ensogl::display::object::Instance; +use ensogl::application::Application; +use ensogl::display::object; // ================= // === Hierarchy === @@ -18,7 +19,6 @@ pub struct Config; #[derive(Clone, Debug)] pub struct Widget { display_object: super::debug::InstanceWithBg, - // shape: debug_shape::View, } /// Width of a single space glyph @@ -27,43 +27,30 @@ pub const SPACE_GLYPH_WIDTH: f32 = 7.224_609_4; impl super::SpanWidget for Widget { type Config = Config; - fn new(config: &Config, ctx: super::ConfigContext) -> Self { + + fn root_object(&self) -> &object::Instance { + &self.display_object.outer + } + + fn new(_: &Config, _: &Application, _: &super::WidgetsFrp) -> Self { let display_object = super::debug::InstanceWithBg::olive(); display_object.inner.use_auto_layout(); display_object.inner.set_children_alignment_left_center().justify_content_center_y(); - - let mut this = Self { display_object }; - this.configure(config, ctx); - this + Self { display_object } } - fn configure(&mut self, _config: &Config, ctx: super::ConfigContext) { - self.display_object.outer.set_parent(ctx.parent_instance); + fn configure(&mut self, _: &Config, ctx: super::ConfigContext) { let offset = ctx.span_tree_node.sibling_offset.as_usize() as f32; self.display_object.inner.set_padding_left(offset * SPACE_GLYPH_WIDTH); + self.display_object.inner.remove_all_children(); let preserve_depth = ctx.span_tree_node.is_chained() || ctx.span_tree_node.is_named_argument(); let next_depth = if preserve_depth { ctx.depth } else { ctx.depth + 1 }; - for child in ctx.span_tree_node.children_iter() { - ctx.builder.child_widget(&self.display_object.inner, child, next_depth); - } - } -} - - -/// Temporary dropdown activation shape definition. -pub mod debug_shape { - use super::*; - ensogl::shape! { - above = [ - crate::component::node::background, - crate::component::node::input::port::hover - ]; - (style:Style) { - let color = Var::::from("srgba(1.0,0.0,0.0,0.1)"); - Rect(Var::canvas_size()).fill(color).into() + for node in ctx.span_tree_node.children_iter() { + let child = ctx.builder.child_widget(node, next_depth); + self.display_object.inner.add_child(&child.root); } } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 7bf9cbf88e86..75a9d8e6adce 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -4,7 +4,9 @@ use crate::prelude::*; use super::debug::InstanceWithBg; use crate::component::node::input::area::TEXT_SIZE; +use ensogl::application::Application; use ensogl::data::color; +use ensogl::display::object; use ensogl_component::text; // ============= @@ -43,14 +45,19 @@ pub struct Widget { impl super::SpanWidget for Widget { type Config = Config; - fn new(config: &Config, ctx: super::ConfigContext) -> Self { + + fn root_object(&self) -> &object::Instance { + &self.root.outer + } + + fn new(_: &Config, app: &Application, _: &super::WidgetsFrp) -> Self { // Embed the label in a vertically centered fixed height container, so that the label's // baseline is aligned with other labels in the same row. - let layers = &ctx.app().display.default_scene.layers; + let layers = &app.display.default_scene.layers; let root = InstanceWithBg::magenta(); root.inner.set_size_y(TEXT_SIZE); - let label = text::Text::new(ctx.app()); + let label = text::Text::new(app); label.set_property_default(text::Size(TEXT_SIZE)); label.set_y(TEXT_SIZE); layers.above_nodes_text.add(&label); @@ -62,18 +69,17 @@ impl super::SpanWidget for Widget { content_change <- frp.content.on_change(); color_change <- frp.text_color.on_change(); weight_change <- frp.text_weight.on_change(); - eval content_change((content) label.set_content(content)); + eval content_change([label] (content) { + label.set_content(content); + }); eval color_change((color) label.set_property_default(color)); eval weight_change((weight) label.set_property_default(weight)); } - let mut this = Self { frp, root, label }; - this.configure(config, ctx); - this + Self { frp, root, label } } fn configure(&mut self, config: &Config, ctx: super::ConfigContext) { - self.root.outer.set_parent(ctx.parent_instance); let is_placeholder = ctx.span_tree_node.is_expected_argument(); let offset = if config.ignore_offset { @@ -85,7 +91,6 @@ impl super::SpanWidget for Widget { self.label.set_x(offset * super::hierarchy::SPACE_GLYPH_WIDTH); - let content = if is_placeholder { ctx.span_tree_node.kind.argument_name().unwrap_or_default() } else { diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index a05dc978d63c..3213f106f1be 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -3,6 +3,7 @@ use crate::prelude::*; use enso_frp as frp; +use ensogl::application::Application; use ensogl::control::io::mouse; use ensogl::data::color; use ensogl::display; @@ -37,7 +38,7 @@ pub mod triangle { above = [ crate::component::node::background, crate::component::node::drag_area, - crate::component::node::input::port::hover + crate::component::node::input::widget::port ]; alignment = left_bottom; (style:Style, color:Vector4) { @@ -75,7 +76,6 @@ ensogl::define_endpoints_2! { content(ImString), current_value(Option), current_crumbs(span_tree::Crumbs), - is_open(bool), } } @@ -98,10 +98,12 @@ pub struct Widget { impl super::SpanWidget for Widget { type Config = Config; - fn new(config: &Config, ctx: super::ConfigContext) -> Self { - let app = ctx.app(); + fn root_object(&self) -> &display::object::Instance { + &self.display_object + } + fn new(_: &Config, app: &Application, widgets_frp: &super::WidgetsFrp) -> Self { // ╭─display_object───────────────╮ // │╭─content_wrapper────────────╮│ // ││╭ shape ╮ ╭─label_wrapper──╮││ @@ -144,7 +146,7 @@ impl super::SpanWidget for Widget { let dropdown = app.new_view::>(); dropdown_wrapper.add_child(&dropdown); - let layers = &ctx.app().display.default_scene.layers; + let layers = &app.display.default_scene.layers; layers.above_nodes.add(&dropdown); dropdown.set_y(-20.0); dropdown.set_max_open_size(Vector2(300.0, 500.0)); @@ -154,7 +156,6 @@ impl super::SpanWidget for Widget { let config_frp = Frp::new(); let network = &config_frp.network; let input = &config_frp.private.input; - let widgets_frp = ctx.frp(); let focus_receiver = display_object.clone_ref(); @@ -213,7 +214,7 @@ impl super::SpanWidget for Widget { ); }; - let mut this = Self { + Self { config_frp, display_object, content_wrapper, @@ -221,13 +222,10 @@ impl super::SpanWidget for Widget { dropdown, activation_shape, label_wrapper, - }; - this.configure(config, ctx); - this + } } fn configure(&mut self, config: &Config, ctx: super::ConfigContext) { - self.display_object.set_parent(ctx.parent_instance); let input = &self.config_frp.public.input; let is_placeholder = ctx.span_tree_node.is_expected_argument(); @@ -251,31 +249,27 @@ impl super::SpanWidget for Widget { // Do not increment the depth. If the dropdown is displayed, it should also display // its arguments. - let mut chain_children = ctx.span_tree_node.chain_children_iter(); + self.label_wrapper.remove_all_children(); + self.content_wrapper.remove_all_children(); - if let Some(first_child) = chain_children.next() { - ctx.builder.child_widget_of_type( - &self.label_wrapper, - first_child, - ctx.depth, - LABEL_CONFIG, - 0, - ); - + if ctx.span_tree_node.is_chained() { + let mut chain_children = ctx.span_tree_node.chain_children_iter(); + if let Some(first_child) = chain_children.next() { + let label = + ctx.builder.child_widget_of_type(first_child, ctx.depth, LABEL_CONFIG, 0); + self.label_wrapper.add_child(&label.root); + } for child in chain_children { // No depth change. If the dropdown is displayed, it should also display its // arguments. - ctx.builder.child_widget(ctx.parent_instance, child, ctx.depth); + let child = ctx.builder.child_widget(child, ctx.depth); + self.content_wrapper.add_child(&child.root); } } else { - ctx.builder.child_widget_of_type( - &self.label_wrapper, - chain_children.into_base(), - ctx.depth, - LABEL_CONFIG, - 0, - ); + let label = + ctx.builder.child_widget_of_type(ctx.span_tree_node, ctx.depth, LABEL_CONFIG, 0); + self.label_wrapper.add_child(&label.root); } } } diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 7858a2fd6f60..aa0dc9ea5120 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -1920,7 +1920,7 @@ impl GraphEditorModel { } if let Some(target) = edge.take_target() { - self.set_input_connected(&target, None, false); // FIXME None + self.set_input_connected(&target, node::ConnectionStatus::Disconnected); if let Some(target_node) = self.nodes.get_cloned_ref(&target.node_id) { target_node.in_edges.remove(&edge_id); } @@ -1928,21 +1928,36 @@ impl GraphEditorModel { } } - fn set_input_connected(&self, target: &EdgeEndpoint, tp: Option, status: bool) { + fn set_input_connected(&self, target: &EdgeEndpoint, status: node::ConnectionStatus) { if let Some(node) = self.nodes.get_cloned(&target.node_id) { - node.view.set_input_connected(&target.port, tp, status); + node.view.set_input_connected(&target.port, status); } } - fn set_edge_target_connection_status(&self, edge_id: EdgeId, status: bool) { + fn set_edge_target_connection_status( + &self, + edge_id: EdgeId, + status: bool, + neutral_color: color::Lcha, + ) { self.with_edge_target(edge_id, |tgt| { - self.set_endpoint_connection_status(edge_id, &tgt, status) + self.set_endpoint_connection_status(edge_id, &tgt, status, neutral_color) }); } - fn set_endpoint_connection_status(&self, edge_id: EdgeId, target: &EdgeEndpoint, status: bool) { - let tp = self.edge_source_type(edge_id); - self.set_input_connected(target, tp, status); + fn set_endpoint_connection_status( + &self, + edge_id: EdgeId, + target: &EdgeEndpoint, + status: bool, + neutral_color: color::Lcha, + ) { + let status = match status { + true => + node::ConnectionStatus::Connected { color: self.edge_color(edge_id, neutral_color) }, + false => node::ConnectionStatus::Disconnected, + }; + self.set_input_connected(target, status); } fn enable_visualization(&self, node_id: impl Into) { @@ -3657,11 +3672,15 @@ fn new_graph_editor(app: &Application) -> GraphEditor { // === Source / Target === + let neutral_color = model.model.styles_frp.get_color(theme::code::types::any::selection); + eval out.on_edge_source_set (((id,tgt)) model.set_edge_source(*id,tgt)); eval out.on_edge_target_set (((id,tgt)) model.set_edge_target(*id,tgt)); - eval out.on_edge_target_set (((id,tgt)) model.set_endpoint_connection_status(*id,tgt,true)); - eval out.on_edge_target_unset (((id,tgt)) model.set_endpoint_connection_status(*id,tgt,false)); + eval out.on_edge_target_set ([model, neutral_color] ((id,tgt)) + model.set_endpoint_connection_status(*id,tgt,true, neutral_color.value().into())); + eval out.on_edge_target_unset ([model, neutral_color] ((id,tgt)) + model.set_endpoint_connection_status(*id,tgt,false, neutral_color.value().into())); eval out.on_edge_source_unset (((id,_)) model.remove_edge_source(*id)); eval out.on_edge_target_unset (((id,_)) model.remove_edge_target(*id)); @@ -3678,7 +3697,6 @@ fn new_graph_editor(app: &Application) -> GraphEditor { out.on_edge_only_source_not_set <+ out.on_edge_target_set_with_source_not_set._0(); out.on_edge_only_source_not_set <+ out.on_edge_source_unset._0(); - let neutral_color = model.model.styles_frp.get_color(theme::code::types::any::selection); eval out.on_edge_source_set ([model,neutral_color]((id, _)) model.refresh_edge_color(*id,neutral_color.value().into())); eval out.on_edge_target_set ([model,neutral_color]((id, _)) @@ -3737,7 +3755,8 @@ fn new_graph_editor(app: &Application) -> GraphEditor { eval inputs.set_node_expression (((id, expr)) model.set_node_expression(id, expr)); eval inputs.edit_node_expression (((id, range, ins)) model.edit_node_expression(id, range, ins)); port_to_refresh <= inputs.set_node_expression.map(f!(((id, _))model.node_in_edges(id))); - eval port_to_refresh ((id) model.set_edge_target_connection_status(*id,true)); + eval port_to_refresh ([model, neutral_color] + (id) model.set_edge_target_connection_status(*id,true, neutral_color.value().into())); // === Remove implementation === out.node_removed <+ inputs.remove_node; diff --git a/lib/rust/ensogl/component/text/src/component/text.rs b/lib/rust/ensogl/component/text/src/component/text.rs index 1436c42836e6..09e47ae0522a 100644 --- a/lib/rust/ensogl/component/text/src/component/text.rs +++ b/lib/rust/ensogl/component/text/src/component/text.rs @@ -740,11 +740,17 @@ impl TextModel { let glyph_system = RefCell::new(glyph_system); let buffer = buffer::Buffer::new(buffer::BufferModel::new()); let layer = CloneRefCell::new(scene.layers.main.clone_ref()); - let lines = Lines::new(Self::new_line_helper( + + let default_size = buffer.formatting.font_size().default.value; + let first_line = Self::new_line_helper( &app.display.default_scene.frp.frame_time, &display_object, - buffer.formatting.font_size().default.value, - )); + default_size, + ); + first_line.set_baseline((-default_size).round()); + first_line.skip_baseline_animation(); + + let lines = Lines::new(first_line); let width_dirty = default(); let height_dirty = default(); let shaped_lines = default(); diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index 5aa449fa752e..25c6a0484343 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -1633,18 +1633,17 @@ impl HierarchyFrp { #[derive(Debug, Deref)] pub struct HierarchyModel { #[deref] - frp: HierarchyFrp, - visible: Cell, - transformation: RefCell, - parent_bind: SharedParentBind, - next_child_index: Cell, + frp: HierarchyFrp, + visible: Cell, + transformation: RefCell, + parent_bind: SharedParentBind, // We are using [`BTreeMap`] here in order to preserve the child insertion order. - children: RefCell>, + children: RefCell>, /// Layer the object was explicitly assigned to by the user, if any. - assigned_layer: RefCell>, + assigned_layer: RefCell>, /// Layer where the object is displayed. It may be set to by user or inherited from the parent. - layer: RefCell>, - dirty: dirty::Flags, + layer: RefCell>, + dirty: dirty::Flags, } impl HierarchyModel { @@ -1653,22 +1652,11 @@ impl HierarchyModel { let visible = default(); let transformation = default(); let parent_bind = default(); - let next_child_index = default(); let children = default(); let assigned_layer = default(); let layer = default(); let dirty = dirty::Flags::new(&parent_bind); - Self { - frp, - visible, - transformation, - parent_bind, - next_child_index, - children, - assigned_layer, - layer, - dirty, - } + Self { frp, visible, transformation, parent_bind, children, assigned_layer, layer, dirty } } } @@ -2021,12 +2009,9 @@ impl InstanceDef { }) } - /// Replaces the parent binding with a new parent. Does nothing if the provided parent is the - /// same as the current one. + /// Replaces the parent binding with a new parent. pub fn set_parent(&self, parent: &InstanceDef) { - if !parent.is_parent_of(self) { - parent.add_child(self); - } + parent.add_child(self); } /// Checks if the provided object is a parent of the current one. @@ -2058,9 +2043,11 @@ impl InstanceDef { } fn register_child(&self, child: &InstanceDef) -> ChildIndex { - let index = self.next_child_index.get(); - self.next_child_index.set(ChildIndex(*index + 1)); - self.children.borrow_mut().insert(index, child.downgrade()); + let mut children_borrow = self.children.borrow_mut(); + let next_key = children_borrow.last_key_value().map_or(0, |(k, _)| **k + 1); + let index = ChildIndex(next_key); + children_borrow.insert(index, child.downgrade()); + drop(children_borrow); self.dirty.modified_children.set(index); index } diff --git a/lib/rust/ensogl/core/src/display/object/transformation.rs b/lib/rust/ensogl/core/src/display/object/transformation.rs index fe1e968b512c..8c12d9abef8e 100644 --- a/lib/rust/ensogl/core/src/display/object/transformation.rs +++ b/lib/rust/ensogl/core/src/display/object/transformation.rs @@ -207,7 +207,7 @@ impl CachedTransformation { } pub fn global_position(&self) -> Vector3 { - (self.matrix * Vector4::new(0.0, 0.0, 0.0, 1.0)).xyz() + self.matrix.column(3).xyz() } } From 5c840ed569c7d2341cf642503e5fadd06b2293bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 4 Apr 2023 19:32:16 +0200 Subject: [PATCH 05/45] use replace_children --- .../src/component/node/input/widget.rs | 359 +----------------- .../component/node/input/widget/hierarchy.rs | 11 +- .../node/input/widget/single_choice.rs | 19 +- .../node/input/widget/vector_editor.rs | 42 +- .../core/src/display/object/instance.rs | 12 +- 5 files changed, 39 insertions(+), 404 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index a57ff49324d1..69cb1341ff69 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -432,7 +432,7 @@ impl RootModel { let mut builder = WidgetTreeBuilder::new(node_expression, app, frp, &*metadata_map, widgets_map); let child = builder.child_widget(tree.root_ref(), 0); - self.display_object.inner.add_child(&child.root); + self.display_object.inner.replace_children(&[child]); self.widgets_map.replace(builder.new_widgets); } @@ -492,12 +492,6 @@ impl<'a, 'b> ConfigContext<'a, 'b> { } } -#[derive(Debug)] -struct ChildWidget { - root: display::object::Instance, - pointer: WidgetTreePointer, -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct WidgetTreePointer { /// The latest set ast::Id in the span tree. @@ -546,7 +540,7 @@ impl<'a> WidgetTreeBuilder<'a> { &mut self, span_tree_node: span_tree::node::Ref<'_>, depth: usize, - ) -> ChildWidget { + ) -> display::object::Instance { self.create_child_widget(span_tree_node, depth, None, None) } @@ -557,7 +551,7 @@ impl<'a> WidgetTreeBuilder<'a> { depth: usize, meta: Metadata, key: usize, - ) -> ChildWidget { + ) -> display::object::Instance { self.create_child_widget(span_tree_node, depth, Some(meta), Some(key)) } @@ -568,7 +562,7 @@ impl<'a> WidgetTreeBuilder<'a> { depth: usize, set_metadata: Option, nested_key: Option, - ) -> ChildWidget { + ) -> display::object::Instance { // This call can recurse into itself within the widget configuration logic. We need to save // the current layer's state, so it can be restored later after visiting the child node. let mut ast_data_to_restore = None; @@ -644,7 +638,7 @@ impl<'a> WidgetTreeBuilder<'a> { self.last_ast_id = id; self.last_ast_id_crumbs = crumbs; } - ChildWidget { pointer: tree_ptr, root } + root } } @@ -705,346 +699,3 @@ impl display::Object for Port { self.shape.display_object() } } - - - -// ======================== -// === KindModel / Kind === -// ======================== - -/// Possible widgets for a node input. -/// -/// Currently, all widget types are hardcoded. This is likely to be a temporary solution. In the -/// future the widget types might be user-defined, similar to visualizations. -#[derive(serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)] -pub enum Kind { - /// A widget for selecting a single value from a list of available options. - #[serde(rename = "Single_Choice")] - SingleChoice, -} - -/// A part of widget model that is dependant on the widget kind. -#[derive(Debug)] -pub enum KindModel { - /// A widget for selecting a single value from a list of available options. - SingleChoice(SingleChoiceModel), -} - -impl KindModel { - fn new( - app: &Application, - display_object: &display::object::Instance, - kind: Kind, - frp: &SampledFrp, - meta: &Option, - node_data: &NodeData, - ) -> Self { - let this = match kind { - Kind::SingleChoice => - Self::SingleChoice(SingleChoiceModel::new(app, display_object, frp)), - }; - - this.update(meta, node_data); - this - } - - fn update(&self, meta: &Option, node_data: &NodeData) { - match self { - KindModel::SingleChoice(inner) => { - let dynamic_entries = meta.as_ref().map(|meta| meta.dynamic_entries.clone()); - let entries = dynamic_entries - .unwrap_or_else(|| node_data.tag_values.iter().map(Into::into).collect()); - - inner.set_port_size(node_data.port_size); - inner.set_entries(entries); - } - } - } - - fn kind(&self) -> Kind { - match self { - Self::SingleChoice(_) => Kind::SingleChoice, - } - } -} - - - -// ====================== -// === Triangle Shape === -// ====================== - -/// Temporary dropdown activation shape definition. -pub mod triangle { - use super::*; - ensogl::shape! { - above = [ - crate::component::node::background, - crate::component::node::input::port::hover - ]; - alignment = center; - (style:Style, color:Vector4) { - let size = Var::canvas_size(); - let radius = 1.0.px(); - let shrink = &radius * 2.0; - let shape = Triangle(size.x() - &shrink, size.y() - &shrink) - .flip_y() - .grow(radius); - shape.fill(color).into() - } - } -} - - - -// ==================== -// === SingleChoice === -// ==================== - -/// A widget for selecting a single value from a list of available options. The options can be -/// provided as a static list of strings from argument `tag_values`, or as a dynamic expression. -#[derive(Debug)] -pub struct SingleChoiceModel { - #[allow(dead_code)] - network: frp::Network, - dropdown: Rc>, - /// temporary click handling - activation_shape: triangle::View, -} - -impl SingleChoiceModel { - fn new( - app: &Application, - display_object: &display::object::Instance, - frp: &SampledFrp, - ) -> Self { - let activation_shape = triangle::View::new(); - activation_shape.set_size(ACTIVATION_SHAPE_SIZE); - display_object.add_child(&activation_shape); - - frp::new_network! { network - init <- source_(); - let focus_in = display_object.on_event::(); - let focus_out = display_object.on_event::(); - is_focused <- bool(&focus_out, &focus_in); - is_open <- frp.set_visible && is_focused; - is_open <- is_open.sampler(); - }; - - let set_current_value = frp.set_current_value.clone_ref(); - let dropdown_output = frp.out_value_changed.clone_ref(); - let request_import = frp.out_request_import.clone_ref(); - let dropdown = LazyDropdown::new( - app, - display_object, - set_current_value, - is_open, - dropdown_output, - request_import, - ); - let dropdown = Rc::new(RefCell::new(dropdown)); - - frp::extend! { network - clicked <- activation_shape.events_deprecated.mouse_down_primary.gate_not(&frp.set_read_only); - toggle_focus <- clicked.map(f!([display_object](()) !display_object.is_focused())); - set_focused <- any(toggle_focus, frp.set_focused); - eval set_focused([display_object](focus) match focus { - true => display_object.focus(), - false => display_object.blur(), - }); - - set_visible <- all(&frp.set_visible, &init)._0(); - shape_alpha <- set_visible.map(|visible| if *visible { 1.0 } else { 0.0 }); - shape_color <- shape_alpha.map(|a| ACTIVATION_SHAPE_COLOR.with_alpha(*a)); - eval shape_color([activation_shape] (color) { - activation_shape.color.set(color::Rgba::from(color).into()); - }); - - eval focus_in((_) dropdown.borrow_mut().initialize_on_open()); - } - - init.emit(()); - - Self { network, dropdown, activation_shape } - } - - fn set_port_size(&self, port_size: Vector2) { - self.activation_shape.set_x(port_size.x() / 2.0); - self.activation_shape - .set_y(-port_size.y() / 2.0 - ACTIVATION_SHAPE_SIZE.y() - ACTIVATION_SHAPE_Y_OFFSET); - self.dropdown.borrow_mut().set_port_size(port_size); - } - - fn set_entries(&self, entries: Vec) { - self.dropdown.borrow_mut().set_entries(entries); - } -} - - - -// ==================== -// === LazyDropdown === -// ==================== - -/// A lazy dropdown that is only initialized when it is opened for the first time. This prevents -/// very long initialization time, as dropdown view creation is currently a very slow process. -/// -/// FIXME [PG]: Improve grid-view creation performance, so that this is no longer needed. -/// https://www.pivotaltracker.com/story/show/184223891 -/// -/// Once grid-view creation is reasonably fast, this might be replaced by direct dropdown -/// initialization on widget creation. -#[derive(Debug)] -enum LazyDropdown { - NotInitialized { - app: Application, - display_object: display::object::Instance, - dropdown_y: f32, - entries: Vec, - set_current_value: frp::Sampler>, - is_open: frp::Sampler, - output_value: frp::Any>, - request_import: frp::Any, - }, - Initialized { - _network: frp::Network, - dropdown: Dropdown, - set_entries: frp::Any>, - }, -} - -impl LazyDropdown { - fn new( - app: &Application, - display_object: &display::object::Instance, - set_current_value: frp::Sampler>, - is_open: frp::Sampler, - output_value: frp::Any>, - request_import: frp::Any, - ) -> Self { - let app = app.clone_ref(); - let display_object = display_object.clone_ref(); - let dropdown_y = default(); - let entries = default(); - LazyDropdown::NotInitialized { - app, - display_object, - dropdown_y, - entries, - set_current_value, - is_open, - output_value, - request_import, - } - } - - fn set_port_size(&mut self, new_port_size: Vector2) { - let y = -new_port_size.y() - DROPDOWN_Y_OFFSET; - match self { - LazyDropdown::Initialized { dropdown, .. } => { - dropdown.set_y(y); - } - LazyDropdown::NotInitialized { dropdown_y, .. } => { - *dropdown_y = y; - } - } - } - - fn set_entries(&mut self, new_entries: Vec) { - match self { - LazyDropdown::Initialized { set_entries, .. } => { - set_entries.emit(new_entries); - } - LazyDropdown::NotInitialized { entries, .. } => { - *entries = new_entries; - } - } - } - - #[profile(Detail)] - fn initialize_on_open(&mut self) { - match self { - LazyDropdown::Initialized { .. } => {} - LazyDropdown::NotInitialized { - app, - display_object, - dropdown_y, - entries, - is_open, - set_current_value, - output_value, - request_import, - } => { - let dropdown = app.new_view::>(); - display_object.add_child(&dropdown); - app.display.default_scene.layers.above_nodes.add(&dropdown); - dropdown.set_y(*dropdown_y); - dropdown.set_max_open_size(Vector2(300.0, 500.0)); - dropdown.allow_deselect_all(true); - - frp::new_network! { network - init <- source_(); - set_entries <- any(...); - - dropdown.set_all_entries <+ set_entries; - entries_and_value <- all(&set_entries, set_current_value); - entries_and_value <- entries_and_value.debounce(); - - selected_entry <- entries_and_value.map(|(e, v)| entry_for_current_value(e, v)); - dropdown.set_selected_entries <+ selected_entry.map(|e| e.iter().cloned().collect()); - - dropdown_entry <- dropdown.selected_entries.map(|e| e.iter().next().cloned()); - // Emit the output value only after actual user action. This prevents the - // dropdown from emitting its initial value when it is opened, which can - // represent slightly different version of code than actually written. - submitted_entry <- dropdown_entry.sample(&dropdown.user_select_action); - dropdown_out_value <- submitted_entry.map(|e| e.as_ref().map(Entry::value)); - dropdown_out_import <- submitted_entry.map(|e| e.as_ref().and_then(Entry::required_import)); - request_import <+ dropdown_out_import.unwrap(); - output_value <+ dropdown_out_value.sample(&dropdown.user_select_action); - - is_open <- all(is_open, &init)._0(); - dropdown.set_open <+ is_open.on_change(); - - // Close the dropdown after a short delay after selection. Because the dropdown - // value application triggers operations that can introduce a few dropped frames, - // we want to delay the dropdown closing animation after that is handled. - // Otherwise the animation finishes within single frame, which looks bad. - let close_after_selection_timer = frp::io::timer::Timeout::new(&network); - close_after_selection_timer.restart <+ dropdown.user_select_action.constant(1); - eval close_after_selection_timer.on_expired((()) display_object.blur()); - } - - set_entries.emit(std::mem::take(entries)); - init.emit(()); - *self = LazyDropdown::Initialized { _network: network, dropdown, set_entries }; - } - } - } -} - -fn entry_for_current_value( - all_entries: &[Entry], - current_value: &Option, -) -> Option { - let current_value = current_value.clone()?; - let found_entry = all_entries.iter().find(|entry| entry.value.as_ref() == current_value); - let with_partial_match = found_entry.or_else(|| { - // Handle parentheses in current value. Entries with parenthesized expressions will match if - // they start with the same expression as the current value. That way it is still matched - // once extra arguments are added to the nested function call. - if current_value.starts_with('(') { - let current_value = current_value.trim_start_matches('(').trim_end_matches(')'); - all_entries.iter().find(|entry| { - let trimmed_value = entry.value.trim_start_matches('(').trim_end_matches(')'); - current_value.starts_with(trimmed_value) - }) - } else { - None - } - }); - - let with_fallback = - with_partial_match.cloned().unwrap_or_else(|| Entry::from_value(current_value.clone())); - Some(with_fallback) -} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs index 1245c9cebf29..a048d82cbe04 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -42,15 +42,16 @@ impl super::SpanWidget for Widget { fn configure(&mut self, _: &Config, ctx: super::ConfigContext) { let offset = ctx.span_tree_node.sibling_offset.as_usize() as f32; self.display_object.inner.set_padding_left(offset * SPACE_GLYPH_WIDTH); - self.display_object.inner.remove_all_children(); let preserve_depth = ctx.span_tree_node.is_chained() || ctx.span_tree_node.is_named_argument(); let next_depth = if preserve_depth { ctx.depth } else { ctx.depth + 1 }; - for node in ctx.span_tree_node.children_iter() { - let child = ctx.builder.child_widget(node, next_depth); - self.display_object.inner.add_child(&child.root); - } + let children = ctx + .span_tree_node + .children_iter() + .map(|node| ctx.builder.child_widget(node, next_depth)) + .collect_vec(); + self.display_object.inner.replace_children(&children); } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 3213f106f1be..9a025e06b54d 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -18,9 +18,9 @@ use crate::component::node::input::widget::Metadata; /// === Constants === /// ================= -const ACTIVATION_SHAPE_COLOR: color::Lch = color::Lch::new(0.56708, 0.23249, 0.71372); +pub const ACTIVATION_SHAPE_COLOR: color::Lch = color::Lch::new(0.56708, 0.23249, 0.71372); const ACTIVATION_SHAPE_Y_OFFSET: f32 = 15.0; -const ACTIVATION_SHAPE_SIZE: Vector2 = Vector2(15.0, 11.0); +pub const ACTIVATION_SHAPE_SIZE: Vector2 = Vector2(15.0, 11.0); /// Distance between the dropdown and the bottom of the port. const DROPDOWN_Y_OFFSET: f32 = 5.0; @@ -250,7 +250,6 @@ impl super::SpanWidget for Widget { // Do not increment the depth. If the dropdown is displayed, it should also display // its arguments. - self.label_wrapper.remove_all_children(); self.content_wrapper.remove_all_children(); if ctx.span_tree_node.is_chained() { @@ -258,18 +257,16 @@ impl super::SpanWidget for Widget { if let Some(first_child) = chain_children.next() { let label = ctx.builder.child_widget_of_type(first_child, ctx.depth, LABEL_CONFIG, 0); - self.label_wrapper.add_child(&label.root); - } - for child in chain_children { - // No depth change. If the dropdown is displayed, it should also display its - // arguments. - let child = ctx.builder.child_widget(child, ctx.depth); - self.content_wrapper.add_child(&child.root); + self.label_wrapper.replace_children(&[label]); } + let content_children = chain_children + .map(|child| ctx.builder.child_widget(child, ctx.depth)) + .collect_vec(); + self.content_wrapper.replace_children(&content_children); } else { let label = ctx.builder.child_widget_of_type(ctx.span_tree_node, ctx.depth, LABEL_CONFIG, 0); - self.label_wrapper.add_child(&label.root); + self.label_wrapper.replace_children(&[label]); } } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs b/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs index 7654178413fb..045054f92665 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs @@ -6,10 +6,9 @@ use crate::prelude::*; -use crate::component::node::input::widget::triangle; -use crate::component::node::input::widget::SampledFrp; -use crate::component::node::input::widget::ACTIVATION_SHAPE_COLOR; -use crate::component::node::input::widget::ACTIVATION_SHAPE_SIZE; +use crate::component::node::input::widget::single_choice::triangle; +use crate::component::node::input::widget::single_choice::ACTIVATION_SHAPE_COLOR; +use crate::component::node::input::widget::single_choice::ACTIVATION_SHAPE_SIZE; use ensogl::application::Application; use ensogl::control::io::mouse; @@ -20,9 +19,16 @@ use ensogl_component::text::Text; -// ============= -// === Model === -// ============= +// ============== +// === Widget === +// ============== + +ensogl::define_endpoints_2! { + Input { + current_value(Option), + current_crumbs(span_tree::Crumbs), + } +} /// A model for the vector editor widget. /// @@ -43,7 +49,7 @@ pub struct Model { pub set_port_size: frp::Source, } -impl Model { +impl Widget { /// A gap between the `activation_shape` and `elements` view. const GAP: f32 = 3.0; @@ -199,23 +205,3 @@ impl Model { opt_iterator.into_iter().flatten() } } - - - -/// ============== -/// === Widget === -/// ============== - -struct Widget {} - -#[derive(Debug, Clone, Copy, PartialEq, Default)] -struct Config {} - -impl super::SpanWidget for Widget { - type Config = Config; - fn new(config: &Config, ctx: super::ConfigContext<'_>) -> Self { - Self {} - } - - fn configure(&mut self, config: &Config, ctx: super::ConfigContext<'_>) {} -} diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index 25c6a0484343..b6922255adab 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -1912,7 +1912,7 @@ impl Model { } if !self.children.borrow().is_empty() { debug_span!("Updating all children.").in_scope(|| { - let children = self.children.borrow().clone(); + let children = self.children.borrow(); children.values().for_each(|weak_child| { weak_child.upgrade().for_each(|child| { child.update_with_origin( @@ -1937,7 +1937,7 @@ impl Model { if self.dirty.modified_children.check_all() { debug_span!("Updating dirty children.").in_scope(|| { self.dirty.modified_children.take().iter().for_each(|ix| { - self.children.borrow().get(ix).and_then(|t| t.upgrade()).for_each( + self.children.borrow().get(&ix).and_then(|t| t.upgrade()).for_each( |child| { child.update_with_origin( scene, @@ -5941,8 +5941,8 @@ mod layout_tests { test.root.add_child(&test.node1); test.run(|| { test.assert_root_computed_size(20.0, 10.0) - .assert_node2_position(0.0, 0.0) - .assert_node1_position(10.0, 0.0); + .assert_node1_position(10.0, 0.0) + .assert_node2_position(0.0, 0.0); }); let node3 = Instance::new(); @@ -5950,8 +5950,8 @@ mod layout_tests { test.root.add_child(&node3); test.run(|| { test.assert_root_computed_size(32.0, 10.0) - .assert_node2_position(0.0, 0.0) - .assert_node1_position(10.0, 0.0); + .assert_node1_position(10.0, 0.0) + .assert_node2_position(0.0, 0.0); }); assert_eq!(node3.position().xy(), Vector2(20.0, 0.0)); } From 8a4cbb6199ff3ed55d89ac43360a1025a91c861d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 4 Apr 2023 21:27:19 +0200 Subject: [PATCH 06/45] fix object parenting after dynamic widget type update --- .../src/component/node/input/area.rs | 6 +- .../src/component/node/input/widget.rs | 17 ++- .../src/component/node/input/widget/label.rs | 34 +---- .../node/input/widget/single_choice.rs | 125 ++++++++++-------- 4 files changed, 91 insertions(+), 91 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index e9668e115c3e..4c8517c22861 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -966,9 +966,9 @@ impl Area { selection_color.target <+ selection_color_rgba.map(|c| color::Lcha::from(c)); // model.ports_label.set_selection_color <+ selection_color.value.map(|c| color::Lch::from(c)); - std_base_color <- all(std_base_color,init)._0(); - profiled_base_color <- all(profiled_base_color,init)._0(); - base_color <- profiled.switch(&std_base_color,&profiled_base_color); + // std_base_color <- all(std_base_color,init)._0(); + // profiled_base_color <- all(profiled_base_color,init)._0(); + // base_color <- profiled.switch(&std_base_color,&profiled_base_color); // eval base_color ((color) model.ports_label.set_property_default(color)); } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 69cb1341ff69..4e47997a815d 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -307,7 +307,6 @@ impl Root { pub fn new(app: &Application) -> Self { let frp = Frp::new(); let model = Rc::new(RootModel::new(app)); - let network = frp.network(); let value_changed = frp.private.output.value_changed.clone_ref(); let request_import = frp.private.output.request_import.clone_ref(); @@ -619,6 +618,7 @@ impl<'a> WidgetTreeBuilder<'a> { match old_widget { Some(mut port) => { port.widget.configure(&meta.config, ctx); + port.update_root(); port } None => { @@ -675,14 +675,25 @@ pub mod port { pub(super) struct Port { shape: port::View, widget: DynWidget, + inner: display::object::Instance, } impl Port { fn new(widget: DynWidget) -> Self { let shape = port::View::new(); shape.color.set(color::Rgba::transparent().into()); - shape.add_child(widget.root_object()); - Self { shape, widget } + let inner = widget.root_object().clone_ref(); + shape.add_child(&inner); + Self { shape, widget, inner } + } + + fn update_root(&mut self) { + let new_root = self.widget.root_object(); + if new_root != &self.inner { + self.shape.remove_child(&self.inner); + self.shape.add_child(new_root); + self.inner = new_root.clone_ref(); + } } fn set_connected(&self, status: ConnectionStatus) { diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 75a9d8e6adce..1a843542ebba 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -15,23 +15,12 @@ use ensogl_component::text; /// Label widget configuration options. #[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Config { - pub ignore_offset: bool, - pub bold: bool, -} - -impl Config { - /// Create a new label widget configuration. - pub const fn expression() -> Self { - Self { ignore_offset: false, bold: false } - } -} +pub struct Config; ensogl::define_endpoints_2! { Input { content(ImString), text_color(color::Lcha), - text_weight(text::Weight), } } @@ -52,7 +41,7 @@ impl super::SpanWidget for Widget { fn new(_: &Config, app: &Application, _: &super::WidgetsFrp) -> Self { // Embed the label in a vertically centered fixed height container, so that the label's - // baseline is aligned with other labels in the same row. + // baseline is properly aligned to center and lines up with other labels in the line. let layers = &app.display.default_scene.layers; let root = InstanceWithBg::magenta(); @@ -66,28 +55,21 @@ impl super::SpanWidget for Widget { let network = &frp.network; frp::extend! { network - content_change <- frp.content.on_change(); color_change <- frp.text_color.on_change(); - weight_change <- frp.text_weight.on_change(); + eval color_change((color) label.set_property_default(color)); + content_change <- frp.content.on_change(); eval content_change([label] (content) { label.set_content(content); }); - eval color_change((color) label.set_property_default(color)); - eval weight_change((weight) label.set_property_default(weight)); } Self { frp, root, label } } - fn configure(&mut self, config: &Config, ctx: super::ConfigContext) { + fn configure(&mut self, _: &Config, ctx: super::ConfigContext) { let is_placeholder = ctx.span_tree_node.is_expected_argument(); - - let offset = if config.ignore_offset { - 0.0 - } else { - let min_offset = if is_placeholder { 1.0f32 } else { 0.0 }; - min_offset.max(ctx.span_tree_node.sibling_offset.as_usize() as f32) - }; + let min_offset = if is_placeholder { 1.0f32 } else { 0.0 }; + let offset = min_offset.max(ctx.span_tree_node.sibling_offset.as_usize() as f32); self.label.set_x(offset * super::hierarchy::SPACE_GLYPH_WIDTH); @@ -105,11 +87,9 @@ impl super::SpanWidget for Widget { color::Lcha::new(0.0, 0.0, 0.0, 1.0) }; - let weight = if config.bold { text::Weight::Bold } else { text::Weight::Normal }; let input = &self.frp.public.input; input.content.emit(content.clone()); input.text_color.emit(text_color); - input.text_weight.emit(weight); } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 9a025e06b54d..1a2d0c085ba8 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -2,6 +2,8 @@ use crate::prelude::*; +use crate::component::node::input::area::TEXT_SIZE; +use crate::component::node::input::widget::Entry; use enso_frp as frp; use ensogl::application::Application; use ensogl::control::io::mouse; @@ -9,10 +11,7 @@ use ensogl::data::color; use ensogl::display; use ensogl::display::object::event; use ensogl_component::drop_down::Dropdown; - - -use crate::component::node::input::widget::Entry; -use crate::component::node::input::widget::Metadata; +use ensogl_component::text; /// ================= /// === Constants === @@ -24,9 +23,6 @@ pub const ACTIVATION_SHAPE_SIZE: Vector2 = Vector2(15.0, 11.0); /// Distance between the dropdown and the bottom of the port. const DROPDOWN_Y_OFFSET: f32 = 5.0; -const LABEL_CONFIG: Metadata = - Metadata::always(super::label::Config { ignore_offset: true, bold: true }); - // ====================== // === Triangle Shape === // ====================== @@ -82,17 +78,16 @@ ensogl::define_endpoints_2! { /// A widget for selecting a single value from a list of available options. The options can be /// provided as a static list of strings from argument `tag_values`, or as a dynamic expression. #[derive(Debug)] +#[allow(dead_code)] pub struct Widget { config_frp: Frp, display_object: display::object::Instance, - #[allow(dead_code)] content_wrapper: display::object::Instance, - #[allow(dead_code)] dropdown_wrapper: display::object::Instance, - #[allow(dead_code)] - dropdown: Dropdown, label_wrapper: display::object::Instance, - #[allow(dead_code)] + args_wrapper: display::object::Instance, + dropdown: Dropdown, + label: text::Text, activation_shape: triangle::View, } @@ -104,53 +99,64 @@ impl super::SpanWidget for Widget { } fn new(_: &Config, app: &Application, widgets_frp: &super::WidgetsFrp) -> Self { - // ╭─display_object───────────────╮ - // │╭─content_wrapper────────────╮│ - // ││╭ shape ╮ ╭─label_wrapper──╮││ - // │││ │ │ │││ - // │││ │ │ │││ - // ││╰───────╯ ╰────────────────╯││ - // │╰────────────────────────────╯│ - // ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ - // │ ◎ dropdown_wrapper size=0 │ - // ╰──────────────────────────────╯ + // ╭─display_object─────────────────────────────────────╮ + // │╭─content_wrapper──────────────────────────────────╮│ + // ││╭ shape ╮ ╭ label_wrapper ────╮ ╭ args_wrapper ─╮ ││ + // │││ │ │ │ │ │ ││ + // │││ │ │ │ │ │ ││ + // ││╰───────╯ ╰───────────────────╯ ╰───────────────╯ ││ + // │╰──────────────────────────────────────────────────╯│ + // ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ + // │ ◎ dropdown_wrapper size=0 │ + // ╰────────────────────────────────────────────────────╯ + let layers = &app.display.default_scene.layers; let dot_color = color::Rgba::from(ACTIVATION_SHAPE_COLOR.with_alpha(1.0)).into(); let activation_shape = triangle::View::new(); activation_shape.color.set(dot_color); activation_shape.set_size(ACTIVATION_SHAPE_SIZE); - // activation_shape.set_y(-ACTIVATION_SHAPE_SIZE.y() - ACTIVATION_SHAPE_Y_OFFSET); + let label = text::Text::new(app); + layers.above_nodes_text.add(&label); + label.set_property_default(text::Size(TEXT_SIZE)); + label.set_property_default(text::Weight::Bold); + label.set_y(TEXT_SIZE); + + let dropdown = app.new_view::>(); + layers.above_nodes.add(&dropdown); + dropdown.set_y(-20.0); + dropdown.set_max_open_size(Vector2(300.0, 500.0)); + dropdown.allow_deselect_all(true); let display_object = display::object::Instance::new(); + let content_wrapper = display_object.new_child(); + content_wrapper.add_child(&activation_shape); + let label_wrapper = content_wrapper.new_child(); + label_wrapper.add_child(&label); + let args_wrapper = content_wrapper.new_child(); + let dropdown_wrapper = display_object.new_child(); + dropdown_wrapper.add_child(&dropdown); + + display_object .use_auto_layout() .set_column_flow() .set_children_alignment_left_center() .justify_content_center_y(); - let content_wrapper = display_object.new_child(); content_wrapper .use_auto_layout() .set_children_alignment_left_center() .justify_content_center_y(); - content_wrapper.add_child(&activation_shape); - let label_wrapper = content_wrapper.new_child(); - label_wrapper + + args_wrapper .use_auto_layout() .set_children_alignment_left_center() .justify_content_center_y(); - let dropdown_wrapper = display_object.new_child(); dropdown_wrapper.set_size((0.0, 0.0)).set_alignment_left_top(); + label_wrapper.set_size_y(TEXT_SIZE); - let dropdown = app.new_view::>(); - dropdown_wrapper.add_child(&dropdown); - let layers = &app.display.default_scene.layers; - layers.above_nodes.add(&dropdown); - dropdown.set_y(-20.0); - dropdown.set_max_open_size(Vector2(300.0, 500.0)); - dropdown.allow_deselect_all(true); let config_frp = Frp::new(); @@ -160,6 +166,8 @@ impl super::SpanWidget for Widget { let focus_receiver = display_object.clone_ref(); frp::extend! { network + label.set_content <+ input.content.on_change(); + let focus_in = focus_receiver.on_event::(); let focus_out = focus_receiver.on_event::(); is_open <- bool(&focus_out, &focus_in); @@ -214,14 +222,17 @@ impl super::SpanWidget for Widget { ); }; + Self { config_frp, display_object, content_wrapper, dropdown_wrapper, + label_wrapper, + args_wrapper, dropdown, activation_shape, - label_wrapper, + label, } } @@ -236,38 +247,36 @@ impl super::SpanWidget for Widget { let has_value = !ctx.span_tree_node.is_insertion_point(); let current_value: Option = has_value.then(|| ctx.expression_at(ctx.span_tree_node.span()).into()); - let content = current_value - .clone() - .or_else(|| config.label.clone()) - .or_else(|| ctx.span_tree_node.kind.argument_name().map(Into::into)) - .unwrap_or_default(); input.current_crumbs(ctx.span_tree_node.crumbs.clone()); input.current_value(current_value); - input.content(content); input.set_entries(config.entries.clone()); - // Do not increment the depth. If the dropdown is displayed, it should also display - // its arguments. - self.content_wrapper.remove_all_children(); + let mut label_expr_span = ctx.span_tree_node.span(); if ctx.span_tree_node.is_chained() { - let mut chain_children = ctx.span_tree_node.chain_children_iter(); - if let Some(first_child) = chain_children.next() { - let label = - ctx.builder.child_widget_of_type(first_child, ctx.depth, LABEL_CONFIG, 0); - self.label_wrapper.replace_children(&[label]); - } - let content_children = chain_children - .map(|child| ctx.builder.child_widget(child, ctx.depth)) - .collect_vec(); - self.content_wrapper.replace_children(&content_children); + let mut chain = ctx.span_tree_node.clone().chain_children_iter(); + if let Some(first_child) = chain.next() { + label_expr_span = first_child.span(); + }; + + // Do not increment the depth. If the dropdown is displayed, it should also display + // its arguments. + let children = + chain.map(|child| ctx.builder.child_widget(child, ctx.depth)).collect_vec(); + self.args_wrapper.replace_children(&children); } else { - let label = - ctx.builder.child_widget_of_type(ctx.span_tree_node, ctx.depth, LABEL_CONFIG, 0); - self.label_wrapper.replace_children(&[label]); + self.args_wrapper.remove_all_children(); } + + let content: ImString = has_value + .then(|| ctx.expression_at(label_expr_span).into()) + .or_else(|| config.label.clone()) + .or_else(|| ctx.span_tree_node.kind.argument_name().map(Into::into)) + .unwrap_or_default(); + + input.content(content); } } From 19062939e9fd38b6efade3ca21a7030b8cf91826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Wed, 12 Apr 2023 12:37:47 +0200 Subject: [PATCH 07/45] lazy replace_children, finish get_port_widget_pointer --- .../src/component/node/input/widget.rs | 50 +++++--- .../node/input/widget/single_choice.rs | 19 ++- .../core/src/display/object/instance.rs | 116 +++++++++++++++++- 3 files changed, 158 insertions(+), 27 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 4e47997a815d..461af81eaf2d 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -440,14 +440,33 @@ impl RootModel { tree_node: &span_tree::node::Ref, ) -> Option { if let Some(id) = tree_node.node.ast_id { - Some(WidgetTreePointer { - id: Some(id), - crumbs: default(), - nested_key: None, - }) + // This span represents an AST node, return a pointer directly to it. + Some(WidgetTreePointer { id: Some(id), crumbs: default() }) } else { - // TODO - None + let root = tree_node.span_tree.root_ref(); + let root_ast_data = root.ast_id.map(|id| (id, 0)); + + // When the node does not represent an AST node, its widget will be identified by the + // closest parent AST node, if it exists. We have to find the closest parent node with + // AST ID, and then calculate the relative crumbs from it to the current node. + let (_, ast_parent_data) = tree_node.crumbs.into_iter().enumerate().fold( + (root, root_ast_data), + |(node, last_seen), (index, crumb)| { + let ast_data = node.node.ast_id.map(|id| (id, index + 1)).or(last_seen); + (node.child(*crumb).expect("Node ref must be valid"), ast_data) + }, + ); + + match ast_parent_data { + // Parent AST node found, return a pointer relative to it. + Some((ast_id, ast_parent_index)) => { + let crumb_slice = &tree_node.crumbs[ast_parent_index..]; + let crumbs = span_tree::Crumbs::new(crumb_slice.to_vec()); + Some(WidgetTreePointer { id: Some(ast_id), crumbs }) + } + // No parent AST node found. Return a pointer from root. + None => Some(WidgetTreePointer { id: None, crumbs: tree_node.crumbs.clone() }), + } } } @@ -494,12 +513,9 @@ impl<'a, 'b> ConfigContext<'a, 'b> { #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct WidgetTreePointer { /// The latest set ast::Id in the span tree. - id: Option, + id: Option, /// Remaining crumbs to the widget, starting from the node with the latest set ast::Id. - crumbs: span_tree::Crumbs, - /// The optional additional identifier of the widget that is attached to the same node as its - /// parent. Used to distinguish between multiple widgets attached to the same node. - nested_key: Option, + crumbs: span_tree::Crumbs, } #[derive(Debug)] @@ -540,7 +556,7 @@ impl<'a> WidgetTreeBuilder<'a> { span_tree_node: span_tree::node::Ref<'_>, depth: usize, ) -> display::object::Instance { - self.create_child_widget(span_tree_node, depth, None, None) + self.create_child_widget(span_tree_node, depth, None) } #[must_use] @@ -549,9 +565,8 @@ impl<'a> WidgetTreeBuilder<'a> { span_tree_node: span_tree::node::Ref<'_>, depth: usize, meta: Metadata, - key: usize, ) -> display::object::Instance { - self.create_child_widget(span_tree_node, depth, Some(meta), Some(key)) + self.create_child_widget(span_tree_node, depth, Some(meta)) } @@ -560,7 +575,6 @@ impl<'a> WidgetTreeBuilder<'a> { span_tree_node: span_tree::node::Ref<'_>, depth: usize, set_metadata: Option, - nested_key: Option, ) -> display::object::Instance { // This call can recurse into itself within the widget configuration logic. We need to save // the current layer's state, so it can be restored later after visiting the child node. @@ -575,7 +589,7 @@ impl<'a> WidgetTreeBuilder<'a> { let prev_crumbs = std::mem::replace(&mut self.last_ast_id_crumbs, span_tree_node.crumbs.clone()); ast_data_to_restore = Some((prev_ast_id, prev_crumbs)); - WidgetTreePointer { id: Some(ast_id), crumbs: default(), nested_key } + WidgetTreePointer { id: Some(ast_id), crumbs: default() } } None => { let this_crumbs = &span_tree_node.crumbs; @@ -585,7 +599,7 @@ impl<'a> WidgetTreeBuilder<'a> { let id = self.last_ast_id; let crumbs_since_id = &this_crumbs[self.last_ast_id_crumbs.len()..]; let crumbs = span_tree::Crumbs::new(crumbs_since_id.to_vec()); - WidgetTreePointer { id, crumbs, nested_key } + WidgetTreePointer { id, crumbs } } }; diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 1a2d0c085ba8..d024186d1ed4 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -166,8 +166,6 @@ impl super::SpanWidget for Widget { let focus_receiver = display_object.clone_ref(); frp::extend! { network - label.set_content <+ input.content.on_change(); - let focus_in = focus_receiver.on_event::(); let focus_out = focus_receiver.on_event::(); is_open <- bool(&focus_out, &focus_in); @@ -193,7 +191,9 @@ impl super::SpanWidget for Widget { eval close_after_selection_timer.on_expired((()) focus_receiver.blur()); } frp::extend! { network - entries_and_value <- all(&input.set_entries, &input.current_value); + current_value <- input.current_value.on_change(); + entries <- input.set_entries.on_change(); + entries_and_value <- all(&entries, ¤t_value); entries_and_value <- entries_and_value.debounce(); entries_and_value <- entries_and_value.buffered_gate(&is_open); } @@ -213,6 +213,19 @@ impl super::SpanWidget for Widget { submitted_entry <- dropdown_entry.sample(&dropdown.user_select_action); dropdown_out_value <- submitted_entry.map(|e| e.as_ref().map(Entry::value)); dropdown_out_import <- submitted_entry.map(|e| e.as_ref().and_then(Entry::required_import)); + + label.set_content <+ input.content.on_change(); + has_value <- entries_and_value.map(|(_, v)| v.is_some()).on_change(); + eval has_value([label] (&has_value) { + let color = if has_value { + color::Lcha::new(0.0, 0.0, 0.0, 1.0) + } else { + color::Lcha::new(0.5, 0.0, 0.0, 1.0) + }; + let weight = if has_value { text::Weight::Bold } else { text::Weight::Normal }; + label.set_property_default(color); + label.set_property_default(weight); + }); } frp::extend! { network diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index b6922255adab..352671b71906 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -1404,6 +1404,20 @@ impl ParentBind { fn parent(&self) -> Option { self.parent.upgrade() } + + /// Drop this parent bind without removing the child from the parent's children list, assuming + /// it has been already done. Does not mark `modified_children` dirty flag, as the `child_index` + /// is already outdated. + fn drop_manually_removed_from_child_list(self, weak_child: WeakInstance) { + if let Some(parent) = self.parent() { + if let Some(child) = weak_child.upgrade() { + child.dirty.new_parent.set(); + } + parent.dirty.removed_children.set(weak_child); + } + // do not perform usual drop + mem::forget(self); + } } impl Drop for ParentBind { @@ -1413,8 +1427,8 @@ impl Drop for ParentBind { parent.dirty.modified_children.unset(&self.child_index); if let Some(child) = weak_child.upgrade() { child.dirty.new_parent.set(); - parent.dirty.removed_children.set(weak_child); } + parent.dirty.removed_children.set(weak_child); } } } @@ -1457,6 +1471,13 @@ impl SharedParentBind { self.data.borrow().as_ref().and_then(|t| t.parent().map(|s| (s, t.child_index))) } + fn matches(&self, parent: &WeakInstance, child_index: ChildIndex) -> bool { + self.data + .borrow() + .as_ref() + .map_or(false, |t| t.child_index == child_index && &t.parent == parent) + } + fn child_index(&self) -> Option { self.data.borrow().as_ref().map(|t| t.child_index) } @@ -1937,7 +1958,7 @@ impl Model { if self.dirty.modified_children.check_all() { debug_span!("Updating dirty children.").in_scope(|| { self.dirty.modified_children.take().iter().for_each(|ix| { - self.children.borrow().get(&ix).and_then(|t| t.upgrade()).for_each( + self.children.borrow().get(ix).and_then(|t| t.upgrade()).for_each( |child| { child.update_with_origin( scene, @@ -2037,9 +2058,92 @@ impl InstanceDef { children.into_iter().for_each(|child| self.add_child(child.display_object())); } - fn replace_children(&self, children: impl IntoIterator) { - self.remove_all_children(); - self.add_children(children); + /// Replace children with object from the provided list. Objects that are already children of + /// this instance will be moved to the new position. Objects that are not children of this + /// instance will change their parent and will be inserted in the right position. + /// + /// This method avoids unnecessary dirty flag updates and is more efficient than removing all + /// children and adding them again. Children that only swapped their position will be marked + /// as modified, but not as having a new parent. Children that are already under the right index + /// will not be marked as modified. + /// + /// Has no effect if the provided list matches the current children list, as long as the + /// internal child indices were already sequential starting from 0. If that's not the case, + /// the children will be marked as updated. + /// + /// NOTE: If the list contain duplicated objects (instances that are clones of the same ref), + /// the behavior is undefined. It will however not cause any memory unsafety and all objects + /// will remain in some valid state. + fn replace_children(&self, new_children: &[T]) { + let this_weak = self.downgrade(); + + for (index, child) in new_children.iter().enumerate() { + let child = child.display_object(); + let child_index = ChildIndex(index); + + let mut bind_borrow = child.parent_bind.data.borrow_mut(); + if let Some(bind) = bind_borrow.as_mut().filter(|bind| bind.parent == this_weak) { + // The child is already a child of this instance. Set the new index. + if bind.child_index != child_index { + self.dirty.modified_children.unset(&bind.child_index); + self.dirty.modified_children.set(child_index); + bind.child_index = child_index; + } + } else { + // This was not a child of this instance. Set the new parent. + drop(bind_borrow); + child.take_parent_bind(); + let new_parent_bind = ParentBind { parent: this_weak.clone(), child_index }; + child.set_parent_bind(new_parent_bind); + self.dirty.modified_children.set(child_index); + } + } + + let mut borrow = self.children.borrow_mut(); + // Drop all children that are not in the new list. + { + for (child_index, weak_instance) in borrow.iter() { + let index = child_index.0; + if let Some(instance) = weak_instance.upgrade() { + let bind_index = instance.parent_bind.child_index(); + if bind_index != Some(*child_index) { + // Bind index updated in the loop above. This means that the child has to + // be preserved. + continue; + } + let child = new_children.get(index); + let matching = child.map_or(false, |c| c.display_object() == &instance); + + if matching { + // The child under this index is the same as the one in the new list. We + // want to preserve it. + continue; + } + + // For all other children, we want to drop them, but making sure to not mark + // any newly inserted children as removed. This can happen, if existing element + // has ben moved to an index occupied by an element we are about to remove. + if let Some(bind) = instance.take_parent_bind() { + if bind.child_index.0 >= new_children.len() { + self.dirty.modified_children.unset(&bind.child_index); + } + bind.drop_manually_removed_from_child_list(weak_instance.clone()); + } + } + } + } + + // Fill in the child list. + borrow.clear(); + for (index, child) in new_children.iter().enumerate() { + let child = child.display_object(); + let child_index = ChildIndex(index); + // Check again if the parent bind is is still in expected state. If the children list + // contained any duplicates, we don't want to insert them twice. + if child.parent_bind.matches(&this_weak, child_index) { + borrow.insert(ChildIndex(index), child.downgrade()); + } + } } fn register_child(&self, child: &InstanceDef) -> ChildIndex { @@ -3949,7 +4053,7 @@ pub trait ObjectOps: Object + AutoLayoutOps + LayoutOps { self.display_object().def.add_children(children); } - fn replace_children(&self, children: impl IntoIterator) { + fn replace_children(&self, children: &[T]) { self.display_object().def.replace_children(children); } From 75722738351cd4e272b00156007bd6c5bd8008ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Wed, 12 Apr 2023 12:55:44 +0200 Subject: [PATCH 08/45] handle widget offsets at port level, add port padding --- .../src/component/node/input/widget.rs | 106 +++++++++++++----- .../src/component/node/input/widget/debug.rs | 5 +- .../component/node/input/widget/hierarchy.rs | 7 -- .../node/input/widget/insertion_point.rs | 38 +++++++ .../src/component/node/input/widget/label.rs | 9 +- .../node/input/widget/single_choice.rs | 43 +++---- .../core/src/display/object/instance.rs | 56 +++++++-- 7 files changed, 186 insertions(+), 78 deletions(-) create mode 100644 app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 461af81eaf2d..2422b45557ea 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -18,6 +18,22 @@ use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use text::index::Byte; + + +// ================= +// === Constants === +// ================= + +/// The horizontal padding of ports. It affects how the port hover should extend the target text +/// boundary on both sides. +pub const PORT_PADDING_X: f32 = 4.0; + +/// Width of a single space glyph. Used to calculate padding based on the text offset between nodes. +// TODO: avoid using hardcoded value. See https://www.pivotaltracker.com/story/show/183567623. +pub const SPACE_GLYPH_WIDTH: f32 = 7.224_609_4; + + + // =========== // === FRP === // =========== @@ -59,6 +75,10 @@ pub struct MetadataPointer { pub trait SpanWidget { /// Configuration associated with specific widget variant. type Config: Debug + Clone + PartialEq; + /// Spacing between this widget and previous sibling. + fn sibling_offset(&self, config: &Self::Config, ctx: ConfigContext) -> usize { + ctx.span_tree_node.sibling_offset.as_usize() + } /// Root display object of a widget. It is returned to the parent widget for positioning. fn root_object(&self) -> &display::object::Instance; /// Create a new widget with given configuration. @@ -139,6 +159,8 @@ macro_rules! define_widget_modules( define_widget_modules! { /// Default widget that only displays text. Label label, + /// Empty widget that does not display anything, used for empty insertion points. + InsertionPoint insertion_point, /// A widget for selecting a single value from a list of available options. SingleChoice single_choice, /// A widget for managing a list of values - adding, removing or reordering them. @@ -199,6 +221,8 @@ impl Metadata { }, Kind::Root | Kind::NamedArgument | Kind::Chained(_) if has_children => Self::always(hierarchy::Config), + Kind::InsertionPoint(inner) if inner.name.is_none() => + Self::always(insertion_point::Config), _ => Self::always(label::Config::default()), } } @@ -631,16 +655,16 @@ impl<'a> WidgetTreeBuilder<'a> { match old_widget { Some(mut port) => { - port.widget.configure(&meta.config, ctx); - port.update_root(); + port.configure(&meta.config, ctx); port } None => { let app = ctx.app(); let frp = ctx.frp(); - let mut widget = DynWidget::new(&meta.config, app, frp); - widget.configure(&meta.config, ctx); - Port::new(widget) + let widget = DynWidget::new(&meta.config, app, frp); + let mut port = Port::new(widget); + port.configure(&meta.config, ctx); + port } } }; @@ -657,14 +681,6 @@ impl<'a> WidgetTreeBuilder<'a> { } -// ================= -// === Constants === -// ================= - -/// The horizontal padding of ports. It affects how the port hover should extend the target text -/// boundary on both sides. -pub const PADDING_X: f32 = 4.0; - // ============ // === Port === @@ -674,53 +690,83 @@ pub const PADDING_X: f32 = 4.0; pub mod port { use super::*; ensogl::shape! { + pointer_events = false; (style:Style, color:Vector4) { let size = Var::canvas_size(); let transparent = Var::::from("srgba(1.0,1.0,1.0,0.00001)"); let shape_color = Var::::from(color); - let hover_shape = Rect(&size).fill(transparent); + // let hover_shape = Rect(&size).fill(transparent); let visual_shape = Rect(&size).corners_radius(size.y() / 2.0).fill(shape_color); - hover_shape.union(visual_shape).into() + visual_shape.into() + // hover_shape.union(visual_shape).into() } } } #[derive(Debug)] pub(super) struct Port { - shape: port::View, - widget: DynWidget, - inner: display::object::Instance, + last_offset: usize, + port_root: display::object::Instance, + widget_root: display::object::Instance, + widget: DynWidget, + port_shape: port::View, } impl Port { fn new(widget: DynWidget) -> Self { - let shape = port::View::new(); - shape.color.set(color::Rgba::transparent().into()); - let inner = widget.root_object().clone_ref(); - shape.add_child(&inner); - Self { shape, widget, inner } + let port_root = display::object::Instance::new(); + let widget_root = widget.root_object().clone_ref(); + let port_shape = port::View::new(); + port_shape.color.set(color::Rgba::transparent().into()); + + port_root.add_child(&widget_root); + port_root.add_child(&port_shape); + port_root.set_alignment_left(); + port_shape + .allow_grow() + .set_margin_left(-PORT_PADDING_X) + .set_margin_right(-PORT_PADDING_X) + .set_alignment_left(); + + Self { port_shape, widget, widget_root, last_offset: 0, port_root } + } + + fn configure(&mut self, config: &Config, ctx: ConfigContext) { + let is_placeholder = ctx.span_tree_node.is_expected_argument(); + let min_offset = if is_placeholder { 1 } else { 0 }; + let offset = min_offset.max(ctx.span_tree_node.sibling_offset.as_usize()); + self.widget.configure(config, ctx); + self.update_root(); + self.set_offset(offset); } fn update_root(&mut self) { let new_root = self.widget.root_object(); - if new_root != &self.inner { - self.shape.remove_child(&self.inner); - self.shape.add_child(new_root); - self.inner = new_root.clone_ref(); + if new_root != &self.widget_root { + self.port_root.remove_child(&self.widget_root); + self.port_root.add_child(new_root); + self.widget_root = new_root.clone_ref(); + } + } + + fn set_offset(&mut self, offset: usize) { + if self.last_offset != offset { + self.last_offset = offset; + self.port_root.set_margin_left(offset as f32 * SPACE_GLYPH_WIDTH); } } fn set_connected(&self, status: ConnectionStatus) { match status { ConnectionStatus::Connected { color } => - self.shape.color.set(color::Rgba::from(color).into()), - ConnectionStatus::Disconnected => self.shape.color.modify(|c| c.xyz().push(0.0)), + self.port_shape.color.set(color::Rgba::from(color).into()), + ConnectionStatus::Disconnected => self.port_shape.color.modify(|c| c.xyz().push(0.0)), }; } } impl display::Object for Port { fn display_object(&self) -> &display::object::Instance { - self.shape.display_object() + self.port_root.display_object() } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs b/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs index 43b517d1a5c8..46243692ab40 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs @@ -12,7 +12,9 @@ mod shape { (style: Style, color: Vector4) { let color = Var::::from(color); let shape = Rect(Var::canvas_size()).fill(color); - shape.into() + let inner = shape.shrink(2.0.px()); + let border = shape - inner; + border.into() } } } @@ -40,6 +42,7 @@ impl InstanceWithBg { pub fn with_color(color: color::Rgba) -> Self { let bg = shape::View::new(); let outer = bg.display_object().clone(); + // let outer = display::object::Instance::new(); let inner = display::object::Instance::new(); // bg.allow_grow(); bg.color.set(color.into()); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs index a048d82cbe04..12fc9c95c46e 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -21,10 +21,6 @@ pub struct Widget { display_object: super::debug::InstanceWithBg, } -/// Width of a single space glyph -// TODO: avoid using hardcoded value. See https://www.pivotaltracker.com/story/show/183567623. -pub const SPACE_GLYPH_WIDTH: f32 = 7.224_609_4; - impl super::SpanWidget for Widget { type Config = Config; @@ -40,9 +36,6 @@ impl super::SpanWidget for Widget { } fn configure(&mut self, _: &Config, ctx: super::ConfigContext) { - let offset = ctx.span_tree_node.sibling_offset.as_usize() as f32; - self.display_object.inner.set_padding_left(offset * SPACE_GLYPH_WIDTH); - let preserve_depth = ctx.span_tree_node.is_chained() || ctx.span_tree_node.is_named_argument(); let next_depth = if preserve_depth { ctx.depth } else { ctx.depth + 1 }; diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs b/app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs new file mode 100644 index 000000000000..69c51ab98bcc --- /dev/null +++ b/app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs @@ -0,0 +1,38 @@ +//! Definition of empty widget that represents insertion point. + +use crate::prelude::*; + +use ensogl::application::Application; +use ensogl::display::object; + + + +// ====================== +// === InsertionPoint === +// ====================== + +/// Insertion point widget configuration options. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Config; + + +/// Insertion point widget. Displays nothing. +#[derive(Clone, Debug)] +pub struct Widget { + root: object::Instance, +} + +impl super::SpanWidget for Widget { + type Config = Config; + + fn root_object(&self) -> &object::Instance { + &self.root + } + + fn new(_: &Config, _: &Application, _: &super::WidgetsFrp) -> Self { + let root = object::Instance::new(); + Self { root } + } + + fn configure(&mut self, _: &Config, _: super::ConfigContext) {} +} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 1a843542ebba..671bd3829ee2 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -54,6 +54,8 @@ impl super::SpanWidget for Widget { let frp = Frp::new(); let network = &frp.network; + let inner = root.inner.clone_ref(); + frp::extend! { network color_change <- frp.text_color.on_change(); eval color_change((color) label.set_property_default(color)); @@ -61,6 +63,9 @@ impl super::SpanWidget for Widget { eval content_change([label] (content) { label.set_content(content); }); + + width <- label.width.on_change(); + eval width((w) { inner.set_size_x(*w); }); } Self { frp, root, label } @@ -68,10 +73,6 @@ impl super::SpanWidget for Widget { fn configure(&mut self, _: &Config, ctx: super::ConfigContext) { let is_placeholder = ctx.span_tree_node.is_expected_argument(); - let min_offset = if is_placeholder { 1.0f32 } else { 0.0 }; - let offset = min_offset.max(ctx.span_tree_node.sibling_offset.as_usize() as f32); - - self.label.set_x(offset * super::hierarchy::SPACE_GLYPH_WIDTH); let content = if is_placeholder { ctx.span_tree_node.kind.argument_name().unwrap_or_default() diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index d024186d1ed4..9e69c38bd13c 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -2,6 +2,7 @@ use crate::prelude::*; +use super::debug::InstanceWithBg; use crate::component::node::input::area::TEXT_SIZE; use crate::component::node::input::widget::Entry; use enso_frp as frp; @@ -81,7 +82,7 @@ ensogl::define_endpoints_2! { #[allow(dead_code)] pub struct Widget { config_frp: Frp, - display_object: display::object::Instance, + display_object: InstanceWithBg, content_wrapper: display::object::Instance, dropdown_wrapper: display::object::Instance, label_wrapper: display::object::Instance, @@ -95,7 +96,7 @@ impl super::SpanWidget for Widget { type Config = Config; fn root_object(&self) -> &display::object::Instance { - &self.display_object + &self.display_object.outer } fn new(_: &Config, app: &Application, widgets_frp: &super::WidgetsFrp) -> Self { @@ -119,7 +120,6 @@ impl super::SpanWidget for Widget { let label = text::Text::new(app); layers.above_nodes_text.add(&label); label.set_property_default(text::Size(TEXT_SIZE)); - label.set_property_default(text::Weight::Bold); label.set_y(TEXT_SIZE); let dropdown = app.new_view::>(); @@ -128,17 +128,18 @@ impl super::SpanWidget for Widget { dropdown.set_max_open_size(Vector2(300.0, 500.0)); dropdown.allow_deselect_all(true); - let display_object = display::object::Instance::new(); - let content_wrapper = display_object.new_child(); + let display_object = InstanceWithBg::magenta(); + let content_wrapper = display_object.inner.new_child(); content_wrapper.add_child(&activation_shape); let label_wrapper = content_wrapper.new_child(); label_wrapper.add_child(&label); let args_wrapper = content_wrapper.new_child(); - let dropdown_wrapper = display_object.new_child(); + let dropdown_wrapper = display_object.inner.new_child(); dropdown_wrapper.add_child(&dropdown); display_object + .inner .use_auto_layout() .set_column_flow() .set_children_alignment_left_center() @@ -163,14 +164,14 @@ impl super::SpanWidget for Widget { let network = &config_frp.network; let input = &config_frp.private.input; - let focus_receiver = display_object.clone_ref(); + let focus_receiver = display_object.inner.clone_ref(); frp::extend! { network + init <- source::<()>(); + let focus_in = focus_receiver.on_event::(); let focus_out = focus_receiver.on_event::(); is_open <- bool(&focus_out, &focus_in); - } - frp::extend! { network let dot_mouse_down = activation_shape.on_event::(); dot_clicked <- dot_mouse_down.filter(mouse::is_primary); @@ -179,8 +180,6 @@ impl super::SpanWidget for Widget { true => focus_receiver.focus(), false => focus_receiver.blur(), }); - } - frp::extend! { network // Close the dropdown after a short delay after selection. Because the dropdown // value application triggers operations that can introduce a few dropped frames, @@ -189,19 +188,15 @@ impl super::SpanWidget for Widget { let close_after_selection_timer = frp::io::timer::Timeout::new(&network); close_after_selection_timer.restart <+ dropdown.user_select_action.constant(1); eval close_after_selection_timer.on_expired((()) focus_receiver.blur()); - } - frp::extend! { network + current_value <- input.current_value.on_change(); entries <- input.set_entries.on_change(); entries_and_value <- all(&entries, ¤t_value); entries_and_value <- entries_and_value.debounce(); entries_and_value <- entries_and_value.buffered_gate(&is_open); - } - frp::extend! { network + dropdown.set_all_entries <+ entries_and_value.map(|(rc, _)| rc.deref().clone()); dropdown.set_open <+ is_open.on_change(); - } - frp::extend! { network selected_entry <- entries_and_value.map(|(e, v)| entry_for_current_value(e, v)); dropdown.set_selected_entries <+ selected_entry.map(|e| e.iter().cloned().collect()); @@ -215,7 +210,11 @@ impl super::SpanWidget for Widget { dropdown_out_import <- submitted_entry.map(|e| e.as_ref().and_then(Entry::required_import)); label.set_content <+ input.content.on_change(); - has_value <- entries_and_value.map(|(_, v)| v.is_some()).on_change(); + label_width <- label.width.on_change(); + eval label_width((w) { label_wrapper.set_size_x(*w); }); + + has_value <- current_value.map(|v| v.is_some()); + has_value <- all(&has_value, &init)._0(); eval has_value([label] (&has_value) { let color = if has_value { color::Lcha::new(0.0, 0.0, 0.0, 1.0) @@ -226,8 +225,6 @@ impl super::SpanWidget for Widget { label.set_property_default(color); label.set_property_default(weight); }); - } - frp::extend! { network widgets_frp.request_import <+ dropdown_out_import.unwrap(); widgets_frp.value_changed <+ dropdown_out_value.map2(&input.current_crumbs, @@ -235,6 +232,7 @@ impl super::SpanWidget for Widget { ); }; + init.emit(()); Self { config_frp, @@ -252,11 +250,6 @@ impl super::SpanWidget for Widget { fn configure(&mut self, config: &Config, ctx: super::ConfigContext) { let input = &self.config_frp.public.input; - let is_placeholder = ctx.span_tree_node.is_expected_argument(); - let min_offset = if is_placeholder { 1.0f32 } else { 0.0 }; - let offset = min_offset.max(ctx.span_tree_node.sibling_offset.as_usize() as f32); - self.display_object.set_margin_left(offset * super::hierarchy::SPACE_GLYPH_WIDTH); - let has_value = !ctx.span_tree_node.is_insertion_point(); let current_value: Option = has_value.then(|| ctx.expression_at(ctx.span_tree_node.span()).into()); diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index 352671b71906..883f9d3b1e64 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -2827,10 +2827,11 @@ pub trait LayoutOps: Object { right: impl Into, bottom: impl Into, left: impl Into, - ) { + ) -> &Self { let horizontal = SideSpacing::new(left.into(), right.into()); let vertical = SideSpacing::new(bottom.into(), top.into()); self.display_object().layout.margin.set(Vector2(horizontal, vertical)); + self } /// Set padding of all sides of the object. Padding is the free space inside the object. @@ -3402,7 +3403,9 @@ impl Model { } else { let child_pos = child.position().get_dim(x); let child_size = child.computed_size().get_dim(x); - max_x = max_x.max(child_pos + child_size); + let child_margin = + child.layout.margin.get_dim(x).end.resolve_pixels_or_default(); + max_x = max_x.max(child_pos + child_size + child_margin); } } else { has_grow_children = true; @@ -3420,10 +3423,11 @@ impl Model { let to_grow = child.layout.grow_factor.get_dim(x) > 0.0; if let Some(alignment) = *child.layout.alignment.get().get_dim(x) && !to_grow { let child_size = child.computed_size().get_dim(x); - let remaining_size = base_size - child_size; - let aligned_position = remaining_size * alignment.normalized(); - child.set_position_dim(x, aligned_position); - max_x = max_x.max(aligned_position + child_size); + let child_margin = child.layout.margin.get_dim(x).resolve_pixels_or_default(); + let remaining_size = base_size - child_size - child_margin.total(); + let aligned_x = remaining_size * alignment.normalized() + child_margin.start; + child.set_position_dim(x, aligned_x); + max_x = max_x.max(aligned_x + child_size + child_margin.end); } } if hug_children { @@ -3438,16 +3442,20 @@ impl Model { for child in &children { let to_grow = child.layout.grow_factor.get_dim(x) > 0.0; if to_grow { - let current_child_size = child.computed_size().get_dim(x); - if self_size != current_child_size || child.should_refresh_layout() { - child.layout.computed_size.set_dim(x, self_size); + let child_size = child.computed_size().get_dim(x); + let child_margin = child.layout.margin.get_dim(x).resolve_pixels_or_default(); + let desired_child_size = self_size - child_margin.total(); + + if desired_child_size != child_size || child.should_refresh_layout() { + child.layout.computed_size.set_dim(x, desired_child_size); child.refresh_layout_internal(x, PassConfig::DoNotHugDirectChildren); } if child.layout.alignment.get().get_dim(x).is_some() { // If child is set to grow, there will never be any leftover space to align - // it. It should always be positioned at 0.0 relative to its parent. - child.set_position_dim(x, 0.0); + // it. It should always be positioned at 0.0 relative to its parent, plus + // taking the margin into account. + child.set_position_dim(x, child_margin.start); } } } @@ -5972,6 +5980,32 @@ mod layout_tests { }); } + #[test] + fn test_manual_layout_margin_alignment() { + let test = TestFlatChildren3::new(); + test.root.set_size((10.0, 10.0)); + test.node1.allow_grow().set_margin_trbl(1.0, 2.0, 3.0, 4.0).set_alignment_left_bottom(); + test.node2 + .set_size((3.0, 3.0)) + .set_margin_trbl(1.0, 2.0, 3.0, 4.0) + .set_alignment_left_center(); + test.node3 + .set_size((3.0, 3.0)) + .set_margin_trbl(1.0, 2.0, 3.0, 4.0) + .set_alignment_right_bottom(); + + test.run(|| { + test.assert_root_computed_size(10.0, 10.0) + .assert_node1_computed_size(4.0, 6.0) + .assert_node2_computed_size(3.0, 3.0) + .assert_node3_computed_size(3.0, 3.0) + .assert_root_position(0.0, 0.0) + .assert_node1_position(4.0, 3.0) + .assert_node2_position(4.0, 4.5) + .assert_node3_position(5.0, 3.0); + }); + } + /// ```text /// ╭─root─────────────╮ /// │╭─node1──╮ │ From 651db04bcc3dae2694238f2176e7e627415d13f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Thu, 13 Apr 2023 13:27:13 +0200 Subject: [PATCH 09/45] wip port mouse handling --- .../src/component/node/input/area.rs | 436 +----------------- .../src/component/node/input/widget.rs | 141 ++++-- .../component/node/input/widget/hierarchy.rs | 5 +- 3 files changed, 113 insertions(+), 469 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 4c8517c22861..d6730e0b144b 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -260,432 +260,6 @@ impl Model { self.root_widget.rebuild_tree_on_metadata_change(&expr.span_tree, &expr.code); } - // #[profile(Debug)] - // fn build_port_shapes_on_new_expression( - // &self, - // expression: &mut Expression, - // area_frp: &FrpEndpoints, - // call_info: &CallInfoMap, - // ) { - // let mut is_header = true; - - // expression.span_tree.root_ref_mut().dfs_with_layer_data(builder, |mut node, builder| { - // let skip_opr = if SKIP_OPERATIONS { - // node.is_operation() && !is_header - // } else { - // let crumb = ast::Crumb::Infix(ast::crumbs::InfixCrumb::Operator); - // node.ast_crumbs.last().map(|t| t == &crumb) == Some(true) - // }; - - // let not_a_port = node.is_positional_insertion_point() - // || node.is_chained() - // || (node.is_root() && !node.children.is_empty()) - // || skip_opr - // || node.is_token() - // || node.is_named_argument() - // || builder.parent_parensed; - - // if let Some(id) = node.ast_id { - // if DEBUG { - // debug!("New id mapping: {id} -> {:?}", node.crumbs); - // } - // id_crumbs_map.insert(id, node.crumbs.clone_ref()); - // } - - // if DEBUG { - // let indent = " ".repeat(4 * builder.depth); - // let skipped = if not_a_port { "(skip)" } else { "" }; - // debug!( - // "{indent}[{},{}] {skipped} {:?} (tp: {:?}) (id: {:?})", - // node.payload.index, - // node.payload.length, - // node.kind.variant_name(), - // node.tp(), - // node.ast_id - // ); - // } - - // let range_before_start = node.payload.index - node.payload.local_index; - // let range_before_end = node.payload.index; - // let range_before = enso_text::Range::new(range_before_start, range_before_end); - // let local_char_offset = code[range_before].chars().count(); - - // let new_parent = if not_a_port { - // builder.parent.clone_ref() - // } else { - // let crumbs = node.crumbs.clone_ref(); - // let port = &mut node; - - // let index = local_char_offset + builder.shift; - // let size = code[port.payload.range()].chars().count(); - // let unit = GLYPH_WIDTH; - // let width = unit * size as f32; - // let width_padded = width + 2.0 * PORT_PADDING_X; - // let height = 18.0; - // let size = Vector2(width, height); - // let padded_size = Vector2(width_padded, height); - // let position_x = unit * index as f32; - - // let port_shape = port.payload.init_shape(size, node::HEIGHT); - - // port_shape.set_x(position_x); - // if DEBUG { - // port_shape.set_y(DEBUG_PORT_OFFSET); - // } - - // if is_header { - // is_header = false; - // self.header.add_child(&port_shape); - // } else { - // builder.parent.add_child(&port_shape); - // } - - // // TODO: StyleWatch is unsuitable here, as it was designed as an internal tool - // for // shape system. (https://www.pivotaltracker.com/story/show/183567648) - // let style_sheet = &self.app.display.default_scene.style_sheet; - // let styles = StyleWatch::new(style_sheet); - // let styles_frp = &self.styles_frp; - // let any_type_sel_color = - // styles_frp.get_color(theme::code::types::any::selection); let - // port_network = &port.network; - - // frp::extend! { port_network - - // // === Aliases === - - // let mouse_over_raw = port_shape.hover.events.mouse_over.clone_ref(); - // let mouse_out = port_shape.hover.events.mouse_out.clone_ref(); - // let mouse_down_raw = port_shape.hover.events.mouse_down_primary.clone_ref(); - - // // === Body Hover === - - // // This is meant to be on top of FRP network. Read more about `Node` docs to - // // learn more about the architecture and the importance of the hover - // // functionality. - - // // Please note, that this is computed first in order to compute - // `ports_visible` // when needed, and thus it has to be run before the - // following lines. area_frp.source.body_hover <+ - // bool(&mouse_out,&mouse_over_raw); - - // // TODO[WD] for FRP3: Consider the following code. Here, we have to first - // // handle `bg_down` and then `mouse_down`. Otherwise, `mouse_down` may - // // trigger some events and can change `ports_visible` status, and thus - // make // the `bg_down` emitted unnecessarily. For example, after - // plugging in // connections to selected port, the `ports_visible` - // will be set to `false`, // and `bg_down` will be emitted, causing - // the node to be selected. This can // be solved by solving in the - // FRP engine all children first, and then their // children (then - // both `bg_down` and `mouse_down` will be resolved before // the - // `ports_visible` changes). bg_down <- - // mouse_down_raw.gate_not(&area_frp.ports_visible); mouse_down <- - // mouse_down_raw.gate(&area_frp.ports_visible); mouse_over <- - // mouse_over_raw.gate(&area_frp.ports_visible); - // area_frp.source.on_background_press <+ bg_down; - - // // === Press === - - // area_frp.source.on_port_press <+ mouse_down.map(f_!([crumbs] - // crumbs.clone_ref())); - - // // === Hover === - - // hovered <- bool(&mouse_out,&mouse_over); - // hover <- hovered.map (f!([crumbs](t) Switch::new(crumbs.clone_ref(),*t))); - // area_frp.source.on_port_hover <+ hover; - - // // === Pointer Style === - - // let port_shape_hover = port_shape.hover.clone_ref(); - // pointer_style_out <- mouse_out.map(|_| default()); - - // init_color <- source::<()>(); - // any_type_sel_color <- all_with(&any_type_sel_color,&init_color, - // |c,_| color::Lcha::from(c)); - // tp <- all_with(&port.tp,&area_frp.set_ports_active, - // |tp,(_,edge_tp)| tp.clone().or_else(||edge_tp.clone())); - // tp_color <- tp.map( - // f!([styles](tp) tp.map_ref(|tp| type_coloring::compute(tp,&styles)))); - // tp_color <- all_with(&tp_color,&any_type_sel_color, - // |tp_color,any_type_sel_color| tp_color.unwrap_or(*any_type_sel_color)); - // in_profiling_mode <- area_frp.view_mode.map(|m| - // matches!(m,view::Mode::Profiling)); pointer_color_over <- - // in_profiling_mode.switch(&tp_color,&any_type_sel_color); - // pointer_style_over <- pointer_color_over.map(move |color| - // cursor::Style::new_highlight(&port_shape_hover,padded_size,Some(color)) - // ); - // pointer_style_over <- pointer_style_over.sample(&mouse_over); - - // pointer_style_hover <- any(pointer_style_over,pointer_style_out); - // pointer_styles <- all[ - // pointer_style_hover, - // self.ports_label.pointer_style, - // self.edit_mode_label.pointer_style - // ]; - // pointer_style <- pointer_styles.fold(); - // area_frp.source.pointer_style <+ pointer_style; - // } - - // if let Some((widget_bind, widget)) = self.init_port_widget(port, size, call_info) - // { widgets_map.insert(widget_bind, crumbs.clone_ref()); - // widget.set_x(position_x); - // builder.parent.add_child(&widget); - - // if port.is_argument() { - // let range = port.span(); - // let code = &expression.code[range]; - // debug!("Setting current value while range is {range:?}, code is - // \"{code}\" \ and full expression is \"{}\".", - // expression.code); widget.set_current_value(Some(code.into())); - // } else { - // widget.set_current_value(None); - // } - // widget.set_visible(true); - - // let port_network = &port.network; - // frp::extend! { port_network - // code_update <- widget.value_changed.map(f!([crumbs](value) { - // let expression = value.clone().unwrap_or_default(); - // (crumbs.clone_ref(), expression) - // })); - // area_frp.source.on_port_code_update <+ code_update; - // area_frp.source.request_import <+ widget.request_import; - // } - // } - - // init_color.emit(()); - - // port_shape.display_object().clone_ref() - // }; - - // if let Some(parent_frp) = &builder.parent_frp { - // frp::extend! { port_network - // node.frp.set_active <+ parent_frp.set_active; - // node.frp.set_hover <+ parent_frp.set_hover; - // node.frp.set_parent_connected <+ parent_frp.set_parent_connected; - // } - // } - // let new_parent_frp = Some(node.frp.output.clone_ref()); - // let new_shift = if !not_a_port { 0 } else { builder.shift + local_char_offset }; - // let parenthesized = node.parenthesized(); - // builder.nested(new_parent, new_parent_frp, parenthesized, new_shift) - // }); - // *self.id_crumbs_map.borrow_mut() = id_crumbs_map; - // *self.widgets_map.borrow_mut() = widgets_map; - // area_frp.set_view_mode.emit(area_frp.view_mode.value()); - // } - - // fn init_port_widget( - // &self, - // port: &mut PortRefMut, - // port_size: Vector2, - // call_info: &CallInfoMap, - // ) -> Option<(WidgetBind, widget::Root)> { - // let call_id = port.kind.call_id().filter(|id| call_info.has_target(id))?; - // let argument_name = port.kind.argument_name()?.to_owned(); - - // let widget_bind = WidgetBind { call_id, argument_name }; - - - // // Try getting the previous widget by exact target/argument ID first, which is - // // necessary when the argument expression was replaced. This lookup can fail - // // when the target expression was replaced, but the widget argument expression - // // wasn't. In that case, try to reuse the widget from old argument node under - // // the same ast ID. - // let prev_widgets_map = self.widgets_map.borrow(); - // let prev_id_crumbs_map = self.id_crumbs_map.borrow(); - // let prev_crumbs = prev_widgets_map - // .get(&widget_bind) - // .or_else(|| port.ast_id.as_ref().and_then(|id| prev_id_crumbs_map.get(id))); - // let prev_widget = prev_crumbs.and_then(|crumbs| { - // let prev_expression = self.expression.borrow(); - // let prev_root = prev_expression.span_tree.root_ref(); - // let prev_node = prev_root.get_descendant(crumbs).ok()?; - // let prev_widget = prev_node.payload.widget.as_ref()?.clone_ref(); - // Some(prev_widget) - // }); - - // let widget = match prev_widget { - // Some(prev_widget) => port.payload.use_existing_widget(prev_widget), - // None => port.payload.init_widget(&self.app), - // }; - - // let tag_values = port.kind.tag_values().unwrap_or_default().to_vec(); - // // widget.set_node_data(widget::NodeData { tag_values, port_size }); - - // Some((widget_bind, widget)) - // } - - /// Initializes FRP network for every port. Please note that the networks are connected - /// hierarchically (children get events from parents), so it is easier to init all networks - /// this way, rather than delegate it to every port. - // #[profile(Debug)] - // fn init_port_frp_on_new_expression( - // &self, - // expression: &mut Expression, - // area_frp: &FrpEndpoints, - // ) { - // let model = &self; - - // let parent_tp: Option>> = None; - // expression.root_ref_mut().dfs_with_layer_data(parent_tp, |node, parent_tp| { - // let frp = &node.frp; - // let port_network = &frp.network; - // let is_token = node.is_token(); - // let crumbs = node.crumbs.clone(); - - // // === Type Computation === - // let parent_tp = parent_tp.clone().unwrap_or_else(|| { - // frp::extend! { port_network - // empty_parent_tp <- source::>(); - // } - // empty_parent_tp.into() - // }); - // frp::extend! { port_network - // final_tp <- all_with3(&parent_tp,&frp.set_definition_type,&frp.set_usage_type, - // move |parent_tp,def_tp,usage_tp| { - // usage_tp.clone().or_else(|| - // if is_token {parent_tp.clone()} else {def_tp.clone()} - // ) - // } - // ); - // frp.source.tp <+ final_tp; - - // area_frp.source.on_port_type_change <+ frp.tp.map(move - // |t|(crumbs.clone(),t.clone())); } - - // // === Code Coloring === - - // let styles = model.styles.clone_ref(); - // let styles_frp = model.styles_frp.clone_ref(); - - // if node.children.is_empty() { - // let is_expected_arg = node.is_expected_argument(); - - // use theme::code::syntax; - // let selected_color = styles_frp.get_color(theme::code::types::selected); - // let std_base_color = styles_frp.get_color(syntax::base); - // let std_disabled_color = styles_frp.get_color(syntax::disabled); - // let std_expected_color = styles_frp.get_color(syntax::expected); - // let std_editing_color = styles_frp.get_color(syntax::base); - // let profiled_base_color = styles_frp.get_color(syntax::profiling::base); - // let profiled_disabled_color = styles_frp.get_color(syntax::profiling::disabled); - // let profiled_expected_color = styles_frp.get_color(syntax::profiling::expected); - // let profiled_editing_color = styles_frp.get_color(syntax::profiling::base); - - // frp::extend! { port_network - // in_profiling_mode <- area_frp.view_mode.map(|m| m.is_profiling()); - // finished <- area_frp.set_profiling_status.map(|s| s.is_finished()); - // profiled <- in_profiling_mode && finished; - // selected <- frp.set_hover || frp.set_parent_connected; - - // init_colors <- source::<()>(); - // std_base_color <- all(std_base_color,init_colors)._0(); - // profiled_base_color <- all(profiled_base_color,init_colors)._0(); - - // profiling_color <- finished.switch(&std_base_color,&profiled_base_color); - // normal_color <- frp.tp.map(f!([styles](t) - // color::Rgba::from(type_coloring::compute_for_code(t.as_ref(),&styles)))); - // base_color <- in_profiling_mode.switch(&normal_color,&profiling_color); - - // disabled_color <- - // profiled.switch(&std_disabled_color,&profiled_disabled_color); - // expected_color <- profiled.switch(&std_expected_color,&profiled_expected_color); - // editing_color <- - // profiled.switch(&std_editing_color,&profiled_editing_color); // TODO: - // `label_color` should be animated, when when we can set text colors // more efficiently. (See https://www.pivotaltracker.com/story/show/183567665) - // label_color <- all_with8( - // &area_frp.editing, - // &selected, - // &area_frp.set_disabled, - // &editing_color, - // &selected_color, - // &disabled_color, - // &expected_color, - // &base_color, - // move |&editing, - // &selected, - // &disabled, - // &editing_color, - // &selected_color, - // &disabled_color, - // &expected_color, - // &base_color| { - // if editing { - // color::Lcha::from(editing_color) - // } else if selected { - // color::Lcha::from(selected_color) - // } else if disabled { - // color::Lcha::from(disabled_color) - // } else if is_expected_arg { - // color::Lcha::from(expected_color) - // } else { - // color::Lcha::from(base_color) - // } - // }, - // ); - // } - - // let index = node.payload.index; - // let length = node.payload.length; - // let label = model.ports_label.clone_ref(); - // frp::extend! { port_network - // eval label_color ([label](color) { - // let range = enso_text::Range::new(index, index + length); - // // TODO: remove unwrap. (https://www.pivotaltracker.com/story/show/183567590) - // let range = enso_text::Range::::try_from(range).unwrap(); - // label.set_property(range,color::Rgba::from(color)); - // }); - // } - - // init_colors.emit(()); - // area_frp.set_view_mode(area_frp.view_mode.value()); - // } - - // // === Highlight Coloring === - - // if let Some(port_shape) = &node.payload.shape { - // let viz_color = color::Animation::new(port_network); - // let any_type_sel_color = - // styles_frp.get_color(theme::code::types::any::selection); - - // frp::extend! { port_network - // normal_viz_color <- all_with(&frp.tp,&frp.set_connected, - // f!([styles](port_tp,(_,edge_tp)) { - // let tp = port_tp.as_ref().or(edge_tp.as_ref()); - // select_color(&styles,tp) - // })); - // init_color <- source::<()>(); - // profiling_viz_color <- all_with(&any_type_sel_color,&init_color, - // |c,_| color::Lcha::from(c)); - // profiling <- area_frp.view_mode.map(|m| m.is_profiling()); - // connected_viz_color <- - // profiling.switch(&normal_viz_color,&profiling_viz_color); is_connected - // <- frp.set_connected.map(|(is_connected,_)| *is_connected); transparent - // <- init_color.constant(color::Lcha::transparent()); viz_color_target - // <- is_connected.switch(&transparent,&connected_viz_color); - - // // We need to make sure that the network contains correct values before we - // // connect the `viz_color` animation. The reason is that the animation will - // // start from the first value that it receives, and during initialization of - // the // network, while some nodes are still set to their defaults, this - // first value // would be incorrect, causing the animation in some cases - // to start from black // (the default color) and animating towards the - // color that we really want to // set. - // init_color.emit(()); - - // viz_color.target <+ viz_color_target; - // eval viz_color.value ((t) - // port_shape.viz.color.set(color::Rgba::from(t).into()) - // ); - // } - // } - // Some(frp.tp.clone_ref().into()) - // }); - - // area_frp.set_view_mode(area_frp.view_mode.value()); - // } - /// Request widgets metadata for all method calls within the expression. #[profile(Debug)] fn request_widgets_metadata(&self, expression: &InputExpression, area_frp: &FrpEndpoints) { @@ -850,8 +424,10 @@ impl Area { // This is meant to be on top of FRP network. Read more about `Node` docs to // learn more about the architecture and the importance of the hover // functionality. - - frp.output.source.body_hover <+ frp.set_hover; + frp.output.source.on_port_hover <+ model.root_widget.on_port_hover; + frp.output.source.on_port_press <+ model.root_widget.on_port_press; + port_hover <- frp.on_port_hover.map(|t| t.is_on()); + frp.output.source.body_hover <+ frp.set_hover || port_hover; // === Cursor setup === @@ -883,7 +459,7 @@ impl Area { port_vis <- all_with(&edit_or_ready, ports_active, |e, (a, _)| !e && *a); frp.output.source.ports_visible <+ port_vis; frp.output.source.editing <+ set_editing; - + model.root_widget.ports_visible <+ frp.ports_visible; // === Label Hover === @@ -903,8 +479,6 @@ impl Area { instance.computed_size().x() })).on_change(); - trace root_widget_width; - padded_edit_label_width <- model.edit_mode_label.width.map(|t| t + 2.0 * TEXT_OFFSET); frp.output.source.width <+ set_editing.switch( diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 2422b45557ea..c9c5332d5c38 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -11,6 +11,7 @@ use enso_config::ARGS; use enso_frp as frp; use enso_text as text; use ensogl::application::Application; +use ensogl::control::io::mouse; use ensogl::data::color; use ensogl::display; use ensogl_component::drop_down::DropdownValue; @@ -40,19 +41,13 @@ pub const SPACE_GLYPH_WIDTH: f32 = 7.224_609_4; ensogl::define_endpoints_2! { Input { - // /// Set the widget's metadata that was received from the language server. It overrides - // /// widget's configuration, even allowing the widget type to be completely changed. When - // /// the metadata is set to `Some` value, the corresponding widget will ignore its span-tree - // /// type. - // set_metadata (MetadataPointer, Option), - - // /// Set the connection status of the port indicated by the breadcrumbs. The optional type - // /// is the type of the edge that was connected or disconnected if the edge was typed. - // set_connected (span_tree::Crumbs, ConnectionStatus), + ports_visible (bool), } Output { value_changed (span_tree::Crumbs, Option), request_import (ImString), + on_port_hover (Switch), + on_port_press (span_tree::Crumbs), } } @@ -219,10 +214,8 @@ impl Metadata { } else { Self::always(label::Config::default()) }, - Kind::Root | Kind::NamedArgument | Kind::Chained(_) if has_children => - Self::always(hierarchy::Config), - Kind::InsertionPoint(inner) if inner.name.is_none() => - Self::always(insertion_point::Config), + Kind::InsertionPoint(_) => Self::always(insertion_point::Config), + _ if has_children => Self::always(hierarchy::Config), _ => Self::always(label::Config::default()), } } @@ -298,8 +291,11 @@ impl DropdownValue for Entry { /// Widget FRP endpoints that can be used by widget views, and go straight to the root. #[derive(Debug, Clone, CloneRef)] pub struct WidgetsFrp { + pub(self) ports_visible: frp::Sampler, pub(self) value_changed: frp::Any<(span_tree::Crumbs, Option)>, pub(self) request_import: frp::Any, + pub(self) on_port_hover: frp::Any>, + pub(self) on_port_press: frp::Any, } @@ -314,7 +310,7 @@ pub struct WidgetsFrp { pub struct Root { #[deref] frp: Frp, - sampled_frp: WidgetsFrp, + widgets_frp: WidgetsFrp, model: Rc, } @@ -332,11 +328,25 @@ impl Root { let frp = Frp::new(); let model = Rc::new(RootModel::new(app)); + let network = &frp.network; + + frp::extend! { network + ports_visible <- frp.ports_visible.sampler(); + } + let value_changed = frp.private.output.value_changed.clone_ref(); let request_import = frp.private.output.request_import.clone_ref(); - let sampled_frp = WidgetsFrp { value_changed, request_import }; + let on_port_hover = frp.private.output.on_port_hover.clone_ref(); + let on_port_press = frp.private.output.on_port_press.clone_ref(); + let widgets_frp = WidgetsFrp { + value_changed, + request_import, + on_port_hover, + on_port_press, + ports_visible, + }; - Self { frp, sampled_frp, model } + Self { frp, widgets_frp, model } } pub fn set_metadata(&self, pointer: MetadataPointer, meta: Option) { @@ -360,7 +370,7 @@ impl Root { } pub fn rebuild_tree(&self, tree: &span_tree::SpanTree, node_expression: &str) { - self.model.rebuild_tree(self.sampled_frp.clone_ref(), tree, node_expression) + self.model.rebuild_tree(self.widgets_frp.clone_ref(), tree, node_expression) } pub fn get_port_display_object( @@ -662,7 +672,7 @@ impl<'a> WidgetTreeBuilder<'a> { let app = ctx.app(); let frp = ctx.frp(); let widget = DynWidget::new(&meta.config, app, frp); - let mut port = Port::new(widget); + let mut port = Port::new(widget, frp); port.configure(&meta.config, ctx); port } @@ -693,51 +703,105 @@ pub mod port { pointer_events = false; (style:Style, color:Vector4) { let size = Var::canvas_size(); - let transparent = Var::::from("srgba(1.0,1.0,1.0,0.00001)"); let shape_color = Var::::from(color); - // let hover_shape = Rect(&size).fill(transparent); - let visual_shape = Rect(&size).corners_radius(size.y() / 2.0).fill(shape_color); + let visual_shape = Rect(&size).corners_radius(size.y() / 2.0).fill(shape_color); visual_shape.into() - // hover_shape.union(visual_shape).into() + } + } +} + +/// Port shape definition. +pub mod port_hover { + use super::*; + ensogl::shape! { + (style:Style) { + let size = Var::canvas_size(); + let instance = Var::::from("srgba(hsva(mod(float(input_global_instance_id + input_mouse_click_count % 10) * 17.0, 100.0) / 100.0, 1.0, 1.0, 0.5))"); + // let transparent = Var::::from("srgba(1.0,1.0,1.0,0.00001)"); + let hover_shape = Rect(&size).fill(instance); + hover_shape.into() } } } #[derive(Debug)] pub(super) struct Port { - last_offset: usize, port_root: display::object::Instance, widget_root: display::object::Instance, widget: DynWidget, port_shape: port::View, + #[allow(dead_code)] + hover_shape: port_hover::View, + #[allow(dead_code)] + network: frp::Network, + crumbs: Rc>, + last_offset: usize, } impl Port { - fn new(widget: DynWidget) -> Self { + fn new(widget: DynWidget, frp: &WidgetsFrp) -> Self { let port_root = display::object::Instance::new(); let widget_root = widget.root_object().clone_ref(); let port_shape = port::View::new(); - port_shape.color.set(color::Rgba::transparent().into()); + let hover_shape = port_hover::View::new(); port_root.add_child(&widget_root); - port_root.add_child(&port_shape); port_root.set_alignment_left(); + port_root.set_z(-1.0); port_shape .allow_grow() .set_margin_left(-PORT_PADDING_X) .set_margin_right(-PORT_PADDING_X) .set_alignment_left(); + hover_shape + .allow_grow() + .set_margin_left(-PORT_PADDING_X) + .set_margin_right(-PORT_PADDING_X) + .set_alignment_left(); + + let mouse_enter = hover_shape.on_event::(); + let mouse_leave = hover_shape.on_event::(); + let mouse_down = hover_shape.on_event::(); - Self { port_shape, widget, widget_root, last_offset: 0, port_root } + let crumbs = Rc::new(RefCell::new(span_tree::Crumbs::default())); + + if frp.ports_visible.value() { + port_root.add_child(&hover_shape); + } + + frp::new_network! { network + hovering <- bool(&mouse_leave, &mouse_enter); + trace hovering; + frp.on_port_hover <+ hovering.map( + f!([crumbs](t) Switch::new(crumbs.borrow().clone(),*t)) + ); + frp.on_port_press <+ mouse_down.map(f!((_) crumbs.borrow().clone())); + eval frp.ports_visible([port_root, hover_shape] (active) { + if *active { + port_root.add_child(&hover_shape); + } else { + port_root.remove_child(&hover_shape); + } + }); + }; + + Self { + port_shape, + hover_shape, + widget, + widget_root, + port_root, + network, + crumbs, + last_offset: 0, + } } fn configure(&mut self, config: &Config, ctx: ConfigContext) { - let is_placeholder = ctx.span_tree_node.is_expected_argument(); - let min_offset = if is_placeholder { 1 } else { 0 }; - let offset = min_offset.max(ctx.span_tree_node.sibling_offset.as_usize()); + self.crumbs.replace(ctx.span_tree_node.crumbs.clone()); + self.set_port_layout(&ctx); self.widget.configure(config, ctx); self.update_root(); - self.set_offset(offset); } fn update_root(&mut self) { @@ -749,7 +813,10 @@ impl Port { } } - fn set_offset(&mut self, offset: usize) { + fn set_port_layout(&mut self, ctx: &ConfigContext) { + let is_placeholder = ctx.span_tree_node.is_expected_argument(); + let min_offset = if is_placeholder { 1 } else { 0 }; + let offset = min_offset.max(ctx.span_tree_node.sibling_offset.as_usize()); if self.last_offset != offset { self.last_offset = offset; self.port_root.set_margin_left(offset as f32 * SPACE_GLYPH_WIDTH); @@ -758,9 +825,13 @@ impl Port { fn set_connected(&self, status: ConnectionStatus) { match status { - ConnectionStatus::Connected { color } => - self.port_shape.color.set(color::Rgba::from(color).into()), - ConnectionStatus::Disconnected => self.port_shape.color.modify(|c| c.xyz().push(0.0)), + ConnectionStatus::Connected { color } => { + self.port_root.add_child(&self.port_shape); + self.port_shape.color.set(color::Rgba::from(color).into()) + } + ConnectionStatus::Disconnected => { + self.port_root.remove_child(&self.port_shape); + } }; } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs index 12fc9c95c46e..a9d3ebb96877 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -36,9 +36,8 @@ impl super::SpanWidget for Widget { } fn configure(&mut self, _: &Config, ctx: super::ConfigContext) { - let preserve_depth = - ctx.span_tree_node.is_chained() || ctx.span_tree_node.is_named_argument(); - let next_depth = if preserve_depth { ctx.depth } else { ctx.depth + 1 }; + let increase_depth = ctx.span_tree_node.is_argument(); + let next_depth = if increase_depth { ctx.depth + 1 } else { ctx.depth }; let children = ctx .span_tree_node From 6083603e0c5db1e1425c256a0397f1c6d7a422bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Fri, 14 Apr 2023 17:11:36 +0200 Subject: [PATCH 10/45] edge dragging --- app/gui/language/span-tree/src/iter.rs | 4 - .../src/controller/graph/widget/metadata.rs | 2 +- .../view/graph-editor/src/component/node.rs | 16 +- .../graph-editor/src/component/node/input.rs | 1 + .../src/component/node/input/area.rs | 74 ++-- .../src/component/node/input/port.rs | 252 ++++++++++++ .../src/component/node/input/widget.rs | 364 +++++++----------- .../src/component/node/input/widget/debug.rs | 13 +- .../src/component/node/input/widget/label.rs | 22 +- .../node/input/widget/single_choice.rs | 2 +- app/gui/view/graph-editor/src/lib.rs | 156 ++++---- lib/rust/ensogl/core/src/animation/loops.rs | 12 +- .../core/src/display/object/instance.rs | 5 +- lib/rust/ensogl/core/src/display/scene.rs | 54 ++- lib/rust/ensogl/core/src/display/world.rs | 24 +- lib/rust/ensogl/core/src/gui/cursor.rs | 41 +- lib/rust/frp/src/nodes.rs | 84 ++++ 17 files changed, 707 insertions(+), 419 deletions(-) create mode 100644 app/gui/view/graph-editor/src/component/node/input/port.rs diff --git a/app/gui/language/span-tree/src/iter.rs b/app/gui/language/span-tree/src/iter.rs index 34ed63a99272..1748b5ebbcb5 100644 --- a/app/gui/language/span-tree/src/iter.rs +++ b/app/gui/language/span-tree/src/iter.rs @@ -97,10 +97,6 @@ impl<'a, T> LeafIterator<'a, T> { TreeFragment::ChainAndDirectChildren => current_node.kind.is_chained(), } } - - pub fn into_base(self) -> node::Ref<'a, T> { - self.base_node - } } diff --git a/app/gui/src/controller/graph/widget/metadata.rs b/app/gui/src/controller/graph/widget/metadata.rs index 896e352624de..34b00a195466 100644 --- a/app/gui/src/controller/graph/widget/metadata.rs +++ b/app/gui/src/controller/graph/widget/metadata.rs @@ -40,7 +40,7 @@ pub fn deserialize_widget_update( } fn map_metadata(resp: response::Widget) -> widget::Metadata { - widget::Metadata { display: resp.display, config: map_config(resp.inner) } + widget::Metadata { display: resp.display, config: map_config(resp.inner), has_port: true } } fn map_config(inner: response::WidgetSpecific) -> widget::Config { diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index 868426ae4d3a..fb13ae12f523 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -509,8 +509,8 @@ impl NodeModel { background -> drag_area; drag_area -> edge::front::corner; drag_area -> edge::front::line; - edge::front::corner -> input::widget::port; - edge::front::line -> input::widget::port; + edge::front::corner -> input::port::shape; + edge::front::line -> input::port::shape; } } @@ -891,9 +891,9 @@ impl Node { hover_onset_delay.set_delay <+ preview_show_delay; hide_tooltip <- preview_show_delay.map(|&delay| delay <= EPSILON); - outout_hover <- model.output.on_port_hover.map(|s| s.is_on()); - hover_onset_delay.start <+ outout_hover.on_true(); - hover_onset_delay.reset <+ outout_hover.on_false(); + output_hover <- model.output.on_port_hover.map(|s| s.is_on()); + hover_onset_delay.start <+ output_hover.on_true(); + hover_onset_delay.reset <+ output_hover.on_false(); hover_onset_active <- bool(&hover_onset_delay.on_reset, &hover_onset_delay.on_end); hover_preview_visible <- has_expression && hover_onset_active; hover_preview_visible <- hover_preview_visible.on_change(); @@ -904,7 +904,7 @@ impl Node { preview_visible <- hover_preview_visible || preview_enabled; preview_visible <- preview_visible.on_change(); - // If the preview is visible while the visualization button is disabled, clicking the + // If the preview is visible while the visualization button is disabled, clic\king the // visualization button hides the preview and keeps the visualization button disabled. vis_button_on <- visualization_button_state.filter(|e| *e).constant(()); vis_button_off <- visualization_button_state.filter(|e| !*e).constant(()); @@ -1131,7 +1131,7 @@ pub mod test_utils { /// 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; + fn input_port_shape(&self) -> Option; } impl NodeModelExt for NodeModel { @@ -1146,7 +1146,7 @@ pub mod test_utils { } } - fn input_port_shape(&self) -> Option { + fn input_port_shape(&self) -> Option { // let ports = self.input.model.ports(); // let port = ports.first()?; // port.shape.as_ref().map(CloneRef::clone_ref) diff --git a/app/gui/view/graph-editor/src/component/node/input.rs b/app/gui/view/graph-editor/src/component/node/input.rs index 2ed65db5af5e..c371dc6c0523 100644 --- a/app/gui/view/graph-editor/src/component/node/input.rs +++ b/app/gui/view/graph-editor/src/component/node/input.rs @@ -6,6 +6,7 @@ // ============== pub mod area; +pub mod port; pub mod widget; pub use area::Area; diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index d6730e0b144b..08c977588e72 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -8,12 +8,12 @@ use ensogl::display::traits::*; use crate::component::type_coloring; use crate::node; use crate::node::input::widget; +use crate::node::input::widget::MetadataPointer; use crate::node::profiling; use crate::view; use crate::Type; use crate::WidgetUpdates; -use crate::component::node::input::widget::MetadataPointer; use enso_frp as frp; use enso_frp; use ensogl::application::Application; @@ -210,8 +210,6 @@ impl Model { self.set_label_layer(&scene.layers.label); let text_color = self.styles.get_color(theme::graph_editor::node::text); - // self.ports_label.set_property_default(text_color); - // self.ports_label.set_property_default(text::Size(TEXT_SIZE)); self.edit_mode_label.set_single_line_mode(true); self.edit_mode_label.disable_command("cursor_move_up"); @@ -242,10 +240,17 @@ impl Model { port.map(|port| self.root_widget.set_connected(&port, status)); } - /// Traverse all `SpanTree` leaves of the given port and emit hover style to set their colors. - fn set_port_hover(&self, target: &Switch) { - // TODO - // self.with_port_mut(&target.value, |t| t.set_hover(target.is_on())) + fn hover_pointer_style(&self, hovered: &Switch) -> Option { + let crumbs = hovered.on()?; + let expr = self.expression.borrow(); + let port = expr.span_tree.get_node(crumbs).ok()?; + let display_object = self.root_widget.get_port_display_object(&port)?; + let tp = port.tp().map(|t| Type(t.into())); + let color = tp.as_ref().map(|tp| type_coloring::compute(tp, &self.styles)); + let pad_x = node::input::port::PORT_PADDING_X * 2.0; + let size = display_object.computed_size() + Vector2(pad_x, 0.0); + let radius = size.y / 2.0; + Some(cursor::Style::new_highlight(display_object, size, radius, color)) } /// Apply widget updates to widgets in this input area. @@ -257,7 +262,7 @@ impl Model { self.root_widget.set_metadata(meta_pointer, update.meta.clone()); } let expr = self.expression.borrow(); - self.root_widget.rebuild_tree_on_metadata_change(&expr.span_tree, &expr.code); + self.root_widget.rebuild_tree_on_metadata_change(&expr.span_tree, &expr.code, &self.styles); } /// Request widgets metadata for all method calls within the expression. @@ -276,26 +281,20 @@ impl Model { #[profile(Debug)] fn set_expression(&self, new_expression: impl Into, area_frp: &FrpEndpoints) { let new_expression = InputExpression::from(new_expression.into()); - // if DEBUG { - warn!("set expression: \n{:?}", new_expression.tree_pretty_printer()); - // } + debug!("set expression: \n{:?}", new_expression.tree_pretty_printer()); - self.root_widget.rebuild_tree(&new_expression.span_tree, &new_expression.code); + self.root_widget.rebuild_tree( + &new_expression.span_tree, + &new_expression.code, + &self.styles, + ); // TODO streams to handle: // area_frp.source.pointer_style <+ pointer_style; // area_frp.source.pointer_style - // pointer_style (cursor::Style), - // width (f32), - // editing (bool), - // ports_visible (bool), - // body_hover (bool), - // on_port_press (Crumbs), - // on_port_hover (Switch), - // on_port_type_change (Crumbs,Option), ?? - // on_background_press (), - // view_mode (view::Mode), + // pointer_style (cursor::Style), <- handle edit mode cursor change + // view_mode (view::Mode), <- frp into widgets to change label color // self.init_port_frp_on_new_expression(&mut new_expression, area_frp); @@ -304,11 +303,6 @@ impl Model { } } -fn select_color(styles: &StyleWatch, tp: Option<&Type>) -> color::Lcha { - let opt_color = tp.as_ref().map(|tp| type_coloring::compute(tp, styles)); - opt_color.unwrap_or_else(|| styles.get_color(theme::code::types::any::selection).into()) -} - // =========== @@ -370,7 +364,6 @@ ensogl::define_endpoints! { body_hover (bool), on_port_press (Crumbs), on_port_hover (Switch), - on_port_type_change (Crumbs,Option), on_port_code_update (Crumbs,ImString), on_background_press (), view_mode (view::Mode), @@ -379,6 +372,9 @@ ensogl::define_endpoints! { /// call's target expression (`self` or first argument). requested_widgets (ast::Id, ast::Id), request_import (ImString), + /// A connected port within the node has been moved. Some edges might need to be updated. + /// This event is already debounced. + input_edges_need_refresh (), } } @@ -460,6 +456,8 @@ impl Area { frp.output.source.ports_visible <+ port_vis; frp.output.source.editing <+ set_editing; model.root_widget.ports_visible <+ frp.ports_visible; + refresh_edges <- model.root_widget.connected_port_updated.debounce(); + frp.output.source.input_edges_need_refresh <+ refresh_edges; // === Label Hover === @@ -468,9 +466,15 @@ impl Area { // === Port Hover === - eval frp.on_port_hover ((t) model.set_port_hover(t)); eval frp.set_connected (((crumbs,status)) model.set_connected(crumbs,*status)); - + hovered_port_pointer <- model.root_widget.on_port_hover.map( + f!((t) model.hover_pointer_style(t).unwrap_or_default()) + ); + pointer_style <- all[ + model.root_widget.pointer_style, + hovered_port_pointer + ].fold(); + frp.output.source.pointer_style <+ pointer_style; // === Properties === let layout_refresh = ensogl::animation::on_before_animations(); @@ -532,18 +536,10 @@ impl Area { use theme::code::syntax; let std_selection_color = model.styles_frp.get_color(syntax::selection); let profiled_selection_color = model.styles_frp.get_color(syntax::profiling::selection); - let std_base_color = model.styles_frp.get_color(syntax::base); - let profiled_base_color = model.styles_frp.get_color(syntax::profiling::base); - selection_color_rgba <- profiled.switch(&std_selection_color,&profiled_selection_color); selection_color.target <+ selection_color_rgba.map(|c| color::Lcha::from(c)); - // model.ports_label.set_selection_color <+ selection_color.value.map(|c| color::Lch::from(c)); - - // std_base_color <- all(std_base_color,init)._0(); - // profiled_base_color <- all(profiled_base_color,init)._0(); - // base_color <- profiled.switch(&std_base_color,&profiled_base_color); - // eval base_color ((color) model.ports_label.set_property_default(color)); + model.edit_mode_label.set_selection_color <+ selection_color.value.map(|c| color::Lch::from(c)); } init.emit(()); diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs new file mode 100644 index 000000000000..0f0365040464 --- /dev/null +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -0,0 +1,252 @@ +//! Definition of all hardcoded node widget variants and common widget FRP API. + +use crate::prelude::*; + +use crate::component::node::input::widget::Config; +use crate::component::node::input::widget::ConfigContext; +use crate::component::node::input::widget::DynWidget; +use crate::component::node::input::widget::SpanWidget; +use crate::component::node::input::widget::WidgetsFrp; +use crate::component::node::ConnectionStatus; +use enso_frp as frp; +use ensogl::application::Application; +use ensogl::control::io::mouse; +use ensogl::data::color; +use ensogl::display; +use ensogl::display::scene::layer::LayerSymbolPartition; + + + +// ================= +// === Constants === +// ================= + +/// The horizontal padding of ports. It affects how the port hover should extend the target text +/// boundary on both sides. +pub const PORT_PADDING_X: f32 = 4.0; + +/// The vertical hover padding of ports at low depth. It affects how the port hover should extend +/// the target text boundary on both sides. +pub const PRIMARY_PORT_HOVER_PADDING_Y: f32 = 4.0; + +/// The maximum depth of the widget port that is still considered primary. This is used to determine +/// the hover area of the port. +pub const PRIMARY_PORT_MAX_DEPTH: usize = 2; + +/// Width of a single space glyph. Used to calculate padding based on the text offset between nodes. +// TODO: avoid using hardcoded value. See https://www.pivotaltracker.com/story/show/183567623. +pub const SPACE_GLYPH_WIDTH: f32 = 7.224_609_4; + + + +// ============ +// === Port === +// ============ + +/// Visible shape of connected ports. +pub mod shape { + use super::*; + ensogl::shape! { + pointer_events = false; + (style:Style, color:Vector4) { + let size = Var::canvas_size(); + let shape_color = Var::::from(color); + let visual_shape = Rect(&size).corners_radius(size.y() / 2.0).fill(shape_color); + visual_shape.into() + } + } +} + +/// Hover area of port shape, reacts to mouse when an edge is dragged. +pub mod hover_shape { + use super::*; + ensogl::shape! { + above = [shape]; + (style:Style) { + let size = Var::canvas_size(); + let transparent = Var::::from("srgba(1.0,1.0,1.0,0.00001)"); + let hover_shape = Rect(&size).fill(transparent); + hover_shape.into() + } + } +} + +#[derive(Clone, CloneRef)] +struct PortHoverLayers { + hover_layer: display::scene::Layer, + hover_partitions: Rc>>>, +} + +impl display::scene::Extension for PortHoverLayers { + fn init(scene: &display::Scene) -> Self { + let hover_layer = scene.layers.main.clone_ref(); + Self { hover_layer, hover_partitions: default() } + } +} + +impl PortHoverLayers { + fn add_to_partition(&self, object: &display::object::Instance, depth: usize) { + let mut hover_partitions = self.hover_partitions.borrow_mut(); + if hover_partitions.len() <= depth { + hover_partitions.resize_with(depth + 1, || { + self.hover_layer.create_symbol_partition::("input port hover") + }) + } + hover_partitions[depth].add(object); + } +} + + +#[derive(Debug)] +pub(super) struct Port { + #[allow(dead_code)] + on_cleanup: frp::DropSource, + #[allow(dead_code)] + network: frp::Network, + crumbs: Rc>, + port_root: display::object::Instance, + widget_root: display::object::Instance, + widget: DynWidget, + port_shape: shape::View, + hover_shape: hover_shape::View, + last_offset: usize, + last_node_depth: usize, + is_primary: bool, +} + +impl Port { + pub fn new(widget: DynWidget, app: &Application, frp: &WidgetsFrp) -> Self { + let port_root = display::object::Instance::new(); + let widget_root = widget.root_object().clone_ref(); + let port_shape = shape::View::new(); + let hover_shape = hover_shape::View::new(); + + port_root.add_child(&widget_root); + port_root.set_alignment_left(); + port_shape + .allow_grow() + .set_margin_left(-PORT_PADDING_X) + .set_margin_right(-PORT_PADDING_X) + .set_alignment_left(); + hover_shape + .allow_grow() + .set_margin_left(-PORT_PADDING_X) + .set_margin_right(-PORT_PADDING_X) + .set_alignment_left(); + + let layers = app.display.default_scene.extension::(); + layers.add_to_partition(hover_shape.display_object(), 0); + + let mouse_enter = hover_shape.on_event::(); + let mouse_leave = hover_shape.on_event::(); + let mouse_down = hover_shape.on_event::(); + + let crumbs = Rc::new(RefCell::new(span_tree::Crumbs::default())); + + if frp.ports_visible.value() { + port_root.add_child(&hover_shape); + } + + frp::new_network! { network + on_cleanup <- on_drop(); + hovering <- bool(&mouse_leave, &mouse_enter); + cleanup_hovering <- on_cleanup.constant(false); + hovering <- any(&hovering, &cleanup_hovering); + hovering <- hovering.on_change(); + + frp.on_port_hover <+ hovering.map( + f!([crumbs](t) Switch::new(crumbs.borrow().clone(),*t)) + ); + + frp.on_port_press <+ mouse_down.map(f!((_) crumbs.borrow().clone())); + eval frp.ports_visible([port_root, hover_shape] (active) { + if *active { + port_root.add_child(&hover_shape); + } else { + port_root.remove_child(&hover_shape); + } + }); + + // Port shape is only connected to the display hierarchy when the port is connected. + // Thus the `on_updated` event is automatically disabled when the port is not connected. + let shape_display_object = port_shape.display_object(); + frp.connected_port_updated <+ shape_display_object.on_transformed; + }; + + Self { + on_cleanup, + port_shape, + hover_shape, + widget, + widget_root, + port_root, + network, + crumbs, + is_primary: false, + last_offset: 0, + last_node_depth: 0, + } + } + + pub fn configure(&mut self, config: &Config, ctx: ConfigContext) { + self.crumbs.replace(ctx.span_tree_node.crumbs.clone()); + self.set_port_layout(&ctx); + self.widget.configure(config, ctx); + self.update_root(); + } + + pub fn set_connected(&self, status: ConnectionStatus) { + match status { + ConnectionStatus::Connected { color } => { + self.port_root.add_child(&self.port_shape); + self.port_shape.color.set(color::Rgba::from(color).into()) + } + ConnectionStatus::Disconnected => { + self.port_root.remove_child(&self.port_shape); + } + }; + } + + fn update_root(&mut self) { + let new_root = self.widget.root_object(); + if new_root != &self.widget_root { + self.port_root.remove_child(&self.widget_root); + self.port_root.add_child(new_root); + self.widget_root = new_root.clone_ref(); + } + } + + fn set_port_layout(&mut self, ctx: &ConfigContext) { + let is_placeholder = ctx.span_tree_node.is_expected_argument(); + let min_offset = if is_placeholder { 1 } else { 0 }; + let offset = min_offset.max(ctx.span_tree_node.sibling_offset.as_usize()); + if self.last_offset != offset { + self.last_offset = offset; + self.port_root.set_margin_left(offset as f32 * SPACE_GLYPH_WIDTH); + } + + let node_depth = ctx.span_tree_node.crumbs.len(); + if self.last_node_depth != node_depth { + self.last_node_depth = node_depth; + let layers = ctx.app().display.default_scene.extension::(); + layers.add_to_partition(self.hover_shape.display_object(), node_depth); + } + + let is_primary = ctx.depth <= PRIMARY_PORT_MAX_DEPTH; + if self.is_primary != is_primary { + self.is_primary = is_primary; + let y_margin = if is_primary { 0.0 } else { PRIMARY_PORT_HOVER_PADDING_Y }; + self.hover_shape.set_margin_top(-y_margin).set_margin_bottom(-y_margin); + } + } + + pub fn into_widget(self) -> DynWidget { + self.widget + } +} + +impl display::Object for Port { + fn display_object(&self) -> &display::object::Instance { + self.port_root.display_object() + } +} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index c9c5332d5c38..4ea732bbbda5 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -6,14 +6,15 @@ use crate::prelude::*; use crate::component::node::input::area::NODE_HEIGHT; use crate::component::node::input::area::TEXT_OFFSET; +use crate::component::node::input::port::Port; use crate::component::node::ConnectionStatus; use enso_config::ARGS; use enso_frp as frp; use enso_text as text; use ensogl::application::Application; -use ensogl::control::io::mouse; -use ensogl::data::color; use ensogl::display; +use ensogl::display::shape::StyleWatch; +use ensogl::gui::cursor; use ensogl_component::drop_down::DropdownValue; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; @@ -21,20 +22,6 @@ use text::index::Byte; -// ================= -// === Constants === -// ================= - -/// The horizontal padding of ports. It affects how the port hover should extend the target text -/// boundary on both sides. -pub const PORT_PADDING_X: f32 = 4.0; - -/// Width of a single space glyph. Used to calculate padding based on the text offset between nodes. -// TODO: avoid using hardcoded value. See https://www.pivotaltracker.com/story/show/183567623. -pub const SPACE_GLYPH_WIDTH: f32 = 7.224_609_4; - - - // =========== // === FRP === // =========== @@ -44,10 +31,15 @@ ensogl::define_endpoints_2! { ports_visible (bool), } Output { - value_changed (span_tree::Crumbs, Option), - request_import (ImString), - on_port_hover (Switch), - on_port_press (span_tree::Crumbs), + value_changed (span_tree::Crumbs, Option), + request_import (ImString), + on_port_hover (Switch), + on_port_press (span_tree::Crumbs), + pointer_style (cursor::Style), + /// Any of the connected port's display object within the widget tree has been updated. This + /// signal is generated using the `on_updated` signal of the `display_object` of the widget, + /// all caveats of that signal apply here as well. + connected_port_updated (), } } @@ -70,10 +62,6 @@ pub struct MetadataPointer { pub trait SpanWidget { /// Configuration associated with specific widget variant. type Config: Debug + Clone + PartialEq; - /// Spacing between this widget and previous sibling. - fn sibling_offset(&self, config: &Self::Config, ctx: ConfigContext) -> usize { - ctx.span_tree_node.sibling_offset.as_usize() - } /// Root display object of a widget. It is returned to the parent widget for positioning. fn root_object(&self) -> &display::object::Instance; /// Create a new widget with given configuration. @@ -99,7 +87,7 @@ macro_rules! define_widget_modules( /// A part of widget model that is dependant on the widget kind. #[derive(Debug)] #[allow(missing_docs)] - enum DynWidget { + pub(super) enum DynWidget { $( $(#[$meta])* $name($module::Widget) @@ -174,14 +162,20 @@ define_widget_modules! { #[allow(missing_docs)] pub struct Metadata { /// The placeholder text value. By default, the parameter name is used. - pub display: Display, - pub config: Config, + pub display: Display, + pub config: Config, + pub has_port: bool, } impl Metadata { const fn always(config: C) -> Self where C: ~const Into { - Self { display: Display::Always, config: config.into() } + Self { display: Display::Always, config: config.into(), has_port: true } + } + + const fn inert(config: C) -> Self + where C: ~const Into { + Self { display: Display::Always, config: config.into(), has_port: false } } /// Widget metadata for static dropdown, based on the tag values provided by suggestion @@ -214,7 +208,9 @@ impl Metadata { } else { Self::always(label::Config::default()) }, - Kind::InsertionPoint(_) => Self::always(insertion_point::Config), + Kind::Token | Kind::Operation if !has_children => Self::inert(label::Config::default()), + Kind::NamedArgument => Self::inert(hierarchy::Config), + Kind::InsertionPoint(_) => Self::inert(insertion_point::Config), _ if has_children => Self::always(hierarchy::Config), _ => Self::always(label::Config::default()), } @@ -291,11 +287,13 @@ impl DropdownValue for Entry { /// Widget FRP endpoints that can be used by widget views, and go straight to the root. #[derive(Debug, Clone, CloneRef)] pub struct WidgetsFrp { - pub(self) ports_visible: frp::Sampler, - pub(self) value_changed: frp::Any<(span_tree::Crumbs, Option)>, - pub(self) request_import: frp::Any, - pub(self) on_port_hover: frp::Any>, - pub(self) on_port_press: frp::Any, + pub(super) ports_visible: frp::Sampler, + pub(super) value_changed: frp::Any<(span_tree::Crumbs, Option)>, + pub(super) request_import: frp::Any, + pub(super) on_port_hover: frp::Any>, + pub(super) on_port_press: frp::Any, + pub(super) pointer_style: frp::Any, + pub(super) connected_port_updated: frp::Any<()>, } @@ -338,17 +336,26 @@ impl Root { let request_import = frp.private.output.request_import.clone_ref(); let on_port_hover = frp.private.output.on_port_hover.clone_ref(); let on_port_press = frp.private.output.on_port_press.clone_ref(); + let pointer_style = frp.private.output.pointer_style.clone_ref(); + let connected_port_updated = frp.private.output.connected_port_updated.clone_ref(); let widgets_frp = WidgetsFrp { value_changed, request_import, on_port_hover, on_port_press, ports_visible, + pointer_style, + connected_port_updated, }; Self { frp, widgets_frp, model } } + /// Override widget metadata. The metadata is used to determine the widget appearance and + /// behavior. By default, the widget metadata will be inferred from its span tree kind and type. + /// However, in some cases, we want to change the selected widget for a given span tree node, + /// and it can be done by calling this method. The set metadata is persistent, and will be + /// applied to any future widget of this node that matches given pointer. pub fn set_metadata(&self, pointer: MetadataPointer, meta: Option) { self.model.set_metadata(pointer, meta); } @@ -363,14 +370,20 @@ impl Root { &self, tree: &span_tree::SpanTree, node_expression: &str, + styles: &StyleWatch, ) { if self.model.metadata_dirty.load(Ordering::Acquire) { - self.rebuild_tree(tree, node_expression); + self.rebuild_tree(tree, node_expression, styles); } } - pub fn rebuild_tree(&self, tree: &span_tree::SpanTree, node_expression: &str) { - self.model.rebuild_tree(self.widgets_frp.clone_ref(), tree, node_expression) + pub fn rebuild_tree( + &self, + tree: &span_tree::SpanTree, + node_expression: &str, + styles: &StyleWatch, + ) { + self.model.rebuild_tree(self.widgets_frp.clone_ref(), tree, node_expression, styles) } pub fn get_port_display_object( @@ -378,7 +391,7 @@ impl Root { tree_node: &span_tree::node::Ref, ) -> Option { let pointer = self.model.get_port_widget_pointer(tree_node)?; - self.model.with_port(&pointer, |w| w.display_object().clone()) + self.model.with_node(&pointer, |w| w.display_object().clone()) } } @@ -390,11 +403,37 @@ impl Root { struct RootModel { app: Application, display_object: debug::InstanceWithBg, - widgets_map: RefCell>, + widgets_map: RefCell>, metadata_map: Rc>>, metadata_dirty: AtomicBool, } +#[derive(Debug)] +pub(super) enum TreeNode { + /// A tree node that contains a port. The port wraps a widget. + Port(Port), + /// A tree node without a port, directly containing a widget. + Widget(DynWidget), +} + +impl TreeNode { + fn port_mut(&mut self) -> Option<&mut Port> { + match self { + TreeNode::Port(port) => Some(port), + TreeNode::Widget(_) => None, + } + } +} + +impl display::Object for TreeNode { + fn display_object(&self) -> &display::object::Instance { + match self { + TreeNode::Port(port) => port.display_object(), + TreeNode::Widget(widget) => widget.root_object(), + } + } +} + mod rectangle { use super::*; ensogl::shape! { @@ -432,9 +471,6 @@ impl RootModel { /// Set the metadata for the given node data. fn set_metadata(&self, pointer: MetadataPointer, meta: Option) { - // TODO: rebuild tree partially after batch updating metadata. - // For now, the tree will be rebuilt completely. - use std::collections::hash_map::Entry; let mut map = self.metadata_map.borrow_mut(); let entry = map.entry(pointer); @@ -457,13 +493,19 @@ impl RootModel { } #[profile(Task)] - fn rebuild_tree(&self, frp: WidgetsFrp, tree: &span_tree::SpanTree, node_expression: &str) { + fn rebuild_tree( + &self, + frp: WidgetsFrp, + tree: &span_tree::SpanTree, + node_expression: &str, + styles: &StyleWatch, + ) { self.metadata_dirty.store(false, Ordering::Release); let app = self.app.clone(); let metadata_map = self.metadata_map.borrow(); let widgets_map = self.widgets_map.take(); let mut builder = - WidgetTreeBuilder::new(node_expression, app, frp, &*metadata_map, widgets_map); + WidgetTreeBuilder::new(node_expression, app, frp, &*metadata_map, widgets_map, styles); let child = builder.child_widget(tree.root_ref(), 0); self.display_object.inner.replace_children(&[child]); self.widgets_map.replace(builder.new_widgets); @@ -504,44 +546,56 @@ impl RootModel { } } - pub fn with_port( + pub fn with_node( &self, pointer: &WidgetTreePointer, - f: impl FnOnce(&Port) -> T, + f: impl FnOnce(&TreeNode) -> T, ) -> Option { self.widgets_map.borrow().get(pointer).map(f) } + pub fn with_node_mut( + &self, + pointer: &WidgetTreePointer, + f: impl FnOnce(&mut TreeNode) -> T, + ) -> Option { + self.widgets_map.borrow_mut().get_mut(pointer).map(f) + } + pub fn with_port_mut( &self, pointer: &WidgetTreePointer, f: impl FnOnce(&mut Port) -> T, ) -> Option { - self.widgets_map.borrow_mut().get_mut(pointer).map(f) + self.widgets_map.borrow_mut().get_mut(pointer).and_then(TreeNode::port_mut).map(f) } } #[derive(Debug)] pub struct ConfigContext<'a, 'b> { - builder: &'a mut WidgetTreeBuilder<'b>, - display: Display, - span_tree_node: span_tree::node::Ref<'a>, - depth: usize, + builder: &'a mut WidgetTreeBuilder<'b>, + pub(super) display: Display, + pub(super) span_tree_node: span_tree::node::Ref<'a>, + pub(super) depth: usize, } impl<'a, 'b> ConfigContext<'a, 'b> { - fn app(&self) -> &Application { + pub fn app(&self) -> &Application { &self.builder.app } - fn frp(&self) -> &WidgetsFrp { + pub fn frp(&self) -> &WidgetsFrp { &self.builder.frp } - fn expression_at(&self, range: text::Range) -> &str { + pub fn expression_at(&self, range: text::Range) -> &str { &self.builder.node_expression[range] } + + pub fn styles(&self) -> &StyleWatch { + self.builder.styles + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -557,11 +611,12 @@ struct WidgetTreeBuilder<'a> { app: Application, frp: WidgetsFrp, metadata_map: &'a HashMap, - old_widgets: HashMap, - new_widgets: HashMap, + old_widgets: HashMap, + new_widgets: HashMap, last_ast_id: Option, last_ast_id_crumbs: span_tree::Crumbs, node_expression: &'a str, + styles: &'a StyleWatch, } impl<'a> WidgetTreeBuilder<'a> { @@ -570,7 +625,8 @@ impl<'a> WidgetTreeBuilder<'a> { app: Application, frp: WidgetsFrp, metadata_map: &'a HashMap, - old_widgets: HashMap, + old_widgets: HashMap, + styles: &'a StyleWatch, ) -> Self { Self { app, @@ -581,6 +637,7 @@ impl<'a> WidgetTreeBuilder<'a> { last_ast_id: default(), last_ast_id_crumbs: default(), node_expression, + styles, } } @@ -656,30 +713,35 @@ impl<'a> WidgetTreeBuilder<'a> { meta_fallback.get_or_insert_with(|| Metadata::from_kind(kind, has_children)) }); - let old_widget = self.old_widgets.remove(&tree_ptr); - let port = { + let old_node = self.old_widgets.remove(&tree_ptr); + let node = { let ctx = ConfigContext { builder: &mut *self, display: meta.display, span_tree_node, depth }; // Widget creation/update can recurse into the builder. All borrows must be dropped // at this point. - - match old_widget { - Some(mut port) => { - port.configure(&meta.config, ctx); - port - } - None => { - let app = ctx.app(); - let frp = ctx.frp(); - let widget = DynWidget::new(&meta.config, app, frp); - let mut port = Port::new(widget, frp); - port.configure(&meta.config, ctx); - port - } + let app = ctx.app(); + let frp = ctx.frp(); + + if meta.has_port { + let mut port = match old_node { + Some(TreeNode::Port(port)) => port, + Some(TreeNode::Widget(widget)) => Port::new(widget, app, frp), + None => Port::new(DynWidget::new(&meta.config, app, frp), app, frp), + }; + port.configure(&meta.config, ctx); + TreeNode::Port(port) + } else { + let mut widget = match old_node { + Some(TreeNode::Port(port)) => port.into_widget(), + Some(TreeNode::Widget(widget)) => widget, + None => DynWidget::new(&meta.config, app, frp), + }; + widget.configure(&meta.config, ctx); + TreeNode::Widget(widget) } }; - let root = port.display_object().clone(); - self.new_widgets.insert(tree_ptr.clone(), port); + let root = node.display_object().clone(); + self.new_widgets.insert(tree_ptr.clone(), node); // After visiting child node, restore previous layer's data. if let Some((id, crumbs)) = ast_data_to_restore { @@ -689,155 +751,3 @@ impl<'a> WidgetTreeBuilder<'a> { root } } - - - -// ============ -// === Port === -// ============ - -/// Port shape definition. -pub mod port { - use super::*; - ensogl::shape! { - pointer_events = false; - (style:Style, color:Vector4) { - let size = Var::canvas_size(); - let shape_color = Var::::from(color); - let visual_shape = Rect(&size).corners_radius(size.y() / 2.0).fill(shape_color); - visual_shape.into() - } - } -} - -/// Port shape definition. -pub mod port_hover { - use super::*; - ensogl::shape! { - (style:Style) { - let size = Var::canvas_size(); - let instance = Var::::from("srgba(hsva(mod(float(input_global_instance_id + input_mouse_click_count % 10) * 17.0, 100.0) / 100.0, 1.0, 1.0, 0.5))"); - // let transparent = Var::::from("srgba(1.0,1.0,1.0,0.00001)"); - let hover_shape = Rect(&size).fill(instance); - hover_shape.into() - } - } -} - -#[derive(Debug)] -pub(super) struct Port { - port_root: display::object::Instance, - widget_root: display::object::Instance, - widget: DynWidget, - port_shape: port::View, - #[allow(dead_code)] - hover_shape: port_hover::View, - #[allow(dead_code)] - network: frp::Network, - crumbs: Rc>, - last_offset: usize, -} - -impl Port { - fn new(widget: DynWidget, frp: &WidgetsFrp) -> Self { - let port_root = display::object::Instance::new(); - let widget_root = widget.root_object().clone_ref(); - let port_shape = port::View::new(); - let hover_shape = port_hover::View::new(); - - port_root.add_child(&widget_root); - port_root.set_alignment_left(); - port_root.set_z(-1.0); - port_shape - .allow_grow() - .set_margin_left(-PORT_PADDING_X) - .set_margin_right(-PORT_PADDING_X) - .set_alignment_left(); - hover_shape - .allow_grow() - .set_margin_left(-PORT_PADDING_X) - .set_margin_right(-PORT_PADDING_X) - .set_alignment_left(); - - let mouse_enter = hover_shape.on_event::(); - let mouse_leave = hover_shape.on_event::(); - let mouse_down = hover_shape.on_event::(); - - let crumbs = Rc::new(RefCell::new(span_tree::Crumbs::default())); - - if frp.ports_visible.value() { - port_root.add_child(&hover_shape); - } - - frp::new_network! { network - hovering <- bool(&mouse_leave, &mouse_enter); - trace hovering; - frp.on_port_hover <+ hovering.map( - f!([crumbs](t) Switch::new(crumbs.borrow().clone(),*t)) - ); - frp.on_port_press <+ mouse_down.map(f!((_) crumbs.borrow().clone())); - eval frp.ports_visible([port_root, hover_shape] (active) { - if *active { - port_root.add_child(&hover_shape); - } else { - port_root.remove_child(&hover_shape); - } - }); - }; - - Self { - port_shape, - hover_shape, - widget, - widget_root, - port_root, - network, - crumbs, - last_offset: 0, - } - } - - fn configure(&mut self, config: &Config, ctx: ConfigContext) { - self.crumbs.replace(ctx.span_tree_node.crumbs.clone()); - self.set_port_layout(&ctx); - self.widget.configure(config, ctx); - self.update_root(); - } - - fn update_root(&mut self) { - let new_root = self.widget.root_object(); - if new_root != &self.widget_root { - self.port_root.remove_child(&self.widget_root); - self.port_root.add_child(new_root); - self.widget_root = new_root.clone_ref(); - } - } - - fn set_port_layout(&mut self, ctx: &ConfigContext) { - let is_placeholder = ctx.span_tree_node.is_expected_argument(); - let min_offset = if is_placeholder { 1 } else { 0 }; - let offset = min_offset.max(ctx.span_tree_node.sibling_offset.as_usize()); - if self.last_offset != offset { - self.last_offset = offset; - self.port_root.set_margin_left(offset as f32 * SPACE_GLYPH_WIDTH); - } - } - - fn set_connected(&self, status: ConnectionStatus) { - match status { - ConnectionStatus::Connected { color } => { - self.port_root.add_child(&self.port_shape); - self.port_shape.color.set(color::Rgba::from(color).into()) - } - ConnectionStatus::Disconnected => { - self.port_root.remove_child(&self.port_shape); - } - }; - } -} - -impl display::Object for Port { - fn display_object(&self) -> &display::object::Instance { - self.port_root.display_object() - } -} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs b/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs index 46243692ab40..5ffd7eeaaefc 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs @@ -2,6 +2,8 @@ use crate::prelude::*; use ensogl::data::color; use ensogl::display; +const DEBUG_BORDER_ENABLED: bool = false; + mod shape { ensogl::shape! { above = [ @@ -41,12 +43,13 @@ impl InstanceWithBg { pub fn with_color(color: color::Rgba) -> Self { let bg = shape::View::new(); - let outer = bg.display_object().clone(); - // let outer = display::object::Instance::new(); let inner = display::object::Instance::new(); - // bg.allow_grow(); - bg.color.set(color.into()); - // outer.add_child(&bg); + let outer = if DEBUG_BORDER_ENABLED { + bg.color.set(color.into()); + bg.display_object().clone() + } else { + display::object::Instance::new() + }; outer.add_child(&inner); Self { bg, outer, inner } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 671bd3829ee2..3c7b9a2cb447 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -8,6 +8,7 @@ use ensogl::application::Application; use ensogl::data::color; use ensogl::display::object; use ensogl_component::text; +use ensogl_hardcoded_theme as theme; // ============= // === Label === @@ -21,6 +22,7 @@ ensogl::define_endpoints_2! { Input { content(ImString), text_color(color::Lcha), + crumbs(span_tree::Crumbs), } } @@ -39,7 +41,7 @@ impl super::SpanWidget for Widget { &self.root.outer } - fn new(_: &Config, app: &Application, _: &super::WidgetsFrp) -> Self { + fn new(_: &Config, app: &Application, widgets_frp: &super::WidgetsFrp) -> Self { // Embed the label in a vertically centered fixed height container, so that the label's // baseline is properly aligned to center and lines up with other labels in the line. @@ -58,12 +60,20 @@ impl super::SpanWidget for Widget { frp::extend! { network color_change <- frp.text_color.on_change(); - eval color_change((color) label.set_property_default(color)); + parent_port_hovered <- widgets_frp.on_port_hover.map2(&frp.crumbs, |h, crumbs| { + h.on().map_or(false, |h| crumbs.starts_with(h)) + }); + label_color <- color_change.map2(&parent_port_hovered, |color, hovered| { + if *hovered { color::Lcha::white() } else { *color } + }); + + eval label_color((color) label.set_property_default(color)); content_change <- frp.content.on_change(); eval content_change([label] (content) { label.set_content(content); }); + width <- label.width.on_change(); eval width((w) { inner.set_size_x(*w); }); } @@ -81,16 +91,18 @@ impl super::SpanWidget for Widget { }; - // let text_color = self.styles.get_color(theme::graph_editor::node::text); let text_color = if is_placeholder { + ctx.styles().get_color(theme::graph_editor::node::text); color::Lcha::new(0.5, 0.0, 0.0, 1.0) } else { - color::Lcha::new(0.0, 0.0, 0.0, 1.0) + let ty = ctx.span_tree_node.kind.tp(); + let ty = ty.map(|t| crate::Type(t.into())); + crate::type_coloring::compute_for_code(ty.as_ref(), &ctx.styles()) }; - let input = &self.frp.public.input; input.content.emit(content.clone()); input.text_color.emit(text_color); + input.crumbs.emit(ctx.span_tree_node.crumbs.clone()); } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 9e69c38bd13c..aeb57fadb046 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -35,7 +35,7 @@ pub mod triangle { above = [ crate::component::node::background, crate::component::node::drag_area, - crate::component::node::input::widget::port + crate::component::node::input::port::shape ]; alignment = left_bottom; (style:Style, color:Vector4) { diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index aa0dc9ea5120..6f7fea0eec5d 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -737,6 +737,7 @@ ensogl::define_endpoints_2! { node_action_skip ((NodeId, bool)), node_edit_mode (bool), nodes_labels_visible (bool), + node_incoming_edge_updates (NodeId), /// `None` value as a visualization path denotes a disabled visualization. @@ -1607,21 +1608,9 @@ impl GraphEditorModelWithNetwork { model.frp.private.output.hover_node_output.emit(output); }); - let neutral_color = model.styles_frp.get_color(theme::code::types::any::selection); - - _eval <- all_with(&node_model.input.frp.on_port_type_change,&neutral_color, - f!(((crumbs,_),neutral_color) - model.with_input_edge_id(node_id,crumbs,|id| - model.refresh_edge_color(id,neutral_color.into()) - ) - )); - - _eval <- all_with(&node_model.input.frp.on_port_type_change,&neutral_color, - f!(((crumbs,_),neutral_color) - model.with_output_edge_id(node_id,crumbs,|id| - model.refresh_edge_color(id,neutral_color.into()) - ) - )); + eval node_model.input.frp.input_edges_need_refresh( + (_) model.frp.private.output.node_incoming_edge_updates.emit(node_id) + ); let is_editing = &node_model.input.frp.editing; expression_change_temporary <- node.on_expression_modified.gate(is_editing); @@ -2310,12 +2299,6 @@ impl GraphEditorModel { (node_id, new_position) } - #[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs. - pub fn refresh_edge_position(&self, edge_id: EdgeId) { - self.refresh_edge_source_position(edge_id); - self.refresh_edge_target_position(edge_id); - } - #[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs. pub fn refresh_edge_source_size(&self, edge_id: EdgeId) { if let Some(edge) = self.edges.get_cloned_ref(&edge_id) { @@ -2343,33 +2326,63 @@ impl GraphEditorModel { } } - #[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs. - pub fn refresh_edge_source_position(&self, edge_id: EdgeId) { + /// Refresh the source and target position of the edge identified by `edge_id`. Only redraws the + /// edge if a modification was made. Return `true` if either of the edge endpoint's position was + /// modified. + pub fn refresh_edge_position(&self, edge_id: EdgeId) -> bool { + let mut redraw = false; if let Some(edge) = self.edges.get_cloned_ref(&edge_id) { if let Some(edge_source) = edge.source() { if let Some(node) = self.nodes.get_cloned_ref(&edge_source.node_id) { - edge.modify_position(|p| { - p.x = node.position().x + node.model().width() / 2.0; - p.y = node.position().y; - }); + let new_position = + node.position().xy() + Vector2::new(node.model().width() / 2.0, 0.0); + let prev_position = edge.position().xy(); + if prev_position != new_position { + redraw = true; + edge.set_xy(new_position); + } } } - }; - } - - #[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs. - pub fn refresh_edge_target_position(&self, edge_id: EdgeId) { - if let Some(edge) = self.edges.get_cloned_ref(&edge_id) { if let Some(edge_target) = edge.target() { if let Some(node) = self.nodes.get_cloned_ref(&edge_target.node_id) { let offset = node.model().input.port_offset(&edge_target.port).unwrap_or_default(); - let pos = node.position().xy() + offset; - edge.view.frp.target_position.emit(pos); - edge.view.frp.redraw.emit(()); + let new_position = node.position().xy() + offset; + let prev_position = edge.view.target_position.get(); + if prev_position != new_position { + redraw = true; + edge.view.frp.target_position.emit(new_position); + } } } - }; + + if redraw { + edge.view.frp.redraw.emit(()); + } + } + redraw + } + + /// Refresh the positions of all edges connected to the given node. This is useful when we know + /// that the node ports has been updated, but we don't track which exact edges are affected. + /// + /// Because edge positions are computed based on the node positions, it is usually done after + /// the layout has been updated. In order to avoid edge flickering, we have to update their + /// layout second time. + /// + /// FIXME: Find a better solution to fix this issue. We either need a layout that can depend on + /// other arbitrary position, or we need the layout update to be multi-stage. + pub fn refresh_incoming_edge_positions_and_relayout(&self, node_ids: &[NodeId]) { + let mut updated = false; + for node_id in node_ids { + for edge_id in self.node_in_edges(node_id) { + updated |= self.refresh_edge_position(edge_id); + } + } + warn!("refresh_incoming_edge_positions_and_relayout {updated} {node_ids:?}"); + if updated { + self.display_object().update(self.scene()); + } } fn map_node(&self, id: NodeId, f: impl FnOnce(Node) -> T) -> Option { @@ -2391,11 +2404,7 @@ impl GraphEditorModel { } fn with_edge_map_source(&self, id: EdgeId, f: impl FnOnce(EdgeEndpoint) -> T) -> Option { - self.with_edge(id, |edge| { - let edge = edge.source.borrow().deref().clone(); - edge.map(f) - }) - .flatten() + self.with_edge(id, |edge| edge.source.borrow().clone().map(f)).flatten() } fn with_edge_map_target(&self, id: EdgeId, f: impl FnOnce(EdgeEndpoint) -> T) -> Option { @@ -2410,52 +2419,6 @@ impl GraphEditorModel { self.with_edge_map_target(id, |endpoint| endpoint) } - // FIXME[WD]: This implementation is slow. Node should allow for easy mapping between Crumbs - // and edges. Should be part of https://github.com/enso-org/ide/issues/822. - fn with_input_edge_id( - &self, - id: NodeId, - crumbs: &span_tree::Crumbs, - f: impl FnOnce(EdgeId) -> T, - ) -> Option { - self.with_node(id, move |node| { - let mut target_edge_id = None; - for edge_id in node.in_edges.keys() { - self.with_edge(edge_id, |edge| { - let ok = edge.target().map(|tgt| tgt.port == crumbs) == Some(true); - if ok { - target_edge_id = Some(edge_id) - } - }); - } - target_edge_id.map(f) - }) - .flatten() - } - - // FIXME[WD]: This implementation is slow. Node should allow for easy mapping between Crumbs - // and edges. Should be part of https://github.com/enso-org/ide/issues/822. - fn with_output_edge_id( - &self, - id: NodeId, - crumbs: &span_tree::Crumbs, - f: impl FnOnce(EdgeId) -> T, - ) -> Option { - self.with_node(id, move |node| { - let mut target_edge_id = None; - for edge_id in node.out_edges.keys() { - self.with_edge(edge_id, |edge| { - let ok = edge.target().map(|tgt| tgt.port == crumbs) == Some(true); - if ok { - target_edge_id = Some(edge_id) - } - }); - } - target_edge_id.map(f) - }) - .flatten() - } - fn with_edge_source(&self, id: EdgeId, f: impl FnOnce(EdgeEndpoint) -> T) -> Option { self.with_edge(id, |edge| { let source = edge.source.borrow().deref().clone(); @@ -3086,6 +3049,11 @@ fn new_graph_editor(app: &Application) -> GraphEditor { 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)); + + incoming_edge_updates_batch <- out.node_incoming_edge_updates.batch(); + eval incoming_edge_updates_batch ( + (nodes) model.refresh_incoming_edge_positions_and_relayout(nodes) + ); } // === Adding Node === @@ -3782,10 +3750,16 @@ fn new_graph_editor(app: &Application) -> GraphEditor { frp::extend! { network - cursor_style_edge_drag <- all_with(&out.some_edge_endpoints_unset,&out.view_mode, - f!([model,neutral_color](some_edges_detached,_) { + // Let the edge update complete before refreshing the cursor. Otherwise, the cursor style can + // be updated too early, before the edge had a chance to be updated with new endpoints. + refresh_detached_edge_cursor <- all(&out.some_edge_endpoints_unset,&out.view_mode); + refresh_detached_edge_cursor <- refresh_detached_edge_cursor._0().debounce(); + + cursor_style_edge_drag <- refresh_detached_edge_cursor.map( + f!([model,neutral_color](some_edges_detached) { if *some_edges_detached { - if let Some(color) = model.first_detached_edge_color(neutral_color.value().into()) { + let color = model.first_detached_edge_color(neutral_color.value().into()); + if let Some(color) = color { cursor::Style::new_color(color).press() } else { cursor::Style::new_color_no_animation(neutral_color.value().into()).press() diff --git a/lib/rust/ensogl/core/src/animation/loops.rs b/lib/rust/ensogl/core/src/animation/loops.rs index b13ea5fccb84..127e2b205d49 100644 --- a/lib/rust/ensogl/core/src/animation/loops.rs +++ b/lib/rust/ensogl/core/src/animation/loops.rs @@ -172,6 +172,7 @@ crate::define_endpoints_2! { on_before_animations(TimeInfo), on_after_animations(TimeInfo), on_before_rendering(TimeInfo), + on_before_rendering_late(TimeInfo), frame_end(TimeInfo), } } @@ -207,6 +208,11 @@ pub fn on_before_rendering() -> enso_frp::Sampler { LOOP_REGISTRY.with(|registry| registry.on_before_rendering.clone_ref()) } +/// Fires before the late stage of rendering is performed. +pub fn on_before_rendering_late() -> enso_frp::Sampler { + LOOP_REGISTRY.with(|registry| registry.on_before_rendering_late.clone_ref()) +} + /// A wrapper for JavaScript `requestAnimationFrame` loop. It allows registering callbacks and also /// exposes FRP endpoints that will emit signals on every loop iteration. #[derive(CloneRef, Derivative, Deref)] @@ -302,8 +308,9 @@ fn on_frame_closure( let on_frame_start = output.on_frame_start.clone_ref(); let on_before_animations = output.on_before_animations.clone_ref(); let on_after_animations = output.on_after_animations.clone_ref(); + let on_before_rendering = output.on_before_rendering.clone_ref(); + let on_before_rendering_late = output.on_before_rendering_late.clone_ref(); let frame_end = output.frame_end.clone_ref(); - let output = output.clone_ref(); let before_animations = before_animations.clone_ref(); let animations = animations.clone_ref(); let _profiler = profiler::start_debug!(profiler::APP_LIFETIME, "@on_frame"); @@ -316,8 +323,9 @@ fn on_frame_closure( .then(move || fixed_fps_sampler.borrow_mut().run(time_info, |t| animations.run_all(t))) .then(move || on_after_animations.emit(time_info)) .then(move || frame_end.emit(time_info)) + .then(move || on_before_rendering.emit(time_info)) .then(move || { - output.on_before_rendering.emit(time_info); + on_before_rendering_late.emit(time_info); drop(_profiler); }) .schedule(); diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index 883f9d3b1e64..6a76363dab07 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -1847,8 +1847,9 @@ impl Model { #[profile(Detail)] pub fn update(&self, scene: &Scene) { self.refresh_layout(); - let origin0 = Matrix4::identity(); - self.update_with_origin(scene, origin0, false, false, None); + let parent_origin = + self.parent().map_or(Matrix4::identity(), |parent| parent.transformation_matrix()); + self.update_with_origin(scene, parent_origin, false, false, None); } /// Update the display object tree transformations based on the parent object origin. See docs diff --git a/lib/rust/ensogl/core/src/display/scene.rs b/lib/rust/ensogl/core/src/display/scene.rs index 3b2ff0eaf999..7dfd22b3d815 100644 --- a/lib/rust/ensogl/core/src/display/scene.rs +++ b/lib/rust/ensogl/core/src/display/scene.rs @@ -701,6 +701,7 @@ pub struct Frp { camera_changed_source: frp::Source, frame_time_source: frp::Source, focused_source: frp::Source, + post_update: frp::Source, } impl Frp { @@ -710,6 +711,7 @@ impl Frp { camera_changed_source <- source(); frame_time_source <- source(); focused_source <- source(); + post_update <- source(); } let shape = shape.clone_ref(); let camera_changed = camera_changed_source.clone_ref().into(); @@ -724,6 +726,7 @@ impl Frp { camera_changed_source, frame_time_source, focused_source, + post_update, } } } @@ -1207,31 +1210,58 @@ impl Deref for Scene { } impl Scene { + /// Perform early phase of scene update. This includes updating camera and the layout of all + /// display objects. No GPU buffers are updated yet, giving the opportunity to perform + /// additional updates that affect the layout of display objects after the main scene layout + /// has been performed. + /// + /// During this phase, the layout updates can be observed using `on_updated` FRP events on each + /// individual display object. Any further updates to the scene may require the `update` + /// method to be manually called on affected objects in order to affect rendering during + /// this frame. #[profile(Debug)] - // FIXME: - #[allow(unused_assignments)] - pub fn update(&self, time: animation::TimeInfo) -> UpdateStatus { - if let Some(context) = &*self.context.borrow() { - debug_span!("Updating.").in_scope(|| { + pub fn early_update(&self, time: animation::TimeInfo) -> UpdateStatus { + if let Some(_) = &*self.context.borrow() { + debug_span!("Early update.").in_scope(|| { let mut scene_was_dirty = false; self.frp.frame_time_source.emit(time.since_animation_loop_started.unchecked_raw()); // Please note that `update_camera` is called first as it may trigger FRP events // which may change display objects layout. - scene_was_dirty = self.update_camera(self) || scene_was_dirty; + scene_was_dirty |= self.update_camera(self); self.display_object.update(self); - scene_was_dirty = self.layers.update() || scene_was_dirty; - scene_was_dirty = self.update_shape() || scene_was_dirty; - scene_was_dirty = self.update_symbols() || scene_was_dirty; + UpdateStatus { scene_was_dirty, pointer_position_changed: false } + }) + } else { + default() + } + } + + /// Perform late phase of scene update. At this point, all display object state is being + /// committed for rendering. This includes updating the layer stack, refreshing GPU buffers and + /// handling mouse events. + #[profile(Debug)] + pub fn late_update( + &self, + time: animation::TimeInfo, + early_status: UpdateStatus, + ) -> UpdateStatus { + if let Some(context) = &*self.context.borrow() { + debug_span!("Late update.").in_scope(|| { + let UpdateStatus { mut scene_was_dirty, mut pointer_position_changed } = + early_status; + scene_was_dirty |= self.layers.update(); + scene_was_dirty |= self.update_shape(); + scene_was_dirty |= self.update_symbols(); self.handle_mouse_over_and_out_events(); - scene_was_dirty = self.shader_compiler.run(context, time) || scene_was_dirty; + scene_was_dirty |= self.shader_compiler.run(context, time); - let pointer_position_changed = self.pointer_position_changed.get(); + pointer_position_changed |= self.pointer_position_changed.get(); self.pointer_position_changed.set(false); // FIXME: setting it to true for now in order to make cursor blinking work. // Text cursor animation is in GLSL. To be handled properly in this PR: // #183406745 - let scene_was_dirty = true; + scene_was_dirty |= true; UpdateStatus { scene_was_dirty, pointer_position_changed } }) } else { diff --git a/lib/rust/ensogl/core/src/display/world.rs b/lib/rust/ensogl/core/src/display/world.rs index 64cb68f073a5..61db40f7f84e 100644 --- a/lib/rust/ensogl/core/src/display/world.rs +++ b/lib/rust/ensogl/core/src/display/world.rs @@ -21,6 +21,7 @@ use crate::display::render::cache_shapes::CacheShapesPass; use crate::display::render::passes::SymbolsRenderPass; use crate::display::scene::DomPath; use crate::display::scene::Scene; +use crate::display::scene::UpdateStatus; use crate::display::shape::primitive::glsl; use crate::display::symbol::registry::RunMode; use crate::display::symbol::registry::SymbolRegistry; @@ -330,10 +331,14 @@ impl WorldDataWithLoop { let data = WorldData::new(&frp.private.output); let on_frame_start = animation::on_frame_start(); let on_before_rendering = animation::on_before_rendering(); + let on_before_rendering_late = animation::on_before_rendering_late(); let network = frp.network(); crate::frp::extend! {network eval on_frame_start ((t) data.run_stats(*t)); - eval on_before_rendering ((t) data.run_next_frame(*t)); + early_update <- on_before_rendering.map(f!((t) data.run_next_frame_early(*t))); + _eval <- on_before_rendering_late.map2(&early_update, + f!((t, early) data.run_next_frame_late(*t, *early)) + ); } Self { frp, data } @@ -554,18 +559,29 @@ impl WorldData { } } - /// Perform to the next frame with the provided time information. + /// Perform to the early step of next frame simulation with the provided time information. + /// See [`Scene::early_update`] for information about actions performed in this step. /// /// Please note that the provided time information from the [`requestAnimationFrame`] JS /// function is more precise than time obtained from the [`window.performance().now()`] one. /// Follow this link to learn more: /// https://stackoverflow.com/questions/38360250/requestanimationframe-now-vs-performance-now-time-discrepancy. #[profile(Objective)] - pub fn run_next_frame(&self, time: animation::TimeInfo) { + pub fn run_next_frame_early(&self, time: animation::TimeInfo) -> UpdateStatus { self.on.before_frame.run_all(time); self.uniforms.time.set(time.since_animation_loop_started.unchecked_raw()); self.scene_dirty.unset_all(); - let update_status = self.default_scene.update(time); + self.default_scene.early_update(time) + } + + /// perform to the late step of next frame simulation with the provided time information. + /// See [`Scene::late_update`] for information about actions performed in this step. + /// + /// Apart from the scene late update, this function also performs garbage collection and actual + /// rendering of the scene using updated GPU buffers. + #[profile(Objective)] + pub fn run_next_frame_late(&self, time: animation::TimeInfo, early_status: UpdateStatus) { + let update_status = self.default_scene.late_update(time, early_status); self.garbage_collector.mouse_events_handled(); self.default_scene.render(update_status); self.on.after_frame.run_all(time); diff --git a/lib/rust/ensogl/core/src/gui/cursor.rs b/lib/rust/ensogl/core/src/gui/cursor.rs index bc0602abf34a..e961cfb8b110 100644 --- a/lib/rust/ensogl/core/src/gui/cursor.rs +++ b/lib/rust/ensogl/core/src/gui/cursor.rs @@ -64,6 +64,7 @@ impl Style { pub fn new_highlight>( host: H, size: Vector2, + radius: f32, color: Option, ) -> Self where @@ -71,12 +72,14 @@ impl Style { { let host = Some(StyleValue::new(host.display_object().clone_ref())); let size = Some(StyleValue::new(size)); + let radius = Some(StyleValue::new(radius)); + let press = Some(StyleValue::new(0.0)); let color = color.map(|color| { let color = color.into(); StyleValue::new(color) }); let port_selection_layer = Some(StyleValue::new_no_animation(true)); - Self { host, size, color, port_selection_layer, ..default() } + Self { host, size, radius, color, port_selection_layer, press, ..default() } } pub fn new_color(color: color::Lcha) -> Self { @@ -127,16 +130,6 @@ impl Style { } -// === Getters === - -#[allow(missing_docs)] -impl Style { - pub fn host_position(&self) -> Option> { - self.host.as_ref().and_then(|t| t.value.as_ref().map(|t| t.position())) - } -} - - // ================== // === CursorView === @@ -158,8 +151,8 @@ pub mod shape { let height : Var = "input_size.y".into(); let press_side_shrink = 2.px(); let press_diff = press_side_shrink * &press; - let radius = 1.px() * radius - &press_diff; - let sides_padding = 1.px() * SIDES_PADDING; + let radius = radius.px() - &press_diff; + let sides_padding = SIDES_PADDING.px(); let width = &width - &press_diff * 2.0 - &sides_padding; let height = &height - &press_diff * 2.0 - &sides_padding; let cursor = Rect((&width,&height)).corners_radius(radius); @@ -407,11 +400,17 @@ impl Cursor { ).constant(()); host_changed <- any_(frp.set_style,scene.frp.camera_changed); - hosted_position <- host_changed.map(f_!(model.style.borrow().host_position())); - is_not_hosted <- hosted_position.map(|p| p.is_none()); + is_not_hosted <- host_changed.map(f!((_) model.style.borrow().host.is_none())); mouse_pos_rt <- mouse.position.gate(&is_not_hosted); + host_moved <- host_changed.flat_map(f!([model] (_) { + match model.style.borrow().host.as_ref().and_then(|t|t.value.as_ref()) { + None => frp::Source::<()>::new().into(), + Some(host) => host.on_transformed.clone(), + } + })); - eval_ host_changed([model,host_position,host_follow_weight] { + host_needs_update <- any(host_moved, host_changed); + eval_ host_needs_update([model,host_position,host_follow_weight] { match model.style.borrow().host.as_ref().and_then(|t|t.value.as_ref()) { None => host_follow_weight.set_target_value(0.0), Some(host) => { @@ -419,9 +418,15 @@ impl Cursor { let m1 = model.scene.layers.cursor.camera().inversed_view_matrix(); let m2 = model.scene.camera().view_matrix(); let position = host.global_position(); - let position = Vector4::new(position.x,position.y,position.z,1.0); + let size = host.computed_size(); + let position = Vector4::new( + position.x + size.x * 0.5, + position.y + size.y * 0.5, + position.z, + 1.0 + ); let position = m2 * (m1 * position); - host_position.set_target_value(Vector3(position.x,position.y,position.z)); + host_position.set_target_value(position.xyz()); } } }); diff --git a/lib/rust/frp/src/nodes.rs b/lib/rust/frp/src/nodes.rs index ae71e30afd53..fea69298e996 100644 --- a/lib/rust/frp/src/nodes.rs +++ b/lib/rust/frp/src/nodes.rs @@ -43,6 +43,14 @@ impl Network { self.register_raw(OwnedSource::new(label)) } + /// A source that emits a single event when it is dropped. This is always returned as an owned + /// node, so its drop timing can be precisely controlled. It is not automatically retained by + /// the network. When the source is cloned, the event will only be emitted after all clones are + /// dropped. + pub fn on_drop(&self, label: Label) -> DropSource { + DropSource::new(label) + } + /// Remember the last event value and allow sampling it anytime. pub fn sampler(&self, label: Label, src: &T) -> Sampler where @@ -422,6 +430,27 @@ impl Network { self.switch_constant(label, check, default(), t) } + /// Map the incoming value into a stream and connect the resulting stream to the output. When a + /// new value is emitted, the previous stream is disconnected and the new one is connected. + pub fn flat_map(&self, label: Label, src: &T, f: F) -> Stream + where + T: EventOutput, + Out: Data, + F: 'static + Fn(&Output) -> Stream, { + let output = self.any_mut(label); + let stream = output.clone().into(); + let last_proxy: RefCell> = RefCell::new(OwnedAny::new("flat_map proxy")); + + self.map(label, src, move |t| { + let stream = f(t); + last_proxy.replace_with(|_| { + let next_proxy = OwnedAny::new1("flat_map proxy", &stream); + output.attach(&next_proxy); + next_proxy + }); + }); + stream + } // === Any === @@ -4664,3 +4693,58 @@ where T: EventOutput } } } + + +// ================== +// === DropSource === +// ================== + + +/// A source that emits a single event when dropped. +#[derive(CloneRef, Debug, Clone)] +pub struct DropSource { + source: OwnedSource<()>, +} + +impl DropSource { + fn new(label: Label) -> Self { + Self { source: OwnedSource::new(label) } + } +} + +impl Drop for DropSource { + fn drop(&mut self) { + self.source.emit_event(&default(), &()); + } +} + +impl HasOutput for DropSource { + type Output = (); +} + +impl ValueProvider for DropSource { + fn value(&self) -> () { + () + } + + fn with(&self, _: impl FnOnce(&()) -> T) -> Option + where Self: Sized { + None + } +} + +impl stream::EventEmitter for DropSource { + fn emit_event(&self, _: CallStack, _: &()) {} + fn register_target(&self, target: stream::EventInput<()>) { + self.source.register_target(target) + } + fn register_watch(&self) -> watch::Handle { + watch::Handle::null() + } +} + +impl HasId for DropSource { + fn id(&self) -> Id { + self.source.id() + } +} From f8ec02be268e6a60ff110c6dcb6a6b86c0e8e675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Fri, 14 Apr 2023 18:06:13 +0200 Subject: [PATCH 11/45] correct spacing for widgets without ports, bigger hover area for primary ports --- .../src/component/node/input/port.rs | 29 ++++++------------- .../src/component/node/input/widget.rs | 22 ++++++++++++++ app/gui/view/graph-editor/src/lib.rs | 1 - .../core/src/display/object/instance.rs | 24 +++++++++++---- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index 0f0365040464..d3bd3f9cc034 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -31,11 +31,7 @@ pub const PRIMARY_PORT_HOVER_PADDING_Y: f32 = 4.0; /// The maximum depth of the widget port that is still considered primary. This is used to determine /// the hover area of the port. -pub const PRIMARY_PORT_MAX_DEPTH: usize = 2; - -/// Width of a single space glyph. Used to calculate padding based on the text offset between nodes. -// TODO: avoid using hardcoded value. See https://www.pivotaltracker.com/story/show/183567623. -pub const SPACE_GLYPH_WIDTH: f32 = 7.224_609_4; +pub const PRIMARY_PORT_MAX_DEPTH: usize = 0; @@ -109,7 +105,6 @@ pub(super) struct Port { widget: DynWidget, port_shape: shape::View, hover_shape: hover_shape::View, - last_offset: usize, last_node_depth: usize, is_primary: bool, } @@ -122,17 +117,17 @@ impl Port { let hover_shape = hover_shape::View::new(); port_root.add_child(&widget_root); - port_root.set_alignment_left(); + widget_root.set_margin_left(0.0); port_shape .allow_grow() .set_margin_left(-PORT_PADDING_X) .set_margin_right(-PORT_PADDING_X) - .set_alignment_left(); + .set_alignment_left_center(); hover_shape .allow_grow() .set_margin_left(-PORT_PADDING_X) .set_margin_right(-PORT_PADDING_X) - .set_alignment_left(); + .set_alignment_left_center(); let layers = app.display.default_scene.extension::(); layers.add_to_partition(hover_shape.display_object(), 0); @@ -183,7 +178,6 @@ impl Port { network, crumbs, is_primary: false, - last_offset: 0, last_node_depth: 0, } } @@ -217,14 +211,6 @@ impl Port { } fn set_port_layout(&mut self, ctx: &ConfigContext) { - let is_placeholder = ctx.span_tree_node.is_expected_argument(); - let min_offset = if is_placeholder { 1 } else { 0 }; - let offset = min_offset.max(ctx.span_tree_node.sibling_offset.as_usize()); - if self.last_offset != offset { - self.last_offset = offset; - self.port_root.set_margin_left(offset as f32 * SPACE_GLYPH_WIDTH); - } - let node_depth = ctx.span_tree_node.crumbs.len(); if self.last_node_depth != node_depth { self.last_node_depth = node_depth; @@ -235,8 +221,11 @@ impl Port { let is_primary = ctx.depth <= PRIMARY_PORT_MAX_DEPTH; if self.is_primary != is_primary { self.is_primary = is_primary; - let y_margin = if is_primary { 0.0 } else { PRIMARY_PORT_HOVER_PADDING_Y }; - self.hover_shape.set_margin_top(-y_margin).set_margin_bottom(-y_margin); + if is_primary { + self.hover_shape.set_size_y(crate::node::HEIGHT); + } else { + self.hover_shape.set_size_y_to_hug(); + } } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 4ea732bbbda5..093b3bf2b8bc 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -22,6 +22,18 @@ use text::index::Byte; +// ================= +// === Constants === +// ================= + +/// Spacing between sibling widgets per each code character that separates them. Current value is +/// based on the space glyph width at node's default font size, so that entering node edit mode +/// introduces the least amount of visual changes. The value can be adjusted once we implement +/// granular edit mode that works with widgets. +pub const WIDGET_SPACING_PER_OFFSET: f32 = 7.224_609_4; + + + // =========== // === FRP === // =========== @@ -714,6 +726,8 @@ impl<'a> WidgetTreeBuilder<'a> { }); let old_node = self.old_widgets.remove(&tree_ptr); + let is_placeholder = span_tree_node.is_expected_argument(); + let sibling_offset = span_tree_node.sibling_offset.as_usize(); let node = { let ctx = ConfigContext { builder: &mut *self, display: meta.display, span_tree_node, depth }; @@ -741,6 +755,14 @@ impl<'a> WidgetTreeBuilder<'a> { } }; let root = node.display_object().clone(); + + let offset = sibling_offset.max(if is_placeholder { 1 } else { 0 }); + let left_margin = offset as f32 * WIDGET_SPACING_PER_OFFSET; + if root.margin().x.start.as_pixels().map_or(true, |px| px != left_margin) { + root.set_margin_left(left_margin); + } + + self.new_widgets.insert(tree_ptr.clone(), node); // After visiting child node, restore previous layer's data. diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 6f7fea0eec5d..7c2ffa492084 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -2379,7 +2379,6 @@ impl GraphEditorModel { updated |= self.refresh_edge_position(edge_id); } } - warn!("refresh_incoming_edge_positions_and_relayout {updated} {node_ids:?}"); if updated { self.display_object().update(self.scene()); } diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index 6a76363dab07..44610515e63d 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -2656,6 +2656,12 @@ pub trait LayoutOps: Object { self.display_object().def.layout.size.get() } + /// Get the margin of the object.P lease note that this is user-set margin, not the computed + /// one. + fn margin(&self) -> Vector2 { + self.display_object().def.layout.margin.get() + } + /// Modify the size of the object. By default, the size is set to hug the children. You can set /// the size either to a fixed pixel value, a percentage parent container size, or to a fraction /// of the free space left after placing siblings with fixed sizes. @@ -3443,20 +3449,26 @@ impl Model { for child in &children { let to_grow = child.layout.grow_factor.get_dim(x) > 0.0; if to_grow { + let can_shrink = child.layout.shrink_factor.get_dim(x) > 0.0; let child_size = child.computed_size().get_dim(x); let child_margin = child.layout.margin.get_dim(x).resolve_pixels_or_default(); - let desired_child_size = self_size - child_margin.total(); + let mut desired_child_size = self_size - child_margin.total(); + + if !can_shrink { + let child_static_size = child.resolve_size_static_values(x, self_size); + desired_child_size = desired_child_size.max(child_static_size); + } if desired_child_size != child_size || child.should_refresh_layout() { child.layout.computed_size.set_dim(x, desired_child_size); child.refresh_layout_internal(x, PassConfig::DoNotHugDirectChildren); } - if child.layout.alignment.get().get_dim(x).is_some() { - // If child is set to grow, there will never be any leftover space to align - // it. It should always be positioned at 0.0 relative to its parent, plus - // taking the margin into account. - child.set_position_dim(x, child_margin.start); + if let Some(alignment) = *child.layout.alignment.get().get_dim(x) { + let remaining_size = self_size - desired_child_size - child_margin.total(); + let aligned_x = + remaining_size * alignment.normalized() + child_margin.start; + child.set_position_dim(x, aligned_x); } } } From 371fb9eb28eb5571362e65848b4e5f5eea42f4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Mon, 17 Apr 2023 21:17:26 +0200 Subject: [PATCH 12/45] better node coloring and more docs --- app/gui/language/span-tree/src/lib.rs | 4 + .../view/graph-editor/src/component/node.rs | 64 +++- .../src/component/node/input/area.rs | 92 +++-- .../src/component/node/input/port.rs | 24 +- .../src/component/node/input/widget.rs | 330 ++++++++++++------ .../component/node/input/widget/hierarchy.rs | 2 +- .../src/component/node/input/widget/label.rs | 14 +- .../node/input/widget/single_choice.rs | 2 +- app/gui/view/graph-editor/src/lib.rs | 3 +- .../ensogl/core/src/control/io/mouse/event.rs | 14 - lib/rust/prelude/src/collections.rs | 27 ++ 11 files changed, 396 insertions(+), 180 deletions(-) diff --git a/app/gui/language/span-tree/src/lib.rs b/app/gui/language/span-tree/src/lib.rs index 11704c80bdf6..25ea649547be 100644 --- a/app/gui/language/span-tree/src/lib.rs +++ b/app/gui/language/span-tree/src/lib.rs @@ -299,6 +299,10 @@ impl SpanTree { write!(buffer, " name={name:?}").unwrap(); } + if let Some(tp) = node.kind.tp() { + write!(buffer, " tp={tp:?}").unwrap(); + } + if let Some(call_id) = node.kind.call_id() { write!(buffer, " call_id={call_id:?}").unwrap(); } diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index fb13ae12f523..941aeb71a870 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -289,13 +289,66 @@ impl Default for Crumbs { } } -#[derive(Debug, Clone, Copy, Default)] + + +// ======================== +// === ConnectionStatus === +// ======================== + +/// The status of a single incoming connection to a specific port of a node. If a port has been +/// disconnected, the status is set to `Disconnected`. All ports are initially disconnected. +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, Default, PartialEq)] pub enum ConnectionStatus { #[default] Disconnected, - Connected { - color: color::Lcha, - }, + Connected(ConnectionData), +} + +impl ConnectionStatus { + /// Create a new `ConnectionStatus` representing an existing connection. + pub fn connected(color: color::Lcha) -> Self { + Self::Connected(ConnectionData { color }) + } + + /// Returns the data associated with the connection, if it exists. + pub fn data(&self) -> Option { + match self { + Self::Connected(data) => Some(*data), + Self::Disconnected => None, + } + } + + /// Combine two statuses of a connection. If the first status is `Disconnected`, the second one + /// will be returned. Otherwise, the first status will be returned. + pub fn or(self, other: Self) -> Self { + match self { + Self::Disconnected => other, + _ => self, + } + } + + /// Returns true if the port is connected. + pub fn is_connected(&self) -> bool { + matches!(self, Self::Connected(_)) + } +} + +impl From> for ConnectionStatus { + fn from(t: Option) -> Self { + match t { + Some(t) => Self::Connected(t), + None => Self::Disconnected, + } + } +} + +/// The data associated with a single existing incoming connection to a port. Holds the color +/// matching connected edge. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ConnectionData { + /// Color of an edge connected to the port. + pub color: color::Lcha, } @@ -647,8 +700,9 @@ impl NodeModel { #[profile(Debug)] fn set_expression_usage_type(&self, crumbs: &Crumbs, tp: &Option) { + warn!("set_expression_usage_type: {:?} {:?}", crumbs, tp); match crumbs.endpoint { - Endpoint::Input => {} //self.input.set_expression_usage_type(&crumbs.crumbs, tp), + Endpoint::Input => self.input.set_expression_usage_type(&crumbs.crumbs, tp), Endpoint::Output => self.output.set_expression_usage_type(&crumbs.crumbs, tp), } } diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 08c977588e72..0c3f442b59d9 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -149,7 +149,7 @@ pub struct Model { id_crumbs_map: RefCell>, styles: StyleWatch, styles_frp: StyleWatchFrp, - root_widget: widget::Root, + widget_tree: widget::Tree, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -170,7 +170,7 @@ impl Model { let expression = default(); let styles = StyleWatch::new(&app.display.default_scene.style_sheet); let styles_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet); - let root_widget = widget::Root::new(&app); + let widget_tree = widget::Tree::new(&app); Self { app, display_object, @@ -179,7 +179,7 @@ impl Model { id_crumbs_map, styles, styles_frp, - root_widget, + widget_tree, } .init() } @@ -190,12 +190,12 @@ impl Model { if edit_mode_active { let expression = self.expression.borrow(); self.edit_mode_label.set_content(expression.code.clone()); - self.display_object.remove_child(&self.root_widget); + self.display_object.remove_child(&self.widget_tree); self.display_object.add_child(&self.edit_mode_label); self.edit_mode_label.set_cursor_at_mouse_position(); } else { self.display_object.remove_child(&self.edit_mode_label); - self.display_object.add_child(&self.root_widget); + self.display_object.add_child(&self.widget_tree); self.edit_mode_label.set_content(""); } self.edit_mode_label.deprecated_set_focus(edit_mode_active); @@ -221,7 +221,7 @@ impl Model { let widgets_origin = Vector2(0.0, -NODE_HEIGHT / 2.0); let label_origin = Vector2(TEXT_OFFSET, TEXT_SIZE / 2.0); - self.root_widget.set_xy(widgets_origin); + self.widget_tree.set_xy(widgets_origin); self.edit_mode_label.set_xy(label_origin); self.set_edit_mode(false); @@ -237,14 +237,22 @@ impl Model { fn set_connected(&self, crumbs: &Crumbs, status: node::ConnectionStatus) { let expr = self.expression.borrow(); let port = expr.span_tree.get_node(crumbs).ok(); - port.map(|port| self.root_widget.set_connected(&port, status)); + port.map(|port| self.widget_tree.set_connected(&port, status)); + } + + /// Set usage type of the given port. + fn set_usage_type(&self, crumbs: &Crumbs, usage_type: Option) { + warn!("set_usage_type({crumbs:?}, {usage_type:?})"); + let expr = self.expression.borrow(); + let port = expr.span_tree.get_node(crumbs).ok(); + port.map(|port| self.widget_tree.set_usage_type(&port, usage_type)); } fn hover_pointer_style(&self, hovered: &Switch) -> Option { let crumbs = hovered.on()?; let expr = self.expression.borrow(); let port = expr.span_tree.get_node(crumbs).ok()?; - let display_object = self.root_widget.get_port_display_object(&port)?; + let display_object = self.widget_tree.get_port_display_object(&port)?; let tp = port.tp().map(|t| Type(t.into())); let color = tp.as_ref().map(|tp| type_coloring::compute(tp, &self.styles)); let pad_x = node::input::port::PORT_PADDING_X * 2.0; @@ -259,10 +267,14 @@ impl Model { for update in updates.iter() { let argument_name = update.argument_name.clone().into(); let meta_pointer = MetadataPointer { call_id: *call_id, argument_name }; - self.root_widget.set_metadata(meta_pointer, update.meta.clone()); + self.widget_tree.set_metadata(meta_pointer, update.meta.clone()); } + } + + /// If the widget tree was marked as dirty since its last update, rebuild it. + fn rebuild_widget_tree_if_dirty(&self) { let expr = self.expression.borrow(); - self.root_widget.rebuild_tree_on_metadata_change(&expr.span_tree, &expr.code, &self.styles); + self.widget_tree.rebuild_tree_if_dirty(&expr.span_tree, &expr.code, &self.styles); } /// Request widgets metadata for all method calls within the expression. @@ -281,23 +293,18 @@ impl Model { #[profile(Debug)] fn set_expression(&self, new_expression: impl Into, area_frp: &FrpEndpoints) { let new_expression = InputExpression::from(new_expression.into()); - debug!("set expression: \n{:?}", new_expression.tree_pretty_printer()); + warn!("set expression: \n{:?}", new_expression.tree_pretty_printer()); - self.root_widget.rebuild_tree( + self.widget_tree.rebuild_tree( &new_expression.span_tree, &new_expression.code, &self.styles, ); // TODO streams to handle: - // area_frp.source.pointer_style <+ pointer_style; - // area_frp.source.pointer_style - // pointer_style (cursor::Style), <- handle edit mode cursor change // view_mode (view::Mode), <- frp into widgets to change label color - - // self.init_port_frp_on_new_expression(&mut new_expression, area_frp); self.request_widgets_metadata(&new_expression, area_frp); *self.expression.borrow_mut() = new_expression; } @@ -335,6 +342,9 @@ ensogl::define_endpoints! { /// Disable the node (aka "skip mode"). set_disabled (bool), + /// Set read-only mode for input ports. + set_read_only (bool), + /// Set the connection status of the port indicated by the breadcrumbs. For connected ports, /// contains the color of connected edge. set_connected (Crumbs, node::ConnectionStatus), @@ -350,8 +360,10 @@ ensogl::define_endpoints! { set_view_mode (view::Mode), set_profiling_status (profiling::Status), - /// Set read-only mode for input ports. - set_read_only (bool), + /// Set the expression USAGE type. This is not the definition type, which can be set with + /// `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), } Output { @@ -420,8 +432,8 @@ impl Area { // This is meant to be on top of FRP network. Read more about `Node` docs to // learn more about the architecture and the importance of the hover // functionality. - frp.output.source.on_port_hover <+ model.root_widget.on_port_hover; - frp.output.source.on_port_press <+ model.root_widget.on_port_press; + frp.output.source.on_port_hover <+ model.widget_tree.on_port_hover; + frp.output.source.on_port_press <+ model.widget_tree.on_port_press; port_hover <- frp.on_port_hover.map(|t| t.is_on()); frp.output.source.body_hover <+ frp.set_hover || port_hover; @@ -453,10 +465,10 @@ impl Area { edit_or_ready <- frp.set_edit_ready_mode || set_editing; reacts_to_hover <- all_with(&edit_or_ready, ports_active, |e, (a, _)| *e && !a); port_vis <- all_with(&edit_or_ready, ports_active, |e, (a, _)| !e && *a); - frp.output.source.ports_visible <+ port_vis; - frp.output.source.editing <+ set_editing; - model.root_widget.ports_visible <+ frp.ports_visible; - refresh_edges <- model.root_widget.connected_port_updated.debounce(); + frp.output.source.ports_visible <+ port_vis; + frp.output.source.editing <+ set_editing; + model.widget_tree.set_ports_visible <+ frp.ports_visible; + refresh_edges <- model.widget_tree.connected_port_updated.debounce(); frp.output.source.input_edges_need_refresh <+ refresh_edges; // === Label Hover === @@ -466,27 +478,26 @@ impl Area { // === Port Hover === - eval frp.set_connected (((crumbs,status)) model.set_connected(crumbs,*status)); - hovered_port_pointer <- model.root_widget.on_port_hover.map( + hovered_port_pointer <- model.widget_tree.on_port_hover.map( f!((t) model.hover_pointer_style(t).unwrap_or_default()) ); pointer_style <- all[ - model.root_widget.pointer_style, + model.widget_tree.pointer_style, hovered_port_pointer ].fold(); frp.output.source.pointer_style <+ pointer_style; // === Properties === let layout_refresh = ensogl::animation::on_before_animations(); - root_widget_width <- layout_refresh.map(f!([model](_) { - let instance = model.root_widget.display_object(); + widget_tree_width <- layout_refresh.map(f!([model](_) { + let instance = model.widget_tree.display_object(); instance.computed_size().x() })).on_change(); padded_edit_label_width <- model.edit_mode_label.width.map(|t| t + 2.0 * TEXT_OFFSET); frp.output.source.width <+ set_editing.switch( - &root_widget_width, + &widget_tree_width, &padded_edit_label_width ); @@ -513,17 +524,28 @@ impl Area { (default(), e.into()) }); - widget_code_update <- model.root_widget.value_changed.map(|(crumbs, value)| { + model.widget_tree.set_view_mode <+ frp.set_view_mode; + widget_code_update <- model.widget_tree.value_changed.map(|(crumbs, value)| { let expression = value.clone().unwrap_or_default(); (crumbs.clone(), expression) }); frp.output.source.on_port_code_update <+ widget_code_update; - frp.output.source.request_import <+ model.root_widget.request_import; + frp.output.source.request_import <+ model.widget_tree.request_import; // === Widgets === - eval frp.update_widgets ((a) model.apply_widget_updates(a)); + eval frp.update_widgets((a) model.apply_widget_updates(a)); + eval frp.set_connected(((crumbs,status)) model.set_connected(crumbs,*status)); + eval frp.set_expression_usage_type(((cr,tp)) model.set_usage_type(cr,tp.clone())); + eval frp.set_disabled ((disabled) model.widget_tree.set_disabled(*disabled)); + widget_tree_invalidated <- any_(...); + widget_tree_invalidated <+ frp.update_widgets; + widget_tree_invalidated <+ frp.set_connected; + widget_tree_invalidated <+ frp.set_disabled; + widget_tree_invalidated <+ frp.set_expression_usage_type; + rebuild_widget_tree <- widget_tree_invalidated.debounce(); + eval rebuild_widget_tree((_) model.rebuild_widget_tree_if_dirty()); // === View Mode === @@ -551,7 +573,7 @@ impl Area { pub fn port_offset(&self, crumbs: &[Crumb]) -> Option> { let expr = self.model.expression.borrow(); let node = expr.get_node(crumbs).ok()?; - let instance = self.model.root_widget.get_port_display_object(&node)?; + let instance = self.model.widget_tree.get_port_display_object(&node)?; let pos = instance.global_position(); let node_pos = self.model.display_object.global_position(); let size = instance.computed_size(); diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index d3bd3f9cc034..9d3c717ae191 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -25,6 +25,9 @@ use ensogl::display::scene::layer::LayerSymbolPartition; /// boundary on both sides. pub const PORT_PADDING_X: f32 = 4.0; +/// The minimum size of the port visual area. +pub const BASE_PORT_HEIGHT: f32 = 18.0; + /// The vertical hover padding of ports at low depth. It affects how the port hover should extend /// the target text boundary on both sides. pub const PRIMARY_PORT_HOVER_PADDING_Y: f32 = 4.0; @@ -119,11 +122,13 @@ impl Port { port_root.add_child(&widget_root); widget_root.set_margin_left(0.0); port_shape + .set_size_y(BASE_PORT_HEIGHT) .allow_grow() .set_margin_left(-PORT_PADDING_X) .set_margin_right(-PORT_PADDING_X) .set_alignment_left_center(); hover_shape + .set_size_y(BASE_PORT_HEIGHT) .allow_grow() .set_margin_left(-PORT_PADDING_X) .set_margin_right(-PORT_PADDING_X) @@ -138,7 +143,7 @@ impl Port { let crumbs = Rc::new(RefCell::new(span_tree::Crumbs::default())); - if frp.ports_visible.value() { + if frp.set_ports_visible.value() { port_root.add_child(&hover_shape); } @@ -154,7 +159,7 @@ impl Port { ); frp.on_port_press <+ mouse_down.map(f!((_) crumbs.borrow().clone())); - eval frp.ports_visible([port_root, hover_shape] (active) { + eval frp.set_ports_visible([port_root, hover_shape] (active) { if *active { port_root.add_child(&hover_shape); } else { @@ -184,6 +189,7 @@ impl Port { pub fn configure(&mut self, config: &Config, ctx: ConfigContext) { self.crumbs.replace(ctx.span_tree_node.crumbs.clone()); + self.set_connected(ctx.state.connection); self.set_port_layout(&ctx); self.widget.configure(config, ctx); self.update_root(); @@ -191,9 +197,9 @@ impl Port { pub fn set_connected(&self, status: ConnectionStatus) { match status { - ConnectionStatus::Connected { color } => { + ConnectionStatus::Connected(data) => { self.port_root.add_child(&self.port_shape); - self.port_shape.color.set(color::Rgba::from(color).into()) + self.port_shape.color.set(color::Rgba::from(data.color).into()) } ConnectionStatus::Disconnected => { self.port_root.remove_child(&self.port_shape); @@ -218,14 +224,12 @@ impl Port { layers.add_to_partition(self.hover_shape.display_object(), node_depth); } - let is_primary = ctx.depth <= PRIMARY_PORT_MAX_DEPTH; + let is_primary = ctx.state.depth <= PRIMARY_PORT_MAX_DEPTH; if self.is_primary != is_primary { self.is_primary = is_primary; - if is_primary { - self.hover_shape.set_size_y(crate::node::HEIGHT); - } else { - self.hover_shape.set_size_y_to_hug(); - } + let margin = if is_primary { -PRIMARY_PORT_HOVER_PADDING_Y } else { 0.0 }; + self.hover_shape.set_margin_top(margin); + self.hover_shape.set_margin_bottom(margin); } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 093b3bf2b8bc..073666045ac9 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -7,6 +7,7 @@ use crate::prelude::*; use crate::component::node::input::area::NODE_HEIGHT; use crate::component::node::input::area::TEXT_OFFSET; use crate::component::node::input::port::Port; +use crate::component::node::ConnectionData; use crate::component::node::ConnectionStatus; use enso_config::ARGS; use enso_frp as frp; @@ -40,7 +41,9 @@ pub const WIDGET_SPACING_PER_OFFSET: f32 = 7.224_609_4; ensogl::define_endpoints_2! { Input { - ports_visible (bool), + set_ports_visible (bool), + set_view_mode (crate::view::Mode), + set_disabled (bool), } Output { value_changed (span_tree::Crumbs, Option), @@ -96,7 +99,14 @@ macro_rules! define_widget_modules( $($name(<$module::Widget as SpanWidget>::Config),)* } - /// A part of widget model that is dependant on the widget kind. + /// The node widget view. Represents one widget of any kind on the node input area. Can + /// change its appearance and behavior depending on the widget metadata updates, without + /// being recreated. New widget can be created using the `new` method, while the existing + /// widget can be reconfigured using the `configure` method. + /// + /// When a new configuration is applied, the existing widget will handle the update using + /// its `configure` method. If the new configuration requires a different widget kind, the + /// widget of new kind will be created and the old one will be dropped. #[derive(Debug)] #[allow(missing_docs)] pub(super) enum DynWidget { @@ -201,7 +211,11 @@ impl Metadata { Self::always(vector_editor::Config::default()) } - fn from_kind(kind: &span_tree::node::Kind, has_children: bool) -> Self { + fn from_kind( + kind: &span_tree::node::Kind, + _usage_type: Option, + has_children: bool, + ) -> Self { use span_tree::node::Kind; const VECTOR_TYPE: &str = "Standard.Base.Data.Vector.Vector"; @@ -299,7 +313,8 @@ impl DropdownValue for Entry { /// Widget FRP endpoints that can be used by widget views, and go straight to the root. #[derive(Debug, Clone, CloneRef)] pub struct WidgetsFrp { - pub(super) ports_visible: frp::Sampler, + pub(super) set_ports_visible: frp::Sampler, + pub(super) set_view_mode: frp::Sampler, pub(super) value_changed: frp::Any<(span_tree::Crumbs, Option)>, pub(super) request_import: frp::Any, pub(super) on_port_hover: frp::Any>, @@ -314,34 +329,34 @@ pub struct WidgetsFrp { // === Widget === // ============== -/// The node widget view. Represents one widget of any kind on the node input area. Can change its -/// appearance and behavior depending on the widget metadata updates, without being recreated. +/// The node widget tree view. Contains all widgets created from the node's span tree. #[derive(Debug, Deref, Clone, CloneRef)] -pub struct Root { +pub struct Tree { #[deref] frp: Frp, widgets_frp: WidgetsFrp, - model: Rc, + model: Rc, } -impl display::Object for Root { +impl display::Object for Tree { fn display_object(&self) -> &display::object::Instance { &self.model.display_object.outer } } -impl Root { +impl Tree { /// Create a new node widget. The widget is initialized to empty state, waiting for first /// `rebuild_tree` call to build appropriate view hierarchy. #[profile(Task)] pub fn new(app: &Application) -> Self { let frp = Frp::new(); - let model = Rc::new(RootModel::new(app)); + let model = Rc::new(TreeModel::new(app)); let network = &frp.network; frp::extend! { network - ports_visible <- frp.ports_visible.sampler(); + set_ports_visible <- frp.set_ports_visible.sampler(); + set_view_mode <- frp.set_view_mode.sampler(); } let value_changed = frp.private.output.value_changed.clone_ref(); @@ -351,11 +366,12 @@ impl Root { let pointer_style = frp.private.output.pointer_style.clone_ref(); let connected_port_updated = frp.private.output.connected_port_updated.clone_ref(); let widgets_frp = WidgetsFrp { + set_ports_visible, + set_view_mode, value_changed, request_import, on_port_hover, on_port_press, - ports_visible, pointer_style, connected_port_updated, }; @@ -372,19 +388,33 @@ impl Root { self.model.set_metadata(pointer, meta); } + pub fn set_usage_type( + &self, + tree_node: &span_tree::node::Ref, + usage_type: Option, + ) { + if let Some(pointer) = self.model.get_port_widget_pointer(tree_node) { + self.model.set_usage_type(pointer, usage_type); + } + } + pub fn set_connected(&self, tree_node: &span_tree::node::Ref, status: ConnectionStatus) { - self.model - .get_port_widget_pointer(tree_node) - .map(|pointer| self.model.with_port_mut(&pointer, |p| p.set_connected(status))); + if let Some(pointer) = self.model.get_port_widget_pointer(tree_node) { + self.model.set_connected(pointer, status); + } } - pub fn rebuild_tree_on_metadata_change( + pub fn set_disabled(&self, disabled: bool) { + self.model.set_disabled(disabled); + } + + pub fn rebuild_tree_if_dirty( &self, tree: &span_tree::SpanTree, node_expression: &str, styles: &StyleWatch, ) { - if self.model.metadata_dirty.load(Ordering::Acquire) { + if self.model.tree_dirty.load(Ordering::Acquire) { self.rebuild_tree(tree, node_expression, styles); } } @@ -407,19 +437,14 @@ impl Root { } } -/// ============= -/// === Model === -/// ============= -#[derive(Debug)] -struct RootModel { - app: Application, - display_object: debug::InstanceWithBg, - widgets_map: RefCell>, - metadata_map: Rc>>, - metadata_dirty: AtomicBool, -} +/// ================ +/// === TreeNode === +/// ================ +/// A single entry in the widget tree. If the widget has an attached port, it will be wrapped in +/// `Port` struct and stored in `Port` variant. Otherwise, the widget will be stored directly using +/// the `Widget` node variant. #[derive(Debug)] pub(super) enum TreeNode { /// A tree node that contains a port. The port wraps a widget. @@ -446,24 +471,25 @@ impl display::Object for TreeNode { } } -mod rectangle { - use super::*; - ensogl::shape! { - alignment = left_bottom; - (style: Style, color: Vector4) { - let color = Var::::from(color); - let width = Var::::from("input_size.x"); - let height = Var::::from("input_size.y"); - let rect = Rect((&width, &height)).corners_radius(5.0.px()); - let shape = rect.fill(color); - shape.into() - } - } -} +/// ================= +/// === TreeModel === +/// ================= + +#[derive(Debug)] +struct TreeModel { + app: Application, + display_object: debug::InstanceWithBg, + widgets_map: RefCell>, + metadata_map: Rc>>, + connected_map: Rc>>, + usage_type_map: Rc>>, + disabled: AtomicBool, + tree_dirty: AtomicBool, +} -impl RootModel { +impl TreeModel { /// Create a new node widget, selecting the appropriate widget type based on the provided /// argument info. fn new(app: &Application) -> Self { @@ -475,33 +501,52 @@ impl RootModel { display_object.inner.set_padding_left(TEXT_OFFSET); display_object.inner.set_padding_right(TEXT_OFFSET); - let widgets_map = default(); - let metadata_map = default(); - let metadata_dirty = default(); - Self { app, display_object, widgets_map, metadata_map, metadata_dirty } + Self { + app, + display_object, + disabled: default(), + widgets_map: default(), + metadata_map: default(), + connected_map: default(), + usage_type_map: default(), + tree_dirty: default(), + } } - /// Set the metadata for the given node data. + /// Set the metadata under given pointer. It may cause the tree to be marked as dirty. fn set_metadata(&self, pointer: MetadataPointer, meta: Option) { - use std::collections::hash_map::Entry; let mut map = self.metadata_map.borrow_mut(); - let entry = map.entry(pointer); - let dirty = match (entry, meta) { - (Entry::Occupied(e), None) => { - e.remove(); - true - } - (Entry::Occupied(mut e), Some(meta)) if e.get() != &meta => { - e.insert(meta); - true - } - (Entry::Vacant(e), Some(meta)) => { - e.insert(meta); - true - } - _ => false, - }; - self.metadata_dirty.store(dirty, Ordering::Release); + let dirty = map.synchronize_entry(pointer, meta); + if dirty { + self.tree_dirty.store(true, Ordering::Release); + } + } + + /// Set the connection status under given widget. It may cause the tree to be marked as dirty. + fn set_connected(&self, pointer: WidgetTreePointer, status: ConnectionStatus) { + let mut map = self.connected_map.borrow_mut(); + let dirty = map.synchronize_entry(pointer, status.data()); + if dirty { + self.tree_dirty.store(true, Ordering::Release); + } + } + + /// Set the usage type of an expression. It may cause the tree to be marked as dirty. + fn set_usage_type(&self, pointer: WidgetTreePointer, usage_type: Option) { + let mut map = self.usage_type_map.borrow_mut(); + warn!("Setting usage type of {:?} to {:?}", pointer, usage_type); + let dirty = map.synchronize_entry(pointer, usage_type); + if dirty { + self.tree_dirty.store(true, Ordering::Release); + } + } + + /// Set the connection status under given widget. It may cause the tree to be marked as dirty. + fn set_disabled(&self, disabled: bool) { + let prev_disabled = self.disabled.swap(disabled, Ordering::AcqRel); + if prev_disabled != disabled { + self.tree_dirty.store(true, Ordering::Release); + } } #[profile(Task)] @@ -512,17 +557,33 @@ impl RootModel { node_expression: &str, styles: &StyleWatch, ) { - self.metadata_dirty.store(false, Ordering::Release); + self.tree_dirty.store(false, Ordering::Release); let app = self.app.clone(); let metadata_map = self.metadata_map.borrow(); + let connected_map = self.connected_map.borrow(); + let usage_type_map = self.usage_type_map.borrow(); let widgets_map = self.widgets_map.take(); - let mut builder = - WidgetTreeBuilder::new(node_expression, app, frp, &*metadata_map, widgets_map, styles); + let node_disabled = self.disabled.load(Ordering::Acquire); + let mut builder = WidgetTreeBuilder::new( + node_expression, + node_disabled, + app, + frp, + &*metadata_map, + &*connected_map, + &*usage_type_map, + widgets_map, + styles, + ); let child = builder.child_widget(tree.root_ref(), 0); self.display_object.inner.replace_children(&[child]); self.widgets_map.replace(builder.new_widgets); } + /// Convert span tree node to a corresponding widget tree pointer. Every node in the span tree + /// has a unique representation in the form of a widget tree pointer, which is more stable + /// across changes in the span tree than [`span_tree::Crumbs`]. The pointer is used to identify + /// the widgets or ports in the widget tree. pub fn get_port_widget_pointer( &self, tree_node: &span_tree::node::Ref, @@ -558,6 +619,9 @@ impl RootModel { } } + /// Perform an operation on a shared reference to a tree node under given pointer. When there is + /// no node under provided pointer, the operation will not be performed and `None` will be + /// returned. pub fn with_node( &self, pointer: &WidgetTreePointer, @@ -566,6 +630,9 @@ impl RootModel { self.widgets_map.borrow().get(pointer).map(f) } + /// Perform an operation on a mutable reference to a tree node under given pointer. When there + /// is no node under provided pointer, the operation will not be performed and `None` will be + /// returned. pub fn with_node_mut( &self, pointer: &WidgetTreePointer, @@ -574,6 +641,9 @@ impl RootModel { self.widgets_map.borrow_mut().get_mut(pointer).map(f) } + /// Perform an operation on a mutable reference to a widget port under given pointer. When there + /// is no node under provided pointer, or when the found node has no port, the operation will + /// not be performed and `None` will be returned. pub fn with_port_mut( &self, pointer: &WidgetTreePointer, @@ -583,28 +653,43 @@ impl RootModel { } } +#[derive(Debug, Default, Clone, PartialEq)] +pub(super) struct NodeState { + pub depth: usize, + pub connection: ConnectionStatus, + pub subtree_connection: ConnectionStatus, + pub disabled: bool, + pub usage_type: Option, +} #[derive(Debug)] pub struct ConfigContext<'a, 'b> { builder: &'a mut WidgetTreeBuilder<'b>, - pub(super) display: Display, + /// Display mode of the widget built with this context. + #[allow(dead_code)] // currently unused, but will be used in the future + pub(super) display: Display, pub(super) span_tree_node: span_tree::node::Ref<'a>, - pub(super) depth: usize, + pub(super) state: NodeState, } impl<'a, 'b> ConfigContext<'a, 'b> { + /// Get the application instance, in which the widget tree is being built. pub fn app(&self) -> &Application { &self.builder.app } + /// Get the FRP endpoints shared by all widgets and ports in this tree. pub fn frp(&self) -> &WidgetsFrp { &self.builder.frp } + /// Get the code expression fragment represented by the given byte range. Can be combined with + /// [`span_tree::node::Ref`]'s `span` method to get the expression of a given span tree node. pub fn expression_at(&self, range: text::Range) -> &str { &self.builder.node_expression[range] } + /// Get the `StyleWatch` used by this node. pub fn styles(&self) -> &StyleWatch { self.builder.styles } @@ -623,20 +708,27 @@ struct WidgetTreeBuilder<'a> { app: Application, frp: WidgetsFrp, metadata_map: &'a HashMap, + connected_map: &'a HashMap, + usage_type_map: &'a HashMap, old_widgets: HashMap, new_widgets: HashMap, last_ast_id: Option, last_ast_id_crumbs: span_tree::Crumbs, + last_node_state: NodeState, node_expression: &'a str, + node_disabled: bool, styles: &'a StyleWatch, } impl<'a> WidgetTreeBuilder<'a> { fn new( node_expression: &'a str, + node_disabled: bool, app: Application, frp: WidgetsFrp, metadata_map: &'a HashMap, + connected_map: &'a HashMap, + usage_type_map: &'a HashMap, old_widgets: HashMap, styles: &'a StyleWatch, ) -> Self { @@ -644,11 +736,15 @@ impl<'a> WidgetTreeBuilder<'a> { app, frp, metadata_map, + connected_map, + usage_type_map, old_widgets, new_widgets: default(), last_ast_id: default(), last_ast_id_crumbs: default(), + last_node_state: default(), node_expression, + node_disabled, styles, } } @@ -672,7 +768,9 @@ impl<'a> WidgetTreeBuilder<'a> { self.create_child_widget(span_tree_node, depth, Some(meta)) } - + /// Create a new widget for given span tree node. This function recursively builds a subtree + /// of widgets, starting from the given node. The built subtree's root display object is + /// returned, so that it can be added to the parent's display hierarchy. fn create_child_widget( &mut self, span_tree_node: span_tree::node::Ref<'_>, @@ -705,8 +803,19 @@ impl<'a> WidgetTreeBuilder<'a> { WidgetTreePointer { id, crumbs } } }; - - + let old_node = self.old_widgets.remove(&tree_ptr); + let is_placeholder = span_tree_node.is_expected_argument(); + let sibling_offset = span_tree_node.sibling_offset.as_usize(); + let usage_type = self.usage_type_map.get(&tree_ptr).cloned(); + + // Get widget metadata. There are three potential sources for metadata, that are used in + // order, whichever is available first: + // 1. The `set_metadata` argument, which can be set by the parent widget if it wants to + // override the metadata for its child. + // 2. The `MetadataPointer` stored in the span tree node. This can be set by an external + // source (e.g. based on language server) to override the default metadata for the node. + // 3. The default metadata for the node, which is determined based on the node kind, usage + // type and whether it has children. let mut meta_fallback = None; let kind = &span_tree_node.kind; let has_children = !span_tree_node.children.is_empty(); @@ -722,47 +831,56 @@ impl<'a> WidgetTreeBuilder<'a> { meta_pointer.and_then(|ptr| self.metadata_map.get(&ptr)) }) .unwrap_or_else(|| { - meta_fallback.get_or_insert_with(|| Metadata::from_kind(kind, has_children)) + meta_fallback.get_or_insert_with(|| { + Metadata::from_kind(kind, usage_type.clone(), has_children) + }) }); - let old_node = self.old_widgets.remove(&tree_ptr); - let is_placeholder = span_tree_node.is_expected_argument(); - let sibling_offset = span_tree_node.sibling_offset.as_usize(); - let node = { - let ctx = - ConfigContext { builder: &mut *self, display: meta.display, span_tree_node, depth }; - // Widget creation/update can recurse into the builder. All borrows must be dropped - // at this point. - let app = ctx.app(); - let frp = ctx.frp(); - - if meta.has_port { - let mut port = match old_node { - Some(TreeNode::Port(port)) => port, - Some(TreeNode::Widget(widget)) => Port::new(widget, app, frp), - None => Port::new(DynWidget::new(&meta.config, app, frp), app, frp), - }; - port.configure(&meta.config, ctx); - TreeNode::Port(port) - } else { - let mut widget = match old_node { - Some(TreeNode::Port(port)) => port.into_widget(), - Some(TreeNode::Widget(widget)) => widget, - None => DynWidget::new(&meta.config, app, frp), - }; - widget.configure(&meta.config, ctx); - TreeNode::Widget(widget) - } + + /// Once we have the metadata and potential old widget to reuse, we have to apply the + /// configuration to the widget. + let connection: ConnectionStatus = self.connected_map.get(&tree_ptr).copied().into(); + let subtree_connection = connection.or(self.last_node_state.subtree_connection); + let disabled = self.node_disabled; + let state = NodeState { depth, connection, subtree_connection, disabled, usage_type }; + let mut state_to_restore = std::mem::replace(&mut self.last_node_state, state.clone()); + + + let ctx = + ConfigContext { builder: &mut *self, display: meta.display, span_tree_node, state }; + let app = ctx.app(); + let frp = ctx.frp(); + + // Widget creation/update can recurse into the builder. All borrows must be dropped + // at this point. + let node = if meta.has_port { + let mut port = match old_node { + Some(TreeNode::Port(port)) => port, + Some(TreeNode::Widget(widget)) => Port::new(widget, app, frp), + None => Port::new(DynWidget::new(&meta.config, app, frp), app, frp), + }; + port.configure(&meta.config, ctx); + TreeNode::Port(port) + } else { + let mut widget = match old_node { + Some(TreeNode::Port(port)) => port.into_widget(), + Some(TreeNode::Widget(widget)) => widget, + None => DynWidget::new(&meta.config, app, frp), + }; + widget.configure(&meta.config, ctx); + TreeNode::Widget(widget) }; - let root = node.display_object().clone(); + self.last_node_state = state_to_restore; + + // Apply left margin to the widget, based on its offset relative to the previous sibling. + let root = node.display_object().clone(); let offset = sibling_offset.max(if is_placeholder { 1 } else { 0 }); let left_margin = offset as f32 * WIDGET_SPACING_PER_OFFSET; if root.margin().x.start.as_pixels().map_or(true, |px| px != left_margin) { root.set_margin_left(left_margin); } - self.new_widgets.insert(tree_ptr.clone(), node); // After visiting child node, restore previous layer's data. diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs index a9d3ebb96877..c7dae41cd97e 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -37,7 +37,7 @@ impl super::SpanWidget for Widget { fn configure(&mut self, _: &Config, ctx: super::ConfigContext) { let increase_depth = ctx.span_tree_node.is_argument(); - let next_depth = if increase_depth { ctx.depth + 1 } else { ctx.depth }; + let next_depth = if increase_depth { ctx.state.depth + 1 } else { ctx.state.depth }; let children = ctx .span_tree_node diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 3c7b9a2cb447..25beb8991266 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -90,13 +90,15 @@ impl super::SpanWidget for Widget { ctx.expression_at(ctx.span_tree_node.span()) }; - - let text_color = if is_placeholder { - ctx.styles().get_color(theme::graph_editor::node::text); - color::Lcha::new(0.5, 0.0, 0.0, 1.0) + let text_color: color::Lcha = if ctx.state.subtree_connection.is_connected() { + ctx.styles().get_color(theme::code::types::selected).into() + } else if ctx.state.disabled { + ctx.styles().get_color(theme::code::syntax::disabled).into() + } else if is_placeholder { + ctx.styles().get_color(theme::code::syntax::expected).into() } else { - let ty = ctx.span_tree_node.kind.tp(); - let ty = ty.map(|t| crate::Type(t.into())); + let ty = ctx.state.usage_type.clone(); + let ty = ty.or_else(|| ctx.span_tree_node.kind.tp().map(|t| crate::Type(t.into()))); crate::type_coloring::compute_for_code(ty.as_ref(), &ctx.styles()) }; diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index aeb57fadb046..d50f6b024399 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -270,7 +270,7 @@ impl super::SpanWidget for Widget { // Do not increment the depth. If the dropdown is displayed, it should also display // its arguments. let children = - chain.map(|child| ctx.builder.child_widget(child, ctx.depth)).collect_vec(); + chain.map(|child| ctx.builder.child_widget(child, ctx.state.depth)).collect_vec(); self.args_wrapper.replace_children(&children); } else { self.args_wrapper.remove_all_children(); diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 7c2ffa492084..2f11a27deedf 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -1942,8 +1942,7 @@ impl GraphEditorModel { neutral_color: color::Lcha, ) { let status = match status { - true => - node::ConnectionStatus::Connected { color: self.edge_color(edge_id, neutral_color) }, + true => node::ConnectionStatus::connected(self.edge_color(edge_id, neutral_color)), false => node::ConnectionStatus::Disconnected, }; self.set_input_connected(target, status); diff --git a/lib/rust/ensogl/core/src/control/io/mouse/event.rs b/lib/rust/ensogl/core/src/control/io/mouse/event.rs index ff5dc060ac11..519105025fcc 100644 --- a/lib/rust/ensogl/core/src/control/io/mouse/event.rs +++ b/lib/rust/ensogl/core/src/control/io/mouse/event.rs @@ -140,20 +140,6 @@ where JsEvent: AsRef ) } - pub fn is_primary(&self) -> bool { - self.button() == mouse::PrimaryButton - } - - /// Indicates whether the secondary mouse button was pressed when the event was triggered. - pub fn is_secondary(&self) -> bool { - self.button() == mouse::SecondaryButton - } - - /// Indicates whether the middle mouse button was pressed when the event was triggered. - pub fn is_middle(&self) -> bool { - self.button() == mouse::MiddleButton - } - /// Return the position relative to the event handler that was used to catch the event. If the /// event handler does not have a position in the DOM, the returned position will be relative to /// the viewport. This can happen if the event handler is, for example, the window. diff --git a/lib/rust/prelude/src/collections.rs b/lib/rust/prelude/src/collections.rs index 989256cd5415..c4cc3ea8b504 100644 --- a/lib/rust/prelude/src/collections.rs +++ b/lib/rust/prelude/src/collections.rs @@ -39,6 +39,33 @@ pub trait HashMapOps: BorrowMut> + Sized { } } } + + /// Writes or removes the value at the given key. Returns `true` if the action had any effect. + /// Returns `false` when equal value was already present at the given key, or when the value + /// was already absent and the removal was requested. + fn synchronize_entry<'a>(&'a mut self, key: K, value: Option) -> bool + where + K: std::hash::Hash + Eq + 'a, + V: PartialEq, { + use std::collections::hash_map::Entry; + let map = self.borrow_mut(); + let entry = map.entry(key); + match (entry, value) { + (Entry::Occupied(e), None) => { + e.remove(); + true + } + (Entry::Occupied(mut e), Some(value)) if e.get() != &value => { + e.insert(value); + true + } + (Entry::Vacant(e), Some(value)) => { + e.insert(value); + true + } + _ => false, + } + } } impl HashMapOps for HashMap {} From 0dfec50b6ca886fe15b22599c88942d4a73300b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 18 Apr 2023 15:24:31 +0200 Subject: [PATCH 13/45] add more docs --- .../view/graph-editor/src/component/node.rs | 2 +- .../src/component/node/input/area.rs | 5 +- .../src/component/node/input/widget.rs | 181 ++++++++++-------- .../src/component/node/input/widget/label.rs | 1 + .../node/input/widget/single_choice.rs | 19 +- app/gui/view/graph-editor/src/lib.rs | 31 +++ 6 files changed, 152 insertions(+), 87 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index 941aeb71a870..93fac16708e1 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -958,7 +958,7 @@ impl Node { preview_visible <- hover_preview_visible || preview_enabled; preview_visible <- preview_visible.on_change(); - // If the preview is visible while the visualization button is disabled, clic\king the + // If the preview is visible while the visualization button is disabled, clicking the // visualization button hides the preview and keeps the visualization button disabled. vis_button_on <- visualization_button_state.filter(|e| *e).constant(()); vis_button_off <- visualization_button_state.filter(|e| !*e).constant(()); diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 0c3f442b59d9..866b5691f3c9 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -241,8 +241,7 @@ impl Model { } /// Set usage type of the given port. - fn set_usage_type(&self, crumbs: &Crumbs, usage_type: Option) { - warn!("set_usage_type({crumbs:?}, {usage_type:?})"); + fn set_expression_usage_type(&self, crumbs: &Crumbs, usage_type: Option) { let expr = self.expression.borrow(); let port = expr.span_tree.get_node(crumbs).ok(); port.map(|port| self.widget_tree.set_usage_type(&port, usage_type)); @@ -537,7 +536,7 @@ impl Area { eval frp.update_widgets((a) model.apply_widget_updates(a)); eval frp.set_connected(((crumbs,status)) model.set_connected(crumbs,*status)); - eval frp.set_expression_usage_type(((cr,tp)) model.set_usage_type(cr,tp.clone())); + eval frp.set_expression_usage_type(((cr,tp)) model.set_expression_usage_type(cr,tp.clone())); eval frp.set_disabled ((disabled) model.widget_tree.set_disabled(*disabled)); widget_tree_invalidated <- any_(...); widget_tree_invalidated <+ frp.update_widgets; diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 073666045ac9..b7dea733f424 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -388,6 +388,8 @@ impl Tree { self.model.set_metadata(pointer, meta); } + /// Set usage type for given span tree node. The usage type is used to determine the widget + /// appearance and default inferred widget metadata. pub fn set_usage_type( &self, tree_node: &span_tree::node::Ref, @@ -398,16 +400,24 @@ impl Tree { } } + /// Set connection status for given span tree node. The connected nodes will be highlighted + /// with a different color, and the widgets might change behavior depending on the connection + /// status. pub fn set_connected(&self, tree_node: &span_tree::node::Ref, status: ConnectionStatus) { if let Some(pointer) = self.model.get_port_widget_pointer(tree_node) { self.model.set_connected(pointer, status); } } + /// Set disabled status for given span tree node. The disabled nodes will be grayed out. + /// The widgets might change behavior depending on the disabled status. pub fn set_disabled(&self, disabled: bool) { self.model.set_disabled(disabled); } + /// Rebuild tree if it has been marked as dirty. The dirty flag is marked whenever more data + /// external to the span-tree is provided, using `set_metadata`, `set_usage_type`, + /// `set_connected` or `set_disabled` methods of the widget tree. pub fn rebuild_tree_if_dirty( &self, tree: &span_tree::SpanTree, @@ -419,6 +429,10 @@ impl Tree { } } + /// Rebuild the widget tree using given span tree expression. All widgets necessary for the + /// provided expression will be created and added to the view hierarchy. If the tree has been + /// already built, existing widgets will be reused in the parts of the expression that did not + /// change since then. pub fn rebuild_tree( &self, tree: &span_tree::SpanTree, @@ -428,6 +442,9 @@ impl Tree { self.model.rebuild_tree(self.widgets_frp.clone_ref(), tree, node_expression, styles) } + /// Get the root display object of the widget port for given span tree node. Not all widgets + /// have a port, so the returned value might be `None` even if a widget exist for given span + /// tree node. pub fn get_port_display_object( &self, tree_node: &span_tree::node::Ref, @@ -562,19 +579,24 @@ impl TreeModel { let metadata_map = self.metadata_map.borrow(); let connected_map = self.connected_map.borrow(); let usage_type_map = self.usage_type_map.borrow(); - let widgets_map = self.widgets_map.take(); + let old_widgets = self.widgets_map.take(); let node_disabled = self.disabled.load(Ordering::Acquire); - let mut builder = WidgetTreeBuilder::new( - node_expression, - node_disabled, + let mut builder = WidgetTreeBuilder { app, frp, - &*metadata_map, - &*connected_map, - &*usage_type_map, - widgets_map, + node_disabled, + node_expression, styles, - ); + metadata_map: &metadata_map, + connected_map: &connected_map, + usage_type_map: &usage_type_map, + old_widgets, + new_widgets: default(), + parent_ast_id: default(), + parent_crumbs: default(), + parent_state: default(), + }; + let child = builder.child_widget(tree.root_ref(), 0); self.display_object.inner.replace_children(&[child]); self.widgets_map.replace(builder.new_widgets); @@ -703,54 +725,49 @@ struct WidgetTreePointer { crumbs: span_tree::Crumbs, } + + +/// ========================= +/// === WidgetTreeBuilder === +/// ========================= + +/// A builder for the widget tree. Maintains transient state necessary during the tree construction, +/// and provides methods for creating child nodes of the tree. Maintains a map of all widgets +/// created so far, and is able to reuse existing widgets under the same location in the tree, only +/// updating their configuration as necessary. #[derive(Debug)] struct WidgetTreeBuilder<'a> { - app: Application, - frp: WidgetsFrp, - metadata_map: &'a HashMap, - connected_map: &'a HashMap, - usage_type_map: &'a HashMap, - old_widgets: HashMap, - new_widgets: HashMap, - last_ast_id: Option, - last_ast_id_crumbs: span_tree::Crumbs, - last_node_state: NodeState, - node_expression: &'a str, - node_disabled: bool, - styles: &'a StyleWatch, + app: Application, + frp: WidgetsFrp, + node_disabled: bool, + node_expression: &'a str, + styles: &'a StyleWatch, + metadata_map: &'a HashMap, + connected_map: &'a HashMap, + usage_type_map: &'a HashMap, + old_widgets: HashMap, + new_widgets: HashMap, + parent_ast_id: Option, + parent_crumbs: span_tree::Crumbs, + parent_state: NodeState, } impl<'a> WidgetTreeBuilder<'a> { - fn new( - node_expression: &'a str, - node_disabled: bool, - app: Application, - frp: WidgetsFrp, - metadata_map: &'a HashMap, - connected_map: &'a HashMap, - usage_type_map: &'a HashMap, - old_widgets: HashMap, - styles: &'a StyleWatch, - ) -> Self { - Self { - app, - frp, - metadata_map, - connected_map, - usage_type_map, - old_widgets, - new_widgets: default(), - last_ast_id: default(), - last_ast_id_crumbs: default(), - last_node_state: default(), - node_expression, - node_disabled, - styles, - } - } - + /// Create a new child widget. The widget type will be automatically inferred, either based on + /// the node kind, or on the metadata provided from the language server. If possible, an + /// existing widget will be reused under the same location in the tree, only updating its + /// configuration as necessary. If no widget can be reused, a new one will be created. + /// + /// The root display object of the created widget will be returned, and it must be inserted into + /// the display object hierarchy by the caller. It will be common that the returned display + /// object will frequently not change between subsequent widget `configure` calls, but it will + /// eventually happen if the child widget is relocated or changed its type. The caller must be + /// prepared to handle that situation, and never rely on it not changing. In order to handle + /// that efficiently, the caller can use the `replace_children` method of + /// [`display::object::InstanceDef`], which will only perform hierarchy updates if the children + /// list has been actually modified. #[must_use] - pub(self) fn child_widget( + pub fn child_widget( &mut self, span_tree_node: span_tree::node::Ref<'_>, depth: usize, @@ -758,8 +775,13 @@ impl<'a> WidgetTreeBuilder<'a> { self.create_child_widget(span_tree_node, depth, None) } + /// Create a new child widget. Does not infer the widget type, but uses the provided metadata + /// instead. That way, the parent widget can explicitly control the type of the child widget. + /// + /// See [`child_widget`] method for more details about widget creation. #[must_use] - pub(self) fn child_widget_of_type( + #[allow(dead_code)] // Currently unused, but will be used in the future in VectorEditor. + pub fn child_widget_of_type( &mut self, span_tree_node: span_tree::node::Ref<'_>, depth: usize, @@ -786,19 +808,19 @@ impl<'a> WidgetTreeBuilder<'a> { // contained a widget for this pointer, we have to reuse it. let tree_ptr = match span_tree_node.ast_id { Some(ast_id) => { - let prev_ast_id = self.last_ast_id.replace(ast_id); - let prev_crumbs = - std::mem::replace(&mut self.last_ast_id_crumbs, span_tree_node.crumbs.clone()); - ast_data_to_restore = Some((prev_ast_id, prev_crumbs)); + let parent_ast_id = self.parent_ast_id.replace(ast_id); + let parent_tail_crumbs = + std::mem::replace(&mut self.parent_crumbs, span_tree_node.crumbs.clone()); + ast_data_to_restore = Some((parent_ast_id, parent_tail_crumbs)); WidgetTreePointer { id: Some(ast_id), crumbs: default() } } None => { let this_crumbs = &span_tree_node.crumbs; // We should always be in a child node since last ast ID. Verify that. - let is_in_ast_subtree = this_crumbs.starts_with(&self.last_ast_id_crumbs); + let is_in_ast_subtree = this_crumbs.starts_with(&self.parent_crumbs); assert!(is_in_ast_subtree, "Not in AST child node."); - let id = self.last_ast_id; - let crumbs_since_id = &this_crumbs[self.last_ast_id_crumbs.len()..]; + let id = self.parent_ast_id; + let crumbs_since_id = &this_crumbs[self.parent_crumbs.len()..]; let crumbs = span_tree::Crumbs::new(crumbs_since_id.to_vec()); WidgetTreePointer { id, crumbs } } @@ -837,14 +859,13 @@ impl<'a> WidgetTreeBuilder<'a> { }); - /// Once we have the metadata and potential old widget to reuse, we have to apply the - /// configuration to the widget. + // Once we have the metadata and potential old widget to reuse, we have to apply the + // configuration to the widget. let connection: ConnectionStatus = self.connected_map.get(&tree_ptr).copied().into(); - let subtree_connection = connection.or(self.last_node_state.subtree_connection); + let subtree_connection = connection.or(self.parent_state.subtree_connection); let disabled = self.node_disabled; let state = NodeState { depth, connection, subtree_connection, disabled, usage_type }; - let mut state_to_restore = std::mem::replace(&mut self.last_node_state, state.clone()); - + let state_to_restore = std::mem::replace(&mut self.parent_state, state.clone()); let ctx = ConfigContext { builder: &mut *self, display: meta.display, span_tree_node, state }; @@ -852,8 +873,12 @@ impl<'a> WidgetTreeBuilder<'a> { let frp = ctx.frp(); // Widget creation/update can recurse into the builder. All borrows must be dropped - // at this point. - let node = if meta.has_port { + // at this point. The `configure` calls on the widgets are allowed to call back into the + // tree builder in order to create their child widgets. Those calls will change builder's + // state to reflect the correct parent node. We need to restore the state after the + // `configure` call has been done, so that the next sibling node will receive correct parent + // data. + let child_node = if meta.has_port { let mut port = match old_node { Some(TreeNode::Port(port)) => port, Some(TreeNode::Widget(widget)) => Port::new(widget, app, frp), @@ -871,23 +896,23 @@ impl<'a> WidgetTreeBuilder<'a> { TreeNode::Widget(widget) }; - self.last_node_state = state_to_restore; + // After visiting child node, restore previous layer's parent data. + self.parent_state = state_to_restore; + if let Some((id, crumbs)) = ast_data_to_restore { + self.parent_ast_id = id; + self.parent_crumbs = crumbs; + } + // Apply left margin to the widget, based on its offset relative to the previous sibling. - let root = node.display_object().clone(); + let child_root = child_node.display_object().clone(); let offset = sibling_offset.max(if is_placeholder { 1 } else { 0 }); let left_margin = offset as f32 * WIDGET_SPACING_PER_OFFSET; - if root.margin().x.start.as_pixels().map_or(true, |px| px != left_margin) { - root.set_margin_left(left_margin); + if child_root.margin().x.start.as_pixels().map_or(true, |px| px != left_margin) { + child_root.set_margin_left(left_margin); } - self.new_widgets.insert(tree_ptr.clone(), node); - - // After visiting child node, restore previous layer's data. - if let Some((id, crumbs)) = ast_data_to_restore { - self.last_ast_id = id; - self.last_ast_id_crumbs = crumbs; - } - root + self.new_widgets.insert(tree_ptr.clone(), child_node); + child_root } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 25beb8991266..62b5df610846 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -31,6 +31,7 @@ ensogl::define_endpoints_2! { pub struct Widget { frp: Frp, root: InstanceWithBg, + #[allow(dead_code)] label: text::Text, } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index d50f6b024399..877043f7fb7b 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -14,15 +14,24 @@ use ensogl::display::object::event; use ensogl_component::drop_down::Dropdown; use ensogl_component::text; + + /// ================= /// === Constants === /// ================= +/// Color of the activation triangle shape. pub const ACTIVATION_SHAPE_COLOR: color::Lch = color::Lch::new(0.56708, 0.23249, 0.71372); -const ACTIVATION_SHAPE_Y_OFFSET: f32 = 15.0; + +/// Height of the activation triangle shape. pub const ACTIVATION_SHAPE_SIZE: Vector2 = Vector2(15.0, 11.0); -/// Distance between the dropdown and the bottom of the port. -const DROPDOWN_Y_OFFSET: f32 = 5.0; + +/// Distance between the top of the dropdown list and the bottom of the widget. +const DROPDOWN_Y_OFFSET: f32 = -20.0; + +/// Maximum allowed size of the dropdown list. If the list needs to be longer or wider than allowed +/// by these values, it will receive a scroll bar. +const DROPDOWN_MAX_SIZE: Vector2 = Vector2(300.0, 500.0); // ====================== // === Triangle Shape === @@ -124,8 +133,8 @@ impl super::SpanWidget for Widget { let dropdown = app.new_view::>(); layers.above_nodes.add(&dropdown); - dropdown.set_y(-20.0); - dropdown.set_max_open_size(Vector2(300.0, 500.0)); + dropdown.set_y(DROPDOWN_Y_OFFSET); + dropdown.set_max_open_size(DROPDOWN_MAX_SIZE); dropdown.allow_deselect_all(true); let display_object = InstanceWithBg::magenta(); diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 2f11a27deedf..935e5c4aa061 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -1612,6 +1612,14 @@ impl GraphEditorModelWithNetwork { (_) model.frp.private.output.node_incoming_edge_updates.emit(node_id) ); + let neutral_color = model.styles_frp.get_color(theme::code::types::any::selection); + _eval <- node_model.output.frp.on_port_type_change.map2(&neutral_color, + f!(((crumbs,_),neutral_color) + model.with_output_edge_id(node_id,crumbs,|id| + model.refresh_edge_color(id,neutral_color.into()) + ) + )); + let is_editing = &node_model.input.frp.editing; expression_change_temporary <- node.on_expression_modified.gate(is_editing); expression_change_permanent <- node.on_expression_modified.gate_not(is_editing); @@ -2417,6 +2425,29 @@ impl GraphEditorModel { self.with_edge_map_target(id, |endpoint| endpoint) } + // FIXME[WD]: This implementation is slow. Node should allow for easy mapping between Crumbs + // and edges. Should be part of https://github.com/enso-org/ide/issues/822. + fn with_output_edge_id( + &self, + id: NodeId, + crumbs: &span_tree::Crumbs, + f: impl FnOnce(EdgeId) -> T, + ) -> Option { + self.with_node(id, move |node| { + let mut target_edge_id = None; + for edge_id in node.out_edges.keys() { + self.with_edge(edge_id, |edge| { + let ok = edge.target().map(|tgt| tgt.port == crumbs) == Some(true); + if ok { + target_edge_id = Some(edge_id) + } + }); + } + target_edge_id.map(f) + }) + .flatten() + } + fn with_edge_source(&self, id: EdgeId, f: impl FnOnce(EdgeEndpoint) -> T) -> Option { self.with_edge(id, |edge| { let source = edge.source.borrow().deref().clone(); From 1db7de21f3f473a62c57d8f905c43e56351204c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 18 Apr 2023 16:36:54 +0200 Subject: [PATCH 14/45] properly propagate expression usage type --- .../view/graph-editor/src/component/node.rs | 20 ++----- .../src/component/node/input/area.rs | 31 +++-------- .../src/component/node/input/widget.rs | 53 +++++++++++++------ .../src/component/node/output/area.rs | 21 ++++---- app/gui/view/graph-editor/src/lib.rs | 9 +--- 5 files changed, 57 insertions(+), 77 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index 93fac16708e1..78540d235a9c 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -378,7 +378,7 @@ ensogl::define_endpoints_2! { /// Set the expression USAGE type. This is not the definition type, which can be set with /// `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), + set_expression_usage_type (ast::Id, Option), update_widgets (WidgetUpdates), set_output_expression_visibility (bool), set_vcs_status (Option), @@ -626,13 +626,6 @@ impl NodeModel { .init() } - #[profile(Debug)] - #[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs. - pub fn get_crumbs_by_id(&self, id: ast::Id) -> Option { - let input_crumbs = self.input.get_crumbs_by_id(id).map(Crumbs::input); - input_crumbs.or_else(|| self.output.get_crumbs_by_id(id).map(Crumbs::output)) - } - #[profile(Debug)] fn init(self) -> Self { self.set_expression(Expression::new_plain("empty")); @@ -699,12 +692,9 @@ impl NodeModel { } #[profile(Debug)] - fn set_expression_usage_type(&self, crumbs: &Crumbs, tp: &Option) { - warn!("set_expression_usage_type: {:?} {:?}", crumbs, tp); - match crumbs.endpoint { - Endpoint::Input => self.input.set_expression_usage_type(&crumbs.crumbs, tp), - Endpoint::Output => self.output.set_expression_usage_type(&crumbs.crumbs, tp), - } + fn set_expression_usage_type(&self, id: ast::Id, tp: &Option) { + self.input.set_expression_usage_type(id, tp); + self.output.set_expression_usage_type(id, tp); } #[profile(Debug)] @@ -826,7 +816,7 @@ impl Node { filtered_usage_type <- input.set_expression_usage_type.filter( move |(_,tp)| *tp != unresolved_symbol_type ); - eval filtered_usage_type (((a,b)) model.set_expression_usage_type(a,b)); + eval filtered_usage_type (((a,b)) model.set_expression_usage_type(*a,b)); eval input.set_expression ((a) model.set_expression(a)); model.input.edit_expression <+ input.edit_expression; out.on_expression_modified <+ model.input.frp.on_port_code_update; diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 866b5691f3c9..972de69bb02b 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -146,7 +146,6 @@ pub struct Model { display_object: display::object::Instance, edit_mode_label: text::Text, expression: RefCell, - id_crumbs_map: RefCell>, styles: StyleWatch, styles_frp: StyleWatchFrp, widget_tree: widget::Tree, @@ -166,22 +165,12 @@ impl Model { let display_object = display::object::Instance::new(); let edit_mode_label = app.new_view::(); - let id_crumbs_map = default(); let expression = default(); let styles = StyleWatch::new(&app.display.default_scene.style_sheet); let styles_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet); let widget_tree = widget::Tree::new(&app); - Self { - app, - display_object, - edit_mode_label, - expression, - id_crumbs_map, - styles, - styles_frp, - widget_tree, - } - .init() + Self { app, display_object, edit_mode_label, expression, styles, styles_frp, widget_tree } + .init() } /// React to edit mode change. Shows and hides appropriate child views according to current @@ -230,7 +219,6 @@ impl Model { fn set_label_layer(&self, layer: &display::scene::Layer) { self.edit_mode_label.add_to_scene_layer(layer); - // self.ports_label.add_to_scene_layer(layer); } /// Set connection status of the given port. @@ -241,10 +229,8 @@ impl Model { } /// Set usage type of the given port. - fn set_expression_usage_type(&self, crumbs: &Crumbs, usage_type: Option) { - let expr = self.expression.borrow(); - let port = expr.span_tree.get_node(crumbs).ok(); - port.map(|port| self.widget_tree.set_usage_type(&port, usage_type)); + fn set_expression_usage_type(&self, id: ast::Id, usage_type: Option) { + self.widget_tree.set_usage_type(id, usage_type); } fn hover_pointer_style(&self, hovered: &Switch) -> Option { @@ -362,7 +348,7 @@ ensogl::define_endpoints! { /// Set the expression USAGE type. This is not the definition type, which can be set with /// `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), + set_expression_usage_type (ast::Id,Option), } Output { @@ -536,7 +522,7 @@ impl Area { eval frp.update_widgets((a) model.apply_widget_updates(a)); eval frp.set_connected(((crumbs,status)) model.set_connected(crumbs,*status)); - eval frp.set_expression_usage_type(((cr,tp)) model.set_expression_usage_type(cr,tp.clone())); + eval frp.set_expression_usage_type(((id,tp)) model.set_expression_usage_type(*id,tp.clone())); eval frp.set_disabled ((disabled) model.widget_tree.set_disabled(*disabled)); widget_tree_invalidated <- any_(...); widget_tree_invalidated <+ frp.update_widgets; @@ -590,11 +576,6 @@ impl Area { .and_then(|t| t.tp().map(|t| Type(t.into()))) } - /// A crumb by AST ID. - pub fn get_crumbs_by_id(&self, id: ast::Id) -> Option { - self.model.id_crumbs_map.borrow().get(&id).cloned() - } - /// Set a scene layer for text rendering. pub fn set_label_layer(&self, layer: &display::scene::Layer) { self.model.set_label_layer(layer); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index b7dea733f424..1cb00efddc75 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -390,14 +390,8 @@ impl Tree { /// Set usage type for given span tree node. The usage type is used to determine the widget /// appearance and default inferred widget metadata. - pub fn set_usage_type( - &self, - tree_node: &span_tree::node::Ref, - usage_type: Option, - ) { - if let Some(pointer) = self.model.get_port_widget_pointer(tree_node) { - self.model.set_usage_type(pointer, usage_type); - } + pub fn set_usage_type(&self, ast_id: ast::Id, usage_type: Option) { + self.model.set_usage_type(ast_id, usage_type); } /// Set connection status for given span tree node. The connected nodes will be highlighted @@ -501,7 +495,7 @@ struct TreeModel { widgets_map: RefCell>, metadata_map: Rc>>, connected_map: Rc>>, - usage_type_map: Rc>>, + usage_type_map: Rc>>, disabled: AtomicBool, tree_dirty: AtomicBool, } @@ -549,10 +543,9 @@ impl TreeModel { } /// Set the usage type of an expression. It may cause the tree to be marked as dirty. - fn set_usage_type(&self, pointer: WidgetTreePointer, usage_type: Option) { + fn set_usage_type(&self, ast_id: ast::Id, usage_type: Option) { let mut map = self.usage_type_map.borrow_mut(); - warn!("Setting usage type of {:?} to {:?}", pointer, usage_type); - let dirty = map.synchronize_entry(pointer, usage_type); + let dirty = map.synchronize_entry(ast_id, usage_type); if dirty { self.tree_dirty.store(true, Ordering::Release); } @@ -675,22 +668,48 @@ impl TreeModel { } } +/// State of a node in the widget tree. Provides additional information about the node's current +/// state, such as its depth in the widget tree, if it's connected, disabled, etc. #[derive(Debug, Default, Clone, PartialEq)] pub(super) struct NodeState { + /// Widget tree node depth, as provided by the parent node. This does not necessarily + /// correspond to the depth in the view hierarchy or span tree, but instead is treated as a + /// logical nesting level in the expressions. It is fully determined by the chain of parent + /// widgets, and thus is more or less independent from the true depth of the widget tree data + /// structure. pub depth: usize, + /// Connection status of the node. Only present at the exact node that is connected, not at + /// any of its parents. pub connection: ConnectionStatus, + /// Connection status of the node's subtree. Contains the status of this node's connection, or + /// it's first parent that is connected. This is the same as `connection` for nodes that + /// are directly connected. pub subtree_connection: ConnectionStatus, + /// Whether the node is disabled, i.e. its expression is not currently used in the computation. + /// Widgets of disabled nodes are usually grayed out. pub disabled: bool, + /// Expression's usage type, as opposed to its definition type stored in the span tree. The + /// usage type represents the type of the expression as it is used in the computation, which + /// may differ from the definition type. For example, the definition type of a variable may be + /// `Number`, but its usage type may be `Vector`, if it is used as a vector component. pub usage_type: Option, } +/// A collection of common data used by all widgets and ports in the widget tree during +/// configuration. Provides the main widget's interface to the tree builder, allowing for creating +/// child widgets. #[derive(Debug)] pub struct ConfigContext<'a, 'b> { builder: &'a mut WidgetTreeBuilder<'b>, - /// Display mode of the widget built with this context. - #[allow(dead_code)] // currently unused, but will be used in the future - pub(super) display: Display, + /// Display mode of the widget. + /// TODO [PG]: Consider handling display modes in the widget tree builder directly, instead of + /// passing it to the widgets. Right now it has not effect at all. + #[allow(dead_code)] + pub(super) display: Display, + /// The span tree node corresponding to the widget being configured. pub(super) span_tree_node: span_tree::node::Ref<'a>, + /// Additional state associated with configured widget tree node, such as its depth, connection + /// status or parent node information. pub(super) state: NodeState, } @@ -744,7 +763,7 @@ struct WidgetTreeBuilder<'a> { styles: &'a StyleWatch, metadata_map: &'a HashMap, connected_map: &'a HashMap, - usage_type_map: &'a HashMap, + usage_type_map: &'a HashMap, old_widgets: HashMap, new_widgets: HashMap, parent_ast_id: Option, @@ -828,7 +847,7 @@ impl<'a> WidgetTreeBuilder<'a> { let old_node = self.old_widgets.remove(&tree_ptr); let is_placeholder = span_tree_node.is_expected_argument(); let sibling_offset = span_tree_node.sibling_offset.as_usize(); - let usage_type = self.usage_type_map.get(&tree_ptr).cloned(); + let usage_type = tree_ptr.id.and_then(|id| self.usage_type_map.get(&id)).cloned(); // Get widget metadata. There are three potential sources for metadata, that are used in // order, whichever is available first: diff --git a/app/gui/view/graph-editor/src/component/node/output/area.rs b/app/gui/view/graph-editor/src/component/node/output/area.rs index 910f39a7be17..19e603f86280 100644 --- a/app/gui/view/graph-editor/src/component/node/output/area.rs +++ b/app/gui/view/graph-editor/src/component/node/output/area.rs @@ -135,7 +135,7 @@ ensogl::define_endpoints! { /// Set the expression USAGE type. This is not the definition type, which can be set with /// `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), + set_expression_usage_type (ast::Id,Option), } Output { @@ -245,12 +245,13 @@ impl Model { /// Update expression type for the particular `ast::Id`. #[profile(Debug)] - fn set_expression_usage_type(&self, crumbs: &Crumbs, tp: &Option) { - if let Ok(port) = self.expression.borrow().span_tree.root_ref().get_descendant(crumbs) { - if let Some(frp) = &port.frp { - frp.set_usage_type(tp) - } - } + fn set_expression_usage_type(&self, id: ast::Id, tp: &Option) { + let crumbs_map = self.id_crumbs_map.borrow(); + let Some(crumbs) = crumbs_map.get(&id) else { return }; + let expression = self.expression.borrow(); + let Ok(port) = expression.span_tree.get_node(crumbs) else { return }; + let Some(frp) = &port.frp else { return }; + frp.set_usage_type(tp); } /// Traverse all span tree nodes that are considered ports. In case of empty span tree, include @@ -498,7 +499,7 @@ impl Area { // === Expression === eval frp.set_expression ((a) model.set_expression(a)); - eval frp.set_expression_usage_type (((a,b)) model.set_expression_usage_type(a,b)); + eval frp.set_expression_usage_type (((a,b)) model.set_expression_usage_type(*a,b)); // === Label Color === @@ -546,10 +547,6 @@ impl Area { .and_then(|t| t.frp.as_ref().and_then(|frp| frp.tp.value())) } - #[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs. - pub fn get_crumbs_by_id(&self, id: ast::Id) -> Option { - self.model.id_crumbs_map.borrow().get(&id).cloned() - } #[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs. pub fn whole_expr_id(&self) -> Option { diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 935e5c4aa061..b36a9178d19a 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -2257,17 +2257,10 @@ impl GraphEditorModel { let node_id = node_id.into(); if let Some(node) = self.nodes.get_cloned_ref(&node_id) { if node.view.model().output.whole_expr_id().contains(&ast_id) { - // TODO[ao]: we must update root output port according to the whole expression type - // due to a bug in engine https://github.com/enso-org/enso/issues/1038. - let crumbs = span_tree::Crumbs::default(); - node.view.model().output.set_expression_usage_type(crumbs, maybe_type.clone()); let enso_type = maybe_type.as_ref().map(|tp| enso::Type::new(&tp.0)); node.view.model().visualization.frp.set_vis_input_type(enso_type); } - let crumbs = node.view.model().get_crumbs_by_id(ast_id); - if let Some(crumbs) = crumbs { - node.view.set_expression_usage_type.emit((crumbs, maybe_type)); - } + node.view.set_expression_usage_type.emit((ast_id, maybe_type)); } } From a790859b2e7bd1fa82643065abe5c09cbd5caa6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 18 Apr 2023 18:58:02 +0200 Subject: [PATCH 15/45] fix edge color and origin width update --- .../src/component/node/input/area.rs | 8 +- .../src/component/node/input/port.rs | 6 +- .../src/component/node/input/widget.rs | 79 ++++--------- .../component/node/input/widget/hierarchy.rs | 15 ++- .../node/input/widget/insertion_point.rs | 3 +- .../src/component/node/input/widget/label.rs | 24 ++-- .../node/input/widget/single_choice.rs | 19 ++- app/gui/view/graph-editor/src/lib.rs | 110 +++++++++++------- 8 files changed, 124 insertions(+), 140 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 972de69bb02b..c9c341aa058e 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -237,11 +237,13 @@ impl Model { let crumbs = hovered.on()?; let expr = self.expression.borrow(); let port = expr.span_tree.get_node(crumbs).ok()?; - let display_object = self.widget_tree.get_port_display_object(&port)?; + let display_object = self.widget_tree.get_widget_display_object(&port)?; let tp = port.tp().map(|t| Type(t.into())); let color = tp.as_ref().map(|tp| type_coloring::compute(tp, &self.styles)); let pad_x = node::input::port::PORT_PADDING_X * 2.0; - let size = display_object.computed_size() + Vector2(pad_x, 0.0); + let min_y = node::input::port::BASE_PORT_HEIGHT; + let computed_size = display_object.computed_size(); + let size = Vector2(computed_size.x + pad_x, computed_size.y.max(min_y)); let radius = size.y / 2.0; Some(cursor::Style::new_highlight(display_object, size, radius, color)) } @@ -558,7 +560,7 @@ impl Area { pub fn port_offset(&self, crumbs: &[Crumb]) -> Option> { let expr = self.model.expression.borrow(); let node = expr.get_node(crumbs).ok()?; - let instance = self.model.widget_tree.get_port_display_object(&node)?; + let instance = self.model.widget_tree.get_widget_display_object(&node)?; let pos = instance.global_position(); let node_pos = self.model.display_object.global_position(); let size = instance.computed_size(); diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index 9d3c717ae191..e349a09a0a8a 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -100,8 +100,6 @@ impl PortHoverLayers { pub(super) struct Port { #[allow(dead_code)] on_cleanup: frp::DropSource, - #[allow(dead_code)] - network: frp::Network, crumbs: Rc>, port_root: display::object::Instance, widget_root: display::object::Instance, @@ -147,7 +145,8 @@ impl Port { port_root.add_child(&hover_shape); } - frp::new_network! { network + let network = &port_root.network; + frp::extend! { network on_cleanup <- on_drop(); hovering <- bool(&mouse_leave, &mouse_enter); cleanup_hovering <- on_cleanup.constant(false); @@ -180,7 +179,6 @@ impl Port { widget, widget_root, port_root, - network, crumbs, is_primary: false, last_node_depth: 0, diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 1cb00efddc75..3f6b9be1a43c 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -1,7 +1,5 @@ //! Definition of all hardcoded node widget variants and common widget FRP API. -mod debug; - use crate::prelude::*; use crate::component::node::input::area::NODE_HEIGHT; @@ -80,7 +78,7 @@ pub trait SpanWidget { /// Root display object of a widget. It is returned to the parent widget for positioning. fn root_object(&self) -> &display::object::Instance; /// Create a new widget with given configuration. - fn new(config: &Self::Config, app: &Application, frp: &WidgetsFrp) -> Self; + fn new(config: &Self::Config, ctx: &ConfigContext) -> Self; /// Update configuration for existing widget. fn configure(&mut self, config: &Self::Config, ctx: ConfigContext); } @@ -138,10 +136,10 @@ macro_rules! define_widget_modules( } } - fn new(config: &Config, app: &Application, frp: &WidgetsFrp) -> Self { + fn new(config: &Config, ctx: &ConfigContext) -> Self { match config { $( - Config::$name(config) => DynWidget::$name(SpanWidget::new(config, app, frp)), + Config::$name(config) => DynWidget::$name(SpanWidget::new(config, ctx)), )* } } @@ -152,7 +150,7 @@ macro_rules! define_widget_modules( SpanWidget::configure(model, config, ctx); },)* (this, _) => { - *this = SpanWidget::new(config, ctx.app(), ctx.frp()); + *this = SpanWidget::new(config, &ctx); this.configure(config, ctx) }, } @@ -340,7 +338,7 @@ pub struct Tree { impl display::Object for Tree { fn display_object(&self) -> &display::object::Instance { - &self.model.display_object.outer + &self.model.display_object } } @@ -357,11 +355,12 @@ impl Tree { frp::extend! { network set_ports_visible <- frp.set_ports_visible.sampler(); set_view_mode <- frp.set_view_mode.sampler(); + on_port_hover <- any(...); + frp.private.output.on_port_hover <+ on_port_hover; } let value_changed = frp.private.output.value_changed.clone_ref(); let request_import = frp.private.output.request_import.clone_ref(); - let on_port_hover = frp.private.output.on_port_hover.clone_ref(); let on_port_press = frp.private.output.on_port_press.clone_ref(); let pointer_style = frp.private.output.pointer_style.clone_ref(); let connected_port_updated = frp.private.output.connected_port_updated.clone_ref(); @@ -398,7 +397,7 @@ impl Tree { /// with a different color, and the widgets might change behavior depending on the connection /// status. pub fn set_connected(&self, tree_node: &span_tree::node::Ref, status: ConnectionStatus) { - if let Some(pointer) = self.model.get_port_widget_pointer(tree_node) { + if let Some(pointer) = self.model.get_node_widget_pointer(tree_node) { self.model.set_connected(pointer, status); } } @@ -436,14 +435,13 @@ impl Tree { self.model.rebuild_tree(self.widgets_frp.clone_ref(), tree, node_expression, styles) } - /// Get the root display object of the widget port for given span tree node. Not all widgets - /// have a port, so the returned value might be `None` even if a widget exist for given span - /// tree node. - pub fn get_port_display_object( + /// Get the root display object of the widget port for given span tree node. Not all nodes must + /// have a distinct widget, so the returned value might be `None`. + pub fn get_widget_display_object( &self, tree_node: &span_tree::node::Ref, ) -> Option { - let pointer = self.model.get_port_widget_pointer(tree_node)?; + let pointer = self.model.get_node_widget_pointer(tree_node)?; self.model.with_node(&pointer, |w| w.display_object().clone()) } } @@ -464,15 +462,6 @@ pub(super) enum TreeNode { Widget(DynWidget), } -impl TreeNode { - fn port_mut(&mut self) -> Option<&mut Port> { - match self { - TreeNode::Port(port) => Some(port), - TreeNode::Widget(_) => None, - } - } -} - impl display::Object for TreeNode { fn display_object(&self) -> &display::object::Instance { match self { @@ -491,7 +480,7 @@ impl display::Object for TreeNode { #[derive(Debug)] struct TreeModel { app: Application, - display_object: debug::InstanceWithBg, + display_object: display::object::Instance, widgets_map: RefCell>, metadata_map: Rc>>, connected_map: Rc>>, @@ -505,12 +494,12 @@ impl TreeModel { /// argument info. fn new(app: &Application) -> Self { let app = app.clone_ref(); - let display_object = debug::InstanceWithBg::gray(); - display_object.inner.use_auto_layout(); - display_object.inner.set_children_alignment_left_center().justify_content_center_y(); - display_object.inner.set_size_y(NODE_HEIGHT); - display_object.inner.set_padding_left(TEXT_OFFSET); - display_object.inner.set_padding_right(TEXT_OFFSET); + let display_object = display::object::Instance::new(); + display_object.use_auto_layout(); + display_object.set_children_alignment_left_center().justify_content_center_y(); + display_object.set_size_y(NODE_HEIGHT); + display_object.set_padding_left(TEXT_OFFSET); + display_object.set_padding_right(TEXT_OFFSET); Self { app, @@ -591,7 +580,7 @@ impl TreeModel { }; let child = builder.child_widget(tree.root_ref(), 0); - self.display_object.inner.replace_children(&[child]); + self.display_object.replace_children(&[child]); self.widgets_map.replace(builder.new_widgets); } @@ -599,7 +588,7 @@ impl TreeModel { /// has a unique representation in the form of a widget tree pointer, which is more stable /// across changes in the span tree than [`span_tree::Crumbs`]. The pointer is used to identify /// the widgets or ports in the widget tree. - pub fn get_port_widget_pointer( + pub fn get_node_widget_pointer( &self, tree_node: &span_tree::node::Ref, ) -> Option { @@ -644,28 +633,6 @@ impl TreeModel { ) -> Option { self.widgets_map.borrow().get(pointer).map(f) } - - /// Perform an operation on a mutable reference to a tree node under given pointer. When there - /// is no node under provided pointer, the operation will not be performed and `None` will be - /// returned. - pub fn with_node_mut( - &self, - pointer: &WidgetTreePointer, - f: impl FnOnce(&mut TreeNode) -> T, - ) -> Option { - self.widgets_map.borrow_mut().get_mut(pointer).map(f) - } - - /// Perform an operation on a mutable reference to a widget port under given pointer. When there - /// is no node under provided pointer, or when the found node has no port, the operation will - /// not be performed and `None` will be returned. - pub fn with_port_mut( - &self, - pointer: &WidgetTreePointer, - f: impl FnOnce(&mut Port) -> T, - ) -> Option { - self.widgets_map.borrow_mut().get_mut(pointer).and_then(TreeNode::port_mut).map(f) - } } /// State of a node in the widget tree. Provides additional information about the node's current @@ -901,7 +868,7 @@ impl<'a> WidgetTreeBuilder<'a> { let mut port = match old_node { Some(TreeNode::Port(port)) => port, Some(TreeNode::Widget(widget)) => Port::new(widget, app, frp), - None => Port::new(DynWidget::new(&meta.config, app, frp), app, frp), + None => Port::new(DynWidget::new(&meta.config, &ctx), app, frp), }; port.configure(&meta.config, ctx); TreeNode::Port(port) @@ -909,7 +876,7 @@ impl<'a> WidgetTreeBuilder<'a> { let mut widget = match old_node { Some(TreeNode::Port(port)) => port.into_widget(), Some(TreeNode::Widget(widget)) => widget, - None => DynWidget::new(&meta.config, app, frp), + None => DynWidget::new(&meta.config, &ctx), }; widget.configure(&meta.config, ctx); TreeNode::Widget(widget) diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs index c7dae41cd97e..536568569247 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -3,7 +3,6 @@ use crate::prelude::*; -use ensogl::application::Application; use ensogl::display::object; // ================= @@ -18,20 +17,20 @@ pub struct Config; /// Hierarchy widget. This widget expands each child of its span tree into a new widget. #[derive(Clone, Debug)] pub struct Widget { - display_object: super::debug::InstanceWithBg, + display_object: object::Instance, } impl super::SpanWidget for Widget { type Config = Config; fn root_object(&self) -> &object::Instance { - &self.display_object.outer + &self.display_object } - fn new(_: &Config, _: &Application, _: &super::WidgetsFrp) -> Self { - let display_object = super::debug::InstanceWithBg::olive(); - display_object.inner.use_auto_layout(); - display_object.inner.set_children_alignment_left_center().justify_content_center_y(); + fn new(_: &Config, _: &super::ConfigContext) -> Self { + let display_object = object::Instance::new(); + display_object.use_auto_layout(); + display_object.set_children_alignment_left_center().justify_content_center_y(); Self { display_object } } @@ -44,6 +43,6 @@ impl super::SpanWidget for Widget { .children_iter() .map(|node| ctx.builder.child_widget(node, next_depth)) .collect_vec(); - self.display_object.inner.replace_children(&children); + self.display_object.replace_children(&children); } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs b/app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs index 69c51ab98bcc..e15af4176de5 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs @@ -2,7 +2,6 @@ use crate::prelude::*; -use ensogl::application::Application; use ensogl::display::object; @@ -29,7 +28,7 @@ impl super::SpanWidget for Widget { &self.root } - fn new(_: &Config, _: &Application, _: &super::WidgetsFrp) -> Self { + fn new(_: &Config, _: &super::ConfigContext) -> Self { let root = object::Instance::new(); Self { root } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 62b5df610846..c44080b83f9e 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -2,9 +2,7 @@ use crate::prelude::*; -use super::debug::InstanceWithBg; use crate::component::node::input::area::TEXT_SIZE; -use ensogl::application::Application; use ensogl::data::color; use ensogl::display::object; use ensogl_component::text; @@ -30,7 +28,7 @@ ensogl::define_endpoints_2! { #[derive(Clone, Debug)] pub struct Widget { frp: Frp, - root: InstanceWithBg, + root: object::Instance, #[allow(dead_code)] label: text::Text, } @@ -39,31 +37,32 @@ impl super::SpanWidget for Widget { type Config = Config; fn root_object(&self) -> &object::Instance { - &self.root.outer + &self.root } - fn new(_: &Config, app: &Application, widgets_frp: &super::WidgetsFrp) -> Self { + fn new(_: &Config, ctx: &super::ConfigContext) -> Self { // Embed the label in a vertically centered fixed height container, so that the label's // baseline is properly aligned to center and lines up with other labels in the line. - - let layers = &app.display.default_scene.layers; - let root = InstanceWithBg::magenta(); - root.inner.set_size_y(TEXT_SIZE); + let app = ctx.app(); + let widgets_frp = ctx.frp(); + let layers = &ctx.app().display.default_scene.layers; + let root = object::Instance::new(); + root.set_size_y(TEXT_SIZE); let label = text::Text::new(app); label.set_property_default(text::Size(TEXT_SIZE)); label.set_y(TEXT_SIZE); layers.above_nodes_text.add(&label); - root.inner.add_child(&label); + root.add_child(&label); let frp = Frp::new(); let network = &frp.network; - let inner = root.inner.clone_ref(); frp::extend! { network color_change <- frp.text_color.on_change(); parent_port_hovered <- widgets_frp.on_port_hover.map2(&frp.crumbs, |h, crumbs| { h.on().map_or(false, |h| crumbs.starts_with(h)) }); + trace parent_port_hovered; label_color <- color_change.map2(&parent_port_hovered, |color, hovered| { if *hovered { color::Lcha::white() } else { *color } }); @@ -74,9 +73,8 @@ impl super::SpanWidget for Widget { label.set_content(content); }); - width <- label.width.on_change(); - eval width((w) { inner.set_size_x(*w); }); + eval width((w) root.set_size_x(*w); ); } Self { frp, root, label } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 877043f7fb7b..292c125eb10c 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -2,11 +2,9 @@ use crate::prelude::*; -use super::debug::InstanceWithBg; use crate::component::node::input::area::TEXT_SIZE; use crate::component::node::input::widget::Entry; use enso_frp as frp; -use ensogl::application::Application; use ensogl::control::io::mouse; use ensogl::data::color; use ensogl::display; @@ -91,7 +89,7 @@ ensogl::define_endpoints_2! { #[allow(dead_code)] pub struct Widget { config_frp: Frp, - display_object: InstanceWithBg, + display_object: display::object::Instance, content_wrapper: display::object::Instance, dropdown_wrapper: display::object::Instance, label_wrapper: display::object::Instance, @@ -105,10 +103,12 @@ impl super::SpanWidget for Widget { type Config = Config; fn root_object(&self) -> &display::object::Instance { - &self.display_object.outer + &self.display_object } - fn new(_: &Config, app: &Application, widgets_frp: &super::WidgetsFrp) -> Self { + fn new(_: &Config, ctx: &super::ConfigContext) -> Self { + let app = ctx.app(); + let widgets_frp = ctx.frp(); // ╭─display_object─────────────────────────────────────╮ // │╭─content_wrapper──────────────────────────────────╮│ // ││╭ shape ╮ ╭ label_wrapper ────╮ ╭ args_wrapper ─╮ ││ @@ -137,18 +137,17 @@ impl super::SpanWidget for Widget { dropdown.set_max_open_size(DROPDOWN_MAX_SIZE); dropdown.allow_deselect_all(true); - let display_object = InstanceWithBg::magenta(); - let content_wrapper = display_object.inner.new_child(); + let display_object = display::object::Instance::new(); + let content_wrapper = display_object.new_child(); content_wrapper.add_child(&activation_shape); let label_wrapper = content_wrapper.new_child(); label_wrapper.add_child(&label); let args_wrapper = content_wrapper.new_child(); - let dropdown_wrapper = display_object.inner.new_child(); + let dropdown_wrapper = display_object.new_child(); dropdown_wrapper.add_child(&dropdown); display_object - .inner .use_auto_layout() .set_column_flow() .set_children_alignment_left_center() @@ -173,7 +172,7 @@ impl super::SpanWidget for Widget { let network = &config_frp.network; let input = &config_frp.private.input; - let focus_receiver = display_object.inner.clone_ref(); + let focus_receiver = display_object.clone_ref(); frp::extend! { network init <- source::<()>(); diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index b36a9178d19a..9e297c30d5c2 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -738,6 +738,7 @@ ensogl::define_endpoints_2! { node_edit_mode (bool), nodes_labels_visible (bool), node_incoming_edge_updates (NodeId), + node_outgoing_edge_updates (NodeId), /// `None` value as a visualization path denotes a disabled visualization. @@ -1612,7 +1613,12 @@ impl GraphEditorModelWithNetwork { (_) model.frp.private.output.node_incoming_edge_updates.emit(node_id) ); + eval node_model.input.frp.width( + (_) model.frp.private.output.node_outgoing_edge_updates.emit(node_id) + ); + let neutral_color = model.styles_frp.get_color(theme::code::types::any::selection); + _eval <- node_model.output.frp.on_port_type_change.map2(&neutral_color, f!(((crumbs,_),neutral_color) model.with_output_edge_id(node_id,crumbs,|id| @@ -2027,9 +2033,6 @@ impl GraphEditorModel { if let Some(node) = self.nodes.get_cloned_ref(&node_id) { node.set_expression.emit(expr); } - for edge_id in self.node_out_edges(node_id) { - self.refresh_edge_source_size(edge_id); - } } fn edit_node_expression( @@ -2101,9 +2104,7 @@ impl GraphEditorModel { node.out_edges.insert(edge_id); edge.set_source(target); edge.view.frp.source_attached.emit(true); - // FIXME: both lines require edge to refresh. Let's make it more efficient. self.refresh_edge_position(edge_id); - self.refresh_edge_source_size(edge_id); } } } @@ -2116,9 +2117,7 @@ impl GraphEditorModel { edge.view.frp.source_attached.emit(false); let first_detached = self.edges.detached_source.is_empty(); self.edges.detached_source.insert(edge_id); - // FIXME: both lines require edge to refresh. Let's make it more efficient. self.refresh_edge_position(edge_id); - self.refresh_edge_source_size(edge_id); if first_detached { self.frp.private.output.on_some_edges_sources_unset.emit(()); } @@ -2299,24 +2298,14 @@ impl GraphEditorModel { (node_id, new_position) } - #[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs. - pub fn refresh_edge_source_size(&self, edge_id: EdgeId) { - if let Some(edge) = self.edges.get_cloned_ref(&edge_id) { - if let Some(edge_source) = edge.source() { - if let Some(node) = self.nodes.get_cloned_ref(&edge_source.node_id) { - edge.view.frp.source_width.emit(node.model().width()); - edge.view.frp.source_height.emit(node.model().height()); - edge.view.frp.redraw.emit(()); - } - } - }; - } - #[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs. pub fn refresh_edge_color(&self, edge_id: EdgeId, neutral_color: color::Lcha) { if let Some(edge) = self.edges.get_cloned_ref(&edge_id) { let color = self.edge_color(edge_id, neutral_color); edge.view.frp.set_color.emit(color); + if let Some(target) = edge.target() { + self.set_input_connected(&target, node::ConnectionStatus::connected(color)); + } }; } @@ -2334,13 +2323,25 @@ impl GraphEditorModel { if let Some(edge) = self.edges.get_cloned_ref(&edge_id) { if let Some(edge_source) = edge.source() { if let Some(node) = self.nodes.get_cloned_ref(&edge_source.node_id) { - let new_position = - node.position().xy() + Vector2::new(node.model().width() / 2.0, 0.0); + let node_width = node.model().width(); + let node_height = node.model().height(); + let new_position = node.position().xy() + Vector2::new(node_width / 2.0, 0.0); + let prev_width = edge.source_width.get(); + let prev_height = edge.source_height.get(); let prev_position = edge.position().xy(); + if prev_position != new_position { redraw = true; edge.set_xy(new_position); } + if prev_width != node_width { + redraw = true; + edge.view.frp.source_width.emit(node_width); + } + if prev_height != node_height { + redraw = true; + edge.view.frp.source_height.emit(node_height); + } } } if let Some(edge_target) = edge.target() { @@ -2363,8 +2364,34 @@ impl GraphEditorModel { redraw } - /// Refresh the positions of all edges connected to the given node. This is useful when we know - /// that the node ports has been updated, but we don't track which exact edges are affected. + /// Refresh the positions of all outgoing edges connected to the given node. Returns `true` if + /// at least one edge has been changed. + pub fn refresh_outgoing_edge_positions(&self, node_ids: &[NodeId]) -> bool { + let mut updated = false; + for node_id in node_ids { + for edge_id in self.node_out_edges(node_id) { + updated |= self.refresh_edge_position(edge_id); + } + } + updated + } + + /// Refresh the positions of all incoming edges connected to the given node. This is useful when + /// we know that the node ports has been updated, but we don't track which exact edges are + /// affected. Returns `true` if at least one edge has been changed. + pub fn refresh_incoming_edge_positions(&self, node_ids: &[NodeId]) -> bool { + let mut updated = false; + for node_id in node_ids { + for edge_id in self.node_in_edges(node_id) { + updated |= self.refresh_edge_position(edge_id); + } + } + updated + } + + /// Force layout update of the graph UI elements. Because display objects track changes made to + /// them, only objects modified since last update will have layout recomputed. Using this + /// function is still discouraged, because changes /// /// Because edge positions are computed based on the node positions, it is usually done after /// the layout has been updated. In order to avoid edge flickering, we have to update their @@ -2372,16 +2399,8 @@ impl GraphEditorModel { /// /// FIXME: Find a better solution to fix this issue. We either need a layout that can depend on /// other arbitrary position, or we need the layout update to be multi-stage. - pub fn refresh_incoming_edge_positions_and_relayout(&self, node_ids: &[NodeId]) { - let mut updated = false; - for node_id in node_ids { - for edge_id in self.node_in_edges(node_id) { - updated |= self.refresh_edge_position(edge_id); - } - } - if updated { - self.display_object().update(self.scene()); - } + pub fn force_update_layout(&self) { + self.display_object().update(self.scene()); } fn map_node(&self, id: NodeId, f: impl FnOnce(Node) -> T) -> Option { @@ -2776,6 +2795,7 @@ fn new_graph_editor(app: &Application) -> GraphEditor { let vis_registry = &model.vis_registry; let out = &frp.private.output; let selection_controller = &model.selection_controller; + let neutral_color = model.model.styles_frp.get_color(theme::code::types::any::selection); @@ -3072,10 +3092,13 @@ fn new_graph_editor(app: &Application) -> GraphEditor { edge_to_remove <- any(edge_to_remove_without_targets,edge_to_remove_without_sources); eval edge_to_remove ((id) model.remove_edge(id)); - incoming_edge_updates_batch <- out.node_incoming_edge_updates.batch(); - eval incoming_edge_updates_batch ( - (nodes) model.refresh_incoming_edge_positions_and_relayout(nodes) - ); + incoming_batch <- out.node_incoming_edge_updates.batch(); + outgoing_batch <- out.node_outgoing_edge_updates.batch(); + incoming_dirty <- incoming_batch.map(f!((n) model.refresh_incoming_edge_positions(n))); + outgoing_dirty <- outgoing_batch.map(f!((n) model.refresh_outgoing_edge_positions(n))); + any_edges_dirty <- incoming_dirty || outgoing_dirty; + force_update_layout <- any_edges_dirty.on_true().debounce(); + eval force_update_layout((_) model.force_update_layout()); } // === Adding Node === @@ -3400,10 +3423,11 @@ fn new_graph_editor(app: &Application) -> GraphEditor { model.set_node_expression_usage_type(*node_id,*ast_id,maybe_type.clone()); *node_id })); - edges_to_refresh <= node_to_refresh.map(f!([nodes](node_id) - nodes.get_cloned_ref(node_id).map(|node| node.all_edges()) - )).unwrap(); - eval edges_to_refresh ((edge) model.refresh_edge_position(*edge)); + edges_to_refresh <= node_to_refresh.map( + f!((node_id) nodes.get_cloned_ref(node_id).map(|node| node.all_edges())) + ).unwrap(); + eval edges_to_refresh ([model, neutral_color] (edge) + model.refresh_edge_color(*edge, neutral_color.value().into())); eval inputs.update_node_widgets(((node, updates)) model.update_node_widgets(*node, updates)); } @@ -3662,8 +3686,6 @@ fn new_graph_editor(app: &Application) -> GraphEditor { // === Source / Target === - let neutral_color = model.model.styles_frp.get_color(theme::code::types::any::selection); - eval out.on_edge_source_set (((id,tgt)) model.set_edge_source(*id,tgt)); eval out.on_edge_target_set (((id,tgt)) model.set_edge_target(*id,tgt)); From 107a357d8d58b95530b664828ed4183d22b97111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 18 Apr 2023 19:07:02 +0200 Subject: [PATCH 16/45] fix label widget hover color --- app/gui/language/span-tree/src/lib.rs | 4 ---- .../graph-editor/src/component/node/input/area.rs | 2 +- .../src/component/node/input/widget/label.rs | 11 +++++------ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/app/gui/language/span-tree/src/lib.rs b/app/gui/language/span-tree/src/lib.rs index 25ea649547be..11704c80bdf6 100644 --- a/app/gui/language/span-tree/src/lib.rs +++ b/app/gui/language/span-tree/src/lib.rs @@ -299,10 +299,6 @@ impl SpanTree { write!(buffer, " name={name:?}").unwrap(); } - if let Some(tp) = node.kind.tp() { - write!(buffer, " tp={tp:?}").unwrap(); - } - if let Some(call_id) = node.kind.call_id() { write!(buffer, " call_id={call_id:?}").unwrap(); } diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index c9c341aa058e..4cbd757957d8 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -280,7 +280,7 @@ impl Model { #[profile(Debug)] fn set_expression(&self, new_expression: impl Into, area_frp: &FrpEndpoints) { let new_expression = InputExpression::from(new_expression.into()); - warn!("set expression: \n{:?}", new_expression.tree_pretty_printer()); + debug!("set expression: \n{:?}", new_expression.tree_pretty_printer()); self.widget_tree.rebuild_tree( &new_expression.span_tree, diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index c44080b83f9e..2c55ee28ef98 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -56,22 +56,21 @@ impl super::SpanWidget for Widget { let frp = Frp::new(); let network = &frp.network; + let hover_color: color::Lcha = ctx.styles().get_color(theme::code::types::selected).into(); frp::extend! { network color_change <- frp.text_color.on_change(); parent_port_hovered <- widgets_frp.on_port_hover.map2(&frp.crumbs, |h, crumbs| { h.on().map_or(false, |h| crumbs.starts_with(h)) }); - trace parent_port_hovered; - label_color <- color_change.map2(&parent_port_hovered, |color, hovered| { - if *hovered { color::Lcha::white() } else { *color } + label_color <- color_change.all_with(&parent_port_hovered, move |color, hovered| { + if *hovered { hover_color } else { *color } }); + label_color <- label_color.on_change(); eval label_color((color) label.set_property_default(color)); content_change <- frp.content.on_change(); - eval content_change([label] (content) { - label.set_content(content); - }); + eval content_change((content) label.set_content(content)); width <- label.width.on_change(); eval width((w) root.set_size_x(*w); ); From cd91a6758d538b98d9c9df3625840751863d0860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Wed, 19 Apr 2023 09:43:46 +0200 Subject: [PATCH 17/45] lint --- app/gui/language/span-tree/src/generate.rs | 3 +- .../src/controller/graph/widget/metadata.rs | 6 ++ .../view/graph-editor/src/component/node.rs | 6 +- .../src/component/node/input/area.rs | 9 ++- .../src/component/node/input/port.rs | 76 +++++++++++++------ .../src/component/node/input/widget.rs | 69 ++++++++++------- .../src/component/node/input/widget/label.rs | 4 +- .../node/input/widget/single_choice.rs | 2 +- .../node/input/widget/vector_editor.rs | 1 + app/gui/view/graph-editor/src/lib.rs | 6 +- lib/rust/ensogl/core/src/display/scene.rs | 2 +- lib/rust/frp/src/nodes.rs | 24 +++--- 12 files changed, 134 insertions(+), 74 deletions(-) diff --git a/app/gui/language/span-tree/src/generate.rs b/app/gui/language/span-tree/src/generate.rs index 27acced247c1..4c403726ca4f 100644 --- a/app/gui/language/span-tree/src/generate.rs +++ b/app/gui/language/span-tree/src/generate.rs @@ -537,7 +537,8 @@ fn generate_node_for_prefix_chain( let app_base = ApplicationBase::from_prefix_chain(this); // If actual method arguments are not resolved, we still want to assign correct call ID to all - // argument spans. This is required for correct handling of + // argument spans. This is required for correct handling of span tree actions, as it is used to + // determine correct reinsertion point for removed span. let fallback_call_id = app_base.call_id; let mut application = app_base.resolve(context); diff --git a/app/gui/src/controller/graph/widget/metadata.rs b/app/gui/src/controller/graph/widget/metadata.rs index 34b00a195466..6f50745e8df8 100644 --- a/app/gui/src/controller/graph/widget/metadata.rs +++ b/app/gui/src/controller/graph/widget/metadata.rs @@ -50,6 +50,12 @@ fn map_config(inner: response::WidgetSpecific) -> widget::Config { entries: Rc::new(map_entries(&values)), } .into(), + response::WidgetSpecific::VectorEditor { item_editor, item_default } => + widget::vector_editor::Config { + item_editor: Some(Rc::new(map_metadata(*item_editor))), + item_default: item_default.into(), + } + .into(), _ => widget::label::Config::default().into(), } } diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index 78540d235a9c..de23a687528c 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -1191,10 +1191,8 @@ pub mod test_utils { } fn input_port_shape(&self) -> Option { - // let ports = self.input.model.ports(); - // let port = ports.first()?; - // port.shape.as_ref().map(CloneRef::clone_ref) - None // TODO + let shapes = self.input.model.port_hover_shapes(); + shapes.into_iter().next() } } } diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 4cbd757957d8..fe5830df2a05 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -224,8 +224,8 @@ impl Model { /// Set connection status of the given port. fn set_connected(&self, crumbs: &Crumbs, status: node::ConnectionStatus) { let expr = self.expression.borrow(); - let port = expr.span_tree.get_node(crumbs).ok(); - port.map(|port| self.widget_tree.set_connected(&port, status)); + let Ok(port) = expr.span_tree.get_node(crumbs) else { return }; + self.widget_tree.set_connected(&port, status); } /// Set usage type of the given port. @@ -295,6 +295,11 @@ impl Model { self.request_widgets_metadata(&new_expression, area_frp); *self.expression.borrow_mut() = new_expression; } + + /// Get hover shapes for all input ports of a node. Used for testing to simulate mouse events. + pub fn port_hover_shapes(&self) -> Vec { + self.widget_tree.port_hover_shapes() + } } diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index e349a09a0a8a..5437e6cef29c 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -38,11 +38,23 @@ pub const PRIMARY_PORT_MAX_DEPTH: usize = 0; -// ============ -// === Port === -// ============ - -/// Visible shape of connected ports. +// ============= +// === Shape === +// ============= + +/// Shapes the port is build from. It consist of the `hover_shape`, which represents a hover area of +/// a height dependent on logical widget depth, and the `shape`, which is a nice, visual highlight +/// representation with padding extending out of the widget bounding box. Both shapes are children +/// of `Port`'s `port_root` display object: +/// +/// ```text +/// hover_shape (appears when hovering over the node while dragging an edge) +/// ◄──────► +/// ╭───┬────────┬──┄ +/// │ │╭──────╮│▼ shape +/// │ │╰──────╯│▲ +/// ╰───┴────────┴──┄ +/// ``` pub mod shape { use super::*; ensogl::shape! { @@ -64,26 +76,28 @@ pub mod hover_shape { (style:Style) { let size = Var::canvas_size(); let transparent = Var::::from("srgba(1.0,1.0,1.0,0.00001)"); - let hover_shape = Rect(&size).fill(transparent); + let hover_shape = Rect(size).fill(transparent); hover_shape.into() } } } +/// An scene extension that holds the partitions of hover shapes for all ports. This is used to +/// visually sort the ports based on port depth in the widget tree. #[derive(Clone, CloneRef)] -struct PortHoverLayers { +struct HoverLayers { hover_layer: display::scene::Layer, hover_partitions: Rc>>>, } -impl display::scene::Extension for PortHoverLayers { +impl display::scene::Extension for HoverLayers { fn init(scene: &display::Scene) -> Self { let hover_layer = scene.layers.main.clone_ref(); Self { hover_layer, hover_partitions: default() } } } -impl PortHoverLayers { +impl HoverLayers { fn add_to_partition(&self, object: &display::object::Instance, depth: usize) { let mut hover_partitions = self.hover_partitions.borrow_mut(); if hover_partitions.len() <= depth { @@ -96,8 +110,13 @@ impl PortHoverLayers { } +// ============ +// === Port === +// ============ + +/// A port on the node side. It can be connected to other ports. #[derive(Debug)] -pub(super) struct Port { +pub struct Port { #[allow(dead_code)] on_cleanup: frp::DropSource, crumbs: Rc>, @@ -106,11 +125,13 @@ pub(super) struct Port { widget: DynWidget, port_shape: shape::View, hover_shape: hover_shape::View, - last_node_depth: usize, - is_primary: bool, + current_depth: usize, + current_primary: bool, } impl Port { + /// Create a new port for given widget. The widget will be placed as a child of the port's + /// `port_root` display object, and its layout size will be used to determine the port's size. pub fn new(widget: DynWidget, app: &Application, frp: &WidgetsFrp) -> Self { let port_root = display::object::Instance::new(); let widget_root = widget.root_object().clone_ref(); @@ -132,7 +153,7 @@ impl Port { .set_margin_right(-PORT_PADDING_X) .set_alignment_left_center(); - let layers = app.display.default_scene.extension::(); + let layers = app.display.default_scene.extension::(); layers.add_to_partition(hover_shape.display_object(), 0); let mouse_enter = hover_shape.on_event::(); @@ -180,11 +201,12 @@ impl Port { widget_root, port_root, crumbs, - is_primary: false, - last_node_depth: 0, + current_primary: false, + current_depth: 0, } } + /// Configure the port and its attached widget. pub fn configure(&mut self, config: &Config, ctx: ConfigContext) { self.crumbs.replace(ctx.span_tree_node.crumbs.clone()); self.set_connected(ctx.state.connection); @@ -193,7 +215,9 @@ impl Port { self.update_root(); } - pub fn set_connected(&self, status: ConnectionStatus) { + /// Update connection status of this port. Changing the connection status will add or remove the + /// port's visible shape from the display hierarchy. + fn set_connected(&self, status: ConnectionStatus) { match status { ConnectionStatus::Connected(data) => { self.port_root.add_child(&self.port_shape); @@ -216,24 +240,32 @@ impl Port { fn set_port_layout(&mut self, ctx: &ConfigContext) { let node_depth = ctx.span_tree_node.crumbs.len(); - if self.last_node_depth != node_depth { - self.last_node_depth = node_depth; - let layers = ctx.app().display.default_scene.extension::(); + if self.current_depth != node_depth { + self.current_depth = node_depth; + let layers = ctx.app().display.default_scene.extension::(); layers.add_to_partition(self.hover_shape.display_object(), node_depth); } + #[allow(clippy::absurd_extreme_comparisons)] let is_primary = ctx.state.depth <= PRIMARY_PORT_MAX_DEPTH; - if self.is_primary != is_primary { - self.is_primary = is_primary; + if self.current_primary != is_primary { + self.current_primary = is_primary; let margin = if is_primary { -PRIMARY_PORT_HOVER_PADDING_Y } else { 0.0 }; self.hover_shape.set_margin_top(margin); self.hover_shape.set_margin_bottom(margin); } } - pub fn into_widget(self) -> DynWidget { + /// Extract the widget out of the port, dropping the port specific display objects. The widget + /// can be reinserted into the display hierarchy of widget tree. + pub(super) fn into_widget(self) -> DynWidget { self.widget } + + /// Get the port's hover shape. Used for testing to simulate mouse events. + pub fn hover_shape(&self) -> &hover_shape::View { + &self.hover_shape + } } impl display::Object for Port { diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 3f6b9be1a43c..7f18ef92d9bc 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -15,8 +15,6 @@ use ensogl::display; use ensogl::display::shape::StyleWatch; use ensogl::gui::cursor; use ensogl_component::drop_down::DropdownValue; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; use text::index::Byte; @@ -107,7 +105,7 @@ macro_rules! define_widget_modules( /// widget of new kind will be created and the old one will be dropped. #[derive(Debug)] #[allow(missing_docs)] - pub(super) enum DynWidget { + pub enum DynWidget { $( $(#[$meta])* $name($module::Widget) @@ -201,12 +199,12 @@ impl Metadata { /// Widget metadata for static dropdown, based on the tag values provided by suggestion /// database. fn static_dropdown(label: Option, tag_values: &[span_tree::TagValue]) -> Metadata { - let entries = Rc::new(tag_values.into_iter().map(Entry::from).collect()); + let entries = Rc::new(tag_values.iter().map(Entry::from).collect()); Self::always(single_choice::Config { label, entries }) } fn vector_editor() -> Metadata { - Self::always(vector_editor::Config::default()) + Self::always(vector_editor::Config { item_editor: None, item_default: "_".into() }) } fn from_kind( @@ -417,7 +415,7 @@ impl Tree { node_expression: &str, styles: &StyleWatch, ) { - if self.model.tree_dirty.load(Ordering::Acquire) { + if self.model.tree_dirty.get() { self.rebuild_tree(tree, node_expression, styles); } } @@ -444,6 +442,16 @@ impl Tree { let pointer = self.model.get_node_widget_pointer(tree_node)?; self.model.with_node(&pointer, |w| w.display_object().clone()) } + + /// Get hover shapes for all ports in the tree. Used for testing to simulate mouse events. + pub fn port_hover_shapes(&self) -> Vec { + self.model + .nodes_map + .borrow() + .values() + .filter_map(|n| Some(n.port()?.hover_shape().clone_ref())) + .collect_vec() + } } @@ -462,6 +470,15 @@ pub(super) enum TreeNode { Widget(DynWidget), } +impl TreeNode { + fn port(&self) -> Option<&Port> { + match self { + TreeNode::Port(port) => Some(port), + TreeNode::Widget(_) => None, + } + } +} + impl display::Object for TreeNode { fn display_object(&self) -> &display::object::Instance { match self { @@ -481,12 +498,12 @@ impl display::Object for TreeNode { struct TreeModel { app: Application, display_object: display::object::Instance, - widgets_map: RefCell>, + nodes_map: RefCell>, metadata_map: Rc>>, connected_map: Rc>>, usage_type_map: Rc>>, - disabled: AtomicBool, - tree_dirty: AtomicBool, + disabled: Cell, + tree_dirty: Cell, } impl TreeModel { @@ -505,7 +522,7 @@ impl TreeModel { app, display_object, disabled: default(), - widgets_map: default(), + nodes_map: default(), metadata_map: default(), connected_map: default(), usage_type_map: default(), @@ -518,7 +535,7 @@ impl TreeModel { let mut map = self.metadata_map.borrow_mut(); let dirty = map.synchronize_entry(pointer, meta); if dirty { - self.tree_dirty.store(true, Ordering::Release); + self.tree_dirty.set(true); } } @@ -527,7 +544,7 @@ impl TreeModel { let mut map = self.connected_map.borrow_mut(); let dirty = map.synchronize_entry(pointer, status.data()); if dirty { - self.tree_dirty.store(true, Ordering::Release); + self.tree_dirty.set(true); } } @@ -536,15 +553,15 @@ impl TreeModel { let mut map = self.usage_type_map.borrow_mut(); let dirty = map.synchronize_entry(ast_id, usage_type); if dirty { - self.tree_dirty.store(true, Ordering::Release); + self.tree_dirty.set(true); } } /// Set the connection status under given widget. It may cause the tree to be marked as dirty. fn set_disabled(&self, disabled: bool) { - let prev_disabled = self.disabled.swap(disabled, Ordering::AcqRel); + let prev_disabled = self.disabled.replace(disabled); if prev_disabled != disabled { - self.tree_dirty.store(true, Ordering::Release); + self.tree_dirty.set(true); } } @@ -556,13 +573,13 @@ impl TreeModel { node_expression: &str, styles: &StyleWatch, ) { - self.tree_dirty.store(false, Ordering::Release); + self.tree_dirty.set(false); let app = self.app.clone(); let metadata_map = self.metadata_map.borrow(); let connected_map = self.connected_map.borrow(); let usage_type_map = self.usage_type_map.borrow(); - let old_widgets = self.widgets_map.take(); - let node_disabled = self.disabled.load(Ordering::Acquire); + let old_nodes = self.nodes_map.take(); + let node_disabled = self.disabled.get(); let mut builder = WidgetTreeBuilder { app, frp, @@ -572,8 +589,8 @@ impl TreeModel { metadata_map: &metadata_map, connected_map: &connected_map, usage_type_map: &usage_type_map, - old_widgets, - new_widgets: default(), + old_nodes, + new_nodes: default(), parent_ast_id: default(), parent_crumbs: default(), parent_state: default(), @@ -581,7 +598,7 @@ impl TreeModel { let child = builder.child_widget(tree.root_ref(), 0); self.display_object.replace_children(&[child]); - self.widgets_map.replace(builder.new_widgets); + self.nodes_map.replace(builder.new_nodes); } /// Convert span tree node to a corresponding widget tree pointer. Every node in the span tree @@ -631,7 +648,7 @@ impl TreeModel { pointer: &WidgetTreePointer, f: impl FnOnce(&TreeNode) -> T, ) -> Option { - self.widgets_map.borrow().get(pointer).map(f) + self.nodes_map.borrow().get(pointer).map(f) } } @@ -731,8 +748,8 @@ struct WidgetTreeBuilder<'a> { metadata_map: &'a HashMap, connected_map: &'a HashMap, usage_type_map: &'a HashMap, - old_widgets: HashMap, - new_widgets: HashMap, + old_nodes: HashMap, + new_nodes: HashMap, parent_ast_id: Option, parent_crumbs: span_tree::Crumbs, parent_state: NodeState, @@ -811,7 +828,7 @@ impl<'a> WidgetTreeBuilder<'a> { WidgetTreePointer { id, crumbs } } }; - let old_node = self.old_widgets.remove(&tree_ptr); + let old_node = self.old_nodes.remove(&tree_ptr); let is_placeholder = span_tree_node.is_expected_argument(); let sibling_offset = span_tree_node.sibling_offset.as_usize(); let usage_type = tree_ptr.id.and_then(|id| self.usage_type_map.get(&id)).cloned(); @@ -898,7 +915,7 @@ impl<'a> WidgetTreeBuilder<'a> { child_root.set_margin_left(left_margin); } - self.new_widgets.insert(tree_ptr.clone(), child_node); + self.new_nodes.insert(tree_ptr.clone(), child_node); child_root } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 2c55ee28ef98..1ead9875cc7a 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -97,11 +97,11 @@ impl super::SpanWidget for Widget { } else { let ty = ctx.state.usage_type.clone(); let ty = ty.or_else(|| ctx.span_tree_node.kind.tp().map(|t| crate::Type(t.into()))); - crate::type_coloring::compute_for_code(ty.as_ref(), &ctx.styles()) + crate::type_coloring::compute_for_code(ty.as_ref(), ctx.styles()) }; let input = &self.frp.public.input; - input.content.emit(content.clone()); + input.content.emit(content); input.text_color.emit(text_color); input.crumbs.emit(ctx.span_tree_node.crumbs.clone()); } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 292c125eb10c..cf72913e4c1b 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -193,7 +193,7 @@ impl super::SpanWidget for Widget { // value application triggers operations that can introduce a few dropped frames, // we want to delay the dropdown closing animation after that is handled. // Otherwise the animation finishes within single frame, which looks bad. - let close_after_selection_timer = frp::io::timer::Timeout::new(&network); + let close_after_selection_timer = frp::io::timer::Timeout::new(network); close_after_selection_timer.restart <+ dropdown.user_select_action.constant(1); eval close_after_selection_timer.on_expired((()) focus_receiver.blur()); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs b/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs index 045054f92665..e2cac352c5b8 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs @@ -4,6 +4,7 @@ //! Currently the view is a simle [`Elements`] component, which will be replaced with a rich //! view in [future tasks](https://github.com/enso-org/enso/issues/5631). +use crate::component::node::input::widget::Metadata; use crate::prelude::*; use crate::component::node::input::widget::single_choice::triangle; diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 9e297c30d5c2..c16a02325c94 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -4160,9 +4160,9 @@ mod tests { // 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_deprecated.emit_mouse_down(PrimaryButton); - port.hover.events_deprecated.emit_mouse_up(PrimaryButton); + let port_hover = node_2.model().input_port_shape().expect("No input port."); + port_hover.events_deprecated.emit_mouse_down(PrimaryButton); + port_hover.events_deprecated.emit_mouse_up(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)); } diff --git a/lib/rust/ensogl/core/src/display/scene.rs b/lib/rust/ensogl/core/src/display/scene.rs index 7dfd22b3d815..e68f73dec7d3 100644 --- a/lib/rust/ensogl/core/src/display/scene.rs +++ b/lib/rust/ensogl/core/src/display/scene.rs @@ -1221,7 +1221,7 @@ impl Scene { /// this frame. #[profile(Debug)] pub fn early_update(&self, time: animation::TimeInfo) -> UpdateStatus { - if let Some(_) = &*self.context.borrow() { + if self.context.borrow().is_some() { debug_span!("Early update.").in_scope(|| { let mut scene_was_dirty = false; self.frp.frame_time_source.emit(time.since_animation_loop_started.unchecked_raw()); diff --git a/lib/rust/frp/src/nodes.rs b/lib/rust/frp/src/nodes.rs index fea69298e996..962ec4d48dbf 100644 --- a/lib/rust/frp/src/nodes.rs +++ b/lib/rust/frp/src/nodes.rs @@ -2334,18 +2334,20 @@ where self.gate.is_dropped() } - fn on_event_if_exists(&self, stack: CallStack, is_active: &bool) -> bool { + fn on_event_if_exists(&self, stack: CallStack, new_active: &bool) -> bool { if let Some(gate) = self.gate.upgrade() { - let new_state = match is_active { - true => BufferedGateState::Active, - false => BufferedGateState::Inactive, - }; - match gate.state.replace(new_state) { - BufferedGateState::Active => {} - BufferedGateState::Inactive => {} - BufferedGateState::Buffered => { + match (gate.state.get(), new_active) { + (BufferedGateState::Active, false) => { + gate.state.set(BufferedGateState::Inactive); + } + (BufferedGateState::Inactive, true) => { + gate.state.set(BufferedGateState::Active); + } + (BufferedGateState::Buffered, true) => { + gate.state.set(BufferedGateState::Active); gate.event.with(|value| gate.emit_event(stack, value)); } + _ => (), } true } else { @@ -4723,9 +4725,7 @@ impl HasOutput for DropSource { } impl ValueProvider for DropSource { - fn value(&self) -> () { - () - } + fn value(&self) {} fn with(&self, _: impl FnOnce(&()) -> T) -> Option where Self: Sized { From 7810d2a2524ae0004e5a62ae64f1f04a372e67a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Wed, 19 Apr 2023 18:21:45 +0200 Subject: [PATCH 18/45] do not create dropdown elements until opened --- .../src/component/node/input/area.rs | 4 +- .../src/component/node/input/widget.rs | 95 ++++++++- .../src/component/node/input/widget/label.rs | 94 ++++++-- .../node/input/widget/single_choice.rs | 201 ++++++++++-------- 4 files changed, 281 insertions(+), 113 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index fe5830df2a05..27ce115fb83b 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -516,7 +516,6 @@ impl Area { (default(), e.into()) }); - model.widget_tree.set_view_mode <+ frp.set_view_mode; widget_code_update <- model.widget_tree.value_changed.map(|(crumbs, value)| { let expression = value.clone().unwrap_or_default(); (crumbs.clone(), expression) @@ -547,6 +546,9 @@ impl Area { finished <- frp.set_profiling_status.map(|s| s.is_finished()); profiled <- in_profiling_mode && finished; + model.widget_tree.set_view_mode <+ frp.set_view_mode; + model.widget_tree.set_profiling_status <+ frp.set_profiling_status; + use theme::code::syntax; let std_selection_color = model.styles_frp.get_color(syntax::selection); let profiled_selection_color = model.styles_frp.get_color(syntax::profiling::selection); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 7f18ef92d9bc..aaef7bb7b98a 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -37,9 +37,10 @@ pub const WIDGET_SPACING_PER_OFFSET: f32 = 7.224_609_4; ensogl::define_endpoints_2! { Input { - set_ports_visible (bool), - set_view_mode (crate::view::Mode), - set_disabled (bool), + set_ports_visible (bool), + set_view_mode (crate::view::Mode), + set_profiling_status (crate::node::profiling::Status), + set_disabled (bool), } Output { value_changed (span_tree::Crumbs, Option), @@ -311,6 +312,7 @@ impl DropdownValue for Entry { pub struct WidgetsFrp { pub(super) set_ports_visible: frp::Sampler, pub(super) set_view_mode: frp::Sampler, + pub(super) set_profiling_status: frp::Sampler, pub(super) value_changed: frp::Any<(span_tree::Crumbs, Option)>, pub(super) request_import: frp::Any, pub(super) on_port_hover: frp::Any>, @@ -351,20 +353,25 @@ impl Tree { let network = &frp.network; frp::extend! { network - set_ports_visible <- frp.set_ports_visible.sampler(); - set_view_mode <- frp.set_view_mode.sampler(); - on_port_hover <- any(...); + set_ports_visible <- frp.set_ports_visible.sampler(); + set_view_mode <- frp.set_view_mode.sampler(); + set_profiling_status <- frp.set_profiling_status.sampler(); + + on_port_hover <- any(...); + on_port_press <- any(...); + trace on_port_hover; frp.private.output.on_port_hover <+ on_port_hover; + frp.private.output.on_port_press <+ on_port_press; } let value_changed = frp.private.output.value_changed.clone_ref(); let request_import = frp.private.output.request_import.clone_ref(); - let on_port_press = frp.private.output.on_port_press.clone_ref(); let pointer_style = frp.private.output.pointer_style.clone_ref(); let connected_port_updated = frp.private.output.connected_port_updated.clone_ref(); let widgets_frp = WidgetsFrp { set_ports_visible, set_view_mode, + set_profiling_status, value_changed, request_import, on_port_hover, @@ -440,6 +447,7 @@ impl Tree { tree_node: &span_tree::node::Ref, ) -> Option { let pointer = self.model.get_node_widget_pointer(tree_node)?; + warn!("get_widget_display_object pointer: {:?}", pointer); self.model.with_node(&pointer, |w| w.display_object().clone()) } @@ -594,6 +602,7 @@ impl TreeModel { parent_ast_id: default(), parent_crumbs: default(), parent_state: default(), + extensions: default(), }; let child = builder.child_widget(tree.root_ref(), 0); @@ -622,7 +631,7 @@ impl TreeModel { let (_, ast_parent_data) = tree_node.crumbs.into_iter().enumerate().fold( (root, root_ast_data), |(node, last_seen), (index, crumb)| { - let ast_data = node.node.ast_id.map(|id| (id, index + 1)).or(last_seen); + let ast_data = node.node.ast_id.map(|id| (id, index)).or(last_seen); (node.child(*crumb).expect("Node ref must be valid"), ast_data) }, ); @@ -695,6 +704,9 @@ pub struct ConfigContext<'a, 'b> { /// Additional state associated with configured widget tree node, such as its depth, connection /// status or parent node information. pub(super) state: NodeState, + /// The length of tree extensions vector before the widget was configured. Used to determine + /// which extensions were added by the widget parents, and which are new. + parent_extensions_len: usize, } impl<'a, 'b> ConfigContext<'a, 'b> { @@ -718,6 +730,61 @@ impl<'a, 'b> ConfigContext<'a, 'b> { pub fn styles(&self) -> &StyleWatch { self.builder.styles } + + /// Set an extension object of specified type at the current tree position. Any descendant + /// widget will be able to access it, as long as it can name its type. This allows for + /// configure-time communication between any widgets inside the widget tree. + pub fn set_extension(&mut self, val: T) { + let id = std::any::TypeId::of::(); + match self.self_extension_index_by_type(id) { + Some(idx) => *self.builder.extensions[idx].downcast_mut().unwrap() = val, + None => { + self.builder.extensions.push(Box::new(val)); + } + } + } + + /// Get an extension object of specified type at the current tree position. The extension object + /// must have been created by any parent widget up in the hierarchy. If it does not exist, this + /// method will return `None`. + pub fn get_extension(&self) -> Option<&T> { + self.any_extension_index_by_type(std::any::TypeId::of::()) + .map(|idx| self.builder.extensions[idx].downcast_ref().unwrap()) + } + + /// Modify an extension object of specified type at the current tree position. The modification + /// will only be visible to the descendants of this widget, even if the extension was added + /// by one of its parents. + pub fn modify_extension(&mut self, f: impl FnOnce(&mut T)) + where T: Any + Default + Clone { + match self.any_extension_index_by_type(std::any::TypeId::of::()) { + // This extension has been created by this widget, so we can modify it directly. + Some(idx) if idx >= self.parent_extensions_len => { + f(self.builder.extensions[idx].downcast_mut().unwrap()); + } + // The extension exist, but has been created by one of the parents. We need to clone it. + Some(idx) => { + let mut val: T = self.builder.extensions[idx].downcast_mut::().unwrap().clone(); + f(&mut val); + self.builder.extensions.push(Box::new(val)); + } + // The extension does not exist yet, so we need to create it from scratch. + None => { + let mut val = T::default(); + f(&mut val); + self.builder.extensions.push(Box::new(val)); + } + } + } + + fn any_extension_index_by_type(&self, id: std::any::TypeId) -> Option { + self.builder.extensions.iter().rposition(|ext| ext.deref().type_id() == id) + } + + fn self_extension_index_by_type(&self, id: std::any::TypeId) -> Option { + let self_extensions = &self.builder.extensions[self.parent_extensions_len..]; + self_extensions.iter().rposition(|ext| ext.deref().type_id() == id) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -753,6 +820,7 @@ struct WidgetTreeBuilder<'a> { parent_ast_id: Option, parent_crumbs: span_tree::Crumbs, parent_state: NodeState, + extensions: Vec>, } impl<'a> WidgetTreeBuilder<'a> { @@ -870,8 +938,14 @@ impl<'a> WidgetTreeBuilder<'a> { let state = NodeState { depth, connection, subtree_connection, disabled, usage_type }; let state_to_restore = std::mem::replace(&mut self.parent_state, state.clone()); - let ctx = - ConfigContext { builder: &mut *self, display: meta.display, span_tree_node, state }; + let parent_extensions_len = self.extensions.len(); + let ctx = ConfigContext { + builder: &mut *self, + display: meta.display, + span_tree_node, + state, + parent_extensions_len, + }; let app = ctx.app(); let frp = ctx.frp(); @@ -905,6 +979,7 @@ impl<'a> WidgetTreeBuilder<'a> { self.parent_ast_id = id; self.parent_crumbs = crumbs; } + self.extensions.truncate(parent_extensions_len); // Apply left margin to the widget, based on its offset relative to the previous sibling. diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 1ead9875cc7a..bb1cf2f82659 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -5,6 +5,7 @@ use crate::prelude::*; use crate::component::node::input::area::TEXT_SIZE; use ensogl::data::color; use ensogl::display::object; +use ensogl::display::shape::StyleWatch; use ensogl_component::text; use ensogl_hardcoded_theme as theme; @@ -19,7 +20,8 @@ pub struct Config; ensogl::define_endpoints_2! { Input { content(ImString), - text_color(color::Lcha), + text_color(ColorState), + text_weight(text::Weight), crumbs(span_tree::Crumbs), } } @@ -51,24 +53,27 @@ impl super::SpanWidget for Widget { let label = text::Text::new(app); label.set_property_default(text::Size(TEXT_SIZE)); label.set_y(TEXT_SIZE); - layers.above_nodes_text.add(&label); + layers.label.add(&label); root.add_child(&label); let frp = Frp::new(); let network = &frp.network; - let hover_color: color::Lcha = ctx.styles().get_color(theme::code::types::selected).into(); - + let styles = ctx.styles(); frp::extend! { network - color_change <- frp.text_color.on_change(); parent_port_hovered <- widgets_frp.on_port_hover.map2(&frp.crumbs, |h, crumbs| { h.on().map_or(false, |h| crumbs.starts_with(h)) }); - label_color <- color_change.all_with(&parent_port_hovered, move |color, hovered| { - if *hovered { hover_color } else { *color } - }); + label_color <- frp.text_color.all_with4( + &parent_port_hovered, &widgets_frp.set_view_mode, &widgets_frp.set_profiling_status, + f!([styles](state, hovered, mode, status) { + state.to_color(*hovered, *mode, *status, &styles) + }) + ); label_color <- label_color.on_change(); + label_weight <- frp.text_weight.on_change(); eval label_color((color) label.set_property_default(color)); + eval label_weight((weight) label.set_property_default(weight)); content_change <- frp.content.on_change(); eval content_change((content) label.set_content(content)); @@ -88,21 +93,82 @@ impl super::SpanWidget for Widget { ctx.expression_at(ctx.span_tree_node.span()) }; - let text_color: color::Lcha = if ctx.state.subtree_connection.is_connected() { - ctx.styles().get_color(theme::code::types::selected).into() + let color_state: ColorState = if ctx.state.subtree_connection.is_connected() { + ColorState::Connected } else if ctx.state.disabled { - ctx.styles().get_color(theme::code::syntax::disabled).into() + ColorState::Disabled } else if is_placeholder { - ctx.styles().get_color(theme::code::syntax::expected).into() + ColorState::Placeholder } else { let ty = ctx.state.usage_type.clone(); let ty = ty.or_else(|| ctx.span_tree_node.kind.tp().map(|t| crate::Type(t.into()))); - crate::type_coloring::compute_for_code(ty.as_ref(), ctx.styles()) + let color = crate::type_coloring::compute_for_code(ty.as_ref(), ctx.styles()); + ColorState::FromType(color) }; + let ext = ctx.get_extension::().copied().unwrap_or_default(); + let text_weight = if ext.bold { text::Weight::Bold } else { text::Weight::Normal }; let input = &self.frp.public.input; input.content.emit(content); - input.text_color.emit(text_color); + input.text_color.emit(color_state); + input.text_weight(text_weight); input.crumbs.emit(ctx.span_tree_node.crumbs.clone()); } } + + + +// ================= +// === Extension === +// ================= + +/// Label extension data that can be set by any of the parent widgets. +#[derive(Clone, Copy, Debug, Default)] +pub struct Extension { + /// Display all descendant labels with bold text weight. + pub bold: bool, +} + + + +// ================== +// === ColorState === +// ================== + +/// Configured color state of a label widget. +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, Default)] +pub enum ColorState { + #[default] + Connected, + Disabled, + Placeholder, + FromType(color::Lcha), +} + +impl ColorState { + fn to_color( + &self, + is_hovered: bool, + view_mode: crate::view::Mode, + status: crate::node::profiling::Status, + styles: &StyleWatch, + ) -> color::Lcha { + use theme::code::syntax; + let profiling_mode = view_mode.is_profiling(); + let profiled = profiling_mode && status.is_finished(); + let color_path = match self { + _ if is_hovered => theme::code::types::selected, + ColorState::Connected => theme::code::types::selected, + ColorState::Disabled if profiled => syntax::profiling::disabled, + ColorState::Placeholder if profiled => syntax::profiling::expected, + ColorState::Disabled => syntax::disabled, + ColorState::Placeholder => syntax::expected, + ColorState::FromType(_) if profiled => syntax::profiling::base, + ColorState::FromType(_) if profiling_mode => syntax::base, + ColorState::FromType(typed) => return *typed, + }; + + styles.get_color(color_path).into() + } +} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index cf72913e4c1b..fb6f36e9ed27 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -2,7 +2,6 @@ use crate::prelude::*; -use crate::component::node::input::area::TEXT_SIZE; use crate::component::node::input::widget::Entry; use enso_frp as frp; use ensogl::control::io::mouse; @@ -10,7 +9,6 @@ use ensogl::data::color; use ensogl::display; use ensogl::display::object::event; use ensogl_component::drop_down::Dropdown; -use ensogl_component::text; @@ -77,7 +75,6 @@ pub struct Config { ensogl::define_endpoints_2! { Input { set_entries(Rc>), - content(ImString), current_value(Option), current_crumbs(span_tree::Crumbs), } @@ -93,9 +90,8 @@ pub struct Widget { content_wrapper: display::object::Instance, dropdown_wrapper: display::object::Instance, label_wrapper: display::object::Instance, - args_wrapper: display::object::Instance, - dropdown: Dropdown, - label: text::Text, + dropdown: Rc>, + label: super::label::Widget, activation_shape: triangle::View, } @@ -109,42 +105,27 @@ impl super::SpanWidget for Widget { fn new(_: &Config, ctx: &super::ConfigContext) -> Self { let app = ctx.app(); let widgets_frp = ctx.frp(); - // ╭─display_object─────────────────────────────────────╮ - // │╭─content_wrapper──────────────────────────────────╮│ - // ││╭ shape ╮ ╭ label_wrapper ────╮ ╭ args_wrapper ─╮ ││ - // │││ │ │ │ │ │ ││ - // │││ │ │ │ │ │ ││ - // ││╰───────╯ ╰───────────────────╯ ╰───────────────╯ ││ - // │╰──────────────────────────────────────────────────╯│ - // ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ - // │ ◎ dropdown_wrapper size=0 │ - // ╰────────────────────────────────────────────────────╯ - - let layers = &app.display.default_scene.layers; + // ╭─display_object────────────────────╮ + // │╭─content_wrapper─────────────────╮│ + // ││ ╭ shape ╮ ╭ label_wrapper ────╮ ││ + // ││ │ │ │ │ ││ + // ││ │ │ │ │ ││ + // ││ ╰───────╯ ╰───────────────────╯ ││ + // │╰─────────────────────────────────╯│ + // ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ + // │ ◎ dropdown_wrapper size=0 │ + // ╰───────────────────────────────────╯ + let dot_color = color::Rgba::from(ACTIVATION_SHAPE_COLOR.with_alpha(1.0)).into(); let activation_shape = triangle::View::new(); activation_shape.color.set(dot_color); activation_shape.set_size(ACTIVATION_SHAPE_SIZE); - let label = text::Text::new(app); - layers.above_nodes_text.add(&label); - label.set_property_default(text::Size(TEXT_SIZE)); - label.set_y(TEXT_SIZE); - - let dropdown = app.new_view::>(); - layers.above_nodes.add(&dropdown); - dropdown.set_y(DROPDOWN_Y_OFFSET); - dropdown.set_max_open_size(DROPDOWN_MAX_SIZE); - dropdown.allow_deselect_all(true); - let display_object = display::object::Instance::new(); let content_wrapper = display_object.new_child(); content_wrapper.add_child(&activation_shape); let label_wrapper = content_wrapper.new_child(); - label_wrapper.add_child(&label); - let args_wrapper = content_wrapper.new_child(); let dropdown_wrapper = display_object.new_child(); - dropdown_wrapper.add_child(&dropdown); display_object @@ -158,15 +139,14 @@ impl super::SpanWidget for Widget { .set_children_alignment_left_center() .justify_content_center_y(); - args_wrapper + label_wrapper .use_auto_layout() .set_children_alignment_left_center() .justify_content_center_y(); dropdown_wrapper.set_size((0.0, 0.0)).set_alignment_left_top(); - label_wrapper.set_size_y(TEXT_SIZE); - + let label = super::label::Widget::new(&default(), ctx); let config_frp = Frp::new(); let network = &config_frp.network; @@ -176,9 +156,11 @@ impl super::SpanWidget for Widget { frp::extend! { network init <- source::<()>(); + initialize_dropdown <- any_(...); let focus_in = focus_receiver.on_event::(); let focus_out = focus_receiver.on_event::(); + initialize_dropdown <+ focus_in; is_open <- bool(&focus_out, &focus_in); let dot_mouse_down = activation_shape.on_event::(); @@ -189,57 +171,66 @@ impl super::SpanWidget for Widget { false => focus_receiver.blur(), }); - // Close the dropdown after a short delay after selection. Because the dropdown - // value application triggers operations that can introduce a few dropped frames, - // we want to delay the dropdown closing animation after that is handled. - // Otherwise the animation finishes within single frame, which looks bad. - let close_after_selection_timer = frp::io::timer::Timeout::new(network); - close_after_selection_timer.restart <+ dropdown.user_select_action.constant(1); - eval close_after_selection_timer.on_expired((()) focus_receiver.blur()); - current_value <- input.current_value.on_change(); entries <- input.set_entries.on_change(); entries_and_value <- all(&entries, ¤t_value); entries_and_value <- entries_and_value.debounce(); + dropdown_set_open <- is_open.on_change(); + dropdown_set_all_entries <- entries_and_value.map(|(e, _)| e.deref().clone()); entries_and_value <- entries_and_value.buffered_gate(&is_open); - dropdown.set_all_entries <+ entries_and_value.map(|(rc, _)| rc.deref().clone()); - dropdown.set_open <+ is_open.on_change(); + // sources from dropdown, lazily initialized + dropdown_user_select_action <- any(...); + dropdown_selected_entries <- any(...); + + // Close the dropdown after a short delay after selection. Because the dropdown + // value application triggers operations that can introduce a few dropped frames, + // we want to delay the dropdown closing animation after that is handled. + // Otherwise the animation finishes within single frame, which looks bad. + let close_after_selection_timer = frp::io::timer::Timeout::new(network); + close_after_selection_timer.restart <+ dropdown_user_select_action.constant(1); + eval close_after_selection_timer.on_expired((()) focus_receiver.blur()); selected_entry <- entries_and_value.map(|(e, v)| entry_for_current_value(e, v)); - dropdown.set_selected_entries <+ selected_entry.map(|e| e.iter().cloned().collect()); + dropdown_set_selected_entries <- selected_entry.map(|e| e.iter().cloned().collect()); - dropdown_entry <- dropdown.selected_entries.map(|e| e.iter().next().cloned()); + dropdown_entry <- dropdown_selected_entries.map(|e: &HashSet| e.iter().next().cloned()); // Emit the output value only after actual user action. This prevents the // dropdown from emitting its initial value when it is opened, which can // represent slightly different version of code than actually written. - submitted_entry <- dropdown_entry.sample(&dropdown.user_select_action); + submitted_entry <- dropdown_entry.sample(&dropdown_user_select_action); dropdown_out_value <- submitted_entry.map(|e| e.as_ref().map(Entry::value)); dropdown_out_import <- submitted_entry.map(|e| e.as_ref().and_then(Entry::required_import)); - label.set_content <+ input.content.on_change(); - label_width <- label.width.on_change(); - eval label_width((w) { label_wrapper.set_size_x(*w); }); - - has_value <- current_value.map(|v| v.is_some()); - has_value <- all(&has_value, &init)._0(); - eval has_value([label] (&has_value) { - let color = if has_value { - color::Lcha::new(0.0, 0.0, 0.0, 1.0) - } else { - color::Lcha::new(0.5, 0.0, 0.0, 1.0) - }; - let weight = if has_value { text::Weight::Bold } else { text::Weight::Normal }; - label.set_property_default(color); - label.set_property_default(weight); - }); - widgets_frp.request_import <+ dropdown_out_import.unwrap(); widgets_frp.value_changed <+ dropdown_out_value.map2(&input.current_crumbs, move |t: &Option, crumbs: &span_tree::Crumbs| (crumbs.clone(), t.clone()) ); }; + frp::extend! { network + dropdown_set_all_entries <- dropdown_set_all_entries.sampler(); + dropdown_set_selected_entries <- dropdown_set_selected_entries.sampler(); + dropdown_set_open <- dropdown_set_open.sampler(); + } + + let dropdown = LazyDropdown { + app: app.clone_ref(), + set_all_entries: dropdown_set_all_entries, + set_selected_entries: dropdown_set_selected_entries, + set_open: dropdown_set_open, + selected_entries: dropdown_selected_entries, + user_select_action: dropdown_user_select_action, + dropdown: None, + }; + let dropdown = Rc::new(RefCell::new(dropdown)); + + frp::extend! { network + eval initialize_dropdown([dropdown, dropdown_wrapper](_) { + dropdown.borrow_mut().init(&dropdown_wrapper); + }); + } + init.emit(()); Self { @@ -248,14 +239,13 @@ impl super::SpanWidget for Widget { content_wrapper, dropdown_wrapper, label_wrapper, - args_wrapper, dropdown, activation_shape, label, } } - fn configure(&mut self, config: &Config, ctx: super::ConfigContext) { + fn configure(&mut self, config: &Config, mut ctx: super::ConfigContext) { let input = &self.config_frp.public.input; let has_value = !ctx.span_tree_node.is_insertion_point(); @@ -266,31 +256,24 @@ impl super::SpanWidget for Widget { input.current_value(current_value); input.set_entries(config.entries.clone()); + if has_value { + ctx.modify_extension::(|ext| ext.bold = true); + } - let mut label_expr_span = ctx.span_tree_node.span(); - - if ctx.span_tree_node.is_chained() { - let mut chain = ctx.span_tree_node.clone().chain_children_iter(); - if let Some(first_child) = chain.next() { - label_expr_span = first_child.span(); - }; - + if ctx.span_tree_node.children.is_empty() { + self.label.configure(&default(), ctx); + self.label_wrapper.replace_children(&[self.label.root_object()]); + } else { + // let mut chain = ctx.span_tree_node.clone().chain_children_iter(); // Do not increment the depth. If the dropdown is displayed, it should also display // its arguments. - let children = - chain.map(|child| ctx.builder.child_widget(child, ctx.state.depth)).collect_vec(); - self.args_wrapper.replace_children(&children); - } else { - self.args_wrapper.remove_all_children(); + let children = ctx + .span_tree_node + .children_iter() + .map(|child| ctx.builder.child_widget(child, ctx.state.depth + 1)) + .collect_vec(); + self.label_wrapper.replace_children(&children); } - - let content: ImString = has_value - .then(|| ctx.expression_at(label_expr_span).into()) - .or_else(|| config.label.clone()) - .or_else(|| ctx.span_tree_node.kind.argument_name().map(Into::into)) - .unwrap_or_default(); - - input.content(content); } } @@ -320,3 +303,45 @@ fn entry_for_current_value( with_partial_match.cloned().unwrap_or_else(|| Entry::from_value(current_value.clone())); Some(with_fallback) } + +#[derive(Debug)] +struct LazyDropdown { + app: ensogl::application::Application, + set_all_entries: frp::Sampler>, + set_selected_entries: frp::Sampler>, + set_open: frp::Sampler, + selected_entries: frp::Any>, + user_select_action: frp::Any<()>, + dropdown: Option>, +} + +impl LazyDropdown { + fn init(&mut self, parent: &display::object::Instance) { + if self.dropdown.is_some() { + return; + } + + let dropdown = self.app.new_view::>(); + let dropdown = self.dropdown.insert(dropdown); + + parent.add_child(dropdown); + let layers = &self.app.display.default_scene.layers; + layers.above_nodes.add(&*dropdown); + dropdown.set_y(DROPDOWN_Y_OFFSET); + dropdown.set_max_open_size(DROPDOWN_MAX_SIZE); + dropdown.allow_deselect_all(true); + + frp::extend! { _network + dropdown.set_all_entries <+ self.set_all_entries; + dropdown.set_selected_entries <+ self.set_selected_entries; + dropdown.set_open <+ self.set_open; + self.selected_entries <+ dropdown.selected_entries; + self.user_select_action <+ dropdown.user_select_action; + } + + + dropdown.set_all_entries.emit(self.set_all_entries.value().clone()); + dropdown.set_selected_entries.emit(self.set_selected_entries.value().clone()); + dropdown.set_open.emit(self.set_open.value().clone()); + } +} From 119f431e6ea3d8658e96777b96286663403abf2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Wed, 19 Apr 2023 23:59:28 +0200 Subject: [PATCH 19/45] fix vector editor default metadata selection --- .../src/component/node/input/widget.rs | 32 +++++++++++++------ .../node/input/widget/vector_editor.rs | 2 +- lib/rust/ensogl/pack/js/src/runner/index.ts | 3 +- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index aaef7bb7b98a..bdb27286f1ef 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -208,23 +208,36 @@ impl Metadata { Self::always(vector_editor::Config { item_editor: None, item_default: "_".into() }) } - fn from_kind( - kind: &span_tree::node::Kind, - _usage_type: Option, - has_children: bool, + fn from_node( + node: &span_tree::node::Ref, + usage_type: Option, + expression: &str, ) -> Self { use span_tree::node::Kind; + let kind = &node.kind; + let has_children = !node.children.is_empty(); + const VECTOR_TYPE: &str = "Standard.Base.Data.Vector.Vector"; let is_array_enabled = ARGS.groups.feature_preview.options.vector_editor.value; + let is_vector = |arg: &span_tree::node::Argument| { + let type_matches = usage_type + .as_ref() + .map(|t| t.as_str()) + .or(arg.tp.as_deref()) + .map_or(false, |tp| tp.contains(VECTOR_TYPE)); + if type_matches { + let node_expr = &expression[node.span()]; + node_expr.starts_with('[') && node_expr.ends_with(']') + } else { + false + } + }; match kind { Kind::Argument(arg) if !arg.tag_values.is_empty() => Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values), - Kind::Argument(arg) - if is_array_enabled - && arg.tp.as_ref().map_or(false, |tp| tp.contains(VECTOR_TYPE)) => - Self::vector_editor(), + Kind::Argument(arg) if is_array_enabled && is_vector(arg) => Self::vector_editor(), Kind::InsertionPoint(arg) if arg.kind.is_expected_argument() => if !arg.tag_values.is_empty() { Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values) @@ -911,7 +924,6 @@ impl<'a> WidgetTreeBuilder<'a> { // type and whether it has children. let mut meta_fallback = None; let kind = &span_tree_node.kind; - let has_children = !span_tree_node.children.is_empty(); let meta = set_metadata .as_ref() .or_else(|| { @@ -925,7 +937,7 @@ impl<'a> WidgetTreeBuilder<'a> { }) .unwrap_or_else(|| { meta_fallback.get_or_insert_with(|| { - Metadata::from_kind(kind, usage_type.clone(), has_children) + Metadata::from_node(&span_tree_node, usage_type.clone(), self.node_expression) }) }); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs b/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs index e2cac352c5b8..d1e0bec374be 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs @@ -4,12 +4,12 @@ //! Currently the view is a simle [`Elements`] component, which will be replaced with a rich //! view in [future tasks](https://github.com/enso-org/enso/issues/5631). -use crate::component::node::input::widget::Metadata; use crate::prelude::*; use crate::component::node::input::widget::single_choice::triangle; use crate::component::node::input::widget::single_choice::ACTIVATION_SHAPE_COLOR; use crate::component::node::input::widget::single_choice::ACTIVATION_SHAPE_SIZE; +use crate::component::node::input::widget::Metadata; use ensogl::application::Application; use ensogl::control::io::mouse; diff --git a/lib/rust/ensogl/pack/js/src/runner/index.ts b/lib/rust/ensogl/pack/js/src/runner/index.ts index 50d78ce0148b..2f61ce4a2270 100644 --- a/lib/rust/ensogl/pack/js/src/runner/index.ts +++ b/lib/rust/ensogl/pack/js/src/runner/index.ts @@ -202,8 +202,9 @@ export class App { if (inputConfig != null) { this.config = inputConfig } + const unrecognized = this.config.loadAll([opts?.config, host.urlParams()]) logger.log(this.config.prettyPrint()) - return this.config.loadAll([opts?.config, host.urlParams()]) + return unrecognized }) if (unrecognized.length > 0) { logger.error(`Unrecognized configuration parameters: ${unrecognized.join(', ')}.`) From 4775d5759f9c92243427bcf7d744315a1a5a88e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Thu, 20 Apr 2023 16:29:27 +0200 Subject: [PATCH 20/45] allow multiple widgets on the same span --- .../src/component/node/input/area.rs | 15 +- .../src/component/node/input/widget.rs | 163 ++++++++++++------ lib/rust/ensogl/core/src/debug/stats.rs | 2 +- lib/rust/frp/src/stream.rs | 16 +- 4 files changed, 127 insertions(+), 69 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 27ce115fb83b..237f1eed51b7 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -37,17 +37,6 @@ pub const TEXT_OFFSET: f32 = 10.0; /// Total height of the node input area. pub const NODE_HEIGHT: f32 = 18.0; -/// Width of a single glyph -// TODO: avoid using hardcoded value. See https://www.pivotaltracker.com/story/show/183567623. -pub const GLYPH_WIDTH: f32 = 7.224_609_4; - -/// Enable visual port debug mode and additional port creation logging. -pub const DEBUG: bool = false; - -/// Visual port offset for debugging purposes. Applied hierarchically. Applied only when `DEBUG` is -/// set to `true`. -pub const DEBUG_PORT_OFFSET: f32 = 5.0; - /// Text size used for input area text. pub const TEXT_SIZE: f32 = 12.0; @@ -237,7 +226,7 @@ impl Model { let crumbs = hovered.on()?; let expr = self.expression.borrow(); let port = expr.span_tree.get_node(crumbs).ok()?; - let display_object = self.widget_tree.get_widget_display_object(&port)?; + let display_object = self.widget_tree.get_port_display_object(&port)?; let tp = port.tp().map(|t| Type(t.into())); let color = tp.as_ref().map(|tp| type_coloring::compute(tp, &self.styles)); let pad_x = node::input::port::PORT_PADDING_X * 2.0; @@ -567,7 +556,7 @@ impl Area { pub fn port_offset(&self, crumbs: &[Crumb]) -> Option> { let expr = self.model.expression.borrow(); let node = expr.get_node(crumbs).ok()?; - let instance = self.model.widget_tree.get_widget_display_object(&node)?; + let instance = self.model.widget_tree.get_port_display_object(&node)?; let pos = instance.global_position(); let node_pos = self.model.display_object.global_position(); let size = instance.computed_size(); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index bdb27286f1ef..d752f4977019 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -340,7 +340,9 @@ pub struct WidgetsFrp { // === Widget === // ============== -/// The node widget tree view. Contains all widgets created from the node's span tree. +/// The node widget tree view. Contains all widgets created from the node's span tree, as well as +/// all input ports of a node. The tree is initialized to empty state, waiting for first +/// `rebuild_tree` call to build appropriate view hierarchy. #[derive(Debug, Deref, Clone, CloneRef)] pub struct Tree { #[deref] @@ -455,13 +457,12 @@ impl Tree { /// Get the root display object of the widget port for given span tree node. Not all nodes must /// have a distinct widget, so the returned value might be `None`. - pub fn get_widget_display_object( + pub fn get_port_display_object( &self, tree_node: &span_tree::node::Ref, ) -> Option { let pointer = self.model.get_node_widget_pointer(tree_node)?; - warn!("get_widget_display_object pointer: {:?}", pointer); - self.model.with_node(&pointer, |w| w.display_object().clone()) + self.model.with_port(pointer, |w| w.display_object().clone()) } /// Get hover shapes for all ports in the tree. Used for testing to simulate mouse events. @@ -519,9 +520,10 @@ impl display::Object for TreeNode { struct TreeModel { app: Application, display_object: display::object::Instance, - nodes_map: RefCell>, + nodes_map: RefCell>, + ports_map: RefCell>, metadata_map: Rc>>, - connected_map: Rc>>, + connected_map: Rc>>, usage_type_map: Rc>>, disabled: Cell, tree_dirty: Cell, @@ -544,6 +546,7 @@ impl TreeModel { display_object, disabled: default(), nodes_map: default(), + ports_map: default(), metadata_map: default(), connected_map: default(), usage_type_map: default(), @@ -561,7 +564,7 @@ impl TreeModel { } /// Set the connection status under given widget. It may cause the tree to be marked as dirty. - fn set_connected(&self, pointer: WidgetTreePointer, status: ConnectionStatus) { + fn set_connected(&self, pointer: MainWidgetPointer, status: ConnectionStatus) { let mut map = self.connected_map.borrow_mut(); let dirty = map.synchronize_entry(pointer, status.data()); if dirty { @@ -611,9 +614,10 @@ impl TreeModel { connected_map: &connected_map, usage_type_map: &usage_type_map, old_nodes, + pointer_usage: default(), new_nodes: default(), - parent_ast_id: default(), - parent_crumbs: default(), + last_ast_id: default(), + last_crumbs_len: default(), parent_state: default(), extensions: default(), }; @@ -621,6 +625,11 @@ impl TreeModel { let child = builder.child_widget(tree.root_ref(), 0); self.display_object.replace_children(&[child]); self.nodes_map.replace(builder.new_nodes); + let mut ports_map_borrow = self.ports_map.borrow_mut(); + ports_map_borrow.clear(); + ports_map_borrow.extend( + builder.pointer_usage.into_iter().filter_map(|(k, v)| Some((k, v.port_index?))), + ); } /// Convert span tree node to a corresponding widget tree pointer. Every node in the span tree @@ -630,10 +639,10 @@ impl TreeModel { pub fn get_node_widget_pointer( &self, tree_node: &span_tree::node::Ref, - ) -> Option { + ) -> Option { if let Some(id) = tree_node.node.ast_id { // This span represents an AST node, return a pointer directly to it. - Some(WidgetTreePointer { id: Some(id), crumbs: default() }) + Some(MainWidgetPointer::new(Some(id), &[])) } else { let root = tree_node.span_tree.root_ref(); let root_ast_data = root.ast_id.map(|id| (id, 0)); @@ -653,24 +662,25 @@ impl TreeModel { // Parent AST node found, return a pointer relative to it. Some((ast_id, ast_parent_index)) => { let crumb_slice = &tree_node.crumbs[ast_parent_index..]; - let crumbs = span_tree::Crumbs::new(crumb_slice.to_vec()); - Some(WidgetTreePointer { id: Some(ast_id), crumbs }) + Some(MainWidgetPointer::new(Some(ast_id), crumb_slice)) } // No parent AST node found. Return a pointer from root. - None => Some(WidgetTreePointer { id: None, crumbs: tree_node.crumbs.clone() }), + None => Some(MainWidgetPointer::new(None, &tree_node.crumbs)), } } } - /// Perform an operation on a shared reference to a tree node under given pointer. When there is - /// no node under provided pointer, the operation will not be performed and `None` will be + /// Perform an operation on a shared reference to a tree port under given pointer. When there is + /// no port under provided pointer, the operation will not be performed and `None` will be /// returned. - pub fn with_node( + pub fn with_port( &self, - pointer: &WidgetTreePointer, - f: impl FnOnce(&TreeNode) -> T, + pointer: MainWidgetPointer, + f: impl FnOnce(&Port) -> T, ) -> Option { - self.nodes_map.borrow().get(pointer).map(f) + let index = *self.ports_map.borrow().get(&pointer)?; + let unique_ptr = WidgetIdentity { main: pointer, index }; + self.nodes_map.borrow().get(&unique_ptr).and_then(TreeNode::port).map(f) } } @@ -800,12 +810,48 @@ impl<'a, 'b> ConfigContext<'a, 'b> { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct WidgetTreePointer { +/// A pointer to main widget of specific node in the span tree. Determines the base of a widget +/// stable identity, and allows widgets to be reused when rebuilding the tree. The pointer is +/// composed of two parts: +/// - `id` is the AST ID of either the node itself, or the closest ancestor node which has one. It +/// can be `None` if there is no such parent, e.g. for a tree Root node. +/// - `crumbs_hash` is a hash of remaining crumbs since last node with stable AST ID, or since the +/// root node if there is no such parent. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct MainWidgetPointer { /// The latest set ast::Id in the span tree. - id: Option, - /// Remaining crumbs to the widget, starting from the node with the latest set ast::Id. - crumbs: span_tree::Crumbs, + id: Option, + /// A hash of remaining crumbs to the widget, starting from the node with the latest set + /// ast::Id. We store a hash instead of the crumbs directly, so the type can be trivially + /// copied. The collision is extremely unlikely due to commonly having very short lists of + /// crumbs to store here and u64 being comparatively extremely large hash space. + crumbs_hash: u64, +} + +/// An unique identity of a widget in the widget tree. It is a combination of a `WidgetTreePointer` +/// and a sequential index of the widget assigned to the same span tree node. Any widget is allowed +/// to create a child widget on the same span tree node, so we need to be able to distinguish +/// between them. Note that only the first widget created for a given span tree node will be able to +/// receive a port and thus be directly connected. +/// +/// For all widgets with identity that shares the same `WidgetTreePointer`, at most one of them +/// will be able to receive a port. The port is assigned to the first widget created for a given +/// node that wants to receive it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct WidgetIdentity { + /// The pointer to the main widget of this widget's node. + main: MainWidgetPointer, + /// The sequential index of a widget assigned to the same span tree node. + index: usize, +} + +impl MainWidgetPointer { + fn new(id: Option, crumbs: &[span_tree::Crumb]) -> Self { + let mut hasher = DefaultHasher::new(); + crumbs.hash(&mut hasher); + let crumbs_hash = hasher.finish(); + Self { id, crumbs_hash } + } } @@ -826,16 +872,26 @@ struct WidgetTreeBuilder<'a> { node_expression: &'a str, styles: &'a StyleWatch, metadata_map: &'a HashMap, - connected_map: &'a HashMap, + connected_map: &'a HashMap, usage_type_map: &'a HashMap, - old_nodes: HashMap, - new_nodes: HashMap, - parent_ast_id: Option, - parent_crumbs: span_tree::Crumbs, + old_nodes: HashMap, + new_nodes: HashMap, + pointer_usage: HashMap, + last_ast_id: Option, + last_crumbs_len: usize, parent_state: NodeState, extensions: Vec>, } +/// Additional information about the usage of a widget pointer while building a tree. This is used +/// to determine which widget should receive a port, and to assign sequential indices to widgets +/// created for the same span tree node. +#[derive(Debug, Default)] +struct PointerUsage { + next_index: usize, + port_index: Option, +} + impl<'a> WidgetTreeBuilder<'a> { /// Create a new child widget. The widget type will be automatically inferred, either based on /// the node kind, or on the metadata provided from the language server. If possible, an @@ -890,29 +946,29 @@ impl<'a> WidgetTreeBuilder<'a> { // Figure out the widget tree pointer for the current node. That pointer determines the // widget identity, allowing it to maintain internal state. If the previous tree already // contained a widget for this pointer, we have to reuse it. - let tree_ptr = match span_tree_node.ast_id { + let main_ptr = match span_tree_node.ast_id { Some(ast_id) => { - let parent_ast_id = self.parent_ast_id.replace(ast_id); + let last_ast_id = self.last_ast_id.replace(ast_id); let parent_tail_crumbs = - std::mem::replace(&mut self.parent_crumbs, span_tree_node.crumbs.clone()); - ast_data_to_restore = Some((parent_ast_id, parent_tail_crumbs)); - WidgetTreePointer { id: Some(ast_id), crumbs: default() } + std::mem::replace(&mut self.last_crumbs_len, span_tree_node.crumbs.len()); + ast_data_to_restore = Some((last_ast_id, parent_tail_crumbs)); + MainWidgetPointer::new(Some(ast_id), &[]) } None => { let this_crumbs = &span_tree_node.crumbs; - // We should always be in a child node since last ast ID. Verify that. - let is_in_ast_subtree = this_crumbs.starts_with(&self.parent_crumbs); - assert!(is_in_ast_subtree, "Not in AST child node."); - let id = self.parent_ast_id; - let crumbs_since_id = &this_crumbs[self.parent_crumbs.len()..]; - let crumbs = span_tree::Crumbs::new(crumbs_since_id.to_vec()); - WidgetTreePointer { id, crumbs } + let id = self.last_ast_id; + let crumbs_since_id = &this_crumbs[self.last_crumbs_len..]; + MainWidgetPointer::new(id, crumbs_since_id) } }; - let old_node = self.old_nodes.remove(&tree_ptr); + + let ptr_usage = self.pointer_usage.entry(main_ptr).or_default(); + let unique_ptr = WidgetIdentity { main: main_ptr, index: ptr_usage.next_index }; + + let old_node = self.old_nodes.remove(&unique_ptr); let is_placeholder = span_tree_node.is_expected_argument(); let sibling_offset = span_tree_node.sibling_offset.as_usize(); - let usage_type = tree_ptr.id.and_then(|id| self.usage_type_map.get(&id)).cloned(); + let usage_type = main_ptr.id.and_then(|id| self.usage_type_map.get(&id)).cloned(); // Get widget metadata. There are three potential sources for metadata, that are used in // order, whichever is available first: @@ -941,10 +997,17 @@ impl<'a> WidgetTreeBuilder<'a> { }) }); + let widget_has_port = match (meta.has_port, &mut ptr_usage.port_index) { + (true, port_index @ None) => { + *port_index = Some(unique_ptr.index); + true + } + _ => false, + }; // Once we have the metadata and potential old widget to reuse, we have to apply the // configuration to the widget. - let connection: ConnectionStatus = self.connected_map.get(&tree_ptr).copied().into(); + let connection: ConnectionStatus = self.connected_map.get(&main_ptr).copied().into(); let subtree_connection = connection.or(self.parent_state.subtree_connection); let disabled = self.node_disabled; let state = NodeState { depth, connection, subtree_connection, disabled, usage_type }; @@ -967,7 +1030,7 @@ impl<'a> WidgetTreeBuilder<'a> { // state to reflect the correct parent node. We need to restore the state after the // `configure` call has been done, so that the next sibling node will receive correct parent // data. - let child_node = if meta.has_port { + let child_node = if widget_has_port { let mut port = match old_node { Some(TreeNode::Port(port)) => port, Some(TreeNode::Widget(widget)) => Port::new(widget, app, frp), @@ -987,9 +1050,9 @@ impl<'a> WidgetTreeBuilder<'a> { // After visiting child node, restore previous layer's parent data. self.parent_state = state_to_restore; - if let Some((id, crumbs)) = ast_data_to_restore { - self.parent_ast_id = id; - self.parent_crumbs = crumbs; + if let Some((id, crumbs_len)) = ast_data_to_restore { + self.last_ast_id = id; + self.last_crumbs_len = crumbs_len; } self.extensions.truncate(parent_extensions_len); @@ -1002,7 +1065,7 @@ impl<'a> WidgetTreeBuilder<'a> { child_root.set_margin_left(left_margin); } - self.new_nodes.insert(tree_ptr.clone(), child_node); + self.new_nodes.insert(unique_ptr, child_node); child_root } } diff --git a/lib/rust/ensogl/core/src/debug/stats.rs b/lib/rust/ensogl/core/src/debug/stats.rs index 9cc515ce732d..0f2401ff808f 100644 --- a/lib/rust/ensogl/core/src/debug/stats.rs +++ b/lib/rust/ensogl/core/src/debug/stats.rs @@ -143,7 +143,7 @@ impl FramedStatsData { /// Clean the per-frame statistics, such as the per-frame number of draw calls. This function /// should be called before any rendering calls were made. fn reset_per_frame_statistics(&mut self) { - self.stats_data.draw_calls = default(); + self.stats_data.draw_calls.clear(); self.stats_data.shader_compile_count = 0; self.stats_data.data_upload_count = 0; self.stats_data.data_upload_size = 0; diff --git a/lib/rust/frp/src/stream.rs b/lib/rust/frp/src/stream.rs index 06bac023455d..efdedcb9f059 100644 --- a/lib/rust/frp/src/stream.rs +++ b/lib/rust/frp/src/stream.rs @@ -245,7 +245,7 @@ pub struct NodeData { /// is borrowed mutable. You should always borrow it only if `during_call` is false. Otherwise, /// if you want to register new outputs during a call, use `new_targets` field instead. It will /// be merged into `targets` directly after the call. - targets: RefCell>>, + targets: RefCell; 1]>>, new_targets: RefCell>>, value_cache: RefCell, ongoing_evaluations: Cell, @@ -293,12 +293,18 @@ impl EventEmitter for NodeData { if self.use_caching() { *self.value_cache.borrow_mut() = value.clone(); } - if let Ok(mut targets) = self.targets.try_borrow_mut() { - targets.retain(|target| !target.data.is_dropped()); - } + let mut cleanup = false; for target in self.targets.borrow().iter() { - target.data.on_event_if_exists(&new_stack, value); + let exists = target.data.on_event_if_exists(&new_stack, value); + cleanup |= !exists; + } + + if cleanup { + if let Ok(mut targets) = self.targets.try_borrow_mut() { + targets.retain(|target| !target.data.is_dropped()); + } } + let mut new_targets = self.new_targets.borrow_mut(); if !new_targets.is_empty() { if let Ok(mut targets) = self.targets.try_borrow_mut() { From d8082a2b185d1d624928b471aa262d22c0ca1405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Thu, 20 Apr 2023 18:38:05 +0200 Subject: [PATCH 21/45] add widget tree hierarchy and iterators --- .../src/component/node/input/widget.rs | 173 ++++++++++++++---- 1 file changed, 134 insertions(+), 39 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index d752f4977019..d603d620c048 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -428,6 +428,7 @@ impl Tree { self.model.set_disabled(disabled); } + /// Rebuild tree if it has been marked as dirty. The dirty flag is marked whenever more data /// external to the span-tree is provided, using `set_metadata`, `set_usage_type`, /// `set_connected` or `set_disabled` methods of the widget tree. @@ -467,11 +468,13 @@ impl Tree { /// Get hover shapes for all ports in the tree. Used for testing to simulate mouse events. pub fn port_hover_shapes(&self) -> Vec { + let nodes = self.model.nodes_map.borrow(); self.model - .nodes_map + .hierarchy .borrow() - .values() - .filter_map(|n| Some(n.port()?.hover_shape().clone_ref())) + .iter() + .filter_map(|n| nodes.get(&n.identity)) + .filter_map(|e| Some(e.node.port()?.hover_shape().clone_ref())) .collect_vec() } } @@ -510,7 +513,23 @@ impl display::Object for TreeNode { } } +/// Hierarchy structure that can be used to quickly navigate the tree. +#[derive(Debug, Clone, Copy)] +struct NodeHierarchy { + identity: WidgetIdentity, + #[allow(dead_code)] + parent_index: Option, + total_descendants: usize, +} +/// Single entry in the tree. +#[derive(Debug)] +struct TreeEntry { + node: TreeNode, + /// Index in the `hierarchy` vector. + #[allow(dead_code)] + index: usize, +} /// ================= /// === TreeModel === @@ -520,12 +539,17 @@ impl display::Object for TreeNode { struct TreeModel { app: Application, display_object: display::object::Instance, - nodes_map: RefCell>, + /// A map from widget identity to the tree node and its index in the `hierarchy` vector. + nodes_map: RefCell>, + /// Hierarchy data for nodes, stored in node insertion order (effectively depth-first). It can + /// be used to quickly find the parent of a node, or iterate over all children or descendants + /// of a node. + hierarchy: RefCell>, ports_map: RefCell>, metadata_map: Rc>>, connected_map: Rc>>, usage_type_map: Rc>>, - disabled: Cell, + node_disabled: Cell, tree_dirty: Cell, } @@ -544,8 +568,9 @@ impl TreeModel { Self { app, display_object, - disabled: default(), + node_disabled: default(), nodes_map: default(), + hierarchy: default(), ports_map: default(), metadata_map: default(), connected_map: default(), @@ -583,12 +608,47 @@ impl TreeModel { /// Set the connection status under given widget. It may cause the tree to be marked as dirty. fn set_disabled(&self, disabled: bool) { - let prev_disabled = self.disabled.replace(disabled); + let prev_disabled = self.node_disabled.replace(disabled); if prev_disabled != disabled { self.tree_dirty.set(true); } } + /// Get parent of a node under given pointer, if exists. + #[allow(dead_code)] + pub fn parent(&self, pointer: WidgetIdentity) -> Option { + let hierarchy = self.hierarchy.borrow(); + let nodes = self.nodes_map.borrow(); + let index = nodes.get(&pointer).map(|entry| entry.index)?; + let parent_index = hierarchy[index].parent_index?; + Some(hierarchy[parent_index].identity) + } + + /// Iterate children of a node under given pointer, if any exist. + #[allow(dead_code)] + pub fn iter_children( + &self, + pointer: WidgetIdentity, + ) -> impl Iterator + '_ { + let hierarchy = self.hierarchy.borrow(); + let nodes = self.nodes_map.borrow(); + let mut total_range = nodes.get(&pointer).map_or(0..0, |entry| { + let start = entry.index + 1; + let total_descendants = hierarchy[entry.index].total_descendants; + start..start + total_descendants + }); + + std::iter::from_fn(move || { + let index = total_range.next()?; + let entry = hierarchy[index]; + // Skip all descendants of the child. The range is now at the next direct child. + if entry.total_descendants > 0 { + total_range.nth(entry.total_descendants - 1); + } + Some(entry.identity) + }) + } + #[profile(Task)] fn rebuild_tree( &self, @@ -603,7 +663,12 @@ impl TreeModel { let connected_map = self.connected_map.borrow(); let usage_type_map = self.usage_type_map.borrow(); let old_nodes = self.nodes_map.take(); - let node_disabled = self.disabled.get(); + let node_disabled = self.node_disabled.get(); + + // Old hierarchy is not used during the rebuild, so we might as well reuse the allocation. + let mut hierarchy = self.hierarchy.take(); + hierarchy.clear(); + let mut builder = WidgetTreeBuilder { app, frp, @@ -614,17 +679,19 @@ impl TreeModel { connected_map: &connected_map, usage_type_map: &usage_type_map, old_nodes, + hierarchy, pointer_usage: default(), new_nodes: default(), - last_ast_id: default(), - last_crumbs_len: default(), parent_state: default(), + last_ast_depth: default(), extensions: default(), }; let child = builder.child_widget(tree.root_ref(), 0); self.display_object.replace_children(&[child]); + self.nodes_map.replace(builder.new_nodes); + self.hierarchy.replace(builder.hierarchy); let mut ports_map_borrow = self.ports_map.borrow_mut(); ports_map_borrow.clear(); ports_map_borrow.extend( @@ -680,14 +747,18 @@ impl TreeModel { ) -> Option { let index = *self.ports_map.borrow().get(&pointer)?; let unique_ptr = WidgetIdentity { main: pointer, index }; - self.nodes_map.borrow().get(&unique_ptr).and_then(TreeNode::port).map(f) + self.nodes_map.borrow().get(&unique_ptr).and_then(|n| n.node.port()).map(f) } } /// State of a node in the widget tree. Provides additional information about the node's current /// state, such as its depth in the widget tree, if it's connected, disabled, etc. -#[derive(Debug, Default, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub(super) struct NodeState { + /// Identity of this node. + pub identity: WidgetIdentity, + /// Index of node in the widget tree, in insertion order. + pub insertion_index: usize, /// Widget tree node depth, as provided by the parent node. This does not necessarily /// correspond to the depth in the view hierarchy or span tree, but instead is treated as a /// logical nesting level in the expressions. It is fully determined by the chain of parent @@ -818,7 +889,7 @@ impl<'a, 'b> ConfigContext<'a, 'b> { /// - `crumbs_hash` is a hash of remaining crumbs since last node with stable AST ID, or since the /// root node if there is no such parent. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct MainWidgetPointer { +pub struct MainWidgetPointer { /// The latest set ast::Id in the span tree. id: Option, /// A hash of remaining crumbs to the widget, starting from the node with the latest set @@ -828,17 +899,17 @@ struct MainWidgetPointer { crumbs_hash: u64, } -/// An unique identity of a widget in the widget tree. It is a combination of a `WidgetTreePointer` +/// An unique identity of a widget in the widget tree. It is a combination of a `MainWidgetPointer` /// and a sequential index of the widget assigned to the same span tree node. Any widget is allowed /// to create a child widget on the same span tree node, so we need to be able to distinguish /// between them. Note that only the first widget created for a given span tree node will be able to /// receive a port and thus be directly connected. /// -/// For all widgets with identity that shares the same `WidgetTreePointer`, at most one of them +/// For all widgets with identity that shares the same `MainWidgetPointer`, at most one of them /// will be able to receive a port. The port is assigned to the first widget created for a given /// node that wants to receive it. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct WidgetIdentity { +pub struct WidgetIdentity { /// The pointer to the main widget of this widget's node. main: MainWidgetPointer, /// The sequential index of a widget assigned to the same span tree node. @@ -874,12 +945,12 @@ struct WidgetTreeBuilder<'a> { metadata_map: &'a HashMap, connected_map: &'a HashMap, usage_type_map: &'a HashMap, - old_nodes: HashMap, - new_nodes: HashMap, + old_nodes: HashMap, + new_nodes: HashMap, + hierarchy: Vec, pointer_usage: HashMap, - last_ast_id: Option, - last_crumbs_len: usize, - parent_state: NodeState, + parent_state: Option, + last_ast_depth: usize, extensions: Vec>, } @@ -888,7 +959,9 @@ struct WidgetTreeBuilder<'a> { /// created for the same span tree node. #[derive(Debug, Default)] struct PointerUsage { + /// Next sequence index that will be assigned to a widget created for the same span tree node. next_index: usize, + /// The pointer index of a widget on this span tree that received a port, if any exist already. port_index: Option, } @@ -941,31 +1014,27 @@ impl<'a> WidgetTreeBuilder<'a> { ) -> display::object::Instance { // This call can recurse into itself within the widget configuration logic. We need to save // the current layer's state, so it can be restored later after visiting the child node. - let mut ast_data_to_restore = None; + let parent_last_ast_depth = self.last_ast_depth; // Figure out the widget tree pointer for the current node. That pointer determines the // widget identity, allowing it to maintain internal state. If the previous tree already // contained a widget for this pointer, we have to reuse it. let main_ptr = match span_tree_node.ast_id { Some(ast_id) => { - let last_ast_id = self.last_ast_id.replace(ast_id); - let parent_tail_crumbs = - std::mem::replace(&mut self.last_crumbs_len, span_tree_node.crumbs.len()); - ast_data_to_restore = Some((last_ast_id, parent_tail_crumbs)); + self.last_ast_depth = span_tree_node.crumbs.len(); MainWidgetPointer::new(Some(ast_id), &[]) } None => { + let ast_id = self.parent_state.as_ref().and_then(|st| st.identity.main.id); let this_crumbs = &span_tree_node.crumbs; - let id = self.last_ast_id; - let crumbs_since_id = &this_crumbs[self.last_crumbs_len..]; - MainWidgetPointer::new(id, crumbs_since_id) + let crumbs_since_id = &this_crumbs[parent_last_ast_depth..]; + MainWidgetPointer::new(ast_id, crumbs_since_id) } }; let ptr_usage = self.pointer_usage.entry(main_ptr).or_default(); let unique_ptr = WidgetIdentity { main: main_ptr, index: ptr_usage.next_index }; - let old_node = self.old_nodes.remove(&unique_ptr); let is_placeholder = span_tree_node.is_expected_argument(); let sibling_offset = span_tree_node.sibling_offset.as_usize(); let usage_type = main_ptr.id.and_then(|id| self.usage_type_map.get(&id)).cloned(); @@ -1005,15 +1074,39 @@ impl<'a> WidgetTreeBuilder<'a> { _ => false, }; + let self_insertion_index = self.hierarchy.len(); + self.hierarchy.push(NodeHierarchy { + identity: unique_ptr, + parent_index: self.parent_state.as_ref().map(|st| st.insertion_index), + // This will be updated later, after the child widgets are created. + total_descendants: 0, + }); + + let old_node = self.old_nodes.remove(&unique_ptr).map(|e| e.node); + // Once we have the metadata and potential old widget to reuse, we have to apply the // configuration to the widget. let connection: ConnectionStatus = self.connected_map.get(&main_ptr).copied().into(); - let subtree_connection = connection.or(self.parent_state.subtree_connection); + let subtree_connection = match self.parent_state.as_ref().map(|s| s.subtree_connection) { + Some(parent_connection) => connection.or(parent_connection), + None => connection, + }; + let disabled = self.node_disabled; - let state = NodeState { depth, connection, subtree_connection, disabled, usage_type }; - let state_to_restore = std::mem::replace(&mut self.parent_state, state.clone()); + let state = NodeState { + identity: unique_ptr, + insertion_index: self_insertion_index, + depth, + connection, + subtree_connection, + disabled, + usage_type, + }; + + let state_to_restore = std::mem::replace(&mut self.parent_state, Some(state.clone())); let parent_extensions_len = self.extensions.len(); + let ctx = ConfigContext { builder: &mut *self, display: meta.display, @@ -1048,15 +1141,16 @@ impl<'a> WidgetTreeBuilder<'a> { TreeNode::Widget(widget) }; + // Once the node has been configured and all its children have been created, we can update + // the hierarchy data. + self.hierarchy[self_insertion_index].total_descendants = + self.hierarchy.len() - self_insertion_index - 1; + // After visiting child node, restore previous layer's parent data. self.parent_state = state_to_restore; - if let Some((id, crumbs_len)) = ast_data_to_restore { - self.last_ast_id = id; - self.last_crumbs_len = crumbs_len; - } + self.last_ast_depth = parent_last_ast_depth; self.extensions.truncate(parent_extensions_len); - // Apply left margin to the widget, based on its offset relative to the previous sibling. let child_root = child_node.display_object().clone(); let offset = sibling_offset.max(if is_placeholder { 1 } else { 0 }); @@ -1065,7 +1159,8 @@ impl<'a> WidgetTreeBuilder<'a> { child_root.set_margin_left(left_margin); } - self.new_nodes.insert(unique_ptr, child_node); + let entry = TreeEntry { node: child_node, index: self_insertion_index }; + self.new_nodes.insert(unique_ptr, entry); child_root } } From ff01e79a47e94609681576eb2aad9719be5391a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Thu, 20 Apr 2023 19:06:19 +0200 Subject: [PATCH 22/45] handle edit mode transition cursor --- .../src/component/node/input/area.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 237f1eed51b7..cf406d71065a 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -222,7 +222,15 @@ impl Model { self.widget_tree.set_usage_type(id, usage_type); } - fn hover_pointer_style(&self, hovered: &Switch) -> Option { + fn body_hover_pointer_style(&self, hovered: &bool) -> cursor::Style { + if *hovered { + cursor::Style::cursor() + } else { + default() + } + } + + fn port_hover_pointer_style(&self, hovered: &Switch) -> Option { let crumbs = hovered.on()?; let expr = self.expression.borrow(); let port = expr.span_tree.get_node(crumbs).ok()?; @@ -277,10 +285,6 @@ impl Model { &self.styles, ); - // TODO streams to handle: - // pointer_style (cursor::Style), <- handle edit mode cursor change - // view_mode (view::Mode), <- frp into widgets to change label color - self.request_widgets_metadata(&new_expression, area_frp); *self.expression.borrow_mut() = new_expression; } @@ -456,13 +460,15 @@ impl Area { label_hovered <- reacts_to_hover && frp.output.body_hover; model.edit_mode_label.set_hover <+ label_hovered && set_editing; + hovered_body_pointer <- label_hovered.map(f!((t) model.body_hover_pointer_style(t))); // === Port Hover === hovered_port_pointer <- model.widget_tree.on_port_hover.map( - f!((t) model.hover_pointer_style(t).unwrap_or_default()) + f!((t) model.port_hover_pointer_style(t).unwrap_or_default()) ); pointer_style <- all[ + hovered_body_pointer, model.widget_tree.pointer_style, hovered_port_pointer ].fold(); From 149a8c77bf395540327dcc9ef9b0e246204c0c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Thu, 20 Apr 2023 19:30:38 +0200 Subject: [PATCH 23/45] lint and tests --- .../graph-editor/src/component/node/input/widget/label.rs | 4 ++-- .../src/component/node/input/widget/single_choice.rs | 6 +++--- lib/rust/ensogl/core/src/display/object/instance.rs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index bb1cf2f82659..b4138c06ee63 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -148,7 +148,7 @@ pub enum ColorState { impl ColorState { fn to_color( - &self, + self, is_hovered: bool, view_mode: crate::view::Mode, status: crate::node::profiling::Status, @@ -166,7 +166,7 @@ impl ColorState { ColorState::Placeholder => syntax::expected, ColorState::FromType(_) if profiled => syntax::profiling::base, ColorState::FromType(_) if profiling_mode => syntax::base, - ColorState::FromType(typed) => return *typed, + ColorState::FromType(typed) => return typed, }; styles.get_color(color_path).into() diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index fb6f36e9ed27..25b7778606d4 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -340,8 +340,8 @@ impl LazyDropdown { } - dropdown.set_all_entries.emit(self.set_all_entries.value().clone()); - dropdown.set_selected_entries.emit(self.set_selected_entries.value().clone()); - dropdown.set_open.emit(self.set_open.value().clone()); + dropdown.set_all_entries.emit(self.set_all_entries.value()); + dropdown.set_selected_entries.emit(self.set_selected_entries.value()); + dropdown.set_open.emit(self.set_open.value()); } } diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index 44610515e63d..1bd93564755c 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -4229,10 +4229,10 @@ mod hierarchy_tests { assert_eq!(node2.my_index(), Some(ChildIndex(0))); node1.add_child(&node2); - assert_eq!(node2.my_index(), Some(ChildIndex(1))); + assert_eq!(node2.my_index(), Some(ChildIndex(0))); node1.add_child(&node3); - assert_eq!(node3.my_index(), Some(ChildIndex(2))); + assert_eq!(node3.my_index(), Some(ChildIndex(1))); node1.remove_child(&node3); assert_eq!(node3.my_index(), None); From 66dafee53c12504539e9e91f21f3bccaa980c7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Thu, 20 Apr 2023 19:33:25 +0200 Subject: [PATCH 24/45] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a3b3d90e2d0..0589fba546b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -132,6 +132,8 @@ quickly understand each button's function. - [File associations are created on Windows and macOS][6077]. This allows opening Enso files by double-clicking them in the file explorer. +- [Added capability to create node widgets with complex UI][6347]. Node widgets + such as dropdown can now be placed in the node and affect the code text flow. #### EnsoGL (rendering engine) @@ -590,6 +592,7 @@ [6253]: https://github.com/enso-org/enso/pull/6253 [6294]: https://github.com/enso-org/enso/pull/6294 [6383]: https://github.com/enso-org/enso/pull/6383 +[6347]: https://github.com/enso-org/enso/pull/6347 #### Enso Compiler From cc5f970292068ce8c4a46816a02293d659c05c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Thu, 20 Apr 2023 20:08:18 +0200 Subject: [PATCH 25/45] fix graph editor tests --- app/gui/view/graph-editor/src/lib.rs | 7 +++++-- lib/rust/ensogl/core/src/display/object/instance.rs | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index c16a02325c94..433a4d0642bf 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -4055,6 +4055,7 @@ impl Display for ExecutionEnvironment { mod tests { use super::*; use application::test_utils::ApplicationExt; + use ensogl::control::io::mouse; use ensogl::control::io::mouse::PrimaryButton; use ensogl::display::scene::test_utils::MouseExt; use node::test_utils::NodeModelExt; @@ -4161,8 +4162,10 @@ mod tests { // 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_hover = node_2.model().input_port_shape().expect("No input port."); - port_hover.events_deprecated.emit_mouse_down(PrimaryButton); - port_hover.events_deprecated.emit_mouse_up(PrimaryButton); + + // Input ports already use new event API. + port_hover.emit_event(mouse::Down::default()); + port_hover.emit_event(mouse::Up::default()); 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)); } diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index 1bd93564755c..bc685daea726 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -4234,6 +4234,9 @@ mod hierarchy_tests { node1.add_child(&node3); assert_eq!(node3.my_index(), Some(ChildIndex(1))); + node1.add_child(&node2); + assert_eq!(node2.my_index(), Some(ChildIndex(2))); + node1.remove_child(&node3); assert_eq!(node3.my_index(), None); } From 202c62f4c28b0e676357408f0a5b58088de8cb4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Thu, 20 Apr 2023 21:02:54 +0200 Subject: [PATCH 26/45] fix span-tree tests --- app/gui/language/span-tree/src/builder.rs | 27 +++++------------------ 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/app/gui/language/span-tree/src/builder.rs b/app/gui/language/span-tree/src/builder.rs index f7b3a27f43a3..efb8d970c2c4 100644 --- a/app/gui/language/span-tree/src/builder.rs +++ b/app/gui/language/span-tree/src/builder.rs @@ -19,13 +19,10 @@ pub trait Builder: Sized { /// Reference to currently built node. fn node_being_built(&mut self) -> &mut Node; - /// The total length of the tree built so far. - fn current_end_offset(&self) -> usize; - /// Add new AST-type child to node. Returns the child's builder which may be used to further /// extend this branch of the tree. fn add_child( - self, + mut self, offset: usize, len: usize, kind: impl Into, @@ -33,7 +30,9 @@ pub trait Builder: Sized { ) -> ChildBuilder { let kind = kind.into(); let node = Node::::new().with_kind(kind).with_size(len.into()); - let sibling_offset = offset.saturating_sub(self.current_end_offset()); + let prev_child = self.node_being_built().children.last(); + let prev_child_end = prev_child.map_or(0, |c| (c.offset + c.node.size).as_usize()); + let sibling_offset = offset.saturating_sub(prev_child_end); let child = node::Child { node, offset: offset.into(), @@ -55,16 +54,8 @@ pub trait Builder: Sized { } /// Add an Empty-type child to node. - fn add_empty_child(mut self, offset: usize, kind: impl Into) -> Self { - let sibling_offset = offset.saturating_sub(self.current_end_offset()); - let child = node::Child { - node: Node::::new().with_kind(kind), - offset: offset.into(), - sibling_offset: sibling_offset.into(), - ast_crumbs: vec![], - }; - self.node_being_built().children.push(child); - self + fn add_empty_child(self, offset: usize, kind: impl Into) -> Self { + self.add_leaf(offset, 0, kind, ast::crumbs![]) } /// Set expression id for this node. @@ -106,9 +97,6 @@ impl Builder for TreeBuilder { fn node_being_built(&mut self) -> &mut Node { &mut self.built } - fn current_end_offset(&self) -> usize { - self.built.size.as_usize() - } } @@ -133,7 +121,4 @@ impl Builder for ChildBuilder { fn node_being_built(&mut self) -> &mut Node { &mut self.built.node } - fn current_end_offset(&self) -> usize { - self.built.offset.as_usize() + self.built.node.size.as_usize() - } } From 7e5601220482b79a396e7efd2e78255f82b7d8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Fri, 21 Apr 2023 12:14:15 +0200 Subject: [PATCH 27/45] use nested label widget for single-child dropdown, improve hover behavior --- Cargo.lock | 1 - .../src/component/node/input/area.rs | 1 + .../src/component/node/input/port.rs | 13 ++-- .../src/component/node/input/widget.rs | 59 ++++++++++++----- .../src/component/node/input/widget/debug.rs | 56 ---------------- .../component/node/input/widget/hierarchy.rs | 2 + .../src/component/node/input/widget/label.rs | 3 + .../node/input/widget/single_choice.rs | 66 +++++++++++-------- .../node/input/widget/vector_editor.rs | 1 - .../ensogl/app/theme/hardcoded/src/lib.rs | 4 ++ .../ensogl/examples/auto-layout/Cargo.toml | 1 - 11 files changed, 101 insertions(+), 106 deletions(-) delete mode 100644 app/gui/view/graph-editor/src/component/node/input/widget/debug.rs diff --git a/Cargo.lock b/Cargo.lock index f93de70f64d4..8a2cc5970d00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2765,7 +2765,6 @@ version = "0.1.0" dependencies = [ "ensogl-core", "ensogl-hardcoded-theme", - "ensogl-text", "wasm-bindgen", ] diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index cf406d71065a..e95c2f970d36 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -27,6 +27,7 @@ use ensogl_component::text::FromInContextSnapped; use ensogl_hardcoded_theme as theme; + // ================= // === Constants === // ================= diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index 5437e6cef29c..bf7dca5227c8 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -8,6 +8,7 @@ use crate::component::node::input::widget::DynWidget; use crate::component::node::input::widget::SpanWidget; use crate::component::node::input::widget::WidgetsFrp; use crate::component::node::ConnectionStatus; + use enso_frp as frp; use ensogl::application::Application; use ensogl::control::io::mouse; @@ -30,7 +31,7 @@ pub const BASE_PORT_HEIGHT: f32 = 18.0; /// The vertical hover padding of ports at low depth. It affects how the port hover should extend /// the target text boundary on both sides. -pub const PRIMARY_PORT_HOVER_PADDING_Y: f32 = 4.0; +pub const PRIMARY_PORT_HOVER_PADDING_Y: f32 = (crate::node::HEIGHT - BASE_PORT_HEIGHT) / 2.0; /// The maximum depth of the widget port that is still considered primary. This is used to determine /// the hover area of the port. @@ -58,6 +59,7 @@ pub const PRIMARY_PORT_MAX_DEPTH: usize = 0; pub mod shape { use super::*; ensogl::shape! { + below = [ensogl::gui::cursor::shape]; pointer_events = false; (style:Style, color:Vector4) { let size = Var::canvas_size(); @@ -92,7 +94,7 @@ struct HoverLayers { impl display::scene::Extension for HoverLayers { fn init(scene: &display::Scene) -> Self { - let hover_layer = scene.layers.main.clone_ref(); + let hover_layer = scene.layers.label.clone_ref(); Self { hover_layer, hover_partitions: default() } } } @@ -250,9 +252,10 @@ impl Port { let is_primary = ctx.state.depth <= PRIMARY_PORT_MAX_DEPTH; if self.current_primary != is_primary { self.current_primary = is_primary; - let margin = if is_primary { -PRIMARY_PORT_HOVER_PADDING_Y } else { 0.0 }; - self.hover_shape.set_margin_top(margin); - self.hover_shape.set_margin_bottom(margin); + let margin = if is_primary { PRIMARY_PORT_HOVER_PADDING_Y } else { 0.0 }; + self.hover_shape.set_size_y(BASE_PORT_HEIGHT + 2.0 * margin); + self.hover_shape.set_margin_top(-margin); + self.hover_shape.set_margin_bottom(-margin); } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index d603d620c048..d56a34139964 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -7,6 +7,7 @@ use crate::component::node::input::area::TEXT_OFFSET; use crate::component::node::input::port::Port; use crate::component::node::ConnectionData; use crate::component::node::ConnectionStatus; + use enso_config::ARGS; use enso_frp as frp; use enso_text as text; @@ -899,6 +900,20 @@ pub struct MainWidgetPointer { crumbs_hash: u64, } +impl MainWidgetPointer { + fn new(id: Option, crumbs: &[span_tree::Crumb]) -> Self { + let mut hasher = DefaultHasher::new(); + crumbs.hash(&mut hasher); + let crumbs_hash = hasher.finish(); + Self { id, crumbs_hash } + } + + /// Convert this pointer to a stable identity of a widget, making it unique among all widgets. + fn to_identity(self, usage: &mut PointerUsage) -> WidgetIdentity { + WidgetIdentity { main: self, index: usage.next_index() } + } +} + /// An unique identity of a widget in the widget tree. It is a combination of a `MainWidgetPointer` /// and a sequential index of the widget assigned to the same span tree node. Any widget is allowed /// to create a child widget on the same span tree node, so we need to be able to distinguish @@ -916,17 +931,14 @@ pub struct WidgetIdentity { index: usize, } -impl MainWidgetPointer { - fn new(id: Option, crumbs: &[span_tree::Crumb]) -> Self { - let mut hasher = DefaultHasher::new(); - crumbs.hash(&mut hasher); - let crumbs_hash = hasher.finish(); - Self { id, crumbs_hash } +impl WidgetIdentity { + /// Whether this widget pointer represents first created widget for its node. + fn is_first(&self) -> bool { + self.index == 0 } } - /// ========================= /// === WidgetTreeBuilder === /// ========================= @@ -965,6 +977,22 @@ struct PointerUsage { port_index: Option, } +impl PointerUsage { + fn next_index(&mut self) -> usize { + self.next_index += 1; + self.next_index - 1 + } + + fn request_port(&mut self, identity: &WidgetIdentity, wants_port: bool) -> bool { + if wants_port && self.port_index.is_none() { + self.port_index = Some(identity.index); + true + } else { + false + } + } +} + impl<'a> WidgetTreeBuilder<'a> { /// Create a new child widget. The widget type will be automatically inferred, either based on /// the node kind, or on the metadata provided from the language server. If possible, an @@ -1033,7 +1061,7 @@ impl<'a> WidgetTreeBuilder<'a> { }; let ptr_usage = self.pointer_usage.entry(main_ptr).or_default(); - let unique_ptr = WidgetIdentity { main: main_ptr, index: ptr_usage.next_index }; + let unique_ptr = main_ptr.to_identity(ptr_usage); let is_placeholder = span_tree_node.is_expected_argument(); let sibling_offset = span_tree_node.sibling_offset.as_usize(); @@ -1066,13 +1094,7 @@ impl<'a> WidgetTreeBuilder<'a> { }) }); - let widget_has_port = match (meta.has_port, &mut ptr_usage.port_index) { - (true, port_index @ None) => { - *port_index = Some(unique_ptr.index); - true - } - _ => false, - }; + let widget_has_port = ptr_usage.request_port(&unique_ptr, meta.has_port); let self_insertion_index = self.hierarchy.len(); self.hierarchy.push(NodeHierarchy { @@ -1153,7 +1175,12 @@ impl<'a> WidgetTreeBuilder<'a> { // Apply left margin to the widget, based on its offset relative to the previous sibling. let child_root = child_node.display_object().clone(); - let offset = sibling_offset.max(if is_placeholder { 1 } else { 0 }); + let offset = match () { + _ if !unique_ptr.is_first() => 0, + _ if is_placeholder => 1, + _ => sibling_offset, + }; + let left_margin = offset as f32 * WIDGET_SPACING_PER_OFFSET; if child_root.margin().x.start.as_pixels().map_or(true, |px| px != left_margin) { child_root.set_margin_left(left_margin); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs b/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs deleted file mode 100644 index 5ffd7eeaaefc..000000000000 --- a/app/gui/view/graph-editor/src/component/node/input/widget/debug.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::prelude::*; -use ensogl::data::color; -use ensogl::display; - -const DEBUG_BORDER_ENABLED: bool = false; - -mod shape { - ensogl::shape! { - above = [ - crate::component::node::background, - crate::component::node::drag_area - ]; - pointer_events = false; - (style: Style, color: Vector4) { - let color = Var::::from(color); - let shape = Rect(Var::canvas_size()).fill(color); - let inner = shape.shrink(2.0.px()); - let border = shape - inner; - border.into() - } - } -} - -#[derive(Clone, Debug)] -pub struct InstanceWithBg { - pub bg: shape::View, - pub outer: display::object::Instance, - pub inner: display::object::Instance, -} - -impl InstanceWithBg { - pub fn magenta() -> Self { - Self::with_color(color::Rgba::new(0.5, 0.0, 0.5, 0.15)) - } - - pub fn olive() -> Self { - Self::with_color(color::Rgba::new(0.5, 0.8, 0.0, 0.2)) - } - - pub fn gray() -> Self { - Self::with_color(color::Rgba::new(0.5, 0.5, 0.5, 0.2)) - } - - pub fn with_color(color: color::Rgba) -> Self { - let bg = shape::View::new(); - let inner = display::object::Instance::new(); - let outer = if DEBUG_BORDER_ENABLED { - bg.color.set(color.into()); - bg.display_object().clone() - } else { - display::object::Instance::new() - }; - outer.add_child(&inner); - Self { bg, outer, inner } - } -} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs index 536568569247..c4f64c7654dc 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -5,6 +5,8 @@ use crate::prelude::*; use ensogl::display::object; + + // ================= // === Hierarchy === // ================ diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index b4138c06ee63..f12583184061 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -3,12 +3,15 @@ use crate::prelude::*; use crate::component::node::input::area::TEXT_SIZE; + use ensogl::data::color; use ensogl::display::object; use ensogl::display::shape::StyleWatch; use ensogl_component::text; use ensogl_hardcoded_theme as theme; + + // ============= // === Label === // ============= diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 25b7778606d4..bf89332e2d25 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -3,12 +3,14 @@ use crate::prelude::*; use crate::component::node::input::widget::Entry; + use enso_frp as frp; use ensogl::control::io::mouse; use ensogl::data::color; use ensogl::display; use ensogl::display::object::event; use ensogl_component::drop_down::Dropdown; +use ensogl_hardcoded_theme as theme; @@ -16,12 +18,12 @@ use ensogl_component::drop_down::Dropdown; /// === Constants === /// ================= -/// Color of the activation triangle shape. -pub const ACTIVATION_SHAPE_COLOR: color::Lch = color::Lch::new(0.56708, 0.23249, 0.71372); - /// Height of the activation triangle shape. pub const ACTIVATION_SHAPE_SIZE: Vector2 = Vector2(15.0, 11.0); +/// Gap between activation shape and the dropdown widget content. +pub const ACTIVATION_SHAPE_GAP: f32 = 5.0; + /// Distance between the top of the dropdown list and the bottom of the widget. const DROPDOWN_Y_OFFSET: f32 = -20.0; @@ -37,11 +39,7 @@ const DROPDOWN_MAX_SIZE: Vector2 = Vector2(300.0, 500.0); pub mod triangle { use super::*; ensogl::shape! { - above = [ - crate::component::node::background, - crate::component::node::drag_area, - crate::component::node::input::port::shape - ]; + below = [crate::component::node::input::port::hover_shape]; alignment = left_bottom; (style:Style, color:Vector4) { let size = Var::canvas_size(); @@ -74,9 +72,10 @@ pub struct Config { ensogl::define_endpoints_2! { Input { - set_entries(Rc>), - current_value(Option), - current_crumbs(span_tree::Crumbs), + set_entries (Rc>), + current_value (Option), + current_crumbs (span_tree::Crumbs), + is_connected (bool), } } @@ -91,7 +90,6 @@ pub struct Widget { dropdown_wrapper: display::object::Instance, label_wrapper: display::object::Instance, dropdown: Rc>, - label: super::label::Widget, activation_shape: triangle::View, } @@ -116,18 +114,18 @@ impl super::SpanWidget for Widget { // │ ◎ dropdown_wrapper size=0 │ // ╰───────────────────────────────────╯ - let dot_color = color::Rgba::from(ACTIVATION_SHAPE_COLOR.with_alpha(1.0)).into(); let activation_shape = triangle::View::new(); - activation_shape.color.set(dot_color); activation_shape.set_size(ACTIVATION_SHAPE_SIZE); + let layers = &ctx.app().display.default_scene.layers; + layers.label.add(&activation_shape); + let display_object = display::object::Instance::new(); let content_wrapper = display_object.new_child(); content_wrapper.add_child(&activation_shape); let label_wrapper = content_wrapper.new_child(); let dropdown_wrapper = display_object.new_child(); - display_object .use_auto_layout() .set_column_flow() @@ -136,6 +134,7 @@ impl super::SpanWidget for Widget { content_wrapper .use_auto_layout() + .set_gap_x(ACTIVATION_SHAPE_GAP) .set_children_alignment_left_center() .justify_content_center_y(); @@ -146,16 +145,14 @@ impl super::SpanWidget for Widget { dropdown_wrapper.set_size((0.0, 0.0)).set_alignment_left_top(); - let label = super::label::Widget::new(&default(), ctx); - let config_frp = Frp::new(); let network = &config_frp.network; let input = &config_frp.private.input; + let styles = ctx.styles(); let focus_receiver = display_object.clone_ref(); frp::extend! { network - init <- source::<()>(); initialize_dropdown <- any_(...); let focus_in = focus_receiver.on_event::(); @@ -163,6 +160,26 @@ impl super::SpanWidget for Widget { initialize_dropdown <+ focus_in; is_open <- bool(&focus_out, &focus_in); + is_hovered <- widgets_frp.on_port_hover.map2(&input.current_crumbs, |h, crumbs| { + h.on().map_or(false, |h| crumbs.starts_with(h)) + }); + is_connected_or_hovered <- input.is_connected || is_hovered; + activation_shape_theme <- is_connected_or_hovered.map(|is_connected_or_hovered| { + if *is_connected_or_hovered { + Some(theme::widget::activation_shape::connected) + } else { + Some(theme::widget::activation_shape::base) + } + }); + activation_shape_theme <- activation_shape_theme.on_change(); + eval activation_shape_theme([styles, activation_shape](path) { + if let Some(path) = path { + let color = styles.get_color(path); + let rgba = color::Rgba::from(color); + activation_shape.color.set(rgba.into()); + } + }); + let dot_mouse_down = activation_shape.on_event::(); dot_clicked <- dot_mouse_down.filter(mouse::is_primary); set_focused <- dot_clicked.map(f!([focus_receiver](_) !focus_receiver.is_focused())); @@ -231,8 +248,6 @@ impl super::SpanWidget for Widget { }); } - init.emit(()); - Self { config_frp, display_object, @@ -241,7 +256,6 @@ impl super::SpanWidget for Widget { label_wrapper, dropdown, activation_shape, - label, } } @@ -255,18 +269,18 @@ impl super::SpanWidget for Widget { input.current_crumbs(ctx.span_tree_node.crumbs.clone()); input.current_value(current_value); input.set_entries(config.entries.clone()); + input.is_connected(ctx.state.subtree_connection.is_connected()); if has_value { ctx.modify_extension::(|ext| ext.bold = true); } if ctx.span_tree_node.children.is_empty() { - self.label.configure(&default(), ctx); - self.label_wrapper.replace_children(&[self.label.root_object()]); + let label_meta = super::Metadata::always(super::label::Config); + let child = + ctx.builder.child_widget_of_type(ctx.span_tree_node, ctx.state.depth, label_meta); + self.label_wrapper.replace_children(&[child]); } else { - // let mut chain = ctx.span_tree_node.clone().chain_children_iter(); - // Do not increment the depth. If the dropdown is displayed, it should also display - // its arguments. let children = ctx .span_tree_node .children_iter() diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs b/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs index d1e0bec374be..39ff4e518070 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs @@ -7,7 +7,6 @@ use crate::prelude::*; use crate::component::node::input::widget::single_choice::triangle; -use crate::component::node::input::widget::single_choice::ACTIVATION_SHAPE_COLOR; use crate::component::node::input::widget::single_choice::ACTIVATION_SHAPE_SIZE; use crate::component::node::input::widget::Metadata; diff --git a/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs b/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs index d06e694346ee..de07601ced70 100644 --- a/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs +++ b/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs @@ -664,6 +664,10 @@ define_themes! { [light:0, dark:1] } } widget { + activation_shape { + base = Lcha(0.56708, 0.23249, 0.71372, 1.0), Lcha(0.56708, 0.23249, 0.71372, 1.0); + connected = graph_editor::node::background , graph_editor::node::background; + } list_view { background = graph_editor::node::background , graph_editor::node::background; highlight = Rgba(0.906,0.914,0.922,1.0) , Lcha(1.0,0.0,0.0,0.15); // rgb(231,233,235) diff --git a/lib/rust/ensogl/examples/auto-layout/Cargo.toml b/lib/rust/ensogl/examples/auto-layout/Cargo.toml index d21e4ad2bb17..fc9a62c17de5 100644 --- a/lib/rust/ensogl/examples/auto-layout/Cargo.toml +++ b/lib/rust/ensogl/examples/auto-layout/Cargo.toml @@ -9,7 +9,6 @@ crate-type = ["cdylib", "rlib"] [dependencies] ensogl-core = { path = "../../core" } -ensogl-text = { path = "../../component/text" } wasm-bindgen = { workspace = true } ensogl-hardcoded-theme = { path = "../../../ensogl/app/theme/hardcoded" } From adfcbdc846126f2224320bfe249ba74537514ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Fri, 21 Apr 2023 12:51:27 +0200 Subject: [PATCH 28/45] store connection data using crumbs --- .../src/component/node/input/area.rs | 4 +- .../src/component/node/input/widget.rs | 108 +++++++++--------- 2 files changed, 55 insertions(+), 57 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index e95c2f970d36..a116c9f38dfb 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -213,9 +213,7 @@ impl Model { /// Set connection status of the given port. fn set_connected(&self, crumbs: &Crumbs, status: node::ConnectionStatus) { - let expr = self.expression.borrow(); - let Ok(port) = expr.span_tree.get_node(crumbs) else { return }; - self.widget_tree.set_connected(&port, status); + self.widget_tree.set_connected(crumbs, status); } /// Set usage type of the given port. diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index d56a34139964..8b5967c79e8a 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -408,19 +408,17 @@ impl Tree { self.model.set_metadata(pointer, meta); } - /// Set usage type for given span tree node. The usage type is used to determine the widget - /// appearance and default inferred widget metadata. + /// Set usage type for given AST node. The usage type is used to determine the widget appearance + /// and default inferred widget metadata. pub fn set_usage_type(&self, ast_id: ast::Id, usage_type: Option) { self.model.set_usage_type(ast_id, usage_type); } - /// Set connection status for given span tree node. The connected nodes will be highlighted - /// with a different color, and the widgets might change behavior depending on the connection + /// Set connection status for given span crumbs. The connected nodes will be highlighted with a + /// different color, and the widgets might change behavior depending on the connection /// status. - pub fn set_connected(&self, tree_node: &span_tree::node::Ref, status: ConnectionStatus) { - if let Some(pointer) = self.model.get_node_widget_pointer(tree_node) { - self.model.set_connected(pointer, status); - } + pub fn set_connected(&self, crumbs: &span_tree::Crumbs, status: ConnectionStatus) { + self.model.set_connected(crumbs, status); } /// Set disabled status for given span tree node. The disabled nodes will be grayed out. @@ -463,7 +461,7 @@ impl Tree { &self, tree_node: &span_tree::node::Ref, ) -> Option { - let pointer = self.model.get_node_widget_pointer(tree_node)?; + let pointer = self.model.get_node_widget_pointer(tree_node); self.model.with_port(pointer, |w| w.display_object().clone()) } @@ -548,7 +546,7 @@ struct TreeModel { hierarchy: RefCell>, ports_map: RefCell>, metadata_map: Rc>>, - connected_map: Rc>>, + connected_map: Rc>>, usage_type_map: Rc>>, node_disabled: Cell, tree_dirty: Cell, @@ -590,9 +588,9 @@ impl TreeModel { } /// Set the connection status under given widget. It may cause the tree to be marked as dirty. - fn set_connected(&self, pointer: MainWidgetPointer, status: ConnectionStatus) { + fn set_connected(&self, crumbs: &span_tree::Crumbs, status: ConnectionStatus) { let mut map = self.connected_map.borrow_mut(); - let dirty = map.synchronize_entry(pointer, status.data()); + let dirty = map.synchronize_entry(crumbs.clone(), status.data()); if dirty { self.tree_dirty.set(true); } @@ -704,13 +702,10 @@ impl TreeModel { /// has a unique representation in the form of a widget tree pointer, which is more stable /// across changes in the span tree than [`span_tree::Crumbs`]. The pointer is used to identify /// the widgets or ports in the widget tree. - pub fn get_node_widget_pointer( - &self, - tree_node: &span_tree::node::Ref, - ) -> Option { + pub fn get_node_widget_pointer(&self, tree_node: &span_tree::node::Ref) -> MainWidgetPointer { if let Some(id) = tree_node.node.ast_id { // This span represents an AST node, return a pointer directly to it. - Some(MainWidgetPointer::new(Some(id), &[])) + MainWidgetPointer::new(Some(id), &[]) } else { let root = tree_node.span_tree.root_ref(); let root_ast_data = root.ast_id.map(|id| (id, 0)); @@ -730,10 +725,10 @@ impl TreeModel { // Parent AST node found, return a pointer relative to it. Some((ast_id, ast_parent_index)) => { let crumb_slice = &tree_node.crumbs[ast_parent_index..]; - Some(MainWidgetPointer::new(Some(ast_id), crumb_slice)) + MainWidgetPointer::new(Some(ast_id), crumb_slice) } // No parent AST node found. Return a pointer from root. - None => Some(MainWidgetPointer::new(None, &tree_node.crumbs)), + None => MainWidgetPointer::new(None, &tree_node.crumbs), } } } @@ -882,6 +877,12 @@ impl<'a, 'b> ConfigContext<'a, 'b> { } } + + +/// ========================================== +/// === MainWidgetPointer / WidgetIdentity === +/// ========================================== + /// A pointer to main widget of specific node in the span tree. Determines the base of a widget /// stable identity, and allows widgets to be reused when rebuilding the tree. The pointer is /// composed of two parts: @@ -917,12 +918,9 @@ impl MainWidgetPointer { /// An unique identity of a widget in the widget tree. It is a combination of a `MainWidgetPointer` /// and a sequential index of the widget assigned to the same span tree node. Any widget is allowed /// to create a child widget on the same span tree node, so we need to be able to distinguish -/// between them. Note that only the first widget created for a given span tree node will be able to -/// receive a port and thus be directly connected. -/// -/// For all widgets with identity that shares the same `MainWidgetPointer`, at most one of them -/// will be able to receive a port. The port is assigned to the first widget created for a given -/// node that wants to receive it. +/// between them. Note that only one widget created for a given span tree node will be able to +/// receive a port. The port is assigned to the first widget created for a given node that wants to +/// receive it. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct WidgetIdentity { /// The pointer to the main widget of this widget's node. @@ -938,34 +936,6 @@ impl WidgetIdentity { } } - -/// ========================= -/// === WidgetTreeBuilder === -/// ========================= - -/// A builder for the widget tree. Maintains transient state necessary during the tree construction, -/// and provides methods for creating child nodes of the tree. Maintains a map of all widgets -/// created so far, and is able to reuse existing widgets under the same location in the tree, only -/// updating their configuration as necessary. -#[derive(Debug)] -struct WidgetTreeBuilder<'a> { - app: Application, - frp: WidgetsFrp, - node_disabled: bool, - node_expression: &'a str, - styles: &'a StyleWatch, - metadata_map: &'a HashMap, - connected_map: &'a HashMap, - usage_type_map: &'a HashMap, - old_nodes: HashMap, - new_nodes: HashMap, - hierarchy: Vec, - pointer_usage: HashMap, - parent_state: Option, - last_ast_depth: usize, - extensions: Vec>, -} - /// Additional information about the usage of a widget pointer while building a tree. This is used /// to determine which widget should receive a port, and to assign sequential indices to widgets /// created for the same span tree node. @@ -993,6 +963,35 @@ impl PointerUsage { } } + + +/// ========================= +/// === WidgetTreeBuilder === +/// ========================= + +/// A builder for the widget tree. Maintains transient state necessary during the tree construction, +/// and provides methods for creating child nodes of the tree. Maintains a map of all widgets +/// created so far, and is able to reuse existing widgets under the same location in the tree, only +/// updating their configuration as necessary. +#[derive(Debug)] +struct WidgetTreeBuilder<'a> { + app: Application, + frp: WidgetsFrp, + node_disabled: bool, + node_expression: &'a str, + styles: &'a StyleWatch, + metadata_map: &'a HashMap, + connected_map: &'a HashMap, + usage_type_map: &'a HashMap, + old_nodes: HashMap, + new_nodes: HashMap, + hierarchy: Vec, + pointer_usage: HashMap, + parent_state: Option, + last_ast_depth: usize, + extensions: Vec>, +} + impl<'a> WidgetTreeBuilder<'a> { /// Create a new child widget. The widget type will be automatically inferred, either based on /// the node kind, or on the metadata provided from the language server. If possible, an @@ -1108,7 +1107,8 @@ impl<'a> WidgetTreeBuilder<'a> { // Once we have the metadata and potential old widget to reuse, we have to apply the // configuration to the widget. - let connection: ConnectionStatus = self.connected_map.get(&main_ptr).copied().into(); + let connection: ConnectionStatus = + self.connected_map.get(&span_tree_node.crumbs).copied().into(); let subtree_connection = match self.parent_state.as_ref().map(|s| s.subtree_connection) { Some(parent_connection) => connection.or(parent_connection), None => connection, From 46fb9f44186a81f5e17c52a7675d6304aada1def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Fri, 21 Apr 2023 13:08:40 +0200 Subject: [PATCH 29/45] fix port memory leak --- .../graph-editor/src/component/node/input/port.rs | 14 +++++++++----- .../ensogl/core/src/display/object/instance.rs | 13 +++---------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index bf7dca5227c8..585a0c9691e2 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -168,7 +168,9 @@ impl Port { port_root.add_child(&hover_shape); } + let port_root_weak = port_root.downgrade(); let network = &port_root.network; + frp::extend! { network on_cleanup <- on_drop(); hovering <- bool(&mouse_leave, &mouse_enter); @@ -181,11 +183,13 @@ impl Port { ); frp.on_port_press <+ mouse_down.map(f!((_) crumbs.borrow().clone())); - eval frp.set_ports_visible([port_root, hover_shape] (active) { - if *active { - port_root.add_child(&hover_shape); - } else { - port_root.remove_child(&hover_shape); + eval frp.set_ports_visible([port_root_weak, hover_shape] (active) { + if let Some(port_root) = port_root_weak.upgrade() { + if *active { + port_root.add_child(&hover_shape); + } else { + port_root.remove_child(&hover_shape); + } } }); diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index bc685daea726..3a9524b85aaa 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -1144,10 +1144,7 @@ pub struct ChildIndex(usize); // ============= /// The main display object structure. Read the docs of [this module](self) to learn more. -#[derive(Derivative)] -#[derive(CloneRef, Deref, From)] -#[derivative(Clone(bound = ""))] -#[derivative(Default(bound = ""))] +#[derive(Clone, CloneRef, Default, Deref, From)] #[repr(transparent)] pub struct Instance { def: InstanceDef, @@ -1163,9 +1160,7 @@ pub struct Instance { /// not caught by rustc yet: https://github.com/rust-lang/rust/issues/57965). This struct allows the /// implementation to be written as [`self.display_object().def.add_child(child)`] instead, which /// will fail to compile after renaming the function in [`InstanceDef`]. -#[derive(Derivative)] -#[derive(CloneRef, Deref)] -#[derivative(Clone(bound = ""))] +#[derive(Clone, CloneRef, Deref)] #[repr(transparent)] pub struct InstanceDef { rc: Rc, @@ -1284,9 +1279,7 @@ impl Display for InstanceDef { // ==================== /// Weak display object instance. -#[derive(Derivative)] -#[derivative(Clone(bound = ""))] -#[derivative(Debug(bound = ""))] +#[derive(Debug, Clone, CloneRef)] pub struct WeakInstance { weak: Weak, } From c99c4dc83d4d5a23929589278a3142a348f95967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Fri, 21 Apr 2023 19:17:31 +0200 Subject: [PATCH 30/45] address part of review comments --- .../src/controller/graph/widget/metadata.rs | 49 +-- .../src/controller/graph/widget/response.rs | 76 ++-- .../view/graph-editor/src/component/node.rs | 68 +--- .../src/component/node/input/area.rs | 44 +- .../src/component/node/input/port.rs | 39 +- .../src/component/node/input/widget.rs | 382 ++++++++++-------- .../component/node/input/widget/hierarchy.rs | 7 +- .../src/component/node/input/widget/label.rs | 16 +- .../node/input/widget/single_choice.rs | 21 +- app/gui/view/graph-editor/src/lib.rs | 25 +- .../core/src/display/object/instance.rs | 9 +- lib/rust/ensogl/core/src/display/scene.rs | 8 +- 12 files changed, 363 insertions(+), 381 deletions(-) diff --git a/app/gui/src/controller/graph/widget/metadata.rs b/app/gui/src/controller/graph/widget/metadata.rs index 6f50745e8df8..ef7b263e11fb 100644 --- a/app/gui/src/controller/graph/widget/metadata.rs +++ b/app/gui/src/controller/graph/widget/metadata.rs @@ -10,20 +10,22 @@ use ide_view::graph_editor::component::node::input::widget; use ide_view::graph_editor::WidgetUpdate; - +/// Deserialize a list of widget configurations from visualization update data. Allows for partial +/// deserialization: if any of the widget configurations fails to deserialize, it will be skipped, +/// but the deserialization will continue. All errors are returned as a separate list. pub fn deserialize_widget_update( data: &VisualizationUpdateData, ) -> (Vec, Vec) { - match serde_json::from_slice::(data) { + match serde_json::from_slice::(data) { Ok(response) => { let updates = response.into_iter().map( |(argument_name, fallable_widget)| -> FallibleResult { - let widget: Option = fallable_widget.0.map_err(|e| { - e.context(format!( - "Failed to deserialize widget data for argument '{argument_name}'" - )) - })?; - let meta = widget.map(map_metadata); + let widget: Option = + fallable_widget.widget.map_err(|e| { + let msg = "Failed to deserialize widget data for argument"; + e.context(format!("{msg} '{argument_name}'")) + })?; + let meta = widget.map(to_metadata); let argument_name = argument_name.to_owned(); Ok(WidgetUpdate { argument_name, meta }) }, @@ -32,27 +34,28 @@ pub fn deserialize_widget_update( updates.partition_result() } Err(err) => { - let err = - err.context("Failed to deserialize a list of arguments in widget response").into(); + let msg = "Failed to deserialize a list of arguments in widget response"; + let err = err.context(msg).into(); (default(), vec![err]) } } } -fn map_metadata(resp: response::Widget) -> widget::Metadata { - widget::Metadata { display: resp.display, config: map_config(resp.inner), has_port: true } +fn to_metadata(resp: response::WidgetDefinition) -> widget::Metadata { + widget::Metadata { display: resp.display, config: to_config(resp.inner), has_port: true } } -fn map_config(inner: response::WidgetSpecific) -> widget::Config { +fn to_config(inner: response::WidgetKindConfiguration) -> widget::Config { match inner { - response::WidgetSpecific::SingleChoice { label, values } => widget::single_choice::Config { - label: label.map(Into::into), - entries: Rc::new(map_entries(&values)), - } - .into(), - response::WidgetSpecific::VectorEditor { item_editor, item_default } => + response::WidgetKindConfiguration::SingleChoice { label, values } => + widget::single_choice::Config { + label: label.map(Into::into), + entries: Rc::new(to_entries(&values)), + } + .into(), + response::WidgetKindConfiguration::ListEditor { item_editor, item_default } => widget::vector_editor::Config { - item_editor: Some(Rc::new(map_metadata(*item_editor))), + item_editor: Some(Rc::new(to_metadata(*item_editor))), item_default: item_default.into(), } .into(), @@ -60,11 +63,11 @@ fn map_config(inner: response::WidgetSpecific) -> widget::Config { } } -fn map_entries(choices: &[response::Choice]) -> Vec { - choices.iter().map(map_entry).collect() +fn to_entries(choices: &[response::Choice]) -> Vec { + choices.iter().map(to_entry).collect() } -fn map_entry(choice: &response::Choice) -> widget::Entry { +fn to_entry(choice: &response::Choice) -> widget::Entry { let value: ImString = (&choice.value).into(); let label = choice.label.as_ref().map_or_else(|| value.clone(), |label| label.into()); widget::Entry { required_import: None, value, label } diff --git a/app/gui/src/controller/graph/widget/response.rs b/app/gui/src/controller/graph/widget/response.rs index 58dc0a587ec1..9ac264f8499f 100644 --- a/app/gui/src/controller/graph/widget/response.rs +++ b/app/gui/src/controller/graph/widget/response.rs @@ -11,54 +11,59 @@ use ide_view::graph_editor::component::node::input::widget; /// === WidgetUpdates === /// ===================== -/// A complete object received from the widget visualization. Contain widget definitions for all -/// requested arguments. -/// -/// Pairs of argument name and associated widget configuration. -pub(super) type WidgetUpdates<'a> = Vec<(&'a str, FallableWidgetData<'a>)>; - -/// A wrapper type that allows deserialization of a widget to partially fail. Failure message of -/// individual widget data deserialization will be preserved, and it will be allowed to continue -/// deserializing other widgets. +/// A top level object received from the widget visualization, which contains widget definitions for +/// all arguments of a single Enso method. Configurations are paired with the name of function +/// argument they are associated with. +pub(super) type WidgetDefinitions<'a> = Vec<(&'a str, FallableWidgetData<'a>)>; + +/// A wrapper type that allows deserialization of a widget definitions to partially fail: failures +/// message of individual widget definition deserialization will be preserved, and deserialization +/// will continue. #[derive(Debug)] -pub(super) struct FallableWidgetData<'a>(pub(super) FallibleResult>>); +pub(super) struct FallableWidgetData<'a> { + pub(super) widget: FallibleResult>>, +} impl<'de: 'a, 'a> serde::Deserialize<'de> for FallableWidgetData<'a> { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de> { - let widget = >::deserialize(deserializer) + let widget = >::deserialize(deserializer) .map_err(|e| failure::err_msg(e.to_string())); - Ok(Self(widget)) + Ok(Self { widget }) } } + /// ============== /// === Widget === /// ============== -/// Describes the widget configuration, as provided by the node metadata from the engine. +/// Widget definition provided from the engine. It is used to define how to display a widget of +/// particular argument expression. When not provided, the default widget will be chosen based on +/// value or expected argument type. /// -/// Must be kept in sync with `Widget` type definition in `Standard.Base.Metadata` module. +/// Must be kept in sync with `Widget` type definition in Enso's `Standard.Base.Metadata` module. /// In order to not ruin forward compatibility, only fields that are currently used by the IDE are /// specified and deserialized. #[derive(Debug, serde::Deserialize)] -pub(super) struct Widget<'a> { - /// The display mode for the parameter. +pub(super) struct WidgetDefinition<'a> { + /// The display mode of this widget. #[serde(default)] pub display: widget::Display, #[serde(borrow, flatten)] - pub inner: WidgetSpecific<'a>, + pub inner: WidgetKindConfiguration<'a>, } -/// Widget type dependant fields. +/// Part of [`WidgetConfiguration`] that is dependant on widget kind. #[derive(Debug, serde::Deserialize)] #[serde(tag = "constructor")] -pub(super) enum WidgetSpecific<'a> { - /// Describes a single value widget (dropdown). +pub(super) enum WidgetKindConfiguration<'a> { + /// A single value widget (dropdown). #[serde(rename = "Single_Choice")] SingleChoice { - /// The placeholder text value. By default, the parameter name is used. + /// The text that is displayed when no value is chosen. By default, the parameter name is + /// used. #[serde(borrow, default)] label: Option<&'a str>, /// A list of choices to display. @@ -66,36 +71,35 @@ pub(super) enum WidgetSpecific<'a> { values: Vec>, }, - /// Describes a list editor widget producing a Vector. - /// Items can be dragged around to change the order, or dragged out to be deleted from the - /// Vector. - #[serde(rename = "Vector_Editor")] - VectorEditor { + /// A list editor widget producing a Vector. Items can be dragged around to change the order, + /// or dragged out to be deleted from the Vector. + #[serde(rename = "List_Editor", alias = "Vector_Editor")] + ListEditor { /// The widget to use for editing the items. - #[serde(borrow)] - item_editor: Box>, - /// The default value for new items inserted when the user clicks the `+` button. + #[serde(borrow, alias = "item_editor")] + item_editor: Box>, + /// The default value for new items inserted when the user adds a new element. #[serde(borrow)] item_default: &'a str, }, - /// Describes a multi value widget. + /// A multi value widget. #[serde(rename = "Multi_Choice")] MultipleChoice, - /// Describe a code parameter. + /// A code parameter. #[serde(rename = "Code_Input")] CodeInput, - /// Describe a boolean parameter. + /// A boolean parameter. #[serde(rename = "Boolean_Input")] BooleanInput, - /// Describe a numeric parameter. + /// A numeric parameter. #[serde(rename = "Numeric_Input")] NumericInput, - /// Describes a text widget. + /// A text widget. #[serde(rename = "Text_Input")] TextInput, @@ -103,7 +107,7 @@ pub(super) enum WidgetSpecific<'a> { #[serde(rename = "Folder_Browse")] FolderBrowse, - /// Describes a file chooser. + /// A file chooser. #[serde(rename = "File_Browse")] FileBrowse, } @@ -126,7 +130,7 @@ pub enum Display { /// A choice in a single or multiselect widget. #[derive(Debug, serde::Deserialize)] pub(super) struct Choice<'a> { - /// The value of the choice. Must be a valid code expression. + /// The value of the choice. Must be a valid Enso expression. pub value: &'a str, /// Custom label to display in the dropdown. If not provided, IDE will create a label based on /// value. diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index de23a687528c..79116974606c 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -291,68 +291,6 @@ impl Default for Crumbs { -// ======================== -// === ConnectionStatus === -// ======================== - -/// The status of a single incoming connection to a specific port of a node. If a port has been -/// disconnected, the status is set to `Disconnected`. All ports are initially disconnected. -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, Default, PartialEq)] -pub enum ConnectionStatus { - #[default] - Disconnected, - Connected(ConnectionData), -} - -impl ConnectionStatus { - /// Create a new `ConnectionStatus` representing an existing connection. - pub fn connected(color: color::Lcha) -> Self { - Self::Connected(ConnectionData { color }) - } - - /// Returns the data associated with the connection, if it exists. - pub fn data(&self) -> Option { - match self { - Self::Connected(data) => Some(*data), - Self::Disconnected => None, - } - } - - /// Combine two statuses of a connection. If the first status is `Disconnected`, the second one - /// will be returned. Otherwise, the first status will be returned. - pub fn or(self, other: Self) -> Self { - match self { - Self::Disconnected => other, - _ => self, - } - } - - /// Returns true if the port is connected. - pub fn is_connected(&self) -> bool { - matches!(self, Self::Connected(_)) - } -} - -impl From> for ConnectionStatus { - fn from(t: Option) -> Self { - match t { - Some(t) => Self::Connected(t), - None => Self::Disconnected, - } - } -} - -/// The data associated with a single existing incoming connection to a port. Holds the color -/// matching connected edge. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct ConnectionData { - /// Color of an edge connected to the port. - pub color: color::Lcha, -} - - - // ============ // === Node === // ============ @@ -365,7 +303,7 @@ ensogl::define_endpoints_2! { disable_visualization (), set_visualization (Option), set_disabled (bool), - set_input_connected (span_tree::Crumbs,ConnectionStatus), + set_input_connected (span_tree::Crumbs,Option), set_expression (Expression), edit_expression (text::Range, ImString), set_skip_macro (bool), @@ -1175,7 +1113,7 @@ pub mod test_utils { /// 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; + fn input_port_hover_shape(&self) -> Option; } impl NodeModelExt for NodeModel { @@ -1190,7 +1128,7 @@ pub mod test_utils { } } - fn input_port_shape(&self) -> Option { + fn input_port_hover_shape(&self) -> Option { let shapes = self.input.model.port_hover_shapes(); shapes.into_iter().next() } diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index a116c9f38dfb..46b8582631fc 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -60,25 +60,25 @@ pub use span_tree::SpanTree; /// Specialized version of `node::Expression`. #[derive(Clone, Default)] #[allow(missing_docs)] -pub struct InputExpression { +pub struct Expression { pub code: ImString, pub span_tree: SpanTree, } -impl Deref for InputExpression { +impl Deref for Expression { type Target = SpanTree; fn deref(&self) -> &Self::Target { &self.span_tree } } -impl DerefMut for InputExpression { +impl DerefMut for Expression { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.span_tree } } -impl Debug for InputExpression { +impl Debug for Expression { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Expression({})", self.code) } @@ -92,7 +92,7 @@ impl Debug for InputExpression { /// a default `Debug` implementation of `Expression`, so it is hidden behind a separate adapter /// and can be chosen by calling `expression.tree_pretty_printer()`. pub struct ExpressionTreePrettyPrint<'a> { - expression: &'a InputExpression, + expression: &'a Expression, } impl<'a> Debug for ExpressionTreePrettyPrint<'a> { @@ -102,7 +102,7 @@ impl<'a> Debug for ExpressionTreePrettyPrint<'a> { } } -impl InputExpression { +impl Expression { /// Wrap the expression into a pretty-printing adapter that implements `Debug` and prints /// detailed span-tree information. See [`SpanTree::debug_print`] method for more details. /// @@ -116,7 +116,7 @@ impl InputExpression { // === Conversions === -impl From for InputExpression { +impl From for Expression { #[profile(Debug)] fn from(t: node::Expression) -> Self { Self { code: t.code, span_tree: t.input_span_tree } @@ -135,7 +135,7 @@ pub struct Model { app: Application, display_object: display::object::Instance, edit_mode_label: text::Text, - expression: RefCell, + expression: RefCell, styles: StyleWatch, styles_frp: StyleWatchFrp, widget_tree: widget::Tree, @@ -211,22 +211,16 @@ impl Model { self.edit_mode_label.add_to_scene_layer(layer); } - /// Set connection status of the given port. - fn set_connected(&self, crumbs: &Crumbs, status: node::ConnectionStatus) { + fn set_connected(&self, crumbs: &Crumbs, status: Option) { self.widget_tree.set_connected(crumbs, status); } - /// Set usage type of the given port. fn set_expression_usage_type(&self, id: ast::Id, usage_type: Option) { self.widget_tree.set_usage_type(id, usage_type); } fn body_hover_pointer_style(&self, hovered: &bool) -> cursor::Style { - if *hovered { - cursor::Style::cursor() - } else { - default() - } + hovered.then(|| cursor::Style::cursor()).unwrap_or_default() } fn port_hover_pointer_style(&self, hovered: &Switch) -> Option { @@ -234,7 +228,7 @@ impl Model { let expr = self.expression.borrow(); let port = expr.span_tree.get_node(crumbs).ok()?; let display_object = self.widget_tree.get_port_display_object(&port)?; - let tp = port.tp().map(|t| Type(t.into())); + let tp = port.tp().map(|t| t.into()); let color = tp.as_ref().map(|tp| type_coloring::compute(tp, &self.styles)); let pad_x = node::input::port::PORT_PADDING_X * 2.0; let min_y = node::input::port::BASE_PORT_HEIGHT; @@ -262,7 +256,7 @@ impl Model { /// Request widgets metadata for all method calls within the expression. #[profile(Debug)] - fn request_widgets_metadata(&self, expression: &InputExpression, area_frp: &FrpEndpoints) { + fn request_widgets_metadata(&self, expression: &Expression, area_frp: &FrpEndpoints) { let call_info = CallInfoMap::scan_expression(&expression.span_tree); for (call_id, info) in call_info.iter() { if let Some(target_id) = info.target_id { @@ -275,7 +269,7 @@ impl Model { /// expression is being edited by the user. #[profile(Debug)] fn set_expression(&self, new_expression: impl Into, area_frp: &FrpEndpoints) { - let new_expression = InputExpression::from(new_expression.into()); + let new_expression = Expression::from(new_expression.into()); debug!("set expression: \n{:?}", new_expression.tree_pretty_printer()); self.widget_tree.rebuild_tree( @@ -331,7 +325,7 @@ ensogl::define_endpoints! { /// Set the connection status of the port indicated by the breadcrumbs. For connected ports, /// contains the color of connected edge. - set_connected (Crumbs, node::ConnectionStatus), + set_connected (Crumbs, Option), /// Update widget metadata for widgets already present in this input area. update_widgets (WidgetUpdates), @@ -524,13 +518,7 @@ impl Area { eval frp.set_connected(((crumbs,status)) model.set_connected(crumbs,*status)); eval frp.set_expression_usage_type(((id,tp)) model.set_expression_usage_type(*id,tp.clone())); eval frp.set_disabled ((disabled) model.widget_tree.set_disabled(*disabled)); - widget_tree_invalidated <- any_(...); - widget_tree_invalidated <+ frp.update_widgets; - widget_tree_invalidated <+ frp.set_connected; - widget_tree_invalidated <+ frp.set_disabled; - widget_tree_invalidated <+ frp.set_expression_usage_type; - rebuild_widget_tree <- widget_tree_invalidated.debounce(); - eval rebuild_widget_tree((_) model.rebuild_widget_tree_if_dirty()); + eval model.widget_tree.rebuild_required((_) model.rebuild_widget_tree_if_dirty()); // === View Mode === @@ -576,7 +564,7 @@ impl Area { .root_ref() .get_descendant(crumbs) .ok() - .and_then(|t| t.tp().map(|t| Type(t.into()))) + .and_then(|t| t.tp().map(|t| t.into())) } /// Set a scene layer for text rendering. diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index 585a0c9691e2..cda6ede32100 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -4,10 +4,10 @@ use crate::prelude::*; use crate::component::node::input::widget::Config; use crate::component::node::input::widget::ConfigContext; +use crate::component::node::input::widget::ConnectionData; use crate::component::node::input::widget::DynWidget; use crate::component::node::input::widget::SpanWidget; use crate::component::node::input::widget::WidgetsFrp; -use crate::component::node::ConnectionStatus; use enso_frp as frp; use ensogl::application::Application; @@ -33,12 +33,6 @@ pub const BASE_PORT_HEIGHT: f32 = 18.0; /// the target text boundary on both sides. pub const PRIMARY_PORT_HOVER_PADDING_Y: f32 = (crate::node::HEIGHT - BASE_PORT_HEIGHT) / 2.0; -/// The maximum depth of the widget port that is still considered primary. This is used to determine -/// the hover area of the port. -pub const PRIMARY_PORT_MAX_DEPTH: usize = 0; - - - // ============= // === Shape === // ============= @@ -84,8 +78,10 @@ pub mod hover_shape { } } -/// An scene extension that holds the partitions of hover shapes for all ports. This is used to -/// visually sort the ports based on port depth in the widget tree. +/// An scene extension that maintains layer partitions for port hover shapes. It is shared by all +/// ports in the scene. The hover shapes are partitioned by span tree depth, so that the hover area +/// of ports deeper in the tree will always be displayed on top, giving them priority to receive +/// mouse events. #[derive(Clone, CloneRef)] struct HoverLayers { hover_layer: display::scene::Layer, @@ -112,13 +108,16 @@ impl HoverLayers { } + // ============ // === Port === // ============ -/// A port on the node side. It can be connected to other ports. +/// Node of a widget tree that can be a source of an edge. Displays a visual representation of the +/// connection below the widget, and handles mouse hover and click events when an edge is dragged. #[derive(Debug)] pub struct Port { + /// Drop source must be kept at the top of the struct, so it will be dropped first. #[allow(dead_code)] on_cleanup: frp::DropSource, crumbs: Rc>, @@ -179,7 +178,7 @@ impl Port { hovering <- hovering.on_change(); frp.on_port_hover <+ hovering.map( - f!([crumbs](t) Switch::new(crumbs.borrow().clone(),*t)) + f!([crumbs](t) Switch::new(crumbs.borrow().clone(), *t)) ); frp.on_port_press <+ mouse_down.map(f!((_) crumbs.borrow().clone())); @@ -194,7 +193,8 @@ impl Port { }); // Port shape is only connected to the display hierarchy when the port is connected. - // Thus the `on_updated` event is automatically disabled when the port is not connected. + // Thus the `on_transformed` event is automatically disabled when the port is not + // connected. let shape_display_object = port_shape.display_object(); frp.connected_port_updated <+ shape_display_object.on_transformed; }; @@ -214,8 +214,8 @@ impl Port { /// Configure the port and its attached widget. pub fn configure(&mut self, config: &Config, ctx: ConfigContext) { - self.crumbs.replace(ctx.span_tree_node.crumbs.clone()); - self.set_connected(ctx.state.connection); + self.crumbs.replace(ctx.span_node.crumbs.clone()); + self.set_connected(ctx.info.connection); self.set_port_layout(&ctx); self.widget.configure(config, ctx); self.update_root(); @@ -223,13 +223,13 @@ impl Port { /// Update connection status of this port. Changing the connection status will add or remove the /// port's visible shape from the display hierarchy. - fn set_connected(&self, status: ConnectionStatus) { + fn set_connected(&self, status: Option) { match status { - ConnectionStatus::Connected(data) => { + Some(data) => { self.port_root.add_child(&self.port_shape); self.port_shape.color.set(color::Rgba::from(data.color).into()) } - ConnectionStatus::Disconnected => { + None => { self.port_root.remove_child(&self.port_shape); } }; @@ -245,15 +245,14 @@ impl Port { } fn set_port_layout(&mut self, ctx: &ConfigContext) { - let node_depth = ctx.span_tree_node.crumbs.len(); + let node_depth = ctx.span_node.crumbs.len(); if self.current_depth != node_depth { self.current_depth = node_depth; let layers = ctx.app().display.default_scene.extension::(); layers.add_to_partition(self.hover_shape.display_object(), node_depth); } - #[allow(clippy::absurd_extreme_comparisons)] - let is_primary = ctx.state.depth <= PRIMARY_PORT_MAX_DEPTH; + let is_primary = ctx.info.nesting_level.is_primary(); if self.current_primary != is_primary { self.current_primary = is_primary; let margin = if is_primary { PRIMARY_PORT_HOVER_PADDING_Y } else { 0.0 }; diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 8b5967c79e8a..0d0df1b38cf4 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -5,21 +5,19 @@ use crate::prelude::*; use crate::component::node::input::area::NODE_HEIGHT; use crate::component::node::input::area::TEXT_OFFSET; use crate::component::node::input::port::Port; -use crate::component::node::ConnectionData; -use crate::component::node::ConnectionStatus; use enso_config::ARGS; use enso_frp as frp; use enso_text as text; use ensogl::application::Application; +use ensogl::data::color; use ensogl::display; use ensogl::display::shape::StyleWatch; use ensogl::gui::cursor; use ensogl_component::drop_down::DropdownValue; +use span_tree::node::Ref as SpanRef; use text::index::Byte; - - // ================= // === Constants === // ================= @@ -30,6 +28,10 @@ use text::index::Byte; /// granular edit mode that works with widgets. pub const WIDGET_SPACING_PER_OFFSET: f32 = 7.224_609_4; +/// The maximum depth of the widget port that is still considered primary. This is used to determine +/// the hover area of the port. +pub const PRIMARY_PORT_MAX_NESTING_LEVEL: usize = 0; + // =========== @@ -53,6 +55,11 @@ ensogl::define_endpoints_2! { /// signal is generated using the `on_updated` signal of the `display_object` of the widget, /// all caveats of that signal apply here as well. connected_port_updated (), + /// Tree data update recently caused it to be marked as dirty. Rebuild is required. + rebuild_required (), + /// Dirty flag has been marked. This signal is fired immediately after the update that + /// caused it. Prefer using `rebuild_required` signal instead, which is debounced. + marked_dirty_sync (), } } @@ -83,6 +90,8 @@ pub trait SpanWidget { fn configure(&mut self, config: &Self::Config, ctx: ConfigContext); } +/// Generate implementation for [`DynWidget`] enum and its associated [`Config`] enum. Those enums +/// are used to represent any possible widget kind and its configuration. macro_rules! define_widget_modules( ($( $(#[$meta:meta])* @@ -138,9 +147,7 @@ macro_rules! define_widget_modules( fn new(config: &Config, ctx: &ConfigContext) -> Self { match config { - $( - Config::$name(config) => DynWidget::$name(SpanWidget::new(config, ctx)), - )* + $(Config::$name(config) => DynWidget::$name(SpanWidget::new(config, ctx)),)* } } @@ -176,8 +183,10 @@ define_widget_modules! { /// === Metadata === /// ================ -/// Widget metadata that comes from an asynchronous visualization. Defines which widget should be -/// used and a set of options that it should allow to choose from. +/// The configuration of a widget and its display properties. Defines how the widget should be +/// displayed, if it should be displayed at all, and whether or not it should have a port. Widgets +/// that declare themselves as having a port will be able to handle edge connections and visually +/// indicate that they are connected. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct Metadata { @@ -209,15 +218,11 @@ impl Metadata { Self::always(vector_editor::Config { item_editor: None, item_default: "_".into() }) } - fn from_node( - node: &span_tree::node::Ref, - usage_type: Option, - expression: &str, - ) -> Self { + fn from_node(span_node: &SpanRef, usage_type: Option, expression: &str) -> Self { use span_tree::node::Kind; - let kind = &node.kind; - let has_children = !node.children.is_empty(); + let kind = &span_node.kind; + let has_children = !span_node.children.is_empty(); const VECTOR_TYPE: &str = "Standard.Base.Data.Vector.Vector"; let is_array_enabled = ARGS.groups.feature_preview.options.vector_editor.value; @@ -228,7 +233,7 @@ impl Metadata { .or(arg.tp.as_deref()) .map_or(false, |tp| tp.contains(VECTOR_TYPE)); if type_matches { - let node_expr = &expression[node.span()]; + let node_expr = &expression[span_node.span()]; node_expr.starts_with('[') && node_expr.ends_with(']') } else { false @@ -369,6 +374,8 @@ impl Tree { let network = &frp.network; frp::extend! { network + frp.private.output.rebuild_required <+ frp.marked_dirty_sync.debounce(); + set_ports_visible <- frp.set_ports_visible.sampler(); set_view_mode <- frp.set_view_mode.sampler(); set_profiling_status <- frp.set_profiling_status.sampler(); @@ -405,26 +412,26 @@ impl Tree { /// and it can be done by calling this method. The set metadata is persistent, and will be /// applied to any future widget of this node that matches given pointer. pub fn set_metadata(&self, pointer: MetadataPointer, meta: Option) { - self.model.set_metadata(pointer, meta); + self.notify_dirty(self.model.set_metadata(pointer, meta)); } /// Set usage type for given AST node. The usage type is used to determine the widget appearance /// and default inferred widget metadata. pub fn set_usage_type(&self, ast_id: ast::Id, usage_type: Option) { - self.model.set_usage_type(ast_id, usage_type); + self.notify_dirty(self.model.set_usage_type(ast_id, usage_type)); } /// Set connection status for given span crumbs. The connected nodes will be highlighted with a /// different color, and the widgets might change behavior depending on the connection /// status. - pub fn set_connected(&self, crumbs: &span_tree::Crumbs, status: ConnectionStatus) { - self.model.set_connected(crumbs, status); + pub fn set_connected(&self, crumbs: &span_tree::Crumbs, status: Option) { + self.notify_dirty(self.model.set_connected(crumbs, status)); } /// Set disabled status for given span tree node. The disabled nodes will be grayed out. /// The widgets might change behavior depending on the disabled status. pub fn set_disabled(&self, disabled: bool) { - self.model.set_disabled(disabled); + self.notify_dirty(self.model.set_disabled(disabled)); } @@ -459,13 +466,13 @@ impl Tree { /// have a distinct widget, so the returned value might be `None`. pub fn get_port_display_object( &self, - tree_node: &span_tree::node::Ref, + span_node: &SpanRef, ) -> Option { - let pointer = self.model.get_node_widget_pointer(tree_node); + let pointer = self.model.get_node_widget_pointer(span_node); self.model.with_port(pointer, |w| w.display_object().clone()) } - /// Get hover shapes for all ports in the tree. Used for testing to simulate mouse events. + /// Get hover shapes for all ports in the tree. Used in tests to manually dispatch mouse events. pub fn port_hover_shapes(&self) -> Vec { let nodes = self.model.nodes_map.borrow(); self.model @@ -476,6 +483,12 @@ impl Tree { .filter_map(|e| Some(e.node.port()?.hover_shape().clone_ref())) .collect_vec() } + + fn notify_dirty(&self, dirty_flag_just_set: bool) { + if dirty_flag_just_set { + self.frp.private.output.marked_dirty_sync.emit(()); + } + } } @@ -516,7 +529,6 @@ impl display::Object for TreeNode { #[derive(Debug, Clone, Copy)] struct NodeHierarchy { identity: WidgetIdentity, - #[allow(dead_code)] parent_index: Option, total_descendants: usize, } @@ -530,6 +542,23 @@ struct TreeEntry { index: usize, } + + +// ====================== +// === ConnectionData === +// ====================== + +/// Data associated with an edge connected to a port in the tree. It is accessible to the connected +/// port, its widget and all its descendants through `connection` and `subtree_connection` fields +/// of [`NodeState`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(super) struct ConnectionData { + /// Color of an edge connected to the port. + pub color: color::Lcha, + /// Span tree depth at which the connection is made. + pub depth: usize, +} + /// ================= /// === TreeModel === /// ================= @@ -544,9 +573,9 @@ struct TreeModel { /// be used to quickly find the parent of a node, or iterate over all children or descendants /// of a node. hierarchy: RefCell>, - ports_map: RefCell>, + ports_map: RefCell>, metadata_map: Rc>>, - connected_map: Rc>>, + connected_map: Rc>>, usage_type_map: Rc>>, node_disabled: Cell, tree_dirty: Cell, @@ -578,39 +607,41 @@ impl TreeModel { } } + /// Mark dirty flag if the tree has been modified. Return true if the flag has been changed. + fn mark_dirty_flag(&self, modified: bool) -> bool { + if modified && !self.tree_dirty.get() { + self.tree_dirty.set(true); + true + } else { + false + } + } + /// Set the metadata under given pointer. It may cause the tree to be marked as dirty. - fn set_metadata(&self, pointer: MetadataPointer, meta: Option) { + fn set_metadata(&self, pointer: MetadataPointer, meta: Option) -> bool { let mut map = self.metadata_map.borrow_mut(); let dirty = map.synchronize_entry(pointer, meta); - if dirty { - self.tree_dirty.set(true); - } + self.mark_dirty_flag(dirty) } /// Set the connection status under given widget. It may cause the tree to be marked as dirty. - fn set_connected(&self, crumbs: &span_tree::Crumbs, status: ConnectionStatus) { + fn set_connected(&self, crumbs: &span_tree::Crumbs, status: Option) -> bool { let mut map = self.connected_map.borrow_mut(); - let dirty = map.synchronize_entry(crumbs.clone(), status.data()); - if dirty { - self.tree_dirty.set(true); - } + let dirty = map.synchronize_entry(crumbs.clone(), status); + self.mark_dirty_flag(dirty) } /// Set the usage type of an expression. It may cause the tree to be marked as dirty. - fn set_usage_type(&self, ast_id: ast::Id, usage_type: Option) { + fn set_usage_type(&self, ast_id: ast::Id, usage_type: Option) -> bool { let mut map = self.usage_type_map.borrow_mut(); let dirty = map.synchronize_entry(ast_id, usage_type); - if dirty { - self.tree_dirty.set(true); - } + self.mark_dirty_flag(dirty) } /// Set the connection status under given widget. It may cause the tree to be marked as dirty. - fn set_disabled(&self, disabled: bool) { + fn set_disabled(&self, disabled: bool) -> bool { let prev_disabled = self.node_disabled.replace(disabled); - if prev_disabled != disabled { - self.tree_dirty.set(true); - } + self.mark_dirty_flag(prev_disabled != disabled) } /// Get parent of a node under given pointer, if exists. @@ -668,7 +699,7 @@ impl TreeModel { let mut hierarchy = self.hierarchy.take(); hierarchy.clear(); - let mut builder = WidgetTreeBuilder { + let mut builder = TreeBuilder { app, frp, node_disabled, @@ -681,12 +712,12 @@ impl TreeModel { hierarchy, pointer_usage: default(), new_nodes: default(), - parent_state: default(), + parent_info: default(), last_ast_depth: default(), extensions: default(), }; - let child = builder.child_widget(tree.root_ref(), 0); + let child = builder.child_widget(tree.root_ref(), default()); self.display_object.replace_children(&[child]); self.nodes_map.replace(builder.new_nodes); @@ -702,18 +733,18 @@ impl TreeModel { /// has a unique representation in the form of a widget tree pointer, which is more stable /// across changes in the span tree than [`span_tree::Crumbs`]. The pointer is used to identify /// the widgets or ports in the widget tree. - pub fn get_node_widget_pointer(&self, tree_node: &span_tree::node::Ref) -> MainWidgetPointer { - if let Some(id) = tree_node.node.ast_id { + pub fn get_node_widget_pointer(&self, span_node: &SpanRef) -> StableSpanIdentity { + if let Some(id) = span_node.ast_id { // This span represents an AST node, return a pointer directly to it. - MainWidgetPointer::new(Some(id), &[]) + StableSpanIdentity::new(Some(id), &[]) } else { - let root = tree_node.span_tree.root_ref(); + let root = span_node.span_tree.root_ref(); let root_ast_data = root.ast_id.map(|id| (id, 0)); // When the node does not represent an AST node, its widget will be identified by the // closest parent AST node, if it exists. We have to find the closest parent node with // AST ID, and then calculate the relative crumbs from it to the current node. - let (_, ast_parent_data) = tree_node.crumbs.into_iter().enumerate().fold( + let (_, ast_parent_data) = span_node.crumbs.into_iter().enumerate().fold( (root, root_ast_data), |(node, last_seen), (index, crumb)| { let ast_data = node.node.ast_id.map(|id| (id, index)).or(last_seen); @@ -724,11 +755,11 @@ impl TreeModel { match ast_parent_data { // Parent AST node found, return a pointer relative to it. Some((ast_id, ast_parent_index)) => { - let crumb_slice = &tree_node.crumbs[ast_parent_index..]; - MainWidgetPointer::new(Some(ast_id), crumb_slice) + let crumb_slice = &span_node.crumbs[ast_parent_index..]; + StableSpanIdentity::new(Some(ast_id), crumb_slice) } // No parent AST node found. Return a pointer from root. - None => MainWidgetPointer::new(None, &tree_node.crumbs), + None => StableSpanIdentity::new(None, &span_node.crumbs), } } } @@ -738,7 +769,7 @@ impl TreeModel { /// returned. pub fn with_port( &self, - pointer: MainWidgetPointer, + pointer: StableSpanIdentity, f: impl FnOnce(&Port) -> T, ) -> Option { let index = *self.ports_map.borrow().get(&pointer)?; @@ -750,31 +781,26 @@ impl TreeModel { /// State of a node in the widget tree. Provides additional information about the node's current /// state, such as its depth in the widget tree, if it's connected, disabled, etc. #[derive(Debug, Clone, PartialEq)] -pub(super) struct NodeState { - /// Identity of this node. +pub(super) struct NodeInfo { + /// Unique identifier of this node within this widget tree. pub identity: WidgetIdentity, /// Index of node in the widget tree, in insertion order. pub insertion_index: usize, - /// Widget tree node depth, as provided by the parent node. This does not necessarily - /// correspond to the depth in the view hierarchy or span tree, but instead is treated as a - /// logical nesting level in the expressions. It is fully determined by the chain of parent - /// widgets, and thus is more or less independent from the true depth of the widget tree data - /// structure. - pub depth: usize, - /// Connection status of the node. Only present at the exact node that is connected, not at - /// any of its parents. - pub connection: ConnectionStatus, - /// Connection status of the node's subtree. Contains the status of this node's connection, or - /// it's first parent that is connected. This is the same as `connection` for nodes that - /// are directly connected. - pub subtree_connection: ConnectionStatus, + /// Logical nesting level of this widget, which was specified by the parent node during its + /// creation. Determines the mouse hover area size and widget indentation. + pub nesting_level: NestingLevel, + /// Data associated with an edge connected to this node's span. Only present at the exact node + /// that is connected, not at any of its children. + pub connection: Option, + /// Data associated with an edge connected to this subtree. Contains the status of this node's + /// connection, or its first parent that is connected. It is the same as `connection` for nodes + /// that are directly connected. + pub subtree_connection: Option, /// Whether the node is disabled, i.e. its expression is not currently used in the computation. /// Widgets of disabled nodes are usually grayed out. pub disabled: bool, - /// Expression's usage type, as opposed to its definition type stored in the span tree. The - /// usage type represents the type of the expression as it is used in the computation, which - /// may differ from the definition type. For example, the definition type of a variable may be - /// `Number`, but its usage type may be `Vector`, if it is used as a vector component. + /// Inferred type of Enso expression at this node's span. May differ from the definition type + /// stored in the span tree. pub usage_type: Option, } @@ -783,20 +809,15 @@ pub(super) struct NodeState { /// child widgets. #[derive(Debug)] pub struct ConfigContext<'a, 'b> { - builder: &'a mut WidgetTreeBuilder<'b>, - /// Display mode of the widget. - /// TODO [PG]: Consider handling display modes in the widget tree builder directly, instead of - /// passing it to the widgets. Right now it has not effect at all. - #[allow(dead_code)] - pub(super) display: Display, + builder: &'a mut TreeBuilder<'b>, /// The span tree node corresponding to the widget being configured. - pub(super) span_tree_node: span_tree::node::Ref<'a>, + pub(super) span_node: span_tree::node::Ref<'a>, /// Additional state associated with configured widget tree node, such as its depth, connection /// status or parent node information. - pub(super) state: NodeState, + pub(super) info: NodeInfo, /// The length of tree extensions vector before the widget was configured. Used to determine /// which extensions were added by the widget parents, and which are new. - parent_extensions_len: usize, + parent_extensions_len: usize, } impl<'a, 'b> ConfigContext<'a, 'b> { @@ -879,29 +900,60 @@ impl<'a, 'b> ConfigContext<'a, 'b> { -/// ========================================== -/// === MainWidgetPointer / WidgetIdentity === -/// ========================================== +/// ==================== +/// === NestingLevel === +/// ==================== + +/// A logical nesting level associated with a widget which determines the mouse hover area size and +/// widget indentation. It is specified by the parent widget when creating a child widget, as an +/// argument to the '[`ConfigContext`]' method. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NestingLevel { + level: usize, +} + +impl NestingLevel { + /// Create a deeper nesting level. The depth of the new level will be one greater than the + /// current one. + pub fn next(self) -> Self { + Self { level: self.level + 1 } + } -/// A pointer to main widget of specific node in the span tree. Determines the base of a widget -/// stable identity, and allows widgets to be reused when rebuilding the tree. The pointer is -/// composed of two parts: -/// - `id` is the AST ID of either the node itself, or the closest ancestor node which has one. It -/// can be `None` if there is no such parent, e.g. for a tree Root node. -/// - `crumbs_hash` is a hash of remaining crumbs since last node with stable AST ID, or since the -/// root node if there is no such parent. + /// Create an optionally deeper nesting level. When `condition` is `false`, the nesting level + /// will remain the same. + pub fn next_if(self, condition: bool) -> Self { + condition.as_some(self.next()).unwrap_or(self) + } + + /// Check if a port at this nesting level is still considered primary. Primary ports have wider + /// hover areas and are indented more. + #[allow(clippy::absurd_extreme_comparisons)] + pub fn is_primary(self) -> bool { + self.level <= PRIMARY_PORT_MAX_NESTING_LEVEL + } +} + + +/// =========================================== +/// === StableSpanIdentity / WidgetIdentity === +/// =========================================== + +/// A stable identifier to a span tree node. Uniquely determines a main widget of specific node in +/// the span tree. It is a base of a widget stable identity, and allows widgets to be reused when +/// rebuilding the tree. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct MainWidgetPointer { - /// The latest set ast::Id in the span tree. +pub struct StableSpanIdentity { + /// AST ID of either the node itself, or the closest ancestor node which has one. Is [`None`] + /// when there is no such parent with assigned AST id. id: Option, - /// A hash of remaining crumbs to the widget, starting from the node with the latest set - /// ast::Id. We store a hash instead of the crumbs directly, so the type can be trivially - /// copied. The collision is extremely unlikely due to commonly having very short lists of - /// crumbs to store here and u64 being comparatively extremely large hash space. + /// A hash of remaining crumbs to the widget, starting from the closest node with assigned AST + /// id. We store a hash instead of the crumbs directly, so the type can be trivially copied. + /// The collision is extremely unlikely due to commonly having very short lists of crumbs + /// to store here and u64 being comparatively extremely large hash space. crumbs_hash: u64, } -impl MainWidgetPointer { +impl StableSpanIdentity { fn new(id: Option, crumbs: &[span_tree::Crumb]) -> Self { let mut hasher = DefaultHasher::new(); crumbs.hash(&mut hasher); @@ -915,30 +967,30 @@ impl MainWidgetPointer { } } -/// An unique identity of a widget in the widget tree. It is a combination of a `MainWidgetPointer` -/// and a sequential index of the widget assigned to the same span tree node. Any widget is allowed -/// to create a child widget on the same span tree node, so we need to be able to distinguish -/// between them. Note that only one widget created for a given span tree node will be able to -/// receive a port. The port is assigned to the first widget created for a given node that wants to -/// receive it. +/// An unique identity of a widget in the widget tree. It is a combination of a [`SpanIdentity`] and +/// a sequential index of the widget assigned to the same span tree node. Any widget is allowed to +/// create a child widget on the same span tree node, so we need to be able to distinguish between +/// them. Note that only one widget created for a given span tree node will be able to receive a +/// port. The port is assigned to the first widget at given span that wants to receive it. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct WidgetIdentity { /// The pointer to the main widget of this widget's node. - main: MainWidgetPointer, + main: StableSpanIdentity, /// The sequential index of a widget assigned to the same span tree node. index: usize, } impl WidgetIdentity { - /// Whether this widget pointer represents first created widget for its node. - fn is_first(&self) -> bool { + /// Whether this widget pointer represents first created widget for its span tree node. + fn is_first_widget_of_span(&self) -> bool { self.index == 0 } } /// Additional information about the usage of a widget pointer while building a tree. This is used /// to determine which widget should receive a port, and to assign sequential indices to widgets -/// created for the same span tree node. +/// created for the same span tree node. Used to transform ambiguous [`SpanIdentity`] into +/// unique [`WidgetIdentity`]. #[derive(Debug, Default)] struct PointerUsage { /// Next sequence index that will be assigned to a widget created for the same span tree node. @@ -965,34 +1017,34 @@ impl PointerUsage { -/// ========================= -/// === WidgetTreeBuilder === -/// ========================= +/// =================== +/// === TreeBuilder === +/// =================== /// A builder for the widget tree. Maintains transient state necessary during the tree construction, /// and provides methods for creating child nodes of the tree. Maintains a map of all widgets /// created so far, and is able to reuse existing widgets under the same location in the tree, only /// updating their configuration as necessary. #[derive(Debug)] -struct WidgetTreeBuilder<'a> { +struct TreeBuilder<'a> { app: Application, frp: WidgetsFrp, node_disabled: bool, node_expression: &'a str, styles: &'a StyleWatch, metadata_map: &'a HashMap, - connected_map: &'a HashMap, + connected_map: &'a HashMap, usage_type_map: &'a HashMap, old_nodes: HashMap, new_nodes: HashMap, hierarchy: Vec, - pointer_usage: HashMap, - parent_state: Option, + pointer_usage: HashMap, + parent_info: Option, last_ast_depth: usize, extensions: Vec>, } -impl<'a> WidgetTreeBuilder<'a> { +impl<'a> TreeBuilder<'a> { /// Create a new child widget. The widget type will be automatically inferred, either based on /// the node kind, or on the metadata provided from the language server. If possible, an /// existing widget will be reused under the same location in the tree, only updating its @@ -1009,10 +1061,10 @@ impl<'a> WidgetTreeBuilder<'a> { #[must_use] pub fn child_widget( &mut self, - span_tree_node: span_tree::node::Ref<'_>, - depth: usize, + span_node: span_tree::node::Ref<'_>, + nesting_level: NestingLevel, ) -> display::object::Instance { - self.create_child_widget(span_tree_node, depth, None) + self.create_child_widget(span_node, nesting_level, None) } /// Create a new child widget. Does not infer the widget type, but uses the provided metadata @@ -1023,11 +1075,11 @@ impl<'a> WidgetTreeBuilder<'a> { #[allow(dead_code)] // Currently unused, but will be used in the future in VectorEditor. pub fn child_widget_of_type( &mut self, - span_tree_node: span_tree::node::Ref<'_>, - depth: usize, + span_node: span_tree::node::Ref<'_>, + nesting_level: NestingLevel, meta: Metadata, ) -> display::object::Instance { - self.create_child_widget(span_tree_node, depth, Some(meta)) + self.create_child_widget(span_node, nesting_level, Some(meta)) } /// Create a new widget for given span tree node. This function recursively builds a subtree @@ -1035,35 +1087,36 @@ impl<'a> WidgetTreeBuilder<'a> { /// returned, so that it can be added to the parent's display hierarchy. fn create_child_widget( &mut self, - span_tree_node: span_tree::node::Ref<'_>, - depth: usize, + span_node: span_tree::node::Ref<'_>, + nesting_level: NestingLevel, set_metadata: Option, ) -> display::object::Instance { // This call can recurse into itself within the widget configuration logic. We need to save // the current layer's state, so it can be restored later after visiting the child node. let parent_last_ast_depth = self.last_ast_depth; + let depth = span_node.crumbs.len(); // Figure out the widget tree pointer for the current node. That pointer determines the // widget identity, allowing it to maintain internal state. If the previous tree already // contained a widget for this pointer, we have to reuse it. - let main_ptr = match span_tree_node.ast_id { + let main_ptr = match span_node.ast_id { Some(ast_id) => { - self.last_ast_depth = span_tree_node.crumbs.len(); - MainWidgetPointer::new(Some(ast_id), &[]) + self.last_ast_depth = depth; + StableSpanIdentity::new(Some(ast_id), &[]) } None => { - let ast_id = self.parent_state.as_ref().and_then(|st| st.identity.main.id); - let this_crumbs = &span_tree_node.crumbs; + let ast_id = self.parent_info.as_ref().and_then(|st| st.identity.main.id); + let this_crumbs = &span_node.crumbs; let crumbs_since_id = &this_crumbs[parent_last_ast_depth..]; - MainWidgetPointer::new(ast_id, crumbs_since_id) + StableSpanIdentity::new(ast_id, crumbs_since_id) } }; let ptr_usage = self.pointer_usage.entry(main_ptr).or_default(); - let unique_ptr = main_ptr.to_identity(ptr_usage); + let widget_id = main_ptr.to_identity(ptr_usage); - let is_placeholder = span_tree_node.is_expected_argument(); - let sibling_offset = span_tree_node.sibling_offset.as_usize(); + let is_placeholder = span_node.is_expected_argument(); + let sibling_offset = span_node.sibling_offset.as_usize(); let usage_type = main_ptr.id.and_then(|id| self.usage_type_map.get(&id)).cloned(); // Get widget metadata. There are three potential sources for metadata, that are used in @@ -1075,7 +1128,7 @@ impl<'a> WidgetTreeBuilder<'a> { // 3. The default metadata for the node, which is determined based on the node kind, usage // type and whether it has children. let mut meta_fallback = None; - let kind = &span_tree_node.kind; + let kind = &span_node.kind; let meta = set_metadata .as_ref() .or_else(|| { @@ -1089,53 +1142,44 @@ impl<'a> WidgetTreeBuilder<'a> { }) .unwrap_or_else(|| { meta_fallback.get_or_insert_with(|| { - Metadata::from_node(&span_tree_node, usage_type.clone(), self.node_expression) + Metadata::from_node(&span_node, usage_type.clone(), self.node_expression) }) }); - let widget_has_port = ptr_usage.request_port(&unique_ptr, meta.has_port); + let widget_has_port = ptr_usage.request_port(&widget_id, meta.has_port); - let self_insertion_index = self.hierarchy.len(); + let insertion_index = self.hierarchy.len(); self.hierarchy.push(NodeHierarchy { - identity: unique_ptr, - parent_index: self.parent_state.as_ref().map(|st| st.insertion_index), + identity: widget_id, + parent_index: self.parent_info.as_ref().map(|info| info.insertion_index), // This will be updated later, after the child widgets are created. total_descendants: 0, }); - let old_node = self.old_nodes.remove(&unique_ptr).map(|e| e.node); + let old_node = self.old_nodes.remove(&widget_id).map(|e| e.node); // Once we have the metadata and potential old widget to reuse, we have to apply the // configuration to the widget. - let connection: ConnectionStatus = - self.connected_map.get(&span_tree_node.crumbs).copied().into(); - let subtree_connection = match self.parent_state.as_ref().map(|s| s.subtree_connection) { - Some(parent_connection) => connection.or(parent_connection), - None => connection, - }; + let connection_color = self.connected_map.get(&span_node.crumbs); + let connection = connection_color.map(|&color| ConnectionData { color, depth }); + let parent_connection = self.parent_info.as_ref().and_then(|info| info.connection); + let subtree_connection = connection.or(parent_connection); let disabled = self.node_disabled; - let state = NodeState { - identity: unique_ptr, - insertion_index: self_insertion_index, - depth, + let info = NodeInfo { + identity: widget_id, + insertion_index, + nesting_level, connection, subtree_connection, disabled, usage_type, }; - let state_to_restore = std::mem::replace(&mut self.parent_state, Some(state.clone())); - + let parent_info = std::mem::replace(&mut self.parent_info, Some(info.clone())); let parent_extensions_len = self.extensions.len(); - let ctx = ConfigContext { - builder: &mut *self, - display: meta.display, - span_tree_node, - state, - parent_extensions_len, - }; + let ctx = ConfigContext { builder: &mut *self, span_node, info, parent_extensions_len }; let app = ctx.app(); let frp = ctx.frp(); @@ -1165,18 +1209,18 @@ impl<'a> WidgetTreeBuilder<'a> { // Once the node has been configured and all its children have been created, we can update // the hierarchy data. - self.hierarchy[self_insertion_index].total_descendants = - self.hierarchy.len() - self_insertion_index - 1; + self.hierarchy[insertion_index].total_descendants = + self.hierarchy.len() - insertion_index - 1; // After visiting child node, restore previous layer's parent data. - self.parent_state = state_to_restore; + self.parent_info = parent_info; self.last_ast_depth = parent_last_ast_depth; self.extensions.truncate(parent_extensions_len); // Apply left margin to the widget, based on its offset relative to the previous sibling. let child_root = child_node.display_object().clone(); let offset = match () { - _ if !unique_ptr.is_first() => 0, + _ if !widget_id.is_first_widget_of_span() => 0, _ if is_placeholder => 1, _ => sibling_offset, }; @@ -1186,8 +1230,8 @@ impl<'a> WidgetTreeBuilder<'a> { child_root.set_margin_left(left_margin); } - let entry = TreeEntry { node: child_node, index: self_insertion_index }; - self.new_nodes.insert(unique_ptr, entry); + let entry = TreeEntry { node: child_node, index: insertion_index }; + self.new_nodes.insert(widget_id, entry); child_root } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs index c4f64c7654dc..9940e61b0cfd 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -37,13 +37,12 @@ impl super::SpanWidget for Widget { } fn configure(&mut self, _: &Config, ctx: super::ConfigContext) { - let increase_depth = ctx.span_tree_node.is_argument(); - let next_depth = if increase_depth { ctx.state.depth + 1 } else { ctx.state.depth }; + let child_level = ctx.info.nesting_level.next_if(ctx.span_node.is_argument()); let children = ctx - .span_tree_node + .span_node .children_iter() - .map(|node| ctx.builder.child_widget(node, next_depth)) + .map(|node| ctx.builder.child_widget(node, child_level)) .collect_vec(); self.display_object.replace_children(&children); } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index f12583184061..70a81fa6d3f6 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -88,23 +88,23 @@ impl super::SpanWidget for Widget { } fn configure(&mut self, _: &Config, ctx: super::ConfigContext) { - let is_placeholder = ctx.span_tree_node.is_expected_argument(); + let is_placeholder = ctx.span_node.is_expected_argument(); let content = if is_placeholder { - ctx.span_tree_node.kind.argument_name().unwrap_or_default() + ctx.span_node.kind.argument_name().unwrap_or_default() } else { - ctx.expression_at(ctx.span_tree_node.span()) + ctx.expression_at(ctx.span_node.span()) }; - let color_state: ColorState = if ctx.state.subtree_connection.is_connected() { + let color_state: ColorState = if ctx.info.subtree_connection.is_some() { ColorState::Connected - } else if ctx.state.disabled { + } else if ctx.info.disabled { ColorState::Disabled } else if is_placeholder { ColorState::Placeholder } else { - let ty = ctx.state.usage_type.clone(); - let ty = ty.or_else(|| ctx.span_tree_node.kind.tp().map(|t| crate::Type(t.into()))); + let ty = ctx.info.usage_type.clone(); + let ty = ty.or_else(|| ctx.span_node.kind.tp().map(|t| crate::Type(t.into()))); let color = crate::type_coloring::compute_for_code(ty.as_ref(), ctx.styles()); ColorState::FromType(color) }; @@ -115,7 +115,7 @@ impl super::SpanWidget for Widget { input.content.emit(content); input.text_color.emit(color_state); input.text_weight(text_weight); - input.crumbs.emit(ctx.span_tree_node.crumbs.clone()); + input.crumbs.emit(ctx.span_node.crumbs.clone()); } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index bf89332e2d25..404cd8dd5077 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -262,29 +262,31 @@ impl super::SpanWidget for Widget { fn configure(&mut self, config: &Config, mut ctx: super::ConfigContext) { let input = &self.config_frp.public.input; - let has_value = !ctx.span_tree_node.is_insertion_point(); + let has_value = !ctx.span_node.is_insertion_point(); let current_value: Option = - has_value.then(|| ctx.expression_at(ctx.span_tree_node.span()).into()); + has_value.then(|| ctx.expression_at(ctx.span_node.span()).into()); - input.current_crumbs(ctx.span_tree_node.crumbs.clone()); + input.current_crumbs(ctx.span_node.crumbs.clone()); input.current_value(current_value); input.set_entries(config.entries.clone()); - input.is_connected(ctx.state.subtree_connection.is_connected()); + input.is_connected(ctx.info.subtree_connection.is_some()); if has_value { ctx.modify_extension::(|ext| ext.bold = true); } - if ctx.span_tree_node.children.is_empty() { + + if ctx.span_node.children.is_empty() { + let child_level = ctx.info.nesting_level; let label_meta = super::Metadata::always(super::label::Config); - let child = - ctx.builder.child_widget_of_type(ctx.span_tree_node, ctx.state.depth, label_meta); + let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, label_meta); self.label_wrapper.replace_children(&[child]); } else { + let child_level = ctx.info.nesting_level.next(); let children = ctx - .span_tree_node + .span_node .children_iter() - .map(|child| ctx.builder.child_widget(child, ctx.state.depth + 1)) + .map(|child| ctx.builder.child_widget(child, child_level)) .collect_vec(); self.label_wrapper.replace_children(&children); } @@ -318,6 +320,7 @@ fn entry_for_current_value( Some(with_fallback) } +/// A wrapper for dropdown that only initializes it when it is first opened. #[derive(Debug)] struct LazyDropdown { app: ensogl::application::Application, diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 433a4d0642bf..262850c4c3f1 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -980,6 +980,18 @@ impl From for Type { } } +impl From<&String> for Type { + fn from(s: &String) -> Self { + Type(s.into()) + } +} + +impl From<&str> for Type { + fn from(s: &str) -> Self { + Type(s.into()) + } +} + impl Display for Type { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) @@ -1923,7 +1935,7 @@ impl GraphEditorModel { } if let Some(target) = edge.take_target() { - self.set_input_connected(&target, node::ConnectionStatus::Disconnected); + self.set_input_connected(&target, None); if let Some(target_node) = self.nodes.get_cloned_ref(&target.node_id) { target_node.in_edges.remove(&edge_id); } @@ -1931,7 +1943,7 @@ impl GraphEditorModel { } } - fn set_input_connected(&self, target: &EdgeEndpoint, status: node::ConnectionStatus) { + fn set_input_connected(&self, target: &EdgeEndpoint, status: Option) { if let Some(node) = self.nodes.get_cloned(&target.node_id) { node.view.set_input_connected(&target.port, status); } @@ -1955,10 +1967,7 @@ impl GraphEditorModel { status: bool, neutral_color: color::Lcha, ) { - let status = match status { - true => node::ConnectionStatus::connected(self.edge_color(edge_id, neutral_color)), - false => node::ConnectionStatus::Disconnected, - }; + let status = status.then(|| self.edge_color(edge_id, neutral_color)); self.set_input_connected(target, status); } @@ -2304,7 +2313,7 @@ impl GraphEditorModel { let color = self.edge_color(edge_id, neutral_color); edge.view.frp.set_color.emit(color); if let Some(target) = edge.target() { - self.set_input_connected(&target, node::ConnectionStatus::connected(color)); + self.set_input_connected(&target, Some(color)); } }; } @@ -4161,7 +4170,7 @@ mod tests { // 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_hover = node_2.model().input_port_shape().expect("No input port."); + let port_hover = node_2.model().input_port_hover_shape().expect("No input port."); // Input ports already use new event API. port_hover.emit_event(mouse::Down::default()); diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index 3a9524b85aaa..7ea135853e61 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -2025,15 +2025,10 @@ impl InstanceDef { } /// Replaces the parent binding with a new parent. - pub fn set_parent(&self, parent: &InstanceDef) { + fn set_parent(&self, parent: &InstanceDef) { parent.add_child(self); } - /// Checks if the provided object is a parent of the current one. - fn is_parent_of(&self, child: &InstanceDef) -> bool { - child.parent_bind.parent_and_child_index().map_or(false, |(parent, _)| &parent.def == self) - } - /// Removes the current parent binding. fn unset_parent(&self) { self.take_parent_bind(); @@ -2649,7 +2644,7 @@ pub trait LayoutOps: Object { self.display_object().def.layout.size.get() } - /// Get the margin of the object.P lease note that this is user-set margin, not the computed + /// Get the margin of the object. Please note that this is user-set margin, not the computed /// one. fn margin(&self) -> Vector2 { self.display_object().def.layout.margin.get() diff --git a/lib/rust/ensogl/core/src/display/scene.rs b/lib/rust/ensogl/core/src/display/scene.rs index e68f73dec7d3..220f0a13e75b 100644 --- a/lib/rust/ensogl/core/src/display/scene.rs +++ b/lib/rust/ensogl/core/src/display/scene.rs @@ -1215,10 +1215,10 @@ impl Scene { /// additional updates that affect the layout of display objects after the main scene layout /// has been performed. /// - /// During this phase, the layout updates can be observed using `on_updated` FRP events on each - /// individual display object. Any further updates to the scene may require the `update` - /// method to be manually called on affected objects in order to affect rendering during - /// this frame. + /// During this phase, the layout updates can be observed using `on_transformed` FRP events on + /// each individual display object. Any further updates to the scene may require the `update` + /// method to be manually called on affected objects in order to affect rendering + /// during this frame. #[profile(Debug)] pub fn early_update(&self, time: animation::TimeInfo) -> UpdateStatus { if self.context.borrow().is_some() { From a05209b45556c5eef6a43ca650a52b736bd7bfff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Fri, 21 Apr 2023 19:50:26 +0200 Subject: [PATCH 31/45] rename widget updates to configuration --- app/gui/src/controller/graph/widget.rs | 48 +++++++-------- .../src/controller/graph/widget/metadata.rs | 23 +++++--- .../src/controller/graph/widget/response.rs | 22 +++---- app/gui/src/presenter/graph.rs | 12 ++-- .../view/graph-editor/src/component/node.rs | 4 +- .../src/component/node/input/area.rs | 18 +++--- .../src/component/node/input/port.rs | 2 +- .../src/component/node/input/widget.rs | 59 ++++++++++--------- .../{vector_editor.rs => list_editor.rs} | 4 +- app/gui/view/graph-editor/src/lib.rs | 19 +++--- 10 files changed, 113 insertions(+), 98 deletions(-) rename app/gui/view/graph-editor/src/component/node/input/widget/{vector_editor.rs => list_editor.rs} (97%) diff --git a/app/gui/src/controller/graph/widget.rs b/app/gui/src/controller/graph/widget.rs index e965a78a25d5..c39d8f6decda 100644 --- a/app/gui/src/controller/graph/widget.rs +++ b/app/gui/src/controller/graph/widget.rs @@ -21,8 +21,8 @@ use ensogl::define_endpoints_2; use ide_view::graph_editor::component::visualization; use ide_view::graph_editor::component::visualization::Metadata; use ide_view::graph_editor::data::enso::Code; -use ide_view::graph_editor::WidgetUpdate; -use ide_view::graph_editor::WidgetUpdates; +use ide_view::graph_editor::ArgumentWidgetConfig; +use ide_view::graph_editor::CallWidgetsConfig; @@ -63,7 +63,7 @@ define_endpoints_2! { } Output { /// Emitted when the node's visualization has been set. - widget_data(NodeId, WidgetUpdates), + widget_data(NodeId, CallWidgetsConfig), } } @@ -148,7 +148,7 @@ impl Model { fn handle_notification( &mut self, notification: Notification, - ) -> Option<(NodeId, WidgetUpdates)> { + ) -> Option<(NodeId, CallWidgetsConfig)> { let report_error = |message, error| { error!("{message}: {error}"); None @@ -171,26 +171,26 @@ impl Model { &mut self, target: ast::Id, data: VisualizationUpdateData, - ) -> Option<(NodeId, WidgetUpdates)> { + ) -> Option<(NodeId, CallWidgetsConfig)> { let query_data = self.widget_queries.get_mut(&target)?; - let (updates, errors) = metadata::deserialize_widget_update(&data); + let (definitions, errors) = metadata::deserialize_widget_definitions(&data); for error in errors { error!("{:?}", error); } - trace!("Widget updates: {updates:?}"); - let updates = Rc::new(updates); - query_data.last_updates = Some(updates.clone()); + trace!("Widget definitions: {definitions:?}"); + let definitions = Rc::new(definitions); + query_data.last_definitions = Some(definitions.clone()); let call_id = query_data.call_expression; - Some((query_data.node_id, WidgetUpdates { call_id, updates })) + Some((query_data.node_id, CallWidgetsConfig { call_id, definitions })) } /// Handle a widget request from presenter. Returns the widget updates if the request can be /// immediately fulfilled from the cache. - fn request_widget(&mut self, request: &Request) -> Option<(NodeId, WidgetUpdates)> { + fn request_widget(&mut self, request: &Request) -> Option<(NodeId, CallWidgetsConfig)> { let suggestion_db = self.graph.suggestion_db(); let suggestion = suggestion_db.lookup(request.call_suggestion).ok()?; @@ -216,7 +216,7 @@ impl Model { // the last known visualization data. Each widget request needs to be responded // to, otherwise the widget might not be displayed after the widget view has // been temporarily removed and created again. - query.last_updates() + query.last_definitions() } } Entry::Vacant(vacant) => { @@ -328,11 +328,11 @@ pub struct Request { /// and maintains enough data to correlate the response with respective widget view. #[derive(Debug)] struct QueryData { - node_id: NodeId, - call_expression: ExpressionId, - method_name: ImString, - arguments: Vec, - last_updates: Option>>, + node_id: NodeId, + call_expression: ExpressionId, + method_name: ImString, + arguments: Vec, + last_definitions: Option>>, } impl QueryData { @@ -341,8 +341,8 @@ impl QueryData { let arguments = suggestion.arguments.iter().map(|arg| arg.name.clone().into()).collect(); let method_name = suggestion.name.clone().into(); let call_expression = req.call_expression; - let last_updates = None; - QueryData { node_id, arguments, method_name, call_expression, last_updates } + let last_definitions = None; + QueryData { node_id, arguments, method_name, call_expression, last_definitions } } /// Update existing query data on new request. Returns true if the visualization query needs to @@ -369,18 +369,18 @@ impl QueryData { visualization_modified } - fn last_updates(&self) -> Option<(NodeId, WidgetUpdates)> { - self.last_updates.as_ref().map(|updates| { + fn last_definitions(&self) -> Option<(NodeId, CallWidgetsConfig)> { + self.last_definitions.as_ref().map(|definitions| { let call_id = self.call_expression; - let updates = WidgetUpdates { call_id, updates: updates.clone() }; - (self.node_id, updates) + let config = CallWidgetsConfig { call_id, definitions: definitions.clone() }; + (self.node_id, config) }) } fn request_visualization(&mut self, manager: &Rc, target_expression: ast::Id) { // When visualization is requested, remove stale queried value to prevent updates while // language server request is pending. - self.last_updates.take(); + self.last_definitions.take(); let vis_metadata = self.visualization_metadata(); manager.request_visualization(target_expression, vis_metadata); } diff --git a/app/gui/src/controller/graph/widget/metadata.rs b/app/gui/src/controller/graph/widget/metadata.rs index ef7b263e11fb..1257b4f96697 100644 --- a/app/gui/src/controller/graph/widget/metadata.rs +++ b/app/gui/src/controller/graph/widget/metadata.rs @@ -7,19 +7,24 @@ use crate::model::execution_context::VisualizationUpdateData; use super::response; use ide_view::graph_editor::component::node::input::widget; -use ide_view::graph_editor::WidgetUpdate; +use ide_view::graph_editor::ArgumentWidgetConfig; -/// Deserialize a list of widget configurations from visualization update data. Allows for partial + +/// ===================================== +/// == deserialize_widget_definitions === +/// ===================================== + +/// Deserialize a list of widget configurations from visualization update data. Allows for partial /// deserialization: if any of the widget configurations fails to deserialize, it will be skipped, /// but the deserialization will continue. All errors are returned as a separate list. -pub fn deserialize_widget_update( +pub fn deserialize_widget_definitions( data: &VisualizationUpdateData, -) -> (Vec, Vec) { +) -> (Vec, Vec) { match serde_json::from_slice::(data) { Ok(response) => { let updates = response.into_iter().map( - |(argument_name, fallable_widget)| -> FallibleResult { + |(argument_name, fallable_widget)| -> FallibleResult { let widget: Option = fallable_widget.widget.map_err(|e| { let msg = "Failed to deserialize widget data for argument"; @@ -27,7 +32,7 @@ pub fn deserialize_widget_update( })?; let meta = widget.map(to_metadata); let argument_name = argument_name.to_owned(); - Ok(WidgetUpdate { argument_name, meta }) + Ok(ArgumentWidgetConfig { argument_name, meta }) }, ); @@ -41,6 +46,10 @@ pub fn deserialize_widget_update( } } +/// == Conversion to Widget Metadata IDE structs === + +/// Convert a widget definition from the engine response into a IDE internal widget metadata struct. +/// See [`widget::Metadata`] for more information. fn to_metadata(resp: response::WidgetDefinition) -> widget::Metadata { widget::Metadata { display: resp.display, config: to_config(resp.inner), has_port: true } } @@ -54,7 +63,7 @@ fn to_config(inner: response::WidgetKindConfiguration) -> widget::Config { } .into(), response::WidgetKindConfiguration::ListEditor { item_editor, item_default } => - widget::vector_editor::Config { + widget::list_editor::Config { item_editor: Some(Rc::new(to_metadata(*item_editor))), item_default: item_default.into(), } diff --git a/app/gui/src/controller/graph/widget/response.rs b/app/gui/src/controller/graph/widget/response.rs index 9ac264f8499f..bbd27d4be71a 100644 --- a/app/gui/src/controller/graph/widget/response.rs +++ b/app/gui/src/controller/graph/widget/response.rs @@ -7,24 +7,24 @@ use ide_view::graph_editor::component::node::input::widget; -/// ===================== -/// === WidgetUpdates === -/// ===================== +/// ========================= +/// === WidgetDefinitions === +/// ========================= /// A top level object received from the widget visualization, which contains widget definitions for /// all arguments of a single Enso method. Configurations are paired with the name of function /// argument they are associated with. -pub(super) type WidgetDefinitions<'a> = Vec<(&'a str, FallableWidgetData<'a>)>; +pub(super) type WidgetDefinitions<'a> = Vec<(&'a str, FallableWidgetDefinition<'a>)>; -/// A wrapper type that allows deserialization of a widget definitions to partially fail: failures -/// message of individual widget definition deserialization will be preserved, and deserialization +/// A wrapper type that allows deserialization of a widget definitions to partially fail: failure +/// message of individual widget definition deserialization will be preserved and deserialization /// will continue. #[derive(Debug)] -pub(super) struct FallableWidgetData<'a> { +pub(super) struct FallableWidgetDefinition<'a> { pub(super) widget: FallibleResult>>, } -impl<'de: 'a, 'a> serde::Deserialize<'de> for FallableWidgetData<'a> { +impl<'de: 'a, 'a> serde::Deserialize<'de> for FallableWidgetDefinition<'a> { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de> { let widget = >::deserialize(deserializer) @@ -35,9 +35,9 @@ impl<'de: 'a, 'a> serde::Deserialize<'de> for FallableWidgetData<'a> { -/// ============== -/// === Widget === -/// ============== +/// ======================== +/// === WidgetDefinition === +/// ======================== /// Widget definition provided from the engine. It is used to define how to display a widget of /// particular argument expression. When not provided, the default widget will be chosen based on diff --git a/app/gui/src/presenter/graph.rs b/app/gui/src/presenter/graph.rs index f8cad3e1e11c..5f901dda1cc0 100644 --- a/app/gui/src/presenter/graph.rs +++ b/app/gui/src/presenter/graph.rs @@ -19,8 +19,8 @@ use ide_view as view; use ide_view::graph_editor::component::node as node_view; use ide_view::graph_editor::component::visualization as visualization_view; use ide_view::graph_editor::EdgeEndpoint; +use view::graph_editor::CallWidgetsConfig; use view::graph_editor::ExecutionEnvironment; -use view::graph_editor::WidgetUpdates; // ============== @@ -264,13 +264,13 @@ impl Model { } /// Map widget controller update data to the node views. - fn map_widget_update_data( + fn map_widget_configuration( &self, node_id: AstNodeId, - updates: WidgetUpdates, - ) -> Option<(ViewNodeId, WidgetUpdates)> { + config: CallWidgetsConfig, + ) -> Option<(ViewNodeId, CallWidgetsConfig)> { let node_id = self.state.view_id_of_ast_node(node_id)?; - Some((node_id, updates)) + Some((node_id, config)) } /// Node was removed in view. @@ -835,7 +835,7 @@ impl Graph { widget.request_widgets <+ widget_request; widget.retain_node_expressions <+ widget_refresh._0().unwrap(); view.update_node_widgets <+ widget.widget_data.filter_map( - f!(((id, updates)) model.map_widget_update_data(*id, updates.clone())) + f!(((id, data)) model.map_widget_configuration(*id, data.clone())) ); } } diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index 79116974606c..0559d75bb37d 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -11,9 +11,9 @@ use crate::component::visualization; use crate::selection::BoundingBox; use crate::tooltip; use crate::view; +use crate::CallWidgetsConfig; use crate::ExecutionEnvironment; use crate::Type; -use crate::WidgetUpdates; use super::edge; use enso_frp as frp; @@ -317,7 +317,7 @@ ensogl::define_endpoints_2! { /// `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 (ast::Id, Option), - update_widgets (WidgetUpdates), + update_widgets (CallWidgetsConfig), set_output_expression_visibility (bool), set_vcs_status (Option), /// Show visualization preview until either editing of the node is finished or the diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 46b8582631fc..9cf29df94008 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -11,8 +11,8 @@ use crate::node::input::widget; use crate::node::input::widget::MetadataPointer; use crate::node::profiling; use crate::view; +use crate::CallWidgetsConfig; use crate::Type; -use crate::WidgetUpdates; use enso_frp as frp; use enso_frp; @@ -238,13 +238,13 @@ impl Model { Some(cursor::Style::new_highlight(display_object, size, radius, color)) } - /// Apply widget updates to widgets in this input area. - fn apply_widget_updates(&self, updates: &WidgetUpdates) { - let WidgetUpdates { call_id, updates } = updates; - for update in updates.iter() { - let argument_name = update.argument_name.clone().into(); + /// Apply widget configuration to widgets in this input area. + fn set_widget_configuration(&self, config: &CallWidgetsConfig) { + let CallWidgetsConfig { call_id, definitions } = config; + for definition in definitions.iter() { + let argument_name = definition.argument_name.clone().into(); let meta_pointer = MetadataPointer { call_id: *call_id, argument_name }; - self.widget_tree.set_metadata(meta_pointer, update.meta.clone()); + self.widget_tree.set_metadata(meta_pointer, definition.meta.clone()); } } @@ -328,7 +328,7 @@ ensogl::define_endpoints! { set_connected (Crumbs, Option), /// Update widget metadata for widgets already present in this input area. - update_widgets (WidgetUpdates), + update_widgets (CallWidgetsConfig), /// Enable / disable port hovering. The optional type indicates the type of the active edge /// if any. It is used to highlight ports if they are missing type information or if their @@ -514,7 +514,7 @@ impl Area { // === Widgets === - eval frp.update_widgets((a) model.apply_widget_updates(a)); + eval frp.update_widgets((a) model.set_widget_configuration(a)); eval frp.set_connected(((crumbs,status)) model.set_connected(crumbs,*status)); eval frp.set_expression_usage_type(((id,tp)) model.set_expression_usage_type(*id,tp.clone())); eval frp.set_disabled ((disabled) model.widget_tree.set_disabled(*disabled)); diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index cda6ede32100..2bdeaa7fd880 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -181,7 +181,7 @@ impl Port { f!([crumbs](t) Switch::new(crumbs.borrow().clone(), *t)) ); - frp.on_port_press <+ mouse_down.map(f!((_) crumbs.borrow().clone())); + frp.on_port_press <+ mouse_down.map(f_!(crumbs.borrow().clone())); eval frp.set_ports_visible([port_root_weak, hover_shape] (active) { if let Some(port_root) = port_root_weak.upgrade() { if *active { diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 0d0df1b38cf4..e4032a9a6626 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -174,7 +174,7 @@ define_widget_modules! { /// A widget for selecting a single value from a list of available options. SingleChoice single_choice, /// A widget for managing a list of values - adding, removing or reordering them. - VectorEditor vector_editor, + ListEditor list_editor, /// Default span tree traversal widget. Hierarchy hierarchy, } @@ -197,27 +197,9 @@ pub struct Metadata { } impl Metadata { - const fn always(config: C) -> Self - where C: ~const Into { - Self { display: Display::Always, config: config.into(), has_port: true } - } - - const fn inert(config: C) -> Self - where C: ~const Into { - Self { display: Display::Always, config: config.into(), has_port: false } - } - - /// Widget metadata for static dropdown, based on the tag values provided by suggestion - /// database. - fn static_dropdown(label: Option, tag_values: &[span_tree::TagValue]) -> Metadata { - let entries = Rc::new(tag_values.iter().map(Entry::from).collect()); - Self::always(single_choice::Config { label, entries }) - } - - fn vector_editor() -> Metadata { - Self::always(vector_editor::Config { item_editor: None, item_default: "_".into() }) - } - + /// Derive widget metadata from the expression, node data in span tree and inferred value type. + /// When no metadata is provided by language server, the metadata returned from this function + /// will be used to create a default widget. fn from_node(span_node: &SpanRef, usage_type: Option, expression: &str) -> Self { use span_tree::node::Kind; @@ -225,7 +207,7 @@ impl Metadata { let has_children = !span_node.children.is_empty(); const VECTOR_TYPE: &str = "Standard.Base.Data.Vector.Vector"; - let is_array_enabled = ARGS.groups.feature_preview.options.vector_editor.value; + let is_list_editor_enabled = ARGS.groups.feature_preview.options.vector_editor.value; let is_vector = |arg: &span_tree::node::Argument| { let type_matches = usage_type .as_ref() @@ -243,7 +225,7 @@ impl Metadata { match kind { Kind::Argument(arg) if !arg.tag_values.is_empty() => Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values), - Kind::Argument(arg) if is_array_enabled && is_vector(arg) => Self::vector_editor(), + Kind::Argument(arg) if is_list_editor_enabled && is_vector(arg) => Self::list_editor(), Kind::InsertionPoint(arg) if arg.kind.is_expected_argument() => if !arg.tag_values.is_empty() { Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values) @@ -257,6 +239,27 @@ impl Metadata { _ => Self::always(label::Config::default()), } } + + const fn always(config: C) -> Self + where C: ~const Into { + Self { display: Display::Always, config: config.into(), has_port: true } + } + + const fn inert(config: C) -> Self + where C: ~const Into { + Self { display: Display::Always, config: config.into(), has_port: false } + } + + /// Widget metadata for static dropdown, based on the tag values provided by suggestion + /// database. + fn static_dropdown(label: Option, tag_values: &[span_tree::TagValue]) -> Metadata { + let entries = Rc::new(tag_values.iter().map(Entry::from).collect()); + Self::always(single_choice::Config { label, entries }) + } + + fn list_editor() -> Metadata { + Self::always(list_editor::Config { item_editor: None, item_default: "_".into() }) + } } /// Widget display mode. Determines when the widget should be expanded. @@ -342,9 +345,9 @@ pub struct WidgetsFrp { -// ============== -// === Widget === -// ============== +// ============ +// === Tree === +// ============ /// The node widget tree view. Contains all widgets created from the node's span tree, as well as /// all input ports of a node. The tree is initialized to empty state, waiting for first @@ -559,6 +562,8 @@ pub(super) struct ConnectionData { pub depth: usize, } + + /// ================= /// === TreeModel === /// ================= diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs b/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs similarity index 97% rename from app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs rename to app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs index 39ff4e518070..0cf46c97d167 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/vector_editor.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs @@ -1,7 +1,7 @@ -//! Module dedicated to the Vector Editor widget. The main structure is [`Model`] which is one of +//! Module dedicated to the List Editor widget. The main structure is [`Model`] which is one of //! the [KindModel](crate::component::node::widget::KindModel) variants. //! -//! Currently the view is a simle [`Elements`] component, which will be replaced with a rich +//! Currently the view is a simple [`Elements`] component, which will be replaced with a rich //! view in [future tasks](https://github.com/enso-org/enso/issues/5631). use crate::prelude::*; diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 262850c4c3f1..a4e4eb867df4 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -637,7 +637,7 @@ ensogl::define_endpoints_2! { set_node_comment ((NodeId,node::Comment)), set_node_position ((NodeId,Vector2)), set_expression_usage_type ((NodeId,ast::Id,Option)), - update_node_widgets ((NodeId,WidgetUpdates)), + update_node_widgets ((NodeId,CallWidgetsConfig)), cycle_visualization (NodeId), set_visualization ((NodeId, Option)), register_visualization (Option), @@ -1120,21 +1120,22 @@ impl Grid { // === WidgetUpdates === // ===================== -/// A structure describing a widget update batch for arguments of single function call. +/// Configuration for widgets of arguments at function call Enso expression. #[derive(Debug, Default, Clone)] -pub struct WidgetUpdates { +pub struct CallWidgetsConfig { /// The function call expression ID. - pub call_id: ast::Id, - /// Update of a widget for each function argument. - pub updates: Rc>, + pub call_id: ast::Id, + /// Configuration of a widget for each function argument. + pub definitions: Rc>, } /// A structure describing a widget update for specific argument of a function call. #[derive(Debug)] -pub struct WidgetUpdate { +pub struct ArgumentWidgetConfig { /// The function argument name that this widget is for. pub argument_name: String, - /// Widget metadata queried from the language server. + /// Widget metadata queried from the language server. When this is `None`, the widget metadata + /// should be inferred automatically. pub meta: Option, } @@ -2272,7 +2273,7 @@ impl GraphEditorModel { } } - fn update_node_widgets(&self, node_id: NodeId, updates: &WidgetUpdates) { + fn update_node_widgets(&self, node_id: NodeId, updates: &CallWidgetsConfig) { if let Some(node) = self.nodes.get_cloned_ref(&node_id) { node.view.update_widgets.emit(updates.clone()); } From 907ea2c43d80d2e54c986850de161143f026cab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Fri, 21 Apr 2023 19:53:17 +0200 Subject: [PATCH 32/45] configuration -> definition --- app/gui/src/controller/graph/widget/metadata.rs | 6 +++--- app/gui/src/controller/graph/widget/response.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/gui/src/controller/graph/widget/metadata.rs b/app/gui/src/controller/graph/widget/metadata.rs index 1257b4f96697..cb4e59ca3ba2 100644 --- a/app/gui/src/controller/graph/widget/metadata.rs +++ b/app/gui/src/controller/graph/widget/metadata.rs @@ -54,15 +54,15 @@ fn to_metadata(resp: response::WidgetDefinition) -> widget::Metadata { widget::Metadata { display: resp.display, config: to_config(resp.inner), has_port: true } } -fn to_config(inner: response::WidgetKindConfiguration) -> widget::Config { +fn to_config(inner: response::WidgetKindDefinition) -> widget::Config { match inner { - response::WidgetKindConfiguration::SingleChoice { label, values } => + response::WidgetKindDefinition::SingleChoice { label, values } => widget::single_choice::Config { label: label.map(Into::into), entries: Rc::new(to_entries(&values)), } .into(), - response::WidgetKindConfiguration::ListEditor { item_editor, item_default } => + response::WidgetKindDefinition::ListEditor { item_editor, item_default } => widget::list_editor::Config { item_editor: Some(Rc::new(to_metadata(*item_editor))), item_default: item_default.into(), diff --git a/app/gui/src/controller/graph/widget/response.rs b/app/gui/src/controller/graph/widget/response.rs index bbd27d4be71a..4d35b1198df4 100644 --- a/app/gui/src/controller/graph/widget/response.rs +++ b/app/gui/src/controller/graph/widget/response.rs @@ -52,13 +52,13 @@ pub(super) struct WidgetDefinition<'a> { #[serde(default)] pub display: widget::Display, #[serde(borrow, flatten)] - pub inner: WidgetKindConfiguration<'a>, + pub inner: WidgetKindDefinition<'a>, } -/// Part of [`WidgetConfiguration`] that is dependant on widget kind. +/// Part of [`WidgetDefinition`] that is dependant on widget kind. #[derive(Debug, serde::Deserialize)] #[serde(tag = "constructor")] -pub(super) enum WidgetKindConfiguration<'a> { +pub(super) enum WidgetKindDefinition<'a> { /// A single value widget (dropdown). #[serde(rename = "Single_Choice")] SingleChoice { @@ -103,7 +103,7 @@ pub(super) enum WidgetKindConfiguration<'a> { #[serde(rename = "Text_Input")] TextInput, - /// Describes a folder chooser. + /// A folder chooser. #[serde(rename = "Folder_Browse")] FolderBrowse, From 8b9a21973a21cf992ec2a2a4a5bcd9bca045719b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Fri, 21 Apr 2023 20:25:39 +0200 Subject: [PATCH 33/45] widget metadata -> configuration --- app/gui/src/controller/graph/widget.rs | 12 +- .../widget/{metadata.rs => configuration.rs} | 18 +-- .../src/controller/graph/widget/response.rs | 2 +- .../src/component/node/input/area.rs | 17 +- .../src/component/node/input/port.rs | 8 +- .../src/component/node/input/widget.rs | 152 +++++++++--------- .../node/input/widget/list_editor.rs | 2 +- .../node/input/widget/single_choice.rs | 2 +- app/gui/view/graph-editor/src/lib.rs | 6 +- 9 files changed, 115 insertions(+), 104 deletions(-) rename app/gui/src/controller/graph/widget/{metadata.rs => configuration.rs} (82%) diff --git a/app/gui/src/controller/graph/widget.rs b/app/gui/src/controller/graph/widget.rs index c39d8f6decda..8ac56d40353d 100644 --- a/app/gui/src/controller/graph/widget.rs +++ b/app/gui/src/controller/graph/widget.rs @@ -1,11 +1,11 @@ //! Widget controller. //! //! The Widget Controller is responsible for querying the language server for information about -//! the node's widget metadata or resolving it from local cache. +//! the node's widget configuration or resolving it from local cache. -mod metadata; +mod configuration; mod response; use crate::prelude::*; @@ -67,11 +67,11 @@ define_endpoints_2! { } } -/// Graph widgets controller. Handles requests for widget metadata using visualizations. Maps +/// Graph widgets controller. Handles requests for widget configuration using visualizations. Maps /// response data to the relevant node Id updates, and dispatches them over the FRP output. /// Guarantees that each individual query eventually receives an update. It internally caches the -/// results of the last queries, so that the metadata can be delivered to the presenter even when no -/// visualization change is necessary. +/// results of the last queries, so that the configuration can be delivered to the presenter even +/// when no visualization change is necessary. #[derive(Debug, Deref)] pub struct Controller { #[deref] @@ -174,7 +174,7 @@ impl Model { ) -> Option<(NodeId, CallWidgetsConfig)> { let query_data = self.widget_queries.get_mut(&target)?; - let (definitions, errors) = metadata::deserialize_widget_definitions(&data); + let (definitions, errors) = configuration::deserialize_widget_definitions(&data); for error in errors { error!("{:?}", error); diff --git a/app/gui/src/controller/graph/widget/metadata.rs b/app/gui/src/controller/graph/widget/configuration.rs similarity index 82% rename from app/gui/src/controller/graph/widget/metadata.rs rename to app/gui/src/controller/graph/widget/configuration.rs index cb4e59ca3ba2..f868b4a46214 100644 --- a/app/gui/src/controller/graph/widget/metadata.rs +++ b/app/gui/src/controller/graph/widget/configuration.rs @@ -30,7 +30,7 @@ pub fn deserialize_widget_definitions( let msg = "Failed to deserialize widget data for argument"; e.context(format!("{msg} '{argument_name}'")) })?; - let meta = widget.map(to_metadata); + let meta = widget.map(to_configuration); let argument_name = argument_name.to_owned(); Ok(ArgumentWidgetConfig { argument_name, meta }) }, @@ -46,15 +46,15 @@ pub fn deserialize_widget_definitions( } } -/// == Conversion to Widget Metadata IDE structs === +/// == Conversion to Widget Configuration IDE structs === -/// Convert a widget definition from the engine response into a IDE internal widget metadata struct. -/// See [`widget::Metadata`] for more information. -fn to_metadata(resp: response::WidgetDefinition) -> widget::Metadata { - widget::Metadata { display: resp.display, config: to_config(resp.inner), has_port: true } +/// Convert a widget definition from the engine response into a IDE internal widget configuration +/// struct. See [`widget::Configuration`] for more information. +fn to_configuration(resp: response::WidgetDefinition) -> widget::Configuration { + widget::Configuration { display: resp.display, kind: to_kind(resp.inner), has_port: true } } -fn to_config(inner: response::WidgetKindDefinition) -> widget::Config { +fn to_kind(inner: response::WidgetKindDefinition) -> widget::DynConfig { match inner { response::WidgetKindDefinition::SingleChoice { label, values } => widget::single_choice::Config { @@ -62,9 +62,9 @@ fn to_config(inner: response::WidgetKindDefinition) -> widget::Config { entries: Rc::new(to_entries(&values)), } .into(), - response::WidgetKindDefinition::ListEditor { item_editor, item_default } => + response::WidgetKindDefinition::ListEditor { item_widget, item_default } => widget::list_editor::Config { - item_editor: Some(Rc::new(to_metadata(*item_editor))), + item_widget: Some(Rc::new(to_configuration(*item_widget))), item_default: item_default.into(), } .into(), diff --git a/app/gui/src/controller/graph/widget/response.rs b/app/gui/src/controller/graph/widget/response.rs index 4d35b1198df4..bcd1f1a829a2 100644 --- a/app/gui/src/controller/graph/widget/response.rs +++ b/app/gui/src/controller/graph/widget/response.rs @@ -77,7 +77,7 @@ pub(super) enum WidgetKindDefinition<'a> { ListEditor { /// The widget to use for editing the items. #[serde(borrow, alias = "item_editor")] - item_editor: Box>, + item_widget: Box>, /// The default value for new items inserted when the user adds a new element. #[serde(borrow)] item_default: &'a str, diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 9cf29df94008..d9300bb8a85a 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -8,7 +8,7 @@ use ensogl::display::traits::*; use crate::component::type_coloring; use crate::node; use crate::node::input::widget; -use crate::node::input::widget::MetadataPointer; +use crate::node::input::widget::OverrideKey; use crate::node::profiling; use crate::view; use crate::CallWidgetsConfig; @@ -238,13 +238,16 @@ impl Model { Some(cursor::Style::new_highlight(display_object, size, radius, color)) } - /// Apply widget configuration to widgets in this input area. - fn set_widget_configuration(&self, config: &CallWidgetsConfig) { + /// Configure widgets associated with single Enso call expression, overriding default widgets + /// generated from span tree. The provided widget configuration is merged with configurations + /// already present in the widget tree. Setting a widget configuration to `None` will remove + /// an override, and a default widget will be used. + fn apply_widget_configuration(&self, config: &CallWidgetsConfig) { let CallWidgetsConfig { call_id, definitions } = config; for definition in definitions.iter() { let argument_name = definition.argument_name.clone().into(); - let meta_pointer = MetadataPointer { call_id: *call_id, argument_name }; - self.widget_tree.set_metadata(meta_pointer, definition.meta.clone()); + let override_key = OverrideKey { call_id: *call_id, argument_name }; + self.widget_tree.set_config_override(override_key, definition.meta.clone()); } } @@ -327,7 +330,7 @@ ensogl::define_endpoints! { /// contains the color of connected edge. set_connected (Crumbs, Option), - /// Update widget metadata for widgets already present in this input area. + /// Update widget configuration for widgets already present in this input area. update_widgets (CallWidgetsConfig), /// Enable / disable port hovering. The optional type indicates the type of the active edge @@ -514,7 +517,7 @@ impl Area { // === Widgets === - eval frp.update_widgets((a) model.set_widget_configuration(a)); + eval frp.update_widgets((a) model.apply_widget_configuration(a)); eval frp.set_connected(((crumbs,status)) model.set_connected(crumbs,*status)); eval frp.set_expression_usage_type(((id,tp)) model.set_expression_usage_type(*id,tp.clone())); eval frp.set_disabled ((disabled) model.widget_tree.set_disabled(*disabled)); diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index 2bdeaa7fd880..a151cbeaea43 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -2,10 +2,10 @@ use crate::prelude::*; -use crate::component::node::input::widget::Config; use crate::component::node::input::widget::ConfigContext; -use crate::component::node::input::widget::ConnectionData; +use crate::component::node::input::widget::DynConfig; use crate::component::node::input::widget::DynWidget; +use crate::component::node::input::widget::EdgeData; use crate::component::node::input::widget::SpanWidget; use crate::component::node::input::widget::WidgetsFrp; @@ -213,7 +213,7 @@ impl Port { } /// Configure the port and its attached widget. - pub fn configure(&mut self, config: &Config, ctx: ConfigContext) { + pub fn configure(&mut self, config: &DynConfig, ctx: ConfigContext) { self.crumbs.replace(ctx.span_node.crumbs.clone()); self.set_connected(ctx.info.connection); self.set_port_layout(&ctx); @@ -223,7 +223,7 @@ impl Port { /// Update connection status of this port. Changing the connection status will add or remove the /// port's visible shape from the display hierarchy. - fn set_connected(&self, status: Option) { + fn set_connected(&self, status: Option) { match status { Some(data) => { self.port_root.add_child(&self.port_shape); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index e4032a9a6626..bb69d6155496 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -63,10 +63,10 @@ ensogl::define_endpoints_2! { } } -/// Information associated with widget metadata which describes which uniquely identifies the -/// widget to reconfigure. +/// A key used for overriding widget configuration. Allows locating the widget that should be +/// configured using provided external data. #[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] -pub struct MetadataPointer { +pub struct OverrideKey { /// The function call associated with the widget. pub call_id: ast::Id, /// The name of function argument at which the widget is located. @@ -90,6 +90,7 @@ pub trait SpanWidget { fn configure(&mut self, config: &Self::Config, ctx: ConfigContext); } + /// Generate implementation for [`DynWidget`] enum and its associated [`Config`] enum. Those enums /// are used to represent any possible widget kind and its configuration. macro_rules! define_widget_modules( @@ -102,14 +103,14 @@ macro_rules! define_widget_modules( /// A widget configuration that determines the widget kind. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] - pub enum Config { + pub enum DynConfig { $($name(<$module::Widget as SpanWidget>::Config),)* } /// The node widget view. Represents one widget of any kind on the node input area. Can - /// change its appearance and behavior depending on the widget metadata updates, without - /// being recreated. New widget can be created using the `new` method, while the existing - /// widget can be reconfigured using the `configure` method. + /// change its appearance and behavior depending on the widget configuration updates, + /// without being recreated. New widget can be created using the `new` method, while the + /// existing widget can be reconfigured using the `configure` method. /// /// When a new configuration is applied, the existing widget will handle the update using /// its `configure` method. If the new configuration requires a different widget kind, the @@ -124,7 +125,7 @@ macro_rules! define_widget_modules( } $( - impl const From<<$module::Widget as SpanWidget>::Config> for Config { + impl const From<<$module::Widget as SpanWidget>::Config> for DynConfig { fn from(config: <$module::Widget as SpanWidget>::Config) -> Self { Self::$name(config) } @@ -138,22 +139,22 @@ macro_rules! define_widget_modules( )* impl SpanWidget for DynWidget { - type Config = Config; + type Config = DynConfig; fn root_object(&self) -> &display::object::Instance { match self { $(DynWidget::$name(inner) => inner.root_object(),)* } } - fn new(config: &Config, ctx: &ConfigContext) -> Self { + fn new(config: &DynConfig, ctx: &ConfigContext) -> Self { match config { - $(Config::$name(config) => DynWidget::$name(SpanWidget::new(config, ctx)),)* + $(DynConfig::$name(config) => DynWidget::$name(SpanWidget::new(config, ctx)),)* } } - fn configure(&mut self, config: &Config, ctx: ConfigContext) { + fn configure(&mut self, config: &DynConfig, ctx: ConfigContext) { match (self, config) { - $((DynWidget::$name(model), Config::$name(config)) => { + $((DynWidget::$name(model), DynConfig::$name(config)) => { SpanWidget::configure(model, config, ctx); },)* (this, _) => { @@ -189,17 +190,22 @@ define_widget_modules! { /// indicate that they are connected. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] -pub struct Metadata { - /// The placeholder text value. By default, the parameter name is used. +pub struct Configuration { + /// Display mode of the widget: determines whether or not the widget should be displayed + /// depending on current tree display mode. pub display: Display, - pub config: Config, + /// Whether or not the widget can receive a port. If `true`, the widget can be wrapped in a + /// [`Port`] struct, but it is not guaranteed. If multiple widgets created at single span node + /// declare themselves as wanting a port, only one of them will actually have one. pub has_port: bool, + /// Configuration specific to given widget kind. + pub kind: DynConfig, } -impl Metadata { - /// Derive widget metadata from the expression, node data in span tree and inferred value type. - /// When no metadata is provided by language server, the metadata returned from this function - /// will be used to create a default widget. +impl Configuration { + /// Derive widget configuration from the expression, node data in span tree and inferred value + /// type. When no metadata is provided by language server, the metadata returned from this + /// function will be used to create a default widget. fn from_node(span_node: &SpanRef, usage_type: Option, expression: &str) -> Self { use span_tree::node::Kind; @@ -240,25 +246,28 @@ impl Metadata { } } - const fn always(config: C) -> Self - where C: ~const Into { - Self { display: Display::Always, config: config.into(), has_port: true } + const fn always(kind: C) -> Self + where C: ~const Into { + Self { display: Display::Always, kind: kind.into(), has_port: true } } - const fn inert(config: C) -> Self - where C: ~const Into { - Self { display: Display::Always, config: config.into(), has_port: false } + const fn inert(kind: C) -> Self + where C: ~const Into { + Self { display: Display::Always, kind: kind.into(), has_port: false } } - /// Widget metadata for static dropdown, based on the tag values provided by suggestion + /// Widget configuration for static dropdown, based on the tag values provided by suggestion /// database. - fn static_dropdown(label: Option, tag_values: &[span_tree::TagValue]) -> Metadata { + fn static_dropdown( + label: Option, + tag_values: &[span_tree::TagValue], + ) -> Configuration { let entries = Rc::new(tag_values.iter().map(Entry::from).collect()); Self::always(single_choice::Config { label, entries }) } - fn list_editor() -> Metadata { - Self::always(list_editor::Config { item_editor: None, item_default: "_".into() }) + fn list_editor() -> Configuration { + Self::always(list_editor::Config { item_widget: None, item_default: "_".into() }) } } @@ -409,17 +418,17 @@ impl Tree { Self { frp, widgets_frp, model } } - /// Override widget metadata. The metadata is used to determine the widget appearance and - /// behavior. By default, the widget metadata will be inferred from its span tree kind and type. - /// However, in some cases, we want to change the selected widget for a given span tree node, - /// and it can be done by calling this method. The set metadata is persistent, and will be - /// applied to any future widget of this node that matches given pointer. - pub fn set_metadata(&self, pointer: MetadataPointer, meta: Option) { - self.notify_dirty(self.model.set_metadata(pointer, meta)); + /// Override widget configuration. The configuration is used to determine the widget appearance + /// and behavior. By default, the widget configuration will be inferred from its span tree kind + /// and type. However, in some cases, we want to change the selected widget for a given span + /// tree node, and it can be done by calling this method. The set metadata is persistent, and + /// will be applied to any future widget of this node that matches given pointer. + pub fn set_config_override(&self, pointer: OverrideKey, meta: Option) { + self.notify_dirty(self.model.set_config_override(pointer, meta)); } /// Set usage type for given AST node. The usage type is used to determine the widget appearance - /// and default inferred widget metadata. + /// and default inferred widget configuration. pub fn set_usage_type(&self, ast_id: ast::Id, usage_type: Option) { self.notify_dirty(self.model.set_usage_type(ast_id, usage_type)); } @@ -439,7 +448,7 @@ impl Tree { /// Rebuild tree if it has been marked as dirty. The dirty flag is marked whenever more data - /// external to the span-tree is provided, using `set_metadata`, `set_usage_type`, + /// external to the span-tree is provided, using `set_config_override`, `set_usage_type`, /// `set_connected` or `set_disabled` methods of the widget tree. pub fn rebuild_tree_if_dirty( &self, @@ -547,15 +556,15 @@ struct TreeEntry { -// ====================== -// === ConnectionData === -// ====================== +// ================ +// === EdgeData === +// ================ /// Data associated with an edge connected to a port in the tree. It is accessible to the connected /// port, its widget and all its descendants through `connection` and `subtree_connection` fields /// of [`NodeState`]. #[derive(Debug, Clone, Copy, PartialEq)] -pub(super) struct ConnectionData { +pub(super) struct EdgeData { /// Color of an edge connected to the port. pub color: color::Lcha, /// Span tree depth at which the connection is made. @@ -579,7 +588,7 @@ struct TreeModel { /// of a node. hierarchy: RefCell>, ports_map: RefCell>, - metadata_map: Rc>>, + override_map: Rc>>, connected_map: Rc>>, usage_type_map: Rc>>, node_disabled: Cell, @@ -605,7 +614,7 @@ impl TreeModel { nodes_map: default(), hierarchy: default(), ports_map: default(), - metadata_map: default(), + override_map: default(), connected_map: default(), usage_type_map: default(), tree_dirty: default(), @@ -622,9 +631,9 @@ impl TreeModel { } } - /// Set the metadata under given pointer. It may cause the tree to be marked as dirty. - fn set_metadata(&self, pointer: MetadataPointer, meta: Option) -> bool { - let mut map = self.metadata_map.borrow_mut(); + /// Set the configuration under given key. It may cause the tree to be marked as dirty. + fn set_config_override(&self, pointer: OverrideKey, meta: Option) -> bool { + let mut map = self.override_map.borrow_mut(); let dirty = map.synchronize_entry(pointer, meta); self.mark_dirty_flag(dirty) } @@ -694,7 +703,7 @@ impl TreeModel { ) { self.tree_dirty.set(false); let app = self.app.clone(); - let metadata_map = self.metadata_map.borrow(); + let metadata_map = self.override_map.borrow(); let connected_map = self.connected_map.borrow(); let usage_type_map = self.usage_type_map.borrow(); let old_nodes = self.nodes_map.take(); @@ -796,11 +805,11 @@ pub(super) struct NodeInfo { pub nesting_level: NestingLevel, /// Data associated with an edge connected to this node's span. Only present at the exact node /// that is connected, not at any of its children. - pub connection: Option, + pub connection: Option, /// Data associated with an edge connected to this subtree. Contains the status of this node's /// connection, or its first parent that is connected. It is the same as `connection` for nodes /// that are directly connected. - pub subtree_connection: Option, + pub subtree_connection: Option, /// Whether the node is disabled, i.e. its expression is not currently used in the computation. /// Widgets of disabled nodes are usually grayed out. pub disabled: bool, @@ -1037,7 +1046,7 @@ struct TreeBuilder<'a> { node_disabled: bool, node_expression: &'a str, styles: &'a StyleWatch, - metadata_map: &'a HashMap, + metadata_map: &'a HashMap, connected_map: &'a HashMap, usage_type_map: &'a HashMap, old_nodes: HashMap, @@ -1082,7 +1091,7 @@ impl<'a> TreeBuilder<'a> { &mut self, span_node: span_tree::node::Ref<'_>, nesting_level: NestingLevel, - meta: Metadata, + meta: Configuration, ) -> display::object::Instance { self.create_child_widget(span_node, nesting_level, Some(meta)) } @@ -1094,7 +1103,7 @@ impl<'a> TreeBuilder<'a> { &mut self, span_node: span_tree::node::Ref<'_>, nesting_level: NestingLevel, - set_metadata: Option, + config_override: Option, ) -> display::object::Instance { // This call can recurse into itself within the widget configuration logic. We need to save // the current layer's state, so it can be restored later after visiting the child node. @@ -1124,21 +1133,21 @@ impl<'a> TreeBuilder<'a> { let sibling_offset = span_node.sibling_offset.as_usize(); let usage_type = main_ptr.id.and_then(|id| self.usage_type_map.get(&id)).cloned(); - // Get widget metadata. There are three potential sources for metadata, that are used in - // order, whichever is available first: - // 1. The `set_metadata` argument, which can be set by the parent widget if it wants to - // override the metadata for its child. - // 2. The `MetadataPointer` stored in the span tree node. This can be set by an external - // source (e.g. based on language server) to override the default metadata for the node. + // Get widget configuration. There are three potential sources for configuration, that are + // used in order, whichever is available first: + // 1. The `config_override` argument, which can be set by the parent widget if it wants to + // override the configuration for its child. + // 2. The override stored in the span tree node, located using `OverrideKey`. This can be + // set by an external source, e.g. based on language server. // 3. The default metadata for the node, which is determined based on the node kind, usage - // type and whether it has children. + // type and whether it has children. let mut meta_fallback = None; let kind = &span_node.kind; - let meta = set_metadata + let configuration = config_override .as_ref() .or_else(|| { let pointer_data = kind.call_id().zip(kind.argument_name()); - let meta_pointer = pointer_data.map(|(call_id, argument_name)| MetadataPointer { + let meta_pointer = pointer_data.map(|(call_id, argument_name)| OverrideKey { call_id, argument_name: argument_name.into(), }); @@ -1147,11 +1156,11 @@ impl<'a> TreeBuilder<'a> { }) .unwrap_or_else(|| { meta_fallback.get_or_insert_with(|| { - Metadata::from_node(&span_node, usage_type.clone(), self.node_expression) + Configuration::from_node(&span_node, usage_type.clone(), self.node_expression) }) }); - let widget_has_port = ptr_usage.request_port(&widget_id, meta.has_port); + let widget_has_port = ptr_usage.request_port(&widget_id, configuration.has_port); let insertion_index = self.hierarchy.len(); self.hierarchy.push(NodeHierarchy { @@ -1163,10 +1172,9 @@ impl<'a> TreeBuilder<'a> { let old_node = self.old_nodes.remove(&widget_id).map(|e| e.node); - // Once we have the metadata and potential old widget to reuse, we have to apply the - // configuration to the widget. + // Prepare the widget node info and build context. let connection_color = self.connected_map.get(&span_node.crumbs); - let connection = connection_color.map(|&color| ConnectionData { color, depth }); + let connection = connection_color.map(|&color| EdgeData { color, depth }); let parent_connection = self.parent_info.as_ref().and_then(|info| info.connection); let subtree_connection = connection.or(parent_connection); @@ -1198,17 +1206,17 @@ impl<'a> TreeBuilder<'a> { let mut port = match old_node { Some(TreeNode::Port(port)) => port, Some(TreeNode::Widget(widget)) => Port::new(widget, app, frp), - None => Port::new(DynWidget::new(&meta.config, &ctx), app, frp), + None => Port::new(DynWidget::new(&configuration.kind, &ctx), app, frp), }; - port.configure(&meta.config, ctx); + port.configure(&configuration.kind, ctx); TreeNode::Port(port) } else { let mut widget = match old_node { Some(TreeNode::Port(port)) => port.into_widget(), Some(TreeNode::Widget(widget)) => widget, - None => DynWidget::new(&meta.config, &ctx), + None => DynWidget::new(&configuration.kind, &ctx), }; - widget.configure(&meta.config, ctx); + widget.configure(&configuration.kind, ctx); TreeNode::Widget(widget) }; diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs b/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs index 0cf46c97d167..4af15f84f9f6 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs @@ -8,7 +8,7 @@ use crate::prelude::*; use crate::component::node::input::widget::single_choice::triangle; use crate::component::node::input::widget::single_choice::ACTIVATION_SHAPE_SIZE; -use crate::component::node::input::widget::Metadata; +use crate::component::node::input::widget::Configuration; use ensogl::application::Application; use ensogl::control::io::mouse; diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 404cd8dd5077..0136e1e3eb91 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -278,7 +278,7 @@ impl super::SpanWidget for Widget { if ctx.span_node.children.is_empty() { let child_level = ctx.info.nesting_level; - let label_meta = super::Metadata::always(super::label::Config); + let label_meta = super::Configuration::always(super::label::Config); let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, label_meta); self.label_wrapper.replace_children(&[child]); } else { diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index a4e4eb867df4..fb99643ad35b 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -1134,9 +1134,9 @@ pub struct CallWidgetsConfig { pub struct ArgumentWidgetConfig { /// The function argument name that this widget is for. pub argument_name: String, - /// Widget metadata queried from the language server. When this is `None`, the widget metadata - /// should be inferred automatically. - pub meta: Option, + /// Widget configuration queried from the language server. When this is `None`, the widget + /// configuration should be inferred automatically. + pub meta: Option, } From 43c2f0975e96c855cb9e356f460891b9e514af84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Fri, 21 Apr 2023 21:10:49 +0200 Subject: [PATCH 34/45] document input widget module --- .../src/component/node/input/area.rs | 26 +++++---- .../src/component/node/input/port.rs | 18 ++++-- .../src/component/node/input/widget.rs | 58 ++++++++++++++++--- 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index d9300bb8a85a..dea3e2de8398 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -257,9 +257,13 @@ impl Model { self.widget_tree.rebuild_tree_if_dirty(&expr.span_tree, &expr.code, &self.styles); } - /// Request widgets metadata for all method calls within the expression. + /// Scan node expressions for all known method calls, for which the language server can provide + /// widget configuration overrides. Emit a request for each such detected call, allowing the + /// controller to request the overrides and provide them. + /// + /// See also: [`controller::graph::widget`] module of `enso-gui` crate. #[profile(Debug)] - fn request_widgets_metadata(&self, expression: &Expression, area_frp: &FrpEndpoints) { + fn request_widget_config_overrides(&self, expression: &Expression, area_frp: &FrpEndpoints) { let call_info = CallInfoMap::scan_expression(&expression.span_tree); for (call_id, info) in call_info.iter() { if let Some(target_id) = info.target_id { @@ -273,7 +277,7 @@ impl Model { #[profile(Debug)] fn set_expression(&self, new_expression: impl Into, area_frp: &FrpEndpoints) { let new_expression = Expression::from(new_expression.into()); - debug!("set expression: \n{:?}", new_expression.tree_pretty_printer()); + debug!("Set expression: \n{:?}", new_expression.tree_pretty_printer()); self.widget_tree.rebuild_tree( &new_expression.span_tree, @@ -281,11 +285,12 @@ impl Model { &self.styles, ); - self.request_widgets_metadata(&new_expression, area_frp); + self.request_widget_config_overrides(&new_expression, area_frp); *self.expression.borrow_mut() = new_expression; } - /// Get hover shapes for all input ports of a node. Used for testing to simulate mouse events. + /// Get hover shapes for all input ports of a node. Mainly used in tests to manually dispatch + /// mouse events. pub fn port_hover_shapes(&self) -> Vec { self.widget_tree.port_hover_shapes() } @@ -366,7 +371,6 @@ ensogl::define_endpoints! { requested_widgets (ast::Id, ast::Id), request_import (ImString), /// A connected port within the node has been moved. Some edges might need to be updated. - /// This event is already debounced. input_edges_need_refresh (), } } @@ -443,11 +447,11 @@ impl Area { // === Show / Hide Phantom Ports === let ports_active = &frp.set_ports_active; - edit_or_ready <- frp.set_edit_ready_mode || set_editing; + edit_or_ready <- frp.set_edit_ready_mode || set_editing; reacts_to_hover <- all_with(&edit_or_ready, ports_active, |e, (a, _)| *e && !a); - port_vis <- all_with(&edit_or_ready, ports_active, |e, (a, _)| !e && *a); - frp.output.source.ports_visible <+ port_vis; - frp.output.source.editing <+ set_editing; + port_vis <- all_with(&edit_or_ready, ports_active, |e, (a, _)| !e && *a); + frp.output.source.ports_visible <+ port_vis; + frp.output.source.editing <+ set_editing; model.widget_tree.set_ports_visible <+ frp.ports_visible; refresh_edges <- model.widget_tree.connected_port_updated.debounce(); frp.output.source.input_edges_need_refresh <+ refresh_edges; @@ -521,7 +525,7 @@ impl Area { eval frp.set_connected(((crumbs,status)) model.set_connected(crumbs,*status)); eval frp.set_expression_usage_type(((id,tp)) model.set_expression_usage_type(*id,tp.clone())); eval frp.set_disabled ((disabled) model.widget_tree.set_disabled(*disabled)); - eval model.widget_tree.rebuild_required((_) model.rebuild_widget_tree_if_dirty()); + eval_ model.widget_tree.rebuild_required(model.rebuild_widget_tree_if_dirty()); // === View Mode === diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index a151cbeaea43..ccde5dc8eaa0 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -96,6 +96,8 @@ impl display::scene::Extension for HoverLayers { } impl HoverLayers { + /// Add a display object to the partition at given depth, effectively setting its display order. + /// If the partition does not exist yet, it will be created. fn add_to_partition(&self, object: &display::object::Instance, depth: usize) { let mut hover_partitions = self.hover_partitions.borrow_mut(); if hover_partitions.len() <= depth { @@ -126,13 +128,17 @@ pub struct Port { widget: DynWidget, port_shape: shape::View, hover_shape: hover_shape::View, + /// Last set tree depth of the port. Allows skipping layout update when the depth has not + /// changed during reconfiguration. current_depth: usize, + /// Whether or not the port was configured as primary. Allows skipping layout update when the + /// hierarchy level has not changed significantly during reconfiguration. current_primary: bool, } impl Port { - /// Create a new port for given widget. The widget will be placed as a child of the port's - /// `port_root` display object, and its layout size will be used to determine the port's size. + /// Create a new port for given widget. The widget will be placed as a child of the port's root + /// display object, and its layout size will be used to determine the port's size. pub fn new(widget: DynWidget, app: &Application, frp: &WidgetsFrp) -> Self { let port_root = display::object::Instance::new(); let widget_root = widget.root_object().clone_ref(); @@ -161,7 +167,7 @@ impl Port { let mouse_leave = hover_shape.on_event::(); let mouse_down = hover_shape.on_event::(); - let crumbs = Rc::new(RefCell::new(span_tree::Crumbs::default())); + let crumbs: Rc> = default(); if frp.set_ports_visible.value() { port_root.add_child(&hover_shape); @@ -212,7 +218,11 @@ impl Port { } } - /// Configure the port and its attached widget. + /// Configure the port and its attached widget. If the widget has changed its root object after + /// reconfiguration, the port display object hierarchy will be updated to use it. + /// + /// See [`crate::component::node::input::widget`] module for more information about widget + /// lifecycle. pub fn configure(&mut self, config: &DynConfig, ctx: ConfigContext) { self.crumbs.replace(ctx.span_node.crumbs.clone()); self.set_connected(ctx.info.connection); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index bb69d6155496..933f0e97b291 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -1,4 +1,47 @@ -//! Definition of all hardcoded node widget variants and common widget FRP API. +//! Node widgets hierarchy. This module defines a widget [`Tree`] view, which manages all widgets +//! and edge ports for a given node. The widgets are organized in a tree structure, where each +//! widget can create multiple child widgets and organize their display objects according to its +//! needs. When node's expression is changed, the widget tree is rebuilt, attempting to preserve +//! as many widgets as possible, which allows widgets to maintain internal state. +//! +//! +//! # Widget Lifecycle +//! +//! The widget lifecycle is managed using [`SpanWidget`] trait. +//! +//! When a widget tree is built for the first time (or the expression has been completely changed), +//! all widgets are created from scratch, using the [`SpanWidget::new`] method. During this phase, +//! each widget can initialize its own view structure and create an FRP network. Immediately after +//! the widget is created, it is configured for the first time using [`SpanWidget::configure`] +//! method. +//! +//! During configuration, the widget should declare its child widgets and place them in its +//! view structure, as well as emit its own internal FRP events to update its view's state. +//! +//! For each subsequent expression change or configuration update, the widget tree is rebuilt, +//! reusing the same widgets for nodes that maintained their identity (see [`WidgetIdentity`]). +//! When a widget is reused, the [`SpanWidget::configure`] method is called, allowing the widget to +//! update its view and declare its child widgets again. Usually, the same children are declared, +//! allowing the build process to propagate down the tree and reuse existing child widgets as well. +//! +//! Whenever a configuration change causes a widget to change its kind (e.g. from a simple label to +//! a single choice dropdown), the widget is removed and a new one is created in its place. +//! +//! +//! # Widget Configuration +//! +//! Each widget kind has its own configuration type, which is used to pass additional data to the +//! widget, as inferred from the expression, or provided by external source as an override. The +//! configuration source is determined in order: +//! 1. If a parent widget has directly provided a configuration for its child, it is always used. +//! Parent widget can provide it by using [`TreeBuilder::child_widget_of_type`] method. +//! 2. If there is a configuration override that matches given span, it is used. The configuration +//! overrides are defined at the whole tree level, and can be provided using +//! [`Tree::set_config_override`] method. +//! 3. The default configuration for the node is created using [`Configuration::from_node`] method. +//! It uses the combination of span tree node kind data and type information to decide which +//! widget is the best fit for the node. + use crate::prelude::*; @@ -78,7 +121,8 @@ pub struct OverrideKey { /// === Widget modules === /// ====================== -/// Common trait for constructing and reconfiguring all widget variants. +/// Common trait for constructing and reconfiguring all widget variants. See "Widget Lifecycle" +/// section of the module documentation for more details. pub trait SpanWidget { /// Configuration associated with specific widget variant. type Config: Debug + Clone + PartialEq; @@ -180,9 +224,9 @@ define_widget_modules! { Hierarchy hierarchy, } -/// ================ -/// === Metadata === -/// ================ +/// ===================== +/// === Configuration === +/// ===================== /// The configuration of a widget and its display properties. Defines how the widget should be /// displayed, if it should be displayed at all, and whether or not it should have a port. Widgets @@ -204,8 +248,8 @@ pub struct Configuration { impl Configuration { /// Derive widget configuration from the expression, node data in span tree and inferred value - /// type. When no metadata is provided by language server, the metadata returned from this - /// function will be used to create a default widget. + /// type. When no configuration is provided with an override, this function will be used to + /// create a default configuration. fn from_node(span_node: &SpanRef, usage_type: Option, expression: &str) -> Self { use span_tree::node::Kind; From 2133de79f79e6d54c46596874e0ec83069cf05a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Mon, 24 Apr 2023 19:12:20 +0200 Subject: [PATCH 35/45] rewrite and test replace_children --- .../src/component/node/input/widget.rs | 20 +- lib/rust/ensogl/core/src/data/dirty.rs | 63 ++- .../core/src/display/object/instance.rs | 462 ++++++++++++++---- 3 files changed, 438 insertions(+), 107 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 933f0e97b291..03b3c9300dda 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -432,13 +432,12 @@ impl Tree { frp::extend! { network frp.private.output.rebuild_required <+ frp.marked_dirty_sync.debounce(); - set_ports_visible <- frp.set_ports_visible.sampler(); - set_view_mode <- frp.set_view_mode.sampler(); + set_ports_visible <- frp.set_ports_visible.sampler(); + set_view_mode <- frp.set_view_mode.sampler(); set_profiling_status <- frp.set_profiling_status.sampler(); - on_port_hover <- any(...); - on_port_press <- any(...); - trace on_port_hover; + on_port_hover <- any(...); + on_port_press <- any(...); frp.private.output.on_port_hover <+ on_port_hover; frp.private.output.on_port_press <+ on_port_press; } @@ -519,7 +518,7 @@ impl Tree { } /// Get the root display object of the widget port for given span tree node. Not all nodes must - /// have a distinct widget, so the returned value might be `None`. + /// have a distinct widget, so the returned value might be [`None`]. pub fn get_port_display_object( &self, span_node: &SpanRef, @@ -594,7 +593,6 @@ struct NodeHierarchy { struct TreeEntry { node: TreeNode, /// Index in the `hierarchy` vector. - #[allow(dead_code)] index: usize, } @@ -787,10 +785,10 @@ impl TreeModel { ); } - /// Convert span tree node to a corresponding widget tree pointer. Every node in the span tree - /// has a unique representation in the form of a widget tree pointer, which is more stable - /// across changes in the span tree than [`span_tree::Crumbs`]. The pointer is used to identify - /// the widgets or ports in the widget tree. + /// Convert span tree node to a representation with stable identity across rebuilds. Every node + /// in the span tree has a unique representation in the form of a [`StableSpanIdentity`], which + /// is more stable across changes in the span tree than [`span_tree::Crumbs`]. The pointer is + /// used to identify the widgets or ports in the widget tree. pub fn get_node_widget_pointer(&self, span_node: &SpanRef) -> StableSpanIdentity { if let Some(id) = span_node.ast_id { // This span represents an AST node, return a pointer directly to it. diff --git a/lib/rust/ensogl/core/src/data/dirty.rs b/lib/rust/ensogl/core/src/data/dirty.rs index 9a020147b38a..06709da8b4a1 100644 --- a/lib/rust/ensogl/core/src/data/dirty.rs +++ b/lib/rust/ensogl/core/src/data/dirty.rs @@ -67,7 +67,6 @@ pub mod traits { fn unset(&mut self); } - // === Arity-1 Operations === /// Abstraction for dirty flags which can perform a dirty check by providing a single argument. @@ -88,6 +87,12 @@ pub mod traits { fn unset(&mut self, arg: &Self::Arg); } + /// Abstraction for dirty flags which can swap the dirtiness state of two elements. Does not + /// trigger any updates. If you want to trigger the update, use `unset` and `check` instead. + #[allow(missing_docs)] + pub trait HasSwap1: HasArg { + fn swap(&mut self, a: Self::Arg, b: Self::Arg); + } // === Shared Global Operations === @@ -132,6 +137,13 @@ pub mod traits { fn unset(&self, arg: &Self::Arg); } + /// Abstraction for dirty flags which can swap the dirtiness state of two elements. Does not + /// trigger any updates. If you want to trigger the update, use `unset` and `check` instead. + #[allow(missing_docs)] + pub trait SharedHasSwap1: HasArg { + fn swap(&self, a: Self::Arg, b: Self::Arg); + } + // === Type Aliases === /// Trait alias for bounds required by all dirty flags. @@ -254,21 +266,26 @@ impl HasSet1 for Flag { impl HasUnset0 for Flag { fn unset(&mut self) { - trace!("Unsetting."); self.data.unset() } } -impl HasUnset1 for Flag -where Arg: Display -{ +impl HasUnset1 for Flag { fn unset(&mut self, arg: &Self::Arg) { - trace!("Unsetting {arg}."); self.data.unset(arg) } } +// === Swap === + +impl HasSwap1 for Flag { + fn swap(&mut self, a: Self::Arg, b: Self::Arg) { + self.data.swap(a, b) + } +} + + // ================== // === RefCellFlag === @@ -367,14 +384,20 @@ impl SharedHasUnset0 for RefCellFlag { } } -impl SharedHasUnset1 for RefCellFlag -where Arg: Display -{ +impl SharedHasUnset1 for RefCellFlag { fn unset(&self, arg: &Self::Arg) { self.data.borrow_mut().unset(arg) } } +// === Swap === + +impl SharedHasSwap1 for RefCellFlag { + fn swap(&self, a: Self::Arg, b: Self::Arg) { + self.data.borrow_mut().swap(a, b) + } +} + // ================== @@ -463,9 +486,7 @@ impl SharedHasUnset0 for SharedFlag { } } -impl SharedHasUnset1 for SharedFlag -where Arg: Display -{ +impl SharedHasUnset1 for SharedFlag { fn unset(&self, arg: &Self::Arg) { self.rc.unset(arg) } @@ -711,6 +732,24 @@ impl HasUnset1 for SetData { } } +impl HasSwap1 for SetData { + fn swap(&mut self, a: Item, b: Item) { + let a_dirty = self.set.contains(&a); + let b_dirty = self.set.contains(&b); + match (a_dirty, b_dirty) { + (true, false) => { + self.set.remove(&a); + self.set.insert(b); + } + (false, true) => { + self.set.remove(&b); + self.set.insert(a); + } + _ => {} + } + } +} + impl Display for SetData { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self.set) diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index 7ea135853e61..11a7621b7a5f 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -1398,30 +1398,33 @@ impl ParentBind { self.parent.upgrade() } - /// Drop this parent bind without removing the child from the parent's children list, assuming - /// it has been already done. Does not mark `modified_children` dirty flag, as the `child_index` - /// is already outdated. - fn drop_manually_removed_from_child_list(self, weak_child: WeakInstance) { - if let Some(parent) = self.parent() { - if let Some(child) = weak_child.upgrade() { - child.dirty.new_parent.set(); - } - parent.dirty.removed_children.set(weak_child); - } - // do not perform usual drop + // Drop this [`ParentBind`] using provided borrows for its parent and its removed child entry. + // This allows clearing the parent children in a batch more efficiently. + fn drop_with_removed_element( + mut self, + parent: &InstanceDef, + removed_children_entry: WeakInstance, + ) { + self.notify_on_drop(parent, removed_children_entry); + // The list is already maintained. Drop the bind without doing it again. mem::forget(self); } + + fn notify_on_drop(&mut self, parent: &InstanceDef, removed_children_entry: WeakInstance) { + debug_assert!(parent.downgrade() == self.parent); + parent.dirty.modified_children.unset(&self.child_index); + if let Some(child) = removed_children_entry.upgrade() { + child.dirty.new_parent.set(); + } + parent.dirty.removed_children.set(removed_children_entry); + } } impl Drop for ParentBind { fn drop(&mut self) { if let Some(parent) = self.parent() { if let Some(weak_child) = parent.children.borrow_mut().remove(&self.child_index) { - parent.dirty.modified_children.unset(&self.child_index); - if let Some(child) = weak_child.upgrade() { - child.dirty.new_parent.set(); - } - parent.dirty.removed_children.set(weak_child); + self.notify_on_drop(&parent, weak_child); } } } @@ -1647,17 +1650,18 @@ impl HierarchyFrp { #[derive(Debug, Deref)] pub struct HierarchyModel { #[deref] - frp: HierarchyFrp, - visible: Cell, - transformation: RefCell, - parent_bind: SharedParentBind, + frp: HierarchyFrp, + visible: Cell, + transformation: RefCell, + parent_bind: SharedParentBind, + next_child_index: Cell, // We are using [`BTreeMap`] here in order to preserve the child insertion order. - children: RefCell>, + children: RefCell>, /// Layer the object was explicitly assigned to by the user, if any. - assigned_layer: RefCell>, + assigned_layer: RefCell>, /// Layer where the object is displayed. It may be set to by user or inherited from the parent. - layer: RefCell>, - dirty: dirty::Flags, + layer: RefCell>, + dirty: dirty::Flags, } impl HierarchyModel { @@ -1666,11 +1670,22 @@ impl HierarchyModel { let visible = default(); let transformation = default(); let parent_bind = default(); + let next_child_index = default(); let children = default(); let assigned_layer = default(); let layer = default(); let dirty = dirty::Flags::new(&parent_bind); - Self { frp, visible, transformation, parent_bind, children, assigned_layer, layer, dirty } + Self { + frp, + visible, + transformation, + parent_bind, + next_child_index, + children, + assigned_layer, + layer, + dirty, + } } } @@ -2065,82 +2080,129 @@ impl InstanceDef { /// will remain in some valid state. fn replace_children(&self, new_children: &[T]) { let this_weak = self.downgrade(); + let mut children_borrow = self.children.borrow_mut(); + let num_children_before = children_borrow.len(); + + let mut pushed_out_children = false; + let mut added_children = 0; + let mut next_free_index = new_children.len().max(*self.next_child_index.get()); + let starting_free_index = next_free_index; + // Update child indices of existing children, maintain their dirty flags. for (index, child) in new_children.iter().enumerate() { let child = child.display_object(); - let child_index = ChildIndex(index); + let new_child_index = ChildIndex(index); let mut bind_borrow = child.parent_bind.data.borrow_mut(); - if let Some(bind) = bind_borrow.as_mut().filter(|bind| bind.parent == this_weak) { - // The child is already a child of this instance. Set the new index. - if bind.child_index != child_index { - self.dirty.modified_children.unset(&bind.child_index); - self.dirty.modified_children.set(child_index); - bind.child_index = child_index; - } - } else { - // This was not a child of this instance. Set the new parent. - drop(bind_borrow); - child.take_parent_bind(); - let new_parent_bind = ParentBind { parent: this_weak.clone(), child_index }; - child.set_parent_bind(new_parent_bind); - self.dirty.modified_children.set(child_index); - } - } + let same_parent_bind = bind_borrow.as_mut().filter(|bind| bind.parent == this_weak); - let mut borrow = self.children.borrow_mut(); - // Drop all children that are not in the new list. - { - for (child_index, weak_instance) in borrow.iter() { - let index = child_index.0; - if let Some(instance) = weak_instance.upgrade() { - let bind_index = instance.parent_bind.child_index(); - if bind_index != Some(*child_index) { - // Bind index updated in the loop above. This means that the child has to - // be preserved. - continue; - } - let child = new_children.get(index); - let matching = child.map_or(false, |c| c.display_object() == &instance); + let free_index = match same_parent_bind { + Some(bind) => { + // The child is already a child of this parent. Update its index. - if matching { - // The child under this index is the same as the one in the new list. We - // want to preserve it. + if bind.child_index == new_child_index { + // The child is already at its destination index. No need to update it. continue; } - // For all other children, we want to drop them, but making sure to not mark - // any newly inserted children as removed. This can happen, if existing element - // has ben moved to an index occupied by an element we are about to remove. - if let Some(bind) = instance.take_parent_bind() { - if bind.child_index.0 >= new_children.len() { - self.dirty.modified_children.unset(&bind.child_index); - } - bind.drop_manually_removed_from_child_list(weak_instance.clone()); + // Move the child to its destination index. In case the newly taken spot was + // occupied, use a swap. The occupied entry will later be moved to the spot + // freed by this element. + let old_index = bind.child_index; + bind.child_index = new_child_index; + + // If the old index was higher than the starting number of children, it means + // that this element was previously pushed out by a swap. We are reusing it, but + // not cleaning up the space it occupied. The cleanup is instead deferred. + pushed_out_children |= *old_index >= starting_free_index; + + old_index + } + None => { + added_children += 1; + // This was not a child of this instance, so it needs to be added as one. Move + // it from its existing parent to this one. + drop(bind_borrow); + drop(child.take_parent_bind()); + let new_parent_bind = + ParentBind { parent: this_weak.clone(), child_index: new_child_index }; + child.set_parent_bind(new_parent_bind); + self.dirty.removed_children.unset(&child.downgrade()); + let free_index = ChildIndex(next_free_index); + next_free_index += 1; + free_index + } + }; + + // If there already was a child present at the destination index, swap them. That child + // will be either maintained in future iterations or deleted. + // + // Note that we want to always attempt BTreeMap insertions before deletions, so we can + // avoid unnecessary tree structure manipulations. When inserting to previously occupied + // element, the tree structure is not modified. + self.dirty.modified_children.swap(free_index, new_child_index); + self.dirty.modified_children.set(new_child_index); + let child_at_dest = children_borrow.insert(new_child_index, child.downgrade()); + if let Some(child_at_dest) = child_at_dest { + if let Some(strong) = child_at_dest.upgrade() { + let mut bind = strong.parent_bind.data.borrow_mut(); + let bind = bind.as_mut().expect("Child should always have a parent bind."); + bind.child_index = free_index; + children_borrow.insert(free_index, child_at_dest); + // In case we just put a child in its final spot, we have to mark as modified. + // If it ends up being deleted, the flag will be cleared anyway. + if bind.parent == this_weak { + self.dirty.modified_children.set(free_index); } } } } - // Fill in the child list. - borrow.clear(); - for (index, child) in new_children.iter().enumerate() { - let child = child.display_object(); - let child_index = ChildIndex(index); - // Check again if the parent bind is is still in expected state. If the children list - // contained any duplicates, we don't want to insert them twice. - if child.parent_bind.matches(&this_weak, child_index) { - borrow.insert(ChildIndex(index), child.downgrade()); + // At this point, all children that were in the new list are in the right position. We + // only need to remove the children that were not in the new list. All of them are still + // in the children list, and their indices are past the inserted indices. + let has_stale_indices = pushed_out_children || starting_free_index > new_children.len(); + let retained_children = new_children.len() - added_children; + let has_elements_to_remove = retained_children < num_children_before; + let need_cleanup = has_elements_to_remove || has_stale_indices; + + dbg!(retained_children, num_children_before, has_elements_to_remove, has_stale_indices); + if need_cleanup { + let mut binds_to_drop = SmallVec::<[(ParentBind, WeakInstance); 8]>::new(); + + // Drop the instances that were removed from the children list. Note that the drop may + // cause the instance to be removed from the children list, so we need to drop the + // instances without holding to borrows. + children_borrow.retain(|index, weak_instance| { + let to_retain = **index < new_children.len(); + if !to_retain { + let instance = weak_instance.upgrade(); + // We do not immediately remove old keys containing pushed-out children when + // they have been reinserted to their appropriate position. To avoid treating + // them as removed, we have to filter them out. Only children that are at their + // correct position should be removed. + let instance = instance.filter(|i| i.parent_bind.child_index() == Some(*index)); + let bind = instance.and_then(|i| i.take_parent_bind()); + let bind_with_instance = bind.map(|bind| (bind, weak_instance.clone())); + binds_to_drop.extend(bind_with_instance); + } + to_retain + }); + + drop(children_borrow); + + self.next_child_index.set(ChildIndex(new_children.len())); + for (bind, weak) in binds_to_drop { + bind.drop_with_removed_element(self, weak) } } } fn register_child(&self, child: &InstanceDef) -> ChildIndex { - let mut children_borrow = self.children.borrow_mut(); - let next_key = children_borrow.last_key_value().map_or(0, |(k, _)| **k + 1); - let index = ChildIndex(next_key); - children_borrow.insert(index, child.downgrade()); - drop(children_borrow); + let index = self.next_child_index.get(); + self.next_child_index.set(ChildIndex(*index + 1)); + self.children.borrow_mut().insert(index, child.downgrade()); + self.dirty.removed_children.unset(&child.downgrade()); self.dirty.modified_children.set(index); index } @@ -4217,18 +4279,250 @@ mod hierarchy_tests { assert_eq!(node2.my_index(), Some(ChildIndex(0))); node1.add_child(&node2); - assert_eq!(node2.my_index(), Some(ChildIndex(0))); + assert_eq!(node2.my_index(), Some(ChildIndex(1))); node1.add_child(&node3); - assert_eq!(node3.my_index(), Some(ChildIndex(1))); + assert_eq!(node3.my_index(), Some(ChildIndex(2))); node1.add_child(&node2); - assert_eq!(node2.my_index(), Some(ChildIndex(2))); + assert_eq!(node2.my_index(), Some(ChildIndex(3))); node1.remove_child(&node3); assert_eq!(node3.my_index(), None); } + struct ReplaceChildrenTest { + root: Instance, + nodes: [Instance; N], + } + + impl ReplaceChildrenTest { + fn new() -> (Instance, [Instance; N], Self) { + let root = Instance::new_named("root"); + let nodes = std::array::from_fn(|n| { + Instance::new_named(Box::leak(format!("{n}").into_boxed_str())) + }); + let nodes_clone = std::array::from_fn(|i| nodes[i].clone()); + (root.clone(), nodes_clone, Self { root, nodes }) + } + + fn prepare_clear_flags(&self) { + self.root.dirty.modified_children.unset_all(); + self.root.dirty.removed_children.unset_all(); + for node in self.nodes.iter() { + node.dirty.new_parent.unset(); + } + } + + #[track_caller] + fn new_node_parents(&self, node_has_new_parent: [bool; N]) { + let status = std::array::from_fn(|n| self.nodes[n].dirty.new_parent.take().check()); + assert_eq!(status, node_has_new_parent); + } + + #[track_caller] + fn children(&self, expected: &[&'static str]) { + let names = self.root.children().iter().map(|node| node.name).collect_vec(); + assert_eq!(names, expected); + } + + #[track_caller] + fn child_indices(&self, expected: &[usize]) { + let indices = self + .root + .children() + .iter() + .map(|node| node.my_index().expect("No index").0) + .collect_vec(); + assert_eq!(indices, expected); + } + + #[track_caller] + fn modified_children(&self, indices: &[usize]) { + let modified = self.root.dirty.modified_children.take().set; + let mut modified = modified.into_iter().map(|idx| idx.0).collect_vec(); + modified.sort(); + assert_eq!(modified, indices); + } + + #[track_caller] + fn removed_children(&self, instances: &[T]) { + let mut removed = self.root.dirty.removed_children.take().set; + for instance in instances { + let instance = instance.display_object(); + let is_removed = removed.remove(&instance.downgrade()); + assert!(is_removed, "Missing removed instance: {:?}", instance.name); + } + assert!( + removed.is_empty(), + "Unexpected removed children: {:?}", + removed.iter().map(|i| i.upgrade().map(|i| i.name)).collect_vec() + ); + } + + #[track_caller] + fn no_removed_children(&self) { + self.removed_children::(&[]); + } + } + + #[test] + fn replace_children_identical_test() { + let (root, nodes, assert) = ReplaceChildrenTest::<5>::new(); + root.replace_children(&nodes); + assert.children(&["0", "1", "2", "3", "4"]); + assert.child_indices(&[0, 1, 2, 3, 4]); + assert.modified_children(&[0, 1, 2, 3, 4]); + assert.removed_children::(&[]); + assert.new_node_parents([true, true, true, true, true]); + + root.replace_children(&nodes); + assert.children(&["0", "1", "2", "3", "4"]); + assert.child_indices(&[0, 1, 2, 3, 4]); + assert.modified_children(&[]); + assert.removed_children::(&[]); + assert.new_node_parents([false, false, false, false, false]); + + root.replace_children::(&[]); + assert.child_indices(&[]); + assert.modified_children(&[]); + assert.removed_children(&nodes); + assert.new_node_parents([true, true, true, true, true]); + } + + #[test] + fn replace_children_subset_test() { + let (root, nodes, assert) = ReplaceChildrenTest::<5>::new(); + root.replace_children(&nodes); + assert.prepare_clear_flags(); + + root.replace_children(&nodes[0..4]); + assert.children(&["0", "1", "2", "3"]); + assert.child_indices(&[0, 1, 2, 3]); + assert.modified_children(&[]); + assert.removed_children(&[&nodes[4]]); + assert.new_node_parents([false, false, false, false, true]); + + + root.replace_children(&nodes[1..4]); + assert.children(&["1", "2", "3"]); + assert.child_indices(&[0, 1, 2]); + assert.modified_children(&[0, 1, 2]); + assert.removed_children(&[&nodes[0]]); + assert.new_node_parents([true, false, false, false, false]); + + root.replace_children(&nodes[2..5]); + assert.children(&["2", "3", "4"]); + assert.child_indices(&[0, 1, 2]); + assert.modified_children(&[0, 1, 2]); + assert.removed_children(&[&nodes[1]]); + assert.new_node_parents([false, true, false, false, true]); + + root.replace_children(&nodes); + assert.children(&["0", "1", "2", "3", "4"]); + assert.modified_children(&[0, 1, 2, 3, 4]); + assert.no_removed_children(); + assert.new_node_parents([true, true, false, false, false]); + + root.replace_children(&[&nodes[0], &nodes[2], &nodes[4]]); + assert.children(&["0", "2", "4"]); + assert.child_indices(&[0, 1, 2]); + assert.modified_children(&[1, 2]); + assert.removed_children(&[&nodes[1], &nodes[3]]); + assert.new_node_parents([false, true, false, true, false]); + + root.replace_children(&nodes); + assert.children(&["0", "1", "2", "3", "4"]); + assert.modified_children(&[1, 2, 3, 4]); + assert.no_removed_children(); + assert.new_node_parents([false, true, false, true, false]); + } + + #[test] + fn replace_children_shuffle_test() { + let (root, nodes, assert) = ReplaceChildrenTest::<5>::new(); + root.replace_children(&nodes); + assert.prepare_clear_flags(); + + root.replace_children(&[&nodes[2..=4], &nodes[0..=1]].concat()); + assert.children(&["2", "3", "4", "0", "1"]); + assert.child_indices(&[0, 1, 2, 3, 4]); + assert.modified_children(&[0, 1, 2, 3, 4]); + assert.no_removed_children(); + assert.new_node_parents([false, false, false, false, false]); + + root.replace_children(&nodes[0..=3]); + assert.children(&["0", "1", "2", "3"]); + assert.child_indices(&[0, 1, 2, 3]); + assert.modified_children(&[0, 1, 2, 3]); + assert.removed_children(&[&nodes[4]]); + assert.new_node_parents([false, false, false, false, true]); + + root.replace_children(&[&nodes[4..=4], &nodes[1..=3], &nodes[0..=0]].concat()); + assert.children(&["4", "1", "2", "3", "0"]); + assert.child_indices(&[0, 1, 2, 3, 4]); + assert.modified_children(&[0, 4]); + assert.no_removed_children(); + assert.new_node_parents([false, false, false, false, true]); + + root.replace_children(&nodes[1..=3]); + assert.children(&["1", "2", "3"]); + assert.child_indices(&[0, 1, 2]); + assert.modified_children(&[0, 1, 2]); + assert.removed_children(&[&nodes[0], &nodes[4]]); + assert.new_node_parents([true, false, false, false, true]); + + root.replace_children(&nodes[1..=4]); + assert.children(&["1", "2", "3", "4"]); + assert.child_indices(&[0, 1, 2, 3]); + assert.modified_children(&[3]); + assert.no_removed_children(); + assert.new_node_parents([false, false, false, false, true]); + } + + #[test] + fn replace_children_keep_flags_test() { + let (root, nodes, assert) = ReplaceChildrenTest::<5>::new(); + root.replace_children(&nodes); + assert.prepare_clear_flags(); + + assert.children(&["0", "1", "2", "3", "4"]); + root.dirty.modified_children.set(ChildIndex(1)); + root.replace_children(&[&nodes[0..=2], &nodes[4..=4]].concat()); + assert.children(&["0", "1", "2", "4"]); + root.replace_children(&nodes); + assert.children(&["0", "1", "2", "3", "4"]); + assert.modified_children(&[1, 3, 4]); + assert.no_removed_children(); + assert.new_node_parents([false, false, false, true, false]); + } + + fn replace_children_replace_all_test() { + let (root, nodes, assert) = ReplaceChildrenTest::<5>::new(); + root.replace_children(&nodes); + assert.prepare_clear_flags(); + + let new_nodes: [_; 10] = std::array::from_fn(|_| Instance::new()); + root.replace_children(&new_nodes); + assert_eq!(root.children(), &new_nodes); + assert.child_indices(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + assert.modified_children(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + assert.removed_children(&nodes); + assert.new_node_parents([true, true, true, true, true]); + + new_nodes.iter().enumerate().for_each(|(i, node)| { + assert_eq!(node.my_index(), Some(ChildIndex(i))); + }); + nodes.iter().for_each(|node| assert_eq!(node.my_index(), None)); + + root.replace_children(&nodes); + assert.children(&["0", "1", "2", "3", "4"]); + assert.child_indices(&[0, 1, 2, 3, 4]); + assert.modified_children(&[0, 1, 2, 3, 4]); + assert.removed_children(&new_nodes); + assert.new_node_parents([true, true, true, true, true]); + } + #[test] fn transformation_test() { let world = World::new(); From 4c2da3c9784d8ae33ef3c273288268e6b9b86839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Mon, 24 Apr 2023 20:24:26 +0200 Subject: [PATCH 36/45] use superbox for widget ports --- .../view/graph-editor/src/component/node.rs | 6 +- .../src/component/node/input/area.rs | 2 +- .../src/component/node/input/port.rs | 75 ++++++------------- .../src/component/node/input/widget.rs | 7 +- .../node/input/widget/single_choice.rs | 1 - lib/rust/ensogl/core/src/display/scene.rs | 3 + 6 files changed, 34 insertions(+), 60 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index 0559d75bb37d..63f3d5eede64 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -500,8 +500,6 @@ impl NodeModel { background -> drag_area; drag_area -> edge::front::corner; drag_area -> edge::front::line; - edge::front::corner -> input::port::shape; - edge::front::line -> input::port::shape; } } @@ -1113,7 +1111,7 @@ pub mod test_utils { /// 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_hover_shape(&self) -> Option; + fn input_port_hover_shape(&self) -> Option; } impl NodeModelExt for NodeModel { @@ -1128,7 +1126,7 @@ pub mod test_utils { } } - fn input_port_hover_shape(&self) -> Option { + fn input_port_hover_shape(&self) -> Option { let shapes = self.input.model.port_hover_shapes(); shapes.into_iter().next() } diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index dea3e2de8398..d81e7f8094bf 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -291,7 +291,7 @@ impl Model { /// Get hover shapes for all input ports of a node. Mainly used in tests to manually dispatch /// mouse events. - pub fn port_hover_shapes(&self) -> Vec { + pub fn port_hover_shapes(&self) -> Vec { self.widget_tree.port_hover_shapes() } } diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index ccde5dc8eaa0..c65377f3e947 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -15,6 +15,7 @@ use ensogl::control::io::mouse; use ensogl::data::color; use ensogl::display; use ensogl::display::scene::layer::LayerSymbolPartition; +use ensogl::display::shape; @@ -33,50 +34,17 @@ pub const BASE_PORT_HEIGHT: f32 = 18.0; /// the target text boundary on both sides. pub const PRIMARY_PORT_HOVER_PADDING_Y: f32 = (crate::node::HEIGHT - BASE_PORT_HEIGHT) / 2.0; -// ============= -// === Shape === -// ============= - -/// Shapes the port is build from. It consist of the `hover_shape`, which represents a hover area of -/// a height dependent on logical widget depth, and the `shape`, which is a nice, visual highlight -/// representation with padding extending out of the widget bounding box. Both shapes are children -/// of `Port`'s `port_root` display object: -/// -/// ```text -/// hover_shape (appears when hovering over the node while dragging an edge) -/// ◄──────► -/// ╭───┬────────┬──┄ -/// │ │╭──────╮│▼ shape -/// │ │╰──────╯│▲ -/// ╰───┴────────┴──┄ -/// ``` -pub mod shape { - use super::*; - ensogl::shape! { - below = [ensogl::gui::cursor::shape]; - pointer_events = false; - (style:Style, color:Vector4) { - let size = Var::canvas_size(); - let shape_color = Var::::from(color); - let visual_shape = Rect(&size).corners_radius(size.y() / 2.0).fill(shape_color); - visual_shape.into() - } - } -} -/// Hover area of port shape, reacts to mouse when an edge is dragged. -pub mod hover_shape { - use super::*; - ensogl::shape! { - above = [shape]; - (style:Style) { - let size = Var::canvas_size(); - let transparent = Var::::from("srgba(1.0,1.0,1.0,0.00001)"); - let hover_shape = Rect(size).fill(transparent); - hover_shape.into() - } - } -} + +// ============================ +// === Shapes / HoverLayers === +// ============================ + +type PortShape = shape::compound::rectangle::Rectangle; + +/// Shape used for handling mouse events in the port, such as hovering or dropping an edge. +pub type HoverShape = shape::compound::rectangle::Rectangle; +type HoverShapeView = shape::compound::rectangle::shape::Shape; /// An scene extension that maintains layer partitions for port hover shapes. It is shared by all /// ports in the scene. The hover shapes are partitioned by span tree depth, so that the hover area @@ -85,12 +53,12 @@ pub mod hover_shape { #[derive(Clone, CloneRef)] struct HoverLayers { hover_layer: display::scene::Layer, - hover_partitions: Rc>>>, + hover_partitions: Rc>>>, } impl display::scene::Extension for HoverLayers { fn init(scene: &display::Scene) -> Self { - let hover_layer = scene.layers.label.clone_ref(); + let hover_layer = scene.layers.port_hover.clone_ref(); Self { hover_layer, hover_partitions: default() } } } @@ -102,7 +70,7 @@ impl HoverLayers { let mut hover_partitions = self.hover_partitions.borrow_mut(); if hover_partitions.len() <= depth { hover_partitions.resize_with(depth + 1, || { - self.hover_layer.create_symbol_partition::("input port hover") + self.hover_layer.create_symbol_partition("input port hover") }) } hover_partitions[depth].add(object); @@ -126,8 +94,8 @@ pub struct Port { port_root: display::object::Instance, widget_root: display::object::Instance, widget: DynWidget, - port_shape: shape::View, - hover_shape: hover_shape::View, + port_shape: PortShape, + hover_shape: HoverShape, /// Last set tree depth of the port. Allows skipping layout update when the depth has not /// changed during reconfiguration. current_depth: usize, @@ -142,8 +110,13 @@ impl Port { pub fn new(widget: DynWidget, app: &Application, frp: &WidgetsFrp) -> Self { let port_root = display::object::Instance::new(); let widget_root = widget.root_object().clone_ref(); - let port_shape = shape::View::new(); - let hover_shape = hover_shape::View::new(); + let port_shape = PortShape::new(); + let hover_shape = HoverShape::new(); + port_shape.set_corner_radius_max().set_pointer_events(false); + hover_shape + .set_pointer_events(true) + .set_color(shape::INVISIBLE_HOVER_COLOR) + .set_border_color(shape::INVISIBLE_HOVER_COLOR); port_root.add_child(&widget_root); widget_root.set_margin_left(0.0); @@ -279,7 +252,7 @@ impl Port { } /// Get the port's hover shape. Used for testing to simulate mouse events. - pub fn hover_shape(&self) -> &hover_shape::View { + pub fn hover_shape(&self) -> &HoverShape { &self.hover_shape } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 03b3c9300dda..9db98a2006dc 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -470,8 +470,9 @@ impl Tree { self.notify_dirty(self.model.set_config_override(pointer, meta)); } - /// Set usage type for given AST node. The usage type is used to determine the widget appearance - /// and default inferred widget configuration. + /// Set the inferred type of the expression for given ast ID. On rebuild, the type will be + /// linked with any widget created on any span with matching AST ID. It is used to determine the + /// widget appearance and default inferred widget configuration. pub fn set_usage_type(&self, ast_id: ast::Id, usage_type: Option) { self.notify_dirty(self.model.set_usage_type(ast_id, usage_type)); } @@ -528,7 +529,7 @@ impl Tree { } /// Get hover shapes for all ports in the tree. Used in tests to manually dispatch mouse events. - pub fn port_hover_shapes(&self) -> Vec { + pub fn port_hover_shapes(&self) -> Vec { let nodes = self.model.nodes_map.borrow(); self.model .hierarchy diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 0136e1e3eb91..9f160deabef0 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -39,7 +39,6 @@ const DROPDOWN_MAX_SIZE: Vector2 = Vector2(300.0, 500.0); pub mod triangle { use super::*; ensogl::shape! { - below = [crate::component::node::input::port::hover_shape]; alignment = left_bottom; (style:Style, color:Vector4) { let size = Var::canvas_size(); diff --git a/lib/rust/ensogl/core/src/display/scene.rs b/lib/rust/ensogl/core/src/display/scene.rs index 220f0a13e75b..736b87e32b46 100644 --- a/lib/rust/ensogl/core/src/display/scene.rs +++ b/lib/rust/ensogl/core/src/display/scene.rs @@ -597,6 +597,7 @@ pub struct HardcodedLayers { pub main: Layer, pub port_selection: Layer, pub label: Layer, + pub port_hover: Layer, pub above_nodes: Layer, pub above_nodes_text: Layer, /// `panel` layer contains all panels with fixed position (not moving with the panned scene) @@ -639,6 +640,7 @@ impl HardcodedLayers { let port_selection = root.create_sublayer_with_camera("port_selection", &port_selection_cam); let label = root.create_sublayer("label"); + let port_hover = root.create_sublayer("port_hover"); let above_nodes = root.create_sublayer("above_nodes"); let above_nodes_text = root.create_sublayer("above_nodes_text"); let panel_background = root.create_sublayer_with_camera("panel_background", &panel_cam); @@ -662,6 +664,7 @@ impl HardcodedLayers { main, port_selection, label, + port_hover, above_nodes, above_nodes_text, panel_background, From aaadab347e35a979bf77b8adb834a9e816396946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Mon, 24 Apr 2023 22:16:01 +0200 Subject: [PATCH 37/45] fix node size on initialization --- .../view/graph-editor/src/component/node.rs | 5 +- .../src/component/node/input/area.rs | 48 +++++++------------ .../src/component/node/input/widget.rs | 7 ++- .../component/node/input/widget/hierarchy.rs | 5 +- app/gui/view/graph-editor/src/lib.rs | 3 +- lib/rust/ensogl/core/src/display/scene.rs | 6 +-- 6 files changed, 30 insertions(+), 44 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index 63f3d5eede64..d46cb4f3b4cc 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -715,6 +715,7 @@ impl Node { let action_bar = &model.action_bar.frp; frp::extend! { network + init <- source::<()>(); // Hook up the display object position updates to the node's FRP. Required to calculate // the bounding box. @@ -790,7 +791,8 @@ impl Node { // === Size === - new_size <- model.input.frp.width.map(f!((w) model.set_width(*w))); + input_width <- all(&model.input.frp.width, &init)._0(); + new_size <- input_width.map(f!((w) model.set_width(*w))); model.output.frp.set_size <+ new_size; @@ -904,7 +906,6 @@ impl Node { eval visualization_visible_on_change ((is_visible) model.visualization.frp.set_visibility(is_visible) ); - init <- source::<()>(); out.visualization_path <+ model.visualization.frp.visualisation.all_with(&init,|def_opt,_| { def_opt.as_ref().map(|def| def.signature.path.clone_ref()) }); diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index d81e7f8094bf..1b417278f6cb 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -427,22 +427,6 @@ impl Area { eval set_editing((is_editing) model.set_edit_mode(*is_editing)); - // Prevent text selection from being created right after entering edit mode. Otherwise, - // a selection would be created between the current mouse position (the position at - // which we clicked) and initial cursor position within edit mode label (the code - // position corresponding to clicked port). - start_editing <- set_editing.on_true(); - stop_editing <- set_editing.on_false(); - start_editing_delayed <- start_editing.debounce(); - reenable_selection_update <- any(&start_editing_delayed, &stop_editing); - selection_update_enabled <- bool(&start_editing, &reenable_selection_update); - eval selection_update_enabled([model] (enabled) { - let cmd_start = "start_newest_selection_end_follow_mouse"; - let cmd_stop = "stop_newest_selection_end_follow_mouse"; - model.edit_mode_label.set_command_enabled(cmd_start, *enabled); - model.edit_mode_label.set_command_enabled(cmd_stop, *enabled); - }); - // === Show / Hide Phantom Ports === @@ -475,20 +459,15 @@ impl Area { frp.output.source.pointer_style <+ pointer_style; // === Properties === - let layout_refresh = ensogl::animation::on_before_animations(); - widget_tree_width <- layout_refresh.map(f!([model](_) { - let instance = model.widget_tree.display_object(); - instance.computed_size().x() - })).on_change(); - - padded_edit_label_width <- model.edit_mode_label.width.map(|t| t + 2.0 * TEXT_OFFSET); - + let widget_tree_object = model.widget_tree.display_object(); + widget_tree_width <- widget_tree_object.on_resized.map(|size| size.x()); + edit_label_width <- all(model.edit_mode_label.width, init)._0(); + padded_edit_label_width <- edit_label_width.map(|t| t + 2.0 * TEXT_OFFSET); frp.output.source.width <+ set_editing.switch( &widget_tree_width, &padded_edit_label_width ); - // === Expression === let frp_endpoints = &frp.output; @@ -553,14 +532,19 @@ impl Area { } /// An offset from node position to a specific port. - pub fn port_offset(&self, crumbs: &[Crumb]) -> Option> { + pub fn port_offset(&self, crumbs: &[Crumb]) -> Vector2 { let expr = self.model.expression.borrow(); - let node = expr.get_node(crumbs).ok()?; - let instance = self.model.widget_tree.get_port_display_object(&node)?; - let pos = instance.global_position(); - let node_pos = self.model.display_object.global_position(); - let size = instance.computed_size(); - Some(pos.xy() - node_pos.xy() + size * 0.5) + let port = expr + .get_node(crumbs) + .ok() + .and_then(|node| self.model.widget_tree.get_port_display_object(&node)); + let initial_position = Vector2(TEXT_OFFSET, NODE_HEIGHT / 2.0); + port.map_or(initial_position, |port| { + let pos = port.global_position(); + let node_pos = self.model.display_object.global_position(); + let size = port.computed_size(); + pos.xy() - node_pos.xy() + size * 0.5 + }) } /// A type of the specified port. diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 9db98a2006dc..63dd0c43d2fc 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -1184,7 +1184,10 @@ impl<'a> TreeBuilder<'a> { // set by an external source, e.g. based on language server. // 3. The default metadata for the node, which is determined based on the node kind, usage // type and whether it has children. - let mut meta_fallback = None; + + // Stack variable necessary to hold an owned configuration initialized in a closure below, + // so we can take a reference to it. + let mut default_config = None; let kind = &span_node.kind; let configuration = config_override .as_ref() @@ -1198,7 +1201,7 @@ impl<'a> TreeBuilder<'a> { meta_pointer.and_then(|ptr| self.metadata_map.get(&ptr)) }) .unwrap_or_else(|| { - meta_fallback.get_or_insert_with(|| { + default_config.get_or_insert_with(|| { Configuration::from_node(&span_node, usage_type.clone(), self.node_expression) }) }); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs index 9940e61b0cfd..98c40d31e901 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -9,11 +9,10 @@ use ensogl::display::object; // ================= // === Hierarchy === -// ================ - -#[derive(Debug, Clone, Copy, PartialEq, Default)] +// ================= /// Label widget configuration options. +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Config; /// Hierarchy widget. This widget expands each child of its span tree into a new widget. diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index fb99643ad35b..4620e9e4edde 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -2356,8 +2356,7 @@ impl GraphEditorModel { } if let Some(edge_target) = edge.target() { if let Some(node) = self.nodes.get_cloned_ref(&edge_target.node_id) { - let offset = - node.model().input.port_offset(&edge_target.port).unwrap_or_default(); + let offset = node.model().input.port_offset(&edge_target.port); let new_position = node.position().xy() + offset; let prev_position = edge.view.target_position.get(); if prev_position != new_position { diff --git a/lib/rust/ensogl/core/src/display/scene.rs b/lib/rust/ensogl/core/src/display/scene.rs index 736b87e32b46..3c121dfafaab 100644 --- a/lib/rust/ensogl/core/src/display/scene.rs +++ b/lib/rust/ensogl/core/src/display/scene.rs @@ -779,7 +779,7 @@ pub struct UpdateStatus { // === SceneData === // ================= -#[derive(Clone, CloneRef, Debug)] +#[derive(Debug)] pub struct SceneData { pub display_object: display::object::Root, pub dom: Dom, @@ -1091,7 +1091,7 @@ impl display::Object for SceneData { #[derive(Clone, CloneRef, Debug)] pub struct Scene { - no_mut_access: SceneData, + no_mut_access: Rc, } impl Scene { @@ -1101,7 +1101,7 @@ impl Scene { display_mode: &Rc>, ) -> Self { let no_mut_access = SceneData::new(stats, on_mut, display_mode); - let this = Self { no_mut_access }; + let this = Self { no_mut_access: Rc::new(no_mut_access) }; this } From cdb00eb07ecba100ffda650ecbd7ce1d32d9e66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Mon, 24 Apr 2023 23:48:11 +0200 Subject: [PATCH 38/45] split single choice widget frp --- app/gui/language/span-tree/src/lib.rs | 56 ++-- .../src/component/node/input/area.rs | 2 +- .../src/component/node/input/widget.rs | 12 +- .../node/input/widget/insertion_point.rs | 7 +- .../node/input/widget/single_choice.rs | 307 ++++++++++-------- app/gui/view/graph-editor/src/lib.rs | 8 +- lib/rust/ensogl/core/src/animation/loops.rs | 18 +- lib/rust/ensogl/core/src/display/scene.rs | 8 +- lib/rust/ensogl/core/src/display/world.rs | 24 +- 9 files changed, 245 insertions(+), 197 deletions(-) diff --git a/app/gui/language/span-tree/src/lib.rs b/app/gui/language/span-tree/src/lib.rs index 11704c80bdf6..2d35e79904d6 100644 --- a/app/gui/language/span-tree/src/lib.rs +++ b/app/gui/language/span-tree/src/lib.rs @@ -224,32 +224,32 @@ impl SpanTree { /// /// Example output with AST ids removed for clarity: /// ```text - /// operator6.join operator31 Join_Kind.Inner ["County"] Root - /// operator6.join operator31 Join_Kind.Inner ["County"] ├── Chained - /// operator6.join operator31 Join_Kind.Inner ["County"] │ ├── Chained - /// operator6.join operator31 Join_Kind.Inner │ │ ├── Chained - /// operator6.join operator31 │ │ │ ├── Chained - /// operator6.join │ │ │ │ ├── Operation - /// ▲ │ │ │ │ │ ├── InsertionPoint(BeforeTarget) - /// operator6 │ │ │ │ │ ├── This - /// ▲ │ │ │ │ │ ├── InsertionPoint(AfterTarget) - /// . │ │ │ │ │ ├── Operation - /// join │ │ │ │ │ ├── Argument - /// ▲ │ │ │ │ │ ╰── InsertionPoint(Append) - /// operator31 │ │ │ │ ╰── Argument name="right" - /// Join_Kind.Inner │ │ │ ╰── Argument name="join_kind" - /// ▲ │ │ │ ├── InsertionPoint(BeforeTarget) - /// Join_Kind │ │ │ ├── This - /// ▲ │ │ │ ├── InsertionPoint(AfterTarget) - /// . │ │ │ ├── Operation - /// Inner │ │ │ ├── Argument - /// ▲ │ │ │ ╰── InsertionPoint(Append) - /// ["County"] │ │ ╰── Argument name="on" - /// [ │ │ ├── Token - /// "County" │ │ ├── Argument - /// ] │ │ ╰── Token - /// ▲│ ╰── InsertionPoint(ExpectedArgument(3)) name="right_prefix" - /// ▲╰── InsertionPoint(ExpectedArgument(4)) name="on_problems" + /// ▷operator4.join operator2 Join_Kind.Inner ["County"]◁Root + /// ▷operator4.join operator2 Join_Kind.Inner ["County"]◁ ├─Chained + /// ▷operator4.join operator2 Join_Kind.Inner ["County"]◁ │ ├─Chained + /// ▷operator4.join operator2 Join_Kind.Inner◁ │ │ ├─Chained + /// ▷operator4.join operator2◁ │ │ │ ├─Chained + /// ▷operator4.join◁ │ │ │ │ ├─Operation + /// ▷◁ │ │ │ │ │ ├─InsertionPoint(BeforeArgument(0)) + /// ▷operator4◁ │ │ │ │ │ ├─Argument name="self" + /// ▷.◁ │ │ │ │ │ ├─Operation + /// ▷◁ │ │ │ │ │ ├─InsertionPoint(BeforeArgument(1)) + /// ▷join◁ │ │ │ │ │ ├─Argument + /// ▷◁ │ │ │ │ │ ╰─InsertionPoint(Append) + /// ▷operator2◁ │ │ │ │ ╰─Argument name="right" + /// ▷Join_Kind.Inner◁ │ │ │ ╰─Argument name="join_kind" + /// ▷◁ │ │ │ ├─InsertionPoint(BeforeArgument(0)) + /// ▷Join_Kind◁ │ │ │ ├─Argument + /// ▷.◁ │ │ │ ├─Operation + /// ▷◁ │ │ │ ├─InsertionPoint(BeforeArgument(1)) + /// ▷Inner◁ │ │ │ ├─Argument + /// ▷◁ │ │ │ ╰─InsertionPoint(Append) + /// ▷["County"]◁ │ │ ╰─Argument name="on" + /// ▷[◁ │ │ ├─Token + /// ▷"County"◁ │ │ ├─Argument + /// ▷]◁ │ │ ╰─Token + /// ▷◁ │ ╰─InsertionPoint(ExpectedArgument) name="right_prefix" + /// ▷◁ ╰─InsertionPoint(ExpectedArgument) name="on_problems" /// ``` pub fn debug_print(&self, code: &str) -> String { use std::fmt::Write; @@ -280,8 +280,8 @@ impl SpanTree { let indent = if let Some(index) = node.crumbs.last() { let is_last = *index == state.num_children - 1; - let indent_targeted = if is_last { "╰── " } else { "├── " }; - let indent_continue = if is_last { " " } else { "│ " }; + let indent_targeted = if is_last { " ╰─" } else { " ├─" }; + let indent_continue = if is_last { " " } else { " │ " }; buffer.push_str(&state.indent); buffer.push_str(indent_targeted); diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 1b417278f6cb..c2f3c8730e30 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -277,7 +277,7 @@ impl Model { #[profile(Debug)] fn set_expression(&self, new_expression: impl Into, area_frp: &FrpEndpoints) { let new_expression = Expression::from(new_expression.into()); - debug!("Set expression: \n{:?}", new_expression.tree_pretty_printer()); + warn!("Set expression: \n{:?}", new_expression.tree_pretty_printer()); self.widget_tree.rebuild_tree( &new_expression.span_tree, diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 63dd0c43d2fc..bce94bc4591d 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -42,7 +42,6 @@ //! It uses the combination of span tree node kind data and type information to decide which //! widget is the best fit for the node. - use crate::prelude::*; use crate::component::node::input::area::NODE_HEIGHT; @@ -61,6 +60,8 @@ use ensogl_component::drop_down::DropdownValue; use span_tree::node::Ref as SpanRef; use text::index::Byte; + + // ================= // === Constants === // ================= @@ -1063,12 +1064,9 @@ impl PointerUsage { } fn request_port(&mut self, identity: &WidgetIdentity, wants_port: bool) -> bool { - if wants_port && self.port_index.is_none() { - self.port_index = Some(identity.index); - true - } else { - false - } + let will_receive_port = wants_port && self.port_index.is_none(); + will_receive_port.then(|| self.port_index = Some(identity.index)); + will_receive_port } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs b/app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs index e15af4176de5..3aa096cbafbd 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/insertion_point.rs @@ -1,4 +1,9 @@ -//! Definition of empty widget that represents insertion point. +//! Definition of empty widget that represents insertion point, which is a span node representing +//! a position where a new expression can be inserted. Does not correspond to any AST, but instead +//! is placed between spans for AST nodes. It is often used as an temporary edge endpoint when +//! dragging an edge. +//! +//! See also [`span_tree::node::InsertionPoint`]. use crate::prelude::*; diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 9f160deabef0..6ac858a85be8 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -31,6 +31,8 @@ const DROPDOWN_Y_OFFSET: f32 = -20.0; /// by these values, it will receive a scroll bar. const DROPDOWN_MAX_SIZE: Vector2 = Vector2(300.0, 500.0); + + // ====================== // === Triangle Shape === // ====================== @@ -101,7 +103,6 @@ impl super::SpanWidget for Widget { fn new(_: &Config, ctx: &super::ConfigContext) -> Self { let app = ctx.app(); - let widgets_frp = ctx.frp(); // ╭─display_object────────────────────╮ // │╭─content_wrapper─────────────────╮│ // ││ ╭ shape ╮ ╭ label_wrapper ────╮ ││ @@ -116,7 +117,7 @@ impl super::SpanWidget for Widget { let activation_shape = triangle::View::new(); activation_shape.set_size(ACTIVATION_SHAPE_SIZE); - let layers = &ctx.app().display.default_scene.layers; + let layers = &app.display.default_scene.layers; layers.label.add(&activation_shape); let display_object = display::object::Instance::new(); @@ -145,108 +146,9 @@ impl super::SpanWidget for Widget { dropdown_wrapper.set_size((0.0, 0.0)).set_alignment_left_top(); let config_frp = Frp::new(); - let network = &config_frp.network; - let input = &config_frp.private.input; - let styles = ctx.styles(); - - let focus_receiver = display_object.clone_ref(); - - frp::extend! { network - initialize_dropdown <- any_(...); - - let focus_in = focus_receiver.on_event::(); - let focus_out = focus_receiver.on_event::(); - initialize_dropdown <+ focus_in; - is_open <- bool(&focus_out, &focus_in); - - is_hovered <- widgets_frp.on_port_hover.map2(&input.current_crumbs, |h, crumbs| { - h.on().map_or(false, |h| crumbs.starts_with(h)) - }); - is_connected_or_hovered <- input.is_connected || is_hovered; - activation_shape_theme <- is_connected_or_hovered.map(|is_connected_or_hovered| { - if *is_connected_or_hovered { - Some(theme::widget::activation_shape::connected) - } else { - Some(theme::widget::activation_shape::base) - } - }); - activation_shape_theme <- activation_shape_theme.on_change(); - eval activation_shape_theme([styles, activation_shape](path) { - if let Some(path) = path { - let color = styles.get_color(path); - let rgba = color::Rgba::from(color); - activation_shape.color.set(rgba.into()); - } - }); - - let dot_mouse_down = activation_shape.on_event::(); - dot_clicked <- dot_mouse_down.filter(mouse::is_primary); - set_focused <- dot_clicked.map(f!([focus_receiver](_) !focus_receiver.is_focused())); - eval set_focused([focus_receiver](focus) match focus { - true => focus_receiver.focus(), - false => focus_receiver.blur(), - }); - - current_value <- input.current_value.on_change(); - entries <- input.set_entries.on_change(); - entries_and_value <- all(&entries, ¤t_value); - entries_and_value <- entries_and_value.debounce(); - dropdown_set_open <- is_open.on_change(); - dropdown_set_all_entries <- entries_and_value.map(|(e, _)| e.deref().clone()); - entries_and_value <- entries_and_value.buffered_gate(&is_open); - - // sources from dropdown, lazily initialized - dropdown_user_select_action <- any(...); - dropdown_selected_entries <- any(...); - - // Close the dropdown after a short delay after selection. Because the dropdown - // value application triggers operations that can introduce a few dropped frames, - // we want to delay the dropdown closing animation after that is handled. - // Otherwise the animation finishes within single frame, which looks bad. - let close_after_selection_timer = frp::io::timer::Timeout::new(network); - close_after_selection_timer.restart <+ dropdown_user_select_action.constant(1); - eval close_after_selection_timer.on_expired((()) focus_receiver.blur()); - - selected_entry <- entries_and_value.map(|(e, v)| entry_for_current_value(e, v)); - dropdown_set_selected_entries <- selected_entry.map(|e| e.iter().cloned().collect()); - - dropdown_entry <- dropdown_selected_entries.map(|e: &HashSet| e.iter().next().cloned()); - // Emit the output value only after actual user action. This prevents the - // dropdown from emitting its initial value when it is opened, which can - // represent slightly different version of code than actually written. - submitted_entry <- dropdown_entry.sample(&dropdown_user_select_action); - dropdown_out_value <- submitted_entry.map(|e| e.as_ref().map(Entry::value)); - dropdown_out_import <- submitted_entry.map(|e| e.as_ref().and_then(Entry::required_import)); - - widgets_frp.request_import <+ dropdown_out_import.unwrap(); - widgets_frp.value_changed <+ dropdown_out_value.map2(&input.current_crumbs, - move |t: &Option, crumbs: &span_tree::Crumbs| (crumbs.clone(), t.clone()) - ); - }; - - frp::extend! { network - dropdown_set_all_entries <- dropdown_set_all_entries.sampler(); - dropdown_set_selected_entries <- dropdown_set_selected_entries.sampler(); - dropdown_set_open <- dropdown_set_open.sampler(); - } - - let dropdown = LazyDropdown { - app: app.clone_ref(), - set_all_entries: dropdown_set_all_entries, - set_selected_entries: dropdown_set_selected_entries, - set_open: dropdown_set_open, - selected_entries: dropdown_selected_entries, - user_select_action: dropdown_user_select_action, - dropdown: None, - }; + let dropdown = LazyDropdown::new(&app, &config_frp.network); let dropdown = Rc::new(RefCell::new(dropdown)); - frp::extend! { network - eval initialize_dropdown([dropdown, dropdown_wrapper](_) { - dropdown.borrow_mut().init(&dropdown_wrapper); - }); - } - Self { config_frp, display_object, @@ -256,6 +158,7 @@ impl super::SpanWidget for Widget { dropdown, activation_shape, } + .init(ctx) } fn configure(&mut self, config: &Config, mut ctx: super::ConfigContext) { @@ -274,7 +177,6 @@ impl super::SpanWidget for Widget { ctx.modify_extension::(|ext| ext.bold = true); } - if ctx.span_node.children.is_empty() { let child_level = ctx.info.nesting_level; let label_meta = super::Configuration::always(super::label::Config); @@ -292,6 +194,116 @@ impl super::SpanWidget for Widget { } } +impl Widget { + fn init(self, ctx: &super::ConfigContext) -> Self { + let is_open = self.init_dropdown_focus(); + self.init_dropdown_values(ctx, is_open); + self.init_activation_shape(ctx); + self + } + + fn init_dropdown_focus(&self) -> frp::Stream { + let focus_receiver = self.display_object.clone_ref(); + let focus_in = focus_receiver.on_event::(); + let focus_out = focus_receiver.on_event::(); + let network = &self.config_frp.network; + let dropdown = &self.dropdown; + let dropdown_frp = &self.dropdown.borrow(); + let dropdown_wrapper = &self.dropdown_wrapper; + frp::extend! { network + eval focus_in([dropdown, dropdown_wrapper](_) { + dropdown.borrow_mut().lazy_init(&dropdown_wrapper); + }); + is_open <- bool(&focus_out, &focus_in); + dropdown_frp.set_open <+ is_open.on_change(); + + // Close the dropdown after a short delay after selection. Because the dropdown + // value application triggers operations that can introduce a few dropped frames, + // we want to delay the dropdown closing animation after that is handled. + // Otherwise the animation finishes within single frame, which looks bad. + let close_after_selection_timer = frp::io::timer::Timeout::new(network); + close_after_selection_timer.restart <+ dropdown_frp.user_select_action.constant(1); + eval close_after_selection_timer.on_expired((()) focus_receiver.blur()); + + } + is_open + } + + + fn init_dropdown_values(&self, ctx: &super::ConfigContext, is_open: frp::Stream) { + let network = &self.config_frp.network; + let dropdown_frp = &self.dropdown.borrow(); + let config_frp = &self.config_frp; + let widgets_frp = ctx.frp(); + + frp::extend! { network + current_value <- config_frp.current_value.on_change(); + entries <- config_frp.set_entries.on_change(); + entries_and_value <- all(&entries, ¤t_value); + entries_and_value <- entries_and_value.debounce(); + dropdown_frp.set_all_entries <+ entries_and_value.map(|(e, _)| e.deref().clone()); + entries_and_value <- entries_and_value.buffered_gate(&is_open); + + selected_entry <- entries_and_value.map(|(e, v)| entry_for_current_value(e, v)); + dropdown_frp.set_selected_entries <+ selected_entry.map(|e| e.iter().cloned().collect()); + + dropdown_entry <- dropdown_frp.selected_entries + .map(|e: &HashSet| e.iter().next().cloned()); + + // Emit the output value only after actual user action. This prevents the + // dropdown from emitting its initial value when it is opened, which can + // represent slightly different version of code than actually written. + submitted_entry <- dropdown_entry.sample(&dropdown_frp.user_select_action); + dropdown_out_value <- submitted_entry.map(|e| e.as_ref().map(Entry::value)); + dropdown_out_import <- submitted_entry.map(|e| e.as_ref().and_then(Entry::required_import)); + + widgets_frp.request_import <+ dropdown_out_import.unwrap(); + widgets_frp.value_changed <+ dropdown_out_value.map2(&config_frp.current_crumbs, + move |t: &Option, crumbs: &span_tree::Crumbs| (crumbs.clone(), t.clone()) + ); + + }; + } + + fn init_activation_shape(&self, ctx: &super::ConfigContext) { + let network = &self.config_frp.network; + let config_frp = &self.config_frp; + let widgets_frp = ctx.frp(); + let styles = ctx.styles(); + let activation_shape = &self.activation_shape; + let focus_receiver = &self.display_object; + frp::extend! { network + is_hovered <- widgets_frp.on_port_hover.map2(&config_frp.current_crumbs, |h, crumbs| { + h.on().map_or(false, |h| crumbs.starts_with(h)) + }); + is_connected_or_hovered <- config_frp.is_connected || is_hovered; + activation_shape_theme <- is_connected_or_hovered.map(|is_connected_or_hovered| { + if *is_connected_or_hovered { + Some(theme::widget::activation_shape::connected) + } else { + Some(theme::widget::activation_shape::base) + } + }); + activation_shape_theme <- activation_shape_theme.on_change(); + eval activation_shape_theme([styles, activation_shape](path) { + if let Some(path) = path { + let color = styles.get_color(path); + let rgba = color::Rgba::from(color); + activation_shape.color.set(rgba.into()); + } + }); + + let dot_mouse_down = activation_shape.on_event::(); + dot_clicked <- dot_mouse_down.filter(mouse::is_primary); + set_focused <- dot_clicked.map(f!([focus_receiver](_) !focus_receiver.is_focused())); + eval set_focused([focus_receiver](focus) match focus { + true => focus_receiver.focus(), + false => focus_receiver.blur(), + }); + + }; + } +} fn entry_for_current_value( all_entries: &[Entry], @@ -303,15 +315,13 @@ fn entry_for_current_value( // Handle parentheses in current value. Entries with parenthesized expressions will match if // they start with the same expression as the current value. That way it is still matched // once extra arguments are added to the nested function call. - if current_value.starts_with('(') { + current_value.starts_with('(').and_option_from(|| { let current_value = current_value.trim_start_matches('(').trim_end_matches(')'); all_entries.iter().find(|entry| { let trimmed_value = entry.value.trim_start_matches('(').trim_end_matches(')'); current_value.starts_with(trimmed_value) }) - } else { - None - } + }) }); let with_fallback = @@ -319,45 +329,80 @@ fn entry_for_current_value( Some(with_fallback) } -/// A wrapper for dropdown that only initializes it when it is first opened. + + +/// ==================== +/// === LazyDropdown === +/// ==================== + +/// A wrapper for dropdown that can be initialized lazily, with all required FRP endpoints to drive +/// it as if was just an ordinary view. Before calling `lazy_init` for the first time, the overhead +/// is minimal, as the actual dropdown view is not created. #[derive(Debug)] struct LazyDropdown { - app: ensogl::application::Application, - set_all_entries: frp::Sampler>, - set_selected_entries: frp::Sampler>, - set_open: frp::Sampler, - selected_entries: frp::Any>, - user_select_action: frp::Any<()>, - dropdown: Option>, + app: ensogl::application::Application, + set_all_entries: frp::Any>, + set_selected_entries: frp::Any>, + set_open: frp::Any, + sampled_set_all_entries: frp::Sampler>, + sampled_set_selected_entries: frp::Sampler>, + sampled_set_open: frp::Sampler, + selected_entries: frp::Any>, + user_select_action: frp::Any<()>, + dropdown: Option>, } impl LazyDropdown { - fn init(&mut self, parent: &display::object::Instance) { + fn new(app: &ensogl::application::Application, network: &frp::Network) -> Self { + frp::extend! { network + set_all_entries <- any(...); + set_selected_entries <- any(...); + set_open <- any(...); + selected_entries <- any(...); + user_select_action <- any(...); + sampled_set_all_entries <- set_all_entries.sampler(); + sampled_set_selected_entries <- set_selected_entries.sampler(); + sampled_set_open <- set_open.sampler(); + } + + Self { + app: app.clone_ref(), + set_all_entries, + set_selected_entries, + set_open, + selected_entries, + user_select_action, + sampled_set_all_entries, + sampled_set_selected_entries, + sampled_set_open, + dropdown: None, + } + } + + /// Perform initialization that actually creates the dropdown. Should be done only once there is + /// a request to open the dropdown. + fn lazy_init(&mut self, parent: &display::object::Instance) { if self.dropdown.is_some() { return; } - let dropdown = self.app.new_view::>(); - let dropdown = self.dropdown.insert(dropdown); - + let dropdown = self.dropdown.insert(self.app.new_view::>()); parent.add_child(dropdown); - let layers = &self.app.display.default_scene.layers; - layers.above_nodes.add(&*dropdown); - dropdown.set_y(DROPDOWN_Y_OFFSET); - dropdown.set_max_open_size(DROPDOWN_MAX_SIZE); - dropdown.allow_deselect_all(true); + self.app.display.default_scene.layers.above_nodes.add(&*dropdown); frp::extend! { _network - dropdown.set_all_entries <+ self.set_all_entries; - dropdown.set_selected_entries <+ self.set_selected_entries; - dropdown.set_open <+ self.set_open; + dropdown.set_all_entries <+ self.sampled_set_all_entries; + dropdown.set_selected_entries <+ self.sampled_set_selected_entries; + dropdown.set_open <+ self.sampled_set_open; self.selected_entries <+ dropdown.selected_entries; self.user_select_action <+ dropdown.user_select_action; } - - dropdown.set_all_entries.emit(self.set_all_entries.value()); - dropdown.set_selected_entries.emit(self.set_selected_entries.value()); - dropdown.set_open.emit(self.set_open.value()); + dropdown.set_y(DROPDOWN_Y_OFFSET); + dropdown.set_max_open_size(DROPDOWN_MAX_SIZE); + dropdown.allow_deselect_all(true); + dropdown.set_all_entries(self.sampled_set_all_entries.value()); + dropdown.set_selected_entries(self.sampled_set_selected_entries.value()); + dropdown.set_open(self.sampled_set_open.value()); } } diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 4620e9e4edde..5a438da59c41 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -1622,12 +1622,12 @@ impl GraphEditorModelWithNetwork { model.frp.private.output.hover_node_output.emit(output); }); - eval node_model.input.frp.input_edges_need_refresh( - (_) model.frp.private.output.node_incoming_edge_updates.emit(node_id) + eval_ node_model.input.frp.input_edges_need_refresh( + model.frp.private.output.node_incoming_edge_updates.emit(node_id) ); - eval node_model.input.frp.width( - (_) model.frp.private.output.node_outgoing_edge_updates.emit(node_id) + eval_ node_model.input.frp.width( + model.frp.private.output.node_outgoing_edge_updates.emit(node_id) ); let neutral_color = model.styles_frp.get_color(theme::code::types::any::selection); diff --git a/lib/rust/ensogl/core/src/animation/loops.rs b/lib/rust/ensogl/core/src/animation/loops.rs index 127e2b205d49..d129fa16c8a7 100644 --- a/lib/rust/ensogl/core/src/animation/loops.rs +++ b/lib/rust/ensogl/core/src/animation/loops.rs @@ -171,8 +171,8 @@ crate::define_endpoints_2! { on_frame_start(Duration), on_before_animations(TimeInfo), on_after_animations(TimeInfo), + on_before_layout(TimeInfo), on_before_rendering(TimeInfo), - on_before_rendering_late(TimeInfo), frame_end(TimeInfo), } } @@ -203,16 +203,16 @@ pub fn on_after_animations() -> enso_frp::Sampler { LOOP_REGISTRY.with(|registry| registry.on_after_animations.clone_ref()) } +/// Fires before the layout is performed. +pub fn on_before_layout() -> enso_frp::Sampler { + LOOP_REGISTRY.with(|registry| registry.on_before_layout.clone_ref()) +} + /// Fires before the rendering is performed. pub fn on_before_rendering() -> enso_frp::Sampler { LOOP_REGISTRY.with(|registry| registry.on_before_rendering.clone_ref()) } -/// Fires before the late stage of rendering is performed. -pub fn on_before_rendering_late() -> enso_frp::Sampler { - LOOP_REGISTRY.with(|registry| registry.on_before_rendering_late.clone_ref()) -} - /// A wrapper for JavaScript `requestAnimationFrame` loop. It allows registering callbacks and also /// exposes FRP endpoints that will emit signals on every loop iteration. #[derive(CloneRef, Derivative, Deref)] @@ -308,8 +308,8 @@ fn on_frame_closure( let on_frame_start = output.on_frame_start.clone_ref(); let on_before_animations = output.on_before_animations.clone_ref(); let on_after_animations = output.on_after_animations.clone_ref(); + let on_before_layout = output.on_before_layout.clone_ref(); let on_before_rendering = output.on_before_rendering.clone_ref(); - let on_before_rendering_late = output.on_before_rendering_late.clone_ref(); let frame_end = output.frame_end.clone_ref(); let before_animations = before_animations.clone_ref(); let animations = animations.clone_ref(); @@ -323,9 +323,9 @@ fn on_frame_closure( .then(move || fixed_fps_sampler.borrow_mut().run(time_info, |t| animations.run_all(t))) .then(move || on_after_animations.emit(time_info)) .then(move || frame_end.emit(time_info)) - .then(move || on_before_rendering.emit(time_info)) + .then(move || on_before_layout.emit(time_info)) .then(move || { - on_before_rendering_late.emit(time_info); + on_before_rendering.emit(time_info); drop(_profiler); }) .schedule(); diff --git a/lib/rust/ensogl/core/src/display/scene.rs b/lib/rust/ensogl/core/src/display/scene.rs index 3c121dfafaab..9a35fb6a22dc 100644 --- a/lib/rust/ensogl/core/src/display/scene.rs +++ b/lib/rust/ensogl/core/src/display/scene.rs @@ -1213,7 +1213,7 @@ impl Deref for Scene { } impl Scene { - /// Perform early phase of scene update. This includes updating camera and the layout of all + /// Perform layout phase of scene update. This includes updating camera and the layout of all /// display objects. No GPU buffers are updated yet, giving the opportunity to perform /// additional updates that affect the layout of display objects after the main scene layout /// has been performed. @@ -1223,7 +1223,7 @@ impl Scene { /// method to be manually called on affected objects in order to affect rendering /// during this frame. #[profile(Debug)] - pub fn early_update(&self, time: animation::TimeInfo) -> UpdateStatus { + pub fn update_layout(&self, time: animation::TimeInfo) -> UpdateStatus { if self.context.borrow().is_some() { debug_span!("Early update.").in_scope(|| { let mut scene_was_dirty = false; @@ -1239,11 +1239,11 @@ impl Scene { } } - /// Perform late phase of scene update. At this point, all display object state is being + /// Perform rendering phase of scene update. At this point, all display object state is being /// committed for rendering. This includes updating the layer stack, refreshing GPU buffers and /// handling mouse events. #[profile(Debug)] - pub fn late_update( + pub fn update_rendering( &self, time: animation::TimeInfo, early_status: UpdateStatus, diff --git a/lib/rust/ensogl/core/src/display/world.rs b/lib/rust/ensogl/core/src/display/world.rs index 61db40f7f84e..8614d653ce42 100644 --- a/lib/rust/ensogl/core/src/display/world.rs +++ b/lib/rust/ensogl/core/src/display/world.rs @@ -330,14 +330,14 @@ impl WorldDataWithLoop { let frp = Frp::new(); let data = WorldData::new(&frp.private.output); let on_frame_start = animation::on_frame_start(); + let on_before_layout = animation::on_before_layout(); let on_before_rendering = animation::on_before_rendering(); - let on_before_rendering_late = animation::on_before_rendering_late(); let network = frp.network(); crate::frp::extend! {network eval on_frame_start ((t) data.run_stats(*t)); - early_update <- on_before_rendering.map(f!((t) data.run_next_frame_early(*t))); - _eval <- on_before_rendering_late.map2(&early_update, - f!((t, early) data.run_next_frame_late(*t, *early)) + layout_update <- on_before_layout.map(f!((t) data.run_next_frame_layout(*t))); + _eval <- on_before_rendering.map2(&layout_update, + f!((t, early) data.run_next_frame_rendering(*t, *early)) ); } @@ -559,29 +559,29 @@ impl WorldData { } } - /// Perform to the early step of next frame simulation with the provided time information. - /// See [`Scene::early_update`] for information about actions performed in this step. + /// Perform to the layout step of next frame simulation with the provided time information. + /// See [`Scene::update_layout`] for information about actions performed in this step. /// /// Please note that the provided time information from the [`requestAnimationFrame`] JS /// function is more precise than time obtained from the [`window.performance().now()`] one. /// Follow this link to learn more: /// https://stackoverflow.com/questions/38360250/requestanimationframe-now-vs-performance-now-time-discrepancy. #[profile(Objective)] - pub fn run_next_frame_early(&self, time: animation::TimeInfo) -> UpdateStatus { + pub fn run_next_frame_layout(&self, time: animation::TimeInfo) -> UpdateStatus { self.on.before_frame.run_all(time); self.uniforms.time.set(time.since_animation_loop_started.unchecked_raw()); self.scene_dirty.unset_all(); - self.default_scene.early_update(time) + self.default_scene.update_layout(time) } - /// perform to the late step of next frame simulation with the provided time information. - /// See [`Scene::late_update`] for information about actions performed in this step. + /// perform to the rendering step of next frame simulation with the provided time information. + /// See [`Scene::update_rendering`] for information about actions performed in this step. /// /// Apart from the scene late update, this function also performs garbage collection and actual /// rendering of the scene using updated GPU buffers. #[profile(Objective)] - pub fn run_next_frame_late(&self, time: animation::TimeInfo, early_status: UpdateStatus) { - let update_status = self.default_scene.late_update(time, early_status); + pub fn run_next_frame_rendering(&self, time: animation::TimeInfo, early_status: UpdateStatus) { + let update_status = self.default_scene.update_rendering(time, early_status); self.garbage_collector.mouse_events_handled(); self.default_scene.render(update_status); self.on.after_frame.run_all(time); From b7eb3b79182be0fd5e55090ee8aae6e5f870753a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 25 Apr 2023 01:01:42 +0200 Subject: [PATCH 39/45] make it clear that span tree holds parent offset --- app/gui/language/span-tree/src/builder.rs | 8 +++--- app/gui/language/span-tree/src/generate.rs | 33 +++++++++------------- app/gui/language/span-tree/src/node.rs | 12 ++++---- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/app/gui/language/span-tree/src/builder.rs b/app/gui/language/span-tree/src/builder.rs index efb8d970c2c4..8bc702ef3e8e 100644 --- a/app/gui/language/span-tree/src/builder.rs +++ b/app/gui/language/span-tree/src/builder.rs @@ -31,12 +31,12 @@ pub trait Builder: Sized { let kind = kind.into(); let node = Node::::new().with_kind(kind).with_size(len.into()); let prev_child = self.node_being_built().children.last(); - let prev_child_end = prev_child.map_or(0, |c| (c.offset + c.node.size).as_usize()); - let sibling_offset = offset.saturating_sub(prev_child_end); + let prev_child_end = prev_child.map_or(0, |c| (c.parent_offset + c.node.size).as_usize()); + let parent_offset = prev_child_end + offset; let child = node::Child { node, - offset: offset.into(), - sibling_offset: sibling_offset.into(), + parent_offset: parent_offset.into(), + sibling_offset: offset.into(), ast_crumbs: crumbs.into_crumbs(), }; ChildBuilder { built: child, parent: self } diff --git a/app/gui/language/span-tree/src/generate.rs b/app/gui/language/span-tree/src/generate.rs index 4c403726ca4f..4ded852758a8 100644 --- a/app/gui/language/span-tree/src/generate.rs +++ b/app/gui/language/span-tree/src/generate.rs @@ -118,9 +118,9 @@ impl ChildGenerator { } fn add_node(&mut self, ast_crumbs: ast::Crumbs, node: Node) -> &mut node::Child { - let offset = self.current_offset; + let parent_offset = self.current_offset; let sibling_offset = self.sibling_offset; - let child = node::Child { node, offset, sibling_offset, ast_crumbs }; + let child = node::Child { node, parent_offset, sibling_offset, ast_crumbs }; self.current_offset += child.node.size; self.sibling_offset = 0.byte_diff(); self.children.push(child); @@ -128,21 +128,16 @@ impl ChildGenerator { } fn generate_empty_node(&mut self, insert_type: InsertionPointType) -> &mut node::Child { - let child = node::Child { - node: Node::::new().with_kind(insert_type), - offset: self.current_offset, - sibling_offset: self.sibling_offset, - ast_crumbs: vec![], - }; - self.sibling_offset = 0.byte_diff(); - self.children.push(child); - self.children.last_mut().unwrap() + self.add_node(vec![], Node::::new().with_kind(insert_type)) } fn reverse_children(&mut self) { self.children.reverse(); + let mut last_parent_offset = 0.byte_diff(); for child in &mut self.children { - child.offset = self.current_offset - child.offset - child.node.size; + child.parent_offset = self.current_offset - child.parent_offset - child.node.size; + child.sibling_offset = child.parent_offset - last_parent_offset; + last_parent_offset = child.parent_offset; } } @@ -822,12 +817,12 @@ fn tree_generate_node( if let Some(leaf_info) = &tree.leaf_info { size = ByteDiff::from(leaf_info.len()); } else { - let mut offset = ByteDiff::from(0); + let mut parent_offset = ByteDiff::from(0); let mut sibling_offset = ByteDiff::from(0); for (index, raw_span_info) in tree.span_info.iter().enumerate() { match raw_span_info { SpanSeed::Space(ast::SpanSeedSpace { space }) => { - offset += ByteDiff::from(space); + parent_offset += ByteDiff::from(space); sibling_offset += ByteDiff::from(space); } SpanSeed::Token(ast::SpanSeedToken { token }) => { @@ -835,8 +830,8 @@ fn tree_generate_node( let size = ByteDiff::from(token.len()); let ast_crumbs = vec![TreeCrumb { index }.into()]; let node = Node { kind, size, ..default() }; - children.push(node::Child { node, offset, sibling_offset, ast_crumbs }); - offset += size; + children.push(node::Child { node, parent_offset, sibling_offset, ast_crumbs }); + parent_offset += size; sibling_offset = 0.byte_diff(); } SpanSeed::Child(ast::SpanSeedChild { node }) => { @@ -844,13 +839,13 @@ fn tree_generate_node( let node = node.generate_node(kind, context)?; let child_size = node.size; let ast_crumbs = vec![TreeCrumb { index }.into()]; - children.push(node::Child { node, offset, sibling_offset, ast_crumbs }); - offset += child_size; + children.push(node::Child { node, parent_offset, sibling_offset, ast_crumbs }); + parent_offset += child_size; sibling_offset = 0.byte_diff(); } } } - size = offset; + size = parent_offset; } let payload = default(); Ok(Node { kind, parenthesized, size, children, ast_id, payload }) diff --git a/app/gui/language/span-tree/src/node.rs b/app/gui/language/span-tree/src/node.rs index 1dd3bc0037c8..ebc4a4744f86 100644 --- a/app/gui/language/span-tree/src/node.rs +++ b/app/gui/language/span-tree/src/node.rs @@ -183,7 +183,7 @@ pub struct Child { /// A child node. pub node: Node, /// An offset counted from the parent node starting index to the start of this node's span. - pub offset: ByteDiff, + pub parent_offset: ByteDiff, /// The offset counted from the end of previous sibling node. pub sibling_offset: ByteDiff, /// AST crumbs which lead from parent to child associated AST node. @@ -194,10 +194,10 @@ impl Child { /// Payload mapping utility. pub fn map(self, f: impl Copy + Fn(T) -> S) -> Child { let node = self.node.map(f); - let offset = self.offset; + let parent_offset = self.parent_offset; let ast_crumbs = self.ast_crumbs; let sibling_offset = self.sibling_offset; - Child { node, offset, sibling_offset, ast_crumbs } + Child { node, parent_offset, sibling_offset, ast_crumbs } } } @@ -370,7 +370,7 @@ impl<'a, T> Ref<'a, T> { None => Err(InvalidCrumb::new(node.children.len(), index, &crumbs).into()), Some(child) => { let node = &child.node; - span_offset += child.offset; + span_offset += child.parent_offset; let sibling_offset = child.sibling_offset; let crumbs = crumbs.into_sub(index); ast_crumbs.extend_from_slice(&child.ast_crumbs); @@ -617,9 +617,9 @@ impl<'a, T> RefMut<'a, T> { crumbs: Crumbs, mut ast_crumbs: ast::Crumbs, ) -> RefMut<'a, T> { - let offset = child.offset; + let offset = child.parent_offset; let node = &mut child.node; - span_begin += child.offset; + span_begin += child.parent_offset; let crumbs = crumbs.into_sub(index); ast_crumbs.extend(child.ast_crumbs.iter().cloned()); Self { node, offset, span_offset: span_begin, crumbs, ast_crumbs } From cf7a04bc3d3f5a22d690ec90503cb142dce66f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 25 Apr 2023 12:04:25 +0200 Subject: [PATCH 40/45] fixes after rebase --- .../view/graph-editor/src/component/node.rs | 2 + .../src/component/node/input/area.rs | 3 +- .../src/component/node/input/port.rs | 3 +- .../src/component/node/input/widget.rs | 4 + .../node/input/widget/list_editor.rs | 138 ++++++++++-------- .../node/input/widget/single_choice.rs | 10 +- .../src/display/shape/compound/rectangle.rs | 10 +- 7 files changed, 104 insertions(+), 66 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index d46cb4f3b4cc..6a078b87e79a 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -333,6 +333,8 @@ ensogl::define_endpoints_2! { /// Indicate whether on hover the quick action icons should appear. show_quick_action_bar_on_hover (bool), set_execution_environment (ExecutionEnvironment), + + /// Set read-only mode for input ports. set_read_only (bool), } Output { diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index c2f3c8730e30..11993b6ca276 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -277,7 +277,7 @@ impl Model { #[profile(Debug)] fn set_expression(&self, new_expression: impl Into, area_frp: &FrpEndpoints) { let new_expression = Expression::from(new_expression.into()); - warn!("Set expression: \n{:?}", new_expression.tree_pretty_printer()); + debug!("Set expression: \n{:?}", new_expression.tree_pretty_printer()); self.widget_tree.rebuild_tree( &new_expression.span_tree, @@ -514,6 +514,7 @@ impl Area { finished <- frp.set_profiling_status.map(|s| s.is_finished()); profiled <- in_profiling_mode && finished; + model.widget_tree.set_read_only <+ frp.set_read_only; model.widget_tree.set_view_mode <+ frp.set_view_mode; model.widget_tree.set_profiling_status <+ frp.set_profiling_status; diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index c65377f3e947..60c63b69626a 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -115,8 +115,7 @@ impl Port { port_shape.set_corner_radius_max().set_pointer_events(false); hover_shape .set_pointer_events(true) - .set_color(shape::INVISIBLE_HOVER_COLOR) - .set_border_color(shape::INVISIBLE_HOVER_COLOR); + .set_color(shape::INVISIBLE_HOVER_COLOR); port_root.add_child(&widget_root); widget_root.set_margin_left(0.0); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index bce94bc4591d..673051a76e76 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -85,6 +85,7 @@ pub const PRIMARY_PORT_MAX_NESTING_LEVEL: usize = 0; ensogl::define_endpoints_2! { Input { set_ports_visible (bool), + set_read_only (bool), set_view_mode (crate::view::Mode), set_profiling_status (crate::node::profiling::Status), set_disabled (bool), @@ -387,6 +388,7 @@ impl DropdownValue for Entry { #[derive(Debug, Clone, CloneRef)] pub struct WidgetsFrp { pub(super) set_ports_visible: frp::Sampler, + pub(super) set_read_only: frp::Sampler, pub(super) set_view_mode: frp::Sampler, pub(super) set_profiling_status: frp::Sampler, pub(super) value_changed: frp::Any<(span_tree::Crumbs, Option)>, @@ -434,6 +436,7 @@ impl Tree { frp.private.output.rebuild_required <+ frp.marked_dirty_sync.debounce(); set_ports_visible <- frp.set_ports_visible.sampler(); + set_read_only <- frp.set_read_only.sampler(); set_view_mode <- frp.set_view_mode.sampler(); set_profiling_status <- frp.set_profiling_status.sampler(); @@ -449,6 +452,7 @@ impl Tree { let connected_port_updated = frp.private.output.connected_port_updated.clone_ref(); let widgets_frp = WidgetsFrp { set_ports_visible, + set_read_only, set_view_mode, set_profiling_status, value_changed, diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs b/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs index 4af15f84f9f6..044ebee7806d 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs @@ -9,13 +9,16 @@ use crate::prelude::*; use crate::component::node::input::widget::single_choice::triangle; use crate::component::node::input::widget::single_choice::ACTIVATION_SHAPE_SIZE; use crate::component::node::input::widget::Configuration; +use crate::component::node::input::widget::WidgetsFrp; use ensogl::application::Application; use ensogl::control::io::mouse; -use ensogl::data::color; use ensogl::display; +use ensogl::display::object::event; +use ensogl::display::shape::StyleWatch; use ensogl_component::list_editor::ListEditor; use ensogl_component::text::Text; +use ensogl_hardcoded_theme as theme; @@ -39,14 +42,13 @@ ensogl::define_endpoints_2! { /// The component does not handle nested arrays well. They should be fixed once [integrated into /// new widget hierarchy](https://github.com/enso-org/enso/issues/5923). #[derive(Clone, CloneRef, Debug)] -pub struct Model { - network: frp::Network, - display_object: display::object::Instance, - list_container: display::object::Instance, - activation_shape: triangle::View, - list: ListEditor, - /// FRP input informing about the port size. - pub set_port_size: frp::Source, +pub struct Widget { + config_frp: Frp, + display_object: display::object::Instance, + child_container: display::object::Instance, + list_container: display::object::Instance, + activation_shape: triangle::View, + list: ListEditor, } impl Widget { @@ -54,14 +56,19 @@ impl Widget { const GAP: f32 = 3.0; /// Create Model for Vector Editor widget. - pub fn new(app: &Application, parent: &display::object::Instance, frp: &SampledFrp) -> Self { + pub fn new(app: &Application, widgets_frp: &WidgetsFrp, styles: &StyleWatch) -> Self { let network = frp::Network::new("vector_editor::Model"); let display_object = display::object::Instance::new(); let list_container = display::object::Instance::new(); + let child_container = display::object::Instance::new(); let activation_shape = triangle::View::new(); let list = ListEditor::new(&app.cursor); + let toggle_color = styles.get_color(theme::widget::activation_shape::connected); activation_shape.set_size(ACTIVATION_SHAPE_SIZE); + activation_shape.color.set(toggle_color.into()); + + display_object.add_child(&child_container); display_object.add_child(&list_container); display_object.add_child(&activation_shape); display_object @@ -70,58 +77,52 @@ impl Widget { .set_gap_y(Self::GAP) .set_children_alignment_center(); display_object.set_size_hug(); - parent.add_child(&display_object); - - frp::extend! { network - set_port_size <- source::(); - } - Self { network, display_object, list_container, activation_shape, list, set_port_size } - .init_toggle(frp) - .init_list_updates(app, frp) - .init_port_size_update() + let config_frp = Frp::new(); + Self { config_frp, display_object, child_container, list_container, activation_shape, list } + .init_toggle(widgets_frp) + .init_list_updates(app, widgets_frp) } - fn init_toggle(self, frp: &SampledFrp) -> Self { - let network = &self.network; + fn init_toggle(self, widgets_frp: &WidgetsFrp) -> Self { + let network = &self.config_frp.network; let display_object = &self.display_object; let activation_shape = &self.activation_shape; let list_container = &self.list_container; let list = &self.list; let dot_clicked = self.activation_shape.on_event::(); + let focus_in = self.display_object.on_event::(); + let focus_out = self.display_object.on_event::(); frp::extend! { network init <- source_(); - toggle_focus <- dot_clicked.map(f!([display_object](_) !display_object.is_focused())); - set_focused <- any(toggle_focus, frp.set_focused); - eval set_focused([display_object, list_container, list](focus) match focus { - true => { - display_object.focus(); - list_container.add_child(&list); - }, - false => { - display_object.blur(); - list_container.remove_child(&list); - }, + set_focused <- dot_clicked.map(f!([display_object](_) !display_object.is_focused())); + eval set_focused([display_object](focus) match focus { + true => display_object.focus(), + false => display_object.blur(), }); - set_visible <- all(&frp.set_visible, &init)._0(); - shape_alpha <- set_visible.map(|visible| if *visible { 1.0 } else { 0.0 }); - shape_color <- shape_alpha.map(|a| ACTIVATION_SHAPE_COLOR.with_alpha(*a)); - eval shape_color([activation_shape] (color) { - activation_shape.color.set(color::Rgba::from(color).into()); + readonly_set <- widgets_frp.set_read_only.on_true(); + do_open <- focus_in.gate_not(&widgets_frp.set_read_only); + do_close <- any_(focus_out, readonly_set); + is_open <- bool(&do_close, &do_open).on_change(); + + eval is_open([list_container, list](open) match open { + true => list_container.add_child(&list), + false => list_container.remove_child(&list), }); } init.emit(()); self } - fn init_list_updates(self, app: &Application, frp: &SampledFrp) -> Self { - let network = &self.network; + fn init_list_updates(self, app: &Application, widgets_frp: &WidgetsFrp) -> Self { + let config_frp = &self.config_frp; + let network = &config_frp.network; let list = &self.list; frp::extend! { network init <- source_(); - value <- all(frp.set_current_value, init)._0(); + value <- all(config_frp.current_value, init)._0(); non_empty_value <- value.filter_map(|v| v.clone()); empty_value <- value.filter_map(|v| v.is_none().then_some(())); eval non_empty_value ([list, app](val) Self::update_list(&app, val.as_str(), &list)); @@ -129,27 +130,15 @@ impl Widget { code_changed_by_user <- list.request_new_item.map(f_!([app, list] Self::push_new_element(&app, &list))); - frp.out_value_changed <+ code_changed_by_user.map(f_!([list] { + value_changed <- code_changed_by_user.map(f_!([list] { Some(ImString::new(Self::construct_code(&list))) })); - } - init.emit(()); - self - } + widgets_frp.value_changed <+ value_changed.map2(&config_frp.current_crumbs, + move |t: &Option, crumbs: &span_tree::Crumbs| (crumbs.clone(), t.clone()) + ); - fn init_port_size_update(self) -> Self { - let network = &self.network; - let display_object = &self.display_object; - let on_transformed = self.display_object.on_transformed.clone_ref(); - let set_port_size = &self.set_port_size; - frp::extend! { network - widget_size <- on_transformed.map(f!((()) display_object.computed_size())).on_change(); - port_and_widget_size <- all(set_port_size, &widget_size); - eval port_and_widget_size ([display_object]((port_sz, sz)) { - display_object.set_x(port_sz.x() / 2.0 - sz.x() / 2.0); - display_object.set_y(-port_sz.y() - sz.y() - 5.0); - }); } + init.emit(()); self } @@ -205,3 +194,38 @@ impl Widget { opt_iterator.into_iter().flatten() } } + +#[derive(Debug, Clone, PartialEq)] +/// VectorEditor widget configuration options. +pub struct Config { + /// Configuration of inner element widgets. If not present, the child widget types have to be + /// automatically inferred. + #[allow(dead_code)] + pub item_widget: Option>, + /// Default expression to insert when adding new elements. + #[allow(dead_code)] + pub item_default: ImString, +} + +impl super::SpanWidget for Widget { + type Config = Config; + + fn root_object(&self) -> &display::object::Instance { + &self.display_object + } + + fn new(_: &Config, ctx: &super::ConfigContext) -> Self { + Self::new(ctx.app(), ctx.frp(), ctx.styles()) + } + + fn configure(&mut self, _: &Config, ctx: super::ConfigContext) { + let current_value: Option = Some(ctx.expression_at(ctx.span_node.span()).into()); + self.config_frp.current_value(current_value); + self.config_frp.current_crumbs(ctx.span_node.crumbs.clone()); + + let child_level = ctx.info.nesting_level.next_if(ctx.span_node.is_argument()); + let label_meta = super::Configuration::always(super::label::Config); + let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, label_meta); + self.child_container.replace_children(&[child]); + } +} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 6ac858a85be8..cb505152699d 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -196,13 +196,14 @@ impl super::SpanWidget for Widget { impl Widget { fn init(self, ctx: &super::ConfigContext) -> Self { - let is_open = self.init_dropdown_focus(); + let is_open = self.init_dropdown_focus(ctx); self.init_dropdown_values(ctx, is_open); self.init_activation_shape(ctx); self } - fn init_dropdown_focus(&self) -> frp::Stream { + fn init_dropdown_focus(&self, ctx: &super::ConfigContext) -> frp::Stream { + let widgets_frp = ctx.frp(); let focus_receiver = self.display_object.clone_ref(); let focus_in = focus_receiver.on_event::(); let focus_out = focus_receiver.on_event::(); @@ -214,7 +215,10 @@ impl Widget { eval focus_in([dropdown, dropdown_wrapper](_) { dropdown.borrow_mut().lazy_init(&dropdown_wrapper); }); - is_open <- bool(&focus_out, &focus_in); + readonly_set <- widgets_frp.set_read_only.on_true(); + do_open <- focus_in.gate_not(&widgets_frp.set_read_only); + do_close <- any_(focus_out, readonly_set); + is_open <- bool(&do_close, &do_open); dropdown_frp.set_open <+ is_open.on_change(); // Close the dropdown after a short delay after selection. Because the dropdown diff --git a/lib/rust/ensogl/core/src/display/shape/compound/rectangle.rs b/lib/rust/ensogl/core/src/display/shape/compound/rectangle.rs index 5e8b849de5ac..9bc44da15182 100644 --- a/lib/rust/ensogl/core/src/display/shape/compound/rectangle.rs +++ b/lib/rust/ensogl/core/src/display/shape/compound/rectangle.rs @@ -106,7 +106,9 @@ impl Rectangle { /// Constructor. pub fn new() -> Self { - Self::default() + Self::default().build(|r| { + r.set_border_color(display::shape::INVISIBLE_HOVER_COLOR); + }) } /// Builder-style modifier, allowing setting shape properties without creating a temporary @@ -145,13 +147,15 @@ impl Rectangle { /// Set the border size of the shape. If you want to use border, you should always set the inset /// at least of the size of the border. If you do not want the border to be animated, you can - /// use [`Self::set_inset_border`] instead. + /// use [`Self::set_inset_border`] instead. To make the border visible, you also need to set the + /// border color using [`Self::set_border_color`]. pub fn set_border(&self, border: f32) -> &Self { self.modify_view(|view| view.border.set(border)) } /// Set both the inset and border at once. See documentation of [`Self::set_border`] and - /// [`Self::set_inset`] to learn more. + /// [`Self::set_inset`] to learn more. To make the border visible, you also need to set the + /// border color using [`Self::set_border_color`]. pub fn set_inset_border(&self, border: f32) -> &Self { self.set_inset(border).set_border(border) } From e5190d55f2c22303f7102fe617a145938b01b33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 25 Apr 2023 16:06:08 +0200 Subject: [PATCH 41/45] self-review --- app/gui/language/span-tree/src/action.rs | 12 +- app/gui/language/span-tree/src/builder.rs | 12 +- app/gui/language/span-tree/src/generate.rs | 6 +- app/gui/language/span-tree/src/node.rs | 22 +-- app/gui/src/controller/graph/widget.rs | 24 ++-- .../controller/graph/widget/configuration.rs | 9 +- .../src/controller/graph/widget/response.rs | 12 +- .../view/graph-editor/src/component/node.rs | 8 +- .../src/component/node/input/area.rs | 23 ++-- .../src/component/node/input/port.rs | 9 +- .../src/component/node/input/widget.rs | 129 ++++++++++-------- .../component/node/input/widget/hierarchy.rs | 22 ++- .../src/component/node/input/widget/label.rs | 25 ++-- .../node/input/widget/list_editor.rs | 2 - .../node/input/widget/single_choice.rs | 39 ++---- app/gui/view/graph-editor/src/lib.rs | 25 ++-- build-config.yaml | 2 +- .../core/src/display/object/instance.rs | 1 - 18 files changed, 190 insertions(+), 192 deletions(-) diff --git a/app/gui/language/span-tree/src/action.rs b/app/gui/language/span-tree/src/action.rs index a09767e3157e..cdd94916342f 100644 --- a/app/gui/language/span-tree/src/action.rs +++ b/app/gui/language/span-tree/src/action.rs @@ -15,9 +15,9 @@ use ast::Ast; -/// ============== -/// === Errors === -/// ============== +// ============== +// === Errors === +// ============== /// Error returned when tried to perform an action which is not available for specific SpanTree /// node. @@ -35,9 +35,9 @@ pub struct AstSpanTreeMismatch; -/// ===================== -/// === Actions Trait === -/// ===================== +// ===================== +// === Actions Trait === +// ===================== /// Action enum used mainly for error messages. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] diff --git a/app/gui/language/span-tree/src/builder.rs b/app/gui/language/span-tree/src/builder.rs index 8bc702ef3e8e..a76b92f9524a 100644 --- a/app/gui/language/span-tree/src/builder.rs +++ b/app/gui/language/span-tree/src/builder.rs @@ -23,7 +23,7 @@ pub trait Builder: Sized { /// extend this branch of the tree. fn add_child( mut self, - offset: usize, + parent_offset: usize, len: usize, kind: impl Into, crumbs: impl IntoCrumbs, @@ -32,11 +32,11 @@ pub trait Builder: Sized { let node = Node::::new().with_kind(kind).with_size(len.into()); let prev_child = self.node_being_built().children.last(); let prev_child_end = prev_child.map_or(0, |c| (c.parent_offset + c.node.size).as_usize()); - let parent_offset = prev_child_end + offset; + let sibling_offset = prev_child_end.saturating_sub(parent_offset); let child = node::Child { node, parent_offset: parent_offset.into(), - sibling_offset: offset.into(), + sibling_offset: sibling_offset.into(), ast_crumbs: crumbs.into_crumbs(), }; ChildBuilder { built: child, parent: self } @@ -67,9 +67,9 @@ pub trait Builder: Sized { -/// ================ -/// === Builders === -/// ================ +// ================ +// === Builders === +// ================ // === SpanTree Builder === diff --git a/app/gui/language/span-tree/src/generate.rs b/app/gui/language/span-tree/src/generate.rs index 4ded852758a8..16461bb056b4 100644 --- a/app/gui/language/span-tree/src/generate.rs +++ b/app/gui/language/span-tree/src/generate.rs @@ -151,9 +151,9 @@ impl ChildGenerator { -/// ============================= -/// === Trait Implementations === -/// ============================= +// ============================= +// === Trait Implementations === +// ============================= /// Helper structure constructed from Ast that consists base of prefix application. /// diff --git a/app/gui/language/span-tree/src/node.rs b/app/gui/language/span-tree/src/node.rs index ebc4a4744f86..ee6a30a2e72c 100644 --- a/app/gui/language/span-tree/src/node.rs +++ b/app/gui/language/span-tree/src/node.rs @@ -573,25 +573,25 @@ impl<'a, T> Ref<'a, T> { #[derive(Debug)] pub struct RefMut<'a, T = ()> { /// The node's ref. - node: &'a mut Node, + node: &'a mut Node, /// An offset counted from the parent node start to the start of this node's span. - pub offset: ByteDiff, + pub parent_offset: ByteDiff, /// Span begin's offset counted from the root expression. - pub span_offset: Byte, + pub span_offset: Byte, /// Crumbs specifying this node position related to root. - pub crumbs: Crumbs, + pub crumbs: Crumbs, /// Ast crumbs locating associated AST node, related to the root's AST node. - pub ast_crumbs: ast::Crumbs, + pub ast_crumbs: ast::Crumbs, } impl<'a, T> RefMut<'a, T> { /// Constructor. pub fn new(node: &'a mut Node) -> Self { - let offset = default(); + let parent_offset = default(); let span_begin = default(); let crumbs = default(); let ast_crumbs = default(); - Self { node, offset, span_offset: span_begin, crumbs, ast_crumbs } + Self { node, parent_offset, span_offset: span_begin, crumbs, ast_crumbs } } /// Payload accessor. @@ -613,16 +613,16 @@ impl<'a, T> RefMut<'a, T> { fn child_from_ref( index: usize, child: &'a mut Child, - mut span_begin: Byte, + span_begin: Byte, crumbs: Crumbs, mut ast_crumbs: ast::Crumbs, ) -> RefMut<'a, T> { - let offset = child.parent_offset; + let parent_offset = child.parent_offset; let node = &mut child.node; - span_begin += child.parent_offset; + let span_offset = span_begin + parent_offset; let crumbs = crumbs.into_sub(index); ast_crumbs.extend(child.ast_crumbs.iter().cloned()); - Self { node, offset, span_offset: span_begin, crumbs, ast_crumbs } + Self { node, parent_offset, span_offset, crumbs, ast_crumbs } } /// Get the reference to child with given index. Fails if index if out of bounds. diff --git a/app/gui/src/controller/graph/widget.rs b/app/gui/src/controller/graph/widget.rs index 8ac56d40353d..2a15760e41de 100644 --- a/app/gui/src/controller/graph/widget.rs +++ b/app/gui/src/controller/graph/widget.rs @@ -26,9 +26,9 @@ use ide_view::graph_editor::CallWidgetsConfig; -/// ================= -/// === Constants === -/// ================= +// ================= +// === Constants === +// ================= /// A module containing the widget visualization method. const WIDGET_VISUALIZATION_MODULE: &str = "Standard.Visualization.Widgets"; @@ -252,9 +252,9 @@ impl Model { -/// ============================ -/// === NodeToWidgetsMapping === -/// ============================ +// ============================ +// === NodeToWidgetsMapping === +// ============================ /// A map of widgets attached to nodes. Used to perform cleanup of node widget queries when node is /// removed. @@ -299,9 +299,9 @@ impl NodeToWidgetsMapping { -/// =============== -/// === Request === -/// =============== +// =============== +// === Request === +// =============== /// Definition of a widget request. Defines the node subexpression that the widgets will be attached /// to, and the method call that corresponds to that expression. @@ -320,9 +320,9 @@ pub struct Request { -/// ================= -/// === QueryData === -/// ================= +// ================= +// === QueryData === +// ================= /// Data of ongoing widget query. Defines which expressions a visualization query is attached to, /// and maintains enough data to correlate the response with respective widget view. diff --git a/app/gui/src/controller/graph/widget/configuration.rs b/app/gui/src/controller/graph/widget/configuration.rs index f868b4a46214..880714b3a88d 100644 --- a/app/gui/src/controller/graph/widget/configuration.rs +++ b/app/gui/src/controller/graph/widget/configuration.rs @@ -15,9 +15,10 @@ use ide_view::graph_editor::ArgumentWidgetConfig; /// == deserialize_widget_definitions === /// ===================================== -/// Deserialize a list of widget configurations from visualization update data. Allows for partial -/// deserialization: if any of the widget configurations fails to deserialize, it will be skipped, -/// but the deserialization will continue. All errors are returned as a separate list. +/// Deserialize a list of widget configurations from definitions provided in visualization update +/// data. Allows for partial deserialization: if any of the widget definitions fails to deserialize, +/// it will be skipped, but the deserialization will continue. All errors are returned as a separate +/// list. pub fn deserialize_widget_definitions( data: &VisualizationUpdateData, ) -> (Vec, Vec) { @@ -32,7 +33,7 @@ pub fn deserialize_widget_definitions( })?; let meta = widget.map(to_configuration); let argument_name = argument_name.to_owned(); - Ok(ArgumentWidgetConfig { argument_name, meta }) + Ok(ArgumentWidgetConfig { argument_name, config: meta }) }, ); diff --git a/app/gui/src/controller/graph/widget/response.rs b/app/gui/src/controller/graph/widget/response.rs index bcd1f1a829a2..1bde5a5c005c 100644 --- a/app/gui/src/controller/graph/widget/response.rs +++ b/app/gui/src/controller/graph/widget/response.rs @@ -7,9 +7,9 @@ use ide_view::graph_editor::component::node::input::widget; -/// ========================= -/// === WidgetDefinitions === -/// ========================= +// ========================= +// === WidgetDefinitions === +// ========================= /// A top level object received from the widget visualization, which contains widget definitions for /// all arguments of a single Enso method. Configurations are paired with the name of function @@ -35,9 +35,9 @@ impl<'de: 'a, 'a> serde::Deserialize<'de> for FallableWidgetDefinition<'a> { -/// ======================== -/// === WidgetDefinition === -/// ======================== +// ======================== +// === WidgetDefinition === +// ======================== /// Widget definition provided from the engine. It is used to define how to display a widget of /// particular argument expression. When not provided, the default widget will be chosen based on diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index 6a078b87e79a..ebb65d90bf3e 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -1108,12 +1108,8 @@ pub mod test_utils { /// 3. If the output port is [`MultiPortView`]. fn output_port_shape(&self) -> Option; - /// 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`]. + /// Return the `Shape` of the first input port of the node. Returns `None` if there are no + /// input ports. fn input_port_hover_shape(&self) -> Option; } diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 11993b6ca276..986f4387b613 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -220,7 +220,7 @@ impl Model { } fn body_hover_pointer_style(&self, hovered: &bool) -> cursor::Style { - hovered.then(|| cursor::Style::cursor()).unwrap_or_default() + hovered.then(cursor::Style::cursor).unwrap_or_default() } fn port_hover_pointer_style(&self, hovered: &Switch) -> Option { @@ -247,7 +247,7 @@ impl Model { for definition in definitions.iter() { let argument_name = definition.argument_name.clone().into(); let override_key = OverrideKey { call_id: *call_id, argument_name }; - self.widget_tree.set_config_override(override_key, definition.meta.clone()); + self.widget_tree.set_config_override(override_key, definition.config.clone()); } } @@ -365,9 +365,9 @@ ensogl::define_endpoints! { on_port_code_update (Crumbs,ImString), on_background_press (), view_mode (view::Mode), - /// A set of widgets attached to a method requires metadata to be queried. The tuple - /// contains the ID of the call expression the widget is attached to, and the ID of that - /// call's target expression (`self` or first argument). + /// A set of widgets attached to a method requests their definitions to be queried from an + /// external source. The tuple contains the ID of the call expression the widget is attached + /// to, and the ID of that call's target expression (`self` or first argument). requested_widgets (ast::Id, ast::Id), request_import (ImString), /// A connected port within the node has been moved. Some edges might need to be updated. @@ -551,12 +551,7 @@ impl Area { /// A type of the specified port. pub fn port_type(&self, crumbs: &Crumbs) -> Option { let expression = self.model.expression.borrow(); - expression - .span_tree - .root_ref() - .get_descendant(crumbs) - .ok() - .and_then(|t| t.tp().map(|t| t.into())) + expression.span_tree.get_node(crumbs).ok().and_then(|t| t.tp().map(|t| t.into())) } /// Set a scene layer for text rendering. @@ -567,9 +562,9 @@ impl Area { -/// =================== -/// === CallInfoMap === -/// =================== +// =================== +// === CallInfoMap === +// =================== #[derive(Debug, Deref)] struct CallInfoMap { diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index 60c63b69626a..fba1b98d9dbf 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -88,8 +88,7 @@ impl HoverLayers { #[derive(Debug)] pub struct Port { /// Drop source must be kept at the top of the struct, so it will be dropped first. - #[allow(dead_code)] - on_cleanup: frp::DropSource, + _on_cleanup: frp::DropSource, crumbs: Rc>, port_root: display::object::Instance, widget_root: display::object::Instance, @@ -113,9 +112,7 @@ impl Port { let port_shape = PortShape::new(); let hover_shape = HoverShape::new(); port_shape.set_corner_radius_max().set_pointer_events(false); - hover_shape - .set_pointer_events(true) - .set_color(shape::INVISIBLE_HOVER_COLOR); + hover_shape.set_pointer_events(true).set_color(shape::INVISIBLE_HOVER_COLOR); port_root.add_child(&widget_root); widget_root.set_margin_left(0.0); @@ -178,7 +175,7 @@ impl Port { }; Self { - on_cleanup, + _on_cleanup: on_cleanup, port_shape, hover_shape, widget, diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 673051a76e76..1e9910d8be62 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -119,9 +119,9 @@ pub struct OverrideKey { } -/// ====================== -/// === Widget modules === -/// ====================== +// ====================== +// === Widget modules === +// ====================== /// Common trait for constructing and reconfiguring all widget variants. See "Widget Lifecycle" /// section of the module documentation for more details. @@ -226,9 +226,9 @@ define_widget_modules! { Hierarchy hierarchy, } -/// ===================== -/// === Configuration === -/// ===================== +// ===================== +// === Configuration === +// ===================== /// The configuration of a widget and its display properties. Defines how the widget should be /// displayed, if it should be displayed at all, and whether or not it should have a port. Widgets @@ -249,7 +249,7 @@ pub struct Configuration { } impl Configuration { - /// Derive widget configuration from the expression, node data in span tree and inferred value + /// Derive widget configuration from Enso expression, node data in span tree and inferred value /// type. When no configuration is provided with an override, this function will be used to /// create a default configuration. fn from_node(span_node: &SpanRef, usage_type: Option, expression: &str) -> Self { @@ -380,9 +380,9 @@ impl DropdownValue for Entry { -/// ================== -/// === WidgetsFrp === -/// ================== +// ================== +// === WidgetsFrp === +// ================== /// Widget FRP endpoints that can be used by widget views, and go straight to the root. #[derive(Debug, Clone, CloneRef)] @@ -469,10 +469,10 @@ impl Tree { /// Override widget configuration. The configuration is used to determine the widget appearance /// and behavior. By default, the widget configuration will be inferred from its span tree kind /// and type. However, in some cases, we want to change the selected widget for a given span - /// tree node, and it can be done by calling this method. The set metadata is persistent, and - /// will be applied to any future widget of this node that matches given pointer. - pub fn set_config_override(&self, pointer: OverrideKey, meta: Option) { - self.notify_dirty(self.model.set_config_override(pointer, meta)); + /// tree node, and it can be done by calling this method. The set configuration is persistent, + /// and will be applied to any future widget of this node that matches given pointer. + pub fn set_config_override(&self, pointer: OverrideKey, config: Option) { + self.notify_dirty(self.model.set_config_override(pointer, config)); } /// Set the inferred type of the expression for given ast ID. On rebuild, the type will be @@ -553,9 +553,9 @@ impl Tree { } -/// ================ -/// === TreeNode === -/// ================ +// ================ +// === TreeNode === +// ================ /// A single entry in the widget tree. If the widget has an attached port, it will be wrapped in /// `Port` struct and stored in `Port` variant. Otherwise, the widget will be stored directly using @@ -621,9 +621,9 @@ pub(super) struct EdgeData { -/// ================= -/// === TreeModel === -/// ================= +// ================= +// === TreeModel === +// ================= #[derive(Debug)] struct TreeModel { @@ -680,9 +680,9 @@ impl TreeModel { } /// Set the configuration under given key. It may cause the tree to be marked as dirty. - fn set_config_override(&self, pointer: OverrideKey, meta: Option) -> bool { + fn set_config_override(&self, pointer: OverrideKey, config: Option) -> bool { let mut map = self.override_map.borrow_mut(); - let dirty = map.synchronize_entry(pointer, meta); + let dirty = map.synchronize_entry(pointer, config); self.mark_dirty_flag(dirty) } @@ -751,7 +751,7 @@ impl TreeModel { ) { self.tree_dirty.set(false); let app = self.app.clone(); - let metadata_map = self.override_map.borrow(); + let override_map = self.override_map.borrow(); let connected_map = self.connected_map.borrow(); let usage_type_map = self.usage_type_map.borrow(); let old_nodes = self.nodes_map.take(); @@ -767,7 +767,7 @@ impl TreeModel { node_disabled, node_expression, styles, - metadata_map: &metadata_map, + override_map: &override_map, connected_map: &connected_map, usage_type_map: &usage_type_map, old_nodes, @@ -920,14 +920,25 @@ impl<'a, 'b> ConfigContext<'a, 'b> { /// Get an extension object of specified type at the current tree position. The extension object /// must have been created by any parent widget up in the hierarchy. If it does not exist, this /// method will return `None`. + /// + /// See also: [`ConfigContext::get_extension_or_default`], [`ConfigContext::modify_extension`]. pub fn get_extension(&self) -> Option<&T> { self.any_extension_index_by_type(std::any::TypeId::of::()) .map(|idx| self.builder.extensions[idx].downcast_ref().unwrap()) } + /// Get a clone of provided extension value, or a default value if it was not provided. + /// + /// See also: [`ConfigContext::get_extension`]. + pub fn get_extension_or_default(&self) -> T { + self.get_extension().map_or_default(Clone::clone) + } + /// Modify an extension object of specified type at the current tree position. The modification /// will only be visible to the descendants of this widget, even if the extension was added /// by one of its parents. + /// + /// See also: [`ConfigContext::get_extension`]. pub fn modify_extension(&mut self, f: impl FnOnce(&mut T)) where T: Any + Default + Clone { match self.any_extension_index_by_type(std::any::TypeId::of::()) { @@ -962,9 +973,9 @@ impl<'a, 'b> ConfigContext<'a, 'b> { -/// ==================== -/// === NestingLevel === -/// ==================== +// ==================== +// === NestingLevel === +// ==================== /// A logical nesting level associated with a widget which determines the mouse hover area size and /// widget indentation. It is specified by the parent widget when creating a child widget, as an @@ -996,9 +1007,9 @@ impl NestingLevel { } -/// =========================================== -/// === StableSpanIdentity / WidgetIdentity === -/// =========================================== +// =========================================== +// === StableSpanIdentity / WidgetIdentity === +// =========================================== /// A stable identifier to a span tree node. Uniquely determines a main widget of specific node in /// the span tree. It is a base of a widget stable identity, and allows widgets to be reused when @@ -1076,9 +1087,9 @@ impl PointerUsage { -/// =================== -/// === TreeBuilder === -/// =================== +// =================== +// === TreeBuilder === +// =================== /// A builder for the widget tree. Maintains transient state necessary during the tree construction, /// and provides methods for creating child nodes of the tree. Maintains a map of all widgets @@ -1091,7 +1102,7 @@ struct TreeBuilder<'a> { node_disabled: bool, node_expression: &'a str, styles: &'a StyleWatch, - metadata_map: &'a HashMap, + override_map: &'a HashMap, connected_map: &'a HashMap, usage_type_map: &'a HashMap, old_nodes: HashMap, @@ -1105,7 +1116,7 @@ struct TreeBuilder<'a> { impl<'a> TreeBuilder<'a> { /// Create a new child widget. The widget type will be automatically inferred, either based on - /// the node kind, or on the metadata provided from the language server. If possible, an + /// the node kind, or on the configuration provided from the language server. If possible, an /// existing widget will be reused under the same location in the tree, only updating its /// configuration as necessary. If no widget can be reused, a new one will be created. /// @@ -1126,8 +1137,9 @@ impl<'a> TreeBuilder<'a> { self.create_child_widget(span_node, nesting_level, None) } - /// Create a new child widget. Does not infer the widget type, but uses the provided metadata - /// instead. That way, the parent widget can explicitly control the type of the child widget. + /// Create a new child widget. Does not infer the widget type, but uses the provided + /// configuration instead. That way, the parent widget can explicitly control the type of the + /// child widget. /// /// See [`child_widget`] method for more details about widget creation. #[must_use] @@ -1136,9 +1148,9 @@ impl<'a> TreeBuilder<'a> { &mut self, span_node: span_tree::node::Ref<'_>, nesting_level: NestingLevel, - meta: Configuration, + config: Configuration, ) -> display::object::Instance { - self.create_child_widget(span_node, nesting_level, Some(meta)) + self.create_child_widget(span_node, nesting_level, Some(config)) } /// Create a new widget for given span tree node. This function recursively builds a subtree @@ -1184,29 +1196,26 @@ impl<'a> TreeBuilder<'a> { // override the configuration for its child. // 2. The override stored in the span tree node, located using `OverrideKey`. This can be // set by an external source, e.g. based on language server. - // 3. The default metadata for the node, which is determined based on the node kind, usage - // type and whether it has children. - - // Stack variable necessary to hold an owned configuration initialized in a closure below, - // so we can take a reference to it. - let mut default_config = None; + // 3. The default configuration for the widget, which is determined based on the node kind, + // usage type and whether it has children. let kind = &span_node.kind; - let configuration = config_override - .as_ref() - .or_else(|| { - let pointer_data = kind.call_id().zip(kind.argument_name()); - let meta_pointer = pointer_data.map(|(call_id, argument_name)| OverrideKey { - call_id, - argument_name: argument_name.into(), - }); - - meta_pointer.and_then(|ptr| self.metadata_map.get(&ptr)) + let config_override = config_override.as_ref(); + let stored_override = || { + self.override_map.get(&OverrideKey { + call_id: kind.call_id()?, + argument_name: kind.argument_name()?.into(), }) - .unwrap_or_else(|| { - default_config.get_or_insert_with(|| { - Configuration::from_node(&span_node, usage_type.clone(), self.node_expression) - }) - }); + }; + let provided_config = config_override.or_else(stored_override); + let inferred_config; + let configuration = match provided_config { + Some(config) => config, + None => { + let ty = usage_type.clone(); + inferred_config = Configuration::from_node(&span_node, ty, self.node_expression); + &inferred_config + } + }; let widget_has_port = ptr_usage.request_port(&widget_id, configuration.has_port); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs index 98c40d31e901..87413daf4126 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/hierarchy.rs @@ -7,6 +7,18 @@ use ensogl::display::object; +// =============== +// === Aliases === +// =============== + +/// A collection type used to collect a temporary list of node child widget roots, so that they can +/// be passed to `replace_children` method in one go. Avoids allocation for small number of +/// children, but also doesn't consume too much stack memory to avoid stack overflow in deep widget +/// hierarchies. +pub type CollectedChildren = SmallVec<[object::Instance; 4]>; + + + // ================= // === Hierarchy === // ================= @@ -37,12 +49,8 @@ impl super::SpanWidget for Widget { fn configure(&mut self, _: &Config, ctx: super::ConfigContext) { let child_level = ctx.info.nesting_level.next_if(ctx.span_node.is_argument()); - - let children = ctx - .span_node - .children_iter() - .map(|node| ctx.builder.child_widget(node, child_level)) - .collect_vec(); - self.display_object.replace_children(&children); + let children_iter = ctx.span_node.children_iter(); + let children = children_iter.map(|node| ctx.builder.child_widget(node, child_level)); + self.display_object.replace_children(&children.collect::()); } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs index 70a81fa6d3f6..b8b3caba7d59 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/label.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/label.rs @@ -96,20 +96,21 @@ impl super::SpanWidget for Widget { ctx.expression_at(ctx.span_node.span()) }; - let color_state: ColorState = if ctx.info.subtree_connection.is_some() { - ColorState::Connected - } else if ctx.info.disabled { - ColorState::Disabled - } else if is_placeholder { - ColorState::Placeholder - } else { - let ty = ctx.info.usage_type.clone(); - let ty = ty.or_else(|| ctx.span_node.kind.tp().map(|t| crate::Type(t.into()))); - let color = crate::type_coloring::compute_for_code(ty.as_ref(), ctx.styles()); - ColorState::FromType(color) + let is_connected = ctx.info.subtree_connection.is_some(); + let color_state = match () { + _ if is_connected => ColorState::Connected, + _ if ctx.info.disabled => ColorState::Disabled, + _ if is_placeholder => ColorState::Placeholder, + _ => { + let span_node_type = ctx.span_node.kind.tp(); + let usage_type = ctx.info.usage_type.clone(); + let ty = usage_type.or_else(|| span_node_type.map(|t| crate::Type(t.into()))); + let color = crate::type_coloring::compute_for_code(ty.as_ref(), ctx.styles()); + ColorState::FromType(color) + } }; - let ext = ctx.get_extension::().copied().unwrap_or_default(); + let ext = ctx.get_extension_or_default::(); let text_weight = if ext.bold { text::Weight::Bold } else { text::Weight::Normal }; let input = &self.frp.public.input; input.content.emit(content); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs b/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs index 044ebee7806d..50534fab3429 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs @@ -57,7 +57,6 @@ impl Widget { /// Create Model for Vector Editor widget. pub fn new(app: &Application, widgets_frp: &WidgetsFrp, styles: &StyleWatch) -> Self { - let network = frp::Network::new("vector_editor::Model"); let display_object = display::object::Instance::new(); let list_container = display::object::Instance::new(); let child_container = display::object::Instance::new(); @@ -87,7 +86,6 @@ impl Widget { fn init_toggle(self, widgets_frp: &WidgetsFrp) -> Self { let network = &self.config_frp.network; let display_object = &self.display_object; - let activation_shape = &self.activation_shape; let list_container = &self.list_container; let list = &self.list; let dot_clicked = self.activation_shape.on_event::(); diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index cb505152699d..7245959fdc96 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -6,7 +6,6 @@ use crate::component::node::input::widget::Entry; use enso_frp as frp; use ensogl::control::io::mouse; -use ensogl::data::color; use ensogl::display; use ensogl::display::object::event; use ensogl_component::drop_down::Dropdown; @@ -14,9 +13,9 @@ use ensogl_hardcoded_theme as theme; -/// ================= -/// === Constants === -/// ================= +// ================= +// === Constants === +// ================= /// Height of the activation triangle shape. pub const ACTIVATION_SHAPE_SIZE: Vector2 = Vector2(15.0, 11.0); @@ -146,7 +145,7 @@ impl super::SpanWidget for Widget { dropdown_wrapper.set_size((0.0, 0.0)).set_alignment_left_top(); let config_frp = Frp::new(); - let dropdown = LazyDropdown::new(&app, &config_frp.network); + let dropdown = LazyDropdown::new(app, &config_frp.network); let dropdown = Rc::new(RefCell::new(dropdown)); Self { @@ -177,20 +176,13 @@ impl super::SpanWidget for Widget { ctx.modify_extension::(|ext| ext.bold = true); } - if ctx.span_node.children.is_empty() { - let child_level = ctx.info.nesting_level; - let label_meta = super::Configuration::always(super::label::Config); - let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, label_meta); - self.label_wrapper.replace_children(&[child]); - } else { - let child_level = ctx.info.nesting_level.next(); - let children = ctx - .span_node - .children_iter() - .map(|child| ctx.builder.child_widget(child, child_level)) - .collect_vec(); - self.label_wrapper.replace_children(&children); - } + let label_config = match ctx.span_node.children.is_empty() { + true => super::Configuration::always(super::label::Config), + false => super::Configuration::always(super::hierarchy::Config), + }; + let child_level = ctx.info.nesting_level; + let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, label_config); + self.label_wrapper.replace_children(&[child]); } } @@ -292,8 +284,7 @@ impl Widget { eval activation_shape_theme([styles, activation_shape](path) { if let Some(path) = path { let color = styles.get_color(path); - let rgba = color::Rgba::from(color); - activation_shape.color.set(rgba.into()); + activation_shape.color.set(color.into()); } }); @@ -335,9 +326,9 @@ fn entry_for_current_value( -/// ==================== -/// === LazyDropdown === -/// ==================== +// ==================== +// === LazyDropdown === +// ==================== /// A wrapper for dropdown that can be initialized lazily, with all required FRP endpoints to drive /// it as if was just an ordinary view. Before calling `lazy_init` for the first time, the overhead diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 5a438da59c41..99df6407b7bc 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -1116,16 +1116,16 @@ impl Grid { -// ===================== -// === WidgetUpdates === -// ===================== +// ========================= +// === CallWidgetsConfig === +// ========================= /// Configuration for widgets of arguments at function call Enso expression. #[derive(Debug, Default, Clone)] pub struct CallWidgetsConfig { /// The function call expression ID. pub call_id: ast::Id, - /// Configuration of a widget for each function argument. + /// Definition of a widget for each function argument. pub definitions: Rc>, } @@ -1136,7 +1136,7 @@ pub struct ArgumentWidgetConfig { pub argument_name: String, /// Widget configuration queried from the language server. When this is `None`, the widget /// configuration should be inferred automatically. - pub meta: Option, + pub config: Option, } @@ -3432,9 +3432,10 @@ fn new_graph_editor(app: &Application) -> GraphEditor { model.set_node_expression_usage_type(*node_id,*ast_id,maybe_type.clone()); *node_id })); - edges_to_refresh <= node_to_refresh.map( - f!((node_id) nodes.get_cloned_ref(node_id).map(|node| node.all_edges())) + edges_to_refresh_batch <- node_to_refresh.map(f!((node_id) + nodes.get_cloned_ref(node_id).map(|node| node.all_edges())) ).unwrap(); + edges_to_refresh <= edges_to_refresh_batch; eval edges_to_refresh ([model, neutral_color] (edge) model.refresh_edge_color(*edge, neutral_color.value().into())); eval inputs.update_node_widgets(((node, updates)) model.update_node_widgets(*node, updates)); @@ -3803,10 +3804,12 @@ fn new_graph_editor(app: &Application) -> GraphEditor { frp::extend! { network - // Let the edge update complete before refreshing the cursor. Otherwise, the cursor style can - // be updated too early, before the edge had a chance to be updated with new endpoints. - refresh_detached_edge_cursor <- all(&out.some_edge_endpoints_unset,&out.view_mode); - refresh_detached_edge_cursor <- refresh_detached_edge_cursor._0().debounce(); + edges_refresh_when_detached <- edges_to_refresh_batch.gate(&out.some_edge_endpoints_unset); + refresh_detached_edge_cursor <- all( + out.some_edge_endpoints_unset, + out.view_mode, + edges_refresh_when_detached + )._0(); cursor_style_edge_drag <- refresh_detached_edge_cursor.map( f!([model,neutral_color](some_edges_detached) { diff --git a/build-config.yaml b/build-config.yaml index d548959fdbec..4a184e493e0f 100644 --- a/build-config.yaml +++ b/build-config.yaml @@ -1,6 +1,6 @@ # Options intended to be common for all developers. -wasm-size-limit: 15.75 MiB +wasm-size-limit: 15.77 MiB required-versions: # NB. The Rust version is pinned in rust-toolchain.toml. diff --git a/lib/rust/ensogl/core/src/display/object/instance.rs b/lib/rust/ensogl/core/src/display/object/instance.rs index 11a7621b7a5f..7d9933b095be 100644 --- a/lib/rust/ensogl/core/src/display/object/instance.rs +++ b/lib/rust/ensogl/core/src/display/object/instance.rs @@ -2166,7 +2166,6 @@ impl InstanceDef { let has_elements_to_remove = retained_children < num_children_before; let need_cleanup = has_elements_to_remove || has_stale_indices; - dbg!(retained_children, num_children_before, has_elements_to_remove, has_stale_indices); if need_cleanup { let mut binds_to_drop = SmallVec::<[(ParentBind, WeakInstance); 8]>::new(); From 00a455b349455a153f045bb4b8d6f328dfd10207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 25 Apr 2023 17:29:47 +0200 Subject: [PATCH 42/45] fix sibling_offset --- app/gui/language/span-tree/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/gui/language/span-tree/src/builder.rs b/app/gui/language/span-tree/src/builder.rs index a76b92f9524a..0fdfcaab610f 100644 --- a/app/gui/language/span-tree/src/builder.rs +++ b/app/gui/language/span-tree/src/builder.rs @@ -32,7 +32,7 @@ pub trait Builder: Sized { let node = Node::::new().with_kind(kind).with_size(len.into()); let prev_child = self.node_being_built().children.last(); let prev_child_end = prev_child.map_or(0, |c| (c.parent_offset + c.node.size).as_usize()); - let sibling_offset = prev_child_end.saturating_sub(parent_offset); + let sibling_offset = parent_offset.saturating_sub(prev_child_end); let child = node::Child { node, parent_offset: parent_offset.into(), From c19da318012ed28074f87226c508ccd14e666f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Tue, 25 Apr 2023 19:30:08 +0200 Subject: [PATCH 43/45] provide widget child id to the parent --- .../src/component/node/input/widget.rs | 104 +++++++++++------- .../node/input/widget/list_editor.rs | 2 +- .../node/input/widget/single_choice.rs | 4 +- 3 files changed, 66 insertions(+), 44 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/widget.rs b/app/gui/view/graph-editor/src/component/node/input/widget.rs index 1e9910d8be62..d5f1e99aba20 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget.rs @@ -1018,20 +1018,24 @@ impl NestingLevel { pub struct StableSpanIdentity { /// AST ID of either the node itself, or the closest ancestor node which has one. Is [`None`] /// when there is no such parent with assigned AST id. - id: Option, - /// A hash of remaining crumbs to the widget, starting from the closest node with assigned AST - /// id. We store a hash instead of the crumbs directly, so the type can be trivially copied. - /// The collision is extremely unlikely due to commonly having very short lists of crumbs - /// to store here and u64 being comparatively extremely large hash space. - crumbs_hash: u64, + ast_id: Option, + /// A hash of remaining data used to distinguish between tree nodes. We store a hash instead of + /// the data directly, so the type can be trivially copied. The collision is extremely unlikely + /// due to u64 being extremely large hash space, compared to the size of the used data. Many + /// nodes are also already fully distinguished by the AST ID alone. + /// + /// Currently we are hashing a portion of span-tree crumbs, starting from the closest node with + /// assigned AST id up to this node. The widgets should not rely on the exact kind of data + /// used, as it may be extended to include more information in the future. + identity_hash: u64, } impl StableSpanIdentity { - fn new(id: Option, crumbs: &[span_tree::Crumb]) -> Self { + fn new(ast_id: Option, crumbs_since_ast: &[span_tree::Crumb]) -> Self { let mut hasher = DefaultHasher::new(); - crumbs.hash(&mut hasher); - let crumbs_hash = hasher.finish(); - Self { id, crumbs_hash } + crumbs_since_ast.hash(&mut hasher); + let identity_hash = hasher.finish(); + Self { ast_id, identity_hash } } /// Convert this pointer to a stable identity of a widget, making it unique among all widgets. @@ -1045,9 +1049,10 @@ impl StableSpanIdentity { /// create a child widget on the same span tree node, so we need to be able to distinguish between /// them. Note that only one widget created for a given span tree node will be able to receive a /// port. The port is assigned to the first widget at given span that wants to receive it. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deref)] pub struct WidgetIdentity { /// The pointer to the main widget of this widget's node. + #[deref] main: StableSpanIdentity, /// The sequential index of a widget assigned to the same span tree node. index: usize, @@ -1115,10 +1120,11 @@ struct TreeBuilder<'a> { } impl<'a> TreeBuilder<'a> { - /// Create a new child widget. The widget type will be automatically inferred, either based on - /// the node kind, or on the configuration provided from the language server. If possible, an - /// existing widget will be reused under the same location in the tree, only updating its - /// configuration as necessary. If no widget can be reused, a new one will be created. + /// Create a new child widget, along with its whole subtree. The widget type will be + /// automatically inferred, either based on the node kind, or on the configuration provided + /// from the language server. If possible, an existing widget will be reused under the same + /// location in the tree, only updating its configuration as necessary. If no widget can be + /// reused, a new one will be created. /// /// The root display object of the created widget will be returned, and it must be inserted into /// the display object hierarchy by the caller. It will be common that the returned display @@ -1134,33 +1140,25 @@ impl<'a> TreeBuilder<'a> { span_node: span_tree::node::Ref<'_>, nesting_level: NestingLevel, ) -> display::object::Instance { - self.create_child_widget(span_node, nesting_level, None) + self.child_widget_of_type(span_node, nesting_level, None) } - /// Create a new child widget. Does not infer the widget type, but uses the provided - /// configuration instead. That way, the parent widget can explicitly control the type of the - /// child widget. + /// Create a new widget for given span tree node, recursively building a subtree of its + /// children. When a widget configuration is not provided, it is inferred automatically from the + /// span tree and expression value type. /// - /// See [`child_widget`] method for more details about widget creation. - #[must_use] - #[allow(dead_code)] // Currently unused, but will be used in the future in VectorEditor. + /// The returned value contains a root display object of created widget child, and it must be + /// inserted into the display hierarchy by the caller. The returned display object will + /// frequently not change between subsequent widget `configure` calls, as long as it can be + /// reused by the tree. The caller must not rely on it not changing. In order to handle that + /// efficiently, the caller can use the `replace_children` method of + /// [`display::object::InstanceDef`], which will only perform hierarchy updates if the children + /// list has been actually modified. pub fn child_widget_of_type( &mut self, span_node: span_tree::node::Ref<'_>, nesting_level: NestingLevel, - config: Configuration, - ) -> display::object::Instance { - self.create_child_widget(span_node, nesting_level, Some(config)) - } - - /// Create a new widget for given span tree node. This function recursively builds a subtree - /// of widgets, starting from the given node. The built subtree's root display object is - /// returned, so that it can be added to the parent's display hierarchy. - fn create_child_widget( - &mut self, - span_node: span_tree::node::Ref<'_>, - nesting_level: NestingLevel, - config_override: Option, + configuration: Option<&Configuration>, ) -> display::object::Instance { // This call can recurse into itself within the widget configuration logic. We need to save // the current layer's state, so it can be restored later after visiting the child node. @@ -1176,7 +1174,7 @@ impl<'a> TreeBuilder<'a> { StableSpanIdentity::new(Some(ast_id), &[]) } None => { - let ast_id = self.parent_info.as_ref().and_then(|st| st.identity.main.id); + let ast_id = self.parent_info.as_ref().and_then(|st| st.identity.main.ast_id); let this_crumbs = &span_node.crumbs; let crumbs_since_id = &this_crumbs[parent_last_ast_depth..]; StableSpanIdentity::new(ast_id, crumbs_since_id) @@ -1188,7 +1186,7 @@ impl<'a> TreeBuilder<'a> { let is_placeholder = span_node.is_expected_argument(); let sibling_offset = span_node.sibling_offset.as_usize(); - let usage_type = main_ptr.id.and_then(|id| self.usage_type_map.get(&id)).cloned(); + let usage_type = main_ptr.ast_id.and_then(|id| self.usage_type_map.get(&id)).cloned(); // Get widget configuration. There are three potential sources for configuration, that are // used in order, whichever is available first: @@ -1199,16 +1197,14 @@ impl<'a> TreeBuilder<'a> { // 3. The default configuration for the widget, which is determined based on the node kind, // usage type and whether it has children. let kind = &span_node.kind; - let config_override = config_override.as_ref(); - let stored_override = || { + let config_override = || { self.override_map.get(&OverrideKey { call_id: kind.call_id()?, argument_name: kind.argument_name()?.into(), }) }; - let provided_config = config_override.or_else(stored_override); let inferred_config; - let configuration = match provided_config { + let configuration = match configuration.or_else(config_override) { Some(config) => config, None => { let ty = usage_type.clone(); @@ -1305,3 +1301,29 @@ impl<'a> TreeBuilder<'a> { child_root } } + + + +// ============= +// === Child === +// ============= + +/// A child structure returned from the tree builder. Contains information about just built widget, +/// which might be useful for the parent widget in order to correctly place it in its view +/// hierarchy. +#[derive(Debug, Clone, Deref)] +struct Child { + /// The widget identity that is stable across rebuilds. The parent might use it to associate + /// internal state with any particular child. When a new child is inserted between two existing + /// children, their identities will be maintained. + #[allow(dead_code)] + pub id: WidgetIdentity, + /// The root object of the widget. In order to make the widget visible, it must be added to the + /// parent's view hierarchy. Every time a widget is [`configure`d], its root object may change. + /// The parent must not assume ownership over a root object of a removed child. The widget + /// [`Tree`] is allowed to reuse any widgets and insert them into different branches. + /// + /// [`configure`d]: SpanWidget::configure + #[deref] + pub root_object: display::object::Instance, +} diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs b/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs index 50534fab3429..bc96b33d0683 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/list_editor.rs @@ -223,7 +223,7 @@ impl super::SpanWidget for Widget { let child_level = ctx.info.nesting_level.next_if(ctx.span_node.is_argument()); let label_meta = super::Configuration::always(super::label::Config); - let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, label_meta); + let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, Some(&label_meta)); self.child_container.replace_children(&[child]); } } diff --git a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs index 7245959fdc96..bccc54aa7125 100644 --- a/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs +++ b/app/gui/view/graph-editor/src/component/node/input/widget/single_choice.rs @@ -176,12 +176,12 @@ impl super::SpanWidget for Widget { ctx.modify_extension::(|ext| ext.bold = true); } - let label_config = match ctx.span_node.children.is_empty() { + let config = match ctx.span_node.children.is_empty() { true => super::Configuration::always(super::label::Config), false => super::Configuration::always(super::hierarchy::Config), }; let child_level = ctx.info.nesting_level; - let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, label_config); + let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, Some(&config)); self.label_wrapper.replace_children(&[child]); } } From ac80f6571c4322b096410d7de442f48994095765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Grabarz?= Date: Wed, 26 Apr 2023 02:50:48 +0200 Subject: [PATCH 44/45] fix list editor cursor and port visibility --- .../src/component/node/input/port.rs | 63 ++++++++++++------- .../ensogl/component/list-editor/src/lib.rs | 10 +-- lib/rust/ensogl/core/src/display/scene.rs | 3 + lib/rust/ensogl/core/src/gui/cursor.rs | 4 +- lib/rust/frp/src/nodes.rs | 17 +++++ 5 files changed, 68 insertions(+), 29 deletions(-) diff --git a/app/gui/view/graph-editor/src/component/node/input/port.rs b/app/gui/view/graph-editor/src/component/node/input/port.rs index fba1b98d9dbf..dea2595511ea 100644 --- a/app/gui/view/graph-editor/src/component/node/input/port.rs +++ b/app/gui/view/graph-editor/src/component/node/input/port.rs @@ -36,44 +36,59 @@ pub const PRIMARY_PORT_HOVER_PADDING_Y: f32 = (crate::node::HEIGHT - BASE_PORT_H -// ============================ -// === Shapes / HoverLayers === -// ============================ +// =========================== +// === Shapes / PortLayers === +// =========================== type PortShape = shape::compound::rectangle::Rectangle; +type PortShapeView = shape::compound::rectangle::shape::Shape; /// Shape used for handling mouse events in the port, such as hovering or dropping an edge. pub type HoverShape = shape::compound::rectangle::Rectangle; type HoverShapeView = shape::compound::rectangle::shape::Shape; -/// An scene extension that maintains layer partitions for port hover shapes. It is shared by all -/// ports in the scene. The hover shapes are partitioned by span tree depth, so that the hover area -/// of ports deeper in the tree will always be displayed on top, giving them priority to receive -/// mouse events. +/// An scene extension that maintains layer partitions for port shapes. It is shared by all ports in +/// the scene. The port selection and hover shapes are partitioned by span tree depth, so that the +/// ports deeper in the tree will always be displayed on top. For hover layers, that gives them +/// priority to receive mouse events. #[derive(Clone, CloneRef)] -struct HoverLayers { - hover_layer: display::scene::Layer, - hover_partitions: Rc>>>, +struct PortLayers { + port_layer: display::scene::Layer, + hover_layer: display::scene::Layer, + partitions: Rc< + RefCell, LayerSymbolPartition)>>, + >, } -impl display::scene::Extension for HoverLayers { +impl display::scene::Extension for PortLayers { fn init(scene: &display::Scene) -> Self { + let port_layer = scene.layers.port.clone_ref(); let hover_layer = scene.layers.port_hover.clone_ref(); - Self { hover_layer, hover_partitions: default() } + Self { port_layer, hover_layer, partitions: default() } } } -impl HoverLayers { +impl PortLayers { /// Add a display object to the partition at given depth, effectively setting its display order. /// If the partition does not exist yet, it will be created. - fn add_to_partition(&self, object: &display::object::Instance, depth: usize) { - let mut hover_partitions = self.hover_partitions.borrow_mut(); - if hover_partitions.len() <= depth { - hover_partitions.resize_with(depth + 1, || { - self.hover_layer.create_symbol_partition("input port hover") + fn add_to_partition( + &self, + port: &display::object::Instance, + hover: &display::object::Instance, + depth: usize, + ) { + let mut partitions = self.partitions.borrow_mut(); + if partitions.len() <= depth { + partitions.resize_with(depth + 1, || { + ( + self.port_layer.create_symbol_partition("input port"), + self.hover_layer.create_symbol_partition("input port hover"), + ) }) } - hover_partitions[depth].add(object); + let (port_partition, hover_partition) = &partitions[depth]; + port_partition.add(port); + hover_partition.add(hover); } } @@ -129,8 +144,8 @@ impl Port { .set_margin_right(-PORT_PADDING_X) .set_alignment_left_center(); - let layers = app.display.default_scene.extension::(); - layers.add_to_partition(hover_shape.display_object(), 0); + let layers = app.display.default_scene.extension::(); + layers.add_to_partition(port_shape.display_object(), hover_shape.display_object(), 0); let mouse_enter = hover_shape.on_event::(); let mouse_leave = hover_shape.on_event::(); @@ -227,8 +242,10 @@ impl Port { let node_depth = ctx.span_node.crumbs.len(); if self.current_depth != node_depth { self.current_depth = node_depth; - let layers = ctx.app().display.default_scene.extension::(); - layers.add_to_partition(self.hover_shape.display_object(), node_depth); + let layers = ctx.app().display.default_scene.extension::(); + let port_shape = self.port_shape.display_object(); + let hover_shape = self.hover_shape.display_object(); + layers.add_to_partition(port_shape, hover_shape, node_depth); } let is_primary = ctx.info.nesting_level.is_primary(); diff --git a/lib/rust/ensogl/component/list-editor/src/lib.rs b/lib/rust/ensogl/component/list-editor/src/lib.rs index f3cd7e4bf8b3..a8ed5176e557 100644 --- a/lib/rust/ensogl/component/list-editor/src/lib.rs +++ b/lib/rust/ensogl/component/list-editor/src/lib.rs @@ -391,7 +391,7 @@ impl ListEditor { let insert_pointer_style = self.init_insertion_points(&on_up, &pos_on_move, &is_dragging); frp::extend! { network - cursor.frp.set_style <+ all [insert_pointer_style, trash_pointer_style].fold(); + cursor.frp.set_style_override <+ all [insert_pointer_style, trash_pointer_style].fold(); } self } @@ -401,7 +401,7 @@ impl ListEditor { on_up: &frp::Stream>, pos_on_move: &frp::Stream, is_dragging: &frp::Stream, - ) -> frp::Stream { + ) -> frp::Stream> { let on_up = on_up.clone_ref(); let pos_on_move = pos_on_move.clone_ref(); let is_dragging = is_dragging.clone_ref(); @@ -436,7 +436,7 @@ impl ListEditor { ); index <= opt_index; enabled <- opt_index.is_some(); - pointer_style <- opt_index.map(|t| t.if_some_or_default(cursor::Style::plus)); + pointer_style <- enabled.then_constant(cursor::Style::plus()); on_up_in_gap <- on_up.gate(&enabled); insert_in_gap <- index.sample(&on_up_in_gap); frp.private.output.request_new_item <+ insert_in_gap.map(|t| Response::gui(*t)); @@ -518,7 +518,7 @@ impl ListEditor { &self, on_up: &frp::Stream>, drag_diff: &frp::Stream, - ) -> (frp::Stream, frp::Stream) { + ) -> (frp::Stream, frp::Stream>) { let on_up = on_up.clone_ref(); let drag_diff = drag_diff.clone_ref(); let model = &self.model; @@ -532,7 +532,7 @@ impl ListEditor { status <- drag_diff.map2(&required_offset, |t, m| t.y.abs() >= *m).on_change(); status_on_up <- on_up.constant(false); status_cleaning_phase <- any(&status, &status_on_up).on_change(); - cursor_style <- status_cleaning_phase.default_or(cursor::Style::trash()); + cursor_style <- status_cleaning_phase.then_constant(cursor::Style::trash()); on <- status.on_true(); perform <- on_up.gate(&status); eval_ on (model.collapse_all_placeholders()); diff --git a/lib/rust/ensogl/core/src/display/scene.rs b/lib/rust/ensogl/core/src/display/scene.rs index 8510c3ff4a53..9f0888fabe07 100644 --- a/lib/rust/ensogl/core/src/display/scene.rs +++ b/lib/rust/ensogl/core/src/display/scene.rs @@ -602,6 +602,7 @@ pub struct HardcodedLayers { pub viz: Layer, pub below_main: Layer, pub main: Layer, + pub port: Layer, pub port_selection: Layer, pub label: Layer, pub port_hover: Layer, @@ -644,6 +645,7 @@ impl HardcodedLayers { let viz = root.create_sublayer("viz"); let below_main = root.create_sublayer("below_main"); let main = root.create_sublayer("main"); + let port = root.create_sublayer("port"); let port_selection = root.create_sublayer_with_camera("port_selection", &port_selection_cam); let label = root.create_sublayer("label"); @@ -669,6 +671,7 @@ impl HardcodedLayers { viz, below_main, main, + port, port_selection, label, port_hover, diff --git a/lib/rust/ensogl/core/src/gui/cursor.rs b/lib/rust/ensogl/core/src/gui/cursor.rs index 9acb86f1feaa..094c27485dae 100644 --- a/lib/rust/ensogl/core/src/gui/cursor.rs +++ b/lib/rust/ensogl/core/src/gui/cursor.rs @@ -198,6 +198,7 @@ pub mod shape { crate::define_endpoints_2! { Input { + set_style_override (Option