# souicss **Repository Path**: mengdj/souicss ## Basic Information - **Project Name**: souicss - **Description**: No description available - **Primary Language**: C++ - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-11 - **Last Updated**: 2026-10-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # direct-css 为 SOUI 和 DuiLib 提供类 Web CSS 的选择器、级联、样式桥接与 Flex 布局能力。 ## 当前能力 - 选择器:tag、`.class`、`#id`、组合器、状态伪类、`:first-child`、`:last-child`、`:only-child`、`:first-of-type`、`:last-of-type`、`:only-of-type`、`:nth-child()`、`:nth-last-child()`、`:nth-of-type()`、`:nth-last-of-type()`、`:empty`、`:root`、含组合器的完整复合选择器 `:not()`、完整属性操作符 - 插槽:宿主自动识别(直接子带 `slot` 属性),`::slotted(sel)` 对插槽内容分发样式,无 `slot` 的直接子进默认插槽 - 级联:`!important`、specificity、stylesheet source order - 样式:文字、字体、可见性、透明度、padding、`overflow` 裁剪、动态颜色/渐变/本地图片 skin、二维与 3D(降维) `transform` 与 `transform-origin` - 背景:多图层(逗号分隔列表)、`background-size`/`position`/`repeat` 完整子集(任意尺寸定位与单轴重复)、http(s) URL 识别(网络加载由宿主完成) - 盒模型与单位:完整 CSS 长度单位(含 `vmin/vmax/ch/ex`)与 `calc()`(内嵌 `min()/max()/clamp()`),块级垂直 margin 折叠 - 动效:`transition`(opacity/transform 逐帧插值)、`@keyframes` 动画与 `animation-*` 属性、easing 函数、`animation-fill-mode` - 定位:`position: static/relative/absolute/fixed/sticky`(sticky 按 relative 应用,滚动 clamp 降级)、四边偏移与同父级 `z-index`(仅定位元素)近似 - Flex:row/column 及 reverse、grow/shrink/basis、`flex`/`flex-flow`、justify(含 space-evenly)、align(含 baseline)、wrap/wrap-reverse、auto margin、分轴 gap、order、min/max - SOUI 原生属性:`soui-*`、`soui-style-*`、`soui-layout-*` 通用透传 - 多 stylesheet:替换、追加、清空,跨表级联保持 source order - SOUI 状态与资源:持久 binding 自动响应状态事件、稳定 class/attribute registry、四态动态 skin 复用与销毁回收 - 工程能力:诊断回调、解析缓存、C++17 filesystem 热重载轮询 - Windows SOUI 集成:SWindow adapter、动态 skin、css-flex layout、GUI demo - Windows DuiLib 集成:CControlUI adapter、常用属性映射、生命周期安全 binding、GUI demo - Windows NIM Duilib 集成:ui::Control adapter、属性子集映射、显式 Unbind 生命周期契约、GUI demo ## 未实现 - 完整 CSS3 其余部分(如 `:lang()`、`::selection` 等更多选择器与伪元素);除 `::slotted`、`::before/::after` 外的伪元素 - `repeating-*` 渐变与 radial 椭圆(`ellipse` / closest-side 等半径关键字、百分比半径)仍不实现 - `background-repeat` 的 `space`/`round`(明确拒绝) - 3D 变换的真实透视渲染与 z 深度排序(核心已把 3D 矩阵按最后一个 `perspective()` 做透视除法,降维投影为 2D 仿射交后端渲染) - `position:sticky` 的滚动吸附(两平台均无通用滚动容器模型,sticky 仅按 relative 应用 inset 偏移) - 多图层背景的后续层渲染(两平台单皮肤能力,仅首层生效)与 http(s) 背景的网络加载(需宿主自行下载后改为本地/data URL) - `box-shadow`/`text-shadow` 渲染(已解析并加入转发白名单,SOUI/DuiLib 无对应控件阴影 API,静默降级) - 完整 CSS stacking context(`z-index` 仅对定位元素生效,static 恢复基线层;无法实现负 z-index 画到父背景之下等浏览器级细节) - DuiLib 的 CSS 定位模型(position 族静默 `Inapplicable` 降级) - Linux/macOS 的 SOUI 集成验证 ## 构建核心库 ```bash cmake -B build -DDIRECTCSS_BUILD_TESTS=ON cmake --build build --config Release ctest --test-dir build -C Release --output-on-failure ``` ## 构建 SOUI GUI demo ```bash cmake -S demos/soui/flex_gui -B demos/soui/flex_gui/build -A x64 \ -DSOUI_KIT_DIR=E:/project/win/builded cmake --build demos/soui/flex_gui/build --config Release ``` 运行: ```text demos/soui/flex_gui/build/Release/flex_gui.exe ``` ## 框架选择(宏开关) 核心库不含任何 UI 框架依赖;选择框架就是选择对应适配器目标,宏随目标自动定义: | 框架 | 构建选项 | CMake 目标 | 自动宏 | |------|----------|------------|--------| | 无(纯核心) | — | `DirectCss::Core` | — | | SOUI | `-DDIRECTCSS_WITH_SOUI=ON` | `DirectCss::Soui` | `DIRECTCSS_WITH_SOUI` | | DuiLib | `-DDIRECTCSS_WITH_DUILIB=ON` | `DirectCss::DuiLib` | `DIRECTCSS_WITH_DUILIB` | | NIM Duilib | `-DDIRECTCSS_WITH_NIM_DUILIB=ON` | `DirectCss::Nim` | `DIRECTCSS_WITH_NIM_DUILIB`(同时定义 `DIRECTCSS_WITH_NIM`) | `find_package(DirectCss)` 的消费者只需链接对应目标,宏会随目标自动定义,不必手写: ```cmake find_package(DirectCss CONFIG REQUIRED) target_link_libraries(app PRIVATE DirectCss::Soui) # 自动获得 DIRECTCSS_WITH_SOUI ``` 不用 CMake 包、手工集成时,SOUI 定义 `DIRECTCSS_WITH_SOUI` 并链接 `directcss-soui`,DuiLib 定义 `DIRECTCSS_WITH_DUILIB` 并链接 `directcss-duilib`,NIM 定义 `DIRECTCSS_WITH_NIM_DUILIB` 并链接 `directcss-nim`。适配器公共头由对应宏守卫。 > 依赖与 CI 覆盖:DuiLib adapter 通过仓内 `third-part/duilib-master.zip`(FetchContent 固定 SHA256)拉取,CI Windows job 会编译 `directcss-duilib` 做回归。SOUI adapter 需要额外传 `-DSOUI_KIT_DIR=`(含 `__cmake/soui4-config.cmake` 与 `lib/Release`),该包不随仓分发,CI 不覆盖,仅在本地 Windows 验证。 > > SOUI 测试只支持 **Release** 配置:开发包只提供 Release 二进制(`lib/Release` + `bin/Release`),CMake 对 smoke 目标也固定链接 Release;Debug 配置下测试进程同时加载 `MSVCP140D/ucrtbased` 与 `soui4.dll`(`MSVCP140`)两个 CRT,在首个跨边界调用(`SouiAdapter.ReadsCssClassFromWindowXmlAttribute` 的 `RegisterExtensions`/`SetAttribute`)即段错误——该现象在 HEAD 干净 worktree 上同样复现,属开发包配置限制,不是适配层缺陷。 ## 接入 ```cpp #include #include directcss::SWindowAdapter::RegisterExtensions(&app); auto *engine = directcss::directcss_create_engine(); engine->SetCssLevel(directcss::CssLevel::Level3); engine->LoadStylesheet(baseCss.data(), baseCss.size()); engine->AppendStylesheet(themeCss.data(), themeCss.size()); directcss::SWindowAdapter::SetCssClass(souiRoot, "page primary"); auto *binding = directcss::SWindowAdapter::Bind(engine, souiRoot); // 状态变化会自动刷新;关闭前按顺序释放。 directcss::SWindowAdapter::Unbind(binding); directcss::directcss_destroy_engine(engine); ``` `LoadStylesheet` 校验并替换全部 stylesheet;`AppendStylesheet` 追加一份;`ClearStylesheets` 清空。`AddStylesheet` 追加并返回句柄,`RemoveStylesheet` / `SetStylesheetEnabled` 按句柄删除或启用/禁用(禁用后不参与级联,句柄 0 或不存在时返回 false)。`RegisterWatch` 在后台轮询文件并调用回调,回调需将 UI 更新切回宿主 UI 线程。 ## DuiLib 接入 ```cpp #include DuiLib::CControlUI *root = /* CPaintManagerUI 中的根控件 */; auto *engine = directcss::directcss_create_engine(); engine->LoadStylesheet(css.data(), css.size()); directcss::DuiControlAdapter::SetCssClass(root, "page"); auto *binding = directcss::DuiControlAdapter::Bind(engine, root); // 关闭前释放;root 先销毁也安全。 directcss::DuiControlAdapter::Unbind(binding); directcss::directcss_destroy_engine(engine); ``` DuiLib 支持可恢复的常用视觉属性、上下文长度、block/flex 布局和明确的 `display:none`/`visibility:hidden` 语义;`duilib-*` 去前缀后单向透传原生属性。已观察控件的 class/自定义属性变更和 option `selectchanged` 会自动刷新,宿主直接修改状态或控件树后仍需调用 `binding->Refresh()`。完整属性、降级和诊断规则见 [DuiLib 适配能力](docs/duilib_capabilities.md)。 构建和运行 DuiLib GUI demo: ```bash cmake -S demos/duilib/css_gui -B demos/duilib/css_gui/build -A x64 cmake --build demos/duilib/css_gui/build --config Release ``` 运行 `demos/duilib/css_gui/build/Release/duilib_css_gui.exe`。 ## NIM(网易云信 Duilib Framework)接入 NIM 适配器面向仓内 vendored 的 `third-part/NIM_Duilib_Framework-master`(网易云信 NIM Duilib 分支),与 DuiLib 适配器平级,仅支持 Windows。 ```cpp #include ui::Control *root = /* NIM 窗口中的根控件(ui::VBox 等) */; auto *engine = directcss::directcss_create_engine(); engine->SetCssLevel(directcss::CssLevel::Level2); // 属性选择器 [x=y] 需要 Level2+,引擎默认 Level1 engine->LoadStylesheet(css.data(), css.size()); directcss::NimControlAdapter::SetCssClass(root, "page"); auto *binding = directcss::NimControlAdapter::Bind(engine, root); // 关闭前:必须先 Unbind 恢复基线,再销毁控件树(NIM 无 OnDestroy 通知)。 directcss::NimControlAdapter::Unbind(binding); directcss::directcss_destroy_engine(engine); ``` 构建和测试(NIM 框架由 `src/nim/NimDuilib.cmake` 从 vendored 源码现场编译,不写回 `third-part/`): ```bash cmake -S . -B build-slot-nim -A x64 -DDIRECTCSS_WITH_NIM_DUILIB=ON -DDIRECTCSS_BUILD_TESTS=ON cmake --build build-slot-nim --config Release ctest --test-dir build-slot-nim -C Release --output-on-failure ``` 涉及目标:适配库 `directcss-nim`(包别名 `DirectCss::Nim`);框架侧两个静态库 `nim_duilib`、`nim_base`;单测目标 `souicss_nim_smoke`。实测(VS2022 / CMake / x64):`souicss_nim_smoke` 21/21 用例在 Debug 与 Release 下均全绿;core 回归 `souicss_smoke` 201/201 双配置通过。 NIM GUI demo(`demos/nim/`,独立自举): ```bash cmake -S demos/nim -B build-slot-nim-demo -A x64 cmake --build build-slot-nim-demo --config Release # 运行 build-slot-nim-demo\Release\nim_css_demo.exe(实测约 1.1MB,窗口稳定存活) ``` 与 DuiLib 适配的关键差异: - **生命周期契约**:`ui::Control` 没有 OnDestroy 通知,宿主必须在 `Unbind()`(恢复基线)之后再销毁控件树;先删树再 `Refresh()` 会悬空。`BindTemplate`/`UnbindTemplate` 同理。 - **排版回写时序**:NIM 在 `WM_SIZE` 与即将绘制时会用原生流式布局覆盖坐标,且根控件要到那时才拿到客户区尺寸。接管几何的宿主需在 `__super::HandleMessage` 之后补 `binding_->Refresh()`(WM_SIZE)与 `binding_->Relayout()`(WM_PAINT,只重排不重算样式,避免排版—重绘死循环)。 - **属性子集**:只桥接 NIM 有对应 setter 的子集(背景色、固定尺寸、margin→布局、统一边框边宽/颜色、display、visibility、text、color、checked、disabled、flex 全套);不桥接 border-style、border-radius、text-align、text-shadow、opacity、transform,也没有 `duilib-*` 式原生透传前缀。 - **颜色字符串**:NIM 颜色一律是字符串,适配层输出大写 `#AARRGGBB`(如 `#FF1A73E8`)。 - **字体**:`font-family` 的值当作 global.xml 中注册的字体 ID 透传给 `Label::SetFont`,需宿主在 global.xml 预先注册该 ID。 - **自动刷新边界**:仅 `SetCssClass`/`SetCssAttribute` 元数据变化自动 Refresh;NIM 侧不挂钩 option selectchanged,勾选/状态变化后需手动 `binding->Refresh()`。 - **编码**:适配层自有源文件为 UTF-8 BOM 且 target 带 `/utf-8`;vendored NIM 源码为 GBK,其注释里的非 ASCII 在 `/utf-8` 下仅 C4828 警告。 完整属性映射、tag/伪类对照与降级规则见 [NIM 适配能力](docs/nim_capabilities.md)。 ## SOUI 专有属性 SOUI 用户指南中的原生属性可通过统一前缀使用: ```css #progress { soui-min: 0; soui-max: 100; soui-value: 72; soui-showtext: 1; } #input { soui-cuetext: "请输入内容"; soui-style-colorborder: #bdc8da; soui-layout-width: 240dp; } ``` 映射规则: - `soui-foo` → `foo` - `soui-style-foo` → `style:foo` - `soui-layout-foo` → `layout:foo` SOUI 属性名匹配不区分大小写。 ## 插槽(Slot)机制 为 SOUI 与 DuiLib 提供类 Web Components 的内容插槽分发 + `::slotted()` 样式。不引入 Shadow DOM,普通选择器仍照常匹配整棵树,`::slotted()` 是额外针对插槽内容的匹配途径(向后兼容,老样式不受影响)。 **插槽宿主自动识别**:一个窗口的直接子节点中,只要有一个带 `slot` 属性,该窗口即成为插槽宿主,它的全部直接子节点都是插槽内容: - `slot="header"` → 命名插槽 `header`; - 无 `slot` 属性 → 默认插槽(name 为空); - 任意深度的后代不是插槽内容,只有直接子节点算。 在 SOUI / DuiLib 布局里给内容窗口写 `slot="name"`(XML 原值或 `SetCssAttribute`),再在 CSS 里用 `::slotted()` 给对应内容设样式: ```css #card { background-color: #eee; } /* 宿主自身 */ #card ::slotted(.header) { color: #900; font-weight: bold; } /* 命名插槽内容 */ ::slotted(p.note) { color: #090; } /* 默认插槽内容 */ ``` - `::slotted(sel)` 的参数是简单复合选择器(tag/class/id/attr/伪类),只允许出现在选择器最右侧; - 宿主上下文用组合符表达,例如 `#card ::slotted(.header)`(`#card` 是祖先); - 特异性取参数的最高值(同 `:not`/`:is` 规则); - `::slotted` 是 Level3 伪元素,使用前需 `engine->SetCssLevel(CssLevel::Level3)`。 SOUI 侧可在 XML 直接写 `slot="header"`(经 `SCssXmlAttrStorage` 读取),也可 `SWindowAdapter::SetCssAttribute(wnd, "slot", "header")`;DuiLib 侧同理(XML 原值或 `DuiControlAdapter::SetCssAttribute`)。 ### 模板插槽(模板复用 + 内容注入) 用于"模板A(公共部分 + 一个内容区),不同页面往内容区注入不同组件、其余公共"的场景。在样式层之上提供**结构归位**: - **插槽宿主容器**:模板窗口树里带 `slot-host="name"` 的窗口(DuiLib 需是 `CContainerUI` 容器),表示"这是名为 name 的内容区";它之外的公共子窗口保持不变。 - **内容子树**:页面提供,其直接子带 `slot="name"` 指向插槽;无 `slot` 进默认插槽。 把内容注入模板并应用样式(SOUI): ```cpp ISlotTemplateBinding *tpl = SWindowAdapter::BindTemplate(engine, templateRoot, contentRoot); // 内容已按 slot 归位到 slot-host 容器;::slotted() 对其生效。 tpl->Refresh(); // 内容/树变化后重刷 SWindowAdapter::UnbindTemplate(tpl); // 不再需要时释放 ``` DuiLib 用 `DuiControlAdapter::BindTemplate / UnbindTemplate`,语义一致。 模板(XML 或代码)示意: ``` templateRoot ├─ header(公共) ├─ slot-host="content"(内容区占位)← 页面组件在此归位 └─ footer(公共) ``` ```css #tplContent ::slotted(#pageCard) { color: #900; } /* 注入内容的样式 */ ``` ### public 模板示例(A/B/C 公共,D 插槽 · Win11 Fluent 风格) 仓库内置 SOUI 与 DuiLib 两个可复用模板:`demos/soui/template_demo/uires/layout/public.xml`(SOUI,随 demo 内置)与 `demos/duilib/template/public.xml`(DuiLib 独立 XML 模板,其 demo 为代码构建)。模板即:A 标题栏、B 左侧导航、C 页脚为公共区,D 是内容区插槽(`slot-host="content"`)。UI 采用 Win11 Fluent 风格:Mica 浅色背景、左侧圆角胶囊导航(选中项主题色浅底 `#e5f0fb` + 主题色 `#0067c0`)、白色圆角内容卡片、右对齐关闭按钮(hover 关闭红)。 对应可运行 demo:`demos/soui/template_demo/`(SOUI,加载 `LAYOUT:public`,Fluent 完整布局)与 `demos/duilib/template_demo/`(DuiLib,代码构建 Fluent 布局);点击 B 导航在订单/设置/关于三个功能页间切换,D 内容被替换、A/B/C 固定不变。 SOUI 加载模板并注入(不同功能页替换 D): ```cpp // 1) 模板布局:public.xml → templateRoot(含 A/B/C + D_slot 容器) // SOUI 资源加载后,templateRoot = 布局 root(或 host 的 GetRoot())。 SHostDialog host(_T("LAYOUT:public")); SWindow *templateRoot = host.GetRoot(); // 2) 功能页内容:其直接子带 slot="content" SWindow *contentRoot = new SWindow(); SWindow *pageList = new SWindow(); pageList->SetName(L"pageList"); SWindowAdapter::SetCssAttribute(pageList, "slot", "content"); contentRoot->InsertChild(pageList); // 3) 归位 + 样式 ISlotTemplateBinding *tpl = SWindowAdapter::BindTemplate(engine, templateRoot, contentRoot); // → pageList 被移入 D_slot,A/B/C 不变 ``` CSS(`::slotted` 需要 `engine->SetCssLevel(CssLevel::Level3)`): ```css #D_slot::slotted(#pageList) { background-color: #f0f4ff; } ``` 换另一个功能页时,创建新的内容树(同样带 `slot="content"`),`BindTemplate` 再次注入即可替换 D 的内容,公共区始终不变。DuiLib 用 `DuiControlAdapter::BindTemplate`,模板写法见 `demos/duilib/template/public.xml`。 两个可运行 GUI demo(`public` 模板 + 三个功能页来回注入 D): ```bash # SOUI(已部署 uires,POST_BUILD 自动复制 SOUI 运行时) cmake -S demos/soui/template_demo -B build-slot-template-demo -A x64 cmake --build build-slot-template-demo --config Release # 运行 build-slot-template-demo\Release\template_demo.exe # DuiLib(POST_BUILD 自动复制 duilib.dll) cmake -S demos/duilib/template_demo -B build-slot-duilib-template -A x64 cmake --build build-slot-duilib-template --config Release # 运行 build-slot-duilib-template\Release\duilib_template_demo.exe ``` > **DuiLib 注意**:插槽宿主(`slot-host`)须为 `CContainerUI`(demo 用 `CVerticalLayoutUI`),且要对空容器显式设宽/高(DuiLib 布局不会给空容器宽度)。不要在 `WM_SIZE` 里调用 `slot_->Refresh()`——`Reparent` 是"清空宿主+移动 content 子",二次调用会把 content 已归位内容清空(content 已空无法再注入)。页面切换应新建内容树并重新 `BindTemplate`(demo 的 `ShowPage` 正是这样)。 ## 文档 - [架构与真实能力](docs/architecture.md) - [DuiLib 适配能力](docs/duilib_capabilities.md) - [NIM 适配能力](docs/nim_capabilities.md) - [CSS → SOUI 映射](docs/css_soui_mapping.md) - [Flex 布局](docs/css_flex_layout.md) - [SOUI 控件属性](docs/soui_controls_properties.md) - [SOUI 离线手册](docs/SOUI用户指南-离线手册.md) ## License 见 [LICENSE](LICENSE)。