Skip to content

DRAFT ETML: Flex 布局算法及 Elisp 实现

约 3980 字大约 13 分钟

elispetml

2025-08-31

这篇文章介绍了 CSS Flex 弹性盒子布局的算法,以及 ETML 中的 emacs-lisp 的实现。理论上,读者可以根据文章中的算法使用任何语言来实现文本块的 flex 布局,并不局限于 emacs-lisp。

CSS Flex 简介

Flex 布局算法

  1. 根据 self 渲染为原始字符串
  2. 根据 order 属性,重新排列子项的顺序

根据 basis 计算每个子项在主轴方向的初始长度 获取 flex 容器宽度 计算所有子项的初始长度总和 如果

  1. 根据 grow, shrink,计算子项经过拉伸后压缩后的宽度
  2. ....

Flex 布局模型

etml-flex 模型继承自 etml-block,因此除了自身定义的属性,也可以使用 etml-block 的所有属性。

布局模型 elisp 代码
etml-flex.el

Flex 子项模型

etml-flex.el
(defclass etml-flex-item (etml-block)
  ((self :initarg :self :type etml-block
         :documentation "item 本身。")
   (order :initarg :order :initform 0 :type integer
          :documentation "item 在 flex 容器中顺序。")
   (basis :initarg :basis :initform 'auto :type symbol
          :documentation "item 在​​分配空间前​​的​​初始尺寸。")
   (grow :initarg :grow :initform 0 :type integer
         :documentation "item 在​​容器有剩余空间时​​的​​增长系数​​。")
   (shrink :initarg :shrink :initform 1 :type integer
           :documentation "item 在​​容器空间不足时​​的​​缩小系数。")
   (align :initarg :align :initform 'auto :type symbol
          :documentation "item 在交叉轴的对齐方式,覆盖容器的 items-align 属性。")))

self

定义 item 本身,它是一个 etml-block 对象。

order

定义 item 在 flex 容器中顺序。

具体实现

容器中的 items 会按照 order 值的大小进行排列展示,对于相同的 order 值,按照其在容器中的原始顺序进行排列。

basis

定义 item 在主轴方向上的初始长度。支持设置为如下的值:

  • 200: 设置一个具体的像素值
  • auto(默认): 根据项目内容或显式设置的 width/height 决定初始尺寸;如果设置了 width(水平主轴) 或 height(垂直主轴),则使用该值;未设置尺寸时,自动根据​​内容大小​​计算初始尺寸。
  • max-content: 内容不换行时的最大宽度​,宽度由内容中最长的单词/元素决定。不受容器宽度限制。
  • min-content: 设置为内容能换行时的最小宽度,宽度由内容中最长的不可断元素(如图片、长单词)决定。不受容器宽度限制。
  • fit-content: 在最大宽度,最小宽度和容器宽度之间计算一个最佳值, 计算公式: fit-content = min(max-content, max(min-content, container-space))
  • content: 内容自身的尺寸,不考虑容器的限制。(在 flex 布局中,contentmax-content 效果一致,只不过 max-content 是 CSS 通用的值,content 专属于 flex-basis)。

grow

定义 item 在主轴上的增长系数。当容器宽度大于 items 默认总宽度时,剩余的空间会按照 grow 的系数来分配。

具体实现

  1. 所有 item 的 grow = 0,都保持默认宽度,不分配剩余空间

  2. 至少有一个 item 的 grow > 0,用如下公式计算每个 item 如何分配剩余空间:

    当前 item 的增长宽度 = (剩余宽度 / 所有 items 的 grow 系数之和) * 当前 item 的 grow 系数

shrink

定义 item 在主轴上的缩减系数。当 items 默认总宽度大于容器宽度时,items 多余的空间会按照 shrink 的系数来分配如何缩减。

具体实现

  1. 所有 item 的 shrink = 0,都保持默认宽度,不缩减。

  2. 至少有一个 item 的 shrink > 0,用如下公式计算每个 item 如何缩减多余的空间:

    当前 item 的缩减宽度 = (剩余宽度 / 所有 items 的 shrink 系数之和) * 当前 item 的 shrink 系数

item 模型中未设置 flex 属性的考虑

CSS 中的 flex 属性没有作为 etml-flex-item 模型的字段,因为它是 basis, grow, shrink 这三个的复合属性,应该在此模型的上层定义。

align

表示单个 item 在交叉轴的对齐方式,其值会覆盖容器的 items-align(表示容器中的所有 items 在交叉轴的对齐方式)。支持设置以下的值:

  • auto(默认值): 继承容器的 items-align 值。如果容器未设置 items-align,回退到 stretch
  • stretch: 项目拉伸以填满交叉轴空间。
  • normal: 同 stretch。
  • flex-start: 项目对齐交叉轴起点​​。
  • flex-end: 项目对齐交叉轴终点。
  • baseline: 项目的第一行文本基线与容器中所有其他项目的基线对齐。若项目无文本,则以其底部边缘为基线。

Flex 容器模型

我们先来了解 Flex 布局用到的每个字段的对布局影响,然后考虑如何将这些字段的组合渲染为布局之后的字符串。

etml-flex.el
(defclass etml-flex (etml-block)
  ((display :initarg :display :initform 'flex :type symbol
            :documentation "容器是块级元素还是内联元素。")
   (direction :initarg :direction :initform 'row :type symbol
              :documentation "主轴方向。")
   (wrap :initarg :wrap :initform 'nowrap :type symbol
         :documentation "换行方式。")
   (content-justify
    :initarg :content-justify :initform 'flex-start :type symbol
    :documentation "主轴对齐方式。")
   (content-align
    :initarg :content-align :initform 'stretch :type symbol
    :documentation "多个主轴在垂直方向的对齐方式。")
   (row-gap :initarg :row-gap :initform 0
            :type number :documentation "多行之间的 gap。")
   (column-gap :initarg :column-gap :initform 0
               :type number :documentation "多列之间的 gap。") 
   (items :initarg :items :type (vector etml-flex-item)
          :documentation "子项目列表")
   (items-align
    :initarg :items-align :initform 'stretch :type symbol
    :documentation "所有 items 在交叉轴的对齐方式。"))
  "ETML flex layout model.")

