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 guinea_app::feature::{FeatureInitContext, Reads};
22use guinea_core::binding::ReducerBinding;
23use guinea_core::scope::Reducer;
24use guinea_router::router::{
25    Mount, NavigateHandle, RouteChain, SegmentEntry, SegmentProps, Ui, single_entry_chain,
26};
27
28/// egui as a [`Ui`].
29pub struct Egui;
30
31impl Ui for Egui {
32    type View<'a> = Node;
33    /// Nothing: an immediate-mode view draws from a snapshot inside the frame
34    /// and holds no reference to state afterwards.
35    type Nodes = ();
36    type Mount = dyn Mount<Self>;
37}
38
39/// Drawing that has not happened yet.
40///
41/// `FnOnce` because a node is drawn exactly once per frame - the next frame
42/// mounts fresh ones.
43pub struct Node(Box<dyn FnOnce(&mut egui::Ui)>);
44
45impl Node {
46    pub fn new(draw: impl FnOnce(&mut egui::Ui) + 'static) -> Self {
47        Self(Box::new(draw))
48    }
49
50    /// Draws into `ui`.
51    pub fn draw(self, ui: &mut egui::Ui) {
52        (self.0)(ui)
53    }
54}
55
56/// A leaf of the route tree, and its own state.
57///
58/// The struct that implements this **is** the page's state, as in the WinUI
59/// and iced backends. What differs is that immediate mode has no later: the
60/// frame that sees the click is the frame that answers it, so there is no
61/// message and no `update` - [`Page::render`] takes `&mut self` and writes
62/// what it decided where it decided it.
63///
64/// What belongs here is what only this page has an opinion about: which row
65/// is picked, which tab is open, what is typed in a filter. What crosses the
66/// segment - what a domain owns, what another page reads - is a reducer, and
67/// reaches this page through [`PageCx::state`].
68pub trait Page: Default + Sized + 'static {
69    /// When `true`, the router keeps this page's reducer states in memory
70    /// while the page is not mounted.
71    const CACHE_STATE_IN_MEMORY: bool = false;
72
73    /// Where `impl Page` was written. `#[segment]` fills it in; an impl
74    /// without it loses only the source link.
75    const DECLARED: Option<guinea_core::actor::shape::Declared> = None;
76
77    /// What this page captured from the route, named by `routes!`. `()` for a
78    /// page that captures nothing.
79    ///
80    /// `PartialEq` because the router's one question about a capture is
81    /// whether it is still the same one - which decides what reinstalls and
82    /// which cached state may come back.
83    type Params: PartialEq + 'static;
84
85    /// What this segment installs, and `()` when it installs nothing.
86    ///
87    /// The list is not written beside the body - it *is* the body's
88    /// obligation: `install` returns it, so a feature that stops being
89    /// installed stops type-checking. Which is also why `install` has no
90    /// default any more.
91    ///
92    /// What is returned is owned by the segment's scope, which is what gives a
93    /// feature its own lifetime.
94    type Installs: guinea_app::feature::Lists;
95
96    fn install(ctx: &FeatureInitContext, params: &Self::Params) -> anyhow::Result<Self::Installs>;
97
98    /// The state it starts with, when `Default` is not it.
99    ///
100    /// A constructor, not an effect: it runs once per mount, beside
101    /// [`install`](Self::install), and anything that has to reach a feature
102    /// belongs there instead.
103    fn init(_ctx: &FeatureInitContext, _params: &Self::Params) -> Self {
104        Self::default()
105    }
106
107    /// Draws the page, and changes it. Runs again for every frame, so this is
108    /// the drawing itself and not a description of it.
109    fn render(&mut self, cx: &mut PageCx<'_, Self>);
110}
111
112/// A branch: draws its own chrome and decides where its child goes. Its own
113/// state, the same way a [`Page`] is.
114pub trait Layout: Default + Sized + 'static {
115    /// Where `impl Layout` was written; see [`Page::DECLARED`].
116    const DECLARED: Option<guinea_core::actor::shape::Declared> = None;
117
118    /// What every page under this layout carries, derived by `routes!` as the
119    /// intersection of their parameters. A layout declares nothing; it is
120    /// handed what all of its children were reached with.
121    type Params: PartialEq + 'static;
122
123    /// What this segment installs, and `()` when it installs nothing.
124    ///
125    /// The list is not written beside the body - it *is* the body's
126    /// obligation: `install` returns it, so a feature that stops being
127    /// installed stops type-checking. Which is also why `install` has no
128    /// default any more.
129    ///
130    /// What is returned is owned by the segment's scope, which is what gives a
131    /// feature its own lifetime.
132    type Installs: guinea_app::feature::Lists;
133
134    fn install(ctx: &FeatureInitContext, params: &Self::Params) -> anyhow::Result<Self::Installs>;
135
136    /// See [`Page::init`].
137    fn init(_ctx: &FeatureInitContext, _params: &Self::Params) -> Self {
138        Self::default()
139    }
140
141    fn render(&mut self, cx: &mut LayoutCx<'_, Self>);
142}
143
144pub const fn segment_entry<P: Page>() -> SegmentEntry<Egui> {
145    SegmentEntry::new::<P>(
146        install_page::<P>,
147        guinea_router::router::same_params::<P::Params>,
148        <P::Installs as guinea_app::feature::Lists>::list,
149        &const { MountPage::<P>(std::marker::PhantomData) } as &dyn Mount<Egui>,
150        P::CACHE_STATE_IN_MEMORY,
151    )
152    .written(P::DECLARED)
153}
154
155pub const fn layout_entry<L: Layout>() -> SegmentEntry<Egui> {
156    SegmentEntry::new::<L>(
157        install_layout::<L>,
158        guinea_router::router::same_params::<L::Params>,
159        <L::Installs as guinea_app::feature::Lists>::list,
160        &const { MountLayout::<L>(std::marker::PhantomData) } as &dyn Mount<Egui>,
161        false,
162    )
163    .written(L::DECLARED)
164}
165
166fn install_page<P: Page>(
167    ctx: &FeatureInitContext,
168    params: &dyn std::any::Any,
169) -> anyhow::Result<()> {
170    let params = guinea_router::router::narrow::<P::Params, P>(params)?;
171    own(ctx, P::install(ctx, params)?);
172    guinea_router::mounted::keep(&ctx.scope, P::init(ctx, params));
173    Ok(())
174}
175
176/// Hands what a segment installed to its scope - a feature's lifetime is the
177/// segment's, and dropping this here would end it at the end of `install`.
178fn own<T: 'static>(ctx: &FeatureInitContext, installed: T) {
179    ctx.scope.own(guinea_core::scope::DropGuard(installed));
180}
181
182fn install_layout<L: Layout>(
183    ctx: &FeatureInitContext,
184    params: &dyn std::any::Any,
185) -> anyhow::Result<()> {
186    let params = guinea_router::router::narrow::<L::Params, L>(params)?;
187    own(ctx, L::install(ctx, params)?);
188    guinea_router::mounted::keep(&ctx.scope, L::init(ctx, params));
189    Ok(())
190}
191
192/// A zero-sized marker per segment type: what a `const` entry points at to get
193/// its `&'static dyn Mount`.
194pub struct MountPage<P>(pub std::marker::PhantomData<P>);
195pub struct MountLayout<L>(pub std::marker::PhantomData<L>);
196
197impl<P: Page> Mount<Egui> for MountPage<P> {
198    fn view<'a>(&self, props: SegmentProps<Egui>, _nodes: &'a ()) -> Node {
199        let at = props.scopes[props.cursor];
200
201        Node::new(move |ui| {
202            let _drawing = guinea_core::observability::Rendering::of(std::any::type_name::<P>());
203            guinea_router::mounted::with::<P, _>(&at, |page| {
204                page.render(&mut PageCx {
205                    ui,
206                    props,
207                    page: std::marker::PhantomData,
208                })
209            })
210        })
211    }
212}
213
214impl<L: Layout> Mount<Egui> for MountLayout<L> {
215    fn view<'a>(&self, props: SegmentProps<Egui>, _nodes: &'a ()) -> Node {
216        let at = props.scopes[props.cursor];
217
218        Node::new(move |ui| {
219            let _drawing = guinea_core::observability::Rendering::of(std::any::type_name::<L>());
220            guinea_router::mounted::with::<L, _>(&at, |layout| {
221                layout.render(&mut LayoutCx {
222                    ui,
223                    props,
224                    layout: std::marker::PhantomData,
225                })
226            })
227        })
228    }
229}
230
231/// A one-segment chain, for a page drawn without a route tree.
232pub fn page_chain<P: Page>() -> &'static [SegmentEntry<Egui>] {
233    single_entry_chain(segment_entry::<P>())
234}
235
236/// What a page's drawing is handed.
237///
238/// Carries the page type, not because drawing needs it, but because reading
239/// does: what a segment may read is a fact about where it sits, and this is
240/// where that fact enters the signature.
241pub struct PageCx<'a, P> {
242    ui: &'a mut egui::Ui,
243    props: SegmentProps<Egui>,
244    page: std::marker::PhantomData<fn() -> P>,
245}
246
247fn feature_of<R: Reducer>(
248    props: &SegmentProps<Egui>,
249) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch) {
250    let binding = props.binding::<R>();
251    (binding.get(), binding.dispatch())
252}
253
254impl<P> PageCx<'_, P> {
255    /// The reducer's state and actions.
256    ///
257    /// No subscription, as in the terminal: egui redraws the whole frame, so
258    /// there is nothing to invalidate - the next pass reads the state again.
259    /// What does need saying is when a frame should happen at all, and that is
260    /// the dispatcher's job.
261    ///
262    /// The feature that answers is the one this page installed, or the nearest
263    /// above that listed `R` in `Exports`. A read that reaches nothing panics
264    /// here, naming the chain it walked.
265    ///
266    /// The state comes shared, not copied: reading it every frame costs a
267    /// count, and a change made mid-frame goes to a copy.
268    pub fn read<R>(&mut self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
269    where
270        R: Reducer,
271    {
272        feature_of::<R>(&self.props)
273    }
274
275    /// The reducer's binding: its state, and a push straight into it.
276    ///
277    /// For state the UI owns outright - what is picked in a tree, which tab
278    /// is open - claimed with `cx.state::<R>().plain()`. There is no domain
279    /// to ask, so there is no actor to ask it through: a click is the whole
280    /// story.
281    ///
282    /// State a feature drives is not this: pushing into it goes behind the
283    /// back of whatever answers for it. Use [`PageCx::read`] and emit.
284    pub fn binding<R>(&self) -> ReducerBinding<R>
285    where
286        R: Reducer,
287    {
288        self.props.binding::<R>()
289    }
290}
291
292impl<P> Reads for PageCx<'_, P> {
293    fn read<R>(&mut self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
294    where
295        R: Reducer + PartialEq,
296    {
297        feature_of::<R>(&self.props)
298    }
299
300    fn dispatch<R>(&self) -> guinea_core::feature::Dispatch
301    where
302        R: Reducer,
303    {
304        self.props.binding::<R>().dispatch()
305    }
306}
307
308impl<P> PageCx<'_, P> {
309    pub fn ui(&mut self) -> &mut egui::Ui {
310        self.ui
311    }
312
313    /// A navigator over the route type the application runs.
314    pub fn navigate<R>(&self) -> NavigateHandle<Egui, R>
315    where
316        R: RouteChain<Egui> + Clone + PartialEq + 'static,
317    {
318        nav::current::<R>()
319    }
320}
321
322/// What a layout's drawing is handed. Same as a page's, plus the child.
323pub struct LayoutCx<'a, L> {
324    ui: &'a mut egui::Ui,
325    props: SegmentProps<Egui>,
326    layout: std::marker::PhantomData<fn() -> L>,
327}
328
329impl<L> LayoutCx<'_, L> {
330    /// See [`PageCx::read`].
331    pub fn read<R>(&mut self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
332    where
333        R: Reducer,
334    {
335        feature_of::<R>(&self.props)
336    }
337
338    /// See [`PageCx::binding`].
339    pub fn binding<R>(&self) -> ReducerBinding<R>
340    where
341        R: Reducer,
342    {
343        self.props.binding::<R>()
344    }
345}
346
347impl<L> Reads for LayoutCx<'_, L> {
348    fn read<R>(&mut self) -> (std::rc::Rc<R>, guinea_core::feature::Dispatch)
349    where
350        R: Reducer + PartialEq,
351    {
352        feature_of::<R>(&self.props)
353    }
354
355    fn dispatch<R>(&self) -> guinea_core::feature::Dispatch
356    where
357        R: Reducer,
358    {
359        self.props.binding::<R>().dispatch()
360    }
361}
362
363impl<L> LayoutCx<'_, L> {
364    pub fn ui(&mut self) -> &mut egui::Ui {
365        self.ui
366    }
367
368    /// A navigator over the route type the application runs.
369    pub fn navigate<R>(&self) -> NavigateHandle<Egui, R>
370    where
371        R: RouteChain<Egui> + Clone + PartialEq + 'static,
372    {
373        nav::current::<R>()
374    }
375
376    /// The next segment down the chain, for the layout to draw where it wants.
377    ///
378    /// Handed over rather than drawn here: a layout takes its `ui` from this
379    /// same context, so a method that drew the child would need the context
380    /// twice at once.
381    pub fn outlet(&self) -> Node {
382        self.props.outlet(&())
383    }
384
385    /// Whether the segment directly below is `P`.
386    ///
387    /// What a tab strip needs, and cheaper than it looks: the chain already
388    /// says which page is mounted, so highlighting the current tab needs
389    /// neither the route nor a copy of it in state.
390    pub fn child_is<P: 'static>(&self) -> bool {
391        self.props
392            .chain
393            .get(self.props.cursor + 1)
394            .is_some_and(|entry| (entry.type_id)() == std::any::TypeId::of::<P>())
395    }
396}