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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/README.md +35 -0
- data/Rakefile +176 -0
- data/bin/console +10 -0
- data/bin/demo +89 -0
- data/bin/setup +8 -0
- data/ext/yoga_layout/extconf.rb +3 -0
- data/lib/yoga_layout.rb +33 -0
- data/lib/yoga_layout/bindings.rb +39 -0
- data/lib/yoga_layout/bindings/enums.rb +131 -0
- data/lib/yoga_layout/bindings/misc.rb +23 -0
- data/lib/yoga_layout/bindings/typedefs.rb +51 -0
- data/lib/yoga_layout/bindings/ygconfig.rb +37 -0
- data/lib/yoga_layout/bindings/ygnode.rb +59 -0
- data/lib/yoga_layout/bindings/ygnode_properties.rb +93 -0
- data/lib/yoga_layout/config.rb +56 -0
- data/lib/yoga_layout/node.rb +333 -0
- data/lib/yoga_layout/version.rb +3 -0
- data/lib/yoga_layout/wrapper.rb +103 -0
- data/yoga_layout.gemspec +45 -0
- metadata +175 -0
@@ -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,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
|
data/yoga_layout.gemspec
ADDED
@@ -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
|