diff --git a/.prettierignore b/.prettierignore index bce3654ee7d9..835c88620e5e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -33,3 +33,4 @@ enso/ # Popular IDEs .idea +.bsp diff --git a/CHANGELOG.md b/CHANGELOG.md index fd6906d5a0bb..db580af7713b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,10 @@ component][3385]. Use the set_font and set_bold_bytes respectively. - [Fixed a text rendering issue in nested sublayer][3486]. +- [Added a new component: Grid View.][3588] It's parametrized by Entry object, + display them arranged in a Grid. It does not instantiate all entries, only + those visible, and re-use created entries during scrolling thus achieving + great performance. #### Enso Standard Library @@ -253,6 +257,7 @@ [3573]: https://github.com/enso-org/enso/pull/3573 [3583]: https://github.com/enso-org/enso/pull/3583 [3581]: https://github.com/enso-org/enso/pull/3581 +[3588]: https://github.com/enso-org/enso/pull/3588 #### Enso Compiler diff --git a/Cargo.lock b/Cargo.lock index 5682d15d5e85..c84d4dd285fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2325,6 +2325,7 @@ dependencies = [ "ensogl-drop-manager", "ensogl-file-browser", "ensogl-flame-graph", + "ensogl-grid-view", "ensogl-label", "ensogl-list-view", "ensogl-scroll-area", @@ -2484,6 +2485,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ensogl-example-grid-view" +version = "0.1.0" +dependencies = [ + "enso-frp", + "enso-text", + "ensogl-core", + "ensogl-grid-view", + "ensogl-hardcoded-theme", + "ensogl-text-msdf-sys", + "wasm-bindgen", +] + [[package]] name = "ensogl-example-list-view" version = "0.1.0" @@ -2627,6 +2641,7 @@ dependencies = [ "ensogl-example-drop-manager", "ensogl-example-easing-animator", "ensogl-example-glyph-system", + "ensogl-example-grid-view", "ensogl-example-list-view", "ensogl-example-mouse-events", "ensogl-example-profiling-run-graph", @@ -2661,6 +2676,20 @@ dependencies = [ "ensogl-text", ] +[[package]] +name = "ensogl-grid-view" +version = "0.1.0" +dependencies = [ + "approx 0.5.1", + "enso-frp", + "ensogl-core", + "ensogl-hardcoded-theme", + "ensogl-scroll-area", + "ensogl-shadow", + "ensogl-text", + "itertools 0.10.3", +] + [[package]] name = "ensogl-gui-component" version = "0.1.0" @@ -3216,14 +3245,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if 1.0.0", "js-sys", "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -4435,7 +4464,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", ] [[package]] @@ -5390,7 +5419,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", ] [[package]] @@ -5533,7 +5562,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", "redox_syscall 0.2.13", "thiserror", ] @@ -7007,7 +7036,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", "serde", "sha1 0.6.1", ] @@ -7018,7 +7047,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", "serde", ] diff --git a/app/gui/src/controller/searcher.rs b/app/gui/src/controller/searcher.rs index 7b6b22c6bdca..4ec889655ac7 100644 --- a/app/gui/src/controller/searcher.rs +++ b/app/gui/src/controller/searcher.rs @@ -25,7 +25,6 @@ use flo_stream::Subscriber; use parser::Parser; - // ============== // === Export === // ============== diff --git a/app/gui/view/component-browser/searcher-list-panel/src/lib.rs b/app/gui/view/component-browser/searcher-list-panel/src/lib.rs index 7a5419ecd2f8..d3678739a069 100644 --- a/app/gui/view/component-browser/searcher-list-panel/src/lib.rs +++ b/app/gui/view/component-browser/searcher-list-panel/src/lib.rs @@ -447,7 +447,7 @@ impl Model { if let Some(navigator) = self.navigator.borrow().as_ref() { navigator.disable() } else { - tracing::log::warn!( + tracing::warn!( "Navigator was not initialised on ComponentBrowserPanel. \ Scroll events will not be handled correctly." ) diff --git a/build-config.yaml b/build-config.yaml index dace8b030cd3..faf3028467e2 100644 --- a/build-config.yaml +++ b/build-config.yaml @@ -1,6 +1,6 @@ # Options intended to be common for all developers. -wasm-size-limit: 5.06 MiB +wasm-size-limit: 5.13 MiB required-versions: cargo-watch: ^8.1.1 diff --git a/lib/rust/ensogl/component/Cargo.toml b/lib/rust/ensogl/component/Cargo.toml index b08b951b22eb..61fda94d9f2c 100644 --- a/lib/rust/ensogl/component/Cargo.toml +++ b/lib/rust/ensogl/component/Cargo.toml @@ -12,6 +12,7 @@ ensogl-file-browser = { path = "file-browser" } ensogl-flame-graph = { path = "flame-graph" } ensogl-label = { path = "label" } ensogl-list-view = { path = "list-view" } +ensogl-grid-view = { path = "grid-view" } ensogl-scroll-area = { path = "scroll-area" } ensogl-scrollbar = { path = "scrollbar" } ensogl-selector = { path = "selector" } diff --git a/lib/rust/ensogl/component/grid-view/Cargo.toml b/lib/rust/ensogl/component/grid-view/Cargo.toml new file mode 100644 index 000000000000..7c70b069c4c0 --- /dev/null +++ b/lib/rust/ensogl/component/grid-view/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ensogl-grid-view" +version = "0.1.0" +authors = ["Enso Team "] +edition = "2021" + +[dependencies] +enso-frp = { path = "../../../frp" } +ensogl-core = { path = "../../core" } +ensogl-hardcoded-theme = { path = "../../app/theme/hardcoded" } +ensogl-shadow = { path = "../shadow" } +ensogl-text = { path = "../text" } +ensogl-scroll-area = { path = "../scroll-area" } +itertools = "0.10.3" + +[dev-dependencies] +approx = "0.5.1" diff --git a/lib/rust/ensogl/component/grid-view/src/entry.rs b/lib/rust/ensogl/component/grid-view/src/entry.rs new file mode 100644 index 000000000000..e49fc22f5638 --- /dev/null +++ b/lib/rust/ensogl/component/grid-view/src/entry.rs @@ -0,0 +1,54 @@ +//! A module with an [`Entry`] abstraction for [`crate::GridView`]. `GridView` can be parametrized +//! by any entry with the specified API. + +use crate::prelude::*; + +use enso_frp as frp; +use ensogl_core::application::Application; +use ensogl_core::display; +use ensogl_core::display::scene::Layer; + + + +// =========== +// === FRP === +// =========== + +ensogl_core::define_endpoints_2! { + Input { + set_model(Model), + set_size(Vector2), + set_params(Params), + } + Output {} +} + +/// FRP Api of a specific Entry. +pub type EntryFrp = Frp<::Model, ::Params>; + + + +// ============= +// === Trait === +// ============= + +/// The abstraction of Entry for [`crate::GridView`]. +/// +/// The entry may be any [`display::Object`] which can provide the [`EntryFRP`] API. +pub trait Entry: CloneRef + Debug + display::Object + 'static { + /// The model of this entry. The entry should be a representation of data from the Model. + /// For example, the entry being just a caption can have [`String`] as its model - the text to + /// be displayed. + type Model: Clone + Debug + Default; + + /// A type parametrizing the various aspects of the entry, independed of the Model (for example + /// the text color). The parameters are set in [`crate::GridView`] and shared between all + /// entries. + type Params: Clone + Debug + Default; + + /// An Entry constructor. + fn new(app: &Application, text_layer: &Option) -> Self; + + /// FRP endpoints getter. + fn frp(&self) -> &EntryFrp; +} diff --git a/lib/rust/ensogl/component/grid-view/src/lib.rs b/lib/rust/ensogl/component/grid-view/src/lib.rs new file mode 100644 index 000000000000..7d5ed1ac4b19 --- /dev/null +++ b/lib/rust/ensogl/component/grid-view/src/lib.rs @@ -0,0 +1,510 @@ +//! Grid View EnsoGL Component. +//! +//! The main structure is [`GridView`] - see its docs for details. + +#![recursion_limit = "1024"] +// === Features === +#![feature(option_result_contains)] +#![feature(trait_alias)] +#![feature(hash_drain_filter)] +// === Standard Linter Configuration === +#![deny(non_ascii_idents)] +#![warn(unsafe_code)] +// === Non-Standard Linter Configuration === +#![warn(missing_copy_implementations)] +#![warn(missing_debug_implementations)] +#![warn(missing_docs)] +#![warn(trivial_casts)] +#![warn(trivial_numeric_casts)] +#![warn(unused_import_braces)] +#![warn(unused_qualifications)] + + +// ============== +// === Export === +// ============== + +pub mod entry; +pub mod scrollable; +pub mod simple; +pub mod visible_area; + +pub use ensogl_scroll_area::Viewport; + + + +/// Commonly used types and functions. +pub mod prelude { + pub use ensogl_core::prelude::*; +} + +use crate::prelude::*; + +use enso_frp as frp; +use ensogl_core::application::command::FrpNetworkProvider; +use ensogl_core::application::Application; +use ensogl_core::display; +use ensogl_core::display::scene::layer::WeakLayer; +use ensogl_core::display::scene::Layer; +use ensogl_core::gui::Widget; + +use crate::entry::EntryFrp; +use crate::visible_area::all_visible_locations; +use crate::visible_area::visible_columns; +use crate::visible_area::visible_rows; +pub use entry::Entry; + + + +// =========== +// === FRP === +// =========== + +/// A row index in [`GridView`]. +pub type Row = usize; +/// A column index in [`GridView`]. +pub type Col = usize; + +ensogl_core::define_endpoints_2! { + + Input { + /// Declare what area of the GridView is visible. The area position is relative to left-top + /// corner of the Grid View. + set_viewport(Viewport), + /// Reset entries, providing number of rows and columns. All currently displayed entries + /// will be detached and their models re-requested. + reset_entries(Row, Col), + /// Provide model for specific entry. Should be called only after `model_for_entry_needed` + /// event for given row and column. After that the entry will be visible. + model_for_entry(Row, Col, EntryModel), + /// Set the entries size. All entries have the same size. + set_entries_size(Vector2), + /// Set the entries parameters. + set_entries_params(EntryParams), + /// Set the layer for any texts rendered by entries. The layer will be passed to entries' + /// constructors. **Performance note**: This will re-instantiate all entries. + set_text_layer(Option), + } + + Output { + row_count(Row), + column_count(Col), + viewport(Viewport), + entries_size(Vector2), + entries_params(EntryParams), + content_size(Vector2), + /// Event emitted when the Grid View needs model for an uncovered entry. + model_for_entry_needed(Row, Col), + } +} + + + +// ============= +// === Model === +// ============= + +// === EntryCreationCtx === + +/// A structure gathering all data required for creating new entry instance. +#[derive(CloneRef, Debug, Derivative)] +#[derivative(Clone(bound = ""))] +struct EntryCreationCtx { + app: Application, + network: frp::WeakNetwork, + set_entry_size: frp::Stream, + set_entry_params: frp::Stream, +} + +impl EntryCreationCtx { + fn create_entry>(&self, text_layer: &Option) -> E { + let entry = E::new(&self.app, text_layer); + if let Some(network) = self.network.upgrade_or_warn() { + let entry_frp = entry.frp(); + let entry_network = entry_frp.network(); + frp::new_bridge_network! { [network, entry_network] grid_view_entry_bridge + init <- source_(); + entry_frp.set_size <+ all(init, self.set_entry_size)._1(); + entry_frp.set_params <+ all(init, self.set_entry_params)._1(); + } + init.emit(()); + } + entry + } +} + +fn set_entry_position(entry: &E, row: Row, col: Col, entry_size: Vector2) { + let x = (col as f32 + 0.5) * entry_size.x; + let y = (row as f32 + 0.5) * -entry_size.y; + entry.set_position_xy(Vector2(x, y)); +} + + +// === Properties === + +#[derive(Copy, Clone, Debug, Default)] +struct Properties { + row_count: usize, + col_count: usize, + viewport: Viewport, + entries_size: Vector2, +} + + +// === Model === + +/// The Model of [`GridView`]. +#[derive(Clone, Debug)] +pub struct Model { + display_object: display::object::Instance, + visible_entries: RefCell>, + free_entries: RefCell>, + entry_creation_ctx: EntryCreationCtx, +} + +impl Model { + fn new(entry_creation_ctx: EntryCreationCtx) -> Self { + let logger = Logger::new("GridView"); + let display_object = display::object::Instance::new(&logger); + let visible_entries = default(); + let free_entries = default(); + Model { display_object, visible_entries, free_entries, entry_creation_ctx } + } +} + +impl Model { + fn update_entries_visibility(&self, properties: Properties) -> Vec<(Row, Col)> { + let Properties { viewport, entries_size, row_count, col_count } = properties; + let mut visible_entries = self.visible_entries.borrow_mut(); + let mut free_entries = self.free_entries.borrow_mut(); + let visible_rows = visible_rows(&viewport, entries_size, row_count); + let visible_cols = visible_columns(&viewport, entries_size, col_count); + let no_longer_visible = visible_entries.drain_filter(|(row, col), _| { + !visible_rows.contains(row) || !visible_cols.contains(col) + }); + let detached = no_longer_visible.map(|(_, entry)| { + entry.unset_parent(); + entry + }); + free_entries.extend(detached); + let uncovered = all_visible_locations(&viewport, entries_size, row_count, col_count) + .filter(|loc| !visible_entries.contains_key(loc)); + uncovered.collect_vec() + } + + fn update_after_entries_size_change(&self, properties: Properties) -> Vec<(Row, Col)> { + let to_model_request = self.update_entries_visibility(properties); + for ((row, col), visible_entry) in &*self.visible_entries.borrow() { + set_entry_position(visible_entry, *row, *col, properties.entries_size); + } + to_model_request + } + + fn reset_entries(&self, properties: Properties) -> Vec<(Row, Col)> { + let Properties { viewport, entries_size, row_count, col_count } = properties; + let mut visible_entries = self.visible_entries.borrow_mut(); + let mut free_entries = self.free_entries.borrow_mut(); + let detached = visible_entries.drain().map(|(_, entry)| { + entry.unset_parent(); + entry + }); + free_entries.extend(detached); + all_visible_locations(&viewport, entries_size, row_count, col_count).collect_vec() + } + + fn drop_all_entries(&self, properties: Properties) -> Vec<(Row, Col)> { + let to_model_request = self.reset_entries(properties); + self.free_entries.borrow_mut().clear(); + to_model_request + } +} + +impl Model { + fn update_entry( + &self, + row: Row, + col: Col, + model: E::Model, + entry_size: Vector2, + text_layer: &Option, + ) { + use std::collections::hash_map::Entry::*; + let mut visible_entries = self.visible_entries.borrow_mut(); + let mut free_entries = self.free_entries.borrow_mut(); + let create_new_entry = || { + let text_layer = text_layer.as_ref().and_then(|l| l.upgrade()); + self.entry_creation_ctx.create_entry(&text_layer) + }; + let entry = match visible_entries.entry((row, col)) { + Occupied(entry) => entry.into_mut(), + Vacant(lack_of_entry) => { + let new_entry = free_entries.pop().unwrap_or_else(create_new_entry); + set_entry_position(&new_entry, row, col, entry_size); + self.display_object.add_child(&new_entry); + lack_of_entry.insert(new_entry) + } + }; + entry.frp().set_model(model); + } +} + + + +// ================ +// === GridView === +// ================ + +/// A template for [`GridView`] structure, where entry parameters and model are separate generic +/// arguments. +/// +/// It may be useful when using GridView in parametrized structs, where we want to avoid rewriting +/// `Entry` bound in each place. Otherwise, it's better to use [`GridView`]. +/// +/// Note that some bounds are still required, as we use [`Widget`] and [`Frp`] nodes. +#[derive(CloneRef, Debug, Deref, Derivative)] +#[derivative(Clone(bound = ""))] +pub struct GridViewTemplate< + Entry: 'static, + EntryModel: frp::node::Data, + EntryParams: frp::node::Data, +> { + widget: Widget, Frp>, +} + +/// Grid View Component. +/// +/// This Component displays any kind of entry `E` in a grid. To have it working, you need to +/// * Set entries size ([`Frp::set_entries_size`]), +/// * Declare (and keep up-to-date) the visible area ([`Frp::set_viewport`]), +/// * Set up logic for providing models (see _Requesting for Models_ section). +/// * Optionally: entries parameters, if given entry does not have sensible default. +/// * Finally, reset the content, providing number of rows and columns ([`Frp::reset_entries`]). +/// +/// # Positioning +/// +/// Please mark, that this structure has its left-top corner docked to (0, 0) point of parent +/// display object, as this is a more intuitive way with handling grids. +/// +/// # Entries Instantiation +/// +/// The entry should implement [`Entry`] trait. Entries are instantiated lazily, only those visible +/// in provided [`Frp::view_area`]. Once entries are no longer visible, are detached, but not +/// dropped and may be re-used to display new entries when needed. This way we can achieve very +/// efficient scrolling. +/// +/// ## Requesting for Models +/// +/// Once an entry is uncovered, the Grid View emits [`Frp::model_for_entry_needed`]. Then the proper +/// model should be provided using [`Frp::model_for_entry`] endpoint - only then the entry will be +/// displayed. +/// +/// **Important**. The [`Frp::model_for_entry_needed`] are emitted once when needed and not repeated +/// anymore, after adding connections to this FRP node in particular. Therefore, be sure, that you +/// connect providing models logic before emitting any of [`Frp::set_entries_size`] or +/// [`Frp::set_viewport`]. +pub type GridView = GridViewTemplate::Model, ::Params>; + +impl GridView { + /// Create new Grid View. + pub fn new(app: &Application) -> Self { + let frp = Frp::new(); + let network = frp.network(); + let input = &frp.private.input; + let out = &frp.private.output; + frp::extend! { network + set_entry_size <- input.set_entries_size.sampler(); + set_entry_params <- input.set_entries_params.sampler(); + } + let entry_creation_ctx = EntryCreationCtx { + app: app.clone_ref(), + network: network.downgrade(), + set_entry_size: set_entry_size.into(), + set_entry_params: set_entry_params.into(), + }; + let model = Rc::new(Model::new(entry_creation_ctx)); + frp::extend! { network + out.row_count <+ input.reset_entries._0(); + out.column_count <+ input.reset_entries._1(); + out.viewport <+ input.set_viewport; + out.entries_size <+ input.set_entries_size; + out.entries_params <+ input.set_entries_params; + prop <- all_with4( + &out.row_count, &out.column_count, &out.viewport, &out.entries_size, + |&row_count, &col_count, &viewport, &entries_size| { + Properties { row_count, col_count, viewport, entries_size } + } + ); + + content_size_params <- all(input.reset_entries, input.set_entries_size); + out.content_size <+ content_size_params.map(|&((rows, cols), esz)| Self::content_size(rows, cols, esz)); + + request_models_after_vis_area_change <= + input.set_viewport.map2(&prop, f!((_, p) model.update_entries_visibility(*p))); + request_models_after_entry_size_change <= input.set_entries_size.map2( + &prop, + f!((_, p) model.update_after_entries_size_change(*p)) + ); + request_models_after_reset <= + input.reset_entries.map2(&prop, f!((_, p) model.reset_entries(*p))); + request_models_after_text_layer_change <= + input.set_text_layer.map2(&prop, f!((_, p) model.drop_all_entries(*p))); + out.model_for_entry_needed <+ request_models_after_vis_area_change; + out.model_for_entry_needed <+ request_models_after_entry_size_change; + out.model_for_entry_needed <+ request_models_after_reset; + out.model_for_entry_needed <+ request_models_after_text_layer_change; + + model_prop_and_layer <- + input.model_for_entry.map3(&prop, &input.set_text_layer, |model, prop, layer| (model.clone(), *prop, layer.clone())); + eval model_prop_and_layer + ((((row, col, entry_model), prop, layer): &((Row, Col, E::Model), Properties, Option)) + model.update_entry(*row, *col, entry_model.clone(), prop.entries_size, layer) + ); + } + let display_object = model.display_object.clone_ref(); + let widget = Widget::new(app, frp, model, display_object); + Self { widget } + } + + fn content_size(row_count: Row, col_count: Col, entries_size: Vector2) -> Vector2 { + let x = col_count as f32 * entries_size.x; + let y = row_count as f32 * entries_size.y; + Vector2(x, y) + } +} + +impl display::Object + for GridViewTemplate +{ + fn display_object(&self) -> &display::object::Instance { + self.widget.display_object() + } +} + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Copy, Clone, CloneRef, Debug, Default)] + struct TestEntryParams { + param: Immutable, + } + + #[derive(Clone, CloneRef, Debug)] + struct TestEntry { + frp: EntryFrp, + param_set: Rc>, + model_set: Rc>, + display_object: display::object::Instance, + } + + impl Entry for TestEntry { + type Model = Immutable; + type Params = TestEntryParams; + + fn new(_app: &Application, _: &Option) -> Self { + let frp = entry::EntryFrp::::new(); + let network = frp.network(); + let param_set = Rc::new(Cell::new(0)); + let model_set = Rc::new(Cell::new(0)); + let display_object = display::object::Instance::new(Logger::new("TestEntry")); + frp::extend! { network + eval frp.input.set_model ((model) model_set.set(**model)); + eval frp.input.set_params ((param) param_set.set(*param.param)); + } + Self { frp, param_set, model_set, display_object } + } + + fn frp(&self) -> &EntryFrp { + &self.frp + } + } + + impl display::Object for TestEntry { + fn display_object(&self) -> &display::object::Instance { + &self.display_object + } + } + + #[test] + fn initializing_grid_view() { + let app = Application::new("root"); + let grid_view = GridView::::new(&app); + let network = grid_view.network(); + frp::extend! { network + updates_requested <- grid_view.model_for_entry_needed.count().sampler(); + } + + let vis_area = Viewport { left: 0.0, top: 0.0, right: 100.0, bottom: -100.0 }; + grid_view.set_entries_size(Vector2(20.0, 20.0)); + grid_view.reset_entries(100, 100); + grid_view.set_viewport(vis_area); + grid_view.set_entries_params(TestEntryParams { param: Immutable(13) }); + + assert_eq!(grid_view.model().visible_entries.borrow().len(), 0); + assert_eq!(updates_requested.value(), 25); + + for i in 0..5 { + for j in 0..5 { + grid_view.model_for_entry(i, j, Immutable(i * 200 + j)); + } + } + + { + let created_entries = grid_view.model().visible_entries.borrow(); + assert_eq!(created_entries.len(), 25); + for ((row, col), entry) in created_entries.iter() { + assert_eq!(entry.model_set.get(), row * 200 + col); + assert_eq!(entry.param_set.get(), 13); + } + } + } + + #[test] + fn updating_entries_after_viewport_change() { + let app = Application::new("root"); + let grid_view = GridView::::new(&app); + let network = grid_view.network(); + let initial_vis_area = Viewport { left: 0.0, top: 0.0, right: 100.0, bottom: -100.0 }; + grid_view.set_entries_size(Vector2(20.0, 20.0)); + grid_view.reset_entries(100, 100); + grid_view.set_viewport(initial_vis_area); + grid_view.set_entries_params(TestEntryParams { param: Immutable(13) }); + + for i in 0..5 { + for j in 0..5 { + grid_view.model_for_entry(i, j, Immutable(i * 200 + j)); + } + } + + frp::extend! { network + updates_requested <- grid_view.model_for_entry_needed.count().sampler(); + } + + let uncovering_new_entries = + Viewport { left: 5.0, top: -5.0, right: 105.0, bottom: -105.0 }; + grid_view.set_viewport(uncovering_new_entries); + assert_eq!(updates_requested.value(), 11); + assert_eq!(grid_view.model().visible_entries.borrow().len(), 25); + + for i in 0..6 { + grid_view.model_for_entry(5, i, Immutable(200 * 5 + i)); + } + for i in 0..5 { + grid_view.model_for_entry(i, 5, Immutable(200 * i + 5)); + } + assert_eq!(grid_view.model().visible_entries.borrow().len(), 36); + + let hiding_old_entries = + Viewport { left: 20.0, top: -20.0, right: 120.0, bottom: -120.0 }; + grid_view.set_viewport(hiding_old_entries); + assert_eq!(updates_requested.value(), 11); // Count should not change. + assert_eq!(grid_view.model().visible_entries.borrow().len(), 25); + assert_eq!(grid_view.model().free_entries.borrow().len(), 11); + } +} diff --git a/lib/rust/ensogl/component/grid-view/src/scrollable.rs b/lib/rust/ensogl/component/grid-view/src/scrollable.rs new file mode 100644 index 000000000000..849345536f56 --- /dev/null +++ b/lib/rust/ensogl/component/grid-view/src/scrollable.rs @@ -0,0 +1,83 @@ +//! Module containing scrollable version of [`GridView`]. + +use crate::prelude::*; + +use crate::Entry; + +use enso_frp as frp; +use ensogl_core::application::command::FrpNetworkProvider; +use ensogl_core::application::Application; +use ensogl_core::display; +use ensogl_core::display::scene::Layer; +use ensogl_scroll_area::ScrollArea; + + + +// ================ +// === GridView === +// ================ + +/// A template for [`GridView`] structure, where entry parameters and model are separate generic +/// arguments, similar to [`crate::GridViewTemplate`] - see its docs for details. +#[derive(CloneRef, Debug, Deref, Derivative)] +#[derivative(Clone(bound = ""))] +pub struct GridViewTemplate { + area: ScrollArea, + #[deref] + grid: crate::GridViewTemplate, + text_layer: Layer, +} + +/// Scrollable Grid View Component. +/// +/// This Component displays any kind of entry `E` in a grid. It's a wrapper putting the +/// [`crate::GridView`] into [`ScrollArea`] and updating the wrapped grid view with [`ScrollArea`]'s +/// viewport. +/// +/// The FRP API of [`crate::GridView`] is exposed as [`Deref`] target. The [`scroll_frp`] +/// method gives access to [`ScrollArea`] API. +/// +/// To have it working you must do same steps as in [`crate::GridView`], but instead of setting +/// viewport you must set size of ScrollArea by calling [`resize`] method, or using `resize` +/// endpoint of [`ScrollArea`] API. +/// +/// See [`crate::GridView`] docs for more info about entries instantiation and process of requesting +/// for Models. +pub type GridView = GridViewTemplate::Model, ::Params>; + +impl GridView { + /// Create new Scrollable Grid View component. + pub fn new(app: &Application) -> Self { + let area = ScrollArea::new(app); + let grid = crate::GridView::::new(app); + area.content().add_child(&grid); + let network = grid.network(); + let text_layer = area.content_layer().create_sublayer(); + grid.set_text_layer(Some(text_layer.downgrade())); + + frp::extend! { network + grid.set_viewport <+ area.viewport; + area.set_content_width <+ grid.content_size.map(|s| s.x); + area.set_content_height <+ grid.content_size.map(|s| s.y); + } + + Self { area, grid, text_layer } + } + + /// Resize the component. It's a wrapper for [`scroll_frp`]`().resize`. + pub fn resize(&self, new_size: Vector2) { + self.area.resize(new_size); + } + + /// Access the [`ScrollArea`] FRP API. This way you can read scroll position, resize component + /// or jump at position. + pub fn scroll_frp(&self) -> &ensogl_scroll_area::Frp { + self.area.deref() + } +} + +impl display::Object for GridViewTemplate { + fn display_object(&self) -> &display::object::Instance { + self.area.display_object() + } +} diff --git a/lib/rust/ensogl/component/grid-view/src/simple.rs b/lib/rust/ensogl/component/grid-view/src/simple.rs new file mode 100644 index 000000000000..58af12ffe57b --- /dev/null +++ b/lib/rust/ensogl/component/grid-view/src/simple.rs @@ -0,0 +1,183 @@ +//! A module defining the [`SimpleGridView`] with all helper structures. + +use crate::prelude::*; +use ensogl_core::display::shape::*; + +use crate::scrollable; +use crate::EntryFrp; + +use ensogl_core::application::command::FrpNetworkProvider; +use ensogl_core::application::frp::API; +use ensogl_core::application::Application; +use ensogl_core::data::color; +use ensogl_core::display; +use ensogl_core::display::scene::Layer; +use ensogl_text as text; + + + +// ================== +// === Background === +// ================== + +/// The background of single Entry. The actually displayed rectangle is shrunk by [`PADDING_PX`] +/// from the shape size, to avoid antialiasing glitches. +pub mod entry_background { + use super::*; + + /// A padding added to the background rectangle to avoid antialiasing glitches. + pub const PADDING_PX: f32 = 5.0; + + ensogl_core::define_shape_system! { + (style:Style, color: Vector4) { + let shape_width : Var = "input_size.x".into(); + let shape_height : Var = "input_size.y".into(); + let width = shape_width - 2.0.px() * PADDING_PX; + let height = shape_height - 2.0.px() * PADDING_PX; + Rect((width, height)).fill(color).into() + } + } +} + + + +// =================== +// === EntryParams === +// =================== + +/// The parameters of [`SimpleGridView`]`s entries. +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub struct EntryParams { + pub bg_color: color::Rgba, + pub bg_margin: f32, + pub font: ImString, + pub text_offset: f32, + pub text_size: text::Size, + pub text_color: color::Rgba, +} + +impl Default for EntryParams { + fn default() -> Self { + Self { + bg_color: color::Rgba::transparent(), + bg_margin: 0.0, + font: text::typeface::font::DEFAULT_FONT.into(), + text_offset: 7.0, + text_size: text::Size(14.0), + text_color: default(), + } + } +} + + + +// ============= +// === Entry === +// ============= + +// === EntryData === + +/// An internal structure of [`Entry`], which may be passed to FRP network. +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub struct EntryData { + display_object: display::object::Instance, + pub label: text::Area, + pub background: entry_background::View, +} + +impl EntryData { + fn new(app: &Application, text_layer: &Option) -> Self { + let logger = Logger::new("list_view::entry::Label"); + let display_object = display::object::Instance::new(&logger); + let label = app.new_view::(); + let background = entry_background::View::new(&logger); + display_object.add_child(&label); + display_object.add_child(&background); + if let Some(layer) = text_layer { + label.add_to_scene_layer(layer); + } + Self { display_object, label, background } + } + + fn update_layout( + &self, + entry_size: Vector2, + bg_margin: f32, + text_size: text::Size, + text_offset: f32, + ) { + use entry_background::PADDING_PX; + let bg_size = entry_size - Vector2(bg_margin, bg_margin) * 2.0; + let bg_size_with_padding = bg_size + Vector2(PADDING_PX, PADDING_PX) * 2.0; + self.background.size.set(bg_size_with_padding); + self.label.set_position_xy(Vector2(text_offset - entry_size.x / 2.0, text_size.raw / 2.0)); + } +} + + +// === Entry === + +/// A [`SimpleGridView`] entry - a label with background. +#[derive(Clone, CloneRef, Debug)] +pub struct Entry { + frp: EntryFrp, + data: Rc, +} + +impl crate::Entry for Entry { + type Model = ImString; + type Params = EntryParams; + + fn new(app: &Application, text_layer: &Option) -> Self { + let data = Rc::new(EntryData::new(app, text_layer)); + let frp = EntryFrp::::new(); + let input = &frp.private().input; + let network = frp.network(); + + enso_frp::extend! { network + bg_color <- input.set_params.map(|p| p.bg_color).on_change(); + bg_margin <- input.set_params.map(|p| p.bg_margin).on_change(); + font <- input.set_params.map(|p| p.font.clone_ref()).on_change(); + text_offset <- input.set_params.map(|p| p.text_offset).on_change(); + text_color <- input.set_params.map(|p| p.text_color).on_change(); + text_size <- input.set_params.map(|p| p.text_size).on_change(); + + layout <- all(input.set_size, bg_margin, text_size, text_offset); + eval layout ((&(es, m, ts, to)) data.update_layout(es, m, ts, to)); + + eval bg_color ((color) data.background.color.set(color.into())); + data.label.set_default_color <+ text_color.on_change(); + data.label.set_font <+ font.on_change().map(ToString::to_string); + data.label.set_default_text_size <+ text_size.on_change(); + + content <- input.set_model.map(|s| s.to_string()); + max_width_px <- input.set_size.map(|size| size.x); + data.label.set_content_truncated <+ all(&content, &max_width_px); + } + Self { frp, data } + } + + fn frp(&self) -> &EntryFrp { + &self.frp + } +} + +impl display::Object for Entry { + fn display_object(&self) -> &display::object::Instance { + &self.data.display_object + } +} + + + +// ====================== +// === SimpleGridView === +// ====================== + +/// The Simple version of Grid View, where each entry is just a label with background. +pub type SimpleGridView = crate::GridView; + +/// The Simple version of Scrollable Grid View, where each entry is just a label with background. +pub type SimpleScrollableGridView = scrollable::GridView; diff --git a/lib/rust/ensogl/component/grid-view/src/visible_area.rs b/lib/rust/ensogl/component/grid-view/src/visible_area.rs new file mode 100644 index 000000000000..a04d336074e1 --- /dev/null +++ b/lib/rust/ensogl/component/grid-view/src/visible_area.rs @@ -0,0 +1,134 @@ +//! Functions for evaluating which part of [`GridView`] is visible. + +use crate::prelude::*; + +use crate::Col; +use crate::Row; +use crate::Viewport; + + + +// ========================================== +// === Ranges of Rows and Columns Visible === +// ========================================== + +fn has_size(v: &Viewport) -> bool { + v.right > v.left + f32::EPSILON && v.top > v.bottom + f32::EPSILON +} + + +/// Return range of visible rows. +pub fn visible_rows(v: &Viewport, entry_size: Vector2, row_count: usize) -> Range { + let first_visible_unrestricted = (v.top / -entry_size.y).floor() as isize; + let first_visible = first_visible_unrestricted.clamp(0, row_count as isize) as Row; + let first_not_visible = if has_size(v) { + let first_not_visible_unrestricted = (v.bottom / -entry_size.y).ceil() as isize; + first_not_visible_unrestricted.clamp(0, row_count as isize) as Row + } else { + first_visible + }; + first_visible..first_not_visible +} + +/// Return range of visible columns. +pub fn visible_columns(v: &Viewport, entry_size: Vector2, col_count: usize) -> Range { + let first_visible_unrestricted = (v.left / entry_size.x).floor() as isize; + let first_visible = first_visible_unrestricted.clamp(0, col_count as isize) as Col; + let first_not_visible = if has_size(v) { + let first_not_visible_unrestricted = (v.right / entry_size.x).ceil() as isize; + first_not_visible_unrestricted.clamp(0, col_count as isize) as Col + } else { + first_visible + }; + first_visible..first_not_visible +} + + + +// ============================= +// === All Visible Locations === +// ============================= + +/// Return iterator over all visible locations (row-column pairs). +pub fn all_visible_locations( + v: &Viewport, + entry_size: Vector2, + row_count: usize, + col_count: usize, +) -> impl Iterator { + let visible_rows = visible_rows(v, entry_size, row_count); + let visible_cols = visible_columns(v, entry_size, col_count); + itertools::iproduct!(visible_rows, visible_cols) +} + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn visible_rows_and_columns() { + const ENTRY_SIZE: Vector2 = Vector2(20.0, 10.0); + const ROW_COUNT: usize = 100; + const COL_COUNT: usize = 100; + + #[derive(Clone, Debug)] + struct Case { + viewport: Viewport, + expected_rows: Range, + expected_cols: Range, + } + + impl Case { + fn new( + (left, top): (f32, f32), + (size_x, size_y): (f32, f32), + expected_rows: Range, + expected_cols: Range, + ) -> Self { + let right = left + size_x; + let bottom = top - size_y; + let viewport = Viewport { left, top, right, bottom }; + Self { viewport, expected_rows, expected_cols } + } + + fn run(&self) { + assert_eq!( + visible_rows(&self.viewport, ENTRY_SIZE, ROW_COUNT), + self.expected_rows, + "Wrong visible rows in {self:?}" + ); + assert_eq!( + visible_columns(&self.viewport, ENTRY_SIZE, COL_COUNT), + self.expected_cols, + "Wrong visible cols in {self:?}" + ); + } + } + + for case in [ + Case::new((0.0, 0.0), (40.0, 40.0), 0..4, 0..2), + Case::new((1.0, -1.0), (40.0, 40.0), 0..5, 0..3), + Case::new((-5.0, 5.0), (30.0, 30.0), 0..3, 0..2), + Case::new((-20.0, 10.0), (30.0, 30.0), 0..2, 0..1), + Case::new((-30.0, 30.0), (30.0, 30.0), 0..0, 0..0), + Case::new((19.9, -9.9), (40.0, 40.0), 0..5, 0..3), + Case::new((20.0, -10.0), (40.0, 40.0), 1..5, 1..3), + Case::new((20.1, -10.1), (40.0, 40.0), 1..6, 1..4), + Case::new((1960.0, -980.0), (40.0, 20.0), 98..100, 98..100), + Case::new((1979.0, -989.0), (40.0, 20.0), 98..100, 98..100), + Case::new((1980.0, -990.0), (40.0, 20.0), 99..100, 99..100), + Case::new((1981.0, -991.0), (40.0, 20.0), 99..100, 99..100), + Case::new((1999.0, -999.0), (40.0, 20.0), 99..100, 99..100), + Case::new((2000.0, -1000.0), (40.0, 20.0), 100..100, 100..100), + Case::new((2001.0, -1001.0), (40.0, 20.0), 100..100, 100..100), + ] { + case.run() + } + } +} diff --git a/lib/rust/ensogl/component/scroll-area/src/lib.rs b/lib/rust/ensogl/component/scroll-area/src/lib.rs index efde0f5557a8..2399475fd7e2 100644 --- a/lib/rust/ensogl/component/scroll-area/src/lib.rs +++ b/lib/rust/ensogl/component/scroll-area/src/lib.rs @@ -102,6 +102,11 @@ impl Viewport { let right = pos.x + size.x; !(top < self.bottom || bottom > self.top || left > self.right || right < self.left) } + + /// Return Viewport's size. + pub fn size(&self) -> Vector2 { + Vector2(self.right - self.left, self.top - self.bottom) + } } @@ -194,7 +199,7 @@ impl ScrollArea { pub fn new(app: &Application) -> ScrollArea { let scene = &app.display.default_scene; let logger = Logger::new("ScrollArea"); - let camera = scene.layers.main.camera(); + let camera = scene.layers.node_searcher.camera(); let display_object = display::object::Instance::new(&logger); let masked_layer = layer::Masked::new(&logger, &camera); let display_object = display::object::InstanceWithLayer::new(display_object, masked_layer); @@ -291,8 +296,8 @@ impl ScrollArea { viewport <- viewport.map(|(position,dimension)|{ Viewport{ top: -position.y, - left: position.x, - right: position.x + dimension.x, + left: -position.x, + right: - position.x + dimension.x, bottom: -position.y - dimension.y, } }); diff --git a/lib/rust/ensogl/core/src/application/frp.rs b/lib/rust/ensogl/core/src/application/frp.rs index 2dbb7a5b0eef..27c6e47b3566 100644 --- a/lib/rust/ensogl/core/src/application/frp.rs +++ b/lib/rust/ensogl/core/src/application/frp.rs @@ -840,7 +840,7 @@ macro_rules! define_endpoints_2 { )? ) => { $crate::define_endpoints_2_normalized! {{ - [<$($($param $(:$($constraints)*)?),*)?>] [<$($($param),*)?>] + [<$($($param $(:$($constraints)*)?),*)?>] [<$($($param),*)?>] [<($($($param),*)?)>] Input { [$($($global_opts)*)? $($($($input_opts)*)?)?] /// Focus the element. Focused elements are meant to receive shortcut events. @@ -938,7 +938,7 @@ macro_rules! define_endpoints_2 { #[macro_export] macro_rules! generate_rc_structs_and_impls { ( - [$($ctx:tt)*] [$($param:tt)*] + [$($ctx:tt)*] [$($param:tt)*] [$($phantom:tt)*] pub struct $name:tt $data_name:tt { $( $(#$field_attr:tt)* @@ -970,7 +970,7 @@ macro_rules! generate_rc_structs_and_impls { #[allow(unused_parens)] #[derive(Debug)] pub struct $data_name $($ctx)* { - _phantom_type_args: PhantomData0 $($param)*, + _phantom_type_args: PhantomData0 $($phantom)*, $( $(#$field_attr)* pub $field : $field_type @@ -1006,7 +1006,7 @@ macro_rules! generate_rc_structs_and_impls { #[macro_export] macro_rules! define_endpoints_2_normalized_public { ({ - [$($ctx:tt)*] [$($param:tt)*] + [$($ctx:tt)*] [$($param:tt)*] [$($phantom:tt)*] Input { $input_opts:tt $( @@ -1067,7 +1067,7 @@ macro_rules! define_endpoints_2_normalized_public { // === Input === $crate::generate_rc_structs_and_impls! { - [$($ctx)*] [$($param)*] + [$($ctx)*] [$($param)*] [$($phantom)*] pub struct Input InputData { $( $(#$in_field_attr)* @@ -1092,7 +1092,7 @@ macro_rules! define_endpoints_2_normalized_public { // === Output === $crate::generate_rc_structs_and_impls! { - [$($ctx)*] [$($param)*] + [$($ctx)*] [$($param)*] [$($phantom)*] pub struct Output OutputData { status_map: (Rc>>>), command_map: (Rc>>) @@ -1135,7 +1135,7 @@ macro_rules! define_endpoints_2_normalized_public { // === Combined === $crate::generate_rc_structs_and_impls! { - [$($ctx)*] [$($param)*] + [$($ctx)*] [$($param)*] [$($phantom)*] pub struct Combined CombinedData { $( $(#$in_field_attr)* @@ -1168,7 +1168,7 @@ macro_rules! define_endpoints_2_normalized_public { #[macro_export] macro_rules! define_endpoints_2_normalized_private { ({ - [$($ctx:tt)*] [$($param:tt)*] + [$($ctx:tt)*] [$($param:tt)*] [$($phantom:tt)*] Input { $input_opts:tt $( @@ -1209,7 +1209,7 @@ macro_rules! define_endpoints_2_normalized_private { // === Input === $crate::generate_rc_structs_and_impls! { - [$($ctx)*] [$($param)*] + [$($ctx)*] [$($param)*] [$($phantom)*] pub struct Input InputData { $( $(#$in_field_attr)* @@ -1233,7 +1233,7 @@ macro_rules! define_endpoints_2_normalized_private { // === Output === $crate::generate_rc_structs_and_impls! { - [$($ctx)*] [$($param)*] + [$($ctx)*] [$($param)*] [$($phantom)*] pub struct Output OutputData { $( $(#$out_field_attr)* @@ -1256,7 +1256,7 @@ macro_rules! define_endpoints_2_normalized_private { #[macro_export] macro_rules! define_endpoints_2_normalized_glue { ({ - [$($ctx:tt)*] [$($param:tt)*] + [$($ctx:tt)*] [$($param:tt)*] [$($phantom:tt)*] Input { $input_opts:tt $( @@ -1456,7 +1456,7 @@ mod tests { #[test] fn test_generate_rc_structs_and_impls() { generate_rc_structs_and_impls! { - [] [] + [] [] [] pub struct Output OutputData { foo: f32, } diff --git a/lib/rust/ensogl/example/Cargo.toml b/lib/rust/ensogl/example/Cargo.toml index 866f0be8b80b..4eb216c06ba7 100644 --- a/lib/rust/ensogl/example/Cargo.toml +++ b/lib/rust/ensogl/example/Cargo.toml @@ -16,6 +16,7 @@ ensogl-example-drop-manager = { path = "drop-manager" } ensogl-example-easing-animator = { path = "easing-animator" } ensogl-example-glyph-system = { path = "glyph-system" } ensogl-example-list-view = { path = "list-view" } +ensogl-example-grid-view = { path = "grid-view" } ensogl-example-mouse-events = { path = "mouse-events" } ensogl-example-profiling-run-graph = { path = "profiling-run-graph" } ensogl-example-render-profile-flamegraph = { path = "render-profile-flamegraph" } diff --git a/lib/rust/ensogl/example/grid-view/Cargo.toml b/lib/rust/ensogl/example/grid-view/Cargo.toml new file mode 100644 index 000000000000..ddccff1f1c5f --- /dev/null +++ b/lib/rust/ensogl/example/grid-view/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ensogl-example-grid-view" +version = "0.1.0" +authors = ["Enso Team "] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +enso-frp = { path = "../../../frp" } +ensogl-core = { path = "../../core" } +ensogl-hardcoded-theme = { path = "../../app/theme/hardcoded" } +ensogl-grid-view = { path = "../../component/grid-view" } +ensogl-text-msdf-sys = { path = "../../component/text/msdf-sys" } +enso-text = { path = "../../../text" } +wasm-bindgen = { version = "0.2.78", features = ["nightly"] } diff --git a/lib/rust/ensogl/example/grid-view/src/lib.rs b/lib/rust/ensogl/example/grid-view/src/lib.rs new file mode 100644 index 000000000000..4328d55de57b --- /dev/null +++ b/lib/rust/ensogl/example/grid-view/src/lib.rs @@ -0,0 +1,92 @@ +//! A debug scene which shows the Scrollable Grid View component. + +#![recursion_limit = "1024"] +// === Features === +#![feature(associated_type_defaults)] +#![feature(drain_filter)] +#![feature(fn_traits)] +#![feature(trait_alias)] +#![feature(type_alias_impl_trait)] +#![feature(unboxed_closures)] +// === Standard Linter Configuration === +#![deny(non_ascii_idents)] +#![warn(unsafe_code)] +// === Non-Standard Linter Configuration === +#![warn(missing_copy_implementations)] +#![warn(missing_debug_implementations)] +#![warn(missing_docs)] +#![warn(trivial_casts)] +#![warn(trivial_numeric_casts)] +#![warn(unused_import_braces)] +#![warn(unused_qualifications)] + +use ensogl_core::prelude::*; +use wasm_bindgen::prelude::*; + +use enso_frp as frp; +use ensogl_core::application::Application; +use ensogl_core::data::color; +use ensogl_core::display::navigation::navigator::Navigator; +use ensogl_core::display::object::ObjectOps; +use ensogl_grid_view as grid_view; +use ensogl_hardcoded_theme as theme; +use ensogl_text_msdf_sys::run_once_initialized; + + + +// =================== +// === Entry Point === +// =================== + +/// An entry point. +#[entry_point] +#[allow(dead_code)] +pub fn main() { + run_once_initialized(|| { + init_tracing(TRACE); + let app = Application::new("root"); + init(&app); + mem::forget(app); + }); +} + + + +// ======================== +// === Init Application === +// ======================== + +fn init(app: &Application) { + theme::builtin::dark::register(&app); + theme::builtin::light::register(&app); + theme::builtin::light::enable(&app); + + let grid_view = grid_view::simple::SimpleScrollableGridView::new(app); + grid_view.scroll_frp().resize(Vector2(400.0, 300.0)); + app.display.default_scene.layers.node_searcher.add_exclusive(&grid_view); + frp::new_network! { network + requested_entry <- grid_view.model_for_entry_needed.map(|(row, col)| { + (*row, *col, ImString::from(format!("Entry ({row}, {col})"))) + }); + grid_view.model_for_entry <+ requested_entry; + } + grid_view.set_entries_size(Vector2(130.0, 28.0)); + let params = grid_view::simple::EntryParams { + bg_color: color::Rgba(0.8, 0.8, 0.9, 1.0), + bg_margin: 1.0, + ..default() + }; + grid_view.set_entries_params(params); + grid_view.reset_entries(1000, 1000); + + app.display.add_child(&grid_view); + let navigator = Navigator::new( + &app.display.default_scene, + &app.display.default_scene.layers.node_searcher.camera(), + ); + navigator.disable_wheel_panning(); + + std::mem::forget(grid_view); + std::mem::forget(network); + std::mem::forget(navigator); +} diff --git a/lib/rust/ensogl/example/src/lib.rs b/lib/rust/ensogl/example/src/lib.rs index d463294fc3ed..bebfaeaadeb0 100644 --- a/lib/rust/ensogl/example/src/lib.rs +++ b/lib/rust/ensogl/example/src/lib.rs @@ -34,6 +34,7 @@ pub use ensogl_example_dom_symbols as dom_symbols; pub use ensogl_example_drop_manager as drop_manager; pub use ensogl_example_easing_animator as easing_animator; pub use ensogl_example_glyph_system as glyph_system; +pub use ensogl_example_grid_view as grid_view; pub use ensogl_example_list_view as list_view; pub use ensogl_example_mouse_events as mouse_events; pub use ensogl_example_profiling_run_graph as profiling_run_graph; diff --git a/lib/rust/frp/src/network.rs b/lib/rust/frp/src/network.rs index b1e588167cc2..d12a33e3c3ef 100644 --- a/lib/rust/frp/src/network.rs +++ b/lib/rust/frp/src/network.rs @@ -144,6 +144,16 @@ impl WeakNetwork { self.data.upgrade().map(|data| Network { data }) } + /// Upgrade to string reference, printing warning if returning `None`. To be used in places + /// where we assume the Network should still exist. + pub fn upgrade_or_warn(&self) -> Option { + let result = self.upgrade(); + if result.is_none() { + tracing::warn!("The Network is dropped in a place where we don't expect."); + } + result + } + /// ID getter of this network. pub fn id(&self) -> NetworkId { NetworkId(self.data.as_ptr() as *const () as usize) diff --git a/lib/rust/frp/src/nodes.rs b/lib/rust/frp/src/nodes.rs index 1611d7f000e2..2597689eef25 100644 --- a/lib/rust/frp/src/nodes.rs +++ b/lib/rust/frp/src/nodes.rs @@ -1634,8 +1634,8 @@ impl OwnedTrace { impl stream::EventConsumer> for OwnedTrace { fn on_event(&self, stack: CallStack, event: &Output) { - tracing::log::debug!("[FRP] {}: {:?}", self.label(), event); - tracing::log::debug!("[FRP] {}", stack); + tracing::debug!("[FRP] {}: {:?}", self.label(), event); + tracing::debug!("[FRP] {}", stack); self.emit_event(stack, event); } } diff --git a/lib/rust/types/src/algebra.rs b/lib/rust/types/src/algebra.rs index cc9a7d26dcff..ed239e43581b 100644 --- a/lib/rust/types/src/algebra.rs +++ b/lib/rust/types/src/algebra.rs @@ -48,7 +48,7 @@ mod vectors { pub type Rotation2 = nalgebra::Rotation2; pub type Rotation3 = nalgebra::Rotation3; - pub fn Vector2(t1: T, t2: T) -> Vector2 { + pub const fn Vector2(t1: T, t2: T) -> Vector2 { Vector2::new(t1, t2) } pub fn Vector3(t1: T, t2: T, t3: T) -> Vector3 {