guinea-eframe 0.13.7

guinea on egui: the router and the application runtime, drawn immediately
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
//! guinea on egui: the same router and features, drawn immediately.
//!
//! The closest relative among the backends is ratatui, not WinUI: egui is
//! immediate, so a view is not a tree that is kept and diffed - it is drawing
//! that happens inside one frame and leaves nothing behind. As there, a view
//! is a [`Node`]: drawing deferred until someone supplies the [`egui::Ui`] to
//! draw into, which is what lets a layout decide where its child goes.
//!
//! What differs from the terminal is who owns the loop. eframe owns it, the
//! way the reactor does under WinUI, so [`run`] hands it over and puts the
//! frame inside `eframe::App::update`. And unlike a terminal, egui sleeps when
//! nothing happens - so work finished on another thread has to wake it, which
//! is what the dispatcher's `request_repaint` is for.

mod dispatcher;
mod nav;
mod run;

pub use run::{MAIN, run};

use std::any::{Any, TypeId};
use std::cell::RefCell;
use std::collections::HashMap;

use guinea_app::feature::{FeatureInitContext, Reaches, Segment};
use guinea_core::binding::ReducerBinding;
use guinea_core::scope::Reducer;
use guinea_router::router::{
    Mount, NavigateHandle, RouteChain, SegmentEntry, SegmentProps, Ui, single_entry_chain,
};

/// egui as a [`Ui`].
pub struct Egui;

impl Ui for Egui {
    type View<'a> = Node;
    /// Nothing: an immediate-mode view draws from a snapshot inside the frame
    /// and holds no reference to state afterwards.
    type Nodes = ();
}

/// Drawing that has not happened yet.
///
/// `FnOnce` because a node is drawn exactly once per frame - the next frame
/// mounts fresh ones.
pub struct Node(Box<dyn FnOnce(&mut egui::Ui)>);

impl Node {
    pub fn new(draw: impl FnOnce(&mut egui::Ui) + 'static) -> Self {
        Self(Box::new(draw))
    }

    /// Draws into `ui`.
    pub fn draw(self, ui: &mut egui::Ui) {
        (self.0)(ui)
    }
}

/// A leaf of the route tree, and its own state.
///
/// The struct that implements this **is** the page's state, as in the WinUI
/// and iced backends. What differs is that immediate mode has no later: the
/// frame that sees the click is the frame that answers it, so there is no
/// message and no `update` - [`Page::render`] takes `&mut self` and writes
/// what it decided where it decided it.
///
/// What belongs here is what only this page has an opinion about: which row
/// is picked, which tab is open, what is typed in a filter. What crosses the
/// segment - what a domain owns, what another page reads - is a reducer, and
/// reaches this page through [`PageCx::state`].
pub trait Page: Default + Sized + 'static {
    /// When `true`, the router keeps this page's reducer states in memory
    /// while the page is not mounted.
    const CACHE_STATE_IN_MEMORY: bool = false;

    /// Where `impl Page` was written. `#[segment]` fills it in; an impl
    /// without it loses only the source link.
    const DECLARED: Option<guinea_core::actor::shape::Declared> = None;

    /// What this page captured from the route, named by `routes!`. `()` for a
    /// page that captures nothing.
    ///
    /// `PartialEq` because the router's one question about a capture is
    /// whether it is still the same one - which decides what reinstalls and
    /// which cached state may come back.
    type Params: PartialEq + 'static;

    /// What this segment installs, and `()` when it installs nothing.
    ///
    /// The list is not written beside the body - it *is* the body's
    /// obligation: `install` returns it, so a feature that stops being
    /// installed stops type-checking. Which is also why `install` has no
    /// default any more.
    ///
    /// What is returned is owned by the segment's scope, which is what gives a
    /// feature its own lifetime.
    type Installs: 'static;

    fn install(ctx: &FeatureInitContext, params: &Self::Params) -> anyhow::Result<Self::Installs>;

    /// The state it starts with, when `Default` is not it.
    ///
    /// A constructor, not an effect: it runs once per mount, beside
    /// [`install`](Self::install), and anything that has to reach a feature
    /// belongs there instead.
    fn init(_ctx: &FeatureInitContext, _params: &Self::Params) -> Self {
        Self::default()
    }

    /// Draws the page, and changes it. Runs again for every frame, so this is
    /// the drawing itself and not a description of it.
    fn render(&mut self, cx: &mut PageCx<'_, Self>);
}

/// A branch: draws its own chrome and decides where its child goes. Its own
/// state, the same way a [`Page`] is.
pub trait Layout: Default + Sized + 'static {
    /// Where `impl Layout` was written; see [`Page::DECLARED`].
    const DECLARED: Option<guinea_core::actor::shape::Declared> = None;

    /// What every page under this layout carries, derived by `routes!` as the
    /// intersection of their parameters. A layout declares nothing; it is
    /// handed what all of its children were reached with.
    type Params: PartialEq + 'static;

    /// What this segment installs, and `()` when it installs nothing.
    ///
    /// The list is not written beside the body - it *is* the body's
    /// obligation: `install` returns it, so a feature that stops being
    /// installed stops type-checking. Which is also why `install` has no
    /// default any more.
    ///
    /// What is returned is owned by the segment's scope, which is what gives a
    /// feature its own lifetime.
    type Installs: 'static;

    fn install(ctx: &FeatureInitContext, params: &Self::Params) -> anyhow::Result<Self::Installs>;

    /// See [`Page::init`].
    fn init(_ctx: &FeatureInitContext, _params: &Self::Params) -> Self {
        Self::default()
    }

    fn render(&mut self, cx: &mut LayoutCx<'_, Self>);
}

