Skip to content

Commit

Permalink
Replace old breadcrumb implementation in the top bar with new breadcr…
Browse files Browse the repository at this point in the history
…umb component. (#7619)

Implements #7312:  Replace the legacy code used for the project breadcrumbs and update them to the new design.

![Peek 2023-08-21 13-40](https://github.com/enso-org/enso/assets/1428930/32d8f066-c80d-4915-8133-6dbc1edf70b3)

![Peek 2023-08-21 13-41](https://github.com/enso-org/enso/assets/1428930/a1e5aa7e-8776-4438-9c1e-80ac1afd0292)

# Important Notes
There is some ugly code around updating the theme. This should be fixed by providing a better mechanism to re-use components with different styles. The current mechanism assumes that a component will use the same theme for every instance. Implementing this is out of scope for this task, though.
  • Loading branch information
MichaelMauderer authored Aug 24, 2023
1 parent a205ad7 commit e21c5b1
Show file tree
Hide file tree
Showing 16 changed files with 427 additions and 1,018 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/gui/src/controller/searcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ impl Searcher {
warn!(
"Cannot update breadcrumbs with component that has no suggestion database \
entry. Invalid component: {:?}",
component
component.suggestion.name()
);
None
}
Expand Down
278 changes: 179 additions & 99 deletions app/gui/src/presenter/graph/call_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

use crate::prelude::*;

use crate::executor::global::spawn_stream_handler;
use crate::model::execution_context::LocalCall;
use crate::presenter::graph::state::State;
use crate::presenter::graph::ViewNodeId;

use enso_frp as frp;
use ensogl_breadcrumbs::Breadcrumb;
use ide_view as view;


Expand All @@ -19,23 +19,171 @@ use ide_view as view;

#[derive(Debug)]
struct Model {
controller: controller::ExecutedGraph,
view: view::project::View,
state: Rc<State>,
controller: controller::ExecutedGraph,
view: view::project::View,
state: Rc<State>,
// Full stack displayed in the breadcrumbs. Includes the deeper levels, that might not be
// active due to the current selection.
stack_history: Rc<RefCell<Vec<view::project_view_top_bar::LocalCall>>>,
}

impl Model {
/// Default constructor. Creates a new model with empty stack.
fn new(
controller: controller::ExecutedGraph,
view: view::project::View,
state: Rc<State>,
) -> Self {
Self { controller, view, state }
Self { controller, view, state, stack_history: default() }
}

fn push_stack(&self, stack: Vec<view::project_view_top_bar::LocalCall>) {
/// Initialize the breadcrumbs view. Initially there is only the main module.
fn init_breadcrumb_view(&self) {
let breadcrumbs = &self.view.top_bar().breadcrumbs;
breadcrumbs.clear();
breadcrumbs.push(Breadcrumb::new_without_icon("main"));
}

/// Returns a closure that stores the call stack in the project metadata. This will store the
/// callstack at the time of calling the closure.
fn store_updated_stack_task(&self) -> impl FnOnce() {
let main_module = self.controller.graph().module.clone_ref();
let controller = self.controller.clone_ref();
move || {
let _transaction = main_module
.undo_redo_repository()
.open_ignored_transaction_or_ignore_current("Updating call stack metadata");
let new_call_stack = controller.call_stack();
let result = main_module.update_project_metadata(|metadata| {
metadata.call_stack = new_call_stack;
});
if let Err(error) = result {
// We cannot really do anything when updating metadata fails.
// Can happen in improbable case of serialization failure.
error!("Failed to store an updated call stack: {error}");
}
}
}


// === UI Control API ===
// Methods that modify the UI state of the breadcrumbs. Note that they will always propagate
// the changes to the controller. These should be exclusivity used to update the state of the
// call stack. They will result in the breadcrumbs view being updated, which in turn will
// result in the methods from the `UI Driven API` being called to update the controller.
// They are also update the internal stack history, which should always be in sync with the
// visible breadcrumbs.

/// Add a part of the call stack. Note that this will check if the stack is already in the
/// breadcrumbs and if so, it will only update the selection.
fn add_stack_levels(
&self,
stack: Vec<view::project_view_top_bar::LocalCall>,
stack_pointer: usize,
) {
if stack.is_empty() {
return;
}

self.stack_history.borrow_mut().truncate(stack_pointer);
self.stack_history.borrow_mut().extend(stack.clone());

let stack = stack.into_iter().map(|call| call.into()).collect_vec();
let breadcrumb_index = stack_pointer + 1;
self.view.top_bar().breadcrumbs.set_entries_from((stack, breadcrumb_index));
}

/// Move the selection in the breadcrumbs to the left. This will result in the controller
/// exiting the stack by the given number of levels.
fn shift_breadcrumb_selection_left(&self, count: usize) {
let breadcrumbs = &self.view.top_bar().breadcrumbs;
for _ in 0..count {
breadcrumbs.move_up();
}
}


// === UI Driven API ===
// These methods are used by the UI (Graph editor and Breadcrumbs) to update the call stack.
// Note that the state of the breadcrumbs is the main source of modifications to the call
// stack. Any modification to the breadcrumbs will be propagated to the controller, thus care
// needs to be taken to avoid infinite loops and any updates to the call stack should go
// through the breadcrumbs.

/// Method to call when exiting a node in the graph editor. This will move the selection in
/// the breadcrumbs (and the call stack itself) one level up.
fn node_exited(&self) {
self.shift_breadcrumb_selection_left(1)
}

/// Method to call when entering a node in the graph editor. This will add a new level to the
/// breadcrumbs. It will update the breadcrumbs in the following way: if the node was already
/// in the breadcrumbs, it will move the selection to that node, otherwise it will clear all
/// the deeper levels and add the new node at the end.
fn node_entered(&self, node_id: ViewNodeId) {
analytics::remote_log_event("integration::node_entered");
if let Some(call) = self.state.ast_node_id_of_view(node_id) {
if let Ok(computed_value) = self.controller.node_computed_value(call) {
if let Some(method_pointer) = computed_value.method_call.as_ref() {
let local_call = LocalCall { call, definition: method_pointer.clone() };
let stack_pointer = self.controller.call_stack().len();
self.add_stack_levels(
vec![view::project_view_top_bar::LocalCall {
call: local_call.call,
definition: local_call.definition.into(),
}],
stack_pointer,
);
} else {
debug!("Ignoring request to enter non-enterable node {call}.")
}
} else {
debug!("Ignoring request to enter not computed node {call}.")
}
} else {
error!("Cannot enter {node_id:?}: no AST node bound to the view.")
}
}

/// Method to call when a breadcrumb is selected. This will update the call stack to match the
/// selection.
fn breadcrumb_selected(&self, index: usize) {
let current_stack = self.controller.call_stack();
if current_stack.len() >= index {
self.pop_stack(current_stack.len() - index);
} else {
let to_push = self.stack_history.borrow().iter().skip(index - 1).cloned().collect_vec();
if to_push.is_empty() {
warn!("Cannot select breadcrumb {index}. No such item in stack history {stack_history:?}.", stack_history = self.stack_history);
return;
}
self.push_stack(to_push);
}
}

fn visible_breadcrumbs(&self, entries: &[Breadcrumb]) {
debug!("Visible Breadcrumbs changed to {entries:?}");
debug_assert_eq!(
entries.len(),
self.stack_history.borrow().len() + 1,
"Visible breadcrumbs should be equal to stack history, but are {entries:#?}, \
while stack history is {stack_history:#?}.",
stack_history = self.stack_history
);
}


// === Controller API ===
// These methods are used to update the state of the call stack in the controller. They are
// called by the UI driven logic and propagate the changes to the controller.

/// Push a new call stack to the current stack. This will notify the controller to enter the
/// stack. If the controller fails to enter the stack, the last item from breadcrumbs will be
/// popped again.
pub fn push_stack(&self, stack: Vec<view::project_view_top_bar::LocalCall>) {
let store_stack = self.store_updated_stack_task();
let controller = self.controller.clone_ref();
let breadcrumbs = self.view.top_bar().breadcrumbs.clone_ref();
executor::global::spawn(async move {
let stack = stack
.into_iter()
Expand All @@ -48,7 +196,10 @@ impl Model {
match controller.enter_stack(stack).await {
Ok(()) => store_stack(),
Err(error) => {
error!("Entering stack failed: {error}.");
// Revert the breadcrumbs
breadcrumbs.pop();
// Log the error.
debug!("Entering stack failed: {error}.");
let event = "integration::entering_node_failed";
let field = "error";
let data = analytics::AnonymousData(|| error.to_string());
Expand All @@ -58,8 +209,8 @@ impl Model {
});
}

fn pop_stack(&self, frame_count: usize) {
debug!("Requesting exiting a part of the call stack.");
/// Pop a part of the call stack. This will notify the controller to exit the stack.
pub fn pop_stack(&self, frame_count: usize) {
analytics::remote_log_event("integration::node_exited");
let controller = self.controller.clone_ref();
let store_stack = self.store_updated_stack_task();
Expand All @@ -77,73 +228,6 @@ impl Model {
}
});
}

fn node_entered(&self, node_id: ViewNodeId) {
debug!("Requesting entering the node {node_id}.");
analytics::remote_log_event("integration::node_entered");
if let Some(call) = self.state.ast_node_id_of_view(node_id) {
if let Ok(computed_value) = self.controller.node_computed_value(call) {
if let Some(method_pointer) = computed_value.method_call.as_ref() {
let controller = self.controller.clone_ref();
let local_call = LocalCall { call, definition: method_pointer.clone() };
let store_stack = self.store_updated_stack_task();
executor::global::spawn(async move {
info!("Entering expression {local_call:?}.");
match controller.enter_stack(vec![local_call]).await {
Ok(()) => store_stack(),
Err(error) => {
error!("Entering node failed: {error}.");
let event = "integration::entering_node_failed";
let field = "error";
let data = analytics::AnonymousData(|| error.to_string());
analytics::remote_log_value(event, field, data);
}
};
});
} else {
info!("Ignoring request to enter non-enterable node {call}.")
}
} else {
info!("Ignoring request to enter not computed node {call}.")
}
} else {
error!("Cannot enter {node_id:?}: no AST node bound to the view.")
}
}

fn node_exited(&self) {
self.pop_stack(1)
}

fn store_updated_stack_task(&self) -> impl FnOnce() {
let main_module = self.controller.graph().module.clone_ref();
let controller = self.controller.clone_ref();
move || {
let _transaction = main_module
.undo_redo_repository()
.open_ignored_transaction_or_ignore_current("Updating call stack metadata");
let new_call_stack = controller.call_stack();
let result = main_module.update_project_metadata(|metadata| {
metadata.call_stack = new_call_stack;
});
if let Err(error) = result {
// We cannot really do anything when updating metadata fails.
// Can happen in improbable case of serialization failure.
error!("Failed to store an updated call stack: {error}");
}
}
}

fn add_breadcrumbs_in_view(&self, stack: Vec<LocalCall>) {
let view_stack = stack
.into_iter()
.map(|frame| view::project_view_top_bar::LocalCall {
call: frame.call,
definition: frame.definition.into(),
})
.collect::<Vec<_>>();
self.view.top_bar().breadcrumbs.push_breadcrumbs(view_stack);
}
}


Expand All @@ -152,6 +236,16 @@ impl Model {
// === CallStack ===
// ======================

fn to_local_call_stack(stack: Vec<LocalCall>) -> Vec<view::project_view_top_bar::LocalCall> {
stack
.into_iter()
.map(|frame| view::project_view_top_bar::LocalCall {
call: frame.call,
definition: frame.definition.into(),
})
.collect()
}

/// Call Stack presenter, synchronizing the call stack of currently displayed graph with
/// the breadcrumbs displayed above. It also handles the entering/exiting nodes requests.
#[derive(Debug)]
Expand All @@ -177,35 +271,21 @@ impl CallStack {
eval graph_editor_view.node_entered ((node) model.node_entered(*node));
eval_ graph_editor_view.node_exited (model.node_exited());

eval breadcrumbs.output.breadcrumb_push ((stack) model.push_stack(stack.clone()));
eval breadcrumbs.output.breadcrumb_pop ((count) model.pop_stack(*count));
selected_update <- breadcrumbs.selected.on_change();
eval selected_update ((index) model.breadcrumb_selected(*index));
entried_update <- breadcrumbs.entries.on_change();
eval entried_update ((entries) model.visible_breadcrumbs(entries));
}

Self { _network: network, model }
.initialize_breadcrumbs()
.setup_controller_notification_handlers()
}

fn setup_controller_notification_handlers(self) -> Self {
use crate::controller::graph::executed::Notification;
let graph_notifications = self.model.controller.subscribe();
let weak = Rc::downgrade(&self.model);
spawn_stream_handler(weak, graph_notifications, move |notification, model| {
info!("Received controller notification {notification:?}");
match notification {
Notification::EnteredStack(stack) => model.add_breadcrumbs_in_view(stack),
Notification::ExitedStack(count) =>
model.view.top_bar().breadcrumbs.pop_breadcrumbs(count),
_ => {}
}
std::future::ready(())
});
self
Self { _network: network, model }.initialize_breadcrumbs()
}

/// Initialize the breadcrumbs view. Initially there is only the main module.
fn initialize_breadcrumbs(self) -> Self {
self.model.init_breadcrumb_view();
let stack = self.model.controller.call_stack();
self.model.add_breadcrumbs_in_view(stack);
let call_stack = to_local_call_stack(stack);
self.model.add_stack_levels(call_stack, 1); // 1 because of the main module
self
}
}
Loading

0 comments on commit e21c5b1

Please sign in to comment.