Merge branch 'feat/forcedwire-force-state-through-settings'
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
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (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
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (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
8b48c494cd
25 files changed
+730
-326
No files matched your search
@@ -21,7 +21,9 @@ fn main() -> ExitCode {
|
||||
eprintln!(
|
||||
"用法:pages_demo [--screen=instances|download|settings|logs] [--width <px>] \
|
||||
[--height <px>] [--theme=light|dark] [--screenshot-theme=light|dark] \
|
||||
[--screenshot <png>] [--renderer=auto|gpu|cpu] [--software-renderer]"
|
||||
[--screenshot <png>] [--renderer=auto|gpu|cpu] [--software-renderer] \
|
||||
[--scale-factor <f32>] [--appearance=real|demo] [--passthrough=on|off] \
|
||||
[--force-state=rest|hover|pressed]"
|
||||
);
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::prefs::{AccentError, ACCENT_PRESETS};
|
||||
|
||||
use crate::app::components::inputs::{text_field, Field};
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::pages::Message;
|
||||
use crate::theme::{to_iced, Ui, STROKE_PX};
|
||||
|
||||
@@ -44,6 +45,7 @@ pub(super) fn push<'a>(
|
||||
ui: &'a Ui,
|
||||
state: &'a State,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
rows: &mut Vec<Element<'a, Message>>,
|
||||
) {
|
||||
let accent = &state.accent;
|
||||
@@ -68,7 +70,7 @@ pub(super) fn push<'a>(
|
||||
.into(),
|
||||
);
|
||||
|
||||
rows.push(presets(ui, accent, lang));
|
||||
rows.push(presets(ui, accent, lang, forced));
|
||||
|
||||
// 非法值:**结构化的原因**,不是「格式不对」这种没法行动的一句话。
|
||||
if let Some(error) = accent.error() {
|
||||
@@ -97,7 +99,12 @@ fn accent_commit() -> Message {
|
||||
///
|
||||
/// 为什么分两行:一行放 9 个控件在设置卡片的正文宽度里会被挤成「7 + 1」折行,最后一颗色块
|
||||
/// 孤零零掉到下一行(出图实测)。分两行是**有意**的排版,不是被挤出来的。
|
||||
fn presets<'a>(ui: &'a Ui, accent: &'a Accent, lang: Lang) -> Element<'a, Message> {
|
||||
fn presets<'a>(
|
||||
ui: &'a Ui,
|
||||
accent: &'a Accent,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let current = accent.persist_value();
|
||||
let head = row![
|
||||
container(label(ui, t(Key::SettingsAppearanceAccentPresets, lang)))
|
||||
@@ -107,6 +114,7 @@ fn presets<'a>(ui: &'a Ui, accent: &'a Accent, lang: Lang) -> Element<'a, Messag
|
||||
t(Key::SettingsAppearanceAccentFactory, lang),
|
||||
current.is_empty(),
|
||||
Change::Accent(String::new()),
|
||||
forced,
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
|
||||
@@ -13,6 +13,7 @@ use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::prefs::BgFill;
|
||||
|
||||
use crate::app::components::inputs::{slider, text_field, Field, Slider};
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
@@ -27,6 +28,7 @@ pub(super) fn push<'a>(
|
||||
ui: &'a Ui,
|
||||
state: &'a State,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
rows: &mut Vec<Element<'a, Message>>,
|
||||
) {
|
||||
// ④ 背景图开关
|
||||
@@ -39,12 +41,14 @@ pub(super) fn push<'a>(
|
||||
t(Key::SettingsAppearanceOff, lang),
|
||||
!state.bg_enabled,
|
||||
Change::BgEnabled(false),
|
||||
forced,
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceOn, lang),
|
||||
state.bg_enabled,
|
||||
Change::BgEnabled(true),
|
||||
forced,
|
||||
),
|
||||
],
|
||||
));
|
||||
@@ -109,18 +113,21 @@ pub(super) fn push<'a>(
|
||||
t(Key::SettingsAppearanceBgFillCover, lang),
|
||||
state.bg_fill == BgFill::Cover,
|
||||
Change::BgFill(BgFill::Cover),
|
||||
forced,
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceBgFillContain, lang),
|
||||
state.bg_fill == BgFill::Contain,
|
||||
Change::BgFill(BgFill::Contain),
|
||||
forced,
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceBgFillTile, lang),
|
||||
state.bg_fill == BgFill::Tile,
|
||||
Change::BgFill(BgFill::Tile),
|
||||
forced,
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
@@ -5,6 +5,7 @@ use unlucky_i18n::catalog::Key;
|
||||
use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::prefs::PaletteSource;
|
||||
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
@@ -17,6 +18,7 @@ pub(super) fn push<'a>(
|
||||
ui: &'a Ui,
|
||||
state: &'a State,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
rows: &mut Vec<Element<'a, Message>>,
|
||||
) {
|
||||
// ⑨ 配色来源
|
||||
@@ -29,18 +31,21 @@ pub(super) fn push<'a>(
|
||||
t(Key::SettingsAppearancePaletteBackground, lang),
|
||||
state.palette_source == PaletteSource::BackgroundImage,
|
||||
Change::PaletteSource(PaletteSource::BackgroundImage),
|
||||
forced,
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
t(Key::SettingsAppearancePaletteWallpaper, lang),
|
||||
state.palette_source == PaletteSource::Wallpaper,
|
||||
Change::PaletteSource(PaletteSource::Wallpaper),
|
||||
forced,
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
t(Key::SettingsAppearancePalettePreset, lang),
|
||||
state.palette_source == PaletteSource::Preset,
|
||||
Change::PaletteSource(PaletteSource::Preset),
|
||||
forced,
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
@@ -85,15 +85,24 @@ mod window;
|
||||
///
|
||||
/// `lang` 是**这一次生效的语言**:`settings::view` 的 `Language.effective`(M179)。壳把各段按
|
||||
/// **视觉顺序**拼起来;每一段的语句逐字来自拆分前的 `section()`。
|
||||
pub fn section<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Message> {
|
||||
///
|
||||
/// `forced` 是 `--force-state` 的落点(M285,`pages::Model::forced` 一路递到 `Group.body`):`None` =
|
||||
/// 按控件自己的观测态画(产品路径的常态);`Some(..)` 只服务取证出图。它只改**画出来的**态——
|
||||
/// 每一段把它交给 [`choice`],由 [`super::super::settings::resolve_state`] 那个唯一 resolve 点落地。
|
||||
pub fn section<'a>(
|
||||
ui: &'a Ui,
|
||||
state: &'a State,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let mut rows: Vec<Element<'a, Message>> = Vec::new();
|
||||
|
||||
// M250:强调色排在最前 —— 它是「配色可自定义」这一页最直接的入口(键一直在,控件是新的)。
|
||||
accent::push(ui, state, lang, &mut rows);
|
||||
window::push(ui, state, lang, &mut rows);
|
||||
accent::push(ui, state, lang, forced, &mut rows);
|
||||
window::push(ui, state, lang, forced, &mut rows);
|
||||
panel::push(ui, state, lang, &mut rows);
|
||||
bg_image::push(ui, state, lang, &mut rows);
|
||||
colour_source::push(ui, state, lang, &mut rows);
|
||||
bg_image::push(ui, state, lang, forced, &mut rows);
|
||||
colour_source::push(ui, state, lang, forced, &mut rows);
|
||||
|
||||
card(
|
||||
ui,
|
||||
@@ -237,11 +246,14 @@ pub(super) fn choice_row<'a>(
|
||||
}
|
||||
|
||||
/// 一个互斥按钮(选中态用 `SecondaryContainer`,未选中用透明 + `OnSurfaceVariant`)。
|
||||
///
|
||||
/// `forced`(M285)只往下递给 [`choice_style`]:`Some` 时按钮按那一态画,`None` 时按观测态画。
|
||||
pub(super) fn choice<'a>(
|
||||
ui: &'a Ui,
|
||||
text: &'a str,
|
||||
selected: bool,
|
||||
change: Change,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let height = ui.metrics().control_height;
|
||||
let radius = ui.radius(ShapeScale::Full, height);
|
||||
@@ -263,7 +275,7 @@ pub(super) fn choice<'a>(
|
||||
)
|
||||
.height(height)
|
||||
.padding(Padding::from([0.0, ui.space(Space::X3)]))
|
||||
.style(choice_style(ui, selected, content_color, radius))
|
||||
.style(choice_style(ui, selected, content_color, radius, forced))
|
||||
.on_press(Message::Appearance(change))
|
||||
.into()
|
||||
}
|
||||
@@ -282,6 +294,7 @@ fn choice_style<'a>(
|
||||
selected: bool,
|
||||
content_color: Color,
|
||||
radius: Radius,
|
||||
forced: Option<Interaction>,
|
||||
) -> impl Fn(&IcedTheme, button::Status) -> button::Style + 'a {
|
||||
move |_theme, status| {
|
||||
let background = if selected {
|
||||
@@ -289,7 +302,7 @@ fn choice_style<'a>(
|
||||
} else {
|
||||
interaction::layer_color(
|
||||
ui,
|
||||
Interaction::from_status(status),
|
||||
crate::pages::settings::resolve_state(forced, status),
|
||||
Role::OnSurface,
|
||||
frame::BASE.role(),
|
||||
)
|
||||
@@ -353,6 +366,7 @@ mod tests {
|
||||
false,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
ui.radius(ShapeScale::Full, ui.metrics().control_height),
|
||||
None,
|
||||
);
|
||||
let theme = ui.theme();
|
||||
let hover = solid(style(&theme, button::Status::Hovered));
|
||||
|
||||
@@ -14,6 +14,7 @@ use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::prefs::WindowCorner;
|
||||
|
||||
use crate::app::components::inputs::{slider, Slider};
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
@@ -27,6 +28,7 @@ pub(super) fn push<'a>(
|
||||
ui: &'a Ui,
|
||||
state: &'a State,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
rows: &mut Vec<Element<'a, Message>>,
|
||||
) {
|
||||
// ① 整窗透明度
|
||||
@@ -59,12 +61,14 @@ pub(super) fn push<'a>(
|
||||
t(Key::SettingsAppearanceCornerSystem, lang),
|
||||
corner == WindowCorner::System,
|
||||
Change::Corner(WindowCorner::System),
|
||||
forced,
|
||||
),
|
||||
choice(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceCornerSquare, lang),
|
||||
corner == WindowCorner::Square,
|
||||
Change::Corner(WindowCorner::Square),
|
||||
forced,
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
@@ -129,7 +129,7 @@ fn appearance_texts(lang: Lang, case: Case) -> Vec<(Key, String)> {
|
||||
probe::take();
|
||||
probe::take_rendered();
|
||||
|
||||
let _tree = super::section(&ui, &state, lang);
|
||||
let _tree = super::section(&ui, &state, lang, None);
|
||||
|
||||
let mut texts: Vec<(Key, String)> = probe::take()
|
||||
.into_iter()
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//! **刻意不接 `cli.rs`**:等 `cli.rs` 具备 `--screen` / `--window-width` 之后,这个 harness 应当
|
||||
//! 删掉,改由正式启动参数出图。参数:`--screen=instances|download|settings|logs`、`--width`、
|
||||
//! `--height`、`--theme` / `--screenshot-theme`(后者优先)、`--screenshot`、`--renderer`、
|
||||
//! `--software-renderer`、`--scale-factor`、`--appearance=real|demo`、`--passthrough=on|off`。
|
||||
//! `--software-renderer`、`--scale-factor`、`--appearance=real|demo`、`--passthrough=on|off`、
|
||||
//! `--force-state=rest|hover|pressed`(M285:把控件钉到那一态出图,`rest` / 不给时截图通道默认就是它)。
|
||||
//! 解析与默认值在 [`request`](M100 拆出去:本文件当时正好 400 行、零余量);没给 `--screenshot`
|
||||
//! 就只是开窗看。
|
||||
//!
|
||||
@@ -40,9 +41,43 @@
|
||||
//! 改成 900×600,落盘的就是 900×600 那张)。现在闸门在 [`should_request_capture`],日志里的
|
||||
//! `落盘次数=` 必须恒为 1。
|
||||
//!
|
||||
//! **为什么不是给每个控件灌 forced**:设置页「外观」区的滑块 / 文本框 / 裸 `iced::button` 都不
|
||||
//! 消费 forced,那样做要改十几个组件文件——为取证去动产品组件,代价与风险都不对,而且新产品
|
||||
//! 组件随时会漏。
|
||||
//! **forced 能到哪儿(M285 订正)**:`--force-state` 只改**画出来的**态,不改状态机。它经
|
||||
//! `pages::Model.forced` → `settings::view(.., forced)` → `Group.body` 递到各控件。
|
||||
//!
|
||||
//! **已经消费的**:外观区 11 处 `choice`、语言 3 枚 / 主题 3 枚 chip、日志 2 颗 `action_button`
|
||||
//! ——这几类(`choice` / chip / `action_button`)一律走 `settings::resolve_state` 这个唯一 resolve 点;
|
||||
//! 另有 `components::buttons::Button` 的 7 处 `.state(..)` 走 `settings::forced_button`
|
||||
//! (update 3 / ai 2 / java 2)。
|
||||
//!
|
||||
//! **仍然不消费的(残留快照——**别**把它们当成「已接」)**:
|
||||
//!
|
||||
//! 1. **Downloads 组整组**:`settings/download.rs` 的 `body` 形参就是 `_forced`(接住不用),组里的
|
||||
//! `switch` / `chip` / 两颗 `Button::new` 都不看 forced。实测 `--screen=download` 加
|
||||
//! `--force-state=rest|hover|pressed` **三张图 md5 全等**(`e283c585fc06a0dd08efaf5596c9aa6f`);
|
||||
//! 2. 外观区的**滑块 / 文本框 / 8 颗预设色块**(`ACCENT_PRESETS: [&str; 8]`;`accent::swatch` 是
|
||||
//! 本地助手,没有 forced 形参,描边闭包只看 `button::Status`);
|
||||
//! 3. `components::selection` 的 `chip` / `switch` 站点里**还没搭线的那几处**(已知:update 的自动
|
||||
//! 检查 filter chip、ai 的 provider chip / reveal chip / 开关、download 的 switch 与 chip、java 的
|
||||
//! `prefs_view` 两对上下移 chip 与 `download_view` 的三处 chip、以及 **`java/view.rs` 运行时的
|
||||
//! 「设为默认」chip**——`action_chip` 在非默认行返回可点的 `Chip::filter`,`runtime_row` 没有
|
||||
//! forced 形参。这一串会漂,接线时请更新)。**同源 chip 里 `general/language.rs` 与
|
||||
//! `general/theme.rs` 那 6 枚是已接的**(见上一段),别算进这里;
|
||||
//! 4. java 的字段 / 预设按钮(`java/fields.rs`、`java/prefs_view.rs`)——`java/download_view.rs` 里
|
||||
//! 一个 `Button` 都没有,它的下载控件是 chip,已在第 3 条;
|
||||
//! 5. **其它组的文本框**:`ai/view.rs` 的 base_url / model / api_key 三只 `Field`(java 的两样在第 4
|
||||
//! 条、download 的整组在第 1 条)。`Field` 本来就有 `.state(..)`,这里是没搭线。
|
||||
//!
|
||||
//! 这份清单是**残留快照,不是契约**:接上任何一处就把它从清单里划掉,别让它变成「文档说没接所以
|
||||
//! 不用接」;也别把它当穷举——判据只有一条:**这个控件有没有拿到 `forced`**。
|
||||
//!
|
||||
//! ⚠️ **别把「没搭线」说成「组件库缺能力」**:`Slider::state` / `Field::state` / `Chip::state` /
|
||||
//! `Switch::state` 全都存在且**在被消费**(它们本来就是 gallery 状态矩阵用的「强制视觉态」)。
|
||||
//! 上面那些站点只是没把 `forced` 递进去;真正缺 forced 入口的只有本地助手 `accent::swatch`。
|
||||
//!
|
||||
//! 本段之前写的是「裸 `iced::button` 都不消费 forced」——**那句正是 M282/M284 放弃像素验收的理由**,
|
||||
//! 已经不成立(残余清单之外的那几类现在出得来像素证据,见 M285 的 `DevDocs/artifacts/forcedwire/`)。
|
||||
//! 但也**别**反过来把「裸 `button(...)`」当成「已消费」的同义词:8 颗预设色块本身就是裸
|
||||
//! `button(...)` 且不消费。
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run -p unlucky-app-iced --example pages_demo -- --screen=instances --width=1180 \
|
||||
|
||||
@@ -146,12 +146,15 @@ pub fn run(request: Request) -> Result<(), String> {
|
||||
iced::application(
|
||||
move || {
|
||||
let mut model = demo_model(boot.screen, boot.width, boot.height);
|
||||
// 截图通道把**消费 forced 的那批控件**(实例页)钉成常态,与产品 `--screenshot`
|
||||
// 的 `ForcedState::Rest` 同一条纪律。它管不到设置页 / 外观区(那边不读 forced),
|
||||
// 那两屏的密闭靠窗口级 passthrough——别把这条当成万能的。
|
||||
if boot.screenshot.is_some() {
|
||||
model.forced = Some(Interaction::Enabled);
|
||||
}
|
||||
// 截图通道把消费 forced 的控件钉成常态,与产品 `--screenshot` 的 `ForcedState::Rest`
|
||||
// 同一条纪律;显式的 `--force-state=hover|pressed` **优先**(那是故意要画那个态)。
|
||||
//
|
||||
// M285:这条线现在真的到得了设置页 / 外观区——`pages::Model.forced` 经
|
||||
// `settings::view(.., forced)` → `Group.body` → 语言档 / 主题档 / 日志按钮 / 外观 choice,
|
||||
// 由 `settings::resolve_state` 落到样式闭包。此前那四处的状态层只有单测证据。
|
||||
model.forced = boot
|
||||
.force_state
|
||||
.or_else(|| boot.screenshot.is_some().then_some(Interaction::Enabled));
|
||||
// M250:gallery 出图也要能看见强调色。**规则不在这里抄** —— 调产品用的同一个
|
||||
// `theme::tokens`(优先级只定义一次)。如实补一句:harness 不建 `App`,所以这里
|
||||
// 没有 D38 取色方案(第三个实参恒 `None`),出图**不覆盖**那条分支。
|
||||
|
||||
@@ -12,6 +12,7 @@ use std::path::PathBuf;
|
||||
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::cli::Backend;
|
||||
use crate::pages::{layout::WidthClass, Screen};
|
||||
|
||||
@@ -47,6 +48,18 @@ pub struct Request {
|
||||
///
|
||||
/// 只对 `--screen=settings` 有意义:别的页没有外观分组,静默接受会造出「以为接了线」的假图(U1)。
|
||||
pub real_appearance: bool,
|
||||
/// 强制交互态(`--force-state=rest|hover|pressed`):`Some` 时**控件按那一态画**(M285)。
|
||||
///
|
||||
/// 为什么这个开关在本文件而不在产品 bin:产品 bin 的 `--screen` 只认 `shell|gallery`,设置页 /
|
||||
/// 外观区只能走本 harness 出图(`examples/pages_demo.rs`)。M282 / M284 各自撞过一次——
|
||||
/// `pages::Model.forced` 到不了设置页,于是「hover 与 pressed 必须不同」这条判据**做不出像素证据**,
|
||||
/// 只能降级成单测。接上之后两条态各自一张图。
|
||||
///
|
||||
/// 词与语义与产品的 `cli::ForcedState` 一致(`rest|hover|pressed`),但**刻意不共用那个 enum**:
|
||||
/// 产品那条通道的默认值与守卫(`--screenshot` 恒钉 `Rest`)是另一套,把两者拧在一起会让
|
||||
/// 「取证通道的默认值」和「产品窗口的默认值」互相污染。这里存的是下游形状 [`Interaction`],
|
||||
/// 落点是 `pages::Model.forced`(`harness/app.rs`)。
|
||||
pub force_state: Option<Interaction>,
|
||||
/// 截图窗口开**指针穿透**(`window::enable_mouse_passthrough` ⇒ winit `set_cursor_hittest(false)`)。
|
||||
///
|
||||
/// 默认:给了 `--screenshot` 就开;**交互模式(没有截图)一律关**——交互窗口收不到鼠标是产品事故,
|
||||
@@ -68,6 +81,7 @@ impl Default for Request {
|
||||
backend: Backend::Auto,
|
||||
scale_factor: None,
|
||||
real_appearance: false,
|
||||
force_state: None,
|
||||
passthrough: false,
|
||||
}
|
||||
}
|
||||
@@ -138,6 +152,20 @@ impl Request {
|
||||
}
|
||||
};
|
||||
}
|
||||
"--force-state" => {
|
||||
// 三档词与产品 bin 同一套(`cli.rs` 的 `--force-state`),报错文案也照抄。
|
||||
request.force_state =
|
||||
Some(match take(&mut args, inline, "--force-state")?.as_str() {
|
||||
"rest" => Interaction::Enabled,
|
||||
"hover" => Interaction::Hovered,
|
||||
"pressed" => Interaction::Pressed,
|
||||
other => {
|
||||
return Err(format!(
|
||||
"--force-state 只认 rest|hover|pressed,收到 {other:?}"
|
||||
))
|
||||
}
|
||||
});
|
||||
}
|
||||
"--passthrough" => {
|
||||
passthrough = Some(match take(&mut args, inline, "--passthrough")?.as_str() {
|
||||
"on" => true,
|
||||
@@ -183,7 +211,7 @@ impl Request {
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
/// 出图文件名片段(`<screen>[-real]-<class>-<flavor>-w<宽>`,归档用)。
|
||||
/// 出图文件名片段(`<screen>[-real][-hover|-pressed]-<class>-<flavor>-w<宽>`,归档用)。
|
||||
pub fn slug(&self) -> String {
|
||||
let class = WidthClass::of(self.width);
|
||||
let flavor = if self.flavor.is_dark() {
|
||||
@@ -198,6 +226,13 @@ impl Request {
|
||||
} else {
|
||||
self.screen.slug().to_owned()
|
||||
};
|
||||
// 强制态也进文件名(`-hover` / `-pressed`):同一屏在两个 forced 下是**两张不同的证据图**,
|
||||
// 归档里必须能分开(与 `-real` 同一条理由)。`rest` / 不给 = 不加后缀 ⇒ 既有文件名逐字不变。
|
||||
let screen = match self.force_state {
|
||||
Some(Interaction::Hovered) => format!("{screen}-hover"),
|
||||
Some(Interaction::Pressed) => format!("{screen}-pressed"),
|
||||
_ => screen,
|
||||
};
|
||||
format!("{screen}-{}-{flavor}-w{width}", class.name())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +282,9 @@ fn body<'a>(
|
||||
content::view(ui, metrics, &model.content, model.lang, model.window_height)
|
||||
}
|
||||
// M159:设置页「更新」组要画 presenter 的更新快照(不在 `States` 里,理由见该组文档)。
|
||||
Screen::Settings => settings::view(ui, metrics, states, language, &model.update),
|
||||
Screen::Settings => {
|
||||
settings::view(ui, metrics, states, language, &model.update, model.forced)
|
||||
}
|
||||
Screen::Logs => logs::view(ui, metrics, &model.logs, model.lang),
|
||||
};
|
||||
|
||||
|
||||
@@ -24,11 +24,12 @@ use unlucky_i18n::{Catalog, Key, Lang};
|
||||
|
||||
use crate::app::components::buttons::{self, Button};
|
||||
use crate::app::components::inputs::{text_field, Field, FieldVariant};
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::app::components::selection::{chip, switch, Chip, Switch};
|
||||
use crate::pages::frame::{card, divider, text_role_owned};
|
||||
use crate::pages::settings::ai::outcome::ProbeKind;
|
||||
use crate::pages::settings::ai::state::{Probe, ProviderId, SaveNotice, Secret, State, Status};
|
||||
use crate::pages::settings::{ai_about, GroupMessage, States};
|
||||
use crate::pages::settings::{ai_about, forced_button, GroupMessage, States};
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
@@ -38,11 +39,21 @@ fn msg(message: ai_about::Message) -> Message {
|
||||
}
|
||||
|
||||
/// 一张卡片(`pages::settings::form` 按下标把 `Lang` 递进来,见 `settings/mod.rs`)。
|
||||
pub(crate) fn body<'a>(ui: &'a Ui, states: States<'a>, lang: Lang) -> Element<'a, Message> {
|
||||
body_of(ui, &states.groups.ai_about, lang)
|
||||
pub(crate) fn body<'a>(
|
||||
ui: &'a Ui,
|
||||
states: States<'a>,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
body_of(ui, &states.groups.ai_about, lang, forced)
|
||||
}
|
||||
|
||||
fn body_of<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Message> {
|
||||
fn body_of<'a>(
|
||||
ui: &'a Ui,
|
||||
state: &'a State,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let catalog = lang.catalog();
|
||||
let mut stacked = column![].spacing(ui.space(Space::X2)).width(Length::Fill);
|
||||
|
||||
@@ -56,7 +67,7 @@ fn body_of<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Message>
|
||||
stacked = stacked.push(model_field(ui, catalog, state));
|
||||
stacked = stacked.push(key_field(ui, catalog, state));
|
||||
stacked = stacked.push(divider(ui));
|
||||
stacked = stacked.push(actions_row(ui, catalog, state));
|
||||
stacked = stacked.push(actions_row(ui, catalog, state, forced));
|
||||
stacked = stacked.push(hint_line(ui, catalog, state));
|
||||
|
||||
if let Some(notice) = &state.notice {
|
||||
@@ -267,10 +278,21 @@ fn key_field<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'a,
|
||||
.into()
|
||||
}
|
||||
|
||||
fn actions_row<'a>(ui: &'a Ui, catalog: Catalog, state: &State) -> Element<'a, Message> {
|
||||
fn actions_row<'a>(
|
||||
ui: &'a Ui,
|
||||
catalog: Catalog,
|
||||
state: &State,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let save_label = catalog.text(Key::SettingsAiSave);
|
||||
let save = if state.dirty && state.storage {
|
||||
buttons::filled(ui, Button::new(save_label, msg(ai_about::Message::Save)))
|
||||
buttons::filled(
|
||||
ui,
|
||||
forced_button(
|
||||
Button::new(save_label, msg(ai_about::Message::Save)),
|
||||
forced,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
buttons::filled(ui, Button::disabled(save_label))
|
||||
};
|
||||
@@ -281,7 +303,13 @@ fn actions_row<'a>(ui: &'a Ui, catalog: Catalog, state: &State) -> Element<'a, M
|
||||
catalog.text(Key::SettingsAiTest)
|
||||
};
|
||||
let test = if state.can_test() {
|
||||
buttons::tonal(ui, Button::new(test_label, msg(ai_about::Message::Test)))
|
||||
buttons::tonal(
|
||||
ui,
|
||||
forced_button(
|
||||
Button::new(test_label, msg(ai_about::Message::Test)),
|
||||
forced,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
buttons::tonal(ui, Button::disabled(test_label))
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ use iced::Element;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_i18n::Lang;
|
||||
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::{Group, Message, States};
|
||||
@@ -39,9 +40,15 @@ pub(super) const GROUP: Group = Group {
|
||||
|
||||
/// 本组的真控件渲染器:有外观状态就画真控件(M93),没有(harness)返回 `None`。
|
||||
///
|
||||
/// `lang` 一路递给 `pages::appearance::section`——它是这一组唯一知道「界面现在是什么语言」的东西。
|
||||
fn body<'a>(ui: &'a Ui, states: States<'a>, lang: Lang) -> Option<Element<'a, Message>> {
|
||||
/// `lang` 一路递给 `pages::appearance::section`——它是这一组唯一知道「界面现在是什么语言」的东西;
|
||||
/// `forced`(`--force-state`,M285)同样一路递下去,由 `section` 分给各段控件。
|
||||
fn body<'a>(
|
||||
ui: &'a Ui,
|
||||
states: States<'a>,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
) -> Option<Element<'a, Message>> {
|
||||
states
|
||||
.appearance
|
||||
.map(|state| crate::pages::appearance::section(ui, state, lang))
|
||||
.map(|state| crate::pages::appearance::section(ui, state, lang, forced))
|
||||
}
|
||||
@@ -59,6 +59,7 @@ use unlucky_presenter::dto::find_row;
|
||||
|
||||
use crate::app::components::buttons::{self, Button};
|
||||
use crate::app::components::inputs::{text_field, Field, FieldVariant};
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::app::components::selection::{chip, switch, Chip, Switch};
|
||||
use crate::pages::frame::text_role_owned;
|
||||
use crate::pages::Message as PageMessage;
|
||||
@@ -69,6 +70,9 @@ use super::{Group, States};
|
||||
|
||||
/// Downloads 分组。
|
||||
///
|
||||
/// `body` 的 `_forced`(`--force-state`,M285)**接住不用**:本组控件是 `components::selection`
|
||||
/// 的 `chip` / `switch` 与 `Field`,components 层没有 forced 开口。留形参只为 `Group.body` 只有一个签名。
|
||||
///
|
||||
/// `rows` 只在**没有快照**时画(harness / 首帧):它们是语义行(出厂值 / 区间 / 三态),
|
||||
/// 不是当前值——M193 用户的投诉正是「把演示值当成了真值」。
|
||||
pub(super) const GROUP: Group = Group {
|
||||
@@ -179,7 +183,12 @@ fn msg(message: Message) -> PageMessage {
|
||||
///
|
||||
/// 为什么不给 [`block`] 加 padding:那会把卡片的**第一行顶距**也撑开(卡内边距 20 + 12),与其它四组
|
||||
/// 的第一行不再对齐;`spacing` 只作用在子节点之间,首行顶距与末行底距仍是卡片的 20。
|
||||
fn body<'a>(ui: &'a Ui, states: States<'a>, lang: Lang) -> Option<Element<'a, PageMessage>> {
|
||||
fn body<'a>(
|
||||
ui: &'a Ui,
|
||||
states: States<'a>,
|
||||
lang: Lang,
|
||||
_forced: Option<Interaction>,
|
||||
) -> Option<Element<'a, PageMessage>> {
|
||||
let group = state_group(&states)?;
|
||||
let drafts = &states.groups.download;
|
||||
let catalog = lang.catalog();
|
||||
|
||||
@@ -372,8 +372,9 @@ mod no_stray_empty_state {
|
||||
fn the_form_holds_exactly_one_column_per_group() {
|
||||
let ui = ui();
|
||||
let update = Update::default();
|
||||
let language = Language::default();
|
||||
|
||||
let narrow = section_columns(&ui, None, states(), Language::default(), &update);
|
||||
let narrow = section_columns(&ui, None, states(), language, &update, None);
|
||||
assert_eq!(
|
||||
narrow.len(),
|
||||
GROUPS.len(),
|
||||
@@ -381,7 +382,7 @@ mod no_stray_empty_state {
|
||||
);
|
||||
|
||||
for index in 0..GROUPS.len() {
|
||||
let single = section_columns(&ui, Some(index), states(), Language::default(), &update);
|
||||
let single = section_columns(&ui, Some(index), states(), language, &update, None);
|
||||
assert_eq!(
|
||||
single.len(),
|
||||
1,
|
||||
|
||||
@@ -38,20 +38,16 @@
|
||||
//!
|
||||
//! 分组元数据只有一份(这里的 [`GROUP`]):`settings/mod.rs` 的 `GROUPS` 直接引用它。
|
||||
|
||||
use iced::border::Radius;
|
||||
use iced::widget::{button, column, container, row};
|
||||
use iced::{
|
||||
Alignment, Background, Border, Color, Element, Length, Padding, Shadow, Theme as IcedTheme,
|
||||
};
|
||||
use iced::widget::{column, container, row};
|
||||
use iced::{Alignment, Element, Length, Padding};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::catalog::Key;
|
||||
use unlucky_i18n::LangChoice;
|
||||
|
||||
use crate::app::components::interaction::{self, Interaction};
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::fonts::install::{CjkInstall, FetchClass};
|
||||
use crate::pages::{frame, Language, Message};
|
||||
use crate::theme::Ui;
|
||||
@@ -59,6 +55,7 @@ use crate::theme::Ui;
|
||||
use super::frame::{card, divider};
|
||||
use super::{Group, States};
|
||||
|
||||
mod language;
|
||||
mod theme;
|
||||
|
||||
/// General 分组(左列名固定英文:左列是**导航**,与导航轨同源,不进 catalog)。
|
||||
@@ -76,10 +73,17 @@ pub(super) const GROUP: Group = Group {
|
||||
};
|
||||
|
||||
/// 这一组的正文(**产品路径**)。
|
||||
pub(super) fn body<'a>(ui: &'a Ui, states: States<'a>, language: Language) -> Element<'a, Message> {
|
||||
///
|
||||
/// `forced` 是 `--force-state`(M285):本组只有语言行 / 主题行 / 日志行三处有可交互控件,三处都往下递。
|
||||
pub(super) fn body<'a>(
|
||||
ui: &'a Ui,
|
||||
states: States<'a>,
|
||||
language: Language,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let catalog = language.effective.catalog();
|
||||
let mut stacked = column![].spacing(0.0).width(Length::Fill);
|
||||
stacked = stacked.push(language_row(ui, language));
|
||||
stacked = stacked.push(language::view(ui, language, forced));
|
||||
// M168(D28 后半):中文字体的下载状态行。紧贴语言行——用户在这里手选中文,反馈就该在这行下面,
|
||||
// 而不是等他切到别的页面才发现「刚才那几秒在干嘛」。
|
||||
stacked = stacked.push(divider(ui));
|
||||
@@ -92,12 +96,13 @@ pub(super) fn body<'a>(ui: &'a Ui, states: States<'a>, language: Language) -> El
|
||||
ui,
|
||||
&catalog,
|
||||
states.groups.language_appearance.theme,
|
||||
forced,
|
||||
));
|
||||
|
||||
// M188:日志文件夹(路径 + 打开 + 复制路径)。放在「启动页/关闭按钮」之前——用户投诉
|
||||
// 「找不到崩溃日志」要的答案,不该埋在只读行后面。
|
||||
stacked = stacked.push(divider(ui));
|
||||
stacked = stacked.push(super::logs_row::folder_row(ui, &catalog));
|
||||
stacked = stacked.push(super::logs_row::folder_row(ui, &catalog, forced));
|
||||
|
||||
// 启动页 / 关闭按钮:还没有后端,只读;文案已经进 catalog。
|
||||
for (label, value) in [
|
||||
@@ -215,66 +220,6 @@ fn font_row<'a>(ui: &'a Ui, language: Language) -> Element<'a, Message> {
|
||||
stacked.into()
|
||||
}
|
||||
|
||||
/// 语言行:标题 + 生效语言回显 + 三档互斥按钮 + 一句说明。
|
||||
fn language_row<'a>(ui: &'a Ui, language: Language) -> Element<'a, Message> {
|
||||
let catalog = language.effective.catalog();
|
||||
let effective = LangChoice::Fixed(language.effective).label_key();
|
||||
|
||||
let head = row![
|
||||
container(frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodyMedium,
|
||||
false,
|
||||
catalog.text(Key::SettingsLanguage),
|
||||
ui.color(Role::OnSurface),
|
||||
))
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center),
|
||||
frame::text_role_owned(
|
||||
ui,
|
||||
TypeRole::LabelLarge,
|
||||
true,
|
||||
catalog.render(
|
||||
Key::SettingsLanguageEffective,
|
||||
&[("language", catalog.text(effective))],
|
||||
),
|
||||
ui.color(Role::OnPrimaryFixedVariant),
|
||||
),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
let chips = row(LangChoice::ALL
|
||||
.iter()
|
||||
.map(|choice| {
|
||||
chip(
|
||||
ui,
|
||||
catalog.text(choice.label_key()),
|
||||
*choice == language.choice,
|
||||
*choice,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
.spacing(ui.space(Space::X1))
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
column![
|
||||
head,
|
||||
chips,
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
catalog.text(Key::SettingsLanguageHint),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(Length::Fill)
|
||||
.padding(Padding::from([ui.space(Space::X2), 0.0]))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一个只读的「标签 + 取值」行(与 `frame::demo_card` 的一行同高同排版)。
|
||||
fn setting_row<'a>(ui: &'a Ui, label: &'a str, value: &'a str) -> Element<'a, Message> {
|
||||
let height = ui.metrics().control_height + Space::X2.dp();
|
||||
@@ -302,89 +247,3 @@ fn setting_row<'a>(ui: &'a Ui, label: &'a str, value: &'a str) -> Element<'a, Me
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一个互斥按钮(选中态 `SecondaryContainer`,未选中透明 + `OnSurfaceVariant`)。
|
||||
///
|
||||
/// 与 `pages/appearance/section/mod.rs` 的 `choice` 同款几何,但文案来自 catalog(那份是英文基线)。
|
||||
///
|
||||
/// **几何(M229)**:取规格卡的 chip(高 32、圆角 8、内距 16、未选中 1px `OutlineVariant`),
|
||||
/// 与同屏的主题三档 chips 完全同源——两行都是「并排的互斥筛选」,长得不一样会让读者以为是
|
||||
/// 两种控件(M229 第一版就是这样:语言 40 高药丸、主题 32 高方角,出图一眼就不齐)。
|
||||
fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> Element<'a, Message> {
|
||||
let box_ = unlucky_design::inkcat::metrics::CHIP;
|
||||
let height = box_.height;
|
||||
let content_color = if selected {
|
||||
ui.color(Role::OnSecondaryContainer)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
};
|
||||
let border_color = ui.color(Role::OutlineVariant);
|
||||
let stroke = crate::theme::STROKE_PX;
|
||||
let radius = ui.radius(box_.shape, height);
|
||||
|
||||
button(
|
||||
container(frame::text_role(
|
||||
ui,
|
||||
TypeRole::LabelLarge,
|
||||
selected,
|
||||
text,
|
||||
content_color,
|
||||
))
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.height(height)
|
||||
.padding(Padding::from([
|
||||
0.0,
|
||||
unlucky_design::inkcat::metrics::CHIP_PAD_X,
|
||||
]))
|
||||
.style(chip_style(
|
||||
ui,
|
||||
selected,
|
||||
content_color,
|
||||
border_color,
|
||||
stroke,
|
||||
radius,
|
||||
))
|
||||
.on_press(Message::Language(choice))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// [`chip`] 的样式闭包(抽成自由函数:单测要**直接**喂 `Hovered` / `Pressed` 两态)。
|
||||
///
|
||||
/// - **α 只从 [`Interaction`] 来**:hover 8% / pressed 10%(M3)。M284 之前两种态合并成一条
|
||||
/// `matches!(status, Hovered | Pressed)` 分支、都叠 `StateLayer::Hover`,按下于是被压成 8%;
|
||||
/// - **合成底是 [`frame::BASE`]**(M237:这一组住在 `frame::card` 里,状态层叠卡底而不是画布)。
|
||||
fn chip_style<'a>(
|
||||
ui: &'a Ui,
|
||||
selected: bool,
|
||||
content_color: Color,
|
||||
border_color: Color,
|
||||
stroke: f32,
|
||||
radius: Radius,
|
||||
) -> impl Fn(&IcedTheme, button::Status) -> button::Style + 'a {
|
||||
move |_theme, status| {
|
||||
let background = if selected {
|
||||
Some(Background::Color(ui.color(Role::SecondaryContainer)))
|
||||
} else {
|
||||
interaction::layer_color(
|
||||
ui,
|
||||
Interaction::from_status(status),
|
||||
Role::OnSurface,
|
||||
frame::BASE.role(),
|
||||
)
|
||||
.map(Background::Color)
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background,
|
||||
text_color: content_color,
|
||||
border: Border {
|
||||
color: border_color,
|
||||
width: if selected { 0.0 } else { stroke },
|
||||
radius,
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
//! 设置页 General 组的**语言行**(M285 `LANGSET`:从 `general.rs` 纯搬移)。
|
||||
//!
|
||||
//! 搬出来的理由只有行数:`general.rs` 390 行、撞 400 行硬线(`components/gate.rs`),
|
||||
//! 而 M285 要给这一行的三枚互斥按钮接 `forced: Option<Interaction>`(`--force-state` 直通)。
|
||||
//! 切法与 [`super::theme`] 完全同构——**一个行控件一个文件**:正文 [`view`] + 它的 [`chip`]
|
||||
//! + [`chip_style`](样式闭包抽成自由函数,单测才能直接喂 `Hovered` / `Pressed` 两态)。
|
||||
//!
|
||||
//! 搬移是逐字搬的:三个函数的语句、注释一个字没动。
|
||||
|
||||
use iced::border::Radius;
|
||||
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::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::catalog::Key;
|
||||
use unlucky_i18n::LangChoice;
|
||||
|
||||
use crate::app::components::interaction::{self, Interaction};
|
||||
use crate::pages::{frame, Language, Message};
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// 语言行:标题 + 生效语言回显 + 三档互斥按钮 + 一句说明。
|
||||
pub(super) fn view<'a>(
|
||||
ui: &'a Ui,
|
||||
language: Language,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let catalog = language.effective.catalog();
|
||||
let effective = LangChoice::Fixed(language.effective).label_key();
|
||||
|
||||
let head = row![
|
||||
container(frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodyMedium,
|
||||
false,
|
||||
catalog.text(Key::SettingsLanguage),
|
||||
ui.color(Role::OnSurface),
|
||||
))
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center),
|
||||
frame::text_role_owned(
|
||||
ui,
|
||||
TypeRole::LabelLarge,
|
||||
true,
|
||||
catalog.render(
|
||||
Key::SettingsLanguageEffective,
|
||||
&[("language", catalog.text(effective))],
|
||||
),
|
||||
ui.color(Role::OnPrimaryFixedVariant),
|
||||
),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
let chips = row(LangChoice::ALL
|
||||
.iter()
|
||||
.map(|choice| {
|
||||
chip(
|
||||
ui,
|
||||
catalog.text(choice.label_key()),
|
||||
*choice == language.choice,
|
||||
*choice,
|
||||
forced,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
.spacing(ui.space(Space::X1))
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
column![
|
||||
head,
|
||||
chips,
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
catalog.text(Key::SettingsLanguageHint),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(Length::Fill)
|
||||
.padding(Padding::from([ui.space(Space::X2), 0.0]))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一个互斥按钮(选中态 `SecondaryContainer`,未选中透明 + `OnSurfaceVariant`)。
|
||||
///
|
||||
/// 与 `pages/appearance/section/mod.rs` 的 `choice` 同款几何,但文案来自 catalog(那份是英文基线)。
|
||||
///
|
||||
/// **几何(M229)**:取规格卡的 chip(高 32、圆角 8、内距 16、未选中 1px `OutlineVariant`),
|
||||
/// 与同屏的主题三档 chips 完全同源——两行都是「并排的互斥筛选」,长得不一样会让读者以为是
|
||||
/// 两种控件(M229 第一版就是这样:语言 40 高药丸、主题 32 高方角,出图一眼就不齐)。
|
||||
fn chip<'a>(
|
||||
ui: &'a Ui,
|
||||
text: &'a str,
|
||||
selected: bool,
|
||||
choice: LangChoice,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let box_ = unlucky_design::inkcat::metrics::CHIP;
|
||||
let height = box_.height;
|
||||
let content_color = if selected {
|
||||
ui.color(Role::OnSecondaryContainer)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
};
|
||||
let border_color = ui.color(Role::OutlineVariant);
|
||||
let stroke = crate::theme::STROKE_PX;
|
||||
let radius = ui.radius(box_.shape, height);
|
||||
|
||||
button(
|
||||
container(frame::text_role(
|
||||
ui,
|
||||
TypeRole::LabelLarge,
|
||||
selected,
|
||||
text,
|
||||
content_color,
|
||||
))
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.height(height)
|
||||
.padding(Padding::from([
|
||||
0.0,
|
||||
unlucky_design::inkcat::metrics::CHIP_PAD_X,
|
||||
]))
|
||||
.style(chip_style(
|
||||
ui,
|
||||
selected,
|
||||
content_color,
|
||||
border_color,
|
||||
stroke,
|
||||
radius,
|
||||
forced,
|
||||
))
|
||||
.on_press(Message::Language(choice))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// [`chip`] 的样式闭包(抽成自由函数:单测要**直接**喂 `Hovered` / `Pressed` 两态)。
|
||||
///
|
||||
/// `forced`(M285)是 `--force-state` 的落点,交给 `settings::resolve_state`(全仓唯一的 resolve 点);
|
||||
/// `None` 时行为与 M285 之前逐字节相同。`pub(super)` 是因为 `general/theme.rs` 的单测要直接喂它。
|
||||
///
|
||||
/// - **α 只从 [`Interaction`] 来**:hover 8% / pressed 10%(M3)。M284 之前两种态合并成一条
|
||||
/// `matches!(status, Hovered | Pressed)` 分支、都叠 `StateLayer::Hover`,按下于是被压成 8%;
|
||||
/// - **合成底是 [`frame::BASE`]**(M237:这一组住在 `frame::card` 里,状态层叠卡底而不是画布)。
|
||||
pub(super) fn chip_style<'a>(
|
||||
ui: &'a Ui,
|
||||
selected: bool,
|
||||
content_color: Color,
|
||||
border_color: Color,
|
||||
stroke: f32,
|
||||
radius: Radius,
|
||||
forced: Option<Interaction>,
|
||||
) -> impl Fn(&IcedTheme, button::Status) -> button::Style + 'a {
|
||||
move |_theme, status| {
|
||||
let background = if selected {
|
||||
Some(Background::Color(ui.color(Role::SecondaryContainer)))
|
||||
} else {
|
||||
interaction::layer_color(
|
||||
ui,
|
||||
crate::pages::settings::resolve_state(forced, status),
|
||||
Role::OnSurface,
|
||||
frame::BASE.role(),
|
||||
)
|
||||
.map(Background::Color)
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background,
|
||||
text_color: content_color,
|
||||
border: Border {
|
||||
color: border_color,
|
||||
width: if selected { 0.0 } else { stroke },
|
||||
radius,
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,14 @@ use crate::theme::Ui;
|
||||
pub(super) const THEME_MODES: [ThemeMode; 3] = [ThemeMode::Dark, ThemeMode::Light, ThemeMode::Auto];
|
||||
|
||||
/// 主题行:标题 + 三档互斥 chip + 一句说明(排版与语言行同族)。
|
||||
pub(super) fn view<'a>(ui: &'a Ui, catalog: &Catalog, mode: ThemeMode) -> Element<'a, Message> {
|
||||
///
|
||||
/// `forced`(M285)只往下递给 chip:`None` = 按观测态画(产品路径的常态)。
|
||||
pub(super) fn view<'a>(
|
||||
ui: &'a Ui,
|
||||
catalog: &Catalog,
|
||||
mode: ThemeMode,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let head = row![
|
||||
container(frame::text_role(
|
||||
ui,
|
||||
@@ -61,6 +68,7 @@ pub(super) fn view<'a>(ui: &'a Ui, catalog: &Catalog, mode: ThemeMode) -> Elemen
|
||||
catalog.text(mode_label(*choice)),
|
||||
*choice == mode,
|
||||
*choice,
|
||||
forced,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
@@ -97,7 +105,13 @@ fn mode_label(mode: ThemeMode) -> Key {
|
||||
///
|
||||
/// 几何取规格卡的 chip(高 32、圆角 8,`inkcat::metrics::CHIP`):主题三档是**可并排的筛选**,
|
||||
/// 不是主行动按钮,用 chip 档比语言行那枚 40 高药丸更贴规格卡。
|
||||
fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, mode: ThemeMode) -> Element<'a, Message> {
|
||||
fn chip<'a>(
|
||||
ui: &'a Ui,
|
||||
text: &'a str,
|
||||
selected: bool,
|
||||
mode: ThemeMode,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let box_ = chipbox::CHIP;
|
||||
let height = box_.height;
|
||||
let content_color = if selected {
|
||||
@@ -128,6 +142,7 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, mode: ThemeMode) -> Eleme
|
||||
border_color,
|
||||
stroke,
|
||||
radius,
|
||||
forced,
|
||||
))
|
||||
.on_press(Message::Theme(mode))
|
||||
.into()
|
||||
@@ -145,6 +160,7 @@ fn chip_style<'a>(
|
||||
border_color: Color,
|
||||
stroke: f32,
|
||||
radius: Radius,
|
||||
forced: Option<Interaction>,
|
||||
) -> impl Fn(&IcedTheme, button::Status) -> button::Style + 'a {
|
||||
move |_theme, status| {
|
||||
let background = if selected {
|
||||
@@ -152,7 +168,7 @@ fn chip_style<'a>(
|
||||
} else {
|
||||
interaction::layer_color(
|
||||
ui,
|
||||
Interaction::from_status(status),
|
||||
crate::pages::settings::resolve_state(forced, status),
|
||||
Role::OnSurface,
|
||||
frame::BASE.role(),
|
||||
)
|
||||
@@ -254,17 +270,19 @@ mod tests {
|
||||
ui.color(Role::OutlineVariant),
|
||||
crate::theme::STROKE_PX,
|
||||
ui.radius(chipbox::CHIP.shape, chipbox::CHIP.height),
|
||||
None,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// **语言行那枚 chip**(`general::chip` 的 `chip_style`)同理:hover 8% / pressed 10%。
|
||||
///
|
||||
/// 为什么这条断言住在 `theme.rs` 而不是 `general.rs`:后者已经 389 行(硬线 400),而
|
||||
/// `general` 的私有项对子模块可见(Rust 的可见性包含后代模块),搬过来不用加 `pub`。
|
||||
/// 为什么这条断言住在 `theme.rs` 而不是语言行自己的文件:M285 起语言行在
|
||||
/// `general/language.rs`,它的 `chip_style` 声明成 `pub(super)`(`general` 子树可见),
|
||||
/// 所以这里不必把它抬成 crate 面。
|
||||
#[test]
|
||||
fn the_language_chip_draws_pressed_darker_than_hover() {
|
||||
use super::super::chip_style as language_chip_style;
|
||||
use super::super::language::chip_style as language_chip_style;
|
||||
|
||||
let ui = ui();
|
||||
assert_two_layers(
|
||||
@@ -276,6 +294,7 @@ mod tests {
|
||||
ui.color(Role::OutlineVariant),
|
||||
crate::theme::STROKE_PX,
|
||||
ui.radius(chipbox::CHIP.shape, chipbox::CHIP.height),
|
||||
None,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::{Catalog, Key, Lang};
|
||||
|
||||
use crate::app::components::buttons::{self, Button};
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::app::components::selection::{chip, Chip};
|
||||
use crate::pages::frame::{card, divider, text_role_owned, BASE};
|
||||
use crate::pages::Message;
|
||||
@@ -32,7 +33,7 @@ use crate::theme::Ui;
|
||||
use super::fields::{actions_row, add_row, hint_line, jvm_field, memory_field, notice_line};
|
||||
use super::state::{Progress, RuntimeRow, State};
|
||||
use super::Message as JavaMessage;
|
||||
use crate::pages::settings::{GroupMessage, States};
|
||||
use crate::pages::settings::{forced_button, GroupMessage, States};
|
||||
|
||||
/// 一条本组消息 → 页面消息。
|
||||
pub(super) fn msg(message: JavaMessage) -> Message {
|
||||
@@ -40,15 +41,25 @@ pub(super) fn msg(message: JavaMessage) -> Message {
|
||||
}
|
||||
|
||||
/// 一张卡片(`pages::settings::form` 按下标把 `Lang` 递进来)。
|
||||
pub(crate) fn body<'a>(ui: &'a Ui, states: States<'a>, lang: Lang) -> Element<'a, Message> {
|
||||
body_of(ui, &states.groups.java, lang)
|
||||
pub(crate) fn body<'a>(
|
||||
ui: &'a Ui,
|
||||
states: States<'a>,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
body_of(ui, &states.groups.java, lang, forced)
|
||||
}
|
||||
|
||||
fn body_of<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Message> {
|
||||
fn body_of<'a>(
|
||||
ui: &'a Ui,
|
||||
state: &'a State,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let catalog = lang.catalog();
|
||||
let mut stacked = column![].spacing(ui.space(Space::X2)).width(Length::Fill);
|
||||
|
||||
stacked = stacked.push(status_row(ui, catalog, state));
|
||||
stacked = stacked.push(status_row(ui, catalog, state, forced));
|
||||
stacked = stacked.push(divider(ui));
|
||||
stacked = stacked.push(runtime_list(ui, catalog, state));
|
||||
stacked = stacked.push(divider(ui));
|
||||
@@ -77,7 +88,12 @@ fn body_of<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Message>
|
||||
///
|
||||
/// 「取消」的语义(r1 复审 P2-5):按下之后到线程真退出之间,界面显示的是**正在取消…**,
|
||||
/// 「立即扫描」保持禁用——否则用户能在旧线程还活着的时候起第二个。
|
||||
fn status_row<'a>(ui: &'a Ui, catalog: Catalog, state: &State) -> Element<'a, Message> {
|
||||
fn status_row<'a>(
|
||||
ui: &'a Ui,
|
||||
catalog: Catalog,
|
||||
state: &State,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let (text, scanning) = match (state.scanning, state.scanned) {
|
||||
(Some(_), _) if state.cancelled => {
|
||||
(catalog.text(Key::SettingsJavaCancelling).to_string(), true)
|
||||
@@ -121,16 +137,22 @@ fn status_row<'a>(ui: &'a Ui, catalog: Catalog, state: &State) -> Element<'a, Me
|
||||
} else {
|
||||
buttons::filled(
|
||||
ui,
|
||||
Button::new(scan_label, msg(JavaMessage::Scan)).base(BASE),
|
||||
forced_button(
|
||||
Button::new(scan_label, msg(JavaMessage::Scan)).base(BASE),
|
||||
forced,
|
||||
),
|
||||
)
|
||||
};
|
||||
// 已经按过取消(线程还没退出)时,「取消」也灰掉:再按一次没有任何意义。
|
||||
let cancel = buttons::tonal(
|
||||
ui,
|
||||
if scanning && !state.cancelled {
|
||||
Button::new(
|
||||
catalog.text(Key::SettingsJavaCancel),
|
||||
msg(JavaMessage::CancelScan),
|
||||
forced_button(
|
||||
Button::new(
|
||||
catalog.text(Key::SettingsJavaCancel),
|
||||
msg(JavaMessage::CancelScan),
|
||||
),
|
||||
forced,
|
||||
)
|
||||
} else {
|
||||
Button::disabled(catalog.text(Key::SettingsJavaCancel))
|
||||
|
||||
@@ -29,7 +29,11 @@ use crate::pages::{frame, Action, Message};
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// 这一行(竖排:标签 / 路径 / 两个按钮 / 一句说明)。
|
||||
pub(super) fn folder_row<'a>(ui: &'a Ui, catalog: &Catalog) -> Element<'a, Message> {
|
||||
pub(super) fn folder_row<'a>(
|
||||
ui: &'a Ui,
|
||||
catalog: &Catalog,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let path = crate::app::logs_folder::logs_dir()
|
||||
.map(|dir| dir.display().to_string())
|
||||
.unwrap_or_else(|| catalog.text(Key::SettingsLogsUnknown).to_string());
|
||||
@@ -40,11 +44,13 @@ pub(super) fn folder_row<'a>(ui: &'a Ui, catalog: &Catalog) -> Element<'a, Messa
|
||||
ui,
|
||||
catalog.text(Key::LogsOpenFolder),
|
||||
Message::Action(Action::OpenLogs),
|
||||
forced,
|
||||
),
|
||||
action_button(
|
||||
ui,
|
||||
catalog.text(Key::SettingsLogsCopy),
|
||||
Message::CopySummary(path.clone()),
|
||||
forced,
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
@@ -97,7 +103,12 @@ fn frame_text_owned<'a>(
|
||||
}
|
||||
|
||||
/// 描边小按钮(与设置页其他「次级动作」同款:透明底、hover 出状态层、全圆角)。
|
||||
fn action_button<'a>(ui: &'a Ui, label: &'a str, message: Message) -> Element<'a, Message> {
|
||||
fn action_button<'a>(
|
||||
ui: &'a Ui,
|
||||
label: &'a str,
|
||||
message: Message,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let height = ui.metrics().control_height;
|
||||
let content = ui.color(Role::OnSurface);
|
||||
let outline = ui.color(Role::OutlineVariant);
|
||||
@@ -107,7 +118,7 @@ fn action_button<'a>(ui: &'a Ui, label: &'a str, message: Message) -> Element<'a
|
||||
)
|
||||
.height(height)
|
||||
.padding(Padding::from([0.0, ui.space(Space::X3)]))
|
||||
.style(action_style(ui, content, outline, radius))
|
||||
.style(action_style(ui, content, outline, radius, forced))
|
||||
.on_press(message)
|
||||
.into()
|
||||
}
|
||||
@@ -122,11 +133,12 @@ fn action_style<'a>(
|
||||
content: Color,
|
||||
outline: Color,
|
||||
radius: Radius,
|
||||
forced: Option<Interaction>,
|
||||
) -> impl Fn(&IcedTheme, button::Status) -> button::Style + 'a {
|
||||
move |_theme, status| {
|
||||
let background = interaction::layer_color(
|
||||
ui,
|
||||
Interaction::from_status(status),
|
||||
crate::pages::settings::resolve_state(forced, status),
|
||||
Role::OnSurface,
|
||||
frame::BASE.role(),
|
||||
)
|
||||
@@ -189,6 +201,7 @@ mod tests {
|
||||
ui.color(Role::OnSurface),
|
||||
ui.color(Role::OutlineVariant),
|
||||
ui.radius(ShapeScale::Full, ui.metrics().control_height),
|
||||
None,
|
||||
);
|
||||
let theme = ui.theme();
|
||||
let hover = solid(style(&theme, button::Status::Hovered));
|
||||
|
||||
@@ -138,6 +138,7 @@ pub(crate) mod java;
|
||||
pub(crate) mod language_appearance;
|
||||
mod logs_row;
|
||||
mod message;
|
||||
mod registry;
|
||||
mod state;
|
||||
pub(crate) mod update;
|
||||
|
||||
@@ -147,21 +148,51 @@ mod tests;
|
||||
pub use message::GroupMessage;
|
||||
pub use state::{demo_groups, Groups, States};
|
||||
|
||||
use iced::widget::column;
|
||||
use iced::widget::{button, column};
|
||||
use iced::{Element, Length};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::{Lang, LangChoice};
|
||||
|
||||
use crate::app::components::buttons::Button;
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::layout::{FrameMetrics, PageLayout};
|
||||
use super::update::Update;
|
||||
use super::Message;
|
||||
|
||||
pub(super) use registry::Group;
|
||||
use registry::{AI_GROUP, GENERAL_GROUP, GROUPS, GROUP_LABELS, JAVA_GROUP, SELECTED, UPDATE_GROUP};
|
||||
|
||||
/// 这一帧某个控件**实际要画**的交互态:`forced`(`--force-state`)优先,否则退回观测到的 `status`。
|
||||
///
|
||||
/// **全仓唯一的 resolve 点(M285)**:语言档 / 主题档 / 日志 `action_button` / 外观 `choice` 一律调它
|
||||
/// ——各自再写一遍 `unwrap_or_else` 就是把同一行语义复制四份,下次只改三处就等着漂。语义与
|
||||
/// `pages/instances.rs` 的 `forced_state` 相同(`Some` 覆盖、`None` 不动),只是裸 `iced::button` 没有
|
||||
/// `.state()`,覆盖只能发生在样式闭包**里面**(换掉它拿到的那一态)。
|
||||
pub(super) fn resolve_state(forced: Option<Interaction>, status: button::Status) -> Interaction {
|
||||
forced.unwrap_or_else(|| Interaction::from_status(status))
|
||||
}
|
||||
|
||||
/// 把一个 `components::buttons::Button` 钉到 `forced` 那一态(`pages/instances.rs` 的 `forced_state`
|
||||
/// 在设置页的这一份):`Some` ⇒ `.state(..)`,`None` ⇒ 原样返回。
|
||||
///
|
||||
/// ⚠️ **优先级是 `state` 赢**(`Button::forced()` = `self.state.or(没有消息 ⇒ Disabled)`,`state` 在前)
|
||||
/// ⇒ `forced_button(Button::disabled(..), Some(Hovered))` 会把禁用按钮**画成 hover**。今天 7 个站点都
|
||||
/// 写在**启用分支里**所以安全,**别**套到 `if..else` 外面。第二份不是抽象:三次法则到 2。
|
||||
pub(super) fn forced_button<'a, M>(
|
||||
button: Button<'a, M>,
|
||||
forced: Option<Interaction>,
|
||||
) -> Button<'a, M> {
|
||||
match forced {
|
||||
Some(state) => button.state(state),
|
||||
None => button,
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置页 General 分组要的两件(M106):用户选的档位 + 这一次生效的语言。
|
||||
///
|
||||
/// 为什么不塞进 `pages::Model`:那是页面与 presenter 之间的**展示契约**(由快照派生),而档位是
|
||||
@@ -187,103 +218,20 @@ impl Default for Language {
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个分组正文的**真控件渲染器**:入参是「画一帧时那份只读状态」(组状态 + 外观状态)
|
||||
/// 与**这一次生效的语言**,返回 `None` = 这一组现在没东西可画(例如外观分组在 harness 路径下
|
||||
/// 没有外观状态),由 [`form`] 退回演示行。
|
||||
///
|
||||
/// 各组自己的渲染器**定义在自己文件里**,签名是这一条:于是「这一组要读哪些状态」由它自己决定,
|
||||
/// 本文件不需要认识任何一组的字段(r1 复审 P1 要的正是这一点)。
|
||||
///
|
||||
/// `lang` 是 M179 加的:槽位此前拿不到语言,唯一挂了真控件的外观组只能写死英文;接进**唯一的那个
|
||||
/// 槽位**比再给 [`form`] 加一条按下标分发的 `else if` 更省行数(本文件贴着 400 行硬顶)。
|
||||
type Body = for<'a> fn(&'a Ui, States<'a>, Lang) -> Option<Element<'a, Message>>;
|
||||
|
||||
/// 一个设置分组:名字、图标、演示行、(可选)把这一组正文画成真控件的渲染器。
|
||||
///
|
||||
/// 除 `name`/`icon`/`rows`/`body` 外没有别的字段:**分组自己的状态归分组自己**(见 [`state`]),
|
||||
/// 这里只装「怎么画」。
|
||||
struct Group {
|
||||
/// 左列显示名(也是表单列的分组标题)。
|
||||
name: &'static str,
|
||||
/// 左列图标。
|
||||
icon: IconKind,
|
||||
/// 演示行(`body` 为 `None` 时画这些;harness 出图也靠它们)。
|
||||
rows: &'static [(&'static str, &'static str)],
|
||||
/// 真控件渲染器;`None` = 这一组还没接真控件,画 `rows`。
|
||||
body: Option<Body>,
|
||||
}
|
||||
|
||||
/// 分组注册表:**顺序 = 左列自上而下**,也是 `crate::pages::appearance::APPEARANCE_GROUP` 的下标来源。
|
||||
///
|
||||
/// 每一项都指向它自己文件里的 `GROUP`(M99 之前是四份字面量挤在本文件里)。
|
||||
const GROUPS: [Group; 6] = [
|
||||
general::GROUP,
|
||||
appearance::GROUP,
|
||||
java::GROUP,
|
||||
download::GROUP,
|
||||
// M159:自更新只**追加**在末尾(插在中间会让 `APPEARANCE_GROUP` / `GENERAL_GROUP` 两个下标
|
||||
// 全部失效,`tests/mod.rs` 的「左列顺序」那条也会红)。
|
||||
update::GROUP,
|
||||
// M174:助手(AI)配置。同样只追加(下标 = `ai_about::AI_GROUP`),正文走按下标分发。
|
||||
ai_about::GROUP,
|
||||
];
|
||||
|
||||
/// 演示选中的分组(没有真实外观状态时——`pages_demo` harness——用它)。
|
||||
const SELECTED: usize = 0;
|
||||
|
||||
/// General 分组在 [`GROUPS`] 里的下标(0)。它与 [`SELECTED`] 同值**是巧合,不是依赖**。
|
||||
///
|
||||
/// 为什么需要它:语言行要的是「当前档位 + 生效语言」([`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;
|
||||
|
||||
/// 左列分组的**显示名** key(下标与 [`GROUPS`] 一一对应,M106)。
|
||||
///
|
||||
/// 为什么不放进 [`Group`]:那会让另外三组的 `GROUP` 字面量(各自文件、各自填页线)编译不过——
|
||||
/// 与 [`GENERAL_GROUP`] 同一条理由。`Group.name` 因此退化为**id**(测试按它认组;显示走这张表)。
|
||||
pub(super) const GROUP_LABELS: [unlucky_i18n::Key; 6] = [
|
||||
unlucky_i18n::Key::SettingsGroupGeneral,
|
||||
unlucky_i18n::Key::SettingsGroupAppearance,
|
||||
unlucky_i18n::Key::SettingsGroupJava,
|
||||
unlucky_i18n::Key::SettingsGroupDownloads,
|
||||
// M159
|
||||
unlucky_i18n::Key::SettingsGroupUpdate,
|
||||
// M174
|
||||
unlucky_i18n::Key::SettingsGroupAssistant,
|
||||
];
|
||||
|
||||
/// 「更新」分组在 [`GROUPS`] 里的下标(真源在 `settings/update.rs`,这里只转发)。
|
||||
///
|
||||
/// 与 [`GENERAL_GROUP`] 同一种做法:这一组要画的是 **presenter 快照**(不在 [`States`] 里),
|
||||
/// 所以由 `view` 的 `update` 形参一路递到下标的 [`form`] 分发点,`GROUP.body` 保持 `None`。
|
||||
const UPDATE_GROUP: usize = update::UPDATE_GROUP;
|
||||
|
||||
/// 「助手(AI)」分组在 [`GROUPS`] 里的下标(真源在 `settings/ai_about.rs`,这里只转发)。
|
||||
///
|
||||
/// 与 [`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`,这里只转发)。
|
||||
///
|
||||
/// M175 起这一组也按下标分发:它要 `Lang`(中英文案)并且要读后台扫描的快照
|
||||
/// (扫描线程不碰 `App`,结论由心跳灌进 `Groups.java`,页面只从 [`States`] 里取)。
|
||||
const JAVA_GROUP: usize = java::JAVA_GROUP;
|
||||
|
||||
/// 整页。
|
||||
///
|
||||
/// `states` 是「画一帧时那份只读状态」:[`States::appearance`] 是 M93 接进来的外观运行期状态
|
||||
/// (`None` = harness / 演示路径,设置页仍画占位行),[`States::groups`] 是各组自己的状态。
|
||||
///
|
||||
/// `forced` 是 `pages::Model::forced`(`--force-state`,M285):一路递给各组控件,由 [`resolve_state`]
|
||||
/// 决定「这一帧画哪一态」。`None` = 照旧按观测态画(产品路径恒为 `None`,取证启动除外)⇒ 默认逐字节不变。
|
||||
pub fn view<'a>(
|
||||
ui: &'a Ui,
|
||||
metrics: FrameMetrics,
|
||||
states: States<'a>,
|
||||
language: Language,
|
||||
update: &'a Update,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
// 选中分组:有真实外观状态时看状态(点左列可切),否则退回演示值 0。
|
||||
let selected = states
|
||||
@@ -296,10 +244,10 @@ pub fn view<'a>(
|
||||
frame::group_nav(ui, selected, language.effective),
|
||||
frame::left_aligned(
|
||||
metrics.column,
|
||||
form(ui, Some(selected), states, language, update),
|
||||
form(ui, Some(selected), states, language, update, forced),
|
||||
),
|
||||
),
|
||||
_ => frame::single_column(metrics, form(ui, None, states, language, update)),
|
||||
_ => frame::single_column(metrics, form(ui, None, states, language, update, forced)),
|
||||
};
|
||||
|
||||
frame::page(ui, metrics, frame::header(ui, language.effective), body)
|
||||
@@ -324,10 +272,11 @@ fn form<'a>(
|
||||
states: States<'a>,
|
||||
language: Language,
|
||||
update: &'a Update,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let mut stack = column![].spacing(ui.space(Space::X6)).width(Length::Fill);
|
||||
|
||||
for section in section_columns(ui, only, states, language, update) {
|
||||
for section in section_columns(ui, only, states, language, update, forced) {
|
||||
stack = stack.push(section);
|
||||
}
|
||||
|
||||
@@ -342,6 +291,7 @@ fn section_columns<'a>(
|
||||
states: States<'a>,
|
||||
language: Language,
|
||||
update: &'a Update,
|
||||
forced: Option<Interaction>,
|
||||
) -> Vec<Element<'a, Message>> {
|
||||
let catalog = language.effective.catalog();
|
||||
let mut columns = Vec::new();
|
||||
@@ -358,20 +308,20 @@ fn section_columns<'a>(
|
||||
// 档位与生效语言不在 [`States`] 里,只能从 `pages::Language` 进来;它与 `appearance` 在不在场
|
||||
// 无关(harness 那条路径也画得出真控件)。
|
||||
let card = if index == GENERAL_GROUP {
|
||||
general::body(ui, states, language)
|
||||
general::body(ui, states, language, forced)
|
||||
} else if index == UPDATE_GROUP {
|
||||
// M159:更新组读 presenter 快照(`view` 的 `update` 形参),与 General 同一条按下标分发。
|
||||
update::body(ui, update, language.effective)
|
||||
update::body(ui, update, language.effective, forced)
|
||||
} else if index == AI_GROUP {
|
||||
// M174:助手组要 i18n 文案与「测试连接」结论,与 General / Update 同一条按下标分发。
|
||||
ai::view::body(ui, states, language.effective)
|
||||
ai::view::body(ui, states, language.effective, forced)
|
||||
} else if index == JAVA_GROUP {
|
||||
// M175:Java 组要 i18n 文案 + 扫描快照,同样按下标分发(`GROUP.body` 保持 `None`)。
|
||||
java::view::body(ui, states, language.effective)
|
||||
java::view::body(ui, states, language.effective, forced)
|
||||
} else {
|
||||
group
|
||||
.body
|
||||
.and_then(|body| body(ui, states, language.effective))
|
||||
.and_then(|body| body(ui, states, language.effective, forced))
|
||||
.unwrap_or_else(|| frame::demo_card(ui, group.rows))
|
||||
};
|
||||
|
||||
@@ -397,3 +347,51 @@ fn section_columns<'a>(
|
||||
|
||||
columns
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod resolve_tests {
|
||||
use super::*;
|
||||
|
||||
/// 两条语义各钉一次(M285):`forced_button` 的 `Some` 钉态 / `None` 原样(**禁用态也会被 `state`
|
||||
/// 盖掉**——`Button::forced()` 里 `state` 在 disabled 默认之前,钉住实际行为,别让站点踩)、
|
||||
/// `resolve_state` 的 `Some` 覆盖观测 status / `None` 退回观测值。`Button::state` 没有 getter,
|
||||
/// 所以断 derived Debug 的那一格。抓不住:按钮真画成那色(出图负责)、站点传错 forced(只有像素)。
|
||||
#[test]
|
||||
fn forced_button_forces_only_when_asked() {
|
||||
let plain = Button::new("x", ());
|
||||
let forced = format!(
|
||||
"{:?}",
|
||||
forced_button(plain.clone(), Some(Interaction::Hovered))
|
||||
);
|
||||
assert!(
|
||||
forced.contains("state: Some(Hovered)"),
|
||||
"forced 必须钉到那一态:{forced}"
|
||||
);
|
||||
let untouched = format!("{:?}", forced_button(plain, None));
|
||||
assert!(
|
||||
untouched.contains("state: None"),
|
||||
"没给 forced 时必须原样返回:{untouched}"
|
||||
);
|
||||
// 禁用按钮同样会被钉态(`state` 覆盖 disabled 默认,类型层不挡):
|
||||
let disabled: Button<'_, ()> =
|
||||
forced_button(Button::disabled("x"), Some(Interaction::Hovered));
|
||||
assert!(
|
||||
format!("{disabled:?}").contains("state: Some(Hovered)"),
|
||||
"state 在 disabled 默认之前 ⇒ 禁用按钮也会被画成 hover/pressed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forced_state_wins_over_the_observed_status() {
|
||||
assert_eq!(
|
||||
resolve_state(Some(Interaction::Hovered), button::Status::Pressed),
|
||||
Interaction::Hovered,
|
||||
"forced 必须覆盖观测到的 status,否则 --force-state 在这四个控件上等于没接线"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_state(None, button::Status::Pressed),
|
||||
Interaction::Pressed,
|
||||
"没给 forced 时必须退回观测值(既有行为不变)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! 设置页的**分组注册表**(M285 `GROUPSPLIT`:从 `settings/mod.rs` **纯搬移**)。
|
||||
//!
|
||||
//! 本文件只装「有哪些分组、怎么画一组」这一份数据:正文渲染器 [`Body`]、分组形状 [`Group`]、
|
||||
//! 顺序表 [`GROUPS`]、左列显示名 [`GROUP_LABELS`],以及按下标分发用的那几个下标常量。
|
||||
//!
|
||||
//! ## 为什么切在这里
|
||||
//!
|
||||
//! `settings/mod.rs` 撞了 400 行硬线(`components/gate.rs`),而 M285 要给 [`Body`] 加一个
|
||||
//! `forced: Option<Interaction>` 形参(`--force-state` 直通,见 `settings::resolve_state`)。
|
||||
//! 切分按**语义**走,不按行数:`mod.rs` 留「整页 → 表单列 → 分组正文」这条流水线,本文件留
|
||||
//! 「分组是什么」。搬移是逐字搬的——除了声明前面的可见性标注(见下),语句一个字没动;注释里',
|
||||
//! 说的「本文件」仍指 `settings/mod.rs`(那些句子说的是**原来那个文件**的行数压力),别读成本文件。
|
||||
//!
|
||||
//! ## 可见性
|
||||
//!
|
||||
//! `registry` 是 `settings` 的**私有子模块**,所以这里写宽一点也出不去:[`Group`] / [`Body`] 是
|
||||
//! `pub(crate)`,因为 `settings/mod.rs` 要 `pub(super) use registry::Group`(E0365:再导出不能比原
|
||||
//! 声明更可见);其余常量是 `pub(super)`,由 `mod.rs` 私有 `use` 进来,子模块仍按既有写法
|
||||
//! `super::GROUPS` / `super::GROUP_LABELS` 取用(私有 `use` 对后代模块可见)。
|
||||
|
||||
use iced::Element;
|
||||
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_i18n::Lang;
|
||||
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::{ai_about, appearance, download, general, java, update, Message, States};
|
||||
|
||||
/// 一个分组正文的**真控件渲染器**:入参是「画一帧时那份只读状态」(组状态 + 外观状态)
|
||||
/// 与**这一次生效的语言**,返回 `None` = 这一组现在没东西可画(例如外观分组在 harness 路径下
|
||||
/// 没有外观状态),由 [`super::form`] 退回演示行。
|
||||
///
|
||||
/// 各组自己的渲染器**定义在自己文件里**,签名是这一条:于是「这一组要读哪些状态」由它自己决定,
|
||||
/// 本文件不需要认识任何一组的字段(r1 复审 P1 要的正是这一点)。
|
||||
///
|
||||
/// `lang` 是 M179 加的:槽位此前拿不到语言,唯一挂了真控件的外观组只能写死英文;接进**唯一的那个
|
||||
/// 槽位**比再给 [`super::form`] 加一条按下标分发的 `else if` 更省行数(本文件贴着 400 行硬顶)。
|
||||
///
|
||||
/// `forced` 是 M285 加的(`pages::Model::forced`,即 `--force-state`):`None` = 按控件自己的观测态画
|
||||
/// (产品路径的常态),`Some(..)` 只服务取证。站点拿它去调 [`super::resolve_state`]——那是全仓唯一的
|
||||
/// resolve 点,别在这里或各组里另写一遍。
|
||||
pub(crate) type Body =
|
||||
for<'a> fn(&'a Ui, States<'a>, Lang, Option<Interaction>) -> Option<Element<'a, Message>>;
|
||||
|
||||
/// 一个设置分组:名字、图标、演示行、(可选)把这一组正文画成真控件的渲染器。
|
||||
///
|
||||
/// 除 `name`/`icon`/`rows`/`body` 外没有别的字段:**分组自己的状态归分组自己**(见 [`super::state`]),
|
||||
/// 这里只装「怎么画」。
|
||||
pub(crate) struct Group {
|
||||
/// 左列显示名(也是表单列的分组标题)。
|
||||
pub(super) name: &'static str,
|
||||
/// 左列图标。
|
||||
pub(super) icon: IconKind,
|
||||
/// 演示行(`body` 为 `None` 时画这些;harness 出图也靠它们)。
|
||||
pub(super) rows: &'static [(&'static str, &'static str)],
|
||||
/// 真控件渲染器;`None` = 这一组还没接真控件,画 `rows`。
|
||||
pub(super) body: Option<Body>,
|
||||
}
|
||||
|
||||
/// 分组注册表:**顺序 = 左列自上而下**,也是 `crate::pages::appearance::APPEARANCE_GROUP` 的下标来源。
|
||||
///
|
||||
/// 每一项都指向它自己文件里的 `GROUP`(M99 之前是四份字面量挤在本文件里)。
|
||||
pub(super) const GROUPS: [Group; 6] = [
|
||||
general::GROUP,
|
||||
appearance::GROUP,
|
||||
java::GROUP,
|
||||
download::GROUP,
|
||||
// M159:自更新只**追加**在末尾(插在中间会让 `APPEARANCE_GROUP` / `GENERAL_GROUP` 两个下标
|
||||
// 全部失效,`tests/mod.rs` 的「左列顺序」那条也会红)。
|
||||
update::GROUP,
|
||||
// M174:助手(AI)配置。同样只追加(下标 = `ai_about::AI_GROUP`),正文走按下标分发。
|
||||
ai_about::GROUP,
|
||||
];
|
||||
|
||||
/// 演示选中的分组(没有真实外观状态时——`pages_demo` harness——用它)。
|
||||
pub(super) const SELECTED: usize = 0;
|
||||
|
||||
/// General 分组在 [`GROUPS`] 里的下标(0)。它与 [`SELECTED`] 同值**是巧合,不是依赖**。
|
||||
///
|
||||
/// 为什么需要它:语言行要的是「当前档位 + 生效语言」([`super::Language`]),而 `Group.body` 只带
|
||||
/// [`States`] 与**生效语言**(`Lang`,M179)——档位 `LangChoice` 装不进去(`States` 也装不下:
|
||||
/// 加字段要动 `settings/state.rs` 与 4 处 struct literal,见本文件文档「仍然共享」第 4 条)。
|
||||
/// 所以这里做一次**按下标的分发**:代价是一行 `if`,换来 `general::GROUP.body` 仍是 `None`
|
||||
/// (`tests/mod.rs` 的「只有外观组带真控件 body」照样成立)。
|
||||
pub(super) const GENERAL_GROUP: usize = 0;
|
||||
|
||||
/// 左列分组的**显示名** key(下标与 [`GROUPS`] 一一对应,M106)。
|
||||
///
|
||||
/// 为什么不放进 [`Group`]:那会让另外三组的 `GROUP` 字面量(各自文件、各自填页线)编译不过——
|
||||
/// 与 [`GENERAL_GROUP`] 同一条理由。`Group.name` 因此退化为**id**(测试按它认组;显示走这张表)。
|
||||
pub(super) const GROUP_LABELS: [unlucky_i18n::Key; 6] = [
|
||||
unlucky_i18n::Key::SettingsGroupGeneral,
|
||||
unlucky_i18n::Key::SettingsGroupAppearance,
|
||||
unlucky_i18n::Key::SettingsGroupJava,
|
||||
unlucky_i18n::Key::SettingsGroupDownloads,
|
||||
// M159
|
||||
unlucky_i18n::Key::SettingsGroupUpdate,
|
||||
// M174
|
||||
unlucky_i18n::Key::SettingsGroupAssistant,
|
||||
];
|
||||
|
||||
/// 「更新」分组在 [`GROUPS`] 里的下标(真源在 `settings/update.rs`,这里只转发)。
|
||||
///
|
||||
/// 与 [`GENERAL_GROUP`] 同一种做法:这一组要画的是 **presenter 快照**(不在 [`States`] 里),
|
||||
/// 所以由 `view` 的 `update` 形参一路递到下标的 [`super::form`] 分发点,`GROUP.body` 保持 `None`。
|
||||
pub(super) const UPDATE_GROUP: usize = update::UPDATE_GROUP;
|
||||
|
||||
/// 「助手(AI)」分组在 [`GROUPS`] 里的下标(真源在 `settings/ai_about.rs`,这里只转发)。
|
||||
///
|
||||
/// 与 [`GENERAL_GROUP`] / [`UPDATE_GROUP`] 同一条做法:由 `view` 的 `language` 形参递到 [`super::form`]
|
||||
/// 按本下标分发,`GROUP.body` 保持 `None`(本组**尚未**迁到 [`Body`] 槽位:槽位现在带 `Lang` 了,
|
||||
/// 迁它要把 `ai::view::body` 的返回值改成 `Option<Element>`,是一次独立重构——M179 不做)。
|
||||
pub(super) const AI_GROUP: usize = ai_about::AI_GROUP;
|
||||
|
||||
/// 「Java」分组在 [`GROUPS`] 里的下标(真源在 `settings/java.rs`,这里只转发)。
|
||||
///
|
||||
/// M175 起这一组也按下标分发:它要 `Lang`(中英文案)并且要读后台扫描的快照
|
||||
/// (扫描线程不碰 `App`,结论由心跳灌进 `Groups.java`,页面只从 [`States`] 里取)。
|
||||
pub(super) const JAVA_GROUP: usize = java::JAVA_GROUP;
|
||||
@@ -161,6 +161,7 @@ fn appearance_texts(lang: Lang) -> Vec<(Key, &'static str)> {
|
||||
effective: lang,
|
||||
},
|
||||
&update,
|
||||
None,
|
||||
);
|
||||
|
||||
probe::take()
|
||||
|
||||
@@ -29,6 +29,7 @@ use unlucky_i18n::{Catalog, Key, Lang};
|
||||
use unlucky_presenter::dto::human_bytes;
|
||||
|
||||
use crate::app::components::buttons::{self, Button};
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::app::components::selection::{chip, Chip};
|
||||
|
||||
use crate::pages::frame::text_role_owned;
|
||||
@@ -38,7 +39,7 @@ use crate::pages::UpdateAction;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::frame::{card, divider};
|
||||
use super::Group;
|
||||
use super::{forced_button, Group};
|
||||
|
||||
/// 「更新」分组在 [`super::GROUPS`] 里的下标(**必须是最后一个**:加组只追加不重排)。
|
||||
pub(super) const UPDATE_GROUP: usize = 4;
|
||||
@@ -57,7 +58,12 @@ pub(super) const GROUP: Group = Group {
|
||||
};
|
||||
|
||||
/// 渲染整组正文。
|
||||
pub(super) fn body<'a>(ui: &'a Ui, update: &'a Update, lang: Lang) -> Element<'a, Message> {
|
||||
pub(super) fn body<'a>(
|
||||
ui: &'a Ui,
|
||||
update: &'a Update,
|
||||
lang: Lang,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let catalog = lang.catalog();
|
||||
let mut stacked = column![].spacing(0.0).width(Length::Fill);
|
||||
|
||||
@@ -73,9 +79,9 @@ pub(super) fn body<'a>(ui: &'a Ui, update: &'a Update, lang: Lang) -> Element<'a
|
||||
channel_label(catalog, &update.channel),
|
||||
));
|
||||
stacked = stacked.push(divider(ui));
|
||||
stacked = stacked.push(auto_check_row(ui, catalog, update));
|
||||
stacked = stacked.push(auto_check_row(ui, catalog, update, forced));
|
||||
stacked = stacked.push(divider(ui));
|
||||
stacked = stacked.push(status_block(ui, catalog, update));
|
||||
stacked = stacked.push(status_block(ui, catalog, update, forced));
|
||||
|
||||
card(ui, stacked)
|
||||
}
|
||||
@@ -121,12 +127,20 @@ fn info_row<'a>(ui: &'a Ui, label: &'a str, value: String) -> Element<'a, Messag
|
||||
/// **忙碌时「立即检查」置灰**(M161):此前它永远可点,而 presenter 的 `begin_check` 在
|
||||
/// inflight 时静默 `return None`——用户点了没反应,还以为启动器卡了。禁用态与状态行同源
|
||||
/// (同一个 [`Update::busy`]):按钮灰着的时候,下面那句状态行一定是「正在检查…」/「正在安装…」。
|
||||
fn auto_check_row<'a>(ui: &'a Ui, catalog: Catalog, update: &Update) -> Element<'a, Message> {
|
||||
fn auto_check_row<'a>(
|
||||
ui: &'a Ui,
|
||||
catalog: Catalog,
|
||||
update: &Update,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let check_now = catalog.text(Key::SettingsUpdateCheckNow);
|
||||
let check = if update.can_check() {
|
||||
buttons::text(
|
||||
ui,
|
||||
Button::new(check_now, Message::Update(UpdateAction::CheckNow)),
|
||||
forced_button(
|
||||
Button::new(check_now, Message::Update(UpdateAction::CheckNow)),
|
||||
forced,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
buttons::text(ui, Button::disabled(check_now))
|
||||
@@ -150,7 +164,12 @@ fn auto_check_row<'a>(ui: &'a Ui, catalog: Catalog, update: &Update) -> Element<
|
||||
}
|
||||
|
||||
/// 上次检查时间 + 结论 + (有候选时)下载按钮 + 说明摘要 + 一句校验提示。
|
||||
fn status_block<'a>(ui: &'a Ui, catalog: Catalog, update: &'a Update) -> Element<'a, Message> {
|
||||
fn status_block<'a>(
|
||||
ui: &'a Ui,
|
||||
catalog: Catalog,
|
||||
update: &'a Update,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let mut block = column![].spacing(ui.space(Space::X2)).width(Length::Fill);
|
||||
|
||||
let checked = if update.checked_at.trim().is_empty() {
|
||||
@@ -174,14 +193,17 @@ fn status_block<'a>(ui: &'a Ui, catalog: Catalog, update: &'a Update) -> Element
|
||||
});
|
||||
block = block.push(text_role_owned(ui, TypeRole::BodySmall, false, text, color));
|
||||
|
||||
if let Some((_version, _size, notes, forced)) = update.available() {
|
||||
if let Some((_version, _size, notes, forced_update)) = update.available() {
|
||||
// 有候选就**永远画出**这一颗按钮:忙碌时置灰(`can_apply` = false),而不是整颗消失。
|
||||
// 消失会让人以为「装到一半的功能没了」;灰着 + 状态行那句「正在安装…」才说得清。
|
||||
let apply_label = catalog.text(Key::SettingsUpdateApply);
|
||||
let apply = if update.can_apply() {
|
||||
buttons::filled(
|
||||
ui,
|
||||
Button::new(apply_label, Message::Update(UpdateAction::Apply)),
|
||||
forced_button(
|
||||
Button::new(apply_label, Message::Update(UpdateAction::Apply)),
|
||||
forced,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
buttons::filled(ui, Button::disabled(apply_label))
|
||||
@@ -189,7 +211,7 @@ fn status_block<'a>(ui: &'a Ui, catalog: Catalog, update: &'a Update) -> Element
|
||||
let mut actions = row![apply]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.align_y(Alignment::Center);
|
||||
if forced {
|
||||
if forced_update {
|
||||
actions = actions.push(text_role_owned(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
@@ -220,9 +242,12 @@ fn status_block<'a>(ui: &'a Ui, catalog: Catalog, update: &'a Update) -> Element
|
||||
));
|
||||
block = block.push(buttons::tonal(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::UpdateBannerRestartNow),
|
||||
Message::Update(UpdateAction::RestartNow),
|
||||
forced_button(
|
||||
Button::new(
|
||||
catalog.text(Key::UpdateBannerRestartNow),
|
||||
Message::Update(UpdateAction::RestartNow),
|
||||
),
|
||||
forced,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ const WIRED: [(&str, usize); 9] = [
|
||||
|
||||
/// 手写样式(不走组件 `.base(..)`)的三处:状态层直接叠在 `frame::BASE.role()` 上。
|
||||
const ROLE_SITES: [(&str, usize); 3] = [
|
||||
("settings/general.rs", 1),
|
||||
("settings/general/language.rs", 1),
|
||||
("settings/general/theme.rs", 1),
|
||||
("settings/logs_row.rs", 1),
|
||||
];
|
||||
@@ -88,8 +88,8 @@ fn every_card_internal_control_site_still_passes_a_base() {
|
||||
include_str!("../settings/java/download_view.rs"),
|
||||
),
|
||||
(
|
||||
"settings/general.rs",
|
||||
include_str!("../settings/general.rs"),
|
||||
"settings/general/language.rs",
|
||||
include_str!("../settings/general/language.rs"),
|
||||
),
|
||||
(
|
||||
"settings/general/theme.rs",
|
||||
|
||||
Reference in new issue
Block a user