fix(app-iced): 面板预算按内容推导 + 版本菜单不画 L2 阴影(M238)

两条都在「视口不终止内容」这一族里,修法各一半:

① 三栏档面板预算从死的 `PANEL_BUDGET = 300` 改成 `panel_budget(menu_items)`
   (内边距 24 + 首屏 76 + 间距 8 + 钉死区 88 + 菜单高 + 列表↔面板 21)。
   旧常数不含列表↔面板那 21、也不随版本菜单行数变,于是 760/900 档面板实得
   286/294px,滚动视口被挤到 54/62px——六枚 loader chip 只露出 10px(736 与 1180
   同样严重,纵向预算与窗宽无关)。修后 760 档视口 98px(chips 31px 整枚在屏)、
   900 档 106px;列表按整行让出:760 档 4→3 整行、900 档 7→6 整行;只有一条
   加载器版本时菜单只占 64px,列表仍是 4 整行。

② 版本菜单改用 `Menu::flat()`:不是「把阴影裁掉」,而是**不在滚动容器里画阴影**。
   `iced_tiny_skia` 0.14 画 quad 阴影走 `draw_pixmap(.., mask: None)`
   (engine.rs:136-143),绕过裁切掩码;`iced_wgpu` 那边是整层 scissor
   (quad.rs:96-101)⇒ 同一棵树两个后端画出来不一样。修前菜单底下 54px 一条被压暗
   (浅色 249→231,−7.2%,含 8px 缝与实例名框内上半部),修后逐像素等于画布色。
   菜单的 L2 令牌不动,真实浮层照旧带阴影。

判据(两条,抓得住的/抓不住的都写进文档注释):
- 结构:`rows/tail.rs` 断言三栏档 760/900 的滚动视口 ≥ 首屏 76px(对生产常量做算术);
- 像素:`options_shot/compact.rs` 断言 chip 描边上下两行都在、且实例名框上方那条缝
  与框内都是纯画布色。
变异自证:预算退回 300 ⇒ 结构断言红("滚动视口只有 54px < 首屏 76px");阴影无视
`flat()` ⇒ 像素断言红("y=660 x=304 是 [231,231,232] 而不是画布色")。

没验:滚动中/底部三态拍不出来(harness 无 `scroll_to` 派发通道,已另开 finding)。
This commit is contained in:
AskaEth committed 2026-11-11 08:28:06 +08:00
1 parent dbc5274965
commit df0944894f
11 files changed
+454 -69

No files matched your search

+14 -13
View File
@@ -19,6 +19,10 @@
//! **容器色不取 `MaterialLayer::Overlay`**:那是 `SurfaceContainerHigh`,比 A1 的 `SurfaceContainer`
//! 高一档;`MaterialLayer` 只用来确认「弹出层属于 Overlay 语义」,颜色以 A1 为准。
//!
//! 阴影不是所有用法都要:被放进**滚动视口**(会被裁切)的菜单要用 [`Menu::flat`]——
//! CPU 后端画阴影时绕过裁切掩码,阴影会漏到视口外压暗下面的内容(M238,理由与实证见
//! `menu/shadow.rs` 的 `flat` 文档)。
//!
//! ## 分组分隔线
//!
//! 复用 [`crate::app::components::divider::full`](一像素线 + `OutlineVariant`),**不自己画线**。
@@ -26,9 +30,7 @@
use iced::widget::text::LineHeight;
use iced::widget::{button, column, container, text};
use iced::{
Alignment, Background, Border, Color, Element, Length, Padding, Pixels, Shadow, Vector,
};
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Pixels, Shadow};
use unlucky_design::color::Role;
use unlucky_design::component_map::Component;
@@ -46,6 +48,10 @@ use super::super::interaction::{self, Base, Interaction};
use super::super::layout;
use super::geometry::MenuGeometry;
mod shadow;
use shadow::panel_shadow;
/// 菜单里的一项。
#[derive(Debug, Clone)]
pub struct MenuItem<'a, M> {
@@ -121,6 +127,7 @@ pub enum Entry<'a, M> {
pub struct Menu<'a, M> {
entries: Vec<Entry<'a, M>>,
width: Length,
shadow: bool,
}
impl<'a, M> Default for Menu<'a, M> {
@@ -135,6 +142,7 @@ impl<'a, M> Menu<'a, M> {
Self {
entries: Vec::new(),
width: Length::Fill,
shadow: true,
}
}
@@ -215,7 +223,7 @@ fn item_palette(ui: &Ui, base: Base, state: Interaction, selected: bool) -> Item
/// 项的禁用态(38% 内容)与状态层拿同一个 [`Base`] 合成——两边同源,改色不会只改一半(M234)。
pub const PANEL_BASE: Base = Base::of(Role::SurfaceContainer);
/// 菜单弹出层(`SurfaceContainer` + L2 阴影)。
/// 菜单弹出层(`SurfaceContainer` + L2 阴影;[`Menu::flat`] 时无阴影)。
pub fn menu<'a, M: Clone + 'a>(ui: &'a Ui, menu: Menu<'a, M>) -> Element<'a, M> {
let geometry = MenuGeometry::of(ui);
let spec = Component::Menu.spec();
@@ -225,7 +233,7 @@ pub fn menu<'a, M: Clone + 'a>(ui: &'a Ui, menu: Menu<'a, M>) -> Element<'a, M>
Elevation::Level2,
"MenuTokens.ContainerElevation"
);
let shadow_spec = elevation.shadow();
let shadow = panel_shadow(ui, elevation.shadow(), menu.shadow);
let panel = ui.color(PANEL_BASE.role());
let shape = geometry.shape;
@@ -246,14 +254,7 @@ pub fn menu<'a, M: Clone + 'a>(ui: &'a Ui, menu: Menu<'a, M>) -> Element<'a, M>
radius: ui.radius_dp(shape),
..Border::default()
},
shadow: Shadow {
color: Color {
a: shadow_spec.alpha,
..ui.color(Role::Shadow)
},
offset: Vector::new(0.0, shadow_spec.offset_y_px),
blur_radius: shadow_spec.blur_px,
},
shadow,
..container::Style::default()
})
.into()