mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 21:41:34 +03:00
Basic dropdown widget integration (#4013)
Implements https://www.pivotaltracker.com/n/projects/2539304/stories/184023445 Added a dropdown widget to graph node for all span tree nodes that have tag values present. When an option is selected, the controller receives a partial expression update, which targets specific crumbs of the expression (similar to how edge endpoint updates work). https://user-images.githubusercontent.com/919491/210219931-8ae418fd-3ac4-44a5-abea-9e670f15cdf9.mp4 # Important Notes Right now the dropdown widget is recreated every time the node is edited, including a dropdown option being selected. This causes it to close every time. I wanted to get around that by diffing span trees, but I wasn't able to do it in useful way. Additionally, current implementation of node input expression view heavily relies on being reinitialized from scratch every time. This led to more necessary changes than I was comfortable with for this task. I believe it will be easier to implement it as part of more complete widget support, especially after dynamic data support, as we will have proper widget type information.
This commit is contained in:
parent
654a8351c8
commit
fe1cf9a9ce
@ -79,6 +79,8 @@
|
||||
the project is changed from the last saved snapshot and lighter when the
|
||||
snapshot matches the current project state.
|
||||
- [Added shortcut to interrupt the program][3967]
|
||||
- [Added suggestion dropdown for function arguments][4013]. The dropdown is
|
||||
present only when the argument is of type that has a predefined set of values.
|
||||
|
||||
#### EnsoGL (rendering engine)
|
||||
|
||||
@ -420,6 +422,7 @@
|
||||
[3967]: https://github.com/enso-org/enso/pull/3967
|
||||
[3987]: https://github.com/enso-org/enso/pull/3987
|
||||
[3997]: https://github.com/enso-org/enso/pull/3997
|
||||
[4013]: https://github.com/enso-org/enso/pull/4013
|
||||
|
||||
#### Enso Compiler
|
||||
|
||||
|
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -5287,9 +5287,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.5.0"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f400b0f7905bf702f9f3dc3df5a121b16c54e9e8012c082905fdf09a931861a"
|
||||
checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
"ucd-trie",
|
||||
@ -5297,9 +5297,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.5.0"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "423c2ba011d6e27b02b482a3707c773d19aec65cc024637aec44e19652e66f63"
|
||||
checksum = "cdc078600d06ff90d4ed238f0119d84ab5d43dbaad278b0e33a8820293b32344"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
@ -5307,9 +5307,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.5.0"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e64e6c2c85031c02fdbd9e5c72845445ca0a724d419aa0bc068ac620c9935c1"
|
||||
checksum = "28a1af60b1c4148bb269006a750cff8e2ea36aff34d2d96cf7be0b14d1bed23c"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
@ -5320,9 +5320,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.5.0"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57959b91f0a133f89a68be874a5c88ed689c19cd729ecdb5d762ebf16c64d662"
|
||||
checksum = "fec8605d59fc2ae0c6c1aefc0c7c7a9769732017c0ce07f7a9cfffa7b4404f20"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"pest",
|
||||
|
@ -342,7 +342,7 @@ impl MainLine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutable AST of the node's expression. Maintains ID.
|
||||
/// Set AST of the node's expression. Maintains ID.
|
||||
pub fn set_expression(&mut self, expression: Ast) {
|
||||
self.modify_expression(move |ast| {
|
||||
*ast = preserving_skip_and_freeze(ast, |ast| *ast = expression.clone());
|
||||
@ -420,7 +420,7 @@ impl MainLine {
|
||||
}
|
||||
|
||||
/// Modify expression, preserving the AST id.
|
||||
fn modify_expression(&mut self, f: impl Fn(&mut Ast)) {
|
||||
fn modify_expression(&mut self, f: impl FnOnce(&mut Ast)) {
|
||||
let id = self.id();
|
||||
match self {
|
||||
Self::Binding { infix, .. } => {
|
||||
|
@ -900,6 +900,9 @@ pub struct SuggestionEntryArgument {
|
||||
pub has_default: bool,
|
||||
/// Optional default value.
|
||||
pub default_value: Option<String>,
|
||||
/// Optional list of possible values that this argument takes.
|
||||
#[serde(default, deserialize_with = "enso_prelude::deserialize_null_as_default")]
|
||||
pub tag_values: Vec<String>,
|
||||
}
|
||||
|
||||
impl SuggestionEntryArgument {
|
||||
@ -911,6 +914,7 @@ impl SuggestionEntryArgument {
|
||||
is_suspended: false,
|
||||
has_default: false,
|
||||
default_value: None,
|
||||
tag_values: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1479,14 +1479,14 @@ pub trait TraversableAst: Sized {
|
||||
|
||||
impl TraversableAst for Ast {
|
||||
fn set_traversing(&self, crumbs: &[Crumb], new_ast: Ast) -> FallibleResult<Self> {
|
||||
let updated_ast = if let Some(first_crumb) = crumbs.first() {
|
||||
match crumbs {
|
||||
[] => Ok(new_ast),
|
||||
[first_crumb, tail_crumbs @ ..] => {
|
||||
let child = self.get(first_crumb)?;
|
||||
let updated_child = child.set_traversing(&crumbs[1..], new_ast)?;
|
||||
self.set(first_crumb, updated_child)?
|
||||
} else {
|
||||
new_ast
|
||||
};
|
||||
Ok(updated_ast)
|
||||
let updated_child = child.set_traversing(tail_crumbs, new_ast)?;
|
||||
self.set(first_crumb, updated_child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_traversing(&self, crumbs: &[Crumb]) -> FallibleResult<&Ast> {
|
||||
|
@ -89,23 +89,23 @@ pub fn without_macros(ast: &Ast) -> Ast {
|
||||
|
||||
/// Execute [`f`], preserving the usage of the [`SKIP`] macro. [`f`] receives AST without [`SKIP`],
|
||||
/// and the macro would be preserved in the final result if it existed. Preserves the id of the AST.
|
||||
pub fn preserving_skip(ast: &mut Ast, f: impl Fn(&mut Ast)) -> Ast {
|
||||
pub fn preserving_skip(ast: &mut Ast, f: impl FnOnce(&mut Ast)) -> Ast {
|
||||
preserving_macro(ast, f, SKIP_MACRO_IDENTIFIER, |info| info.skip)
|
||||
}
|
||||
|
||||
/// Execute [`f`], preserving the usage of the [`SKIP`] macro. [`f`] receives AST without [`SKIP`],
|
||||
/// and the macro would be preserved in the final result if it existed. Preserves the id of the AST.
|
||||
pub fn preserving_freeze(ast: &mut Ast, f: impl Fn(&mut Ast)) -> Ast {
|
||||
pub fn preserving_freeze(ast: &mut Ast, f: impl FnOnce(&mut Ast)) -> Ast {
|
||||
preserving_macro(ast, f, FREEZE_MACRO_IDENTIFIER, |info| info.freeze)
|
||||
}
|
||||
|
||||
/// A combination oof [`preserving_skip`] and [`preserving_freeze`]. Preserves both macros.
|
||||
pub fn preserving_skip_and_freeze(ast: &mut Ast, f: impl Fn(&mut Ast)) -> Ast {
|
||||
pub fn preserving_skip_and_freeze(ast: &mut Ast, f: impl FnOnce(&mut Ast)) -> Ast {
|
||||
let skip = SKIP_MACRO_IDENTIFIER;
|
||||
let freeze = FREEZE_MACRO_IDENTIFIER;
|
||||
let is_skipped = |info: &MacrosInfo| info.skip;
|
||||
let is_frozen = |info: &MacrosInfo| info.freeze;
|
||||
let preserve_freeze = |ast: &mut Ast| *ast = preserving_macro(ast, &f, freeze, is_frozen);
|
||||
let preserve_freeze = move |ast: &mut Ast| *ast = preserving_macro(ast, f, freeze, is_frozen);
|
||||
preserving_macro(ast, preserve_freeze, skip, is_skipped)
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ pub fn preserving_skip_and_freeze(ast: &mut Ast, f: impl Fn(&mut Ast)) -> Ast {
|
||||
/// existed. Preserves the id of the AST.
|
||||
fn preserving_macro(
|
||||
ast: &mut Ast,
|
||||
f: impl Fn(&mut Ast),
|
||||
f: impl FnOnce(&mut Ast),
|
||||
macro_name: &str,
|
||||
does_contain_macro: impl Fn(&MacrosInfo) -> bool,
|
||||
) -> Ast {
|
||||
|
@ -666,7 +666,7 @@ mod test {
|
||||
id_map.generate(14..15);
|
||||
id_map.generate(4..11);
|
||||
let ast = parser.parse_line_ast_with_id_map("2 + foo bar - 3", id_map.clone()).unwrap();
|
||||
let mut tree = ast.generate_tree(&context::Empty).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
|
||||
|
||||
// Check the expression ids we defined:
|
||||
for id_map_entry in id_map.vec {
|
||||
@ -706,7 +706,7 @@ mod test {
|
||||
fn generate_span_tree_with_chains() {
|
||||
let parser = Parser::new_or_panic();
|
||||
let ast = parser.parse_line_ast("2 + 3 + foo bar baz 13 + 5").unwrap();
|
||||
let mut tree = ast.generate_tree(&context::Empty).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
|
||||
clear_expression_ids(&mut tree.root);
|
||||
|
||||
let expected = TreeBuilder::new(26)
|
||||
@ -748,7 +748,7 @@ mod test {
|
||||
fn generating_span_tree_from_right_assoc_operator() {
|
||||
let parser = Parser::new_or_panic();
|
||||
let ast = parser.parse_line_ast("1,2,3").unwrap();
|
||||
let mut tree = ast.generate_tree(&context::Empty).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
|
||||
clear_expression_ids(&mut tree.root);
|
||||
|
||||
let expected = TreeBuilder::new(5)
|
||||
@ -774,7 +774,7 @@ mod test {
|
||||
// The star makes `SectionSides` ast being one of the parameters of + chain. First + makes
|
||||
// SectionRight, and last + makes SectionLeft.
|
||||
let ast = parser.parse_line_ast("+ * + + 2 +").unwrap();
|
||||
let mut tree = ast.generate_tree(&context::Empty).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
|
||||
clear_expression_ids(&mut tree.root);
|
||||
|
||||
let expected = TreeBuilder::new(11)
|
||||
@ -808,7 +808,7 @@ mod test {
|
||||
fn generating_span_tree_from_right_assoc_section() {
|
||||
let parser = Parser::new_or_panic();
|
||||
let ast = parser.parse_line_ast(",2,").unwrap();
|
||||
let mut tree = ast.generate_tree(&context::Empty).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
|
||||
clear_expression_ids(&mut tree.root);
|
||||
|
||||
let expected = TreeBuilder::new(3)
|
||||
@ -834,7 +834,7 @@ mod test {
|
||||
id_map.generate(0..29);
|
||||
let expression = "if foo then (a + b) x else ()";
|
||||
let ast = parser.parse_line_ast_with_id_map(expression, id_map.clone()).unwrap();
|
||||
let mut tree = ast.generate_tree(&context::Empty).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
|
||||
|
||||
// Check if expression id is set
|
||||
let (_, expected_id) = id_map.vec.first().unwrap();
|
||||
@ -884,7 +884,7 @@ mod test {
|
||||
let parser = Parser::new_or_panic();
|
||||
let expression = "[a,b]";
|
||||
let ast = parser.parse_line_ast(expression).unwrap();
|
||||
let mut tree = ast.generate_tree(&context::Empty).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
|
||||
|
||||
// Check the other fields
|
||||
clear_expression_ids(&mut tree.root);
|
||||
@ -912,7 +912,7 @@ mod test {
|
||||
let mut id_map = IdMap::default();
|
||||
id_map.generate(0..2);
|
||||
let ast = parser.parse_line_ast_with_id_map("(4", id_map.clone()).unwrap();
|
||||
let mut tree = ast.generate_tree(&context::Empty).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
|
||||
|
||||
// Check the expression id:
|
||||
let (_, expected_id) = id_map.vec.first().unwrap();
|
||||
@ -934,7 +934,7 @@ mod test {
|
||||
fn generating_span_tree_for_lambda() {
|
||||
let parser = Parser::new_or_panic();
|
||||
let ast = parser.parse_line_ast("foo a-> b + c").unwrap();
|
||||
let mut tree = ast.generate_tree(&context::Empty).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap();
|
||||
clear_expression_ids(&mut tree.root);
|
||||
|
||||
let expected = TreeBuilder::new(13)
|
||||
@ -951,10 +951,13 @@ mod test {
|
||||
fn generating_span_tree_for_unfinished_call() {
|
||||
let parser = Parser::new_or_panic();
|
||||
let this_param =
|
||||
ArgumentInfo { name: Some("self".to_owned()), tp: Some("Any".to_owned()) };
|
||||
let param1 =
|
||||
ArgumentInfo { name: Some("arg1".to_owned()), tp: Some("Number".to_owned()) };
|
||||
let param2 = ArgumentInfo { name: Some("arg2".to_owned()), tp: None };
|
||||
ArgumentInfo { name: Some("self".to_owned()), tp: Some("Any".to_owned()), ..default() };
|
||||
let param1 = ArgumentInfo {
|
||||
name: Some("arg1".to_owned()),
|
||||
tp: Some("Number".to_owned()),
|
||||
..default()
|
||||
};
|
||||
let param2 = ArgumentInfo { name: Some("arg2".to_owned()), tp: None, ..default() };
|
||||
|
||||
|
||||
// === Single function name ===
|
||||
@ -962,7 +965,7 @@ mod test {
|
||||
let ast = parser.parse_line_ast("foo").unwrap();
|
||||
let invocation_info = CalledMethodInfo { parameters: vec![this_param.clone()] };
|
||||
let ctx = MockContext::new_single(ast.id.unwrap(), invocation_info);
|
||||
let mut tree = SpanTree::new(&ast, &ctx).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = SpanTree::new(&ast, &ctx).unwrap();
|
||||
match tree.root_ref().leaf_iter().collect_vec().as_slice() {
|
||||
[_func, arg0] => assert_eq!(arg0.argument_info().as_ref(), Some(&this_param)),
|
||||
sth_else => panic!("There should be 2 leaves, found: {}", sth_else.len()),
|
||||
@ -981,7 +984,7 @@ mod test {
|
||||
let ast = parser.parse_line_ast("foo here").unwrap();
|
||||
let invocation_info = CalledMethodInfo { parameters: vec![this_param.clone()] };
|
||||
let ctx = MockContext::new_single(ast.id.unwrap(), invocation_info);
|
||||
let mut tree = SpanTree::new(&ast, &ctx).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = SpanTree::new(&ast, &ctx).unwrap();
|
||||
match tree.root_ref().leaf_iter().collect_vec().as_slice() {
|
||||
[_func, arg0] => assert_eq!(arg0.argument_info().as_ref(), Some(&this_param)),
|
||||
sth_else => panic!("There should be 2 leaves, found: {}", sth_else.len()),
|
||||
@ -1002,7 +1005,7 @@ mod test {
|
||||
parameters: vec![this_param.clone(), param1.clone(), param2.clone()],
|
||||
};
|
||||
let ctx = MockContext::new_single(ast.id.unwrap(), invocation_info);
|
||||
let mut tree = SpanTree::new(&ast, &ctx).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = SpanTree::new(&ast, &ctx).unwrap();
|
||||
match tree.root_ref().leaf_iter().collect_vec().as_slice() {
|
||||
[_func, arg0, arg1, arg2] => {
|
||||
assert_eq!(arg0.argument_info().as_ref(), Some(&this_param));
|
||||
@ -1032,7 +1035,7 @@ mod test {
|
||||
let invocation_info =
|
||||
CalledMethodInfo { parameters: vec![this_param, param1.clone(), param2.clone()] };
|
||||
let ctx = MockContext::new_single(ast.id.unwrap(), invocation_info);
|
||||
let mut tree = SpanTree::new(&ast, &ctx).unwrap(): SpanTree;
|
||||
let mut tree: SpanTree = SpanTree::new(&ast, &ctx).unwrap();
|
||||
match tree.root_ref().leaf_iter().collect_vec().as_slice() {
|
||||
[_, _this, _, _, _func, _, arg1, arg2] => {
|
||||
assert_eq!(arg1.argument_info().as_ref(), Some(¶m1));
|
||||
|
@ -77,18 +77,19 @@ use crate::generate::Context;
|
||||
pub struct ArgumentInfo {
|
||||
pub name: Option<String>,
|
||||
pub tp: Option<String>,
|
||||
pub tag_values: Vec<String>,
|
||||
}
|
||||
|
||||
impl ArgumentInfo {
|
||||
/// Constructor.
|
||||
pub fn new(name: Option<String>, tp: Option<String>) -> Self {
|
||||
Self { name, tp }
|
||||
pub fn new(name: Option<String>, tp: Option<String>, tag_values: Vec<String>) -> Self {
|
||||
Self { name, tp, tag_values }
|
||||
}
|
||||
|
||||
/// Specialized constructor for "this" argument.
|
||||
pub fn this(tp: Option<String>) -> Self {
|
||||
let name = Some(node::This::NAME.into());
|
||||
Self { name, tp }
|
||||
Self { name, tp, tag_values: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,12 +126,14 @@ impl Kind {
|
||||
/// `ArgumentInfo` getter. Returns `None` if the node could not be attached with the
|
||||
/// information.
|
||||
pub fn argument_info(&self) -> Option<ArgumentInfo> {
|
||||
match self {
|
||||
Self::This(t) => Some(ArgumentInfo::new(Some(t.name().into()), t.tp.clone())),
|
||||
Self::Argument(t) => Some(ArgumentInfo::new(t.name.clone(), t.tp.clone())),
|
||||
Self::InsertionPoint(t) => Some(ArgumentInfo::new(t.name.clone(), t.tp.clone())),
|
||||
_ => None,
|
||||
}
|
||||
Some(match self {
|
||||
Self::This(t) => ArgumentInfo::this(t.tp.clone()),
|
||||
Self::Argument(t) =>
|
||||
ArgumentInfo::new(t.name.clone(), t.tp.clone(), t.tag_values.clone()),
|
||||
Self::InsertionPoint(t) =>
|
||||
ArgumentInfo::new(t.name.clone(), t.tp.clone(), t.tag_values.clone()),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// `ArgumentInfo` setter. Returns bool indicating whether the operation was possible
|
||||
@ -145,11 +147,13 @@ impl Kind {
|
||||
Self::Argument(t) => {
|
||||
t.name = argument_info.name;
|
||||
t.tp = argument_info.tp;
|
||||
t.tag_values = argument_info.tag_values;
|
||||
true
|
||||
}
|
||||
Self::InsertionPoint(t) => {
|
||||
t.name = argument_info.name;
|
||||
t.tp = argument_info.tp;
|
||||
t.tag_values = argument_info.tag_values;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
@ -250,6 +254,7 @@ pub struct Argument {
|
||||
pub removable: bool,
|
||||
pub name: Option<String>,
|
||||
pub tp: Option<String>,
|
||||
pub tag_values: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
@ -305,6 +310,7 @@ pub struct InsertionPoint {
|
||||
pub kind: InsertionPointType,
|
||||
pub name: Option<String>,
|
||||
pub tp: Option<String>,
|
||||
pub tag_values: Vec<String>,
|
||||
}
|
||||
|
||||
// === Constructors ===
|
||||
|
@ -870,6 +870,34 @@ impl Handle {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the given node's expression by rewriting a part of it, as specified by span crumbs.
|
||||
///
|
||||
/// This will not modify AST IDs of any part of the expression that is not selected by the
|
||||
/// crumbs.
|
||||
#[profile(Debug)]
|
||||
pub fn set_expression_span(
|
||||
&self,
|
||||
id: ast::Id,
|
||||
crumbs: &span_tree::Crumbs,
|
||||
expression_text: impl Str,
|
||||
context: &impl SpanTreeContext,
|
||||
) -> FallibleResult {
|
||||
let node_ast = self.node_info(id)?.expression();
|
||||
let node_span_tree: SpanTree = SpanTree::new(&node_ast, context)?;
|
||||
let port = node_span_tree.get_node(crumbs)?;
|
||||
let new_node_ast = if expression_text.as_ref().is_empty() {
|
||||
if port.is_action_available(Action::Erase) {
|
||||
port.erase(&node_ast)?
|
||||
} else {
|
||||
port.set(&node_ast, Ast::blank())?
|
||||
}
|
||||
} else {
|
||||
let new_expression_ast = self.parse_node_expression(expression_text)?;
|
||||
port.set(&node_ast, new_expression_ast)?
|
||||
};
|
||||
self.set_expression_ast(id, new_node_ast)
|
||||
}
|
||||
|
||||
/// Set node's position.
|
||||
pub fn set_node_position(
|
||||
&self,
|
||||
|
@ -245,6 +245,7 @@ pub fn register_views(app: &Application) {
|
||||
app.views.register::<ensogl_component::text::Text>();
|
||||
app.views.register::<ensogl_component::selector::NumberPicker>();
|
||||
app.views.register::<ensogl_component::selector::NumberRangePicker>();
|
||||
app.views.register::<ensogl_component::drop_down::Dropdown<ImString>>();
|
||||
|
||||
// As long as .label() of a View is the same, shortcuts and commands are currently also
|
||||
// expected to be the same, so it should not be important which concrete type parameter of
|
||||
|
@ -861,6 +861,7 @@ pub fn to_span_tree_param(param_info: &Argument) -> span_tree::ArgumentInfo {
|
||||
// TODO [mwu] Check if database actually do must always have both of these filled.
|
||||
name: Some(param_info.name.clone()),
|
||||
tp: Some(param_info.repr_type.clone()),
|
||||
tag_values: param_info.tag_values.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1155,6 +1156,7 @@ mod test {
|
||||
is_suspended: false,
|
||||
has_default: false,
|
||||
default_value: None,
|
||||
tag_values: Vec::new(),
|
||||
};
|
||||
let entry = Entry::new_method(defined_in, on_type, "entry", return_type, true)
|
||||
.with_arguments(vec![argument])
|
||||
@ -1246,6 +1248,7 @@ mod test {
|
||||
is_suspended: false,
|
||||
has_default: false,
|
||||
default_value: None,
|
||||
tag_values: Vec::new(),
|
||||
};
|
||||
let add_argument =
|
||||
SuggestionArgumentUpdate::Add { index: 1, argument: new_argument.clone() };
|
||||
|
@ -182,6 +182,7 @@ macro_rules! mock_suggestion_database_entry_argument {
|
||||
is_suspended: false,
|
||||
has_default: false,
|
||||
default_value: None,
|
||||
tag_values: Vec::new(),
|
||||
}
|
||||
};
|
||||
($name:ident: $($path:ident).*) => {
|
||||
@ -191,6 +192,7 @@ macro_rules! mock_suggestion_database_entry_argument {
|
||||
is_suspended: false,
|
||||
has_default: false,
|
||||
default_value: None,
|
||||
tag_values: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,6 @@ pub type ViewConnection = view::graph_editor::EdgeId;
|
||||
pub type AstConnection = controller::graph::Connection;
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
@ -147,6 +146,26 @@ impl Model {
|
||||
self.state.update_from_view().set_node_expression(id, expression);
|
||||
}
|
||||
|
||||
/// Update a part of node expression under specific span tree crumbs. Preserves identity of
|
||||
/// unaffected parts of the expression.
|
||||
fn node_expression_span_set(
|
||||
&self,
|
||||
id: ViewNodeId,
|
||||
crumbs: &span_tree::Crumbs,
|
||||
expression: ImString,
|
||||
) {
|
||||
self.log_action(
|
||||
|| {
|
||||
let expression = expression.as_str();
|
||||
let update = self.state.update_from_view();
|
||||
let ast_id = update.check_node_expression_span_update(id, crumbs, expression)?;
|
||||
let graph = self.controller.graph();
|
||||
Some(graph.set_expression_span(ast_id, crumbs, expression, &self.controller))
|
||||
},
|
||||
"update expression input span",
|
||||
);
|
||||
}
|
||||
|
||||
/// The user skipped the node by pressing on the "skip" button next to it.
|
||||
fn node_action_skip(&self, id: ViewNodeId, enabled: bool) {
|
||||
self.log_action(
|
||||
@ -620,6 +639,7 @@ impl Graph {
|
||||
eval view.nodes_collapsed(((nodes, _)) model.nodes_collapsed(nodes));
|
||||
eval view.enabled_visualization_path(((node_id, path)) model.node_visualization_changed(*node_id, path.clone()));
|
||||
eval view.node_expression_set(((node_id, expression)) model.node_expression_set(*node_id, expression.clone_ref()));
|
||||
eval view.node_expression_span_set(((node_id, crumbs, expression)) model.node_expression_span_set(*node_id, crumbs, expression.clone_ref()));
|
||||
eval view.node_action_skip(((node_id, enabled)) model.node_action_skip(*node_id, *enabled));
|
||||
eval view.node_action_freeze(((node_id, enabled)) model.node_action_freeze(*node_id, *enabled));
|
||||
|
||||
|
@ -16,7 +16,6 @@ use ide_view::graph_editor::component::visualization as visualization_view;
|
||||
use ide_view::graph_editor::EdgeEndpoint;
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Nodes ===
|
||||
// =============
|
||||
@ -742,6 +741,28 @@ impl<'a> ViewChange<'a> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if an expression span change is valid and has any effect. Returns node AST id.
|
||||
/// Returns `None` if no changes to the expression are needed or when the span doesn't exist.
|
||||
pub fn check_node_expression_span_update(
|
||||
&self,
|
||||
id: ViewNodeId,
|
||||
crumbs: &span_tree::Crumbs,
|
||||
new_span_expression: &str,
|
||||
) -> Option<AstNodeId> {
|
||||
let mut nodes = self.nodes.borrow_mut();
|
||||
let ast_id = nodes.ast_id_of_view(id)?;
|
||||
let displayed = nodes.get_mut(ast_id)?;
|
||||
let code = displayed.expression.code.as_str();
|
||||
|
||||
let port_ref = displayed.expression.input_span_tree.get_node(crumbs).ok()?;
|
||||
let span = port_ref.span();
|
||||
let span_as_range = enso_text::Range::new(span.start, span.end);
|
||||
let span_expression = &code[span_as_range];
|
||||
debug!("Checking expression span update: {} -> {}", span_expression, new_span_expression);
|
||||
let expression_has_changed = span_expression != new_span_expression;
|
||||
expression_has_changed.then_some(ast_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -74,6 +74,7 @@ pub mod mock {
|
||||
is_suspended: false,
|
||||
has_default: false,
|
||||
default_value: None,
|
||||
tag_values: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,6 +85,7 @@ pub mod mock {
|
||||
is_suspended: false,
|
||||
has_default: false,
|
||||
default_value: None,
|
||||
tag_values: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,6 +96,7 @@ pub mod mock {
|
||||
is_suspended: false,
|
||||
has_default: false,
|
||||
default_value: None,
|
||||
tag_values: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,12 +132,7 @@ impl Layout {
|
||||
) -> Self {
|
||||
let local_scope_rows = local_scope_entry_count.div_ceil(COLUMN_COUNT);
|
||||
let filtered_groups = groups.map(|mut g| {
|
||||
g.drain_filter(|g| {
|
||||
if g.original_height == 2 {
|
||||
warn!("GRPPP: {g:#?}");
|
||||
}
|
||||
g.height == 0
|
||||
});
|
||||
g.drain_filter(|g| g.height == 0);
|
||||
g
|
||||
});
|
||||
let col_heights: [usize; COLUMN_COUNT] = filtered_groups
|
||||
@ -259,9 +254,6 @@ impl Layout {
|
||||
|
||||
/// Add group to the top of given column.
|
||||
pub fn push_group(&mut self, column: Col, group: Group) -> Row {
|
||||
if group.original_height == 2 {
|
||||
warn!("PUSH GROUP {group:#?}");
|
||||
}
|
||||
let group_column = &mut self.columns[column];
|
||||
let prev_header_row = group_column.top_row;
|
||||
let next_header_row = group_column.top_row - group.height - HEADER_HEIGHT_IN_ROWS;
|
||||
|
@ -130,10 +130,12 @@ fn init(app: &Application) {
|
||||
let node1_id = graph_editor.model.add_node();
|
||||
let node2_id = graph_editor.model.add_node();
|
||||
let node3_id = graph_editor.model.add_node();
|
||||
let node4_id = graph_editor.model.add_node();
|
||||
|
||||
graph_editor.frp.set_node_position.emit((node1_id, Vector2(-150.0, 50.0)));
|
||||
graph_editor.frp.set_node_position.emit((node2_id, Vector2(50.0, 50.0)));
|
||||
graph_editor.frp.set_node_position.emit((node3_id, Vector2(150.0, 250.0)));
|
||||
graph_editor.frp.set_node_position.emit((node4_id, Vector2(50.0, 150.0)));
|
||||
|
||||
|
||||
let expression_1 = expression_mock();
|
||||
@ -167,6 +169,9 @@ fn init(app: &Application) {
|
||||
let bar_node = graph_editor.model.add_node_below(node3_id);
|
||||
graph_editor.set_node_expression.emit((bar_node, Expression::new_plain("bar")));
|
||||
|
||||
let expression_4 = expression_mock_trim();
|
||||
graph_editor.frp.set_node_expression.emit((node4_id, expression_4));
|
||||
|
||||
|
||||
// === Connections ===
|
||||
|
||||
@ -327,8 +332,11 @@ pub fn expression_mock() -> Expression {
|
||||
let pattern = Some("var1".to_string());
|
||||
let code = "[1,2,3]".to_string();
|
||||
let parser = Parser::new_or_panic();
|
||||
let this_param =
|
||||
span_tree::ArgumentInfo { name: Some("self".to_owned()), tp: Some("Text".to_owned()) };
|
||||
let this_param = span_tree::ArgumentInfo {
|
||||
name: Some("self".to_owned()),
|
||||
tp: Some("Text".to_owned()),
|
||||
..default()
|
||||
};
|
||||
let parameters = vec![this_param];
|
||||
let ast = parser.parse_line_ast(&code).unwrap();
|
||||
let invocation_info = span_tree::generate::context::CalledMethodInfo { parameters };
|
||||
@ -386,21 +394,30 @@ pub fn expression_mock3() -> Expression {
|
||||
// let code = "image.blur ((foo bar) baz)".to_string();
|
||||
let code = "Vector x y z".to_string();
|
||||
let parser = Parser::new_or_panic();
|
||||
let this_param =
|
||||
span_tree::ArgumentInfo { name: Some("self".to_owned()), tp: Some("Image".to_owned()) };
|
||||
let this_param = span_tree::ArgumentInfo {
|
||||
name: Some("self".to_owned()),
|
||||
tp: Some("Image".to_owned()),
|
||||
..default()
|
||||
};
|
||||
let param0 = span_tree::ArgumentInfo {
|
||||
name: Some("radius".to_owned()),
|
||||
tp: Some("Number".to_owned()),
|
||||
..default()
|
||||
};
|
||||
let param1 = span_tree::ArgumentInfo {
|
||||
name: Some("name".to_owned()),
|
||||
tp: Some("Text".to_owned()),
|
||||
..default()
|
||||
};
|
||||
let param1 =
|
||||
span_tree::ArgumentInfo { name: Some("name".to_owned()), tp: Some("Text".to_owned()) };
|
||||
let param2 = span_tree::ArgumentInfo {
|
||||
name: Some("area".to_owned()),
|
||||
tp: Some("Vector Int".to_owned()),
|
||||
..default()
|
||||
};
|
||||
let param3 = span_tree::ArgumentInfo {
|
||||
name: Some("matrix".to_owned()),
|
||||
tp: Some("Vector String".to_owned()),
|
||||
..default()
|
||||
};
|
||||
let parameters = vec![this_param, param0, param1, param2, param3];
|
||||
let ast = parser.parse_line_ast(&code).unwrap();
|
||||
@ -412,3 +429,37 @@ pub fn expression_mock3() -> Expression {
|
||||
let code = code.into();
|
||||
Expression { pattern, code, whole_expression_id, input_span_tree, output_span_tree }
|
||||
}
|
||||
|
||||
pub fn expression_mock_trim() -> Expression {
|
||||
let pattern = Some("trim_node".to_string());
|
||||
let code = "\" hello \".trim".to_string();
|
||||
let parser = Parser::new_or_panic();
|
||||
let this_param = span_tree::ArgumentInfo {
|
||||
name: Some("self".to_owned()),
|
||||
tp: Some("Text".to_owned()),
|
||||
..default()
|
||||
};
|
||||
let param0 = span_tree::ArgumentInfo {
|
||||
name: Some("where".to_owned()),
|
||||
tp: Some("Location".to_owned()),
|
||||
tag_values: vec![
|
||||
"Location.Start".to_owned(),
|
||||
"Location.End".to_owned(),
|
||||
"Location.Both".to_owned(),
|
||||
],
|
||||
};
|
||||
let param1 = span_tree::ArgumentInfo {
|
||||
name: Some("what".to_owned()),
|
||||
tp: Some("Text".to_owned()),
|
||||
..default()
|
||||
};
|
||||
let parameters = vec![this_param, param0, param1];
|
||||
let ast = parser.parse_line_ast(&code).unwrap();
|
||||
let invocation_info = span_tree::generate::context::CalledMethodInfo { parameters };
|
||||
let ctx = span_tree::generate::MockContext::new_single(ast.id.unwrap(), invocation_info);
|
||||
let output_span_tree = span_tree::SpanTree::default();
|
||||
let input_span_tree = span_tree::SpanTree::new(&ast, &ctx).unwrap();
|
||||
let whole_expression_id = default();
|
||||
let code = code.into();
|
||||
Expression { pattern, code, whole_expression_id, input_span_tree, output_span_tree }
|
||||
}
|
||||
|
@ -325,7 +325,12 @@ ensogl::define_endpoints_2! {
|
||||
/// Press event. Emitted when user clicks on non-active part of the node, like its
|
||||
/// background. In edit mode, the whole node area is considered non-active.
|
||||
background_press (),
|
||||
/// Emitted when node expression is modified as a whole. Does not include partial changes on
|
||||
/// individual spans, which are emitted via `expression_span` output.
|
||||
expression (ImString),
|
||||
/// Emitted when node expression is edited in context of specific span. Does not include
|
||||
/// changes to the expression as a whole, which are emitted via `expression` output.
|
||||
expression_span (span_tree::Crumbs, ImString),
|
||||
comment (Comment),
|
||||
skip (bool),
|
||||
freeze (bool),
|
||||
@ -740,6 +745,7 @@ impl Node {
|
||||
eval filtered_usage_type (((a,b)) model.set_expression_usage_type(a,b));
|
||||
eval input.set_expression ((a) model.set_expression(a));
|
||||
out.expression <+ model.input.frp.expression;
|
||||
out.expression_span <+ model.input.frp.on_port_code_update;
|
||||
model.input.set_connected <+ input.set_input_connected;
|
||||
model.input.set_disabled <+ input.set_disabled;
|
||||
model.output.set_expression_visibility <+ input.set_output_expression_visibility;
|
||||
|
@ -7,5 +7,6 @@
|
||||
|
||||
pub mod area;
|
||||
pub mod port;
|
||||
pub mod widget;
|
||||
|
||||
pub use area::Area;
|
||||
|
@ -127,7 +127,7 @@ impl From<node::Expression> for Expression {
|
||||
fn from(t: node::Expression) -> Self {
|
||||
// The length difference between `code` and `viz_code` so far.
|
||||
let mut shift = 0.byte();
|
||||
let mut span_tree = t.input_span_tree.map(|_| port::Model::default());
|
||||
let mut span_tree: SpanTree = t.input_span_tree.map(|()| port::Model::default());
|
||||
let mut viz_code = String::new();
|
||||
let code = t.code;
|
||||
span_tree.root_ref_mut().dfs_with_layer_data(ExprConversion::default(), |node, info| {
|
||||
@ -288,6 +288,7 @@ impl Model {
|
||||
let code = &expression.viz_code;
|
||||
expression.span_tree.root_ref_mut().dfs_with_layer_data(builder, |mut node, builder| {
|
||||
let is_parensed = node.is_parensed();
|
||||
let last_argument = node.children.iter().rposition(|child| child.is_argument());
|
||||
let skip_opr = if SKIP_OPERATIONS {
|
||||
node.is_operation() && !is_header
|
||||
} else {
|
||||
@ -337,6 +338,8 @@ impl Model {
|
||||
let padded_size = Vector2(width_padded, height);
|
||||
let size = Vector2(width, height);
|
||||
let port_shape = port.payload_mut().init_shape(size, node::HEIGHT);
|
||||
let argument_info = port.argument_info();
|
||||
let port_widget = port.payload_mut().init_widget(&self.app, argument_info, node::HEIGHT);
|
||||
|
||||
port_shape.set_x(unit * index as f32);
|
||||
if DEBUG {
|
||||
@ -430,6 +433,28 @@ impl Model {
|
||||
pointer_style <- pointer_styles.fold();
|
||||
area_frp.source.pointer_style <+ pointer_style;
|
||||
}
|
||||
|
||||
if let Some(port_widget) = port_widget {
|
||||
port_widget.set_x(unit * index as f32);
|
||||
builder.parent.add_child(&port_widget);
|
||||
let range = port.payload.range();
|
||||
let code = &expression.viz_code[range];
|
||||
let last_arg_crumb = builder.parent_last_argument;
|
||||
port_widget.set_current_value(Some(code.into()));
|
||||
frp::extend! { port_network
|
||||
area_frp.source.on_port_code_update <+ port_widget.value_changed.map(
|
||||
f!([crumbs](v) {
|
||||
let crumbs = crumbs.clone_ref();
|
||||
let is_last_argument = crumbs.last().copied() == last_arg_crumb;
|
||||
(crumbs.clone_ref(), v.clone().unwrap_or_else(|| {
|
||||
if is_last_argument { default() } else { "_".into() }
|
||||
}))
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init_color.emit(());
|
||||
area_frp.set_view_mode.emit(area_frp.view_mode.value());
|
||||
port_shape.display_object().clone_ref()
|
||||
@ -444,7 +469,7 @@ impl Model {
|
||||
}
|
||||
let new_parent_frp = Some(node.frp.output.clone_ref());
|
||||
let new_shift = if !not_a_port { 0 } else { builder.shift + local_char_offset };
|
||||
builder.nested(new_parent, new_parent_frp, is_parensed, new_shift)
|
||||
builder.nested(new_parent, new_parent_frp, is_parensed, last_argument, new_shift)
|
||||
});
|
||||
*self.id_crumbs_map.borrow_mut() = id_crumbs_map;
|
||||
}
|
||||
@ -720,6 +745,7 @@ ensogl::define_endpoints! {
|
||||
on_port_press (Crumbs),
|
||||
on_port_hover (Switch<Crumbs>),
|
||||
on_port_type_change (Crumbs,Option<Type>),
|
||||
on_port_code_update (Crumbs,ImString),
|
||||
on_background_press (),
|
||||
view_mode (view::Mode),
|
||||
}
|
||||
@ -923,6 +949,8 @@ struct PortLayerBuilder {
|
||||
shift: usize,
|
||||
/// The depth at which the current expression is, where root is at depth 0.
|
||||
depth: usize,
|
||||
/// The crumb of last child argument of parent node. Does not count insertion points.
|
||||
parent_last_argument: Option<Crumb>,
|
||||
}
|
||||
|
||||
impl PortLayerBuilder {
|
||||
@ -932,15 +960,16 @@ impl PortLayerBuilder {
|
||||
parent: impl display::Object,
|
||||
parent_frp: Option<port::FrpEndpoints>,
|
||||
parent_parensed: bool,
|
||||
parent_last_argument: Option<Crumb>,
|
||||
shift: usize,
|
||||
depth: usize,
|
||||
) -> Self {
|
||||
let parent = parent.display_object().clone_ref();
|
||||
Self { parent_frp, parent, parent_parensed, shift, depth }
|
||||
Self { parent_frp, parent, parent_parensed, shift, depth, parent_last_argument }
|
||||
}
|
||||
|
||||
fn empty(parent: impl display::Object) -> Self {
|
||||
Self::new(parent, default(), default(), default(), default())
|
||||
Self::new(parent, default(), default(), default(), default(), default())
|
||||
}
|
||||
|
||||
/// Create a nested builder with increased depth and updated `parent_frp`.
|
||||
@ -950,11 +979,12 @@ impl PortLayerBuilder {
|
||||
parent: display::object::Instance,
|
||||
new_parent_frp: Option<port::FrpEndpoints>,
|
||||
parent_parensed: bool,
|
||||
parent_last_argument: Option<Crumb>,
|
||||
shift: usize,
|
||||
) -> Self {
|
||||
let depth = self.depth + 1;
|
||||
let parent_frp = new_parent_frp.or_else(|| self.parent_frp.clone());
|
||||
Self::new(parent, parent_frp, parent_parensed, shift, depth)
|
||||
Self::new(parent, parent_frp, parent_parensed, parent_last_argument, shift, depth)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,8 +5,10 @@ use enso_text::unit::*;
|
||||
use ensogl::display::shape::*;
|
||||
|
||||
use crate::node::input::area;
|
||||
use crate::node::input::widget::Widget;
|
||||
use crate::Type;
|
||||
|
||||
use ensogl::application::Application;
|
||||
use ensogl::data::color;
|
||||
use ensogl::display;
|
||||
|
||||
@ -141,6 +143,7 @@ ensogl::define_endpoints! {
|
||||
|
||||
Output {
|
||||
tp (Option<Type>),
|
||||
new_value (String),
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,11 +154,10 @@ ensogl::define_endpoints! {
|
||||
pub struct Model {
|
||||
pub frp: Frp,
|
||||
pub shape: Option<Shape>,
|
||||
pub name: Option<String>,
|
||||
pub widget: Option<Widget>,
|
||||
pub index: ByteDiff,
|
||||
pub local_index: ByteDiff,
|
||||
pub length: ByteDiff,
|
||||
pub highlight_color: color::Lcha, // TODO needed? and other fields?
|
||||
}
|
||||
|
||||
impl Deref for Model {
|
||||
@ -182,6 +184,18 @@ impl Model {
|
||||
self.shape.as_ref().unwrap().clone_ref()
|
||||
}
|
||||
|
||||
/// Widget initialization. Same rules apply as for the shape initialization.
|
||||
pub fn init_widget(
|
||||
&mut self,
|
||||
app: &Application,
|
||||
argument_info: Option<span_tree::ArgumentInfo>,
|
||||
node_height: f32,
|
||||
) -> Option<Widget> {
|
||||
let Some(argument_info) = argument_info else { return None };
|
||||
self.widget = Widget::new(app, argument_info, node_height);
|
||||
self.widget.clone_ref()
|
||||
}
|
||||
|
||||
/// The range of this port.
|
||||
pub fn range(&self) -> enso_text::Range<ByteDiff> {
|
||||
let start = self.index;
|
||||
|
169
app/gui/view/graph-editor/src/component/node/input/widget.rs
Normal file
169
app/gui/view/graph-editor/src/component/node/input/widget.rs
Normal file
@ -0,0 +1,169 @@
|
||||
//! Definition of all hardcoded node widget variants and common widget FRP API.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use enso_frp as frp;
|
||||
use ensogl::application::Application;
|
||||
use ensogl::data::color;
|
||||
use ensogl::display;
|
||||
use ensogl::display::object::event;
|
||||
use ensogl_component::drop_down::Dropdown;
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// === NodeWidget ===
|
||||
// ==================
|
||||
|
||||
ensogl::define_endpoints_2! {
|
||||
Input {
|
||||
set_current_value(Option<ImString>),
|
||||
set_focused(bool),
|
||||
}
|
||||
Output {
|
||||
value_changed(Option<ImString>),
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible widgets for a node input.
|
||||
///
|
||||
/// Currently all widget types are hardcoded. This is likely to be a temporary solution. In the
|
||||
/// future the set of widget types might be dynamic, similar to visualizations.
|
||||
#[derive(Clone, Debug, CloneRef)]
|
||||
pub enum Widget {
|
||||
/// A widget for selecting a single value from a list of available options.
|
||||
SingleChoice(SingleChoice),
|
||||
}
|
||||
|
||||
impl Widget {
|
||||
/// Create a new node widget, selecting the appropriate widget type based on the provided
|
||||
/// argument info.
|
||||
pub fn new(
|
||||
app: &Application,
|
||||
argument_info: span_tree::ArgumentInfo,
|
||||
node_height: f32,
|
||||
) -> Option<Self> {
|
||||
// TODO [PG] Support more widgets, use engine provided widget type.
|
||||
// https://www.pivotaltracker.com/story/show/184012753
|
||||
if !argument_info.tag_values.is_empty() {
|
||||
Some(Self::SingleChoice(SingleChoice::new(app, argument_info, node_height)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn frp(&self) -> &Frp {
|
||||
match self {
|
||||
Self::SingleChoice(s) => &s.frp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Widget {
|
||||
type Target = Frp;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.frp()
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for Widget {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
match self {
|
||||
Self::SingleChoice(s) => &s.display_object,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =================
|
||||
// === Dot Shape ===
|
||||
// =================
|
||||
|
||||
/// Temporary dropdown activation shape definition.
|
||||
pub mod dot {
|
||||
use super::*;
|
||||
ensogl::shape! {
|
||||
above = [
|
||||
crate::component::node::background,
|
||||
crate::component::node::input::port::hover
|
||||
];
|
||||
(style:Style, color:Vector4) {
|
||||
let size = Var::canvas_size();
|
||||
let radius = Min::min(size.x(),size.y()) / 2.0;
|
||||
let shape = Rect(size).corners_radius(radius);
|
||||
shape.fill(color).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === SingleChoice ===
|
||||
// ====================
|
||||
|
||||
/// A widget for selecting a single value from a list of available options. The options can be
|
||||
/// provided as a static list of strings from argument `tag_values`, or as a dynamic expression.
|
||||
#[derive(Clone, Debug, CloneRef)]
|
||||
pub struct SingleChoice {
|
||||
display_object: display::object::Instance,
|
||||
frp: Frp,
|
||||
#[allow(dead_code)]
|
||||
dropdown: Dropdown<ImString>,
|
||||
/// temporary click handling
|
||||
activation_dot: dot::View,
|
||||
}
|
||||
|
||||
impl SingleChoice {
|
||||
fn new(app: &Application, argument_info: span_tree::ArgumentInfo, node_height: f32) -> Self {
|
||||
let display_object = display::object::Instance::new();
|
||||
let dropdown = app.new_view::<Dropdown<ImString>>();
|
||||
display_object.add_child(&dropdown);
|
||||
app.display.default_scene.layers.above_nodes.add(&dropdown);
|
||||
let frp = Frp::new();
|
||||
let network = &frp.network;
|
||||
let input = &frp.input;
|
||||
let output = &frp.private.output;
|
||||
dropdown.set_y(-node_height);
|
||||
dropdown.set_max_open_size(Vector2(300.0, 500.0));
|
||||
|
||||
let activation_dot = dot::View::new();
|
||||
let color: color::Rgba = color::Lcha::new(0.56708, 0.23249, 0.71372, 1.0).into();
|
||||
activation_dot.color.set(color.into());
|
||||
activation_dot.set_size((15.0, 15.0));
|
||||
activation_dot.set_y(-node_height / 2.0);
|
||||
display_object.add_child(&activation_dot);
|
||||
|
||||
|
||||
if !argument_info.tag_values.is_empty() {
|
||||
let entries: Vec<ImString> = argument_info.tag_values.iter().map(Into::into).collect();
|
||||
dropdown.set_all_entries(entries);
|
||||
dropdown.allow_deselect_all(true);
|
||||
} else {
|
||||
// TODO [PG]: Support dynamic entries.
|
||||
// https://www.pivotaltracker.com/story/show/184012743
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
frp::extend! { network
|
||||
dropdown.set_selected_entries <+ input.set_current_value.map(|s| s.iter().cloned().collect());
|
||||
first_selected_entry <- dropdown.selected_entries.map(|e| e.iter().next().cloned());
|
||||
output.value_changed <+ first_selected_entry.on_change();
|
||||
|
||||
let dot_clicked = activation_dot.events.mouse_down_primary.clone_ref();
|
||||
toggle_focus <- dot_clicked.map(f!([display_object](()) !display_object.is_focused()));
|
||||
set_focused <- any(toggle_focus, input.set_focused);
|
||||
eval set_focused([display_object](focus) match focus {
|
||||
true => display_object.focus(),
|
||||
false => display_object.blur(),
|
||||
});
|
||||
|
||||
let focus_in = display_object.on_event::<event::FocusIn>();
|
||||
let focus_out = display_object.on_event::<event::FocusOut>();
|
||||
is_focused <- bool(&focus_out, &focus_in);
|
||||
|
||||
dropdown.set_open <+ is_focused;
|
||||
}
|
||||
|
||||
Self { display_object, frp, dropdown, activation_dot }
|
||||
}
|
||||
}
|
@ -681,6 +681,7 @@ ensogl::define_endpoints_2! {
|
||||
node_position_set ((NodeId, Vector2)),
|
||||
node_position_set_batched ((NodeId, Vector2)),
|
||||
node_expression_set ((NodeId, ImString)),
|
||||
node_expression_span_set ((NodeId, span_tree::Crumbs, ImString)),
|
||||
node_comment_set ((NodeId, String)),
|
||||
node_entered (NodeId),
|
||||
node_exited (),
|
||||
@ -1555,6 +1556,10 @@ impl GraphEditorModelWithNetwork {
|
||||
));
|
||||
|
||||
eval node.expression((t) model.frp.private.output.node_expression_set.emit((node_id,t.into())));
|
||||
eval node.expression_span([model]((crumbs,code)) {
|
||||
let args = (node_id, crumbs.clone(), code.clone());
|
||||
model.frp.private.output.node_expression_span_set.emit(args)
|
||||
});
|
||||
|
||||
|
||||
// === Actions ===
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Options intended to be common for all developers.
|
||||
|
||||
wasm-size-limit: 15.35 MiB
|
||||
wasm-size-limit: 15.45 MiB
|
||||
|
||||
required-versions:
|
||||
cargo-watch: ^8.1.1
|
||||
|
@ -178,7 +178,6 @@ impl ensogl_grid_view::Entry for Entry {
|
||||
#[allow(clippy::manual_clamp)]
|
||||
width.max(params.min_width).min(params.max_width)
|
||||
});
|
||||
trace limited_entry_width;
|
||||
out.minimum_column_width <+ limited_entry_width;
|
||||
|
||||
view_width <- max_width.map2(&text_offset, |width, offset| Some(width - offset));
|
||||
|
@ -103,23 +103,8 @@ impl_clone_ref_as_clone_no_from!(web_sys::EventTarget);
|
||||
|
||||
// === Option ===
|
||||
|
||||
/// Trait for types that can be internally cloned using `CloneRef`, like `Option<&T>`.
|
||||
#[allow(missing_docs)]
|
||||
pub trait ClonedRef {
|
||||
type Output;
|
||||
fn cloned_ref(&self) -> Self::Output;
|
||||
}
|
||||
|
||||
impl<T: CloneRef> ClonedRef for Option<&T> {
|
||||
type Output = Option<T>;
|
||||
fn cloned_ref(&self) -> Self::Output {
|
||||
self.map(|t| t.clone_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: CloneRef> ClonedRef for Option<&mut T> {
|
||||
type Output = Option<T>;
|
||||
fn cloned_ref(&self) -> Self::Output {
|
||||
impl<T: CloneRef> CloneRef for Option<T> {
|
||||
fn clone_ref(&self) -> Self {
|
||||
self.as_ref().map(|t| t.clone_ref())
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user