| 85 |
85 |
|
//! host draws it rather than what it holds, so no finding — but a vocabulary
|
| 86 |
86 |
|
//! that grows anchoring should know this was the first place it mattered.
|
| 87 |
87 |
|
|
| 88 |
|
- |
use quasi_router::layout::{self, FieldKind, Priority, Tone, Width};
|
|
88 |
+ |
use quasi_declare::declare;
|
|
89 |
+ |
use quasi_router::layout::{FieldKind, Priority, Tone};
|
| 89 |
90 |
|
use quasi_router::{
|
| 90 |
|
- |
Act, Action, Choice, Consult, Field, Node, RegionKind, Request, Response, RouteError, Router,
|
| 91 |
|
- |
Run, Screen, Slot,
|
|
91 |
+ |
Action, Choice, Consult, Field, Node, RegionKind, Request, Response, RouteError, Router,
|
|
92 |
+ |
Screen, Slot,
|
| 92 |
93 |
|
};
|
| 93 |
94 |
|
|
| 94 |
95 |
|
use super::{Panel, Panels, Where};
|
| 289 |
290 |
|
}
|
| 290 |
291 |
|
|
| 291 |
292 |
|
/// The toolbar, as a region something else holds.
|
| 292 |
|
- |
pub fn body(state: &Panels<'_>) -> Slot {
|
| 293 |
|
- |
// A row, said as one. The members were going into the band's body, which is
|
| 294 |
|
- |
// a column, and every renderer that read it drew a toolbar one control per
|
| 295 |
|
- |
// line; quasi-webview happened to look right because a stylesheet was
|
| 296 |
|
- |
// laying the band out and the description was not saying anything.
|
| 297 |
|
- |
//
|
| 298 |
|
- |
// `Shed` rather than `Wrap` because the shipped bar drops controls when it
|
| 299 |
|
- |
// runs out of room rather than growing a second line: the panel toggles
|
| 300 |
|
- |
// already carry what each is worth (`Panel::worth`), which is the declared
|
| 301 |
|
- |
// replacement for the `screen_w < 900.0` collapse, and a fallback that kept
|
| 302 |
|
- |
// everything would leave those ranks saying nothing.
|
| 303 |
|
- |
//
|
| 304 |
|
- |
// The row is built as a row and then given to the region, which is what
|
| 305 |
|
- |
// lets the builders below take a `Run` and add to it. They used to take the
|
| 306 |
|
- |
// whole `Slot`, and what they were relying on was that this function had
|
| 307 |
|
- |
// already called `across` on it: a promise nothing in their signatures made
|
| 308 |
|
- |
// and no compiler could check. `Run::new` is the only way to make a row and
|
| 309 |
|
- |
// it takes the fallback, so a helper holding one is holding a row that has
|
| 310 |
|
- |
// already said what it does when it is tight.
|
| 311 |
|
- |
let bar = Run::new(layout::Fallback::Shed);
|
| 312 |
|
- |
let bar = here(bar, state);
|
| 313 |
|
- |
let bar = holding(bar, state);
|
| 314 |
|
- |
let bar = looking(bar, state);
|
| 315 |
|
- |
let bar = panels(bar, state);
|
| 316 |
|
- |
Slot::new(BAR, RegionKind::Band).across(bar)
|
|
293 |
+ |
/// The toolbar, read off the app.
|
|
294 |
+ |
///
|
|
295 |
+ |
/// One read for the whole bar, because the bar is one row: `Fallback::Shed`
|
|
296 |
+ |
/// ranks across everything in it, so what makes room for the search box is a
|
|
297 |
+ |
/// breadcrumb four segments away. Four regions with a run each would shed
|
|
298 |
+ |
/// independently, which is a different bar wearing the same layout.
|
|
299 |
+ |
pub(super) struct Bar {
|
|
300 |
+ |
/// Where you are.
|
|
301 |
+ |
place: Place,
|
|
302 |
+ |
/// What the reader is collecting, while they are collecting anything.
|
|
303 |
+ |
///
|
|
304 |
+ |
/// Drawn whenever the basket holds anything, in a folder as much as in a
|
|
305 |
+ |
/// walk: the basket is filled while walking but it is not the walk, and a
|
|
306 |
+ |
/// set of samples that vanished when the reader stepped into a folder would
|
|
307 |
+ |
/// be a set they could not trust. Nothing at all when it is empty, because
|
|
308 |
+ |
/// an empty basket is not a query with no results, it is no query.
|
|
309 |
+ |
basket: Option<Basket>,
|
|
310 |
+ |
/// What you are looking for.
|
|
311 |
+ |
looking: Looking,
|
|
312 |
+ |
/// What is showing.
|
|
313 |
+ |
panels: Vec<Toggle>,
|
|
314 |
+ |
/// Whether there is anything to undo.
|
|
315 |
+ |
undoable: bool,
|
| 317 |
316 |
|
}
|
| 318 |
317 |
|
|
| 319 |
|
- |
/// Where you are, which is one of three things.
|
|
318 |
+ |
/// Where you are, which is one of four things said as one shape.
|
| 320 |
319 |
|
///
|
| 321 |
|
- |
/// A trail of [`Node::Link`]s rather than acts. Both call a route; the
|
| 322 |
|
- |
/// difference is what the reader sees, and `Link`'s own header has the argument:
|
| 323 |
|
- |
/// "making every linked value a button would put a row of bevels down the first
|
| 324 |
|
- |
/// column of half a dashboard". A breadcrumb is the case that argument was
|
| 325 |
|
- |
/// written for.
|
| 326 |
|
- |
fn here(bar: Run, state: &Panels<'_>) -> Run {
|
| 327 |
|
- |
match state.bar.place() {
|
| 328 |
|
- |
Where::Folder { trail } => {
|
| 329 |
|
- |
let mut bar = bar.beside(
|
| 330 |
|
- |
Node::Link {
|
| 331 |
|
- |
text: "/".to_owned(),
|
| 332 |
|
- |
action: Action::post("/here/root"),
|
| 333 |
|
- |
},
|
| 334 |
|
- |
Priority::Essential,
|
| 335 |
|
- |
);
|
| 336 |
|
- |
for (depth, crumb) in trail.iter().enumerate() {
|
| 337 |
|
- |
// The last crumb is where you are, so it goes nowhere. Said as
|
| 338 |
|
- |
// prose rather than as a link that does nothing, which is the
|
| 339 |
|
- |
// shipped row's `selectable_label(is_last, ..)` made explicit.
|
| 340 |
|
- |
if depth + 1 == trail.len() {
|
| 341 |
|
- |
bar = bar.beside(
|
| 342 |
|
- |
Node::Text {
|
| 343 |
|
- |
text: crumb.name.clone(),
|
| 344 |
|
- |
tone: Tone::Info,
|
| 345 |
|
- |
},
|
| 346 |
|
- |
Priority::Essential,
|
| 347 |
|
- |
);
|
| 348 |
|
- |
} else {
|
| 349 |
|
- |
bar = bar.beside(
|
| 350 |
|
- |
Node::Link {
|
| 351 |
|
- |
text: crumb.name.clone(),
|
| 352 |
|
- |
action: Action::post(format!("/here/{}/{}", crumb.id, depth + 1)),
|
| 353 |
|
- |
},
|
| 354 |
|
- |
// A crumb further from where you are is the first thing
|
| 355 |
|
- |
// a narrow bar can lose: the path is still walkable
|
| 356 |
|
- |
// from the root, which never drops.
|
| 357 |
|
- |
Priority::Optional,
|
| 358 |
|
- |
);
|
| 359 |
|
- |
}
|
| 360 |
|
- |
}
|
| 361 |
|
- |
bar
|
| 362 |
|
- |
}
|
| 363 |
|
- |
// A mode rather than a place, so the way out is a control and not a
|
| 364 |
|
- |
// shorter path. The shipped breadcrumb puts Clear in the same segment
|
| 365 |
|
- |
// as the label for exactly this reason: "the mode label and the exit
|
| 366 |
|
- |
// affordance occupy one row, not two".
|
| 367 |
|
- |
Where::Collection { name } => {
|
| 368 |
|
- |
leaving(bar, format!("Collection: {name}"), "Back to browsing")
|
| 369 |
|
- |
}
|
| 370 |
|
- |
// A mode, and a trail inside it. The steps are links for the folder
|
| 371 |
|
- |
// crumbs' reason and the way out is an act for the collection's, so
|
| 372 |
|
- |
// this is the one place where both are true at once: you can go back
|
| 373 |
|
- |
// one sample, or out of walking altogether.
|
| 374 |
|
- |
Where::Similar { trail } => {
|
| 375 |
|
- |
let last = trail.len().saturating_sub(1);
|
| 376 |
|
- |
let mut bar = bar;
|
| 377 |
|
- |
for (at, step) in trail.iter().enumerate() {
|
| 378 |
|
- |
let says = if at == 0 {
|
| 379 |
|
- |
let asked = if step.near_dup {
|
| 380 |
|
- |
"Duplicates of"
|
| 381 |
|
- |
} else {
|
| 382 |
|
- |
"Similar to"
|
| 383 |
|
- |
};
|
| 384 |
|
- |
format!("{asked}: {}", step.name)
|
| 385 |
|
- |
} else {
|
| 386 |
|
- |
step.name.clone()
|
| 387 |
|
- |
};
|
| 388 |
|
- |
if at == last {
|
| 389 |
|
- |
bar = bar.beside(
|
| 390 |
|
- |
Node::Text {
|
| 391 |
|
- |
text: says,
|
| 392 |
|
- |
tone: Tone::Info,
|
| 393 |
|
- |
},
|
| 394 |
|
- |
Priority::Essential,
|
| 395 |
|
- |
);
|
| 396 |
|
- |
} else {
|
| 397 |
|
- |
bar = bar.beside(
|
| 398 |
|
- |
Node::Link {
|
| 399 |
|
- |
text: says,
|
| 400 |
|
- |
action: Action::post(format!("/here/similar/{at}")),
|
| 401 |
|
- |
},
|
| 402 |
|
- |
Priority::Optional,
|
| 403 |
|
- |
);
|
| 404 |
|
- |
}
|
| 405 |
|
- |
}
|
| 406 |
|
- |
bar.beside(
|
| 407 |
|
- |
Node::Act(Act::new("Back to browsing", Action::post("/here/leave"))),
|
| 408 |
|
- |
Priority::Essential,
|
| 409 |
|
- |
)
|
| 410 |
|
- |
.beside(
|
| 411 |
|
- |
Node::text("Results are ranked by similarity, so column sort is off."),
|
| 412 |
|
- |
Priority::Optional,
|
| 413 |
|
- |
)
|
| 414 |
|
- |
}
|
| 415 |
|
- |
// A mode with no trail inside it. You did not arrive here from one
|
| 416 |
|
- |
// sample, so there is nowhere in it to go back to; the way out is the
|
| 417 |
|
- |
// only way, and the basket itself is beside this on the same bar.
|
| 418 |
|
- |
Where::Basket { anchors } => leaving(
|
| 419 |
|
- |
bar,
|
| 420 |
|
- |
format!("Near all {anchors}"),
|
| 421 |
|
- |
"Back to browsing",
|
| 422 |
|
- |
)
|
| 423 |
|
- |
.beside(
|
| 424 |
|
- |
Node::text("Results are ranked by how near they are to the whole basket, so column sort is off."),
|
| 425 |
|
- |
Priority::Optional,
|
| 426 |
|
- |
),
|
| 427 |
|
- |
}
|
|
320 |
+ |
/// A folder is a trail you walk out of and the three modes are places you leave,
|
|
321 |
+ |
/// and that is the whole of the difference: the steps are the same steps, the
|
|
322 |
+ |
/// mode's label is a step with nowhere to go back to, and only a folder has a
|
|
323 |
+ |
/// root above it.
|
|
324 |
+ |
struct Place {
|
|
325 |
+ |
/// Whether there is a root above the trail.
|
|
326 |
+ |
rooted: bool,
|
|
327 |
+ |
/// The trail, or the mode's label as a trail of one.
|
|
328 |
+ |
steps: Vec<Step>,
|
|
329 |
+ |
/// The way out of a mode, where you are in one.
|
|
330 |
+ |
out: Option<&'static str>,
|
|
331 |
+ |
/// What a mode says about the ordering it forces, where it forces one.
|
|
332 |
+ |
ranked: Option<&'static str>,
|
| 428 |
333 |
|
}
|
| 429 |
334 |
|
|
| 430 |
|
- |
/// What you are looking *for*, which is beside where you are rather than part of
|
| 431 |
|
- |
/// it.
|
| 432 |
|
- |
///
|
| 433 |
|
- |
/// Drawn whenever the basket holds anything, in a folder as much as in a walk:
|
| 434 |
|
- |
/// the basket is filled while walking but it is not the walk, and a set of
|
| 435 |
|
- |
/// samples the reader is collecting that vanished when they stepped into a
|
| 436 |
|
- |
/// folder would be a set they could not trust.
|
| 437 |
|
- |
///
|
| 438 |
|
- |
/// Nothing at all when it is empty. An empty basket is not a query with no
|
| 439 |
|
- |
/// results, it is no query, and a control for a question nobody has asked is
|
| 440 |
|
- |
/// noise on a bar that already sheds members to fit.
|
| 441 |
|
- |
fn holding(bar: Run, state: &Panels<'_>) -> Run {
|
|
335 |
+ |
/// One step of a trail.
|
|
336 |
+ |
struct Step {
|
|
337 |
+ |
says: String,
|
|
338 |
+ |
/// Where stepping back to it is asked for.
|
|
339 |
+ |
///
|
|
340 |
+ |
/// Nothing on the last one, because the last one is where you are. Said as
|
|
341 |
+ |
/// prose rather than as a link that does nothing, which is the shipped row's
|
|
342 |
+ |
/// `selectable_label(is_last, ..)` made explicit.
|
|
343 |
+ |
back: Option<String>,
|
|
344 |
+ |
}
|
|
345 |
+ |
|
|
346 |
+ |
/// What the reader is collecting.
|
|
347 |
+ |
struct Basket {
|
|
348 |
+ |
/// Each anchor, which is a thing to stop asking about.
|
|
349 |
+ |
anchors: Vec<Anchor>,
|
|
350 |
+ |
/// What showing them reads, while the answer is not already on screen.
|
|
351 |
+ |
///
|
|
352 |
+ |
/// Not offered while it is already showing: the answer on screen is the
|
|
353 |
+ |
/// answer to this, and a control that re-asks a question already answered
|
|
354 |
+ |
/// reads as one that would ask a different one.
|
|
355 |
+ |
show: Option<String>,
|
|
356 |
+ |
}
|
|
357 |
+ |
|
|
358 |
+ |
/// One thing the reader is looking for something near.
|
|
359 |
+ |
struct Anchor {
|
|
360 |
+ |
/// Its position, which is the address that takes it out.
|
|
361 |
+ |
at: usize,
|
|
362 |
+ |
name: String,
|
|
363 |
+ |
}
|
|
364 |
+ |
|
|
365 |
+ |
/// What you are looking for.
|
|
366 |
+ |
struct Looking {
|
|
367 |
+ |
query: String,
|
|
368 |
+ |
/// Which of the two scopes is chosen.
|
|
369 |
+ |
scope: &'static str,
|
|
370 |
+ |
/// How many results, while a filter is narrowing them.
|
|
371 |
+ |
results: Option<String>,
|
|
372 |
+ |
}
|
|
373 |
+ |
|
|
374 |
+ |
/// One panel toggle, which is latching because a panel is open or shut.
|
|
375 |
+ |
struct Toggle {
|
|
376 |
+ |
/// The name its address is built from.
|
|
377 |
+ |
at: &'static str,
|
|
378 |
+ |
/// What it says, which carries the filter count on the one toggle that has
|
|
379 |
+ |
/// one.
|
|
380 |
+ |
///
|
|
381 |
+ |
/// The shipped toggle takes an `Option<usize>` badge for this and nothing
|
|
382 |
+ |
/// else does.
|
|
383 |
+ |
label: String,
|
|
384 |
+ |
/// Whether the panel is open.
|
|
385 |
+ |
on: bool,
|
|
386 |
+ |
/// What it is worth when the bar runs out of room.
|
|
387 |
+ |
///
|
|
388 |
+ |
/// The declared replacement for the shipped bar's `screen_w < 900.0`
|
|
389 |
+ |
/// collapse: each toggle says what it is worth and `Shed` reads the ranks.
|
|
390 |
+ |
worth: Priority,
|
|
391 |
+ |
/// What it looks like while it is on.
|
|
392 |
+ |
tone: Tone,
|
|
393 |
+ |
}
|
|
394 |
+ |
|
|
395 |
+ |
/// What the toolbar draws, read off the app.
|
|
396 |
+ |
pub(super) fn read(state: &Panels<'_>) -> Bar {
|
|
397 |
+ |
let searching = state.bar.searching();
|
| 442 |
398 |
|
let held = state.bar.basket();
|
| 443 |
|
- |
if held.is_empty() {
|
| 444 |
|
- |
return bar;
|
| 445 |
|
- |
}
|
| 446 |
|
- |
|
| 447 |
|
- |
let mut bar = bar.beside(
|
| 448 |
|
- |
Node::Text {
|
| 449 |
|
- |
text: "Looking for:".to_owned(),
|
| 450 |
|
- |
tone: Tone::Info,
|
| 451 |
|
- |
},
|
| 452 |
|
- |
Priority::Essential,
|
| 453 |
|
- |
);
|
| 454 |
|
- |
// Each anchor is a link that takes it out, which is the trail's shape turned
|
| 455 |
|
- |
// around: a step is somewhere to go back to, and an anchor is something to
|
| 456 |
|
- |
// stop asking about. Both are the only thing you can do to one.
|
| 457 |
|
- |
for (at, name) in held.iter().enumerate() {
|
| 458 |
|
- |
bar = bar.beside(
|
| 459 |
|
- |
Node::Link {
|
| 460 |
|
- |
text: name.clone(),
|
| 461 |
|
- |
action: Action::post(format!("/basket/{at}/remove")),
|
| 462 |
|
- |
},
|
| 463 |
|
- |
Priority::Optional,
|
| 464 |
|
- |
);
|
| 465 |
|
- |
}
|
| 466 |
|
- |
|
| 467 |
|
- |
// Not offered while it is already showing: the answer on screen is the
|
| 468 |
|
- |
// answer to this, and a control that re-asks a question already answered
|
| 469 |
|
- |
// reads as one that would ask a different one.
|
| 470 |
|
- |
if !state.bar.basket_showing() {
|
| 471 |
|
- |
bar = bar.beside(
|
| 472 |
|
- |
Node::Act(Act::new(
|
|
399 |
+ |
let showing = state.bar.showing();
|
|
400 |
+ |
Bar {
|
|
401 |
+ |
place: place(state.bar.place()),
|
|
402 |
+ |
basket: (!held.is_empty()).then(|| Basket {
|
|
403 |
+ |
anchors: held
|
|
404 |
+ |
.iter()
|
|
405 |
+ |
.enumerate()
|
|
406 |
+ |
.map(|(at, name)| Anchor {
|
|
407 |
+ |
at,
|
|
408 |
+ |
name: name.clone(),
|
|
409 |
+ |
})
|
|
410 |
+ |
.collect(),
|
|
411 |
+ |
show: (!state.bar.basket_showing()).then(|| {
|
| 473 |
412 |
|
if held.len() == 1 {
|
| 474 |
413 |
|
"Show what is near it".to_owned()
|
| 475 |
414 |
|
} else {
|
| 476 |
415 |
|
format!("Show what is near all {}", held.len())
|
| 477 |
|
- |
},
|
| 478 |
|
- |
Action::post("/basket/show"),
|
| 479 |
|
- |
)),
|
| 480 |
|
- |
Priority::Essential,
|
| 481 |
|
- |
);
|
| 482 |
|
- |
}
|
| 483 |
|
- |
|
| 484 |
|
- |
bar.beside(
|
| 485 |
|
- |
Node::Act(Act::new("Empty the basket", Action::post("/basket/clear"))),
|
| 486 |
|
- |
Priority::Optional,
|
| 487 |
|
- |
)
|
| 488 |
|
- |
}
|
| 489 |
|
- |
|
| 490 |
|
- |
/// A mode you are in, and the way out of it.
|
| 491 |
|
- |
fn leaving(bar: Run, says: String, out: &str) -> Run {
|
| 492 |
|
- |
bar.beside(
|
| 493 |
|
- |
Node::Text {
|
| 494 |
|
- |
text: says,
|
| 495 |
|
- |
tone: Tone::Info,
|
|
416 |
+ |
}
|
|
417 |
+ |
}),
|
|
418 |
+ |
}),
|
|
419 |
+ |
looking: Looking {
|
|
420 |
+ |
query: searching.query.clone(),
|
|
421 |
+ |
scope: if searching.everywhere {
|
|
422 |
+ |
"all"
|
|
423 |
+ |
} else {
|
|
424 |
+ |
"folder"
|
|
425 |
+ |
},
|
|
426 |
+ |
results: searching.filtered.then(|| searching.results.to_string()),
|
| 496 |
427 |
|
},
|
| 497 |
|
- |
Priority::Essential,
|
| 498 |
|
- |
)
|
| 499 |
|
- |
.beside(
|
| 500 |
|
- |
Node::Act(Act::new(out, Action::post("/here/leave"))),
|
| 501 |
|
- |
Priority::Essential,
|
| 502 |
|
- |
)
|
|
428 |
+ |
panels: Panel::ALL
|
|
429 |
+ |
.into_iter()
|
|
430 |
+ |
.map(|panel| {
|
|
431 |
+ |
let on = showing.contains(&panel);
|
|
432 |
+ |
Toggle {
|
|
433 |
+ |
at: panel.as_str(),
|
|
434 |
+ |
label: if panel == Panel::Filters && searching.filters > 0 {
|
|
435 |
+ |
format!("{} ({})", panel.label(), searching.filters)
|
|
436 |
+ |
} else {
|
|
437 |
+ |
panel.label().to_owned()
|
|
438 |
+ |
},
|
|
439 |
+ |
on,
|
|
440 |
+ |
worth: panel.worth(),
|
|
441 |
+ |
tone: if on { Tone::Info } else { Tone::Neutral },
|
|
442 |
+ |
}
|
|
443 |
+ |
})
|
|
444 |
+ |
.collect(),
|
|
445 |
+ |
undoable: state.bar.undoable(),
|
|
446 |
+ |
}
|
| 503 |
447 |
|
}
|
| 504 |
448 |
|
|
| 505 |
|
- |
/// What you are looking for.
|
| 506 |
|
- |
fn looking(bar: Run, state: &Panels<'_>) -> Run {
|
| 507 |
|
- |
let searching = state.bar.searching();
|
|
449 |
+ |
/// Where you are, as the one shape the four places share.
|
|
450 |
+ |
fn place(place: Where) -> Place {
|
|
451 |
+ |
match place {
|
|
452 |
+ |
Where::Folder { trail } => Place {
|
|
453 |
+ |
rooted: true,
|
|
454 |
+ |
steps: steps(trail.len(), |at| {
|
|
455 |
+ |
(
|
|
456 |
+ |
trail[at].name.clone(),
|
|
457 |
+ |
format!("/here/{}/{}", trail[at].id, at + 1),
|
|
458 |
+ |
)
|
|
459 |
+ |
}),
|
|
460 |
+ |
out: None,
|
|
461 |
+ |
ranked: None,
|
|
462 |
+ |
},
|
|
463 |
+ |
// A mode rather than a place, so the way out is a control and not a
|
|
464 |
+ |
// shorter path. The shipped breadcrumb puts Clear in the same segment as
|
|
465 |
+ |
// the label for exactly this reason: the mode label and the exit
|
|
466 |
+ |
// affordance occupy one row, not two.
|
|
467 |
+ |
Where::Collection { name } => moded(format!("Collection: {name}"), None),
|
|
468 |
+ |
// A mode, and a trail inside it. The steps are links for the folder
|
|
469 |
+ |
// crumbs' reason and the way out is an act for the collection's, so this
|
|
470 |
+ |
// is the one place both are true at once: you can go back one sample, or
|
|
471 |
+ |
// out of walking altogether.
|
|
472 |
+ |
Where::Similar { trail } => Place {
|
|
473 |
+ |
rooted: false,
|
|
474 |
+ |
steps: steps(trail.len(), |at| {
|
|
475 |
+ |
let says = if at == 0 {
|
|
476 |
+ |
let asked = if trail[at].near_dup {
|
|
477 |
+ |
"Duplicates of"
|
|
478 |
+ |
} else {
|
|
479 |
+ |
"Similar to"
|
|
480 |
+ |
};
|
|
481 |
+ |
format!("{asked}: {}", trail[at].name)
|
|
482 |
+ |
} else {
|
|
483 |
+ |
trail[at].name.clone()
|
|
484 |
+ |
};
|
|
485 |
+ |
(says, format!("/here/similar/{at}"))
|
|
486 |
+ |
}),
|
|
487 |
+ |
out: Some("Back to browsing"),
|
|
488 |
+ |
ranked: Some("Results are ranked by similarity, so column sort is off."),
|
|
489 |
+ |
},
|
|
490 |
+ |
// A mode with no trail inside it. You did not arrive here from one
|
|
491 |
+ |
// sample, so there is nowhere in it to go back to; the way out is the
|
|
492 |
+ |
// only way, and the basket itself is beside this on the same bar.
|
|
493 |
+ |
Where::Basket { anchors } => moded(
|
|
494 |
+ |
format!("Near all {anchors}"),
|
|
495 |
+ |
Some(
|
|
496 |
+ |
"Results are ranked by how near they are to the whole basket, \
|
|
497 |
+ |
so column sort is off.",
|
|
498 |
+ |
),
|
|
499 |
+ |
),
|
|
500 |
+ |
}
|
|
501 |
+ |
}
|
| 508 |
502 |
|
|
| 509 |
|
- |
let mut bar = bar
|
| 510 |
|
- |
.beside(
|
| 511 |
|
- |
Node::Field(Box::new(
|
| 512 |
|
- |
Field::new(FieldKind::Text, QUERY, "Search")
|
| 513 |
|
- |
.value(&searching.query)
|
| 514 |
|
- |
.hint("Search samples...")
|
| 515 |
|
- |
// What `trailing_width` was measuring for, said instead of
|
| 516 |
|
- |
// measured. quasi 0.17.0, settled by Max: fill is determined at
|
| 517 |
|
- |
// the description stage. It is the default, so this line changes
|
| 518 |
|
- |
// no pixel -- and it is the difference between a row that
|
| 519 |
|
- |
// happens to look right and one that says what it means.
|
| 520 |
|
- |
.width(Width::Fill)
|
| 521 |
|
- |
// A question and not a write: it narrows what the browser
|
| 522 |
|
- |
// is showing and nothing is stored. The wait is the typing
|
| 523 |
|
- |
// wait, so a query typed in one go asks once.
|
| 524 |
|
- |
.consulting(Consult::new(Action::post("/search"))),
|
| 525 |
|
- |
)),
|
| 526 |
|
- |
Priority::Essential,
|
| 527 |
|
- |
)
|
| 528 |
|
- |
.beside(
|
|
503 |
+ |
/// A mode, which is a trail of one step you cannot walk back into.
|
|
504 |
+ |
fn moded(says: String, ranked: Option<&'static str>) -> Place {
|
|
505 |
+ |
Place {
|
|
506 |
+ |
rooted: false,
|
|
507 |
+ |
steps: vec![Step { says, back: None }],
|
|
508 |
+ |
out: Some("Back to browsing"),
|
|
509 |
+ |
ranked,
|
|
510 |
+ |
}
|
|
511 |
+ |
}
|
|
512 |
+ |
|
|
513 |
+ |
/// A trail, with the last step going nowhere because it is where you are.
|
|
514 |
+ |
fn steps(count: usize, say: impl Fn(usize) -> (String, String)) -> Vec<Step> {
|
|
515 |
+ |
let last = count.saturating_sub(1);
|
|
516 |
+ |
(0..count)
|
|
517 |
+ |
.map(|at| {
|
|
518 |
+ |
let (says, back) = say(at);
|
|
519 |
+ |
Step {
|
|
520 |
+ |
says,
|
|
521 |
+ |
back: (at != last).then_some(back),
|
|
522 |
+ |
}
|
|
523 |
+ |
})
|
|
524 |
+ |
.collect()
|
|
525 |
+ |
}
|
|
526 |
+ |
|
|
527 |
+ |
declare! {
|
|
528 |
+ |
/// The toolbar, as a region something else holds.
|
|
529 |
+ |
///
|
|
530 |
+ |
/// A row, said as one. The members were going into the band's body, which is
|
|
531 |
+ |
/// a column, and every renderer that read it drew a toolbar one control per
|
|
532 |
+ |
/// line; quasi-webview happened to look right because a stylesheet was
|
|
533 |
+ |
/// laying the band out and the description was not saying anything.
|
|
534 |
+ |
pub(super) shape body(bar: &Bar) -> Slot;
|
|
535 |
+ |
|
|
536 |
+ |
region BAR as Band {
|
|
537 |
+ |
// `Shed` rather than `Wrap` because the shipped bar drops controls when
|
|
538 |
+ |
// it runs out of room rather than growing a second line, and every
|
|
539 |
+ |
// member below says what it is worth.
|
|
540 |
+ |
//
|
|
541 |
+ |
// One run and not four. The ranks are read across the whole bar, so what
|
|
542 |
+ |
// makes room for the search box is a breadcrumb four segments away; four
|
|
543 |
+ |
// runs would each shed alone and no far crumb would ever pay for a near
|
|
544 |
+ |
// control.
|
|
545 |
+ |
across Shed {
|
|
546 |
+ |
// Where you are. A trail of `Link`s rather than acts: `Link`'s own
|
|
547 |
+ |
// header has the argument, that "making every linked value a button
|
|
548 |
+ |
// would put a row of bevels down the first column of half a
|
|
549 |
+ |
// dashboard", and a breadcrumb is the case it was written for.
|
|
550 |
+ |
//
|
|
551 |
+ |
// The root never drops, so the path stays walkable from it however
|
|
552 |
+ |
// narrow the window gets.
|