citrine-native 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Citrine
4
+ module Native
5
+ # 样式能力矩阵(机器可读;文档见 docs/design/style-matrix.md):
6
+ # 把 citrine 的样式 IR(`Style.normalize` 之后的 snake_case 键)逐个登记"在本后端
7
+ # 落到哪里"。三档状态:
8
+ #
9
+ # :mapped 自动映射到 libui 的既有语义(容器内边距、追加时的 stretchy…)
10
+ # :painted 只能自绘:`element(:area)` 会自动消费其中**视觉底板**那几个
11
+ # (background / border* / border_radius,见 Entry#area?),
12
+ # 文字类的键要在 on_draw 里用 Painter 表达
13
+ # :ignored 原生控件没有对应概念(阴影、动画、溢出…),dev_mode 提醒后忽略
14
+ #
15
+ # 为什么要有这张表(而不是把判断散在提醒代码里):应用的样式是"一份代码、多个
16
+ # 渲染目标",没有单一出处时"这条样式到底生效了没有"就只能靠人肉记忆——两个 demo
17
+ # 的移植期反复踩过(2026-09-15 的 Windows 实测又踩了一次)。提醒文案、文档与测试
18
+ # 都从这一份表读,改一处就够。
19
+ module StyleMatrix
20
+ MAPPED = :mapped
21
+ PAINTED = :painted
22
+ IGNORED = :ignored
23
+ STATUSES = [MAPPED, PAINTED, IGNORED].freeze
24
+
25
+ Entry = Struct.new(:status, :mapping, :note, :area, keyword_init: true) do
26
+ # area 元素是否**自动消费**这个键(是 :painted 里的视觉底板子集)
27
+ def area? = area == true
28
+ end
29
+
30
+ # 分组登记(同组共享文案;键清单来自两个 demo 的实际 CSS 普查 +
31
+ # citrine 核心的样式词表,见文档附表)
32
+ GROUPS = [
33
+ # ── 布局:能落到 box 的既有语义 ──────────────────────────
34
+ { keys: %i[display], status: MAPPED,
35
+ mapping: "box 的方向(stack / row 已表达)",
36
+ note: "只有 flex 布局有对应;grid 等 display 值无对应" },
37
+ { keys: %i[flex_direction], status: MAPPED,
38
+ mapping: "uiNewVerticalBox / uiNewHorizontalBox(由 stack / row 合成)",
39
+ note: "用户不必手写;换方向要换控件,运行期改无效" },
40
+ { keys: %i[gap], status: MAPPED,
41
+ mapping: "uiBoxSetPadded(0 / 非 0 两档)",
42
+ note: "像素级间距给不了:libui 的 padded 是开关" },
43
+ { keys: %i[padding padding_top padding_right padding_bottom padding_left],
44
+ status: MAPPED, mapping: "同 gap(容器内边距 → uiBoxSetPadded)",
45
+ note: "只有两档;单边的 padding_* 不细分" },
46
+ { keys: %i[flex_grow], status: MAPPED, mapping: "追加时的 stretchy",
47
+ note: "libui 是布尔不是权重:多个 stretchy 子控件等分剩余空间" },
48
+ { keys: %i[flex], status: MAPPED,
49
+ mapping: "同 flex_grow(CSS 简写:数值 > 0 才算拉伸)",
50
+ note: "flex-basis / flex-shrink 无对应" },
51
+
52
+ # ── 视觉底板:area 自动消费(L2)────────────────────────
53
+ { keys: %i[background], status: PAINTED, area: true,
54
+ mapping: "area 的底板填充(框架在 on_draw 之前画)",
55
+ note: "原生控件无法着色:放在 box / label 上不生效" },
56
+ { keys: %i[border], status: PAINTED, area: true,
57
+ mapping: "area 的描边(接受 \"1px solid #rrggbb\" 或纯颜色串)",
58
+ note: "只画实线;dashed / dotted 按实线画并提醒" },
59
+ { keys: %i[border_color border_width], status: PAINTED, area: true,
60
+ mapping: "area 的描边颜色 / 宽度(border 简写的展开形式)",
61
+ note: "分边(border_top…)无对应" },
62
+ { keys: %i[border_radius], status: PAINTED, area: true,
63
+ mapping: "area 底板的圆角(uiDrawPath 圆弧)",
64
+ note: "超过短边一半会被夹取并提醒;四角不同半径无对应" },
65
+
66
+ # ── 文字:只能自绘(Painter 能力)───────────────────────
67
+ { keys: %i[color font_size font_weight font_family letter_spacing],
68
+ status: PAINTED,
69
+ mapping: "painter.text(…, color: / size: / weight: / family:)",
70
+ note: "原生 label / button 的字体与颜色没有公开 API" },
71
+ { keys: %i[text_align line_height font_variant_numeric],
72
+ status: PAINTED,
73
+ mapping: "自绘时手工排(Painter 的 align: / 行距 / 等宽数字要自己算)",
74
+ note: "Painter#text 的 align: 需要同时给 width:" },
75
+
76
+ # ── 无对应概念 ─────────────────────────────────────────
77
+ { keys: %i[width height min_width max_width min_height max_height],
78
+ status: IGNORED, mapping: nil,
79
+ note: "libui 是拉伸式布局,没有尺寸来源;area 用 size: prop(仅滚动面板)" },
80
+ { keys: %i[margin margin_top margin_right margin_bottom margin_left],
81
+ status: IGNORED, mapping: nil, note: "靠容器 gap 近似" },
82
+ { keys: %i[align_items justify_content align_self order],
83
+ status: IGNORED, mapping: nil, note: "libui 的 box 没有对齐 / 分布能力" },
84
+ { keys: %i[position inset top right bottom left z_index],
85
+ status: IGNORED, mapping: nil, note: "原生控件没有定位与层叠(box 顺序即层序)" },
86
+ { keys: %i[box_shadow opacity outline outline_width outline_offset transform filter],
87
+ status: IGNORED, mapping: nil, note: "原生控件无法绘制这些效果" },
88
+ { keys: %i[transition animation],
89
+ status: IGNORED, mapping: nil, note: "原生控件自己管绘制时机,没有补间动画" },
90
+ { keys: %i[overflow overflow_x overflow_y white_space text_overflow word_break],
91
+ status: IGNORED, mapping: nil,
92
+ note: "溢出 / 省略号要自绘(Painter 里手工截断,两个 demo 都已这么做)" },
93
+ { keys: %i[cursor user_select pointer_events],
94
+ status: IGNORED, mapping: nil, note: "原生控件自己管指针外观" },
95
+ { keys: %i[border_style border_top border_right border_bottom border_left box_sizing],
96
+ status: IGNORED, mapping: nil, note: "只支持整体描边(border / border_color / border_width)" },
97
+ { keys: %i[visibility float clear],
98
+ status: IGNORED, mapping: nil, note: "要隐藏元素请在组件里条件渲染(透明容器语义)" }
99
+ ].freeze
100
+
101
+ TABLE = GROUPS.each_with_object({}) do |group, table|
102
+ group[:keys].each do |key|
103
+ table[key] = Entry.new(status: group[:status], mapping: group[:mapping],
104
+ note: group[:note], area: group[:area])
105
+ end
106
+ end.freeze
107
+
108
+ UNKNOWN = Entry.new(status: IGNORED, mapping: nil,
109
+ note: "未在矩阵中登记,按无对应概念处理").freeze
110
+
111
+ module_function
112
+
113
+ # 未登记的键 → UNKNOWN(提醒时也会说"未登记",别让应用以为登记过)
114
+ def entry(key) = TABLE[key] || UNKNOWN
115
+
116
+ def status(key) = entry(key).status
117
+
118
+ def registered?(key) = TABLE.key?(key)
119
+
120
+ # area 会自动消费的键(渲染器的底板绘制用它,测试也用它做覆盖断言)
121
+ def area_keys = TABLE.select { |_key, value| value.area? }.keys.freeze
122
+
123
+ # 自动映射的键(提醒时要静音;实际落地在 renderer 的 apply_padding / stretchy?)
124
+ def mapped_keys = TABLE.select { |_key, value| value.status == MAPPED }.keys.freeze
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Citrine
4
+ module Native
5
+ # 定时器(设计 2.5):后台 Thread + sleep,到点经适配层的 `queue_main` 排回主线程。
6
+ #
7
+ # handle = Citrine::Native.every(200) { tick } # 周期
8
+ # once = Citrine::Native.after(500) { once } # 一次
9
+ # handle.stop # 幂等;可从主线程/回调内调用
10
+ #
11
+ # 为什么不用 libui 的 uiTimer:它只能从主线程注册、且生命周期绑在主循环上;走
12
+ # "后台线程 + queue_main" 之后,定时器只依赖适配层的跨线程通道(也就是文档给应用的
13
+ # 唯一跨线程规则),停下也能从任何线程调用。
14
+ #
15
+ # 回调体在主线程执行(与所有控件操作同线程),异常按"回调里的异常不中断应用"的既有
16
+ # 口径打到 stderr 后继续(周期定时器继续跑,单次定时器就此结束)。
17
+ class Timer
18
+ attr_reader :interval
19
+
20
+ def self.every(milliseconds, &block) = new(milliseconds, repeat: true, &block)
21
+ def self.after(milliseconds, &block) = new(milliseconds, repeat: false, &block)
22
+
23
+ def initialize(milliseconds, repeat:, &block)
24
+ raise ArgumentError, "定时器需要块:Citrine::Native.every(ms) { … }" unless block
25
+
26
+ interval = milliseconds.to_f
27
+ raise ArgumentError, "定时器间隔应为正数毫秒,收到 #{milliseconds.inspect}" unless interval.positive?
28
+
29
+ @interval = interval
30
+ @repeat = repeat
31
+ @block = block
32
+ @stopped = false
33
+ @thread = Thread.new { run }
34
+ @thread.name = "citrine-native-timer"
35
+ end
36
+
37
+ # 幂等(重复 stop 无副作用),可从主线程或在定时器回调内调用
38
+ def stop
39
+ @stopped = true
40
+ @thread&.kill
41
+ self
42
+ end
43
+
44
+ def stopped? = @stopped
45
+ def running? = !@stopped
46
+
47
+ private
48
+
49
+ def run
50
+ loop do
51
+ sleep(@interval / 1000.0)
52
+ break if @stopped
53
+
54
+ queue_tick
55
+ break unless @repeat
56
+ end
57
+ end
58
+
59
+ # 到点:把回调排回主线程。stop 与"排到主线程"之间有竞争窗口(stop 可能在块执行前
60
+ # 发生,比如组件卸载),所以块内再查一次 @stopped——卸载后不该再被定时器叫醒。
61
+ #
62
+ # 用 **queue_main_once**(执行完就释放引用)而不是 queue_main:定时器每次到点都排一个
63
+ # 新闭包,而 queue_main 会把闭包常驻住(Fiddle 闭包不能被 GC 回收,见 widgets/libui.rb),
64
+ # 于是常驻写法等于"每个 tick 漏一个闭包"——NA-2 实测 50ms 定时器 5 秒内
65
+ # closures/ticks = 54/52(慢漏但无界),而两个移植 demo 都在用 `every`。
66
+ # 定时器的闭包本来就只需执行一次,语义等价;按钮/按键那类**必须**常驻的订阅闭包仍走 queue_main。
67
+ def queue_tick
68
+ widgets = Citrine::Native.active_widgets
69
+ if widgets.nil?
70
+ warn "[citrine-native] 定时器到点了但没有活动后端(Citrine::Native.active_widgets 为空):" \
71
+ "定时器要在 Citrine::Native.run/start 之后创建"
72
+ return
73
+ end
74
+
75
+ widgets.queue_main_once do
76
+ tick unless @stopped
77
+ end
78
+ end
79
+
80
+ def tick
81
+ @block.call
82
+ rescue StandardError => e
83
+ warn "[citrine-native] 定时器回调抛出 #{e.class}: #{e.message}"
84
+ warn(e.backtrace.first(8).map { |line| " #{line}" }.join("\n"))
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Citrine
4
+ module Native
5
+ # 首个发布版本(N5):N0–N4 全部落地后定版。
6
+ # 发布流程见 CHANGELOG.md 与 .github/workflows/release.yml(Trusted Publishing)。
7
+ VERSION = "0.1.0"
8
+ end
9
+ end