feat(app-iced): --force-state 穿过设置页与外观区(M285 FORCEDWIRE)
pages::Model.forced 此前只到得了外壳 + 实例页(唯一消费者 pages/instances.rs:159), 设置页「外观」区的滑块/文本框/裸 iced::button 全都不消费它 ⇒ M284 那四处状态层 (语言档 / 主题档 / 日志 action_button / 外观 choice)只有单测证据,做不出像素判据。 改动: - settings/mod.rs: resolve_state(forced, status) 与 forced_button(button, forced) 成为全仓唯一两个「forced 怎么解释」的点;view → form → section_columns 一路 新增 forced 形参(默认 None ⇒ 今天的行为逐字节不变)。 - Group.body(registry.rs)加第 4 个形参 Option<Interaction>,六组统一签名。 - 四类裸 iced::button 控件把 forced 递进样式闭包并经 resolve_state 落地: 语言档 chip(general/language.rs)、主题档 chip(general/theme.rs)、 日志 action_button(logs_row.rs)、外观 choice(appearance/section/mod.rs, 含 accent/window/bg_image/colour_source 共 10 处调用)。 - components::buttons::Button 的三组(update / ai / java)走 forced_button 的 .state()。 - harness: request.rs 解析 --force-state=rest|hover|pressed(未知值报错,词与产品 cli.rs 同一套),slug 加 -hover/-pressed;app.rs 把它写进 model.forced, 截图通道默认 Rest 的纪律保持(显式值优先)。 - pages/mod.rs: 唯一调用点把 model.forced 传给 settings::view(397 → 399 行)。 单测:resolve_state 的 Some 覆盖 / None 退回;forced_button 的 Some 钉态 / None 原样。
This commit is contained in:
1 parent
767727efb7
commit
7b80b0e314
19 files changed
+397
-79
No files matched your search
@@ -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();
|
||||
|
||||
@@ -47,6 +47,7 @@ use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::catalog::Key;
|
||||
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::fonts::install::{CjkInstall, FetchClass};
|
||||
use crate::pages::{frame, Language, Message};
|
||||
use crate::theme::Ui;
|
||||
@@ -72,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::view(ui, language));
|
||||
stacked = stacked.push(language::view(ui, language, forced));
|
||||
// M168(D28 后半):中文字体的下载状态行。紧贴语言行——用户在这里手选中文,反馈就该在这行下面,
|
||||
// 而不是等他切到别的页面才发现「刚才那几秒在干嘛」。
|
||||
stacked = stacked.push(divider(ui));
|
||||
@@ -88,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 [
|
||||
|
||||
@@ -24,7 +24,11 @@ use crate::pages::{frame, Language, Message};
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// 语言行:标题 + 生效语言回显 + 三档互斥按钮 + 一句说明。
|
||||
pub(super) fn view<'a>(ui: &'a Ui, language: Language) -> Element<'a, Message> {
|
||||
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();
|
||||
|
||||
@@ -60,6 +64,7 @@ pub(super) fn view<'a>(ui: &'a Ui, language: Language) -> Element<'a, Message> {
|
||||
catalog.text(choice.label_key()),
|
||||
*choice == language.choice,
|
||||
*choice,
|
||||
forced,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
@@ -90,7 +95,13 @@ pub(super) fn view<'a>(ui: &'a Ui, language: Language) -> Element<'a, Message> {
|
||||
/// **几何(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> {
|
||||
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 {
|
||||
@@ -124,6 +135,7 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> El
|
||||
border_color,
|
||||
stroke,
|
||||
radius,
|
||||
forced,
|
||||
))
|
||||
.on_press(Message::Language(choice))
|
||||
.into()
|
||||
@@ -131,16 +143,20 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> El
|
||||
|
||||
/// [`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` 里,状态层叠卡底而不是画布)。
|
||||
fn chip_style<'a>(
|
||||
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 {
|
||||
@@ -148,7 +164,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(),
|
||||
)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -148,7 +148,7 @@ 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;
|
||||
@@ -156,6 +156,8 @@ 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};
|
||||
@@ -165,6 +167,36 @@ 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`(`Button::state(…)`,`Some` 覆盖、`None` 不动)相同;
|
||||
/// 差别只在 iced 的裸 `button` 没有 `.state()`:样式闭包的签名固定收 `button::Status`,所以覆盖只能
|
||||
/// 发生在闭包**里面**(换掉它拿到的那一态),而不是控件上。
|
||||
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`
|
||||
/// 在设置页的这一份)。
|
||||
///
|
||||
/// **这是第二份,不是抽象**(M285):实例页那条线的控件本身就是 `components::buttons::Button`
|
||||
///(有 `state(Interaction)`),而设置页的 `choice` / `chip` / `action_button` 是裸 `iced::button`——
|
||||
/// 覆盖只能落在样式闭包里(见 [`resolve_state`])。三次法则现在到 2;第三次真出现时落点是
|
||||
/// `components/interaction/`,不是这里。
|
||||
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 之间的**展示契约**(由快照派生),而档位是
|
||||
@@ -194,12 +226,17 @@ impl Default for Language {
|
||||
///
|
||||
/// `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
|
||||
@@ -212,10 +249,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)
|
||||
@@ -240,10 +277,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);
|
||||
}
|
||||
|
||||
@@ -258,6 +296,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();
|
||||
@@ -274,20 +313,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))
|
||||
};
|
||||
|
||||
@@ -313,3 +352,50 @@ fn section_columns<'a>(
|
||||
|
||||
columns
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod resolve_tests {
|
||||
use super::*;
|
||||
|
||||
/// `forced` 必须**覆盖**观测到的 status;不给 `forced` 时必须**退回**观测值(M285)。
|
||||
///
|
||||
/// 抓得住:把 `Some` 分支改成忽略 forced(直接 `Interaction::from_status(status)`)——第一条立刻红,
|
||||
/// 而那正好等于「`--force-state` 在四个控件上没接线」。
|
||||
/// 抓不住:forced 真的画进了 PNG(那是 `pages_demo --force-state=hover|pressed` 的像素判据)。
|
||||
/// `forced_button` 只做一件事:`Some` 钉态、`None` 原样(M285)。
|
||||
///
|
||||
/// `Button::state` 是私有字段、没有 getter,所以断 **derived Debug** 里的那一格。
|
||||
/// 抓得住:`Some` 分支被改成恒 `None`(第一条红);`None` 分支也顺手 `.state(..)`(第二条红)。
|
||||
/// 抓不住:按钮真画成那色(那是出图的事)。
|
||||
#[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}"
|
||||
);
|
||||
}
|
||||
|
||||
#[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 时必须退回观测值(既有行为不变)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,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::{ai_about, appearance, download, general, java, update, Message, States};
|
||||
@@ -36,7 +37,12 @@ use super::{ai_about, appearance, download, general, java, update, Message, Stat
|
||||
///
|
||||
/// `lang` 是 M179 加的:槽位此前拿不到语言,唯一挂了真控件的外观组只能写死英文;接进**唯一的那个
|
||||
/// 槽位**比再给 [`super::form`] 加一条按下标分发的 `else if` 更省行数(本文件贴着 400 行硬顶)。
|
||||
pub(crate) type Body = for<'a> fn(&'a Ui, States<'a>, Lang) -> Option<Element<'a, Message>>;
|
||||
///
|
||||
/// `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>>;
|
||||
|
||||
/// 一个设置分组:名字、图标、演示行、(可选)把这一组正文画成真控件的渲染器。
|
||||
///
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user