Files
Unlucky-Launcher/crates/app/src/theme_runtime.rs
T
AskaEth bc41dfcf7d
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

153 lines
6.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 主题与外观的运行期逻辑(从 `ui_bind.rs` 拆出,守住单文件 ≤400 行)。
//!
//! 全是 `impl Inner` 的方法:配置热切轮询、系统主题对齐、外观令牌下发、背景图/模糊层重建。
//! 拆出来的边界是「时间驱动的状态维护」——`ui_bind.rs` 只留「建窗口 + 每拍驱动 + 灌属性」。
use slint::ComponentHandle;
use crate::bg_image::{self, BackgroundLayers};
use crate::conv;
use crate::prefs::{AppearancePrefs, BgFill, ThemeMode};
use crate::toast::ToastKind;
use crate::ui_bind::{
BackgroundKey, Inner, CONFIG_POLL_TICKS, SYSTEM_THEME_POLL_TICKS, TILE_REGENERATE_STEP,
};
impl Inner {
pub(crate) fn poll_settings(&self) {
if !self.ticks.get().is_multiple_of(CONFIG_POLL_TICKS) {
return;
}
let Some((appearance, theme)) = self.bridge.borrow_mut().reload_if_changed() else {
return;
};
{
let mut state = self.state.borrow_mut();
state.prefs.appearance = appearance;
state.prefs.theme = theme;
}
self.apply_theme();
self.apply_appearance();
}
/// 系统主题对齐:启动时先读一次真实系统主题(避免 auto 首帧按浅色画再翻黑);
/// 之后仅在「auto + 后端不认识系统主题」(`Theme.system-theme-known == false`)时兜底轮询。
pub(crate) fn poll_system_theme(&self) {
let auto = matches!(self.state.borrow().prefs.theme, ThemeMode::Auto);
let backend_known = self.ui.get_system_theme_known();
let due = !self.system_theme_probed.get()
|| (auto && !backend_known && self.ticks.get().is_multiple_of(SYSTEM_THEME_POLL_TICKS));
if !due {
return;
}
self.system_theme_probed.set(true);
let value = match unlucky_core::platform::theme_probe::probe() {
unlucky_core::platform::theme_probe::SystemTheme::Light => 1,
unlucky_core::platform::theme_probe::SystemTheme::Dark => 2,
unlucky_core::platform::theme_probe::SystemTheme::Unknown => 0,
};
if self.ui.get_system_theme() != value {
self.ui.set_system_theme(value);
}
}
/// 启动时的系统主题对齐(首帧前调用一次)。
pub(crate) fn align_system_theme(&self) {
self.poll_system_theme();
}
/// 主题三态 → 根组件属性;`ui/main.slint` 里的 changed 处理器再赋给 `Theme` 全局
/// (auto 时 Theme 内部读系统的 `Palette.color-scheme` 与本进程探测到的系统主题)。
pub(crate) fn apply_theme(&self) {
let mode = self.state.borrow().prefs.theme;
self.ui.set_theme_mode(conv::theme_mode(mode));
}
/// 外观(高级)四件 → 根组件属性(进而写进 `Theme` 全局)。
///
/// 背景图只在「开关打开 + 路径非空 + 换了输入」时才重新解码/生成模糊层。
pub(crate) fn apply_appearance(&self) {
let appearance = self.state.borrow().prefs.appearance.clone();
self.ui.set_bg_fill(conv::bg_fill(appearance.bg_fill));
self.ui.set_bg_dim(appearance.bg_dim.clamp(0.0, 0.9));
self.ui
.set_panel_alpha(appearance.panel_alpha.clamp(0.4, 1.0));
self.ui.set_glass(appearance.glass);
self.ui
.set_accent_color(conv::accent_color(&appearance.accent_color));
self.ui
.set_accent_hex(appearance.accent_color.clone().into());
self.refresh_background(&appearance);
}
/// 按需重建背景图 + 模糊层。
fn refresh_background(&self, appearance: &AppearancePrefs) {
if !appearance.bg_active() {
let had = self.background_key.borrow_mut().take().is_some();
if had {
self.background.replace(BackgroundLayers::default());
self.ui.set_background_image(slint::Image::default());
self.ui.set_background_blur(slint::Image::default());
}
return;
}
let key = BackgroundKey {
path: appearance.bg_image.clone(),
fill: appearance.bg_fill,
viewport: self.tile_viewport(appearance.bg_fill),
};
if self.background_key.borrow().as_ref() == Some(&key) {
return;
}
self.background_key.replace(Some(key.clone()));
// 模糊层按窗口物理尺寸放大(软件渲染器不做图片缩放插值,见 bg_image::prepare);
// tile 之外的模式窗口缩放不重建画布,模糊层被拉伸也无感。
let real_viewport = self.window_physical_size();
match bg_image::prepare(&key.path, key.fill, real_viewport) {
Some(layers) => {
self.ui.set_background_image(layers.image.clone());
self.ui.set_background_blur(layers.blur.clone());
self.background.replace(layers);
}
None => {
// 载入失败(路径不存在 / 不是 PNG):清空图层并提示一次,别留上一张图的残影
self.background.replace(BackgroundLayers::default());
self.ui.set_background_image(slint::Image::default());
self.ui.set_background_blur(slint::Image::default());
self.state.borrow_mut().toasts.push(
ToastKind::Warn,
"背景图载入失败(当前只支持 PNG,且路径需存在)",
);
}
}
}
/// tile 画布的窗口尺寸(量化到 [`TILE_REGENERATE_STEP`],避免拖动窗口时反复重算)。
fn tile_viewport(&self, fill: BgFill) -> (u32, u32) {
if fill != BgFill::Tile {
return (0, 0);
}
let (width, height) = self.window_physical_size();
(
width / TILE_REGENERATE_STEP * TILE_REGENERATE_STEP,
height / TILE_REGENERATE_STEP * TILE_REGENERATE_STEP,
)
}
/// 窗口物理像素尺寸(取不到时 (0, 0),tile 退化为单块)。
fn window_physical_size(&self) -> (u32, u32) {
let size = self.ui.window().size();
(size.width, size.height)
}
/// 落盘当前偏好(主题 + 外观四件)。
pub(crate) fn persist_settings(&self) {
let state = self.state.borrow();
self.bridge
.borrow_mut()
.persist(&state.prefs.appearance, state.prefs.theme);
}
}