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,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../ffi/library"
|
|
4
|
+
|
|
5
|
+
module Zaniah
|
|
6
|
+
module Platform
|
|
7
|
+
module Windows
|
|
8
|
+
# A native ConPTY process. Output is UTF-8 VT data, compatible with the
|
|
9
|
+
# same terminal parser used by Unix PTYs. Empty means idle, nil means EOF.
|
|
10
|
+
class Terminal
|
|
11
|
+
P, I, U, N, V = Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_UINT, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_VOID
|
|
12
|
+
attr_reader :pid
|
|
13
|
+
def initialize(command: nil, columns: 80, rows: 24, cwd: nil, env: {})
|
|
14
|
+
raise Error, "ConPTY requires 64-bit Windows Ruby" unless Fiddle::SIZEOF_VOIDP == 8
|
|
15
|
+
@kernel = FFI::Library.new("kernel32.dll")
|
|
16
|
+
@output, @pending, @closed, @eof = Queue.new, "".b, false, false
|
|
17
|
+
input_read, @input_write = pipe
|
|
18
|
+
@output_read, output_write = pipe
|
|
19
|
+
console = [0].pack("J")
|
|
20
|
+
check_hresult(@kernel.fn(:CreatePseudoConsole, [I, P, P, U, P], I).call(coordinate(columns, rows), input_read, output_write, 0, console))
|
|
21
|
+
@console = console.unpack1("J")
|
|
22
|
+
size = [0].pack("J")
|
|
23
|
+
@kernel.fn(:InitializeProcThreadAttributeList, [P, U, U, P], I).call(0, 1, 0, size)
|
|
24
|
+
attributes = Fiddle::Pointer.malloc(size.unpack1("J"), Fiddle::RUBY_FREE)
|
|
25
|
+
check(@kernel.fn(:InitializeProcThreadAttributeList, [P, U, U, P], I).call(attributes, 1, 0, size), "InitializeProcThreadAttributeList")
|
|
26
|
+
check(@kernel.fn(:UpdateProcThreadAttribute, [P, U, N, P, N, P, P], I).call(attributes, 0, 0x20016, @console, 8, 0, 0), "UpdateProcThreadAttribute")
|
|
27
|
+
startup = "\0".b * 112
|
|
28
|
+
startup[0, 4] = [112].pack("I")
|
|
29
|
+
startup[60, 4] = [0x100].pack("I") # STARTF_USESTDHANDLES
|
|
30
|
+
startup[104, 8] = [attributes.to_i].pack("J")
|
|
31
|
+
info = "\0".b * 24
|
|
32
|
+
command ||= ENV.fetch("COMSPEC", "cmd.exe")
|
|
33
|
+
line = command.is_a?(Array) ? command.map { |part| self.class.quote_argument(part.to_s) }.join(" ") : command.to_s
|
|
34
|
+
line = wide(line)
|
|
35
|
+
directory = cwd ? wide(File.expand_path(cwd)) : nil
|
|
36
|
+
environment = env.empty? ? nil : (ENV.to_h.merge(env.transform_keys(&:to_s).transform_values(&:to_s)).sort_by { |key, _| key.downcase }.map { |key, value| "#{key}=#{value}\0" }.join + "\0").encode("UTF-16LE").b
|
|
37
|
+
check(@kernel.fn(:CreateProcessW, [P, P, P, P, I, U, P, P, P, P], I).call(0, line, 0, 0, 0, 0x00080400, environment || 0, directory || 0, startup, info), "CreateProcessW")
|
|
38
|
+
close_handle(input_read)
|
|
39
|
+
close_handle(output_write)
|
|
40
|
+
input_read = output_write = nil
|
|
41
|
+
@process, thread, @pid = info.unpack("JJI")
|
|
42
|
+
close_handle(thread)
|
|
43
|
+
@reader = Thread.new do
|
|
44
|
+
loop do
|
|
45
|
+
bytes, count = "\0".b * 65_536, [0].pack("I")
|
|
46
|
+
ok = @kernel.fn(:ReadFile, [P, P, U, P, P], I, need_gvl: false).call(@output_read, bytes, bytes.bytesize, count, 0)
|
|
47
|
+
break if ok.zero? || count.unpack1("I").zero?
|
|
48
|
+
@output << bytes.byteslice(0, count.unpack1("I"))
|
|
49
|
+
end
|
|
50
|
+
ensure
|
|
51
|
+
@output << nil
|
|
52
|
+
end
|
|
53
|
+
ensure
|
|
54
|
+
@kernel.fn(:DeleteProcThreadAttributeList, [P], V).call(attributes) if attributes
|
|
55
|
+
close_handle(input_read) if input_read
|
|
56
|
+
close_handle(output_write) if output_write
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# CommandLineToArgvW/CRT escaping: backslashes before quotes and the
|
|
60
|
+
# closing delimiter must be doubled; other backslashes are literal.
|
|
61
|
+
def self.quote_argument(value)
|
|
62
|
+
return value unless value.empty? || value.match?(/[\s"]/)
|
|
63
|
+
'"' + value.gsub(/(\\*)"/) { Regexp.last_match(1) * 2 + '\\"' }.sub(/(\\+)\z/) { Regexp.last_match(1) * 2 } + '"'
|
|
64
|
+
end
|
|
65
|
+
def wide(text) = text.encode("UTF-16LE").b + "\0\0"
|
|
66
|
+
def coordinate(columns, rows)
|
|
67
|
+
raise ArgumentError, "invalid terminal size" unless columns.between?(1, 32_767) && rows.between?(1, 32_767)
|
|
68
|
+
[columns, rows].pack("s2").unpack1("l")
|
|
69
|
+
end
|
|
70
|
+
def pipe
|
|
71
|
+
read_handle, write_handle = [0].pack("J"), [0].pack("J")
|
|
72
|
+
check(@kernel.fn(:CreatePipe, [P, P, P, U], I).call(read_handle, write_handle, 0, 0), "CreatePipe")
|
|
73
|
+
[read_handle.unpack1("J"), write_handle.unpack1("J")]
|
|
74
|
+
end
|
|
75
|
+
def read_available(limit: 65_536)
|
|
76
|
+
raise ArgumentError, "limit must be positive" unless limit.positive?
|
|
77
|
+
until @output.empty?
|
|
78
|
+
bytes = @output.pop
|
|
79
|
+
if bytes.nil?
|
|
80
|
+
@eof = true
|
|
81
|
+
break
|
|
82
|
+
end
|
|
83
|
+
@pending << bytes
|
|
84
|
+
end
|
|
85
|
+
return nil if @pending.empty? && @eof
|
|
86
|
+
@pending.slice!(0, limit)
|
|
87
|
+
end
|
|
88
|
+
def write(bytes)
|
|
89
|
+
raise IOError, "terminal closed" if @closed
|
|
90
|
+
offset = 0
|
|
91
|
+
while offset < bytes.bytesize
|
|
92
|
+
chunk = bytes.byteslice(offset, bytes.bytesize - offset)
|
|
93
|
+
count = [0].pack("I")
|
|
94
|
+
check(@kernel.fn(:WriteFile, [P, P, U, P, P], I, need_gvl: false).call(@input_write, chunk, chunk.bytesize, count, 0), "WriteFile")
|
|
95
|
+
written = count.unpack1("I")
|
|
96
|
+
raise IOError, "ConPTY write made no progress" if written.zero?
|
|
97
|
+
offset += written
|
|
98
|
+
end
|
|
99
|
+
offset
|
|
100
|
+
end
|
|
101
|
+
def resize(columns, rows)
|
|
102
|
+
check_hresult(@kernel.fn(:ResizePseudoConsole, [P, I], I).call(@console, coordinate(columns, rows)))
|
|
103
|
+
end
|
|
104
|
+
def alive? = !@closed && @kernel.fn(:WaitForSingleObject, [P, U], U).call(@process, 0) == 258
|
|
105
|
+
def close
|
|
106
|
+
return if @closed
|
|
107
|
+
@closed = true
|
|
108
|
+
close_handle(@input_write)
|
|
109
|
+
# ReadFile releases the GVL so the reader drains ConPTY while closing.
|
|
110
|
+
@kernel.fn(:ClosePseudoConsole, [P], V, need_gvl: false).call(@console)
|
|
111
|
+
@reader.join(2)
|
|
112
|
+
close_handle(@output_read)
|
|
113
|
+
close_handle(@process)
|
|
114
|
+
end
|
|
115
|
+
def close_handle(handle) = @kernel.fn(:CloseHandle, [P], I).call(handle)
|
|
116
|
+
def check(result, name)
|
|
117
|
+
raise SystemCallError.new(name, Fiddle.last_error) if result.zero?
|
|
118
|
+
result
|
|
119
|
+
end
|
|
120
|
+
def check_hresult(result)
|
|
121
|
+
raise Error, "ConPTY failure 0x#{(result & 0xffffffff).to_s(16)}" unless (result & 0x80000000).zero?
|
|
122
|
+
result
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
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 Windows
|
|
9
|
+
class Watcher
|
|
10
|
+
P, I, U, V = Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_UINT, Fiddle::TYPE_VOID
|
|
11
|
+
def initialize(paths, latency: 0.05)
|
|
12
|
+
@kernel, @handles, @closed = FFI::Library.new("kernel32.dll"), [], false
|
|
13
|
+
paths = Array(paths).map { |path| File.expand_path(path) }
|
|
14
|
+
raise ArgumentError, "watch paths required" if paths.empty?
|
|
15
|
+
paths.map { |path| File.directory?(path) ? path : File.dirname(path) }.uniq.each do |path|
|
|
16
|
+
handle = @kernel.fn(:CreateFileW, [P, U, U, P, U, U, P], P).call(path.encode("UTF-16LE").b + "\0\0", 1, 7, 0, 3, 0x42000000, 0)
|
|
17
|
+
raise SystemCallError.new("CreateFileW #{path}", Fiddle.last_error) if handle.to_i == -1 || handle.to_i == (1 << 64) - 1
|
|
18
|
+
event = @kernel.fn(:CreateEventW, [P, I, I, P], P).call(0, 1, 0, 0)
|
|
19
|
+
raise Error, "CreateEventW failed" if event.null?
|
|
20
|
+
overlapped = Fiddle::Pointer.malloc(32, Fiddle::RUBY_FREE)
|
|
21
|
+
overlapped[0, 32] = "\0" * 32
|
|
22
|
+
overlapped[24, 8] = [event.to_i].pack("J")
|
|
23
|
+
entry = [path, handle, event, overlapped, Fiddle::Pointer.malloc(65_536, Fiddle::RUBY_FREE)]
|
|
24
|
+
@handles << entry
|
|
25
|
+
issue(entry)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
def issue(entry)
|
|
29
|
+
_, handle, event, overlapped, buffer = entry
|
|
30
|
+
@kernel.fn(:ResetEvent, [P], I).call(event)
|
|
31
|
+
ok = @kernel.fn(:ReadDirectoryChangesW, [P, P, U, I, U, P, P, P], I).call(handle, buffer, 65_536, 1, 0x17f, 0, overlapped, 0)
|
|
32
|
+
raise SystemCallError.new("ReadDirectoryChangesW", Fiddle.last_error) if ok.zero? && Fiddle.last_error != 997
|
|
33
|
+
end
|
|
34
|
+
def poll(timeout: 0)
|
|
35
|
+
return [] if @closed
|
|
36
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
37
|
+
events = []
|
|
38
|
+
loop do
|
|
39
|
+
@handles.each do |entry|
|
|
40
|
+
root, handle, event, overlapped, buffer = entry
|
|
41
|
+
next unless @kernel.fn(:WaitForSingleObject, [P, U], U).call(event, 0).zero?
|
|
42
|
+
count = [0].pack("I")
|
|
43
|
+
ok = @kernel.fn(:GetOverlappedResult, [P, P, P, I], I).call(handle, overlapped, count, 0)
|
|
44
|
+
size = count.unpack1("I")
|
|
45
|
+
if ok.zero? || size.zero?
|
|
46
|
+
events << FileEvent.new(:overflow, root, nil)
|
|
47
|
+
else
|
|
48
|
+
offset, old_path = 0, nil
|
|
49
|
+
loop do
|
|
50
|
+
following, action, length = buffer[offset, 12].unpack("I3")
|
|
51
|
+
break if offset + 12 + length > size
|
|
52
|
+
name = buffer[offset + 12, length].force_encoding("UTF-16LE").encode("UTF-8")
|
|
53
|
+
path = File.join(root, name.tr("\\", "/"))
|
|
54
|
+
if action == 4
|
|
55
|
+
old_path = path
|
|
56
|
+
else
|
|
57
|
+
kind = {1 => :created, 2 => :deleted, 3 => :modified, 5 => :renamed}.fetch(action, :modified)
|
|
58
|
+
events << FileEvent.new(kind, path, action == 5 ? old_path : nil)
|
|
59
|
+
old_path = nil
|
|
60
|
+
end
|
|
61
|
+
break if following.zero?
|
|
62
|
+
offset += following
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
issue(entry)
|
|
66
|
+
end
|
|
67
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
68
|
+
break if !events.empty? || remaining <= 0
|
|
69
|
+
# Wait on groups supported by Win32 instead of starting Ruby watcher threads.
|
|
70
|
+
@handles.each_slice(64) do |entries|
|
|
71
|
+
handles = entries.map { |entry| entry[2].to_i }.pack("J*")
|
|
72
|
+
@kernel.fn(:WaitForMultipleObjects, [U, P, I, U], U).call(entries.length, handles, 0, [(remaining * 1000).ceil, 50].min)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
events.uniq
|
|
76
|
+
end
|
|
77
|
+
def close
|
|
78
|
+
return if @closed
|
|
79
|
+
@closed = true
|
|
80
|
+
@handles.each do |_, handle, event, overlapped, _|
|
|
81
|
+
@kernel.fn(:CancelIoEx, [P, P], I).call(handle, overlapped)
|
|
82
|
+
@kernel.fn(:GetOverlappedResult, [P, P, P, I], I, need_gvl: false).call(handle, overlapped, [0].pack("I"), 1)
|
|
83
|
+
@kernel.fn(:CloseHandle, [P], I).call(handle)
|
|
84
|
+
@kernel.fn(:CloseHandle, [P], I).call(event)
|
|
85
|
+
end
|
|
86
|
+
@handles.clear
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle/import"
|
|
4
|
+
require_relative "../../ffi/library"
|
|
5
|
+
require_relative "../../gpu/open_gl"
|
|
6
|
+
|
|
7
|
+
module Zaniah
|
|
8
|
+
module Platform
|
|
9
|
+
module Windows
|
|
10
|
+
def self.displays
|
|
11
|
+
user, scale = FFI::Library.new("user32.dll"), FFI::Library.new("shcore.dll")
|
|
12
|
+
ptype, int, uint = Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_UINT
|
|
13
|
+
result, error = [], nil
|
|
14
|
+
callback = Fiddle::Closure::BlockCaller.new(int, [ptype, ptype, ptype, Fiddle::TYPE_INTPTR_T]) do |monitor, _dc, _rect, _data|
|
|
15
|
+
begin
|
|
16
|
+
info = [104].pack("I") + "\0".b * 100
|
|
17
|
+
raise Error, "GetMonitorInfoW failed" if user.fn(:GetMonitorInfoW, [ptype, ptype], int).call(monitor, info).zero?
|
|
18
|
+
x, y, right, bottom = info[4, 16].unpack("l4")
|
|
19
|
+
dx, dy = [96].pack("I"), [96].pack("I")
|
|
20
|
+
scale.fn(:GetDpiForMonitor, [ptype, int, ptype, ptype], int).call(monitor, 0, dx, dy)
|
|
21
|
+
factor = dx.unpack1("I") / 96.0
|
|
22
|
+
name = info[40, 64].force_encoding("UTF-16LE").encode("UTF-8").split("\0", 2).first
|
|
23
|
+
result << Display.new(monitor.to_i, name, Bounds.new(x / factor, y / factor, (right - x) / factor, (bottom - y) / factor), factor, (info[36, 4].unpack1("I") & 1) != 0)
|
|
24
|
+
1
|
|
25
|
+
rescue StandardError => exception
|
|
26
|
+
error = exception
|
|
27
|
+
0
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
user.fn(:EnumDisplayMonitors, [ptype, ptype, ptype, Fiddle::TYPE_INTPTR_T], int).call(0, 0, callback, 0)
|
|
31
|
+
raise error if error
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
module Types
|
|
35
|
+
extend Fiddle::Importer
|
|
36
|
+
WindowClass = struct ["unsigned int size", "unsigned int style", "void *procedure", "int class_extra", "int window_extra", "void *instance", "void *icon", "void *cursor", "void *background", "void *menu_name", "void *class_name", "void *small_icon"]
|
|
37
|
+
PixelFormat = struct ["unsigned short size", "unsigned short version", "unsigned int flags", "unsigned char pixel_type", "unsigned char color_bits", "unsigned char red_bits", "unsigned char red_shift", "unsigned char green_bits", "unsigned char green_shift", "unsigned char blue_bits", "unsigned char blue_shift", "unsigned char alpha_bits", "unsigned char alpha_shift", "unsigned char accum_bits", "unsigned char accum_red_bits", "unsigned char accum_green_bits", "unsigned char accum_blue_bits", "unsigned char accum_alpha_bits", "unsigned char depth_bits", "unsigned char stencil_bits", "unsigned char aux_buffers", "unsigned char layer_type", "unsigned char reserved", "unsigned int layer_mask", "unsigned int visible_mask", "unsigned int damage_mask"]
|
|
38
|
+
OpenFileName = struct ["unsigned int size", "void *owner", "void *instance", "void *filter", "void *custom_filter", "unsigned int max_custom_filter", "unsigned int filter_index", "void *file", "unsigned int max_file", "void *file_title", "unsigned int max_file_title", "void *initial_dir", "void *title", "unsigned int flags", "unsigned short file_offset", "unsigned short extension_offset", "void *default_extension", "intptr_t custom_data", "void *hook", "void *template_name", "void *reserved", "unsigned int reserved_size", "unsigned int flags_ex"]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Window < Headless::Window
|
|
42
|
+
def displays = Windows.displays
|
|
43
|
+
I, U, P, N, V = Fiddle::TYPE_INT, Fiddle::TYPE_UINT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INTPTR_T, Fiddle::TYPE_VOID
|
|
44
|
+
INSTANCES = {}
|
|
45
|
+
attr_reader :handle
|
|
46
|
+
|
|
47
|
+
def initialize(gpu: :opengl, **options)
|
|
48
|
+
raise Error, "native Windows backend requires a 64-bit Ruby" unless Fiddle::SIZEOF_VOIDP == 8
|
|
49
|
+
super(**options)
|
|
50
|
+
@user, @kernel, @gdi = FFI::Library.new("user32.dll"), FFI::Library.new("kernel32.dll"), FFI::Library.new("gdi32.dll")
|
|
51
|
+
@shell, @imm, @gl = FFI::Library.new("shell32.dll"), FFI::Library.new("imm32.dll"), FFI::Library.new("opengl32.dll")
|
|
52
|
+
@user.fn(:SetProcessDpiAwarenessContext, [P], I).call(-4)
|
|
53
|
+
instance = @kernel.fn(:GetModuleHandleW, [P], P).call(0)
|
|
54
|
+
@class_name = wide("ZaniahWindow#{object_id}")
|
|
55
|
+
@procedure = Fiddle::Closure::BlockCaller.new(N, [P, U, N, N]) do |handle, message, wparam, lparam|
|
|
56
|
+
window = INSTANCES[handle.to_i]
|
|
57
|
+
if window
|
|
58
|
+
window.native_callback { window.message(message, wparam, lparam) }
|
|
59
|
+
else
|
|
60
|
+
@user.fn(:DefWindowProcW, [P, U, N, N], N).call(handle, message, wparam, lparam)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
definition = Types::WindowClass.malloc(Fiddle::RUBY_FREE)
|
|
64
|
+
definition.to_ptr[0, Types::WindowClass.size] = "\0" * Types::WindowClass.size
|
|
65
|
+
definition.size, definition.style, definition.procedure = Types::WindowClass.size, 0x0023, @procedure
|
|
66
|
+
definition.instance, definition.class_name = instance, Fiddle::Pointer[@class_name]
|
|
67
|
+
definition.cursor = @user.fn(:LoadCursorW, [P, P], P).call(0, 32512)
|
|
68
|
+
raise Error, "RegisterClassExW failed" if @user.fn(:RegisterClassExW, [P], I).call(definition).zero?
|
|
69
|
+
@instance = instance
|
|
70
|
+
@handle = @user.fn(:CreateWindowExW, [U, P, P, U, I, I, I, I, P, P, P, P], P).call(0, @class_name, wide(@title), 0x00CF0000, 100, 100, content_size.width.to_i, content_size.height.to_i, 0, 0, instance, 0)
|
|
71
|
+
raise Error, "CreateWindowExW failed" if @handle.null?
|
|
72
|
+
INSTANCES[@handle.to_i] = self
|
|
73
|
+
@scale_factor = @user.fn(:GetDpiForWindow, [P], U).call(@handle) / 96.0
|
|
74
|
+
client = "\0" * 16
|
|
75
|
+
@user.fn(:GetClientRect, [P, P], I).call(@handle, client)
|
|
76
|
+
_, _, width, height = client.unpack("l4")
|
|
77
|
+
@content_size = Size.new(width / @scale_factor, height / @scale_factor)
|
|
78
|
+
@dc = @user.fn(:GetDC, [P], P).call(@handle)
|
|
79
|
+
@device.release
|
|
80
|
+
@device = create_device(gpu)
|
|
81
|
+
@shell.fn(:DragAcceptFiles, [P, I], V).call(@handle, 1)
|
|
82
|
+
@user.fn(:ShowWindow, [P, I], I).call(@handle, 5)
|
|
83
|
+
@user.fn(:UpdateWindow, [P], I).call(@handle)
|
|
84
|
+
end
|
|
85
|
+
def wide(text) = text.to_s.encode("UTF-16LE").b + "\0\0".b
|
|
86
|
+
def create_device(backend)
|
|
87
|
+
raise ArgumentError, "Win32 supports :opengl" unless [:opengl, :gl].include?(backend)
|
|
88
|
+
descriptor = Types::PixelFormat.malloc(Fiddle::RUBY_FREE)
|
|
89
|
+
descriptor.to_ptr[0, Types::PixelFormat.size] = "\0" * Types::PixelFormat.size
|
|
90
|
+
descriptor.size, descriptor.version, descriptor.flags = Types::PixelFormat.size, 1, 0x25
|
|
91
|
+
descriptor.color_bits, descriptor.alpha_bits = 32, 8
|
|
92
|
+
format = @gdi.fn(:ChoosePixelFormat, [P, P], I).call(@dc, descriptor)
|
|
93
|
+
raise Error, "ChoosePixelFormat failed" if format.zero?
|
|
94
|
+
raise Error, "SetPixelFormat failed" if @gdi.fn(:SetPixelFormat, [P, I, P], I).call(@dc, format, descriptor).zero?
|
|
95
|
+
temporary = @gl.fn(:wglCreateContext, [P], P).call(@dc)
|
|
96
|
+
raise Error, "wglCreateContext failed" if temporary.null?
|
|
97
|
+
@gl.fn(:wglMakeCurrent, [P, P], I).call(@dc, temporary)
|
|
98
|
+
resolver = ->(name) { @gl.fn(:wglGetProcAddress, [P], P).call(name) }
|
|
99
|
+
address = resolver.call("wglCreateContextAttribsARB")
|
|
100
|
+
raise Error, "WGL_ARB_create_context unavailable" if address.to_i <= 3 || address.to_i == -1
|
|
101
|
+
attributes = [0x2091, 3, 0x2092, 3, 0x9126, 1, 0].pack("i*")
|
|
102
|
+
@context = Fiddle::Function.new(address, [P, P, P], P).call(@dc, 0, attributes)
|
|
103
|
+
@gl.fn(:wglMakeCurrent, [P, P], I).call(0, 0)
|
|
104
|
+
@gl.fn(:wglDeleteContext, [P], I).call(temporary)
|
|
105
|
+
raise Error, "OpenGL 3.3 core context unavailable" if @context.null?
|
|
106
|
+
make_current
|
|
107
|
+
swap = resolver.call("wglSwapIntervalEXT")
|
|
108
|
+
Fiddle::Function.new(swap, [I], I).call(1) if swap.to_i > 3
|
|
109
|
+
GPU::OpenGL.new(self, library: @gl, resolver: resolver)
|
|
110
|
+
end
|
|
111
|
+
def make_current
|
|
112
|
+
raise Error, "wglMakeCurrent failed" if @gl.fn(:wglMakeCurrent, [P, P], I).call(@dc, @context).zero?
|
|
113
|
+
end
|
|
114
|
+
def swap_buffers = @gdi.fn(:SwapBuffers, [P], I).call(@dc)
|
|
115
|
+
def title=(title)
|
|
116
|
+
super
|
|
117
|
+
@user.fn(:SetWindowTextW, [P, P], I).call(@handle, wide(title)) if @handle
|
|
118
|
+
end
|
|
119
|
+
def tick
|
|
120
|
+
event = "\0" * 48
|
|
121
|
+
while !closed? && @user.fn(:PeekMessageW, [P, P, U, U, U], I).call(event, 0, 0, 0, 1) != 0
|
|
122
|
+
@user.fn(:TranslateMessage, [P], I).call(event)
|
|
123
|
+
@user.fn(:DispatchMessageW, [P], N).call(event)
|
|
124
|
+
end
|
|
125
|
+
raise @native_error if @native_error
|
|
126
|
+
super
|
|
127
|
+
end
|
|
128
|
+
def run
|
|
129
|
+
until closed?
|
|
130
|
+
tick
|
|
131
|
+
@user.fn(:MsgWaitForMultipleObjectsEx, [U, P, U, U, U], U).call(0, 0, 50, 0x04FF, 4) unless dirty? || closed?
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
def native_callback
|
|
135
|
+
yield
|
|
136
|
+
rescue StandardError => error
|
|
137
|
+
@native_error = error
|
|
138
|
+
0
|
|
139
|
+
end
|
|
140
|
+
def modifiers
|
|
141
|
+
[[0x11, "ctrl"], [0x12, "alt"], [0x10, "shift"], [0x5B, "cmd"]].filter_map do |code, name|
|
|
142
|
+
name unless (@user.fn(:GetKeyState, [I], Fiddle::TYPE_SHORT).call(code) & 0x8000).zero?
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
KEYS = {8 => "backspace", 9 => "tab", 13 => "enter", 27 => "esc", 32 => "space", 33 => "pageup", 34 => "pagedown", 35 => "end", 36 => "home", 37 => "left", 38 => "up", 39 => "right", 40 => "down", 46 => "delete"}.freeze
|
|
146
|
+
def message(message, wparam, lparam)
|
|
147
|
+
case message
|
|
148
|
+
when 0x0010 then close; return 0
|
|
149
|
+
when 0x0233
|
|
150
|
+
dropped_files(wparam)
|
|
151
|
+
return 0
|
|
152
|
+
when 0x001a, 0x031a
|
|
153
|
+
@on_appearance&.call(appearance)
|
|
154
|
+
request_frame
|
|
155
|
+
when 0x0005
|
|
156
|
+
width, height = lparam & 0xffff, (lparam >> 16) & 0xffff
|
|
157
|
+
resize(width / @scale_factor, height / @scale_factor) if width.positive? && height.positive?
|
|
158
|
+
when 0x0003 then @on_moved&.call([signed16(lparam), signed16(lparam >> 16)])
|
|
159
|
+
when 0x02e0
|
|
160
|
+
@scale_factor = (wparam & 0xffff) / 96.0
|
|
161
|
+
left, top, right, bottom = Fiddle::Pointer.new(lparam)[0, 16].unpack("l4")
|
|
162
|
+
@user.fn(:SetWindowPos, [P, P, I, I, I, I, U], I).call(@handle, 0, left, top, right - left, bottom - top, 0x0014)
|
|
163
|
+
when 0x000f
|
|
164
|
+
paint = "\0" * 128
|
|
165
|
+
@user.fn(:BeginPaint, [P, P], P).call(@handle, paint)
|
|
166
|
+
@user.fn(:EndPaint, [P, P], I).call(@handle, paint)
|
|
167
|
+
request_frame
|
|
168
|
+
return 0
|
|
169
|
+
when 0x0100, 0x0101, 0x0104, 0x0105
|
|
170
|
+
character = @user.fn(:MapVirtualKeyW, [U, U], U).call(wparam, 2) & 0x7fffffff
|
|
171
|
+
name = KEYS[wparam] || ((0x70..0x87).cover?(wparam) ? "f#{wparam - 0x6f}" : character.between?(32, 0x10ffff) ? character.chr(Encoding::UTF_8).downcase : "key#{wparam}")
|
|
172
|
+
stroke = (modifiers + [name]).join("-")
|
|
173
|
+
input([0x0100, 0x0104].include?(message) ? Input::KeyDown.new(stroke, (lparam & (1 << 30)) != 0) : Input::KeyUp.new(stroke))
|
|
174
|
+
return 0 unless [0x0104, 0x0105].include?(message)
|
|
175
|
+
when 0x0102
|
|
176
|
+
if (0xd800..0xdbff).cover?(wparam)
|
|
177
|
+
@high_surrogate = wparam
|
|
178
|
+
elsif (0xdc00..0xdfff).cover?(wparam) && @high_surrogate
|
|
179
|
+
commit_character(0x10000 + ((@high_surrogate - 0xd800) << 10) + wparam - 0xdc00)
|
|
180
|
+
@high_surrogate = nil
|
|
181
|
+
else
|
|
182
|
+
@high_surrogate = nil
|
|
183
|
+
commit_character(wparam) unless (0xd800..0xdfff).cover?(wparam)
|
|
184
|
+
end
|
|
185
|
+
return 0
|
|
186
|
+
when 0x0109
|
|
187
|
+
return 1 if wparam == 0xffff
|
|
188
|
+
commit_character(wparam)
|
|
189
|
+
return 0
|
|
190
|
+
when 0x010d, 0x010e then input(Input::Composition.new("", [0, 0]))
|
|
191
|
+
when 0x010f
|
|
192
|
+
with_ime do |context|
|
|
193
|
+
if (lparam & 0x800) != 0
|
|
194
|
+
input(Input::Composition.new("", [0, 0]))
|
|
195
|
+
input(Input::TextInput.new(composition_string(context, 0x800)))
|
|
196
|
+
end
|
|
197
|
+
if (lparam & 8) != 0
|
|
198
|
+
text = composition_string(context, 8)
|
|
199
|
+
cursor = @imm.fn(:ImmGetCompositionStringW, [P, U, P, U], I).call(context, 0x80, 0, 0)
|
|
200
|
+
input(Input::Composition.new(text, [[cursor, 0].max, 0]))
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
return 0
|
|
204
|
+
when 0x0200, 0x0201, 0x0202, 0x0203, 0x0204, 0x0205, 0x0206, 0x0207, 0x0208, 0x0209, 0x020a, 0x020e
|
|
205
|
+
mouse_message(message, wparam, lparam)
|
|
206
|
+
return 0
|
|
207
|
+
end
|
|
208
|
+
@user.fn(:DefWindowProcW, [P, U, N, N], N).call(@handle, message, wparam, lparam)
|
|
209
|
+
end
|
|
210
|
+
def signed16(number) = (number & 0xffff) >= 0x8000 ? (number & 0xffff) - 0x10000 : number & 0xffff
|
|
211
|
+
def commit_character(code)
|
|
212
|
+
input(Input::TextInput.new(code.chr(Encoding::UTF_8))) if code >= 32 && code != 127 && code <= 0x10ffff
|
|
213
|
+
end
|
|
214
|
+
def mouse_message(message, wparam, lparam)
|
|
215
|
+
x, y = signed16(lparam), signed16(lparam >> 16)
|
|
216
|
+
if [0x020a, 0x020e].include?(message)
|
|
217
|
+
point = [x, y].pack("l2")
|
|
218
|
+
@user.fn(:ScreenToClient, [P, P], I).call(@handle, point)
|
|
219
|
+
x, y = point.unpack("l2")
|
|
220
|
+
end
|
|
221
|
+
position, mods = Point.new(x / @scale_factor, y / @scale_factor), modifiers
|
|
222
|
+
if [0x020a, 0x020e].include?(message)
|
|
223
|
+
amount = signed16(wparam >> 16) / 3.0
|
|
224
|
+
input(Input::ScrollWheel.new(position, message == 0x020a ? Point.new(0, -amount) : Point.new(amount, 0), :changed, mods))
|
|
225
|
+
elsif message == 0x0200
|
|
226
|
+
input(Input::MouseMove.new(position, mods))
|
|
227
|
+
else
|
|
228
|
+
double = [0x0203, 0x0206, 0x0209].include?(message)
|
|
229
|
+
down = double || [0x0201, 0x0204, 0x0207].include?(message)
|
|
230
|
+
button = message <= 0x0203 ? :left : message <= 0x0206 ? :right : :middle
|
|
231
|
+
down ? @user.fn(:SetCapture, [P], P).call(@handle) : @user.fn(:ReleaseCapture, [], I).call
|
|
232
|
+
input(down ? Input::MouseDown.new(position, button, mods, double ? 2 : 1) : Input::MouseUp.new(position, button, mods))
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
def with_ime
|
|
236
|
+
context = @imm.fn(:ImmGetContext, [P], P).call(@handle)
|
|
237
|
+
yield context unless context.null?
|
|
238
|
+
ensure
|
|
239
|
+
@imm.fn(:ImmReleaseContext, [P, P], I).call(@handle, context) if context && !context.null?
|
|
240
|
+
end
|
|
241
|
+
def composition_string(context, kind)
|
|
242
|
+
size = @imm.fn(:ImmGetCompositionStringW, [P, U, P, U], I).call(context, kind, 0, 0)
|
|
243
|
+
return "" unless size.positive?
|
|
244
|
+
bytes = "\0" * size
|
|
245
|
+
@imm.fn(:ImmGetCompositionStringW, [P, U, P, U], I).call(context, kind, bytes, size)
|
|
246
|
+
bytes.force_encoding("UTF-16LE").encode("UTF-8", invalid: :replace)
|
|
247
|
+
end
|
|
248
|
+
def ime_state=(bounds)
|
|
249
|
+
@ime_state = bounds
|
|
250
|
+
return unless @handle && bounds.is_a?(Bounds)
|
|
251
|
+
with_ime do |context|
|
|
252
|
+
point = [(bounds.x * @scale_factor).to_i, (bounds.bottom * @scale_factor).to_i]
|
|
253
|
+
form = [0x20, *point, 0, 0, 0, 0].pack("l7")
|
|
254
|
+
@imm.fn(:ImmSetCompositionWindow, [P, P], I).call(context, form)
|
|
255
|
+
candidate = [0, 0x40, *point, 0, 0, 0, 0].pack("l8")
|
|
256
|
+
@imm.fn(:ImmSetCandidateWindow, [P, P], I).call(context, candidate)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
def clipboard
|
|
260
|
+
with_clipboard do
|
|
261
|
+
memory = @user.fn(:GetClipboardData, [U], P).call(13)
|
|
262
|
+
next "" if memory.null?
|
|
263
|
+
pointer = @kernel.fn(:GlobalLock, [P], P).call(memory)
|
|
264
|
+
next "" if pointer.null?
|
|
265
|
+
size = @kernel.fn(:GlobalSize, [P], Fiddle::TYPE_SIZE_T).call(memory)
|
|
266
|
+
values = pointer[0, size].unpack("v*").take_while { |value| !value.zero? }
|
|
267
|
+
@kernel.fn(:GlobalUnlock, [P], I).call(memory)
|
|
268
|
+
values.pack("v*").force_encoding("UTF-16LE").encode("UTF-8", invalid: :replace)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
def clipboard=(text)
|
|
272
|
+
bytes = wide(text)
|
|
273
|
+
with_clipboard do
|
|
274
|
+
memory = @kernel.fn(:GlobalAlloc, [U, Fiddle::TYPE_SIZE_T], P).call(2, bytes.bytesize)
|
|
275
|
+
raise NoMemoryError, "clipboard allocation failed" if memory.null?
|
|
276
|
+
pointer = @kernel.fn(:GlobalLock, [P], P).call(memory)
|
|
277
|
+
pointer[0, bytes.bytesize] = bytes
|
|
278
|
+
@kernel.fn(:GlobalUnlock, [P], I).call(memory)
|
|
279
|
+
@user.fn(:EmptyClipboard, [], I).call
|
|
280
|
+
result = @user.fn(:SetClipboardData, [U, P], P).call(13, memory)
|
|
281
|
+
if result.null?
|
|
282
|
+
@kernel.fn(:GlobalFree, [P], P).call(memory)
|
|
283
|
+
raise Error, "SetClipboardData failed"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
def with_clipboard
|
|
288
|
+
raise Error, "clipboard is busy" if @user.fn(:OpenClipboard, [P], I).call(@handle).zero?
|
|
289
|
+
begin
|
|
290
|
+
yield
|
|
291
|
+
ensure
|
|
292
|
+
@user.fn(:CloseClipboard, [], I).call
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
def clipboard_paths
|
|
296
|
+
with_clipboard do
|
|
297
|
+
drop = @user.fn(:GetClipboardData, [U], P).call(15)
|
|
298
|
+
next [] if drop.null?
|
|
299
|
+
count = @shell.fn(:DragQueryFileW, [P, U, P, U], U).call(drop, 0xffffffff, 0, 0)
|
|
300
|
+
Array.new(count) do |i|
|
|
301
|
+
size = @shell.fn(:DragQueryFileW, [P, U, P, U], U).call(drop, i, 0, 0)
|
|
302
|
+
bytes = "\0" * ((size + 1) * 2)
|
|
303
|
+
@shell.fn(:DragQueryFileW, [P, U, P, U], U).call(drop, i, bytes, size + 1)
|
|
304
|
+
bytes.byteslice(0, size * 2).force_encoding("UTF-16LE").encode("UTF-8")
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
def dropped_files(drop)
|
|
309
|
+
count = @shell.fn(:DragQueryFileW, [P, U, P, U], U).call(drop, 0xffffffff, 0, 0)
|
|
310
|
+
paths = Array.new(count) do |index|
|
|
311
|
+
size = @shell.fn(:DragQueryFileW, [P, U, P, U], U).call(drop, index, 0, 0)
|
|
312
|
+
bytes = "\0" * ((size + 1) * 2)
|
|
313
|
+
@shell.fn(:DragQueryFileW, [P, U, P, U], U).call(drop, index, bytes, size + 1)
|
|
314
|
+
bytes.byteslice(0, size * 2).force_encoding("UTF-16LE").encode("UTF-8")
|
|
315
|
+
end
|
|
316
|
+
point = "\0" * 8
|
|
317
|
+
@shell.fn(:DragQueryPoint, [P, P], I).call(drop, point)
|
|
318
|
+
x, y = point.unpack("l2")
|
|
319
|
+
input(Input::FileDrop.new(paths.freeze, Point.new(x / @scale_factor, y / @scale_factor)))
|
|
320
|
+
ensure
|
|
321
|
+
@shell.fn(:DragFinish, [P], V).call(drop)
|
|
322
|
+
end
|
|
323
|
+
def appearance
|
|
324
|
+
registry = FFI::Library.new("advapi32.dll")
|
|
325
|
+
value, size = [1].pack("I"), [4].pack("I")
|
|
326
|
+
result = registry.fn(:RegGetValueW, [P, P, P, U, P, P, P], I).call(-2147483647,
|
|
327
|
+
wide("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"), wide("AppsUseLightTheme"), 0x10, 0, value, size)
|
|
328
|
+
result.zero? && value.unpack1("I").zero? ? :dark : :light
|
|
329
|
+
end
|
|
330
|
+
def on_appearance(&block) = @on_appearance = block
|
|
331
|
+
def context_menu(items, position: nil)
|
|
332
|
+
menu = @user.fn(:CreatePopupMenu, [], P).call
|
|
333
|
+
raise Error, "CreatePopupMenu failed" if menu.null?
|
|
334
|
+
items.each_with_index do |(label, action), index|
|
|
335
|
+
@user.fn(:AppendMenuW, [P, U, N, P], I).call(menu, action ? 0 : 1, index + 1, wide(label))
|
|
336
|
+
end
|
|
337
|
+
point = position ? [(position.x * @scale_factor).round, (position.y * @scale_factor).round].pack("l2") : "\0" * 8
|
|
338
|
+
position ? @user.fn(:ClientToScreen, [P, P], I).call(@handle, point) : @user.fn(:GetCursorPos, [P], I).call(point)
|
|
339
|
+
x, y = point.unpack("l2")
|
|
340
|
+
selected = @user.fn(:TrackPopupMenuEx, [P, U, I, I, P, P], U).call(menu, 0x182, x, y, @handle, 0)
|
|
341
|
+
items[selected - 1][1]&.call if selected.positive?
|
|
342
|
+
ensure
|
|
343
|
+
@user.fn(:DestroyMenu, [P], I).call(menu) if menu && !menu.null?
|
|
344
|
+
end
|
|
345
|
+
def cursor_style=(style)
|
|
346
|
+
id = {arrow: 32512, text: 32513, pointer: 32649, crosshair: 32515, resize_horizontal: 32644, resize_vertical: 32645}.fetch(style)
|
|
347
|
+
cursor = @user.fn(:LoadCursorW, [P, P], P).call(0, id)
|
|
348
|
+
@user.fn(:SetCursor, [P], P).call(cursor)
|
|
349
|
+
end
|
|
350
|
+
def prompt_for_paths(multiple: false, directories: false, save: false)
|
|
351
|
+
return directory_dialog if directories
|
|
352
|
+
dialog = Types::OpenFileName.malloc(Fiddle::RUBY_FREE)
|
|
353
|
+
dialog.to_ptr[0, Types::OpenFileName.size] = "\0" * Types::OpenFileName.size
|
|
354
|
+
bytes = "\0" * 131_072
|
|
355
|
+
dialog.size, dialog.owner, dialog.file, dialog.max_file = Types::OpenFileName.size, @handle, Fiddle::Pointer[bytes], bytes.bytesize / 2
|
|
356
|
+
dialog.flags = 0x00080000 | 0x00000800 | (multiple ? 0x200 : 0) | (save ? 2 : 0x1000)
|
|
357
|
+
library = FFI::Library.new("comdlg32.dll")
|
|
358
|
+
return [] if library.fn(save ? :GetSaveFileNameW : :GetOpenFileNameW, [P], I).call(dialog).zero?
|
|
359
|
+
entries = bytes.force_encoding("UTF-16LE").encode("UTF-8").split("\0").take_while { |text| !text.empty? }
|
|
360
|
+
entries.length > 1 ? entries.drop(1).map { |file| File.join(entries.first, file) } : entries
|
|
361
|
+
end
|
|
362
|
+
def directory_dialog
|
|
363
|
+
# BROWSEINFOW is pointer-only except flags and the final image index.
|
|
364
|
+
title = wide("Choose a directory")
|
|
365
|
+
info = [@handle.to_i, 0, 0, Fiddle::Pointer[title].to_i, 0x41, 0, 0, 0].pack("J4Ix4J2Ix4")
|
|
366
|
+
@shell.fn(:SHBrowseForFolderW, [P], P).call(info).then do |pidl|
|
|
367
|
+
return [] if pidl.null?
|
|
368
|
+
bytes = "\0" * 65_536
|
|
369
|
+
ok = @shell.fn(:SHGetPathFromIDListEx, [P, P, U, U], I).call(pidl, bytes, bytes.bytesize / 2, 0)
|
|
370
|
+
FFI::Library.new("ole32.dll").fn(:CoTaskMemFree, [P], V).call(pidl)
|
|
371
|
+
ok.zero? ? [] : [bytes.force_encoding("UTF-16LE").encode("UTF-8").split("\0", 2).first]
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
def open_url(url)
|
|
375
|
+
@shell.fn(:ShellExecuteW, [P, P, P, P, P, I], P).call(@handle, wide("open"), wide(url), 0, 0, 1).to_i > 32
|
|
376
|
+
end
|
|
377
|
+
def toggle_fullscreen
|
|
378
|
+
if @restore_rect
|
|
379
|
+
@user.fn(:SetWindowLongPtrW, [P, I, N], N).call(@handle, -16, 0x00CF0000)
|
|
380
|
+
left, top, right, bottom = @restore_rect.unpack("l4")
|
|
381
|
+
@user.fn(:SetWindowPos, [P, P, I, I, I, I, U], I).call(@handle, 0, left, top, right - left, bottom - top, 0x0020)
|
|
382
|
+
@restore_rect = nil
|
|
383
|
+
else
|
|
384
|
+
@restore_rect = "\0" * 16
|
|
385
|
+
@user.fn(:GetWindowRect, [P, P], I).call(@handle, @restore_rect)
|
|
386
|
+
monitor = @user.fn(:MonitorFromWindow, [P, U], P).call(@handle, 2)
|
|
387
|
+
info = [40].pack("I") + "\0" * 36
|
|
388
|
+
@user.fn(:GetMonitorInfoW, [P, P], I).call(monitor, info)
|
|
389
|
+
left, top, right, bottom = info[4, 16].unpack("l4")
|
|
390
|
+
@user.fn(:SetWindowLongPtrW, [P, I, N], N).call(@handle, -16, 0x80000000)
|
|
391
|
+
@user.fn(:SetWindowPos, [P, P, I, I, I, I, U], I).call(@handle, 0, left, top, right - left, bottom - top, 0x0020)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
def close
|
|
395
|
+
return false unless super
|
|
396
|
+
@gl.fn(:wglMakeCurrent, [P, P], I).call(0, 0)
|
|
397
|
+
@gl.fn(:wglDeleteContext, [P], I).call(@context)
|
|
398
|
+
@user.fn(:ReleaseDC, [P, P], I).call(@handle, @dc)
|
|
399
|
+
INSTANCES.delete(@handle.to_i)
|
|
400
|
+
@user.fn(:DestroyWindow, [P], I).call(@handle)
|
|
401
|
+
@user.fn(:UnregisterClassW, [P, P], I).call(@class_name, @instance)
|
|
402
|
+
true
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "platform/display"
|
|
4
|
+
require_relative "platform/headless/window"
|
|
5
|
+
require_relative "platform/tui/window"
|
|
6
|
+
|
|
7
|
+
module Zaniah
|
|
8
|
+
module Platform
|
|
9
|
+
def self.displays(backend: :auto, **options)
|
|
10
|
+
backend = RUBY_PLATFORM.include?("darwin") ? :mac : RUBY_PLATFORM.match?(/mswin|mingw/) ? :windows : :linux if backend == :auto
|
|
11
|
+
return [] if [:headless, :tui].include?(backend)
|
|
12
|
+
raise ArgumentError, "unknown platform #{backend}" unless [:mac, :linux, :windows].include?(backend)
|
|
13
|
+
require_relative "platform/#{backend}"
|
|
14
|
+
const_get(backend.to_s.capitalize).displays(**options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.watch(paths, latency: 0.05)
|
|
18
|
+
platform = RUBY_PLATFORM.include?("darwin") ? :mac : RUBY_PLATFORM.match?(/mswin|mingw/) ? :windows : :linux
|
|
19
|
+
require_relative "platform/#{platform}/watcher"
|
|
20
|
+
const_get(platform.to_s.capitalize)::Watcher.new(paths, latency: latency)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.open_window(backend: :headless, **options)
|
|
24
|
+
case backend
|
|
25
|
+
when :headless then Headless::Window.new(**options)
|
|
26
|
+
when :tui then TUI::Window.new(**options)
|
|
27
|
+
when :mac, :linux, :windows
|
|
28
|
+
require_relative "platform/#{backend}"
|
|
29
|
+
const_get(backend.to_s.capitalize)::Window.new(**options)
|
|
30
|
+
else raise ArgumentError, "unknown platform #{backend}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|