Skip to main content

guinea_eframe/
lib.rs

1//! guinea on egui: the same router and features, drawn immediately.
2//!
3//! The closest relative among the backends is ratatui, not WinUI: egui is
4//! immediate, so a view is not a tree that is kept and diffed - it is drawing
5//! that happens inside one frame and leaves nothing behind. As there, a view
6//! is a [`Node`]: drawing deferred until someone supplies the [`egui::Ui`] to
7//! draw into, which is what lets a layout decide where its child goes.
8//!
9//! What differs from the terminal is who owns the loop. eframe owns it, the
10//! way the reactor does under WinUI, so [`run`] hands it over and puts the
11//! frame inside `eframe::App::update`. And unlike a terminal, egui sleeps when
12//! nothing happens - so work finished on another thread has to wake it, which
13//! is what the dispatcher's `request_repaint` is for.
14
15mod dispatcher;
16mod nav;
17mod run;
18
19pub use run::{MAIN, run};
20
21use std::any::{Any, TypeId};
22use std::cell::RefCell;
23use std::collections::HashMap;
24
25use guinea_app::feature::{FeatureInitContext, Reaches, Reads, Segment};
26use guinea_core::binding::ReducerBinding;
27use guinea_core::scope::Reducer;
28use guinea_router::router::{
29    Mount, NavigateHandle, RouteChain, SegmentEntry, SegmentProps, Ui, single_entry_chain,
30};
31
32/// egui as a [`Ui`].
33pub struct Egui;
34
35impl Ui for Egui {
36    type View<'a> = Node;
37    /// Nothing: an immediate-mode view draws from a snapshot inside the frame
38    /// and holds no reference to state afterwards.
39    type Nodes = ();
40}
41
42/// Drawing that has not happened yet.
43///
44/// `FnOnce` because a node is drawn exactly once per frame - the next frame
45/// mounts fresh ones.
46pub struct Node(Box<dyn FnOnce(&mut egui::Ui)>);
47
48impl Node {
49    pub fn new(draw: impl FnOnce(&mut egui::Ui) + 'static) -> Self {
50        Self(Box::new(draw))
51    }
52
53    /// Draws into `ui`.
54    pub fn draw(self, ui: &mut egui::Ui) {
55        (self.0)(ui)
56    }
57}
58
59/// A leaf of the route tree, and its own state.
60///
61/// The struct that implements this **is** the page's state, as in the WinUI
62/// and iced backends. What differs is that immediate mode has no later: the
63/// frame that sees the click is the frame that answers it, so there is no
64/// message and no `update` - [`Page::render`] takes `&mut self` and writes
65/// what it decided where it decided it.
66///
67/// What belongs here is what only this page has an opinion about: which row
68/// is picked, which tab is open, what is typed in a filter. What crosses the
69/// segment - what a domain owns, what another page reads - is a reducer, and
70/// reaches this page through [`PageCx::state`].
71pub trait Page: Default + Sized + 'static {
72    /// When `true`, the router keeps this page's reducer states in memory
73    /// while the page is not mounted.
74    const CACHE_STATE_IN_MEMORY: bool = false;
75
76    /// Where `impl Page` was written. `#[segment]` fills it in; an impl
77    /// without it loses only the source link.
78    const DECLARED: Option<guinea_core::actor::shape::Declared> = None;
79
80    /// What this page captured from the route, named by `routes!`. `()` for a
81    /// page that captures nothing.
82    ///
83    /// `PartialEq` because the router's one question about a capture is
84    /// whether it is still the same one - which decides what reinstalls and
85    /// which cached state may come back.
86    type Params: PartialEq + 'static;
87
88    /// What this segment installs, and `()` when it installs nothing.
89    ///
90    /// The list is not written beside the body - it *is* the body's
91    /// obligation: `install` returns it, so a feature that stops being
92    /// installed stops type-checking. Which is also why `install` has no
93    /// default any more.
94    ///
95    /// What is returned is owned by the segment's scope, which is what gives a
96    /// feature its own lifetime.
97    type Installs: 'static;
98
99    fn install(ctx: &FeatureInitContext, params: &Self::Params) -> anyhow::Result<Self::Installs>;
100
101    /// The state it starts with, when `Default` is not it.
102    ///
103    /// A constructor, not an effect: it runs once per mount, beside
104    /// [`install`](Self::install), and anything that has to reach a feature
105    /// belongs there instead.
106    fn init(_ctx: &FeatureInitContext, _params: &Self::Params) -> Self {
107        Self::default()
108    }
109
110    /// Draws the page, and changes it. Runs again for every frame, so this is
111    /// the drawing itself and not a description of it.
112    fn render(&mut self, cx: &mut PageCx<'_, Self>);
113}
114
115/// A branch: draws its own chrome and decides where its child goes. Its own
116/// state, the same way a [`Page`] is.
117pub trait Layout: Default + Sized + 'static {
118    /// Where `impl Layout` was written; see [`Page::DECLARED`].
119    const DECLARED: Option<guinea_core::actor::shape::Declared> = None;
120
121    /// What every page under this layout carries, derived by `routes!` as the
122    /// intersection of their parameters. A layout declares nothing; it is
123    /// handed what all of its children were reached with.
124    type Params: PartialEq + 'static;
125
126    /// What this segment installs, and `()` when it installs nothing.
127    ///
128    /// The list is not written beside the body - it *is* the body's
129    /// obligation: `install` returns it, so a feature that stops being
130    /// installed stops type-checking. Which is also why `install` has no
131    /// default any more.
132    ///
133    /// What is returned is owned by the segment's scope, which is what gives a
134    /// feature its own lifetime.
135    type Installs: 'static;
136
137    fn install(ctx: &FeatureInitContext, params: &Self::Params) -> anyhow::Result<Self::Installs>;
138
139    /// See [`Page::init`].
140    fn init(_ctx: &FeatureInitContext, _params: &Self::Params) -> Self {
141        Self::default()
142    }
143
144    fn render(&mut self, cx: &mut LayoutCx<'_, Self>);
145}
146
147pub const fn segment_entry<P: Page>() -> SegmentEntry<Egui> {
148    SegmentEntry::new::<P>(
149        install_page::<P>,
150        guinea_router::router::same_params::<P::Params>,
151        &const { MountPage::<P>(std::marker::PhantomData) },
152        P::CACHE_STATE_IN_MEMORY,
153    )
154    .written(P::DECLARED)
155}
156
157pub const fn layout_entry<L: Layout>() -> SegmentEntry<Egui> {
158    SegmentEntry::new::<L>(
159        install_layout::<L>,
160        guinea_router::router::same_params::<L::Params>,
161        &const { MountLayout::<L>(std::marker::PhantomData) },
162        false,
163    )
164    .written(L::DECLARED)
165}
166
167fn install_page<P: Page>(
168    ctx: &FeatureInitContext,
169    params: &dyn std::any::Any,
170) -> anyhow::Result<()> {
171    let params = guinea_router::router::narrow::<P::Params, P>(params)?;
172    own(ctx, P::install(ctx, params)?);
173    keep(ctx, P::init(ctx, params));
174    Ok(())
175}
176
177/// Hands what a segment installed to its scope - a feature's lifetime is the
178/// segment's, and dropping this here would end it at the end of `install`.
179fn own<T: 'static>(ctx: &FeatureInitContext, installed: T) {
180    ctx.scope.own(guinea_core::scope::DropGuard(installed));
181}
182
183fn install_layout<L: Layout>(
184    ctx: &FeatureInitContext,
185    params: &dyn std::any::Any,
186) -> anyhow::Result<()> {
187    let params = guinea_router::router::narrow::<L::Params, L>(params)?;
188    own(ctx, L::install(ctx, params)?);
189    keep(ctx, L::init(ctx, params));
190    Ok(())
191}
192
193thread_local! {
194    /// Every mounted segment's own state, by the scope it is mounted in and
195    /// what it is.
196    ///
197    /// A segment's state has to outlive the frame and die with the mount,
198    /// and egui gives it nowhere to live: a [`Node`] is drawn once and
199    /// dropped. So the scope holds it - through this, because a scope keeps
200    /// reducers and teardowns, not nodes.
201    static MOUNTED: RefCell<HashMap<(usize, TypeId), Option<Box<dyn Any>>>> =
202        RefCell::new(HashMap::new());
203}
204
205/// Holds `node` for as long as the segment being installed is mounted.
206fn keep<S: 'static>(ctx: &FeatureInitContext, node: S) {
207    let at = (ctx.scope.key(), TypeId::of::<S>());
208
209    MOUNTED.with(|mounted| mounted.borrow_mut().insert(at, Some(Box::new(node))));
210    ctx.scope.own(Forget(at));
211}
212
213/// Drops a segment's state when its scope goes.
214struct Forget((usize, TypeId));
215
216impl guinea_core::scope::Teardown for Forget {
217    fn teardown(self) {
218        // `try_with`: a scope can outlive the thread local at thread
219        // teardown, and this runs from a `Drop`.
220        let _ = MOUNTED.try_with(|mounted| mounted.borrow_mut().remove(&self.0));
221    }
222}
223
224/// Draws with the segment's own state.
225///
226/// Taken out for the frame and put back after it, rather than borrowed
227/// across it: a segment draws its child inside its own drawing, and a page
228/// that navigates while drawing ends its own mount - after which there is
229/// nowhere to put anything back, and the state goes with it.
230///
231/// A segment mounted with no `install` behind it - which a test does, and
232/// nothing else - draws from a default that lasts the frame.
233fn with_mounted<S: Default + 'static, R>(scope: usize, draw: impl FnOnce(&mut S) -> R) -> R {
234    let at = (scope, TypeId::of::<S>());
235
236    let taken = MOUNTED.with(|mounted| mounted.borrow_mut().get_mut(&at).and_then(Option::take));
237    let mut node = taken
238        .and_then(|node| node.downcast::<S>().ok())
239        .map_or_else(S::default, |node| *node);
240
241    let drawn = draw(&mut node);
242
243    MOUNTED.with(|mounted| {
244        if let Some(slot) = mounted.borrow_mut().get_mut(&at) {
245            *slot = Some(Box::new(node));
246        }
247    });
248
249    drawn
250}
251
252/// A zero-sized marker per segment type: what a `const` entry points at to get
253/// its `&'static dyn Mount`.
254pub struct MountPage<P>(pub std::marker::PhantomData<P>);
255pub struct MountLayout<L>(pub std::marker::PhantomData<L>);
256
257impl<P: Page> Mount<Egui> for MountPage<P> {
258    fn view<'a>(&self, props: SegmentProps<Egui>, _nodes: &'a ()) -> Node {
259        let at = props.scopes[props.cursor].key();
260
261        Node::new(move |ui| {
262            let _drawing = guinea_core::devtools::Rendering::of(std::any::type_name::<P>());
263            with_mounted::<P, _>(at, |page| {
264                page.render(&mut PageCx {
265                    ui,
266                    props,
267                    page: std::marker::PhantomData,
268                })
269            })
270        })
271    }
272}
273
274impl<L: Layout> Mount<Egui> for MountLayout<L> {
275    fn view<'a>(&self, props: SegmentProps<Egui>, _nodes: &'a ()) -> Node {
276        let at = props.scopes[props.cursor].key();
277
278        Node::new(move |ui| {
279            let _drawing = guinea_core::devtools::Rendering::of(std::any::type_name::<L>());
280            with_mounted::<L, _>(at, |layout| {
281                layout.render(&mut LayoutCx {
282                    ui,
283                    props,
284                    layout: std::marker::PhantomData,
285                })
286            })
287        })
288    }
289}
290
291/// A one-segment chain, for a page drawn without a route tree.
292pub fn page_chain<P: Page>() -> &'static [SegmentEntry<Egui>] {
293    single_entry_chain(segment_entry::<P>())
294}
295
296/// What a page's drawing is handed.
297///
298/// Carries the page type, not because drawing needs it, but because reading
299/// does: what a segment may read is a fact about where it sits, and this is
300/// where that fact enters the signature.
301pub struct PageCx<'a, P> {
302    ui: &'a mut egui::Ui,
303    props: SegmentProps<Egui>,
304    page: std::marker::PhantomData<fn() -> P>,
305}
306
307impl<P: Segment> PageCx<'_, P> {
308    /// The reducer's state and actions.
309    ///
310    /// No subscription, as in the terminal: egui redraws the whole frame, so
311    /// there is nothing to invalidate - the next pass reads the state again.
312    /// What does need saying is when a frame should happen at all, and that is
313    /// the dispatcher's job.
314    ///
315    /// Which feature answers is settled at build time: this page installed it,
316    /// or a segment above listed it in `Exports`. The `_` is [`Reaches`]'s
317    /// index, which says which of several impls applied - Rust has no partial
318    /// turbofish, so it has to be written.
319    ///
320    /// The state comes shared, not copied: reading it every frame costs a
321    /// count, and a change made mid-frame goes to a copy.
322    pub fn state<R, I>(&self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
323    where
324        R: Reducer,
325        P: Reaches<R, I>,
326    {
327        let binding = self.props.binding::<R>();
328        (binding.get(), binding.dispatch())
329    }
330
331    /// The reducer's binding: its state, and a push straight into it.
332    ///
333    /// For state the UI owns outright - what is picked in a tree, which tab
334    /// is open - claimed with `cx.state::<R>().plain()`. There is no domain
335    /// to ask, so there is no actor to ask it through: a click is the whole
336    /// story.
337    ///
338    /// State a feature drives is not this: pushing into it goes behind the
339    /// back of whatever answers for it. Use [`PageCx::state`] and emit.
340    pub fn binding<R, I>(&self) -> ReducerBinding<R>
341    where
342        R: Reducer,
343        P: Reaches<R, I>,
344    {
345        self.props.binding::<R>()
346    }
347}
348
349impl<P: Segment> Reads for PageCx<'_, P> {
350    type Segment = P;
351
352    fn read<R, I>(&mut self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
353    where
354        R: Reducer + PartialEq,
355        P: Reaches<R, I>,
356    {
357        self.state::<R, I>()
358    }
359
360    fn dispatch<R, I>(&self) -> guinea_core::feature::Dispatch
361    where
362        R: Reducer,
363        P: Reaches<R, I>,
364    {
365        self.props.binding::<R>().dispatch()
366    }
367}
368
369impl<P> PageCx<'_, P> {
370    pub fn ui(&mut self) -> &mut egui::Ui {
371        self.ui
372    }
373
374    /// A navigator over the route type the application runs.
375    pub fn navigate<R>(&self) -> NavigateHandle<Egui, R>
376    where
377        R: RouteChain<Egui> + Clone + PartialEq + 'static,
378    {
379        nav::current::<R>()
380    }
381}
382
383/// What a layout's drawing is handed. Same as a page's, plus the child.
384pub struct LayoutCx<'a, L> {
385    ui: &'a mut egui::Ui,
386    props: SegmentProps<Egui>,
387    layout: std::marker::PhantomData<fn() -> L>,
388}
389
390impl<L: Segment> LayoutCx<'_, L> {
391    /// See [`PageCx::state`].
392    pub fn state<R, I>(&self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
393    where
394        R: Reducer,
395        L: Reaches<R, I>,
396    {
397        let binding = self.props.binding::<R>();
398        (binding.get(), binding.dispatch())
399    }
400
401    /// See [`PageCx::binding`].
402    pub fn binding<R, I>(&self) -> ReducerBinding<R>
403    where
404        R: Reducer,
405        L: Reaches<R, I>,
406    {
407        self.props.binding::<R>()
408    }
409}
410
411impl<L: Segment> Reads for LayoutCx<'_, L> {
412    type Segment = L;
413
414    fn read<R, I>(&mut self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
415    where
416        R: Reducer + PartialEq,
417        L: Reaches<R, I>,
418    {
419        self.state::<R, I>()
420    }
421
422    fn dispatch<R, I>(&self) -> guinea_core::feature::Dispatch
423    where
424        R: Reducer,
425        L: Reaches<R, I>,
426    {
427        self.props.binding::<R>().dispatch()
428    }
429}
430
431impl<L> LayoutCx<'_, L> {
432    pub fn ui(&mut self) -> &mut egui::Ui {
433        self.ui
434    }
435
436    /// A navigator over the route type the application runs.
437    pub fn navigate<R>(&self) -> NavigateHandle<Egui, R>
438    where
439        R: RouteChain<Egui> + Clone + PartialEq + 'static,
440    {
441        nav::current::<R>()
442    }
443
444    /// The next segment down the chain, for the layout to draw where it wants.
445    ///
446    /// Handed over rather than drawn here: a layout takes its `ui` from this
447    /// same context, so a method that drew the child would need the context
448    /// twice at once.
449    pub fn outlet(&self) -> Node {
450        self.props.outlet(&())
451    }
452
453    /// Whether the segment directly below is `P`.
454    ///
455    /// What a tab strip needs, and cheaper than it looks: the chain already
456    /// says which page is mounted, so highlighting the current tab needs
457    /// neither the route nor a copy of it in state.
458    pub fn child_is<P: 'static>(&self) -> bool {
459        self.props
460            .chain
461            .get(self.props.cursor + 1)
462            .is_some_and(|entry| (entry.type_id)() == std::any::TypeId::of::<P>())
463    }
464}