use itertools::Itertools; use std::rc::Rc; use sycamore::prelude::*; use web_sys::{ KeyboardEvent, MouseEvent, wasm_bindgen::JsCast }; use crate::{ AppState, assembly::{ Element, HalfCurvatureRegulator, InversiveDistanceRegulator, Regulator, RegulatorKey }, specified::SpecifiedValue }; // an editable view of a regulator #[component(inline_props)] fn RegulatorInput(regulator: Rc) -> View { // get the regulator's measurement and set point signals let measurement = regulator.measurement(); let set_point = regulator.set_point(); // the `valid` signal tracks whether the last entered value is a valid set // point specification let valid = create_signal(true); // the `value` signal holds the current set point specification let value = create_signal( set_point.with_untracked(|set_pt| set_pt.spec.clone()) ); // this `reset_value` closure resets the input value to the regulator's set // point specification let reset_value = move || { batch(|| { valid.set(true); value.set(set_point.with(|set_pt| set_pt.spec.clone())); }) }; // reset the input value whenever the regulator's set point specification // is updated create_effect(reset_value); view! { input( r#type="text", class=move || { if valid.get() { set_point.with(|set_pt| { if set_pt.is_present() { "regulator-input constraint" } else { "regulator-input" } }) } else { "regulator-input invalid" } }, placeholder=measurement.with(|result| result.to_string()), bind:value=value, on:change=move |_| { valid.set( match SpecifiedValue::try_from(value.get_clone_untracked()) { Ok(set_pt) => { set_point.set(set_pt); true } Err(_) => false } ) }, on:keydown={ move |event: KeyboardEvent| { match event.key().as_str() { "Escape" => reset_value(), _ => () } } } ) } } pub trait OutlineItem { fn outline_item(self: Rc, element: Rc) -> View; } impl OutlineItem for InversiveDistanceRegulator { fn outline_item(self: Rc, element: Rc) -> View { let other_subject_label = if self.subjects[0] == element { self.subjects[1].label() } else { self.subjects[0].label() }.clone(); view! { li(class="regulator") { div(class="regulator-label") { (other_subject_label) } div(class="regulator-type") { "Inversive distance" } RegulatorInput(regulator=self) div(class="status") } } } } impl OutlineItem for HalfCurvatureRegulator { fn outline_item(self: Rc, _element: Rc) -> View { view! { li(class="regulator") { div(class="regulator-label") // for spacing div(class="regulator-type") { "Half-curvature" } RegulatorInput(regulator=self) div(class="status") } } } } // a list item that shows a regulator in an outline view of an element #[component(inline_props)] fn RegulatorOutlineItem(regulator_key: RegulatorKey, element: Rc) -> View { let state = use_context::(); let regulator = state.assembly.regulators.with( |regs| regs[regulator_key].clone() ); regulator.outline_item(element) } // a list item that shows an element in an outline view of an assembly #[component(inline_props)] fn ElementOutlineItem(element: Rc) -> View { let state = use_context::(); let class = { let element_for_class = element.clone(); state.selection.map( move |sel| if sel.contains(&element_for_class) { "selected" } else { "" } ) }; let label = element.label().clone(); let representation = element.representation().clone(); let rep_components = move || { representation.with( |rep| rep.iter().map( |u| { let u_str = format!("{:.3}", u).replace("-", "\u{2212}"); view! { div { (u_str) } } } ).collect::>() ) }; let regulated = element.regulators().map(|regs| regs.len() > 0); let regulator_list = element.regulators().map( move |elt_reg_keys| elt_reg_keys .clone() .into_iter() .sorted_by_key( |®_key| state.assembly.regulators.with( |regs| regs[reg_key].subjects().len() ) ) .collect() ); let details_node = create_node_ref(); view! { li { details(ref=details_node) { summary( class=class.get(), on:keydown={ let element_for_handler = element.clone(); move |event: KeyboardEvent| { match event.key().as_str() { "Enter" => { state.select(&element_for_handler, event.shift_key()); event.prevent_default(); }, "ArrowRight" if regulated.get() => { let _ = details_node .get() .unchecked_into::() .set_attribute("open", ""); }, "ArrowLeft" => { let _ = details_node .get() .unchecked_into::() .remove_attribute("open"); }, _ => () } } } ) { div( class="element-switch", on:click=|event: MouseEvent| event.stop_propagation() ) div( class="element", on:click={ let state_for_handler = state.clone(); let element_for_handler = element.clone(); move |event: MouseEvent| { state_for_handler.select(&element_for_handler, event.shift_key()); event.stop_propagation(); event.prevent_default(); } } ) { div(class="element-label") { (label) } div(class="element-representation") { (rep_components) } div(class="status") } } ul(class="regulators") { Keyed( list=regulator_list, view=move |reg_key| { let element_for_view = element.clone(); view! { RegulatorOutlineItem( regulator_key=reg_key, element=element_for_view ) } }, key=|reg_key| reg_key.clone() ) } } } } } // a component that lists the elements of the current assembly, showing each // element's regulators in a collapsible sub-list. its implementation is based // on Kate Morley's HTML + CSS tree views: // // https://iamkate.com/code/tree-views/ // #[component] pub fn Outline() -> View { let state = use_context::(); // list the elements alphabetically by ID let element_list = state.assembly.elements.map( |elts| elts .clone() .into_iter() .sorted_by_key(|(_, elt)| elt.id().clone()) .collect() ); view! { ul( id="outline", on:click={ let state = use_context::(); move |_| state.selection.update(|sel| sel.clear()) } ) { Keyed( list=element_list, view=|(_, elt)| view! { ElementOutlineItem(element=elt) }, key=|(_, elt)| elt.serial() ) } } }