Files
Unlucky-Launcher/crates/app-iced/src/components/gallery/inputs.rs
T
AskaEth d616d65ea1
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Release / 选槽位(默认两个 Windows) (push) Waiting to run
Unlucky Launcher v0.2.3
2026-11-08 00:15:58 +00:00

342 lines
12 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.
//! gallery 里的输入类小节(D30 的状态矩阵登记):TextField / Search / Slider / Menu。
//!
//! ## 为什么分成四个 `part`
//!
//! 一整段(四组件 × 五态 × 两式)比 760 高的窗口还高,`--screen=gallery` 的截图只能看到顶部。
//! 所以本文件把证据拆成四个自足的 `part`([`text_field_part`] / [`search_part`] /
//! [`slider_part`] / [`menu_part`]),[`section`] 按 D30 的顺序把它们竖排——
//! 出图时用副本工程把 `section()` 换成单个 `part`(只调段序、不改组件代码,报告里有 diff 与说明)。
//!
//! ## 矩阵形状
//!
//! ```text
//! TextField(两行 × 五态) 列 = enabled / hovered / pressed / focused / disabled
//! filled | [字段] | ...
//! outlined | [字段] | ...
//! 另加三格:空 / 有值 / 错误(9.7 的判据要单独一眼)
//!
//! Search (五行 + 一个展开面板)
//! Slider (五态一行 + 0% / 50% / 100% 三行 + 离散 4 段一行)
//! Menu (五态各一项的一只菜单 + 一只「展开态」菜单:选中 / 分隔线 / 禁用)
//! ```
//!
//! ## 每个格子都强制状态
//!
//! 与按钮家族同一个理由:截图时没法让 20 个控件同时 hover。`state(..)` 只是取证开关,
//! 不动任何状态机——图上的五态与运行期五态同源。
use iced::widget::{column, container, row, text};
use iced::{Element, Length};
use unlucky_design::color::Role;
use unlucky_design::icons::IconKind;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use crate::theme::Ui;
use super::super::inputs::{
self, Field, FieldVariant, Menu, MenuItem, SearchBar, SearchView, Slider,
};
use super::super::interaction::Interaction;
use super::{cell_width, heading, row_label, state_cells, DemoMessage};
/// 演示消息(组件只认泛型,gallery 的映射在这里落地)。
fn pressed(id: &'static str) -> DemoMessage {
DemoMessage::Pressed(id)
}
/// 输入类家族整段(四个 part 竖排;见模块文档的「为什么分成四个 part」)。
pub(super) fn section<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
column![
heading(
ui,
"inputs · text field / search / slider / menu",
"12b §9/§11/§8 + 15 §20:TextField 高 56(无标签 40)· Search 条 56 药丸 + 图标可视边 16 \
(= pad 12 + offset 4)· Slider 轨 16 + 竖条 4×44 · Menu 项 48 + L2 阴影"
),
text_field_part(ui),
search_part(ui),
slider_part(ui),
menu_part(ui),
]
.spacing(ui.space(Space::X8))
.into()
}
/// 一行小字(图例/说明)。
fn note<'a>(ui: &'a Ui, value: &'a str) -> Element<'a, DemoMessage> {
let style = ui.style(TypeRole::BodySmall);
text(value)
.size(style.size_px)
.font(ui.font_of(style))
.color(ui.color(Role::OnSurfaceVariant))
.into()
}
/// 固定宽的矩阵格。
fn cell<'a>(ui: &'a Ui, content: impl Into<Element<'a, DemoMessage>>) -> Element<'a, DemoMessage> {
container(content)
.width(Length::Fixed(cell_width(ui)))
.into()
}
/// 五态列头。
fn state_header<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
let style = ui.style(TypeRole::LabelSmall);
let mut head = row![row_label(ui, "")].spacing(ui.space(Space::X2));
for (name, width) in state_cells(ui) {
head = head.push(
container(
text(name)
.size(style.size_px)
.font(ui.font_of(style))
.color(ui.color(Role::OnSurfaceVariant)),
)
.width(Length::Fixed(width)),
);
}
head.into()
}
/// 一行矩阵(左侧标签 + 五格)。
fn state_row<'a>(
ui: &'a Ui,
label: &'a str,
cells: Vec<Element<'a, DemoMessage>>,
) -> Element<'a, DemoMessage> {
row![
row_label(ui, label),
row(cells).spacing(ui.space(Space::X2))
]
.spacing(ui.space(Space::X2))
.into()
}
/// TextField:两式 × 五态 + 空 / 有值 / 错误 三格。
fn text_field_part<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
let mut matrix = column![state_header(ui)].spacing(ui.space(Space::X2));
for variant in FieldVariant::ALL {
let cells = Interaction::ALL
.iter()
.map(|state| {
cell(
ui,
inputs::text_field(
ui,
Field::new("实例一")
.variant(variant)
.label("名称")
.state(*state),
|_| pressed("field"),
),
)
})
.collect::<Vec<_>>();
matrix = matrix.push(state_row(ui, variant.name(), cells));
}
// 三格用 **320** 宽(= 2 个矩阵格):9.2 的最小宽 280 只在这里可验(矩阵格 160 更窄)
let wide = Length::Fixed(cell_width(ui) * 2.0);
let extras = row![
inputs::text_field(
ui,
Field::new("")
.label("名称")
.placeholder("请输入名称")
.width(wide),
|_| pressed("field"),
),
inputs::text_field(
ui,
Field::new("1.21.4")
.label("名称")
.leading(IconKind::Folder)
.width(wide),
|_| pressed("field"),
),
inputs::text_field(
ui,
Field::new("")
.variant(FieldVariant::Outlined)
.label("名称")
.placeholder("必填")
.trailing(IconKind::Error)
.supporting("该名称已被占用")
.error()
.width(wide),
|_| pressed("field"),
),
]
.spacing(ui.space(Space::X2));
column![
note(
ui,
"列 = 五态 · 行 = 两式。静息指示线/描边 1px、聚焦与错误 2px(A1 FocusOutlineWidth);\
错误态线/标签/辅助文本全 Error"
),
matrix,
note(
ui,
"另加:空(占位 OnSurfaceVariant)/ 有值(带前置图标 24)/ 错误(2px + 辅助文本)"
),
extras,
]
.spacing(ui.space(Space::X2))
.into()
}
/// Search:五态 bar 竖排 + 一个展开的 docked 面板。
fn search_part<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
let bar_width = ui.space(Space::X8) * 12.0;
let mut bars = column![].spacing(ui.space(Space::X2));
for state in Interaction::ALL {
let bar = inputs::search_bar(
ui,
SearchBar::new("")
.placeholder("搜索实例 / 版本 / mod")
.state(state)
.width(Length::Fixed(bar_width)),
|_| pressed("search"),
);
bars = bars.push(
row![row_label(ui, state.name()), bar]
.spacing(ui.space(Space::X2))
.align_y(iced::Alignment::Center),
);
}
let results: &[&str] = &[
"实例:原版 1.21.4",
"版本:Fabric 0.16.9",
"mod:Sodium 0.6.5",
];
let view = inputs::search_view(
ui,
SearchView::new("1.21", results, "没有匹配的实例").placeholder("搜索实例 / 版本 / mod"),
|_| pressed("search"),
);
column![
note(
ui,
"bar:高 56 · 圆角 full(28) · SurfaceContainerHigh · 图标可视边 16 = pad 12 + offset 4(M69 不变量)· \
聚焦 1px Secondary;view:docked,头部 56 + 1px Outline 分隔 + 结果区最小高 240"
),
row![bars, view].spacing(ui.space(Space::X6)),
]
.spacing(ui.space(Space::X2))
.into()
}
/// Slider:五态一行 + 0%/50%/100% 三行 + 离散 4 段一行。
fn slider_part<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
let cells = Interaction::ALL
.iter()
.map(|state| {
let enabled = !state.is_disabled();
let mut spec = Slider::new(ui, 0.5).state(*state);
if enabled {
spec = spec.on_change(|_| pressed("slider"));
}
cell(ui, inputs::slider(spec))
})
.collect::<Vec<_>>();
let track_width = ui.space(Space::X8) * 14.0;
let mut values = column![].spacing(ui.space(Space::X2));
for (label, fraction) in [("0%", 0.0), ("50%", 0.5), ("100%", 1.0)] {
let spec = Slider::new(ui, fraction)
.on_change(|_| pressed("slider"))
.state(Interaction::Enabled);
values = values.push(
row![
row_label(ui, label),
container(inputs::slider(spec)).width(Length::Fixed(track_width)),
]
.spacing(ui.space(Space::X2))
.align_y(iced::Alignment::Center),
);
}
// 离散:4 段 = 3 个内部停靠点(25 / 50 / 75%),当前 25% ⇒ 能看到 50% 与 75% 两个点
let discrete = row![
row_label(ui, "离散 4"),
container(inputs::slider(
Slider::new(ui, 0.25)
.steps(4)
.on_change(|_| pressed("slider"))
.state(Interaction::Enabled)
))
.width(Length::Fixed(track_width)),
]
.spacing(ui.space(Space::X2))
.align_y(iced::Alignment::Center);
column![
note(
ui,
"轨 16 full(Primary / SecondaryContainer)· 竖条 4×44 full(overflow 上下各 14)· \
focus/pressed 收窄到 2 · 命中 48 · 离散停靠点 4"
),
state_row(ui, "五态", cells),
note(ui, "0% / 50% / 100%:竖条在两端贴边但不越界(左缘 = 轨道左缘,右缘 = 轨道右缘)"),
values,
note(
ui,
"离散:steps(4) ⇒ 3 个内部停靠点(25/50/75%),未激活轨在停靠点两侧留 6 空隙,\n 激活段只画到竖条中心",
),
discrete,
]
.spacing(ui.space(Space::X2))
.into()
}
/// Menu:一只五态菜单 + 一只展开态菜单(选中 / 分组线 / 禁用)。
fn menu_part<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
let width = ui.space(Space::X8) * 8.0;
let mut states = Menu::new().width(Length::Fixed(width));
for state in Interaction::ALL {
let mut item = MenuItem::new("重命名", pressed("menu")).state(state);
if state.is_disabled() {
item = MenuItem::disabled("重命名").state(state);
}
states = states.item(item.leading(IconKind::Folder));
}
let open = Menu::new()
.width(Length::Fixed(width))
.item(MenuItem::new("重命名", pressed("menu")).leading(IconKind::Folder))
.item(
MenuItem::new("复制实例", pressed("menu"))
.leading(IconKind::Download)
.trailing(IconKind::Check)
.selected(),
)
.divider()
.item(MenuItem::disabled("删除").leading(IconKind::Trash));
column![
note(
ui,
"容器 SurfaceContainer + L2 阴影(blur 6 / offset 2 / Shadow 角色)· 圆角 4(令牌表)· \
项高 48 · 水平 12 · 上下留白 8 · 选中项 SecondaryContainer 指示器内缩 4、圆角 8"
),
row![
column![note(ui, "五态(每项强制一态)"), inputs::menu(ui, states)]
.spacing(ui.space(Space::X2)),
column![
note(ui, "展开:含选中项 / 分组分隔线(复用 divider)/ 不可点项"),
inputs::menu(ui, open)
]
.spacing(ui.space(Space::X2)),
]
.spacing(ui.space(Space::X6)),
]
.spacing(ui.space(Space::X2))
.into()
}