Merge branch 'feat/m227-narrow-window-layout-budget-narrowf'
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
a06b7ff4ec
11 files changed
+502
-173
No files matched your search
@@ -173,7 +173,7 @@ pub fn view<'a>(
|
|||||||
view_sized(ui, metrics, model, lang, 760.0)
|
view_sized(ui, metrics, model, lang, 760.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 同 [`view`],但显式给**窗口高度**(版本列表要按整行取高,见 [`rows::whole_rows`])。
|
/// 同 [`view`],但显式给**窗口高度**(版本列表要按整行取高,见 [`rows::TailShare::list_height`])。
|
||||||
pub fn view_sized<'a>(
|
pub fn view_sized<'a>(
|
||||||
ui: &'a Ui,
|
ui: &'a Ui,
|
||||||
metrics: FrameMetrics,
|
metrics: FrameMetrics,
|
||||||
@@ -247,18 +247,14 @@ fn body<'a>(
|
|||||||
rails::three_column(ui, catalog, model, rails, body_height)
|
rails::three_column(ui, catalog, model, rails, body_height)
|
||||||
} else {
|
} else {
|
||||||
// 三栏不成立(可用宽 < 600):类型与主版本原样退回顶部 chips,列表吃掉整条可用宽。
|
// 三栏不成立(可用宽 < 600):类型与主版本原样退回顶部 chips,列表吃掉整条可用宽。
|
||||||
// 高度分法跟着档位走:chips 档多花两行 chip,面板必须收回自己的一半(M125 缺陷 1)。
|
// 高度分法跟着档位走:chips 档多花两行 chip,面板必须收回自己的预算(M125 缺陷 1)。
|
||||||
|
// M227:这一档也改成「面板先保预算、列表按整行吃剩下的」——预算比三栏档紧得多
|
||||||
|
// (`rows::CHIPS_PANEL_BUDGET` 192 vs `PANEL_BUDGET` 300),清单见 `rows/tail.rs`。
|
||||||
|
let share = rows::TailShare::of(rails.tier, &install::panel_state(model));
|
||||||
column![
|
column![
|
||||||
rails::stacked_chips(ui, catalog, model),
|
rails::stacked_chips(ui, catalog, model),
|
||||||
divider::full(ui),
|
divider::full(ui),
|
||||||
rows::content(
|
rows::content(ui, catalog, model, share, share.list_height(body_height)),
|
||||||
ui,
|
|
||||||
catalog,
|
|
||||||
model,
|
|
||||||
rows::TailShare::of(rails.tier, &install::panel_state(model)),
|
|
||||||
// chips 档不按整行取高:实测那里已经是 4.0 整行、零残影,别动。
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
.spacing(ui.space(Space::X2))
|
.spacing(ui.space(Space::X2))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|||||||
@@ -10,15 +10,16 @@
|
|||||||
//! 所以列表独立滚动(`rows::list`),面板固定占尾部一块高度。
|
//! 所以列表独立滚动(`rows::list`),面板固定占尾部一块高度。
|
||||||
//!
|
//!
|
||||||
//! **M125 的例外**:chips 档(窗口 < 736,竖栏退回顶部 chip 行)里这块面板太贵——它按内容高
|
//! **M125 的例外**:chips 档(窗口 < 736,竖栏退回顶部 chip 行)里这块面板太贵——它按内容高
|
||||||
//! 钉死会把版本列表压成 18px 的缝。那一档改成「列表与面板各吃一半、面板自己可滚」
|
//! 钉死会把版本列表压成 18px 的缝。那一档改成「面板先保预算、列表按整行吃剩下的」
|
||||||
//! (`rows::TailShare::Shared`)——但只在**面板真的占高度**时(`Options` / `Install`);
|
//! (`rows::TailShare::Narrow`,预算见 `rows/tail.rs::CHIPS_PANEL_BUDGET`)——而且只在
|
||||||
//! 只画一行提示的 `Hint` 态仍钉死,否则 8 个版本的列表会被砍到 4 行(r1 复审 P2-1)。
|
//! **面板真的占高度**时(`Options` / `Install`);只画一行提示的 `Hint` 态仍钉死,
|
||||||
|
//! 否则 8 个版本的列表会被砍到 4 行(r1 复审 P2-1)。
|
||||||
//!
|
//!
|
||||||
//! ## 面板的滚动分层(M222 D30 六轮打磨后的实际形态)
|
//! ## 面板的滚动分层(M227 之后的实际形态)
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! 版本行列表 scrollable ← 第 1 层(任何档位都在)
|
//! 版本行列表 scrollable ← 第 1 层(任何档位都在)
|
||||||
//! └ 选项区 scrollable(Shared 档才有) ← 第 2 层(`options/view.rs::section`,只装 head + chips)
|
//! └ 选项区 scrollable(Shared / Narrow) ← 第 2 层(`options/view.rs::section`)
|
||||||
//! └ 加载器版本 menu 的 scrollable ← 第 3 层(`options/view.rs::version_menu_height`)
|
//! └ 加载器版本 menu 的 scrollable ← 第 3 层(`options/view.rs::version_menu_height`)
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
@@ -28,12 +29,15 @@
|
|||||||
//! **鼠标落在内层上滚轮会不会冒泡到外层,本仓仍没有实测通道**(静态图拍不到滚动嵌套)。
|
//! **鼠标落在内层上滚轮会不会冒泡到外层,本仓仍没有实测通道**(静态图拍不到滚动嵌套)。
|
||||||
//! iced 在指针悬停于内层时由内层消费滚动,所以「滚到底之后继续滚会带动外层」属**未验证行为**;
|
//! iced 在指针悬停于内层时由内层消费滚动,所以「滚到底之后继续滚会带动外层」属**未验证行为**;
|
||||||
//! 两个方向的坏结果分别是「面板卡住滚不动」与「一滚就跳到外层」。
|
//! 两个方向的坏结果分别是「面板卡住滚不动」与「一滚就跳到外层」。
|
||||||
|
//! **M227 加了新的嵌套**:窄档里版本菜单在选项区的滚动区**之内**(三栏档它在钉死区、不嵌套),
|
||||||
|
//! 于是窄档是「菜单 → 选项区 → (列表外层没有)」两级嵌套,这条未验项在窄档变得更相关。
|
||||||
//!
|
//!
|
||||||
//! ## 钉死页脚**已经做了**(M222 `6f87c8b` 起)
|
//! ## 钉死页脚(M222 `6f87c8b` 起,M227 收窄)
|
||||||
//!
|
//!
|
||||||
//! `options::section` 现在是「可滚的上半块 + 钉死的下半块」:钉死区内是**加载器版本菜单 +
|
//! `options::section` 是「可滚的上半块 + 钉死的下半块」:三栏档钉死区是**说明行 + 版本菜单 +
|
||||||
//! 实例名输入框**(D30 四审:菜单也钉住,否则它正好落在裁切线上)。所以「实例名被滚走」
|
//! 实例名**(D30 四审:菜单也钉住,否则它正好落在裁切线上);chips 档(`Narrow`)钉死区
|
||||||
//! 这个风险已经消除,只剩上面那条滚轮冒泡未验。
|
//! **只剩实例名**——窄档的 459px 装不下「钉住菜单」+「3 整行列表」两个都要(算术见
|
||||||
|
//! `rows/tail.rs::CHIPS_PANEL_BUDGET`),于是菜单进滚动区(滚得到、滚动条可见)。
|
||||||
//!
|
//!
|
||||||
//! ## 只有意图,没有乐观回显
|
//! ## 只有意图,没有乐观回显
|
||||||
//!
|
//!
|
||||||
@@ -78,6 +82,7 @@ use super::super::frame;
|
|||||||
use super::super::Message;
|
use super::super::Message;
|
||||||
use super::model::{Download, InstallView};
|
use super::model::{Download, InstallView};
|
||||||
use super::options::OptionsView;
|
use super::options::OptionsView;
|
||||||
|
use super::rows::TailShare;
|
||||||
|
|
||||||
/// 面板要画哪一种形态(**决策与画法分开**:决策是纯函数,单测直接钉它)。
|
/// 面板要画哪一种形态(**决策与画法分开**:决策是纯函数,单测直接钉它)。
|
||||||
///
|
///
|
||||||
@@ -110,50 +115,57 @@ pub(super) fn panel_state(model: &Download) -> PanelState<'_> {
|
|||||||
|
|
||||||
/// 底部的安装面板。
|
/// 底部的安装面板。
|
||||||
///
|
///
|
||||||
/// `shared` = 选项区走「面板先保预算、列表按整行吃剩下的」那一档(`rows::TailShare::Shared`
|
/// `share` = 分高方式(`rows::TailShare`,面板内部形态跟着它走):
|
||||||
/// 的带版本列表分支;分高规则见 `rows/tail.rs::whole_rows`,**不是**早期的「对半分」)。
|
|
||||||
/// 此时选项区自己分成两段(`options::section`):
|
|
||||||
///
|
///
|
||||||
/// - **滚动区**:head(版本 id + 安装按钮)+ 六枚加载器 chips;
|
/// - [`TailShare::Pinned`]:内容照原样铺开(面板按内容高钉在底部);
|
||||||
/// - **钉死区**:说明行(有说明时;否则是「加载器版本」标签)+ 版本菜单 + 实例名输入框。
|
/// - [`TailShare::Shared`](三栏档 + 带版本列表的选项区):
|
||||||
|
/// **滚动区** = head(版本 id + 安装按钮)+ 六枚加载器 chips;
|
||||||
|
/// **钉死区** = 说明行(有说明时;否则是「加载器版本」标签)+ 版本菜单 + 实例名输入框;
|
||||||
|
/// - [`TailShare::Narrow`](chips 档 + 面板占高度):
|
||||||
|
/// **滚动区** = head + 六枚 chips + 说明行 + 版本菜单;
|
||||||
|
/// **钉死区**只剩实例名输入框(理由与预算见 `rows/tail.rs::CHIPS_PANEL_BUDGET`)。
|
||||||
///
|
///
|
||||||
/// 菜单与说明分别是 D30 四审 / 八审搬进钉死区的(裁切位置由钉死区高度决定,与窗宽无关)。
|
/// 菜单在 `Shared` 档是钉住的(D30 四审:裁切位置由钉死区高度决定,与窗宽无关,760 下它会正好
|
||||||
|
/// 落在切线上),在 `Narrow` 档则**在滚动区里**——窄档没有多余高度同时钉住「菜单 + 3 整行列表」,
|
||||||
|
/// 而列表那 3 整行是 M125 像素守门的下限。两支的取舍都写在 `rows/tail.rs`。
|
||||||
///
|
///
|
||||||
/// **两个 Shared 分支的现实不同,别一概而论**(`rows/tail.rs::TailShare::of`):
|
/// **两支「面板先保预算」的分支(`Shared` / `Narrow`)的现实不同,别一概而论**
|
||||||
|
/// (`rows/tail.rs::TailShare::of`):
|
||||||
///
|
///
|
||||||
/// - **三栏档 + 带加载器版本列表**:`has_loader_versions()` 要求 `problem.is_none()`,所以这一支
|
/// - **三栏档 + 带加载器版本列表**(`Shared`):`has_loader_versions()` 要求 `problem.is_none()`,
|
||||||
/// 滚动区里**只会有 head + chips**,错误行与它不可达;
|
/// 所以这一支滚动区里**只会有 head + chips**,错误行与它不可达;
|
||||||
/// - **chips 档(可用宽 < 600)**:`TailShare::of` 对 `Options`/`Install` 无条件 `Shared`,
|
/// - **chips 档**(`Narrow`):`TailShare::of` 对 `Options`/`Install` 无条件 `Narrow`,
|
||||||
/// `problem` **可以是** `Some`(静态不兼容 / 版本列表取不到 / 空表三种),那时错误行**会在
|
/// `problem` **可以是** `Some`(静态不兼容 / 版本列表取不到 / 空表三种),那时错误行
|
||||||
/// 滚动区里出现**、可能被滚走;`show_versions()` 为假时钉死区只剩实例名(没有菜单、也没有
|
/// **在滚动区里**、可能被滚走;`show_versions()` 为假时滚动区只剩 head + chips + 错误行,
|
||||||
/// 说明那一行)。这个行为域正好是 M227 要重新设计的地方,这里只如实记录现状。
|
/// 钉死区只有实例名。
|
||||||
pub(super) fn panel<'a>(ui: &'a Ui, catalog: Catalog, model: &'a Download) -> Element<'a, Message> {
|
pub(super) fn panel<'a>(ui: &'a Ui, catalog: Catalog, model: &'a Download) -> Element<'a, Message> {
|
||||||
panel_with(ui, catalog, model, false)
|
panel_with(ui, catalog, model, TailShare::Pinned)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 同 [`panel`],但显式给「面板先保预算」这一档的标志(只有 `rows::content` 需要)。
|
/// 同 [`panel`],但显式给分高方式(只有 `rows::content` 需要)。
|
||||||
pub(super) fn panel_with<'a>(
|
pub(super) fn panel_with<'a>(
|
||||||
ui: &'a Ui,
|
ui: &'a Ui,
|
||||||
catalog: Catalog,
|
catalog: Catalog,
|
||||||
model: &'a Download,
|
model: &'a Download,
|
||||||
shared: bool,
|
share: TailShare,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let content = match panel_state(model) {
|
let content = match panel_state(model) {
|
||||||
PanelState::Install(install) => installing(ui, catalog, install),
|
PanelState::Install(install) => installing(ui, catalog, install),
|
||||||
// 选中版本之后**先给选项,再点「安装」**(M105:以前是选中就直接开工,等于没有加载器维度)
|
// 选中版本之后**先给选项,再点「安装」**(M105:以前是选中就直接开工,等于没有加载器维度)
|
||||||
PanelState::Options(options) => super::options::section(ui, catalog, options, shared),
|
PanelState::Options(options) => super::options::section(ui, catalog, options, share),
|
||||||
PanelState::Hint => hint(ui, catalog),
|
PanelState::Hint => hint(ui, catalog),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 上下各 12 的面板留白(`[X3, 0]` 的两元组是「垂直 / 水平」)。窄档预算把它算进去了。
|
||||||
let panel = container(content)
|
let panel = container(content)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.padding(Padding::from([ui.space(Space::X3), 0.0]));
|
.padding(Padding::from([ui.space(Space::X3), 0.0]));
|
||||||
// 面板先保预算档:面板吃满自己那份(`rows/tail.rs::whole_rows` 之后剩下的高度),
|
// 面板先保预算那两支:面板吃满自己那份(列表按整行取高之后剩下的高度),
|
||||||
// 选项区的 `scrollable` 才有确定高度可吃。
|
// 选项区的 `scrollable` 才有确定高度可吃。
|
||||||
if shared {
|
if matches!(share, TailShare::Pinned) {
|
||||||
panel.height(Length::Fill).into()
|
|
||||||
} else {
|
|
||||||
panel.into()
|
panel.into()
|
||||||
|
} else {
|
||||||
|
panel.height(Length::Fill).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
//! 草稿(选中版本 + 三个选项)住在 `App`,由 `refresh_view` 带进页面模型——与分类筛选同一个做法,
|
//! 草稿(选中版本 + 三个选项)住在 `App`,由 `refresh_view` 带进页面模型——与分类筛选同一个做法,
|
||||||
//! 因为 `refresh_view` 每次事件都会把整棵模型重建,页面自己存不住任何东西。
|
//! 因为 `refresh_view` 每次事件都会把整棵模型重建,页面自己存不住任何东西。
|
||||||
|
|
||||||
|
mod fade;
|
||||||
mod model;
|
mod model;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod s1_tests;
|
mod s1_tests;
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
//! 选项区滚动视口底部的**淡出遮罩**(M227 返工)。
|
||||||
|
//!
|
||||||
|
//! 单独一个文件只为守 400 行硬线(`components::gate::every_source_file_stays_under_the_line_limit`)。
|
||||||
|
|
||||||
|
use iced::widget::{column, container, space};
|
||||||
|
use iced::{Color, Element, Length};
|
||||||
|
|
||||||
|
use unlucky_design::elevation::MaterialLayer;
|
||||||
|
|
||||||
|
use crate::theme::Ui;
|
||||||
|
|
||||||
|
use super::super::frame;
|
||||||
|
use super::super::Message;
|
||||||
|
|
||||||
|
/// 窄档滚动视口底部的淡出高度(像素)。
|
||||||
|
///
|
||||||
|
/// 取值落在 8~12 这一段:视口在 660 处硬裁,版本菜单的容器上沿(639)露出来 21px 且**一个字形
|
||||||
|
/// 都没有**——D30 返工把它判成「空壳蓝条」(深色下容器 `(22,33,41)` vs 画布 `(9,21,28)`,
|
||||||
|
/// CR 1.99,像渲染错而不是「下面还有内容」)。12px 遮罩让最后 12px 单调渐变到画布色,
|
||||||
|
/// 留下 8px 圆角上沿做发现性提示。
|
||||||
|
pub(super) const VIEWPORT_FADE_PX: f32 = 12.0;
|
||||||
|
|
||||||
|
/// 滚动视口底部的**淡出遮罩**:把内容在最后 `height` 像素内渐变到**画布色**。
|
||||||
|
///
|
||||||
|
/// 为什么需要它:`scrollable` 会硬裁内容(渲染层的 `with_layer`),裁切位置落在什么上面是不确定的
|
||||||
|
/// ——窄档正落在版本菜单容器上沿,用户看到的是一条没有字形的色块,而不是「还能往下滚」。
|
||||||
|
///
|
||||||
|
/// 为什么是**画布色的 α 阶梯**而不是白/黑雾:遮罩的颜色必须等于它下方那块背景的颜色,否则
|
||||||
|
/// 浅色档会浮出一层灰幕、深色档会浮出白雾。`ui.layer(MaterialLayer::Canvas)` 就是页面底色
|
||||||
|
/// (含面板 α),于是浅色淡出到纸面、深色淡出到墨底,两档都不新增颜色;方向也对
|
||||||
|
/// (内容向背景淡出,不是叠一层高光)。
|
||||||
|
///
|
||||||
|
/// 为什么用 12 条 1px 实色块拼:iced 0.14 没有渐变原语。1px 步进的 α 斜坡在 100% DPI 下与
|
||||||
|
/// 连续渐变的差别在 1/255 量级——D30 的逐像素量测只会看到 α 单调、无硬边。
|
||||||
|
///
|
||||||
|
/// 形状上它必须**贴在视口内**(调用侧把它放在滚动区底部的 `stack!` 上):视口下沿是 660,
|
||||||
|
/// 钉死区的实例名从 668 起,所以遮罩压不到任何控件(M227 返工的零回退证据就是逐像素
|
||||||
|
/// 差异只有 648–660 这 12 行)。
|
||||||
|
pub(super) fn viewport_fade<'a>(ui: &'a Ui, height: f32) -> Element<'a, Message> {
|
||||||
|
let canvas = ui.layer(MaterialLayer::Canvas);
|
||||||
|
let band_height = height / VIEWPORT_FADE_STEPS as f32;
|
||||||
|
|
||||||
|
let mut bands = column![].width(Length::Fill);
|
||||||
|
for index in 0..VIEWPORT_FADE_STEPS {
|
||||||
|
let alpha = canvas.a * step_alpha(index);
|
||||||
|
bands = bands.push(
|
||||||
|
container(space::horizontal())
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fixed(band_height))
|
||||||
|
.style(move |_theme| frame::filled(Color { a: alpha, ..canvas })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
bands.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 遮罩的条数:每条高 `VIEWPORT_FADE_PX / VIEWPORT_FADE_STEPS` 像素(现在是 1px)。
|
||||||
|
///
|
||||||
|
/// 单测直接对**这一对生产常量** + [`step_alpha`] 断言,所以改步数或改曲线都会当场变红
|
||||||
|
/// ——M227 r2 复审实测过「单测空转」的形态:旧测试在函数体里又抄了一遍 `12`,改生产常量它照样绿。
|
||||||
|
pub(super) const VIEWPORT_FADE_STEPS: usize = 12;
|
||||||
|
|
||||||
|
/// 第 `index` 条(0 = 最上面、`VIEWPORT_FADE_STEPS - 1` = 视口底边)的**不透明度系数**。
|
||||||
|
///
|
||||||
|
/// 顶端 ≈0(几乎透出内容)→ 底端 = 1(完全是画布色,与视口外的背景接上);`t^1.6` 让前几条更淡,
|
||||||
|
/// 观感是「内容渐隐到背景」,不是一层均匀的纱。
|
||||||
|
pub(super) fn step_alpha(index: usize) -> f32 {
|
||||||
|
// t:0 < t <= 1(`index + 1` 让顶端不落在 0 上,否则第一条完全看不见)。
|
||||||
|
let t = (index + 1) as f32 / VIEWPORT_FADE_STEPS as f32;
|
||||||
|
t.powf(1.6)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// 遮罩的几何自检:**对生产常量断言**,不是对测试里抄的副本。
|
||||||
|
///
|
||||||
|
/// 抓得住(r2 复审指出的空转形态:旧测试在函数体里又抄了一遍 `12`,改生产常量照样绿):
|
||||||
|
/// `VIEWPORT_FADE_STEPS` 改成 6(每档 2px ⇒ 见得到阶梯)、改成不能整除高度的数(半像素带)、
|
||||||
|
/// `step_alpha` 写成常数(等于一层纯色幕)、把曲线拉直(`^1.0`,前几条不再更淡)、
|
||||||
|
/// 或让末项小于 1(视口底边与画布之间留一道缝)。
|
||||||
|
#[test]
|
||||||
|
fn the_fade_bands_are_one_pixel_and_the_ramp_is_convex_to_opaque() {
|
||||||
|
// ① 每条**恰好 1px**:这是设计口径(「12 条 1px 实色块」),也是「肉眼分不出阶梯」的前提。
|
||||||
|
// 换成 6 条就是 2px/条、换成 5 条还会出半像素带 —— 两种都当场红。
|
||||||
|
let band = VIEWPORT_FADE_PX / VIEWPORT_FADE_STEPS as f32;
|
||||||
|
assert_eq!(
|
||||||
|
VIEWPORT_FADE_STEPS as f32, VIEWPORT_FADE_PX,
|
||||||
|
"步数必须等于高度像素数:{VIEWPORT_FADE_PX}px 要 1px 一条,现在 {VIEWPORT_FADE_STEPS} 条 ⇒ {band}px/条"
|
||||||
|
);
|
||||||
|
assert_eq!(band, 1.0, "每条恰好 1px");
|
||||||
|
|
||||||
|
// ② α 严格递增、除末条外都不透明(末条必须是画布色本身,与视口外无缝接上)。
|
||||||
|
let mut previous = 0.0_f32;
|
||||||
|
let mut previous_delta = 0.0_f32;
|
||||||
|
for index in 0..VIEWPORT_FADE_STEPS - 1 {
|
||||||
|
let alpha = step_alpha(index);
|
||||||
|
let delta = alpha - previous;
|
||||||
|
assert!(
|
||||||
|
delta > 0.0,
|
||||||
|
"第 {index} 条的 α ({alpha}) 必须大于前一条 ({previous}):遮罩要单调变浓"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
alpha < 1.0,
|
||||||
|
"第 {index} 条就已不透明,后面 {VIEWPORT_FADE_STEPS} 条白给"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
delta >= previous_delta,
|
||||||
|
"第 {index} 条比前一条浓得更少({delta} < {previous_delta}):曲线要凸(t^1.6),否则顶端不再「几乎透出内容」"
|
||||||
|
);
|
||||||
|
previous = alpha;
|
||||||
|
previous_delta = delta;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
step_alpha(0) < 0.1,
|
||||||
|
"最上面一条 α = {} 不够淡:遮罩顶端要几乎透出内容",
|
||||||
|
step_alpha(0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
step_alpha(VIEWPORT_FADE_STEPS - 1),
|
||||||
|
1.0,
|
||||||
|
"最末一条必须是画布色本身(否则与视口外的背景之间有一道缝)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,14 +28,16 @@ fn version_menu_height_counts_whole_items_only() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// S1(D30 r1)的装配判据:**带加载器版本列表**的选项区在任何档位都与列表对半分。
|
/// S1(D30 r1)的装配判据:**带加载器版本列表**的选项区在三栏档与列表共享高度。
|
||||||
///
|
///
|
||||||
/// 实测背景(1180×760):选项区从 204px 长到 372px 时,版本列表被压成 2.4 行(110px),
|
/// 实测背景(1180×760):选项区从 204px 长到 372px 时,版本列表被压成 2.4 行(110px),
|
||||||
/// 底缘切在第三行图标上;对半分之后列表回到 4.6 行、面板自己可滚。
|
/// 底缘切在第三行图标上;改成分高之后列表回到 4.6 行、面板自己可滚。
|
||||||
///
|
///
|
||||||
/// 抓得住:有人把这条规则删掉(`TailShare::of` 只判 chips 档)——列表又会掉回 2 行;
|
/// M227 起 chips 档单独一支(`Narrow`):同样的「先保预算」思路,但钉死区只留实例名,
|
||||||
/// 或者把它放宽成「任何选项区都对半分」(原版没有版本列表时不该对半分,那会把 6 行列表
|
/// 因为窄档 459px 装不下「钉住菜单 + 3 整行列表」(算术见 `rows/tail.rs::CHIPS_PANEL_BUDGET`)。
|
||||||
/// 砍成 4 行)。
|
///
|
||||||
|
/// 抓得住:有人把 arm 2 删掉(`TailShare::of` 只判 chips 档)——三栏档列表又会掉回 2 行;
|
||||||
|
/// 或者把它放宽成「任何选项区都共享」(原版没有版本列表时不该共享,那会把 6 行列表砍成 4 行)。
|
||||||
#[test]
|
#[test]
|
||||||
fn tall_options_panel_shares_height_at_every_tier() {
|
fn tall_options_panel_shares_height_at_every_tier() {
|
||||||
use crate::pages::download::install::PanelState;
|
use crate::pages::download::install::PanelState;
|
||||||
@@ -61,14 +63,19 @@ fn tall_options_panel_shares_height_at_every_tier() {
|
|||||||
VersionRailTier::Compact,
|
VersionRailTier::Compact,
|
||||||
VersionRailTier::Chips,
|
VersionRailTier::Chips,
|
||||||
] {
|
] {
|
||||||
|
let expected = if tier == VersionRailTier::Chips {
|
||||||
|
TailShare::Narrow
|
||||||
|
} else {
|
||||||
|
TailShare::Shared
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
TailShare::of(tier, &PanelState::Options(&tall)),
|
TailShare::of(tier, &PanelState::Options(&tall)),
|
||||||
TailShare::Shared,
|
expected,
|
||||||
"{tier:?}:带版本列表的选项区必须与列表对半分"
|
"{tier:?}:带版本列表的选项区必须与列表共享高度"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原版没有版本列表 ⇒ 仍然钉死(三栏档不该无谓对半分)。
|
// 原版没有版本列表 ⇒ 仍然钉死(三栏档不该无谓共享)。
|
||||||
let plain_state = AppState::new();
|
let plain_state = AppState::new();
|
||||||
let plain = OptionsView::of(
|
let plain = OptionsView::of(
|
||||||
&version_row,
|
&version_row,
|
||||||
@@ -85,7 +92,7 @@ fn tall_options_panel_shares_height_at_every_tier() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不兼容(有 problem)时列表不画 ⇒ 也不该对半分。
|
// 不兼容(有 problem)时列表不画 ⇒ 也不该共享。
|
||||||
let blocked_state = state_with(LoaderKind::LiteLoader, "1.13", Ok(Vec::new()));
|
let blocked_state = state_with(LoaderKind::LiteLoader, "1.13", Ok(Vec::new()));
|
||||||
let blocked = OptionsView::of(
|
let blocked = OptionsView::of(
|
||||||
&row("1.13", VersionKind::Release),
|
&row("1.13", VersionKind::Release),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//! 选项区的控件树(M105)。
|
//! 选项区的控件树(M105;窄档形态 M227)。
|
||||||
//!
|
//!
|
||||||
//! ## 版面(424 正文列里的四段)
|
//! ## 版面(424 正文列里的四段,三栏档 `Shared`)
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! ── 滚动区(Shared 档)────────────────────────────
|
//! ── 滚动区 ──────────────────────────────────────
|
||||||
//! 1.12.2 [安装]
|
//! 1.12.2 [安装]
|
||||||
//! [原版][Forge][NeoForge][Fabric][Quilt][LiteLoader] ← 六枚一行
|
//! [原版][Forge][NeoForge][Fabric][Quilt][LiteLoader] ← 六枚一行
|
||||||
//! ── 钉死区 ──────────────────────────────────────
|
//! ── 钉死区 ──────────────────────────────────────
|
||||||
@@ -13,11 +13,29 @@
|
|||||||
//! [实例名 ]
|
//! [实例名 ]
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! - **钉死档**(原版等不带版本列表的面板)走另一条路:标签、说明、菜单、实例名都在同一列里
|
//! 窄档(chips 档,可用宽 < 600,`rows::TailShare::Narrow`)**钉死区只剩实例名**,说明行与
|
||||||
|
//! 菜单都进滚动区——那里的高度只够「钉住实例名 + 3 整行版本列表」,算术见
|
||||||
|
//! `rows/tail.rs::CHIPS_PANEL_BUDGET`:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! ── 滚动区(首屏 104px = head 40 + 间距 4 + chips 32 + 间距 8 + 说明 20)──
|
||||||
|
//! 1.12.2 [安装]
|
||||||
|
//! [原版][Forge][NeoForge][Fabric][Quilt][LiteLoader]
|
||||||
|
//! Vanilla parent only — Forge goes through the CLI
|
||||||
|
//! ┌ 1.12.2-SNAPSHOT ┐ ← 露在折线下方,滚得到(滚动条在右)
|
||||||
|
//! ── 视口底 12px 淡出遮罩(`fade::viewport_fade`)────
|
||||||
|
//! ── 钉死区 ──────────────────────────────────────
|
||||||
|
//! [实例名 ]
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! - **钉死档**(原版等不带版本列表的面板)走第三条路:标签、说明、菜单、实例名都在同一列里
|
||||||
//! 顺序铺开(`04` 基线图就是这一档,不能改);
|
//! 顺序铺开(`04` 基线图就是这一档,不能改);
|
||||||
//! - 不兼容时的原因行(Error 色)在滚动区末尾(**chips 档**可达;三栏档 + 带版本列表时
|
//! - 不兼容时的原因行(Error 色)在滚动区末尾(**窄档**可达;三栏档 + 带版本列表时
|
||||||
//! `problem` 恒为 `None`,所以那支看不到它);
|
//! `problem` 恒为 `None`,所以那支看不到它);
|
||||||
//! - 说明只有 LiteLoader 有(`Key::DownloadOptionsLiteLoaderVanillaOnly`)。
|
//! - 说明只有 LiteLoader 有(`Key::DownloadOptionsLiteLoaderVanillaOnly`);
|
||||||
|
//! - 窄档的滚动视口底边会硬裁内容(M227 返工前正切在版本菜单容器上沿,露出 21px 没有字形的
|
||||||
|
//! 色块),所以那里铺一条 12px 淡出遮罩把内容渐变到画布色——遮罩在视口内(≤660),
|
||||||
|
//! 压不到钉死区的实例名(≥668)。
|
||||||
//!
|
//!
|
||||||
//! ## 为什么加载器是**六枚一行**(M222 D30 r1 实测订正)
|
//! ## 为什么加载器是**六枚一行**(M222 D30 r1 实测订正)
|
||||||
//!
|
//!
|
||||||
@@ -32,7 +50,7 @@
|
|||||||
//! 现在的做法:五家永远都能点,选中不兼容的那家时下面出现一行具名原因,同时「安装」变成禁用态。
|
//! 现在的做法:五家永远都能点,选中不兼容的那家时下面出现一行具名原因,同时「安装」变成禁用态。
|
||||||
//! 于是「为什么不能装」和「怎么才能装」永远同屏。
|
//! 于是「为什么不能装」和「怎么才能装」永远同屏。
|
||||||
|
|
||||||
use iced::widget::{column, container, row, scrollable};
|
use iced::widget::{column, container, row, scrollable, stack};
|
||||||
use iced::{Alignment, Element, Length};
|
use iced::{Alignment, Element, Length};
|
||||||
|
|
||||||
use unlucky_design::color::Role;
|
use unlucky_design::color::Role;
|
||||||
@@ -49,7 +67,9 @@ use crate::app::components::selection::{chip, Chip};
|
|||||||
use crate::theme::Ui;
|
use crate::theme::Ui;
|
||||||
|
|
||||||
use super::super::frame;
|
use super::super::frame;
|
||||||
|
use super::super::rows::TailShare;
|
||||||
use super::super::Message;
|
use super::super::Message;
|
||||||
|
use super::fade;
|
||||||
use super::model::{Loader, LoaderVersionsView, OptionsView};
|
use super::model::{Loader, LoaderVersionsView, OptionsView};
|
||||||
|
|
||||||
// 加载器 chip 的顺序与集合**委派**给 `Loader::ALL`(原版 / Forge / NeoForge / Fabric /
|
// 加载器 chip 的顺序与集合**委派**给 `Loader::ALL`(原版 / Forge / NeoForge / Fabric /
|
||||||
@@ -72,26 +92,36 @@ pub(super) fn version_menu_height(items: usize) -> f32 {
|
|||||||
|
|
||||||
/// 选项区(面板的下半块):**可滚的上半块 + 钉死的下半块**。
|
/// 选项区(面板的下半块):**可滚的上半块 + 钉死的下半块**。
|
||||||
///
|
///
|
||||||
/// `scroll` = 面板此刻是不是「自己可滚」的那一档(`rows::TailShare::Shared`)。是的话:
|
/// `share` = 分高方式(`rows::TailShare`,三个形态见 `install::panel_with` 的文档):
|
||||||
///
|
///
|
||||||
/// - **滚动区**:head(版本 id + 安装按钮)+ 六枚加载器 chips(**chips 档**还可能有错误行,见下);
|
/// - `Pinned`(原版等不带版本列表的面板)走另一条路:标签、说明、菜单、实例名都在同一列里
|
||||||
/// - **钉死区**:说明行(有说明时;否则是「加载器版本」标签)+ 版本菜单 + 实例名输入框。
|
/// 顺序铺开(`04` 基线图就是这一档,不能改);
|
||||||
|
/// - `Shared`(三栏档 + 带版本列表):
|
||||||
|
/// - **滚动区**:head(版本 id + 安装按钮)+ 六枚加载器 chips;
|
||||||
|
/// - **钉死区**:说明行(有说明时;否则是「加载器版本」标签)+ 版本菜单 + 实例名输入框。
|
||||||
|
/// - `Narrow`(chips 档 + 面板占高度,M227):
|
||||||
|
/// - **滚动区**:head + 六枚 chips + 说明行 + 错误行 + **版本菜单**;
|
||||||
|
/// - **钉死区**:只有实例名输入框。
|
||||||
///
|
///
|
||||||
/// 为什么菜单也要钉(D30 四审):裁切位置由**钉死区高度**决定,与窗宽无关(1180 与 720 都切在
|
/// 为什么菜单在三栏档钉、在窄档滚:裁切位置由**钉死区高度**决定(D30 四审),
|
||||||
/// 同一个 y)。之前只钉实例名,菜单就正好落在切线上——760 下菜单容器 642–706 只有 18px 可见、
|
/// 760 下菜单曾正好落在切线上(容器 642–706 只有 18px 可见、条目文字一格没画出来)。
|
||||||
/// 条目文字一格都没画出来。把菜单一起钉住之后,它不再依赖首屏剩余高度分配,可达性由结构保证;
|
/// 三栏档把菜单一起钉住之后不再依赖首屏剩余高度分配;窄档没有那么多高度——
|
||||||
/// **两个 Shared 分支不一样**(`rows/tail.rs::TailShare::of`):
|
/// 钉住菜单 + 说明 + 实例名要 180~228px,再加首屏 head+chips 76 就把列表挤到 ≤1 整行,
|
||||||
/// - 三栏档 + 带版本列表:`has_loader_versions()` 要求 `problem.is_none()` ⇒ 滚动区只有
|
/// 而 M125 的像素守门要求 ≥3 整行。所以窄档只钉实例名,其余进滚动区(预算算术见
|
||||||
/// head + chips,错误行不可达;
|
/// `rows/tail.rs::CHIPS_PANEL_BUDGET`)。
|
||||||
/// - chips 档(可用宽 < 600,`of` 对 `Options`/`Install` 无条件 Shared):`problem` 可以是
|
///
|
||||||
/// `Some`,错误行**会进滚动区**、可能被滚走;`show_versions()` 为假时钉死区只剩实例名。
|
/// D30 八审实测(1180×760,`Shared`):滚动区可见高 ≈96–104px,内容(head 40 + chips 32 +
|
||||||
/// D30 八审实测:滚动区可见高 ≈96–104px,内容(head 40 + chips 32 + 间距 4)≈76px ⇒
|
/// 间距 4)≈76px ⇒ head(墨迹 462–501)与六枚 chips(506–537,32px 四角闭合)完整在首屏。
|
||||||
/// 1180×760 下 head(墨迹 462–501)与六枚 chips(506–537,32px 四角闭合)完整在首屏。
|
/// M227 在 tip 上按同一条判据复量(视口 = 面板内容顶 → 钉死区上一行 − 8px 间距):
|
||||||
|
/// 1180×760 = 462→563 = **101px**、1180×900 = 594→703 = **109px**、窄档 720×760 = 539→659 =
|
||||||
|
/// **120px**(r2 复审量的 116px 落在这个区间内,差别只在「钉死区从哪一行算起」;三档都远大于
|
||||||
|
/// 首屏内容 76px,结论不变)。窄档那 120px 对首屏 104px(head 40 + 4 + chips 32 + 8 + 说明行 20)
|
||||||
|
/// 留 16px 余量 ⇒ head、六枚 chips、说明行三者完整在首屏,版本菜单露出容器上沿(滚动条在右)。
|
||||||
pub(crate) fn section<'a>(
|
pub(crate) fn section<'a>(
|
||||||
ui: &'a Ui,
|
ui: &'a Ui,
|
||||||
catalog: Catalog,
|
catalog: Catalog,
|
||||||
options: &'a OptionsView,
|
options: &'a OptionsView,
|
||||||
scroll: bool,
|
share: TailShare,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let head = row![
|
let head = row![
|
||||||
container(frame::text_role(
|
container(frame::text_role(
|
||||||
@@ -109,8 +139,8 @@ pub(crate) fn section<'a>(
|
|||||||
|
|
||||||
// 钉死档(原版 / 没有加载器版本列表)**保持旧结构**:标签与说明都在列内。
|
// 钉死档(原版 / 没有加载器版本列表)**保持旧结构**:标签与说明都在列内。
|
||||||
// 为什么不一并改:`04` 是别处基线图的对照,它的面板矮、列表 6 行——把标签撤出会
|
// 为什么不一并改:`04` 是别处基线图的对照,它的面板矮、列表 6 行——把标签撤出会
|
||||||
// 让面板矮 26px,列表涨到 302px(6.86 行)反而切出半行。方案 B 只对 Shared 档生效。
|
// 让面板矮 26px,列表涨到 302px(6.86 行)反而切出半行。方案 B 只对共享档生效。
|
||||||
if !scroll {
|
if matches!(share, TailShare::Pinned) {
|
||||||
let mut plain = column![
|
let mut plain = column![
|
||||||
head,
|
head,
|
||||||
frame::text_role(
|
frame::text_role(
|
||||||
@@ -163,25 +193,32 @@ pub(crate) fn section<'a>(
|
|||||||
return plain.into();
|
return plain.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动区只剩 head + 六枚 chips(方案 B,D30 六审裁决;chips 档还可能有错误行,见 `section` 文档)。
|
// 共享档的滚动区以 head + 六枚 chips 起头(方案 B,D30 六审裁决;错误行见下)。
|
||||||
//
|
//
|
||||||
// 为什么把「安装选项」标签与中性说明都撤出滚动区:
|
// 为什么把「安装选项」标签撤出滚动区:五审 82px → 六审 103px 的视口里,
|
||||||
// 五审 82px → 六审 103px 的视口里,head(40) + 标签(17) + chips(32) + 两处间距(16) = 105px,
|
// head(40) + 标签(17) + chips(32) + 两处间距(16) = 105px,差 2px 就把 chip 的底边框裁掉;
|
||||||
// 差 2px 就把 chip 的底边框裁掉;再塞说明(20px)就差 27~29px,说明整块 0 像素。
|
// 再塞说明(20px)就差 27~29px,说明整块 0 像素。
|
||||||
// 并排标签(方案 C)在这里不可行:六枚 chip 实测已占 404px,正文列只有 424px,
|
// 并排标签(方案 C)在这里不可行:六枚 chip 实测已占 404px 正文列只有 424px,
|
||||||
// 加上「安装选项」必然横向溢出、把第六枚裁掉——那是拿一个已完成项换另一个。
|
// 加上「安装选项」必然横向溢出、把第六枚裁掉——那是拿一个已完成项换另一个。
|
||||||
// 所以按塔授权的 B 落地:**说明**搬到钉死区(见下面的 `pinned`);「安装选项」标签在
|
// 所以按塔授权的 B 落地:**说明**搬到钉死区(见下面的 `pinned`);「安装选项」标签在
|
||||||
// Shared 档**删掉**(不是搬——八审裁决,见下三行),Pinned 档保留。滚动区只装控件。
|
// 共享档**删掉**(不是搬——八审裁决,见下三行),Pinned 档保留。滚动区只装控件。
|
||||||
// Shared 档**没有**「安装选项」标签:这是设计决定(D30 八审裁决)——六枚 chip 自解释,
|
// 共享档**没有**「安装选项」标签:这是设计决定(D30 八审裁决)——六枚 chip 自解释,
|
||||||
// 且正文列只有 424px、六枚 chip 已占 404px,加标签必然横向裁掉最后一枚。
|
// 且正文列只有 424px、六枚 chip 已占 404px,加标签必然横向裁掉最后一枚。
|
||||||
// 原版等 Pinned 档仍保留该标签(见上面 `!scroll` 那条路)。
|
// 原版等 Pinned 档仍保留该标签(见上面 `Pinned` 那条路)。
|
||||||
|
let narrow = matches!(share, TailShare::Narrow);
|
||||||
let mut body = column![head, loader_row(ui, catalog, options)]
|
let mut body = column![head, loader_row(ui, catalog, options)]
|
||||||
.spacing(ui.space(Space::X1))
|
.spacing(ui.space(Space::X1))
|
||||||
.width(Length::Fill);
|
.width(Length::Fill);
|
||||||
|
|
||||||
// 中性说明**只在钉死区画一份**(八审:这里原先也画了一份,两处像素级重影)。
|
// 窄档:说明行(或「加载器版本」标签)与版本菜单**都进滚动区**——钉死区只剩实例名。
|
||||||
// 「加载器版本」这一段只在**真该有列表**时出现:原版没有、静态不兼容没有(原因在下面那行)、
|
// 顺序必须是 head → chips → 说明 → 菜单:预算(`CHIPS_PANEL_BUDGET`)保证前 104px 完整,
|
||||||
// 取不到也没有(同样在原因行)。留一个空标题只会让人以为界面缺了一块。
|
// 菜单露在折线下方(滚动条可见,滚得到)。
|
||||||
|
if narrow && show_versions(options) {
|
||||||
|
body = body.push(versions_label(ui, catalog, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中性说明在三栏档**只在钉死区画一份**(八审:这里原先也画了一份,两处像素级重影)。
|
||||||
|
// 窄档没有钉死的那一行(见上),所以它在这里画、也只画一次。
|
||||||
if let Some(problem) = &options.problem {
|
if let Some(problem) = &options.problem {
|
||||||
body = body.push(frame::text_role(
|
body = body.push(frame::text_role(
|
||||||
ui,
|
ui,
|
||||||
@@ -192,30 +229,14 @@ pub(crate) fn section<'a>(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 钉死区:加载器版本菜单 + 实例名(都不参与滚动)。见 `section` 的文档。
|
if narrow && show_versions(options) {
|
||||||
|
body = body.push(version_list(ui, catalog, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 钉死区:三栏档是「说明/标签 + 菜单 + 实例名」,窄档只剩实例名。见 `section` 的文档。
|
||||||
let mut pinned = column![].spacing(ui.space(Space::X2)).width(Length::Fill);
|
let mut pinned = column![].spacing(ui.space(Space::X2)).width(Length::Fill);
|
||||||
if show_versions(options) {
|
if !narrow && show_versions(options) {
|
||||||
// 菜单上方那一行:**有中性说明就显示说明**(它讲的是「这一家的叠法」,位置就在
|
pinned = pinned.push(versions_label(ui, catalog, options));
|
||||||
// 版本菜单正上方,语义对得上);没有说明时才退回通用标签。
|
|
||||||
// 注意标签不是「搬」下来的:Shared 档的「安装选项」标签是**删掉**的(八审裁决,见上)。
|
|
||||||
// 高度相同(BodySmall 与 LabelMedium 都是一行),所以钉死区不涨。
|
|
||||||
let line = match &options.note {
|
|
||||||
Some(note) => frame::text_role(
|
|
||||||
ui,
|
|
||||||
TypeRole::BodySmall,
|
|
||||||
false,
|
|
||||||
note.as_str(),
|
|
||||||
ui.color(Role::OnSurfaceVariant),
|
|
||||||
),
|
|
||||||
None => frame::text_role(
|
|
||||||
ui,
|
|
||||||
TypeRole::LabelMedium,
|
|
||||||
false,
|
|
||||||
catalog.text(Key::DownloadOptionsLoaderVersion),
|
|
||||||
ui.color(Role::OnSurfaceVariant),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
pinned = pinned.push(line);
|
|
||||||
pinned = pinned.push(version_list(ui, catalog, options));
|
pinned = pinned.push(version_list(ui, catalog, options));
|
||||||
}
|
}
|
||||||
pinned = pinned.push(text_field(
|
pinned = pinned.push(text_field(
|
||||||
@@ -227,16 +248,62 @@ pub(crate) fn section<'a>(
|
|||||||
Message::SetInstanceName,
|
Message::SetInstanceName,
|
||||||
));
|
));
|
||||||
|
|
||||||
column![
|
let scroll_area: Element<'a, Message> = scrollable(body)
|
||||||
scrollable(body)
|
.height(Length::Fill)
|
||||||
.height(Length::Fill)
|
.style(frame::scroll_style(ui))
|
||||||
.style(frame::scroll_style(ui)),
|
.into();
|
||||||
pinned,
|
|
||||||
]
|
// 窄档:视口底部铺一条 12px 淡出遮罩(M227 返工 ①,理由见 `fade::viewport_fade`)。
|
||||||
.spacing(ui.space(Space::X2))
|
// 三栏档不铺:那里滚动区内容恒为 head 40 + 间距 4 + chips 32 = 76px,而视口 ≈118px,
|
||||||
.height(Length::Fill)
|
// 永远不会出现「半截内容」;铺上去只是「画布盖画布」,反而动了 M222 已验的 1180 像素。
|
||||||
.width(Length::Fill)
|
let scroll_area: Element<'a, Message> = if narrow {
|
||||||
.into()
|
stack![
|
||||||
|
scroll_area,
|
||||||
|
container(fade::viewport_fade(ui, fade::VIEWPORT_FADE_PX))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.align_y(Alignment::End),
|
||||||
|
]
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
scroll_area
|
||||||
|
};
|
||||||
|
|
||||||
|
column![scroll_area, pinned]
|
||||||
|
.spacing(ui.space(Space::X2))
|
||||||
|
.height(Length::Fill)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 版本菜单上方那一行:**有中性说明就显示说明**(它讲的是「这一家的叠法」,位置就在版本菜单
|
||||||
|
/// 正上方,语义对得上);没有说明时才退回通用标签。
|
||||||
|
///
|
||||||
|
/// 高度相同(`BodySmall` 与 `LabelMedium` 都是一行),所以无论放滚动区还是钉死区都不改预算。
|
||||||
|
/// 注意标签不是「搬」下来的:共享档的「安装选项」标签是**删掉**的(八审裁决,见 `section`)。
|
||||||
|
fn versions_label<'a>(
|
||||||
|
ui: &'a Ui,
|
||||||
|
catalog: Catalog,
|
||||||
|
options: &'a OptionsView,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
match &options.note {
|
||||||
|
Some(note) => frame::text_role(
|
||||||
|
ui,
|
||||||
|
TypeRole::BodySmall,
|
||||||
|
false,
|
||||||
|
note.as_str(),
|
||||||
|
ui.color(Role::OnSurfaceVariant),
|
||||||
|
),
|
||||||
|
None => frame::text_role(
|
||||||
|
ui,
|
||||||
|
TypeRole::LabelMedium,
|
||||||
|
false,
|
||||||
|
catalog.text(Key::DownloadOptionsLoaderVersion),
|
||||||
|
ui.color(Role::OnSurfaceVariant),
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 加载器版本这一段要不要画(见 `section` 的注释)。
|
/// 加载器版本这一段要不要画(见 `section` 的注释)。
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ pub(super) fn three_column<'a>(
|
|||||||
rails: VersionRails,
|
rails: VersionRails,
|
||||||
body_height: f32,
|
body_height: f32,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
|
// 分高方式与列表高度都从 `TailShare` 一处取(`Pinned` 档不给整行高度)。
|
||||||
|
let share = rows::TailShare::of(rails.tier, &super::install::panel_state(model));
|
||||||
row![
|
row![
|
||||||
panel(
|
panel(
|
||||||
ui,
|
ui,
|
||||||
@@ -92,8 +94,8 @@ pub(super) fn three_column<'a>(
|
|||||||
ui,
|
ui,
|
||||||
catalog,
|
catalog,
|
||||||
model,
|
model,
|
||||||
rows::TailShare::of(rails.tier, &super::install::panel_state(model)),
|
share,
|
||||||
Some(rows::whole_rows(body_height)),
|
share.list_height(body_height),
|
||||||
))
|
))
|
||||||
.width(Length::Fixed(rails.list))
|
.width(Length::Fixed(rails.list))
|
||||||
.height(Length::Fill),
|
.height(Length::Fill),
|
||||||
|
|||||||
@@ -29,11 +29,14 @@
|
|||||||
//! 都在面板钉死区,见 `options/view.rs::section`)。
|
//! 都在面板钉死区,见 `options/view.rs::section`)。
|
||||||
//! 同样的 760 高窗口里:三栏档列表还剩 158px(3 整行),chips 档只剩 18px。
|
//! 同样的 760 高窗口里:三栏档列表还剩 158px(3 整行),chips 档只剩 18px。
|
||||||
//!
|
//!
|
||||||
//! 修法是 [`TailShare`] 两档:**不带加载器版本列表**的选项区在三栏档照旧钉死(面板整块在屏内),
|
//! 修法是 [`TailShare`] 三档:**不带加载器版本列表**的选项区在三栏档照旧钉死(面板整块在屏内),
|
||||||
//! chips 档**且面板真的占高度**
|
//! chips 档**且面板真的占高度**
|
||||||
//! (选项区展开 / 安装中)时改成「**面板先保预算、剩下按整行给列表**」
|
//! (选项区展开 / 安装中)时改成「**面板先保预算、剩下按整行给列表**」
|
||||||
//! (`tail::whole_rows` + `PANEL_BUDGET = 300`):1180×760 下列表 **176px = 4 整行**,
|
//! (`tail::narrow_rows` + `CHIPS_PANEL_BUDGET = 192`):700/720×760 下列表 **132px = 3 整行**、
|
||||||
//! 面板拿走其余(≈307px)。实例名那格**已经钉死**在面板底部、不需要滚(M222 `6f87c8b`)。
|
//! 面板拿走其余(208px)。M227 之前这一档是「Fill 与 Fill 五五分」——面板只分到 164px,
|
||||||
|
//! 钉死区(说明 + 菜单 + 实例名 ≈180px)把它吃穿,滚动区剩 0、head / 六枚 chip 全不在屏。
|
||||||
|
//! 三栏档 + 带版本列表的选项区走另一支(`tail::whole_rows` + `PANEL_BUDGET = 300`):
|
||||||
|
//! 1180×760 列表 **176px = 4 整行**、面板 ≈294px,那里菜单与说明都钉在面板底部。
|
||||||
//! 「刚进页面还没选版本」(`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),五五分会砍成
|
||||||
@@ -82,13 +85,14 @@ use super::states;
|
|||||||
|
|
||||||
mod tail;
|
mod tail;
|
||||||
|
|
||||||
pub(crate) use tail::{whole_rows, TailShare};
|
pub(crate) use tail::TailShare;
|
||||||
|
|
||||||
/// 列表列(三栏档与 chips 档共用一份)。
|
/// 列表列(三栏档与 chips 档共用一份)。
|
||||||
///
|
///
|
||||||
/// `share` 决定「列表」与「尾部面板」怎么分这块高度(真实分支见 `tail::TailShare::of`):
|
/// `share` 决定「列表」与「尾部面板」怎么分这块高度(真实分支见 `tail::TailShare::of`):
|
||||||
/// `Pinned` = 面板按内容高钉在底部、列表吃剩下的;`Shared` = 面板先吃 `tail::PANEL_BUDGET`、
|
/// `Pinned` = 面板按内容高钉在底部、列表吃剩下的;`Shared` / `Narrow` = 面板先吃预算、
|
||||||
/// 列表按**整行**吃剩下的(`tail::whole_rows`)。哪些组合走哪一支由那一处决定,这里不重复判。
|
/// 列表按**整行**吃剩下的(`tail::whole_rows` / `tail::narrow_rows`,两者差别在面板内部
|
||||||
|
/// 钉什么,见 `tail::CHIPS_PANEL_BUDGET`)。哪些组合走哪一支由那一处决定,这里不重复判。
|
||||||
pub(super) fn content<'a>(
|
pub(super) fn content<'a>(
|
||||||
ui: &'a Ui,
|
ui: &'a Ui,
|
||||||
catalog: Catalog,
|
catalog: Catalog,
|
||||||
@@ -105,40 +109,38 @@ pub(super) fn content<'a>(
|
|||||||
} else if visible.is_empty() {
|
} else if visible.is_empty() {
|
||||||
states::empty(ui, catalog)
|
states::empty(ui, catalog)
|
||||||
} else {
|
} else {
|
||||||
// 只有 `Shared`(面板先保预算)那一档才按整行取高:`Pinned` 档(面板矮)实测已经
|
// 只有「面板先保预算」那两支(`Shared` / `Narrow`)才按整行取高:`Pinned` 档(面板矮)
|
||||||
// 是整行(`04` 图),取整反而会把它从 6 行缩到 5 行。
|
// 实测已经是整行(`04` 图),取整反而会把它从 6 行缩到 5 行。
|
||||||
let height = if matches!(share, TailShare::Shared) {
|
let height = if matches!(share, TailShare::Pinned) {
|
||||||
list_height
|
|
||||||
} else {
|
|
||||||
None
|
None
|
||||||
|
} else {
|
||||||
|
list_height
|
||||||
};
|
};
|
||||||
list(ui, catalog, model, &visible, height)
|
list(ui, catalog, model, &visible, height)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 列表与尾部面板之间**总是**有一条分隔线(三态一致):M123 的取证图里「选好了版本」
|
// 列表与尾部面板之间**总是**有一条分隔线(三态一致):M123 的取证图里「选好了版本」
|
||||||
// 那一态原先没有它,版本行与选项区第一行(版本号 + 安装按钮)读起来像同一块内容。
|
// 那一态原先没有它,版本行与选项区第一行(版本号 + 安装按钮)读起来像同一块内容。
|
||||||
let shared = matches!(share, TailShare::Shared);
|
|
||||||
let tail: Element<'a, Message> = column![
|
let tail: Element<'a, Message> = column![
|
||||||
divider::full(ui),
|
divider::full(ui),
|
||||||
install::panel_with(ui, catalog, model, shared)
|
install::panel_with(ui, catalog, model, share)
|
||||||
]
|
]
|
||||||
.spacing(ui.space(Space::X3))
|
.spacing(ui.space(Space::X3))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
// chips 档 / 带版本列表的选项区:面板收回自己的一半。
|
// chips 档 / 带版本列表的选项区:面板吃满自己那份。
|
||||||
//
|
//
|
||||||
// `PanelState::Options` 走**面板内部**的滚动(上半块可滚 + 实例名钉页脚,见 `install::panel`),
|
// `PanelState::Options` 走**面板内部**的滚动(上半块可滚 + 实例名钉页脚,见 `install::panel`),
|
||||||
// 所以这里不再整块套一层 scrollable——那一层曾让窄窗出现两条并列滚动条,也因为把实例名
|
// 所以这里不再整块套一层 scrollable——那一层曾让窄窗出现两条并列滚动条,也因为把实例名
|
||||||
// 卷进滚动区而让它掉到首屏外(D30 r2 S7)。其它面板(安装中/失败)内容短,保留外层滚动。
|
// 卷进滚动区而让它掉到首屏外(D30 r2 S7)。其它面板(安装中/失败)内容短,保留外层滚动。
|
||||||
let tail: Element<'a, Message> = match (share, panel_state(model)) {
|
let tail: Element<'a, Message> = match (share, panel_state(model)) {
|
||||||
(TailShare::Shared, PanelState::Options(_)) => tail,
|
(TailShare::Pinned, _) | (_, PanelState::Options(_)) => tail,
|
||||||
(TailShare::Shared, _) => scrollable(tail)
|
(TailShare::Shared | TailShare::Narrow, _) => scrollable(tail)
|
||||||
.id(iced::widget::Id::new(PANEL_SCROLL_ID))
|
.id(iced::widget::Id::new(PANEL_SCROLL_ID))
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.style(frame::scroll_style(ui))
|
.style(frame::scroll_style(ui))
|
||||||
.into(),
|
.into(),
|
||||||
(TailShare::Pinned, _) => tail,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
column![
|
column![
|
||||||
@@ -273,7 +275,8 @@ fn list<'a>(
|
|||||||
.scroller_width(frame::SCROLLBAR_WIDTH)
|
.scroller_width(frame::SCROLLBAR_WIDTH)
|
||||||
.margin(frame::SCROLLBAR_MARGIN),
|
.margin(frame::SCROLLBAR_MARGIN),
|
||||||
))
|
))
|
||||||
// `Some` = 三栏档的 `Shared` 分支按**整行**取高(见 `tail::whole_rows`);`None` = 其它(含 chips 档)用 Fill。
|
// `Some` = 面板先保预算那一档按**整行**取高(三栏档 `tail::whole_rows` /
|
||||||
|
// chips 档 `tail::narrow_rows`);`None` = `Pinned`(面板矮)用 Fill。
|
||||||
.height(height.map_or(Length::Fill, Length::Fixed))
|
.height(height.map_or(Length::Fill, Length::Fixed))
|
||||||
.style(frame::scroll_style(ui))
|
.style(frame::scroll_style(ui))
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
@@ -1,85 +1,111 @@
|
|||||||
//! 「列表与尾巴面板怎么分高」+ 版本行的整行取整(M125 / M222 D30)。
|
//! 「列表与尾巴面板怎么分高」+ 版本行的整行取整(M125 / M222 D30 / M227)。
|
||||||
//!
|
//!
|
||||||
//! 从 `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`],见 `rows.rs::content` 的用法)。
|
//! [`whole_rows`])与 M227 的 chips 档预算([`CHIPS_STACK`] / [`CHIPS_LIST_CHROME`] /
|
||||||
|
//! [`CHIPS_PANEL_BUDGET`] / [`narrow_rows`])。
|
||||||
|
|
||||||
use crate::pages::layout::VersionRailTier;
|
use crate::pages::layout::VersionRailTier;
|
||||||
|
|
||||||
use crate::pages::download::install::PanelState;
|
use crate::pages::download::install::PanelState;
|
||||||
|
|
||||||
/// 列表与尾部面板怎么分这块高度(M125 缺陷 1 的修法)。
|
/// 列表与尾部面板怎么分这块高度(M125 缺陷 1 的修法;M227 拆出 chips 档一支)。
|
||||||
///
|
///
|
||||||
/// | 档 | 分法 | 为什么 |
|
/// | 档位 × 面板形态 | 分法 | 为什么 |
|
||||||
/// |---|---|---|
|
/// |---|---|---|
|
||||||
/// | `Wide` / `Narrow` / `Compact` + 面板**不带**加载器版本列表 | [`TailShare::Pinned`]:面板按内容高钉在底部,列表吃剩下的 | 原版面板矮(`04` 实测列表 6 行),整块在屏内 |
|
/// | `Wide` / `Narrow` / `Compact` + 面板**不带**加载器版本列表 | [`TailShare::Pinned`] | 原版面板矮(`04` 实测列表 6 行),整块在屏内 |
|
||||||
/// | 任何档 + 选项区**带加载器版本列表** | [`TailShare::Shared`]:面板先吃 [`PANEL_BUDGET`],剩下按整行给列表([`whole_rows`]) | 带版本列表的选项区实高 ≈307px,对半分(旧规则)把列表压到 110px;现在 1180×760 是 **176px = 4 整行** |
|
/// | 任何档 + 选项区**带加载器版本列表** | [`TailShare::Shared`]:面板先吃 [`PANEL_BUDGET`],剩下按整行给列表([`whole_rows`]) | 带版本列表的选项区实高 ≈294~307px;旧规则(对半分)把列表压到 110px,现在 1180×760 是 **176px = 4 整行** |
|
||||||
/// | `Chips` + 面板**占高度**(选项区展开 / 安装中) | [`TailShare::Shared`] | chips 档比三栏档多花两行 chip,列表只剩 **18px**(M123 实测 0.4 行)——版本号读不出来就换不了版本 |
|
/// | `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) |
|
||||||
///
|
///
|
||||||
/// 分高方式按「档位 + 面板形态」判;**具体像素**由窗口高度算(`pages/download.rs::view_sized`
|
/// 分高方式按「档位 + 面板形态」判;**具体像素**由窗口高度算(`pages/download.rs::view_sized`
|
||||||
/// 把 `model.window_height` 经 `frame::body_height` 传下来,见 [`whole_rows`])。
|
/// 把 `model.window_height` 经 `frame::body_height` 传下来,见 [`whole_rows`] / [`narrow_rows`])。
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum TailShare {
|
pub(crate) enum TailShare {
|
||||||
/// 面板钉在底部(三栏档)。
|
/// 面板钉在底部(面板矮的档位)。
|
||||||
Pinned,
|
Pinned,
|
||||||
/// 面板先吃预算、列表按整行吃剩下的(带版本列表的选项区 / chips 档占高度时)。
|
/// 三栏档 + 带版本列表的选项区:面板先吃预算、列表按整行吃剩下的;钉死区含**说明/标签
|
||||||
|
/// + 版本菜单 + 实例名**(D30 四审/八审判定:1180 档高度够,菜单钉住最省事)。
|
||||||
Shared,
|
Shared,
|
||||||
|
/// chips 档(可用宽 < 600)+ 面板占高度:同一套「先保预算、列表按整行」,
|
||||||
|
/// 但面板钉死区**只留实例名**,head / 六枚 chip / 说明 / 版本菜单全在滚动区
|
||||||
|
/// (理由见 [`CHIPS_PANEL_BUDGET`]:459 里装不下「钉住菜单」+「3 整行列表」)。
|
||||||
|
Narrow,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TailShare {
|
impl TailShare {
|
||||||
/// 档位 + **面板形态** → 分高方式(纯函数,单测直接钉它)。
|
/// 档位 + **面板形态** → 分高方式(纯函数,单测直接钉它)。
|
||||||
///
|
///
|
||||||
/// 两个条件必须同时成立才共享:
|
/// 三条 arm,**不是**「两个条件同时成立」才共享(M227 重写:旧文档只写 M125 那一条,
|
||||||
/// 1. 档位是 `Chips`(多花两行 chip,竖排预算最紧的那一档);
|
/// 把 arm 2 的「任何档位」漏了 —— r8 复审 improve finding):
|
||||||
/// 2. 面板真的**占高度**——`Options`(选中了版本、选项区展开)或 `Install`(进度 / 失败)。
|
/// 1. `Chips`(可用宽 < 600,多花两行 chip、竖排预算最紧的那一档)**且**面板真占高度
|
||||||
|
/// (`Options` 选中了版本 / `Install` 进度或失败)⇒ [`TailShare::Narrow`];
|
||||||
|
/// 2. **任何档位**的 `Options` 只要 [`has_loader_versions`](super::super::options::OptionsView::has_loader_versions)
|
||||||
|
/// (选项区带加载器版本列表 ⇒ 实高 ≈294~307px)⇒ [`TailShare::Shared`]。三栏档只走这条,
|
||||||
|
/// chips 档被 arm 1 先接走;
|
||||||
|
/// 3. 其余 ⇒ [`TailShare::Pinned`]:不带版本列表的 `Options`(原版 / 不兼容 / 空表)与
|
||||||
|
/// `Hint`。
|
||||||
///
|
///
|
||||||
/// `Hint`(刚进页面还没选版本)只是**一行**提示(≈30px),对半分会让列表从 **7 整行**
|
/// `Hint`(刚进页面还没选版本)只是**一行**提示(≈30px),让它吃预算会把列表从 **7 整行**
|
||||||
/// 掉到 **4 整行**、下半块留 ≈160px 空画布(r1 复审 P2-1 实测:main 在 700×760 的
|
/// 砍到 **4 整行**、下半块留 ≈160px 空画布(r1 复审 P2-1 实测:main 在 700×760 的
|
||||||
/// `Hint` 态是 7 整行——不是 8,第 8 条墨迹其实是 `Pick a version, then install` 那句
|
/// `Hint` 态是 7 整行——不是 8,第 8 条墨迹其实是 `Pick a version, then install` 那句
|
||||||
/// hint,横向范围对不上版本号;r2 修回 7 整行)。所以 `Hint` **在任何档位都钉死**。
|
/// hint,横向范围对不上版本号;r2 修回 7 整行)。所以 `Hint` **在任何档位都钉死**。
|
||||||
pub(crate) fn of(tier: VersionRailTier, state: &PanelState<'_>) -> Self {
|
pub(crate) fn of(tier: VersionRailTier, state: &PanelState<'_>) -> Self {
|
||||||
match (tier, state) {
|
match (tier, state) {
|
||||||
|
// M227:chips 档(窄档)单独一支 —— 同一份「先保预算」思路,但钉死区只留实例名。
|
||||||
(VersionRailTier::Chips, PanelState::Options(_) | PanelState::Install(_)) => {
|
(VersionRailTier::Chips, PanelState::Options(_) | PanelState::Install(_)) => {
|
||||||
Self::Shared
|
Self::Narrow
|
||||||
}
|
}
|
||||||
// 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`,列表按**整行**取(`whole_rows`,
|
||||||
// 标定 44/97)⇒ 1180×760 列表 **176px = 4 整行**、面板 ≈307px;
|
// 标定 44/97)⇒ 1180×760 列表 **176px = 4 整行**、面板 ≈294px;
|
||||||
// 900 档 308px = 7 整行。实例名与版本菜单都在面板钉死区(`options::section`)。
|
// 900 档 308px = 7 整行。实例名与版本菜单都在面板钉死区(`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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 列表视口该拿多少高(`None` = 面板按内容高钉死,列表吃剩下的 `Fill`)。
|
||||||
|
///
|
||||||
|
/// `column_height` 两个分支的入参口径一致:都传
|
||||||
|
/// [`frame::body_height`](crate::pages::frame::body_height) 的返回值
|
||||||
|
/// (chips 档上方那两行 chip 由 [`narrow_rows`] 自己扣,调用点不用管)。
|
||||||
|
pub(crate) fn list_height(self, column_height: f32) -> Option<f32> {
|
||||||
|
match self {
|
||||||
|
Self::Pinned => None,
|
||||||
|
Self::Shared => Some(whole_rows(column_height)),
|
||||||
|
Self::Narrow => Some(narrow_rows(column_height)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 版本行的**实测节距**(D30 三审:`04` 图行内下载图标的行首 265/309/353/397/441/485,
|
/// 版本行的**实测节距**(D30 三审:`04` 图行内下载图标的行首 265/309/353/397/441/485,
|
||||||
/// 节距恰 **44.0**;首行选中高亮矩形 253–296 也是 44px)。
|
/// 节距恰 **44.0**;首行选中高亮矩形 253–296 也是 44px)。
|
||||||
///
|
///
|
||||||
/// 列表是 `Fill + scrollable` 时,视口高度不是节距整数倍就会切在某一行中间——D30 r2 实测
|
/// 列表是 `Fill + scrollable` 时,视口高度不是节距整数倍就会切在某一行中间——D30 r2 实测
|
||||||
/// `01` 是 245px(5.3 行),英文态露出 6px 竖干、中文态露出「预览版」上半截。三栏档的
|
/// `01` 是 245px(5.3 行),英文态露出 6px 竖干、中文态露出「预览版」上半截。`Shared` 与
|
||||||
/// `Shared`(面板先保预算)那一支现在按整行取高([`whole_rows`]),底缘落在行边界上。
|
/// `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)。
|
||||||
///
|
///
|
||||||
/// 1180×760:正文列 580 ⇒ floor((580−97−300) / 44) × 44 = **176px = 4 整行**,分隔线落在 437。
|
/// 1180×760:正文列 580 ⇒ floor((580−97−300) / 44) × 44 = **176px = 4 整行**,分隔线落在 437。
|
||||||
/// 1180×900:floor((720−97−300) / 44) × 44 = **308px = 7 整行**(八审实测分隔线 569)。
|
/// 1180×900:floor((720−97−300) / 44) × 44 = **308px = 7 整行**(八审实测分隔线 569)。
|
||||||
/// **改搜索条 / 表头高度时要同步这个数。**
|
/// **改搜索条 / 表头高度时要同步这个数。**
|
||||||
pub(crate) const LIST_CHROME: f32 = 97.0;
|
pub(crate) const LIST_CHROME: f32 = 97.0;
|
||||||
|
|
||||||
/// 面板(滚动区 + 钉死区)的**最小预算**。
|
/// 三栏档面板(滚动区 + 钉死区)的**最小预算**。
|
||||||
///
|
///
|
||||||
/// 滚动区(八审后只剩 head 40 + 六枚 chips 32 + 间距 4 = **76**);
|
/// 滚动区(八审后只剩 head 40 + 六枚 chips 32 + 间距 4 = **76**);
|
||||||
/// 钉死区 = 说明/标签行 17 + 间距 8 + 菜单 64 + 间距 8 + 实例名 56 ≈ **153**。
|
/// 钉死区 = 说明/标签行 20 + 间距 8 + 菜单 64 + 间距 8 + 实例名 56 = **156**。
|
||||||
/// 取 300 覆盖两者(76 + 153 = 229)再留余量给 chips 底边框与滚动条。
|
/// 取 300 覆盖两者(76 + 156 = 232)再留余量给 chips 底边框与滚动条。
|
||||||
/// D30 五审实测:上一版「对半分」把滚动区压到 82px——宽档切「安装选项」字身、六枚 chip
|
/// D30 五审实测:上一版「对半分」把滚动区压到 82px——宽档切「安装选项」字身、六枚 chip
|
||||||
/// 0 像素;窄档 24px 全被留白吃掉。
|
/// 0 像素;窄档 24px 全被留白吃掉。窄档另有一套更紧的预算,见 [`CHIPS_PANEL_BUDGET`]。
|
||||||
pub(crate) const PANEL_BUDGET: f32 = 300.0;
|
pub(crate) const PANEL_BUDGET: f32 = 300.0;
|
||||||
|
|
||||||
/// 正文列高度 → 列表视口的**整行高度**(只给三栏档的 Shared 分支用)。
|
/// 正文列高度 → 列表视口的**整行高度**(给三栏档的 `Shared` 分支用)。
|
||||||
///
|
///
|
||||||
/// 分高规则从「对半分」改成「**面板先保预算,剩下的按整行给列表**」:760 档
|
/// 分高规则从「对半分」改成「**面板先保预算,剩下的按整行给列表**」:760 档
|
||||||
/// (正文列 580)⇒ 580 − 97 − 300 = 183 ⇒ floor(183/44)×44 = **176px = 4 整行**;
|
/// (正文列 580)⇒ 580 − 97 − 300 = 183 ⇒ floor(183/44)×44 = **176px = 4 整行**;
|
||||||
@@ -90,3 +116,85 @@ pub(crate) fn whole_rows(column_height: f32) -> f32 {
|
|||||||
let room = (column_height - LIST_CHROME - PANEL_BUDGET).max(ROW_PITCH);
|
let room = (column_height - LIST_CHROME - PANEL_BUDGET).max(ROW_PITCH);
|
||||||
(room / ROW_PITCH).floor() * ROW_PITCH
|
(room / ROW_PITCH).floor() * ROW_PITCH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// chips 档(可用宽 < 600)里 `rows::content` **上方**那一坨的实测高(M227)。
|
||||||
|
///
|
||||||
|
/// 分类 tabs 48 + 间距 8 + 主版本 chip 条 48 + 间距 8 + 分隔线 1 + 间距 8 = **121**
|
||||||
|
/// (720×760 实测:正文列顶 156 → `rows::content` 顶 277)。
|
||||||
|
/// **改 `rails::stacked_chips` 的高度时同步这个数**(`Element` 没有几何内省,只能实测 + 常量)。
|
||||||
|
pub(crate) const CHIPS_STACK: f32 = 121.0;
|
||||||
|
|
||||||
|
/// chips 档里 `rows::content` 自身的固定占位(M227 实测)。
|
||||||
|
///
|
||||||
|
/// 列表之前:搜索条 56 + 间距 8 + 分隔线 1 + 间距 8 + 计数头 17 + 间距 8 = 98
|
||||||
|
/// (实测内容顶 277 → 列表顶 375);列表之后还要给尾部:间距 8 + 分隔线 1 + 间距 12 = 21
|
||||||
|
/// (实测列表底 551 → 分隔线 559 → 面板顶 572)⇒ **119**。
|
||||||
|
pub(crate) const CHIPS_LIST_CHROME: f32 = 119.0;
|
||||||
|
|
||||||
|
/// chips 档里选项区**至少要拿到**的高度(M227 的窄档预算)。
|
||||||
|
///
|
||||||
|
/// 窄档的取舍:钉死区只留实例名,其余全进滚动区(`options::section` 的 `Narrow` 一支)。
|
||||||
|
/// 预算 = 上下内边距 12×2 + 首屏内容 104 + 滚动区↔钉死区间距 8 + 实例名 56 = **192**;
|
||||||
|
/// 其中首屏 104 = head 40 + 间距 4 + 六枚 chip 32 + 间距 8 + 说明/标签行 20。
|
||||||
|
///
|
||||||
|
/// 为什么菜单在窄档**不钉**(三栏档是钉的):700/720×760 的 `rows::content` 只有 **459**,
|
||||||
|
/// 若钉死区还要含菜单(说明 20 + 8 + 菜单 64~112 + 8 + 实例名 56 + 内边距 24 = 180~228),
|
||||||
|
/// 再加首屏 head+chips 76 就是 256~304,列表只剩 ≤1 整行 —— 而 M125 的像素守门
|
||||||
|
/// (`tests/options_shot/chips.rs` ②)要求 **≥3 整行 = 132**。两者在 459 里互斥,
|
||||||
|
/// 所以窄档把菜单放进滚动区(滚得到、滚动条可见),钉死区只留实例名。
|
||||||
|
///
|
||||||
|
/// 五审实测的坏形态(本预算要消灭的):滚动区视口被钉死区吃成 **0**(五审记的「24px」其实是
|
||||||
|
/// 面板内边距 12 + 间距 8 + 说明行顶端),head / 六枚 chip / 说明一个像素都不在屏;
|
||||||
|
/// 实例名输入框被挤成 **35px**(五审记 36),值墨迹的降部被自己的底边框裁掉。
|
||||||
|
#[doc(alias = "窄档预算")]
|
||||||
|
pub(crate) const CHIPS_PANEL_BUDGET: f32 = 192.0;
|
||||||
|
|
||||||
|
/// 正文列高度 → chips 档列表视口的**整行高度**(M227)。
|
||||||
|
///
|
||||||
|
/// 720×760:580 − 121([`CHIPS_STACK`])− 119([`CHIPS_LIST_CHROME`])− 192([`CHIPS_PANEL_BUDGET`])
|
||||||
|
/// = 148 ⇒ floor(148/44)×44 = **132px = 3 整行**;面板拿走剩下的 208(滚动区 120 ≥ 首屏 104)。
|
||||||
|
/// 与 [`whole_rows`] 同一条公式,只是分子换成窄档的三段固定占位。
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn narrow_rows(column_height: f32) -> f32 {
|
||||||
|
let room =
|
||||||
|
(column_height - CHIPS_STACK - CHIPS_LIST_CHROME - CHIPS_PANEL_BUDGET).max(ROW_PITCH);
|
||||||
|
(room / ROW_PITCH).floor() * ROW_PITCH
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// 窄档预算的算术(M227):760 档恰好 3 整行、面板剩下的高度够首屏 + 实例名。
|
||||||
|
///
|
||||||
|
/// 抓得住:把预算改小(列表涨到 4 整行 ⇒ 滚动区 < 首屏 104,说明行被切)或改大
|
||||||
|
/// (列表掉到 2 整行 ⇒ M125 的像素守门 ② 红)。
|
||||||
|
#[test]
|
||||||
|
fn the_chips_budget_keeps_three_whole_rows_at_760() {
|
||||||
|
let body = 580.0; // 760 − 标题栏 48 − 页头 84 − 上下页边距 48
|
||||||
|
assert_eq!(narrow_rows(body), 132.0, "720×760 的 chips 档列表 = 3 整行");
|
||||||
|
let panel = body - CHIPS_STACK - CHIPS_LIST_CHROME - narrow_rows(body);
|
||||||
|
assert_eq!(panel, 208.0, "面板拿剩下的那块");
|
||||||
|
// 滚动区 = 面板 − 上下内边距 24 − 实例名 56 − 滚动区↔钉死区间距 8
|
||||||
|
assert!(
|
||||||
|
panel - 24.0 - 56.0 - 8.0 >= 104.0,
|
||||||
|
"滚动区 {} < 首屏 104(head 40 + 4 + chips 32 + 8 + 说明 20)",
|
||||||
|
panel - 24.0 - 56.0 - 8.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 三栏档那支不动(M227 回归钉子):1180×760 = 176(4 整行)、900 高 = 308(7 整行)。
|
||||||
|
#[test]
|
||||||
|
fn the_three_column_budget_is_unchanged() {
|
||||||
|
assert_eq!(whole_rows(580.0), 176.0);
|
||||||
|
assert_eq!(whole_rows(720.0), 308.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `list_height` 把三档的「列表吃多少」收敛到一处(`Pinned` = 不给定高)。
|
||||||
|
#[test]
|
||||||
|
fn list_height_follows_the_share() {
|
||||||
|
assert_eq!(TailShare::Pinned.list_height(580.0), None);
|
||||||
|
assert_eq!(TailShare::Shared.list_height(580.0), Some(176.0));
|
||||||
|
assert_eq!(TailShare::Narrow.list_height(580.0), Some(132.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,8 @@ use super::{bands, feed, ui, Shot, CHIPS_SHOT};
|
|||||||
/// 修前(M123 取证,2026-11-05,tiny-skia,700×760,`ULL_DL_DEMO=options-fabric`):
|
/// 修前(M123 取证,2026-11-05,tiny-skia,700×760,`ULL_DL_DEMO=options-fabric`):
|
||||||
/// chips 档多出两行 chip,选项区又整块钉在列表下面(316px),于是列表列只剩一条 **18px** 的缝,
|
/// chips 档多出两行 chip,选项区又整块钉在列表下面(316px),于是列表列只剩一条 **18px** 的缝,
|
||||||
/// 选中行的底色带实测 19px(行高 44)——版本号一个字读不出来,用户换不了版本。
|
/// 选中行的底色带实测 19px(行高 44)——版本号一个字读不出来,用户换不了版本。
|
||||||
/// 修法见 `rows::TailShare`:chips 档改成「列表与面板各吃一半,面板自己可滚」。
|
/// 修法见 `rows::TailShare`:chips 档改成「面板先保预算、列表按整行吃剩下的」
|
||||||
|
/// (M227 起是 `TailShare::Narrow` + `rows/tail.rs::CHIPS_PANEL_BUDGET`,700/720×760 = 3 整行)。
|
||||||
///
|
///
|
||||||
/// ```text
|
/// ```text
|
||||||
/// ULL_DL_CHIPS_SHOT=02-options-fabric-en-light-chips700.png \
|
/// ULL_DL_CHIPS_SHOT=02-options-fabric-en-light-chips700.png \
|
||||||
@@ -15,13 +16,18 @@ use super::{bands, feed, ui, Shot, CHIPS_SHOT};
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// 抓得住:①首行版本行的底色带 = 44(整行在屏);②**列表区 ≥ 3 整行**(面板上沿那条通栏
|
/// 抓得住:①首行版本行的底色带 = 44(整行在屏);②**列表区 ≥ 3 整行**(面板上沿那条通栏
|
||||||
/// 分隔线就是下界);③分隔线下方 96px 内有一条 ≥ 80px 的实色带(面板头那条实心「安装」按钮)——
|
/// 分隔线就是下界)——这是窄档预算的硬下限:面板钉死区若重新钉住版本菜单(M227 之前的样子),
|
||||||
|
/// 它在 459px 里只能再留 1 整行给列表;③面板头部在屏:断言在面板上沿分隔线下方 96px 的窗口
|
||||||
|
/// (分隔线 514 ⇒ 窗口 518–610)里找一条 ≥ 80px 的同色带,窄档实测命中 **y=550、宽 94px**,
|
||||||
|
/// 那是**实心「安装」按钮的药丸本身**——版本菜单容器的上沿在 636–639,落在窗口之外,与本条无关
|
||||||
|
/// (r2 复审订正:旧注释把「菜单容器的满宽底边」也列成可能来源,那是错的)。
|
||||||
/// 「能换版本」与「能看见选项面板」是这条断言的成对验收。
|
/// 「能换版本」与「能看见选项面板」是这条断言的成对验收。
|
||||||
///
|
///
|
||||||
/// 抓不住:文本有没有被**横向**裁掉(那要看字形宽度,见 `tests/rails.rs` 的标签断言与
|
/// 抓不住:文本有没有被**横向**裁掉(那要看字形宽度,见 `tests/rails.rs` 的标签断言与
|
||||||
/// 报告里的量测表)、hover/tooltip 层(静态图拍不到)、面板内部滚动到底后长什么样
|
/// 报告里的量测表)、hover/tooltip 层(静态图拍不到)、面板内部滚动到底后长什么样
|
||||||
/// (静态图只有一帧,只能拍到 scroll offset 0)——所以这里**不**断言「面板没被窗口底边切」:
|
/// (静态图只有一帧,只能拍到 scroll offset 0)——所以这里**不**断言「面板没被窗口底边切」:
|
||||||
/// chips 档的面板本来就是可滚的一半,实例名那一格确实落在折线以下。
|
/// chips 档的面板本来自己可滚,实例名钉在面板底部。**M227 起**滚动区首屏装
|
||||||
|
/// head + 六枚 chip + 说明行(104px),版本菜单露在折线下方、滚得到。
|
||||||
#[test]
|
#[test]
|
||||||
fn a_chips_tier_shot_keeps_a_whole_version_row_readable() {
|
fn a_chips_tier_shot_keeps_a_whole_version_row_readable() {
|
||||||
let Some(path) = feed(CHIPS_SHOT) else {
|
let Some(path) = feed(CHIPS_SHOT) else {
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ fn the_demo_data_is_sane() {
|
|||||||
assert!(demo.rows.iter().all(|row| !row.supporting.contains('版')));
|
assert!(demo.rows.iter().all(|row| !row.supporting.contains('版')));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 列表与尾部面板怎么分高:**档位 × 面板形态**两个条件同时成立才对半分。
|
/// 列表与尾部面板怎么分高:**档位 + 面板形态** → 三档之一(`Pinned` / `Shared` / `Narrow`)。
|
||||||
///
|
///
|
||||||
/// 修前(M123):面板一律按内容高钉在底部,700×760 的 chips 档里列表只剩 18px(0.4 行)。
|
/// 修前(M123):面板一律按内容高钉在底部,700×760 的 chips 档里列表只剩 18px(0.4 行)。
|
||||||
/// M125 r1 修法只判档位,又踩出另一条回归:chips 档「刚进页面还没选版本」(`Hint`,面板只有
|
/// M125 r1 修法只判档位,又踩出另一条回归:chips 档「刚进页面还没选版本」(`Hint`,面板只有
|
||||||
@@ -154,12 +154,14 @@ fn the_demo_data_is_sane() {
|
|||||||
/// (复审 P2-1 实测;main 那一态是 **7 整行**——不是 8:第 8 条墨迹其实是
|
/// (复审 P2-1 实测;main 那一态是 **7 整行**——不是 8:第 8 条墨迹其实是
|
||||||
/// `Pick a version, then install` 那句 hint,横向范围对不上版本号。r2 修回 7 整行)。
|
/// `Pick a version, then install` 那句 hint,横向范围对不上版本号。r2 修回 7 整行)。
|
||||||
///
|
///
|
||||||
/// 现在的判据:`Chips` **且**面板真占高度(`Options` / `Install`)才 `Shared`;
|
/// 现在的判据(M227 重写,旧文档只写了 M125 那一条条件 —— r8 复审 improve finding):
|
||||||
/// `Hint` 在任何档位都 `Pinned`。
|
/// ① `Chips` **且**面板真占高度(`Options` / `Install`)⇒ `Narrow`;
|
||||||
|
/// ② **任何档位**的 `Options` 只要带加载器版本列表 ⇒ `Shared`(三栏档只走这条);
|
||||||
|
/// ③ 其余(含 `Hint`)⇒ `Pinned`。
|
||||||
///
|
///
|
||||||
/// 抓得住:①chips 档改回「只判档位」(`Hint` 跟着对半分 ⇒ 列表掉到 4 行,像素守门
|
/// 抓得住:①chips 档改回「Pinned」(面板把列表压成缝 ⇒ 像素守门
|
||||||
/// `ready::a_ready_shot_keeps_the_whole_visible_list` 立刻红);②三栏档也改成 `Shared`
|
/// `ready::a_ready_shot_keeps_the_whole_visible_list` / `chips::a_chips_tier_shot_…` 立刻红);
|
||||||
/// (1180 下选项面板被切成一半,`an_options_shot_…` 会红)。
|
/// ②三栏档也改成 `Narrow`(1180 下钉死区少了菜单与说明,`an_options_shot_…` 的量测会红)。
|
||||||
/// 抓不住:**实际分到多少像素**(`iced` 的 `Element` 没有几何内省,那只能出图量)。
|
/// 抓不住:**实际分到多少像素**(`iced` 的 `Element` 没有几何内省,那只能出图量)。
|
||||||
#[test]
|
#[test]
|
||||||
fn the_chips_tier_shares_the_height_only_when_the_panel_takes_room() {
|
fn the_chips_tier_shares_the_height_only_when_the_panel_takes_room() {
|
||||||
@@ -207,8 +209,8 @@ fn the_chips_tier_shares_the_height_only_when_the_panel_takes_room() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
TailShare::of(VersionRailTier::Chips, &panel_state(&options)),
|
TailShare::of(VersionRailTier::Chips, &panel_state(&options)),
|
||||||
TailShare::Shared,
|
TailShare::Narrow,
|
||||||
"chips 档 + 选项区展开:对半分(列表 4 整行、面板自己可滚)"
|
"chips 档 + 选项区展开:窄档预算(面板只钉实例名,列表 3 整行)"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
TailShare::of(VersionRailTier::Chips, &panel_state(&hint)),
|
TailShare::of(VersionRailTier::Chips, &panel_state(&hint)),
|
||||||
|
|||||||
Reference in new issue
Block a user