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 | |