Skip to main content

tilo_app/framework/
upkeep.rs

1//! What a view keeps up beside its widgets, that no widget of its own
2//! does: the work it takes in on every sync of its window (background
3//! reads landing, a request another part made), whether it still owes the
4//! window frames or a wake, whether a capture waits for it, what it asks of
5//! the host (the look the window shows itself in, the collaboration its
6//! host actions share), and the presentation it keeps with the window
7//! across a reopen. A view says it with
8//! [`ViewCx::upkeep`](super::ViewCx::upkeep) around what it lays out, so it
9//! never needs a widget of its own to do it.
10
11use ui_core::application_view::{
12    ApplicationFocusRestore, ApplicationLocalView, ApplicationViewState,
13};
14use ui_core::collaboration::{CollabEvent, CollabRequest};
15use ui_core::presence::PresenceCtx;
16use ui_core::text_render::TextRenderer;
17use ui_core::theme::{AppearanceIntent, Theme};
18use ui_core::time::Instant;
19use ui_core::vello::Scene;
20use ui_core::vello::kurbo::Affine;
21use ui_core::view::ThawError;
22use ui_core::widget::{AppModel, Collaborative, Decorator, Element, ModelPath, Widget};
23
24type Hook = Box<dyn FnMut()>;
25type Ask = Box<dyn Fn() -> bool>;
26type Wake = Box<dyn Fn() -> Option<Instant>>;
27type Appearance = Box<dyn Fn() -> Option<AppearanceIntent>>;
28type TakeRequests = Box<dyn FnMut() -> Vec<CollabRequest>>;
29type HandleEvent = Box<dyn FnMut(&CollabEvent)>;
30type Restore = Box<dyn FnMut(&[u8]) -> Result<(), String>>;
31
32/// The presentation a view keeps with its window ([`Upkeep::kept_view`]).
33struct KeptView {
34    app_id: String,
35    version: u32,
36    capture: Box<dyn Fn() -> Vec<u8>>,
37    restore: Restore,
38}
39
40/// What a view keeps up beside its widgets. See the [module docs](self).
41/// Each source is asked whenever the window asks its widgets the same, so it
42/// reads the view's state as it is then.
43#[derive(Default)]
44pub struct Upkeep {
45    sync: Option<Hook>,
46    busy: Option<Ask>,
47    next_wake: Option<Wake>,
48    pending: Option<Ask>,
49    presented: Option<Hook>,
50    appearance: Option<Appearance>,
51    collaboration: Option<(TakeRequests, HandleEvent)>,
52    view: Option<KeptView>,
53}
54
55impl Upkeep {
56    #[must_use]
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// Run `sync` on every sync of the window, before the view's widgets
62    /// sync: take in what work finished, move the view's state, so the
63    /// parts keyed by it are built again in the same frame.
64    #[must_use]
65    pub fn syncing(mut self, sync: impl FnMut() + 'static) -> Self {
66        self.sync = Some(Box::new(sync));
67        self
68    }
69
70    /// Whether work the view runs is under way that the window keeps
71    /// drawing frames for while it lasts.
72    #[must_use]
73    pub fn busy(mut self, busy: impl Fn() -> bool + 'static) -> Self {
74        self.busy = Some(Box::new(busy));
75        self
76    }
77
78    /// When the view next wants a sync while nothing else happens (a read
79    /// on a timer, a notice that expires).
80    #[must_use]
81    pub fn next_wake(mut self, next: impl Fn() -> Option<Instant> + 'static) -> Self {
82        self.next_wake = Some(Box::new(next));
83        self
84    }
85
86    /// Whether what the view shows is still on its way (a first reading):
87    /// a still capture waits for it, and a live window asks no frames on
88    /// its account.
89    #[must_use]
90    pub fn pending_visual_work(mut self, pending: impl Fn() -> bool + 'static) -> Self {
91        self.pending = Some(Box::new(pending));
92        self
93    }
94
95    /// Run `presented` after each frame the view draws.
96    #[must_use]
97    pub fn presenting(mut self, presented: impl FnMut() + 'static) -> Self {
98        self.presented = Some(Box::new(presented));
99        self
100    }
101
102    /// The look the window shows itself in while `appearance` says one (a
103    /// page that changes the look shows it the instant it changes); the
104    /// host applies what differs.
105    #[must_use]
106    pub fn appearance(
107        mut self,
108        appearance: impl Fn() -> Option<AppearanceIntent> + 'static,
109    ) -> Self {
110        self.appearance = Some(Box::new(appearance));
111        self
112    }
113
114    /// Share the app's collaboration connection with the view's host
115    /// actions: `take` drains what they ask the collaboration host, and
116    /// `handle` hands them each event it answers with.
117    #[must_use]
118    pub fn collaboration(
119        mut self,
120        take: impl FnMut() -> Vec<CollabRequest> + 'static,
121        handle: impl FnMut(&CollabEvent) + 'static,
122    ) -> Self {
123        self.collaboration = Some((Box::new(take), Box::new(handle)));
124        self
125    }
126
127    /// Keep what `capture` says of the view's presentation (the page it
128    /// shows) with the window, as the app `app_id` at `version`: a reopened
129    /// window hands it to `restore`, which refuses what it cannot show.
130    #[must_use]
131    pub fn kept_view(
132        mut self,
133        app_id: impl Into<String>,
134        version: u32,
135        capture: impl Fn() -> Vec<u8> + 'static,
136        restore: impl FnMut(&[u8]) -> Result<(), String> + 'static,
137    ) -> Self {
138        self.view = Some(KeptView {
139            app_id: app_id.into(),
140            version,
141            capture: Box::new(capture),
142            restore: Box::new(restore),
143        });
144        self
145    }
146}
147
148/// `content`, kept up as `upkeep` says.
149pub(crate) fn upkeep(upkeep: Upkeep, content: Element) -> Element {
150    Box::new(Kept { upkeep, content })
151}
152
153struct Kept {
154    upkeep: Upkeep,
155    content: Element,
156}
157
158impl Decorator for Kept {
159    type Inner = dyn Widget;
160
161    fn inner(&self) -> &(dyn Widget + 'static) {
162        self.content.as_ref()
163    }
164
165    fn inner_mut(&mut self) -> &mut (dyn Widget + 'static) {
166        self.content.as_mut()
167    }
168
169    fn provide<'a>(&'a self, request: &mut ui_core::widget::capability::Request<'a, '_>) {
170        if self.upkeep.view.is_some() && request.wants::<dyn ApplicationViewState>() {
171            request.provide::<dyn ApplicationViewState>(self);
172            return;
173        }
174        self.content.provide(request);
175    }
176
177    fn provide_mut<'a>(
178        &'a mut self,
179        request: &mut ui_core::widget::capability::RequestMut<'a, '_>,
180    ) {
181        if self.upkeep.collaboration.is_some() && request.wants::<dyn Collaborative>() {
182            request.provide::<dyn Collaborative>(self);
183            return;
184        }
185        if self.upkeep.view.is_some() && request.wants::<dyn ApplicationViewState>() {
186            request.provide::<dyn ApplicationViewState>(self);
187            return;
188        }
189        self.content.provide_mut(request);
190    }
191
192    // Whoever reaches the view's widget by its type finds it through the
193    // part that keeps it up.
194    fn as_any(&self) -> Option<&dyn std::any::Any> {
195        self.content.as_any()
196    }
197
198    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
199        self.content.as_any_mut()
200    }
201
202    fn name(&self) -> &'static str {
203        "Upkeep"
204    }
205
206    // It adds no structure of its own: what the content changed is all.
207    fn take_structure_change(&mut self) -> ui_core::widget::StructureChange {
208        self.content.take_structure_change()
209    }
210
211    fn sync(&mut self, state: &AppModel, path: &ModelPath) {
212        if let Some(sync) = self.upkeep.sync.as_mut() {
213            sync();
214        }
215        self.content.sync(state, path);
216    }
217
218    fn draw(
219        &mut self,
220        scene: &mut Scene,
221        transform: Affine,
222        text_renderer: &mut TextRenderer,
223        theme: &Theme,
224        presence: &PresenceCtx,
225    ) {
226        self.content
227            .draw(scene, transform, text_renderer, theme, presence);
228        if let Some(presented) = self.upkeep.presented.as_mut() {
229            presented();
230        }
231    }
232
233    fn needs_animation_frame(&self) -> bool {
234        self.upkeep.busy.as_ref().is_some_and(|busy| busy()) || self.content.needs_animation_frame()
235    }
236
237    fn next_wake_deadline(&self) -> Option<Instant> {
238        let own = self.upkeep.next_wake.as_ref().and_then(|next| next());
239        match (own, self.content.next_wake_deadline()) {
240            (Some(own), Some(content)) => Some(own.min(content)),
241            (own, content) => own.or(content),
242        }
243    }
244
245    fn has_pending_visual_work(&self) -> bool {
246        self.upkeep
247            .pending
248            .as_ref()
249            .is_some_and(|pending| pending())
250            || self.content.has_pending_visual_work()
251    }
252
253    fn appearance_intent(&self) -> Option<AppearanceIntent> {
254        self.upkeep
255            .appearance
256            .as_ref()
257            .and_then(|appearance| appearance())
258            .or_else(|| self.content.appearance_intent())
259    }
260}
261
262impl Collaborative for Kept {
263    fn take_collab_requests(&mut self) -> Vec<CollabRequest> {
264        self.upkeep
265            .collaboration
266            .as_mut()
267            .map_or_else(Vec::new, |(take, _)| take())
268    }
269
270    fn handle_collab_event(&mut self, event: &CollabEvent) {
271        if let Some((_, handle)) = self.upkeep.collaboration.as_mut() {
272            handle(event);
273        }
274    }
275}
276
277impl ApplicationViewState for Kept {
278    fn capture_application_view(&self) -> Result<Option<ApplicationLocalView>, ThawError> {
279        Ok(self.upkeep.view.as_ref().map(|view| ApplicationLocalView {
280            app_id: view.app_id.clone(),
281            version: view.version,
282            payload: (view.capture)(),
283        }))
284    }
285
286    fn restore_application_view(
287        &mut self,
288        local: &ApplicationLocalView,
289        _doc: &ui_core::automerge::AutoCommit,
290    ) -> Result<ApplicationFocusRestore, ThawError> {
291        let view = self
292            .upkeep
293            .view
294            .as_mut()
295            .ok_or_else(|| ThawError::State("no kept view".into()))?;
296        if local.app_id != view.app_id || local.version != view.version {
297            return Err(ThawError::State(format!(
298                "a view of {} at version {}, not {} at {}",
299                local.app_id, local.version, view.app_id, view.version
300            )));
301        }
302        (view.restore)(&local.payload).map_err(ThawError::State)?;
303        Ok(ApplicationFocusRestore::Generic)
304    }
305}
306
307#[cfg(test)]
308#[path = "upkeep_tests.rs"]
309mod tests;