Skip to main content

tilo_app/framework/
publishing.rs

1//! What a part of a view publishes beside its resource that no binding
2//! covers: the live entities it shows ([`LiveRow`]s) and the resource's
3//! edits bound to them, which of the resource's actions its live state
4//! makes unavailable, search entries for what the resource does not hold,
5//! whether the secrets behind them are unlocked, what it looks up for the
6//! platform search's queries, and what it asks of the host (a document to
7//! open, the search to show). A view says it with
8//! [`ViewCx::publishing`](super::ViewCx::publishing); the root collects it as
9//! it collects a hosted surface's, so a view never needs a widget of its own
10//! to publish what it shows.
11
12use 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>;
27/// The edits bound to a row, put into the `Vec<ActionDecl<R>>` it is
28/// handed (and nothing else); whether the row has any.
29type 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
37/// What a part of a view publishes beside the resource. Each source is read
38/// whenever the resource is published or searched, so it reads the view's
39/// [`LocalState`](ui_core::LocalState)s and settings as they are then.
40pub struct Publishes<R: Resource> {
41    sources: Sources,
42    resource: PhantomData<fn() -> R>,
43}
44
45/// The sources of a [`Publishes`], without its resource type, so the widget
46/// that publishes them is compiled once for every app.
47#[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    /// Whether the system's search index forgets the entries once the app
56    /// closes.
57    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    /// The live external entities the view shows, published as children of
80    /// the resource and merged by id with its document children. Keep it to
81    /// what fits a published tree (the rows on screen, a bounded page) and
82    /// resolve the rest with [`Self::live_row`].
83    #[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    /// The live row with an id [`Self::live_rows`] leaves out, so an action
90    /// addressed to any entity the view knows still reaches it.
91    #[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    /// Whether the live row `id`, which [`Self::live_row`] does not know
98    /// now, may be known once what the view is reading lands: an agent
99    /// addressing it is answered then. Start that read here if it has not
100    /// started; the window must be woken when it lands.
101    #[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    /// Edits of the resource bound to the live row `row` (rename *this*
108    /// file, invite *this* person): typed builders of the resource's
109    /// actions, published on that row and applied through the same
110    /// permission check, confirmation and undo as the resource's own
111    /// declarations. A row itself carries only view and effect operations;
112    /// this is how it offers a document change.
113    #[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    /// The resource's own actions that differ from their declaration right
129    /// now, as the view's live state says (a mode switch unavailable while
130    /// recording, with why). Read whenever the resource is published, so
131    /// the palette, agents and the door follow the view: an action
132    /// unavailable here is refused with its reason.
133    #[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    /// Search entries for what the view shows that the resource does not
140    /// hold (a vault's items, a page's text), indexed with the resource's
141    /// own as the app's one search source. Read whenever someone searches,
142    /// so keep it to what the view already has at hand.
143    #[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    /// Say that the system's search index forgets [`Self::search_entries`]
150    /// once the app closes: entries that are the machine's state of the
151    /// moment (the processes running) or a catalogue the system searches by
152    /// itself. They are found only while the app runs, and a closed app
153    /// leaves none behind to open into something that is gone.
154    #[must_use]
155    pub fn search_entries_while_running(mut self) -> Self {
156        self.sources.transient = true;
157        self
158    }
159
160    /// Whether the secrets behind the view's secret-bearing search entries
161    /// ([`SearchEntry::secret_bearing`]) are unlocked now: a vault that
162    /// opened and found its key. The system's search index finds those
163    /// entries only while the app last said `Some(true)`; `None` says
164    /// nothing.
165    #[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    /// Look up what the platform search asks for beyond what the view holds
172    /// (a server's catalogue, a mailbox not yet fetched), and offer what
173    /// comes back through [`Self::search_entries`]. `query` gets the latest
174    /// query on the UI thread before the view syncs; queries asked between
175    /// frames are coalesced. Never asked of a past version.
176    #[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    /// Whether the results for the latest [`Self::search_query`] are still
183    /// coming (a server is still looking). A search already showing waits
184    /// for them and shows them once this turns false, without anyone typing
185    /// again. Read after every sync.
186    #[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    /// What the view asks of the host, one at a time, taken after every
193    /// frame: a document chosen here to open in its own app, the platform
194    /// search to show.
195    #[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
205/// `content`, publishing what `publishes` says on the resource `R`. `wake`
206/// is the live window's, `None` in a past version, which is asked nothing.
207pub(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    // Only a view that looks queries up is asked them.
228    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    // Whoever reaches the view's widget by its type (a test, a host that
275    // embeds it) finds it through the part that publishes beside it.
276    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    // Publishing adds no structure of its own: a change its content describes
289    // stays described, so a list mounting rows settles instead of rebuilding
290    // the window's tree around it.
291    fn take_structure_change(&mut self) -> ui_core::widget::StructureChange {
292        self.content.take_structure_change()
293    }
294
295    // The latest query reaches the view before it syncs; what it says of
296    // its answer is read after.
297    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        // Asked with nothing to fill: whether the row has any edit at all.
343        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/// Coalesced delivery of the platform search's queries to a view that
381/// looks them up.
382#[derive(Clone)]
383pub(crate) struct SearchQueries(Rc<RefCell<QueryDelivery>>);
384
385#[derive(Default)]
386struct QueryDelivery {
387    last: Option<SearchQuery>,
388    pending: Option<SearchQuery>,
389    /// The view said its results for `last` are still coming.
390    awaiting: bool,
391    /// Wakes the window, so a query asked between frames reaches the
392    /// view without waiting for input. `None` where no query is ever
393    /// delivered (History).
394    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    /// Whether the view has yet to answer the latest query: it has not
406    /// been given it, or said its results are still coming.
407    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    /// The latest query asked.
413    pub(crate) fn last(&self) -> Option<SearchQuery> {
414        self.0.borrow().last.clone()
415    }
416
417    /// What the view says of its results for the latest query.
418    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}