rbgl 0.1.0 → 1.0.1

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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +20 -2
  4. data/README.md +130 -74
  5. data/Rakefile +7 -0
  6. data/bench/renderer_bench.rb +283 -0
  7. data/docs/.nojekyll +1 -0
  8. data/docs/index.html +38 -0
  9. data/examples/array_test.rb +1 -1
  10. data/examples/color_test.rb +1 -1
  11. data/examples/cube_spinning.rb +1 -1
  12. data/examples/gradient.rb +1 -1
  13. data/examples/multi_return_test.rb +1 -1
  14. data/examples/plasma.rb +1 -1
  15. data/examples/sphere_raymarch.rb +1 -1
  16. data/examples/triangle_window.rb +1 -1
  17. data/exe/rbgl +42 -0
  18. data/lib/rbgl/cli/doctor.rb +155 -0
  19. data/lib/rbgl/engine/buffer.rb +150 -40
  20. data/lib/rbgl/engine/clip_space_clipper.rb +163 -0
  21. data/lib/rbgl/engine/context.rb +133 -63
  22. data/lib/rbgl/engine/framebuffer.rb +153 -48
  23. data/lib/rbgl/engine/immutable_color.rb +32 -0
  24. data/lib/rbgl/engine/pipeline.rb +38 -7
  25. data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +195 -0
  26. data/lib/rbgl/engine/rasterizer/line_renderer.rb +88 -0
  27. data/lib/rbgl/engine/rasterizer/point_renderer.rb +35 -0
  28. data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +136 -0
  29. data/lib/rbgl/engine/rasterizer.rb +65 -171
  30. data/lib/rbgl/engine/shader/base_shader.rb +51 -0
  31. data/lib/rbgl/engine/shader/builtins.rb +257 -0
  32. data/lib/rbgl/engine/shader/dynamic_data.rb +73 -0
  33. data/lib/rbgl/engine/shader.rb +5 -318
  34. data/lib/rbgl/engine/texture.rb +264 -51
  35. data/lib/rbgl/engine.rb +4 -1
  36. data/lib/rbgl/gui/backend.rb +21 -34
  37. data/lib/rbgl/gui/backend_factory.rb +89 -0
  38. data/lib/rbgl/gui/cocoa/backend.rb +59 -46
  39. data/lib/rbgl/gui/cocoa/key_mapper.rb +48 -0
  40. data/lib/rbgl/gui/event.rb +4 -2
  41. data/lib/rbgl/gui/file_backend/bmp_writer.rb +63 -0
  42. data/lib/rbgl/gui/file_backend/frame_writer.rb +35 -0
  43. data/lib/rbgl/gui/file_backend/png_writer.rb +18 -0
  44. data/lib/rbgl/gui/file_backend/ppm_writer.rb +26 -0
  45. data/lib/rbgl/gui/file_backend.rb +50 -55
  46. data/lib/rbgl/gui/wayland/backend.rb +214 -60
  47. data/lib/rbgl/gui/wayland/codec.rb +50 -0
  48. data/lib/rbgl/gui/wayland/connection.rb +188 -231
  49. data/lib/rbgl/gui/wayland/event_dispatcher.rb +165 -0
  50. data/lib/rbgl/gui/wayland/global_binder.rb +46 -0
  51. data/lib/rbgl/gui/wayland/input_mapper.rb +61 -0
  52. data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +63 -0
  53. data/lib/rbgl/gui/wayland/protocol_objects/display.rb +54 -0
  54. data/lib/rbgl/gui/wayland/protocol_objects/seat.rb +115 -0
  55. data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +157 -0
  56. data/lib/rbgl/gui/wayland/protocol_objects/surface.rb +33 -0
  57. data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +60 -0
  58. data/lib/rbgl/gui/wayland/protocol_objects.rb +8 -0
  59. data/lib/rbgl/gui/window/event_dispatcher.rb +30 -0
  60. data/lib/rbgl/gui/window/render_loop.rb +111 -0
  61. data/lib/rbgl/gui/window.rb +74 -84
  62. data/lib/rbgl/gui/x11/atom_cache.rb +29 -0
  63. data/lib/rbgl/gui/x11/backend.rb +73 -49
  64. data/lib/rbgl/gui/x11/connection.rb +284 -219
  65. data/lib/rbgl/gui/x11/event_parser.rb +71 -0
  66. data/lib/rbgl/gui/x11/key_mapper.rb +48 -0
  67. data/lib/rbgl/gui/x11/pixel_encoder.rb +164 -0
  68. data/lib/rbgl/gui/x11/request_encoder.rb +155 -0
  69. data/lib/rbgl/gui/x11/setup_parser.rb +138 -0
  70. data/lib/rbgl/gui/x11/transport.rb +81 -0
  71. data/lib/rbgl/gui/x11/x_authority.rb +107 -0
  72. data/lib/rbgl/gui.rb +3 -5
  73. data/lib/rbgl/version.rb +1 -1
  74. metadata +63 -24
  75. data/examples/chemical_heartbeat.rb +0 -166
  76. data/examples/dark_transit.rb +0 -166
  77. data/examples/fractured_orb.rb +0 -428
  78. data/examples/fractured_orb_rb.rb +0 -598
  79. data/examples/hexagonal_flow.rb +0 -333
  80. data/examples/teapot.rb +0 -362
  81. data/examples/teapot_mcu.rb +0 -344
