Slint 布局系统内幕:从编译期降级到运行时求解的两阶段架构剖析
【免费下载链接】slintSlint is an open-source declarative GUI toolkit to build native user interfaces for Rust, C++, JavaScript, or Python apps.项目地址: https://gitcode.com/GitHub_Trending/sl/slint
本文面向 Slint 的贡献者、嵌入引擎与扩展布局功能的开发者,系统剖析 Slint 布局系统从编译期降级(lowering)到运行时求解的完整实现链路。读者将掌握
LayoutInfo约束系统、layout_items()收缩/增长算法、网格与 Flexbox 求解流程、布局缓存与 repeater 间接寻址机制,以及新增布局属性、调试布局问题的标准操作模式。文中所有结论均以当前仓库源码(internal/core/layout.rs、internal/compiler/layout.rs、internal/compiler/passes/lower_layout.rs等)为事实依据。
概述:两阶段架构与四种布局类型
Slint 的布局系统由两个截然不同的阶段组成:
- 编译期(Compile-time):布局元素被降级(lower)为约束表达式与缓存结构。这一阶段发生在 internal/compiler/passes/lower_layout.rs 中的
lower_layouts()函数(第 470 行起),它会遍历组件树,将HorizontalLayout、VerticalLayout、GridLayout、FlexboxLayout等布局元素转换为一组合成属性(synthetic properties)与绑定表达式。 - 运行时(Runtime):约束被求值,位置与尺寸被实际计算。这一阶段的核心算法全部位于 internal/core/layout.rs,包括
layout_items()、solve_box_layout()、solve_grid_layout()、organize_grid_layout()等公开函数。
这种分工的意义在于:编译期把"布局应该怎么排"的静态结构固化下来,运行时只需要做纯数值计算,从而在嵌入式与 MCU 等低算力环境下也能保持可预测的性能。
Slint 提供四类布局元素,覆盖不同的排布需求:
| 布局类型 | 说明 |
|---|---|
HorizontalLayout/VerticalLayout | 线性盒式布局(box layout),沿主轴依次排布子元素 |
GridLayout | 二维网格布局,支持行/列定位与colspan/rowspan跨单元格 |
Dialog | 特殊的网格布局,按平台约定对按钮进行排序(DialogButtonRole) |
FlexboxLayout | CSS Flexbox 风格布局,支持 wrap、direction 等特性 |
其中Dialog在编译器内部复用GridLayout数据结构——从 internal/compiler/layout.rs 第 684-686 行可以看到,GridLayout结构体携带一个可选的dialog_button_roles: Option<Vec<SmolStr>>字段,当网格实际是一个Dialog时,所有按钮位于单元格起始位置并按角色排序。
关键文件地图
布局系统横跨编译器与运行时两层,各文件职责如下:
| 文件 | 用途 |
|---|---|
| internal/core/layout.rs | 运行时布局求解算法:layout_items、solve_box_layout、solve_grid_layout、organize_grid_layout等 |
| internal/compiler/layout.rs | 编译器侧布局数据结构:GridLayout、BoxLayout、FlexboxLayout、LayoutConstraints、LayoutGeometry |
| internal/compiler/passes/lower_layout.rs | 将布局元素降级为表达式与合成属性的 pass |
| internal/compiler/passes/default_geometry.rs | 设置默认 width/height(在布局降级之后运行) |
| internal/compiler/llr/lower_layout_expression.rs | 将布局表达式转换为 LLR(低层表示) |
| internal/compiler/expression_tree.rs | 定义LayoutCacheAccess、GridRepeaterCacheAccess、OrganizeGridLayout等表达式节点 |
约束系统:LayoutInfo 与约束合并
LayoutInfo(运行时)
运行时每个元素沿一个轴(水平或垂直)的约束由一个LayoutInfo描述。其定义位于 internal/core/layout.rs 第 22-37 行:
#[repr(C)] #[derive(Clone, Copy, Debug, PartialEq)] pub struct LayoutInfo { /// The maximum size for the item. pub max: Coord, /// The maximum size in percentage of the parent (value between 0 and 100). pub max_percent: Coord, /// The minimum size for this item. pub min: Coord, /// The minimum size in percentage of the parent (value between 0 and 100). pub min_percent: Coord, /// the preferred size pub preferred: Coord, /// the stretch factor pub stretch: f32, }字段含义总结:
- min / max:元素在该轴上的硬性最小/最大尺寸,单位
Coord(在桌面端通常是f32逻辑像素,在整数平台上可退化为i32)。 - min_percent / max_percent:同样的 min/max,但表示为父尺寸的百分比(0~100)。
- preferred:首选尺寸(元素"自然想要"的大小)。
- stretch:伸缩因子,
0.0表示不参与伸展(不长大)。尺寸类型为Coord,伸缩因子为f32。
LayoutInfo的Default实现(第 39-50 行)给出了默认值:min = 0、max = Coord::MAX、min_percent = 0、max_percent = 100、preferred = 0、stretch = 0。
约束合并规则
当约束需要合并时(例如嵌套布局中父布局聚合所有子项的约束),LayoutInfo::merge()(第 57-66 行)执行如下"最紧约束优先"策略:
pub fn merge(&self, other: &LayoutInfo) -> Self { Self { min: self.min.max(other.min), max: self.max.min(other.max), min_percent: self.min_percent.max(other.min_percent), max_percent: self.max_percent.min(other.max_percent), preferred: self.preferred.max(other.preferred), stretch: self.stretch.min(other.stretch), } }| 字段 | 合并策略 | 原因 |
|---|---|---|
| min / min_percent | 取较大者(更紧) | 两个下限中更严格的一个决定了最小可用空间 |
| max / max_percent | 取较小者(更紧) | 两个上限中更严格的一个决定了最大允许空间 |
| preferred | 取较大者 | 首选尺寸取大值,保证内容不被压缩 |
| stretch | 取较小者 | 取小伸缩因子,避免超出任何一方的伸展意愿 |
源码第 52-55 行的注释明确提示:这套合并逻辑在 C++ 生成器的生成代码以及编译器const_propagationpass 中有对应的重复实现(后者会在编译期折叠常量LayoutInfo的合并),修改时需保持三者一致。此外preferred_bounded()(第 70-72 行)将首选尺寸夹紧到 [min, max] 区间内,用于生成最终落到元素上的尺寸。
约束属性
在.slint语言层面,开发者可以给任意元素指定以下约束属性,编译器会把它们收集进LayoutConstraints:
min-width、min-heightmax-width、max-heightpreferred-width、preferred-heighthorizontal-stretch、vertical-stretch
LayoutConstraints结构体定义在 internal/compiler/layout.rs 第 187-205 行,每个约束都是一个Option<NamedReference>(指向元素上对应属性的绑定表达式),外加fixed_width/fixed_height两个固定尺寸标志,以及一个LayoutConstraintLocality(第 210-219 行)——逐项记录每个约束是直接设置在本元素上(覆盖自基组件)还是继承自基组件。继承来的约束已经烘焙进元素的layoutinfo-*合成属性中,因此通过 layout-info 测量过该单元格的父布局不得再次应用它们,否则会造成双重计数或 height-for-width 环路。LayoutConstraints::new()(第 237 行起)在收集约束的同时,还会在同时指定width与min-width等冗余约束时产生诊断信息。
布局求解算法
盒式布局与网格布局共用同一套核心求解算法layout_items()(internal/core/layout.rs 第 270-289 行):
1. 将所有项初始尺寸设为首选值(preferred) 2. 计算所需总尺寸(preferred 之和) 3. 若总尺寸 > 可用空间: → 按伸缩因子加权收缩各项,且尊重 min 约束。 当所有项都没有伸缩因子时,每项让出相同的像素数, 因此小项会先于大项耗尽空间;触及 min 的项被冻结, 剩余差额在其他项之间重新分配。 4. 若总尺寸 < 可用空间: → 按伸缩因子比例增长各项 → stretch = 0 的项保持首选尺寸不变 5. 按间距(spacing)顺序分配位置与文档中的伪代码一一对应,layout_items()的真实实现是:
pub fn layout_items(data: &mut [LayoutData], start_pos: Coord, size: Coord, spacing: Coord) { let size_without_spacing = size - spacing * (data.len() - 1) as Coord; let mut pref = 0 as Coord; for it in data.iter_mut() { it.size = it.pref; pref += it.pref; } if size_without_spacing >= pref { adjust_items::<Grow>(data, size_without_spacing); } else if size_without_spacing < pref { adjust_items::<Shrink>(data, size_without_spacing); } let mut pos = start_pos; for it in data.iter_mut() { it.pos = pos; pos = Saturating::add(pos, Saturating::add(it.size, spacing)); } }其中第 5 步"顺序分配位置"在代码里体现为末尾的for循环:pos从start_pos出发,累加每项尺寸与间距(使用Saturating::add防止Coord为整数类型时溢出)。
增长与收缩的精细控制
adjust_items::<Grow>与adjust_items::<Shrink>(第 208-268 行)实现了迭代式分配:每一轮先统计所有"还能继续增长/收缩"(can_grow() > 0)的项及其伸缩因子总和,计算本轮可分配量,按grow * actual_stretch(stretch)分发;触及 min/max 边界(can_grow() <= 0)的项被排除在后续轮次之外。注意actual_stretch的兜底逻辑(第 219 行):当所有项的伸缩因子总和为 0 时,退化为 1.0,即"每项平均让出相同像素"——这正是文档所述"小项先耗尽、大项后耗尽"的行为来源。
代码中还处理了一个数值细节(第 255-266 行):当Coord为整数且每项可分配量不足 1 像素时,剩余像素会被整体让给伸缩因子最大的那一项,避免死循环。
该算法自带单元测试test_layout_items(第 291-314 行),是理解算法行为的绝佳入口:三项目标分别为[100,200]、[50,300]、[50,150](pref 均为 100)时,650 空间下按各自 max 上限分配为200/300/150,200 空间下按各自 min 下限收缩为100/50/50,300 空间下则全部落在首选尺寸100/100/100。
盒布局对齐模式
当各项在未触发收缩的情况下仍有多余空间("装得下")时,由alignment属性决定摆放方式:
| 对齐模式 | 行为 |
|---|---|
Stretch | 拉伸各项填满空间(默认) |
Start | 从起始端紧密排布 |
Center | 居中排布 |
End | 靠末端排布 |
SpaceBetween | 项之间等距(两端无间隙) |
SpaceAround | 项周围等距(含两端各半份间隙) |
SpaceEvenly | 含两端在内的等距间隙 |
对齐模式的运行时实现位于solve_box_layout()(第 1229 行起)的对齐 switch 分支,编译器侧的LayoutGeometry.alignment字段(internal/compiler/layout.rs 第 576 行)负责把元素上的alignment属性绑定提取出来。盒布局还支持每项独立的cross-axis-self-alignment覆盖(运行时对应LayoutItemInfo.cross_axis_self_alignment,见 internal/core/layout.rs 第 1181-1182 行)。
网格布局
GridLayout的两个轴分别独立求解,唯一的例外是垂直方向 pass 会在水平方向 pass 已解出的列宽上测量 height-for-width 单元格。整体流程分三步:
- Organize(组织):将单元格定义转换为行/列归属。运行时函数为
organize_grid_layout()(internal/core/layout.rs 第 802 行起),输入包含GridLayoutInputData、repeater_indices与repeater_steps。 - Solve horizontal(水平求解):计算各列宽度与 x 位置,入口为
solve_grid_layout()(第 1035 行起)。 - Solve vertical(垂直求解):计算各行高度与 y 位置。
colspan/rowspan大于 1 的单元格无法在单轮内确定约束,需要迭代式约束分发——从to_layout_data()(第 318 行起)的代码可以看到,对于跨多列/行的单元格(has_spans = true),算法先把单元格的 min/max/pref/stretch 汇总到所覆盖的每一列/行上,再在后续轮次中根据实际列宽/行高反向修正跨度单元格的尺寸(col_or_row_and_span负责将扁平索引映射为"起始行/列 + 跨度")。
Flexbox 布局
与盒布局、网格布局不同,FlexboxLayout的两个轴同时求解。其布局算法由taffycrate 提供——internal/core/Cargo.toml 第 154 行的依赖声明为:
taffy = { version = "0.10", default-features = false, features = ["flexbox", "taffy_tree", "alloc"] }taffy 实现了 CSS flexbox 算法,Slint 在SolveFlexboxLayout/SolveFlexboxLayoutWithMeasure表达式(internal/compiler/expression_tree.rs)中调用它,并在运行时通过FlexboxLayoutInfoCrossAxisWithMeasure把 taffy 的 measure 回调接回 Slint 的 layout-info 系统(详见"测量重复单元格"一节)。
编译期降级
lower_layouts()pass(internal/compiler/passes/lower_layout.rs 第 470 行起)负责把布局元素转换为可执行表达式。以GridLayout为例,完整降级链路为:
GridLayout element ↓ lower_grid_layout() // lower_layout.rs 第 629 行起 ↓ 创建合成属性(synthetic properties): - layout-organized-data (单元格组织结果) - layout-cache-h (水平方向位置/尺寸缓存) - layout-cache-v (垂直方向位置/尺寸缓存) - layoutinfo-h, layoutinfo-v(约束信息) ↓ 子元素的 x/y/width/height 绑定到缓存访问表达式盒布局与 Flexbox 布局分别由lower_box_layout()(第 1748 行起)与lower_flexbox_layout()(第 1917 行起)处理,其中lower_box_layout会把Layout::BoxLayout写入元素的d.layout供后续 pass 使用(第 1913 行)。
关键生成表达式
降级过程中编译器生成以下表达式节点(定义见 internal/compiler/expression_tree.rs):
| 表达式 | 用途 |
|---|---|
OrganizeGridLayout | 计算单元格的行/列归属 |
SolveBoxLayout | 计算盒布局各项的位置与尺寸 |
SolveGridLayout | 计算网格布局各项的位置与尺寸 |
SolveFlexboxLayout | 计算 flexbox 布局各项的位置与尺寸 |
ComputeLayoutInfo | 计算合并后的约束 |
LayoutCacheAccess | 从缓存读取位置/尺寸(盒布局与网格静态项) |
GridRepeaterCacheAccess | 两级间接缓存读取(网格中的 repeater) |
从表达式节点的类型签名可以看出设计意图:OrganizeGridLayout返回ArrayOfU16(单元格索引数组),SolveBoxLayout/SolveFlexboxLayout返回LayoutCache类型,而LayoutCacheAccess/GridRepeaterCacheAccess返回LogicalLength——即子元素的x/width绑定最终都解析为对缓存数组的数值读取。
关键数据结构
编译器侧
全部定义在 internal/compiler/layout.rs:
GridLayout(第 678-690 行):包含所有GridLayoutElement(单元格)、LayoutGeometry(padding、spacing、alignment),以及两个附加职责字段——当网格实际是Dialog时携带dialog_button_roles(平台相关按钮排序),以及uses_auto标志(标记是否有任何行/列表达式使用auto)。BoxLayout(第 735-742 行):包含方向(orientation)、各项LayoutItem、LayoutGeometry,以及可选的cross_alignment(cross-axis-alignment属性绑定)。LayoutConstraints(第 187-205 行):每个min-/max-/preferred-宽高与每个 stretch 各一个Option<NamedReference>,加上fixed_width/fixed_height两个固定尺寸标志,以及LayoutConstraintLocality(第 210-219 行)——逐个记录每个命名引用是设置在元素自身(override)还是继承自基组件。继承者已烘焙进元素的layoutinfo-*,因此通过 layout-info 测量过该单元格的父布局不得再重复应用它们,否则会重复计数或引入 height-for-width 环路。LayoutGeometry(第 573-578 行):rect(x/y/width/height 引用)、spacing(水平/垂直间距)、alignment、padding(四边内边距)。其构造函数(第 590-618 行)实现了spacing到spacing-horizontal/spacing-vertical、padding到padding-left/padding-right/padding-top/padding-bottom的"伪属性"默认值注入(init_fake_property)。
运行时
全部定义在 internal/core/layout.rs:
GridLayoutData(第 470-475 行):可用尺寸size、spacing、padding,以及organize_grid_layout()产生的GridLayoutOrganizedData,作为solve_grid_layout()的输入。BoxLayoutData(第 1134-1140 行):可用尺寸、间距、内边距、LayoutAlignment,以及每个单元格一份的LayoutItemInfo借用切片(Slice<LayoutItemInfo>)。LayoutItemInfo(第 1178 行起):单个单元格的constraint: LayoutInfo、逐项 cross-axis 对齐覆盖cross_axis_self_alignment,以及视觉排序用的layout_order字段。
布局缓存格式
布局缓存是一个扁平的SharedVector<Coord>(即SharedVector<f32>),为布局的全部子元素存储已求解的位置与尺寸。每个子元素占用 2 个槽位:[pos, size](水平方向即[x, width],垂直方向即[y, height])。水平与垂直轴使用独立的缓存(layout-cache-h与layout-cache-v)。编译器侧的常量BOX_LAYOUT_CACHE_ENTRIES_PER_CELL = 2(internal/compiler/layout.rs 第 17 行)直接对应这个"每单元格 2 槽"的约定。
静态布局(无 repeater)
当所有子元素在编译期已知时,缓存就是一个简单扁平数组:
cache = [pos0, size0, pos1, size1, ..., posN, sizeN]访问方式:cache[index],其中index = child_idx * 2为位置槽,child_idx * 2 + 1为尺寸槽。此时索引是编译期常量,代码生成器直接内联数组下标。
标准缓存(盒布局 + repeater)
HorizontalLayout/VerticalLayout/FlexboxLayout使用标准缓存(经由LayoutCacheGenerator生成)。静态子元素占据固定槽位;每个 repeater 实例恰好贡献一个单元格(一个 pos + 一个 size)。当存在 repeater 时,其实例被连续存放在缓存末尾的块中,静态区域内的一个jump cell(跳转单元格)指向该块的起始偏移。
repeater_indices:形如(start_cell_index, instance_count)的配对序列——每个 repeater 一对。
示例:1 个固定单元格,随后是含 3 个实例的 repeater
repeater_indices = [1, 3] // repeater 从单元格 1 开始,共 3 个实例 cache = [ 0., 50., // 固定单元格: pos=0, size=50 4., 5., // jump cell: 指向偏移 4(第一个动态槽) 80., 50., // 重复实例 0 160., 50., // 重复实例 1 240., 50., // 重复实例 2 ]访问公式:cache[cache[jump_index] + repeater_index * entries_per_item]
jump_index:jump cell 的缓存下标(编译期已知)repeater_index:第几个实例(0..count),运行时值entries_per_item:坐标缓存中为 2(pos + size),编译期已知
运行时solve_box_layout()(internal/core/layout.rs 第 1229 行起)正是按此格式产出结果向量:结果长度为cells.len() * 2 + repeater_indices.len()(第 1232 行),多出来的部分即 jump cell。
两级间接缓存(网格布局 + repeater)
GridLayout(经由GridLayoutCacheGenerator)对任何repeater 都使用两级间接缓存,无论 repeater 是单子元素还是多子元素。与标准缓存一样使用 jump cell 间接寻址,但关键区别在于:步长(stride)是可变且动态的。
- 盒布局的步长恒定为
entries_per_item(坐标缓存即 2); - 网格布局 + repeater 的步长为
step * entries_per_item,其中step是每个实例的子元素数量。步长可以是:- 编译期常量:当 repeater 的所有子元素都是静态的;
- 运行时值:当 repeater 实例内嵌套了 repeater 时,从 jump cell 自身读取。
这使得网格能同时处理单子元素 repeater(step=1)与多子元素 repeater(step=N),且支持内部再嵌套 repeater。
repeater_steps:每个 repeater 一项的向量——记录每个实例贡献多少个子元素。
示例:1 个 repeater,3 个行实例,每行 2 个子元素(step=2)
slint! { GridLayout { for _ in 3: Row { Rectangle {} Rectangle {} } } };repeater_indices = [0, 3] // 从单元格 0 开始,3 个实例 repeater_steps = [2] // 每个实例 2 个子元素 cache = [ 2., 4., // [0-1] jump cell: data_base=2, stride=4 (step*2) 0., 50., 0., 50., // [2-5] 行 0 数据: child0=(pos=0,size=50), child1=(pos=0,size=50) 50., 50., 50., 50., // [6-9] 行 1 数据 100., 50., 100., 50., // [10-13] 行 2 数据 ]注意 jump cell 本身占两个槽位:data_base(数据块起始下标)与stride。当各行的子元素数量不一致(锯齿状,jagged)时,步长取所有行中子元素数量的最大值,较短的行按该步长补齐(pad)。
访问公式:cache[cache[jump_index] + ri * stride + child_offset]
jump_index:编译期已知(jump cell 的下标,恒为jump_cell_pos * 2)ri:repeater 实例下标(0..count),运行时值,来自$repeater_indexstride:step * 2——静态 repeater 子元素时为编译期字面量;行内含嵌套 repeater 时从cache[jump_index + 1]读取child_offset:行内第几个子元素(0, 2, 4, ...),每个子元素编译期已知
运行时侧的organize_grid_layout()与solve_grid_layout()都接收repeater_steps切片,GridLayoutOrganizedData::col_or_row_and_span借助它把扁平单元格索引正确映射到实例/子元素坐标。
子元素如何从缓存读取
在编译期降级(lower_layout.rs)期间,每个子元素都会得到类似如下的绑定:
// 网格中的静态子元素: x: layout_cache_h[4] // 直接下标,编译期已知 width: layout_cache_h[5] // 盒布局中的重复子元素 —— 标准缓存 (LayoutCacheAccess): x: layout_cache_h[cache[2] + $repeater_index * 2] width: layout_cache_h[cache[2] + $repeater_index * 2 + 1] // 网格布局中的重复元素(即使是单子元素)—— 两级间接缓存 (GridRepeaterCacheAccess): // 单子元素: step=1, stride=2 (step * entries_per_item) // 多子元素: step=N, stride=N*2 x: layout_cache_h[cache[jump_cell] + $repeater_index * stride + child_offset] width: layout_cache_h[cache[jump_cell] + $repeater_index * stride + child_offset + 1]这些访问在表达式树中分别表示为Expression::LayoutCacheAccess(标准缓存,用于盒布局与网格静态项)或Expression::GridRepeaterCacheAccess(网格 repeater,任意 repeater 结构),代码生成器(internal/compiler/generator/rust.rs、internal/compiler/generator/cpp.rs)会将其编译为上述对应的运行时访问模式。
测量重复单元格(Measuring Repeated Cells)
在未传入 cross 尺寸的 pass 中——例如GridLayout求解、VerticalLayout主轴 pass——静态 height-for-width 单元格(例如自动换行的Text)会自我测量:它的width绑定指向布局缓存,因此在计算 layout-info 时(text_layout_info会把小于 0 的 cross 约束当作"使用当前宽度")Text能读到布局分配给它的宽度。其他 pass 则给静态单元格传入显式约束。
重复单元格无法以这种方式读取自身宽度:布局请求的是整个实例的 layout-info,实例经由layoutinfo-v-with-constraint(见synthesize_layoutinfo_v_with_constraint,internal/compiler/passes/lower_layout.rs 第 394 行起;该 pass 自底向上遍历,为每个 height-for-width 依赖子树合成参数化布局信息函数,从而打破父查询子项垂直信息时的递归环路)而非读取self.width,因此在固定的 cross 尺寸下测量——垂直信息使用实例的首选宽度,水平信息使用无界高度。布局因此必须通过RepeatedItemTree上的访问器把真实尺寸传入:
| 访问器 | 底层实现 | 提供方 |
|---|---|---|
layout_item_info_at_cross_width(w) | SubComponent::layout_info_v_at_cross_width_for_repeated | 任何在已知宽度下进行的垂直 pass:VerticalLayout主轴 pass、HorizontalLayout正交 pass、GridLayout垂直 pass |
layout_item_info_at_cross_height(h) | SubComponent::layout_info_h_at_cross_height_for_repeated | 任何在已知高度下进行的水平 pass:HorizontalLayout主轴 pass、VerticalLayout正交 pass |
flexbox_layout_item_info_at_cross_width(w)/_height(h) | 上述两个表达式 | FlexboxLayout求解 |
选择哪个访问器取决于正在计算的朝向,而非盒布局自身的方向:VerticalLayout在主轴 pass 调用layout_item_info_at_cross_width、在正交 pass 调用layout_item_info_at_cross_height,HorizontalLayout则相反。GridLayout只出现在第一行(cross-width 访问器)——因为它先求解水平方向,若在已解出的高度上测量单元格,会让水平求解去读垂直缓存,而本节末尾将说明这是绝对禁止的。
SubComponent字段位于 internal/compiler/llr/item_tree.rs;生成器在 internal/compiler/generator/rust.rs 与 internal/compiler/generator/cpp.rs 中发射这些访问器,解释器则在 internal/interpreter/eval_layout.rs 与 internal/interpreter/instance.rs 中镜像实现。
尺寸来源因布局类型而异:
- 盒布局,主轴 pass:为所有单元格转发同一个尺寸(布局自身的 cross 内容尺寸),位于
Expression::WithLayoutItemInfo::repeated_cross_size。 - 盒布局,正交 pass:没有可转发的单一尺寸——
Expression::BoxLayoutInfoOrthoWithMeasure先求解主轴,再以每个实例各自已解出的主轴尺寸测量之,对应BoxMeasureCell::Repeated,此时repeated_cross_size为None。 GridLayout:每列宽度不同,因此从layout-cache-h中读取每个单元格自己的槽位:LayoutRepeatedElement::cross_width(internal/compiler/layout.rs)是单元格自身的width绑定,其中 repeater 下标被替换为GRID_MEASURE_REPEATER_INDEX_LOCAL局部变量,生成的循环将其绑定到实例下标。重复Row中的重复子元素改用SubComponent::grid_row_child_cross_width,按子元素的扁平下标(GRID_MEASURE_CHILD_INDEX_LOCAL)寻址——一个表达式服务于所有此类子元素,RowChildTemplateInfo::Repeated::measure_at_cross_width记录其适用范围;Row的静态子元素则保留普通的无约束 layout-info。FlexboxLayout:尺寸传递两次——先一次性传入容器 cross 宽度(Expression::WithFlexboxLayoutItemInfo::repeated_cross_width,仅 column flex),再在 taffy 的 measure 回调中按单元格传入(SolveFlexboxLayoutWithMeasure、FlexboxLayoutInfoCrossAxisWithMeasure),以 taffy 实际分配的尺寸重新测量。
从垂直 pass 读取水平缓存,仅在水平求解不回读垂直缓存时才是安全的。重复的 width-for-height 单元格会破坏这一点:网格通过其普通layout_info_h测量它,而该值又拉取实例自身的高度,这个高度来自网格的垂直缓存——形成绑定环路。mark_grid_h_solve_reads_v_cache会将此类网格的每个单元格标记为GridLayoutCell::h_solve_reads_v_cache(internal/compiler/layout.rs 第 459 行起的GridLayoutCell结构),垂直 pass 随后退回到实例的普通 layout-info,从而打破环路。静态单元格是安全的:网格求解时cell_layout_info不传 cross 尺寸,因此带layoutinfo-h-with-constraint的单元格在无界高度下测量,不带该约束的单元格则永远不会读取自身高度。
常见修改模式
新增一个布局属性
- 在 internal/compiler/builtin_elements.rs 中把属性加入内置布局元素;
- 在 internal/compiler/layout.rs 的
LayoutGeometry或LayoutConstraints中处理该属性(LayoutGeometry::new()展示了从元素绑定创建引用、并经由init_fake_property注入padding-*/spacing-*伪属性的标准流程); - 更新 internal/compiler/passes/lower_layout.rs 以提取并使用该属性;
- 如需运行时数据结构配合,更新 internal/core/layout.rs 中的对应结构体;
- 在 tests/cases/layout/ 下新增测试用例(该目录现有 157 个
.slint测试文件,覆盖盒布局对齐、交叉轴对齐、百分比、repeater、flexbox 等场景,可直接参考box_alignment.slint、box_cross_alignment.slint、grid_conditional_row.slint等既有用例的组织方式)。
调试布局问题
- 检查约束传播:在 internal/core/layout.rs 的
LayoutInfo::merge()中加eprintln!,观察每个 min/max/preferred 的合并结果; - 检查求解过程:在
layout_items()中加日志,观察 shrink/grow 各轮迭代的分配; - 验证缓存访问:检查生成代码中
LayoutCacheAccess/GridRepeaterCacheAccess的下标是否与"每单元格 2 槽""jump cell 偏移"一致; - 使用 inspector:以 Slint inspector 运行应用查看元素边界框,从视觉上定位越界或塌缩的布局。
新增对齐模式
- 在 internal/core/layout.rs 的
LayoutAlignment枚举中加入新变体; - 在
solve_box_layout()的对齐 switch 分支中处理之(目前SpaceBetween/SpaceAround/SpaceEvenly的间隙计算都在该分支完成); - 若需要新语法,在编译器中增加解析支持;
- 为新对齐方式添加测试用例(如
tests/cases/layout/box_alignment.slint)。
面向 Agent 的关键概念
- 两阶段架构:编译期创建结构,运行时求值——任何布局改动都要考虑两侧的影响面(编译器降级 + 运行时求解)。
- 独立轴求解:水平与垂直分别求解(针对水平、垂直与网格布局),唯一的例外是垂直 pass 会在已解出的宽度上测量 height-for-width 单元格。
- 约束收紧:合并取最严格边界(min 取大、max 取小),保证子布局永远不超出父布局给出的硬约束。
- 伸缩因子:控制多余空间的分配方式(0 = 不增长);未设置任何伸缩因子时退化为逐项平均分配。
- 缓存间接寻址:jump cell + 可变步长使 repeater 无需改变运行时结构即可增减实例;两级间接缓存专门解决网格中多子元素 repeater 与嵌套 repeater 的寻址。
- 默认几何:元素默认占父元素 100%(除非内容决定尺寸),由
default_geometry.rs在布局降级之后设置。
测试布局变更
布局测试横跨两个独立 Cargo workspace:test-driver-rust与test-driver-interpreter位于独立的tests/workspace,gallery位于独立的examples/workspace,因此在仓库根目录运行时需要显式指定--manifest-path(tests/run_tests.sh 已为你处理了这些路径):
# 运行全部布局专项测试 cargo test --manifest-path tests/Cargo.toml -p test-driver-rust --test layout cargo test --manifest-path tests/Cargo.toml -p test-driver-interpreter layout # 按子串过滤运行特定用例(不要加 sh/bash 前缀,run_tests.sh 本身可执行) tests/run_tests.sh rust grid_conditional_row tests/run_tests.sh interpreter grid_conditional_row tests/run_tests.sh cpp grid_conditional_row # 运行全部 interpreter 测试(较快) cargo test --manifest-path tests/Cargo.toml -p test-driver-interpreter # 可视化验证(供人工查看) cargo run --manifest-path examples/Cargo.toml -p gallerytests/run_tests.sh封装了三种 driver(rust / interpreter / cpp)的统一入口,其内部会为不同 driver 自动拼接正确的 manifest 路径与 filter 参数;gallery示例聚合了 Slint 全部内置控件与多种布局形态,是肉眼验证布局改动的首选工具。
【免费下载链接】slintSlint is an open-source declarative GUI toolkit to build native user interfaces for Rust, C++, JavaScript, or Python apps.项目地址: https://gitcode.com/GitHub_Trending/sl/slint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考