pub const fn segment_entry<P: Page>() -> SegmentEntry<Egui> {
    SegmentEntry::new::<P>(
        install_page::<P>,
        guinea_router::router::same_params::<P::Params>,
        &const { MountPage::<P>(std::marker::PhantomData) },
        P::CACHE_STATE_IN_MEMORY,
    )
    .written(P::DECLARED)
}

pub const fn layout_entry<L: Layout>() -> SegmentEntry<Egui> {
    SegmentEntry::new::<L>(
        install_layout::<L>,
        guinea_router::router::same_params::<L::Params>,
        &const { MountLayout::<L>(std::marker::PhantomData) },
        false,
    )
    .written(L::DECLARED)
}

fn install_page<P: Page>(
    ctx: &FeatureInitContext,
    params: &dyn std::any::Any,
) -> anyhow::Result<()> {
    let params = guinea_router::router::narrow::<P::Params, P>(params)?;
    own(ctx, P::install(ctx, params)?);
    keep(ctx, P::init(ctx, params));
    Ok(())
}

/// Hands what a segment installed to its scope - a feature's lifetime is the
/// segment's, and dropping this here would end it at the end of `install`.
fn own<T: 'static>(ctx: &FeatureInitContext, installed: T) {
    ctx.scope.own(guinea_core::scope::DropGuard(installed));
}

fn install_layout<L: Layout>(
    ctx: &FeatureInitContext,
    params: &dyn std::any::Any,
) -> anyhow::Result<()> {
    let params = guinea_router::router::narrow::<L::Params, L>(params)?;
    own(ctx, L::install(ctx, params)?);
    keep(ctx, L::init(ctx, params));
    Ok(())
}

thread_local! {
    /// Every mounted segment's own state, by the scope it is mounted in and
    /// what it is.
    ///
    /// A segment's state has to outlive the frame and die with the mount,
    /// and egui gives it nowhere to live: a [`Node`] is drawn once and
    /// dropped. So the scope holds it - through this, because a scope keeps
    /// reducers and teardowns, not nodes.
    static MOUNTED: RefCell<HashMap<(usize, TypeId), Option<Box<dyn Any>>>> =
        RefCell::new(HashMap::new());
}

/// Holds `node` for as long as the segment being installed is mounted.
fn keep<S: 'static>(ctx: &FeatureInitContext, node: S) {
    let at = (ctx.scope.key(), TypeId::of::<S>());

    MOUNTED.with(|mounted| mounted.borrow_mut().insert(at, Some(Box::new(node))));
    ctx.scope.own(Forget(at));
}

/// Drops a segment's state when its scope goes.
struct Forget((usize, TypeId));

impl guinea_core::scope::Teardown for Forget {
    fn teardown(self) {
        // `try_with`: a scope can outlive the thread local at thread
        // teardown, and this runs from a `Drop`.
        let _ = MOUNTED.try_with(|mounted| mounted.borrow_mut().remove(&self.0));
    }
}

/// Draws with the segment's own state.
///
/// Taken out for the frame and put back after it, rather than borrowed
/// across it: a segment draws its child inside its own drawing, and a page
/// that navigates while drawing ends its own mount - after which there is
/// nowhere to put anything back, and the state goes with it.
///
/// A segment mounted with no `install` behind it - which a test does, and
/// nothing else - draws from a default that lasts the frame.
fn with_mounted<S: Default + 'static, R>(scope: usize, draw: impl FnOnce(&mut S) -> R) -> R {
    let at = (scope, TypeId::of::<S>());

    let taken = MOUNTED.with(|mounted| mounted.borrow_mut().get_mut(&at).and_then(Option::take));
    let mut node = taken
        .and_then(|node| node.downcast::<S>().ok())
        .map_or_else(S::default, |node| *node);

    let drawn = draw(&mut node);

    MOUNTED.with(|mounted| {
        if let Some(slot) = mounted.borrow_mut().get_mut(&at) {
            *slot = Some(Box::new(node));
        }
    });

    drawn
}

/// A zero-sized marker per segment type: what a `const` entry points at to get
/// its `&'static dyn Mount`.
pub struct MountPage<P>(pub std::marker::PhantomData<P>);
pub struct MountLayout<L>(pub std::marker::PhantomData<L>);

impl<P: Page> Mount<Egui> for MountPage<P> {
    fn view<'a>(&self, props: SegmentProps<Egui>, _nodes: &'a ()) -> Node {
        let at = props.scopes[props.cursor].key();

        Node::new(move |ui| {
            let _drawing = guinea_core::devtools::Rendering::of(std::any::type_name::<P>());
            with_mounted::<P, _>(at, |page| {
                page.render(&mut PageCx {
                    ui,
                    props,
                    page: std::marker::PhantomData,
                })
            })
        })
    }
}

