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