diff --git a/CHANGELOG.md b/CHANGELOG.md index 33dd7250e32d..45796336123e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 4406bfc8e182..c43935069a42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/app/gui/view/component-browser/Cargo.toml b/app/gui/view/component-browser/Cargo.toml new file mode 100644 index 000000000000..9c9e6f7bd626 --- /dev/null +++ b/app/gui/view/component-browser/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ide-view-component-browser" +version = "0.1.0" +authors = ["Enso Team "] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +ide-view-component-group = { path = "component-group" } diff --git a/app/gui/view/component-browser/component-group/Cargo.toml b/app/gui/view/component-browser/component-group/Cargo.toml new file mode 100644 index 000000000000..4d51662da6fd --- /dev/null +++ b/app/gui/view/component-browser/component-group/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ide-view-component-group" +version = "0.1.0" +authors = ["Enso Team "] +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" } + diff --git a/app/gui/view/component-browser/component-group/src/lib.rs b/app/gui/view/component-browser/component-group/src/lib.rs new file mode 100644 index 000000000000..c362fb540262 --- /dev/null +++ b/app/gui/view/component-browser/component-group/src/lib.rs @@ -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 = "input_size.x".into(); + let sprite_height: Var = "input_size.y".into(); + let color = Var::::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 { + 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), + set_background_color(Rgba), + set_size(Vector2), + } + Output {} +} + +impl component::Frp 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>, + background: background::View, + entries: list_view::ListView, +} + +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; diff --git a/app/gui/view/component-browser/src/lib.rs b/app/gui/view/component-browser/src/lib.rs new file mode 100644 index 000000000000..07745a29ef0d --- /dev/null +++ b/app/gui/view/component-browser/src/lib.rs @@ -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; + diff --git a/app/gui/view/debug_scene/Cargo.toml b/app/gui/view/debug_scene/Cargo.toml index 927e070041e8..ca30943baccc 100644 --- a/app/gui/view/debug_scene/Cargo.toml +++ b/app/gui/view/debug_scene/Cargo.toml @@ -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" } diff --git a/app/gui/view/debug_scene/component-group/Cargo.toml b/app/gui/view/debug_scene/component-group/Cargo.toml new file mode 100644 index 000000000000..562495e9cff6 --- /dev/null +++ b/app/gui/view/debug_scene/component-group/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "debug-scene-component-group" +version = "0.1.0" +authors = ["Enso Team "] +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"] } diff --git a/app/gui/view/debug_scene/component-group/src/lib.rs b/app/gui/view/debug_scene/component-group/src/lib.rs new file mode 100644 index 000000000000..18e3869ba383 --- /dev/null +++ b/app/gui/view/debug_scene/component-group/src/lib.rs @@ -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, +} + +impl MockEntries { + fn new(entries: Vec) -> Self { + Self { entries } + } + + fn get_entry(&self, i: usize) -> Option { + self.entries.get(i).cloned() + } +} + +impl list_view::entry::ModelProvider for MockEntries { + fn entry_count(&self) -> usize { + self.entries.len() + } + + fn get(&self, id: usize) -> Option { + 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::(); + 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); +} diff --git a/app/gui/view/debug_scene/interface/src/lib.rs b/app/gui/view/debug_scene/interface/src/lib.rs index 73d9e0d19977..e6833cc83cd2 100644 --- a/app/gui/view/debug_scene/interface/src/lib.rs +++ b/app/gui/view/debug_scene/interface/src/lib.rs @@ -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); diff --git a/app/gui/view/debug_scene/src/lib.rs b/app/gui/view/debug_scene/src/lib.rs index d3aa4cc5683a..ea9cec53dd4a 100644 --- a/app/gui/view/debug_scene/src/lib.rs +++ b/app/gui/view/debug_scene/src/lib.rs @@ -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; diff --git a/app/gui/view/debug_scene/visualization/src/lib.rs b/app/gui/view/debug_scene/visualization/src/lib.rs index 0c508f0a16a4..7701ab4d9e28 100644 --- a/app/gui/view/debug_scene/visualization/src/lib.rs +++ b/app/gui/view/debug_scene/visualization/src/lib.rs @@ -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); diff --git a/build/run.js b/build/run.js index 6b02ba530c24..0fffea70d7e5 100755 --- a/build/run.js +++ b/build/run.js @@ -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. diff --git a/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs b/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs index 710a532ab2ea..47db73836edb 100644 --- a/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs +++ b/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs @@ -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; diff --git a/lib/rust/ensogl/component/list-view/src/entry.rs b/lib/rust/ensogl/component/list-view/src/entry.rs index 6fc55809c6f9..281b62a55fbc 100644 --- a/lib/rust/ensogl/component/list-view/src/entry.rs +++ b/lib/rust/ensogl/component/list-view/src/entry.rs @@ -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>, + max_width_px: Rc>, 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::(); + 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); } diff --git a/lib/rust/ensogl/component/list-view/src/entry/list.rs b/lib/rust/ensogl/component/list-view/src/entry/list.rs index c0d86ed2f61c..f0814ac08f45 100644 --- a/lib/rust/ensogl/component/list-view/src/entry/list.rs +++ b/lib/rust/ensogl/component/list-view/src/entry/list.rs @@ -127,8 +127,9 @@ where E::Model: Default } } - /// Update displayed entries to show the given range. - pub fn update_entries(&self, mut range: Range) { + /// 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, 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> + 'static, mut range: Range, + 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); } diff --git a/lib/rust/ensogl/component/list-view/src/lib.rs b/lib/rust/ensogl/component/list-view/src/lib.rs index bcf8605f351b..b2053be92581 100644 --- a/lib/rust/ensogl/component/list-view/src/lib.rs +++ b/lib/rust/ensogl/component/list-view/src/lib.rs @@ -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 = "input_size.x".into(); let sprite_height : Var = "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::::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 Model { 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 Model { 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, 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 { @@ -250,18 +262,21 @@ ensogl_core::define_endpoints! { /// Deselect all entries. deselect_entries(), - resize (Vector2), - scroll_jump (f32), - set_entries (entry::AnyModelProvider), - select_entry (entry::Id), - chose_entry (entry::Id), + resize(Vector2), + scroll_jump(f32), + set_entries(entry::AnyModelProvider), + 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), - chosen_entry (Option), - size (Vector2), - scroll_position (f32), + selected_entry(Option), + chosen_entry(Option), + size(Vector2), + scroll_position(f32), } } @@ -311,9 +326,28 @@ where E::Model: Default let view_y = DEPRECATED_Animation::::new(network); let selection_y = DEPRECATED_Animation::::new(network); let selection_height = DEPRECATED_Animation::::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); diff --git a/lib/rust/ensogl/component/text/Cargo.toml b/lib/rust/ensogl/component/text/Cargo.toml index e32eb66fefbd..b57f4f7ecf84 100644 --- a/lib/rust/ensogl/component/text/Cargo.toml +++ b/lib/rust/ensogl/component/text/Cargo.toml @@ -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" } diff --git a/lib/rust/ensogl/component/text/embedded-fonts/build.rs b/lib/rust/ensogl/component/text/embedded-fonts/build.rs index 9cf0e0d1dc41..78a77d54f9e7 100644 --- a/lib/rust/ensogl/component/text/embedded-fonts/build.rs +++ b/lib/rust/ensogl/component/text/embedded-fonts/build.rs @@ -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 { diff --git a/lib/rust/ensogl/component/text/src/component/area.rs b/lib/rust/ensogl/component/text/src/component/area.rs index 137143614fd4..8b865e17a7ed 100644 --- a/lib/rust/ensogl/component/text/src/component/area.rs +++ b/lib/rust/ensogl/component/text/src/component/area.rs @@ -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; diff --git a/lib/rust/ensogl/core/src/display/scene/layer.rs b/lib/rust/ensogl/core/src/display/scene/layer.rs index 6b2aab6c8beb..4226edadfee7 100644 --- a/lib/rust/ensogl/core/src/display/scene/layer.rs +++ b/lib/rust/ensogl/core/src/display/scene/layer.rs @@ -1097,6 +1097,9 @@ impl ShapeSystemInfoTemplate { /// scene.layers.add_shapes_order_dependency::(); /// scene.layers.add_shapes_order_dependency::(); /// ``` +/// +/// 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 => { diff --git a/lib/rust/shapely/macros/src/derive_entry_point.rs b/lib/rust/shapely/macros/src/derive_entry_point.rs index eb9f86a6772b..a7c575d7929a 100644 --- a/lib/rust/shapely/macros/src/derive_entry_point.rs +++ b/lib/rust/shapely/macros/src/derive_entry_point.rs @@ -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-", "");