diff --git a/.prettierignore b/.prettierignore index 5075cafe4b7a..bce3654ee7d9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -30,3 +30,6 @@ Cargo.lock # TODO [mwu]: Adjust Engine build to not leave them. ci-build/ enso/ + +# Popular IDEs +.idea diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b3726c39094..5cfec9bbd178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,10 @@ node cration.][3186] - [Fixed developer console error about failing to decode a notification "executionContext/visualisationEvaluationFailed"][3193] +- [New Version of the Node Searcher - the Component Browser][3530] The available + methods, atoms and functions are presented in nice, categorized view. The most + popular tools are available at hand. The The panel is unstable, and thus is + available under the `--enable-new-component-browser` flag. #### EnsoGL (rendering engine) @@ -236,6 +240,7 @@ [3519]: https://github.com/enso-org/enso/pull/3519 [3523]: https://github.com/enso-org/enso/pull/3523 [3528]: https://github.com/enso-org/enso/pull/3528 +[3530]: https://github.com/enso-org/enso/pull/3530 [3542]: https://github.com/enso-org/enso/pull/3542 [3551]: https://github.com/enso-org/enso/pull/3551 [3552]: https://github.com/enso-org/enso/pull/3552 diff --git a/Cargo.lock b/Cargo.lock index bbb7326a09b6..5682d15d5e85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1516,8 +1516,8 @@ dependencies = [ "ensogl-selector", "ensogl-text-msdf-sys", "ide-view-component-group", + "ide-view-component-list-panel", "js-sys", - "searcher-list-panel", "wasm-bindgen", ] @@ -1798,7 +1798,7 @@ dependencies = [ "tracing-subscriber", "unicase 2.6.0", "url 2.2.2", - "uuid 1.1.0", + "uuid 1.1.2", "walkdir", "which", "whoami", @@ -1944,6 +1944,7 @@ dependencies = [ "ast", "bimap", "console_error_panic_hook", + "convert_case 0.5.0", "double-representation", "engine-protocol", "enso-callback", @@ -1969,6 +1970,7 @@ dependencies = [ "futures 0.3.21", "fuzzly", "ide-view", + "ide-view-component-group", "itertools 0.10.3", "js-sys", "json-rpc", @@ -3757,7 +3759,7 @@ dependencies = [ "tracing-subscriber", "unicase 2.6.0", "url 2.2.2", - "uuid 1.1.0", + "uuid 1.1.2", "walkdir", "which", "whoami", @@ -3778,9 +3780,11 @@ dependencies = [ "enso-shapely", "ensogl", "ensogl-component", + "ensogl-gui-component", "ensogl-hardcoded-theme", "ensogl-text", "ensogl-text-msdf-sys", + "ide-view-component-browser", "ide-view-graph-editor", "js-sys", "multi-map", @@ -3796,6 +3800,16 @@ dependencies = [ "welcome-screen", ] +[[package]] +name = "ide-view-component-browser" +version = "0.1.0" +dependencies = [ + "enso-prelude", + "ensogl-text", + "ide-view-component-group", + "ide-view-component-list-panel", +] + [[package]] name = "ide-view-component-group" version = "0.1.0" @@ -3812,6 +3826,25 @@ dependencies = [ "failure", ] +[[package]] +name = "ide-view-component-list-panel" +version = "0.1.0" +dependencies = [ + "approx 0.5.1", + "enso-frp", + "ensogl-core", + "ensogl-derive-theme", + "ensogl-gui-component", + "ensogl-hardcoded-theme", + "ensogl-list-view", + "ensogl-scroll-area", + "ensogl-selector", + "ensogl-shadow", + "ensogl-text", + "ide-view-component-group", + "ordered-float", +] + [[package]] name = "ide-view-graph-editor" version = "0.1.0" @@ -5780,25 +5813,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "searcher-list-panel" -version = "0.1.0" -dependencies = [ - "approx 0.5.1", - "enso-frp", - "ensogl-core", - "ensogl-derive-theme", - "ensogl-gui-component", - "ensogl-hardcoded-theme", - "ensogl-list-view", - "ensogl-scroll-area", - "ensogl-selector", - "ensogl-shadow", - "ensogl-text", - "ide-view-component-group", - "ordered-float", -] - [[package]] name = "secrecy" version = "0.8.0" @@ -7000,9 +7014,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93bbc61e655a4833cf400d0d15bf3649313422fa7572886ad6dab16d79886365" +checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" dependencies = [ "getrandom 0.2.6", "serde", diff --git a/app/gui/Cargo.toml b/app/gui/Cargo.toml index 3aab36fedc36..0b40465b420c 100644 --- a/app/gui/Cargo.toml +++ b/app/gui/Cargo.toml @@ -31,12 +31,14 @@ ensogl-drop-manager = { path = "../../lib/rust/ensogl/component/drop-manager" } fuzzly = { path = "../../lib/rust/fuzzly" } ast = { path = "language/ast/impl" } ide-view = { path = "view" } +ide-view-component-group = { path = "view/component-browser/component-group" } engine-protocol = { path = "controller/engine-protocol" } json-rpc = { path = "../../lib/rust/json-rpc" } parser = { path = "language/parser" } span-tree = { path = "language/span-tree" } bimap = { version = "0.4.0" } console_error_panic_hook = { version = "0.1.6" } +convert_case = { version = "0.5.0" } failure = { version = "0.1.6" } flo_stream = { version = "0.4.0" } futures = { version = "0.3.1" } diff --git a/app/gui/config/src/lib.rs b/app/gui/config/src/lib.rs index 518d6acd5930..dcbbf8383226 100644 --- a/app/gui/config/src/lib.rs +++ b/app/gui/config/src/lib.rs @@ -66,5 +66,6 @@ ensogl::read_args! { test_workflow : String, skip_min_version_check : bool, preferred_engine_version : semver::Version, + enable_new_component_browser : bool, } } diff --git a/app/gui/controller/double-representation/src/tp.rs b/app/gui/controller/double-representation/src/tp.rs index c19d46a39647..bbf589738974 100644 --- a/app/gui/controller/double-representation/src/tp.rs +++ b/app/gui/controller/double-representation/src/tp.rs @@ -165,6 +165,17 @@ impl Display for QualifiedName { } +// === Comparison === + +impl PartialEq for QualifiedName { + fn eq(&self, rhs: &module::QualifiedName) -> bool { + self.project_name == rhs.project_name + && self.module_segments == rhs.id.parent_segments() + && self.name == rhs.id.name().as_ref() + } +} + + // ============= // === Tests === diff --git a/app/gui/src/controller/searcher.rs b/app/gui/src/controller/searcher.rs index 45f51a6c7a5a..7b6b22c6bdca 100644 --- a/app/gui/src/controller/searcher.rs +++ b/app/gui/src/controller/searcher.rs @@ -1807,7 +1807,9 @@ pub mod test { // Verify the contents of the components list loaded by the Searcher. let components = searcher.components(); if let [module_group] = &components.top_modules()[..] { - assert_eq!(module_group.name, entry1.module.to_string()); + let expected_group_name = + format!("{}.{}", entry1.module.project_name.project, entry1.module.name()); + assert_eq!(module_group.name, expected_group_name); let entries = module_group.entries.borrow(); assert_matches!(entries.as_slice(), [e1, e2] if e1.suggestion.name == entry1.name && e2.suggestion.name == entry9.name); } else { diff --git a/app/gui/src/controller/searcher/component.rs b/app/gui/src/controller/searcher/component.rs index a85f46520e9b..e5ffa87c61c3 100644 --- a/app/gui/src/controller/searcher/component.rs +++ b/app/gui/src/controller/searcher/component.rs @@ -6,6 +6,9 @@ use crate::prelude::*; use crate::model::suggestion_database; +use convert_case::Case; +use convert_case::Casing; + // ============== // === Export === @@ -29,6 +32,33 @@ pub type MatchInfo = controller::searcher::action::MatchInfo; +// ============== +// === Errors === +// ============== + +// === NoSuchGroup=== + +#[allow(missing_docs)] +#[derive(Clone, Debug, Fail)] +#[fail(display = "No component group with the index {} in section {}.", index, section_name)] +pub struct NoSuchGroup { + section_name: CowString, + index: usize, +} + + +// === NoSuchComponent === + +#[allow(missing_docs)] +#[derive(Clone, Debug, Fail)] +#[fail(display = "No component entry with the index {} in {}.", index, group_name)] +pub struct NoSuchComponent { + group_name: CowString, + index: usize, +} + + + // ============= // === Order === // ============= @@ -37,25 +67,18 @@ pub type MatchInfo = controller::searcher::action::MatchInfo; /// [`Group::update_sorting_and_visibility`]. #[derive(Copy, Clone, Debug)] pub enum Order { + /// The same order of components as when the group was built. + /// Will use the [`Group::initial_entries_order`] field. + Initial, /// Order non-modules by name, followed by modules (also by name). ByNameNonModulesThenModules, - /// Order [`Component`]s by [`Component::match_info`] score (best scores first). + /// Order [`Component`]s by [`Component::match_info`] score. The matching entries will go + /// first, and the _lesser_ score will take precedence. That is due to way of displaying + /// components in component browser - the lower (with greater indices) entries are more + /// handy. ByMatch, } -impl Order { - /// Compare two [`Component`]s according to [`Order`]. - fn compare(&self, a: &Component, b: &Component) -> std::cmp::Ordering { - match self { - Order::ByNameNonModulesThenModules => { - let cmp_can_be_entered = a.can_be_entered().cmp(&b.can_be_entered()); - cmp_can_be_entered.then_with(|| a.label().cmp(b.label())) - } - Order::ByMatch => a.match_info.borrow().cmp(&*b.match_info.borrow()).reverse(), - } - } -} - // ================= @@ -87,8 +110,8 @@ impl Component { } /// The label which should be displayed in the Component Browser. - pub fn label(&self) -> &str { - &self.suggestion.name + pub fn label(&self) -> String { + self.to_string() } /// Checks if component is filtered out. @@ -109,7 +132,7 @@ impl Component { /// It should be called each time the filtering pattern changes. pub fn update_matching_info(&self, pattern: impl Str) { let label = self.label(); - let matches = fuzzly::matches(label, pattern.as_ref()); + let matches = fuzzly::matches(&label, pattern.as_ref()); let subsequence = matches.and_option_from(|| { let metric = fuzzly::metric::default(); fuzzly::find_best_subsequence(label, pattern, metric) @@ -121,6 +144,20 @@ impl Component { } } +impl Display for Component { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let self_type_not_here = + self.suggestion.self_type.as_ref().filter(|t| *t != &self.suggestion.module); + if let Some(self_type) = self_type_not_here { + let self_name = self_type.name.from_case(Case::Snake).to_case(Case::Title); + let name = self.suggestion.name.from_case(Case::Snake).to_case(Case::Lower); + write!(f, "{} {}", self_name, name) + } else { + write!(f, "{}", self.suggestion.name.from_case(Case::Snake).to_case(Case::Lower)) + } + } +} + // ============ @@ -190,6 +227,31 @@ impl List { self.module_groups.get(&component).map(|mg| &mg.content) } + /// Get the component from Top Modules by index. + pub fn top_module_entry_by_index( + &self, + group_index: usize, + entry_index: usize, + ) -> FallibleResult { + self.top_modules().entry_by_index("Sub-modules".into(), group_index, entry_index) + } + + /// Get the component from Favorites section by index. + pub fn favorites_entry_by_index( + &self, + group_index: usize, + entry_index: usize, + ) -> FallibleResult { + self.favorites.entry_by_index("Favorites".into(), group_index, entry_index) + } + + /// Get the component from Local Scope section by index. + pub fn local_scope_entry_by_index(&self, index: usize) -> FallibleResult { + let error = + || NoSuchComponent { group_name: self.local_scope.name.to_string().into(), index }; + self.local_scope.get_entry(index).ok_or_else(error).map_err(|e| e.into()) + } + /// Update matching info in all components according to the new filtering pattern. pub fn update_filtering(&self, pattern: impl AsRef) { let pattern = pattern.as_ref(); @@ -197,13 +259,14 @@ impl List { component.update_matching_info(pattern) } let pattern_not_empty = !pattern.is_empty(); - let components_order = + let submodules_order = if pattern_not_empty { Order::ByMatch } else { Order::ByNameNonModulesThenModules }; + let favorites_order = if pattern_not_empty { Order::ByMatch } else { Order::Initial }; for group in self.all_groups_not_in_favorites() { - group.update_sorting_and_visibility(components_order); + group.update_sorting(submodules_order); } for group in self.favorites.iter() { - group.update_visibility(); + group.update_sorting(favorites_order); } self.filtered.set(pattern_not_empty); } @@ -317,11 +380,10 @@ pub(crate) mod tests { .entries .borrow() .iter() - .filter(|c| matches!(*c.match_info.borrow(), MatchInfo::Matches { .. })) + .take_while(|c| matches!(*c.match_info.borrow(), MatchInfo::Matches { .. })) .map(|c| *c.id) .collect_vec(); assert_eq!(ids_of_matches, expected_ids); - assert_eq!(group.visible.get(), !expected_ids.is_empty()); } #[test] @@ -343,6 +405,13 @@ pub(crate) mod tests { let list = builder.build(); list.update_filtering("fu"); + let match_infos = list.top_modules()[0] + .entries + .borrow() + .iter() + .map(|c| c.match_info.borrow().clone()) + .collect_vec(); + DEBUG!("{match_infos:?}"); assert_ids_of_matches_entries(&list.top_modules()[0], &[2, 3]); assert_ids_of_matches_entries(&list.favorites[0], &[3, 2]); assert_ids_of_matches_entries(&list.local_scope, &[2]); diff --git a/app/gui/src/controller/searcher/component/builder.rs b/app/gui/src/controller/searcher/component/builder.rs index 80255c824547..b0461cf13904 100644 --- a/app/gui/src/controller/searcher/component/builder.rs +++ b/app/gui/src/controller/searcher/component/builder.rs @@ -169,12 +169,12 @@ impl List { pub fn build(self) -> component::List { let components_order = component::Order::ByNameNonModulesThenModules; for group in self.module_groups.values() { - group.content.update_sorting_and_visibility(components_order); + group.content.update_sorting(components_order); if let Some(flattened) = &group.flattened_content { - flattened.update_sorting_and_visibility(components_order); + flattened.update_sorting(components_order); } } - self.local_scope.update_sorting_and_visibility(components_order); + self.local_scope.update_sorting(components_order); let top_modules_iter = self.module_groups.values().filter(|g| g.is_top_module); let mut top_mdl_bld = component::group::AlphabeticalListBuilder::default(); top_mdl_bld.extend(top_modules_iter.clone().map(|g| g.content.clone_ref())); @@ -239,12 +239,12 @@ mod tests { list.top_modules.iter().map(Into::into).collect(); let expected = vec![ ComparableGroupData { - name: "test.Test.TopModule1", + name: "Test.TopModule1", component_id: Some(0), entries: vec![5, 6, 2, 3], }, ComparableGroupData { - name: "test.Test.TopModule2", + name: "Test.TopModule2", component_id: Some(1), entries: vec![7], }, @@ -255,12 +255,12 @@ mod tests { list.top_modules_flattened.iter().map(Into::into).collect(); let expected = vec![ ComparableGroupData { - name: "test.Test.TopModule1", + name: "Test.TopModule1", component_id: Some(0), entries: vec![5, 6, 8, 9, 10, 2, 3, 4], }, ComparableGroupData { - name: "test.Test.TopModule2", + name: "Test.TopModule2", component_id: Some(1), entries: vec![7], }, @@ -274,12 +274,12 @@ mod tests { .collect(); let expected: BTreeMap = [ (0, ComparableGroupData { - name: "test.Test.TopModule1", + name: "Test.TopModule1", component_id: Some(0), entries: vec![5, 6, 2, 3], }), (1, ComparableGroupData { - name: "test.Test.TopModule2", + name: "Test.TopModule2", component_id: Some(1), entries: vec![7], }), diff --git a/app/gui/src/controller/searcher/component/group.rs b/app/gui/src/controller/searcher/component/group.rs index 6feb30f67a22..2b33be0907a0 100644 --- a/app/gui/src/controller/searcher/component/group.rs +++ b/app/gui/src/controller/searcher/component/group.rs @@ -5,10 +5,14 @@ use crate::prelude::*; use crate::controller::searcher::component; use crate::controller::searcher::component::Component; +use crate::controller::searcher::component::MatchInfo; +use crate::controller::searcher::component::NoSuchComponent; +use crate::controller::searcher::component::NoSuchGroup; use crate::model::execution_context; use crate::model::suggestion_database; use ensogl::data::color; +use std::cmp; @@ -20,15 +24,17 @@ use ensogl::data::color; #[allow(missing_docs)] #[derive(Clone, Debug, Default)] pub struct Data { - pub name: ImString, - pub color: Option, + pub name: ImString, + pub color: Option, /// A component corresponding to this group, e.g. the module of whose content the group /// contains. - pub component_id: Option, - pub entries: RefCell>, - /// A flag indicating that the group should be displayed in the Component Browser. It may be - /// hidden in some scenarios, e.g. when all items are filtered out. - pub visible: Cell, + pub component_id: Option, + /// The entries in the same order as when the group was built. Used to restore it in some cases + /// - see [`Self::update_sorting`] and [`component::Order`]. The vector may be empty if the + /// group is not meant to have initial order restored. + pub initial_entries_order: Vec, + pub entries: RefCell>, + pub matched_items: Cell, } impl Data { @@ -37,8 +43,9 @@ impl Data { name: name.into(), color: None, component_id, + initial_entries_order: default(), entries: default(), - visible: default(), + matched_items: Cell::new(0), } } } @@ -74,7 +81,9 @@ impl Group { /// Create empty group referring to some module component. pub fn from_entry(component_id: component::Id, entry: &suggestion_database::Entry) -> Self { let name: String = if entry.module.is_top_module() { - (&entry.module).into() + let project = &entry.module.project_name.project; + let module = entry.module.name(); + format!("{}.{}", project, module) } else { entry.module.name().into() }; @@ -98,29 +107,83 @@ impl Group { let any_components_found_in_db = !looked_up_components.is_empty(); any_components_found_in_db.then(|| { let group_data = Data { - name: group.name.clone(), - color: group.color, - component_id: None, - visible: Cell::new(true), - entries: RefCell::new(looked_up_components), + name: group.name.clone(), + color: group.color, + component_id: None, + matched_items: Cell::new(looked_up_components.len()), + initial_entries_order: looked_up_components.clone(), + entries: RefCell::new(looked_up_components), }; Group { data: Rc::new(group_data) } }) } - /// Update the group sorting according to the `order` and call [`update_visibility`]. - pub fn update_sorting_and_visibility(&self, order: component::Order) { - // The `sort_by_key` method is not suitable here, because the closure it takes - // cannot return reference nor [`Ref`], and we don't want to copy anything here. - self.entries.borrow_mut().sort_by(|a, b| order.compare(a, b)); - self.update_visibility(); + /// Update the group sorting according to the `order` and update information about matched items + /// count. + pub fn update_sorting(&self, order: component::Order) { + match order { + component::Order::Initial => self.restore_initial_order(), + component::Order::ByNameNonModulesThenModules => + self.sort_by_name_non_modules_then_modules(), + component::Order::ByMatch => self.sort_by_match(), + } + let entries = self.entries.borrow(); + let matched_items = entries.iter().take_while(|c| !c.is_filtered_out()).count(); + self.matched_items.set(matched_items); + } + + fn restore_initial_order(&self) { + let mut entries = self.entries.borrow_mut(); + if entries.len() != self.initial_entries_order.len() { + tracing::error!( + "Tried to restore initial order in group where \ + `initial_entries_order` is not initialized or up-to-date. Will keep the \ + old order." + ) + } else { + *entries = self.initial_entries_order.clone() + } + } + + fn sort_by_name_non_modules_then_modules(&self) { + let mut entries = self.entries.borrow_mut(); + entries.sort_by(|a, b| { + let cmp_can_be_entered = a.can_be_entered().cmp(&b.can_be_entered()); + cmp_can_be_entered.then_with(|| a.label().cmp(&b.label())) + }) + } + + fn sort_by_match(&self) { + let mut entries = self.entries.borrow_mut(); + entries.sort_by(|a, b| { + Self::entry_match_ordering(&*a.match_info.borrow(), &*b.match_info.borrow()) + }); } - /// Sets the [`visible`] flag to [`true`] if at least one of the group's entries is not - /// filtered out. Sets the flag to [`false`] otherwise. - pub fn update_visibility(&self) { - let visible = !self.entries.borrow().iter().all(|c| c.is_filtered_out()); - self.visible.set(visible); + /// Return the entry match ordering when sorting by match. See [`component::Order::ByMatch`]. + fn entry_match_ordering(lhs: &MatchInfo, rhs: &MatchInfo) -> cmp::Ordering { + match (lhs, rhs) { + (MatchInfo::DoesNotMatch, MatchInfo::DoesNotMatch) => cmp::Ordering::Equal, + (MatchInfo::DoesNotMatch, MatchInfo::Matches { .. }) => cmp::Ordering::Greater, + (MatchInfo::Matches { .. }, MatchInfo::DoesNotMatch) => cmp::Ordering::Less, + (MatchInfo::Matches { subsequence: lhs }, MatchInfo::Matches { subsequence: rhs }) => + lhs.compare_scores(rhs), + } + } + + /// Get the number of entries. + pub fn len(&self) -> usize { + self.entries.borrow().len() + } + + /// Check if the group is empty. + pub fn is_empty(&self) -> bool { + self.entries.borrow().is_empty() + } + + /// Get cloned-ref entry under the index. + pub fn get_entry(&self, index: usize) -> Option { + self.entries.borrow().get(index).map(|e| e.clone_ref()) } } @@ -141,6 +204,24 @@ impl List { pub fn new(groups: Vec) -> Self { Self { groups: Rc::new(groups) } } + + /// Get entry under given group and entry index. + pub fn entry_by_index( + &self, + section_name: CowString, + group_index: usize, + entry_index: usize, + ) -> FallibleResult { + let error = || NoSuchGroup { section_name, index: group_index }; + let group = self.groups.get(group_index).ok_or_else(error)?; + let error = || NoSuchComponent { + group_name: group.name.to_string().into(), + index: entry_index, + }; + let entries = group.entries.borrow(); + let component = entries.get(entry_index).ok_or_else(error)?; + Ok(component.clone_ref()) + } } impl FromIterator for List { @@ -162,6 +243,12 @@ impl AsRef<[Group]> for List { } } +impl AsRef for List { + fn as_ref(&self) -> &List { + self + } +} + // ======================== diff --git a/app/gui/src/ide/initializer.rs b/app/gui/src/ide/initializer.rs index 29ca324e76e4..73fcdb745f39 100644 --- a/app/gui/src/ide/initializer.rs +++ b/app/gui/src/ide/initializer.rs @@ -237,6 +237,11 @@ pub fn register_views(app: &Application) { app.views.register::(); app.views.register::(); app.views.register::(); + app.views.register::(); + app.views.register::(); + app.views.register::(); + app.views.register::(); + app.views.register::(); app.views.register::(); app.views.register::(); app.views.register::(); diff --git a/app/gui/src/model/suggestion_database/entry.rs b/app/gui/src/model/suggestion_database/entry.rs index ad653cd5a8c6..6e5d6996cbd4 100644 --- a/app/gui/src/model/suggestion_database/entry.rs +++ b/app/gui/src/model/suggestion_database/entry.rs @@ -356,7 +356,7 @@ impl Entry { name, arguments, return_type, - documentation_html, + documentation_html: Self::make_html_docs(documentation, documentation_html), module: module.try_into()?, self_type: None, kind: Kind::Atom, @@ -376,7 +376,7 @@ impl Entry { name, arguments, return_type, - documentation_html, + documentation_html: Self::make_html_docs(documentation, documentation_html), module: module.try_into()?, self_type: Some(self_type.try_into()?), kind: Kind::Method, @@ -402,23 +402,46 @@ impl Entry { kind: Kind::Local, scope: Scope::InModule { range: scope.into() }, }, - Module { module, documentation_html, .. } => { + Module { module, documentation, documentation_html, .. } => { let module_name: module::QualifiedName = module.clone().try_into()?; Self { - documentation_html, - name: module_name.id().name().into(), - arguments: default(), - module: module_name, - self_type: None, - kind: Kind::Module, - scope: Scope::Everywhere, - return_type: module, + documentation_html: Self::make_html_docs(documentation, documentation_html), + name: module_name.id().name().into(), + arguments: default(), + module: module_name, + self_type: None, + kind: Kind::Module, + scope: Scope::Everywhere, + return_type: module, } } }; Ok(this) } + /// Returns the documentation in html depending on the information received from the Engine. + /// + /// Depending on the engine version, we may receive the documentation in HTML format already, + /// or the raw text which needs to be parsed. This function takes two fields of + /// [`language_server::types::SuggestionEntry`] and depending on availability, returns the + /// HTML docs fields, or parsed raw docs field. + fn make_html_docs(docs: Option, docs_html: Option) -> Option { + if docs_html.is_some() { + docs_html + } else { + docs.map(|docs| { + let parser = parser::DocParser::new(); + match parser { + Ok(p) => { + let output = p.generate_html_doc_pure((*docs).to_string()); + output.unwrap_or(docs) + } + Err(_) => docs, + } + }) + } + } + /// Apply modification to the entry. pub fn apply_modifications( &mut self, diff --git a/app/gui/src/presenter/project.rs b/app/gui/src/presenter/project.rs index 5ca34f85d135..7c90d6b554b7 100644 --- a/app/gui/src/presenter/project.rs +++ b/app/gui/src/presenter/project.rs @@ -87,7 +87,7 @@ impl Model { } } - fn editing_committed( + fn editing_committed_old_searcher( &self, node: ViewNodeId, entry_id: Option, @@ -109,6 +109,24 @@ impl Model { } } + fn editing_committed( + &self, + node: ViewNodeId, + entry_id: Option, + ) -> bool { + let searcher = self.searcher.take(); + if let Some(searcher) = searcher { + if let Some(created_node) = searcher.expression_accepted(entry_id) { + self.graph.assign_node_view_explicitly(node, created_node); + false + } else { + true + } + } else { + false + } + } + fn editing_aborted(&self) { let searcher = self.searcher.take(); if let Some(searcher) = searcher { @@ -196,6 +214,9 @@ impl Project { } }); + graph_view.remove_node <+ view.editing_committed_old_searcher.filter_map(f!([model]((node_view, entry)) { + model.editing_committed_old_searcher(*node_view, *entry).as_some(*node_view) + })); graph_view.remove_node <+ view.editing_committed.filter_map(f!([model]((node_view, entry)) { model.editing_committed(*node_view, *entry).as_some(*node_view) })); @@ -223,7 +244,6 @@ impl Project { let network = &self.network; let project = &self.model.view; let graph = self.model.view.graph(); - let searcher = self.model.view.searcher(); frp::extend! { network eval_ graph.node_editing_started([]analytics::remote_log_event("graph_editor::node_editing_started")); eval_ graph.node_editing_finished([]analytics::remote_log_event("graph_editor::node_editing_finished")); @@ -236,7 +256,6 @@ impl Project { eval_ graph.visualization_shown([]analytics::remote_log_event("graph_editor::visualization_shown")); eval_ graph.visualization_hidden([]analytics::remote_log_event("graph_editor::visualization_hidden")); eval_ graph.on_edge_endpoint_unset([]analytics::remote_log_event("graph_editor::connection_removed")); - eval_ searcher.used_as_suggestion([]analytics::remote_log_event("searcher::used_as_suggestion")); eval_ project.editing_committed([]analytics::remote_log_event("project::editing_committed")); } self diff --git a/app/gui/src/presenter/searcher.rs b/app/gui/src/presenter/searcher.rs index a1297fbdf0fc..ed6157b9737a 100644 --- a/app/gui/src/presenter/searcher.rs +++ b/app/gui/src/presenter/searcher.rs @@ -3,17 +3,22 @@ use crate::prelude::*; +use crate::controller::searcher::action::Suggestion; use crate::controller::searcher::Notification; use crate::controller::searcher::UserAction; use crate::executor::global::spawn_stream_handler; +use crate::model::suggestion_database::entry::Kind; use crate::presenter; use crate::presenter::graph::AstNodeId; use crate::presenter::graph::ViewNodeId; use enso_frp as frp; use ide_view as view; +use ide_view::component_browser::list_panel::LabeledAnyModelProvider; use ide_view::graph_editor::component::node as node_view; use ide_view::project::SearcherParams; +use ide_view::project::SearcherVariant; +use ide_view_component_group::set::SectionId; // ============== @@ -86,6 +91,96 @@ impl Model { provider::create_providers_from_controller(&self.logger, &self.controller) } + fn suggestion_accepted( + &self, + id: view::component_browser::list_panel::EntryId, + ) -> Option<(ViewNodeId, node_view::Expression)> { + let component = self.component_by_view_id(id); + let new_code = component.and_then(|component| { + let suggestion = Suggestion::FromDatabase(component.suggestion.clone_ref()); + self.controller.use_suggestion(suggestion) + }); + match new_code { + Ok(new_code) => { + let new_code_and_trees = node_view::Expression::new_plain(new_code); + Some((self.input_view, new_code_and_trees)) + } + Err(err) => { + error!(self.logger, "Error while applying suggestion: {err}"); + None + } + } + } + + fn expression_accepted( + &self, + entry_id: Option, + ) -> Option { + if let Some(entry_id) = entry_id { + self.suggestion_accepted(entry_id); + } + self.controller.commit_node().map(Some).unwrap_or_else(|err| { + error!(self.logger, "Error while committing node expression: {err}"); + None + }) + } + + fn component_by_view_id( + &self, + id: view::component_browser::list_panel::EntryId, + ) -> FallibleResult { + let components = self.controller.components(); + match id.group.section { + SectionId::Favorites => + components.favorites_entry_by_index(id.group.index, id.entry_id), + SectionId::LocalScope => components.local_scope_entry_by_index(id.entry_id), + SectionId::SubModules => + components.top_module_entry_by_index(id.group.index, id.entry_id), + } + } + + fn create_submodules_providers(&self) -> Vec { + provider::from_component_group_list(self.controller.components().top_modules()) + } + + fn create_favorites_providers(&self) -> Vec { + provider::from_component_group_list(&self.controller.components().favorites) + } + + fn create_local_scope_provider(&self) -> LabeledAnyModelProvider { + provider::from_component_group(&self.controller.components().local_scope) + } + + fn documentation_of_component( + &self, + id: Option, + ) -> String { + let component = id.and_then(|id| self.component_by_view_id(id).ok()); + if let Some(component) = component { + if let Some(documentation) = &component.suggestion.documentation_html { + let title = match component.suggestion.kind { + Kind::Atom => format!("Atom {}", component.suggestion.name), + Kind::Function => format!("Function {}", component.suggestion.name), + Kind::Local => format!("Node {}", component.suggestion.name), + Kind::Method => format!( + "Method {}{}{}", + component.suggestion.name, + if component.suggestion.self_type.is_some() { " of " } else { "" }, + component.suggestion.self_type.as_ref().map_or("", |tp| &tp.name), + ), + Kind::Module => format!("Module {}", component.suggestion.name), + }; + format!("

