yoga-python 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {yoga_python-0.1.2 → yoga_python-0.1.3}/PKG-INFO +1 -1
- yoga_python-0.1.3/benchmarks/bench_bindings.py +109 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/pyproject.toml +1 -1
- {yoga_python-0.1.2 → yoga_python-0.1.3}/src/yoga/__init__.pyi +43 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/src/yoga/yoga.cpp +658 -9
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_tree_mutation.py +15 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/uv.lock +1 -1
- {yoga_python-0.1.2 → yoga_python-0.1.3}/.github/workflows/create-release.yml +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/.github/workflows/quality.yml +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/.github/workflows/release.yml +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/.gitignore +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/.python-version +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/CMakeLists.txt +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/LICENSE +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/README.md +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/pyrightconfig.json +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/src/yoga/__init__.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/src/yoga/py.typed +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/conftest.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_absolute_position.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_align_content.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_align_items.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_align_self.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_android_news_feed.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_aspect_ratio.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_auto.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_baseline.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_baseline_func.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_border.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_box_sizing.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_clone_node.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_clone_nofree.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_computed_margin.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_computed_padding.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_config.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_default_values.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_dimension.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_dirtied.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_dirty_marking.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_display.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_display_contents.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_edge.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_events.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_flex.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_flex_direction.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_flex_gap.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_flex_wrap.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_float_optional.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_free_order.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_gap.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_had_overflow.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_intrinsic_size.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_justify_content.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_layoutable_children.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_margin.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_measure.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_measure_cache.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_measure_mode.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_min_max_dimension.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_no_free.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_node_callback.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_node_child.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_ordinals.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_padding.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_percentage.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_persistence.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_persistent_node_cloning.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_relayout.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_rounding.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_rounding_function.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_rounding_measure.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_scale_change.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_size_overflow.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_small_value_buffer.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_static_position.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_style.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_style_value_pool.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_value.py +0 -0
- {yoga_python-0.1.2 → yoga_python-0.1.3}/tests/test_zero_out_layout.py +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Focused microbenchmarks for yoga-python binding hot paths.
|
|
2
|
+
|
|
3
|
+
Run with:
|
|
4
|
+
uv run python benchmarks/bench_bindings.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import statistics
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import yoga
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _bench(fn, *, iterations: int, warmup: int) -> dict[str, int]:
|
|
16
|
+
for _ in range(warmup):
|
|
17
|
+
fn()
|
|
18
|
+
times: list[int] = []
|
|
19
|
+
for _ in range(iterations):
|
|
20
|
+
start = time.perf_counter_ns()
|
|
21
|
+
fn()
|
|
22
|
+
times.append(time.perf_counter_ns() - start)
|
|
23
|
+
times.sort()
|
|
24
|
+
return {
|
|
25
|
+
"median_ns": times[len(times) // 2],
|
|
26
|
+
"mean_ns": int(statistics.mean(times)),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> None:
|
|
31
|
+
node = yoga.Node()
|
|
32
|
+
vals = {
|
|
33
|
+
"width": 80,
|
|
34
|
+
"height": 24,
|
|
35
|
+
"flex_grow": 1.0,
|
|
36
|
+
"flex_shrink": 1.0,
|
|
37
|
+
"flex_direction": "row",
|
|
38
|
+
"justify_content": "space-between",
|
|
39
|
+
"align_items": "center",
|
|
40
|
+
"overflow": "hidden",
|
|
41
|
+
"position_type": "relative",
|
|
42
|
+
"padding_top": 1.0,
|
|
43
|
+
"padding_right": 2.0,
|
|
44
|
+
"padding_bottom": 1.0,
|
|
45
|
+
"padding_left": 2.0,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def fast_same() -> None:
|
|
49
|
+
yoga.configure_node_fast(node, **vals)
|
|
50
|
+
|
|
51
|
+
def fast_toggle() -> None:
|
|
52
|
+
fast_toggle.state = not fast_toggle.state
|
|
53
|
+
yoga.configure_node_fast(node, **(vals if fast_toggle.state else {**vals, "width": 81}))
|
|
54
|
+
|
|
55
|
+
fast_toggle.state = False
|
|
56
|
+
|
|
57
|
+
def python_setters_same() -> None:
|
|
58
|
+
node.width = 80
|
|
59
|
+
node.height = 24
|
|
60
|
+
node.flex_grow = 1.0
|
|
61
|
+
node.flex_shrink = 1.0
|
|
62
|
+
node.flex_direction = yoga.FlexDirection.Row
|
|
63
|
+
node.justify_content = yoga.Justify.SpaceBetween
|
|
64
|
+
node.align_items = yoga.Align.Center
|
|
65
|
+
node.overflow = yoga.Overflow.Hidden
|
|
66
|
+
node.position_type = yoga.PositionType.Relative
|
|
67
|
+
node.set_padding(yoga.Edge.Top, 1.0)
|
|
68
|
+
node.set_padding(yoga.Edge.Right, 2.0)
|
|
69
|
+
node.set_padding(yoga.Edge.Bottom, 1.0)
|
|
70
|
+
node.set_padding(yoga.Edge.Left, 2.0)
|
|
71
|
+
|
|
72
|
+
def python_setters_toggle() -> None:
|
|
73
|
+
python_setters_toggle.state = not python_setters_toggle.state
|
|
74
|
+
node.width = 80 if python_setters_toggle.state else 81
|
|
75
|
+
python_setters_same()
|
|
76
|
+
|
|
77
|
+
python_setters_toggle.state = False
|
|
78
|
+
|
|
79
|
+
parent = yoga.Node()
|
|
80
|
+
children = [yoga.Node() for _ in range(500)]
|
|
81
|
+
for child in children:
|
|
82
|
+
child.width = 1
|
|
83
|
+
|
|
84
|
+
def set_children_batch() -> None:
|
|
85
|
+
parent.set_children(children)
|
|
86
|
+
|
|
87
|
+
def rebuild_insertions() -> None:
|
|
88
|
+
parent.remove_all_children()
|
|
89
|
+
for i, child in enumerate(children):
|
|
90
|
+
parent.insert_child(child, i)
|
|
91
|
+
|
|
92
|
+
cases = [
|
|
93
|
+
("configure_node_fast same", fast_same, 5000, 200),
|
|
94
|
+
("configure_node_fast toggled", fast_toggle, 5000, 200),
|
|
95
|
+
("python setters same", python_setters_same, 5000, 200),
|
|
96
|
+
("python setters toggled", python_setters_toggle, 5000, 200),
|
|
97
|
+
("set_children(500)", set_children_batch, 200, 20),
|
|
98
|
+
("remove_all+insert 500", rebuild_insertions, 200, 20),
|
|
99
|
+
]
|
|
100
|
+
for label, fn, iterations, warmup in cases:
|
|
101
|
+
result = _bench(fn, iterations=iterations, warmup=warmup)
|
|
102
|
+
print(f"{label:<28} median={result['median_ns']:>8,}ns mean={result['mean_ns']:>8,}ns")
|
|
103
|
+
|
|
104
|
+
parent.free_recursive()
|
|
105
|
+
node.free()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
|
+
type LayoutRepaintFact = tuple[object, int, bool, bool, int, int, int, int, int, int, int, int]
|
|
5
|
+
|
|
4
6
|
# Enums (nanobind enum types)
|
|
5
7
|
class Direction:
|
|
6
8
|
Inherit: Direction
|
|
@@ -577,6 +579,47 @@ class Node:
|
|
|
577
579
|
def layout_padding(self, edge: Edge | int) -> float: ...
|
|
578
580
|
def layout_border(self, edge: Edge | int) -> float: ...
|
|
579
581
|
|
|
582
|
+
# Batch layout helpers
|
|
583
|
+
def configure_node_fast(
|
|
584
|
+
node: Node,
|
|
585
|
+
*,
|
|
586
|
+
width: float | str | None = None,
|
|
587
|
+
height: float | str | None = None,
|
|
588
|
+
min_width: float | str | None = None,
|
|
589
|
+
min_height: float | str | None = None,
|
|
590
|
+
max_width: float | str | None = None,
|
|
591
|
+
max_height: float | str | None = None,
|
|
592
|
+
flex_grow: float | None = None,
|
|
593
|
+
flex_shrink: float | None = None,
|
|
594
|
+
flex_basis: float | str | None = None,
|
|
595
|
+
flex_direction: str | None = None,
|
|
596
|
+
flex_wrap: str | None = None,
|
|
597
|
+
justify_content: str | None = None,
|
|
598
|
+
align_items: str | None = None,
|
|
599
|
+
align_self: str | None = None,
|
|
600
|
+
gap: float | None = None,
|
|
601
|
+
overflow: str | None = None,
|
|
602
|
+
position_type: str | None = None,
|
|
603
|
+
padding_top: float = 0.0,
|
|
604
|
+
padding_right: float = 0.0,
|
|
605
|
+
padding_bottom: float = 0.0,
|
|
606
|
+
padding_left: float = 0.0,
|
|
607
|
+
margin: float | None = None,
|
|
608
|
+
margin_top: float | None = None,
|
|
609
|
+
margin_right: float | None = None,
|
|
610
|
+
margin_bottom: float | None = None,
|
|
611
|
+
margin_left: float | None = None,
|
|
612
|
+
pos_top: float | str | None = None,
|
|
613
|
+
pos_right: float | str | None = None,
|
|
614
|
+
pos_bottom: float | str | None = None,
|
|
615
|
+
pos_left: float | str | None = None,
|
|
616
|
+
) -> None: ...
|
|
617
|
+
def clear_node_cache(node: Node) -> None: ...
|
|
618
|
+
def apply_layout_tree(
|
|
619
|
+
root: object, offsets: dict[str, int], origin_x: int = 0, origin_y: int = 0
|
|
620
|
+
) -> list[LayoutRepaintFact]: ...
|
|
621
|
+
def get_layout_batch(node: Node) -> tuple[int, int, int, int]: ...
|
|
622
|
+
|
|
580
623
|
# Event system
|
|
581
624
|
def event_subscribe(callback: Callable[[int, EventType, Any], None]) -> None: ...
|
|
582
625
|
def event_reset() -> None: ...
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#include <nanobind/nanobind.h>
|
|
2
2
|
#include <nanobind/stl/vector.h>
|
|
3
|
+
#include <nanobind/stl/string.h>
|
|
3
4
|
|
|
5
|
+
#include <cmath>
|
|
4
6
|
#include <string>
|
|
7
|
+
#include <unordered_map>
|
|
5
8
|
#include <unordered_set>
|
|
6
9
|
|
|
7
10
|
#include <yoga/Yoga.h>
|
|
@@ -12,6 +15,58 @@
|
|
|
12
15
|
namespace nb = nanobind;
|
|
13
16
|
namespace yoga = facebook::yoga;
|
|
14
17
|
|
|
18
|
+
// Interned Python strings for fast pointer comparison in configure_node_fast().
|
|
19
|
+
// Initialized in NB_MODULE; pointer compare is ~10x faster than nb::cast<std::string>.
|
|
20
|
+
namespace interned {
|
|
21
|
+
static PyObject* row = nullptr;
|
|
22
|
+
static PyObject* column = nullptr;
|
|
23
|
+
static PyObject* column_reverse = nullptr;
|
|
24
|
+
static PyObject* row_reverse = nullptr;
|
|
25
|
+
static PyObject* wrap = nullptr;
|
|
26
|
+
static PyObject* wrap_reverse = nullptr;
|
|
27
|
+
static PyObject* nowrap = nullptr;
|
|
28
|
+
static PyObject* flex_start = nullptr;
|
|
29
|
+
static PyObject* flex_end = nullptr;
|
|
30
|
+
static PyObject* center = nullptr;
|
|
31
|
+
static PyObject* space_between = nullptr;
|
|
32
|
+
static PyObject* space_around = nullptr;
|
|
33
|
+
static PyObject* space_evenly = nullptr;
|
|
34
|
+
static PyObject* stretch = nullptr;
|
|
35
|
+
static PyObject* baseline = nullptr;
|
|
36
|
+
static PyObject* auto_ = nullptr;
|
|
37
|
+
static PyObject* visible = nullptr;
|
|
38
|
+
static PyObject* hidden = nullptr;
|
|
39
|
+
static PyObject* scroll = nullptr;
|
|
40
|
+
static PyObject* relative = nullptr;
|
|
41
|
+
static PyObject* absolute = nullptr;
|
|
42
|
+
static PyObject* static_ = nullptr;
|
|
43
|
+
|
|
44
|
+
static void init() {
|
|
45
|
+
row = PyUnicode_InternFromString("row");
|
|
46
|
+
column = PyUnicode_InternFromString("column");
|
|
47
|
+
column_reverse = PyUnicode_InternFromString("column-reverse");
|
|
48
|
+
row_reverse = PyUnicode_InternFromString("row-reverse");
|
|
49
|
+
wrap = PyUnicode_InternFromString("wrap");
|
|
50
|
+
wrap_reverse = PyUnicode_InternFromString("wrap-reverse");
|
|
51
|
+
nowrap = PyUnicode_InternFromString("nowrap");
|
|
52
|
+
flex_start = PyUnicode_InternFromString("flex-start");
|
|
53
|
+
flex_end = PyUnicode_InternFromString("flex-end");
|
|
54
|
+
center = PyUnicode_InternFromString("center");
|
|
55
|
+
space_between = PyUnicode_InternFromString("space-between");
|
|
56
|
+
space_around = PyUnicode_InternFromString("space-around");
|
|
57
|
+
space_evenly = PyUnicode_InternFromString("space-evenly");
|
|
58
|
+
stretch = PyUnicode_InternFromString("stretch");
|
|
59
|
+
baseline = PyUnicode_InternFromString("baseline");
|
|
60
|
+
auto_ = PyUnicode_InternFromString("auto");
|
|
61
|
+
visible = PyUnicode_InternFromString("visible");
|
|
62
|
+
hidden = PyUnicode_InternFromString("hidden");
|
|
63
|
+
scroll = PyUnicode_InternFromString("scroll");
|
|
64
|
+
relative = PyUnicode_InternFromString("relative");
|
|
65
|
+
absolute = PyUnicode_InternFromString("absolute");
|
|
66
|
+
static_ = PyUnicode_InternFromString("static");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
15
70
|
// Unified context for all node callbacks + user context.
|
|
16
71
|
// Stored in node->getContext() as a single allocation.
|
|
17
72
|
struct NodeContext {
|
|
@@ -37,6 +92,27 @@ static std::unordered_set<yoga::Node*> nanobindManagedNodes;
|
|
|
37
92
|
static std::unordered_set<NodeContext*> allNodeContexts;
|
|
38
93
|
static std::unordered_set<CloneContext*> allCloneContexts;
|
|
39
94
|
|
|
95
|
+
// Per-node field cache for configure_node_fast.
|
|
96
|
+
// Caches previous PyObject* pointers and float values per yoga::Node.
|
|
97
|
+
// On each configure call, fields are compared by pointer identity first;
|
|
98
|
+
// yoga setters are only called when the value actually changed.
|
|
99
|
+
enum CacheSlot : int {
|
|
100
|
+
CS_WIDTH=0, CS_HEIGHT, CS_MIN_WIDTH, CS_MIN_HEIGHT,
|
|
101
|
+
CS_MAX_WIDTH, CS_MAX_HEIGHT,
|
|
102
|
+
CS_FLEX_GROW, CS_FLEX_SHRINK, CS_FLEX_BASIS,
|
|
103
|
+
CS_FLEX_DIRECTION, CS_FLEX_WRAP,
|
|
104
|
+
CS_JUSTIFY_CONTENT, CS_ALIGN_ITEMS, CS_ALIGN_SELF,
|
|
105
|
+
CS_GAP, CS_OVERFLOW, CS_POSITION_TYPE,
|
|
106
|
+
CS_MARGIN, CS_MARGIN_TOP, CS_MARGIN_RIGHT, CS_MARGIN_BOTTOM, CS_MARGIN_LEFT,
|
|
107
|
+
CS_POS_TOP, CS_POS_RIGHT, CS_POS_BOTTOM, CS_POS_LEFT,
|
|
108
|
+
CS_COUNT // = 26
|
|
109
|
+
};
|
|
110
|
+
struct NodeCache {
|
|
111
|
+
PyObject* ptrs[CS_COUNT] = {};
|
|
112
|
+
float padding[4] = {-1.f, -1.f, -1.f, -1.f}; // TRBL
|
|
113
|
+
};
|
|
114
|
+
static std::unordered_map<yoga::Node*, NodeCache> node_cache_;
|
|
115
|
+
|
|
40
116
|
// --- NodeContext helpers ---
|
|
41
117
|
|
|
42
118
|
static NodeContext* getNodeContext(yoga::Node* node) {
|
|
@@ -74,6 +150,9 @@ static void cleanupCloneContext(yoga::Config& config) {
|
|
|
74
150
|
// --- Safe free functions ---
|
|
75
151
|
|
|
76
152
|
static void safeNodeFree(yoga::Node& self) {
|
|
153
|
+
// Evict from configure_node_fast cache
|
|
154
|
+
node_cache_.erase(&self);
|
|
155
|
+
|
|
77
156
|
// Clean up Python context
|
|
78
157
|
cleanupNodeContext(&self);
|
|
79
158
|
|
|
@@ -227,6 +306,9 @@ static void yogaDirtiedCallback(YGNodeConstRef node) {
|
|
|
227
306
|
NB_MODULE(yoga, m) {
|
|
228
307
|
m.doc() = "Python binding for Facebook Yoga layout engine (using nanobind)";
|
|
229
308
|
|
|
309
|
+
// Initialize interned strings for fast pointer comparison
|
|
310
|
+
interned::init();
|
|
311
|
+
|
|
230
312
|
nb::enum_<YGDirection>(m, "Direction")
|
|
231
313
|
.value("Inherit", YGDirectionInherit)
|
|
232
314
|
.value("LTR", YGDirectionLTR)
|
|
@@ -841,17 +923,27 @@ NB_MODULE(yoga, m) {
|
|
|
841
923
|
.def("remove_child", [](yoga::Node& self, yoga::Node& child) { YGNodeRemoveChild(&self, &child); }, nb::arg("child"))
|
|
842
924
|
.def("remove_all_children", [](yoga::Node& self) { YGNodeRemoveAllChildren(&self); })
|
|
843
925
|
.def("set_children", [](yoga::Node& self, const std::vector<yoga::Node*>& children) {
|
|
926
|
+
const size_t currentCount = self.getChildCount();
|
|
927
|
+
if (currentCount == children.size()) {
|
|
928
|
+
bool identical = true;
|
|
929
|
+
for (size_t i = 0; i < currentCount; i++) {
|
|
930
|
+
if (self.getChild(i) != children[i]) {
|
|
931
|
+
identical = false;
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (identical) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
std::unordered_set<yoga::Node*> nextChildren(children.begin(), children.end());
|
|
941
|
+
|
|
844
942
|
// Clear owner for old children that are owned by self and not in the new list
|
|
845
|
-
for (size_t i = 0; i <
|
|
943
|
+
for (size_t i = 0; i < currentCount; i++) {
|
|
846
944
|
auto* oldChild = self.getChild(i);
|
|
847
|
-
if (oldChild->getOwner() == &self) {
|
|
848
|
-
|
|
849
|
-
for (auto* newChild : children) {
|
|
850
|
-
if (newChild == oldChild) { inNewList = true; break; }
|
|
851
|
-
}
|
|
852
|
-
if (!inNewList) {
|
|
853
|
-
oldChild->setOwner(nullptr);
|
|
854
|
-
}
|
|
945
|
+
if (oldChild->getOwner() == &self && !nextChildren.contains(oldChild)) {
|
|
946
|
+
oldChild->setOwner(nullptr);
|
|
855
947
|
}
|
|
856
948
|
}
|
|
857
949
|
self.setChildren(children);
|
|
@@ -997,6 +1089,562 @@ NB_MODULE(yoga, m) {
|
|
|
997
1089
|
.def("set_flex_basis_stretch", [](yoga::Node& self) { YGNodeStyleSetFlexBasisStretch(&self); })
|
|
998
1090
|
.def("_node_id", [](yoga::Node& self) -> uintptr_t { return reinterpret_cast<uintptr_t>(&self); });
|
|
999
1091
|
|
|
1092
|
+
// ── configure_node_fast: bulk yoga configuration in one C++ call ──
|
|
1093
|
+
//
|
|
1094
|
+
// Replaces ~6 individual Python→C++ calls + 24 Python None-checks with
|
|
1095
|
+
// a single call. All dimension parsing, enum mapping, and None handling
|
|
1096
|
+
// happens in C++. Field-level caching skips unchanged properties.
|
|
1097
|
+
//
|
|
1098
|
+
// Dimension args: None=auto/skip, int/float=point, str "50%"=percent, str "auto"=auto
|
|
1099
|
+
// Enum args: None=skip, str=mapped to yoga enum
|
|
1100
|
+
// Numeric args: None=skip, int/float=applied directly
|
|
1101
|
+
// Padding: always applied (float, includes caller's border offset)
|
|
1102
|
+
|
|
1103
|
+
m.def("configure_node_fast", [](
|
|
1104
|
+
yoga::Node& node,
|
|
1105
|
+
// Dimensions
|
|
1106
|
+
nb::object width, nb::object height,
|
|
1107
|
+
nb::object min_width, nb::object min_height,
|
|
1108
|
+
nb::object max_width, nb::object max_height,
|
|
1109
|
+
// Flex
|
|
1110
|
+
nb::object flex_grow, nb::object flex_shrink, nb::object flex_basis,
|
|
1111
|
+
// Enums (None = skip)
|
|
1112
|
+
nb::object flex_direction, nb::object flex_wrap,
|
|
1113
|
+
nb::object justify_content, nb::object align_items, nb::object align_self,
|
|
1114
|
+
// Gap, overflow, position, display
|
|
1115
|
+
nb::object gap, nb::object overflow, nb::object position_type,
|
|
1116
|
+
// Padding (always set, includes border offset from caller)
|
|
1117
|
+
float padding_top, float padding_right,
|
|
1118
|
+
float padding_bottom, float padding_left,
|
|
1119
|
+
// Margin
|
|
1120
|
+
nb::object margin, nb::object margin_top, nb::object margin_right,
|
|
1121
|
+
nb::object margin_bottom, nb::object margin_left,
|
|
1122
|
+
// Position edges
|
|
1123
|
+
nb::object pos_top, nb::object pos_right,
|
|
1124
|
+
nb::object pos_bottom, nb::object pos_left
|
|
1125
|
+
) {
|
|
1126
|
+
// ── Helper lambdas ──
|
|
1127
|
+
|
|
1128
|
+
// Parse dimension: returns (value, unit) where unit is 0=auto, 1=point, 2=percent
|
|
1129
|
+
auto parse_dim = [](nb::handle obj, float& out_val) -> int {
|
|
1130
|
+
if (obj.is_none()) return 0; // auto/unset
|
|
1131
|
+
if (nb::isinstance<nb::int_>(obj) || nb::isinstance<nb::float_>(obj)) {
|
|
1132
|
+
out_val = nb::cast<float>(obj);
|
|
1133
|
+
return 1; // point
|
|
1134
|
+
}
|
|
1135
|
+
if (nb::isinstance<nb::str>(obj)) {
|
|
1136
|
+
std::string s = nb::cast<std::string>(obj);
|
|
1137
|
+
if (s == "auto") return 0;
|
|
1138
|
+
try {
|
|
1139
|
+
if (!s.empty() && s.back() == '%') {
|
|
1140
|
+
s.pop_back();
|
|
1141
|
+
out_val = std::stof(s);
|
|
1142
|
+
return 2; // percent
|
|
1143
|
+
}
|
|
1144
|
+
out_val = std::stof(s);
|
|
1145
|
+
return 1; // point
|
|
1146
|
+
} catch (const std::exception&) {
|
|
1147
|
+
return 0; // malformed string → treat as auto/unset
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return 0;
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
// ── Field-level cache: skip unchanged yoga setter calls ──
|
|
1154
|
+
auto& cache = node_cache_[&node];
|
|
1155
|
+
|
|
1156
|
+
// Macro: skip if PyObject* identity unchanged (most common case for
|
|
1157
|
+
// interned strings and small ints which Python caches globally)
|
|
1158
|
+
#define CACHE_CHECK(slot, obj) \
|
|
1159
|
+
if (obj.ptr() == cache.ptrs[slot]) goto skip_##slot; \
|
|
1160
|
+
cache.ptrs[slot] = obj.ptr();
|
|
1161
|
+
#define CACHE_SKIP(slot) skip_##slot: (void)0;
|
|
1162
|
+
|
|
1163
|
+
// ── Dimensions (width/height: None → auto) ──
|
|
1164
|
+
float val;
|
|
1165
|
+
int unit;
|
|
1166
|
+
|
|
1167
|
+
CACHE_CHECK(CS_WIDTH, width)
|
|
1168
|
+
unit = parse_dim(width, val);
|
|
1169
|
+
if (unit == 1) YGNodeStyleSetWidth(&node, val);
|
|
1170
|
+
else if (unit == 2) YGNodeStyleSetWidthPercent(&node, val);
|
|
1171
|
+
else YGNodeStyleSetWidthAuto(&node);
|
|
1172
|
+
CACHE_SKIP(CS_WIDTH)
|
|
1173
|
+
|
|
1174
|
+
CACHE_CHECK(CS_HEIGHT, height)
|
|
1175
|
+
unit = parse_dim(height, val);
|
|
1176
|
+
if (unit == 1) YGNodeStyleSetHeight(&node, val);
|
|
1177
|
+
else if (unit == 2) YGNodeStyleSetHeightPercent(&node, val);
|
|
1178
|
+
else YGNodeStyleSetHeightAuto(&node);
|
|
1179
|
+
CACHE_SKIP(CS_HEIGHT)
|
|
1180
|
+
|
|
1181
|
+
// min/max: None → skip (don't reset)
|
|
1182
|
+
CACHE_CHECK(CS_MIN_WIDTH, min_width)
|
|
1183
|
+
unit = parse_dim(min_width, val);
|
|
1184
|
+
if (unit == 1) YGNodeStyleSetMinWidth(&node, val);
|
|
1185
|
+
else if (unit == 2) YGNodeStyleSetMinWidthPercent(&node, val);
|
|
1186
|
+
CACHE_SKIP(CS_MIN_WIDTH)
|
|
1187
|
+
|
|
1188
|
+
CACHE_CHECK(CS_MIN_HEIGHT, min_height)
|
|
1189
|
+
unit = parse_dim(min_height, val);
|
|
1190
|
+
if (unit == 1) YGNodeStyleSetMinHeight(&node, val);
|
|
1191
|
+
else if (unit == 2) YGNodeStyleSetMinHeightPercent(&node, val);
|
|
1192
|
+
CACHE_SKIP(CS_MIN_HEIGHT)
|
|
1193
|
+
|
|
1194
|
+
CACHE_CHECK(CS_MAX_WIDTH, max_width)
|
|
1195
|
+
unit = parse_dim(max_width, val);
|
|
1196
|
+
if (unit == 1) YGNodeStyleSetMaxWidth(&node, val);
|
|
1197
|
+
else if (unit == 2) YGNodeStyleSetMaxWidthPercent(&node, val);
|
|
1198
|
+
CACHE_SKIP(CS_MAX_WIDTH)
|
|
1199
|
+
|
|
1200
|
+
CACHE_CHECK(CS_MAX_HEIGHT, max_height)
|
|
1201
|
+
unit = parse_dim(max_height, val);
|
|
1202
|
+
if (unit == 1) YGNodeStyleSetMaxHeight(&node, val);
|
|
1203
|
+
else if (unit == 2) YGNodeStyleSetMaxHeightPercent(&node, val);
|
|
1204
|
+
CACHE_SKIP(CS_MAX_HEIGHT)
|
|
1205
|
+
|
|
1206
|
+
// ── Flex ──
|
|
1207
|
+
CACHE_CHECK(CS_FLEX_GROW, flex_grow)
|
|
1208
|
+
if (!flex_grow.is_none())
|
|
1209
|
+
YGNodeStyleSetFlexGrow(&node, nb::cast<float>(flex_grow));
|
|
1210
|
+
CACHE_SKIP(CS_FLEX_GROW)
|
|
1211
|
+
|
|
1212
|
+
CACHE_CHECK(CS_FLEX_SHRINK, flex_shrink)
|
|
1213
|
+
if (!flex_shrink.is_none())
|
|
1214
|
+
YGNodeStyleSetFlexShrink(&node, nb::cast<float>(flex_shrink));
|
|
1215
|
+
CACHE_SKIP(CS_FLEX_SHRINK)
|
|
1216
|
+
|
|
1217
|
+
CACHE_CHECK(CS_FLEX_BASIS, flex_basis)
|
|
1218
|
+
if (!flex_basis.is_none()) {
|
|
1219
|
+
unit = parse_dim(flex_basis, val);
|
|
1220
|
+
if (unit == 1) YGNodeStyleSetFlexBasis(&node, val);
|
|
1221
|
+
else if (unit == 2) YGNodeStyleSetFlexBasisPercent(&node, val);
|
|
1222
|
+
else YGNodeStyleSetFlexBasisAuto(&node);
|
|
1223
|
+
}
|
|
1224
|
+
CACHE_SKIP(CS_FLEX_BASIS)
|
|
1225
|
+
|
|
1226
|
+
// ── Enum props (interned pointer compare first, string fallback) ──
|
|
1227
|
+
CACHE_CHECK(CS_FLEX_DIRECTION, flex_direction)
|
|
1228
|
+
if (!flex_direction.is_none() && nb::isinstance<nb::str>(flex_direction)) {
|
|
1229
|
+
PyObject* p = flex_direction.ptr();
|
|
1230
|
+
YGFlexDirection fd = YGFlexDirectionColumn;
|
|
1231
|
+
if (p == interned::row) fd = YGFlexDirectionRow;
|
|
1232
|
+
else if (p == interned::column_reverse) fd = YGFlexDirectionColumnReverse;
|
|
1233
|
+
else if (p == interned::row_reverse) fd = YGFlexDirectionRowReverse;
|
|
1234
|
+
else if (p != interned::column) {
|
|
1235
|
+
// Fallback: non-interned string
|
|
1236
|
+
std::string s = nb::cast<std::string>(flex_direction);
|
|
1237
|
+
if (s == "row") fd = YGFlexDirectionRow;
|
|
1238
|
+
else if (s == "column-reverse") fd = YGFlexDirectionColumnReverse;
|
|
1239
|
+
else if (s == "row-reverse") fd = YGFlexDirectionRowReverse;
|
|
1240
|
+
}
|
|
1241
|
+
YGNodeStyleSetFlexDirection(&node, fd);
|
|
1242
|
+
}
|
|
1243
|
+
CACHE_SKIP(CS_FLEX_DIRECTION)
|
|
1244
|
+
|
|
1245
|
+
CACHE_CHECK(CS_FLEX_WRAP, flex_wrap)
|
|
1246
|
+
if (!flex_wrap.is_none() && nb::isinstance<nb::str>(flex_wrap)) {
|
|
1247
|
+
PyObject* p = flex_wrap.ptr();
|
|
1248
|
+
YGWrap w = YGWrapNoWrap;
|
|
1249
|
+
if (p == interned::wrap) w = YGWrapWrap;
|
|
1250
|
+
else if (p == interned::wrap_reverse) w = YGWrapWrapReverse;
|
|
1251
|
+
else if (p != interned::nowrap) {
|
|
1252
|
+
std::string s = nb::cast<std::string>(flex_wrap);
|
|
1253
|
+
if (s == "wrap") w = YGWrapWrap;
|
|
1254
|
+
else if (s == "wrap-reverse") w = YGWrapWrapReverse;
|
|
1255
|
+
}
|
|
1256
|
+
YGNodeStyleSetFlexWrap(&node, w);
|
|
1257
|
+
}
|
|
1258
|
+
CACHE_SKIP(CS_FLEX_WRAP)
|
|
1259
|
+
|
|
1260
|
+
CACHE_CHECK(CS_JUSTIFY_CONTENT, justify_content)
|
|
1261
|
+
if (!justify_content.is_none() && nb::isinstance<nb::str>(justify_content)) {
|
|
1262
|
+
PyObject* p = justify_content.ptr();
|
|
1263
|
+
YGJustify j = YGJustifyFlexStart;
|
|
1264
|
+
if (p == interned::flex_end) j = YGJustifyFlexEnd;
|
|
1265
|
+
else if (p == interned::center) j = YGJustifyCenter;
|
|
1266
|
+
else if (p == interned::space_between) j = YGJustifySpaceBetween;
|
|
1267
|
+
else if (p == interned::space_around) j = YGJustifySpaceAround;
|
|
1268
|
+
else if (p == interned::space_evenly) j = YGJustifySpaceEvenly;
|
|
1269
|
+
else if (p != interned::flex_start) {
|
|
1270
|
+
std::string s = nb::cast<std::string>(justify_content);
|
|
1271
|
+
if (s == "flex-end") j = YGJustifyFlexEnd;
|
|
1272
|
+
else if (s == "center") j = YGJustifyCenter;
|
|
1273
|
+
else if (s == "space-between") j = YGJustifySpaceBetween;
|
|
1274
|
+
else if (s == "space-around") j = YGJustifySpaceAround;
|
|
1275
|
+
else if (s == "space-evenly") j = YGJustifySpaceEvenly;
|
|
1276
|
+
}
|
|
1277
|
+
YGNodeStyleSetJustifyContent(&node, j);
|
|
1278
|
+
}
|
|
1279
|
+
CACHE_SKIP(CS_JUSTIFY_CONTENT)
|
|
1280
|
+
|
|
1281
|
+
CACHE_CHECK(CS_ALIGN_ITEMS, align_items)
|
|
1282
|
+
if (!align_items.is_none() && nb::isinstance<nb::str>(align_items)) {
|
|
1283
|
+
PyObject* p = align_items.ptr();
|
|
1284
|
+
YGAlign a = YGAlignStretch;
|
|
1285
|
+
if (p == interned::flex_start) a = YGAlignFlexStart;
|
|
1286
|
+
else if (p == interned::flex_end) a = YGAlignFlexEnd;
|
|
1287
|
+
else if (p == interned::center) a = YGAlignCenter;
|
|
1288
|
+
else if (p == interned::baseline) a = YGAlignBaseline;
|
|
1289
|
+
else if (p == interned::auto_) a = YGAlignAuto;
|
|
1290
|
+
else if (p != interned::stretch) {
|
|
1291
|
+
std::string s = nb::cast<std::string>(align_items);
|
|
1292
|
+
if (s == "flex-start") a = YGAlignFlexStart;
|
|
1293
|
+
else if (s == "flex-end") a = YGAlignFlexEnd;
|
|
1294
|
+
else if (s == "center") a = YGAlignCenter;
|
|
1295
|
+
else if (s == "baseline") a = YGAlignBaseline;
|
|
1296
|
+
else if (s == "auto") a = YGAlignAuto;
|
|
1297
|
+
}
|
|
1298
|
+
YGNodeStyleSetAlignItems(&node, a);
|
|
1299
|
+
}
|
|
1300
|
+
CACHE_SKIP(CS_ALIGN_ITEMS)
|
|
1301
|
+
|
|
1302
|
+
CACHE_CHECK(CS_ALIGN_SELF, align_self)
|
|
1303
|
+
if (!align_self.is_none() && nb::isinstance<nb::str>(align_self)) {
|
|
1304
|
+
PyObject* p = align_self.ptr();
|
|
1305
|
+
YGAlign a = YGAlignAuto;
|
|
1306
|
+
if (p == interned::stretch) a = YGAlignStretch;
|
|
1307
|
+
else if (p == interned::flex_start) a = YGAlignFlexStart;
|
|
1308
|
+
else if (p == interned::flex_end) a = YGAlignFlexEnd;
|
|
1309
|
+
else if (p == interned::center) a = YGAlignCenter;
|
|
1310
|
+
else if (p == interned::baseline) a = YGAlignBaseline;
|
|
1311
|
+
else if (p != interned::auto_) {
|
|
1312
|
+
std::string s = nb::cast<std::string>(align_self);
|
|
1313
|
+
if (s == "stretch") a = YGAlignStretch;
|
|
1314
|
+
else if (s == "flex-start") a = YGAlignFlexStart;
|
|
1315
|
+
else if (s == "flex-end") a = YGAlignFlexEnd;
|
|
1316
|
+
else if (s == "center") a = YGAlignCenter;
|
|
1317
|
+
else if (s == "baseline") a = YGAlignBaseline;
|
|
1318
|
+
}
|
|
1319
|
+
YGNodeStyleSetAlignSelf(&node, a);
|
|
1320
|
+
}
|
|
1321
|
+
CACHE_SKIP(CS_ALIGN_SELF)
|
|
1322
|
+
|
|
1323
|
+
// ── Gap ──
|
|
1324
|
+
CACHE_CHECK(CS_GAP, gap)
|
|
1325
|
+
if (!gap.is_none())
|
|
1326
|
+
YGNodeStyleSetGap(&node, YGGutterAll, nb::cast<float>(gap));
|
|
1327
|
+
CACHE_SKIP(CS_GAP)
|
|
1328
|
+
|
|
1329
|
+
// ── Overflow ──
|
|
1330
|
+
CACHE_CHECK(CS_OVERFLOW, overflow)
|
|
1331
|
+
if (!overflow.is_none() && nb::isinstance<nb::str>(overflow)) {
|
|
1332
|
+
PyObject* p = overflow.ptr();
|
|
1333
|
+
YGOverflow o = YGOverflowVisible;
|
|
1334
|
+
if (p == interned::hidden) o = YGOverflowHidden;
|
|
1335
|
+
else if (p == interned::scroll) o = YGOverflowScroll;
|
|
1336
|
+
else if (p != interned::visible) {
|
|
1337
|
+
std::string s = nb::cast<std::string>(overflow);
|
|
1338
|
+
if (s == "hidden") o = YGOverflowHidden;
|
|
1339
|
+
else if (s == "scroll") o = YGOverflowScroll;
|
|
1340
|
+
}
|
|
1341
|
+
YGNodeStyleSetOverflow(&node, o);
|
|
1342
|
+
}
|
|
1343
|
+
CACHE_SKIP(CS_OVERFLOW)
|
|
1344
|
+
|
|
1345
|
+
// ── Position type ──
|
|
1346
|
+
CACHE_CHECK(CS_POSITION_TYPE, position_type)
|
|
1347
|
+
if (!position_type.is_none() && nb::isinstance<nb::str>(position_type)) {
|
|
1348
|
+
PyObject* p = position_type.ptr();
|
|
1349
|
+
YGPositionType pt = YGPositionTypeRelative;
|
|
1350
|
+
if (p == interned::absolute) pt = YGPositionTypeAbsolute;
|
|
1351
|
+
else if (p == interned::static_) pt = YGPositionTypeStatic;
|
|
1352
|
+
else if (p != interned::relative) {
|
|
1353
|
+
std::string s = nb::cast<std::string>(position_type);
|
|
1354
|
+
if (s == "absolute") pt = YGPositionTypeAbsolute;
|
|
1355
|
+
}
|
|
1356
|
+
YGNodeStyleSetPositionType(&node, pt);
|
|
1357
|
+
}
|
|
1358
|
+
CACHE_SKIP(CS_POSITION_TYPE)
|
|
1359
|
+
|
|
1360
|
+
// ── Padding (always set, caller includes border offset) ──
|
|
1361
|
+
if (padding_top != cache.padding[0]) {
|
|
1362
|
+
cache.padding[0] = padding_top;
|
|
1363
|
+
YGNodeStyleSetPadding(&node, YGEdgeTop, padding_top);
|
|
1364
|
+
}
|
|
1365
|
+
if (padding_right != cache.padding[1]) {
|
|
1366
|
+
cache.padding[1] = padding_right;
|
|
1367
|
+
YGNodeStyleSetPadding(&node, YGEdgeRight, padding_right);
|
|
1368
|
+
}
|
|
1369
|
+
if (padding_bottom != cache.padding[2]) {
|
|
1370
|
+
cache.padding[2] = padding_bottom;
|
|
1371
|
+
YGNodeStyleSetPadding(&node, YGEdgeBottom, padding_bottom);
|
|
1372
|
+
}
|
|
1373
|
+
if (padding_left != cache.padding[3]) {
|
|
1374
|
+
cache.padding[3] = padding_left;
|
|
1375
|
+
YGNodeStyleSetPadding(&node, YGEdgeLeft, padding_left);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// ── Margin ──
|
|
1379
|
+
CACHE_CHECK(CS_MARGIN, margin)
|
|
1380
|
+
if (!margin.is_none())
|
|
1381
|
+
YGNodeStyleSetMargin(&node, YGEdgeAll, nb::cast<float>(margin));
|
|
1382
|
+
CACHE_SKIP(CS_MARGIN)
|
|
1383
|
+
|
|
1384
|
+
CACHE_CHECK(CS_MARGIN_TOP, margin_top)
|
|
1385
|
+
if (!margin_top.is_none())
|
|
1386
|
+
YGNodeStyleSetMargin(&node, YGEdgeTop, nb::cast<float>(margin_top));
|
|
1387
|
+
CACHE_SKIP(CS_MARGIN_TOP)
|
|
1388
|
+
|
|
1389
|
+
CACHE_CHECK(CS_MARGIN_RIGHT, margin_right)
|
|
1390
|
+
if (!margin_right.is_none())
|
|
1391
|
+
YGNodeStyleSetMargin(&node, YGEdgeRight, nb::cast<float>(margin_right));
|
|
1392
|
+
CACHE_SKIP(CS_MARGIN_RIGHT)
|
|
1393
|
+
|
|
1394
|
+
CACHE_CHECK(CS_MARGIN_BOTTOM, margin_bottom)
|
|
1395
|
+
if (!margin_bottom.is_none())
|
|
1396
|
+
YGNodeStyleSetMargin(&node, YGEdgeBottom, nb::cast<float>(margin_bottom));
|
|
1397
|
+
CACHE_SKIP(CS_MARGIN_BOTTOM)
|
|
1398
|
+
|
|
1399
|
+
CACHE_CHECK(CS_MARGIN_LEFT, margin_left)
|
|
1400
|
+
if (!margin_left.is_none())
|
|
1401
|
+
YGNodeStyleSetMargin(&node, YGEdgeLeft, nb::cast<float>(margin_left));
|
|
1402
|
+
CACHE_SKIP(CS_MARGIN_LEFT)
|
|
1403
|
+
|
|
1404
|
+
// ── Position edges ──
|
|
1405
|
+
CACHE_CHECK(CS_POS_TOP, pos_top)
|
|
1406
|
+
unit = parse_dim(pos_top, val);
|
|
1407
|
+
if (unit == 1) YGNodeStyleSetPosition(&node, YGEdgeTop, val);
|
|
1408
|
+
else if (unit == 2) YGNodeStyleSetPositionPercent(&node, YGEdgeTop, val);
|
|
1409
|
+
CACHE_SKIP(CS_POS_TOP)
|
|
1410
|
+
|
|
1411
|
+
CACHE_CHECK(CS_POS_RIGHT, pos_right)
|
|
1412
|
+
unit = parse_dim(pos_right, val);
|
|
1413
|
+
if (unit == 1) YGNodeStyleSetPosition(&node, YGEdgeRight, val);
|
|
1414
|
+
else if (unit == 2) YGNodeStyleSetPositionPercent(&node, YGEdgeRight, val);
|
|
1415
|
+
CACHE_SKIP(CS_POS_RIGHT)
|
|
1416
|
+
|
|
1417
|
+
CACHE_CHECK(CS_POS_BOTTOM, pos_bottom)
|
|
1418
|
+
unit = parse_dim(pos_bottom, val);
|
|
1419
|
+
if (unit == 1) YGNodeStyleSetPosition(&node, YGEdgeBottom, val);
|
|
1420
|
+
else if (unit == 2) YGNodeStyleSetPositionPercent(&node, YGEdgeBottom, val);
|
|
1421
|
+
CACHE_SKIP(CS_POS_BOTTOM)
|
|
1422
|
+
|
|
1423
|
+
CACHE_CHECK(CS_POS_LEFT, pos_left)
|
|
1424
|
+
unit = parse_dim(pos_left, val);
|
|
1425
|
+
if (unit == 1) YGNodeStyleSetPosition(&node, YGEdgeLeft, val);
|
|
1426
|
+
else if (unit == 2) YGNodeStyleSetPositionPercent(&node, YGEdgeLeft, val);
|
|
1427
|
+
CACHE_SKIP(CS_POS_LEFT)
|
|
1428
|
+
|
|
1429
|
+
#undef CACHE_CHECK
|
|
1430
|
+
#undef CACHE_SKIP
|
|
1431
|
+
},
|
|
1432
|
+
nb::arg("node"),
|
|
1433
|
+
nb::arg("width") = nb::none(), nb::arg("height") = nb::none(),
|
|
1434
|
+
nb::arg("min_width") = nb::none(), nb::arg("min_height") = nb::none(),
|
|
1435
|
+
nb::arg("max_width") = nb::none(), nb::arg("max_height") = nb::none(),
|
|
1436
|
+
nb::arg("flex_grow") = nb::none(), nb::arg("flex_shrink") = nb::none(),
|
|
1437
|
+
nb::arg("flex_basis") = nb::none(),
|
|
1438
|
+
nb::arg("flex_direction") = nb::none(), nb::arg("flex_wrap") = nb::none(),
|
|
1439
|
+
nb::arg("justify_content") = nb::none(), nb::arg("align_items") = nb::none(),
|
|
1440
|
+
nb::arg("align_self") = nb::none(),
|
|
1441
|
+
nb::arg("gap") = nb::none(), nb::arg("overflow") = nb::none(),
|
|
1442
|
+
nb::arg("position_type") = nb::none(),
|
|
1443
|
+
nb::arg("padding_top") = 0.0f, nb::arg("padding_right") = 0.0f,
|
|
1444
|
+
nb::arg("padding_bottom") = 0.0f, nb::arg("padding_left") = 0.0f,
|
|
1445
|
+
nb::arg("margin") = nb::none(), nb::arg("margin_top") = nb::none(),
|
|
1446
|
+
nb::arg("margin_right") = nb::none(), nb::arg("margin_bottom") = nb::none(),
|
|
1447
|
+
nb::arg("margin_left") = nb::none(),
|
|
1448
|
+
nb::arg("pos_top") = nb::none(), nb::arg("pos_right") = nb::none(),
|
|
1449
|
+
nb::arg("pos_bottom") = nb::none(), nb::arg("pos_left") = nb::none(),
|
|
1450
|
+
"Configure all layout properties on a yoga node in a single C++ call.");
|
|
1451
|
+
|
|
1452
|
+
// Evict a node from the configure_node_fast cache (called on node destruction).
|
|
1453
|
+
m.def("clear_node_cache", [](yoga::Node& node) {
|
|
1454
|
+
node_cache_.erase(&node);
|
|
1455
|
+
}, nb::arg("node"),
|
|
1456
|
+
"Remove a node from the configure_node_fast field cache.");
|
|
1457
|
+
|
|
1458
|
+
// ── apply_layout_tree: walk a widget tree, apply yoga layout, collect deltas ──
|
|
1459
|
+
//
|
|
1460
|
+
// Accelerator for widget frameworks that maintain a tree of Python objects
|
|
1461
|
+
// with yoga nodes attached. Walks the tree in C++, reads yoga layout
|
|
1462
|
+
// results, writes absolute geometry back to Python __slots__, clears dirty
|
|
1463
|
+
// flags, and returns a list of "repaint facts" for nodes whose geometry
|
|
1464
|
+
// changed or were dirty.
|
|
1465
|
+
//
|
|
1466
|
+
// The `offsets` dict maps slot names to byte offsets (from tp_basicsize or
|
|
1467
|
+
// __slots__) so the function can read/write arbitrary Python objects without
|
|
1468
|
+
// knowing their class. Required keys:
|
|
1469
|
+
// _x, _y, _layout_width, _layout_height — int slots for absolute geometry
|
|
1470
|
+
// _dirty, _subtree_dirty — bool slots for dirty flags
|
|
1471
|
+
// _children — list slot for tree traversal
|
|
1472
|
+
// _parent — slot for parent reference
|
|
1473
|
+
// _yoga_node — slot holding the yoga.Node
|
|
1474
|
+
// _on_size_change — optional callable(width, height)
|
|
1475
|
+
//
|
|
1476
|
+
// Returns list of tuples:
|
|
1477
|
+
// (node, parent_id, has_children, was_dirty,
|
|
1478
|
+
// old_x, old_y, old_w, old_h, new_x, new_y, new_w, new_h)
|
|
1479
|
+
m.def("apply_layout_tree", [](nb::object root, nb::dict offsets_dict, int origin_x, int origin_y) {
|
|
1480
|
+
struct Offsets {
|
|
1481
|
+
Py_ssize_t x, y, layout_width, layout_height;
|
|
1482
|
+
Py_ssize_t dirty, subtree_dirty, children, parent, yoga_node;
|
|
1483
|
+
Py_ssize_t on_size_change;
|
|
1484
|
+
} off;
|
|
1485
|
+
auto get_off = [&](const char* name) -> Py_ssize_t {
|
|
1486
|
+
return nb::cast<Py_ssize_t>(offsets_dict[name]);
|
|
1487
|
+
};
|
|
1488
|
+
off.x = get_off("_x");
|
|
1489
|
+
off.y = get_off("_y");
|
|
1490
|
+
off.layout_width = get_off("_layout_width");
|
|
1491
|
+
off.layout_height = get_off("_layout_height");
|
|
1492
|
+
off.dirty = get_off("_dirty");
|
|
1493
|
+
off.subtree_dirty = get_off("_subtree_dirty");
|
|
1494
|
+
off.children = get_off("_children");
|
|
1495
|
+
off.parent = get_off("_parent");
|
|
1496
|
+
off.yoga_node = get_off("_yoga_node");
|
|
1497
|
+
off.on_size_change = get_off("_on_size_change");
|
|
1498
|
+
|
|
1499
|
+
auto read_slot = [](PyObject* obj, Py_ssize_t offset) -> PyObject* {
|
|
1500
|
+
return offset < 0 ? nullptr : *(PyObject**)((char*)obj + offset);
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
auto write_slot = [](PyObject* obj, Py_ssize_t offset, PyObject* new_val) {
|
|
1504
|
+
PyObject** slot = (PyObject**)((char*)obj + offset);
|
|
1505
|
+
PyObject* old = *slot;
|
|
1506
|
+
Py_INCREF(new_val);
|
|
1507
|
+
*slot = new_val;
|
|
1508
|
+
Py_XDECREF(old);
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
auto read_long_slot = [&](PyObject* obj, Py_ssize_t offset) -> int {
|
|
1512
|
+
if (offset < 0) return 0;
|
|
1513
|
+
PyObject* value = read_slot(obj, offset);
|
|
1514
|
+
if (!value || value == Py_None) return 0;
|
|
1515
|
+
long result = PyLong_AsLong(value);
|
|
1516
|
+
if (PyErr_Occurred()) {
|
|
1517
|
+
PyErr_Clear();
|
|
1518
|
+
return 0;
|
|
1519
|
+
}
|
|
1520
|
+
return static_cast<int>(result);
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
nb::list facts;
|
|
1524
|
+
struct Walker {
|
|
1525
|
+
Offsets& off;
|
|
1526
|
+
decltype(read_slot)& read;
|
|
1527
|
+
decltype(write_slot)& write;
|
|
1528
|
+
decltype(read_long_slot)& read_long;
|
|
1529
|
+
nb::list& facts;
|
|
1530
|
+
|
|
1531
|
+
void walk(PyObject* node, int parent_x, int parent_y) {
|
|
1532
|
+
PyObject* yoga_py = read(node, off.yoga_node);
|
|
1533
|
+
if (!yoga_py || yoga_py == Py_None) return;
|
|
1534
|
+
|
|
1535
|
+
yoga::Node* yoga_node;
|
|
1536
|
+
try {
|
|
1537
|
+
yoga_node = nb::cast<yoga::Node*>(nb::handle(yoga_py));
|
|
1538
|
+
} catch (...) {
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
int lx = static_cast<int>(YGNodeLayoutGetLeft(yoga_node));
|
|
1543
|
+
int ly = static_cast<int>(YGNodeLayoutGetTop(yoga_node));
|
|
1544
|
+
int lw = static_cast<int>(YGNodeLayoutGetWidth(yoga_node));
|
|
1545
|
+
int lh = static_cast<int>(YGNodeLayoutGetHeight(yoga_node));
|
|
1546
|
+
|
|
1547
|
+
const int old_x = read_long(node, off.x);
|
|
1548
|
+
const int old_y = read_long(node, off.y);
|
|
1549
|
+
const int old_w = read_long(node, off.layout_width);
|
|
1550
|
+
const int old_h = read_long(node, off.layout_height);
|
|
1551
|
+
const bool was_dirty = off.dirty >= 0 && read(node, off.dirty) == Py_True;
|
|
1552
|
+
|
|
1553
|
+
PyObject* old_w_obj = read(node, off.layout_width);
|
|
1554
|
+
PyObject* old_h_obj = read(node, off.layout_height);
|
|
1555
|
+
Py_XINCREF(old_w_obj);
|
|
1556
|
+
Py_XINCREF(old_h_obj);
|
|
1557
|
+
|
|
1558
|
+
const int abs_x = lx + parent_x;
|
|
1559
|
+
const int abs_y = ly + parent_y;
|
|
1560
|
+
const bool geom_changed = old_x != abs_x || old_y != abs_y || old_w != lw || old_h != lh;
|
|
1561
|
+
|
|
1562
|
+
if (geom_changed) {
|
|
1563
|
+
PyObject* py_x = PyLong_FromLong(abs_x);
|
|
1564
|
+
PyObject* py_y = PyLong_FromLong(abs_y);
|
|
1565
|
+
PyObject* py_w = PyLong_FromLong(lw);
|
|
1566
|
+
PyObject* py_h = PyLong_FromLong(lh);
|
|
1567
|
+
write(node, off.x, py_x);
|
|
1568
|
+
write(node, off.y, py_y);
|
|
1569
|
+
write(node, off.layout_width, py_w);
|
|
1570
|
+
write(node, off.layout_height, py_h);
|
|
1571
|
+
Py_DECREF(py_x);
|
|
1572
|
+
Py_DECREF(py_y);
|
|
1573
|
+
Py_DECREF(py_w);
|
|
1574
|
+
Py_DECREF(py_h);
|
|
1575
|
+
}
|
|
1576
|
+
if (was_dirty) write(node, off.dirty, Py_False);
|
|
1577
|
+
if (off.subtree_dirty >= 0 && read(node, off.subtree_dirty) == Py_True) {
|
|
1578
|
+
write(node, off.subtree_dirty, Py_False);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
PyObject* children = read(node, off.children);
|
|
1582
|
+
const bool has_children =
|
|
1583
|
+
children && PyList_Check(children) && PyList_GET_SIZE(children) > 0;
|
|
1584
|
+
if (was_dirty || geom_changed) {
|
|
1585
|
+
PyObject* parent = read(node, off.parent);
|
|
1586
|
+
facts.append(nb::make_tuple(
|
|
1587
|
+
nb::borrow<nb::object>(node),
|
|
1588
|
+
parent && parent != Py_None ? reinterpret_cast<uintptr_t>(parent) : 0,
|
|
1589
|
+
has_children,
|
|
1590
|
+
was_dirty,
|
|
1591
|
+
old_x,
|
|
1592
|
+
old_y,
|
|
1593
|
+
old_w,
|
|
1594
|
+
old_h,
|
|
1595
|
+
abs_x,
|
|
1596
|
+
abs_y,
|
|
1597
|
+
lw,
|
|
1598
|
+
lh
|
|
1599
|
+
));
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
PyObject* size_cb = off.on_size_change >= 0 ? read(node, off.on_size_change) : nullptr;
|
|
1603
|
+
if (size_cb && size_cb != Py_None) {
|
|
1604
|
+
const bool w_changed = !old_w_obj || PyLong_AsLong(old_w_obj) != lw;
|
|
1605
|
+
const bool h_changed = !old_h_obj || PyLong_AsLong(old_h_obj) != lh;
|
|
1606
|
+
if (w_changed || h_changed) {
|
|
1607
|
+
PyObject* result = PyObject_CallFunction(size_cb, "ii", lw, lh);
|
|
1608
|
+
if (result) Py_DECREF(result);
|
|
1609
|
+
else PyErr_Clear();
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
Py_XDECREF(old_w_obj);
|
|
1613
|
+
Py_XDECREF(old_h_obj);
|
|
1614
|
+
|
|
1615
|
+
if (has_children) {
|
|
1616
|
+
Py_ssize_t n = PyList_GET_SIZE(children);
|
|
1617
|
+
for (Py_ssize_t i = 0; i < n; ++i) {
|
|
1618
|
+
walk(PyList_GET_ITEM(children, i), abs_x, abs_y);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
Walker walker{off, read_slot, write_slot, read_long_slot, facts};
|
|
1625
|
+
walker.walk(root.ptr(), origin_x, origin_y);
|
|
1626
|
+
return facts;
|
|
1627
|
+
}, nb::arg("root"), nb::arg("offsets"), nb::arg("origin_x") = 0, nb::arg("origin_y") = 0,
|
|
1628
|
+
"Walk a widget tree in C++, apply yoga layout results to Python __slots__,\n"
|
|
1629
|
+
"clear dirty flags, and return repaint facts for changed nodes.\n\n"
|
|
1630
|
+
"The offsets dict maps slot names (_x, _y, _layout_width, _layout_height,\n"
|
|
1631
|
+
"_dirty, _subtree_dirty, _children, _parent, _yoga_node, _on_size_change)\n"
|
|
1632
|
+
"to byte offsets so arbitrary Python objects can be read/written by slot.");
|
|
1633
|
+
|
|
1634
|
+
// ── get_layout_batch: read all 4 layout results in one C++ call ──
|
|
1635
|
+
//
|
|
1636
|
+
// Returns (left, top, width, height) as integers, avoiding 4 separate
|
|
1637
|
+
// Python→C++ property getter round-trips.
|
|
1638
|
+
m.def("get_layout_batch", [](yoga::Node& node) -> nb::tuple {
|
|
1639
|
+
return nb::make_tuple(
|
|
1640
|
+
static_cast<int>(YGNodeLayoutGetLeft(&node)),
|
|
1641
|
+
static_cast<int>(YGNodeLayoutGetTop(&node)),
|
|
1642
|
+
static_cast<int>(YGNodeLayoutGetWidth(&node)),
|
|
1643
|
+
static_cast<int>(YGNodeLayoutGetHeight(&node))
|
|
1644
|
+
);
|
|
1645
|
+
}, nb::arg("node"),
|
|
1646
|
+
"Get (left, top, width, height) layout results as integers in one call.");
|
|
1647
|
+
|
|
1000
1648
|
// Clean up all Python-side resources during module teardown.
|
|
1001
1649
|
// Using a capsule ensures cleanup runs when the module dict is cleared,
|
|
1002
1650
|
// before nanobind's leak checker fires.
|
|
@@ -1017,5 +1665,6 @@ NB_MODULE(yoga, m) {
|
|
|
1017
1665
|
allCloneContexts.clear();
|
|
1018
1666
|
|
|
1019
1667
|
nanobindManagedNodes.clear();
|
|
1668
|
+
node_cache_.clear();
|
|
1020
1669
|
});
|
|
1021
1670
|
}
|
|
@@ -91,3 +91,18 @@ class TestTreeMutation:
|
|
|
91
91
|
|
|
92
92
|
root.free_recursive()
|
|
93
93
|
root_child0.free()
|
|
94
|
+
|
|
95
|
+
def test_set_children_same_order_is_noop(self):
|
|
96
|
+
config = Config()
|
|
97
|
+
root = Node(config)
|
|
98
|
+
root_child0 = Node(config)
|
|
99
|
+
root_child1 = Node(config)
|
|
100
|
+
|
|
101
|
+
root.set_children([root_child0, root_child1])
|
|
102
|
+
root.set_children([root_child0, root_child1])
|
|
103
|
+
|
|
104
|
+
assert get_children(root) == [root_child0, root_child1]
|
|
105
|
+
assert root_child0.owner is root
|
|
106
|
+
assert root_child1.owner is root
|
|
107
|
+
|
|
108
|
+
root.free_recursive()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|