Sync breadcrumbs and documentation panel. (#7508)

Implements #7310.

![Peek 2023-08-09 16-07](https://github.com/enso-org/enso/assets/1428930/1a244e38-5c34-4c8b-8885-1cf84ac7b6a7)
This commit is contained in:
Michael Mauderer 2023-08-15 12:01:24 +01:00 committed by GitHub
parent 7a272ec152
commit 7f19b09d13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 192 additions and 109 deletions

31
Cargo.lock generated
View File

@ -2203,6 +2203,7 @@ dependencies = [
"enso-text",
"enso-web",
"ensogl",
"ensogl-breadcrumbs",
"ensogl-component",
"ensogl-drop-manager",
"ensogl-dynamic-assets",
@ -2629,6 +2630,19 @@ dependencies = [
"ensogl-text",
]
[[package]]
name = "ensogl-breadcrumbs"
version = "0.1.0"
dependencies = [
"enso-frp",
"ensogl-core",
"ensogl-derive-theme",
"ensogl-grid-view",
"ensogl-hardcoded-theme",
"ensogl-icons",
"ensogl-text",
]
[[package]]
name = "ensogl-button"
version = "0.1.0"
@ -4328,11 +4342,11 @@ dependencies = [
"enso-frp",
"enso-prelude",
"ensogl",
"ensogl-breadcrumbs",
"ensogl-gui-component",
"ensogl-hardcoded-theme",
"ensogl-text",
"ide-view-component-list-panel",
"ide-view-component-list-panel-breadcrumbs",
"ide-view-documentation",
"ide-view-graph-editor",
]
@ -4357,19 +4371,6 @@ dependencies = [
"ordered-float",
]
[[package]]
name = "ide-view-component-list-panel-breadcrumbs"
version = "0.1.0"
dependencies = [
"enso-frp",
"ensogl-core",
"ensogl-derive-theme",
"ensogl-grid-view",
"ensogl-hardcoded-theme",
"ensogl-icons",
"ensogl-text",
]
[[package]]
name = "ide-view-component-list-panel-grid"
version = "0.1.0"
@ -4398,11 +4399,11 @@ dependencies = [
"enso-profiler",
"enso-suggestion-database",
"ensogl",
"ensogl-breadcrumbs",
"ensogl-component",
"ensogl-hardcoded-theme",
"horrorshow",
"ide-ci",
"ide-view-component-list-panel-breadcrumbs",
"ide-view-graph-editor",
"serde_json",
"tokio",

View File

@ -33,6 +33,7 @@ ensogl-dynamic-assets = { path = "../../lib/rust/ensogl/component/dynamic-assets
ensogl-text-msdf = { path = "../../lib/rust/ensogl/component/text/src/font/msdf" }
ensogl-hardcoded-theme = { path = "../../lib/rust/ensogl/app/theme/hardcoded" }
ensogl-drop-manager = { path = "../../lib/rust/ensogl/component/drop-manager" }
ensogl-breadcrumbs = { path = "../../lib/rust/ensogl/component/breadcrumbs" }
fuzzly = { path = "../../lib/rust/fuzzly" }
ast = { path = "language/ast/impl" }
parser = { path = "language/parser" }

View File

@ -5,6 +5,7 @@ use crate::prelude::*;
use crate::controller::graph::ImportType;
use crate::controller::graph::RequiredImport;
use crate::controller::searcher::breadcrumbs::BreadcrumbEntry;
use crate::model::execution_context::GroupQualifiedName;
use crate::model::module::NodeEditStatus;
use crate::model::suggestion_database;
@ -354,16 +355,7 @@ impl Searcher {
}
/// Enter the specified module. The displayed content of the browser will be updated.
pub fn enter_entry(&self, entry: usize) -> FallibleResult {
let id = {
let data = self.data.borrow();
let error = || NoSuchComponent { index: entry };
let component = data.components.displayed().get(entry).ok_or_else(error)?;
component.id().ok_or(NotEnterable { index: entry })?
};
let bc_builder = breadcrumbs::Builder::new(&self.database);
let breadcrumbs = bc_builder.build(id);
self.breadcrumbs.set_content(breadcrumbs);
pub fn enter_entry(&self, _entry: usize) -> FallibleResult {
self.reload_list();
Ok(())
}
@ -374,11 +366,43 @@ impl Searcher {
self.breadcrumbs.names()
}
/// Select the breadcrumb with the index [`id`]. The displayed content of the browser will be
/// updated.
/// Set the selected breadcrumb. The `id` is the index of the breadcrumb from left to right.
pub fn select_breadcrumb(&self, id: usize) {
self.breadcrumbs.select(id);
self.reload_list();
}
/// Set the breadcrumbs to match the component at the given `index`. The index refers to the
/// displayed list of components. Returns the full breadcrumb for the entry, if there is one.
pub fn update_breadcrumbs(&self, index: usize) -> Option<Vec<BreadcrumbEntry>> {
let data = self.data.borrow();
if let Some(component) = data.components.displayed().get(index) {
if let Some(id) = component.id() {
let bc_builder = breadcrumbs::Builder::new(&self.database);
let breadcrumbs = bc_builder.build(id).collect_vec();
assert!(breadcrumbs.iter().all(|e| self.database.lookup(e.id()).is_ok()));
self.breadcrumbs.set_content(breadcrumbs.clone().into_iter());
Some(breadcrumbs)
} else {
warn!(
"Cannot update breadcrumbs with component that has no suggestion database \
entry. Invalid component: {:?}",
component
);
None
}
} else {
warn!("Update breadcrumbs called with invalid index: {}", index);
None
}
}
/// Return the documentation for the breadcrumb.
pub fn documentation_for_selected_breadcrumb(&self) -> Option<EntryDocumentation> {
let selected = self.breadcrumbs.selected();
let component = selected?;
assert!(self.database.lookup(component).is_ok());
let docs = self.database.documentation_for_entry(component);
Some(docs)
}
/// Set the Searcher Input.
@ -1217,30 +1241,6 @@ pub mod test {
assert_eq!(notification, Some(Notification::NewComponentList));
}
#[test]
fn entering_module() {
let mut fixture =
Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| {
data.expect_completion(client, None, &(0..11).collect_vec());
data.expect_completion(client, None, &(0..11).collect_vec());
});
let searcher = &fixture.searcher;
searcher.reload_list();
fixture.test.run_until_stalled();
// There are two virtual entries and two top-modules.
assert_eq!(searcher.components().displayed().len(), 4);
let mut subscriber = searcher.subscribe();
searcher.enter_entry(3).expect("Entering entry failed");
fixture.test.run_until_stalled();
let list = searcher.components();
assert_eq!(list.displayed().len(), 1);
assert_eq!(list.displayed()[0].suggestion, fixture.test_method_3_suggestion());
let notification = subscriber.next().boxed_local().expect_ready();
assert_eq!(notification, Some(Notification::NewComponentList));
}
#[test]
fn picked_completions_list_maintaining() {
let fixture = Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| {

View File

@ -6,6 +6,7 @@ use crate::model::suggestion_database;
use double_representation::name::QualifiedName;
use double_representation::name::QualifiedNameRef;
use ensogl_icons::icon;
use model::suggestion_database::Entry;
@ -32,9 +33,11 @@ impl Breadcrumbs {
/// Set the list of breadcrumbs to be displayed in the breadcrumbs panel.
pub fn set_content(&self, breadcrumbs: impl Iterator<Item = BreadcrumbEntry>) {
let breadcrumbs: Vec<_> = breadcrumbs.collect();
let mut borrowed = self.list.borrow_mut();
*borrowed = breadcrumbs.collect();
self.select(borrowed.len());
*borrowed = breadcrumbs;
let len = borrowed.len();
self.select(len.saturating_sub(1));
}
/// A list of breadcrumbs' text labels to be displayed in the panel.
@ -60,12 +63,8 @@ impl Breadcrumbs {
/// Returns a currently selected breadcrumb id. Returns [`None`] if the top level breadcrumb
/// is selected.
pub fn selected(&self) -> Option<suggestion_database::entry::Id> {
if self.is_top_module() {
None
} else {
let index = self.selected.get();
self.list.borrow().get(index - 1).map(BreadcrumbEntry::id)
}
self.list.borrow().get(index).map(BreadcrumbEntry::id)
}
}
@ -81,6 +80,7 @@ pub struct BreadcrumbEntry {
displayed_name: ImString,
component_id: suggestion_database::entry::Id,
qualified_name: QualifiedName,
icon: Option<icon::Id>,
}
impl BreadcrumbEntry {
@ -98,18 +98,33 @@ impl BreadcrumbEntry {
pub fn qualified_name(&self) -> &QualifiedName {
&self.qualified_name
}
/// An icon of the entry.
pub fn icon(&self) -> Option<icon::Id> {
self.icon
}
/// Return a [`ensogl_breadcrumbs::Breadcrumb`] with the entries name and icon.
pub fn view_without_icon(&self) -> ensogl_breadcrumbs::Breadcrumb {
ensogl_breadcrumbs::Breadcrumb::new(self.name().as_str(), None)
}
/// Return a [`ensogl_breadcrumbs::Breadcrumb`] with the entries name but no icon.
pub fn view_with_icon(&self) -> ensogl_breadcrumbs::Breadcrumb {
ensogl_breadcrumbs::Breadcrumb::new(self.name().as_str(), self.icon())
}
}
impl From<(suggestion_database::entry::Id, Rc<Entry>)> for BreadcrumbEntry {
fn from((component_id, entry): (suggestion_database::entry::Id, Rc<Entry>)) -> Self {
let qualified_name = entry.qualified_name();
let displayed_name = entry.name.clone();
BreadcrumbEntry { displayed_name, component_id, qualified_name }
let icon = Some(entry.as_ref().icon());
BreadcrumbEntry { displayed_name, component_id, qualified_name, icon }
}
}
// ===============
// === Builder ===
// ===============

View File

@ -86,10 +86,7 @@ impl Model {
) -> Option<(ViewNodeId, text::Range<text::Byte>, ImString)> {
let new_code = self.controller.use_suggestion_by_index(id);
match new_code {
Ok(text::Change { range, text }) => {
self.update_breadcrumbs();
Some((self.input_view, range, text.into()))
}
Ok(text::Change { range, text }) => Some((self.input_view, range, text.into())),
Err(err) => {
error!("Error while applying suggestion: {err}.");
None
@ -101,20 +98,23 @@ impl Model {
self.controller.select_breadcrumb(id);
}
fn update_breadcrumbs(&self) {
let names = self.controller.breadcrumbs().into_iter();
let browser = &self.view;
// We only update the breadcrumbs starting from the second element because the first
// one is reserved as a section name.
let from = 1;
let breadcrumbs_from = (names.map(Into::into).collect(), from);
browser.model().documentation.breadcrumbs.set_entries_from(breadcrumbs_from);
fn module_entered(&self, entry: component_grid::EntryId) {
if let Err(error) = self.controller.enter_entry(entry) {
error!("Failed to enter entry in Component Browser: {error}")
}
}
fn module_entered(&self, entry: component_grid::EntryId) {
match self.controller.enter_entry(entry) {
Ok(()) => self.update_breadcrumbs(),
Err(error) => error!("Failed to enter entry in Component Browser: {error}"),
fn update_breadcrumbs(&self, target_entry: component_grid::EntryId) {
let breadcrumbs = self.controller.update_breadcrumbs(target_entry);
if let Some(breadcrumbs) = breadcrumbs {
let browser = &self.view;
let breadcrumbs_count = breadcrumbs.len();
let without_icon =
breadcrumbs[0..breadcrumbs_count - 1].iter().map(|crumb| crumb.view_without_icon());
let with_icon =
breadcrumbs[breadcrumbs_count - 1..].iter().map(|crumb| crumb.view_with_icon());
let all = without_icon.chain(with_icon).collect_vec();
browser.model().documentation.breadcrumbs.set_entries(all);
}
}
@ -138,9 +138,19 @@ impl Model {
self.controller.documentation_for_entry(id)
}
fn docs_for_breadcrumb(&self) -> Option<EntryDocumentation> {
self.controller.documentation_for_selected_breadcrumb()
}
fn should_select_first_entry(&self) -> bool {
self.controller.is_filtering() || self.controller.is_input_empty()
}
fn on_entry_for_docs_selected(&self, id: Option<component_grid::EntryId>) {
if let Some(id) = id {
self.update_breadcrumbs(id);
}
}
}
/// The Searcher presenter, synchronizing state between searcher view and searcher controller.
@ -267,12 +277,17 @@ impl ComponentBrowserSearcher {
docs <- docs_params.filter_map(f!([model]((_, entry)) {
entry.map(|entry_id| model.documentation_of_component(entry_id))
}));
docs_from_breadcrumbs <- breadcrumbs.selected.map(f!((selected){
model.breadcrumb_selected(*selected);
model.docs_for_breadcrumb()
})).unwrap();
docs <- any(docs,docs_from_breadcrumbs);
documentation.frp.display_documentation <+ docs;
eval grid.active ((entry) model.on_entry_for_docs_selected(*entry));
eval_ grid.suggestion_accepted([]analytics::remote_log_event("component_browser::suggestion_accepted"));
eval grid.active((entry) model.suggestion_selected(*entry));
eval grid.module_entered((id) model.module_entered(*id));
eval breadcrumbs.selected((id) model.breadcrumb_selected(*id));
}
let weak_model = Rc::downgrade(&model);

View File

@ -71,7 +71,7 @@ impl Default for EntryDocumentation {
pub struct LinkedDocPage {
/// The name of the liked entry. It is used to produce a unique ID for the link.
pub name: Rc<QualifiedName>,
/// The intermediate reprentation of the linked entry's documentation.
/// The intermediate representation of the linked entry's documentation.
pub page: EntryDocumentation,
}

View File

@ -19,6 +19,7 @@ use enso_doc_parser::doc_sections::HtmlString;
use enso_doc_parser::DocSection;
use enso_doc_parser::Tag;
use enso_text::Location;
use ensogl_icons::icon;
use language_server::types::FieldAction;
@ -528,6 +529,17 @@ impl Entry {
// === Other Properties ===
macro_rules! kind_to_icon {
([ $( $variant:ident ),* ] $kind:ident) => {
{
use ensogl_icons::icon::Id;
match $kind {
$( Kind::$variant => Id::$variant, )*
}
}
}
}
impl Entry {
/// Return the Method Id of suggested method.
///
@ -621,6 +633,14 @@ impl Entry {
})
.flatten()
}
/// Returns the icon of the entry.
pub fn icon(&self) -> icon::Id {
let kind = self.kind;
let icon_name = self.icon_name.as_ref();
let icon = icon_name.and_then(|n| n.to_pascal_case().parse().ok());
icon.unwrap_or_else(|| for_each_kind_variant!(kind_to_icon(kind)))
}
}

View File

@ -13,7 +13,7 @@ ensogl = { path = "../../../../lib/rust/ensogl" }
ensogl-gui-component = { path = "../../../../lib/rust/ensogl/component/gui" }
ensogl-hardcoded-theme = { path = "../../../../lib/rust/ensogl/app/theme/hardcoded" }
enso-prelude = { path = "../../../../lib/rust/prelude" }
ide-view-component-list-panel-breadcrumbs = { path = "../../../../lib/rust/ensogl/component/breadcrumbs" }
ensogl-breadcrumbs = { path = "../../../../lib/rust/ensogl/component/breadcrumbs" }
ide-view-component-list-panel = { path = "component-list-panel" }
ide-view-documentation = { path = "../documentation" }
ide-view-graph-editor = { path = "../graph-editor" }

View File

@ -35,8 +35,8 @@ use ide_view_graph_editor::component::node::HEIGHT as NODE_HEIGHT;
// === Export ===
// ==============
pub use ensogl_breadcrumbs as breadcrumbs;
pub use ide_view_component_list_panel as component_list_panel;
pub use ide_view_component_list_panel_breadcrumbs as breadcrumbs;

View File

@ -16,7 +16,7 @@ ensogl = { path = "../../../../lib/rust/ensogl" }
ensogl-component = { path = "../../../../lib/rust/ensogl/component" }
ensogl-hardcoded-theme = { path = "../../../../lib/rust/ensogl/app/theme/hardcoded" }
ide-view-graph-editor = { path = "../graph-editor" }
ide-view-component-list-panel-breadcrumbs = { path = "../../../../lib/rust/ensogl/component/breadcrumbs" }
ensogl-breadcrumbs = { path = "../../../../lib/rust/ensogl/component/breadcrumbs" }
wasm-bindgen = { workspace = true }
serde_json = { workspace = true }
horrorshow = "0.8.4"

View File

@ -44,7 +44,7 @@ use ide_view_graph_editor as graph_editor;
pub mod html;
pub use ide_view_component_list_panel_breadcrumbs as breadcrumbs;
pub use ensogl_breadcrumbs as breadcrumbs;
@ -151,8 +151,8 @@ impl Model {
fn set_initial_breadcrumbs(&self) {
let breadcrumb = breadcrumbs::Breadcrumb::new(INITIAL_SECTION_NAME, None);
self.breadcrumbs.set_entries_from((vec![breadcrumb], 0));
self.breadcrumbs.show_ellipsis(true);
self.breadcrumbs.set_entries(vec![breadcrumb]);
self.breadcrumbs.show_ellipsis(false);
}
/// Set size of the documentation view.

View File

@ -145,7 +145,7 @@ pub fn main() {
panel.show();
let breadcrumbs = app.new_view::<Breadcrumbs>();
breadcrumbs.set_y(400.0);
breadcrumbs.set_y(500.0);
breadcrumbs.frp().set_size(Vector2(500.0, 100.0));
breadcrumbs.set_entries_from((
vec![

View File

@ -383,6 +383,8 @@ define_themes! { [light:0, dark:1]
text_y_offset = 6.0, 6.0;
text_padding_left = 0.0, 0.0;
text_size = 11.5, 11.5;
icon_x_offset = 2.0, 2.0;
icon_y_offset = 6.0, 6.0;
selected_color = Rgba(1.0, 1.0, 1.0, 1.0), Rgba(1.0, 1.0, 1.0, 1.0);
highlight_corners_radius = 15.0, 15.0;
greyed_out_color = Rgba(1.0, 1.0, 1.0, 0.15), Rgba(1.0, 1.0, 1.0, 0.15);

View File

@ -1,5 +1,5 @@
[package]
name = "ide-view-component-list-panel-breadcrumbs"
name = "ensogl-breadcrumbs"
version = "0.1.0"
authors = ["Enso Team <contact@enso.org>"]
edition = "2021"

View File

@ -113,6 +113,8 @@ pub struct Style {
pub text_y_offset: f32,
pub text_padding_left: f32,
pub text_size: f32,
pub icon_x_offset: f32,
pub icon_y_offset: f32,
pub selected_color: color::Rgba,
pub highlight_corners_radius: f32,
pub greyed_out_color: color::Rgba,
@ -125,7 +127,7 @@ pub struct Style {
/// A model for the entry in the breadcrumbs list.
#[allow(missing_docs)]
#[derive(Clone, CloneRef, Debug, Default)]
#[derive(Clone, CloneRef, Debug, Default, PartialEq)]
pub enum Model {
#[default]
Ellipsis,
@ -186,8 +188,8 @@ impl EntryData {
let separator = separator::View::new();
let state = default();
let icon: any_icon::View = default();
icon.set_size((ICON_WIDTH, ICON_WIDTH));
ellipsis.set_size((ellipsis::ICON_WIDTH, ellipsis::ICON_WIDTH));
icon.set_size((ICON_WIDTH, ICON_WIDTH));
display_object.add_child(&icon);
display_object.add_child(&ellipsis);
Self { display_object, state, text, ellipsis, separator, icon }
@ -223,7 +225,6 @@ impl EntryData {
if let Some(icon) = icon {
self.display_object.add_child(&self.icon);
self.icon.icon.set(icon.any_cached_shape_location());
self.text.set_x(ICON_WIDTH);
} else {
self.icon.unset_parent();
self.text.set_x(0.0);
@ -256,15 +257,22 @@ impl EntryData {
}
}
fn update_layout(&self, contour: Contour, text_padding: f32, text_y_offset: f32) {
fn update_layout(
&self,
contour: Contour,
text_padding: f32,
text_y_offset: f32,
icon_x_offset: f32,
icon_y_offset: f32,
) {
let size = contour.size;
let icon_offset = if self.has_icon() { ICON_WIDTH } else { 0.0 };
self.text.set_xy(Vector2(icon_offset + text_padding - size.x / 2.0, text_y_offset));
self.separator.set_size(Vector2(separator::ICON_WIDTH, size.y));
self.ellipsis.set_size(Vector2(ellipsis::ICON_WIDTH, size.y));
self.icon.set_size(Vector2(ICON_WIDTH, size.y));
self.icon.set_x(-ICON_WIDTH);
self.icon.set_y(-2.0);
self.icon.set_x(-size.x / 2.0 - icon_x_offset);
self.icon.set_y(-ICON_WIDTH - icon_y_offset);
}
fn set_default_color(&self, color: color::Lcha) {
@ -372,10 +380,16 @@ impl ensogl_grid_view::Entry for Entry {
text_color <- input.set_params.map(|p| p.style.selected_color).cloned_into().on_change();
text_y_offset <- input.set_params.map(|p| p.style.text_y_offset).on_change();
text_size <- input.set_params.map(|p| p.style.text_size).on_change();
icon_x_offset <- input.set_params.map(|p| p.style.icon_x_offset).on_change();
icon_y_offset <- input.set_params.map(|p| p.style.icon_y_offset).on_change();
greyed_out_color <- input.set_params.map(|p| p.style.greyed_out_color).cloned_into().on_change();
highlight_corners_radius <- input.set_params.map(|p| p.style.highlight_corners_radius).on_change();
greyed_out_from <- input.set_params.map(|p| p.greyed_out_start).on_change();
transparent_color <- init.constant(color::Lcha::transparent());
new_model <- input.set_model.on_change();
// === Appearance ===
col <- input.set_location._1();
should_grey_out <- all_with(&col, &greyed_out_from,
@ -397,7 +411,6 @@ impl ensogl_grid_view::Entry for Entry {
size: *size - Vector2(*margin, *margin) * 2.0,
corners_radius: 0.0,
});
eval color((c) data.set_default_color(*c));
eval font((f) data.set_font(f.to_string()));
eval text_size((s) data.set_default_text_size(*s));
@ -410,24 +423,35 @@ impl ensogl_grid_view::Entry for Entry {
out.hover_highlight_color <+ hover_color;
out.selection_highlight_color <+ init.constant(color::Lcha::transparent());
// === Override column width ===
// We need to adjust the width of the grid view column depending on the width of
// the entry.
out.override_column_width <+ input.set_model.map2(&text_padding,
f!([data](model, text_padding) {
data.set_model(model);
data.width(*text_padding)
})
);
// For text entries, we also listen for [`Text::width`] changes.
text_width <- data.text.width.filter(f_!(data.is_text_displayed()));
entry_width <- text_width.map2(&text_padding, f!((w, o) data.text_width(*w, *o)));
out.override_column_width <+ entry_width;
layout <- all(contour, text_padding, text_y_offset, input.set_model);
eval layout ((&(c, to, tyo, _)) data.update_layout(c, to, tyo));
// === Layout ===
override_column_width <- new_model.map2(&text_padding,
f!([data](model, text_padding) {
data.set_model(model);
data.width(*text_padding)
})
);
layout <- all6(
&contour,
&text_padding,
&text_y_offset,
&new_model,
&icon_x_offset,
&icon_y_offset);
eval layout ((&(c, to, tyo, _, ix, iy)) data.update_layout(c, to, tyo, ix, iy));
// === Override column width ===
// We need to adjust the width of the grid view column depending on the width of
// the entry.
out.override_column_width <+ override_column_width;
}
init.emit(());
Self { frp, data }

View File

@ -477,6 +477,8 @@ ensogl_core::define_endpoints_2! {
push(Breadcrumb),
/// Set the displayed breadcrumbs starting from the specific index.
set_entries_from((Vec<Breadcrumb>, BreadcrumbId)),
/// Set the displayed breadcrumbs.
set_entries(Vec<Breadcrumb>),
/// Set the breadcrumb at a specified index.
set_entry((BreadcrumbId, Breadcrumb)),
/// Enable or disable displaying of the ellipsis icon at the end of the list.
@ -534,7 +536,10 @@ impl Breadcrumbs {
eval_ input.clear(model.clear());
selected <- selected_grid_col.map(|(_, col)| col / 2);
eval input.push((b) model.push(b));
eval input.set_entries_from(((entries, from)) model.set_entries(entries, *from));
set_entries_from_zero <- input.set_entries.map(|entries| (entries.clone(), 0));
set_entries_from <- any(set_entries_from_zero, input.set_entries_from);
eval set_entries_from(((entries, from)) model.set_entries(entries, *from));
eval input.set_entry(((index, entry)) model.set_entry(entry, *index));
out.selected <+ selected;