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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "font_db"
|
|
4
|
+
require_relative "glyph"
|
|
5
|
+
require_relative "line_layout"
|
|
6
|
+
require_relative "shaper"
|
|
7
|
+
|
|
8
|
+
module Zaniah
|
|
9
|
+
module TextSystem
|
|
10
|
+
# Layout-only state can belong to a background worker without GPU atlases or
|
|
11
|
+
# sharing the rendering thread's mutable caches. Providers are owner-local.
|
|
12
|
+
class Typesetter
|
|
13
|
+
attr_reader :font_db, :font, :shaper, :segmenter
|
|
14
|
+
|
|
15
|
+
def initialize(font: nil, font_db: Zaniah.configuration.font_db, capacity: 2000,
|
|
16
|
+
shaper: Zaniah.configuration.shaper, segmenter: Zaniah.configuration.segmenter)
|
|
17
|
+
require "alhena"
|
|
18
|
+
raise ArgumentError, "layout cache capacity must be positive" unless capacity.is_a?(Integer) && capacity.positive?
|
|
19
|
+
@font_db = provider(font_db, :font_db, %i[find fallback]) { FontDB.new }
|
|
20
|
+
@shaper = provider(shaper, :shaper, [:shape]) { Shaper.new }
|
|
21
|
+
@segmenter = provider(segmenter, :segmenter, [:grapheme_clusters]) { Unicode }
|
|
22
|
+
@font, @capacity = font || @font_db.find, capacity
|
|
23
|
+
@cache = {}
|
|
24
|
+
@layout_lookup, @layout_keys = [nil, nil, nil], {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def layout_line(text, font: nil, size: 14)
|
|
28
|
+
raise ArgumentError, "text must be valid UTF-8" unless text.is_a?(String) && text.encoding == Encoding::UTF_8 && text.valid_encoding?
|
|
29
|
+
raise ArgumentError, "font size must be finite and positive" unless size.is_a?(Numeric) && size.finite? && size.positive?
|
|
30
|
+
font ||= @font
|
|
31
|
+
cached = cached_layout(text, font, size)
|
|
32
|
+
return cached if cached
|
|
33
|
+
|
|
34
|
+
text = text.dup.freeze
|
|
35
|
+
boundaries = grapheme_boundaries(text)
|
|
36
|
+
glyphs = shape_glyphs(glyphs_for(text, font, size), text, size)
|
|
37
|
+
scale = size.to_f / font.units_per_em
|
|
38
|
+
line = LineLayout.new(text, glyphs.freeze, glyphs.sum(&:advance), font.ascent * scale,
|
|
39
|
+
-font.descent * scale, size, carets_for(glyphs, boundaries).freeze)
|
|
40
|
+
cache_layout(line, text, font, size)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Copy only layout state, never a Renderer's atlases/rasterizer. Custom
|
|
44
|
+
# providers explicitly deep-copy their state without mutating the source.
|
|
45
|
+
def fork(capacity: @capacity)
|
|
46
|
+
copies = []
|
|
47
|
+
options = {font: @font, font_db: @font_db, shaper: @shaper, segmenter: @segmenter}.to_h do |kind, value|
|
|
48
|
+
copy = layout_copy(kind, value)
|
|
49
|
+
copies << copy unless copy.equal?(value)
|
|
50
|
+
[kind, copy]
|
|
51
|
+
end
|
|
52
|
+
Typesetter.new(**options, capacity: capacity).tap { |copy| copy.instance_variable_set(:@owned_font, options[:font]) }
|
|
53
|
+
rescue StandardError
|
|
54
|
+
copies.reverse.uniq(&:object_id).each do |copy|
|
|
55
|
+
copy.close if copy.respond_to?(:close)
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil # Preserve the original copy/validation error.
|
|
58
|
+
end
|
|
59
|
+
raise
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def close
|
|
63
|
+
return if @closed
|
|
64
|
+
@closed = true
|
|
65
|
+
@cache.clear
|
|
66
|
+
@layout_keys.clear
|
|
67
|
+
[@shaper, @segmenter, @font_db, @owned_font].uniq(&:object_id).each { |value| value.close if value.respond_to?(:close) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def cached_layout(text, font, size)
|
|
73
|
+
@layout_lookup[0], @layout_lookup[1], @layout_lookup[2] = text, font, size
|
|
74
|
+
return unless (cached = @cache.delete(@layout_lookup))
|
|
75
|
+
@cache[@layout_keys.fetch(cached.object_id)] = cached
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def grapheme_boundaries(text)
|
|
79
|
+
characters = @segmenter.grapheme_clusters(text)
|
|
80
|
+
raise Error, "segmenter must partition the UTF-8 text into nonempty strings" unless characters.is_a?(Array) && characters.all? { |part| part.is_a?(String) && !part.empty? && part.valid_encoding? } && characters.join == text
|
|
81
|
+
boundaries, byte = [0], 0
|
|
82
|
+
characters.each { |character| boundaries << (byte += character.bytesize) }
|
|
83
|
+
boundaries
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def glyphs_for(text, font, size)
|
|
87
|
+
x, offset, glyphs = 0.0, 0, []
|
|
88
|
+
text.each_char do |character|
|
|
89
|
+
if (character.ord.between?(0xFE00, 0xFE0F) || character.ord.between?(0xE0100, 0xE01EF)) && !glyphs.empty?
|
|
90
|
+
previous = glyphs.pop
|
|
91
|
+
base = text.byteslice(previous.start, previous.finish - previous.start).each_char.first
|
|
92
|
+
id = previous.font.glyph_id(base, variation_selector: character.ord)
|
|
93
|
+
glyphs << Glyph.new(previous.font, id, previous.start, offset + character.bytesize, previous.x, previous.advance)
|
|
94
|
+
offset += character.bytesize
|
|
95
|
+
next
|
|
96
|
+
end
|
|
97
|
+
selected = @font_db.fallback(character.ord, font)
|
|
98
|
+
id = selected.glyph_id(character)
|
|
99
|
+
advance = selected.advance(id, size: size)
|
|
100
|
+
glyphs << Glyph.new(selected, id, offset, offset + character.bytesize, x, advance)
|
|
101
|
+
x += advance
|
|
102
|
+
offset += character.bytesize
|
|
103
|
+
end
|
|
104
|
+
glyphs
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def shape_glyphs(glyphs, text, size)
|
|
108
|
+
glyphs = @shaper.shape(glyphs, size: size, text: text)
|
|
109
|
+
valid = glyphs.is_a?(Array) && glyphs.all? do |glyph|
|
|
110
|
+
glyph.is_a?(Glyph) && glyph.start.is_a?(Integer) && glyph.finish.is_a?(Integer) &&
|
|
111
|
+
glyph.start >= 0 && glyph.finish.between?(glyph.start + 1, text.bytesize) &&
|
|
112
|
+
glyph.x.is_a?(Numeric) && glyph.x.finite? && glyph.advance.is_a?(Numeric) && glyph.advance.finite?
|
|
113
|
+
end
|
|
114
|
+
raise Error, "shaper must return finite Glyph values with valid UTF-8 byte ranges" unless valid
|
|
115
|
+
glyphs
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def carets_for(glyphs, boundaries)
|
|
119
|
+
carets, pen, boundary = [[0, 0.0]], 0.0, 1
|
|
120
|
+
glyphs.chunk { |glyph| [glyph.start, glyph.finish] }.each do |(first, last), cluster|
|
|
121
|
+
advance = cluster.sum(&:advance)
|
|
122
|
+
first_boundary = boundary
|
|
123
|
+
boundary += 1 while boundary < boundaries.length && boundaries[boundary] <= last
|
|
124
|
+
count = boundary - first_boundary
|
|
125
|
+
count.times do |index|
|
|
126
|
+
carets << [boundaries[first_boundary + index], pen + advance * (index + 1) / count]
|
|
127
|
+
end
|
|
128
|
+
pen += advance
|
|
129
|
+
end
|
|
130
|
+
carets
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def cache_layout(line, text, font, size)
|
|
134
|
+
@layout_keys.delete(@cache.shift.last.object_id) if @cache.length >= @capacity
|
|
135
|
+
key = @layout_keys[line.object_id] = [text, font, size].freeze
|
|
136
|
+
@cache[key] = line
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def layout_copy(kind, value)
|
|
140
|
+
native = value.singleton_methods.empty?
|
|
141
|
+
return Alhena::Font.new(value.data, index: value.index, axes: value.axis_values) if kind == :font && native && value.instance_of?(Alhena::Font)
|
|
142
|
+
return FontDB.new(paths: value.paths) if kind == :font_db && native && value.instance_of?(FontDB)
|
|
143
|
+
return Shaper.new if kind == :shaper && native && value.instance_of?(Shaper)
|
|
144
|
+
return Unicode if kind == :segmenter && value.equal?(Unicode)
|
|
145
|
+
raise CopyError, "#{kind} provider must implement #layout_copy for independent text layout" unless value.respond_to?(:layout_copy)
|
|
146
|
+
copy = value.layout_copy
|
|
147
|
+
methods = {font: %i[units_per_em ascent descent family glyph_id advance tables], font_db: %i[find fallback], shaper: [:shape], segmenter: [:grapheme_clusters]}.fetch(kind)
|
|
148
|
+
unless !copy.equal?(value) && methods.all? { |name| copy.respond_to?(name) }
|
|
149
|
+
copy.close if !copy.equal?(value) && copy.respond_to?(:close)
|
|
150
|
+
raise CopyError, "#{kind} #layout_copy must return an independent provider responding to #{methods.join(', ')}"
|
|
151
|
+
end
|
|
152
|
+
copy
|
|
153
|
+
rescue CopyError
|
|
154
|
+
raise
|
|
155
|
+
rescue StandardError => error
|
|
156
|
+
raise CopyError, "#{kind} layout copy failed: #{error.message}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def provider(value, kind, methods)
|
|
160
|
+
return yield if value == :native
|
|
161
|
+
raise ArgumentError, "unsupported #{kind} provider #{value.inspect}; use :native or an object responding to #{methods.join(', ')}" unless methods.all? { |name| value.respond_to?(name) }
|
|
162
|
+
value
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
require_relative "typesetter/copy_error"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "text_system/glyph"
|
|
4
|
+
require_relative "text_system/line_layout"
|
|
5
|
+
require_relative "text_system/font_db"
|
|
6
|
+
require_relative "text_system/shaper"
|
|
7
|
+
require_relative "text_system/atlas"
|
|
8
|
+
require_relative "text_system/typesetter"
|
|
9
|
+
require_relative "text_system/renderer"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Unicode
|
|
5
|
+
# Ruby's Unicode-aware regexp engine implements extended grapheme clusters.
|
|
6
|
+
def self.grapheme_clusters(text) = text.grapheme_clusters
|
|
7
|
+
def self.width(text, ambiguous: 1)
|
|
8
|
+
require "rbconfig"
|
|
9
|
+
require "unicode/display_width"
|
|
10
|
+
::Unicode::DisplayWidth.of(text, ambiguous, emoji: :rgi)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
class UniformList < Element
|
|
5
|
+
attr_accessor :scroll_y
|
|
6
|
+
attr_reader :visible_range
|
|
7
|
+
|
|
8
|
+
def initialize(count:, row_height:, &render_item)
|
|
9
|
+
super()
|
|
10
|
+
raise ArgumentError, "invalid list dimensions" unless count >= 0 && row_height.positive?
|
|
11
|
+
@count, @row_height, @render_item, @scroll_y = count, row_height, render_item, 0
|
|
12
|
+
style(overflow: :hidden)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def request_layout(cx)
|
|
16
|
+
viewport = @style[:height]
|
|
17
|
+
viewport = viewport.resolve(cx.window.content_size.height) if viewport.is_a?(Length)
|
|
18
|
+
viewport = cx.window.content_size.height unless viewport.is_a?(Numeric)
|
|
19
|
+
@scroll_y = @scroll_y.clamp(0, [@count * @row_height - viewport, 0].max)
|
|
20
|
+
first = (@scroll_y / @row_height).floor
|
|
21
|
+
last = [first + (viewport / @row_height).ceil + 1, @count].min
|
|
22
|
+
@visible_range = first...last
|
|
23
|
+
@children = @visible_range.map do |index|
|
|
24
|
+
@render_item.call(index).style(position: :absolute, left: 0, right: 0,
|
|
25
|
+
top: index * @row_height - @scroll_y, height: @row_height)
|
|
26
|
+
end
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/zaniah/view.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
class View
|
|
5
|
+
include LengthUnits
|
|
6
|
+
|
|
7
|
+
def div = Div.new
|
|
8
|
+
def text(value, **options) = Text.new(value, **options)
|
|
9
|
+
def list(**options, &render_item) = List.new(**options, &render_item)
|
|
10
|
+
def svg(source, **options) = SVG.new(source, **options)
|
|
11
|
+
def render(_cx) = raise NotImplementedError, "implement View#render"
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/zaniah.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "zaniah/version"
|
|
4
|
+
require_relative "zaniah/data_compat"
|
|
5
|
+
require_relative "zaniah/error"
|
|
6
|
+
require_relative "zaniah/configuration"
|
|
7
|
+
|
|
8
|
+
module Zaniah
|
|
9
|
+
def self.configuration
|
|
10
|
+
@configuration ||= Configuration.new(font_raster: :native, shaper: :native,
|
|
11
|
+
font_db: :native, segmenter: :native)
|
|
12
|
+
end
|
|
13
|
+
def self.configure = yield(configuration)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require_relative "zaniah/geometry"
|
|
17
|
+
require_relative "zaniah/unicode"
|
|
18
|
+
require_relative "zaniah/scene"
|
|
19
|
+
require_relative "zaniah/png"
|
|
20
|
+
require_relative "zaniah/gpu"
|
|
21
|
+
require_relative "zaniah/layout"
|
|
22
|
+
require_relative "zaniah/app"
|
|
23
|
+
require_relative "zaniah/process_pool"
|
|
24
|
+
require_relative "zaniah/input"
|
|
25
|
+
require_relative "zaniah/element"
|
|
26
|
+
Zaniah.autoload(:SVG, File.expand_path("zaniah/svg", __dir__))
|
|
27
|
+
require_relative "zaniah/list"
|
|
28
|
+
require_relative "zaniah/platform"
|
|
29
|
+
require_relative "zaniah/text_system"
|
data/sig/app.rbs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Zaniah
|
|
2
|
+
class Subscription
|
|
3
|
+
def initialize: () { () -> void } -> void
|
|
4
|
+
def detach: () -> void
|
|
5
|
+
def attached?: () -> bool
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class Entity[T] < Data
|
|
9
|
+
attr_reader id: Integer
|
|
10
|
+
attr_reader generation: Integer
|
|
11
|
+
def self.new: [U] (Integer id, Integer generation) -> Entity[U]
|
|
12
|
+
| [U] (id: Integer, generation: Integer) -> Entity[U]
|
|
13
|
+
def read: (App app) -> T
|
|
14
|
+
def update: [U] (App app) { (T, EntityContext[T]) -> U } -> U
|
|
15
|
+
def release: (App app) -> void
|
|
16
|
+
end
|
|
17
|
+
class EntityContext[T]
|
|
18
|
+
attr_reader app: App
|
|
19
|
+
attr_reader entity: Entity[T]
|
|
20
|
+
def initialize: (App app, Entity[T] entity) -> void
|
|
21
|
+
def notify: () -> void
|
|
22
|
+
def emit: [E] (E event) -> void
|
|
23
|
+
def observe: [U] (Entity[U] target) { (T, U, EntityContext[T]) -> void } -> Subscription
|
|
24
|
+
def subscribe: [U, E] (Entity[U] target) { (T, U, E, EntityContext[T]) -> void } -> Subscription
|
|
25
|
+
def spawn: [U] () { (EntityContext[T]) -> U } -> Task[U]
|
|
26
|
+
def background: [U] () { () -> U } -> U
|
|
27
|
+
def update: () { (App) -> void } -> void
|
|
28
|
+
def theme: () -> untyped
|
|
29
|
+
end
|
|
30
|
+
class App
|
|
31
|
+
attr_reader windows: Array[Platform::Headless::Window]
|
|
32
|
+
attr_reader executor: TaskExecutor
|
|
33
|
+
def initialize: () -> void
|
|
34
|
+
def new_entity: [T] () { (EntityContext[T]) -> T } -> Entity[T]
|
|
35
|
+
def read: [T] (Entity[T] entity) -> T
|
|
36
|
+
def update: [T, U] (Entity[T] entity) { (T, EntityContext[T]) -> U } -> U
|
|
37
|
+
def release: [T] (Entity[T] entity) -> void
|
|
38
|
+
def alive?: [T] (Entity[T] entity) -> bool
|
|
39
|
+
def entity_count: () -> Integer
|
|
40
|
+
# Global values are application-owned heterogeneous state.
|
|
41
|
+
def global: (Symbol | String key) -> untyped
|
|
42
|
+
| [T] (Symbol | String key, T value) -> T
|
|
43
|
+
def listen: [T] (:notify | :emit kind, Entity[T] target, ?owner: Entity[untyped]?) { (*untyped) -> void } -> Subscription
|
|
44
|
+
def enqueue: (:notify | :emit kind, Entity[untyped] entity, ?untyped event) -> void
|
|
45
|
+
def flush_effects: () -> void
|
|
46
|
+
def open_window: (?backend: Platform::backend, ?width: Numeric, ?height: Numeric, ?title: String, ?scale_factor: Numeric, ?input: IO, ?output: IO, ?gpu: :metal | :opengl | :gl, ?display_server: :auto | :x11 | :wayland) ?{ (Platform::Headless::Window) -> (Element | Scene | nil) } -> Platform::Headless::Window
|
|
47
|
+
def run: () -> void
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Task is the framework's cancellable future, shared by both executor queues.
|
|
51
|
+
class Task[T]
|
|
52
|
+
class Cancelled < Error
|
|
53
|
+
end
|
|
54
|
+
class Timeout < Error
|
|
55
|
+
end
|
|
56
|
+
def initialize: () -> void
|
|
57
|
+
def resolve: (?T? value, ?error: Exception?) -> void
|
|
58
|
+
def await: (?timeout: Numeric?) -> T
|
|
59
|
+
def cancel: () -> void
|
|
60
|
+
def done?: () -> bool
|
|
61
|
+
def on_complete: () { () -> void } -> Subscription
|
|
62
|
+
end
|
|
63
|
+
class TaskExecutor
|
|
64
|
+
def self.current: () -> TaskExecutor?
|
|
65
|
+
def initialize: (?workers: Integer) -> void
|
|
66
|
+
def background: [T] () { () -> T } -> Task[T]
|
|
67
|
+
def spawn: [T] () { () -> T } -> Task[T]
|
|
68
|
+
def await: [T] (Task[T] task, ?timeout: Numeric?) -> T
|
|
69
|
+
def post: () { () -> void } -> void
|
|
70
|
+
def drain: () -> void
|
|
71
|
+
def wait: (Numeric seconds) -> void
|
|
72
|
+
def shutdown: () -> void
|
|
73
|
+
end
|
|
74
|
+
class ProcessPool[Input, Output]
|
|
75
|
+
def initialize: (?workers: Integer, handler: String, ?requires: Array[String], ?load_paths: Array[String], ?max_pending: Integer, ?max_bytes: Integer) -> void
|
|
76
|
+
| (?workers: Integer, ?max_pending: Integer, ?max_bytes: Integer) { (Input) -> Output } -> void
|
|
77
|
+
def submit: (Input data) -> Task[Output]
|
|
78
|
+
def shutdown: (?timeout: Numeric) -> void
|
|
79
|
+
end
|
|
80
|
+
end
|
data/sig/elements.rbs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
module Zaniah
|
|
2
|
+
class FrameContext
|
|
3
|
+
attr_reader window: Platform::Headless::Window
|
|
4
|
+
attr_reader scene: Scene
|
|
5
|
+
attr_reader dispatcher: Input::Dispatcher
|
|
6
|
+
attr_reader text_system: TextSystem::_System?
|
|
7
|
+
def initialize: (Platform::Headless::Window window) -> void
|
|
8
|
+
def state: [T] (Object key) { () -> T } -> T
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class Element
|
|
12
|
+
include LengthUnits
|
|
13
|
+
attr_reader layout_node: Layout::Node?
|
|
14
|
+
# Child constructors forward their render blocks through super(); ignored here.
|
|
15
|
+
def initialize: () ?{ (*untyped) -> void } -> void
|
|
16
|
+
def child: (Element? element) -> self
|
|
17
|
+
def children: () -> Array[Element]
|
|
18
|
+
| (Array[Element?] elements) -> self
|
|
19
|
+
def style: (**Layout::style_value properties) -> self
|
|
20
|
+
def key: (Object value) -> self
|
|
21
|
+
def with_state: [T] () { () -> T } -> self
|
|
22
|
+
def bg: (color color) -> self
|
|
23
|
+
def border_color: (color color) -> self
|
|
24
|
+
def rounded: (radius radius) -> self
|
|
25
|
+
def tooltip: (String text) -> self
|
|
26
|
+
def context_menu: (menu items) -> self
|
|
27
|
+
def flex: () -> self
|
|
28
|
+
def flex_row: () -> self
|
|
29
|
+
def flex_col: () -> self
|
|
30
|
+
def items_center: () -> self
|
|
31
|
+
def justify_center: () -> self
|
|
32
|
+
def w_full: () -> self
|
|
33
|
+
def h_full: () -> self
|
|
34
|
+
def flex_1: () -> self
|
|
35
|
+
def overflow_hidden: () -> self
|
|
36
|
+
def w: (dimension value) -> self
|
|
37
|
+
def h: (dimension value) -> self
|
|
38
|
+
def min_w: (dimension value) -> self
|
|
39
|
+
def min_h: (dimension value) -> self
|
|
40
|
+
def max_w: (dimension value) -> self
|
|
41
|
+
def max_h: (dimension value) -> self
|
|
42
|
+
def p: (Layout::style_value value) -> self
|
|
43
|
+
def m: (Layout::style_value value) -> self
|
|
44
|
+
def gap: (dimension value) -> self
|
|
45
|
+
def border: (Layout::style_value value) -> self
|
|
46
|
+
def border_b: (dimension value) -> self
|
|
47
|
+
def on_click: () { (Input::MouseDown, FrameContext) -> void } -> self
|
|
48
|
+
def on_hover: () { (Input::MouseMove, FrameContext) -> void } -> self
|
|
49
|
+
def on_drag: () { (Input::MouseMove, FrameContext) -> void } -> self
|
|
50
|
+
def on_scroll_wheel: () { (Input::ScrollWheel, FrameContext) -> void } -> self
|
|
51
|
+
def on_mouse_down: () { (Input::MouseDown, FrameContext) -> void } -> self
|
|
52
|
+
def on_mouse_up: () { (Input::MouseUp, FrameContext) -> void } -> self
|
|
53
|
+
def request_layout: (FrameContext context) -> Layout::Node
|
|
54
|
+
def prepaint: (Bounds bounds, Object? state, FrameContext context) -> void
|
|
55
|
+
def paint: (Bounds bounds, Object? state, Object? prepaint, FrameContext context) -> void
|
|
56
|
+
end
|
|
57
|
+
class Div < Element
|
|
58
|
+
end
|
|
59
|
+
class Text < Element
|
|
60
|
+
def initialize: (String text, ?size: Numeric, ?color: color, ?font: TextSystem::_Font?) -> void
|
|
61
|
+
def measured: () { (Numeric width, Numeric height) -> Layout::measurement } -> self
|
|
62
|
+
end
|
|
63
|
+
class Image < Element
|
|
64
|
+
def initialize: (String path) -> void
|
|
65
|
+
end
|
|
66
|
+
class Canvas < Element
|
|
67
|
+
def initialize: () { (Bounds, FrameContext) -> void } -> void
|
|
68
|
+
end
|
|
69
|
+
class Overlay < Div
|
|
70
|
+
end
|
|
71
|
+
class Anchored < Div
|
|
72
|
+
def initialize: (anchor: Point, ?offset: Point) -> void
|
|
73
|
+
end
|
|
74
|
+
class UniformList < Element
|
|
75
|
+
attr_accessor scroll_y: Numeric
|
|
76
|
+
attr_reader visible_range: Range[Integer]?
|
|
77
|
+
def initialize: (count: Integer, row_height: Numeric) { (Integer) -> Element } -> void
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class SVG < Element
|
|
81
|
+
interface _Outline
|
|
82
|
+
def commands: () -> Array[Symbol]
|
|
83
|
+
def coordinates: () -> Array[Float]
|
|
84
|
+
def empty?: () -> bool
|
|
85
|
+
def bounds: () -> [Numeric, Numeric, Numeric, Numeric]
|
|
86
|
+
end
|
|
87
|
+
class Path
|
|
88
|
+
def self.parse: (String source) -> _Outline
|
|
89
|
+
def initialize: (String source) -> void
|
|
90
|
+
def parse: () -> _Outline
|
|
91
|
+
end
|
|
92
|
+
attr_reader width: Numeric
|
|
93
|
+
attr_reader height: Numeric
|
|
94
|
+
attr_reader view_box: Array[Numeric]
|
|
95
|
+
def self.parse: (String source, ?color: String) -> SVG
|
|
96
|
+
def self.open: (String path, ?color: String) -> SVG
|
|
97
|
+
def initialize: (String source, ?color: String) -> void
|
|
98
|
+
def texture: (?width: Integer, ?height: Integer, ?color: String) -> GPU::Texture
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class List < Element
|
|
102
|
+
attr_reader scroll_y: Float
|
|
103
|
+
attr_reader visible_range: Range[Integer]
|
|
104
|
+
attr_reader heights: HeightIndex
|
|
105
|
+
def initialize: (count: Integer, ?estimated_height: Numeric, ?overscan: Integer) { (Integer) -> Element } -> void
|
|
106
|
+
def scroll_y=: (Numeric value) -> Numeric
|
|
107
|
+
def total_height: () -> Float
|
|
108
|
+
def update_height: (Integer index, Numeric height) -> self
|
|
109
|
+
def scroll_to: (Integer index, ?align: :start | :center | :end | :nearest) -> self
|
|
110
|
+
class HeightIndex
|
|
111
|
+
attr_reader count: Integer
|
|
112
|
+
def initialize: (Integer count, Numeric estimate) -> void
|
|
113
|
+
def []: (Integer index) -> Float
|
|
114
|
+
def update: (Integer index, Numeric height) -> Float
|
|
115
|
+
def prefix: (Integer index) -> Float
|
|
116
|
+
def total: () -> Float
|
|
117
|
+
def index_at: (Numeric y) -> Integer
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class View
|
|
122
|
+
include LengthUnits
|
|
123
|
+
def div: () -> Div
|
|
124
|
+
def text: (String value, ?size: Numeric, ?color: color, ?font: TextSystem::_Font?) -> Text
|
|
125
|
+
def render: (FrameContext | EntityContext[untyped] context) -> Element
|
|
126
|
+
def svg: (String source, ?color: String) -> SVG
|
|
127
|
+
def list: (count: Integer, ?estimated_height: Numeric, ?overscan: Integer) { (Integer) -> Element } -> List
|
|
128
|
+
end
|
|
129
|
+
end
|
data/sig/gpu.rbs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module Zaniah
|
|
2
|
+
module GPU
|
|
3
|
+
type format = :r8 | :rgba8
|
|
4
|
+
type shader = :quad | :mono_sprite | :poly_sprite
|
|
5
|
+
interface _Device
|
|
6
|
+
def width: () -> Numeric
|
|
7
|
+
def height: () -> Numeric
|
|
8
|
+
def pixels: () -> String
|
|
9
|
+
def resize: (Numeric width, Numeric height) -> void
|
|
10
|
+
def create_texture: (Integer width, Integer height, ?format: format, ?data: String?) -> Texture
|
|
11
|
+
def create_buffer: (Integer size, ?usage: Symbol, ?mutable: bool) -> Buffer
|
|
12
|
+
def create_pipeline: (shader: shader, ?blend: Symbol) -> Pipeline
|
|
13
|
+
def begin_frame: (?clear: color) -> FrameEncoder
|
|
14
|
+
def render: (Scene scene, ?clear: color) -> void
|
|
15
|
+
def write_png: (String path) -> Integer
|
|
16
|
+
def release: () -> void
|
|
17
|
+
end
|
|
18
|
+
def self.create: (?Platform::Headless::Window? window, ?backend: :software | :metal | :opengl, ?width: Numeric?, ?height: Numeric?) -> _Device
|
|
19
|
+
|
|
20
|
+
class Buffer
|
|
21
|
+
attr_reader size: Integer
|
|
22
|
+
attr_reader usage: Symbol
|
|
23
|
+
attr_reader data: String
|
|
24
|
+
def initialize: (Integer size, ?usage: Symbol, ?mutable: bool) -> void
|
|
25
|
+
def write: (String bytes, ?offset: Integer) -> self
|
|
26
|
+
def release: () -> String
|
|
27
|
+
end
|
|
28
|
+
class Texture
|
|
29
|
+
attr_reader width: Integer
|
|
30
|
+
attr_reader height: Integer
|
|
31
|
+
attr_reader format: format
|
|
32
|
+
attr_reader data: String
|
|
33
|
+
attr_reader revision: Integer
|
|
34
|
+
def initialize: (Integer width, Integer height, ?format: format, ?data: String?) -> void
|
|
35
|
+
def upload: (Integer x, Integer y, Integer width, Integer height, String bytes) -> self
|
|
36
|
+
def release: () -> Integer
|
|
37
|
+
end
|
|
38
|
+
class Pipeline < Data
|
|
39
|
+
attr_reader shader: shader
|
|
40
|
+
attr_reader blend: Symbol
|
|
41
|
+
def self.new: (shader shader, Symbol blend) -> Pipeline
|
|
42
|
+
| (shader: shader, blend: Symbol) -> Pipeline
|
|
43
|
+
end
|
|
44
|
+
class FrameEncoder
|
|
45
|
+
def initialize: (_Device device, color clear) -> void
|
|
46
|
+
def set_pipeline: (Pipeline pipeline) -> Pipeline
|
|
47
|
+
def set_uniforms: (**Numeric | Array[Numeric] values) -> Hash[Symbol, Numeric | Array[Numeric]]
|
|
48
|
+
def set_texture: (Integer slot, Texture texture) -> Texture
|
|
49
|
+
def set_scissor: (Bounds? bounds) -> Bounds?
|
|
50
|
+
def draw_instanced: (vertex_count: Integer, instance_buffer: Buffer, instance_count: Integer) -> void
|
|
51
|
+
def present: () -> void
|
|
52
|
+
end
|
|
53
|
+
class Software
|
|
54
|
+
attr_reader width: Integer
|
|
55
|
+
attr_reader height: Integer
|
|
56
|
+
attr_reader pixels: String
|
|
57
|
+
def initialize: (Numeric width, Numeric height) -> void
|
|
58
|
+
def resize: (Numeric width, Numeric height) -> void
|
|
59
|
+
def create_texture: (Integer width, Integer height, ?format: format, ?data: String?) -> Texture
|
|
60
|
+
def create_buffer: (Integer size, ?usage: Symbol, ?mutable: bool) -> Buffer
|
|
61
|
+
def create_pipeline: (shader: shader, ?blend: Symbol) -> Pipeline
|
|
62
|
+
def begin_frame: (?clear: color) -> FrameEncoder
|
|
63
|
+
def render: (Scene scene, ?clear: color) -> String
|
|
64
|
+
def write_png: (String path) -> Integer
|
|
65
|
+
def release: () -> String
|
|
66
|
+
end
|
|
67
|
+
class Metal
|
|
68
|
+
attr_reader width: Numeric
|
|
69
|
+
attr_reader height: Numeric
|
|
70
|
+
attr_reader draw_calls: Integer
|
|
71
|
+
def initialize: (Platform::Mac::Window window) -> void
|
|
72
|
+
def resize: (Numeric width, Numeric height) -> void
|
|
73
|
+
def create_texture: (Integer width, Integer height, ?format: format, ?data: String?) -> Texture
|
|
74
|
+
def create_buffer: (Integer size, ?usage: Symbol, ?mutable: bool) -> Buffer
|
|
75
|
+
def create_pipeline: (shader: shader, ?blend: Symbol) -> Pipeline
|
|
76
|
+
def begin_frame: (?clear: color) -> FrameEncoder
|
|
77
|
+
def render: (Scene scene, ?clear: color) -> void
|
|
78
|
+
def pixels: () -> String
|
|
79
|
+
def write_png: (String path) -> Integer
|
|
80
|
+
def release: () -> void
|
|
81
|
+
end
|
|
82
|
+
class OpenGL
|
|
83
|
+
attr_reader width: Numeric
|
|
84
|
+
attr_reader height: Numeric
|
|
85
|
+
attr_reader draw_calls: Integer
|
|
86
|
+
def resize: (Numeric width, Numeric height) -> void
|
|
87
|
+
def create_texture: (Integer width, Integer height, ?format: format, ?data: String?) -> Texture
|
|
88
|
+
def create_buffer: (Integer size, ?usage: Symbol, ?mutable: bool) -> Buffer
|
|
89
|
+
def create_pipeline: (shader: shader, ?blend: Symbol) -> Pipeline
|
|
90
|
+
def begin_frame: (?clear: color) -> FrameEncoder
|
|
91
|
+
def render: (Scene scene, ?clear: color, ?capture: bool) -> void
|
|
92
|
+
def pixels: () -> String
|
|
93
|
+
def write_png: (String path) -> Integer
|
|
94
|
+
def release: () -> void
|
|
95
|
+
end
|
|
96
|
+
# Optional offscreen triangle probe, not a full window-renderer interface.
|
|
97
|
+
class Vulkan
|
|
98
|
+
attr_reader width: Integer
|
|
99
|
+
attr_reader height: Integer
|
|
100
|
+
attr_reader pixels: String
|
|
101
|
+
attr_reader device_name: String
|
|
102
|
+
def initialize: (?width: Integer, ?height: Integer) -> void
|
|
103
|
+
def render_triangle: (vertex_spirv: String, fragment_spirv: String) -> void
|
|
104
|
+
def write_png: (String path) -> Integer
|
|
105
|
+
def release: () -> void
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|