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_style::TextStyle;
9use ravenrs_widgets::grid::Row;
10use ravenrs_widgets::{
11 BuildCx, Button, ButtonStyle, Card, GridRowAction, HStack, IconName, Keyed, NodeId, Text, VStack, View,
12 ViewExt,
13};
14
15use crate::list_view::CellText;
16use crate::session::{Session, SpaceFactory, Words};
17use crate::values::key_from_text;
18use crate::{DetailView, ListView, SystemModule};
19
20#[derive(Clone, Debug)]
22#[non_exhaustive]
23#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
24pub struct PaneWords {
25 pub new_title: String,
27 pub edit_title: String,
29 pub add: String,
31 pub save_changes: String,
33 pub cancel: String,
35 pub edit_row: String,
37 pub delete_row: String,
39 pub delete_question: String,
41 pub delete_detail: String,
43 pub delete_answer: String,
45}
46
47#[cfg_attr(feature = "live", ravenrs_live::expose)]
48impl PaneWords {
49 pub fn new(
51 new_title: impl Into<String>,
52 edit_title: impl Into<String>,
53 add: impl Into<String>,
54 delete_question: impl Into<String>,
55 delete_detail: impl Into<String>,
56 delete_answer: impl Into<String>,
57 ) -> Self {
58 let answer: String = delete_answer.into();
59 let texts = ravenrs_app_i18n::Texts::new(ravenrs_app_i18n::Language::BrazilianPortuguese);
60 Self {
61 new_title: new_title.into(),
62 edit_title: edit_title.into(),
63 add: add.into(),
64 save_changes: String::new(),
65 cancel: String::new(),
66 edit_row: String::new(),
67 delete_row: answer.clone(),
68 delete_question: delete_question.into(),
69 delete_detail: delete_detail.into(),
70 delete_answer: answer,
71 }
72 .with_texts(&texts)
73 }
74
75 pub fn with_texts(mut self, texts: &ravenrs_app_i18n::Texts) -> Self {
77 self.save_changes = texts.get("pane.save_changes");
78 self.cancel = texts.get("pane.cancel");
79 self.edit_row = texts.get("pane.edit_row");
80 self
81 }
82}
83
84type DoneHandler = Rc<dyn Fn(&'static str)>;
85type FailHandler = Rc<dyn Fn(String)>;
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89enum Outcome {
90 Saved,
91 Deleted,
92}
93
94#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
97pub struct EntityPane {
98 session: Session,
99 entity: EntityInfo,
100 words: PaneWords,
101 told: RefCell<Option<DoneHandler>>,
102 failed: RefCell<Option<FailHandler>>,
103 cell_text: Option<CellText>,
104 seen: Cell<u64>,
105 beside_from: f32,
106 form_width: f32,
107 version: Option<Rc<dyn Fn() -> u64>>,
108}
109
110impl std::fmt::Debug for EntityPane {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("EntityPane").field("entity", &self.entity.name).finish_non_exhaustive()
113 }
114}
115
116#[cfg_attr(feature = "live", ravenrs_live::expose)]
117impl EntityPane {
118 pub fn new<S: ObjectSpace + 'static>(
120 entity: EntityInfo,
121 space: impl Fn() -> S + 'static,
122 words: Words,
123 pane: PaneWords,
124 ) -> Result<Self, ModelError> {
125 let application = Application::builder(entity.caption.clone())
126 .module(SystemModule::with_words(words.clone()))
127 .entity(entity.clone())
128 .build()?;
129 let factory: SpaceFactory =
130 Rc::new(move || Rc::new(RefCell::new(space())) as Rc<RefCell<dyn ObjectSpace>>);
131 let session = Session::standalone(application, factory, words);
132 session.open_new(&entity.name);
133 Ok(Self {
134 session,
135 entity,
136 words: pane,
137 told: RefCell::default(),
138 failed: RefCell::default(),
139 cell_text: None,
140 seen: Cell::new(0),
141 beside_from: 760.0,
142 form_width: 340.0,
143 version: None,
144 })
145 }
146
147 pub fn on_done(self, handler: impl Fn(&'static str) + 'static) -> Self {
149 *self.told.borrow_mut() = Some(Rc::new(handler));
150 self
151 }
152
153 pub fn on_failed(self, handler: impl Fn(String) + 'static) -> Self {
155 *self.failed.borrow_mut() = Some(Rc::new(handler));
156 self
157 }
158
159 pub fn cell_text(mut self, text: impl Fn(&str, &Object) -> Option<String> + 'static) -> Self {
161 self.cell_text = Some(Rc::new(text));
162 self
163 }
164
165 pub fn beside_from(mut self, width: f32) -> Self {
167 self.beside_from = width;
168 self
169 }
170
171 pub fn version(mut self, version: impl Fn() -> u64 + 'static) -> Self {
174 self.version = Some(Rc::new(version));
175 self
176 }
177
178 fn view_of(&self, kind: &str) -> Option<ViewInfo> {
179 ViewInfo::from_model(&self.session.model(), &format!("{}_{kind}", self.entity.name)).ok()
180 }
181
182 fn body(&self, beside: bool) -> impl View + use<> {
183 let session = self.session.clone();
184 let form = session.form();
185 let editing = form.as_ref().is_some_and(|form| !form.is_new);
186 let words = self.words.clone();
187 let list_view = self.view_of("ListView").map(|view| {
188 let (opener, entity) = (session.clone(), self.entity.clone());
189 let key_of = {
190 let entity = entity.clone();
191 move |row: &Row| {
192 let field = entity.key_field()?;
193 let at = row_key_position(&entity, field.name.as_str());
194 row.get(at).map(|cell| key_from_text(&entity, &cell.key_text()))
195 }
196 };
197 let (asker, remover) = (session.clone(), self.clone_handles());
198 let open_key = key_of.clone();
199 let question = words.clone();
200 let open_entity = entity.clone();
201 let mut list = ListView::new(session.clone(), session.frame(), view)
202 .compact()
203 .row_action(GridRowAction::new(words.edit_row.clone(), IconName::Pencil, move |row| {
204 if let Some(key) = open_key(row) {
205 opener.open_detail(&open_entity.name, &key);
206 }
207 }))
208 .row_action(GridRowAction::new(words.delete_row.clone(), IconName::Trash2, move |row| {
209 let Some(key) = key_of(row) else { return };
210 let (session, remover) = (asker.clone(), remover.clone());
211 let name = entity.name.clone();
212 asker.confirm().ask(
213 question.delete_question.clone(),
214 question.delete_detail.clone(),
215 question.delete_answer.clone(),
216 move || remover.delete(&session, &name, &key),
217 );
218 }));
219 if let Some(text) = &self.cell_text {
220 list = list.cell_text(text.clone());
221 }
222 list
223 });
224 let detail = self.view_of("DetailView").map(|view| DetailView::new(session.clone(), view));
225 let key = form.as_ref().map_or(0, |form| Rc::as_ptr(form) as usize as u64);
226 let title = if editing { words.edit_title.clone() } else { words.new_title.clone() };
227 let handles = self.clone_handles();
228 let (saver, canceler, entity_name) = (session.clone(), session.clone(), self.entity.name.clone());
229 let again = entity_name.clone();
230 let buttons = HStack::new((
231 editing.then(|| {
232 Button::new(words.cancel.clone())
233 .style(ButtonStyle::Secondary)
234 .on_click(move || canceler.open_new(&again))
235 }),
236 Button::new(if editing { words.save_changes.clone() } else { words.add.clone() })
237 .icon(if editing { IconName::Check } else { IconName::Plus })
238 .on_click(move || handles.save(&saver, &entity_name)),
239 ))
240 .spacing(8.0);
241 let form_card = Card::new(
242 VStack::new((Text::new(title).style(TextStyle::Headline), Keyed::new(key, detail), buttons))
243 .spacing(12.0)
244 .align(Align::Stretch),
245 );
246 let list_card = Card::new(VStack::new((list_view,)).align(Align::Stretch));
247 let arranged: ravenrs_widgets::AnyView = if beside {
248 Box::new(
249 HStack::new((list_card.grow(1.0), form_card.width(self.form_width)))
250 .spacing(16.0)
251 .align(Align::Start),
252 )
253 } else {
254 Box::new(VStack::new((list_card, form_card)).spacing(16.0).align(Align::Stretch))
255 };
256 arranged
257 }
258
259 fn clone_handles(&self) -> Handles {
260 Handles { told: self.told.borrow().clone(), failed: self.failed.borrow().clone() }
261 }
262}
263
264fn row_key_position(entity: &EntityInfo, key: &str) -> usize {
266 let shown: Vec<&str> = entity.fields.iter().filter(|f| f.in_list).map(|f| f.name.as_str()).collect();
267 shown.iter().position(|name| *name == key).unwrap_or(shown.len())
268}
269
270#[derive(Clone)]
272struct Handles {
273 told: Option<DoneHandler>,
274 failed: Option<FailHandler>,
275}
276
277#[cfg_attr(feature = "live", ravenrs_live::expose)]
278impl Handles {
279 fn say(&self, outcome: Outcome) {
280 if let Some(told) = &self.told {
281 told(match outcome {
282 Outcome::Saved => "saved",
283 Outcome::Deleted => "deleted",
284 });
285 }
286 }
287
288 fn fail(&self, session: &Session, message: String) {
289 session.set_error(None);
290 if let Some(failed) = &self.failed {
291 failed(message);
292 }
293 }
294
295 fn save(&self, session: &Session, entity: &str) {
296 let (saved, name) = (self.clone(), entity.to_owned());
297 let done = session.save_then(move |session| {
298 session.open_new(&name);
299 saved.say(Outcome::Saved);
300 });
301 match done {
302 Ok(()) => {}
303 Err(_) if session.is_checking() => {}
305 Err(message) => self.fail(session, message),
306 }
307 }
308
309 fn delete(&self, session: &Session, entity: &str, key: &Value) {
310 let found = session.frame().space().borrow().find(entity, key);
311 let Ok(Some(object)) = found else { return };
312 match session.delete(&[object]) {
313 Ok(()) => {
314 session.open_new(entity);
315 self.say(Outcome::Deleted);
316 }
317 Err(message) => self.fail(session, message),
318 }
319 }
320}
321
322impl View for EntityPane {
323 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
324 if let Some(version) = &self.version {
325 let now = version();
326 if self.seen.replace(now) != now {
327 self.session.records_changed();
328 }
329 }
330 let from = self.beside_from;
331 let beside = cx.viewport().width >= from;
332 let body = self.body(beside);
333 let parts = cx.child(&body);
334 let confirm = cx.child(self.session.confirm());
335 cx.container(
336 ravenrs_layout::LayoutStyle {
337 align: Align::Stretch,
338 ..ravenrs_layout::LayoutStyle::stack(ravenrs_layout::Axis::Vertical)
339 },
340 &[parts, confirm],
341 ravenrs_widgets::Paint::None,
342 )
343 }
344}