diff --git a/Where/WhereUI/AGENTS.md b/Where/WhereUI/AGENTS.md index a26ed3b27..69ff3cd98 100644 --- a/Where/WhereUI/AGENTS.md +++ b/Where/WhereUI/AGENTS.md @@ -85,6 +85,9 @@ Layering, localization, preview, and testing conventions live in the feature Project Locations-card GPS points through the cache's shared `RegionArtworkProjection`. Never project, simplify, or spatially reduce artwork in a card's `body`. +- Keep the Locations background on the selected year’s complete visited-region set, + in catalog order independent of card rank. Omit the catch-all Other entry from + the print. Use the injected outline cache. - Keep the Elsewhere summary count and artwork on the same secondary-region input. Use its own stylesheet tokens and the shared outline renderers. - Keep Locations-card points on `YearReportModel`'s loaded diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index cc68f4d47..6e9a4607f 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -427,6 +427,19 @@ interactive viewport. Flyover's appearance, device, Dynamic Type, contrast, layout-direction, and bold-text choices are session-only and apply only to registered content. +### Locations background + +The Locations root screen uses a stationary monochrome pattern of all regions +visited in the selected year, including manually recorded visits and Elsewhere. +The catch-all Other entry is omitted because it has no geographic outline. +Catalog order keeps the pattern stable when card rankings change. Planned stays +do not contribute. The shared outline cache supplies the region paths. Full-view +rosette rings sit behind staggered region outlines with a half-cell row offset. Slender +regions receive a bounded size adjustment to balance their visual weight. The +monochrome ink uses faint strokes without a shadow or blur. Appearance belongs +to `WhereStylesheet.locationsBackground`; Reduce Transparency hides the ink. +Empty, initial-loading, and failed screens show only the full-view rosette texture. + ## Testing Swift Testing in [`Tests/`](Tests) (`WhereUITests`), hosted in `StuffTestHost` diff --git a/Where/WhereUI/SnapshotTests/LocationsBackgroundSnapshotTests.swift b/Where/WhereUI/SnapshotTests/LocationsBackgroundSnapshotTests.swift new file mode 100644 index 000000000..e38609355 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/LocationsBackgroundSnapshotTests.swift @@ -0,0 +1,9 @@ +import SnapshotKitTesting +import Testing +@testable import WhereUI + +struct LocationsBackgroundSnapshotTests { + @Test func locationsBackground() async { + await assertSnapshots(of: LocationsBackground.self) + } +} diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Empty_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Empty_iPhone.png new file mode 100644 index 000000000..70728982e --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Empty_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:084a5618ea0ab06bece4c5176c925e2a833826ff53c09a6d6b554df7b491da83 +size 369638 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Empty_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Empty_iPhone_dark.png new file mode 100644 index 000000000..baf37a2b5 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Empty_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fee0319b8a89d7d5656026242ff0de7262dc2e57fbe2ea3e450953eb5b5c245 +size 388610 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Many_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Many_iPhone.png new file mode 100644 index 000000000..052be5c3c --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Many_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:881ee533fb4fe9fc2c96b774216fc370d757959194fc7f3dc58e2b647ff9c8e0 +size 531318 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Many_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Many_iPhone_dark.png new file mode 100644 index 000000000..6932d59ff --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Many_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a47925423e9c997e48d6ee32b41dd09435246fd0a5fd409e5ef8dbf5812627cf +size 554837 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.One_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.One_iPhone.png new file mode 100644 index 000000000..98944484e --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.One_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2780d2f256c09a6bce824545d8e5aa418ad1a30fb011e210ee4b5cd46b98a6b +size 494333 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.One_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.One_iPhone_dark.png new file mode 100644 index 000000000..24446635e --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.One_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f60cd391fcd615a56978fbf013fc74e7046f6fe24b770df39f38780ebb19d99 +size 514845 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.ReduceTransparency_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.ReduceTransparency_iPhone.png new file mode 100644 index 000000000..059e8e263 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.ReduceTransparency_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ced93b976947a701da8e13d21135994279ccc94f3a43263fb7f6aac044dee311 +size 61465 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.ReduceTransparency_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.ReduceTransparency_iPhone_dark.png new file mode 100644 index 000000000..aaaf2996d --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.ReduceTransparency_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:974c3938eb41b9639c6486705fa6af269d4942f9f2004832dc672e0a4cc22b0c +size 68816 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Several_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Several_iPhone.png new file mode 100644 index 000000000..9666fcab5 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Several_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8ed6cd58d9441184c388ac03eb8c212c20736940d267488f480f020de85c60c +size 509680 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Several_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Several_iPhone_dark.png new file mode 100644 index 000000000..2d55ae1db --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsBackgroundSnapshotTests/locationsBackground.Several_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9aa9be77876b37b566809d33e8bb481e031d3adfa974e73617ff2e408e504a7a +size 532104 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.DotsHidden_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.DotsHidden_iPhone.png index 3127cfe02..b34079f68 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.DotsHidden_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.DotsHidden_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea65986917ead93a42e55e798faabb07a05c597bfeb7fc7570e554c088ab28ab -size 2343318 +oid sha256:cc3fc6fd4ec2b6c5fd03fb9d3e058b0a4af0fef89bfb7a363f0e2ab07247d1e9 +size 2601034 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.DotsHidden_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.DotsHidden_iPhone_dark.png index a163db8e6..099f0c219 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.DotsHidden_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.DotsHidden_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acd3fba4d749eb46f9b21366fd72b720be6a380755773b835540f97b9977684c -size 2468047 +oid sha256:81269dc33a6c3ffa92453d676b3e333ebe8d0bd2653ef45627be725b72660bcd +size 2737971 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ElsewhereOnly_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ElsewhereOnly_iPhone.png index 9c0f24c44..ec695d1a7 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ElsewhereOnly_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ElsewhereOnly_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1e57bca53ce5ba3ddc4e471b46f83e6f85f374cd370f47d6d074d5c3c39baa4 -size 108230 +oid sha256:d95b379c43ba668478a28a19ef15d793084900c92ca0473bfb0cd6baf3f1a67d +size 410157 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ElsewhereOnly_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ElsewhereOnly_iPhone_dark.png index 06ecb4e3a..aecca35ee 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ElsewhereOnly_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ElsewhereOnly_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:416e3130e990a8610db37ee93f013cb0116b753daf3df724b2653d65af9a9ac6 -size 117727 +oid sha256:f276b14e5ee4d558d0544f69fb17d61ed4802342b530dcfe80bdf7efe6fb640a +size 430269 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Empty_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Empty_iPhone.png index 8e2dce736..5c61f27cd 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Empty_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Empty_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37d69a3fb9df28fea02c5ffab806634e4ae8961f3793f23007f5bbbbd125fc3a -size 102464 +oid sha256:e3f2518f771591acdfa3832370b8a2ac2dc5119c0ec3708e096aa2e06326bbf0 +size 409395 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Empty_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Empty_iPhone_dark.png index bd821fd95..2b847bff7 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Empty_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Empty_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3dfb3e5eb42e3ea298a2e2edb6ca3b0b721d010ade7249a0bfbbb976f878ecb8 -size 111154 +oid sha256:d607c480e9bd4df36ad492b79dd1048394a9fb3f5ba3ec3ddb550b90e072874d +size 428423 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ForecastsHidden_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ForecastsHidden_iPhone.png index bcb3b3880..18d81feb0 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ForecastsHidden_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ForecastsHidden_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfa1ea9194318013bb1eae315ccada9aeb9e086d20a8f9f53020974a012ccee2 -size 2325029 +oid sha256:036c5542e63f994a9f968f04d0b68932b6ecb6aeec35b554ec44db235a0d58f5 +size 2574976 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ForecastsHidden_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ForecastsHidden_iPhone_dark.png index ed8d764f4..7058c3bdb 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ForecastsHidden_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ForecastsHidden_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7cbb7e30a13d8d48d078b672a88f808ff8286d7d1ddfad4507d23844259e9e1 -size 2450820 +oid sha256:d9a9c21af8730b642b1f70085c1c29033fbd698171dea4a6175eb4d3e7e38dd8 +size 2725776 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad.png index bd40bbf94..b9b99a0d4 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd9d75c4ecff75feb95f6df398932782b848efbbc781a480f3827d188a491b4c -size 3868472 +oid sha256:0d9515f7cb64cde07afaee53ae0e7e0964ab8cf981025a7daa4809de59a613cd +size 4747385 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_accessibility.png index ed95b40d9..cfa0579c2 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ce85f668387df26849045d52f5799e8db965a9a5eda12875f765268d832f81e -size 2597593 +oid sha256:e92c0935c368894f6f1b46509797ff5cfa0a3c3ac17454fc6aec5bbba11f560f +size 3377200 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_ax5.png index 70c265800..3d0f58873 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d69c8883b2c0a55de90dbdf3c77a21073b22b2c645d87b286c7680b6b41a9cb -size 4880488 +oid sha256:828f78981a865f41e640c0782c10a07d874dff84d3754313ae0c9b45a60d3cc1 +size 5547486 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_contrast.png index 6bd85290a..dfa0795db 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1599c1f4b89fbc46c47255c5acf12cb621a0f624b407fe28081d1a673041b40b -size 3796322 +oid sha256:1bb7c6896f8c19da314e9c1dda277ed03bd4a4b0196d470aa2071687b6d1bfe8 +size 4660084 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_dark.png index 7544c2de1..006758489 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ee6a33c1fb3e66e4ff8ae229fd509677eade1b25de1cdf2861421b1122ed64a -size 4168144 +oid sha256:b91905e7921da7cffcb36f2ed336f1df564e79d385807a4feafff30b35074cfa +size 5056320 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone.png index 67962a441..e6ff56a61 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:511b9cdc2deedaaf965cce4e1c5c9c4f398022186cd9c838a4ed9742b205c2a3 -size 2350330 +oid sha256:0108ba741fe279c727ad744274cfa9151e36c0572cd20cca37d0a09a07c92d1c +size 2609082 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_accessibility.png index dc6abffa3..782dfc745 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3d040017417061c12c1e5da7008af2c6f3ffd1b0ea7ee9a371cf533cef40ffc -size 1584939 +oid sha256:825314e1b892fb5695edf7dbd2a99c37738242fafcb47e72f450b2b6a85fcc30 +size 1775567 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_ax5.png index 933358ca6..95d133ab9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f24328842799078cd325646fe9ad9c1d9d7d6419d47e52cd36e05ff899b3d7a -size 3414264 +oid sha256:56a953a86e8c25c1c0a7107582598f0d8cd6950edec2d758e6d02dc82757dfd2 +size 3597759 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_contrast.png index df36b8b00..114db31da 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9876ac8a58dd9a7d8ebe916cb1a6c051737e8ee478f72ad65c6f3a741acff4a -size 2252058 +oid sha256:4ba8e955bc01ec757a31321eaa246d675d03c6e4c154b7f2ac56d5244d3fb10c +size 2489697 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_dark.png index 5b8ae10da..1d4f60464 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36750a85488c3963f9fef02e94a77afb8660e298d5b5141c9c9291d1c0f3a2c1 -size 2475433 +oid sha256:048b5fca0b720cde1d33a4aec43c8477b87abd7c11afe464e48612b51734db46 +size 2746534 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.MissingDays_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.MissingDays_iPhone.png index cff9f256d..436132328 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.MissingDays_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.MissingDays_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c3658cc955e28876306afa4a3ea43a612fd85256b393c2714278038505a208f -size 1214991 +oid sha256:6a1998dbd58bcef70c63b6ea068639567042d35e563906823b3419574d2d1f39 +size 1607660 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.MissingDays_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.MissingDays_iPhone_dark.png index 6584910f0..c935e89f9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.MissingDays_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.MissingDays_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cddb6bafb801f82b41e8ea630e803768eacacbc09587223a05e5e732d3169c5d -size 1239858 +oid sha256:285ec0efcae6f97fed4d23dbd435e36f7d16580dc803ddb480b68ff8fd29263f +size 1604414 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.PlannedStay_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.PlannedStay_iPhone.png index 100915767..39cf1dda7 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.PlannedStay_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.PlannedStay_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42734aef9c3a3938a05406ca90e02fe0ceee56d7efbe732b687363fe68e87c0b -size 2219256 +oid sha256:92405a1e53e701f1d4aa8b09f0ce0b7887734aaf00466e78cc2965813f4c1211 +size 2502877 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.PlannedStay_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.PlannedStay_iPhone_dark.png index 2c272ce6f..544272bfb 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.PlannedStay_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.PlannedStay_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ea3f675f323578301020a94a1c7bf1fdf9ff07fd3113db2380854b5fa481911 -size 2295318 +oid sha256:79cb98a8b4b27ed0d9180caa5629a66d7765c53171c5c6d8607a2b098eef4181 +size 2563217 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone.png index 6e1524191..3a3897028 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:875ffd945db9f8f4eff53761b035f4ee9a0d80b28c0cec0f3dfba6d7bb5ef471 -size 1031894 +oid sha256:a2bfdc61974bb5df29af5be9ce6a0ce41cc7990c6a36bd92e846b21fde864909 +size 1246567 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_accessibility.png index 18f9fb1d7..4078a3033 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3824b30e1786a5ae9d1eb80f332c538cdc94a0a7d47bf4340aad2307b1a06b41 -size 914021 +oid sha256:258ba7ee45acbdfb02fb51a04e722f23b7bb1add944bea79f6f085385f797ff7 +size 1105035 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_ax5.png index 0dbcacf50..34c0f4745 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:851d7a31cedb1004dcf8a01fde396223c8c7f6558f854ce3a5deade17df61ae0 -size 2360955 +oid sha256:bde0ca4685e5b60ade7f233c7e42327c218e7f4d62eef24a61dd8e94011ebe89 +size 2425715 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_dark.png index 235118837..fff731bc6 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocations_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2161ade105f31e655f24f3d778fe649263b253552be256a1828955f2e2b85f80 -size 794734 +oid sha256:d3a1937c43bb8c8b23ed1aa075acbf80b9492b1092824d8150887c910c620ed7 +size 1021068 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.LoggedIn_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.LoggedIn_iPhone.png index 9db8aa454..1ea3f7af9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.LoggedIn_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.LoggedIn_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a746c88054d92de90811faa8efdbd7bc012398befa665de19c89c57cb929fe01 -size 184526 +oid sha256:d16add60528d23a9e1f6a32e9dcb043be3971c2f7ec38cd33d3fd215f58b9c7d +size 487909 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.LoggedIn_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.LoggedIn_iPhone_dark.png index 942d25067..442768755 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.LoggedIn_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.LoggedIn_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f657d8b0b440aeec16d0046b1a6a5a70bd8e37573d16e0f16b78edcd9fc27137 -size 179383 +oid sha256:f319864554c3488ff48a7f1ed227324e4f0133b18521f9d26b0fa986a05f5c73 +size 502233 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.RecordingConfigurationWarning_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.RecordingConfigurationWarning_iPhone.png index bf9eb8472..0f6495aef 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.RecordingConfigurationWarning_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.RecordingConfigurationWarning_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:929d4a8a0cf7be1fbf906afae13af64f1069bc53a4b24c6356426e4092be4579 -size 185252 +oid sha256:62f09e8946b77dbfca7e8fd64a26d1847113b2cd5ce02d6d1083ff8e83845c09 +size 488531 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.RecordingConfigurationWarning_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.RecordingConfigurationWarning_iPhone_dark.png index 995284afb..c0e5c0643 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.RecordingConfigurationWarning_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RootViewSnapshotTests/root.RecordingConfigurationWarning_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bd09f67f7ba9d3c225f56280698a2aea27b150921f90df37d82e33102c7a3c7 -size 179699 +oid sha256:1ec2978b0beaf23ccb2aee6f5c4a9cfcb4c54bcd76195fd08e22ca3ff419371f +size 502264 diff --git a/Where/WhereUI/Sources/Primary/LocationsBackground.swift b/Where/WhereUI/Sources/Primary/LocationsBackground.swift new file mode 100644 index 000000000..5d03d2b64 --- /dev/null +++ b/Where/WhereUI/Sources/Primary/LocationsBackground.swift @@ -0,0 +1,133 @@ +import BroadwayUI +import RegionKit +import SwiftUI + +/// Stationary, noninteractive paper texture for the root Locations viewport. +struct LocationsBackground: View { + let regions: [Region] + + @Environment(\.stylesheet) private var stylesheet + @Environment(\.regionOutlinePathCache) private var cache + @State private var artwork: LocationsBackgroundArtwork? + + private var style: WhereStylesheet.LocationsBackgroundStyle { + stylesheet.locationsBackground + } + + private var requestedRegions: [Region] { + style.showsInk ? regions.filter { $0 != .other } : [] + } + + var body: some View { + ZStack { + style.paper + if style.showsInk { + SecurityPrintRosette( + tint: style.ink, + wobble: style.rosette.wobble, + lineWidth: style.rosette.lineWidth, + primaryRingSpacing: style.rosette.primaryRingSpacing, + secondaryRingSpacing: style.rosette.secondaryRingSpacing, + primaryOpacity: style.rosetteOpacity, + secondaryOpacity: style.rosetteOpacity, + ) + silhouettes + } + } + .clipped() + .allowsHitTesting(false) + .accessibilityHidden(true) + .task(id: requestedRegions) { + guard let cache else { return } + if let loaded = await LocationsBackgroundArtwork.load( + regions: requestedRegions, + cache: cache, + ) { + guard !Task.isCancelled else { return } + artwork = loaded + } + } + } + + private var silhouettes: some View { + GeometryReader { proxy in + let items = artwork?.items(for: requestedRegions) ?? [] + let cells = LocationsBackgroundLayout.cells( + count: items.count, + in: proxy.size, + preferredCellSize: style.preferredCellSize, + ) + ZStack(alignment: .topLeading) { + ForEach(cells) { cell in + let item = items[cell.artworkIndex] + RegionOutlineArtwork( + path: item.path, + tint: style.ink, + style: balancedArtwork(for: item.path), + ) + .frame(width: cell.frame.width, height: cell.frame.height) + .position(x: cell.frame.midX, y: cell.frame.midY) + } + } + } + } + + private func balancedArtwork(for path: Path) -> WhereStylesheet.CardStyle.RegionShape.Artwork { + var artwork = style.artwork + let bounds = path.boundingRect + let shortSide = min(bounds.width, bounds.height) + guard shortSide > 0 else { return artwork } + // Give slender regions more room without changing the shared geographic projection. + artwork.scale *= min( + style.maximumAspectScale, + sqrt(max(bounds.width, bounds.height) / shortSide), + ) + return artwork + } +} + +#if DEBUG + import SnapshotKit + + extension LocationsBackground: SnapshotProviding { + static var snapshots: [SnapshotCase] { + artworkSnapshot(name: "Empty", regions: []) + artworkSnapshot(name: "One", regions: [.california]) + artworkSnapshot(name: "Several", regions: [.california, .newYork, .canada, .other]) + artworkSnapshot(name: "Many", regions: Array(Region.allCases.prefix(40))) + artworkSnapshot( + name: "ReduceTransparency", + regions: [.canada, .other], + reduceTransparency: true, + ) + } + + private static func artworkSnapshot( + name: String, + regions: [Region], + reduceTransparency: Bool = false, + ) -> SnapshotCase { + let cache = RegionOutlinePathCache() + return whereSnapshot( + name: name, + configurations: .phoneLightDark, + measurementReadiness: .immediate, + onReadyToSnapshot: { + _ = await LocationsBackgroundArtwork.load(regions: regions, cache: cache) + }, + ) { + LocationsBackground(regions: regions) + .environment(\.regionOutlinePathCache, cache) + .bTraitOverrides { traits, overrides in + var accessibility = traits.accessibility + accessibility.isReduceTransparencyEnabled = reduceTransparency + overrides.accessibility = accessibility + } + } + } + } + + #Preview { + LocationsBackground.snapshotPreviews + } +#endif diff --git a/Where/WhereUI/Sources/Primary/LocationsBackgroundArtwork.swift b/Where/WhereUI/Sources/Primary/LocationsBackgroundArtwork.swift new file mode 100644 index 000000000..56b6ea3a3 --- /dev/null +++ b/Where/WhereUI/Sources/Primary/LocationsBackgroundArtwork.swift @@ -0,0 +1,36 @@ +import RegionKit +import SwiftUI + +/// Cached outlines retain their membership so obsolete loads cannot decorate a new year. +struct LocationsBackgroundArtwork { + struct Item: Identifiable { + let region: Region + let path: Path + + var id: Region { + region + } + } + + let items: [Item] + + static func regions(in ranking: RegionRanking) -> [Region] { + let visited = Set((ranking.primary + ranking.secondary).map(\.region)) + return Region.allCases.filter { visited.contains($0) } + } + + func items(for regions: [Region]) -> [Item] { + items.map(\.region) == regions ? items : [] + } + + static func load(regions: [Region], cache: RegionOutlinePathCache) async -> Self? { + var items: [Item] = [] + for region in regions { + guard !Task.isCancelled else { return nil } + let path = await cache.path(for: region, resolution: .small) + items.append(Item(region: region, path: path)) + } + guard !Task.isCancelled else { return nil } + return Self(items: items) + } +} diff --git a/Where/WhereUI/Sources/Primary/LocationsBackgroundLayout.swift b/Where/WhereUI/Sources/Primary/LocationsBackgroundLayout.swift new file mode 100644 index 000000000..448892f12 --- /dev/null +++ b/Where/WhereUI/Sources/Primary/LocationsBackgroundLayout.swift @@ -0,0 +1,47 @@ +import CoreGraphics + +/// A staggered region lattice that preserves full membership and extends beyond the viewport. +enum LocationsBackgroundLayout { + struct Cell: Identifiable { + let id: Int + let artworkIndex: Int + let frame: CGRect + } + + static func cells(count: Int, in size: CGSize, preferredCellSize: CGFloat) -> [Cell] { + guard count > 0, size.width > 0, size.height > 0, preferredCellSize > 0 else { + return [] + } + var columns = max(1, Int(size.width / preferredCellSize)) + var rows = max(1, Int(size.height / preferredCellSize)) + while columns * rows < count { + if size.width / CGFloat(columns + 1) > size.height / CGFloat(rows + 1) { + columns += 1 + } else { + rows += 1 + } + } + let side = min(size.width / (CGFloat(columns) + 0.5), size.height / CGFloat(rows)) + let coveringColumns = Int((size.width / side).rounded(.up)) + let coveringRows = Int((size.height / side).rounded(.up)) + var cells: [Cell] = [] + // Overscan every edge so the repeat can be clipped naturally. + for row in -1 ... coveringRows { + for column in -1 ... coveringColumns { + let frame = CGRect( + x: (CGFloat(column) + (row.isMultiple(of: 2) ? 0 : 0.5)) * side, + y: CGFloat(row) * side, + width: side, + height: side, + ) + let index = row * columns + column + cells.append(Cell( + id: cells.count, + artworkIndex: (index % count + count) % count, + frame: frame, + )) + } + } + return cells + } +} diff --git a/Where/WhereUI/Sources/Primary/LocationsView.swift b/Where/WhereUI/Sources/Primary/LocationsView.swift index 7f66ed4ab..e1cc21a0d 100644 --- a/Where/WhereUI/Sources/Primary/LocationsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsView.swift @@ -50,6 +50,11 @@ struct LocationsView: View { NavigationStack { screen + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background { + LocationsBackground(regions: backgroundRegions) + .ignoresSafeArea() + } .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItemGroup(placement: .topBarTrailing) { @@ -237,6 +242,15 @@ struct LocationsView: View { ) } + private var backgroundRegions: [Region] { + switch report.loadState { + case .failed: + [] + case .idle, .loaded, .loading: + LocationsBackgroundArtwork.regions(in: report.ranking) + } + } + private var primaryRegions: [Region] { report.ranking.primary.map(\.region).filter { $0 != .other } } @@ -334,22 +348,14 @@ private struct ResolveToolbarLabel: View { #if DEBUG extension LocationsView: SnapshotProviding { - /// The raised settle floor on `Loaded` outlasts the native glass toolbar - /// material adaptation (seen pre-adaptation once on the equivalent - /// pre-split screen) — same mechanism as `RootView.LoggedIn`. + /// Allow native glass adaptation and the background artwork to settle before capture. static var snapshots: [SnapshotCase] { - whereSnapshot( - name: "Loaded", - configurations: .fullContentScreenDefaults, - measurementReadiness: .immediate, - settle: .settledAtLeast(minDuration: 1.0), - ) { - LocationsView(report: PreviewSupport.loadedYearReportModel()) - } + loadedSnapshot() whereSnapshot( name: "PlannedStay", configurations: .fullContentPhoneLightDark, measurementReadiness: .immediate, + settle: .settledAtLeast(minDuration: 1.0), ) { LocationsView(report: PreviewSupport.plannedStayYearReportModel()) } @@ -357,6 +363,7 @@ private struct ResolveToolbarLabel: View { name: "ForecastsHidden", configurations: .fullContentPhoneLightDark, measurementReadiness: .immediate, + settle: .settledAtLeast(minDuration: 1.0), ) { LocationsView(report: forecastsHiddenReport()) } @@ -364,6 +371,7 @@ private struct ResolveToolbarLabel: View { name: "Empty", configurations: .phoneLightDark, measurementReadiness: .immediate, + settle: .settledAtLeast(minDuration: 1.0), ) { LocationsView(report: PreviewSupport.emptyYearReportModel()) } @@ -371,6 +379,7 @@ private struct ResolveToolbarLabel: View { name: "MissingDays", configurations: .fullContentPhoneLightDark, measurementReadiness: .immediate, + settle: .settledAtLeast(minDuration: 1.0), ) { LocationsView(report: PreviewSupport.missingDaysYearReportModel()) } @@ -378,6 +387,7 @@ private struct ResolveToolbarLabel: View { name: "ElsewhereOnly", configurations: .phoneLightDark, measurementReadiness: .immediate, + settle: .settledAtLeast(minDuration: 1.0), ) { LocationsView(report: PreviewSupport.elsewhereOnlyYearReportModel()) } @@ -385,6 +395,7 @@ private struct ResolveToolbarLabel: View { name: "DotsHidden", configurations: .fullContentPhoneLightDark, measurementReadiness: .immediate, + settle: .settledAtLeast(minDuration: 1.0), ) { LocationsView( report: PreviewSupport.loadedYearReportModelWithLocationDotsHidden(), @@ -392,6 +403,30 @@ private struct ResolveToolbarLabel: View { } } + private static func loadedSnapshot() -> SnapshotCase { + let report = PreviewSupport.loadedYearReportModel() + let cache = RegionOutlinePathCache() + let regions = LocationsBackgroundArtwork.regions(in: report.ranking) + return whereSnapshot( + name: "Loaded", + configurations: .fullContentScreenDefaults, + measurementReadiness: .immediate, + settle: .settledAtLeast(minDuration: 1.0), + onReadyToSnapshot: { + // Keep geometry ready across the accessibility renderer's reparenting. + // Pixel stability alone can settle on the unloaded symbol fallback. + for region in regions { + _ = await cache.path(for: region, resolution: .small) + _ = await cache.path(for: region, resolution: .medium) + _ = await cache.path(for: region, resolution: .micro) + } + }, + ) { + LocationsView(report: report) + .environment(\.regionOutlinePathCache, cache) + } + } + private static func forecastsHiddenReport() -> YearReportModel { PreviewSupport.loadedYearReportModelWithEstimatedTimeHidden() } diff --git a/Where/WhereUI/Sources/Shared/WhereStylesheet.swift b/Where/WhereUI/Sources/Shared/WhereStylesheet.swift index fe0f517d0..e2a58b796 100644 --- a/Where/WhereUI/Sources/Shared/WhereStylesheet.swift +++ b/Where/WhereUI/Sources/Shared/WhereStylesheet.swift @@ -17,6 +17,7 @@ struct WhereStylesheet: BStylesheet { var spacing = Spacing() var size = Size() var card = CardStyles.standard + var locationsBackground = LocationsBackgroundStyle() var locationCardStack = LocationCardStackStyle.standard var locationWelcome = LocationWelcomeStyle.standard var calendar = CalendarStyle.standard @@ -87,6 +88,7 @@ struct WhereStylesheet: BStylesheet { // Reduce Transparency flattens the cards: drop the decorative rim-glow // layer (the translucent halo) on both card variants. if traits.accessibility.isReduceTransparencyEnabled { + locationsBackground.showsInk = false card.regular.glow.radius = 0 card.compact.glow.radius = 0 card.constellation.haloOpacity = 0 @@ -2593,6 +2595,33 @@ extension EnvironmentValues { } } +// MARK: - Locations background + +extension WhereStylesheet { + /// Flat security print behind the selected year's Locations content. + struct LocationsBackgroundStyle: Equatable { + var paper = Color(uiColor: .systemBackground) + var ink = Color.primary + var showsInk = true + var preferredCellSize: CGFloat = 54 + var artwork = CardStyle.RegionShape.Artwork( + center: CGPoint(x: 0.5, y: 0.5), + extent: CGSize(width: 0.545, height: 0.545), + scale: 1, + fillOpacity: 0, + stroke: .init(opacity: 0.043, width: 1.1), + ) + var maximumAspectScale: CGFloat = 1.25 + var rosette = CardStyle.Rosette( + wobble: 0.04, + lineWidth: 0.5, + primaryRingSpacing: 18, + secondaryRingSpacing: 25, + ) + var rosetteOpacity: Double = 0.015 + } +} + extension WhereStylesheet { /// A resolved arrangement, independent of the trait that selected it. enum ContentLayout: Equatable { diff --git a/Where/WhereUI/Tests/LocationsBackgroundArtworkTests.swift b/Where/WhereUI/Tests/LocationsBackgroundArtworkTests.swift new file mode 100644 index 000000000..85c19ddd9 --- /dev/null +++ b/Where/WhereUI/Tests/LocationsBackgroundArtworkTests.swift @@ -0,0 +1,45 @@ +import RegionKit +import Testing +@testable import WhereUI + +struct LocationsBackgroundArtworkTests { + @Test func membershipIncludesBothRanksAndIgnoresRankChanges() { + let first = RegionRanking( + primary: [.init(region: .canada, days: 30), .init(region: .newYork, days: 20)], + secondary: [.init(region: .other, days: 1), .init(region: .canada, days: 2)], + ) + let reordered = RegionRanking( + primary: [.init(region: .newYork, days: 40)], + secondary: [.init(region: .canada, days: 30), .init(region: .other, days: 1)], + ) + let regions = LocationsBackgroundArtwork.regions(in: first) + #expect(Set(regions) == Set([Region.canada, .newYork, .other])) + #expect(regions.count == 3) + #expect(regions == LocationsBackgroundArtwork.regions(in: reordered)) + #expect(LocationsBackgroundArtwork.regions(in: .init(primary: [], secondary: [])).isEmpty) + } + + @Test func changedMembershipHidesPreviousArtwork() async throws { + let artwork = try #require(await LocationsBackgroundArtwork.load( + regions: [.canada, .other], + cache: RegionOutlinePathCache(), + )) + #expect(artwork.items(for: [.canada, .other]).count == 2) + #expect(artwork.items[0].path.isEmpty == false) + #expect(artwork.items[1].path.isEmpty) + #expect(artwork.items(for: [.newYork]).isEmpty) + #expect(artwork.items(for: []).isEmpty) + } + + @MainActor + @Test func cancelledLoadCannotPublishArtwork() async { + let task = Task { @MainActor in + await LocationsBackgroundArtwork.load( + regions: [.canada], + cache: RegionOutlinePathCache(), + ) + } + task.cancel() + #expect(await task.value == nil) + } +} diff --git a/Where/WhereUI/Tests/LocationsBackgroundLayoutTests.swift b/Where/WhereUI/Tests/LocationsBackgroundLayoutTests.swift new file mode 100644 index 000000000..2effd7ccb --- /dev/null +++ b/Where/WhereUI/Tests/LocationsBackgroundLayoutTests.swift @@ -0,0 +1,55 @@ +import CoreGraphics +import Testing +@testable import WhereUI + +struct LocationsBackgroundLayoutTests { + @Test(arguments: [1, 3, 40, 200]) + func everyRegionHasAFullyVisibleCell(count: Int) { + let size = CGSize(width: 390, height: 844) + let cells = LocationsBackgroundLayout.cells(count: count, in: size, preferredCellSize: 64) + let viewport = CGRect(origin: .zero, size: size).insetBy(dx: -0.001, dy: -0.001) + let visibleRegions = cells.compactMap { cell -> Int? in + guard viewport.contains(cell.frame) else { return nil } + return cell.artworkIndex + } + #expect(Set(visibleRegions) == Set(0 ..< count)) + #expect(Set(cells.map(\.id)).count == cells.count) + #expect(cells.allSatisfy { $0.frame.width == $0.frame.height && $0.frame.width > 0 }) + } + + @Test func alternateRowsHaveAHalfCellOffset() throws { + let cells = LocationsBackgroundLayout.cells( + count: 5, + in: CGSize(width: 390, height: 844), + preferredCellSize: 64, + ) + let first = try #require(cells.first { abs($0.frame.minY) < 0.001 }) + let second = try #require(cells.first { $0.frame.minY > first.frame.minY }) + let third = try #require(cells.first { $0.frame.minY > second.frame.minY }) + #expect(abs(second.frame.minX - first.frame.minX - first.frame.width / 2) < 0.001) + #expect(abs(third.frame.minX - first.frame.minX) < 0.001) + } + + @Test func patternExtendsBeyondEveryEdge() { + let size = CGSize(width: 390, height: 844) + let cells = LocationsBackgroundLayout.cells(count: 5, in: size, preferredCellSize: 64) + #expect(cells.contains { $0.frame.minX < 0 }) + #expect(cells.contains { $0.frame.maxX > size.width }) + #expect(cells.contains { $0.frame.minY < 0 }) + #expect(cells.contains { $0.frame.maxY > size.height }) + for row in Dictionary(grouping: cells, by: { $0.frame.minY }).values { + for (left, right) in zip(row, row.dropFirst()) { + #expect(right.artworkIndex == (left.artworkIndex + 1) % 5) + #expect(abs(left.frame.maxX - right.frame.minX) < 0.001) + } + } + } + + @Test func emptyHistoryAndInvalidSizesProduceNoRegionCells() { + let size = CGSize(width: 390, height: 844) + let cells = LocationsBackgroundLayout.cells(count: 0, in: size, preferredCellSize: 64) + #expect(cells.isEmpty) + #expect(LocationsBackgroundLayout.cells(count: 3, in: .zero, preferredCellSize: 64).isEmpty) + #expect(LocationsBackgroundLayout.cells(count: 3, in: size, preferredCellSize: 0).isEmpty) + } +} diff --git a/Where/WhereUI/Tests/WhereStylesheetTests.swift b/Where/WhereUI/Tests/WhereStylesheetTests.swift index 2d9309207..4ce8ac715 100644 --- a/Where/WhereUI/Tests/WhereStylesheetTests.swift +++ b/Where/WhereUI/Tests/WhereStylesheetTests.swift @@ -619,6 +619,29 @@ struct WhereStylesheetTests { #expect(evidence.loadingMinHeight == 200) } + @Test func locationsBackgroundStyle() { + let background = style.locationsBackground + #expect(background.paper == Color(uiColor: .systemBackground)) + #expect(background.ink == Color.primary) + #expect(background.showsInk) + #expect(background.preferredCellSize == 54) + #expect(background.artwork == .init( + center: CGPoint(x: 0.5, y: 0.5), + extent: CGSize(width: 0.545, height: 0.545), + scale: 1, + fillOpacity: 0, + stroke: .init(opacity: 0.043, width: 1.1), + )) + #expect(background.maximumAspectScale == 1.25) + #expect(background.rosette == .init( + wobble: 0.04, + lineWidth: 0.5, + primaryRingSpacing: 18, + secondaryRingSpacing: 25, + )) + #expect(background.rosetteOpacity == 0.015) + } + @Test func elsewhereCardStyle() { let card = style.elsewhereCard #expect(card.cornerRadius == 28) @@ -948,6 +971,7 @@ struct WhereStylesheetTests { #expect(resolved.card.constellation.haloOpacity == 0) #expect(resolved.card.constellation.coreOpacity == 0.92) #expect(resolved.privacyPassportCard.disclosure.fillOpacity == 0.16) + #expect(resolved.locationsBackground.showsInk == false) #expect(resolved.elsewhereCard.surface.usesOpaquePaper) #expect(resolved.elsewhereCard.surface.shadowOpacity == 0) }