eui-ruby 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 +31 -0
- data/LICENSE +21 -0
- data/README.md +192 -0
- data/examples/counter.rb +65 -0
- data/lib/eui/app.rb +83 -0
- data/lib/eui/assets.rb +76 -0
- data/lib/eui/blake3.rb +216 -0
- data/lib/eui/component.rb +105 -0
- data/lib/eui/dsl.rb +166 -0
- data/lib/eui/errors.rb +19 -0
- data/lib/eui/manifest.rb +109 -0
- data/lib/eui/proto/frame.rb +243 -0
- data/lib/eui/proto/limits.rb +47 -0
- data/lib/eui/proto/node.rb +385 -0
- data/lib/eui/proto/op.rb +196 -0
- data/lib/eui/proto/reader.rb +125 -0
- data/lib/eui/proto/style.rb +250 -0
- data/lib/eui/proto/writer.rb +89 -0
- data/lib/eui/proto.rb +22 -0
- data/lib/eui/server.rb +175 -0
- data/lib/eui/session.rb +289 -0
- data/lib/eui/theme.rb +73 -0
- data/lib/eui/version.rb +5 -0
- data/lib/eui/view/diff.rb +212 -0
- data/lib/eui/view/style.rb +198 -0
- data/lib/eui/view/tree.rb +378 -0
- data/lib/eui/websocket.rb +195 -0
- data/lib/eui-ruby.rb +4 -0
- data/lib/eui.rb +32 -0
- metadata +80 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'limits'
|
|
4
|
+
require_relative 'style'
|
|
5
|
+
|
|
6
|
+
module EUI
|
|
7
|
+
module Proto
|
|
8
|
+
# The closed set of primitive node kinds (`spec/02-wire-format.md` §4.1).
|
|
9
|
+
#
|
|
10
|
+
# Everything a person would call a widget — button, dialog, table, date
|
|
11
|
+
# picker — is composed from these on the server, which is why the
|
|
12
|
+
# catalogue grows without shipping a new client.
|
|
13
|
+
module NodeKind
|
|
14
|
+
BY_NAME = {
|
|
15
|
+
'box' => 0x01, 'text' => 0x02, 'image' => 0x03, 'icon' => 0x04,
|
|
16
|
+
'input' => 0x05, 'textarea' => 0x06, 'scroll' => 0x07, 'list' => 0x08,
|
|
17
|
+
'canvas' => 0x09, 'spacer' => 0x0A, 'divider' => 0x0B, 'overlay' => 0x0C,
|
|
18
|
+
'slot' => 0x0D, 'sizer' => 0x0E, 'audio' => 0x0F, 'video' => 0x10,
|
|
19
|
+
'scene' => 0x11
|
|
20
|
+
}.freeze
|
|
21
|
+
BY_CODE = BY_NAME.invert.freeze
|
|
22
|
+
|
|
23
|
+
LEAF = %w[text icon spacer divider audio video scene].map { |n| BY_NAME[n] }.freeze
|
|
24
|
+
INERT = %w[spacer divider].map { |n| BY_NAME[n] }.freeze
|
|
25
|
+
|
|
26
|
+
def self.code(name)
|
|
27
|
+
BY_NAME.fetch(name.to_s) { raise ViewError, "unknown node kind '#{name}'" }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.name(code)
|
|
31
|
+
BY_CODE.fetch(code) { raise DecodeError, "unknown node kind #{code}" }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.leaf?(code) = LEAF.include?(code)
|
|
35
|
+
def self.inert?(code) = INERT.include?(code)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Input and lifecycle events (`spec/06-events.md`).
|
|
39
|
+
module EventKind
|
|
40
|
+
BY_NAME = {
|
|
41
|
+
'click' => 0x01, 'double_click' => 0x02, 'pointer_down' => 0x03, 'pointer_up' => 0x04,
|
|
42
|
+
'pointer_move' => 0x05, 'pointer_enter' => 0x06, 'pointer_leave' => 0x07,
|
|
43
|
+
'key_down' => 0x08, 'key_up' => 0x09, 'text_input' => 0x0A, 'focus' => 0x0B,
|
|
44
|
+
'blur' => 0x0C, 'change' => 0x0D, 'submit' => 0x0E, 'scroll' => 0x0F,
|
|
45
|
+
'resize' => 0x10, 'context_menu' => 0x11, 'drag_start' => 0x12, 'drag_over' => 0x13,
|
|
46
|
+
'drop' => 0x14, 'long_press' => 0x15, 'window' => 0x16, 'ended' => 0x17,
|
|
47
|
+
'time_update' => 0x18, 'wake' => 0x19, 'file_pick' => 0x1A, 'file_save' => 0x1B,
|
|
48
|
+
'location' => 0x1C, 'nfc_tag' => 0x1D, 'file_drag' => 0x1E, 'back' => 0x1F,
|
|
49
|
+
'level' => 0x20
|
|
50
|
+
}.freeze
|
|
51
|
+
BY_CODE = BY_NAME.invert.freeze
|
|
52
|
+
|
|
53
|
+
# The protocol version an event arrived in: an event a client cannot
|
|
54
|
+
# decode is left out at encode time rather than sent, so the view still
|
|
55
|
+
# renders and the widget simply never hears from it.
|
|
56
|
+
SINCE = { 0x20 => 3 }.freeze
|
|
57
|
+
|
|
58
|
+
def self.code(name)
|
|
59
|
+
BY_NAME.fetch(name.to_s) { raise ViewError, "unknown event '#{name}'" }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.name(code)
|
|
63
|
+
BY_CODE.fetch(code) { raise DecodeError, "unknown event kind #{code}" }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.since(code) = SINCE.fetch(code, 1)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A string: interned in the session's atom table, or carried inline.
|
|
70
|
+
TextRef = Struct.new(:atom, :inline) do
|
|
71
|
+
def self.atom_ref(id) = new(Integer(id), nil)
|
|
72
|
+
def self.inline_ref(s) = new(nil, s.to_s)
|
|
73
|
+
|
|
74
|
+
def self.decode(reader)
|
|
75
|
+
case (tag = reader.u8)
|
|
76
|
+
when 0x00 then atom_ref(reader.varint32)
|
|
77
|
+
when 0x01 then inline_ref(reader.str(Limits::MAX_INLINE_STR, 'inline string'))
|
|
78
|
+
else raise DecodeError, "unknown TextRef tag #{tag}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def encode(writer)
|
|
83
|
+
if atom
|
|
84
|
+
writer.u8(0x00).varint(atom)
|
|
85
|
+
else
|
|
86
|
+
writer.u8(0x01).str(inline)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# A property value (`spec/02-wire-format.md` §4.4).
|
|
92
|
+
#
|
|
93
|
+
# Tagged rather than inferred from the Ruby object: a string may be an
|
|
94
|
+
# atom or an inline run, and 32 bytes may be an asset hash or a label.
|
|
95
|
+
class Value
|
|
96
|
+
NULL = 0x00
|
|
97
|
+
BOOL = 0x01
|
|
98
|
+
INT = 0x02
|
|
99
|
+
FLOAT = 0x03
|
|
100
|
+
ATOM = 0x04
|
|
101
|
+
STR = 0x05
|
|
102
|
+
ASSET = 0x06
|
|
103
|
+
COLOR = 0x07
|
|
104
|
+
LIST = 0x08
|
|
105
|
+
|
|
106
|
+
attr_reader :tag, :value
|
|
107
|
+
|
|
108
|
+
def initialize(tag, value = nil)
|
|
109
|
+
@tag = tag
|
|
110
|
+
@value = value
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class << self
|
|
114
|
+
def null = new(NULL)
|
|
115
|
+
def bool(b) = new(BOOL, b ? true : false)
|
|
116
|
+
def int(n) = new(INT, Integer(n))
|
|
117
|
+
def float(f)
|
|
118
|
+
f = Float(f)
|
|
119
|
+
raise ViewError, 'a float on the wire must be finite' unless f.finite?
|
|
120
|
+
|
|
121
|
+
new(FLOAT, f)
|
|
122
|
+
end
|
|
123
|
+
def atom(id) = new(ATOM, Integer(id))
|
|
124
|
+
def str(s) = new(STR, s.to_s)
|
|
125
|
+
def asset(hash)
|
|
126
|
+
raise ViewError, 'an asset is 32 bytes' unless hash.bytesize == Limits::HASH_BYTES
|
|
127
|
+
|
|
128
|
+
new(ASSET, hash.b)
|
|
129
|
+
end
|
|
130
|
+
def color(ref) = new(COLOR, ref)
|
|
131
|
+
def list(items) = new(LIST, items)
|
|
132
|
+
|
|
133
|
+
# A plain Ruby value as the wire would carry it. Strings go inline:
|
|
134
|
+
# interning is the encoder's decision, not this one's.
|
|
135
|
+
def from(ruby)
|
|
136
|
+
case ruby
|
|
137
|
+
when nil then null
|
|
138
|
+
when true, false then bool(ruby)
|
|
139
|
+
when Integer then int(ruby)
|
|
140
|
+
when Float then float(ruby)
|
|
141
|
+
when String, Symbol then str(ruby.to_s)
|
|
142
|
+
when ColorRef then color(ruby)
|
|
143
|
+
when Array then list(ruby.map { |i| from(i) })
|
|
144
|
+
when Value then ruby
|
|
145
|
+
else raise ViewError, "a prop cannot carry #{ruby.class}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.decode(reader, depth = 1)
|
|
151
|
+
raise DecodeError, 'value nesting' if depth > Limits::MAX_VALUE_DEPTH
|
|
152
|
+
|
|
153
|
+
case (tag = reader.u8)
|
|
154
|
+
when NULL then null
|
|
155
|
+
when BOOL
|
|
156
|
+
case reader.u8
|
|
157
|
+
when 0 then bool(false)
|
|
158
|
+
when 1 then bool(true)
|
|
159
|
+
else raise DecodeError, 'bool must be 0 or 1'
|
|
160
|
+
end
|
|
161
|
+
when INT then int(reader.svarint)
|
|
162
|
+
when FLOAT then float(reader.f64)
|
|
163
|
+
when ATOM then atom(reader.varint32)
|
|
164
|
+
when STR then str(reader.str(Limits::MAX_INLINE_STR, 'inline string'))
|
|
165
|
+
when ASSET then asset(reader.take(Limits::HASH_BYTES))
|
|
166
|
+
when COLOR then color(ColorRef.new(reader.u16))
|
|
167
|
+
when LIST
|
|
168
|
+
count = reader.varint32_max(Limits::MAX_VALUE_LIST, 'value list length')
|
|
169
|
+
list(Array.new(count) { decode(reader, depth + 1) })
|
|
170
|
+
else raise DecodeError, "unknown Value tag #{tag}"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def encode(writer)
|
|
175
|
+
case @tag
|
|
176
|
+
when NULL then writer.u8(NULL)
|
|
177
|
+
when BOOL then writer.u8(BOOL).u8(@value ? 1 : 0)
|
|
178
|
+
when INT then writer.u8(INT).svarint(@value)
|
|
179
|
+
when FLOAT then writer.u8(FLOAT).f64(@value)
|
|
180
|
+
when ATOM then writer.u8(ATOM).varint(@value)
|
|
181
|
+
when STR then writer.u8(STR).str(@value)
|
|
182
|
+
when ASSET then writer.u8(ASSET).raw(@value)
|
|
183
|
+
when COLOR then writer.u8(COLOR).u16(@value.bits)
|
|
184
|
+
when LIST
|
|
185
|
+
writer.u8(LIST).varint(@value.length)
|
|
186
|
+
@value.each { |item| item.encode(writer) }
|
|
187
|
+
end
|
|
188
|
+
writer
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# What a handler sees: plain Ruby, with atoms left as their ids for
|
|
192
|
+
# the session to resolve against its own table.
|
|
193
|
+
def to_ruby
|
|
194
|
+
case @tag
|
|
195
|
+
when NULL then nil
|
|
196
|
+
when LIST then @value.map(&:to_ruby)
|
|
197
|
+
when COLOR then @value
|
|
198
|
+
else @value
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def ==(other)
|
|
203
|
+
other.is_a?(Value) && other.tag == @tag && other.value == @value
|
|
204
|
+
end
|
|
205
|
+
alias eql? ==
|
|
206
|
+
|
|
207
|
+
def hash = [@tag, @value].hash
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# What an event does when it happens.
|
|
211
|
+
Handler = Struct.new(:kind, :chunk, :name) do
|
|
212
|
+
SERVER = 0x00
|
|
213
|
+
LOCAL = 0x01
|
|
214
|
+
BOTH = 0x02
|
|
215
|
+
|
|
216
|
+
# Round trip: the client emits an `Event` frame naming this atom.
|
|
217
|
+
def self.server(atom) = new(SERVER, nil, Integer(atom))
|
|
218
|
+
# Runs entirely on the client, in the metered VM. No network traffic.
|
|
219
|
+
def self.local(chunk) = new(LOCAL, Integer(chunk), nil)
|
|
220
|
+
# Runs locally for the immediate feedback, then tells the server.
|
|
221
|
+
def self.local_then_server(chunk, atom) = new(BOTH, Integer(chunk), Integer(atom))
|
|
222
|
+
|
|
223
|
+
def self.decode(reader)
|
|
224
|
+
case (tag = reader.u8)
|
|
225
|
+
when SERVER then server(reader.varint32)
|
|
226
|
+
when LOCAL then local(reader.varint32)
|
|
227
|
+
when BOTH then local_then_server(reader.varint32, reader.varint32)
|
|
228
|
+
else raise DecodeError, "unknown Handler tag #{tag}"
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def encode(writer)
|
|
233
|
+
case kind
|
|
234
|
+
when SERVER then writer.u8(SERVER).varint(name)
|
|
235
|
+
when LOCAL then writer.u8(LOCAL).varint(chunk)
|
|
236
|
+
when BOTH then writer.u8(BOTH).varint(chunk).varint(name)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# One node of a subtree, with its props and handlers held as ranges into
|
|
242
|
+
# the owning [Subtree]'s side arrays.
|
|
243
|
+
FlatNode = Struct.new(:kind, :id, :style, :key, :text, :props, :handlers, :child_count, keyword_init: true)
|
|
244
|
+
|
|
245
|
+
# A subtree, stored pre-order.
|
|
246
|
+
#
|
|
247
|
+
# Reconstructing the shape needs nothing but `child_count`: a node's
|
|
248
|
+
# first child is the next entry, and its next sibling is found by
|
|
249
|
+
# skipping that child's own descendants. Decoding is iterative, so a
|
|
250
|
+
# hostile 10 000-deep tree costs a bounds check rather than the stack.
|
|
251
|
+
class Subtree
|
|
252
|
+
attr_reader :nodes, :props, :handlers
|
|
253
|
+
|
|
254
|
+
def initialize
|
|
255
|
+
@nodes = []
|
|
256
|
+
@props = []
|
|
257
|
+
@handlers = []
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def root = @nodes.first
|
|
261
|
+
|
|
262
|
+
def props_of(node)
|
|
263
|
+
start, len = node.props
|
|
264
|
+
@props[start, len] || []
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def handlers_of(node)
|
|
268
|
+
start, len = node.handlers
|
|
269
|
+
@handlers[start, len] || []
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def push(kind:, id:, style:, key: 0, text: nil, props: [], handlers: [], child_count: 0)
|
|
273
|
+
prop_start = @props.length
|
|
274
|
+
@props.concat(props)
|
|
275
|
+
handler_start = @handlers.length
|
|
276
|
+
@handlers.concat(handlers)
|
|
277
|
+
@nodes << FlatNode.new(
|
|
278
|
+
kind: kind, id: id, style: style, key: key, text: text,
|
|
279
|
+
props: [prop_start, props.length], handlers: [handler_start, handlers.length],
|
|
280
|
+
child_count: child_count
|
|
281
|
+
)
|
|
282
|
+
self
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def self.decode(reader)
|
|
286
|
+
out = new
|
|
287
|
+
pending = []
|
|
288
|
+
loop do
|
|
289
|
+
raise DecodeError, 'node count' if out.nodes.length >= Limits::MAX_NODES
|
|
290
|
+
|
|
291
|
+
child_count = out.send(:decode_one, reader)
|
|
292
|
+
if child_count.positive?
|
|
293
|
+
raise DecodeError, 'tree depth' if pending.length >= Limits::MAX_TREE_DEPTH
|
|
294
|
+
|
|
295
|
+
pending << child_count
|
|
296
|
+
else
|
|
297
|
+
while (top = pending.last)
|
|
298
|
+
pending[-1] = top - 1
|
|
299
|
+
break unless pending[-1].zero?
|
|
300
|
+
|
|
301
|
+
pending.pop
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
return out if pending.empty?
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def encode(writer)
|
|
309
|
+
@nodes.each do |node|
|
|
310
|
+
props = props_of(node)
|
|
311
|
+
handlers = handlers_of(node)
|
|
312
|
+
|
|
313
|
+
flags = 0
|
|
314
|
+
flags |= 0x01 unless node.key.zero?
|
|
315
|
+
flags |= 0x02 if node.text
|
|
316
|
+
flags |= 0x04 unless props.empty?
|
|
317
|
+
flags |= 0x08 unless handlers.empty?
|
|
318
|
+
|
|
319
|
+
writer.u8(node.kind).u8(flags).varint(node.id).varint(node.style)
|
|
320
|
+
writer.varint(node.key) unless node.key.zero?
|
|
321
|
+
node.text&.encode(writer)
|
|
322
|
+
unless props.empty?
|
|
323
|
+
writer.varint(props.length)
|
|
324
|
+
props.each do |(atom, value)|
|
|
325
|
+
writer.varint(atom)
|
|
326
|
+
value.encode(writer)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
unless handlers.empty?
|
|
330
|
+
writer.varint(handlers.length)
|
|
331
|
+
handlers.each do |(event, handler)|
|
|
332
|
+
writer.u8(event)
|
|
333
|
+
handler.encode(writer)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
writer.varint(node.child_count)
|
|
337
|
+
end
|
|
338
|
+
writer
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
private
|
|
342
|
+
|
|
343
|
+
def decode_one(reader)
|
|
344
|
+
kind = reader.u8
|
|
345
|
+
NodeKind.name(kind)
|
|
346
|
+
flags = reader.u8
|
|
347
|
+
raise DecodeError, 'reserved node flags set' if (flags & 0xF0) != 0
|
|
348
|
+
|
|
349
|
+
id = reader.varint32
|
|
350
|
+
raise DecodeError, 'node id must be non-zero' if id.zero?
|
|
351
|
+
|
|
352
|
+
style = reader.varint32
|
|
353
|
+
key = (flags & 0x01).zero? ? 0 : reader.varint32
|
|
354
|
+
text = (flags & 0x02).zero? ? nil : TextRef.decode(reader)
|
|
355
|
+
|
|
356
|
+
props = []
|
|
357
|
+
unless (flags & 0x04).zero?
|
|
358
|
+
count = reader.varint32_max(Limits::MAX_PROPS, 'props per node')
|
|
359
|
+
count.times { props << [reader.varint32, Value.decode(reader)] }
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
handlers = []
|
|
363
|
+
unless (flags & 0x08).zero?
|
|
364
|
+
count = reader.varint32_max(Limits::MAX_HANDLERS, 'handlers per node')
|
|
365
|
+
count.times do
|
|
366
|
+
event = reader.u8
|
|
367
|
+
EventKind.name(event) # refuse one this revision does not define
|
|
368
|
+
handlers << [event, Handler.decode(reader)]
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
if NodeKind.inert?(kind) && (text || !props.empty? || !handlers.empty?)
|
|
373
|
+
raise DecodeError, 'inert node kind carries content'
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
child_count = reader.varint32_max(Limits::MAX_CHILDREN, 'children per node')
|
|
377
|
+
raise DecodeError, 'a leaf kind carries children' if NodeKind.leaf?(kind) && child_count.positive?
|
|
378
|
+
|
|
379
|
+
push(kind: kind, id: id, style: style, key: key, text: text, props: props,
|
|
380
|
+
handlers: handlers, child_count: child_count)
|
|
381
|
+
child_count
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
data/lib/eui/proto/op.rb
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'node'
|
|
4
|
+
require_relative 'style'
|
|
5
|
+
|
|
6
|
+
module EUI
|
|
7
|
+
module Proto
|
|
8
|
+
# One operation in a batch (`spec/02-wire-format.md` §5).
|
|
9
|
+
#
|
|
10
|
+
# Structural only. Whether an op is *coherent* — that the node it names
|
|
11
|
+
# exists, that the atom it references was defined — is session state and
|
|
12
|
+
# belongs to the session, not here.
|
|
13
|
+
class Op
|
|
14
|
+
DEF_ATOM = 0x10
|
|
15
|
+
DEF_STYLE = 0x11
|
|
16
|
+
DEF_COLOR = 0x12
|
|
17
|
+
DEF_CHUNK = 0x13
|
|
18
|
+
DEF_CHUNK_BYTES = 0x14
|
|
19
|
+
DEF_FONT = 0x15
|
|
20
|
+
MOUNT = 0x20
|
|
21
|
+
REPLACE = 0x21
|
|
22
|
+
SET_STYLE = 0x22
|
|
23
|
+
SET_TEXT = 0x23
|
|
24
|
+
SET_PROP = 0x24
|
|
25
|
+
INSERT_CHILD = 0x25
|
|
26
|
+
REMOVE_CHILD = 0x26
|
|
27
|
+
MOVE_CHILD = 0x27
|
|
28
|
+
SET_HANDLER = 0x28
|
|
29
|
+
CLEAR_HANDLER = 0x29
|
|
30
|
+
FOCUS = 0x2A
|
|
31
|
+
SCROLL_TO = 0x2B
|
|
32
|
+
NOTIFY = 0x2C
|
|
33
|
+
|
|
34
|
+
attr_reader :opcode, :fields
|
|
35
|
+
|
|
36
|
+
def initialize(opcode, fields = {})
|
|
37
|
+
@opcode = opcode
|
|
38
|
+
@fields = fields
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def [](name) = @fields[name]
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
def def_atom(id, value) = new(DEF_ATOM, id: id, value: value)
|
|
45
|
+
def def_style(id, record) = new(DEF_STYLE, id: id, record: record)
|
|
46
|
+
def def_color(id, rgba) = new(DEF_COLOR, id: id, rgba: rgba)
|
|
47
|
+
def def_chunk(id, hash) = new(DEF_CHUNK, id: id, hash: hash)
|
|
48
|
+
def def_chunk_bytes(id, bytes) = new(DEF_CHUNK_BYTES, id: id, bytes: bytes)
|
|
49
|
+
def def_font(role, faces) = new(DEF_FONT, role: role, faces: faces)
|
|
50
|
+
def mount(subtree) = new(MOUNT, subtree: subtree)
|
|
51
|
+
def replace(node, subtree) = new(REPLACE, node: node, subtree: subtree)
|
|
52
|
+
def set_style(node, style) = new(SET_STYLE, node: node, style: style)
|
|
53
|
+
def set_text(node, text) = new(SET_TEXT, node: node, text: text)
|
|
54
|
+
def set_prop(node, prop, value) = new(SET_PROP, node: node, prop: prop, value: value)
|
|
55
|
+
def insert_child(parent, index, subtree) = new(INSERT_CHILD, parent: parent, index: index, subtree: subtree)
|
|
56
|
+
def remove_child(parent, index, count) = new(REMOVE_CHILD, parent: parent, index: index, count: count)
|
|
57
|
+
def move_child(parent, from, to) = new(MOVE_CHILD, parent: parent, from: from, to: to)
|
|
58
|
+
def set_handler(node, event, handler) = new(SET_HANDLER, node: node, event: event, handler: handler)
|
|
59
|
+
def clear_handler(node, event) = new(CLEAR_HANDLER, node: node, event: event)
|
|
60
|
+
def focus(node) = new(FOCUS, node: node)
|
|
61
|
+
def scroll_to(node, x, y) = new(SCROLL_TO, node: node, x: x, y: y)
|
|
62
|
+
def notify(title, body = '', tag = '') = new(NOTIFY, title: title, body: body, tag: tag)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.decode(reader)
|
|
66
|
+
case (opcode = reader.u8)
|
|
67
|
+
when DEF_ATOM
|
|
68
|
+
def_atom(nonzero(reader.varint32, 'atom id'), reader.str(Limits::MAX_ATOM_BYTES, 'atom value'))
|
|
69
|
+
when DEF_STYLE
|
|
70
|
+
def_style(nonzero(reader.varint32, 'style id'), StyleRecord.decode(reader))
|
|
71
|
+
when DEF_COLOR
|
|
72
|
+
def_color(nonzero(reader.varint32, 'color id'), reader.u32)
|
|
73
|
+
when DEF_CHUNK
|
|
74
|
+
def_chunk(nonzero(reader.varint32, 'chunk id'), reader.take(Limits::HASH_BYTES))
|
|
75
|
+
when DEF_CHUNK_BYTES
|
|
76
|
+
def_chunk_bytes(nonzero(reader.varint32, 'chunk id'), reader.bytes(Limits::MAX_CHUNK_BYTES, 'chunk bytes'))
|
|
77
|
+
when DEF_FONT
|
|
78
|
+
role = reader.u8
|
|
79
|
+
raise DecodeError, 'font role' if role > Limits::MAX_FONT_ROLE
|
|
80
|
+
|
|
81
|
+
count = reader.varint32
|
|
82
|
+
raise DecodeError, 'a font role with no face' if count.zero?
|
|
83
|
+
raise DecodeError, 'font faces' if count > Limits::MAX_FACES_PER_ROLE
|
|
84
|
+
|
|
85
|
+
def_font(role, Array.new(count) { reader.take(Limits::HASH_BYTES) })
|
|
86
|
+
when MOUNT then mount(Subtree.decode(reader))
|
|
87
|
+
when REPLACE then replace(nonzero(reader.varint32, 'node id'), Subtree.decode(reader))
|
|
88
|
+
when SET_STYLE then set_style(nonzero(reader.varint32, 'node id'), reader.varint32)
|
|
89
|
+
when SET_TEXT then set_text(nonzero(reader.varint32, 'node id'), TextRef.decode(reader))
|
|
90
|
+
when SET_PROP
|
|
91
|
+
set_prop(nonzero(reader.varint32, 'node id'), reader.varint32, Value.decode(reader))
|
|
92
|
+
when INSERT_CHILD
|
|
93
|
+
insert_child(nonzero(reader.varint32, 'node id'), reader.varint32, Subtree.decode(reader))
|
|
94
|
+
when REMOVE_CHILD
|
|
95
|
+
remove_child(nonzero(reader.varint32, 'node id'), reader.varint32, reader.varint32)
|
|
96
|
+
when MOVE_CHILD
|
|
97
|
+
move_child(nonzero(reader.varint32, 'node id'), reader.varint32, reader.varint32)
|
|
98
|
+
when SET_HANDLER
|
|
99
|
+
node = nonzero(reader.varint32, 'node id')
|
|
100
|
+
event = reader.u8
|
|
101
|
+
EventKind.name(event)
|
|
102
|
+
set_handler(node, event, Handler.decode(reader))
|
|
103
|
+
when CLEAR_HANDLER
|
|
104
|
+
node = nonzero(reader.varint32, 'node id')
|
|
105
|
+
event = reader.u8
|
|
106
|
+
EventKind.name(event)
|
|
107
|
+
clear_handler(node, event)
|
|
108
|
+
when FOCUS then focus(nonzero(reader.varint32, 'node id'))
|
|
109
|
+
when SCROLL_TO then scroll_to(nonzero(reader.varint32, 'node id'), reader.svarint, reader.svarint)
|
|
110
|
+
when NOTIFY
|
|
111
|
+
notify(reader.str(Limits::MAX_NOTIFY_TITLE, 'notification title'),
|
|
112
|
+
reader.str(Limits::MAX_NOTIFY_BODY, 'notification body'),
|
|
113
|
+
reader.str(Limits::MAX_NOTIFY_TAG, 'notification tag'))
|
|
114
|
+
else raise DecodeError, "unknown opcode #{opcode}"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.nonzero(value, what)
|
|
119
|
+
raise DecodeError, "#{what} must be non-zero" if value.zero?
|
|
120
|
+
|
|
121
|
+
value
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def encode(writer)
|
|
125
|
+
f = @fields
|
|
126
|
+
writer.u8(@opcode)
|
|
127
|
+
case @opcode
|
|
128
|
+
when DEF_ATOM then writer.varint(f[:id]).str(f[:value])
|
|
129
|
+
when DEF_STYLE
|
|
130
|
+
writer.varint(f[:id])
|
|
131
|
+
f[:record].encode(writer)
|
|
132
|
+
when DEF_COLOR then writer.varint(f[:id]).u32(f[:rgba])
|
|
133
|
+
when DEF_CHUNK then writer.varint(f[:id]).raw(f[:hash])
|
|
134
|
+
when DEF_CHUNK_BYTES then writer.varint(f[:id]).bytes(f[:bytes])
|
|
135
|
+
when DEF_FONT
|
|
136
|
+
writer.u8(f[:role]).varint(f[:faces].length)
|
|
137
|
+
f[:faces].each { |face| writer.raw(face) }
|
|
138
|
+
when MOUNT then f[:subtree].encode(writer)
|
|
139
|
+
when REPLACE
|
|
140
|
+
writer.varint(f[:node])
|
|
141
|
+
f[:subtree].encode(writer)
|
|
142
|
+
when SET_STYLE then writer.varint(f[:node]).varint(f[:style])
|
|
143
|
+
when SET_TEXT
|
|
144
|
+
writer.varint(f[:node])
|
|
145
|
+
f[:text].encode(writer)
|
|
146
|
+
when SET_PROP
|
|
147
|
+
writer.varint(f[:node]).varint(f[:prop])
|
|
148
|
+
f[:value].encode(writer)
|
|
149
|
+
when INSERT_CHILD
|
|
150
|
+
writer.varint(f[:parent]).varint(f[:index])
|
|
151
|
+
f[:subtree].encode(writer)
|
|
152
|
+
when REMOVE_CHILD then writer.varint(f[:parent]).varint(f[:index]).varint(f[:count])
|
|
153
|
+
when MOVE_CHILD then writer.varint(f[:parent]).varint(f[:from]).varint(f[:to])
|
|
154
|
+
when SET_HANDLER
|
|
155
|
+
writer.varint(f[:node]).u8(f[:event])
|
|
156
|
+
f[:handler].encode(writer)
|
|
157
|
+
when CLEAR_HANDLER then writer.varint(f[:node]).u8(f[:event])
|
|
158
|
+
when FOCUS then writer.varint(f[:node])
|
|
159
|
+
when SCROLL_TO then writer.varint(f[:node]).svarint(f[:x]).svarint(f[:y])
|
|
160
|
+
when NOTIFY then writer.str(f[:title]).str(f[:body]).str(f[:tag])
|
|
161
|
+
else raise Error, "cannot encode opcode #{@opcode}"
|
|
162
|
+
end
|
|
163
|
+
writer
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def ==(other)
|
|
167
|
+
other.is_a?(Op) && other.opcode == @opcode && other.fields == @fields
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# An ordered run of ops carrying a sequence number. The client acks the
|
|
172
|
+
# last one it applied, and applies a batch all or nothing.
|
|
173
|
+
Batch = Struct.new(:seq, :ops) do
|
|
174
|
+
def self.decode(reader)
|
|
175
|
+
seq = reader.varint
|
|
176
|
+
count = reader.varint32_max(Limits::MAX_OPS_PER_BATCH, 'ops per batch')
|
|
177
|
+
notifications = 0
|
|
178
|
+
ops = Array.new(count) do
|
|
179
|
+
op = Op.decode(reader)
|
|
180
|
+
if op.opcode == Op::NOTIFY
|
|
181
|
+
notifications += 1
|
|
182
|
+
raise DecodeError, 'notifications per batch' if notifications > Limits::MAX_NOTIFY_PER_BATCH
|
|
183
|
+
end
|
|
184
|
+
op
|
|
185
|
+
end
|
|
186
|
+
new(seq, ops)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def encode(writer)
|
|
190
|
+
writer.varint(seq).varint(ops.length)
|
|
191
|
+
ops.each { |op| op.encode(writer) }
|
|
192
|
+
writer
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'limits'
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
|
|
6
|
+
module EUI
|
|
7
|
+
module Proto
|
|
8
|
+
# A cursor over bytes that refuses anything it was not promised.
|
|
9
|
+
#
|
|
10
|
+
# Two rules do most of the work: a varint must be minimally encoded, and
|
|
11
|
+
# a frame must account for every byte it declared. Both are the classic
|
|
12
|
+
# route to a parser disagreeing with itself.
|
|
13
|
+
class Reader
|
|
14
|
+
def initialize(bytes)
|
|
15
|
+
@bytes = bytes.b
|
|
16
|
+
@pos = 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def remaining
|
|
20
|
+
@bytes.bytesize - @pos
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def eof?
|
|
24
|
+
remaining.zero?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def take(count)
|
|
28
|
+
raise DecodeError, "short read: wanted #{count}, #{remaining} left" if count > remaining
|
|
29
|
+
|
|
30
|
+
slice = @bytes.byteslice(@pos, count)
|
|
31
|
+
@pos += count
|
|
32
|
+
slice
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def u8
|
|
36
|
+
take(1).unpack1('C')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def u16
|
|
40
|
+
take(2).unpack1('v')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def u32
|
|
44
|
+
take(4).unpack1('V')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def u64
|
|
48
|
+
take(8).unpack1('Q<')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def f64
|
|
52
|
+
value = take(8).unpack1('E')
|
|
53
|
+
raise DecodeError, 'float must be finite' unless value.finite?
|
|
54
|
+
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def array(count)
|
|
59
|
+
take(count)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# LEB128, and only the minimal spelling of it: a multi-byte encoding
|
|
63
|
+
# whose last byte is `0x00` is an error rather than a normalisation.
|
|
64
|
+
def varint(max_bytes = 10)
|
|
65
|
+
value = 0
|
|
66
|
+
shift = 0
|
|
67
|
+
count = 0
|
|
68
|
+
loop do
|
|
69
|
+
byte = u8
|
|
70
|
+
count += 1
|
|
71
|
+
raise DecodeError, 'varint too long' if count > max_bytes
|
|
72
|
+
|
|
73
|
+
value |= (byte & 0x7F) << shift
|
|
74
|
+
break if (byte & 0x80).zero?
|
|
75
|
+
raise DecodeError, 'non-minimal varint' if count == max_bytes
|
|
76
|
+
|
|
77
|
+
shift += 7
|
|
78
|
+
end
|
|
79
|
+
raise DecodeError, 'non-minimal varint' if count > 1 && value < (1 << (7 * (count - 1)))
|
|
80
|
+
|
|
81
|
+
value
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def varint32
|
|
85
|
+
value = varint(5)
|
|
86
|
+
raise DecodeError, 'varint overflows u32' if value > 0xFFFF_FFFF
|
|
87
|
+
|
|
88
|
+
value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def varint32_max(max, what)
|
|
92
|
+
value = varint32
|
|
93
|
+
raise DecodeError, "#{what} above #{max}" if value > max
|
|
94
|
+
|
|
95
|
+
value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def svarint
|
|
99
|
+
raw = varint(10)
|
|
100
|
+
(raw >> 1) ^ -(raw & 1)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def bytes(max, what)
|
|
104
|
+
len = varint32
|
|
105
|
+
raise DecodeError, "#{what} of #{len} bytes, at most #{max}" if len > max
|
|
106
|
+
|
|
107
|
+
take(len)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def str(max, what)
|
|
111
|
+
value = bytes(max, what).force_encoding(Encoding::UTF_8)
|
|
112
|
+
raise DecodeError, "#{what} is not valid UTF-8" unless value.valid_encoding?
|
|
113
|
+
|
|
114
|
+
value
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Trailing bytes are an error, not padding.
|
|
118
|
+
def finish!
|
|
119
|
+
raise DecodeError, "#{remaining} trailing bytes" unless eof?
|
|
120
|
+
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|