zaniah 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +18 -0
- data/README.md +14 -4
- data/assets/icons/check.svg +1 -0
- data/assets/icons/close.svg +1 -0
- data/assets/icons/info.svg +1 -0
- data/assets/icons/menu.svg +1 -0
- data/assets/icons/search.svg +1 -0
- data/assets/icons/warning.svg +1 -0
- data/docs/accessibility.md +61 -0
- data/docs/adr/004-style-resolution.md +17 -0
- data/docs/adr/005-instance-layout.md +43 -0
- data/docs/adr/006-component-model.md +17 -0
- data/docs/adr/007-text-buffer.md +17 -0
- data/docs/adr/008-accessibility-bridge.md +19 -0
- data/docs/adr/009-grid-scope.md +17 -0
- data/docs/animation.md +46 -0
- data/docs/components.md +109 -0
- data/docs/gallery/command-palette-dark.png +0 -0
- data/docs/gallery/command-palette-high_contrast.png +0 -0
- data/docs/gallery/command-palette-light.png +0 -0
- data/docs/gallery/components-dark.png +0 -0
- data/docs/gallery/components-high_contrast.png +0 -0
- data/docs/gallery/components-light.png +0 -0
- data/docs/gallery/context-menu-dark.png +0 -0
- data/docs/gallery/context-menu-high_contrast.png +0 -0
- data/docs/gallery/context-menu-light.png +0 -0
- data/docs/gallery/dialog-dark.png +0 -0
- data/docs/gallery/dialog-high_contrast.png +0 -0
- data/docs/gallery/dialog-light.png +0 -0
- data/docs/gallery/drawer-left-dark.png +0 -0
- data/docs/gallery/drawer-left-high_contrast.png +0 -0
- data/docs/gallery/drawer-left-light.png +0 -0
- data/docs/gallery/drawer-right-dark.png +0 -0
- data/docs/gallery/drawer-right-high_contrast.png +0 -0
- data/docs/gallery/drawer-right-light.png +0 -0
- data/docs/gallery/menu-dark.png +0 -0
- data/docs/gallery/menu-high_contrast.png +0 -0
- data/docs/gallery/menu-light.png +0 -0
- data/docs/gallery/modal-dark.png +0 -0
- data/docs/gallery/modal-high_contrast.png +0 -0
- data/docs/gallery/modal-light.png +0 -0
- data/docs/gallery/popover-bottom-dark.png +0 -0
- data/docs/gallery/popover-bottom-high_contrast.png +0 -0
- data/docs/gallery/popover-bottom-light.png +0 -0
- data/docs/gallery/popover-left-dark.png +0 -0
- data/docs/gallery/popover-left-high_contrast.png +0 -0
- data/docs/gallery/popover-left-light.png +0 -0
- data/docs/gallery/popover-right-dark.png +0 -0
- data/docs/gallery/popover-right-high_contrast.png +0 -0
- data/docs/gallery/popover-right-light.png +0 -0
- data/docs/gallery/popover-top-dark.png +0 -0
- data/docs/gallery/popover-top-high_contrast.png +0 -0
- data/docs/gallery/popover-top-light.png +0 -0
- data/docs/gallery/toast-danger-dark.png +0 -0
- data/docs/gallery/toast-danger-high_contrast.png +0 -0
- data/docs/gallery/toast-danger-light.png +0 -0
- data/docs/gallery/toast-info-dark.png +0 -0
- data/docs/gallery/toast-info-high_contrast.png +0 -0
- data/docs/gallery/toast-info-light.png +0 -0
- data/docs/gallery/toast-success-dark.png +0 -0
- data/docs/gallery/toast-success-high_contrast.png +0 -0
- data/docs/gallery/toast-success-light.png +0 -0
- data/docs/gallery/toast-warning-dark.png +0 -0
- data/docs/gallery/toast-warning-high_contrast.png +0 -0
- data/docs/gallery/toast-warning-light.png +0 -0
- data/docs/gallery/tooltip-bottom-dark.png +0 -0
- data/docs/gallery/tooltip-bottom-high_contrast.png +0 -0
- data/docs/gallery/tooltip-bottom-light.png +0 -0
- data/docs/gallery/tooltip-left-dark.png +0 -0
- data/docs/gallery/tooltip-left-high_contrast.png +0 -0
- data/docs/gallery/tooltip-left-light.png +0 -0
- data/docs/gallery/tooltip-right-dark.png +0 -0
- data/docs/gallery/tooltip-right-high_contrast.png +0 -0
- data/docs/gallery/tooltip-right-light.png +0 -0
- data/docs/gallery/tooltip-top-dark.png +0 -0
- data/docs/gallery/tooltip-top-high_contrast.png +0 -0
- data/docs/gallery/tooltip-top-light.png +0 -0
- data/docs/index.html +74 -0
- data/docs/layout.md +31 -0
- data/docs/native.md +7 -2
- data/docs/site.css +32 -0
- data/docs/text.md +35 -0
- data/docs/theme.md +50 -0
- data/docs/tui.md +36 -0
- data/examples/gallery.rb +155 -0
- data/examples/native_ime.rb +13 -6
- data/lib/zaniah/accessibility/linux/service.rb +361 -0
- data/lib/zaniah/accessibility/linux.rb +23 -0
- data/lib/zaniah/accessibility/mac.rb +136 -0
- data/lib/zaniah/accessibility/native_tree.rb +53 -0
- data/lib/zaniah/accessibility/node.rb +12 -0
- data/lib/zaniah/accessibility/tree.rb +61 -0
- data/lib/zaniah/accessibility/windows/provider.rb +273 -0
- data/lib/zaniah/accessibility/windows.rb +38 -0
- data/lib/zaniah/accessibility.rb +63 -0
- data/lib/zaniah/animation/animator.rb +95 -0
- data/lib/zaniah/animation/easing.rb +45 -0
- data/lib/zaniah/animation/spring.rb +36 -0
- data/lib/zaniah/animation/transition.rb +5 -0
- data/lib/zaniah/animation.rb +6 -0
- data/lib/zaniah/app.rb +22 -2
- data/lib/zaniah/color.rb +16 -0
- data/lib/zaniah/devtools/hot_reload.rb +40 -0
- data/lib/zaniah/devtools/inspector.rb +111 -0
- data/lib/zaniah/devtools/stats_overlay.rb +16 -0
- data/lib/zaniah/devtools.rb +13 -0
- data/lib/zaniah/element.rb +210 -15
- data/lib/zaniah/frame_context.rb +4 -1
- data/lib/zaniah/gpu/frame_encoder.rb +10 -1
- data/lib/zaniah/gpu/instance_packing.rb +33 -5
- data/lib/zaniah/gpu/metal.rb +26 -10
- data/lib/zaniah/gpu/open_gl.rb +35 -14
- data/lib/zaniah/gpu/software.rb +116 -14
- data/lib/zaniah/gpu/vulkan/scene.frag +51 -0
- data/lib/zaniah/gpu/vulkan/scene.frag.spv +0 -0
- data/lib/zaniah/gpu/vulkan/scene.vert +50 -0
- data/lib/zaniah/gpu/vulkan/scene.vert.spv +0 -0
- data/lib/zaniah/gpu/vulkan.rb +282 -36
- data/lib/zaniah/gpu.rb +8 -3
- data/lib/zaniah/input/dispatcher.rb +96 -12
- data/lib/zaniah/input/focus_handle.rb +13 -4
- data/lib/zaniah/input/focus_tree.rb +77 -0
- data/lib/zaniah/input/keymap.rb +77 -0
- data/lib/zaniah/input.rb +1 -0
- data/lib/zaniah/interactivity.rb +30 -0
- data/lib/zaniah/layout/engine.rb +173 -36
- data/lib/zaniah/layout/grid.rb +134 -0
- data/lib/zaniah/layout/style.rb +16 -1
- data/lib/zaniah/layout.rb +1 -0
- data/lib/zaniah/length.rb +3 -0
- data/lib/zaniah/length_units.rb +7 -0
- data/lib/zaniah/list.rb +27 -15
- data/lib/zaniah/platform/appearance.rb +11 -0
- data/lib/zaniah/platform/headless/window.rb +101 -9
- data/lib/zaniah/platform/linux/appearance_aware.rb +8 -0
- data/lib/zaniah/platform/linux/wayland_window.rb +3 -1
- data/lib/zaniah/platform/linux/window.rb +4 -2
- data/lib/zaniah/platform/mac/app.rb +1 -1
- data/lib/zaniah/platform/mac/core_text.rb +3 -0
- data/lib/zaniah/platform/mac/window.rb +9 -1
- data/lib/zaniah/platform/tui/window.rb +1 -1
- data/lib/zaniah/platform/windows/window.rb +12 -2
- data/lib/zaniah/platform.rb +1 -0
- data/lib/zaniah/scene.rb +137 -28
- data/lib/zaniah/scroll_state.rb +117 -0
- data/lib/zaniah/scroll_view.rb +94 -0
- data/lib/zaniah/style/gradient.rb +28 -0
- data/lib/zaniah/style/ring.rb +13 -0
- data/lib/zaniah/style/shadow.rb +13 -0
- data/lib/zaniah/style/style_set.rb +43 -0
- data/lib/zaniah/style/transform.rb +40 -0
- data/lib/zaniah/svg.rb +19 -0
- data/lib/zaniah/text.rb +261 -6
- data/lib/zaniah/text_buffer.rb +119 -0
- data/lib/zaniah/text_selection.rb +18 -0
- data/lib/zaniah/text_system/atlas_cache.rb +1 -1
- data/lib/zaniah/text_system/fallback.rb +24 -0
- data/lib/zaniah/text_system/font_db.rb +4 -1
- data/lib/zaniah/text_system/kinsoku.rb +28 -0
- data/lib/zaniah/text_system/line_breaker.rb +54 -0
- data/lib/zaniah/text_system/paragraph.rb +99 -0
- data/lib/zaniah/text_system/renderer.rb +13 -2
- data/lib/zaniah/text_system/typesetter.rb +4 -0
- data/lib/zaniah/text_system.rb +3 -0
- data/lib/zaniah/theme.rb +39 -0
- data/lib/zaniah/ui/charts.rb +165 -0
- data/lib/zaniah/ui/choice_fields.rb +134 -0
- data/lib/zaniah/ui/component.rb +73 -0
- data/lib/zaniah/ui/controls.rb +439 -0
- data/lib/zaniah/ui/data.rb +364 -0
- data/lib/zaniah/ui/editors.rb +67 -0
- data/lib/zaniah/ui/forms.rb +131 -0
- data/lib/zaniah/ui/layout_components.rb +205 -0
- data/lib/zaniah/ui/modal.rb +129 -0
- data/lib/zaniah/ui/overlays.rb +282 -0
- data/lib/zaniah/ui/pickers.rb +169 -0
- data/lib/zaniah/ui/primitives.rb +199 -0
- data/lib/zaniah/ui/scrollbar.rb +104 -0
- data/lib/zaniah/ui/structure.rb +224 -0
- data/lib/zaniah/ui/text_inputs.rb +169 -0
- data/lib/zaniah/ui/variants.rb +23 -0
- data/lib/zaniah/ui.rb +18 -0
- data/lib/zaniah/unicode.rb +42 -0
- data/lib/zaniah/uniform_list.rb +27 -6
- data/lib/zaniah/version.rb +1 -1
- data/lib/zaniah.rb +14 -0
- data/sig/accessibility.rbs +34 -0
- data/sig/animation.rbs +54 -0
- data/sig/app.rbs +2 -1
- data/sig/devtools.rbs +37 -0
- data/sig/elements.rbs +88 -6
- data/sig/gpu.rbs +9 -2
- data/sig/input.rbs +31 -5
- data/sig/layout.rbs +2 -1
- data/sig/platform.rbs +24 -4
- data/sig/scene.rbs +15 -2
- data/sig/style.rbs +82 -0
- data/sig/text.rbs +35 -0
- data/sig/theme.rbs +64 -0
- data/sig/ui.rbs +390 -0
- data/sig/zaniah.rbs +56 -3
- data/tools/generate_component_gallery.rb +48 -0
- metadata +147 -2
data/lib/zaniah/gpu/vulkan.rb
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require "fiddle/import"
|
|
4
4
|
require_relative "../ffi/library"
|
|
5
|
+
require_relative "instance_packing"
|
|
5
6
|
|
|
6
7
|
module Zaniah
|
|
7
8
|
module GPU
|
|
8
|
-
#
|
|
9
|
-
# It renders a vertex-index triangle into RGBA8 and reads back native memory.
|
|
10
|
-
# Window presentation and general Scene rendering remain Metal/OpenGL work.
|
|
9
|
+
# Offscreen Vulkan renderer and user-supplied SPIR-V validation path.
|
|
11
10
|
class Vulkan
|
|
11
|
+
INSTANCE_STRIDE = InstancePacking::STRIDE
|
|
12
12
|
module Types
|
|
13
13
|
extend Fiddle::Importer
|
|
14
14
|
Instance = struct ["int s_type", "void *next", "unsigned int flags", "void *application", "unsigned int layer_count", "void *layers", "unsigned int extension_count", "void *extensions"]
|
|
@@ -26,11 +26,15 @@ module Zaniah
|
|
|
26
26
|
Shader = struct ["int s_type", "void *next", "unsigned int flags", "size_t code_size", "void *code"]
|
|
27
27
|
Stage = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int stage", "unsigned long long shader", "void *name", "void *specialization"]
|
|
28
28
|
VertexInput = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int binding_count", "void *bindings", "unsigned int attribute_count", "void *attributes"]
|
|
29
|
+
VertexBinding = struct ["unsigned int binding", "unsigned int stride", "int input_rate"]
|
|
30
|
+
VertexAttribute = struct ["unsigned int location", "unsigned int binding", "int format", "unsigned int offset"]
|
|
29
31
|
Assembly = struct ["int s_type", "void *next", "unsigned int flags", "int topology", "unsigned int primitive_restart"]
|
|
30
32
|
Viewport = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int viewport_count", "void *viewports", "unsigned int scissor_count", "void *scissors"]
|
|
31
33
|
Rasterization = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int depth_clamp", "unsigned int discard", "int polygon_mode", "unsigned int cull_mode", "int front_face", "unsigned int depth_bias", "float depth_bias_constant", "float depth_bias_clamp", "float depth_bias_slope", "float line_width"]
|
|
32
34
|
Multisample = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int samples", "unsigned int sample_shading", "float min_sample_shading", "void *sample_mask", "unsigned int alpha_to_coverage", "unsigned int alpha_to_one"]
|
|
33
35
|
Blend = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int logic_enable", "int logic_op", "unsigned int attachment_count", "void *attachments", "float constants[4]"]
|
|
36
|
+
Dynamic = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int count", "void *states"]
|
|
37
|
+
PushConstant = struct ["unsigned int stage_flags", "unsigned int offset", "unsigned int size"]
|
|
34
38
|
Layout = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int set_count", "void *sets", "unsigned int push_count", "void *pushes"]
|
|
35
39
|
Pipeline = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int stage_count", "void *stages", "void *vertex_input", "void *assembly", "void *tessellation", "void *viewport", "void *rasterization", "void *multisample", "void *depth_stencil", "void *blend", "void *dynamic", "unsigned long long layout", "unsigned long long render_pass", "unsigned int subpass", "unsigned long long base_pipeline", "int base_index"]
|
|
36
40
|
CommandPool = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int family"]
|
|
@@ -38,16 +42,28 @@ module Zaniah
|
|
|
38
42
|
CommandBegin = struct ["int s_type", "void *next", "unsigned int flags", "void *inheritance"]
|
|
39
43
|
RenderBegin = struct ["int s_type", "void *next", "unsigned long long render_pass", "unsigned long long framebuffer", "int x", "int y", "unsigned int width", "unsigned int height", "unsigned int clear_count", "void *clears"]
|
|
40
44
|
MemoryBarrier = struct ["int s_type", "void *next", "unsigned int source_access", "unsigned int destination_access"]
|
|
45
|
+
ImageBarrier = struct ["int s_type", "void *next", "unsigned int source_access", "unsigned int destination_access", "int old_layout", "int new_layout", "unsigned int source_family", "unsigned int destination_family", "unsigned long long image", "unsigned int aspect", "unsigned int mip_level", "unsigned int level_count", "unsigned int array_layer", "unsigned int layer_count"]
|
|
41
46
|
Fence = struct ["int s_type", "void *next", "unsigned int flags"]
|
|
42
47
|
Submit = struct ["int s_type", "void *next", "unsigned int wait_count", "void *wait_semaphores", "void *wait_stages", "unsigned int command_count", "void *commands", "unsigned int signal_count", "void *signal_semaphores"]
|
|
48
|
+
DescriptorBinding = struct ["unsigned int binding", "int descriptor_type", "unsigned int count", "unsigned int stage_flags", "void *immutable_samplers"]
|
|
49
|
+
DescriptorLayout = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int count", "void *bindings"]
|
|
50
|
+
DescriptorPoolSize = struct ["int descriptor_type", "unsigned int count"]
|
|
51
|
+
DescriptorPool = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int max_sets", "unsigned int count", "void *sizes"]
|
|
52
|
+
DescriptorAllocation = struct ["int s_type", "void *next", "unsigned long long pool", "unsigned int count", "void *layouts"]
|
|
53
|
+
DescriptorImage = struct ["unsigned long long sampler", "unsigned long long view", "int layout"]
|
|
54
|
+
DescriptorWrite = struct ["int s_type", "void *next", "unsigned long long set", "unsigned int binding", "unsigned int array_element", "unsigned int count", "int descriptor_type", "void *images", "void *buffers", "void *texel_views"]
|
|
55
|
+
Sampler = struct ["int s_type", "void *next", "unsigned int flags", "int mag_filter", "int min_filter", "int mipmap_mode", "int address_u", "int address_v", "int address_w", "float mip_lod_bias", "unsigned int anisotropy", "float max_anisotropy", "unsigned int compare", "int compare_op", "float min_lod", "float max_lod", "int border_color", "unsigned int unnormalized"]
|
|
43
56
|
end
|
|
44
57
|
|
|
45
58
|
P, I, U, Q, V = Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_UINT, -Fiddle::TYPE_LONG_LONG, Fiddle::TYPE_VOID
|
|
46
|
-
attr_reader :width, :height, :pixels, :device_name
|
|
59
|
+
attr_reader :width, :height, :pixels, :device_name, :draw_calls
|
|
47
60
|
|
|
48
61
|
def initialize(width: 128, height: 128)
|
|
49
62
|
raise ArgumentError, "invalid Vulkan image dimensions" unless [width, height].all? { |n| n.is_a?(Integer) && n.positive? } && width * height <= 16_777_216
|
|
50
63
|
@width, @height, @arena, @resources, @memories = width, height, [], [], []
|
|
64
|
+
@native_textures, @scene_pipelines = {}, {}
|
|
65
|
+
@instance_buffer = @instance_memory = @instance_capacity = nil
|
|
66
|
+
@pixels = @draw_calls = nil
|
|
51
67
|
@lib = FFI::Library.new("libvulkan.so.1", "vulkan-1.dll", "libvulkan.1.dylib", "libMoltenVK.dylib")
|
|
52
68
|
output = keep("\0".b * 8)
|
|
53
69
|
check(call(:vkCreateInstance, [P, P, P], I, build(Types::Instance, s_type: 1), 0, output), "vkCreateInstance")
|
|
@@ -76,6 +92,7 @@ module Zaniah
|
|
|
76
92
|
@memory_properties = keep("\0".b * 1024)
|
|
77
93
|
call(:vkGetPhysicalDeviceMemoryProperties, [P, P], V, @physical, @memory_properties)
|
|
78
94
|
create_target
|
|
95
|
+
@arena.clear
|
|
79
96
|
rescue StandardError, LoadError
|
|
80
97
|
release
|
|
81
98
|
raise
|
|
@@ -144,7 +161,18 @@ module Zaniah
|
|
|
144
161
|
@buffer = create("Buffer", build(Types::Buffer, s_type: 12, size: @width * @height * 4, usage: 2))
|
|
145
162
|
@read_memory = allocate_for("Buffer", @buffer, required_flags: 6)
|
|
146
163
|
@pool = create("CommandPool", build(Types::CommandPool, s_type: 39, flags: 2, family: @family))
|
|
147
|
-
|
|
164
|
+
binding = build(Types::DescriptorBinding, descriptor_type: 1, count: 1, stage_flags: 16)
|
|
165
|
+
@descriptor_layout = create("DescriptorSetLayout",
|
|
166
|
+
build(Types::DescriptorLayout, s_type: 32, count: 1, bindings: binding))
|
|
167
|
+
pool_size = build(Types::DescriptorPoolSize, descriptor_type: 1, count: 4096)
|
|
168
|
+
@descriptor_pool = create("DescriptorPool",
|
|
169
|
+
build(Types::DescriptorPool, s_type: 33, max_sets: 4096, count: 1, sizes: pool_size))
|
|
170
|
+
@sampler = create("Sampler", build(Types::Sampler, s_type: 31, mag_filter: 0, min_filter: 0,
|
|
171
|
+
mipmap_mode: 0, address_u: 2, address_v: 2, address_w: 2, max_lod: 1.0))
|
|
172
|
+
push = build(Types::PushConstant, stage_flags: 1, size: 8)
|
|
173
|
+
@layout = create("PipelineLayout", build(Types::Layout, s_type: 30,
|
|
174
|
+
set_count: 1, sets: keep([@descriptor_layout].pack("Q")), push_count: 1, pushes: push))
|
|
175
|
+
@white = Texture.new(1, 1, data: "\xff".b * 4)
|
|
148
176
|
end
|
|
149
177
|
|
|
150
178
|
def shader(bytes)
|
|
@@ -153,62 +181,277 @@ module Zaniah
|
|
|
153
181
|
end
|
|
154
182
|
create("ShaderModule", build(Types::Shader, s_type: 16, code_size: bytes.bytesize, code: keep(bytes)))
|
|
155
183
|
end
|
|
156
|
-
def pipeline(vertex_spirv, fragment_spirv)
|
|
157
|
-
|
|
158
|
-
|
|
184
|
+
def pipeline(vertex_spirv, fragment_spirv, topology: 3, instanced: false)
|
|
185
|
+
vertex_shader, fragment_shader = shader(vertex_spirv), shader(fragment_spirv)
|
|
186
|
+
vertex = build(Types::Stage, s_type: 18, stage: 1, shader: vertex_shader, name: keep("main\0"))
|
|
187
|
+
fragment = build(Types::Stage, s_type: 18, stage: 16, shader: fragment_shader, name: keep("main\0"))
|
|
159
188
|
stages = keep(vertex.to_ptr[0, Types::Stage.size] + fragment.to_ptr[0, Types::Stage.size])
|
|
160
|
-
vertex_input =
|
|
161
|
-
|
|
189
|
+
vertex_input = if instanced
|
|
190
|
+
binding = build(Types::VertexBinding, stride: INSTANCE_STRIDE * 4, input_rate: 1)
|
|
191
|
+
attributes = 10.times.map do |location|
|
|
192
|
+
value = build(Types::VertexAttribute, location: location, format: 109, offset: location * 16)
|
|
193
|
+
value.to_ptr[0, Types::VertexAttribute.size]
|
|
194
|
+
end.join
|
|
195
|
+
build(Types::VertexInput, s_type: 19, binding_count: 1, bindings: binding,
|
|
196
|
+
attribute_count: 10, attributes: keep(attributes))
|
|
197
|
+
else
|
|
198
|
+
build(Types::VertexInput, s_type: 19)
|
|
199
|
+
end
|
|
200
|
+
assembly = build(Types::Assembly, s_type: 20, topology: topology)
|
|
162
201
|
viewport = keep([0, 0, @width, @height, 0, 1].pack("f6"))
|
|
163
202
|
scissor = keep([0, 0, @width, @height].pack("i2I2"))
|
|
164
203
|
viewport_state = build(Types::Viewport, s_type: 22, viewport_count: 1, viewports: viewport, scissor_count: 1, scissors: scissor)
|
|
165
204
|
raster = build(Types::Rasterization, s_type: 23, front_face: 1, line_width: 1.0)
|
|
166
205
|
multisample = build(Types::Multisample, s_type: 24, samples: 1)
|
|
167
|
-
blend_attachment = keep([
|
|
206
|
+
blend_attachment = keep([1, 1, 7, 0, 1, 7, 0, 15].pack("I8"))
|
|
168
207
|
blend = build(Types::Blend, s_type: 26, attachment_count: 1, attachments: blend_attachment)
|
|
208
|
+
dynamic = build(Types::Dynamic, s_type: 27, count: 1, states: keep([1].pack("I")))
|
|
169
209
|
info = build(Types::Pipeline, s_type: 28, stage_count: 2, stages: stages,
|
|
170
210
|
vertex_input: vertex_input, assembly: assembly, viewport: viewport_state,
|
|
171
|
-
rasterization: raster, multisample: multisample, blend: blend, layout: @layout,
|
|
211
|
+
rasterization: raster, multisample: multisample, blend: blend, dynamic: dynamic, layout: @layout,
|
|
172
212
|
render_pass: @render_pass, base_index: -1)
|
|
173
213
|
output = keep("\0".b * 8)
|
|
174
214
|
check(call(:vkCreateGraphicsPipelines, [P, Q, U, P, P, P], I, @device, 0, 1, info, 0, output), "vkCreateGraphicsPipelines")
|
|
175
215
|
result = output.unpack1("Q")
|
|
176
216
|
@resources << [:vkDestroyPipeline, result]
|
|
177
217
|
result
|
|
218
|
+
ensure
|
|
219
|
+
destroy_resource(:vkDestroyShaderModule, vertex_shader) if vertex_shader
|
|
220
|
+
destroy_resource(:vkDestroyShaderModule, fragment_shader) if fragment_shader
|
|
178
221
|
end
|
|
179
222
|
|
|
180
|
-
def
|
|
223
|
+
def render(scene, clear: "#0000")
|
|
181
224
|
raise IOError, "Vulkan device released" unless @device
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
225
|
+
arena_start = @arena.length
|
|
226
|
+
bytes, batches = InstancePacking.pack(scene)
|
|
227
|
+
command = begin_command
|
|
228
|
+
submitted = false
|
|
229
|
+
transients = []
|
|
230
|
+
descriptors = batches.map { |(_kind, texture, _clip), _first, _count| prepare_texture(texture || @white, command, transients) }
|
|
231
|
+
upload_instances(bytes) unless bytes.empty?
|
|
232
|
+
render_pass(command, clear)
|
|
233
|
+
@draw_calls = 0
|
|
234
|
+
unless bytes.empty?
|
|
235
|
+
call(:vkCmdBindVertexBuffers, [P, U, U, P, P], V, command, 0, 1,
|
|
236
|
+
keep([@instance_buffer].pack("Q")), keep([0].pack("Q")))
|
|
237
|
+
call(:vkCmdPushConstants, [P, Q, U, U, U, P], V, command, @layout, 1, 0, 8,
|
|
238
|
+
keep([@width.to_f, @height.to_f].pack("f2")))
|
|
239
|
+
batches.each_with_index do |((kind, _texture, clip), first, count), index|
|
|
240
|
+
scissor = scissor_bytes(clip)
|
|
241
|
+
next unless scissor
|
|
242
|
+
call(:vkCmdSetScissor, [P, U, U, P], V, command, 0, 1, keep(scissor))
|
|
243
|
+
pipeline_handle = scene_pipeline(kind)
|
|
244
|
+
call(:vkCmdBindPipeline, [P, I, Q], V, command, 0, pipeline_handle)
|
|
245
|
+
descriptor = descriptors[index]
|
|
246
|
+
call(:vkCmdBindDescriptorSets, [P, I, Q, U, U, P, U, P], V,
|
|
247
|
+
command, 0, @layout, 0, 1, keep([descriptor].pack("Q")), 0, 0)
|
|
248
|
+
vertices = kind == :triangle ? 3 : 4
|
|
249
|
+
call(:vkCmdDraw, [P, U, U, U, U], V, command, vertices, count, 0, first)
|
|
250
|
+
@draw_calls += 1
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
call(:vkCmdEndRenderPass, [P], V, command)
|
|
254
|
+
copy_target(command)
|
|
255
|
+
submitted = true
|
|
256
|
+
@pixels = submit(command, read: true)
|
|
257
|
+
ensure
|
|
258
|
+
free_command(command) if command && !submitted
|
|
259
|
+
transients&.each { |buffer, memory| destroy_buffer_memory(buffer, memory) }
|
|
260
|
+
@arena&.slice!(arena_start, @arena.length - arena_start) if arena_start
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def resize(width, height)
|
|
264
|
+
width, height = Integer(width), Integer(height)
|
|
265
|
+
raise ArgumentError, "invalid Vulkan image dimensions" unless [width, height].all?(&:positive?) && width * height <= 16_777_216
|
|
266
|
+
return if width == @width && height == @height
|
|
267
|
+
release
|
|
268
|
+
initialize(width: width, height: height)
|
|
269
|
+
end
|
|
270
|
+
def create_texture(width, height, **options) = Texture.new(width, height, **options)
|
|
271
|
+
def create_buffer(size, **options) = Buffer.new(size, **options)
|
|
272
|
+
def create_pipeline(shader:, blend: :premultiplied) = Pipeline.new(shader, blend)
|
|
273
|
+
def begin_frame(clear: "#0000") = FrameEncoder.new(self, clear)
|
|
274
|
+
|
|
275
|
+
def scene_pipeline(kind)
|
|
276
|
+
topology = kind == :triangle ? 3 : 4
|
|
277
|
+
(@scene_pipelines ||= {})[topology] ||= begin
|
|
278
|
+
directory = File.join(__dir__, "vulkan")
|
|
279
|
+
pipeline(File.binread(File.join(directory, "scene.vert.spv")),
|
|
280
|
+
File.binread(File.join(directory, "scene.frag.spv")), topology: topology, instanced: true)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def begin_command
|
|
185
285
|
output = keep("\0".b * 8)
|
|
186
286
|
info = build(Types::CommandAllocation, s_type: 40, pool: @pool, level: 0, count: 1)
|
|
187
287
|
check(call(:vkAllocateCommandBuffers, [P, P, P], I, @device, info, output), "vkAllocateCommandBuffers")
|
|
188
288
|
command = output.unpack1("J")
|
|
189
|
-
check(call(:vkBeginCommandBuffer, [P, P], I, command,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
289
|
+
check(call(:vkBeginCommandBuffer, [P, P], I, command,
|
|
290
|
+
build(Types::CommandBegin, s_type: 42, flags: 1)), "vkBeginCommandBuffer")
|
|
291
|
+
command
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def render_pass(command, clear)
|
|
295
|
+
color = Color.parse(clear).premultiplied
|
|
296
|
+
info = build(Types::RenderBegin, s_type: 43, render_pass: @render_pass, framebuffer: @framebuffer,
|
|
297
|
+
width: @width, height: @height, clear_count: 1, clears: keep(color.pack("f4")))
|
|
298
|
+
call(:vkCmdBeginRenderPass, [P, P, I], V, command, info, 0)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def upload_instances(bytes)
|
|
302
|
+
if !@instance_buffer || @instance_capacity < bytes.bytesize
|
|
303
|
+
destroy_buffer_memory(@instance_buffer, @instance_memory) if @instance_buffer
|
|
304
|
+
@instance_capacity = [bytes.bytesize, 4096].max
|
|
305
|
+
@instance_buffer = create("Buffer", build(Types::Buffer, s_type: 12,
|
|
306
|
+
size: @instance_capacity, usage: 0x80))
|
|
307
|
+
@instance_memory = allocate_for("Buffer", @instance_buffer, required_flags: 6)
|
|
308
|
+
end
|
|
309
|
+
output = keep("\0".b * 8)
|
|
310
|
+
check(call(:vkMapMemory, [P, Q, Q, Q, U, P], I, @device, @instance_memory,
|
|
311
|
+
0, bytes.bytesize, 0, output), "vkMapMemory")
|
|
312
|
+
Fiddle::Pointer.new(output.unpack1("J"))[0, bytes.bytesize] = bytes
|
|
313
|
+
call(:vkUnmapMemory, [P, Q], V, @device, @instance_memory)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def prepare_texture(texture, command, transients)
|
|
317
|
+
entry = @native_textures[texture]
|
|
318
|
+
return entry[:descriptor] if entry && entry[:revision] == texture.revision
|
|
319
|
+
if entry
|
|
320
|
+
destroy_resource(:vkDestroyImageView, entry[:view])
|
|
321
|
+
destroy_resource(:vkDestroyImage, entry[:image])
|
|
322
|
+
free_memory(entry[:memory])
|
|
323
|
+
else
|
|
324
|
+
output = keep("\0".b * 8)
|
|
325
|
+
info = build(Types::DescriptorAllocation, s_type: 34, pool: @descriptor_pool,
|
|
326
|
+
count: 1, layouts: keep([@descriptor_layout].pack("Q")))
|
|
327
|
+
check(call(:vkAllocateDescriptorSets, [P, P, P], I, @device, info, output), "vkAllocateDescriptorSets")
|
|
328
|
+
entry = {descriptor: output.unpack1("Q")}
|
|
329
|
+
@native_textures[texture] = entry
|
|
330
|
+
end
|
|
331
|
+
entry[:image] = create("Image", build(Types::Image, s_type: 14, image_type: 1,
|
|
332
|
+
format: texture.format == :r8 ? 9 : 37, width: texture.width, height: texture.height,
|
|
333
|
+
depth: 1, mip_levels: 1, layers: 1, samples: 1, usage: 6))
|
|
334
|
+
entry[:memory] = allocate_for("Image", entry[:image], required_flags: 0)
|
|
335
|
+
entry[:view] = create("ImageView", build(Types::ImageView, s_type: 15, image: entry[:image],
|
|
336
|
+
view_type: 1, format: texture.format == :r8 ? 9 : 37, aspect: 1, level_count: 1, layer_count: 1))
|
|
337
|
+
staging = create("Buffer", build(Types::Buffer, s_type: 12, size: texture.data.bytesize, usage: 1))
|
|
338
|
+
staging_memory = allocate_for("Buffer", staging, required_flags: 6)
|
|
339
|
+
upload_memory(staging_memory, texture.data)
|
|
340
|
+
transition_image(command, entry[:image], 0, 7, 0, 0x1000, 1, 0x1000)
|
|
341
|
+
region = keep([0, 0, 0, 1, 0, 0, 1, 0, 0, 0, texture.width, texture.height, 1].pack("Q I2 I4 i3 I3"))
|
|
342
|
+
call(:vkCmdCopyBufferToImage, [P, Q, Q, I, U, P], V,
|
|
343
|
+
command, staging, entry[:image], 7, 1, region)
|
|
344
|
+
transition_image(command, entry[:image], 7, 5, 0x1000, 0x20, 0x1000, 0x80)
|
|
345
|
+
image = build(Types::DescriptorImage, sampler: @sampler, view: entry[:view], layout: 5)
|
|
346
|
+
write = build(Types::DescriptorWrite, s_type: 35, set: entry[:descriptor], count: 1,
|
|
347
|
+
descriptor_type: 1, images: image)
|
|
348
|
+
call(:vkUpdateDescriptorSets, [P, U, P, U, P], V, @device, 1, write, 0, 0)
|
|
349
|
+
entry[:revision] = texture.revision
|
|
350
|
+
transients << [staging, staging_memory]
|
|
351
|
+
entry[:descriptor]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def upload_memory(memory, bytes)
|
|
355
|
+
output = keep("\0".b * 8)
|
|
356
|
+
check(call(:vkMapMemory, [P, Q, Q, Q, U, P], I, @device, memory,
|
|
357
|
+
0, bytes.bytesize, 0, output), "vkMapMemory")
|
|
358
|
+
Fiddle::Pointer.new(output.unpack1("J"))[0, bytes.bytesize] = bytes
|
|
359
|
+
call(:vkUnmapMemory, [P, Q], V, @device, memory)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def transition_image(command, image, old_layout, new_layout, source_access, destination_access, source_stage, destination_stage)
|
|
363
|
+
barrier = build(Types::ImageBarrier, s_type: 45, source_access: source_access,
|
|
364
|
+
destination_access: destination_access, old_layout: old_layout, new_layout: new_layout,
|
|
365
|
+
source_family: 0xffffffff, destination_family: 0xffffffff, image: image,
|
|
366
|
+
aspect: 1, level_count: 1, layer_count: 1)
|
|
367
|
+
call(:vkCmdPipelineBarrier, [P, U, U, U, U, P, U, P, U, P], V,
|
|
368
|
+
command, source_stage, destination_stage, 0, 0, 0, 0, 0, 1, barrier)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def scissor_bytes(clip)
|
|
372
|
+
return [0, 0, @width, @height].pack("i2I2") unless clip
|
|
373
|
+
left, top = [clip.x.floor, 0].max, [clip.y.floor, 0].max
|
|
374
|
+
right, bottom = [clip.right.ceil, @width].min, [clip.bottom.ceil, @height].min
|
|
375
|
+
return if left >= right || top >= bottom
|
|
376
|
+
[left, top, right - left, bottom - top].pack("i2I2")
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def copy_target(command)
|
|
196
380
|
region = keep([0, 0, 0, 1, 0, 0, 1, 0, 0, 0, @width, @height, 1].pack("Q I2 I4 i3 I3"))
|
|
197
|
-
call(:vkCmdCopyImageToBuffer, [P, Q, I, Q, U, P], V,
|
|
198
|
-
|
|
199
|
-
|
|
381
|
+
call(:vkCmdCopyImageToBuffer, [P, Q, I, Q, U, P], V,
|
|
382
|
+
command, @image, 6, @buffer, 1, region)
|
|
383
|
+
barrier = build(Types::MemoryBarrier, s_type: 46,
|
|
384
|
+
source_access: 0x1000, destination_access: 0x2000)
|
|
385
|
+
call(:vkCmdPipelineBarrier, [P, U, U, U, U, P, U, P, U, P], V,
|
|
386
|
+
command, 0x1000, 0x4000, 0, 1, barrier, 0, 0, 0, 0)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def submit(command, read: false)
|
|
200
390
|
check(call(:vkEndCommandBuffer, [P], I, command), "vkEndCommandBuffer")
|
|
201
391
|
fence = create("Fence", build(Types::Fence, s_type: 8))
|
|
202
|
-
|
|
203
|
-
check(call(:vkQueueSubmit, [P, U, P, Q], I, @queue, 1,
|
|
204
|
-
check(call(:vkWaitForFences, [P, U, P, U, Q], I, @device, 1,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
392
|
+
info = build(Types::Submit, s_type: 4, command_count: 1, commands: keep([command].pack("J")))
|
|
393
|
+
check(call(:vkQueueSubmit, [P, U, P, Q], I, @queue, 1, info, fence), "vkQueueSubmit")
|
|
394
|
+
check(call(:vkWaitForFences, [P, U, P, U, Q], I, @device, 1,
|
|
395
|
+
keep([fence].pack("Q")), 1, 10_000_000_000), "vkWaitForFences")
|
|
396
|
+
read ? read_target : nil
|
|
397
|
+
ensure
|
|
398
|
+
destroy_resource(:vkDestroyFence, fence) if fence
|
|
399
|
+
free_command(command)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def free_command(command)
|
|
403
|
+
call(:vkFreeCommandBuffers, [P, Q, U, P], V,
|
|
404
|
+
@device, @pool, 1, [command].pack("J")) if command && @device
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def read_target
|
|
408
|
+
output = keep("\0".b * 8)
|
|
409
|
+
check(call(:vkMapMemory, [P, Q, Q, Q, U, P], I, @device, @read_memory,
|
|
410
|
+
0, @width * @height * 4, 0, output), "vkMapMemory")
|
|
411
|
+
bytes = Fiddle::Pointer.new(output.unpack1("J"))[0, @width * @height * 4]
|
|
412
|
+
bytes.bytes.each_slice(4).flat_map do |red, green, blue, alpha|
|
|
413
|
+
alpha.zero? ? [0, 0, 0, 0] : [[red * 255 / alpha, 255].min,
|
|
414
|
+
[green * 255 / alpha, 255].min, [blue * 255 / alpha, 255].min, alpha]
|
|
415
|
+
end.pack("C*")
|
|
416
|
+
ensure
|
|
417
|
+
call(:vkUnmapMemory, [P, Q], V, @device, @read_memory) if output&.unpack1("J").to_i.positive?
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def destroy_resource(name, handle)
|
|
421
|
+
return unless handle
|
|
422
|
+
call(name, [P, Q, P], V, @device, handle, 0)
|
|
423
|
+
@resources.delete_if { |_function, value| value == handle }
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def free_memory(memory)
|
|
427
|
+
return unless memory
|
|
428
|
+
call(:vkFreeMemory, [P, Q, P], V, @device, memory, 0)
|
|
429
|
+
@memories.delete(memory)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def destroy_buffer_memory(buffer, memory)
|
|
433
|
+
destroy_resource(:vkDestroyBuffer, buffer)
|
|
434
|
+
free_memory(memory)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def render_triangle(vertex_spirv:, fragment_spirv:)
|
|
438
|
+
raise IOError, "Vulkan device released" unless @device
|
|
439
|
+
arena_start = @arena.length
|
|
440
|
+
pipeline_handle = pipeline(vertex_spirv, fragment_spirv)
|
|
441
|
+
command = begin_command
|
|
442
|
+
submitted = false
|
|
443
|
+
render_pass(command, "#000f")
|
|
444
|
+
call(:vkCmdSetScissor, [P, U, U, P], V, command, 0, 1, keep(scissor_bytes(nil)))
|
|
445
|
+
call(:vkCmdBindPipeline, [P, I, Q], V, command, 0, pipeline_handle)
|
|
446
|
+
call(:vkCmdDraw, [P, U, U, U, U], V, command, 3, 1, 0, 0)
|
|
447
|
+
call(:vkCmdEndRenderPass, [P], V, command)
|
|
448
|
+
copy_target(command)
|
|
449
|
+
submitted = true
|
|
450
|
+
@pixels = submit(command, read: true)
|
|
451
|
+
ensure
|
|
452
|
+
free_command(command) if command && !submitted
|
|
453
|
+
destroy_resource(:vkDestroyPipeline, pipeline_handle) if pipeline_handle
|
|
454
|
+
@arena&.slice!(arena_start, @arena.length - arena_start) if arena_start
|
|
212
455
|
end
|
|
213
456
|
def write_png(path)
|
|
214
457
|
raise Error, "render a Vulkan frame before capture" unless @pixels
|
|
@@ -227,6 +470,9 @@ module Zaniah
|
|
|
227
470
|
@resources&.clear
|
|
228
471
|
@memories&.clear
|
|
229
472
|
@arena&.clear
|
|
473
|
+
@native_textures&.clear
|
|
474
|
+
@scene_pipelines&.clear
|
|
475
|
+
@instance_buffer = @instance_memory = @instance_capacity = nil
|
|
230
476
|
end
|
|
231
477
|
end
|
|
232
478
|
end
|
data/lib/zaniah/gpu.rb
CHANGED
|
@@ -9,10 +9,15 @@ require_relative "gpu/software"
|
|
|
9
9
|
module Zaniah
|
|
10
10
|
module GPU
|
|
11
11
|
def self.create(window = nil, backend: :software, width: nil, height: nil)
|
|
12
|
-
return window.create_device(backend) if window && backend != :software
|
|
13
|
-
raise ArgumentError, "backend requires a native window" unless backend == :software
|
|
14
12
|
size = window&.content_size
|
|
15
|
-
|
|
13
|
+
return window.create_device(backend) if window && !%i[software vulkan].include?(backend)
|
|
14
|
+
dimensions = [(width || size&.width || 800).to_i, (height || size&.height || 600).to_i]
|
|
15
|
+
return Software.new(*dimensions) if backend == :software
|
|
16
|
+
if backend == :vulkan
|
|
17
|
+
require_relative "gpu/vulkan"
|
|
18
|
+
return Vulkan.new(width: dimensions[0], height: dimensions[1])
|
|
19
|
+
end
|
|
20
|
+
raise ArgumentError, "backend requires a native window"
|
|
16
21
|
end
|
|
17
22
|
end
|
|
18
23
|
end
|
|
@@ -5,34 +5,90 @@ module Zaniah
|
|
|
5
5
|
Hit = Data.define(:bounds, :owner)
|
|
6
6
|
|
|
7
7
|
class Dispatcher
|
|
8
|
-
attr_reader :focused
|
|
8
|
+
attr_reader :focused, :focus_origin, :focus_tree
|
|
9
9
|
|
|
10
|
-
def initialize(keymap: Keymap.
|
|
10
|
+
def initialize(keymap: Keymap.default_ui)
|
|
11
11
|
@keymap, @hits = keymap, []
|
|
12
|
+
@transforms = [Transform.identity]
|
|
13
|
+
@focus_tree = FocusTree.new
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
def focus(handle)
|
|
15
|
-
return
|
|
16
|
+
def focus(handle, origin: :programmatic)
|
|
17
|
+
return unless handle.nil? || handle.focusable
|
|
18
|
+
@focus_tree.register(handle) if handle
|
|
19
|
+
if @focused == handle
|
|
20
|
+
@focus_origin = origin
|
|
21
|
+
return
|
|
22
|
+
end
|
|
16
23
|
@focused&.on_focus&.call(false)
|
|
17
|
-
@focused = handle
|
|
24
|
+
@focused, @focus_origin = handle, origin
|
|
18
25
|
handle&.on_focus&.call(true)
|
|
26
|
+
reveal(handle)
|
|
19
27
|
end
|
|
20
28
|
|
|
29
|
+
def focus_visible? = @focus_origin == :keyboard
|
|
30
|
+
|
|
21
31
|
def key(key)
|
|
22
32
|
chain = @focused ? @focused.ancestors : []
|
|
23
33
|
context = chain.reverse.each_with_object({}) { |handle, result| result.merge!(handle.context) }
|
|
24
34
|
action = @keymap.dispatch(key, context: context)
|
|
25
35
|
return action if action.nil? || action == :pending
|
|
36
|
+
target = case action
|
|
37
|
+
when :focus_next then @focus_tree.next(@focused)
|
|
38
|
+
when :focus_previous then @focus_tree.previous(@focused)
|
|
39
|
+
when :focus_left then @focus_tree.spatial(@focused, :left)
|
|
40
|
+
when :focus_right then @focus_tree.spatial(@focused, :right)
|
|
41
|
+
when :focus_up then @focus_tree.spatial(@focused, :up)
|
|
42
|
+
when :focus_down then @focus_tree.spatial(@focused, :down)
|
|
43
|
+
end
|
|
44
|
+
if target
|
|
45
|
+
focus(target, origin: :keyboard)
|
|
46
|
+
return action
|
|
47
|
+
end
|
|
26
48
|
chain.each { |handle| break if handle.on_action&.call(action) }
|
|
27
49
|
action
|
|
28
50
|
end
|
|
29
51
|
|
|
30
|
-
def
|
|
31
|
-
|
|
52
|
+
def register_focus(handle)
|
|
53
|
+
owner = handle.owner&.respond_to?(:parent) ? handle.owner.parent : nil
|
|
54
|
+
while owner
|
|
55
|
+
if owner.respond_to?(:focus_handle) && (parent = owner.focus_handle)
|
|
56
|
+
unless parent.equal?(handle)
|
|
57
|
+
handle.parent = parent
|
|
58
|
+
break
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
owner = owner.respond_to?(:parent) ? owner.parent : nil
|
|
62
|
+
end
|
|
63
|
+
@focus_tree.register(handle)
|
|
64
|
+
end
|
|
32
65
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
@
|
|
66
|
+
def input(event)
|
|
67
|
+
return false unless @focused
|
|
68
|
+
@focused.ancestors.each { |handle| return true if handle.on_input&.call(event) }
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def clear_hits
|
|
73
|
+
@hits.clear
|
|
74
|
+
@focus_tree.clear
|
|
75
|
+
@transforms.replace([Transform.identity])
|
|
76
|
+
end
|
|
77
|
+
def hits = @hits.map { |bounds, _, owner, _, clip| Hit.new(clip ? bounds.intersect(clip) : bounds, owner) }.freeze
|
|
78
|
+
|
|
79
|
+
def hover_chain(position)
|
|
80
|
+
return [] unless position
|
|
81
|
+
owner = @hits.reverse_each.find { |hit| hit_contains?(hit, position) }&.[](2)
|
|
82
|
+
[].tap do |chain|
|
|
83
|
+
while owner
|
|
84
|
+
chain << owner
|
|
85
|
+
owner = owner.respond_to?(:parent) ? owner.parent : nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def hit(bounds, clip: nil, owner: nil, transform: @transforms.last, &handler)
|
|
91
|
+
@hits << [bounds, handler, owner, transform, clip || @clip]
|
|
36
92
|
end
|
|
37
93
|
|
|
38
94
|
def clip(bounds)
|
|
@@ -43,20 +99,48 @@ module Zaniah
|
|
|
43
99
|
@clip = previous
|
|
44
100
|
end
|
|
45
101
|
|
|
102
|
+
def push_transform(transform)
|
|
103
|
+
raise ArgumentError, "expected a Transform" unless transform.is_a?(Transform)
|
|
104
|
+
@transforms << @transforms.last.compose(transform)
|
|
105
|
+
pushed = true
|
|
106
|
+
yield
|
|
107
|
+
ensure
|
|
108
|
+
@transforms.pop if pushed
|
|
109
|
+
end
|
|
110
|
+
|
|
46
111
|
def mouse(event)
|
|
47
112
|
if @capture
|
|
48
113
|
@capture.call(event)
|
|
49
114
|
@capture = nil if event.is_a?(MouseUp)
|
|
50
115
|
return true
|
|
51
116
|
end
|
|
52
|
-
@hits.reverse_each do |
|
|
53
|
-
next unless
|
|
117
|
+
@hits.reverse_each do |hit|
|
|
118
|
+
next unless hit_contains?(hit, event.position)
|
|
119
|
+
handler = hit[1]
|
|
54
120
|
handled = handler.call(event)
|
|
55
121
|
@capture = handler if handled == :capture && event.is_a?(MouseDown)
|
|
56
122
|
return true if handled
|
|
57
123
|
end
|
|
58
124
|
false
|
|
59
125
|
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def hit_contains?(hit, position)
|
|
130
|
+
bounds, _, _, transform, clip = hit
|
|
131
|
+
return false if clip && !clip.contains?(position)
|
|
132
|
+
bounds.contains?(transform.inverse.apply(position))
|
|
133
|
+
rescue ArgumentError
|
|
134
|
+
false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def reveal(handle)
|
|
138
|
+
owner, bounds = handle&.owner, handle&.bounds
|
|
139
|
+
while owner
|
|
140
|
+
owner.scroll_to(bounds, align: :nearest) if bounds && owner.is_a?(ScrollView)
|
|
141
|
+
owner = owner.respond_to?(:parent) ? owner.parent : nil
|
|
142
|
+
end
|
|
143
|
+
end
|
|
60
144
|
end
|
|
61
145
|
end
|
|
62
146
|
end
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
module Zaniah
|
|
4
4
|
module Input
|
|
5
5
|
class FocusHandle
|
|
6
|
-
attr_reader :parent, :children, :context
|
|
7
|
-
attr_accessor :on_action, :on_focus
|
|
6
|
+
attr_reader :parent, :children, :context, :owner
|
|
7
|
+
attr_accessor :on_action, :on_focus, :on_input, :tab_index, :focusable, :bounds
|
|
8
8
|
|
|
9
|
-
def initialize(parent: nil, context: {})
|
|
10
|
-
@parent, @context, @children = parent, context, []
|
|
9
|
+
def initialize(parent: nil, context: {}, owner: nil, tab_index: nil, focusable: true, bounds: nil)
|
|
10
|
+
@parent, @context, @owner, @children = parent, context, owner, []
|
|
11
|
+
@tab_index, @focusable, @bounds = tab_index, focusable, bounds
|
|
11
12
|
parent&.children&.push(self)
|
|
12
13
|
end
|
|
13
14
|
|
|
@@ -19,6 +20,14 @@ module Zaniah
|
|
|
19
20
|
end
|
|
20
21
|
list
|
|
21
22
|
end
|
|
23
|
+
|
|
24
|
+
def parent=(value)
|
|
25
|
+
raise ArgumentError, "focus handle cannot parent itself" if value.equal?(self)
|
|
26
|
+
return if @parent.equal?(value)
|
|
27
|
+
@parent&.children&.delete(self)
|
|
28
|
+
@parent = value
|
|
29
|
+
value&.children&.push(self) unless value&.children&.include?(self)
|
|
30
|
+
end
|
|
22
31
|
end
|
|
23
32
|
end
|
|
24
33
|
end
|