@@ -1,86 +1,106 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "socket"
4
+ require_relative "../backend"
5
+ require_relative "atom_cache"
6
+ require_relative "event_parser"
7
+ require_relative "key_mapper"
8
+ require_relative "request_encoder"
9
+ require_relative "setup_parser"
10
+ require_relative "transport"
11
+ require_relative "x_authority"
4
12
 
5
13
  module RBGL
6
14
  module GUI
7
15
  module X11
8
16
  class Connection
17
+ DEFAULT_REPLY_TIMEOUT = 5.0
18
+
19
+ ATOM_NAMES = {
20
+ wm_protocols: "WM_PROTOCOLS",
21
+ wm_delete_window: "WM_DELETE_WINDOW"
22
+ }.freeze
23
+
9
24
  attr_reader :default_screen, :resource_id_base, :resource_id_mask
10
25
  attr_reader :root, :root_depth, :root_visual, :white_pixel, :black_pixel
26
+ attr_reader :maximum_request_length, :bits_per_pixel, :scanline_pad
27
+ attr_reader :visual_class, :red_mask, :green_mask, :blue_mask, :image_byte_order
11
28
 
12
- def initialize(display_name)
13
- host, display_num, _screen_num = parse_display_name(display_name)
29
+ def initialize(display_name, env: ENV, reply_timeout: DEFAULT_REPLY_TIMEOUT)
30
+ host, display_num, screen_num = parse_display_name(display_name)
14
31
  @socket = connect(host, display_num)
15
- @next_seq = 1
32
+ @transport = Transport.new(@socket)
16
33
  @resource_id_counter = 0
17
34
  @pending_events = []
18
-
19
- handshake
35
+ @pending_replies = {}
36
+ @sequence = 0
37
+ @default_screen = screen_num
38
+ @reply_timeout = reply_timeout
39
+
40
+ handshake(host: host, display_num: display_num, env: env)
41
+ load_keyboard_mapping
42
+ rescue StandardError
43
+ @transport&.close
44
+ raise
20
45
  end
21
46
 
22
47
  def generate_id
23
- id = @resource_id_base | @resource_id_counter
48
+ masked_counter = @resource_id_counter & @resource_id_mask
49
+ raise GUI::BackendUnavailable, "X11 resource ID space exhausted" if masked_counter != @resource_id_counter
50
+
51
+ id = @resource_id_base | masked_counter
24
52
  @resource_id_counter += 1
