max / audiofiles
1 file changed,
+236 insertions,
-167 deletions
| @@ -63,11 +63,9 @@ | |||
| 63 | 63 | //! [`Candidate::confident`](super::Candidate::confident) can do exactly that; | |
| 64 | 64 | //! choosing the colour is not the description's. | |
| 65 | 65 | ||
| 66 | + | use quasi_declare::declare; | |
| 66 | 67 | use quasi_router::layout::Tone; | |
| 67 | - | use quasi_router::{ | |
| 68 | - | Act, Action, Figure, Node, Prose, RegionKind, Request, Response, Rest, RouteError, Router, Row, | |
| 69 | - | Screen, Slot, | |
| 70 | - | }; | |
| 68 | + | use quasi_router::{Action, Request, Response, Rest, RouteError, Router}; | |
| 71 | 69 | ||
| 72 | 70 | use super::{Group, Panels, Queued, Scope}; | |
| 73 | 71 | ||
| @@ -107,7 +105,7 @@ | |||
| 107 | 105 | .post("/review/dismiss", dismiss) | |
| 108 | 106 | .post("/review/rescan", rescan) | |
| 109 | 107 | .post("/review/close", close) | |
| 110 | - | .post("/review/{at}/read", read) | |
| 108 | + | .post("/review/{at}/read", open_tag) | |
| 111 | 109 | } | |
| 112 | 110 | ||
| 113 | 111 | /// `GET /review` | |
| @@ -115,15 +113,15 @@ | |||
| 115 | 113 | /// A refusal when there is nothing queued, which is the shipped screen's own | |
| 116 | 114 | /// exit said the only way a route can say it. See the module header. | |
| 117 | 115 | fn index(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 118 | - | Ok(screen(&queued(state)?).into()) | |
| 116 | + | Ok(showing(&queued(state)?)) | |
| 119 | 117 | } | |
| 120 | 118 | ||
| 121 | 119 | /// `POST /review/{at}/read` | |
| 122 | - | fn read(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> { | |
| 120 | + | fn open_tag(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> { | |
| 123 | 121 | let queued = queued(state)?; | |
| 124 | 122 | let at = group_at(&queued, &request)?; | |
| 125 | 123 | state.queue.read(at); | |
| 126 | - | Ok(screen(&queued).into()) | |
| 124 | + | Ok(showing(&queued)) | |
| 127 | 125 | } | |
| 128 | 126 | ||
| 129 | 127 | /// `POST /review/rows/{at}/tick` | |
| @@ -142,21 +140,21 @@ | |||
| 142 | 140 | return Err(RouteError::not_found("no such candidate")); | |
| 143 | 141 | } | |
| 144 | 142 | state.queue.tick(at); | |
| 145 | - | Ok(screen(&queued).into()) | |
| 143 | + | Ok(showing(&queued)) | |
| 146 | 144 | } | |
| 147 | 145 | ||
| 148 | 146 | /// `POST /review/rows/check` | |
| 149 | 147 | fn check_shown(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 150 | 148 | let queued = queued(state)?; | |
| 151 | 149 | state.queue.tick_shown(true); | |
| 152 | - | Ok(screen(&queued).into()) | |
| 150 | + | Ok(showing(&queued)) | |
| 153 | 151 | } | |
| 154 | 152 | ||
| 155 | 153 | /// `POST /review/rows/uncheck` | |
| 156 | 154 | fn uncheck_shown(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 157 | 155 | let queued = queued(state)?; | |
| 158 | 156 | state.queue.tick_shown(false); | |
| 159 | - | Ok(screen(&queued).into()) | |
| 157 | + | Ok(showing(&queued)) | |
| 160 | 158 | } | |
| 161 | 159 | ||
| 162 | 160 | /// `POST /review/accept/{scope}` | |
| @@ -180,7 +178,7 @@ | |||
| 180 | 178 | } | |
| 181 | 179 | ||
| 182 | 180 | state.queue.accept(scope); | |
| 183 | - | Ok(screen(&queued).into()) | |
| 181 | + | Ok(showing(&queued)) | |
| 184 | 182 | } | |
| 185 | 183 | ||
| 186 | 184 | /// `POST /review/accept-confident` | |
| @@ -190,7 +188,7 @@ | |||
| 190 | 188 | return Err(RouteError::not_found("nothing clears its threshold")); | |
| 191 | 189 | } | |
| 192 | 190 | state.queue.accept_confident(); | |
| 193 | - | Ok(screen(&queued).into()) | |
| 191 | + | Ok(showing(&queued)) | |
| 194 | 192 | } | |
| 195 | 193 | ||
| 196 | 194 | /// `POST /review/dismiss` | |
| @@ -198,7 +196,7 @@ | |||
| 198 | 196 | let queued = queued(state)?; | |
| 199 | 197 | open_group(&queued)?; | |
| 200 | 198 | state.queue.dismiss(); | |
| 201 | - | Ok(screen(&queued).into()) | |
| 199 | + | Ok(showing(&queued)) | |
| 202 | 200 | } | |
| 203 | 201 | ||
| 204 | 202 | /// `POST /review/rescan` | |
| @@ -212,14 +210,14 @@ | |||
| 212 | 210 | return Err(RouteError::not_found("a pass is already running")); | |
| 213 | 211 | } | |
| 214 | 212 | state.queue.rescan(); | |
| 215 | - | Ok(screen(&queued).into()) | |
| 213 | + | Ok(showing(&queued)) | |
| 216 | 214 | } | |
| 217 | 215 | ||
| 218 | 216 | /// `POST /review/close` | |
| 219 | 217 | fn close(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 220 | 218 | let queued = queued(state)?; | |
| 221 | 219 | state.queue.close(); | |
| 222 | - | Ok(screen(&queued).into()) | |
| 220 | + | Ok(showing(&queued)) | |
| 223 | 221 | } | |
| 224 | 222 | ||
| 225 | 223 | /// The queue, refusing the screen when there is nothing in it. | |
| @@ -251,154 +249,155 @@ | |||
| 251 | 249 | .ok_or_else(|| RouteError::not_found("no tag is open")) | |
| 252 | 250 | } | |
| 253 | 251 | ||
| 254 | - | /// The screen: what the pass found, the tags, and the open one. | |
| 255 | - | fn screen(queued: &Queued) -> Screen { | |
| 256 | - | Screen::sidebar_content("Review Tags") | |
| 257 | - | .with(head(queued)) | |
| 258 | - | .with( | |
| 259 | - | Slot::new("review-split", RegionKind::Split) | |
| 260 | - | .with(Node::Region(tags(queued))) | |
| 261 | - | .with(Node::Region(group(queued))), | |
| 262 | - | ) | |
| 263 | - | .with(foot(queued)) | |
| 252 | + | /// The screen, read and then described. | |
| 253 | + | pub(super) fn showing(queued: &Queued) -> Response { | |
| 254 | + | Response::from(screen(&read(queued))) | |
| 264 | 255 | } | |
| 265 | 256 | ||
| 266 | - | /// What the pass found, and the one queue-wide gesture. | |
| 267 | - | fn head(queued: &Queued) -> Slot { | |
| 268 | - | let mut band = Slot::new(HEAD, RegionKind::Band) | |
| 269 | - | .with(Node::page("Review Tags")) | |
| 270 | - | .with(Node::text(format!( | |
| 257 | + | /// What the screen draws, read off the queue. | |
| 258 | + | struct Review { | |
| 259 | + | /// What the pass found, in one line. | |
| 260 | + | found: String, | |
| 261 | + | /// The queue-wide accept, while there is anything in it. | |
| 262 | + | confident: Option<Accept>, | |
| 263 | + | /// Every tag with something waiting under it. | |
| 264 | + | groups: Vec<Listed>, | |
| 265 | + | /// The open tag, if one is open. | |
| 266 | + | open: Option<Open>, | |
| 267 | + | /// Whether a pass is running. | |
| 268 | + | rescanning: bool, | |
| 269 | + | /// What the last accept did, if it has said anything. | |
| 270 | + | said: Option<String>, | |
| 271 | + | /// How many suggestions across the whole queue clear their threshold. | |
| 272 | + | confidently: String, | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | /// The closest thing left to the auto-apply this layer stopped doing, and still | |
| 276 | + | /// a decision made with the count on screen. | |
| 277 | + | struct Accept { | |
| 278 | + | /// What the button reads. | |
| 279 | + | label: String, | |
| 280 | + | /// What it says before it runs. | |
| 281 | + | confirm: String, | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | /// One tag in the list this screen navigates by. | |
| 285 | + | struct Listed { | |
| 286 | + | /// The tag itself. | |
| 287 | + | tag: String, | |
| 288 | + | /// How much is waiting under it, on the line beneath. | |
| 289 | + | waiting: String, | |
| 290 | + | /// Where it sits, which is what the address carries. | |
| 291 | + | at: usize, | |
| 292 | + | /// Whether it is the one open. | |
| 293 | + | current: bool, | |
| 294 | + | } | |
| 295 | + | ||
| 296 | + | /// The open tag: what it would do, and to what. | |
| 297 | + | struct Open { | |
| 298 | + | /// The tag itself. | |
| 299 | + | tag: String, | |
| 300 | + | /// How many samples would get it. | |
| 301 | + | said: String, | |
| 302 | + | /// The accept over the whole group. | |
| 303 | + | all: String, | |
| 304 | + | /// The accept over what clears the threshold, while that is a real subset. | |
| 305 | + | /// | |
| 306 | + | /// Otherwise it is a second button that does what the first one does, which | |
| 307 | + | /// is the shipped screen's own gate. | |
| 308 | + | confident: Option<String>, | |
| 309 | + | /// The accept over what is ticked, while anything is. | |
| 310 | + | checked: Option<String>, | |
| 311 | + | /// What dismissing the tag says before it runs. | |
| 312 | + | dismiss: String, | |
| 313 | + | /// Whether anything is ticked, which is what offers the unticking. | |
| 314 | + | ticked: bool, | |
| 315 | + | /// The candidates whose names have been resolved. | |
| 316 | + | shown: Vec<Waiting>, | |
| 317 | + | /// Whether none of them are. | |
| 318 | + | bare: bool, | |
| 319 | + | /// How many candidates there are in all. | |
| 320 | + | /// | |
| 321 | + | /// See the module header: a window over rows that were never fetched, with | |
| 322 | + | /// no way to widen it because the buttons act on the whole group. | |
| 323 | + | candidates: usize, | |
| 324 | + | /// How many of them are drawn. | |
| 325 | + | drawn: usize, | |
| 326 | + | /// Whether the second number is smaller than the first. | |
| 327 | + | windowed: bool, | |
| 328 | + | } | |
| 329 | + | ||
| 330 | + | /// One candidate under the open tag. | |
| 331 | + | struct Waiting { | |
| 332 | + | /// What the sample is called, or its hash until the name is resolved. | |
| 333 | + | name: String, | |
| 334 | + | /// The score, and whether it clears the threshold. | |
| 335 | + | /// | |
| 336 | + | /// The band as a fact rather than as a contrast level. A renderer is free to | |
| 337 | + | /// draw it the way the shipped row does. | |
| 338 | + | meta: String, | |
| 339 | + | /// Whether it is ticked. | |
| 340 | + | accepted: bool, | |
| 341 | + | /// Where it sits, which is what the tick carries. | |
| 342 | + | at: usize, | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | /// What the screen draws, read off the queue. | |
| 346 | + | fn read(queued: &Queued) -> Review { | |
| 347 | + | let total = total_of(queued); | |
| 348 | + | Review { | |
| 349 | + | found: format!( | |
| 271 | 350 | "{} suggestion{} across {} tag{} \u{b7} {} of {} sample{}", | |
| 272 | - | total_of(queued), | |
| 273 | - | plural(total_of(queued)), | |
| 351 | + | total, | |
| 352 | + | plural(total), | |
| 274 | 353 | queued.groups.len(), | |
| 275 | 354 | plural(queued.groups.len()), | |
| 276 | 355 | queued.suggested, | |
| 277 | 356 | queued.considered, | |
| 278 | 357 | plural(queued.considered), | |
| 279 | - | ))) | |
| 280 | - | // The promise the whole screen rests on, and it is a fact about the | |
| 281 | - | // queue rather than about any control, so it is prose. | |
| 282 | - | .with(Node::text("Nothing is applied until you accept it.")); | |
| 283 | - | ||
| 284 | - | // The closest thing left to the auto-apply this layer stopped doing, and | |
| 285 | - | // still a decision made with the count on screen. Offered only when there is | |
| 286 | - | // something in it, which is the shipped button's own gate. | |
| 287 | - | if queued.confident > 0 { | |
| 288 | - | band = band.with(Node::Act( | |
| 289 | - | Act::new( | |
| 290 | - | format!("Accept {} confident", queued.confident), | |
| 291 | - | Action::post("/review/accept-confident"), | |
| 292 | - | ) | |
| 293 | - | .confirm(format!( | |
| 358 | + | ), | |
| 359 | + | confident: (queued.confident > 0).then(|| Accept { | |
| 360 | + | label: format!("Accept {} confident", queued.confident), | |
| 361 | + | confirm: format!( | |
| 294 | 362 | "{} suggestions above their tags' thresholds will be applied across every tag. \ | |
| 295 | 363 | Accept?", | |
| 296 | 364 | queued.confident | |
| 297 | - | )), | |
| 298 | - | )); | |
| 299 | - | } | |
| 300 | - | ||
| 301 | - | band | |
| 302 | - | } | |
| 303 | - | ||
| 304 | - | /// The tags, which is what this screen navigates by. | |
| 305 | - | fn tags(queued: &Queued) -> Slot { | |
| 306 | - | Slot::new(TAGS, RegionKind::Pane) | |
| 307 | - | .with(Node::section("Tags")) | |
| 308 | - | .with(Node::list(queued.groups.iter().enumerate().map( | |
| 309 | - | |(at, group)| { | |
| 310 | - | let mut row = Row::new(group.tag.clone()) | |
| 311 | - | .secondary(Prose::Text(format!( | |
| 312 | - | "{} suggestion{}{}", | |
| 313 | - | group.candidates, | |
| 314 | - | plural(group.candidates), | |
| 315 | - | if group.confident > 0 { | |
| 316 | - | format!(", {} confident", group.confident) | |
| 317 | - | } else { | |
| 318 | - | String::new() | |
| 319 | - | } | |
| 320 | - | ))) | |
| 321 | - | .activate(Action::post(format!("/review/{at}/read"))); | |
| 322 | - | if at == queued.at { | |
| 323 | - | row.current = true; | |
| 324 | - | } | |
| 325 | - | row | |
| 326 | - | }, | |
| 327 | - | ))) | |
| 328 | - | } | |
| 329 | - | ||
| 330 | - | /// The open tag: what it would do, and to what. | |
| 331 | - | fn group(queued: &Queued) -> Slot { | |
| 332 | - | let pane = Slot::new(GROUP, RegionKind::Pane); | |
| 333 | - | let Some(open) = queued.groups.get(queued.at) else { | |
| 334 | - | return pane.with(Node::empty("Choose a tag to review.")); | |
| 335 | - | }; | |
| 336 | - | ||
| 337 | - | let mut pane = pane | |
| 338 | - | .with(Node::section(open.tag.clone())) | |
| 339 | - | .with(Node::text(format!( | |
| 340 | - | "{} sample{} would get this tag.", | |
| 341 | - | open.candidates, | |
| 342 | - | plural(open.candidates) | |
| 343 | - | ))) | |
| 344 | - | .with(Node::Act(Act::new( | |
| 345 | - | format!("Accept all {}", open.candidates), | |
| 346 | - | Action::post("/review/accept/all"), | |
| 347 | - | ))); | |
| 348 | - | ||
| 349 | - | // Only when it is a real subset. Otherwise it is a second button that does | |
| 350 | - | // what the first one does, which is the shipped screen's own gate. | |
| 351 | - | if open.confident > 0 && open.confident < open.candidates { | |
| 352 | - | pane = pane.with(Node::Act(Act::new( | |
| 353 | - | format!("Accept {} confident", open.confident), | |
| 354 | - | Action::post("/review/accept/confident"), | |
| 355 | - | ))); | |
| 356 | - | } | |
| 357 | - | if open.checked > 0 { | |
| 358 | - | pane = pane.with(Node::Act(Act::new( | |
| 359 | - | format!("Accept {} checked", open.checked), | |
| 360 | - | Action::post("/review/accept/checked"), | |
| 361 | - | ))); | |
| 362 | - | } | |
| 363 | - | ||
| 364 | - | pane = pane | |
| 365 | - | .with(Node::Act( | |
| 366 | - | Act::new("Dismiss tag", Action::post("/review/dismiss")) | |
| 367 | - | .tone(Tone::Danger) | |
| 368 | - | .confirm(format!( | |
| 369 | - | "\"{}\" and its {} suggestions will be dropped from the queue. Dismiss?", | |
| 370 | - | open.tag, open.candidates | |
| 371 | - | )), | |
| 372 | - | )) | |
| 373 | - | // Ticking is bounded to what is drawn, and that is deliberate rather | |
| 374 | - | // than incidental: ticking 44,000 invisible boxes would make "Accept | |
| 375 | - | // checked" silently mean "accept everything", which is the distinction | |
| 376 | - | // the three scopes exist to keep. | |
| 377 | - | .with(Node::Act(Act::new( | |
| 378 | - | "Check all shown", | |
| 379 | - | Action::post("/review/rows/check"), | |
| 380 | - | ))); | |
| 381 | - | if open.checked > 0 { | |
| 382 | - | pane = pane.with(Node::Act(Act::new( | |
| 383 | - | "Uncheck all shown", | |
| 384 | - | Action::post("/review/rows/uncheck"), | |
| 385 | - | ))); | |
| 386 | - | } | |
| 387 | - | ||
| 388 | - | if queued.shown.is_empty() { | |
| 389 | - | return pane.with(Node::empty("Nothing is waiting under this tag.")); | |
| 390 | - | } | |
| 391 | - | ||
| 392 | - | pane.with(Node::List { | |
| 393 | - | rows: queued | |
| 394 | - | .shown | |
| 365 | + | ), | |
| 366 | + | }), | |
| 367 | + | groups: queued | |
| 368 | + | .groups | |
| 395 | 369 | .iter() | |
| 396 | 370 | .enumerate() | |
| 397 | - | .map(|(at, candidate)| { | |
| 398 | - | Row::new(candidate.name.clone()) | |
| 399 | - | // The band as a fact rather than as a contrast level. A | |
| 400 | - | // renderer is free to draw it the way the shipped row does. | |
| 401 | - | .meta(format!( | |
| 371 | + | .map(|(at, group)| Listed { | |
| 372 | + | tag: group.tag.clone(), | |
| 373 | + | waiting: waiting(group), | |
| 374 | + | at, | |
| 375 | + | current: at == queued.at, | |
| 376 | + | }) | |
| 377 | + | .collect(), | |
| 378 | + | open: queued.groups.get(queued.at).map(|open| Open { | |
| 379 | + | tag: open.tag.clone(), | |
| 380 | + | said: format!( | |
| 381 | + | "{} sample{} would get this tag.", | |
| 382 | + | open.candidates, | |
| 383 | + | plural(open.candidates) | |
| 384 | + | ), | |
| 385 | + | all: format!("Accept all {}", open.candidates), | |
| 386 | + | confident: (open.confident > 0 && open.confident < open.candidates) | |
| 387 | + | .then(|| format!("Accept {} confident", open.confident)), | |
| 388 | + | checked: (open.checked > 0).then(|| format!("Accept {} checked", open.checked)), | |
| 389 | + | dismiss: format!( | |
| 390 | + | "\"{}\" and its {} suggestions will be dropped from the queue. Dismiss?", | |
| 391 | + | open.tag, open.candidates | |
| 392 | + | ), | |
| 393 | + | ticked: open.checked > 0, | |
| 394 | + | shown: queued | |
| 395 | + | .shown | |
| 396 | + | .iter() | |
| 397 | + | .enumerate() | |
| 398 | + | .map(|(at, candidate)| Waiting { | |
| 399 | + | name: candidate.name.clone(), | |
| 400 | + | meta: format!( | |
| 402 | 401 | "{:.0}%{}", | |
| 403 | 402 | candidate.score * 100.0, | |
| 404 | 403 | if candidate.confident { | |
| @@ -406,43 +405,155 @@ | |||
| 406 | 405 | } else { | |
| 407 | 406 | "" | |
| 408 | 407 | } | |
| 409 | - | )) | |
| 410 | - | .toggling( | |
| 411 | - | candidate.accepted, | |
| 412 | - | Action::post(format!("/review/rows/{at}/tick")), | |
| 413 | - | ) | |
| 414 | - | }) | |
| 415 | - | .collect(), | |
| 416 | - | // See the module header: a window over rows that were never fetched, | |
| 417 | - | // with no way to widen it because the buttons act on the whole group. | |
| 418 | - | more: (open.candidates > queued.shown.len()) | |
| 419 | - | .then(|| Rest::page(0, queued.shown.len()).of(open.candidates)), | |
| 420 | - | }) | |
| 408 | + | ), | |
| 409 | + | accepted: candidate.accepted, | |
| 410 | + | at, | |
| 411 | + | }) | |
| 412 | + | .collect(), | |
| 413 | + | bare: queued.shown.is_empty(), | |
| 414 | + | candidates: open.candidates, | |
| 415 | + | drawn: queued.shown.len(), | |
| 416 | + | windowed: open.candidates > queued.shown.len(), | |
| 417 | + | }), | |
| 418 | + | rescanning: queued.rescanning, | |
| 419 | + | said: queued.said.clone(), | |
| 420 | + | confidently: queued.confident.to_string(), | |
| 421 | + | } | |
| 421 | 422 | } | |
| 422 | 423 | ||
| 423 | - | /// The way out, and the way to start again. | |
| 424 | - | fn foot(queued: &Queued) -> Slot { | |
| 425 | - | let mut band = Slot::new(FOOT, RegionKind::Band) | |
| 426 | - | .with(Node::Act(Act::new("Close", Action::post("/review/close")))); | |
| 424 | + | /// How much is waiting under one tag, on the line under its name. | |
| 425 | + | fn waiting(group: &Group) -> String { | |
| 426 | + | format!( | |
| 427 | + | "{} suggestion{}{}", | |
| 428 | + | group.candidates, | |
| 429 | + | plural(group.candidates), | |
| 430 | + | if group.confident > 0 { | |
| 431 | + | format!(", {} confident", group.confident) | |
| 432 | + | } else { | |
| 433 | + | String::new() | |
| 434 | + | } | |
| 435 | + | ) | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | declare! { | |
| 439 | + | /// The screen: what the pass found, the tags, and the open one. | |
| 440 | + | shape screen(review: &Review) -> Screen; | |
| 441 | + | ||
| 442 | + | screen sidebar_content "Review Tags" { | |
| 443 | + | include head(review); | |
| 444 | + | region "review-split" as Split { | |
| 445 | + | include tags(review); | |
| 446 | + | include group(review); | |
| 447 | + | } | |
| 448 | + | include foot(review); | |
| 449 | + | } | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | declare! { | |
| 453 | + | /// What the pass found, and the one queue-wide gesture. | |
| 454 | + | shape head(review: &Review) -> Slot; | |
| 455 | + | ||
| 456 | + | region HEAD as Band { | |
| 457 | + | page "Review Tags"; | |
| 458 | + | text &review.found; | |
| 459 | + | ||
| 460 | + | // The promise the whole screen rests on, and it is a fact about the | |
| 461 | + | // queue rather than about any control, so it is prose. | |
| 462 | + | text "Nothing is applied until you accept it."; | |
| 463 | + | ||
| 464 | + | // The closest thing left to the auto-apply this layer stopped doing, | |
| 465 | + | // and still a decision made with the count on screen. Offered only when | |
| 466 | + | // there is something in it, which is the shipped button's own gate. | |
| 467 | + | for accept in review.confident.iter() { | |
| 468 | + | act &accept.label to post "/review/accept-confident" { | |
| 469 | + | confirm &accept.confirm; | |
| 470 | + | } | |
| 471 | + | } | |
| 472 | + | } | |
| 473 | + | } | |
| 474 | + | ||
| 475 | + | declare! { | |
| 476 | + | /// The tags, which is what this screen navigates by. | |
| 477 | + | shape tags(review: &Review) -> Slot; | |
| 478 | + | ||
| 479 | + | region TAGS as Pane { | |
| 480 | + | section "Tags"; | |
| 481 | + | list { | |
| 482 | + | for listed in review.groups.iter() { | |
| 483 | + | row &listed.tag { | |
| 484 | + | secondary &listed.waiting; | |
| 485 | + | current listed.current; | |
| 486 | + | activate to post "/review/{listed.at}/read"; | |
| 487 | + | } | |
| 488 | + | } | |
| 489 | + | } | |
| 490 | + | } | |
| 491 | + | } | |
| 492 | + | ||
| 493 | + | declare! { | |
| 494 | + | /// The open tag: what it would do, and to what. | |
| 495 | + | shape group(review: &Review) -> Slot; |
Lines truncated