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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +46 -0
- data/GOALS.md +361 -0
- data/LICENSE +21 -0
- data/README.md +293 -0
- data/lib/citrine/native/app.rb +126 -0
- data/lib/citrine/native/area_handle.rb +49 -0
- data/lib/citrine/native/painter.rb +625 -0
- data/lib/citrine/native/pointer_event.rb +48 -0
- data/lib/citrine/native/renderer.rb +884 -0
- data/lib/citrine/native/style_matrix.rb +127 -0
- data/lib/citrine/native/timer.rb +88 -0
- data/lib/citrine/native/version.rb +9 -0
- data/lib/citrine/native/widgets/libui.rb +1022 -0
- data/lib/citrine/native/widgets/memory.rb +526 -0
- data/lib/citrine/native/widgets.rb +179 -0
- data/lib/citrine/native.rb +89 -0
- data/lib/citrine-native.rb +5 -0
- metadata +130 -0
|
@@ -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
|