yoga_layout 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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