Compare commits

...

6 Commits

Author SHA1 Message Date
Emilia Allison ad57482dea
Bump rev version for web
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-21 10:05:40 -05:00
Emilia Allison 12684c0eea
Merge plates
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
i wrote this so late it probably does not work ughhhhhhhhh
2024-02-20 23:31:56 -05:00
Emilia Allison 0ec29f6783
Reorganize tree_callbacks
I am going to make too many
2024-02-20 21:26:24 -05:00
Emilia Allison e01468b63a
Reorganize main_window_callbacks
There were so many
2024-02-20 21:18:53 -05:00
Emilia Allison 8f82c4e224
Linear volume heatmap visualization
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-18 22:01:08 -05:00
Emilia Allison 2fdc15c9aa
Accept higher version of lib on web 2024-02-18 22:00:41 -05:00
21 changed files with 672 additions and 311 deletions

4
Cargo.lock generated
View File

@ -633,7 +633,7 @@ dependencies = [
[[package]] [[package]]
name = "plate-tool-lib" name = "plate-tool-lib"
version = "0.3.0" version = "0.3.1"
dependencies = [ dependencies = [
"csv", "csv",
"getrandom", "getrandom",
@ -648,7 +648,7 @@ dependencies = [
[[package]] [[package]]
name = "plate-tool-web" name = "plate-tool-web"
version = "0.3.0" version = "0.3.1"
dependencies = [ dependencies = [
"csv", "csv",
"getrandom", "getrandom",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "plate-tool-web" name = "plate-tool-web"
version = "0.3.0" version = "0.3.1"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -36,6 +36,12 @@ dialog > form[method="dialog"] {
} }
} }
.shifted_dialog {
top: 50vh;
left: 50vw;
margin: 0;
}
.close_button { .close_button {
color: red; color: red;
position: absolute; position: absolute;

View File

@ -1,221 +0,0 @@
#![allow(non_snake_case)]
use js_sys::Array;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{
Blob, HtmlAnchorElement, HtmlDialogElement, HtmlElement,
HtmlFormElement, HtmlInputElement, Url,
};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::state_to_csv;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn create_close_button(close: &Closure<dyn FnMut(Event)>) -> HtmlElement {
let document = web_sys::window().unwrap().document().unwrap();
let close_button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
close_button.set_text_content(Some(""));
close_button.set_class_name("close_button");
close_button.set_onclick(Some(close.as_ref().unchecked_ref()));
close_button
}
pub fn toggle_in_transfer_hashes_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.in_transfer_hashes ^= true;
})
})
}
pub fn new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>,
) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(false);
})
}
pub fn open_new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>,
) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(true);
})
}
pub fn new_button_callback(
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<web_sys::MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let confirm =
window.confirm_with_message("This will reset all plates and transfers. Proceed?");
if let Ok(confirm) = confirm {
if confirm {
main_dispatch.set(MainState::default());
ct_dispatch.set(CurrentTransfer::default());
}
}
})
}
fn save_str(data: &str, name: &str) {
let blob =
Blob::new_with_str_sequence(&Array::from_iter(std::iter::once(JsValue::from_str(data))));
if let Ok(blob) = blob {
let url = Url::create_object_url_with_blob(&blob).expect("We have a blob, why not URL?");
// Beneath is the cool hack to download files
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let anchor = document
.create_element("a")
.unwrap()
.dyn_into::<HtmlAnchorElement>()
.unwrap();
anchor.set_download(name);
anchor.set_href(&url);
anchor.click();
}
}
pub fn export_csv_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if main_state.transfers.is_empty() {
web_sys::window()
.unwrap()
.alert_with_message("No transfers to export.")
.unwrap();
return;
}
web_sys::window().unwrap().alert_with_message("CSV export is currently not importable. Export as JSON if you'd like to back up your work!").unwrap();
if let Ok(csv) = state_to_csv(&main_state) {
save_str(&csv, "transfers.csv");
}
})
}
pub fn export_json_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if let Ok(json) = serde_json::to_string(&main_state) {
save_str(&json, "plate-tool-state.json");
} else {
web_sys::window()
.unwrap()
.alert_with_message("Failed to export.")
.unwrap();
}
})
}
pub fn input_json_input_callback(
main_dispatch: Dispatch<MainState>,
modal: HtmlDialogElement,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) = &fr1.result().ok().and_then(|v| v.as_string()) {
let ms = serde_json::from_str::<MainState>(value);
match ms {
Ok(ms) => main_dispatch.set(ms),
Err(e) => log::debug!("{:?}", e),
};
modal.close();
}
});
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
})
}
pub fn import_json_button_callback(main_dispatch: Dispatch<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
modal.append_child(&close_button).unwrap();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".json");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
input_json_input_callback(main_dispatch, modal)
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
}
pub use super::import_csv_callbacks::import_transfer_csv_callback;
pub use super::import_csv_callbacks::import_transfer_csv_submit_callback;
pub use super::import_csv_callbacks::import_transfer_csv_onload_callback;
pub use super::import_csv_callbacks::import_transfer_csv_input_callback;

