fix(app-iced): M157 r2 详情列加滚动区 + 启动参数卡对齐信息卡(D30 复审)

- 详情列(含卡片与动作行)套 scrollable:1180×1000 下被裁的内容可达、滚动条与左列同一套几何;右侧 X2 内缩,卡片描边不再被滚动条压住
- 启动参数卡改用 frame::card(card_padding 21 + OutlinedCard 描边),标题 LabelMedium 不加粗——与相邻信息卡同款
- 段间距 X3→X2,卡片不再比内容长
- 演示夹具补 Game arguments(--fullscreen)
- presenter host 单测临时目录加序号(同一进程并行用例互删导致 flake)
This commit is contained in:
AskaEth committed 2026-11-07 10:36:35 +08:00
1 parent e2021645d7
commit c76a1437e6
5 files changed
+56 -23

No files matched your search

+1 -1
View File
@@ -103,7 +103,7 @@ fn demo_view() -> LaunchArgsView {
LaunchArgsView { LaunchArgsView {
instance_id: DEMO[SELECTED].name.to_string(), instance_id: DEMO[SELECTED].name.to_string(),
jvm_args: "-XX:+UseG1GC".to_string(), jvm_args: "-XX:+UseG1GC".to_string(),
game_args: String::new(), game_args: "--fullscreen".to_string(),
memory_mb: (!gated).then_some(4096), memory_mb: (!gated).then_some(4096),
memory_effective_mb: if gated { 2048 } else { 4096 }, memory_effective_mb: if gated { 2048 } else { 4096 },
java_path: String::new(), java_path: String::new(),
+33 -5
View File
@@ -4,8 +4,8 @@
//! → 一条分隔线 → 正文段 / 运行状态行 → 动作行(Duplicate / Open folder / Delete)。 //! → 一条分隔线 → 正文段 / 运行状态行 → 动作行(Duplicate / Open folder / Delete)。
//! 窄窗(<840)时列表退成上一屏,本文件只画详情 + 一个「返回」按钮。 //! 窄窗(<840)时列表退成上一屏,本文件只画详情 + 一个「返回」按钮。
use iced::widget::{column, container, row}; use iced::widget::{column, container, row, scrollable};
use iced::{Alignment, Element, Length}; use iced::{Alignment, Element, Length, Padding};
use unlucky_design::color::Role; use unlucky_design::color::Role;
use unlucky_design::spacing::Space; use unlucky_design::spacing::Space;
@@ -48,8 +48,13 @@ pub(super) fn detail_column<'a>(
// M157:启动参数区在正文段之后、动作行之前(详情列 424px 内,卡片式)。 // M157:启动参数区在正文段之后、动作行之前(详情列 424px 内,卡片式)。
body = body.push(super::launch_args::section(ui, &detail.launch, form, texts)); body = body.push(super::launch_args::section(ui, &detail.launch, form, texts));
// 页头 + 元信息网格 + **一条**分隔线以下才是正文与操作区(M84 那两条线是占位时期的排版残留) // 页头 + 元信息网格 + **一条**分隔线以下才是正文与操作区(M84 那两条线是占位时期的排版残留)。
frame::left_aligned(metrics.column, body.push(action_row(ui, forced, texts))) //
// M157 r2(D30 复审):详情列的内容(正文段 + 启动参数卡 + 动作行)在 1180×1000 下**超出一屏**,
// 而这里原来没有滚动区 ⇒ Max memory 以下的控件被静默裁掉、够不着。现在整列套一个滚动区
// (与左列列表同一套滚动条样式与宽度),被裁内容可达;滚动条是唯一入口,没有第二份状态。
let content = body.push(action_row(ui, forced, texts));
frame::left_aligned(metrics.column, scroller(ui, content))
} }
pub(super) fn narrow_detail<'a>( pub(super) fn narrow_detail<'a>(
@@ -89,7 +94,30 @@ pub(super) fn narrow_detail<'a>(
ui.color(Role::OnSurfaceVariant), ui.color(Role::OnSurfaceVariant),
)); ));
frame::body(metrics, body.push(action_row(ui, forced, texts))) frame::body(
metrics,
scroller(ui, body.push(action_row(ui, forced, texts))),
)
}
/// 详情列的滚动区(M157 r2):与左列列表同一套滚动条几何(宽 6 / 内缩 2 / 同一 `style`)。
///
/// 右侧留 `Space::X2` 内缩:滚动条是**覆盖**在内容上的,不留这条缝时卡片的 1dp 描边会被
/// 滚动条压住(D30 复审里「卡片右边框缺一截」就是它)。
fn scroller<'a>(ui: &'a Ui, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
let content = container(content)
.width(Length::Fill)
.padding(Padding::default().right(ui.space(Space::X2)));
scrollable(content)
.direction(scrollable::Direction::Vertical(
scrollable::Scrollbar::new()
.width(frame::SCROLLBAR_WIDTH)
.scroller_width(frame::SCROLLBAR_WIDTH)
.margin(frame::SCROLLBAR_MARGIN),
))
.height(Length::Fill)
.style(frame::scroll_style(ui))
.into()
} }
/// 正文段(演示数据才有)与运行状态行(真数据才有)——两者都是可选的,顺序固定。 /// 正文段(演示数据才有)与运行状态行(真数据才有)——两者都是可选的,顺序固定。
@@ -15,12 +15,10 @@
//! //!
//! 文案全走 [`Texts`](守门见 `instances/tests/i18n.rs` 的白名单与死 key 扫描)。 //! 文案全走 [`Texts`](守门见 `instances/tests/i18n.rs` 的白名单与死 key 扫描)。
use iced::widget::{column, container, row}; use iced::widget::{column, row};
use iced::{Background, Element, Length}; use iced::{Element, Length};
use unlucky_design::color::Role; use unlucky_design::color::Role;
use unlucky_design::elevation::MaterialLayer;
use unlucky_design::shape::ShapeScale;
use unlucky_design::spacing::Space; use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole; use unlucky_design::typography::TypeRole;
@@ -47,8 +45,10 @@ pub(super) fn section<'a>(
form: &'a LaunchForm, form: &'a LaunchForm,
texts: Texts, texts: Texts,
) -> Element<'a, PageMessage> { ) -> Element<'a, PageMessage> {
// 段间距 X2(8dp):卡内 5 个字段 + 三态 + 动作行在 424 宽下已经很长,
// 用 X3 会让详情列更长一屏;行内间距仍是 X1(4dp),读起来不挤。
let mut body = column![frame_title(ui, texts.launch_title())] let mut body = column![frame_title(ui, texts.launch_title())]
.spacing(ui.space(Space::X3)) .spacing(ui.space(Space::X2))
.width(Length::Fill); .width(Length::Fill);
body = body.push(args_field( body = body.push(args_field(
@@ -94,22 +94,17 @@ pub(super) fn section<'a>(
body = body.push(backend_row(ui, facts, form, texts)); body = body.push(backend_row(ui, facts, form, texts));
body = body.push(actions_row(ui, texts)); body = body.push(actions_row(ui, texts));
container(body) // 与相邻信息卡**同一套**卡壳(`frame::card`:`card_padding` 21dp + OutlinedCard 的圆角/描边)。
.width(Length::Fill) super::super::frame::card(ui, body)
.padding(ui.space(Space::X3))
.style(move |_theme| container::Style {
background: Some(Background::Color(ui.layer(MaterialLayer::Card))),
border: ui.outlined(ShapeScale::Medium),
..container::Style::default()
})
.into()
} }
/// 卡片标题:**与信息卡的标签同款**(`LabelMedium` 不加粗、`OnSurfaceVariant`),
/// 复审 ③⑤ 抓的就是这里(原来用了 `emphasized = true`,比同屏其它标签重一档)。
fn frame_title<'a>(ui: &'a Ui, title: &'a str) -> Element<'a, PageMessage> { fn frame_title<'a>(ui: &'a Ui, title: &'a str) -> Element<'a, PageMessage> {
super::super::frame::text_role( super::super::frame::text_role(
ui, ui,
TypeRole::LabelMedium, TypeRole::LabelMedium,
true, false,
title, title,
ui.color(Role::OnSurfaceVariant), ui.color(Role::OnSurfaceVariant),
) )
@@ -61,7 +61,7 @@ const PAGE_SOURCES: [(&str, &str); 6] = [
/// | `isolated` / `shared` | core 的隔离档位**稳定 id**(`IsolationMode::as_str`),`isolation()` 只认这两档,其余原样上屏 | /// | `isolated` / `shared` | core 的隔离档位**稳定 id**(`IsolationMode::as_str`),`isolation()` 只认这两档,其余原样上屏 |
/// | `pid` / `reason` | `Catalog::render` 的**占位符名**(与 catalog 里的 `{pid}` / `{reason}` 对应) | /// | `pid` / `reason` | `Catalog::render` 的**占位符名**(与 catalog 里的 `{pid}` / `{reason}` 对应) |
/// | `Instances` | `Texts::keys()` 的**族名前缀**(`Key::ALL` 的过滤条件,不是文案) | /// | `Instances` | `Texts::keys()` 的**族名前缀**(`Key::ALL` 的过滤条件,不是文案) |
const ALLOWED_LITERALS: [&str; 17] = [ const ALLOWED_LITERALS: [&str; 18] = [
"{date} {time} UTC", "{date} {time} UTC",
"isolated", "isolated",
"shared", "shared",
@@ -79,6 +79,8 @@ const ALLOWED_LITERALS: [&str; 17] = [
// M157:`instances/demo.rs` 的**出图夹具**(用户数据的形状,不是界面文案): // M157:`instances/demo.rs` 的**出图夹具**(用户数据的形状,不是界面文案):
// 一个示例 JVM 参数、两个示例 MC 版本、一个示例 Java 路径,以及夹具开关的环境变量名与取值。 // 一个示例 JVM 参数、两个示例 MC 版本、一个示例 Java 路径,以及夹具开关的环境变量名与取值。
"-XX:+UseG1GC", "-XX:+UseG1GC",
// M157 r2:示例游戏参数(同上,是夹具不是文案)。
"--fullscreen",
"26.2-snapshot-2", "26.2-snapshot-2",
"1.20.4", "1.20.4",
"C:/Java/temurin-21/bin/java.exe", "C:/Java/temurin-21/bin/java.exe",
+9 -1
View File
@@ -203,6 +203,7 @@ mod tests {
/// 最小的 tempdir(与本 crate 其它 host 测试同款:不引第三方 tempfile)。 /// 最小的 tempdir(与本 crate 其它 host 测试同款:不引第三方 tempfile)。
mod tempdir { mod tempdir {
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
pub struct Dir { pub struct Dir {
pub path: PathBuf, pub path: PathBuf,
@@ -210,7 +211,14 @@ mod tests {
impl Dir { impl Dir {
pub fn new(tag: &str) -> Self { pub fn new(tag: &str) -> Self {
let path = std::env::temp_dir().join(format!("{tag}-{}", std::process::id())); // 每个用例一个目录:同一进程里跑多个用例时,只用 PID 命名会互相 `remove_dir_all`
// (并行跑就是「系统找不到指定的路径」——M157 r2 实测)。
static COUNTER: AtomicU64 = AtomicU64::new(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"{tag}-{}-{seq}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&path); let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).expect("建临时目录"); std::fs::create_dir_all(&path).expect("建临时目录");
Self { path } Self { path }