mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 13:02:07 +03:00
Visual indication of outdated VCS snapshot (#3950)
This PR provides a visual indication of whether the project's current state differs from the most recent snapshot saved in the VCS. The project name displayed in the IDE changes to a darker text to indicate that the VCS snapshot is outdated, and back to a lighter text when the current project state corresponds to the last saved VCS snapshot. https://user-images.githubusercontent.com/117099775/208088438-20dfc2aa-2a7d-47bf-bc12-3d3dff7a4974.mp4 The outdated project snapshot indicator is set when: * A node is moved. * A node is added or removed. * The text editor is used to edit the text. * The project is auto-saved, and the auto-saved project state does not correspond to the last saved snapshot in the VCS. The outdated project snapshot indicator is cleared when: * A new project snapshot is successfully saved using `ctrl+s`. * The project is auto-saved, and the auto-saved project state is confirmed to correspond to the last saved snapshot in the VCS. This occurs, for example, when a project change is undone and the project is reverted to the last saved snapshot state. The auto-save events do not occur immediately after a project change but have a short delay, thus the VCS status update is affected by the same delay when triggered by an auto-save event.
This commit is contained in:
parent
49204e92cf
commit
4b28f8f8f0
@ -74,6 +74,10 @@
|
||||
- [Added scroll bounce animation][3836] which activates when scrolling past the
|
||||
end of scrollable content.
|
||||
- [Added project snapshot saving on shortcut][3923]
|
||||
- [The color of the displayed project name indicates whether the project's
|
||||
current state is saved in a snapshot.][3950] The project name is darker when
|
||||
the project is changed from the last saved snapshot and lighter when the
|
||||
snapshot matches the current project state.
|
||||
- [Added shortcut to interrupt the program][3967]
|
||||
|
||||
#### EnsoGL (rendering engine)
|
||||
@ -405,6 +409,7 @@
|
||||
[3885]: https://github.com/enso-org/enso/pull/3885
|
||||
[3919]: https://github.com/enso-org/enso/pull/3919
|
||||
[3923]: https://github.com/enso-org/enso/pull/3923
|
||||
[3950]: https://github.com/enso-org/enso/pull/3950
|
||||
[3964]: https://github.com/enso-org/enso/pull/3964
|
||||
[3967]: https://github.com/enso-org/enso/pull/3967
|
||||
|
||||
|
@ -27,4 +27,7 @@ pub mod code {
|
||||
|
||||
/// Signals that requested file doesn’t exist.
|
||||
pub const FILE_NOT_FOUND: i64 = 1003;
|
||||
|
||||
/// Signals that requested project is already under version control.
|
||||
pub const VCS_ALREADY_EXISTS: i64 = 1005;
|
||||
}
|
||||
|
@ -194,6 +194,10 @@ trait API {
|
||||
/// Return a list of all project states that are saved to the VCS.
|
||||
#[MethodInput=VcsListInput, rpc_name="vcs/list"]
|
||||
fn list_vcs(&self, root: Path, limit: Option<usize>) -> response::ListVcs;
|
||||
|
||||
/// Returns the current status of the changes made to the project.
|
||||
#[MethodInput=VcsStatusInput, rpc_name="vcs/status"]
|
||||
fn vcs_status(&self, root: Path) -> response::VcsStatus;
|
||||
}}
|
||||
|
||||
|
||||
@ -203,7 +207,7 @@ trait API {
|
||||
// ==============
|
||||
|
||||
/// Check if the given `Error` value corresponds to an RPC call timeout.
|
||||
///
|
||||
///
|
||||
/// Recognizes both client- and server-side timeouts.
|
||||
#[rustfmt::skip]
|
||||
pub fn is_timeout_error(error: &failure::Error) -> bool {
|
||||
|
@ -108,7 +108,7 @@ pub struct GetComponentGroups {
|
||||
}
|
||||
|
||||
/// Response of `save_vcs` method.
|
||||
#[derive(Hash, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Hash, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct SaveVcs {
|
||||
@ -117,9 +117,19 @@ pub struct SaveVcs {
|
||||
}
|
||||
|
||||
/// Response of `list_vcs` method.
|
||||
#[derive(Hash, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Hash, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct ListVcs {
|
||||
pub saves: Vec<SaveVcs>,
|
||||
}
|
||||
|
||||
/// Response of `vcs_status` method.
|
||||
#[derive(Hash, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct VcsStatus {
|
||||
pub dirty: bool,
|
||||
pub changed: Vec<Path>,
|
||||
pub last_save: SaveVcs,
|
||||
}
|
||||
|
@ -8,12 +8,10 @@ use crate::controller::ide::StatusNotificationPublisher;
|
||||
use double_representation::import;
|
||||
use double_representation::name::project;
|
||||
use double_representation::name::QualifiedName;
|
||||
use engine_protocol::common::error::code;
|
||||
use engine_protocol::language_server::MethodPointer;
|
||||
use engine_protocol::language_server::Path;
|
||||
use enso_frp::web::platform;
|
||||
use enso_frp::web::platform::Platform;
|
||||
use json_rpc::error::RpcError;
|
||||
use parser_scala::Parser;
|
||||
|
||||
|
||||
@ -245,18 +243,7 @@ impl Project {
|
||||
let root_path = Path::new(project_root_id, &path_segments);
|
||||
let language_server = self.model.json_rpc();
|
||||
async move {
|
||||
let response = language_server.save_vcs(&root_path, &None).await;
|
||||
if let Err(RpcError::RemoteError(json_rpc::messages::Error {
|
||||
code: error_code, ..
|
||||
})) = response
|
||||
{
|
||||
if error_code == code::FILE_NOT_FOUND {
|
||||
language_server.init_vcs(&root_path).await?;
|
||||
language_server.save_vcs(&root_path, &None).await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
response?;
|
||||
language_server.save_vcs(&root_path, &None).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -322,65 +309,29 @@ mod tests {
|
||||
|
||||
// === Project Snapshotting ===
|
||||
|
||||
/// Structure that keeps track of whether the VCS is initialized and how many commits are made.
|
||||
/// Structure that keeps track of how many commits are made.
|
||||
struct VcsMockState {
|
||||
init: Cell<bool>,
|
||||
commit_count: Cell<usize>,
|
||||
}
|
||||
|
||||
/// Mock project that updates a VcsMockState on relevant language server API calls.
|
||||
fn setup_mock_project(vcs: Rc<VcsMockState>) -> model::Project {
|
||||
let json_client = language_server::MockClient::default();
|
||||
let project_root_id = crate::test::mock::data::ROOT_ID;
|
||||
let path_segments: [&str; 0] = [];
|
||||
let root_path = Path::new(project_root_id, &path_segments);
|
||||
let vcs_entry = language_server::response::SaveVcs {
|
||||
commit_id: "commit_id".into(),
|
||||
message: "message".into(),
|
||||
};
|
||||
let vcs_err = RpcError::RemoteError(json_rpc::messages::Error {
|
||||
code: code::FILE_NOT_FOUND,
|
||||
message: "VCS is not initialized.".into(),
|
||||
data: None,
|
||||
});
|
||||
let root_path = Path::new(Uuid::default(), &path_segments);
|
||||
|
||||
let vcs_clone = vcs.clone();
|
||||
let root_path_clone = root_path.clone();
|
||||
json_client.expect.init_vcs(move |path| {
|
||||
assert_eq!(path, &root_path_clone);
|
||||
assert!(!vcs_clone.init.get(), "VCS is already initialized.");
|
||||
vcs_clone.init.set(true);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let vcs_clone = vcs.clone();
|
||||
let root_path_clone = root_path.clone();
|
||||
let vcs_entry_clone = vcs_entry.clone();
|
||||
json_client.expect.save_vcs(move |path, name| {
|
||||
assert_eq!(path, &root_path_clone);
|
||||
assert_eq!(name, &None);
|
||||
if vcs_clone.init.get() {
|
||||
let count = vcs_clone.commit_count.get();
|
||||
vcs_clone.commit_count.set(count + 1);
|
||||
Ok(vcs_entry_clone)
|
||||
} else {
|
||||
Err(vcs_err)
|
||||
}
|
||||
});
|
||||
|
||||
// Expect a 2nd call to `write_vcs` if the first failed because the VCS was not initialized.
|
||||
let vcs_entry = language_server::response::SaveVcs::default();
|
||||
json_client.expect.save_vcs(move |path, name| {
|
||||
assert_eq!(path, &root_path);
|
||||
assert_eq!(name, &None);
|
||||
assert!(vcs.init.get(), "VCS is not initialized.");
|
||||
let count = vcs.commit_count.get();
|
||||
vcs.commit_count.set(count + 1);
|
||||
Ok(vcs_entry)
|
||||
});
|
||||
|
||||
let ls = engine_protocol::language_server::Connection::new_mock_rc(json_client);
|
||||
let ls = language_server::Connection::new_mock_rc(json_client);
|
||||
let mut project = model::project::MockAPI::new();
|
||||
model::project::test::expect_root_id(&mut project, project_root_id);
|
||||
model::project::test::expect_root_id(&mut project, Uuid::default());
|
||||
project.expect_json_rpc().returning_st(move || ls.clone_ref());
|
||||
Rc::new(project)
|
||||
}
|
||||
@ -388,30 +339,12 @@ mod tests {
|
||||
#[wasm_bindgen_test]
|
||||
fn save_project_snapshot() {
|
||||
TestWithLocalPoolExecutor::set_up().run_task(async move {
|
||||
let vcs =
|
||||
Rc::new(VcsMockState { init: Cell::new(true), commit_count: Cell::new(2) });
|
||||
let vcs = Rc::new(VcsMockState { commit_count: Cell::new(2) });
|
||||
let project = setup_mock_project(vcs.clone());
|
||||
let project_controller = controller::Project::new(project, default());
|
||||
let result = project_controller.save_project_snapshot().await;
|
||||
assert_matches!(result, Ok(()));
|
||||
assert!(vcs.init.get(), "VCS is not initialized.");
|
||||
assert_eq!(vcs.commit_count.get(), 3);
|
||||
});
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn save_project_snapshot_vcs_not_init() {
|
||||
TestWithLocalPoolExecutor::set_up().run_task(async move {
|
||||
let vcs = Rc::new(VcsMockState {
|
||||
init: Cell::new(false),
|
||||
commit_count: Cell::new(0),
|
||||
});
|
||||
let project = setup_mock_project(vcs.clone());
|
||||
let project_controller = controller::Project::new(project, default());
|
||||
let result = project_controller.save_project_snapshot().await;
|
||||
assert_matches!(result, Ok(()));
|
||||
assert!(vcs.init.get(), "VCS is not initialized.");
|
||||
assert_eq!(vcs.commit_count.get(), 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +168,8 @@ pub type Synchronized = synchronized::Project;
|
||||
pub enum Notification {
|
||||
/// One of the backend connections has been lost.
|
||||
ConnectionLost(BackendConnection),
|
||||
/// Indicates that the project VCS status has changed.
|
||||
VcsStatusChanged(VcsStatus),
|
||||
}
|
||||
|
||||
/// Denotes one of backend connections used by a project.
|
||||
@ -179,6 +181,16 @@ pub enum BackendConnection {
|
||||
LanguageServerBinary,
|
||||
}
|
||||
|
||||
/// The VCS status indicates whether the project has been modified compared the most recent VCS
|
||||
/// snapshot.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum VcsStatus {
|
||||
/// The project has been modified since the last VCS snapshot.
|
||||
Dirty,
|
||||
/// The project is in the same state as the last VCS snapshot.
|
||||
Clean,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============
|
||||
|
@ -14,7 +14,9 @@ use crate::transport::web::WebSocket;
|
||||
use double_representation::name::project;
|
||||
use engine_protocol::binary;
|
||||
use engine_protocol::binary::message::VisualisationContext;
|
||||
use engine_protocol::common::error::code;
|
||||
use engine_protocol::language_server;
|
||||
use engine_protocol::language_server::response;
|
||||
use engine_protocol::language_server::CapabilityRegistration;
|
||||
use engine_protocol::language_server::ContentRoot;
|
||||
use engine_protocol::language_server::ExpressionUpdates;
|
||||
@ -23,6 +25,7 @@ use engine_protocol::project_manager;
|
||||
use engine_protocol::project_manager::MissingComponentAction;
|
||||
use engine_protocol::project_manager::ProjectName;
|
||||
use flo_stream::Subscriber;
|
||||
use json_rpc::error::RpcError;
|
||||
use parser_scala::Parser;
|
||||
|
||||
|
||||
@ -167,6 +170,32 @@ impl ContentRoots {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ========================
|
||||
// === VCS status check ===
|
||||
// ========================
|
||||
|
||||
/// Check whether the current state of the project differs from the most recent snapshot in the VCS,
|
||||
/// and emit a notification.
|
||||
#[profile(Detail)]
|
||||
async fn check_vcs_status_and_notify(
|
||||
project_root_id: Uuid,
|
||||
language_server: Rc<language_server::Connection>,
|
||||
publisher: notification::Publisher<model::project::Notification>,
|
||||
) -> json_rpc::Result<response::VcsStatus> {
|
||||
let path_segments: [&str; 0] = [];
|
||||
let root_path = language_server::Path::new(project_root_id, &path_segments);
|
||||
let status = language_server.vcs_status(&root_path).await;
|
||||
let notify_status = match &status {
|
||||
Ok(response::VcsStatus { dirty: true, .. }) | Err(_) => model::project::VcsStatus::Dirty,
|
||||
Ok(response::VcsStatus { dirty: false, .. }) => model::project::VcsStatus::Clean,
|
||||
};
|
||||
publisher.notify(model::project::Notification::VcsStatusChanged(notify_status));
|
||||
status
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Model ===
|
||||
// =============
|
||||
@ -306,6 +335,7 @@ impl Project {
|
||||
let json_rpc_handler = ret.json_event_handler();
|
||||
crate::executor::global::spawn(json_rpc_events.for_each(json_rpc_handler));
|
||||
|
||||
ret.initialize_vcs().await.map_err(|err| wrap(err.into()))?;
|
||||
ret.acquire_suggestion_db_updates_capability().await.map_err(|err| wrap(err.into()))?;
|
||||
Ok(ret)
|
||||
}
|
||||
@ -448,6 +478,8 @@ impl Project {
|
||||
// This generalization should be reconsidered once the old JSON-RPC handler is phased out.
|
||||
// See: https://github.com/enso-org/ide/issues/587
|
||||
let publisher = self.notifications.clone_ref();
|
||||
let project_root_id = self.project_content_root_id();
|
||||
let language_server = self.json_rpc().clone_ref();
|
||||
let weak_suggestion_db = Rc::downgrade(&self.suggestion_db);
|
||||
let weak_content_roots = Rc::downgrade(&self.content_roots);
|
||||
let execution_update_handler = self.execution_update_handler();
|
||||
@ -465,7 +497,20 @@ impl Project {
|
||||
// Event Handling
|
||||
match event {
|
||||
Event::Notification(Notification::FileEvent(_)) => {}
|
||||
Event::Notification(Notification::TextAutoSave(_)) => {}
|
||||
Event::Notification(Notification::TextAutoSave(_)) => {
|
||||
let publisher = publisher.clone_ref();
|
||||
let language_server = language_server.clone_ref();
|
||||
executor::global::spawn(async move {
|
||||
let status = check_vcs_status_and_notify(
|
||||
project_root_id,
|
||||
language_server,
|
||||
publisher,
|
||||
);
|
||||
if let Err(err) = status.await {
|
||||
error!("Error while checking project VCS status: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
Event::Notification(Notification::ExpressionUpdates(updates)) => {
|
||||
let ExpressionUpdates { context_id, updates } = updates;
|
||||
let execution_update = ExecutionUpdate::ExpressionUpdates(updates);
|
||||
@ -533,6 +578,25 @@ impl Project {
|
||||
.acquire_capability(&capability.method, &capability.register_options)
|
||||
}
|
||||
|
||||
/// Initialize the VCS if it was not already initialized.
|
||||
#[profile(Detail)]
|
||||
async fn initialize_vcs(&self) -> json_rpc::Result<()> {
|
||||
let project_root_id = self.project_content_root_id();
|
||||
let path_segments: [&str; 0] = [];
|
||||
let root_path = language_server::Path::new(project_root_id, &path_segments);
|
||||
let language_server = self.json_rpc().clone_ref();
|
||||
let response = language_server.init_vcs(&root_path).await;
|
||||
if let Err(RpcError::RemoteError(json_rpc::messages::Error {
|
||||
code: code::VCS_ALREADY_EXISTS,
|
||||
..
|
||||
})) = response
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
#[profile(Task)]
|
||||
fn load_module(
|
||||
&self,
|
||||
@ -663,6 +727,7 @@ mod test {
|
||||
use engine_protocol::types::Sha3_224;
|
||||
use futures::SinkExt;
|
||||
use json_rpc::expect_call;
|
||||
use std::assert_matches::assert_matches;
|
||||
|
||||
|
||||
#[allow(unused)]
|
||||
@ -687,16 +752,17 @@ mod test {
|
||||
binary_client.expect_event_stream().return_once(|| binary_events.boxed_local());
|
||||
let json_events_sender = json_client.setup_events();
|
||||
|
||||
let initial_suggestions_db = language_server::response::GetSuggestionDatabase {
|
||||
entries: vec![],
|
||||
current_version: 0,
|
||||
};
|
||||
let initial_suggestions_db =
|
||||
response::GetSuggestionDatabase { entries: vec![], current_version: 0 };
|
||||
expect_call!(json_client.get_suggestions_database() => Ok(initial_suggestions_db));
|
||||
let capability_reg =
|
||||
CapabilityRegistration::create_receives_suggestions_database_updates();
|
||||
let method = capability_reg.method;
|
||||
let options = capability_reg.register_options;
|
||||
expect_call!(json_client.acquire_capability(method,options) => Ok(()));
|
||||
let path_segments: [&str; 0] = [];
|
||||
let root_path = language_server::Path::new(Uuid::default(), &path_segments);
|
||||
expect_call!(json_client.init_vcs(root_path) => Ok(()));
|
||||
|
||||
setup_mock_json(&mut json_client);
|
||||
setup_mock_binary(&mut binary_client);
|
||||
@ -838,4 +904,51 @@ mod test {
|
||||
assert_eq!(value_info.typename, value_update.typename.clone().map(ImString::new));
|
||||
assert_eq!(value_info.method_call, value_update.method_pointer);
|
||||
}
|
||||
|
||||
|
||||
// === VCS status check ===
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn check_project_vcs_status() {
|
||||
TestWithLocalPoolExecutor::set_up().run_task(async move {
|
||||
let json_client = language_server::MockClient::default();
|
||||
let path_segments: [&str; 0] = [];
|
||||
let root_path = language_server::Path::new(Uuid::default(), &path_segments);
|
||||
|
||||
let vcs_status = response::VcsStatus::default();
|
||||
let root_path_clone = root_path.clone();
|
||||
expect_call!(json_client.vcs_status(root_path_clone) => Ok(vcs_status));
|
||||
|
||||
let vcs_status = response::VcsStatus { dirty: true, ..Default::default() };
|
||||
expect_call!(json_client.vcs_status(root_path) => Ok(vcs_status));
|
||||
|
||||
let ls = language_server::Connection::new_mock_rc(json_client);
|
||||
let publisher = notification::Publisher::default();
|
||||
let mut subscriber = publisher.subscribe();
|
||||
|
||||
let result =
|
||||
check_vcs_status_and_notify(Uuid::default(), ls.clone_ref(), publisher.clone_ref())
|
||||
.await;
|
||||
let message = subscriber.next().await;
|
||||
assert_matches!(result, Ok(response::VcsStatus { dirty: false, .. }));
|
||||
assert_matches!(
|
||||
message,
|
||||
Some(model::project::Notification::VcsStatusChanged(
|
||||
model::project::VcsStatus::Clean
|
||||
))
|
||||
);
|
||||
|
||||
let result =
|
||||
check_vcs_status_and_notify(Uuid::default(), ls.clone_ref(), publisher.clone_ref())
|
||||
.await;
|
||||
let message = subscriber.next().await;
|
||||
assert_matches!(result, Ok(response::VcsStatus { dirty: true, .. }));
|
||||
assert_matches!(
|
||||
message,
|
||||
Some(model::project::Notification::VcsStatusChanged(
|
||||
model::project::VcsStatus::Dirty
|
||||
))
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,9 @@ use crate::presenter::graph::ViewNodeId;
|
||||
use enso_frp as frp;
|
||||
use ide_view as view;
|
||||
use ide_view::project::SearcherParams;
|
||||
use model::module::NotificationKind;
|
||||
use model::project::Notification;
|
||||
use model::project::VcsStatus;
|
||||
|
||||
|
||||
|
||||
@ -171,13 +174,20 @@ impl Model {
|
||||
|
||||
fn save_project_snapshot(&self) {
|
||||
let controller = self.controller.clone_ref();
|
||||
let breadcrumbs = self.view.graph().model.breadcrumbs.clone_ref();
|
||||
executor::global::spawn(async move {
|
||||
if let Err(err) = controller.save_project_snapshot().await {
|
||||
error!("Error while saving module: {err}");
|
||||
error!("Error while saving project snapshot: {err}");
|
||||
} else {
|
||||
breadcrumbs.set_project_changed(false);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn set_project_changed(&self, changed: bool) {
|
||||
self.view.graph().model.breadcrumbs.set_project_changed(changed);
|
||||
}
|
||||
|
||||
fn execution_context_interrupt(&self) {
|
||||
let controller = self.graph_controller.clone_ref();
|
||||
executor::global::spawn(async move {
|
||||
@ -301,14 +311,32 @@ impl Project {
|
||||
let weak = Rc::downgrade(&self.model);
|
||||
spawn_stream_handler(weak, notifications, |notification, model| {
|
||||
info!("Processing notification {notification:?}");
|
||||
let message = match notification {
|
||||
model::project::Notification::ConnectionLost(_) =>
|
||||
crate::BACKEND_DISCONNECTED_MESSAGE,
|
||||
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);
|
||||
}
|
||||
Notification::VcsStatusChanged(VcsStatus::Dirty) => {
|
||||
model.set_project_changed(true);
|
||||
}
|
||||
Notification::VcsStatusChanged(VcsStatus::Clean) => {
|
||||
model.set_project_changed(false);
|
||||
}
|
||||
};
|
||||
let message = view::status_bar::event::Label::from(message);
|
||||
model.status_bar.add_event(message);
|
||||
std::future::ready(())
|
||||
});
|
||||
|
||||
let notifications = self.model.module_model.subscribe();
|
||||
let weak = Rc::downgrade(&self.model);
|
||||
spawn_stream_handler(weak, notifications, move |notification, model| {
|
||||
match notification.kind {
|
||||
NotificationKind::Invalidate
|
||||
| NotificationKind::CodeChanged { .. }
|
||||
| NotificationKind::MetadataChanged => model.set_project_changed(true),
|
||||
}
|
||||
futures::future::ready(())
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -135,6 +135,8 @@ ensogl::define_endpoints! {
|
||||
/// The `gap_width` describes an empty space on the left of all the content. This space will
|
||||
/// be covered by the background and is intended to make room for windows control buttons.
|
||||
gap_width (f32),
|
||||
/// Set whether the project was changed since the last snapshot save.
|
||||
set_project_changed(bool),
|
||||
}
|
||||
Output {
|
||||
/// Signalizes when a new breadcrumb is pushed.
|
||||
@ -517,6 +519,7 @@ impl Breadcrumbs {
|
||||
frp.source.project_name_hovered <+ model.project_name.is_hovered;
|
||||
frp.source.project_mouse_down <+ model.project_name.mouse_down;
|
||||
|
||||
eval frp.input.set_project_changed((v) model.project_name.set_project_changed(v));
|
||||
|
||||
// === User Interaction ===
|
||||
|
||||
|
@ -18,7 +18,7 @@ use ensogl::gui::cursor;
|
||||
use ensogl::DEPRECATED_Animation;
|
||||
use ensogl_component::text;
|
||||
use ensogl_component::text::formatting::Size as TextSize;
|
||||
use ensogl_hardcoded_theme as theme;
|
||||
use ensogl_hardcoded_theme::graph_editor::breadcrumbs as breadcrumbs_theme;
|
||||
|
||||
|
||||
|
||||
@ -75,6 +75,8 @@ ensogl::define_endpoints! {
|
||||
/// Indicates the IDE is in edit mode. This means a click on some editable text should
|
||||
/// start editing it.
|
||||
ide_text_edit_mode (bool),
|
||||
/// Set whether the project was changed since the last snapshot save.
|
||||
set_project_changed(bool),
|
||||
}
|
||||
|
||||
Output {
|
||||
@ -136,7 +138,7 @@ impl ProjectNameModel {
|
||||
// FIXME : StyleWatch is unsuitable here, as it was designed as an internal tool for shape
|
||||
// system (#795)
|
||||
let style = StyleWatch::new(&scene.style_sheet);
|
||||
let base_color = style.get_color(theme::graph_editor::breadcrumbs::transparent);
|
||||
let base_color = style.get_color(breadcrumbs_theme::transparent);
|
||||
let text_size: TextSize = TEXT_SIZE.into();
|
||||
let text_field = app.new_view::<text::Text>();
|
||||
text_field.set_property_default(base_color);
|
||||
@ -250,9 +252,13 @@ impl ProjectName {
|
||||
// FIXME : StyleWatch is unsuitable here, as it was designed as an internal tool for shape
|
||||
// system (#795)
|
||||
let styles = StyleWatch::new(&scene.style_sheet);
|
||||
let hover_color = styles.get_color(theme::graph_editor::breadcrumbs::hover);
|
||||
let deselected_color = styles.get_color(theme::graph_editor::breadcrumbs::deselected::left);
|
||||
let selected_color = styles.get_color(theme::graph_editor::breadcrumbs::selected);
|
||||
let saved_hover_color = styles.get_color(breadcrumbs_theme::hover);
|
||||
let saved_deselected_color = styles.get_color(breadcrumbs_theme::deselected::left);
|
||||
let saved_selected_color = styles.get_color(breadcrumbs_theme::selected);
|
||||
let unsaved_hover_color = styles.get_color(breadcrumbs_theme::unsaved::hover);
|
||||
let unsaved_deselected_color =
|
||||
styles.get_color(breadcrumbs_theme::unsaved::deselected::left);
|
||||
let unsaved_selected_color = styles.get_color(breadcrumbs_theme::unsaved::selected);
|
||||
let animations = Animations::new(network);
|
||||
|
||||
frp::extend! { network
|
||||
@ -264,16 +270,19 @@ impl ProjectName {
|
||||
&model.view.events.mouse_over);
|
||||
frp.source.mouse_down <+ model.view.events.mouse_down_primary;
|
||||
|
||||
not_selected <- frp.output.selected.map(|selected| !selected);
|
||||
mouse_over_if_not_selected <- model.view.events.mouse_over.gate(¬_selected);
|
||||
mouse_out_if_not_selected <- model.view.events.mouse_out.gate(¬_selected);
|
||||
eval_ mouse_over_if_not_selected(
|
||||
animations.color.set_target_value(hover_color);
|
||||
);
|
||||
eval_ mouse_out_if_not_selected(
|
||||
animations.color.set_target_value(deselected_color);
|
||||
);
|
||||
on_deselect <- not_selected.gate(¬_selected).constant(());
|
||||
text_color <- all3(
|
||||
&frp.output.selected,
|
||||
&frp.source.is_hovered,
|
||||
&frp.input.set_project_changed,
|
||||
).map(move |(selected, hovered, changed)| match (*selected, *hovered, *changed) {
|
||||
(true, _, true) => unsaved_selected_color,
|
||||
(true, _, false) => saved_selected_color,
|
||||
(false, false, true) => unsaved_deselected_color,
|
||||
(false, false, false) => saved_deselected_color,
|
||||
(false, true, true) => unsaved_hover_color,
|
||||
(false, true, false) => saved_hover_color,
|
||||
});
|
||||
eval text_color((&color) animations.color.set_target_value(color));
|
||||
|
||||
edit_click <- mouse_down.gate(&frp.ide_text_edit_mode);
|
||||
start_editing <- any(edit_click,frp.input.start_editing);
|
||||
@ -307,19 +316,18 @@ impl ProjectName {
|
||||
eval commit_text((text) model.commit(text));
|
||||
on_commit <- commit_text.constant(());
|
||||
|
||||
not_selected <- frp.output.selected.map(|selected| !selected);
|
||||
on_deselect <- not_selected.gate(¬_selected).constant(());
|
||||
frp.output.source.edit_mode <+ on_deselect.to_false();
|
||||
|
||||
|
||||
// === Selection ===
|
||||
|
||||
eval_ frp.select( animations.color.set_target_value(selected_color) );
|
||||
frp.output.source.selected <+ frp.select.to_true();
|
||||
|
||||
set_inactive <- any(&frp.deselect,&on_commit);
|
||||
eval_ set_inactive ([text,animations]{
|
||||
eval_ set_inactive ([text] {
|
||||
text.deprecated_set_focus(false);
|
||||
text.remove_all_cursors();
|
||||
animations.color.set_target_value(deselected_color);
|
||||
});
|
||||
frp.output.source.selected <+ set_inactive.to_false();
|
||||
|
||||
|
@ -587,11 +587,19 @@ define_themes! { [light:0, dark:1]
|
||||
full = Lcha(0.0,0.0,0.0,0.7) , Lcha(1.0,0.0,0.0,0.7);
|
||||
transparent = Lcha(0.0,0.0,0.0,0.4) , Lcha(1.0,0.0,0.0,0.4);
|
||||
selected = Lcha(0.0,0.0,0.0,0.7) , Lcha(1.0,0.0,0.0,0.7);
|
||||
hover = Lcha(0.0,0.0,0.0,0.7) , Lcha(1.0,0.0,0.0,0.7);
|
||||
hover = Lcha(0.0,0.0,0.0,0.6) , Lcha(1.0,0.0,0.0,0.6);
|
||||
deselected {
|
||||
left = Lcha(0.0,0.0,0.0,0.5) , Lcha(1.0,0.0,0.0,0.5);
|
||||
left = Lcha(0.0,0.0,0.0,0.4) , Lcha(1.0,0.0,0.0,0.4);
|
||||
right = Lcha(0.0,0.0,0.0,0.2) , Lcha(1.0,0.0,0.0,0.2);
|
||||
}
|
||||
unsaved {
|
||||
selected = Lcha(0.0,0.0,0.0,1.0) , Lcha(1.0,0.0,0.0,1.0);
|
||||
hover = Lcha(0.0,0.0,0.0,1.0) , Lcha(1.0,0.0,0.0,1.0);
|
||||
deselected {
|
||||
left = Lcha(0.0,0.0,0.0,0.8) , Lcha(1.0,0.0,0.0,0.8);
|
||||
right = Lcha(0.0,0.0,0.0,0.6) , Lcha(1.0,0.0,0.0,0.6);
|
||||
}
|
||||
}
|
||||
background = application::background , application::background;
|
||||
background {
|
||||
corner_radius = 8.0 , 8.0;
|
||||
|
Loading…
Reference in New Issue
Block a user