25
53
  id
26
54
  end
27
55
 
28
56
  def flush
29
- @socket.flush
57
+ @transport.flush
30
58
  end
31
59
 
32
- def pending
33
- ready = IO.select([@socket], nil, nil, 0)
34
- ready ? 1 : 0
60
+ def atom(name)
61
+ atom_cache.fetch(name)
35
62
  end
36
63
 
37
- def create_window(depth:, wid:, parent:, x:, y:, width:, height:,
38
- border_width:, window_class:, visual:, value_mask:, values:)
39
- class_val = case window_class
40
- when :input_output then 1
41
- when :input_only then 2
42
- else 0
43
- end
44
-
45
- mask = 0
46
- value_list = []
47
-
48
- if value_mask.include?(:back_pixel)
49
- mask |= 0x0002
50
- value_list << values[:back_pixel]
51
- end
64
+ def wm_delete_window_atom
65
+ atom(:wm_delete_window)
66
+ end
52
67
 
53
- if value_mask.include?(:event_mask)
54
- mask |= 0x0800
55
- event_mask = 0
56
- values[:event_mask].each do |ev|
57
- event_mask |= case ev
58
- when :exposure then 0x8000
59
- when :key_press then 0x0001
60
- when :key_release then 0x0002
61
- when :button_press then 0x0004
62
- when :button_release then 0x0008
63
- when :pointer_motion then 0x0040
64
- when :structure_notify then 0x020000
65
- else 0
66
- end
67
- end
68
- value_list << event_mask
69
- end
68
+ def wm_protocols_atom
69
+ atom(:wm_protocols)
70
+ end
71
+
72
+ def enable_wm_delete_window(window)
73
+ change_property(window, :wm_protocols, :atom, [wm_delete_window_atom], format: 32)
74
+ end
75
+
76
+ def key_for_keycode(keycode)
77
+ load_keyboard_mapping unless @keyboard_mapping
78
+ keysyms = @keyboard_mapping[keycode]
79
+ keysym = keysyms&.find { |candidate| !candidate.zero? }
80
+ KeyMapper.key(keysym, fallback: keycode)
81
+ end
70
82
 
71
- request = [
72
- depth,
73
- wid,
74
- parent,
75
- x, y,
76
- width, height,
77
- border_width,
78
- class_val,
79
- visual,
80
- mask
81
- ].pack("CVVSSSSSVV") + value_list.pack("V*")
83
+ def pending
84
+ @pending_events.length + @transport.pending
85
+ end
82
86
 
83
- send_request(1, request)
87
+ def create_window(depth:, wid:, parent:, x:, y:, width:, height:,
88
+ border_width:, window_class:, visual:, value_mask:, values:)
89
+ depth_byte, request = request_encoder.create_window_data(
90
+ depth: depth,
91
+ wid: wid,
92
+ parent: parent,
93
+ x: x,
94
+ y: y,
95
+ width: width,
96
+ height: height,
97
+ border_width: border_width,
98
+ window_class: window_class,
99
+ visual: visual,
100
+ value_mask: value_mask,
101
+ values: values
102
+ )
103
+ send_request(1, request, depth_byte)
84
104
  end
85
105
 
86
106
  def map_window(wid)
@@ -92,82 +112,56 @@ module RBGL
92
112
  end
93
113
 
94
114
  def create_gc(gc_id, drawable, values = {})
95
- mask = 0
96
- value_list = []
97
-
98
- if values[:foreground]
99
- mask |= 0x0004
100
- value_list << values[:foreground]
101
- end
102
-
103
- if values[:background]
104
- mask |= 0x0008
105
- value_list << values[:background]
106
- end
107
-
108
- request = [gc_id, drawable, mask].pack("VVV") + value_list.pack("V*")
115
+ request = request_encoder.create_gc_data(gc_id, drawable, values)
109
116
  send_request(55, request)
110
117
  end
111
118
 
