Grid View Headers (#3656)

This commit is contained in:
Adam Obuchowicz 2022-08-24 16:36:23 +02:00 committed by GitHub
parent d87a32d019
commit d4142cfee3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1961 additions and 531 deletions

View File

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

View File

@ -4,6 +4,8 @@
use enso_build::prelude::*;
fn main() -> Result {
let build_config_yaml = include_str!("../../build-config.yaml");
let config = enso_build::config::load_yaml(build_config_yaml)?;

View File

@ -15,6 +15,13 @@ use ensogl_core::display::scene::Layer;
use ensogl_core::display::Attribute;
// ==============
// === Export ===
// ==============
pub mod visible;
// ===============
// === Contour ===
@ -52,10 +59,11 @@ ensogl_core::define_endpoints_2! { <Model: (frp::node::Data), Params: (frp::node
Output {
/// Disabled entries does not react for mouse events, and cannot be selected.
disabled(bool),
/// Entry's contour. Defines what part of the entry will react for mouse events, and also
/// defines the shape of the selection/hover highlight (in case of
/// [selectable](crate::selectable) grid views.).
/// Entry's contour. Defines what part of the entry will react for mouse events.
contour(Contour),
/// In [selectable](crate::selectable) grid views, this defines the shape of the
/// selection/hover highlight in case when this entry is selected.
highlight_contour(Contour),
/// Override column's width. If multiple entries from the same column emit this event,
/// only the last one is applied. See [`crate::GridView`] documentation for more details.
override_column_width(f32),

View File

@ -0,0 +1,164 @@
//! A module with content related to entries visible in GridView.
use crate::prelude::*;
use crate::entry;
use crate::Col;
use crate::ColumnWidths;
use crate::Entry;
use crate::Row;
use ensogl_core::application::Application;
use ensogl_core::display;
use ensogl_core::display::scene::Layer;
// =================
// === Constants ===
// =================
/// The distance the mouse should be moved in last couple of frames to highlight entry as hovered.
///
/// We avoid hovering the entry when mouse does not move for better UX (e.g. during scrolling with
/// mouse wheel).
const MOUSE_MOVEMENT_NEEDED_TO_HOVER_PX: f32 = 1.5;
// ====================
// === VisibleEntry ===
// ====================
/// An `Entry` instance visible inside Grid View.
#[derive(Clone, CloneRef, Debug)]
#[clone_ref(bound = "Entry: CloneRef")]
pub struct VisibleEntry<Entry> {
/// The entry instance.
pub entry: Entry,
/// The overlay shape, catching the mouse events for the entry.
pub overlay: entry::overlay::View,
}
impl<E: display::Object> display::Object for VisibleEntry<E> {
fn display_object(&self) -> &display::object::Instance {
self.entry.display_object()
}
}
// ===================
// === CreationCtx ===
// ===================
/// A structure gathering all data required for creating new entry instance.
#[allow(missing_docs)]
#[derive(CloneRef, Debug, Derivative)]
#[derivative(Clone(bound = ""))]
pub struct CreationCtx<EntryParams> {
pub app: Application,
pub network: frp::WeakNetwork,
pub set_entry_size: frp::Stream<Vector2>,
pub set_entry_params: frp::Stream<EntryParams>,
pub entry_contour: frp::Any<(Row, Col, entry::Contour)>,
pub entry_hovered: frp::Any<Option<(Row, Col)>>,
pub entry_selected: frp::Any<Option<(Row, Col)>>,
pub entry_accepted: frp::Any<(Row, Col)>,
pub override_column_width: frp::Any<(Col, f32)>,
}
impl<EntryParams> CreationCtx<EntryParams>
where EntryParams: frp::node::Data
{
/// Create new entry instance.
///
/// The new instance will have all its FRP endpoints connected to appropriate endpoints of
/// `self` and overlay mouse events.
pub fn create_entry<E: Entry<Params = EntryParams>>(
&self,
text_layer: Option<&Layer>,
) -> VisibleEntry<E> {
let entry = E::new(&self.app, text_layer);
let overlay = entry::overlay::View::new(Logger::new("EntryOverlay"));
entry.add_child(&overlay);
if let Some(network) = self.network.upgrade_or_warn() {
let entry_frp = entry.frp();
let entry_network = entry_frp.network();
let mouse = &self.app.display.default_scene.mouse.frp;
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();
contour <- all(init, entry_frp.contour)._1();
eval contour ((c) overlay.set_contour(*c));
let events = &overlay.events;
let disabled = &entry_frp.disabled;
let location = entry_frp.set_location.clone_ref();
self.entry_contour <+ all_with(&location, &contour, |&(r, c), &cont| (r, c, cont));
column <- location._1();
// We make a distinction between "hovered" state and "mouse_in" state, because
// we want to highlight entry as hovered only when mouse moves a bit.
hovering <- any(...);
hovering <+ events.mouse_out.constant(false);
mouse_in <- bool(&events.mouse_out, &events.mouse_over);
// We can receive `mouse_over` event a couple of frames after actual hovering.
// Therefore, we count our "mouse move" from a couple of frames before.
mouse_pos_some_time_ago <- mouse.prev_position.previous().previous().previous();
mouse_over_movement_start <- mouse_pos_some_time_ago.sample(&events.mouse_over);
mouse_over_with_start_pos <- all(mouse.position, mouse_over_movement_start).gate(&mouse_in);
mouse_move_which_hovers <- mouse_over_with_start_pos.filter(
|(pos, start_pos)| (pos - start_pos).norm() > MOUSE_MOVEMENT_NEEDED_TO_HOVER_PX
);
hovered <- mouse_move_which_hovers.gate_not(&hovering).gate_not(disabled);
hovering <+ hovered.constant(true);
selected <- events.mouse_down.gate_not(disabled);
accepted <- events.mouse_down_primary.gate_not(disabled);
self.entry_hovered <+ location.sample(&hovered).map(|l| Some(*l));
self.entry_selected <+ location.sample(&selected).map(|l| Some(*l));
self.entry_accepted <+ location.sample(&accepted);
self.override_column_width <+ entry_frp.override_column_width.map2(
&column,
|width, col| (*col, *width)
);
}
init.emit(());
}
VisibleEntry { entry, overlay }
}
}
// ================
// === Position ===
// ================
/// Get base X position of entry at given column.
pub fn position_x(col: Col, entry_size: Vector2, column_widths: &ColumnWidths) -> f32 {
let x_offset = column_widths.pos_offset(col) + column_widths.width_diff(col) / 2.0;
(col as f32 + 0.5) * entry_size.x + x_offset
}
/// Get base Y position of entry at given row.
pub fn position_y(row: Row, entry_size: Vector2) -> f32 {
(row as f32 + 0.5) * -entry_size.y
}
/// Get base position of entry at given row and column.
pub fn position(row: Row, col: Col, entry_size: Vector2, column_widths: &ColumnWidths) -> Vector2 {
Vector2(position_x(col, entry_size, column_widths), position_y(row, entry_size))
}
/// Set the proper position of entry at given row and column.
pub fn set_position<E: display::Object>(
entry: &E,
row: Row,
col: Col,
entry_size: Vector2,
column_widths: &ColumnWidths,
) {
entry.set_position_xy(position(row, col, entry_size, column_widths));
}

View File

@ -0,0 +1,710 @@
//! The module containing [`GridView`] with headers.
use crate::prelude::*;
use crate::entry;
use crate::entry::visible::VisibleEntry;
use crate::visible_area;
use crate::Col;
use crate::ColumnWidths;
use crate::Entry;
use crate::Properties;
use crate::Row;
use ensogl_core::application::Application;
use ensogl_core::display;
use ensogl_core::display::scene::layer::WeakLayer;
use ensogl_core::display::scene::Layer;
use ensogl_scroll_area::Viewport;
// ===========
// === FRP ===
// ===========
/// A structure with layers where the headers are displayed. Designed to be used in FRP networks.
///
/// To avoid unpredictable layer lifetime, all the references are weak.
#[allow(missing_docs)]
#[derive(Clone, Debug, Default)]
pub struct WeakLayers {
pub header: Option<WeakLayer>,
pub text: Option<WeakLayer>,
}
impl WeakLayers {
/// Constructor.
pub fn new(header: &Layer, text: Option<&Layer>) -> Self {
Self { header: Some(header.downgrade()), text: text.map(|l| l.downgrade()) }
}
/// Upgrade the main layer for headers.
pub fn upgrade_header(&self) -> Option<Layer> {
self.header.as_ref()?.upgrade()
}
/// Upgrade the text layer for headers.
pub fn upgrade_text(&self) -> Option<Layer> {
self.text.as_ref()?.upgrade()
}
}
ensogl_core::define_endpoints_2! { <HeaderModel: (frp::node::Data)>
Input {
set_layers(WeakLayers),
/// The information about section, should be provided when the [`section_info_needed`]
/// output is emitted.
///
/// The first row in the range is considered a header of the section. The model is an
/// [`Entry::Model`] for the header instance (displayed in case when the section is scrolled
/// down).
section_info(Range<Row>, Col, HeaderModel),
reset_sections(),
}
Output {
/// Emitted when the information of the section where given location belongs is needed.
///
/// As a response, the `section_info` input should be called for given column, until then
/// the header won't be displayed.
section_info_needed(Row, Col),
header_shown(Row, Col),
header_position_changed(Row, Col, Vector2),
header_hidden(Row, Col),
}
}
// =============
// === Model ===
// =============
/// A pushed-down header visible on the top of the viewport.
///
/// We push down headers of scrolled-down sections to have them always visible. See the
/// [main component documentation](GridView).
#[derive(Clone, Debug)]
pub struct VisibleHeader<HeaderEntry> {
section_rows: Range<Row>,
entry: VisibleEntry<HeaderEntry>,
}
impl<HeaderEntry: Entry> VisibleHeader<HeaderEntry> {
fn header_position(
&self,
col: Col,
entry_size: Vector2,
viewport: Viewport,
column_widths: &ColumnWidths,
) -> Vector2 {
let contour = self.entry.entry.frp().contour.value();
let max_y = entry::visible::position_y(self.section_rows.start, entry_size);
let next_section_y = entry::visible::position_y(self.section_rows.end, entry_size);
let min_y = next_section_y + entry_size.y / 2.0 + contour.size.y / 2.0;
let y = (viewport.top - contour.size.y / 2.0).min(max_y).max(min_y);
Vector2(entry::visible::position_x(col, entry_size, column_widths), y)
}
}
/// A structure containing data of [`GridView`] with headers.
#[derive(Clone, Debug)]
pub struct Model<InnerGrid, HeaderEntry, HeaderParams> {
grid: InnerGrid,
/// The cloned-ref instance of ColumnWidths structure from `grid`.
column_widths: ColumnWidths,
visible_headers: RefCell<HashMap<Col, VisibleHeader<HeaderEntry>>>,
free_headers: RefCell<Vec<VisibleEntry<HeaderEntry>>>,
entry_creation_ctx: entry::visible::CreationCtx<HeaderParams>,
}
impl<InnerGrid, HeaderEntry, HeaderParams> Model<InnerGrid, HeaderEntry, HeaderParams> {
fn new<E: Entry>(
grid: InnerGrid,
entry_creation_ctx: entry::visible::CreationCtx<HeaderParams>,
) -> Self
where
InnerGrid: AsRef<crate::GridView<E>>,
{
let visible_headers = default();
let free_headers = default();
let column_widths = grid.as_ref().model().column_widths.clone_ref();
Self { grid, column_widths, visible_headers, free_headers, entry_creation_ctx }
}
}
impl<InnerGrid, HeaderEntry: display::Object, HeaderParams>
Model<InnerGrid, HeaderEntry, HeaderParams>
{
fn hide_no_longer_visible_headers(&self, properties: Properties) -> Vec<(Row, Col)> {
let Properties { row_count, col_count, viewport, entries_size } = properties;
let widths = &self.column_widths;
let mut visible_headers = self.visible_headers.borrow_mut();
let mut free_headers = self.free_headers.borrow_mut();
let cols_range = visible_area::visible_columns(viewport, entries_size, col_count, widths);
let highest_visible_row =
visible_area::visible_rows(viewport, entries_size, row_count).start;
let freed = visible_headers.drain_filter(|col, header| {
!cols_range.contains(col) || !header.section_rows.contains(&highest_visible_row)
});
let detached = freed.map(|(col, header)| {
header.entry.entry.unset_parent();
((header.section_rows.start, col), header.entry)
});
let (locations, entries): (Vec<_>, Vec<_>) = detached.unzip();
free_headers.extend(entries);
locations
}
fn needed_info_for_uncovered_sections(&self, properties: Properties) -> Vec<(Row, Col)> {
let Properties { row_count, col_count, viewport, entries_size } = properties;
let widths = &self.column_widths;
let visible_headers = self.visible_headers.borrow();
let cols_range = visible_area::visible_columns(viewport, entries_size, col_count, widths);
let highest_visible_row =
visible_area::visible_rows(viewport, entries_size, row_count).start;
let uncovered = cols_range.filter_map(|col| {
(!visible_headers.contains_key(&col)).as_some((highest_visible_row, col))
});
uncovered.collect()
}
fn reset_entries(&self, properties: Properties) -> Vec<(Row, Col)> {
let Properties { row_count, col_count, viewport, entries_size } = properties;
let widths = &self.column_widths;
let mut visible_headers = self.visible_headers.borrow_mut();
let mut free_headers = self.free_headers.borrow_mut();
let highest_visible_row =
visible_area::visible_rows(viewport, entries_size, row_count).start;
let detached = visible_headers.drain().map(|(_, header)| {
header.entry.entry.unset_parent();
header.entry
});
free_headers.extend(detached);
let visible_columns =
visible_area::visible_columns(viewport, entries_size, col_count, widths);
visible_columns.map(|col| (highest_visible_row, col)).collect()
}
fn drop_all_entries(&self, properties: Properties) -> Vec<(Row, Col)> {
let to_section_info_request = self.reset_entries(properties);
self.free_headers.borrow_mut().clear();
to_section_info_request
}
}
impl<InnerGrid, HeaderEntry: Entry> Model<InnerGrid, HeaderEntry, HeaderEntry::Params> {
fn header_position(
&self,
row: Row,
col: Col,
entry_size: Vector2,
viewport: Viewport,
) -> Option<Vector2> {
let visible_headers = self.visible_headers.borrow();
let widths = &self.column_widths;
let header = visible_headers.get(&col).filter(|h| h.section_rows.start == row);
header.map(|h| h.header_position(col, entry_size, viewport, widths))
}
/// The y position of line between the header displayed on the top of the viewport and the rest
/// of entries. If no header is displayed in given column, the top of the viewport is returned.
fn header_separator(&self, col: Col, entry_size: Vector2, viewport: Viewport) -> f32 {
let visible_headers = self.visible_headers.borrow();
let widths = &self.column_widths;
let header = visible_headers.get(&col);
let separator_if_header_visible = header.map(|h| {
h.header_position(col, entry_size, viewport, widths).y
- h.entry.entry.frp().contour.value().size.y / 2.0
});
separator_if_header_visible.unwrap_or(viewport.top)
}
fn update_headers_positions(&self, properties: Properties) -> Vec<(Row, Col, Vector2)> {
let Properties { viewport, entries_size, .. } = properties;
let visible_headers = self.visible_headers.borrow();
let widths = &self.column_widths;
let updated_positions = visible_headers.iter().filter_map(|(col, header)| {
let new_position = header.header_position(*col, entries_size, viewport, widths);
(header.entry.position().xy() != new_position).as_some_from(|| {
header.entry.set_position_xy(new_position);
(header.section_rows.start, *col, new_position)
})
});
updated_positions.collect()
}
fn update_header_size(&self, col: Col, properties: Properties) {
let entries_size = properties.entries_size;
let width_diff = self.column_widths.width_diff(col);
let header = self.visible_headers.borrow().get(&col).map(|h| h.entry.clone_ref());
if let Some(header) = header {
header.entry.frp().set_size(entries_size + Vector2(width_diff, 0.0))
}
}
fn update_section(
&self,
rows: Range<Row>,
col: Col,
model: HeaderEntry::Model,
entry_size: Vector2,
viewport: Viewport,
layers: &WeakLayers,
) -> (Row, Col, Vector2)
where
InnerGrid: display::Object,
{
use std::collections::hash_map::Entry::*;
let mut visible_headers = self.visible_headers.borrow_mut();
let mut free_headers = self.free_headers.borrow_mut();
let widths = &self.column_widths;
let create_new_entry = || {
let text_layer = layers.upgrade_text();
let entry = self.entry_creation_ctx.create_entry(text_layer.as_ref());
if let Some(layer) = layers.upgrade_header() {
layer.add_exclusive(&entry);
}
entry
};
let entry = match visible_headers.entry(col) {
Occupied(mut entry) => {
entry.get_mut().section_rows = rows;
entry.into_mut()
}
Vacant(lack_of_entry) => {
let new_entry = free_headers.pop().unwrap_or_else(create_new_entry);
self.grid.add_child(&new_entry);
let new_header_entry =
VisibleHeader { section_rows: rows, entry: new_entry };
lack_of_entry.insert(new_header_entry)
}
};
let entry_frp = entry.entry.entry.frp();
entry_frp.set_model(model);
entry_frp.set_location((entry.section_rows.start, col));
let width_offset = self.column_widths.width_diff(col);
entry_frp.set_size(entry_size + Vector2(width_offset, 0.0));
let position = entry.header_position(col, entry_size, viewport, widths);
entry.entry.set_position_xy(position);
(entry.section_rows.start, col, position)
}
}
// ================
// === GridView ===
// ================
/// A template for [`GridView`] structure, wrapping any version of Grid View, and where entry
/// parameters and model are separate generic arguments.
#[derive(CloneRef, Debug, Derivative)]
#[derivative(Clone(bound = ""))]
pub struct GridViewTemplate<
Entry,
InnerGridView,
HeaderEntry,
HeaderModel: frp::node::Data,
HeaderParams,
> {
frp: Frp<HeaderModel>,
model: Rc<Model<InnerGridView, HeaderEntry, HeaderParams>>,
entry_type: PhantomData<Entry>,
}
impl<Entry, InnerGridView, HeaderEntry, HeaderModel: frp::node::Data, HeaderParams> Deref
for GridViewTemplate<Entry, InnerGridView, HeaderEntry, HeaderModel, HeaderParams>
{
type Target = InnerGridView;
fn deref(&self) -> &InnerGridView {
&self.model.grid
}
}
/// Grid View with Headers.
///
/// This is an extended version of [the base Grid View](crate::GridView). Here each column is
/// assumed to be arranged in sections. The first entry of each section is treated as header. When
/// the section is scrolled down, the header remains visible.
///
/// The structure derefs to the [base GridView FRP API](crate::Frp). To access the API specific
/// to headers use [`Self::header_frp`] method.
///
/// # Headers
///
/// Those headers which are pushed down can (and should) be instantiated in a separate layer, passed
/// with `set_layers` input. User should ensure, that the layer is displayed over the normal
/// entries. Those headers can be the same type as main entries, but does not have to. They should
/// have the same [`Entry::Params`] type, however.
///
/// ## Requesting for Models
///
/// Only the pushed-down, currently visible headers are instantiated. The models for the header
/// instances and information about their section should be provided on demand, as a reaction
/// for [`Frp::section_info_needed`] event, by emitting [`Frp::section_info`] with proper data.
///
/// **Important**. The [`Frp::section_info_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 [`crate::Frp::set_entries_size`] or
/// [`crate::Frp::set_viewport`].
pub type GridView<Entry, HeaderEntry> = GridViewTemplate<
Entry,
crate::GridView<Entry>,
HeaderEntry,
<HeaderEntry as crate::Entry>::Model,
<HeaderEntry as crate::Entry>::Params,
>;
impl<E: Entry, HeaderEntry: Entry<Params = E::Params>> GridView<E, HeaderEntry> {
/// Create new Grid View with headers.
pub fn new(app: &Application) -> Self {
let grid = crate::GridView::<E>::new(app);
Self::new_wrapping(grid)
}
}
impl<E, InnerGridView, HeaderEntry>
GridViewTemplate<E, InnerGridView, HeaderEntry, HeaderEntry::Model, HeaderEntry::Params>
where
E: Entry<Params = HeaderEntry::Params>,
InnerGridView: AsRef<crate::GridView<E>> + display::Object + 'static,
HeaderEntry: Entry,
{
/// Add the "headers" feature to an arbitrary `InnerGridView` and returns as a new component.
pub fn new_wrapping(grid: InnerGridView) -> Self {
let frp = Frp::new();
let entry_creation_ctx = grid.as_ref().model().entry_creation_ctx.clone_ref();
let model = Rc::new(Model::new(grid, entry_creation_ctx));
let grid_frp = model.grid.as_ref().frp();
let network = frp.network();
let input = &frp.private.input;
let out = &frp.private.output;
frp::extend! { network
viewport_changed <- grid_frp.viewport.constant(());
entries_size_changed <- grid_frp.entries_size.constant(());
column_resized <- grid_frp.column_resized.constant(());
headers_may_be_hidden <- any(viewport_changed, entries_size_changed, column_resized);
header_hidden <= headers_may_be_hidden.map2(
&grid_frp.properties,
f!(((), props) model.hide_no_longer_visible_headers(*props))
);
out.header_hidden <+ header_hidden;
request_sections_after_viewport_change <= grid_frp.viewport.map2(
&grid_frp.properties,
f!((_, props) model.needed_info_for_uncovered_sections(*props))
);
request_sections_after_entry_size_change <= grid_frp.entries_size.map2(
&grid_frp.properties,
f!((_, props) model.needed_info_for_uncovered_sections(*props))
);
request_sections_after_reset <= grid_frp.reset_entries.map2(
&grid_frp.properties,
f!((_, props) model.reset_entries(*props))
);
request_sections_after_sections_reset <= frp.reset_sections.map2(
&grid_frp.properties,
f!((_, props) model.reset_entries(*props))
);
request_sections_after_layer_change <= frp.set_layers.map2(
&grid_frp.properties,
f!((_, props) model.drop_all_entries(*props))
);
request_sections_after_column_resize <= grid_frp.column_resized.map2(
&grid_frp.properties,
f!((_, props) model.needed_info_for_uncovered_sections(*props))
);
out.section_info_needed <+ request_sections_after_viewport_change;
out.section_info_needed <+ request_sections_after_entry_size_change;
out.section_info_needed <+ request_sections_after_reset;
out.section_info_needed <+ request_sections_after_sections_reset;
out.section_info_needed <+ request_sections_after_layer_change;
out.section_info_needed <+ request_sections_after_column_resize;
pos_should_be_updated <- any(viewport_changed, entries_size_changed, column_resized);
position_update <= pos_should_be_updated.map2(
&grid_frp.properties,
f!(((), props) model.update_headers_positions(*props))
);
out.header_position_changed <+ position_update;
section_update <- input.section_info.map3(
&grid_frp.properties,
&frp.set_layers,
f!(((rows, col, m): &(Range<usize>, usize, HeaderEntry::Model), props, layers)
model.update_section(rows.clone(), *col, m.clone(), props.entries_size, props.viewport, layers))
);
out.header_shown <+ section_update.map(|&(row, col, _)| (row, col));
out.header_position_changed <+ section_update;
column_resize_params <- all(&grid_frp.column_resized, &grid_frp.properties);
eval column_resize_params ((&((col, _), props)) model.update_header_size(col, props));
}
let entry_type = PhantomData;
Self { frp, model, entry_type }
}
/// Returns the current header position by its row and column index.
///
/// If the entry at given location is not a pushed-down header, the method returns `None`.
/// You may be also interested in [`header_or_entry_position`] method.
pub fn header_position(&self, row: Row, col: Col) -> Option<Vector2> {
let entry_size = self.model.grid.as_ref().entries_size.value();
let viewport = self.model.grid.as_ref().viewport.value();
self.model.header_position(row, col, entry_size, viewport)
}
/// Returns the y position where pushed-down header ends, and below which the standard entries
/// are visible.
///
/// If the column does not have pushed-down header visible, the top of the viewport is returned.
pub fn header_separator(&self, col: Col) -> f32 {
let entry_size = self.model.grid.as_ref().entries_size.value();
let viewport = self.model.grid.as_ref().viewport.value();
self.model.header_separator(col, entry_size, viewport)
}
/// If the entry at given location is a pushed-down header, return its current (pushed-down
/// position), otherwise returns the base position for that entry (same as
/// [`crate::GridView::entry_position`]).
pub fn header_or_entry_position(&self, row: Row, column: Col) -> Vector2 {
let header_pos = self.header_position(row, column);
header_pos.unwrap_or_else(|| self.model.grid.as_ref().entry_position(row, column))
}
}
impl<Entry, InnerGridView, HeaderEntry, HeaderModel, HeaderParams>
GridViewTemplate<Entry, InnerGridView, HeaderEntry, HeaderModel, HeaderParams>
where
HeaderModel: frp::node::Data,
HeaderParams: frp::node::Data,
{
/// Return the pushed-down header instance at given position, or `None` if the entry at this
/// position is not a pushed-down header.
pub fn get_header(&self, row: Row, col: Col) -> Option<HeaderEntry>
where HeaderEntry: CloneRef {
let headers = self.model.visible_headers.borrow();
let header = headers.get(&col).filter(|h| h.section_rows.start == row);
header.map(|e| e.entry.entry.clone_ref())
}
/// Return the FRP API related to headers.
pub fn header_frp(&self) -> &Frp<HeaderModel> {
&self.frp
}
}
impl<E: Entry, InnerGridView, HeaderEntry, HeaderModel, HeaderParams> AsRef<crate::GridView<E>>
for GridViewTemplate<E, InnerGridView, HeaderEntry, HeaderModel, HeaderParams>
where
InnerGridView: AsRef<crate::GridView<E>>,
HeaderModel: frp::node::Data,
HeaderParams: frp::node::Data,
{
fn as_ref(&self) -> &crate::GridView<E> {
self.model.grid.as_ref()
}
}
impl<Entry, InnerGridView, HeaderEntry, HeaderModel, HeaderParams> AsRef<Self>
for GridViewTemplate<Entry, InnerGridView, HeaderEntry, HeaderModel, HeaderParams>
where HeaderModel: frp::node::Data
{
fn as_ref(&self) -> &Self {
self
}
}
impl<Entry, InnerGridView, HeaderEntry, HeaderModel, HeaderParams> display::Object
for GridViewTemplate<Entry, InnerGridView, HeaderEntry, HeaderModel, HeaderParams>
where
InnerGridView: display::Object,
HeaderModel: frp::node::Data,
HeaderParams: frp::node::Data,
{
fn display_object(&self) -> &display::object::Instance {
self.model.grid.display_object()
}
}
// =============
// === Tests ===
// =============
#[cfg(test)]
mod tests {
use super::*;
use crate::entry::Contour;
use crate::tests::TestEntry as ParentEntry;
use crate::EntryFrp;
use ensogl_core::application::frp::API;
use ensogl_core::application::Application;
#[derive(Clone, CloneRef, Debug)]
struct TestEntry {
parent: ParentEntry,
}
impl Entry for TestEntry {
type Model = <ParentEntry as Entry>::Model;
type Params = <ParentEntry as Entry>::Params;
fn new(app: &Application, text_layer: Option<&Layer>) -> Self {
let parent = <ParentEntry as Entry>::new(app, text_layer);
let frp = &parent.frp;
frp.private()
.output
.contour
.emit(Contour { size: Vector2(9.0, 9.0), corners_radius: 0.0 });
TestEntry { parent }
}
fn frp(&self) -> &EntryFrp<Self> {
self.parent.frp()
}
}
impl display::Object for TestEntry {
fn display_object(&self) -> &display::object::Instance {
self.parent.display_object()
}
}
fn setup_grid_view<const COL_COUNT: Col>(
app: &Application,
network: &frp::Network,
row_count: Row,
headers: [impl IntoIterator<Item = (Row, <TestEntry as Entry>::Model)>; COL_COUNT],
) -> GridView<TestEntry, TestEntry> {
let headers: [BTreeMap<_, _>; COL_COUNT] = headers.map(|i| i.into_iter().collect());
let headers_layer = app.display.default_scene.layers.main.create_sublayer();
let grid_view = GridView::<TestEntry, TestEntry>::new(app);
let header_frp = grid_view.header_frp();
header_frp.set_layers(WeakLayers::new(&headers_layer, None));
frp::extend! { network
grid_view.model_for_entry <+ grid_view.model_for_entry_needed.map(|&(row, col)| (row, col, Immutable(row + col)));
header_frp.section_info <+ header_frp.section_info_needed.map(move |&(row, col)| {
let (&section_start, model) = headers[col].range(..=row).last().unwrap();
let section_end = headers[col].range((row+1)..).next().map_or(row_count, |(row, _)| *row);
(section_start..section_end, col, *model)
});
}
grid_view.set_entries_size(Vector2(10.0, 10.0));
grid_view
}
fn check_headers_positions<const COL_COUNT: Col>(
grid_view: &GridView<TestEntry, TestEntry>,
expected: [Vector2; COL_COUNT],
) {
let visible_headers = get_sorted_headers(grid_view);
assert_eq!(visible_headers.len(), COL_COUNT);
for ((_, header), expected_pos) in visible_headers.into_iter().zip(expected.into_iter()) {
assert_eq!(header.entry.position().xy(), expected_pos);
}
}
fn check_headers_models<const COL_COUNT: Col>(
grid_view: &GridView<TestEntry, TestEntry>,
expected: [usize; COL_COUNT],
) {
let visible_headers = get_sorted_headers(grid_view);
assert_eq!(visible_headers.len(), COL_COUNT);
for ((_, header), expected_model) in visible_headers.into_iter().zip(expected.into_iter()) {
assert_eq!(header.entry.entry.parent.model_set.get(), expected_model);
}
}
fn check_headers_sections<const COL_COUNT: Col>(
grid_view: &GridView<TestEntry, TestEntry>,
expected: [(Range<Row>, Col); COL_COUNT],
) {
let visible_headers = get_sorted_headers(grid_view);
assert_eq!(visible_headers.len(), COL_COUNT);
for ((col, header), (expected_rows, expected_col)) in
visible_headers.into_iter().zip(expected.into_iter())
{
assert_eq!(col, expected_col);
assert_eq!(header.section_rows, expected_rows);
}
}
fn get_sorted_headers(
grid_view: &GridView<TestEntry, TestEntry>,
) -> Vec<(Col, VisibleHeader<TestEntry>)> {
let visible_headers = grid_view.model.visible_headers.borrow();
visible_headers
.iter()
.map(|(col, header)| (*col, header.clone()))
.sorted_by_key(|(col, _)| *col)
.collect()
}
#[test]
fn initializing_headers_and_pushing_them_down() {
let network = frp::Network::new("tests::initializing_headers_in_column");
let app = Application::new("root");
let headers_info = [[(0, Immutable(11))], [(1, Immutable(12))], [(0, Immutable(13))]];
let grid = setup_grid_view(&app, &network, 4, headers_info);
grid.set_viewport(Viewport { left: 5.0, right: 26.0, top: -12.0, bottom: -25.0 });
grid.reset_entries(4, 3);
check_headers_sections(&grid, [(0..4, 0), (1..4, 1), (0..4, 2)]);
check_headers_models(&grid, [11, 12, 13]);
check_headers_positions(&grid, [
Vector2(5.0, -16.5),
Vector2(15.0, -16.5),
Vector2(25.0, -16.5),
]);
grid.set_viewport(Viewport { left: 5.0, right: 28.0, top: -17.0, bottom: -30.0 });
check_headers_sections(&grid, [(0..4, 0), (1..4, 1), (0..4, 2)]);
check_headers_models(&grid, [11, 12, 13]);
check_headers_positions(&grid, [
Vector2(5.0, -21.5),
Vector2(15.0, -21.5),
Vector2(25.0, -21.5),
]);
}
#[test]
fn pushing_headers_down() {
let app = Application::new("root");
let network = frp::Network::new("tests::initializing_headers_in_column");
let sections = [
vec![(0, Immutable(1)), (1, Immutable(4)), (4, Immutable(6))],
vec![(0, Immutable(2)), (2, Immutable(5)), (4, Immutable(7))],
vec![(0, Immutable(3)), (3, Immutable(8))],
];
let grid = setup_grid_view(&app, &network, 8, sections);
grid.set_viewport(Viewport { left: 0.0, right: 10.0, top: -0.0, bottom: -20.0 });
grid.reset_entries(8, 3);
check_headers_positions(&grid, [Vector2(5.0, -5.0)]);
check_headers_models(&grid, [1]);
check_headers_sections(&grid, [(0..1, 0)]);
grid.set_viewport(Viewport { left: 5.0, right: 15.0, top: -5.0, bottom: -25.0 });
check_headers_positions(&grid, [Vector2(5.0, -5.5), Vector2(15.0, -9.5)]);
check_headers_models(&grid, [1, 2]);
check_headers_sections(&grid, [(0..1, 0), (0..2, 1)]);
grid.set_viewport(Viewport { left: 5.0, right: 15.0, top: -10.0, bottom: -30.0 });
check_headers_positions(&grid, [Vector2(5.0, -15.0), Vector2(15.0, -14.5)]);
check_headers_models(&grid, [4, 2]);
check_headers_sections(&grid, [(1..4, 0), (0..2, 1)]);
grid.set_viewport(Viewport { left: 15.0, right: 25.0, top: -10.0, bottom: -30.0 });
check_headers_positions(&grid, [Vector2(15.0, -14.5), Vector2(25.0, -14.5)]);
check_headers_models(&grid, [2, 3]);
check_headers_sections(&grid, [(0..2, 1), (0..3, 2)]);
grid.set_viewport(Viewport { left: 5.0, right: 15.0, top: -35.0, bottom: -45.0 });
check_headers_positions(&grid, [Vector2(5.0, -35.5), Vector2(15.0, -35.5)]);
check_headers_models(&grid, [4, 5]);
check_headers_sections(&grid, [(1..4, 0), (2..4, 1)]);
}
}

View File

@ -1,12 +1,24 @@
//! Grid View EnsoGL Component.
//!
//! The main structure is [`GridView`] - see its docs for details.
//! There are many variants of the Grid View component:
//! * the basic one: [`GridView`],
//! * with scroll bars: [`scrollable::GridView`],
//! * with selection and mouse hover highlights: [`selectable::GridView`],
//! * with sections and headers visible even when scrolled down: [`header::GridView`].
//! * The combinations of the features of above three, like [`selectable::GridViewWithHeaders`] or
//! [`scrollable::SeleectableGridViewWithHeraders`].
//!
//! Each variant expose the [`FRP`] structure as its main API - additional APIs (like for handling
//! highlight or headers) are available through accessors (like `header_frp` or
//! `selection_highlight_frp`). Also, as every variant is based on the basic [`GridView`], they
//! implement `AsRef<GridView>`.
#![recursion_limit = "1024"]
// === Features ===
#![feature(option_result_contains)]
#![feature(trait_alias)]
#![feature(hash_drain_filter)]
#![feature(type_alias_impl_trait)]
#![feature(bool_to_option)]
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
@ -27,6 +39,7 @@
pub mod column_widths;
pub mod entry;
pub mod header;
pub mod scrollable;
pub mod selectable;
pub mod simple;
@ -55,7 +68,6 @@ 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::column_widths::ColumnWidths;
@ -66,23 +78,45 @@ use crate::visible_area::visible_rows;
pub use entry::Entry;
// =================
// === Constants ===
// =================
const MOUSE_MOVEMENT_NEEDED_TO_HOVER_PX: f32 = 1.5;
// ===========
// === FRP ===
// ===========
// === Row and Col Aliases ===
/// A row index in [`GridView`].
pub type Row = usize;
/// A column index in [`GridView`].
pub type Col = usize;
// === Properties ===
/// A set of GridView properties used in many operations.
#[allow(missing_docs)]
#[derive(Copy, Clone, Debug, Default)]
pub struct Properties {
pub row_count: usize,
pub col_count: usize,
pub viewport: Viewport,
pub entries_size: Vector2,
}
impl Properties {
/// Return iterator over all visible locations (row-column pairs).
pub fn all_visible_locations(
&self,
column_widths: &ColumnWidths,
) -> impl Iterator<Item = (Row, Col)> {
let Self { row_count, col_count, viewport, entries_size } = *self;
all_visible_locations(viewport, entries_size, row_count, col_count, column_widths)
}
}
// === The Endpoints ===
ensogl_core::define_endpoints_2! {
<EntryModel: (frp::node::Data), EntryParams: (frp::node::Data)>
Input {
@ -122,31 +156,15 @@ ensogl_core::define_endpoints_2! {
entries_size(Vector2),
entries_params(EntryParams),
content_size(Vector2),
properties(Properties),
/// Event emitted when the Grid View needs model for an uncovered entry.
model_for_entry_needed(Row, Col),
entry_shown(Row, Col),
entry_contour(Row, Col, entry::Contour),
entry_hovered(Option<(Row, Col)>),
entry_selected(Option<(Row, Col)>),
entry_accepted(Row, Col),
}
}
// ====================
// === VisibleEntry ===
// ====================
#[derive(Clone, CloneRef, Debug)]
#[clone_ref(bound = "Entry: CloneRef")]
struct VisibleEntry<Entry> {
entry: Entry,
overlay: entry::overlay::View,
}
impl<E: display::Object> display::Object for VisibleEntry<E> {
fn display_object(&self) -> &display::object::Instance {
self.entry.display_object()
column_resized(Col, f32),
}
}
@ -156,138 +174,21 @@ impl<E: display::Object> display::Object for VisibleEntry<E> {
// === Model ===
// =============
// === EntryCreationCtx ===
/// A structure gathering all data required for creating new entry instance.
#[derive(CloneRef, Debug, Derivative)]
#[derivative(Clone(bound = ""))]
struct EntryCreationCtx<EntryParams> {
app: Application,
network: frp::WeakNetwork,
set_entry_size: frp::Stream<Vector2>,
set_entry_params: frp::Stream<EntryParams>,
entry_hovered: frp::Any<Option<(Row, Col)>>,
entry_selected: frp::Any<Option<(Row, Col)>>,
entry_accepted: frp::Any<(Row, Col)>,
override_column_width: frp::Any<(Col, f32)>,
}
impl<EntryParams> EntryCreationCtx<EntryParams>
where EntryParams: frp::node::Data
{
fn create_entry<E: Entry<Params = EntryParams>>(
&self,
text_layer: Option<&Layer>,
) -> VisibleEntry<E> {
let entry = E::new(&self.app, text_layer);
let overlay = entry::overlay::View::new(Logger::new("EntryOverlay"));
entry.add_child(&overlay);
if let Some(network) = self.network.upgrade_or_warn() {
let entry_frp = entry.frp();
let entry_network = entry_frp.network();
let mouse = &self.app.display.default_scene.mouse.frp;
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();
contour <- all(init, entry_frp.contour)._1();
eval contour ((c) overlay.set_contour(*c));
let events = &overlay.events;
let disabled = &entry_frp.disabled;
let location = entry_frp.set_location.clone_ref();
column <- location._1();
// We make a distinction between "hovered" state and "mouse_in" state, because
// we want to highlight entry as hovered only when mouse moves a bit.
hovering <- any(...);
hovering <+ events.mouse_out.constant(false);
mouse_in <- bool(&events.mouse_out, &events.mouse_over);
// We can receive `mouse_over` event a couple of frames after actual hovering.
// Therefore, we count our "mouse move" from a couple of frames before.
mouse_pos_some_time_ago <- mouse.prev_position.previous().previous().previous();
mouse_over_movement_start <- mouse_pos_some_time_ago.sample(&events.mouse_over);
mouse_over_with_start_pos <- all(mouse.position, mouse_over_movement_start).gate(&mouse_in);
mouse_move_which_hovers <- mouse_over_with_start_pos.filter(
|(pos, start_pos)| (pos - start_pos).norm() > MOUSE_MOVEMENT_NEEDED_TO_HOVER_PX
);
hovered <- mouse_move_which_hovers.gate_not(&hovering).gate_not(disabled);
hovering <+ hovered.constant(true);
selected <- events.mouse_down.gate_not(disabled);
accepted <- events.mouse_down_primary.gate_not(disabled);
self.entry_hovered <+ location.sample(&hovered).map(|l| Some(*l));
self.entry_selected <+ location.sample(&selected).map(|l| Some(*l));
self.entry_accepted <+ location.sample(&accepted);
self.override_column_width <+ entry_frp.override_column_width.map2(
&column,
|width, col| (*col, *width)
);
}
init.emit(());
}
VisibleEntry { entry, overlay }
}
}
fn entry_position(
row: Row,
col: Col,
entry_size: Vector2,
column_widths: &ColumnWidths,
) -> Vector2 {
let x_offset = column_widths.pos_offset(col) + column_widths.width_diff(col) / 2.0;
let x = (col as f32 + 0.5) * entry_size.x + x_offset;
let y = (row as f32 + 0.5) * -entry_size.y;
Vector2(x, y)
}
fn set_entry_position<E: display::Object>(
entry: &E,
row: Row,
col: Col,
entry_size: Vector2,
column_widths: &ColumnWidths,
) {
entry.set_position_xy(entry_position(row, col, entry_size, column_widths));
}
// === Properties ===
#[derive(Copy, Clone, Debug, Default)]
struct Properties {
row_count: usize,
col_count: usize,
viewport: Viewport,
entries_size: Vector2,
}
impl Properties {
fn all_visible_locations(
&self,
column_widths: &ColumnWidths,
) -> impl Iterator<Item = (Row, Col)> {
let rows = self.row_count;
let cols = self.col_count;
all_visible_locations(&self.viewport, self.entries_size, rows, cols, column_widths)
}
}
// === Model ===
/// The Model of [`GridView`].
#[allow(missing_docs)]
#[derive(Clone, Debug)]
pub struct Model<Entry, EntryParams> {
display_object: display::object::Instance,
visible_entries: RefCell<HashMap<(Row, Col), VisibleEntry<Entry>>>,
free_entries: RefCell<Vec<VisibleEntry<Entry>>>,
entry_creation_ctx: EntryCreationCtx<EntryParams>,
column_widths: ColumnWidths,
display_object: display::object::Instance,
visible_entries: RefCell<HashMap<(Row, Col), entry::visible::VisibleEntry<Entry>>>,
free_entries: RefCell<Vec<entry::visible::VisibleEntry<Entry>>>,
pub entry_creation_ctx: entry::visible::CreationCtx<EntryParams>,
column_widths: ColumnWidths,
}
impl<Entry, EntryParams> Model<Entry, EntryParams> {
fn new(entry_creation_ctx: EntryCreationCtx<EntryParams>) -> Self {
fn new(entry_creation_ctx: entry::visible::CreationCtx<EntryParams>) -> Self {
let logger = Logger::new("GridView");
let display_object = display::object::Instance::new(&logger);
let visible_entries = default();
@ -303,8 +204,8 @@ impl<Entry: display::Object, EntryParams> Model<Entry, EntryParams> {
let widths = &self.column_widths;
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, rows);
let visible_cols = visible_columns(&viewport, entries_size, cols, widths);
let visible_rows = visible_rows(viewport, entries_size, rows);
let visible_cols = visible_columns(viewport, entries_size, cols, widths);
let no_longer_visible = visible_entries.drain_filter(|(row, col), _| {
!visible_rows.contains(row) || !visible_cols.contains(col)
});
@ -313,7 +214,7 @@ impl<Entry: display::Object, EntryParams> Model<Entry, EntryParams> {
entry
});
free_entries.extend(detached);
let uncovered = all_visible_locations(&viewport, entries_size, rows, cols, widths)
let uncovered = all_visible_locations(viewport, entries_size, rows, cols, widths)
.filter(|loc| !visible_entries.contains_key(loc));
uncovered.collect_vec()
}
@ -323,7 +224,7 @@ impl<Entry: display::Object, EntryParams> Model<Entry, EntryParams> {
for ((row, col), visible_entry) in &*self.visible_entries.borrow() {
let size = properties.entries_size;
let widths = &self.column_widths;
set_entry_position(visible_entry, *row, *col, size, widths);
entry::visible::set_position(visible_entry, *row, *col, size, widths);
}
to_model_request
}
@ -384,7 +285,13 @@ impl<E: Entry> Model<E, E::Params> {
Occupied(entry) => (entry.into_mut(), false),
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.column_widths);
entry::visible::set_position(
&new_entry,
row,
col,
entry_size,
&self.column_widths,
);
self.display_object.add_child(&new_entry);
(lack_of_entry.insert(new_entry), true)
}
@ -408,18 +315,20 @@ impl<E: Entry> Model<E, E::Params> {
properties: Properties,
) -> Vec<(Row, Col)> {
let to_model_request = self.update_entries_visibility(properties);
let entries_size = properties.entries_size;
// We must not emit FRP events when some state is borrowed to avoid double borrows.
// So the following code block isolates operations with borrowed fields from emitting
// FRP events.
let entries_and_sizes = {
let borrowed = self.visible_entries.borrow();
// We are not interested in columns to the left of the resized column.
let entries = borrowed.iter().filter(|((_, col), _)| *col >= resized_column);
let entries_and_sizes = entries.map(|((row, col), entry)| {
let size = properties.entries_size;
set_entry_position(entry, *row, *col, size, &self.column_widths);
let should_update_pos = borrowed.iter().filter(|((_, col), _)| *col >= resized_column);
for ((row, col), entry) in should_update_pos {
entry::visible::set_position(entry, *row, *col, entries_size, &self.column_widths);
}
let should_update_size = borrowed.iter().filter(|((_, col), _)| *col == resized_column);
let entries_and_sizes = should_update_size.map(|((_, col), entry)| {
let width_diff = self.column_widths.width_diff(*col);
(entry.clone_ref(), size + Vector2(width_diff, 0.0))
(entry.clone_ref(), entries_size + Vector2(width_diff, 0.0))
});
entries_and_sizes.collect_vec()
};
@ -519,7 +428,7 @@ pub type GridView<E> = GridViewTemplate<E, <E as Entry>::Model, <E as Entry>::Pa
impl<E: Entry> GridView<E> {
/// Create new Grid View.
pub fn new(app: &Application) -> Self {
fn new(app: &Application) -> Self {
let frp = Frp::new();
let network = frp.network();
let input = &frp.private.input;
@ -529,11 +438,12 @@ impl<E: Entry> GridView<E> {
set_entry_params <- input.set_entries_params.sampler();
override_column_width <- any(...);
}
let entry_creation_ctx = EntryCreationCtx {
let entry_creation_ctx = entry::visible::CreationCtx {
app: app.clone_ref(),
network: network.downgrade(),
set_entry_size: set_entry_size.into(),
set_entry_params: set_entry_params.into(),
entry_contour: out.entry_contour.clone_ref(),
entry_hovered: out.entry_hovered.clone_ref(),
entry_selected: out.entry_selected.clone_ref(),
entry_accepted: out.entry_accepted.clone_ref(),
@ -541,47 +451,51 @@ impl<E: Entry> GridView<E> {
};
let model = Rc::new(Model::new(entry_creation_ctx));
frp::extend! { network
out.grid_size <+ input.resize_grid;
out.grid_size <+ input.reset_entries;
out.viewport <+ input.set_viewport;
out.entries_size <+ input.set_entries_size;
out.entries_params <+ input.set_entries_params;
prop <- all_with3(
&out.grid_size, &out.viewport, &out.entries_size,
grid_size <- any(input.resize_grid, input.reset_entries);
// We want to update `properties` output first, as some could listen for specific
// event (e.g. the `viewport` and expect the `properties` output is up-to-date.
out.properties <+ all_with3(
&grid_size, &input.set_viewport, &input.set_entries_size,
|&(row_count, col_count), &viewport, &entries_size| {
Properties { row_count, col_count, viewport, entries_size }
}
);
out.grid_size <+ grid_size;
out.grid_size <+ input.reset_entries;
out.viewport <+ input.set_viewport;
out.entries_size <+ input.set_entries_size;
out.entries_params <+ input.set_entries_params;
set_column_width <- any(&input.set_column_width, &override_column_width);
resized_column <- set_column_width.map2(
&prop,
&out.properties,
f!(((col, width), prop) {
model.resize_column(*col,*width, *prop);
*col
})
);
out.column_resized <+ set_column_width;
column_resized <- resized_column.constant(());
content_size_params <- all(out.grid_size, input.set_entries_size, column_resized);
out.content_size <+ content_size_params.map(f!((&((rows, cols), esz, _)) model.content_size(rows, cols, esz)));
request_models_after_vis_area_change <=
input.set_viewport.map2(&prop, f!((_, p) model.update_entries_visibility(*p)));
input.set_viewport.map2(&out.properties, f!((_, p) model.update_entries_visibility(*p)));
request_model_after_grid_size_change <=
input.resize_grid.map2(&prop, f!((_, p) model.update_entries_visibility(*p)));
input.resize_grid.map2(&out.properties, f!((_, p) model.update_entries_visibility(*p)));
request_models_after_entry_size_change <= input.set_entries_size.map2(
&prop,
&out.properties,
f!((_, p) model.update_after_entries_size_change(*p))
);
request_models_after_reset <=
input.reset_entries.map2(&prop, f!((_, p) model.reset_entries(*p)));
input.reset_entries.map2(&out.properties, f!((_, p) model.reset_entries(*p)));
request_models_after_column_resize <=
resized_column.map2(&prop, f!((col, p) model.update_after_column_resize(*col, *p)));
resized_column.map2(&out.properties, f!((col, p) model.update_after_column_resize(*col, *p)));
request_models_after_text_layer_change <=
input.set_text_layer.map2(&prop, f!((_, p) model.drop_all_entries(*p)));
input.set_text_layer.map2(&out.properties, f!((_, p) model.drop_all_entries(*p)));
request_models_for_request <= input.request_model_for_visible_entries.map2(
&prop,
&out.properties,
f!([model](_, p) p.all_visible_locations(&model.column_widths).collect_vec()),
);
out.model_for_entry_needed <+ request_models_after_vis_area_change;
@ -600,7 +514,7 @@ impl<E: Entry> GridView<E> {
// inform everyone that the entry is visible. They may want to immediately get the entry
// with [`get_entry`] method.
model_prop_and_layer <-
input.model_for_entry.map3(&prop, &input.set_text_layer, |model, prop, layer| (model.clone(), *prop, layer.clone()));
input.model_for_entry.map3(&out.properties, &input.set_text_layer, |m, p, l| (m.clone(), *p, l.clone()));
eval model_prop_and_layer
((((row, col, entry_model), prop, layer): &((Row, Col, E::Model), Properties, Option<WeakLayer>))
model.update_entry(*row, *col, entry_model.clone(), prop.entries_size, layer)
@ -615,15 +529,23 @@ impl<E: Entry> GridView<E> {
impl<Entry, EntryModel, EntryParams> GridViewTemplate<Entry, EntryModel, EntryParams>
where
Entry: CloneRef,
EntryModel: frp::node::Data,
EntryParams: frp::node::Data,
{
fn get_entry(&self, row: Row, column: Col) -> Option<Entry> {
/// Get the entry instance for given row and column, or `None` if no entry is instantiated at
/// given location.
pub fn get_entry(&self, row: Row, column: Col) -> Option<Entry>
where Entry: CloneRef {
let entries = self.widget.model().visible_entries.borrow();
let entry = entries.get(&(row, column));
entry.map(|e| e.entry.clone_ref())
}
/// Return the position of the Entry instance for given row and column.
pub fn entry_position(&self, row: Row, column: Col) -> Vector2 {
let column_widths = &self.widget.model().column_widths;
entry::visible::position(row, column, self.entries_size.value(), column_widths)
}
}
impl<Entry, EntryModel: frp::node::Data, EntryParams: frp::node::Data> AsRef<Self>
@ -649,20 +571,21 @@ impl<Entry, EntryModel: frp::node::Data, EntryParams: frp::node::Data> display::
// =============
#[cfg(test)]
mod tests {
pub(crate) mod tests {
use super::*;
use ensogl_core::display::scene::Layer;
#[derive(Copy, Clone, CloneRef, Debug, Default)]
struct TestEntryParams {
param: Immutable<usize>,
pub struct TestEntryParams {
pub param: Immutable<usize>,
}
#[derive(Clone, CloneRef, Debug)]
struct TestEntry {
frp: EntryFrp<Self>,
param_set: Rc<Cell<usize>>,
model_set: Rc<Cell<usize>>,
display_object: display::object::Instance,
pub struct TestEntry {
pub frp: EntryFrp<Self>,
pub param_set: Rc<Cell<usize>>,
pub model_set: Rc<Cell<usize>>,
pub display_object: display::object::Instance,
}
impl Entry for TestEntry {

View File

@ -2,9 +2,11 @@
use crate::prelude::*;
use crate::header;
use crate::selectable;
use crate::Entry;
use crate::header::WeakLayers;
use enso_frp as frp;
use ensogl_core::application::command::FrpNetworkProvider;
use ensogl_core::application::Application;
@ -13,7 +15,6 @@ use ensogl_core::display::scene::Layer;
use ensogl_scroll_area::ScrollArea;
// ================
// === GridView ===
// ================
@ -23,10 +24,12 @@ use ensogl_scroll_area::ScrollArea;
#[derive(Clone, CloneRef, Debug, Deref)]
#[clone_ref(bound = "InnerGridView: CloneRef")]
pub struct GridViewTemplate<InnerGridView> {
area: ScrollArea,
area: ScrollArea,
#[deref]
inner_grid: InnerGridView,
text_layer: Layer,
inner_grid: InnerGridView,
text_layer: Layer,
header_layer: Immutable<Option<Layer>>,
header_text_layer: Immutable<Option<Layer>>,
}
/// Scrollable Grid View Component.
@ -46,15 +49,45 @@ pub struct GridViewTemplate<InnerGridView> {
/// for Models.
pub type GridView<E> = GridViewTemplate<crate::GridView<E>>;
/// Scrollable Grid View with Headers
///
/// This Component displays any kind of entry `E` in a grid, where each column is organized in
/// sections. The headers of each section will remain visible during scrolling down.
///
/// Essentially, it's a [scrollable `GridView`](GridView) wrapping the
/// [`GridView` with headers](selectable::GridView). See their respective documentations for usage
/// information.
///
/// **Important** After construction, the Scroll Area content will have sub-layers for headers
/// already set up. There is no need of calling [`header::Frp::set_layers`] endpoint.
pub type GridViewWithHeaders<E, H> = GridViewTemplate<header::GridView<E, H>>;
/// Scrollable and Selectable Grid View Component.
///
/// This Component displays any kind of entry `E` in a grid, inside the Scroll area and allowing
/// displaying highlights for hovered and selected entries.
///
/// Essentially, it's a [scrollable `GridView`](GridView) wrapping the [selectable `GridView`]. See
/// their respective documentations for usage information.
/// Essentially, it's a [scrollable `GridView`](GridView) wrapping the
/// [selectable `GridView`](selectable::GridView). See their respective documentations for usage
/// information.
pub type SelectableGridView<E> = GridViewTemplate<selectable::GridView<E>>;
/// Scrollable and Selectable Grid View Component With Headers.
///
/// This Component displays any kind of entry `E` in a grid, inside the Scroll area and allowing
/// displaying highlights for hovered and selected entries. Each column is organized in
/// sections. The headers of each section will remain visible during scrolling down
///
/// This is a most feature-rich Grid View versin in this crate, thus the most complex.
/// Inside it is a [scrollable `GridView`](GridView) wrapping the
/// [selectable `GridView`](selectable::GridViewWithHeaders) version
/// [with headers](header::GridView) See their respective documentations for usage information.
///
/// **Important** After construction, the Scroll Area content will have sub-layers for headers
/// already set up. There is no need of calling [`header::Frp::set_layers`] endpoint.
pub type SelectableGridViewWithHeaders<E, H> =
GridViewTemplate<selectable::GridViewWithHeaders<E, H>>;
impl<InnerGridView> GridViewTemplate<InnerGridView> {
/// Create new Scrollable Grid View component wrapping a created instance of `inner_grid`.
pub fn new_wrapping<E>(app: &Application, inner_grid: InnerGridView) -> Self
@ -66,6 +99,8 @@ impl<InnerGridView> GridViewTemplate<InnerGridView> {
area.content().add_child(&inner_grid);
let network = base_grid.network();
let text_layer = area.content_layer().create_sublayer();
let header_layer = default();
let header_text_layer = default();
base_grid.set_text_layer(Some(text_layer.downgrade()));
frp::extend! { network
@ -74,7 +109,35 @@ impl<InnerGridView> GridViewTemplate<InnerGridView> {
area.set_content_height <+ base_grid.content_size.map(|s| s.y);
}
Self { area, inner_grid, text_layer }
Self { area, inner_grid, text_layer, header_layer, header_text_layer }
}
/// Create new Scrollable Grid View component wrapping a created instance of `inner_grid` which
/// is a variant of Grid View with Headers.
///
/// The Scroll Area content will have created sub-layers for headers. There is no need of
/// calling [`header::Frp::set_layers`] endpoint.
pub fn new_wrapping_with_headers<E, NestedGridView, Header>(
app: &Application,
inner_grid: InnerGridView,
) -> Self
where
E: Entry,
Header: Entry,
InnerGridView: AsRef<crate::GridView<E>>
+ AsRef<
header::GridViewTemplate<E, NestedGridView, Header, Header::Model, Header::Params>,
> + display::Object,
{
let mut this = Self::new_wrapping(app, inner_grid);
let header_grid: &header::GridViewTemplate<_, _, _, _, _> = this.inner_grid.as_ref();
let header_frp = header_grid.header_frp();
let header_layer = this.area.content_layer().create_sublayer();
let header_text_layer = this.area.content_layer().create_sublayer();
header_frp.set_layers(WeakLayers::new(&header_layer, Some(&header_text_layer)));
this.header_layer = Immutable(Some(header_layer));
this.header_text_layer = Immutable(Some(header_text_layer));
this
}
/// Resize the component. It's a wrapper for [`scroll_frp`]`().resize`.
@ -96,6 +159,13 @@ impl<E: Entry> GridView<E> {
}
}
impl<E: Entry, H: Entry<Params = E::Params>> GridViewWithHeaders<E, H> {
/// Create new scrollable [`SelectableGridView`] component.
pub fn new(app: &Application) -> Self {
Self::new_wrapping_with_headers(app, header::GridView::new(app))
}
}
impl<E: Entry> SelectableGridView<E> {
/// Create new scrollable [`SelectableGridView`] component.
pub fn new(app: &Application) -> Self {
@ -103,6 +173,13 @@ impl<E: Entry> SelectableGridView<E> {
}
}
impl<E: Entry, H: Entry<Params = E::Params>> SelectableGridViewWithHeaders<E, H> {
/// Create new scrollable [`SelectableGridView`] component.
pub fn new(app: &Application) -> Self {
Self::new_wrapping_with_headers(app, selectable::GridViewWithHeaders::new(app))
}
}
impl<InnerGridView> display::Object for GridViewTemplate<InnerGridView> {
fn display_object(&self) -> &display::object::Instance {
self.area.display_object()

View File

@ -2,6 +2,7 @@
use crate::prelude::*;
use crate::header;
use crate::Entry;
use ensogl_core::application::Application;
@ -23,17 +24,15 @@ pub mod highlight;
/// 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<
Entry: 'static,
EntryModel: frp::node::Data,
EntryParams: frp::node::Data,
> {
#[derivative(Clone(bound = "InnerGridView: Clone"))]
#[clone_ref(bound = "InnerGridView: CloneRef")]
pub struct GridViewTemplate<InnerGridView, Entry, EntryParams: frp::node::Data> {
#[deref]
grid: crate::GridViewTemplate<Entry, EntryModel, EntryParams>,
grid: InnerGridView,
highlights: highlight::shape::View,
selection_handler: highlight::Handler<Entry, EntryModel, EntryParams>,
hover_handler: highlight::Handler<Entry, EntryModel, EntryParams>,
header_highlights: Immutable<Option<highlight::shape::View>>,
selection_handler: highlight::SelectionHandler<InnerGridView, Entry, EntryParams>,
hover_handler: highlight::HoverHandler<InnerGridView, Entry, EntryParams>,
}
/// The Selectable Grid View.
@ -63,32 +62,95 @@ pub struct GridViewTemplate<
///
/// The "Masked layer" mode may be set for highlight and selection independently, by calling
/// [`highlight::FRP::setup_masked_layer`] on the proper highlight API.
pub type GridView<E> = GridViewTemplate<E, <E as Entry>::Model, <E as Entry>::Params>;
pub type GridView<E> = GridViewTemplate<crate::GridView<E>, E, <E as Entry>::Params>;
/// The Selectable Grid View with Headers.
///
/// An extension of [the `GridView` with headers](header::GridView), where hovered and selected
/// entries or pushed-down headers will be highlighted.
///
/// # Highlight Shape
///
/// The selection shape in this case is implemented in a pretty tricky way. We want to render the
/// highlight between specific `Entry` elements (e.g. between the text and the background). However,
/// the headers are rendered in different layer, and we want the selection to be above the header
/// background and below the text entries at the same time, which is not possible.
///
/// Therefore, this version has two highlight shapes instantiated, one for normal entries and one
/// for headers, the latter being clipped at the header bottom (using the [highlight shape clipping
/// ability](highlight::shape::AttrSetter::top_clip)).
///
/// # Highlight Mask in *Masked Layer* Mode.
///
/// The grid view displayed in the masked layer also is a grid view with headers. As In a scrolled
/// grid view, the user can select a partially visible entry behind the group's header, we manually
/// clip the highlight shape in the mask in such case, so the highlight will not be over the header.
pub type GridViewWithHeaders<E, HeaderEntry> =
GridViewTemplate<header::GridView<E, HeaderEntry>, E, <E as Entry>::Params>;
impl<InnerGridView, E: Entry> GridViewTemplate<InnerGridView, E, E::Params>
where
InnerGridView: AsRef<crate::GridView<E>> + display::Object,
highlight::SelectionHandler<InnerGridView, E, E::Params>:
highlight::HasConstructor<InnerGridView = InnerGridView>,
highlight::HoverHandler<InnerGridView, E, E::Params>:
highlight::HasConstructor<InnerGridView = InnerGridView>,
{
fn new_wrapping(app: &Application, grid: InnerGridView) -> Self {
let highlights = highlight::shape::View::new(Logger::new("highlights"));
let header_highlights = Immutable(None);
let selection_handler = highlight::SelectionHandler::new_connected(app, &grid);
let hover_handler = highlight::HoverHandler::new_connected(app, &grid);
grid.add_child(&highlights);
selection_handler.connect_with_shape(&highlights);
hover_handler.connect_with_shape(&highlights);
let grid_frp = grid.as_ref().frp();
let network = grid_frp.network();
frp::extend! { network
eval grid_frp.viewport ([highlights](&vp) {
highlight::shape::set_viewport(&highlights, vp);
});
}
Self { grid, highlights, header_highlights, selection_handler, hover_handler }
}
}
impl<E: Entry> GridView<E> {
/// Create new Selectable Grid View instance.
pub fn new(app: &Application) -> Self {
let grid = crate::GridView::<E>::new(app);
let highlights = highlight::shape::View::new(Logger::new("highlights"));
let selection_handler = highlight::Handler::new_for_selection_connected(app, &grid);
let hover_handler = highlight::Handler::new_for_hover_connected(app, &grid);
grid.add_child(&highlights);
selection_handler.connect_with_shape::<highlight::shape::SelectionAttrSetter>(&highlights);
hover_handler.connect_with_shape::<highlight::shape::HoverAttrSetter>(&highlights);
let network = grid.frp().network();
frp::extend! { network
eval grid.viewport ([highlights](&vp) highlight::shape::set_viewport(&highlights, vp));
}
Self { grid, highlights, selection_handler, hover_handler }
Self::new_wrapping(app, crate::GridView::<E>::new(app))
}
}
impl<Entry, EntryModel, EntryParams> GridViewTemplate<Entry, EntryModel, EntryParams>
where
EntryModel: frp::node::Data,
EntryParams: frp::node::Data,
impl<E: Entry, HeaderEntry: Entry<Params = E::Params>> GridViewWithHeaders<E, HeaderEntry> {
/// Create new Selectable Grid View With Headers instance.
pub fn new(app: &Application) -> Self {
let mut this = Self::new_wrapping(app, header::GridView::<E, HeaderEntry>::new(app));
let header_highlights = highlight::shape::View::new(Logger::new("header_highlights"));
this.grid.add_child(&header_highlights);
this.selection_handler.connect_with_header_shape(&header_highlights);
this.hover_handler.connect_with_header_shape(&header_highlights);
let network = this.grid.frp().network();
let header_frp = this.grid.header_frp();
frp::extend! { network
eval this.grid.viewport ([header_highlights](&vp) {
highlight::shape::set_viewport(&header_highlights, vp);
});
eval header_frp.set_layers([header_highlights](layers) if let Some(layer) = layers.upgrade_header() {
layer.add_exclusive(&header_highlights);
});
}
this.header_highlights = Immutable(Some(header_highlights));
this
}
}
impl<InnerGridView, Entry, EntryParams> GridViewTemplate<InnerGridView, Entry, EntryParams>
where EntryParams: frp::node::Data
{
/// Access to the Selection Highlight FRP.
pub fn selection_highlight_frp(&self) -> &highlight::Frp<EntryParams> {
@ -101,21 +163,18 @@ where
}
}
impl<Entry, EntryModel, EntryParams> AsRef<crate::GridViewTemplate<Entry, EntryModel, EntryParams>>
for GridViewTemplate<Entry, EntryModel, EntryParams>
where
EntryModel: frp::node::Data,
EntryParams: frp::node::Data,
impl<InnerGridView, E: Entry, T> AsRef<T> for GridViewTemplate<InnerGridView, E, E::Params>
where InnerGridView: AsRef<T>
{
fn as_ref(&self) -> &crate::GridViewTemplate<Entry, EntryModel, EntryParams> {
&self.grid
fn as_ref(&self) -> &T {
self.grid.as_ref()
}
}
impl<Entry, EntryModel, EntryParams> display::Object
for GridViewTemplate<Entry, EntryModel, EntryParams>
impl<InnerGridView, Entry, EntryParams> display::Object
for GridViewTemplate<InnerGridView, Entry, EntryParams>
where
EntryModel: frp::node::Data,
InnerGridView: display::Object,
EntryParams: frp::node::Data,
{
fn display_object(&self) -> &display::object::Instance {
@ -133,7 +192,7 @@ where
mod tests {
use super::*;
use crate::entry;
use crate::entry_position;
use crate::header::WeakLayers;
use crate::Col;
use crate::EntryFrp;
use crate::Row;
@ -194,6 +253,7 @@ mod tests {
model.selected.set(*selected);
model.hovered.set(*hovered);
});
out.highlight_contour <+ input.set_model.map(|m| m.contour);
out.contour <+ input.set_model.map(|m| m.contour);
out.selection_highlight_color <+ input.set_model.map(|m| m.color);
out.hover_highlight_color <+ input.set_model.map(|m| m.color);
@ -234,7 +294,8 @@ mod tests {
for (row, col) in iproduct!(0..2, 0..2) {
grid_view.select_entry(Some((row, col)));
let column_widths = &grid_view.model().column_widths;
let expected_pos = entry_position(row, col, Vector2(20.0, 20.0), column_widths);
let expected_pos =
entry::visible::position(row, col, Vector2(20.0, 20.0), column_widths);
assert_eq!(highlight_frp.position.value(), expected_pos);
assert_eq!(highlight_frp.contour.value(), CONTOUR_VARIANTS[row]);
assert_eq!(highlight_frp.color.value(), COLOR_VARIANTS[col]);
@ -279,4 +340,51 @@ mod tests {
assert!(!entries[1].selected.get());
assert!(entries[2].selected.get());
}
#[test]
fn selecting_header() {
init_tracing(TRACE);
let app = Application::new("root");
let network = frp::Network::new("selecting_header");
let grid_view = GridViewWithHeaders::<TestEntry, TestEntry>::new(&app);
let headers_layer = app.display.default_scene.layers.main.create_sublayer();
grid_view.header_frp().set_layers(WeakLayers::new(&headers_layer, None));
let entries = (0..3).map(|i| Rc::new(TestEntryModel::new(i, 0))).collect_vec();
let models = entries.clone();
let header_model = Rc::new(TestEntryModel::new(1, 0));
let selection_state = || entries.iter().map(|e| e.selected.get()).collect_vec();
let headers = grid_view.header_frp();
frp::extend! { network
grid_view.model_for_entry <+
grid_view.model_for_entry_needed.map(move |&(r, c)| (r, c, models[r].clone_ref()));
headers.section_info <+
headers.section_info_needed.filter_map(move |&(r, _)| (r > 0).as_some(((1..3), 0, header_model.clone_ref())));
}
grid_view.set_entries_size(Vector2(20.0, 20.0));
let viewport_having_header_pushed_down =
Viewport { left: 1.0, top: -21.0, right: 19.0, bottom: -51.0 };
let viewport_having_header_pushed_down_further =
Viewport { left: 1.0, top: -30.0, right: 19.0, bottom: -60.0 };
let viewport_with_no_pushed_header =
Viewport { left: 1.0, top: -15.0, right: 19.0, bottom: -45.0 };
grid_view.set_viewport(viewport_having_header_pushed_down);
grid_view.reset_entries(3, 1);
grid_view.select_entry(Some((1, 0)));
assert_eq!(selection_state(), vec![false, true, false]);
assert_eq!(grid_view.selection_highlight_frp().position.value(), Vector2(10.0, -31.0));
grid_view.set_viewport(viewport_having_header_pushed_down_further);
assert_eq!(selection_state(), vec![false, true, false]);
assert_eq!(grid_view.selection_highlight_frp().position.value(), Vector2(10.0, -40.0));
tracing::debug!("About to go up");
grid_view.set_viewport(viewport_with_no_pushed_header);
assert_eq!(selection_state(), vec![false, true, false]);
assert_eq!(grid_view.selection_highlight_frp().position.value(), Vector2(10.0, -30.0));
grid_view.set_viewport(viewport_having_header_pushed_down);
assert_eq!(selection_state(), vec![false, true, false]);
assert_eq!(grid_view.selection_highlight_frp().position.value(), Vector2(10.0, -31.0));
}
}

View File

@ -4,9 +4,10 @@
use crate::prelude::*;
use crate::entry;
use crate::entry_position;
use crate::selectable::highlight::shape::HoverAttrSetter;
use crate::selectable::highlight::shape::SelectionAttrSetter;
use crate::header;
use crate::selectable::highlight::connected_entry::ConnectedEntry;
use crate::selectable::highlight::connected_entry::EndpointsGetter;
use crate::selectable::highlight::layer::HasConstructor as TRAIT_HasConstructor;
use crate::Col;
use crate::Entry;
use crate::Row;
@ -21,11 +22,29 @@ use ensogl_core::Animation;
// === Export ===
// ==============
pub mod connected_entry;
pub mod layer;
pub mod shape;
// ============
// === Kind ===
// ============
/// A module with marker types for each highlight kind. Those structs may implement various traits
/// with operations specific for given highlight, for example [shape::AttrSetter] or
/// [connected_entry::EndpointsGetter].
#[allow(missing_docs, missing_copy_implementations)]
pub mod kind {
#[derive(Debug)]
pub struct Selection;
#[derive(Debug)]
pub struct Hover;
}
// ===========
// === FRP ===
// ===========
@ -41,106 +60,58 @@ ensogl_core::define_endpoints_2! { <EntryParams: (frp::node::Data)>
position(Vector2),
contour(entry::Contour),
color(color::Rgba),
/// Used in [Grid Views with headers](header::GridView). The y position of line separating
/// the header of scrolled down section from the rest of entries. If other entry than
/// this header is selected, the highlight should be top-clipped at this position.
header_separator(f32),
/// The y position of line clipping the highlight from the top. Usually it's just the top
/// of the viewport, but may be a lower line in case where a header in
/// [Grid Views with headers](header::GridView) should cover part of the highlight.
top_clip(f32),
is_masked_layer_set(bool),
}
}
/// A subset of [`entry::FRP`] endpoints used by the specific highlight [`Handler`].
#[derive(Clone, CloneRef, Debug)]
pub struct EntryEndpoints {
/// The "is_highlighted" input: may be `is_selected` or `is_hovered`.
flag: frp::Any<bool>,
location: frp::Stream<(Row, Col)>,
contour: frp::Stream<entry::Contour>,
color: frp::Stream<color::Rgba>,
}
/// All highlight animations.
#[derive(Clone, CloneRef, Debug)]
pub struct Animations {
position: Animation<Vector2>,
position_jump: Animation<Vector2>,
position: frp::Stream<Vector2>,
size: Animation<Vector2>,
corners_radius: Animation<f32>,
color: Animation<color::Rgba>,
top_clip_jump: Animation<f32>,
top_clip: frp::Stream<f32>,
}
impl Animations {
/// Set up the animations and connect their targets with handler's FRP.
fn new<EntryParams: frp::node::Data>(frp: &Frp<EntryParams>) -> Self {
let network = frp.network();
let position = Animation::<Vector2>::new(network);
let position_jump = Animation::<Vector2>::new(network);
let size = Animation::<Vector2>::new(network);
let corners_radius = Animation::<f32>::new(network);
let color = Animation::<color::Rgba>::new(network);
let top_clip_jump = Animation::<f32>::new(network);
frp::extend! { network
init <- source_();
position.target <+ frp.position;
position <- all_with(&position_jump.value, &frp.position, |jump, pos| pos + jump);
size.target <+ frp.contour.map(|&c| c.size);
corners_radius.target <+ frp.contour.map(|&c| c.corners_radius);
color.target <+ frp.color;
top_clip <- all_with(&top_clip_jump.value, &frp.top_clip, |jump, clip| clip + jump);
// Skip animations when the highlight is not visible.
size_val <- all(init, size.value)._1();
not_visible <- size_val.map(|sz| sz.x < f32::EPSILON || sz.y < f32::EPSILON);
corners_radius.skip <+ frp.color.gate(&not_visible).constant(());
position.skip <+ frp.position.gate(&not_visible).constant(());
position_jump.skip <+ frp.position.gate(&not_visible).constant(());
color.skip <+ frp.color.gate(&not_visible).constant(());
}
init.emit(());
Self { position, size, corners_radius, color }
}
}
// ======================
// === HighlightGuard ===
// ======================
/// A guard managing the connections between highlighted entry and the [`Handler`] FRP outputs.
///
/// Until dropped, this structure keeps connected the entry endpoints declaring the highlight
/// appearance (`position`, `contour` and `color`) to the appropriate [`Handler`] endpoints.
/// Also, the entry's flag (`is_selected` or `is_hovered`) will be set to `true` on construction and
/// set back to `false` on drop.
#[derive(Debug)]
struct ConnectedEntryGuard {
network: frp::Network,
/// An event emitted when we should drop this guard, for example when the Entry instance is
/// re-used in another location.
should_be_dropped: frp::Stream,
dropped: frp::Source,
}
impl ConnectedEntryGuard {
/// Create guard for entry FRP at given location.
fn new_for_entry<EntryParams: frp::node::Data>(
entry_frp: EntryEndpoints,
row: Row,
col: Col,
frp: &api::private::Output<EntryParams>,
) -> Self {
let network = frp::Network::new("HighlightedEntryGuard");
frp::extend! { network
init <- source_();
dropped <- source_();
contour <- all(init, entry_frp.contour)._1();
color <- all(init, entry_frp.color)._1();
entry_frp.flag <+ init.constant(true);
entry_frp.flag <+ dropped.constant(false);
frp.contour <+ contour;
frp.color <+ color;
location_change <- entry_frp.location.filter(move |loc| *loc != (row, col));
should_be_dropped <- location_change.constant(());
}
init.emit(());
Self { network, dropped, should_be_dropped }
}
}
impl Drop for ConnectedEntryGuard {
fn drop(&mut self) {
self.dropped.emit(());
Self { position_jump, position, size, corners_radius, color, top_clip_jump, top_clip }
}
}
@ -150,100 +121,55 @@ impl Drop for ConnectedEntryGuard {
// === Model ===
// =============
/// Function returning [`EntryEndpoints`] of given entry, used as parameter of highlight
/// [`Handler`].
pub type EntryEndpointsGetter<Entry> = fn(&Entry) -> EntryEndpoints;
/// The inner data structure for [`Handler`].
#[derive(Derivative)]
#[derivative(Debug)]
pub struct Data<Entry: 'static, EntryModel: frp::node::Data, EntryParams: frp::node::Data> {
app: Application,
grid: crate::GridViewTemplate<Entry, EntryModel, EntryParams>,
connected_entry: Cell<Option<(Row, Col)>>,
guard: RefCell<Option<ConnectedEntryGuard>>,
output: api::private::Output<EntryParams>,
animations: Animations,
pub struct Data<InnerGridView, Entry> {
app: Application,
grid: InnerGridView,
animations: Animations,
#[derivative(Debug = "ignore")]
entry_endpoints_getter: EntryEndpointsGetter<Entry>,
layers: RefCell<Option<layer::Handler<Entry, EntryModel, EntryParams>>>,
layers: RefCell<Option<layer::Handler<InnerGridView>>>,
entry_type: PhantomData<Entry>,
}
impl<Entry, EntryModel, EntryParams> Data<Entry, EntryModel, EntryParams>
where
EntryModel: frp::node::Data,
EntryParams: frp::node::Data,
impl<InnerGridView, Entry> Data<InnerGridView, Entry>
where InnerGridView: CloneRef
{
fn new(
app: &Application,
grid: &crate::GridViewTemplate<Entry, EntryModel, EntryParams>,
output: &api::private::Output<EntryParams>,
animations: Animations,
entry_endpoints_getter: EntryEndpointsGetter<Entry>,
) -> Self {
fn new(app: &Application, grid: &InnerGridView, animations: Animations) -> Self {
Self {
app: app.clone_ref(),
grid: grid.clone_ref(),
connected_entry: default(),
guard: default(),
output: output.clone_ref(),
animations,
entry_endpoints_getter,
layers: default(),
entry_type: default(),
}
}
fn drop_guard(&self) {
self.connected_entry.set(None);
self.guard.take();
}
}
impl<E: Entry> Data<E, E::Model, E::Params> {
/// Drop old [`ConnectedEntryGuard`] and create a new one for new highlighted entry.
fn connect_new_highlighted_entry(self: &Rc<Self>, location: Option<(Row, Col)>) {
if let Some((row, col)) = location {
let current_entry = self.connected_entry.get();
let entry_changed = current_entry.map_or(true, |loc| loc != (row, col));
if entry_changed {
self.guard.take();
let entry = self.grid.get_entry(row, col);
*self.guard.borrow_mut() = entry.map(|e| {
let endpoints = (self.entry_endpoints_getter)(&e);
ConnectedEntryGuard::new_for_entry(endpoints, row, col, &self.output)
});
self.connected_entry.set(Some((row, col)));
self.set_up_guard_dropping();
}
} else {
self.drop_guard()
}
}
fn set_up_guard_dropping(self: &Rc<Self>) {
if let Some(guard) = &*self.guard.borrow() {
let network = &guard.network;
let this = self;
frp::extend! { network
eval_ guard.should_be_dropped (this.drop_guard());
}
}
}
fn setup_masked_layer(&self, layer: Option<&WeakLayer>) -> bool {
impl<InnerGrid, E: Entry> Data<InnerGrid, E>
where InnerGrid: AsRef<crate::GridView<E>>
{
fn setup_masked_layer(
&self,
layer: Option<&WeakLayer>,
entries_params: frp::Stream<E::Params>,
) -> bool
where
layer::Handler<InnerGrid>: layer::HasConstructor<InnerGrid = InnerGrid>,
{
self.layers.take();
let new_layers = layer.and_then(|l| l.upgrade()).map(|layer| {
let layers = layer::Handler::new(&self.app, &layer, &self.grid);
let layers = layer::Handler::<InnerGrid>::new(&self.app, &layer, &self.grid);
let shape = &layers.shape;
let network = layers.grid.network();
let highlight_grid_frp = layers.grid.frp();
let frp = &self.output;
self.connect_with_shape::<HoverAttrSetter>(network, shape, false, None);
let network = layers.grid.as_ref().network();
let highlight_grid_frp = layers.grid.as_ref().frp();
self.connect_with_shape::<kind::Hover>(network, shape, false, None, None);
frp::extend! { network
highlight_grid_frp.set_entries_params <+ frp.entries_params;
highlight_grid_frp.set_entries_params <+ entries_params;
}
HoverAttrSetter::set_color(shape, color::Rgba::black());
SelectionAttrSetter::set_size(shape, default());
kind::Hover::set_color(shape, color::Rgba::black());
kind::Selection::set_size(shape, default());
layers
});
let is_layer_set = new_layers.is_some();
@ -262,13 +188,15 @@ impl<E: Entry> Data<E, E::Model, E::Params> {
shape: &shape::View,
connect_color: bool,
hide: Option<&frp::Stream<bool>>,
bottom_clip: Option<&frp::Stream<f32>>,
) {
let grid_frp = self.grid.frp();
let grid_frp = self.grid.as_ref().frp();
frp::extend! { network
init <- source_();
position <- all(init, self.animations.position.value)._1();
position <- all(init, self.animations.position)._1();
size <- all(init, self.animations.size.value)._1();
corners_radius <- all(init, self.animations.corners_radius.value)._1();
top_clip <- all(init, self.animations.top_clip)._1();
}
let size = if let Some(hide) = hide {
frp::extend! { network
@ -283,6 +211,8 @@ impl<E: Entry> Data<E, E::Model, E::Params> {
eval pos_and_viewport ([shape](&(pos, vp)) Setter::set_position(&shape, pos, vp));
eval size ([shape](&size) Setter::set_size(&shape, size));
eval corners_radius ([shape](&r) Setter::set_corners_radius(&shape, r));
top_clip_and_viewport <- all(top_clip, grid_frp.viewport);
eval top_clip_and_viewport ([shape](&(c, v)) Setter::set_top_clip(&shape, c, v));
}
if connect_color {
frp::extend! { network
@ -290,6 +220,13 @@ impl<E: Entry> Data<E, E::Model, E::Params> {
eval color ([shape](&color) Setter::set_color(&shape, color));
}
}
if let Some(bottom_clip) = bottom_clip {
frp::extend! {network
bottom_clip <- all(&init, bottom_clip)._1();
bottom_clip_and_viewport <- all(bottom_clip, grid_frp.viewport);
eval bottom_clip_and_viewport ([shape](&(c, v)) Setter::set_bottom_clip(&shape, c, v));
}
}
init.emit(())
}
}
@ -304,21 +241,102 @@ impl<E: Entry> Data<E, E::Model, E::Params> {
///
/// This is a helper structure for [`selectable::GridView`] which handles a single highlight:
/// selection or hover:
/// * It provides an FRP API for given highlight, where the exact position, contour and color may be
/// read, and the _Masked layer_ highlight mode may be configured (see the [grid
/// view](`selectable::GridView`) documentation for more info about modes.
/// * It can be connected to highlight shape, so it's position, contour and color will be updated.
/// * It provides an FRP API for given highlight, where the exact position, contour, color and
/// clipping may be read, and the _Masked layer_ highlight mode may be configured (see the [grid
/// view](`selectable::GridView`) documentation for more info about modes).
/// * It can be connected to highlight shape, so it's position, contour, color and clipping will be
/// updated.
/// * It sets/unsets the proper flag on [`Entry`] instance (`set_selected` or `set_hovered`).
///
/// The `Kind` parameter should be any from [`kind`] module.
#[allow(missing_docs)]
#[derive(CloneRef, Debug, Derivative, Deref)]
#[derivative(Clone(bound = ""))]
pub struct Handler<Entry: 'static, EntryModel: frp::node::Data, EntryParams: frp::node::Data> {
pub struct Handler<Kind, InnerGridView, Entry, EntryParams: frp::node::Data> {
#[deref]
pub frp: Frp<EntryParams>,
model: Rc<Data<Entry, EntryModel, EntryParams>>,
pub frp: Frp<EntryParams>,
model: Rc<Data<InnerGridView, Entry>>,
kind_type: PhantomData<Kind>,
}
impl<E: Entry> Handler<E, E::Model, E::Params> {
/// The handler of selection highlight.
pub type SelectionHandler<InnerGridView, Entry, EntryParams> =
Handler<kind::Selection, InnerGridView, Entry, EntryParams>;
/// The handler of mouse hover highlight.
pub type HoverHandler<InnerGridView, Entry, EntryParams> =
Handler<kind::Hover, InnerGridView, Entry, EntryParams>;
impl<InnerGridView, E: Entry> SelectionHandler<InnerGridView, E, E::Params>
where InnerGridView: AsRef<crate::GridView<E>>
{
/// Create selection highlight handler.
///
/// The returned handler will have `entry_highlighted` properly connected to the passed `grid`.
pub fn new_connected(app: &Application, grid: &InnerGridView) -> Self
where Self: HasConstructor<InnerGridView = InnerGridView> {
let this = Self::new(app, grid);
this.entry_highlighted.attach(&grid.as_ref().entry_selected);
this
}
}
impl<InnerGridView, E: Entry> HoverHandler<InnerGridView, E, E::Params>
where InnerGridView: AsRef<crate::GridView<E>>
{
/// Create hover highlight handler.
///
/// The returned handler will have `entry_highlighted` properly connected to the passed `grid`.
pub fn new_connected(app: &Application, grid: &InnerGridView) -> Self
where Self: HasConstructor<InnerGridView = InnerGridView> {
let this = Self::new(app, grid);
this.entry_highlighted.attach(&grid.as_ref().entry_hovered);
this
}
}
impl<Kind: shape::AttrSetter, InnerGridView, E: Entry> Handler<Kind, InnerGridView, E, E::Params>
where InnerGridView: AsRef<crate::GridView<E>>
{
/// Connects shape with the handler.
///
/// The shape will be updated (using the specified `shape::AttrSetter`) according to the
/// `position`, `contour`, `color` and `top_clip` outputs.
pub fn connect_with_shape(&self, shape: &shape::View) {
let network = self.frp.network();
let shape_hidden = (&self.frp.is_masked_layer_set).into();
self.model.connect_with_shape::<Kind>(network, shape, true, Some(&shape_hidden), None);
}
}
impl<Kind: shape::AttrSetter, E: Entry, HeaderEntry: Entry>
Handler<Kind, header::GridView<E, HeaderEntry>, E, E::Params>
{
/// Connects shape designed for highlighting headers with the handler.
///
/// The shape will be updated (using the specified `shape::AttrSetter`) according to the
/// `position`, `contour`, `color` and `top_clip` outputs, and it's bottom clip will be always
/// set to the current column's pushed-down header bottom (the `header_separator` value of
/// [`header::FRP`]).
pub fn connect_with_header_shape(&self, shape: &shape::View) {
let network = self.frp.network();
let shape_hidden = (&self.frp.is_masked_layer_set).into();
let bottom_clip = (&self.frp.header_separator).into();
self.model.connect_with_shape::<Kind>(
network,
shape,
true,
Some(&shape_hidden),
Some(&bottom_clip),
);
}
}
/// A trait implemented by every [`Handler`] able to be constructed.
pub trait HasConstructor {
/// The exact type of the _inner_ Grid View.
type InnerGridView;
/// Create new Handler.
///
/// This is a generic constructor: the exact entry endpoints handled by it should be specified
@ -327,88 +345,188 @@ impl<E: Entry> Handler<E, E::Model, E::Params> {
///
/// Use [`new_for_selection_connected`] or [`new_for_hover_connected`] to create a fully working
/// handler for specific highlight.
pub fn new(
app: &Application,
grid: &crate::GridView<E>,
entry_endpoints_getter: EntryEndpointsGetter<E>,
) -> Self {
fn new(app: &Application, grid: &Self::InnerGridView) -> Self;
}
impl<Kind: EndpointsGetter, E: Entry> HasConstructor
for Handler<Kind, crate::GridView<E>, E, E::Params>
{
type InnerGridView = crate::GridView<E>;
fn new(app: &Application, grid: &Self::InnerGridView) -> Self {
let frp = Frp::new();
let animations = Animations::new(&frp);
let out = &frp.private.output;
let model = Rc::new(Data::new(app, grid, out, animations, entry_endpoints_getter));
let network = frp.network();
let animations = Animations::new(&frp);
let entry_getter =
f!((r, c) grid.get_entry(r, c).map(|e| Kind::get_endpoints_from_entry(&e)));
let connected_entry_out = connected_entry::Output::new(network);
let connected_entry = ConnectedEntry::new(entry_getter, connected_entry_out.clone_ref());
let model = Rc::new(Data::new(app, grid, animations.clone_ref()));
let out = &frp.private.output;
let grid_frp = grid.frp();
frp::extend! {network
// === Updating `connected_entry` Field ===
shown_with_highlighted <-
grid_frp.entry_shown.map2(&frp.entry_highlighted, |a, b| (*a, *b));
highlighted_is_shown <-
shown_with_highlighted.map(|(sh, hlt)| hlt.contains(sh).and_option(*hlt));
should_reconnect <- any(frp.entry_highlighted, highlighted_is_shown);
eval should_reconnect ((loc) model.connect_new_highlighted_entry(*loc));
shown_with_highlighted.filter_map(|(sh, hlt)| hlt.contains(sh).and_option(Some(*hlt)));
should_reconnect_entry <- any(frp.entry_highlighted, highlighted_is_shown);
eval should_reconnect_entry ((loc) connected_entry.connect_new_highlighted_entry(*loc));
// === Highlight Position ===
became_highlighted <- frp.entry_highlighted.filter_map(|l| *l);
let column_widths = model.grid.model().column_widths.clone_ref();
out.position <+ became_highlighted.all_with(
&grid_frp.entries_size,
move |&(row, col), &es| entry_position(row, col, es, &column_widths)
position_after_highlight <- became_highlighted.map(
f!((&(row, col)) model.grid.entry_position(row, col))
);
none_highlightd <- frp.entry_highlighted.filter(|opt| opt.is_none()).constant(());
out.contour <+ none_highlightd.constant(default());
out.position <+ position_after_highlight;
prev_position <- out.position.previous();
new_jump <- position_after_highlight.map2(&prev_position, |pos, prev| prev - pos);
animations.position_jump.target <+ new_jump;
animations.position_jump.skip <+ new_jump.constant(());
animations.position_jump.target <+ new_jump.constant(Vector2::zero());
// === Color and Contour ===
out.contour <+ connected_entry_out.contour;
out.color <+ connected_entry_out.color;
none_highlighted <- frp.entry_highlighted.filter(|opt| opt.is_none()).constant(());
out.contour <+ none_highlighted.constant(default());
// === Setting up Masked Layer ===
out.entries_params <+ frp.set_entries_params;
let entries_params = out.entries_params.clone_ref();
out.is_masked_layer_set <+
frp.setup_masked_layer.map(f!((layer) model.setup_masked_layer(layer.as_ref())));
frp.setup_masked_layer.map(f!([model, entries_params](layer) model.setup_masked_layer(layer.as_ref(), (&entries_params).into())));
}
Self { frp, model }
}
/// Create selection highlight handler.
///
/// The returned handler will have `entry_highlighted` properly connected to the passed `grid`.
pub fn new_for_selection_connected(app: &Application, grid: &crate::GridView<E>) -> Self {
let this = Self::new(app, grid, Self::selection_endpoints_getter);
this.entry_highlighted.attach(&grid.entry_selected);
this
}
/// Create hover highlight handler.
///
/// The returned handler will have `entry_highlighted` properly connected to the passed `grid`.
pub fn new_for_hover_connected(app: &Application, grid: &crate::GridView<E>) -> Self {
let this = Self::new(app, grid, Self::hover_endpoints_getter);
this.entry_highlighted.attach(&grid.entry_hovered);
this
}
fn selection_endpoints_getter(entry: &E) -> EntryEndpoints {
let frp = entry.frp();
EntryEndpoints {
flag: frp.set_selected.clone_ref(),
location: frp.set_location.clone_ref().into(),
contour: frp.contour.clone_ref().into(),
color: frp.selection_highlight_color.clone_ref().into(),
}
}
fn hover_endpoints_getter(entry: &E) -> EntryEndpoints {
let frp = entry.frp();
EntryEndpoints {
flag: frp.set_hovered.clone_ref(),
location: frp.set_location.clone_ref().into(),
contour: frp.contour.clone_ref().into(),
color: frp.hover_highlight_color.clone_ref().into(),
}
}
/// Connects shape with the handler.
///
/// The shape will be updated (using the specified `shape::AttrSetter`) according to the
/// `position`, `contour` and `color` outputs.
pub fn connect_with_shape<Setter: shape::AttrSetter>(&self, shape: &shape::View) {
let network = self.frp.network();
let shape_hidden = (&self.frp.is_masked_layer_set).into();
self.model.connect_with_shape::<Setter>(network, shape, true, Some(&shape_hidden));
Self { frp, model, kind_type: default() }
}
}
impl<Kind: EndpointsGetter, E: Entry, HeaderEntry: Entry<Params = E::Params>> HasConstructor
for Handler<Kind, header::GridView<E, HeaderEntry>, E, E::Params>
{
type InnerGridView = header::GridView<E, HeaderEntry>;
fn new(app: &Application, grid: &Self::InnerGridView) -> Self {
let frp = Frp::new();
let network = frp.network();
let animations = Animations::new(&frp);
let entry_getter =
f!((r, c) grid.get_entry(r, c).map(|e| Kind::get_endpoints_from_entry(&e)));
let header_getter =
f!((r, c) grid.get_header(r, c).map(|e| Kind::get_endpoints_from_entry(&e)));
let connected_entry_out = connected_entry::Output::new(network);
let connected_header_out = connected_entry::Output::new(network);
let connected_entry = ConnectedEntry::new(entry_getter, connected_entry_out.clone_ref());
let connected_header = ConnectedEntry::new(header_getter, connected_header_out.clone_ref());
let model = Rc::new(Data::new(app, grid, animations.clone_ref()));
let out = &frp.private.output;
let grid_frp = grid.frp();
let headers_frp = grid.header_frp();
frp::extend! {network
// === Updating `connected_entry` Field ===
shown_with_highlighted <-
grid_frp.entry_shown.map2(&frp.entry_highlighted, |a, b| (*a, *b));
highlighted_is_shown <-
shown_with_highlighted.filter_map(|(sh, hlt)| hlt.contains(sh).and_option(Some(*hlt)));
should_reconnect_entry <- any(frp.entry_highlighted, highlighted_is_shown);
eval should_reconnect_entry ((loc) connected_entry.connect_new_highlighted_entry(*loc));
// === Updating `connected_header` Field ===
header_shown_with_highlighted <-
headers_frp.header_shown.map2(&frp.entry_highlighted, |a, b| (*a, *b));
highlighted_header_is_shown <-
header_shown_with_highlighted.filter_map(|(sh, hlt)| hlt.contains(sh).and_option(Some(*hlt)));
should_reconnect_header <- any(frp.entry_highlighted, highlighted_header_is_shown);
eval should_reconnect_header ((loc) connected_header.connect_new_highlighted_entry(*loc));
header_hidden_with_highlighted <-
headers_frp.header_hidden.map2(&frp.entry_highlighted, |a, b| (*a, *b));
should_disconnect_header <- header_hidden_with_highlighted.filter_map(|(hd, hlt)| hlt.contains(hd).and_option(*hlt));
eval_ should_disconnect_header (connected_header.drop_guard());
// === Highlight Position ===
became_highlighted <- frp.entry_highlighted.filter_map(|l| *l);
position_after_highlight <- became_highlighted.map(
f!((&(row, col)) model.grid.header_or_entry_position(row, col))
);
position_after_header_disconnect <- should_disconnect_header.map(
f!((&(row, col)) model.grid.header_or_entry_position(row, col))
);
highligthed_header_pos_change <- headers_frp.header_position_changed.map2(
&frp.entry_highlighted,
|&(row, col, pos), h| h.contains(&(row, col)).as_some(pos)
);
highligthed_header_pos_change <- highligthed_header_pos_change.filter_map(|p| *p);
out.position <+ position_after_highlight;
out.position <+ position_after_header_disconnect;
out.position <+ highligthed_header_pos_change;
prev_position <- out.position.previous();
new_jump <- position_after_highlight.map2(&prev_position, |pos, prev| prev - pos);
animations.position_jump.target <+ new_jump;
animations.position_jump.skip <+ new_jump.constant(());
animations.position_jump.target <+ new_jump.constant(Vector2(0.0, 0.0));
// === Contour and Color ===
let header_connected = &connected_header_out.is_entry_connected;
out.contour <+ all_with3(
header_connected,
&connected_entry_out.contour,
&connected_header_out.contour,
|&from_header, &entry, &header| if from_header {header} else {entry}
);
out.color <+ all_with3(
header_connected,
&connected_entry_out.color,
&connected_header_out.color,
|&from_header, &entry, &header| if from_header {header} else {entry}
);
none_highlighted <- frp.entry_highlighted.filter(|opt| opt.is_none()).constant(());
out.contour <+ none_highlighted.constant(default());
// === Setting up Masked Layer ===
out.entries_params <+ frp.set_entries_params;
let entries_params = out.entries_params.clone_ref();
out.is_masked_layer_set <+
frp.setup_masked_layer.map(f!([model, entries_params](layer) model.setup_masked_layer(layer.as_ref(), (&entries_params).into())));
// === Highlight Shape Clipping ===
highlight_column <- became_highlighted._1();
header_moved_in_highlight_column <- headers_frp.header_position_changed.map2(
&frp.entry_highlighted,
|&(_, col, _), h| h.map_or(false, |(_, h_col)| col == h_col)
).filter(|v| *v).constant(());
out.header_separator <+ all_with(&highlight_column, &header_moved_in_highlight_column, f!((col, ()) model.grid.header_separator(*col)));
new_top_clip <- all_with3(&out.header_separator, header_connected, &grid_frp.viewport, |&sep, &hc, v| if hc {v.top} else {sep});
out.top_clip <+ new_top_clip;
prev_top_clip <- out.top_clip.previous();
new_clip_jump <- new_top_clip.sample(&became_highlighted).map2(&prev_top_clip, |c, p| p - c);
animations.top_clip_jump.target <+ new_clip_jump;
animations.top_clip_jump.skip <+ new_clip_jump.constant(());
animations.top_clip_jump.target <+ new_clip_jump.constant(0.0);
}
Self { frp, model, kind_type: default() }
}
}

View File

@ -0,0 +1,219 @@
//! A module containing [`ConnectedEntry`] structure.
use crate::prelude::*;
use crate::entry;
use crate::selectable::highlight;
use crate::Col;
use crate::Entry;
use crate::Row;
use ensogl_core::data::color;
// ======================
// === EntryEndpoints ===
// ======================
/// A subset of [`entry::FRP`] endpoints used by the specific [kind of highlight](highlight::kind)
#[allow(missing_docs)]
#[derive(Clone, CloneRef, Debug)]
pub struct EntryEndpoints {
/// The "is_highlighted" input: may be `is_selected` or `is_hovered`.
pub flag: frp::Any<bool>,
pub location: frp::Stream<(Row, Col)>,
pub contour: frp::Stream<entry::Contour>,
pub color: frp::Stream<color::Rgba>,
}
// === EntryEndpointsGetter ===
/// A trait implemented by [all highlight kinds](highlight::kind), with method for extracting
/// endpoints related to this kind from any [`Entry`].
#[allow(missing_docs)]
pub trait EndpointsGetter {
fn get_endpoints_from_entry<E: Entry>(entry: &E) -> EntryEndpoints;
}
impl EndpointsGetter for highlight::kind::Selection {
fn get_endpoints_from_entry<E: Entry>(entry: &E) -> EntryEndpoints {
let frp = entry.frp();
EntryEndpoints {
flag: frp.set_selected.clone_ref(),
location: frp.set_location.clone_ref().into(),
contour: frp.highlight_contour.clone_ref().into(),
color: frp.selection_highlight_color.clone_ref().into(),
}
}
}
impl EndpointsGetter for highlight::kind::Hover {
fn get_endpoints_from_entry<E: Entry>(entry: &E) -> EntryEndpoints {
let frp = entry.frp();
EntryEndpoints {
flag: frp.set_hovered.clone_ref(),
location: frp.set_location.clone_ref().into(),
contour: frp.highlight_contour.clone_ref().into(),
color: frp.hover_highlight_color.clone_ref().into(),
}
}
}
// =============
// === Guard ===
// =============
/// A guard managing the connections between highlighted entry and the FRP [`Output`]s.
///
/// Until dropped, this structure keeps connected the entry endpoints declaring the highlight
/// appearance (`position`, `contour` and `color`) to the appropriate [`Output`]'s endpoints.
/// Also, the entry's flag (`is_selected` or `is_hovered`) will be set to `true` on construction and
/// set back to `false` on drop.
#[derive(Debug)]
struct Guard {
network: frp::Network,
/// An event emitted when we should drop this guard and try to create new with the same
/// location, for example when the Entry instance is re-used in another location.
should_be_dropped: frp::Stream,
dropped: frp::Source,
}
impl Guard {
/// Create guard for entry FRP at given location.
fn new_for_entry(
entry_frp: EntryEndpoints,
row: Row,
col: Col,
highlight_frp: &Output,
) -> Self {
let network = frp::Network::new("HighlightedEntryGuard");
frp::extend! { network
init <- source_();
dropped <- source_();
contour <- all(init, entry_frp.contour)._1();
color <- all(init, entry_frp.color)._1();
entry_frp.flag <+ init.constant(true);
entry_frp.flag <+ dropped.constant(false);
highlight_frp.contour <+ contour;
highlight_frp.color <+ color;
location_change <- entry_frp.location.filter(move |loc| *loc != (row, col));
should_be_dropped <- location_change.constant(());
}
init.emit(());
Self { network, dropped, should_be_dropped }
}
}
impl Drop for Guard {
fn drop(&mut self) {
self.dropped.emit(());
}
}
// ==============
// === Output ===
// ==============
/// An output information from the [`ConnectedEntry`] regarding e.g. highlight contour and shape.
#[allow(missing_docs)]
#[derive(CloneRef, Clone, Debug)]
pub struct Output {
pub is_entry_connected: frp::Source<bool>,
pub contour: frp::Any<entry::Contour>,
pub color: frp::Any<color::Rgba>,
}
impl Output {
/// Create Output endpoints in given `network`.
pub fn new(network: &frp::Network) -> Self {
frp::extend! { network
is_entry_connected <- source::<bool>();
contour <- any(...);
color <- any(...);
}
Self { is_entry_connected, contour, color }
}
}
// ======================
// === ConnectedEntry ===
// ======================
/// Get the Entry Endpoints for given row and column, or `None` if no entry is instantiated at
/// this location.
pub trait Getter = Fn(Row, Col) -> Option<EntryEndpoints> + 'static;
/// The structure containing information of the entry which is currently connected to the defined
/// set of [`Output`]s.
///
/// It is used in [`highlight::Handler`], which is responsible for updating this struct to the
/// actually highlighted entry using [`connect_new_highlighted_entry`] method. The handler should
/// also decide which connected entry output should be propagated to [its API](highlight::FRP)
/// (there may be more than one, for example the base entry and the pushed-down header in
/// [`selectable::GridViewWithHeaders`]).
#[derive(Derivative)]
#[derivative(Debug(bound = ""))]
pub struct ConnectedEntry<EntryGetter> {
#[derivative(Debug = "ignore")]
entry_getter: EntryGetter,
output: Output,
location: Cell<Option<(Row, Col)>>,
guard: RefCell<Option<Guard>>,
}
impl<EntryGetter> ConnectedEntry<EntryGetter> {
/// Create new structure. No entry will be connected.
pub fn new(entry_getter: EntryGetter, output: Output) -> Rc<Self> {
Rc::new(Self { entry_getter, output, location: default(), guard: default() })
}
/// Drop old [`ConnectedEntryGuard`] and create a new one for new highlighted entry.
pub fn connect_new_highlighted_entry(self: &Rc<Self>, location: Option<(Row, Col)>)
where EntryGetter: Getter {
if let Some((row, col)) = location {
let current_loc = self.location.get();
let loc_changed = current_loc.map_or(true, |loc| loc != (row, col));
if loc_changed {
self.guard.take();
let entry = (self.entry_getter)(row, col);
if let Some(entry) = entry {
*self.guard.borrow_mut() =
Some(Guard::new_for_entry(entry, row, col, &self.output));
self.location.set(Some((row, col)));
self.output.is_entry_connected.emit(true);
self.set_up_guard_dropping();
} else {
self.drop_guard();
}
}
} else {
self.drop_guard();
}
}
/// Drop old [`ConnectedEntryGuard`]
pub fn drop_guard(&self) {
self.location.set(None);
self.guard.take();
self.output.is_entry_connected.emit(false);
}
fn set_up_guard_dropping(self: &Rc<Self>)
where EntryGetter: 'static {
if let Some(guard) = &*self.guard.borrow() {
let network = &guard.network;
let this = self;
frp::extend! { network
eval_ guard.should_be_dropped (this.drop_guard());
}
}
}
}

View File

@ -3,10 +3,12 @@
use crate::prelude::*;
use crate::header;
use crate::selectable::highlight::shape;
use crate::Entry;
use ensogl_core::application::Application;
use ensogl_core::display;
use ensogl_core::display::scene::Layer;
@ -19,32 +21,41 @@ use ensogl_core::display::scene::Layer;
///
/// The handler can be created for some specific layer and _base_ grid view. It will add two
/// sub-layers:
/// * one with the new _inner_ [`crate::GridView`] component;
/// * one with the new _inner_ [Grid View component variant](crate);
/// * second being set up as a text layer of aforementioned grid view.
/// The _inner_ Grid View will be fully synchronized with the _base_ one except the entries'
/// parameters. The layers will be masked with highlight [`shape`], so only the highlighted fragment
/// of the _inner_ grid view is actually displayed.
///
/// See [`selection::GridView`] docs for usage example.
#[derive(CloneRef, Debug, Derivative)]
#[derivative(Clone(bound = ""))]
pub struct Handler<Entry: 'static, EntryModel: frp::node::Data, EntryParams: frp::node::Data> {
entries: Layer,
text: Layer,
mask: Layer,
#[derive(Clone, CloneRef, Debug)]
#[clone_ref(bound = "InnerGridView: CloneRef")]
pub struct Handler<InnerGridView> {
entries: Layer,
text: Layer,
mask: Layer,
header: Immutable<Option<Layer>>,
header_text: Immutable<Option<Layer>>,
/// The _inner_ grid view.
pub grid: crate::GridViewTemplate<Entry, EntryModel, EntryParams>,
pub grid: InnerGridView,
/// The shape being a mask for the sub-layers.
pub shape: shape::View,
pub shape: shape::View,
}
impl<E: Entry> Handler<E, E::Model, E::Params> {
/// Create new handler for given layer and _base_ [`GridView`](crate::GridView).
pub fn new(app: &Application, parent_layer: &Layer, base_grid: &crate::GridView<E>) -> Self {
let grid = crate::GridView::new(app);
impl<InnerGridView> Handler<InnerGridView> {
fn new_wrapping<E: Entry>(
parent_layer: &Layer,
grid: InnerGridView,
base_grid: &InnerGridView,
) -> Self
where
InnerGridView: AsRef<crate::GridView<E>> + display::Object,
{
let shape = shape::View::new(Logger::new("HighlightMask"));
let entries = parent_layer.create_sublayer();
let text = parent_layer.create_sublayer();
let header = default();
let header_text = default();
let mask = Layer::new_with_cam(
Logger::new("grid_view::HighlightLayers::mask"),
&parent_layer.camera(),
@ -52,7 +63,9 @@ impl<E: Entry> Handler<E, E::Model, E::Params> {
entries.set_mask(&mask);
text.set_mask(&mask);
entries.add_exclusive(&grid);
grid.set_text_layer(Some(text.downgrade()));
let grid_frp = grid.as_ref().frp();
let base_grid_frp = base_grid.as_ref().frp();
grid_frp.set_text_layer(Some(text.downgrade()));
mask.add_exclusive(&shape);
base_grid.add_child(&grid);
grid.add_child(&shape);
@ -60,24 +73,67 @@ impl<E: Entry> Handler<E, E::Model, E::Params> {
// The order of instructions below is very important! We need to initialize the viewport
// and entries size first, because its required to receive `model_for_entry` events after
// resizing grid properly.
let network = grid.network();
let network = grid_frp.network();
frp::extend! { network
init <- source_();
viewport <- all(init, base_grid.viewport)._1();
viewport <- all(init, base_grid_frp.viewport)._1();
eval viewport ([shape](&vp) shape::set_viewport(&shape, vp));
grid.set_viewport <+ viewport;
grid.set_entries_size <+ all(init, base_grid.entries_size)._1();
grid.resize_grid <+ all(init, base_grid.grid_size)._1();
grid.model_for_entry <+ base_grid.model_for_entry;
grid_frp.set_viewport <+ viewport;
grid_frp.set_entries_size <+ all(init, base_grid_frp.entries_size)._1();
grid_frp.resize_grid <+ all(init, base_grid_frp.grid_size)._1();
grid_frp.model_for_entry <+ base_grid_frp.model_for_entry;
different_entry_hovered <- grid.entry_hovered.map2(&base_grid.entry_hovered, |e1, e2| e1 != e2);
base_grid.hover_entry <+ grid.entry_hovered.gate(&different_entry_hovered);
base_grid.select_entry <+ grid.entry_selected;
base_grid.accept_entry <+ grid.entry_accepted;
different_entry_hovered <- grid_frp.entry_hovered.map2(&base_grid_frp.entry_hovered, |e1, e2| e1 != e2);
base_grid_frp.hover_entry <+ grid_frp.entry_hovered.gate(&different_entry_hovered);
base_grid_frp.select_entry <+ grid_frp.entry_selected;
base_grid_frp.accept_entry <+ grid_frp.entry_accepted;
}
init.emit(());
base_grid.request_model_for_visible_entries();
base_grid_frp.request_model_for_visible_entries();
Self { entries, text, mask, grid, shape }
Self { entries, text, header, header_text, mask, grid, shape }
}
}
/// A trait implemented by every [`Handler`] able to be constructed.
pub trait HasConstructor {
/// The exact type of the _inner_ Grid View.
type InnerGrid;
/// Create new handler for given layer and _base_ Grid View.
fn new(app: &Application, parent_layer: &Layer, base_grid: &Self::InnerGrid) -> Self;
}
impl<E: Entry> HasConstructor for Handler<crate::GridView<E>> {
type InnerGrid = crate::GridView<E>;
fn new(app: &Application, parent_layer: &Layer, base_grid: &Self::InnerGrid) -> Self {
let grid = crate::GridView::new(app);
Self::new_wrapping(parent_layer, grid, base_grid)
}
}
impl<E: Entry, HeaderEntry: Entry<Params = E::Params>> HasConstructor
for Handler<header::GridView<E, HeaderEntry>>
{
type InnerGrid = header::GridView<E, HeaderEntry>;
fn new(app: &Application, parent_layer: &Layer, base_grid: &Self::InnerGrid) -> Self {
let grid = header::GridView::new(app);
let mut this = Self::new_wrapping(parent_layer, grid, base_grid);
let header_frp = this.grid.header_frp();
let base_header_frp = base_grid.header_frp();
frp::extend! { network
header_frp.section_info <+ base_header_frp.section_info;
}
let header = parent_layer.create_sublayer();
let header_text = parent_layer.create_sublayer();
header_frp.set_layers(header::WeakLayers::new(&header, Some(&header_text)));
header.set_mask(&this.mask);
header_text.set_mask(&this.mask);
this.header = Immutable(Some(header));
this.header_text = Immutable(Some(header_text));
this
}
}

View File

@ -19,6 +19,8 @@
use crate::prelude::*;
use crate::selectable::highlight::kind;
use ensogl_core::data::color;
use ensogl_scroll_area::Viewport;
@ -40,17 +42,26 @@ ensogl_core::define_shape_system! {
highlights_sizes: Vector4,
hover_color: Vector4,
selection_color: Vector4,
highlights_y_clip: Vector4,
) {
let viewport_width = Var::<Pixels>::from("input_size.x");
let viewport_height = Var::<Pixels>::from("input_size.y");
let viewport = Rect((viewport_width, viewport_height));
let viewport = Rect((&viewport_width, &viewport_height));
let viewport = viewport.corners_radius(corners_radii.x().px());
let hover_clip_height = &viewport_height - highlights_y_clip.x().px() - highlights_y_clip.y().px();
let hover_clip_y_pos = (highlights_y_clip.y().px() - highlights_y_clip.x().px()) / 2.0;
let hover_clip = Rect((&viewport_width, hover_clip_height));
let hover_clip = hover_clip.translate((0.0.px(), hover_clip_y_pos));
let hover = Rect(highlights_sizes.xy().px()).corners_radius(corners_radii.y().px());
let hover = hover.translate(highlights_pos.xy().px());
let hover = (&hover * &viewport).fill(hover_color);
let hover = (&hover * &viewport * &hover_clip).fill(hover_color);
let selection_clip_height = viewport_height - highlights_y_clip.z().px() - highlights_y_clip.w().px();
let selection_clip_y_pos = (highlights_y_clip.w().px() - highlights_y_clip.z().px()) / 2.0;
let selection_clip = Rect((viewport_width, selection_clip_height));
let selection_clip = selection_clip.translate((0.0.px(), selection_clip_y_pos));
let selection = Rect(highlights_sizes.zw().px()).corners_radius(corners_radii.z().px());
let selection = selection.translate(highlights_pos.zw().px());
let selection = (&selection * &viewport).fill(selection_color);
let selection = (&selection * &viewport * &selection_clip).fill(selection_color);
let highlights = &hover + &selection;
highlights.into()
}
@ -81,16 +92,11 @@ pub trait AttrSetter {
fn set_size(shape: &View, size: Vector2);
fn set_corners_radius(shape: &View, radius: f32);
fn set_color(shape: &View, color: color::Rgba);
fn set_top_clip(shape: &View, y: f32, viewport: Viewport);
fn set_bottom_clip(shape: &View, y: f32, viewport: Viewport);
}
// === HoverAttrSetter ===
/// Struct with setters for all attributes for hover highlight.
#[derive(Copy, Clone, Debug)]
pub struct HoverAttrSetter;
impl AttrSetter for HoverAttrSetter {
impl AttrSetter for kind::Hover {
fn set_position(shape: &View, position: Vector2, viewport: Viewport) {
let viewport_position = viewport.center_point();
let relative_pos = position - viewport_position;
@ -108,24 +114,29 @@ impl AttrSetter for HoverAttrSetter {
}
fn set_corners_radius(shape: &View, radius: f32) {
let mut old_radii = shape.corners_radii.get();
old_radii.y = radius;
shape.corners_radii.set(old_radii);
let mut attr = shape.corners_radii.get();
attr.y = radius;
shape.corners_radii.set(attr);
}
fn set_color(shape: &View, color: color::Rgba) {
shape.hover_color.set(color.into())
}
fn set_top_clip(shape: &View, y: f32, viewport: Viewport) {
let mut attr = shape.highlights_y_clip.get();
attr.x = viewport.top - y;
shape.highlights_y_clip.set(attr);
}
fn set_bottom_clip(shape: &View, y: f32, viewport: Viewport) {
let mut attr = shape.highlights_y_clip.get();
attr.y = y - viewport.bottom;
shape.highlights_y_clip.set(attr);
}
}
// === SelectionAttrSetter ===
/// Struct with setters for all attributes for selection highlight.
#[derive(Copy, Clone, Debug)]
pub struct SelectionAttrSetter;
impl AttrSetter for SelectionAttrSetter {
impl AttrSetter for kind::Selection {
fn set_position(shape: &View, position: Vector2, viewport: Viewport) {
let viewport_position = viewport.center_point();
let relative_pos = position - viewport_position;
@ -143,12 +154,24 @@ impl AttrSetter for SelectionAttrSetter {
}
fn set_corners_radius(shape: &View, radius: f32) {
let mut old_radii = shape.corners_radii.get();
old_radii.z = radius;
shape.corners_radii.set(old_radii);
let mut attr = shape.corners_radii.get();
attr.z = radius;
shape.corners_radii.set(attr);
}
fn set_color(shape: &View, color: color::Rgba) {
shape.selection_color.set(color.into())
}
fn set_top_clip(shape: &View, y: f32, viewport: Viewport) {
let mut attr = shape.highlights_y_clip.get();
attr.z = viewport.top - y;
shape.highlights_y_clip.set(attr);
}
fn set_bottom_clip(shape: &View, y: f32, viewport: Viewport) {
let mut attr = shape.highlights_y_clip.get();
attr.w = y - viewport.bottom;
shape.highlights_y_clip.set(attr);
}
}

View File

@ -173,6 +173,7 @@ impl crate::Entry for Entry {
out.override_column_width <+ input.set_model.filter_map(|m| *m.override_width);
out.contour <+ contour;
out.highlight_contour <+ contour;
out.disabled <+ disabled;
out.hover_highlight_color <+ hover_color;
out.selection_highlight_color <+ selection_color;
@ -209,3 +210,8 @@ pub type SimpleSelectableGridView = selectable::GridView<Entry>;
/// The Simple version of scrollable and selectable Grid View, where each entry is just a label with
/// background.
pub type SimpleScrollableSelectableGridView = scrollable::SelectableGridView<Entry>;
/// The Simple version of scrollable and selectable Grid View with headers, where each header or
/// entry is just a label with background.
pub type SimpleScrollableSelectableGridViewWithHeaders =
scrollable::SelectableGridViewWithHeaders<Entry, Entry>;

View File

@ -13,13 +13,13 @@ use crate::Viewport;
// === Ranges of Rows and Columns Visible ===
// ==========================================
fn has_size(v: &Viewport) -> bool {
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<Row> {
pub fn visible_rows(v: Viewport, entry_size: Vector2, row_count: usize) -> Range<Row> {
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) {
@ -33,7 +33,7 @@ pub fn visible_rows(v: &Viewport, entry_size: Vector2, row_count: usize) -> Rang
/// Return range of visible columns.
pub fn visible_columns(
v: &Viewport,
v: Viewport,
entry_size: Vector2,
col_count: usize,
column_widths: &ColumnWidths,
@ -94,7 +94,7 @@ pub fn visible_columns(
/// Return iterator over all visible locations (row-column pairs).
pub fn all_visible_locations(
v: &Viewport,
v: Viewport,
entry_size: Vector2,
row_count: usize,
col_count: usize,
@ -144,12 +144,12 @@ mod tests {
fn run(&self) {
assert_eq!(
visible_rows(&self.viewport, ENTRY_SIZE, ROW_COUNT),
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.column_widths),
visible_columns(self.viewport, ENTRY_SIZE, COL_COUNT, &self.column_widths),
self.expected_cols,
"Wrong visible cols in {self:?}"
);
@ -201,7 +201,7 @@ mod tests {
fn run(self) {
assert_eq!(
visible_columns(&self.viewport, ENTRY_SIZE, COL_COUNT, &self.column_widths),
visible_columns(self.viewport, ENTRY_SIZE, COL_COUNT, &self.column_widths),
self.expected_cols,
"Wrong visible cols in {self:?}"
);

View File

@ -279,8 +279,8 @@ impl<T: Scalar> Dim3 for Var<Vector4<T>> {
impl<T: Scalar> Dim4 for Var<Vector4<T>> {
fn w(&self) -> Var<T> {
match self {
Self::Static(t) => Var::Static(t.z.clone()),
Self::Dynamic(t) => Var::Dynamic(format!("{}.z", t).into()),
Self::Static(t) => Var::Static(t.w.clone()),
Self::Dynamic(t) => Var::Dynamic(format!("{}.w", t).into()),
}
}
}

View File

@ -30,6 +30,8 @@ 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_grid_view::Col;
use ensogl_grid_view::Row;
use ensogl_hardcoded_theme as theme;
use ensogl_text_msdf_sys::run_once_initialized;
@ -52,19 +54,31 @@ pub fn main() {
});
}
fn entry_model(row: Row, col: Col) -> grid_view::simple::EntryModel {
grid_view::simple::EntryModel {
text: format!("Entry ({row}, {col})").into(),
disabled: Immutable(row == col),
override_width: Immutable(if col == 1 && row == 5 { Some(180.0) } else { None }),
}
}
fn setup_grid_view(app: &Application) -> grid_view::simple::SimpleScrollableSelectableGridView {
let view = grid_view::simple::SimpleScrollableSelectableGridView::new(app);
fn setup_grid_view(
app: &Application,
) -> grid_view::simple::SimpleScrollableSelectableGridViewWithHeaders {
let view = grid_view::simple::SimpleScrollableSelectableGridViewWithHeaders::new(app);
let header_frp = view.header_frp();
frp::new_network! { network
requested_entry <- view.model_for_entry_needed.map(|(row, col)| {
let model = grid_view::simple::EntryModel {
text: format!("Entry ({row}, {col})").into(),
disabled: Immutable(row == col),
override_width: Immutable(if *col == 1 && *row == 5 { Some(180.0) } else { None }),
};
(*row, *col, model)
requested_entry <-
view.model_for_entry_needed.map(|&(row, col)| (row, col, entry_model(row, col)));
requested_section <- header_frp.section_info_needed.map(|&(row, col)| {
let sections_size = 2 + col;
let section_start = row - (row % sections_size);
let section_end = section_start + sections_size;
let model = entry_model(section_start, col);
(section_start..section_end, col, model)
});
view.model_for_entry <+ requested_entry;
header_frp.section_info <+ requested_section;
entry_hovered <- view.entry_hovered.filter_map(|l| *l);
entry_selected <- view.entry_selected.filter_map(|l| *l);
eval entry_hovered ([]((row, col)) tracing::debug!("Hovered entry ({row}, {col})."));
@ -75,6 +89,8 @@ fn setup_grid_view(app: &Application) -> grid_view::simple::SimpleScrollableSele
let params = grid_view::simple::EntryParams {
bg_color: color::Rgba(0.8, 0.8, 0.9, 1.0),
bg_margin: 1.0,
hover_color: color::Rgba(0.0, 1.0, 0.0, 1.0),
selection_color: color::Rgba(1.0, 0.0, 0.0, 1.0),
..default()
};
view.set_entries_params(params);