fix(app-iced): M95 r1 五条视觉复审(悬挂缩进 / 会话身份 / 摘要可见 / 贴底 / 描边)

- P2-1 **悬挂缩进真做了**(之前文档在骗人,代码里只有 `row![bar, text]`):
  每行拆「前缀 + 正文」两个文本控件(前缀 = 第一个空格及其之前,含空格;
  `split_prefix` 在第一个「词」>20 字符时不拆),正文带 Fill 宽度自己折行 ⇒
  续行落在前缀宽度之后。于是 `at com.mojang…(GLX.java:64)` 不再拆成「`at` 独占一行」。
  文档同步写清代价:续行缩进量 = 该行前缀宽度。
- P2-2 会话身份上页头:`Logs::with_status(lang)` 用 `LogsStatus` 模板拼
  `{instance} · {when} · {outcome}`,结局文案走 `LaunchOutcomeNormal/Crash/Silent`
  三 key(bridge 文档里那条「LogOutcome 三态 → 三 key」的结构化出口,此前 UI 侧零调用点)。
  中文模板差在全角竖线(守卫要求 zh≠en,也确实更符合中文排版)。
- P2-3 摘要钉成诊断卡**固定页脚**(不进滚动区):「One-line summary」+ Copy summary
  紧邻它要复制的那段等宽文本;卡片头不再放复制按钮(避免两处入口)。
- P2-4 日志卡改 `panel::scroll_tail`(`Anchor::End`):偏移按「距底多远」记,内容增长时
  跟随尾部;「用户上滚后是否冻结」的边界写在 `panel::scroll_tail` 文档里(结论:不冻结,
  冻结要 app 层回灌滚动状态,已登记)。
- P2-5 两张主卡补 1px 描边:`panel::card` 改用 `Component::OutlinedCard.spec().shape` +
  `ui.outlined(..)`,与 `frame::card` 同一套令牌。

测试:新增 `a_line_is_split_into_prefix_and_body_for_the_hanging_indent`(含 CJK 行与
「不拆」的两种边界)与 `status_line_names_the_session_with_the_launch_outcome_keys`;
key 清单补 `LogsStatus` 与三个 `LaunchOutcome*`;demo 的 state.build 改为收 `Lang`。

验证:`fmt --check` 通过;`clippy -p unlucky-app-iced --all-targets -D warnings` 干净;
`test -p unlucky-app-iced` 274+6 全绿、`-p unlucky-i18n` 23 全绿。
图:`.screens/logs-{empty,live,crash}-{light,dark}.png`(1180×760)、
`r2-live-tall-light.png`(1180×1080,栈帧可见,P2-1 证据)。
This commit is contained in:
AskaEth committed 2026-11-02 22:26:59 +08:00
1 parent 063a1dba39
commit 1bccf89e5c
10 files changed
+293 -93

No files matched your search

+55
View File
@@ -24,6 +24,9 @@
//! (`rules.d/*.yaml` 的 `cause.key: diagnose.cause.…`)。DTO 没带 `key`,页面就把规则原文
//! 当**数据**画出来,页面的界面文案(标题 / 按钮 / 分组名 / 空态)仍然全部走 i18n key。
use unlucky_i18n::catalog::{Catalog, Key};
use unlucky_i18n::Lang;
/// 一行日志的等级(presenter `LogLevel` 的展示投影)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Level {
@@ -148,6 +151,9 @@ pub struct Logs {
pub when: Option<String>,
/// 结局。
pub outcome: Option<Outcome>,
/// 页头副标题(`实例 · 时间 · 结局`):**构造时**拼好(见 [`Logs::with_status`]),
/// 页面只负责显示——i18n 的「一句话 = 一个 key」不许在渲染里拼句子。
pub status: Option<String>,
/// 日志行(顺序 = 文件顺序)。
pub lines: Vec<Line>,
/// 诊断结论(顺序 = presenter 给的顺序,页面不重排)。
@@ -155,6 +161,21 @@ pub struct Logs {
}
impl Logs {
/// 补上页头副标题(M95 r2,视觉复审 P2-2)。
///
/// 为什么在这里做而不是在页面里:`Header::subtitle` 要 `&'a str`,而拼出来的是 `String`;
/// 让模型持有它,页面就永远只借不拼。
#[must_use]
pub fn with_status(mut self, lang: Lang) -> Self {
self.status = status_line(
self.instance.as_deref(),
self.when.as_deref(),
self.outcome,
lang,
);
self
}
/// 没有任何可显示的东西(空态判据:行与诊断都空)。
#[must_use]
pub fn is_empty(&self) -> bool {
@@ -216,3 +237,37 @@ impl Logs {
/// 摘要行的版本头(改了格式就换代,别让旧 issue 里的摘要含义漂移)。
pub const SUMMARY_HEADER: &str = "ULL-LOGS v1";
/// 页头副标题:`{instance} · {when} · {outcome}`。
///
/// 结局文案走 [`Key::LaunchOutcomeNormal`] / [`Key::LaunchOutcomeCrash`] /
/// [`Key::LaunchOutcomeSilent`] —— 就是 `unlucky_i18n::bridge` 文档里写了很久的那条
/// 「`LogOutcome` 三态 → 三 key 结构化出口」,M95 r2 之前它们在 UI 侧**零调用点**(视觉复审 P2-2)。
///
/// 没有实例名或结局时返回 `None`(不知道是哪一场就别编),页头退回静态副标题。
#[must_use]
pub fn status_line(
instance: Option<&str>,
when: Option<&str>,
outcome: Option<Outcome>,
lang: Lang,
) -> Option<String> {
let instance = instance?;
let outcome = outcome?;
let catalog = Catalog::of(lang);
let outcome_key = match outcome {
Outcome::Ok => Key::LaunchOutcomeNormal,
Outcome::Crashed => Key::LaunchOutcomeCrash,
Outcome::SilentFail => Key::LaunchOutcomeSilent,
};
Some(catalog.render(
Key::LogsStatus,
&[
("instance", instance),
("when", when.unwrap_or("—")),
("outcome", catalog.text(outcome_key)),
],
))
}