1use std::collections::BTreeSet;
5
6use ravenrs_app::ModelNode;
7use ravenrs_layout::Align;
8use ravenrs_reactive::{Binding, State};
9use ravenrs_style::{Semantic, TextStyle};
10use ravenrs_widgets::{
11 Alert, AnyView, BuildCx, Button, ButtonSize, ButtonStyle, FormField, HStack, NodeId, ScrollView, Text,
12 TextField, Tone, TreeNode, TreeView, VStack, View, ViewExt,
13};
14
15use crate::Session;
16
17const TREE_HEIGHT: f32 = 520.0;
18const SEPARATOR: char = '/';
19
20fn tree_of(node: &ModelNode, path: &str) -> TreeNode {
21 let key = if path.is_empty() { node.id.clone() } else { format!("{path}{SEPARATOR}{}", node.id) };
22 let children = node.children.iter().map(|child| tree_of(child, &key));
23 TreeNode::new(key.clone(), node.id.clone()).children(children.collect::<Vec<_>>())
24}
25
26#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
28pub struct ModelEditor {
29 session: Session,
30 selected: State<Option<String>>,
31 expanded: State<BTreeSet<String>>,
32 search: State<String>,
33 drafts: State<std::collections::BTreeMap<String, String>>,
34 error: State<Option<String>>,
35}
36
37impl std::fmt::Debug for ModelEditor {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("ModelEditor").finish_non_exhaustive()
40 }
41}
42
43#[cfg_attr(feature = "live", ravenrs_live::expose)]
44impl ModelEditor {
45 pub fn new(session: Session) -> Self {
47 Self {
48 session,
49 selected: State::new(None),
50 expanded: State::new(BTreeSet::new()),
51 search: State::new(String::new()),
52 drafts: State::new(std::collections::BTreeMap::new()),
53 error: State::new(None),
54 }
55 }
56
57 fn settings(&self, path: &str) -> AnyView {
58 let model = self.session.model();
59 let ids: Vec<&str> = path.split(SEPARATOR).collect();
60 let Some(node) = model.at(&ids) else { return Box::new(Text::new(path.to_owned())) };
61 let words = self.session.words();
62 let mut fields: Vec<AnyView> = Vec::new();
63 for name in node.properties.keys() {
64 let (slot, stored) = (format!("{path}{SEPARATOR}{name}"), node.typed(name).unwrap_or_default());
65 let (read, write) = (self.drafts.clone(), self.drafts.clone());
66 let (reading, writing, shown) = (slot.clone(), slot, stored);
67 let binding = Binding::new(
68 move || read.get().get(&reading).cloned().unwrap_or_else(|| shown.clone()),
69 move |text: String| {
70 let mut drafts = write.peek().clone();
71 drafts.insert(writing.clone(), text);
72 write.set(drafts);
73 },
74 );
75 fields.push(Box::new(FormField::new(name.clone(), TextField::new(binding))));
76 }
77 let (session, error, drafts) = (self.session.clone(), self.error.clone(), self.drafts.clone());
78 let target = path.to_owned();
79 let apply = Button::new(words.apply.clone()).on_click(move || {
80 let prefix = format!("{target}{SEPARATOR}");
81 let waiting = drafts.peek().clone();
82 let ids: Vec<&str> = target.split(SEPARATOR).collect();
83 let mut problem = None;
84 for (slot, text) in &waiting {
85 if let Some(name) = slot.strip_prefix(&prefix).filter(|name| !name.contains(SEPARATOR))
86 && let Err(why) = session.set_model_setting(&ids, name, text)
87 {
88 problem = Some(why);
89 }
90 }
91 error.set(problem);
92 drafts.set(std::collections::BTreeMap::new());
93 });
94 let (session, error, drafts) = (self.session.clone(), self.error.clone(), self.drafts.clone());
95 let restore = Button::new(words.restore_model.clone())
96 .style(ButtonStyle::Ghost)
97 .size(ButtonSize::Small)
98 .on_click(move || {
99 error.set(session.restore_model_settings().err());
100 drafts.set(std::collections::BTreeMap::new());
101 });
102 Box::new(
103 VStack::new((
104 Text::new(path.to_owned()).style(TextStyle::Caption),
105 VStack::new(fields).spacing(8.0).align(Align::Stretch),
106 HStack::new(((!node.properties.is_empty()).then_some(apply), restore))
107 .spacing(8.0)
108 .align(Align::Center),
109 ))
110 .spacing(12.0)
111 .align(Align::Stretch),
112 )
113 }
114}
115
116impl View for ModelEditor {
117 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
118 let words = self.session.words();
119 let allowed = self
120 .session
121 .frame()
122 .security()
123 .is_some_and(|security| security.with(ravenrs_app::Permissions::can_edit_model));
124 if !allowed {
125 return cx.child(
126 &Alert::new(words.model_editor.clone(), words.access_denied.clone()).tone(Tone::Danger),
127 );
128 }
129 let model = self.session.model();
130 let nodes: Vec<TreeNode> = model.children.iter().map(|node| tree_of(node, "")).collect();
131 let (read, write) = (self.selected.clone(), self.selected.clone());
132 let selected = Binding::new(move || read.get(), move |key| write.set(key));
133 let (read, write) = (self.expanded.clone(), self.expanded.clone());
134 let expanded = Binding::new(move || read.get(), move |keys| write.set(keys));
135 let (read, write) = (self.search.clone(), self.search.clone());
136 let search = Binding::new(move || read.get(), move |text| write.set(text));
137 let tree = TreeView::new(nodes)
138 .selected(selected)
139 .expanded(expanded)
140 .filter(self.search.get())
141 .height(TREE_HEIGHT)
142 .label(words.model_editor.clone());
143 let side: AnyView = match self.selected.get() {
144 Some(path) => self.settings(&path),
145 None => Box::new(
146 Text::new(words.pick_model_node.clone())
147 .style(TextStyle::Callout)
148 .foreground(Semantic::InkMuted),
149 ),
150 };
151 let content = VStack::new((
152 self.error.get().map(|why| Alert::new(words.model_editor.clone(), why).tone(Tone::Danger)),
153 HStack::new((
154 VStack::new((TextField::new(search).placeholder(words.search_model.clone()), tree))
155 .spacing(8.0)
156 .align(Align::Stretch)
157 .grow(1.0),
158 ScrollView::new((side,)).grow(1.0),
159 ))
160 .spacing(16.0)
161 .align(Align::Stretch),
162 ))
163 .spacing(12.0)
164 .align(Align::Stretch);
165 cx.child(&content)
166 }
167}