citrine-native 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +18 -38
- data/GOALS.md +28 -361
- data/README.md +27 -271
- data/lib/citrine/native/painter.rb +17 -274
- data/lib/citrine/native/version.rb +1 -1
- data/lib/citrine/native/widgets.rb +8 -4
- data/lib/citrine/native.rb +71 -21
- data/lib/citrine-native.rb +8 -2
- metadata +6 -49
- data/lib/citrine/native/widgets/libui.rb +0 -1022
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module Citrine
|
|
4
4
|
module Native
|
|
5
|
-
#
|
|
5
|
+
# 自绘面板的绘制协议(冻结接口见 docs/design/native-area.md 2.2,文档在后端包)。
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# 三个部分各司其职,参数归一只有一份,各后端实现不会漂移:
|
|
8
8
|
# Primitives —— 图元签名 + 颜色/字重/对齐/圆角归一 + "未支持用法"的提醒收集(纯 Ruby)
|
|
9
|
-
# Painter —— 真 libui 绘制(Primitives 的平台实现;坐标是面板本地像素,左上角原点)
|
|
10
9
|
# Recording —— 只记录图元调用序列的桩实现(Memory 后端与"画了什么"的断言用)
|
|
10
|
+
# 各后端 —— 真绘制实现(libui 包:ctx 版 Painter + TextCache;
|
|
11
|
+
# GTK 包:GtkDrawingArea + Cairo,规划中)
|
|
11
12
|
#
|
|
12
|
-
# 生命周期:**一个面板一次 on_draw 一个
|
|
13
|
-
# 跨帧复用的只有文本布局(TextCache
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
class Painter
|
|
13
|
+
# 生命周期:**一个面板一次 on_draw 一个 painter 实例,不跨帧复用**。
|
|
14
|
+
# 跨帧复用的只有文本布局(libui 包的 TextCache 按面板持有、随面板销毁释放——
|
|
15
|
+
# text layout / attributed string 不归 Ruby GC 管,漏 free 就是每帧漏 C 内存)。
|
|
16
|
+
module Painter
|
|
17
17
|
# ── "未支持用法/无效值"的提醒收集 ────────────────────────
|
|
18
|
-
#
|
|
18
|
+
# painter 每帧新建(见上),自己 warn 会每帧刷屏;这里只按 key 收集,
|
|
19
19
|
# 交给渲染器按 dev_mode 去重后输出(Renderer#report_painter_warnings)。
|
|
20
20
|
module Warnings
|
|
21
21
|
# key => message(同一 key 只留第一条)
|
|
@@ -42,7 +42,7 @@ module Citrine
|
|
|
42
42
|
DEFAULT_LINE_COLOR = "#000000"
|
|
43
43
|
DEFAULT_TEXT_SIZE = 13
|
|
44
44
|
|
|
45
|
-
#
|
|
45
|
+
# 各后端的约定:宽度为负 = 不换行(libui:Width < 0 → CGFLOAT_MAX)
|
|
46
46
|
NO_WRAP = -1.0
|
|
47
47
|
|
|
48
48
|
# 对齐只在给定宽度内生效(布局的 Width 决定外接矩形),没有 width 时按左对齐
|
|
@@ -102,7 +102,7 @@ module Citrine
|
|
|
102
102
|
[entry.measured_width, entry.measured_height]
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
-
#
|
|
105
|
+
# 块内裁剪(后端的 save/clip/restore):块里照常画,矩形外的部分被裁掉
|
|
106
106
|
def clip(x, y, w, h, &block)
|
|
107
107
|
raise ArgumentError, "clip 需要块:p.clip(x, y, w, h) { … }" unless block
|
|
108
108
|
|
|
@@ -229,7 +229,7 @@ module Citrine
|
|
|
229
229
|
if key != :left && width.nil?
|
|
230
230
|
note_warning(:align_without_width,
|
|
231
231
|
"[citrine-native] text 的 align: #{key.inspect} 需要同时给 width:" \
|
|
232
|
-
"
|
|
232
|
+
"(对齐是在给定宽度内对齐),本次按左对齐绘制")
|
|
233
233
|
return :left
|
|
234
234
|
end
|
|
235
235
|
key
|
|
@@ -249,7 +249,7 @@ module Citrine
|
|
|
249
249
|
end
|
|
250
250
|
|
|
251
251
|
# ── 平台实现(emit_*)与文本布局入口 ──────────────────
|
|
252
|
-
#
|
|
252
|
+
# 各后端实现:libui 包(ctx 调用)/ GTK 包(Cairo,规划中)/ Recording(记录)。
|
|
253
253
|
|
|
254
254
|
def emit_rect(_x, _y, _w, _h, _fill, _stroke, _line_width, _radius)
|
|
255
255
|
raise NotImplementedError, "#{self.class}#emit_rect 未实现"
|
|
@@ -284,265 +284,18 @@ module Citrine
|
|
|
284
284
|
end
|
|
285
285
|
end
|
|
286
286
|
|
|
287
|
-
# 一条文本布局缓存项:绘制对象 + 度量结果 +
|
|
287
|
+
# 一条文本布局缓存项:绘制对象 + 度量结果 + 释放所需的平台对象。
|
|
288
288
|
# 不是冻结接口的一部分(应用只拿到绘制方法,拿不到它)。
|
|
289
289
|
TextEntry = Struct.new(:text, :size, :weight, :family, :color, :wrap_width, :align,
|
|
290
290
|
:layout, :attr_string, :font, :measured_width, :measured_height,
|
|
291
291
|
keyword_init: true)
|
|
292
292
|
|
|
293
|
-
include Primitives
|
|
294
|
-
|
|
295
|
-
attr_reader :width, :height
|
|
296
|
-
|
|
297
|
-
# @param ctx [Fiddle::Pointer] uiDrawContext(只在本次 Draw 回调内有效)
|
|
298
|
-
# @param cache [TextCache] 文本布局缓存:跨帧复用,由适配层按面板持有(随面板销毁 clear!)
|
|
299
|
-
# @param clip [Array, nil] 当前可见区 [x, y, w, h](内容坐标;nil = 整块面板可见)
|
|
300
|
-
def initialize(ctx:, width:, height:, cache:, clip: nil)
|
|
301
|
-
self.class.libui!
|
|
302
|
-
@ctx = ctx
|
|
303
|
-
@width = width.to_f
|
|
304
|
-
@height = height.to_f
|
|
305
|
-
@cache = cache
|
|
306
|
-
@clip = clip || [0.0, 0.0, @width, @height]
|
|
307
|
-
@brush = ::LibUI::FFI::DrawBrush.malloc
|
|
308
|
-
@brush.Type = ::LibUI::DrawBrushTypeSolid
|
|
309
|
-
@stroke = ::LibUI::FFI::DrawStrokeParams.malloc
|
|
310
|
-
@stroke.Cap = ::LibUI::DrawLineCapFlat
|
|
311
|
-
@stroke.Join = ::LibUI::DrawLineJoinMiter
|
|
312
|
-
@stroke.MiterLimit = ::LibUI::DrawDefaultMiterLimit
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
private
|
|
316
|
-
|
|
317
|
-
# ── 平台实现:真 libui 调用 ──────────────────────────────
|
|
318
|
-
# 路径(uiDrawPath)是"一次性"对象:画完立刻 uiDrawFreePath,不能跨图元复用
|
|
319
|
-
# (libui 没有 reset API,且路径不归 Ruby GC 管——漏 free 就是每帧漏一段 C 内存)。
|
|
320
|
-
|
|
321
|
-
def emit_rect(x, y, w, h, fill, stroke, line_width, radius)
|
|
322
|
-
with_path do |path|
|
|
323
|
-
if radius.positive?
|
|
324
|
-
round_rect(path, x, y, w, h, radius)
|
|
325
|
-
else
|
|
326
|
-
::LibUI.draw_path_add_rectangle(path, x, y, w, h)
|
|
327
|
-
end
|
|
328
|
-
::LibUI.draw_path_end(path)
|
|
329
|
-
fill_path(path, fill)
|
|
330
|
-
stroke_path(path, stroke, line_width)
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def emit_line(x1, y1, x2, y2, color, width)
|
|
335
|
-
with_path do |path|
|
|
336
|
-
::LibUI.draw_path_new_figure(path, x1, y1)
|
|
337
|
-
::LibUI.draw_path_line_to(path, x2, y2)
|
|
338
|
-
::LibUI.draw_path_end(path)
|
|
339
|
-
stroke_path(path, color, width)
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
def emit_polyline(points, color, width)
|
|
344
|
-
with_path do |path|
|
|
345
|
-
trace(path, points)
|
|
346
|
-
::LibUI.draw_path_end(path)
|
|
347
|
-
stroke_path(path, color, width)
|
|
348
|
-
end
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
def emit_polygon(points, fill, stroke, line_width)
|
|
352
|
-
with_path do |path|
|
|
353
|
-
trace(path, points)
|
|
354
|
-
::LibUI.draw_path_close_figure(path)
|
|
355
|
-
::LibUI.draw_path_end(path)
|
|
356
|
-
fill_path(path, fill)
|
|
357
|
-
stroke_path(path, stroke, line_width)
|
|
358
|
-
end
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
def emit_text(entry, x, y)
|
|
362
|
-
# (x, y) 是整段文本外接矩形的**左上角**(libui 的语义),不是基线
|
|
363
|
-
::LibUI.draw_text(@ctx, entry.layout, x, y)
|
|
364
|
-
self
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
def emit_clip_begin(x, y, w, h)
|
|
368
|
-
::LibUI.draw_save(@ctx)
|
|
369
|
-
with_path do |path|
|
|
370
|
-
::LibUI.draw_path_add_rectangle(path, x, y, w, h)
|
|
371
|
-
::LibUI.draw_path_end(path)
|
|
372
|
-
::LibUI.draw_clip(@ctx, path)
|
|
373
|
-
end
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
def emit_clip_end
|
|
377
|
-
::LibUI.draw_restore(@ctx)
|
|
378
|
-
self
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
def text_entry(text, size:, weight:, family:, color:, wrap_width:, align:)
|
|
382
|
-
@cache.entry(string: text, size: size, weight: weight, family: family, color: color,
|
|
383
|
-
wrap_width: wrap_width, align: align)
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
def with_path
|
|
387
|
-
path = ::LibUI.draw_new_path(::LibUI::DrawFillModeWinding)
|
|
388
|
-
begin
|
|
389
|
-
yield path
|
|
390
|
-
ensure
|
|
391
|
-
::LibUI.draw_free_path(path)
|
|
392
|
-
end
|
|
393
|
-
self
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
def trace(path, points)
|
|
397
|
-
points.each_with_index do |(x, y), index|
|
|
398
|
-
index.zero? ? ::LibUI.draw_path_new_figure(path, x, y)
|
|
399
|
-
: ::LibUI.draw_path_line_to(path, x, y)
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
# 圆角矩形:四角各一段 90° 圆弧(y 轴向下,角度从 +x 往 +y 增长即顺时针)
|
|
404
|
-
def round_rect(path, x, y, w, h, radius)
|
|
405
|
-
half_pi = Math::PI / 2
|
|
406
|
-
::LibUI.draw_path_new_figure_with_arc(path, x + radius, y + radius, radius, Math::PI, half_pi, 0)
|
|
407
|
-
::LibUI.draw_path_line_to(path, x + w - radius, y)
|
|
408
|
-
::LibUI.draw_path_arc_to(path, x + w - radius, y + radius, radius, -half_pi, half_pi, 0)
|
|
409
|
-
::LibUI.draw_path_line_to(path, x + w, y + h - radius)
|
|
410
|
-
::LibUI.draw_path_arc_to(path, x + w - radius, y + h - radius, radius, 0.0, half_pi, 0)
|
|
411
|
-
::LibUI.draw_path_line_to(path, x + radius, y + h)
|
|
412
|
-
::LibUI.draw_path_arc_to(path, x + radius, y + h - radius, radius, half_pi, half_pi, 0)
|
|
413
|
-
::LibUI.draw_path_close_figure(path)
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
def fill_path(path, color)
|
|
417
|
-
return if color.nil?
|
|
418
|
-
|
|
419
|
-
@brush.R = color[0]
|
|
420
|
-
@brush.G = color[1]
|
|
421
|
-
@brush.B = color[2]
|
|
422
|
-
@brush.A = color[3]
|
|
423
|
-
::LibUI.draw_fill(@ctx, path, @brush)
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
def stroke_path(path, color, width)
|
|
427
|
-
return if color.nil? || width <= 0
|
|
428
|
-
|
|
429
|
-
@brush.R = color[0]
|
|
430
|
-
@brush.G = color[1]
|
|
431
|
-
@brush.B = color[2]
|
|
432
|
-
@brush.A = color[3]
|
|
433
|
-
@stroke.Thickness = width
|
|
434
|
-
::LibUI.draw_stroke(@ctx, path, @brush, @stroke)
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
# 文本布局缓存(每个面板一份,见类注释)。面板销毁时必须 clear!。
|
|
438
|
-
#
|
|
439
|
-
# 键除设计里写的 (string, size, weight, family) 还带上 color / wrap_width / align:
|
|
440
|
-
# 颜色是烘进 attributed string 的属性(涨跌红绿靠它),宽度与对齐决定换行与外接
|
|
441
|
-
# 矩形——少任何一项,缓存命中都会拿到"另一段文本"的布局。
|
|
442
|
-
class TextCache
|
|
443
|
-
ALIGN_CODES = { left: :DrawTextAlignLeft, center: :DrawTextAlignCenter,
|
|
444
|
-
right: :DrawTextAlignRight }.freeze
|
|
445
|
-
|
|
446
|
-
def initialize
|
|
447
|
-
Painter.libui!
|
|
448
|
-
@entries = {}
|
|
449
|
-
@fonts = {}
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
# 取(或建)一条布局缓存
|
|
453
|
-
def entry(string:, size:, weight:, family:, color:, wrap_width:, align:)
|
|
454
|
-
@entries[[string, size, weight, family, color, wrap_width, align]] ||=
|
|
455
|
-
build(string, size, weight, family, color, wrap_width, align)
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
# 释放全部 libui 对象(顺序:layout → 它引用的 attributed string / 字体描述符)
|
|
459
|
-
def clear!
|
|
460
|
-
@entries.each_value do |cached|
|
|
461
|
-
::LibUI.draw_free_text_layout(cached.layout)
|
|
462
|
-
::LibUI.free_attributed_string(cached.attr_string)
|
|
463
|
-
end
|
|
464
|
-
@entries.clear
|
|
465
|
-
@fonts.each_value do |font|
|
|
466
|
-
# 只有 uiLoadControlFont 填过的描述符能交给 uiFreeFontDescriptor;
|
|
467
|
-
# 自建 family 的(Family 指向我们自己的 buffer)交给它会被 free 掉
|
|
468
|
-
# 不该 free 的内存——实测直接 abort 进程(GOALS 变更日志 NA-1)
|
|
469
|
-
::LibUI.free_font_descriptor(font[:descriptor]) if font[:libui_owned]
|
|
470
|
-
end
|
|
471
|
-
@fonts.clear
|
|
472
|
-
self
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
# 缓存条目数(测试断言"布局真的被复用"用:画 100 帧,条目数不该跟着涨)
|
|
476
|
-
def entry_count = @entries.size
|
|
477
|
-
|
|
478
|
-
def font_count = @fonts.size
|
|
479
|
-
|
|
480
|
-
private
|
|
481
|
-
|
|
482
|
-
def build(string, size, weight, family, color, wrap_width, align)
|
|
483
|
-
font = font_for(size, weight, family)
|
|
484
|
-
attr_string = attributed_string(string, color)
|
|
485
|
-
params = ::LibUI::FFI::DrawTextLayoutParams.malloc
|
|
486
|
-
params.String = attr_string
|
|
487
|
-
params.DefaultFont = font[:descriptor]
|
|
488
|
-
params.Width = wrap_width
|
|
489
|
-
params.Align = ::LibUI.const_get(ALIGN_CODES.fetch(align))
|
|
490
|
-
layout = ::LibUI.draw_new_text_layout(params)
|
|
491
|
-
|
|
492
|
-
width_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DOUBLE, Fiddle::RUBY_FREE)
|
|
493
|
-
height_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DOUBLE, Fiddle::RUBY_FREE)
|
|
494
|
-
::LibUI.draw_text_layout_extents(layout, width_ptr, height_ptr)
|
|
495
|
-
TextEntry.new(text: string, size: size, weight: weight, family: family, color: color,
|
|
496
|
-
wrap_width: wrap_width, align: align, layout: layout,
|
|
497
|
-
attr_string: attr_string, font: font,
|
|
498
|
-
measured_width: double_at(width_ptr), measured_height: double_at(height_ptr))
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
# 字号与字重都写在**字体描述符**上(libui 用 params.DefaultFont 铺满整段文本),
|
|
502
|
-
# 颜色作为属性烘进 attributed string(涨跌红绿)
|
|
503
|
-
def attributed_string(string, color)
|
|
504
|
-
attr_string = ::LibUI.new_attributed_string(string)
|
|
505
|
-
bytes = string.bytesize
|
|
506
|
-
return attr_string if bytes.zero? || color.nil?
|
|
507
|
-
|
|
508
|
-
# uiAttributedStringSetAttribute 接管属性所有权(uiFreeAttributedString 连它们一起释放),
|
|
509
|
-
# 因此这里**不能**再 uiFreeAttribute——那是 double free
|
|
510
|
-
::LibUI.attributed_string_set_attribute(attr_string, ::LibUI.new_color_attribute(*color), 0, bytes)
|
|
511
|
-
attr_string
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
# 字体描述符缓存:默认走 uiLoadControlFont(系统控制字体,Family 由 libui 分配),
|
|
515
|
-
# 显式给了 family 就自己填——那条路的 Family 指向我们 malloc 的 buffer,
|
|
516
|
-
# 结构体与 buffer 都交给 Fiddle 的 RUBY_FREE 释放(见 clear! 的说明)
|
|
517
|
-
def font_for(size, weight, family)
|
|
518
|
-
@fonts[[size, weight, family]] ||= if family.nil?
|
|
519
|
-
descriptor = ::LibUI::FFI::FontDescriptor.malloc
|
|
520
|
-
::LibUI.load_control_font(descriptor)
|
|
521
|
-
descriptor.Size = size
|
|
522
|
-
descriptor.Weight = weight
|
|
523
|
-
{ descriptor: descriptor, buffer: nil, libui_owned: true }
|
|
524
|
-
else
|
|
525
|
-
buffer = Fiddle::Pointer.malloc(family.bytesize + 1, Fiddle::RUBY_FREE)
|
|
526
|
-
buffer[0, family.bytesize + 1] = "#{family}\0"
|
|
527
|
-
descriptor = ::LibUI::FFI::FontDescriptor.malloc
|
|
528
|
-
descriptor.Family = buffer
|
|
529
|
-
descriptor.Size = size
|
|
530
|
-
descriptor.Weight = weight
|
|
531
|
-
descriptor.Italic = ::LibUI::TextItalicNormal
|
|
532
|
-
descriptor.Stretch = ::LibUI::TextStretchNormal
|
|
533
|
-
{ descriptor: descriptor, buffer: buffer, libui_owned: false }
|
|
534
|
-
end
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
def double_at(pointer) = pointer[0, Fiddle::SIZEOF_DOUBLE].unpack1("d")
|
|
538
|
-
end
|
|
539
|
-
|
|
540
293
|
# 记录图元调用序列(Memory 后端与 demo 的绘制断言用;不画任何东西)。
|
|
541
|
-
#
|
|
294
|
+
# 与各后端的真实现共用同一份 Primitives:归一后的参数进 @calls。
|
|
542
295
|
#
|
|
543
|
-
#
|
|
296
|
+
# 度量是**桩**:没有平台字体度量,measure_text 按字符数粗估
|
|
544
297
|
# (CJK 按两个字符宽),只保证"量出来是正数、随字号变大"这类弱断言。
|
|
545
|
-
#
|
|
298
|
+
# 需要真度量的测试跑真控件冒烟(后端包的冒烟脚本)。
|
|
546
299
|
class Recording
|
|
547
300
|
include Primitives
|
|
548
301
|
|
|
@@ -610,16 +363,6 @@ module Citrine
|
|
|
610
363
|
units * size * 0.55
|
|
611
364
|
end
|
|
612
365
|
end
|
|
613
|
-
|
|
614
|
-
class << self
|
|
615
|
-
# libui 只在真要用它时加载:Memory 后端下的单测不碰动态库
|
|
616
|
-
def libui!
|
|
617
|
-
require "libui"
|
|
618
|
-
rescue LoadError => e
|
|
619
|
-
raise Citrine::Native::ToolkitUnavailableError,
|
|
620
|
-
"自绘面板需要 libui(#{e.message}):纯逻辑测试请用 Memory 后端"
|
|
621
|
-
end
|
|
622
|
-
end
|
|
623
366
|
end
|
|
624
367
|
end
|
|
625
368
|
end
|
|
@@ -17,14 +17,18 @@ module Citrine
|
|
|
17
17
|
# ——五个回调槽必须在创建时全部装上
|
|
18
18
|
module Widgets
|
|
19
19
|
class << self
|
|
20
|
-
#
|
|
20
|
+
# 默认后端:核心包不带控件实现,解析交给 Citrine::Native.default_backend
|
|
21
|
+
# (后端包被 require 时自设,如 -libui 设 :libui)。测试注入 Memory 后端:
|
|
21
22
|
# Renderer.new(widgets: Widgets::Memory.new)
|
|
22
23
|
def default
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
backend = Citrine::Native.default_backend
|
|
25
|
+
raise Citrine::Native::BackendNotSelectedError,
|
|
26
|
+
"没有选择后端:请传 backend: / widgets:,或先 require 后端 gem" "(citrine-native-libui / citrine-native-gtk)" unless backend
|
|
27
|
+
|
|
28
|
+
Citrine::Native.resolve_backend(backend)
|
|
25
29
|
end
|
|
26
30
|
|
|
27
|
-
# 内存打桩后端(CRuby
|
|
31
|
+
# 内存打桩后端(CRuby 单测用;不加载任何 UI 工具包)
|
|
28
32
|
def memory
|
|
29
33
|
Memory.new
|
|
30
34
|
end
|
data/lib/citrine/native.rb
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Citrine::Native
|
|
3
|
+
# Citrine::Native —— Citrine 组件的 CRuby 原生运行时**核心**(后端无关)。
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# 这里只有平台无关的部分:Renderer(节点树 → 控件树翻译)、App(窗口生命周期)、
|
|
6
|
+
# StyleMatrix、Painter 协议(Primitives + Recording)、Widgets::Base 协议 +
|
|
7
|
+
# Memory 桩、定时器。具体控件由后端 gem 提供,按名字选择:
|
|
7
8
|
#
|
|
8
|
-
# require "citrine-native"
|
|
9
|
+
# require "citrine-native" # 核心
|
|
10
|
+
# Citrine::Native.run(Counter, backend: :libui) # → citrine-native-libui
|
|
11
|
+
# Citrine::Native.run(Counter, backend: :gtk) # → citrine-native-gtk
|
|
9
12
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# label { "计数:#{count}" }
|
|
15
|
-
# button(on_click: -> { self.count += 1 }) { "点我 +1" }
|
|
16
|
-
# end
|
|
17
|
-
# end
|
|
18
|
-
# end
|
|
13
|
+
# 约定:后端 gem 名 = citrine-native-<名字>,控件适配类 =
|
|
14
|
+
# Citrine::Native::Widgets::<CamelCase>(名字)(libui → Libui,gtk → Gtk,qt → Qt)。
|
|
15
|
+
# 非常规命名用 Citrine::Native.register_backend 注册。也可以直接传实例:
|
|
16
|
+
# Citrine::Native.run(Counter, widgets: SomeBackend.new)
|
|
19
17
|
#
|
|
20
|
-
#
|
|
18
|
+
# 后端包(如 citrine-native-libui)被加载时会自登记,并把自己设为
|
|
19
|
+
# default_backend——所以"require 后端 gem 之后不传 backend:"也能跑。
|
|
21
20
|
|
|
22
21
|
require "citrine"
|
|
23
22
|
|
|
@@ -29,8 +28,11 @@ module Citrine
|
|
|
29
28
|
# 元素/属性在原生后端没有对应概念(GOALS 4.3:不静默降级)
|
|
30
29
|
class UnsupportedElementError < Error; end
|
|
31
30
|
|
|
32
|
-
# 后端工具包不可用(缺少 libui 动态库等)
|
|
31
|
+
# 后端工具包不可用(缺少 libui/GTK 动态库等)
|
|
33
32
|
class ToolkitUnavailableError < Error; end
|
|
33
|
+
|
|
34
|
+
# 未选择后端(core 自己不带控件实现)
|
|
35
|
+
class BackendNotSelectedError < Error; end
|
|
34
36
|
end
|
|
35
37
|
end
|
|
36
38
|
|
|
@@ -51,6 +53,40 @@ module Citrine
|
|
|
51
53
|
# 挂载的那个"同口径)。定时器靠它把自己排回主线程,应用一般不用碰。
|
|
52
54
|
attr_accessor :active_widgets
|
|
53
55
|
|
|
56
|
+
# 默认后端名(符号):由后端包在被加载时自设(如 -libui 设 :libui),
|
|
57
|
+
# 应用也可以显式指定。nil = 未设(run 时不传 backend:/widgets: 会报错)。
|
|
58
|
+
attr_accessor :default_backend
|
|
59
|
+
|
|
60
|
+
# 已登记的后端(名字 → 控件适配类全名)
|
|
61
|
+
def registry
|
|
62
|
+
@registry ||= {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 登记后端(非常规命名的出口;常规命名靠 resolve_backend 的约定即可)
|
|
66
|
+
def register_backend(name, widgets:)
|
|
67
|
+
registry[name.to_sym] = widgets.to_s
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# 按名字解析并实例化后端:约定 require "citrine-native-<名字>",
|
|
72
|
+
# 控件类 = Citrine::Native::Widgets::<Camel>(名字)
|
|
73
|
+
def resolve_backend(name)
|
|
74
|
+
name = name.to_sym
|
|
75
|
+
widgets_class = registry[name] ||
|
|
76
|
+
"Citrine::Native::Widgets::#{camelize(name)}"
|
|
77
|
+
require "citrine-native-#{name}"
|
|
78
|
+
widgets_class.split("::").inject(Object) { |mod, constant| mod.const_get(constant) }.new
|
|
79
|
+
rescue LoadError => e
|
|
80
|
+
raise ToolkitUnavailableError,
|
|
81
|
+
"后端 #{name} 不可用(#{e.message}):gem citrine-native-#{name} " \
|
|
82
|
+
"没有装,或它的运行时依赖缺失"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# libui → Libui;gtk → Gtk;windows_font → WindowsFont
|
|
86
|
+
def camelize(name)
|
|
87
|
+
name.to_s.split("_").map { |part| part.capitalize }.join
|
|
88
|
+
end
|
|
89
|
+
|
|
54
90
|
# 周期定时器(设计 2.5)→ Timer 句柄,可 #stop;块在主线程执行
|
|
55
91
|
#
|
|
56
92
|
# @ticker = Citrine::Native.every(200) { self.tick }
|
|
@@ -62,10 +98,12 @@ module Citrine
|
|
|
62
98
|
|
|
63
99
|
# 起一个原生窗口应用(阻塞到窗口关闭)。
|
|
64
100
|
#
|
|
65
|
-
# Citrine::Native.run(Counter, title: "计数器", width: 400, height: 300)
|
|
101
|
+
# Citrine::Native.run(Counter, backend: :libui, title: "计数器", width: 400, height: 300)
|
|
66
102
|
#
|
|
67
103
|
# component 可以是组件类(无 prop 构造)或已构造的实例。
|
|
68
|
-
#
|
|
104
|
+
# 后端三选一:backend:(名字,按上面的约定解析)/ widgets:(现成实例)/
|
|
105
|
+
# 都不传则用 default_backend(后端包被 require 时自设)。其余关键字直接
|
|
106
|
+
# 作为窗口描述交给渲染器:title / width / height / margined。
|
|
69
107
|
#
|
|
70
108
|
# dev_mode 默认开:原生运行时没有构建管线(脚本即应用),开发期提醒
|
|
71
109
|
# (未支持的样式键/属性、未声明方向的 box)应当直接可见;显式传
|
|
@@ -74,15 +112,27 @@ module Citrine
|
|
|
74
112
|
# signals 默认 nil(不接管进程级信号):库不该悄悄覆盖宿主已有的处理器。
|
|
75
113
|
# 把运行脚本当独立进程时传 `signals: :default`,Ctrl+C / SIGTERM 就会走
|
|
76
114
|
# "退出主循环 → 有序拆解",而不是硬杀(见 App#trap_quit!)。
|
|
77
|
-
def run(component, dev_mode: true, widgets: nil, **options)
|
|
115
|
+
def run(component, dev_mode: true, widgets: nil, backend: nil, **options)
|
|
78
116
|
Citrine.dev_mode = dev_mode unless dev_mode.nil?
|
|
79
|
-
App.new(component, widgets: widgets, **options).run
|
|
117
|
+
App.new(component, widgets: pick_backend(widgets, backend), **options).run
|
|
80
118
|
end
|
|
81
119
|
|
|
82
120
|
# 只建窗口挂组件、不进主循环(测试与自管事件循环用)
|
|
83
|
-
def start(component, dev_mode: nil, widgets: nil, **options)
|
|
121
|
+
def start(component, dev_mode: nil, widgets: nil, backend: nil, **options)
|
|
84
122
|
Citrine.dev_mode = dev_mode unless dev_mode.nil?
|
|
85
|
-
App.new(component, widgets: widgets, **options).tap(&:setup)
|
|
123
|
+
App.new(component, widgets: pick_backend(widgets, backend), **options).tap(&:setup)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def pick_backend(widgets, backend)
|
|
129
|
+
return widgets if widgets
|
|
130
|
+
return resolve_backend(backend) if backend
|
|
131
|
+
return Widgets.default if default_backend
|
|
132
|
+
|
|
133
|
+
raise BackendNotSelectedError,
|
|
134
|
+
"没有选择后端:请传 backend: :libui / :gtk(需要安装对应后端 gem)," \
|
|
135
|
+
"或 widgets: 后端实例;也可以 require 后端 gem(它会自设默认后端)"
|
|
86
136
|
end
|
|
87
137
|
end
|
|
88
138
|
end
|
data/lib/citrine-native.rb
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# gem 入口(RubyGems 命名惯例:require "citrine-native"
|
|
4
|
-
|
|
3
|
+
# gem 入口(RubyGems 命名惯例:require "citrine-native")。
|
|
4
|
+
# 核心 = 后端无关:Renderer / App / StyleMatrix / Painter 协议 / Widgets 协议 +
|
|
5
|
+
# Memory 桩 + 定时器。控件实现按 backend: 选择(libui / gtk / …),
|
|
6
|
+
# 见 lib/citrine/native.rb 头部的说明。
|
|
5
7
|
require_relative "citrine/native"
|
|
8
|
+
|
|
9
|
+
# 后端 gem(citrine-native-libui / citrine-native-gtk / …)各自提供:
|
|
10
|
+
# Citrine::Native.register_backend(:名字, widgets: "Citrine::Native::Widgets::类名")
|
|
11
|
+
# Citrine::Native.default_backend = :名字 # 可选:require 即默认
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: citrine-native
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ShiningRay
|
|
@@ -23,20 +23,6 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.2'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: libui
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0.1'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0.1'
|
|
40
26
|
- !ruby/object:Gem::Dependency
|
|
41
27
|
name: base64
|
|
42
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,38 +37,10 @@ dependencies:
|
|
|
51
37
|
- - ">="
|
|
52
38
|
- !ruby/object:Gem::Version
|
|
53
39
|
version: '0.2'
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '5.0'
|
|
61
|
-
type: :development
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '5.0'
|
|
68
|
-
- !ruby/object:Gem::Dependency
|
|
69
|
-
name: rake
|
|
70
|
-
requirement: !ruby/object:Gem::Requirement
|
|
71
|
-
requirements:
|
|
72
|
-
- - "~>"
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
version: '13.0'
|
|
75
|
-
type: :development
|
|
76
|
-
prerelease: false
|
|
77
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
-
requirements:
|
|
79
|
-
- - "~>"
|
|
80
|
-
- !ruby/object:Gem::Version
|
|
81
|
-
version: '13.0'
|
|
82
|
-
description: citrine-native:Citrine 组件的 CRuby 原生运行时。不经 Opal/JS,同一份信号式组件代码直接用 ruby
|
|
83
|
-
启动为桌面窗口应用(Shoes 精神)。实现 Citrine::Renderer 协议,首版后端为 libui(原生控件)。
|
|
40
|
+
description: 'citrine-native:Citrine 组件的 CRuby 原生运行时**核心**(后端无关)。Renderer / App /
|
|
41
|
+
StyleMatrix / Painter 协议 / Widgets 协议 + Memory 桩。控件实现按 backend: 选择:citrine-native-libui(libui)、citrine-native-gtk(GTK3)、以及未来的其它后端。'
|
|
84
42
|
email:
|
|
85
|
-
-
|
|
43
|
+
- tsowly@hotmail.com
|
|
86
44
|
executables: []
|
|
87
45
|
extensions: []
|
|
88
46
|
extra_rdoc_files: []
|
|
@@ -102,7 +60,6 @@ files:
|
|
|
102
60
|
- lib/citrine/native/timer.rb
|
|
103
61
|
- lib/citrine/native/version.rb
|
|
104
62
|
- lib/citrine/native/widgets.rb
|
|
105
|
-
- lib/citrine/native/widgets/libui.rb
|
|
106
63
|
- lib/citrine/native/widgets/memory.rb
|
|
107
64
|
homepage: https://github.com/ShiningRay/citrine-native
|
|
108
65
|
licenses:
|
|
@@ -117,7 +74,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
117
74
|
requirements:
|
|
118
75
|
- - ">="
|
|
119
76
|
- !ruby/object:Gem::Version
|
|
120
|
-
version: 3.1
|
|
77
|
+
version: '3.1'
|
|
121
78
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
79
|
requirements:
|
|
123
80
|
- - ">="
|
|
@@ -126,5 +83,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
126
83
|
requirements: []
|
|
127
84
|
rubygems_version: 4.0.20
|
|
128
85
|
specification_version: 4
|
|
129
|
-
summary:
|
|
86
|
+
summary: Backend-agnostic native runtime core for Citrine components
|
|
130
87
|
test_files: []
|