Merge branch 'feat/wire-disabled-control-base-into-product'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
dbc5274965
21 files changed
+579
-42
No files matched your search
@@ -39,7 +39,7 @@ mod tests;
|
|||||||
|
|
||||||
pub use pieces::{
|
pub use pieces::{
|
||||||
card, divider, empty_state, filled, gap, glyph, glyph_box, scroll_style, text_action,
|
card, divider, empty_state, filled, gap, glyph, glyph_box, scroll_style, text_action,
|
||||||
text_role, text_role_centered, text_role_owned, ICON_PX, ICON_SMALL_PX, SCROLLBAR_MARGIN,
|
text_role, text_role_centered, text_role_owned, BASE, ICON_PX, ICON_SMALL_PX, SCROLLBAR_MARGIN,
|
||||||
SCROLLBAR_WIDTH,
|
SCROLLBAR_WIDTH,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use unlucky_design::shape::ShapeScale;
|
|||||||
use unlucky_design::spacing::Space;
|
use unlucky_design::spacing::Space;
|
||||||
use unlucky_design::typography::{TypeRole, TypeStyle};
|
use unlucky_design::typography::{TypeRole, TypeStyle};
|
||||||
|
|
||||||
|
use crate::app::components::interaction::Base;
|
||||||
use crate::icons;
|
use crate::icons;
|
||||||
use crate::pages::{Action, Message};
|
use crate::pages::{Action, Message};
|
||||||
use crate::theme::Ui;
|
use crate::theme::Ui;
|
||||||
@@ -32,11 +33,29 @@ pub const SCROLLBAR_WIDTH: f32 = 6.0;
|
|||||||
/// 滑块离卡片内边的留白(2px,避免贴边)。
|
/// 滑块离卡片内边的留白(2px,避免贴边)。
|
||||||
pub const SCROLLBAR_MARGIN: f32 = 2.0;
|
pub const SCROLLBAR_MARGIN: f32 = 2.0;
|
||||||
|
|
||||||
|
/// 描边卡片**真正画出来的底色**([`card`] 的 `background` 就是它)。
|
||||||
|
///
|
||||||
|
/// 装配方式与 `components` 的容器同一条纪律(M234):容器自己画哪个角色,就导出哪个常量,
|
||||||
|
/// 里面放的控件用 `.base(BASE)` 把这条信息传下去——两边同源,改色不会漂一半。
|
||||||
|
///
|
||||||
|
/// 取值不是「反正是 surface」而是**画法本身**:`card()` 画的是 [`MaterialLayer::Card`]
|
||||||
|
/// (= `Role::SurfaceContainerLow`,乘面板 α),所以常量也从**同一个** `MaterialLayer` 取角色。
|
||||||
|
/// `frame/tests.rs` 有一条断言把「画出来的颜色」与「常量」逐值对上,另有源码结构断言盯着
|
||||||
|
/// `card()` 不许改画别的层。
|
||||||
|
///
|
||||||
|
/// ⚠️ 面板 α < 1(GPU 档的用户设置)时卡片画的是**半透明**合成色,而 [`Base`] 只装得下一个
|
||||||
|
/// `Role`:禁用态会按不透明的角色色合成,与卡片实色差一个 α(M234 的 `base.rs` 已登记这条局限)。
|
||||||
|
/// CPU 档(`--software-renderer` 验收图)α 恒为 1,两者逐值相同。
|
||||||
|
pub const BASE: Base = Base::of(MaterialLayer::Card.surface_role());
|
||||||
|
|
||||||
/// 描边卡片(长表单容器)。圆角取**规格卡**的 16(`Ui::card_shape`),不是组件表的 12。
|
/// 描边卡片(长表单容器)。圆角取**规格卡**的 16(`Ui::card_shape`),不是组件表的 12。
|
||||||
///
|
///
|
||||||
/// M229 换肤:规格卡 `CARD` 小节把卡片画成 `rx=16`,而 `component_map` 的 `OutlinedCard` 是
|
/// M229 换肤:规格卡 `CARD` 小节把卡片画成 `rx=16`,而 `component_map` 的 `OutlinedCard` 是
|
||||||
/// `Medium(12)`(官方值)。M228 把这条差异登记为「换肤时取 `inkcat::metrics::CARD`」,这里就是那次取值;
|
/// `Medium(12)`(官方值)。M228 把这条差异登记为「换肤时取 `inkcat::metrics::CARD`」,这里就是那次取值;
|
||||||
/// 组件表的值不动(它还服务别的面)。描边仍走 `OutlineVariant` 1px。
|
/// 组件表的值不动(它还服务别的面)。描边仍走 `OutlineVariant` 1px。
|
||||||
|
///
|
||||||
|
/// M237:卡片里放的禁用 / 可交互控件要接 [`BASE`](`.base(frame::BASE)`),否则它们会把
|
||||||
|
/// 状态层与禁用色叠在画布色上——卡片内的禁用按钮因此看起来「形丢了」(M223 的视觉缺陷)。
|
||||||
pub fn card<'a>(ui: &'a Ui, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
pub fn card<'a>(ui: &'a Ui, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||||
let shape = ui.card_shape();
|
let shape = ui.card_shape();
|
||||||
container(content)
|
container(content)
|
||||||
@@ -124,6 +143,10 @@ pub fn empty_state<'a>(
|
|||||||
///
|
///
|
||||||
/// 只画静息、hover、pressed 三态(focus/disabled 归 M75 的 `interaction.rs`);
|
/// 只画静息、hover、pressed 三态(focus/disabled 归 M75 的 `interaction.rs`);
|
||||||
/// 消息是页面自己的 [`Message::Action`],与 app 的 `Message` 完全解耦。
|
/// 消息是页面自己的 [`Message::Action`],与 app 的 `Message` 完全解耦。
|
||||||
|
///
|
||||||
|
/// ⚠️ 状态层按**画布底**合成(`Role::Surface`):它只服务页头带那一排动作(页头带就是 Canvas 层)。
|
||||||
|
/// 放进卡片/面板里必须换成那个容器的 `BASE`——这里没有 `base` 形参,是因为今天零个卡内调用点;
|
||||||
|
/// 真要在卡里用它,加一个 `base` 参数而不是在这里猜(M237 登记的接线口径)。
|
||||||
pub fn text_action<'a>(ui: &'a Ui, label: &'a str, action: Action) -> Element<'a, Message> {
|
pub fn text_action<'a>(ui: &'a Ui, label: &'a str, action: Action) -> Element<'a, Message> {
|
||||||
let spec = Component::TextButton.spec();
|
let spec = Component::TextButton.spec();
|
||||||
let height = ui.metrics().control_height;
|
let height = ui.metrics().control_height;
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
use super::pieces::{EMPTY_ICON_BLOCK, EMPTY_ICON_GLYPH};
|
use super::pieces::{EMPTY_ICON_BLOCK, EMPTY_ICON_GLYPH};
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use unlucky_design::color::SchemeFlavor;
|
use unlucky_design::color::{Role, SchemeFlavor};
|
||||||
|
use unlucky_design::elevation::MaterialLayer;
|
||||||
use unlucky_design::shape::ShapeScale;
|
use unlucky_design::shape::ShapeScale;
|
||||||
use unlucky_design::typography::Platform;
|
use unlucky_design::typography::Platform;
|
||||||
|
|
||||||
use crate::pages::layout::{frame_metrics, PaneKit};
|
use crate::app::components::interaction::Base;
|
||||||
|
use crate::pages::layout::{self, frame_metrics, PaneKit};
|
||||||
use crate::theme::Families;
|
use crate::theme::Families;
|
||||||
|
|
||||||
fn ui() -> Ui {
|
fn ui() -> Ui {
|
||||||
@@ -147,3 +149,69 @@ fn body_height_subtracts_the_real_shell_chrome() {
|
|||||||
// 窗口比外壳还矮时不返回负数(不许把版面算成 NaN/负值)。
|
// 窗口比外壳还矮时不返回负数(不许把版面算成 NaN/负值)。
|
||||||
assert_eq!(body_height(&ui, metrics, 100.0, true), 0.0);
|
assert_eq!(body_height(&ui, metrics, 100.0, true), 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **卡片导出的底 = 卡片真正画出来的底色**(M237)。
|
||||||
|
///
|
||||||
|
/// 抓得住:①`BASE` 被换成「看着像」的角色(`Surface` / `SurfaceContainer` /
|
||||||
|
/// `SurfaceContainerHighest` 都能通过「是个容器色」这种含糊的判据);②`card()` 改画别的材质层
|
||||||
|
/// 而常量没跟上(两边漂一半,卡片里的禁用控件又会与卡底脱节)。
|
||||||
|
///
|
||||||
|
/// 抓不住:面板 α < 1 时的**半透明**合成——`Ui::layer` 会把角色乘 α,而 [`Base`] 只装得下
|
||||||
|
/// 一个 `Role`。CPU 验收档 α 恒 1,两者逐值相同;GPU 档的这条差已登记在 `pieces::BASE` 上。
|
||||||
|
///
|
||||||
|
/// 判据是**页面级像素**(截图量出来的那份,`pages/settings/tests/java_shot.rs`)的前置条件:
|
||||||
|
/// 这一条红了,那边的「叠卡片底」就是拿一个错的值在比。
|
||||||
|
#[test]
|
||||||
|
fn the_card_base_is_the_colour_the_card_actually_paints() {
|
||||||
|
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||||
|
let ui = Ui::with_platform(
|
||||||
|
flavor,
|
||||||
|
Platform::Windows,
|
||||||
|
Families {
|
||||||
|
text: "Segoe UI",
|
||||||
|
display: "Segoe UI",
|
||||||
|
mono: "Consolas",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
BASE.role(),
|
||||||
|
MaterialLayer::Card.surface_role(),
|
||||||
|
"{flavor:?}: 卡片底必须是 `card()` 画的那一层的角色"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ui.layer(MaterialLayer::Card),
|
||||||
|
ui.color(BASE.role()),
|
||||||
|
"{flavor:?}: `card()` 画的颜色与 `BASE` 求出来的不是同一个值"
|
||||||
|
);
|
||||||
|
// 卡片底 ≠ 画布底:相等的话「卡片里的禁用控件叠哪一层」永远看不出区别(判据恒绿)
|
||||||
|
assert_ne!(BASE, Base::CANVAS, "{flavor:?}: 卡片底不能是画布底");
|
||||||
|
assert_ne!(
|
||||||
|
ui.color(BASE.role()),
|
||||||
|
ui.color(Role::Surface),
|
||||||
|
"{flavor:?}: 卡片色与画布色必须可分"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **源码结构**:`card()` 用 [`MaterialLayer::Card`] 画底,`BASE` 从**同一个**层取角色。
|
||||||
|
///
|
||||||
|
/// 为什么还要一条扫源码的:iced 的 `Element` 没有内省接口(容器样式是闭包),「画的是哪一层」
|
||||||
|
/// 在运行期拿不到;而这件事本身就是契约——容器画什么,就导出什么。换个写法(先存进局部变量)
|
||||||
|
/// 会绕过它,那属于重构后要一起改判据的正常代价(与 `pages/tests.rs` 里 `frame::body` 那两条同源)。
|
||||||
|
#[test]
|
||||||
|
fn the_card_base_and_the_card_paint_share_one_layer() {
|
||||||
|
const PIECES: &str = include_str!("pieces.rs");
|
||||||
|
|
||||||
|
let body = layout::source::function_body(PIECES, "pub fn card<'a>(");
|
||||||
|
assert!(
|
||||||
|
body.contains("ui.layer(MaterialLayer::Card)"),
|
||||||
|
"`frame::card` 的底色必须取 `MaterialLayer::Card`(`BASE` 就是从它推出来的):\n{body}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let base = layout::source::statement(PIECES, "pub const BASE");
|
||||||
|
assert!(
|
||||||
|
base.contains("Base::of(") && base.contains("MaterialLayer::Card"),
|
||||||
|
"`frame::BASE` 必须从 `card()` 画的那一层取角色:{base}"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -252,6 +252,9 @@ fn list_column<'a>(
|
|||||||
.style(frame::scroll_style(ui));
|
.style(frame::scroll_style(ui));
|
||||||
|
|
||||||
// 底部内缩:裁切线落在卡片的内边距带里,而不是压在圆角描边上(M102 / 视觉复审 ②)。
|
// 底部内缩:裁切线落在卡片的内边距带里,而不是压在圆角描边上(M102 / 视觉复审 ②)。
|
||||||
|
//
|
||||||
|
// M237:这一层画的是 `MaterialLayer::Card`(= `frame::BASE` 的同一个层),所以卡里的
|
||||||
|
// 列表行把底传 `frame::BASE`(见 `list_row`)——不另立常量,避免同一层出现两个「真源」。
|
||||||
container(column![head, scroller].height(Length::Fill))
|
container(column![head, scroller].height(Length::Fill))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
@@ -312,6 +315,10 @@ fn list_row<'a>(
|
|||||||
if let Some(state) = forced {
|
if let Some(state) = forced {
|
||||||
item = item.state(state);
|
item = item.state(state);
|
||||||
}
|
}
|
||||||
|
// M237 复审 P2-1:列表卡画的是 `MaterialLayer::Card`(本文件 `list_column` 的 style),
|
||||||
|
// 行的 hover / 按下状态层按 `base.role()` 合成(`components/navigation/list_item.rs::palette`),
|
||||||
|
// 不给底就落回 `Base::CANVAS` —— 卡片里的行抬起时会叠在画布色上,与本轮修的禁用态同一类错。
|
||||||
|
item = item.base(frame::BASE);
|
||||||
|
|
||||||
list_item(ui, item)
|
list_item(ui, item)
|
||||||
}
|
}
|
||||||
@@ -195,6 +195,10 @@ fn detail_head<'a>(
|
|||||||
.width(Length::Fill);
|
.width(Length::Fill);
|
||||||
|
|
||||||
// 主操作:**全屏唯一**实心 accent(rubric K2)。不可用时退化成禁用态(只清消息,不改身份)。
|
// 主操作:**全屏唯一**实心 accent(rubric K2)。不可用时退化成禁用态(只清消息,不改身份)。
|
||||||
|
//
|
||||||
|
// M237:这一行**不在卡片里**(详情列直接铺在画布上),所以这两个按钮的底保持
|
||||||
|
// `Base::CANVAS`——把卡底传进来反而会让禁用色叠错层。卡片内的控件(MODS / SAVES /
|
||||||
|
// 启动参数区)各自传 `frame::BASE`,见那几个模块。
|
||||||
let launch = if detail.can_launch {
|
let launch = if detail.can_launch {
|
||||||
buttons::filled(
|
buttons::filled(
|
||||||
ui,
|
ui,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ use crate::app::components::selection::{chip, Chip};
|
|||||||
|
|
||||||
use crate::theme::Ui;
|
use crate::theme::Ui;
|
||||||
|
|
||||||
|
use super::super::frame::BASE;
|
||||||
use super::super::model::LaunchFacts;
|
use super::super::model::LaunchFacts;
|
||||||
use super::super::Message as PageMessage;
|
use super::super::Message as PageMessage;
|
||||||
use super::state::{LaunchForm, Message};
|
use super::state::{LaunchForm, Message};
|
||||||
@@ -120,7 +121,7 @@ fn args_field<'a>(
|
|||||||
on_input: impl Fn(String) -> PageMessage + 'a,
|
on_input: impl Fn(String) -> PageMessage + 'a,
|
||||||
) -> Element<'a, PageMessage> {
|
) -> Element<'a, PageMessage> {
|
||||||
column![
|
column![
|
||||||
text_field(ui, Field::new(value).label(label), on_input),
|
text_field(ui, Field::new(value).label(label).base(BASE), on_input),
|
||||||
super::super::frame::text_role(
|
super::super::frame::text_role(
|
||||||
ui,
|
ui,
|
||||||
TypeRole::BodySmall,
|
TypeRole::BodySmall,
|
||||||
@@ -143,7 +144,7 @@ fn text_field_with_hint<'a>(
|
|||||||
on_input: impl Fn(String) -> PageMessage + 'a,
|
on_input: impl Fn(String) -> PageMessage + 'a,
|
||||||
) -> Element<'a, PageMessage> {
|
) -> Element<'a, PageMessage> {
|
||||||
column![
|
column![
|
||||||
text_field(ui, Field::new(value).label(label), on_input),
|
text_field(ui, Field::new(value).label(label).base(BASE), on_input),
|
||||||
hint,
|
hint,
|
||||||
]
|
]
|
||||||
.spacing(ui.space(Space::X1))
|
.spacing(ui.space(Space::X1))
|
||||||
@@ -188,7 +189,8 @@ fn backend_row<'a>(
|
|||||||
} else {
|
} else {
|
||||||
spec.disabled()
|
spec.disabled()
|
||||||
};
|
};
|
||||||
chips = chips.push(chip(ui, spec));
|
// M237:启动参数区是一张 `frame::card`,chip 的禁用配色(12% 容器 / 38% 内容)叠卡底。
|
||||||
|
chips = chips.push(chip(ui, spec.base(BASE)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut meta = column![
|
let mut meta = column![
|
||||||
@@ -226,8 +228,14 @@ fn backend_row<'a>(
|
|||||||
|
|
||||||
fn actions_row<'a>(ui: &'a Ui, texts: Texts) -> Element<'a, PageMessage> {
|
fn actions_row<'a>(ui: &'a Ui, texts: Texts) -> Element<'a, PageMessage> {
|
||||||
row![
|
row![
|
||||||
buttons::filled(ui, Button::new(texts.launch_save(), page(Message::Save))),
|
buttons::filled(
|
||||||
buttons::text(ui, Button::new(texts.launch_reset(), page(Message::Reset))),
|
ui,
|
||||||
|
Button::new(texts.launch_save(), page(Message::Save)).base(BASE)
|
||||||
|
),
|
||||||
|
buttons::text(
|
||||||
|
ui,
|
||||||
|
Button::new(texts.launch_reset(), page(Message::Reset)).base(BASE)
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.spacing(ui.space(Space::X2))
|
.spacing(ui.space(Space::X2))
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
@@ -290,6 +290,9 @@ fn entry_row<'a>(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
// M237:开关住在 `frame::card` 里,禁用态的 12% 轨道 / 38% 滑块要叠**卡底**(`frame::BASE`),
|
||||||
|
// 不是画布底——否则「游戏在跑 / 请求在飞」那两档的开关会与卡片底色脱节。
|
||||||
|
let toggle = toggle.base(frame::BASE);
|
||||||
// 出图取证(`--force-state`)把开关也钉到同一档:否则截图里只有按钮是 hover、
|
// 出图取证(`--force-state`)把开关也钉到同一档:否则截图里只有按钮是 hover、
|
||||||
// 开关还是静息态,视觉复审没法判"悬停时的开关长什么样"。
|
// 开关还是静息态,视觉复审没法判"悬停时的开关长什么样"。
|
||||||
let toggle = match forced {
|
let toggle = match forced {
|
||||||
|
|||||||
@@ -236,7 +236,9 @@ pub(super) fn section<'a>(
|
|||||||
Message::Instances(PageMessage::ReloadSaves),
|
Message::Instances(PageMessage::ReloadSaves),
|
||||||
),
|
),
|
||||||
forced,
|
forced,
|
||||||
),
|
)
|
||||||
|
// M237:卡片里的控件把底传成卡底(hover / 禁用状态层叠卡实色)。
|
||||||
|
.base(frame::BASE),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// 旧列表还在时照旧画出来(失败不清列表),用户能看见上一次读到的东西;
|
// 旧列表还在时照旧画出来(失败不清列表),用户能看见上一次读到的东西;
|
||||||
@@ -322,7 +324,9 @@ fn head<'a>(
|
|||||||
Message::Instances(PageMessage::OpenSavesFolder),
|
Message::Instances(PageMessage::OpenSavesFolder),
|
||||||
),
|
),
|
||||||
forced,
|
forced,
|
||||||
),
|
)
|
||||||
|
// M237:这枚按钮在 SAVES 卡里 ⇒ 状态层叠卡底(`frame::BASE`),不是画布底。
|
||||||
|
.base(frame::BASE),
|
||||||
);
|
);
|
||||||
row![
|
row![
|
||||||
container(left)
|
container(left)
|
||||||
|
|||||||
@@ -169,10 +169,12 @@ fn summary_block<'a>(ui: &'a Ui, catalog: &Catalog, logs: &Logs) -> Element<'a,
|
|||||||
iced::widget::Space::new().width(Length::Fill),
|
iced::widget::Space::new().width(Length::Fill),
|
||||||
buttons::text(
|
buttons::text(
|
||||||
ui,
|
ui,
|
||||||
|
// M237:页脚按钮在诊断卡(`panel::card`)里 ⇒ 状态层叠卡底,不是画布底。
|
||||||
Button::new(
|
Button::new(
|
||||||
catalog.text(Key::LogsCopySummary),
|
catalog.text(Key::LogsCopySummary),
|
||||||
Message::CopySummary(logs.summary()),
|
Message::CopySummary(logs.summary()),
|
||||||
),
|
)
|
||||||
|
.base(panel::BASE),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|||||||
@@ -20,11 +20,19 @@ use iced::{Background, Element, Length};
|
|||||||
|
|
||||||
use unlucky_design::elevation::MaterialLayer;
|
use unlucky_design::elevation::MaterialLayer;
|
||||||
|
|
||||||
|
use crate::app::components::interaction::Base;
|
||||||
use crate::theme::Ui;
|
use crate::theme::Ui;
|
||||||
|
|
||||||
use super::super::frame;
|
use super::super::frame;
|
||||||
use super::super::Message;
|
use super::super::Message;
|
||||||
|
|
||||||
|
/// 本卡的底:**与 [`frame::BASE`] 是同一个常量**(两张卡画的是同一个 `Card` 材质层,只是
|
||||||
|
/// 一张按内容撑高、一张定高)。
|
||||||
|
///
|
||||||
|
/// 卡片里的控件(`Copy summary` 这类 Text 按钮)要用 `.base(BASE)`:它们的 hover / 禁用状态层
|
||||||
|
/// 都得叠在卡片实色上,不是画布上。
|
||||||
|
pub const BASE: Base = frame::BASE;
|
||||||
|
|
||||||
/// 定高卡片(`Card` 材质层 + `OutlinedCard` 形状与 **1px 描边**,与 `frame::card` 同一套令牌:
|
/// 定高卡片(`Card` 材质层 + `OutlinedCard` 形状与 **1px 描边**,与 `frame::card` 同一套令牌:
|
||||||
/// 实例页 / 下载页的卡都带这条描边,日志页的两张主卡不能例外)。
|
/// 实例页 / 下载页的卡都带这条描边,日志页的两张主卡不能例外)。
|
||||||
pub fn card<'a>(
|
pub fn card<'a>(
|
||||||
|
|||||||
@@ -335,10 +335,11 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> El
|
|||||||
let background = if selected {
|
let background = if selected {
|
||||||
Some(Background::Color(ui.color(Role::SecondaryContainer)))
|
Some(Background::Color(ui.color(Role::SecondaryContainer)))
|
||||||
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
|
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
|
||||||
|
// M237:这一组整个住在 `frame::card` 里,状态层叠**卡底**(不是画布)。
|
||||||
Some(Background::Color(ui.state_over(
|
Some(Background::Color(ui.state_over(
|
||||||
StateLayer::Hover,
|
StateLayer::Hover,
|
||||||
Role::OnSurface,
|
Role::OnSurface,
|
||||||
Role::Surface,
|
frame::BASE.role(),
|
||||||
)))
|
)))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -120,10 +120,11 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, mode: ThemeMode) -> Eleme
|
|||||||
let background = if selected {
|
let background = if selected {
|
||||||
Some(Background::Color(ui.color(Role::SecondaryContainer)))
|
Some(Background::Color(ui.color(Role::SecondaryContainer)))
|
||||||
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
|
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
|
||||||
|
// M237:主题三档同样住在设置卡里,状态层叠卡底(`frame::BASE`)。
|
||||||
Some(Background::Color(ui.state_over(
|
Some(Background::Color(ui.state_over(
|
||||||
StateLayer::Hover,
|
StateLayer::Hover,
|
||||||
Role::OnSurface,
|
Role::OnSurface,
|
||||||
Role::Surface,
|
frame::BASE.role(),
|
||||||
)))
|
)))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use unlucky_design::typography::TypeRole;
|
|||||||
use unlucky_i18n::{Catalog, Key};
|
use unlucky_i18n::{Catalog, Key};
|
||||||
|
|
||||||
use crate::app::components::selection::{chip, Chip};
|
use crate::app::components::selection::{chip, Chip};
|
||||||
use crate::pages::frame::{divider, text_role_owned};
|
use crate::pages::frame::{divider, text_role_owned, BASE};
|
||||||
use crate::pages::Message;
|
use crate::pages::Message;
|
||||||
use crate::theme::Ui;
|
use crate::theme::Ui;
|
||||||
|
|
||||||
@@ -122,22 +122,24 @@ fn choice_row<'a>(
|
|||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let title = format!("Java {}", choice.major);
|
let title = format!("Java {}", choice.major);
|
||||||
let mut badges = row![].spacing(ui.space(Space::X1));
|
let mut badges = row![].spacing(ui.space(Space::X1));
|
||||||
|
// M237:徽标是**禁用配色**(`Chip::badge` = 12% 容器 / 38% 内容),而这个板块整个住在
|
||||||
|
// `frame::card` 里 ⇒ 底必须是卡底,否则徽标会把禁用色叠在画布色上。
|
||||||
if choice.lts {
|
if choice.lts {
|
||||||
badges = badges.push(chip(
|
badges = badges.push(chip(
|
||||||
ui,
|
ui,
|
||||||
Chip::badge(catalog.text(Key::SettingsJavaDownloadLts)),
|
Chip::badge(catalog.text(Key::SettingsJavaDownloadLts)).base(BASE),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if choice.baseline {
|
if choice.baseline {
|
||||||
badges = badges.push(chip(
|
badges = badges.push(chip(
|
||||||
ui,
|
ui,
|
||||||
Chip::badge(catalog.text(Key::SettingsJavaDownloadBaseline)),
|
Chip::badge(catalog.text(Key::SettingsJavaDownloadBaseline)).base(BASE),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if recommended {
|
if recommended {
|
||||||
badges = badges.push(chip(
|
badges = badges.push(chip(
|
||||||
ui,
|
ui,
|
||||||
Chip::badge(catalog.text(Key::SettingsJavaDownloadRecommended)),
|
Chip::badge(catalog.text(Key::SettingsJavaDownloadRecommended)).base(BASE),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,11 +258,12 @@ fn install_button<'a>(
|
|||||||
Key::SettingsJavaDownloadAction
|
Key::SettingsJavaDownloadAction
|
||||||
});
|
});
|
||||||
if busy {
|
if busy {
|
||||||
return chip(ui, Chip::badge(label));
|
// 忙时那枚「下载」是**禁用配色**的徽标(不是可点 chip),同样叠在卡底上(M237)。
|
||||||
|
return chip(ui, Chip::badge(label).base(BASE));
|
||||||
}
|
}
|
||||||
chip(
|
chip(
|
||||||
ui,
|
ui,
|
||||||
Chip::filter(label, false, msg(JavaMessage::Download(major))),
|
Chip::filter(label, false, msg(JavaMessage::Download(major))).base(BASE),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use unlucky_i18n::{Catalog, Key};
|
|||||||
|
|
||||||
use crate::app::components::buttons::{self, Button};
|
use crate::app::components::buttons::{self, Button};
|
||||||
use crate::app::components::inputs::{text_field, Field, FieldVariant};
|
use crate::app::components::inputs::{text_field, Field, FieldVariant};
|
||||||
use crate::pages::frame::text_role_owned;
|
use crate::pages::frame::{text_role_owned, BASE};
|
||||||
use crate::pages::Message;
|
use crate::pages::Message;
|
||||||
use crate::theme::Ui;
|
use crate::theme::Ui;
|
||||||
|
|
||||||
@@ -23,16 +23,19 @@ use super::view::msg;
|
|||||||
use super::Message as JavaMessage;
|
use super::Message as JavaMessage;
|
||||||
|
|
||||||
/// 手动添加一行:输入框 + 「添加」按钮 + 原因。
|
/// 手动添加一行:输入框 + 「添加」按钮 + 原因。
|
||||||
|
///
|
||||||
|
/// M237:这一行住在 `frame::card` 里,输入框与按钮都把底传成 `frame::BASE`(禁用/状态层叠卡底)。
|
||||||
pub(super) fn add_row<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'a, Message> {
|
pub(super) fn add_row<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'a, Message> {
|
||||||
let field = Field::new(&state.add_draft)
|
let field = Field::new(&state.add_draft)
|
||||||
.variant(FieldVariant::Outlined)
|
.variant(FieldVariant::Outlined)
|
||||||
.label(catalog.text(Key::SettingsJavaAdd))
|
.label(catalog.text(Key::SettingsJavaAdd))
|
||||||
.placeholder(catalog.text(Key::SettingsJavaAddPlaceholder))
|
.placeholder(catalog.text(Key::SettingsJavaAddPlaceholder))
|
||||||
.supporting(catalog.text(Key::SettingsJavaAddHint));
|
.supporting(catalog.text(Key::SettingsJavaAddHint))
|
||||||
|
.base(BASE);
|
||||||
let add = if state.add_draft.trim().is_empty() || !state.storage {
|
let add = if state.add_draft.trim().is_empty() || !state.storage {
|
||||||
buttons::outlined(
|
buttons::outlined(
|
||||||
ui,
|
ui,
|
||||||
Button::disabled(catalog.text(Key::SettingsJavaAddAction)),
|
Button::disabled(catalog.text(Key::SettingsJavaAddAction)).base(BASE),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
buttons::outlined(
|
buttons::outlined(
|
||||||
@@ -40,7 +43,8 @@ pub(super) fn add_row<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Ele
|
|||||||
Button::new(
|
Button::new(
|
||||||
catalog.text(Key::SettingsJavaAddAction),
|
catalog.text(Key::SettingsJavaAddAction),
|
||||||
msg(JavaMessage::Add),
|
msg(JavaMessage::Add),
|
||||||
),
|
)
|
||||||
|
.base(BASE),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
row![
|
row![
|
||||||
@@ -71,7 +75,8 @@ pub(super) fn memory_field<'a>(
|
|||||||
);
|
);
|
||||||
let field = Field::new(&state.memory)
|
let field = Field::new(&state.memory)
|
||||||
.variant(FieldVariant::Outlined)
|
.variant(FieldVariant::Outlined)
|
||||||
.label(catalog.text(Key::SettingsJavaMemory));
|
.label(catalog.text(Key::SettingsJavaMemory))
|
||||||
|
.base(BASE);
|
||||||
// 校验不过就把输入框本身标红(不只是下面那行字):用户一眼看到「是这个框有问题」。
|
// 校验不过就把输入框本身标红(不只是下面那行字):用户一眼看到「是这个框有问题」。
|
||||||
let field = match state.validate() {
|
let field = match state.validate() {
|
||||||
Err(Refusal::MemoryEmpty | Refusal::MemoryNotNumber | Refusal::MemoryRange) => {
|
Err(Refusal::MemoryEmpty | Refusal::MemoryNotNumber | Refusal::MemoryRange) => {
|
||||||
@@ -124,7 +129,8 @@ pub(super) fn jvm_field<'a>(
|
|||||||
.variant(FieldVariant::Outlined)
|
.variant(FieldVariant::Outlined)
|
||||||
.label(catalog.text(Key::SettingsJavaJvm))
|
.label(catalog.text(Key::SettingsJavaJvm))
|
||||||
.placeholder(catalog.text(Key::SettingsJavaJvmPlaceholder))
|
.placeholder(catalog.text(Key::SettingsJavaJvmPlaceholder))
|
||||||
.supporting(catalog.text(Key::SettingsJavaJvmHint));
|
.supporting(catalog.text(Key::SettingsJavaJvmHint))
|
||||||
|
.base(BASE);
|
||||||
// 两种拒绝都长在这个框上:引号没配对、或者粘了超过 1024 字符的一整段(M199)。
|
// 两种拒绝都长在这个框上:引号没配对、或者粘了超过 1024 字符的一整段(M199)。
|
||||||
let field = if matches!(
|
let field = if matches!(
|
||||||
state.validate(),
|
state.validate(),
|
||||||
@@ -165,9 +171,9 @@ pub(super) fn actions_row<'a>(
|
|||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let label = catalog.text(Key::SettingsJavaSave);
|
let label = catalog.text(Key::SettingsJavaSave);
|
||||||
if state.can_save() {
|
if state.can_save() {
|
||||||
buttons::filled(ui, Button::new(label, msg(JavaMessage::Save)))
|
buttons::filled(ui, Button::new(label, msg(JavaMessage::Save)).base(BASE))
|
||||||
} else {
|
} else {
|
||||||
buttons::filled(ui, Button::disabled(label))
|
buttons::filled(ui, Button::disabled(label).base(BASE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use unlucky_i18n::{Catalog, Key};
|
|||||||
use crate::app::components::buttons::{self, Button};
|
use crate::app::components::buttons::{self, Button};
|
||||||
use crate::app::components::inputs::{text_field, Field, FieldVariant};
|
use crate::app::components::inputs::{text_field, Field, FieldVariant};
|
||||||
use crate::app::components::selection::{chip, Chip};
|
use crate::app::components::selection::{chip, Chip};
|
||||||
use crate::pages::frame::{divider, text_role_owned};
|
use crate::pages::frame::{divider, text_role_owned, BASE};
|
||||||
use crate::pages::Message;
|
use crate::pages::Message;
|
||||||
use crate::theme::Ui;
|
use crate::theme::Ui;
|
||||||
|
|
||||||
@@ -52,7 +52,8 @@ fn install_root<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'
|
|||||||
.variant(FieldVariant::Outlined)
|
.variant(FieldVariant::Outlined)
|
||||||
.label(catalog.text(Key::SettingsJavaInstallRoot))
|
.label(catalog.text(Key::SettingsJavaInstallRoot))
|
||||||
.placeholder(catalog.text(Key::SettingsJavaInstallRootPlaceholder))
|
.placeholder(catalog.text(Key::SettingsJavaInstallRootPlaceholder))
|
||||||
.supporting(catalog.text(Key::SettingsJavaInstallRootHint));
|
.supporting(catalog.text(Key::SettingsJavaInstallRootHint))
|
||||||
|
.base(BASE);
|
||||||
let field = if prefs.validate().is_err() {
|
let field = if prefs.validate().is_err() {
|
||||||
field.error()
|
field.error()
|
||||||
} else {
|
} else {
|
||||||
@@ -184,7 +185,8 @@ fn vendor_order<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'
|
|||||||
Button::new(
|
Button::new(
|
||||||
catalog.text(Key::SettingsJavaVendorReset),
|
catalog.text(Key::SettingsJavaVendorReset),
|
||||||
msg(JavaMessage::ResetPriority),
|
msg(JavaMessage::ResetPriority),
|
||||||
),
|
)
|
||||||
|
.base(BASE),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
list.into()
|
list.into()
|
||||||
@@ -215,7 +217,8 @@ fn vendor_row<'a>(
|
|||||||
catalog.text(Key::SettingsJavaVendorUp),
|
catalog.text(Key::SettingsJavaVendorUp),
|
||||||
false,
|
false,
|
||||||
msg(JavaMessage::MoveVendor { index, up: true }),
|
msg(JavaMessage::MoveVendor { index, up: true }),
|
||||||
),
|
)
|
||||||
|
.base(BASE),
|
||||||
);
|
);
|
||||||
let down = chip(
|
let down = chip(
|
||||||
ui,
|
ui,
|
||||||
@@ -223,7 +226,8 @@ fn vendor_row<'a>(
|
|||||||
catalog.text(Key::SettingsJavaVendorDown),
|
catalog.text(Key::SettingsJavaVendorDown),
|
||||||
false,
|
false,
|
||||||
msg(JavaMessage::MoveVendor { index, up: false }),
|
msg(JavaMessage::MoveVendor { index, up: false }),
|
||||||
),
|
)
|
||||||
|
.base(BASE),
|
||||||
);
|
);
|
||||||
row![
|
row![
|
||||||
container(
|
container(
|
||||||
@@ -266,7 +270,8 @@ fn auto_install<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'
|
|||||||
catalog.text(Key::SettingsJavaAutoInstallOn),
|
catalog.text(Key::SettingsJavaAutoInstallOn),
|
||||||
on,
|
on,
|
||||||
msg(JavaMessage::ToggleAutoInstall(true)),
|
msg(JavaMessage::ToggleAutoInstall(true)),
|
||||||
),
|
)
|
||||||
|
.base(BASE),
|
||||||
),
|
),
|
||||||
chip(
|
chip(
|
||||||
ui,
|
ui,
|
||||||
@@ -274,7 +279,8 @@ fn auto_install<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'
|
|||||||
catalog.text(Key::SettingsJavaAutoInstallOff),
|
catalog.text(Key::SettingsJavaAutoInstallOff),
|
||||||
!on,
|
!on,
|
||||||
msg(JavaMessage::ToggleAutoInstall(false)),
|
msg(JavaMessage::ToggleAutoInstall(false)),
|
||||||
),
|
)
|
||||||
|
.base(BASE),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.spacing(ui.space(Space::X1));
|
.spacing(ui.space(Space::X1));
|
||||||
@@ -304,9 +310,12 @@ fn auto_install<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'
|
|||||||
fn actions<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'a, Message> {
|
fn actions<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'a, Message> {
|
||||||
let label = catalog.text(Key::SettingsJavaPrefsSave);
|
let label = catalog.text(Key::SettingsJavaPrefsSave);
|
||||||
let save = if state.prefs.dirty() && state.storage {
|
let save = if state.prefs.dirty() && state.storage {
|
||||||
buttons::filled(ui, Button::new(label, msg(JavaMessage::SavePrefs)))
|
buttons::filled(
|
||||||
|
ui,
|
||||||
|
Button::new(label, msg(JavaMessage::SavePrefs)).base(BASE),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
buttons::filled(ui, Button::disabled(label))
|
buttons::filled(ui, Button::disabled(label).base(BASE))
|
||||||
};
|
};
|
||||||
let hint = if state.prefs.dirty() {
|
let hint = if state.prefs.dirty() {
|
||||||
catalog.text(Key::SettingsJavaPrefsUnsaved).to_string()
|
catalog.text(Key::SettingsJavaPrefsUnsaved).to_string()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ use unlucky_i18n::{Catalog, Key, Lang};
|
|||||||
|
|
||||||
use crate::app::components::buttons::{self, Button};
|
use crate::app::components::buttons::{self, Button};
|
||||||
use crate::app::components::selection::{chip, Chip};
|
use crate::app::components::selection::{chip, Chip};
|
||||||
use crate::pages::frame::{card, divider, text_role_owned};
|
use crate::pages::frame::{card, divider, text_role_owned, BASE};
|
||||||
use crate::pages::Message;
|
use crate::pages::Message;
|
||||||
use crate::theme::Ui;
|
use crate::theme::Ui;
|
||||||
|
|
||||||
@@ -114,10 +114,15 @@ fn status_row<'a>(ui: &'a Ui, catalog: Catalog, state: &State) -> Element<'a, Me
|
|||||||
};
|
};
|
||||||
|
|
||||||
let scan_label = catalog.text(Key::SettingsJavaScan);
|
let scan_label = catalog.text(Key::SettingsJavaScan);
|
||||||
|
// M237:这一整张卡片是 `frame::card` 画的(`Role::SurfaceContainerLow`),所以卡片里的
|
||||||
|
// 控件把底传成 `frame::BASE`——禁用态的 10% / 12% 叠在卡底上,不是画布上。
|
||||||
let scan = if scanning || !state.storage {
|
let scan = if scanning || !state.storage {
|
||||||
buttons::filled(ui, Button::disabled(scan_label))
|
buttons::filled(ui, Button::disabled(scan_label).base(BASE))
|
||||||
} else {
|
} else {
|
||||||
buttons::filled(ui, Button::new(scan_label, msg(JavaMessage::Scan)))
|
buttons::filled(
|
||||||
|
ui,
|
||||||
|
Button::new(scan_label, msg(JavaMessage::Scan)).base(BASE),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
// 已经按过取消(线程还没退出)时,「取消」也灰掉:再按一次没有任何意义。
|
// 已经按过取消(线程还没退出)时,「取消」也灰掉:再按一次没有任何意义。
|
||||||
let cancel = buttons::tonal(
|
let cancel = buttons::tonal(
|
||||||
@@ -129,7 +134,8 @@ fn status_row<'a>(ui: &'a Ui, catalog: Catalog, state: &State) -> Element<'a, Me
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Button::disabled(catalog.text(Key::SettingsJavaCancel))
|
Button::disabled(catalog.text(Key::SettingsJavaCancel))
|
||||||
},
|
}
|
||||||
|
.base(BASE),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 「扫描已取消」那句只在线程**真退出之后**出现;退出之前状态行本身写着「正在取消…」。
|
// 「扫描已取消」那句只在线程**真退出之后**出现;退出之前状态行本身写着「正在取消…」。
|
||||||
@@ -227,7 +233,7 @@ fn runtime_row<'a>(
|
|||||||
None => (String::new(), ui.color(Role::OnSurfaceVariant)),
|
None => (String::new(), ui.color(Role::OnSurfaceVariant)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let action = chip(ui, action_chip(catalog, state, runtime));
|
let action = chip(ui, action_chip(catalog, state, runtime).base(BASE));
|
||||||
|
|
||||||
let mut block = column![row![
|
let mut block = column![row![
|
||||||
container(
|
container(
|
||||||
|
|||||||
@@ -105,7 +105,12 @@ fn action_button<'a>(ui: &'a Ui, label: &'a str, message: Message) -> Element<'a
|
|||||||
.style(move |_theme: &IcedTheme, status| {
|
.style(move |_theme: &IcedTheme, status| {
|
||||||
let background =
|
let background =
|
||||||
matches!(status, button::Status::Hovered | button::Status::Pressed).then(|| {
|
matches!(status, button::Status::Hovered | button::Status::Pressed).then(|| {
|
||||||
Background::Color(ui.state_over(StateLayer::Hover, Role::OnSurface, Role::Surface))
|
// M237:这一行住在设置卡里(`frame::card`),hover 状态层叠卡底。
|
||||||
|
Background::Color(ui.state_over(
|
||||||
|
StateLayer::Hover,
|
||||||
|
Role::OnSurface,
|
||||||
|
frame::BASE.role(),
|
||||||
|
))
|
||||||
});
|
});
|
||||||
button::Style {
|
button::Style {
|
||||||
background,
|
background,
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
//! 「Java」卡片里的**禁用控件叠的是卡底**——页面级**像素**判据(M237)。
|
||||||
|
//!
|
||||||
|
//! ## 为什么非要从页面量
|
||||||
|
//!
|
||||||
|
//! `interaction::disabled::tests` 能证明「`Base` 传对了,合成就是对的」,`buttons::tests::base`
|
||||||
|
//! 能证明「`Button::base` 接到了样式闭包」——两条都**不**证明页面那天真的把 `frame::BASE` 传下去了
|
||||||
|
//! (M229 的教训:参数在、语义在,8 个调用点全传死 `Role::Surface`)。只有页面自己的像素能证明
|
||||||
|
//! 「这一屏里的禁用按钮是叠在卡底上画的」。
|
||||||
|
//!
|
||||||
|
//! ## 量什么
|
||||||
|
//!
|
||||||
|
//! `ULL_JAVA_STATE=scanning` 那一格里有一颗**禁用**的 Filled 按钮(「立即扫描」;「保存」同样是
|
||||||
|
//! 禁用态,但在这张卡的下半段、1180×760 的一帧里落在折叠线以下,量不到),
|
||||||
|
//! 它住在 `frame::card` 里(卡底 = `MaterialLayer::Card` = `SurfaceContainerLow`)。
|
||||||
|
//! 禁用容器 = `OnSurface` @10% 叠在底上,于是「叠卡底」与「叠画布」是**两个不同的逐位值**
|
||||||
|
//! (实测的 8 位 RGB,十进制写在这里——色值十六进制字面量在源码里是禁的,见
|
||||||
|
//! `tests/no_hardcoded_tokens.rs`):
|
||||||
|
//!
|
||||||
|
//! | 板 | 叠画布(`Base::CANVAS`) | 叠卡底(`frame::BASE`) | 逐位差 |
|
||||||
|
//! |---|---|---|---|
|
||||||
|
//! | Silver(浅) | (226, 226, 227) | (221, 221, 222) | 5/255 |
|
||||||
|
//! | Ink(深) | (37, 43, 36) | (40, 45, 40) | 3/255 |
|
||||||
|
//!
|
||||||
|
//! 差得这么小正是**必须逐位匹配**的理由:肉眼(以及 `±10` 容差的扫描器)分不出这两档,
|
||||||
|
//! 而「叠错层」这件事本身是确定的。两个值都在测试里**现算**
|
||||||
|
//! (`interaction::disabled_button_colors`),不写死——换板 / 改令牌时判据跟着走;
|
||||||
|
//! 上表只是给人读的量测记录。
|
||||||
|
//!
|
||||||
|
//! ## 跑法(1180×760 @1.0,`--software-renderer`)
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! EXE=${CARGO_TARGET_DIR:-target}/debug/unlucky-launcher-iced
|
||||||
|
//! OUT=${CARGO_TARGET_DIR:-target}/m237 && mkdir -p "$OUT"
|
||||||
|
//!
|
||||||
|
//! ULL_START_ROUTE=settings ULL_SETTINGS_GROUP=2 ULL_JAVA_STATE=scanning \
|
||||||
|
//! $EXE --software-renderer --scale-factor=1 \
|
||||||
|
//! --screenshot "$OUT/java-light.png" --screenshot-theme=light
|
||||||
|
//! ULL_M237_JAVA_SHOT="$OUT/java-light.png" cargo test -p unlucky-app-iced java_shot
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! `ULL_SETTINGS_GROUP=2` 是「Java」组(`settings::java::JAVA_GROUP`);不给就画出 General 组,
|
||||||
|
//! 那张图上没有禁用填充按钮,判据会拿不到样本并**大声报出来**(不是静默通过)。
|
||||||
|
//!
|
||||||
|
//! ## CI 上这条判据是**跳过**的(复审 P1-1)
|
||||||
|
//!
|
||||||
|
//! `.github/workflows/ci.yml` 的 test 作业在 ubuntu 与 windows 上都跑
|
||||||
|
//! `cargo test --workspace --all-targets`,而**出图步骤只给 `options_shot` 备了四张图**
|
||||||
|
//! (`ULL_DL_*_SHOT`,Windows 一条)。本判据要的这张 Java 图**不在出图清单里**
|
||||||
|
//! (`grep -r ULL_M237 .github/` 零命中)⇒ 在 CI 上缺图是**预期**,实现里因此**跳过而不是失败**
|
||||||
|
//! (见 [`not_fed`]:初版写成「CI 就 panic」,`CI=true` 实测 exit 101,一合就红)。
|
||||||
|
//!
|
||||||
|
//! 要让它真的进 CI 守门(跨 scope,塔另排):① ci.yml 加一条 `if: runner.os == 'Windows'` 的
|
||||||
|
//! 出图步骤(命令见上面的「跑法」);② 把 `ULL_M237_JAVA_SHOT` 写进 `$GITHUB_ENV`;
|
||||||
|
//! ③ 把 `not_fed` 里那条 `eprintln!` 换成 `assert!(!(cfg!(windows) && in_ci()), …)`
|
||||||
|
//! ——与 `pages/download/tests/options_shot/feed.rs::not_fed`(`:104-124`)同一口径。
|
||||||
|
//!
|
||||||
|
//! ## 抓得住 / 抓不住
|
||||||
|
//!
|
||||||
|
//! - 抓得住:① 页面调用点漏了 `.base(..)`(回落到 `Base::CANVAS`)——卡区里会出现「叠画布」档、
|
||||||
|
//! 「叠卡底」档消失;② `frame::BASE` 被改成别的角色(判据跟着 `BASE` 走,于是与像素对不上);
|
||||||
|
//! ③ 出图通道换了组 / 换了主题(判据先用卡底色**认出**板,认不出就红)。
|
||||||
|
//! - 抓不住:① 面板 α < 1 的 GPU 档(本判据读的是 tiny-skia 的图,α 恒 1);② 卡外的控件
|
||||||
|
//! (判据只看卡区);③ 具体是哪一颗按钮变成了卡底(只数颜色总量,不定位控件)。
|
||||||
|
|
||||||
|
use unlucky_design::color::SchemeFlavor;
|
||||||
|
use unlucky_design::typography::Platform;
|
||||||
|
|
||||||
|
use crate::app::components::interaction::{self, Base};
|
||||||
|
use crate::pages::frame;
|
||||||
|
use crate::theme::{Families, Ui};
|
||||||
|
|
||||||
|
const FAMILIES: Families = Families {
|
||||||
|
text: "Segoe UI",
|
||||||
|
display: "Segoe UI",
|
||||||
|
mono: "Consolas",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 出图路径(判据的输入)。
|
||||||
|
const SHOT_ENV: &str = "ULL_M237_JAVA_SHOT";
|
||||||
|
/// 卡区里「叠卡底」的禁用容器像素下限(两颗 40 高的药丸,扣掉字形与圆角后实测 ≈3100)。
|
||||||
|
const MIN_HITS: usize = 1000;
|
||||||
|
/// 「正确档 : 错误档」的最小倍数(抗锯齿会在边缘撞上另一档的精确值,实测个位到十几像素)。
|
||||||
|
const STRAY_RATIO: usize = 10;
|
||||||
|
/// 卡区的下限(一张 400+ 宽的卡,实测 ≈32 万像素)——低于它说明这一屏根本没有卡。
|
||||||
|
const MIN_CARD_PX: usize = 50_000;
|
||||||
|
|
||||||
|
fn ui(flavor: SchemeFlavor) -> Ui {
|
||||||
|
Ui::with_platform(flavor, Platform::Windows, FAMILIES)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// iced 颜色 → 8 位三元组(与 `to_iced` 同一条口径:`f32` 0..1 四舍五入回 0..255)。
|
||||||
|
fn rgb(color: iced::Color) -> [u8; 3] {
|
||||||
|
let byte = |channel: f32| (channel * 255.0).round().clamp(0.0, 255.0) as u8;
|
||||||
|
[byte(color.r), byte(color.g), byte(color.b)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 一张 RGB 图(只留本判据要的两件事:尺寸 + 取点)。
|
||||||
|
struct Shot {
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
pixels: Vec<[u8; 3]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shot {
|
||||||
|
fn load(path: &std::path::Path) -> Self {
|
||||||
|
let bytes = std::fs::read(path).expect("读截图");
|
||||||
|
let image = image::load_from_memory(&bytes).expect("解 PNG").to_rgb8();
|
||||||
|
let (width, height) = (image.width() as usize, image.height() as usize);
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
pixels: image.pixels().map(|pixel| pixel.0).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn at(&self, x: usize, y: usize) -> [u8; 3] {
|
||||||
|
self.pixels[y * self.width + x]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 逐位相等的像素数(`±10` 容差在这里没用:两档底只差 5 个 RGB 单位)。
|
||||||
|
fn count(&self, color: [u8; 3]) -> usize {
|
||||||
|
self.pixels.iter().filter(|pixel| **pixel == color).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 某颜色像素的外接矩形 `(x0, y0, x1, y1)`(闭区间),一个都没有时 `None`。
|
||||||
|
fn bbox(&self, color: [u8; 3]) -> Option<(usize, usize, usize, usize)> {
|
||||||
|
let mut found = None;
|
||||||
|
for y in 0..self.height {
|
||||||
|
for x in 0..self.width {
|
||||||
|
if self.at(x, y) == color {
|
||||||
|
let (x0, y0, x1, y1) = found.unwrap_or((x, y, x, y));
|
||||||
|
found = Some((x0.min(x), y0.min(y), x1.max(x), y1.max(y)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 矩形内的逐位相等像素数。
|
||||||
|
fn count_in(&self, color: [u8; 3], rect: (usize, usize, usize, usize)) -> usize {
|
||||||
|
let (x0, y0, x1, y1) = rect;
|
||||||
|
(y0..=y1.min(self.height - 1))
|
||||||
|
.map(|y| {
|
||||||
|
(x0..=x1.min(self.width - 1))
|
||||||
|
.filter(|x| self.at(*x, y) == color)
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按**卡底色**认出这张图是哪块板(认不出 = 这一屏没有卡 / 主题对不上,直接红)。
|
||||||
|
fn flavor_of(shot: &Shot) -> SchemeFlavor {
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||||
|
let card = rgb(ui(flavor).color(frame::BASE.role()));
|
||||||
|
let hits = shot.count(card);
|
||||||
|
if hits >= MIN_CARD_PX {
|
||||||
|
candidates.push((flavor, card, hits));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
candidates.len(),
|
||||||
|
1,
|
||||||
|
"这一屏里「卡底」色的像素必须恰好落在一种板上,实测 {candidates:?}:\
|
||||||
|
检查出图命令是否带了 `--screenshot-theme` 与 `ULL_SETTINGS_GROUP=2`"
|
||||||
|
);
|
||||||
|
candidates[0].0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 没喂图时的统一裁决:**一律响亮地跳过**,绝不 panic。
|
||||||
|
///
|
||||||
|
/// ## 为什么这里不能照 `options_shot/feed.rs` 那样「CI 就硬失败」(复审 P1-1)
|
||||||
|
///
|
||||||
|
/// `options_shot` 那条硬失败(`feed.rs:104-124` 的 `!(cfg!(windows) && in_ci())`)成立的前提是
|
||||||
|
/// `.github/workflows/ci.yml` 的 test 作业**真的**在 Windows 上出那四张图、并把 `ULL_DL_*_SHOT`
|
||||||
|
/// 写进 `$GITHUB_ENV`(`ci.yml:92-123`)。本判据的图**不在任何出图清单里**
|
||||||
|
/// (`grep -r ULL_M237 .github/` 零命中),而 `cargo test --workspace --all-targets` 在 ubuntu 与
|
||||||
|
/// windows 两个 runner 上都跑 ⇒ 照抄那条口径会让**每个 PR 一亮红**:实测
|
||||||
|
/// `CI=true cargo test -p unlucky-app-iced --lib java_shot` 会 panic(初版就是这么错的)。
|
||||||
|
///
|
||||||
|
/// ## 要在 CI 上真的守门(跨 scope,由塔另排)
|
||||||
|
///
|
||||||
|
/// 1. `ci.yml` 加一条 `if: runner.os == 'Windows'` 的出图步骤(命令见本文件模块文档的「跑法」);
|
||||||
|
/// 2. 把 `ULL_M237_JAVA_SHOT` 写进 `$GITHUB_ENV`(与 `ULL_DL_*_SHOT` 同一条路);
|
||||||
|
/// 3. 那时把本函数末尾那条 `eprintln!` 换成
|
||||||
|
/// `assert!(!(cfg!(windows) && in_ci()), "守门未喂图:{SHOT_ENV} 没设 …")`
|
||||||
|
/// —— 与 `options_shot/feed.rs::not_fed` 同一口径:**只有「会出图的平台 + CI」才硬失败**,
|
||||||
|
/// 别的平台缺图是预期。
|
||||||
|
fn not_fed() {
|
||||||
|
let ci = std::env::var_os("CI").is_some_and(|value| !value.is_empty());
|
||||||
|
eprintln!("[pages] 页面级像素判据未喂图:{SHOT_ENV} 没设,跳过(跑法见本文件模块文档)。");
|
||||||
|
if ci {
|
||||||
|
eprintln!(
|
||||||
|
"[pages] 当前是 CI:本仓 CI 不出这张图(ci.yml 只给 options_shot 注入 ULL_DL_*_SHOT),\
|
||||||
|
所以这里跳过是**预期**,不是漏跑。让它守门的步骤见 not_fed 的文档。"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_java_card_composites_disabled_controls_over_its_own_base() {
|
||||||
|
let Some(path) = std::env::var_os(SHOT_ENV).filter(|value| !value.is_empty()) else {
|
||||||
|
not_fed();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let shot = Shot::load(std::path::Path::new(&path));
|
||||||
|
let flavor = flavor_of(&shot);
|
||||||
|
let ui = ui(flavor);
|
||||||
|
|
||||||
|
let over_card = rgb(interaction::disabled_button_colors(&ui, frame::BASE).0);
|
||||||
|
let over_canvas = rgb(interaction::disabled_button_colors(&ui, Base::CANVAS).0);
|
||||||
|
assert_ne!(
|
||||||
|
over_card, over_canvas,
|
||||||
|
"{flavor:?}: 叠卡底与叠画布的禁用容器算出来一样 —— 判据自己立不住"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 卡区 = 卡底色像素的外接矩形(卡是这一屏最大的同色块)
|
||||||
|
let card_color = rgb(ui.color(frame::BASE.role()));
|
||||||
|
let rect = shot
|
||||||
|
.bbox(card_color)
|
||||||
|
.unwrap_or_else(|| panic!("{flavor:?}: 图里找不到卡底色 {card_color:?}"));
|
||||||
|
|
||||||
|
let hits = shot.count_in(over_card, rect);
|
||||||
|
let stray = shot.count_in(over_canvas, rect);
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"[pages] OK flavor={flavor:?} 卡区 {rect:?}:叠卡底 {over_card:?} {hits} px · \
|
||||||
|
叠画布 {over_canvas:?} {stray} px"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
hits >= MIN_HITS,
|
||||||
|
"{flavor:?}: 卡区里「叠 {:?}」的禁用容器只有 {hits} px(应 ≥{MIN_HITS})—— \
|
||||||
|
卡片里的禁用控件没把 `frame::BASE` 传下去(还在按画布合成)",
|
||||||
|
frame::BASE.role()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
hits > stray * STRAY_RATIO,
|
||||||
|
"{flavor:?}: 卡区里「叠卡底」{hits} px,而「叠画布」{stray} px(要求前者 ≥ 后者 ×{STRAY_RATIO})—— \
|
||||||
|
卡区里仍有一部分禁用控件按 `Role::Surface` 合成"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ mod appearance_section;
|
|||||||
mod download_section;
|
mod download_section;
|
||||||
mod general_section;
|
mod general_section;
|
||||||
mod java_section;
|
mod java_section;
|
||||||
|
mod java_shot;
|
||||||
mod scroll_fit;
|
mod scroll_fit;
|
||||||
mod update_section;
|
mod update_section;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
//!
|
//!
|
||||||
//! 每条断言都注明了「抓得住什么 / 抓不住什么」——只测到纯函数,渲染真像素另见报告里的图与量测。
|
//! 每条断言都注明了「抓得住什么 / 抓不住什么」——只测到纯函数,渲染真像素另见报告里的图与量测。
|
||||||
|
|
||||||
|
mod base_wiring;
|
||||||
|
|
||||||
use super::layout::{self, frame_metrics, measure_cap_px, PaneKit, WidthClass};
|
use super::layout::{self, frame_metrics, measure_cap_px, PaneKit, WidthClass};
|
||||||
use super::nav::NAV;
|
use super::nav::NAV;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
//! 页面层**接线**守门:卡片里的控件有没有把 `frame::BASE` 传下去(M237)。
|
||||||
|
//!
|
||||||
|
//! ## 为什么还要一条「数接线点」的判据
|
||||||
|
//!
|
||||||
|
//! 像素判据(`pages/settings/tests/java_shot.rs`)只落在**一屏能拍到的**控件上。实例详情页的
|
||||||
|
//! MODS / SAVES 两张卡在现有夹具下拍不到禁用控件:`ULL_MODS_FIXTURE` 把 `running` 写死成
|
||||||
|
//! `false`(开关全是启用态),SAVES 的动作行在 1180×760 的折叠线以下。那几处接线于是**没有
|
||||||
|
//! 任何渲染证据**能守——有人删掉一个 `.base(..)`,图和一整张像素判据都不会变。
|
||||||
|
//!
|
||||||
|
//! 这里退一步,只数「每个文件里出现了几次接线调用」。它证明的是**接线还在**,不是「画对了」;
|
||||||
|
//! 真正的「叠的是哪一层底」由像素判据与 `interaction::disabled::tests` 两层负责。
|
||||||
|
//!
|
||||||
|
//! 清单里有两种接线:**禁用态**(`Button::disabled` / `Switch::disabled` / `Chip::disabled`)与
|
||||||
|
//! **hover / 选中状态层**(`Text`·`Outlined` 按钮、`ListItem` 行、手写 chip 的 `state_over`)。
|
||||||
|
//! 像素判据只量到禁用态那一档(`pages/settings/tests/java_shot.rs` 的 Java 卡),
|
||||||
|
//! 所以 `instances.rs`(列表行,复审 P2-1)这种**只有状态层**的接线只能靠这份计数守。
|
||||||
|
//!
|
||||||
|
//! ## 抓得住 / 抓不住
|
||||||
|
//!
|
||||||
|
//! - 抓得住:删掉 / 注释掉某个调用点的 `.base(..)`、或把整个文件的接线回退(数字掉一即红)。
|
||||||
|
//! - 抓不住:① 传**错的**底(换成 `Base::CANVAS` 数字不变,只有像素判据能抓);
|
||||||
|
//! ② 把接线搬进辅助函数(那是等价重构,改这里的数字并在提交信息里说明搬去了哪);
|
||||||
|
//! ③ 新加的控件(新调用点没人接线时数字不会变——这条只守**已经签过的**那几处)。
|
||||||
|
//!
|
||||||
|
//! ## 数字怎么来的
|
||||||
|
//!
|
||||||
|
//! 每个文件里 `.base(` 的出现次数(`grep -o "\.base(" <file> | wc -l`),三个手写样式的文件
|
||||||
|
//! 数是 `BASE.role()`(它们不走组件的 `.base(..)`,而是把状态层直接叠在 `frame::BASE` 上)。
|
||||||
|
|
||||||
|
/// `(文件, 期望的接线次数)`:文件路径相对 `pages/`。
|
||||||
|
const WIRED: [(&str, usize); 9] = [
|
||||||
|
("instances.rs", 1),
|
||||||
|
("instances/mods.rs", 1),
|
||||||
|
("instances/saves.rs", 2),
|
||||||
|
("instances/launch_args.rs", 5),
|
||||||
|
("logs/diagnostic.rs", 1),
|
||||||
|
("settings/java/view.rs", 4),
|
||||||
|
("settings/java/fields.rs", 7),
|
||||||
|
("settings/java/prefs_view.rs", 8),
|
||||||
|
("settings/java/download_view.rs", 5),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 手写样式(不走组件 `.base(..)`)的三处:状态层直接叠在 `frame::BASE.role()` 上。
|
||||||
|
const ROLE_SITES: [(&str, usize); 3] = [
|
||||||
|
("settings/general.rs", 1),
|
||||||
|
("settings/general/theme.rs", 1),
|
||||||
|
("settings/logs_row.rs", 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 数**非注释行**里 `needle` 的出现次数。
|
||||||
|
///
|
||||||
|
/// 整行注释(`//` / `///` / `//!`)先剔掉:**把接线注释掉不算接线**,而「注释掉一行」恰恰是
|
||||||
|
/// 最容易发生的回退。复审 r2 实测过:只按 `source.matches()` 数,注释掉
|
||||||
|
/// `item.base(frame::BASE)` 之后本判据仍是绿的(注释里还留着 `.base(` 这四个字符)。
|
||||||
|
fn count(haystack: &str, needle: &str) -> usize {
|
||||||
|
haystack
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.trim_start().starts_with("//"))
|
||||||
|
.map(|line| line.matches(needle).count())
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_card_internal_control_site_still_passes_a_base() {
|
||||||
|
let sources: [(&str, &str); 12] = [
|
||||||
|
("instances.rs", include_str!("../instances.rs")),
|
||||||
|
("instances/mods.rs", include_str!("../instances/mods.rs")),
|
||||||
|
("instances/saves.rs", include_str!("../instances/saves.rs")),
|
||||||
|
(
|
||||||
|
"instances/launch_args.rs",
|
||||||
|
include_str!("../instances/launch_args.rs"),
|
||||||
|
),
|
||||||
|
("logs/diagnostic.rs", include_str!("../logs/diagnostic.rs")),
|
||||||
|
(
|
||||||
|
"settings/java/view.rs",
|
||||||
|
include_str!("../settings/java/view.rs"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"settings/java/fields.rs",
|
||||||
|
include_str!("../settings/java/fields.rs"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"settings/java/prefs_view.rs",
|
||||||
|
include_str!("../settings/java/prefs_view.rs"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"settings/java/download_view.rs",
|
||||||
|
include_str!("../settings/java/download_view.rs"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"settings/general.rs",
|
||||||
|
include_str!("../settings/general.rs"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"settings/general/theme.rs",
|
||||||
|
include_str!("../settings/general/theme.rs"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"settings/logs_row.rs",
|
||||||
|
include_str!("../settings/logs_row.rs"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (name, want) in WIRED {
|
||||||
|
let source = sources
|
||||||
|
.iter()
|
||||||
|
.find(|(file, _)| *file == name)
|
||||||
|
.map(|(_, source)| *source)
|
||||||
|
.unwrap_or_else(|| panic!("{name} 不在本判据的清单里(清单与文件对不上)"));
|
||||||
|
let got = count(source, ".base(");
|
||||||
|
assert_eq!(
|
||||||
|
got, want,
|
||||||
|
"{name}: `.base(..)` 出现 {got} 次,期望 {want} 次 —— 卡片内的控件必须把 \
|
||||||
|
`frame::BASE` 传下去(`pages/frame/pieces.rs` 的 `BASE`)。\
|
||||||
|
如果你是把接线搬进了辅助函数(等价重构),把这里的数字跟着改并说明搬去了哪。"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, want) in ROLE_SITES {
|
||||||
|
let source = sources
|
||||||
|
.iter()
|
||||||
|
.find(|(file, _)| *file == name)
|
||||||
|
.map(|(_, source)| *source)
|
||||||
|
.unwrap_or_else(|| panic!("{name} 不在本判据的清单里(清单与文件对不上)"));
|
||||||
|
let got = count(source, "BASE.role()");
|
||||||
|
assert_eq!(
|
||||||
|
got, want,
|
||||||
|
"{name}: `BASE.role()` 出现 {got} 次,期望 {want} 次 —— 手写按钮的 hover / 禁用\
|
||||||
|
状态层必须叠在卡片底上(画布底会让它在卡里偏色,M237)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user