tilo_app/framework/
publishing.rs1use std::cell::RefCell;
13use std::marker::PhantomData;
14use std::rc::Rc;
15
16use tilo_resource::{ActionDecl, Resource, SearchEntry};
17use ui_core::search::SearchQuery;
18use ui_core::widget::{AppHostIntent, AppModel, Decorator, Element, ModelPath, Widget};
19
20use super::effect::EffectStatus;
21use super::rows::LiveRow;
22use super::surface::SurfacePublication;
23
24type Rows = Rc<dyn Fn() -> Vec<LiveRow>>;
25type Row = Rc<dyn Fn(&str) -> Option<LiveRow>>;
26type Pending = Rc<dyn Fn(&str) -> bool>;
27type RowEdits = Rc<dyn Fn(&str, &mut dyn std::any::Any) -> bool>;
30type Statuses = Rc<dyn Fn() -> Vec<EffectStatus>>;
31type Intents = Rc<RefCell<dyn FnMut() -> Option<AppHostIntent>>>;
32type Entries = Rc<dyn Fn() -> Vec<SearchEntry>>;
33type Unlocked = Rc<dyn Fn() -> Option<bool>>;
34type OnQuery = Rc<RefCell<dyn FnMut(&SearchQuery)>>;
35type Answering = Rc<dyn Fn() -> bool>;
36
37pub struct Publishes<R: Resource> {
41 sources: Sources,
42 resource: PhantomData<fn() -> R>,
43}
44
45#[derive(Clone, Default)]
48struct Sources {
49 rows: Option<Rows>,
50 row: Option<Row>,
51 pending: Option<Pending>,
52 row_edits: Option<RowEdits>,
53 availability: Option<Statuses>,
54 entries: Option<Entries>,
55 transient: bool,
58 unlocked: Option<Unlocked>,
59 query: Option<OnQuery>,
60 answering: Option<Answering>,
61 intents: Option<Intents>,
62}
63
64impl<R: Resource> Default for Publishes<R> {
65 fn default() -> Self {
66 Self {
67 sources: Sources::default(),
68 resource: PhantomData,
69 }
70 }
71}
72
73impl<R: Resource> Publishes<R> {
74 #[must_use]
75 pub fn new() -> Self {
76 Self::default()
77 }
78
79 #[must_use]
84 pub fn live_rows(mut self, rows: impl Fn() -> Vec<LiveRow> + 'static) -> Self {
85 self.sources.rows = Some(Rc::new(rows));
86 self
87 }
88
89 #[must_use]
92 pub fn live_row(mut self, row: impl Fn(&str) -> Option<LiveRow> + 'static) -> Self {
93 self.sources.row = Some(Rc::new(row));
94 self
95 }
96
97 #[must_use]
102 pub fn live_row_pending(mut self, pending: impl Fn(&str) -> bool + 'static) -> Self {
103 self.sources.pending = Some(Rc::new(pending));
104 self
105 }
106
107 #[must_use]
114 pub fn row_edits(mut self, edits: impl Fn(&str) -> Vec<ActionDecl<R>> + 'static) -> Self {
115 self.sources.row_edits = Some(Rc::new(move |row, into| {
116 let offered = edits(row);
117 if offered.is_empty() {
118 return false;
119 }
120 if let Some(into) = into.downcast_mut::<Vec<ActionDecl<R>>>() {
121 into.extend(offered);
122 }
123 true
124 }));
125 self
126 }
127
128 #[must_use]
134 pub fn availability(mut self, statuses: impl Fn() -> Vec<EffectStatus> + 'static) -> Self {
135 self.sources.availability = Some(Rc::new(statuses));
136 self
137 }
138
139 #[must_use]
144 pub fn search_entries(mut self, entries: impl Fn() -> Vec<SearchEntry> + 'static) -> Self {
145 self.sources.entries = Some(Rc::new(entries));
146 self
147 }
148
149 #[must_use]
155 pub fn search_entries_while_running(mut self) -> Self {
156 self.sources.transient = true;
157 self
158 }
159
160 #[must_use]
166 pub fn secrets_unlocked(mut self, unlocked: impl Fn() -> Option<bool> + 'static) -> Self {
167 self.sources.unlocked = Some(Rc::new(unlocked));
168 self
169 }
170
171 #[must_use]
177 pub fn search_query(mut self, query: impl FnMut(&SearchQuery) + 'static) -> Self {
178 self.sources.query = Some(Rc::new(RefCell::new(query)));
179 self
180 }
181
182 #[must_use]
187 pub fn search_pending(mut self, pending: impl Fn() -> bool + 'static) -> Self {
188 self.sources.answering = Some(Rc::new(pending));
189 self
190 }
191
192 #[must_use]
196 pub fn host_intents(
197 mut self,
198 intents: impl FnMut() -> Option<AppHostIntent> + 'static,
199 ) -> Self {
200 self.sources.intents = Some(Rc::new(RefCell::new(intents)));
201 self
202 }
203}
204
205pub(crate) fn publishing<R: Resource>(
208 publishes: Publishes<R>,
209 content: Element,
210 wake: Option<ui_core::WakeHandle>,
211) -> Element {
212 element(
213 std::any::TypeId::of::<R>(),
214 publishes.sources,
215 content,
216 wake,
217 )
218}
219
220#[inline(never)]
221fn element(
222 resource: std::any::TypeId,
223 sources: Sources,
224 content: Element,
225 wake: Option<ui_core::WakeHandle>,
226) -> Element {
227 let queries = SearchQueries::waking(wake.filter(|_| sources.query.is_some()));
229 Box::new(Publishing {
230 resource,
231 sources,
232 content,
233 queries,
234 })
235}
236
237struct Publishing {
238 resource: std::any::TypeId,
239 sources: Sources,
240 content: Element,
241 queries: SearchQueries,
242}
243
244impl Decorator for Publishing {
245 type Inner = dyn Widget;
246
247 fn inner(&self) -> &(dyn Widget + 'static) {
248 self.content.as_ref()
249 }
250
251 fn inner_mut(&mut self) -> &mut (dyn Widget + 'static) {
252 self.content.as_mut()
253 }
254
255 fn provide<'a>(&'a self, request: &mut ui_core::widget::capability::Request<'a, '_>) {
256 if request.wants::<dyn SurfacePublication>() {
257 request.provide::<dyn SurfacePublication>(self);
258 return;
259 }
260 self.content.provide(request);
261 }
262
263 fn provide_mut<'a>(
264 &'a mut self,
265 request: &mut ui_core::widget::capability::RequestMut<'a, '_>,
266 ) {
267 if request.wants::<dyn SurfacePublication>() {
268 request.provide::<dyn SurfacePublication>(self);
269 return;
270 }
271 self.content.provide_mut(request);
272 }
273
274 fn as_any(&self) -> Option<&dyn std::any::Any> {
277 self.content.as_any()
278 }
279
280 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
281 self.content.as_any_mut()
282 }
283
284 fn name(&self) -> &'static str {
285 "Publishing"
286 }
287
288 fn take_structure_change(&mut self) -> ui_core::widget::StructureChange {
292 self.content.take_structure_change()
293 }
294
295 fn sync(&mut self, state: &AppModel, path: &ModelPath) {
298 if let Some(query) = self.queries.take()
299 && let Some(on_query) = &self.sources.query
300 {
301 (on_query.borrow_mut())(&query);
302 }
303 self.content.sync(state, path);
304 self.queries.settle(
305 self.sources
306 .answering
307 .as_ref()
308 .is_some_and(|answering| answering()),
309 );
310 }
311}
312
313impl SurfacePublication for Publishing {
314 fn resource(&self) -> std::any::TypeId {
315 self.resource
316 }
317 fn availability(&self) -> Vec<EffectStatus> {
318 self.sources
319 .availability
320 .as_ref()
321 .map_or_else(Vec::new, |statuses| statuses())
322 }
323 fn live_rows(&self) -> Vec<LiveRow> {
324 self.sources
325 .rows
326 .as_ref()
327 .map_or_else(Vec::new, |rows| rows())
328 }
329 fn live_row(&self, id: &str) -> Option<LiveRow> {
330 self.sources.row.as_ref().and_then(|row| row(id))
331 }
332 fn live_row_pending(&self, id: &str) -> bool {
333 self.sources
334 .pending
335 .as_ref()
336 .is_some_and(|pending| pending(id))
337 }
338 fn binds_rows(&self) -> bool {
339 self.sources.row_edits.is_some()
340 }
341 fn offers_on_row(&self, row: &str) -> bool {
342 self.sources
344 .row_edits
345 .as_ref()
346 .is_some_and(|edits| edits(row, &mut ()))
347 }
348 fn row_declarations(&self, row: &str, into: &mut dyn std::any::Any) -> bool {
349 self.sources
350 .row_edits
351 .as_ref()
352 .is_some_and(|edits| edits(row, into))
353 }
354 fn search_entries(&self) -> Vec<SearchEntry> {
355 self.sources
356 .entries
357 .as_ref()
358 .map_or_else(Vec::new, |entries| entries())
359 }
360 fn secrets_unlocked(&self) -> Option<bool> {
361 self.sources
362 .unlocked
363 .as_ref()
364 .and_then(|unlocked| unlocked())
365 }
366 fn kept_in_search_index(&self) -> bool {
367 !self.sources.transient
368 }
369 fn search_queries(&self) -> Option<SearchQueries> {
370 Some(self.queries.clone())
371 }
372 fn take_host_intent(&mut self) -> Option<AppHostIntent> {
373 self.sources
374 .intents
375 .as_ref()
376 .and_then(|intents| (intents.borrow_mut())())
377 }
378}
379
380#[derive(Clone)]
383pub(crate) struct SearchQueries(Rc<RefCell<QueryDelivery>>);
384
385#[derive(Default)]
386struct QueryDelivery {
387 last: Option<SearchQuery>,
388 pending: Option<SearchQuery>,
389 awaiting: bool,
391 wake: Option<ui_core::WakeHandle>,
395}
396
397impl SearchQueries {
398 pub(crate) fn waking(wake: Option<ui_core::WakeHandle>) -> Self {
399 Self(Rc::new(RefCell::new(QueryDelivery {
400 wake,
401 ..QueryDelivery::default()
402 })))
403 }
404
405 pub(crate) fn answering(&self) -> bool {
408 let delivery = self.0.borrow();
409 delivery.wake.is_some() && (delivery.pending.is_some() || delivery.awaiting)
410 }
411
412 pub(crate) fn last(&self) -> Option<SearchQuery> {
414 self.0.borrow().last.clone()
415 }
416
417 pub(crate) fn settle(&self, awaiting: bool) {
419 self.0.borrow_mut().awaiting = awaiting;
420 }
421
422 pub(crate) fn submit(&self, query: &SearchQuery) {
423 if query
424 .text()
425 .chars()
426 .nth(ui_core::search::MAX_SEARCH_QUERY_SCALARS)
427 .is_some()
428 {
429 return;
430 }
431 let wake = {
432 let mut delivery = self.0.borrow_mut();
433 if delivery.last.as_ref() == Some(query) {
434 return;
435 }
436 delivery.last = Some(query.clone());
437 delivery.pending = Some(query.clone());
438 delivery.awaiting = false;
439 delivery.wake.clone()
440 };
441 if let Some(wake) = wake {
442 wake.wake();
443 }
444 }
445
446 pub(crate) fn take(&self) -> Option<SearchQuery> {
447 self.0.borrow_mut().pending.take()
448 }
449}