Skip to main content

max / quasi

4.9 KB · 108 lines History Blame Raw
1 //! The seam between a description and bytes.
2 //!
3 //! No host adapter emits markup, and that is the important decision in the set
4 //! of them.
5 //!
6 //! A hosted axum route and a Tauri custom-protocol handler both serve HTML to a
7 //! webview. If the HTML were generated in either, the other would need a second
8 //! copy of it, and two copies of a markup emitter is the divergence the whole
9 //! stack exists to end. So markup belongs to the webview renderer, which is
10 //! `makeover-webview`, and every adapter takes it as a parameter.
11 //!
12 //! Today `makeover-webview` emits a stylesheet, form fields and table cells,
13 //! which is phase A. Whole-screen markup is phase B and is not written, so an
14 //! app supplies its own [`Serves`] in the meantime. When phase B lands, the
15 //! implementation moves there and every host adapter picks it up at once
16 //! instead of one at a time.
17
18 use quasi_router::{Node, Screen};
19
20 /// Turns a description into the bytes a browser gets.
21 ///
22 /// An HTTP host's contract, and not the thing the three renderers share. It was
23 /// called `Render` until 2026-08-12, which cost the first hour of writing the
24 /// second renderer: `quasi-tui` implements none of this and cannot. A terminal
25 /// answers cells in a buffer rather than a `String`, and a content type is an
26 /// HTTP header. What a webview, a terminal and an egui frame have in common is
27 /// the description itself, which is a type they already take; a trait over that
28 /// would have one method taking `&Screen` and returning `()`, which buys
29 /// nothing. Two renderers sharing no trait is a fact about renderers rather
30 /// than a hole.
31 ///
32 /// Two methods, because [`Response`](quasi_router::Response) has two members
33 /// and a renderer genuinely does different work for each: a screen is a
34 /// document and a fragment is the inside of one element.
35 ///
36 /// Errors are not a third method. An adapter turns a
37 /// [`RouteError`](quasi_router::RouteError) into a [`Node::Notice`] and asks
38 /// for a fragment, so failure becomes UI through the same path as everything
39 /// else. That is decision 9 holding at the boundary rather than only in the
40 /// router.
41 pub trait Serves: Send + Sync + 'static {
42 /// A whole document.
43 fn screen(&self, screen: &Screen) -> String;
44
45 /// The inside of one region.
46 fn fragment(&self, node: &Node) -> String;
47
48 /// One region's new contents, carried beside the region being replaced.
49 ///
50 /// [`Response::invalidates`](quasi_router::Response::invalidates): the row
51 /// the write was aimed at is the [`fragment`](Self::fragment), and the
52 /// count in the header is one of these. Both travel on one answer, so the
53 /// two never disagree and no second request is made for a fact the router
54 /// already had.
55 ///
56 /// Unlike the other two this carries the region's id, because an
57 /// out-of-band update is applied by name rather than by where it was
58 /// aimed. A fragment does not need one: the router said where it goes in
59 /// [`RETARGET`](crate::htmx::RETARGET).
60 ///
61 /// # Why it defaults to nothing
62 ///
63 /// A renderer answering something other than markup has no out-of-band
64 /// channel to put this in, the same reason [`content_type`](Self::content_type)
65 /// is overridable. Returning nothing is that renderer saying so, and it is
66 /// the honest default: a JSON client is told what changed by the payload it
67 /// already parses.
68 fn invalidated(&self, region: &str, node: &Node) -> String {
69 let _ = (region, node);
70 String::new()
71 }
72
73 /// The inside of the overlay container, for a screen drawn over the page.
74 ///
75 /// [`Outcome::Over`](quasi_router::Outcome::Over). No document and no
76 /// shell: what is under it already has both, and replacing them is exactly
77 /// what an overlay is not.
78 ///
79 /// # Why it defaults to the whole screen
80 ///
81 /// A renderer with no overlay container cannot layer anything, and the
82 /// honest fallback is to draw the screen: the user reaches the palette,
83 /// without it floating. Returning nothing would be the silent no-op
84 /// `Outcome`'s exhaustiveness exists to prevent, and this is the same
85 /// argument one level in.
86 fn overlay(&self, screen: &Screen) -> String {
87 self.screen(screen)
88 }
89
90 /// The id of the element an overlay is drawn into.
91 ///
92 /// `None` says this renderer has no overlay container, which is what makes
93 /// [`overlay`](Self::overlay)'s default a whole-screen swap rather than
94 /// markup aimed at an element that is not there.
95 fn overlay_target(&self) -> Option<&str> {
96 None
97 }
98
99 /// What the document says it is.
100 ///
101 /// Overridable for a renderer answering something other than HTML, which is
102 /// the seam a JSON client or a test harness needs. Defaults to HTML because
103 /// that is what every webview host wants.
104 fn content_type(&self) -> &'static str {
105 "text/html; charset=utf-8"
106 }
107 }
108