112
- def put_image(format:, drawable:, gc:, width:, height:, dst_x:, dst_y:, depth:, data:)
113
- format_byte = case format
114
- when :bitmap then 0
115
- when :xy_pixmap then 1
116
- when :z_pixmap then 2
117
- else 2
118
- end
119
-
120
- left_pad = 0
121
-
122
- header = [
123
- drawable,
124
- gc,
125
- width, height,
126
- dst_x, dst_y,
127
- left_pad,
128
- depth
129
- ].pack("VVvvvvCC") + "\x00\x00"
119
+ def put_image(format:, drawable:, gc:, width:, height:, dst_x:, dst_y:, depth:, data:, bytes_per_line:)
120
+ maximum_data = (@maximum_request_length * 4) - 24
121
+ if bytes_per_line > maximum_data
122
+ raise GUI::BackendUnavailable, "An X11 image row exceeds the server request limit"
123
+ end
130
124
 
131
- send_request_with_data(72, format_byte, header, data)
125
+ rows_per_request = [maximum_data / bytes_per_line, 1].max
126
+ offset_y = 0
127
+ while offset_y < height
128
+ row_count = [rows_per_request, height - offset_y].min
129
+ chunk = data.byteslice(offset_y * bytes_per_line, row_count * bytes_per_line)
130
+ send_image_chunk(
131
+ format: format,
132
+ drawable: drawable,
133
+ gc: gc,
134
+ width: width,
135
+ height: row_count,
136
+ dst_x: dst_x,
137
+ dst_y: dst_y + offset_y,
138
+ depth: depth,
139
+ data: chunk
140
+ )
141
+ offset_y += row_count
142
+ end
132
143
  end
133
144
 
134
- def change_property(window, property, type, data, mode: :replace)
135
- mode_val = case mode
136
- when :replace then 0
137
- when :prepend then 1
138
- when :append then 2
139
- else 0
140
- end
141
-
142
- property_atom = get_atom(property)
143
- type_atom = get_atom(type)
144
-
145
- format = 8
146
- data_bytes = data.to_s
147
-
148
- request = [
145
+ def change_property(window, property, type, data, mode: :replace, format: 8)
146
+ property_atom = atom(property)
147
+ type_atom = atom(type)
148
+ mode_val, request = request_encoder.change_property_data(
149
149
  window,
150
150
  property_atom,
151
151
  type_atom,
152
- format,
153
- data_bytes.bytesize
154
- ].pack("VVVCV") + "\x00\x00\x00" + pad_to_4(data_bytes)
155
-
152
+ data,
153
+ mode: mode,
154
+ format: format
155
+ )
156
156
  send_request(18, request, mode_val)
157
157
  end
158
158
 
159
159
  def intern_atom(name, only_if_exists: false)
160
- request = [
161
- name.bytesize,
162
- 0
163
- ].pack("vv") + pad_to_4(name)
164
-
165
- send_request(16, request, only_if_exists ? 1 : 0)
160
+ request = request_encoder.intern_atom_data(name)
161
+ sequence = send_request(16, request, only_if_exists ? 1 : 0)
166
162
  flush
167
163
 
168
- reply = read_reply
169
- return 0 unless reply
170
-
164
+ reply = read_reply(sequence)
171
165
  reply[8, 4].unpack1("V")
172
166
  end
173
167
 
@@ -179,19 +173,25 @@ module RBGL
179
173
  end
180
174
 
181
175
  def read_events
182
- while pending > 0
176
+ while @transport.pending > 0
183
177
  event = read_event
184
178
  @pending_events << event if event
185
179
  end
186
180
  end
187
181
 
182
+ def close
183
+ @transport.close
184
+ end
185
+
188
186
  private
189
187
 
190
188
  def parse_display_name(name)
