max / audiofiles
2 files changed,
+275 insertions,
-131 deletions
| @@ -4277,7 +4277,7 @@ | |||
| 4277 | 4277 | ||
| 4278 | 4278 | [[package]] | |
| 4279 | 4279 | name = "quasi-router" | |
| 4280 | - | version = "0.101.13" | |
| 4280 | + | version = "0.101.14" | |
| 4281 | 4281 | dependencies = [ | |
| 4282 | 4282 | "makeover-layout", | |
| 4283 | 4283 | ] | |
| @@ -7578,10 +7578,6 @@ | |||
| 7578 | 7578 | name = "painhours" | |
| 7579 | 7579 | version = "0.1.0" | |
| 7580 | 7580 | ||
| 7581 | - | [[patch.unused]] | |
| 7582 | - | name = "quasi-type" | |
| 7583 | - | version = "0.1.3" | |
| 7584 | - | ||
| 7585 | 7581 | [[patch.unused]] | |
| 7586 | 7582 | name = "quasi-axum" | |
| 7587 | 7583 | version = "0.101.1" | |
| @@ -7609,3 +7605,7 @@ | |||
| 7609 | 7605 | [[patch.unused]] | |
| 7610 | 7606 | name = "quasi-webview" | |
| 7611 | 7607 | version = "0.101.1" | |
| 7608 | + | ||
| 7609 | + | [[patch.unused]] | |
| 7610 | + | name = "quasi-type" | |
| 7611 | + | version = "0.1.3" |
| @@ -63,11 +63,9 @@ | |||
| 63 | 63 | //! filed shape and this is not quite it -- what is here is several forms rather | |
| 64 | 64 | //! than one form with two submits -- so it is recorded rather than counted. | |
| 65 | 65 | ||
| 66 | - | use quasi_router::layout::{FieldKind, Tone, Width}; | |
| 67 | - | use quasi_router::{ | |
| 68 | - | Act, Action, Field, Node, RegionKind, Request, Response, RouteError, Router, Row, Screen, Slot, | |
| 69 | - | Tag, | |
| 70 | - | }; | |
| 66 | + | use quasi_declare::declare; | |
| 67 | + | use quasi_router::layout::Tone; | |
| 68 | + | use quasi_router::{Act, Action, Request, Response, RouteError, Router}; | |
| 71 | 69 | ||
| 72 | 70 | use super::{Narrowing, Panels}; | |
| 73 | 71 | ||
| @@ -89,23 +87,34 @@ | |||
| 89 | 87 | /// every section below them and made hunting for one a linear scan. A group is | |
| 90 | 88 | /// a [`Node::section`] with a row of tags under it, which is the same shape one | |
| 91 | 89 | /// level up from `ui.horizontal_wrapped`. | |
| 92 | - | const KEY_GROUPS: [(&str, [&str; 12]); 2] = [ | |
| 93 | - | ( | |
| 94 | - | "Major", | |
| 95 | - | [ | |
| 90 | + | const KEY_GROUPS: [KeyGroup; 2] = [ | |
| 91 | + | KeyGroup { | |
| 92 | + | name: "Major", | |
| 93 | + | keys: [ | |
| 96 | 94 | "C major", "C# major", "D major", "D# major", "E major", "F major", "F# major", | |
| 97 | 95 | "G major", "G# major", "A major", "A# major", "B major", | |
| 98 | 96 | ], | |
| 99 | - | ), | |
| 100 | - | ( | |
| 101 | - | "Minor", | |
| 102 | - | [ | |
| 97 | + | }, | |
| 98 | + | KeyGroup { | |
| 99 | + | name: "Minor", | |
| 100 | + | keys: [ | |
| 103 | 101 | "C minor", "C# minor", "D minor", "D# minor", "E minor", "F minor", "F# minor", | |
| 104 | 102 | "G minor", "G# minor", "A minor", "A# minor", "B minor", | |
| 105 | 103 | ], | |
| 106 | - | ), | |
| 104 | + | }, | |
| 107 | 105 | ]; | |
| 108 | 106 | ||
| 107 | + | /// One heading and the keys under it. | |
| 108 | + | /// | |
| 109 | + | /// Named fields rather than a pair, because a description names what it draws | |
| 110 | + | /// and `.1` is not a name. Third const in the transition to be reshaped for it. | |
| 111 | + | struct KeyGroup { | |
| 112 | + | /// The heading. | |
| 113 | + | name: &'static str, | |
| 114 | + | /// The twelve keys under it, spelled the way the library spells them. | |
| 115 | + | keys: [&'static str; 12], | |
| 116 | + | } | |
| 117 | + | ||
| 109 | 118 | // The six axes, and the geometry each one is fixed by. | |
| 110 | 119 | // | |
| 111 | 120 | // They live here rather than in a renderer: the description reads them, and | |
| @@ -234,7 +243,7 @@ | |||
| 234 | 243 | ||
| 235 | 244 | /// `GET /filters` | |
| 236 | 245 | fn index(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 237 | - | Ok(screen(state).into()) | |
| 246 | + | Ok(showing(state)) | |
| 238 | 247 | } | |
| 239 | 248 | ||
| 240 | 249 | /// `POST /filters/axis/{key}` | |
| @@ -263,31 +272,31 @@ | |||
| 263 | 272 | lower.filter(|low| *low > axis.axis.lo), | |
| 264 | 273 | upper.filter(|high| *high < axis.axis.hi), | |
| 265 | 274 | ); | |
| 266 | - | Ok(screen(state).into()) | |
| 275 | + | Ok(showing(state)) | |
| 267 | 276 | } | |
| 268 | 277 | ||
| 269 | 278 | /// `POST /filters/axis/{key}/clear` | |
| 270 | 279 | fn widen(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> { | |
| 271 | 280 | let axis = axis_named(state, request.captures.require("key")?)?; | |
| 272 | 281 | state.filters.narrow(axis.key, None, None); | |
| 273 | - | Ok(screen(state).into()) | |
| 282 | + | Ok(showing(state)) | |
| 274 | 283 | } | |
| 275 | 284 | ||
| 276 | 285 | /// `POST /filters/keys/mode` | |
| 277 | 286 | fn key_mode(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> { | |
| 278 | 287 | let compatible = request.payload.get("mode") == Some("compatible"); | |
| 279 | 288 | state.filters.set_key_mode(compatible); | |
| 280 | - | Ok(screen(state).into()) | |
| 289 | + | Ok(showing(state)) | |
| 281 | 290 | } | |
| 282 | 291 | ||
| 283 | 292 | /// `POST /filters/keys/{key}/toggle` | |
| 284 | 293 | fn toggle_key(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> { | |
| 285 | 294 | let key = request.captures.require("key")?; | |
| 286 | - | if !KEY_GROUPS.iter().any(|(_, keys)| keys.contains(&key)) { | |
| 295 | + | if !KEY_GROUPS.iter().any(|group| group.keys.contains(&key)) { | |
| 287 | 296 | return Err(RouteError::not_found("no such key")); | |
| 288 | 297 | } | |
| 289 | 298 | state.filters.toggle_key(key); | |
| 290 | - | Ok(screen(state).into()) | |
| 299 | + | Ok(showing(state)) | |
| 291 | 300 | } | |
| 292 | 301 | ||
| 293 | 302 | /// `POST /filters/keys/clear` | |
| @@ -296,7 +305,7 @@ | |||
| 296 | 305 | return Err(RouteError::not_found("no key is wanted")); | |
| 297 | 306 | } | |
| 298 | 307 | state.filters.clear_keys(); | |
| 299 | - | Ok(screen(state).into()) | |
| 308 | + | Ok(showing(state)) | |
| 300 | 309 | } | |
| 301 | 310 | ||
| 302 | 311 | /// `POST /filters/tags/typing` | |
| @@ -304,7 +313,7 @@ | |||
| 304 | 313 | state | |
| 305 | 314 | .filters | |
| 306 | 315 | .typed(request.payload.get("tag").unwrap_or_default()); | |
| 307 | - | Ok(screen(state).into()) | |
| 316 | + | Ok(showing(state)) | |
| 308 | 317 | } | |
| 309 | 318 | ||
| 310 | 319 | /// `POST /filters/tags/add` | |
| @@ -328,7 +337,7 @@ | |||
| 328 | 337 | // The box empties on the way through, which is the shipped path's own | |
| 329 | 338 | // behaviour and is the half a bare `require` would lose. | |
| 330 | 339 | state.filters.typed(""); | |
| 331 | - | Ok(screen(state).into()) | |
| 340 | + | Ok(showing(state)) | |
| 332 | 341 | } | |
| 333 | 342 | ||
| 334 | 343 | /// `POST /filters/tags/{tag}/remove` | |
| @@ -338,7 +347,7 @@ | |||
| 338 | 347 | return Err(RouteError::not_found("not required")); | |
| 339 | 348 | } | |
| 340 | 349 | state.filters.unrequire(tag); | |
| 341 | - | Ok(screen(state).into()) | |
| 350 | + | Ok(showing(state)) | |
| 342 | 351 | } | |
| 343 | 352 | ||
| 344 | 353 | /// `POST /filters/tags/clear` | |
| @@ -347,7 +356,7 @@ | |||
| 347 | 356 | return Err(RouteError::not_found("no tag is required")); | |
| 348 | 357 | } | |
| 349 | 358 | state.filters.clear_tags(); | |
| 350 | - | Ok(screen(state).into()) | |
| 359 | + | Ok(showing(state)) | |
| 351 | 360 | } | |
| 352 | 361 | ||
| 353 | 362 | /// `POST /filters/clear` | |
| @@ -356,7 +365,7 @@ | |||
| 356 | 365 | return Err(RouteError::not_found("nothing is filtering")); | |
| 357 | 366 | } | |
| 358 | 367 | state.filters.clear_all(); | |
| 359 | - | Ok(screen(state).into()) | |
| 368 | + | Ok(showing(state)) | |
| 360 | 369 | } | |
| 361 | 370 | ||
| 362 | 371 | /// `POST /filters/save` | |
| @@ -374,208 +383,330 @@ | |||
| 374 | 383 | typed.to_owned() | |
| 375 | 384 | }; | |
| 376 | 385 | state.filters.save_collection(&name); | |
| 377 | - | Ok(screen(state).into()) | |
| 386 | + | Ok(showing(state)) | |
| 378 | 387 | } | |
| 379 | 388 | ||
| 380 | - | /// The screen: the axes, the keys, the tags, and what matched. | |
| 381 | - | fn screen(state: &Panels<'_>) -> Screen { | |
| 382 | - | let mut pane = Screen::sidebar_content("Filters") | |
| 383 | - | .with(axes(state)) | |
| 384 | - | .with(keys(state)) | |
| 385 | - | .with(tags(state)) | |
| 386 | - | .with(foot(state)); | |
| 387 | - | if state.filters.active() { | |
| 388 | - | pane = pane.with(saving(state)); | |
| 389 | - | } | |
| 390 | - | pane | |
| 389 | + | /// The panel, read and then described. | |
| 390 | + | pub(super) fn showing(state: &Panels<'_>) -> Response { | |
| 391 | + | Response::from(screen(&read(state))) | |
| 391 | 392 | } | |
| 392 | 393 | ||
| 393 | - | /// The six numeric axes, one interval each. | |
| 394 | - | fn axes(state: &Panels<'_>) -> Slot { | |
| 395 | - | let mut band = Slot::new(AXES, RegionKind::Band).with(Node::section("Filters")); | |
| 396 | - | for narrowing in state.filters.axes() { | |
| 397 | - | band = band.with(axis_form(&narrowing)); | |
| 398 | - | } | |
| 399 | - | band | |
| 394 | + | /// What the panel draws, read off the app. | |
| 395 | + | struct Panel { | |
| 396 | + | /// The six numeric axes, in the order the panel offers them. | |
| 397 | + | axes: Vec<Axis>, | |
| 398 | + | /// The two matching rules, and which one is in force. | |
| 399 | + | modes: [Mode; 2], | |
| 400 | + | /// The key groups, with each key's pill and whether it is held down. | |
| 401 | + | groups: [Group; 2], | |
| 402 | + | /// Whether any key is wanted, which is what offers the clear. | |
| 403 | + | keyed: bool, | |
| 404 | + | /// The tags every result has to carry. | |
| 405 | + | tags: Vec<String>, | |
| 406 | + | /// Whether no tag is required. | |
| 407 | + | untagged: bool, | |
| 408 | + | /// What is in the tag box. | |
| 409 | + | typing: String, | |
| 410 | + | /// What matched, pluralised for one. | |
| 411 | + | matched: String, | |
| 412 | + | /// Whether anything is filtering at all. | |
| 413 | + | active: bool, | |
| 414 | + | /// The name the save box would take if nothing were typed into it. | |
| 415 | + | describes: String, | |
| 400 | 416 | } | |
| 401 | 417 | ||
| 402 | - | /// One axis, as the question it is. | |
| 418 | + | /// One numeric axis, as the question it is. | |
| 403 | 419 | /// | |
| 404 | - | /// A form rather than a bare field because the two ends submit together: the | |
| 405 | - | /// route needs both, and a `changes` per end would have it guessing at the one | |
| 406 | - | /// that did not move. | |
| 407 | - | fn axis_form(narrowing: &Narrowing) -> Node { | |
| 408 | - | let axis = narrowing.axis; | |
| 409 | - | let mut field = Field { | |
| 410 | - | min: Some(number(axis.lo, axis.decimals)), | |
| 411 | - | max: Some(number(axis.hi, axis.decimals)), | |
| 412 | - | step: Some(number(axis.speed, axis.decimals)), | |
| 413 | - | // The suffix carried its leading space because it was going straight | |
| 414 | - | // into a DragValue. The description writes the symbol alone and the | |
| 415 | - | // spacing is the renderer's. | |
| 416 | - | unit: unit(axis.suffix), | |
| 417 | - | hint: axis.hint.map(str::to_owned), | |
| 418 | - | width: Width::Fill, | |
| 419 | - | ..Field::interval( | |
| 420 | - | lower_name(narrowing.key), | |
| 421 | - | upper_name(narrowing.key), | |
| 422 | - | axis.title, | |
| 423 | - | ) | |
| 424 | - | }; | |
| 425 | - | if let Some(low) = narrowing.lower { | |
| 426 | - | field = field.value(number(low, axis.decimals)); | |
| 427 | - | } | |
| 428 | - | if let Some(high) = narrowing.upper { | |
| 429 | - | field = field.upper_value(number(high, axis.decimals)); | |
| 430 | - | } | |
| 431 | - | ||
| 432 | - | let mut region = | |
| 433 | - | Slot::new(format!("filters-axis-{}", narrowing.key), RegionKind::Group).with(Node::Form { | |
| 434 | - | action: Action::post(format!("/filters/axis/{}", narrowing.key)), | |
| 435 | - | submit: "Apply".into(), | |
| 436 | - | fields: vec![field], | |
| 437 | - | }); | |
| 438 | - | // Offered only when the axis is narrowed, which is the shipped section's own | |
| 439 | - | // gate: a clear link on an axis nobody has touched is a control that does | |
| 440 | - | // nothing and says the axis is doing something. | |
| 441 | - | if narrowing.lower.is_some() || narrowing.upper.is_some() { | |
| 442 | - | region = region.with(Node::Act(Act::new( | |
| 443 | - | "Clear", | |
| 444 | - | Action::post(format!("/filters/axis/{}/clear", narrowing.key)), | |
| 445 | - | ))); | |
| 446 | - | } | |
| 447 | - | Node::Region(region) | |
| 420 | + | /// Every number is a string here because the description carries bounds as | |
| 421 | + | /// text: `RangeAxis::decimals` is what the shipped DragValue was given, and | |
| 422 | + | /// [`number`] is the one place the two meet. | |
| 423 | + | struct Axis { | |
| 424 | + | /// The key the axis is addressed by. | |
| 425 | + | key: &'static str, | |
| 426 | + | /// What the question is called. | |
| 427 | + | title: &'static str, | |
| 428 | + | /// The name the lower end submits under. | |
| 429 | + | lower_name: String, | |
| 430 | + | /// The name the upper end submits under. | |
| 431 | + | upper_name: String, | |
| 432 | + | /// The sentinel edges, which are the rule each end is checked against. | |
| 433 | + | min: String, | |
| 434 | + | max: String, | |
| 435 | + | /// The granularity the value moves in. | |
| 436 | + | step: String, | |
| 437 | + | /// The unit symbol, where the axis has one. | |
| 438 | + | unit: Option<String>, | |
| 439 | + | /// What the axis means, where that needs saying. | |
| 440 | + | hint: Option<&'static str>, | |
| 441 | + | /// The lower end wanted, if one is. | |
| 442 | + | lower: Option<String>, | |
| 443 | + | /// The upper end wanted, if one is. | |
| 444 | + | upper: Option<String>, | |
| 445 | + | /// Whether either end is set. | |
| 446 | + | /// | |
| 447 | + | /// The shipped section's own gate on the clear: a clear link on an axis | |
| 448 | + | /// nobody has touched is a control that does nothing and says the axis is | |
| 449 | + | /// doing something. | |
| 450 | + | narrowed: bool, | |
| 448 | 451 | } | |
| 449 | 452 | ||
| 450 | - | /// The keys wanted, and how they are matched. | |
| 451 | - | fn keys(state: &Panels<'_>) -> Slot { | |
| 453 | + | /// One matching rule, as the act that takes it. | |
| 454 | + | /// | |
| 455 | + | /// Two acts rather than a two-option field, because picking a matching rule is | |
| 456 | + | /// not a value being submitted with anything: it takes effect as it is pressed, | |
| 457 | + | /// which is what the shipped segmented control does. | |
| 458 | + | struct Mode { | |
| 459 | + | /// What the button reads. | |
| 460 | + | label: &'static str, | |
| 461 | + | /// What it writes. | |
| 462 | + | value: &'static str, | |
| 463 | + | /// Why it is there. | |
| 464 | + | why: &'static str, | |
| 465 | + | /// Whether it is the rule in force, which is what greys it out. | |
| 466 | + | held: bool, | |
| 467 | + | } | |
| 468 | + | ||
| 469 | + | /// One key group, drawn. | |
| 470 | + | struct Group { | |
| 471 | + | /// The heading. | |
| 472 | + | name: &'static str, | |
| 473 | + | /// The twelve keys under it. | |
| 474 | + | keys: Vec<Key>, | |
| 475 | + | } | |
| 476 | + | ||
| 477 | + | /// One key, as the pill it is. | |
| 478 | + | struct Key { | |
| 479 | + | /// What the pill reads: the note alone, which is what a producer scans for. | |
| 480 | + | note: &'static str, | |
| 481 | + | /// The whole key, which is what the query compares and the address carries. | |
| 482 | + | spelling: &'static str, | |
| 483 | + | /// Whether it is held down. | |
| 484 | + | wanted: bool, | |
| 485 | + | } | |
| 486 | + | ||
| 487 | + | /// What the panel draws, read off the app. | |
| 488 | + | fn read(state: &Panels<'_>) -> Panel { | |
| 452 | 489 | let held = state.filters.keys(); | |
| 453 | - | let mut band = Slot::new(KEYS, RegionKind::Band).with(Node::section("Key")); | |
| 454 | - | ||
| 455 | - | // Two acts rather than a two-option field, because picking a matching rule | |
| 456 | - | // is not a value being submitted with anything: it takes effect as it is | |
| 457 | - | // pressed, which is what the shipped segmented control does. | |
| 458 | - | for (compatible, label, why) in [ | |
| 459 | - | (false, "Exact", "Match only the keys chosen"), | |
| 460 | - | ( | |
| 461 | - | true, | |
| 462 | - | "Compatible", | |
| 463 | - | "Include musically compatible keys, around the circle of fifths", | |
| 490 | + | let tags = state.filters.tags(); | |
| 491 | + | let matched = state.filters.matched(); | |
| 492 | + | Panel { | |
| 493 | + | axes: state | |
| 494 | + | .filters | |
| 495 | + | .axes() | |
| 496 | + | .into_iter() | |
| 497 | + | .map(|narrowing| axis(&narrowing)) | |
| 498 | + | .collect(), | |
| 499 | + | modes: [ | |
| 500 | + | Mode { | |
| 501 | + | label: "Exact", | |
| 502 | + | value: "exact", | |
| 503 | + | why: "Match only the keys chosen", | |
| 504 | + | held: !held.compatible, | |
| 505 | + | }, | |
| 506 | + | Mode { | |
| 507 | + | label: "Compatible", | |
| 508 | + | value: "compatible", | |
| 509 | + | why: "Include musically compatible keys, around the circle of fifths", | |
| 510 | + | held: held.compatible, | |
| 511 | + | }, | |
| 512 | + | ], | |
| 513 | + | groups: KEY_GROUPS.map(|group| Group { | |
| 514 | + | name: group.name, | |
| 515 | + | keys: group | |
| 516 | + | .keys | |
| 517 | + | .iter() | |
| 518 | + | .map(|spelling| Key { | |
| 519 | + | note: spelling.split(' ').next().unwrap_or(spelling), | |
| 520 | + | spelling, | |
| 521 | + | wanted: held.wanted.iter().any(|held| held == spelling), | |
| 522 | + | }) | |
| 523 | + | .collect(), | |
| 524 | + | }), | |
| 525 | + | keyed: !held.wanted.is_empty(), | |
| 526 | + | untagged: tags.is_empty(), | |
| 527 | + | tags, | |
| 528 | + | typing: state.filters.typing(), | |
| 529 | + | matched: format!( | |
| 530 | + | "{matched} sample{} match", | |
| 531 | + | if matched == 1 { "" } else { "s" } | |
| 464 | 532 | ), | |
| 465 | - | ] { | |
| 466 | - | let mut act = Act::new( | |
| 467 | - | label, | |
| 468 | - | Action::post("/filters/keys/mode") | |
| 469 | - | .with("mode", if compatible { "compatible" } else { "exact" }), | |
| 470 | - | ); | |
| 471 | - | if held.compatible == compatible { | |
| 472 | - | act = act.disabled(); | |
| 473 | - | } | |
| 474 | - | band = band.with(Node::Act(act)).with(Node::text(why)); | |
| 533 | + | active: state.filters.active(), | |
| 534 | + | describes: state.filters.describes(), | |
| 475 | 535 | } | |
| 536 | + | } | |
| 476 | 537 | ||
| 477 | - | for (group, spellings) in KEY_GROUPS { | |
| 478 | - | band = band.with(Node::text(group)); | |
| 479 | - | for spelling in spellings { | |
| 480 | - | // The pill reads as the note and stores the whole key, which is the | |
| 481 | - | // shipped row's own split: "C#" is what a producer scans for and | |
| 482 | - | // "C# minor" is what the query compares. | |
| 483 | - | let note = spelling.split(' ').next().unwrap_or(spelling); | |
| 484 | - | let wanted = held.wanted.iter().any(|held| held == spelling); | |
| 485 | - | band = band.with(Node::Token( | |
| 486 | - | Tag::chip( | |
| 487 | - | note, | |
| 488 | - | Action::post(format!("/filters/keys/{spelling}/toggle")), | |
| 489 | - | ) | |
| 538 | + | /// One axis, in the words the question uses. | |
| 539 | + | fn axis(narrowing: &Narrowing) -> Axis { | |
| 540 | + | let axis = narrowing.axis; | |
| 541 | + | Axis { | |
| 542 | + | key: narrowing.key, | |
| 543 | + | title: axis.title, | |
| 544 | + | lower_name: lower_name(narrowing.key), | |
| 545 | + | upper_name: upper_name(narrowing.key), | |
| 546 | + | min: number(axis.lo, axis.decimals), | |
| 547 | + | max: number(axis.hi, axis.decimals), | |
| 548 | + | step: number(axis.speed, axis.decimals), | |
| 549 | + | unit: unit(axis.suffix), | |
| 550 | + | hint: axis.hint, | |
| 551 | + | lower: narrowing.lower.map(|low| number(low, axis.decimals)), | |
| 552 | + | upper: narrowing.upper.map(|high| number(high, axis.decimals)), | |
| 553 | + | narrowed: narrowing.lower.is_some() || narrowing.upper.is_some(), | |
| 554 | + | } | |
| 555 | + | } | |
| 556 | + | ||
| 557 | + | declare! { | |
| 558 | + | /// The screen: the axes, the keys, the tags, and what matched. | |
| 559 | + | shape screen(panel: &Panel) -> Screen; | |
| 560 | + | ||
| 561 | + | screen sidebar_content "Filters" { | |
| 562 | + | include axes(panel); | |
| 563 | + | include keys(panel); | |
| 564 | + | include tags(panel); | |
| 565 | + | include foot(panel); | |
| 566 | + | include saving(panel) when panel.active; | |
| 567 | + | } | |
| 568 | + | } | |
| 569 | + | ||
| 570 | + | declare! { | |
| 571 | + | /// The six numeric axes, one interval each. | |
| 572 | + | shape axes(panel: &Panel) -> Slot; | |
| 573 | + | ||
| 574 | + | region AXES as Band { | |
| 575 | + | section "Filters"; | |
| 576 | + | for axis in panel.axes.iter() { | |
| 577 | + | include axis_form(axis); | |
| 578 | + | } | |
| 579 | + | } | |
| 580 | + | } | |
| 581 | + | ||
| 582 | + | declare! { | |
| 583 | + | /// One axis, as the question it is. | |
| 584 | + | /// | |
| 585 | + | /// A form rather than a bare field because the two ends submit together: | |
| 586 | + | /// the route needs both, and a `changes` per end would have it guessing at | |
| 587 | + | /// the one that did not move. | |
| 588 | + | /// | |
| 589 | + | /// `upper_name` is a setting rather than the second argument to | |
| 590 | + | /// `Field::interval`, and it landed in quasi-router 0.101.14 for this | |
| 591 | + | /// screen: the `field` production says a kind, a name and a label, and | |
| 592 | + | /// everything else about a field is said in its body. | |
| 593 | + | shape axis_form(axis: &Axis) -> Node; | |
| 594 | + | ||
| 595 | + | region "filters-axis-{axis.key}" as Group { | |
| 596 | + | form post "/filters/axis/{axis.key}" { | |
| 597 | + | submit "Apply"; | |
| 598 | + | field Interval &axis.lower_name axis.title { | |
| 599 | + | upper_name &axis.upper_name; | |
| 600 | + | within &axis.min &axis.max; | |
| 601 | + | step &axis.step; | |
| 602 | + | width Fill; | |
| 603 | + | for symbol in axis.unit.iter() { | |
| 604 | + | unit symbol; | |
| 605 | + | } | |
| 606 | + | for &hint in axis.hint.iter() { | |
| 607 | + | hint hint; | |
| 608 | + | } | |
| 609 | + | for low in axis.lower.iter() { | |
| 610 | + | value low; | |
| 611 | + | } | |
| 612 | + | for high in axis.upper.iter() { | |
| 613 | + | upper_value high; | |
| 614 | + | } | |
| 615 | + | } | |
| 616 | + | } | |
| 617 | + | act "Clear" to post "/filters/axis/{axis.key}/clear" when axis.narrowed; | |
| 618 | + | } | |
| 619 | + | } | |
| 620 | + | ||
| 621 | + | declare! { | |
| 622 | + | /// The keys wanted, and how they are matched. | |
| 623 | + | shape keys(panel: &Panel) -> Slot; | |
| 624 | + | ||
| 625 | + | region KEYS as Band { | |
| 626 | + | section "Key"; | |
| 627 | + | ||
| 628 | + | for mode in panel.modes.iter() { | |
| 629 | + | act mode.label to post "/filters/keys/mode" with "mode" mode.value { | |
| 630 | + | disabled when mode.held; | |
| 631 | + | } | |
| 632 | + | text mode.why; | |
| 633 | + | } |
Lines truncated