From 1c8aa26f90cd1055759761d8b1623b2b0ec3af09 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Thu, 12 May 2022 12:30:00 +0300 Subject: [PATCH] Wide Componet Group List (#3409) [ci no changelog needed] [Task link](https://www.pivotaltracker.com/story/show/181414466) This PR brings a new UI component: Wide Component Group. This is a three-column headerless container similar to Component Group. See the updated `component-group` demo scene: https://user-images.githubusercontent.com/6566674/166933866-e5bee142-5176-4a02-bc18-a5bfd96ccbe2.mp4 --- Cargo.lock | 2 + .../enso-profiler-enso-data/src/beanpole.rs | 8 +- app/gui/enso-profiler-enso-data/src/lib.rs | 11 +- .../component-group/Cargo.toml | 1 + .../component-group/src/lib.rs | 18 +- .../component-group/src/wide.rs | 447 ++++++++++++++++++ .../debug_scene/component-group/Cargo.toml | 1 + .../debug_scene/component-group/src/lib.rs | 158 +++++-- .../sequence-diagram/src/labeled_line.rs | 2 + .../component/sequence-diagram/src/lib.rs | 12 +- .../component/sequence-diagram/src/shape.rs | 3 +- lib/rust/ensogl/component/tooltip/src/lib.rs | 12 +- lib/rust/ensogl/core/src/application.rs | 4 +- lib/rust/profiler/src/format.rs | 5 + 14 files changed, 631 insertions(+), 53 deletions(-) create mode 100644 app/gui/view/component-browser/component-group/src/wide.rs diff --git a/Cargo.lock b/Cargo.lock index e5da54a9e02..20346b911b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -747,6 +747,7 @@ dependencies = [ "ensogl-core", "ensogl-hardcoded-theme", "ensogl-list-view", + "ensogl-selector", "ensogl-text-msdf-sys", "ide-view-component-group", "wasm-bindgen", @@ -2331,6 +2332,7 @@ dependencies = [ "ensogl-core", "ensogl-gui-component", "ensogl-hardcoded-theme", + "ensogl-label", "ensogl-list-view", "ensogl-text", ] diff --git a/app/gui/enso-profiler-enso-data/src/beanpole.rs b/app/gui/enso-profiler-enso-data/src/beanpole.rs index 8e4347b9dd7..b236edff1c1 100644 --- a/app/gui/enso-profiler-enso-data/src/beanpole.rs +++ b/app/gui/enso-profiler-enso-data/src/beanpole.rs @@ -5,15 +5,11 @@ //! and many other data complexities); as a result, it is better suited to display of large numbers //! of events spread unevenly over a long time range than any UML renderers I[KW] am aware of. - - -// =============== -// === Diagram === -// =============== - use crate::backend::Direction; use crate::Metadata; + + /// The data necessary to create a diagram of message timings. #[derive(Debug, Default)] pub struct Diagram<'a> { diff --git a/app/gui/enso-profiler-enso-data/src/lib.rs b/app/gui/enso-profiler-enso-data/src/lib.rs index 2fa32261097..5a1e4a09710 100644 --- a/app/gui/enso-profiler-enso-data/src/lib.rs +++ b/app/gui/enso-profiler-enso-data/src/lib.rs @@ -14,14 +14,19 @@ #![warn(trivial_numeric_casts)] #![warn(unused_import_braces)] -pub mod backend; -pub mod beanpole; - use serde::Serializer; use std::fmt::Display; use std::fmt::Formatter; +// ============== +// === Export === +// ============== + +pub mod backend; +pub mod beanpole; + + // ================ // === Metadata === diff --git a/app/gui/view/component-browser/component-group/Cargo.toml b/app/gui/view/component-browser/component-group/Cargo.toml index 4d51662da6f..eafe128ffee 100644 --- a/app/gui/view/component-browser/component-group/Cargo.toml +++ b/app/gui/view/component-browser/component-group/Cargo.toml @@ -14,4 +14,5 @@ ensogl-gui-component = { version = "0.1.0", path = "../../../../../lib/rust/enso ensogl-hardcoded-theme = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" } ensogl-list-view = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/list-view" } ensogl-text = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/text" } +ensogl-label = { path = "../../../../../lib/rust/ensogl/component/label/" } 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 e72ddfe1e29..6a3ec7b6326 100644 --- a/app/gui/view/component-browser/component-group/src/lib.rs +++ b/app/gui/view/component-browser/component-group/src/lib.rs @@ -6,6 +6,7 @@ //! To learn more about component groups, see the [Component Browser Design //! Document](https://github.com/enso-org/design/blob/e6cffec2dd6d16688164f04a4ef0d9dff998c3e7/epics/component-browser/design.md). +#![recursion_limit = "512"] // === Standard Linter Configuration === #![deny(non_ascii_idents)] #![warn(unsafe_code)] @@ -17,14 +18,13 @@ #![warn(trivial_numeric_casts)] #![warn(unused_import_braces)] #![warn(unused_qualifications)] -#![recursion_limit = "512"] +use ensogl_core::application::traits::*; use ensogl_core::display::shape::*; use ensogl_core::prelude::*; use enso_frp as frp; use ensogl_core::application::shortcut::Shortcut; -use ensogl_core::application::traits::*; use ensogl_core::application::Application; use ensogl_core::data::color::Rgba; use ensogl_core::display; @@ -34,6 +34,13 @@ use ensogl_list_view as list_view; use ensogl_text as text; +// ============== +// === Export === +// ============== + +pub mod wide; + + // ================= // === Constants === @@ -67,13 +74,8 @@ pub mod background { ensogl_core::define_shape_system! { below = [list_view::background]; (style:Style, color:Vector4) { - let sprite_width: Var = "input_size.x".into(); - let sprite_height: Var = "input_size.y".into(); let color = Var::::from(color); - // TODO[MC,WD]: We should use Plane here, but it has a bug - renders wrong color. See: - // https://github.com/enso-org/enso/pull/3373#discussion_r849054476 - let shape = Rect((&sprite_width, &sprite_height)).fill(color); - shape.into() + Plane().fill(color).into() } } } diff --git a/app/gui/view/component-browser/component-group/src/wide.rs b/app/gui/view/component-browser/component-group/src/wide.rs new file mode 100644 index 00000000000..879d96ee3ea --- /dev/null +++ b/app/gui/view/component-browser/component-group/src/wide.rs @@ -0,0 +1,447 @@ +//! A multi-column [Component Group] without header. +//! +//! Almost every type in this module is parametrized with `COLUMNS` const generic, that represents +//! the count of columns the widget will have. The default value is `3`. (see +//! [`DEFAULT_COLUMNS_COUNT`]) +//! +//! The widget is defined by the [`View`]. +//! +//! To learn more about component groups, see the [Component Browser Design +//! Document](https://github.com/enso-org/design/blob/e6cffec2dd6d16688164f04a4ef0d9dff998c3e7/epics/component-browser/design.md). +//! +//! [Component Group]: crate::component_group::View + +use ensogl_core::application::traits::*; +use ensogl_core::display::shape::*; +use ensogl_core::prelude::*; + +use crate::EntryId; + +use enso_frp as frp; +use ensogl_core::application::shortcut::Shortcut; +use ensogl_core::application::Application; +use ensogl_core::data::color::Rgba; +use ensogl_core::display; +use ensogl_gui_component::component; +use ensogl_label::Label; +use ensogl_list_view as list_view; +use list_view::entry::AnyModelProvider; + + + +// ================= +// === Constants === +// ================= + +/// The default count of columns to display. +pub const DEFAULT_COLUMNS_COUNT: usize = 3; +const ENTRY_HEIGHT: f32 = list_view::entry::HEIGHT; +const MINIMAL_HEIGHT: f32 = ENTRY_HEIGHT; + + + +// =============== +// === Aliases === +// =============== + +/// Type of the component group items. +type Entry = list_view::entry::Label; + +newtype_prim! { + /// An index of the column. + ColumnId(usize); +} + + + +// ======================== +// === Background Shape === +// ======================== + +/// The background of the Wide Component Group. +pub mod background { + use super::*; + + ensogl_core::define_shape_system! { + below = [list_view::background]; + (style:Style, color:Vector4) { + let color = Var::::from(color); + Plane().fill(color).into() + } + } +} + + + +// ===================== +// === ModelProvider === +// ===================== + +/// A [`list_view::entry::ModelProvider`] wrapper that splits entries into `COLUMNS` lists. +/// +/// Entries are distributed evenly between lists. If the entry count is not divisible by `COLUMNS` - +/// the lists with lower indices will have more entries. +#[derive(Debug, Clone, CloneRef, Default)] +pub struct ModelProvider { + inner: AnyModelProvider, + column_id: Immutable, +} + +impl ModelProvider { + /// Wrap [`AnyModelProvider`] and split its entries into `COLUMNS` lists. The returned instance + /// provides entries for the column with `column_id`. + fn wrap(inner: &AnyModelProvider, column_id: ColumnId) -> AnyModelProvider { + AnyModelProvider::new(Self { + inner: inner.clone_ref(), + column_id: Immutable(column_id), + }) + } +} + +impl list_view::entry::ModelProvider for ModelProvider { + fn entry_count(&self) -> usize { + let total_entry_count = self.inner.entry_count(); + entry_count_in_column::(*self.column_id, total_entry_count) + } + + fn get(&self, id: EntryId) -> Option { + let total_entry_count = self.inner.entry_count(); + let idx = local_idx_to_global::(*self.column_id, id, total_entry_count); + self.inner.get(idx) + } +} + + + +// =========== +// === FRP === +// =========== + +ensogl_core::define_endpoints_2! { + Input { + /// Accept the currently selected suggestion. Should be bound to "Suggestion Acceptance Key" + /// described in + /// [Component Browser Design Doc](https://github.com/enso-org/design/blob/main/epics/component-browser/design.md#key-binding-dictionary) + accept_suggestion(), + select_entry(ColumnId, EntryId), + set_entries(AnyModelProvider), + set_background_color(Rgba), + set_width(f32), + set_no_items_label_text(String), + } + Output { + selected_entry(Option), + suggestion_accepted(EntryId), + expression_accepted(EntryId), + /// While resizing the list of entries, the selection will follow the selected entry if + /// possible. If the entry disappears, the selection will move to some visible entry in the same + /// column if possible. If there are no more entries in this column, the selection will move to + /// the next non-empty column to the left. + selection_position_target(Vector2), + entry_count(usize), + size(Vector2), + } +} + +impl component::Frp> for Frp { + fn init( + api: &Self::Private, + _app: &Application, + model: &Model, + _style: &StyleWatchFrp, + ) { + let network = &api.network; + let input = &api.input; + let out = &api.output; + frp::extend! { network + entry_count <- input.set_entries.map(|p| p.entry_count()); + out.entry_count <+ entry_count; + + selected_column_and_entry <- any(...); + update_selected_entry <- selected_column_and_entry.sample(&out.size); + select_entry <- any(&input.select_entry, &update_selected_entry); + eval select_entry([model]((column, entry)) { + let column = model.non_empty_column(*column); + if let Some(column) = column { + let real_entry_id = column.reverse_index(*entry); + column.select_entry(real_entry_id); + } + }); + + eval input.set_background_color((c) model.background.color.set(c.into())); + + eval input.set_no_items_label_text((text) model.set_no_items_label_text(text)); + + // === Background size === + + background_height <- any(...); + let background_width = input.set_width.clone_ref(); + size <- all_with(&background_width, &background_height, + |width, height| Vector2(*width, *height)); + eval size((size) model.background.size.set(*size)); + out.size <+ size; + + // === "No items" label === + + no_entries_provided <- entry_count.map(|c| *c == 0); + show_no_items_label <- no_entries_provided.on_true(); + hide_no_items_label <- no_entries_provided.on_false(); + eval_ show_no_items_label(model.show_no_items_label()); + eval_ hide_no_items_label(model.hide_no_items_label()); + } + + for column in model.columns.iter() { + let col_id = column.id.clone_ref(); + frp::extend! { network + // === Accepting suggestions === + + accepted_entry <- column.selected_entry.sample(&input.accept_suggestion).filter_map(|e| *e); + chosen_entry <- column.chosen_entry.filter_map(|e| *e); + out.suggestion_accepted <+ accepted_entry.map2(&entry_count, f!( + [](&e, &total) local_idx_to_global::(col_id, e, total) + )); + out.expression_accepted <+ chosen_entry.map2(&entry_count, f!( + [](&e, &total) local_idx_to_global::(col_id, e, total) + )); + + + // === Selection position === + + entry_selected <- column.selected_entry.filter_map(|e| *e); + selected_column_and_entry <+ entry_selected.map(f!([column](e) (col_id, column.reverse_index(*e)))); + on_column_selected <- column.selected_entry.map(|e| e.is_some()).on_true(); + eval_ on_column_selected(model.on_column_selected(col_id)); + selection_pos <- column.selection_position_target.sample(&on_column_selected); + out.selection_position_target <+ selection_pos.map(f!((pos) column.selection_position(*pos))); + + + // === set_entries === + + out.selected_entry <+ column.selected_entry.map2(&entry_count, move |entry, total| { + entry.map(|e| local_idx_to_global::(col_id, e, *total)) + }); + entries <- input.set_entries.map(move |p| ModelProvider::::wrap(p, col_id)); + background_height <+ entries.map(f_!(model.background_height())); + eval entries((e) column.set_entries(e)); + _eval <- all_with(&entries, &out.size, f!((_, size) column.resize_and_place(*size))); + } + } + } + + fn default_shortcuts() -> Vec { + use ensogl_core::application::shortcut::ActionType::*; + (&[(Press, "tab", "accept_suggestion")]) + .iter() + .map(|(a, b, c)| View::::self_shortcut(*a, *b, *c)) + .collect() + } +} + + + +// ============== +// === Column === +// ============== + +/// An internal representation of the column. +/// +/// `COLUMNS` is the total count of columns in the widget. +#[derive(Debug, Clone, CloneRef, Deref)] +struct Column { + id: ColumnId, + provider: Rc>>, + #[deref] + list_view: list_view::ListView, +} + +impl Column { + /// Constructor. + fn new(app: &Application, id: ColumnId) -> Self { + Self { id, provider: default(), list_view: app.new_view::>() } + } + + /// An entry count for this column. + fn len(&self) -> usize { + self.provider.get().entry_count() + } + + /// Transforms `entry_id` into the actual [`EntryId`] for the underlying + /// [`list_view::ListView`]. + /// + /// [`EntryId`] of the Wide Component Group counts from the bottom (the bottom most entry has an + /// id of 0), but the underlying [`list_view::ListView`] starts its ids from the top (so + /// that the top most entry has an id of 0). This function converts the former to the + /// latter. + fn reverse_index(&self, entry_id: EntryId) -> EntryId { + reverse_index(entry_id, self.len()) + } + + /// Update the entries list, a setter for [`list_view::ListView::set_entries`]. + fn set_entries(&self, provider: &AnyModelProvider) { + self.provider.set(provider.clone_ref()); + self.list_view.set_entries(provider); + } + + /// Resize the column and update its position. + fn resize_and_place(&self, size: Vector2) { + let width = size.x / COLUMNS as f32; + let bg_height = size.y; + let height = self.len() as f32 * ENTRY_HEIGHT; + self.list_view.resize(Vector2(width, height)); + + let left_border = -(COLUMNS as f32 * width / 2.0) + width / 2.0; + let pos_x = left_border + width * *self.id as f32; + let half_height = height / 2.0; + let background_bottom = -bg_height / 2.0; + let pos_y = background_bottom + half_height; + self.list_view.set_position_x(pos_x); + self.list_view.set_position_y(pos_y); + } + + /// Transform the position relative to the column into the position relative to the whole + /// widget. + fn selection_position(&self, pos: Vector2) -> Vector2 { + self.position().xy() + pos + } +} + + + +// ============= +// === Model === +// ============= + +/// The Model of the [`View`] component. Consists of `COLUMNS` columns. +#[derive(Clone, CloneRef, Debug)] +pub struct Model { + display_object: display::object::Instance, + background: background::View, + columns: Rc>>, + no_items_label: Label, +} + +impl display::Object for Model { + fn display_object(&self) -> &display::object::Instance { + &self.display_object + } +} + +impl component::Model for Model { + fn label() -> &'static str { + "WideComponentGroupView" + } + + fn new(app: &Application, logger: &Logger) -> Self { + let display_object = display::object::Instance::new(&logger); + let background = background::View::new(&logger); + display_object.add_child(&background); + let columns: Vec<_> = (0..COLUMNS).map(|i| Column::new(app, ColumnId::new(i))).collect(); + let columns = Rc::new(columns); + for column in columns.iter() { + column.hide_selection(); + column.set_background_color(Rgba::transparent()); + column.show_background_shadow(false); + column.set_background_corners_radius(0.0); + display_object.add_child(&**column); + } + let no_items_label = Label::new(app); + + Model { no_items_label, display_object, background, columns } + } +} + +impl Model { + /// Set the text content of the "no items" label. + fn set_no_items_label_text(&self, text: &str) { + self.no_items_label.set_content(text); + } + + /// Make the "no items" label visible. + fn show_no_items_label(&self) { + self.display_object.add_child(&self.no_items_label); + } + + /// Hide the "no items" label. + fn hide_no_items_label(&self) { + self.display_object.remove_child(&self.no_items_label); + } + + /// Returns the rightmost non-empty column with index less or equal to `index`. + fn non_empty_column(&self, index: ColumnId) -> Option<&Column> { + let indexes_to_the_right = (0..=*index).rev(); + let mut columns_to_the_right = indexes_to_the_right.flat_map(|i| self.columns.get(i)); + columns_to_the_right.find(|col| col.len() > 0) + } + + /// Deselect entries in all columns except the one with provided `column_index`. We ensure that + /// at all times only a single entry across all columns is selected. + fn on_column_selected(&self, column_id: ColumnId) { + let other_columns = self.columns.iter().enumerate().filter(|(i, _)| *i != *column_id); + for (_, column) in other_columns { + column.deselect_entries(); + } + } + + /// Calculate the height of the component. It can't be less than [`MINIMAL_HEIGHT`]. + fn background_height(&self) -> f32 { + if let Some(largest_column) = self.columns.first() { + let entry_count_in_largest_column = largest_column.len(); + let background_height = entry_count_in_largest_column as f32 * ENTRY_HEIGHT; + background_height.max(MINIMAL_HEIGHT) + } else { + MINIMAL_HEIGHT + } + } +} + + + +// ============ +// === View === +// ============ + +/// The implementation of the visual component described in the module's documentation. +pub type View = + component::ComponentView, Frp>; + + + +// =============== +// === Helpers === +// =============== + +/// Return the number of entries in the column with `index`. +fn entry_count_in_column( + column_id: ColumnId, + total_entry_count: usize, +) -> usize { + let evenly_distributed_count = total_entry_count / COLUMNS; + let remainder = total_entry_count % COLUMNS; + let has_remainder = remainder > 0; + let column_contains_remaining_entries = *column_id < remainder; + if has_remainder && column_contains_remaining_entries { + evenly_distributed_count + 1 + } else { + evenly_distributed_count + } +} + +/// Transform the index inside column to the "global" index inside the Wide Component Group. +/// +/// The entry #1 in column with index 2 is the second item from the bottom in the third column and +/// has a "global" index of `COLUMNS + 2 = 5`. +fn local_idx_to_global( + column: ColumnId, + entry: EntryId, + total_entry_count: usize, +) -> EntryId { + let reversed_index = + reverse_index(entry, entry_count_in_column::(column, total_entry_count)); + COLUMNS * reversed_index + *column +} + +/// "Reverse" the index in such a way that the first entry becomes the last, and the last becomes +/// the first. +fn reverse_index(index: EntryId, entries_count: usize) -> EntryId { + entries_count.saturating_sub(index).saturating_sub(1) +} diff --git a/app/gui/view/debug_scene/component-group/Cargo.toml b/app/gui/view/debug_scene/component-group/Cargo.toml index 562495e9cff..e1045a0e2e6 100644 --- a/app/gui/view/debug_scene/component-group/Cargo.toml +++ b/app/gui/view/debug_scene/component-group/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] enso-frp = { path = "../../../../../lib/rust/frp" } ensogl-core = { path = "../../../../../lib/rust/ensogl/core" } +ensogl-selector = { path = "../../../../../lib/rust/ensogl/component/selector" } ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" } ensogl-list-view = { path = "../../../../../lib/rust/ensogl/component/list-view" } ensogl-text-msdf-sys = { path = "../../../../../lib/rust/ensogl/component/text/msdf-sys" } diff --git a/app/gui/view/debug_scene/component-group/src/lib.rs b/app/gui/view/debug_scene/component-group/src/lib.rs index 1a45517e2d4..6327afc9d6f 100644 --- a/app/gui/view/debug_scene/component-group/src/lib.rs +++ b/app/gui/view/debug_scene/component-group/src/lib.rs @@ -22,8 +22,11 @@ use ensogl_core::frp; use ensogl_core::Animation; use ensogl_hardcoded_theme as theme; use ensogl_list_view as list_view; +use ensogl_selector as selector; +use ensogl_selector::Bounds; use ensogl_text_msdf_sys::run_once_initialized; use ide_view_component_group as component_group; +use list_view::entry::AnyModelProvider; @@ -47,27 +50,48 @@ pub fn main() { // === Mock Entries === // ==================== -#[derive(Clone, Debug)] +const PREPARED_ITEMS: &[&str; 8] = &[ + "long sample entry with text overflowing the width", + "convert", + "table input", + "text input", + "number input", + "table output", + "data output", + "data input", +]; + +#[derive(Debug)] struct MockEntries { entries: Vec, + count: Cell, } impl MockEntries { - fn new(entries: Vec) -> Self { - Self { entries } + fn new(count: usize) -> Rc { + Rc::new(Self { + entries: PREPARED_ITEMS.iter().cycle().take(count).map(ToString::to_string).collect(), + count: Cell::new(count), + }) } - fn get_entry(&self, i: usize) -> Option { - self.entries.get(i).cloned() + fn set_count(&self, count: usize) { + if self.entries.len() >= count { + self.count.set(count); + } + } + + fn get_entry(&self, id: list_view::entry::Id) -> Option { + self.entries.get(id).cloned() } } impl list_view::entry::ModelProvider for MockEntries { fn entry_count(&self) -> usize { - self.entries.len() + self.count.get() } - fn get(&self, id: usize) -> Option { + fn get(&self, id: list_view::entry::Id) -> Option { self.get_entry(id) } } @@ -78,37 +102,71 @@ impl list_view::entry::ModelProvider for MockEntries { // === Init Application === // ======================== + +// === Helpers ==== + +fn create_selection() -> list_view::selection::View { + let selection = list_view::selection::View::new(Logger::new("Selection")); + selection.color.set(color::Rgba(0.527, 0.554, 0.18, 1.0).into()); + selection.size.set(Vector2(150.0, list_view::entry::HEIGHT)); + selection.corner_radius.set(5.0); + selection +} + +fn component_group(app: &Application) -> component_group::View { + let component_group = app.new_view::(); + let group_name = "Long group name with text overflowing the width"; + component_group.set_header(group_name.to_string()); + component_group.set_width(150.0); + component_group.set_position_x(-300.0); + component_group.set_background_color(color::Rgba(0.927, 0.937, 0.913, 1.0)); + component_group +} + +fn wide_component_group(app: &Application) -> component_group::wide::View { + let wide_component_group = app.new_view::(); + wide_component_group.set_position_x(100.0); + wide_component_group.set_width(450.0); + wide_component_group.set_background_color(color::Rgba(0.927, 0.937, 0.913, 1.0)); + wide_component_group.set_no_items_label_text("No local variables."); + wide_component_group +} + +fn slider(app: &Application) -> selector::NumberPicker { + let slider = app.new_view::(); + app.display.add_child(&slider); + slider.frp.resize(Vector2(400.0, 50.0)); + slider.frp.allow_click_selection(true); + slider.frp.set_bounds(Bounds::new(0.0, 15.0)); + slider.set_position_y(250.0); + slider.frp.set_caption(Some("Items count:".to_string())); + slider +} + + +// === init === + fn init(app: &Application) { theme::builtin::dark::register(&app); theme::builtin::light::register(&app); theme::builtin::light::enable(&app); - let mock_entries = MockEntries::new(vec![ - "long sample entry with text overflowing the width".into(), - "convert".into(), - "table input".into(), - "text input".into(), - "number input".into(), - "table input".into(), - "data output".into(), - "data input".into(), - ]); - + let slider = slider(app); let network = frp::Network::new("Component Group Debug Scene"); - let selection = list_view::selection::View::new(Logger::new("Selection")); - selection.color.set(color::Rgba(0.527, 0.554, 0.18, 1.0).into()); - selection.size.set(Vector2(150.0, list_view::entry::HEIGHT)); - selection.corner_radius.set(5.0); + let selection = create_selection(); let selection_animation = Animation::::new(&network); - let component_group = app.new_view::(); - let provider = list_view::entry::AnyModelProvider::new(mock_entries); - let group_name = "Long group name with text overflowing the width"; - component_group.set_header(group_name.to_string()); - component_group.set_entries(provider); - component_group.set_width(150.0); - component_group.set_background_color(color::Rgba(0.927, 0.937, 0.913, 1.0)); + let wide_selection = create_selection(); + let wide_selection_animation = Animation::::new(&network); + + let component_group = component_group(app); app.display.add_child(&component_group); - app.display.add_child(&selection); + component_group.add_child(&selection); + let wide_component_group = wide_component_group(app); + app.display.add_child(&wide_component_group); + wide_component_group.add_child(&wide_selection); + + + // === Regular Component Group === frp::extend! { network selection_animation.target <+ component_group.selection_position_target; @@ -121,7 +179,47 @@ fn init(app: &Application) { selection_animation.target.emit(component_group.selection_position_target.value()); selection_animation.skip.emit(()); + + // === Wide Component Group === + + frp::extend! { network + wide_selection_animation.target <+ wide_component_group.selection_position_target; + eval wide_selection_animation.value ((pos) wide_selection.set_position_xy(*pos)); + + eval wide_component_group.suggestion_accepted ([](id) DEBUG!("[Wide] Accepted Suggestion {id}")); + eval wide_component_group.expression_accepted ([](id) DEBUG!("[Wide] Accepted Expression {id}")); + + no_entries <- wide_component_group.entry_count.map(|count| *count == 0); + hide_selection <- no_entries.on_true(); + show_selection <- no_entries.on_false(); + eval_ hide_selection (wide_selection.color.set(color::Rgba::transparent().into())); + eval_ show_selection (wide_selection.color.set(color::Rgba(0.527, 0.554, 0.18, 1.0).into())); + } + wide_selection_animation.target.emit(wide_component_group.selection_position_target.value()); + wide_selection_animation.skip.emit(()); + + + // === Setup slider to change entry count === + + let mock_entries = MockEntries::new(25); + let model_provider = AnyModelProvider::from(mock_entries.clone_ref()); + frp::extend! { network + int_value <- slider.frp.output.value.map(|v| *v as usize); + eval int_value([component_group, wide_component_group](i) { + mock_entries.set_count(*i); + component_group.set_entries(model_provider.clone_ref()); + wide_component_group.set_entries(model_provider.clone_ref()); + }); + } + slider.frp.set_value(10.0); + // Select the bottom left entry at the start. + let first_column = component_group::wide::ColumnId::new(0); + wide_component_group.select_entry(first_column, 0); + + std::mem::forget(slider); std::mem::forget(network); std::mem::forget(selection); std::mem::forget(component_group); + std::mem::forget(wide_component_group); + std::mem::forget(wide_selection); } diff --git a/lib/rust/ensogl/component/sequence-diagram/src/labeled_line.rs b/lib/rust/ensogl/component/sequence-diagram/src/labeled_line.rs index 36e21f7acec..3d75a2bc992 100644 --- a/lib/rust/ensogl/component/sequence-diagram/src/labeled_line.rs +++ b/lib/rust/ensogl/component/sequence-diagram/src/labeled_line.rs @@ -1,10 +1,12 @@ //! A visual line capped with an arrow, that shows a tooltip on mouse hover. + use ensogl_core::display::shape::*; use ensogl_core::prelude::*; use crate::shape; use crate::shape::CAP_WIDTH; use crate::shape::HOVER_PADDING; + use ensogl::frp; use ensogl_core::application::tooltip; use ensogl_core::application::Application; diff --git a/lib/rust/ensogl/component/sequence-diagram/src/lib.rs b/lib/rust/ensogl/component/sequence-diagram/src/lib.rs index a2de689e434..c59e54fd62e 100644 --- a/lib/rust/ensogl/component/sequence-diagram/src/lib.rs +++ b/lib/rust/ensogl/component/sequence-diagram/src/lib.rs @@ -13,13 +13,11 @@ #![warn(unused_import_braces)] #![warn(unused_qualifications)] -pub mod labeled_line; -pub mod shape; - use ensogl_core::prelude::*; use crate::labeled_line::Cap; use crate::labeled_line::LabeledLine; + use enso_profiler_data::Profile; use enso_profiler_enso_data::Metadata; use ensogl::frp; @@ -29,6 +27,14 @@ use ensogl_core::display::shape::StyleWatchFrp; use ensogl_gui_component::component; +// ============== +// === Export === +// ============== + +pub mod labeled_line; +pub mod shape; + + // ================= // === Constants === diff --git a/lib/rust/ensogl/component/sequence-diagram/src/shape.rs b/lib/rust/ensogl/component/sequence-diagram/src/shape.rs index c5f072db1ac..95ac2ff7b19 100644 --- a/lib/rust/ensogl/component/sequence-diagram/src/shape.rs +++ b/lib/rust/ensogl/component/sequence-diagram/src/shape.rs @@ -1,8 +1,9 @@ //! Shape used for the LabeledLine. + +use ensogl_core::display::shape::*; use ensogl_core::prelude::*; use ensogl_core::data::color; -use ensogl_core::display::shape::*; diff --git a/lib/rust/ensogl/component/tooltip/src/lib.rs b/lib/rust/ensogl/component/tooltip/src/lib.rs index 68e70a8a137..463124eb231 100644 --- a/lib/rust/ensogl/component/tooltip/src/lib.rs +++ b/lib/rust/ensogl/component/tooltip/src/lib.rs @@ -1,6 +1,9 @@ //! The `Tooltip` shows extra information for UI components. It is pegged to the cursor location //! and appears when it receives information to show. -use ensogl_core as ensogl; + +// === Standard Linter Configuration === +#![deny(non_ascii_idents)] +#![warn(unsafe_code)] use ensogl::prelude::*; @@ -9,13 +12,20 @@ use ensogl::animation::hysteretic::HystereticAnimation; use ensogl::application::Application; use ensogl::display; use ensogl::display::shape::StyleWatch; +use ensogl_core as ensogl; use ensogl_core::application::tooltip::Placement; use ensogl_core::application::tooltip::Style; use ensogl_label::Label; + +// ============== +// === Export === +// ============== + pub use ensogl::application::tooltip; + // ================= // === Constants === // ================= diff --git a/lib/rust/ensogl/core/src/application.rs b/lib/rust/ensogl/core/src/application.rs index 98a596e4416..2fe466192c5 100644 --- a/lib/rust/ensogl/core/src/application.rs +++ b/lib/rust/ensogl/core/src/application.rs @@ -13,7 +13,6 @@ use crate::gui::cursor::Cursor; use crate::system::web; - // ============== // === Export === // ============== @@ -24,8 +23,11 @@ pub mod frp; pub mod shortcut; pub mod tooltip; pub mod view; + pub use view::View; + + /// A module with commonly used traits to mass import. pub mod traits { pub use crate::application::view::View as TRAIT_View; diff --git a/lib/rust/profiler/src/format.rs b/lib/rust/profiler/src/format.rs index b41ccfe57f4..f6ca8627e8b 100644 --- a/lib/rust/profiler/src/format.rs +++ b/lib/rust/profiler/src/format.rs @@ -7,11 +7,16 @@ use serde::Deserialize; use serde::Serialize; +// ============== +// === Export === +// ============== pub mod builder; pub use builder::Builder; + + /// Metadata of any type. pub type AnyMetadata = Box;