191
- if name =~ /^(?:(.+):)?(\d+)(?:\.(\d+))?$/
192
- [::Regexp.last_match(1), ::Regexp.last_match(2).to_i, (::Regexp.last_match(3) || 0).to_i]
189
+ if name =~ /^(?:(.*):)?(\d+)(?:\.(\d+))?$/
190
+ host = ::Regexp.last_match(1)
191
+ host = nil if host && host.empty?
192
+ [host, ::Regexp.last_match(2).to_i, (::Regexp.last_match(3) || 0).to_i]
193
193
  else
194
- raise "Invalid display name: #{name}"
194
+ raise GUI::BackendUnavailable, "Invalid X11 display name: #{name}"
195
195
  end
196
196
  end
197
197
 
@@ -204,140 +204,205 @@ module RBGL
204
204
  end
205
205
  end
206
206
 
207
- def handshake
208
- init_request = [
209
- 0x6C,
210
- 0,
211
- 11, 0,
212
- 0, 0,
213
- 0, 0
214
- ].pack("CCvvvvvv")
207
+ def handshake(host: nil, display_num: 0, env: ENV)
208
+ auth_host = local_display_host?(host) ? nil : host
209
+ cookie = XAuthority.cookie_for(host: auth_host, display_number: display_num, env: env)
210
+ auth_name = cookie ? XAuthority::AUTH_NAME : ""
211
+ auth_data = cookie || ""
212
+ init_request = [0x6C, 0, 11, 0, auth_name.bytesize, auth_data.bytesize, 0].pack("CCvvvvv")
213
+ init_request << request_encoder.pad_to_4(auth_name)
214
+ init_request << request_encoder.pad_to_4(auth_data)
215
215
 
216
- @socket.write(init_request)
217
- @socket.flush
216
+ @transport.write(init_request)
217
+ @transport.flush
218
218
 
219
- header = @socket.read(8)
220
- status = header.unpack1("C")
219
+ header = @transport.read_exact(8, timeout: @reply_timeout)
220
+ raise GUI::BackendUnavailable, "X11 connection closed during handshake" unless header&.bytesize == 8
221
221
 
222
- raise "X11 connection failed" unless status == 1
222
+ status = header.unpack1("C")
223
+ unless status == 1
224
+ reason_length = header.getbyte(1)
225
+ reason = if reason_length.positive?
226
+ @transport.read_exact(pad_length(reason_length), timeout: @reply_timeout)
227
+ .byteslice(0, reason_length)
228
+ end
229
+ detail = reason && !reason.empty? ? ": #{reason}" : ""
230
+ raise GUI::BackendUnavailable, "X11 connection failed#{detail}"
231
+ end
223
232
 
224
233
  additional_length = header[6, 2].unpack1("v")
225
- data = @socket.read(additional_length * 4)
234
+ data = @transport.read_exact(additional_length * 4, timeout: @reply_timeout)
226
235
 
227
236
  parse_server_info(data)
237
+ rescue EOFError, IO::WaitReadable => error
238
+ raise GUI::BackendUnavailable, "X11 handshake failed: #{error.message}"
228
239
  end
229
240
 
230
241
  def parse_server_info(data)
