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}