mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 03:51:43 +03:00
Component List Panel View (#3495)
This commit is contained in:
parent
7a2d304fa0
commit
655793aa78
47
Cargo.lock
generated
47
Cargo.lock
generated
@ -1493,6 +1493,22 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debug-scene-component-list-panel-view"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"enso-frp",
|
||||
"enso-profiler",
|
||||
"ensogl-core",
|
||||
"ensogl-hardcoded-theme",
|
||||
"ensogl-list-view",
|
||||
"ensogl-selector",
|
||||
"ensogl-text-msdf-sys",
|
||||
"ide-view-component-group",
|
||||
"searcher-list-panel",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debug-scene-icons"
|
||||
version = "0.1.0"
|
||||
@ -1863,6 +1879,7 @@ name = "enso-debug-scene"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"debug-scene-component-group",
|
||||
"debug-scene-component-list-panel-view",
|
||||
"debug-scene-icons",
|
||||
"debug-scene-interface",
|
||||
"debug-scene-visualization",
|
||||
@ -3560,7 +3577,7 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5617e92fc2f2501c3e2bc6ce547cad841adba2bae5b921c7e52510beca6d084c"
|
||||
dependencies = [
|
||||
"base64 0.11.0",
|
||||
"base64 0.13.0",
|
||||
"bytes 1.1.0",
|
||||
"http",
|
||||
"httpdate 1.0.2",
|
||||
@ -3690,6 +3707,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"enso-frp",
|
||||
"ensogl",
|
||||
"ensogl-core",
|
||||
"ensogl-gui-component",
|
||||
"ensogl-hardcoded-theme",
|
||||
"ensogl-label",
|
||||
@ -4638,6 +4656,15 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96bcbab4bfea7a59c2c0fe47211a1ac4e3e96bea6eb446d704f310bc5c732ae2"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.1.0"
|
||||
@ -5653,6 +5680,24 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "searcher-list-panel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"approx 0.5.1",
|
||||
"enso-frp",
|
||||
"ensogl-core",
|
||||
"ensogl-gui-component",
|
||||
"ensogl-hardcoded-theme",
|
||||
"ensogl-list-view",
|
||||
"ensogl-scroll-area",
|
||||
"ensogl-selector",
|
||||
"ensogl-shadow",
|
||||
"ensogl-text",
|
||||
"ide-view-component-group",
|
||||
"ordered-float 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secrecy"
|
||||
version = "0.8.0"
|
||||
|
@ -9,3 +9,4 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
ide-view-component-group = { path = "component-group" }
|
||||
ensogl-text = { path = "../../../../lib/rust/ensogl/component/text" }
|
||||
|
@ -10,6 +10,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
enso-frp = { version = "0.1.0", path = "../../../../../lib/rust/frp" }
|
||||
ensogl = { version = "0.1.0", path = "../../../../../lib/rust/ensogl" }
|
||||
ensogl-core = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/core" }
|
||||
ensogl-gui-component = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/gui" }
|
||||
ensogl-shadow = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/shadow" }
|
||||
ensogl-hardcoded-theme = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }
|
||||
|
@ -545,9 +545,9 @@ impl component::Frp<Model> for Frp {
|
||||
/// module-level documentation to learn more.
|
||||
///
|
||||
/// A component group consists of several shapes with a strict rendering order. The order of the
|
||||
/// fields in [`LayersInner`] struct represent the rendering order of layers, with `background`
|
||||
/// being the bottom-most and `header_text` being the top-most.
|
||||
/// fields in [`LayersInner`] struct represent the rendering order of layers.
|
||||
#[derive(Debug, Clone, CloneRef)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Layers {
|
||||
normal: LayersInner,
|
||||
selection: LayersInner,
|
||||
|
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "searcher-list-panel"
|
||||
version = "0.1.0"
|
||||
authors = ["Enso Team <contact@enso.org>"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
enso-frp = { path = "../../../../../lib/rust/frp" }
|
||||
ensogl-core = { path = "../../../../../lib/rust/ensogl/core" }
|
||||
ensogl-gui-component = { path = "../../../../../lib/rust/ensogl/component/gui/" }
|
||||
ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }
|
||||
ensogl-list-view = { path = "../../../../../lib/rust/ensogl/component/list-view" }
|
||||
ensogl-scroll-area = { path = "../../../../../lib/rust/ensogl/component/scroll-area" }
|
||||
ensogl-selector = { path = "../../../../../lib/rust/ensogl/component/selector" }
|
||||
ensogl-shadow = { path = "../../../../../lib/rust/ensogl/component/shadow" }
|
||||
ensogl-text = { path = "../../../../../lib/rust/ensogl/component/text" }
|
||||
ide-view-component-group = { path = "../component-group" }
|
||||
ordered-float = "3.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
@ -0,0 +1,239 @@
|
||||
//! Wrapper around multiple [`component_group::View`] that provides a layout where the
|
||||
//! [`component_group::View`] are stacked in three columns. Designed for use in the sections of a
|
||||
//! Component Browser Panel.
|
||||
|
||||
use ensogl_core::display::shape::*;
|
||||
use ensogl_core::prelude::*;
|
||||
|
||||
use crate::Layers;
|
||||
use enso_frp as frp;
|
||||
use ensogl_core::application::frp::API;
|
||||
use ensogl_core::application::Application;
|
||||
use ensogl_core::data::color;
|
||||
use ensogl_core::define_endpoints_2;
|
||||
use ensogl_core::display;
|
||||
use ensogl_core::display::style;
|
||||
use ensogl_gui_component::component;
|
||||
use ensogl_list_view as list_view;
|
||||
use ide_view_component_group as component_group;
|
||||
use ordered_float::OrderedFloat;
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Model ===
|
||||
// =============
|
||||
|
||||
/// Contains a [`AnyModelProvider`] with a label. Can be used to populate a
|
||||
/// [`component_group::View`].
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LabeledAnyModelProvider {
|
||||
/// Label of the data provided to be used as a header of the list.
|
||||
pub label: String,
|
||||
/// Content to be used to populate a list.
|
||||
pub content: list_view::entry::AnyModelProvider<component_group::Entry>,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Model ===
|
||||
// =============
|
||||
|
||||
/// The Model of the [`ColumnGrid`] component.
|
||||
#[derive(Clone, Debug, CloneRef)]
|
||||
pub struct Model {
|
||||
app: Application,
|
||||
display_object: display::object::Instance,
|
||||
content: Rc<RefCell<Vec<component_group::View>>>,
|
||||
size: Rc<Cell<Vector2>>,
|
||||
layers: Rc<RefCell<Option<Layers>>>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn new(app: &Application) -> Self {
|
||||
let logger = Logger::new("ColumnGrid");
|
||||
let app = app.clone_ref();
|
||||
let display_object = display::object::Instance::new(&logger);
|
||||
Self { app, display_object, content: default(), size: default(), layers: default() }
|
||||
}
|
||||
|
||||
fn update_content_layout(&self, content: &[LabeledAnyModelProvider], style: &Style) -> Vector2 {
|
||||
const NUMBER_OF_COLUMNS: usize = 3;
|
||||
let overall_width = style.content_width - 2.0 * style.content_padding;
|
||||
let column_width = (overall_width - 2.0 * style.column_gap) / NUMBER_OF_COLUMNS as f32;
|
||||
let content = content
|
||||
.iter()
|
||||
.map(|LabeledAnyModelProvider { content, label }| {
|
||||
let view = self.app.new_view::<component_group::View>();
|
||||
if let Some(layers) = self.layers.borrow().as_ref() {
|
||||
view.model().set_layers(&layers.groups);
|
||||
} else {
|
||||
tracing::log::warn!("Created ColumnGrid entry without layers.");
|
||||
}
|
||||
view.set_width(column_width);
|
||||
view.set_entries(content);
|
||||
view.set_header(label.as_str());
|
||||
self.display_object.add_child(&view);
|
||||
view
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let mut columns = vec![vec![]; NUMBER_OF_COLUMNS];
|
||||
// We need to subtract one `column_gap` as we only need (n-1) gaps, but through iteration
|
||||
// below we add one gap per item. So we initialise the heights with `-column_gap`.
|
||||
let mut heights = [-style.column_gap; NUMBER_OF_COLUMNS];
|
||||
|
||||
for (ix, entry) in content.iter().enumerate() {
|
||||
let column_index = ix % NUMBER_OF_COLUMNS;
|
||||
columns[column_index].push(entry);
|
||||
heights[column_index] += entry.size.value().y + style.column_gap;
|
||||
}
|
||||
let height: f32 = heights.into_iter().map(OrderedFloat).max().unwrap_or_default().into();
|
||||
|
||||
let mut entry_ix = 0;
|
||||
for (ix, column) in columns.iter().enumerate() {
|
||||
// The +0.5 required as a way to center the columns in the x direction by shifting by an
|
||||
// additional half-width.
|
||||
let pos_x = (column_width + style.column_gap) * (ix as f32 + 0.5);
|
||||
let mut pos_y = -height;
|
||||
for entry in column {
|
||||
let entry_height = entry.size.value().y;
|
||||
entry.set_position_y(pos_y + entry_height / 2.0);
|
||||
entry.set_position_x(pos_x);
|
||||
entry.set_color(style.get_entry_color_for_index(entry_ix));
|
||||
|
||||
entry_ix += 1;
|
||||
pos_y += entry_height;
|
||||
pos_y += style.column_gap;
|
||||
}
|
||||
}
|
||||
|
||||
*self.content.borrow_mut() = content;
|
||||
let height: f32 = heights.into_iter().map(OrderedFloat).max().unwrap_or_default().into();
|
||||
let width = self.size.get().x;
|
||||
self.size.set(Vector2::new(width, height));
|
||||
self.size.get()
|
||||
}
|
||||
|
||||
/// Assign a set of layers to render the component group in. Must be called after constructing
|
||||
/// the [`View`].
|
||||
pub(crate) fn set_layers(&self, layers: &Layers) {
|
||||
self.content.borrow().iter().for_each(|entry| entry.model().set_layers(&layers.groups));
|
||||
layers.scroll_layer.add_exclusive(&self.display_object);
|
||||
self.layers.set(layers.clone_ref());
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for Model {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.display_object
|
||||
}
|
||||
}
|
||||
|
||||
impl component::Model for Model {
|
||||
fn label() -> &'static str {
|
||||
"ColumnGrid"
|
||||
}
|
||||
|
||||
fn new(app: &Application, _logger: &DefaultWarningLogger) -> Self {
|
||||
Self::new(app)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===========
|
||||
// === FRP ===
|
||||
// ===========
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Style {
|
||||
column_gap: f32,
|
||||
entry_colors: [color::Rgba; 4],
|
||||
content_width: f32,
|
||||
content_padding: f32,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Choose a color from the `entry_colors` based on the index of the entry within the
|
||||
/// [`ColumnGrid`].
|
||||
fn get_entry_color_for_index(&self, ix: usize) -> color::Rgba {
|
||||
self.entry_colors[ix % self.entry_colors.len()]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
define_endpoints_2! {
|
||||
Input{
|
||||
set_content(Vec<LabeledAnyModelProvider>),
|
||||
}
|
||||
Output{
|
||||
size(Vector2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn get_layout(
|
||||
network: &enso_frp::Network,
|
||||
style: &StyleWatchFrp,
|
||||
) -> (enso_frp::Stream<Style>, enso_frp::stream::WeakNode<enso_frp::SourceData>) {
|
||||
let searcher_theme_path: style::Path =
|
||||
ensogl_hardcoded_theme::application::component_browser::searcher::HERE.into();
|
||||
let theme_path: style::Path =
|
||||
searcher_theme_path.sub("list_panel").sub("section").sub("column_grid");
|
||||
let column_gap = style.get_number(theme_path.sub("column_gap"));
|
||||
let entry_color_0 = style.get_color(theme_path.sub("entry_color_0"));
|
||||
let entry_color_1 = style.get_color(theme_path.sub("entry_color_1"));
|
||||
let entry_color_2 = style.get_color(theme_path.sub("entry_color_2"));
|
||||
let entry_color_3 = style.get_color(theme_path.sub("entry_color_3"));
|
||||
|
||||
let theme_path: style::Path = searcher_theme_path.sub("list_panel");
|
||||
let content_padding = style.get_number(theme_path.sub("content_padding"));
|
||||
let content_width = style.get_number(theme_path.sub("content_width"));
|
||||
|
||||
frp::extend! { network
|
||||
init <- source_();
|
||||
|
||||
entry_colors <- all5(&init, &entry_color_0,&entry_color_1,&entry_color_2,&entry_color_3);
|
||||
entry_colors <- entry_colors.map(|(_,c1,c2,c3,c4)| [*c1,*c2,*c3,*c4]);
|
||||
|
||||
layout_update <- all5(&init, &column_gap, &entry_colors, &content_padding, &content_width);
|
||||
layout_update <- layout_update.map(|(_, column_gap,entry_colors,content_padding,content_width)|{
|
||||
Style{
|
||||
column_gap:*column_gap,
|
||||
entry_colors:*entry_colors,
|
||||
content_padding:*content_padding,
|
||||
content_width:*content_width
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
(layout_update, init)
|
||||
}
|
||||
|
||||
impl component::Frp<Model> for Frp {
|
||||
fn init(
|
||||
frp_api: &<Self as API>::Private,
|
||||
_app: &Application,
|
||||
model: &Model,
|
||||
style: &StyleWatchFrp,
|
||||
) {
|
||||
let network = &frp_api.network;
|
||||
let (layout_update, init) = get_layout(network, style);
|
||||
|
||||
frp::extend! { network
|
||||
content_update <- all(&frp_api.input.set_content,&layout_update);
|
||||
size_update <- content_update.map(f!(((content,layout))
|
||||
model.update_content_layout(content,layout))
|
||||
);
|
||||
frp_api.output.size <+ size_update;
|
||||
}
|
||||
init.emit(());
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around multiple [`component_group::View`] that provides a layout where the
|
||||
/// `[component_group::View`] are stacked in three columns. Designed for use in the sections of a
|
||||
/// Component Browser Panel.
|
||||
pub type ColumnGrid = component::ComponentView<Model, Frp>;
|
688
app/gui/view/component-browser/searcher-list-panel/src/lib.rs
Normal file
688
app/gui/view/component-browser/searcher-list-panel/src/lib.rs
Normal file
@ -0,0 +1,688 @@
|
||||
//! This module defines the [`ComponentBrowserPanel`], sub-content of the Component Browser, that
|
||||
//! shows the available components grouped by categories. It also defines the shape that the
|
||||
//! Component Browser Menu will be placed on, as this will appear as a single continuous shape.
|
||||
//!
|
||||
//! To learn more about the Component Browser and its components, see the [Component Browser Design
|
||||
//! Document](https://github.com/enso-org/design/blob/e6cffec2dd6d16688164f04a4ef0d9dff998c3e7/epics/component-browser/design.md).
|
||||
|
||||
#![recursion_limit = "512"]
|
||||
// === Features ===
|
||||
#![allow(incomplete_features)]
|
||||
#![feature(negative_impls)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(bool_to_option)]
|
||||
#![feature(cell_update)]
|
||||
#![feature(const_type_id)]
|
||||
#![feature(drain_filter)]
|
||||
#![feature(entry_insert)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(marker_trait_attr)]
|
||||
#![feature(specialization)]
|
||||
#![feature(trait_alias)]
|
||||
#![feature(type_alias_impl_trait)]
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(trace_macros)]
|
||||
#![feature(const_trait_impl)]
|
||||
#![feature(slice_as_chunks)]
|
||||
// === Standard Linter Configuration ===
|
||||
#![deny(non_ascii_idents)]
|
||||
#![warn(unsafe_code)]
|
||||
// === Non-Standard Linter Configuration ===
|
||||
#![allow(clippy::option_map_unit_fn)]
|
||||
#![allow(clippy::precedence)]
|
||||
#![allow(dead_code)]
|
||||
#![deny(unconditional_recursion)]
|
||||
#![warn(missing_copy_implementations)]
|
||||
#![warn(missing_debug_implementations)]
|
||||
#![warn(missing_docs)]
|
||||
#![warn(trivial_casts)]
|
||||
#![warn(trivial_numeric_casts)]
|
||||
#![warn(unused_import_braces)]
|
||||
#![warn(unused_qualifications)]
|
||||
|
||||
|
||||
pub mod column_grid;
|
||||
|
||||
use ensogl_core::display::shape::*;
|
||||
use ensogl_core::prelude::*;
|
||||
|
||||
pub use column_grid::LabeledAnyModelProvider;
|
||||
use enso_frp as frp;
|
||||
use ensogl_core::application::frp::API;
|
||||
use ensogl_core::application::Application;
|
||||
use ensogl_core::data::color;
|
||||
use ensogl_core::define_endpoints_2;
|
||||
use ensogl_core::display;
|
||||
use ensogl_core::display::scene::Layer;
|
||||
use ensogl_core::display::shape::StyleWatchFrp;
|
||||
use ensogl_core::display::style;
|
||||
use ensogl_gui_component::component;
|
||||
use ensogl_hardcoded_theme::application::component_browser::searcher as searcher_theme;
|
||||
use ensogl_list_view as list_view;
|
||||
use ensogl_scroll_area::ScrollArea;
|
||||
use ensogl_shadow as shadow;
|
||||
use ensogl_text as text;
|
||||
use ide_view_component_group as component_group;
|
||||
use ide_view_component_group::Layers as GroupLayers;
|
||||
use searcher_theme::list_panel as list_panel_theme;
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Layers ===
|
||||
// ==============
|
||||
|
||||
#[derive(Debug, Clone, CloneRef)]
|
||||
struct Layers {
|
||||
groups: GroupLayers,
|
||||
base: Layer,
|
||||
selection: Layer,
|
||||
selection_mask: Layer,
|
||||
scroll_layer: Layer,
|
||||
}
|
||||
|
||||
impl Layers {
|
||||
fn new(app: &Application, scroll_area: &ScrollArea) -> Self {
|
||||
let main_camera = app.display.default_scene.layers.main.camera();
|
||||
let base = Layer::new_with_cam(app.logger.sub("component_groups"), &main_camera);
|
||||
let selection = Layer::new_with_cam(app.logger.sub("selection"), &main_camera);
|
||||
let selection_mask = Layer::new_with_cam(app.logger.sub("selection_mask"), &main_camera);
|
||||
selection.set_mask(&selection_mask);
|
||||
app.display.default_scene.layers.main.add_sublayer(&base);
|
||||
app.display.default_scene.layers.main.add_sublayer(&selection);
|
||||
let content = &scroll_area.content_layer();
|
||||
let groups = GroupLayers::new(&app.logger, content, &selection);
|
||||
let scroll_layer = scroll_area.content_layer().clone_ref();
|
||||
Self { base, selection, groups, selection_mask, scroll_layer }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Shapes ===
|
||||
// ==============
|
||||
|
||||
// === Layout Constants ===
|
||||
|
||||
/// Extra space around shape to allow for shadows.
|
||||
const SHADOW_PADDING: f32 = 25.0;
|
||||
|
||||
const FAVOURITES_SECTION_HEADING_LABEL: &str = "Favorite Data Science Tools";
|
||||
const LOCAL_SCOPE_SECTION_HEADING_LABEL: &str = "Local Scope";
|
||||
const SUB_MODULES_SECTION_HEADING_LABEL: &str = "Sub Modules";
|
||||
|
||||
const INFINITE: f32 = 999999.0;
|
||||
|
||||
|
||||
// === Style ===
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Style {
|
||||
content: ContentStyle,
|
||||
section: SectionStyle,
|
||||
menu: MenuStyle,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
fn size_inner(&self) -> Vector2 {
|
||||
let width = self.content.size.x;
|
||||
let height = self.content.size.y + self.menu.height;
|
||||
Vector2::new(width, height)
|
||||
}
|
||||
|
||||
fn size(&self) -> Vector2 {
|
||||
self.size_inner().map(|value| value + 2.0 * SHADOW_PADDING)
|
||||
}
|
||||
|
||||
fn menu_divider_y_pos(&self) -> f32 {
|
||||
self.size_inner().y / 2.0 - self.menu.height
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct ContentStyle {
|
||||
/// Size of the Component List Panel content area.
|
||||
size: Vector2,
|
||||
/// Radius of the rounded corners.
|
||||
corner_radius: f32,
|
||||
/// Extra space between scroll area and backgound shape edge.
|
||||
padding: f32,
|
||||
/// Color used for the panel background.
|
||||
background_color: color::Rgba,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct SectionHeadingStyle {
|
||||
/// Font size used for section headers.
|
||||
size: text::style::Size,
|
||||
/// Font used for section headers.
|
||||
font: String,
|
||||
/// Color used for section headers.
|
||||
color: color::Rgba,
|
||||
/// Distance between the section header and the section content.
|
||||
offset: f32,
|
||||
}
|
||||
|
||||
impl SectionHeadingStyle {
|
||||
/// Returns the size of the header, which consists of the header text and the padding between
|
||||
/// text and content.
|
||||
fn height(&self) -> f32 {
|
||||
self.size.raw + self.offset
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct SectionStyle {
|
||||
heading: SectionHeadingStyle,
|
||||
/// Thickness of the line that divides the sections within the Component List Panel.
|
||||
divider_height: f32,
|
||||
/// Color used for the Divider.
|
||||
divider_color: color::Rgba,
|
||||
favourites_section_base_color: color::Rgba,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct MenuStyle {
|
||||
/// Height of the area reserved for Component Browser Panel Menu.
|
||||
height: f32,
|
||||
/// Thickness of the line that divides the Component List Panel from the Component Browser
|
||||
/// Panel Menu.
|
||||
divider_height: f32,
|
||||
/// Color used for the Divider.
|
||||
divider_color: color::Rgba,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
fn from_theme(
|
||||
network: &enso_frp::Network,
|
||||
style: &StyleWatchFrp,
|
||||
) -> (enso_frp::Stream<Style>, enso_frp::stream::WeakNode<enso_frp::SourceData>) {
|
||||
let theme_path: style::Path = list_panel_theme::HERE.into();
|
||||
|
||||
let content_width = style.get_number(theme_path.sub("content_width"));
|
||||
let content_height = style.get_number(theme_path.sub("content_height"));
|
||||
let content_corner_radius = style.get_number(theme_path.sub("content_corner_radius"));
|
||||
let content_padding = style.get_number(theme_path.sub("content_padding"));
|
||||
let content_background_color = style.get_color(theme_path.sub("content_background_color"));
|
||||
|
||||
let menu_height = style.get_number(theme_path.sub("menu_height"));
|
||||
let menu_divider_color = style.get_color(theme_path.sub("menu_divider_color"));
|
||||
let menu_divider_height = style.get_number(theme_path.sub("menu_divider_height"));
|
||||
|
||||
let section_divider_height = style.get_number(theme_path.sub("section_divider_height"));
|
||||
let section_heading_size = style.get_number(theme_path.sub("section_heading_size"));
|
||||
let section_heading_font = style.get_text(theme_path.sub("section_heading_font"));
|
||||
let section_heading_color = style.get_color(theme_path.sub("section_heading_color"));
|
||||
let section_divider_color = style.get_color(theme_path.sub("section_divider_color"));
|
||||
|
||||
let favourites_section_base_color =
|
||||
style.get_color(theme_path.sub("favourites_section_base_color"));
|
||||
|
||||
frp::extend! { network
|
||||
init <- source_();
|
||||
|
||||
content_size <- all3(&init,&content_width, &content_height).map(|(_,x,y)|Vector2::new(*x,*y));
|
||||
|
||||
section_heading_layout_data <- all4(
|
||||
&init,
|
||||
§ion_heading_size,
|
||||
§ion_heading_font,
|
||||
§ion_heading_color
|
||||
);
|
||||
section_heading_layout <- section_heading_layout_data.map(|(_,size,font,color)| {
|
||||
SectionHeadingStyle{
|
||||
size:text::style::Size(*size),
|
||||
font:font.clone(),
|
||||
color:*color,
|
||||
offset:*size
|
||||
}
|
||||
});
|
||||
section_layout_data <- all5(
|
||||
&init,
|
||||
§ion_heading_layout,
|
||||
§ion_divider_height,
|
||||
§ion_divider_color,
|
||||
&favourites_section_base_color
|
||||
);
|
||||
section_layout <- section_layout_data.map(
|
||||
|(_,heading,divider_height,divider_color,favourites_section_base_color)|{
|
||||
SectionStyle{
|
||||
heading:heading.clone(),
|
||||
divider_height:*divider_height,
|
||||
divider_color:*divider_color,
|
||||
favourites_section_base_color:*favourites_section_base_color
|
||||
}
|
||||
});
|
||||
content_layout_data <- all5(
|
||||
&init,
|
||||
&content_size,
|
||||
&content_corner_radius,
|
||||
&content_padding,
|
||||
&content_background_color
|
||||
);
|
||||
content_layout <- content_layout_data.map(
|
||||
|(_,size,corner_radius,padding,background_color)|{
|
||||
ContentStyle {
|
||||
size:*size,
|
||||
corner_radius:*corner_radius,
|
||||
padding:*padding,
|
||||
background_color:*background_color,
|
||||
}
|
||||
});
|
||||
|
||||
menu_layout_data <- all4(&init,&menu_height,&menu_divider_color,&menu_divider_height);
|
||||
menu_layout <- menu_layout_data.map(|(_,height,divider_color,divider_height)|{
|
||||
MenuStyle {
|
||||
height:*height,
|
||||
divider_height:*divider_height,
|
||||
divider_color:*divider_color,
|
||||
}
|
||||
});
|
||||
|
||||
layout_data <- all4(&init,&content_layout,§ion_layout,&menu_layout);
|
||||
layout <- layout_data.map(|(_,content,section,menu)| {
|
||||
Style {
|
||||
content:content.clone(),
|
||||
section:section.clone(),
|
||||
menu:menu.clone(),
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
(layout, init)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum to indicate whether sections headers should be placed above or below a section. Dead code
|
||||
/// is allowed as only one option is used at compile time.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum SectionHeaderPlacement {
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
/// Indicates whether sections headers should be placed above or below a section.
|
||||
const SECTION_HEADER_PLACEMENT: SectionHeaderPlacement = SectionHeaderPlacement::Bottom;
|
||||
|
||||
|
||||
// === Shape Definition ===
|
||||
|
||||
mod background {
|
||||
use super::*;
|
||||
|
||||
ensogl_core::define_shape_system! {
|
||||
(style:Style,bg_color:Vector4) {
|
||||
let theme_path: style::Path = list_panel_theme::HERE.into();
|
||||
|
||||
let alpha = Var::<f32>::from(format!("({0}.w)",bg_color));
|
||||
let bg_color = &Var::<color::Rgba>::from(bg_color.clone());
|
||||
|
||||
let content_width = style.get_number(theme_path.sub("content_width"));
|
||||
let content_height = style.get_number(theme_path.sub("content_height"));
|
||||
let content_corner_radius = style.get_number(theme_path.sub("content_corner_radius"));
|
||||
let menu_divider_color = style.get_color(theme_path.sub("menu_divider_color"));
|
||||
let menu_divider_height = style.get_number(theme_path.sub("menu_divider_height"));
|
||||
let menu_height = style.get_number(theme_path.sub("menu_height"));
|
||||
|
||||
let width = content_width;
|
||||
let height = content_height + menu_height;
|
||||
|
||||
let divider_y_pos = height / 2.0 - menu_height;
|
||||
|
||||
let left_width = &(width/2.0).px();
|
||||
let left = Rect((left_width,height.px())).translate_x(-left_width/2.0);
|
||||
|
||||
let right_width = &(width/2.0 + 2.0 * content_corner_radius).px();
|
||||
let right = Rect((right_width,height.px())).corners_radius(content_corner_radius.px());
|
||||
let right = right.translate_x((width/4.0-content_corner_radius).px());
|
||||
|
||||
let divider = Rect((width.px(),menu_divider_height.px()));
|
||||
let divider = divider.fill(menu_divider_color);
|
||||
let divider = divider.translate_y(divider_y_pos.px());
|
||||
|
||||
let base_shape = &(left + right);
|
||||
let background = base_shape.fill(bg_color);
|
||||
let shadow = shadow::from_shape_with_alpha(base_shape.into(),&alpha,style);
|
||||
|
||||
(shadow + background + divider).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod hline {
|
||||
use super::*;
|
||||
|
||||
ensogl_core::define_shape_system! {
|
||||
(style:Style) {
|
||||
let theme_path: style::Path = list_panel_theme::HERE.into();
|
||||
let width = Var::<Pixels>::from("input_size.x");
|
||||
let height = Var::<Pixels>::from("input_size.y");
|
||||
let section_divider_color = style.get_color(theme_path.sub("section_divider_color"));
|
||||
let rect = Rect((width,height));
|
||||
let rect = rect.fill(section_divider_color);
|
||||
rect.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Model ===
|
||||
// =============
|
||||
|
||||
/// The Model of Select Component.
|
||||
#[derive(Clone, CloneRef, Debug)]
|
||||
pub struct Model {
|
||||
app: Application,
|
||||
logger: Logger,
|
||||
display_object: display::object::Instance,
|
||||
background: background::View,
|
||||
scroll_area: ScrollArea,
|
||||
favourites_section: ColumnSection,
|
||||
local_scope_section: WideSection,
|
||||
sub_modules_section: ColumnSection,
|
||||
layers: Layers,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn new(app: &Application) -> Self {
|
||||
let logger = Logger::new("ComponentBrowserPanel");
|
||||
let app = app.clone_ref();
|
||||
let display_object = display::object::Instance::new(&logger);
|
||||
|
||||
let background = background::View::new(&logger);
|
||||
display_object.add_child(&background);
|
||||
app.display.default_scene.layers.below_main.add_exclusive(&background);
|
||||
|
||||
let favourites_section = Self::init_column_section(&app);
|
||||
let local_scope_section = Self::init_wide_section(&app);
|
||||
let sub_modules_section = Self::init_column_section(&app);
|
||||
|
||||
let scroll_area = ScrollArea::new(&app);
|
||||
display_object.add_child(&scroll_area);
|
||||
|
||||
let layers = Layers::new(&app, &scroll_area);
|
||||
layers.base.add_exclusive(&scroll_area);
|
||||
|
||||
favourites_section.set_parent(scroll_area.content());
|
||||
local_scope_section.set_parent(scroll_area.content());
|
||||
sub_modules_section.set_parent(scroll_area.content());
|
||||
|
||||
// Required for correct clipping. The components need to be set up with the
|
||||
// `scroll_area.content_layer` to be masked correctly by the [`ScrollArea`].
|
||||
favourites_section.set_layers(&layers);
|
||||
local_scope_section.set_layers(&layers);
|
||||
sub_modules_section.set_layers(&layers);
|
||||
|
||||
Self {
|
||||
app,
|
||||
display_object,
|
||||
background,
|
||||
scroll_area,
|
||||
favourites_section,
|
||||
local_scope_section,
|
||||
sub_modules_section,
|
||||
layers,
|
||||
logger,
|
||||
}
|
||||
}
|
||||
|
||||
fn init_column_section(app: &Application) -> ColumnSection {
|
||||
let content = app.new_view::<column_grid::ColumnGrid>();
|
||||
LabeledSection::new(content, app)
|
||||
}
|
||||
|
||||
fn init_wide_section(app: &Application) -> WideSection {
|
||||
let content = app.new_view::<component_group::wide::View>();
|
||||
content.set_no_items_label_text("No Entries.");
|
||||
LabeledSection::new(content, app)
|
||||
}
|
||||
|
||||
fn update_style(&self, style: &Style) {
|
||||
self.sub_modules_section.set_style(style);
|
||||
self.local_scope_section.set_style(style);
|
||||
self.favourites_section.set_style(style);
|
||||
|
||||
self.background.bg_color.set(style.content.background_color.into());
|
||||
self.background.size.set(style.size());
|
||||
|
||||
let local_scope_content = &self.local_scope_section.content;
|
||||
local_scope_content.set_position_x(style.content.padding + style.content.size.x / 2.0);
|
||||
local_scope_content.set_width(style.size_inner().x - 2.0 * style.content.padding);
|
||||
self.local_scope_section.content.set_color(style.section.favourites_section_base_color);
|
||||
self.local_scope_section.label.set_content(LOCAL_SCOPE_SECTION_HEADING_LABEL);
|
||||
|
||||
self.favourites_section.content.set_position_x(style.content.padding);
|
||||
self.favourites_section.label.set_content(SUB_MODULES_SECTION_HEADING_LABEL);
|
||||
|
||||
self.sub_modules_section.content.set_position_x(style.content.padding);
|
||||
self.sub_modules_section.label.set_content(SUB_MODULES_SECTION_HEADING_LABEL);
|
||||
|
||||
self.scroll_area.resize(Vector2::new(
|
||||
style.content.size.x - style.content.padding,
|
||||
style.content.size.y - style.content.padding,
|
||||
));
|
||||
self.scroll_area.set_position_xy(Vector2::new(
|
||||
-style.content.size.x / 2.0,
|
||||
style.content.size.y / 2.0 - style.menu.height / 2.0,
|
||||
));
|
||||
self.scroll_area.set_corner_radius_bottom_right(style.content.corner_radius);
|
||||
}
|
||||
|
||||
fn recompute_layout(&self, style: &Style) {
|
||||
self.update_style(style);
|
||||
|
||||
let favourites_section_height = self.favourites_section.height(style);
|
||||
let local_scope_height = self.local_scope_section.height(style);
|
||||
let sub_modules_height = self.sub_modules_section.height(style);
|
||||
|
||||
self.favourites_section.set_base_position_y(0.0, style);
|
||||
self.local_scope_section.set_base_position_y(-favourites_section_height, style);
|
||||
let sub_modules_position = -favourites_section_height - local_scope_height;
|
||||
self.sub_modules_section.set_base_position_y(sub_modules_position, style);
|
||||
|
||||
let full_height = favourites_section_height + local_scope_height + sub_modules_height;
|
||||
self.scroll_area.set_content_height(full_height);
|
||||
self.scroll_area.jump_to_y(full_height);
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for Model {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.display_object
|
||||
}
|
||||
}
|
||||
|
||||
impl component::Model for Model {
|
||||
fn label() -> &'static str {
|
||||
"ComponentBrowserPanel"
|
||||
}
|
||||
|
||||
fn new(app: &Application, _logger: &DefaultWarningLogger) -> Self {
|
||||
Self::new(app)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
// === Labeled Section ===
|
||||
// =======================
|
||||
|
||||
/// Struct that contains the components contained in a section of the Component Browser Panel. Also
|
||||
/// provides some utility functions for shape and layout handling.
|
||||
#[derive(Clone, Debug)]
|
||||
struct LabeledSection<T> {
|
||||
pub label: text::Area,
|
||||
pub divider: hline::View,
|
||||
pub content: T,
|
||||
}
|
||||
|
||||
impl<T: CloneRef> CloneRef for LabeledSection<T> {
|
||||
fn clone_ref(&self) -> Self {
|
||||
LabeledSection {
|
||||
label: self.label.clone_ref(),
|
||||
divider: self.divider.clone_ref(),
|
||||
content: self.content.clone_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type WideSection = LabeledSection<component_group::wide::View>;
|
||||
type ColumnSection = LabeledSection<column_grid::ColumnGrid>;
|
||||
|
||||
impl<T: CloneRef> LabeledSection<T> {
|
||||
pub fn new(content: T, app: &Application) -> Self {
|
||||
let logger = Logger::new("LabeledSection");
|
||||
let label = text::Area::new(app);
|
||||
let divider = hline::View::new(logger);
|
||||
Self { label, divider, content }
|
||||
}
|
||||
|
||||
fn set_style(&self, style: &Style) {
|
||||
self.divider.size.set(Vector2(INFINITE, style.section.divider_height));
|
||||
self.label.set_default_color(style.section.heading.color);
|
||||
self.label.set_default_text_size(style.section.heading.size);
|
||||
self.label.set_font(style.section.heading.font.clone());
|
||||
// TODO[MM]: These magic numbers will be removed with https://github.com/enso-org/enso/pull/3537
|
||||
self.label.set_position_y(-0.75 * style.section.heading.size.raw);
|
||||
self.label.set_position_x(3.0 + style.content.padding);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ObjectOps + CloneRef> LabeledSection<T> {
|
||||
fn set_parent(&self, parent: impl ObjectOps) {
|
||||
parent.add_child(&self.content);
|
||||
parent.add_child(&self.label);
|
||||
parent.add_child(&self.divider);
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait that provides functionality for layouting and layer setting for structs used in the
|
||||
/// `LabeledSection`.
|
||||
trait SectionContent {
|
||||
fn set_layers(&self, layers: &Layers);
|
||||
fn height(&self) -> f32;
|
||||
fn set_position_top_y(&self, position_y: f32);
|
||||
}
|
||||
|
||||
impl SectionContent for component_group::wide::View {
|
||||
fn set_layers(&self, layers: &Layers) {
|
||||
self.model().set_layers(&layers.groups);
|
||||
}
|
||||
|
||||
fn height(&self) -> f32 {
|
||||
self.size.value().y
|
||||
}
|
||||
|
||||
fn set_position_top_y(&self, position_y: f32) {
|
||||
self.set_position_y(position_y - self.height() / 2.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl SectionContent for column_grid::ColumnGrid {
|
||||
fn set_layers(&self, layers: &Layers) {
|
||||
self.model().set_layers(layers);
|
||||
}
|
||||
|
||||
fn height(&self) -> f32 {
|
||||
self.size.value().y
|
||||
}
|
||||
|
||||
fn set_position_top_y(&self, position_y: f32) {
|
||||
self.set_position_y(position_y);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SectionContent + CloneRef> LabeledSection<T> {
|
||||
fn set_layers(&self, layers: &Layers) {
|
||||
self.content.set_layers(layers);
|
||||
layers.scroll_layer.add_exclusive(&self.label);
|
||||
self.label.add_to_scene_layer(&layers.scroll_layer);
|
||||
layers.scroll_layer.add_exclusive(&self.divider);
|
||||
}
|
||||
|
||||
/// Full height of the section including header.
|
||||
fn height(&self, style: &Style) -> f32 {
|
||||
let label_height = style.section.heading.height();
|
||||
let body_height = self.content.height();
|
||||
// TODO[MM]: This magic number will be removed with https://github.com/enso-org/enso/pull/3537
|
||||
let next_section_offset = 29.0;
|
||||
body_height + label_height + next_section_offset
|
||||
}
|
||||
|
||||
/// Set the top y position of the section.
|
||||
fn set_base_position_y(&self, position_y: f32, style: &Style) {
|
||||
match SECTION_HEADER_PLACEMENT {
|
||||
SectionHeaderPlacement::Top => {
|
||||
// TODO[MM] This magic number will be removed with https://github.com/enso-org/enso/pull/3537
|
||||
let label_pos = position_y - self.label.height.value() / 1.5;
|
||||
self.label.set_position_y(label_pos);
|
||||
self.divider.set_position_y(position_y);
|
||||
let offset_from_top = style.section.heading.offset + style.section.heading.height();
|
||||
let content_position_y = position_y - offset_from_top;
|
||||
self.content.set_position_top_y(content_position_y);
|
||||
}
|
||||
SectionHeaderPlacement::Bottom => {
|
||||
// TODO[MM]: This magic number will be removed with https://github.com/enso-org/enso/pull/3537
|
||||
let offset_from_top = 1.0;
|
||||
let label_offset = self.content.height() + self.label.height.value() / 1.5;
|
||||
let label_pos = position_y - label_offset - offset_from_top;
|
||||
self.label.set_position_y(label_pos);
|
||||
let divider_offset = self.content.height() - style.section.divider_height + 2.0;
|
||||
// TODO[MM]: This magic number will be removed with https://github.com/enso-org/enso/pull/3537
|
||||
let divider_pos = position_y - divider_offset - offset_from_top;
|
||||
self.divider.set_position_y(divider_pos);
|
||||
self.content.set_position_top_y(position_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===========
|
||||
// === FRP ===
|
||||
// ===========
|
||||
|
||||
define_endpoints_2! {
|
||||
Input{
|
||||
set_local_scope_section(list_view::entry::AnyModelProvider<component_group::Entry>),
|
||||
set_favourites_section(Vec<LabeledAnyModelProvider>),
|
||||
set_sub_modules_section(Vec<LabeledAnyModelProvider>),
|
||||
}
|
||||
Output{}
|
||||
}
|
||||
|
||||
impl component::Frp<Model> for Frp {
|
||||
fn init(
|
||||
frp_api: &<Self as API>::Private,
|
||||
_app: &Application,
|
||||
model: &Model,
|
||||
style: &StyleWatchFrp,
|
||||
) {
|
||||
let network = &frp_api.network;
|
||||
let (layout_update, init_layout) = Style::from_theme(network, style);
|
||||
frp::extend! { network
|
||||
model.favourites_section.content.set_content <+ frp_api.input.set_favourites_section;
|
||||
model.local_scope_section.content.set_entries <+ frp_api.input.set_local_scope_section;
|
||||
model.sub_modules_section.content.set_content <+ frp_api.input.set_sub_modules_section;
|
||||
content_update <- any3_(
|
||||
&frp_api.input.set_favourites_section,
|
||||
&frp_api.input.set_local_scope_section,
|
||||
&frp_api.input.set_sub_modules_section,
|
||||
);
|
||||
recompute_layout <- all(&content_update,&layout_update);
|
||||
eval recompute_layout(((_,layout)) model.recompute_layout(layout) );
|
||||
}
|
||||
init_layout.emit(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A sub-content of the Component Browser, that shows the available Component List Sections.
|
||||
/// Each Component List Section contains named tiles called Component List Groups. To learn more
|
||||
/// see the [Component Browser Design Document](https://github.com/enso-org/design/blob/e6cffec2dd6d16688164f04a4ef0d9dff998c3e7/epics/component-browser/design.md).
|
||||
pub type ComponentBrowserPanel = component::ComponentView<Model, Frp>;
|
@ -8,6 +8,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
debug-scene-component-list-panel-view = { path = "component-list-panel-view" }
|
||||
debug-scene-component-group = { path = "component-group" }
|
||||
debug-scene-icons = { path = "icons" }
|
||||
debug-scene-interface = { path = "interface" }
|
||||
|
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "debug-scene-component-list-panel-view"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
enso-frp = { path = "../../../../../lib/rust/frp" }
|
||||
enso-profiler = { path = "../../../../../lib/rust/profiler" }
|
||||
ensogl-core = { path = "../../../../../lib/rust/ensogl/core" }
|
||||
ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }
|
||||
ensogl-list-view = { path = "../../../../../lib/rust/ensogl/component/list-view" }
|
||||
ensogl-selector = { path = "../../../../../lib/rust/ensogl/component/selector" }
|
||||
ensogl-text-msdf-sys = { path = "../../../../../lib/rust/ensogl/component/text/msdf-sys" }
|
||||
ide-view-component-group = { path = "../../component-browser/component-group" }
|
||||
searcher-list-panel = { path = "../../component-browser/searcher-list-panel" }
|
||||
wasm-bindgen = { version = "0.2.78", features = ["nightly"] }
|
199
app/gui/view/debug_scene/component-list-panel-view/src/lib.rs
Normal file
199
app/gui/view/debug_scene/component-list-panel-view/src/lib.rs
Normal file
@ -0,0 +1,199 @@
|
||||
//! Example scene showing simple usage of a shape system.
|
||||
|
||||
#![recursion_limit = "512"]
|
||||
// === Features ===
|
||||
#![allow(incomplete_features)]
|
||||
#![feature(negative_impls)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(bool_to_option)]
|
||||
#![feature(cell_update)]
|
||||
#![feature(const_type_id)]
|
||||
#![feature(drain_filter)]
|
||||
#![feature(entry_insert)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(marker_trait_attr)]
|
||||
#![feature(specialization)]
|
||||
#![feature(trait_alias)]
|
||||
#![feature(type_alias_impl_trait)]
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(trace_macros)]
|
||||
#![feature(const_trait_impl)]
|
||||
#![feature(slice_as_chunks)]
|
||||
// === Standard Linter Configuration ===
|
||||
#![deny(non_ascii_idents)]
|
||||
#![warn(unsafe_code)]
|
||||
// === Non-Standard Linter Configuration ===
|
||||
#![allow(clippy::option_map_unit_fn)]
|
||||
#![allow(clippy::precedence)]
|
||||
#![allow(dead_code)]
|
||||
#![deny(unconditional_recursion)]
|
||||
#![warn(missing_copy_implementations)]
|
||||
#![warn(missing_debug_implementations)]
|
||||
#![warn(missing_docs)]
|
||||
#![warn(trivial_casts)]
|
||||
#![warn(trivial_numeric_casts)]
|
||||
#![warn(unused_import_braces)]
|
||||
#![warn(unused_qualifications)]
|
||||
|
||||
use ensogl_core::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use component_group::icon;
|
||||
use ensogl_core::application::Application;
|
||||
use ensogl_core::display::navigation::navigator::Navigator;
|
||||
use ensogl_core::display::object::ObjectOps;
|
||||
use ensogl_hardcoded_theme as theme;
|
||||
use ensogl_list_view as list_view;
|
||||
use ensogl_list_view::entry::GlyphHighlightedLabelModel;
|
||||
use ide_view_component_group as component_group;
|
||||
use list_view::entry::AnyModelProvider;
|
||||
use searcher_list_panel::ComponentBrowserPanel;
|
||||
use searcher_list_panel::LabeledAnyModelProvider;
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === Mock Entries ===
|
||||
// ====================
|
||||
|
||||
const PREPARED_ITEMS: &[(&str, icon::Id)] = &[
|
||||
("long sample entry with text overflowing the width", icon::Id::Star),
|
||||
("convert", icon::Id::Convert),
|
||||
("table input", icon::Id::DataInput),
|
||||
("text input", icon::Id::TextInput),
|
||||
("number input", icon::Id::NumberInput),
|
||||
("table output", icon::Id::TableEdit),
|
||||
("dataframe clean", icon::Id::DataframeClean),
|
||||
("data input", icon::Id::DataInput),
|
||||
];
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MockEntries {
|
||||
entries: Vec<component_group::entry::Model>,
|
||||
count: Cell<usize>,
|
||||
}
|
||||
|
||||
impl MockEntries {
|
||||
fn new(count: usize) -> Rc<Self> {
|
||||
Rc::new(Self {
|
||||
entries: PREPARED_ITEMS
|
||||
.iter()
|
||||
.cycle()
|
||||
.take(count)
|
||||
.map(|&(label, icon)| component_group::entry::Model {
|
||||
icon,
|
||||
highlighted_text: GlyphHighlightedLabelModel {
|
||||
label: label.to_owned(),
|
||||
highlighted: default(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
count: Cell::new(count),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_entry(&self, id: list_view::entry::Id) -> Option<component_group::entry::Model> {
|
||||
self.entries.get(id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl list_view::entry::ModelProvider<component_group::Entry> for MockEntries {
|
||||
fn entry_count(&self) -> usize {
|
||||
self.count.get()
|
||||
}
|
||||
|
||||
fn get(&self, id: list_view::entry::Id) -> Option<component_group::entry::Model> {
|
||||
self.get_entry(id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===============================
|
||||
// === Initialisation Helpers ===
|
||||
// ===============================
|
||||
|
||||
fn init_sub_modules_section(searcher_list_panel: &ComponentBrowserPanel) {
|
||||
let sub_module_data = vec![
|
||||
MockEntries::new(4),
|
||||
MockEntries::new(6),
|
||||
MockEntries::new(8),
|
||||
MockEntries::new(6),
|
||||
MockEntries::new(4),
|
||||
MockEntries::new(4),
|
||||
];
|
||||
let sub_module_data = sub_module_data
|
||||
.into_iter()
|
||||
.map(|mock_entries| LabeledAnyModelProvider {
|
||||
content: AnyModelProvider::from(mock_entries.clone_ref()),
|
||||
label: "Header".into(),
|
||||
})
|
||||
.collect_vec();
|
||||
searcher_list_panel.set_sub_modules_section(sub_module_data);
|
||||
}
|
||||
|
||||
fn init_favourites_section(searcher_list_panel: &ComponentBrowserPanel) {
|
||||
let local_scope_data = vec![
|
||||
MockEntries::new(6),
|
||||
MockEntries::new(4),
|
||||
MockEntries::new(3),
|
||||
MockEntries::new(6),
|
||||
MockEntries::new(3),
|
||||
MockEntries::new(6),
|
||||
];
|
||||
let local_scope_data = local_scope_data
|
||||
.into_iter()
|
||||
.map(|mock_entries| LabeledAnyModelProvider {
|
||||
content: AnyModelProvider::from(mock_entries.clone_ref()),
|
||||
label: "Header".into(),
|
||||
})
|
||||
.collect_vec();
|
||||
searcher_list_panel.set_favourites_section(local_scope_data);
|
||||
}
|
||||
|
||||
fn init_local_cope_section(searcher_list_panel: &ComponentBrowserPanel) {
|
||||
let mock_entries = MockEntries::new(20);
|
||||
let model_provider = AnyModelProvider::from(mock_entries.clone_ref());
|
||||
searcher_list_panel.set_local_scope_section(model_provider.clone_ref());
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
// === Entry Point ===
|
||||
// ===================
|
||||
|
||||
/// The example entry point.
|
||||
#[entry_point]
|
||||
#[allow(dead_code)]
|
||||
pub fn main() {
|
||||
ensogl_text_msdf_sys::run_once_initialized(|| {
|
||||
let app = &Application::new("root");
|
||||
theme::builtin::light::register(&app);
|
||||
theme::builtin::light::enable(&app);
|
||||
|
||||
let world = &app.display;
|
||||
let scene = &world.default_scene;
|
||||
let camera = scene.camera().clone_ref();
|
||||
let navigator = Navigator::new(scene, &camera);
|
||||
navigator.disable_wheel_panning();
|
||||
|
||||
let searcher_list_panel = ComponentBrowserPanel::new(app);
|
||||
|
||||
init_local_cope_section(&searcher_list_panel);
|
||||
init_favourites_section(&searcher_list_panel);
|
||||
init_sub_modules_section(&searcher_list_panel);
|
||||
|
||||
world.add_child(&searcher_list_panel);
|
||||
world.keep_alive_forever();
|
||||
|
||||
world
|
||||
.on
|
||||
.before_frame
|
||||
.add(move |_time| {
|
||||
let _keep_alive = &searcher_list_panel;
|
||||
let _keep_alive = &navigator;
|
||||
})
|
||||
.forget();
|
||||
})
|
||||
}
|
@ -21,6 +21,7 @@
|
||||
// ==============
|
||||
|
||||
pub use debug_scene_component_group as component_group;
|
||||
pub use debug_scene_component_list_panel_view as component_list_panel_view;
|
||||
pub use debug_scene_icons as icons;
|
||||
pub use debug_scene_interface as interface;
|
||||
pub use debug_scene_visualization as visualization;
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Options intended to be common for all developers.
|
||||
|
||||
wasm-size-limit: 4.86 MiB
|
||||
wasm-size-limit: 4.98 MiB
|
||||
|
||||
required-versions:
|
||||
node: =16.15.0
|
||||
|
@ -180,6 +180,37 @@ define_themes! { [light:0, dark:1]
|
||||
show_delay_duration_ms = 150.0, 150.0;
|
||||
}
|
||||
component_browser {
|
||||
searcher {
|
||||
list_panel {
|
||||
content_width = 400.0, 400.0;
|
||||
content_height = 398.0, 398.0;
|
||||
content_padding = 3.0, 3.0;
|
||||
content_corner_radius = 15.0, 15.0;
|
||||
content_background_color = Rgba::new(252.0 / 256.0, 254.0 / 255.0, 1.0, 1.0),Rgba::new(252.0 / 256.0, 254.0 / 255.0, 1.0, 1.0);
|
||||
|
||||
section_divider_height = 2.0, 2.0;
|
||||
section_heading_size = 16.0, 16.0;
|
||||
section_heading_font = "Causten-Semibold", "Causten-Semibold";
|
||||
section_heading_color = Rgb(0.4510, 0.4510, 0.4510), Rgb(0.4510, 0.4510, 0.4510);
|
||||
section_divider_color = Rgb(0.4510, 0.4510, 0.4510), Rgb(0.4510, 0.4510, 0.4510);
|
||||
|
||||
menu_height = 35.0, 35.0;
|
||||
menu_divider_color = Rgb(0.7804, 0.7804, 0.7804), Rgb(0.7804, 0.7804, 0.7804);
|
||||
menu_divider_height = 0.5,0.5;
|
||||
|
||||
favourites_section_base_color = Rgba::new(0.0, 0.42, 0.64, 1.0),Rgba::new(0.0, 0.42, 0.64, 1.0);
|
||||
|
||||
section {
|
||||
column_grid {
|
||||
column_gap = 2.0, 2.0;
|
||||
entry_color_0 = Rgba(0.527, 0.554, 0.18, 1.0),Rgba(0.527, 0.554, 0.18, 1.0);
|
||||
entry_color_1 = Rgba(43.0 / 255.0, 117.0 / 255.0, 239.0 / 255.0, 1.0),Rgba(43.0 / 255.0, 117.0 / 255.0, 239.0 / 255.0, 1.0);
|
||||
entry_color_2 = Rgba(62.0 / 255.0, 139.0 / 255.0, 41.0 / 255.0, 1.0),Rgba(62.0 / 255.0, 139.0 / 255.0, 41.0 / 255.0, 1.0);
|
||||
entry_color_3 = Rgba(192.0 / 255.0, 71.0 / 255.0, 171.0 / 255.0, 1.0),Rgba(192.0 / 255.0, 71.0 / 255.0, 171.0 / 255.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
component_group {
|
||||
header {
|
||||
text {
|
||||
@ -212,6 +243,8 @@ define_themes! { [light:0, dark:1]
|
||||
selection_color_intensity = 1.0, 1.0;
|
||||
dimmed_color_intensity = 0.5, 0.5;
|
||||
entry_list {
|
||||
background = Rgba::new(1.0, 0.0, 0.0, 0.5), Rgba::new(1.0, 0.0, 0.0, 0.5);
|
||||
highlight = Rgba::new(1.0, 0.0, 0.0, 0.5), Rgba::new(1.0, 0.0, 0.0, 0.5);
|
||||
selected_color = Rgba::white(), Rgba::white();
|
||||
text {
|
||||
font = "DejaVuSans", "DejaVuSans";
|
||||
@ -229,7 +262,8 @@ define_themes! { [light:0, dark:1]
|
||||
padding = 7.0, 7.0;
|
||||
}
|
||||
highlight {
|
||||
height = 30.0, 30.0
|
||||
height = 30.0, 30.0;
|
||||
corner_radius = 0.0, 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ pub mod overlay {
|
||||
|
||||
/// Information about displayed fragment of entries list.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
struct View {
|
||||
pub struct View {
|
||||
position_y: f32,
|
||||
size: Vector2<f32>,
|
||||
}
|
||||
|
@ -39,8 +39,16 @@ ensogl_core::define_endpoints! {
|
||||
Input {
|
||||
/// Set the width and height in px.
|
||||
resize (Vector2),
|
||||
/// Set the corners radius (in pixels) of the visible area.
|
||||
set_corner_radius (f32),
|
||||
/// Set the corners radius (in pixels) of all corners of the visible area.
|
||||
set_corner_radius (f32),
|
||||
/// Set the corners radius (in pixels) of the top right corners of the visible area.
|
||||
set_corner_radius_top_right (f32),
|
||||
/// Set the corners radius (in pixels) of the top left corners of the visible area.
|
||||
set_corner_radius_top_left (f32),
|
||||
/// Set the corners radius (in pixels) of the bottom right corners of the visible area.
|
||||
set_corner_radius_bottom_right (f32),
|
||||
/// Set the corners radius (in pixels) of the bottom left corners of the visible area.
|
||||
set_corner_radius_bottom_left (f32),
|
||||
/// Set the content width in px. Affects how far one can scroll horizontally.
|
||||
set_content_width (f32),
|
||||
/// Set the content height in px. Affects how far one can scroll vertically.
|
||||
@ -72,11 +80,18 @@ ensogl_core::define_endpoints! {
|
||||
mod mask {
|
||||
use super::*;
|
||||
ensogl_core::define_shape_system! {
|
||||
(style:Style, corner_radius: f32) {
|
||||
(style:Style, corner_radius_top_right: f32, corner_radius_top_left: f32,
|
||||
corner_radius_bottom_right: f32, corner_radius_bottom_left: f32) {
|
||||
let width: Var<Pixels> = "input_size.x".into();
|
||||
let height: Var<Pixels> = "input_size.y".into();
|
||||
let color = color::Rgba::white();
|
||||
shape::Rect((&width, &height)).corners_radius(corner_radius).fill(color).into()
|
||||
let rect = shape::Rect((width, height)).corners_radiuses(
|
||||
corner_radius_top_left,
|
||||
corner_radius_top_right,
|
||||
corner_radius_bottom_left,
|
||||
corner_radius_bottom_right,
|
||||
);
|
||||
rect.fill(color).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -180,7 +195,24 @@ impl ScrollArea {
|
||||
|
||||
eval frp.resize((size) model.resize(*size));
|
||||
|
||||
eval frp.set_corner_radius((radius) model.mask.corner_radius.set(*radius));
|
||||
// === Shape ===
|
||||
|
||||
frp.set_corner_radius_top_right <+ frp.set_corner_radius;
|
||||
frp.set_corner_radius_bottom_right <+ frp.set_corner_radius;
|
||||
frp.set_corner_radius_top_left <+ frp.set_corner_radius;
|
||||
frp.set_corner_radius_bottom_left <+ frp.set_corner_radius;
|
||||
eval frp.set_corner_radius_top_right((radius)
|
||||
model.mask.corner_radius_top_right.set(*radius);
|
||||
);
|
||||
eval frp.set_corner_radius_bottom_right((radius)
|
||||
model.mask.corner_radius_bottom_right.set(*radius);
|
||||
);
|
||||
eval frp.set_corner_radius_top_left((radius)
|
||||
model.mask.corner_radius_top_left.set(*radius);
|
||||
);
|
||||
eval frp.set_corner_radius_bottom_left((radius)
|
||||
model.mask.corner_radius_bottom_left.set(*radius);
|
||||
);
|
||||
|
||||
|
||||
// === Scrolling ===
|
||||
|
@ -24,7 +24,7 @@ macro_rules! def_style_property {
|
||||
|
||||
impl $name {
|
||||
/// Constructor.
|
||||
pub fn new(raw: $field_type) -> $name {
|
||||
pub const fn new(raw: $field_type) -> $name {
|
||||
$name { raw }
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ pub mod world;
|
||||
pub mod traits {
|
||||
use super::*;
|
||||
pub use object::traits::*;
|
||||
pub use object::ObjectOps;
|
||||
}
|
||||
|
||||
/// Common types.
|
||||
|
@ -9,7 +9,7 @@ use crate::display::style::data::DataMatch;
|
||||
use crate::display::style::Path;
|
||||
|
||||
use enso_frp as frp;
|
||||
|
||||
use enso_prelude::tracing::log;
|
||||
|
||||
|
||||
// =================
|
||||
@ -84,36 +84,53 @@ impl StyleWatchFrp {
|
||||
sampler
|
||||
}
|
||||
|
||||
/// Queries style sheet value for a number. Returns 0.0 if not found.
|
||||
/// Queries style sheet value for a number. Emits a warning and returns 0.0 if not found.
|
||||
pub fn get_number(&self, path: impl Into<Path>) -> frp::Sampler<f32> {
|
||||
let network = &self.network;
|
||||
let path = path.into();
|
||||
let warning = format!("Tried to access undefined number from theme: {}", &path);
|
||||
let (source, current) = self.get_internal(path);
|
||||
frp::extend! { network
|
||||
value <- source.map(|t| t.number().unwrap_or(0.0));
|
||||
value <- source.map(move |t| t.number().unwrap_or_else(|| {
|
||||
log::warn!("{}", warning);
|
||||
0.0
|
||||
}));
|
||||
sampler <- value.sampler();
|
||||
}
|
||||
source.emit(current);
|
||||
sampler
|
||||
}
|
||||
|
||||
/// Queries style sheet color, if not found fallbacks to [`FALLBACK_COLOR`].
|
||||
/// Queries style sheet color, if not found fallbacks to [`FALLBACK_COLOR`] and emits a warning.
|
||||
pub fn get_color<T: Into<Path>>(&self, path: T) -> frp::Sampler<color::Rgba> {
|
||||
let network = &self.network;
|
||||
let path = path.into();
|
||||
let warning = format!("Tried to access undefined color from theme: {}", &path);
|
||||
let (source, current) = self.get_internal(path);
|
||||
frp::extend! { network
|
||||
value <- source.map(|t| t.color().unwrap_or(FALLBACK_COLOR));
|
||||
value <- source.map(move |t| t.color().unwrap_or_else(|| {
|
||||
log::warn!("{}", warning);
|
||||
FALLBACK_COLOR
|
||||
}));
|
||||
sampler <- value.sampler();
|
||||
}
|
||||
source.emit(current);
|
||||
sampler
|
||||
}
|
||||
|
||||
/// Queries the style sheet for a text. Returns empty string if not found.
|
||||
/// Queries the style sheet for a text. Emits a warning and returns empty string if not found.
|
||||
pub fn get_text<T: Into<Path>>(&self, path: T) -> frp::Sampler<String> {
|
||||
let network = &self.network;
|
||||
let path = path.into();
|
||||
let warning = format!("Tried to access undefined text from theme: {}", &path);
|
||||
let (source, current) = self.get_internal(path);
|
||||
frp::extend! { network
|
||||
value <- source.map(|t| t.text().unwrap_or_default());
|
||||
value <- source.map(move |t| {
|
||||
t.text().unwrap_or_else(|| {
|
||||
log::warn!("{}", warning);
|
||||
default()
|
||||
})
|
||||
});
|
||||
sampler <- value.sampler();
|
||||
}
|
||||
source.emit(current);
|
||||
@ -192,6 +209,12 @@ impl StyleWatch {
|
||||
self.get(path).number().unwrap_or(fallback)
|
||||
}
|
||||
|
||||
/// Queries style sheet number value, if not found computes it from a closure.
|
||||
pub fn get_number_or_else<F>(&self, path: impl Into<Path>, fallback: F) -> f32
|
||||
where F: FnOnce() -> f32 {
|
||||
self.get(path).number().unwrap_or_else(fallback)
|
||||
}
|
||||
|
||||
/// Queries style sheet number value. Returns 0 if not found.
|
||||
pub fn get_number(&self, path: impl Into<Path>) -> f32 {
|
||||
self.get_number_or(path, 0.0)
|
||||
|
@ -1588,7 +1588,7 @@ impl<T: EventOutput> OwnedTrace<T> {
|
||||
|
||||
impl<T: EventOutput> stream::EventConsumer<Output<T>> for OwnedTrace<T> {
|
||||
fn on_event(&self, stack: CallStack, event: &Output<T>) {
|
||||
DEBUG!("[FRP] {self.label()}: {event:?}");
|
||||
ERROR!("[FRP] {self.label()}: {event:?}");
|
||||
DEBUG!("[FRP] {stack}");
|
||||
self.emit_event(stack, event);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user