mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 12:42:16 +03:00
Component Group View with static header and without icons (#3373)
Add an initial version of the visual component for displaying the Component Group View. The component contains a header (for displaying the Group Name) and a list of labels (for displaying the component names). https://www.pivotaltracker.com/story/show/181724889 #### Visuals A screenshot from a debug scene demonstrating the component: <img width="251" alt="Screenshot 2022-04-13 at 20 07 56" src="https://user-images.githubusercontent.com/273837/163243304-21c3ad78-4813-4368-b3bb-844d979da699.png"> Screenshots from other debug scenes (`list_view` and `text_area`), demonstrating that the other components still display correctly: <img width="202" alt="Screenshot 2022-04-13 at 20 08 56" src="https://user-images.githubusercontent.com/273837/163243428-de9dc1c7-5a9f-45e0-9325-db60cece9768.png"> <img width="403" alt="Screenshot 2022-04-13 at 20 08 48" src="https://user-images.githubusercontent.com/273837/163243432-895061d9-5bd9-4349-8679-eb63b0f6724d.png"> A screenshot of the Node Searcher's list, showing that long entries in a ListView are now truncated, and an ellipsis character is added in place of removed characters: <img width="651" alt="Screenshot 2022-04-13 at 20 10 16" src="https://user-images.githubusercontent.com/273837/163243664-5b671969-7aa0-4bef-8fd2-825602d85848.png"> # Important Notes - Adding support for the text truncation feature in `ListView` required some changes in the`list_view::Entry`-related APIs. - An embedded font was added (DejaVuSans-Bold) for use in the Component Group View debug scene, and 5 unused embedded fonts were removed.
This commit is contained in:
parent
0ea5dc2a6f
commit
e75df61b2c
@ -2,6 +2,9 @@
|
||||
|
||||
#### Visual Environment
|
||||
|
||||
- [Long names on the Node Searcher's list are truncated.][3373] The part of the
|
||||
name that doesn't fit in the Searcher's window is replaced with an ellipsis
|
||||
character ("…").
|
||||
- [Magnet Alignment algorithm is used while placing new nodes][3366]. When we
|
||||
find an available free space for a new node, the node gets aligned with the
|
||||
surrounding nodes horizontally and vertically. This helps to preserve a nice
|
||||
@ -154,6 +157,7 @@
|
||||
[3349]: https://github.com/enso-org/enso/pull/3349
|
||||
[3361]: https://github.com/enso-org/enso/pull/3361
|
||||
[3364]: https://github.com/enso-org/enso/pull/3364
|
||||
[3373]: https://github.com/enso-org/enso/pull/3373
|
||||
[3377]: https://github.com/enso-org/enso/pull/3377
|
||||
[3366]: https://github.com/enso-org/enso/pull/3366
|
||||
[3379]: https://github.com/enso-org/enso/pull/3379
|
||||
|
27
Cargo.lock
generated
27
Cargo.lock
generated
@ -739,6 +739,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debug-scene-component-group"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"enso-frp",
|
||||
"ensogl-core",
|
||||
"ensogl-hardcoded-theme",
|
||||
"ensogl-list-view",
|
||||
"ensogl-text-msdf-sys",
|
||||
"ide-view-component-group",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debug-scene-interface"
|
||||
version = "0.1.0"
|
||||
@ -941,6 +954,7 @@ dependencies = [
|
||||
name = "enso-debug-scene"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"debug-scene-component-group",
|
||||
"debug-scene-interface",
|
||||
"debug-scene-visualization",
|
||||
]
|
||||
@ -1706,7 +1720,6 @@ dependencies = [
|
||||
"enso-text",
|
||||
"enso-types",
|
||||
"ensogl-core",
|
||||
"ensogl-hardcoded-theme",
|
||||
"ensogl-text-embedded-fonts",
|
||||
"ensogl-text-msdf-sys",
|
||||
"wasm-bindgen-test",
|
||||
@ -2257,6 +2270,18 @@ dependencies = [
|
||||
"welcome-screen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ide-view-component-group"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"enso-frp",
|
||||
"ensogl-core",
|
||||
"ensogl-gui-component",
|
||||
"ensogl-hardcoded-theme",
|
||||
"ensogl-list-view",
|
||||
"ensogl-text",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ide-view-graph-editor"
|
||||
version = "0.1.0"
|
||||
|
11
app/gui/view/component-browser/Cargo.toml
Normal file
11
app/gui/view/component-browser/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "ide-view-component-browser"
|
||||
version = "0.1.0"
|
||||
authors = ["Enso Team <contact@enso.org>"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
ide-view-component-group = { path = "component-group" }
|
17
app/gui/view/component-browser/component-group/Cargo.toml
Normal file
17
app/gui/view/component-browser/component-group/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "ide-view-component-group"
|
||||
version = "0.1.0"
|
||||
authors = ["Enso Team <contact@enso.org>"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
enso-frp = { version = "0.1.0", path = "../../../../../lib/rust/frp" }
|
||||
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-hardcoded-theme = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }
|
||||
ensogl-list-view = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/list-view" }
|
||||
ensogl-text = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/text" }
|
||||
|
250
app/gui/view/component-browser/component-group/src/lib.rs
Normal file
250
app/gui/view/component-browser/component-group/src/lib.rs
Normal file
@ -0,0 +1,250 @@
|
||||
//! This module defines a widget for displaying a list of entries of a component group and the name
|
||||
//! of the component group.
|
||||
//!
|
||||
//! The widget is defined by the [`View`].
|
||||
//!
|
||||
//! To learn more about component groups, see the [Component Browser Design
|
||||
//! Document](https://github.com/enso-org/design/blob/e6cffec2dd6d16688164f04a4ef0d9dff998c3e7/epics/component-browser/design.md).
|
||||
|
||||
// === Standard Linter Configuration ===
|
||||
#![deny(non_ascii_idents)]
|
||||
#![warn(unsafe_code)]
|
||||
// === Non-Standard Linter Configuration ===
|
||||
#![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 enso_frp as frp;
|
||||
use ensogl_core::application::Application;
|
||||
use ensogl_core::data::color::Rgba;
|
||||
use ensogl_core::display;
|
||||
use ensogl_core::display::shape::*;
|
||||
use ensogl_gui_component::component;
|
||||
use ensogl_hardcoded_theme::application::component_browser::component_group as theme;
|
||||
use ensogl_list_view as list_view;
|
||||
use ensogl_text as text;
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
const HEADER_FONT: &str = "DejaVuSans-Bold";
|
||||
|
||||
|
||||
|
||||
// ==========================
|
||||
// === Shapes Definitions ===
|
||||
// ==========================
|
||||
|
||||
|
||||
// === Background ===
|
||||
|
||||
/// The background of the [`View`].
|
||||
pub mod background {
|
||||
use super::*;
|
||||
|
||||
ensogl_core::define_shape_system! {
|
||||
below = [list_view::background];
|
||||
(style:Style, color:Vector4) {
|
||||
let sprite_width: Var<Pixels> = "input_size.x".into();
|
||||
let sprite_height: Var<Pixels> = "input_size.y".into();
|
||||
let color = Var::<Rgba>::from(color);
|
||||
// TODO[MC,WD]: We should use Plane here, but it has a bug - renders wrong color. See:
|
||||
// https://github.com/enso-org/enso/pull/3373#discussion_r849054476
|
||||
let shape = Rect((&sprite_width, &sprite_height)).fill(color);
|
||||
shape.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
// === Header Geometry ===
|
||||
// =======================
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
struct HeaderGeometry {
|
||||
height: f32,
|
||||
padding_left: f32,
|
||||
padding_right: f32,
|
||||
padding_bottom: f32,
|
||||
}
|
||||
|
||||
impl HeaderGeometry {
|
||||
fn from_style(style: &StyleWatchFrp, network: &frp::Network) -> frp::Sampler<Self> {
|
||||
let height = style.get_number(theme::header::height);
|
||||
let padding_left = style.get_number(theme::header::padding::left);
|
||||
let padding_right = style.get_number(theme::header::padding::right);
|
||||
let padding_bottom = style.get_number(theme::header::padding::bottom);
|
||||
|
||||
frp::extend! { network
|
||||
init <- source_();
|
||||
theme <- all_with5(&init,&height,&padding_left,&padding_right,&padding_bottom,
|
||||
|_,&height,&padding_left,&padding_right,&padding_bottom|
|
||||
Self{height,padding_left,padding_right,padding_bottom}
|
||||
);
|
||||
theme_sampler <- theme.sampler();
|
||||
}
|
||||
init.emit(());
|
||||
theme_sampler
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===========
|
||||
// === FRP ===
|
||||
// ===========
|
||||
|
||||
ensogl_core::define_endpoints_2! {
|
||||
Input {
|
||||
set_header(String),
|
||||
set_entries(list_view::entry::AnyModelProvider<list_view::entry::Label>),
|
||||
set_background_color(Rgba),
|
||||
set_size(Vector2),
|
||||
}
|
||||
Output {}
|
||||
}
|
||||
|
||||
impl component::Frp<Model> for Frp {
|
||||
fn init(api: &Self::Private, _app: &Application, model: &Model, style: &StyleWatchFrp) {
|
||||
let network = &api.network;
|
||||
let input = &api.input;
|
||||
let header_text_size = style.get_number(theme::header::text::size);
|
||||
frp::extend! { network
|
||||
|
||||
// === Geometry ===
|
||||
|
||||
let header_geometry = HeaderGeometry::from_style(style, network);
|
||||
size_and_header_geometry <- all(&input.set_size, &header_geometry);
|
||||
eval size_and_header_geometry(((size, hdr_geom)) model.resize(*size, *hdr_geom));
|
||||
|
||||
|
||||
// === Header ===
|
||||
|
||||
init <- source_();
|
||||
header_text_size <- all(&header_text_size, &init)._0();
|
||||
model.header.set_default_text_size <+ header_text_size.map(|v| text::Size(*v));
|
||||
_set_header <- input.set_header.map2(&size_and_header_geometry, f!(
|
||||
(text, (size, hdr_geom)) {
|
||||
model.header_text.replace(text.clone());
|
||||
model.update_header_width(*size, *hdr_geom);
|
||||
})
|
||||
);
|
||||
eval input.set_background_color((c)
|
||||
model.background.color.set(c.into()));
|
||||
|
||||
|
||||
// === Entries ===
|
||||
|
||||
model.entries.set_background_color(HOVER_COLOR);
|
||||
model.entries.show_background_shadow(false);
|
||||
model.entries.set_background_corners_radius(0.0);
|
||||
model.entries.set_background_color <+ input.set_background_color;
|
||||
model.entries.set_entries <+ input.set_entries;
|
||||
}
|
||||
init.emit(());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Model ===
|
||||
// =============
|
||||
|
||||
/// The Model of the [`View`] component.
|
||||
#[derive(Clone, CloneRef, Debug)]
|
||||
pub struct Model {
|
||||
display_object: display::object::Instance,
|
||||
header: text::Area,
|
||||
header_text: Rc<RefCell<String>>,
|
||||
background: background::View,
|
||||
entries: list_view::ListView<list_view::entry::Label>,
|
||||
}
|
||||
|
||||
impl display::Object for Model {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.display_object
|
||||
}
|
||||
}
|
||||
|
||||
impl component::Model for Model {
|
||||
fn label() -> &'static str {
|
||||
"ComponentGroup"
|
||||
}
|
||||
|
||||
fn new(app: &Application, logger: &Logger) -> Self {
|
||||
let header_text = default();
|
||||
let display_object = display::object::Instance::new(&logger);
|
||||
let background = background::View::new(&logger);
|
||||
let header = text::Area::new(app);
|
||||
let entries = list_view::ListView::new(app);
|
||||
display_object.add_child(&background);
|
||||
display_object.add_child(&header);
|
||||
display_object.add_child(&entries);
|
||||
|
||||
header.set_font(HEADER_FONT);
|
||||
let label_layer = &app.display.default_scene.layers.label;
|
||||
header.add_to_scene_layer(label_layer);
|
||||
|
||||
Model { display_object, header, header_text, background, entries }
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn resize(&self, size: Vector2, header_geometry: HeaderGeometry) {
|
||||
// === Background ===
|
||||
|
||||
self.background.size.set(size);
|
||||
|
||||
|
||||
// === Header Text ===
|
||||
|
||||
let header_padding_left = header_geometry.padding_left;
|
||||
let header_text_x = -size.x / 2.0 + header_padding_left;
|
||||
let header_text_height = self.header.height.value();
|
||||
let header_padding_bottom = header_geometry.padding_bottom;
|
||||
let header_height = header_geometry.height;
|
||||
let header_bottom_y = size.y / 2.0 - header_height;
|
||||
let header_text_y = header_bottom_y + header_text_height + header_padding_bottom;
|
||||
self.header.set_position_xy(Vector2(header_text_x, header_text_y));
|
||||
self.update_header_width(size, header_geometry);
|
||||
|
||||
|
||||
// === Entries ===
|
||||
|
||||
self.entries.resize(size - Vector2(0.0, header_height));
|
||||
self.entries.set_position_y(-header_height / 2.0);
|
||||
}
|
||||
|
||||
fn update_header_width(&self, size: Vector2, header_geometry: HeaderGeometry) {
|
||||
let header_padding_left = header_geometry.padding_left;
|
||||
let header_padding_right = header_geometry.padding_right;
|
||||
let max_text_width = size.x - header_padding_left - header_padding_right;
|
||||
self.header.set_content_truncated(self.header_text.borrow().clone(), max_text_width);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============
|
||||
// === View ===
|
||||
// ============
|
||||
|
||||
/// A widget for displaying the entries and name of a component group.
|
||||
///
|
||||
/// The widget is rendered as a header label, a list of entries below it, and a colored background.
|
||||
///
|
||||
/// To learn more about component groups, see the [Component Browser Design
|
||||
/// Document](https://github.com/enso-org/design/blob/e6cffec2dd6d16688164f04a4ef0d9dff998c3e7/epics/component-browser/design.md).
|
||||
pub type View = component::ComponentView<Model, Frp>;
|
19
app/gui/view/component-browser/src/lib.rs
Normal file
19
app/gui/view/component-browser/src/lib.rs
Normal file
@ -0,0 +1,19 @@
|
||||
// === Standard Linter Configuration ===
|
||||
#![deny(non_ascii_idents)]
|
||||
#![warn(unsafe_code)]
|
||||
// === Non-Standard Linter Configuration ===
|
||||
#![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)]
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub use ide_view_component_group as component_group;
|
||||
|
@ -8,5 +8,6 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
debug-scene-component-group = { path = "component-group" }
|
||||
debug-scene-interface = { path = "interface" }
|
||||
debug-scene-visualization = { path = "visualization" }
|
||||
|
17
app/gui/view/debug_scene/component-group/Cargo.toml
Normal file
17
app/gui/view/debug_scene/component-group/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "debug-scene-component-group"
|
||||
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-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }
|
||||
ensogl-list-view = { path = "../../../../../lib/rust/ensogl/component/list-view" }
|
||||
ensogl-text-msdf-sys = { path = "../../../../../lib/rust/ensogl/component/text/msdf-sys" }
|
||||
ide-view-component-group = { path = "../../component-browser/component-group" }
|
||||
wasm-bindgen = { version = "0.2.78", features = ["nightly"] }
|
106
app/gui/view/debug_scene/component-group/src/lib.rs
Normal file
106
app/gui/view/debug_scene/component-group/src/lib.rs
Normal file
@ -0,0 +1,106 @@
|
||||
//! A debug scene which shows the Component Group visual component.
|
||||
|
||||
// === Standard Linter Configuration ===
|
||||
#![deny(non_ascii_idents)]
|
||||
#![warn(unsafe_code)]
|
||||
// === Non-Standard Linter Configuration ===
|
||||
#![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 ensogl_core::application::Application;
|
||||
use ensogl_core::data::color;
|
||||
use ensogl_core::display::object::ObjectOps;
|
||||
use ensogl_hardcoded_theme as theme;
|
||||
use ensogl_list_view as list_view;
|
||||
use ensogl_text_msdf_sys::run_once_initialized;
|
||||
use ide_view_component_group as component_group;
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
// === Entry Point ===
|
||||
// ===================
|
||||
|
||||
/// An entry point.
|
||||
#[entry_point]
|
||||
pub fn main() {
|
||||
run_once_initialized(|| {
|
||||
let app = Application::new("root");
|
||||
init(&app);
|
||||
mem::forget(app);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === Mock Entries ===
|
||||
// ====================
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct MockEntries {
|
||||
entries: Vec<String>,
|
||||
}
|
||||
|
||||
impl MockEntries {
|
||||
fn new(entries: Vec<String>) -> Self {
|
||||
Self { entries }
|
||||
}
|
||||
|
||||
fn get_entry(&self, i: usize) -> Option<String> {
|
||||
self.entries.get(i).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl list_view::entry::ModelProvider<list_view::entry::Label> for MockEntries {
|
||||
fn entry_count(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
fn get(&self, id: usize) -> Option<String> {
|
||||
self.get_entry(id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ========================
|
||||
// === Init Application ===
|
||||
// ========================
|
||||
|
||||
fn init(app: &Application) {
|
||||
theme::builtin::dark::register(&app);
|
||||
theme::builtin::light::register(&app);
|
||||
theme::builtin::light::enable(&app);
|
||||
|
||||
let mock_entries = MockEntries::new(vec![
|
||||
"long sample entry with text overflowing the width".into(),
|
||||
"convert".into(),
|
||||
"table input".into(),
|
||||
"text input".into(),
|
||||
"number input".into(),
|
||||
"table input".into(),
|
||||
"data output".into(),
|
||||
"data input".into(),
|
||||
]);
|
||||
|
||||
|
||||
let component_group = app.new_view::<component_group::View>();
|
||||
let provider = list_view::entry::AnyModelProvider::new(mock_entries);
|
||||
let group_name = "Long group name with text overflowing the width";
|
||||
component_group.set_header(group_name.to_string());
|
||||
component_group.set_entries(provider);
|
||||
component_group.set_size(Vector2(150.0, 200.0));
|
||||
component_group.set_background_color(color::Rgba(0.927, 0.937, 0.913, 1.0));
|
||||
app.display.add_child(&component_group);
|
||||
|
||||
std::mem::forget(component_group);
|
||||
}
|
@ -45,9 +45,9 @@ use uuid::Uuid;
|
||||
const STUB_MODULE: &str = "from Base import all\n\nmain = IO.println \"Hello\"\n";
|
||||
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[entry_point]
|
||||
#[allow(dead_code)]
|
||||
pub fn entry_point_interface() {
|
||||
pub fn main() {
|
||||
run_once_initialized(|| {
|
||||
let app = Application::new("root");
|
||||
init(&app);
|
||||
|
@ -20,5 +20,6 @@
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub use debug_scene_component_group as component_group;
|
||||
pub use debug_scene_interface as interface;
|
||||
pub use debug_scene_visualization as visualization;
|
||||
|
@ -92,9 +92,9 @@ fn constructor_graph() -> visualization::java_script::Definition {
|
||||
visualization::java_script::Definition::new_builtin(sources).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[entry_point]
|
||||
#[allow(dead_code, missing_docs)]
|
||||
pub fn entry_point_visualization() {
|
||||
pub fn main() {
|
||||
run_once_initialized(|| {
|
||||
let app = Application::new("root");
|
||||
init(&app);
|
||||
|
@ -198,7 +198,7 @@ commands.build.rust = async function (argv) {
|
||||
console.log('Minimizing the WASM binary.')
|
||||
await gzip(paths.wasm.main, paths.wasm.mainGz)
|
||||
|
||||
const limitMb = 4.67
|
||||
const limitMb = 4.05
|
||||
await checkWasmSize(paths.wasm.mainGz, limitMb)
|
||||
}
|
||||
// Copy WASM files from temporary directory to Webpack's `dist` directory.
|
||||
|
@ -179,6 +179,21 @@ define_themes! { [light:0, dark:1]
|
||||
hide_delay_duration_ms = 150.0, 150.0;
|
||||
show_delay_duration_ms = 150.0, 150.0;
|
||||
}
|
||||
component_browser {
|
||||
component_group {
|
||||
header {
|
||||
text {
|
||||
size = 12.0, 12.0;
|
||||
}
|
||||
height = 30.0, 30.0;
|
||||
padding {
|
||||
left = 16.5, 16.5;
|
||||
right = 2.5, 2.5;
|
||||
bottom = 5.0, 5.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
searcher {
|
||||
action_list_gap = 10.0, 10.0;
|
||||
padding = 5.0, 5.0;
|
||||
|
@ -66,6 +66,9 @@ pub trait Entry: CloneRef + Debug + display::Object + 'static {
|
||||
/// Update content with new model.
|
||||
fn update(&self, model: &Self::Model);
|
||||
|
||||
/// Resize the entry's view to fit a new width.
|
||||
fn set_max_width(&self, max_width_px: f32);
|
||||
|
||||
/// Set the layer of all [`text::Area`] components inside. The [`text::Area`] component is
|
||||
/// handled in a special way, and is often in different layer than shapes. See TODO comment
|
||||
/// in [`text::Area::add_to_scene_layer`] method.
|
||||
@ -84,10 +87,19 @@ pub trait Entry: CloneRef + Debug + display::Object + 'static {
|
||||
pub struct Label {
|
||||
display_object: display::object::Instance,
|
||||
label: text::Area,
|
||||
text: Rc<RefCell<String>>,
|
||||
max_width_px: Rc<Cell<f32>>,
|
||||
network: enso_frp::Network,
|
||||
style_watch: StyleWatchFrp,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
fn update_label_content(&self) {
|
||||
let text = self.text.borrow().clone();
|
||||
self.label.set_content_truncated(text, self.max_width_px.get());
|
||||
}
|
||||
}
|
||||
|
||||
impl Entry for Label {
|
||||
type Model = String;
|
||||
|
||||
@ -95,6 +107,8 @@ impl Entry for Label {
|
||||
let logger = Logger::new("list_view::entry::Label");
|
||||
let display_object = display::object::Instance::new(logger);
|
||||
let label = app.new_view::<ensogl_text::Area>();
|
||||
let text = default();
|
||||
let max_width_px = default();
|
||||
let network = frp::Network::new("list_view::entry::Label");
|
||||
let style_watch = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
|
||||
let color = style_watch.get_color(theme::widget::list_view::text);
|
||||
@ -111,11 +125,19 @@ impl Entry for Label {
|
||||
eval size ((size) label.set_position_y(size/2.0));
|
||||
}
|
||||
init.emit(());
|
||||
Self { display_object, label, network, style_watch }
|
||||
Self { display_object, label, text, max_width_px, network, style_watch }
|
||||
}
|
||||
|
||||
fn update(&self, model: &Self::Model) {
|
||||
self.label.set_content(model);
|
||||
self.text.replace(model.clone());
|
||||
self.update_label_content();
|
||||
}
|
||||
|
||||
fn set_max_width(&self, max_width_px: f32) {
|
||||
if self.max_width_px.get() != max_width_px {
|
||||
self.max_width_px.set(max_width_px);
|
||||
self.update_label_content();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_label_layer(&self, label_layer: &display::scene::Layer) {
|
||||
@ -176,6 +198,10 @@ impl Entry for GlyphHighlightedLabel {
|
||||
self.highlight.emit(&model.highlighted);
|
||||
}
|
||||
|
||||
fn set_max_width(&self, max_width_px: f32) {
|
||||
self.inner.set_max_width(max_width_px);
|
||||
}
|
||||
|
||||
fn set_label_layer(&self, layer: &display::scene::Layer) {
|
||||
self.inner.set_label_layer(layer);
|
||||
}
|
||||
|
@ -127,8 +127,9 @@ where E::Model: Default
|
||||
}
|
||||
}
|
||||
|
||||
/// Update displayed entries to show the given range.
|
||||
pub fn update_entries(&self, mut range: Range<entry::Id>) {
|
||||
/// Update displayed entries to show the given range and limit their display width to at most
|
||||
/// `max_width_px`.
|
||||
pub fn update_entries(&self, mut range: Range<entry::Id>, max_width_px: f32) {
|
||||
range.end = range.end.min(self.provider.get().entry_count());
|
||||
if range != self.entries_range.get() {
|
||||
debug!(self.logger, "Update entries for {range:?}");
|
||||
@ -152,13 +153,18 @@ where E::Model: Default
|
||||
});
|
||||
self.entries_range.set(range);
|
||||
}
|
||||
for entry in self.entries.borrow().iter() {
|
||||
entry.entry.set_max_width(max_width_px);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update displayed entries, giving new provider.
|
||||
/// Update displayed entries, giving new provider. New entries created by the function have
|
||||
/// their maximum width set to `max_width_px`.
|
||||
pub fn update_entries_new_provider(
|
||||
&self,
|
||||
provider: impl Into<entry::AnyModelProvider<E>> + 'static,
|
||||
mut range: Range<entry::Id>,
|
||||
max_width_px: f32,
|
||||
) {
|
||||
const MAX_SAFE_ENTRIES_COUNT: usize = 1000;
|
||||
let provider = provider.into();
|
||||
@ -173,7 +179,12 @@ where E::Model: Default
|
||||
range.end = range.end.min(provider.entry_count());
|
||||
let models = range.clone().map(|id| (id, provider.get(id)));
|
||||
let mut entries = self.entries.borrow_mut();
|
||||
entries.resize_with(range.len(), || self.create_new_entry());
|
||||
let create_new_entry_with_max_width = || {
|
||||
let entry = self.create_new_entry();
|
||||
entry.entry.set_max_width(max_width_px);
|
||||
entry
|
||||
};
|
||||
entries.resize_with(range.len(), create_new_entry_with_max_width);
|
||||
for (entry, (id, model)) in entries.iter().zip(models) {
|
||||
Self::update_entry(&self.logger, entry, id, &model);
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ use enso_frp as frp;
|
||||
use ensogl_core::application;
|
||||
use ensogl_core::application::shortcut;
|
||||
use ensogl_core::application::Application;
|
||||
use ensogl_core::data::color;
|
||||
use ensogl_core::display;
|
||||
use ensogl_core::display::scene::layer::LayerId;
|
||||
use ensogl_core::display::shape::*;
|
||||
@ -98,16 +99,16 @@ pub mod background {
|
||||
|
||||
ensogl_core::define_shape_system! {
|
||||
below = [selection];
|
||||
(style:Style) {
|
||||
(style: Style, shadow_alpha: f32, corners_radius_px: f32, color: Vector4) {
|
||||
let sprite_width : Var<Pixels> = "input_size.x".into();
|
||||
let sprite_height : Var<Pixels> = "input_size.y".into();
|
||||
let width = sprite_width - SHADOW_PX.px() * 2.0 - SHAPE_PADDING.px() * 2.0;
|
||||
let height = sprite_height - SHADOW_PX.px() * 2.0 - SHAPE_PADDING.px() * 2.0;
|
||||
let color = style.get_color(theme::widget::list_view::background);
|
||||
let rect = Rect((&width,&height)).corners_radius(CORNER_RADIUS_PX.px());
|
||||
let color = Var::<color::Rgba>::from(color);
|
||||
let rect = Rect((&width,&height)).corners_radius(corners_radius_px);
|
||||
let shape = rect.fill(color);
|
||||
|
||||
let shadow = shadow::from_shape(rect.into(),style);
|
||||
let shadow = shadow::from_shape_with_alpha(rect.into(), &shadow_alpha, style);
|
||||
|
||||
(shadow + shape).into()
|
||||
}
|
||||
@ -154,6 +155,11 @@ impl<E: Entry> Model<E> {
|
||||
Model { app, entries, selection, background, scrolled_area, display_object }
|
||||
}
|
||||
|
||||
fn show_background_shadow(&self, value: bool) {
|
||||
let alpha = if value { 1.0 } else { 0.0 };
|
||||
self.background.shadow_alpha.set(alpha);
|
||||
}
|
||||
|
||||
fn padding(&self) -> f32 {
|
||||
// FIXME : StyleWatch is unsuitable here, as it was designed as an internal tool for shape
|
||||
// system (#795)
|
||||
@ -161,23 +167,29 @@ impl<E: Entry> Model<E> {
|
||||
styles.get_number(ensogl_hardcoded_theme::application::searcher::padding)
|
||||
}
|
||||
|
||||
fn doubled_padding_with_shape_padding(&self) -> f32 {
|
||||
2.0 * self.padding() + SHAPE_PADDING
|
||||
}
|
||||
|
||||
/// Update the displayed entries list when _view_ has changed - the list was scrolled or
|
||||
/// resized.
|
||||
fn update_after_view_change(&self, view: &View) {
|
||||
let visible_entries = Self::visible_entries(view, self.entries.entry_count());
|
||||
let padding_px = self.padding();
|
||||
let padding = 2.0 * padding_px + SHAPE_PADDING;
|
||||
let padding = self.doubled_padding_with_shape_padding();
|
||||
let padding = Vector2(padding, padding);
|
||||
let entry_width = view.size.x - padding.x;
|
||||
let shadow = Vector2(2.0 * SHADOW_PX, 2.0 * SHADOW_PX);
|
||||
self.entries.set_position_x(-view.size.x / 2.0);
|
||||
self.background.size.set(view.size + padding + shadow);
|
||||
self.scrolled_area.set_position_y(view.size.y / 2.0 - view.position_y);
|
||||
self.entries.update_entries(visible_entries);
|
||||
self.entries.update_entries(visible_entries, entry_width);
|
||||
}
|
||||
|
||||
fn set_entries(&self, provider: entry::AnyModelProvider<E>, view: &View) {
|
||||
let visible_entries = Self::visible_entries(view, provider.entry_count());
|
||||
self.entries.update_entries_new_provider(provider, visible_entries);
|
||||
let padding = self.doubled_padding_with_shape_padding();
|
||||
let entry_width = view.size.x - padding;
|
||||
self.entries.update_entries_new_provider(provider, visible_entries, entry_width);
|
||||
}
|
||||
|
||||
fn visible_entries(View { position_y, size }: &View, entry_count: usize) -> Range<entry::Id> {
|
||||
@ -250,18 +262,21 @@ ensogl_core::define_endpoints! {
|
||||
/// Deselect all entries.
|
||||
deselect_entries(),
|
||||
|
||||
resize (Vector2<f32>),
|
||||
scroll_jump (f32),
|
||||
set_entries (entry::AnyModelProvider<E>),
|
||||
select_entry (entry::Id),
|
||||
chose_entry (entry::Id),
|
||||
resize(Vector2<f32>),
|
||||
scroll_jump(f32),
|
||||
set_entries(entry::AnyModelProvider<E>),
|
||||
select_entry(entry::Id),
|
||||
chose_entry(entry::Id),
|
||||
show_background_shadow(bool),
|
||||
set_background_corners_radius(f32),
|
||||
set_background_color(color::Rgba),
|
||||
}
|
||||
|
||||
Output {
|
||||
selected_entry (Option<entry::Id>),
|
||||
chosen_entry (Option<entry::Id>),
|
||||
size (Vector2<f32>),
|
||||
scroll_position (f32),
|
||||
selected_entry(Option<entry::Id>),
|
||||
chosen_entry(Option<entry::Id>),
|
||||
size(Vector2<f32>),
|
||||
scroll_position(f32),
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,9 +326,28 @@ where E::Model: Default
|
||||
let view_y = DEPRECATED_Animation::<f32>::new(network);
|
||||
let selection_y = DEPRECATED_Animation::<f32>::new(network);
|
||||
let selection_height = DEPRECATED_Animation::<f32>::new(network);
|
||||
let style = StyleWatchFrp::new(&scene.style_sheet);
|
||||
use theme::widget::list_view as list_view_style;
|
||||
let default_background_color = style.get_color(list_view_style::background);
|
||||
|
||||
frp::extend! { network
|
||||
|
||||
// === Background ===
|
||||
|
||||
init <- source_();
|
||||
default_show_background_shadow <- init.constant(true);
|
||||
show_background_shadow <- any(
|
||||
&default_show_background_shadow,&frp.show_background_shadow);
|
||||
eval show_background_shadow ((t) model.show_background_shadow(*t));
|
||||
default_background_corners_radius <- init.constant(background::CORNER_RADIUS_PX);
|
||||
background_corners_radius <- any(
|
||||
&default_background_corners_radius,&frp.set_background_corners_radius);
|
||||
eval background_corners_radius ((px) model.background.corners_radius_px.set(*px));
|
||||
default_background_color <- all(&default_background_color,&init)._0();
|
||||
background_color <- any(&default_background_color,&frp.set_background_color);
|
||||
eval background_color ((color) model.background.color.set(color.into()));
|
||||
|
||||
|
||||
// === Mouse Position ===
|
||||
|
||||
mouse_in <- all_with(&mouse.position,&frp.size,f!((pos,size)
|
||||
@ -329,6 +363,7 @@ where E::Model: Default
|
||||
|
||||
|
||||
// === Selected Entry ===
|
||||
|
||||
frp.source.selected_entry <+ frp.select_entry.map(|id| Some(*id));
|
||||
|
||||
selection_jump_on_one_up <- frp.move_selection_up.constant(-1);
|
||||
@ -452,6 +487,7 @@ where E::Model: Default
|
||||
));
|
||||
}
|
||||
|
||||
init.emit(());
|
||||
view_y.set_target_value(MAX_SCROLL);
|
||||
view_y.skip();
|
||||
frp.scroll_jump(MAX_SCROLL);
|
||||
|
@ -16,7 +16,6 @@ enso-types = { path = "../../../types" }
|
||||
ensogl-core = { path = "../../core" }
|
||||
ensogl-text-embedded-fonts = { path = "embedded-fonts" }
|
||||
ensogl-text-msdf-sys = { path = "msdf-sys" }
|
||||
ensogl-hardcoded-theme = { path = "../../app/theme/hardcoded" }
|
||||
const_format = "0.2.22"
|
||||
xi-rope = { version = "0.3.0" }
|
||||
|
||||
|
@ -75,16 +75,8 @@ mod deja_vu {
|
||||
std::io::copy(&mut input_stream, &mut output_stream).unwrap();
|
||||
}
|
||||
|
||||
pub const FONTS_TO_EXTRACT: &[&str] = &[
|
||||
"DejaVuSans",
|
||||
"DejaVuSans-ExtraLight",
|
||||
"DejaVuSansMono",
|
||||
"DejaVuSansMono-Bold",
|
||||
"DejaVuSansMono-Oblique",
|
||||
"DejaVuSansCondensed",
|
||||
"DejaVuSerif",
|
||||
"DejaVuSerifCondensed",
|
||||
];
|
||||
pub const FONTS_TO_EXTRACT: &[&str] =
|
||||
&["DejaVuSans", "DejaVuSans-Bold", "DejaVuSansMono", "DejaVuSansMono-Bold"];
|
||||
|
||||
pub fn extract_all_fonts(package_path: &path::Path) {
|
||||
for font_name in FONTS_TO_EXTRACT {
|
||||
|
@ -271,6 +271,12 @@ ensogl_core::define_endpoints! {
|
||||
/// MSDF texture, etc.).
|
||||
set_font (String),
|
||||
set_content (String),
|
||||
/// Set content, truncating the trailing characters on every line to fit a width in pixels
|
||||
/// when rendered with current font and font size. The truncated substrings are replaced
|
||||
/// with an ellipsis character ("…").
|
||||
///
|
||||
/// Unix (`\n`) and MS-DOS (`\r\n`) style line endings are recognized.
|
||||
set_content_truncated (String, f32),
|
||||
}
|
||||
Output {
|
||||
pointer_style (cursor::Style),
|
||||
@ -480,6 +486,10 @@ impl Area {
|
||||
input.insert(s);
|
||||
input.remove_all_cursors();
|
||||
});
|
||||
input.set_content <+ input.set_content_truncated.map(f!(((text, max_width_px)) {
|
||||
m.text_truncated_with_ellipsis(text.clone(), m.default_font_size(), *max_width_px)
|
||||
}));
|
||||
|
||||
|
||||
// === Font ===
|
||||
|
||||
@ -871,6 +881,86 @@ impl AreaModel {
|
||||
last_offset - cursor_offset
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn line_truncated_with_ellipsis(&self, line: &str, _: style::Size, _: f32) -> String {
|
||||
line.to_string()
|
||||
}
|
||||
|
||||
/// Truncate a `line` of text if its length on screen exceeds `max_width_px` when rendered
|
||||
/// using the current font at `font_size`. Return the truncated string with an ellipsis ("…")
|
||||
/// character appended, or `content` if not truncated.
|
||||
///
|
||||
/// The truncation point is chosen such that the resulting string with ellipsis will fit in
|
||||
/// `max_width_px` if possible. The `line` must not contain newline characters.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn line_truncated_with_ellipsis(
|
||||
&self,
|
||||
line: &str,
|
||||
font_size: style::Size,
|
||||
max_width_px: f32,
|
||||
) -> String {
|
||||
const ELLIPSIS: char = '\u{2026}';
|
||||
let mut pen = pen::Pen::new(&self.glyph_system.borrow().font);
|
||||
let mut truncation_point = 0.bytes();
|
||||
let truncate = line.char_indices().any(|(i, ch)| {
|
||||
let char_info = pen::CharInfo::new(ch, font_size.raw);
|
||||
let pen_info = pen.advance(Some(char_info));
|
||||
let next_width = pen_info.offset + char_info.size;
|
||||
if next_width > max_width_px {
|
||||
return true;
|
||||
}
|
||||
let width_of_ellipsis = pen::CharInfo::new(ELLIPSIS, font_size.raw).size;
|
||||
let char_length: Bytes = ch.len_utf8().into();
|
||||
if next_width + width_of_ellipsis <= max_width_px {
|
||||
truncation_point = Bytes::from(i) + char_length;
|
||||
}
|
||||
false
|
||||
});
|
||||
if truncate {
|
||||
let truncated_content = line[..truncation_point.as_usize()].to_string();
|
||||
truncated_content + String::from(ELLIPSIS).as_str()
|
||||
} else {
|
||||
line.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Truncate trailing characters on every line of `text` that exceeds `max_width_px` when
|
||||
/// rendered using the current font at `font_size`. Return `text` with every truncated
|
||||
/// substring replaced with an ellipsis character ("…").
|
||||
///
|
||||
/// The truncation point of every line is chosen such that the truncated string with ellipsis
|
||||
/// will fit in `max_width_px` if possible. Unix (`\n`) and MS-DOS (`\r\n`) style line endings
|
||||
/// are recognized and preserved in the returned string.
|
||||
fn text_truncated_with_ellipsis(
|
||||
&self,
|
||||
text: String,
|
||||
font_size: style::Size,
|
||||
max_width_px: f32,
|
||||
) -> String {
|
||||
let lines = text.split_inclusive('\n');
|
||||
/// Return the length of a trailing Unix (`\n`) or MS-DOS (`\r\n`) style line ending in
|
||||
/// `s`, or 0 if not found.
|
||||
fn length_of_trailing_line_ending(s: &str) -> usize {
|
||||
if s.ends_with("\r\n") {
|
||||
2
|
||||
} else if s.ends_with('\n') {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
let tuples_of_lines_and_endings =
|
||||
lines.map(|line| line.split_at(line.len() - length_of_trailing_line_ending(line)));
|
||||
let lines_truncated_with_ellipsis = tuples_of_lines_and_endings.map(|(line, ending)| {
|
||||
self.line_truncated_with_ellipsis(line, font_size, max_width_px) + ending
|
||||
});
|
||||
lines_truncated_with_ellipsis.collect()
|
||||
}
|
||||
|
||||
fn default_font_size(&self) -> style::Size {
|
||||
*self.buffer.style.get().size.default()
|
||||
}
|
||||
|
||||
fn new_line(&self, index: usize) -> Line {
|
||||
let line = Line::new(&self.logger);
|
||||
let y_offset = -((index + 1) as f32) * LINE_HEIGHT + LINE_VERTICAL_OFFSET;
|
||||
|
@ -1097,6 +1097,9 @@ impl<T> ShapeSystemInfoTemplate<T> {
|
||||
/// scene.layers.add_shapes_order_dependency::<shape::View, input::port::hover::View>();
|
||||
/// scene.layers.add_shapes_order_dependency::<input::port::hover::View, input::port::viz::View>();
|
||||
/// ```
|
||||
///
|
||||
/// A shape listed on the left side of an arrow (`->`) will be ordered below the shape listed on
|
||||
/// the right side of the arrow.
|
||||
#[macro_export]
|
||||
macro_rules! shapes_order_dependencies {
|
||||
($scene:expr => {
|
||||
|
@ -7,6 +7,7 @@ use crate::prelude::*;
|
||||
// ===================
|
||||
|
||||
fn crate_name_to_fn_name(name: &str) -> String {
|
||||
let name = name.replace("debug-scene-", "");
|
||||
let name = name.replace("ensogl-example-", "");
|
||||
let name = name.replace("enso-example-", "");
|
||||
let name = name.replace("enso-", "");
|
||||
|
Loading…
Reference in New Issue
Block a user