From 47bd1bfc2f4aa61790383bc30ec0965d642d35f6 Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:31:15 +0100 Subject: [PATCH 01/13] Add files via upload --- crates/anyps5-launcher/src/screens/dialog.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/anyps5-launcher/src/screens/dialog.rs b/crates/anyps5-launcher/src/screens/dialog.rs index c98be20e..53f34e8d 100644 --- a/crates/anyps5-launcher/src/screens/dialog.rs +++ b/crates/anyps5-launcher/src/screens/dialog.rs @@ -33,8 +33,14 @@ use crate::ui::theme::{self, TextStyle, color, radius, shadows, size, space, ty} /// The card's width; it narrows to the screen minus a margin on a small window. const CARD_WIDTH: f32 = 560.0; -/// How dark the page behind the card is at rest. -const SCRIM: f32 = 0.55; +/// How dark the page behind the card is at rest. Light: the frosted veil below does most of the +/// work, this only keeps the page calm where the veil is not drawn (High Contrast, no GPU). +const SCRIM: f32 = 0.18; +/// The frosted veil over the page: the blurred backdrop (the game's art and colours) in place of +/// the sharp page, so the card sits on a soft blur of what is behind it. One full-screen glass +/// draw while the card is open, nothing when it is closed. +const VEIL_BLACK: f32 = 0.28; +const VEIL_ALPHA: f32 = 0.9; /// The room between the card's edge and its content. const CARD_PADDING: f32 = space::XL; /// The card rises this far while it opens (a transform, not a reflow); none under "Reduced". @@ -287,6 +293,10 @@ fn paint( let mut text_painter = painter.clone(); text_painter.set_opacity(v); paint_scrim(&painter, screen, SCRIM * v); + let mut veil = GlassStyle::fullscreen_panel(); + veil.black = VEIL_BLACK * v; + veil.alpha = VEIL_ALPHA * v; + paint_glass(ctx, &painter, screen, veil); let width = card_width(screen.width()); let inner = width - 2.0 * CARD_PADDING; From 999c96366ab0ea5e6c722baed5837698179f6f18 Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:31:42 +0100 Subject: [PATCH 02/13] Add files via upload --- .github/workflows/ci-manual-win.yml | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/ci-manual-win.yml diff --git a/.github/workflows/ci-manual-win.yml b/.github/workflows/ci-manual-win.yml new file mode 100644 index 00000000..72645929 --- /dev/null +++ b/.github/workflows/ci-manual-win.yml @@ -0,0 +1,53 @@ +name: ci-manual-win + +# Solo avvio manuale (scheda Actions -> ci -> Run workflow). Nessun trigger su push o pull request. +on: + workflow_dispatch: + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +# The online crates come from anyplaynet/network, checked out beside the launcher at the commit in +# network.ref (one full SHA). +defaults: + run: + working-directory: launcher + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + path: launcher + - id: network-pin + name: Read the pinned network revision + shell: bash + run: | + ref=$(tr -d '[:space:]' < "$GITHUB_WORKSPACE/launcher/network.ref") + [[ $ref =~ ^[0-9a-f]{40}$ ]] || { echo "network.ref must hold one full commit SHA" >&2; exit 1; } + echo "ref=$ref" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + repository: anyplaynet/network + ref: ${{ steps.network-pin.outputs.ref }} + path: network + - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 + with: + toolchain: stable + targets: x86_64-pc-windows-msvc + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 + with: + workspaces: launcher + - name: Release build + run: cargo build --release --target x86_64-pc-windows-msvc + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: anyplay-launcher-windows-x64 + path: launcher/target/x86_64-pc-windows-msvc/release/anyplay-launcher.exe + if-no-files-found: error From 638c96ccfbe74d5ad2dfb2e81c410218b20a1d47 Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:48:54 +0100 Subject: [PATCH 03/13] Add files via upload --- crates/anyps5-launcher/src/screens/dialog.rs | 19 +- crates/anyps5-launcher/src/ui/chrome.rs | 8 +- crates/anyps5-launcher/src/ui/mod.rs | 1 + crates/anyps5-launcher/src/ui/pageblur.rs | 182 +++++++++++++++++++ 4 files changed, 197 insertions(+), 13 deletions(-) create mode 100644 crates/anyps5-launcher/src/ui/pageblur.rs diff --git a/crates/anyps5-launcher/src/screens/dialog.rs b/crates/anyps5-launcher/src/screens/dialog.rs index 53f34e8d..70bed15f 100644 --- a/crates/anyps5-launcher/src/screens/dialog.rs +++ b/crates/anyps5-launcher/src/screens/dialog.rs @@ -27,20 +27,15 @@ use crate::ui::glass::{GlassStyle, paint_glass}; use crate::ui::i18n::Language; use crate::ui::icons::Icon; use crate::ui::input::Command; +use crate::ui::pageblur; use crate::ui::sound::Cue; use crate::ui::text; use crate::ui::theme::{self, TextStyle, color, radius, shadows, size, space, ty}; /// The card's width; it narrows to the screen minus a margin on a small window. const CARD_WIDTH: f32 = 560.0; -/// How dark the page behind the card is at rest. Light: the frosted veil below does most of the -/// work, this only keeps the page calm where the veil is not drawn (High Contrast, no GPU). -const SCRIM: f32 = 0.18; -/// The frosted veil over the page: the blurred backdrop (the game's art and colours) in place of -/// the sharp page, so the card sits on a soft blur of what is behind it. One full-screen glass -/// draw while the card is open, nothing when it is closed. -const VEIL_BLACK: f32 = 0.28; -const VEIL_ALPHA: f32 = 0.9; +/// How dark the page behind the card is at rest. +const SCRIM: f32 = 0.55; /// The room between the card's edge and its content. const CARD_PADDING: f32 = space::XL; /// The card rises this far while it opens (a transform, not a reflow); none under "Reduced". @@ -275,6 +270,7 @@ fn paint( Motion::Overlay, ); if v <= 0.001 && !open { + pageblur::release(ctx, id); return None; } let v = v.clamp(0.0, 1.0); @@ -292,11 +288,10 @@ fn paint( // Text carries explicit colours, so only the painter's opacity fades it with the card. let mut text_painter = painter.clone(); text_painter.set_opacity(v); + // The flat dim holds until the blurred page arrives (a frame or two) and stays under + // High Contrast; the blur then fades in over it. paint_scrim(&painter, screen, SCRIM * v); - let mut veil = GlassStyle::fullscreen_panel(); - veil.black = VEIL_BLACK * v; - veil.alpha = VEIL_ALPHA * v; - paint_glass(ctx, &painter, screen, veil); + pageblur::paint(ctx, &painter, screen, id, open, v); let width = card_width(screen.width()); let inner = width - 2.0 * CARD_PADDING; diff --git a/crates/anyps5-launcher/src/ui/chrome.rs b/crates/anyps5-launcher/src/ui/chrome.rs index d3c9c274..57c52127 100644 --- a/crates/anyps5-launcher/src/ui/chrome.rs +++ b/crates/anyps5-launcher/src/ui/chrome.rs @@ -1799,6 +1799,10 @@ pub fn fullscreen_panel( black: style.black * v.clamp(0.0, 1.0), overlay: fade(style.overlay, v), material_mix: lerp(0.0, style.material_mix, v.clamp(0.0, 1.0)), + // The glass covers the page it opens over in the first half of the opening and + // lets it show again on the way out; before, it was opaque from the first frame, + // so the page vanished in one frame and the panel seemed to cut in. + alpha: style.alpha * (v * 2.0).clamp(0.0, 1.0), ..style }; paint_glass(ctx, &painter, screen, faded); @@ -1810,7 +1814,9 @@ pub fn fullscreen_panel( }; let area = scale_rect(screen, scale); let mut child = ui.new_child(UiBuilder::new().max_rect(area)); - child.set_opacity(v.clamp(0.0, 1.0)); + // The contents come in once the glass has mostly covered the page, so the two pages' + // texts never overlap. + child.set_opacity(((v - 0.25) / 0.75).clamp(0.0, 1.0)); result = Some(add_contents(&mut child, v)); }); result diff --git a/crates/anyps5-launcher/src/ui/mod.rs b/crates/anyps5-launcher/src/ui/mod.rs index 0a4028c0..5d13c7e6 100644 --- a/crates/anyps5-launcher/src/ui/mod.rs +++ b/crates/anyps5-launcher/src/ui/mod.rs @@ -51,6 +51,7 @@ pub mod nav_bindings; pub mod nudge; pub mod os_emoji; pub mod osk; +pub mod pageblur; pub mod pad_hub; pub mod pad_info; pub mod press; diff --git a/crates/anyps5-launcher/src/ui/pageblur.rs b/crates/anyps5-launcher/src/ui/pageblur.rs new file mode 100644 index 00000000..86c78229 --- /dev/null +++ b/crates/anyps5-launcher/src/ui/pageblur.rs @@ -0,0 +1,182 @@ +//! The page behind a card, blurred (docs/DESIGN.md 5.9.1). +//! +//! A question stands on a soft blur of the page it covers (the tiles, the hero, the text), not on +//! the theme's backdrop alone. egui draws the page in one pass, so the page cannot be blurred while +//! it is drawn; instead the window is captured ONCE when the card opens, shrunk to an eighth, +//! blurred on the CPU (a few thousand pixels) and kept as a small texture that the card's veil +//! stretches over the screen. While the card is open nothing is recomputed: a frame costs one +//! textured quad. The texture is dropped when the card is gone. +//! +//! Until the capture arrives (one or two frames) and under High Contrast the caller keeps its flat +//! scrim. + +use egui::{ + Color32, ColorImage, Context, Event, Id, Painter, Rect, TextureHandle, TextureOptions, + UserData, ViewportCommand, pos2, +}; + +/// How much the page is shrunk before it is blurred (the blur's strength grows with it). +const SHRINK: usize = 8; +/// Box blur radius, in shrunk pixels, and the number of passes (two boxes are close to a gaussian). +const RADIUS: usize = 3; +const PASSES: usize = 2; +/// How dark the blurred page is under the card (1 = as it is). +const DIM: f32 = 0.60; +/// Seconds for the blur to fade in once the capture has arrived. +const READY_FADE: f32 = 0.15; + +/// Marks the capture this module asked for, so no other screenshot is taken for its own. +struct Tag; + +#[derive(Clone, Default)] +struct Held { + texture: Option, + asked: bool, +} + +/// Paints the blurred page over `rect` for the card `id`; `v` is the card's openness. +pub fn paint(ctx: &Context, painter: &Painter, rect: Rect, id: Id, open: bool, v: f32) { + if crate::ui::theme::high_contrast() { + return; + } + let key = id.with("page-blur"); + let mut held = ctx.data(|d| d.get_temp::(key)).unwrap_or_default(); + if open && !held.asked { + // The card is at about 0 on its first frame, so the capture shows the page alone. + held.asked = true; + ctx.send_viewport_cmd(ViewportCommand::Screenshot(UserData::new(Tag))); + ctx.request_repaint(); + } + if held.asked && held.texture.is_none() { + let shot = ctx.input(|i| { + i.events.iter().find_map(|event| match event { + Event::Screenshot { + user_data, image, .. + } if user_data.data.as_ref().is_some_and(|d| d.is::()) => { + Some(image.clone()) + } + _ => None, + }) + }); + if let Some(image) = shot { + held.texture = Some(ctx.load_texture( + "page-blur", + blurred(&image), + TextureOptions::LINEAR, + )); + } + } + ctx.data_mut(|d| d.insert_temp(key, held.clone())); + let Some(texture) = &held.texture else { + return; + }; + let ready = ctx.animate_bool_with_time(key.with("ready"), true, READY_FADE); + let alpha = (v * ready).clamp(0.0, 1.0); + if alpha <= 0.0 { + return; + } + let a = (alpha * 255.0).round(); + let k = (a * DIM).round() as u8; + // Premultiplied: the image fades in and dims at once. + let tint = Color32::from_rgba_premultiplied(k, k, k, a as u8); + painter.image( + texture.id(), + rect, + Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), + tint, + ); +} + +/// Drops what the card kept (call once its exit is over). +pub fn release(ctx: &Context, id: Id) { + let key = id.with("page-blur"); + if ctx.data(|d| d.get_temp::(key)).is_some() { + ctx.data_mut(|d| d.remove::(key)); + } +} + +/// The capture shrunk to an eighth and blurred: averages a few samples of each block, then two +/// separable box passes with clamped edges. +fn blurred(src: &ColorImage) -> ColorImage { + let [w, h] = src.size; + let sw = (w / SHRINK).max(1); + let sh = (h / SHRINK).max(1); + let mut px = vec![[0.0_f32; 3]; sw * sh]; + for y in 0..sh { + let (y0, y1) = (y * h / sh, ((y + 1) * h / sh).max(y * h / sh + 1)); + for x in 0..sw { + let (x0, x1) = (x * w / sw, ((x + 1) * w / sw).max(x * w / sw + 1)); + let mut sum = [0.0_f32; 3]; + let mut n = 0.0_f32; + for sy in (y0..y1).step_by(2) { + for sx in (x0..x1).step_by(2) { + let c = src.pixels[sy.min(h - 1) * w + sx.min(w - 1)].to_array(); + sum[0] += f32::from(c[0]); + sum[1] += f32::from(c[1]); + sum[2] += f32::from(c[2]); + n += 1.0; + } + } + px[y * sw + x] = [sum[0] / n, sum[1] / n, sum[2] / n]; + } + } + for _ in 0..PASSES { + px = box_pass(&px, sw, sh, true); + px = box_pass(&px, sw, sh, false); + } + let mut rgba = Vec::with_capacity(sw * sh * 4); + for p in &px { + rgba.push(p[0].round().clamp(0.0, 255.0) as u8); + rgba.push(p[1].round().clamp(0.0, 255.0) as u8); + rgba.push(p[2].round().clamp(0.0, 255.0) as u8); + rgba.push(255); + } + ColorImage::from_rgba_unmultiplied([sw, sh], &rgba) +} + +/// One box blur along a row (`horizontal`) or a column, edges clamped. +fn box_pass(src: &[[f32; 3]], w: usize, h: usize, horizontal: bool) -> Vec<[f32; 3]> { + let mut out = vec![[0.0_f32; 3]; src.len()]; + let (lines, len) = if horizontal { (h, w) } else { (w, h) }; + let at = |line: usize, i: usize| { + if horizontal { + line * w + i + } else { + i * w + line + } + }; + let taps = (2 * RADIUS + 1) as f32; + for line in 0..lines { + for i in 0..len { + let mut sum = [0.0_f32; 3]; + for d in 0..=2 * RADIUS { + let j = (i + d).saturating_sub(RADIUS).min(len - 1); + let p = src[at(line, j)]; + sum[0] += p[0]; + sum[1] += p[1]; + sum[2] += p[2]; + } + out[at(line, i)] = [sum[0] / taps, sum[1] / taps, sum[2] / taps]; + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_flat_page_stays_flat() { + let src = ColorImage::from_rgba_unmultiplied([64, 32], &[40, 80, 120, 255].repeat(64 * 32)); + let out = blurred(&src); + assert_eq!(out.size, [8, 4]); + assert!(out.pixels.iter().all(|c| c.to_array() == [40, 80, 120, 255])); + } + + #[test] + fn a_tiny_window_still_gives_one_pixel() { + let src = ColorImage::from_rgba_unmultiplied([3, 2], &[10, 10, 10, 255].repeat(6)); + assert_eq!(blurred(&src).size, [1, 1]); + } +} From 800993a30ae90a7b85bb2279121360589c40d668 Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:58:57 +0100 Subject: [PATCH 04/13] Add files via upload --- crates/anyps5-launcher/src/ui/pageblur.rs | 34 +++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/anyps5-launcher/src/ui/pageblur.rs b/crates/anyps5-launcher/src/ui/pageblur.rs index 86c78229..f8884d74 100644 --- a/crates/anyps5-launcher/src/ui/pageblur.rs +++ b/crates/anyps5-launcher/src/ui/pageblur.rs @@ -10,9 +10,11 @@ //! Until the capture arrives (one or two frames) and under High Contrast the caller keeps its flat //! scrim. +use std::sync::{Arc, Mutex}; + use egui::{ - Color32, ColorImage, Context, Event, Id, Painter, Rect, TextureHandle, TextureOptions, - UserData, ViewportCommand, pos2, + Color32, ColorImage, Context, Id, Painter, Rect, ScreenshotCallback, TextureHandle, + TextureOptions, ViewportCommand, pos2, }; /// How much the page is shrunk before it is blurred (the blur's strength grows with it). @@ -25,13 +27,15 @@ const DIM: f32 = 0.60; /// Seconds for the blur to fade in once the capture has arrived. const READY_FADE: f32 = 0.15; -/// Marks the capture this module asked for, so no other screenshot is taken for its own. -struct Tag; +/// Where the screenshot callback drops the capture (egui 0.37 answers with a one-shot callback, +/// not with an event, so each request has its own slot and no other screenshot is mistaken for it). +type Slot = Arc>>>; #[derive(Clone, Default)] struct Held { texture: Option, asked: bool, + slot: Slot, } /// Paints the blurred page over `rect` for the card `id`; `v` is the card's openness. @@ -44,20 +48,20 @@ pub fn paint(ctx: &Context, painter: &Painter, rect: Rect, id: Id, open: bool, v if open && !held.asked { // The card is at about 0 on its first frame, so the capture shows the page alone. held.asked = true; - ctx.send_viewport_cmd(ViewportCommand::Screenshot(UserData::new(Tag))); + let slot = held.slot.clone(); + let repaint = ctx.clone(); + ctx.send_viewport_cmd(ViewportCommand::Screenshot(ScreenshotCallback::new( + move |image| { + if let Ok(mut guard) = slot.lock() { + *guard = Some(image); + } + repaint.request_repaint(); + }, + ))); ctx.request_repaint(); } if held.asked && held.texture.is_none() { - let shot = ctx.input(|i| { - i.events.iter().find_map(|event| match event { - Event::Screenshot { - user_data, image, .. - } if user_data.data.as_ref().is_some_and(|d| d.is::()) => { - Some(image.clone()) - } - _ => None, - }) - }); + let shot = held.slot.lock().ok().and_then(|mut guard| guard.take()); if let Some(image) = shot { held.texture = Some(ctx.load_texture( "page-blur", From 8161f48dbf1fe11473fd06ef6cf1e4696e2d3bc2 Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:19:55 +0100 Subject: [PATCH 05/13] Add files via upload --- crates/anyps5-launcher/src/screens/dialog.rs | 12 ++-- crates/anyps5-launcher/src/ui/pageblur.rs | 73 +++++++++++++++++++- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/crates/anyps5-launcher/src/screens/dialog.rs b/crates/anyps5-launcher/src/screens/dialog.rs index 70bed15f..0982c95c 100644 --- a/crates/anyps5-launcher/src/screens/dialog.rs +++ b/crates/anyps5-launcher/src/screens/dialog.rs @@ -341,10 +341,14 @@ fn paint( ..shadows::toast() }, ); - let mut style = GlassStyle::glass(radius::LARGE, 0.6); - style.black *= v; - style.alpha = v; - paint_glass(ctx, &painter, rect, style); + // The card is a translucent pane over the blurred page; until that page has been + // captured (and under High Contrast) it is the usual glass panel. + if !pageblur::paint_card(ctx, &painter, rect, radius::LARGE, id, v) { + let mut style = GlassStyle::glass(radius::LARGE, 0.6); + style.black *= v; + style.alpha = v; + paint_glass(ctx, &painter, rect, style); + } let mut y = rect.min.y + CARD_PADDING; let x = rect.min.x + CARD_PADDING; diff --git a/crates/anyps5-launcher/src/ui/pageblur.rs b/crates/anyps5-launcher/src/ui/pageblur.rs index f8884d74..40d4f0da 100644 --- a/crates/anyps5-launcher/src/ui/pageblur.rs +++ b/crates/anyps5-launcher/src/ui/pageblur.rs @@ -12,11 +12,14 @@ use std::sync::{Arc, Mutex}; +use egui::epaint::RectShape; use egui::{ - Color32, ColorImage, Context, Id, Painter, Rect, ScreenshotCallback, TextureHandle, - TextureOptions, ViewportCommand, pos2, + Color32, ColorImage, Context, Id, Painter, Rect, ScreenshotCallback, Stroke, StrokeKind, + TextureHandle, TextureOptions, ViewportCommand, pos2, }; +use crate::ui::theme::{self, color}; + /// How much the page is shrunk before it is blurred (the blur's strength grows with it). const SHRINK: usize = 8; /// Box blur radius, in shrunk pixels, and the number of passes (two boxes are close to a gaussian). @@ -26,6 +29,11 @@ const PASSES: usize = 2; const DIM: f32 = 0.60; /// Seconds for the blur to fade in once the capture has arrived. const READY_FADE: f32 = 0.15; +/// How bright the blurred page shows through the card itself (the page around it is dimmed to +/// `DIM`, so the card reads as a lighter frosted pane). +const CARD_BRIGHT: f32 = 0.92; +/// How much of the card's blue tint lies over the blurred page (0 = clear glass, 1 = opaque). +const CARD_TINT: f32 = 0.38; /// Where the screenshot callback drops the capture (egui 0.37 answers with a one-shot callback, /// not with an event, so each request has its own slot and no other screenshot is mistaken for it). @@ -91,6 +99,67 @@ pub fn paint(ctx: &Context, painter: &Painter, rect: Rect, id: Id, open: bool, v ); } +/// Paints the card's own pane: the blurred page behind it, seen through a translucent blue tint. +/// Call after [`paint`] in the same frame. Returns `false` when there is no blurred page to show +/// yet (or under High Contrast), so the caller draws its usual panel instead. +pub fn paint_card( + ctx: &Context, + painter: &Painter, + rect: Rect, + corner: f32, + id: Id, + v: f32, +) -> bool { + if theme::high_contrast() { + return false; + } + let key = id.with("page-blur"); + let Some(held) = ctx.data(|d| d.get_temp::(key)) else { + return false; + }; + let Some(texture) = &held.texture else { + return false; + }; + let ready = ctx.animate_bool_with_time(key.with("ready"), true, READY_FADE); + let alpha = (v * ready).clamp(0.0, 1.0); + if alpha <= 0.0 { + return true; + } + let screen = ctx.content_rect(); + let uv = Rect::from_min_max( + pos2( + (rect.min.x - screen.min.x) / screen.width(), + (rect.min.y - screen.min.y) / screen.height(), + ), + pos2( + (rect.max.x - screen.min.x) / screen.width(), + (rect.max.y - screen.min.y) / screen.height(), + ), + ); + let a = (alpha * 255.0).round(); + let k = (a * CARD_BRIGHT).round() as u8; + // Premultiplied: the page fades in and dims at once, rounded like the card. + let tint = Color32::from_rgba_premultiplied(k, k, k, a as u8); + painter.add(RectShape::filled(rect, corner, tint).with_texture(texture.id(), uv)); + // The card's blue, left see-through. + let wash = if theme::is_light() { + theme::scrim(CARD_TINT * alpha) + } else { + Color32::from_rgba_unmultiplied(12, 26, 58, (CARD_TINT * alpha * 255.0).round() as u8) + }; + painter.rect_filled(rect, corner, wash); + painter.add(RectShape::stroke( + rect, + corner, + Stroke::new( + 1.0 / ctx.pixels_per_point(), + theme::fade(color::hairline_glass(), alpha), + ), + StrokeKind::Inside, + )); + true +} + /// Drops what the card kept (call once its exit is over). pub fn release(ctx: &Context, id: Id) { let key = id.with("page-blur"); From faa72958e1443b00a9daed73efc25ac078fc49cc Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:40:05 +0100 Subject: [PATCH 06/13] Add files via upload --- crates/anyps5-launcher/src/app/panels.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/anyps5-launcher/src/app/panels.rs b/crates/anyps5-launcher/src/app/panels.rs index c731aa0a..c2e2b7d2 100644 --- a/crates/anyps5-launcher/src/app/panels.rs +++ b/crates/anyps5-launcher/src/app/panels.rs @@ -1019,13 +1019,15 @@ impl PanelHost { let events = self.take_events(wanted); let kind = QuitKind::CancelUnlock; if cx.model.cancel_ask.is_none() { + // The question is answered (or gone): the card still has to play its exit, so this + // does not return before `show`. Returning here left the panel in its closing state + // with nothing drawing it, so it vanished at once and, on the next question, showed + // for one frame at full strength, left again and only then opened. self.cancel_unlock.close(); if wanted { cx.model.dismiss_overlay(); } - return; - } - if wanted && !self.cancel_unlock.is_active() { + } else if wanted && !self.cancel_unlock.is_active() { self.cancel_unlock.open(kind); } else if !wanted && self.cancel_unlock.is_active() { if cx.model.overlay.is_some_and(Overlay::is_side_panel) { From 72b2d9bfbb4d17d6441276972eb0d3ed6641e2c9 Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:54:10 +0100 Subject: [PATCH 07/13] Add files via upload --- crates/anyps5-launcher/src/ui/background.rs | 7 +++++ crates/anyps5-launcher/src/ui/pageblur.rs | 7 ++--- crates/anyps5-launcher/src/ui/theme.rs | 30 +++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/crates/anyps5-launcher/src/ui/background.rs b/crates/anyps5-launcher/src/ui/background.rs index 25f9b50f..5c12b5dd 100644 --- a/crates/anyps5-launcher/src/ui/background.rs +++ b/crates/anyps5-launcher/src/ui/background.rs @@ -184,6 +184,13 @@ impl Backdrop { let light = self.theme.is_light() && !self.window_glass; let t = ctx.animate_to("backdrop/light", f32::from(light), Motion::Fade); crate::ui::theme::set_light(t); + // The question cards wear the colour of the look on screen (Game Colors: the game's). + let tones = if self.theme == Theme::GameColors { + self.game_palette + } else { + self.theme.palette() + }; + crate::ui::theme::set_card_wash(tones.card_wash()); let dt = f64::from(ctx.input(|i| i.unstable_dt)); let paused = self.paused || ctx.reduced_motion() || self.window_glass; self.clock.advance(dt, paused); diff --git a/crates/anyps5-launcher/src/ui/pageblur.rs b/crates/anyps5-launcher/src/ui/pageblur.rs index 40d4f0da..909de9d0 100644 --- a/crates/anyps5-launcher/src/ui/pageblur.rs +++ b/crates/anyps5-launcher/src/ui/pageblur.rs @@ -32,7 +32,7 @@ const READY_FADE: f32 = 0.15; /// How bright the blurred page shows through the card itself (the page around it is dimmed to /// `DIM`, so the card reads as a lighter frosted pane). const CARD_BRIGHT: f32 = 0.92; -/// How much of the card's blue tint lies over the blurred page (0 = clear glass, 1 = opaque). +/// How much of the card's tint (the look's own colour) lies over the blurred page (0 = clear glass, 1 = opaque). const CARD_TINT: f32 = 0.38; /// Where the screenshot callback drops the capture (egui 0.37 answers with a one-shot callback, @@ -141,11 +141,12 @@ pub fn paint_card( // Premultiplied: the page fades in and dims at once, rounded like the card. let tint = Color32::from_rgba_premultiplied(k, k, k, a as u8); painter.add(RectShape::filled(rect, corner, tint).with_texture(texture.id(), uv)); - // The card's blue, left see-through. + // The card's tint, left see-through: the colour of the look on screen, not a fixed blue. let wash = if theme::is_light() { theme::scrim(CARD_TINT * alpha) } else { - Color32::from_rgba_unmultiplied(12, 26, 58, (CARD_TINT * alpha * 255.0).round() as u8) + let [r, g, b] = theme::card_wash().map(|c| (c * 255.0).round() as u8); + Color32::from_rgba_unmultiplied(r, g, b, (CARD_TINT * alpha * 255.0).round() as u8) }; painter.rect_filled(rect, corner, wash); painter.add(RectShape::stroke( diff --git a/crates/anyps5-launcher/src/ui/theme.rs b/crates/anyps5-launcher/src/ui/theme.rs index 7411a580..42b88bee 100644 --- a/crates/anyps5-launcher/src/ui/theme.rs +++ b/crates/anyps5-launcher/src/ui/theme.rs @@ -178,6 +178,30 @@ pub fn with_light(light: bool, f: impl FnOnce() -> R) -> R { out } +/// The tint of the question cards (the cancel / quit popups): the active look's own colour, as the +/// three channels of a dark tone. Set by the backdrop each frame from the palette on screen (Game +/// Colors included); read by [`crate::ui::pageblur`]. Starts as Cosmic's blue. +static CARD_WASH: [AtomicU32; 3] = [AtomicU32::new(0), AtomicU32::new(0), AtomicU32::new(0)]; + +pub fn set_card_wash(rgb: [f32; 3]) { + for (slot, v) in CARD_WASH.iter().zip(rgb) { + slot.store(v.clamp(0.0, 1.0).to_bits(), Ordering::Relaxed); + } +} + +/// The card tint as straight sRGB floats (0..=1). +pub fn card_wash() -> [f32; 3] { + let [r, g, b] = &CARD_WASH; + let load = |a: &AtomicU32| f32::from_bits(a.load(Ordering::Relaxed)); + let wash = [load(r), load(g), load(b)]; + // Nothing set yet (all zero bits is 0.0): Cosmic's tint, as before. + if wash == [0.0, 0.0, 0.0] { + Theme::Cosmic.palette().card_wash() + } else { + wash + } +} + /// High Contrast (docs/DESIGN.md 3.14): brighter ink and edges and no frosted glass. Set by the /// shell from the preference; read by the colour tokens and the glass. static HIGH_CONTRAST: AtomicBool = AtomicBool::new(false); @@ -366,6 +390,12 @@ pub struct Palette { } impl Palette { + /// The tint a question card wears over the blurred page: the look's deep tone lifted most of + /// the way to its middle one, so a card on Sunset is wine, on Aurora teal, on Cosmic blue. + pub fn card_wash(&self) -> [f32; 3] { + let at = |i: usize| self.deep[i] + (self.middle[i] - self.deep[i]) * 0.6; + [at(0), at(1), at(2)] + } pub fn deep_color(&self) -> Color32 { rgb(self.deep[0], self.deep[1], self.deep[2]) } From b7d82e1ad63914c2aa017fa8f324a9e1f871b843 Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:18:32 +0100 Subject: [PATCH 08/13] Add files via upload --- docs/DESIGN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index bbf974d2..af64e537 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1663,11 +1663,11 @@ member's volume (0 to 200 %) stays a stepper. Every question that stands alone is one card in the middle of the dimmed page, like a console's dialog: the uninstall question, the restore question, Close Game, the missing toolkit release, Delete in Captures and every question of the online area (Block, Remove Friend, Unsend, Leave Party, Remove from Party, Leave Conversation). One component draws them (`dialog::draw`; `side::draw` hands a `question` view to it), so they look and answer the same. -- **The page behind** is dimmed with black 0.55 (a local constant); the card is glass (`radius::LARGE` 20, black 0.6, the toast's shadow), 560 wide (less on a narrow window: the screen minus 2 x 24), padding 24. The card opens with the overlay spring: a fade and a rise of 12 dp (a fade only under "Reduced"). A click on the dimmed page does nothing and plays no cue, so nothing is answered by accident. The card leaves with the same spring and fade; its buttons stay on it while it leaves but no longer answer. The one exception to the title being the question is the missing toolkit release, whose title is the game's name (the sentence under it asks). A question's "no" is flagged on its row (`Row::refusal`), never recognised by its id. +- **The page behind** is dimmed with black 0.55 (a local constant); the card is glass (`radius::LARGE` 20, black 0.6, the toast's shadow) and, over the blurred page, wears the colour of the look on screen: `Palette::card_wash` (the deep tone lifted 60 % of the way to the middle one: blue on Cosmic, teal on Aurora, wine on Sunset, copper on Ember, petrol on Tide; on Game Colors the palette the selected game gives), which the backdrop publishes every frame (`theme::set_card_wash`) and `pageblur::paint_card` lays at 0.38. Daylight keeps its paper veil (the ink is dark) and High Contrast the flat panel. It is 560 wide (less on a narrow window: the screen minus 2 x 24), padding 24. The card opens with the overlay spring: a fade and a rise of 12 dp (a fade only under "Reduced"). A click on the dimmed page does nothing and plays no cue, so nothing is answered by accident. The card leaves with the same spring and fade; its buttons stay on it while it leaves but no longer answer. The one exception to the title being the question is the missing toolkit release, whose title is the game's name (the sentence under it asks). A question's "no" is flagged on its row (`Row::refusal`), never recognised by its id. - **Content**, top to bottom with 8 between the lines: the title (28 light, `text`; the question in sentence case: "Remove this installation?", "Restore this backup?", "Close the game?", "Block Lumen?"; at most 3 lines), the sentence (17 regular, `text_label`, leading 1.45; it wraps, in Turkish too, and is bound at 8 lines), a quiet line in `text_detail` 14 only when a button carries a state (the download under way), then 24 and the buttons. Nothing else. - **Buttons** are `ConsoleButton` Large (54), 12 apart, in one row of equal widths when the widest label fits that many times with the gaps, and one under another (each the full width) when it does not. Order is SYSTEM.md 3: the primary or destructive answer first, **Cancel last**. The destructive answer is a secondary button in the danger ink, never a fill; only a question with no destructive answer has a primary (the first answer that is not a refusal: Restore, Download v0.1.2). Cancel, Keep Playing and "Follow Global Setting" are secondary. - **Focus** opens on the safe answer (Cancel, Keep Playing) and on Download where the way on changes nothing. Left and Up go to the previous button, Right and Down to the next (a bump at the ends), Select (Enter, A) runs the focused one, Back (Esc, B) cancels. The ring shows only for keys and the pad. The card brings no hint bar: Cancel is on screen, so "Back" is not repeated (SYSTEM.md 13), and Select is the button in focus. -- Gallery: `confirm` (uninstall), `restore`, `toolkit-missing`, `captures-confirm` and `friends-confirm*`. +- Gallery: `confirm` (uninstall), `restore`, `toolkit-missing`, `captures-confirm` and `friends-confirm*`; `home-cancel-ask` with `--theme=cosmic|aurora|sunset|ember|tide|daylight` shows the card's tint on each look. Opened by "..." or `O` / Y / Menu / Options. Title = game name, subtitle `gameSettings`, rows below. (S-game-settings.) Rows in order, with the condition for each: From b7fdfe7810d103c9ee0b391c44c9ed51fdcab8bb Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:37:13 +0100 Subject: [PATCH 09/13] Add files via upload --- crates/anyps5-launcher/src/screens/settings.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/anyps5-launcher/src/screens/settings.rs b/crates/anyps5-launcher/src/screens/settings.rs index b9fa9d79..abdf355c 100644 --- a/crates/anyps5-launcher/src/screens/settings.rs +++ b/crates/anyps5-launcher/src/screens/settings.rs @@ -2656,7 +2656,11 @@ fn draw( area.max, ); let mut child = ui.new_child(UiBuilder::new().max_rect(list_rect)); - child.set_opacity(fade_in.clamp(0.0, 1.0)); + // Multiplied, not set: the panel fades its whole content (title, rail, rows) with its own + // openness, and `set_opacity` would replace that fade with the rows' own. The rows then stayed + // fully opaque while the page closed (the rest of the panel was already gone) and vanished in + // the last frame. + child.multiply_opacity(fade_in.clamp(0.0, 1.0)); let options = ListOptions { accent, language, From 7f8d79409dd7e3ce06a9f0fd055bb45e78903eb0 Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:57:19 +0100 Subject: [PATCH 10/13] Add files via upload --- crates/anyps5-launcher/src/ui/chrome.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/anyps5-launcher/src/ui/chrome.rs b/crates/anyps5-launcher/src/ui/chrome.rs index 57c52127..95e8bfcb 100644 --- a/crates/anyps5-launcher/src/ui/chrome.rs +++ b/crates/anyps5-launcher/src/ui/chrome.rs @@ -1781,7 +1781,16 @@ pub fn fullscreen_panel( add_contents: impl FnOnce(&mut Ui, f32) -> R, ) -> Option { let id = Id::unique(id); - let v = ctx.animate_to((id, "open"), if open { 1.0 } else { 0.0 }, Motion::Overlay); + // `animate_from_to`, not `animate_to`: a panel is only drawn while it is active, so its tracker + // is born on the very frame it opens, and `animate_to` starts a new tracker at its target (no + // entrance). The first opening (the "..." of a game, say) then appeared at once; from the + // second on the tracker existed, parked at 0, and the spring played. + let v = ctx.animate_from_to( + (id, "open"), + 0.0, + if open { 1.0 } else { 0.0 }, + Motion::Overlay, + ); if v <= 0.001 && !open { return None; } From 809f50540914cf6583069366dc7227a84401d11d Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:32:32 +0100 Subject: [PATCH 11/13] Add files via upload --- crates/anyps5-launcher/src/ui/mod.rs | 2 +- crates/anyps5-launcher/src/ui/pageblur.rs | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/anyps5-launcher/src/ui/mod.rs b/crates/anyps5-launcher/src/ui/mod.rs index 5d13c7e6..c6468f70 100644 --- a/crates/anyps5-launcher/src/ui/mod.rs +++ b/crates/anyps5-launcher/src/ui/mod.rs @@ -51,9 +51,9 @@ pub mod nav_bindings; pub mod nudge; pub mod os_emoji; pub mod osk; -pub mod pageblur; pub mod pad_hub; pub mod pad_info; +pub mod pageblur; pub mod press; pub mod ps_button; pub mod reveal; diff --git a/crates/anyps5-launcher/src/ui/pageblur.rs b/crates/anyps5-launcher/src/ui/pageblur.rs index 909de9d0..2bcb231e 100644 --- a/crates/anyps5-launcher/src/ui/pageblur.rs +++ b/crates/anyps5-launcher/src/ui/pageblur.rs @@ -71,11 +71,8 @@ pub fn paint(ctx: &Context, painter: &Painter, rect: Rect, id: Id, open: bool, v if held.asked && held.texture.is_none() { let shot = held.slot.lock().ok().and_then(|mut guard| guard.take()); if let Some(image) = shot { - held.texture = Some(ctx.load_texture( - "page-blur", - blurred(&image), - TextureOptions::LINEAR, - )); + held.texture = + Some(ctx.load_texture("page-blur", blurred(&image), TextureOptions::LINEAR)); } } ctx.data_mut(|d| d.insert_temp(key, held.clone())); @@ -245,7 +242,11 @@ mod tests { let src = ColorImage::from_rgba_unmultiplied([64, 32], &[40, 80, 120, 255].repeat(64 * 32)); let out = blurred(&src); assert_eq!(out.size, [8, 4]); - assert!(out.pixels.iter().all(|c| c.to_array() == [40, 80, 120, 255])); + assert!( + out.pixels + .iter() + .all(|c| c.to_array() == [40, 80, 120, 255]) + ); } #[test] From 76f737dced4bad92e8ccdaa22e8933dc69cc5452 Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:32:59 +0100 Subject: [PATCH 12/13] Delete .github/workflows/ci-manual-win.yml --- .github/workflows/ci-manual-win.yml | 53 ----------------------------- 1 file changed, 53 deletions(-) delete mode 100644 .github/workflows/ci-manual-win.yml diff --git a/.github/workflows/ci-manual-win.yml b/.github/workflows/ci-manual-win.yml deleted file mode 100644 index 72645929..00000000 --- a/.github/workflows/ci-manual-win.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: ci-manual-win - -# Solo avvio manuale (scheda Actions -> ci -> Run workflow). Nessun trigger su push o pull request. -on: - workflow_dispatch: - -permissions: - contents: read - -env: - CARGO_TERM_COLOR: always - -# The online crates come from anyplaynet/network, checked out beside the launcher at the commit in -# network.ref (one full SHA). -defaults: - run: - working-directory: launcher - -jobs: - build: - runs-on: windows-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - path: launcher - - id: network-pin - name: Read the pinned network revision - shell: bash - run: | - ref=$(tr -d '[:space:]' < "$GITHUB_WORKSPACE/launcher/network.ref") - [[ $ref =~ ^[0-9a-f]{40}$ ]] || { echo "network.ref must hold one full commit SHA" >&2; exit 1; } - echo "ref=$ref" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - repository: anyplaynet/network - ref: ${{ steps.network-pin.outputs.ref }} - path: network - - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 - with: - toolchain: stable - targets: x86_64-pc-windows-msvc - - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 - with: - workspaces: launcher - - name: Release build - run: cargo build --release --target x86_64-pc-windows-msvc - - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: anyplay-launcher-windows-x64 - path: launcher/target/x86_64-pc-windows-msvc/release/anyplay-launcher.exe - if-no-files-found: error From 3591332ff9546f0f25a5f3c3c2bca1bc8aff1177 Mon Sep 17 00:00:00 2001 From: Lenny <142605298+Lenny4250gh@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:36:41 +0100 Subject: [PATCH 13/13] Add files via upload --- crates/anyps5-launcher/src/app/panels.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/anyps5-launcher/src/app/panels.rs b/crates/anyps5-launcher/src/app/panels.rs index c2e2b7d2..1f8110f8 100644 --- a/crates/anyps5-launcher/src/app/panels.rs +++ b/crates/anyps5-launcher/src/app/panels.rs @@ -1052,14 +1052,19 @@ impl PanelHost { /// The question before the window closes while games are unlocked or removed. fn quit_ui(&mut self, cx: &mut OverlayCx<'_>, wanted: bool) { let events = self.take_events(wanted); - let Some(kind) = cx.model.quit_ask else { + // Once the question is answered `quit_ask` is gone, but the card still has to play its + // exit, so this does not return before `show` (the card draws from what it showed last; + // the kind only matters while it is open). Returning here left the panel in its closing + // state with nothing drawing it: it vanished at once and, on the next question, showed + // for one frame at full strength, left again and only then opened. + let asked = cx.model.quit_ask; + let kind = asked.unwrap_or(QuitKind::Unlocking); + if asked.is_none() { self.quit.close(); if wanted { cx.model.dismiss_overlay(); } - return; - }; - if wanted && !self.quit.is_active() { + } else if wanted && !self.quit.is_active() { self.quit.open(kind); } else if !wanted && self.quit.is_active() { if cx.model.overlay.is_some_and(Overlay::is_side_panel) {