Skip to main content

max / quasi

9.3 KB · 222 lines History Blame Raw
1 //! Where this renderer drew each described row, for a host that has to read a
2 //! gesture the description does not carry.
3 //!
4 //! <!-- wiki: quasi-overview -->
5 //!
6 //! # Why this is here and not in the vocabulary
7 //!
8 //! asked what, if anything, a description should hand a host about where it
9 //! drew what. The answer is nothing. A description says what is on the screen
10 //! and never where, which is the same rule that keeps `Act::shows` a picture
11 //! rather than a box and keeps an anchor a described id rather than a point.
12 //! Geometry is the *renderer's* answer and a different answer per host: a
13 //! terminal has cells, a webview has the DOM, and this host has rects.
14 //!
15 //! So the fact travels from the renderer to its own host, beside the drawing,
16 //! rather than through the description. A webview host asks the DOM the same
17 //! question and needs nothing from here.
18 //!
19 //! # What it is for
20 //!
21 //! audiofiles drags samples into a DAW. That gesture is not an outcome -- an OS
22 //! drag is this host's, and `quasi::panel::dragging_out` performs it -- and to
23 //! start it correctly the host has to know two things this renderer knew and
24 //! did not say: whether the press landed on a row at all, and which row. Without
25 //! them a press on the toolbar could start a drag, and a press on an unchosen
26 //! row dragged the chosen set instead of the row under the cursor.
27 //!
28 //! # Why egui's own memory rather than a return value
29 //!
30 //! `Immediate::screen` and `chromed` answer `Option<Fired>`, which is what the
31 //! user set off. Widening that to carry geometry would change every call site
32 //! in every host to thread a fact almost none of them want. The rects are
33 //! per-frame data belonging to a `Context`, which is what `Context` data is for,
34 //! and a host that never asks pays a `Vec` push per row and nothing else.
35
36 use egui::{Context, Id, Pos2, Rect, Ui};
37 use quasi_router::Anchor;
38
39 /// A described row, and where this renderer put it.
40 #[derive(Debug, Clone, PartialEq, Eq)]
41 pub struct RowAt {
42 /// The row's own value, when the description gave it one.
43 ///
44 /// `Row::value`, which is the identifier the description
45 /// already uses for a row: it is what a tick travels under and what
46 /// survives a reorder. `None` on a row that carries none, where the index
47 /// is all there is.
48 pub value: Option<String>,
49 /// Where the row sat in the list or table this frame.
50 ///
51 /// Positional, so it does not survive a reorder. Read it only against the
52 /// same frame's description.
53 pub index: usize,
54 }
55
56 /// The key the rects live under, which is this crate's and not a host's.
57 fn slot() -> Id {
58 Id::new("quasi-immediate::rows")
59 }
60
61 /// What is remembered between the drawing and the asking.
62 #[derive(Clone, Default)]
63 struct Drawn {
64 /// The pass these rects were measured in.
65 ///
66 /// egui redraws on its own schedule, so a host asking about a pointer needs
67 /// the rects from the frame it is asking about rather than whatever was
68 /// left behind. Rows are cleared when the number moves rather than by a
69 /// begin-frame hook, because this crate has none.
70 pass: u64,
71 /// Value, position, rect, and whether the row is part of a live selection.
72 ///
73 /// The last is `Anchor::Selection`'s other half. A staged tick lives in
74 /// the runtime's view and is passed in; a live selection is the
75 /// description's, so it arrives here with the row that carried it.
76 rows: Vec<(Option<String>, usize, Rect, bool)>,
77 /// Where each described region was drawn, by `Slot::id`.
78 ///
79 /// The rows above answer "is the pointer over one"; these answer "where is
80 /// the thing this menu was anchored to", which is the same class of fact
81 /// and belongs in the same place rather than in a second store that can
82 /// disagree about which pass it is describing.
83 regions: Vec<(String, Rect)>,
84 /// Where each *named* control was drawn, by `Act::id`.
85 ///
86 /// Only the named ones. `Act::id` is `None` on nearly every control, so a
87 /// screen that anchors nothing pushes nothing here.
88 acts: Vec<(String, Rect)>,
89 }
90
91 /// Say a row was drawn here.
92 ///
93 /// Called for every described row whether or not it answers a gesture: a row
94 /// that is neither pressable nor menued still occupies the space, and "is the
95 /// pointer over a row" is the question that has to be answerable for the
96 /// dangerous case -- a press on the toolbar that would otherwise start a drag.
97 pub(crate) fn note_row(ui: &Ui, value: Option<&str>, index: usize, rect: Rect, chosen: bool) {
98 noting(ui, |drawn| {
99 drawn
100 .rows
101 .push((value.map(ToOwned::to_owned), index, rect, chosen));
102 });
103 }
104
105 /// Say a region was drawn here.
106 ///
107 /// Every described region, whether or not anything is ever anchored to it, for
108 /// [`note_row`]'s reason: nothing in a description says which regions an app
109 /// anchors to, and the answer has to exist before the question is asked.
110 pub(crate) fn note_region(ui: &Ui, id: &str, rect: Rect) {
111 noting(ui, |drawn| drawn.regions.push((id.to_owned(), rect)));
112 }
113
114 /// Say a named control was drawn here.
115 ///
116 /// Called only for an [`Act`](quasi_router::Act) carrying an
117 /// [`id`](quasi_router::Act::id), so a screen with no anchored menus pays
118 /// nothing.
119 pub(crate) fn note_act(ui: &Ui, id: &str, rect: Rect) {
120 noting(ui, |drawn| drawn.acts.push((id.to_owned(), rect)));
121 }
122
123 /// Clear a stale pass and hand the store to whoever is adding to it.
124 ///
125 /// One place, so the pass check cannot be forgotten by a note function added
126 /// later -- which is exactly how a menu would come to be anchored at where its
127 /// control sat two frames ago.
128 fn noting(ui: &Ui, add: impl FnOnce(&mut Drawn)) {
129 let pass = ui.ctx().cumulative_pass_nr();
130 ui.ctx().data_mut(|data| {
131 let drawn: &mut Drawn = data.get_temp_mut_or_default(slot());
132 if drawn.pass != pass {
133 drawn.pass = pass;
134 drawn.rows.clear();
135 drawn.regions.clear();
136 drawn.acts.clear();
137 }
138 add(drawn);
139 });
140 }
141
142 /// Where the thing an anchor names was drawn, if it was drawn this pass.
143 ///
144 /// This is the ruling in `600c9e42` doing its work: the description named a
145 /// thing, and the renderer that drew it is the one that answers where.
146 ///
147 /// `ticked` is the values the view holds ticked, which is **half** of what
148 /// [`Anchor::Selection`] resolves against -- the runtime owns that set and this
149 /// module does not, so it is passed in rather than reached for. The other half
150 /// is `Row::chosen`, a selection already in force, which the description
151 /// carries and which therefore arrived with the row.
152 ///
153 /// `None` means the anchor named nothing on this pass: a region or control that
154 /// is not on the screen, or a selection with nothing in it. The caller falls
155 /// back to drawing the menu unanchored, which is what every renderer does with
156 /// an anchor it cannot resolve.
157 pub(crate) fn anchor_rect(ctx: &Context, anchor: &Anchor, ticked: &[String]) -> Option<Rect> {
158 ctx.data(|data| {
159 let drawn: Drawn = data.get_temp(slot())?;
160 match anchor {
161 Anchor::Region(id) => find(&drawn.regions, id),
162 Anchor::Control(id) => find(&drawn.acts, id),
163 // The box around every ticked row, so a menu over a set opens
164 // against the set rather than against whichever member happens to
165 // be first. Rows scrolled out of view contribute nothing, which is
166 // right: they were not drawn, so this renderer has no rect for them
167 // and inventing one would put the menu off screen.
168 Anchor::Selection => drawn
169 .rows
170 .iter()
171 .filter(|(value, _, _, chosen)| {
172 *chosen
173 || value
174 .as_deref()
175 .is_some_and(|value| ticked.iter().any(|tick| tick == value))
176 })
177 .map(|(_, _, rect, _)| *rect)
178 .reduce(Rect::union),
179 }
180 })
181 }
182
183 /// The last rect noted under a name, or `None` if nothing was.
184 ///
185 /// Last wins, matching [`row_at`]: where one name is drawn twice -- a region
186 /// inside an overlay over the region it names -- the later one is on top.
187 fn find(noted: &[(String, Rect)], id: &str) -> Option<Rect> {
188 noted
189 .iter()
190 .rev()
191 .find(|(name, _)| name == id)
192 .map(|(_, rect)| *rect)
193 }
194
195 /// The described row under this point, if the point is over one.
196 ///
197 /// `None` means the point is not over a row: over the chrome, over a control
198 /// beside the table, over nothing. That is the answer a host guarding a drag
199 /// wants, and it is the one this renderer could not give before.
200 ///
201 /// Last drawn wins. Rows do not overlap within a list or a table, and where two
202 /// do -- an overlay's row over the row underneath -- the later one is on top,
203 /// which is the order they were drawn in.
204 ///
205 /// Reads the frame that has most recently drawn. A host asking before anything
206 /// has been drawn gets `None` rather than a stale answer.
207 #[must_use]
208 pub fn row_at(ctx: &Context, pos: Pos2) -> Option<RowAt> {
209 ctx.data(|data| {
210 let drawn: Drawn = data.get_temp(slot())?;
211 drawn
212 .rows
213 .iter()
214 .rev()
215 .find(|(_, _, rect, _)| rect.contains(pos))
216 .map(|(value, index, _, _)| RowAt {
217 value: value.clone(),
218 index: *index,
219 })
220 })
221 }
222