Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bern

一个基于 iced 的运行时驱动 UI 框架。

核心构想:控件、布局、行为三者解耦。布局是运行期文本文件—— 同一个二进制,换设备只需换布局文件;深浅色不靠任何外部配置,直接写死在 每个控件代码里,用 iced 的 Theme 作为标准接口。

三个文件体系

体系 位置 内容 决定什么
控件 src/widgets/*.rs 每个控件一个文件,实现 WidgetDef 控件行为 + 深浅色配色
布局 layouts/{common,desktop}/*.ron 平铺的两张表:areas + widgets 界面长什么样

原生窗口标题栏

Bern 默认保留操作系统标题栏;应用可以通过 WindowOptions 单独隐藏标题栏, 不影响其他 Bern 项目:

iced::application(boot, update, view)
    .window(
        bern::WindowOptions::new()
            .hide_title_bar(true)
            .into_settings(),
    )
    .run()

hide_title_bar(true) 在 macOS 上会保留 decorations,并启用透明标题栏、 隐藏标题文字及 full-size content view。这样内容可以延伸到窗口顶部,同时 保留 AppKit 的原生圆角、阴影和窗口按钮。其他平台目前回退为关闭窗口装饰; 如需鼠标拖动或自定义窗口按钮,应在应用界面层提供相应交互。

NipaPlay 三大窗口键

window_controls 是从 Flutter NipaPlay 的 WindowControlButtons 直接移植的 自定义标题栏控件。每个矩形按钮为 46×40px,完整三键总宽 138px;悬浮和 按下背景使用 90ms easeOutCubic 动画。普通按钮随深浅色显示半透明背景, 关闭按钮悬浮为 #E81123、按下为 #C50F1F 并使用白色图标。

// 完整:最小化、最大化/还原、关闭
Widget(id: "window_controls", kind: "window_controls", area: "chrome",
       props: {
           "maximized": "false",
           "leading_items": "dark_mode_rounded:切换主题:theme_toggle:20,settings_rounded:设置:settings:20",
       })

// 只保留关闭键,宽度自动缩为 46px
Widget(id: "dialog_close", kind: "window_controls", area: "title",
       props: { "close_only": "true" })

控件发布 EventKind::WindowControl(action);原生窗口直接从应用的 update 返回 bern::perform_window_control_action(action)maximized 用于在 crop_square_rounded(最大化)与 filter_none_rounded(还原)图标及 Tooltip 之间切换。close_only 版本也可以由应用把 Close 解释成关闭虚拟窗口,而 不是退出整个进程。

leading_items 会在最小化键左侧插入任意数量的同风格矩形按钮,格式为 icon:tooltip:event_id[:icon_size],多项用逗号分隔。点击扩展按钮发布普通 Pressed 事件;它们的 event_id 和 Tab 项一样必须注册到应用的 ids.rs。 完整三大键固定占 138px,每增加一项总宽自动增加 46px;close_only=true 时忽略扩展项并保持 46px 宽。

三大键和扩展键的 Tooltip 与 icon_button 共用同一套主题样式:深色为深灰 底白字,浅色为近白底黑字;样式在 Bern 当前主题构建时解析,不受应用外层 固定 iced 主题影响。

布局:平铺,不嵌套

布局文件不写嵌套树,而是两张平表,用 id 表达层级:

Layout(
    name: "phone login",
    areas: [
        Area(id: "root", kind: Stack),
        Area(id: "form", kind: Column, parent: "root", padding: 16, spacing: 10),
    ],
    widgets: [
        // 页面背景就是一个普通控件:rect + z: -1
        Widget(id: "bg", kind: "rect", area: "root", z: -1, size: Fill),
        Widget(id: "greeting", kind: "title", area: "form", props: { "text": "Welcome" }),
        Widget(id: "login", kind: "button", area: "form", size: Fill, props: { "label": "Sign in" }),
    ],
)
  • Area 是布局容器(Row / Column / Stack),parent 指向父区域——任意深度都能表达,但没有缩进地狱;
  • Widgetarea 声明它属于哪个区域,size 声明尺寸策略(Auto / Fill / Fixed(px) / Weight(n));
  • 区域内排列规则:子区域在前,控件在后(Stack 内按 z 排序)。

交互 id:集中在单个 rs 文件

所有交互控件button / icon_button / text_input)的 id 集中在应用 的一个 ids.rs 文件里管理,这是单一事实来源:

pub const THEME_TOGGLE: &str = "theme_toggle";
pub const ALL: &[&str] = &[THEME_TOGGLE, /* ... */];
  • 启动时 registry.ids().register_all(ids::ALL) 注册;
  • 框架构建布局时校验:交互控件 id 必须已注册,否则报 BuildError::UnregisteredId——布局里拼错/漏改 id 会在启动时直接暴露;
  • 应用代码一律用常量匹配事件,不手写字符串字面量;
  • ids.rs 里带一个测试,反向校验布局里的交互 id 与文件声明完全一致, 防止两侧漂移。

