| 16 |
16 |
|
//! What is left here is what a `Screen` adds over a node: the address a control
|
| 17 |
17 |
|
//! carries, the values a form gathers, and the selection a row's tick joins.
|
| 18 |
18 |
|
//! None of that is `makeover-layout`'s, so none of it can be down there.
|
|
19 |
+ |
//!
|
|
20 |
+ |
//! # The walk is split in two, and the table is why
|
|
21 |
+ |
//!
|
|
22 |
+ |
//! [`draw`] is exhaustive over `Node` and dispatches to [`leaf`] for the nodes
|
|
23 |
+ |
//! that carry no address, and to [`container`] for the ones that hold others or
|
|
24 |
+ |
//! reach a screen's own facts.
|
|
25 |
+ |
//!
|
|
26 |
+ |
//! The split is forced rather than tidy: `makeover_immediate::table` draws a cell
|
|
27 |
+ |
//! through a closure that already holds the `Ui` and the renderer, and a closure
|
|
28 |
+ |
//! cannot also hold the pass mutably. So a cell draws its ordinary nodes through
|
|
29 |
+ |
//! `leaf`, and collects the two that carry an address to fire after the table
|
|
30 |
+ |
//! has finished with the borrow.
|
| 19 |
31 |
|
|
| 20 |
32 |
|
use egui::{RichText, Ui};
|
| 21 |
33 |
|
use makeover_immediate::widget;
|
| 22 |
34 |
|
use makeover_immediate::{Filling, field, frame};
|
| 23 |
35 |
|
use quasi_router::layout;
|
| 24 |
|
- |
use quasi_router::{Act, Action, Node, Params, Row, Slot};
|
|
36 |
+ |
use quasi_router::{Act, Action, Cells, Node, Params, Row, Slot};
|
| 25 |
37 |
|
|
| 26 |
|
- |
use crate::Pass;
|
|
38 |
+ |
use crate::{Immediate, Pass};
|
|
39 |
+ |
|
|
40 |
+ |
/// What a column is assumed to need when nothing measured it.
|
|
41 |
+ |
///
|
|
42 |
+ |
/// `Sizing::lengths` is how an app says a column's longest value is wider than
|
|
43 |
+ |
/// its name, and a described table carries no such measurement: the description
|
|
44 |
+ |
/// says what a column *is*, not how long its contents turned out. So every
|
|
45 |
+ |
/// column falls back to this, and `egui_extras` sizes the remainder.
|
|
46 |
+ |
const CELL_WIDTH: f32 = 120.0;
|
| 27 |
47 |
|
|
| 28 |
48 |
|
/// Draw one node.
|
| 29 |
49 |
|
///
|
| 35 |
55 |
|
/// it describes.
|
| 36 |
56 |
|
pub(crate) fn draw(pass: &mut Pass<'_>, ui: &mut Ui, node: &Node) {
|
| 37 |
57 |
|
match node {
|
| 38 |
|
- |
Node::Heading { level, text } => {
|
| 39 |
|
- |
let size = ui.text_style_height(&egui::TextStyle::Body)
|
| 40 |
|
- |
* match level {
|
| 41 |
|
- |
layout::Heading::Page => 1.6,
|
| 42 |
|
- |
layout::Heading::Section => 1.3,
|
| 43 |
|
- |
layout::Heading::Subsection => 1.1,
|
| 44 |
|
- |
};
|
| 45 |
|
- |
ui.label(
|
| 46 |
|
- |
RichText::new(text)
|
| 47 |
|
- |
.size(size)
|
| 48 |
|
- |
.strong()
|
| 49 |
|
- |
.color(pass.immediate.palette.content),
|
| 50 |
|
- |
);
|
| 51 |
|
- |
}
|
| 52 |
|
- |
|
| 53 |
|
- |
Node::Text { text, .. } => {
|
| 54 |
|
- |
ui.label(RichText::new(text).color(pass.immediate.palette.content));
|
| 55 |
|
- |
}
|
| 56 |
|
- |
|
| 57 |
|
- |
// Markdown arrives as source, so that every renderer answers it its own
|
| 58 |
|
- |
// way. This one has no rich text of its own worth the name, so it takes
|
| 59 |
|
- |
// the plain rendering: `**bold**` reads as `bold` rather than as four
|
| 60 |
|
- |
// characters of syntax, which is the outcome `Node::Text` would have
|
| 61 |
|
- |
// given anyway and is the honest floor until egui grows a markdown
|
| 62 |
|
- |
// widget worth adopting.
|
| 63 |
|
- |
Node::Rich { source, .. } => {
|
| 64 |
|
- |
ui.label(
|
| 65 |
|
- |
RichText::new(docengine::render_plain(source))
|
| 66 |
|
- |
.color(pass.immediate.palette.content),
|
| 67 |
|
- |
);
|
| 68 |
|
- |
}
|
|
58 |
+ |
// The nodes that carry no address. Split out so a table cell can
|
|
59 |
+ |
// draw them: the cell closure holds the `Ui` and cannot also hold
|
|
60 |
+ |
// the pass mutably, and these need nothing but the palette.
|
|
61 |
+ |
Node::Heading { .. }
|
|
62 |
+ |
| Node::Text { .. }
|
|
63 |
+ |
| Node::Rich { .. }
|
|
64 |
+ |
| Node::Figure(_)
|
|
65 |
+ |
| Node::Notice { .. }
|
|
66 |
+ |
| Node::Meter(_) => leaf(pass.immediate, ui, node),
|
| 69 |
67 |
|
|
| 70 |
68 |
|
Node::Act(act) => {
|
| 71 |
69 |
|
act_node(pass, ui, act, None);
|
| 82 |
80 |
|
}
|
| 83 |
81 |
|
}
|
| 84 |
82 |
|
|
| 85 |
|
- |
Node::Figure(figure) => {
|
| 86 |
|
- |
widget::figure(
|
| 87 |
|
- |
ui,
|
| 88 |
|
- |
&figure.as_layout(),
|
| 89 |
|
- |
&pass.immediate.palette,
|
| 90 |
|
- |
&pass.immediate.widget,
|
| 91 |
|
- |
);
|
| 92 |
|
- |
}
|
| 93 |
|
- |
|
| 94 |
83 |
|
Node::Token(tag) => {
|
| 95 |
84 |
|
let pressed = widget::token(
|
| 96 |
85 |
|
ui,
|
| 108 |
97 |
|
}
|
| 109 |
98 |
|
}
|
| 110 |
99 |
|
|
| 111 |
|
- |
Node::Notice { tone, text, .. } => {
|
| 112 |
|
- |
// The tone carries it, and the surface says it is a thing set on
|
| 113 |
|
- |
// the page rather than part of the flow. Where a toast lands
|
| 114 |
|
- |
// against a banner is renderer policy and this renderer has one
|
| 115 |
|
- |
// place to put either, which is where the caller drew it.
|
| 116 |
|
- |
frame(
|
| 117 |
|
- |
ui,
|
| 118 |
|
- |
layout::Depth::Raised,
|
| 119 |
|
- |
&pass.immediate.palette,
|
| 120 |
|
- |
pass.immediate.frame,
|
| 121 |
|
- |
|ui| {
|
| 122 |
|
- |
ui.label(RichText::new(text).color(pass.immediate.palette.tone(*tone)));
|
| 123 |
|
- |
},
|
| 124 |
|
- |
);
|
| 125 |
|
- |
}
|
| 126 |
|
- |
|
| 127 |
100 |
|
Node::StandIn { message, act, .. } => {
|
| 128 |
101 |
|
ui.label(RichText::new(message).color(pass.immediate.palette.content_muted));
|
| 129 |
102 |
|
if let Some(act) = act {
|
| 135 |
108 |
|
}
|
| 136 |
109 |
|
}
|
| 137 |
110 |
|
|
|
111 |
+ |
/// The nodes that carry no address.
|
|
112 |
+ |
///
|
|
113 |
+ |
/// Everything here needs the palette and nothing else, which is what makes a
|
|
114 |
+ |
/// table cell able to draw one: the cell closure already holds the `Ui` and the
|
|
115 |
+ |
/// renderer, and cannot also hold the pass mutably.
|
|
116 |
+ |
///
|
|
117 |
+ |
/// The catch-all is unreachable through [`draw`], which is exhaustive and sends
|
|
118 |
+ |
/// only these here. It is a private split of one walk rather than a second walk,
|
|
119 |
+ |
/// so the guarantee that a new `Node` member stops the build lives up there.
|
|
120 |
+ |
fn leaf(immediate: &Immediate, ui: &mut Ui, node: &Node) {
|
|
121 |
+ |
match node {
|
|
122 |
+ |
Node::Heading { level, text } => {
|
|
123 |
+ |
let size = ui.text_style_height(&egui::TextStyle::Body)
|
|
124 |
+ |
* match level {
|
|
125 |
+ |
layout::Heading::Page => 1.6,
|
|
126 |
+ |
layout::Heading::Section => 1.3,
|
|
127 |
+ |
layout::Heading::Subsection => 1.1,
|
|
128 |
+ |
};
|
|
129 |
+ |
ui.label(
|
|
130 |
+ |
RichText::new(text)
|
|
131 |
+ |
.size(size)
|
|
132 |
+ |
.strong()
|
|
133 |
+ |
.color(immediate.palette.content),
|
|
134 |
+ |
);
|
|
135 |
+ |
}
|
|
136 |
+ |
|
|
137 |
+ |
Node::Text { text, .. } => {
|
|
138 |
+ |
ui.label(RichText::new(text).color(immediate.palette.content));
|
|
139 |
+ |
}
|
|
140 |
+ |
|
|
141 |
+ |
// Markdown arrives as source, so that every renderer answers it its own
|
|
142 |
+ |
// way. This one has no rich text of its own worth the name, so it takes
|
|
143 |
+ |
// the plain rendering: `**bold**` reads as `bold` rather than as four
|
|
144 |
+ |
// characters of syntax, which is the outcome `Node::Text` would have
|
|
145 |
+ |
// given anyway and is the honest floor until egui grows a markdown
|
|
146 |
+ |
// widget worth adopting.
|
|
147 |
+ |
Node::Rich { source, .. } => {
|
|
148 |
+ |
ui.label(
|
|
149 |
+ |
RichText::new(docengine::render_plain(source)).color(immediate.palette.content),
|
|
150 |
+ |
);
|
|
151 |
+ |
}
|
|
152 |
+ |
|
|
153 |
+ |
Node::Figure(figure) => {
|
|
154 |
+ |
widget::figure(
|
|
155 |
+ |
ui,
|
|
156 |
+ |
&figure.as_layout(),
|
|
157 |
+ |
&immediate.palette,
|
|
158 |
+ |
&immediate.widget,
|
|
159 |
+ |
);
|
|
160 |
+ |
}
|
|
161 |
+ |
|
|
162 |
+ |
Node::Notice { tone, text, .. } => {
|
|
163 |
+ |
// The tone carries it, and the surface says it is a thing set on
|
|
164 |
+ |
// the page rather than part of the flow. Where a toast lands
|
|
165 |
+ |
// against a banner is renderer policy and this renderer has one
|
|
166 |
+ |
// place to put either, which is where the caller drew it.
|
|
167 |
+ |
frame(
|
|
168 |
+ |
ui,
|
|
169 |
+ |
layout::Depth::Raised,
|
|
170 |
+ |
&immediate.palette,
|
|
171 |
+ |
immediate.frame,
|
|
172 |
+ |
|ui| {
|
|
173 |
+ |
ui.label(RichText::new(text).color(immediate.palette.tone(*tone)));
|
|
174 |
+ |
},
|
|
175 |
+ |
);
|
|
176 |
+ |
}
|
|
177 |
+ |
|
|
178 |
+ |
Node::Meter(meter) => {
|
|
179 |
+ |
widget::meter(
|
|
180 |
+ |
ui,
|
|
181 |
+ |
&meter.as_layout(),
|
|
182 |
+ |
&immediate.palette,
|
|
183 |
+ |
&immediate.widget,
|
|
184 |
+ |
);
|
|
185 |
+ |
}
|
|
186 |
+ |
|
|
187 |
+ |
_ => unreachable!("an addressed node reached the leaf walk"),
|
|
188 |
+ |
}
|
|
189 |
+ |
}
|
|
190 |
+ |
|
| 138 |
191 |
|
/// The nodes that hold other nodes, or that a screen's own facts reach into.
|
| 139 |
192 |
|
///
|
| 140 |
193 |
|
/// Split from [`draw`] where the line fell naturally rather than to satisfy a
|
| 177 |
230 |
|
}
|
| 178 |
231 |
|
}
|
| 179 |
232 |
|
|
| 180 |
|
- |
Node::Table { .. } => {
|
| 181 |
|
- |
// Deliberately not drawn yet, and said rather than skipped
|
| 182 |
|
- |
// silently. `makeover_immediate::table` draws one, and what it
|
| 183 |
|
- |
// takes is a body closure per cell; wiring a described table's
|
| 184 |
|
- |
// cells through that is the next node, not a line here. A caller
|
| 185 |
|
- |
// meeting this sees nothing and should file it rather than wonder.
|
| 186 |
|
- |
ui.label(
|
| 187 |
|
- |
RichText::new("(a described table is not drawn by this renderer yet)")
|
| 188 |
|
- |
.color(pass.immediate.palette.content_muted),
|
| 189 |
|
- |
);
|
|
233 |
+ |
Node::Table { columns, rows } => {
|
|
234 |
+ |
table(pass, ui, columns, rows);
|
| 190 |
235 |
|
}
|
| 191 |
236 |
|
|
| 192 |
237 |
|
Node::Select {
|
| 198 |
243 |
|
select(pass, ui, options, chosen.as_deref(), action.as_ref());
|
| 199 |
244 |
|
}
|
| 200 |
245 |
|
|
| 201 |
|
- |
Node::Meter(meter) => {
|
| 202 |
|
- |
widget::meter(
|
| 203 |
|
- |
ui,
|
| 204 |
|
- |
&meter.as_layout(),
|
| 205 |
|
- |
&pass.immediate.palette,
|
| 206 |
|
- |
&pass.immediate.widget,
|
| 207 |
|
- |
);
|
| 208 |
|
- |
}
|
| 209 |
|
- |
|
| 210 |
246 |
|
Node::Stats { figures } => {
|
| 211 |
247 |
|
// Across rather than down, which is the one thing a strip says: a
|
| 212 |
248 |
|
// terminal stacks them because it has no width to spare, and a
|
| 410 |
446 |
|
});
|
| 411 |
447 |
|
}
|
| 412 |
448 |
|
|
|
449 |
+ |
/// A described table.
|
|
450 |
+ |
///
|
|
451 |
+ |
/// The narrowing, the header carets and the tracks are all
|
|
452 |
+ |
/// `makeover_immediate::table`'s; what is here is the walk that turns a
|
|
453 |
+ |
/// described cell into the nodes inside it, and the two facts a `Screen` adds
|
|
454 |
+ |
/// over a `Column`: the address a row opens, and the address a heading reorders
|
|
455 |
+ |
/// by.
|
|
456 |
+ |
///
|
|
457 |
+ |
/// **A cell is a run of nodes, so drawing one is [`draw`] again.** That is the
|
|
458 |
+ |
/// property the containment migration bought every renderer, and it is why a
|
|
459 |
+ |
/// button in a cell needs no special case here: it is a `Node::Act` like any
|
|
460 |
+ |
/// other, and it fires through the same `Pass`.
|
|
461 |
+ |
fn table(pass: &mut Pass<'_>, ui: &mut Ui, columns: &[quasi_router::Column], rows: &[Cells]) {
|
|
462 |
+ |
let borrowed: Vec<layout::Column<'_>> = columns.iter().map(|c| c.as_layout()).collect();
|
|
463 |
+ |
|
|
464 |
+ |
// Every row's `current` flag, read by index. `Body::selected` takes a
|
|
465 |
+ |
// predicate rather than a set, so an app whose selection is a range does not
|
|
466 |
+ |
// have to build a collection to be asked.
|
|
467 |
+ |
let current = |at: usize| rows.get(at).is_some_and(|row| row.current);
|
|
468 |
+ |
let body = makeover_immediate::table::Body {
|
|
469 |
+ |
rows: rows.len(),
|
|
470 |
+ |
selected: Some(¤t),
|
|
471 |
+ |
scroll_to: None,
|
|
472 |
+ |
};
|
|
473 |
+ |
let sizing = makeover_immediate::table::Sizing {
|
|
474 |
+ |
lengths: &[],
|
|
475 |
+ |
fallback: CELL_WIDTH,
|
|
476 |
+ |
};
|
|
477 |
+ |
|
|
478 |
+ |
// What the user pressed, collected rather than fired inside the closure:
|
|
479 |
+ |
// the closure holds `&mut Ui` and the pass at once, and firing needs the
|
|
480 |
+ |
// pass mutably.
|
|
481 |
+ |
let mut fired: Option<(Action, Params)> = None;
|
|
482 |
+ |
|
|
483 |
+ |
let reordered = makeover_immediate::table::table(
|
|
484 |
+ |
ui,
|
|
485 |
+ |
&borrowed,
|
|
486 |
+ |
&body,
|
|
487 |
+ |
&sizing,
|
|
488 |
+ |
&pass.immediate.palette,
|
|
489 |
+ |
&pass.immediate.table,
|
|
490 |
+ |
|ui, column, at| {
|
|
491 |
+ |
let Some(row) = rows.get(at) else { return };
|
|
492 |
+ |
let Some(index) = columns.iter().position(|c| c.name == column.name) else {
|
|
493 |
+ |
return;
|
|
494 |
+ |
};
|
|
495 |
+ |
let Some(cell) = row.values.get(index) else {
|
|
496 |
+ |
return;
|
|
497 |
+ |
};
|
|
498 |
+ |
for node in &cell.parts {
|
|
499 |
+ |
// A cell's contents are ordinary nodes, but a press inside one
|
|
500 |
+ |
// cannot reach the pass from here. Only the two that carry an
|
|
501 |
+ |
// address are collected; everything else draws.
|
|
502 |
+ |
match node {
|
|
503 |
+ |
Node::Act(act) if act.state != Some(layout::State::Disabled) => {
|
|
504 |
+ |
if widget::act(
|
|
505 |
+ |
ui,
|
|
506 |
+ |
&act.as_layout(),
|
|
507 |
+ |
&pass.immediate.palette,
|
|
508 |
+ |
&pass.immediate.widget,
|
|
509 |
+ |
)
|
|
510 |
+ |
.clicked()
|
|
511 |
+ |
{
|
|
512 |
+ |
fired = Some((act.action.clone(), Params::new()));
|
|
513 |
+ |
}
|
|
514 |
+ |
}
|
|
515 |
+ |
Node::Link { text, action } => {
|
|
516 |
+ |
if ui
|
|
517 |
+ |
.link(RichText::new(text).color(pass.immediate.palette.action))
|
|
518 |
+ |
.clicked()
|
|
519 |
+ |
{
|
|
520 |
+ |
fired = Some((action.clone(), Params::new()));
|
|
521 |
+ |
}
|
|
522 |
+ |
}
|
|
523 |
+ |
other => leaf(pass.immediate, ui, other),
|
|
524 |
+ |
}
|
|
525 |
+ |
}
|
|
526 |
+ |
// Opening the row itself, from whichever cell was clicked. A table
|
|
527 |
+ |
// row has no single element to hang it on the way a list row hangs
|
|
528 |
+ |
// it on its primary text.
|
|
529 |
+ |
if let Some(action) = &row.activate
|
|
530 |
+ |
&& ui.response().clicked()
|
|
531 |
+ |
{
|
|
532 |
+ |
fired = Some((action.clone(), Params::new()));
|
|
533 |
+ |
}
|
|
534 |
+ |
},
|
|
535 |
+ |
);
|
|
536 |
+ |
|
|
537 |
+ |
if let Some((action, payload)) = fired {
|
|
538 |
+ |
pass.fire(&action, payload, None);
|
|
539 |
+ |
}
|
|
540 |
+ |
|
|
541 |
+ |
// A heading that was pressed reorders by that column, and the column says
|
|
542 |
+ |
// what that calls. `sortable` is `reorder.is_some()`, so a column with no
|
|
543 |
+ |
// address answers no press.
|
|
544 |
+ |
if let Some(column) = reordered
|
|
545 |
+ |
&& let Some(described) = columns.iter().find(|c| c.name == column.name)
|
|
546 |
+ |
&& let Some(action) = &described.reorder
|
|
547 |
+ |
{
|
|
548 |
+ |
pass.fire(action, Params::new(), None);
|
|
549 |
+ |
}
|
|
550 |
+ |
}
|
|
551 |
+ |
|
| 413 |
552 |
|
/// A region, drawn as the surface its kind names.
|
| 414 |
553 |
|
pub(crate) fn region(pass: &mut Pass<'_>, ui: &mut Ui, slot: &Slot) {
|
| 415 |
554 |
|
// Readiness first: a region that is not ready has nothing to draw and says
|