yoga-layout-python 0.1.0__py3-none-any.whl

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 (51) hide show
  1. yoga/YGConfig.py +92 -0
  2. yoga/YGEnums.py +404 -0
  3. yoga/YGMacros.py +9 -0
  4. yoga/YGNode.py +367 -0
  5. yoga/YGNodeLayout.py +81 -0
  6. yoga/YGNodeStyle.py +876 -0
  7. yoga/YGPixelGrid.py +42 -0
  8. yoga/YGValue.py +49 -0
  9. yoga/__init__.py +16 -0
  10. yoga/algorithm/AbsoluteLayout.py +417 -0
  11. yoga/algorithm/Align.py +34 -0
  12. yoga/algorithm/Baseline.py +54 -0
  13. yoga/algorithm/BoundAxis.py +55 -0
  14. yoga/algorithm/Cache.py +93 -0
  15. yoga/algorithm/CalculateLayout.py +1651 -0
  16. yoga/algorithm/FlexDirection.py +76 -0
  17. yoga/algorithm/FlexLine.py +130 -0
  18. yoga/algorithm/PixelGrid.py +56 -0
  19. yoga/algorithm/SizingMode.py +39 -0
  20. yoga/algorithm/TrailingPosition.py +34 -0
  21. yoga/algorithm/__init__.py +18 -0
  22. yoga/config/Config.py +137 -0
  23. yoga/config/__init__.py +9 -0
  24. yoga/debug/AssertFatal.py +24 -0
  25. yoga/debug/Log.py +49 -0
  26. yoga/debug/__init__.py +10 -0
  27. yoga/event/__init__.py +9 -0
  28. yoga/event/event.py +123 -0
  29. yoga/node/CachedMeasurement.py +51 -0
  30. yoga/node/LayoutResults.py +225 -0
  31. yoga/node/LayoutableChildren.py +79 -0
  32. yoga/node/Node.py +566 -0
  33. yoga/node/__init__.py +11 -0
  34. yoga/numeric/Comparison.py +46 -0
  35. yoga/numeric/FloatMath.py +24 -0
  36. yoga/numeric/FloatOptional.py +65 -0
  37. yoga/numeric/__init__.py +10 -0
  38. yoga/style/GridLine.py +44 -0
  39. yoga/style/GridTrack.py +47 -0
  40. yoga/style/SmallValueBuffer.py +133 -0
  41. yoga/style/Style.py +763 -0
  42. yoga/style/StyleLength.py +88 -0
  43. yoga/style/StyleSizeLength.py +117 -0
  44. yoga/style/StyleValueHandle.py +98 -0
  45. yoga/style/StyleValuePool.py +191 -0
  46. yoga/style/__init__.py +16 -0
  47. yoga_layout_python-0.1.0.dist-info/METADATA +158 -0
  48. yoga_layout_python-0.1.0.dist-info/RECORD +51 -0
  49. yoga_layout_python-0.1.0.dist-info/WHEEL +5 -0
  50. yoga_layout_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  51. yoga_layout_python-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,88 @@