布局是积木:一个布局可以调用另一个布局

布局文件可以像积木一样组合:用 kind: "layout" 控件把另一个布局文件 作为控件绘制。src 通过 LayoutStore 解析——先找 desktop 目录,找不到 再回退到 common 目录。

// layouts/common/login_form.ron —— 共享积木
Layout(
    name: "login_form",
    areas: [ Area(id: "form", kind: Column, padding: 16, spacing: 10) ],
    widgets: [
        Widget(id: "greeting", kind: "title", area: "form", props: { "text": "Welcome" }),
        Widget(id: "login", kind: "button", area: "form", size: Fill, props: { "label": "Sign in" }),
    ],
)
// layouts/desktop/login_page.ron —— 桌面页面,把积木画进来
Layout(
    name: "login_page",
    areas: [ Area(id: "root", kind: Stack) ],
    widgets: [
        Widget(id: "bg", kind: "rect", area: "root", z: -1, size: Fill),
        Widget(id: "form", kind: "layout", area: "root", props: { "src": "login_form" }),
    ],
)

被嵌入的布局里所有 id 都会加上前缀(form.greetingform.login), 所以同一个积木可以在一个页面里用多次,事件也能区分是哪个实例发出的。

layout 控件支持 size: Weight(...) / Fill 占用父级剩余空间,并可用 align_x: "left|center|right"align_y: "top|center|bottom" 对齐内嵌 内容。例如顶栏右侧操作区:

Widget(id: "topbar_actions", kind: "layout", area: "topbar", size: Weight(1),
       props: { "src": "topbar_actions", "align_x": "right" })

需要让 Row / Column / Stack 容器本身填满父级时,可在 Area 上分别 设置 width: Fillheight: Fill;两轴分开配置不会让桌面顶栏意外占满 整个窗口高度。

图标包:Material Icons(默认)

bern 内嵌了 Flutter 的 Material Icons 字体与名称映射(Apache-2.0), icon_buttonicon 控件的图标名直接用 Flutter 里的名字:

Widget(id: "back", kind: "icon_button", area: "actions",
       props: { "icon": "arrow_back_rounded", "tooltip": "返回" })
Widget(id: "heart", kind: "icon", area: "actions", props: { "name": "favorite_rounded", "size": "16" })

icon_buttontooltip 是强制属性且不能为空;鼠标悬浮会显示提示文字。 缺失时布局构建直接返回 BuildError::MissingProp,避免只有图标却没有可发现 含义的按钮进入应用。Tooltip 会跟随当前主题:深色模式使用深灰容器和白字, 浅色模式使用近白容器和黑字;默认 6px 圆角,文字内边距为上下 3px、左右 9px。

  • 8825 个图标,名字与 Icons.xxx 完全一致(adddark_modefavorite_rounded……);
  • 未知名字回退为普通文本字形("→" 这类字符仍然可用);
  • 应用启动时调用一次 bern::icons::load() 加载字体:
fn boot() -> (App, iced::Task<AppMessage>) {
    (App::load(), bern::icons::load().map(AppMessage::FontLoaded))
}

矢量形变底层(图标果冻切换)

