Files
Unlucky-Launcher/crates/app-iced/src/components/selection/chip.rs
T
AskaEth df331f6a60 feat(m175): Java 设置页接真运行时(扫描 / 手动路径 / 默认 JDK / 内存 / JVM 参数)
用户实测四条抱怨逐条对应:
- 「没有自动检测本机 Java」:首屏自动扫一次盘(托管根 + JAVA_HOME + PATH + 标准安装位置 +
  注册表),之后由「立即扫描」手动触发;扫描在后台线程里跑,带 done/total 进度与取消
- 「显示 Temurin 21.0.4 不知道是不是硬编码」:演示行与绊线测试一起删掉,列表来自真读盘
- 「没一个能改」:默认内存(MB,512~65536,范围与 schema 同源)与 JVM 参数可真改、真落盘、读回
- 「不能手动指定 Java 路径 / 不能改默认版本」:手动添加走严格校验(真跑 java -version),
  设为默认写 java.selected_path 并给出「为什么选它」(当前实例的版本门槛 + 每行满足与否)

core:java::manual(严格探测 + 厂商识别 + 名单持久化)、java::staged(候选收集 + 带进度与
取消的扫描)、java::proc(带超时且可打断的子进程执行:卡住的 java -version 会被 kill)。
i18n:catalog/settings/java.rs 55 条中英 + 金表按装配顺序插入。

复审 r1(p2-5items)全修:
- P2-1 「设为默认」不再清脏:只写 java.selected_path,内存/JVM 草稿一个字节都不动
- P2-2 错误文案全部走 i18n(三档「写不进去」/「路径太长」/「名单写不进去」各自一个 key),
  不再把 Rust 枚举名当人话
- P2-3 手动名单落盘有正面断言(加一条 → 从盘上新读一份 → 在)
- P2-4 版本号显示到小版本(17.0.13 / 17.0.9 可区分),解析不出来时给原始版本串
- P2-5 每候选 8 秒超时并 kill、取消打断正在跑的探测、扫描槽挂在 State 上、线程活着期间
  不许起第二个(界面显示「正在取消…」)

复审 r2(p2-2items)全修:
- P2-1 空态不再指向不存在的「Java 页」,改成真实存在的 `ull java install 21` 命令行入口
- P2-2 `auto_scan_due` 增加 `!report.cancelled`:取消过一次之后本进程不再自动扫,
  配置文件再变(存内存 / 改主题)也不会把取消无声撤销;手动「立即扫描」不受影响
- 顺带:proc 的 kill+wait 补「子进程真死了」的回归守卫(标记文件从未出现)、
  core 的 Schema 错误改成页面自己的 key(不再把中文 Display 直出给英文界面)、
  默认态 chip 用新的 `Chip::badge`(不再挂一条永远收不到的消息)
2026-11-08 19:52:04 +08:00

