Skip to main content

max / quasi

8.4 KB · 181 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::{Anchor, Candidate, 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. A
23 /// terminal answers cells in a buffer rather than a `String`, and a content
24 /// type is an HTTP header. What a webview, a terminal and an egui frame have
25 /// in common is the description itself, which is a type they already take; a
26 /// trait over that would have one method taking `&Screen` and returning ``,
27 /// which buys nothing. Two renderers sharing no trait is a fact about
28 /// renderers rather than a hole.
29 ///
30 /// Two methods, because [`Response`](quasi_router::Response) has two members
31 /// and a renderer genuinely does different work for each: a screen is a
32 /// document and a fragment is the inside of one element.
33 ///
34 /// Errors are not a third method. An adapter turns a
35 /// [`RouteError`](quasi_router::RouteError) into a [`Node::Notice`] and asks
36 /// for a fragment, so failure becomes UI through the same path as everything
37 /// else. That is decision 9 holding at the boundary rather than only in the
38 /// router.
39 pub trait Serves: Send + Sync + 'static {
40 /// A whole document.
41 fn screen(&self, screen: &Screen) -> String;
42
43 /// The inside of one region.
44 fn fragment(&self, node: &Node) -> String;
45
46 /// One region's new contents, carried beside the region being replaced.
47 ///
48 /// [`Response::invalidates`](quasi_router::Response::invalidates): the row
49 /// the write was aimed at is the [`fragment`](Self::fragment), and the
50 /// count in the header is one of these. Both travel on one answer, so the
51 /// two never disagree and no second request is made for a fact the router
52 /// already had.
53 ///
54 /// Unlike the other two this carries the region's id, because an
55 /// out-of-band update is applied by name rather than by where it was
56 /// aimed. A fragment does not need one: the router said where it goes in
57 /// [`RETARGET`](crate::htmx::RETARGET).
58 ///
59 /// # Why it defaults to nothing
60 ///
61 /// A renderer answering something other than markup has no out-of-band
62 /// channel to put this in, the same reason [`content_type`](Self::content_type)
63 /// is overridable. Returning nothing is that renderer saying so, and it is
64 /// the honest default: a JSON client is told what changed by the payload it
65 /// already parses.
66 fn invalidated(&self, region: &str, node: &Node) -> String {
67 let _ = (region, node);
68 String::new()
69 }
70
71 /// The inside of the overlay container, for a screen drawn over the page.
72 ///
73 /// [`Outcome::Over`](quasi_router::Outcome::Over). No document and no
74 /// shell: what is under it already has both, and replacing them is exactly
75 /// what an overlay is not.
76 ///
77 /// # Why it defaults to the whole screen
78 ///
79 /// A renderer with no overlay container cannot layer anything, and the
80 /// honest fallback is to draw the screen: the user reaches the palette,
81 /// without it floating. Returning nothing would be the silent no-op
82 /// `Outcome`'s exhaustiveness exists to prevent, and this is the same
83 /// argument one level in.
84 fn overlay(&self, screen: &Screen) -> String {
85 self.screen(screen)
86 }
87
88 /// One field's suggestion list, for the field that owns it.
89 ///
90 /// [`Outcome::Suggestions`](quasi_router::Outcome::Suggestions). The field
91 /// is named by [`Field::name`](quasi_router::Field::name) and never by an
92 /// id: the list belongs to the control, so a renderer that needs a document
93 /// id derives one from the name and says so in
94 /// [`suggestions_target`](Self::suggestions_target).
95 ///
96 /// # Why this has no default
97 ///
98 /// A default returning nothing is the silent no-op
99 /// [`Outcome`](quasi_router::Outcome)'s exhaustiveness exists to prevent,
100 /// one level in: the route answered with candidates, the user typed to get
101 /// them, and a renderer that quietly drew none would look like a route with
102 /// nothing to suggest. [`overlay`](Self::overlay) can default because
103 /// drawing the screen unlayered is a worse *rendering* of the same answer;
104 /// there is no such fallback here.
105 fn suggestions(&self, field: &str, options: &[Candidate]) -> String;
106
107 /// The id of the element a field's suggestion list is drawn into.
108 ///
109 /// [`overlay_target`](Self::overlay_target)'s counterpart, and `None` means
110 /// the same thing: this renderer has no container to aim at, so the answer
111 /// is sent without a retarget and the client puts it wherever it was aimed.
112 ///
113 /// Derived from the field's name rather than authored, which is what makes
114 /// a suggestion list a thing a field owns rather than two elements a
115 /// description has to keep pointing at each other.
116 fn suggestions_target(&self, field: &str) -> Option<String> {
117 let _ = field;
118 None
119 }
120
121 /// The inside of a popover container, for a screen drawn at a point.
122 ///
123 /// [`Outcome::Anchored`](quasi_router::Outcome::Anchored). No document and
124 /// no shell, for [`overlay`](Self::overlay)'s reason: what is under it has
125 /// both.
126 ///
127 /// The anchor is not a parameter because it is already spent. It named the
128 /// container this lands in, which is what
129 /// [`anchored_target`](Self::anchored_target) answered, and asking a
130 /// renderer to write the same fact into the markup as well would be one
131 /// thing decided twice.
132 ///
133 /// # Why it defaults to the overlay
134 ///
135 /// A renderer with no popover container cannot draw at a point, and an
136 /// unanchored overlay is the honest fallback the same way an unlayered
137 /// screen is [`overlay`](Self::overlay)'s: the user reaches the menu,
138 /// without it floating where they pressed. Drawing nothing would be the
139 /// silent no-op [`Outcome`](quasi_router::Outcome)'s exhaustiveness exists
140 /// to prevent.
141 fn anchored(&self, screen: &Screen) -> String {
142 self.overlay(screen)
143 }
144
145 /// The id of the element an anchored screen is drawn into.
146 ///
147 /// [`overlay_target`](Self::overlay_target)'s counterpart, taking the
148 /// anchor because that is the whole difference: an overlay lands in the one
149 /// container an app has, and this lands in the container belonging to the
150 /// thing that was anchored to.
151 ///
152 /// `None` means what it means on the other two: this renderer has no such
153 /// container, so the answer is sent without a retarget and lands where it
154 /// was aimed. That is also the honest answer for an anchor naming something
155 /// this document does not have — a region that is not on the screen, a
156 /// control carrying no [`Act::id`](quasi_router::Act::id) — which is a
157 /// description bug and is treated the way every other one is.
158 fn anchored_target(&self, anchor: &Anchor) -> Option<String> {
159 let _ = anchor;
160 None
161 }
162
163 /// The id of the element an overlay is drawn into.
164 ///
165 /// `None` says this renderer has no overlay container, which is what makes
166 /// [`overlay`](Self::overlay)'s default a whole-screen swap rather than
167 /// markup aimed at an element that is not there.
168 fn overlay_target(&self) -> Option<&str> {
169 None
170 }
171
172 /// What the document says it is.
173 ///
174 /// Overridable for a renderer answering something other than HTML, which is
175 /// the seam a JSON client or a test harness needs. Defaults to HTML because
176 /// that is what every webview host wants.
177 fn content_type(&self) -> &'static str {
178 "text/html; charset=utf-8"
179 }
180 }
181