Merge branch 'feat/fix-narrow-window-chips-squeeze-and-menu'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
74be510114
11 files changed
+454
-69
No files matched your search
@@ -19,6 +19,10 @@
|
|||||||
//! **容器色不取 `MaterialLayer::Overlay`**:那是 `SurfaceContainerHigh`,比 A1 的 `SurfaceContainer`
|
//! **容器色不取 `MaterialLayer::Overlay`**:那是 `SurfaceContainerHigh`,比 A1 的 `SurfaceContainer`
|
||||||
//! 高一档;`MaterialLayer` 只用来确认「弹出层属于 Overlay 语义」,颜色以 A1 为准。
|
//! 高一档;`MaterialLayer` 只用来确认「弹出层属于 Overlay 语义」,颜色以 A1 为准。
|
||||||
//!
|
//!
|
||||||
|
//! 阴影不是所有用法都要:被放进**滚动视口**(会被裁切)的菜单要用 [`Menu::flat`]——
|
||||||
|
//! CPU 后端画阴影时绕过裁切掩码,阴影会漏到视口外压暗下面的内容(M238,理由与实证见
|
||||||
|
//! `menu/shadow.rs` 的 `flat` 文档)。
|
||||||
|
//!
|
||||||
//! ## 分组分隔线
|
//! ## 分组分隔线
|
||||||
//!
|
//!
|
||||||
//! 复用 [`crate::app::components::divider::full`](一像素线 + `OutlineVariant`),**不自己画线**。
|
//! 复用 [`crate::app::components::divider::full`](一像素线 + `OutlineVariant`),**不自己画线**。
|
||||||
@@ -26,9 +30,7 @@
|
|||||||
|
|
||||||
use iced::widget::text::LineHeight;
|
use iced::widget::text::LineHeight;
|
||||||
use iced::widget::{button, column, container, text};
|
use iced::widget::{button, column, container, text};
|
||||||
use iced::{
|
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Pixels, Shadow};
|
||||||
Alignment, Background, Border, Color, Element, Length, Padding, Pixels, Shadow, Vector,
|
|
||||||
};
|
|
||||||
|
|
||||||
use unlucky_design::color::Role;
|
use unlucky_design::color::Role;
|
||||||
use unlucky_design::component_map::Component;
|
use unlucky_design::component_map::Component;
|
||||||
@@ -46,6 +48,10 @@ use super::super::interaction::{self, Base, Interaction};
|
|||||||
use super::super::layout;
|
use super::super::layout;
|
||||||
use super::geometry::MenuGeometry;
|
use super::geometry::MenuGeometry;
|
||||||
|
|
||||||
|
mod shadow;
|
||||||
|
|
||||||
|
use shadow::panel_shadow;
|
||||||
|
|
||||||
/// 菜单里的一项。
|
/// 菜单里的一项。
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MenuItem<'a, M> {
|
pub struct MenuItem<'a, M> {
|
||||||
@@ -121,6 +127,7 @@ pub enum Entry<'a, M> {
|
|||||||
pub struct Menu<'a, M> {
|
pub struct Menu<'a, M> {
|
||||||
entries: Vec<Entry<'a, M>>,
|
entries: Vec<Entry<'a, M>>,
|
||||||
width: Length,
|
width: Length,
|
||||||
|
shadow: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, M> Default for Menu<'a, M> {
|
impl<'a, M> Default for Menu<'a, M> {
|
||||||
@@ -135,6 +142,7 @@ impl<'a, M> Menu<'a, M> {
|
|||||||
Self {
|
Self {
|
||||||
entries: Vec::new(),
|
entries: Vec::new(),
|
||||||
width: Length::Fill,
|
width: Length::Fill,
|
||||||
|
shadow: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +223,7 @@ fn item_palette(ui: &Ui, base: Base, state: Interaction, selected: bool) -> Item
|
|||||||
/// 项的禁用态(38% 内容)与状态层拿同一个 [`Base`] 合成——两边同源,改色不会只改一半(M234)。
|
/// 项的禁用态(38% 内容)与状态层拿同一个 [`Base`] 合成——两边同源,改色不会只改一半(M234)。
|
||||||
pub const PANEL_BASE: Base = Base::of(Role::SurfaceContainer);
|
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> {
|
pub fn menu<'a, M: Clone + 'a>(ui: &'a Ui, menu: Menu<'a, M>) -> Element<'a, M> {
|
||||||
let geometry = MenuGeometry::of(ui);
|
let geometry = MenuGeometry::of(ui);
|
||||||
let spec = Component::Menu.spec();
|
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,
|
Elevation::Level2,
|
||||||
"MenuTokens.ContainerElevation"
|
"MenuTokens.ContainerElevation"
|
||||||
);
|
);
|
||||||
let shadow_spec = elevation.shadow();
|
let shadow = panel_shadow(ui, elevation.shadow(), menu.shadow);
|
||||||
let panel = ui.color(PANEL_BASE.role());
|
let panel = ui.color(PANEL_BASE.role());
|
||||||
let shape = geometry.shape;
|
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),
|
radius: ui.radius_dp(shape),
|
||||||
..Border::default()
|
..Border::default()
|
||||||
},
|
},
|
||||||
shadow: 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,
|
|
||||||
},
|
|
||||||
..container::Style::default()
|
..container::Style::default()
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
//! 菜单「要不要画 L2 阴影」(M238)。
|
||||||
|
//!
|
||||||
|
//! 从 `menu.rs` 拆出来只为守 400 行硬线(`components::gate::every_source_file_stays_under_the_line_limit`):
|
||||||
|
//! 那边加完「内联列表口径」正好 400 行、零余量。搬移的是**新增**的两小块,没有改动既有逻辑。
|
||||||
|
|
||||||
|
use iced::{Color, Shadow, Vector};
|
||||||
|
|
||||||
|
use unlucky_design::color::Role;
|
||||||
|
use unlucky_design::elevation::ShadowSpec;
|
||||||
|
|
||||||
|
use crate::theme::Ui;
|
||||||
|
|
||||||
|
use super::Menu;
|
||||||
|
|
||||||
|
impl<'a, M> Menu<'a, M> {
|
||||||
|
/// **内联列表口径**:不画 L2 阴影(容器色 / 圆角 / 项几何照旧)。
|
||||||
|
///
|
||||||
|
/// 用在「菜单被放进**会被裁切的容器**(滚动视口)」的场合——下载页的加载器版本列表就是
|
||||||
|
/// 这一种:它是面板里的一串版本,不是浮在页面上的弹出层。
|
||||||
|
///
|
||||||
|
/// 为什么必须在这里让步而不是「让它裁掉」:`iced_tiny_skia` 0.14 画 quad 阴影时用
|
||||||
|
/// `pixmap.draw_pixmap(.., mask: None)`(`iced_tiny_skia/src/engine.rs:136-143`),**绕过**
|
||||||
|
/// 了 `clip_mask` ⇒ 阴影照画到视口之外,压暗下面钉死区的实例名(M238 实测:浅色画面布
|
||||||
|
/// `(249,249,250)` 被压成 `(231,231,232)`,−7.2%)。`iced_wgpu` 那条路走整层的 scissor
|
||||||
|
/// (`iced_wgpu/src/quad.rs:96-101`)⇒ 同一棵树在 GPU / CPU 两个后端上**画出来的不一样**。
|
||||||
|
/// 不画阴影让两边一致,也把「浮层」语义收回到真正浮起来的地方。
|
||||||
|
///
|
||||||
|
/// 层级观感:`Component::Menu.spec().elevation` 仍是 **L2**(令牌表不动),真实浮层
|
||||||
|
/// (gallery / 将来的下拉)照旧带阴影;只有「内联列表」这一种用法放弃阴影,靠
|
||||||
|
/// `SurfaceContainer` 底色 + 4 圆角与画布区分(L1 卡片 → L2 菜单这条阶梯不被削平)。
|
||||||
|
pub fn flat(mut self) -> Self {
|
||||||
|
self.shadow = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 面板阴影:`elevated` 为假([`Menu::flat`])时给 `Shadow::default()`——**不是**把 α 调小,
|
||||||
|
/// 而是让 tiny-skia 的 `shadow.color.a > 0.0` 一判就直接跳过整块阴影绘制。
|
||||||
|
///
|
||||||
|
/// 单独抽出来是为了让「内联列表不画阴影」这条可断言:`Element` 没有几何/样式内省,
|
||||||
|
/// 单测只能钉这个纯函数(见 `menu/tests.rs`)。
|
||||||
|
pub(super) fn panel_shadow(ui: &Ui, spec: ShadowSpec, elevated: bool) -> Shadow {
|
||||||
|
if !elevated {
|
||||||
|
return Shadow::default();
|
||||||
|
}
|
||||||
|
Shadow {
|
||||||
|
color: Color {
|
||||||
|
a: spec.alpha,
|
||||||
|
..ui.color(Role::Shadow)
|
||||||
|
},
|
||||||
|
offset: Vector::new(0.0, spec.offset_y_px),
|
||||||
|
blur_radius: spec.blur_px,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -121,3 +121,28 @@ fn menu_shapes_build() {
|
|||||||
let _: Element<'static, u8> = sample(flavor);
|
let _: Element<'static, u8> = sample(flavor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// M238:`flat()`(内联列表口径)**整块阴影不画**,不是「把 α 调小」。
|
||||||
|
///
|
||||||
|
/// 抓得住:有人把 `flat` 实现成「α × 0.5」或只改 blur(`Shadow{ alpha>0, blur=0 }` 在 tiny-skia 里
|
||||||
|
/// 仍会走阴影分支),以及有人把 `menu()` 里那个 `panel_shadow(.., menu.shadow)` 的第三参写死成
|
||||||
|
/// `true`(那样 flat 菜单又带上阴影、漏回视口外)。
|
||||||
|
/// 抓不住:阴影**画出来**的效果(`Element` 没有样式内省);这条只钉决策函数,
|
||||||
|
/// 画出来的证据是 736 那张像素图(`tests/options_shot/compact.rs`)。
|
||||||
|
#[test]
|
||||||
|
fn flat_menus_skip_the_shadow_instead_of_toning_it_down() {
|
||||||
|
let ui = ui(SchemeFlavor::Light);
|
||||||
|
let spec = Elevation::Level2.shadow();
|
||||||
|
|
||||||
|
let elevated = panel_shadow(&ui, spec, true);
|
||||||
|
assert_eq!(elevated.blur_radius, 6.0);
|
||||||
|
assert_eq!((elevated.offset.x, elevated.offset.y), (0.0, 2.0));
|
||||||
|
assert_eq!(elevated.color.a, 0.14, "浮层:令牌给的 α 原样上身");
|
||||||
|
|
||||||
|
let flat = panel_shadow(&ui, spec, false);
|
||||||
|
assert_eq!(
|
||||||
|
(flat.color.a, flat.blur_radius),
|
||||||
|
(0.0, 0.0),
|
||||||
|
"内联列表:α 与 blur 都归零(tiny-skia 见 α = 0 直接跳过整块阴影,不是淡一点)"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -251,12 +251,14 @@ fn body<'a>(
|
|||||||
// 三栏不成立(可用宽 < 600):类型与主版本原样退回顶部 chips,列表吃掉整条可用宽。
|
// 三栏不成立(可用宽 < 600):类型与主版本原样退回顶部 chips,列表吃掉整条可用宽。
|
||||||
// 高度分法跟着档位走:chips 档多花两行 chip,面板必须收回自己的预算(M125 缺陷 1)。
|
// 高度分法跟着档位走:chips 档多花两行 chip,面板必须收回自己的预算(M125 缺陷 1)。
|
||||||
// M227:这一档也改成「面板先保预算、列表按整行吃剩下的」——预算比三栏档紧得多
|
// M227:这一档也改成「面板先保预算、列表按整行吃剩下的」——预算比三栏档紧得多
|
||||||
// (`rows::CHIPS_PANEL_BUDGET` 192 vs `PANEL_BUDGET` 300),清单见 `rows/tail.rs`。
|
// (`rows::CHIPS_PANEL_BUDGET` 192 vs 三栏档的 `rows::panel_budget`),清单见 `rows/tail.rs`。
|
||||||
let share = rows::TailShare::of(rails.tier, &install::panel_state(model));
|
let state = install::panel_state(model);
|
||||||
|
let share = rows::TailShare::of(rails.tier, &state);
|
||||||
|
let list_height = share.list_height(body_height, rows::menu_items(&state));
|
||||||
column![
|
column![
|
||||||
rails::stacked_chips(ui, catalog, model),
|
rails::stacked_chips(ui, catalog, model),
|
||||||
divider::full(ui),
|
divider::full(ui),
|
||||||
rows::content(ui, catalog, model, share, share.list_height(body_height)),
|
rows::content(ui, catalog, model, share, list_height),
|
||||||
]
|
]
|
||||||
.spacing(ui.space(Space::X2))
|
.spacing(ui.space(Space::X2))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|||||||
@@ -36,4 +36,4 @@ mod tests;
|
|||||||
mod view;
|
mod view;
|
||||||
|
|
||||||
pub use model::{InstallDraft, Loader, OptionsView};
|
pub use model::{InstallDraft, Loader, OptionsView};
|
||||||
pub(crate) use view::section;
|
pub(crate) use view::{section, version_menu_height};
|
||||||
@@ -325,4 +325,18 @@ impl OptionsView {
|
|||||||
pub fn loader_version_arg(&self) -> Option<&str> {
|
pub fn loader_version_arg(&self) -> Option<&str> {
|
||||||
(!self.loader_version.is_empty()).then_some(self.loader_version.as_str())
|
(!self.loader_version.is_empty()).then_some(self.loader_version.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 版本菜单里会有**几条**(`0` = 这一态不画菜单)。
|
||||||
|
///
|
||||||
|
/// 与 [`OptionsView::has_loader_versions`] 同源:判据一致(不是原版、没有静态原因、
|
||||||
|
/// 列表真的就位),所以 `>0` ⟺ `has_loader_versions()`。
|
||||||
|
/// 用途(M238):三栏档的面板预算按菜单高**推导**(`rows/tail.rs::panel_budget`)——
|
||||||
|
/// 菜单一行还是两行,差的是一整行列表(48px)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn loader_version_count(&self) -> usize {
|
||||||
|
match &self.versions {
|
||||||
|
LoaderVersionsView::Ready(items) if self.has_loader_versions() => items.len(),
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ use super::model::{Loader, LoaderVersionsView, OptionsView};
|
|||||||
/// 为什么按**条目数**取(M222 D30 r1 S1):LiteLoader 对 1.12.2 只有一条版本,原来固定撑到
|
/// 为什么按**条目数**取(M222 D30 r1 S1):LiteLoader 对 1.12.2 只有一条版本,原来固定撑到
|
||||||
/// 两行高(112px),那 48px 纯属空档,却正好是把上面版本列表挤成 2.4 行的元凶之一。
|
/// 两行高(112px),那 48px 纯属空档,却正好是把上面版本列表挤成 2.4 行的元凶之一。
|
||||||
/// 单测 `options/view::tests` 直接钉这两档的数值。
|
/// 单测 `options/view::tests` 直接钉这两档的数值。
|
||||||
pub(super) fn version_menu_height(items: usize) -> f32 {
|
pub(crate) fn version_menu_height(items: usize) -> f32 {
|
||||||
const ITEM: f32 = 48.0;
|
const ITEM: f32 = 48.0;
|
||||||
let rows = items.clamp(1, 2) as f32;
|
let rows = items.clamp(1, 2) as f32;
|
||||||
rows * ITEM + Space::X2.dp() * 2.0
|
rows * ITEM + Space::X2.dp() * 2.0
|
||||||
@@ -112,11 +112,11 @@ pub(super) fn version_menu_height(items: usize) -> f32 {
|
|||||||
///
|
///
|
||||||
/// D30 八审实测(1180×760,`Shared`):滚动区可见高 ≈96–104px,内容(head 40 + chips 32 +
|
/// D30 八审实测(1180×760,`Shared`):滚动区可见高 ≈96–104px,内容(head 40 + chips 32 +
|
||||||
/// 间距 4)≈76px ⇒ head(墨迹 462–501)与六枚 chips(506–537,32px 四角闭合)完整在首屏。
|
/// 间距 4)≈76px ⇒ head(墨迹 462–501)与六枚 chips(506–537,32px 四角闭合)完整在首屏。
|
||||||
/// M227 在 tip 上按同一条判据复量(视口 = 面板内容顶 → 钉死区上一行 − 8px 间距):
|
/// **M238 复量**(同一判据:视口 = 面板内容顶 → 钉死区上一行 − 8px 间距):1180×760 与 736×760
|
||||||
/// 1180×760 = 462→563 = **101px**、1180×900 = 594→703 = **109px**、窄档 720×760 = 539→659 =
|
/// 修前都只有 **54px**(这解释了「六枚 chip 只露 10px」——不是 736 独有),修后 **98px**;
|
||||||
/// **120px**(r2 复审量的 116px 落在这个区间内,差别只在「钉死区从哪一行算起」;三档都远大于
|
/// 900 档修前 62px、修后 106px。差数来源见 `rows/tail.rs::panel_budget`。
|
||||||
/// 首屏内容 76px,结论不变)。窄档那 120px 对首屏 104px(head 40 + 4 + chips 32 + 8 + 说明行 20)
|
/// 窄档 720×760 = **120px**,对首屏 104px(head 40 + 4 + chips 32 + 8 + 说明行 20)留 16px 余量
|
||||||
/// 留 16px 余量 ⇒ head、六枚 chips、说明行三者完整在首屏,版本菜单露出容器上沿(滚动条在右)。
|
/// ⇒ head、六枚 chips、说明行三者完整在首屏,版本菜单露出容器上沿(滚动条在右)。
|
||||||
pub(crate) fn section<'a>(
|
pub(crate) fn section<'a>(
|
||||||
ui: &'a Ui,
|
ui: &'a Ui,
|
||||||
catalog: Catalog,
|
catalog: Catalog,
|
||||||
@@ -354,7 +354,11 @@ fn version_list<'a>(
|
|||||||
LoaderVersionsView::Failed(_) => frame::gap(0.0),
|
LoaderVersionsView::Failed(_) => frame::gap(0.0),
|
||||||
LoaderVersionsView::Ready(items) if items.is_empty() => frame::gap(0.0),
|
LoaderVersionsView::Ready(items) if items.is_empty() => frame::gap(0.0),
|
||||||
LoaderVersionsView::Ready(items) => {
|
LoaderVersionsView::Ready(items) => {
|
||||||
let mut list = Menu::new().width(Length::Fill);
|
// **内联列表口径**(M238):这张菜单被套在下面的 `scrollable` 里,而 CPU 后端画
|
||||||
|
// quad 阴影时绕过裁切掩码 ⇒ L2 阴影会漏出视口、把钉死区的实例名压暗 7.2%
|
||||||
|
// (浅色画像布 `(249,249,250)` → `(231,231,232)`)。理由与两后端差异见
|
||||||
|
// `components::inputs::menu::shadow` 的 `flat` 文档。
|
||||||
|
let mut list = Menu::new().width(Length::Fill).flat();
|
||||||
for item in items {
|
for item in items {
|
||||||
let mut entry =
|
let mut entry =
|
||||||
MenuItem::new(item.as_str(), Message::SetLoaderVersion(item.clone()));
|
MenuItem::new(item.as_str(), Message::SetLoaderVersion(item.clone()));
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ pub(super) fn three_column<'a>(
|
|||||||
body_height: f32,
|
body_height: f32,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
// 分高方式与列表高度都从 `TailShare` 一处取(`Pinned` 档不给整行高度)。
|
// 分高方式与列表高度都从 `TailShare` 一处取(`Pinned` 档不给整行高度)。
|
||||||
let share = rows::TailShare::of(rails.tier, &super::install::panel_state(model));
|
// M238:面板预算按版本菜单的行数推,所以 `list_height` 也要拿到那个条数。
|
||||||
|
let state = super::install::panel_state(model);
|
||||||
|
let share = rows::TailShare::of(rails.tier, &state);
|
||||||
|
let list_height = share.list_height(body_height, rows::menu_items(&state));
|
||||||
row![
|
row![
|
||||||
panel(
|
panel(
|
||||||
ui,
|
ui,
|
||||||
@@ -90,15 +93,9 @@ pub(super) fn three_column<'a>(
|
|||||||
rails.major_rail,
|
rails.major_rail,
|
||||||
major_rail(ui, catalog, model, rails.major_rail)
|
major_rail(ui, catalog, model, rails.major_rail)
|
||||||
),
|
),
|
||||||
container(rows::content(
|
container(rows::content(ui, catalog, model, share, list_height))
|
||||||
ui,
|
.width(Length::Fixed(rails.list))
|
||||||
catalog,
|
.height(Length::Fill),
|
||||||
model,
|
|
||||||
share,
|
|
||||||
share.list_height(body_height),
|
|
||||||
))
|
|
||||||
.width(Length::Fixed(rails.list))
|
|
||||||
.height(Length::Fill),
|
|
||||||
]
|
]
|
||||||
.spacing(super::super::layout::rails::RAIL_GAP)
|
.spacing(super::super::layout::rails::RAIL_GAP)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|||||||
@@ -35,8 +35,10 @@
|
|||||||
//! (`tail::narrow_rows` + `CHIPS_PANEL_BUDGET = 192`):700/720×760 下列表 **132px = 3 整行**、
|
//! (`tail::narrow_rows` + `CHIPS_PANEL_BUDGET = 192`):700/720×760 下列表 **132px = 3 整行**、
|
||||||
//! 面板拿走其余(208px)。M227 之前这一档是「Fill 与 Fill 五五分」——面板只分到 164px,
|
//! 面板拿走其余(208px)。M227 之前这一档是「Fill 与 Fill 五五分」——面板只分到 164px,
|
||||||
//! 钉死区(说明 + 菜单 + 实例名 ≈180px)把它吃穿,滚动区剩 0、head / 六枚 chip 全不在屏。
|
//! 钉死区(说明 + 菜单 + 实例名 ≈180px)把它吃穿,滚动区剩 0、head / 六枚 chip 全不在屏。
|
||||||
//! 三栏档 + 带版本列表的选项区走另一支(`tail::whole_rows` + `PANEL_BUDGET = 300`):
|
//! 三栏档 + 带版本列表的选项区走另一支(`tail::whole_rows` + `tail::panel_budget`):
|
||||||
//! 1180×760 列表 **176px = 4 整行**、面板 ≈294px,那里菜单与说明都钉在面板底部。
|
//! 1180×760 列表 **132px = 3 整行**、面板 330px(滚动视口 98px),那里菜单与说明都钉在面板底部。
|
||||||
|
//! M238 之前那支用的是**死的** `PANEL_BUDGET = 300`:面板实得 286px,滚动视口只有 54px
|
||||||
|
//! ⇒ 六枚 chip 被裁成 10px 的上缘(736 与 1180 同病,纵向预算与窗宽无关)。
|
||||||
//! 「刚进页面还没选版本」(`Hint`,面板只有一行提示)**不共享**:那一态 main 本来就有
|
//! 「刚进页面还没选版本」(`Hint`,面板只有一行提示)**不共享**:那一态 main 本来就有
|
||||||
//! **7 整行**在屏(不是 8:第 8 条墨迹其实是 `Pick a version, then install` 那句 hint,
|
//! **7 整行**在屏(不是 8:第 8 条墨迹其实是 `Pick a version, then install` 那句 hint,
|
||||||
//! 横向范围对不上版本号;700×760 上放 8 行要 352px,实测只剩 309~322px),五五分会砍成
|
//! 横向范围对不上版本号;700×760 上放 8 行要 352px,实测只剩 309~322px),五五分会砍成
|
||||||
@@ -85,7 +87,7 @@ use super::states;
|
|||||||
|
|
||||||
mod tail;
|
mod tail;
|
||||||
|
|
||||||
pub(crate) use tail::TailShare;
|
pub(crate) use tail::{menu_items, TailShare};
|
||||||
|
|
||||||
/// 列表列(三栏档与 chips 档共用一份)。
|
/// 列表列(三栏档与 chips 档共用一份)。
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
//! 「列表与尾巴面板怎么分高」+ 版本行的整行取整(M125 / M222 D30 / M227)。
|
//! 「列表与尾巴面板怎么分高」+ 版本行的整行取整(M125 / M222 D30 / M227 / M238)。
|
||||||
//!
|
//!
|
||||||
//! 从 `rows.rs` 拆出来的理由只有一条:那边撞 400 行硬线(项目自带
|
//! 从 `rows.rs` 拆出来的理由只有一条:那边撞 400 行硬线(项目自带
|
||||||
//! `components::gate::every_source_file_stays_under_the_line_limit` 会判红)。本文件是
|
//! `components::gate::every_source_file_stays_under_the_line_limit` 会判红)。本文件是
|
||||||
//! [`TailShare`] 的**纯搬移**,加上 M222 新增的三个只读量([`ROW_PITCH`] / [`LIST_CHROME`] /
|
//! [`TailShare`] 的**纯搬移**,加上 M222 新增的三个只读量([`ROW_PITCH`] / [`LIST_CHROME`] /
|
||||||
//! [`whole_rows`])与 M227 的 chips 档预算([`CHIPS_STACK`] / [`CHIPS_LIST_CHROME`] /
|
//! [`whole_rows`])与 M227 的 chips 档预算([`CHIPS_STACK`] / [`CHIPS_LIST_CHROME`] /
|
||||||
//! [`CHIPS_PANEL_BUDGET`] / [`narrow_rows`])。
|
//! [`CHIPS_PANEL_BUDGET`] / [`narrow_rows`])。
|
||||||
|
//!
|
||||||
|
//! M238 把三栏档那个钉死的 `PANEL_BUDGET = 300` 换成**按面板内容推导**的 [`panel_budget`]
|
||||||
|
//! (首屏 + 钉死区 + 内外边距 + 列表↔面板那层),理由:那个常数不含 [`TAIL_CHROME`]、
|
||||||
|
//! 也不随版本菜单行数变,760/900 档下滚动视口只剩 54/62px,六枚 chip 被裁成 10px。
|
||||||
|
|
||||||
use crate::pages::layout::VersionRailTier;
|
use crate::pages::layout::VersionRailTier;
|
||||||
|
|
||||||
@@ -15,7 +19,7 @@ use crate::pages::download::install::PanelState;
|
|||||||
/// | 档位 × 面板形态 | 分法 | 为什么 |
|
/// | 档位 × 面板形态 | 分法 | 为什么 |
|
||||||
/// |---|---|---|
|
/// |---|---|---|
|
||||||
/// | `Wide` / `Narrow` / `Compact` + 面板**不带**加载器版本列表 | [`TailShare::Pinned`] | 原版面板矮(`04` 实测列表 6 行),整块在屏内 |
|
/// | `Wide` / `Narrow` / `Compact` + 面板**不带**加载器版本列表 | [`TailShare::Pinned`] | 原版面板矮(`04` 实测列表 6 行),整块在屏内 |
|
||||||
/// | 任何档 + 选项区**带加载器版本列表** | [`TailShare::Shared`]:面板先吃 [`PANEL_BUDGET`],剩下按整行给列表([`whole_rows`]) | 带版本列表的选项区实高 ≈294~307px;旧规则(对半分)把列表压到 110px,现在 1180×760 是 **176px = 4 整行** |
|
/// | 任何档 + 选项区**带加载器版本列表** | [`TailShare::Shared`]:面板先吃 [`panel_budget`],剩下按整行给列表([`whole_rows`]) | 面板要「首屏 76 + 钉死区(说明 + 菜单 + 实例名)」才算完整;旧规则(对半分)把列表压到 110px,M238 前那个常数 300 又把滚动视口挤到 54px(六枚 chip 只露 10px)。现在 1180×760 是 **132px = 3 整行**、视口 98px |
|
||||||
/// | `Chips` + 面板**占高度**(选项区展开 / 安装中) | [`TailShare::Narrow`]:面板先吃 [`CHIPS_PANEL_BUDGET`],剩下按整行给列表([`narrow_rows`]) | 窄档可用高比三栏档少两行 chip(459 vs 580)⇒ 只钉实例名,head/chips/说明/菜单进滚动区;700/720×760 列表 **132px = 3 整行** |
|
/// | `Chips` + 面板**占高度**(选项区展开 / 安装中) | [`TailShare::Narrow`]:面板先吃 [`CHIPS_PANEL_BUDGET`],剩下按整行给列表([`narrow_rows`]) | 窄档可用高比三栏档少两行 chip(459 vs 580)⇒ 只钉实例名,head/chips/说明/菜单进滚动区;700/720×760 列表 **132px = 3 整行** |
|
||||||
/// | 任何档 + 只画 `Hint`(没选版本,面板只有一行) | [`TailShare::Pinned`] | 一行提示不值得让面板吃预算:700×760 下 main 是 **7 整行**在屏(不是 8——第 8 条墨迹其实是 `Pick a version, then install` 那句 hint,横向范围对不上版本号;要放 8 行得 352px,实测只剩 309~322px),对半分只剩 **4 整行**、下面留 160px 空白(r1 复审 P2-1,r2 修回 7) |
|
/// | 任何档 + 只画 `Hint`(没选版本,面板只有一行) | [`TailShare::Pinned`] | 一行提示不值得让面板吃预算:700×760 下 main 是 **7 整行**在屏(不是 8——第 8 条墨迹其实是 `Pick a version, then install` 那句 hint,横向范围对不上版本号;要放 8 行得 352px,实测只剩 309~322px),对半分只剩 **4 整行**、下面留 160px 空白(r1 复审 P2-1,r2 修回 7) |
|
||||||
///
|
///
|
||||||
@@ -42,7 +46,7 @@ impl TailShare {
|
|||||||
/// 1. `Chips`(可用宽 < 600,多花两行 chip、竖排预算最紧的那一档)**且**面板真占高度
|
/// 1. `Chips`(可用宽 < 600,多花两行 chip、竖排预算最紧的那一档)**且**面板真占高度
|
||||||
/// (`Options` 选中了版本 / `Install` 进度或失败)⇒ [`TailShare::Narrow`];
|
/// (`Options` 选中了版本 / `Install` 进度或失败)⇒ [`TailShare::Narrow`];
|
||||||
/// 2. **任何档位**的 `Options` 只要 [`has_loader_versions`](super::super::options::OptionsView::has_loader_versions)
|
/// 2. **任何档位**的 `Options` 只要 [`has_loader_versions`](super::super::options::OptionsView::has_loader_versions)
|
||||||
/// (选项区带加载器版本列表 ⇒ 实高 ≈294~307px)⇒ [`TailShare::Shared`]。三栏档只走这条,
|
/// (选项区带加载器版本列表 ⇒ 钉死区 152~200px,见 [`panel_budget`])⇒ [`TailShare::Shared`]。三栏档只走这条,
|
||||||
/// chips 档被 arm 1 先接走;
|
/// chips 档被 arm 1 先接走;
|
||||||
/// 3. 其余 ⇒ [`TailShare::Pinned`]:不带版本列表的 `Options`(原版 / 不兼容 / 空表)与
|
/// 3. 其余 ⇒ [`TailShare::Pinned`]:不带版本列表的 `Options`(原版 / 不兼容 / 空表)与
|
||||||
/// `Hint`。
|
/// `Hint`。
|
||||||
@@ -59,9 +63,10 @@ impl TailShare {
|
|||||||
}
|
}
|
||||||
// M222 D30 r1/r2:**带加载器版本列表**的选项区在任何档位都太高。三栏档修前
|
// M222 D30 r1/r2:**带加载器版本列表**的选项区在任何档位都太高。三栏档修前
|
||||||
// 面板 204→372px,版本列表被压到 2.4 行(110px)且底缘切在第三行图标上。
|
// 面板 204→372px,版本列表被压到 2.4 行(110px)且底缘切在第三行图标上。
|
||||||
// 现在走 Shared:面板先吃 `PANEL_BUDGET`,列表按**整行**取(`whole_rows`,
|
// 现在走 Shared:面板先吃 `panel_budget`(按菜单行数推,M238),列表按**整行**取
|
||||||
// 标定 44/97)⇒ 1180×760 列表 **176px = 4 整行**、面板 ≈294px;
|
// (`whole_rows`,标定 44/97)⇒ 1180×760 列表 **132px = 3 整行**、面板 330px、
|
||||||
// 900 档 308px = 7 整行。实例名与版本菜单都在面板钉死区(`options::section`)。
|
// 滚动视口 98px(首屏 76 整块在屏);900 档 264px = 6 整行、视口 106px。
|
||||||
|
// 实例名与版本菜单都在面板钉死区(`options::section`)。
|
||||||
(_, PanelState::Options(options)) if options.has_loader_versions() => Self::Shared,
|
(_, PanelState::Options(options)) if options.has_loader_versions() => Self::Shared,
|
||||||
_ => Self::Pinned,
|
_ => Self::Pinned,
|
||||||
}
|
}
|
||||||
@@ -72,10 +77,13 @@ impl TailShare {
|
|||||||
/// `column_height` 两个分支的入参口径一致:都传
|
/// `column_height` 两个分支的入参口径一致:都传
|
||||||
/// [`frame::body_height`](crate::pages::frame::body_height) 的返回值
|
/// [`frame::body_height`](crate::pages::frame::body_height) 的返回值
|
||||||
/// (chips 档上方那两行 chip 由 [`narrow_rows`] 自己扣,调用点不用管)。
|
/// (chips 档上方那两行 chip 由 [`narrow_rows`] 自己扣,调用点不用管)。
|
||||||
pub(crate) fn list_height(self, column_height: f32) -> Option<f32> {
|
///
|
||||||
|
/// `menu_items` 只有 `Shared` 档用得上(面板预算按版本菜单的行数推,见 [`panel_budget`]);
|
||||||
|
/// 其余两支忽略它——调用点统一传 [`menu_items`] 的返回值即可。
|
||||||
|
pub(crate) fn list_height(self, column_height: f32, menu_items: usize) -> Option<f32> {
|
||||||
match self {
|
match self {
|
||||||
Self::Pinned => None,
|
Self::Pinned => None,
|
||||||
Self::Shared => Some(whole_rows(column_height)),
|
Self::Shared => Some(whole_rows(column_height, menu_items)),
|
||||||
Self::Narrow => Some(narrow_rows(column_height)),
|
Self::Narrow => Some(narrow_rows(column_height)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,34 +97,84 @@ impl TailShare {
|
|||||||
/// `Narrow` 两支都按整行取高,底缘落在行边界上。
|
/// `Narrow` 两支都按整行取高,底缘落在行边界上。
|
||||||
pub(crate) const ROW_PITCH: f32 = 44.0;
|
pub(crate) const ROW_PITCH: f32 = 44.0;
|
||||||
|
|
||||||
/// 三栏档「列表以外、列内要扣掉的固定占位」(**列顶 156 → 列表顶 253**,D30 三审实测 = 97)。
|
/// 三栏档「列表以外、列内要扣掉的固定占位」(**列顶 156 → 列表顶 253**,D30 三审实测 = 97
|
||||||
|
/// = 搜索条 56 + 间距 8 + 分隔线 1 + 间距 8 + 计数头 16 + 间距 8)。
|
||||||
///
|
///
|
||||||
/// 1180×760:正文列 580 ⇒ floor((580−97−300) / 44) × 44 = **176px = 4 整行**,分隔线落在 437。
|
/// 1180×760:正文列 580,面板按内容要 [`panel_budget`] ⇒ 列表 **132px = 3 整行**(M238 前是
|
||||||
/// 1180×900:floor((720−97−300) / 44) × 44 = **308px = 7 整行**(八审实测分隔线 569)。
|
/// 176px = 4 整行,代价见 [`panel_budget`])。
|
||||||
/// **改搜索条 / 表头高度时要同步这个数。**
|
/// **改搜索条 / 表头高度时要同步这个数。**
|
||||||
pub(crate) const LIST_CHROME: f32 = 97.0;
|
pub(crate) const LIST_CHROME: f32 = 97.0;
|
||||||
|
|
||||||
/// 三栏档面板(滚动区 + 钉死区)的**最小预算**。
|
/// 面板自身的上下内边距之和(`install::panel_with` 的 `Padding::from([Space::X3, 0])` = 12 + 12)。
|
||||||
|
pub(crate) const PANEL_PAD_V: f32 = 24.0;
|
||||||
|
|
||||||
|
/// 面板滚动区的**首屏**:head 40 + 间距 4 + 六枚 loader chip 32 = **76**。
|
||||||
///
|
///
|
||||||
/// 滚动区(八审后只剩 head 40 + 六枚 chips 32 + 间距 4 = **76**);
|
/// 「首屏整块在屏」是本档的硬下限:低于它,chips 就被视口裁成一条缝(M238 实测 736×760
|
||||||
/// 钉死区 = 说明/标签行 20 + 间距 8 + 菜单 64 + 间距 8 + 实例名 56 = **156**。
|
/// 修前只剩 **10px**),而用户看不出「下面还有六枚 chip」——那正是 M227 在 chips 档消灭过的形态。
|
||||||
/// 取 300 覆盖两者(76 + 156 = 232)再留余量给 chips 底边框与滚动条。
|
pub(crate) const FIRST_SCREEN: f32 = 76.0;
|
||||||
/// D30 五审实测:上一版「对半分」把滚动区压到 82px——宽档切「安装选项」字身、六枚 chip
|
|
||||||
/// 0 像素;窄档 24px 全被留白吃掉。窄档另有一套更紧的预算,见 [`CHIPS_PANEL_BUDGET`]。
|
/// 选项区里「滚动区 ↔ 钉死区」的间距(`options::section` 的 `Space::X2`)。
|
||||||
pub(crate) const PANEL_BUDGET: f32 = 300.0;
|
pub(crate) const SECTION_GAP: f32 = 8.0;
|
||||||
|
|
||||||
|
/// `Shared` 档钉死区里**除版本菜单以外**的固定占位 = 说明/标签 16 + 间距 8 + 间距 8 + 实例名 56。
|
||||||
|
pub(crate) const PINNED_CHROME: f32 = 88.0;
|
||||||
|
|
||||||
|
/// 列表与面板之间那一层固定占位(`rows::content` 的 `tail`:间距 8 + 分隔线 1 + 间距 12)。
|
||||||
|
pub(crate) const TAIL_CHROME: f32 = 21.0;
|
||||||
|
|
||||||
/// 正文列高度 → 列表视口的**整行高度**(给三栏档的 `Shared` 分支用)。
|
/// 正文列高度 → 列表视口的**整行高度**(给三栏档的 `Shared` 分支用)。
|
||||||
///
|
///
|
||||||
/// 分高规则从「对半分」改成「**面板先保预算,剩下的按整行给列表**」:760 档
|
/// 分高规则是「**面板按内容要够,剩下的按整行给列表**」——[`panel_budget`] 算的就是「够」
|
||||||
/// (正文列 580)⇒ 580 − 97 − 300 = 183 ⇒ floor(183/44)×44 = **176px = 4 整行**;
|
/// (首屏 76 + 钉死区 + 面板内外边距),公式本身从 D30 起没变,变的是分子。
|
||||||
/// 900 档 ⇒ 720 − 97 − 300 = 323 ⇒ **308px = 7 整行**(五审在 900 已验 7 行)。
|
///
|
||||||
/// 公式不变的部分是「列表只吃整行」,变的是分子——这正是塔要的「一次性预算重分配」。
|
/// | 正文列 | 菜单行数 | 列表 | 面板 | 滚动视口 |
|
||||||
|
/// |---|---|---|---|---|
|
||||||
|
/// | 580(760 窗) | 2 行(≥2 条版本) | **132 = 3 整行** | 330 | **98** |
|
||||||
|
/// | 580 | 1 行(只有 1 条版本) | 176 = 4 整行 | 286 | 102 |
|
||||||
|
/// | 720(900 窗) | 2 行 | 264 = 6 整行 | 338 | 106 |
|
||||||
|
/// | 720 | 1 行 | 308 = 7 整行 | 294 | 110 |
|
||||||
|
///
|
||||||
|
/// M238 之前这里钉死 `PANEL_BUDGET = 300`(不含 [`TAIL_CHROME`]):760/900 档面板实得
|
||||||
|
/// 286/294px,滚动视口被挤到 **54/62px**,六枚 chip 只露出 10px。预算必须**按内容推**,
|
||||||
|
/// 不能是一个跟菜单行数无关的常数。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub(crate) fn whole_rows(column_height: f32) -> f32 {
|
pub(crate) fn whole_rows(column_height: f32, menu_items: usize) -> f32 {
|
||||||
let room = (column_height - LIST_CHROME - PANEL_BUDGET).max(ROW_PITCH);
|
let room = (column_height - LIST_CHROME - panel_budget(menu_items)).max(ROW_PITCH);
|
||||||
(room / ROW_PITCH).floor() * ROW_PITCH
|
(room / ROW_PITCH).floor() * ROW_PITCH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 三栏档面板「首屏整块 + 钉死区完整」要多少高(含列表↔面板那层 [`TAIL_CHROME`])。
|
||||||
|
///
|
||||||
|
/// = 内边距 24 + 首屏 76 + 间距 8 + 钉死区(88 + 菜单高)+ 列表↔面板 21。
|
||||||
|
///
|
||||||
|
/// - 菜单 2 行(`≥2` 条加载器版本,`version_menu_height` 封顶两行)⇒ **329**;
|
||||||
|
/// - 菜单 1 行(只有一条)⇒ **281**。
|
||||||
|
///
|
||||||
|
/// 为什么把 [`TAIL_CHROME`] 算进来:它在**同一列**里吃高度,但不在面板盒子里。M238 前那个
|
||||||
|
/// 300 没算它,于是「预算 300」实际只换来 **286px** 的面板(300 − 21 那层,再算上整行取整
|
||||||
|
/// 多出的 7px),比面板需要的 308 少 22px——那 22px 就是从滚动视口里借的。
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn panel_budget(menu_items: usize) -> f32 {
|
||||||
|
PANEL_PAD_V
|
||||||
|
+ FIRST_SCREEN
|
||||||
|
+ SECTION_GAP
|
||||||
|
+ PINNED_CHROME
|
||||||
|
+ crate::pages::download::options::version_menu_height(menu_items)
|
||||||
|
+ TAIL_CHROME
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 面板里那张版本菜单有几条(决定菜单高、进而决定 [`panel_budget`])。
|
||||||
|
///
|
||||||
|
/// `0` = 这一态不画菜单(`Hint` / `Install` / 原版 / 不兼容 / 取不到版本表);`Shared` 档只在
|
||||||
|
/// `has_loader_versions()` 成立时才走,所以那里恒 ≥ 1。
|
||||||
|
pub(crate) fn menu_items(state: &PanelState<'_>) -> usize {
|
||||||
|
match state {
|
||||||
|
PanelState::Options(options) => options.loader_version_count(),
|
||||||
|
PanelState::Hint | PanelState::Install(_) => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// chips 档(可用宽 < 600)里 `rows::content` **上方**那一坨的实测高(M227)。
|
/// chips 档(可用宽 < 600)里 `rows::content` **上方**那一坨的实测高(M227)。
|
||||||
///
|
///
|
||||||
/// 分类 tabs 48 + 间距 8 + 主版本 chip 条 48 + 间距 8 + 分隔线 1 + 间距 8 = **121**
|
/// 分类 tabs 48 + 间距 8 + 主版本 chip 条 48 + 间距 8 + 分隔线 1 + 间距 8 = **121**
|
||||||
@@ -183,18 +241,78 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 三栏档那支不动(M227 回归钉子):1180×760 = 176(4 整行)、900 高 = 308(7 整行)。
|
/// **M238 的结构判据**:三栏档在 760 / 900 两档正文列下,面板的滚动视口都 ≥ 首屏 76。
|
||||||
|
///
|
||||||
|
/// 视口 = 面板高 − [`PANEL_PAD_V`] − [`SECTION_GAP`] − 钉死区([`PINNED_CHROME`] + 菜单高),
|
||||||
|
/// 面板高 = 正文列 − [`LIST_CHROME`] − 列表 − [`TAIL_CHROME`]。这条算术就是渲染层真正的
|
||||||
|
/// 分高链路(`rails::three_column` → `rows::content` → `install::panel_with` → `options::section`),
|
||||||
|
/// 每个加数都取自生产常量,不是测试里抄的副本。
|
||||||
|
///
|
||||||
|
/// 抓得住:把 `panel_budget` 退回「死的 300」(= M238 前的形态)⇒ 760 档视口 54、900 档 62,
|
||||||
|
/// 两条都红;把预算改成不含 [`TAIL_CHROME`] ⇒ 两者再各少 21;把 [`FIRST_SCREEN`] 调小
|
||||||
|
/// (比如有人以为首屏只需 head + chips = 72)不明显红,但那属于口径调整,不属于回归。
|
||||||
|
/// 抓不住:像素上 chips 到底露出几行(那要真图,见 `tests/options_shot/compact.rs`);
|
||||||
|
/// 也不管 chips 档(`narrow_rows` 那一支另有它的钉子)。
|
||||||
#[test]
|
#[test]
|
||||||
fn the_three_column_budget_is_unchanged() {
|
fn the_three_column_panel_keeps_its_first_screen_at_760_and_900() {
|
||||||
assert_eq!(whole_rows(580.0), 176.0);
|
let pinned = |items: usize| {
|
||||||
assert_eq!(whole_rows(720.0), 308.0);
|
PINNED_CHROME + crate::pages::download::options::version_menu_height(items)
|
||||||
|
};
|
||||||
|
let viewport = |body: f32, items: usize| {
|
||||||
|
let list = whole_rows(body, items);
|
||||||
|
let panel = body - LIST_CHROME - list - TAIL_CHROME;
|
||||||
|
panel - PANEL_PAD_V - SECTION_GAP - pinned(items)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 硬判据先跑:**滚动视口 ≥ 首屏**(这是这条测试存在的理由,先红先看)
|
||||||
|
for (body, items) in [(580.0, 3), (580.0, 1), (720.0, 3), (720.0, 1)] {
|
||||||
|
assert!(
|
||||||
|
viewport(body, items) >= FIRST_SCREEN,
|
||||||
|
"正文列 {body} / 菜单 {items} 条:滚动视口只有 {}px < 首屏 {FIRST_SCREEN}px ⇒ \
|
||||||
|
六枚 chip 会被裁成一条缝(M238 修前 760 档实测 54px / chips 只露 10px)",
|
||||||
|
viewport(body, items)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 紧接着钉住具体像素:它们让「视口够」不是靠偶然的取整余量。
|
||||||
|
// 760 窗(正文列 580):2 行菜单 = 3 整行列表;1 行菜单可以把第 4 行留下。
|
||||||
|
assert_eq!(
|
||||||
|
whole_rows(580.0, 3),
|
||||||
|
132.0,
|
||||||
|
"≥2 条版本 ⇒ 2 行菜单 ⇒ 3 整行列表"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
whole_rows(580.0, 1),
|
||||||
|
176.0,
|
||||||
|
"只有 1 条版本 ⇒ 菜单 1 行 ⇒ 4 整行列表"
|
||||||
|
);
|
||||||
|
assert_eq!(viewport(580.0, 3), 98.0);
|
||||||
|
assert_eq!(viewport(580.0, 1), 102.0);
|
||||||
|
|
||||||
|
// 900 窗(正文列 720)
|
||||||
|
assert_eq!(whole_rows(720.0, 3), 264.0);
|
||||||
|
assert_eq!(whole_rows(720.0, 1), 308.0);
|
||||||
|
assert_eq!(viewport(720.0, 3), 106.0);
|
||||||
|
assert_eq!(viewport(720.0, 1), 110.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 三栏档的预算**跟着菜单行数走**(M238):菜单多一行,预算多 48,列表少一行。
|
||||||
|
///
|
||||||
|
/// 抓得住:`whole_rows` 的 `menu_items` 参数被忽略(两档预算相等);`panel_budget` 漏掉
|
||||||
|
/// [`TAIL_CHROME`](329 → 308);`PINNED_CHROME` 写小(那会让视口虚高、真图露馅)。
|
||||||
|
#[test]
|
||||||
|
fn the_three_column_budget_follows_the_version_menu_height() {
|
||||||
|
assert_eq!(panel_budget(1), 281.0, "24 + 76 + 8 + 88 + 64 + 21");
|
||||||
|
assert_eq!(panel_budget(2), 329.0, "菜单封顶两行 ⇒ 112");
|
||||||
|
assert_eq!(panel_budget(3), panel_budget(2), "3 条以上也还是两行");
|
||||||
|
assert_eq!(panel_budget(2) - panel_budget(1), 48.0, "一行的差 = 项高");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `list_height` 把三档的「列表吃多少」收敛到一处(`Pinned` = 不给定高)。
|
/// `list_height` 把三档的「列表吃多少」收敛到一处(`Pinned` = 不给定高)。
|
||||||
#[test]
|
#[test]
|
||||||
fn list_height_follows_the_share() {
|
fn list_height_follows_the_share() {
|
||||||
assert_eq!(TailShare::Pinned.list_height(580.0), None);
|
assert_eq!(TailShare::Pinned.list_height(580.0, 0), None);
|
||||||
assert_eq!(TailShare::Shared.list_height(580.0), Some(176.0));
|
assert_eq!(TailShare::Shared.list_height(580.0, 3), Some(132.0));
|
||||||
assert_eq!(TailShare::Narrow.list_height(580.0), Some(132.0));
|
assert_eq!(TailShare::Narrow.list_height(580.0, 0), Some(132.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,54 @@
|
|||||||
//! Compact 档(可用宽 600~675)主版本栏的像素守门:M125 缺陷 2。
|
//! Compact 档(可用宽 600~675)的像素守门:M125 缺陷 2(主版本栏)+ M238(面板首屏 / 阴影越界)。
|
||||||
|
//!
|
||||||
|
//! M238 两条都钉在**同一张 736×760 图**上(`04-…-compact736.png`):
|
||||||
|
//! ①六枚 loader chip 整枚在屏(修前被面板预算挤得只剩 10px 高的上缘);
|
||||||
|
//! ②版本菜单的 L2 阴影不许漏出滚动视口、压暗下面钉死区的实例名(修前浅色画面布 `(249,249,250)`
|
||||||
|
//! 被压成 `(231,231,232)`,−7.2%)。
|
||||||
|
|
||||||
|
use unlucky_design::color::Role;
|
||||||
|
|
||||||
use super::{bands, feed, ui, Shot, COMPACT_SHOT};
|
use super::{bands, feed, ui, Shot, COMPACT_SHOT};
|
||||||
|
|
||||||
|
/// iced 的 `Color` → 截图里的 8 位 RGB(tiny-skia 侧就是 `round(分量 × 255)`,截图不带 α)。
|
||||||
|
fn rgb(color: iced::Color) -> [u8; 3] {
|
||||||
|
let channel = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||||
|
[channel(color.r), channel(color.g), channel(color.b)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `y` 这一行里有多少段(每段 ≥ [`PILL_RUN_MIN`])与 `color` 同色的连续像素。
|
||||||
|
///
|
||||||
|
/// 用来区分「一排药丸的描边」与「一条通栏分隔线」:两者可能是同一个颜色(分隔线也用
|
||||||
|
/// `OutlineVariant`),但药丸是**多段**、分隔线是**一段通到底**。
|
||||||
|
fn pill_runs(shot: &Shot, y: usize, left: usize, right: usize, color: [u8; 3]) -> usize {
|
||||||
|
let (mut runs, mut run) = (0, 0);
|
||||||
|
for x in left..right {
|
||||||
|
if shot.at(x, y) == color {
|
||||||
|
run += 1;
|
||||||
|
} else {
|
||||||
|
if run >= PILL_RUN_MIN {
|
||||||
|
runs += 1;
|
||||||
|
}
|
||||||
|
run = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if run >= PILL_RUN_MIN {
|
||||||
|
runs += 1;
|
||||||
|
}
|
||||||
|
runs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 药丸描边那一段的最小长度:32 高的圆角药丸(半径 16)在顶边/底边留下的直线段约 28~68px,
|
||||||
|
/// 取 16 足以跨过圆角收缩,又能甩掉侧边那 1px 竖线。
|
||||||
|
const PILL_RUN_MIN: usize = 16;
|
||||||
|
|
||||||
|
/// `y` 这一行在 `[left, right)` 里是否**整行同色**;是则给出那个颜色。
|
||||||
|
fn uniform_row(shot: &Shot, y: usize, left: usize, right: usize) -> Option<[u8; 3]> {
|
||||||
|
let color = shot.at(left, y);
|
||||||
|
(left..right)
|
||||||
|
.all(|x| shot.at(x, y) == color)
|
||||||
|
.then_some(color)
|
||||||
|
}
|
||||||
|
|
||||||
/// `04-…-compact736.png`(736×760,`ULL_DL_COMPACT_SHOT`)——**Compact 档主版本栏(缺陷 2)**。
|
/// `04-…-compact736.png`(736×760,`ULL_DL_COMPACT_SHOT`)——**Compact 档主版本栏(缺陷 2)**。
|
||||||
///
|
///
|
||||||
/// 修前(M123 取证;本文件作者在 736×760 那张图上逐项量过):主版本栏宽 64,`ListItem`
|
/// 修前(M123 取证;本文件作者在 736×760 那张图上逐项量过):主版本栏宽 64,`ListItem`
|
||||||
@@ -75,3 +122,124 @@ fn a_compact_tier_shot_draws_an_icon_for_the_first_major_entry() {
|
|||||||
64 宽的通栏行说明它又画成文字了"
|
64 宽的通栏行说明它又画成文字了"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **M238 ①**:736×760 下六枚 loader chip **整枚**在面板滚动视口里(不是一条 10px 的缝)。
|
||||||
|
///
|
||||||
|
/// 修前实测(M238,`before-736x760-*.png`):面板拿 286px、滚动视口只剩 **54px**
|
||||||
|
/// (head 40 + 间距 4 之后轮到 chips 只剩 10px),六枚 chip 的下边框一个像素都不在屏;
|
||||||
|
/// 修后(预算按面板内容推导,`rows/tail.rs::panel_budget`):视口 **98px**,chip 描边
|
||||||
|
/// 上下各一条(y=462 / y=493,高 31px)。
|
||||||
|
///
|
||||||
|
/// 抓得住:面板预算退回「死的 300」(视口 54 ⇒ 下边框消失,只剩上边框那一条描边行)、
|
||||||
|
/// 或 chip 被任何东西裁掉下缘(判据是「**药丸描边**行至少有两条、且相距 ≥ 28」——圆角药丸的
|
||||||
|
/// 上/下边框各是一排 ≥16px 的多段同色像素,裁掉任何一条都当场红;分隔线虽然同色但是**通栏
|
||||||
|
/// 一段**,用段数把两者分开)。
|
||||||
|
/// 抓不住:chip 的文案有没有被**横向**裁掉(要看字形宽度)、六枚到底是不是一行(那是
|
||||||
|
/// `rails.rs` 的单测)、hover/焦点态。
|
||||||
|
#[test]
|
||||||
|
fn a_compact_tier_shot_draws_the_whole_chip_row() {
|
||||||
|
let Some(path) = feed(COMPACT_SHOT) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let shot = Shot::load(&path);
|
||||||
|
let bands = bands(736.0);
|
||||||
|
let ui = ui();
|
||||||
|
let left = bands.list.0 + 8;
|
||||||
|
let right = bands.list.0 + bands.list.1 as usize - 8;
|
||||||
|
|
||||||
|
// 未选中的 chip 有 1px `OutlineVariant` 描边(选中项无描边,见 `selection/paint/chip.rs`)
|
||||||
|
let edge = rgb(ui.color(Role::OutlineVariant));
|
||||||
|
let strokes: Vec<usize> = (400..560)
|
||||||
|
.filter(|y| pill_runs(&shot, *y, left, right, edge) >= 2)
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
strokes.len() >= 2,
|
||||||
|
"chip 的描边行只有 {} 条({strokes:?}):六枚 chip 被面板视口裁成一条缝 \
|
||||||
|
—— 下边框一个像素都不在屏,用户看不出「下面还有加载器 chip」",
|
||||||
|
strokes.len()
|
||||||
|
);
|
||||||
|
let (top, bottom) = (strokes[0], strokes[strokes.len() - 1]);
|
||||||
|
assert!(
|
||||||
|
bottom - top >= 28,
|
||||||
|
"chip 的上下描边相距只有 {}px(y={top}..{bottom}):chip 本体 32 高,\
|
||||||
|
被裁的话两个描边行会挨在一起",
|
||||||
|
bottom - top
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **M238 ②**:版本菜单的 L2 阴影不许画到滚动视口之外(压暗下面钉死区的实例名)。
|
||||||
|
///
|
||||||
|
/// 修前实测(浅色):菜单容器 `(238,238,239)` 底下那条 8px 缝、以及实例名框内的空白带
|
||||||
|
/// 全被压成 `(231,231,232)`(−7.2%,与 M222/M227 视觉审记的「实例名被压暗 6.9%」同一处),
|
||||||
|
/// 深色画面布 `(15,21,15)` 被压成 `(14,20,14)`。压暗带的**长度跟布局有关**:修前的旧预算下
|
||||||
|
/// 是菜单底往下 54px 一整条;只把预算修好、阴影留着时缩到 8px(= blur 6 + offset 2 那圈)。
|
||||||
|
/// 所以两条修法各管一半,判据按「缝 + 框内都必须纯画布色」写,两种布局都抓得住。
|
||||||
|
/// 根因在 `iced_tiny_skia` 0.14:quad 阴影用 `draw_pixmap(.., mask: None)` 画
|
||||||
|
/// (`engine.rs:136-143`),绕过裁切掩码;`iced_wgpu` 那条走整层 scissor(`quad.rs:96-101`),
|
||||||
|
/// 于是同一棵树在 CPU / GPU 上画出来不一样。修法 = 不在这条滚动链里画阴影(`Menu::flat`)。
|
||||||
|
///
|
||||||
|
/// 抓得住:`Menu::flat` 被撤(阴影回来 ⇒ 缝与框内变成混合色)、α 被调大调小(只要非 0 就红)、
|
||||||
|
/// 或者有人把菜单挪出滚动区又不裁(`uniform_row` 的定位仍会命中实例名框)。
|
||||||
|
/// 抓不住:**滚动中**阴影会不会跟出来——静态图只有一帧,`pages::harness` 目前没有
|
||||||
|
/// `scroll_to` 的派发通道(`rows.rs::PANEL_SCROLL_ID` 的注释是「拿到了就能派」,实际没人写),
|
||||||
|
/// 所以「滚到中部/底部」这三态在本 mission 无法出图,只能靠「阴影根本不画」从机制上排除。
|
||||||
|
#[test]
|
||||||
|
fn a_compact_tier_shot_keeps_the_menu_shadow_inside_the_scroll_viewport() {
|
||||||
|
let Some(path) = feed(COMPACT_SHOT) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let shot = Shot::load(&path);
|
||||||
|
let bands = bands(736.0);
|
||||||
|
let left = bands.list.0 + 8;
|
||||||
|
let right = bands.list.0 + bands.list.1 as usize - 8;
|
||||||
|
let canvas = shot.at(bands.list.0 + 2, 12);
|
||||||
|
|
||||||
|
// 实例名框的两条通栏 1px 描边:整行同色,且「上 3 行」与「下 3 行」互为同色、又与它自己不同
|
||||||
|
// ——通栏细线才满足这个形状;菜单块的边界不满足(块内 3 行是选中项底色)。
|
||||||
|
let hairline = |y: usize| -> bool {
|
||||||
|
uniform_row(&shot, y, left, right).is_some_and(|color| {
|
||||||
|
matches!(
|
||||||
|
(uniform_row(&shot, y - 3, left, right), uniform_row(&shot, y + 3, left, right)),
|
||||||
|
(Some(above), Some(below)) if above == below && color != above
|
||||||
|
)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let hairlines: Vec<usize> = (shot.height / 2..shot.height - 16)
|
||||||
|
.filter(|y| hairline(*y))
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
hairlines.len() >= 2,
|
||||||
|
"找不到实例名输入框的上下描边(命中 {hairlines:?}):这条断言要靠它定位「钉死区」"
|
||||||
|
);
|
||||||
|
let field_top = hairlines[hairlines.len() - 2];
|
||||||
|
let field_bottom = hairlines[hairlines.len() - 1];
|
||||||
|
assert!(
|
||||||
|
field_bottom > field_top + 40,
|
||||||
|
"实例名框只有 {}px 高(y={field_top}..{field_bottom}):它被挤扁了,先修那一处再看阴影",
|
||||||
|
field_bottom - field_top + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
// ① 框上沿往上 8px = 「滚动视口 → 钉死区」的缝:必须**纯画布色**
|
||||||
|
let middle = (left + right) / 2;
|
||||||
|
for y in field_top - 8..field_top {
|
||||||
|
for x in [left + 8, middle, right - 16] {
|
||||||
|
assert_eq!(
|
||||||
|
shot.at(x, y),
|
||||||
|
canvas,
|
||||||
|
"y={y} x={x} 是 {:?} 而不是画布色 {canvas:?}:菜单的 L2 阴影漏出了滚动视口,\
|
||||||
|
压暗了视口下面那条 8px 的缝(M238 修前实测 −7.2%)",
|
||||||
|
shot.at(x, y)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ② 框内标签以下的空白带也不许被压暗(修前那里是 231 而不是 249)
|
||||||
|
for y in field_top + 36..field_bottom - 8 {
|
||||||
|
assert_eq!(
|
||||||
|
shot.at(right - 24, y),
|
||||||
|
canvas,
|
||||||
|
"y={y} 的实例名框内是 {:?} 而不是画布色 {canvas:?}:阴影画到了框里面",
|
||||||
|
shot.at(right - 24, y)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user