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,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../ffi/library"
|
|
4
|
+
require_relative "../file_event"
|
|
5
|
+
|
|
6
|
+
module Zaniah
|
|
7
|
+
module Platform
|
|
8
|
+
module Linux
|
|
9
|
+
class Watcher
|
|
10
|
+
I, P = Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP
|
|
11
|
+
def initialize(paths, latency: 0.05)
|
|
12
|
+
@lib = FFI::Library.new(nil)
|
|
13
|
+
fd = @lib.fn(:inotify_init1, [I], I).call(0x800 | 0x80000)
|
|
14
|
+
raise SystemCallError.new("inotify_init1", Fiddle.last_error) if fd.negative?
|
|
15
|
+
@io, @directories, @moves, @closed = IO.for_fd(fd), {}, {}, false
|
|
16
|
+
paths = Array(paths).map { |path| File.expand_path(path) }
|
|
17
|
+
raise ArgumentError, "watch paths required" if paths.empty?
|
|
18
|
+
paths.each { |path| add_tree(File.directory?(path) ? path : File.dirname(path)) }
|
|
19
|
+
end
|
|
20
|
+
def add_tree(path)
|
|
21
|
+
([path] + Dir.glob(File.join(path, "**", "*"), File::FNM_DOTMATCH).select { |entry| File.basename(entry) != "." && File.directory?(entry) && !File.symlink?(entry) }).each do |directory|
|
|
22
|
+
descriptor = @lib.fn(:inotify_add_watch, [I, P, I], I).call(@io.fileno, directory, 0x00000fff)
|
|
23
|
+
raise SystemCallError.new("inotify_add_watch #{directory}", Fiddle.last_error) if descriptor.negative?
|
|
24
|
+
@directories[descriptor] = directory
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
def poll(timeout: 0)
|
|
28
|
+
return [] if @closed || !IO.select([@io], nil, nil, timeout)
|
|
29
|
+
events = []
|
|
30
|
+
while (bytes = @io.read_nonblock(65_536, exception: false)).is_a?(String)
|
|
31
|
+
offset = 0
|
|
32
|
+
while offset + 16 <= bytes.bytesize
|
|
33
|
+
descriptor, mask, cookie, length = bytes.byteslice(offset, 16).unpack("iIII")
|
|
34
|
+
name = bytes.byteslice(offset + 16, length).split("\0", 2).first.to_s
|
|
35
|
+
offset += 16 + length
|
|
36
|
+
if (mask & 0x4000) != 0
|
|
37
|
+
events << FileEvent.new(:overflow, nil, nil)
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
directory = @directories[descriptor]
|
|
41
|
+
next unless directory
|
|
42
|
+
path = name.empty? ? directory : File.join(directory, name)
|
|
43
|
+
if (mask & 0x8000) != 0
|
|
44
|
+
@directories.delete(descriptor)
|
|
45
|
+
elsif (mask & 0x40) != 0
|
|
46
|
+
@moves[cookie] = path
|
|
47
|
+
events << FileEvent.new(:deleted, path, nil)
|
|
48
|
+
elsif (mask & 0x80) != 0
|
|
49
|
+
old = @moves.delete(cookie)
|
|
50
|
+
events << FileEvent.new(old ? :renamed : :created, path, old)
|
|
51
|
+
elsif (mask & 0x300) != 0
|
|
52
|
+
events << FileEvent.new((mask & 0x200) != 0 ? :deleted : :created, path, nil)
|
|
53
|
+
elsif (mask & (2 | 4 | 8 | 0x400 | 0x800)) != 0
|
|
54
|
+
events << FileEvent.new(:modified, path, nil)
|
|
55
|
+
end
|
|
56
|
+
add_tree(path) if (mask & 0x40000000) != 0 && (mask & 0x180) != 0 && File.directory?(path)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
@moves.clear
|
|
60
|
+
events.uniq
|
|
61
|
+
end
|
|
62
|
+
def close
|
|
63
|
+
return if @closed
|
|
64
|
+
@closed = true
|
|
65
|
+
@io.close
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../ffi/wayland"
|
|
4
|
+
|
|
5
|
+
module Zaniah
|
|
6
|
+
module Platform
|
|
7
|
+
module Linux
|
|
8
|
+
module WaylandDisplayDiscovery
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def read
|
|
12
|
+
ptype, int, uint = Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_UINT
|
|
13
|
+
connection, outputs, manager = FFI::Wayland.new, {}, nil
|
|
14
|
+
registry = connection.request(connection.display, 1, 0, new_interface: "wl_registry", version: 1)
|
|
15
|
+
connection.listen(registry, [
|
|
16
|
+
[[uint, ptype, uint], ->(_registry, id, name, version) do
|
|
17
|
+
name = name.to_s
|
|
18
|
+
if name == "wl_output"
|
|
19
|
+
output = connection.request(registry, 0, id, name, [version, 2].min, 0, new_interface: name, version: [version, 2].min)
|
|
20
|
+
state = outputs[id] = {proxy: output, x: 0, y: 0, width: 0, height: 0, scale: 1, name: "Display #{id}"}
|
|
21
|
+
connection.listen(output, [
|
|
22
|
+
[[int, int, int, int, int, ptype, ptype, int], ->(_output, x, y, _mw, _mh, _subpixel, make, model, transform) { state.merge!(x: x, y: y, name: "#{make.to_s} #{model.to_s}", transform: transform) }],
|
|
23
|
+
[[uint, int, int, int], ->(_output, flags, width, height, _refresh) { state.merge!(width: width, height: height) if (flags & 1) != 0 }],
|
|
24
|
+
[[], nil], [[int], ->(_output, scale) { state[:scale] = [scale, 1].max }]
|
|
25
|
+
])
|
|
26
|
+
elsif name == "zxdg_output_manager_v1"
|
|
27
|
+
manager = connection.request(registry, 0, id, name, [version, 3].min, 0, new_interface: name, version: [version, 3].min)
|
|
28
|
+
end
|
|
29
|
+
end],
|
|
30
|
+
[[uint], ->(_registry, id) { outputs.delete(id) }]
|
|
31
|
+
])
|
|
32
|
+
connection.roundtrip
|
|
33
|
+
connection.roundtrip
|
|
34
|
+
if manager
|
|
35
|
+
outputs.each_value do |state|
|
|
36
|
+
output = connection.request(manager, 1, 0, state[:proxy], new_interface: "zxdg_output_v1")
|
|
37
|
+
connection.listen(output, [
|
|
38
|
+
[[int, int], ->(_output, x, y) { state[:logical_position] = [x, y] }],
|
|
39
|
+
[[int, int], ->(_output, width, height) { state[:logical_size] = [width, height] }],
|
|
40
|
+
[[], nil], [[ptype], ->(_output, name) { state[:name] = name.to_s }], [[ptype], nil]
|
|
41
|
+
])
|
|
42
|
+
end
|
|
43
|
+
connection.roundtrip
|
|
44
|
+
end
|
|
45
|
+
outputs.map do |id, state|
|
|
46
|
+
scale = state[:scale]
|
|
47
|
+
x, y = state[:logical_position] || [state[:x] / scale.to_f, state[:y] / scale.to_f]
|
|
48
|
+
width, height = state[:logical_size] || [state[:width] / scale.to_f, state[:height] / scale.to_f]
|
|
49
|
+
if !state[:logical_size] && state.fetch(:transform, 0).odd?
|
|
50
|
+
width, height = height, width
|
|
51
|
+
end
|
|
52
|
+
Display.new(id, state[:name], Bounds.new(x, y, width, height), scale, false)
|
|
53
|
+
end
|
|
54
|
+
ensure
|
|
55
|
+
connection&.close
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../ffi/wayland"
|
|
4
|
+
require_relative "../../gpu/open_gl"
|
|
5
|
+
require_relative "appearance_aware"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
module Zaniah
|
|
10
|
+
module Platform
|
|
11
|
+
module Linux
|
|
12
|
+
class WaylandWindow < Headless::Window
|
|
13
|
+
include AppearanceAware
|
|
14
|
+
def displays = Linux.displays(display_server: :wayland)
|
|
15
|
+
P, I, U, V = Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_UINT, Fiddle::TYPE_VOID
|
|
16
|
+
GLOBALS = {"wl_compositor" => 4, "wl_shm" => 1, "wl_seat" => 5, "wl_output" => 2,
|
|
17
|
+
"wl_data_device_manager" => 3, "xdg_wm_base" => 1,
|
|
18
|
+
"zwp_text_input_manager_v3" => 1, "zxdg_decoration_manager_v1" => 1}.freeze
|
|
19
|
+
attr_reader :handle
|
|
20
|
+
def initialize(gpu: :opengl, **options)
|
|
21
|
+
super(**options)
|
|
22
|
+
@connection, @globals, @outputs, @offers = FFI::Wayland.new, {}, {}, {}
|
|
23
|
+
@offer_actions = {}
|
|
24
|
+
@entered_outputs = []
|
|
25
|
+
@position, @modifiers, @serial = Point.new(0, 0), [], 0
|
|
26
|
+
registry = @connection.request(@connection.display, 1, 0, new_interface: "wl_registry", version: 1)
|
|
27
|
+
@connection.listen(registry, [
|
|
28
|
+
[[U, P, U], ->(_registry, name, interface, version) { registry_global(registry, name, interface.to_s, version) }],
|
|
29
|
+
[[U], ->(_registry, name) { @outputs.delete(name) }]
|
|
30
|
+
])
|
|
31
|
+
@connection.roundtrip
|
|
32
|
+
%w[wl_compositor xdg_wm_base].each { |name| raise Error, "Wayland compositor lacks #{name}" unless @globals[name] }
|
|
33
|
+
@handle = @connection.request(@globals["wl_compositor"], 0, 0, new_interface: "wl_surface")
|
|
34
|
+
@connection.listen(@handle, [
|
|
35
|
+
[[P], ->(_surface, output) { @entered_outputs |= [output.to_i]; output_scale }],
|
|
36
|
+
[[P], ->(_surface, output) { @entered_outputs.delete(output.to_i); output_scale }]
|
|
37
|
+
])
|
|
38
|
+
@shell_surface = @connection.request(@globals["xdg_wm_base"], 2, 0, @handle, new_interface: "xdg_surface")
|
|
39
|
+
@connection.listen(@shell_surface, [[[U], ->(surface, serial) { @connection.request(surface, 4, serial); @configured = true; request_frame }]])
|
|
40
|
+
@toplevel = @connection.request(@shell_surface, 1, 0, new_interface: "xdg_toplevel")
|
|
41
|
+
@connection.listen(@toplevel, [
|
|
42
|
+
[[I, I, P], ->(_toplevel, width, height, _states) { resize(width, height) if width.positive? && height.positive? }],
|
|
43
|
+
[[], ->(_toplevel) { @close_requested = true }]
|
|
44
|
+
])
|
|
45
|
+
@connection.request(@toplevel, 2, @title)
|
|
46
|
+
@connection.request(@toplevel, 3, "org.zaniah.app")
|
|
47
|
+
if (manager = @globals["zxdg_decoration_manager_v1"])
|
|
48
|
+
@decoration = @connection.request(manager, 1, 0, @toplevel, new_interface: "zxdg_toplevel_decoration_v1")
|
|
49
|
+
@connection.listen(@decoration, [[[U], nil]])
|
|
50
|
+
@connection.request(@decoration, 1, 2)
|
|
51
|
+
end
|
|
52
|
+
setup_text_input
|
|
53
|
+
setup_clipboard
|
|
54
|
+
@connection.request(@handle, 6)
|
|
55
|
+
@connection.roundtrip
|
|
56
|
+
raise Error, "Wayland did not configure the initial surface" unless @configured
|
|
57
|
+
@device.release
|
|
58
|
+
@device = create_device(gpu)
|
|
59
|
+
rescue StandardError, LoadError
|
|
60
|
+
cleanup_native
|
|
61
|
+
raise
|
|
62
|
+
end
|
|
63
|
+
def registry_global(registry, name, interface, version)
|
|
64
|
+
return unless GLOBALS.key?(interface)
|
|
65
|
+
object = @connection.request(registry, 0, name, interface, [version, GLOBALS[interface]].min, 0,
|
|
66
|
+
new_interface: interface, version: [version, GLOBALS[interface]].min)
|
|
67
|
+
if interface == "wl_output"
|
|
68
|
+
@outputs[name] = [object, 1]
|
|
69
|
+
@connection.listen(object, [
|
|
70
|
+
[[I, I, I, I, I, P, P, I], nil], [[U, I, I, I], nil], [[], nil],
|
|
71
|
+
[[I], ->(_output, scale) { @outputs[name][1] = scale; output_scale if @handle }]
|
|
72
|
+
])
|
|
73
|
+
else
|
|
74
|
+
@globals[interface] = object
|
|
75
|
+
end
|
|
76
|
+
case interface
|
|
77
|
+
when "xdg_wm_base"
|
|
78
|
+
@connection.listen(object, [[[U], ->(shell, serial) { @connection.request(shell, 3, serial) }]])
|
|
79
|
+
when "wl_seat"
|
|
80
|
+
@connection.listen(object, [[[U], ->(seat, capabilities) { seat_capabilities(seat, capabilities) }], [[P], nil]])
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
def output_scale
|
|
84
|
+
active = @outputs.values.select { |value| @entered_outputs.include?(value[0].to_i) }
|
|
85
|
+
return if active.empty? || !@handle
|
|
86
|
+
scale = [active.map(&:last).max, 1].max
|
|
87
|
+
return if scale == @scale_factor
|
|
88
|
+
@scale_factor = scale
|
|
89
|
+
@connection.request(@handle, 8, scale)
|
|
90
|
+
resize(content_size.width, content_size.height)
|
|
91
|
+
end
|
|
92
|
+
def resize(width, height)
|
|
93
|
+
if @egl_window
|
|
94
|
+
@wayland_egl.fn(:wl_egl_window_resize, [P, I, I, I, I], V).call(@egl_window, (width * @scale_factor).round, (height * @scale_factor).round, 0, 0)
|
|
95
|
+
end
|
|
96
|
+
super
|
|
97
|
+
end
|
|
98
|
+
def create_device(backend)
|
|
99
|
+
raise ArgumentError, "Wayland supports :opengl" unless [:gl, :opengl].include?(backend)
|
|
100
|
+
@egl, @wayland_egl = FFI::Library.new("libEGL.so.1"), FFI::Library.new("libwayland-egl.so.1")
|
|
101
|
+
@egl_display = @egl.fn(:eglGetDisplay, [P], P).call(@connection.display)
|
|
102
|
+
major, minor = [0].pack("i"), [0].pack("i")
|
|
103
|
+
egl_check(@egl.fn(:eglInitialize, [P, P, P], U).call(@egl_display, major, minor), "eglInitialize")
|
|
104
|
+
egl_check(@egl.fn(:eglBindAPI, [U], U).call(0x30A2), "eglBindAPI OpenGL")
|
|
105
|
+
attrs = [0x3024, 8, 0x3023, 8, 0x3022, 8, 0x3021, 8, 0x3033, 4, 0x3040, 8, 0x3038].pack("i*")
|
|
106
|
+
config, count = [0].pack("J"), [0].pack("i")
|
|
107
|
+
egl_check(@egl.fn(:eglChooseConfig, [P, P, P, I, P], U).call(@egl_display, attrs, config, 1, count), "eglChooseConfig")
|
|
108
|
+
raise Error, "no EGL OpenGL window configuration" if count.unpack1("i").zero?
|
|
109
|
+
config = config.unpack1("J")
|
|
110
|
+
attributes = [0x3098, 3, 0x30fb, 3, 0x30fd, 1, 0x3038].pack("i*")
|
|
111
|
+
@egl_context = @egl.fn(:eglCreateContext, [P, P, P, P], P).call(@egl_display, config, 0, attributes)
|
|
112
|
+
raise Error, "EGL OpenGL 3.3 core context unavailable" if @egl_context.null?
|
|
113
|
+
@egl_window = @wayland_egl.fn(:wl_egl_window_create, [P, I, I], P).call(@handle, (content_size.width * @scale_factor).round, (content_size.height * @scale_factor).round)
|
|
114
|
+
@egl_surface = @egl.fn(:eglCreateWindowSurface, [P, P, P, P], P).call(@egl_display, config, @egl_window, 0)
|
|
115
|
+
raise Error, "eglCreateWindowSurface failed" if @egl_surface.null?
|
|
116
|
+
make_current
|
|
117
|
+
@egl.fn(:eglSwapInterval, [P, I], U).call(@egl_display, 1)
|
|
118
|
+
GPU::OpenGL.new(self, library: FFI::Library.new("libGL.so.1"), resolver: ->(name) { @egl.fn(:eglGetProcAddress, [P], P).call(name) })
|
|
119
|
+
end
|
|
120
|
+
def egl_check(value, name)
|
|
121
|
+
raise Error, "#{name}: EGL error 0x#{@egl.fn(:eglGetError, [], U).call.to_s(16)}" if value.zero?
|
|
122
|
+
end
|
|
123
|
+
def make_current = egl_check(@egl.fn(:eglMakeCurrent, [P] * 4, U).call(@egl_display, @egl_surface, @egl_surface, @egl_context), "eglMakeCurrent")
|
|
124
|
+
def swap_buffers = egl_check(@egl.fn(:eglSwapBuffers, [P, P], U).call(@egl_display, @egl_surface), "eglSwapBuffers")
|
|
125
|
+
def title=(title)
|
|
126
|
+
super
|
|
127
|
+
@connection.request(@toplevel, 2, title) if @toplevel
|
|
128
|
+
end
|
|
129
|
+
def tick
|
|
130
|
+
return if closed?
|
|
131
|
+
@connection.poll
|
|
132
|
+
poll_appearance
|
|
133
|
+
return close if @close_requested
|
|
134
|
+
finish_drop if @pending_drop
|
|
135
|
+
if @repeat_key && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= @repeat_at
|
|
136
|
+
keyboard_key(@repeat_key, 1, held: true)
|
|
137
|
+
@repeat_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 1.0 / @repeat_rate
|
|
138
|
+
end
|
|
139
|
+
super
|
|
140
|
+
end
|
|
141
|
+
def run
|
|
142
|
+
until closed?
|
|
143
|
+
@connection.poll(timeout: dirty? ? 0 : 0.05)
|
|
144
|
+
tick
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
def seat_capabilities(seat, capabilities)
|
|
148
|
+
if (capabilities & 1) != 0 && !@pointer
|
|
149
|
+
@pointer = @connection.request(seat, 0, 0, new_interface: "wl_pointer")
|
|
150
|
+
@connection.listen(@pointer, [
|
|
151
|
+
[[U, P, I, I], ->(_pointer, serial, _surface, x, y) { @serial = @pointer_serial = serial; @position = Point.new(x / 256.0, y / 256.0); self.cursor_style = :arrow }],
|
|
152
|
+
[[U, P], nil],
|
|
153
|
+
[[U, I, I], ->(_pointer, _time, x, y) { @position = Point.new(x / 256.0, y / 256.0); input(Input::MouseMove.new(@position, @modifiers)) }],
|
|
154
|
+
[[U, U, U, U], ->(_pointer, serial, _time, button, state) { @serial = serial; pointer_button(button, state) }],
|
|
155
|
+
[[U, U, I], ->(_pointer, _time, axis, value) { input(Input::ScrollWheel.new(@position, axis.zero? ? Point.new(0, value / 256.0) : Point.new(value / 256.0, 0), :changed, @modifiers)) }],
|
|
156
|
+
[[], nil], [[U], nil], [[U, U], nil], [[U, I], nil]
|
|
157
|
+
])
|
|
158
|
+
end
|
|
159
|
+
if (capabilities & 2) != 0 && !@keyboard
|
|
160
|
+
@keyboard = @connection.request(seat, 1, 0, new_interface: "wl_keyboard")
|
|
161
|
+
@xkb = FFI::Library.new("libxkbcommon.so.0")
|
|
162
|
+
@connection.listen(@keyboard, [
|
|
163
|
+
[[U, I, U], ->(_keyboard, format, fd, size) { keyboard_map(format, fd, size) }],
|
|
164
|
+
[[U, P, P], ->(_keyboard, serial, _surface, _keys) { @serial = serial }],
|
|
165
|
+
[[U, P], ->(_keyboard, _serial, _surface) { @repeat_key = nil; @modifiers = [] }],
|
|
166
|
+
[[U, U, U, U], ->(_keyboard, serial, _time, key, state) { @serial = serial; keyboard_key(key, state) }],
|
|
167
|
+
[[U, U, U, U, U], ->(_keyboard, serial, depressed, latched, locked, group) { @serial = serial; keyboard_modifiers(depressed, latched, locked, group) }],
|
|
168
|
+
[[I, I], ->(_keyboard, rate, delay) { @repeat_rate, @repeat_delay = rate, delay }]
|
|
169
|
+
])
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
def pointer_button(code, state)
|
|
173
|
+
button = {0x110 => :left, 0x111 => :right, 0x112 => :middle}.fetch(code, :other)
|
|
174
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
175
|
+
count = @last_click && @last_click[0] == code && now - @last_click[1] < 0.4 && (@position.x - @last_click[2].x).abs < 4 && (@position.y - @last_click[2].y).abs < 4 ? 2 : 1
|
|
176
|
+
@last_click = [code, now, @position] if state == 1
|
|
177
|
+
input(state == 1 ? Input::MouseDown.new(@position, button, @modifiers, count) : Input::MouseUp.new(@position, button, @modifiers))
|
|
178
|
+
end
|
|
179
|
+
def keyboard_map(format, fd, size)
|
|
180
|
+
io = IO.for_fd(fd)
|
|
181
|
+
return unless format == 1
|
|
182
|
+
raise Error, "Wayland keymap exceeds size limit" if size > 16_777_216
|
|
183
|
+
source = io.read(size)
|
|
184
|
+
@xkb.fn(:xkb_state_unref, [P], V).call(@key_state) if @key_state
|
|
185
|
+
@xkb.fn(:xkb_keymap_unref, [P], V).call(@keymap) if @keymap
|
|
186
|
+
@key_context ||= @xkb.fn(:xkb_context_new, [I], P).call(0)
|
|
187
|
+
@keymap = @xkb.fn(:xkb_keymap_new_from_string, [P, P, I, I], P).call(@key_context, source, 1, 0)
|
|
188
|
+
raise Error, "invalid Wayland XKB keymap" if @keymap.null?
|
|
189
|
+
@key_state = @xkb.fn(:xkb_state_new, [P], P).call(@keymap)
|
|
190
|
+
ensure
|
|
191
|
+
io&.close
|
|
192
|
+
end
|
|
193
|
+
def keyboard_modifiers(depressed, latched, locked, group)
|
|
194
|
+
return unless @key_state
|
|
195
|
+
@xkb.fn(:xkb_state_update_mask, [P, U, U, U, U, U, U], U).call(@key_state, depressed, latched, locked, 0, 0, group)
|
|
196
|
+
@modifiers = {"Control" => "ctrl", "Mod1" => "alt", "Shift" => "shift", "Mod4" => "cmd"}.filter_map do |native, key|
|
|
197
|
+
key if @xkb.fn(:xkb_state_mod_name_is_active, [P, P, U], I).call(@key_state, native, 8) == 1
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
def keyboard_key(code, state, held: false)
|
|
201
|
+
return unless @key_state
|
|
202
|
+
unless held
|
|
203
|
+
if state == 1 && @repeat_rate.to_i.positive? && @xkb.fn(:xkb_keymap_key_repeats, [P, U], I).call(@keymap, code + 8) == 1
|
|
204
|
+
@repeat_key, @repeat_at = code, Process.clock_gettime(Process::CLOCK_MONOTONIC) + @repeat_delay / 1000.0
|
|
205
|
+
elsif state.zero? && @repeat_key == code
|
|
206
|
+
@repeat_key = nil
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
symbol = @xkb.fn(:xkb_state_key_get_one_sym, [P, U], U).call(@key_state, code + 8)
|
|
210
|
+
bytes = "\0" * 128
|
|
211
|
+
@xkb.fn(:xkb_keysym_get_name, [U, P, Fiddle::TYPE_SIZE_T], I).call(symbol, bytes, bytes.bytesize)
|
|
212
|
+
key = bytes.split("\0", 2).first.downcase
|
|
213
|
+
key = {"return" => "enter", "escape" => "esc", "prior" => "pageup", "next" => "pagedown"}.fetch(key, key)
|
|
214
|
+
stroke = (@modifiers + [key]).join("-")
|
|
215
|
+
input(state == 1 ? Input::KeyDown.new(stroke, held) : Input::KeyUp.new(stroke)) unless state == 1 && @composing
|
|
216
|
+
return unless state == 1 && !@composing && (@modifiers & %w[ctrl cmd]).empty?
|
|
217
|
+
length = @xkb.fn(:xkb_state_key_get_utf8, [P, U, P, Fiddle::TYPE_SIZE_T], I).call(@key_state, code + 8, bytes, bytes.bytesize)
|
|
218
|
+
text = bytes.byteslice(0, [length, 0].max).force_encoding("UTF-8")
|
|
219
|
+
input(Input::TextInput.new(text)) unless text.empty? || text.match?(/\A[\x00-\x1f\x7f]+\z/)
|
|
220
|
+
end
|
|
221
|
+
def setup_text_input
|
|
222
|
+
manager, seat = @globals.values_at("zwp_text_input_manager_v3", "wl_seat")
|
|
223
|
+
return unless manager && seat
|
|
224
|
+
@text_input = @connection.request(manager, 1, 0, seat, new_interface: "zwp_text_input_v3")
|
|
225
|
+
@text_events = []
|
|
226
|
+
@connection.listen(@text_input, [
|
|
227
|
+
[[P], ->(text_input, surface) { if surface.to_i == @handle.to_i; @connection.request(text_input, 1); @connection.request(text_input, 5, 0, 0); @connection.request(text_input, 7); end }],
|
|
228
|
+
[[P], ->(text_input, _surface) { @connection.request(text_input, 2); @connection.request(text_input, 7); @composing = false }],
|
|
229
|
+
[[P, I, I], ->(_text_input, text, start, finish) { value = text.null? ? "" : text.to_s.force_encoding("UTF-8"); @text_events << Input::Composition.new(value, [start, finish - start]); @composing = !value.empty? }],
|
|
230
|
+
[[P], ->(_text_input, text) { @text_events << Input::TextInput.new(text.to_s.force_encoding("UTF-8")) unless text.null? }],
|
|
231
|
+
[[U, U], ->(_text_input, before, after) { @delete_surrounding = [before, after] }],
|
|
232
|
+
[[U], ->(_text_input, _serial) { @on_delete_surrounding&.call(*@delete_surrounding) if @delete_surrounding; @delete_surrounding = nil; @text_events.each { |event| input(event) }; @text_events.clear }]
|
|
233
|
+
])
|
|
234
|
+
end
|
|
235
|
+
def on_delete_surrounding(&block) = @on_delete_surrounding = block
|
|
236
|
+
def surrounding_text(text, cursor:, anchor: cursor)
|
|
237
|
+
return unless @text_input
|
|
238
|
+
raise ArgumentError, "Wayland surrounding text is limited to 4000 bytes" if text.bytesize > 4000
|
|
239
|
+
@connection.request(@text_input, 3, text, cursor, anchor)
|
|
240
|
+
@connection.request(@text_input, 7)
|
|
241
|
+
end
|
|
242
|
+
def ime_state=(bounds)
|
|
243
|
+
@ime_state = bounds
|
|
244
|
+
return unless @text_input && bounds.is_a?(Bounds)
|
|
245
|
+
@connection.request(@text_input, 6, bounds.x.to_i, bounds.y.to_i, bounds.width.ceil, bounds.height.ceil)
|
|
246
|
+
@connection.request(@text_input, 7)
|
|
247
|
+
end
|
|
248
|
+
def setup_clipboard
|
|
249
|
+
manager, seat = @globals.values_at("wl_data_device_manager", "wl_seat")
|
|
250
|
+
return unless manager && seat
|
|
251
|
+
@data_device = @connection.request(manager, 1, 0, seat, new_interface: "wl_data_device")
|
|
252
|
+
@connection.listen(@data_device, [
|
|
253
|
+
[[P], ->(_device, offer) { @offers[offer.to_i] = []; @connection.listen(offer, [[[P], ->(_offer, mime) { @offers[offer.to_i] << mime.to_s }], [[U], nil], [[U], ->(_offer, action) { @offer_actions[offer.to_i] = action }]]) }],
|
|
254
|
+
[[U, P, I, I, P], ->(_device, serial, _surface, x, y, offer) { drag_enter(serial, x, y, offer) }],
|
|
255
|
+
[[], ->(_device) { destroy_offer(@drag_offer) unless @pending_drop; @drag_offer = nil unless @pending_drop }],
|
|
256
|
+
[[U, I, I], ->(_device, _time, x, y) { @drag_position = Point.new(x / 256.0, y / 256.0) }],
|
|
257
|
+
[[], ->(_device) { @pending_drop = !!@drag_offer }],
|
|
258
|
+
[[P], ->(_device, offer) { destroy_offer(@selection); @selection = offer.null? ? nil : offer }]
|
|
259
|
+
])
|
|
260
|
+
end
|
|
261
|
+
def clipboard=(text)
|
|
262
|
+
raise Error, "Wayland clipboard requires a focused input seat" unless @data_device && @serial.positive?
|
|
263
|
+
@clipboard = text.to_s.encode("UTF-8")
|
|
264
|
+
@source = @connection.request(@globals["wl_data_device_manager"], 0, 0, new_interface: "wl_data_source")
|
|
265
|
+
@connection.listen(@source, [
|
|
266
|
+
[[P], nil], [[P, I], ->(_source, _mime, fd) { io = IO.for_fd(fd); begin; io.write(@clipboard); rescue Errno::EPIPE; ensure; io.close; end }],
|
|
267
|
+
[[], ->(_source) { @clipboard = nil }], [[], nil], [[], nil], [[U], nil]
|
|
268
|
+
])
|
|
269
|
+
@connection.request(@source, 0, "text/plain;charset=utf-8")
|
|
270
|
+
@connection.request(@source, 0, "text/plain")
|
|
271
|
+
@connection.request(@data_device, 1, @source, @serial)
|
|
272
|
+
end
|
|
273
|
+
def clipboard = @clipboard || read_selection(["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"])
|
|
274
|
+
def clipboard_paths
|
|
275
|
+
Window.file_paths(read_selection(["text/uri-list"]))
|
|
276
|
+
end
|
|
277
|
+
def drag_enter(serial, x, y, offer)
|
|
278
|
+
return if offer.null?
|
|
279
|
+
@drag_offer, @drag_position = offer, Point.new(x / 256.0, y / 256.0)
|
|
280
|
+
accept = @offers.fetch(offer.to_i, []).include?("text/uri-list")
|
|
281
|
+
@connection.request(offer, 0, serial, accept ? "text/uri-list" : 0)
|
|
282
|
+
@connection.request(offer, 4, accept ? 1 : 0, accept ? 1 : 0) if @connection.version(offer) >= 3
|
|
283
|
+
end
|
|
284
|
+
def finish_drop
|
|
285
|
+
@pending_drop = false
|
|
286
|
+
offer = @drag_offer
|
|
287
|
+
accepted = @connection.version(offer) < 3 || @offer_actions[offer.to_i] == 1
|
|
288
|
+
paths = accepted ? Window.file_paths(read_offer(offer, ["text/uri-list"])) : []
|
|
289
|
+
@connection.request(offer, 3) if @connection.version(offer) >= 3 && !paths.empty?
|
|
290
|
+
destroy_offer(offer)
|
|
291
|
+
@drag_offer = nil
|
|
292
|
+
input(Input::FileDrop.new(paths.freeze, @drag_position)) unless paths.empty?
|
|
293
|
+
end
|
|
294
|
+
def destroy_offer(offer)
|
|
295
|
+
return unless offer
|
|
296
|
+
@connection.request(offer, 2, destroy: true)
|
|
297
|
+
@offers.delete(offer.to_i)
|
|
298
|
+
@offer_actions.delete(offer.to_i)
|
|
299
|
+
end
|
|
300
|
+
def read_selection(types) = read_offer(@selection, types)
|
|
301
|
+
def read_offer(offer, types)
|
|
302
|
+
return "" unless offer
|
|
303
|
+
mime = types.find { |type| @offers.fetch(offer.to_i, []).include?(type) }
|
|
304
|
+
return "" unless mime
|
|
305
|
+
reader, writer = IO.pipe
|
|
306
|
+
@connection.request(offer, 1, mime, writer.fileno)
|
|
307
|
+
@connection.library.fn(:wl_display_flush, [P], I).call(@connection.display)
|
|
308
|
+
writer.close
|
|
309
|
+
result, deadline = "".b, Process.clock_gettime(Process::CLOCK_MONOTONIC) + 2
|
|
310
|
+
loop do
|
|
311
|
+
@connection.poll
|
|
312
|
+
raise Error, "Wayland selection transfer timed out" unless IO.select([reader], nil, nil, [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max)
|
|
313
|
+
data = reader.read_nonblock(65_536, exception: false)
|
|
314
|
+
break if data.nil?
|
|
315
|
+
result << data if data.is_a?(String)
|
|
316
|
+
raise Error, "clipboard exceeds 16 MiB" if result.bytesize > 16_777_216
|
|
317
|
+
end
|
|
318
|
+
result.force_encoding("UTF-8").scrub
|
|
319
|
+
ensure
|
|
320
|
+
reader&.close
|
|
321
|
+
writer&.close unless writer&.closed?
|
|
322
|
+
end
|
|
323
|
+
def cursor_style=(style)
|
|
324
|
+
return unless @pointer_serial && @globals["wl_shm"]
|
|
325
|
+
@cursors ||= FFI::Library.new("libwayland-cursor.so.0")
|
|
326
|
+
@cursor_theme ||= @cursors.fn(:wl_cursor_theme_load, [P, I, P], P).call(0, 24, @globals["wl_shm"])
|
|
327
|
+
name = {arrow: "left_ptr", text: "xterm", pointer: "hand2", crosshair: "crosshair", resize_horizontal: "sb_h_double_arrow", resize_vertical: "sb_v_double_arrow"}.fetch(style)
|
|
328
|
+
cursor = @cursors.fn(:wl_cursor_theme_get_cursor, [P, P], P).call(@cursor_theme, name)
|
|
329
|
+
return if cursor.null?
|
|
330
|
+
images = cursor[8, 8].unpack1("J")
|
|
331
|
+
image = Fiddle::Pointer.new(Fiddle::Pointer.new(images)[0, 8].unpack1("J"))
|
|
332
|
+
_, _, hotspot_x, hotspot_y = image[0, 16].unpack("I4")
|
|
333
|
+
buffer = @cursors.fn(:wl_cursor_image_get_buffer, [P], P).call(image)
|
|
334
|
+
@cursor_surface ||= @connection.request(@globals["wl_compositor"], 0, 0, new_interface: "wl_surface")
|
|
335
|
+
@connection.request(@pointer, 0, @pointer_serial, @cursor_surface, hotspot_x, hotspot_y)
|
|
336
|
+
@connection.request(@cursor_surface, 1, buffer, 0, 0)
|
|
337
|
+
@connection.request(@cursor_surface, 2, 0, 0, 0x7fffffff, 0x7fffffff)
|
|
338
|
+
@connection.request(@cursor_surface, 6)
|
|
339
|
+
end
|
|
340
|
+
def prompt_for_paths(**options) = Linux::Window.instance_method(:prompt_for_paths).bind_call(self, **options)
|
|
341
|
+
def open_url(url) = Linux::Window.instance_method(:open_url).bind_call(self, url)
|
|
342
|
+
def toggle_fullscreen
|
|
343
|
+
@fullscreen = !@fullscreen
|
|
344
|
+
@fullscreen ? @connection.request(@toplevel, 11, 0) : @connection.request(@toplevel, 12)
|
|
345
|
+
end
|
|
346
|
+
def close
|
|
347
|
+
return false unless super
|
|
348
|
+
close_appearance
|
|
349
|
+
cleanup_native
|
|
350
|
+
true
|
|
351
|
+
end
|
|
352
|
+
def cleanup_native
|
|
353
|
+
if @egl_display && !@egl_display.null?
|
|
354
|
+
@egl.fn(:eglMakeCurrent, [P] * 4, U).call(@egl_display, 0, 0, 0)
|
|
355
|
+
@egl.fn(:eglDestroySurface, [P, P], U).call(@egl_display, @egl_surface) if @egl_surface
|
|
356
|
+
@egl.fn(:eglDestroyContext, [P, P], U).call(@egl_display, @egl_context) if @egl_context
|
|
357
|
+
@egl.fn(:eglTerminate, [P], U).call(@egl_display)
|
|
358
|
+
end
|
|
359
|
+
@wayland_egl.fn(:wl_egl_window_destroy, [P], V).call(@egl_window) if @egl_window
|
|
360
|
+
@cursors.fn(:wl_cursor_theme_destroy, [P], V).call(@cursor_theme) if @cursor_theme
|
|
361
|
+
@xkb.fn(:xkb_state_unref, [P], V).call(@key_state) if @key_state
|
|
362
|
+
@xkb.fn(:xkb_keymap_unref, [P], V).call(@keymap) if @keymap
|
|
363
|
+
@xkb.fn(:xkb_context_unref, [P], V).call(@key_context) if @key_context
|
|
364
|
+
@connection&.close
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|