Component List Panel View (#3495)

This commit is contained in:
Michael Mauderer 2022-06-22 16:39:32 +01:00 committed by GitHub
parent 7a2d304fa0
commit 655793aa78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1328 additions and 20 deletions

47
Cargo.lock generated
View File

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

View File

@ -9,3 +9,4 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
ide-view-component-group = { path = "component-group" }
ensogl-text = { path = "../../../../lib/rust/ensogl/component/text" }

View File

@ -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" }

View File

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

View File

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

View File

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

View 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,
&section_heading_size,
&section_heading_font,
&section_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,
&section_heading_layout,
&section_divider_height,
&section_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,&section_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>;

View File

@ -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" }

View File

@ -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"] }

View 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();
})
}

View File

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

View File

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

View File

@ -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;
}
}
}

View File

@ -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>,
}

View File

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

View File

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

View File

@ -28,6 +28,7 @@ pub mod world;
pub mod traits {
use super::*;
pub use object::traits::*;
pub use object::ObjectOps;
}
/// Common types.

View File

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

View File

@ -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);
}