框架的 [core::morph] 模块是引擎级的「图标形变」基础:用 ttf-parser 直接从内嵌字体提取字形轮廓(二次/三次贝塞尔全部采样成折线),按弧长均匀 重采样、归一化到 24×24 图标网格,再在两个字形之间逐点插值。配合带过冲 回弹的果冻缓动([morph::jelly]),图标从 1 切换到 2 时是真正的矢量 扭曲形变,而不是交叉淡入。

  • icon_buttonicon prop 变化时自动触发形变(例如主题开关的 light_mode_roundeddark_mode_rounded),morph_duration_ms 调时长 (默认 420ms);
  • morph_icon 是独立的形变图标控件,布局里可直接使用:
Widget(id: "toggle", kind: "morph_icon", area: "actions",
       props: { "icon": "dark_mode_rounded", "size": "20" })
  • 任意两个图标都能形变:轮廓数量不一致时(月牙 1 轮廓 → 太阳 9 轮廓), 缺失的轮廓从对方字形质心“生长/收缩”,视觉上就是果冻鼓包/回缩;
  • 字形提取有缓存,同一字符只解析一次;动画运行在控件自身状态里,引擎 自动跟随主题揭示(RevealWrapper),控件无需额外订阅。

深浅色:写进控件代码,用规范接口

深浅色不允许在单独的地方(主题文件、配置项)配置。每个控件在它自己的 .rs 文件里内置浅色和深色配色,构建时从 BuildContext::theme&iced::Theme)取色:

  • rect 背景 = theme.palette().background
  • text / title = theme.palette().text
  • button = 主题的 primary 系列色(含 hover/pressed)
  • icon_button = theme.palette().text

切换深浅色就是切换 iced::Theme(Light/Dark)这一个标准接口,所有控件 自动跟着变,没有任何外部配置参与。

框架里有一个主题路由([ThemeRouter]):它是运行期唯一持有当前 iced::Theme 和 primary 强调色的地方。set_accent 会重建当前深浅模式的 自定义 iced 主题,theme_for 在切换模式时保留强调色;registry.build 把主题分发给每个控件。控件只负责「拿到主题 → 用自己的调色板着色」。

背景的圆形切换动画(参考 nipaplay)

rect 背景控件在颜色因交互切换时(例如点击主题开关),会以按下按钮的 坐标为原点做圆形揭示动画,与 nipaplay 的深浅色切换一致:

  • 切到深色:新颜色从按钮位置向外铺开,直到盖满控件;
  • 切回浅色:反向——旧颜色圆从边缘向内收缩到按钮位置,露出新颜色;
  • 非交互式的颜色变化(没有按钮坐标)直接切换,不做动画;
  • 时长默认 420ms、easeOutCubic 缓动,可用 duration_ms 属性调整:
Widget(id: "bg", kind: "rect", area: "root", z: -1, size: Fill,
       props: { "duration_ms": "600" })

机制:可交互控件(如 icon_button)按下时把自身中心坐标写入共享的 [PressOrigin],背景控件构建时读取并消费它。

通知式两阶段切换

点击主题开关时,深浅色模式不会立即切换,而是走通知式两阶段流程:

  1. 背景从按钮位置开始圆形揭示目标色(此时模式还是旧的);
  2. 引擎层自动跟随:注册表构建每个控件时自动用 [RevealWrapper] 包裹(背景控件除外),wrapper 在事件驱动下把控件坐标 注册进 [ThemeReveal] 协调器(不轮询);
  3. 圆形扫过控件坐标时,协调器向该控件投递一条一次性命令take_command),wrapper 用目标主题重新构建该控件——按钮、文本、 标题、图标全部随扩散依次切换到目标色,控件本身无需任何订阅代码;
  4. 整个扩散动画完成后(所有控件必然已被覆盖),背景发布 LayoutMessage::ThemeRevealDone
  5. 应用收到通知后才真正切换 ThemeRouter——保证动画全程可见,且切换瞬间 每个控件下方的背景颜色都已改变。

