dyna3/app-proto/src/components/outline.rs
Aaron Fenyes 27edbfb010 Streamline axis naming
This makes it simpler, from the programmer's perspective, to get the
name of an axis as a string slice and to format an axis name into a
string. To me, the matching method `Axis::name` seems more direct than
the explicit lookup table that it replaces, and I'm hoping that it will
be about as easy for the compiler to inline, or even easier.

Implementing `Display` enables us to hand an `Axis` to a string
formatter without any explicit conversion. It adds extra code in the
short run, but I'd expect it to simplify our code in the long run by
fitting into the conventions set by the Rust standard library.
2025-10-07 15:36:12 -07:00

275 lines
No EOL
9.4 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,
PointCoordinateRegulator,
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")
}
}
}
}
impl OutlineItem for PointCoordinateRegulator {
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
let name = format!("{} coordinate", self.axis);
view! {
li(class = "regulator") {
div(class = "regulator-label") // for spacing
div(class = "regulator-type") { (name) }
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::<Vec<_>>()
);
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) }
input(
r#type = "checkbox",
bind:checked = element.ghost(),
on:click = |event: MouseEvent| event.stop_propagation()
)
}
}
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::<Vec<_>>()
);
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()
)
}
}
}