{title}

{documentation}") + } else { + provider::Action::doc_placeholder_for(&Suggestion::FromDatabase( + component.suggestion.clone_ref(), + )) + } + } else { + default() + } + } + fn should_auto_select_first_action(&self) -> bool { let user_action = self.controller.current_user_action(); let list_not_empty = matches!(self.controller.actions(), controller::searcher::Actions::Loaded {list} if list.matching_count() > 0); @@ -120,7 +215,6 @@ impl Searcher { let network = frp::Network::new("presenter::Searcher"); let graph = &model.view.graph().frp; - let searcher = &model.view.searcher().frp; frp::extend! { network eval graph.node_expression_set ([model]((changed_node, expr)) { @@ -130,14 +224,48 @@ impl Searcher { }); action_list_changed <- source::<()>(); - new_providers <- action_list_changed.map(f_!(model.create_providers())); - searcher.set_actions <+ new_providers; select_entry <- action_list_changed.filter(f_!(model.should_auto_select_first_action())); - searcher.select_action <+ select_entry.constant(0); + } + + match model.view.searcher() { + SearcherVariant::ComponentBrowser(browser) => { + let list_view = &browser.model().list; + let documentation = &browser.model().documentation; + frp::extend! { network + list_view.set_sub_modules_section <+ + action_list_changed.map(f_!(model.create_submodules_providers())); + list_view.set_favourites_section <+ + action_list_changed.map(f_!(model.create_favorites_providers())); + list_view.set_local_scope_section <+ + action_list_changed.map(f_!(model.create_local_scope_provider().content)); + new_input <- list_view.suggestion_accepted.filter_map(f!((e) model.suggestion_accepted(*e))); + trace new_input; + graph.set_node_expression <+ new_input; - used_as_suggestion <- searcher.used_as_suggestion.filter_map(|entry| *entry); - new_input <- used_as_suggestion.filter_map(f!((e) model.entry_used_as_suggestion(*e))); - graph.set_node_expression <+ new_input; + current_docs <- all_with( + &action_list_changed, + &list_view.selected_entry, + f!((_, entry) model.documentation_of_component(*entry)) + ); + documentation.frp.display_documentation <+ current_docs; + + eval_ list_view.suggestion_accepted([]analytics::remote_log_event("component_browser::suggestion_accepted")); + } + } + SearcherVariant::OldNodeSearcher(searcher) => { + let searcher = &searcher.frp; + + frp::extend! { network + new_providers <- action_list_changed.map(f_!(model.create_providers())); + searcher.set_actions <+ new_providers; + searcher.select_action <+ select_entry.constant(0); + used_as_suggestion <- searcher.used_as_suggestion.filter_map(|entry| *entry); + new_input <- used_as_suggestion.filter_map(f!((e) model.entry_used_as_suggestion(*e))); + graph.set_node_expression <+ new_input; + + eval_ searcher.used_as_suggestion([]analytics::remote_log_event("searcher::used_as_suggestion")); + } + } } let weak_model = Rc::downgrade(&model); @@ -187,17 +315,30 @@ impl Searcher { Ok(Self::new(parent, searcher_controller, view, input)) } - /// Commit editing. + /// Commit editing in the old Node Searcher. /// /// This method takes `self`, as the presenter (with the searcher view) should be dropped once /// editing finishes. The `entry_id` might be none in case where the searcher should accept - /// the node input without any entry selected. If the commitment will result in creating a new + /// the node input without any entry selected. If the commitment results in creating a new /// node, its AST id is returned. #[profile(Task)] pub fn commit_editing(self, entry_id: Option) -> Option { self.model.commit_editing(entry_id) } + /// Expression accepted in Compnent Browser. + /// + /// This method takes `self`, as the presenter (with the searcher view) should be dropped once + /// editing finishes. The `entry_id` might be none in case where the user want to accept + /// the node input without any entry selected. If the commitment results in creating a new + /// node, its AST id is returned. + pub fn expression_accepted( + self, + entry_id: Option, + ) -> Option { + self.model.expression_accepted(entry_id) + } + /// Abort editing, without taking any action. /// /// This method takes `self`, as the presenter (with the searcher view) should be dropped once diff --git a/app/gui/src/presenter/searcher/provider.rs b/app/gui/src/presenter/searcher/provider.rs index 13ac9b18a44a..476d79c5dd49 100644 --- a/app/gui/src/presenter/searcher/provider.rs +++ b/app/gui/src/presenter/searcher/provider.rs @@ -5,9 +5,12 @@ use crate::prelude::*; use crate::controller::searcher::action::MatchInfo; use crate::model::suggestion_database; +use enso_text as text; use ensogl_component::list_view; use ensogl_component::list_view::entry::GlyphHighlightedLabel; use ide_view as view; +use ide_view::component_browser::list_panel::LabeledAnyModelProvider; +use ide_view_component_group as component_group_view; @@ -61,7 +64,12 @@ pub struct Action { } impl Action { - fn doc_placeholder_for(suggestion: &controller::searcher::action::Suggestion) -> String { + /// Get the documentation for a suggestion in case when this suggestion does not have + /// a documentation. + /// + /// Usually something like "Function foo - no documentation available". The returned string is + /// documentation in HTML format. + pub fn doc_placeholder_for(suggestion: &controller::searcher::action::Suggestion) -> String { use controller::searcher::action::Suggestion; let code = match suggestion { Suggestion::FromDatabase(suggestion) => { @@ -148,3 +156,74 @@ impl ide_view::searcher::DocumentationProvider for Action { } } } + +/// Component Provider getting entries from a [`controller::searcher::component::Group`]. +#[derive(Clone, CloneRef, Debug)] +pub struct Component { + group: controller::searcher::component::Group, +} + +impl Component { + /// Create component provider based of the given group. + pub fn new(group: controller::searcher::component::Group) -> Self { + Self { group } + } +} + +impl list_view::entry::ModelProvider for Component { + fn entry_count(&self) -> usize { + self.group.matched_items.get() + } + + fn get(&self, id: usize) -> Option { + let component = self.group.get_entry(id)?; + let match_info = component.match_info.borrow(); + let label = component.label(); + let highlighted = bytes_of_matched_letters(&*match_info, &label); + Some(component_group_view::entry::Model { + icon: component_group_view::icon::Id::AddColumn, + highlighted_text: list_view::entry::GlyphHighlightedLabelModel { label, highlighted }, + }) + } +} + +fn bytes_of_matched_letters(match_info: &MatchInfo, label: &str) -> Vec> { + if let MatchInfo::Matches { subsequence } = match_info { + let mut char_iter = label.char_indices().enumerate(); + subsequence + .indices + .iter() + .filter_map(|idx| loop { + if let Some(char) = char_iter.next() { + let (char_idx, (byte_id, char)) = char; + if char_idx == *idx { + let start = enso_text::unit::Bytes(byte_id as i32); + let end = enso_text::unit::Bytes((byte_id + char.len_utf8()) as i32); + break Some(enso_text::Range::new(start, end)); + } + } else { + break None; + } + }) + .collect() + } else { + default() + } +} + +/// Get [`LabeledAnyModelProvider`] for given component group. +pub fn from_component_group( + group: &controller::searcher::component::Group, +) -> LabeledAnyModelProvider { + LabeledAnyModelProvider { + label: group.name.clone_ref(), + content: Rc::new(Component::new(group.clone_ref())).into(), + } +} + +/// Get vector of [`LabeledAnyModelProvider`] for given component group list. +pub fn from_component_group_list( + groups: &impl AsRef, +) -> Vec { + groups.as_ref().iter().map(from_component_group).collect() +} diff --git a/app/gui/view/Cargo.toml b/app/gui/view/Cargo.toml index 858b56590a01..55a4015bf1f5 100644 --- a/app/gui/view/Cargo.toml +++ b/app/gui/view/Cargo.toml @@ -17,9 +17,11 @@ enso-shapely = { path = "../../../lib/rust/shapely" } engine-protocol = { path = "../controller/engine-protocol" } ensogl = { path = "../../../lib/rust/ensogl" } ensogl-component = { path = "../../../lib/rust/ensogl/component" } +ensogl-gui-component = { path = "../../../lib/rust/ensogl/component/gui" } ensogl-text = { path = "../../../lib/rust/ensogl/component/text" } ensogl-text-msdf-sys = { path = "../../../lib/rust/ensogl/component/text/msdf-sys" } ensogl-hardcoded-theme = { path = "../../../lib/rust/ensogl/app/theme/hardcoded" } +ide-view-component-browser = { path = "component-browser" } ide-view-graph-editor = { path = "graph-editor" } parser = { path = "../language/parser" } span-tree = { path = "../language/span-tree" } diff --git a/app/gui/view/component-browser/Cargo.toml b/app/gui/view/component-browser/Cargo.toml index b993f6af079e..09a1e128dac9 100644 --- a/app/gui/view/component-browser/Cargo.toml +++ b/app/gui/view/component-browser/Cargo.toml @@ -8,5 +8,7 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] +enso-prelude = { path = "../../../../lib/rust/prelude" } ide-view-component-group = { path = "component-group" } +ide-view-component-list-panel = { path = "searcher-list-panel" } ensogl-text = { path = "../../../../lib/rust/ensogl/component/text" } diff --git a/app/gui/view/component-browser/component-group/src/lib.rs b/app/gui/view/component-browser/component-group/src/lib.rs index 256d3056a242..72010c87eaa0 100644 --- a/app/gui/view/component-browser/component-group/src/lib.rs +++ b/app/gui/view/component-browser/component-group/src/lib.rs @@ -43,6 +43,7 @@ #![recursion_limit = "512"] // === Features === #![feature(option_result_contains)] +#![feature(derive_default_enum)] // === Standard Linter Configuration === #![deny(non_ascii_idents)] #![warn(unsafe_code)] @@ -122,14 +123,10 @@ pub mod selection_box { ensogl::define_shape_system! { pointer_events = false; - (style:Style) { + (style:Style,corners_radius:f32) { let width: Var = "input_size.x".into(); let height: Var = "input_size.y".into(); - let corners_radius = style.get_number(theme::selection::corners_radius); - let padding_y = style.get_number(theme::selection::vertical_padding); - let padding_x = style.get_number(theme::selection::horizontal_padding); - let shape = Rect((width - padding_x.px(), height - padding_y.px())); - shape.corners_radius(corners_radius.px()).into() + Rect((width, height)).corners_radius(corners_radius.px()).into() } } } @@ -218,6 +215,42 @@ pub mod header_overlay { +// ====================== +// === SelectionStyle === +// ====================== + +#[derive(Debug, Copy, Clone, Default)] +struct SelectionStyle { + padding_x: f32, + height: f32, + header_height: f32, + corners_radius: f32, + header_corners_radius: f32, +} + +impl SelectionStyle { + fn from_style(style: &StyleWatchFrp, network: &frp::Network) -> frp::Sampler { + let padding_x = style.get_number(theme::selection::horizontal_padding); + let height = style.get_number(theme::selection::height); + let header_height = style.get_number(theme::selection::header_height); + let corners_radius = style.get_number(theme::selection::corners_radius); + let header_corners_radius = style.get_number(theme::selection::header_corners_radius); + frp::extend! { network + init <- source_(); + theme <- all_with6(&init,&height,&header_height,&corners_radius,&header_corners_radius, + &padding_x, |_,&height,&header_height,&corners_radius,&header_corners_radius, + &padding_x| + Self {height,header_height,corners_radius,header_corners_radius, + padding_x} ); + theme_sampler <- theme.sampler(); + } + init.emit(()); + theme_sampler + } +} + + + // ======================= // === Header Geometry === // ======================= @@ -378,6 +411,7 @@ ensogl::define_endpoints_2! { header_accepted(), selection_size(Vector2), selection_position_target(Vector2), + selection_corners_radius(f32), size(Vector2) } } @@ -486,11 +520,12 @@ impl component::Frp for Frp { out.is_header_selected <+ bool(&deselect_header, &select_header).on_change(); model.entries.select_entry <+ select_header.constant(None); + let selection_style = SelectionStyle::from_style(style, network); out.selection_size <+ all_with3( - &header_geometry, + &selection_style, &out.is_header_selected, &out.focused, - f!((geom, h_sel, _) model.selection_size(geom.height, *h_sel)) + f!((style, h_sel, _) model.selection_size(*h_sel, *style)) ); out.selection_position_target <+ all_with5( &out.is_header_selected, @@ -502,6 +537,12 @@ impl component::Frp for Frp { model.selection_position(*h_sel, *h_geom, *size, *esp, *h_pos) ) ); + out.selection_corners_radius <+ all_with3( + &out.is_header_selected, + &selection_style, + &out.focused, + f!((h_sel, style,_) model.selection_corners_radius(*h_sel, *style)) + ); } @@ -775,9 +816,18 @@ impl Model { } } - fn selection_size(&self, header_height: f32, is_header_selected: bool) -> Vector2 { - let height = if is_header_selected { header_height } else { list_view::entry::HEIGHT }; - Vector2(self.entries.size.value().x, height) + fn selection_size(&self, is_header_selected: bool, style: SelectionStyle) -> Vector2 { + let width = self.entries.size.value().x - style.padding_x * 2.0; + let height = if is_header_selected { style.header_height } else { style.height }; + Vector2(width, height) + } + + fn selection_corners_radius(&self, is_header_selected: bool, style: SelectionStyle) -> f32 { + if is_header_selected { + style.header_corners_radius + } else { + style.corners_radius + } } } diff --git a/app/gui/view/component-browser/component-group/src/set.rs b/app/gui/view/component-browser/component-group/src/set.rs index ed111cef169f..82c6aa4d71ed 100644 --- a/app/gui/view/component-browser/component-group/src/set.rs +++ b/app/gui/view/component-browser/component-group/src/set.rs @@ -13,7 +13,6 @@ use crate::wide; use crate::View; use enso_frp as frp; -use ensogl::data::OptVec; @@ -144,6 +143,7 @@ propagated_events! { header_accepted: GroupId, selection_position_target: (GroupId, Vector2), selection_size: (GroupId, Vector2), + selection_corners_radius: (GroupId, f32), focused: (GroupId, bool), } } @@ -153,14 +153,37 @@ propagated_events! { // === Wrapper === // =============== -newtype_prim! { - /// An index of the component group. - GroupId(usize); +/// A Component Groups List Section identifier. +#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)] +pub enum SectionId { + /// The "Favorite Tools" section. + #[default] + Favorites, + /// The "Local Scope" section. + LocalScope, + /// The "Sub-Modules" section. + SubModules, +} + +/// A Group identifier. If `section` is [`SectionId::LocalScope`], the `index` should be 0, as that +/// section has always only one group. +#[allow(missing_docs)] +#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct GroupId { + pub section: SectionId, + pub index: usize, +} + +impl GroupId { + /// Get id of the only group in "Local Scope" section. + pub fn local_scope_group() -> Self { + GroupId { section: SectionId::LocalScope, index: default() } + } } /// The storage for the groups inside Wrapper. We store both a [`Group`] itself to manage its focus /// and [`PropagatedEvents`] to propagate its FRP outputs. -type Groups = Rc>>; +type Groups = Rc>>; /// A wrapper around the FRP outputs of the several component groups. /// @@ -198,29 +221,40 @@ impl Wrapper { frp::extend! { network eval events.mouse_in_group((group_id) { - groups.borrow().iter_enumerate().for_each(|(idx, (g, _))| { - if idx != *group_id { g.defocus(); } + groups.borrow().iter().for_each(|(id, (g, _))| { + if id != group_id { g.defocus(); } }); - if let Some((g, _)) = groups.borrow().safe_index(*group_id) { g.focus(); } + if let Some((g, _)) = groups.borrow().get(group_id) { g.focus(); } }); } Self { groups, events } } - /// Start managing a new group. Returned [`GroupId`] is non-unique, it might be reused by new - /// groups if this one removed by calling [`Self::remove`]. - pub fn add(&self, group: Group) -> GroupId { + /// Start managing a new group. If there is already managed group under given id, the old group + /// is no longer managed, and it's returned. Returned [`GroupId`] is non-unique, it might be + /// reused by new groups if this one removed by calling [`Self::remove`]. + pub fn add(&self, id: GroupId, group: Group) -> Option { let events = PropagatedEvents::new(); self.events.attach(&events); - let id = self.groups.borrow_mut().insert((group.clone_ref(), events.clone_ref())); + let old = self.groups.borrow_mut().insert(id, (group.clone_ref(), events.clone_ref())); self.setup_frp_propagation(&group, id, events); - id + old.map(|(group, _)| group) + } + + /// Get a group by a [`GroupId`]. + pub fn get(&self, id: &GroupId) -> Option { + self.groups.borrow().get(id).map(|(group, _)| group).map(CloneRef::clone_ref) } /// Stop managing of a group. A freed [`GroupId`] might be reused by new groups later. pub fn remove(&self, group_id: GroupId) { - self.groups.borrow_mut().remove(group_id); + self.groups.borrow_mut().remove(&group_id); + } + + /// Stop managing all groups of a section. + pub fn remove_section(&self, section: SectionId) { + self.groups.borrow_mut().retain(|&id, _| id.section != section); } fn setup_frp_propagation(&self, group: &Group, id: GroupId, events: PropagatedEvents) { @@ -240,6 +274,7 @@ impl Wrapper { (expression_accepted, move |e| (id, *e)), (selection_position_target, move |p| (id, *p)), (selection_size, move |p| (id, *p)), + (selection_corners_radius, move |r| (id, *r)), (is_header_selected, move |h| (id, *h)), (header_accepted, move |_| id) } @@ -252,7 +287,9 @@ impl Wrapper { (selected_entry, move |e| e.map(|e| (id, e))), (suggestion_accepted, move |e| (id, *e)), (expression_accepted, move |e| (id, *e)), - (selection_position_target, move |p| (id, *p)) + (selection_position_target, move |p| (id, *p)), + (selection_size, move |p| (id, *p)), + (selection_corners_radius, move |r| (id, *r)) } } } diff --git a/app/gui/view/component-browser/component-group/src/wide.rs b/app/gui/view/component-browser/component-group/src/wide.rs index 5509c18c656c..59dc687f6a65 100644 --- a/app/gui/view/component-browser/component-group/src/wide.rs +++ b/app/gui/view/component-browser/component-group/src/wide.rs @@ -17,6 +17,7 @@ use crate::background; use crate::entry; use crate::theme; use crate::Colors; +use crate::SelectionStyle; use enso_frp as frp; use ensogl::application::shortcut::Shortcut; @@ -123,6 +124,7 @@ ensogl::define_endpoints_2! { /// the next non-empty column to the left. selection_position_target(Vector2), selection_size(Vector2), + selection_corners_radius(f32), entry_count(usize), size(Vector2), } @@ -140,6 +142,7 @@ impl component::Frp> for Frp { let out = &api.output; let colors = Colors::from_main_color(network, style, &input.set_color, &input.set_dimmed); let padding = style.get_number(theme::entry_list::padding); + let column_padding = style.get_number(theme::selection::wide_group_column_padding); frp::extend! { network init <- source_(); entry_count <- input.set_entries.map(|p| p.entry_count()); @@ -171,7 +174,20 @@ impl component::Frp> for Frp { eval size((size) model.background.size.set(*size)); eval size((size) model.selection_background.size.set(*size)); out.size <+ size; - out.selection_size <+ background_width.map(|&width| Vector2(width / 3.0,list_view::entry::HEIGHT)); + let selection_style = SelectionStyle::from_style(style, network); + out.selection_size <+ background_width.all_with4( + &selection_style, + &column_padding, + &out.focused, + |&width,&style,&column_padding,_| { + let width = (width - 2.0 * column_padding) / COLUMNS as f32; + let height = style.height; + Vector2(width, height) + } + ); + out.selection_corners_radius <+ all_with(&selection_style, &out.focused, + |style,_| style.corners_radius + ); // === "No items" label === diff --git a/app/gui/view/component-browser/searcher-list-panel/Cargo.toml b/app/gui/view/component-browser/searcher-list-panel/Cargo.toml index 2fb170fd42ee..ceb825e430f7 100644 --- a/app/gui/view/component-browser/searcher-list-panel/Cargo.toml +++ b/app/gui/view/component-browser/searcher-list-panel/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "searcher-list-panel" +name = "ide-view-component-list-panel" version = "0.1.0" authors = ["Enso Team "] edition = "2021" diff --git a/app/gui/view/component-browser/searcher-list-panel/src/column_grid.rs b/app/gui/view/component-browser/searcher-list-panel/src/column_grid.rs index 5900a6c8f1d0..8e5f29429951 100644 --- a/app/gui/view/component-browser/searcher-list-panel/src/column_grid.rs +++ b/app/gui/view/component-browser/searcher-list-panel/src/column_grid.rs @@ -5,18 +5,22 @@ use ensogl_core::display::shape::*; use ensogl_core::prelude::*; +use crate::searcher_theme; +use crate::GroupId; use crate::Layers; +use crate::SectionId; + use enso_frp as frp; use ensogl_core::application::frp::API; use ensogl_core::application::Application; use ensogl_core::data::color; use ensogl_core::define_endpoints_2; use ensogl_core::display; -use ensogl_core::display::style; use ensogl_gui_component::component; use ensogl_list_view as list_view; use ensogl_scroll_area as scroll_area; use ide_view_component_group as component_group; +use ide_view_component_group::set::Group; use ordered_float::OrderedFloat; @@ -38,7 +42,7 @@ struct Entry { #[derive(Clone, Debug, Default)] pub struct LabeledAnyModelProvider { /// Label of the data provided to be used as a header of the list. - pub label: String, + pub label: ImString, /// Content to be used to populate a list. pub content: list_view::entry::AnyModelProvider, } @@ -67,7 +71,15 @@ impl Model { Self { app, display_object, content: default(), size: default(), layers: default() } } - fn update_content_layout(&self, content: &[LabeledAnyModelProvider], style: &Style) -> Vector2 { + fn update_content_layout( + &self, + content: &[LabeledAnyModelProvider], + style: &Style, + (section, group_wrapper): &(SectionId, component_group::set::Wrapper), + ) -> Vector2 { + // Ensure we do not keept the old entries in the group_wrapper. + group_wrapper.remove_section(*section); + const NUMBER_OF_COLUMNS: usize = 3; let overall_width = style.content_width - 2.0 * style.content_padding; let column_width = (overall_width - 2.0 * style.column_gap) / NUMBER_OF_COLUMNS as f32; @@ -86,6 +98,10 @@ impl Model { view.set_entries(content); view.set_header(label.as_str()); self.display_object.add_child(&view); + group_wrapper.add( + GroupId { section: *section, index }, + Group::OneColumn(view.clone_ref()), + ); Some(Entry { index, content: view, visible: false }) } else { None @@ -205,6 +221,7 @@ impl Style { define_endpoints_2! { Input{ + set_group_wrapper((SectionId, component_group::set::Wrapper)), set_content(Vec), set_scroll_viewport(scroll_area::Viewport), } @@ -218,21 +235,19 @@ fn get_layout( network: &enso_frp::Network, style: &StyleWatchFrp, ) -> (enso_frp::Stream