扩散方向由背景自动判定:变暗时圆向外扩大(扫到即命令),变亮时圆向内 收缩(控件离开旧色圆即命令)。非交互式颜色变化(没有按钮坐标)直接切换。 这一层是框架的底层机制:控件零参与(引擎自动包裹),应用只负责在收到 ThemeRevealDone 后切换模式,互不耦合。

全局界面缩放(引擎级)

所有控件都能整体放大:registry.scale() 持有一个共享缩放因子 (默认 1.0,范围 1.0..=2.0)。Registry::build 会把整棵界面包进一个 缩放包装器:

  • 布局用「虚拟窗口」(窗口尺寸 ÷ scale)排布内容,再整体放大 scale 倍, 内容始终填满窗口、不会跑出屏幕;
  • 绘制对整棵子树施加缩放变换,鼠标坐标按 1/scale 反向换算,点击照常命中;
  • 控件代码零感知,应用设置一次、下一次构建整棵界面就按新比例渲染:
app.registry.scale().set(1.5);

helloworld 的视频页有一个「缩放滑块」,value 0..1 映射到 1.0..2.0。

横向 Tab(移植自 nipaplay 左上角导航)

h_tab 控件就是 nipaplay 主界面左上角那排 Tab 的移植:加粗标签、 悬浮时 1.1 倍平滑放大(200ms ease-out),选中的标签用主题强调色,底部 有一条 3px 高的胶囊指示器,切换时在标签之间滑动(300ms)。

Widget(id: "nav", kind: "h_tab", area: "topbar",
       props: { "items": "首页:tab_home,视频:tab_video,媒体库:tab_library" })
  • items 是逗号分隔的 label:key 列表,key 就是该项的交互 id—— 和按钮一样写在应用的 ids.rs 里,构建布局时框架会校验每一项都已注册;
  • 按下某个 Tab 发布 (key, Pressed) 事件,应用按 id 切换内容;高亮和 胶囊移动由控件自己维护,应用无需驱动;
  • 所有标签始终加粗:选中与未选中标签都用粗体字族渲染;selected prop 只负责当前项的强调色和底部胶囊位置(应用点击后写回,和滑块 value 同一套路);
  • 多页面切换:应用收到 Tab 事件后,把页面容器(kind: "layout") 的 src 换成另一个布局文件即可换页——helloworld 里就是 hello_card / page_video / page_library 三个独立布局在运行时切换, 同一个二进制不重新编译;
  • 配色全部来自 iced 主题(深浅色写死在控件代码里):未选中标签 深色 60% / 浅色 54% 透明度,选中标签和胶囊用主题强调色;
  • 可调属性:font_sizehover_scaleduration_ms(悬浮)、 indicator_ms(胶囊滑动)、indicator_heightindicator_radiusitem_paddingselected

iced 0.14 的字体库默认没有粗体字面,Weight::Bold 会被忽略。框架的 fonts 模块启动时加载系统粗体中文字体(macOS 用 Hiragino Sans GB), 应用在 boot 里调用一次 bern::fonts::load_bold() 即可让加粗生效; 找不到粗体字体会优雅回退为常规字重。

滑块与进度条(胶囊轨道 + 果冻滑块)

slider(可拖动)和 progress(纯展示)是视频播放进度条的移植, 照抄 nipaplay 的 VideoProgressBar

  • 4px 胶囊轨道(文字色低透明度),已播放部分用文字色胶囊段——深色下 是白色(与 nipaplay 完全一致),浅色下自动变深,任何主题都可见;
  • 28 × 16 胶囊滑块(圆角 = 高的一半,跟随主题文字色)+ 两道柔和阴影;
  • 悬浮时滑块放大 8%(160ms easeOutCubic,逐帧动画);
  • 按下时滑块被弹簧压扁(变窄变长,刚度 620 / 阻尼 22),松开后换 低阻尼弹簧(刚度 360 / 阻尼 7.2)过冲回弹并振荡落定——这就是 nipaplay 的果冻动画,参数原样照抄。