View File

@ -0,0 +1,64 @@
#![allow(non_snake_case)]
use js_sys::Array;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{
Blob, HtmlAnchorElement, HtmlDialogElement, HtmlElement, HtmlFormElement, HtmlInputElement, Url,
};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::state_to_csv;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn export_csv_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if main_state.transfers.is_empty() {
web_sys::window()
.unwrap()
.alert_with_message("No transfers to export.")
.unwrap();
return;
}
web_sys::window().unwrap().alert_with_message("CSV export is currently not importable. Export as JSON if you'd like to back up your work!").unwrap();
if let Ok(csv) = state_to_csv(&main_state) {
save_str(&csv, "transfers.csv");
}
})
}
pub fn export_json_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if let Ok(json) = serde_json::to_string(&main_state) {
save_str(&json, "plate-tool-state.json");
} else {
web_sys::window()
.unwrap()
.alert_with_message("Failed to export.")
.unwrap();
}
})
}
fn save_str(data: &str, name: &str) {
let blob =
Blob::new_with_str_sequence(&Array::from_iter(std::iter::once(JsValue::from_str(data))));
if let Ok(blob) = blob {
let url = Url::create_object_url_with_blob(&blob).expect("We have a blob, why not URL?");
// Beneath is the cool hack to download files
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let anchor = document
.create_element("a")
.unwrap()
.dyn_into::<HtmlAnchorElement>()
.unwrap();
anchor.set_download(name);
anchor.set_href(&url);
anchor.click();
}
}

View File

@ -15,7 +15,7 @@ use plate_tool_lib::transfer_region::{Region, TransferRegion};
use plate_tool_lib::csv::{auto, string_well_to_pt, TransferRecord}; use plate_tool_lib::csv::{auto, string_well_to_pt, TransferRecord};
use super::main_window_callbacks::create_close_button; use super::create_close_button;
pub fn import_transfer_csv_input_callback( pub fn import_transfer_csv_input_callback(
main_dispatch: Dispatch<MainState>, main_dispatch: Dispatch<MainState>,

View File

@ -0,0 +1,98 @@
#![allow(non_snake_case)]
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{HtmlDialogElement, HtmlFormElement, HtmlInputElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::MainState;
use super::create_close_button;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn input_json_input_callback(
main_dispatch: Dispatch<MainState>,
modal: HtmlDialogElement,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) = &fr1.result().ok().and_then(|v| v.as_string()) {
let ms = serde_json::from_str::<MainState>(value);
match ms {
Ok(ms) => main_dispatch.set(ms),
Err(e) => log::debug!("{:?}", e),
};
modal.close();
}
});
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
})
}
pub fn import_json_button_callback(main_dispatch: Dispatch<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
modal.append_child(&close_button).unwrap();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".json");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
input_json_input_callback(main_dispatch, modal)
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
}

View File

@ -0,0 +1,18 @@
mod export_callbacks;
mod input_json_callbacks;
mod import_csv_callbacks;
mod settings_callbacks;
mod plate_edits_callbacks;
mod util;
pub use util::*;
pub use export_callbacks::*;
pub use input_json_callbacks::*;
pub use import_csv_callbacks::*;
pub use settings_callbacks::*;
pub use plate_edits_callbacks::*;

