定位、图层与浮层
绝对定位把盒子移出流,让它在兄弟之上绘制:下拉菜单、提示气泡、菜单与模态遮罩都是某个宿主盒子的浮层。本章描述样式属性参考中 :position、:layer、:anchor、:placement、:left、:top 与 :z-index 背后的模型:定位宿主如何选出、浮层落在哪里、锚点如何解析、重叠盒子如何绘制,以及浮层如何拼进宿主的行。流容器本身见盒与常规流、Flex 布局与Grid 布局,坐标系见渲染模型。
定位与定位宿主
Section titled “定位与定位宿主”:position absolute 的盒子离开父级常规流。它不向父级的内容尺寸贡献任何东西,不占自己的行,也不推动兄弟,所以父级的高度仍由流内容与显式高度决定。绝对定位的盒子称为浮层,为它定出位置的盒子是它的定位宿主。
宿主是最近的非行内祖先——即最近一个 :outer 不是 inline 的祖先,通常就是最近的 box——:layer root 时则是根本身。因此行内父节点把它下面的绝对子节点交给最近的外围块盒托管。:outer 为 inline 的盒子自己永远不作为宿主,文本节点也不作为宿主。
没有锚点时,定位原点是宿主的内容盒原点:水平方向是宿主的左边框加左内边距再加 :left,垂直方向是宿主的顶部内边距加 :top。:left 的百分比以宿主的 padding 盒宽(边框盒减去左右边框)为基准,:top 的百分比以宿主的边框盒高为基准;无锚点的根层盒子两者都按根的边框盒度量,因为根就是它的宿主。
浮层相对宿主的边框盒布局:可用宽是宿主的边框盒宽,并且按内容收缩,所以声明的 :width (% 50) 解析为宿主宽的一半。宿主不为浮层预留空间,想让浮层占据某个槽位,就要给宿主显式高度。绘制被裁剪到宿主:只有宿主的边框盒各行存在,浮层越过宿主底边就被切掉;负偏移超出宿主的部分不会绘制——偏移值本身保留,几何查询仍报告请求的位置。浮层属于宿主的画布而不是它的内容:宿主滚动时,浮层不随流移动,保持相对宿主的位置。
示例。 一个只有一行水平内边距的宿主,把 :left (px 3)、:top (lh 1) 的浮层放在 x 4、y 1;同宿主的浮层即使高 5 行,宿主高度仍是 1 行。高 3 行的宿主配 :top (lh 2)、高 5 行的浮层,只显示浮层的第 1 行。
root 图层与页面级遮罩
Section titled “root 图层与页面级遮罩”:layer root 时定位宿主是根,浮层因此在根的顶层绘制,而不是画在它的逻辑父级里。它的逻辑位置不变:选择器、继承与树查询仍按原来的父级,只有定位与绘制移到整页。
没有锚点时,根层盒子从根的边框盒起度量偏移与百分比——不扣除内边距或边框——所以 :left (px 0) 与 :top (lh 0) 就是页面原点,:width (% 100) 配 :height (% 100) 覆盖整页。这就是常见的页面级遮罩;由于盒子仍保留逻辑父级,遮罩仍能被它所在树位置的规则、类与继承作用到。
根层盒子与根自己的浮层共享同一个绘制列表,按 :z-index 排序;又因为根在每个子树之后才合成自己的行,根层盒子会画在所有嵌套宿主的局部浮层之上。根层盒子写在哪棵子树里,不影响它的绘制位置。
示例。 一个写在小嵌套盒子里、宽高都是 (% 100) 的遮罩,在 12×4 的根上量得 12×4,盖住兄弟子树里的 o;:z-index 1 的根层盒子低于根上 :z-index 5 的浮层,高于某个嵌套宿主里 :z-index 9 的浮层。
:anchor 让浮层跟随一个节点,而不是相对宿主原点。它的值用 :id 命名目标:锚点的值与 id 都归一为名字,目标是在浮层所在根的整棵树里、按文档顺序第一个匹配该名字的节点——是整棵树,不只是宿主的子树。目标缺失、被隐藏,或位于浮层自身之内时,盒子完全不放置:不绘制、不拥有任何 buffer 格子、不能获得焦点,尽管它仍有一个布局盒。
放置把浮层相对目标的边框盒摆好。默认的 bottom-start 让浮层上边贴目标下边、左缘对齐;top-start 让浮层下边贴目标上边;-end 变体改为右缘对齐。:left 与 :top 在此之上作为附加偏移。首选的垂直一侧会越出宿主时,若另一侧放得下就翻到另一侧;无论哪种情况,最终位置都被夹在宿主之内,所以锚定浮层不会画到宿主画布之外,宿主变窄时它也保持可见。
放置会重复到稳定,所以目标位于另一个浮层内部时,等那个浮层移动后仍能得到最终位置。这一遍在每次布局与 flush 之后运行,因此目标移动后浮层会跟上,尽管目标自身的移动并不会单独触发浮层重排。解析结果按浮层缓存,缓存的目标不再匹配或不再属于同一棵树时才重新解析,所以未变化的目标不会让重复放置反复遍历整棵树。
示例。 8 行高的宿主里,位于 y 6 的目标配一个 2 行高的 bottom-start 提示,首选一侧会到 y 8,于是翻到目标上方、落在 y 4。位于 y 5 的目标配一个 6 行高、:left (px 20) 的提示,两侧都放不下,两个坐标都被夹取,提示落在 x 16、y 2。锚定在另一个提示内部节点的提示,稳定在该节点下方一行。
z-index 与遮挡
Section titled “z-index 与遮挡”同一个宿主的浮层按 :z-index 升序绘制;取值相同则保持文档顺序,后出现的浮层画在上面。初值是 0,允许负值,所以浮层也可以压到兄弟之下。
相对定位的流子节点不出流,但参与同一顺序。它保留自己的槽位与布局几何;当它的 :z-index 高于最低浮层的取值时,它画在更低的浮层之后、更高的浮层之前,并通过自己没填满的格子透出下方浮层的背景。比较针对的是最低浮层的取值,所以同一个 z 值对流子节点与浮层意味着同一个堆叠位置,初值 0 的相对子节点会被取值 0 或更高的浮层盖住。
z-index 只在单个宿主的列表内比较。不同宿主的浮层由合成与嵌套决定顺序,与 z 无关:嵌套宿主的浮层无论取什么值,都画在祖先宿主浮层之前,因为祖先把自己的浮层拼进已经包含该子树内容的行里。跨宿主比较 z 值因此没有意义,调高嵌套浮层的 z 也无法把它抬到外层宿主的浮层之上。
命中测试与绘制顺序相反:z 最高者先命中,取值相同则后出现的浮层先。被浮层覆盖的格子属于浮层的内容,所以该处的点击或命中会解析到浮层内,永远不会到达被盖住的流节点。首个绘制行被完全盖住的节点不拥有 buffer 格子——ebox-node-position 返回 nil、ebox-node-at 返回别的节点——也不能获得焦点;只是部分被盖的节点保留未被盖住的格子。
示例。 两个重叠的 4 像素浮层,z 分别 0 和 1 时结果为 BBBB,交换取值为 AAAA;两者都为 0 时仍是后出现的获胜。z 5 的流子节点对上 z 0 的浮层会画出 RRRR,但 z 5 的相对子节点遇到 z 10 的浮层就盖不住它。
插图: 浮层拼进宿主的行,布局不变
(etaf-view (column :width '(ch 40) :item-gap '(lh 0) (box :padding '((lh 1) (ch 2)) :background-color (etaf-theme-token :ui-panel-bg "#f6f8fa") (text :color (etaf-theme-token :ui-panel-fg "#1f2328") "宿主行")) (box :padding '((lh 1) (ch 2)) :background-color (etaf-theme-token :ui-accent-soft-bg "#ddf4ff") (text :color (etaf-theme-token :ui-accent "#0969da") "浮层拼进这一行,布局不动")) (box :padding '((lh 1) (ch 2)) :background-color (etaf-theme-token :ui-disabled-bg "#e5e7eb") (text :color (etaf-theme-token :ui-disabled-fg "#687386") "宿主的其余行不受影响"))))浮层如何拼进下面的行
Section titled “浮层如何拼进下面的行”合成把每个浮层拼进宿主的行。浮层的外边距盒从它被放置的位置开始;宿主的每条边框盒行,都用浮层在对应偏移处的行替换其下的像素,结果化作一个扁平的片段列表,在整行写入 buffer 时一次性拼接。只有浮层覆盖的行受影响——宿主其余各行仍保持流内容。
边框装饰在浮层拼接之后应用,所以上边框或下边框的 overline/underline 会继续跨越浮层,浮层不会把边框断掉。浮层改变的是像素、命中与可见性,从不改变布局:宿主尺寸不变、兄弟不移动、浮层下方的流不受影响。
浮层的代价与它覆盖的行数成正比。宿主合成并缓存自己的行,浮层的行参与这份缓存;浮层移动时,只重绘它离开的行与它到达的行,所以移动一行的提示气泡不会重写整个宿主。这套 damage 记账属于 增量更新与发布 一章。
ebox-node-geometry 以根坐标报告浮层的边框盒——宿主 margin 原点加浮层相对宿主的位置,再加它自己的 margin——所以浮层的位置可以直接与任何其它节点比较,跨嵌套宿主也一样。
:position、:layer、:anchor 或 :placement 的非法取值、非整数的 :z-index、不是尺寸的 :left/:top 都会在声明编译时被拒绝,早于任何布局,对应的条件由样式属性参考描述。
锚点目标缺失不是错误:浮层只是不被放置。常见原因是 id 拼错、目标被隐藏、目标位于浮层自身之内,以及存在重复 id 而只有文档顺序里第一个节点会被跟随。未放置的浮层悄无声息,下拉不出现时值得先检查目标 id 与目标是否被绘制。
已放置却看不见的浮层,通常是被裁或被盖。没有显式高度的宿主在流之后没有行,放在流之后的浮层会被切掉;恢复办法是给宿主设计所需的高度。z 更低会让浮层压在兄弟之下;嵌套宿主的浮层无论 z 多高,都会压在祖先宿主的浮层之下。被盖住的节点报告不出 buffer 位置、也不能获得焦点,这是有意的遮挡行为,不是缺陷。