// 可拖动滑块:发布 (id, Changed(value)),value ∈ 0..=1
Widget(id: "seek", kind: "slider", area: "root", props: { "value": "0.35" })

// 纯展示进度条:不响应交互,按 value 显示
Widget(id: "seek_progress", kind: "progress", area: "root", props: { "value": "0.35" })
  • value 属性是唯一数据源:拖动/点击后应用把新值写回布局,下一次构建 即按新值渲染(helloworld 里 set_seek 更新滑块);
  • 滑块是交互控件,id 照旧写在 ids.rs;内嵌在页面布局里时事件 id 会带 嵌入前缀(如 page.seek),应用用 {容器id}.{控件id} 匹配;
  • 可调属性:valuesize(宽度策略)。

下拉菜单(移植自 nipaplay BlurDropdown)

dropdown 控件与 nipaplay 的模糊下拉菜单外观一致:

Widget(id: "lib_sort", kind: "dropdown", area: "root",
       props: { "items": "名称:sort_name,大小:sort_size,时间:sort_time",
                "selected": "sort_name" })
  • 40px 圆角触发器:边框在打开时变强调色,右侧箭头旋转 180°;
  • 点击弹出下方菜单:200ms 淡入 + 0.95→1 缩放,圆角 6 面板 + 柔和阴影, 菜单项带分隔线、选中底色、悬浮高亮;
  • 点击菜单项发布 (item_key, Pressed) 并关闭;点击菜单外(遮罩)关闭;
  • items 的键是交互 id(进 ids.rs),selected 由应用点击后写回—— 和 Tab / 滑块同一套路;
  • 菜单浮层通过 iced overlay 实现,引擎的缩放包装器(ScaleWrapper)与 主题揭示包装器都会自动转发浮层,缩放状态下菜单位置/尺寸同样按比例。

圆角矩形按钮(移植自 nipaplay 大屏可聚焦动作)

round_button 是 nipaplay 大屏操作按钮的移植:固定的圆角 8 表面, 悬浮时表面不变、只有内容放大并出现强调色描边:

Widget(id: "lib_sync", kind: "round_button", area: "root",
       props: { "icon": "sync_rounded", "label": "同步" })
  • 浅色 82% / 深色 10% 的白色填充,圆角 8,默认带 1px 文字色描边 (一眼可辨「有容器」);
  • 悬浮时内容放大 1.035(140ms easeOutCubic)+ 描边换 2px 强调色;
  • 图标(Material,带果冻形变)+ 粗体标签,内容色浅色 black87 / 深色白;
  • 可调属性:labeliconicon_sizefont_sizescaleduration_ms; 按下发布 (id, Pressed) 并记录主题揭示原点。
  • 悬浮时图标和文字变成主题强调色(移开恢复),配合内容放大有明确的 交互反馈。

无容器图标+文本按钮(action_button)

action_buttonicon_button 同一个悬浮缩放核心,但没有容器—— 只有图标和文字,悬浮时整体放大:

Widget(id: "lib_sort_btn", kind: "action_button", area: "root",
       props: { "icon": "sort_by_alpha_rounded", "label": "排序" })
  • 21px 图标 + 8px 间距 + 15px 粗体标签(nipaplay 动作按钮布局);
  • 无背景、无边框,悬浮放大默认 1.08(140ms easeOutCubic), scale / duration_ms 可调;
  • 图标 Material(带果冻形变),颜色跟随文字色;按下发布 (id, Pressed)
  • 悬浮时图标和文字变成主题强调色(和 icon_button 一致)。

输入框(移植自 nipaplay 媒体库搜索框)

text_input 直接照抄 nipaplay 媒体库的搜索框外观:

Widget(id: "lib_search", kind: "text_input", area: "root",
       props: { "placeholder": "搜索媒体库", "value": "" })
  • 提示文字(placeholder)由布局文件传入,默认显示;
  • 左侧 Material search_rounded 图标,正文用粗体;
  • 浅色 82% / 深色 9% 的白色填充,圆角 8 边框(文字色 10%),聚焦时 强调色 2px 边框;
  • 输入发布 (id, TextChanged(text)),应用把新词写回布局 value—— 和滑块/下拉同一套路(布局是唯一数据源)。