View File

@ -0,0 +1,51 @@
#![allow(non_snake_case)]
use js_sys::Array;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{
Blob, HtmlAnchorElement, HtmlDialogElement, HtmlElement, HtmlFormElement, HtmlInputElement, Url,
};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::state_to_csv;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>,
) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(false);
})
}
pub fn open_new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>,
) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(true);
})
}
pub fn new_button_callback(
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<web_sys::MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let confirm =
window.confirm_with_message("This will reset all plates and transfers. Proceed?");
if let Ok(confirm) = confirm {
if confirm {
main_dispatch.set(MainState::default());
ct_dispatch.set(CurrentTransfer::default());
}
}
})
}

View File

@ -0,0 +1,30 @@
#![allow(non_snake_case)]
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::MainState;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn toggle_in_transfer_hashes_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.in_transfer_hashes ^= true;
})
})
}
pub fn toggle_volume_heatmap_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.volume_heatmap ^= true;
})
})
}

View File

@ -0,0 +1,21 @@
#![allow(non_snake_case)]
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::HtmlElement;
use yew::prelude::*;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn create_close_button(close: &Closure<dyn FnMut(Event)>) -> HtmlElement {
let document = web_sys::window().unwrap().document().unwrap();
let close_button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
close_button.set_text_content(Some(""));
close_button.set_class_name("close_button");
close_button.set_onclick(Some(close.as_ref().unchecked_ref()));
close_button
}

View File

@ -2,4 +2,3 @@ pub mod main_window_callbacks;
pub mod new_plate_dialog_callbacks; pub mod new_plate_dialog_callbacks;
pub mod transfer_menu_callbacks; pub mod transfer_menu_callbacks;
pub mod tree_callbacks; pub mod tree_callbacks;
mod import_csv_callbacks;

View File

@ -0,0 +1,74 @@
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlElement, HtmlInputElement, HtmlOptionElement, HtmlSelectElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::MainState;
use plate_tool_lib::plate::PlateFormat;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn open_plate_info_callback(
plate_menu_id: UseStateHandle<Option<Uuid>>,
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
plate_menu_id.set(Some(Uuid::from_u128(id)));
}
}
})
}
pub fn plate_info_close_callback(plate_menu_id: UseStateHandle<Option<Uuid>>) -> NoParamsCallback {
Box::new(move |_| {
plate_menu_id.set(None);
})
}
pub fn plate_info_delete_callback(
main_dispatch: Dispatch<MainState>,
plate_menu_id: UseStateHandle<Option<Uuid>>,
) -> NoParamsCallback {
Box::new(move |_| {
if let Some(id) = *plate_menu_id {
main_dispatch.reduce_mut(|state| {
state.del_plate(id);
});
}
})
}
pub fn rename_onchange(id: Uuid, main_dispatch: Dispatch<MainState>) -> Callback<Event> {
Callback::from(move |e: Event| {
log::debug!("Changed name");
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.unwrap();
main_dispatch.reduce_mut(|state| state.rename_plate(id, &input.value()))
})
}
pub fn format_onchange(id: Uuid, main_dispatch: Dispatch<MainState>) -> Callback<Event> {
Callback::from(move |e: Event| {
log::debug!("Changing plate format");
let new_format: Option<PlateFormat> = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlSelectElement>()
.unwrap()
.selected_options()
.get_with_index(0)
.map(|el| el.dyn_into::<HtmlOptionElement>().unwrap())
.map(|opt_el| opt_el.value())
.and_then(|value| PlateFormat::try_from(value.as_str()).ok());
if let Some(format) = new_format {
main_dispatch.reduce_mut(|state| state.change_format(id, &format));
}
})
}

View File