231
- @resource_id_base = data[4, 4].unpack1("V")
232
- @resource_id_mask = data[8, 4].unpack1("V")
233
-
234
- vendor_length = data[16, 2].unpack1("v")
235
- num_screens = data[20, 1].unpack1("C")
236
- num_formats = data[21, 1].unpack1("C")
237
-
238
- offset = 32 + pad_length(vendor_length) + num_formats * 8
239
-
240
- if num_screens > 0
241
- @root = data[offset, 4].unpack1("V")
242
- @root_depth = data[offset + 38, 1].unpack1("C")
243
- @root_visual = data[offset + 32, 4].unpack1("V")
244
- @white_pixel = data[offset + 8, 4].unpack1("V")
245
- @black_pixel = data[offset + 12, 4].unpack1("V")
246
- end
242
+ setup = SetupParser.new.parse(data)
243
+ screen = setup.screens.fetch(@default_screen)
244
+ format = setup.pixmap_formats.find { |candidate| candidate.depth == screen.root_depth }
245
+ depth = screen.depths.find { |candidate| candidate.depth == screen.root_depth }
246
+ visual = depth&.visuals&.find { |candidate| candidate.id == screen.root_visual }
247
+ raise GUI::BackendUnavailable, "No X11 pixmap format for root depth #{screen.root_depth}" unless format
248
+ raise GUI::BackendUnavailable, "No X11 visual group for root depth #{screen.root_depth}" unless depth
249
+ raise GUI::BackendUnavailable, "No X11 root visual metadata for #{screen.root_visual}" unless visual
250
+
251
+ @resource_id_base = setup.resource_id_base
252
+ @resource_id_mask = setup.resource_id_mask
253
+ @maximum_request_length = setup.maximum_request_length
254
+ @minimum_keycode = setup.minimum_keycode
255
+ @maximum_keycode = setup.maximum_keycode
256
+ @image_byte_order = setup.image_byte_order
257
+ @root = screen.root
258
+ @root_depth = screen.root_depth
259
+ @root_visual = screen.root_visual
260
+ @white_pixel = screen.white_pixel
261
+ @black_pixel = screen.black_pixel
262
+ @bits_per_pixel = format.bits_per_pixel
263
+ @scanline_pad = format.scanline_pad
264
+ @visual_class = visual.visual_class
265
+ @red_mask = visual.red_mask
266
+ @green_mask = visual.green_mask
267
+ @blue_mask = visual.blue_mask
268
+ rescue IndexError
269
+ raise GUI::BackendUnavailable, "X11 screen #{@default_screen} is unavailable"
247
270
  end
248
271
 
249
272
  def send_request(opcode, data, extra = 0)
250
- length = (4 + data.bytesize + 3) / 4
251
- header = [opcode, extra, length].pack("CCv")
252
-
253
- padding_size = length * 4 - 4 - data.bytesize
254
- padded_data = data + ("\x00" * padding_size)
255
-
256
- @socket.write(header + padded_data)
257
- @next_seq += 1
273
+ @transport.write(request_encoder.request_packet(opcode, data, extra))
274
+ next_sequence
258
275
  end
259
276
 
260
277
  def send_request_with_data(opcode, extra, header_data, bulk_data)
261
- total_data = header_data + bulk_data
262
- length = (4 + total_data.bytesize + 3) / 4
278
+ @transport.write(request_encoder.request_packet_with_data(opcode, extra, header_data, bulk_data))
279
+ next_sequence
280
+ end
263
281
 
264
- header = [opcode, extra, length].pack("CCv")
265
- padding_size = length * 4 - 4 - total_data.bytesize
266
- padded = total_data + ("\x00" * padding_size)
282
+ def send_image_chunk(format:, drawable:, gc:, width:, height:, dst_x:, dst_y:, depth:, data:)
283
+ format_byte, header, image_data = request_encoder.put_image_data(
284
+ format: format,
285
+ drawable: drawable,
286
+ gc: gc,
287
+ width: width,
288
+ height: height,
289
+ dst_x: dst_x,
290
+ dst_y: dst_y,
291
+ depth: depth,
292
+ data: data
293
+ )
294
+ send_request_with_data(72, format_byte, header, image_data)
295
+ end
267
296
 
268
- @socket.write(header + padded)
269
- @next_seq += 1
297
+ def load_keyboard_mapping
298
+ count = @maximum_keycode - @minimum_keycode + 1
299
+ request = [@minimum_keycode, count, 0].pack("CCv")
300
+ sequence = send_request(101, request)
301
+ flush
302
+ reply = read_reply(sequence)
303
+ keysyms_per_keycode = reply.getbyte(1)
304
+ keysyms = reply.byteslice(32..).unpack("V*")
305
+ @keyboard_mapping = {}
306
+
307
+ count.times do |index|
308
+ offset = index * keysyms_per_keycode
309
+ @keyboard_mapping[@minimum_keycode + index] = keysyms.slice(offset, keysyms_per_keycode)
310
+ end
270
311
  end
