Skip to main content

ravenrs_app_ui/
form.rs

1//! A form in a window, for a record an application keeps in its own way.
2
3use 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/// What the window of a form says.
17#[derive(Clone, Debug, PartialEq, Eq)]
18#[non_exhaustive]
19#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
20pub struct FormText {
21    /// Its title.
22    pub title: String,
23    /// What it says under the title.
24    pub description: Option<String>,
25    /// The button that keeps the record.
26    pub save: String,
27}
28
29#[cfg_attr(feature = "live", ravenrs_live::expose)]
30impl FormText {
31    /// A window with this title and this button.
32    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    /// What it says under the title.
37    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/// A form of an entity in a window. It checks its rules and hands the record over: keeping it is
57/// up to whoever asked, so a record with rules of its own keeps going through them. A handle.
58#[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    /// A form of this entity, among these. References find their records in `space`.
71    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    /// Takes a record that met its rules, and whether it is new; false leaves the form open.
109    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    /// The width of the window. 440 by default.
115    pub fn width(self, width: f32) -> Self {
116        self.0.width.set(width);
117        self
118    }
119
120    /// Scrolls its fields inside this height.
121    pub fn scrolling_height(self, height: f32) -> Self {
122        self.0.height.set(Some(height));
123        self
124    }
125
126    /// Says what a field is called when that depends on the record.
127    pub fn captions(self, caption: FormCaption) -> Self {
128        self.0.session.set_form_caption(caption);
129        self
130    }
131
132    /// What its screens share.
133    pub fn session(&self) -> &Session {
134        &self.0.session
135    }
136
137    /// Opens on a new record, filled in by `fill`.
138    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    /// Opens on the record with this key, changed by `fill` first. Nothing happens if it is gone.
145    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    /// Changes a field of the record on the form.
160    pub fn set(&self, field: &str, value: impl Into<Value>) {
161        self.0.session.set_form_value(field, value.into());
162    }
163
164    /// Whether it is open.
165    pub fn is_open(&self) -> bool {
166        *self.0.open.peek()
167    }
168
169    /// Closes it, keeping nothing.
170    pub fn close(&self) {
171        self.0.open.set(false);
172    }
173
174    /// Hands over the record if it meets its rules, and closes if it was taken.
175    pub fn save(&self) {
176        // A second click can arrive before the form is gone.
177        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}