用户实测四条抱怨逐条对应: - 「没有自动检测本机 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`(不再挂一条永远收不到的消息)
204 lines
7.4 KiB
Rust
204 lines
7.4 KiB
Rust
//! 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(),
|
||
}
|
||
}
|