271
312
 
272
- def read_reply
273
- header = @socket.read(32)
274
- return nil unless header && header.bytesize == 32
313
+ def read_reply(expected_sequence = nil)
314
+ return @pending_replies.delete(expected_sequence) if expected_sequence && @pending_replies.key?(expected_sequence)
275
315
 
276
- additional = header[4, 4].unpack1("V")
277
- if additional > 0
278
- header + @socket.read(additional * 4)
279
- else
280
- header
316
+ loop do
317
+ packet = read_packet
318
+ type = packet.getbyte(0)
319
+ sequence = packet.byteslice(2, 2).unpack1("v")
320
+
321
+ case type
322
+ when 0
323
+ raise_protocol_error(packet)
324
+ when 1
325
+ return packet if expected_sequence.nil? || sequence == expected_sequence
326
+
327
+ @pending_replies[sequence] = packet
328
+ else
329
+ event = parse_event(packet)
330
+ @pending_events << event if event
331
+ end
281
332
  end
282
333
  end
283
334
 
284
335
  def read_event
285
- data = @socket.read(32)
286
- return nil unless data && data.bytesize == 32
287
-
288
- event_type = data.unpack1("C") & 0x7F
289
-
290
- case event_type
291
- when 2
292
- keycode = data[1, 1].unpack1("C")
293
- { type: :key_press, keycode: keycode }
294
- when 3
295
- keycode = data[1, 1].unpack1("C")
296
- { type: :key_release, keycode: keycode }
297
- when 4
298
- x, y = data[24, 4].unpack("ss")
299
- button = data[1, 1].unpack1("C")
300
- { type: :button_press, x: x, y: y, button: button }
301
- when 5
302
- x, y = data[24, 4].unpack("ss")
303
- button = data[1, 1].unpack1("C")
304
- { type: :button_release, x: x, y: y, button: button }
305
- when 6
306
- x, y = data[24, 4].unpack("ss")
307
- { type: :motion_notify, x: x, y: y }
308
- when 12
309
- { type: :exposure }
310
- when 22
311
- width, height = data[20, 4].unpack("vv")
312
- { type: :configure_notify, width: width, height: height }
313
- when 33
314
- window = data[4, 4].unpack1("V")
315
- atom = data[8, 4].unpack1("V")
316
- { type: :client_message, window: window, data: atom }
317
- else
318
- { type: :unknown, code: event_type }
336
+ packet = read_packet
337
+ type = packet.getbyte(0)
338
+ return raise_protocol_error(packet) if type.zero?
339
+
340
+ if type == 1
341
+ sequence = packet.byteslice(2, 2).unpack1("v")
342
+ @pending_replies[sequence] = packet
343
+ return nil
319
344
  end
320
- end
321
345
 
322
- def get_atom(name)
323
- case name
324
- when :wm_name then 39
325
- when :string then 31
326
- when :wm_protocols then intern_atom("WM_PROTOCOLS")
327
- when :wm_delete_window then intern_atom("WM_DELETE_WINDOW")
328
- else
329
- name.is_a?(Integer) ? name : intern_atom(name.to_s)
330
- end
346
+ parse_event(packet)
331
347
  end
332
348
 
333
- def pad_to_4(str)
334
- padding = (4 - str.bytesize % 4) % 4
335
- str + ("\x00" * padding)
349
+ def resolve_atom(name)
350
+ return name if name.is_a?(Integer)
351
+
352
+ intern_atom(ATOM_NAMES.fetch(name, name.to_s))
336
353
  end
337
354
 
338
355
  def pad_length(len)
339
356
  ((len + 3) / 4) * 4
340
357
  end
