mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 17:11:31 +03:00
Grid View Headers (#3656)
This commit is contained in:
parent
d87a32d019
commit
d4142cfee3
@ -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
|
||||
|
@ -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)?;
|
||||
|
@ -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),
|
||||
|
164
lib/rust/ensogl/component/grid-view/src/entry/visible.rs
Normal file
164
lib/rust/ensogl/component/grid-view/src/entry/visible.rs
Normal 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));
|
||||
}
|
710
lib/rust/ensogl/component/grid-view/src/header.rs
Normal file
710
lib/rust/ensogl/component/grid-view/src/header.rs
Normal 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 (§ion_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)]);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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(¬_visible).constant(());
|
||||
position.skip <+ frp.position.gate(¬_visible).constant(());
|
||||
position_jump.skip <+ frp.position.gate(¬_visible).constant(());
|
||||
color.skip <+ frp.color.gate(¬_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() }
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
|
@ -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:?}"
|
||||
);
|
||||
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user