feat(app-iced): 单行输入框透出 on_submit,回车接上存档改名与强调色提交

债务台账 P18(M250 + M247 的共同缺口):iced 0.14 的 text_input 有 on_submit、
没有 on_unfocus(builder 逐条核过:on_input/on_input_maybe/on_submit/on_submit_maybe/
on_paste/on_paste_maybe,on_unfocus 零命中),而本仓的 text_field 只透出
on_input ⇒ 行内重命名只能点按钮、强调色拿不到「用户确认」这一下。

- field.rs:Field 变 `Field<'a, M>`(M 只被新加的 on_submit 用到,调用点的类型由
  text_field 的 on_input 一并推出,一行都不用改),回车消息唯一从 submit_message()
  取,`on_submit_maybe(None)` 是默认档 ⇒ 既有调用点逐位不变;禁用态与 on_input 同一
  条规矩被摘掉。
- 存档改名:回车与「改名」按钮共用 commit_rename() 一个消息产地,空名/原名不变/
  非法字符那几道校验因此对回车同样成立。
- 强调色:回车发 Change::AccentCommit(不是又一次 Accent(原文))⇒ 立即 commit(),
  把「打了一半就放着」的 #12 亮成红框 + 错行;逐键不报短错的规矩不变,落盘仍取
  上一次合法值。
- 判据:submit_is_opt_in(未接就是 None)、text_field_forwards_the_submit_callback、
  submit_is_wired_only_at_the_two_call_sites(白名单扫源码)、
  enter_takes_the_same_message_as_the_rename_button、the_rename_field_wires_enter、
  the_accent_field_wires_enter_to_commit、enter_commits_a_half_typed_value_and_
  persists_only_legal_ones。真键盘回车量不到(Element 无内省、造不出 iced::Renderer
  的测试替身),报告里单列。
This commit is contained in:
AskaEth committed 2026-11-11 19:55:21 +08:00
1 parent 026b9f6e94
commit f7cfc8ded6
13 files changed
+384 -22

No files matched your search

@@ -91,6 +91,9 @@ mod wall;
#[cfg(test)]
pub(super) use model::clear_handles;
pub use model::{failure_line, from_state, Row, Saves, Shot, ShotPreview};
/// 「改名」那条消息的产地(实现在 [`rows`] 里)——只给用例用,与 `op_line` 同一条口径。
#[cfg(test)]
pub(super) use rows::commit_rename;
/// 写操作成功那句话的产地(实现在 [`rows`] 里)。
///
/// 生产代码走 `model::from_state → rows::op_line`;这条 re-export 只给**用例**用
@@ -14,9 +14,10 @@
//! 三枚按钮与「打开存档文件夹」同一条口径:`buttons::text` + `frame::BASE`(M237:卡片里的控件
//! 把底传成卡底,hover/禁用状态才叠在卡实色上),出图取证时整行一起跟 `--force-state`。
//!
//! 输入框用 `components::inputs::text_field`(与启动参数区同一个控件)。**回车即提交**这件事
//! 由 `text_input` 的 `on_submit` 给不了(`Field` 没有开这个口子),所以提交靠右边的「改名」按钮
//! ——少一个键,但多一条谁都能看见的路。
//! 输入框用 `components::inputs::text_field`(与启动参数区同一个控件)。**回车 = 点「改名」**
//! (M252 / P18):`Field::on_submit` 与右边那枚按钮共用 [`commit_rename`] 这一个消息产地,
//! 所以空名 / 原名不变 / 非法字符走的仍是原来那条校验(`app/update/instances.rs` 的
//! 空名与「没改」两条短路 + presenter 那边的目录名规则),**回车没有绕开任何一道边界**。
use iced::widget::{column, row};
use iced::{Alignment, Element, Length};
@@ -98,17 +99,14 @@ fn rename_row<'a>(
// M237:输入框住在卡里 ⇒ 状态层与描边叠卡底。
.base(frame::BASE)
.placeholder(texts.saves_rename_placeholder())
.width(Length::Fill),
.width(Length::Fill)
// P18:回车与右枚按钮**同一个**消息产地(见 `commit_rename` 的文档)。
.on_submit(|| Message::Instances(commit_rename())),
|value| Message::Instances(PageMessage::RenameWorldChanged(value)),
);
row![
field,
action(
ui,
texts.saves_rename_confirm(),
PageMessage::CommitRenameWorld,
forced
),
action(ui, texts.saves_rename_confirm(), commit_rename(), forced),
action(
ui,
texts.saves_cancel(),
@@ -122,6 +120,15 @@ fn rename_row<'a>(
.into()
}
/// 「改名」那一下要发的那条页面意图——**按钮与回车共用的唯一产地**(M252 / P18)。
///
/// 为什么抽成一个函数:「回车走的是不是同一条路」在 iced 里量不到(`Element` 没有内省接口,
/// 见 `pages::layout::source` 的模块文档),能钉住的只有**两条路拿的是同一个 Message**——
/// 于是「空名 / 原名不变 / 非法字符不许提交」那几道校验自动对回车同样成立。
pub(crate) fn commit_rename() -> PageMessage {
PageMessage::CommitRenameWorld
}
/// 名字 + 副文案(两行)。
fn labels<'a>(ui: &'a Ui, item: &'a Row, color: Role) -> iced::widget::Column<'a, Message> {
column![
@@ -10,6 +10,9 @@ mod launch_args;
/// M235 的存档区模型判据(快照 → 行 / 计数 / 失败线 / UTC 时间)。
mod saves;
/// M252 / P18 的**回车**接线(行内重命名:回车 = 点「改名」)。
mod rename;
use super::super::layout::source;
use super::super::layout::{frame_metrics, measure_cap_px, PaneKit, WidthClass};
@@ -0,0 +1,52 @@
//! M252 / P18:行内重命名的**回车**接线。
//!
//! iced 的 `Element` 没有内省接口(见 `pages::layout::source` 的模块文档),所以「真按下回车会
//! 发生什么」在单测里量不到。这里能钉住的两件事:
//!
//! | 断言 | 抓得住 | 抓不住 |
//! |---|---|---|
//! | [`enter_takes_the_same_message_as_the_rename_button`] | 「回车」与「点改名」共用的那条消息被换成别的(取消 / 重开…);空名 / 原名不变 / 非法字符那几道校验因此**自动**对回车成立——它们挂在同一个 Message 后面 | 校验本身对不对(在 `app/update/instances.rs` 与 presenter,本 mission 一字未改) |
//! | [`the_rename_field_wires_enter`] | `rename_row` 里那枚输入框的 `.on_submit(..)` 被删掉 / 接成别的(T27 反例) | 回车键真的到达控件(那是 iced 的 `Widget::update`) |
//!
//! 也就是说:判据①「回车真的提交一次」里,**「提交的是什么」被钉住了,「按下去会不会到这儿」没有**
//! (理由与替代验证写在 M252 报告的「没验的部分」一节)。
use crate::pages::layout::source;
use super::super::saves::commit_rename;
use super::super::state::Message as PageMessage;
/// 回车与「改名」按钮共用同一个消息产地 ⇒ 走同一套校验。
///
/// `PageMessage` 只有 `Debug + Clone`(没有 `PartialEq`,那些枚举不在本 mission 的 scope),
/// 所以这里比 `Debug` 文本:枚举的 `Debug` 打印变体名与字段,变体不同必然文本不同。
#[test]
fn enter_takes_the_same_message_as_the_rename_button() {
assert_eq!(
format!("{:?}", commit_rename()),
format!("{:?}", PageMessage::CommitRenameWorld),
"回车必须发「提交重命名」,不能是取消/重开/逐键输入"
);
assert!(
!format!("{:?}", commit_rename()).contains("Cancel"),
"回车不许接到取消上(那会让用户按回车丢掉草稿)"
);
}
/// `rename_row` 里那枚输入框真的接了 `on_submit`,且接的是 `commit_rename`(与按钮同一个)。
///
/// 抓得住(T27 反例):把 `.on_submit(..)` 那一行删掉 → 第一条断言红;把它接成
/// `CancelRenameWorld` → 第二条断言红。
/// 抓不住:回车键真的到达控件(iced 的键路径,见模块文档)。
#[test]
fn the_rename_field_wires_enter() {
let body = source::function_body(include_str!("../saves/rows.rs"), "fn rename_row");
assert!(
body.contains(".on_submit("),
"输入框没接回车(去掉接线这条必须红),实际函数体:\n{body}"
);
assert!(
body.matches("commit_rename()").count() >= 2,
"回车与「改名」按钮必须共用 `commit_rename()`(两条路同一个 Message),实际函数体:\n{body}"
);
}