| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
|
| 18 |
use egui::Ui; |
| 19 |
use quasi_router::{ |
| 20 |
Action, Chrome, Message, Method, Node, Outcome, Params, Request, Response, Screen, |
| 21 |
}; |
| 22 |
|
| 23 |
use crate::{Fired, Immediate, View, layout_notice}; |
| 24 |
|
| 25 |
|
| 26 |
#[derive(Debug, Clone, PartialEq, Eq)] |
| 27 |
pub enum Step { |
| 28 |
|
| 29 |
Idle, |
| 30 |
|
| 31 |
Call(Request), |
| 32 |
|
| 33 |
Ask(String), |
| 34 |
|
| 35 |
Open(String), |
| 36 |
} |
| 37 |
|
| 38 |
|
| 39 |
|
| 40 |
|
| 41 |
|
| 42 |
#[derive(Debug, Clone)] |
| 43 |
struct Layer { |
| 44 |
screen: Screen, |
| 45 |
view: View, |
| 46 |
} |
| 47 |
|
| 48 |
|
| 49 |
#[derive(Debug, Clone)] |
| 50 |
pub struct Runtime { |
| 51 |
screen: Screen, |
| 52 |
view: View, |
| 53 |
|
| 54 |
chrome: Chrome, |
| 55 |
|
| 56 |
|
| 57 |
|
| 58 |
|
| 59 |
|
| 60 |
under: Vec<Layer>, |
| 61 |
|
| 62 |
history: Vec<Request>, |
| 63 |
|
| 64 |
here: Option<Request>, |
| 65 |
|
| 66 |
asked: Option<(Action, Params)>, |
| 67 |
|
| 68 |
saying: Option<Message>, |
| 69 |
} |
| 70 |
|
| 71 |
impl Runtime { |
| 72 |
|
| 73 |
#[must_use] |
| 74 |
pub fn new(screen: Screen) -> Self { |
| 75 |
let mut runtime = Self { |
| 76 |
screen, |
| 77 |
view: View::new(), |
| 78 |
chrome: Chrome::new(), |
| 79 |
under: Vec::new(), |
| 80 |
history: Vec::new(), |
| 81 |
here: None, |
| 82 |
asked: None, |
| 83 |
saying: None, |
| 84 |
}; |
| 85 |
runtime.view.seed(&runtime.screen); |
| 86 |
runtime |
| 87 |
} |
| 88 |
|
| 89 |
|
| 90 |
#[must_use] |
| 91 |
pub fn with_chrome(mut self, chrome: Chrome) -> Self { |
| 92 |
self.chrome = chrome; |
| 93 |
self |
| 94 |
} |
| 95 |
|
| 96 |
|
| 97 |
#[must_use] |
| 98 |
pub const fn screen(&self) -> &Screen { |
| 99 |
&self.screen |
| 100 |
} |
| 101 |
|
| 102 |
|
| 103 |
#[must_use] |
| 104 |
pub const fn overlaid(&self) -> bool { |
| 105 |
!self.under.is_empty() |
| 106 |
} |
| 107 |
|
| 108 |
|
| 109 |
|
| 110 |
|
| 111 |
|
| 112 |
|
| 113 |
pub fn show(&mut self, ui: &mut Ui, immediate: &Immediate) -> Step { |
| 114 |
if let Some(binding) = self.pressed_binding(ui.ctx()) { |
| 115 |
return Self::call(&binding); |
| 116 |
} |
| 117 |
|
| 118 |
|
| 119 |
|
| 120 |
for layer in &self.under { |
| 121 |
let mut pass = crate::Pass { |
| 122 |
immediate, |
| 123 |
view: &mut layer.view.clone(), |
| 124 |
fired: None, |
| 125 |
}; |
| 126 |
crate::region::screen_regions(&mut pass, ui, &layer.screen); |
| 127 |
} |
| 128 |
|
| 129 |
let fired = if self.under.is_empty() { |
| 130 |
immediate.screen(ui, &self.screen, &mut self.view) |
| 131 |
} else { |
| 132 |
let mut fired = None; |
| 133 |
egui::Area::new(ui.id().with("quasi-overlay")) |
| 134 |
.order(egui::Order::Foreground) |
| 135 |
.show(ui.ctx(), |ui| { |
| 136 |
egui::Frame::popup(ui.style()) |
| 137 |
.shadow(immediate.palette().cast()) |
| 138 |
.show(ui, |ui| { |
| 139 |
fired = immediate.screen(ui, &self.screen, &mut self.view); |
| 140 |
}); |
| 141 |
}); |
| 142 |
fired |
| 143 |
}; |
| 144 |
|
| 145 |
|
| 146 |
|
| 147 |
if ui.ctx().input(|i| i.key_pressed(egui::Key::Escape)) { |
| 148 |
if self.dismiss() { |
| 149 |
return Step::Idle; |
| 150 |
} |
| 151 |
return self.back(); |
| 152 |
} |
| 153 |
|
| 154 |
match fired { |
| 155 |
Some(Fired { |
| 156 |
action, |
| 157 |
payload, |
| 158 |
confirm, |
| 159 |
}) => match confirm { |
| 160 |
Some(prompt) => { |
| 161 |
self.asked = Some((action, payload)); |
| 162 |
Step::Ask(prompt) |
| 163 |
} |
| 164 |
None => Self::send(&action, payload), |
| 165 |
}, |
| 166 |
None => Step::Idle, |
| 167 |
} |
| 168 |
} |
| 169 |
|
| 170 |
|
| 171 |
|
| 172 |
|
| 173 |
|
| 174 |
|
| 175 |
pub fn say(&mut self, text: impl Into<String>) { |
| 176 |
self.screen.notices.push(Node::Notice { |
| 177 |
kind: quasi_router::layout::Notice::Banner, |
| 178 |
tone: quasi_router::layout::Tone::Danger, |
| 179 |
text: text.into(), |
| 180 |
}); |
| 181 |
} |
| 182 |
|
| 183 |
|
| 184 |
|
| 185 |
|
| 186 |
|
| 187 |
pub fn answer(&mut self, yes: bool) -> Step { |
| 188 |
match self.asked.take() { |
| 189 |
Some((action, payload)) if yes => Self::send(&action, payload), |
| 190 |
_ => Step::Idle, |
| 191 |
} |
| 192 |
} |
| 193 |
|
| 194 |
|
| 195 |
pub fn back(&mut self) -> Step { |
| 196 |
match self.history.pop() { |
| 197 |
Some(request) => { |
| 198 |
self.here = Some(request.clone()); |
| 199 |
Step::Call(request) |
| 200 |
} |
| 201 |
None => Step::Idle, |
| 202 |
} |
| 203 |
} |
| 204 |
|
| 205 |
|
| 206 |
|
| 207 |
|
| 208 |
|
| 209 |
fn dismiss(&mut self) -> bool { |
| 210 |
match self.under.pop() { |
| 211 |
Some(layer) => { |
| 212 |
self.screen = layer.screen; |
| 213 |
self.view = layer.view; |
| 214 |
true |
| 215 |
} |
| 216 |
None => false, |
| 217 |
} |
| 218 |
} |
| 219 |
|
| 220 |
|
| 221 |
fn pressed_binding(&self, ctx: &egui::Context) -> Option<Action> { |
| 222 |
if self.chrome.bindings.is_empty() { |
| 223 |
return None; |
| 224 |
} |
| 225 |
ctx.input(|input| { |
| 226 |
self.chrome.bindings.iter().find_map(|binding| { |
| 227 |
let (key, modifiers) = parse(&binding.key)?; |
| 228 |
input |
| 229 |
.key_pressed(key) |
| 230 |
.then(|| input.modifiers.matches_logically(modifiers)) |
| 231 |
.and_then(|held| held.then(|| binding.action.clone())) |
| 232 |
}) |
| 233 |
}) |
| 234 |
} |
| 235 |
|
| 236 |
|
| 237 |
pub fn apply(&mut self, request: &Request, response: Response) -> Option<Request> { |
| 238 |
let Response { |
| 239 |
outcome, |
| 240 |
notice, |
| 241 |
address, |
| 242 |
invalidates, |
| 243 |
} = response; |
| 244 |
self.saying = notice.or(self.saying.take()); |
| 245 |
|
| 246 |
match outcome { |
| 247 |
|
| 248 |
|
| 249 |
|
| 250 |
Outcome::Over(screen) => { |
| 251 |
let under = Layer { |
| 252 |
screen: std::mem::replace(&mut self.screen, screen), |
| 253 |
view: std::mem::replace(&mut self.view, View::new()), |
| 254 |
}; |
| 255 |
self.under.push(under); |
| 256 |
self.view.seed(&self.screen); |
| 257 |
self.announce(); |
| 258 |
None |
| 259 |
} |
| 260 |
Outcome::Screen(screen) => { |
| 261 |
|
| 262 |
|
| 263 |
self.under.clear(); |
| 264 |
self.remember(request, address.as_ref()); |
| 265 |
self.screen = screen; |
| 266 |
self.view.reset(); |
| 267 |
self.view.seed(&self.screen); |
| 268 |
self.announce(); |
| 269 |
None |
| 270 |
} |
| 271 |
Outcome::Fragment { region, node } => { |
| 272 |
let mut missing: Vec<String> = Vec::new(); |
| 273 |
if !self.screen.replace(®ion, node) { |
| 274 |
missing.push(region); |
| 275 |
} |
| 276 |
for stale in invalidates { |
| 277 |
if !self.screen.replace(&stale.region, stale.node) { |
| 278 |
missing.push(stale.region); |
| 279 |
} |
| 280 |
} |
| 281 |
if !missing.is_empty() { |
| 282 |
let named = missing |
| 283 |
.iter() |
| 284 |
.map(|region| format!("`{region}`")) |
| 285 |
.collect::<Vec<_>>() |
| 286 |
.join(", "); |
| 287 |
let subject = if missing.len() == 1 { "is" } else { "are" }; |
| 288 |
self.saying = Some(layout_notice(format!( |
| 289 |
"{named} {subject} not on this screen" |
| 290 |
))); |
| 291 |
} |
| 292 |
self.announce(); |
| 293 |
None |
| 294 |
} |
| 295 |
|
| 296 |
|
| 297 |
|
| 298 |
Outcome::Goto(action) => { |
| 299 |
let path = action.destination.route()?; |
| 300 |
let request = Request::get(path.to_string()).carrying(action.carried.clone()); |
| 301 |
self.remember(&request, None); |
| 302 |
Some(request) |
| 303 |
} |
| 304 |
} |
| 305 |
} |
| 306 |
|
| 307 |
|
| 308 |
fn remember(&mut self, request: &Request, address: Option<&quasi_router::Address>) { |
| 309 |
let place = match address { |
| 310 |
Some(quasi_router::Address::Enters(_)) => true, |
| 311 |
Some(quasi_router::Address::Unchanged) => false, |
| 312 |
Some(quasi_router::Address::Replaces(_)) => { |
| 313 |
self.here = Some(request.clone()); |
| 314 |
return; |
| 315 |
} |
| 316 |
None => request.method == Method::Get, |
| 317 |
}; |
| 318 |
if place && let Some(previous) = self.here.replace(request.clone()) { |
| 319 |
self.history.push(previous); |
| 320 |
} |
| 321 |
} |
| 322 |
|
| 323 |
|
| 324 |
fn announce(&mut self) { |
| 325 |
if let Some(Message { |
| 326 |
kind, tone, text, .. |
| 327 |
}) = self.saying.take() |
| 328 |
{ |
| 329 |
self.screen.notices.push(Node::Notice { kind, tone, text }); |
| 330 |
} |
| 331 |
} |
| 332 |
|
| 333 |
|
| 334 |
|
| 335 |
|
| 336 |
|
| 337 |
|
| 338 |
fn send(action: &Action, extra: Params) -> Step { |
| 339 |
let Some(path) = action.destination.route() else { |
| 340 |
return Step::Open(action.destination.as_str().to_string()); |
| 341 |
}; |
| 342 |
let mut payload = extra; |
| 343 |
payload.absorb(action.params.clone()); |
| 344 |
Step::Call(Request { |
| 345 |
method: action.method, |
| 346 |
path: path.to_string(), |
| 347 |
captures: Params::new(), |
| 348 |
payload, |
| 349 |
carried: action.carried.clone(), |
| 350 |
}) |
| 351 |
} |
| 352 |
|
| 353 |
|
| 354 |
fn call(action: &Action) -> Step { |
| 355 |
Self::send(action, Params::new()) |
| 356 |
} |
| 357 |
} |
| 358 |
|
| 359 |
|
| 360 |
|
| 361 |
|
| 362 |
|
| 363 |
|
| 364 |
|
| 365 |
fn parse(key: &str) -> Option<(egui::Key, egui::Modifiers)> { |
| 366 |
let mut modifiers = egui::Modifiers::NONE; |
| 367 |
let mut base = None; |
| 368 |
for part in key.split('+') { |
| 369 |
let part = part.trim(); |
| 370 |
if part.is_empty() { |
| 371 |
return None; |
| 372 |
} |
| 373 |
match part.to_ascii_lowercase().as_str() { |
| 374 |
"ctrl" | "control" => modifiers.ctrl = true, |
| 375 |
"alt" | "option" => modifiers.alt = true, |
| 376 |
"shift" => modifiers.shift = true, |
| 377 |
"meta" | "cmd" | "super" => modifiers.command = true, |
| 378 |
_ if base.is_some() => return None, |
| 379 |
other => base = egui::Key::from_name(other).or_else(|| egui::Key::from_name(part)), |
| 380 |
} |
| 381 |
} |
| 382 |
base.map(|key| (key, modifiers)) |
| 383 |
} |
| 384 |
|