@ -0,0 +1,217 @@
use plate_tool_lib::{plate::PlateType, plate_instances::PlateInstance};
use uuid::Uuid;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{
HtmlDialogElement, HtmlElement, HtmlFormElement, HtmlInputElement, HtmlOptionElement,
HtmlSelectElement,
};
use yew::prelude::*;
use yewdux::prelude::*;
use std::rc::Rc;
use std::str::FromStr;
use crate::components::callbacks::main_window_callbacks::create_close_button;
use crate::components::states::MainState;
pub fn merge_button_callback(main_dispatch: Dispatch<MainState>, id: Uuid) -> Callback<MouseEvent> {
Callback::from(move |_| {
let dialog_opt = create_merge_dialog(main_dispatch.clone(), id);
if let Some(dialog) = dialog_opt {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let _ = body.append_child(&dialog);
dialog.set_open(true);
}
})
}
fn create_merge_dialog(main_dispatch: Dispatch<MainState>, id: Uuid) -> Option<HtmlDialogElement> {
let other_plates = get_all_plates_except(main_dispatch.clone(), id);
if other_plates.is_empty() {
return None;
}
let dialog = create_dialog();
let select = create_select_for_plates(&other_plates);
dialog.append_child(&select).unwrap();
let select_rc = Rc::new(select);
let ok_button = create_ok_button(main_dispatch.clone(), select_rc.clone(), id);
dialog.append_child(&ok_button).unwrap();
Some(dialog)
}
fn create_dialog() -> HtmlDialogElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let dialog = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
dialog.set_class_name("dialog shifted_dialog");
let onclose_callback = {
let dialog = dialog.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
dialog.remove();
})
};
dialog.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
dialog.append_child(&close_button).unwrap();
let header = document.create_element("h2").unwrap();
header.set_inner_html("Merge Plates");
dialog.append_child(&header).unwrap();
dialog
}
fn create_ok_button(
main_dispatch: Dispatch<MainState>,
select: Rc<HtmlSelectElement>,
id: Uuid,
) -> HtmlElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
button.set_inner_html("Ok");
let callback = ok_button_callback(main_dispatch, select, id);
button.set_onclick(Some(callback.as_ref().unchecked_ref()));
callback.forget();
button
}
fn ok_button_callback(
main_dispatch: Dispatch<MainState>,
select: Rc<HtmlSelectElement>,
to_id: Uuid,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |_: Event| {
let selected = select
.selected_options()
.get_with_index(0)
.map(|el| el.dyn_into::<HtmlOptionElement>().unwrap())
.map(|opt_el| opt_el.value())
.and_then(|uuid_str| Uuid::from_str(&uuid_str).ok());
if let Some(from_id) = selected {
let plate_type = find_plate_type(main_dispatch.clone(), from_id).unwrap();
let binding = main_dispatch.get();
let merge_to = match plate_type {
PlateType::Source => binding.source_plates.iter().find(|x| x.get_uuid() == to_id).unwrap(),
PlateType::Destination => binding
.destination_plates
.iter()
.find(|x| x.get_uuid() == to_id).unwrap(),
};
main_dispatch.reduce_mut(|x| {
let merged_transfers = x.transfers.iter_mut().filter(|x| match plate_type {
PlateType::Source => x.source_id,
PlateType::Destination => x.dest_id
} == from_id);
for transfer in merged_transfers {
match plate_type {
PlateType::Source => {
transfer.source_id = to_id;
transfer.transfer_region.source_plate = merge_to.plate;
},
PlateType::Destination => {
transfer.dest_id = to_id;
transfer.transfer_region.dest_plate = merge_to.plate;
}
}
}
match plate_type {
PlateType::Source => x.source_plates.retain(|y| y.get_uuid() != from_id),
PlateType::Destination => x.destination_plates.retain(|y| y.get_uuid() != from_id)
};
});
}
})
}
fn create_select_for_plates(infos: &Vec<(Uuid, String)>) -> HtmlSelectElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let select = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for info in infos {
let _ = select.append_child(&create_option_for_plate(info));
}
select
}
fn create_option_for_plate(info: &(Uuid, String)) -> HtmlOptionElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let opt = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
opt.set_value(&info.0.to_string());
opt.set_inner_text(&info.1);
opt
}
fn get_all_plates_except(main_dispatch: Dispatch<MainState>, id: Uuid) -> Vec<(Uuid, String)> {
let plate_type = find_plate_type(main_dispatch.clone(), id);
if plate_type.is_none() {
return Vec::new();
} // Return early if not found somehow
let plate_type = plate_type.unwrap();
let binding = main_dispatch.get();
let all_plates_iter = match plate_type {
PlateType::Source => binding.source_plates.iter(),
PlateType::Destination => binding.destination_plates.iter(),
};
all_plates_iter
.map(|x| (x.get_uuid(), x.name.to_string()))
.filter(|y| y.0 != id)
.collect()
}
fn find_plate_type(main_dispatch: Dispatch<MainState>, id: Uuid) -> Option<PlateType> {
if main_dispatch
.get()
.source_plates
.iter()
.any(|x| x.get_uuid() == id)
{
return Some(PlateType::Source);
} else if main_dispatch
.get()
.destination_plates
.iter()
.any(|x| x.get_uuid() == id)
{
return Some(PlateType::Destination);
} else {
return None;
}
}