1
+ """
2
+ Copyright (c) Meta Platforms, Inc. and affiliates.
3
+
4
+ This source code is licensed under the MIT license found in the
5
+ LICENSE file in the root directory of this source tree.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from ..YGEnums import YGUnit
13
+ from ..YGValue import YGValue
14
+ from ..numeric.FloatMath import float32
15
+ from ..numeric.FloatOptional import FloatOptional, inexactEquals as inexactEqualsFloatOptional
16
+ from ..numeric.Comparison import isUndefined, isinf
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class StyleLength:
21
+ value_: FloatOptional = FloatOptional()
22
+ unit_: YGUnit = YGUnit.YGUnitUndefined
23
+
24
+ @staticmethod
25
+ def points(value: float) -> "StyleLength":
26
+ return (
27
+ StyleLength.undefined()
28
+ if isUndefined(value) or isinf(value)
29
+ else StyleLength(FloatOptional(value), YGUnit.YGUnitPoint)
30
+ )
31
+
32
+ @staticmethod
33
+ def percent(value: float) -> "StyleLength":
34
+ return (
35
+ StyleLength.undefined()
36
+ if isUndefined(value) or isinf(value)
37
+ else StyleLength(FloatOptional(value), YGUnit.YGUnitPercent)
38
+ )
39
+
40
+ @staticmethod
41
+ def ofAuto() -> "StyleLength":
42
+ return StyleLength(FloatOptional(), YGUnit.YGUnitAuto)
43
+
44
+ @staticmethod
45
+ def undefined() -> "StyleLength":
46
+ return StyleLength(FloatOptional(), YGUnit.YGUnitUndefined)
47
+
48
+ def isAuto(self) -> bool:
49
+ return self.unit_ == YGUnit.YGUnitAuto
50
+
51
+ def isUndefined(self) -> bool:
52
+ return self.unit_ == YGUnit.YGUnitUndefined
53
+
54
+ def isPoints(self) -> bool:
55
+ return self.unit_ == YGUnit.YGUnitPoint
56
+
57
+ def isPercent(self) -> bool:
58
+ return self.unit_ == YGUnit.YGUnitPercent
59
+
60
+ def isDefined(self) -> bool:
61
+ return not self.isUndefined()
62
+
63
+ def value(self) -> FloatOptional:
64
+ return self.value_
65
+
66
+ def resolve(self, referenceLength: float) -> FloatOptional:
67
+ if self.unit_ == YGUnit.YGUnitPoint:
68
+ return self.value_
69
+ if self.unit_ == YGUnit.YGUnitPercent:
70
+ return FloatOptional(
71
+ float32(
72
+ float32(float32(self.value_.unwrap()) * float32(referenceLength))
73
+ * float32(0.01)
74
+ )
75
+ )
76
+ return FloatOptional()
77
+
78
+ def asYGValue(self) -> YGValue:
79
+ return YGValue(self.value_.unwrap(), self.unit_)
80
+
81
+ def inexactEquals(self, other: "StyleLength") -> bool:
82
+ return self.unit_ == other.unit_ and inexactEqualsFloatOptional(
83
+ self.value_, other.value_
84
+ )
85
+
86
+
87
+ def inexactEquals(a: StyleLength, b: StyleLength) -> bool:
88
+ return a.inexactEquals(b)
@@ -0,0 +1,117 @@
1
+ """
2
+ Copyright (c) Meta Platforms, Inc. and affiliates.
3
+
4
+ This source code is licensed under the MIT license found in the
5
+ LICENSE file in the root directory of this source tree.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from ..YGEnums import YGUnit
13
+ from ..YGValue import YGValue
14
+ from ..numeric.FloatMath import float32
15
+ from ..numeric.FloatOptional import FloatOptional, inexactEquals as inexactEqualsFloatOptional
16
+ from ..numeric.Comparison import isUndefined, isinf
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class StyleSizeLength:
21
+ value_: FloatOptional = FloatOptional()
22
+ unit_: YGUnit = YGUnit.YGUnitUndefined
23
+
24
+ @staticmethod
25
+ def points(value: float) -> "StyleSizeLength":
26
+ return (
27
+ StyleSizeLength.undefined()
28
+ if isUndefined(value) or isinf(value)
29
+ else StyleSizeLength(FloatOptional(value), YGUnit.YGUnitPoint)
30
+ )
31
+
32
+ @staticmethod
33
+ def percent(value: float) -> "StyleSizeLength":
34
+ return (
35
+ StyleSizeLength.undefined()
36
+ if isUndefined(value) or isinf(value)
37
+ else StyleSizeLength(FloatOptional(value), YGUnit.YGUnitPercent)
38
+ )
39
+
40
+ @staticmethod
41
+ def stretch(fraction: float) -> "StyleSizeLength":
42
+ return (
43
+ StyleSizeLength.undefined()
44
+ if isUndefined(fraction) or isinf(fraction)
45
+ else StyleSizeLength(FloatOptional(fraction), YGUnit.YGUnitStretch)
46
+ )
47
+
48
+ @staticmethod
49
+ def ofAuto() -> "StyleSizeLength":
50
+ return StyleSizeLength(FloatOptional(), YGUnit.YGUnitAuto)
51
+
52
+ @staticmethod
53
+ def ofMaxContent() -> "StyleSizeLength":
54
+ return StyleSizeLength(FloatOptional(), YGUnit.YGUnitMaxContent)
55
+
56
+ @staticmethod
57
+ def ofFitContent() -> "StyleSizeLength":
58
+ return StyleSizeLength(FloatOptional(), YGUnit.YGUnitFitContent)
59
+
60
+ @staticmethod
61
+ def ofStretch() -> "StyleSizeLength":
62
+ return StyleSizeLength(FloatOptional(), YGUnit.YGUnitStretch)
63
+
64
+ @staticmethod
65
+ def undefined() -> "StyleSizeLength":
66
+ return StyleSizeLength(FloatOptional(), YGUnit.YGUnitUndefined)
67
+
68
+ def isAuto(self) -> bool:
69
+ return self.unit_ == YGUnit.YGUnitAuto
70
+
71
+ def isMaxContent(self) -> bool:
72
+ return self.unit_ == YGUnit.YGUnitMaxContent
73
+
74
+ def isFitContent(self) -> bool:
75
+ return self.unit_ == YGUnit.YGUnitFitContent
76
+
77
+ def isStretch(self) -> bool:
78
+ return self.unit_ == YGUnit.YGUnitStretch
79
+
80
+ def isUndefined(self) -> bool:
81
+ return self.unit_ == YGUnit.YGUnitUndefined
82
+
83
+ def isDefined(self) -> bool:
84
+ return not self.isUndefined()
85
+
86
+ def isPoints(self) -> bool:
87
+ return self.unit_ == YGUnit.YGUnitPoint
88
+
89
+ def isPercent(self) -> bool:
90
+ return self.unit_ == YGUnit.YGUnitPercent
91
+
92
+ def value(self) -> FloatOptional:
93
+ return self.value_
94
+
95
+ def resolve(self, referenceLength: float) -> FloatOptional:
96
+ if self.unit_ == YGUnit.YGUnitPoint:
97
+ return self.value_
98
+ if self.unit_ == YGUnit.YGUnitPercent:
99
+ return FloatOptional(
100
+ float32(
101
+ float32(float32(self.value_.unwrap()) * float32(referenceLength))
102
+ * float32(0.01)
103
+ )
104
+ )
105
+ return FloatOptional()
106
+
107
+ def asYGValue(self) -> YGValue:
108
+ return YGValue(self.value_.unwrap(), self.unit_)
109
+
110
+ def inexactEquals(self, other: "StyleSizeLength") -> bool:
111
+ return self.unit_ == other.unit_ and inexactEqualsFloatOptional(
112
+ self.value_, other.value_
113
+ )
114
+
115
+
116
+ def inexactEquals(a: StyleSizeLength, b: StyleSizeLength) -> bool:
117
+ return a.inexactEquals(b)
@@ -0,0 +1,98 @@
1
+ """
2
+ Copyright (c) Meta Platforms, Inc. and affiliates.
3
+
4
+ This source code is licensed under the MIT license found in the
5
+ LICENSE file in the root directory of this source tree.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ class StyleValueHandle:
12
+ """
13
+ A compact (16-bit) handle to a length or number in a style.
14
+
15
+ The value may be embedded directly in the handle if simple,
16
+ or the handle may instead point to an index within a StyleValuePool.
17
+ """
18
+
19
+ # Bit masks for handle encoding
20
+ HANDLE_TYPE_MASK = 0b0000_0000_0000_0111
21
+ HANDLE_INDEXED_MASK = 0b0000_0000_0000_1000
22
+ HANDLE_VALUE_MASK = 0b1111_1111_1111_0000
23
+
24
+ class Type:
25
+ Undefined = 0
26
+ Point = 1
27
+ Percent = 2
28
+ Number = 3
29
+ Auto = 4
30
+ Keyword = 5
31
+
32
+ class Keyword:
33
+ MaxContent = 0
34
+ FitContent = 1
35
+ Stretch = 2
36
+
37
+ def __init__(self, repr_: int = 0) -> None:
38
+ self._repr = repr_
39
+
40
+ @classmethod
41
+ def ofAuto(cls) -> "StyleValueHandle":
42
+ """Create a handle representing 'auto'."""
43
+ handle = cls()
44
+ handle._set_type(cls.Type.Auto)
45
+ return handle
46
+
47
+ @classmethod
48
+ def ofUndefined(cls) -> "StyleValueHandle":
49
+ """Create a handle representing 'undefined'."""
50
+ handle = cls()
51
+ handle._set_type(cls.Type.Undefined)
52
+ return handle
53
+
54
+ def isUndefined(self) -> bool:
55
+ """Check if the handle represents undefined."""
56
+ return self._type() == self.Type.Undefined
57
+
58
+ def isDefined(self) -> bool:
59
+ """Check if the handle represents a defined value."""
60
+ return not self.isUndefined()
61
+
62
+ def isAuto(self) -> bool:
63
+ """Check if the handle represents 'auto'."""
64
+ return self._type() == self.Type.Auto
65
+
66
+ def _type(self) -> int:
67
+ """Get the type encoded in the handle."""
68
+ return self._repr & self.HANDLE_TYPE_MASK
69
+
70
+ def _set_type(self, handle_type: int) -> None:
71
+ """Set the type in the handle."""
72
+ self._repr &= ~self.HANDLE_TYPE_MASK
73
+ self._repr |= handle_type
74
+
75
+ def _value(self) -> int:
76
+ """Get the value encoded in the handle."""
77
+ return self._repr >> 4
78
+
79
+ def _set_value(self, value: int) -> None:
80
+ """Set the value in the handle."""
81
+ self._repr &= ~self.HANDLE_VALUE_MASK
82
+ self._repr |= (value << 4)
83
+
84
+ def isValueIndexed(self) -> bool:
85
+ """Check if the value is stored in an index."""
86
+ return (self._repr & self.HANDLE_INDEXED_MASK) != 0
87
+
88
+ def setValueIsIndexed(self) -> None:
89
+ """Mark the value as indexed."""
90
+ self._repr |= self.HANDLE_INDEXED_MASK
91
+
92
+ def clearValueIsIndexed(self) -> None:
93
+ """Mark the value as inline, not indexed."""
94
+ self._repr &= ~self.HANDLE_INDEXED_MASK
95
+
96
+ def isKeyword(self, keyword: int) -> bool:
97
+ """Check if the handle represents a specific keyword."""
98
+ return self._type() == self.Type.Keyword and self._value() == keyword
@@ -0,0 +1,191 @@
1
+ """
2
+ Copyright (c) Meta Platforms, Inc. and affiliates.
3
+
4
+ This source code is licensed under the MIT license found in the
5
+ LICENSE file in the root directory of this source tree.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import struct
11
+ from typing import Optional
12
+
13
+ from .SmallValueBuffer import SmallValueBuffer
14
+ from .StyleLength import StyleLength
15
+ from .StyleSizeLength import StyleSizeLength
16
+ from .StyleValueHandle import StyleValueHandle
17
+ from ..numeric.FloatOptional import FloatOptional
18
+
19
+
20
+ class StyleValuePool:
21
+ """
22
+ Pool for compact storage of sparse lengths and numbers.
23
+
24
+ Values are referred to using StyleValueHandle. In most cases
25
+ StyleValueHandle can embed the value directly, but if not,
26
+ the value is stored within a buffer provided by the pool.
27
+ """
28
+
29
+ # Number of inline slots before falling back to heap allocation
30
+ INLINE_BUFFER_SIZE = 4
31
+
32
+ def __init__(self) -> None:
33
+ self._buffer = SmallValueBuffer(buffer_size=self.INLINE_BUFFER_SIZE)
34
+
35
+ def store_length(self, handle: StyleValueHandle, length: StyleLength) -> None:
36
+ """Store a StyleLength in the handle."""
37
+ if length.isUndefined():
38
+ handle._set_type(StyleValueHandle.Type.Undefined)
39
+ handle.clearValueIsIndexed()
40
+ elif length.isAuto():
41
+ handle._set_type(StyleValueHandle.Type.Auto)
42
+ handle.clearValueIsIndexed()
43
+ else:
44
+ type_ = (
45
+ StyleValueHandle.Type.Point
46
+ if length.isPoints()
47
+ else StyleValueHandle.Type.Percent
48
+ )
49
+ self._store_value(handle, length.value().unwrap(), type_)
50
+
51
+ def store_size(self, handle: StyleValueHandle, size_value: StyleSizeLength) -> None:
52
+ """Store a StyleSizeLength in the handle."""
53
+ if size_value.isUndefined():
54
+ handle._set_type(StyleValueHandle.Type.Undefined)
55
+ handle.clearValueIsIndexed()
56
+ elif size_value.isAuto():
57
+ handle._set_type(StyleValueHandle.Type.Auto)
58
+ handle.clearValueIsIndexed()
59
+ elif size_value.isMaxContent():
60
+ self._store_keyword(handle, StyleValueHandle.Keyword.MaxContent)
61
+ elif size_value.isStretch():
62
+ self._store_keyword(handle, StyleValueHandle.Keyword.Stretch)
63
+ elif size_value.isFitContent():
64
+ self._store_keyword(handle, StyleValueHandle.Keyword.FitContent)
65
+ else:
66
+ type_ = (
67
+ StyleValueHandle.Type.Point
68
+ if size_value.isPoints()
69
+ else StyleValueHandle.Type.Percent
70
+ )
71
+ self._store_value(handle, size_value.value().unwrap(), type_)
72
+
73
+ def store_number(
74
+ self, handle: StyleValueHandle, number: FloatOptional
75
+ ) -> None:
76
+ """Store a number in the handle."""
77
+ if number.isUndefined():
78
+ handle._set_type(StyleValueHandle.Type.Undefined)
79
+ handle.clearValueIsIndexed()
80
+ else:
81
+ self._store_value(handle, number.unwrap(), StyleValueHandle.Type.Number)
82
+
83
+ def get_length(self, handle: StyleValueHandle) -> StyleLength:
84
+ """Get a StyleLength from the handle."""
85
+ if handle.isUndefined():
86
+ return StyleLength.undefined()
87
+ elif handle.isAuto():
88
+ return StyleLength.ofAuto()
89
+ else:
90
+ value = self._get_float_value(handle)
91
+ if handle._type() == StyleValueHandle.Type.Point:
92
+ return StyleLength.points(value)
93
+ else:
94
+ return StyleLength.percent(value)
95
+
96
+ def get_size(self, handle: StyleValueHandle) -> StyleSizeLength:
97
+ """Get a StyleSizeLength from the handle."""
98
+ if handle.isUndefined():
99
+ return StyleSizeLength.undefined()
100
+ elif handle.isAuto():
101
+ return StyleSizeLength.ofAuto()
102
+ elif handle.isKeyword(StyleValueHandle.Keyword.MaxContent):
103
+ return StyleSizeLength.ofMaxContent()
104
+ elif handle.isKeyword(StyleValueHandle.Keyword.FitContent):
105
+ return StyleSizeLength.ofFitContent()
106
+ elif handle.isKeyword(StyleValueHandle.Keyword.Stretch):
107
+ return StyleSizeLength.ofStretch()
108
+ else:
109
+ value = self._get_float_value(handle)
110
+ if handle._type() == StyleValueHandle.Type.Point:
111
+ return StyleSizeLength.points(value)
112
+ else:
113
+ return StyleSizeLength.percent(value)
114
+
115
+ def get_number(self, handle: StyleValueHandle) -> FloatOptional:
116
+ """Get a number from the handle."""
117
+ if handle.isUndefined():
118
+ return FloatOptional()
119
+ else:
120
+ value = self._get_float_value(handle)
121
+ return FloatOptional(value)
122
+
123
+ def _store_value(
124
+ self, handle: StyleValueHandle, value: float, type_: int
125
+ ) -> None:
126
+ """Store a float value in the handle."""
127
+ handle._set_type(type_)
128
+
129
+ if self._is_integer_packable(value):
130
+ if handle.isValueIndexed():
131
+ handle.clearValueIsIndexed()
132
+ handle._set_value(self._pack_inline_integer(value))
133
+ else:
134
+ packed = struct.pack("<f", value)
135
+ int_value = struct.unpack("<I", packed)[0]
136
+ new_index = (
137
+ self._buffer.replace(handle._value(), int_value)
138
+ if handle.isValueIndexed()
139
+ else self._buffer.push(int_value)
140
+ )
141
+ handle._set_value(new_index)
142
+ handle.setValueIsIndexed()
143
+
144
+ def _store_keyword(
145
+ self, handle: StyleValueHandle, keyword: int
146
+ ) -> None:
147
+ """Store a keyword in the handle."""
148
+ handle._set_type(StyleValueHandle.Type.Keyword)
149
+ if handle.isValueIndexed():
150
+ new_index = self._buffer.replace(handle._value(), keyword)
151
+ handle._set_value(new_index)
152
+ else:
153
+ handle._set_value(keyword)
154
+
155
+ def _get_float_value(self, handle: StyleValueHandle) -> float:
156
+ """Extract a float value from the handle."""
157
+ if handle.isValueIndexed():
158
+ int_value = self._buffer.get32(handle._value())
159
+ return struct.unpack("<f", struct.pack("<I", int_value))[0]
160
+ else:
161
+ return self._unpack_inline_integer(handle._value())
162
+
163
+ @staticmethod
164
+ def _is_integer_packable(f: float) -> bool:
165
+ """Check if a float can be packed as an inline integer."""
166
+ k_max_inline_abs_value = (1 << 11) - 1 # 2047
167
+ i = int(f)
168
+ return float(i) == f and -k_max_inline_abs_value <= i <= k_max_inline_abs_value
169
+
170
+ @staticmethod
171
+ def _pack_inline_integer(value: float) -> int:
172
+ """Pack a float as an inline integer."""
173
+ is_negative = 1 if value < 0 else 0
174
+ magnitude = abs(int(value))
175
+ return (is_negative << 11) | magnitude
176
+
177
+ @staticmethod
178
+ def _unpack_inline_integer(value: int) -> float:
179
+ """Unpack an inline integer to a float."""
180
+ k_value_sign_mask = 0b0000_1000_0000_0000
181
+ k_value_magnitude_mask = 0b0000_0111_1111_1111
182
+
183
+ is_negative = (value & k_value_sign_mask) != 0
184
+ magnitude = value & k_value_magnitude_mask
185
+ return float(-magnitude if is_negative else magnitude)
186
+
187
+ def copy(self) -> "StyleValuePool":
188
+ """Create a copy of this pool."""
189
+ new_pool = StyleValuePool()
190
+ new_pool._buffer = self._buffer.copy()
191
+ return new_pool
yoga/style/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """
2
+ Copyright (c) Meta Platforms, Inc. and affiliates.
3
+
4
+ This source code is licensed under the MIT license found in the
5
+ LICENSE file in the root directory of this source tree.
6
+ """
7
+
8
+ from .GridLine import *
9
+ from .GridTrack import *
10
+ from .SmallValueBuffer import *
11
+ from .Style import *
12
+ from .StyleLength import *
13
+ from .StyleSizeLength import *
14
+ from .StyleValueHandle import *
15
+ from .StyleValuePool import *
16
+
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: yoga-layout-python
3
+ Version: 0.1.0
4
+ Summary: Pure Python port of Meta Yoga layout engine
5
+ Author: Meta Platforms, Inc. and affiliates.
6
+ Maintainer: Quantmew Contributors
7
+ License: MIT License
8
+
9
+ Copyright (c) Facebook, Inc. and its affiliates.
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Project-URL: Homepage, https://github.com/quantmew/yoga-layout-python
30
+ Project-URL: Repository, https://github.com/quantmew/yoga-layout-python
31
+ Project-URL: Documentation, https://yogalayout.dev/
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3 :: Only
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
42
+ Requires-Python: >=3.9
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Provides-Extra: dev
46
+ Requires-Dist: pytest>=7.0; extra == "dev"
47
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
48
+ Requires-Dist: black>=23.0; extra == "dev"
49
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
50
+ Requires-Dist: mypy>=1.0; extra == "dev"
51
+ Provides-Extra: test
52
+ Requires-Dist: pytest>=7.0; extra == "test"
53
+ Requires-Dist: pytest-cov>=4.0; extra == "test"
54
+ Dynamic: license-file
55
+
56
+ # yoga-layout-python
57
+
58
+ `yoga-layout-python` is a pure Python port of Meta's Yoga layout engine.
59
+
60
+ ## Current Status
61
+
62
+ - Public enums, value/config primitives, and the core layout algorithm are implemented in Python.
63
+ - Internal module layout mirrors upstream `yoga/` so translation and parity work can stay file-for-file aligned.
64
+ - Upstream test translations pass, and a Python-vs-C++ differential parity harness is available under `tools/`.
65
+
66
+ ## Installation
67
+
68
+ ```bash
69
+ pip install -e .
70
+ ```
71
+
72
+ For development:
73
+
74
+ ```bash
75
+ pip install -e ".[dev]"
76
+ ```
77
+
78
+ ## API Shape
79
+
80
+ The Python package keeps Yoga's public naming style:
81
+
82
+ ```python
83
+ from yoga import *
84
+
85
+ config = YGConfigNew()
86
+ node = YGNodeNewWithConfig(config)
87
+ YGNodeStyleSetWidth(node, 100)
88
+ YGNodeStyleSetHeight(node, 50)
89
+ YGNodeCalculateLayout(node, YGUndefined, YGUndefined, YGDirectionLTR)
90
+
91
+ width = YGNodeLayoutGetWidth(node)
92
+ height = YGNodeLayoutGetHeight(node)
93
+ ```
94
+
95
+ ## Running Tests
96
+
97
+ ```bash
98
+ # Run all tests
99
+ pytest
100
+
101
+ # Run with coverage
102
+ pytest --cov=yoga --cov-report=term-missing
103
+
104
+ # Run specific test file
105
+ pytest tests/test_smoke.py -v
106
+ ```
107
+
108
+ ## Differential Parity
109
+
110
+ The repository includes a differential parity runner that compares Python layout
111
+ results against the vendored upstream C++ Yoga engine on the same captured tree.
112
+
113
+ ```bash
114
+ # Run the default parity suite
115
+ python tools/run_differential_ci.py
116
+
117
+ # Rebuild the C++ runner first
118
+ python tools/run_differential_ci.py --force-rebuild
119
+
120
+ # Increase the random batch size
121
+ python tools/run_differential_ci.py --random-count 1000 --seed 20260317
122
+ ```
123
+
124
+ On failure, the script writes a repro capture to
125
+ `build/differential_failure_capture.json`.
126
+
127
+ The lower-level harness remains available if you want the raw output format:
128
+
129
+ ```bash
130
+ python tools/differential_test.py
131
+ ```
132
+
133
+ ## Project Structure
134
+
135
+ ```
136
+ src/yoga/
137
+ ├── algorithm/ # Core layout algorithms (CalculateLayout, FlexLine, etc.)
138
+ ├── config/ # Configuration handling
139
+ ├── debug/ # Debug utilities (logging, assertions)
140
+ ├── event/ # Event system
141
+ ├── node/ # Node class and layout results
142
+ ├── numeric/ # Numeric utilities (FloatOptional, Comparison)
143
+ ├── style/ # Style system (Style, StyleLength, etc.)
144
+ └── *.py # Public API modules
145
+
146
+ tools/
147
+ ├── differential_cpp_runner.cpp # C++ capture runner used for parity checks
148
+ ├── differential_test.py # Python/C++ differential harness
149
+ └── run_differential_ci.py # Stable CLI entrypoint for parity checks
150
+ ```
151
+
152
+ ## License
153
+
154
+ This project uses the same MIT license as upstream Yoga. See `LICENSE`.
155
+
156
+ ## Contributing
157
+
158
+ Contributions are welcome! Please see the upstream [Yoga repository](https://github.com/facebook/yoga) for reference.