diff --git a/crates/app-iced/src/components/selection/chip.rs b/crates/app-iced/src/components/selection/chip.rs index fc149a1..ba1b8fe 100644 --- a/crates/app-iced/src/components/selection/chip.rs +++ b/crates/app-iced/src/components/selection/chip.rs @@ -14,6 +14,24 @@ //! M3 的 chip **没有圆形状态层**:hover / pressed 就是把状态层叠在**自己的容器**上, //! 而容器尺寸 = 命中区尺寸。所以 chip 直接走 `button` 的样式闭包(与按钮家族同一条产线), //! 不需要「48 外壳 + 40 圆」那套。 +//! +//! ## 「选中」与「当前项」:两个档位,一套颜色(M206) +//! +//! 这两件事**不是**「选中 / 未选中」,而是「可点的选中」与「不可点的当前项」——它们必须长得 +//! **一样**(同一个视觉语汇),差别只在**有没有反馈**: +//! +//! | 档位 | 构造器 | 容器 / 文字 | 对比度下限 | 五态反馈 | 什么时候用 | +//! |---|---|---|---|---|---| +//! | A 可点选中 | [`Chip::filter`] `(label, true, msg)` | `SecondaryContainer` / `OnSecondaryContainer` | **≥ 4.5:1**(WCAG AA 正文;实测 7.21:1 浅 / 7.21:1 深) | 有(hover/pressed 状态层) | 点它**会改变**选中项(下载组的分段单选、预置项开关) | +//! | B 不可点当前项 | [`Chip::current`] | **同 A,一个像素都不差** | **≥ 4.5:1**(同 7.21:1) | **无**(没有可点性就没有反馈) | 「这就是当前生效的那个」,再点一次没有动作(Java 组的「默认」) | +//! | (不是档位)暂不可用 | [`Chip::badge`] / [`.disabled()`](Chip::disabled) | `OnSurface`@12% / @38% | **不适用**:它是禁用语汇(实测 1.84:1 浅 / 2.28:1 深) | — | 真的点不了(下载进行中把「安装」灰掉、LTS/基线这类标签) | +//! +//! **判据只有一句**:这个 chip 点下去**会不会改变**它表示的值? +//! 会 → A;不会(它已经是当前值)→ B;**根本不能点**(忙/不可用)→ 禁用档。 +//! 尤其:**不许拿 `badge` 表示「当前默认」**——那是把「不可用」的语汇贴在「答案」上, +//! M204 实测出来就是整页最不可读的一段(1.84:1)。这一条不靠调用方记住,`Chip::current` +//! 与 `paint::current_item` 的文档 + `pages::settings::java::tests::marker` 的三条断言 +//! (对比度 / 同族 / 禁用语义)一起把它钉在组件层。 use iced::widget::{button, container, row, text}; use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow}; @@ -38,6 +56,8 @@ pub struct Chip<'a, M> { trailing: Option, on_press: Option, state: Option, + /// 「当前项」标记([`Chip::current`]):选中配色照画,但**不**降成禁用配色、也不吃五态。 + marker: bool, } impl<'a, M> Chip<'a, M> { @@ -58,8 +78,12 @@ impl<'a, M> Chip<'a, M> { /// **纯展示**的 filter chip(选中态、无消息 ⇒ 永远不可交互)。 /// - /// 与 `filter(label, true, msg).disabled()` 等价,但**不要求调用方编一条永远收不到的消息**: - /// 本轮 Java 页那颗「默认」标记就是这么用的(`Message::Noop` 已删,别再拿一条真消息当占位)。 + /// 与 `filter(label, true, msg).disabled()` 等价,但**不要求调用方编一条永远收不到的消息**。 + /// + /// ⚠️ **它不是「当前项」**(M206 纠正):`badge` 走的是**禁用**配色(`OnSurface`@38% 叠 + /// surface,实测 **1.84:1 浅 / 2.28:1 深**),那是「暂不可用」的语汇。要表达「这行就是当前 + /// 生效的那个」,用 [`Chip::current`]——它和下载组的选中 chip 同一套颜色,7.21:1。 + /// 仍然用 `badge` 的两处都是**真禁用语义**:下载进行中把「安装」灰掉、以及 LTS/基线/推荐这类标签。 pub fn badge(label: &'a str) -> Self { Self { label, @@ -69,6 +93,29 @@ impl<'a, M> Chip<'a, M> { trailing: None, on_press: None, state: None, + marker: false, + } + } + + /// **当前项**标记:`SecondaryContainer` + `OnSecondaryContainer`(与可点的选中 chip + /// **同一套配色**),没有消息 ⇒ 也不吃 hover / pressed / 禁用态。 + /// + /// 语义:「这一行就是当前生效的那个」。它与 [`Chip::filter`]`(label, true, msg)` 的差别 + /// **只在有没有动作**——点当前值不会改变任何东西,所以给它一个永远不响的消息是撒谎 + /// (`Message::Noop` 已删,见 `Chip::badge` 的旧注)。判据表见本文档开头的「选中与当前项」。 + /// + /// 为什么不能写成「`filter(.., true, ..)` + `.disabled()`」:`disabled()` 会走禁用配色 + /// (38% 文字、12% 容器),当前项就又变成不可读了——这正是 M206 要修的那件事。 + pub fn current(label: &'a str) -> Self { + Self { + label, + variant: ChipVariant::Filter, + selected: true, + leading: None, + trailing: None, + on_press: None, + state: None, + marker: true, } } @@ -92,6 +139,7 @@ impl<'a, M> Chip<'a, M> { trailing: None, on_press: Some(on_press), state: None, + marker: false, } } @@ -112,6 +160,25 @@ impl<'a, M> Chip<'a, M> { self.state = Some(state); self } + + /// 静息时的外观——**唯一**把「这颗 chip 属于哪个档位」翻成颜色的地方。 + /// + /// `chip()` 用它求文字/图标色,测试也用它量对比度:档位的判据(当前项 / 选中 / 禁用) + /// 只有这一处,所以「实现改回旧配色」这类变异会被 + /// `pages::settings::java::tests::marker` 的对比度断言直接抓住。 + pub fn paint(&self, ui: &Ui) -> paint::ChipPaint { + let radius = ChipGeometry::of(ui).radius; + if self.marker { + return paint::current_item(ui, radius); + } + // 没有消息 = 禁用(`Chip::badge` 与 `.disabled()` 都走这里)。 + let state = if self.on_press.is_none() { + Interaction::Disabled + } else { + self.state.unwrap_or(Interaction::Enabled) + }; + paint::chip(ui, self.variant, self.selected, state, radius) + } } /// 画一个 chip。 @@ -119,22 +186,14 @@ pub fn chip<'a, M: Clone + 'a>(ui: &'a Ui, spec: Chip<'a, M>) -> Element<'a, M> let geometry = ChipGeometry::of(ui); let variant = spec.variant; let selected = spec.selected; - let disabled = spec.on_press.is_none(); + let marker = spec.marker; let forced = spec.state; // 文字/图标色只看「选中 + 禁用」(A1:Filter/Input 的 label 色在 hover/focus/pressed 不变), - // 所以这里静态求值;容器(含状态层)在样式闭包里跟态走。 - let at_rest = paint::chip( - ui, - variant, - selected, - if disabled { - Interaction::Disabled - } else { - forced.unwrap_or(Interaction::Enabled) - }, - geometry.radius, - ); + // 所以这里静态求值;容器(含状态层)在样式闭包里跟态走。档位的判据在 `Chip::paint` 一处 + // (M206:当前项标记走 `paint::current_item`——**不是**禁用态。它的不可点性与「忙/不可用」 + // 是两件事:前者是「已经选中的那个」(选中配色),后者是「暂不可用」(38% 文字))。 + let at_rest = spec.paint(ui); let mut content = row![].align_y(Alignment::Center).spacing(geometry.gap); if let Some(kind) = spec.leading { @@ -177,8 +236,13 @@ pub fn chip<'a, M: Clone + 'a>(ui: &'a Ui, spec: Chip<'a, M>) -> Element<'a, M> left: geometry.pad, }) .style(move |_theme, status| { - let state = forced.unwrap_or_else(|| Interaction::from_status(status)); - let paint = paint::chip(ui, variant, selected, state, geometry.radius); + // 标记不吃五态:没有可点性就没有反馈(`--force-state` 也不该把它画成 hover)。 + let paint = if marker { + paint::current_item(ui, geometry.radius) + } else { + let state = forced.unwrap_or_else(|| Interaction::from_status(status)); + paint::chip(ui, variant, selected, state, geometry.radius) + }; button::Style { background: paint.frame.fill.map(Background::Color), text_color: paint.label, diff --git a/crates/app-iced/src/components/selection/paint/chip.rs b/crates/app-iced/src/components/selection/paint/chip.rs index 3277f33..503ff16 100644 --- a/crates/app-iced/src/components/selection/paint/chip.rs +++ b/crates/app-iced/src/components/selection/paint/chip.rs @@ -77,3 +77,30 @@ pub fn chip( icon: ui.color(icon_role), } } + +/// 「**当前项**」标记的外观(M206):**就是选中 chip 的那一套**,一个像素都不另起一套。 +/// +/// 为什么要一个具名入口而不是让调用点自己拼 `chip(.., Filter, true, Enabled, ..)`: +/// +/// 1. 这个语义(「这行就是当前生效的那个」)此前被画成了 [`super::super::Chip::badge`] 的 +/// **禁用**配色(`OnSurface`@38% 叠 surface,M204 实测 **1.84:1 浅 / 2.28:1 深**)——那是 +/// 「暂不可用」的语汇,而当前项不是不可用,它是**已经选中的那个**。修法不是给禁用配色调亮, +/// 而是让当前项改用**选中**的语汇; +/// 2. 与左侧导航里相邻的「下载」组的选中 chip(`Chip::filter(.., true, ..)`,容器 +/// `SecondaryContainer`、文字 `OnSecondaryContainer`,实测 **7.21:1 浅 / 7.21:1 深**) +/// **同族**:两页表达「选中」只用一套颜色,用户不用在两页之间切换心智模型。身份由 +/// `pages::settings::java::tests::marker` 的 +/// `the_current_default_marker_speaks_the_download_groups_language` 钉住——谁把其中一页 +/// 改成另一套配色,那条立刻红。具体的 8 bit 色值不写在这里:`tests/no_hardcoded_tokens.rs` +/// 连注释里的十六进制色值也拦,色值只该在报告与令牌表里出现。 +/// +/// 与选中 chip 的唯一差别**不在颜色**:标记没有消息(`on_press == None`),所以它不吃 hover / +/// pressed / disabled 五态(见 `Chip::current` 的文档)。对比度下限是 [`crate::components::selection::paint`] +/// 同一条正文口径(WCAG AA 4.5:1,`unlucky_design::palette::wcag::AA_TEXT`)。 +/// +/// 与选中 chip 的唯一差别**不在颜色**:标记没有消息(`on_press == None`),所以它不吃 hover / +/// pressed / disabled 五态(见 `Chip::current` 的文档)。对比度下限是 [`crate::components::selection::paint`] +/// 同一条正文口径(WCAG AA 4.5:1,`unlucky_design::palette::wcag::AA_TEXT`)。 +pub fn current_item(ui: &Ui, radius: f32) -> ChipPaint { + chip(ui, ChipVariant::Filter, true, Interaction::Enabled, radius) +} diff --git a/crates/app-iced/src/components/selection/paint/mod.rs b/crates/app-iced/src/components/selection/paint/mod.rs index b52e4ee..648f40a 100644 --- a/crates/app-iced/src/components/selection/paint/mod.rs +++ b/crates/app-iced/src/components/selection/paint/mod.rs @@ -35,7 +35,7 @@ mod radio; mod switch; pub use check::{checkbox, checkbox_layer_role}; -pub use chip::{chip, ChipVariant}; +pub use chip::{chip, current_item, ChipVariant}; pub use radio::{radio, radio_layer_role}; pub use switch::{switch, switch_layer}; diff --git a/crates/app-iced/src/pages/settings/java/tests.rs b/crates/app-iced/src/pages/settings/java/tests.rs index d26f603..e9fe3d2 100644 --- a/crates/app-iced/src/pages/settings/java/tests.rs +++ b/crates/app-iced/src/pages/settings/java/tests.rs @@ -299,3 +299,7 @@ mod runtime; #[path = "tests/prefs.rs"] mod prefs; + +// M206:「当前默认」那颗标记的可读性(对比度 / 同族 / 禁用语义)断言。 +#[path = "tests/marker.rs"] +mod marker; diff --git a/crates/app-iced/src/pages/settings/java/tests/marker.rs b/crates/app-iced/src/pages/settings/java/tests/marker.rs new file mode 100644 index 0000000..cb91b73 --- /dev/null +++ b/crates/app-iced/src/pages/settings/java/tests/marker.rs @@ -0,0 +1,218 @@ +//! 「当前默认」那颗标记的**可读性**断言(M206)。 +//! +//! 起因是 M204 的视觉复测量出来的两个数:Java 页用 `Chip::badge` 表示「哪个 JDK 是默认」, +//! 而 `badge` 走的是**禁用**配色(`OnSurface`@38% 叠 surface)⇒ 字/底只有 **1.84:1(浅)/ +//! 2.28:1(深)**,成了整页最不可读的一段;而同一张设置页里相邻的「下载」组表达「选中」用的是 +//! `SecondaryContainer` + `OnSecondaryContainer`(**7.21:1**)。同一个「选中」在两页是两种语汇。 +//! +//! 这里钉三件事(每条都写过「抓得住 / 抓不住」): +//! +//! | 断言 | 抓得住 | 抓不住 | +//! |---|---|---| +//! | [`the_current_default_marker_clears_wcag_aa`] | 当前项标记的字/底对比度掉到 4.5:1 以下(例如 `view::action_chip` 改回 `Chip::badge`、或 `Chip::current` 的 `marker` 位丢了) | 字体粗细 / 字号是否够大(`LabelLarge` 是正文口径,本来就得 4.5);真截图上的抗锯齿边缘 | +//! | [`the_current_default_marker_speaks_the_download_groups_language`] | 两页的「选中」被拆成两套配色(谁再另起一套立刻红) | 「两页在同一张图上看起来协调」这种整体观感(那要视觉 reviewer 判) | +//! | [`the_disabled_chip_still_looks_disabled`] | 有人为了让当前项好看,顺手把 `badge` / `.disabled()` 也接到选中配色上(「忙/不可用」于是看起来可点) | 禁用态够不够灰的美学判断——只钉到令牌:字 38%、容器 12% | +//! +//! **它们都不碰像素**:颜色是「角色 → 令牌 → 对比度」这条链上的值断言,真图另存 +//! `DevDocs/artifacts/m206/`(D30 §2.1 三件套)。 + +use iced::Color; + +use unlucky_design::color::{Color as TokenColor, Role, SchemeFlavor}; +use unlucky_design::palette::wcag::AA_TEXT; +use unlucky_design::typography::Platform; +use unlucky_i18n::Lang; + +use crate::app::components::interaction; +use crate::app::components::selection::paint::ChipPaint; +use crate::app::components::selection::Chip; +use crate::theme::{Families, Ui}; + +use super::super::seed; +use super::super::state::State; +use super::super::view::action_chip; + +const FAMILIES: Families = Families { + text: "Segoe UI", + display: "Segoe UI", + mono: "Consolas", +}; + +const FLAVORS: [SchemeFlavor; 2] = [SchemeFlavor::Light, SchemeFlavor::Dark]; + +fn ui(flavor: SchemeFlavor) -> Ui { + Ui::with_platform(flavor, Platform::Windows, FAMILIES) +} + +/// iced 的 sRGB 分量 → 令牌色([`crate::theme::to_iced`] 的逆;`to_iced` 是从 8 bit 来的, +/// 所以来回一趟无误差)。 +fn token(color: Color) -> TokenColor { + let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; + TokenColor::rgba(q(color.r), q(color.g), q(color.b), q(color.a)) +} + +/// 一颗 chip 的「字色 vs 底色」。容器为空(透明)时按 surface 算——chip 的透明态本来就压在 +/// 卡片底上,这条口径与 `wcag::check` 的 `(OnSurfaceVariant, Surface)` 一致。 +fn ink_ratio(paint: &ChipPaint, ui: &Ui) -> f32 { + let background = paint.frame.fill.unwrap_or_else(|| ui.color(Role::Surface)); + token(paint.label).contrast_ratio(token(background)) +} + +/// 截图种子里那两行 JDK(`ULL_JAVA_STATE=two` 用的同一份),其中**第二行是当前默认**。 +fn seeded() -> State { + seed::build("two").expect("种子 two 必须存在") +} + +// ------------------------------------------------- ① 对比度:当前项标记必须过 AA 正文 + +/// 「当前默认」那颗标记的字色与底色 ≥ **4.5:1**(WCAG 2.x AA 正文口径),浅深两档都要过。 +/// +/// 改前实测(M204,`Chip::badge` 的禁用配色):**1.84:1 浅 / 2.28:1 深**;改后(本断言钉的): +/// **7.21:1 浅 / 7.21:1 深**——与下载组的选中 chip 同一个数,因为就是同一套角色。 +/// +/// 变异证明(只改实现侧):把 `view::action_chip` 的 `Chip::current` 换回 `Chip::badge`, +/// 这条立刻红(比值掉回 1.84 / 2.28);把 `Chip::current` 里的 `marker: true` 改成 `false` +/// 也一样红(它会退回「无消息 ⇒ 禁用」那条路)。 +#[test] +fn the_current_default_marker_clears_wcag_aa() { + let catalog = Lang::ZhCn.catalog(); + let state = seeded(); + + for flavor in FLAVORS { + let ui = ui(flavor); + // 种子里选中的是第二行(`C:\Program Files\Microsoft\jdk-21.0.4`)。 + let marker_row = state + .runtimes + .iter() + .find(|runtime| super::super::state::same_path(&runtime.binary, &state.selected)) + .expect("种子里必须有一行是当前默认"); + let paint = action_chip(catalog, &state, marker_row).paint(&ui); + + // 先量数(这条是任务书点名要的那条):谁把配色改回禁用灰,红在这里。 + let ratio = ink_ratio(&paint, &ui); + assert!( + ratio >= AA_TEXT, + "{flavor:?}:「当前默认」的字/底只有 {ratio:.2}:1,低于 AA 正文 {AA_TEXT}:1 —— \ + 这正是 M206 要修的那段(改前 1.84 浅 / 2.28 深)" + ); + + // 再钉角色名:比值恰好过线也可能是碰巧换了个色,这里把「用的是哪两个角色」写死。 + assert_eq!( + paint.frame.fill, + Some(ui.color(Role::SecondaryContainer)), + "{flavor:?}:当前项标记的容器必须是 SecondaryContainer(与下载组选中 chip 同族),\ + 不是禁用态的 12% 灰" + ); + assert_eq!( + paint.label, + ui.color(Role::OnSecondaryContainer), + "{flavor:?}:当前项标记的字色必须是 OnSecondaryContainer,不是 OnSurface@38%" + ); + } +} + +// ------------------------------------------------- ② 同族:两页表达「选中」只用一套颜色 + +/// Java 的「当前默认」与下载组的选中 chip **必须逐字段同色**。 +/// +/// 下载组那一侧(M201 已验收)是 `Chip::filter(option, selected=true, msg)`;这里照原样构造一颗 +/// ——**不是**照着角色名手抄一遍,所以谁把其中一页换套配色,这条就红。 +/// +/// 变异证明:把 `Chip::current` 的容器改成 `PrimaryContainer`(或任何另一个角色),红。 +#[test] +fn the_current_default_marker_speaks_the_download_groups_language() { + let catalog = Lang::ZhCn.catalog(); + let state = seeded(); + + for flavor in FLAVORS { + let ui = ui(flavor); + let marker_row = state + .runtimes + .iter() + .find(|runtime| super::super::state::same_path(&runtime.binary, &state.selected)) + .expect("种子里必须有一行是当前默认"); + let marker = action_chip(catalog, &state, marker_row).paint(&ui); + + // 下载组的分段单选:选中的那颗就是 `filter(.., true, ..)`。 + let download_selected = Chip::<()>::filter("8", true, ()); + assert_eq!( + marker, + download_selected.paint(&ui), + "{flavor:?}:两页的「选中」必须是同一套视觉语汇(M206)" + ); + + // 反面:另一行(非默认)是可点的「设为默认」——未选中 filter,且**不是**禁用灰。 + let other = state + .runtimes + .iter() + .find(|runtime| !super::super::state::same_path(&runtime.binary, &state.selected)) + .expect("种子里必须有一行不是默认"); + let actionable = action_chip(catalog, &state, other).paint(&ui); + assert_eq!( + actionable.label, + ui.color(Role::OnSurfaceVariant), + "{flavor:?}:「设为默认」是可点的未选中 filter(OnSurfaceVariant),不是禁用" + ); + assert_ne!( + actionable.frame.fill, marker.frame.fill, + "{flavor:?}:可点与当前项必须分得开(一个透明描边 / 一个填充)" + ); + } +} + +// ------------------------------------------------- ③ 反向:禁用态仍然要看起来禁用 + +/// 修当前项**不许**顺手把禁用语义改亮:`Chip::badge` / `.disabled()` 仍走 +/// 「字 `OnSurface`@38% + 容器 `OnSurface`@12%」,且与当前项标记**肉眼可分的两档**。 +/// +/// 变异证明:把 `Chip::paint` 里「没消息 = 禁用」那条改成走选中配色(或把 `badge` 改成 +/// `marker: true`),这条红——那会让「下载进行中,安装按钮灰掉」看起来像可点。 +#[test] +fn the_disabled_chip_still_looks_disabled() { + let ui_light = ui(SchemeFlavor::Light); + let ui_dark = ui(SchemeFlavor::Dark); + + for ui in [&ui_light, &ui_dark] { + let flavor = ui.flavor(); + // 下载组里「忙 ⇒ 灰掉」的那颗与 Java 的「默认」标记在语义上相反,必须画得不一样。 + let disabled = Chip::<()>::filter("安装", true, ()).disabled().paint(ui); + let marker = Chip::<()>::current("默认").paint(ui); + + let ink38 = interaction::disabled_content_over_role(ui, Role::OnSurface, false); + let container12 = interaction::disabled_colors(ui, Role::Surface).0; + + assert_eq!( + disabled.label, ink38, + "{flavor:?}:禁用字仍是 OnSurface@38%" + ); + assert_eq!( + disabled.frame.fill, + Some(container12), + "{flavor:?}:禁用容器仍是 OnSurface@12%(选中禁用的填充走 38%,见 selection::tests)" + ); + assert_ne!( + disabled.label, marker.label, + "{flavor:?}:禁用与当前项的文字色不许相同" + ); + assert_ne!( + disabled.frame.fill, marker.frame.fill, + "{flavor:?}:禁用与当前项的容器色不许相同" + ); + + // 禁用档**不**承诺 4.5:1(它是「用不了」的语汇,官方 α 就是 38%):这条只记数值, + // 不当门槛——否则将来有人真去调亮禁用态时会被一条假门禁拦住。 + let ratio = ink_ratio(&disabled, ui); + assert!( + ratio < AA_TEXT, + "{flavor:?}:禁用档意外地过 AA 了({ratio:.2}:1)——若是有意调亮,请同步改这条注释与 \ + `interaction::disabled` 的令牌口径,别让「禁用」和「当前项」两档混成一个" + ); + } + + // `Chip::badge` 仍是禁用档(它就是「没消息 ⇒ 禁用」那条路的产物)。 + let badge = Chip::<()>::badge("LTS").paint(&ui_light); + let disabled = Chip::<()>::filter("LTS", true, ()) + .disabled() + .paint(&ui_light); + assert_eq!(badge, disabled, "`Chip::badge` 与 `.disabled()` 必须同色"); +} diff --git a/crates/app-iced/src/pages/settings/java/view.rs b/crates/app-iced/src/pages/settings/java/view.rs index 978c3a4..612912e 100644 --- a/crates/app-iced/src/pages/settings/java/view.rs +++ b/crates/app-iced/src/pages/settings/java/view.rs @@ -227,23 +227,7 @@ fn runtime_row<'a>( None => (String::new(), ui.color(Role::OnSurfaceVariant)), }; - let action = if is_default { - chip( - ui, - // 已选中那颗是**纯展示**的 badge(没有消息,不可交互):r2 复审要求清掉 - // 「禁用态 chip 上还挂着一条真消息」的残留。 - Chip::badge(catalog.text(Key::SettingsJavaDefault)), - ) - } else { - chip( - ui, - Chip::filter( - catalog.text(Key::SettingsJavaSetDefault), - false, - msg(JavaMessage::SelectRuntime(runtime.binary.clone())), - ), - ) - }; + let action = chip(ui, action_chip(catalog, state, runtime)); let mut block = column![row![ container( @@ -290,3 +274,28 @@ fn runtime_row<'a>( } block.into() } + +/// 一行右侧的动作位(M206 把它抽成纯函数,好让断言直接对着它量对比度): +/// +/// - **当前默认**那一行 → [`Chip::current`]:与下载组选中 chip **同一套颜色** +/// (`SecondaryContainer` + `OnSecondaryContainer`),但没有消息、不吃五态; +/// - 其余 → 可点的 [`Chip::filter`]`(…, false, …)`「设为默认」。 +/// +/// 为什么不用 `Chip::badge` 表示当前默认:`badge` 走的是**禁用**配色(`OnSurface`@38% 叠 surface, +/// M204 实测 **1.84:1 浅 / 2.28:1 深**)。把「暂不可用」的语汇贴在「哪个 JDK 是默认」这个 +/// **答案本身**上,就是 M206 要修的那件事。档位判据表见 `components::selection::chip` 的模块文档。 +pub(super) fn action_chip<'a>( + catalog: Catalog, + state: &State, + runtime: &'a RuntimeRow, +) -> Chip<'a, Message> { + if super::state::same_path(&runtime.binary, &state.selected) { + Chip::current(catalog.text(Key::SettingsJavaDefault)) + } else { + Chip::filter( + catalog.text(Key::SettingsJavaSetDefault), + false, + msg(JavaMessage::SelectRuntime(runtime.binary.clone())), + ) + } +}