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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +70 -0
- data/assets/fonts/Abel-Regular.ttf +0 -0
- data/assets/fonts/OFL.txt +93 -0
- data/docs/adr/000-template.md +24 -0
- data/docs/adr/001-native-gpu-backends.md +36 -0
- data/docs/adr/002-objective-c-aggregates.md +34 -0
- data/docs/adr/003-fiddle-call-boundary.md +33 -0
- data/docs/adr/README.md +10 -0
- data/docs/native.md +66 -0
- data/docs/process_pool.md +67 -0
- data/docs/text.md +90 -0
- data/docs/vector_and_list.md +52 -0
- data/examples/ffi_bench.rb +16 -0
- data/examples/linux_smoke.rb +53 -0
- data/examples/mac_event_check.rb +61 -0
- data/examples/mac_poll_bench.rb +24 -0
- data/examples/metal_state_check.rb +72 -0
- data/examples/native_bench.rb +18 -0
- data/examples/native_drop.rb +82 -0
- data/examples/native_font.rb +15 -0
- data/examples/native_ime.rb +50 -0
- data/examples/native_smoke.rb +75 -0
- data/examples/native_text_bench.rb +83 -0
- data/examples/native_watch.rb +25 -0
- data/examples/vulkan_triangle.rb +20 -0
- data/examples/word_count_worker.rb +8 -0
- data/lib/zaniah/anchored.rb +10 -0
- data/lib/zaniah/app.rb +130 -0
- data/lib/zaniah/bounds.rb +22 -0
- data/lib/zaniah/canvas.rb +15 -0
- data/lib/zaniah/color.rb +35 -0
- data/lib/zaniah/configuration.rb +5 -0
- data/lib/zaniah/corners.rb +7 -0
- data/lib/zaniah/data_compat.rb +25 -0
- data/lib/zaniah/device_pixels.rb +7 -0
- data/lib/zaniah/div.rb +5 -0
- data/lib/zaniah/edges.rb +7 -0
- data/lib/zaniah/element.rb +106 -0
- data/lib/zaniah/entity.rb +9 -0
- data/lib/zaniah/entity_context.rb +17 -0
- data/lib/zaniah/error.rb +5 -0
- data/lib/zaniah/ffi/com.rb +24 -0
- data/lib/zaniah/ffi/library.rb +29 -0
- data/lib/zaniah/ffi/objc.rb +86 -0
- data/lib/zaniah/ffi/struct/signature.rb +46 -0
- data/lib/zaniah/ffi/struct.rb +61 -0
- data/lib/zaniah/ffi/wayland.rb +140 -0
- data/lib/zaniah/frame_context.rb +14 -0
- data/lib/zaniah/geometry.rb +16 -0
- data/lib/zaniah/gpu/buffer.rb +23 -0
- data/lib/zaniah/gpu/frame_encoder.rb +43 -0
- data/lib/zaniah/gpu/instance_packing.rb +53 -0
- data/lib/zaniah/gpu/metal.rb +209 -0
- data/lib/zaniah/gpu/open_gl.rb +210 -0
- data/lib/zaniah/gpu/pipeline.rb +7 -0
- data/lib/zaniah/gpu/software.rb +126 -0
- data/lib/zaniah/gpu/texture.rb +33 -0
- data/lib/zaniah/gpu/vulkan.rb +233 -0
- data/lib/zaniah/gpu.rb +18 -0
- data/lib/zaniah/hsla.rb +14 -0
- data/lib/zaniah/image.rb +19 -0
- data/lib/zaniah/input/action_registry.rb +12 -0
- data/lib/zaniah/input/composition.rb +7 -0
- data/lib/zaniah/input/context_predicate.rb +87 -0
- data/lib/zaniah/input/dispatcher.rb +59 -0
- data/lib/zaniah/input/file_drop.rb +7 -0
- data/lib/zaniah/input/focus_handle.rb +24 -0
- data/lib/zaniah/input/key_down.rb +7 -0
- data/lib/zaniah/input/key_up.rb +7 -0
- data/lib/zaniah/input/keymap/binding.rb +9 -0
- data/lib/zaniah/input/keymap.rb +55 -0
- data/lib/zaniah/input/keystroke.rb +20 -0
- data/lib/zaniah/input/mouse_down.rb +7 -0
- data/lib/zaniah/input/mouse_move.rb +7 -0
- data/lib/zaniah/input/mouse_up.rb +7 -0
- data/lib/zaniah/input/scroll_wheel.rb +7 -0
- data/lib/zaniah/input/text_input.rb +7 -0
- data/lib/zaniah/input.rb +17 -0
- data/lib/zaniah/layout/available_space.rb +11 -0
- data/lib/zaniah/layout/engine.rb +214 -0
- data/lib/zaniah/layout/node.rb +36 -0
- data/lib/zaniah/layout/style.rb +19 -0
- data/lib/zaniah/layout/tree.rb +16 -0
- data/lib/zaniah/layout.rb +11 -0
- data/lib/zaniah/length.rb +14 -0
- data/lib/zaniah/length_units.rb +9 -0
- data/lib/zaniah/list/height_index.rb +68 -0
- data/lib/zaniah/list.rb +101 -0
- data/lib/zaniah/overlay.rb +10 -0
- data/lib/zaniah/platform/display.rb +7 -0
- data/lib/zaniah/platform/file_event.rb +7 -0
- data/lib/zaniah/platform/headless/window.rb +185 -0
- data/lib/zaniah/platform/linux/appearance_aware.rb +60 -0
- data/lib/zaniah/platform/linux/displays.rb +55 -0
- data/lib/zaniah/platform/linux/free_type.rb +80 -0
- data/lib/zaniah/platform/linux/watcher.rb +70 -0
- data/lib/zaniah/platform/linux/wayland_display_discovery.rb +60 -0
- data/lib/zaniah/platform/linux/wayland_window.rb +369 -0
- data/lib/zaniah/platform/linux/window.rb +418 -0
- data/lib/zaniah/platform/linux.rb +3 -0
- data/lib/zaniah/platform/mac/app.rb +67 -0
- data/lib/zaniah/platform/mac/core_text.rb +94 -0
- data/lib/zaniah/platform/mac/displays.rb +23 -0
- data/lib/zaniah/platform/mac/watcher.rb +63 -0
- data/lib/zaniah/platform/mac/window.rb +298 -0
- data/lib/zaniah/platform/mac.rb +19 -0
- data/lib/zaniah/platform/tui/input_decoder.rb +145 -0
- data/lib/zaniah/platform/tui/text_renderer.rb +40 -0
- data/lib/zaniah/platform/tui/window.rb +91 -0
- data/lib/zaniah/platform/windows/terminal.rb +127 -0
- data/lib/zaniah/platform/windows/watcher.rb +91 -0
- data/lib/zaniah/platform/windows/window.rb +407 -0
- data/lib/zaniah/platform/windows.rb +3 -0
- data/lib/zaniah/platform.rb +34 -0
- data/lib/zaniah/png.rb +118 -0
- data/lib/zaniah/point.rb +8 -0
- data/lib/zaniah/process_pool/worker.rb +7 -0
- data/lib/zaniah/process_pool.rb +239 -0
- data/lib/zaniah/process_wire.rb +127 -0
- data/lib/zaniah/quad_packer.rb +15 -0
- data/lib/zaniah/scene/sprite_batch.rb +7 -0
- data/lib/zaniah/scene.rb +167 -0
- data/lib/zaniah/scene_renderer.rb +8 -0
- data/lib/zaniah/size.rb +5 -0
- data/lib/zaniah/subscription.rb +14 -0
- data/lib/zaniah/svg/path.rb +134 -0
- data/lib/zaniah/svg/rasterizable.rb +186 -0
- data/lib/zaniah/svg.rb +295 -0
- data/lib/zaniah/task/cancelled.rb +7 -0
- data/lib/zaniah/task/timeout.rb +7 -0
- data/lib/zaniah/task.rb +51 -0
- data/lib/zaniah/task_executor.rb +105 -0
- data/lib/zaniah/text.rb +24 -0
- data/lib/zaniah/text_system/atlas/entry.rb +9 -0
- data/lib/zaniah/text_system/atlas.rb +102 -0
- data/lib/zaniah/text_system/atlas_cache.rb +146 -0
- data/lib/zaniah/text_system/font_db/face.rb +9 -0
- data/lib/zaniah/text_system/font_db.rb +193 -0
- data/lib/zaniah/text_system/glyph.rb +7 -0
- data/lib/zaniah/text_system/line_layout.rb +20 -0
- data/lib/zaniah/text_system/renderer.rb +196 -0
- data/lib/zaniah/text_system/shaper.rb +430 -0
- data/lib/zaniah/text_system/typesetter/copy_error.rb +9 -0
- data/lib/zaniah/text_system/typesetter.rb +168 -0
- data/lib/zaniah/text_system.rb +9 -0
- data/lib/zaniah/unicode.rb +13 -0
- data/lib/zaniah/uniform_list.rb +30 -0
- data/lib/zaniah/version.rb +5 -0
- data/lib/zaniah/view.rb +13 -0
- data/lib/zaniah.rb +29 -0
- data/sig/app.rbs +80 -0
- data/sig/elements.rbs +129 -0
- data/sig/gpu.rbs +108 -0
- data/sig/input.rbs +114 -0
- data/sig/layout.rbs +53 -0
- data/sig/native.rbs +88 -0
- data/sig/platform.rbs +94 -0
- data/sig/scene.rbs +52 -0
- data/sig/shaper.rbs +8 -0
- data/sig/text.rbs +137 -0
- data/sig/zaniah.rbs +120 -0
- data/tools/check_dependencies.rb +52 -0
- 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
|
data/lib/zaniah/color.rb
ADDED
|
@@ -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,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
|
+
|
data/lib/zaniah/div.rb
ADDED
data/lib/zaniah/edges.rb
ADDED
|
@@ -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,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
|
data/lib/zaniah/error.rb
ADDED
|
@@ -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"
|