|
1 |
+ |
//! The described settings screen, in a window beside the shipped one.
|
|
2 |
+ |
//!
|
|
3 |
+ |
//! Beside rather than instead of, which is the whole arrangement: with the
|
|
4 |
+ |
//! feature on, opening Settings opens both windows, and the two can be compared
|
|
5 |
+ |
//! by looking at them. A port that replaced the working panel on the way in
|
|
6 |
+ |
//! would have to be right first time.
|
|
7 |
+ |
//!
|
|
8 |
+ |
//! # What this module is, and what it deliberately is not
|
|
9 |
+ |
//!
|
|
10 |
+ |
//! It is the *host* half of the port, and it is small on purpose: fifty lines of
|
|
11 |
+ |
//! plumbing against a described screen that knows nothing about egui. Everything
|
|
12 |
+ |
//! it does is one of four things, and none of them is drawing:
|
|
13 |
+ |
//!
|
|
14 |
+ |
//! 1. resolve the host facts the screen needs ([`ThemeChoice`], the palette),
|
|
15 |
+ |
//! 2. hold the [`Runtime`] across frames, because a frame does not outlive itself,
|
|
16 |
+ |
//! 3. hand a [`Step`] to the router and the answer back to the runtime,
|
|
17 |
+ |
//! 4. put a route failure somewhere the user can see it.
|
|
18 |
+ |
//!
|
|
19 |
+ |
//! There is no `if let Node::...` anywhere here, and there should never be one.
|
|
20 |
+ |
//! The moment this file starts deciding what a node looks like, the drawing has
|
|
21 |
+ |
//! left `quasi-immediate` and the port has become a second renderer.
|
|
22 |
+ |
|
|
23 |
+ |
use quasi_immediate::{Immediate, Runtime, Step};
|
|
24 |
+ |
use quasi_router::Request;
|
|
25 |
+ |
|
|
26 |
+ |
use super::{Settings, ThemeChoice};
|
|
27 |
+ |
use crate::state::BrowserState;
|
|
28 |
+ |
use crate::ui::theme;
|
|
29 |
+ |
|
|
30 |
+ |
/// The described screen's own state, held across frames.
|
|
31 |
+ |
///
|
|
32 |
+ |
/// A `Runtime` and nothing else. What the user typed and ticked lives inside it,
|
|
33 |
+ |
/// which is the half egui does not hold for a described screen: the fields are
|
|
34 |
+ |
/// rebuilt from the description every frame, so their buffers have to outlive
|
|
35 |
+ |
/// one.
|
|
36 |
+ |
#[derive(Debug, Default)]
|
|
37 |
+ |
pub struct Described {
|
|
38 |
+ |
runtime: Option<Runtime>,
|
|
39 |
+ |
}
|
|
40 |
+ |
|
|
41 |
+ |
impl Described {
|
|
42 |
+ |
/// Forget the screen, so the next open reads the store again.
|
|
43 |
+ |
///
|
|
44 |
+ |
/// Called when the window closes rather than on every frame: the runtime is
|
|
45 |
+ |
/// what holds unsent edits, and rebuilding it per frame would throw away
|
|
46 |
+ |
/// what the user was typing.
|
|
47 |
+ |
pub fn close(&mut self) {
|
|
48 |
+ |
self.runtime = None;
|
|
49 |
+ |
}
|
|
50 |
+ |
}
|
|
51 |
+ |
|
|
52 |
+ |
/// Draw the described settings window, and act on whatever was pressed.
|
|
53 |
+ |
pub fn draw(ctx: &egui::Context, state: &mut BrowserState) {
|
|
54 |
+ |
// Taken out of the state for the frame, because the router borrows the
|
|
55 |
+ |
// backend off the same struct. Put back below whatever happens.
|
|
56 |
+ |
let mut runtime = state.described.runtime.take();
|
|
57 |
+ |
let themes = themes();
|
|
58 |
+ |
|
|
59 |
+ |
let mut open = true;
|
|
60 |
+ |
egui::Window::new("Settings (described)")
|
|
61 |
+ |
.open(&mut open)
|
|
62 |
+ |
.default_width(420.0)
|
|
63 |
+ |
.show(ctx, |ui| {
|
|
64 |
+ |
let immediate = Immediate::new(theme::palette());
|
|
65 |
+ |
|
|
66 |
+ |
// The first frame has no screen yet, so it asks for one. Everything
|
|
67 |
+ |
// after it is the loop below.
|
|
68 |
+ |
let runtime = match &mut runtime {
|
|
69 |
+ |
Some(runtime) => runtime,
|
|
70 |
+ |
none => match answer(state, &themes, Request::get("/settings")) {
|
|
71 |
+ |
Ok(screen) => none.insert(Runtime::new(screen)),
|
|
72 |
+ |
Err(message) => {
|
|
73 |
+ |
ui.label(message);
|
|
74 |
+ |
return;
|
|
75 |
+ |
}
|
|
76 |
+ |
},
|
|
77 |
+ |
};
|
|
78 |
+ |
|
|
79 |
+ |
match runtime.show(ui, &immediate) {
|
|
80 |
+ |
Step::Idle => {}
|
|
81 |
+ |
Step::Call(request) => match answer(state, &themes, request.clone()) {
|
|
82 |
+ |
Ok(screen) => {
|
|
83 |
+ |
runtime.apply(&request, quasi_router::Response::screen(screen));
|
|
84 |
+ |
}
|
|
85 |
+ |
Err(message) => runtime.say(message),
|
|
86 |
+ |
},
|
|
87 |
+ |
// A described control asked before acting. The shipped panel has
|
|
88 |
+ |
// no equivalent, so this is drawn where it is asked rather than
|
|
89 |
+ |
// in a second window.
|
|
90 |
+ |
Step::Ask(question) => {
|
|
91 |
+ |
ui.label(&question);
|
|
92 |
+ |
ui.horizontal(|ui| {
|
|
93 |
+ |
if ui.button("Yes").clicked() {
|
|
94 |
+ |
let step = runtime.answer(true);
|
|
95 |
+ |
if let Step::Call(request) = step {
|
|
96 |
+ |
match answer(state, &themes, request.clone()) {
|
|
97 |
+ |
Ok(screen) => {
|
|
98 |
+ |
runtime.apply(
|
|
99 |
+ |
&request,
|
|
100 |
+ |
quasi_router::Response::screen(screen),
|
|
101 |
+ |
);
|
|
102 |
+ |
}
|
|
103 |
+ |
Err(message) => runtime.say(message),
|
|
104 |
+ |
}
|
|
105 |
+ |
}
|
|
106 |
+ |
}
|
|
107 |
+ |
if ui.button("No").clicked() {
|
|
108 |
+ |
runtime.answer(false);
|
|
109 |
+ |
}
|
|
110 |
+ |
});
|
|
111 |
+ |
}
|
|
112 |
+ |
// Nothing here addresses anything outside the app yet. Said
|
|
113 |
+ |
// rather than ignored, so the first control that does is a
|
|
114 |
+ |
// message and not a silence.
|
|
115 |
+ |
Step::Open(address) => runtime.say(format!("Nothing here opens {address}.")),
|
|
116 |
+ |
}
|
|
117 |
+ |
});
|
|
118 |
+ |
|
|
119 |
+ |
state.described.runtime = runtime;
|
|
120 |
+ |
if !open {
|
|
121 |
+ |
state.settings.show_manager = false;
|
|
122 |
+ |
state.described.close();
|
|
123 |
+ |
}
|
|
124 |
+ |
}
|
|
125 |
+ |
|
|
126 |
+ |
/// Ask the router, and flatten a refusal into something a user can read.
|
|
127 |
+ |
///
|
|
128 |
+ |
/// The one place the app's own error vocabulary and the router's meet. A
|
|
129 |
+ |
/// `RouteError` carries a class and a message; a window has room for the
|
|
130 |
+ |
/// message.
|
|
131 |
+ |
fn answer(
|
|
132 |
+ |
state: &BrowserState,
|
|
133 |
+ |
themes: &[ThemeChoice],
|
|
134 |
+ |
request: Request,
|
|
135 |
+ |
) -> Result<quasi_router::Screen, String> {
|
|
136 |
+ |
let config = super::FromBackend(&*state.backend);
|
|
137 |
+ |
let settings = Settings {
|
|
138 |
+ |
config: &config,
|
|
139 |
+ |
themes,
|
|
140 |
+ |
};
|
|
141 |
+ |
let response = super::router()
|
|
142 |
+ |
.handle(&settings, request)
|
|
143 |
+ |
.map_err(|error| error.message.clone())?;
|
|
144 |
+ |
match response.outcome {
|
|
145 |
+ |
quasi_router::Outcome::Screen(screen) => Ok(screen),
|
|
146 |
+ |
other => Err(format!("the settings routes answered {other:?}")),
|
|
147 |
+ |
}
|
|
148 |
+ |
}
|
|
149 |
+ |
|
|
150 |
+ |
/// The themes the host has resolved, as the description names them.
|
|
151 |
+ |
fn themes() -> Vec<ThemeChoice> {
|
|
152 |
+ |
theme::list_themes()
|
|
153 |
+ |
.into_iter()
|
|
154 |
+ |
.map(|meta| ThemeChoice {
|
|
155 |
+ |
id: meta.id,
|
|
156 |
+ |
name: meta.name,
|
|
157 |
+ |
variant: meta.variant,
|
|
158 |
+ |
})
|
|
159 |
+ |
.collect()
|
|
160 |
+ |
}
|