View File

@ -0,0 +1,9 @@
mod select_callbacks;
mod info_dialog_callbacks;
mod merge_callbacks;
pub use select_callbacks::*;
pub use info_dialog_callbacks::*;
pub use merge_callbacks::*;

View File

@ -1,81 +1,12 @@
use std::rc::Rc; use std::rc::Rc;
use uuid::Uuid; use uuid::Uuid;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlElement, HtmlInputElement, HtmlSelectElement, HtmlOptionElement}; use web_sys::{EventTarget, HtmlElement};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState}; use crate::components::states::{CurrentTransfer, MainState};
use plate_tool_lib::{transfer_region::Region, plate::PlateFormat}; use plate_tool_lib::transfer_region::Region;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn open_plate_info_callback(
plate_menu_id: UseStateHandle<Option<Uuid>>,
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
plate_menu_id.set(Some(Uuid::from_u128(id)));
}
}
})
}
pub fn plate_info_close_callback(
plate_menu_id: UseStateHandle<Option<Uuid>>,
) -> NoParamsCallback {
Box::new(move |_| {
plate_menu_id.set(None);
})
}
pub fn plate_info_delete_callback(
main_dispatch: Dispatch<MainState>,
plate_menu_id: UseStateHandle<Option<Uuid>>,
) -> NoParamsCallback {
Box::new(move |_| {
if let Some(id) = *plate_menu_id {
main_dispatch.reduce_mut(|state| {
state.del_plate(id);
});
}
})
}
pub fn rename_onchange(id: Uuid, main_dispatch: Dispatch<MainState>) -> Callback<Event> {
Callback::from(move |e: Event| {
log::debug!("Changed name");
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.unwrap();
main_dispatch.reduce_mut(|state| state.rename_plate(id, &input.value()))
})
}
pub fn format_onchange(id: Uuid, main_dispatch: Dispatch<MainState>) -> Callback<Event> {
Callback::from(move |e: Event| {
log::debug!("Changing plate format");
let new_format: Option<PlateFormat> = e.target()
.expect("Event must have target")
.dyn_into::<HtmlSelectElement>()
.unwrap()
.selected_options()
.get_with_index(0)
.map(|el| {
el.dyn_into::<HtmlOptionElement>().unwrap()
})
.map(|opt_el| opt_el.value())
.and_then(|value| PlateFormat::try_from(value.as_str()).ok());
if let Some(format) = new_format {
main_dispatch.reduce_mut(|state| state.change_format(id, &format));
}
})
}
pub fn source_plate_select_callback( pub fn source_plate_select_callback(
main_dispatch: Dispatch<MainState>, main_dispatch: Dispatch<MainState>,
@ -121,7 +52,11 @@ pub fn destination_plate_select_callback(
}) })
} }
pub fn transfer_select_callback(main_state: Rc<MainState>, main_dispatch: Dispatch<MainState>, ct_dispatch: Dispatch<CurrentTransfer>) -> Callback<MouseEvent> { pub fn transfer_select_callback(
main_state: Rc<MainState>,
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| { Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target(); let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok()); let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());

View File

@ -43,6 +43,11 @@ pub fn MainWindow() -> Html {
main_window_callbacks::toggle_in_transfer_hashes_callback(main_dispatch) main_window_callbacks::toggle_in_transfer_hashes_callback(main_dispatch)
}; };
let toggle_volume_heatmap_callback = {
let main_dispatch = main_dispatch.clone();
main_window_callbacks::toggle_volume_heatmap_callback(main_dispatch)
};
let new_plate_dialog_is_open = use_state_eq(|| false); let new_plate_dialog_is_open = use_state_eq(|| false);
let new_plate_dialog_callback = let new_plate_dialog_callback =
main_window_callbacks::new_plate_dialog_callback(new_plate_dialog_is_open.clone()); main_window_callbacks::new_plate_dialog_callback(new_plate_dialog_is_open.clone());
@ -98,6 +103,7 @@ pub fn MainWindow() -> Html {
<button>{"Styles"}</button> <button>{"Styles"}</button>
<div> <div>
<button onclick={toggle_in_transfer_hashes_callback}>{"Toggle transfer hashes"}</button> <button onclick={toggle_in_transfer_hashes_callback}>{"Toggle transfer hashes"}</button>
<button onclick={toggle_volume_heatmap_callback}>{"Toggle volume heatmap"}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -40,26 +40,40 @@ pub fn Plate(props: &PlateProps) -> Html {
m_end_handle.set(Some(pt2)); m_end_handle.set(Some(pt2));
} }
let tooltip_map = { let tooltip_map: HashMap<(u8, u8), Vec<&Transfer>>;
let volume_map: HashMap<(u8, u8), f32>;
let volume_max: f32;
{
let transfers = main_state.transfers.iter().filter(|t| match props.ptype { let transfers = main_state.transfers.iter().filter(|t| match props.ptype {
PlateType::Source => t.source_id == props.source_plate.get_uuid(), PlateType::Source => t.source_id == props.source_plate.get_uuid(),
PlateType::Destination => t.dest_id == props.destination_plate.get_uuid(), PlateType::Destination => t.dest_id == props.destination_plate.get_uuid(),
}); });
let mut tooltip_map: HashMap<(u8, u8), Vec<&Transfer>> = HashMap::new(); let mut tooltip_map_temp: HashMap<(u8, u8), Vec<&Transfer>> = HashMap::new();
let mut volume_map_temp: HashMap<(u8,u8), f32> = HashMap::new();
let mut volume_max_temp: f32 = f32::NEG_INFINITY;
for transfer in transfers { for transfer in transfers {
let wells = match props.ptype { let wells = match props.ptype {
PlateType::Source => transfer.transfer_region.get_source_wells(), PlateType::Source => transfer.transfer_region.get_source_wells(),
PlateType::Destination => transfer.transfer_region.get_destination_wells(), PlateType::Destination => transfer.transfer_region.get_destination_wells(),
}; };
for well in wells { for well in wells {
if let Some(val) = tooltip_map.get_mut(&well) { if let Some(val) = tooltip_map_temp.get_mut(&well) {
val.push(transfer); val.push(transfer);
} else { } else {
tooltip_map.insert(well, vec![transfer]); tooltip_map_temp.insert(well, vec![transfer]);
} }
if let Some(val) = volume_map_temp.get_mut(&well) {
*val += transfer.volume;
} else {
log::info!("well: {:?}, vol: {:?}", well, transfer.volume);
volume_map_temp.insert(well, transfer.volume);
}
volume_max_temp = f32::max(volume_max_temp, transfer.volume);
} }
} }
tooltip_map tooltip_map = tooltip_map_temp;
volume_map = volume_map_temp;
volume_max = volume_max_temp;
}; };
let wells = match props.ptype { let wells = match props.ptype {
@ -120,18 +134,39 @@ pub fn Plate(props: &PlateProps) -> Html {
let row_header = html! {<th>{num_to_letters(i)}</th>}; let row_header = html! {<th>{num_to_letters(i)}</th>};
let row = (1..=width) let row = (1..=width)
.map(|j| { .map(|j| {
let color = {
if !main_state.preferences.volume_heatmap {
tooltip_map.get(&(i,j))
.and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
} else {
volume_map.get(&(i,j))
.map(|t| PALETTE.get_linear(*t as f64, volume_max as f64))
}
};
let title = {
let mut out = String::new();
let used_by = tooltip_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
.collect::<Vec<_>>().join(", ")));
if let Some(val) = used_by {
out += &val;
}
let volume_sum = volume_map.get(&(i,j))
.map(|t| format!("Volume: {}", t));
if let Some(val) = volume_sum {
if !out.is_empty() { out += "\n" }
out += &val;
}
out
};
html! { html! {
<PlateCell i={i} j={j} <PlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))} selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()} mouse={mouse_callback.clone()}
in_transfer={wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes} in_transfer={wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes}
color={tooltip_map.get(&(i,j)) color={ color }
.and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
}
cell_height={props.cell_height} cell_height={props.cell_height}
title={tooltip_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone()) title={title}
.collect::<Vec<_>>().join(", ")))}
/> />
} }
}) })

