feat(app-iced): M104 AGENTRAIL —— 右栗三档 + 布局数学 + 独立页复用同一会话视图
用户定案(D16):右侧常驻可折叠、允许拆分成独立页、中栗宽度可调(磁铁吸附)。 本 mission 只做壳:三档状态 + 布局数学 + 一份会话视图;真数据仍归 M103,一行 presenter 都不调。 - 右栗三档:Collapsed(48) / Expanded(280~360,默认 320,锚=令牌 ai_panel_width) / Detached(Route::Agent); 会话、草稿、未读在切档时一条不丢(拆出去再收回,话说了一半还在) - 布局数学:中栗 = 窗口 − 右栗 − 拖拽缝(8),下限 472(= 行宽上限 424 + 两侧页边距); 右栗一展开,页面按更窄的宽度重新排版(收缩而不是溢出);拖拽磁铁吸附到 280/320/360,超上限夹紧 - 独立页与右栗共用 conversation::view 这一份实现(输入=纯数据,输出=agent::Message) - 控件:多行输入框(Enter 发送 / Shift+Enter 换行,走 text_editor::key_binding 的 modifiers 分流)、 发送禁用态、流式中断、未读点 + 可读计数、⌄ 收起 / ⇥ 拆出 / ⇤ 收回;Esc 收栗、Tab 焦点环、Enter 激活 - 文案全部进 crates/i18n 的新域 catalog/agent.rs(+ golden 追加 20 行),零硬编码中文 - 取证:ULL_AGENT_RAIL / ULL_AGENT_FIXTURE / ULL_AGENT_WIDTH(截图走产品 bin,标题栏与三键可见) - 消息不走 pages::Message(外壳级视图),走 app::Message::Agent(..) → app/update/agent.rs - 删掉 M68 骨架回落(shell::view + shell/fallback.rs):Route::Agent 现在有真页面 门禁:fmt 0 / clippy(workspace,-D warnings) 0 / pages_demo 0 / app-iced lib 418 条全绿
This commit is contained in:
1 parent
875ac159ee
commit
c3f20784a8
20 files changed
+2463
-153
No files matched your search
@@ -1,75 +0,0 @@
|
||||
//! M68 骨架卡:**只给 `Route::Agent` 的回落路径用**(AI 独立页 P1 才落地)。
|
||||
//!
|
||||
//! 内容区 = 页头(`TitleLarge`)+ 一张 `OutlinedCard`("Shell ready" + 一段说明文字)。
|
||||
//! 它原本住在 `shell/mod.rs`,M90 r2 为了让 `mod.rs` 回到 400 行以内搬到这里;
|
||||
//! 行为一个字没改(`shell::view` 的输出与拆前逐字节相同)。
|
||||
//!
|
||||
//! 为什么不能直接删:删了 `shell::view` 就没有调用点,而 `mod shell` 是私有的 ——
|
||||
//! 私有模块里的 `pub fn` 无人使用会被 `dead_code` 判死,`-D warnings` 直接红。
|
||||
|
||||
use iced::widget::{column, container};
|
||||
use iced::{Alignment, Background, Element, Length, Padding};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::component_map::Component;
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::label;
|
||||
|
||||
// ---------------------------------------------------------------- 内容区
|
||||
|
||||
pub(super) fn content<'a>(ui: &'a Ui, title: &'a str, body: &'a str) -> Element<'a, Message> {
|
||||
let metrics = ui.metrics();
|
||||
// 卡片档位由组件表决定(圆角/字级),组件层不自己挑
|
||||
let card_spec = Component::OutlinedCard.spec();
|
||||
let body_role = card_spec.support.unwrap_or(card_spec.text);
|
||||
|
||||
let header = container(label(
|
||||
ui,
|
||||
TypeRole::TitleLarge,
|
||||
true,
|
||||
title,
|
||||
ui.color(Role::OnSurface),
|
||||
))
|
||||
.width(Length::Fill)
|
||||
.height(metrics.page_header_height)
|
||||
.align_y(Alignment::Center)
|
||||
.padding(Padding::from([0.0, ui.space(Space::X6)]));
|
||||
|
||||
let card = container(
|
||||
column![
|
||||
label(
|
||||
ui,
|
||||
card_spec.text,
|
||||
true,
|
||||
"Shell ready",
|
||||
ui.color(Role::OnSurface),
|
||||
),
|
||||
label(ui, body_role, false, body, ui.color(Role::OnSurfaceVariant)),
|
||||
]
|
||||
.spacing(ui.space(Space::X2)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(metrics.card_padding)
|
||||
.style(move |_theme| container::Style {
|
||||
background: Some(Background::Color(ui.layer(MaterialLayer::Card))),
|
||||
border: ui.outlined(card_spec.shape),
|
||||
..container::Style::default()
|
||||
});
|
||||
|
||||
let body_area = container(card)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(ui.space(Space::X6))
|
||||
.align_y(Alignment::Start);
|
||||
|
||||
column![header, body_area]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//! 首屏骨架(**不做业务**):自绘标题栏 + 左侧导航轨 + 内容区(页头 + 一张卡片)。
|
||||
//! 首屏骨架(**不做业务**):自绘标题栏 + 左侧导航轨 + 内容区 + **右侧 AI 助手栏**(M104)。
|
||||
//!
|
||||
//! ## 与参考判据的对应(`research/12b-component-fidelity.md`)
|
||||
//!
|
||||
@@ -15,9 +15,18 @@
|
||||
//! 一屏只出现 4 档字号(22 / 16 / 14 / 12),且**没有实心 accent**(rubric K2:一屏最多一个实心
|
||||
//! Primary,骨架期一个都不要)。阴影只给浮层,骨架里没有浮层,所以**全屏零阴影**(rubric K3)。
|
||||
|
||||
mod fallback;
|
||||
mod nav;
|
||||
|
||||
/// AI 助手栏与 AI 独立页(M104):**物理目录是 `crates/app-iced/src/pages/agent/`**,
|
||||
/// 用 `#[path]` 挂到外壳的命名空间。
|
||||
///
|
||||
/// 为什么是 `#[path]` 而不是在 `pages/mod.rs` 加一行 `mod agent;`:`pages/mod.rs` 是四条页面线
|
||||
/// 共用的声明文件(本 mission 的 scope 没给它),而右栗是**外壳的一部分**(它挂在任何页面右边)。
|
||||
/// 先例是 `app/mod.rs` 挂 `../components/mod.rs`。物理路径只有一份,逻辑名字有两个用途:
|
||||
/// 宿主(右栗)归外壳,视图(会话组件)归页面目录。
|
||||
#[path = "../pages/agent/mod.rs"]
|
||||
pub(crate) mod agent;
|
||||
|
||||
pub use nav::NavItem;
|
||||
|
||||
use iced::widget::text::LineHeight;
|
||||
@@ -36,34 +45,7 @@ use crate::cli::ForcedState;
|
||||
use crate::icons;
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// 整棵树(**内容槽位版**:外壳画标题栏 / 缩放热区 / 导航轨,中间那块交给调用方)。
|
||||
///
|
||||
/// 这是产品页面走的入口(M90 r2 / P1-1)。为什么要它:`decorations: false` 的无边框窗口里,
|
||||
/// 拖动 / 缩放 / 最小化 / 关闭这四条消息的构造点**只有本模块**,页面树自己画的「标题栏留白带」
|
||||
/// 没有任何控件,于是产品页会变成「拖不动、缩不了、点不到关闭」。
|
||||
///
|
||||
/// 分工写死:
|
||||
///
|
||||
/// - 本模块负责:标题栏(拖动面 = **整条 48px 栏里从窗口左缘到第一个 caption** 的 `mouse_area`,
|
||||
/// 没有单独的 8px 热区;三个 caption 键各 46×48)、东/南两条缩放热区(各 `Space::X1` = 4px)、
|
||||
/// 导航轨、根容器底色;
|
||||
/// - 调用方负责:`content`(页面内容区)。它拿到的可用宽度是 `窗口宽 − rail 88`,
|
||||
/// 而页面自己的 `layout::frame_metrics` 仍以**窗口宽**为输入(它内部会再减 rail 与页边距),
|
||||
/// 两边的账不会算串。
|
||||
///
|
||||
/// 面板 α(D36 / M93)不从这里传:它是**表面色**的属性,唯一施加点是 `Ui::layer`
|
||||
/// (`crate::theme`)。外壳的根容器 / 标题栏 / 导航轨都走 `ui.layer(..)`,因此自动同源。
|
||||
pub fn view_with_content<'a>(
|
||||
ui: &'a Ui,
|
||||
selected: NavItem,
|
||||
app_title: &'a str,
|
||||
content: Element<'a, Message>,
|
||||
forced: Option<ForcedState>,
|
||||
) -> Element<'a, Message> {
|
||||
view_with_background(ui, selected, app_title, None, content, forced)
|
||||
}
|
||||
|
||||
/// 同 [`view_with_content`],但可以在整棵树**最底下**垫一层背景(M93 的真图背景层)。
|
||||
/// 整棵树(**内容槽位版**):外壳画标题栏 / 缩放热区 / 导航轨 / 右栗,中间那块交给调用方。
|
||||
///
|
||||
/// `background` 由 `pages::appearance::layer` 产出(已与 `Message::Page` 映射过);垫在 `stack!`
|
||||
/// 最底层,于是「面板 α < 1」时能真的看见它——这就是 D34 在像素上的落点。
|
||||
@@ -73,39 +55,24 @@ pub fn view_with_background<'a>(
|
||||
app_title: &'a str,
|
||||
background: Option<Element<'a, Message>>,
|
||||
content: Element<'a, Message>,
|
||||
right: Option<Element<'a, Message>>,
|
||||
overlay: Option<Element<'a, Message>>,
|
||||
forced: Option<ForcedState>,
|
||||
) -> Element<'a, Message> {
|
||||
let work_area = column![
|
||||
titlebar(ui, app_title, forced),
|
||||
row![nav::rail(ui, selected, forced), content]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
// 右栗(M104)是 `row` 的**第三个兄弟**而不是叠层:它是布局宽度的一部分,压在上面会让
|
||||
// 中栗以为自己还有那么宽(那就是任务里禁止的「溢出」)。它的宽度由 `agent::docked` 自己声明。
|
||||
let mut work = row![nav::rail(ui, selected, forced), content]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
if let Some(right) = right {
|
||||
work = work.push(right);
|
||||
}
|
||||
|
||||
window_frame(ui, background, work_area.into())
|
||||
}
|
||||
let work_area = column![titlebar(ui, app_title, forced), work]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
/// 整棵树(M68 骨架版:内容区是「页头 + 一张占位卡」)。
|
||||
///
|
||||
/// **保留给 `Route::Agent` 回落**(AI 独立页 P1 才落地)。删掉它会连带
|
||||
/// `content` / `label` / `nav::rail` 变成死代码,而 `mod shell` 是私有的 ⇒ `-D warnings` 直接红。
|
||||
pub fn view<'a>(
|
||||
ui: &'a Ui,
|
||||
selected: NavItem,
|
||||
app_title: &'a str,
|
||||
page_title: &'a str,
|
||||
body: &'a str,
|
||||
forced: Option<ForcedState>,
|
||||
) -> Element<'a, Message> {
|
||||
view_with_content(
|
||||
ui,
|
||||
selected,
|
||||
app_title,
|
||||
fallback::content(ui, page_title, body),
|
||||
forced,
|
||||
)
|
||||
window_frame(ui, background, work_area.into(), overlay)
|
||||
}
|
||||
|
||||
/// 无边框窗口的「外壳壳子」:背景层 + 工作区 + 两条缩放热区 + 根容器底色。
|
||||
@@ -114,6 +81,7 @@ fn window_frame<'a>(
|
||||
ui: &'a Ui,
|
||||
background: Option<Element<'a, Message>>,
|
||||
work_area: Element<'a, Message>,
|
||||
overlay: Option<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
// 无边框窗口没有系统缩放边框 → 自己给两条热区(东/南)。
|
||||
// **必须叠层而不是兄弟列**:当兄弟列会吃掉 4px 布局宽(右留白变 28、caption 按钮离窗角 4px,
|
||||
@@ -148,10 +116,14 @@ fn window_frame<'a>(
|
||||
// 背景在最底层(M93 / D34)。有背景层时**根容器不再盖实色画布**:否则那张不透明的
|
||||
// `Canvas` 会把背景图整块挡住(r2 实测:panel α=1 时背景完全看不见)。
|
||||
// 各面板(标题栏 / 导航轨 / 卡片 / 页头带)自己画底色,空隙里露出的就是背景图。
|
||||
// 拖拽层(M104)压在最上面:它只在「按下右栗分隔条」期间存在,作用是把指针 x 变成拖拽消息
|
||||
// (`mouse_area::on_move` 只在自己的边界内回报坐标,所以这一层必须铺满整窗)。
|
||||
let has_background = background.is_some();
|
||||
let shell = match background {
|
||||
Some(layer) => stack![layer, work_area, east, south],
|
||||
None => stack![work_area, east, south],
|
||||
let shell = match (background, overlay) {
|
||||
(Some(layer), Some(overlay)) => stack![layer, work_area, east, south, overlay],
|
||||
(Some(layer), None) => stack![layer, work_area, east, south],
|
||||
(None, Some(overlay)) => stack![work_area, east, south, overlay],
|
||||
(None, None) => stack![work_area, east, south],
|
||||
};
|
||||
|
||||
container(shell)
|
||||
|
||||
Reference in new issue
Block a user