feat(m179): 外观分组接真语言并拆成五段文件(只接管子,不补文案)
M172 审计:`pages/appearance/**` 中文界面下真正本地化 0/27,两个根因各断一刀。
① 拆文件(纯搬移):`section.rs` 395 行 → `section/` 五个文件,行数守卫只剩 5 行余量,
不拆就加不了任何东西。
- `section/mod.rs`(壳:section() + card() + 五个行控件 + 取词口 t)
- `section/window.rs`(① 整窗 α + ② 圆角,含 notice/说明段)
- `section/panel.rs`(③ 面板 α)、`section/bg_image.rs`(④⑤⑥⑦⑧)、
`section/colour_source.rs`(⑨)
取证:每段与原文件做「去空白/去尾逗号」diff,4/4 相同;只有五处机械改写
(`rows = rows.push(…)`→`rows.push(…)`、`t(Key::X)`→`t(Key::X, lang)`、
`super::notice`→`super::super::notice`、一处 `.into()`、rustfmt 换行)。
② 语言接到渲染层:`settings::Body` 从 `fn(&Ui, States)` 变成 `fn(&Ui, States, Lang)`,
`form` 传 `language.effective`,`settings/appearance.rs` 收 `lang` 转给
`pages::appearance::section(ui, state, lang)`,`section::t(key, lang)` 取词。
选「接进唯一那个槽位」而不是再给 `form` 加一条按下标分发的 `else if`:`settings/mod.rs`
贴着 400 行硬顶(M195 还要在此落地),这条路净增 0 行且不可能与分发表漂开。
验收点:`pages/appearance/**` 里已经搜不到 `Lang::En`。
③ 能变红的守卫:`the_appearance_group_speaks_chinese_when_the_interface_does`
—— 走 `settings::view` 真分发渲染一帧,断言取词记录里含汉字、英文那帧不含汉字且同一 key
中英译文不同。取证口是 `appearance::probe`(`#[cfg(test)]` thread_local):iced 0.14
没有可遍历控件树的公开接口(`Widget::children()` 返回状态 Tree;`operate` 要 Renderer),
所以取证点退到「字进树之前」的唯一取词口。变异证明:把 `t()` 改回 `Lang::En.catalog()`
⇒ 该用例红(失败信息逐条列出四条英文);还原 ⇒ 绿。
只接语言管子,不补文案:本组其余英文仍是字面量(行号见 M179 报告)。真实窗口截图、
macOS/Linux 字体差异未验。
This commit is contained in:
1 parent
e386cfbeee
commit
b457f0acc8
14 files changed
+745
-422
No files matched your search
@@ -20,7 +20,7 @@
|
||||
//! | [`background`] | 背景图解码(png/jpeg/bmp)、平铺网格、暗化系数、可读诊断 | 读盘一次(`load`) |
|
||||
//! | `palette` | D38 三级优先序的调用侧 + 提示文案 | 无(像素由 `background` 给) |
|
||||
//! | `state` | 运行期状态:把上面三件的输入输出连起来 | 只在 `refresh_background` |
|
||||
//! | `section` | 设置页「外观」分组(真控件) | 无 |
|
||||
//! | `section/` | 设置页「外观」分组(真控件):壳 + 五段控件,M179 从单文件拆开 | 无 |
|
||||
//! | `picture` | 背景层 Element:真图直出(Cover/Contain/Tile)+ 暗化叠层 | 无 |
|
||||
//!
|
||||
//! ## 偏离登记(D30 要求逐条写清)
|
||||
@@ -65,10 +65,12 @@ pub mod background;
|
||||
|
||||
mod palette;
|
||||
mod picture;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod probe;
|
||||
mod section;
|
||||
mod state;
|
||||
|
||||
/// 状态提示的 **tonal notice**(`section.rs` 的 400 行硬顶把它放在模块根)。
|
||||
/// 状态提示的 **tonal notice**(行数硬顶把 `section/` 撑开之后它仍放在模块根)。
|
||||
///
|
||||
/// `SurfaceContainerHighest` 底 + `ShapeScale::Small` 圆角 + 两侧 `X2` 内边距,文字 `OnSurfaceVariant`。
|
||||
/// 用它而不是「换色的正文」:容器才是 M3 表达「当前状态」的形态,也和说明段在**形态**上分开
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//! **测试专用**:记录「画一帧时从词表取出的每一条译文」。
|
||||
//!
|
||||
//! ## 为什么需要这一层
|
||||
//!
|
||||
//! 判据是「中文界面下外观分组渲染出来的字里必须含汉字」,但 iced 0.14 **没有**可以遍历控件树的
|
||||
//! 公开接口:
|
||||
//!
|
||||
//! - `Element::as_widget()` 拿到的是 `&dyn Widget`,而 `Widget::children()` 返回的是**状态** `Tree`
|
||||
//! (不是子控件),没有 `as_any` 之类的向下转型口;
|
||||
//! - 想走 `Widget::operate` 得自己造 `Tree` + 跑布局 + 传 `Renderer`,而单测里**没有渲染器**
|
||||
//! (`pages/download/tests/options_screen.rs` 记的正是这条边界)。
|
||||
//!
|
||||
//! 所以取证点退到「字进树之前」那一层:`section::t` 是本分组**唯一**的取词口,它把每条译文
|
||||
//! 顺手记一份,测试再断言这批字里有没有汉字。这不是「看源码字符串」那种假守卫——它要求真的把
|
||||
//! [`crate::pages::appearance::section`] 跑一遍(并且走 `settings::view` 的语言分发),只是不画像素。
|
||||
//!
|
||||
//! ## 为什么是 thread-local
|
||||
//!
|
||||
//! `cargo test` 默认多线程跑,每个测试一个线程;thread-local 让并发测试的取证互不串台,
|
||||
//! 也不需要加锁或串行化。
|
||||
//!
|
||||
//! 整个模块只在 `#[cfg(test)]` 下编译,产品路径零代价。
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use unlucky_i18n::catalog::Key;
|
||||
|
||||
// 自上次 `take` 以来记录下的 `(key, 译文)`,顺序 = 取词顺序。
|
||||
thread_local! {
|
||||
// clippy 1.99.1(= 本仓 CI 的 stable)对这条 lint **误报**:初始化器已经写成 `const { … }`
|
||||
// 它还是报「can be made const」。最小复现(clippy-driver 原地可验,无本仓任何符号):
|
||||
// `thread_local!{ static X: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) }; }`
|
||||
// —— 同一个文件加不加 `const` 报同一句。等 clippy 修掉这条误报即可删掉本行 allow。
|
||||
#[allow(clippy::missing_const_for_thread_local)]
|
||||
static TEXTS: RefCell<Vec<(Key, &'static str)>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
/// 记一条(由 [`super::section::t`] 调用;不对外)。
|
||||
pub(crate) fn record(key: Key, text: &'static str) {
|
||||
TEXTS.with(|texts| texts.borrow_mut().push((key, text)));
|
||||
}
|
||||
|
||||
/// 取走并清空本线程记录下的取词(测试用)。
|
||||
pub(crate) fn take() -> Vec<(Key, &'static str)> {
|
||||
TEXTS.with(|texts| std::mem::take(&mut *texts.borrow_mut()))
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
//! 设置页「外观」分组(**真控件**)+ 背景层 Element + 面板层配色。
|
||||
//!
|
||||
//! ## 控件与键的对应
|
||||
//!
|
||||
//! | 控件 | 键 | 改动消息 |
|
||||
//! |---|---|---|
|
||||
//! | 整窗透明度滑块(下限 0.08) | `appearance.window_opacity` | [`Change::WindowOpacity`] |
|
||||
//! | 窗口圆角(跟随系统 / 直角,M116) | `appearance.window_corner` | [`Change::Corner`] |
|
||||
//! | 面板透明度滑块(下限 0.4,CPU 档禁用) | `appearance.panel_alpha` | [`Change::PanelAlpha`] |
|
||||
//! | 背景图开关 | `appearance.bg_image_enabled` | [`Change::BgEnabled`] |
|
||||
//! | 背景图路径(文本框) | `appearance.bg_image` | [`Change::BgImage`] |
|
||||
//! | 填充(封面 / 拉伸 / 平铺) | `appearance.bg_fill` | [`Change::BgFill`] |
|
||||
//! | 暗化强度(0~0.95) | `appearance.bg_dim` | [`Change::BgDim`] |
|
||||
//! | 配色来源(三档) | `appearance.palette_source` | [`Change::PaletteSource`] |
|
||||
//!
|
||||
//! 文案一律英文基线(i18n 是别的 mission;本文件不拼中文、不 import 尚不存在的 crate)。
|
||||
//!
|
||||
//! ## 为什么「值」与「生效值」分开显示
|
||||
//!
|
||||
//! CPU 档下面板 α 被强制 1.0(D36),而用户请求值可能是 0.5。滑块位置按**生效值**画、旁边写
|
||||
//! 「requested 0.50 → forced 1.00」,这样「为什么拖了没反应」在界面上就有答案,而不是只躺在日志里。
|
||||
|
||||
use iced::widget::{button, column, container, row, space};
|
||||
use iced::{
|
||||
Alignment, Background, Border, Color, Element, Length, Padding, Shadow, Theme as IcedTheme,
|
||||
};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::component_map::Component;
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::catalog::Key;
|
||||
use unlucky_presenter::prefs::{BgFill, PaletteSource, WindowCorner};
|
||||
|
||||
use crate::app::components::inputs::{slider, text_field, Field, Slider};
|
||||
use crate::pages::{frame, Message};
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::alpha::{
|
||||
panel_alpha_from_fraction, panel_fraction, window_alpha_from_fraction, window_fraction,
|
||||
WINDOW_ALPHA_FLOOR,
|
||||
};
|
||||
use super::background::{self, BG_DIM_MAX};
|
||||
use super::state::State;
|
||||
use super::Change;
|
||||
|
||||
/// 「外观」分组整块(一张卡)。
|
||||
pub fn section<'a>(ui: &'a Ui, state: &'a State) -> Element<'a, Message> {
|
||||
let mut rows = column![].spacing(ui.space(Space::X1)).width(Length::Fill);
|
||||
|
||||
// ① 整窗透明度
|
||||
let window_percent = format!("{:.0}%", state.window_opacity * 100.0);
|
||||
rows = rows.push(slider_row(
|
||||
ui,
|
||||
"Window transparency",
|
||||
window_percent,
|
||||
format!(
|
||||
"Applies to the whole window. Floor {:.2}; 1.00 = fully opaque.",
|
||||
WINDOW_ALPHA_FLOOR
|
||||
),
|
||||
state.window_opacity < 1.0,
|
||||
slider(
|
||||
Slider::new(ui, window_fraction(state.window_opacity)).on_change(|fraction| {
|
||||
Message::Appearance(Change::WindowOpacity(window_alpha_from_fraction(fraction)))
|
||||
}),
|
||||
),
|
||||
));
|
||||
|
||||
// ② 窗口圆角(M116):只有两档 + 一句按平台说明;不做 8/12/16 档(系统不暴露半径)。
|
||||
let corner = state.window_corner();
|
||||
rows = rows.push(choice_row(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceCorner),
|
||||
vec![
|
||||
choice(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceCornerSystem),
|
||||
corner == WindowCorner::System,
|
||||
Change::Corner(WindowCorner::System),
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceCornerSquare),
|
||||
corner == WindowCorner::Square,
|
||||
Change::Corner(WindowCorner::Square),
|
||||
),
|
||||
],
|
||||
));
|
||||
// **tonal notice**(r5 视觉终稿):M3 里表达「当前状态」的惯用形态是**容器**,不是改字色。
|
||||
// 只在「跟随系统 + α<1」出现;与下面的说明段之间再留 `Space::X1`(列间距 X1 + 这里 X1 = X2)。
|
||||
if corner == WindowCorner::System && state.window_opacity < 1.0 {
|
||||
rows = rows.push(super::notice(ui, t(Key::SettingsAppearanceCornerSuspended)));
|
||||
let gap: Element<'a, Message> = space::vertical().height(ui.space(Space::X1)).into();
|
||||
rows = rows.push(gap);
|
||||
}
|
||||
let tone = ui.color(Role::OnSurfaceVariant);
|
||||
rows = rows.push(note_line(ui, t(Key::SettingsAppearanceCornerHint), tone));
|
||||
|
||||
// ③ 面板透明度(CPU 档强制 1.0)
|
||||
let forced = state.panel_is_forced();
|
||||
let panel_percent = if forced {
|
||||
format!("100% (forced from {:.2})", state.panel_alpha)
|
||||
} else {
|
||||
format!("{:.0}%", state.effective_panel_alpha() * 100.0)
|
||||
};
|
||||
let panel_hint = if forced {
|
||||
"Software renderer: panels stay fully opaque (window transparency still works)."
|
||||
} else {
|
||||
"Only the panel/card layer. Floor 0.40; independent of window transparency."
|
||||
};
|
||||
let panel_control = if forced {
|
||||
slider(Slider::new(ui, panel_fraction(1.0)))
|
||||
} else {
|
||||
slider(
|
||||
Slider::new(ui, panel_fraction(state.panel_alpha)).on_change(|fraction| {
|
||||
Message::Appearance(Change::PanelAlpha(panel_alpha_from_fraction(fraction)))
|
||||
}),
|
||||
)
|
||||
};
|
||||
rows = rows.push(slider_row(
|
||||
ui,
|
||||
"Panel transparency",
|
||||
panel_percent,
|
||||
panel_hint.to_owned(),
|
||||
!forced,
|
||||
panel_control,
|
||||
));
|
||||
|
||||
// ④ 背景图开关
|
||||
rows = rows.push(choice_row(
|
||||
ui,
|
||||
"Background image",
|
||||
vec![
|
||||
choice(ui, "Off", !state.bg_enabled, Change::BgEnabled(false)),
|
||||
choice(ui, "On", state.bg_enabled, Change::BgEnabled(true)),
|
||||
],
|
||||
));
|
||||
|
||||
// ⑤ 路径(文本框:受控,输入即生效)
|
||||
let path_field: Element<'a, Message> = text_field(
|
||||
ui,
|
||||
Field::new(&state.bg_path)
|
||||
.label("Image path")
|
||||
.placeholder("D:/Pictures/background.png"),
|
||||
|value| Message::Appearance(Change::BgImage(value)),
|
||||
);
|
||||
rows = rows.push(
|
||||
column![label(ui, "Image path"), path_field]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(Length::Fill),
|
||||
);
|
||||
|
||||
// ⑥ 诊断(读盘失败 / CPU 降级 / 配色回落)——**不许静默**,且必须在**首屏可见处**
|
||||
// (复审 r1 P2-3:文案落在折叠线以下等于没提示)。位置就在背景图那两行的紧下方。
|
||||
for note in state.diagnostics() {
|
||||
rows = rows.push(note_line(ui, note, ui.color(Role::Error)));
|
||||
}
|
||||
if let Some(note) = state.backend_note() {
|
||||
rows = rows.push(note_line(ui, note, ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
if let Some(note) = state
|
||||
.image()
|
||||
.and_then(|image| background::small_tile_note(image, state.bg_fill))
|
||||
{
|
||||
rows = rows.push(note_line(ui, ¬e, ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
if let Some(note) = state.palette_note() {
|
||||
rows = rows.push(note_line(ui, note, ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
let resolved = format!("Colours: {}", state.palette_level());
|
||||
rows = rows.push(note_line(ui, &resolved, ui.color(Role::OnSurfaceVariant)));
|
||||
|
||||
// ⑦ 填充
|
||||
rows = rows.push(choice_row(
|
||||
ui,
|
||||
"Fill",
|
||||
vec![
|
||||
choice(
|
||||
ui,
|
||||
"Cover",
|
||||
state.bg_fill == BgFill::Cover,
|
||||
Change::BgFill(BgFill::Cover),
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
"Contain",
|
||||
state.bg_fill == BgFill::Contain,
|
||||
Change::BgFill(BgFill::Contain),
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
"Tile",
|
||||
state.bg_fill == BgFill::Tile,
|
||||
Change::BgFill(BgFill::Tile),
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
// ⑧ 暗化
|
||||
rows = rows.push(slider_row(
|
||||
ui,
|
||||
"Dim",
|
||||
format!("{:.0}%", state.bg_dim * 100.0),
|
||||
format!(
|
||||
"Blends the image toward the panel colour so text stays readable. Max {BG_DIM_MAX:.2}."
|
||||
),
|
||||
state.bg_dim > 0.0,
|
||||
slider(
|
||||
Slider::new(ui, state.bg_dim / BG_DIM_MAX)
|
||||
.on_change(|fraction| Message::Appearance(Change::BgDim(fraction * BG_DIM_MAX))),
|
||||
),
|
||||
));
|
||||
|
||||
// ⑨ 配色来源
|
||||
rows = rows.push(choice_row(
|
||||
ui,
|
||||
"Colour source",
|
||||
vec![
|
||||
choice(
|
||||
ui,
|
||||
"Background",
|
||||
state.palette_source == PaletteSource::BackgroundImage,
|
||||
Change::PaletteSource(PaletteSource::BackgroundImage),
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
"Wallpaper",
|
||||
state.palette_source == PaletteSource::Wallpaper,
|
||||
Change::PaletteSource(PaletteSource::Wallpaper),
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
"Preset",
|
||||
state.palette_source == PaletteSource::Preset,
|
||||
Change::PaletteSource(PaletteSource::Preset),
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
card(ui, rows)
|
||||
}
|
||||
|
||||
/// 外观卡片:底色走 `ui.layer`(面板 α 的唯一来源),与其它页的卡片同源。
|
||||
fn card<'a>(ui: &'a Ui, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||
let spec = Component::OutlinedCard.spec();
|
||||
let background = ui.layer(MaterialLayer::Card);
|
||||
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.padding(ui.metrics().card_padding)
|
||||
.style(move |_theme| container::Style {
|
||||
background: Some(Background::Color(background)),
|
||||
border: ui.outlined(spec.shape),
|
||||
..container::Style::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- 行与控件
|
||||
|
||||
fn label<'a>(ui: &'a Ui, content: &'a str) -> Element<'a, Message> {
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodyMedium,
|
||||
false,
|
||||
content,
|
||||
ui.color(Role::OnSurface),
|
||||
)
|
||||
}
|
||||
|
||||
/// 设置页文案的 i18n 取词口(M116 起新条目一律走词表,不再写字面量)。
|
||||
///
|
||||
/// 为什么是 [`Lang::En`]:本文件的既有文案是英文基线(i18n 接线是另一条 mission,
|
||||
/// 见本文件头部「文案一律英文基线」)。走词表的意义是**新增条目已经有中英两套**,
|
||||
/// 接线时只换这一行,不用回来重写控件。
|
||||
fn t(key: Key) -> &'static str {
|
||||
unlucky_i18n::Lang::En.catalog().text(key)
|
||||
}
|
||||
|
||||
fn note_line<'a>(ui: &'a Ui, content: &str, color: Color) -> Element<'a, Message> {
|
||||
// 诊断文案是**运行时拼出来的**(路径、错误原因),只能走 owned 版本。
|
||||
frame::text_role_owned(ui, TypeRole::BodySmall, false, content.to_owned(), color)
|
||||
}
|
||||
|
||||
/// 一行「标签 + 值 + 滑块 + 说明」。
|
||||
fn slider_row<'a>(
|
||||
ui: &'a Ui,
|
||||
title: &'a str,
|
||||
value: String,
|
||||
hint: String,
|
||||
enabled: bool,
|
||||
control: Element<'a, Message>,
|
||||
) -> Element<'a, Message> {
|
||||
let value_color = if enabled {
|
||||
ui.color(Role::OnSurface)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
};
|
||||
|
||||
let head = row![
|
||||
container(label(ui, title))
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center),
|
||||
frame::text_role_owned(ui, TypeRole::LabelLarge, true, value, value_color),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
column![
|
||||
head,
|
||||
control,
|
||||
frame::text_role_owned(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
hint,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(Length::Fill)
|
||||
.padding(Padding::from([ui.space(Space::X2), 0.0]))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一行「标签 + 一排互斥按钮」。
|
||||
fn choice_row<'a>(
|
||||
ui: &'a Ui,
|
||||
title: &'a str,
|
||||
choices: Vec<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
row![
|
||||
container(label(ui, title))
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center),
|
||||
row(choices)
|
||||
.spacing(ui.space(Space::X1))
|
||||
.align_y(Alignment::Center),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center)
|
||||
.padding(Padding::from([ui.space(Space::X2), 0.0]))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一个互斥按钮(选中态用 `SecondaryContainer`,未选中用透明 + `OnSurfaceVariant`)。
|
||||
fn choice<'a>(ui: &'a Ui, text: &'a str, selected: bool, change: Change) -> Element<'a, Message> {
|
||||
let height = ui.metrics().control_height;
|
||||
let content_color = if selected {
|
||||
ui.color(Role::OnSecondaryContainer)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
};
|
||||
|
||||
button(
|
||||
container(frame::text_role(
|
||||
ui,
|
||||
TypeRole::LabelLarge,
|
||||
selected,
|
||||
text,
|
||||
content_color,
|
||||
))
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.height(height)
|
||||
.padding(Padding::from([0.0, ui.space(Space::X3)]))
|
||||
.style(move |_theme: &IcedTheme, status| {
|
||||
let background = if selected {
|
||||
Some(Background::Color(ui.color(Role::SecondaryContainer)))
|
||||
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
|
||||
Some(Background::Color(ui.state_over(
|
||||
unlucky_design::color::StateLayer::Hover,
|
||||
Role::OnSurface,
|
||||
Role::Surface,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background,
|
||||
text_color: content_color,
|
||||
border: Border {
|
||||
radius: ui.radius(ShapeScale::Full, height),
|
||||
..Border::default()
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
})
|
||||
.on_press(Message::Appearance(change))
|
||||
.into()
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//! 外观分组第 ④ ⑤ ⑥ ⑦ ⑧ 段:**背景图**——开关、路径、诊断、填充档、暗化强度。
|
||||
//!
|
||||
//! 这五段挤在一起是因为它们读同一份 `bg_*` 状态,而且**诊断必须紧贴控件**(复审 r1 P2-3:
|
||||
//! 读盘失败 / CPU 降级 / 配色回落这些提示落在折叠线以下等于没提示)。
|
||||
|
||||
use iced::widget::column;
|
||||
use iced::{Element, Length};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_presenter::prefs::BgFill;
|
||||
|
||||
use crate::app::components::inputs::{slider, text_field, Field, Slider};
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::background::{self, BG_DIM_MAX};
|
||||
use super::super::state::State;
|
||||
use super::super::Change;
|
||||
use super::{choice, choice_row, label, note_line, slider_row};
|
||||
|
||||
/// 把 ④ 开关、⑤ 路径、⑥ 诊断、⑦ 填充、⑧ 暗化按视觉顺序推进 `rows`。
|
||||
pub(super) fn push<'a>(ui: &'a Ui, state: &'a State, rows: &mut Vec<Element<'a, Message>>) {
|
||||
// ④ 背景图开关
|
||||
rows.push(choice_row(
|
||||
ui,
|
||||
"Background image",
|
||||
vec![
|
||||
choice(ui, "Off", !state.bg_enabled, Change::BgEnabled(false)),
|
||||
choice(ui, "On", state.bg_enabled, Change::BgEnabled(true)),
|
||||
],
|
||||
));
|
||||
|
||||
// ⑤ 路径(文本框:受控,输入即生效)
|
||||
let path_field: Element<'a, Message> = text_field(
|
||||
ui,
|
||||
Field::new(&state.bg_path)
|
||||
.label("Image path")
|
||||
.placeholder("D:/Pictures/background.png"),
|
||||
|value| Message::Appearance(Change::BgImage(value)),
|
||||
);
|
||||
rows.push(
|
||||
column![label(ui, "Image path"), path_field]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
|
||||
// ⑥ 诊断(读盘失败 / CPU 降级 / 配色回落)——**不许静默**,且必须在**首屏可见处**
|
||||
// (复审 r1 P2-3:文案落在折叠线以下等于没提示)。位置就在背景图那两行的紧下方。
|
||||
for note in state.diagnostics() {
|
||||
rows.push(note_line(ui, note, ui.color(Role::Error)));
|
||||
}
|
||||
if let Some(note) = state.backend_note() {
|
||||
rows.push(note_line(ui, note, ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
if let Some(note) = state
|
||||
.image()
|
||||
.and_then(|image| background::small_tile_note(image, state.bg_fill))
|
||||
{
|
||||
rows.push(note_line(ui, ¬e, ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
if let Some(note) = state.palette_note() {
|
||||
rows.push(note_line(ui, note, ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
let resolved = format!("Colours: {}", state.palette_level());
|
||||
rows.push(note_line(ui, &resolved, ui.color(Role::OnSurfaceVariant)));
|
||||
|
||||
// ⑦ 填充
|
||||
rows.push(choice_row(
|
||||
ui,
|
||||
"Fill",
|
||||
vec![
|
||||
choice(
|
||||
ui,
|
||||
"Cover",
|
||||
state.bg_fill == BgFill::Cover,
|
||||
Change::BgFill(BgFill::Cover),
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
"Contain",
|
||||
state.bg_fill == BgFill::Contain,
|
||||
Change::BgFill(BgFill::Contain),
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
"Tile",
|
||||
state.bg_fill == BgFill::Tile,
|
||||
Change::BgFill(BgFill::Tile),
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
// ⑧ 暗化
|
||||
rows.push(slider_row(
|
||||
ui,
|
||||
"Dim",
|
||||
format!("{:.0}%", state.bg_dim * 100.0),
|
||||
format!(
|
||||
"Blends the image toward the panel colour so text stays readable. Max {BG_DIM_MAX:.2}."
|
||||
),
|
||||
state.bg_dim > 0.0,
|
||||
slider(
|
||||
Slider::new(ui, state.bg_dim / BG_DIM_MAX)
|
||||
.on_change(|fraction| Message::Appearance(Change::BgDim(fraction * BG_DIM_MAX))),
|
||||
),
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! 外观分组第 ⑨ 段:**配色来源**(背景图 / 壁纸 / 色卡三档;它选的正是 D38 三级优先序的起点)。
|
||||
|
||||
use iced::Element;
|
||||
|
||||
use unlucky_presenter::prefs::PaletteSource;
|
||||
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::state::State;
|
||||
use super::super::Change;
|
||||
use super::{choice, choice_row};
|
||||
|
||||
/// 把 ⑨ 配色来源推进行 `rows`。
|
||||
pub(super) fn push<'a>(ui: &'a Ui, state: &'a State, rows: &mut Vec<Element<'a, Message>>) {
|
||||
// ⑨ 配色来源
|
||||
rows.push(choice_row(
|
||||
ui,
|
||||
"Colour source",
|
||||
vec![
|
||||
choice(
|
||||
ui,
|
||||
"Background",
|
||||
state.palette_source == PaletteSource::BackgroundImage,
|
||||
Change::PaletteSource(PaletteSource::BackgroundImage),
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
"Wallpaper",
|
||||
state.palette_source == PaletteSource::Wallpaper,
|
||||
Change::PaletteSource(PaletteSource::Wallpaper),
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
"Preset",
|
||||
state.palette_source == PaletteSource::Preset,
|
||||
Change::PaletteSource(PaletteSource::Preset),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
//! 设置页「外观」分组(**真控件**)+ 背景层 Element + 面板层配色。
|
||||
//!
|
||||
//! ## 控件与键的对应
|
||||
//!
|
||||
//! | 控件 | 键 | 改动消息 |
|
||||
//! |---|---|---|
|
||||
//! | 整窗透明度滑块(下限 0.08) | `appearance.window_opacity` | [`Change::WindowOpacity`] |
|
||||
//! | 窗口圆角(跟随系统 / 直角,M116) | `appearance.window_corner` | [`Change::Corner`] |
|
||||
//! | 面板透明度滑块(下限 0.4,CPU 档禁用) | `appearance.panel_alpha` | [`Change::PanelAlpha`] |
|
||||
//! | 背景图开关 | `appearance.bg_image_enabled` | [`Change::BgEnabled`] |
|
||||
//! | 背景图路径(文本框) | `appearance.bg_image` | [`Change::BgImage`] |
|
||||
//! | 填充(封面 / 拉伸 / 平铺) | `appearance.bg_fill` | [`Change::BgFill`] |
|
||||
//! | 暗化强度(0~0.95) | `appearance.bg_dim` | [`Change::BgDim`] |
|
||||
//! | 配色来源(三档) | `appearance.palette_source` | [`Change::PaletteSource`] |
|
||||
//!
|
||||
//! ## 文件分工(M179 `I18NSEAM`:纯搬移 + 语言接线)
|
||||
//!
|
||||
//! 本模块原来是一个 395 行的 `section.rs`,而全仓行数守卫是 400(`components/gate.rs`),
|
||||
//! 剩 5 行——加不了任何东西。M179 按「一段控件一个文件」切开了它:
|
||||
//!
|
||||
//! | 文件 | 是什么 |
|
||||
//! |---|---|
|
||||
//! | 本文件(`section/mod.rs`) | **壳**:[`section`] 按顺序拼各段 + [`card`] 卡片 + 行控件([`label`] / [`slider_row`] / [`choice_row`] / [`choice`] / [`note_line`])+ 取词口 [`t`] |
|
||||
//! | [`window`] | ① 整窗透明度 + ② 窗口圆角(含「因整窗 α 不生效」的 notice 与说明段) |
|
||||
//! | [`panel`] | ③ 面板透明度(CPU 档强制 1.0,值 / 生效值分开显示) |
|
||||
//! | [`bg_image`] | ④ 背景图开关 + ⑤ 路径 + ⑥ 诊断 + ⑦ 填充 + ⑧ 暗化 |
|
||||
//! | [`colour_source`] | ⑨ 配色来源(三档) |
|
||||
//!
|
||||
//! 拆分口径:**纯搬移**——每段的语句逐字搬过去,只有下面这几处机械改写(M179 报告里用「去空白/
|
||||
//! 去尾逗号 diff」逐段证明:改完之后与原文件逐字相同,断言一条没动):
|
||||
//!
|
||||
//! 1. `rows = rows.push(…)` → `rows.push(…)`(壳现在传 `&mut Vec<Element>`);
|
||||
//! 2. `t(Key::X)` → `t(Key::X, lang)`(本 mission 的语言接线);
|
||||
//! 3. `super::notice(…)` → `super::super::notice(…)`(多了一层模块);
|
||||
//! 4. 唯一一处非 `Element` 实参:`rows.push(column![…])` → 末尾加 `.into()`(`Vec::push` 不做 `Into`);
|
||||
//! 5. rustfmt 因为多出来的 `, lang` 把两处调用换成了多行。
|
||||
//!
|
||||
//! ## 语言(M179 只接管子,不补文案)
|
||||
//!
|
||||
//! [`t`] 是**唯一**取词口,现在收 `lang`(真值从 `settings::view` 的 `Language.effective`
|
||||
//! → `Group.body` → [`section`] 一路递进来),所以中文界面上走词表的那 5 处(窗口圆角一行、
|
||||
//! 两个选项、说明段、notice)真的是中文。**其余文案仍是英文基线**(字面量,没有 key)——
|
||||
//! 补文案是下一条 mission,M179 只负责让「管子」通到这一层。
|
||||
//!
|
||||
//! ## 为什么「值」与「生效值」分开显示
|
||||
//!
|
||||
//! CPU 档下面板 α 被强制 1.0(D36),而用户请求值可能是 0.5。滑块位置按**生效值**画、旁边写
|
||||
//! 「requested 0.50 → forced 1.00」,这样「为什么拖了没反应」在界面上就有答案,而不是只躺在日志里。
|
||||
|
||||
use iced::widget::{button, column, container, row};
|
||||
use iced::{
|
||||
Alignment, Background, Border, Color, Element, Length, Padding, Shadow, Theme as IcedTheme,
|
||||
};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::component_map::Component;
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::catalog::Key;
|
||||
use unlucky_i18n::Lang;
|
||||
|
||||
use crate::pages::{frame, Message};
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::state::State;
|
||||
use super::Change;
|
||||
|
||||
mod bg_image;
|
||||
mod colour_source;
|
||||
mod panel;
|
||||
mod window;
|
||||
|
||||
/// 「外观」分组整块(一张卡)。
|
||||
///
|
||||
/// `lang` 是**这一次生效的语言**:`settings::view` 的 `Language.effective`(M179)。壳把各段按
|
||||
/// **视觉顺序**拼起来;每一段的语句逐字来自拆分前的 `section()`。
|
||||
pub fn section<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Message> {
|
||||
let mut rows: Vec<Element<'a, Message>> = Vec::new();
|
||||
|
||||
window::push(ui, state, lang, &mut rows);
|
||||
panel::push(ui, state, &mut rows);
|
||||
bg_image::push(ui, state, &mut rows);
|
||||
colour_source::push(ui, state, &mut rows);
|
||||
|
||||
card(
|
||||
ui,
|
||||
column(rows)
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
}
|
||||
|
||||
/// 外观卡片:底色走 `ui.layer`(面板 α 的唯一来源),与其它页的卡片同源。
|
||||
fn card<'a>(ui: &'a Ui, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||
let spec = Component::OutlinedCard.spec();
|
||||
let background = ui.layer(MaterialLayer::Card);
|
||||
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.padding(ui.metrics().card_padding)
|
||||
.style(move |_theme| container::Style {
|
||||
background: Some(Background::Color(background)),
|
||||
border: ui.outlined(spec.shape),
|
||||
..container::Style::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- 行与控件
|
||||
//
|
||||
// 五个行控件由各段共用,所以留在壳里(`pub(super)` = 只给 `section/` 下的兄弟模块用)。
|
||||
|
||||
pub(super) fn label<'a>(ui: &'a Ui, content: &'a str) -> Element<'a, Message> {
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodyMedium,
|
||||
false,
|
||||
content,
|
||||
ui.color(Role::OnSurface),
|
||||
)
|
||||
}
|
||||
|
||||
/// 设置页文案的 i18n 取词口(M116 起新条目一律走词表,不再写字面量)。
|
||||
///
|
||||
/// `lang` 是**这一次生效的语言**,由 [`section`] 一路递下来(M179 `I18NSEAM` 之前这里按英文目录
|
||||
/// 取词,于是中文界面上本分组的 27 条文案里真正本地化 0 条)。现在这个函数是全模块
|
||||
/// **唯一**的取词点:谁硬编码英文,就不会经过这里——M179 报告里逐行列了那些字面量,补文案是
|
||||
/// 下一条 mission。
|
||||
pub(super) fn t(key: Key, lang: Lang) -> &'static str {
|
||||
let text = lang.catalog().text(key);
|
||||
// 测试专用:把「这一帧真的取了哪条译文」记一份(iced 0.14 的 `Element` 没有可遍历控件树的
|
||||
// 公开接口,所以「渲染出来的字是不是中文」只能在取词口取证,见 `appearance::probe`)。
|
||||
#[cfg(test)]
|
||||
super::probe::record(key, text);
|
||||
text
|
||||
}
|
||||
|
||||
pub(super) fn note_line<'a>(ui: &'a Ui, content: &str, color: Color) -> Element<'a, Message> {
|
||||
// 诊断文案是**运行时拼出来的**(路径、错误原因),只能走 owned 版本。
|
||||
frame::text_role_owned(ui, TypeRole::BodySmall, false, content.to_owned(), color)
|
||||
}
|
||||
|
||||
/// 一行「标签 + 值 + 滑块 + 说明」。
|
||||
pub(super) fn slider_row<'a>(
|
||||
ui: &'a Ui,
|
||||
title: &'a str,
|
||||
value: String,
|
||||
hint: String,
|
||||
enabled: bool,
|
||||
control: Element<'a, Message>,
|
||||
) -> Element<'a, Message> {
|
||||
let value_color = if enabled {
|
||||
ui.color(Role::OnSurface)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
};
|
||||
|
||||
let head = row![
|
||||
container(label(ui, title))
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center),
|
||||
frame::text_role_owned(ui, TypeRole::LabelLarge, true, value, value_color),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
column![
|
||||
head,
|
||||
control,
|
||||
frame::text_role_owned(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
hint,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(Length::Fill)
|
||||
.padding(Padding::from([ui.space(Space::X2), 0.0]))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一行「标签 + 一排互斥按钮」。
|
||||
pub(super) fn choice_row<'a>(
|
||||
ui: &'a Ui,
|
||||
title: &'a str,
|
||||
choices: Vec<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
row![
|
||||
container(label(ui, title))
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center),
|
||||
row(choices)
|
||||
.spacing(ui.space(Space::X1))
|
||||
.align_y(Alignment::Center),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center)
|
||||
.padding(Padding::from([ui.space(Space::X2), 0.0]))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一个互斥按钮(选中态用 `SecondaryContainer`,未选中用透明 + `OnSurfaceVariant`)。
|
||||
pub(super) fn choice<'a>(
|
||||
ui: &'a Ui,
|
||||
text: &'a str,
|
||||
selected: bool,
|
||||
change: Change,
|
||||
) -> Element<'a, Message> {
|
||||
let height = ui.metrics().control_height;
|
||||
let content_color = if selected {
|
||||
ui.color(Role::OnSecondaryContainer)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
};
|
||||
|
||||
button(
|
||||
container(frame::text_role(
|
||||
ui,
|
||||
TypeRole::LabelLarge,
|
||||
selected,
|
||||
text,
|
||||
content_color,
|
||||
))
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.height(height)
|
||||
.padding(Padding::from([0.0, ui.space(Space::X3)]))
|
||||
.style(move |_theme: &IcedTheme, status| {
|
||||
let background = if selected {
|
||||
Some(Background::Color(ui.color(Role::SecondaryContainer)))
|
||||
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
|
||||
Some(Background::Color(ui.state_over(
|
||||
unlucky_design::color::StateLayer::Hover,
|
||||
Role::OnSurface,
|
||||
Role::Surface,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background,
|
||||
text_color: content_color,
|
||||
border: Border {
|
||||
radius: ui.radius(ShapeScale::Full, height),
|
||||
..Border::default()
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
})
|
||||
.on_press(Message::Appearance(change))
|
||||
.into()
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//! 外观分组第 ③ 段:**面板透明度**滑块(下限 0.4;CPU 档强制 1.0 并禁用滑块)。
|
||||
|
||||
use iced::Element;
|
||||
|
||||
use crate::app::components::inputs::{slider, Slider};
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::alpha::{panel_alpha_from_fraction, panel_fraction};
|
||||
use super::super::state::State;
|
||||
use super::super::Change;
|
||||
use super::slider_row;
|
||||
|
||||
/// 把 ③ 面板透明度推进 `rows`。
|
||||
pub(super) fn push<'a>(ui: &'a Ui, state: &'a State, rows: &mut Vec<Element<'a, Message>>) {
|
||||
// ③ 面板透明度(CPU 档强制 1.0)
|
||||
let forced = state.panel_is_forced();
|
||||
let panel_percent = if forced {
|
||||
format!("100% (forced from {:.2})", state.panel_alpha)
|
||||
} else {
|
||||
format!("{:.0}%", state.effective_panel_alpha() * 100.0)
|
||||
};
|
||||
let panel_hint = if forced {
|
||||
"Software renderer: panels stay fully opaque (window transparency still works)."
|
||||
} else {
|
||||
"Only the panel/card layer. Floor 0.40; independent of window transparency."
|
||||
};
|
||||
let panel_control = if forced {
|
||||
slider(Slider::new(ui, panel_fraction(1.0)))
|
||||
} else {
|
||||
slider(
|
||||
Slider::new(ui, panel_fraction(state.panel_alpha)).on_change(|fraction| {
|
||||
Message::Appearance(Change::PanelAlpha(panel_alpha_from_fraction(fraction)))
|
||||
}),
|
||||
)
|
||||
};
|
||||
rows.push(slider_row(
|
||||
ui,
|
||||
"Panel transparency",
|
||||
panel_percent,
|
||||
panel_hint.to_owned(),
|
||||
!forced,
|
||||
panel_control,
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! 外观分组第 ① ② 段:**整窗**两件——整窗透明度滑块、窗口圆角(含 notice 与说明段)。
|
||||
//!
|
||||
//! 这两段是唯一**一个控件牵动另一个控件**的地方:圆角「跟随系统」时把整窗透明度调低会让圆角失效
|
||||
//! (`DwmExtendFrameIntoClientArea` 的世界里没有半透明边框),所以只有这一对放在同一个文件里——
|
||||
//! notice 的判据(`corner == System && window_opacity < 1.0`)跨这两段。
|
||||
|
||||
use iced::widget::space;
|
||||
use iced::Element;
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_i18n::catalog::Key;
|
||||
use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::prefs::WindowCorner;
|
||||
|
||||
use crate::app::components::inputs::{slider, Slider};
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::alpha::{window_alpha_from_fraction, window_fraction, WINDOW_ALPHA_FLOOR};
|
||||
use super::super::state::State;
|
||||
use super::super::Change;
|
||||
use super::{choice, choice_row, note_line, slider_row, t};
|
||||
|
||||
/// 把 ① 整窗透明度与 ② 窗口圆角按视觉顺序推进 `rows`。
|
||||
pub(super) fn push<'a>(
|
||||
ui: &'a Ui,
|
||||
state: &'a State,
|
||||
lang: Lang,
|
||||
rows: &mut Vec<Element<'a, Message>>,
|
||||
) {
|
||||
// ① 整窗透明度
|
||||
let window_percent = format!("{:.0}%", state.window_opacity * 100.0);
|
||||
rows.push(slider_row(
|
||||
ui,
|
||||
"Window transparency",
|
||||
window_percent,
|
||||
format!(
|
||||
"Applies to the whole window. Floor {:.2}; 1.00 = fully opaque.",
|
||||
WINDOW_ALPHA_FLOOR
|
||||
),
|
||||
state.window_opacity < 1.0,
|
||||
slider(
|
||||
Slider::new(ui, window_fraction(state.window_opacity)).on_change(|fraction| {
|
||||
Message::Appearance(Change::WindowOpacity(window_alpha_from_fraction(fraction)))
|
||||
}),
|
||||
),
|
||||
));
|
||||
|
||||
// ② 窗口圆角(M116):只有两档 + 一句按平台说明;不做 8/12/16 档(系统不暴露半径)。
|
||||
let corner = state.window_corner();
|
||||
rows.push(choice_row(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceCorner, lang),
|
||||
vec![
|
||||
choice(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceCornerSystem, lang),
|
||||
corner == WindowCorner::System,
|
||||
Change::Corner(WindowCorner::System),
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceCornerSquare, lang),
|
||||
corner == WindowCorner::Square,
|
||||
Change::Corner(WindowCorner::Square),
|
||||
),
|
||||
],
|
||||
));
|
||||
// **tonal notice**(r5 视觉终稿):M3 里表达「当前状态」的惯用形态是**容器**,不是改字色。
|
||||
// 只在「跟随系统 + α<1」出现;与下面的说明段之间再留 `Space::X1`(列间距 X1 + 这里 X1 = X2)。
|
||||
if corner == WindowCorner::System && state.window_opacity < 1.0 {
|
||||
rows.push(super::super::notice(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceCornerSuspended, lang),
|
||||
));
|
||||
let gap: Element<'a, Message> = space::vertical().height(ui.space(Space::X1)).into();
|
||||
rows.push(gap);
|
||||
}
|
||||
let tone = ui.color(Role::OnSurfaceVariant);
|
||||
rows.push(note_line(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceCornerHint, lang),
|
||||
tone,
|
||||
));
|
||||
}
|
||||
Reference in new issue
Block a user