多窗口分区背景(split_pane)

split_pane 把页面像桌面多窗口工作区一样切成多个区块,区块之间使用 主题自适应的 1px 细线分隔。horizontal 表示从左到右排列,vertical 表示从上到下排列:

Widget(id: "workspace", kind: "split_pane", area: "root", size: Fill,
       props: {
           "direction": "horizontal",
           "panes": "navigation,editor,inspector",
           "weights": "1,4,2",
           "divider_width": "1",
           "divider_inset": "16",
       })
  • panes 是逗号分隔的布局名,由 LayoutStore 解析并分别嵌入每个区块;
  • weights 控制区块比例,缺省时等分;嵌入控件的事件 id 会自动带上 workspace.pane0workspace.pane1 等前缀;
  • 不传 panes 时,用 sections: "3" 创建纯分区背景,内容可由页面自己的 区域覆盖上去;
  • 某个 pane 对应的布局里可以再次放置 split_pane,从而组合左右和上下分区;
  • divider_inset 控制分隔线两端留白(默认 12px):竖线留出上下边距, 横线留出左右边距;
  • 背景复用 rect 的深浅色圆形揭示动画。当前边界为静态分隔线,不支持拖拽。

虚拟窗口(virtual_window)

virtual_window 参考 Nipaplay 的 NipaplayWindow:在当前页面里显示一个 带圆角、阴影和窗口控制区的模态虚拟窗口。窗口使用全视口 overlay,即使 声明在页面内容区里也能覆盖顶部 Tab;内容由另一个 RON 布局提供:

Widget(id: "settings_window", kind: "virtual_window", area: "root", size: Fill,
       props: {
           "src": "settings_content",
           "title": "设置",
           "width": "850",
           "height_factor": "0.8",
           "scrim_alpha": "0.06",
       })
  • 默认居中,普通模式保留 20px 外边距;双击标题栏切换到保留 10px 边距的 铺满模式,再次双击还原;
  • 标题栏支持鼠标拖动,窗口始终保留一段可见标题区,避免完全拖出页面;
  • 默认 15px 圆角、柔和阴影和 250ms easeOutBack 入场缩放;
  • macOS 默认显示左上红色关闭点,其他平台显示右上 Fluent 关闭按钮;可用 close_style: "mac" / "fluent" 强制指定;
  • 点击关闭按钮或窗口外遮罩会发布 (window_id, Other("close"));窗口 id 因此需要注册在应用 ids.rs
  • src 指向 LayoutStore 中的内容布局,内部事件 id 自动带窗口前缀;
  • 常用属性:widthheightheight_factormarginfilled_marginradiustitle_heighttitle_sizecontent_paddingscrim_alphadismiss_on_scrimshow_closeclose_styleanimation_msmode

设置界面控件

Bern 提供 NipaPlay 桌面设置页需要的布局积木和独立设置项控件:

  • side_tabitems 直接传入 icon:文本:编号,多项用逗号分隔;点击后由 控件自身发布选中的编号,应用只需切换右侧页面;
  • rounded_container:用 src 承载一个子布局,默认 8px 圆角、0.5px 半透明边框,可调整 paddingradiusbackground_opacityborder_opacity
  • switch:Fluent 风格胶囊开关,使用当前主题的 primary 强调色并发布 Toggled(bool)
  • scroll_layout:只负责为 src 指向的子布局提供滚动视口,不保存页面内容;
  • divider:由布局显式放置的主题自适应细分割线;
  • setting_label:只绘制设置项的图标、标题和副标题,由布局把它与已有的 switchdropdownslider 放进同一个 Row
  • setting_colorsetting_hotkeysetting_actionsetting_info:用于 Bern 没有通用控件可复用的颜色、快捷键、操作和信息设置项。
Widget(id: "settings_nav", kind: "side_tab", area: "left", size: Fill,
       props: { "items": "palette_rounded:外观:0,language_rounded:语言:1",
                "selected": "0" })

