zaniah 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.
Files changed (166) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +70 -0
  5. data/assets/fonts/Abel-Regular.ttf +0 -0
  6. data/assets/fonts/OFL.txt +93 -0
  7. data/docs/adr/000-template.md +24 -0
  8. data/docs/adr/001-native-gpu-backends.md +36 -0
  9. data/docs/adr/002-objective-c-aggregates.md +34 -0
  10. data/docs/adr/003-fiddle-call-boundary.md +33 -0
  11. data/docs/adr/README.md +10 -0
  12. data/docs/native.md +66 -0
  13. data/docs/process_pool.md +67 -0
  14. data/docs/text.md +90 -0
  15. data/docs/vector_and_list.md +52 -0
  16. data/examples/ffi_bench.rb +16 -0
  17. data/examples/linux_smoke.rb +53 -0
  18. data/examples/mac_event_check.rb +61 -0
  19. data/examples/mac_poll_bench.rb +24 -0
  20. data/examples/metal_state_check.rb +72 -0
  21. data/examples/native_bench.rb +18 -0
  22. data/examples/native_drop.rb +82 -0
  23. data/examples/native_font.rb +15 -0
  24. data/examples/native_ime.rb +50 -0
  25. data/examples/native_smoke.rb +75 -0
  26. data/examples/native_text_bench.rb +83 -0
  27. data/examples/native_watch.rb +25 -0
  28. data/examples/vulkan_triangle.rb +20 -0
  29. data/examples/word_count_worker.rb +8 -0
  30. data/lib/zaniah/anchored.rb +10 -0
  31. data/lib/zaniah/app.rb +130 -0
  32. data/lib/zaniah/bounds.rb +22 -0
  33. data/lib/zaniah/canvas.rb +15 -0
  34. data/lib/zaniah/color.rb +35 -0
  35. data/lib/zaniah/configuration.rb +5 -0
  36. data/lib/zaniah/corners.rb +7 -0
  37. data/lib/zaniah/data_compat.rb +25 -0
  38. data/lib/zaniah/device_pixels.rb +7 -0
  39. data/lib/zaniah/div.rb +5 -0
  40. data/lib/zaniah/edges.rb +7 -0
  41. data/lib/zaniah/element.rb +106 -0
  42. data/lib/zaniah/entity.rb +9 -0
  43. data/lib/zaniah/entity_context.rb +17 -0
  44. data/lib/zaniah/error.rb +5 -0
  45. data/lib/zaniah/ffi/com.rb +24 -0
  46. data/lib/zaniah/ffi/library.rb +29 -0
  47. data/lib/zaniah/ffi/objc.rb +86 -0
  48. data/lib/zaniah/ffi/struct/signature.rb +46 -0
  49. data/lib/zaniah/ffi/struct.rb +61 -0
  50. data/lib/zaniah/ffi/wayland.rb +140 -0
  51. data/lib/zaniah/frame_context.rb +14 -0
  52. data/lib/zaniah/geometry.rb +16 -0
  53. data/lib/zaniah/gpu/buffer.rb +23 -0
  54. data/lib/zaniah/gpu/frame_encoder.rb +43 -0
  55. data/lib/zaniah/gpu/instance_packing.rb +53 -0
  56. data/lib/zaniah/gpu/metal.rb +209 -0
  57. data/lib/zaniah/gpu/open_gl.rb +210 -0
  58. data/lib/zaniah/gpu/pipeline.rb +7 -0
  59. data/lib/zaniah/gpu/software.rb +126 -0
  60. data/lib/zaniah/gpu/texture.rb +33 -0
  61. data/lib/zaniah/gpu/vulkan.rb +233 -0
  62. data/lib/zaniah/gpu.rb +18 -0
  63. data/lib/zaniah/hsla.rb +14 -0
  64. data/lib/zaniah/image.rb +19 -0
  65. data/lib/zaniah/input/action_registry.rb +12 -0
  66. data/lib/zaniah/input/composition.rb +7 -0
  67. data/lib/zaniah/input/context_predicate.rb +87 -0
  68. data/lib/zaniah/input/dispatcher.rb +59 -0
  69. data/lib/zaniah/input/file_drop.rb +7 -0
  70. data/lib/zaniah/input/focus_handle.rb +24 -0
  71. data/lib/zaniah/input/key_down.rb +7 -0
  72. data/lib/zaniah/input/key_up.rb +7 -0
  73. data/lib/zaniah/input/keymap/binding.rb +9 -0
  74. data/lib/zaniah/input/keymap.rb +55 -0
  75. data/lib/zaniah/input/keystroke.rb +20 -0
  76. data/lib/zaniah/input/mouse_down.rb +7 -0
  77. data/lib/zaniah/input/mouse_move.rb +7 -0
  78. data/lib/zaniah/input/mouse_up.rb +7 -0
  79. data/lib/zaniah/input/scroll_wheel.rb +7 -0
  80. data/lib/zaniah/input/text_input.rb +7 -0
  81. data/lib/zaniah/input.rb +17 -0
  82. data/lib/zaniah/layout/available_space.rb +11 -0
  83. data/lib/zaniah/layout/engine.rb +214 -0
  84. data/lib/zaniah/layout/node.rb +36 -0
  85. data/lib/zaniah/layout/style.rb +19 -0
  86. data/lib/zaniah/layout/tree.rb +16 -0
  87. data/lib/zaniah/layout.rb +11 -0
  88. data/lib/zaniah/length.rb +14 -0
  89. data/lib/zaniah/length_units.rb +9 -0
  90. data/lib/zaniah/list/height_index.rb +68 -0
  91. data/lib/zaniah/list.rb +101 -0
  92. data/lib/zaniah/overlay.rb +10 -0
  93. data/lib/zaniah/platform/display.rb +7 -0
  94. data/lib/zaniah/platform/file_event.rb +7 -0
  95. data/lib/zaniah/platform/headless/window.rb +185 -0
  96. data/lib/zaniah/platform/linux/appearance_aware.rb +60 -0
  97. data/lib/zaniah/platform/linux/displays.rb +55 -0
  98. data/lib/zaniah/platform/linux/free_type.rb +80 -0
  99. data/lib/zaniah/platform/linux/watcher.rb +70 -0
  100. data/lib/zaniah/platform/linux/wayland_display_discovery.rb +60 -0
  101. data/lib/zaniah/platform/linux/wayland_window.rb +369 -0
  102. data/lib/zaniah/platform/linux/window.rb +418 -0
  103. data/lib/zaniah/platform/linux.rb +3 -0
  104. data/lib/zaniah/platform/mac/app.rb +67 -0
  105. data/lib/zaniah/platform/mac/core_text.rb +94 -0
  106. data/lib/zaniah/platform/mac/displays.rb +23 -0
  107. data/lib/zaniah/platform/mac/watcher.rb +63 -0
  108. data/lib/zaniah/platform/mac/window.rb +298 -0
  109. data/lib/zaniah/platform/mac.rb +19 -0
  110. data/lib/zaniah/platform/tui/input_decoder.rb +145 -0
  111. data/lib/zaniah/platform/tui/text_renderer.rb +40 -0
  112. data/lib/zaniah/platform/tui/window.rb +91 -0
  113. data/lib/zaniah/platform/windows/terminal.rb +127 -0
  114. data/lib/zaniah/platform/windows/watcher.rb +91 -0
  115. data/lib/zaniah/platform/windows/window.rb +407 -0
  116. data/lib/zaniah/platform/windows.rb +3 -0
  117. data/lib/zaniah/platform.rb +34 -0
  118. data/lib/zaniah/png.rb +118 -0
  119. data/lib/zaniah/point.rb +8 -0
  120. data/lib/zaniah/process_pool/worker.rb +7 -0
  121. data/lib/zaniah/process_pool.rb +239 -0
  122. data/lib/zaniah/process_wire.rb +127 -0
  123. data/lib/zaniah/quad_packer.rb +15 -0
  124. data/lib/zaniah/scene/sprite_batch.rb +7 -0
  125. data/lib/zaniah/scene.rb +167 -0
  126. data/lib/zaniah/scene_renderer.rb +8 -0
  127. data/lib/zaniah/size.rb +5 -0
  128. data/lib/zaniah/subscription.rb +14 -0
  129. data/lib/zaniah/svg/path.rb +134 -0
  130. data/lib/zaniah/svg/rasterizable.rb +186 -0
  131. data/lib/zaniah/svg.rb +295 -0
  132. data/lib/zaniah/task/cancelled.rb +7 -0
  133. data/lib/zaniah/task/timeout.rb +7 -0
  134. data/lib/zaniah/task.rb +51 -0
  135. data/lib/zaniah/task_executor.rb +105 -0
  136. data/lib/zaniah/text.rb +24 -0
  137. data/lib/zaniah/text_system/atlas/entry.rb +9 -0
  138. data/lib/zaniah/text_system/atlas.rb +102 -0
  139. data/lib/zaniah/text_system/atlas_cache.rb +146 -0
  140. data/lib/zaniah/text_system/font_db/face.rb +9 -0
  141. data/lib/zaniah/text_system/font_db.rb +193 -0
  142. data/lib/zaniah/text_system/glyph.rb +7 -0
  143. data/lib/zaniah/text_system/line_layout.rb +20 -0
  144. data/lib/zaniah/text_system/renderer.rb +196 -0
  145. data/lib/zaniah/text_system/shaper.rb +430 -0
  146. data/lib/zaniah/text_system/typesetter/copy_error.rb +9 -0
  147. data/lib/zaniah/text_system/typesetter.rb +168 -0
  148. data/lib/zaniah/text_system.rb +9 -0
  149. data/lib/zaniah/unicode.rb +13 -0
  150. data/lib/zaniah/uniform_list.rb +30 -0
  151. data/lib/zaniah/version.rb +5 -0
  152. data/lib/zaniah/view.rb +13 -0
  153. data/lib/zaniah.rb +29 -0
  154. data/sig/app.rbs +80 -0
  155. data/sig/elements.rbs +129 -0
  156. data/sig/gpu.rbs +108 -0
  157. data/sig/input.rbs +114 -0
  158. data/sig/layout.rbs +53 -0
  159. data/sig/native.rbs +88 -0
  160. data/sig/platform.rbs +94 -0
  161. data/sig/scene.rbs +52 -0
  162. data/sig/shaper.rbs +8 -0
  163. data/sig/text.rbs +137 -0
  164. data/sig/zaniah.rbs +120 -0
  165. data/tools/check_dependencies.rb +52 -0
  166. metadata +249 -0
