mirror of
https://github.com/enso-org/enso.git
synced 2024-08-17 00:40:34 +03:00
Introducing new notifications to the GUI (#7458)
This PR replaces statusbar and popup components with the new notification API. I have moved debug mode status notification to the bottom of the window, so it is separate from other notifications. ![obraz](https://github.com/enso-org/enso/assets/1548407/0e28bdd5-3567-4d8c-b580-d53099a00715) ![obraz](https://github.com/enso-org/enso/assets/1548407/47468d56-c4fc-4a0a-876d-2b059509a29c) ![obraz](https://github.com/enso-org/enso/assets/1548407/359dd764-305a-4a75-b9a4-75674bcc9990)
This commit is contained in:
parent
6534d0a925
commit
90e3f9ccef
@ -27,7 +27,7 @@ pub use engine_protocol::project_manager::ProjectName;
|
||||
// ============================
|
||||
|
||||
/// The handle used to pair the ProcessStarted and ProcessFinished notifications.
|
||||
pub type BackgroundTaskHandle = usize;
|
||||
pub type BackgroundTaskHandle = Uuid;
|
||||
|
||||
/// A notification which should be displayed to the User on the status bar.
|
||||
#[allow(missing_docs)]
|
||||
@ -44,8 +44,7 @@ pub enum StatusNotification {
|
||||
/// A publisher for status notification events.
|
||||
#[derive(Clone, CloneRef, Debug, Default)]
|
||||
pub struct StatusNotificationPublisher {
|
||||
publisher: notification::Publisher<StatusNotification>,
|
||||
next_process_handle: Rc<Cell<usize>>,
|
||||
publisher: notification::Publisher<StatusNotification>,
|
||||
}
|
||||
|
||||
impl StatusNotificationPublisher {
|
||||
@ -68,8 +67,7 @@ impl StatusNotificationPublisher {
|
||||
#[profile(Debug)]
|
||||
pub fn publish_background_task(&self, label: impl Into<String>) -> BackgroundTaskHandle {
|
||||
let label = label.into();
|
||||
let handle = self.next_process_handle.get();
|
||||
self.next_process_handle.set(handle + 1);
|
||||
let handle = Uuid::new_v4();
|
||||
let notification = StatusNotification::BackgroundTaskStarted { label, handle };
|
||||
executor::global::spawn(self.publisher.publish(notification));
|
||||
handle
|
||||
|
@ -24,6 +24,8 @@ pub use initializer::Initializer;
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/// Constant notification ID, so we can reuse the same notification.
|
||||
pub const BACKEND_DISCONNECTED_NOTIFICATION_ID: &str = "backend-disconnected-toast";
|
||||
/// Text that shows up in the statusbar when any of the backend connections is lost.
|
||||
pub const BACKEND_DISCONNECTED_MESSAGE: &str =
|
||||
"Connection to the backend has been lost. Please try restarting IDE.";
|
||||
|
@ -88,7 +88,6 @@ impl Initializer {
|
||||
if enso_config::ARGS.groups.profile.options.emit_user_timing_measurements.value {
|
||||
ensogl_app.display.connect_profiler_to_user_timing();
|
||||
}
|
||||
let status_bar = view.status_bar().clone_ref();
|
||||
ensogl_app.display.add_child(&view);
|
||||
// TODO [mwu] Once IDE gets some well-defined mechanism of reporting
|
||||
// issues to user, such information should be properly passed
|
||||
@ -102,7 +101,7 @@ impl Initializer {
|
||||
}
|
||||
Err(error) => {
|
||||
let message = format!("Failed to initialize application: {error}");
|
||||
status_bar.add_event(ide_view::status_bar::event::Label::new(message));
|
||||
ide_view::notification::logged::error(message, &None);
|
||||
Err(FailedIde { view })
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ use crate::presenter;
|
||||
|
||||
use enso_frp as frp;
|
||||
use ide_view as view;
|
||||
use ide_view::graph_editor::SharedHashMap;
|
||||
|
||||
|
||||
// ==============
|
||||
@ -55,19 +54,14 @@ impl Model {
|
||||
// We know the name of new project before it loads. We set it right now to avoid
|
||||
// displaying a placeholder on the scene during loading.
|
||||
let project_view = self.view.project();
|
||||
let status_bar = self.view.status_bar().clone_ref();
|
||||
let project_name = project_view.top_bar().project_name();
|
||||
project_name.set_name(project_model.name().to_string());
|
||||
|
||||
let status_notifications = self.controller.status_notifications().clone_ref();
|
||||
let ide_controller = self.controller.clone_ref();
|
||||
let project_controller = controller::Project::new(project_model, status_notifications);
|
||||
let project_presenter = presenter::Project::initialize(
|
||||
ide_controller,
|
||||
project_controller,
|
||||
project_view,
|
||||
status_bar,
|
||||
);
|
||||
let project_presenter =
|
||||
presenter::Project::initialize(ide_controller, project_controller, project_view);
|
||||
crate::executor::global::spawn(async move {
|
||||
match project_presenter.await {
|
||||
Ok(project) => {
|
||||
@ -172,44 +166,41 @@ impl Presenter {
|
||||
|
||||
#[profile(Detail)]
|
||||
fn init(self) -> Self {
|
||||
self.setup_status_bar_notification_handler();
|
||||
self.setup_controller_notification_handler();
|
||||
self.setup_user_facing_notification_handler();
|
||||
self.setup_controller_internal_notification_handler();
|
||||
self.model.clone_ref().setup_and_display_new_project();
|
||||
executor::global::spawn(self.clone_ref().set_projects_list_on_welcome_screen());
|
||||
self
|
||||
}
|
||||
|
||||
fn setup_status_bar_notification_handler(&self) {
|
||||
use controller::ide::BackgroundTaskHandle as ControllerHandle;
|
||||
use ide_view::status_bar::process::Id as ViewHandle;
|
||||
fn setup_user_facing_notification_handler(&self) {
|
||||
use view::notification::logged as notification;
|
||||
|
||||
let process_map = SharedHashMap::<ControllerHandle, ViewHandle>::new();
|
||||
let status_bar = self.model.view.status_bar().clone_ref();
|
||||
let status_notifications = self.model.controller.status_notifications().subscribe();
|
||||
let weak = Rc::downgrade(&self.model);
|
||||
spawn_stream_handler(weak, status_notifications, move |notification, _| {
|
||||
match notification {
|
||||
StatusNotification::Event { label } => {
|
||||
status_bar.add_event(ide_view::status_bar::event::Label::new(label));
|
||||
notification::info(label, &None);
|
||||
}
|
||||
StatusNotification::BackgroundTaskStarted { label, handle } => {
|
||||
status_bar.add_process(ide_view::status_bar::process::Label::new(label));
|
||||
let view_handle = status_bar.last_process.value();
|
||||
process_map.insert(handle, view_handle);
|
||||
let id = notification::Id::from(handle);
|
||||
let notification = notification::Notification::from(id);
|
||||
notification.update(|opts| {
|
||||
opts.set_always_present();
|
||||
opts.set_raw_text_content(label);
|
||||
});
|
||||
notification.show();
|
||||
}
|
||||
StatusNotification::BackgroundTaskFinished { handle } => {
|
||||
if let Some(view_handle) = process_map.remove(&handle) {
|
||||
status_bar.finish_process(view_handle);
|
||||
} else {
|
||||
warn!("Controllers finished process not displayed in view");
|
||||
}
|
||||
notification::Id::from(handle).dismiss();
|
||||
}
|
||||
}
|
||||
futures::future::ready(())
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_controller_notification_handler(&self) {
|
||||
fn setup_controller_internal_notification_handler(&self) {
|
||||
let stream = self.model.controller.subscribe();
|
||||
let weak = Rc::downgrade(&self.model);
|
||||
spawn_stream_handler(weak, stream, move |notification, model| {
|
||||
|
@ -8,6 +8,7 @@ use crate::presenter;
|
||||
use crate::presenter::searcher::ai::AISearcher;
|
||||
use crate::presenter::searcher::SearcherPresenter;
|
||||
use crate::presenter::ComponentBrowserSearcher;
|
||||
use crate::EXECUTION_FAILED_MESSAGE;
|
||||
|
||||
use engine_protocol::language_server::ExecutionEnvironment;
|
||||
use enso_frp as frp;
|
||||
@ -18,6 +19,7 @@ use ide_view::project::SearcherType;
|
||||
use model::module::NotificationKind;
|
||||
use model::project::Notification;
|
||||
use model::project::VcsStatus;
|
||||
use view::notification::logged as notification;
|
||||
|
||||
|
||||
|
||||
@ -44,13 +46,12 @@ struct Model {
|
||||
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<Box<dyn SearcherPresenter>>>,
|
||||
available_projects: Rc<RefCell<Vec<(ImString, Uuid)>>>,
|
||||
shortcut_transaction: RefCell<Option<Rc<model::undo_redo::Transaction>>>,
|
||||
execution_failed_process_id: Rc<Cell<Option<view::status_bar::process::Id>>>,
|
||||
execution_failed_notification: notification::Notification,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@ -60,7 +61,6 @@ impl Model {
|
||||
controller: controller::Project,
|
||||
init_result: controller::project::InitializationResult,
|
||||
view: view::project::View,
|
||||
status_bar: view::status_bar::View,
|
||||
) -> Self {
|
||||
let graph_controller = init_result.main_graph;
|
||||
let text_controller = init_result.main_module_text;
|
||||
@ -74,20 +74,27 @@ impl Model {
|
||||
let searcher = default();
|
||||
let available_projects = default();
|
||||
let shortcut_transaction = default();
|
||||
let execution_failed_process_id = default();
|
||||
let options = notification::UpdateOptions {
|
||||
render: Some(EXECUTION_FAILED_MESSAGE.into()),
|
||||
options: notification::Options {
|
||||
auto_close: Some(notification::AutoClose::Never()),
|
||||
r#type: Some(notification::Type::Error),
|
||||
..default()
|
||||
},
|
||||
};
|
||||
let execution_failed_notification = notification::Notification::new(options);
|
||||
Model {
|
||||
controller,
|
||||
module_model,
|
||||
graph_controller,
|
||||
ide_controller,
|
||||
view,
|
||||
status_bar,
|
||||
graph,
|
||||
code,
|
||||
searcher,
|
||||
available_projects,
|
||||
shortcut_transaction,
|
||||
execution_failed_process_id,
|
||||
execution_failed_notification,
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,17 +227,11 @@ impl Model {
|
||||
fn execution_complete(&self) {
|
||||
self.view.graph().frp.set_read_only(false);
|
||||
self.view.graph().frp.execution_complete.emit(());
|
||||
if let Some(id) = self.execution_failed_process_id.get() {
|
||||
self.status_bar.finish_process(id);
|
||||
}
|
||||
self.execution_failed_notification.dismiss();
|
||||
}
|
||||
|
||||
fn execution_failed(&self) {
|
||||
let message = crate::EXECUTION_FAILED_MESSAGE;
|
||||
let message = view::status_bar::process::Label::from(message);
|
||||
self.status_bar.add_process(message);
|
||||
let id = self.status_bar.last_process.value();
|
||||
self.execution_failed_process_id.set(Some(id));
|
||||
self.execution_failed_notification.show();
|
||||
}
|
||||
|
||||
fn execution_context_interrupt(&self) {
|
||||
@ -281,7 +282,6 @@ impl Model {
|
||||
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();
|
||||
@ -292,14 +292,17 @@ impl Model {
|
||||
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}."));
|
||||
let message = format!("Error opening project: {error}");
|
||||
notification::error(message, &None);
|
||||
}
|
||||
} else {
|
||||
error!("Project with id {id} not found.");
|
||||
notification::error(format!("Project with id {id} not found."), &None);
|
||||
}
|
||||
} else {
|
||||
error!("Project Manager API not available, cannot open project.");
|
||||
notification::error(
|
||||
"Project Manager API not available, cannot open project.",
|
||||
&None,
|
||||
);
|
||||
}
|
||||
app.hide_progress_indicator();
|
||||
view.show_graph_editor();
|
||||
@ -363,10 +366,9 @@ impl Project {
|
||||
controller: controller::Project,
|
||||
init_result: controller::project::InitializationResult,
|
||||
view: view::project::View,
|
||||
status_bar: view::status_bar::View,
|
||||
) -> Self {
|
||||
let network = frp::Network::new("presenter::Project");
|
||||
let model = Model::new(ide_controller, controller, init_result, view, status_bar);
|
||||
let model = Model::new(ide_controller, controller, init_result, view);
|
||||
let model = Rc::new(model);
|
||||
Self { network, model }.init()
|
||||
}
|
||||
@ -481,8 +483,12 @@ impl Project {
|
||||
match notification {
|
||||
Notification::ConnectionLost(_) => {
|
||||
let message = crate::BACKEND_DISCONNECTED_MESSAGE;
|
||||
let message = view::status_bar::event::Label::from(message);
|
||||
model.status_bar.add_event(message);
|
||||
let options = notification::Options {
|
||||
auto_close: Some(notification::AutoClose::Never()),
|
||||
toast_id: Some(crate::BACKEND_DISCONNECTED_NOTIFICATION_ID.into()),
|
||||
..Default::default()
|
||||
};
|
||||
notification::error(message, &Some(options));
|
||||
}
|
||||
Notification::VcsStatusChanged(VcsStatus::Dirty) => {
|
||||
model.set_project_changed(true);
|
||||
@ -539,13 +545,11 @@ impl Project {
|
||||
ide_controller: controller::Ide,
|
||||
controller: controller::Project,
|
||||
view: view::project::View,
|
||||
status_bar: view::status_bar::View,
|
||||
) -> FallibleResult<Self> {
|
||||
debug!("Initializing project controller...");
|
||||
let init_result = controller.initialize().await?;
|
||||
debug!("Project controller initialized.");
|
||||
let presenter =
|
||||
Self::new(ide_controller, controller.clone(), init_result, view, status_bar);
|
||||
let presenter = Self::new(ide_controller, controller.clone(), init_result, view);
|
||||
debug!("Project presenter created.");
|
||||
// Following the project initialization, the Undo/Redo stack should be empty.
|
||||
// This makes sure that any initial modifications resulting from the GUI initialization
|
||||
|
@ -19,7 +19,6 @@ use ensogl::prelude::*;
|
||||
|
||||
use enso_frp as frp;
|
||||
use ensogl::application::Application;
|
||||
use ensogl::control::io::mouse;
|
||||
use ensogl::display::object::ObjectOps;
|
||||
use ensogl::display::shape::StyleWatch;
|
||||
use ensogl::gui::text;
|
||||
@ -35,7 +34,6 @@ use ide_view::graph_editor::NodeProfilingStatus;
|
||||
use ide_view::graph_editor::Type;
|
||||
use ide_view::project;
|
||||
use ide_view::root;
|
||||
use ide_view::status_bar;
|
||||
use parser::Parser;
|
||||
use span_tree::TagValue;
|
||||
|
||||
@ -115,7 +113,6 @@ fn init(app: &Application) {
|
||||
|
||||
code_editor.text_area().set_content(STUB_MODULE.to_owned());
|
||||
|
||||
root_view.status_bar().add_event(status_bar::event::Label::new("This is a status message."));
|
||||
project_view.debug_push_breadcrumb();
|
||||
|
||||
root_view.switch_view_to_project();
|
||||
@ -250,23 +247,6 @@ fn init(app: &Application) {
|
||||
graph_editor.set_available_execution_environments(make_dummy_execution_environments());
|
||||
|
||||
|
||||
// === Pop-up ===
|
||||
|
||||
// Create node to trigger a pop-up.
|
||||
let node_id = graph_editor.model.add_node();
|
||||
graph_editor.frp.set_node_position.emit((node_id, Vector2(-300.0, -100.0)));
|
||||
let expression = expression_mock_string("Click me to show a pop-up");
|
||||
graph_editor.frp.set_node_expression.emit((node_id, expression));
|
||||
graph_editor.model.with_node(node_id, |node| {
|
||||
let popup = project_view.popup();
|
||||
let network = node.network();
|
||||
let node_clicked = node.on_event::<mouse::Down>();
|
||||
frp::extend! { network
|
||||
eval_ node_clicked (popup.set_label.emit("This is a test pop-up."));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// === Rendering ===
|
||||
|
||||
// let tgt_type = dummy_type_generator.get_dummy_type();
|
||||
|
@ -1,83 +0,0 @@
|
||||
//! A pop-up that signals about enabling/disabling Debug Mode of Graph Editor.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::popup;
|
||||
|
||||
use ensogl::application::Application;
|
||||
use ensogl::display;
|
||||
use frp::stream::EventOutput;
|
||||
use frp::HasLabel;
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/// Mitigate limitations of constant strings concatenation.
|
||||
macro_rules! define_debug_mode_shortcut {
|
||||
($shortcut:literal) => {
|
||||
/// A keyboard shortcut used to enable/disable Debug Mode.
|
||||
pub const DEBUG_MODE_SHORTCUT: &str = $shortcut;
|
||||
const DEBUG_MODE_ENABLED: &str =
|
||||
concat!("Debug Mode enabled. To disable, press `", $shortcut, "`.");
|
||||
};
|
||||
}
|
||||
define_debug_mode_shortcut!("ctrl shift d");
|
||||
const DEBUG_MODE_DISABLED: &str = "Debug Mode disabled.";
|
||||
const LABEL_VISIBILITY_DELAY_MS: f32 = 3_000.0;
|
||||
|
||||
|
||||
|
||||
// ===========
|
||||
// === FRP ===
|
||||
// ===========
|
||||
|
||||
ensogl::define_endpoints! {
|
||||
Input {
|
||||
// Debug Mode was enabled.
|
||||
enabled(),
|
||||
// Debug Mode was disabled.
|
||||
disabled(),
|
||||
}
|
||||
Output {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============
|
||||
// === View ===
|
||||
// ============
|
||||
|
||||
/// A pop-up that signals about enabling/disabling Debug Mode of Graph Editor.
|
||||
#[derive(Debug, Clone, CloneRef, Deref, display::Object)]
|
||||
pub struct View {
|
||||
#[deref]
|
||||
frp: Frp,
|
||||
#[display_object]
|
||||
popup: popup::View,
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// Constructor.
|
||||
pub fn new(app: &Application) -> Self {
|
||||
let frp = Frp::new();
|
||||
let network = &frp.network;
|
||||
let popup = popup::View::new(app);
|
||||
|
||||
popup.set_delay(LABEL_VISIBILITY_DELAY_MS);
|
||||
|
||||
frp::extend! { network
|
||||
eval_ frp.enabled (popup.set_label(DEBUG_MODE_ENABLED.to_string()));
|
||||
eval_ frp.disabled (popup.set_label(DEBUG_MODE_DISABLED.to_string()));
|
||||
}
|
||||
|
||||
Self { frp, popup }
|
||||
}
|
||||
|
||||
/// Get the FRP node for the content of the pop-up, for testing purposes.
|
||||
pub fn content_frp_node(&self) -> impl EventOutput<Output = String> + HasLabel {
|
||||
self.popup.content_frp_node()
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(drain_filter)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(let_chains)]
|
||||
#![feature(once_cell)]
|
||||
#![feature(option_result_contains)]
|
||||
#![feature(specialization)]
|
||||
#![feature(trait_alias)]
|
||||
@ -31,14 +33,11 @@
|
||||
|
||||
#[allow(clippy::option_map_unit_fn)]
|
||||
pub mod code_editor;
|
||||
pub mod debug_mode_popup;
|
||||
pub mod notification;
|
||||
pub mod popup;
|
||||
pub mod project;
|
||||
pub mod project_list;
|
||||
pub mod root;
|
||||
pub mod searcher;
|
||||
pub mod status_bar;
|
||||
|
||||
pub use ide_view_component_browser as component_browser;
|
||||
pub use ide_view_documentation as documentation;
|
||||
|
@ -1,391 +1,77 @@
|
||||
//! This is Rust wrapper for the [`react-toastify`](https://fkhadra.github.io/react-toastify) library.
|
||||
//! A pop-up notification that can be controlled by FRP.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use ide_view::notification;
|
||||
//! # fn main() -> Result<(), wasm_bindgen::JsValue> {
|
||||
//! use ide_view::notification::UpdateOptions;
|
||||
//! let handle = notification::info(
|
||||
//! "Undo triggered in UI.",
|
||||
//! &Some(notification::Options {
|
||||
//! theme: Some(notification::Theme::Dark),
|
||||
//! auto_close: Some(notification::AutoClose::Never()),
|
||||
//! draggable: Some(false),
|
||||
//! close_on_click: Some(false),
|
||||
//! ..Default::default()
|
||||
//! }),
|
||||
//! )?;
|
||||
//! handle.update(&UpdateOptions::default().render_string("Undo done."))?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//! Non-FRP, foundational API is available in the [api](crate::notification::api) module.
|
||||
|
||||
use crate::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use crate::notification::logged::Notification;
|
||||
use crate::notification::logged::UpdateOptions;
|
||||
|
||||
use ensogl::application::Application;
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub mod api;
|
||||
pub mod js;
|
||||
|
||||
pub use js::Id;
|
||||
pub mod logged;
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
// === Primary API ===
|
||||
// ===================
|
||||
// ===========
|
||||
// === FRP ===
|
||||
// ===========
|
||||
|
||||
/// Send any kind of notification.
|
||||
pub fn send_any(message: &str, r#type: Type, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||
let options = match options {
|
||||
Some(options) => options.try_into()?,
|
||||
None => JsValue::UNDEFINED,
|
||||
};
|
||||
js::toast(message, r#type, &options)
|
||||
}
|
||||
|
||||
/// Send an info notification.
|
||||
pub fn info(message: &str, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||
send_any(message, Type::Info, options)
|
||||
}
|
||||
|
||||
/// Send a warning notification.
|
||||
pub fn warning(message: &str, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||
send_any(message, Type::Warning, options)
|
||||
}
|
||||
|
||||
/// Send a error notification.
|
||||
pub fn error(message: &str, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||
send_any(message, Type::Error, options)
|
||||
}
|
||||
|
||||
/// Send a success notification.
|
||||
pub fn success(message: &str, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||
send_any(message, Type::Success, options)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============================
|
||||
// === JS-conversion helpers ===
|
||||
// =============================
|
||||
|
||||
|
||||
// === Rust->JS error ===
|
||||
|
||||
/// Convert arbitrary Rust error value into JsValue-based error.
|
||||
pub fn to_js_error(error: impl std::error::Error) -> JsValue {
|
||||
js_sys::Error::new(&error.to_string()).into()
|
||||
}
|
||||
|
||||
|
||||
// === Rust->JS conversion ===
|
||||
|
||||
/// Macro that implements TryFrom for given `type` to/from JsValue using
|
||||
/// [`gloo-utils::format::JsValueSerdeExt`].
|
||||
///
|
||||
/// Implements:
|
||||
/// - `TryFrom<&type> for JsValue`
|
||||
/// - `TryFrom<JsValue> for type`
|
||||
macro_rules! impl_try_from_jsvalue {
|
||||
($type:ty) => {
|
||||
impl TryFrom<&$type> for JsValue {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: &$type) -> Result<Self, Self::Error> {
|
||||
JsValue::from_serde(value).map_err($crate::notification::to_js_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JsValue> for $type {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(js_value: JsValue) -> Result<Self, Self::Error> {
|
||||
js_value.into_serde().map_err($crate::notification::to_js_error)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// === SerializableJsValue ===
|
||||
|
||||
/// A wrapper around a `JsValue` that implements the `Serialize` and `Deserialize` traits.
|
||||
#[derive(Clone, Debug, Deref, DerefMut, AsRef, From)]
|
||||
pub struct SerializableJsValue(pub JsValue);
|
||||
|
||||
impl Serialize for SerializableJsValue {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let value = js_sys::JSON::stringify(self).map(String::from).map_err(|e| {
|
||||
serde::ser::Error::custom(format!("Failed to stringify JsValue: {e:?}"))
|
||||
})?;
|
||||
let value = serde_json::from_str::<serde_json::Value>(&value)
|
||||
.map_err(|e| serde::ser::Error::custom(format!("Failed to parse JSON: {e:?}")))?;
|
||||
value.serialize(serializer)
|
||||
ensogl::define_endpoints! {
|
||||
Input {
|
||||
/// Toggles visibility.
|
||||
is_enabled(bool),
|
||||
/// Customize notification.
|
||||
set_options(UpdateOptions),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SerializableJsValue {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
JsValue::from_serde(&value).map(SerializableJsValue).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===============
|
||||
// === Options ===
|
||||
// ===============
|
||||
|
||||
// === AutoClose ===
|
||||
|
||||
/// Helper structure for [`AutoClose`] so it serializes in a way compatible with the JS library.
|
||||
///
|
||||
/// Users of this module should not deal directly with this type. Instead, rely on
|
||||
/// [`AutoClose::After`] or [`AutoClose::Never`]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum AutoCloseInner {
|
||||
/// Auto close after a delay expressed in milliseconds.
|
||||
Delay(u32),
|
||||
/// Do not auto close. The boolean value must be `false`.
|
||||
ShouldEver(bool),
|
||||
}
|
||||
|
||||
/// Represents the auto-close delay of a notification.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Deref)]
|
||||
#[serde(transparent)]
|
||||
pub struct AutoClose(AutoCloseInner);
|
||||
|
||||
impl AutoClose {
|
||||
/// Auto close after a delay expressed in milliseconds.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn After(delay_ms: u32) -> Self {
|
||||
Self(AutoCloseInner::Delay(delay_ms))
|
||||
}
|
||||
|
||||
/// Do not auto close.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Never() -> Self {
|
||||
Self(AutoCloseInner::ShouldEver(false))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === Type ===
|
||||
|
||||
/// Represents the type of a notification.
|
||||
///
|
||||
/// Affects styling and icon.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, strum::AsRefStr)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Type {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
Default,
|
||||
}
|
||||
|
||||
|
||||
// === Position ===
|
||||
|
||||
/// Represents the position of a notification on the screen.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Position {
|
||||
TopRight,
|
||||
TopCenter,
|
||||
TopLeft,
|
||||
BottomRight,
|
||||
BottomCenter,
|
||||
BottomLeft,
|
||||
}
|
||||
|
||||
|
||||
// === DraggableDirection ===
|
||||
|
||||
/// Direction that the notification can be dragged (swiped) to dismiss it.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum DraggableDirection {
|
||||
X,
|
||||
Y,
|
||||
Output {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// === Theme ===
|
||||
// ============
|
||||
// === View ===
|
||||
// ============
|
||||
|
||||
/// Themes supported by the library.
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Theme {
|
||||
Light,
|
||||
Dark,
|
||||
Colored,
|
||||
}
|
||||
|
||||
|
||||
// === Options ===
|
||||
|
||||
/// Customization options for the notification.
|
||||
///
|
||||
/// Note that it is not necessary to set any of these. All options marked as `None` will be
|
||||
/// auto-filled with default values.
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Options {
|
||||
/// Unique notification identifier.
|
||||
pub toast_id: Option<String>,
|
||||
/// Type of the notification, affecting styling and icon. Default: `Type::Default`.
|
||||
pub r#type: Option<Type>,
|
||||
/// The position where the toast should appear. Default: `bottom-right`.
|
||||
pub position: Option<Position>,
|
||||
/// Time in milliseconds after the toast is removed. Default: `5000`.
|
||||
pub auto_close: Option<AutoClose>,
|
||||
/// Whether to show a close button. Default: `true`.
|
||||
pub close_button: Option<bool>,
|
||||
// pub transition: Option<ToastTransition>,
|
||||
/// Hide or show the progress bar. `Default: false`
|
||||
pub hide_progress_bar: Option<bool>,
|
||||
/// Pause the timer when the mouse hover the toast. Default: `true`.
|
||||
pub pause_on_hover: Option<bool>,
|
||||
/// Pause the timer when the window loses focus. Default: `true`.
|
||||
pub pause_on_focus_loss: Option<bool>,
|
||||
/// Remove the toast when clicked. Default: `true`.
|
||||
pub close_on_click: Option<bool>,
|
||||
/// An optional css class to set.
|
||||
pub class_name: Option<String>,
|
||||
/// An optional css class to set for the toast content.
|
||||
pub body_class_name: Option<String>,
|
||||
/// An optional inline style to apply. This should be a JS object with CSS properties.
|
||||
pub style: Option<SerializableJsValue>,
|
||||
/// An optional inline style to apply for the toast content. This should be a JS object with
|
||||
/// CSS properties.
|
||||
pub body_style: Option<SerializableJsValue>,
|
||||
/// An optional css class to set for the progress bar.
|
||||
pub progress_class_name: Option<String>,
|
||||
/// An optional inline style to apply for the progress bar. This should be a JS object with
|
||||
/// CSS properties.
|
||||
pub progress_style: Option<SerializableJsValue>,
|
||||
/// Allow toast to be draggable. `Default: true`.
|
||||
pub draggable: Option<bool>,
|
||||
/// The percentage of the toast's width it takes for a drag to dismiss a toast. `Default: 80`.
|
||||
pub draggable_percent: Option<f32>,
|
||||
/// Specify in which direction should you swipe to dismiss the toast. `Default: "x"`.
|
||||
pub draggable_direction: Option<DraggableDirection>,
|
||||
/// Set id to handle multiple `ToastContainer` instances.
|
||||
pub container_id: Option<String>,
|
||||
/// Define the [ARIA role](https://www.w3.org/WAI/PF/aria/roles) for the notification. `Default: "alert"`.
|
||||
pub role: Option<String>,
|
||||
/// Add a delay in ms before the toast appear.
|
||||
pub delay: Option<u32>,
|
||||
/// Whether the ongoing action is still in progress.
|
||||
///
|
||||
/// The auto-close timer will not start if this is set to `true`.
|
||||
pub is_loading: Option<bool>,
|
||||
/// Support right to left display content. `Default: false`.
|
||||
pub rtl: Option<bool>,
|
||||
/// Theme to use.
|
||||
pub theme: Option<Theme>,
|
||||
/// Used to display a custom icon. Set it to `false` to prevent the icons from being displayed
|
||||
pub icon: Option<bool>,
|
||||
/// Any additional data to pass to the toast.
|
||||
pub data: Option<SerializableJsValue>,
|
||||
}
|
||||
|
||||
impl_try_from_jsvalue! {Options}
|
||||
|
||||
|
||||
// === Update ===
|
||||
|
||||
/// Update options for a toast.
|
||||
///
|
||||
/// Just like [`Options`], but also includes a `render` field that allows to update the
|
||||
/// notification message.
|
||||
#[derive(Debug, Serialize, Deserialize, Default, Deref, DerefMut)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateOptions {
|
||||
/// New state of the toast.
|
||||
/// A reusable popup notification.
|
||||
#[derive(Debug, Clone, CloneRef, Deref)]
|
||||
pub struct View {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[serde(flatten)]
|
||||
pub options: Options,
|
||||
/// Used to update a toast. Pass any valid ReactNode(string, number, component).
|
||||
pub render: Option<SerializableJsValue>,
|
||||
frp: Frp,
|
||||
notification: Notification,
|
||||
}
|
||||
|
||||
impl_try_from_jsvalue! { UpdateOptions }
|
||||
impl View {
|
||||
/// Constructor.
|
||||
pub fn new(_app: &Application) -> Self {
|
||||
let frp = Frp::new();
|
||||
let network = &frp.network;
|
||||
let notification = Notification::default();
|
||||
|
||||
impl UpdateOptions {
|
||||
/// Allows set a new message in the notification.
|
||||
///
|
||||
/// Useful only for update a toast.
|
||||
pub fn render_string(mut self, render: impl AsRef<str>) -> Self {
|
||||
let message = render.as_ref();
|
||||
let value = JsValue::from_str(message);
|
||||
self.render = Some(value.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Tests ===
|
||||
// =============
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wasm_bindgen_test::wasm_bindgen_test;
|
||||
use wasm_bindgen_test::wasm_bindgen_test_configure;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
fn test_auto_close_delay() {
|
||||
let auto_close = AutoClose::After(5000);
|
||||
let serialized = serde_json::to_string(&auto_close).unwrap();
|
||||
assert_eq!(serialized, "5000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auto_close_never() {
|
||||
let auto_close = AutoClose::Never();
|
||||
let serialized = serde_json::to_string(&auto_close).unwrap();
|
||||
assert_eq!(serialized, "false");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_options_marshalling() {
|
||||
let options = Options { theme: Some(Theme::Dark), ..default() };
|
||||
let js_value = JsValue::try_from(&options).unwrap();
|
||||
// Make sure that `js_value` is a valid JS object.
|
||||
assert!(js_value.is_object());
|
||||
let js_object = js_value.dyn_into::<js_sys::Object>().unwrap();
|
||||
// Make sure that `js_object` has a `theme` property.
|
||||
assert!(js_object.has_own_property(&"theme".into()));
|
||||
// Make sure that `js_object.theme` is a string.
|
||||
let theme = js_sys::Reflect::get(&js_object, &"theme".into()).unwrap();
|
||||
assert!(theme.is_string());
|
||||
// Make sure that `js_object.theme` is equal to "dark".
|
||||
let theme = theme.as_string().unwrap();
|
||||
assert_eq!(theme, "dark");
|
||||
frp::extend! { network
|
||||
eval frp.is_enabled ([notification] (enabled) {
|
||||
if *enabled {
|
||||
notification.show()
|
||||
} else {
|
||||
notification.dismiss()
|
||||
}
|
||||
});
|
||||
|
||||
eval frp.set_options ([notification] (options) {
|
||||
notification.update(|opts| {
|
||||
*opts = options.clone();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
Self { frp, notification }
|
||||
}
|
||||
}
|
||||
|
579
app/gui/view/src/notification/api.rs
Normal file
579
app/gui/view/src/notification/api.rs
Normal file
@ -0,0 +1,579 @@
|
||||
//! This is Rust wrapper for the [`react-toastify`](https://fkhadra.github.io/react-toastify) library.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use ide_view::notification::api::*;
|
||||
//! # fn main() -> Result<(), wasm_bindgen::JsValue> {
|
||||
//! let handle = info(
|
||||
//! "Undo triggered in UI.",
|
||||
//! &Some(Options {
|
||||
//! theme: Some(Theme::Dark),
|
||||
//! auto_close: Some(AutoClose::Never()),
|
||||
//! draggable: Some(false),
|
||||
//! close_on_click: Some(false),
|
||||
//! ..Default::default()
|
||||
//! }),
|
||||
//! )?;
|
||||
//! handle.update(&UpdateOptions::default().raw_text_content("Undo done."))?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use crate::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::notification::js;
|
||||
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub use crate::notification::js::Id;
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
// === Primary API ===
|
||||
// ===================
|
||||
|
||||
/// Send any kind of notification.
|
||||
pub fn send_any(message: &Content, r#type: Type, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||
let options = match options {
|
||||
Some(options) => options.try_into()?,
|
||||
None => JsValue::UNDEFINED,
|
||||
};
|
||||
js::toast(message, r#type, &options)
|
||||
}
|
||||
|
||||
/// Send an info notification.
|
||||
pub fn info(message: impl Into<Content>, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||
send_any(&message.into(), Type::Info, options)
|
||||
}
|
||||
|
||||
/// Send a warning notification.
|
||||
pub fn warning(message: impl Into<Content>, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||
send_any(&message.into(), Type::Warning, options)
|
||||
}
|
||||
|
||||
/// Send a error notification.
|
||||
pub fn error(message: impl Into<Content>, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||
send_any(&message.into(), Type::Error, options)
|
||||
}
|
||||
|
||||
/// Send a success notification.
|
||||
pub fn success(message: impl Into<Content>, options: &Option<Options>) -> Result<Id, JsValue> {
|
||||
send_any(&message.into(), Type::Success, options)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============================
|
||||
// === JS-conversion helpers ===
|
||||
// =============================
|
||||
|
||||
|
||||
// === Rust->JS error ===
|
||||
|
||||
/// Convert arbitrary Rust error value into JsValue-based error.
|
||||
pub fn to_js_error(error: impl std::error::Error) -> JsValue {
|
||||
js_sys::Error::new(&error.to_string()).into()
|
||||
}
|
||||
|
||||
|
||||
// === Rust->JS conversion ===
|
||||
|
||||
/// Macro that implements TryFrom for given `type` to/from JsValue using
|
||||
/// [`gloo-utils::format::JsValueSerdeExt`].
|
||||
///
|
||||
/// Implements:
|
||||
/// - `TryFrom<&type> for JsValue`
|
||||
/// - `TryFrom<JsValue> for type`
|
||||
macro_rules! impl_try_from_jsvalue {
|
||||
($type:ty) => {
|
||||
impl TryFrom<&$type> for JsValue {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: &$type) -> Result<Self, Self::Error> {
|
||||
JsValue::from_serde(value).map_err($crate::notification::api::to_js_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JsValue> for $type {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(js_value: JsValue) -> Result<Self, Self::Error> {
|
||||
js_value.into_serde().map_err($crate::notification::api::to_js_error)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// === SerializableJsValue ===
|
||||
|
||||
/// A wrapper around a `JsValue` that implements the `Serialize` and `Deserialize` traits.
|
||||
#[derive(Clone, Debug, Deref, DerefMut, AsRef, From, PartialEq)]
|
||||
pub struct SerializableJsValue(pub JsValue);
|
||||
|
||||
impl Serialize for SerializableJsValue {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let value = js_sys::JSON::stringify(self).map(String::from).map_err(|e| {
|
||||
serde::ser::Error::custom(format!("Failed to stringify JsValue: {e:?}"))
|
||||
})?;
|
||||
let value = serde_json::from_str::<serde_json::Value>(&value)
|
||||
.map_err(|e| serde::ser::Error::custom(format!("Failed to parse JSON: {e:?}")))?;
|
||||
value.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SerializableJsValue {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
JsValue::from_serde(&value).map(SerializableJsValue).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===============
|
||||
// === Content ===
|
||||
// ===============
|
||||
|
||||
/// The notification content.
|
||||
///
|
||||
/// In future this will be extended to support more than just plain text (e.g. HTML).
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum Content {
|
||||
/// Plain text.
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl From<&str> for Content {
|
||||
fn from(text: &str) -> Self {
|
||||
Content::Text(text.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Content {
|
||||
fn from(text: String) -> Self {
|
||||
Content::Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&String> for Content {
|
||||
fn from(text: &String) -> Self {
|
||||
Content::Text(text.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===============
|
||||
// === Options ===
|
||||
// ===============
|
||||
|
||||
// === AutoClose ===
|
||||
|
||||
/// Helper structure for [`AutoClose`] so it serializes in a way compatible with the JS library.
|
||||
///
|
||||
/// Users of this module should not deal directly with this type. Instead, rely on
|
||||
/// [`AutoClose::After`] or [`AutoClose::Never`]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum AutoCloseInner {
|
||||
/// Auto close after a delay expressed in milliseconds.
|
||||
Delay(u32),
|
||||
/// Do not auto close. The boolean value must be `false`.
|
||||
ShouldEver(bool),
|
||||
}
|
||||
|
||||
/// Represents the auto-close delay of a notification.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Deref)]
|
||||
#[serde(transparent)]
|
||||
pub struct AutoClose(AutoCloseInner);
|
||||
|
||||
impl AutoClose {
|
||||
/// Auto close after a delay expressed in milliseconds.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn After(delay_ms: u32) -> Self {
|
||||
Self(AutoCloseInner::Delay(delay_ms))
|
||||
}
|
||||
|
||||
/// Do not auto close.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Never() -> Self {
|
||||
Self(AutoCloseInner::ShouldEver(false))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === Type ===
|
||||
|
||||
/// Represents the type of a notification.
|
||||
///
|
||||
/// Affects styling and icon.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, strum::AsRefStr, strum::Display)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Type {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
Default,
|
||||
}
|
||||
|
||||
|
||||
// === Position ===
|
||||
|
||||
/// Represents the position of a notification on the screen.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Position {
|
||||
TopRight,
|
||||
TopCenter,
|
||||
TopLeft,
|
||||
BottomRight,
|
||||
BottomCenter,
|
||||
BottomLeft,
|
||||
}
|
||||
|
||||
|
||||
// === DraggableDirection ===
|
||||
|
||||
/// Direction that the notification can be dragged (swiped) to dismiss it.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum DraggableDirection {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// === Theme ===
|
||||
|
||||
/// Themes supported by the library.
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Theme {
|
||||
Light,
|
||||
Dark,
|
||||
Colored,
|
||||
}
|
||||
|
||||
|
||||
// === Options ===
|
||||
|
||||
/// Customization options for the notification.
|
||||
///
|
||||
/// Note that it is not necessary to set any of these. All options marked as `None` will be
|
||||
/// auto-filled with default values.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Options {
|
||||
/// Unique notification identifier.
|
||||
pub toast_id: Option<String>,
|
||||
/// Type of the notification, affecting styling and icon. Default: `Type::Default`.
|
||||
pub r#type: Option<Type>,
|
||||
/// The position where the toast should appear. Default: `bottom-right`.
|
||||
pub position: Option<Position>,
|
||||
/// Time in milliseconds after the toast is removed. Default: `5000`.
|
||||
pub auto_close: Option<AutoClose>,
|
||||
/// Whether to show a close button. Default: `true`.
|
||||
pub close_button: Option<bool>,
|
||||
// pub transition: Option<ToastTransition>,
|
||||
/// Hide or show the progress bar. `Default: false`
|
||||
pub hide_progress_bar: Option<bool>,
|
||||
/// Pause the timer when the mouse hover the toast. Default: `true`.
|
||||
pub pause_on_hover: Option<bool>,
|
||||
/// Pause the timer when the window loses focus. Default: `true`.
|
||||
pub pause_on_focus_loss: Option<bool>,
|
||||
/// Remove the toast when clicked. Default: `true`.
|
||||
pub close_on_click: Option<bool>,
|
||||
/// An optional css class to set.
|
||||
pub class_name: Option<String>,
|
||||
/// An optional css class to set for the toast content.
|
||||
pub body_class_name: Option<String>,
|
||||
/// An optional inline style to apply. This should be a JS object with CSS properties.
|
||||
pub style: Option<SerializableJsValue>,
|
||||
/// An optional inline style to apply for the toast content. This should be a JS object with
|
||||
/// CSS properties.
|
||||
pub body_style: Option<SerializableJsValue>,
|
||||
/// An optional css class to set for the progress bar.
|
||||
pub progress_class_name: Option<String>,
|
||||
/// An optional inline style to apply for the progress bar. This should be a JS object with
|
||||
/// CSS properties.
|
||||
pub progress_style: Option<SerializableJsValue>,
|
||||
/// Allow toast to be draggable. `Default: true`.
|
||||
pub draggable: Option<bool>,
|
||||
/// The percentage of the toast's width it takes for a drag to dismiss a toast. `Default: 80`.
|
||||
pub draggable_percent: Option<f32>,
|
||||
/// Specify in which direction should you swipe to dismiss the toast. `Default: "x"`.
|
||||
pub draggable_direction: Option<DraggableDirection>,
|
||||
/// Set id to handle multiple `ToastContainer` instances.
|
||||
pub container_id: Option<String>,
|
||||
/// Define the [ARIA role](https://www.w3.org/WAI/PF/aria/roles) for the notification. `Default: "alert"`.
|
||||
pub role: Option<String>,
|
||||
/// Add a delay in ms before the toast appear.
|
||||
pub delay: Option<u32>,
|
||||
/// Whether the ongoing action is still in progress.
|
||||
///
|
||||
/// The auto-close timer will not start if this is set to `true`.
|
||||
pub is_loading: Option<bool>,
|
||||
/// Support right to left display content. `Default: false`.
|
||||
pub rtl: Option<bool>,
|
||||
/// Theme to use.
|
||||
pub theme: Option<Theme>,
|
||||
/// Used to display a custom icon. Set it to `false` to prevent the icons from being displayed
|
||||
pub icon: Option<bool>,
|
||||
/// Any additional data to pass to the toast.
|
||||
pub data: Option<SerializableJsValue>,
|
||||
}
|
||||
|
||||
impl_try_from_jsvalue! {Options}
|
||||
|
||||
impl Options {
|
||||
/// Popup will not disappear on itself nor allow user to dismiss it.
|
||||
///
|
||||
/// The only way fot this notification to disappear is to call `dismiss()` manually. Otherwise,
|
||||
/// it will stay on screen indefinitely.
|
||||
pub fn always_present(mut self) -> Self {
|
||||
self.set_always_present();
|
||||
self
|
||||
}
|
||||
|
||||
/// Mutable version of [`Options::always_present`].
|
||||
pub fn set_always_present(&mut self) {
|
||||
self.auto_close = Some(AutoClose::Never());
|
||||
self.close_button = Some(false);
|
||||
self.close_on_click = Some(false);
|
||||
self.draggable = Some(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === Update ===
|
||||
|
||||
/// Update options for a toast.
|
||||
///
|
||||
/// Just like [`Options`], but also includes a `render` field that allows to update the
|
||||
/// notification message.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, Deref, DerefMut)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateOptions {
|
||||
/// New state of the toast.
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[serde(flatten)]
|
||||
pub options: Options,
|
||||
/// Used to update a toast content.
|
||||
pub render: Option<Content>,
|
||||
}
|
||||
|
||||
impl_try_from_jsvalue! { UpdateOptions }
|
||||
|
||||
impl UpdateOptions {
|
||||
/// Allows set a new message in the notification.
|
||||
///
|
||||
/// Useful only for update a toast.
|
||||
pub fn raw_text_content(mut self, render: impl AsRef<str>) -> Self {
|
||||
let message = render.as_ref();
|
||||
self.render = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Allows set a new message in the notification.
|
||||
///
|
||||
/// This sets is as plain text (it will not be interpreted as HTML).
|
||||
pub fn set_raw_text_content(&mut self, render: impl Into<String>) {
|
||||
let content = Content::Text(render.into());
|
||||
self.render = Some(content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === Notification ===
|
||||
// ====================
|
||||
|
||||
/// A persistent notification.
|
||||
///
|
||||
/// It can be updated or dismissed.
|
||||
#[derive(Clone, CloneRef, Debug)]
|
||||
pub struct Notification {
|
||||
/// The unique notification id. We use it to dismiss or update the notification.
|
||||
id: Rc<RefCell<Id>>,
|
||||
/// The notification options. They will be used to show/update the notification.
|
||||
options: Rc<RefCell<UpdateOptions>>,
|
||||
/// The options that we have used to show the notification the last time.
|
||||
last_shown: Rc<RefCell<Option<UpdateOptions>>>,
|
||||
}
|
||||
|
||||
impl Default for Notification {
|
||||
fn default() -> Self {
|
||||
Self::new(default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for Notification {
|
||||
fn from(id: Id) -> Self {
|
||||
let id = Rc::new(RefCell::new(id));
|
||||
let options = default();
|
||||
let last_shown = default();
|
||||
Self { id, options, last_shown }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Notification {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&*self.id.borrow(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
/// Create a new notification archetype.
|
||||
///
|
||||
/// It will not be shown until you call [`Notification::show`].
|
||||
pub fn new(options: UpdateOptions) -> Self {
|
||||
let id = Id::new_unique();
|
||||
let id = Rc::new(RefCell::new(id));
|
||||
let options = Rc::new(RefCell::new(options));
|
||||
let last_shown = default();
|
||||
Self { id, options, last_shown }
|
||||
}
|
||||
|
||||
/// Update the notification state.
|
||||
///
|
||||
/// If the visualization is being shown, it will be updated. If not, changes will appear the
|
||||
/// next time the notification is [`shown`](Notification::show).
|
||||
pub fn set_options(&self, options: UpdateOptions) -> Result<(), JsValue> {
|
||||
self.options.replace(options);
|
||||
let ret = self.id.borrow().update(&self.options.borrow());
|
||||
if ret.is_ok() {
|
||||
let new_options = self.options.borrow().clone();
|
||||
// If the ID was updated, we need to keep track of it.
|
||||
if let Some(new_id) = new_options.toast_id.as_ref() {
|
||||
self.id.replace(new_id.as_str().into());
|
||||
}
|
||||
self.last_shown.replace(Some(new_options));
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Convenience wrapper for [`Notification::set_options`].
|
||||
///
|
||||
/// Allows to modify the options in place.
|
||||
pub fn update(&self, f: impl FnOnce(&mut UpdateOptions)) -> Result<(), JsValue> {
|
||||
let mut options = self.options.take();
|
||||
f(&mut options);
|
||||
self.set_options(options)
|
||||
}
|
||||
|
||||
/// Display the notification.
|
||||
///
|
||||
/// If it is already being shown, nothing will happen.
|
||||
pub fn show(&self) -> Result<(), JsValue> {
|
||||
if !self.id.borrow().is_active()? {
|
||||
let options = self.options.borrow_mut();
|
||||
let content = options.render.as_ref();
|
||||
let options_js = (&*options).try_into()?;
|
||||
let toast_id_field = JsValue::from_str(js::TOAST_ID_FIELD_IN_OPTIONS);
|
||||
let toast_type = options.r#type.unwrap_or(Type::Info);
|
||||
js_sys::Reflect::set(&options_js, &toast_id_field, &self.id.borrow())?;
|
||||
let toast = |content| js::toast(content, toast_type, &options_js);
|
||||
if let Some(content) = content {
|
||||
toast(content)?;
|
||||
} else {
|
||||
const EMPTY_MESSAGE: &str = "";
|
||||
toast(&EMPTY_MESSAGE.into())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dismiss the notification.
|
||||
pub fn dismiss(&self) -> Result<(), JsValue> {
|
||||
let ret = self.id.borrow().dismiss();
|
||||
if ret.is_ok() {
|
||||
// We generate a new ID. This is because we don't want to reuse the same ID.
|
||||
// Otherwise, while the old notification is still fading out, an attempt to
|
||||
// show a new notification with the same ID will silently fail.
|
||||
let id = Id::new_unique();
|
||||
self.id.replace(id);
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Tests ===
|
||||
// =============
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wasm_bindgen_test::wasm_bindgen_test;
|
||||
use wasm_bindgen_test::wasm_bindgen_test_configure;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
fn auto_close_delay() {
|
||||
let auto_close = AutoClose::After(5000);
|
||||
let serialized = serde_json::to_string(&auto_close).unwrap();
|
||||
assert_eq!(serialized, "5000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_close_never() {
|
||||
let auto_close = AutoClose::Never();
|
||||
let serialized = serde_json::to_string(&auto_close).unwrap();
|
||||
assert_eq!(serialized, "false");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn options_marshalling() {
|
||||
let options = Options { theme: Some(Theme::Dark), ..default() };
|
||||
let js_value = JsValue::try_from(&options).unwrap();
|
||||
// Make sure that `js_value` is a valid JS object.
|
||||
assert!(js_value.is_object());
|
||||
let js_object = js_value.dyn_into::<js_sys::Object>().unwrap();
|
||||
// Make sure that `js_object` has a `theme` property.
|
||||
assert!(js_object.has_own_property(&"theme".into()));
|
||||
// Make sure that `js_object.theme` is a string.
|
||||
let theme = js_sys::Reflect::get(&js_object, &"theme".into()).unwrap();
|
||||
assert!(theme.is_string());
|
||||
// Make sure that `js_object.theme` is equal to "dark".
|
||||
let theme = theme.as_string().unwrap();
|
||||
assert_eq!(theme, "dark");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn contents_marshalling() {
|
||||
let text_contents = Content::Text("Hello, world!".into());
|
||||
let js_value = JsValue::from_serde(&text_contents).unwrap();
|
||||
// Make sure that `js_value` is a valid JS String.
|
||||
assert!(js_value.is_string());
|
||||
let js_string = js_value.as_string().unwrap();
|
||||
// Make sure that `js_string` is equal to "Hello, world!".
|
||||
assert_eq!(js_string, "Hello, world!");
|
||||
// Check for round-trip.
|
||||
let back = js_value.into_serde::<Content>().unwrap();
|
||||
assert_eq!(back, text_contents);
|
||||
}
|
||||
}
|
@ -4,8 +4,12 @@
|
||||
use crate::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::notification::Type;
|
||||
use crate::notification::UpdateOptions;
|
||||
use crate::notification::api::Content;
|
||||
use crate::notification::api::Type;
|
||||
use crate::notification::api::UpdateOptions;
|
||||
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
|
||||
@ -15,8 +19,12 @@ use crate::notification::UpdateOptions;
|
||||
|
||||
/// Toastify's [`toast API`](https://fkhadra.github.io/react-toastify/api/toast) field name within
|
||||
/// our application JS object.
|
||||
const TOAST_FIELD_NAME: &str = "toast";
|
||||
pub const TOAST_FIELD_NAME: &str = "toast";
|
||||
|
||||
/// The name of the field with the toast ID in the options object.
|
||||
///
|
||||
/// See [the documentation](https://fkhadra.github.io/react-toastify/api/toast/#props) for details.
|
||||
pub const TOAST_ID_FIELD_IN_OPTIONS: &str = "toastId";
|
||||
|
||||
|
||||
// ========================
|
||||
@ -40,12 +48,34 @@ pub fn get_toast() -> Result<ToastAPI, JsValue> {
|
||||
// === JS bindings ===
|
||||
// ===================
|
||||
|
||||
// Wrappers for [`toast`](https://react-hot-toast.com/docs/toast) API.
|
||||
/// Wrappers for [`toast`](https://react-hot-toast.com/docs/toast) API and related helpers.
|
||||
#[wasm_bindgen(inline_js = r#"
|
||||
export function sendToast(toast, message, method, options) {
|
||||
const target = toast[method];
|
||||
return target(message, options);
|
||||
}
|
||||
|
||||
// See [`pretty_print`] Rust wrapper below for the documentation.
|
||||
export function prettyPrint(value) {
|
||||
try {
|
||||
// If it's an `Error`, print its standard properties.
|
||||
if (value instanceof Error) {
|
||||
const { name, message, stack } = value;
|
||||
const errorDetails = { name, message, stack };
|
||||
return JSON.stringify(errorDetails, null, 2);
|
||||
}
|
||||
|
||||
// If it's an object (not `null` and not an `Array`), pretty-print its properties.
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
// If it's a primitive value or an `Array`, just convert it to a string.
|
||||
return String(value);
|
||||
} catch (error) {
|
||||
return prettyPrint(error);
|
||||
}
|
||||
}
|
||||
"#)]
|
||||
extern "C" {
|
||||
/// The wrapper for the toastify API.
|
||||
@ -60,7 +90,7 @@ extern "C" {
|
||||
#[derive(Clone, Debug)]
|
||||
pub type Id;
|
||||
|
||||
/// The unique identifier of a toast.
|
||||
/// The unique identifier of a toast container.
|
||||
#[derive(Clone, Debug)]
|
||||
pub type ContainerId;
|
||||
|
||||
@ -69,7 +99,7 @@ extern "C" {
|
||||
#[allow(unsafe_code)]
|
||||
pub fn sendToast(
|
||||
this: &ToastAPI,
|
||||
message: &str,
|
||||
message: &JsValue,
|
||||
method: &str,
|
||||
options: &JsValue,
|
||||
) -> Result<Id, JsValue>;
|
||||
@ -119,6 +149,18 @@ extern "C" {
|
||||
#[wasm_bindgen(catch, method)]
|
||||
#[allow(unsafe_code)]
|
||||
pub fn done(this: &ToastAPI, id: &Id) -> Result<(), JsValue>;
|
||||
|
||||
/// Pretty-prints any value to a string.
|
||||
///
|
||||
/// This function's primary purpose is to pretty-print `Error` values, including all
|
||||
/// accessible details. However, it can also handle other types of values, converting
|
||||
/// them to strings as accurately as possible. For `Error` instances, it outputs standard
|
||||
/// properties (`name`, `message`, `stack`). For non-null objects and arrays, it converts
|
||||
/// them to a JSON string with indentation. For all other types of values, it uses
|
||||
/// JavaScript's default string conversion.
|
||||
#[wasm_bindgen(js_name = prettyPrint)]
|
||||
#[allow(unsafe_code)]
|
||||
pub fn pretty_print(value: &JsValue) -> String;
|
||||
}
|
||||
|
||||
|
||||
@ -129,12 +171,46 @@ extern "C" {
|
||||
|
||||
impl ToastAPI {
|
||||
/// Send the toast notification.
|
||||
pub fn send(&self, message: &str, method: &str, options: &JsValue) -> Result<Id, JsValue> {
|
||||
sendToast(self, message, method, options)
|
||||
pub fn send(&self, message: &Content, method: &str, options: &JsValue) -> Result<Id, JsValue> {
|
||||
let message =
|
||||
JsValue::from_serde(message).map_err(crate::notification::api::to_js_error)?;
|
||||
sendToast(self, &message, method, options)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Id {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&pretty_print(self), f)
|
||||
}
|
||||
}
|
||||
impl From<&str> for Id {
|
||||
fn from(id: &str) -> Self {
|
||||
let js_value = JsValue::from_str(id);
|
||||
js_value.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Uuid> for Id {
|
||||
fn from(id: Uuid) -> Self {
|
||||
Self::new(id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Id {
|
||||
/// Create a new identifier, using the given string.
|
||||
///
|
||||
/// The two identifiers are equal if their string representations are equal. Please note, that
|
||||
/// identifiers of different notifications must not be equal.
|
||||
pub fn new(id: impl AsRef<str>) -> Self {
|
||||
id.as_ref().into()
|
||||
}
|
||||
|
||||
/// Generate a new, unique identifier.
|
||||
pub fn new_unique() -> Self {
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
Self::new(uuid.to_string())
|
||||
}
|
||||
|
||||
/// Dismisses the toast.
|
||||
pub fn dismiss(&self) -> Result<(), JsValue> {
|
||||
get_toast()?.dismiss(self)
|
||||
@ -156,7 +232,6 @@ impl Id {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ContainerId {
|
||||
/// Clear queue of notifications for this container (relevant if limit is set).
|
||||
pub fn clear_waiting_queue(&self) -> Result<(), JsValue> {
|
||||
@ -165,7 +240,44 @@ impl ContainerId {
|
||||
}
|
||||
|
||||
/// Wrapper for sending arbitrary kind of toast.
|
||||
pub fn toast(message: &str, r#type: Type, options: &JsValue) -> Result<Id, JsValue> {
|
||||
pub fn toast(message: &Content, r#type: Type, options: &JsValue) -> Result<Id, JsValue> {
|
||||
let method: &str = r#type.as_ref();
|
||||
get_toast()?.send(message, method, options)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ======================
|
||||
// === Error handling ===
|
||||
// ======================
|
||||
|
||||
/// Extension trait for `Result<T, JsValue>` that provides a method for handling JavaScript errors.
|
||||
pub trait HandleJsError<T>: Sized {
|
||||
/// Format pretty error message.
|
||||
fn pretty_print_error(message: Option<&str>, error: &JsValue) -> String {
|
||||
let mut ret = String::from("Error received from JavaScript.");
|
||||
if let Some(message) = message {
|
||||
ret.push(' ');
|
||||
ret.push_str(message);
|
||||
}
|
||||
ret.push(' ');
|
||||
ret.push_str(&pretty_print(error));
|
||||
ret
|
||||
}
|
||||
|
||||
/// Handle JS error by logging it and returning `None`.
|
||||
fn handle_js_err(self, message: &str) -> Option<T>;
|
||||
|
||||
/// Handle JS error by logging it along with dynamically generated message and returning `None`.
|
||||
fn handle_js_err_with(self, message_provider: impl FnOnce() -> String) -> Option<T> {
|
||||
self.handle_js_err(&message_provider())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> HandleJsError<T> for Result<T, JsValue> {
|
||||
fn handle_js_err(self, message: &str) -> Option<T> {
|
||||
self.handle_err(|e| {
|
||||
error!("{}", Self::pretty_print_error(Some(message), &e));
|
||||
})
|
||||
}
|
||||
}
|
||||
|
181
app/gui/view/src/notification/logged.rs
Normal file
181
app/gui/view/src/notification/logged.rs
Normal file
@ -0,0 +1,181 @@
|
||||
//! This module provides a [notification API](crate::notification::api) wrapper that:
|
||||
//! * Logs most of the actions.
|
||||
//! * Handles errors by logging and dropping them.
|
||||
//!
|
||||
//! The API returns [`Result`]s with [`JsValue`] as an error type which is often inconvenient for
|
||||
//! our use-cases. We often use notifications to display errors and we don't have a way to handle
|
||||
//! errors from the notifications themselves.
|
||||
//!
|
||||
//! In general, where the original API returns [`Result`]s, this wrapper returns [`Option`]s.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::notification;
|
||||
use crate::notification::js::HandleJsError;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub use crate::notification::api::AutoClose;
|
||||
pub use crate::notification::api::Content;
|
||||
pub use crate::notification::api::Options;
|
||||
pub use crate::notification::api::Type;
|
||||
pub use crate::notification::api::UpdateOptions;
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
// === Primary API ===
|
||||
// ===================
|
||||
|
||||
/// Send any kind of notification.
|
||||
pub fn send_any(message: &Content, r#type: Type, options: &Option<Options>) -> Option<Id> {
|
||||
let log_content = format!("Sending notification with message {message:?} and type {type:?}");
|
||||
match r#type {
|
||||
Type::Warning => warn!("{log_content}"),
|
||||
Type::Error => error!("{log_content}"),
|
||||
_ => info!("{log_content}"),
|
||||
};
|
||||
notification::api::send_any(message, r#type, options).map(Id).handle_js_err_with(|| {
|
||||
format!("Failed to send {type} notification with message {message:?}.")
|
||||
})
|
||||
}
|
||||
|
||||
/// Send an info notification.
|
||||
pub fn info(message: impl Into<Content>, options: &Option<Options>) -> Option<Id> {
|
||||
send_any(&message.into(), Type::Info, options)
|
||||
}
|
||||
|
||||
/// Send a warning notification.
|
||||
pub fn warning(message: impl Into<Content>, options: &Option<Options>) -> Option<Id> {
|
||||
send_any(&message.into(), Type::Warning, options)
|
||||
}
|
||||
|
||||
/// Send a error notification.
|
||||
pub fn error(message: impl Into<Content>, options: &Option<Options>) -> Option<Id> {
|
||||
send_any(&message.into(), Type::Error, options)
|
||||
}
|
||||
|
||||
/// Send a success notification.
|
||||
pub fn success(message: impl Into<Content>, options: &Option<Options>) -> Option<Id> {
|
||||
send_any(&message.into(), Type::Success, options)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==========
|
||||
// === Id ===
|
||||
// ==========
|
||||
|
||||
/// The unique identifier of a toast.
|
||||
#[derive(Clone, Debug, Display)]
|
||||
pub struct Id(notification::api::Id);
|
||||
|
||||
impl From<notification::api::Id> for Id {
|
||||
fn from(id: notification::api::Id) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Id {
|
||||
fn from(id: &str) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Uuid> for Id {
|
||||
fn from(id: Uuid) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Id {
|
||||
/// Close the notification.
|
||||
pub fn dismiss(&self) {
|
||||
info!("Dismissing the notification {self}.");
|
||||
self.0.dismiss().handle_js_err_with(|| format!("Failed to dismiss notification {self:?}."));
|
||||
}
|
||||
|
||||
/// Completes the controlled progress bar.
|
||||
pub fn done(&self) {
|
||||
info!("Completing the notification {self}.");
|
||||
self.0.done().handle_js_err_with(|| format!("Failed to complete notification {self:?}."));
|
||||
}
|
||||
|
||||
/// Check if a toast is displayed or not.
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.0
|
||||
.is_active()
|
||||
.handle_js_err_with(|| format!("Failed to check if notification {self:?} is active."))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Update a toast.
|
||||
pub fn update(&self, options: &UpdateOptions) {
|
||||
info!("Updating the notification {self} with new options {options:?}.");
|
||||
self.0
|
||||
.update(options)
|
||||
.handle_js_err_with(|| format!("Failed to update notification {self:?}."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === Notification ===
|
||||
// ====================
|
||||
|
||||
/// Same as super::Notification, but with all errors handled by logging them using error! macro.
|
||||
|
||||
/// A persistent notification.
|
||||
#[derive(Clone, CloneRef, Debug, Display)]
|
||||
pub struct Notification(notification::api::Notification);
|
||||
|
||||
impl Default for Notification {
|
||||
fn default() -> Self {
|
||||
Self::new(default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for Notification {
|
||||
fn from(id: Id) -> Self {
|
||||
Self(notification::api::Notification::from(id.0))
|
||||
}
|
||||
}
|
||||
impl Notification {
|
||||
/// Create a new notification archetype.
|
||||
///
|
||||
/// It will not be shown until you call [`Notification::show`].
|
||||
pub fn new(options: UpdateOptions) -> Self {
|
||||
info!("Creating a new notification with options {options:?}.");
|
||||
Self(notification::api::Notification::new(options))
|
||||
}
|
||||
|
||||
/// Update the notification state.
|
||||
///
|
||||
/// If the visualization is being shown, it will be updated. If not, changes will appear the
|
||||
/// next time the notification is [
|
||||
pub fn update(&self, f: impl FnOnce(&mut UpdateOptions)) {
|
||||
info!("Updating the notification {self}.");
|
||||
self.0.update(f).handle_js_err_with(|| format!("Failed to update notification {self}."));
|
||||
}
|
||||
|
||||
/// Display the notification.
|
||||
///
|
||||
/// If it is already being shown, nothing will happen.
|
||||
pub fn show(&self) {
|
||||
info!("Showing the notification {self}.");
|
||||
self.0.show().handle_js_err_with(|| format!("Failed to show notification {self}."));
|
||||
}
|
||||
|
||||
/// Dismiss the notification.
|
||||
pub fn dismiss(&self) {
|
||||
info!("Dismissing the notification {self}.");
|
||||
self.0.dismiss().handle_js_err_with(|| format!("Failed to dismiss notification {self}."));
|
||||
}
|
||||
}
|
@ -7,13 +7,10 @@ use ensogl::system::web::traits::*;
|
||||
use crate::code_editor;
|
||||
use crate::component_browser;
|
||||
use crate::component_browser::component_list_panel;
|
||||
use crate::debug_mode_popup;
|
||||
use crate::debug_mode_popup::DEBUG_MODE_SHORTCUT;
|
||||
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::popup;
|
||||
use crate::project_list::ProjectList;
|
||||
|
||||
use enso_config::ARGS;
|
||||
@ -50,6 +47,18 @@ use ide_view_project_view_top_bar::ProjectViewTopBar;
|
||||
const INPUT_CHANGE_DELAY_MS: i32 = 200;
|
||||
|
||||
|
||||
/// Mitigate limitations of constant strings concatenation.
|
||||
macro_rules! define_debug_mode_shortcut {
|
||||
($shortcut:literal) => {
|
||||
/// A keyboard shortcut used to enable/disable Debug Mode.
|
||||
pub const DEBUG_MODE_SHORTCUT: &str = $shortcut;
|
||||
const DEBUG_MODE_ENABLED: &str =
|
||||
concat!("Debug Mode enabled. To disable, press `", $shortcut, "`.");
|
||||
};
|
||||
}
|
||||
|
||||
define_debug_mode_shortcut!("ctrl shift d");
|
||||
|
||||
|
||||
// ===========
|
||||
// === FRP ===
|
||||
@ -191,8 +200,7 @@ struct Model {
|
||||
code_editor: code_editor::View,
|
||||
fullscreen_vis: Rc<RefCell<Option<visualization::fullscreen::Panel>>>,
|
||||
project_list: Rc<ProjectList>,
|
||||
debug_mode_popup: debug_mode_popup::View,
|
||||
popup: popup::View,
|
||||
debug_mode_popup: Rc<crate::notification::View>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@ -202,16 +210,13 @@ impl Model {
|
||||
let graph_editor = app.new_view::<GraphEditor>();
|
||||
let code_editor = app.new_view::<code_editor::View>();
|
||||
let fullscreen_vis = default();
|
||||
let debug_mode_popup = debug_mode_popup::View::new(app);
|
||||
let popup = popup::View::new(app);
|
||||
let debug_mode_popup = Rc::new(crate::notification::View::new(app));
|
||||
let project_view_top_bar = ProjectViewTopBar::new(app);
|
||||
let project_list = Rc::new(ProjectList::new(app));
|
||||
|
||||
display_object.add_child(&graph_editor);
|
||||
display_object.add_child(&code_editor);
|
||||
display_object.add_child(&searcher);
|
||||
display_object.add_child(&debug_mode_popup);
|
||||
display_object.add_child(&popup);
|
||||
display_object.add_child(&project_view_top_bar);
|
||||
display_object.remove_child(&searcher);
|
||||
|
||||
@ -225,7 +230,6 @@ impl Model {
|
||||
fullscreen_vis,
|
||||
project_list,
|
||||
debug_mode_popup,
|
||||
popup,
|
||||
}
|
||||
}
|
||||
|
||||
@ -465,7 +469,6 @@ impl View {
|
||||
disable_navigation <- searcher_active || frp.project_list_shown;
|
||||
graph.set_navigator_disabled <+ disable_navigation;
|
||||
|
||||
model.popup.set_label <+ graph.visualization_update_error._1();
|
||||
graph.set_read_only <+ frp.set_read_only;
|
||||
graph.set_debug_mode <+ frp.source.debug_mode;
|
||||
|
||||
@ -745,11 +748,17 @@ impl View {
|
||||
let frp = &self.frp;
|
||||
let network = &frp.network;
|
||||
let popup = &self.model.debug_mode_popup;
|
||||
frp::extend! { network
|
||||
frp.source.debug_mode <+ bool(&frp.disable_debug_mode, &frp.enable_debug_mode);
|
||||
|
||||
popup.enabled <+ frp.enable_debug_mode;
|
||||
popup.disabled <+ frp.disable_debug_mode;
|
||||
let mut options = crate::notification::api::UpdateOptions::default();
|
||||
options.set_always_present();
|
||||
options.set_raw_text_content(DEBUG_MODE_ENABLED);
|
||||
options.position = Some(crate::notification::api::Position::BottomRight);
|
||||
popup.set_options(options);
|
||||
|
||||
frp::extend! { network
|
||||
debug_mode <- bool(&frp.disable_debug_mode, &frp.enable_debug_mode);
|
||||
frp.source.debug_mode <+ debug_mode;
|
||||
popup.is_enabled <+ debug_mode;
|
||||
}
|
||||
self
|
||||
}
|
||||
@ -807,16 +816,6 @@ impl View {
|
||||
pub fn project_list(&self) -> &ProjectList {
|
||||
&self.model.project_list
|
||||
}
|
||||
|
||||
/// Debug Mode Popup
|
||||
pub fn debug_mode_popup(&self) -> &debug_mode_popup::View {
|
||||
&self.model.debug_mode_popup
|
||||
}
|
||||
|
||||
/// Pop-up
|
||||
pub fn popup(&self) -> &popup::View {
|
||||
&self.model.popup
|
||||
}
|
||||
}
|
||||
|
||||
impl FrpNetworkProvider for View {
|
||||
|
@ -10,7 +10,6 @@ use enso_frp as frp;
|
||||
use ensogl::application;
|
||||
use ensogl::application::Application;
|
||||
use ensogl::display;
|
||||
use ensogl::display::shape::StyleWatchFrp;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
||||
@ -36,7 +35,6 @@ pub struct Model {
|
||||
app: Application,
|
||||
display_object: display::object::Instance,
|
||||
state: Rc<CloneCell<State>>,
|
||||
status_bar: crate::status_bar::View,
|
||||
welcome_view: crate::welcome_screen::View,
|
||||
project_view: Rc<CloneCell<Option<crate::project::View>>>,
|
||||
}
|
||||
@ -47,13 +45,11 @@ impl Model {
|
||||
let app = app.clone_ref();
|
||||
let display_object = display::object::Instance::new();
|
||||
let state = Rc::new(CloneCell::new(State::WelcomeScreen));
|
||||
let status_bar = crate::status_bar::View::new(&app);
|
||||
display_object.add_child(&status_bar);
|
||||
let welcome_view = app.new_view::<crate::welcome_screen::View>();
|
||||
let project_view = Rc::new(CloneCell::new(None));
|
||||
display_object.add_child(&welcome_view);
|
||||
|
||||
Self { app, display_object, state, status_bar, welcome_view, project_view }
|
||||
Self { app, display_object, state, welcome_view, project_view }
|
||||
}
|
||||
|
||||
/// Switch displayed view from Project View to Welcome Screen. Project View will not be
|
||||
@ -83,15 +79,6 @@ impl Model {
|
||||
fn init_project_view(&self) {
|
||||
if self.project_view.get().is_none() {
|
||||
let view = self.app.new_view::<crate::project::View>();
|
||||
let network = &view.network;
|
||||
let status_bar = &self.status_bar;
|
||||
let display_object = &self.display_object;
|
||||
frp::extend! { network
|
||||
fs_vis_shown <- view.fullscreen_visualization_shown.on_true();
|
||||
fs_vis_hidden <- view.fullscreen_visualization_shown.on_false();
|
||||
eval fs_vis_shown ((_) status_bar.unset_parent());
|
||||
eval fs_vis_hidden ([display_object, status_bar](_) display_object.add_child(&status_bar));
|
||||
}
|
||||
self.project_view.set(Some(view));
|
||||
}
|
||||
}
|
||||
@ -136,30 +123,20 @@ impl View {
|
||||
let model = Model::new(app);
|
||||
let frp = Frp::new();
|
||||
let network = &frp.network;
|
||||
let style = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
|
||||
let offset_y = style.get_number(ensogl_hardcoded_theme::application::status_bar::offset_y);
|
||||
|
||||
frp::extend! { network
|
||||
init <- source::<()>();
|
||||
|
||||
eval_ frp.switch_view_to_project(model.switch_view_to_project());
|
||||
eval_ frp.switch_view_to_welcome_screen(model.switch_view_to_welcome_screen());
|
||||
offset_y <- all(&init,&offset_y)._1();
|
||||
eval offset_y ((offset_y) model.status_bar.set_y(*offset_y));
|
||||
|
||||
model.status_bar.add_event <+ app.frp.show_notification.map(|message| {
|
||||
message.into()
|
||||
eval app.frp.show_notification([](message) {
|
||||
crate::notification::logged::info(message, &None);
|
||||
});
|
||||
}
|
||||
init.emit(());
|
||||
Self { model, frp }
|
||||
}
|
||||
|
||||
/// Status Bar view from Project View.
|
||||
pub fn status_bar(&self) -> &crate::status_bar::View {
|
||||
&self.model.status_bar
|
||||
}
|
||||
|
||||
/// Lazily initializes Project View.
|
||||
pub fn project(&self) -> crate::project::View {
|
||||
self.model.get_or_init_project_view()
|
||||
|
@ -1,308 +0,0 @@
|
||||
//! A module containing IDE status bar component definitions (frp, model, view, etc.)
|
||||
//!
|
||||
//! The component is currently rather a stub: it has endpoints for setting many events and
|
||||
//! processes and keep them in a list, but it shows only a label of the last event/process
|
||||
//! added.
|
||||
|
||||
//TODO[ao] Implement the status bar according to https://github.com/enso-org/ide/issues/1193
|
||||
// description
|
||||
|
||||
use crate::prelude::*;
|
||||
use ensogl::display::shape::*;
|
||||
|
||||
use crate::graph_editor::component::node::input::area::TEXT_SIZE;
|
||||
|
||||
use ensogl::application::Application;
|
||||
use ensogl::display;
|
||||
use ensogl::display::camera::Camera2d;
|
||||
use ensogl_hardcoded_theme as theme;
|
||||
use ensogl_text as text;
|
||||
use std::future::Future;
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/// The height of the status bar.
|
||||
const HEIGHT: f32 = 28.0;
|
||||
/// Padding inside the status bar.
|
||||
pub const PADDING: f32 = 12.0;
|
||||
/// Margin between status bar and edge of the screen
|
||||
const MARGIN: f32 = 12.0;
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Event ===
|
||||
// =============
|
||||
|
||||
/// Structures related to events in a status bar.
|
||||
pub mod event {
|
||||
use crate::prelude::*;
|
||||
|
||||
/// An id of some event displayed in a status bar.
|
||||
#[derive(Clone, CloneRef, Copy, Debug, Default, Eq, From, Hash, Into, PartialEq)]
|
||||
pub struct Id(pub usize);
|
||||
|
||||
im_string_newtype! {
|
||||
/// A label assigned to some event displayed in a status bar.
|
||||
Label
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===============
|
||||
// === Process ===
|
||||
// ===============
|
||||
|
||||
/// Structures related to processes in a status bar.
|
||||
pub mod process {
|
||||
use crate::prelude::*;
|
||||
|
||||
/// An id of some process displayed in a status bar.
|
||||
#[derive(Clone, CloneRef, Copy, Debug, Default, Eq, From, Hash, Into, PartialEq)]
|
||||
pub struct Id(pub u64);
|
||||
|
||||
impl Id {
|
||||
/// Return the next id.
|
||||
pub fn next(self) -> Id {
|
||||
Id(self.0 + 1)
|
||||
}
|
||||
}
|
||||
|
||||
im_string_newtype! {
|
||||
/// A label assigned to some process displayed in a status bar.
|
||||
Label
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===========
|
||||
// === FRP ===
|
||||
// ===========
|
||||
|
||||
ensogl::define_endpoints! {
|
||||
Input {
|
||||
add_event (event::Label),
|
||||
add_process (process::Label),
|
||||
finish_process (process::Id),
|
||||
clear_all (),
|
||||
}
|
||||
Output {
|
||||
last_event (event::Id),
|
||||
last_process (process::Id),
|
||||
displayed_event (Option<event::Id>),
|
||||
displayed_process (Option<process::Id>),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Model ===
|
||||
// =============
|
||||
|
||||
/// An internal model of Status Bar component
|
||||
#[derive(Clone, CloneRef, Debug, display::Object)]
|
||||
struct Model {
|
||||
display_object: display::object::Instance,
|
||||
root: display::object::Instance,
|
||||
background: Rectangle,
|
||||
label: text::Text,
|
||||
events: Rc<RefCell<Vec<event::Label>>>,
|
||||
processes: Rc<RefCell<HashMap<process::Id, process::Label>>>,
|
||||
next_process_id: Rc<RefCell<process::Id>>,
|
||||
camera: Camera2d,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn new(app: &Application) -> Self {
|
||||
let scene = &app.display.default_scene;
|
||||
let display_object = display::object::Instance::new();
|
||||
let root = display::object::Instance::new();
|
||||
let background: Rectangle = default();
|
||||
let label = text::Text::new(app);
|
||||
let events = default();
|
||||
let processes = default();
|
||||
let next_process_id = Rc::new(RefCell::new(process::Id(1)));
|
||||
let camera = scene.camera();
|
||||
|
||||
scene.layers.panel.add(&background);
|
||||
scene.layers.panel_text.add(&label);
|
||||
|
||||
use theme::application::status_bar;
|
||||
let text_color_path = status_bar::text;
|
||||
let style = StyleWatch::new(&app.display.default_scene.style_sheet);
|
||||
let text_color = style.get_color(text_color_path);
|
||||
label.frp.set_property(.., text_color);
|
||||
label.frp.set_property_default(text_color);
|
||||
let style = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
|
||||
background.set_style(status_bar::background::HERE, &style);
|
||||
|
||||
Self { display_object, root, background, label, events, processes, next_process_id, camera }
|
||||
.init()
|
||||
}
|
||||
|
||||
fn init(self) -> Self {
|
||||
self.display_object.add_child(&self.root);
|
||||
self.root.add_child(&self.background);
|
||||
self.root.add_child(&self.label);
|
||||
|
||||
self.update_layout();
|
||||
self.camera_changed();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn camera_changed(&self) {
|
||||
let screen = self.camera.screen();
|
||||
let y = screen.height / 2.0 - MARGIN;
|
||||
self.root.set_y(y.round());
|
||||
}
|
||||
|
||||
fn update_layout(&self) {
|
||||
let label_width = self.label.width.value();
|
||||
self.label.set_x(-label_width / 2.0);
|
||||
self.label.set_y(-HEIGHT / 2.0 + TEXT_SIZE / 2.0);
|
||||
|
||||
let bg_width = if label_width > 0.0 { label_width + 2.0 * PADDING } else { 0.0 };
|
||||
let bg_height = HEIGHT;
|
||||
self.background.set_size(Vector2(bg_width, bg_height));
|
||||
self.background.set_x(-bg_width / 2.0);
|
||||
self.background.set_y(-HEIGHT / 2.0 - bg_height / 2.0);
|
||||
}
|
||||
|
||||
fn add_event(&self, label: &event::Label) -> event::Id {
|
||||
let mut events = self.events.borrow_mut();
|
||||
let new_id = event::Id(events.len());
|
||||
events.push(label.clone_ref());
|
||||
new_id
|
||||
}
|
||||
|
||||
fn add_process(&self, label: &process::Label) -> process::Id {
|
||||
let mut processes = self.processes.borrow_mut();
|
||||
let mut next_process_id = self.next_process_id.borrow_mut();
|
||||
let new_id = *next_process_id;
|
||||
*next_process_id = next_process_id.next();
|
||||
processes.insert(new_id, label.clone_ref());
|
||||
new_id
|
||||
}
|
||||
|
||||
/// Returns true if there was process with given id.
|
||||
fn finish_process(&self, id: process::Id) -> bool {
|
||||
self.processes.borrow_mut().remove(&id).is_some()
|
||||
}
|
||||
|
||||
/// Returns empty string if no event received so far.
|
||||
fn last_event_message(&self) -> event::Label {
|
||||
self.events.borrow().last().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn clear_all(&self) {
|
||||
self.events.borrow_mut().clear();
|
||||
self.processes.borrow_mut().clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============
|
||||
// === View ===
|
||||
// ============
|
||||
|
||||
/// The StatusBar component view.
|
||||
///
|
||||
/// The status bar gathers information about events and processes occurring in the Application.
|
||||
// TODO: This is a stub. Extend it when doing https://github.com/enso-org/ide/issues/1193
|
||||
#[derive(Clone, CloneRef, Debug, display::Object)]
|
||||
pub struct View {
|
||||
frp: Frp,
|
||||
#[display_object]
|
||||
model: Model,
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// Create new StatusBar view.
|
||||
pub fn new(app: &Application) -> Self {
|
||||
let frp = Frp::new();
|
||||
let model = Model::new(app);
|
||||
let network = &frp.network;
|
||||
let scene = &app.display.default_scene;
|
||||
|
||||
enso_frp::extend! { network
|
||||
event_added <- frp.add_event.map(f!((label) model.add_event(label)));
|
||||
process_added <- frp.add_process.map(f!((label) model.add_process(label)));
|
||||
_process_finished <- frp.finish_process.filter_map(f!((id)
|
||||
model.finish_process(*id).as_some(*id)
|
||||
));
|
||||
displayed_process_finished <- frp.finish_process
|
||||
.map2(&frp.output.displayed_process, |fin,dis|(*fin,*dis))
|
||||
.filter(|(fin,dis)| dis.contains(fin));
|
||||
|
||||
label_after_adding_event <- frp.add_event.map(
|
||||
|label| AsRef::<ImString>::as_ref(label).clone_ref()
|
||||
);
|
||||
label_after_adding_process <- frp.add_process.map(
|
||||
|label| AsRef::<ImString>::as_ref(label).clone_ref()
|
||||
);
|
||||
label_after_finishing_process <- displayed_process_finished.map(
|
||||
f_!([model] AsRef::<ImString>::as_ref(&model.last_event_message()).clone_ref())
|
||||
);
|
||||
|
||||
label <- any(label_after_adding_event,label_after_adding_process,label_after_finishing_process);
|
||||
eval label ((label) model.label.set_content(label.to_string()));
|
||||
|
||||
eval_ frp.clear_all (model.clear_all());
|
||||
|
||||
frp.source.last_event <+ event_added;
|
||||
frp.source.last_process <+ process_added;
|
||||
|
||||
frp.source.displayed_event <+ event_added.map(|id| Some(*id));
|
||||
frp.source.displayed_event <+ process_added.constant(None);
|
||||
frp.source.displayed_event <+ frp.clear_all.constant(None);
|
||||
frp.source.displayed_process <+ process_added.map(|id| Some(*id));
|
||||
frp.source.displayed_process <+ event_added.constant(None);
|
||||
frp.source.displayed_process <+ displayed_process_finished.constant(None);
|
||||
frp.source.displayed_process <+ frp.clear_all.constant(None);
|
||||
|
||||
eval_ model.label.output.width (model.update_layout());
|
||||
eval_ scene.frp.camera_changed (model.camera_changed());
|
||||
}
|
||||
|
||||
Self { frp, model }
|
||||
}
|
||||
|
||||
/// Returns a future will add a new process to the status bar, evaluate `f` and then mark the
|
||||
/// process as finished.
|
||||
pub fn process<'f, F>(
|
||||
&self,
|
||||
label: process::Label,
|
||||
f: F,
|
||||
) -> impl Future<Output = F::Output> + 'f
|
||||
where
|
||||
F: Future + 'f,
|
||||
{
|
||||
let add_process = self.frp.add_process.clone_ref();
|
||||
let last_process = self.frp.last_process.clone_ref();
|
||||
let finish_process = self.frp.finish_process.clone_ref();
|
||||
async move {
|
||||
add_process.emit(label);
|
||||
let id = last_process.value();
|
||||
let result = f.await;
|
||||
finish_process.emit(id);
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for View {
|
||||
type Target = Frp;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.frp
|
||||
}
|
||||
}
|
@ -132,7 +132,14 @@ export default function App(props: AppProps) {
|
||||
* will redirect the user between the login/register pages and the dashboard. */
|
||||
return (
|
||||
<>
|
||||
<toastify.ToastContainer position="top-center" theme="light" closeOnClick={false} />
|
||||
<toastify.ToastContainer
|
||||
position="top-center"
|
||||
theme="light"
|
||||
closeOnClick={false}
|
||||
draggable={false}
|
||||
toastClassName="text-sm leading-170 bg-frame-selected rounded-2xl backdrop-blur-3xl"
|
||||
transition={toastify.Zoom}
|
||||
/>
|
||||
<Router basename={getMainPageUrl().pathname}>
|
||||
<AppRouter {...props} />
|
||||
</Router>
|
||||
|
@ -8,6 +8,10 @@ body {
|
||||
background-blend-mode: lighten;
|
||||
}
|
||||
|
||||
.Toastify--animate {
|
||||
animation-duration: 0.2s;
|
||||
}
|
||||
|
||||
/* These styles MUST still be copied
|
||||
* as `.enso-dashboard body` and `.enso-dashboard html` make no sense. */
|
||||
.enso-dashboard,
|
||||
|
@ -42,43 +42,6 @@ async fn create_new_project_and_add_nodes() {
|
||||
assert_eq!(added_node.view.set_expression.value().code, "");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn debug_mode() {
|
||||
let test = Fixture::setup_new_project().await;
|
||||
let project = test.project_view();
|
||||
let graph_editor = test.graph_editor();
|
||||
|
||||
assert!(!graph_editor.debug_mode.value());
|
||||
|
||||
// Turning On
|
||||
let expect_mode = project.debug_mode.next_event();
|
||||
let expect_popup_message = project.debug_mode_popup().content_frp_node().next_event();
|
||||
project.enable_debug_mode();
|
||||
assert!(expect_mode.expect());
|
||||
let message = expect_popup_message.expect();
|
||||
assert!(
|
||||
message.contains("Debug Mode enabled"),
|
||||
"Message \"{message}\" does not mention enabling Debug mode"
|
||||
);
|
||||
assert!(
|
||||
message.contains(enso_gui::view::debug_mode_popup::DEBUG_MODE_SHORTCUT),
|
||||
"Message \"{message}\" does not inform about shortcut to turn mode off"
|
||||
);
|
||||
assert!(graph_editor.debug_mode.value());
|
||||
|
||||
// Turning Off
|
||||
let expect_mode = project.debug_mode.next_event();
|
||||
let expect_popup_message = project.debug_mode_popup().content_frp_node().next_event();
|
||||
project.disable_debug_mode();
|
||||
assert!(!expect_mode.expect());
|
||||
let message = expect_popup_message.expect();
|
||||
assert!(
|
||||
message.contains("Debug Mode disabled"),
|
||||
"Message \"{message}\" does not mention disabling of debug mode"
|
||||
);
|
||||
assert!(!graph_editor.debug_mode.value());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn zooming() {
|
||||
let test = Fixture::setup_new_project().await;
|
||||
|
@ -475,14 +475,6 @@ define_themes! { [light:0, dark:1]
|
||||
}
|
||||
}
|
||||
}
|
||||
status_bar {
|
||||
offset_y = -30.0, -30.0;
|
||||
text = text, text;
|
||||
background {
|
||||
color = graph_editor::node::background , graph_editor::node::background;
|
||||
corner_radius = 14.0 , 14.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
code {
|
||||
types {
|
||||
|
Loading…
Reference in New Issue
Block a user