mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 11:52:59 +03:00
Finishing Vector Editor (#6470)
This commit is contained in:
parent
cd92d90f9f
commit
d6fa36d793
@ -135,7 +135,9 @@
|
||||
- [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.
|
||||
- [The IDE UI element for selecting the execution mode of the project is now
|
||||
sending messages to the backend.][6341].
|
||||
sending messages to the backend][6341].
|
||||
- [List Editor Widget][6470]. Now you can edit lists by clicking buttons on
|
||||
nodes or by dragging the elements.
|
||||
|
||||
#### EnsoGL (rendering engine)
|
||||
|
||||
@ -198,7 +200,8 @@
|
||||
[5895]: https://github.com/enso-org/enso/pull/6130
|
||||
[6035]: https://github.com/enso-org/enso/pull/6035
|
||||
[6097]: https://github.com/enso-org/enso/pull/6097
|
||||
[6097]: https://github.com/enso-org/enso/pull/6341
|
||||
[6341]: https://github.com/enso-org/enso/pull/6341
|
||||
[6470]: https://github.com/enso-org/enso/pull/6470
|
||||
|
||||
#### Enso Standard Library
|
||||
|
||||
|
@ -623,6 +623,14 @@ impl<T> SpanSeed<T> {
|
||||
pub fn child(node: T) -> Self {
|
||||
Self::Child(SpanSeedChild { node })
|
||||
}
|
||||
|
||||
pub fn token(token: String) -> Self {
|
||||
Self::Token(SpanSeedToken { token })
|
||||
}
|
||||
|
||||
pub fn is_child(&self) -> bool {
|
||||
matches!(self, SpanSeed::Child { .. })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -12,6 +12,7 @@ use crate::SpanTree;
|
||||
use ast::opr::match_named_argument;
|
||||
use ast::opr::ArgWithOffset;
|
||||
use ast::Ast;
|
||||
use ast::SpanSeed;
|
||||
|
||||
|
||||
|
||||
@ -142,6 +143,33 @@ impl<'a, T> Implementation for node::Ref<'a, T> {
|
||||
),
|
||||
};
|
||||
Ok(infix.into_ast())
|
||||
} else if let ast::Shape::Tree(tree) = ast.shape() {
|
||||
let mut tree = tree.clone();
|
||||
let span_info = &mut tree.span_info;
|
||||
let has_children =
|
||||
span_info.iter().any(|span| matches!(span, ast::SpanSeed::Child(_)));
|
||||
match *kind {
|
||||
BeforeArgument(index) => {
|
||||
span_info.insert(index, ast::SpanSeed::child(new));
|
||||
span_info.insert(index + 1, ast::SpanSeed::token(",".to_owned()));
|
||||
span_info.insert(index + 2, ast::SpanSeed::space(1).unwrap());
|
||||
}
|
||||
Append => {
|
||||
let last_token_index = span_info
|
||||
.iter()
|
||||
.rposition(|span| matches!(span, ast::SpanSeed::Token(_)));
|
||||
let index = last_token_index.unwrap_or(0);
|
||||
if has_children {
|
||||
span_info.insert(index, ast::SpanSeed::token(",".to_owned()));
|
||||
span_info.insert(index + 1, ast::SpanSeed::space(1).unwrap());
|
||||
span_info.insert(index + 2, ast::SpanSeed::child(new));
|
||||
} else {
|
||||
span_info.insert(index, ast::SpanSeed::child(new));
|
||||
}
|
||||
}
|
||||
_ => unreachable!("Wrong insertion point in tree."),
|
||||
}
|
||||
Ok(ast.with_shape(tree))
|
||||
} else {
|
||||
let mut prefix = ast::prefix::Chain::from_ast_non_strict(&ast);
|
||||
let item = ast::prefix::Argument::new(new, DEFAULT_OFFSET, None);
|
||||
@ -236,7 +264,6 @@ impl<'a, T> Implementation for node::Ref<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn erase_impl<C: Context>(&self) -> Option<EraseOperation<C>> {
|
||||
if self.node.kind.removable() {
|
||||
Some(Box::new(move |root, context| {
|
||||
@ -245,6 +272,11 @@ impl<'a, T> Implementation for node::Ref<'a, T> {
|
||||
let mut ast = root.get_traversing(parent_crumbs)?;
|
||||
let is_named_argument = match_named_argument(ast).is_some();
|
||||
|
||||
// When an element is removed, we have to find an adequate span tree node that
|
||||
// could become a new temporary target of dragged edge. It should be a node that
|
||||
// has an reverse set operation to the erase we are performing now.
|
||||
let mut reinsert_crumbs = None;
|
||||
|
||||
if is_named_argument {
|
||||
// When erasing named argument, we need to remove the whole argument, not only
|
||||
// the value part. The named argument AST is always a single infix expression,
|
||||
@ -266,6 +298,28 @@ impl<'a, T> Implementation for node::Ref<'a, T> {
|
||||
infix.args.pop();
|
||||
}
|
||||
Ok(infix.into_ast())
|
||||
} else if let (Crumb::Tree(crumb), ast::Shape::Tree(tree)) =
|
||||
(last_crumb, ast.shape())
|
||||
{
|
||||
let index = crumb.index;
|
||||
let mut tree = tree.clone();
|
||||
let span_info: &mut Vec<_> = &mut tree.span_info;
|
||||
let after = &span_info[index + 1..];
|
||||
let before = &span_info[..index];
|
||||
let is_child = |span: &SpanSeed<Ast>| span.is_child();
|
||||
let child_after_offset = after.iter().position(is_child);
|
||||
let child_before_offset = before.iter().rposition(is_child);
|
||||
let (insertion_point_offset, removed_range) =
|
||||
match (child_after_offset, child_before_offset) {
|
||||
(Some(after), _) => (-1, index..=index + after),
|
||||
(None, Some(before)) => (-2, before + 1..=index),
|
||||
(None, None) => (-1, index..=index),
|
||||
};
|
||||
|
||||
reinsert_crumbs =
|
||||
Some(self.crumbs.relative_sibling(insertion_point_offset));
|
||||
span_info.drain(removed_range);
|
||||
Ok(ast.with_shape(tree))
|
||||
} else {
|
||||
let mut prefix = ast::prefix::Chain::from_ast_non_strict(&ast);
|
||||
prefix.args.pop();
|
||||
@ -327,17 +381,19 @@ impl<'a, T> Implementation for node::Ref<'a, T> {
|
||||
// placeholder. The position of placeholder is not guaranteed to be in the same
|
||||
// place as the removed argument, as it might have been out of order. To find
|
||||
// the correct placeholder position, we need to search regenerated span-tree.
|
||||
let reinsert_crumbs = self.kind.definition_index().and_then(|_| {
|
||||
let call_id = self.kind.call_id();
|
||||
let name = self.kind.argument_name();
|
||||
let reinsert_crumbs = reinsert_crumbs.or_else(|| {
|
||||
self.kind.definition_index().and_then(|_| {
|
||||
let call_id = self.kind.call_id();
|
||||
let name = self.kind.argument_name();
|
||||
|
||||
let found = new_span_tree.root_ref().find_node(|node| {
|
||||
node.kind.is_insertion_point()
|
||||
&& node.kind.call_id() == call_id
|
||||
&& node.kind.argument_name() == name
|
||||
});
|
||||
let found = new_span_tree.root_ref().find_node(|node| {
|
||||
node.kind.is_insertion_point()
|
||||
&& node.kind.call_id() == call_id
|
||||
&& node.kind.argument_name() == name
|
||||
});
|
||||
|
||||
found.map(|found| found.crumbs)
|
||||
found.map(|found| found.crumbs)
|
||||
})
|
||||
});
|
||||
|
||||
// For non-resolved arguments, use the preceding insertion point. After the
|
||||
|
@ -819,6 +819,11 @@ fn tree_generate_node<T: Payload>(
|
||||
} else {
|
||||
let mut parent_offset = ByteDiff::from(0);
|
||||
let mut sibling_offset = ByteDiff::from(0);
|
||||
let first_token_or_child =
|
||||
tree.span_info.iter().find(|span| !matches!(span, SpanSeed::Space(_)));
|
||||
let is_array = matches!(first_token_or_child, Some(SpanSeed::Token(ast::SpanSeedToken { token })) if token == "[");
|
||||
let last_token_index =
|
||||
tree.span_info.iter().rposition(|span| matches!(span, SpanSeed::Token(_)));
|
||||
for (index, raw_span_info) in tree.span_info.iter().enumerate() {
|
||||
match raw_span_info {
|
||||
SpanSeed::Space(ast::SpanSeedSpace { space }) => {
|
||||
@ -826,6 +831,16 @@ fn tree_generate_node<T: Payload>(
|
||||
sibling_offset += ByteDiff::from(space);
|
||||
}
|
||||
SpanSeed::Token(ast::SpanSeedToken { token }) => {
|
||||
if is_array && Some(index) == last_token_index {
|
||||
let kind = InsertionPointType::Append;
|
||||
children.push(node::Child {
|
||||
node: Node::<T>::new().with_kind(kind),
|
||||
parent_offset,
|
||||
sibling_offset,
|
||||
ast_crumbs: vec![],
|
||||
});
|
||||
sibling_offset = 0.byte_diff();
|
||||
}
|
||||
let kind = node::Kind::Token;
|
||||
let size = ByteDiff::from(token.len());
|
||||
let ast_crumbs = vec![TreeCrumb { index }.into()];
|
||||
@ -835,7 +850,18 @@ fn tree_generate_node<T: Payload>(
|
||||
sibling_offset = 0.byte_diff();
|
||||
}
|
||||
SpanSeed::Child(ast::SpanSeedChild { node }) => {
|
||||
let kind = node::Kind::argument();
|
||||
if is_array {
|
||||
let kind = InsertionPointType::BeforeArgument(index);
|
||||
children.push(node::Child {
|
||||
node: Node::<T>::new().with_kind(kind),
|
||||
parent_offset,
|
||||
sibling_offset,
|
||||
ast_crumbs: vec![],
|
||||
});
|
||||
sibling_offset = 0.byte_diff();
|
||||
}
|
||||
|
||||
let kind = node::Kind::argument().with_removable(is_array);
|
||||
let node = node.generate_node(kind, context)?;
|
||||
let child_size = node.size;
|
||||
let ast_crumbs = vec![TreeCrumb { index }.into()];
|
||||
|
@ -255,6 +255,15 @@ impl Crumbs {
|
||||
vec.push(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Create crumbs to the sibling node, which is `offset` nodes away from the current node.
|
||||
pub fn relative_sibling(&self, offset: isize) -> Self {
|
||||
let mut vec = self.vec.deref().clone();
|
||||
if let Some(last) = vec.last_mut() {
|
||||
*last = last.saturating_add_signed(offset)
|
||||
}
|
||||
Self { vec: Rc::new(vec) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoIterator<Item = Crumb>> From<T> for Crumbs {
|
||||
@ -391,7 +400,7 @@ impl<'a, T> Ref<'a, T> {
|
||||
}
|
||||
|
||||
/// Iterator over all direct children producing `Ref`s.
|
||||
pub fn children_iter(self) -> impl DoubleEndedIterator<Item = Ref<'a, T>> {
|
||||
pub fn children_iter(self) -> impl DoubleEndedIterator<Item = Ref<'a, T>> + Clone {
|
||||
let children_count = self.node.children.len();
|
||||
(0..children_count).map(move |i| self.clone().child(i).unwrap())
|
||||
}
|
||||
|
@ -300,18 +300,15 @@ impl Model {
|
||||
let update = self.state.update_from_view();
|
||||
let ast_to_remove = update.remove_connection(id)?;
|
||||
Some(self.controller.disconnect(&ast_to_remove).map(|target_crumbs| {
|
||||
target_crumbs.and_then(|crumbs| {
|
||||
if let Some(crumbs) = target_crumbs {
|
||||
trace!(
|
||||
"Updating edge target after disconnecting it. New crumbs: {crumbs:?}"
|
||||
);
|
||||
// If we are still using this edge (e.g. when dragging it), we need to
|
||||
// update its target endpoint. Otherwise it will not reflect expression
|
||||
// update performed on the target node.
|
||||
let edge = self.view.model.edges.get_cloned_ref(&id)?;
|
||||
let outdated_target = edge.target()?;
|
||||
edge.set_target(EdgeEndpoint::new(outdated_target.node_id, crumbs));
|
||||
Some(())
|
||||
});
|
||||
self.view.replace_detached_edge_target((id, crumbs));
|
||||
};
|
||||
}))
|
||||
},
|
||||
"delete connection",
|
||||
|
@ -166,7 +166,7 @@ impl BreadcrumbsModel {
|
||||
pub fn new(app: Application, frp: &Frp) -> Self {
|
||||
let scene = &app.display.default_scene;
|
||||
let project_name = app.new_view();
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_named("Breadcrumbs");
|
||||
let root = display::object::Instance::new();
|
||||
let breadcrumbs_container = display::object::Instance::new();
|
||||
let scene = scene.clone_ref();
|
||||
|
@ -779,7 +779,7 @@ macro_rules! define_components {
|
||||
/// Constructor.
|
||||
#[allow(clippy::vec_init_then_push)]
|
||||
pub fn new() -> Self {
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_named(stringify!($name));
|
||||
$(let $field = <$field_type>::new();)*
|
||||
$(display_object.add_child(&$field);)*
|
||||
let mut shape_view_events:Vec<PointerTarget_DEPRECATED> = Vec::default();
|
||||
@ -1304,7 +1304,7 @@ impl EdgeModelData {
|
||||
/// Constructor.
|
||||
#[profile(Debug)]
|
||||
pub fn new(scene: &Scene, network: &frp::Network) -> Self {
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_named("Edge");
|
||||
let front = Front::new();
|
||||
let back = Back::new();
|
||||
let joint = joint::View::new();
|
||||
|
@ -513,7 +513,7 @@ impl NodeModel {
|
||||
let background = background::View::new();
|
||||
let drag_area = drag_area::View::new();
|
||||
let vcs_indicator = vcs::StatusIndicator::new(app);
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_named("Node");
|
||||
|
||||
display_object.add_child(&profiling_label);
|
||||
display_object.add_child(&drag_area);
|
||||
|
@ -19,6 +19,7 @@ use enso_frp;
|
||||
use ensogl::application::Application;
|
||||
use ensogl::data::color;
|
||||
use ensogl::display;
|
||||
use ensogl::display::world::with_context;
|
||||
use ensogl::gui::cursor;
|
||||
use ensogl::Animation;
|
||||
use ensogl_component::text;
|
||||
@ -152,13 +153,14 @@ impl Model {
|
||||
#[profile(Debug)]
|
||||
pub fn new(app: &Application) -> Self {
|
||||
let app = app.clone_ref();
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_named("input");
|
||||
|
||||
let edit_mode_label = app.new_view::<text::Text>();
|
||||
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);
|
||||
with_context(|ctx| ctx.layers.widget.add(&widget_tree));
|
||||
Self { app, display_object, edit_mode_label, expression, styles, styles_frp, widget_tree }
|
||||
.init()
|
||||
}
|
||||
|
@ -23,10 +23,14 @@ use ensogl::display::shape;
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/// The horizontal padding of ports. It affects how the port hover should extend the target text
|
||||
/// The horizontal padding of ports. It affects how the port shape should extend the target text
|
||||
/// boundary on both sides.
|
||||
pub const PORT_PADDING_X: f32 = 4.0;
|
||||
|
||||
/// The horizontal padding of port hover areas. It affects how the port hover should extend the
|
||||
/// target text boundary on both sides.
|
||||
pub const HOVER_PADDING_X: f32 = 2.0;
|
||||
|
||||
/// The minimum size of the port visual area.
|
||||
pub const BASE_PORT_HEIGHT: f32 = 18.0;
|
||||
|
||||
@ -122,7 +126,7 @@ impl Port {
|
||||
/// 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 port_root = display::object::Instance::new_named("Port");
|
||||
let widget_root = widget.root_object().clone_ref();
|
||||
let port_shape = PortShape::new();
|
||||
let hover_shape = HoverShape::new();
|
||||
@ -140,8 +144,8 @@ impl Port {
|
||||
hover_shape
|
||||
.set_size_y(BASE_PORT_HEIGHT)
|
||||
.allow_grow()
|
||||
.set_margin_left(-PORT_PADDING_X)
|
||||
.set_margin_right(-PORT_PADDING_X)
|
||||
.set_margin_left(-HOVER_PADDING_X)
|
||||
.set_margin_right(-HOVER_PADDING_X)
|
||||
.set_alignment_left_center();
|
||||
|
||||
let layers = app.display.default_scene.extension::<PortLayers>();
|
||||
@ -235,6 +239,7 @@ impl Port {
|
||||
self.port_root.remove_child(&self.widget_root);
|
||||
self.port_root.add_child(new_root);
|
||||
self.widget_root = new_root.clone_ref();
|
||||
self.widget_root.set_margin_left(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,6 +269,18 @@ impl Port {
|
||||
self.widget
|
||||
}
|
||||
|
||||
/// Get a reference to a widget currently wrapped by the port. The widget may change during
|
||||
/// the next tree rebuild.
|
||||
pub(super) fn widget(&self) -> &DynWidget {
|
||||
&self.widget
|
||||
}
|
||||
|
||||
/// Get a mutable reference to a widget currently wrapped by the port. The widget may change
|
||||
/// during the next tree rebuild.
|
||||
pub(super) fn widget_mut(&mut self) -> &mut DynWidget {
|
||||
&mut self.widget
|
||||
}
|
||||
|
||||
/// Get the port's hover shape. Used for testing to simulate mouse events.
|
||||
pub fn hover_shape(&self) -> &HoverShape {
|
||||
&self.hover_shape
|
||||
|
@ -134,6 +134,15 @@ pub trait SpanWidget {
|
||||
fn new(config: &Self::Config, ctx: &ConfigContext) -> Self;
|
||||
/// Update configuration for existing widget.
|
||||
fn configure(&mut self, config: &Self::Config, ctx: ConfigContext);
|
||||
/// Receive a reference of the tree items for which the ownership transfer was requested. Called
|
||||
/// by the tree when [`WidgetsFrp::transfer_ownership`] signal is used.
|
||||
fn receive_ownership(
|
||||
&mut self,
|
||||
original_request: TransferRequest,
|
||||
nodes: Vec<(WidgetIdentity, TreeNode)>,
|
||||
) {
|
||||
_ = (original_request, nodes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -209,6 +218,15 @@ macro_rules! define_widget_modules(
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_ownership(&mut self,
|
||||
req: TransferRequest,
|
||||
nodes: Vec<(WidgetIdentity, TreeNode)>,
|
||||
) {
|
||||
match (self) {
|
||||
$(DynWidget::$name(model) => model.receive_ownership(req, nodes),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
@ -252,7 +270,12 @@ impl Configuration {
|
||||
/// 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<crate::Type>, expression: &str) -> Self {
|
||||
fn from_node(
|
||||
span_node: &SpanRef,
|
||||
usage_type: Option<crate::Type>,
|
||||
expression: &str,
|
||||
is_directly_connected: bool,
|
||||
) -> Self {
|
||||
use span_tree::node::Kind;
|
||||
|
||||
let kind = &span_node.kind;
|
||||
@ -260,46 +283,51 @@ impl Configuration {
|
||||
|
||||
const VECTOR_TYPE: &str = "Standard.Base.Data.Vector.Vector";
|
||||
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
|
||||
let node_expr = &expression[span_node.span()];
|
||||
let looks_like_vector = node_expr.starts_with('[') && node_expr.ends_with(']');
|
||||
let type_is_vector = |tp: &Option<String>| {
|
||||
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[span_node.span()];
|
||||
node_expr.starts_with('[') && node_expr.ends_with(']')
|
||||
} else {
|
||||
false
|
||||
}
|
||||
.or(tp.as_deref())
|
||||
.map_or(false, |tp| tp.contains(VECTOR_TYPE))
|
||||
};
|
||||
|
||||
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_list_editor_enabled && is_vector(arg) => Self::list_editor(),
|
||||
Kind::Argument(_) if is_list_editor_enabled && looks_like_vector => Self::list_editor(),
|
||||
Kind::Root if is_list_editor_enabled && looks_like_vector => Self::list_editor(),
|
||||
Kind::InsertionPoint(arg) if arg.kind.is_expected_argument() =>
|
||||
if !arg.tag_values.is_empty() {
|
||||
if is_list_editor_enabled && (type_is_vector(&arg.tp) || looks_like_vector) {
|
||||
Self::list_editor()
|
||||
} else if !arg.tag_values.is_empty() {
|
||||
Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values)
|
||||
} else {
|
||||
Self::always(label::Config::default())
|
||||
},
|
||||
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),
|
||||
Kind::InsertionPoint(_) =>
|
||||
Self::maybe_with_port(insertion_point::Config, is_directly_connected),
|
||||
_ if has_children => Self::always(hierarchy::Config),
|
||||
_ => Self::always(label::Config::default()),
|
||||
}
|
||||
}
|
||||
|
||||
const fn maybe_with_port<C>(kind: C, has_port: bool) -> Self
|
||||
where C: ~const Into<DynConfig> {
|
||||
Self { display: Display::Always, kind: kind.into(), has_port }
|
||||
}
|
||||
|
||||
const fn always<C>(kind: C) -> Self
|
||||
where C: ~const Into<DynConfig> {
|
||||
Self { display: Display::Always, kind: kind.into(), has_port: true }
|
||||
Self::maybe_with_port(kind, true)
|
||||
}
|
||||
|
||||
const fn inert<C>(kind: C) -> Self
|
||||
where C: ~const Into<DynConfig> {
|
||||
Self { display: Display::Always, kind: kind.into(), has_port: false }
|
||||
Self::maybe_with_port(kind, false)
|
||||
}
|
||||
|
||||
/// Widget configuration for static dropdown, based on the tag values provided by suggestion
|
||||
@ -391,6 +419,13 @@ pub struct WidgetsFrp {
|
||||
pub(super) set_read_only: frp::Sampler<bool>,
|
||||
pub(super) set_view_mode: frp::Sampler<crate::view::Mode>,
|
||||
pub(super) set_profiling_status: frp::Sampler<crate::node::profiling::Status>,
|
||||
/// Remove given tree node's reference from the widget tree, and send its only remaining strong
|
||||
/// reference to a new widget owner using [`SpanWidget::receive_ownership`] method. This will
|
||||
/// effectively give up tree's ownership of that node, and will prevent its view from being
|
||||
/// reused.
|
||||
///
|
||||
/// NOTE: Calling this during rebuild will have no effect.
|
||||
pub(super) transfer_ownership: frp::Any<TransferRequest>,
|
||||
pub(super) value_changed: frp::Any<(span_tree::Crumbs, Option<ImString>)>,
|
||||
pub(super) request_import: frp::Any<ImString>,
|
||||
pub(super) on_port_hover: frp::Any<Switch<span_tree::Crumbs>>,
|
||||
@ -399,6 +434,20 @@ pub struct WidgetsFrp {
|
||||
pub(super) connected_port_updated: frp::Any<()>,
|
||||
}
|
||||
|
||||
/// A request for widget tree item ownership transfer. See [`WidgetsFrp::transfer_ownership`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
pub struct TransferRequest {
|
||||
/// The widget ID that will receive the ownership of the node. Usually this is the ID of the
|
||||
/// widget that sends the request, which can be obtained from [`NodeInfo::identity`], which is
|
||||
/// provided on [`ConfigContext`].
|
||||
pub new_owner: WidgetIdentity,
|
||||
/// The ID of the node that should be transferred. Usually one of the node's children, which
|
||||
/// can be obtained from [`Child`] instance returned from [`TreeBuilder::child_widget`].
|
||||
pub to_transfer: WidgetIdentity,
|
||||
/// Whether the whole subtree should be transferred, or just the node itself.
|
||||
pub whole_subtree: bool,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============
|
||||
@ -434,6 +483,8 @@ impl Tree {
|
||||
|
||||
frp::extend! { network
|
||||
frp.private.output.rebuild_required <+ frp.marked_dirty_sync.debounce();
|
||||
transfer_ownership <- any(...);
|
||||
eval transfer_ownership((request) model.transfer_ownership(*request));
|
||||
|
||||
set_ports_visible <- frp.set_ports_visible.sampler();
|
||||
set_read_only <- frp.set_read_only.sampler();
|
||||
@ -455,6 +506,7 @@ impl Tree {
|
||||
set_read_only,
|
||||
set_view_mode,
|
||||
set_profiling_status,
|
||||
transfer_ownership,
|
||||
value_changed,
|
||||
request_import,
|
||||
on_port_hover,
|
||||
@ -561,7 +613,7 @@ impl Tree {
|
||||
/// `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 {
|
||||
pub 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.
|
||||
@ -569,12 +621,29 @@ pub(super) enum TreeNode {
|
||||
}
|
||||
|
||||
impl TreeNode {
|
||||
fn port(&self) -> Option<&Port> {
|
||||
#[allow(missing_docs)]
|
||||
pub fn port(&self) -> Option<&Port> {
|
||||
match self {
|
||||
TreeNode::Port(port) => Some(port),
|
||||
TreeNode::Widget(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub fn widget(&self) -> &DynWidget {
|
||||
match self {
|
||||
TreeNode::Port(port) => port.widget(),
|
||||
TreeNode::Widget(widget) => widget,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub fn widget_mut(&mut self) -> &mut DynWidget {
|
||||
match self {
|
||||
TreeNode::Port(port) => port.widget_mut(),
|
||||
TreeNode::Widget(widget) => widget,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for TreeNode {
|
||||
@ -648,7 +717,7 @@ impl TreeModel {
|
||||
/// argument info.
|
||||
fn new(app: &Application) -> Self {
|
||||
let app = app.clone_ref();
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_named("Tree");
|
||||
display_object.use_auto_layout();
|
||||
display_object.set_children_alignment_left_center().justify_content_center_y();
|
||||
display_object.set_size_y(NODE_HEIGHT);
|
||||
@ -716,6 +785,22 @@ impl TreeModel {
|
||||
Some(hierarchy[parent_index].identity)
|
||||
}
|
||||
|
||||
/// Iterate over a node with given pointer and all its descendants, if it exists in the tree.
|
||||
#[allow(dead_code)]
|
||||
pub fn iter_subtree(
|
||||
&self,
|
||||
pointer: WidgetIdentity,
|
||||
) -> impl Iterator<Item = WidgetIdentity> + '_ {
|
||||
let hierarchy = self.hierarchy.borrow();
|
||||
let nodes = self.nodes_map.borrow();
|
||||
let total_range = nodes.get(&pointer).map_or(0..0, |entry| {
|
||||
let start = entry.index;
|
||||
let total_descendants = hierarchy[entry.index].total_descendants;
|
||||
start..start + 1 + total_descendants
|
||||
});
|
||||
total_range.map(move |index| hierarchy[index].identity)
|
||||
}
|
||||
|
||||
/// Iterate children of a node under given pointer, if any exist.
|
||||
#[allow(dead_code)]
|
||||
pub fn iter_children(
|
||||
@ -741,6 +826,27 @@ impl TreeModel {
|
||||
})
|
||||
}
|
||||
|
||||
/// Prevent a widget from being reused in future rebuild. Send its only remaining strong
|
||||
/// reference to the new owner.
|
||||
fn transfer_ownership(&self, request: TransferRequest) {
|
||||
let mut nodes = Vec::new();
|
||||
if request.whole_subtree {
|
||||
let iter = self.iter_subtree(request.to_transfer);
|
||||
let mut nodes_map = self.nodes_map.borrow_mut();
|
||||
nodes.extend(iter.filter_map(move |id| Some((id, nodes_map.remove(&id)?.node))));
|
||||
} else {
|
||||
let mut nodes_map = self.nodes_map.borrow_mut();
|
||||
if let Some(entry) = nodes_map.remove(&request.to_transfer) {
|
||||
nodes.push((request.to_transfer, entry.node));
|
||||
}
|
||||
}
|
||||
|
||||
let mut nodes_map = self.nodes_map.borrow_mut();
|
||||
if let Some(owner) = nodes_map.get_mut(&request.new_owner) {
|
||||
owner.node.widget_mut().receive_ownership(request, nodes);
|
||||
}
|
||||
}
|
||||
|
||||
#[profile(Task)]
|
||||
fn rebuild_tree(
|
||||
&self,
|
||||
@ -780,7 +886,7 @@ impl TreeModel {
|
||||
};
|
||||
|
||||
let child = builder.child_widget(tree.root_ref(), default());
|
||||
self.display_object.replace_children(&[child]);
|
||||
self.display_object.replace_children(&[child.root_object]);
|
||||
|
||||
self.nodes_map.replace(builder.new_nodes);
|
||||
self.hierarchy.replace(builder.hierarchy);
|
||||
@ -1014,7 +1120,7 @@ impl NestingLevel {
|
||||
/// 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)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
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.
|
||||
@ -1049,7 +1155,7 @@ 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, Deref)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deref, Default)]
|
||||
pub struct WidgetIdentity {
|
||||
/// The pointer to the main widget of this widget's node.
|
||||
#[deref]
|
||||
@ -1139,7 +1245,7 @@ impl<'a> TreeBuilder<'a> {
|
||||
&mut self,
|
||||
span_node: span_tree::node::Ref<'_>,
|
||||
nesting_level: NestingLevel,
|
||||
) -> display::object::Instance {
|
||||
) -> Child {
|
||||
self.child_widget_of_type(span_node, nesting_level, None)
|
||||
}
|
||||
|
||||
@ -1159,7 +1265,7 @@ impl<'a> TreeBuilder<'a> {
|
||||
span_node: span_tree::node::Ref<'_>,
|
||||
nesting_level: NestingLevel,
|
||||
configuration: Option<&Configuration>,
|
||||
) -> display::object::Instance {
|
||||
) -> Child {
|
||||
// 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;
|
||||
@ -1188,6 +1294,12 @@ impl<'a> TreeBuilder<'a> {
|
||||
let sibling_offset = span_node.sibling_offset.as_usize();
|
||||
let usage_type = main_ptr.ast_id.and_then(|id| self.usage_type_map.get(&id)).cloned();
|
||||
|
||||
// Prepare the widget node info and build context.
|
||||
let connection_color = self.connected_map.get(&span_node.crumbs);
|
||||
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);
|
||||
|
||||
// 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
|
||||
@ -1208,7 +1320,9 @@ impl<'a> TreeBuilder<'a> {
|
||||
Some(config) => config,
|
||||
None => {
|
||||
let ty = usage_type.clone();
|
||||
inferred_config = Configuration::from_node(&span_node, ty, self.node_expression);
|
||||
let expr = &self.node_expression;
|
||||
let connected = connection.is_some();
|
||||
inferred_config = Configuration::from_node(&span_node, ty, expr, connected);
|
||||
&inferred_config
|
||||
}
|
||||
};
|
||||
@ -1225,12 +1339,6 @@ impl<'a> TreeBuilder<'a> {
|
||||
|
||||
let old_node = self.old_nodes.remove(&widget_id).map(|e| e.node);
|
||||
|
||||
// Prepare the widget node info and build context.
|
||||
let connection_color = self.connected_map.get(&span_node.crumbs);
|
||||
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);
|
||||
|
||||
let disabled = self.node_disabled;
|
||||
let info = NodeInfo {
|
||||
identity: widget_id,
|
||||
@ -1298,7 +1406,7 @@ impl<'a> TreeBuilder<'a> {
|
||||
|
||||
let entry = TreeEntry { node: child_node, index: insertion_index };
|
||||
self.new_nodes.insert(widget_id, entry);
|
||||
child_root
|
||||
Child { id: widget_id, root_object: child_root }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ impl super::SpanWidget for Widget {
|
||||
}
|
||||
|
||||
fn new(_: &Config, _: &super::ConfigContext) -> Self {
|
||||
let display_object = object::Instance::new();
|
||||
let display_object = object::Instance::new_named("widget::Hierarchy");
|
||||
display_object.use_auto_layout();
|
||||
display_object.set_children_alignment_left_center().justify_content_center_y();
|
||||
Self { display_object }
|
||||
@ -50,7 +50,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_iter = ctx.span_node.children_iter();
|
||||
let children = children_iter.map(|node| ctx.builder.child_widget(node, child_level));
|
||||
let children =
|
||||
children_iter.map(|node| ctx.builder.child_widget(node, child_level).root_object);
|
||||
self.display_object.replace_children(&children.collect::<CollectedChildren>());
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ use ensogl::display::object;
|
||||
pub struct Config;
|
||||
|
||||
|
||||
/// Insertion point widget. Displays nothing.
|
||||
/// Insertion point widget. Displays nothing when not connected.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Widget {
|
||||
root: object::Instance,
|
||||
@ -34,7 +34,8 @@ impl super::SpanWidget for Widget {
|
||||
}
|
||||
|
||||
fn new(_: &Config, _: &super::ConfigContext) -> Self {
|
||||
let root = object::Instance::new();
|
||||
let root = object::Instance::new_named("widget::InsertionPoint");
|
||||
root.set_size(Vector2::<f32>::zero());
|
||||
Self { root }
|
||||
}
|
||||
|
||||
|
@ -51,11 +51,9 @@ impl super::SpanWidget for Widget {
|
||||
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 root = object::Instance::new_named("widget::Label");
|
||||
let label = text::Text::new(app);
|
||||
label.set_property_default(text::Size(TEXT_SIZE));
|
||||
label.set_y(TEXT_SIZE);
|
||||
layers.label.add(&label);
|
||||
root.add_child(&label);
|
||||
let frp = Frp::new();
|
||||
@ -81,7 +79,12 @@ impl super::SpanWidget for Widget {
|
||||
eval content_change((content) label.set_content(content));
|
||||
|
||||
width <- label.width.on_change();
|
||||
height <- label.height.on_change();
|
||||
eval width((w) root.set_size_x(*w); );
|
||||
eval height([root, label] (h) {
|
||||
root.set_size_y(*h);
|
||||
label.set_y(*h);
|
||||
});
|
||||
}
|
||||
|
||||
Self { frp, root, label }
|
||||
|
@ -1,195 +1,356 @@
|
||||
//! 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 simple [`Elements`] component, which will be replaced with a rich
|
||||
//! view in [future tasks](https://github.com/enso-org/enso/issues/5631).
|
||||
//! Module dedicated to the [List Editor widget](Widget).
|
||||
|
||||
// FIXME[ao]: This code miss important documentation (e.g. for `Element`, `DragData` and `ListItem`)
|
||||
// and may be unreadable at some places. It should be improved in several next debugging PRs.
|
||||
|
||||
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::area::TEXT_SIZE;
|
||||
use crate::component::node::input::widget::Configuration;
|
||||
use crate::component::node::input::widget::WidgetsFrp;
|
||||
use crate::component::node::input::widget::TransferRequest;
|
||||
use crate::component::node::input::widget::TreeNode;
|
||||
use crate::component::node::input::widget::WidgetIdentity;
|
||||
|
||||
use ensogl::application::Application;
|
||||
use ensogl::control::io::mouse;
|
||||
use ensogl::display;
|
||||
use ensogl::display::object::event;
|
||||
use ensogl::display::shape::StyleWatch;
|
||||
use ensogl::display::object;
|
||||
use ensogl::display::world::with_context;
|
||||
use ensogl_component::list_editor::ListEditor;
|
||||
use ensogl_component::text::Text;
|
||||
use ensogl_hardcoded_theme as theme;
|
||||
use span_tree::node::Kind;
|
||||
use std::fmt::Write;
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Widget ===
|
||||
// ==============
|
||||
// ===============
|
||||
// === Element ===
|
||||
// ===============
|
||||
|
||||
ensogl::define_endpoints_2! {
|
||||
Input {
|
||||
current_value(Option<ImString>),
|
||||
current_crumbs(span_tree::Crumbs),
|
||||
#[derive(Debug)]
|
||||
struct Element {
|
||||
display_object: object::Instance,
|
||||
content: object::Instance,
|
||||
#[allow(dead_code)]
|
||||
background: display::shape::Rectangle,
|
||||
expr_range: Range<usize>,
|
||||
item_crumb: usize,
|
||||
alive: Option<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DragData {
|
||||
element_id: WidgetIdentity,
|
||||
element: Element,
|
||||
expression: String,
|
||||
#[allow(dead_code)]
|
||||
owned_subtree: Vec<(WidgetIdentity, TreeNode)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, CloneRef)]
|
||||
struct ListItem {
|
||||
element_id: Immutable<WidgetIdentity>,
|
||||
display_object: object::Instance,
|
||||
drag_data: Rc<RefCell<Option<DragData>>>,
|
||||
}
|
||||
|
||||
impl PartialEq for ListItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.element_id == other.element_id
|
||||
}
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
fn take_drag_data(&self) -> Option<DragData> {
|
||||
let mut borrow = self.drag_data.borrow_mut();
|
||||
let can_take = matches!(&*borrow, Some(data) if data.element_id == *self.element_id);
|
||||
can_take.and_option_from(|| borrow.take())
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for ListItem {
|
||||
fn display_object(&self) -> &object::Instance {
|
||||
&self.display_object
|
||||
}
|
||||
}
|
||||
|
||||
impl Element {
|
||||
fn new() -> Self {
|
||||
let display_object = object::Instance::new_named("Element");
|
||||
let content = object::Instance::new_named("Content");
|
||||
let background = display::shape::Rectangle::new();
|
||||
background.set_color(display::shape::INVISIBLE_HOVER_COLOR);
|
||||
background.allow_grow().set_alignment_left_center();
|
||||
content.use_auto_layout().set_children_alignment_left_center();
|
||||
with_context(|ctx| ctx.layers.label.add(&background));
|
||||
display_object.replace_children(&[background.display_object(), &content]);
|
||||
Self {
|
||||
display_object,
|
||||
content,
|
||||
background,
|
||||
expr_range: default(),
|
||||
alive: default(),
|
||||
item_crumb: default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for Element {
|
||||
fn display_object(&self) -> &object::Instance {
|
||||
&self.display_object
|
||||
}
|
||||
}
|
||||
|
||||
/// A model for the vector editor widget.
|
||||
///
|
||||
/// Currently it displays an activation shape (a triangle) which, on click, displays the widget
|
||||
/// view. The view is a [`ListEditor`] - see its documentation for available GUI actions. Currently
|
||||
/// only adding new elements is supported.
|
||||
///
|
||||
/// 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 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<Text>,
|
||||
display_object: object::Instance,
|
||||
network: frp::Network,
|
||||
model: Rc<RefCell<Model>>,
|
||||
}
|
||||
|
||||
impl Widget {
|
||||
/// 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, widgets_frp: &WidgetsFrp, styles: &StyleWatch) -> Self {
|
||||
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
|
||||
.use_auto_layout()
|
||||
.set_column_count(1)
|
||||
.set_gap_y(Self::GAP)
|
||||
.set_children_alignment_center();
|
||||
display_object.set_size_hug();
|
||||
|
||||
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, widgets_frp: &WidgetsFrp) -> Self {
|
||||
let network = &self.config_frp.network;
|
||||
let display_object = &self.display_object;
|
||||
let list_container = &self.list_container;
|
||||
let list = &self.list;
|
||||
let dot_clicked = self.activation_shape.on_event::<mouse::Down>();
|
||||
let focus_in = self.display_object.on_event::<event::FocusIn>();
|
||||
let focus_out = self.display_object.on_event::<event::FocusOut>();
|
||||
fn init_list_updates(self, ctx: &super::ConfigContext) -> Self {
|
||||
let widgets_frp = ctx.frp();
|
||||
let network = &self.network;
|
||||
let model = &self.model;
|
||||
let list = self.model.borrow().list.clone_ref();
|
||||
|
||||
frp::extend! { network
|
||||
init <- source_();
|
||||
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(),
|
||||
});
|
||||
// Adding elements.
|
||||
requested_new <- list.request_new_item.filter_map(|resp| resp.gui_interaction_payload());
|
||||
widgets_frp.value_changed <+ requested_new.filter_map(f!((idx) model.borrow_mut().on_new_item(*idx)));
|
||||
|
||||
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, 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(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));
|
||||
eval_ empty_value ([list] Self::clear_list(&list));
|
||||
|
||||
code_changed_by_user <-
|
||||
list.request_new_item.map(f_!([app, list] Self::push_new_element(&app, &list)));
|
||||
value_changed <- code_changed_by_user.map(f_!([list] {
|
||||
Some(ImString::new(Self::construct_code(&list)))
|
||||
// Inserting dragged elements.
|
||||
inserted_by_user <- list.on_item_added.filter_map(|resp| resp.gui_interaction_payload());
|
||||
widgets_frp.value_changed <+ inserted_by_user.filter_map(f!([list, model](index) {
|
||||
let item = list.item_at(*index)?;
|
||||
model.borrow_mut().on_item_added(item, *index)
|
||||
}));
|
||||
widgets_frp.value_changed <+ value_changed.map2(&config_frp.current_crumbs,
|
||||
move |t: &Option<ImString>, crumbs: &span_tree::Crumbs| (crumbs.clone(), t.clone())
|
||||
);
|
||||
|
||||
// Removing dragged elements.
|
||||
removed_by_user <- list.on_item_removed.filter_map(|resp| resp.clone().gui_interaction_payload());
|
||||
remove_request <- removed_by_user.filter_map(f!([model] ((_, item)) {
|
||||
let item = item.borrow_mut().take()?;
|
||||
model.borrow_mut().on_item_removed(item)
|
||||
}));
|
||||
widgets_frp.transfer_ownership <+ remove_request._1();
|
||||
widgets_frp.value_changed <+ remove_request._0().map(|crumb| (crumb.clone(), None));
|
||||
}
|
||||
init.emit(());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_list(list: &ListEditor<Text>) {
|
||||
for _ in 0..list.items().len() {
|
||||
list.remove(0);
|
||||
#[derive(Debug)]
|
||||
struct Model {
|
||||
self_id: WidgetIdentity,
|
||||
list: ListEditor<ListItem>,
|
||||
elements: HashMap<WidgetIdentity, Element>,
|
||||
default_value: String,
|
||||
expression: String,
|
||||
crumbs: span_tree::Crumbs,
|
||||
drag_data_rc: Rc<RefCell<Option<DragData>>>,
|
||||
received_drag: Option<DragData>,
|
||||
insertion_indices: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn new(ctx: &super::ConfigContext, display_object: &object::Instance) -> Self {
|
||||
let list = ListEditor::new(&ctx.app().cursor);
|
||||
list.set_size_hug_y(TEXT_SIZE).allow_grow_y();
|
||||
display_object.use_auto_layout().set_children_alignment_left_center();
|
||||
Self {
|
||||
self_id: ctx.info.identity,
|
||||
list,
|
||||
elements: default(),
|
||||
default_value: default(),
|
||||
expression: default(),
|
||||
crumbs: default(),
|
||||
drag_data_rc: default(),
|
||||
received_drag: default(),
|
||||
insertion_indices: default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_list(app: &Application, code: &str, list: &ListEditor<Text>) {
|
||||
let mut codes = Self::parse_array_code(code).fuse();
|
||||
let mut widgets_kept = 0;
|
||||
for widget in list.items() {
|
||||
match codes.next() {
|
||||
Some(code) => {
|
||||
widgets_kept += 1;
|
||||
if widget.content.value().to_string() != code {
|
||||
widget.set_content(ImString::new(code));
|
||||
}
|
||||
fn configure(&mut self, root: &object::Instance, cfg: &Config, mut ctx: super::ConfigContext) {
|
||||
self.expression.clear();
|
||||
self.default_value.clear();
|
||||
self.expression.push_str(ctx.expression_at(ctx.span_node.span()));
|
||||
self.crumbs = ctx.span_node.crumbs.clone();
|
||||
|
||||
// Right now, nested list editors are broken. Prevent them from being created. Whenever
|
||||
// a nested list editor is requested, we instead use a hierarchical widget to display the
|
||||
// child list items as ordinary expressions.
|
||||
let ext = ctx.get_extension_or_default::<Extension>();
|
||||
let already_in_list = ext.already_in_list;
|
||||
ctx.set_extension(Extension { already_in_list: true });
|
||||
|
||||
if already_in_list {
|
||||
let child = ctx.builder.child_widget_of_type(
|
||||
ctx.span_node,
|
||||
ctx.info.nesting_level,
|
||||
Some(&super::Configuration::always(super::hierarchy::Config)),
|
||||
);
|
||||
root.replace_children(&[&child.root_object]);
|
||||
} else if ctx.span_node.is_insertion_point() {
|
||||
write!(self.default_value, "[{}]", cfg.item_default).unwrap();
|
||||
self.configure_insertion_point(root, ctx)
|
||||
} else {
|
||||
self.default_value.push_str(&cfg.item_default);
|
||||
self.configure_vector(root, cfg, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_insertion_point(&mut self, root: &object::Instance, ctx: super::ConfigContext) {
|
||||
self.elements.clear();
|
||||
let insertion_point = ctx.builder.child_widget_of_type(
|
||||
ctx.span_node,
|
||||
ctx.info.nesting_level,
|
||||
Some(&super::Configuration::always(super::label::Config)),
|
||||
);
|
||||
root.replace_children(&[self.list.display_object(), &*insertion_point]);
|
||||
}
|
||||
|
||||
fn configure_vector(
|
||||
&mut self,
|
||||
root: &object::Instance,
|
||||
cfg: &Config,
|
||||
ctx: super::ConfigContext,
|
||||
) {
|
||||
let no_nest = ctx.info.nesting_level;
|
||||
let nest = ctx.info.nesting_level.next();
|
||||
let child_config = cfg.item_widget.as_deref();
|
||||
|
||||
let mut build_child_widget = |i, nest, config, allow_margin: bool| {
|
||||
let mut node = ctx.span_node.clone().child(i).expect("invalid index");
|
||||
if !allow_margin {
|
||||
node.sibling_offset = 0.into();
|
||||
}
|
||||
ctx.builder.child_widget_of_type(node, nest, config)
|
||||
};
|
||||
|
||||
let mut open_bracket = None;
|
||||
let mut close_bracket = None;
|
||||
let mut last_insert_crumb = None;
|
||||
let mut list_items = SmallVec::<[ListItem; 16]>::new();
|
||||
|
||||
self.insertion_indices.clear();
|
||||
for (index, child) in ctx.span_node.node.children.iter().enumerate() {
|
||||
let node = &child.node;
|
||||
let start = child.parent_offset.value as usize;
|
||||
let range = start..start + node.size.value as usize;
|
||||
let expr = &self.expression[range.clone()];
|
||||
|
||||
match node.kind {
|
||||
Kind::Token if expr == "[" && open_bracket.is_none() => {
|
||||
let child = build_child_widget(index, no_nest, None, true);
|
||||
open_bracket = Some(child.root_object);
|
||||
}
|
||||
None => {
|
||||
list.remove(widgets_kept);
|
||||
Kind::Token if expr == "]" && close_bracket.is_none() => {
|
||||
let child = build_child_widget(index, no_nest, None, false);
|
||||
close_bracket = Some(child.root_object);
|
||||
}
|
||||
Kind::InsertionPoint(_) => {
|
||||
last_insert_crumb = Some(index);
|
||||
}
|
||||
Kind::Argument(_) if last_insert_crumb.is_some() => {
|
||||
let insert_index = last_insert_crumb.take().unwrap();
|
||||
let insert = build_child_widget(insert_index, no_nest, None, false);
|
||||
let item = build_child_widget(index, nest, child_config, false);
|
||||
let element = self.elements.entry(item.id).or_insert_with(|| {
|
||||
self.received_drag.take().map_or_else(Element::new, |d| d.element)
|
||||
});
|
||||
element.alive = Some(());
|
||||
element.item_crumb = index;
|
||||
element.expr_range = range;
|
||||
element.content.replace_children(&[&*insert, &*item]);
|
||||
self.insertion_indices.push(insert_index);
|
||||
list_items.push(ListItem {
|
||||
element_id: Immutable(item.id),
|
||||
display_object: element.display_object.clone(),
|
||||
drag_data: self.drag_data_rc.clone(),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for code in codes {
|
||||
let widget = Text::new(app);
|
||||
widget.set_content(ImString::new(code));
|
||||
app.display.default_scene.layers.label.add(&widget);
|
||||
list.push(widget);
|
||||
|
||||
self.elements.retain(|_, child| child.alive.take().is_some());
|
||||
self.insertion_indices.extend(last_insert_crumb);
|
||||
|
||||
let current_items = self.list.items();
|
||||
list_diff(¤t_items, &list_items, |op| match op {
|
||||
DiffOp::Delete { at, old, present_later } =>
|
||||
if present_later.is_some()
|
||||
|| list_items.iter().any(|i| i.display_object == old.display_object)
|
||||
{
|
||||
self.list.take_item(at);
|
||||
} else {
|
||||
self.list.remove(at);
|
||||
},
|
||||
DiffOp::Insert { at, new } => {
|
||||
self.list.insert_item(at, new.clone_ref());
|
||||
}
|
||||
DiffOp::Update { at, old, new } =>
|
||||
if old.display_object() != new.display_object() {
|
||||
self.list.replace_item(at, new.clone_ref());
|
||||
},
|
||||
});
|
||||
|
||||
let append_insert = last_insert_crumb
|
||||
.map(|index| build_child_widget(index, no_nest, None, false).root_object);
|
||||
let (open_bracket, close_bracket) = open_bracket.zip(close_bracket).unzip();
|
||||
let mut children = SmallVec::<[&object::Instance; 4]>::new();
|
||||
children.extend(&open_bracket);
|
||||
children.push(self.list.display_object());
|
||||
children.extend(&append_insert);
|
||||
children.extend(&close_bracket);
|
||||
root.replace_children(&children);
|
||||
}
|
||||
|
||||
fn on_item_removed(&mut self, item: ListItem) -> Option<(span_tree::Crumbs, TransferRequest)> {
|
||||
let element = self.elements.get(&item.element_id)?;
|
||||
let crumbs = self.crumbs.sub(element.item_crumb);
|
||||
let request = TransferRequest {
|
||||
new_owner: self.self_id,
|
||||
to_transfer: *item.element_id,
|
||||
whole_subtree: true,
|
||||
};
|
||||
Some((crumbs, request))
|
||||
}
|
||||
|
||||
fn receive_ownership_of_dragged_item(
|
||||
&mut self,
|
||||
req: TransferRequest,
|
||||
owned_subtree: Vec<(WidgetIdentity, TreeNode)>,
|
||||
) {
|
||||
let element_id = req.to_transfer;
|
||||
let element = self.elements.remove(&element_id);
|
||||
if let Some(element) = element {
|
||||
let expression = self.expression[element.expr_range.clone()].to_owned();
|
||||
let drag_data = DragData { element_id, element, expression, owned_subtree };
|
||||
self.drag_data_rc.replace(Some(drag_data));
|
||||
} else {
|
||||
error!("Grabbed item not found.");
|
||||
}
|
||||
}
|
||||
|
||||
fn push_new_element(app: &Application, list: &ListEditor<Text>) {
|
||||
let widget = Text::new(app);
|
||||
widget.set_content("_");
|
||||
list.push(widget);
|
||||
fn on_item_added(
|
||||
&mut self,
|
||||
item: ListItem,
|
||||
at: usize,
|
||||
) -> Option<(span_tree::Crumbs, Option<ImString>)> {
|
||||
self.received_drag = item.take_drag_data();
|
||||
let expression: ImString = mem::take(&mut self.received_drag.as_mut()?.expression).into();
|
||||
match &self.insertion_indices[..] {
|
||||
&[] => Some((self.crumbs.clone(), Some(expression))),
|
||||
ids => ids.get(at).map(|idx| (self.crumbs.sub(*idx), Some(expression))),
|
||||
}
|
||||
}
|
||||
|
||||
fn construct_code(list: &ListEditor<Text>) -> String {
|
||||
let subwidgets = list.items().into_iter();
|
||||
let mut subwidgets_codes = subwidgets.map(|sub| sub.content.value().to_string());
|
||||
format!("[{}]", subwidgets_codes.join(","))
|
||||
}
|
||||
|
||||
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()
|
||||
fn on_new_item(&mut self, at: usize) -> Option<(span_tree::Crumbs, Option<ImString>)> {
|
||||
let expression: ImString = self.default_value.clone().into();
|
||||
match &self.insertion_indices[..] {
|
||||
&[] => Some((self.crumbs.clone(), Some(expression))),
|
||||
ids => ids.get(at).map(|idx| (self.crumbs.sub(*idx), Some(expression))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,22 +369,119 @@ pub struct Config {
|
||||
impl super::SpanWidget for Widget {
|
||||
type Config = Config;
|
||||
|
||||
fn root_object(&self) -> &display::object::Instance {
|
||||
fn root_object(&self) -> &object::Instance {
|
||||
&self.display_object
|
||||
}
|
||||
|
||||
fn new(_: &Config, ctx: &super::ConfigContext) -> Self {
|
||||
Self::new(ctx.app(), ctx.frp(), ctx.styles())
|
||||
console_log!("NEW");
|
||||
let display_object = object::Instance::new_named("widget::ListEditor");
|
||||
let model = Model::new(ctx, &display_object);
|
||||
let network = frp::Network::new("widget::ListEditor");
|
||||
Self { display_object, network, model: Rc::new(RefCell::new(model)) }.init_list_updates(ctx)
|
||||
}
|
||||
|
||||
fn configure(&mut self, _: &Config, ctx: super::ConfigContext) {
|
||||
let current_value: Option<ImString> = 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());
|
||||
fn configure(&mut self, cfg: &Config, ctx: super::ConfigContext) {
|
||||
console_log!("CONFIGURE");
|
||||
let mut model = self.model.borrow_mut();
|
||||
model.configure(&self.display_object, cfg, ctx);
|
||||
}
|
||||
|
||||
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, Some(&label_meta));
|
||||
self.child_container.replace_children(&[child]);
|
||||
fn receive_ownership(&mut self, req: TransferRequest, nodes: Vec<(WidgetIdentity, TreeNode)>) {
|
||||
let mut model = self.model.borrow_mut();
|
||||
model.receive_ownership_of_dragged_item(req, nodes);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum DiffOp<'old, 'new, T> {
|
||||
Delete { at: usize, old: &'old T, present_later: Option<usize> },
|
||||
Insert { at: usize, new: &'new T },
|
||||
Update { at: usize, old: &'old T, new: &'new T },
|
||||
}
|
||||
|
||||
fn list_diff<'old, 'new, T>(
|
||||
old_elements: &'old [T],
|
||||
new_elements: &'new [T],
|
||||
mut f: impl FnMut(DiffOp<'old, 'new, T>),
|
||||
) where
|
||||
T: PartialEq,
|
||||
{
|
||||
// Indices for next elements to process in both lists.
|
||||
let mut current_old = 0;
|
||||
let mut current_new = 0;
|
||||
|
||||
// The current index of insertion or deletion in the list that is being processed. Effectively
|
||||
// the number of insertions or equal pairs that have been processed so far.
|
||||
let mut at = 0;
|
||||
|
||||
while current_old < old_elements.len() && current_new < new_elements.len() {
|
||||
let old = &old_elements[current_old];
|
||||
let new = &new_elements[current_new];
|
||||
|
||||
// Next pair of elements are equal, so we don't need to do anything.
|
||||
if old == new {
|
||||
f(DiffOp::Update { at, old, new });
|
||||
current_old += 1;
|
||||
current_new += 1;
|
||||
at += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let remaining_old = &old_elements[current_old + 1..];
|
||||
let remaining_new = &new_elements[current_new + 1..];
|
||||
|
||||
let old_still_in_new_list = remaining_new.contains(old);
|
||||
if !old_still_in_new_list {
|
||||
f(DiffOp::Delete { at, old, present_later: None });
|
||||
current_old += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let index_in_remaining_old = remaining_old.iter().position(|x| x == new);
|
||||
match index_in_remaining_old {
|
||||
// Not present in old, thus it is an insertion.
|
||||
None => {
|
||||
f(DiffOp::Insert { at, new });
|
||||
at += 1;
|
||||
current_new += 1;
|
||||
continue;
|
||||
}
|
||||
// Present in old. Delete all elements in between and insert the matching one.
|
||||
Some(advance) => {
|
||||
f(DiffOp::Delete { at, old, present_later: Some(current_old + advance + 1) });
|
||||
for k in 0..advance {
|
||||
let present_later = remaining_old[k + 1..].iter().position(|old| old == new);
|
||||
f(DiffOp::Delete { at, old: &remaining_old[k], present_later });
|
||||
}
|
||||
current_old += advance + 1;
|
||||
let old = &old_elements[current_old];
|
||||
let new = &new_elements[current_new];
|
||||
f(DiffOp::Update { at, old, new });
|
||||
current_old += 1;
|
||||
current_new += 1;
|
||||
at += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
while current_old < old_elements.len() {
|
||||
f(DiffOp::Delete { at, old: &old_elements[current_old], present_later: None });
|
||||
current_old += 1;
|
||||
}
|
||||
while current_new < new_elements.len() {
|
||||
f(DiffOp::Insert { at, new: &new_elements[current_new] });
|
||||
current_new += 1;
|
||||
at += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =================
|
||||
// === Extension ===
|
||||
// =================
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct Extension {
|
||||
already_in_list: bool,
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ impl super::SpanWidget for Widget {
|
||||
let layers = &app.display.default_scene.layers;
|
||||
layers.label.add(&activation_shape);
|
||||
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_named("widget::SingleChoice");
|
||||
let content_wrapper = display_object.new_child();
|
||||
content_wrapper.add_child(&activation_shape);
|
||||
let label_wrapper = content_wrapper.new_child();
|
||||
@ -182,7 +182,7 @@ impl super::SpanWidget for Widget {
|
||||
};
|
||||
let child_level = ctx.info.nesting_level;
|
||||
let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, Some(&config));
|
||||
self.label_wrapper.replace_children(&[child]);
|
||||
self.label_wrapper.replace_children(&[child.root_object]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,7 +171,7 @@ impl Model {
|
||||
/// Constructor.
|
||||
#[profile(Debug)]
|
||||
pub fn new(app: &Application, frp: &Frp) -> Self {
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_named("output");
|
||||
let ports = display::object::Instance::new();
|
||||
let app = app.clone_ref();
|
||||
let label = app.new_view::<text::Text>();
|
||||
|
@ -619,8 +619,7 @@ ensogl::define_endpoints_2! {
|
||||
set_detached_edge_sources (EdgeEndpoint),
|
||||
set_edge_source ((EdgeId, EdgeEndpoint)),
|
||||
set_edge_target ((EdgeId, EdgeEndpoint)),
|
||||
unset_edge_source (EdgeId),
|
||||
unset_edge_target (EdgeId),
|
||||
replace_detached_edge_target ((EdgeId, span_tree::Crumbs)),
|
||||
connect_nodes ((EdgeEndpoint,EdgeEndpoint)),
|
||||
deselect_all_nodes (),
|
||||
press_node_input (EdgeEndpoint),
|
||||
@ -1830,7 +1829,7 @@ impl GraphEditorModel {
|
||||
pub fn new(app: &Application, cursor: cursor::Cursor, frp: &Frp) -> Self {
|
||||
let network = frp.network();
|
||||
let scene = &app.display.default_scene;
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_named("GraphEditor");
|
||||
let nodes = Nodes::new();
|
||||
let edges = Edges::new();
|
||||
let vis_registry = visualization::Registry::with_default_visualizations();
|
||||
@ -2183,6 +2182,22 @@ impl GraphEditorModel {
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_detached_edge_target(&self, edge_id: EdgeId, crumbs: &span_tree::Crumbs) {
|
||||
if !self.edges.detached_source.contains(&edge_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(edge) = self.edges.get_cloned_ref(&edge_id) {
|
||||
if let Some(target) = edge.take_target() {
|
||||
self.set_input_connected(&target, None);
|
||||
let port = crumbs.clone();
|
||||
let new_target = EdgeEndpoint { port, ..target };
|
||||
edge.set_target(new_target);
|
||||
self.refresh_edge_position(edge_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn take_edges_with_detached_targets(&self) -> HashSet<EdgeId> {
|
||||
let edges = self.edges.detached_target.mem_take();
|
||||
self.check_edge_attachment_status_and_emit_events();
|
||||
@ -3647,6 +3662,7 @@ fn new_graph_editor(app: &Application) -> GraphEditor {
|
||||
|
||||
eval out.on_edge_source_unset (((id,_)) model.remove_edge_source(*id));
|
||||
eval out.on_edge_target_unset (((id,_)) model.remove_edge_target(*id));
|
||||
eval inputs.replace_detached_edge_target (((id,tgt)) model.replace_detached_edge_target(*id,tgt));
|
||||
|
||||
is_only_tgt_not_set <-
|
||||
out.on_edge_source_set.map(f!(((id,_)) model.with_edge_map_target(*id,|_|()).is_none()));
|
||||
@ -3660,6 +3676,8 @@ 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();
|
||||
|
||||
eval inputs.replace_detached_edge_target ([model,neutral_color]((id, _))
|
||||
model.refresh_edge_color(*id,neutral_color.value().into()));
|
||||
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, _))
|
||||
|
@ -116,7 +116,7 @@
|
||||
"primary": false
|
||||
},
|
||||
"vectorEditor": {
|
||||
"value": false,
|
||||
"value": true,
|
||||
"description": "Show Vector Editor widget on nodes.",
|
||||
"primary": false
|
||||
},
|
||||
|
@ -199,3 +199,27 @@ body {
|
||||
background: #b96a50;
|
||||
margin: 0.8em -1em;
|
||||
}
|
||||
|
||||
#debug-root {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
#debug-root > .debug-layer {
|
||||
position: absolute;
|
||||
top: 50vh;
|
||||
left: 50vw;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
#debug-root > .debug-layer * {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Options intended to be common for all developers.
|
||||
|
||||
wasm-size-limit: 15.80 MiB
|
||||
wasm-size-limit: 15.83 MiB
|
||||
|
||||
required-versions:
|
||||
# NB. The Rust version is pinned in rust-toolchain.toml.
|
||||
|
@ -34,9 +34,9 @@ impl<T: display::Object> Item<T> {
|
||||
|
||||
pub fn new_from_placeholder(elem: T, placeholder: StrongPlaceholder) -> Self {
|
||||
let frp = Frp::new();
|
||||
placeholder.add_child(&elem);
|
||||
let network = frp.network();
|
||||
let elem_obj = elem.display_object();
|
||||
placeholder.replace_children(&[&elem_obj]);
|
||||
let margin_left = Animation::<f32>::new_with_init(network, 0.0);
|
||||
let elem_offset = Animation::<Vector2>::new_with_init(network, elem.position().xy());
|
||||
margin_left.simulator.update_spring(|s| s * crate::DEBUG_ANIMATION_SPRING_FACTOR);
|
||||
|
@ -73,6 +73,7 @@
|
||||
#![allow(clippy::bool_to_int_with_if)]
|
||||
#![allow(clippy::let_and_return)]
|
||||
|
||||
use ensogl_core::display::shape::compound::rectangle::*;
|
||||
use ensogl_core::display::world::*;
|
||||
use ensogl_core::prelude::*;
|
||||
|
||||
@ -82,7 +83,6 @@ use ensogl_core::data::color;
|
||||
use ensogl_core::display;
|
||||
use ensogl_core::display::object::Event;
|
||||
use ensogl_core::display::object::ObjectOps;
|
||||
use ensogl_core::display::shape::compound::rectangle::*;
|
||||
use ensogl_core::gui::cursor;
|
||||
use ensogl_core::gui::cursor::Cursor;
|
||||
use ensogl_core::gui::cursor::Trash;
|
||||
@ -93,7 +93,6 @@ use placeholder::StrongPlaceholder;
|
||||
use placeholder::WeakPlaceholder;
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
@ -153,6 +152,10 @@ impl<T> Response<T> {
|
||||
pub fn gui(payload: T) -> Self {
|
||||
Self::new(payload, true)
|
||||
}
|
||||
|
||||
pub fn gui_interaction_payload(self) -> Option<T> {
|
||||
self.gui_interaction.then_some(self.payload)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -203,6 +206,20 @@ impl<T: display::Object> ItemOrPlaceholder<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the item element with a new one. Returns old element if it was replaced.
|
||||
pub fn replace_element(&mut self, element: T) -> Option<T> {
|
||||
match self {
|
||||
ItemOrPlaceholder::Item(t) =>
|
||||
if t.elem.display_object() == element.display_object() {
|
||||
Some(mem::replace(&mut t.elem, element))
|
||||
} else {
|
||||
let new_item = Item::new_from_placeholder(element, t.placeholder.clone_ref());
|
||||
Some(mem::replace(t, new_item).elem)
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the display object of this item or placeholder. In case the placeholder is weak and does
|
||||
/// not exist anymore, it will return [`None`].
|
||||
pub fn display_object(&self) -> Option<display::object::Instance> {
|
||||
@ -344,9 +361,9 @@ impl<T> Model<T> {
|
||||
pub fn new(cursor: &Cursor) -> Self {
|
||||
let cursor = cursor.clone_ref();
|
||||
let items = default();
|
||||
let root = display::object::Instance::new();
|
||||
let layout = display::object::Instance::new();
|
||||
let layout_with_icons = display::object::Instance::new();
|
||||
let root = display::object::Instance::new_named("ListEditor");
|
||||
let layout = display::object::Instance::new_named("layout");
|
||||
let layout_with_icons = display::object::Instance::new_named("layout_with_icons");
|
||||
let gap = default();
|
||||
layout_with_icons.use_auto_layout();
|
||||
layout.use_auto_layout();
|
||||
@ -354,7 +371,7 @@ impl<T> Model<T> {
|
||||
root.add_child(&layout_with_icons);
|
||||
let add_elem_icon = Rectangle().build(|t| {
|
||||
t.set_corner_radius_max()
|
||||
.set_size((24.0, 24.0))
|
||||
.set_size((14.0, 14.0))
|
||||
.set_color(color::Rgba::new(0.0, 0.0, 0.0, 0.2));
|
||||
});
|
||||
layout_with_icons.add_child(&add_elem_icon);
|
||||
@ -390,7 +407,7 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
|
||||
let network = self.frp.network();
|
||||
let model = &self.model;
|
||||
|
||||
let on_add_elem_icon_down = model.borrow().add_elem_icon.on_event::<mouse::Down>();
|
||||
let on_add_elem_icon_up = model.borrow().add_elem_icon.on_event::<mouse::Up>();
|
||||
let on_down = model.borrow().layout.on_event_capturing::<mouse::Down>();
|
||||
let on_up_source = scene.on_event::<mouse::Up>();
|
||||
let on_move = scene.on_event::<mouse::Move>();
|
||||
@ -400,7 +417,7 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
|
||||
let drag_target = cursor::DragTarget::new();
|
||||
frp::extend! { network
|
||||
|
||||
frp.private.output.request_new_item <+ on_add_elem_icon_down.map(f_!([model] {
|
||||
frp.private.output.request_new_item <+ on_add_elem_icon_up.map(f_!([model] {
|
||||
Response::gui(model.borrow().len())
|
||||
}));
|
||||
|
||||
@ -455,13 +472,13 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
|
||||
}
|
||||
|
||||
self.init_add_and_remove();
|
||||
let (is_dragging, _drag_diff, no_drag) =
|
||||
let no_drag =
|
||||
self.init_dragging(cursor, &on_up, &on_up_cleaning_phase, &on_down, &target, &pos_diff);
|
||||
frp::extend! { network
|
||||
on_up_close <- on_up.gate(&is_close);
|
||||
}
|
||||
self.init_dropping(&on_up_close, &dragged_item_bbox_center, &is_close);
|
||||
let insert_pointer_style = self.init_insertion_points(&on_up, &pos_on_move, &is_dragging);
|
||||
let insert_pointer_style = self.init_insertion_points(cursor, &on_up, &pos_on_move);
|
||||
|
||||
frp::extend! { network
|
||||
cursor.frp.set_style_override <+ insert_pointer_style;
|
||||
@ -481,13 +498,12 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
|
||||
|
||||
fn init_insertion_points(
|
||||
&self,
|
||||
cursor: &Cursor,
|
||||
on_up: &frp::Stream<Event<mouse::Up>>,
|
||||
pos_on_move: &frp::Stream<Vector2>,
|
||||
is_dragging: &frp::Stream<bool>,
|
||||
) -> frp::Stream<Option<cursor::Style>> {
|
||||
let on_up = on_up.clone_ref();
|
||||
let pos_on_move = pos_on_move.clone_ref();
|
||||
let is_dragging = is_dragging.clone_ref();
|
||||
|
||||
let frp = &self.frp;
|
||||
let network = self.frp.network();
|
||||
@ -496,6 +512,11 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
|
||||
|
||||
frp::extend! { network
|
||||
gaps <- model_borrowed.layout.on_resized.map(f_!(model.gaps()));
|
||||
|
||||
// We are debouncing the `is_dragging` stream to avoid double-borrow of list editor, as
|
||||
// this event is fired immediately after list-editor instructs cursor to stop dragging.
|
||||
is_dragging <- cursor.frp.is_dragging.debounce();
|
||||
|
||||
opt_index <- all_with7(
|
||||
&frp.gap,
|
||||
&gaps,
|
||||
@ -519,7 +540,7 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
|
||||
).on_change();
|
||||
index <= opt_index;
|
||||
enabled <- opt_index.is_some();
|
||||
pointer_style <- enabled.then_constant(cursor::Style::plus());
|
||||
pointer_style <- enabled.then_constant(cursor::Style::plus()).on_change();
|
||||
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));
|
||||
@ -544,7 +565,8 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
|
||||
|
||||
let on_item_removed = &frp.private.output.on_item_removed;
|
||||
eval frp.remove([model, on_item_removed] (index) {
|
||||
if let Some(item) = model.borrow_mut().trash_item_at(*index) {
|
||||
let item = model.borrow_mut().trash_item_at(*index);
|
||||
if let Some(item) = item {
|
||||
on_item_removed.emit(Response::api((*index, Rc::new(RefCell::new(Some(item))))));
|
||||
}
|
||||
});
|
||||
@ -560,7 +582,7 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
|
||||
on_down: &frp::Stream<Event<mouse::Down>>,
|
||||
target: &frp::Stream<display::object::Instance>,
|
||||
pos_diff: &frp::Stream<Vector2>,
|
||||
) -> (frp::Stream<bool>, frp::Stream<Vector2>, frp::Stream<bool>) {
|
||||
) -> frp::Stream<bool> {
|
||||
let model = &self.model;
|
||||
let on_up = on_up.clone_ref();
|
||||
let on_up_cleaning_phase = on_up_cleaning_phase.clone_ref();
|
||||
@ -591,15 +613,15 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
|
||||
start <- status.on_true();
|
||||
target_on_start <- target.sample(&start);
|
||||
let on_item_removed = &frp.private.output.on_item_removed;
|
||||
eval target_on_start([model, on_item_removed, cursor] (t) {
|
||||
let indexed_item = model.borrow_mut().start_item_drag(t);
|
||||
if let Some((index, item)) = indexed_item {
|
||||
eval target_on_start([model, cursor, on_item_removed] (t) {
|
||||
let item = model.borrow_mut().start_item_drag(t);
|
||||
if let Some((index, item)) = item {
|
||||
cursor.start_drag(item.clone_ref());
|
||||
on_item_removed.emit(Response::gui((index, Rc::new(RefCell::new(Some(item))))));
|
||||
}
|
||||
});
|
||||
}
|
||||
(status, drag_diff, no_drag)
|
||||
no_drag
|
||||
}
|
||||
|
||||
/// Implementation of dropping items logic, including showing empty placeholders when the item
|
||||
@ -659,6 +681,35 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
|
||||
pub fn items(&self) -> Vec<T> {
|
||||
self.model.borrow().items.iter().flat_map(|item| item.as_item().cloned()).collect()
|
||||
}
|
||||
|
||||
pub fn insert_item(&self, index: Index, item: T) {
|
||||
self.model.borrow_mut().insert(index, item);
|
||||
}
|
||||
|
||||
pub fn replace_item(&self, index: Index, new_item: T) -> Option<T> {
|
||||
let mut model = self.model.borrow_mut();
|
||||
let index = model.index_to_item_or_placeholder_index(index)?;
|
||||
let item = model.items.get_mut(index)?;
|
||||
item.replace_element(new_item)
|
||||
}
|
||||
|
||||
pub fn take_item(&self, index: Index) -> Option<T> {
|
||||
let mut model = self.model.borrow_mut();
|
||||
let index = model.index_to_item_or_placeholder_index(index)?;
|
||||
match model.items.remove(index) {
|
||||
ItemOrPlaceholder::Item(item) => {
|
||||
model.item_count_changed();
|
||||
Some(item.elem)
|
||||
}
|
||||
ItemOrPlaceholder::Placeholder(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_at(&self, index: Index) -> Option<T> {
|
||||
let model = self.model.borrow();
|
||||
let index = model.index_to_item_or_placeholder_index(index)?;
|
||||
model.items.get(index).and_then(|item| item.as_item().cloned())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: display::Object + CloneRef + 'static> SharedModel<T> {
|
||||
@ -792,6 +843,7 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
|
||||
self.push(item)
|
||||
};
|
||||
self.reposition_items();
|
||||
self.item_count_changed();
|
||||
index
|
||||
}
|
||||
|
||||
@ -906,8 +958,8 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
|
||||
/// See docs of [`Self::start_item_drag_at`] for more information.
|
||||
fn start_item_drag(&mut self, target: &display::object::Instance) -> Option<(Index, T)> {
|
||||
let objs = target.rev_parent_chain().reversed();
|
||||
let tarrget_index = objs.into_iter().find_map(|t| self.item_index_of(&t));
|
||||
if let Some((index, index_or_placeholder_index)) = tarrget_index {
|
||||
let target_index = objs.into_iter().find_map(|t| self.item_index_of(&t));
|
||||
if let Some((index, index_or_placeholder_index)) = target_index {
|
||||
self.start_item_drag_at(index_or_placeholder_index).map(|item| (index, item))
|
||||
} else {
|
||||
warn!("Could not find the item to drag.");
|
||||
@ -1005,24 +1057,28 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
|
||||
/// Place the currently dragged element in the given index. The item will be enclosed in the
|
||||
/// [`Item`] object, will handles its animation. See the documentation of
|
||||
/// [`ItemOrPlaceholder`] to learn more.
|
||||
fn place_dragged_item(&mut self, index: ItemOrPlaceholderIndex) -> Option<Index> {
|
||||
fn place_dragged_item(&mut self, index: ItemOrPlaceholderIndex) -> Option<Index>
|
||||
where T: Debug {
|
||||
if let Some(item) = self.cursor.stop_drag_if_is::<T>() {
|
||||
self.collapse_all_placeholders_no_margin_update();
|
||||
if let Some((index, placeholder)) = self.get_indexed_merged_placeholder_at(index) {
|
||||
let actual_index = if let Some((index, placeholder)) =
|
||||
self.get_indexed_merged_placeholder_at(index)
|
||||
{
|
||||
placeholder.set_target_size(placeholder.computed_size().x);
|
||||
item.update_xy(|t| t - placeholder.global_position().xy());
|
||||
self.items[index] =
|
||||
Item::new_from_placeholder(item.clone_ref(), placeholder).into();
|
||||
self.items[index] = Item::new_from_placeholder(item, placeholder).into();
|
||||
index
|
||||
} else {
|
||||
// This branch should never be reached, as when dragging an item we always create
|
||||
// a placeholder for it (see the [`Self::add_insertion_point_if_type_match`]
|
||||
// function). However, in case something breaks, we want it to still
|
||||
// provide the user with the correct outcome.
|
||||
self.items.insert(index, Item::new(item.clone_ref()).into());
|
||||
self.items.insert(index, Item::new(item).into());
|
||||
warn!("An element was inserted without a placeholder. This should not happen.");
|
||||
}
|
||||
index
|
||||
};
|
||||
self.reposition_items();
|
||||
self.item_or_placeholder_index_to_index(index)
|
||||
self.item_or_placeholder_index_to_index(actual_index)
|
||||
} else {
|
||||
warn!("Called function to insert dragged element, but no element is being dragged.");
|
||||
None
|
||||
|
@ -3,6 +3,7 @@ use ensogl_core::prelude::*;
|
||||
|
||||
use ensogl_core::data::color;
|
||||
use ensogl_core::display;
|
||||
use ensogl_core::display::world::with_context;
|
||||
use ensogl_core::Animation;
|
||||
|
||||
|
||||
@ -111,7 +112,7 @@ pub struct PlaceholderModel {
|
||||
impl PlaceholderModel {
|
||||
fn new() -> Self {
|
||||
let frp = Frp::new();
|
||||
let root = display::object::Instance::new();
|
||||
let root = display::object::Instance::new_named("Placeholder");
|
||||
let self_ref = default();
|
||||
let collapsing = default();
|
||||
let size = Animation::<f32>::new(frp.network());
|
||||
@ -124,6 +125,9 @@ impl PlaceholderModel {
|
||||
.set_border_color(color::Rgba::new(1.0, 0.0, 0.0, 1.0));
|
||||
});
|
||||
root.add_child(&viz);
|
||||
with_context(|ctx| {
|
||||
ctx.layers.above_nodes.add(&viz);
|
||||
});
|
||||
viz
|
||||
});
|
||||
Self { frp, root, self_ref, collapsing, size, _deubg_viz }
|
||||
|
@ -247,18 +247,18 @@ impl ScrollArea {
|
||||
#[profile(Detail)]
|
||||
pub fn new(app: &Application) -> ScrollArea {
|
||||
let scene = &app.display.default_scene;
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_named("ScrollArea");
|
||||
let masked_layer = layer::Masked::new();
|
||||
let display_object = display::object::InstanceWithLayer::new(display_object, masked_layer);
|
||||
|
||||
let content_layer = display_object.layer.masked_layer.create_sublayer("content_layer");
|
||||
let ui_layer = display_object.layer.masked_layer.create_sublayer("ui_layer");
|
||||
|
||||
let content = display::object::Instance::new();
|
||||
let content = display::object::Instance::new_named("content");
|
||||
display_object.add_child(&content);
|
||||
content_layer.add(&content);
|
||||
|
||||
let scrollbars = display::object::Instance::new();
|
||||
let scrollbars = display::object::Instance::new_named("scrollbars");
|
||||
display_object.add_child(&scrollbars);
|
||||
ui_layer.add(&scrollbars);
|
||||
|
||||
|
@ -734,7 +734,7 @@ impl TextModel {
|
||||
let app = app.clone_ref();
|
||||
let scene = &app.display.default_scene;
|
||||
let selection_map = default();
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_named("Text");
|
||||
let glyph_system = font::glyph::System::new(scene, font::DEFAULT_FONT_MONO);
|
||||
frp.private.output.glyph_system.emit(Some(glyph_system.clone()));
|
||||
let glyph_system = RefCell::new(glyph_system);
|
||||
|
@ -504,7 +504,7 @@ impl System {
|
||||
#[profile(Debug)]
|
||||
pub fn new_glyph(&self) -> Glyph {
|
||||
let context = self.context.clone();
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_no_debug();
|
||||
let font = self.font.clone_ref();
|
||||
let glyph_id = default();
|
||||
let line_byte_offset = default();
|
||||
|
@ -205,7 +205,7 @@ impl Camera2dData {
|
||||
let z_zoom_1 = 1.0;
|
||||
let matrix = default();
|
||||
let dirty = Dirty::new();
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_no_debug();
|
||||
let zoom_update_registry = default();
|
||||
let screen_update_registry = default();
|
||||
display_object.modify_position(|p| p.z = 1.0);
|
||||
|
@ -1119,6 +1119,25 @@ use unit2::Fraction;
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === CONSTANTS ===
|
||||
// =================
|
||||
|
||||
/// Enable debugging of display objects hierarchy. When enabled, all display objects will be
|
||||
/// represented in the DOM tree, which can be inspected using browser devtools inspector.
|
||||
pub const ENABLE_DOM_DEBUG: bool = false;
|
||||
|
||||
/// Enable DOM debugging for all display objects as a default. When enabled, all display objects
|
||||
/// created with `new` or `new_named` constructors will be represented in the debug DOM tree. When
|
||||
/// disabled, only display objects created with explicitly enabled debugging (e.g. using `new_debug`
|
||||
/// constructor) will have that behavior.
|
||||
///
|
||||
/// Has effect only when [`ENABLE_DOM_DEBUG`] is already enabled.
|
||||
pub const ENABLE_DOM_DEBUG_ALL: bool = true;
|
||||
|
||||
/// The name of a display object when not specified. Object names are visible in the DOM inspector.
|
||||
pub const DEFAULT_NAME: &str = "UnnamedDisplayObject";
|
||||
|
||||
// ==========
|
||||
// === Id ===
|
||||
// ==========
|
||||
@ -1144,7 +1163,7 @@ pub struct ChildIndex(usize);
|
||||
// =============
|
||||
|
||||
/// The main display object structure. Read the docs of [this module](self) to learn more.
|
||||
#[derive(Clone, CloneRef, Default, Deref, From)]
|
||||
#[derive(Clone, CloneRef, Deref, From)]
|
||||
#[repr(transparent)]
|
||||
pub struct Instance {
|
||||
def: InstanceDef,
|
||||
@ -1182,33 +1201,65 @@ pub struct Model {
|
||||
hierarchy: HierarchyModel,
|
||||
event: EventModel,
|
||||
layout: LayoutModel,
|
||||
debug_dom: Option<enso_web::HtmlDivElement>,
|
||||
}
|
||||
|
||||
|
||||
// === Contructors ===
|
||||
|
||||
impl Instance {
|
||||
/// Constructor.
|
||||
#[profile(Debug)]
|
||||
/// Constructor with default name. Will have DOM debugging enabled if [`ENABLE_DOM_DEBUG_ALL`]
|
||||
/// flag is enabled.
|
||||
pub fn new() -> Self {
|
||||
default()
|
||||
Self::new_named_with_debug(DEFAULT_NAME, ENABLE_DOM_DEBUG_ALL)
|
||||
}
|
||||
|
||||
/// Constructor of a named display object. The name is used for debugging purposes only.
|
||||
/// Constructor with DOM debugging always disabled.
|
||||
pub fn new_no_debug() -> Self {
|
||||
Self::new_named_with_debug(DEFAULT_NAME, false)
|
||||
}
|
||||
|
||||
/// Constructor with DOM debugging always enabled.
|
||||
pub fn new_debug() -> Self {
|
||||
Self::new_named_with_debug(DEFAULT_NAME, true)
|
||||
}
|
||||
|
||||
/// Constructor with custom name. Will have DOM debugging enabled if [`ENABLE_DOM_DEBUG_ALL`]
|
||||
/// flag is enabled.
|
||||
pub fn new_named(name: &'static str) -> Self {
|
||||
Self { def: InstanceDef::new_named(name) }
|
||||
Self::new_named_with_debug(name, ENABLE_DOM_DEBUG_ALL)
|
||||
}
|
||||
|
||||
/// Constructor with custom name and DOM debugging always disabled.
|
||||
pub fn new_named_no_debug(name: &'static str) -> Self {
|
||||
Self::new_named_with_debug(name, false)
|
||||
}
|
||||
|
||||
/// Constructor with custom name and DOM debugging always enabled.
|
||||
pub fn new_named_debug(name: &'static str) -> Self {
|
||||
Self::new_named_with_debug(name, true)
|
||||
}
|
||||
|
||||
/// Constructor with custom name and DOM debugging enabled with an argument.
|
||||
pub fn new_named_with_debug(name: &'static str, enable_debug: bool) -> Self {
|
||||
Self { def: InstanceDef::new(name, enable_debug) }
|
||||
}
|
||||
}
|
||||
|
||||
impl InstanceDef {
|
||||
/// Constructor.
|
||||
pub fn new() -> Self {
|
||||
Self { rc: Rc::new(Model::new()) }.init_events_handling()
|
||||
}
|
||||
#[profile(Debug)]
|
||||
fn new(name: &'static str, enable_debug: bool) -> Self {
|
||||
let mut model = Model::new_named(name);
|
||||
if ENABLE_DOM_DEBUG && enable_debug {
|
||||
use enso_web::prelude::*;
|
||||
if let Some(document) = enso_web::window.document() {
|
||||
let dom = document.create_div_or_panic();
|
||||
dom.set_attribute_or_warn("data-name", name);
|
||||
model.debug_dom = Some(dom);
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor.
|
||||
pub fn new_named(name: &'static str) -> Self {
|
||||
Self { rc: Rc::new(Model::new_named(name)) }.init_events_handling()
|
||||
Self { rc: Rc::new(model) }.init_events_handling().init_dom_debug(enable_debug)
|
||||
}
|
||||
|
||||
/// ID getter of this display object.
|
||||
@ -1229,14 +1280,24 @@ impl Model {
|
||||
let hierarchy = HierarchyModel::new(&network);
|
||||
let event = EventModel::new(&network);
|
||||
let layout = LayoutModel::default();
|
||||
Self { network, hierarchy, event, layout, name }
|
||||
let debug_dom = None;
|
||||
Self { network, hierarchy, event, layout, name, debug_dom }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Model {
|
||||
fn drop(&mut self) {
|
||||
if ENABLE_DOM_DEBUG {
|
||||
if let Some(dom) = self.debug_dom.take() {
|
||||
dom.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Impls ===
|
||||
|
||||
impl Default for InstanceDef {
|
||||
impl Default for Instance {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
@ -1342,6 +1403,7 @@ impl Root {
|
||||
/// Constructor of a named display object. The name is used for debugging purposes only.
|
||||
pub fn new_named(name: &'static str) -> Self {
|
||||
let def = Instance::new_named(name);
|
||||
def.set_size((0.0, 0.0));
|
||||
Self { def }.init()
|
||||
}
|
||||
|
||||
@ -2273,7 +2335,7 @@ impl Model {
|
||||
self.transformation.borrow().rotation()
|
||||
}
|
||||
|
||||
/// Transformation matrix of the object in the parent coordinate space.
|
||||
/// Transformation matrix of the object in the camera coordinate space.
|
||||
fn transformation_matrix(&self) -> Matrix4<f32> {
|
||||
self.transformation.borrow().matrix()
|
||||
}
|
||||
@ -2405,6 +2467,77 @@ impl InstanceDef {
|
||||
self
|
||||
}
|
||||
|
||||
fn init_dom_debug(self, enable_debug: bool) -> Self {
|
||||
use enso_web::prelude::*;
|
||||
use std::fmt::Write;
|
||||
|
||||
if !(ENABLE_DOM_DEBUG && enable_debug) {
|
||||
return self;
|
||||
}
|
||||
|
||||
let style_string = RefCell::new(String::new());
|
||||
let display = Rc::new(Cell::new(""));
|
||||
let last_parent_node = RefCell::new(None);
|
||||
let weak = self.downgrade();
|
||||
let network = &self.network;
|
||||
frp::extend! { network
|
||||
eval_ self.on_show (display.set(""));
|
||||
eval_ self.on_hide (display.set("display:none;"));
|
||||
eval_ self.on_transformed ([display] {
|
||||
let Some(object) = weak.upgrade() else { return };
|
||||
let Some(dom) = object.debug_dom.as_ref() else { return };
|
||||
let mut parent_node = last_parent_node.borrow_mut();
|
||||
let transform;
|
||||
let new_parent;
|
||||
|
||||
let upgrade = |l: &LayerAssignment| l.layer.upgrade();
|
||||
if let Some(layer) = object.assigned_layer.borrow().as_ref().and_then(upgrade) {
|
||||
transform = object.transformation.borrow().matrix();
|
||||
new_parent = layer.debug_dom.clone();
|
||||
} else if let Some(parent) = object.parent().and_then(|p| p.debug_dom.clone()) {
|
||||
transform = object.transformation.borrow().local_matrix();
|
||||
new_parent = Some(parent);
|
||||
} else if let Some(layer) = object.layer.borrow().as_ref().and_then(upgrade) {
|
||||
transform = object.transformation.borrow().matrix();
|
||||
new_parent = layer.debug_dom.clone();
|
||||
} else {
|
||||
transform = Matrix4::zero();
|
||||
new_parent = None;
|
||||
}
|
||||
|
||||
|
||||
if *parent_node != new_parent {
|
||||
if let Some(parent) = &new_parent {
|
||||
parent.append_child(dom).unwrap();
|
||||
} else {
|
||||
dom.remove();
|
||||
}
|
||||
*parent_node = new_parent;
|
||||
}
|
||||
|
||||
let size = object.computed_size();
|
||||
let transform = transform.as_slice();
|
||||
let mut style_string = style_string.borrow_mut();
|
||||
let x = size.x();
|
||||
let y = size.y();
|
||||
let display = display.get();
|
||||
let (first, rest) = transform.split_first().unwrap();
|
||||
write!(style_string, "\
|
||||
{display}\
|
||||
width:{x}px;\
|
||||
height:{y}px;\
|
||||
transform:matrix3d({first:.4}"
|
||||
).unwrap();
|
||||
rest.iter().for_each(|f| write!(style_string, ",{f:.4}").unwrap());
|
||||
style_string.write_str(");").unwrap();
|
||||
dom.set_attribute_or_warn("style", &*style_string);
|
||||
style_string.clear();
|
||||
});
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn emit_event_impl(
|
||||
event: &event::SomeEvent,
|
||||
parent: Option<Instance>,
|
||||
|
@ -202,6 +202,10 @@ impl CachedTransformation {
|
||||
// === Getters ===
|
||||
|
||||
impl CachedTransformation {
|
||||
pub fn local_matrix(&self) -> Matrix4<f32> {
|
||||
self.transform_matrix
|
||||
}
|
||||
|
||||
pub fn matrix(&self) -> Matrix4<f32> {
|
||||
self.matrix
|
||||
}
|
||||
|
@ -602,6 +602,7 @@ pub struct HardcodedLayers {
|
||||
pub viz: Layer,
|
||||
pub below_main: Layer,
|
||||
pub main: Layer,
|
||||
pub widget: Layer,
|
||||
pub port: Layer,
|
||||
pub port_selection: Layer,
|
||||
pub label: Layer,
|
||||
@ -645,6 +646,7 @@ impl HardcodedLayers {
|
||||
let viz = root.create_sublayer("viz");
|
||||
let below_main = root.create_sublayer("below_main");
|
||||
let main = root.create_sublayer("main");
|
||||
let widget = root.create_sublayer("widget");
|
||||
let port = root.create_sublayer("port");
|
||||
let port_selection =
|
||||
root.create_sublayer_with_camera("port_selection", &port_selection_cam);
|
||||
@ -671,6 +673,7 @@ impl HardcodedLayers {
|
||||
viz,
|
||||
below_main,
|
||||
main,
|
||||
widget,
|
||||
port,
|
||||
port_selection,
|
||||
label,
|
||||
@ -970,6 +973,7 @@ impl SceneData {
|
||||
// Updating all other cameras (the main camera was already updated, so it will be skipped).
|
||||
self.layers.iter_sublayers_and_masks_nested(|layer| {
|
||||
let dirty = layer.camera().update(scene);
|
||||
layer.update_debug_view();
|
||||
was_dirty = was_dirty || dirty;
|
||||
});
|
||||
|
||||
|
@ -172,7 +172,6 @@ impl DomScene {
|
||||
dom.set_style_or_warn("position", "absolute");
|
||||
dom.set_style_or_warn("top", "0px");
|
||||
dom.set_style_or_warn("overflow", "hidden");
|
||||
dom.set_style_or_warn("overflow", "hidden");
|
||||
dom.set_style_or_warn("width", "100%");
|
||||
dom.set_style_or_warn("height", "100%");
|
||||
// We ignore pointer events to avoid stealing them from other DomScenes.
|
||||
|
@ -434,6 +434,9 @@ pub struct LayerModel {
|
||||
scissor_box: RefCell<Option<ScissorBox>>,
|
||||
mem_mark: Rc<()>,
|
||||
pub flags: LayerFlags,
|
||||
/// When [`display::object::ENABLE_DOM_DEBUG`] is enabled all display objects on this layer
|
||||
/// will be represented by a DOM in this DOM layer.
|
||||
pub debug_dom: Option<enso_web::HtmlDivElement>,
|
||||
}
|
||||
|
||||
impl Debug for LayerModel {
|
||||
@ -451,6 +454,9 @@ impl Debug for LayerModel {
|
||||
impl Drop for LayerModel {
|
||||
fn drop(&mut self) {
|
||||
self.remove_from_parent();
|
||||
if let Some(dom) = &mut self.debug_dom {
|
||||
dom.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -461,6 +467,27 @@ impl LayerModel {
|
||||
let on_mut = on_depth_order_dirty(&parent);
|
||||
let depth_order_dirty = dirty::SharedBool::new(on_mut);
|
||||
let sublayers = Sublayers::new(&parent);
|
||||
let mut debug_dom = default();
|
||||
|
||||
if display::object::ENABLE_DOM_DEBUG {
|
||||
use enso_web::prelude::*;
|
||||
if let Some(document) = enso_web::window.document() {
|
||||
let root = document.get_html_element_by_id("debug-root").unwrap_or_else(|| {
|
||||
let root = document.create_html_element_or_panic("div");
|
||||
root.set_id("debug-root");
|
||||
root.set_style_or_warn("z-index", "100");
|
||||
document.body().unwrap().append_child(&root).unwrap();
|
||||
root
|
||||
});
|
||||
|
||||
let dom = document.create_div_or_panic();
|
||||
dom.set_class_name("debug-layer");
|
||||
dom.set_attribute_or_warn("data-layer-name", &name);
|
||||
root.append_child(&dom).unwrap();
|
||||
debug_dom = Some(dom);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
name,
|
||||
depth_order_dirty,
|
||||
@ -479,6 +506,7 @@ impl LayerModel {
|
||||
mask: default(),
|
||||
scissor_box: default(),
|
||||
mem_mark: default(),
|
||||
debug_dom,
|
||||
}
|
||||
}
|
||||
|
||||
@ -677,6 +705,41 @@ impl LayerModel {
|
||||
was_dirty
|
||||
}
|
||||
|
||||
/// Update this layer's DOM debug object with current camera transform.
|
||||
pub(crate) fn update_debug_view(&self) {
|
||||
if !display::object::ENABLE_DOM_DEBUG {
|
||||
return;
|
||||
}
|
||||
|
||||
use display::camera::camera2d::Projection;
|
||||
use enso_web::prelude::*;
|
||||
use std::fmt::Write;
|
||||
|
||||
let Some(dom) = &self.debug_dom else { return };
|
||||
let camera = self.camera.borrow();
|
||||
|
||||
let trans_cam = camera.transformation_matrix().try_inverse();
|
||||
let mut trans_cam = trans_cam.expect("Camera's matrix is not invertible.");
|
||||
let half_dim_y = camera.screen().height / 2.0;
|
||||
let fovy_slope = camera.half_fovy_slope();
|
||||
let near = half_dim_y / fovy_slope;
|
||||
|
||||
match camera.projection() {
|
||||
Projection::Perspective { .. } => {
|
||||
trans_cam.prepend_translation_mut(&Vector3(0.0, 0.0, near));
|
||||
}
|
||||
Projection::Orthographic => {}
|
||||
}
|
||||
trans_cam.append_nonuniform_scaling_mut(&Vector3(1.0, -1.0, 1.0));
|
||||
|
||||
let mut transform = String::with_capacity(100);
|
||||
let (first, rest) = trans_cam.as_slice().split_first().unwrap();
|
||||
write!(transform, "perspective({near}px) matrix3d({first:.4}").unwrap();
|
||||
rest.iter().for_each(|f| write!(transform, ",{f:.4}").unwrap());
|
||||
transform.write_str(")").unwrap();
|
||||
dom.set_style_or_warn("transform", transform);
|
||||
}
|
||||
|
||||
/// Compute a combined [`DependencyGraph`] for the layer taking into consideration the global
|
||||
/// dependency graph (from root [`Layer`]), the local one (per layer), and individual shape
|
||||
/// preferences (see the "Compile Time Shapes Ordering Relations" section in docs of [`Layer`]
|
||||
|
@ -137,6 +137,11 @@ pub trait Shape: 'static + Sized + AsRef<Self::InstanceParams> {
|
||||
fn flavor(_data: &Self::ShapeData) -> ShapeSystemFlavor {
|
||||
ShapeSystemFlavor { flavor: 0 }
|
||||
}
|
||||
fn enable_dom_debug() -> bool {
|
||||
// By default, only shapes with alignment compatible with layout are enabled for DOM debug.
|
||||
// Otherwise, due to different shape offsets, the debug view is very confusing.
|
||||
Self::default_alignment() == alignment::Dim2::left_bottom()
|
||||
}
|
||||
}
|
||||
|
||||
/// An alias for [`Shape]` where [`Shape::ShapeData`] is [`Default`].
|
||||
@ -323,7 +328,9 @@ impl<S: Shape> ShapeSystem<S> {
|
||||
let instance_id = sprite.instance_id;
|
||||
let global_id = sprite.global_instance_id;
|
||||
let shape = S::new_instance_params(&self.gpu_params, instance_id);
|
||||
let display_object = display::object::Instance::new_named("ShapeSystem");
|
||||
let debug = S::enable_dom_debug();
|
||||
let display_object =
|
||||
display::object::Instance::new_named_with_debug(type_name::<S>(), debug);
|
||||
display_object.add_child(&sprite);
|
||||
// FIXME: workaround:
|
||||
// display_object.use_auto_layout();
|
||||
|
@ -564,7 +564,7 @@ impl SymbolData {
|
||||
let bindings = default();
|
||||
let stats = SymbolStats::new(stats);
|
||||
let context = default();
|
||||
let display_object = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_no_debug();
|
||||
let is_hidden = Rc::new(Cell::new(false));
|
||||
|
||||
let instance_scope = surface.instance_scope();
|
||||
|
@ -126,7 +126,7 @@ pub struct SizedObject {
|
||||
impl SizedObject {
|
||||
fn new(attr: Attribute<Vector2<f32>>, transform: &Attribute<Matrix4<f32>>) -> Self {
|
||||
let size = Size::new(attr);
|
||||
let display_object = display::object::Instance::new_named("Sprite");
|
||||
let display_object = display::object::Instance::new_named_no_debug("Sprite");
|
||||
let weak_display_object = display_object.downgrade();
|
||||
let network = &display_object.network;
|
||||
frp::extend! { network
|
||||
|
@ -152,7 +152,7 @@ impl<S: Shape> Drop for ShapeViewModel<S> {
|
||||
|
||||
impl<S: Shape> ShapeViewModel<S> {
|
||||
/// Constructor.
|
||||
pub fn new_with_data(data: S::ShapeData) -> Self {
|
||||
fn new_with_data(data: S::ShapeData) -> Self {
|
||||
let (shape, _) = world::with_context(|t| t.layers.DETACHED.instantiate(&data, default()));
|
||||
let events_deprecated = PointerTarget_DEPRECATED::new();
|
||||
let pointer_targets = default();
|
||||
|
@ -210,8 +210,9 @@ crate::define_endpoints_2! {
|
||||
scene_position (Vector3),
|
||||
/// Change between the current and the previous scene position.
|
||||
scene_position_delta (Vector3),
|
||||
start_drag (),
|
||||
start_drag(),
|
||||
stop_drag(),
|
||||
is_dragging(bool),
|
||||
}
|
||||
}
|
||||
|
||||
@ -239,15 +240,15 @@ impl CursorModel {
|
||||
/// Constructor.
|
||||
pub fn new(scene: &Scene, frp: WeakFrp) -> Self {
|
||||
let scene = scene.clone_ref();
|
||||
let display_object = display::object::Instance::new();
|
||||
let dragged_elem = display::object::Instance::new();
|
||||
let display_object = display::object::Instance::new_no_debug();
|
||||
let dragged_elem = display::object::Instance::new_named("dragged_elem");
|
||||
let view = shape::View::new();
|
||||
let port_selection = shape::View::new();
|
||||
let style = default();
|
||||
|
||||
display_object.add_child(&view);
|
||||
display_object.add_child(&port_selection);
|
||||
view.add_child(&dragged_elem);
|
||||
scene.add_child(&dragged_elem);
|
||||
let tgt_layer = &scene.layers.cursor;
|
||||
let port_selection_layer = &scene.layers.port_selection;
|
||||
tgt_layer.add(&view);
|
||||
@ -563,7 +564,7 @@ impl Cursor {
|
||||
eval position ((t) model.display_object.set_position(*t));
|
||||
eval front_color ((t) model.view.color.set(t.into()));
|
||||
eval port_selection_color ((t) model.port_selection.color.set(t.into()));
|
||||
|
||||
eval_ screen_position (model.update_drag_position());
|
||||
|
||||
// === Outputs ===
|
||||
|
||||
@ -571,6 +572,7 @@ impl Cursor {
|
||||
frp.private.output.screen_position <+ screen_position;
|
||||
frp.private.output.scene_position <+ scene_position;
|
||||
frp.private.output.scene_position_delta <+ scene_position_delta;
|
||||
frp.private.output.is_dragging <+ bool(&frp.stop_drag, &frp.start_drag);
|
||||
}
|
||||
|
||||
// Hide on init.
|
||||
@ -592,21 +594,34 @@ impl CursorModel {
|
||||
warn!("Can't start dragging an item because another item is already being dragged.");
|
||||
} else {
|
||||
let object = target.display_object().clone();
|
||||
self.dragged_elem.add_child(&object);
|
||||
let target_position = object.global_position().xy();
|
||||
let cursor_position = self.frp.scene_position.value().xy();
|
||||
object.set_xy(target_position - cursor_position);
|
||||
|
||||
if let Some(object_layer) = object.display_layer() {
|
||||
object_layer.add(&self.dragged_elem);
|
||||
}
|
||||
let scene = scene();
|
||||
let camera = scene.camera();
|
||||
let zoom = camera.zoom();
|
||||
self.dragged_elem.set_scale_xy((zoom, zoom));
|
||||
let screen_pos = self.frp.screen_position.value().xy();
|
||||
let offset = scene.screen_to_object_space(&object, screen_pos);
|
||||
object.set_xy(-offset);
|
||||
self.dragged_elem.add_child(&object);
|
||||
|
||||
*self.dragged_item.borrow_mut() = Some((Box::new(target), object));
|
||||
|
||||
self.frp.private.output.start_drag.emit(());
|
||||
}
|
||||
}
|
||||
|
||||
fn update_drag_position(&self) {
|
||||
if self.dragged_item.borrow().is_some() {
|
||||
let scene = scene();
|
||||
let layer = self.dragged_elem.display_layer();
|
||||
let camera = layer.map_or(scene.camera(), |l| l.camera());
|
||||
|
||||
let screen_pos = self.frp.screen_position.value().xy() / camera.zoom();
|
||||
let pos_in_layer = camera.inversed_view_matrix() * screen_pos.push(0.0).push(1.0);
|
||||
self.dragged_elem.set_xy(pos_in_layer.xy());
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the dragged item and return it as [`Any`]. If you want to retrieve the item if it is
|
||||
/// of a particular type, use the [`Self::stop_drag_if_is`] method instead.
|
||||
pub fn stop_drag(&self) -> Option<Box<dyn Any>> {
|
||||
|
@ -537,6 +537,7 @@ mock_data! { Document => EventTarget
|
||||
|
||||
// === Window ===
|
||||
mock_data! { Window => EventTarget
|
||||
fn document(&self) -> Option<Document>;
|
||||
fn open_with_url_and_target(&self, url: &str, target: &str)
|
||||
-> Result<Option<Window>, JsValue>;
|
||||
fn request_animation_frame(&self, callback: &Function) -> Result<i32, JsValue>;
|
||||
@ -687,6 +688,11 @@ impl From<HtmlDivElement> for EventTarget {
|
||||
default()
|
||||
}
|
||||
}
|
||||
impl PartialEq<HtmlDivElement> for HtmlDivElement {
|
||||
fn eq(&self, _: &HtmlDivElement) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === HtmlDivElement ===
|
||||
|
@ -664,10 +664,8 @@ ops! { HtmlElementOps for HtmlElement
|
||||
fn set_style_or_warn(&self, name: impl AsRef<str>, value: impl AsRef<str>) {
|
||||
let name = name.as_ref();
|
||||
let value = value.as_ref();
|
||||
let values = format!("\"{name}\" = \"{value}\" on \"{self:?}\"");
|
||||
let warn_msg: &str = &format!("Failed to set style {values}");
|
||||
if self.style().set_property(name, value).is_err() {
|
||||
warn!("{warn_msg}");
|
||||
warn!("Failed to set style \"{name}\" = \"{value}\" on \"{self:?}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user