Skip to main content

tilo_app/framework/
env.rs

1//! What a view and a custom surface both reach: the record's dispatcher
2//! and live state, the run it is part of, its storage and effects.
3//! [`ViewCx`](super::ViewCx) and [`SurfaceCx`](super::SurfaceCx) dereference
4//! to it, so each reads the same accessors.
5
6use std::rc::{Rc, Weak};
7
8use tilo_resource::Resource;
9use ui_core::LocalState;
10use ui_core::record::RecordSaveStatus;
11
12use super::dispatch::Dispatcher;
13use super::effect::{EffectAnswer, EffectCx, Effects, Projection};
14use super::live::Live;
15use super::scenario::{Run, RunMode};
16use super::settings::{Setting, ViewStorage};
17
18/// The record, run and storage a view or surface works in. See the module
19/// docs.
20pub struct RecordEnv<R: Resource> {
21    pub(crate) live: Live<R>,
22    pub(crate) alive: Weak<()>,
23    pub(crate) dispatch: Dispatcher<R>,
24    pub(crate) saved: LocalState<RecordSaveStatus>,
25    pub(crate) run: Run,
26    pub(crate) storage: Rc<ViewStorage>,
27    pub(crate) effects: Effects<R>,
28}
29
30impl<R: Resource> Clone for RecordEnv<R> {
31    fn clone(&self) -> Self {
32        Self {
33            live: self.live.clone(),
34            alive: self.alive.clone(),
35            dispatch: self.dispatch.clone(),
36            saved: self.saved.clone(),
37            run: self.run.clone(),
38            storage: Rc::clone(&self.storage),
39            effects: self.effects.clone(),
40        }
41    }
42}
43
44impl<R: Resource> RecordEnv<R> {
45    /// The same environment for a scope that lives as long as `alive`.
46    pub(crate) fn living(&self, alive: &Rc<()>) -> Self {
47        Self {
48            alive: Rc::downgrade(alive),
49            ..self.clone()
50        }
51    }
52
53    /// Open, in this window and in place of the record it shows, what a
54    /// launch of the app naming `arguments` opens: the record
55    /// [`ResourceApp::launch_record`](super::ResourceApp::launch_record)
56    /// names, with its launch actions — the document a person chose with
57    /// the app's own Open. The window's view is rebuilt for that record on
58    /// the next frame. Refused in a past version and a capture.
59    ///
60    /// The outcome answers once the host opened it, or why it could not.
61    ///
62    /// # Errors
63    /// Returns that the window cannot open another record.
64    pub fn open_here(
65        &self,
66        arguments: &[String],
67    ) -> Result<ui_core::application::ReplacementOutcome, String> {
68        self.run.open_here(arguments)
69    }
70
71    /// The names of the participants, for showing the ids actions record
72    /// ([`People::name`](super::People::name)).
73    pub fn people(&self) -> super::People {
74        self.run.people.clone()
75    }
76
77    /// Where edits go. Custom widgets keep a clone.
78    pub fn dispatcher(&self) -> Dispatcher<R> {
79        self.dispatch.clone()
80    }
81
82    /// The dispatcher of the resource nested in this one at `id`, whose
83    /// edits are this resource's action `wrap(edit)`: a rich-text body, a
84    /// draft, a SQL buffer. See [`Dispatcher::child`].
85    pub fn child<C: Resource>(
86        &self,
87        id: &str,
88        wrap: impl Fn(C::Action) -> R::Action + 'static,
89    ) -> Dispatcher<C> {
90        self.dispatch.child(id, wrap)
91    }
92
93    /// Who this view is for: the participant reading and editing through
94    /// it, the same id an action they invoke reads as its author
95    /// ([`ActionCx::author`](tilo_resource::ActionCx::author)). Compare the
96    /// authors the resource records with it ("your message", "you reacted")
97    /// instead of keeping the reader in the resource state, which is the
98    /// same for everyone. `None` in a view of a past version.
99    pub fn me(&self) -> Option<&str> {
100        self.dispatch.me()
101    }
102
103    /// Whether this view shows a past version in History mode, drawn from a
104    /// frozen document. It must start no work there: a surface that fetches,
105    /// watches or spawns a worker is built without it and presents what the
106    /// document holds.
107    pub fn is_past(&self) -> bool {
108        self.dispatch.is_past()
109    }
110
111    /// Whether this participant may only view, when the view is built.
112    /// Bound controls already follow permission changes on their own; to show
113    /// or disable something else while the participant may only view, bind to
114    /// [`ViewCx::read_only_state`](super::ViewCx::read_only_state), which follows role changes while the app
115    /// runs.
116    pub fn read_only(&self) -> bool {
117        self.dispatch.read_only()
118    }
119
120    /// Whether the record's latest edits are durable, for a status line.
121    /// Saving is automatic; there is nothing to save by hand.
122    pub fn save_status(&self) -> LocalState<RecordSaveStatus> {
123        self.saved.clone()
124    }
125
126    /// What the view is built for: a window someone uses, a past version in
127    /// History, or a screenshot. Start work that reaches outside the process
128    /// (an engine, a socket, a probe) only when it [is live](RunMode::is_live).
129    pub fn mode(&self) -> RunMode {
130        self.run.mode
131    }
132
133    /// The capture's stand-in for a live source of type `V`
134    /// ([`Fixture::provide`](super::Fixture::provide)); `None` outside a
135    /// capture, or when the scenario provides none.
136    pub fn fixture<V: 'static>(&self) -> Option<Rc<V>> {
137        self.run.fixture::<V>()
138    }
139
140    /// The content store the host lent the app: opaque blobs by hash that
141    /// outlive it. A capture's holds the blobs its scenario put there.
142    pub fn content(&self) -> Option<tilo_content::ContentStore> {
143        self.run.content()
144    }
145
146    /// What the launch that opened this window named — the file a person
147    /// opened the app with, its command line — for a view that shows what
148    /// the launch's actions ([`ResourceApp::launch`](super::ResourceApp::launch))
149    /// made: the note a launch imported, opened. Empty for a plain start and
150    /// for a past version.
151    pub fn launch(&self) -> super::launch::LaunchInput<'_> {
152        self.run.launch()
153    }
154
155    /// What the window's launch named (paths, URLs), for a view that acts
156    /// on them beyond the actions the launch took: the file a save writes
157    /// back to. Empty for a plain start, a past version and a capture.
158    pub fn launch_arguments(&self) -> &[String] {
159        &self.run.launched.arguments
160    }
161
162    /// The value of the app's own option `name` (`--purpose`) the window's
163    /// launch named; see [`ResourceApp::OPTIONS`](super::ResourceApp::OPTIONS).
164    /// `None` for a past version.
165    pub fn launch_option(&self, name: &str) -> Option<&str> {
166        super::launch::option(&self.run.launched.options, name)
167    }
168
169    /// Whether the window's launch named the app's switch `name`; see
170    /// [`ResourceApp::FLAGS`](super::ResourceApp::FLAGS).
171    pub fn launch_flag(&self, name: &str) -> bool {
172        super::launch::flag(&self.run.launched.options, name)
173    }
174
175    /// The service of type `T` the host that mounts the app in its own
176    /// process lent it ([`HostContext::lend`](ui_core::application::HostContext::lend)),
177    /// taken once: the shell's connection to the session's process
178    /// supervisor for a terminal it hosts. `None` in the app's own process,
179    /// and for a past version or a capture, which start nothing.
180    pub fn take_lent<S: 'static>(&self) -> Option<S> {
181        if !self.run.mode.is_live() {
182            return None;
183        }
184        self.run.launched.lent.take::<S>()
185    }
186
187    /// Whether the window is private ([`ResourceApp::PRIVATE_WINDOWS`](super::ResourceApp::PRIVATE_WINDOWS)):
188    /// its record, settings and files are thrown away with the process. A
189    /// view keeps what it would otherwise remember on the device (visits,
190    /// a session to restore) to itself.
191    pub fn is_private(&self) -> bool {
192        self.run.private
193    }
194
195    /// The app's typed settings of type `S`, kept current while this scope
196    /// lives. `S` declares whether they follow the person or stay on the
197    /// device ([`Scope`](ui_core::storage::Scope)).
198    pub fn settings<S>(&self) -> Setting<S>
199    where
200        S: ui_core::storage::SettingsSchema + Clone + PartialEq + Send + Sync,
201    {
202        self.storage.follow(self.alive.clone())
203    }
204
205    /// The device this window runs on: stable across launches, distinct on
206    /// each of the person's devices. `None` in a view of a past version. An
207    /// action that records where something happened (a speed test run on
208    /// this device) takes it as an argument, so every replica applies the
209    /// same value.
210    pub fn device(&self) -> Option<&str> {
211        self.storage.device()
212    }
213
214    /// The person's own record of `P` ([`PersonRecord`](super::PersonRecord)):
215    /// theirs alone, kept in their own room and synced to their other
216    /// devices, never to a collaborator, whatever room this record is in.
217    /// `None` where the host keeps no records of the person's (a view of a
218    /// past version) or the record cannot be opened.
219    pub fn person<P: Resource>(&self) -> Option<super::PersonRecord<P>> {
220        self.storage.person::<P>()
221    }
222
223    /// The person's record `P` that one app keeps for all of theirs — their
224    /// accounts, which Settings keeps: the same record on every device, held
225    /// here too and converging with the keeper's over the person's own room.
226    /// Changed only by its owner and editors
227    /// ([`SharedRecord`](tilo_resource::SharedRecord),
228    /// [`PersonRecord::editable`](super::PersonRecord::editable)); for its
229    /// owner it is [`Self::person`].
230    pub fn shared_person<P: tilo_resource::SharedRecord>(&self) -> Option<super::PersonRecord<P>> {
231        self.storage.shared_person::<P>()
232    }
233
234    /// The app's files, secrets and settings documents, as its host gave
235    /// them. A secret of one document item is
236    /// `cx.storage().secrets().item(purpose, id)`.
237    pub fn storage(&self) -> &ui_core::storage::AppStorage {
238        self.storage.storage()
239    }
240
241    /// Carry out the entries of the [`Outbox`](tilo_resource::Outbox) at
242    /// `outbox` that this device owns, and record how each settled.
243    ///
244    /// Every replica holds the entries; `owns` says which are this device's
245    /// to carry out (the device whose disk a file operation names, the one
246    /// holding the mail account). For each such pending entry the framework
247    /// runs `run` in the live app — never in History or a capture — at once
248    /// or as a job through its [`EffectCx`], and records the answer with
249    /// the outbox's settle edit: content from outside, nobody's undo step.
250    /// The answer settles the entry: [`Effected::settled`](super::Effected::settled)
251    /// with how it went, [`Effected::value`](super::Effected::value) with
252    /// the result `O`, or [`Effected::done`](super::Effected::done) for
253    /// `O`'s default. An `Err` leaves the entry pending, to run again after
254    /// the next change or when the returned [`Projection`] is told the
255    /// outside changed (the server came back); a failure for good is a
256    /// [`Settled::Failed`](tilo_resource::Settled::Failed).
257    pub fn outbox<E, O>(
258        &self,
259        outbox: tilo_resource::Path<R, tilo_resource::Outbox<E, O>>,
260        owns: impl Fn(&R::State, &E) -> bool + 'static,
261        run: impl FnMut(&tilo_resource::Id, E, &mut EffectCx<R>) -> EffectAnswer + 'static,
262    ) -> Projection
263    where
264        R: Resource<State = R> + tilo_resource::Model + 'static,
265        E: tilo_resource::Model + Clone + 'static,
266        O: serde::Serialize + serde::de::DeserializeOwned + Default + 'static,
267    {
268        self.effects
269            .outbox(self.alive.clone(), outbox, owns, run, false)
270    }
271
272    /// [`Self::outbox`], where an entry is the outside half of the action
273    /// that queued it: one an admitted invocation on this device queues runs
274    /// at once under that invocation — `cx.invocation()` names it, and its
275    /// custody authorises what the handler does for it (a native host that
276    /// acts only on behalf of an invocation) — and the invocation completes
277    /// only once its entries settled, failing with a failed one. Entries
278    /// queued elsewhere, replayed or still pending after a restart run as
279    /// [`Self::outbox`] runs them, with no invocation.
280    pub fn outbox_awaited<E, O>(
281        &self,
282        outbox: tilo_resource::Path<R, tilo_resource::Outbox<E, O>>,
283        owns: impl Fn(&R::State, &E) -> bool + 'static,
284        run: impl FnMut(&tilo_resource::Id, E, &mut EffectCx<R>) -> EffectAnswer + 'static,
285    ) -> Projection
286    where
287        R: Resource<State = R> + tilo_resource::Model + 'static,
288        E: tilo_resource::Model + Clone + 'static,
289        O: serde::Serialize + serde::de::DeserializeOwned + Default + 'static,
290    {
291        self.effects
292            .outbox(self.alive.clone(), outbox, owns, run, true)
293    }
294}