data/lib/zaniah/app.rb ADDED
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "subscription"
4
+ require_relative "entity"
5
+ require_relative "entity_context"
6
+ require_relative "task_executor"
7
+
8
+ module Zaniah
9
+ class App
10
+ attr_reader :windows, :executor
11
+
12
+ def initialize
13
+ @slots, @generations, @free, @windows, @globals = [], [], [], [], {}
14
+ @listeners, @effects, @updating, @flushing = {}, [], 0, false
15
+ @executor = TaskExecutor.new
16
+ end
17
+
18
+ def new_entity
19
+ id = @free.pop || @slots.length
20
+ @generations[id] ||= 0
21
+ entity = Entity.new(id, @generations[id])
22
+ @slots[id] = yield(EntityContext.new(self, entity))
23
+ entity
24
+ rescue Exception
25
+ @slots[id] = nil if id
26
+ @free << id if id && !@free.include?(id)
27
+ raise
28
+ end
29
+
30
+ def read(entity)
31
+ validate(entity)
32
+ @slots[entity.id]
33
+ end
34
+
35
+ def update(entity)
36
+ validate(entity)
37
+ @updating += 1
38
+ begin
39
+ yield(@slots[entity.id], EntityContext.new(self, entity))
40
+ ensure
41
+ @updating -= 1
42
+ flush_effects if @updating.zero?
43
+ end
44
+ end
45
+
46
+ def release(entity)
47
+ validate(entity)
48
+ object = @slots[entity.id]
49
+ @slots[entity.id] = nil
50
+ @generations[entity.id] += 1
51
+ @free << entity.id
52
+ @listeners.delete_if { |(target, _), _| target == entity }
53
+ @listeners.each_value { |listeners| listeners.delete_if { |owner, _| owner == entity } }
54
+ object.dispose if object.respond_to?(:dispose)
55
+ end
56
+
57
+ def entity_count = @slots.length - @free.length
58
+
59
+ def global(key, value = (missing = true))
60
+ missing ? @globals.fetch(key) : @globals[key] = value
61
+ end
62
+
63
+ def listen(kind, target, owner: nil, &callback)
64
+ validate(target)
65
+ entry = [owner, callback]
66
+ list = (@listeners[[target, kind]] ||= [])
67
+ list << entry
68
+ Subscription.new { list.delete(entry) }
69
+ end
70
+
71
+ def enqueue(kind, entity, event = nil)
72
+ @effects << [kind, entity, event]
73
+ flush_effects if @updating.zero? && !@flushing
74
+ end
75
+
76
+ def flush_effects
77
+ return if @flushing
78
+ @flushing = true
79
+ dirty = false
80
+ until @effects.empty?
81
+ effects, @effects = @effects, []
82
+ notified = {}
83
+ effects.each do |kind, entity, event|
84
+ next unless alive?(entity)
85
+ next if kind == :notify && notified[entity]
86
+ notified[entity] = true if kind == :notify
87
+ dirty ||= kind == :notify
88
+ (@listeners[[entity, kind]] || []).dup.each do |owner, callback|
89
+ next if owner && !alive?(owner)
90
+ if owner
91
+ args = [read(owner), read(entity)]
92
+ args << event if kind == :emit
93
+ callback.call(*args, EntityContext.new(self, owner))
94
+ else
95
+ callback.call(read(entity), event)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ @windows.each(&:request_frame) if dirty
101
+ ensure
102
+ @flushing = false
103
+ end
104
+
105
+ def open_window(**options, &render)
106
+ window = Platform.open_window(**options)
107
+ @windows << window
108
+ window.draw(&render) if render
109
+ window
110
+ end
111
+
112
+ def run
113
+ until @windows.empty? || @windows.all?(&:closed?)
114
+ @executor.drain
115
+ @windows.reject(&:closed?).each(&:tick)
116
+ @executor.wait(0.05) unless @windows.any?(&:dirty?)
117
+ end
118
+ ensure
119
+ @executor.shutdown
120
+ end
121
+
122
+ def alive?(entity) = @generations[entity.id] == entity.generation && !@free.include?(entity.id) && entity.id < @slots.length
123
+
124
+ private
125
+
126
+ def validate(entity)
127
+ raise Error, "entity has been released" unless alive?(entity)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ Bounds = Data.define(:x, :y, :width, :height) do
5
+ def right = x + width
6
+ def bottom = y + height
7
+ def contains?(point) = point.x >= x && point.y >= y && point.x < right && point.y < bottom
8
+
9
+ def intersect(other)
10
+ left, top = [x, other.x].max, [y, other.y].max
11
+ Bounds.new(left, top, [[right, other.right].min - left, 0].max,
12
+ [[bottom, other.bottom].min - top, 0].max)
13
+ end
14
+
15
+ def inset(edges)
16
+ edges = Edges.all(edges) if edges.is_a?(Numeric)
17
+ Bounds.new(x + edges.left, y + edges.top,
18
+ [width - edges.left - edges.right, 0].max,
19
+ [height - edges.top - edges.bottom, 0].max)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ class Canvas < Element
5
+ def initialize(&paint)
6
+ super()
7
+ @painter = paint
8
+ end
9
+
10
+ def paint(bounds, state, prepaint, cx)
11
+ super
12
+ @painter.call(bounds, cx)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ Color = Data.define(:r, :g, :b, :a) do
5
+ def self.parse(value)
6
+ return value if value.is_a?(self)
7
+ return new(*value, *(value.length == 3 ? [1.0] : [])) if value.is_a?(Array)
8
+ hex = value.to_s.delete_prefix("#")
9
+ hex = hex.chars.map { |character| character * 2 }.join if [3, 4].include?(hex.length)
10
+ raise ArgumentError, "expected #RGB, #RGBA, #RRGGBB or #RRGGBBAA" unless hex.match?(/\A[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?\z/)
11
+ channels = hex.scan(/../).map { |channel| channel.to_i(16) / 255.0 }
12
+ channels << 1.0 if channels.length == 3
13
+ new(*channels)
14
+ end
15
+
16
+ def opacity(value) = Color.new(r, g, b, a * value)
17
+ def premultiplied = [r * a, g * a, b * a, a]
18
+ def to_a = [r, g, b, a]
19
+
20
+ def mix(other, amount)
21
+ Color.new(*to_a.zip(Color.parse(other).to_a).map { |x, y| x + (y - x) * amount })
22
+ end
23
+
24
+ def to_hsla
25
+ max, min = [r, g, b].max, [r, g, b].min
26
+ delta, lightness = max - min, (max + min) / 2.0
27
+ return HSLA.new(0, 0, lightness, a) if delta.zero?
28
+ hue = if max == r then ((g - b) / delta) % 6
29
+ elsif max == g then (b - r) / delta + 2
30
+ else (r - g) / delta + 4
31
+ end
32
+ HSLA.new(hue / 6.0, delta / (1 - (2 * lightness - 1).abs), lightness, a)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ Configuration = Struct.new(:font_raster, :shaper, :font_db, :segmenter, keyword_init: true)
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ Corners = Data.define(:top_left, :top_right, :bottom_right, :bottom_left) do
5
+ def self.all(value) = new(value, value, value, value)
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby 3.2 added Data; Struct provides the same record operations used here.
4
+ unless defined?(Data)
5
+ Data = Struct
6
+ def Data.define(*members, &block)
7
+ Struct.new(*members) do
8
+ members.each { |member| undef_method :"#{member}=" }
9
+ define_method(:initialize) do |*values, **keywords|
10
+ if keywords.any?
11
+ raise ArgumentError, "expected either positional or keyword members" unless values.empty? && keywords.keys.sort == members.sort
12
+ values = members.map { |member| keywords.fetch(member) }
13
+ end
14
+ raise ArgumentError, "wrong number of members" unless values.length == members.length
15
+ super(*values)
16
+ freeze
17
+ end
18
+ define_method(:with) do |**changes|
19
+ changes.empty? ? self : self.class.new(**to_h.merge(changes))
20
+ end
21
+ class_eval(&block) if block
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ DevicePixels = Data.define(:value) do
5
+ def logical(scale) = value / Float(scale)
6
+ end
7
+ end
data/lib/zaniah/div.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ class Div < Element; end
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ Edges = Data.define(:top, :right, :bottom, :left) do
5
+ def self.all(value) = new(value, value, value, value)
6
+ end
7
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ class Element
5
+ include LengthUnits
6
+
7
+ attr_reader :layout_node
8
+
9
+ def initialize
10
+ @style, @children, @handlers = Layout::Style.new, [], {}
11
+ @background, @border_color, @radius = "#0000", "#0000", 0
12
+ end
13
+
14
+ def child(element)
15
+ @children << element if element
16
+ self
17
+ end
18
+
19
+ def children(elements = nil)
20
+ return @children unless elements
21
+ elements.each { |element| child(element) }
22
+ self
23
+ end
24
+
25
+ def style(**properties)
26
+ @style = @style.merge(**properties)
27
+ self
28
+ end
29
+
30
+ def key(value) = (@key = value; self)
31
+ def with_state(&initial) = (@state_initializer = initial; self)
32
+ def bg(color) = (@background = color; self)
33
+ def border_color(color) = (@border_color = color; self)
34
+ def rounded(radius) = (@radius = radius; self)
35
+ def tooltip(text) = (@tooltip = text.to_s; self)
36
+ def context_menu(items) = (@context_menu = items; self)
37
+ def flex = style(display: :flex)
38
+ def flex_row = style(flex_direction: :row)
39
+ def flex_col = style(flex_direction: :column)
40
+ def items_center = style(align_items: :center)
41
+ def justify_center = style(justify_content: :center)
42
+ def w_full = style(width: percent(100))
43
+ def h_full = style(height: percent(100))
44
+ def flex_1 = style(flex_grow: 1, flex_basis: 0)
45
+ def overflow_hidden = style(overflow: :hidden)
46
+
47
+ {w: :width, h: :height, min_w: :min_width, min_h: :min_height,
48
+ max_w: :max_width, max_h: :max_height, p: :padding, m: :margin,
49
+ gap: :gap, border: :border, border_b: :border_bottom}.each do |method, property|
50
+ define_method(method) { |value| style(**{property => value}) }
51
+ end
52
+
53
+ %i[click hover drag scroll_wheel mouse_down mouse_up].each do |kind|
54
+ define_method("on_#{kind}") { |&block| @handlers[kind] = block; self }
55
+ end
56
+
57
+ def request_layout(cx)
58
+ @state = cx.state(@key, &@state_initializer) if @key && @state_initializer
59
+ @layout_node = Layout::Node.new(style: @style,
60
+ children: @children.map { |child| child.request_layout(cx) })
61
+ end
62
+
63
+ def prepaint(bounds, _state, cx)
64
+ unless @handlers.empty? && !@tooltip && !@context_menu
65
+ cx.dispatcher.hit(bounds) do |event|
66
+ if event.is_a?(Input::MouseDown) && event.button == :right && @context_menu
67
+ cx.window.context_menu(@context_menu, position: event.position)
68
+ next true
69
+ end
70
+ cx.window.offer_tooltip(@tooltip, position: event.position) if @tooltip && event.is_a?(Input::MouseMove)
71
+ kind = case event
72
+ when Input::MouseDown then @handlers.key?(:mouse_down) ? :mouse_down : :click
73
+ when Input::MouseMove then @dragging && @handlers.key?(:drag) ? :drag : :hover
74
+ when Input::MouseUp then :mouse_up
75
+ when Input::ScrollWheel then :scroll_wheel
76
+ end
77
+ handler = @handlers[kind]
78
+ @dragging = true if event.is_a?(Input::MouseDown) && @handlers[:drag]
79
+ @dragging = false if event.is_a?(Input::MouseUp)
80
+ handler&.call(event, cx)
81
+ @dragging && event.is_a?(Input::MouseDown) ? :capture : !!handler || !!(@tooltip && event.is_a?(Input::MouseMove))
82
+ end
83
+ end
84
+ visit = -> { @children.each { |child| child.prepaint(child.layout_node.bounds, nil, cx) } }
85
+ @style[:overflow] == :visible ? visit.call : cx.dispatcher.clip(bounds, &visit)
86
+ end
87
+
88
+ def paint(bounds, _state, _prepaint, cx)
89
+ border = @style[:border].is_a?(Numeric) ? @style[:border] : 0
90
+ cx.scene.quad(bounds.x, bounds.y, bounds.width, bounds.height,
91
+ color: @background, radius: @radius, border_width: border, border_color: @border_color)
92
+ paint_children = -> { @children.each { |child| child.paint(child.layout_node.bounds, nil, nil, cx) unless child.layout_node.style[:display] == :none } }
93
+ @style[:overflow] == :visible ? paint_children.call : cx.scene.clip(bounds, &paint_children)
94
+ end
95
+ end
96
+ end
97
+
98
+ require_relative "frame_context"
99
+ require_relative "div"
100
+ require_relative "text"
101
+ require_relative "image"
102
+ require_relative "canvas"
103
+ require_relative "overlay"
104
+ require_relative "anchored"
105
+ require_relative "uniform_list"
106
+ require_relative "view"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ Entity = Data.define(:id, :generation) do
5
+ def read(app) = app.read(self)
6
+ def update(app, &block) = app.update(self, &block)
7
+ def release(app) = app.release(self)
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ class EntityContext
5
+ attr_reader :app, :entity
6
+
7
+ def initialize(app, entity) = (@app, @entity = app, entity)
8
+ def notify = app.enqueue(:notify, entity)
9
+ def emit(event) = app.enqueue(:emit, entity, event)
10
+ def observe(target, &callback) = app.listen(:notify, target, owner: entity, &callback)
11
+ def subscribe(target, &callback) = app.listen(:emit, target, owner: entity, &callback)
12
+ def spawn(&block) = app.executor.spawn { block.call(self) }
13
+ def background(&block) = app.executor.background(&block).await
14
+ def update(&block) = app.executor.post { block.call(app) }
15
+ def theme = app.global(:theme)
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "library"
4
+
5
+ module Zaniah
6
+ module FFI
7
+ module COM
8
+ extend self
9
+ def vcall(this, index, types, result, *values)
10
+ raise ArgumentError, "null COM interface" if this.to_i.zero?
11
+ table = Fiddle::Pointer.new(this.to_i)[0, Fiddle::SIZEOF_VOIDP].unpack1("J")
12
+ address = Fiddle::Pointer.new(table)[index * Fiddle::SIZEOF_VOIDP, Fiddle::SIZEOF_VOIDP].unpack1("J")
13
+ @functions ||= {}
14
+ function = @functions[[address, types, result]] ||= Fiddle::Function.new(address, [Fiddle::TYPE_VOIDP, *types], result)
15
+ function.call(this, *values)
16
+ end
17
+ def release(this) = vcall(this, 2, [], Fiddle::TYPE_INT)
18
+ def check(status)
19
+ raise Zaniah::Error, "COM failure 0x#{(status & 0xffffffff).to_s(16)}" unless (status & 0x80000000).zero?
20
+ status
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle"
4
+
5
+ module Zaniah
6
+ module FFI
7
+ class Library
8
+ attr_reader :handle
9
+ def initialize(*candidates)
10
+ candidates.each do |candidate|
11
+ begin
12
+ @handle = Fiddle.dlopen(candidate)
13
+ break
14
+ rescue Fiddle::DLError
15
+ next
16
+ end
17
+ end
18
+ raise LoadError, "native library not found: #{candidates.join(', ')}" unless @handle
19
+ @functions = {}
20
+ end
21
+
22
+ def fn(name, args, result, need_gvl: true)
23
+ @functions[[name, args, result, need_gvl]] ||= Fiddle::Function.new(
24
+ @handle[name.to_s], args, result, name: name.to_s, need_gvl: need_gvl
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "struct"
4
+
5
+ module Zaniah
6
+ module FFI
7
+ module ObjC
8
+ extend self
9
+ P = Fiddle::TYPE_VOIDP
10
+ LIB = Library.new("/usr/lib/libobjc.A.dylib")
11
+ MESSAGE_SEND = LIB.handle["objc_msgSend"]
12
+ SCALARS = {void: Fiddle::TYPE_VOID, pointer: P, int: Fiddle::TYPE_INT, uint: Fiddle::TYPE_UINT,
13
+ long: Fiddle::TYPE_INT64_T, ulong: Fiddle::TYPE_UINT64_T, double: Fiddle::TYPE_DOUBLE,
14
+ float: Fiddle::TYPE_FLOAT, bool: Fiddle::TYPE_UCHAR}.freeze
15
+ NO_ARGUMENTS = [].freeze
16
+ @selectors, @signatures, @closures, @classes = {}, {}, [], {}
17
+ @signature_lookup = [nil, nil]
18
+
19
+ def klass(name)
20
+ @classes[name] || begin
21
+ value = LIB.fn(:objc_getClass, [P], P).call(name).to_i
22
+ @classes[name] = value unless value.zero?
23
+ value
24
+ end
25
+ end
26
+ def selector(name) = @selectors[name] ||= LIB.fn(:sel_registerName, [P], P).call(name.to_s).to_i
27
+ def send(receiver, name, *values, args: NO_ARGUMENTS, result: :pointer)
28
+ raise ArgumentError, "wrong number of native arguments" unless values.length == args.length
29
+ @signature_lookup[0], @signature_lookup[1] = args, result
30
+ signature = @signatures[@signature_lookup]
31
+ unless signature
32
+ # Fiddle already implements scalar calling conventions. Only aggregate
33
+ # arguments/returns need the lower-level libffi packing path.
34
+ signature = if SCALARS.key?(result) && args.all? { |type| SCALARS.key?(type) }
35
+ Fiddle::Function.new(MESSAGE_SEND, [P, P, *args.map { |type| SCALARS.fetch(type) }],
36
+ result == :pointer ? Fiddle::TYPE_UINTPTR_T : SCALARS.fetch(result), need_gvl: true)
37
+ else
38
+ Struct::Signature.new([:pointer, :pointer, *args], result)
39
+ end
40
+ @signatures[[args.dup.freeze, result].freeze] = signature
41
+ end
42
+ return signature.call(receiver, selector(name), *values) if signature.is_a?(Fiddle::Function)
43
+ address = MESSAGE_SEND
44
+ if RUBY_PLATFORM.include?("x86_64") && [:rect, :region, :clear_color].include?(result)
45
+ address = LIB.handle["objc_msgSend_stret"]
46
+ end
47
+ signature.call(address, receiver, selector(name), *values)
48
+ end
49
+ def string(value)
50
+ bytes = value.to_s.encode(Encoding::UTF_8)
51
+ send(klass("NSString"), "stringWithUTF8String:", Fiddle::Pointer[bytes + "\0"], args: [:pointer])
52
+ end
53
+ def text(object)
54
+ return "" if object.to_i.zero?
55
+ pointer = send(object, "UTF8String")
56
+ pointer.zero? ? "" : Fiddle::Pointer.new(pointer).to_s.force_encoding(Encoding::UTF_8)
57
+ end
58
+ def alloc(name) = send(klass(name), "alloc")
59
+ def new(name) = send(alloc(name), "init")
60
+ def release(object)
61
+ send(object, "release", result: :void) unless object.to_i.zero?
62
+ end
63
+
64
+ def subclass(name, parent, protocols: [])
65
+ found = klass(name)
66
+ return found unless found.zero?
67
+ klass = LIB.fn(:objc_allocateClassPair, [P, P, Fiddle::TYPE_SIZE_T], P).call(self.klass(parent), name, 0).to_i
68
+ protocols.each do |protocol|
69
+ ptr = LIB.fn(:objc_getProtocol, [P], P).call(protocol)
70
+ LIB.fn(:class_addProtocol, [P, P], Fiddle::TYPE_BOOL).call(klass, ptr) unless ptr.null?
71
+ end
72
+ yield klass
73
+ LIB.fn(:objc_registerClassPair, [P], Fiddle::TYPE_VOID).call(klass)
74
+ klass
75
+ end
76
+
77
+ def method(klass, name, args: [], result: :void, encoding:, &block)
78
+ signature = Struct::Signature.new([:pointer, :pointer, *args], result)
79
+ closure = signature.closure(&block)
80
+ @closures << signature
81
+ ok = LIB.fn(:class_addMethod, [P, P, P, P], Fiddle::TYPE_BOOL).call(klass, selector(name), closure, encoding)
82
+ raise LoadError, "cannot install #{name}" unless ok
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ module FFI
5
+ module Struct
6
+ class Signature
7
+ def initialize(arguments, result)
8
+ @arguments, @result = arguments, result
9
+ @types = arguments.map { |type| Struct.type(type).to_i }.pack("J*")
10
+ @cif = Fiddle::Pointer.malloc(256, Fiddle::RUBY_FREE)
11
+ status = LIB.fn(:ffi_prep_cif, [P, I, I, P, P], I).call(@cif, ABI, arguments.length, Struct.type(result), @types)
12
+ raise LoadError, "libffi rejected signature (#{status})" unless status.zero?
13
+ end
14
+
15
+ def call(address, *arguments)
16
+ raise ArgumentError, "wrong number of native arguments" unless arguments.length == @arguments.length
17
+ values = @arguments.zip(arguments).map { |type, value| Struct.pack(type, value) }
18
+ pointers = values.map { |value| Fiddle::Pointer[value].to_i }.pack("J*")
19
+ result = Fiddle::Pointer.malloc(64, Fiddle::RUBY_FREE)
20
+ LIB.fn(:ffi_call, [P, P, P, P], V).call(@cif, address, result, pointers)
21
+ Struct.unpack(@result, result)
22
+ end
23
+
24
+ def closure(&block)
25
+ location = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, Fiddle::RUBY_FREE)
26
+ memory = LIB.fn(:ffi_closure_alloc, [Fiddle::TYPE_SIZE_T, P], P).call(256, location)
27
+ raise NoMemoryError, "ffi_closure_alloc" if memory.null?
28
+ callback = Fiddle::Closure::BlockCaller.new(V, [P, P, P, P]) do |_cif, result, args, _user|
29
+ arguments = @arguments.each_with_index.map do |type, index|
30
+ address = Fiddle::Pointer.new(args)[index * Fiddle::SIZEOF_VOIDP, Fiddle::SIZEOF_VOIDP].unpack1("J")
31
+ Struct.unpack(type, Fiddle::Pointer.new(address))
32
+ end
33
+ bytes = Struct.pack(@result, block.call(*arguments))
34
+ Fiddle::Pointer.new(result)[0, bytes.bytesize] = bytes unless bytes.empty?
35
+ end
36
+ code = location[0, Fiddle::SIZEOF_VOIDP].unpack1("J")
37
+ status = LIB.fn(:ffi_prep_closure_loc, [P, P, P, P, P], I).call(memory, @cif, callback, 0, code)
38
+ raise LoadError, "libffi rejected closure (#{status})" unless status.zero?
39
+ # Objective-C classes outlive Ruby wrappers: retain their IMPs for process lifetime.
40
+ (@closures ||= []) << [memory, callback]
41
+ code
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "library"
4
+
5
+ module Zaniah
6
+ module FFI
7
+ # Fiddle's public Function API has scalar types only. libffi itself handles
8
+ # aggregate arguments/returns, including arm64 homogeneous float aggregates.
9
+ module Struct
10
+ extend self
11
+ P = Fiddle::TYPE_VOIDP
12
+ I = Fiddle::TYPE_INT
13
+ V = Fiddle::TYPE_VOID
14
+ LIB = Library.new("/usr/lib/libffi.dylib", "libffi.so.8", "libffi.so.7", "libffi-8.dll")
15
+ ABI = if RUBY_PLATFORM.include?("aarch64") || RUBY_PLATFORM.include?("arm64") then 1
16
+ elsif RUBY_PLATFORM.match?(/mingw|mswin/) then 2
17
+ else 2
18
+ end
19
+ TYPES = {void: ["void", nil], pointer: ["pointer", "J"], int: ["sint32", "i!"],
20
+ uint: ["uint32", "I!"], long: ["sint64", "q"], ulong: ["uint64", "Q"],
21
+ double: ["double", "d"], float: ["float", "f"], bool: ["uint8", "C"]}.freeze
22
+ AGGREGATES = {point: [:double, :double], size: [:double, :double],
23
+ rect: [:double, :double, :double, :double], range: [:ulong, :ulong],
24
+ region: [:ulong] * 6, scissor: [:ulong] * 4, clear_color: [:double] * 4}.freeze
25
+
26
+ def type(name)
27
+ @types ||= {}
28
+ @types[name] ||= if TYPES.key?(name)
29
+ Fiddle::Pointer.new(LIB.handle["ffi_type_#{TYPES.fetch(name)[0]}"])
30
+ else
31
+ elements = (AGGREGATES.fetch(name).map { |t| type(t).to_i } << 0).pack("J*")
32
+ pointer = Fiddle::Pointer.malloc(24, Fiddle::RUBY_FREE)
33
+ pointer[0, 24] = [0, 0, 13, Fiddle::Pointer[elements].to_i].pack("JSSx4J")
34
+ (@keep ||= []) << elements
35
+ pointer
36
+ end
37
+ end
38
+
39
+ def pack(name, value)
40
+ return value.pack(AGGREGATES.fetch(name).map { |t| TYPES.fetch(t)[1] }.join) if AGGREGATES.key?(name)
41
+ format = TYPES.fetch(name)[1]
42
+ return "" unless format
43
+ [name == :pointer ? (value.respond_to?(:to_ptr) ? value.to_ptr.to_i : value.to_i) : value].pack(format)
44
+ end
45
+
46
+ def unpack(name, pointer)
47
+ return nil if name == :void
48
+ if AGGREGATES.key?(name)
49
+ format = AGGREGATES.fetch(name).map { |t| TYPES.fetch(t)[1] }.join
50
+ pointer[0, pack(name, Array.new(AGGREGATES[name].length, 0)).bytesize].unpack(format)
51
+ else
52
+ format = TYPES.fetch(name)[1]
53
+ pointer[0, [0].pack(format).bytesize].unpack1(format)
54
+ end
55
+ end
56
+
57
+ end
58
+ end
59
+ end
60
+
61
+ require_relative "struct/signature"