#![allow(non_snake_case)] use lazy_static::lazy_static; use regex::Regex; use serde::{Deserialize, Serialize}; use wasm_bindgen::JsCast; use web_sys::{EventTarget, HtmlInputElement}; use uuid::Uuid; use yew::prelude::*; use yewdux::prelude::*; use crate::data::{transfer::Transfer, transfer_region::Region}; use super::states::{CurrentTransfer, MainState}; #[function_component] pub fn TransferMenu() -> Html { let (main_state, main_dispatch) = use_store::(); let (ct_state, ct_dispatch) = use_store::(); let on_name_change = { let ct_dispatch = ct_dispatch.clone(); Callback::from(move |e: Event| { let target: Option = e.target(); let input = target.and_then(|t| t.dyn_into::().ok()); if let Some(input) = input { ct_dispatch.reduce_mut(|state| { state.transfer.name = input.value().clone(); }); } } )}; let on_src_region_change = { let ct_dispatch = ct_dispatch.clone(); Callback::from(move |e: Event| { let target: Option = e.target(); let input = target.and_then(|t| t.dyn_into::().ok()); if let Some(input) = input { if let Ok(rd) = RegionDisplay::try_from(input.value()) { ct_dispatch.reduce_mut(|state| { state.transfer.transfer_region.source_region = Region::from(&rd); }); input.set_custom_validity(""); } else { input.set_custom_validity("Invalid region.") } } }) }; let on_dest_region_change = { let ct_dispatch = ct_dispatch.clone(); Callback::from(move |e: Event| { let target: Option = e.target(); let input = target.and_then(|t| t.dyn_into::().ok()); if let Some(input) = input { if let Ok(rd) = RegionDisplay::try_from(input.value()) { ct_dispatch.reduce_mut(|state| { state.transfer.transfer_region.dest_region = Region::from(&rd); }); input.set_custom_validity(""); } else { input.set_custom_validity("Invalid region.") } } }) }; let on_source_interleave_x_change = { let ct_dispatch = ct_dispatch.clone(); Callback::from(move |e: Event| { let target: Option = e.target(); let input = target.and_then(|t| t.dyn_into::().ok()); if let Some(input) = input { if let Ok(num) = input.value().parse::() { ct_dispatch.reduce_mut(|state| { state.transfer.transfer_region.interleave_source = (num, state.transfer.transfer_region.interleave_source.1); }); } } }) }; let on_source_interleave_y_change = { let ct_dispatch = ct_dispatch.clone(); Callback::from(move |e: Event| { let target: Option = e.target(); let input = target.and_then(|t| t.dyn_into::().ok()); if let Some(input) = input { if let Ok(num) = input.value().parse::() { ct_dispatch.reduce_mut(|state| { state.transfer.transfer_region.interleave_source = (state.transfer.transfer_region.interleave_source.0, num); }); } } }) }; let on_dest_interleave_x_change = { let ct_dispatch = ct_dispatch.clone(); Callback::from(move |e: Event| { let target: Option = e.target(); let input = target.and_then(|t| t.dyn_into::().ok()); if let Some(input) = input { if let Ok(num) = input.value().parse::() { ct_dispatch.reduce_mut(|state| { state.transfer.transfer_region.interleave_dest = (num, state.transfer.transfer_region.interleave_dest.1); }); } } }) }; let on_dest_interleave_y_change = { let ct_dispatch = ct_dispatch.clone(); Callback::from(move |e: Event| { let target: Option = e.target(); let input = target.and_then(|t| t.dyn_into::().ok()); if let Some(input) = input { if let Ok(num) = input.value().parse::() { ct_dispatch.reduce_mut(|state| { state.transfer.transfer_region.interleave_dest = (state.transfer.transfer_region.interleave_dest.0, num); }); } } }) }; let new_transfer_button_callback = { let main_dispatch = main_dispatch.clone(); let main_state = main_state.clone(); let ct_dispatch = ct_dispatch.clone(); Callback::from(move |_: MouseEvent| { main_dispatch.reduce_mut(|state| { state.selected_transfer = Uuid::nil(); }); ct_dispatch.reduce_mut(|state| { state.transfer = Transfer::default(); state.transfer.source_id = main_state.selected_source_plate; state.transfer.dest_id = main_state.selected_dest_plate; }); }) }; let save_transfer_button_callback = { let main_dispatch = main_dispatch.clone(); let main_state = main_state.clone(); let ct_state = ct_state.clone(); Callback::from(move |_: MouseEvent| { log::debug!("Button pressed"); if main_state.selected_transfer.is_nil() { if let Some(spi) = main_state .source_plates .iter() .find(|spi| spi.get_uuid() == main_state.selected_source_plate) { if let Some(dpi) = main_state .destination_plates .iter() .find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate) { let new_transfer = Transfer::new( spi.clone(), dpi.clone(), ct_state.transfer.transfer_region, ct_state.transfer.name.clone() ); main_dispatch.reduce_mut(|state| state.transfers.push(new_transfer)); } } } else { if let Some(index) = main_state.transfers.iter() .position(|t| t.get_uuid() == main_state.selected_transfer) { main_dispatch.reduce_mut(|state| { state.transfers[index] = ct_state.transfer.clone(); }); } } }) }; let delete_transfer_button_callback = { let main_dispatch = main_dispatch.clone(); let main_state = main_state.clone(); let ct_state = ct_state.clone(); let new_callback = new_transfer_button_callback.clone(); Callback::from(move |e: MouseEvent| { if main_state.selected_transfer.is_nil() { () // Maybe reset transfer? } else { if let Some(index) = main_state.transfers.iter() .position(|t| t.get_uuid() == ct_state.transfer.get_uuid()) { main_dispatch.reduce_mut(|state| { state.transfers.remove(index); state.selected_transfer = Uuid::nil(); }); new_callback.emit(e); // We need a new transfer now } } }) }; html! {
{"Source Interleave "}
{"Destination Interleave "}
} } #[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)] pub struct RegionDisplay { pub text: String, pub col_start: u8, pub row_start: u8, pub col_end: u8, pub row_end: u8, } impl TryFrom for RegionDisplay { type Error = &'static str; fn try_from(value: String) -> Result { lazy_static! { static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap(); } if let Some(captures) = REGION_REGEX.captures(&value) { if captures.len() != 5 { return Err("Not enough capture groups"); } let col_start = letters_to_num(&captures[1]).ok_or("Column start failed to parse")?; let col_end = letters_to_num(&captures[3]).ok_or("Column end failed to parse")?; let row_start: u8 = captures[2] .parse::() .or(Err("Row start failed to parse"))?; let row_end: u8 = captures[4] .parse::() .or(Err("Row end failed to parse"))?; return Ok(RegionDisplay { text: value, col_start, row_start, col_end, row_end, }); } else { return Err("Regex match failed"); } } } impl From<&Region> for RegionDisplay { fn from(value: &Region) -> Self { match *value { Region::Point((col, row)) => { RegionDisplay::try_from((col, row, col, row)).ok().unwrap() } Region::Rect(c1, c2) => RegionDisplay::try_from((c1.0, c1.1, c2.0, c2.1)) .ok() .unwrap(), } } } impl From<&RegionDisplay> for Region { fn from(value: &RegionDisplay) -> Self { if value.col_start == value.col_end && value.row_start == value.row_end { Region::Point((value.col_start, value.row_start)) } else { Region::Rect( (value.col_start, value.row_start), (value.col_end, value.row_end), ) } } } impl TryFrom<(u8, u8, u8, u8)> for RegionDisplay { type Error = &'static str; fn try_from(value: (u8, u8, u8, u8)) -> Result { // (Column Start, Row Start, Column End, Row End) // This can only possibly fail if one of the coordinates is zero... let cs = num_to_letters(value.0).ok_or("Column start failed to parse")?; let ce = num_to_letters(value.2).ok_or("Column end failed to parse")?; Ok(RegionDisplay { text: format!("{}{}:{}{}", cs, value.1, ce, value.3), col_start: value.0, row_start: value.1, col_end: value.2, row_end: value.3, }) } } fn letters_to_num(letters: &str) -> Option { let mut num: u8 = 0; for (i, letter) in letters.chars().rev().enumerate() { let n = letter as u8; if n < 65 || n > 90 { return None; } num = num.checked_add((26_i32.pow(i as u32) * (n as i32 - 64)).try_into().ok()?)?; } return Some(num); } fn num_to_letters(num: u8) -> Option { if num == 0 { return None; } // Otherwise, we will not return none! // As another note, we can't represent higher than "IV" anyway; // thus there's no reason for a loop (26^n with n>1 will NOT occur). let mut text = "".to_string(); let mut digit1 = num.div_euclid(26u8); let mut digit2 = num.rem_euclid(26u8); if digit1 > 0 && digit2 == 0u8 { digit1 -= 1; digit2 = 26; } if digit1 != 0 { text.push((64 + digit1) as char) } text.push((64 + digit2) as char); return Some(text.to_string()); } #[cfg(test)] mod tests { use wasm_bindgen_test::*; use super::{letters_to_num, num_to_letters, RegionDisplay}; #[test] #[wasm_bindgen_test] fn test_letters_to_num() { assert_eq!(letters_to_num("D"), Some(4)); assert_eq!(letters_to_num("d"), None); assert_eq!(letters_to_num("AD"), Some(26 + 4)); assert_eq!(letters_to_num("CG"), Some(3 * 26 + 7)); } #[test] #[wasm_bindgen_test] fn test_num_to_letters() { println!("27 is {:?}", num_to_letters(27)); assert_eq!(num_to_letters(1), Some("A".to_string())); assert_eq!(num_to_letters(26), Some("Z".to_string())); assert_eq!(num_to_letters(27), Some("AA".to_string())); assert_eq!(num_to_letters(111), Some("DG".to_string())); } #[test] #[wasm_bindgen_test] fn test_l2n_and_n2l() { assert_eq!( num_to_letters(letters_to_num("A").unwrap()), Some("A".to_string()) ); assert_eq!( num_to_letters(letters_to_num("BJ").unwrap()), Some("BJ".to_string()) ); for i in 1..=255 { assert_eq!(letters_to_num(&num_to_letters(i as u8).unwrap()), Some(i)); } } #[test] #[wasm_bindgen_test] fn test_try_from_string_for_regiondisplay() { let desired = RegionDisplay { text: "A1:E5".to_string(), row_start: 1, row_end: 5, col_start: 1, col_end: 5, }; assert_eq!(desired, "A1:E5".to_string().try_into().unwrap()); } }