Breadcrumbs integration (#3720)

[ci no changelog needed]
[Task link](https://www.pivotaltracker.com/story/show/182675703)

This PR implements the actual integration of the breadcrumbs panel with the component list panel. A special breadcrumbs controller (`controller::searcher::breadcrumbs`) is tracking the currently opened module with a list of its parents. The searcher presenter uses the API of the controller to sync the displayed list of breadcrumbs with the controller state.


https://user-images.githubusercontent.com/6566674/193064122-7d3fc4d6-9148-4ded-a73e-767ac9ac83f8.mp4

# Important Notes
- There is an `All` breadcrumb displayed at all times at the beginning of the list. It will be replaced with a section name as part of [Section Title on Component Browser's Breadcrumbs Panel](https://www.pivotaltracker.com/story/show/182610561) task.
- I changed the implementation of `project::main_module_id`, `project::QualifiedName::main_module`, and `API::main_module` so that they are logically connected which each other.
- I adjusted the Breadcrumbs View to avoid "appearance" animation glitches when opening new modules. `set_entries` was replaced with the `set_entries_from` endpoint.
This commit is contained in:
Ilya Bogdanov 2022-10-03 13:54:09 +03:00 committed by GitHub
parent 11acad5cff
commit 0d74ab6124
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 389 additions and 99 deletions

View File

@ -91,8 +91,6 @@ impl Display for Id {
impl Id {
/// Construct a module's ID value from a name segments sequence.
///
/// Fails if the given sequence is empty.
pub fn new(segments: impl IntoIterator<Item = ReferentName>) -> Id {
let segments = segments.into_iter().collect_vec();
Id { segments }
@ -100,7 +98,7 @@ impl Id {
/// Construct a module's ID value from a name segments sequence.
///
/// Fails if the sequence is empty or if any of the segments is not a valid referent name.
/// Fails if any of the segments is not a valid referent name.
pub fn try_new(segments: impl IntoIterator<Item: AsRef<str>>) -> FallibleResult<Id> {
let texts = segments.into_iter();
let names = texts.map(|text| ReferentName::new(text.as_ref()));
@ -175,11 +173,8 @@ impl QualifiedName {
}
/// Create a qualified name for the project's main module.
///
/// It is special, as its name consists only from the project name, unlike other modules'
/// qualified names.
pub fn new_main(project_name: project::QualifiedName) -> QualifiedName {
Self::new(project_name, Id::new(std::iter::empty()))
Self::new(project_name, project::main_module_id())
}
/// Constructs a qualified name from its text representation.
@ -295,7 +290,16 @@ impl QualifiedName {
/// Check if the name refers to some library's top module.
pub fn is_top_module(&self) -> bool {
self.id.segments.len() == 1
self.id.segments.len() <= 1
}
/// Check if the name refers to some project's Main module.
pub fn is_main_module(&self) -> bool {
match self.id.segments.len() {
0 => true,
1 if self.id.segments[0] == PROJECTS_MAIN_MODULE => true,
_ => false,
}
}
/// Get the top module containing the module referred by this name. Return self if it is already
@ -307,9 +311,24 @@ impl QualifiedName {
/// Get the parent module of the module referred by this name. Returns [`None`] if it is a top
/// module.
pub fn parent_module(&self) -> Option<Self> {
let id = Id::try_new(self.id.parent_segments()).ok()?;
let project_name = self.project_name.clone();
Some(Self { project_name, id })
if self.is_top_module() {
None
} else {
let id = Id::try_new(self.id.parent_segments()).ok()?;
let project_name = self.project_name.clone();
Some(Self { project_name, id })
}
}
/// Returns an iterator over all parent modules. The `self` is not included.
pub fn parent_modules(&self) -> impl Iterator<Item = Self> {
let mut current = self.clone();
iter::from_fn(move || {
current.parent_module().map(|parent| {
current = parent.clone();
parent
})
})
}
}

View File

@ -23,7 +23,11 @@ pub const BASE_LIBRARY_NAME: &str = "Base";
/// The full path of the [`BASE_LIBRARY_NAME`] project in the [`STANDARD_NAMESPACE`].
pub const STANDARD_BASE_LIBRARY_PATH: &str = concatcp!(STANDARD_NAMESPACE, ".", BASE_LIBRARY_NAME);
/// The identifier of the project's main module.
pub fn main_module_id() -> crate::module::Id {
// We can just assume that `PROJECTS_MAIN_MODULE` is valid. This is verified by a test.
crate::module::Id::try_new([ast::constants::PROJECTS_MAIN_MODULE]).unwrap()
}
// ==============
// === Errors ===
@ -179,4 +183,10 @@ mod test {
fn qualified_name_of_standard_base_library_does_not_panic() {
let _ = QualifiedName::standard_base_library();
}
#[test]
fn main_module_id_test() {
// Should not panic.
main_module_id();
}
}

View File

@ -22,12 +22,6 @@ use parser::Parser;
/// The label of compiling stdlib message process.
pub const COMPILING_STDLIB_LABEL: &str = "Compiling standard library. It can take up to 1 minute.";
/// The name of the module initially opened in the project view.
///
/// Currently, this name is hardcoded in the engine services and is populated for each project
/// created using engine's Project Picker service.
pub const INITIAL_MODULE_NAME: &str = "Main";
/// Name of the main definition.
///
/// This is the definition whose graph will be opened on IDE start.
@ -52,12 +46,6 @@ pub fn main_method_ptr(
module_path.method_pointer(project_name, MAIN_DEFINITION_NAME)
}
/// The identifier of the project's main module.
pub fn main_module_id() -> model::module::Id {
// We can just assume that `INITIAL_MODULE_NAME` is valid. This is verified by a test.
model::module::Id::try_new([INITIAL_MODULE_NAME]).unwrap()
}
// =================
@ -124,7 +112,7 @@ impl Project {
pub async fn initialize(&self) -> FallibleResult<InitializationResult> {
let project = self.model.clone_ref();
let parser = self.model.parser();
let module_path = self.initial_module_path()?;
let module_path = self.initial_module_path();
let file_path = module_path.file_path().clone();
// TODO [mwu] This solution to recreate missing main file should be considered provisional
@ -161,7 +149,7 @@ impl Project {
impl Project {
/// Returns the path to the initially opened module in the given project.
fn initial_module_path(&self) -> FallibleResult<model::module::Path> {
fn initial_module_path(&self) -> model::module::Path {
crate::ide::initial_module_path(&self.model)
}
@ -262,12 +250,6 @@ mod tests {
enso_config::engine_version_requirement();
}
#[test]
fn main_module_id_test() {
// Should not panic.
main_module_id();
}
#[test]
fn new_project_engine_version_fills_requirements() {
let requirements = enso_config::engine_version_requirement();

View File

@ -616,7 +616,7 @@ impl Searcher {
/// The list of modules and their content displayed in `Submodules` section of the browser.
pub fn top_modules(&self) -> group::AlphabeticalList {
let components = self.components();
if let Some(selected) = self.breadcrumbs.currently_selected() {
if let Some(selected) = self.breadcrumbs.selected() {
components.submodules_of(selected).map(CloneRef::clone_ref).unwrap_or_default()
} else {
components.top_modules().clone_ref()
@ -637,7 +637,7 @@ impl Searcher {
/// The list of components displayed in `Local Scope` section of the browser.
pub fn local_scope(&self) -> group::Group {
let components = self.components();
if let Some(selected) = self.breadcrumbs.currently_selected() {
if let Some(selected) = self.breadcrumbs.selected() {
components.get_module_content(selected).map(CloneRef::clone_ref).unwrap_or_default()
} else {
components.local_scope
@ -646,7 +646,31 @@ impl Searcher {
/// Enter the specified module. The displayed content of the browser will be updated.
pub fn enter_module(&self, module: &component::Id) {
self.breadcrumbs.push(*module);
let builder = breadcrumbs::Builder::new(&self.database, self.components());
let breadcrumbs = builder.build(module);
self.breadcrumbs.set_content(breadcrumbs);
self.notifier.notify(Notification::NewActionList);
}
/// Whether the last module in the breadcrumbs list contains more descendants or not.
pub fn last_module_has_submodules(&self) -> bool {
let last_module = self.breadcrumbs.last();
let components = self.components();
let get_submodules = |module| components.submodules_of(module).map(CloneRef::clone_ref);
let submodules = last_module.and_then(get_submodules);
submodules.map_or(false, |submodules| !submodules.is_empty())
}
/// A list of breadcrumbs' text labels to be displayed. The list is updated by
/// [`Self::enter_module`].
pub fn breadcrumbs(&self) -> Vec<ImString> {
self.breadcrumbs.names()
}
/// Select the breadcrumb with the index [`id`]. The displayed content of the browser will be
/// updated.
pub fn select_breadcrumb(&self, id: usize) {
self.breadcrumbs.select(id);
self.notifier.notify(Notification::NewActionList);
}

View File

@ -4,6 +4,10 @@ use crate::prelude::*;
use crate::controller::searcher::component;
use double_representation::module;
use model::suggestion_database::entry::QualifiedName;
use model::suggestion_database::Entry;
// ===================
@ -11,13 +15,12 @@ use crate::controller::searcher::component;
// ===================
/// A controller that keeps the path of entered modules in the Searcher and provides the
/// functionality of the breadcrumbs panel.
///
/// TODO: The actual implementation would be finished in
/// [Breadcrumbs Panel integration task](https://www.pivotaltracker.com/story/show/182675703).
/// functionality of the breadcrumbs panel. The integration between the
/// controller and the view is done by the [searcher presenter](crate::presenter::searcher).
#[derive(Debug, Clone, CloneRef, Default)]
pub struct Breadcrumbs {
currently_selected: Rc<Cell<Option<component::Id>>>,
list: Rc<RefCell<Vec<BreadcrumbEntry>>>,
selected: Rc<Cell<usize>>,
}
impl Breadcrumbs {
@ -26,18 +29,151 @@ impl Breadcrumbs {
default()
}
/// Push the new breadcrumb to the breadcrumbs panel.
pub fn push(&self, id: component::Id) {
self.currently_selected.set(Some(id));
/// Set the list of breadcrumbs to be displayed in the breadcrumbs panel.
pub fn set_content(&self, breadcrumbs: impl Iterator<Item = BreadcrumbEntry>) {
let mut borrowed = self.list.borrow_mut();
*borrowed = breadcrumbs.collect();
self.select(borrowed.len());
}
/// Returns true if the currently selected breadcrumb is the root one.
/// A list of breadcrumbs' text labels to be displayed in the panel.
pub fn names(&self) -> Vec<ImString> {
self.list.borrow().iter().map(|entry| entry.name()).collect()
}
/// The last (right-most) breadcrumb in the list.
pub fn last(&self) -> Option<component::Id> {
self.list.borrow().last().map(BreadcrumbEntry::id)
}
/// Mark the entry with the given index as selected.
pub fn select(&self, id: usize) {
self.selected.set(id);
}
/// Returns true if the currently selected breadcrumb is the first one.
pub fn is_top_module(&self) -> bool {
self.currently_selected.get().is_none()
self.selected.get() == 0
}
/// Returns a currently selected breadcrumb id.
pub fn currently_selected(&self) -> Option<component::Id> {
self.currently_selected.get()
/// Returns a currently selected breadcrumb id. Returns [`None`] if the top level breadcrumb
/// is selected.
pub fn selected(&self) -> Option<component::Id> {
if self.is_top_module() {
None
} else {
let index = self.selected.get();
self.list.borrow().get(index - 1).map(BreadcrumbEntry::id)
}
}
}
// =======================
// === BreadcrumbEntry ===
// =======================
/// A single entry in the breadcrumbs panel.
#[derive(Debug, Clone)]
pub struct BreadcrumbEntry {
displayed_name: ImString,
component_id: component::Id,
qualified_name: QualifiedName,
}
impl BreadcrumbEntry {
/// A displayed label of the entry.
pub fn name(&self) -> ImString {
self.displayed_name.clone_ref()
}
/// A component id of the entry.
pub fn id(&self) -> component::Id {
self.component_id
}
/// A qualified name of the entry.
pub fn qualified_name(&self) -> &QualifiedName {
&self.qualified_name
}
}
impl From<(component::Id, Rc<Entry>)> for BreadcrumbEntry {
fn from((component_id, entry): (component::Id, Rc<Entry>)) -> Self {
let qualified_name = entry.qualified_name();
let displayed_name = ImString::new(&entry.name);
BreadcrumbEntry { displayed_name, component_id, qualified_name }
}
}
// ===============
// === Builder ===
// ===============
/// A builder for the breadcrumbs list. It is used to include all parent modules when pushing the
/// new breadcrumb to the panel.
#[derive(Debug)]
pub struct Builder<'a> {
database: &'a model::SuggestionDatabase,
components: component::List,
}
impl<'a> Builder<'a> {
/// Constructor.
pub fn new(database: &'a model::SuggestionDatabase, components: component::List) -> Self {
Self { database, components }
}
/// Build a list of breadcrumbs for a specified module. The list will contain:
/// 1. The main module of the project.
/// 2. All parent modules of the [`module`].
/// 3. The [`module`] itself.
///
/// Returns an empty vector if the [`module`] is not found in the database or in the
/// components list.
pub fn build(self, module: &component::Id) -> Box<dyn Iterator<Item = BreadcrumbEntry>> {
let (module_name, entry) = match self.module_name_and_entry(module) {
Some(name_and_entry) => name_and_entry,
None => return Box::new(iter::empty()),
};
let project_name = module_name.project_name.clone();
let main_module_name = module::QualifiedName::new_main(project_name.clone());
let main_module = self.lookup(&main_module_name);
let to_main_module_entry = |entry: (component::Id, Rc<Entry>)| BreadcrumbEntry {
displayed_name: String::from(project_name.project).into(),
..entry.into()
};
let main_module = main_module.map(to_main_module_entry).into_iter();
let parents = self.collect_parents(&module_name);
let iter = iter::once(entry).chain(parents).chain(main_module).rev();
Box::new(iter)
}
fn module_name_and_entry(
&self,
module: &component::Id,
) -> Option<(Rc<module::QualifiedName>, BreadcrumbEntry)> {
let module_name = self.components.module_qualified_name(*module)?;
let entry = BreadcrumbEntry::from(self.lookup(&module_name)?);
Some((module_name, entry))
}
fn lookup(&self, name: &module::QualifiedName) -> Option<(component::Id, Rc<Entry>)> {
self.database.lookup_by_qualified_name(name.into_iter())
}
/// Collect all parent modules of the given module.
///
/// Panics if the module is not found in the database.
fn collect_parents(&self, name: &module::QualifiedName) -> Vec<BreadcrumbEntry> {
let parents = name.parent_modules();
let database_entries = parents.filter_map(|name| self.lookup(&name));
// Note: it would be nice to avoid allocation here, but we need to reverse the
// iterator later, so returning `impl Iterator` is not an option. We can only reverse
// `DoubleEndedIterator`.
database_entries.map(BreadcrumbEntry::from).collect()
}
}

View File

@ -8,6 +8,7 @@ use crate::model::suggestion_database;
use convert_case::Case;
use convert_case::Casing;
use double_representation::module;
// ==============
@ -201,8 +202,9 @@ impl Display for Component {
#[allow(missing_docs)]
#[derive(Clone, CloneRef, Debug)]
pub struct ModuleGroups {
pub content: Group,
pub submodules: group::AlphabeticalList,
pub qualified_name: Rc<module::QualifiedName>,
pub content: Group,
pub submodules: group::AlphabeticalList,
}
@ -257,6 +259,11 @@ impl List {
self.module_groups.get(&component).map(|mg| &mg.content)
}
/// Get the qualified name of the module. Returns [`None`] if given component is not a module.
pub fn module_qualified_name(&self, component: Id) -> Option<Rc<module::QualifiedName>> {
self.module_groups.get(&component).map(|mg| mg.qualified_name.clone_ref())
}
/// Update matching info in all components according to the new filtering pattern.
pub fn update_filtering(&self, pattern: impl AsRef<str>) {
let pattern = pattern.as_ref();

View File

@ -40,6 +40,7 @@ use double_representation::project;
#[allow(missing_docs)]
#[derive(Clone, Debug)]
pub struct ModuleGroups {
pub qualified_name: module::QualifiedName,
pub content: component::Group,
/// The flattened content contains the content of a module and all its submodules. Is set to
/// `Some` only when such flattened content is needed (decided during construction).
@ -55,20 +56,33 @@ impl ModuleGroups {
/// Construct the builder without content nor submodules.
///
/// The existence of flattened content is decided during construction.
pub fn new(component_id: component::Id, entry: &suggestion_database::Entry) -> Self {
///
/// Returns [`FallibleResult::Err`] if entry's qualified name can't be converted into a module
/// name.
pub fn new(
component_id: component::Id,
entry: &suggestion_database::Entry,
) -> FallibleResult<Self> {
let is_top_module = entry.module.is_top_module();
let qualified_name = entry.qualified_name();
let qualified_name = module::QualifiedName::from_all_segments(qualified_name.into_iter())?;
let mk_group = || component::Group::from_entry(component_id, entry);
Self {
Ok(Self {
qualified_name,
content: mk_group(),
flattened_content: is_top_module.as_some_from(mk_group),
submodules: default(),
is_top_module,
}
})
}
/// Build [`component::ModuleGroups`] structure with appropriately sorted submodules.
pub fn build(self) -> component::ModuleGroups {
component::ModuleGroups { content: self.content, submodules: self.submodules.build() }
component::ModuleGroups {
qualified_name: Rc::new(self.qualified_name),
content: self.content,
submodules: self.submodules.build(),
}
}
}
@ -141,6 +155,17 @@ impl List {
component_inserted_somewhere = true;
}
}
} else {
// Entry has no parent module, so either it belongs to the main module of the
// project, or it is a main module itself.
if !entry.is_main_module() {
let project_name = entry.module.project_name.clone();
let main_module = module::QualifiedName::new_main(project_name);
if let Some(main_group) = self.lookup_module_group(db, &main_module) {
main_group.content.entries.borrow_mut().push(component.clone_ref());
component_inserted_somewhere = true;
}
}
}
if component_inserted_somewhere {
self.all_components.push(component);
@ -203,11 +228,20 @@ impl List {
if self.module_groups.contains_key(&module_id) {
self.module_groups.get_mut(&module_id)
} else {
let groups = ModuleGroups::new(module_id, &*db_entry);
let groups = ModuleGroups::new(module_id, &*db_entry).ok()?;
if let Some(module) = module.parent_module() {
if let Some(parent_groups) = self.lookup_module_group(db, &module) {
parent_groups.submodules.push(groups.content.clone_ref())
}
} else {
// Module has no parent, so it is a top-level module that can be added as a
// submodule of the main module of the project.
let main_module = module::QualifiedName::new_main(module.project_name.clone());
if main_module != *module {
if let Some(main_groups) = self.lookup_module_group(db, &main_module) {
main_groups.submodules.push(groups.content.clone_ref());
}
}
}
Some(self.module_groups.entry(module_id).or_insert(groups))
}

View File

@ -2,7 +2,6 @@
use crate::prelude::*;
use crate::controller::project::INITIAL_MODULE_NAME;
use crate::presenter::Presenter;
use analytics::AnonymousData;
@ -104,8 +103,6 @@ pub struct FailedIde {
/// The Path of the module initially opened after opening project in IDE.
pub fn initial_module_path(project: &model::Project) -> FallibleResult<model::module::Path> {
model::module::Path::from_name_segments(project.project_content_root_id(), &[
INITIAL_MODULE_NAME,
])
pub fn initial_module_path(project: &model::Project) -> model::module::Path {
project.main_module_path()
}

View File

@ -100,27 +100,22 @@ pub trait API: Debug {
///
/// This module is special, as it needs to be referred by the project name itself.
fn main_module(&self) -> model::module::QualifiedName {
let id = controller::project::main_module_id();
let name = self.qualified_name();
model::module::QualifiedName::new(name, id)
model::module::QualifiedName::new_main(name)
}
// TODO [mwu] The code below likely should be preferred but does not work
// because language server does not support using project name
// for project's main module in some contexts.
// This is tracked by: https://github.com/enso-org/enso/issues/1543
// use model::module::QualifiedName;
// ReferentName::try_from(self.name().as_str())
// .map(QualifiedName::new_main)
// .map_err(Into::into)
/// Get the file path of the project's `Main` module.
fn main_module_path(&self) -> model::module::Path {
let main_name = self.main_module();
let content_root_id = self.project_content_root_id();
model::module::Path::from_id(content_root_id, &main_name.id)
}
/// Get a model of the project's main module.
#[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes
fn main_module_model<'a>(&'a self) -> BoxFuture<'a, FallibleResult<model::Module>> {
async move {
let main_name = self.main_module();
let content_root_id = self.project_content_root_id();
let main_path = model::module::Path::from_id(content_root_id, &main_name.id);
let main_path = self.main_module_path();
self.module(main_path).await
}
.boxed_local()

View File

@ -378,6 +378,14 @@ impl Entry {
_ => Some(self.module.clone()),
}
}
/// Returns true if this entry is a main module of the project.
pub fn is_main_module(&self) -> bool {
match self.kind {
Kind::Module => self.module.is_main_module(),
_ => false,
}
}
}

View File

@ -19,6 +19,7 @@ use crate::presenter::graph::ViewNodeId;
use enso_frp as frp;
use ide_view as view;
use ide_view::component_browser::list_panel;
use ide_view::component_browser::list_panel::BreadcrumbId;
use ide_view::component_browser::list_panel::EnteredModule;
use ide_view::component_browser::list_panel::LabeledAnyModelProvider;
use ide_view::graph_editor::component::node as node_view;
@ -190,24 +191,47 @@ impl Model {
}
}
fn breadcrumb_selected(&self, id: BreadcrumbId) {
self.controller.select_breadcrumb(id);
}
fn set_breadcrumbs(&self, names: impl Iterator<Item = ImString>) {
if let SearcherVariant::ComponentBrowser(browser) = self.view.searcher() {
// We only update the breadcrumbs starting from the second element because the first
// one is reserved as a section name.
let from = 1;
let breadcrumbs_from = (names.map(Into::into).collect(), from);
browser.model().list.set_breadcrumbs_from(breadcrumbs_from);
}
}
fn show_breadcrumbs_ellipsis(&self, show: bool) {
if let SearcherVariant::ComponentBrowser(browser) = self.view.searcher() {
browser.model().list.show_breadcrumbs_ellipsis(show);
}
}
fn module_entered(&self, module: EnteredModule) {
self.enter_module(module);
}
fn enter_module(&self, module: EnteredModule) -> Option<()> {
match module {
let id = match module {
EnteredModule::Entry(group, entry_id) => {
let view_id = list_panel::EntryId { group, entry_id };
let component = self.component_by_view_id(view_id)?;
let id = component.id()?;
self.controller.enter_module(&id);
component.id()?
}
EnteredModule::Group(group_id) => {
let group = self.group_by_view_id(group_id)?;
let id = group.component_id?;
self.controller.enter_module(&id);
group.component_id?
}
}
};
self.controller.enter_module(&id);
let names = self.controller.breadcrumbs();
self.set_breadcrumbs(names.into_iter());
let show_ellipsis = self.controller.last_module_has_submodules();
self.show_breadcrumbs_ellipsis(show_ellipsis);
Some(())
}
@ -375,6 +399,7 @@ impl Searcher {
eval_ list_view.suggestion_accepted([]analytics::remote_log_event("component_browser::suggestion_accepted"));
eval list_view.suggestion_selected((entry) model.suggestion_selected(*entry));
eval list_view.module_entered((id) model.module_entered(*id));
eval list_view.selected_breadcrumb((id) model.breadcrumb_selected(*id));
}
}
SearcherVariant::OldNodeSearcher(searcher) => {

View File

@ -355,7 +355,7 @@ async fn binary_visualization_updates_test_hlp() {
use ensogl::system::web::sleep;
let logger = Logger::new("Test");
let module_path = enso_gui::initial_module_path(&project).unwrap();
let module_path = enso_gui::initial_module_path(&project);
let method = module_path.method_pointer(project.qualified_name(), MAIN_DEFINITION_NAME);
let module_qualified_name = project.qualified_module_name(&module_path);
let module = project.module(module_path).await.unwrap();

View File

@ -201,9 +201,14 @@ impl EntryData {
self.text.set_default_text_size(size);
}
fn is_state_switch(&self, model: &Model) -> bool {
fn is_state_change(&self, model: &Model) -> bool {
match model {
Model::Text(_) => self.state.get() != State::Text,
Model::Text(new_text) => {
let previous_state_was_not_text = self.state.get() != State::Text;
let previous_text = String::from(self.text.content.value());
let text_was_different = previous_text.as_str() != new_text.as_str();
previous_state_was_not_text || text_was_different
}
Model::Separator => self.state.get() != State::Separator,
Model::Ellipsis => self.state.get() != State::Ellipsis,
}
@ -284,7 +289,7 @@ impl ensogl_grid_view::Entry for Entry {
color_anim.target <+ should_grey_out.map(|should| if *should { 1.0 } else { 0.0 });
target_color <- all_with3(&text_color, &greyed_out_color, &color_anim.value, mix);
appear_anim.target <+ init.constant(1.0);
model_was_set <- input.set_model.map(f!((model) data.is_state_switch(model))).on_true();
model_was_set <- input.set_model.map(f!((model) data.is_state_change(model))).on_true();
should_appear <- any(&init, &model_was_set);
eval_ should_appear({
appear_anim.target.emit(0.0);

View File

@ -84,7 +84,8 @@ const SCROLLING_THRESHOLD_FRACTION: f32 = 0.5;
type GridView = grid_view::selectable::GridView<Entry>;
type Entries = Rc<RefCell<Vec<Breadcrumb>>>;
type BreadcrumbId = usize;
/// The index of the breadcrumb in the list.
pub type BreadcrumbId = usize;
@ -346,12 +347,22 @@ impl Model {
self.grid.set_entries_params(params);
}
/// Push a new breadcrumb to the top of the stack. Immediately selects added breadcrumb.
/// A newly added breadcrumb will be placed after the currently selected one. All inactive
/// (greyed out) breadcrumbs will be removed.
pub fn push(&self, breadcrumb: &Breadcrumb, selected: BreadcrumbId) {
self.entries.borrow_mut().truncate(selected + 1);
self.entries.borrow_mut().push(breadcrumb.clone_ref());
/// Set the breadcrumbs starting from the [`starting_from`] index. Existing entries after
/// [`starting_from`] will be overwritten. [`self.entries`] will be extended if needed to fit
/// all added entries.
/// Immediately selects the last breadcrumb. All inactive (greyed out) breadcrumbs will be
/// removed.
pub fn set_entries(&self, starting_from: usize, new_entries: &[Breadcrumb]) {
{
let mut borrowed = self.entries.borrow_mut();
let end_of_overwritten_entries = starting_from + new_entries.len();
borrowed.truncate(end_of_overwritten_entries);
let len = borrowed.len();
let count_to_overwrite = len.saturating_sub(starting_from);
let range_to_overwrite = starting_from..len;
borrowed[range_to_overwrite].clone_from_slice(&new_entries[..count_to_overwrite]);
borrowed.extend(new_entries.iter().map(CloneRef::clone_ref).skip(count_to_overwrite));
}
let new_col_count = self.grid_columns();
self.grid.resize_grid(1, new_col_count);
self.grid.request_model_for_visible_entries();
@ -360,6 +371,13 @@ impl Model {
}
}
/// Push a new breadcrumb to the top of the stack. Immediately selects added breadcrumb.
/// A newly added breadcrumb will be placed after the currently selected one. All inactive
/// (greyed out) breadcrumbs will be removed.
pub fn push(&self, breadcrumb: &Breadcrumb) {
self.set_entries(self.entries.borrow().len(), &[breadcrumb.clone_ref()]);
}
/// Move the selection to the previous breadcrumb. Stops at the first one. There is always at
/// least one breadcrumb selected.
pub fn move_up(&self) {
@ -412,8 +430,8 @@ ensogl_core::define_endpoints_2! {
select(BreadcrumbId),
/// Add a new breadcrumb after the currently selected one.
push(Breadcrumb),
/// Set a list of displayed breadcrumbs, rewriting any previously added breadcrumbs.
set_entries(Vec<Breadcrumb>),
/// Set the displayed breadcrumbs starting from the specific index.
set_entries_from((Vec<Breadcrumb>, usize)),
/// Enable or disable displaying of the ellipsis icon at the end of the list.
show_ellipsis(bool),
/// Remove all breadcrumbs.
@ -465,10 +483,8 @@ impl Breadcrumbs {
eval selected_grid_col(((_row, col)) model.grey_out(Some(col + 1)));
eval_ input.clear(model.clear());
selected <- selected_grid_col.map(|(_, col)| col / 2);
_eval <- input.push.map2(&selected, f!((b, s) model.push(b, *s)));
entries <= input.set_entries.map(f!((e) { model.clear(); e.clone() }));
_eval <- entries.map2(&selected, f!((entry, s) model.push(entry, *s)));
eval selected([](id) tracing::debug!("Selected breadcrumb: {id}"));
eval input.push((b) model.push(b));
eval input.set_entries_from(((entries, from)) model.set_entries(*from, entries));
out.selected <+ selected;
scroll_anim.target <+ all_with3(&model.grid.content_size, &input.set_size, &model.grid

View File

@ -95,6 +95,7 @@ pub mod layouting;
mod navigator;
pub use breadcrumbs::BreadcrumbId;
pub use column_grid::LabeledAnyModelProvider;
pub use component_group::set::EnteredModule;
pub use component_group::set::GroupId;
@ -463,7 +464,6 @@ impl Model {
breadcrumbs.set_base_layer(&layers.navigator);
display_object.add_child(&breadcrumbs);
breadcrumbs.show_ellipsis(true);
breadcrumbs.set_entries(vec![breadcrumbs::Breadcrumb::new("All")]);
let selection = selection_box::View::new(&app.logger);
scroll_area.add_child(&selection);
@ -512,6 +512,11 @@ impl Model {
LabeledSection::new(content, app)
}
fn set_initial_breadcrumbs(&self) {
self.breadcrumbs.set_entries_from((vec![breadcrumbs::Breadcrumb::new("All")], 0));
self.breadcrumbs.show_ellipsis(true);
}
fn update_style(&self, style: &Style) {
// Background
@ -935,6 +940,10 @@ define_endpoints_2! {
set_local_scope_section(list_view::entry::AnyModelProvider<component_group::Entry>),
set_favourites_section(Vec<LabeledAnyModelProvider>),
set_sub_modules_section(Vec<LabeledAnyModelProvider>),
/// See [`breadcrumbs::Breadcrumb::Frp::set_entries_from`].
set_breadcrumbs_from((Vec<breadcrumbs::Breadcrumb>, usize)),
/// Show or hide the ellipsis at the end of the breadcrumbs list.
show_breadcrumbs_ellipsis(bool),
/// The component browser is displayed on screen.
show(),
/// The component browser is hidden from screen.
@ -948,6 +957,7 @@ define_endpoints_2! {
/// The last selected suggestion.
suggestion_selected(EntryId),
size(Vector2),
selected_breadcrumb(BreadcrumbId),
}
}
@ -1069,6 +1079,14 @@ impl component::Frp<Model> for Frp {
let weak_color = style.get_color(list_panel_theme::navigator_icon_weak_color);
let params = icon::Params { strong_color, weak_color };
model.section_navigator.set_bottom_buttons_entry_params(params);
// === Breadcrumbs ===
model.breadcrumbs.set_entries_from <+ input.set_breadcrumbs_from;
model.breadcrumbs.show_ellipsis <+ input.show_breadcrumbs_ellipsis;
output.selected_breadcrumb <+ model.breadcrumbs.selected;
eval_ input.show(model.set_initial_breadcrumbs());
}
layout_frp.init.emit(());
selection_animation.skip.emit(());

View File

@ -1,6 +1,6 @@
# Options intended to be common for all developers.
wasm-size-limit: 14.79 MiB
wasm-size-limit: 14.80 MiB
required-versions:
cargo-watch: ^8.1.1

View File

@ -76,10 +76,18 @@ impl ColumnWidths {
/// `colunm` can be equal to `number_of_columns` passed to the [`Self::new`] or
/// [`Self::resize`].
pub fn pos_offset(&self, column: usize) -> f32 {
let borrowed = self.width_diffs.borrow();
let len = borrowed.len();
if column == 0 {
0.0
} else if column > len {
tracing::warn!(
"Trying to get a position offset of a column that does not exist. \
{column} > {len}. Returning 0.0."
);
0.0
} else {
self.width_diffs.borrow().query(column - 1)
borrowed.query(column - 1)
}
}

View File

@ -304,6 +304,12 @@ macro_rules! im_string_newtype_without_serde {
}
}
impl From<ImString> for $name {
fn from(t:ImString) -> Self {
Self::new(t)
}
}
impl From<&str> for $name {
fn from(t:&str) -> Self {
Self::new(t)