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, Reads};
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: guinea_app::feature::Lists;
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: guinea_app::feature::Lists;
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        <P::Installs as guinea_app::feature::Lists>::list,
152        &const { MountPage::<P>(std::marker::PhantomData) },
153        P::CACHE_STATE_IN_MEMORY,
154    )
155    .written(P::DECLARED)
156}
157
158pub const fn layout_entry<L: Layout>() -> SegmentEntry<Egui> {
159    SegmentEntry::new::<L>(
160        install_layout::<L>,
161        guinea_router::router::same_params::<L::Params>,
162        <L::Installs as guinea_app::feature::Lists>::list,
163        &const { MountLayout::<L>(std::marker::PhantomData) },
164        false,
165    )
166    .written(L::DECLARED)
167}
168
169fn install_page<P: Page>(
170    ctx: &FeatureInitContext,
171    params: &dyn std::any::Any,
172) -> anyhow::Result<()> {
173    let params = guinea_router::router::narrow::<P::Params, P>(params)?;
174    own(ctx, P::install(ctx, params)?);
175    keep(ctx, P::init(ctx, params));
176    Ok(())
177}
178
179/// Hands what a segment installed to its scope - a feature's lifetime is the
180/// segment's, and dropping this here would end it at the end of `install`.
181fn own<T: 'static>(ctx: &FeatureInitContext, installed: T) {
182    ctx.scope.own(guinea_core::scope::DropGuard(installed));
183}
184
185fn install_layout<L: Layout>(
186    ctx: &FeatureInitContext,
187    params: &dyn std::any::Any,
188) -> anyhow::Result<()> {
189    let params = guinea_router::router::narrow::<L::Params, L>(params)?;
190    own(ctx, L::install(ctx, params)?);
191    keep(ctx, L::init(ctx, params));
192    Ok(())
193}
194
195thread_local! {
196    /// Every mounted segment's own state, by the scope it is mounted in and
197    /// what it is.
198    ///
199    /// A segment's state has to outlive the frame and die with the mount,
200    /// and egui gives it nowhere to live: a [`Node`] is drawn once and
201    /// dropped. So the scope holds it - through this, because a scope keeps
202    /// reducers and teardowns, not nodes.
203    static MOUNTED: RefCell<HashMap<(usize, TypeId), Option<Box<dyn Any>>>> =
204        RefCell::new(HashMap::new());
205}
206
207/// Holds `node` for as long as the segment being installed is mounted.
208fn keep<S: 'static>(ctx: &FeatureInitContext, node: S) {
209    let at = (ctx.scope.key(), TypeId::of::<S>());
210
211    MOUNTED.with(|mounted| mounted.borrow_mut().insert(at, Some(Box::new(node))));
212    ctx.scope.own(Forget(at));
213}
214
215/// Drops a segment's state when its scope goes.
216struct Forget((usize, TypeId));
217
218impl guinea_core::scope::Teardown for Forget {
219    fn teardown(self) {
220        // `try_with`: a scope can outlive the thread local at thread
221        // teardown, and this runs from a `Drop`.
222        let _ = MOUNTED.try_with(|mounted| mounted.borrow_mut().remove(&self.0));
223    }
224}
225
226/// Draws with the segment's own state.
227///
228/// Taken out for the frame and put back after it, rather than borrowed
229/// across it: a segment draws its child inside its own drawing, and a page
230/// that navigates while drawing ends its own mount - after which there is
231/// nowhere to put anything back, and the state goes with it.
232///
233/// A segment mounted with no `install` behind it - which a test does, and
234/// nothing else - draws from a default that lasts the frame.
235fn with_mounted<S: Default + 'static, R>(scope: usize, draw: impl FnOnce(&mut S) -> R) -> R {
236    let at = (scope, TypeId::of::<S>());
237
238    let taken = MOUNTED.with(|mounted| mounted.borrow_mut().get_mut(&at).and_then(Option::take));
239    let mut node = taken
240        .and_then(|node| node.downcast::<S>().ok())
241        .map_or_else(S::default, |node| *node);
242
243    let drawn = draw(&mut node);
244
245    MOUNTED.with(|mounted| {
246        if let Some(slot) = mounted.borrow_mut().get_mut(&at) {
247            *slot = Some(Box::new(node));
248        }
249    });
250
251    drawn
252}
253
254/// A zero-sized marker per segment type: what a `const` entry points at to get
255/// its `&'static dyn Mount`.
256pub struct MountPage<P>(pub std::marker::PhantomData<P>);
257pub struct MountLayout<L>(pub std::marker::PhantomData<L>);
258
259impl<P: Page> Mount<Egui> for MountPage<P> {
260    fn view<'a>(&self, props: SegmentProps<Egui>, _nodes: &'a ()) -> Node {
261        let at = props.scopes[props.cursor].key();
262
263        Node::new(move |ui| {
264            let _drawing = guinea_core::observability::Rendering::of(std::any::type_name::<P>());
265            with_mounted::<P, _>(at, |page| {
266                page.render(&mut PageCx {
267                    ui,
268                    props,
269                    page: std::marker::PhantomData,
270                })
271            })
272        })
273    }
274}
275
276impl<L: Layout> Mount<Egui> for MountLayout<L> {
277    fn view<'a>(&self, props: SegmentProps<Egui>, _nodes: &'a ()) -> Node {
278        let at = props.scopes[props.cursor].key();
279
280        Node::new(move |ui| {
281            let _drawing = guinea_core::observability::Rendering::of(std::any::type_name::<L>());
282            with_mounted::<L, _>(at, |layout| {
283                layout.render(&mut LayoutCx {
284                    ui,
285                    props,
286                    layout: std::marker::PhantomData,
287                })
288            })
289        })
290    }
291}
292
293/// A one-segment chain, for a page drawn without a route tree.
294pub fn page_chain<P: Page>() -> &'static [SegmentEntry<Egui>] {
295    single_entry_chain(segment_entry::<P>())
296}
297
298/// What a page's drawing is handed.
299///
300/// Carries the page type, not because drawing needs it, but because reading
301/// does: what a segment may read is a fact about where it sits, and this is
302/// where that fact enters the signature.
303pub struct PageCx<'a, P> {
304    ui: &'a mut egui::Ui,
305    props: SegmentProps<Egui>,
306    page: std::marker::PhantomData<fn() -> P>,
307}
308
309fn feature_of<R: Reducer>(
310    props: &SegmentProps<Egui>,
311) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch) {
312    let binding = props.binding::<R>();
313    (binding.get(), binding.dispatch())
314}
315
316impl<P> PageCx<'_, P> {
317    /// The reducer's state and actions.
318    ///
319    /// No subscription, as in the terminal: egui redraws the whole frame, so
320    /// there is nothing to invalidate - the next pass reads the state again.
321    /// What does need saying is when a frame should happen at all, and that is
322    /// the dispatcher's job.
323    ///
324    /// The feature that answers is the one this page installed, or the nearest
325    /// above that listed `R` in `Exports`. A read that reaches nothing panics
326    /// here, naming the chain it walked.
327    ///
328    /// The state comes shared, not copied: reading it every frame costs a
329    /// count, and a change made mid-frame goes to a copy.
330    pub fn read<R>(&mut self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
331    where
332        R: Reducer,
333    {
334        feature_of::<R>(&self.props)
335    }
336
337    /// The reducer's binding: its state, and a push straight into it.
338    ///
339    /// For state the UI owns outright - what is picked in a tree, which tab
340    /// is open - claimed with `cx.state::<R>().plain()`. There is no domain
341    /// to ask, so there is no actor to ask it through: a click is the whole
342    /// story.
343    ///
344    /// State a feature drives is not this: pushing into it goes behind the
345    /// back of whatever answers for it. Use [`PageCx::read`] and emit.
346    pub fn binding<R>(&self) -> ReducerBinding<R>
347    where
348        R: Reducer,
349    {
350        self.props.binding::<R>()
351    }
352}
353
354impl<P> Reads for PageCx<'_, P> {
355    fn read<R>(&mut self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
356    where
357        R: Reducer + PartialEq,
358    {
359        feature_of::<R>(&self.props)
360    }
361
362    fn dispatch<R>(&self) -> guinea_core::feature::Dispatch
363    where
364        R: Reducer,
365    {
366        self.props.binding::<R>().dispatch()
367    }
368}
369
370impl<P> PageCx<'_, P> {
371    pub fn ui(&mut self) -> &mut egui::Ui {
372        self.ui
373    }
374
375    /// A navigator over the route type the application runs.
376    pub fn navigate<R>(&self) -> NavigateHandle<Egui, R>
377    where
378        R: RouteChain<Egui> + Clone + PartialEq + 'static,
379    {
380        nav::current::<R>()
381    }
382}
383
384/// What a layout's drawing is handed. Same as a page's, plus the child.
385pub struct LayoutCx<'a, L> {
386    ui: &'a mut egui::Ui,
387    props: SegmentProps<Egui>,
388    layout: std::marker::PhantomData<fn() -> L>,
389}
390
391impl<L> LayoutCx<'_, L> {
392    /// See [`PageCx::read`].
393    pub fn read<R>(&mut self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
394    where
395        R: Reducer,
396    {
397        feature_of::<R>(&self.props)
398    }
399
400    /// See [`PageCx::binding`].
401    pub fn binding<R>(&self) -> ReducerBinding<R>
402    where
403        R: Reducer,
404    {
405        self.props.binding::<R>()
406    }
407}
408
409impl<L> Reads for LayoutCx<'_, L> {
410    fn read<R>(&mut self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
411    where
412        R: Reducer + PartialEq,
413    {
414        feature_of::<R>(&self.props)
415    }
416
417    fn dispatch<R>(&self) -> guinea_core::feature::Dispatch
418    where
419        R: Reducer,
420    {
421        self.props.binding::<R>().dispatch()
422    }
423}
424
425impl<L> LayoutCx<'_, L> {
426    pub fn ui(&mut self) -> &mut egui::Ui {
427        self.ui
428    }
429
430    /// A navigator over the route type the application runs.
431    pub fn navigate<R>(&self) -> NavigateHandle<Egui, R>
432    where
433        R: RouteChain<Egui> + Clone + PartialEq + 'static,
434    {
435        nav::current::<R>()
436    }
437
438    /// The next segment down the chain, for the layout to draw where it wants.
439    ///
440    /// Handed over rather than drawn here: a layout takes its `ui` from this
441    /// same context, so a method that drew the child would need the context
442    /// twice at once.
443    pub fn outlet(&self) -> Node {
444        self.props.outlet(&())
445    }
446
447    /// Whether the segment directly below is `P`.
448    ///
449    /// What a tab strip needs, and cheaper than it looks: the chain already
450    /// says which page is mounted, so highlighting the current tab needs
451    /// neither the route nor a copy of it in state.
452    pub fn child_is<P: 'static>(&self) -> bool {
453        self.props
454            .chain
455            .get(self.props.cursor + 1)
456            .is_some_and(|entry| (entry.type_id)() == std::any::TypeId::of::<P>())
457    }
458}