- README 客户端冒烟验证表:新增两条断言(窗口全程恒定 + 形状与卡片框一致、岛外鼠标穿透),
并把"开合不震动"改成按"岛"描述(窗口冻结 / 岛 0.00px / 无过冲);空闲细缝更新为 6x22
- README 个性化设置表与交付清单:去掉已失效的"Windows 36px 最小窗口高度"说明
- docs/winisland-design-tokens.md:新增"实际采纳了哪些"对照表
(固定包围盒窗口、弹簧参数、连续圆角、交叉淡入、描边/投影、亚克力、命中控制、6 锚点、字号模型,
以及明确未采纳的音乐/歌词/小组件部分)
验证:verify:desktop 54/54(离线复跑 48/48)、打包产物 portable 54/54、verify:e2e 133/133、verify:web 19/19
lint / format:check / typecheck 全通过;服务端安装包同步重打(18:53)
50 KiB
WinIsland 视觉设计系统(Design Token 规格)
来源:D:\classhelper\.cache\winisland-ref\WinIsland-master(只读 checkout)。
所有数值均取自代码常量,标注了「常量名 = 值 @ 文件」。未在代码中找到的项明确写「未找到」。
关键前提:岛内所有几何尺寸都乘以 compact_scale(默认 1.0)或 expanded_scale(默认 1.0),
下文数值默认 scale = 1.0。文字颜色在渲染入口被硬编码为 Color::WHITE(src/core/render.rs
draw_expanded_layer / draw_compact_layer 传 text_color: Color::WHITE, text_color_sec: Color::WHITE),
所有"次级/次要"文字靠 alpha 或 灰度通道值 表达,而不是换颜色。
1. 几何(几何 token)
1.1 两个状态的尺寸
| 项 | compact / mini | expanded | 来源 |
|---|---|---|---|
| 宽 | base_width = 120.0 |
expanded_width = 360.0 |
src/core/config.rs AppConfig::default() |
| 高 | base_height = 27.0 |
expanded_height = 200.0 |
同上 |
| 圆角 r | h / 2(=13.5)→ 胶囊/体育场形 |
min(48*scale, w/2, h/2) = 48.0 |
src/window/app/frame.rs default_target_r;src/utils/shape.rs expanded_island_radius() |
| 全局缩放 | compact_scale = 1.0 |
expanded_scale = 1.0 |
src/core/config.rs |
圆角函数原文(src/utils/shape.rs):
pub(crate) fn expanded_island_radius(w: f32, h: f32, scale: f32) -> f32 {
(48.0 * scale).min(w / 2.0).min(h / 2.0).max(0.0)
}
compact 圆角:IslandSprings::new 里 r = (base_height * compact_scale) / 2.0(src/window/app.rs);
展开时 default_target_r = compact_content_h / 2.0(src/window/app/frame.rs update_spring_targets)。
1.2 compact 宽度的动态规则(不是固定 120)
src/window/app/layout.rs compute_lyric_target_width():
- 无媒体 / 不在播放态:宽 =
base_width= 120 - 有媒体但无歌词:宽 =
base_width + 35.0= 155 - 有歌词(不滚动):
w = 60.0 + 文本宽,再clamp(base_width+35, MAX_LYRIC_WIDTH)min_w = base_width + 35.0、MAX_LYRIC_WIDTH = 700.0(src/core/config.rs)
- 有歌词(滚动模式,
lyrics_scroll = true):natural_w = 60.0 + text_w,超过lyrics_scroll_max_width(默认300.0,default_lyrics_scroll_max_width)则固定为该上限;内部可用文本宽 =(fixed_w - 59.0) * compact_scale - 二级歌词存在时:高度 =
max(base_height*scale, mini_lyric_pair_height), 其中lyric_pair_height = 主字号*(1 + 0.85 + 0.18) + 8*scale(SECONDARY_LYRIC_SCALE = 0.85、LYRIC_PAIR_GAP_SCALE = 0.18@src/core/render/mini.rs) → 默认字号 12 时 = 12×2.03 + 8 = 32.36 px 高,r ≈ 16.18
1.3 挂件(compact widget)导致的最小宽度
src/ui/widget/compact/mod.rs:
CONTENT_EDGE_INSET = 9.0、CONTENT_GAP = 7.0widget_width():Time = 48.0、ResourceUsage = 132.0- 三组对齐宽度:
aligned_layout_width = 9*2 + left + center + right + 7*(占用组数-1) - 单侧扩展宽:
extension_width = CONTENT_EDGE_INSET + strip_width + CONTENT_GAP(例:左 Time → 9+48+7 = 64;ResourceUsage → 9+132+7 = 148) - 最终 compact 宽 =
max(布局宽, base_width)
1.4 compact overlay(通知 / 音量)会替换 base 尺寸
| overlay | 宽 | 高 | 圆角 | 来源 |
|---|---|---|---|---|
| 通知 | max(base_width, 330.0) * scale = 330 |
max(base_height, 82.0) * scale = 82 |
h/2 = 41 |
src/ui/compact/notification.rs target_size();r 取自 src/window/app/frame.rs(size.height/2.0) |
| 音量 | (base_width + 72.0) * scale = 192 |
(base_height + 10.0) * scale = 37 |
h/2 = 18.5 |
src/ui/compact/volume.rs target_size() |
1.5 隐藏态尺寸
hidden_width默认5.0(default_hidden_width@src/core/config.rs),MIN_HIDDEN_WIDTH = 0.0、MAX_HIDDEN_WIDTH = 400.0- 隐藏时高度收缩到
hidden_width * compact_scale,并保留hidden_reveal_h = 1.0(1px 可点击边); 位移距离hide_distance = concealed_height + TOP_OFFSET(src/window/app/layout.rs)
1.6 锚点(两种状态之间什么不动)
定位全部来自 src/window/app/layout.rs compute_island_layout() / compute_window_position():
- 顶部居中停靠(默认):
island_y = TOP_OFFSET = 10.0(顶边固定),offset_x = (os_w - w) / 2.0(水平中心固定) - 底部停靠:
island_y = os_h - TOP_OFFSET - h(底边固定,向上生长) - 左/右停靠:
offset_x = TOP_OFFSET或os_w - TOP_OFFSET - w(对应侧边固定) TOP_OFFSET: i32 = 10(src/core/config.rs)= 岛顶边距屏幕顶 10 px- 状态切换由三个弹簧同时插值:
w/h/r参数为stiffness = 0.10, damping = 0.68(src/window/app/frame.rs), 页面横移view用0.12 / 0.68;隐藏用(0.12, 0.70)隐藏 /(0.08, 0.78)显示 - compact 内容不跟随变大的岛做垂直居中重排:mini 内容画在
stable_island_y(= 顶边)加上base_h带内居中 (src/core/render/mini.rs:封面y = stable_offset_y + (base_h - size)/2,歌词center_y = stable_offset_y + base_h/2)。 效果=岛向下长大,胶囊内容始终贴在顶部那条带里。 - 交叉淡出:
expanded_alpha = expansion_progress² * (1-hide_progress),mini_alpha = clamp(1 - expansion_progress*1.5, 0, 1) * (1-hide_progress)(MINI_FADE_RATE = 1.5@src/core/render.rs)→ 即进度 0~0.667 之间完成交叉淡出
1.7 有没有"挖孔/刘海"造型?
没有。 代码里没有 SetWindowRgn / CreateRoundRectRgn / notch / cutout 相关调用(已全仓 grep 确认),
也没有"顶部贴边 + 上方两角变直角"的特例分支。造型是:
- 一个悬浮胶囊(compact,r = h/2),距屏幕顶 10 px;
- 展开后是四角一致的连续超椭圆(squircle),r 最大 48。
圆角实现不是普通圆弧,而是 Flutter RoundedSuperellipse 的移植(src/utils/shape.rs,
注释标明 "Rounded-superellipse geometry adapted from Flutter under the BSD-3-Clause license"):
superellipse_parameters():11 项查表PARAMETERS(如(2.0, 1.13276676) … (6.43023796, 2.98020421)), ratio > 5 时线性外推exponent = 6.43023796 + 1.559599389*(ratio-5.0)、factor = 2.98020421 + 0.522807185*(ratio-5.0)Octant::new():13 项BEZIER_FACTORS((0.7078, 8.3194) … (0.9705, 1.2880)),用 conic + cubic 拼四分之一角- 路径缓存
PATH_CACHE_CAPACITY = 64 - Web 复刻提示:CSS
border-radius是正圆角,观感会偏"软";要还原请用 SVG path 或corner-shape: squircle(若可用)/clip-path近似。未找到等价的简单圆角公式——真实几何就是上面那张表。
1.8 窗口留白 / 裁切
PADDING: f32 = 80.0(src/core/config.rs);窗口尺寸 =(岛宽 + 80, 岛高 + 80)(src/window/app/layout.rsrequired_window_size())→ 岛四周各 40 px 透明留白,供阴影/动画使用- 绘制时
canvas.clip_path(island_path, ClipOp::Intersect),所有内容裁在岛形内; 1px 描边在canvas.restore()之后绘制(src/core/render.rsdraw_island()) - 挂件网格额外内缩,避免内容进圆角(
src/ui/widget/expanded/mod.rswidget_grid_layout):corner_inset = radius * (1.0 - FRAC_1_SQRT_2); // 48 * 0.29289 = 14.06 inset = (corner_inset + 8.0*scale).max(24.0*scale); // = 24.0(scale=1) gap = 7.0 * scale;
1.9 expanded 内容网格
WIDGET_GRID_COLS = 6、WIDGET_GRID_ROWS = 3、WIDGET_GRID_SLOTS = 18(src/core/config.rs)- 槽位宽 =
(inner_w - 7*5) / 6,高 =(inner_h - 7*2) / 3; 用 360×200、inset 24 计算:inner = 312×152 → slot ≈ 46.17 × 46.0(src/ui/widget/expanded/mod.rs) - 组件占格
WidgetKind::span():Clock = (2,1)、ResourceUsage = (2,1)、Calendar = (2,2)、Settings = (1,1)(src/core/config.rs)
2. 配色(颜色 token)
2.1 岛背景(src/core/render/background.rs draw_background())
| 样式 | 颜色值 | 说明 |
|---|---|---|
default(SOLID_STYLE) |
Color::BLACK = #000000,完全不透明 |
bg_color |
glass + host backdrop |
Color::from_argb(150, 10, 10, 14) = rgba(10,10,14,0.588) |
draw_host_glass() |
glass 无 backdrop(回退) |
Color::from_argb(205, 32, 32, 36) = rgba(32,32,36,0.804) |
fallback_color |
dynamic 底色 |
Color::from_rgb(32, 32, 36) = #202024,不透明 |
draw_effect_base() |
dynamic 叠加层 |
Color::from_argb(120, 20, 20, 24) = rgba(20,20,24,0.471) |
盖在旋转模糊封面之上 |
dynamic 无封面回退 |
同 glass 分支(150/10,10,14 或 205/32,32,36) |
dynamic 封面动效参数(同一函数):
- 集显:
rotate_speed = 0.015 rad/s, drift_speed_x = 0.075, drift_speed_y = 0.06, drift_amp_x = 10.0, drift_amp_y = 7.5 - 独显:
rotate_speed = 0.03, drift_speed_x = 0.15, drift_speed_y = 0.12, drift_amp_x = 20.0, drift_amp_y = 15.0 - 旋转矩形边长
side_len = diagonal * 1.3
亚克力/backdrop 真实实现(src/window/d3d.rs + src/utils/win32.rs):
Windows 11 DWMWA_USE_HOSTBACKDROPBRUSH,失败时退回 SetWindowCompositionAttribute
(AccentPolicy { state: 5, flags: 0, gradient_color: 0 },state 5 = host backdrop);
再用 Compositor::CreateHostBackdropBrush() + RoundedRectangleGeometry 裁圆角
(HOST_BACKDROP_INSET = 1.0,几何 = w-2, h-2, radius-1)。
未找到任何模糊半径数值——模糊由系统 HostBackdropBrush 决定。
2.2 描边(src/core/render.rs draw_island_border())
BORDER_WIDTH = 1.0; BORDER_INSET = BORDER_WIDTH / 2.0; // 0.5
SOLID_STYLE = "default"; SOLID_BORDER_ALPHA = 30; EFFECT_BORDER_ALPHA = 40;
- 颜色 = 纯白,alpha:
default样式:rgba(255,255,255, 30/255=0.118),并再乘(1 - expansion_progress)(展开后描边消失)glass/dynamic:rgba(255,255,255, 40/255=0.157),不随展开衰减
- 描边矩形 =
(x+0.5, y+0.5, w-1, h-1),圆角r-0.5
2.3 阴影(src/core/render.rs draw_expanded_shadow(),仅展开时)
- 颜色
Color::from_argb((28.0 * opacity) as u8, 0,0,0)= rgba(0,0,0,0.11) opacity = expansion_progress² * (1 - hide_progress),opacity <= 0.01直接跳过- 偏移
offset_y = 2.0 * scale;模糊sigma = min(3.0*scale, margin/3.0)(margin = 到画布边界的余量) - 用
ClipOp::Difference只画在岛形之外 - 未找到内阴影(没有任何 inner shadow 实现)
2.4 分隔线
| 位置 | 颜色 | 尺寸 | 来源 |
|---|---|---|---|
| compact 挂件之间 | rgba(255,255,255, 0.14*alpha) |
宽 max(scale,1),从 top+25%h 到 bottom-25%h |
src/ui/widget/compact/mod.rs draw_separator() |
| compact 资源占用内部 | rgba(255,255,255, 0.10*alpha) |
宽 max(0.75*scale, 0.75),半高 DIVIDER_HALF_HEIGHT = 6.0*scale |
src/ui/widget/compact/resource_usage.rs |
| expanded 资源环分隔 | rgba(白, 0.09*alpha) |
宽 scale.max(0.75),从 y+0.2h 到 y+0.8h |
src/ui/widget/expanded/resource_usage.rs |
| 设置窗口分隔线 | rgba(255,255,255,26) 暗 / rgba(20,28,40,18) 亮 |
— | src/utils/color.rs |
2.5 文字颜色与不透明度分层
主文字一律 #FFFFFF;次级用 alpha:
| 元素 | 有效颜色 | 来源 |
|---|---|---|
| 通知 app 名 | rgba(255,255,255, 0.65*alpha) |
src/ui/compact/notification.rs |
| 通知标题 | rgba(255,255,255, alpha) |
同上 |
| 通知详情 | rgba(255,255,255, 0.72*alpha) |
同上 |
| 音量标签 | rgba(255,255,255, 0.9*alpha) |
src/ui/compact/volume.rs |
| expanded 歌手 | rgba(白, alpha*0.6) |
src/ui/expanded/music_view.rs draw_track_text() |
| 迷你歌词主行 | 白 alpha | src/core/render/mini.rs |
| 迷你歌词次行 | rgb(176,176,176)(SECONDARY_LYRIC_CHANNEL = 176) |
同上 |
| 逐字歌词"未唱"部分 | rgb(190,190,190)(PENDING_LYRIC_CHANNEL = 190) |
同上 |
| 进度条时间 | 白,PROGRESS_TIME_IDLE_ALPHA = 0.5 → hover 1.0(线性插值) |
src/ui/expanded/music_view.rs |
| 挂件时钟 | 白 alpha(bold) | src/ui/widget/expanded/time.rs |
| 挂件日历 月 | 0.78*alpha |
src/ui/widget/expanded/calendar.rs |
| 挂件日历 日 | 1.0*alpha |
同上 |
| 挂件日历 星期 | 0.62*alpha |
同上 |
| 挂件资源 数值 | 1.0*alpha(bold) |
src/ui/widget/expanded/resource_usage.rs |
| 挂件资源 标签 | 0.58*alpha |
同上 |
| 封面占位块 | rgba(白, 0.15*alpha) |
src/ui/expanded/music_view.rs draw_placeholder() |
2.6 强调色 / 状态色
岛内(src/ui/widget/resource_usage.rs):
CPU_COLOR = rgb(50, 190, 246) // #32BEF6
RAM_COLOR = rgb(175, 82, 222) // #AF52DE
WARNING_COLOR = rgb(255, 159, 10) // #FF9F0A
CRITICAL_COLOR = rgb(255, 69, 58) // #FF453A
阈值曲线 usage_color(base, usage):
usage <= 0.75→ base0.75 < usage <= 0.9→ base ↔ WARNING 线性混合,t = (usage-0.75)/0.150.9 < usage→ WARNING ↔ CRITICAL 线性混合,t = (usage-0.9)/0.1采样间隔SAMPLE_INTERVAL = 1s;无数值时显示—。
进度条/音量条(src/ui/expanded/music_view.rs、src/ui/compact/volume.rs):
PROGRESS_TRACK_ALPHA = 0.15; // 轨道 rgba(白, 0.15*alpha)
PROGRESS_TIME_IDLE_ALPHA = 0.5; // 时间文字静止透明度
// 音量轨道 rgba(255,255,255, 0.28*alpha),填充纯白 alpha
音量的"填充"就是白色(不是强调色),图标仍是白色。
可视化条颜色来自专辑封面(src/ui/expanded/music_view/palette.rs):
- 把封面画到 8×8 的 GPU surface(
PALETTE_SAMPLE_SIZE = 8)读回求平均 RGB primary = brighten(avg, 1.3)、secondary = brighten(avg, 1.5)brighten用 BT.601 亮度0.299R + 0.587G + 0.114B;若 < 80 则整体加80 - brightness(抬到最低亮度 80)- 调色板 =
[primary, secondary, primary],缓存上限 50 条 - 无封面默认值(
default_media_palette):rgb(180,180,180)+rgb(100,100,100) - 采样完全失败的兜底:
rgb(200,200,200)
2.7 不同状态是否不同配色?
- 同一套:底 + 白字 + alpha 分层;通知 / 音量 / 音乐 / 挂件没有各自的独立配色, 视觉区分靠 尺寸(330×82 / 192×37 / 120×27) 与 内容排版,而不是换色。
- 差异只在两处:可视化条用封面取色(2.6),资源占用用 CPU/RAM/警告/危险四色(2.6)。
- 岛背景三样式(default/glass/dynamic)才是真正的配色分支(2.1)。
2.8 设置窗口色板(另一套,src/utils/color.rs,供参考)
暗色主题 dark_settings_theme():
win_bg #1C1C1E sidebar_bg #242426 group_bg #2C2C2E card_highlight #48484A
accent #0A84FF text_pri #F5F5F7 text_sec #AEAEB2 disabled #636366
danger #E6372D toggle_on #30D158 toggle_off #636366
sidebar_hover rgba(255,255,255,20) separator rgba(255,255,255,26)
popup_bg #323234 popup_border rgba(255,255,255,40) popup_shadow rgba(0,0,0,60)
popup_separator rgba(255,255,255,30) control_bg #3A3A3C control_hover #48484A
control_disabled #303032 control_border rgba(255,255,255,36)
group_border rgba(255,255,255,24) shadow rgba(0,0,0,45) scrollbar rgba(255,255,255,60)
亮色主题 light_settings_theme():
win_bg #F4F5F7 sidebar_bg #FAFAFC group_bg #FFFFFF card_highlight #EFF2F7
text_pri #1F2228 text_sec #5B626E disabled #9297A1 accent #0067C0
danger #C42B1C toggle_on #0078D4 toggle_off #B0B5BE
selection_bg #E0EEFA selection_text #005599
sidebar_hover rgba(20,32,48,14) separator rgba(24,32,44,18)
popup_bg #FFFFFF popup_border rgba(20,28,40,24) popup_shadow rgba(20,28,40,20)
control_bg #F7F8FA control_hover #EBEFF4 control_disabled #F1F2F5
control_border rgba(20,28,40,24) group_border rgba(20,28,40,14)
shadow rgba(20,28,40,20) scrollbar rgba(52,58,68,52)
设置窗口另有"岛预览"配色(与真实渲染略有差异,src/utils/settings_ui/renderer/widget_preview.rs):
glass 预览 rgba(24,24,28,220);dynamic 预览为线性渐变 rgb(18,12,36) → rgb(8,24,48)(左上→右下);
default 预览 rgb(10,10,10);预览描边 alpha 52(glass) / 38(其他);预览阴影 rgba(0,0,0,72),偏移 +4。
→ 若前端需要一张静态渐变替代 dynamic,这两个色值就是现成可抄的。
3. 排版(字体 token)
3.1 字体族(src/utils/font.rs)
- 用户自定义字体:
config.custom_font_path→FontMgr::new_from_data()(优先于全部) - ASCII 文本:
"Microsoft YaHei"→"Segoe UI"→ 系统默认(legacy_make_typeface) - 非 ASCII:逐字符匹配
match_family_style_character("", style, &["zh-CN","ja-JP","en-US"], ch), 再依次尝试"Segoe UI Emoji"→"Microsoft YaHei"→"Segoe UI Symbol"→"Segoe UI" - 合成粗体:
needs_synthetic_bold()= 请求weight >= 600且字体本身weight < 600→font.set_embolden(true) - 亚像素渲染始终开启
font.set_subpixel(true)
字重只有两档:FontStyle::bold()(700)与 FontStyle::normal()(400);
滚动文本判定粗体用 style.weight() >= 700(src/utils/scroll.rs)。
字距 / 行高:代码里未找到 letter-spacing 或 line-height 参数;
行距全部通过显式 y 偏移实现(见下表)。文本绘制走 Skia draw_str,
大字文本(标题/歌手)render_as_paths = true(src/utils/scroll.rs 调用点)。
3.2 字号表(默认 config.font_size = 0.0 时用内置值;>0 时用户值参与计算)
| 用途 | 字号 | 字重 | 说明 / 来源 |
|---|---|---|---|
| mini 歌词主行 | font_size*0.8*scale 或 12.0*scale |
normal | src/core/render/mini.rs lyric_font_size() |
| mini 歌词次行 | 主字号 × SECONDARY_LYRIC_SCALE = 0.85 |
normal | 同上 |
| mini 歌词对高度 | 主*(1+0.85+0.18) + 8*scale |
— | lyric_pair_height() |
| mini 插件文字(主) | font_size*0.7*scale 或 11.0*scale |
bold | PLUGIN_FONT_SCALE = 0.7、DEFAULT_PLUGIN_FONT_SIZE = 11.0 |
| mini 插件文字(次) | 主 × 0.8 |
normal | PLUGIN_SECONDARY_FONT_SCALE = 0.8,行距 font_size*1.3(PLUGIN_SECONDARY_LINE_SPACING = 1.3) |
| expanded 曲名 | font_size*scale 或 15.0*scale |
bold | src/ui/expanded/music_view.rs |
| expanded 歌手 | 同上 15.0*scale | normal | 基线 = 曲名基线 + 22.0*scale |
| 进度时间 | font_size*0.67*scale 或 10.0*scale |
normal | PROGRESS_TIME_FONT_SCALE = 0.67、DEFAULT_PROGRESS_TIME_FONT_SIZE = 10.0 |
| 通知 app 名 | 11.0*scale | normal | src/ui/compact/notification.rs |
| 通知标题 | 13.0*scale | bold | 同上 |
| 通知详情 | 11.0*scale | normal | 行间距 DETAIL_LINE_GAP = 21.0*scale |
| 音量标签 | 12.0*scale | normal | src/ui/compact/volume.rs |
| compact 时钟挂件 | clamp(h*0.43, 10, 12) |
bold | src/ui/widget/compact/time.rs |
| compact 资源—标签 | METRIC_LABEL_SIZE = 7.0*scale |
bold | src/ui/widget/compact/resource_usage.rs |
| compact 资源—数值 | METRIC_VALUE_SIZE = 10.0*scale |
bold | 同上 |
| expanded 时钟挂件 | min(h*0.60, w*0.31),最小 13.0*scale |
bold | src/ui/widget/expanded/time.rs |
| expanded 日历—月 | clamp(h*0.14, 9, 15)*scale |
bold | src/ui/widget/expanded/calendar.rs |
| expanded 日历—日 | min(h*0.53, w*0.70),最小 26.0*scale |
bold | 同上 |
| expanded 日历—星期 | clamp(h*0.12, 8, 12)*scale |
normal | 同上 |
| expanded 资源—圆环值 | clamp(d*0.27, 7, 10.5*scale),d = 圆环直径 |
bold | src/ui/widget/expanded/resource_usage.rs |
| expanded 资源—标签 | clamp(h*0.12, 6, 8*scale) |
bold | 同上 |
| 设置窗口—行标签/值 | 13.0 | normal | src/utils/settings_ui/renderer/items.rs |
| 设置窗口—页签标签 | 11.5 | normal(选中为 bold) | src/window/settings/renderer.rs |
| 设置窗口—弹窗项 | 12.0 | normal | 同上 |
| 设置窗口—Markdown | BODY_SIZE = 12.0,LINE_HEIGHT = 18.0 |
— | src/window/settings/pages/plugins/markdown.rs |
字体基线规则(值得照抄):
- 居中对齐用 字形包围盒 居中:
baseline_y = bounds.top + (bounds.h - glyph.h)/2 - glyph.top(src/ui/widget/expanded/mod.rsdraw_widget_text_centered()) - 时间文字基线 = 进度条中心 +
0.35*字号(PROGRESS_TIME_BASELINE_SCALE = 0.35) - 迷你歌词单行时
primary_y = center_y + size/3;双行时按堆叠高度对齐(见 5.1)
3.3 是否随窗口尺寸 / 用户设置缩放
- 随
compact_scale/expanded_scale缩放(乘在所有尺寸上) - 字号受
config.font_size(默认 0 = 用内置值)影响:只有 mini 歌词、mini 插件、expanded 曲名/歌手、进度时间用它 - 挂件字号是几何驱动的(按自身矩形宽高 clamp),与
font_size无关 - 设置窗口有自己的
settings_theme(system/ 暗 / 亮)
4. 层级与效果
4.1 圆角
见 1.7。总结:连续超椭圆(squircle),compact 满圆角(h/2),expanded 最大 48。
挂件卡片 widget_corner_radius = min(12.0*scale, min(w,h)/2)(src/ui/widget/expanded/mod.rs)。
迷你封面 MINI_COVER_RADIUS = 5.0,expanded 封面 16.0*scale,通知图标 11.0*scale。
4.2 阴影 / 发光
- 外阴影:仅 expanded,
rgba(0,0,0,0.11),y+2,blur σ =min(3*scale, margin/3)(见 2.3) - 内阴影:未找到
- 发光:只出现在资源占用圆环——
glow圆半径diameter*0.57,颜色accent,alpha0.07*alpha(src/ui/widget/expanded/resource_usage.rs) - 设置窗口:窗口阴影
rgba(0,0,0,90)、弹出菜单阴影theme.popup_shadow+ 偏移 +2(src/window/settings/renderer.rs); 拖拽幽灵阴影rgba(0,0,0,90),偏移 +4
4.3 模糊半径(全部数值)
| 场景 | 参数 | 来源 |
|---|---|---|
| 运动模糊(整体) | 横向 = ` | w速度 |
| 封面翻转 | `σ = (1- | cos |
| 暂停按钮速度模糊 | ` | 弹簧速度 |
| 快进/快退按钮 | (1 - t/0.3).max(0) * 6.0 * scale,x×0.3 |
同文件 draw_skip_button() |
| 歌词切换(Blur 模式) | σ = 6.0*scale,x 压缩 LYRIC_TRANSITION_BLUR_X_SCALE = 0.25,位移 LYRIC_TRANSITION_OFFSET = 10.0*scale |
src/core/render/mini.rs |
| dynamic 背景封面 | 先降采样到 64×64,再 image_filters::blur((8.0, 8.0), TileMode::Clamp) |
src/utils/backdrop.rs |
| host backdrop(亚克力) | 未找到数值(由系统 HostBackdropBrush 决定,仅 HOST_BACKDROP_INSET = 1.0) |
src/window/d3d.rs |
4.4 不透明度分层与可见性阈值
MIN_VISIBLE_OPACITY = 0.01 // 低于此不绘制
MIN_BLUR_SIGMA = 0.1
MINI_FADE_RATE = 1.5 // mini 淡出速率
COLLAPSED_VISUALIZER_HEIGHT_SCALE = 0.45 // 收起时可视化高度 = 45%
expanded_alpha = expansion_progress² * (1 - hide_progress)(平方 → 展开"迟入场")mini_alpha = clamp(1 - expansion_progress*1.5, 0, 1) * (1 - hide_progress)- 可视化高度比例
visualizer_height_scale = 0.45 + 0.55 * expansion_progress - 歌词额外淡出
LYRIC_EXPANSION_FADE_RATE = 2.5(src/core/render/mini.rs) - 展开页横向滑动:
page_shift = view_offset * current_w; 音乐页translate(-page_shift),挂件页translate(current_w - page_shift)(src/core/render/expanded.rs);view弹簧0.12/0.68
4.5 卡片四周留白 / 裁切
见 1.8:窗口 = 岛 + 80(每侧 40 透明);内容 clip_path(Intersect);
描边在裁切外画;挂件网格 inset 24。
5. 布局骨架
5.1 compact(mini)120×27
src/core/render/mini.rs:
MIN_MINI_CONTENT_WIDTH = 45.0 // 可用宽小于此不画内容
MINI_COVER_SIZE = 18.0 // 封面尺寸
MINI_COVER_LEFT_INSET = 10.0 // 封面左边距
MINI_COVER_RADIUS = 5.0 // 封面圆角
MINI_VISUALIZER_RIGHT_INSET = 17.0
MINI_VISUALIZER_WIDTH_SCALE = 0.55
MINI_VISUALIZER_SMOOTHING = (0.6, 0.08) // (上升, 下降)
LYRIC_LEFT_INSET = 30.0
LYRIC_RIGHT_INSET = 29.0
- 左:18×18 封面,x = 10,y 垂直居中(
(base_h - 18)/2 = 4.5),圆角 5,中心裁剪 - 中:歌词,可用区 x ∈ [30, w-29],垂直居中;主行居中(滚动模式改为左对齐并从 x=30 起滚动),
次行始终水平居中;主行单行时
y = center_y + size/3,双行时stack_h = size + size*0.85 + size*0.18,主行y = center_y - stack_h/2 + size*0.8, 次行y = center_y + stack_h/2 - secondary*0.2 - 右:可视化条,中心 x =
w - 17,中心 y =base_h/2 - 无媒体时改由 compact 挂件条接管:Left / Center / Right 三组;
有 mini 内容时只画 Left 与(Center+Right)两组并贴两端,并在内容两侧画竖分隔线
(
src/ui/widget/compact/mod.rsdraw()) - 挂件宽:时间 48、资源 132;边缘 inset 9;组内间距 7
5.2 expanded 音乐页 360×200
src/ui/expanded/music_view.rs 常量全表:
CONTENT_PADDING = 24.0 // 四周内边距
COVER_SIZE = 64.0 // 封面 64×64,圆角 16.0*scale,位置 (24,24)
TRACK_TEXT_GAP = 18.0 // 封面右到文字左
TRACK_TEXT_RIGHT_INSET = 70.0 // 文字右侧预留(给可视化条)
TRACK_TITLE_BASELINE_OFFSET= 26.0 // 曲名基线 y = 24 + 26 = 50
PROGRESS_TOP_GAP = 24.0 // 进度条中心 y = 24 + 64 + 24 = 112
PROGRESS_TIME_WIDTH = 28.0 // 两端时间文字预留宽
PROGRESS_TIME_GAP = 4.0
PROGRESS_BAR_HEIGHT = 6.5 // 常态条高
PROGRESS_BAR_HOVER_GROWTH = 3.5 // hover 增至 10.0
PLAYBACK_CONTROLS_TOP_GAP = 42.0 // 控制行中心 y = 112 + bar_h/2 + 42 ≈ 157.25
SKIP_BUTTON_GAP = 75.0 // 上一首/下一首中心距播放键 75
PAGE_ARROW_RIGHT_INSET = 7.5 // 右侧翻页条 x = w - 7.5,y = h/2
PAGE_ARROW_FADE_RATE = 5.0
COLLAPSED_VISUALIZER_INSET = 17.0 // 视觉条右侧 inset 随展开 17 → 37
EXPANDED_VISUALIZER_INSET = 37.0
VISUALIZER_TITLE_OFFSET = 4.0 // 视觉条中心 y = 曲名基线 - 4
- 左:64 封面;中:曲名(15 bold)/歌手(15, α0.6) 两行,x = 24+64+18 = 106;右:可视化条
- 进度条:左右各内缩 24,两端 28 宽时间 + 4 间距;条圆角 = bar_h/2;
填充宽度
max(bar_total_w*progress, bar_h);填充只圆左端(RRect 左上/左下半径 = bar_h/2,右侧 0) - 控制行中心 x = 180;播放键在中心、上一首 105、下一首 255
- 点击热区(
src/ui/expanded/music_view/controls.rs):播放 40×40,上/下一首 36×36, 进度条热区高 16.0*scale,封面整块可双击(激活媒体 App) - 动画参数:
PROGRESS_SMOOTHING = 0.15、PROGRESS_HOVER_SMOOTHING = 0.18、PROGRESS_HOVER_SNAP_THRESHOLD = 0.005、PROGRESS_START_THRESHOLD = 0.02、PROGRESS_JUMP_THRESHOLD = 0.3、PLAY_STATE_RESPONSE = 0.18、SKIP_ANIMATION_DURATION_SECS = 0.5、封面翻转 0.6s、PAUSE_CONTROL_STIFFNESS = 0.18、PAUSE_CONTROL_DAMPING = 0.64、PAUSE_CONTROL_MIN_SCALE = 0.8、PAUSE_CONTROL_MAX_SCALE = 1.03、PAUSE_CONTROL_PRESS_VELOCITY = -0.18 - 播放/暂停过渡:
cover_scale = 0.85 + 0.15*pause_t、cover_brightness = 0.75 + 0.25*pause_t(即暂停时封面缩小到 85% 并压暗到 75%),图标用smoothstep交叉淡出
5.3 expanded 挂件页(同 360×200)
- 6×3 网格,inset 24,gap 7(见 1.9)
- 卡片:背景
rgba(28,28,30, 0.05*alpha),描边1px rgba(255,255,255, 0.16*alpha),圆角min(12*scale, min(w,h)/2) - 左侧翻页条 x = 7.5,y =
h/2(3×14 圆角矩形,α0.4);右侧同(x = w-7.5) - 内置挂件各视图排布:
- 时钟:整卡居中,字号
min(h*0.6, w*0.31)(≥13),bold,HH:MM格式 - 日历:月
y+0.08h..0.26h(bold,α0.78)/日y+0.27h..0.75h(bold,字号min(0.53h,0.70w),≥26)/ 星期y+0.78h..0.92h(normal,α0.62) - 资源占用(2×1):中间竖分隔(α0.09,
y+0.2h..0.8h);两个圆环各占一半: 环心(center_x, top+0.43h),直径min(0.58h, 0.62w)(≥20),描边min(3*scale, d*0.12)圆头, 底环 α0.13,进度弧从 -90° 起扫usage*360°(α0.92),值文字 bold 居中,标签在top+0.76h(α0.58) - 设置(1×1):整卡居中齿轮图标,
icon_scale = min(w,h)*0.38/24,α0.72
- 时钟:整卡居中,字号
5.4 compact 各业务视图差异
| 视图 | 尺寸 | 排布 |
|---|---|---|
| 时间挂件 | 48×27 | 居中 HH:MM,字号 clamp(0.43h, 10..12) bold,基线 center_y + size*0.36 |
| 资源挂件 | 132×27 | 两个 metric 各 (w-8)/2 宽;标签 7px bold 在左(accent α0.78),数值 10px bold 在右(白); |
文字基线 center_y + scale;下方 2px 进度轨(y = 基线+5),轨 α0.13,填充 accent α0.9;中间分隔 α0.1,半高 6 |
||
| 通知 | 330×82 | 图标 42×42 圆角 11 @ x+18 垂直居中;文本左 = 有图标 x+72 / 无图标 x+20;右内缩 18; |
app 名基线 top+22(11px α0.65);标题 13px bold,基线 top+34(无 app 名)/ top+43(有 app 名)/ 垂直居中 top+(h+13)/2(两者皆无);详情 11px α0.72 在标题下 21px |
||
| 音量 | 192×37 | 喇叭图标 20px,中心 x = left+21;标签 12px @ left+37(α0.9);轨道从 label_x + label_w + 26 到 right-14,高 4 圆角 2,轨 α0.28,填充白 α1.0 |
5.5 动画时长(通知 / 音量)
- 通知:
DISPLAY_DURATION = 5s,ENTER_DURATION = 220ms(ease_out_cubic = 1-(1-t)³),FADE_DURATION = 280ms;入场位移(1-enter)*7.0*scale(从上滑下 7px) - 音量:
DISPLAY_DURATION = 1600ms,FADE_DURATION = 240ms,VOLUME_CHANGE_THRESHOLD = 0.002(音量变化阈值,低于此不显示) - 歌词切换进度用
smoothstep:p*p*(3-2p)(src/core/render/mini.rs) - 长文本跑马灯(
src/utils/scroll.rs):先停 2.0s,再以 35.0*scale px/s 滚动, 循环间隔full_w + 50*scale,裁剪区(x, y-size*1.2, max_w, size*1.5)
6. 图标
体系:全部是 Skia 内的 SVG path 数据 + 填充(PaintStyle::Fill),没有线性描边图标体系,
也没有 icon font。颜色一律单色:由 text_color(= 白)+ alpha 决定;只有资源环用 accent 色。
未找到线宽(stroke-width)概念——唯一的描边图标是设置窗口的翻页箭头(1.8px, round cap/join)。
| 图标 | 路径坐标系 | 缩放 | 实际视觉尺寸 | 来源 |
|---|---|---|---|---|
| 播放 ▶ | 640 单位(128..544) | 0.083 * scale |
≈ 34.5 × 37 px | src/icons/controls.rs draw_play_button |
| 暂停 ⏸ | 640 单位 | 0.083 * scale |
≈ 34.5 × 37 px | draw_pause_button |
| 快退/快进三角 | 640 单位(中心 336,320) | path_scale * global_scale,静止 0.050(左)与 0.055(右);动画中 0.050~0.055 |
约 21 × 22 px | draw_control_triangle,src/ui/expanded/music_view.rs draw_skip_button(中心 ±10.92*scale) |
| 音乐(封面占位) | 1024 单位 | scale*1.8,内部 0.024 |
随卡片 | src/icons/music.rs |
| 设置齿轮 | 1024 单位 | min(w,h)*0.38/24,内部 0.024 |
卡片 38% 短边 | src/icons/settings.rs |
| 音量(4 档) | 25.75 单位 | size / 25.75,偏移 (-11.089286, -7.883929) |
传 20.0*scale | src/icons/volume.rs;档位 level<=0→静音, <=1/3→低, <=2/3→中, else→高 |
| 翻页指示条 | 不是 path | — | 3.0*scale × 14.0*scale 圆角矩形,圆角 2.0*scale,α×0.4 |
src/icons/arrows.rs |
其他图标使用点:
- 通知 App 图标:
AppDisplayInfo::GetLogo(Size{64,64}),按 alpha>8 计算可见包围盒,等比缩放塞进 42×42,圆角 11 裁切 - 封面:64×64(expanded)与 18×18(mini),中心裁剪,
FilterMode::Linear + MipmapMode::Linear - 迷你封面圆角 5、expanded 封面圆角 16(连续超椭圆路径,不是普通
clip_rrect)
7. 明确「未找到」的项
- 刘海/挖孔造型:不存在(无
SetWindowRgn、无顶部直角特例) - 内阴影 / inset shadow:代码中无任何实现
- 字距(letter-spacing)与行高倍数:无;行距全靠显式 y 偏移
- 亚克力的模糊半径/透明度数值:由 Windows
HostBackdropBrush系统决定,代码只给HOST_BACKDROP_INSET = 1.0 - 强调色用于岛内 UI:岛内几乎没有强调色(只有资源占用的 CPU/RAM/警告/危险色);
统一强调色
#0A84FF仅存在于设置窗口(COLOR_ACCENT) - 岛自身的亮/暗主题:岛屿永远是深底白字;明暗主题只在设置窗口
- 固定渐变色背景:真实
dynamic背景是封面图,没有固定渐变; 仅设置页预览用rgb(18,12,36) → rgb(8,24,48)(可当静态替代) - CSS 等价的超椭圆圆角公式:真实实现是 Flutter 超椭圆查表(
src/utils/shape.rs),没有简单的 border-radius 参数
8. 给 Web/Electron 复刻的映射速查
- 岛容器:定位在屏幕顶上
top: 10px、水平居中,尺寸与圆角用 spring 插值 (cubic-bezier或 JS 弹簧,参考stiffness 0.10 / damping 0.68);overflow: hidden做裁切 - compact:
120×27(有歌词时宽度跟着文本走,上限 700;只有媒体无歌词时 155),border-radius: 13.5px(胶囊) - expanded:
360×200,border-radius: 48px(想要 iOS 味请用 SVG 超椭圆路径) - 背景:
#000不透明;"glass" 用backdrop-filter+background: rgba(10,10,14,.588); "dynamic" 用封面模糊层 +rgba(20,20,24,.471)叠加 - 描边:
1px solid rgba(255,255,255,.118)(纯色样式,展开时淡出)/.157(glass、dynamic) - 阴影:仅展开
0 2px σ rgba(0,0,0,.11)(σ ≈ 3px) - 文字:
-apple-system / "Microsoft YaHei" / "Segoe UI",白 + alpha 分层(.65 / .72 / .9 / .6 / .5 / .58 / .62) - 图标:内联 SVG,
fill: currentColor,播放/暂停按 640 坐标系、尺寸 ≈ 34.5×37
附:班级小助手实际采纳了哪些(本次重写对照表)
| WinIsland 做法 | 出处 | 我们的落地 | 验证 |
|---|---|---|---|
| 窗口一次建大包围盒,形变发生在窗口内 | window/app/layout.rs required_window_size() + PADDING=80 |
窗口固定为「最大形态 + ISLAND_SHADOW_PAD(10)」,开合期间一帧都不 setBounds |
逐帧窗口签名恒为 476x282@482,-2(只有 1 种) |
| 尺寸/圆角由弹簧驱动 | utils/physics.rs + app.rs IslandSprings(w/h/r = 0.10/0.68) |
src/island/spring.ts 同参数同更新式(dt 以帧计、damping^dt、换向保留 35% 动量 + 20% 距离上限),并加零过冲钳制 |
岛中心/边缘波动 0.00px、无过冲 |
| 连续圆角(超椭圆) | utils/shape.rs continuous_rounded_rect_path / expanded_island_radius = min(48*scale, w/2, h/2) |
src/island/squircle.ts 超椭圆角路径;展开半径 min(48×圆角/20, w/2, h/2) |
形状 bbox 与卡片框一致(±2px) |
内容交叉淡入 expanded=progress²、mini=1-1.5×progress |
core/render.rs(MINI_FADE_RATE=1.5) |
两层常驻、透明度按同一公式 | 截图与 DOM 断言 |
| 描边 1px 白 alpha 30(纯色)/ 40(glass) | core/render.rs BORDER_*_ALPHA |
--wn-border 取同值(.118 / .157) |
计算样式断言 |
展开投影 rgba(0,0,0,.11)、y+2、σ=3 |
draw_expanded_shadow |
drop-shadow(0 2px 3px rgba(0,0,0,.11)) |
截图 |
| 纯黑底 / 亚克力(HostBackdropBrush) | background.rs + win32.rs DWMWA_USE_HOSTBACKDROPBRUSH |
纯黑底 / win.setBackgroundMaterial('acrylic') |
计算样式 + getBackgroundMaterial() |
set_cursor_hittest 命中控制 |
frame.rs(set_cursor_hittest) |
默认 setIgnoreMouseEvents(true,{forward:true}),渲染层命中测试后切换 |
岛内 true / 岛外 false |
空闲细缝 hidden_width = 5 |
core/config.rs |
6px 宽竖条(窗口固定,不再受 Windows 最小窗口高度限制) | 空闲 6x22 → 来消息 268x44 |
6 个停靠位 DockPosition |
core/config.rs |
顶/底 × 左/中/右 6 锚点,岛(而非窗口)精确落在锚点 | 6 锚点坐标断言 |
font_size = 基础字号 × 各文本系数 |
core/render/mini.rs、ui/expanded/music_view.rs |
基础字号 11–20px × 系数表(标题 1.08/正文 0.95/次要 0.78/徽标与按钮 0.76/胶囊 0.92、0.74) | 13→19px 全部文本倍率 1.462 |
| 未采纳:音乐/歌词/小组件/音量浮窗 | ui/expanded/**、ui/widget/** |
业务不同(我们做通知/作业/叫人/紧急) | — |