Open Project Dialog (#5607)

Closes #5022

This is basically a reimplementation of the Open Project Dialog that was present in the IDE a while ago. Now it uses the modern shiny `grid-view` instead of the old rusty `list-view`.

https://user-images.githubusercontent.com/6566674/219052041-ff99aa37-249c-4a63-93a5-5acd6b221dc8.mp4
This commit is contained in:
Ilya Bogdanov 2023-02-20 18:47:48 +04:00 committed by GitHub
parent 172f72941b
commit 19beb01cf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 506 additions and 288 deletions

1
Cargo.lock generated
View File

@ -4166,6 +4166,7 @@ dependencies = [
"enso-shapely",
"ensogl",
"ensogl-component",
"ensogl-derive-theme",
"ensogl-gui-component",
"ensogl-hardcoded-theme",
"ensogl-text",

View File

@ -106,6 +106,8 @@ pub enum Notification {
NewProjectCreated,
/// User opened an existing project.
ProjectOpened,
/// User closed the project.
ProjectClosed,
}
@ -141,6 +143,9 @@ pub trait ManagingProjectAPI {
/// Open the project with given UUID.
fn open_project(&self, id: Uuid) -> BoxFuture<FallibleResult>;
/// Close the currently opened project. Does nothing if no project is open.
fn close_project(&self);
/// Open project by name. It makes two calls to the Project Manager: one for listing projects
/// and then for the project opening.
fn open_project_by_name(&self, name: String) -> BoxFuture<FallibleResult> {

View File

@ -92,6 +92,7 @@ impl API for Handle {
fn current_project(&self) -> Option<model::Project> {
self.current_project.get()
}
fn status_notifications(&self) -> &StatusNotificationPublisher {
&self.status_notifications
}
@ -132,13 +133,17 @@ impl ManagingProjectAPI for Handle {
let project_mgr = self.project_manager.clone_ref();
let new_project = Project::new_opened(project_mgr, new_project_id);
self.current_project.set(Some(new_project.await?));
let notify = self.notifications.publish(Notification::NewProjectCreated);
executor::global::spawn(notify);
self.notifications.notify(Notification::NewProjectCreated);
Ok(())
}
.boxed_local()
}
fn close_project(&self) {
self.current_project.set(None);
self.notifications.notify(Notification::ProjectClosed);
}
#[profile(Objective)]
fn list_projects(&self) -> BoxFuture<FallibleResult<Vec<ProjectMetadata>>> {
async move { Ok(self.project_manager.list_projects(&None).await?.projects) }.boxed_local()
@ -150,7 +155,7 @@ impl ManagingProjectAPI for Handle {
let project_mgr = self.project_manager.clone_ref();
let new_project = model::project::Synchronized::new_opened(project_mgr, id);
self.current_project.set(Some(new_project.await?));
executor::global::spawn(self.notifications.publish(Notification::ProjectOpened));
self.notifications.notify(Notification::ProjectOpened);
Ok(())
}
.boxed_local()

View File

@ -82,6 +82,13 @@ impl Model {
});
}
fn close_project(&self) {
*self.current_project.borrow_mut() = None;
// Clear the graph editor so that it will not display any nodes from the previous
// project when the new project is loaded.
self.view.project().graph().remove_all_nodes();
}
/// Open a project by name. It makes two calls to Project Manager: one for listing projects and
/// a second one for opening the project.
#[profile(Task)]
@ -211,6 +218,9 @@ impl Presenter {
controller::ide::Notification::NewProjectCreated
| controller::ide::Notification::ProjectOpened =>
model.setup_and_display_new_project(),
controller::ide::Notification::ProjectClosed => {
model.close_project();
}
}
futures::future::ready(())
});

View File

@ -8,6 +8,7 @@ use crate::presenter;
use crate::presenter::graph::ViewNodeId;
use enso_frp as frp;
use ensogl::system::js;
use ide_view as view;
use ide_view::project::SearcherParams;
use model::module::NotificationKind;
@ -16,6 +17,16 @@ use model::project::VcsStatus;
// =================
// === Constants ===
// =================
/// We don't know how long the project opening will take, but we still want to show a fake progress
/// indicator for the user. This constant represents a progress percentage that will be displayed.
const OPEN_PROJECT_SPINNER_PROGRESS: f32 = 0.8;
// =============
// === Model ===
// =============
@ -24,15 +35,16 @@ use model::project::VcsStatus;
#[allow(unused)]
#[derive(Debug)]
struct Model {
controller: controller::Project,
module_model: model::Module,
graph_controller: controller::ExecutedGraph,
ide_controller: controller::Ide,
view: view::project::View,
status_bar: view::status_bar::View,
graph: presenter::Graph,
code: presenter::Code,
searcher: RefCell<Option<presenter::Searcher>>,
controller: controller::Project,
module_model: model::Module,
graph_controller: controller::ExecutedGraph,
ide_controller: controller::Ide,
view: view::project::View,
status_bar: view::status_bar::View,
graph: presenter::Graph,
code: presenter::Code,
searcher: RefCell<Option<presenter::Searcher>>,
available_projects: Rc<RefCell<Vec<(ImString, Uuid)>>>,
}
impl Model {
@ -54,6 +66,7 @@ impl Model {
);
let code = presenter::Code::new(text_controller, &view);
let searcher = default();
let available_projects = default();
Model {
controller,
module_model,
@ -64,6 +77,7 @@ impl Model {
graph,
code,
searcher,
available_projects,
}
}
@ -213,8 +227,53 @@ impl Model {
}
})
}
}
/// Prepare a list of projects to display in the Open Project dialog.
fn project_list_opened(&self, project_list_ready: frp::Source<()>) {
let controller = self.ide_controller.clone_ref();
let projects_list = self.available_projects.clone_ref();
executor::global::spawn(async move {
if let Ok(api) = controller.manage_projects() {
if let Ok(projects) = api.list_projects().await {
let projects = projects.into_iter();
let projects = projects.map(|p| (p.name.clone().into(), p.id)).collect_vec();
*projects_list.borrow_mut() = projects;
project_list_ready.emit(());
}
}
})
}
/// User clicked a project in the Open Project dialog. Open it.
fn open_project(&self, id_in_list: &usize) {
let controller = self.ide_controller.clone_ref();
let projects_list = self.available_projects.clone_ref();
let view = self.view.clone_ref();
let status_bar = self.status_bar.clone_ref();
let id = *id_in_list;
executor::global::spawn(async move {
let app = js::app_or_panic();
app.show_progress_indicator(OPEN_PROJECT_SPINNER_PROGRESS);
view.hide_graph_editor();
if let Ok(api) = controller.manage_projects() {
api.close_project();
let uuid = projects_list.borrow().get(id).map(|(_name, uuid)| *uuid);
if let Some(uuid) = uuid {
if let Err(error) = api.open_project(uuid).await {
error!("Error opening project: {error}.");
status_bar.add_event(format!("Error opening project: {error}."));
}
} else {
error!("Project with id {id} not found.");
}
} else {
error!("Project Manager API not available, cannot open project.");
}
app.hide_progress_indicator();
view.show_graph_editor();
})
}
}
// ===============
@ -255,8 +314,29 @@ impl Project {
let view = &model.view.frp;
let breadcrumbs = &model.view.graph().model.breadcrumbs;
let graph_view = &model.view.graph().frp;
let project_list = &model.view.project_list();
frp::extend! { network
project_list_ready <- source_();
project_list.grid.reset_entries <+ project_list_ready.map(f_!([model]{
let cols = 1;
let rows = model.available_projects.borrow().len();
(rows, cols)
}));
entry_model <- project_list.grid.model_for_entry_needed.map(f!([model]((row, col)) {
let projects = model.available_projects.borrow();
let project = projects.get(*row);
project.map(|(name, _)| (*row, *col, name.clone_ref()))
})).filter_map(|t| t.clone());
project_list.grid.model_for_entry <+ entry_model;
open_project_list <- view.project_list_shown.on_true();
eval_ open_project_list(model.project_list_opened(project_list_ready.clone_ref()));
selected_project <- project_list.grid.entry_selected.filter_map(|e| *e);
eval selected_project(((row, _col)) model.open_project(row));
project_list.grid.select_entry <+ selected_project.constant(None);
eval view.searcher ([model](params) {
if let Some(params) = params {
model.setup_searcher_presenter(*params)

View File

@ -17,6 +17,7 @@ enso-shapely = { path = "../../../lib/rust/shapely" }
engine-protocol = { path = "../controller/engine-protocol" }
ensogl = { path = "../../../lib/rust/ensogl" }
ensogl-component = { path = "../../../lib/rust/ensogl/component" }
ensogl-derive-theme = { path = "../../../lib/rust/ensogl/app/theme/derive" }
ensogl-gui-component = { path = "../../../lib/rust/ensogl/component/gui" }
ensogl-text = { path = "../../../lib/rust/ensogl/component/text" }
ensogl-text-msdf = { path = "../../../lib/rust/ensogl/component/text/src/font/msdf" }

View File

@ -33,8 +33,8 @@
pub mod code_editor;
pub mod component_browser;
pub mod debug_mode_popup;
pub mod open_dialog;
pub mod project;
pub mod project_list;
pub mod root;
pub mod searcher;
pub mod status_bar;

View File

@ -1,80 +0,0 @@
//! A module with [`OpenDialog`] component.
use crate::prelude::*;
use enso_frp as frp;
use ensogl::application::Application;
use ensogl::display;
use ensogl::display::shape::StyleWatchFrp;
use ensogl_component::file_browser::FileBrowser;
use ensogl_hardcoded_theme as theme;
// ==============
// === Export ===
// ==============
pub mod project_list;
// ==================
// === OpenDialog ===
// ==================
/// An Open Dialog GUI component.
///
/// This is component bounding together projects-to-open list and the file browser. It does not
/// provide frp endpoints by its own: you should just go directly to the `project_list` or
/// `file_browser` field.
#[allow(missing_docs)]
#[derive(Clone, CloneRef, Debug)]
pub struct OpenDialog {
network: frp::Network,
pub project_list: project_list::ProjectList,
pub file_browser: FileBrowser,
display_object: display::object::Instance,
style_watch: StyleWatchFrp,
}
impl OpenDialog {
/// Create Open Dialog component.
pub fn new(app: &Application) -> Self {
let network = frp::Network::new("OpenDialog");
let style_watch = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let project_list = project_list::ProjectList::new(app);
let file_browser = FileBrowser::new();
// Once FileBrowser will be implemented as component, it should be instantiated this way:
//let file_browser = app.new_view::<FileBrowser>();
let display_object = display::object::Instance::new();
display_object.add_child(&project_list);
display_object.add_child(&file_browser);
app.display.default_scene.layers.panel.add(&display_object);
use theme::application as theme_app;
let project_list_width = style_watch.get_number(theme_app::project_list::width);
let file_browser_width = style_watch.get_number(theme_app::file_browser::width);
let gap_between_panels = style_watch.get_number(theme_app::open_dialog::gap_between_panels);
frp::extend! { network
init <- source::<()>();
width <- all_with4(&project_list_width,&file_browser_width,&gap_between_panels,&init,
|pw,fw,g,()| *pw + *g + *fw
);
project_list_x <- all_with(&width,&project_list_width,|w,pw| - *w / 2.0 + *pw / 2.0);
file_browser_x <- all_with(&width,&file_browser_width, |w,fw| w / 2.0 - *fw / 2.0);
eval project_list_x ((x) project_list.set_x(*x));
eval file_browser_x ((x) file_browser.set_x(*x));
}
init.emit(());
Self { network, project_list, file_browser, display_object, style_watch }
}
}
impl display::Object for OpenDialog {
fn display_object(&self) -> &display::object::Instance {
&self.display_object
}
}

View File

@ -1,146 +0,0 @@
//! A module containing [`ProjectList`] component and all related structures.
use crate::prelude::*;
use ensogl::display::shape::*;
use enso_frp as frp;
use ensogl::application::Application;
use ensogl::display;
use ensogl_component::list_view;
use ensogl_component::shadow;
use ensogl_hardcoded_theme::application::project_list as theme;
use ensogl_text as text;
// =============
// === Entry ===
// =============
/// The entry in project list.
pub type Entry = list_view::entry::Label;
// ==============
// === Shapes ===
// ==============
mod background {
use super::*;
pub const SHADOW_PX: f32 = 10.0;
pub const CORNER_RADIUS_PX: f32 = 16.0;
ensogl::shape! {
(style:Style) {
let sprite_width : Var<Pixels> = "input_size.x".into();
let sprite_height : Var<Pixels> = "input_size.y".into();
let width = sprite_width - SHADOW_PX.px() * 2.0;
let height = sprite_height - SHADOW_PX.px() * 2.0;
let color = style.get_color(theme::background);
let border_size = style.get_number(theme::bar::border_size);
let bar_height = style.get_number(theme::bar::height);
let rect = Rect((&width,&height)).corners_radius(CORNER_RADIUS_PX.px());
let shape = rect.fill(color);
let shadow = shadow::from_shape(rect.into(),style);
let toolbar_border = Rect((width, border_size.px()))
.translate_y(height / 2.0 - bar_height.px())
.fill(style.get_color(theme::bar::border_color));
(shadow + shape + toolbar_border).into()
}
}
}
// ===================
// === ProjectList ===
// ===================
/// The Project List GUI Component.
///
/// This is a list of projects in a nice frame with title.
#[derive(Clone, CloneRef, Debug)]
pub struct ProjectList {
network: frp::Network,
display_object: display::object::Instance,
background: background::View, //TODO[ao] use Card instead.
caption: text::Text,
list: list_view::ListView<Entry>,
style_watch: StyleWatchFrp,
}
impl Deref for ProjectList {
type Target = list_view::Frp<Entry>;
fn deref(&self) -> &Self::Target {
&self.list.frp
}
}
impl ProjectList {
/// Create Project List Component.
pub fn new(app: &Application) -> Self {
let network = frp::Network::new("ProjectList");
let display_object = display::object::Instance::new();
let background = background::View::new();
let caption = app.new_view::<text::Text>();
let list = app.new_view::<list_view::ListView<Entry>>();
display_object.add_child(&background);
display_object.add_child(&caption);
display_object.add_child(&list);
app.display.default_scene.layers.panel.add(&display_object);
caption.set_content("Open Project");
caption.add_to_scene_layer(&app.display.default_scene.layers.panel_text);
list.set_label_layer(&app.display.default_scene.layers.panel_text);
ensogl::shapes_order_dependencies! {
app.display.default_scene => {
background -> list_view::selection;
list_view::background -> background;
}
}
let style_watch = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let width = style_watch.get_number(theme::width);
let height = style_watch.get_number(theme::height);
let bar_height = style_watch.get_number(theme::bar::height);
let padding = style_watch.get_number(theme::padding);
let color = style_watch.get_color(theme::bar::label::color);
let label_size = style_watch.get_number(theme::bar::label::size);
frp::extend! { network
init <- source::<()>();
size <- all_with3(&width,&height,&init,|w,h,()|
Vector2(w + background::SHADOW_PX * 2.0,h + background::SHADOW_PX * 2.0)
);
list_size <- all_with4(&width,&height,&bar_height,&init,|w,h,bh,()|
Vector2(*w,*h - *bh));
list_y <- all_with(&bar_height,&init, |bh,()| -*bh / 2.0);
caption_xy <- all_with4(&width,&height,&padding,&init,
|w,h,p,()| Vector2(-*w / 2.0 + *p, *h / 2.0 - p)
);
color <- all(&color,&init)._0();
label_size <- all(&label_size,&init)._0();
eval size ((size) background.set_size(*size););
eval list_size ((size) list.resize(*size));
eval list_y ((y) list.set_y(*y));
eval caption_xy ((xy) caption.set_xy(*xy));
eval color ((color) caption.set_property_default(color));
eval label_size ((size) caption.set_property_default(text::Size(*size)));
};
init.emit(());
Self { network, display_object, background, caption, list, style_watch }
}
}
impl display::Object for ProjectList {
fn display_object(&self) -> &display::object::Instance {
&self.display_object
}
}

View File

@ -15,7 +15,7 @@ use crate::graph_editor::component::node::Expression;
use crate::graph_editor::component::visualization;
use crate::graph_editor::GraphEditor;
use crate::graph_editor::NodeId;
use crate::open_dialog::OpenDialog;
use crate::project_list::ProjectList;
use crate::searcher;
use enso_config::ARGS;
@ -69,12 +69,16 @@ impl SearcherParams {
ensogl::define_endpoints! {
Input {
/// Open the Open File or Project Dialog.
show_open_dialog(),
/// Open the Open Project Dialog.
show_project_list(),
/// Close the Open Project Dialog without further action
hide_project_list(),
/// Close the searcher without taking any actions
close_searcher(),
/// Close the Open File or Project Dialog without further action
close_open_dialog(),
/// Show the graph editor.
show_graph_editor(),
/// Hide the graph editor.
hide_graph_editor(),
/// Simulates a style toggle press event.
toggle_style(),
/// Saves a snapshot of the current state of the project to the VCS.
@ -110,7 +114,7 @@ ensogl::define_endpoints! {
editing_aborted (NodeId),
editing_committed_old_searcher (NodeId, Option<searcher::entry::Id>),
editing_committed (NodeId, Option<component_list_panel::grid::GroupEntryId>),
open_dialog_shown (bool),
project_list_shown (bool),
code_editor_shown (bool),
style (Theme),
fullscreen_visualization_shown (bool),
@ -248,7 +252,7 @@ struct Model {
searcher: SearcherVariant,
code_editor: code_editor::View,
fullscreen_vis: Rc<RefCell<Option<visualization::fullscreen::Panel>>>,
open_dialog: Rc<OpenDialog>,
project_list: Rc<ProjectList>,
debug_mode_popup: debug_mode_popup::View,
}
@ -269,7 +273,7 @@ impl Model {
window_control_buttons
});
let window_control_buttons = Immutable(window_control_buttons);
let open_dialog = Rc::new(OpenDialog::new(app));
let project_list = Rc::new(ProjectList::new(app));
display_object.add_child(&graph_editor);
display_object.add_child(&code_editor);
@ -287,7 +291,7 @@ impl Model {
searcher,
code_editor,
fullscreen_vis,
open_dialog,
project_list,
debug_mode_popup,
}
}
@ -377,12 +381,20 @@ impl Model {
js::fullscreen();
}
fn show_open_dialog(&self) {
self.display_object.add_child(&*self.open_dialog);
fn show_project_list(&self) {
self.display_object.add_child(&*self.project_list);
}
fn hide_open_dialog(&self) {
self.display_object.remove_child(&*self.open_dialog);
fn hide_project_list(&self) {
self.display_object.remove_child(&*self.project_list);
}
fn show_graph_editor(&self) {
self.display_object.add_child(&*self.graph_editor);
}
fn hide_graph_editor(&self) {
self.display_object.remove_child(&*self.graph_editor);
}
}
@ -458,8 +470,7 @@ impl View {
let network = &frp.network;
let searcher = &model.searcher.frp(network);
let graph = &model.graph_editor.frp;
let project_list = &model.open_dialog.project_list;
let file_browser = &model.open_dialog.file_browser;
let project_list = &model.project_list;
let searcher_anchor = DEPRECATED_Animation::<Vector2<f32>>::new(network);
// FIXME[WD]: Think how to refactor it, as it needs to be done before model, as we do not
@ -484,6 +495,9 @@ impl View {
frp::extend! { network
eval shape ((shape) model.on_dom_shape_changed(shape));
eval_ frp.show_graph_editor(model.show_graph_editor());
eval_ frp.hide_graph_editor(model.hide_graph_editor());
// === Searcher Position and Size ===
let main_cam = app.display.default_scene.layers.main.camera();
@ -625,17 +639,15 @@ impl View {
);
// === Opening Open File or Project Dialog ===
// === Project Dialog ===
eval_ frp.show_open_dialog (model.show_open_dialog());
project_chosen <- project_list.chosen_entry.constant(());
file_chosen <- file_browser.entry_chosen.constant(());
eval_ frp.show_project_list (model.show_project_list());
project_chosen <- project_list.grid.entry_selected.constant(());
mouse_down <- scene.mouse.frp.down.constant(());
clicked_on_bg <- mouse_down.filter(f_!(scene.mouse.target.get().is_background()));
should_be_closed <- any(frp.close_open_dialog,project_chosen,file_chosen,clicked_on_bg);
eval_ should_be_closed (model.hide_open_dialog());
frp.source.open_dialog_shown <+ bool(&should_be_closed,&frp.show_open_dialog);
should_be_closed <- any(frp.hide_project_list,project_chosen,clicked_on_bg);
eval_ should_be_closed (model.hide_project_list());
frp.source.project_list_shown <+ bool(&should_be_closed,&frp.show_project_list);
// === Style toggle ===
@ -675,13 +687,13 @@ impl View {
let documentation = model.searcher.documentation();
searcher_active <- searcher.is_hovered || documentation.frp.is_selected;
disable_navigation <- searcher_active || frp.open_dialog_shown;
disable_navigation <- searcher_active || frp.project_list_shown;
graph.set_navigator_disabled <+ disable_navigation;
// === Disabling Dropping ===
frp.source.drop_files_enabled <+ init.constant(true);
frp.source.drop_files_enabled <+ frp.open_dialog_shown.map(|v| !v);
frp.source.drop_files_enabled <+ frp.project_list_shown.map(|v| !v);
// === Debug Mode ===
@ -712,9 +724,9 @@ impl View {
&self.model.code_editor
}
/// Open File or Project Dialog
pub fn open_dialog(&self) -> &OpenDialog {
&self.model.open_dialog
/// Open Project Dialog
pub fn project_list(&self) -> &ProjectList {
&self.model.project_list
}
/// Debug Mode Popup
@ -751,9 +763,9 @@ impl application::View for View {
fn default_shortcuts() -> Vec<application::shortcut::Shortcut> {
use shortcut::ActionType::*;
[
(Press, "!is_searcher_opened", "cmd o", "show_open_dialog"),
(Press, "!is_searcher_opened", "cmd o", "show_project_list"),
(Press, "is_searcher_opened", "escape", "close_searcher"),
(Press, "open_dialog_shown", "escape", "close_open_dialog"),
(Press, "project_list_shown", "escape", "hide_project_list"),
(Press, "", "cmd alt shift t", "toggle_style"),
(Press, "", "cmd s", "save_project_snapshot"),
(Press, "", "cmd r", "restore_project_snapshot"),

View File

@ -0,0 +1,288 @@
//! An interactive list of projects. It used to quickly switch between projects from inside the
//! Project View.
use crate::prelude::*;
use ensogl::display::shape::*;
use enso_frp as frp;
use ensogl::application::frp::API;
use ensogl::application::Application;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::scene::Layer;
use ensogl_component::grid_view;
use ensogl_component::list_view;
use ensogl_component::shadow;
use ensogl_derive_theme::FromTheme;
use ensogl_hardcoded_theme::application::project_list as theme;
use ensogl_text as text;
// ==============
// === Styles ===
// ==============
// === Style ===
#[derive(Debug, Clone, Copy, Default, FromTheme)]
#[base_path = "theme"]
#[allow(missing_docs)]
pub struct Style {
width: f32,
height: f32,
shadow_extent: f32,
corners_radius: f32,
paddings: f32,
#[theme_path = "theme::entry::height"]
entry_height: f32,
#[theme_path = "theme::bar::height"]
bar_height: f32,
#[theme_path = "theme::bar::label::padding"]
label_padding: f32,
#[theme_path = "theme::bar::label::size"]
bar_label_size: f32,
#[theme_path = "theme::bar::label::color"]
bar_label_color: color::Rgba,
}
// === Entry Style ===
#[derive(Debug, Clone, Copy, Default, FromTheme)]
#[base_path = "theme::entry"]
#[allow(missing_docs)]
pub struct EntryStyle {
corners_radius: f32,
selection_color: color::Rgba,
hover_color: color::Rgba,
#[theme_path = "theme::entry::text::padding_left"]
text_padding_left: f32,
#[theme_path = "theme::entry::text::padding_bottom"]
text_padding_bottom: f32,
#[theme_path = "theme::entry::text::size"]
text_size: f32,
#[theme_path = "theme::entry::text::color"]
text_color: color::Rgba,
}
// =============
// === Entry ===
// =============
// === Data ===
/// The model of the list entry. Displays the name of the project.
#[derive(Debug, Clone, CloneRef)]
struct Data {
display_object: display::object::Instance,
text: text::Text,
}
impl Data {
fn new(app: &Application, text_layer: Option<&Layer>) -> Self {
let display_object = display::object::Instance::new();
let text = text::Text::new(app);
display_object.add_child(&text);
if let Some(text_layer) = text_layer {
text.add_to_scene_layer(text_layer);
}
Self { display_object, text }
}
fn set_text(&self, text: ImString) {
self.text.set_content(text);
}
}
// === Entry ===
/// The list entry. Displays the name of the project.
#[derive(Debug, Clone, CloneRef)]
pub struct Entry {
data: Data,
frp: grid_view::entry::EntryFrp<Self>,
}
impl display::Object for Entry {
fn display_object(&self) -> &display::object::Instance {
&self.data.display_object
}
}
impl grid_view::Entry for Entry {
type Model = ImString;
type Params = ();
fn new(app: &Application, text_layer: Option<&Layer>) -> Self {
let frp = grid_view::entry::EntryFrp::<Self>::new();
let data = Data::new(app, text_layer);
let network = frp.network();
let input = &frp.private().input;
let out = &frp.private().output;
let style_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let style = EntryStyle::from_theme(network, &style_frp);
frp::extend! { network
corners_radius <- style.update.map(|s| s.corners_radius);
hover_color <- style.update.map(|s| s.hover_color.into());
selection_color <- style.update.map(|s| s.selection_color.into());
text_padding_left <- style.update.map(|s| s.text_padding_left);
text_padding_bottom <- style.update.map(|s| s.text_padding_bottom);
text_size <- style.update.map(|s| s.text_size);
text_color <- style.update.map(|s| color::Lcha::from(s.text_color));
eval input.set_model((text) data.set_text(text.clone_ref()));
contour <- input.set_size.map(|s| grid_view::entry::Contour::rectangular(*s));
out.contour <+ contour;
out.highlight_contour <+ contour.map2(&corners_radius, |c,r| c.with_corners_radius(*r));
out.hover_highlight_color <+ hover_color;
out.selection_highlight_color <+ selection_color;
width <- input.set_size.map(|s| s.x);
_eval <- all_with(&width, &text_padding_left, f!((w, p) data.text.set_x(-w / 2.0 + p)));
eval text_padding_bottom((p) data.text.set_y(*p));
data.text.set_property_default <+ text_size.map(|s| text::Size(*s)).cloned_into_some();
data.text.set_property_default <+ text_color.cloned_into_some();
}
style.init.emit(());
Self { data, frp }
}
fn frp(&self) -> &grid_view::entry::EntryFrp<Self> {
&self.frp
}
}
// ==================
// === Background ===
// ==================
mod background {
use super::*;
ensogl::shape! {
(style:Style) {
let sprite_width: Var<Pixels> = "input_size.x".into();
let sprite_height: Var<Pixels> = "input_size.y".into();
let shadow_extent = style.get_number(theme::shadow_extent);
let width = sprite_width - shadow_extent.px() * 2.0;
let height = sprite_height - shadow_extent.px() * 2.0;
let color = style.get_color(theme::background);
let border_size = style.get_number(theme::bar::border_size);
let bar_height = style.get_number(theme::bar::height);
let corners_radius = style.get_number(theme::corners_radius);
let rect = Rect((&width,&height)).corners_radius(corners_radius.px());
let shape = rect.fill(color);
let shadow = shadow::from_shape(rect.into(),style);
let toolbar_border = Rect((width, border_size.px()))
.translate_y(height / 2.0 - bar_height.px())
.fill(style.get_color(theme::bar::border_color));
(shadow + shape + toolbar_border).into()
}
}
}
// ===================
// === ProjectList ===
// ===================
/// The Project List GUI Component.
///
/// This is a list of projects in a nice frame with a title.
#[derive(Clone, CloneRef, Debug)]
pub struct ProjectList {
network: frp::Network,
display_object: display::object::Instance,
background: background::View,
caption: text::Text,
#[allow(missing_docs)]
pub grid: grid_view::scrollable::SelectableGridView<Entry>,
}
impl ProjectList {
/// Create Project List Component.
pub fn new(app: &Application) -> Self {
let network = frp::Network::new("ProjectList");
let display_object = display::object::Instance::new();
let background = background::View::new();
let caption = app.new_view::<text::Text>();
let grid = grid_view::scrollable::SelectableGridView::new(app);
display_object.add_child(&background);
display_object.add_child(&caption);
display_object.add_child(&grid);
app.display.default_scene.layers.panel.add(&display_object);
caption.set_content("Open Project");
caption.add_to_scene_layer(&app.display.default_scene.layers.panel_text);
ensogl::shapes_order_dependencies! {
app.display.default_scene => {
background -> list_view::selection;
list_view::background -> background;
}
}
let style_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let style = Style::from_theme(&network, &style_frp);
frp::extend! { network
init <- source::<()>();
width <- style.update.map(|s| s.width);
height <- style.update.map(|s| s.height);
shadow_extent <- style.update.map(|s| s.shadow_extent);
bar_height <- style.update.map(|s| s.bar_height);
label_padding <- style.update.map(|s| s.label_padding);
label_color <- style.update.map(|s| s.bar_label_color);
label_size <- style.update.map(|s| s.bar_label_size);
corners_radius <- style.update.map(|s| s.corners_radius);
entry_height <- style.update.map(|s| s.entry_height);
paddings <- style.update.map(|s| s.paddings);
content_size <- all_with3(&width, &height, &init, |w,h,()| Vector2(*w,*h));
size <- all_with3(&width, &height, &shadow_extent, |w, h, s|
Vector2(w + s * 2.0, h + s * 2.0)
);
grid_size <- all_with3(&content_size, &bar_height, &paddings,
|s, h, p| s - Vector2(0.0, *h) - Vector2(*p * 2.0, *p * 2.0)
);
grid_width <- grid_size.map(|s| s.x);
caption_xy <- all_with3(&width,&height,&label_padding,
|w,h,p| Vector2(-*w / 2.0 + *p, *h / 2.0 - p)
);
eval caption_xy ((xy) caption.set_xy(*xy));
eval label_color((color) caption.set_property_default(color));
eval label_size((size) caption.set_property_default(text::Size(*size)));
_eval <- all_with(&grid_width, &entry_height,
f!((w, h) grid.set_entries_size(Vector2(*w, *h)))
);
eval size((size) background.set_size(*size););
eval grid_size((size) grid.scroll_frp().resize(*size));
eval corners_radius((r) grid.scroll_frp().set_corner_radius_bottom_left(*r));
eval corners_radius((r) grid.scroll_frp().set_corner_radius_bottom_right(*r));
grid_x <- grid_width.map(|width| -width / 2.0);
grid_y <- all_with3(&content_size, &bar_height, &paddings, |s,h,p| s.y / 2.0 - *h - *p);
_eval <- all_with(&grid_x, &grid_y, f!((x, y) grid.set_xy(Vector2(*x, *y))));
}
style.init.emit(());
init.emit(());
Self { network, display_object, background, caption, grid }
}
}
impl display::Object for ProjectList {
fn display_object(&self) -> &display::object::Instance {
&self.display_object
}
}

View File

@ -368,30 +368,32 @@ define_themes! { [light:0, dark:1]
}
}
}
file_browser {
width = 0.0, 0.0; // Should be updated when file browser will be implemented.
height = 421.0, 421.0;
}
open_dialog {
// Should be updated when file browser will be implemented.
gap_between_panels = 0.0, 0.0;
}
project_list {
width = 202.0 , 202.0;
padding = 16.0, 16.0;
height = 421.0, 421.0;
height = 428.0, 428.0;
background = Rgba(0.992,0.996,1.0,1.0), Rgba(0.182,0.188,0.196,1.0);
text = widget::list_view::text, widget::list_view::text;
text {
size = 12.0, 12.0;
padding = 6.0 , 6.0 ;
}
shadow_extent = 10.0, 10.0;
corners_radius = 16.0, 16.0;
paddings = 4.0, 4.0;
bar {
height = 45.0, 45.0;
height = 45.0, 45.0;
border_size = 1.0, 1.0;
border_color = Rgba(0.808,0.808,0.808,1.0) , Rgba(0.808,0.808,0.808,1.0);
label {
size = 12.0, 12.0;
padding = 16.0, 16.0;
size = 12.0, 12.0;
color = Rgba(0.439,0.439,0.439,1.0), Rgba(0.439,0.439,0.439,1.0);
}
}
entry {
height = 25.0, 25.0;
corners_radius = application::project_list::corners_radius, application::project_list::corners_radius;
selection_color = Rgba::transparent(), Rgba::transparent();
hover_color = Rgba(0.906,0.914,0.922,1.0), Rgba(0.906,0.914,0.922,1.0);
text {
padding_left = 10.0, 10.0;
padding_bottom = 7.0, 7.0;
size = 12.0, 12.0;
color = Rgba(0.439,0.439,0.439,1.0), Rgba(0.439,0.439,0.439,1.0);
}
}

View File

@ -39,6 +39,11 @@ impl Contour {
pub fn rectangular(size: Vector2) -> Self {
Self { size, corners_radius: 0.0 }
}
/// Adjust the corners radius of the contour.
pub fn with_corners_radius(self, radius: f32) -> Self {
Self { corners_radius: radius, ..self }
}
}

View File

@ -35,6 +35,18 @@ pub mod js_bindings {
#[wasm_bindgen(method)]
#[wasm_bindgen(js_name = registerSetShadersRustFn)]
pub fn register_set_shaders_rust_fn(this: &App, closure: &Closure<dyn FnMut(JsValue)>);
/// Show a spinner covering the whole viewport.
#[allow(unsafe_code)]
#[wasm_bindgen(method)]
#[wasm_bindgen(js_name = showProgressIndicator)]
pub fn show_progress_indicator(this: &App, progress: f32);
/// Hide a spinner.
#[allow(unsafe_code)]
#[wasm_bindgen(method)]
#[wasm_bindgen(js_name = hideProgressIndicator)]
pub fn hide_progress_indicator(this: &App);
}
}
@ -50,6 +62,9 @@ pub mod js_bindings {
impl App {
pub fn register_get_shaders_rust_fn(&self, _closure: &Closure<dyn FnMut() -> JsValue>) {}
pub fn register_set_shaders_rust_fn(&self, _closure: &Closure<dyn FnMut(JsValue)>) {}
pub fn show_progress_indicator(&self, _progress: f32) {}
pub fn hide_progress_indicator(&self) {}
}
}

View File

@ -201,6 +201,7 @@ export class App {
wasmFunctions: string[] = []
beforeMainEntryPoints = new Map<string, wasm.BeforeMainEntryPoint>()
mainEntryPoints = new Map<string, wasm.EntryPoint>()
progressIndicator: wasm.ProgressIndicator | null = null
initialized = false
constructor(opts?: {
@ -402,6 +403,25 @@ export class App {
}
}
/** Show a spinner. The displayed progress is constant. */
showProgressIndicator(progress: number) {
if (this.progressIndicator) {
this.hideProgressIndicator()
}
this.progressIndicator = new wasm.ProgressIndicator(this.config)
this.progressIndicator.set(progress)
}
/** Hide the progress indicator. */
hideProgressIndicator() {
if (this.progressIndicator) {
// Setting the progress to 100% is necessary to allow animation to finish.
this.progressIndicator.set(1)
this.progressIndicator.destroy()
this.progressIndicator = null
}
}
/** Run both before main entry points and main entry point. */
async runEntryPoints() {
const entryPointName = this.config.groups.startup.options.entry.value

View File

@ -17,7 +17,7 @@ const ghostColor = '#00000020'
const topLayerIndex = '1000'
/** Visual representation of the loader. */
class ProgressIndicator {
export class ProgressIndicator {
dom: HTMLDivElement
track: HTMLElement
indicator: HTMLElement