| ... | @@ -280,50 +280,13 @@ struct Picked { | ... | @@ -280,50 +280,13 @@ struct Picked { |
| 280 | build_folder: bool, | 280 | build_folder: bool, |
| 281 | restart: bool, | 281 | restart: bool, |
| 282 | list_changes: bool, | 282 | list_changes: bool, |
| | 283 | check: bool, |
| 283 | } | 284 | } |
| 284 | | 285 | |
| 285 | /// What a newer build changes: `summary`, which unfolds the titles under their kinds when | 286 | /// What a newer build changes, unfolded under the popup's foot while `listed`. |
| 286 | /// `listed`. Returns whether the summary was clicked. | 287 | fn changes_list(ui: &mut Ui, changes: &update::Changes, listed: bool) { |
| 287 | fn changes_list(ui: &mut Ui, summary: &str, changes: &update::Changes, listed: bool) -> bool { | | |
| 288 | let theme = ui.theme.clone(); | 288 | let theme = ui.theme.clone(); |
| 289 | let line = theme.font_size * 1.6; | 289 | let line = theme.font_size * 1.6; |
| 290 | let toggle = ui.open( | | |
| 291 | "summary", | | |
| 292 | Spec { | | |
| 293 | flags: Flags::CLICKABLE, | | |
| 294 | size: [fill(), px(line)], | | |
| 295 | gap: 4.0, | | |
| 296 | role: Some(accesskit::Role::Button), | | |
| 297 | ..Spec::default() | | |
| 298 | }, | | |
| 299 | ); | | |
| 300 | if let Some(node) = ui.access(toggle) { | | |
| 301 | node.set_label(summary); | | |
| 302 | node.set_expanded(listed); | | |
| 303 | } | | |
| 304 | ui.leaf( | | |
| 305 | "label", | | |
| 306 | Spec { | | |
| 307 | size: [fit(), px(line)], | | |
| 308 | text: Some(summary), | | |
| 309 | overflow: Overflow::Ellipsis, | | |
| 310 | ..Spec::default() | | |
| 311 | }, | | |
| 312 | ); | | |
| 313 | ui.leaf( | | |
| 314 | "chevron", | | |
| 315 | Spec { | | |
| 316 | size: [fit(), px(line)], | | |
| 317 | icon: Some(if listed { | | |
| 318 | art::CHEVRON_UP | | |
| 319 | } else { | | |
| 320 | ui::shell::CHEVRON | | |
| 321 | }), | | |
| 322 | color: Some(theme.text_dim), | | |
| 323 | ..Spec::default() | | |
| 324 | }, | | |
| 325 | ); | | |
| 326 | ui.close(); | | |
| 327 | // Unfolds to the titles' height, scrolling past a few dozen lines. | 290 | // Unfolds to the titles' height, scrolling past a few dozen lines. |
| 328 | let list = ui.id("changes"); | 291 | let list = ui.id("changes"); |
| 329 | let rows = ui | 292 | let rows = ui |
| ... | @@ -410,7 +373,126 @@ fn changes_list(ui: &mut Ui, summary: &str, changes: &update::Changes, listed: b | ... | @@ -410,7 +373,126 @@ fn changes_list(ui: &mut Ui, summary: &str, changes: &update::Changes, listed: b |
| 410 | ui.close(); | 373 | ui.close(); |
| 411 | ui.close(); | 374 | ui.close(); |
| 412 | } | 375 | } |
| 413 | ui.signal(toggle).clicked | 376 | } |
| | 377 | |
| | 378 | /// The popup's foot, one row tall whatever the update's state: Check for Updates, or the |
| | 379 | /// newer build on its way or here, which unfolds what it changes, and what installs it. |
| | 380 | fn update_row(ui: &mut Ui, update: &update::Status, listed: bool, picked: &mut Picked) { |
| | 381 | let theme = ui.theme.clone(); |
| | 382 | let height = theme.font_size * 2.0; |
| | 383 | let changes = match update { |
| | 384 | update::Status::Ready(_, _, changes) | update::Status::Available(_, changes) => { |
| | 385 | Some(changes).filter(|changes| !changes.list.is_empty()) |
| | 386 | } |
| | 387 | _ => None, |
| | 388 | }; |
| | 389 | ui.open( |
| | 390 | "update", |
| | 391 | Spec { |
| | 392 | size: [fill(), px(height)], |
| | 393 | gap: 8.0, |
| | 394 | ..Spec::default() |
| | 395 | }, |
| | 396 | ); |
| | 397 | let note = match update { |
| | 398 | update::Status::Downloading(_) => Some("Downloading update…"), |
| | 399 | update::Status::Ready(..) => Some("Update ready"), |
| | 400 | update::Status::Available(..) => Some("Update available"), |
| | 401 | _ => None, |
| | 402 | }; |
| | 403 | match note { |
| | 404 | None => { |
| | 405 | picked.check = ui |
| | 406 | .leaf( |
| | 407 | "check", |
| | 408 | Spec { |
| | 409 | flags: Flags::CLICKABLE, |
| | 410 | size: [fit(), px(height)], |
| | 411 | icon: Some(art::UPDATE), |
| | 412 | text: Some("Check for Updates"), |
| | 413 | color: Some(theme.accent), |
| | 414 | role: Some(accesskit::Role::Link), |
| | 415 | ..Spec::default() |
| | 416 | }, |
| | 417 | ) |
| | 418 | .clicked; |
| | 419 | } |
| | 420 | Some(note) => { |
| | 421 | let toggle = ui.open( |
| | 422 | "note", |
| | 423 | Spec { |
| | 424 | flags: if changes.is_some() { |
| | 425 | Flags::CLICKABLE |
| | 426 | } else { |
| | 427 | Flags::default() |
| | 428 | }, |
| | 429 | size: [fit(), px(height)], |
| | 430 | gap: 4.0, |
| | 431 | role: changes.is_some().then_some(accesskit::Role::Button), |
| | 432 | ..Spec::default() |
| | 433 | }, |
| | 434 | ); |
| | 435 | // The version and what it brings, which the row leaves to its tooltip. |
| | 436 | let version = |
| | 437 | update_note(update).map_or_else(String::new, |note| format!("Snowbound {note}")); |
| | 438 | let summary = changes.and_then(update::summary); |
| | 439 | if let Some(node) = ui.access(toggle) { |
| | 440 | node.set_label(note); |
| | 441 | node.set_description(version.as_str()); |
| | 442 | if changes.is_some() { |
| | 443 | node.set_expanded(listed); |
| | 444 | } |
| | 445 | } |
| | 446 | ui.leaf( |
| | 447 | "label", |
| | 448 | Spec { |
| | 449 | size: [fit(), px(height)], |
| | 450 | text: Some(note), |
| | 451 | icon: Some(art::UPDATE), |
| | 452 | overflow: Overflow::Ellipsis, |
| | 453 | ..Spec::default() |
| | 454 | }, |
| | 455 | ); |
| | 456 | if changes.is_some() { |
| | 457 | ui.leaf( |
| | 458 | "chevron", |
| | 459 | Spec { |
| | 460 | size: [fit(), px(height)], |
| | 461 | icon: Some(if listed { |
| | 462 | art::CHEVRON_UP |
| | 463 | } else { |
| | 464 | ui::shell::CHEVRON |
| | 465 | }), |
| | 466 | color: Some(theme.text_dim), |
| | 467 | ..Spec::default() |
| | 468 | }, |
| | 469 | ); |
| | 470 | } |
| | 471 | ui.close(); |
| | 472 | ui::popup::tooltip(ui, &version, "", summary.as_deref()); |
| | 473 | picked.list_changes = changes.is_some() && ui.signal(toggle).clicked; |
| | 474 | } |
| | 475 | } |
| | 476 | ui.leaf( |
| | 477 | "space", |
| | 478 | Spec { |
| | 479 | size: [fill(), px(1.0)], |
| | 480 | ..Spec::default() |
| | 481 | }, |
| | 482 | ); |
| | 483 | match update { |
| | 484 | update::Status::Ready(..) => { |
| | 485 | picked.restart = ui::button(ui, "restart", "Restart to Update").clicked; |
| | 486 | } |
| | 487 | update::Status::Available(..) => { |
| | 488 | picked.build_folder = ui::button(ui, "build-folder", "Build Folder").clicked; |
| | 489 | } |
| | 490 | _ => {} |
| | 491 | } |
| | 492 | ui.close(); |
| | 493 | if let Some(changes) = changes { |
| | 494 | changes_list(ui, changes, listed); |
| | 495 | } |
| 414 | } | 496 | } |
| 415 | | 497 | |
| 416 | /// Lays out the open popup's contents. | 498 | /// Lays out the open popup's contents. |
| ... | @@ -872,48 +954,6 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked { | ... | @@ -872,48 +954,6 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked { |
| 872 | } | 954 | } |
| 873 | ui.close(); | 955 | ui.close(); |
| 874 | | 956 | |
| 875 | if let Some(note) = update_note(facts.update) { | | |
| 876 | rule(ui, "update-rule"); | | |
| 877 | ui.open( | | |
| 878 | "update", | | |
| 879 | Spec { | | |
| 880 | axis: Axis::Y, | | |
| 881 | size: [fill(), children()], | | |
| 882 | gap: 4.0, | | |
| 883 | ..Spec::default() | | |
| 884 | }, | | |
| 885 | ); | | |
| 886 | text(ui, "note", &format!("Snowbound {note}"), theme.text); | | |
| 887 | if let update::Status::Ready(_, _, changes) | update::Status::Available(_, changes) = | | |
| 888 | facts.update | | |
| 889 | && let Some(summary) = update::summary(changes) | | |
| 890 | { | | |
| 891 | picked.list_changes = changes_list(ui, &summary, changes, facts.changes_listed); | | |
| 892 | } | | |
| 893 | ui.open( | | |
| 894 | "actions", | | |
| 895 | Spec { | | |
| 896 | size: [fill(), children()], | | |
| 897 | pad: [0.0, 4.0], | | |
| 898 | gap: 8.0, | | |
| 899 | ..Spec::default() | | |
| 900 | }, | | |
| 901 | ); | | |
| 902 | ui.leaf( | | |
| 903 | "space", | | |
| 904 | Spec { | | |
| 905 | size: [fill(), px(1.0)], | | |
| 906 | ..Spec::default() | | |
| 907 | }, | | |
| 908 | ); | | |
| 909 | picked.build_folder = ui::button(ui, "build-folder", "Build Folder").clicked; | | |
| 910 | if matches!(facts.update, update::Status::Ready(..)) { | | |
| 911 | picked.restart = ui::button(ui, "restart", "Restart to Update").clicked; | | |
| 912 | } | | |
| 913 | ui.close(); | | |
| 914 | ui.close(); | | |
| 915 | } | | |
| 916 | | | |
| 917 | ui.open( | 957 | ui.open( |
| 918 | "controls", | 958 | "controls", |
| 919 | Spec { | 959 | Spec { |
| ... | @@ -957,6 +997,8 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked { | ... | @@ -957,6 +997,8 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked { |
| 957 | picked.show_file = button(ui, "show-file", platform::SHOW_FILE, facts.local); | 997 | picked.show_file = button(ui, "show-file", platform::SHOW_FILE, facts.local); |
| 958 | picked.sync_now = button(ui, "sync-now", "Sync Now", !facts.offline); | 998 | picked.sync_now = button(ui, "sync-now", "Sync Now", !facts.offline); |
| 959 | ui.close(); | 999 | ui.close(); |
| | 1000 | rule(ui, "update-rule"); |
| | 1001 | update_row(ui, facts.update, facts.changes_listed, &mut picked); |
| 960 | picked | 1002 | picked |
| 961 | } | 1003 | } |
| 962 | | 1004 | |
| ... | @@ -1113,6 +1155,7 @@ impl State { | ... | @@ -1113,6 +1155,7 @@ impl State { |
| 1113 | } | 1155 | } |
| 1114 | _ if picked.restart => self.restart_to_update(), | 1156 | _ if picked.restart => self.restart_to_update(), |
| 1115 | _ if picked.list_changes => self.updates.changes_listed ^= true, | 1157 | _ if picked.list_changes => self.updates.changes_listed ^= true, |
| | 1158 | _ if picked.check => self.updates.check_now(), |
| 1116 | _ => {} | 1159 | _ => {} |
| 1117 | } | 1160 | } |
| 1118 | Ok(()) | 1161 | Ok(()) |
| ... | @@ -1446,6 +1489,16 @@ mod tests { | ... | @@ -1446,6 +1489,16 @@ mod tests { |
| 1446 | update: ready(false), | 1489 | update: ready(false), |
| 1447 | ..facts(sections(|_| status(true, 0, None))) | 1490 | ..facts(sections(|_| status(true, 0, None))) |
| 1448 | }), | 1491 | }), |
| | 1492 | ("downloading", || Facts { |
| | 1493 | update: Box::leak(Box::new(update::Status::Downloading( |
| | 1494 | update::Version::parse("2026-10-01-r5").unwrap(), |
| | 1495 | ))), |
| | 1496 | ..facts(sections(|_| status(true, 0, None))) |
| | 1497 | }), |
| | 1498 | ("available", || Facts { |
| | 1499 | update: available(), |
| | 1500 | ..facts(sections(|_| status(true, 0, None))) |
| | 1501 | }), |
| 1449 | ("update-listed", || Facts { | 1502 | ("update-listed", || Facts { |
| 1450 | update: ready(true), | 1503 | update: ready(true), |
| 1451 | changes_listed: true, | 1504 | changes_listed: true, |
| ... | @@ -1472,6 +1525,39 @@ mod tests { | ... | @@ -1472,6 +1525,39 @@ mod tests { |
| 1472 | ))) | 1525 | ))) |
| 1473 | } | 1526 | } |
| 1474 | | 1527 | |
| | 1528 | /// The build `ready` brings, newer than one that can't install it itself. |
| | 1529 | fn available() -> &'static update::Status { |
| | 1530 | let update::Status::Ready(version, _, changes) = ready(false) else { |
| | 1531 | unreachable!() |
| | 1532 | }; |
| | 1533 | Box::leak(Box::new(update::Status::Available( |
| | 1534 | version.clone(), |
| | 1535 | changes.clone(), |
| | 1536 | ))) |
| | 1537 | } |
| | 1538 | |
| | 1539 | /// The popup stands as tall with an update on its way, here or none: its foot reads |
| | 1540 | /// Check for Updates or holds the update in the same row. |
| | 1541 | #[test] |
| | 1542 | fn an_update_keeps_the_popup_its_height() { |
| | 1543 | let heights: Vec<(&str, f32)> = states() |
| | 1544 | .into_iter() |
| | 1545 | .filter(|(name, _)| ["up-to-date", "downloading", "available", "update"].contains(name)) |
| | 1546 | .map(|(name, made)| { |
| | 1547 | let mut ui = ui(Appearance::Light); |
| | 1548 | let mut now = Instant::now(); |
| | 1549 | settle(&mut ui, &mut now, &made()); |
| | 1550 | let [_, top, _, bottom] = ui.rect(id()).unwrap(); |
| | 1551 | (name, bottom - top) |
| | 1552 | }) |
| | 1553 | .collect(); |
| | 1554 | assert_eq!(heights.len(), 4); |
| | 1555 | assert!( |
| | 1556 | heights.windows(2).all(|pair| pair[0].1 == pair[1].1), |
| | 1557 | "{heights:?}" |
| | 1558 | ); |
| | 1559 | } |
| | 1560 | |
| 1475 | /// Work offline's switch, and everything below it, stays put as it turns on and off. | 1561 | /// Work offline's switch, and everything below it, stays put as it turns on and off. |
| 1476 | #[test] | 1562 | #[test] |
| 1477 | fn toggling_offline_moves_nothing() { | 1563 | fn toggling_offline_moves_nothing() { |