1use std::cell::{Cell, RefCell};
4use std::rc::Rc;
5
6use ravenrs_app::{Application, EntityInfo, ModelError, Object, ObjectSpace, Value, ViewInfo};
7use ravenrs_layout::Align;
8use ravenrs_reactive::{Binding, State};
9use ravenrs_widgets::{
10 AnyView, BuildCx, Button, ButtonStyle, Dialog, IconName, Keyed, NodeId, ScrollView, VStack, View,
11};
12
13use crate::session::{FormCaption, Session, SpaceFactory, Words};
14use crate::{DetailView, EditorRegistry, SystemModule};
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18#[non_exhaustive]
19#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
20pub struct FormText {
21 pub title: String,
23 pub description: Option<String>,
25 pub save: String,
27}
28
29#[cfg_attr(feature = "live", ravenrs_live::expose)]
30impl FormText {
31 pub fn new(title: impl Into<String>, save: impl Into<String>) -> Self {
33 Self { title: title.into(), description: None, save: save.into() }
34 }
35
36 pub fn description(mut self, text: impl Into<String>) -> Self {
38 self.description = Some(text.into());
39 self
40 }
41}
42
43type SaveHandler = Rc<dyn Fn(&Object, bool) -> bool>;
44
45struct FormInner {
46 session: Session,
47 entity: EntityInfo,
48 view: ViewInfo,
49 open: State<bool>,
50 text: State<FormText>,
51 on_save: RefCell<Option<SaveHandler>>,
52 width: Cell<f32>,
53 height: Cell<Option<f32>>,
54}
55
56#[derive(Clone)]
59#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
60pub struct EntityForm(Rc<FormInner>);
61
62impl std::fmt::Debug for EntityForm {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("EntityForm").field("entity", &self.0.entity.name).finish_non_exhaustive()
65 }
66}
67
68#[cfg_attr(feature = "live", ravenrs_live::expose)]
69impl EntityForm {
70 pub fn new<S: ObjectSpace + 'static>(
72 entity: &str,
73 entities: Vec<EntityInfo>,
74 space: impl Fn() -> S + 'static,
75 words: Words,
76 editors: EditorRegistry,
77 ) -> Result<Self, ModelError> {
78 let application = entities
79 .iter()
80 .cloned()
81 .fold(
82 Application::builder(entity).module(SystemModule::with_words(words.clone())),
83 ravenrs_app::ApplicationBuilder::entity,
84 )
85 .build()?;
86 let info = entities
87 .iter()
88 .find(|found| found.name == entity)
89 .cloned()
90 .ok_or_else(|| ModelError(format!("there is no entity {entity}")))?;
91 let view = ViewInfo::from_model(&application.model(), &format!("{entity}_DetailView"))?;
92 let factory: SpaceFactory =
93 Rc::new(move || Rc::new(RefCell::new(space())) as Rc<RefCell<dyn ObjectSpace>>);
94 let session = Session::with_editors(application, factory, words, editors);
95 session.set_lists_field_errors(false);
96 Ok(Self(Rc::new(FormInner {
97 session,
98 entity: info,
99 view,
100 open: State::new(false),
101 text: State::new(FormText::new("", "")),
102 on_save: RefCell::default(),
103 width: Cell::new(440.0),
104 height: Cell::new(None),
105 })))
106 }
107
108 pub fn on_save(self, handler: impl Fn(&Object, bool) -> bool + 'static) -> Self {
110 *self.0.on_save.borrow_mut() = Some(Rc::new(handler));
111 self
112 }
113
114 pub fn width(self, width: f32) -> Self {
116 self.0.width.set(width);
117 self
118 }
119
120 pub fn scrolling_height(self, height: f32) -> Self {
122 self.0.height.set(Some(height));
123 self
124 }
125
126 pub fn captions(self, caption: FormCaption) -> Self {
128 self.0.session.set_form_caption(caption);
129 self
130 }
131
132 pub fn session(&self) -> &Session {
134 &self.0.session
135 }
136
137 pub fn start_new(&self, text: FormText, fill: impl FnOnce(&mut Object)) {
139 let mut object = self.0.entity.new_object();
140 fill(&mut object);
141 self.open_on(text, object, true);
142 }
143
144 pub fn start_edit(&self, text: FormText, key: &Value, fill: impl FnOnce(&mut Object)) {
146 let found = self.0.session.frame().space().borrow().find(&self.0.entity.name, key);
147 if let Ok(Some(mut object)) = found {
148 fill(&mut object);
149 self.open_on(text, object, false);
150 }
151 }
152
153 fn open_on(&self, text: FormText, object: Object, is_new: bool) {
154 self.0.session.open_object(&self.0.entity, object, is_new);
155 self.0.text.set(text);
156 self.0.open.set(true);
157 }
158
159 pub fn set(&self, field: &str, value: impl Into<Value>) {
161 self.0.session.set_form_value(field, value.into());
162 }
163
164 pub fn is_open(&self) -> bool {
166 *self.0.open.peek()
167 }
168
169 pub fn close(&self) {
171 self.0.open.set(false);
172 }
173
174 pub fn save(&self) {
176 if !self.is_open() {
178 return;
179 }
180 let Some((object, is_new)) = self.0.session.check_form() else { return };
181 let handler = self.0.on_save.borrow().clone();
182 if handler.is_none_or(|handler| handler(&object, is_new)) {
183 self.close();
184 }
185 }
186}
187
188impl View for EntityForm {
189 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
190 let inner = &self.0;
191 if !inner.open.get() {
192 return cx.empty();
193 }
194 let text = inner.text.get();
195 let form = inner.session.form().map_or(0, |form| Rc::as_ptr(&form) as usize as u64);
196 let fields = Keyed::new(form, DetailView::new(inner.session.clone(), inner.view.clone()));
197 let content: AnyView = match inner.height.get() {
198 Some(height) => Box::new(
199 ScrollView::new((VStack::new((fields,)).align(Align::Stretch),))
200 .height(height)
201 .scrollbar_outside(14.0),
202 ),
203 None => Box::new(fields),
204 };
205 let open = inner.open.clone();
206 let (read, write) = (open.clone(), open.clone());
207 let binding = Binding::new(move || read.get(), move |value| write.set(value));
208 let saving = self.clone();
209 let mut dialog = Dialog::new(binding, text.title.clone())
210 .width(inner.width.get())
211 .content(content)
212 .action(
213 Button::new(inner.session.words().cancel.clone())
214 .style(ButtonStyle::Secondary)
215 .on_click(move || open.set(false)),
216 )
217 .action(Button::new(text.save.clone()).icon(IconName::Check).on_click(move || saving.save()));
218 if let Some(description) = text.description.clone() {
219 dialog = dialog.description(description);
220 }
221 cx.child(&dialog)
222 }
223}