mirror of
https://github.com/enso-org/enso.git
synced 2024-12-03 04:45:25 +03:00
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:
parent
86df862f01
commit
6d9785375d
@ -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);
|
||||
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
@ -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 }
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
211
gui/src/rust/ensogl/lib/components/src/list_view/entry/list.rs
Normal file
211
gui/src/rust/ensogl/lib/components/src/list_view/entry/list.rs
Normal 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 }
|
||||
}
|
@ -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 }
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user