display

表示容器表现为块级别元素还是内联元素。支持设置为下面两种值:

  • flex:渲染后的容器是一个块级别元素
  • inline-flex:渲染后的容器是一个内联元素

具体实现

这个值决定了 flex 容器渲染成字符串后是否要在最后加一个换行符。

  • flex:最终渲染成的字符串最后加一行像素空格行
  • inline-flex:直接返回最终渲染成的字符串

direction

表示主轴的方向,即容器内的项目的排列方向。支持设置为下面四种值:

  • row(默认值):主轴为水平方向,起点在左端。
  • row-reverse:主轴为水平方向,起点在右端。
  • column:主轴为垂直方向,起点在上沿。
  • column-reverse:主轴为垂直方向,起点在下沿。

具体实现

这个值决定了在所有 items block 的拼接方式。

  • row:使用 etml-block-concat 水平方向拼接所有 items
  • row-reverse:items 倒序之后,再使用 etml-block-concat 拼接
  • column:使用 etml-block-stack 垂直方向拼接所有 items
  • column-reverse:items 倒序之后,再使用 etml-block-stack 拼接

wrap

表示如果一条轴线排不下,如何换行。支持设置为下面三种值:

  • nowrap:不换行,所有 items 排列在一行
  • wrap:换行,换行到上一行
  • wrap-reverse:换行,换行到下一行

在实现上,需要综合考虑该值与后续要介绍的 items-flex(拉伸因子,收缩因子和基础宽度) 来计算每个 item 的最终宽度。如果计算后的所有 items 最终总宽度小于等于容器宽度,wrap 值没有影响。

如果计算后的所有 items 最终总宽度大于容器宽度,wrap 值会有如下影响:

不同 wrap 值在溢出时如何处理

  1. nowrap:溢出容器宽度但仍不换行,需要根据继承自 etml-blockoverflow 属性处理。
  2. wrapwrap-reverse:溢出容器部分的 items 按照属性要求进行换行;换行之后,原本行的 items 需要按照 items-flex 重新计算每个 item 的宽度(比如需要拉伸)。

容器模型未设置 flow 字段的考虑

flowdirection、wrap 的复合属性,应该由上层的模型定义。

content-justify

定义了 items 在主轴上的对齐方式。支持设置为如下的值:

提醒

下面提到的 container-width 指的是 flex 容器容纳内容的宽度,不包含 padding, border 和 margin 部分。items-width 指的是 flex 容器中一行的 items 的总宽度,每个 item 的宽度是包含内容、padding、border 和 margin 的总宽度。items-num 指的是一行 items 的数量。

  • flex-start 起点对齐
    items 从行(列)首开始排列。第一个 item 的主轴起始位置紧贴行(列)首边缘,后续 item 与前一个 item 紧贴排列。

    具体实现

    items 结尾用 (- container-width items-width) 像素空格填充。

  • flex-end 终点对齐
    items 向行(列)尾方向排列。最后一个 item 的主轴结尾位置距紧贴行(列)尾边缘,其他 item 与后一个 item 紧贴排列。

    具体实现

    items 开头用 (- container-width items-width) 像素空格填充。

  • center 居中对齐
    items 沿行中轴线排列。所有 items 彼此紧贴且整体居中,行(列)首边缘与第一个项目之间、行(列)尾边缘与最后一个项目之间留有等距空间(若剩余空间为负值,则项目会向两个方向等距溢出)。

    具体实现

    items 两侧用 (/ (- container-width items-width) 2) 像素空格填充。

  • space-between 两端对齐
    items 均匀分布在行内。若剩余空间为负或仅存在单个 item 时,该值效果等同于 flex-start。其他情况下,首项的主轴主轴起始位置紧贴行(列)首边缘,末项的主轴结尾位置紧贴行(列)尾边缘,其余相邻项目间距保持相等。

    具体实现

    在 items 之间用 (/ (- container-width items-width) (1- items-num)) 像素空格填充

  • space-around 环绕对齐
    items 均匀分布,并在两端保留 item 间距的一半尺寸空间。若剩余空间为负或仅存在单个项目时,该值效果等同于 center。其他情况下,相邻项目间距相等,首项与容器边缘/末项与容器边缘的间距为相邻项目间距的一半。

    具体实现

    开头和结尾用 (/ (container-width - items-width) (* 2 items-num)) 像素空格填充,items 之间用 (/ (container-width - items-width) items-num) 像素空格填充。

  • space-evenly 均匀对齐
    flex 项都沿着主轴均匀分布在指定的对齐容器中。相邻 flex 项之间的间距,主轴起始位置到第一个 flex 项的间距,主轴结束位置到最后一个 flex 项的间距,都完全一样。

    具体实现

    开头和结尾 以及 items 之间都用 (/ (container-width - items-width) (1+ items-num)) 空格像素填充。

content-align

多根轴线在交叉方向的对齐方式。

  • flex-start
  • flex-end
  • center
  • space-between
  • space-around
  • stretch

row-gap

多行之间的 gap 距离。

column-gap

多列之间的 gap 距离

items

子项列表

items-align

控制 items 在交叉轴上的位置(交叉轴指与主轴垂直的那个轴)

参考