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:
Mateusz Czapliński 2022-04-14 12:37:40 +02:00 committed by GitHub
parent 0ea5dc2a6f
commit e75df61b2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 664 additions and 40 deletions

View File

@ -2,6 +2,9 @@
#### Visual Environment #### 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 - [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 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 surrounding nodes horizontally and vertically. This helps to preserve a nice
@ -154,6 +157,7 @@
[3349]: https://github.com/enso-org/enso/pull/3349 [3349]: https://github.com/enso-org/enso/pull/3349
[3361]: https://github.com/enso-org/enso/pull/3361 [3361]: https://github.com/enso-org/enso/pull/3361
[3364]: https://github.com/enso-org/enso/pull/3364 [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 [3377]: https://github.com/enso-org/enso/pull/3377
[3366]: https://github.com/enso-org/enso/pull/3366 [3366]: https://github.com/enso-org/enso/pull/3366
[3379]: https://github.com/enso-org/enso/pull/3379 [3379]: https://github.com/enso-org/enso/pull/3379

27
Cargo.lock generated
View File

@ -739,6 +739,19 @@ dependencies = [
"syn", "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]] [[package]]
name = "debug-scene-interface" name = "debug-scene-interface"
version = "0.1.0" version = "0.1.0"
@ -941,6 +954,7 @@ dependencies = [
name = "enso-debug-scene" name = "enso-debug-scene"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"debug-scene-component-group",
"debug-scene-interface", "debug-scene-interface",
"debug-scene-visualization", "debug-scene-visualization",
] ]
@ -1706,7 +1720,6 @@ dependencies = [
"enso-text", "enso-text",
"enso-types", "enso-types",
"ensogl-core", "ensogl-core",
"ensogl-hardcoded-theme",
"ensogl-text-embedded-fonts", "ensogl-text-embedded-fonts",
"ensogl-text-msdf-sys", "ensogl-text-msdf-sys",
"wasm-bindgen-test", "wasm-bindgen-test",
@ -2257,6 +2270,18 @@ dependencies = [
"welcome-screen", "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]] [[package]]
name = "ide-view-graph-editor" name = "ide-view-graph-editor"
version = "0.1.0" version = "0.1.0"

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

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

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

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

View File

@ -8,5 +8,6 @@ edition = "2021"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
debug-scene-component-group = { path = "component-group" }
debug-scene-interface = { path = "interface" } debug-scene-interface = { path = "interface" }
debug-scene-visualization = { path = "visualization" } debug-scene-visualization = { path = "visualization" }

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

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

View File

@ -45,9 +45,9 @@ use uuid::Uuid;
const STUB_MODULE: &str = "from Base import all\n\nmain = IO.println \"Hello\"\n"; const STUB_MODULE: &str = "from Base import all\n\nmain = IO.println \"Hello\"\n";
#[wasm_bindgen] #[entry_point]
#[allow(dead_code)] #[allow(dead_code)]
pub fn entry_point_interface() { pub fn main() {
run_once_initialized(|| { run_once_initialized(|| {
let app = Application::new("root"); let app = Application::new("root");
init(&app); init(&app);

View File

@ -20,5 +20,6 @@
// === Export === // === Export ===
// ============== // ==============
pub use debug_scene_component_group as component_group;
pub use debug_scene_interface as interface; pub use debug_scene_interface as interface;
pub use debug_scene_visualization as visualization; pub use debug_scene_visualization as visualization;

View File

@ -92,9 +92,9 @@ fn constructor_graph() -> visualization::java_script::Definition {
visualization::java_script::Definition::new_builtin(sources).unwrap() visualization::java_script::Definition::new_builtin(sources).unwrap()
} }
#[wasm_bindgen] #[entry_point]
#[allow(dead_code, missing_docs)] #[allow(dead_code, missing_docs)]
pub fn entry_point_visualization() { pub fn main() {
run_once_initialized(|| { run_once_initialized(|| {
let app = Application::new("root"); let app = Application::new("root");
init(&app); init(&app);

View File

@ -198,7 +198,7 @@ commands.build.rust = async function (argv) {
console.log('Minimizing the WASM binary.') console.log('Minimizing the WASM binary.')
await gzip(paths.wasm.main, paths.wasm.mainGz) await gzip(paths.wasm.main, paths.wasm.mainGz)
const limitMb = 4.67 const limitMb = 4.05
await checkWasmSize(paths.wasm.mainGz, limitMb) await checkWasmSize(paths.wasm.mainGz, limitMb)
} }
// Copy WASM files from temporary directory to Webpack's `dist` directory. // Copy WASM files from temporary directory to Webpack's `dist` directory.

View File

@ -179,6 +179,21 @@ define_themes! { [light:0, dark:1]
hide_delay_duration_ms = 150.0, 150.0; hide_delay_duration_ms = 150.0, 150.0;
show_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 { searcher {
action_list_gap = 10.0, 10.0; action_list_gap = 10.0, 10.0;
padding = 5.0, 5.0; padding = 5.0, 5.0;

View File

@ -66,6 +66,9 @@ pub trait Entry: CloneRef + Debug + display::Object + 'static {
/// Update content with new model. /// Update content with new model.
fn update(&self, model: &Self::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 /// 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 /// handled in a special way, and is often in different layer than shapes. See TODO comment
/// in [`text::Area::add_to_scene_layer`] method. /// in [`text::Area::add_to_scene_layer`] method.
@ -84,10 +87,19 @@ pub trait Entry: CloneRef + Debug + display::Object + 'static {
pub struct Label { pub struct Label {
display_object: display::object::Instance, display_object: display::object::Instance,
label: text::Area, label: text::Area,
text: Rc<RefCell<String>>,
max_width_px: Rc<Cell<f32>>,
network: enso_frp::Network, network: enso_frp::Network,
style_watch: StyleWatchFrp, 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 { impl Entry for Label {
type Model = String; type Model = String;
@ -95,6 +107,8 @@ impl Entry for Label {
let logger = Logger::new("list_view::entry::Label"); let logger = Logger::new("list_view::entry::Label");
let display_object = display::object::Instance::new(logger); let display_object = display::object::Instance::new(logger);
let label = app.new_view::<ensogl_text::Area>(); 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 network = frp::Network::new("list_view::entry::Label");
let style_watch = StyleWatchFrp::new(&app.display.default_scene.style_sheet); let style_watch = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let color = style_watch.get_color(theme::widget::list_view::text); 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)); eval size ((size) label.set_position_y(size/2.0));
} }
init.emit(()); 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) { 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) { fn set_label_layer(&self, label_layer: &display::scene::Layer) {
@ -176,6 +198,10 @@ impl Entry for GlyphHighlightedLabel {
self.highlight.emit(&model.highlighted); 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) { fn set_label_layer(&self, layer: &display::scene::Layer) {
self.inner.set_label_layer(layer); self.inner.set_label_layer(layer);
} }

View File

@ -127,8 +127,9 @@ where E::Model: Default
} }
} }
/// Update displayed entries to show the given range. /// Update displayed entries to show the given range and limit their display width to at most
pub fn update_entries(&self, mut range: Range<entry::Id>) { /// `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()); range.end = range.end.min(self.provider.get().entry_count());
if range != self.entries_range.get() { if range != self.entries_range.get() {
debug!(self.logger, "Update entries for {range:?}"); debug!(self.logger, "Update entries for {range:?}");
@ -152,13 +153,18 @@ where E::Model: Default
}); });
self.entries_range.set(range); 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( pub fn update_entries_new_provider(
&self, &self,
provider: impl Into<entry::AnyModelProvider<E>> + 'static, provider: impl Into<entry::AnyModelProvider<E>> + 'static,
mut range: Range<entry::Id>, mut range: Range<entry::Id>,
max_width_px: f32,
) { ) {
const MAX_SAFE_ENTRIES_COUNT: usize = 1000; const MAX_SAFE_ENTRIES_COUNT: usize = 1000;
let provider = provider.into(); let provider = provider.into();
@ -173,7 +179,12 @@ where E::Model: Default
range.end = range.end.min(provider.entry_count()); range.end = range.end.min(provider.entry_count());
let models = range.clone().map(|id| (id, provider.get(id))); let models = range.clone().map(|id| (id, provider.get(id)));
let mut entries = self.entries.borrow_mut(); 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) { for (entry, (id, model)) in entries.iter().zip(models) {
Self::update_entry(&self.logger, entry, id, &model); Self::update_entry(&self.logger, entry, id, &model);
} }

View File

@ -39,6 +39,7 @@ use enso_frp as frp;
use ensogl_core::application; use ensogl_core::application;
use ensogl_core::application::shortcut; use ensogl_core::application::shortcut;
use ensogl_core::application::Application; use ensogl_core::application::Application;
use ensogl_core::data::color;
use ensogl_core::display; use ensogl_core::display;
use ensogl_core::display::scene::layer::LayerId; use ensogl_core::display::scene::layer::LayerId;
use ensogl_core::display::shape::*; use ensogl_core::display::shape::*;
@ -98,16 +99,16 @@ pub mod background {
ensogl_core::define_shape_system! { ensogl_core::define_shape_system! {
below = [selection]; 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_width : Var<Pixels> = "input_size.x".into();
let sprite_height : Var<Pixels> = "input_size.y".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 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 height = sprite_height - SHADOW_PX.px() * 2.0 - SHAPE_PADDING.px() * 2.0;
let color = style.get_color(theme::widget::list_view::background); let color = Var::<color::Rgba>::from(color);
let rect = Rect((&width,&height)).corners_radius(CORNER_RADIUS_PX.px()); let rect = Rect((&width,&height)).corners_radius(corners_radius_px);
let shape = rect.fill(color); 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() (shadow + shape).into()
} }
@ -154,6 +155,11 @@ impl<E: Entry> Model<E> {
Model { app, entries, selection, background, scrolled_area, display_object } 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 { fn padding(&self) -> f32 {
// FIXME : StyleWatch is unsuitable here, as it was designed as an internal tool for shape // FIXME : StyleWatch is unsuitable here, as it was designed as an internal tool for shape
// system (#795) // system (#795)
@ -161,23 +167,29 @@ impl<E: Entry> Model<E> {
styles.get_number(ensogl_hardcoded_theme::application::searcher::padding) 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 /// Update the displayed entries list when _view_ has changed - the list was scrolled or
/// resized. /// resized.
fn update_after_view_change(&self, view: &View) { fn update_after_view_change(&self, view: &View) {
let visible_entries = Self::visible_entries(view, self.entries.entry_count()); let visible_entries = Self::visible_entries(view, self.entries.entry_count());
let padding_px = self.padding(); let padding = self.doubled_padding_with_shape_padding();
let padding = 2.0 * padding_px + SHAPE_PADDING;
let padding = Vector2(padding, 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); let shadow = Vector2(2.0 * SHADOW_PX, 2.0 * SHADOW_PX);
self.entries.set_position_x(-view.size.x / 2.0); self.entries.set_position_x(-view.size.x / 2.0);
self.background.size.set(view.size + padding + shadow); self.background.size.set(view.size + padding + shadow);
self.scrolled_area.set_position_y(view.size.y / 2.0 - view.position_y); 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) { fn set_entries(&self, provider: entry::AnyModelProvider<E>, view: &View) {
let visible_entries = Self::visible_entries(view, provider.entry_count()); 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> { 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 all entries.
deselect_entries(), deselect_entries(),
resize (Vector2<f32>), resize(Vector2<f32>),
scroll_jump (f32), scroll_jump(f32),
set_entries (entry::AnyModelProvider<E>), set_entries(entry::AnyModelProvider<E>),
select_entry (entry::Id), select_entry(entry::Id),
chose_entry (entry::Id), chose_entry(entry::Id),
show_background_shadow(bool),
set_background_corners_radius(f32),
set_background_color(color::Rgba),
} }
Output { Output {
selected_entry (Option<entry::Id>), selected_entry(Option<entry::Id>),
chosen_entry (Option<entry::Id>), chosen_entry(Option<entry::Id>),
size (Vector2<f32>), size(Vector2<f32>),
scroll_position (f32), scroll_position(f32),
} }
} }
@ -311,9 +326,28 @@ where E::Model: Default
let view_y = DEPRECATED_Animation::<f32>::new(network); let view_y = DEPRECATED_Animation::<f32>::new(network);
let selection_y = DEPRECATED_Animation::<f32>::new(network); let selection_y = DEPRECATED_Animation::<f32>::new(network);
let selection_height = 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 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 Position ===
mouse_in <- all_with(&mouse.position,&frp.size,f!((pos,size) mouse_in <- all_with(&mouse.position,&frp.size,f!((pos,size)
@ -329,6 +363,7 @@ where E::Model: Default
// === Selected Entry === // === Selected Entry ===
frp.source.selected_entry <+ frp.select_entry.map(|id| Some(*id)); frp.source.selected_entry <+ frp.select_entry.map(|id| Some(*id));
selection_jump_on_one_up <- frp.move_selection_up.constant(-1); 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.set_target_value(MAX_SCROLL);
view_y.skip(); view_y.skip();
frp.scroll_jump(MAX_SCROLL); frp.scroll_jump(MAX_SCROLL);

View File

@ -16,7 +16,6 @@ enso-types = { path = "../../../types" }
ensogl-core = { path = "../../core" } ensogl-core = { path = "../../core" }
ensogl-text-embedded-fonts = { path = "embedded-fonts" } ensogl-text-embedded-fonts = { path = "embedded-fonts" }
ensogl-text-msdf-sys = { path = "msdf-sys" } ensogl-text-msdf-sys = { path = "msdf-sys" }
ensogl-hardcoded-theme = { path = "../../app/theme/hardcoded" }
const_format = "0.2.22" const_format = "0.2.22"
xi-rope = { version = "0.3.0" } xi-rope = { version = "0.3.0" }

View File

@ -75,16 +75,8 @@ mod deja_vu {
std::io::copy(&mut input_stream, &mut output_stream).unwrap(); std::io::copy(&mut input_stream, &mut output_stream).unwrap();
} }
pub const FONTS_TO_EXTRACT: &[&str] = &[ pub const FONTS_TO_EXTRACT: &[&str] =
"DejaVuSans", &["DejaVuSans", "DejaVuSans-Bold", "DejaVuSansMono", "DejaVuSansMono-Bold"];
"DejaVuSans-ExtraLight",
"DejaVuSansMono",
"DejaVuSansMono-Bold",
"DejaVuSansMono-Oblique",
"DejaVuSansCondensed",
"DejaVuSerif",
"DejaVuSerifCondensed",
];
pub fn extract_all_fonts(package_path: &path::Path) { pub fn extract_all_fonts(package_path: &path::Path) {
for font_name in FONTS_TO_EXTRACT { for font_name in FONTS_TO_EXTRACT {

View File

@ -271,6 +271,12 @@ ensogl_core::define_endpoints! {
/// MSDF texture, etc.). /// MSDF texture, etc.).
set_font (String), set_font (String),
set_content (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 { Output {
pointer_style (cursor::Style), pointer_style (cursor::Style),
@ -480,6 +486,10 @@ impl Area {
input.insert(s); input.insert(s);
input.remove_all_cursors(); 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 === // === Font ===
@ -871,6 +881,86 @@ impl AreaModel {
last_offset - cursor_offset 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 { fn new_line(&self, index: usize) -> Line {
let line = Line::new(&self.logger); let line = Line::new(&self.logger);
let y_offset = -((index + 1) as f32) * LINE_HEIGHT + LINE_VERTICAL_OFFSET; let y_offset = -((index + 1) as f32) * LINE_HEIGHT + LINE_VERTICAL_OFFSET;

View File

@ -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::<shape::View, input::port::hover::View>();
/// scene.layers.add_shapes_order_dependency::<input::port::hover::View, input::port::viz::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_export]
macro_rules! shapes_order_dependencies { macro_rules! shapes_order_dependencies {
($scene:expr => { ($scene:expr => {

View File

@ -7,6 +7,7 @@ use crate::prelude::*;
// =================== // ===================
fn crate_name_to_fn_name(name: &str) -> String { 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("ensogl-example-", "");
let name = name.replace("enso-example-", ""); let name = name.replace("enso-example-", "");
let name = name.replace("enso-", ""); let name = name.replace("enso-", "");