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.
@@ -2,20 +2,20 @@
2
2
 
3
3
  module Citrine
4
4
  module Native
5
- # 自绘面板的绘制薄层(冻结接口见 docs/design/native-area.md 2.2)。
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 一个 Painter 实例,不跨帧复用**。
13
- # 跨帧复用的只有文本布局(TextCache):它由适配层按面板持有、随面板销毁释放——
14
- # libui 的 text layout / attributed string / font descriptor 不归 Ruby GC 管,
15
- # 漏 free 就是每帧漏一段 C 内存(每帧每格新建 layout 还会明显掉帧)。
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
- # Painter 每帧新建(见上),自己 warn 会每帧刷屏;这里只按 key 收集,
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
- # libui 的约定:宽度为负 = 不换行(uiDrawNewTextLayout 里 Width < 0 → CGFLOAT_MAX)
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
- # 块内裁剪(libui save/clip/restore):块里照常画,矩形外的部分被裁掉
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
- "(libui 的对齐是在给定宽度内对齐),本次按左对齐绘制")
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
- # 两个实现:Painter(libui 调用)/ Recording(记录调用序列)。
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
- # 一条文本布局缓存项:绘制对象 + 度量结果 + 释放所需的 libui 对象。
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
- # 与真 Painter 共用同一份 Primitives:归一后的参数进 @calls。
294
+ # 与各后端的真实现共用同一份 Primitives:归一后的参数进 @calls。
542
295
  #
543
- # 度量是**桩**:没有 libui 就没有真字体度量,measure_text 按字符数粗估
296
+ # 度量是**桩**:没有平台字体度量,measure_text 按字符数粗估
544
297
  # (CJK 按两个字符宽),只保证"量出来是正数、随字号变大"这类弱断言。
545
- # 需要真度量的测试跑真控件冒烟(test/support/libui_scenario.rb)。
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
@@ -4,6 +4,6 @@ module Citrine
4
4
  module Native
5
5
  # 首个发布版本(N5):N0–N4 全部落地后定版。
6
6
  # 发布流程见 CHANGELOG.md 与 .github/workflows/release.yml(Trusted Publishing)。
7
- VERSION = "0.1.0"
7
+ VERSION = "0.2.0"
8
8
  end
9
9
  end
@@ -17,14 +17,18 @@ module Citrine
17
17
  # ——五个回调槽必须在创建时全部装上
18
18
  module Widgets
19
19
  class << self
20
- # 默认后端:libui(决策 N-1)。测试注入 Memory 后端:
20
+ # 默认后端:核心包不带控件实现,解析交给 Citrine::Native.default_backend
21
+ # (后端包被 require 时自设,如 -libui 设 :libui)。测试注入 Memory 后端:
21
22
  # Renderer.new(widgets: Widgets::Memory.new)
22
23
  def default
23
- require_relative "widgets/libui"
24
- Libui.new
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 单测用;不加载 libui)
31
+ # 内存打桩后端(CRuby 单测用;不加载任何 UI 工具包)
28
32
  def memory
29
33
  Memory.new
30
34
  end
@@ -1,23 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Citrine::Native Citrine 组件的 CRuby 原生运行时(Shoes 式:ruby app.rb 直接起窗口)。
3
+ # Citrine::Native —— Citrine 组件的 CRuby 原生运行时**核心**(后端无关)。
4
4
  #
5
- # 设计与计划见 GOALS.md。本 gem 实现 `Citrine::Renderer` 的平台钩子(renderer.rb),
6
- # 把节点树翻译成原生控件树;只依赖 citrine 的平台无关核心,**不引入 Opal**。
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
- # class Counter < Citrine::Component
11
- # state :count, 0
12
- # def view
13
- # stack(gap: 8) do
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
- # Citrine::Native.run(Counter, title: "计数器", width: 400, height: 300)
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
- # 其余关键字直接作为窗口描述交给渲染器:title / width / height / margined。
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
@@ -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.1.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
- - !ruby/object:Gem::Dependency
55
- name: minitest
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
- - shiningray@users.noreply.github.com
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.0
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: CRuby native runtime for Citrine components (Shoes-style desktop apps)
86
+ summary: Backend-agnostic native runtime core for Citrine components
130
87
  test_files: []