| ... | ... | @@ -280,50 +280,13 @@ struct Picked { |
| 280 | 280 | build_folder: bool, |
| 281 | 281 | restart: bool, |
| 282 | 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 | | /// `listed`. Returns whether the summary was clicked. |
| 287 | | fn changes_list(ui: &mut Ui, summary: &str, changes: &update::Changes, listed: bool) -> bool { |
| 286 | /// What a newer build changes, unfolded under the popup's foot while `listed`. |
| 287 | fn changes_list(ui: &mut Ui, changes: &update::Changes, listed: bool) { |
| 288 | 288 | let theme = ui.theme.clone(); |
| 289 | 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 | 290 | // Unfolds to the titles' height, scrolling past a few dozen lines. |
| 328 | 291 | let list = ui.id("changes"); |
| 329 | 292 | let rows = ui |
| ... | ... | @@ -410,7 +373,126 @@ fn changes_list(ui: &mut Ui, summary: &str, changes: &update::Changes, listed: b |
| 410 | 373 | ui.close(); |
| 411 | 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 | 498 | /// Lays out the open popup's contents. |
| ... | ... | @@ -872,48 +954,6 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked { |
| 872 | 954 | } |
| 873 | 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 | 957 | ui.open( |
| 918 | 958 | "controls", |
| 919 | 959 | Spec { |
| ... | ... | @@ -957,6 +997,8 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked { |
| 957 | 997 | picked.show_file = button(ui, "show-file", platform::SHOW_FILE, facts.local); |
| 958 | 998 | picked.sync_now = button(ui, "sync-now", "Sync Now", !facts.offline); |
| 959 | 999 | ui.close(); |
| 1000 | rule(ui, "update-rule"); |
| 1001 | update_row(ui, facts.update, facts.changes_listed, &mut picked); |
| 960 | 1002 | picked |
| 961 | 1003 | } |
| 962 | 1004 | |
| ... | ... | @@ -1113,6 +1155,7 @@ impl State { |
| 1113 | 1155 | } |
| 1114 | 1156 | _ if picked.restart => self.restart_to_update(), |
| 1115 | 1157 | _ if picked.list_changes => self.updates.changes_listed ^= true, |
| 1158 | _ if picked.check => self.updates.check_now(), |
| 1116 | 1159 | _ => {} |
| 1117 | 1160 | } |
| 1118 | 1161 | Ok(()) |
| ... | ... | @@ -1446,6 +1489,16 @@ mod tests { |
| 1446 | 1489 | update: ready(false), |
| 1447 | 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 | 1502 | ("update-listed", || Facts { |
| 1450 | 1503 | update: ready(true), |
| 1451 | 1504 | changes_listed: true, |
| ... | ... | @@ -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 | 1561 | /// Work offline's switch, and everything below it, stays put as it turns on and off. |
| 1476 | 1562 | #[test] |
| 1477 | 1563 | fn toggling_offline_moves_nothing() { |