Sync breadcrumbs and documentation panel. (#7508)

Implements #7310.

![Peek 2023-08-09 16-07](https://github.com/enso-org/enso/assets/1428930/1a244e38-5c34-4c8b-8885-1cf84ac7b6a7)
This commit is contained in:
Michael Mauderer 2023-08-15 12:01:24 +01:00 committed by GitHub
parent 7a272ec152
commit 7f19b09d13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 192 additions and 109 deletions

31
Cargo.lock generated
View File

@ -2203,6 +2203,7 @@ dependencies = [
"enso-text", "enso-text",
"enso-web", "enso-web",
"ensogl", "ensogl",
"ensogl-breadcrumbs",
"ensogl-component", "ensogl-component",
"ensogl-drop-manager", "ensogl-drop-manager",
"ensogl-dynamic-assets", "ensogl-dynamic-assets",
@ -2629,6 +2630,19 @@ dependencies = [
"ensogl-text", "ensogl-text",
] ]
[[package]]
name = "ensogl-breadcrumbs"
version = "0.1.0"
dependencies = [
"enso-frp",
"ensogl-core",
"ensogl-derive-theme",
"ensogl-grid-view",
"ensogl-hardcoded-theme",
"ensogl-icons",
"ensogl-text",
]
[[package]] [[package]]
name = "ensogl-button" name = "ensogl-button"
version = "0.1.0" version = "0.1.0"
@ -4328,11 +4342,11 @@ dependencies = [
"enso-frp", "enso-frp",
"enso-prelude", "enso-prelude",
"ensogl", "ensogl",
"ensogl-breadcrumbs",
"ensogl-gui-component", "ensogl-gui-component",
"ensogl-hardcoded-theme", "ensogl-hardcoded-theme",
"ensogl-text", "ensogl-text",
"ide-view-component-list-panel", "ide-view-component-list-panel",
"ide-view-component-list-panel-breadcrumbs",
"ide-view-documentation", "ide-view-documentation",
"ide-view-graph-editor", "ide-view-graph-editor",
] ]
@ -4357,19 +4371,6 @@ dependencies = [
"ordered-float", "ordered-float",
] ]
[[package]]
name = "ide-view-component-list-panel-breadcrumbs"
version = "0.1.0"
dependencies = [
"enso-frp",
"ensogl-core",
"ensogl-derive-theme",
"ensogl-grid-view",
"ensogl-hardcoded-theme",
"ensogl-icons",
"ensogl-text",
]
[[package]] [[package]]
name = "ide-view-component-list-panel-grid" name = "ide-view-component-list-panel-grid"
version = "0.1.0" version = "0.1.0"
@ -4398,11 +4399,11 @@ dependencies = [
"enso-profiler", "enso-profiler",
"enso-suggestion-database", "enso-suggestion-database",
"ensogl", "ensogl",
"ensogl-breadcrumbs",
"ensogl-component", "ensogl-component",
"ensogl-hardcoded-theme", "ensogl-hardcoded-theme",
"horrorshow", "horrorshow",
"ide-ci", "ide-ci",
"ide-view-component-list-panel-breadcrumbs",
"ide-view-graph-editor", "ide-view-graph-editor",
"serde_json", "serde_json",
"tokio", "tokio",

View File

@ -33,6 +33,7 @@ ensogl-dynamic-assets = { path = "../../lib/rust/ensogl/component/dynamic-assets
ensogl-text-msdf = { path = "../../lib/rust/ensogl/component/text/src/font/msdf" } ensogl-text-msdf = { path = "../../lib/rust/ensogl/component/text/src/font/msdf" }
ensogl-hardcoded-theme = { path = "../../lib/rust/ensogl/app/theme/hardcoded" } ensogl-hardcoded-theme = { path = "../../lib/rust/ensogl/app/theme/hardcoded" }
ensogl-drop-manager = { path = "../../lib/rust/ensogl/component/drop-manager" } ensogl-drop-manager = { path = "../../lib/rust/ensogl/component/drop-manager" }
ensogl-breadcrumbs = { path = "../../lib/rust/ensogl/component/breadcrumbs" }
fuzzly = { path = "../../lib/rust/fuzzly" } fuzzly = { path = "../../lib/rust/fuzzly" }
ast = { path = "language/ast/impl" } ast = { path = "language/ast/impl" }
parser = { path = "language/parser" } parser = { path = "language/parser" }

View File

@ -5,6 +5,7 @@ use crate::prelude::*;
use crate::controller::graph::ImportType; use crate::controller::graph::ImportType;
use crate::controller::graph::RequiredImport; use crate::controller::graph::RequiredImport;
use crate::controller::searcher::breadcrumbs::BreadcrumbEntry;
use crate::model::execution_context::GroupQualifiedName; use crate::model::execution_context::GroupQualifiedName;
use crate::model::module::NodeEditStatus; use crate::model::module::NodeEditStatus;
use crate::model::suggestion_database; use crate::model::suggestion_database;
@ -354,16 +355,7 @@ impl Searcher {
} }
/// Enter the specified module. The displayed content of the browser will be updated. /// Enter the specified module. The displayed content of the browser will be updated.
pub fn enter_entry(&self, entry: usize) -> FallibleResult { pub fn enter_entry(&self, _entry: usize) -> FallibleResult {
let id = {
let data = self.data.borrow();
let error = || NoSuchComponent { index: entry };
let component = data.components.displayed().get(entry).ok_or_else(error)?;
component.id().ok_or(NotEnterable { index: entry })?
};
let bc_builder = breadcrumbs::Builder::new(&self.database);
let breadcrumbs = bc_builder.build(id);
self.breadcrumbs.set_content(breadcrumbs);
self.reload_list(); self.reload_list();
Ok(()) Ok(())
} }
@ -374,11 +366,43 @@ impl Searcher {
self.breadcrumbs.names() self.breadcrumbs.names()
} }
/// Select the breadcrumb with the index [`id`]. The displayed content of the browser will be /// Set the selected breadcrumb. The `id` is the index of the breadcrumb from left to right.
/// updated.
pub fn select_breadcrumb(&self, id: usize) { pub fn select_breadcrumb(&self, id: usize) {
self.breadcrumbs.select(id); self.breadcrumbs.select(id);
self.reload_list(); }
/// Set the breadcrumbs to match the component at the given `index`. The index refers to the
/// displayed list of components. Returns the full breadcrumb for the entry, if there is one.
pub fn update_breadcrumbs(&self, index: usize) -> Option<Vec<BreadcrumbEntry>> {
let data = self.data.borrow();
if let Some(component) = data.components.displayed().get(index) {
if let Some(id) = component.id() {
let bc_builder = breadcrumbs::Builder::new(&self.database);
let breadcrumbs = bc_builder.build(id).collect_vec();
assert!(breadcrumbs.iter().all(|e| self.database.lookup(e.id()).is_ok()));
self.breadcrumbs.set_content(breadcrumbs.clone().into_iter());
Some(breadcrumbs)
} else {
warn!(
"Cannot update breadcrumbs with component that has no suggestion database \
entry. Invalid component: {:?}",
component
);
None
}
} else {
warn!("Update breadcrumbs called with invalid index: {}", index);
None
}
}
/// Return the documentation for the breadcrumb.
pub fn documentation_for_selected_breadcrumb(&self) -> Option<EntryDocumentation> {
let selected = self.breadcrumbs.selected();
let component = selected?;
assert!(self.database.lookup(component).is_ok());
let docs = self.database.documentation_for_entry(component);
Some(docs)
} }
/// Set the Searcher Input. /// Set the Searcher Input.
@ -1217,30 +1241,6 @@ pub mod test {
assert_eq!(notification, Some(Notification::NewComponentList)); assert_eq!(notification, Some(Notification::NewComponentList));
} }
#[test]
fn entering_module() {
let mut fixture =
Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| {
data.expect_completion(client, None, &(0..11).collect_vec());
data.expect_completion(client, None, &(0..11).collect_vec());
});
let searcher = &fixture.searcher;
searcher.reload_list();
fixture.test.run_until_stalled();
// There are two virtual entries and two top-modules.
assert_eq!(searcher.components().displayed().len(), 4);
let mut subscriber = searcher.subscribe();
searcher.enter_entry(3).expect("Entering entry failed");
fixture.test.run_until_stalled();
let list = searcher.components();
assert_eq!(list.displayed().len(), 1);
assert_eq!(list.displayed()[0].suggestion, fixture.test_method_3_suggestion());
let notification = subscriber.next().boxed_local().expect_ready();
assert_eq!(notification, Some(Notification::NewComponentList));
}
#[test] #[test]
fn picked_completions_list_maintaining() { fn picked_completions_list_maintaining() {
let fixture = Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| { let fixture = Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| {

View File

@ -6,6 +6,7 @@ use crate::model::suggestion_database;
use double_representation::name::QualifiedName; use double_representation::name::QualifiedName;
use double_representation::name::QualifiedNameRef; use double_representation::name::QualifiedNameRef;
use ensogl_icons::icon;
use model::suggestion_database::Entry; use model::suggestion_database::Entry;
@ -32,9 +33,11 @@ impl Breadcrumbs {
/// Set the list of breadcrumbs to be displayed in the breadcrumbs panel. /// Set the list of breadcrumbs to be displayed in the breadcrumbs panel.
pub fn set_content(&self, breadcrumbs: impl Iterator<Item = BreadcrumbEntry>) { pub fn set_content(&self, breadcrumbs: impl Iterator<Item = BreadcrumbEntry>) {
let breadcrumbs: Vec<_> = breadcrumbs.collect();
let mut borrowed = self.list.borrow_mut(); let mut borrowed = self.list.borrow_mut();
*borrowed = breadcrumbs.collect(); *borrowed = breadcrumbs;
self.select(borrowed.len()); let len = borrowed.len();
self.select(len.saturating_sub(1));
} }
/// A list of breadcrumbs' text labels to be displayed in the panel. /// A list of breadcrumbs' text labels to be displayed in the panel.
@ -60,12 +63,8 @@ impl Breadcrumbs {
/// Returns a currently selected breadcrumb id. Returns [`None`] if the top level breadcrumb /// Returns a currently selected breadcrumb id. Returns [`None`] if the top level breadcrumb
/// is selected. /// is selected.
pub fn selected(&self) -> Option<suggestion_database::entry::Id> { pub fn selected(&self) -> Option<suggestion_database::entry::Id> {
if self.is_top_module() { let index = self.selected.get();
None self.list.borrow().get(index).map(BreadcrumbEntry::id)
} else {
let index = self.selected.get();
self.list.borrow().get(index - 1).map(BreadcrumbEntry::id)
}
} }
} }
@ -81,6 +80,7 @@ pub struct BreadcrumbEntry {
displayed_name: ImString, displayed_name: ImString,
component_id: suggestion_database::entry::Id, component_id: suggestion_database::entry::Id,
qualified_name: QualifiedName, qualified_name: QualifiedName,
icon: Option<icon::Id>,
} }
impl BreadcrumbEntry { impl BreadcrumbEntry {
@ -98,18 +98,33 @@ impl BreadcrumbEntry {
pub fn qualified_name(&self) -> &QualifiedName { pub fn qualified_name(&self) -> &QualifiedName {
&self.qualified_name &self.qualified_name
} }
/// An icon of the entry.
pub fn icon(&self) -> Option<icon::Id> {
self.icon
}
/// Return a [`ensogl_breadcrumbs::Breadcrumb`] with the entries name and icon.
pub fn view_without_icon(&self) -> ensogl_breadcrumbs::Breadcrumb {
ensogl_breadcrumbs::Breadcrumb::new(self.name().as_str(), None)
}
/// Return a [`ensogl_breadcrumbs::Breadcrumb`] with the entries name but no icon.
pub fn view_with_icon(&self) -> ensogl_breadcrumbs::Breadcrumb {
ensogl_breadcrumbs::Breadcrumb::new(self.name().as_str(), self.icon())
}
} }
impl From<(suggestion_database::entry::Id, Rc<Entry>)> for BreadcrumbEntry { impl From<(suggestion_database::entry::Id, Rc<Entry>)> for BreadcrumbEntry {
fn from((component_id, entry): (suggestion_database::entry::Id, Rc<Entry>)) -> Self { fn from((component_id, entry): (suggestion_database::entry::Id, Rc<Entry>)) -> Self {
let qualified_name = entry.qualified_name(); let qualified_name = entry.qualified_name();
let displayed_name = entry.name.clone(); let displayed_name = entry.name.clone();
BreadcrumbEntry { displayed_name, component_id, qualified_name } let icon = Some(entry.as_ref().icon());
BreadcrumbEntry { displayed_name, component_id, qualified_name, icon }
} }
} }
// =============== // ===============
// === Builder === // === Builder ===
// =============== // ===============

View File

@ -86,10 +86,7 @@ impl Model {
) -> Option<(ViewNodeId, text::Range<text::Byte>, ImString)> { ) -> Option<(ViewNodeId, text::Range<text::Byte>, ImString)> {
let new_code = self.controller.use_suggestion_by_index(id); let new_code = self.controller.use_suggestion_by_index(id);
match new_code { match new_code {
Ok(text::Change { range, text }) => { Ok(text::Change { range, text }) => Some((self.input_view, range, text.into())),
self.update_breadcrumbs();
Some((self.input_view, range, text.into()))
}
Err(err) => { Err(err) => {
error!("Error while applying suggestion: {err}."); error!("Error while applying suggestion: {err}.");
None None
@ -101,20 +98,23 @@ impl Model {
self.controller.select_breadcrumb(id); self.controller.select_breadcrumb(id);
} }
fn update_breadcrumbs(&self) { fn module_entered(&self, entry: component_grid::EntryId) {
let names = self.controller.breadcrumbs().into_iter(); if let Err(error) = self.controller.enter_entry(entry) {
let browser = &self.view; error!("Failed to enter entry in Component Browser: {error}")
// 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().documentation.breadcrumbs.set_entries_from(breadcrumbs_from);
} }
fn module_entered(&self, entry: component_grid::EntryId) { fn update_breadcrumbs(&self, target_entry: component_grid::EntryId) {
match self.controller.enter_entry(entry) { let breadcrumbs = self.controller.update_breadcrumbs(target_entry);
Ok(()) => self.update_breadcrumbs(), if let Some(breadcrumbs) = breadcrumbs {
Err(error) => error!("Failed to enter entry in Component Browser: {error}"), let browser = &self.view;
let breadcrumbs_count = breadcrumbs.len();
let without_icon =
breadcrumbs[0..breadcrumbs_count - 1].iter().map(|crumb| crumb.view_without_icon());
let with_icon =
breadcrumbs[breadcrumbs_count - 1..].iter().map(|crumb| crumb.view_with_icon());
let all = without_icon.chain(with_icon).collect_vec();
browser.model().documentation.breadcrumbs.set_entries(all);
} }
} }
@ -138,9 +138,19 @@ impl Model {
self.controller.documentation_for_entry(id) self.controller.documentation_for_entry(id)
} }
fn docs_for_breadcrumb(&self) -> Option<EntryDocumentation> {
self.controller.documentation_for_selected_breadcrumb()
}
fn should_select_first_entry(&self) -> bool { fn should_select_first_entry(&self) -> bool {
self.controller.is_filtering() || self.controller.is_input_empty() self.controller.is_filtering() || self.controller.is_input_empty()
} }
fn on_entry_for_docs_selected(&self, id: Option<component_grid::EntryId>) {
if let Some(id) = id {
self.update_breadcrumbs(id);
}
}
} }
/// The Searcher presenter, synchronizing state between searcher view and searcher controller. /// The Searcher presenter, synchronizing state between searcher view and searcher controller.
@ -267,12 +277,17 @@ impl ComponentBrowserSearcher {
docs <- docs_params.filter_map(f!([model]((_, entry)) { docs <- docs_params.filter_map(f!([model]((_, entry)) {
entry.map(|entry_id| model.documentation_of_component(entry_id)) entry.map(|entry_id| model.documentation_of_component(entry_id))
})); }));
docs_from_breadcrumbs <- breadcrumbs.selected.map(f!((selected){
model.breadcrumb_selected(*selected);
model.docs_for_breadcrumb()
})).unwrap();
docs <- any(docs,docs_from_breadcrumbs);
documentation.frp.display_documentation <+ docs; documentation.frp.display_documentation <+ docs;
eval grid.active ((entry) model.on_entry_for_docs_selected(*entry));
eval_ grid.suggestion_accepted([]analytics::remote_log_event("component_browser::suggestion_accepted")); eval_ grid.suggestion_accepted([]analytics::remote_log_event("component_browser::suggestion_accepted"));
eval grid.active((entry) model.suggestion_selected(*entry)); eval grid.active((entry) model.suggestion_selected(*entry));
eval grid.module_entered((id) model.module_entered(*id)); eval grid.module_entered((id) model.module_entered(*id));
eval breadcrumbs.selected((id) model.breadcrumb_selected(*id));
} }
let weak_model = Rc::downgrade(&model); let weak_model = Rc::downgrade(&model);

View File

@ -71,7 +71,7 @@ impl Default for EntryDocumentation {
pub struct LinkedDocPage { pub struct LinkedDocPage {
/// The name of the liked entry. It is used to produce a unique ID for the link. /// The name of the liked entry. It is used to produce a unique ID for the link.
pub name: Rc<QualifiedName>, pub name: Rc<QualifiedName>,
/// The intermediate reprentation of the linked entry's documentation. /// The intermediate representation of the linked entry's documentation.
pub page: EntryDocumentation, pub page: EntryDocumentation,
} }

View File

@ -19,6 +19,7 @@ use enso_doc_parser::doc_sections::HtmlString;
use enso_doc_parser::DocSection; use enso_doc_parser::DocSection;
use enso_doc_parser::Tag; use enso_doc_parser::Tag;
use enso_text::Location; use enso_text::Location;
use ensogl_icons::icon;
use language_server::types::FieldAction; use language_server::types::FieldAction;
@ -528,6 +529,17 @@ impl Entry {
// === Other Properties === // === Other Properties ===
macro_rules! kind_to_icon {
([ $( $variant:ident ),* ] $kind:ident) => {
{
use ensogl_icons::icon::Id;
match $kind {
$( Kind::$variant => Id::$variant, )*
}
}
}
}
impl Entry { impl Entry {
/// Return the Method Id of suggested method. /// Return the Method Id of suggested method.
/// ///
@ -621,6 +633,14 @@ impl Entry {
}) })
.flatten() .flatten()
} }
/// Returns the icon of the entry.
pub fn icon(&self) -> icon::Id {
let kind = self.kind;
let icon_name = self.icon_name.as_ref();
let icon = icon_name.and_then(|n| n.to_pascal_case().parse().ok());
icon.unwrap_or_else(|| for_each_kind_variant!(kind_to_icon(kind)))
}
} }

View File

@ -13,7 +13,7 @@ ensogl = { path = "../../../../lib/rust/ensogl" }
ensogl-gui-component = { path = "../../../../lib/rust/ensogl/component/gui" } ensogl-gui-component = { path = "../../../../lib/rust/ensogl/component/gui" }
ensogl-hardcoded-theme = { path = "../../../../lib/rust/ensogl/app/theme/hardcoded" } ensogl-hardcoded-theme = { path = "../../../../lib/rust/ensogl/app/theme/hardcoded" }
enso-prelude = { path = "../../../../lib/rust/prelude" } enso-prelude = { path = "../../../../lib/rust/prelude" }
ide-view-component-list-panel-breadcrumbs = { path = "../../../../lib/rust/ensogl/component/breadcrumbs" } ensogl-breadcrumbs = { path = "../../../../lib/rust/ensogl/component/breadcrumbs" }
ide-view-component-list-panel = { path = "component-list-panel" } ide-view-component-list-panel = { path = "component-list-panel" }
ide-view-documentation = { path = "../documentation" } ide-view-documentation = { path = "../documentation" }
ide-view-graph-editor = { path = "../graph-editor" } ide-view-graph-editor = { path = "../graph-editor" }

View File

@ -35,8 +35,8 @@ use ide_view_graph_editor::component::node::HEIGHT as NODE_HEIGHT;
// === Export === // === Export ===
// ============== // ==============
pub use ensogl_breadcrumbs as breadcrumbs;
pub use ide_view_component_list_panel as component_list_panel; pub use ide_view_component_list_panel as component_list_panel;
pub use ide_view_component_list_panel_breadcrumbs as breadcrumbs;

View File

@ -16,7 +16,7 @@ ensogl = { path = "../../../../lib/rust/ensogl" }
ensogl-component = { path = "../../../../lib/rust/ensogl/component" } ensogl-component = { path = "../../../../lib/rust/ensogl/component" }
ensogl-hardcoded-theme = { path = "../../../../lib/rust/ensogl/app/theme/hardcoded" } ensogl-hardcoded-theme = { path = "../../../../lib/rust/ensogl/app/theme/hardcoded" }
ide-view-graph-editor = { path = "../graph-editor" } ide-view-graph-editor = { path = "../graph-editor" }
ide-view-component-list-panel-breadcrumbs = { path = "../../../../lib/rust/ensogl/component/breadcrumbs" } ensogl-breadcrumbs = { path = "../../../../lib/rust/ensogl/component/breadcrumbs" }
wasm-bindgen = { workspace = true } wasm-bindgen = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
horrorshow = "0.8.4" horrorshow = "0.8.4"

View File

@ -44,7 +44,7 @@ use ide_view_graph_editor as graph_editor;
pub mod html; pub mod html;
pub use ide_view_component_list_panel_breadcrumbs as breadcrumbs; pub use ensogl_breadcrumbs as breadcrumbs;
@ -151,8 +151,8 @@ impl Model {
fn set_initial_breadcrumbs(&self) { fn set_initial_breadcrumbs(&self) {
let breadcrumb = breadcrumbs::Breadcrumb::new(INITIAL_SECTION_NAME, None); let breadcrumb = breadcrumbs::Breadcrumb::new(INITIAL_SECTION_NAME, None);
self.breadcrumbs.set_entries_from((vec![breadcrumb], 0)); self.breadcrumbs.set_entries(vec![breadcrumb]);
self.breadcrumbs.show_ellipsis(true); self.breadcrumbs.show_ellipsis(false);
} }
/// Set size of the documentation view. /// Set size of the documentation view.

View File

@ -145,7 +145,7 @@ pub fn main() {
panel.show(); panel.show();
let breadcrumbs = app.new_view::<Breadcrumbs>(); let breadcrumbs = app.new_view::<Breadcrumbs>();
breadcrumbs.set_y(400.0); breadcrumbs.set_y(500.0);
breadcrumbs.frp().set_size(Vector2(500.0, 100.0)); breadcrumbs.frp().set_size(Vector2(500.0, 100.0));
breadcrumbs.set_entries_from(( breadcrumbs.set_entries_from((
vec![ vec![

View File

@ -383,6 +383,8 @@ define_themes! { [light:0, dark:1]
text_y_offset = 6.0, 6.0; text_y_offset = 6.0, 6.0;
text_padding_left = 0.0, 0.0; text_padding_left = 0.0, 0.0;
text_size = 11.5, 11.5; text_size = 11.5, 11.5;
icon_x_offset = 2.0, 2.0;
icon_y_offset = 6.0, 6.0;
selected_color = Rgba(1.0, 1.0, 1.0, 1.0), Rgba(1.0, 1.0, 1.0, 1.0); selected_color = Rgba(1.0, 1.0, 1.0, 1.0), Rgba(1.0, 1.0, 1.0, 1.0);
highlight_corners_radius = 15.0, 15.0; highlight_corners_radius = 15.0, 15.0;
greyed_out_color = Rgba(1.0, 1.0, 1.0, 0.15), Rgba(1.0, 1.0, 1.0, 0.15); greyed_out_color = Rgba(1.0, 1.0, 1.0, 0.15), Rgba(1.0, 1.0, 1.0, 0.15);

View File

@ -1,5 +1,5 @@
[package] [package]
name = "ide-view-component-list-panel-breadcrumbs" name = "ensogl-breadcrumbs"
version = "0.1.0" version = "0.1.0"
authors = ["Enso Team <contact@enso.org>"] authors = ["Enso Team <contact@enso.org>"]
edition = "2021" edition = "2021"

View File

@ -113,6 +113,8 @@ pub struct Style {
pub text_y_offset: f32, pub text_y_offset: f32,
pub text_padding_left: f32, pub text_padding_left: f32,
pub text_size: f32, pub text_size: f32,
pub icon_x_offset: f32,
pub icon_y_offset: f32,
pub selected_color: color::Rgba, pub selected_color: color::Rgba,
pub highlight_corners_radius: f32, pub highlight_corners_radius: f32,
pub greyed_out_color: color::Rgba, pub greyed_out_color: color::Rgba,
@ -125,7 +127,7 @@ pub struct Style {
/// A model for the entry in the breadcrumbs list. /// A model for the entry in the breadcrumbs list.
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Clone, CloneRef, Debug, Default)] #[derive(Clone, CloneRef, Debug, Default, PartialEq)]
pub enum Model { pub enum Model {
#[default] #[default]
Ellipsis, Ellipsis,
@ -186,8 +188,8 @@ impl EntryData {
let separator = separator::View::new(); let separator = separator::View::new();
let state = default(); let state = default();
let icon: any_icon::View = default(); let icon: any_icon::View = default();
icon.set_size((ICON_WIDTH, ICON_WIDTH));
ellipsis.set_size((ellipsis::ICON_WIDTH, ellipsis::ICON_WIDTH)); ellipsis.set_size((ellipsis::ICON_WIDTH, ellipsis::ICON_WIDTH));
icon.set_size((ICON_WIDTH, ICON_WIDTH));
display_object.add_child(&icon); display_object.add_child(&icon);
display_object.add_child(&ellipsis); display_object.add_child(&ellipsis);
Self { display_object, state, text, ellipsis, separator, icon } Self { display_object, state, text, ellipsis, separator, icon }
@ -223,7 +225,6 @@ impl EntryData {
if let Some(icon) = icon { if let Some(icon) = icon {
self.display_object.add_child(&self.icon); self.display_object.add_child(&self.icon);
self.icon.icon.set(icon.any_cached_shape_location()); self.icon.icon.set(icon.any_cached_shape_location());
self.text.set_x(ICON_WIDTH);
} else { } else {
self.icon.unset_parent(); self.icon.unset_parent();
self.text.set_x(0.0); self.text.set_x(0.0);
@ -256,15 +257,22 @@ impl EntryData {
} }
} }
fn update_layout(&self, contour: Contour, text_padding: f32, text_y_offset: f32) { fn update_layout(
&self,
contour: Contour,
text_padding: f32,
text_y_offset: f32,
icon_x_offset: f32,
icon_y_offset: f32,
) {
let size = contour.size; let size = contour.size;
let icon_offset = if self.has_icon() { ICON_WIDTH } else { 0.0 }; let icon_offset = if self.has_icon() { ICON_WIDTH } else { 0.0 };
self.text.set_xy(Vector2(icon_offset + text_padding - size.x / 2.0, text_y_offset)); self.text.set_xy(Vector2(icon_offset + text_padding - size.x / 2.0, text_y_offset));
self.separator.set_size(Vector2(separator::ICON_WIDTH, size.y)); self.separator.set_size(Vector2(separator::ICON_WIDTH, size.y));
self.ellipsis.set_size(Vector2(ellipsis::ICON_WIDTH, size.y)); self.ellipsis.set_size(Vector2(ellipsis::ICON_WIDTH, size.y));
self.icon.set_size(Vector2(ICON_WIDTH, size.y)); self.icon.set_size(Vector2(ICON_WIDTH, size.y));
self.icon.set_x(-ICON_WIDTH); self.icon.set_x(-size.x / 2.0 - icon_x_offset);
self.icon.set_y(-2.0); self.icon.set_y(-ICON_WIDTH - icon_y_offset);
} }
fn set_default_color(&self, color: color::Lcha) { fn set_default_color(&self, color: color::Lcha) {
@ -372,10 +380,16 @@ impl ensogl_grid_view::Entry for Entry {
text_color <- input.set_params.map(|p| p.style.selected_color).cloned_into().on_change(); text_color <- input.set_params.map(|p| p.style.selected_color).cloned_into().on_change();
text_y_offset <- input.set_params.map(|p| p.style.text_y_offset).on_change(); text_y_offset <- input.set_params.map(|p| p.style.text_y_offset).on_change();
text_size <- input.set_params.map(|p| p.style.text_size).on_change(); text_size <- input.set_params.map(|p| p.style.text_size).on_change();
icon_x_offset <- input.set_params.map(|p| p.style.icon_x_offset).on_change();
icon_y_offset <- input.set_params.map(|p| p.style.icon_y_offset).on_change();
greyed_out_color <- input.set_params.map(|p| p.style.greyed_out_color).cloned_into().on_change(); greyed_out_color <- input.set_params.map(|p| p.style.greyed_out_color).cloned_into().on_change();
highlight_corners_radius <- input.set_params.map(|p| p.style.highlight_corners_radius).on_change(); highlight_corners_radius <- input.set_params.map(|p| p.style.highlight_corners_radius).on_change();
greyed_out_from <- input.set_params.map(|p| p.greyed_out_start).on_change(); greyed_out_from <- input.set_params.map(|p| p.greyed_out_start).on_change();
transparent_color <- init.constant(color::Lcha::transparent()); transparent_color <- init.constant(color::Lcha::transparent());
new_model <- input.set_model.on_change();
// === Appearance ===
col <- input.set_location._1(); col <- input.set_location._1();
should_grey_out <- all_with(&col, &greyed_out_from, should_grey_out <- all_with(&col, &greyed_out_from,
@ -397,7 +411,6 @@ impl ensogl_grid_view::Entry for Entry {
size: *size - Vector2(*margin, *margin) * 2.0, size: *size - Vector2(*margin, *margin) * 2.0,
corners_radius: 0.0, corners_radius: 0.0,
}); });
eval color((c) data.set_default_color(*c)); eval color((c) data.set_default_color(*c));
eval font((f) data.set_font(f.to_string())); eval font((f) data.set_font(f.to_string()));
eval text_size((s) data.set_default_text_size(*s)); eval text_size((s) data.set_default_text_size(*s));
@ -410,24 +423,35 @@ impl ensogl_grid_view::Entry for Entry {
out.hover_highlight_color <+ hover_color; out.hover_highlight_color <+ hover_color;
out.selection_highlight_color <+ init.constant(color::Lcha::transparent()); out.selection_highlight_color <+ init.constant(color::Lcha::transparent());
// === Override column width ===
// We need to adjust the width of the grid view column depending on the width of
// the entry.
out.override_column_width <+ input.set_model.map2(&text_padding,
f!([data](model, text_padding) {
data.set_model(model);
data.width(*text_padding)
})
);
// For text entries, we also listen for [`Text::width`] changes. // For text entries, we also listen for [`Text::width`] changes.
text_width <- data.text.width.filter(f_!(data.is_text_displayed())); text_width <- data.text.width.filter(f_!(data.is_text_displayed()));
entry_width <- text_width.map2(&text_padding, f!((w, o) data.text_width(*w, *o))); entry_width <- text_width.map2(&text_padding, f!((w, o) data.text_width(*w, *o)));
out.override_column_width <+ entry_width; out.override_column_width <+ entry_width;
layout <- all(contour, text_padding, text_y_offset, input.set_model);
eval layout ((&(c, to, tyo, _)) data.update_layout(c, to, tyo)); // === Layout ===
override_column_width <- new_model.map2(&text_padding,
f!([data](model, text_padding) {
data.set_model(model);
data.width(*text_padding)
})
);
layout <- all6(
&contour,
&text_padding,
&text_y_offset,
&new_model,
&icon_x_offset,
&icon_y_offset);
eval layout ((&(c, to, tyo, _, ix, iy)) data.update_layout(c, to, tyo, ix, iy));
// === Override column width ===
// We need to adjust the width of the grid view column depending on the width of
// the entry.
out.override_column_width <+ override_column_width;
} }
init.emit(()); init.emit(());
Self { frp, data } Self { frp, data }

View File

@ -477,6 +477,8 @@ ensogl_core::define_endpoints_2! {
push(Breadcrumb), push(Breadcrumb),
/// Set the displayed breadcrumbs starting from the specific index. /// Set the displayed breadcrumbs starting from the specific index.
set_entries_from((Vec<Breadcrumb>, BreadcrumbId)), set_entries_from((Vec<Breadcrumb>, BreadcrumbId)),
/// Set the displayed breadcrumbs.
set_entries(Vec<Breadcrumb>),
/// Set the breadcrumb at a specified index. /// Set the breadcrumb at a specified index.
set_entry((BreadcrumbId, Breadcrumb)), set_entry((BreadcrumbId, Breadcrumb)),
/// Enable or disable displaying of the ellipsis icon at the end of the list. /// Enable or disable displaying of the ellipsis icon at the end of the list.
@ -534,7 +536,10 @@ impl Breadcrumbs {
eval_ input.clear(model.clear()); eval_ input.clear(model.clear());
selected <- selected_grid_col.map(|(_, col)| col / 2); selected <- selected_grid_col.map(|(_, col)| col / 2);
eval input.push((b) model.push(b)); eval input.push((b) model.push(b));
eval input.set_entries_from(((entries, from)) model.set_entries(entries, *from));
set_entries_from_zero <- input.set_entries.map(|entries| (entries.clone(), 0));
set_entries_from <- any(set_entries_from_zero, input.set_entries_from);
eval set_entries_from(((entries, from)) model.set_entries(entries, *from));
eval input.set_entry(((index, entry)) model.set_entry(entry, *index)); eval input.set_entry(((index, entry)) model.set_entry(entry, *index));
out.selected <+ selected; out.selected <+ selected;