impl<L: Layout> Mount<Egui> for MountLayout<L> {
    fn view<'a>(&self, props: SegmentProps<Egui>, _nodes: &'a ()) -> Node {
        let at = props.scopes[props.cursor].key();

        Node::new(move |ui| {
            let _drawing = guinea_core::devtools::Rendering::of(std::any::type_name::<L>());
            with_mounted::<L, _>(at, |layout| {
                layout.render(&mut LayoutCx {
                    ui,
                    props,
                    layout: std::marker::PhantomData,
                })
            })
        })
    }
}

/// A one-segment chain, for a page drawn without a route tree.
pub fn page_chain<P: Page>() -> &'static [SegmentEntry<Egui>] {
    single_entry_chain(segment_entry::<P>())
}

/// What a page's drawing is handed.
///
/// Carries the page type, not because drawing needs it, but because reading
/// does: what a segment may read is a fact about where it sits, and this is
/// where that fact enters the signature.
pub struct PageCx<'a, P> {
    ui: &'a mut egui::Ui,
    props: SegmentProps<Egui>,
    page: std::marker::PhantomData<fn() -> P>,
}

impl<P: Segment> PageCx<'_, P> {
    /// The reducer's state and actions.
    ///
    /// No subscription, as in the terminal: egui redraws the whole frame, so
    /// there is nothing to invalidate - the next pass reads the state again.
    /// What does need saying is when a frame should happen at all, and that is
    /// the dispatcher's job.
    ///
    /// Which feature answers is settled at build time: this page installed it,
    /// or a segment above listed it in `Exports`. The `_` is [`Reaches`]'s
    /// index, which says which of several impls applied - Rust has no partial
    /// turbofish, so it has to be written.
    ///
    /// The state comes shared, not copied: reading it every frame costs a
    /// count, and a change made mid-frame goes to a copy.
    pub fn state<R, I>(&self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
    where
        R: Reducer,
        P: Reaches<R, I>,
    {
        let binding = self.props.binding::<R>();
        (binding.get(), binding.dispatch())
    }

    /// The reducer's binding: its state, and a push straight into it.
    ///
    /// For state the UI owns outright - what is picked in a tree, which tab
    /// is open - claimed with `cx.state::<R>().plain()`. There is no domain
    /// to ask, so there is no actor to ask it through: a click is the whole
    /// story.
    ///
    /// State a feature drives is not this: pushing into it goes behind the
    /// back of whatever answers for it. Use [`PageCx::state`] and emit.
    pub fn binding<R, I>(&self) -> ReducerBinding<R>
    where
        R: Reducer,
        P: Reaches<R, I>,
    {
        self.props.binding::<R>()
    }
}

impl<P> PageCx<'_, P> {
    pub fn ui(&mut self) -> &mut egui::Ui {
        self.ui
    }

    /// A navigator over the route type the application runs.
    pub fn navigate<R>(&self) -> NavigateHandle<Egui, R>
    where
        R: RouteChain<Egui> + Clone + PartialEq + 'static,
    {
        nav::current::<R>()
    }
}

/// What a layout's drawing is handed. Same as a page's, plus the child.
pub struct LayoutCx<'a, L> {
    ui: &'a mut egui::Ui,
    props: SegmentProps<Egui>,
    layout: std::marker::PhantomData<fn() -> L>,
}

impl<L: Segment> LayoutCx<'_, L> {
    /// See [`PageCx::state`].
    pub fn state<R, I>(&self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
    where
        R: Reducer,
        L: Reaches<R, I>,
    {
        let binding = self.props.binding::<R>();
        (binding.get(), binding.dispatch())
    }

    /// See [`PageCx::binding`].
    pub fn binding<R, I>(&self) -> ReducerBinding<R>
    where
        R: Reducer,
        L: Reaches<R, I>,
    {
        self.props.binding::<R>()
    }
}

impl<L> LayoutCx<'_, L> {
    pub fn ui(&mut self) -> &mut egui::Ui {
        self.ui
    }

    /// A navigator over the route type the application runs.
    pub fn navigate<R>(&self) -> NavigateHandle<Egui, R>
    where
        R: RouteChain<Egui> + Clone + PartialEq + 'static,
    {
        nav::current::<R>()
    }

    /// The next segment down the chain, for the layout to draw where it wants.
    ///
    /// Handed over rather than drawn here: a layout takes its `ui` from this
    /// same context, so a method that drew the child would need the context
    /// twice at once.
    pub fn outlet(&self) -> Node {
        self.props.outlet(&())
    }

    /// Whether the segment directly below is `P`.
    ///
    /// What a tab strip needs, and cheaper than it looks: the chain already
    /// says which page is mounted, so highlighting the current tab needs
    /// neither the route nor a copy of it in state.
    pub fn child_is<P: 'static>(&self) -> bool {
        self.props
            .chain
            .get(self.props.cursor + 1)
            .is_some_and(|entry| (entry.type_id)() == std::any::TypeId::of::<P>())
    }
}