View File

@ -26,7 +26,7 @@ impl ColorPalette {
] ]
} }
pub fn _get_u8(&self, t: u8) -> [f64; 3] { fn get_u8(&self, t: u8) -> [f64; 3] {
assert!(t > 0, "t must be greater than zero!"); assert!(t > 0, "t must be greater than zero!");
self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64) self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64)
} }
@ -43,6 +43,11 @@ impl ColorPalette {
self.get(Self::space_evenly(index)) self.get(Self::space_evenly(index))
} }
pub fn get_linear(&self, t: f64, max: f64) -> [f64; 3] {
let scaled = t / max;
self.get(scaled)
}
fn space_evenly(x: usize) -> f64 { fn space_evenly(x: usize) -> f64 {
let e: usize = (x.ilog2() + 1) as usize; let e: usize = (x.ilog2() + 1) as usize;
let d: usize = 2usize.pow(e as u32); let d: usize = 2usize.pow(e as u32);

View File

@ -15,12 +15,15 @@ pub struct CurrentTransfer {
#[derive(PartialEq, Clone, Copy, Serialize, Deserialize)] #[derive(PartialEq, Clone, Copy, Serialize, Deserialize)]
pub struct Preferences { pub struct Preferences {
#[serde(default)]
pub in_transfer_hashes: bool, pub in_transfer_hashes: bool,
#[serde(default)]
pub volume_heatmap: bool,
} }
impl Default for Preferences { impl Default for Preferences {
fn default() -> Self { fn default() -> Self {
Self { in_transfer_hashes: true } Self { in_transfer_hashes: true, volume_heatmap: false }
} }
} }

View File

@ -181,6 +181,11 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
move |_| dialog_close_callback.emit(()) move |_| dialog_close_callback.emit(())
}; };
let onclose_secondary = {
let dialog_close_callback = props.dialog_close_callback.clone();
move |_| dialog_close_callback.emit(())
};
let rename_onchange = tree_callbacks::rename_onchange(props.id, main_dispatch.clone()); let rename_onchange = tree_callbacks::rename_onchange(props.id, main_dispatch.clone());
let format_onchange = tree_callbacks::format_onchange(props.id, main_dispatch.clone()); let format_onchange = tree_callbacks::format_onchange(props.id, main_dispatch.clone());
@ -209,6 +214,11 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
); );
} }
let merge_button_callback = {
let main_dispatch = main_dispatch.clone();
tree_callbacks::merge_button_callback(main_dispatch, props.id)
};
html! { html! {
<dialog ref={dialog_ref} class="dialog" onclose={onclose}> <dialog ref={dialog_ref} class="dialog" onclose={onclose}>
<h2>{"Plate Info"}</h2> <h2>{"Plate Info"}</h2>
@ -219,6 +229,7 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
{ for plate_format_options } { for plate_format_options }
</select> </select>
} }
<button onclick={merge_button_callback} onclick={onclose_secondary}>{"Merge"}</button>
<form class="modal_close" method="dialog"><button /></form> <form class="modal_close" method="dialog"><button /></form>
</dialog> </dialog>
} }