358
+
359
+ def local_display_host?(host)
360
+ host.nil? || host.empty? || host == "unix"
361
+ end
362
+
363
+ def request_encoder
364
+ @request_encoder ||= RequestEncoder.new
365
+ end
366
+
367
+ def event_parser
368
+ @event_parser ||= EventParser.new
369
+ end
370
+
371
+ def atom_cache
372
+ @atom_cache ||= AtomCache.new { |name| resolve_atom(name) }
373
+ end
374
+
375
+ def read_packet
376
+ header = @transport.read_exact(32, timeout: @reply_timeout)
377
+ return header unless header.getbyte(0) == 1
378
+
379
+ additional = header.byteslice(4, 4).unpack1("V")
380
+ additional.positive? ? header + @transport.read_exact(additional * 4, timeout: @reply_timeout) : header
381
+ rescue EOFError, IO::WaitReadable => error
382
+ raise GUI::BackendUnavailable, error.message
383
+ end
384
+
385
+ def parse_event(packet)
386
+ event = event_parser.parse(packet)
387
+ @keyboard_mapping = nil if event&.[](:type) == :mapping_notify && event[:request] == :keyboard
388
+ event
389
+ end
390
+
391
+ def raise_protocol_error(packet)
392
+ error_code = packet.getbyte(1)
393
+ sequence = packet.byteslice(2, 2).unpack1("v")
394
+ bad_value = packet.byteslice(4, 4).unpack1("V")
395
+ minor_opcode = packet.byteslice(8, 2).unpack1("v")
396
+ major_opcode = packet.getbyte(10)
397
+ raise GUI::BackendUnavailable,
398
+ "X11 protocol error #{error_code} for request #{sequence} " \
399
+ "(opcode #{major_opcode}.#{minor_opcode}, value #{bad_value})"
400
+ end
401
+
402
+ def next_sequence
403
+ @sequence = (@sequence + 1) & 0xFFFF
404
+ end
405
+
341
406
  end
342
407
  end
343
408
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ module X11
6
+ class EventParser
7
+ def parse(data)
8
+ return nil unless data && data.bytesize == 32
9
+
10
+ event_type = data.unpack1("C") & 0x7F
11
+
12
+ case event_type
13
+ when 2
14
+ key_event(:key_press, data)
15
+ when 3
16
+ key_event(:key_release, data)
17
+ when 4
18
+ button_event(:button_press, data)
19
+ when 5
20
+ button_event(:button_release, data)
21
+ when 6
22
+ x, y = data[24, 4].unpack("s<s<")
23
+ { type: :motion_notify, x: x, y: y }
24
+ when 12
25
+ { type: :exposure }
26
+ when 22
27
+ width, height = data[20, 4].unpack("vv")
28
+ { type: :configure_notify, width: width, height: height }
29
+ when 33
30
+ {
31
+ type: :client_message,
32
+ format: data[1, 1].unpack1("C"),
33
+ window: data[4, 4].unpack1("V"),
34
+ message_type: data[8, 4].unpack1("V"),
35
+ data32: data[12, 20].unpack("V5")
36
+ }
37
+ when 34
38
+ {
39
+ type: :mapping_notify,
40
+ request: mapping_request(data.getbyte(1)),
41
+ first_keycode: data.getbyte(4),
42
+ count: data.getbyte(5)
43
+ }
44
+ else
45
+ { type: :unknown, code: event_type }
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def key_event(type, data)
52
+ { type: type, keycode: data[1, 1].unpack1("C") }
53
+ end
54
+
55
+ def button_event(type, data)
56
+ x, y = data[24, 4].unpack("s<s<")
57
+ {
58
+ type: type,
59
+ x: x,
60
+ y: y,
61
+ button: data[1, 1].unpack1("C")
62
+ }
63
+ end
64
+
65
+ def mapping_request(code)
66
+ { 0 => :modifier, 1 => :keyboard, 2 => :pointer }.fetch(code, :unknown)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end