mirror of
https://github.com/enso-org/enso.git
synced 2024-12-28 10:22:43 +03:00
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:
parent
172f72941b
commit
19beb01cf3
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -4166,6 +4166,7 @@ dependencies = [
|
||||
"enso-shapely",
|
||||
"ensogl",
|
||||
"ensogl-component",
|
||||
"ensogl-derive-theme",
|
||||
"ensogl-gui-component",
|
||||
"ensogl-hardcoded-theme",
|
||||
"ensogl-text",
|
||||
|
@ -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> {
|
||||
|
@ -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()
|
||||
|
@ -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(())
|
||||
});
|
||||
|
@ -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)
|
||||
|
@ -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" }
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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"),
|
||||
|
288
app/gui/view/src/project_list.rs
Normal file
288
app/gui/view/src/project_list.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user