///|
/// **moobile 的类型化样式层。**
///
/// 设计要点(对应 README「架构级改造的空间」):
///
/// 1. **属性集是封闭的** —— 只有下面这些方法存在。`display:grid`、
/// `position:sticky`、`::before` 这些**根本没有构造器**,
/// 所以"Web 能跑、移动端不能"在**编译期就不可能发生**。
/// (DESIGN 原则 3:可移植子集优先于表达力。)
///
/// 2. **每个属性的值类型由签名钉死** —— `font_weight` 只收 `FontWeight`,
/// 所以不可能出现"把数字塞给 fontWeight"这种静默非法。
///
/// 3. **一份数据、两个后端** —— `StyleValue` 同时携带"是不是长度",
/// 于是 RN 侧产出数字、CSS 侧产出 `"16px"`,不需要两边各存一套。
///
/// 词表取自 **React Native 的 style props**(它本身就已经是一份被策展过的
/// 可移植子集:flexbox + 有限属性集,且刻意排除了 grid / sticky / 伪元素)。
pub struct Style {
entries : Array[(String, StyleValue)]
}
///|
/// 值的形态,已足够覆盖 RN 与 CSS 的 style 取值:
///
/// | 构造器 | RN 产出 | CSS 产出 |
/// |---|---|---|
/// | `Unitless(d)` | 数字 | `"1"` |
/// | `Px(d)` | 数字 | `"16px"` |
/// | `Pct(d)` | `"50%"` | `"50%"` |
/// | `Em(d)` | 数字(按同一样式里的 `fontSize` 换算) | `"0.1em"` |
/// | `Auto` | `"auto"` | `"auto"` |
/// | `Str(s)` | 字符串 | 字符串 |
///
/// ★ `Em` 是实测补上的:yi 大量使用 `letter-spacing: .1em` 这类相对单位,
/// 而 **RN 的 `letterSpacing` 只认点数**。为它单独设构造器,
/// 就把"每次调用点手工乘一遍"这个易错动作消灭了。
pub(all) enum StyleValue {
Unitless(Double)
Px(Double)
Pct(Double)
Em(Double)
Auto
Str(String)
} derive(Eq)
///|
pub fn Style::new() -> Style {
{ entries: [] }
}
///|
/// 取出条目(后端按顺序消费)。
pub fn Style::entries(self : Style) -> Array[(String, StyleValue)] {
self.entries
}
///|
/// **按下状态**的样式 —— 给触屏与 web 同一套表达(源 CSS 的 `:hover` 降级到这里)。
///
/// 用**键前缀**(`press:`)而不是给 `StyleValue` 加变体:加变体会让每一个构造器都得处理
/// "带不带状态",污染整个类型化属性集;键前缀只影响一次遍历,形状仍是扁平的 (String, Value)。
///
/// ⚠️ **语义**:它把**当前已有的键**都加上前缀 —— 所以**`.press()` 落在链的哪一格,
/// 就决定它前面那一段是按下态**。要让"源 CSS 的 `:hover` 是按下态、常态仍是常态",
/// 链的形状只能是 **hover 的键 → `.press()` → 常态的键**(它夹在中间):
///
/// ```text
/// .border_color("#8a2518") // 源 CSS 的 :hover ⇒ 落在 .press() 之前 ⇒ 按下态 ✅
/// .press()
/// .background_color("#e6d8bc") // 源 CSS 的常态 ⇒ 落在 .press() 之后 ⇒ 仍是常态 ✅
/// .min_width(@style.px(116.0)) // grid 列宽也是常态调用,必须在这一侧 ✅
/// ```
///
/// 三种落法只有这一种对,而**错的两种都很难看见**(产物编得过、判据全绿,只是语义反了):
/// · 写在**链尾**(把常态整段标成按下态)—— 2026-10-23 第十七轮就这么修的,实测后果:
/// 卦卡的 `.min_width(116).flex_grow(1)` 变成"只在按住时生效" ⇒ **网格塌成单列**
/// (容器 331px、卡片 54px);第十八轮又量到更重的一层:`button.bagua-mode` 的常态
/// 只剩 `borderColor` + `color` 两条 —— 没内边距、没底色、没边框、没字号,
/// **按住才变成一个按钮**。
/// · 写在**链中间但两侧写反**(常态在前、hover 在后)—— 同样是整体互换,只是这次
/// 连"hover 那一半"也没落在按下态上。
///
/// 生成器侧的组装只有**一处**(`style-emit.js` 的 `emitModule`),判据在
/// `tools/regen_styles_check.mjs` 的"按下态组装顺序"那一节。
///
/// ⚠️ 消费侧(库的 `Pressable` 接线)**尚未就绪**(2026-10-23):生成器默认不产出它
/// (`DSH_PRESS_FROM_HOVER=1` 才开),等接线稳了再默认打开。
pub fn Style::press(self : Style) -> Style {
let out : Array[(String, StyleValue)] = []
for i = 0; i < self.entries.length(); i = i + 1 {
let (k, v) = self.entries[i]
out.push(("press:" + k, v))
}
{ entries: out }
}
///|
/// 键前缀(`render.mbt` 与生成物共用一个常量,别各自写死字符串)。
pub let press_prefix : String = "press:"
///|
/// 把一份 `(键, 值)` 列表**原样**装回 `Style` —— **不按名字过滤**。
///
/// 为什么不复用"按名字 match"的写法(那种写法踩过,2026-10-23):那种白名单只适用于
/// **特定用途**("哪些键会被文字继承")。一旦拿它去重建**常态样式**,名单外的键
/// (`minWidth` / `flexGrow` …)就被**静默丢掉** —— 症状是"样式看起来没生效",
/// 排查方向会被带偏。这个构造器只做类型还原,不认识名字,所以不会漏键。
pub fn Style::from_entries(entries : Array[(String, StyleValue)]) -> Style {
{ entries: entries.copy() }
}
///|
pub fn Style::is_empty(self : Style) -> Bool {
self.entries.length() == 0
}
// ---------------------------------------------------------------- 内部写入器
///|
fn Style::unitless(self : Style, key : String, v : Double) -> Style {
self.entries.push((key, Unitless(v)))
self
}
///|
fn Style::len(self : Style, key : String, v : Double) -> Style {
self.entries.push((key, Px(v)))
self
}
///|
fn Style::kw(self : Style, key : String, v : String) -> Style {
self.entries.push((key, Str(v)))
self
}
///|
fn Style::dim(self : Style, key : String, v : Dimension) -> Style {
self.entries.push((key, v.to_style_value()))
self
}
// ------------------------------------------------------------------ 关键字枚举
// 刻意只列 Web 与 RN 都有等价物的取值。
///|
pub(all) enum FlexDirection {
Row
Column
RowReverse
ColumnReverse
}
///|
pub fn FlexDirection::to_string(self : FlexDirection) -> String {
match self {
Row => "row"
Column => "column"
RowReverse => "row-reverse"
ColumnReverse => "column-reverse"
}
}
///|
pub(all) enum FlexWrap {
NoWrap
Wrap
WrapReverse
}
///|
pub fn FlexWrap::to_string(self : FlexWrap) -> String {
match self {
NoWrap => "nowrap"
Wrap => "wrap"
WrapReverse => "wrap-reverse"
}
}
///|
pub(all) enum Align {
FlexStart
FlexEnd
Center
Stretch
Baseline
}
///|
pub fn Align::to_string(self : Align) -> String {
match self {
FlexStart => "flex-start"
FlexEnd => "flex-end"
Center => "center"
Stretch => "stretch"
Baseline => "baseline"
}
}
///|
pub(all) enum AlignSelf {
Auto
FlexStart
FlexEnd
Center
Stretch
Baseline
}
///|
pub fn AlignSelf::to_string(self : AlignSelf) -> String {
match self {
Auto => "auto"
FlexStart => "flex-start"
FlexEnd => "flex-end"
Center => "center"
Stretch => "stretch"
Baseline => "baseline"
}
}
///|
pub(all) enum Justify {
FlexStart
FlexEnd
Center
SpaceBetween
SpaceAround
SpaceEvenly
}
///|
pub fn Justify::to_string(self : Justify) -> String {
match self {
FlexStart => "flex-start"
FlexEnd => "flex-end"
Center => "center"
SpaceBetween => "space-between"
SpaceAround => "space-around"
SpaceEvenly => "space-evenly"
}
}
///|
pub(all) enum Position {
Relative
Absolute
}
///|
pub fn Position::to_string(self : Position) -> String {
match self {
Relative => "relative"
Absolute => "absolute"
}
}
///|
pub(all) enum Overflow {
Visible
Hidden
Scroll
}
///|
pub fn Overflow::to_string(self : Overflow) -> String {
match self {
Visible => "visible"
Hidden => "hidden"
Scroll => "scroll"
}
}
///|
pub(all) enum BorderStyle {
Solid
Dotted
Dashed
}
///|
pub fn BorderStyle::to_string(self : BorderStyle) -> String {
match self {
Solid => "solid"
Dotted => "dotted"
Dashed => "dashed"
}
}
///|
/// ⚠️ 注意:`fontWeight` 在 RN 里是**字符串**,不是数字。
/// 把它做成枚举,就杜绝了"被 `Number()` 吃掉变成 700"这个静默 bug(README 的 F3)。
pub(all) enum FontWeight {
Normal
Bold
W100
W200
W300
W400
W500
W600
W700
W800
W900
}
///|
pub fn FontWeight::to_string(self : FontWeight) -> String {
match self {
Normal => "normal"
Bold => "bold"
W100 => "100"
W200 => "200"
W300 => "300"
W400 => "400"
W500 => "500"
W600 => "600"
W700 => "700"
W800 => "800"
W900 => "900"
}
}
///|
pub(all) enum FontStyle {
Normal
Italic
}
///|
pub fn FontStyle::to_string(self : FontStyle) -> String {
match self {
Normal => "normal"
Italic => "italic"
}
}
///|
pub(all) enum TextAlign {
Auto
Left
Right
Center
Justify
}
///|
pub fn TextAlign::to_string(self : TextAlign) -> String {
match self {
Auto => "auto"
Left => "left"
Right => "right"
Center => "center"
Justify => "justify"
}
}
///|
pub(all) enum TextDecorationLine {
None
Underline
LineThrough
UnderlineLineThrough
}
///|
pub fn TextDecorationLine::to_string(self : TextDecorationLine) -> String {
match self {
None => "none"
Underline => "underline"
LineThrough => "line-through"
UnderlineLineThrough => "underline line-through"
}
}
///|
pub(all) enum TextTransform {
None
Uppercase
Lowercase
Capitalize
}
///|
pub fn TextTransform::to_string(self : TextTransform) -> String {
match self {
None => "none"
Uppercase => "uppercase"
Lowercase => "lowercase"
Capitalize => "capitalize"
}
}
///|
/// 尺寸:`auto` / 绝对长度 / 百分比。用类型把它们分开,就不需要
/// `width` / `width_pct` / `width_auto` 三个方法。
pub(all) enum Dimension {
Auto
Px(Double)
Pct(Double)
}
///|
pub fn Dimension::to_style_value(self : Dimension) -> StyleValue {
match self {
Auto => Auto
Px(d) => Px(d)
Pct(d) => Pct(d)
}
}
// ------------------------------------------------------------------ 简短构造器
// 让调用点写 `@style.px(16)` 而不是 `@style.Dimension::Px(16)`。
///|
pub fn px(d : Double) -> Dimension {
Px(d)
}
///|
pub fn pct(d : Double) -> Dimension {
Pct(d)
}
///|
pub fn auto() -> Dimension {
Auto
}
// ---------------------------------------------------------------------- 布局
///|
pub fn Style::flex(self : Style, v : Double) -> Style {
self.unitless("flex", v)
}
///|
pub fn Style::flex_direction(self : Style, v : FlexDirection) -> Style {
self.kw("flexDirection", v.to_string())
}
///|
pub fn Style::flex_wrap(self : Style, v : FlexWrap) -> Style {
self.kw("flexWrap", v.to_string())
}
///|
pub fn Style::flex_grow(self : Style, v : Double) -> Style {
self.unitless("flexGrow", v)
}
///|
pub fn Style::flex_shrink(self : Style, v : Double) -> Style {
self.unitless("flexShrink", v)
}
///|
pub fn Style::flex_basis(self : Style, v : Dimension) -> Style {
self.dim("flexBasis", v)
}
///|
pub fn Style::align_items(self : Style, v : Align) -> Style {
self.kw("alignItems", v.to_string())
}
///|
pub fn Style::align_self(self : Style, v : AlignSelf) -> Style {
self.kw("alignSelf", v.to_string())
}
///|
pub fn Style::justify_content(self : Style, v : Justify) -> Style {
self.kw("justifyContent", v.to_string())
}
///|
pub fn Style::position(self : Style, v : Position) -> Style {
self.kw("position", v.to_string())
}
///|
pub fn Style::top(self : Style, v : Dimension) -> Style {
self.dim("top", v)
}
///|
pub fn Style::right_(self : Style, v : Dimension) -> Style {
self.dim("right", v)
}
///|
pub fn Style::bottom(self : Style, v : Dimension) -> Style {
self.dim("bottom", v)
}
///|
pub fn Style::left(self : Style, v : Dimension) -> Style {
self.dim("left", v)
}
///|
pub fn Style::z_index(self : Style, v : Double) -> Style {
self.unitless("zIndex", v)
}
///|
pub fn Style::overflow(self : Style, v : Overflow) -> Style {
self.kw("overflow", v.to_string())
}
// ---------------------------------------------------------------------- 尺寸
///|
pub fn Style::width(self : Style, v : Dimension) -> Style {
self.dim("width", v)
}
///|
pub fn Style::height(self : Style, v : Dimension) -> Style {
self.dim("height", v)
}
///|
pub fn Style::min_width(self : Style, v : Dimension) -> Style {
self.dim("minWidth", v)
}
///|
pub fn Style::max_width(self : Style, v : Dimension) -> Style {
self.dim("maxWidth", v)
}
///|
pub fn Style::min_height(self : Style, v : Dimension) -> Style {
self.dim("minHeight", v)
}
///|
pub fn Style::max_height(self : Style, v : Dimension) -> Style {
self.dim("maxHeight", v)
}
///|
pub fn Style::aspect_ratio(self : Style, v : Double) -> Style {
self.unitless("aspectRatio", v)
}
// ---------------------------------------------------------------------- 间隙
// RN 0.71+ 与 RNW 都支持 gap,属于可移植子集。
///|
pub fn Style::gap(self : Style, v : Double) -> Style {
self.len("gap", v)
}
///|
pub fn Style::row_gap(self : Style, v : Double) -> Style {
self.len("rowGap", v)
}
///|
pub fn Style::column_gap(self : Style, v : Double) -> Style {
self.len("columnGap", v)
}
// ---------------------------------------------------------------------- 外边距
///|
pub fn Style::margin(self : Style, v : Dimension) -> Style {
self.dim("margin", v)
}
///|
pub fn Style::margin_top(self : Style, v : Dimension) -> Style {
self.dim("marginTop", v)
}
///|
pub fn Style::margin_bottom(self : Style, v : Dimension) -> Style {
self.dim("marginBottom", v)
}
///|
pub fn Style::margin_left(self : Style, v : Dimension) -> Style {
self.dim("marginLeft", v)
}
///|
pub fn Style::margin_right(self : Style, v : Dimension) -> Style {
self.dim("marginRight", v)
}
///|
pub fn Style::margin_horizontal(self : Style, v : Dimension) -> Style {
self.dim("marginHorizontal", v)
}
///|
pub fn Style::margin_vertical(self : Style, v : Dimension) -> Style {
self.dim("marginVertical", v)
}
// ---------------------------------------------------------------------- 内边距
///|
pub fn Style::padding(self : Style, v : Dimension) -> Style {
self.dim("padding", v)
}
///|
pub fn Style::padding_top(self : Style, v : Dimension) -> Style {
self.dim("paddingTop", v)
}
///|
pub fn Style::padding_bottom(self : Style, v : Dimension) -> Style {
self.dim("paddingBottom", v)
}
///|
pub fn Style::padding_left(self : Style, v : Dimension) -> Style {
self.dim("paddingLeft", v)
}
///|
pub fn Style::padding_right(self : Style, v : Dimension) -> Style {
self.dim("paddingRight", v)
}
///|
pub fn Style::padding_horizontal(self : Style, v : Dimension) -> Style {
self.dim("paddingHorizontal", v)
}
///|
pub fn Style::padding_vertical(self : Style, v : Dimension) -> Style {
self.dim("paddingVertical", v)
}
// ---------------------------------------------------------------------- 边框
///|
pub fn Style::border_width(self : Style, v : Double) -> Style {
self.len("borderWidth", v)
}
///|
pub fn Style::border_top_width(self : Style, v : Double) -> Style {
self.len("borderTopWidth", v)
}
///|
pub fn Style::border_bottom_width(self : Style, v : Double) -> Style {
self.len("borderBottomWidth", v)
}
///|
pub fn Style::border_left_width(self : Style, v : Double) -> Style {
self.len("borderLeftWidth", v)
}
///|
pub fn Style::border_right_width(self : Style, v : Double) -> Style {
self.len("borderRightWidth", v)
}
///|
pub fn Style::border_color(self : Style, v : String) -> Style {
self.kw("borderColor", v)
}
///|
pub fn Style::border_style(self : Style, v : BorderStyle) -> Style {
self.kw("borderStyle", v.to_string())
}
///|
pub fn Style::border_radius(self : Style, v : Double) -> Style {
self.len("borderRadius", v)
}
///|
pub fn Style::border_top_left_radius(self : Style, v : Double) -> Style {
self.len("borderTopLeftRadius", v)
}
///|
pub fn Style::border_top_right_radius(self : Style, v : Double) -> Style {
self.len("borderTopRightRadius", v)
}
///|
pub fn Style::border_bottom_left_radius(self : Style, v : Double) -> Style {
self.len("borderBottomLeftRadius", v)
}
///|
pub fn Style::border_bottom_right_radius(self : Style, v : Double) -> Style {
self.len("borderBottomRightRadius", v)
}
// ---------------------------------------------------------------------- 视觉
///|
pub fn Style::background_color(self : Style, v : String) -> Style {
self.kw("backgroundColor", v)
}
///|
pub fn Style::opacity(self : Style, v : Double) -> Style {
self.unitless("opacity", v)
}
// ---------------------------------------------------------------------- 文字
///|
pub fn Style::color(self : Style, v : String) -> Style {
self.kw("color", v)
}
///|
pub fn Style::font_size(self : Style, v : Double) -> Style {
self.len("fontSize", v)
}
///|
pub fn Style::font_family(self : Style, v : String) -> Style {
self.kw("fontFamily", v)
}
///|
pub fn Style::font_weight(self : Style, v : FontWeight) -> Style {
self.kw("fontWeight", v.to_string())
}
///|
pub fn Style::font_style(self : Style, v : FontStyle) -> Style {
self.kw("fontStyle", v.to_string())
}
///|
pub fn Style::line_height(self : Style, v : Double) -> Style {
self.len("lineHeight", v)
}
///|
pub fn Style::letter_spacing(self : Style, v : Double) -> Style {
self.len("letterSpacing", v)
}
///|
pub fn Style::text_align(self : Style, v : TextAlign) -> Style {
self.kw("textAlign", v.to_string())
}
///|
pub fn Style::text_decoration_line(self : Style, v : TextDecorationLine) -> Style {
self.kw("textDecorationLine", v.to_string())
}
///|
pub fn Style::text_transform(self : Style, v : TextTransform) -> Style {
self.kw("textTransform", v.to_string())
}
///|
/// 指针形状(见 `Cursor` 的注释:**只有 web 有**,原生静默不画)。
///
/// ⚠️ 它**不改布局**,只改"这东西能不能点"的手感 ⇒ 别拿它当可点性的**唯一**表达:
/// 真正的可点性靠 `Pressable` / `on_click`(`cursor` 只是 web 上的视觉补充)。
pub fn Style::cursor(self : Style, v : Cursor) -> Style {
self.kw("cursor", v.to_string())
}
///|
/// 阴影 → **一条值**(`offsetX offsetY blurRadius spreadDistance color`)。
///
/// 为什么合成一条而不是发四个键:`boxShadow` 在现代 RN(New Architecture)与 `react-native-web`
/// **都**是一条字符串的键(RN 0.76 起 `boxShadow` 进登记表,取值形如 `"5px 5px 5px 5px rgba(...)"`)。
/// 发成 `shadowOffsetX/Y` 那套是**老写法**(Android 还得叠 `elevation`),两套并存会让"同一个阴影
/// 在两个后端长得不一样"。**这一条由 `tools/style_platform_check.mjs` 当场断言**:
/// 如果哪天 RN 的登记表里没有 `boxShadow` 了,那条门会红 —— 那时再回来改这里,别凭记忆。
///
/// ⚠️ 参数是**语义**(往哪偏、糊多少、扩散多少、什么色),**不是** CSS 字符串拼装权:
/// 调用点写不出 `"0 3px 12px red"` 这种自由形态,只能给数。
fn shadow_value(color : String, x : Double, y : Double, blur : Double, spread : Double) -> String {
let buf = StringBuilder::new()
buf.write_string(x.to_string())
buf.write_string("px ")
buf.write_string(y.to_string())
buf.write_string("px ")
buf.write_string(blur.to_string())
buf.write_string("px ")
buf.write_string(spread.to_string())
buf.write_string("px ")
buf.write_string(color)
buf.to_string()
}
///|
/// **阴影**(层次感的来源:卡片 / 浮层 / 下拉)。
///
/// ⚠️ 这是"甲级能力"里的第一条(`PLAN-ui §3.1`:各宿主都有对应物,只是形状不同):
/// web/RNW 与 RN 都吃这条字符串,所以**不需要按后端分叉**——分叉会带来"两端不一样"的新账。
/// ⚠️ 阴影**不占布局**(它画在盒子外面):窄容器里向上/向左的阴影会被裁掉,这与 CSS 同源,
/// 别指望它当边框用(要"看得见的边界"用 `border_*` + `line-2` 那个语义色)。
pub fn Style::box_shadow(
self : Style,
color : String,
x : Double,
y : Double,
blur : Double,
spread : Double
) -> Style {
self.kw("boxShadow", shadow_value(color, x, y, blur, spread))
}
///|
/// **一条变换**(`transform` 里的一个动作)。
///
/// ## 为什么是"有序的一串"而不是几个字段
///
/// 变换**顺序敏感**:`translate` 之后再 `scale`,与反过来**不是同一个结果**(后者缩放的才是位移后的坐标系)。
/// 所以 API 收 `Array[Transform]`,**数组顺序就是施加顺序** —— 换成结构体字段
/// (`x` / `scale` / `rotate` 各一格)就把"顺序"这件事永久丢掉了。
///
/// ## 为什么吐**字符串**(实测:两端都认,**不需要按后端分叉**)
///
/// 这是甲级三样里最容易被想当然的一条 —— 直觉是"RN 要数组、web 要字符串 ⇒ 必须分叉"。
/// **读两端自己的源码之后,这句不成立**:
///
/// · **RN**(`Libraries/StyleSheet/processTransform.js`):签名就是 `Array