tilo_app/framework/
form.rs1use std::any::Any;
9use std::collections::BTreeMap;
10use std::marker::PhantomData;
11use std::sync::{Arc, Mutex, PoisonError};
12
13use serde::Serialize;
14use tilo_actions::ActionBinding;
15use tilo_resource::{Invoke, Params, Path, Resource, Segment};
16use ui_core::LocalState;
17
18use super::controls::Actions;
19use super::dispatch::Submit;
20
21trait Slot: Send + Sync {
23 fn json(&self) -> Result<serde_json::Value, String>;
24 fn clear(&self);
25 fn any(&self) -> &dyn Any;
26}
27
28struct Typed<V: Send + Sync + 'static>(LocalState<V>);
29
30impl<V> Slot for Typed<V>
31where
32 V: Serialize + Default + Send + Sync + 'static,
33{
34 fn json(&self) -> Result<serde_json::Value, String> {
35 self.0
36 .with(|value| serde_json::to_value(value))
37 .map_err(|error| error.to_string())
38 }
39
40 fn clear(&self) {
41 self.0.set(V::default());
42 }
43
44 fn any(&self) -> &dyn Any {
45 &self.0
46 }
47}
48
49type Slots = Arc<Mutex<BTreeMap<String, Box<dyn Slot>>>>;
50
51pub struct Form<P: Params> {
54 slots: Slots,
55 marker: PhantomData<fn() -> P>,
56}
57
58impl<P: Params> Clone for Form<P> {
59 fn clone(&self) -> Self {
60 Self {
61 slots: Arc::clone(&self.slots),
62 marker: PhantomData,
63 }
64 }
65}
66
67impl<P: Params> Form<P> {
68 pub(crate) fn new() -> Self {
69 Self {
70 slots: Arc::default(),
71 marker: PhantomData,
72 }
73 }
74
75 pub(crate) fn field<V>(&self, path: &Path<P, V>) -> LocalState<V>
77 where
78 V: Serialize + Default + Send + Sync + 'static,
79 {
80 let key = match path.segments() {
81 [Segment::Field(key)] => key.clone(),
82 other => other
83 .iter()
84 .map(ToString::to_string)
85 .collect::<Vec<_>>()
86 .join("."),
87 };
88 let mut slots = self.slots.lock().unwrap_or_else(PoisonError::into_inner);
89 if let Some(state) = slots
90 .get(&key)
91 .and_then(|slot| slot.any().downcast_ref::<LocalState<V>>())
92 {
93 return state.clone();
94 }
95 let state = LocalState::new(V::default());
96 slots.insert(key, Box::new(Typed(state.clone())));
97 state
98 }
99
100 #[must_use]
103 pub fn paths(&self) -> P::Paths {
104 P::paths()
105 }
106
107 pub fn values(&self) -> Result<P, String> {
112 read::<P>(&self.slots)
113 }
114
115 pub fn clear(&self) {
117 clear(&self.slots);
118 }
119
120 #[must_use]
124 pub fn submit(&self) -> FormSubmit<P> {
125 FormSubmit {
126 slots: Arc::clone(&self.slots),
127 marker: PhantomData,
128 }
129 }
130}
131
132fn read<P: Params>(slots: &Slots) -> Result<P, String> {
133 let fields = slots
134 .lock()
135 .unwrap_or_else(PoisonError::into_inner)
136 .iter()
137 .map(|(key, slot)| Ok((key.clone(), slot.json()?)))
138 .collect::<Result<serde_json::Map<_, _>, String>>()?;
139 serde_json::from_value(serde_json::Value::Object(fields)).map_err(|error| error.to_string())
140}
141
142fn clear(slots: &Slots) {
143 for slot in slots
144 .lock()
145 .unwrap_or_else(PoisonError::into_inner)
146 .values()
147 {
148 slot.clear();
149 }
150}
151
152pub struct FormSubmit<P: Params> {
154 slots: Slots,
155 marker: PhantomData<fn() -> P>,
156}
157
158impl<P> Submit<P::Resource> for FormSubmit<P>
159where
160 P: Params,
161 P::Resource: Resource,
162{
163 fn action_id(&self) -> &'static str {
164 P::ACTION.id
165 }
166
167 fn into_invoke(self) -> Result<Invoke<P::Resource>, String> {
168 let params = read::<P>(&self.slots)?;
169 clear(&self.slots);
170 Ok(P::ACTION.with(params))
171 }
172
173 fn control_binding(self, actions: &Actions<P::Resource>) -> Option<ActionBinding<()>> {
174 let slots = self.slots;
175 actions.deferred_binding(P::ACTION.id, move || {
176 let params = read::<P>(&slots)?;
177 let arguments = P::ACTION.arguments(¶ms)?;
178 clear(&slots);
179 Ok(arguments)
180 })
181 }
182}
183
184pub enum Attached<V> {
187 Record {
189 id: String,
190 state: LocalState<V>,
191 binding: Option<Box<ActionBinding<V>>>,
192 },
193 Local { id: String, state: LocalState<V> },
195}
196
197#[doc(hidden)]
199pub enum RecordField {}
200
201#[doc(hidden)]
203pub enum FormField {}
204
205#[derive(Clone, Default)]
207pub(crate) struct Forms(
208 std::rc::Rc<std::cell::RefCell<std::collections::HashMap<std::any::TypeId, Box<dyn Any>>>>,
209);
210
211impl Forms {
212 pub(crate) fn get<P: Params>(&self) -> Form<P> {
213 let key = std::any::TypeId::of::<P>();
214 let mut forms = self.0.borrow_mut();
215 if let Some(form) = forms
216 .get(&key)
217 .and_then(|form| form.downcast_ref::<Form<P>>())
218 {
219 return form.clone();
220 }
221 let form = Form::<P>::new();
222 forms.insert(key, Box::new(form.clone()));
223 form
224 }
225}