Widget(id: "auto_play", kind: "switch", area: "row",
       props: { "value": "true" })

Widget(id: "card", kind: "rounded_container", area: "content",
       props: { "src": "playback_options", "radius": "8" })

// playback_options.ron:同一 Row 里直接组装说明文字和已有控件
Area(id: "row_auto_next", kind: Row, parent: "root",
     padding: 14, spacing: 12, align_y: "center", width: Fill)
Widget(id: "auto_next_label", kind: "setting_label", area: "row_auto_next",
       size: Weight(1), props: { "icon": "skip_next_rounded",
       "title": "自动连播", "subtitle": "播放结束后自动播放下一集" })
Widget(id: "auto_next", kind: "switch", area: "row_auto_next",
       props: { "value": "true" })

Widget(id: "play_core", kind: "dropdown", area: "row_play_core",
       size: Fixed(200), props: {
           "items": "MDK:play_core_mdk,Libmpv:play_core_libmpv,Erika:play_core_erika",
           "selected": "play_core_mdk",
       })

推荐结构是“滚动页布局 → 分区标题 + rounded_container → 语义化分区子布局 → setting_label + 已有交互控件 + divider”。交互控件和下拉选项的语义化 id 必须集中登记到应用的 ids.rs;应用行为层只负责把新值写回布局节点。

页面背景 rect 还支持 light_color / dark_color#RRGGBB#AARRGGBB 精确色值,同时保留主题圆形揭示动画。

目录结构

Bern/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── core/
│   │   ├── widget.rs     # WidgetDef、LayoutMessage、事件桥
│   │   ├── registry.rs   # 注册表 + 布局运行时(areas -> iced 树)
│   │   ├── layout.rs     # RON 布局解析(areas + widgets)
│   │   ├── morph.rs      # 矢量形变底层(字形提取/重采样/果冻插值)
│   │   └── store.rs      # 布局目录加载(common + 设备)
│   ├── icons/            # Material Icons(内嵌字体 + 名称映射)
│   └── widgets/          # 每个控件一个文件
│       ├── h_tab.rs      # 横向 Tab(悬浮放大 + 底部胶囊指示器)
│       ├── rect.rs       # 背景/色块
│       ├── title.rs      # 标题
│       ├── icon.rs       # 图标
│       ├── morph_icon.rs # 形变图标(矢量果冻切换)
│       ├── icon_button.rs # 图标按钮(悬浮放大 + 图标形变)
│       ├── split_pane.rs # 左右/上下多窗口分区 + 细线边界
│       ├── virtual_window.rs # 可拖动、可铺满的模态虚拟窗口
│       ├── side_tab.rs # 左侧设置 Tab(图标 + 文本 + 编号)
│       ├── rounded_container.rs # 可嵌入布局的圆角矩形容器
│       ├── scroll_layout.rs # 可滚动的子布局容器
│       ├── divider.rs # 布局显式组装的细分割线
│       ├── switch.rs # Fluent 风格设置开关
│       ├── setting_label.rs # 设置项图标、标题和副标题
│       ├── setting_color.rs # 独立颜色设置项
│       ├── setting_hotkey.rs # 独立快捷键设置项
│       ├── setting_action.rs # 独立操作设置项
│       ├── setting_info.rs # 独立信息设置项
│       ├── slider.rs     # 滑块 + 进度条(胶囊轨道 + 果冻滑块)
│       ├── text.rs
│       ├── text_input.rs
│       └── button.rs
└── layouts/
    ├── common/
    │   └── login_form.ron
    └── desktop/
        └── login_page.ron

assets/fonts/                # 内嵌的 MaterialIcons-Regular.otf + 许可证

用法

let store = bern::LayoutStore::load("layouts/desktop", "layouts/common")?;
let layout = store.resolve("login_page").expect("layout exists");

let registry = bern::builtin_registry();
let element = registry.build(layout, &iced::Theme::Dark, &store)?;

布局里的控件事件以 (widget_id, event) 的通用形式产生 (bern::LayoutMessage::Event),应用层再映射到自己的类型化消息。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages