用户原话「默认左侧固定,允许折叠」:M297 盘查确认 iced 从来没接过折叠按钮, presenter 的 toggle_nav/nav_collapsed 是 Slint 遗留、唯一调用方在已退役的 crates/app。 ## 令牌(先定数值再动手) - nav_collapsed_width = 56 + 2 × Space::X1 = **64**:胶囊(M283 定稿 56×32,本轮不动) 两侧各留 4px 才不被裁切;旧值 48 的死因写进注释(48 − 2×4 = 40 < 56)。 - 断言:折叠宽 ≥ 胶囊宽 + 2×X1、≥ touch_target 48、在 4px 栅格上。 ## 折叠按钮与折叠态 - 按钮放**轨底、头像位之上**:轨顶一个像素都不动(M283/M299 的展开态对照图仍可比), 轨底本来就是「固定贴底」的槽,两态下位置不变。几何与导航项同一套(56×32 药丸 + 52 命中高)。 - 折叠态:隐藏文字标签与账户名,每项/头像/按钮都补 M3 气泡 tooltip(折叠态不许是一列无名符号); 纵节奏不变(项高仍 52),所以切换时图标不上下跳。五项图标两两不同由用例钉住。 ## 动效(复用底盘、零新常数) - 帧泵加**第二条并列宽度轨**(nav ⇄ rail),时长/曲线复用同一张 rail_spec(); 借用右栗那条会让左轨折叠改掉右栗的目标(暗耦合),tower 已否掉那个方案。 - 起跑在 update 侧;启动按落盘档对表(幂等,仅在没有轨道在跑时);折叠**不置 view_dirty** —— 宽度在 view 里逐帧采样,帧由轨道自己续,跑完 needs_frames() 回 false。 ## 落盘(任务要求「重启读回仍折叠」) - core schema 登记 ui.nav.collapsed(纯增量 bool,不碰 schemaVersion);presenter 桥加 load/persist 一对(走唯一写入口 write_with,现读→改→落盘);启动 construct 读回。 - 截图通道不读用户配置(D30),折叠态出图走 ULL_NAV_COLLAPSED=1。 ## 判据 - app-iced: 1053 passed(新增 3 条接线用例 + 7 条 shell/几何/树结构用例 + 6 条帧泵用例); presenter: 408 passed(新增落盘往返 1 条);fmt / clippy --workspace --all-targets 均 0。 - 变异实验:删 observe_nav ⇒ 宽度停在 88 红;删启动对表 ⇒ 首次展开硬跳红。 - 像素(DevDocs/artifacts/m300/):折叠态内容左缘 112→88(24px 真回收)、药丸两态都 56 宽 且折叠档右缘 59 < 64(不裁切)、图标行是展开态子集(纵节奏不变)。 ## 已知偏离(已登记,tower 批了紧随 mission) crates/app-iced/src/pages/layout.rs 的 rail_width() 仍是 const fn(恒 88),所以折叠态内容块 宽度不变、整块左移 24px ⇒ 右侧多一条 24px 空白带(像素图里保留、未裁)。
198 lines
10 KiB
Rust
198 lines
10 KiB
Rust
//! `App` 的两个构造器(从 `state.rs` 搬出来守 400 行硬线;M119 拆,纯搬不改)。
|
||
//!
|
||
//! | 构造器 | 谁用 | 碰不碰磁盘 |
|
||
//! |---|---|---|
|
||
//! | [`App::new`](super::App::new) | 生产(`main` → `boot`) | 按启动参数选后端 + 打开设置桥 |
|
||
//! | [`App::with_presenter`](super::App::with_presenter) | 单测(后端与桥都由调用方给) | **不碰** |
|
||
//!
|
||
//! 两个都走 `with_presenter`:生产路径只是先造好后端与桥。**字段初始化表**也在这一个文件里
|
||
//! (构造 = 字段表 + 三件外部依赖),加字段时只改这一处。
|
||
|
||
use iced::window;
|
||
|
||
use unlucky_design::color::SchemeFlavor;
|
||
use unlucky_presenter::backend::{build_backend, config_path};
|
||
use unlucky_presenter::dto::Backend;
|
||
use unlucky_presenter::prefs::ThemeMode;
|
||
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||
use unlucky_presenter::state::AppState;
|
||
|
||
use crate::cli::Options;
|
||
use crate::pages;
|
||
use crate::theme::{Families, Ui};
|
||
use crate::ui::lang::LangState;
|
||
|
||
use super::super::WINDOW_SIZE;
|
||
use super::boot_probe::app_options;
|
||
use super::App;
|
||
|
||
impl App {
|
||
/// 生产构造:按启动参数选后端、打开设置桥。
|
||
///
|
||
/// **截图通道强制 mock**:出图要可复现(D30),真 host 会按本机盘上的实例清单画面,
|
||
/// 同一份代码在两台机器上出不了同一张图。交互模式走真 host(数据根解析不出来时
|
||
/// [`build_backend`] 自己退回 mock)。
|
||
pub(in crate::app) fn new(options: Options, families: Families, flavor: SchemeFlavor) -> Self {
|
||
let backend = build_backend(&app_options(&options));
|
||
// P2-2:设置桥必须与后端指向**同一个数据根**。早先这里无条件 `SettingsBridge::open()`
|
||
// (按便携配置解析),于是 `--data-dir X` 会出现「实例来自 X、主题/设置读写默认根」的一进程
|
||
// 两个数据根。Slint 版是同一条口径:`app/src/ui_bind.rs` 的
|
||
// `Some(root) => SettingsBridge::with_path(config_path(root))`。
|
||
let bridge = match &options.data_dir {
|
||
Some(root) => SettingsBridge::with_path(config_path(root)),
|
||
None => SettingsBridge::open(),
|
||
};
|
||
// M188:日志目录也在这一拍登记——它是从**同一份**设置桥反推的(`--data-dir` 同样生效),
|
||
// 所以设置页显示的路径与 `launcher.log` 的真实落点不会各说各话。
|
||
crate::app::logs_folder::register(&bridge);
|
||
Self::with_presenter(options, families, flavor, backend, bridge)
|
||
}
|
||
|
||
/// 注入式构造(单测用:后端与桥都由调用方给,**不碰磁盘**)。
|
||
pub(in crate::app) fn with_presenter(
|
||
options: Options,
|
||
families: Families,
|
||
flavor: SchemeFlavor,
|
||
backend: Box<dyn Backend>,
|
||
bridge: SettingsBridge,
|
||
) -> Self {
|
||
let width = WINDOW_SIZE.width;
|
||
let height = WINDOW_SIZE.height;
|
||
// 主题档:截图通道**不读机器配置**(D30:同一份代码在两台机器上要出同一张图)——
|
||
// 由 `--screenshot-theme` 钉死(浅 → Silver / `light`,深 → Ink / `dark`)。
|
||
// 产品路径才读 `appearance.theme_mode`。复审 P2:此前这里两种通道都读文件,
|
||
// 设置页「主题」那一行的选中态会随出图机器的配置漂移。
|
||
let theme_mode = match options.screenshot.is_some() {
|
||
true => match options.shot_flavor().unwrap_or(SchemeFlavor::Light) {
|
||
SchemeFlavor::Light => ThemeMode::Light,
|
||
SchemeFlavor::Dark => ThemeMode::Dark,
|
||
},
|
||
false => bridge.load().1,
|
||
};
|
||
// 外观两半分开读:`load()` 给 8 个 UI 写入字段,`load_base()` 给 4 个只读下层键
|
||
// (整窗 α / 标题栏三态 / 配色来源 / 窗口圆角)。M93 之前这些键在 app 侧**根本读不到**。
|
||
//
|
||
// 截图通道**不读用户配置**(D30:同一份代码在两台机器上要出同一张图):外观一律取出厂值,
|
||
// 矩阵需要的值由 `ULL_APPEARANCE_*` 显式灌进来。
|
||
let (appearance_prefs, appearance_base) = if options.screenshot.is_some() {
|
||
(
|
||
unlucky_presenter::prefs::AppearancePrefs::factory(),
|
||
unlucky_presenter::prefs::AppearanceBase::factory(),
|
||
)
|
||
} else {
|
||
(bridge.load().0, bridge.load_base())
|
||
};
|
||
// 面板 α / 玻璃的降级判据要的是**实际**后端,不是 `--renderer` 的请求值(复审 r1 P2-2):
|
||
// 默认 `Auto` + 无独显时请求是 Auto、实际是 tiny-skia,只看请求值就会漏掉 D36 的强制项。
|
||
let backend_tier = super::super::backend::effective_backend(options.backend);
|
||
let mut appearance = pages::appearance::State::new(
|
||
&appearance_prefs,
|
||
&appearance_base,
|
||
backend_tier,
|
||
flavor,
|
||
);
|
||
// 取证开关(D30):`ULL_SETTINGS_GROUP=<下标>` 直达设置页的某一组(出图要看见语言行)。
|
||
if let Some(index) =
|
||
crate::ui::lang::screenshot_settings_group(options.screenshot.is_some())
|
||
{
|
||
appearance.selected_group = index;
|
||
}
|
||
// 界面语言(M106):**M92 已经探测对了,问题是一直没人用**——`pages::Model.lang` 此前在
|
||
// `view::model` 里被写死成 `Lang::En`,于是中文系统上开出来的是全英文界面。
|
||
let lang = LangState::boot(&bridge, options.screenshot.is_some());
|
||
// M168(复审 r2 P2-2):出图通道**不许联网**——`log_cjk` 那一拍会走 `after_probe`,
|
||
// 一台 zh-locale 且没有中文字体的机器上会真的下 17 MiB,并可能在出图过程中注册新字体
|
||
// (D30:同一份代码要出同一张图)。产品路径不设这个开关。
|
||
crate::fonts::install::set_offline(options.screenshot.is_some());
|
||
let mut view = pages::Model::new(
|
||
pages::Screen::Instances,
|
||
width,
|
||
height,
|
||
Some(pages::Screen::Instances.nav()),
|
||
);
|
||
view.lang = lang.lang();
|
||
|
||
let mut app = Self {
|
||
ui: Ui::new(flavor, families),
|
||
families,
|
||
options,
|
||
backend,
|
||
bridge,
|
||
state: AppState::new(),
|
||
ticks: 0,
|
||
config_due_at: unlucky_presenter::theme::CONFIG_POLL_TICKS,
|
||
theme_mode,
|
||
lang,
|
||
last_tick: std::time::Instant::now(),
|
||
width,
|
||
view,
|
||
// M256:换页过渡期的「旧页」快照。启动时没有旧页。
|
||
previous_view: None,
|
||
view_dirty: false,
|
||
watch_until: None,
|
||
window: None::<window::Id>,
|
||
frames: 0,
|
||
refreshes: 0,
|
||
auto_selected: false,
|
||
// M168:中文字体下载状态。构造这一拍 `lang.log_cjk()` 与启动编排的 `fonts::resolve()`
|
||
// 已经可能发起下载,所以初值就取**当前快照**(不能假定是 Unknown,否则第一拍会白重建)。
|
||
font_seen: crate::fonts::install::status(),
|
||
height,
|
||
appearance,
|
||
appearance_prefs,
|
||
hwnd: None,
|
||
settings: pages::settings::Groups::default(),
|
||
// 出厂是**空会话**(M119 删了 M104 的样本会话);取证开关 `ULL_AGENT_*` 的档位
|
||
// 与夹具在 `boot_agent` 里落地(它要先让 presenter 知道栏可见性,见那里)。
|
||
agent: crate::shell::agent::AgentUi::new(),
|
||
agent_host: super::presenter::AgentHost::from_env(),
|
||
// 下载页安装草稿(M105):出厂态 = 没选中、原版、两个空串
|
||
download_draft: pages::InstallDraft::default(),
|
||
launch_form: pages::LaunchForm::default(),
|
||
adopt_form: super::super::adopt::Form::default(),
|
||
install_cancelled_seen: 0,
|
||
launch_ends_seen: 0,
|
||
mods_failed_seen: None,
|
||
dialog_card: None,
|
||
dialog_model: None,
|
||
toast_tracks: super::toast::ToastTracks::default(),
|
||
content: pages::content::PageState::default(),
|
||
account_form: pages::account::Form::new(),
|
||
account: pages::account::Page::build(
|
||
&AppState::new(),
|
||
&pages::account::Form::new(),
|
||
&mut pages::account::avatar::Cache::default(),
|
||
unlucky_i18n::Lang::En,
|
||
width,
|
||
),
|
||
avatars: pages::account::avatar::Cache::default(),
|
||
corner_applied: None,
|
||
};
|
||
// M229:设置页「主题」行的选中态来自设置组自有状态(`language_appearance::State::theme`),
|
||
// 唯一写者是 `App::set_theme_mode`;这里把启动时读到的档位灌进去,首帧才不会画错档。
|
||
app.settings.language_appearance.theme = theme_mode;
|
||
// M300:导航轨的折叠档(`ui.nav.collapsed`)。**截图通道不读用户配置**(D30:同一份代码
|
||
// 在两台机器上要出同一张图):出图一律展开态,折叠态那两张图由 `ULL_NAV_COLLAPSED` 显式灌。
|
||
app.state.prefs.nav_collapsed = if app.options.screenshot.is_some() {
|
||
crate::app::update::boot_env::env_nav_collapsed()
|
||
} else {
|
||
app.bridge.load_nav_collapsed()
|
||
};
|
||
// 配色来源档可能已经指向一张背景图:那时 `Ui` 必须由解析出的方案构造,而不是色板。
|
||
app.rebuild_ui();
|
||
// M174:助手配置的首屏值(真值来自 `config/launcher.json`;截图通道走 `ULL_AI_STATE`)。
|
||
app.hydrate_ai_settings();
|
||
// M175:Java 分组的首屏值 + **首屏自动扫一次盘**(截图通道走 `ULL_JAVA_STATE`)。
|
||
// M223: adopt wizard screenshot seed (evidence channel only; `ULL_ADOPT_STATE`).
|
||
app.hydrate_adopt_state();
|
||
app.hydrate_java_settings();
|
||
// 两行日志:①档位 + 探测结论 + 生效语言 ②中文策略(D28 缺字体不许静默)。
|
||
// 下一个人据此分得清「探测错了」还是「探测对了但没用上」。
|
||
app.lang.log();
|
||
app.lang.log_cjk();
|
||
// M298:出图取证用的 Toast 夹具(`ULL_TOAST_FIXTURE`;产品路径读不到这个变量)。
|
||
super::toast_shot::seed(&mut app);
|
||
app
|
||
}
|
||
}
|