yoga_layout 0.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.
@@ -0,0 +1,333 @@
1
+ require 'yoga_layout/wrapper'
2
+
3
+ module YogaLayout
4
+ # YogaLayout::Node is the main object you will be interfacing with when using
5
+ # Yoga in Ruby. YogaLayout::Node is a thin FFI wrapper around the core Yoga
6
+ # library.
7
+ #
8
+ # Use Nodes to build up a tree that describes your layout. Set layout and
9
+ # style properties to define your layout constraints.
10
+ #
11
+ # Once you have set up a tree of nodes with styles you will want to get the
12
+ # result of a layout calculation. Call {#calculate_layout} to perform layout
13
+ # calculation. Once this function returns the results of the layout
14
+ # calculation is stored on each node. Traverse the tree and retrieve the
15
+ # values from each node.
16
+ class Node < YogaLayout::Wrapper
17
+ # Quickly create a bunch of nodes with this handy node literal syntax.
18
+ #
19
+ # @param opts [Hash] save for the :children option, all of these are passed
20
+ # to {#set_styles}
21
+ # @option opts [Array<Node>] :children ([]) Children, in order, to add to
22
+ # this node.
23
+ # @return [Node]
24
+ def self.[](opts = {})
25
+ children = opts.delete(:children) || []
26
+ node = new.set_styles(opts)
27
+ children.each_with_index do |child, idx|
28
+ node.insert_child(child, idx)
29
+ end
30
+ node
31
+ end
32
+
33
+ # Create a new Node with a specific config.
34
+ #
35
+ # @param config [YogaLayout::Config]
36
+ # @return [YogaLayout::Node]
37
+ def self.new_with_config(config)
38
+ new(auto_ptr(YogaLayout::Bindings.YGNodeNewWithConfig(config.pointer)), config)
39
+ end
40
+
41
+ # @override
42
+ # @api private
43
+ def self.unsafe_new_pointer
44
+ YogaLayout::Bindings.YGNodeNew
45
+ end
46
+
47
+ # @override
48
+ # @api private
49
+ def self.unsafe_free_pointer(pointer)
50
+ YogaLayout::Bindings.YGNodeFree(pointer)
51
+ end
52
+
53
+ def initialize(auto_ptr = nil, config = nil)
54
+ super(auto_ptr)
55
+
56
+ @children = []
57
+ @parent = nil
58
+ @data = nil
59
+ @measure_func = nil
60
+ @baseline_func = nil
61
+ @config = config
62
+ end
63
+
64
+ # Set many styles at once.
65
+ #
66
+ # Uses ruby magic to make it easier to set styles.
67
+ #
68
+ # @param [Hash<#to_s, any>] Map from style property to value
69
+ # @return [Node] self
70
+ def set_styles(styles)
71
+ styles.each do |prop, value|
72
+ method_name = "style_set_#{prop}"
73
+
74
+ if respond_to?(method_name)
75
+ if method(method_name).arity == 1
76
+ # Handle eg, flex_direction: :row
77
+ public_send(method_name, value)
78
+ else
79
+ # Handle eg, padding: 25
80
+ public_send(method_name, :all, value)
81
+ end
82
+ else
83
+ # handle eg, margin_top: 50
84
+ method_name, _, edge = method_name.rpartition('_')
85
+ public_send(method_name, edge.to_sym, value)
86
+ end
87
+ end
88
+ self
89
+ end
90
+
91
+ # Retrieve all layout information as a hash
92
+ #
93
+ # @return [Hash]
94
+ def layout
95
+ Hash[
96
+ self.class.layout_props.map { |method, prop| [prop, public_send(method)] }
97
+ ]
98
+ end
99
+
100
+ # Retrieve all style information as a hash
101
+ #
102
+ # @return [Hash]
103
+ def styles
104
+ Hash[
105
+ self.class.style_props.map { |method, prop| [prop, public_send(method)] }
106
+ ]
107
+ end
108
+
109
+ def self.style_props
110
+ @style_props ||= {}
111
+ end
112
+
113
+ def self.layout_props
114
+ @layout_props ||= {}
115
+ end
116
+
117
+ # Automagically map every YGNode* function that recieves a YGNodeRef as the
118
+ # first value. :tada:.
119
+ ::YogaLayout::Bindings.functions.values.each do |fn_info|
120
+ native_name, args, return_type = fn_info
121
+ native_name = native_name.to_s
122
+
123
+ # Don't expose methods that return pointers automatically: We should
124
+ # always wrap those pointers in Ruby objects for safety.
125
+ next if return_type == :YGNodeRef
126
+ next if return_type == :YGConfigRef
127
+ next if return_type == :pointer
128
+
129
+ # Only map style getters/setters automatically
130
+ next unless native_name =~ /^YGNode(Style|Layout)(Set|Get)/
131
+ next unless args.first == :YGNodeRef
132
+
133
+ match = Regexp.last_match
134
+ domain = match[1]
135
+ action = match[2]
136
+ prop = YogaLayout.underscore(match.post_match)
137
+
138
+ ruby_name = YogaLayout.underscore(native_name.gsub(/^YGNode/, ''))
139
+ map_method(ruby_name.to_sym, native_name.to_sym)
140
+
141
+ if args[1] == :YGEdge
142
+ # generate direct accessors for each edge property
143
+ # eg, style_set_border_top or style_set_border_all
144
+ YogaLayout::Bindings::Edge.to_h.each do |edge_sym, _|
145
+ # Cannot get layout properties of multi-edge shorthands
146
+ if [:horizontal, :vertical, :all].include?(edge_sym) && action == 'Get'
147
+ next
148
+ end
149
+
150
+ sub_name = "#{ruby_name}_#{edge_sym}".to_sym
151
+ map_method(sub_name, native_name.to_sym, [edge_sym])
152
+ layout_props[sub_name] = "#{prop}_#{edge_sym}".to_sym if domain == 'Layout' && action == 'Get'
153
+ style_props[sub_name] = "#{prop}_#{edge_sym}".to_sym if domain == 'Style' && action == 'Get'
154
+ end
155
+ end
156
+
157
+
158
+ # record information about getters, used for the #layout and #style
159
+ # instance methods
160
+ if domain && action == 'Get' && args.size == 1
161
+ layout_props[ruby_name.to_sym] = prop.to_sym if domain == 'Layout'
162
+ style_props[ruby_name.to_sym] = prop.to_sym if domain == 'Style'
163
+ end
164
+ end
165
+ style_props.freeze
166
+ layout_props.freeze
167
+
168
+ def reset
169
+ if has_children?
170
+ raise Error, 'Cannot reset a node which still has children attached'
171
+ end
172
+
173
+ unless parent.nil?
174
+ raise Error, 'Cannot reset a node still attached to a parent'
175
+ end
176
+
177
+ YogaLayout::Bindings.YGNodeReset(pointer)
178
+ @measure_func = nil
179
+ @baseline_func = nil
180
+ @data = nil
181
+ end
182
+
183
+ def insert_child(node, idx)
184
+ unless node.is_a?(self.class)
185
+ raise TypeError, "Child #{node.inspect} must be a YogaLayout::Node"
186
+ end
187
+
188
+ if node.parent
189
+ raise Error, "Child #{node.inspect} already has a parent, it must be removed first."
190
+ end
191
+
192
+ if has_measure_func?
193
+ raise Error, 'Cannot add child: Nodes with measure functions cannot have children.'
194
+ end
195
+
196
+ YogaLayout::Bindings.YGNodeInsertChild(pointer, node.pointer, idx)
197
+ @children.insert(idx, node)
198
+ node.parent = self
199
+ self
200
+ end
201
+
202
+ def remove_child(node)
203
+ # If asked to remove a child that isn't a child, Yoga just does nothing, so this is okay
204
+ YogaLayout::Bindings.YGNodeRemoveChild(pointer, node.pointer)
205
+ if @children.delete(node)
206
+ node.parent = nil
207
+ end
208
+ self
209
+ end
210
+
211
+ def get_child(idx)
212
+ ruby_child = @children[idx]
213
+ child_pointer = YogaLayout::Bindings.YGNodeGetChild(pointer, idx)
214
+ return nil if ruby_child.nil? && child_pointer.nil?
215
+ unless ruby_child && ruby_child.pointer.address == child_pointer.address
216
+ raise Error, "Ruby child #{ruby_child.inspect} (index #{idx}) does not wrap native child #{child_pointer}"
217
+ end
218
+ ruby_child
219
+ end
220
+
221
+ map_method(:get_child_count, :YGNodeGetChildCount)
222
+
223
+ def get_parent
224
+ ruby_parent = parent
225
+ parent_pointer = YogaLayout::Bindings.YGNodeGetParent(pointer)
226
+ return nil if ruby_parent.nil? && parent_pointer.nil?
227
+ unless ruby_parent && ruby_parent.pointer == parent_pointer
228
+ raise Error, "Ruby parent #{ruby_parent.inspect} does not wrap native parent #{parent_pointer}"
229
+ end
230
+ ruby_parent
231
+ end
232
+
233
+ def set_measure_func(callable = nil, &block)
234
+ if has_children?
235
+ raise Error, 'Cannot set measure function: Nodes with measure functions cannot have children.'
236
+ end
237
+ @measure_func = callable || block
238
+ if @measure_func
239
+ YogaLayout::Bindings.YGNodeSetMeasureFunc(pointer, native_measure_func)
240
+ else
241
+ YogaLayout::Bindings.YGNodeSetMeasureFunc(pointer, nil)
242
+ end
243
+
244
+ self
245
+ end
246
+
247
+ def get_measure_func
248
+ @measure_func
249
+ end
250
+
251
+ def set_baseline_func(callable = nil, &block)
252
+ @baseline_func = callable || block
253
+ if @baseline_func
254
+ YogaLayout::Bindings.YGNodeSetBaselineFunc(pointer, native_baseline_func)
255
+ else
256
+ YogaLayout::Bindings.YGNodeSetBaselineFunc(pointer, nil)
257
+ end
258
+
259
+ self
260
+ end
261
+
262
+ def get_baseline_func
263
+ @baseline_func
264
+ end
265
+
266
+ def mark_dirty
267
+ unless has_measure_func?
268
+ raise Error, 'Only leaf nodes with custom measure functions should manually mark themselves as diry'
269
+ end
270
+
271
+ YogaLayout::Bindings.YGNodeMarkDirty(pointer)
272
+
273
+ self
274
+ end
275
+
276
+ map_method(:dirty?, :YGNodeIsDirty)
277
+
278
+ map_method(:print, :YGNodePrint)
279
+
280
+ def has_measure_func?
281
+ get_measure_func != nil
282
+ end
283
+
284
+ def has_children?
285
+ get_child_count != 0
286
+ end
287
+
288
+ def calculate_layout(par_width = nil, par_height = nil, par_dir = nil)
289
+ undefined = ::Float::NAN
290
+ YogaLayout::Bindings.YGNodeCalculateLayout(
291
+ pointer,
292
+ par_width || undefined,
293
+ par_height || undefined,
294
+ par_dir || :inherit
295
+ )
296
+ self
297
+ end
298
+
299
+ protected
300
+
301
+ attr_accessor :parent
302
+
303
+ def native_measure_func
304
+ @native_measure_func ||= ::FFI::Function.new(
305
+ :YGSize, [
306
+ :YGNodeRef,
307
+ :float,
308
+ :YGMeasureMode,
309
+ :float,
310
+ :YGMeasureMode,
311
+ ],
312
+ method(:native_measure_func_callback)
313
+ )
314
+ end
315
+
316
+ def native_measure_func_callback(_, widht, width_mode, height, height_mode)
317
+ # we ignore the YGNodeRef param because we can just use "self" instead
318
+ @measure_func.call(self, widht, width_mode, height, height_mode)
319
+ end
320
+
321
+ def native_baseline_func
322
+ @native_baseline_func ||= ::FFI::Function.new(
323
+ :float,
324
+ [:YGNodeRef, :float, :float],
325
+ method(:native_baseline_func_callback)
326
+ )
327
+ end
328
+
329
+ def native_baseline_func_callback(_, width, height)
330
+ @baseline_func.call(self, width, height)
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,3 @@
1
+ module YogaLayout
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,103 @@
1
+ module YogaLayout
2
+ # Abstract base class for wrapping a FFI::AutoPointer in a Ruby class
3
+ class Wrapper
4
+ # Create a new native object.
5
+ # Must be implemented by a subclass.
6
+ #
7
+ # @abstract
8
+ # @api private
9
+ def self.unsafe_new_pointer
10
+ raise ::NotImplementedError
11
+ end
12
+
13
+ # Free a native object as created by unsafe_new_pointer
14
+ # Must be implemented by a subclass.
15
+ #
16
+ # @abstract
17
+ # @api private
18
+ def self.unsafe_free_pointer(pointer)
19
+ raise ::NotImplementedError
20
+ end
21
+
22
+ # Convert a pointer into an AutoPointer.
23
+ #
24
+ # A FFI::AutoPointer automatically calls its destructor function when it
25
+ # would be garbage collected by the Ruby runtime. By using autopointers, we
26
+ # can extend the Ruby garbage collector to cover memory allocated by Yoga.
27
+ #
28
+ # Care must be taken to maintain a reference to each autopointer for as
29
+ # long as a function could retrieve that node.
30
+ #
31
+ # @api private
32
+ # @param pointer [FFI::Pointer]
33
+ # @return [FFI::AutoPointer]
34
+ def self.auto_ptr(pointer)
35
+ ::FFI::AutoPointer.new(pointer, self.method(:unsafe_free_pointer))
36
+ end
37
+
38
+ # Define a method on this class that wraps the given YogaLayout::Binding
39
+ # FFI function. The wrapper's {#pointer} will be passed as the first argument
40
+ # to the FFI function.
41
+ #
42
+ # @api private
43
+ # @param ruby_name [Symbol] The name of the method to define on this class.
44
+ # @param binding_name [Sybmol] The name of the FFI function in {YogaLayout::Binding.functions}.
45
+ # @param memo_args [Array<any>] Positional arguments to pass after `self.pointer`
46
+ def self.map_method(ruby_name, binding_name, memo_args=[])
47
+ info = YogaLayout::Bindings.functions.fetch(binding_name)
48
+ _, binding_args, return_type = info
49
+ reciever_type, *method_args = binding_args
50
+
51
+ args_names = method_args.each_with_index.map do |type, i|
52
+ as_string = if type.is_a?(Symbol)
53
+ type.to_s.gsub(/^YG/, '')
54
+ elsif type.respond_to?(:name)
55
+ type.name.to_s.split('::').last
56
+ else
57
+ "unknown"
58
+ end
59
+ YogaLayout.underscore(as_string) + "_#{i}"
60
+ end
61
+
62
+ # remove things we aren't buying
63
+ args_names.shift(memo_args.length)
64
+ args_list_literal = args_names.join(', ')
65
+ memo_args_literal = (['pointer'] + memo_args.map(&:inspect) + args_names).join(', ')
66
+
67
+ defn = <<-EOS
68
+ def #{ruby_name}(#{args_list_literal})
69
+ ::YogaLayout::Bindings.#{binding_name}(#{memo_args_literal})
70
+ end
71
+ EOS
72
+
73
+ class_eval(defn)
74
+ end
75
+
76
+ # Create a new instane of this wrapper class.
77
+ #
78
+ # A new underlying native object will be created, unless you pass an
79
+ # existing AutoPointer.
80
+ #
81
+ # @param auto_ptr [FFI::AutoPointer] wrap this native object
82
+ def initialize(auto_ptr = nil)
83
+ auto_ptr ||= self.class.auto_ptr(self.class.unsafe_new_pointer)
84
+
85
+ unless auto_ptr.is_a?(::FFI::AutoPointer)
86
+ raise ::TypeError, "auto_ptr must be an FFI::AutoPointer, was #{auto_ptr.inspect}"
87
+ end
88
+
89
+ @pointer = auto_ptr
90
+ end
91
+
92
+ # @api private
93
+ attr_reader :pointer
94
+
95
+ def ==(other)
96
+ other.class == self.class && other.pointer.address == pointer.address
97
+ end
98
+
99
+ def inspect
100
+ "#<#{self.class} pointer=#{pointer.inspect}>"
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,45 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "yoga_layout/version"
5
+
6
+ # Safe git_ls_files
7
+ def git_ls_files(path)
8
+ operation = "git -C #{path} ls-files -z"
9
+ files = `#{operation}`.split("\x0")
10
+ raise "Failed optation #{operation.inspect}" unless $?.success?
11
+ files
12
+ end
13
+
14
+ Gem::Specification.new do |spec|
15
+ spec.name = "yoga_layout"
16
+ spec.version = YogaLayout::VERSION
17
+ spec.authors = ["Jake Teton-Landis"]
18
+ spec.email = ["jake.tl@airbnb.com"]
19
+
20
+ spec.summary = %q{FFI-based wrapper of the cross-platform Yoga layout library}
21
+ spec.homepage = "https://github.com/justjake/yoga"
22
+
23
+ spec.files = [
24
+ # All the files tracked in git, except for tests.
25
+ *git_ls_files('.').reject { |f| f.match(%r{^(test|spec|features)/}) },
26
+
27
+ # These files are copied by Rake inot ext/yoga_layout during build, but are
28
+ # not tracked in Git.
29
+ *Dir['ext/yoga_layout/*.{c,h}'],
30
+ ]
31
+
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+ spec.extensions = ["ext/yoga_layout/extconf.rb"]
36
+
37
+ spec.add_dependency 'ffi'
38
+
39
+ spec.add_development_dependency "bundler", "~> 1.15"
40
+ spec.add_development_dependency "rake", "~> 10.0"
41
+ spec.add_development_dependency "rake-compiler"
42
+ spec.add_development_dependency "rspec", "~> 3.0"
43
+ spec.add_development_dependency 'pry'
44
+ spec.add_development_dependency 'drawille' # for a demo
45
+ end