204 lines
7.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Chip(`research/12b` §13):高 **32** / 圆角 **8** / 水平内边距 **8** / 元素间距 **8** /
//! 图标 **18**(`IconSize::Button` = 官方 `AssistChipTokens.IconSize`)/ 描边 1px `OutlineVariant`。
//!
//! ## 三类(§13 的 Assist / Filter / Input)
//!
//! | 类 | 选中态 | 容器 | 文字 | 前置图标 |
//! |---|---|---|---|---|
//! | Assist | **无**(一次动作,不是开关) | 透明 + 1px 描边 | `OnSurface` | `Primary` |
//! | Filter | 有 | 未选透明+描边;选中 `SecondaryContainer` **无描边** | `OnSurfaceVariant` / `OnSecondaryContainer` | 选中态才给勾 |
//! | Input | 有 | 同 Filter | 同 Filter | 可给 + 后置关闭图标 |
//!
//! ## 为什么 Chip 不用 `selection::shell` 那条透明外壳
//!
//! M3 的 chip **没有圆形状态层**:hover / pressed 就是把状态层叠在**自己的容器**上,
//! 而容器尺寸 = 命中区尺寸。所以 chip 直接走 `button` 的样式闭包(与按钮家族同一条产线),
//! 不需要「48 外壳 + 40 圆」那套。
use iced::widget::{button, container, row, text};
use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow};
use unlucky_design::icons::IconKind;
use unlucky_design::typography::TypeRole;
use crate::icons;
use crate::theme::Ui;
use super::super::interaction::Interaction;
use super::geometry::ChipGeometry;
use super::paint::{self, ChipVariant};
/// 一个 chip(可带前后图标;`on_press` 为 `None` = 禁用)。
#[derive(Debug, Clone)]
pub struct Chip<'a, M> {
label: &'a str,
variant: ChipVariant,
selected: bool,
leading: Option<IconKind>,
trailing: Option<IconKind>,
on_press: Option<M>,
state: Option<Interaction>,
}
impl<'a, M> Chip<'a, M> {
/// Assist chip:辅助动作,**没有**选中态。
pub fn assist(label: &'a str, on_press: M) -> Self {
Self::new(label, ChipVariant::Assist, false, on_press)
}
/// Filter chip:筛选开关(`selected` 决定容器是否 `SecondaryContainer`)。
pub fn filter(label: &'a str, selected: bool, on_press: M) -> Self {
Self::new(label, ChipVariant::Filter, selected, on_press)
}
/// Input chip:用户输入的实体(可选中 + 可带关闭图标)。
pub fn input(label: &'a str, on_press: M) -> Self {
Self::new(label, ChipVariant::Input, false, on_press)
}
/// **纯展示**的 filter chip(选中态、无消息 ⇒ 永远不可交互)。
///
/// 与 `filter(label, true, msg).disabled()` 等价,但**不要求调用方编一条永远收不到的消息**:
/// 本轮 Java 页那颗「默认」标记就是这么用的(`Message::Noop` 已删,别再拿一条真消息当占位)。
pub fn badge(label: &'a str) -> Self {
Self {
label,
variant: ChipVariant::Filter,
selected: true,
leading: None,
trailing: None,
on_press: None,
state: None,
}
}
/// 转成禁用:**只清消息**,图标 / 选中态 / 状态层全部保留(复审 P2-1)。
///
/// 官方禁用态是「容器/描边 12%、文字与**图标** 38%」——只降**不删**。早先的
/// `Chip::disabled(label, variant)` 把 leading / trailing / selected 一起丢掉,实测
/// assist 78→54、filter·on 74→50(连填充和勾都没了),禁用列里 `filter·on` 与 `filter`
/// 逐像素同形,看不出选中与否。
pub fn disabled(mut self) -> Self {
self.on_press = None;
self
}
fn new(label: &'a str, variant: ChipVariant, selected: bool, on_press: M) -> Self {
Self {
label,
variant,
selected,
leading: None,
trailing: None,
on_press: Some(on_press),
state: None,
}
}
/// 前置图标(Assist / Filter 用)。
pub fn leading(mut self, kind: IconKind) -> Self {
self.leading = Some(kind);
self
}
/// 后置图标(Input chip 的关闭键)。
pub fn trailing(mut self, kind: IconKind) -> Self {
self.trailing = Some(kind);
self
}
/// 强制视觉态(gallery 取证用;运行期不要调)。
pub fn state(mut self, state: Interaction) -> Self {
self.state = Some(state);
self
}
}
/// 画一个 chip。
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 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,
);
let mut content = row![].align_y(Alignment::Center).spacing(geometry.gap);
if let Some(kind) = spec.leading {
// Q9:图标一律经固定尺寸容器;盒 18 ≠ RENDER_PX(24),所以必须走 `view_at` 按 18 烘焙,
// 否则线宽会被布局缩水(复审 r1 P2-2)。
content = content.push(super::super::layout::centered_fixed(
geometry.icon,
geometry.icon,
icons::view_at::<M>(kind, at_rest.icon, geometry.icon),
));
}
let style = ui.style(TypeRole::LabelLarge);
content = content.push(
text(spec.label)
.size(style.size_px)
.font(ui.font_of(ui.emphasized(TypeRole::LabelLarge)))
.color(at_rest.label),
);
if let Some(kind) = spec.trailing {
content = content.push(super::super::layout::centered_fixed(
geometry.icon,
geometry.icon,
icons::view_at::<M>(kind, at_rest.icon, geometry.icon),
));
}
// 垂直居中必须在内容侧做(`button` 把内容放左上角)
let body = container(content)
.height(Length::Fill)
.align_y(Alignment::Center);
let element = button(body)
.height(Length::Fixed(geometry.height))
.padding(Padding {
top: 0.0,
right: geometry.pad,
bottom: 0.0,
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);
button::Style {
background: paint.frame.fill.map(Background::Color),
text_color: paint.label,
border: Border {
color: paint.frame.outline.unwrap_or(iced::Color::TRANSPARENT),
width: if paint.frame.outline.is_some() {
paint.frame.stroke
} else {
0.0
},
radius: iced::border::Radius::new(paint.frame.radius),
},
shadow: Shadow::default(),
snap: true,
}
});
match spec.on_press {
Some(message) => element.on_press(message).into(),
None => element.into(),
}
}