Skip to content

Commit

Permalink
A Stub for Vector Editor Widget. (#6142)
Browse files Browse the repository at this point in the history
Fixes #5946

Adds a vector editor widget under the node. It reacts to code changes, but does not allow any editing: this will be continuously added in next tasks.

The position is often wrong due to limitations of the display object hierarchy. It should be changed anyway when [embedding into the node](#5923). But because it looks bad, it's shown only with `--featurePreview.vectorEditor` flag.

https://user-images.githubusercontent.com/3919101/227955735-f96fc23d-7e87-4042-8586-c1154523e871.mp4
  • Loading branch information
farmaazon authored Apr 3, 2023
1 parent 0d7682b commit 75df048
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 10 deletions.
12 changes: 6 additions & 6 deletions app/gui/view/graph-editor/src/component/node/input/area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,17 +574,16 @@ impl Model {
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() {
let range = port.span();
let code = &expression.code[range];
debug!("Setting current value while range is {range:?}, code is \"{code}\" \
debug!("Setting current value while range is {port_range:?}, code is \"{port_code}\" \
and full expression is \"{}\".", expression.code);
widget.set_current_value(Some(code.into()));
widget.set_current_value(Some(port_code.into()));
} else {
widget.set_current_value(None);
}
Expand Down Expand Up @@ -659,7 +658,8 @@ impl Model {
};

let tag_values = port.kind.tag_values().unwrap_or_default().to_vec();
widget.set_node_data(widget::NodeData { tag_values, port_size });
let tp = port.kind.tp().cloned();
widget.set_node_data(widget::NodeData { tag_values, port_size, tp });

Some((widget_bind, widget))
}
Expand Down
32 changes: 28 additions & 4 deletions app/gui/view/graph-editor/src/component/node/input/widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use crate::prelude::*;

use enso_config::ARGS;
use enso_frp as frp;
use ensogl::application::Application;
use ensogl::data::color;
Expand All @@ -11,6 +12,13 @@ use ensogl_component::drop_down::Dropdown;
use ensogl_component::drop_down::DropdownValue;


// ==============
// === Export ===
// ==============

pub mod vector_editor;



/// =================
/// === Constants ===
Expand Down Expand Up @@ -121,6 +129,7 @@ impl DropdownValue for Entry {
pub struct NodeData {
pub tag_values: Vec<span_tree::TagValue>,
pub port_size: Vector2,
pub tp: Option<String>,
}


Expand All @@ -133,7 +142,7 @@ pub struct NodeData {
/// 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)]
struct SampledFrp {
pub struct SampledFrp {
set_current_value: frp::Sampler<Option<ImString>>,
set_visible: frp::Sampler<bool>,
set_focused: frp::Sampler<bool>,
Expand Down Expand Up @@ -221,10 +230,13 @@ impl Model {

#[profile(Task)]
fn set_widget_data(&self, frp: &SampledFrp, meta: &Option<Metadata>, node_data: &NodeData) {
trace!("Setting widget data: {:?} {:?}", meta, node_data);

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 = has_tag_values.then_some(Kind::SingleChoice);
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());
Expand Down Expand Up @@ -267,13 +279,18 @@ 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.
#[derive(Debug)]
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 {
Expand All @@ -288,6 +305,8 @@ 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);
Expand All @@ -304,12 +323,17 @@ 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,
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
//! Module dedicated to the Vector 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
//! view in [future tasks](https://github.com/enso-org/enso/issues/5631).
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 ensogl::application::Application;
use ensogl::control::io::mouse;
use ensogl::data::color;
use ensogl::display;



// ================
// === Elements ===
// ================

/// A simple component displaying codes of the Vector elements. Will be replaced with
/// `Vector Editor` view when it will be implemented (
#[derive(Clone, CloneRef, Debug)]
pub struct Elements {
app: Application,
display_object: display::object::Instance,
items: Rc<RefCell<Vec<ensogl_component::text::Text>>>,
}

impl Elements {
const HEIGHT: f32 = 16.0;
const GAP: f32 = 10.0;

fn new(app: &Application) -> Self {
let app = app.clone_ref();
let display_object = display::object::Instance::new();
let items = default();
display_object.use_auto_layout().set_gap_x(Self::GAP);
display_object.set_size_y(Self::HEIGHT);
Self { app, display_object, items }
}

fn clear_elements(&self) {
self.items.borrow_mut().clear();
}

fn set_elements<'a>(&self, codes: impl IntoIterator<Item = &'a str>) {
let mut codes = codes.into_iter();
let mut items = self.items.borrow_mut();
let mut remaining_count = 0;
for (element, code) in items.iter_mut().zip(&mut codes) {
remaining_count += 1;
if element.content.value().to_string() != code {
element.set_content(ImString::new(code));
}
}
items.truncate(remaining_count);
for code in codes {
let new_element = ensogl_component::text::Text::new(&self.app);
self.display_object.add_child(&new_element);
new_element.set_content(ImString::new(code));
self.app.display.default_scene.layers.label.add(&new_element);
items.push(new_element);
}
}
}

impl display::Object for Elements {
fn display_object(&self) -> &display::object::Instance {
&self.display_object
}
}



// =============
// === Model ===
// =============

/// A model for the vector editor widget.
///
/// Currently it displays an activation shape (a triangle) which, on click, displays the widget
/// view. Currently the widget view is simple [`Elements`] component, which just displays element's
/// code.
///
/// 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,
elements_container: display::object::Instance,
activation_shape: triangle::View,
elements: Elements,
/// FRP input informing about the port size.
pub set_port_size: frp::Source<Vector2>,
}

impl Model {
/// A gap between the `activation_shape` and `elements` view.
const GAP: f32 = 3.0;

/// Create Model for Vector Editor widget.
pub fn new(app: &Application, parent: &display::object::Instance, frp: &SampledFrp) -> Self {
let network = frp::Network::new("vector_editor::Model");
let display_object = display::object::Instance::new();
let elements_container = display::object::Instance::new();
let activation_shape = triangle::View::new();
let elements = Elements::new(app);

activation_shape.set_size(ACTIVATION_SHAPE_SIZE);
display_object.add_child(&elements_container);
display_object.add_child(&activation_shape);
display_object
.use_auto_layout()
.set_column_count(1)
.set_gap_y(Self::GAP)
.set_children_alignment_center();
display_object.set_size_hug();
elements_container.set_size_hug();
parent.add_child(&display_object);

frp::extend! { network
init <- source_();
set_port_size <- source::<Vector2>();
let dot_clicked = activation_shape.on_event::<mouse::Down>();
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, elements_container, elements](focus) match focus {
true => {
display_object.focus();
elements_container.add_child(&elements);
},
false => {
display_object.blur();
elements_container.remove_child(&elements);
},
});

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

value <- all(frp.set_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 ((val) elements.set_elements(Self::parse_array_code(val.as_str())));
eval empty_value ((()) elements.clear_elements());

widget_size <- display_object.on_updated.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 {
network,
display_object,
activation_shape,
elements,
elements_container,
set_port_size,
}
}

fn parse_array_code(code: &str) -> impl Iterator<Item = &str> {
let looks_like_array = code.starts_with('[') && code.ends_with(']');
let opt_iterator = looks_like_array.then(|| {
let without_braces = code.trim_start_matches([' ', '[']).trim_end_matches([' ', ']']);
let elements_with_trailing_spaces = without_braces.split(',');
elements_with_trailing_spaces.map(|s| s.trim())
});
opt_iterator.into_iter().flatten()
}
}
5 changes: 5 additions & 0 deletions app/ide-desktop/lib/content-config/src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@
"value": "light",
"description": "Color theme.",
"primary": false
},
"vectorEditor": {
"value": false,
"description": "Show Vector Editor widget on nodes.",
"primary": false
}
}
},
Expand Down

0 comments on commit 75df048

Please sign in to comment.