dyna3/app-proto/src/outline.rs
Aaron Fenyes c6b628d424 Store elements and regulators without keys
Since we're no longer using storage keys to refer to elements and
regulators, we don't need to store these items in keyed collections
anymore.

To keep element and regulator pointers in `BTreeSet` collections, we
implement `Ord` for `Serial` trait objects. As a bonus, this lets us
turn the element-wise regulator collections back into `BTreeSet`.
2025-05-04 12:32:37 -07:00

260 lines
No EOL
8.6 KiB
Rust

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
},
specified::SpecifiedValue
};
// an editable view of a regulator
#[component(inline_props)]
fn RegulatorInput(regulator: Rc<dyn Regulator>) -> 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<Self>, element: &Rc<dyn Element>) -> View;
}
impl OutlineItem for InversiveDistanceRegulator {
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View {
let other_subject_label = if self.subjects[0] == element.clone() {
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<Self>, _element: &Rc<dyn Element>) -> 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 an element in an outline view of an assembly
#[component(inline_props)]
fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
let state = use_context::<AppState>();
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::<Vec<_>>()
)
};
let regulated = element.regulators().map(|regs| regs.len() > 0);
let regulator_list = element.regulators().map(
|regs| regs
.clone()
.into_iter()
.sorted_by_key(|reg| reg.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::<web_sys::Element>()
.set_attribute("open", "");
},
"ArrowLeft" => {
let _ = details_node
.get()
.unchecked_into::<web_sys::Element>()
.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| reg.outline_item(&element),
key=|reg| reg.serial()
)
}
}
}
}
}
// 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::<AppState>();
// list the elements alphabetically by ID
/* TO DO */
// this code is designed to generalize easily to other sort keys. if we only
// ever wanted to sort by ID, we could do that more simply using the
// `elements_by_id` index
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::<AppState>();
move |_| state.selection.update(|sel| sel.clear())
}
) {
Keyed(
list=element_list,
view=|elt| view! {
ElementOutlineItem(element=elt)
},
key=|elt| elt.serial()
)
}
}
}