Modify list view api to support custom entry (https://github.com/enso-org/ide/pull/1722)

Also fixed issue with not-masked selected entry in visualization chooser.

Original commit: b398ad9c84
This commit is contained in:
Adam Obuchowicz 2021-07-22 19:37:22 +02:00 committed by GitHub
parent 86df862f01
commit 6d9785375d
11 changed files with 558 additions and 452 deletions

View File

@ -5,8 +5,6 @@ use crate::prelude::*;
use ensogl_core::system::web;
use ensogl_core::application::Application;
use ensogl_core::display::object::ObjectOps;
use ensogl_core::display::shape::*;
use ensogl_core::data::color;
use ensogl_text_msdf_sys::run_once_initialized;
use ensogl_gui_components::list_view;
use logger::TraceLogger as Logger;
@ -40,19 +38,6 @@ pub fn entry_point_list_view() {
// === Mock Entries ===
// ====================
mod icon {
use super::*;
ensogl_core::define_shape_system! {
(style:Style,id:f32) {
let width = list_view::entry::ICON_SIZE.px();
let height = list_view::entry::ICON_SIZE.px();
let color : Var<color::Rgba> = "rgba(input_id/16.0,0.0,0.0,1.0)".into();
Rect((&width,&height)).fill(color).into()
}
}
}
#[derive(Clone,Debug)]
struct MockEntries {
logger : Logger,
@ -68,20 +53,16 @@ impl MockEntries {
}
}
impl list_view::entry::ModelProvider for MockEntries {
impl list_view::entry::ModelProvider<list_view::entry::GlyphHighlightedLabel> for MockEntries {
fn entry_count(&self) -> usize { self.entries_count }
fn get(&self, id:usize) -> Option<list_view::entry::Model> {
fn get(&self, id:usize) -> Option<list_view::entry::GlyphHighlightedLabelModel> {
if id >= self.entries_count {
None
} else {
use list_view::entry::ICON_SIZE;
let icon = icon::View::new(&self.logger);
icon.size.set(Vector2(ICON_SIZE,ICON_SIZE));
icon.id.set(id as f32);
let model = list_view::entry::Model::new(iformat!("Entry {id}")).with_icon(icon);
if id == 10 { Some(model.highlight(std::iter::once((Bytes(1)..Bytes(3)).into()))) }
else { Some(model) }
let label = iformat!("Entry {id}");
let highlighted = if id == 10 { vec![(Bytes(1)..Bytes(3)).into()] } else { vec![] };
Some(list_view::entry::GlyphHighlightedLabelModel {label,highlighted})
}
}
}
@ -97,8 +78,8 @@ fn init(app:&Application) {
theme::builtin::light::register(&app);
theme::builtin::light::enable(&app);
let list_view = app.new_view::<list_view::ListView>();
let provider = list_view::entry::AnyModelProvider::from(MockEntries::new(app,1000));
let list_view = app.new_view::<list_view::ListView<list_view::entry::GlyphHighlightedLabel>>();
let provider = list_view::entry::AnyModelProvider::new(MockEntries::new(app,1000));
list_view.frp.resize(Vector2(100.0,160.0));
list_view.frp.set_entries(provider);
app.display.add_child(&list_view);

View File

@ -74,7 +74,7 @@ pub mod chooser_hover_area {
ensogl_core::define_endpoints! {
Input {
set_entries (list_view::entry::AnyModelProvider),
set_entries (list_view::entry::AnyModelProvider<Entry>),
set_icon_size (Vector2),
set_icon_padding (Vector2),
hide_selection_menu (),
@ -96,6 +96,9 @@ ensogl_core::define_endpoints! {
// === Model ===
// =============
/// A type of Entry used in DropDownMenu's ListView.
pub type Entry = list_view::entry::Label;
#[derive(Clone,Debug)]
struct Model {
logger : Logger,
@ -106,10 +109,10 @@ struct Model {
icon_overlay : chooser_hover_area::View,
label : text::Area,
selection_menu : list_view::ListView,
selection_menu : list_view::ListView<Entry>,
// `SingleMaskedProvider` allows us to hide the selected element.
content : RefCell<Option<list_view::entry::SingleMaskedProvider>>,
content : RefCell<Option<list_view::entry::SingleMaskedProvider<Entry>>>,
}
impl Model {
@ -152,7 +155,8 @@ impl Model {
self.selection_menu.unset_parent()
}
fn get_content_item(&self, id:Option<list_view::entry::Id>) -> Option<list_view::entry::Model> {
fn get_content_item
(&self, id:Option<list_view::entry::Id>) -> Option<<Entry as list_view::entry::Entry>::Model> {
self.content.borrow().as_ref()?.get(id?)
}
@ -215,9 +219,9 @@ impl DropDownMenu {
// === Input Processing ===
eval frp.input.set_entries ([model](entries) {
let entries:list_view::entry::SingleMaskedProvider = entries.clone_ref().into();
let entries:list_view::entry::SingleMaskedProvider<Entry> = entries.clone_ref().into();
model.content.set(entries.clone());
let entries:list_view::entry::AnyModelProvider = entries.into();
let entries = list_view::entry::AnyModelProvider::<Entry>::new(entries);
model.selection_menu.frp.set_entries.emit(entries);
});
@ -311,12 +315,12 @@ impl DropDownMenu {
// clear the mask.
content.clear_mask();
if let Some(item) = model.get_content_item(Some(*entry_id)) {
model.set_label(&item.label)
model.set_label(&item)
};
// Remove selected item from menu list
content.set_mask(*entry_id);
// Update menu content.
let entries:list_view::entry::AnyModelProvider = content.clone().into();
let entries = list_view::entry::AnyModelProvider::<Entry>::new(content.clone());
model.selection_menu.frp.set_entries.emit(entries);
};
};

View File

@ -36,7 +36,7 @@ const SHAPE_PADDING:f32 = 5.0;
mod selection {
use super::*;
pub const CORNER_RADIUS_PX:f32 = entry::LABEL_SIZE;
pub const CORNER_RADIUS_PX:f32 = 12.0;
ensogl_core::define_shape_system! {
(style:Style) {
@ -95,16 +95,17 @@ struct View {
/// The Model of Select Component.
#[derive(Clone,CloneRef,Debug)]
struct Model {
struct Model<E:entry::Entry> {
app : Application,
entries : entry::List,
entries : entry::List<E>,
selection : selection::View,
background : background::View,
scrolled_area : display::object::Instance,
display_object : display::object::Instance,
}
impl Model {
impl<E:entry::Entry> Model<E> {
fn new(app:&Application) -> Self {
let app = app.clone_ref();
let logger = Logger::new("SelectionContainer");
@ -141,7 +142,7 @@ impl Model {
self.entries.update_entries(visible_entries);
}
fn set_entries(&self, provider:entry::AnyModelProvider, view:&View) {
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);
}
@ -151,10 +152,10 @@ impl Model {
0..0
} else {
let entry_at_y_saturating = |y:f32| {
match entry::List::entry_at_y_position(y,entry_count) {
entry::IdAtYPosition::AboveFirst => 0,
entry::IdAtYPosition::UnderLast => entry_count - 1,
entry::IdAtYPosition::Entry(id) => id,
match entry::List::<E>::entry_at_y_position(y,entry_count) {
entry::list::IdAtYPosition::AboveFirst => 0,
entry::list::IdAtYPosition::UnderLast => entry_count - 1,
entry::list::IdAtYPosition::Entry(id) => id,
}
};
let first = entry_at_y_saturating(*position_y);
@ -191,6 +192,7 @@ impl Model {
// ===========
ensogl_core::define_endpoints! {
<E>
Input {
/// Move selection one position up.
move_selection_up(),
@ -211,7 +213,7 @@ ensogl_core::define_endpoints! {
resize (Vector2<f32>),
scroll_jump (f32),
set_entries (entry::AnyModelProvider),
set_entries (entry::AnyModelProvider<E>),
select_entry (entry::Id),
chose_entry (entry::Id),
}
@ -226,27 +228,28 @@ ensogl_core::define_endpoints! {
// ========================
// === Select Component ===
// ========================
// ==========================
// === ListView Component ===
// ==========================
/// Select Component.
/// ListView Component.
///
/// Select is a displayed list of entries with possibility of selecting one and "chosing" by
/// clicking or pressing enter.
/// This is a displayed list of entries (of any type `E`) with possibility of selecting one and
/// "choosing" by clicking or pressing enter. The basic entry types are defined in [`entry`] module.
#[allow(missing_docs)]
#[derive(Clone,CloneRef,Debug)]
pub struct ListView {
model : Model,
pub frp : Frp,
pub struct ListView<E:entry::Entry> {
model : Model<E>,
pub frp : Frp<E>,
}
impl Deref for ListView {
type Target = Frp;
impl<E:entry::Entry> Deref for ListView<E> {
type Target = Frp<E>;
fn deref(&self) -> &Self::Target { &self.frp }
}
impl ListView {
impl<E:entry::Entry> ListView<E>
where E::Model : Default {
/// Constructor.
pub fn new(app:&Application) -> Self {
let frp = Frp::new();
@ -279,7 +282,7 @@ impl ListView {
scene.screen_to_object_space(&model.scrolled_area,*pos).y
}));
mouse_pointed_entry <- mouse_y_in_scroll.map(f!([model](y)
entry::List::entry_at_y_position(*y,model.entries.entry_count()).entry()
entry::List::<E>::entry_at_y_position(*y,model.entries.entry_count()).entry()
));
@ -336,7 +339,7 @@ impl ListView {
// === Selection Size and Position ===
target_selection_y <- frp.selected_entry.map(|id|
id.map_or(0.0,entry::List::position_y_of_entry)
id.map_or(0.0,entry::List::<E>::position_y_of_entry)
);
target_selection_height <- frp.selected_entry.map(f!([](id)
if id.is_some() {entry::HEIGHT} else {0.0}
@ -361,7 +364,7 @@ impl ListView {
// === Scrolling ===
selection_top_after_move_up <- selected_entry_after_move_up.map(|id|
id.map(|id| entry::List::y_range_of_entry(id).end)
id.map(|id| entry::List::<E>::y_range_of_entry(id).end)
);
min_scroll_after_move_up <- selection_top_after_move_up.map(|top|
top.unwrap_or(MAX_SCROLL)
@ -370,7 +373,7 @@ impl ListView {
current.max(*min)
);
selection_bottom_after_move_down <- selected_entry_after_move_down.map(|id|
id.map(|id| entry::List::y_range_of_entry(id).start)
id.map(|id| entry::List::<E>::y_range_of_entry(id).start)
);
max_scroll_after_move_down <- selection_bottom_after_move_down.map2(&frp.size,
|y,size| y.map_or(MAX_SCROLL, |y| y + size.y)
@ -420,15 +423,15 @@ impl ListView {
}
}
impl display::Object for ListView {
impl<E:entry::Entry> display::Object for ListView<E> {
fn display_object(&self) -> &display::object::Instance { &self.model.display_object }
}
impl application::command::FrpNetworkProvider for ListView {
impl<E:entry::Entry> application::command::FrpNetworkProvider for ListView<E> {
fn network(&self) -> &frp::Network { &self.frp.network }
}
impl application::View for ListView {
impl<E:entry::Entry> application::View for ListView<E> {
fn label() -> &'static str { "ListView" }
fn new(app:&Application) -> Self { ListView::new(app) }
fn app(&self) -> &Application { &self.model.app }

View File

@ -1,12 +1,14 @@
//! A single entry in Select
//! A single entry in [`crate::list_view::ListView`].
pub mod list;
use crate::prelude::*;
use enso_frp as frp;
use ensogl_core::application::Application;
use ensogl_core::display;
use ensogl_core::display::scene::layer::LayerId;
use ensogl_core::display::shape::StyleWatch;
use ensogl_core::display::shape::StyleWatchFrp;
use ensogl_text as text;
use ensogl_theme;
use ensogl_theme as theme;
@ -18,98 +20,210 @@ use ensogl_theme;
pub const PADDING:f32 = 14.0;
/// The overall entry's height (including padding).
pub const HEIGHT:f32 = 30.0;
/// The text size of entry's labe.
pub const LABEL_SIZE:f32 = 12.0;
/// The size in pixels of icons inside entries.
pub const ICON_SIZE:f32 = 0.0; // TODO[ao] restore when we create icons for the searcher.
/// The gap between icon and label.
pub const ICON_LABEL_GAP:f32 = 0.0; // TODO[ao] restore when we create icons for the searcher.
// ===================
// === Entry Model ===
// ===================
// ==================================
// === Type Aliases and Reexports ===
// ==================================
/// Entry id. 0 is the first entry in component.
pub type Id = usize;
/// A model on which the view bases.
#[allow(missing_docs)]
#[derive(Clone,Debug,Default)]
pub struct Model {
pub label : String,
pub highlighted : Vec<text::Range<text::Bytes>>,
pub icon : Option<display::object::Any>,
}
impl Model {
/// Create model of simple entry with given label.
///
/// The model won't have icon nor higlighting, but those can be set using `highlight` and
/// `with_icon`.
pub fn new(label:impl Str) -> Self {
Self {
label : label.into(),
highlighted : default(),
icon : default(),
}
}
/// Add highlighting to the entry and return it.
pub fn highlight(mut self, bytes:impl IntoIterator<Item=text::Range<text::Bytes>>) -> Self {
self.highlighted.extend(bytes.into_iter());
self
}
/// Add icon to the entry and return it.
pub fn with_icon(mut self, icon:impl display::Object + 'static) -> Self {
self.icon = Some(icon.into_any());
self
}
}
impl<T:Display> From<T> for Model {
fn from(item: T) -> Self {
Model::new(item.to_string())
}
}
pub use list::List;
// === Entry Model Provider ===
/// The Entry Model Provider for select component.
// =============
// === Trait ===
// =============
/// An object which can be entry in [`crate::ListView`] component.
///
/// The select does not display all entries at once, instead it lazily ask for models of entries
/// when they're about to be displayed. So setting the select content is essentially providing
/// implementor of this trait.
pub trait ModelProvider : Debug {
/// The entries should not assume any padding - it will be granted by ListView itself. The Display
/// Object position of this component is docked to the middle of left entry's boundary. It differs
/// from usual behaviour of EnsoGl components, but makes the entries alignment much simpler.
///
/// This trait abstracts over model and its updating in order to support re-using shapes and gui
/// components, so they are not deleted and created again. The ListView component does not create
/// Entry object for each entry provided, and during scrolling, the instantiated objects will be
/// reused: they position will be changed and they will be updated using `update` method.
pub trait Entry: CloneRef + Debug + display::Object + 'static {
/// The model of this entry. The entry should be a representation of data from the Model.
/// For example, the entry being just a caption can have [`String`] as its model - the text to
/// be displayed.
type Model : Debug + Default;
/// An Object constructor.
fn new(app:&Application) -> Self;
/// Update content with new model.
fn update(&self, model:&Self::Model);
/// 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.
fn set_label_layer(&self, label_layer:&display::scene::Layer);
}
// =======================
// === Implementations ===
// =======================
// === Label ===
/// The [`Entry`] being a single text field displaying String.
#[derive(Clone,CloneRef,Debug)]
pub struct Label {
display_object : display::object::Instance,
label : text::Area,
network : enso_frp::Network,
style_watch : StyleWatchFrp,
}
impl Entry for Label {
type Model = String;
fn new(app: &Application) -> Self {
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 network = frp::Network::new("list_view::entry::Label");
let style_watch = StyleWatchFrp::new(&app.display.scene().style_sheet);
let color = style_watch.get_color(theme::widget::list_view::text);
let size = style_watch.get_number(theme::widget::list_view::text::size);
display_object.add_child(&label);
frp::extend! { network
init <- source::<()>();
color <- all(&color,&init)._0();
size <- all(&size,&init)._0();
label.set_default_color <+ color;
label.set_default_text_size <+ size.map(|v| text::Size(*v));
eval size ((size) label.set_position_y(size/2.0));
}
init.emit(());
Self {display_object,label,network,style_watch}
}
fn update(&self, model: &Self::Model) {
self.label.set_content(model);
}
fn set_label_layer(&self, label_layer:&display::scene::Layer) {
self.label.add_to_scene_layer(label_layer);
}
}
impl display::Object for Label {
fn display_object(&self) -> &display::object::Instance { &self.display_object }
}
// === HighlightedLabel ===
/// The model for [`HighlightedLabel`], being an entry displayed as a single label with highlighted
/// some parts of text.
#[derive(Clone,Debug,Default)]
pub struct GlyphHighlightedLabelModel {
/// Displayed text.
pub label:String,
/// A list of ranges of highlighted bytes.
pub highlighted:Vec<text::Range<text::Bytes>>,
}
/// The [`Entry`] similar to the [`Label`], but allows highlighting some parts of text.
#[derive(Clone,CloneRef,Debug)]
pub struct GlyphHighlightedLabel {
inner : Label,
highlight : frp::Source<Vec<text::Range<text::Bytes>>>,
}
impl Entry for GlyphHighlightedLabel {
type Model = GlyphHighlightedLabelModel;
fn new(app: &Application) -> Self {
let inner = Label::new(app);
let network = &inner.network;
let highlight_color = inner.style_watch.get_color(theme::widget::list_view::text::highlight);
let label = &inner.label;
frp::extend! { network
highlight <- source::<Vec<text::Range<text::Bytes>>>();
highlight_changed <- all(highlight,highlight_color);
eval highlight_changed ([label]((highlight,color)) {
for range in highlight {
label.set_color_bytes(range,color);
}
});
}
Self {inner,highlight}
}
fn update(&self, model: &Self::Model) {
self.inner.update(&model.label);
self.highlight.emit(&model.highlighted);
}
fn set_label_layer(&self, layer:&display::scene::Layer) {
self.inner.set_label_layer(layer);
}
}
impl display::Object for GlyphHighlightedLabel {
fn display_object(&self) -> &display::object::Instance { self.inner.display_object() }
}
// =======================
// === Model Providers ===
// =======================
// === The Trait ===
/// The Model Provider for ListView's entries of type `E`.
///
/// The [`crate::ListView`] component does not display all entries at once, instead it lazily ask
/// for models of entries when they're about to be displayed. So setting the select content is
/// essentially providing an implementor of this trait.
pub trait ModelProvider<E> : Debug {
/// Number of all entries.
fn entry_count(&self) -> usize;
/// Get the model of entry with given id. The implementors should return `None` onlt when
/// Get the model of entry with given id. The implementors should return `None` only when
/// requested id greater or equal to entries count.
fn get(&self, id:Id) -> Option<Model>;
fn get(&self, id:Id) -> Option<E::Model>
where E : Entry;
}
/// A wrapper for shared instance of some ModelProvider.
#[derive(Clone,CloneRef,Debug,Shrinkwrap)]
pub struct AnyModelProvider(Rc<dyn ModelProvider>);
impl<T:ModelProvider + 'static> From<T> for AnyModelProvider {
fn from(provider:T) -> Self { Self(Rc::new(provider)) }
// === AnyModelProvider ===
/// A wrapper for shared instance of some Provider of models for `E` entries.
#[derive(Debug,Shrinkwrap)]
pub struct AnyModelProvider<E>(Rc<dyn ModelProvider<E>>);
impl<E> Clone for AnyModelProvider<E> { fn clone (&self) -> Self { Self(self.0.clone()) }}
impl<E> CloneRef for AnyModelProvider<E> { fn clone_ref(&self) -> Self { Self(self.0.clone_ref()) }}
impl<E> AnyModelProvider<E> {
/// Create from typed provider.
pub fn new<T:ModelProvider<E>+'static>(provider:T) -> Self { Self(Rc::new(provider)) }
}
impl<T:ModelProvider + 'static> From<Rc<T>> for AnyModelProvider {
impl<E, T:ModelProvider<E>+'static> From<Rc<T>> for AnyModelProvider<E> {
fn from(provider:Rc<T>) -> Self { Self(provider) }
}
impl Default for AnyModelProvider {
fn default() -> Self {EmptyProvider.into()}
impl<E> Default for AnyModelProvider<E> {
fn default() -> Self { Self::new(EmptyProvider) }
}
// === Empty Model Provider ===
// === EmptyProvider ===
/// An Entry Model Provider giving no entries.
///
@ -117,35 +231,37 @@ impl Default for AnyModelProvider {
#[derive(Clone,CloneRef,Copy,Debug)]
pub struct EmptyProvider;
impl ModelProvider for EmptyProvider {
fn entry_count(&self) -> usize { 0 }
fn get (&self, _:usize) -> Option<Model> { None }
impl<E> ModelProvider<E> for EmptyProvider {
fn entry_count(&self) -> usize { 0 }
fn get (&self, _:usize) -> Option<E::Model> where E : Entry { None }
}
// === Model Provider for Vectors ===
// === ModelProvider for Vectors ===
impl<T:Into<Model> + Debug + Clone> ModelProvider for Vec<T> {
impl<E,T> ModelProvider<E> for Vec<T>
where E : Entry,
T : Debug + Clone + Into<E::Model> {
fn entry_count(&self) -> usize {
self.len()
}
fn get(&self, id:usize) -> Option<Model> {
fn get(&self, id:usize) -> Option<E::Model> {
Some(<[T]>::get(self, id)?.clone().into())
}
}
// === Masked Model Provider ===
// === SingleMaskedProvider ===
/// An Entry Model Provider that wraps a `AnyModelProvider` and allows the masking of a single item.
#[derive(Clone,Debug)]
pub struct SingleMaskedProvider {
content : AnyModelProvider,
pub struct SingleMaskedProvider<E> {
content : AnyModelProvider<E>,
mask : Cell<Option<Id>>,
}
impl ModelProvider for SingleMaskedProvider {
impl<E:Debug> ModelProvider<E> for SingleMaskedProvider<E> {
fn entry_count(&self) -> usize {
match self.mask.get() {
None => self.content.entry_count(),
@ -153,13 +269,14 @@ impl ModelProvider for SingleMaskedProvider {
}
}
fn get(&self, ix:usize) -> Option<Model> {
fn get(&self, ix:usize) -> Option<E::Model>
where E : Entry {
let internal_ix = self.unmasked_index(ix);
self.content.get(internal_ix)
}
}
impl SingleMaskedProvider {
impl<E> SingleMaskedProvider<E> {
/// Return the index to the unmasked underlying data. Will only be valid to use after
/// calling `clear_mask`.
@ -208,8 +325,8 @@ impl SingleMaskedProvider {
}
}
impl From<AnyModelProvider> for SingleMaskedProvider {
fn from(content:AnyModelProvider) -> Self {
impl<E> From<AnyModelProvider<E>> for SingleMaskedProvider<E> {
fn from(content:AnyModelProvider<E>) -> Self {
let mask = default();
SingleMaskedProvider{content,mask}
}
@ -218,249 +335,9 @@ impl From<AnyModelProvider> for SingleMaskedProvider {
// =============
// === Entry ===
// === Tests ===
// =============
/// A displayed entry in select component.
///
/// The Display Object position of this component is docked to the middle of left entry's boundary.
/// It differs from usual behaviour of EnsoGl components, but makes the entries alignment much
/// simpler.
#[derive(Clone,CloneRef,Debug)]
pub struct Entry {
app : Application,
id : Rc<Cell<Option<Id>>>,
label : text::Area,
icon : Rc<CloneCell<Option<display::object::Any>>>,
display_object : display::object::Instance,
}
impl Entry {
/// Create new entry view.
pub fn new(logger:impl AnyLogger, app:&Application) -> Self {
let app = app.clone_ref();
let id = default();
let label = app.new_view::<text::Area>();
let icon = Rc::new(CloneCell::new(None));
let display_object = display::object::Instance::new(logger);
display_object.add_child(&label);
label.set_position_xy(Vector2(PADDING + ICON_SIZE + ICON_LABEL_GAP, LABEL_SIZE/2.0));
// FIXME : StyleWatch is unsuitable here, as it was designed as an internal tool for shape system (#795)
let styles = StyleWatch::new(&app.display.scene().style_sheet);
let text_color = styles.get_color(ensogl_theme::widget::list_view::text);
label.set_default_color(text_color);
label.set_default_text_size(text::Size(LABEL_SIZE));
Entry{app,id,label,icon,display_object}
}
/// Set the new model for this view.
///
/// This function updates icon and label.
pub fn set_model(&self, id:Id, model:&Model) {
if let Some(old_icon) = self.icon.get() {
self.remove_child(&old_icon);
}
if let Some(new_icon) = &model.icon {
self.add_child(&new_icon);
new_icon.set_position_xy(Vector2(PADDING + ICON_SIZE/2.0, 0.0));
}
self.id.set(Some(id));
self.icon.set(model.icon.clone());
self.label.set_content(&model.label);
// FIXME : StyleWatch is unsuitable here, as it was designed as an internal tool for shape
// system (#795)
let styles = StyleWatch::new(&self.app.display.scene().style_sheet);
let highlight = styles.get_color(ensogl_theme::widget::list_view::text::highlight);
for highlighted in &model.highlighted {
self.label.set_color_bytes(highlighted,highlight);
}
}
}
impl display::Object for Entry {
fn display_object(&self) -> &display::object::Instance { &self.display_object }
}
// =================
// === EntryList ===
// =================
/// The output of `entry_at_y_position`
#[allow(missing_docs)]
#[derive(Copy,Clone,Debug,Eq,Hash,PartialEq)]
pub enum IdAtYPosition {
AboveFirst, UnderLast, Entry(Id)
}
impl IdAtYPosition {
/// Returns id of entry if present.
pub fn entry(&self) -> Option<Id> {
if let Self::Entry(id) = self { Some(*id) }
else { None }
}
}
/// A view containing an entry list, arranged in column.
///
/// Not all entries are displayed at once, only those visible.
#[derive(Clone,CloneRef,Debug)]
pub struct List {
logger : Logger,
app : Application,
display_object : display::object::Instance,
entries : Rc<RefCell<Vec<Entry>>>,
entries_range : Rc<CloneCell<Range<Id>>>,
provider : Rc<CloneRefCell<AnyModelProvider>>,
label_layer : Rc<Cell<LayerId>>,
}
impl List {
/// Entry List View constructor.
pub fn new(parent:impl AnyLogger, app:&Application) -> Self {
let app = app.clone_ref();
let logger = Logger::sub(parent,"entry::List");
let entries = default();
let entries_range = Rc::new(CloneCell::new(default()..default()));
let display_object = display::object::Instance::new(&logger);
let provider = default();
let label_layer = Rc::new(Cell::new(app.display.scene().layers.label.id));
List {logger,app,display_object,entries,entries_range,provider,label_layer}
}
/// The number of all entries in List, including not displayed.
pub fn entry_count(&self) -> usize {
self.provider.get().entry_count()
}
/// The number of all displayed entries in List.
pub fn visible_entry_count(&self) -> usize {
self.entries_range.get().len()
}
/// Y position of entry with given id, relative to Entry List position.
pub fn position_y_of_entry(id:Id) -> f32 { id as f32 * -HEIGHT }
/// Y range of entry with given id, relative to Entry List position.
pub fn y_range_of_entry(id:Id) -> Range<f32> {
let position = Self::position_y_of_entry(id);
(position - HEIGHT / 2.0)..(position + HEIGHT / 2.0)
}
/// Y range of all entries in this list, including not displayed.
pub fn y_range_of_all_entries(entry_count:usize) -> Range<f32> {
let start = if entry_count > 0 {
Self::position_y_of_entry(entry_count - 1) - HEIGHT / 2.0
} else {
HEIGHT / 2.0
};
let end = HEIGHT / 2.0;
start..end
}
/// Get the entry id which lays on given y coordinate.
pub fn entry_at_y_position(y:f32, entry_count:usize) -> IdAtYPosition {
use IdAtYPosition::*;
let all_entries_start = Self::y_range_of_all_entries(entry_count).start;
if y > HEIGHT/2.0 { AboveFirst }
else if y < all_entries_start { UnderLast }
else { Entry((-y/HEIGHT + 0.5) as Id) }
}
/// Update displayed entries to show the given range.
pub fn update_entries(&self, mut range:Range<Id>) {
range.end = range.end.min(self.provider.get().entry_count());
if range != self.entries_range.get() {
debug!(self.logger, "Update entries for {range:?}");
let provider = self.provider.get();
let current_entries:HashSet<Id> = with(self.entries.borrow_mut(), |mut entries| {
entries.resize_with(range.len(),|| self.create_new_entry());
entries.iter().filter_map(|entry| entry.id.get()).collect()
});
let missing = range.clone().filter(|id| !current_entries.contains(id));
// The provider is provided by user, so we should not keep any borrow when calling its
// methods.
let models = missing.map(|id| (id,provider.get(id)));
with(self.entries.borrow(), |entries| {
let is_outdated = |e:&Entry| e.id.get().map_or(true, |i| !range.contains(&i));
let outdated = entries.iter().filter(|e| is_outdated(e));
for (entry,(id,model)) in outdated.zip(models) {
Self::update_entry(&self.logger,entry,id,&model);
}
});
self.entries_range.set(range);
}
}
/// Update displayed entries, giving new provider.
pub fn update_entries_new_provider
(&self, provider:impl Into<AnyModelProvider> + 'static, mut range:Range<Id>) {
const MAX_SAFE_ENTRIES_COUNT:usize = 1000;
let provider = provider.into();
if provider.entry_count() > MAX_SAFE_ENTRIES_COUNT {
error!(self.logger, "ListView entry count exceed {MAX_SAFE_ENTRIES_COUNT} - so big \
number of entries can cause visual glitches, e.g. https://github.com/enso-org/ide/\
issues/757 or https://github.com/enso-org/ide/issues/758");
}
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());
for (entry,(id,model)) in entries.iter().zip(models) {
Self::update_entry(&self.logger,entry,id,&model);
}
self.entries_range.set(range);
self.provider.set(provider);
}
/// Sets the scene layer where the labels will be placed.
pub fn set_label_layer(&self, label_layer:LayerId) {
if let Some(layer) = self.app.display.scene().layers.get(self.label_layer.get()) {
for entry in &*self.entries.borrow() {
entry.label.remove_from_scene_layer(&self.app.display.scene().layers.label);
entry.label.add_to_scene_layer(&layer);
}
} else {
error!(self.logger, "Cannot set layer {label_layer:?} for labels: the layer does not \
exist in the scene");
}
self.label_layer.set(label_layer);
}
fn create_new_entry(&self) -> Entry {
let entry = Entry::new(&self.logger,&self.app);
if let Some(layer) = self.app.display.scene().layers.get(self.label_layer.get()) {
entry.label.remove_from_scene_layer(&self.app.display.scene().layers.label);
entry.label.add_to_scene_layer(&layer);
} else {
error!(self.logger, "Cannot set layer {self.label_layer:?} for labels: the layer does \
not exist in the scene");
}
self.add_child(&entry);
entry
}
fn update_entry(logger:&Logger, entry:&Entry, id:Id, model:&Option<Model>) {
debug!(logger, "Setting new model {model:?} for entry {id}; \
old entry: {entry.id.get():?}.");
match model {
Some(model) => entry.set_model(id,model),
None => {
error!(logger, "Model provider didn't return model for id {id}.");
entry.set_model(id,&default())
}
};
entry.set_position_y(Self::position_y_of_entry(id));
}
}
impl display::Object for List {
fn display_object(&self) -> &display::object::Instance { &self.display_object }
}
#[cfg(test)]
mod tests {
use super::*;
@ -468,38 +345,38 @@ mod tests {
#[test]
fn test_masked_provider() {
let test_data = vec!["A", "B", "C", "D"];
let test_models = test_data.into_iter().map(|label| Model::new(label)).collect_vec();
let provider:AnyModelProvider = test_models.into();
let provider:SingleMaskedProvider = provider.into();
let test_models = test_data.into_iter().map(|label| label.to_owned()).collect_vec();
let provider = AnyModelProvider::<Label>::new(test_models);
let provider:SingleMaskedProvider<Label> = provider.into();
assert_eq!(provider.entry_count(), 4);
assert_eq!(provider.get(0).unwrap().label, "A");
assert_eq!(provider.get(1).unwrap().label, "B");
assert_eq!(provider.get(2).unwrap().label, "C");
assert_eq!(provider.get(3).unwrap().label, "D");
assert_eq!(provider.get(0).unwrap(), "A");
assert_eq!(provider.get(1).unwrap(), "B");
assert_eq!(provider.get(2).unwrap(), "C");
assert_eq!(provider.get(3).unwrap(), "D");
provider.set_mask_raw(0);
assert_eq!(provider.entry_count(), 3);
assert_eq!(provider.get(0).unwrap().label, "B");
assert_eq!(provider.get(1).unwrap().label, "C");
assert_eq!(provider.get(2).unwrap().label, "D");
assert_eq!(provider.get(0).unwrap(), "B");
assert_eq!(provider.get(1).unwrap(), "C");
assert_eq!(provider.get(2).unwrap(), "D");
provider.set_mask_raw(1);
assert_eq!(provider.entry_count(), 3);
assert_eq!(provider.get(0).unwrap().label, "A");
assert_eq!(provider.get(1).unwrap().label, "C");
assert_eq!(provider.get(2).unwrap().label, "D");
assert_eq!(provider.get(0).unwrap(), "A");
assert_eq!(provider.get(1).unwrap(), "C");
assert_eq!(provider.get(2).unwrap(), "D");
provider.set_mask_raw(2);
assert_eq!(provider.entry_count(), 3);
assert_eq!(provider.get(0).unwrap().label, "A");
assert_eq!(provider.get(1).unwrap().label, "B");
assert_eq!(provider.get(2).unwrap().label, "D");
assert_eq!(provider.get(0).unwrap(), "A");
assert_eq!(provider.get(1).unwrap(), "B");
assert_eq!(provider.get(2).unwrap(), "D");
provider.set_mask_raw(3);
assert_eq!(provider.entry_count(), 3);
assert_eq!(provider.get(0).unwrap().label, "A");
assert_eq!(provider.get(1).unwrap().label, "B");
assert_eq!(provider.get(2).unwrap().label, "C");
assert_eq!(provider.get(0).unwrap(), "A");
assert_eq!(provider.get(1).unwrap(), "B");
assert_eq!(provider.get(2).unwrap(), "C");
}
}

View File

@ -0,0 +1,211 @@
//! A module defining entry [`List`] structure: a view of ListView entries arranged in column.
use crate::prelude::*;
use crate::list_view::entry;
use crate::list_view::entry::Entry;
use ensogl_core::application::Application;
use ensogl_core::display;
use ensogl_core::display::scene::layer::LayerId;
// ======================
// === DisplayedEntry ===
// ======================
/// A displayed entry in select component.
///
/// The Display Object position of this component is docked to the middle of left entry's boundary.
/// It differs from usual behaviour of EnsoGL components, but makes the entries alignment much
/// simpler: In vast majority of cases we want to align list elements to the left.
#[allow(missing_docs)]
#[derive(Clone,CloneRef,Debug)]
pub struct DisplayedEntry<E:CloneRef> {
pub id : Rc<Cell<Option<entry::Id>>>,
pub entry : E,
}
// =================
// === EntryList ===
// =================
/// The output of `entry_at_y_position`
#[allow(missing_docs)]
#[derive(Copy,Clone,Debug,Eq,Hash,PartialEq)]
pub enum IdAtYPosition {
AboveFirst, UnderLast, Entry(entry::Id)
}
impl IdAtYPosition {
/// Returns id of entry if present.
pub fn entry(&self) -> Option<entry::Id> {
if let Self::Entry(id) = self { Some(*id) }
else { None }
}
}
/// A view containing an entry list, arranged in column.
///
/// Not all entries are displayed at once, only those visible.
#[derive(Clone,CloneRef,Debug)]
pub struct List<E:CloneRef> {
logger : Logger,
app : Application,
display_object : display::object::Instance,
entries : Rc<RefCell<Vec<DisplayedEntry<E>>>>,
entries_range : Rc<CloneCell<Range<entry::Id>>>,
provider : Rc<CloneRefCell<entry::AnyModelProvider<E>>>,
label_layer : Rc<Cell<LayerId>>,
}
impl<E: Entry> List<E>
where E::Model : Default {
/// Entry List View constructor.
pub fn new(parent:impl AnyLogger, app:&Application) -> Self {
let app = app.clone_ref();
let logger = Logger::sub(parent,"entry::List");
let entries = default();
let entries_range = Rc::new(CloneCell::new(default()..default()));
let display_object = display::object::Instance::new(&logger);
let provider = default();
let label_layer = Rc::new(Cell::new(app.display.scene().layers.label.id));
List {logger,app,display_object,entries,entries_range,provider,label_layer}
}
/// The number of all entries in List, including not displayed.
pub fn entry_count(&self) -> usize {
self.provider.get().entry_count()
}
/// The number of all displayed entries in List.
pub fn visible_entry_count(&self) -> usize {
self.entries_range.get().len()
}
/// Y position of entry with given id, relative to Entry List position.
pub fn position_y_of_entry(id:entry::Id) -> f32 { id as f32 * -entry::HEIGHT }
/// Y range of entry with given id, relative to Entry List position.
pub fn y_range_of_entry(id:entry::Id) -> Range<f32> {
let position = Self::position_y_of_entry(id);
(position - entry::HEIGHT / 2.0)..(position + entry::HEIGHT / 2.0)
}
/// Y range of all entries in this list, including not displayed.
pub fn y_range_of_all_entries(entry_count:usize) -> Range<f32> {
let start = if entry_count > 0 {
Self::position_y_of_entry(entry_count - 1) - entry::HEIGHT / 2.0
} else {
entry::HEIGHT / 2.0
};
let end = entry::HEIGHT / 2.0;
start..end
}
/// Get the entry id which lays on given y coordinate.
pub fn entry_at_y_position(y:f32, entry_count:usize) -> IdAtYPosition {
use IdAtYPosition::*;
let all_entries_start = Self::y_range_of_all_entries(entry_count).start;
if y > entry::HEIGHT/2.0 { AboveFirst }
else if y < all_entries_start { UnderLast }
else { Entry((-y/entry::HEIGHT + 0.5) as entry::Id) }
}
/// Update displayed entries to show the given range.
pub fn update_entries(&self, mut range:Range<entry::Id>) {
range.end = range.end.min(self.provider.get().entry_count());
if range != self.entries_range.get() {
debug!(self.logger, "Update entries for {range:?}");
let provider = self.provider.get();
let current_entries:HashSet<entry::Id> = with(self.entries.borrow_mut(), |mut entries| {
entries.resize_with(range.len(),|| self.create_new_entry());
entries.iter().filter_map(|entry| entry.id.get()).collect()
});
let missing = range.clone().filter(|id| !current_entries.contains(id));
// The provider is provided by user, so we should not keep any borrow when calling its
// methods.
let models = missing.map(|id| (id,provider.get(id)));
with(self.entries.borrow(), |entries| {
let is_outdated = |e:&DisplayedEntry<E>| e.id.get().map_or(true, |i| !range.contains(&i));
let outdated = entries.iter().filter(|e| is_outdated(e));
for (entry,(id,model)) in outdated.zip(models) {
Self::update_entry(&self.logger,entry,id,&model);
}
});
self.entries_range.set(range);
}
}
/// Update displayed entries, giving new provider.
pub fn update_entries_new_provider
(&self, provider:impl Into<entry::AnyModelProvider<E>> + 'static, mut range:Range<entry::Id>) {
const MAX_SAFE_ENTRIES_COUNT:usize = 1000;
let provider = provider.into();
if provider.entry_count() > MAX_SAFE_ENTRIES_COUNT {
error!(self.logger, "ListView entry count exceed {MAX_SAFE_ENTRIES_COUNT} - so big \
number of entries can cause visual glitches, e.g. https://github.com/enso-org/ide/\
issues/757 or https://github.com/enso-org/ide/issues/758");
}
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());
for (entry,(id,model)) in entries.iter().zip(models) {
Self::update_entry(&self.logger,entry,id,&model);
}
self.entries_range.set(range);
self.provider.set(provider);
}
/// Sets the scene layer where the labels will be placed.
pub fn set_label_layer(&self, label_layer:LayerId) {
if let Some(layer) = self.app.display.scene().layers.get(self.label_layer.get()) {
for entry in &*self.entries.borrow() {
entry.entry.set_label_layer(&layer);
}
} else {
error!(self.logger, "Cannot set layer {label_layer:?} for labels: the layer does not \
exist in the scene");
}
self.label_layer.set(label_layer);
}
fn create_new_entry(&self) -> DisplayedEntry<E> {
let layers = &self.app.display.scene().layers;
let layer = layers.get(self.label_layer.get()).unwrap_or_else(|| {
error!(self.logger, "Cannot set layer {self.label_layer:?} for labels: the layer does \
not exist in the scene");
layers.main.clone_ref()
});
let entry = DisplayedEntry {
id : default(),
entry : E::new(&self.app)
};
entry.entry.set_label_layer(&layer);
entry.entry.set_position_x(entry::PADDING);
self.add_child(&entry.entry);
entry
}
fn update_entry(logger:&Logger, entry:&DisplayedEntry<E>, id:entry::Id, model:&Option<E::Model>) {
debug!(logger, "Setting new model {model:?} for entry {id}; \
old entry: {entry.id.get():?}.");
entry.id.set(Some(id));
match model {
Some(model) => entry.entry.update(model),
None => {
error!(logger, "Model provider didn't return model for id {id}.");
entry.entry.update(&default());
}
};
entry.entry.set_position_y(Self::position_y_of_entry(id));
}
}
impl<E:CloneRef> display::Object for List<E> {
fn display_object(&self) -> &display::object::Instance { &self.display_object }
}

View File

@ -252,12 +252,12 @@ macro_rules! define_endpoints {
/// Frp network and endpoints.
#[derive(Debug,Clone,CloneRef)]
#[allow(missing_docs)]
pub struct Frp $(<$($param),*>)? {
pub struct Frp $(<$($param:Debug+'static),*>)? {
pub network : $crate::frp::Network,
pub output : FrpEndpoints $(<$($param),*>)?,
}
impl $(<$($param),*>)? Frp $(<$($param),*>)? {
impl $(<$($param:Debug+'static),*>)? Frp $(<$($param),*>)? {
/// Create Frp endpoints within and the associated network.
pub fn new() -> Self {
let network = $crate::frp::Network::new(file!());
@ -266,19 +266,19 @@ macro_rules! define_endpoints {
}
/// Create Frp endpoints within the provided network.
pub fn extend(network:&$crate::frp::Network) -> FrpEndpoints {
pub fn extend(network:&$crate::frp::Network) -> FrpEndpoints $(<$($param),*>)? {
let input = FrpInputs::new(network);
FrpEndpoints::new(network,input)
}
}
impl $(<$($param),*>)? Default for Frp $(<$($param),*>)? {
impl $(<$($param:Debug+'static),*>)? Default for Frp $(<$($param),*>)? {
fn default() -> Self {
Self::new()
}
}
impl $(<$($param),*>)? Deref for Frp $(<$($param),*>)? {
impl $(<$($param:Debug+'static),*>)? Deref for Frp $(<$($param),*>)? {
type Target = FrpEndpoints $(<$($param),*>)?;
fn deref(&self) -> &Self::Target {
&self.output
@ -289,18 +289,23 @@ macro_rules! define_endpoints {
#[derive(Debug,Clone,CloneRef)]
#[allow(missing_docs)]
#[allow(unused_parens)]
pub struct FrpInputs $(<$($param),*>)? {
$( $(#[doc=$($in_doc)*])* pub $in_field : $crate::frp::Any<($($in_field_type)*)>),*
// Clippy thinks `_param` is a field we want to add in future, but it is not: it is to
// suppress "not used generic param" error.
#[allow(clippy::manual_non_exhaustive)]
pub struct FrpInputs $(<$($param:Debug+'static),*>)? {
$( $(#[doc=$($in_doc)*])* pub $in_field : $crate::frp::Any<($($in_field_type)*)>,)*
_params : ($($(PhantomData<$param>),*)?),
}
#[allow(unused_parens)]
impl FrpInputs $(<$($param),*>)? {
impl $(<$($param:Debug+'static),*>)? FrpInputs $(<$($param),*>)? {
/// Constructor.
pub fn new(network:&$crate::frp::Network) -> Self {
$crate::frp::extend! { $($($global_opts)*)? $($($input_opts)*)? network
$($in_field <- any_mut();)*
}
Self { $($in_field),* }
let _params = default();
Self { $($in_field),*, _params }
}
$($crate::define_endpoints_emit_alias!{$in_field ($($in_field_type)*)})*
@ -310,7 +315,10 @@ macro_rules! define_endpoints {
#[derive(Debug,Clone,CloneRef)]
#[allow(unused_parens)]
#[allow(missing_docs)]
pub struct FrpEndpoints $(<$($param),*>)? {
// Clippy thinks `_param` is a field we want to add in future, but it is not: it is to
// suppress "not used generic param" error.
#[allow(clippy::manual_non_exhaustive)]
pub struct FrpEndpoints $(<$($param:Debug+'static),*>)? {
pub input : FrpInputs $(<$($param),*>)?,
// TODO[WD]: Consider making it private and exposing only on-demand with special macro
// usage syntax.
@ -318,20 +326,21 @@ macro_rules! define_endpoints {
pub status_map : Rc<RefCell<HashMap<String,$crate::frp::Sampler<bool>>>>,
pub command_map : Rc<RefCell<HashMap<String,$crate::application::command::Command>>>,
$($(#[doc=$($out_doc)*])*
pub $out_field : $crate::frp::Sampler<($($out_field_type)*)>
),*
pub $out_field : $crate::frp::Sampler<($($out_field_type)*)>,
)*
_params : ($($(PhantomData<$param>),*)?),
}
impl $(<$($param),*>)? Deref for FrpEndpoints $(<$($param),*>)? {
impl $(<$($param:Debug+'static),*>)? Deref for FrpEndpoints $(<$($param),*>)? {
type Target = FrpInputs $(<$($param),*>)?;
fn deref(&self) -> &Self::Target {
&self.input
}
}
impl $(<$($param),*>)? FrpEndpoints $(<$($param),*>)? {
impl $(<$($param:Debug+'static),*>)? FrpEndpoints $(<$($param),*>)? {
/// Constructor.
pub fn new(network:&$crate::frp::Network, input:FrpInputs) -> Self {
pub fn new(network:&$crate::frp::Network, input:FrpInputs $(<$($param),*>)?) -> Self {
use $crate::application::command::*;
let source = FrpOutputsSource::new(network);
let mut status_map : HashMap<String,$crate::frp::Sampler<bool>> = default();
@ -348,28 +357,34 @@ macro_rules! define_endpoints {
{command_map $in_field ($($in_field_type)*) input.$in_field })*
let status_map = Rc::new(RefCell::new(status_map));
let command_map = Rc::new(RefCell::new(command_map));
Self {source,input,status_map,command_map,$($out_field),*}
let _params = default();
Self {source,input,status_map,command_map,$($out_field),*,_params}
}
}
/// Frp output setters.
#[derive(Debug,Clone,CloneRef)]
#[allow(unused_parens)]
pub(crate) struct FrpOutputsSource $(<$($param),*>)? {
$(pub(crate) $out_field : $crate::frp::Any<($($out_field_type)*)>),*
// Clippy thinks `_param` is a field we want to add in future, but it is not: it is to
// suppress "not used generic param" error.
#[allow(clippy::manual_non_exhaustive)]
pub(crate) struct FrpOutputsSource $(<$($param:Debug+'static),*>)? {
$(pub(crate) $out_field : $crate::frp::Any<($($out_field_type)*)>,)*
_params : ($($(PhantomData<$param>),*)?),
}
impl $(<$($param),*>)? FrpOutputsSource $(<$($param),*>)? {
impl $(<$($param:Debug+'static),*>)? FrpOutputsSource $(<$($param),*>)? {
/// Constructor.
pub fn new(network:&$crate::frp::Network) -> Self {
$crate::frp::extend! { network
$($out_field <- any(...);)*
}
Self {$($out_field),*}
let _params = default();
Self {$($out_field),*,_params}
}
}
impl $crate::application::command::CommandApi for Frp {
impl $(<$($param:Debug+'static),*>)? $crate::application::command::CommandApi for Frp $(<$($param),*>)? {
fn command_api(&self)
-> Rc<RefCell<HashMap<String,$crate::application::command::Command>>> {
self.command_map.clone()
@ -380,7 +395,7 @@ macro_rules! define_endpoints {
}
}
impl $crate::application::command::FrpNetworkProvider for Frp {
impl $(<$($param:Debug+'static),*>)? $crate::application::command::FrpNetworkProvider for Frp $(<$($param),*>)? {
fn network(&self) -> &$crate::frp::Network { &self.network }
}
};

View File

@ -404,6 +404,7 @@ define_themes! { [light:0, dark:1]
text {
highlight = selection, Rgba(0.275,0.549,0.839,1.0); // ... , rgb(70 140 214)
selection = Lcha(0.7,0.0,0.125,0.7) , Lcha(0.7,0.0,0.125,0.7);
size = 12.0, 12.0;
}
}
}

View File

@ -39,6 +39,7 @@ use ide_view::graph_editor::GraphEditor;
use ide_view::graph_editor::SharedHashMap;
use utils::iter::split_by_predicate;
use futures::future::LocalBoxFuture;
use ide_view::searcher::entry::GlyphHighlightedLabel;
// ========================
@ -1105,7 +1106,7 @@ impl Model {
let list_is_empty = actions.matching_count() == 0;
let user_action = searcher.current_user_action();
let intended_function = searcher.intended_function_suggestion();
let provider = DataProviderForView
let provider = SuggestionsProviderForView
{ actions,user_action,intended_function};
self.view.searcher().set_actions(Rc::new(provider));
@ -1757,13 +1758,13 @@ pub enum AttachingResult<T>{
// ===========================
#[derive(Clone,Debug)]
struct DataProviderForView {
struct SuggestionsProviderForView {
actions : Rc<controller::searcher::action::List>,
user_action : controller::searcher::UserAction,
intended_function : Option<controller::searcher::action::Suggestion>,
}
impl DataProviderForView {
impl SuggestionsProviderForView {
fn doc_placeholder_for(suggestion:&controller::searcher::action::Suggestion) -> String {
let title = match suggestion.kind {
suggestion_database::entry::Kind::Atom => "Atom",
@ -1776,18 +1777,17 @@ impl DataProviderForView {
}
}
impl list_view::entry::ModelProvider for DataProviderForView {
impl list_view::entry::ModelProvider<GlyphHighlightedLabel> for SuggestionsProviderForView {
fn entry_count(&self) -> usize {
self.actions.matching_count()
}
fn get(&self, id: usize) -> Option<list_view::entry::Model> {
fn get(&self, id: usize) -> Option<list_view::entry::GlyphHighlightedLabelModel> {
let action = self.actions.get_cloned(id)?;
if let MatchInfo::Matches {subsequence} = action.match_info {
let caption = action.action.to_string();
let model = list_view::entry::Model::new(caption.clone());
let mut char_iter = caption.char_indices().enumerate();
let highlighted_iter = subsequence.indices.iter().filter_map(|idx| loop {
let label = action.action.to_string();
let mut char_iter = label.char_indices().enumerate();
let highlighted = subsequence.indices.iter().filter_map(|idx| loop {
if let Some(char) = char_iter.next() {
let (char_idx,(byte_id,char)) = char;
if char_idx == *idx {
@ -1798,16 +1798,15 @@ impl list_view::entry::ModelProvider for DataProviderForView {
} else {
break None;
}
});
let model = model.highlight(highlighted_iter);
Some(model)
}).collect();
Some(list_view::entry::GlyphHighlightedLabelModel {label,highlighted})
} else {
None
}
}
}
impl ide_view::searcher::DocumentationProvider for DataProviderForView {
impl ide_view::searcher::DocumentationProvider for SuggestionsProviderForView {
fn get(&self) -> Option<String> {
use controller::searcher::UserAction::*;
self.intended_function.as_ref().and_then(|function| match self.user_action {

View File

@ -147,10 +147,16 @@ impl VisualizationChooser {
// === Showing Entries ===
menu_appears <- menu.menu_visible.gate(&menu.menu_visible).constant(());
input_type_changed <- frp.set_vis_input_type.gate(&menu.menu_visible).constant(());
refresh_entries <- any(menu_appears,input_type_changed);
frp.source.entries <+ refresh_entries.map2(&frp.vis_input_type,f!([model] ((),input_type){
menu_appears <- menu.menu_visible.gate(&menu.menu_visible).constant(());
// We want to update entries according to the input type, but only when it changed and
// menu is visible.
input_type_when_visible <- frp.set_vis_input_type.gate(&menu.menu_visible);
input_type_when_appeared <- frp.set_vis_input_type.sample(&menu_appears);
input_type <- any(input_type_when_visible,input_type_when_appeared);
input_type_changed <- input_type.on_change();
frp.source.entries <+ input_type_changed.map(f!([model] (input_type){
let entries = Rc::new(model.entries(input_type));
let provider = list_view::entry::AnyModelProvider::from(entries.clone_ref());
model.selection_menu.set_entries(provider);

View File

@ -64,3 +64,9 @@ impl Display for Path {
f.write_str(&self.name)
}
}
impl From<Path> for String {
fn from(path:Path) -> Self {
path.to_string()
}
}

View File

@ -83,12 +83,15 @@ impl<T:DocumentationProvider + 'static> From<Rc<T>> for AnyDocumentationProvider
// === Model ===
// =============
/// A type of ListView entry used in searcher.
pub type Entry = list_view::entry::GlyphHighlightedLabel;
#[derive(Clone,CloneRef,Debug)]
struct Model {
app : Application,
logger : Logger,
display_object : display::object::Instance,
list : ListView,
list : ListView<Entry>,
documentation : documentation::View,
doc_provider : Rc<CloneRefCell<AnyDocumentationProvider>>,
}
@ -99,7 +102,7 @@ impl Model {
let app = app.clone_ref();
let logger = Logger::new("SearcherView");
let display_object = display::object::Instance::new(&logger);
let list = app.new_view::<ListView>();
let list = app.new_view::<ListView<Entry>>();
let documentation = documentation::View::new(scene);
let doc_provider = default();
scene.layers.above_nodes.add_exclusive(&list);
@ -143,7 +146,7 @@ ensogl::define_endpoints! {
Input {
/// Use the selected action as a suggestion and add it to the current input.
use_as_suggestion (),
set_actions (entry::AnyModelProvider,AnyDocumentationProvider),
set_actions (entry::AnyModelProvider<list_view::entry::GlyphHighlightedLabel>,AnyDocumentationProvider),
select_action (entry::Id),
show (),
hide (),
@ -234,9 +237,9 @@ impl View {
/// The list is represented list-entry-model and documentation provider. It's a helper for FRP
/// `set_suggestion` input (FRP nodes cannot be generic).
pub fn set_actions
(&self, provider:Rc<impl list_view::entry::ModelProvider + DocumentationProvider + 'static>) {
let entries : list_view::entry::AnyModelProvider = provider.clone_ref().into();
let documentation : AnyDocumentationProvider = provider.into();
(&self, provider:Rc<impl list_view::entry::ModelProvider<Entry> + DocumentationProvider + 'static>) {
let entries : list_view::entry::AnyModelProvider<Entry> = provider.clone_ref().into();
let documentation : AnyDocumentationProvider = provider.into();
self.frp.set_actions(entries,documentation);
}