mini player + dark glass: eight shapes run by the hidden main as the one brain, dissolving covers and accents, drag from anywhere, and the space between the panes turned to real acrylic - with the webview flags that keep rest frames honest

This commit is contained in:
KeelrLeek committed 2026-08-26 07:56:00 -05:00
1 parent 9b84b30aef
commit dc7ea16b22
33 files changed
+2432 -5

No files matched your search

+112 -1
View File
@@ -1,4 +1,4 @@
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Serialize};
use tauri::{Emitter, Manager, State};
use soulseek_client::{Client, Config, DownloadState, DownloadUpdate, Event};
@@ -78,6 +78,8 @@ pub struct Settings {
pub volume_normalization: bool,
/// Word-by-word lyric fill — experimental, default off.
pub word_timing: bool,
/// Dark-glass acrylic backdrop, default off.
pub glass_mode: bool,
}
#[tauri::command]
@@ -117,6 +119,7 @@ pub async fn get_settings(
download_source_prefs: cfg.download_source_prefs,
volume_normalization: config::volume_normalization(&cfg),
word_timing: config::word_timing(&cfg),
glass_mode: config::glass_mode(&cfg),
username: cfg.username,
})
}
@@ -145,6 +148,7 @@ pub async fn set_settings(
download_source_prefs: Option<config::SourcePrefs>,
volume_normalization: Option<bool>,
word_timing: Option<bool>,
glass_mode: Option<bool>,
) -> Result<(), String> {
let mut cfg = config::load(&app);
let library_folder_moved = download_dir
@@ -216,6 +220,9 @@ pub async fn set_settings(
if word_timing.is_some() {
cfg.word_timing = word_timing;
}
if glass_mode.is_some() {
cfg.glass_mode = glass_mode;
}
config::save(&app, &cfg);
// A new library folder is a new library. Re-base immediately rather than
@@ -4857,6 +4864,108 @@ pub fn player_control(
Ok(())
}
/// Enter mini mode: show the pre-built mini window (declared in
/// tauri.conf.json, created hidden at startup — runtime window creation
/// from a command half-initialises on Windows) and hide the main one. The
/// main window is HIDDEN, never closed: its frontend stays alive as the one
/// brain the mini remote-controls (see src/lib/miniBridge.svelte.js).
#[tauri::command]
pub async fn mini_open(app: tauri::AppHandle, width: f64, height: f64) -> Result<(), String> {
let mini = app
.get_webview_window("mini")
.ok_or_else(|| "mini window missing from config".to_string())?;
let _ = mini.set_size(tauri::LogicalSize::new(width, height));
mini.show().map_err(|e| e.to_string())?;
let _ = mini.set_focus();
if let Some(main) = app.get_webview_window("main") {
let _ = main.hide();
}
Ok(())
}
/// Leave mini mode: bring the full app back and tuck the mini away warm
/// (hidden, not closed — reopening is instant and its mirror stays hot).
#[tauri::command]
pub async fn mini_close(app: tauri::AppHandle) -> Result<(), String> {
if let Some(main) = app.get_webview_window("main") {
let _ = main.show();
let _ = main.set_focus();
}
if let Some(mini) = app.get_webview_window("mini") {
let _ = mini.hide();
}
Ok(())
}
/// The OS half of dark-glass mode: acrylic blur behind the calling window
/// (the window itself is transparent; the CSS half — :root.glass — thins the
/// window canvas so the blur shows through).
///
/// NOT tauri's Effect::Acrylic: on Win11 22H2+ that picks the DWMSBT system
/// backdrop, which Windows SUSPENDS whenever the maximized webview gets
/// promoted to a hardware overlay — every hover animation or compositor
/// scroll blinked the glass dark for a beat (Kyle, 2026-08-26). The legacy
/// accent-policy acrylic below is composed with the window itself and holds
/// through interaction.
#[tauri::command]
pub fn window_glass(window: tauri::WebviewWindow, on: bool) -> Result<(), String> {
// Clear any system backdrop a previous toggle/build may have applied.
let _ = window.set_effects(None::<tauri::utils::config::WindowEffectsConfig>);
let hwnd = window.hwnd().map_err(|e| e.to_string())?.0 as *mut std::ffi::c_void;
#[repr(C)]
struct AccentPolicy {
state: u32,
flags: u32,
gradient: u32, // 0xAABBGGRR
animation: u32,
}
#[repr(C)]
struct AttribData {
attrib: u32,
data: *mut std::ffi::c_void,
size: usize,
}
#[allow(non_snake_case)]
#[link(name = "kernel32")]
extern "system" {
fn GetModuleHandleA(name: *const u8) -> *mut std::ffi::c_void;
fn GetProcAddress(
module: *mut std::ffi::c_void,
name: *const u8,
) -> *mut std::ffi::c_void;
}
type Swca = unsafe extern "system" fn(*mut std::ffi::c_void, *mut AttribData) -> i32;
unsafe {
let user32 = GetModuleHandleA(b"user32.dll\0".as_ptr());
let f = if user32.is_null() {
std::ptr::null_mut()
} else {
GetProcAddress(user32, b"SetWindowCompositionAttribute\0".as_ptr())
};
if f.is_null() {
return Err("SetWindowCompositionAttribute unavailable".into());
}
let f: Swca = std::mem::transmute(f);
let mut policy = AccentPolicy {
state: if on { 4 } else { 0 }, // ACCENT_ENABLE_ACRYLICBLURBEHIND / ACCENT_DISABLED
flags: 0,
// A whisper of black in the OS layer (acrylic treats alpha 0 as
// "don't draw"); the real darkening is the CSS canvas tint.
gradient: if on { 0x2E00_0000 } else { 0 },
animation: 0,
};
let mut data = AttribData {
attrib: 0x13, // WCA_ACCENT_POLICY
data: &mut policy as *mut _ as _,
size: std::mem::size_of::<AccentPolicy>(),
};
f(hwnd, &mut data);
}
Ok(())
}
#[cfg(test)]
mod hero_processing_tests {
use super::*;
@@ -4937,3 +5046,5 @@ mod hero_processing_tests {
assert_eq!(trimmed.dimensions(), (256, 120));
}
}