Merge branch 'feat/m179-i18nseam-thread-lang-into-page-bodi'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
124e474656
19 files changed
+765
-440
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,
|
||||
));
|
||||
}
|
||||
@@ -268,7 +268,7 @@ pub fn app<'a>(
|
||||
|
||||
/// 同 [`app`],但可以带上**外观运行期状态**。
|
||||
///
|
||||
/// `Some(state)` ⇒ 设置页「外观」分组画**真控件**(`pages/appearance/section.rs`);
|
||||
/// `Some(state)` ⇒ 设置页「外观」分组画**真控件**(`pages/appearance/section/`);
|
||||
/// `None` ⇒ 画演示行,与 [`app`] 逐像素等价。harness 的 `--appearance=real` 走的就是这一支:
|
||||
/// M100 之前 harness 恒传 `None`,于是 D30 要的「带真实外观控件」那一屏**根本不存在**——
|
||||
/// 报告上写着「设置页已接真控件」,图里却只有演示行(U1 的形状)。
|
||||
|
||||
@@ -5,15 +5,16 @@
|
||||
//! | 这里 | 内容 |
|
||||
//! |---|---|
|
||||
//! | [`GROUP`] | 左列名 / 图标 / 演示行(产品路径走 [`super::ai::view::body`]) |
|
||||
//! | [`AI_GROUP`] | 本组在 `GROUPS` 里的下标(与 General / Update 同一条**按下标分发**:本组要 i18n 文案与 `Lang` 形参,`Group.body` 的签名里没有它) |
|
||||
//! | [`AI_GROUP`] | 本组在 `GROUPS` 里的下标(与 General / Update 同一条**按下标分发**:本组尚未迁到 `Group.body` 槽位,见下节) |
|
||||
//! | [`Message`] | 本线的消息口(落点 `app/update/settings/ai_about.rs`) |
|
||||
//! | [`State`] | 转发 `ai::State`(`Groups.ai_about` 的载荷类型不变,`settings/state.rs` 一行不用改) |
|
||||
//!
|
||||
//! ## 为什么 `body` 是 `None` 而不是 `Some(..)`
|
||||
//!
|
||||
//! `Group.body` 的签名是 `fn(&Ui, States) -> Option<Element>`:拿不到 `Lang`,也就画不出中英两套
|
||||
//! 文案(本组的硬要求是「不得硬编码中文」)。M106 的语言行与 M159 的更新组是同一个理由走了
|
||||
//! 按下标分发;本组沿用同一条路,代价是 `settings/mod.rs` 里多一行 `else if`。
|
||||
//! `Group.body` 的槽位(M179 起)拿的是 [`States`](super::States) 与**生效语言** `Lang`——本组要的
|
||||
//! 两件它其实都装得下。本组仍走按下标分发,是因为**还没迁过去**:槽位要求返回 `Option<Element>`,
|
||||
//! 而 `ai::view::body` 返回 `Element`,把本组(以及 Java 组)统一迁到槽位上是一次独立重构。
|
||||
//! M106 的语言行与 M159 的更新组当初也是同一个理由走上这条路,代价是 `settings/mod.rs` 多一行 `else if`。
|
||||
//!
|
||||
//! ## 密钥纪律
|
||||
//!
|
||||
|
||||
@@ -5,20 +5,25 @@
|
||||
//! 「Appearance」这个名字**;分组在左列的下标仍由 `crate::pages::appearance::APPEARANCE_GROUP`
|
||||
//! 声明,`settings/tests/mod.rs` 拿它和本文件比对(顺序被调换会红)。
|
||||
//!
|
||||
//! **这一组归 language+appearance 线**:真控件的实现全在 `pages/appearance/section.rs`(本文件只负责
|
||||
//! **这一组归 language+appearance 线**:真控件的实现全在 `pages/appearance/section/`(本文件只负责
|
||||
//! 把它挂进注册表),所以改控件时不用碰设置页目录。
|
||||
//!
|
||||
//! M101 r2:本组的渲染器签名改成收 [`States`](组状态 + 外观状态),于是「这一组要读哪些状态」
|
||||
//! 由本文件决定——`settings/mod.rs` 不再认识 `appearance::State`。
|
||||
//!
|
||||
//! M179 `I18NSEAM`:签名再加一个 `lang`(`Group.body` 的槽位本身就带语言,见 `settings/mod.rs` 的
|
||||
//! `Body` 类型别名),于是这一组的中文界面文案真的走词表——此前它只能写死 `Lang::En`,
|
||||
//! 因为渲染器拿不到「这一次生效的语言」。
|
||||
|
||||
use iced::Element;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_i18n::Lang;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::{Group, Message, States};
|
||||
|
||||
/// Appearance 分组。`body` 指着 [`body`](它再转给 `pages/appearance/section.rs` 的真控件);
|
||||
/// Appearance 分组。`body` 指着 [`body`](它再转给 `pages/appearance/section/` 的真控件);
|
||||
/// 没有外观运行期状态时(harness / `pages_demo`)返回 `None`,由 `form` 退回下面的演示行。
|
||||
pub(super) const GROUP: Group = Group {
|
||||
name: "Appearance",
|
||||
@@ -33,8 +38,10 @@ pub(super) const GROUP: Group = Group {
|
||||
};
|
||||
|
||||
/// 本组的真控件渲染器:有外观状态就画真控件(M93),没有(harness)返回 `None`。
|
||||
fn body<'a>(ui: &'a Ui, states: States<'a>) -> Option<Element<'a, Message>> {
|
||||
///
|
||||
/// `lang` 一路递给 `pages::appearance::section`——它是这一组唯一知道「界面现在是什么语言」的东西。
|
||||
fn body<'a>(ui: &'a Ui, states: States<'a>, lang: Lang) -> Option<Element<'a, Message>> {
|
||||
states
|
||||
.appearance
|
||||
.map(|state| crate::pages::appearance::section(ui, state))
|
||||
.map(|state| crate::pages::appearance::section(ui, state, lang))
|
||||
}
|
||||
@@ -4,10 +4,11 @@
|
||||
//!
|
||||
//! ## 这一组为什么特殊
|
||||
//!
|
||||
//! 它**不走** `Group::body`:那个渲染器的签名是 `fn(&Ui, States) -> Option<Element>`,`States` 只有
|
||||
//! `appearance` + `groups` 两格,装不下「当前语言档位 + 生效语言」。语言行要的两件在
|
||||
//! `pages::Language` 里,由 `pages::settings::view` 的 `language` 形参一路递进来、`form` 按下标
|
||||
//! 转交 [`body`];所以 `GROUP.body` 仍是 `None`,`tests/mod.rs` 那条「只有外观组带真控件」照样成立。
|
||||
//! 它**不走** `Group::body`:那个渲染器拿的是 [`States`](super::States) 与**生效语言** `Lang`
|
||||
//! (M179 起;此前连 `Lang` 都没有),而语言行要的是「当前语言**档位** `LangChoice` + 生效语言」
|
||||
//! 这一对——档位不在 `States` 里。所以它由 `pages::settings::view` 的 `language` 形参一路递进来、
|
||||
//! `form` 按下标转交 [`body`];`GROUP.body` 仍是 `None`,`tests/mod.rs` 那条「只有外观组带真控件」
|
||||
//! 照样成立。
|
||||
//!
|
||||
//! ## 语言切换**没有**走 M101 的 `GroupMessage` 投递口(偏离,已登记)
|
||||
//!
|
||||
@@ -284,7 +285,7 @@ fn setting_row<'a>(ui: &'a Ui, label: &'a str, value: &'a str) -> Element<'a, Me
|
||||
|
||||
/// 一个互斥按钮(选中态 `SecondaryContainer`,未选中透明 + `OnSurfaceVariant`)。
|
||||
///
|
||||
/// 与 `pages/appearance/section.rs` 的 `choice` 同款,但文案来自 catalog(那份是英文基线)。
|
||||
/// 与 `pages/appearance/section/mod.rs` 的 `choice` 同款,但文案来自 catalog(那份是英文基线)。
|
||||
fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> Element<'a, Message> {
|
||||
let height = ui.metrics().control_height;
|
||||
let content_color = if selected {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//! | `crates/i18n/src/catalog/settings/general.rs` / `appearance.rs` | 两组的文案 key |
|
||||
//! | `crates/app-iced/src/app/update/settings/language_appearance.rs` | 消息的落点 |
|
||||
//!
|
||||
//! 真控件那半边在 `pages/appearance/section.rs`(`appearance.rs` 只负责把 `body` 挂上去)。
|
||||
//! 真控件那半边在 `pages/appearance/section/`(`appearance.rs` 只负责把 `body` 挂上去)。
|
||||
|
||||
/// 语言 + 外观线的状态(**只归本线**)。
|
||||
///
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
//! | 分组 | 文件(正文 / 状态 / 消息) | 谁会动它 |
|
||||
//! |---|---|---|
|
||||
//! | General | [`general`] | language+appearance 线(语言下拉 + 主题 / 启动页) |
|
||||
//! | Appearance | [`appearance`] | language+appearance 线(真控件已在 `pages/appearance/section.rs`) |
|
||||
//! | Appearance | [`appearance`] | language+appearance 线(真控件已在 `pages/appearance/section/`) |
|
||||
//! | Java | [`java`] | java 线 |
|
||||
//! | Downloads | [`download`] | download 线 |
|
||||
//! | Updates | [`update`] | update 线(M159,按下标分发) |
|
||||
@@ -117,7 +117,7 @@
|
||||
//! 与 M106 同形状),要么读 `Groups` 里本线的字段。两种形状都是「谁先撞谁付一次钱」的一次性代价,
|
||||
//! 不是永久性四路撞车。
|
||||
//! 5. **首帧取数 / 异步发起**(同上,r2 复审 P1 补)—— [`Body`] 是
|
||||
//! `fn(&Ui, States) -> Option<Element>`,返回不了 `Task`;而且 `settings::view` → `pages::render`
|
||||
//! `fn(&Ui, States, Lang) -> Option<Element>`,返回不了 `Task`;而且 `settings::view` → `pages::render`
|
||||
//! → `App::view` **一路都返回 `Element`**(iced 0.14 的 `view` 也只返回 `Element`,`view(&self)`
|
||||
//! 又改不了状态),**view 期间产出的 `Task` 没有出口**。所以只有一条路:在 `app/update.rs` 的
|
||||
//! `Message::Boot` 分支补一次首帧派发(**1 个共享文件**——`on_boot` 本来就在派
|
||||
@@ -193,13 +193,16 @@ impl Default for Language {
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个分组正文的**真控件渲染器**:入参是「画一帧时那份只读状态」(组状态 + 外观状态),
|
||||
/// 返回 `None` = 这一组现在没东西可画(例如外观分组在 harness 路径下没有外观状态),
|
||||
/// 由 [`form`] 退回演示行。
|
||||
/// 一个分组正文的**真控件渲染器**:入参是「画一帧时那份只读状态」(组状态 + 外观状态)
|
||||
/// 与**这一次生效的语言**,返回 `None` = 这一组现在没东西可画(例如外观分组在 harness 路径下
|
||||
/// 没有外观状态),由 [`form`] 退回演示行。
|
||||
///
|
||||
/// 各组自己的渲染器**定义在自己文件里**,签名是这一条:于是「这一组要读哪些状态」由它自己决定,
|
||||
/// 本文件不需要认识任何一组的字段(r1 复审 P1 要的正是这一点)。
|
||||
type Body = for<'a> fn(&'a Ui, States<'a>) -> Option<Element<'a, Message>>;
|
||||
///
|
||||
/// `lang` 是 M179 加的:槽位此前拿不到语言,唯一挂了真控件的外观组只能写死英文;接进**唯一的那个
|
||||
/// 槽位**比再给 [`form`] 加一条按下标分发的 `else if` 更省行数(本文件贴着 400 行硬顶)。
|
||||
type Body = for<'a> fn(&'a Ui, States<'a>, Lang) -> Option<Element<'a, Message>>;
|
||||
|
||||
/// 一个设置分组:名字、图标、演示行、(可选)把这一组正文画成真控件的渲染器。
|
||||
///
|
||||
@@ -236,9 +239,9 @@ const SELECTED: usize = 0;
|
||||
|
||||
/// General 分组在 [`GROUPS`] 里的下标(0)。它与 [`SELECTED`] 同值**是巧合,不是依赖**。
|
||||
///
|
||||
/// 为什么需要它:语言行要的是「当前档位 + 生效语言」([`Language`]),而 `Group.body` 的签名只带
|
||||
/// [`States`](外观 + 组状态)——给 `States` 加字段要动 `settings/state.rs` 与 4 处 struct literal
|
||||
/// (见本文件文档「仍然共享」第 4 条),给 `Body` 加形参会令另外三组各自的 `body` 函数编译不过。
|
||||
/// 为什么需要它:语言行要的是「当前档位 + 生效语言」([`Language`]),而 `Group.body` 只带
|
||||
/// [`States`] 与**生效语言**(`Lang`,M179)——档位 `LangChoice` 装不进去(`States` 也装不下:
|
||||
/// 加字段要动 `settings/state.rs` 与 4 处 struct literal,见本文件文档「仍然共享」第 4 条)。
|
||||
/// 所以这里做一次**按下标的分发**:代价是一行 `if`,换来 `general::GROUP.body` 仍是 `None`
|
||||
/// (`tests/mod.rs` 的「只有外观组带真控件 body」照样成立)。
|
||||
const GENERAL_GROUP: usize = 0;
|
||||
@@ -266,9 +269,9 @@ const UPDATE_GROUP: usize = update::UPDATE_GROUP;
|
||||
|
||||
/// 「助手(AI)」分组在 [`GROUPS`] 里的下标(真源在 `settings/ai_about.rs`,这里只转发)。
|
||||
///
|
||||
/// 与 [`GENERAL_GROUP`] / [`UPDATE_GROUP`] 同一条做法:本组要画中英两套文案(`Lang`)与
|
||||
/// 「测试连接」的结论,`Group.body` 的签名里没有 `Lang`,所以由 `view` 的 `language` 形参一路
|
||||
/// 递到 [`form`] 的分发点,`GROUP.body` 保持 `None`。
|
||||
/// 与 [`GENERAL_GROUP`] / [`UPDATE_GROUP`] 同一条做法:由 `view` 的 `language` 形参递到 [`form`]
|
||||
/// 按本下标分发,`GROUP.body` 保持 `None`(本组**尚未**迁到 [`Body`] 槽位:槽位现在带 `Lang` 了,
|
||||
/// 迁它要把 `ai::view::body` 的返回值改成 `Option<Element>`,是一次独立重构——M179 不做)。
|
||||
const AI_GROUP: usize = ai_about::AI_GROUP;
|
||||
|
||||
/// 「Java」分组在 [`GROUPS`] 里的下标(真源在 `settings/java.rs`,这里只转发)。
|
||||
@@ -315,8 +318,8 @@ pub fn view<'a>(
|
||||
///
|
||||
/// **分发点(两条路,M106 起就并存)**:
|
||||
///
|
||||
/// 1. `GROUP.body`——`Some(f)` 让 `f` 自己去 [`States`] 里取它要的数据(`None` 返回值 ⇒ 退回
|
||||
/// 演示行),`None` 直接画演示行([`frame::demo_card`]);
|
||||
/// 1. `GROUP.body`——`Some(f)` 让 `f` 自己去 [`States`] 里取它要的数据、并从形参拿**生效语言**
|
||||
/// (`None` 返回值 ⇒ 退回演示行),`None` 直接画演示行([`frame::demo_card`]);
|
||||
/// 2. **按下标分发**——General(要语言档位)、Update(要 presenter 快照)、Assistant(要 `Lang`
|
||||
/// 与测试结论)、Java(要 `Lang` 与扫描槽)四组在下面各有 `else if`,它们的 `GROUP.body`
|
||||
/// 仍是 `None`(理由见各自的 `<组>_GROUP` 常量)。
|
||||
@@ -357,7 +360,7 @@ fn form<'a>(
|
||||
} else {
|
||||
group
|
||||
.body
|
||||
.and_then(|body| body(ui, states))
|
||||
.and_then(|body| body(ui, states, language.effective))
|
||||
.unwrap_or_else(|| frame::demo_card(ui, group.rows))
|
||||
};
|
||||
|
||||
|
||||
@@ -2,12 +2,25 @@
|
||||
//! 一条 section 断言都没有;这 3 条是新增,不是搬移)。
|
||||
//!
|
||||
//! **归 language+appearance 线**:这是设置页唯一接了真控件的一组,真控件实现全在
|
||||
//! `pages/appearance/section.rs`;本文件只守「注册表这一侧的接线」。
|
||||
//! `pages/appearance/section/`;本文件只守「注册表这一侧的接线」。
|
||||
//! 本组已经把 `body` 接上了;`tests/mod.rs` 那条「只有一组有真控件」的**聚合绊线已在 M101 r2 删除**
|
||||
//! (被四条 per-group 断言完全蕴含——代价是「唯一性」从此没人守,那段说明写在 `tests/mod.rs` 同处)。
|
||||
//!
|
||||
//! M179 `I18NSEAM` 加的第 4 条守的是**语言真的进到了渲染层**(此前这一组在中文界面上 0/27 条本地化)。
|
||||
|
||||
use super::super::appearance;
|
||||
use super::super::{demo_groups, view, Language, States};
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_i18n::catalog::Key;
|
||||
use unlucky_i18n::{Lang, LangChoice};
|
||||
use unlucky_presenter::prefs::{AppearanceBase, AppearancePrefs};
|
||||
|
||||
use crate::cli::Backend;
|
||||
use crate::pages::appearance::{probe, State as AppearanceState};
|
||||
use crate::pages::layout::{frame_metrics, PaneKit};
|
||||
use crate::pages::Update;
|
||||
use crate::theme::{Families, Ui};
|
||||
|
||||
/// 这一组的**演示行**(没有外观运行期状态时才画)。
|
||||
///
|
||||
@@ -50,3 +63,109 @@ fn the_appearance_group_keeps_a_real_control_body() {
|
||||
`body` 是本文件那个本地 fn;具体渲染器由那一行源码 + 截图担保,函数指针相等性断言不了)"
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- 语言(M179 I18NSEAM)
|
||||
|
||||
/// **本组的语言绊线**:中文界面下,外观分组渲染出来的字里**必须含汉字**。
|
||||
///
|
||||
/// 走的是真路:`settings::view`(真语言分发)→ `Group.body(lang)` → `pages::appearance::section`
|
||||
/// → `section::t(key, lang)`;取词口 `t` 把每条译文记进 `pages::appearance::probe`,测试断言这批字。
|
||||
///
|
||||
/// 为什么不在 `Element` 上遍历:iced 0.14 **没有**可遍历控件树的公开接口(`Widget::children()`
|
||||
/// 返回的是状态 `Tree`;走 `operate` 要 `Tree` + 布局 + `Renderer`,单测里没有渲染器)。取证点
|
||||
/// 因此退到「字进树之前」的唯一取词口,代价是**看不见像素**——字体能不能画出汉字要截图判
|
||||
/// (M179 报告「没验的部分」列了这条)。
|
||||
///
|
||||
/// 抓得住:① `section::t` 退回写死 `Lang::En`(M179 之前正是这一行 ⇒ 中文界面上这一组 0/27
|
||||
/// 条本地化);② `settings::view` 不把 `language.effective` 递给 `Group.body`(语言到不了渲染层);
|
||||
/// ③ 五个 key 的译文在金表里丢了中文。
|
||||
/// 抓不住:本组其余文案仍是英文**字面量**(没有 key,M172 口径的 27 条里占 22 条)——这条守的是
|
||||
/// 语言的**管子**,不是文案;补文案是下一条 mission(M179 报告逐行列了那些字面量)。
|
||||
#[test]
|
||||
fn the_appearance_group_speaks_chinese_when_the_interface_does() {
|
||||
let zh = appearance_texts(Lang::ZhCn);
|
||||
assert!(
|
||||
!zh.is_empty(),
|
||||
"外观分组一条词表文案都没取到:渲染路径没走到 `section::t`(语言管子断了?)"
|
||||
);
|
||||
assert!(
|
||||
zh.iter()
|
||||
.any(|(key, _)| *key == Key::SettingsAppearanceCorner),
|
||||
"渲染路径没画到「窗口圆角」那一行:取词记录里没有它的 key,{zh:?}"
|
||||
);
|
||||
|
||||
let han: Vec<&str> = zh
|
||||
.iter()
|
||||
.map(|(_, text)| *text)
|
||||
.filter(|text| text.chars().any(is_han))
|
||||
.collect();
|
||||
assert!(
|
||||
!han.is_empty(),
|
||||
"中文界面下外观分组渲染出来的字全是非汉字(`section::t` 没跟随语言):{zh:?}"
|
||||
);
|
||||
|
||||
// 反向:英文界面下同一批字里不许出现汉字(否则「含汉字」可能来自写死的中文,而不是语言管子)。
|
||||
let en = appearance_texts(Lang::En);
|
||||
assert!(
|
||||
en.iter().all(|(_, text)| !text.chars().any(is_han)),
|
||||
"英文界面下外观分组出现了汉字:{en:?}"
|
||||
);
|
||||
let corner = |texts: &[(Key, &'static str)]| {
|
||||
texts
|
||||
.iter()
|
||||
.find(|(key, _)| *key == Key::SettingsAppearanceCorner)
|
||||
.map(|(_, text)| *text)
|
||||
};
|
||||
assert_ne!(
|
||||
corner(&zh),
|
||||
corner(&en),
|
||||
"同一个 key 在中英两种语言下取到了同一条译文:语言没生效"
|
||||
);
|
||||
}
|
||||
|
||||
/// 画一帧设置页(选中「外观」分组)并取走这一帧从词表取到的全部译文。
|
||||
fn appearance_texts(lang: Lang) -> Vec<(Key, &'static str)> {
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
let ui = Ui::new(SchemeFlavor::Light, FAMILIES);
|
||||
let state = AppearanceState::new(
|
||||
&AppearancePrefs::factory(),
|
||||
&AppearanceBase::factory(),
|
||||
Backend::Auto,
|
||||
SchemeFlavor::Light,
|
||||
);
|
||||
assert_eq!(
|
||||
state.selected_group,
|
||||
crate::pages::appearance::APPEARANCE_GROUP,
|
||||
"工厂态默认选中「外观」分组(否则这一帧根本画不到本组)"
|
||||
);
|
||||
// 前面几条用例不取词,这里先清一次,避免把别的渲染留下的记录算进来。
|
||||
probe::take();
|
||||
|
||||
let metrics = frame_metrics(PaneKit::Supporting, 1180.0);
|
||||
let update = Update::default();
|
||||
let _tree = view(
|
||||
&ui,
|
||||
metrics,
|
||||
States {
|
||||
appearance: Some(&state),
|
||||
groups: demo_groups(),
|
||||
},
|
||||
Language {
|
||||
choice: LangChoice::System,
|
||||
effective: lang,
|
||||
},
|
||||
&update,
|
||||
);
|
||||
|
||||
probe::take()
|
||||
}
|
||||
|
||||
/// 有没有汉字(判据与 `app/view/tests/launch.rs` 的 `is_cjk` 同一段码位)。
|
||||
fn is_han(ch: char) -> bool {
|
||||
('\u{4e00}'..='\u{9fff}').contains(&ch)
|
||||
}
|
||||
@@ -8,8 +8,8 @@
|
||||
//! 假值不许回来,语言行的三档必须真的能在两套目录里取到词。
|
||||
//!
|
||||
//! 接线形状:语言行的正文由 [`general::body`] 画(入口是 `pages::settings` 的 `GENERAL_GROUP`
|
||||
//! 按下标分发),`GROUP.body` 仍是 `None`——那个槽位的签名是 `fn(&Ui, States) -> Option<Element>`,
|
||||
//! `States` 只有 `appearance` + `groups` 两格,装不下「当前语言档位」。所以 M101 r2 的
|
||||
//! 按下标分发),`GROUP.body` 仍是 `None`——那个槽位现在带**生效语言**(`Lang`,M179)但仍是
|
||||
//! `States` + 语言,装不下「当前语言档位」(`LangChoice`)。所以 M101 r2 的
|
||||
//! 「本组接真控件只需改两个文件」这条口径,在语言行上**仍然是两个文件**。
|
||||
|
||||
use super::super::general;
|
||||
@@ -187,8 +187,8 @@ fn the_language_row_offers_three_choices_in_both_languages() {
|
||||
/// **绊线**:这一组仍不走 `Group.body`。
|
||||
///
|
||||
/// 为什么不是「接了真控件」:语言行的入口是 `pages::Language`(`GENERAL_GROUP` 那条分发),
|
||||
/// 而 `Group.body` 那个槽位的签名是 `fn(&Ui, &pages::appearance::State)`——装不下「当前语言档位」。
|
||||
/// 谁把语言行塞进 `body`,就得先给它换签名(那会改到另外三组的文件),这条断言逼他先想一遍。
|
||||
/// 而 `Group.body` 那个槽位只拿到 `States` 与**生效语言**(`Lang`,M179)——装不下「当前语言档位」。
|
||||
/// 谁把语言行塞进 `body`,就得先给它换签名(那会改到别人那个组的文件),这条断言逼他先想一遍。
|
||||
#[test]
|
||||
fn the_general_group_does_not_use_the_appearance_body_slot() {
|
||||
assert!(
|
||||
|
||||
@@ -40,7 +40,8 @@ fn the_java_group_keeps_the_folder_icon_from_the_visual_review() {
|
||||
|
||||
/// `body` 槽位仍是 `None`:本组靠 `settings/mod.rs` 的按下标分发([`java::JAVA_GROUP`])画正文。
|
||||
///
|
||||
/// 抓得住:谁把 `body: Some(..)` 挂上来却不给它 `Lang`(那样中文界面上这一组会退回英文)。
|
||||
/// 抓得住:谁把 `body: Some(..)` 挂上来(M179 之后槽位自带 `Lang`,所以「挂了 body 却没语言」
|
||||
/// 这种漂移不再可能——挂上来就必然拿到语言;这条守的是**分发形状**别变)。
|
||||
/// 抓不住:分发那一行本身有没有被删(`settings/mod.rs` 的 `form` 里有 `else if`;删了这组就空白)。
|
||||
#[test]
|
||||
fn the_java_group_is_dispatched_by_index_not_by_body() {
|
||||
|
||||
@@ -163,9 +163,10 @@ fn the_split_kept_the_left_column_order() {
|
||||
/// 抓不住:各组的**字段**(那些活在各组自己的文件里,本文件本来就不该知道,也就没有可断言的东西)。
|
||||
///
|
||||
/// **M106 的例外要读清楚**:General 已经接了真控件(语言行),但它**不走 `Group.body`**——
|
||||
/// 那个槽位的签名是 `fn(&Ui, States) -> Option<Element>`,`States` 只有 `appearance` + `groups`
|
||||
/// 两格,装不下「当前语言档位 + 生效语言」。它走 `settings::view` 的 `language` 形参 +
|
||||
/// `GENERAL_GROUP` 那条按下标分发,`body` 仍是 `None`,所以本断言不动。
|
||||
/// 那个槽位的签名是 `fn(&Ui, States, Lang) -> Option<Element>`(`Lang` 是 M179 加的),
|
||||
/// `States` 只有 `appearance` + `groups` 两格、槽位只有**生效语言**,都装不下「当前语言档位 +
|
||||
/// 生效语言」这一对。它走 `settings::view` 的 `language` 形参 + `GENERAL_GROUP` 那条按下标分发,
|
||||
/// `body` 仍是 `None`,所以本断言不动。
|
||||
/// 判据没变:**`body: Some(..)` 仍然只有外观组**。
|
||||
#[test]
|
||||
fn the_settings_module_does_not_name_any_group_state() {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
//!
|
||||
//! ## 为什么这一组不走 `Group.body`
|
||||
//!
|
||||
//! `Group.body` 的签名是 `fn(&Ui, States) -> Option<Element>`,而 `States` 只有「外观状态 + 各组
|
||||
//! 自己的状态」两格。更新状态是 **presenter 快照**(不是某一组的字段),与 M106 的语言行同一种
|
||||
//! 情形:`settings/mod.rs` 按下标([`UPDATE_GROUP`])把 `settings::view` 的 `update` 形参交给
|
||||
//! [`body`],`GROUP.body` 仍是 `None`。
|
||||
//! `Group.body` 的槽位拿的是 [`States`](super::States) 与**生效语言** `Lang`(M179 起),而更新状态
|
||||
//! 是 **presenter 快照**(`pages::Update`,不是某一组的字段、也不在 `States` 里),槽位装不下它。
|
||||
//! 与 M106 的语言行同一种情形:`settings/mod.rs` 按下标([`UPDATE_GROUP`])把 `settings::view` 的
|
||||
//! `update` 形参交给 [`body`],`GROUP.body` 仍是 `None`。
|
||||
//!
|
||||
//! 消息也**不**走 `GroupMessage`:更新动作是「跨页的一次性意图」(检查 / 下载安装 / 忽略 /
|
||||
//! 重启),落点是 `app/update/update_action.rs`,`pages::Message::Update(UpdateAction)` 一支就够。
|
||||
|
||||
Reference in new issue
Block a user