diff --git a/src/algorithm/cost_calculation.rs b/src/algorithm/cost_calculation.rs index f26280d..cc8f476 100644 --- a/src/algorithm/cost_calculation.rs +++ b/src/algorithm/cost_calculation.rs @@ -374,8 +374,7 @@ fn calc_station_exit_cost( station.get_pos(), node, true, - ) - .map(|c| c / 5.0); + ); } } } diff --git a/src/algorithm/executor.rs b/src/algorithm/executor.rs index ee84f0f..f1c9d9b 100644 --- a/src/algorithm/executor.rs +++ b/src/algorithm/executor.rs @@ -28,6 +28,7 @@ use crate::{ IDData, IDManager, }, + Error, }; /// The response from the algorithm. @@ -40,6 +41,8 @@ pub struct AlgorithmResponse { /// The data for the [`IDManager`] after the algorithm has run, ensuring the /// main thread will not create IDs in conflict with those in the map. pub id_manager_data: IDData, + /// If an error occurred during the algorithm, this contains it. + pub error: Option, } /// The inner state of the executor. @@ -51,6 +54,8 @@ struct ExecutorState { done: bool, /// The waker for the stream. waker: Option, + /// Any error that occurred during the algorithm. + error: Option, } /// The executor for the algorithm. @@ -74,6 +79,7 @@ impl AlgorithmExecutor { last_res: None, done: false, waker: None, + error: None, })), midway_updates, }; @@ -112,6 +118,9 @@ impl AlgorithmExecutor { closure_executor .update_last_res(map, IDManager::to_data(), res.is_ok()) .await; + closure_executor + .set_error(res.err()) + .await; closure_executor .mark_done() .await; @@ -129,6 +138,7 @@ impl AlgorithmExecutor { success, map, id_manager_data, + error: None, }; self.inner @@ -147,6 +157,23 @@ impl AlgorithmExecutor { .take() } + /// Set the error of the algorithm. + async fn set_error(&self, error: Option) { + self.inner + .lock() + .await + .error = error; + } + + /// Get the error of the algorithm. + async fn get_error(&self) -> Option { + self.inner + .lock() + .await + .error + .clone() + } + /// Mark the algorithm and thus stream as done. async fn mark_done(&self) { self.inner @@ -209,13 +236,20 @@ impl Stream for AlgorithmExecutor { let res = this .pop_last_res() .now_or_never(); + let error = this + .get_error() + .now_or_never() + .flatten(); let done = this .get_done() .now_or_never() .unwrap_or(false); match res { - Some(Some(res)) => std::task::Poll::Ready(Some(res)), + Some(Some(mut res)) => { + res.error = error; + std::task::Poll::Ready(Some(res)) + }, Some(None) if done => std::task::Poll::Ready(None), _ => std::task::Poll::Pending, } diff --git a/src/components/atoms/canvas_info_box.rs b/src/components/atoms/canvas_info_box.rs index 5538be4..110e1f8 100644 --- a/src/components/atoms/canvas_info_box.rs +++ b/src/components/atoms/canvas_info_box.rs @@ -1,52 +1,28 @@ //! Contains the [`CanvasInfoBox`] component. -use ev::MouseEvent; use leptos::*; -use wasm_bindgen::JsCast; use crate::MapState; /// A generic canvas info box that others can be based upon. #[allow(clippy::needless_pass_by_value)] // cannot be a reference because of the `Fn` trait #[component] -pub fn CanvasInfoBox( +pub fn CanvasInfoBox( /// The title of the info box, title: S, /// If the info box should be shown. click_position: Signal>, - /// Gets called if the info box is closed by clicking outside the box. - on_close: C, /// The body of the info box if applicable. #[prop(optional)] children: Option, ) -> impl IntoView where S: ToString + 'static, - C: Fn() + 'static, { let info_box_ref: NodeRef = create_node_ref(); let map_state = use_context::>().expect("to have found the global map state"); - let on_outside_click = move |e: MouseEvent| { - // actual dom node that got clicked on - let target_node = e - .target() - .and_then(|t| { - t.dyn_ref::() - .cloned() - }); - - // if the clicked node is outside the modal itself - if !info_box_ref - .get() - .unwrap() - .contains(target_node.as_ref()) - { - on_close(); - } - }; - let show = move || { click_position .get() @@ -90,8 +66,7 @@ where tabindex="-1" style:display=move || if show() {"block"} else {"none"} class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 w-full md:inset-0 h-[calc(100%-0.125rem)] max-h-full" - style:pointer-events="none" - on:click=on_outside_click> + style:pointer-events="none">
impl IntoView { let canvas_ref = create_node_ref::(); let map_state = use_context::>().expect("to have found the global map state"); + let error_state = + use_context::>().expect("to have found the global error state"); // ensures we know the size of the canvas and that one page resizing, the canvas // is also resized. @@ -99,13 +104,13 @@ pub fn Canvas() -> impl IntoView { _ref=canvas_ref on:mousedown=move |ev| map_state.update(|state| on_mouse_down(state, ev.as_ref(), ev.shift_key())) - on:mouseup=move |ev| map_state.update(|state| on_mouse_up(state, ev.as_ref(), ev.shift_key())) + on:mouseup=move |ev| map_state.update(|state| on_mouse_up(state, error_state, ev.as_ref(), ev.shift_key())) on:mousemove=move |ev| on_mouse_move(&map_state, ev.as_ref()) on:mouseout=move |_| map_state.update(on_mouse_out) on:dblclick=move |ev| map_state.update(|state| on_dbl_click(state, ev.as_ref(), ev.shift_key())) on:touchstart=move |ev| map_state.update(|state| on_mouse_down(state, ev.as_ref(), ev.shift_key())) - on:touchend=move |ev| map_state.update(|state| on_mouse_up(state, ev.as_ref(), ev.shift_key())) + on:touchend=move |ev| map_state.update(|state| on_mouse_up(state, error_state, ev.as_ref(), ev.shift_key())) on:touchmove=move |ev| on_mouse_move(&map_state, ev.as_ref()) on:touchcancel=move |_| map_state.update(on_mouse_out) diff --git a/src/components/canvas/mouse_up.rs b/src/components/canvas/mouse_up.rs index cc579d0..9fea962 100644 --- a/src/components/canvas/mouse_up.rs +++ b/src/components/canvas/mouse_up.rs @@ -1,5 +1,9 @@ //! Contains the mouseup event handler for the [`Canvas`] component. +use leptos::{ + RwSignal, + SignalUpdate, +}; use web_sys::UiEvent; use super::other::{ @@ -7,7 +11,10 @@ use super::other::{ recalculate_edge_nodes, }; use crate::{ - components::state::ActionType, + components::{ + state::ActionType, + ErrorState, + }, models::{ GridNode, SelectedStation, @@ -18,7 +25,12 @@ use crate::{ /// Listener for the [mouseup] event on the canvas. /// /// [mouseup]: https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseup_event -pub fn on_mouse_up(map_state: &mut MapState, ev: &UiEvent, shift_key: bool) { +pub fn on_mouse_up( + map_state: &mut MapState, + error_state: RwSignal, + ev: &UiEvent, + shift_key: bool, +) { if ev.detail() > 1 { return; } @@ -51,7 +63,9 @@ pub fn on_mouse_up(map_state: &mut MapState, ev: &UiEvent, shift_key: bool) { }, ActionType::RemoveLine => { if let Some(selected_line) = map.line_at_node(mouse_pos) { - map.remove_line(selected_line.get_id()); + if let Err(err) = map.remove_line(selected_line.get_id()) { + error_state.update(|state| state.set_error(err)); + } } }, ActionType::Lock => { diff --git a/src/components/mod.rs b/src/components/mod.rs index d167029..f27559f 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -13,6 +13,7 @@ mod state; pub use pages::Home; pub use state::{ CanvasState, + ErrorState, MapState, StateProvider, }; diff --git a/src/components/molecules/edge_info_box.rs b/src/components/molecules/edge_info_box.rs index ff1193d..5a6b501 100644 --- a/src/components/molecules/edge_info_box.rs +++ b/src/components/molecules/edge_info_box.rs @@ -145,12 +145,7 @@ pub fn EdgeInfoBox() -> impl IntoView { + click_position=position>
impl IntoView { + let error_state = + use_context::>().expect("to have found the global error state"); + + let on_click = move |_| { + error_state.update(|state| state.clear_error()); + }; + + let has_error = move || { + error_state + .get() + .has_error() + }; + let error_message = move || { + let err = error_state + .get() + .get_error() + .map(|e| e.to_user_friendly_string()); + + if err.is_some() { + let f = Closure::wrap(Box::new(move || { + error_state.update(|state| state.clear_error()); + }) as Box); + window() + .set_timeout_with_callback_and_timeout_and_arguments_0( + f.as_ref() + .unchecked_ref(), + 3000, + ) + .unwrap(); + f.forget(); + } + + err + }; + + view! { + +
+
+
+ x + {error_message} +
+
+
+
+ } +} diff --git a/src/components/molecules/file_downloader.rs b/src/components/molecules/file_downloader.rs index 0ef2fcd..e32feb9 100644 --- a/src/components/molecules/file_downloader.rs +++ b/src/components/molecules/file_downloader.rs @@ -16,6 +16,7 @@ use super::FileType; use crate::{ components::{ atoms::Button, + ErrorState, MapState, }, unwrap_or_return, @@ -27,31 +28,42 @@ use crate::{ pub fn FileDownloader() -> impl IntoView { let map_state = use_context::>().expect("to have found the global map state"); + let error_state = + use_context::>().expect("to have found the global error state"); let download_map = move |file_type: FileType| { - let encoded = unwrap_or_return!(match file_type { - FileType::Json => { - let state = map_state.get_untracked(); - encode_map( - state.get_map(), - state.get_canvas_state(), - ) - }, - FileType::GraphML => return, - }); + let encoded = unwrap_or_return!( + error_state, + match file_type { + FileType::Json => { + let state = map_state.get_untracked(); + encode_map( + state.get_map(), + state.get_canvas_state(), + ) + }, + FileType::GraphML => return, + } + ); let options = BlobPropertyBag::new(); options.set_type(file_type.to_mime_type()); let str_sequence = std::iter::once(JsValue::from_str(&encoded)).collect::(); - let blob = unwrap_or_return!(Blob::new_with_str_sequence_and_options( - &str_sequence, - &options - )); - let url = unwrap_or_return!(Url::create_object_url_with_blob(&blob)); + let blob = unwrap_or_return!( + error_state, + Blob::new_with_str_sequence_and_options(&str_sequence, &options) + ); + let url = unwrap_or_return!( + error_state, + Url::create_object_url_with_blob(&blob) + ); - let elem = unwrap_or_return!(document().create_element("a")) - .dyn_into::() - .expect("to convert the element to an anchor element"); + let elem = unwrap_or_return!( + error_state, + document().create_element("a") + ) + .dyn_into::() + .expect("to convert the element to an anchor element"); elem.set_href(&url); elem.set_download(&format!( @@ -63,7 +75,10 @@ pub fn FileDownloader() -> impl IntoView { )); elem.click(); - unwrap_or_return!(Url::revoke_object_url(&url)); + unwrap_or_return!( + error_state, + Url::revoke_object_url(&url) + ); }; view! { diff --git a/src/components/molecules/file_modal.rs b/src/components/molecules/file_modal.rs index 756dee3..e95a0ba 100644 --- a/src/components/molecules/file_modal.rs +++ b/src/components/molecules/file_modal.rs @@ -10,9 +10,12 @@ use wasm_bindgen::{ use web_sys::HtmlInputElement; use crate::{ - components::atoms::{ - Button, - Modal, + components::{ + atoms::{ + Button, + Modal, + }, + ErrorState, }, unwrap_or_return, Error, @@ -43,6 +46,9 @@ fn get_file(input: &HtmlInputElement, on_submit: S) where S: Fn(FileType, String) + 'static, { + let error_state = + use_context::>().expect("to have found the global error state"); + let Some(file) = input .files() .and_then(|l| l.item(0)) @@ -64,11 +70,13 @@ where let cb = Closure::new(move |v: JsValue| { on_submit( file_type, - unwrap_or_return!(v - .as_string() - .ok_or(Error::other( - "file contents should be a string" - ))), + unwrap_or_return!( + error_state, + v.as_string() + .ok_or(Error::other( + "file contents should be a string" + )) + ), ); }); diff --git a/src/components/molecules/mod.rs b/src/components/molecules/mod.rs index f5b6f00..20f061d 100644 --- a/src/components/molecules/mod.rs +++ b/src/components/molecules/mod.rs @@ -2,12 +2,14 @@ //! interactions. mod edge_info_box; +mod error_box; mod file_downloader; mod file_modal; mod settings_modal; mod station_info_box; pub use edge_info_box::EdgeInfoBox; +pub use error_box::ErrorBox; pub use file_downloader::FileDownloader; pub use file_modal::{ FileModal, diff --git a/src/components/molecules/station_info_box.rs b/src/components/molecules/station_info_box.rs index 5584c0b..40c5362 100644 --- a/src/components/molecules/station_info_box.rs +++ b/src/components/molecules/station_info_box.rs @@ -98,12 +98,7 @@ pub fn StationInfoBox() -> impl IntoView { + click_position=position>
"Name:\n" impl IntoView { + click_position=position> <> diff --git a/src/components/organisms/canvas_controls.rs b/src/components/organisms/canvas_controls.rs index e36e716..b1fa54e 100644 --- a/src/components/organisms/canvas_controls.rs +++ b/src/components/organisms/canvas_controls.rs @@ -36,6 +36,7 @@ use crate::{ StationInfoBox, }, CanvasState, + ErrorState, MapState, }, models::Map, @@ -88,6 +89,8 @@ pub fn CanvasControls() -> impl IntoView { let container_ref = create_node_ref::
(); let map_state = use_context::>().expect("to have found the global map state"); + let error_state = + use_context::>().expect("to have found the global error state"); let (executor, _) = create_signal( PoolExecutor::::new(1).expect("failed to start web-worker pool"), ); @@ -138,20 +141,37 @@ pub fn CanvasControls() -> impl IntoView { { map_state.update(|state| { if partial { - unwrap_or_return!(state - .get_mut_map() - .update_from_partial(&resp.map)); + unwrap_or_return!( + error_state, + state + .get_mut_map() + .update_from_partial(&resp.map) + ); } else { state.set_map(resp.map); } }); IDManager::from_data(resp.id_manager_data); + } else { + map_state.update(|state| { + if let Some(map) = state + .get_last_loaded() + .cloned() + { + state.set_map(map); + } + }) + } + if let Some(error) = resp.error { + error_state.update(|state| { + state.set_error(error); + }); } }; // Dispatch the algorithm request. let algorithm_req = create_action(move |req: &AlgorithmRequest| { - map_state.update_untracked(|state| { + map_state.update(|state| { state.clear_all_selections(); }); @@ -303,7 +323,7 @@ pub fn CanvasControls() -> impl IntoView { when=has_parts_selected fallback=move || view!{ }>