Skip to main content

ravenrs_app_ui/
model_editor.rs

1//! The application's model as a tree, whose settings a person who can may change, in their own
2//! layer. It is kept simple on purpose: a tree with its settings beside it, one at a time.
3
4use 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/// The application's model, for whoever may change it.
27#[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    /// Over the model of this session.
46    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}