pythonnative 0.20.0__py3-none-any.whl → 0.22.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.
- pythonnative/__init__.py +14 -3
- pythonnative/animated.py +420 -135
- pythonnative/cli/pn.py +450 -956
- pythonnative/components.py +519 -235
- pythonnative/events.py +210 -0
- pythonnative/gestures.py +875 -0
- pythonnative/layout.py +463 -149
- pythonnative/mutations.py +130 -0
- pythonnative/native_views/__init__.py +161 -97
- pythonnative/native_views/android.py +1050 -1124
- pythonnative/native_views/base.py +108 -18
- pythonnative/native_views/desktop.py +460 -417
- pythonnative/native_views/ios.py +1918 -1916
- pythonnative/project/__init__.py +68 -0
- pythonnative/project/android.py +504 -0
- pythonnative/project/builder.py +555 -0
- pythonnative/project/config.py +642 -0
- pythonnative/project/doctor.py +233 -0
- pythonnative/project/icons.py +247 -0
- pythonnative/project/ios.py +344 -0
- pythonnative/project/permissions.py +343 -0
- pythonnative/project/runtime_assets.py +272 -0
- pythonnative/reconciler.py +540 -470
- pythonnative/screen.py +5 -2
- pythonnative/sdk/_components.py +2 -2
- pythonnative/templates/android_template/app/build.gradle +2 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/METADATA +10 -2
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/RECORD +32 -21
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -129
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/top_level.txt +0 -0
pythonnative/layout.py
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
"""Pure-Python flexbox layout engine.
|
|
2
2
|
|
|
3
|
-
Computes
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Computes positions and sizes for every node in a layout tree based on
|
|
4
|
+
CSS flexbox-inspired style properties. Inspired by Facebook's Yoga and
|
|
5
|
+
React Native's layout system, but implemented entirely in Python so
|
|
6
|
+
PythonNative does not depend on a native layout library.
|
|
7
7
|
|
|
8
8
|
The engine is invoked by the reconciler after each commit pass:
|
|
9
9
|
|
|
10
|
-
1. The reconciler
|
|
11
|
-
[`LayoutNode`][pythonnative.layout.LayoutNode] tree
|
|
12
|
-
|
|
13
|
-
``measure`` callback to leaves whose natural size depends on their
|
|
14
|
-
content (text, images).
|
|
10
|
+
1. The reconciler maintains a parallel
|
|
11
|
+
[`LayoutNode`][pythonnative.layout.LayoutNode] tree (cached across
|
|
12
|
+
passes — clean subtrees keep their nodes, dirty ones are rebuilt).
|
|
15
13
|
2. [`calculate_layout`][pythonnative.layout.calculate_layout] is called
|
|
16
14
|
with the viewport size; it recursively determines each node's
|
|
17
15
|
``(x, y, width, height)`` relative to its parent's coordinate space.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
Nodes that are **not dirty** and are measured under the same
|
|
17
|
+
constraints as the previous pass return their memoized size without
|
|
18
|
+
recursing, which turns full-tree layout into dirty-subtree layout.
|
|
19
|
+
3. The reconciler walks the tree again and emits a frame op for each
|
|
20
|
+
native view whose frame actually changed.
|
|
21
21
|
|
|
22
22
|
The algorithm supports:
|
|
23
23
|
|
|
@@ -26,6 +26,12 @@ The algorithm supports:
|
|
|
26
26
|
``flex_end`` / ``space_between`` / ``space_around`` / ``space_evenly``),
|
|
27
27
|
``align_items`` (``stretch`` / ``flex_start`` / ``center`` /
|
|
28
28
|
``flex_end``), and ``align_self`` overrides per child.
|
|
29
|
+
- **Wrapping**: ``flex_wrap`` (``nowrap`` / ``wrap`` / ``wrap_reverse``)
|
|
30
|
+
with ``align_content`` controlling how lines share leftover
|
|
31
|
+
cross-axis space (``stretch`` default, plus the justify palette).
|
|
32
|
+
- **Direction**: ``direction: "rtl"`` flips row layouts, ``start`` /
|
|
33
|
+
``end`` edge keys (``margin_start``, ``padding_end``, absolute
|
|
34
|
+
``start`` / ``end`` insets) resolve against the inherited direction.
|
|
29
35
|
- **Sizing**: explicit ``width`` / ``height`` (numbers or percentages),
|
|
30
36
|
``min_width`` / ``max_width`` / ``min_height`` / ``max_height``
|
|
31
37
|
constraints, ``aspect_ratio``, and content-based sizing via the
|
|
@@ -33,13 +39,12 @@ The algorithm supports:
|
|
|
33
39
|
- **Flex distribution**: ``flex`` (RN shorthand for grow factor with
|
|
34
40
|
``flex_basis: 0``), ``flex_grow``, ``flex_shrink``, ``flex_basis``.
|
|
35
41
|
- **Absolute positioning**: ``position: "absolute"`` with ``top``,
|
|
36
|
-
``right``, ``bottom``, ``left``
|
|
42
|
+
``right``, ``bottom``, ``left`` (and ``start`` / ``end``) insets.
|
|
37
43
|
Absolute children are positioned relative to the parent's padding box
|
|
38
44
|
and do not participate in flex distribution.
|
|
39
|
-
- **Spacing**: ``padding`` (scalar, dict
|
|
40
|
-
``
|
|
41
|
-
``
|
|
42
|
-
(alias: ``gap``).
|
|
45
|
+
- **Spacing**: ``padding`` / ``margin`` (scalar, dict, or per-edge
|
|
46
|
+
keys), inter-child ``spacing`` (aliases: ``gap``, ``column_gap`` /
|
|
47
|
+
``row_gap`` per axis).
|
|
43
48
|
|
|
44
49
|
Example:
|
|
45
50
|
```python
|
|
@@ -79,17 +84,23 @@ LAYOUT_STYLE_KEYS = frozenset(
|
|
|
79
84
|
"flex_grow",
|
|
80
85
|
"flex_shrink",
|
|
81
86
|
"flex_basis",
|
|
87
|
+
"flex_wrap",
|
|
82
88
|
"align_self",
|
|
89
|
+
"align_content",
|
|
83
90
|
"position",
|
|
84
91
|
"top",
|
|
85
92
|
"right",
|
|
86
93
|
"bottom",
|
|
87
94
|
"left",
|
|
95
|
+
"start",
|
|
96
|
+
"end",
|
|
88
97
|
"margin",
|
|
89
98
|
"margin_top",
|
|
90
99
|
"margin_bottom",
|
|
91
100
|
"margin_left",
|
|
92
101
|
"margin_right",
|
|
102
|
+
"margin_start",
|
|
103
|
+
"margin_end",
|
|
93
104
|
"margin_horizontal",
|
|
94
105
|
"margin_vertical",
|
|
95
106
|
"padding",
|
|
@@ -97,6 +108,8 @@ LAYOUT_STYLE_KEYS = frozenset(
|
|
|
97
108
|
"padding_bottom",
|
|
98
109
|
"padding_left",
|
|
99
110
|
"padding_right",
|
|
111
|
+
"padding_start",
|
|
112
|
+
"padding_end",
|
|
100
113
|
"padding_horizontal",
|
|
101
114
|
"padding_vertical",
|
|
102
115
|
"flex_direction",
|
|
@@ -104,7 +117,10 @@ LAYOUT_STYLE_KEYS = frozenset(
|
|
|
104
117
|
"align_items",
|
|
105
118
|
"spacing",
|
|
106
119
|
"gap",
|
|
120
|
+
"row_gap",
|
|
121
|
+
"column_gap",
|
|
107
122
|
"aspect_ratio",
|
|
123
|
+
"direction",
|
|
108
124
|
}
|
|
109
125
|
)
|
|
110
126
|
"""Style keys that affect layout (and are consumed by the layout engine)."""
|
|
@@ -115,7 +131,12 @@ FLEX_DIRECTION_COLUMN_REVERSE = "column_reverse"
|
|
|
115
131
|
FLEX_DIRECTION_ROW = "row"
|
|
116
132
|
FLEX_DIRECTION_ROW_REVERSE = "row_reverse"
|
|
117
133
|
|
|
118
|
-
#
|
|
134
|
+
# flex_wrap values
|
|
135
|
+
WRAP_NOWRAP = "nowrap"
|
|
136
|
+
WRAP_WRAP = "wrap"
|
|
137
|
+
WRAP_REVERSE = "wrap_reverse"
|
|
138
|
+
|
|
139
|
+
# justify_content / align_content values
|
|
119
140
|
JUSTIFY_FLEX_START = "flex_start"
|
|
120
141
|
JUSTIFY_CENTER = "center"
|
|
121
142
|
JUSTIFY_FLEX_END = "flex_end"
|
|
@@ -134,6 +155,10 @@ ALIGN_STRETCH = "stretch"
|
|
|
134
155
|
POSITION_RELATIVE = "relative"
|
|
135
156
|
POSITION_ABSOLUTE = "absolute"
|
|
136
157
|
|
|
158
|
+
# direction values
|
|
159
|
+
DIRECTION_LTR = "ltr"
|
|
160
|
+
DIRECTION_RTL = "rtl"
|
|
161
|
+
|
|
137
162
|
# Friendly aliases on cross-axis alignment props.
|
|
138
163
|
_ALIGN_ALIASES = {
|
|
139
164
|
"start": ALIGN_FLEX_START,
|
|
@@ -256,8 +281,14 @@ def _resolve_padding_for(
|
|
|
256
281
|
parent_w: float,
|
|
257
282
|
parent_h: float,
|
|
258
283
|
prefix: str,
|
|
284
|
+
direction: str = DIRECTION_LTR,
|
|
259
285
|
) -> Tuple[float, float, float, float]:
|
|
260
|
-
"""Resolve padding/margin from `style`, honoring per-edge overrides.
|
|
286
|
+
"""Resolve padding/margin from `style`, honoring per-edge overrides.
|
|
287
|
+
|
|
288
|
+
``{prefix}_start`` / ``{prefix}_end`` resolve to the left/right
|
|
289
|
+
edge according to ``direction`` and take precedence over the
|
|
290
|
+
physical ``left`` / ``right`` keys (matching React Native).
|
|
291
|
+
"""
|
|
261
292
|
base_l, base_t, base_r, base_b = _padding_edges(style.get(prefix), parent_w, parent_h)
|
|
262
293
|
|
|
263
294
|
h_override = _resolve_value(style.get(f"{prefix}_horizontal"), parent_w)
|
|
@@ -281,6 +312,19 @@ def _resolve_padding_for(
|
|
|
281
312
|
base_t = top
|
|
282
313
|
if bottom is not None:
|
|
283
314
|
base_b = bottom
|
|
315
|
+
|
|
316
|
+
start = _resolve_value(style.get(f"{prefix}_start"), parent_w)
|
|
317
|
+
end = _resolve_value(style.get(f"{prefix}_end"), parent_w)
|
|
318
|
+
if start is not None:
|
|
319
|
+
if direction == DIRECTION_RTL:
|
|
320
|
+
base_r = start
|
|
321
|
+
else:
|
|
322
|
+
base_l = start
|
|
323
|
+
if end is not None:
|
|
324
|
+
if direction == DIRECTION_RTL:
|
|
325
|
+
base_l = end
|
|
326
|
+
else:
|
|
327
|
+
base_r = end
|
|
284
328
|
return (base_l, base_t, base_r, base_b)
|
|
285
329
|
|
|
286
330
|
|
|
@@ -362,13 +406,50 @@ def _resolve_align(value: Any, default: str = ALIGN_STRETCH) -> str:
|
|
|
362
406
|
|
|
363
407
|
|
|
364
408
|
def _resolve_justify(value: Any) -> str:
|
|
365
|
-
"""Normalize a `justify_content` value, applying aliases."""
|
|
409
|
+
"""Normalize a `justify_content` / `align_content` value, applying aliases."""
|
|
366
410
|
if value is None:
|
|
367
411
|
return JUSTIFY_FLEX_START
|
|
368
412
|
s = str(value)
|
|
369
413
|
return _JUSTIFY_ALIASES.get(s, s)
|
|
370
414
|
|
|
371
415
|
|
|
416
|
+
def _resolve_gaps(style: Dict[str, Any], is_row: bool) -> Tuple[float, float]:
|
|
417
|
+
"""Return ``(main_gap, cross_gap)`` for a container.
|
|
418
|
+
|
|
419
|
+
``column_gap`` is the horizontal gap and ``row_gap`` the vertical
|
|
420
|
+
gap (CSS semantics); ``spacing`` / ``gap`` set the main-axis gap
|
|
421
|
+
when the specific key is absent.
|
|
422
|
+
"""
|
|
423
|
+
generic = _to_float(style.get("spacing"))
|
|
424
|
+
if generic is None:
|
|
425
|
+
generic = _to_float(style.get("gap"))
|
|
426
|
+
col = _to_float(style.get("column_gap"))
|
|
427
|
+
row = _to_float(style.get("row_gap"))
|
|
428
|
+
if is_row:
|
|
429
|
+
main = col if col is not None else generic
|
|
430
|
+
cross = row
|
|
431
|
+
else:
|
|
432
|
+
main = row if row is not None else generic
|
|
433
|
+
cross = col
|
|
434
|
+
return (main or 0.0, cross or 0.0)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _wrap_mode(style: Dict[str, Any]) -> str:
|
|
438
|
+
mode = style.get("flex_wrap")
|
|
439
|
+
if mode in (WRAP_WRAP, WRAP_REVERSE):
|
|
440
|
+
return str(mode)
|
|
441
|
+
if mode == "wrap-reverse":
|
|
442
|
+
return WRAP_REVERSE
|
|
443
|
+
return WRAP_NOWRAP
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _resolve_direction(style: Dict[str, Any], inherited: str) -> str:
|
|
447
|
+
own = style.get("direction")
|
|
448
|
+
if own in (DIRECTION_LTR, DIRECTION_RTL):
|
|
449
|
+
return str(own)
|
|
450
|
+
return inherited
|
|
451
|
+
|
|
452
|
+
|
|
372
453
|
# ======================================================================
|
|
373
454
|
# LayoutNode
|
|
374
455
|
# ======================================================================
|
|
@@ -393,6 +474,9 @@ class LayoutNode:
|
|
|
393
474
|
user_data: Free-form attribute the caller may use to associate
|
|
394
475
|
each layout node with the corresponding native view; the
|
|
395
476
|
engine itself does not inspect it.
|
|
477
|
+
dirty: When ``False``, the node may serve repeat measurements
|
|
478
|
+
from its memo (set by the reconciler's incremental-layout
|
|
479
|
+
cache). Fresh nodes start dirty.
|
|
396
480
|
x: Computed x-coordinate relative to the parent's coordinate
|
|
397
481
|
space.
|
|
398
482
|
y: Computed y-coordinate relative to the parent's coordinate
|
|
@@ -406,11 +490,15 @@ class LayoutNode:
|
|
|
406
490
|
"children",
|
|
407
491
|
"measure",
|
|
408
492
|
"user_data",
|
|
493
|
+
"dirty",
|
|
409
494
|
"x",
|
|
410
495
|
"y",
|
|
411
496
|
"width",
|
|
412
497
|
"height",
|
|
413
498
|
"_pn_scroll_axis",
|
|
499
|
+
"_measure_memo",
|
|
500
|
+
"_lines",
|
|
501
|
+
"_direction",
|
|
414
502
|
)
|
|
415
503
|
|
|
416
504
|
def __init__(
|
|
@@ -424,6 +512,7 @@ class LayoutNode:
|
|
|
424
512
|
self.children = list(children) if children else []
|
|
425
513
|
self.measure = measure
|
|
426
514
|
self.user_data = user_data
|
|
515
|
+
self.dirty: bool = True
|
|
427
516
|
self.x: float = 0.0
|
|
428
517
|
self.y: float = 0.0
|
|
429
518
|
self.width: float = 0.0
|
|
@@ -436,6 +525,16 @@ class LayoutNode:
|
|
|
436
525
|
# actually scroll). The reconciler stamps this when building the
|
|
437
526
|
# layout tree for ``ScrollView`` elements.
|
|
438
527
|
self._pn_scroll_axis: Optional[str] = None
|
|
528
|
+
# Measurement memo: ``(avail_w, avail_h, forced_w, forced_h,
|
|
529
|
+
# direction, out_w, out_h)`` from the most recent measurement.
|
|
530
|
+
# Served when the node is clean (``dirty == False``) and the
|
|
531
|
+
# inputs match, skipping the entire subtree's flex math.
|
|
532
|
+
self._measure_memo: Optional[Tuple[float, float, Optional[float], Optional[float], str, float, float]] = None
|
|
533
|
+
# Flex lines computed during measurement, consumed by the
|
|
534
|
+
# positioning pass (single-line containers store one line).
|
|
535
|
+
self._lines: Optional[List["_FlexLine"]] = None
|
|
536
|
+
# Resolved direction ("ltr" / "rtl") inherited at measure time.
|
|
537
|
+
self._direction: str = DIRECTION_LTR
|
|
439
538
|
|
|
440
539
|
def __repr__(self) -> str:
|
|
441
540
|
return (
|
|
@@ -445,12 +544,28 @@ class LayoutNode:
|
|
|
445
544
|
)
|
|
446
545
|
|
|
447
546
|
|
|
547
|
+
class _FlexLine:
|
|
548
|
+
"""One row/column of children produced by line partitioning."""
|
|
549
|
+
|
|
550
|
+
__slots__ = ("children", "main_used", "cross_size")
|
|
551
|
+
|
|
552
|
+
def __init__(self, children: List[LayoutNode]) -> None:
|
|
553
|
+
self.children = children
|
|
554
|
+
self.main_used: float = 0.0
|
|
555
|
+
self.cross_size: float = 0.0
|
|
556
|
+
|
|
557
|
+
|
|
448
558
|
# ======================================================================
|
|
449
559
|
# Public entry point
|
|
450
560
|
# ======================================================================
|
|
451
561
|
|
|
452
562
|
|
|
453
|
-
def calculate_layout(
|
|
563
|
+
def calculate_layout(
|
|
564
|
+
node: LayoutNode,
|
|
565
|
+
available_width: float,
|
|
566
|
+
available_height: float,
|
|
567
|
+
direction: str = DIRECTION_LTR,
|
|
568
|
+
) -> None:
|
|
454
569
|
"""Compute layout for `node` and all descendants, in place.
|
|
455
570
|
|
|
456
571
|
Sizes the root from its own style and content. Callers that want
|
|
@@ -465,8 +580,10 @@ def calculate_layout(node: LayoutNode, available_width: float, available_height:
|
|
|
465
580
|
``math.inf`` for unbounded (e.g., horizontal scroll).
|
|
466
581
|
available_height: Available height in points. Pass
|
|
467
582
|
``math.inf`` for unbounded (e.g., vertical scroll).
|
|
583
|
+
direction: Base writing direction (``"ltr"`` or ``"rtl"``)
|
|
584
|
+
inherited by nodes that don't set their own.
|
|
468
585
|
"""
|
|
469
|
-
_measure_node(node, available_width, available_height)
|
|
586
|
+
_measure_node(node, available_width, available_height, direction=direction)
|
|
470
587
|
node.x = 0.0
|
|
471
588
|
node.y = 0.0
|
|
472
589
|
_position_children(node)
|
|
@@ -483,6 +600,7 @@ def _measure_node(
|
|
|
483
600
|
avail_h: float,
|
|
484
601
|
forced_w: Optional[float] = None,
|
|
485
602
|
forced_h: Optional[float] = None,
|
|
603
|
+
direction: str = DIRECTION_LTR,
|
|
486
604
|
) -> None:
|
|
487
605
|
"""Compute ``node.width`` / ``node.height`` and recursively size children.
|
|
488
606
|
|
|
@@ -494,8 +612,29 @@ def _measure_node(
|
|
|
494
612
|
content. Used by parents to enforce flex distribution or
|
|
495
613
|
cross-axis stretch.
|
|
496
614
|
forced_h: As ``forced_w`` but for height.
|
|
615
|
+
direction: Writing direction inherited from the parent.
|
|
497
616
|
"""
|
|
498
617
|
style = node.style
|
|
618
|
+
resolved_direction = _resolve_direction(style, direction)
|
|
619
|
+
|
|
620
|
+
# Incremental-layout memo: a clean node measured under identical
|
|
621
|
+
# inputs reuses its previous result without recursing — its whole
|
|
622
|
+
# subtree keeps the sizes from the prior pass.
|
|
623
|
+
memo = node._measure_memo
|
|
624
|
+
if (
|
|
625
|
+
memo is not None
|
|
626
|
+
and not node.dirty
|
|
627
|
+
and memo[0] == avail_w
|
|
628
|
+
and memo[1] == avail_h
|
|
629
|
+
and memo[2] == forced_w
|
|
630
|
+
and memo[3] == forced_h
|
|
631
|
+
and memo[4] == resolved_direction
|
|
632
|
+
):
|
|
633
|
+
node.width = memo[5]
|
|
634
|
+
node.height = memo[6]
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
node._direction = resolved_direction
|
|
499
638
|
|
|
500
639
|
explicit_w = forced_w if forced_w is not None else _resolve_value(style.get("width"), avail_w)
|
|
501
640
|
explicit_h = forced_h if forced_h is not None else _resolve_value(style.get("height"), avail_h)
|
|
@@ -531,6 +670,7 @@ def _measure_node(
|
|
|
531
670
|
|
|
532
671
|
node.width = max(width, 0.0)
|
|
533
672
|
node.height = max(height, 0.0)
|
|
673
|
+
node._measure_memo = (avail_w, avail_h, forced_w, forced_h, resolved_direction, node.width, node.height)
|
|
534
674
|
|
|
535
675
|
|
|
536
676
|
def _measure_leaf(
|
|
@@ -566,9 +706,10 @@ def _measure_container(
|
|
|
566
706
|
) -> Tuple[float, float]:
|
|
567
707
|
"""Layout flex children and determine the container's own size."""
|
|
568
708
|
style = node.style
|
|
709
|
+
direction = node._direction
|
|
569
710
|
base_w = explicit_w if explicit_w is not None else avail_w
|
|
570
711
|
base_h = explicit_h if explicit_h is not None else avail_h
|
|
571
|
-
pad_l, pad_t, pad_r, pad_b = _resolve_padding_for(style, base_w, base_h, "padding")
|
|
712
|
+
pad_l, pad_t, pad_r, pad_b = _resolve_padding_for(style, base_w, base_h, "padding", direction)
|
|
572
713
|
pad_x = pad_l + pad_r
|
|
573
714
|
pad_y = pad_t + pad_b
|
|
574
715
|
|
|
@@ -626,15 +767,36 @@ def _child_cross_size(child: LayoutNode, is_row: bool) -> float:
|
|
|
626
767
|
return child.height if is_row else child.width
|
|
627
768
|
|
|
628
769
|
|
|
629
|
-
def
|
|
770
|
+
def _child_margins(
|
|
771
|
+
child: LayoutNode,
|
|
772
|
+
parent_w: float,
|
|
773
|
+
parent_h: float,
|
|
774
|
+
direction: str,
|
|
775
|
+
) -> Tuple[float, float, float, float]:
|
|
776
|
+
return _resolve_padding_for(child.style, parent_w, parent_h, "margin", direction)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _child_outer_main(
|
|
780
|
+
child: LayoutNode,
|
|
781
|
+
is_row: bool,
|
|
782
|
+
parent_w: float,
|
|
783
|
+
parent_h: float,
|
|
784
|
+
direction: str,
|
|
785
|
+
) -> float:
|
|
630
786
|
"""Main-axis extent including margins."""
|
|
631
|
-
margins =
|
|
787
|
+
margins = _child_margins(child, parent_w, parent_h, direction)
|
|
632
788
|
margin_main = (margins[0] + margins[2]) if is_row else (margins[1] + margins[3])
|
|
633
789
|
return _child_main_size(child, is_row) + margin_main
|
|
634
790
|
|
|
635
791
|
|
|
636
|
-
def _child_outer_cross(
|
|
637
|
-
|
|
792
|
+
def _child_outer_cross(
|
|
793
|
+
child: LayoutNode,
|
|
794
|
+
is_row: bool,
|
|
795
|
+
parent_w: float,
|
|
796
|
+
parent_h: float,
|
|
797
|
+
direction: str,
|
|
798
|
+
) -> float:
|
|
799
|
+
margins = _child_margins(child, parent_w, parent_h, direction)
|
|
638
800
|
margin_cross = (margins[1] + margins[3]) if is_row else (margins[0] + margins[2])
|
|
639
801
|
return _child_cross_size(child, is_row) + margin_cross
|
|
640
802
|
|
|
@@ -646,15 +808,16 @@ def _measure_child_flexed(
|
|
|
646
808
|
cross_force: Optional[float],
|
|
647
809
|
is_row: bool,
|
|
648
810
|
main_bounded: bool,
|
|
811
|
+
direction: str,
|
|
649
812
|
) -> None:
|
|
650
813
|
"""Re-measure a child with optional forced main-axis size and cross hint."""
|
|
651
814
|
fallback_main = math.inf if not main_bounded else cross_avail
|
|
652
815
|
if is_row:
|
|
653
816
|
avail_w = main_size if main_size is not None else fallback_main
|
|
654
|
-
_measure_node(child, avail_w, cross_avail, forced_w=main_size, forced_h=cross_force)
|
|
817
|
+
_measure_node(child, avail_w, cross_avail, forced_w=main_size, forced_h=cross_force, direction=direction)
|
|
655
818
|
else:
|
|
656
819
|
avail_h = main_size if main_size is not None else fallback_main
|
|
657
|
-
_measure_node(child, cross_avail, avail_h, forced_w=cross_force, forced_h=main_size)
|
|
820
|
+
_measure_node(child, cross_avail, avail_h, forced_w=cross_force, forced_h=main_size, direction=direction)
|
|
658
821
|
|
|
659
822
|
|
|
660
823
|
def _resolve_cross_force(
|
|
@@ -663,6 +826,7 @@ def _resolve_cross_force(
|
|
|
663
826
|
cross_avail: float,
|
|
664
827
|
cross_bounded: bool,
|
|
665
828
|
is_row: bool,
|
|
829
|
+
direction: str,
|
|
666
830
|
) -> Optional[float]:
|
|
667
831
|
"""Compute the cross-axis size to force on a child, or ``None`` to let it size naturally.
|
|
668
832
|
|
|
@@ -680,7 +844,7 @@ def _resolve_cross_force(
|
|
|
680
844
|
cross_key = "height" if is_row else "width"
|
|
681
845
|
if cross_key in child.style and child.style.get(cross_key) is not None:
|
|
682
846
|
return None
|
|
683
|
-
margins =
|
|
847
|
+
margins = _child_margins(child, cross_avail, cross_avail, direction)
|
|
684
848
|
margin_cross = (margins[1] + margins[3]) if is_row else (margins[0] + margins[2])
|
|
685
849
|
return max(0.0, cross_avail - margin_cross)
|
|
686
850
|
|
|
@@ -694,21 +858,26 @@ def _layout_flex_children(
|
|
|
694
858
|
) -> Tuple[float, float]:
|
|
695
859
|
"""Layout the in-flow children of `parent` along the flex axes.
|
|
696
860
|
|
|
861
|
+
Children are measured, partitioned into flex lines (one line unless
|
|
862
|
+
``flex_wrap`` is enabled), grown/shrunk per line, and stretched to
|
|
863
|
+
their line's cross size. The computed line structure is stored on
|
|
864
|
+
``parent._lines`` for the positioning pass.
|
|
865
|
+
|
|
697
866
|
Returns ``(used_main, used_cross)`` — the total content size used
|
|
698
|
-
by the in-flow children, including inter-child
|
|
699
|
-
|
|
700
|
-
|
|
867
|
+
by the in-flow children, including inter-child gaps but excluding
|
|
868
|
+
the parent's own padding. The caller adds padding back in for the
|
|
869
|
+
container's outer size.
|
|
701
870
|
"""
|
|
702
871
|
style = parent.style
|
|
703
|
-
|
|
704
|
-
is_row = _is_row(
|
|
872
|
+
flex_direction = style.get("flex_direction", FLEX_DIRECTION_COLUMN)
|
|
873
|
+
is_row = _is_row(flex_direction)
|
|
874
|
+
direction = parent._direction
|
|
705
875
|
|
|
706
876
|
main_avail = content_w if is_row else content_h
|
|
707
877
|
cross_avail = content_h if is_row else content_w
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
spacing = spacing_v or 0.0
|
|
878
|
+
main_gap, cross_gap = _resolve_gaps(style, is_row)
|
|
879
|
+
wrap = _wrap_mode(style)
|
|
880
|
+
wrapping = wrap != WRAP_NOWRAP and main_bounded and math.isfinite(main_avail)
|
|
712
881
|
|
|
713
882
|
in_flow: List[LayoutNode] = []
|
|
714
883
|
absolute: List[LayoutNode] = []
|
|
@@ -720,87 +889,170 @@ def _layout_flex_children(
|
|
|
720
889
|
|
|
721
890
|
align_items = _resolve_align(style.get("align_items"), default=ALIGN_STRETCH)
|
|
722
891
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
892
|
+
# ------------------------------------------------------------------
|
|
893
|
+
# Pass 1: initial measurement (basis for grow children, natural
|
|
894
|
+
# size otherwise). Single-line containers stretch against the full
|
|
895
|
+
# cross axis here (matching the nowrap fast path); wrapping
|
|
896
|
+
# containers defer stretching until line cross sizes are known.
|
|
897
|
+
# ------------------------------------------------------------------
|
|
726
898
|
for child in in_flow:
|
|
727
899
|
grow = _flex_grow(child.style)
|
|
728
900
|
basis = _flex_basis(child.style, main_avail)
|
|
729
|
-
cross_force =
|
|
901
|
+
cross_force = (
|
|
902
|
+
None
|
|
903
|
+
if wrapping
|
|
904
|
+
else _resolve_cross_force(child, align_items, cross_avail, cross_bounded, is_row, direction)
|
|
905
|
+
)
|
|
730
906
|
|
|
731
907
|
if grow > 0:
|
|
732
908
|
initial_main = basis if basis is not None else 0.0
|
|
733
|
-
_measure_child_flexed(child, initial_main, cross_avail, cross_force, is_row, main_bounded)
|
|
734
|
-
flex_total += grow
|
|
735
|
-
flex_entries.append((child, grow, basis))
|
|
909
|
+
_measure_child_flexed(child, initial_main, cross_avail, cross_force, is_row, main_bounded, direction)
|
|
736
910
|
elif basis is not None:
|
|
737
|
-
_measure_child_flexed(child, basis, cross_avail, cross_force, is_row, main_bounded)
|
|
911
|
+
_measure_child_flexed(child, basis, cross_avail, cross_force, is_row, main_bounded, direction)
|
|
738
912
|
else:
|
|
739
913
|
avail_for_child_main = math.inf if not main_bounded else main_avail
|
|
740
914
|
if is_row:
|
|
741
|
-
_measure_node(child, avail_for_child_main, cross_avail, forced_h=cross_force)
|
|
915
|
+
_measure_node(child, avail_for_child_main, cross_avail, forced_h=cross_force, direction=direction)
|
|
742
916
|
else:
|
|
743
|
-
_measure_node(child, cross_avail, avail_for_child_main, forced_w=cross_force)
|
|
917
|
+
_measure_node(child, cross_avail, avail_for_child_main, forced_w=cross_force, direction=direction)
|
|
918
|
+
|
|
919
|
+
# ------------------------------------------------------------------
|
|
920
|
+
# Pass 2: partition children into flex lines.
|
|
921
|
+
# ------------------------------------------------------------------
|
|
922
|
+
lines: List[_FlexLine] = []
|
|
923
|
+
if wrapping:
|
|
924
|
+
current: List[LayoutNode] = []
|
|
925
|
+
current_main = 0.0
|
|
926
|
+
for child in in_flow:
|
|
927
|
+
outer = _child_outer_main(child, is_row, content_w, content_h, direction)
|
|
928
|
+
extra = outer if not current else outer + main_gap
|
|
929
|
+
if current and current_main + extra > main_avail + 1e-9:
|
|
930
|
+
lines.append(_FlexLine(current))
|
|
931
|
+
current = [child]
|
|
932
|
+
current_main = outer
|
|
933
|
+
else:
|
|
934
|
+
current.append(child)
|
|
935
|
+
current_main += extra
|
|
936
|
+
if current:
|
|
937
|
+
lines.append(_FlexLine(current))
|
|
938
|
+
elif in_flow:
|
|
939
|
+
lines.append(_FlexLine(list(in_flow)))
|
|
940
|
+
|
|
941
|
+
# ------------------------------------------------------------------
|
|
942
|
+
# Pass 3: per-line grow / shrink along the main axis.
|
|
943
|
+
# ------------------------------------------------------------------
|
|
944
|
+
for line in lines:
|
|
945
|
+
flex_total = 0.0
|
|
946
|
+
fixed_main_total = 0.0
|
|
947
|
+
flex_basis_total = 0.0
|
|
948
|
+
flex_entries: List[Tuple[LayoutNode, float, Optional[float]]] = []
|
|
949
|
+
for child in line.children:
|
|
950
|
+
grow = _flex_grow(child.style)
|
|
951
|
+
if grow > 0:
|
|
952
|
+
basis = _flex_basis(child.style, main_avail) or 0.0
|
|
953
|
+
margins = _child_margins(child, content_w, content_h, direction)
|
|
954
|
+
margin_main = (margins[0] + margins[2]) if is_row else (margins[1] + margins[3])
|
|
955
|
+
flex_total += grow
|
|
956
|
+
flex_basis_total += basis + margin_main
|
|
957
|
+
flex_entries.append((child, grow, basis))
|
|
958
|
+
else:
|
|
959
|
+
fixed_main_total += _child_outer_main(child, is_row, content_w, content_h, direction)
|
|
960
|
+
if len(line.children) > 1:
|
|
961
|
+
fixed_main_total += main_gap * (len(line.children) - 1)
|
|
962
|
+
|
|
963
|
+
if flex_total > 0 and main_bounded and math.isfinite(main_avail):
|
|
964
|
+
remaining = max(0.0, main_avail - fixed_main_total - flex_basis_total)
|
|
965
|
+
for child, grow, basis in flex_entries:
|
|
966
|
+
extra = (grow / flex_total) * remaining
|
|
967
|
+
child_main = (basis or 0.0) + extra
|
|
968
|
+
cross_force = (
|
|
969
|
+
None
|
|
970
|
+
if wrapping
|
|
971
|
+
else _resolve_cross_force(child, align_items, cross_avail, cross_bounded, is_row, direction)
|
|
972
|
+
)
|
|
973
|
+
_measure_child_flexed(child, child_main, cross_avail, cross_force, is_row, main_bounded, direction)
|
|
974
|
+
|
|
975
|
+
if main_bounded and math.isfinite(main_avail):
|
|
976
|
+
total_main = sum(_child_outer_main(c, is_row, content_w, content_h, direction) for c in line.children)
|
|
977
|
+
if len(line.children) > 1:
|
|
978
|
+
total_main += main_gap * (len(line.children) - 1)
|
|
979
|
+
overflow = total_main - main_avail
|
|
980
|
+
if overflow > 0:
|
|
981
|
+
shrinks = [(c, _flex_shrink(c.style)) for c in line.children]
|
|
982
|
+
total_shrink = sum(s * _child_main_size(c, is_row) for c, s in shrinks)
|
|
983
|
+
if total_shrink > 0:
|
|
984
|
+
for child, shrink in shrinks:
|
|
985
|
+
if shrink <= 0:
|
|
986
|
+
continue
|
|
987
|
+
take = (shrink * _child_main_size(child, is_row) / total_shrink) * overflow
|
|
988
|
+
new_main = max(0.0, _child_main_size(child, is_row) - take)
|
|
989
|
+
cross_force = (
|
|
990
|
+
None
|
|
991
|
+
if wrapping
|
|
992
|
+
else _resolve_cross_force(child, align_items, cross_avail, cross_bounded, is_row, direction)
|
|
993
|
+
)
|
|
994
|
+
_measure_child_flexed(
|
|
995
|
+
child, new_main, cross_avail, cross_force, is_row, main_bounded, direction
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
line.main_used = sum(_child_outer_main(c, is_row, content_w, content_h, direction) for c in line.children)
|
|
999
|
+
if len(line.children) > 1:
|
|
1000
|
+
line.main_used += main_gap * (len(line.children) - 1)
|
|
1001
|
+
|
|
1002
|
+
# ------------------------------------------------------------------
|
|
1003
|
+
# Pass 4: line cross sizes (+ align_content stretch) and per-line
|
|
1004
|
+
# cross stretching for wrapped containers.
|
|
1005
|
+
# ------------------------------------------------------------------
|
|
1006
|
+
for line in lines:
|
|
1007
|
+
line.cross_size = max(
|
|
1008
|
+
(_child_outer_cross(c, is_row, content_w, content_h, direction) for c in line.children),
|
|
1009
|
+
default=0.0,
|
|
1010
|
+
)
|
|
744
1011
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if main_bounded and math.isfinite(main_avail):
|
|
768
|
-
total_main = sum(_child_outer_main(c, is_row, content_w, content_h) for c in in_flow)
|
|
769
|
-
if len(in_flow) > 1:
|
|
770
|
-
total_main += spacing * (len(in_flow) - 1)
|
|
771
|
-
overflow = total_main - main_avail
|
|
772
|
-
if overflow > 0:
|
|
773
|
-
shrinks = [(c, _flex_shrink(c.style)) for c in in_flow]
|
|
774
|
-
total_shrink = sum(s * _child_main_size(c, is_row) for c, s in shrinks)
|
|
775
|
-
if total_shrink > 0:
|
|
776
|
-
for child, shrink in shrinks:
|
|
777
|
-
if shrink <= 0:
|
|
778
|
-
continue
|
|
779
|
-
take = (shrink * _child_main_size(child, is_row) / total_shrink) * overflow
|
|
780
|
-
new_main = max(0.0, _child_main_size(child, is_row) - take)
|
|
781
|
-
cross_force = _resolve_cross_force(child, align_items, cross_avail, cross_bounded, is_row)
|
|
782
|
-
_measure_child_flexed(child, new_main, cross_avail, cross_force, is_row, main_bounded)
|
|
1012
|
+
if wrapping and lines:
|
|
1013
|
+
align_content = _resolve_justify(style.get("align_content", "stretch"))
|
|
1014
|
+
if align_content == ALIGN_STRETCH and cross_bounded and math.isfinite(cross_avail):
|
|
1015
|
+
total_cross = sum(line.cross_size for line in lines) + cross_gap * (len(lines) - 1)
|
|
1016
|
+
if total_cross < cross_avail:
|
|
1017
|
+
extra_each = (cross_avail - total_cross) / len(lines)
|
|
1018
|
+
for line in lines:
|
|
1019
|
+
line.cross_size += extra_each
|
|
1020
|
+
for line in lines:
|
|
1021
|
+
for child in line.children:
|
|
1022
|
+
cross_force = _resolve_cross_force(child, align_items, line.cross_size, True, is_row, direction)
|
|
1023
|
+
if cross_force is not None and abs(cross_force - _child_cross_size(child, is_row)) > 1e-9:
|
|
1024
|
+
_measure_child_flexed(
|
|
1025
|
+
child,
|
|
1026
|
+
_child_main_size(child, is_row),
|
|
1027
|
+
line.cross_size,
|
|
1028
|
+
cross_force,
|
|
1029
|
+
is_row,
|
|
1030
|
+
main_bounded,
|
|
1031
|
+
direction,
|
|
1032
|
+
)
|
|
783
1033
|
|
|
784
1034
|
for child in absolute:
|
|
785
|
-
_measure_absolute(child, content_w, content_h)
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1035
|
+
_measure_absolute(child, content_w, content_h, direction)
|
|
1036
|
+
|
|
1037
|
+
parent._lines = lines
|
|
1038
|
+
|
|
1039
|
+
if wrapping:
|
|
1040
|
+
used_main = max((line.main_used for line in lines), default=0.0)
|
|
1041
|
+
used_cross = sum(line.cross_size for line in lines)
|
|
1042
|
+
if len(lines) > 1:
|
|
1043
|
+
used_cross += cross_gap * (len(lines) - 1)
|
|
1044
|
+
else:
|
|
1045
|
+
used_main = lines[0].main_used if lines else 0.0
|
|
1046
|
+
used_cross = lines[0].cross_size if lines else 0.0
|
|
794
1047
|
return used_main, used_cross
|
|
795
1048
|
|
|
796
1049
|
|
|
797
|
-
def _measure_absolute(child: LayoutNode, parent_w: float, parent_h: float) -> None:
|
|
798
|
-
"""Measure an absolutely-positioned child using `top` / `left` / etc."""
|
|
1050
|
+
def _measure_absolute(child: LayoutNode, parent_w: float, parent_h: float, direction: str) -> None:
|
|
1051
|
+
"""Measure an absolutely-positioned child using `top` / `left` / `start` / etc."""
|
|
799
1052
|
style = child.style
|
|
800
1053
|
explicit_w = _resolve_value(style.get("width"), parent_w)
|
|
801
1054
|
explicit_h = _resolve_value(style.get("height"), parent_h)
|
|
802
|
-
left =
|
|
803
|
-
right = _resolve_value(style.get("right"), parent_w)
|
|
1055
|
+
left, right = _absolute_horizontal_insets(style, parent_w, direction)
|
|
804
1056
|
top = _resolve_value(style.get("top"), parent_h)
|
|
805
1057
|
bottom = _resolve_value(style.get("bottom"), parent_h)
|
|
806
1058
|
|
|
@@ -811,7 +1063,30 @@ def _measure_absolute(child: LayoutNode, parent_w: float, parent_h: float) -> No
|
|
|
811
1063
|
|
|
812
1064
|
avail_w = explicit_w if explicit_w is not None else parent_w
|
|
813
1065
|
avail_h = explicit_h if explicit_h is not None else parent_h
|
|
814
|
-
_measure_node(child, avail_w, avail_h, forced_w=explicit_w, forced_h=explicit_h)
|
|
1066
|
+
_measure_node(child, avail_w, avail_h, forced_w=explicit_w, forced_h=explicit_h, direction=direction)
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def _absolute_horizontal_insets(
|
|
1070
|
+
style: Dict[str, Any],
|
|
1071
|
+
parent_w: float,
|
|
1072
|
+
direction: str,
|
|
1073
|
+
) -> Tuple[Optional[float], Optional[float]]:
|
|
1074
|
+
"""Resolve ``left`` / ``right``, honoring ``start`` / ``end`` overrides."""
|
|
1075
|
+
left = _resolve_value(style.get("left"), parent_w)
|
|
1076
|
+
right = _resolve_value(style.get("right"), parent_w)
|
|
1077
|
+
start = _resolve_value(style.get("start"), parent_w)
|
|
1078
|
+
end = _resolve_value(style.get("end"), parent_w)
|
|
1079
|
+
if start is not None:
|
|
1080
|
+
if direction == DIRECTION_RTL:
|
|
1081
|
+
right = start
|
|
1082
|
+
else:
|
|
1083
|
+
left = start
|
|
1084
|
+
if end is not None:
|
|
1085
|
+
if direction == DIRECTION_RTL:
|
|
1086
|
+
left = end
|
|
1087
|
+
else:
|
|
1088
|
+
right = end
|
|
1089
|
+
return left, right
|
|
815
1090
|
|
|
816
1091
|
|
|
817
1092
|
# ======================================================================
|
|
@@ -824,21 +1099,23 @@ def _position_children(parent: LayoutNode) -> None:
|
|
|
824
1099
|
|
|
825
1100
|
Sizes are already computed by [`_measure_node`][pythonnative.layout._measure_node];
|
|
826
1101
|
this pass walks the same tree and applies ``justify_content`` /
|
|
827
|
-
``align_items`` / absolute positioning to
|
|
828
|
-
coordinates relative to each parent's
|
|
1102
|
+
``align_items`` / ``align_content`` / absolute positioning to
|
|
1103
|
+
determine concrete coordinates relative to each parent's
|
|
1104
|
+
coordinate space.
|
|
829
1105
|
"""
|
|
830
1106
|
if not parent.children:
|
|
831
1107
|
return
|
|
832
1108
|
|
|
833
1109
|
style = parent.style
|
|
834
|
-
|
|
835
|
-
is_row = _is_row(
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1110
|
+
flex_direction = style.get("flex_direction", FLEX_DIRECTION_COLUMN)
|
|
1111
|
+
is_row = _is_row(flex_direction)
|
|
1112
|
+
direction = parent._direction
|
|
1113
|
+
rtl = direction == DIRECTION_RTL
|
|
1114
|
+
# In RTL, rows flip their visual order; ``row_reverse`` flips back.
|
|
1115
|
+
reverse = _is_reverse(flex_direction) != (rtl and is_row)
|
|
1116
|
+
pad_l, pad_t, pad_r, pad_b = _resolve_padding_for(style, parent.width, parent.height, "padding", direction)
|
|
1117
|
+
main_gap, cross_gap = _resolve_gaps(style, is_row)
|
|
1118
|
+
wrap = _wrap_mode(style)
|
|
842
1119
|
align_items = _resolve_align(style.get("align_items"), default=ALIGN_STRETCH)
|
|
843
1120
|
justify = _resolve_justify(style.get("justify_content"))
|
|
844
1121
|
|
|
@@ -855,45 +1132,75 @@ def _position_children(parent: LayoutNode) -> None:
|
|
|
855
1132
|
main_size = content_w if is_row else content_h
|
|
856
1133
|
cross_size = content_h if is_row else content_w
|
|
857
1134
|
|
|
858
|
-
|
|
859
|
-
if
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1135
|
+
lines = parent._lines
|
|
1136
|
+
if not lines:
|
|
1137
|
+
lines = [_FlexLine(list(in_flow))] if in_flow else []
|
|
1138
|
+
for line in lines:
|
|
1139
|
+
line.main_used = sum(_child_outer_main(c, is_row, content_w, content_h, direction) for c in line.children)
|
|
1140
|
+
if len(line.children) > 1:
|
|
1141
|
+
line.main_used += main_gap * (len(line.children) - 1)
|
|
1142
|
+
line.cross_size = cross_size
|
|
1143
|
+
|
|
1144
|
+
multi_line = wrap != WRAP_NOWRAP and len(lines) >= 1 and parent._lines is not None
|
|
1145
|
+
|
|
1146
|
+
# Cross-axis placement of the lines themselves.
|
|
1147
|
+
if multi_line:
|
|
1148
|
+
total_lines_cross = sum(line.cross_size for line in lines) + cross_gap * max(0, len(lines) - 1)
|
|
1149
|
+
align_content = _resolve_justify(style.get("align_content", "stretch"))
|
|
1150
|
+
if align_content == ALIGN_STRETCH:
|
|
1151
|
+
align_content = JUSTIFY_FLEX_START
|
|
1152
|
+
free_cross = max(0.0, cross_size - total_lines_cross)
|
|
1153
|
+
cross_offset, cross_between = _justify_offsets(align_content, free_cross, len(lines))
|
|
1154
|
+
else:
|
|
1155
|
+
cross_offset, cross_between = 0.0, 0.0
|
|
1156
|
+
|
|
1157
|
+
ordered_lines = list(lines)
|
|
1158
|
+
if wrap == WRAP_REVERSE and multi_line:
|
|
1159
|
+
ordered_lines.reverse()
|
|
1160
|
+
|
|
1161
|
+
cross_cursor = cross_offset
|
|
1162
|
+
for line_index, line in enumerate(ordered_lines):
|
|
1163
|
+
line_cross = line.cross_size if multi_line else cross_size
|
|
1164
|
+
free_main = max(0.0, main_size - line.main_used)
|
|
1165
|
+
main_offset, between = _justify_offsets(justify, free_main, len(line.children))
|
|
1166
|
+
|
|
1167
|
+
cursor = main_offset
|
|
1168
|
+
ordered = list(reversed(line.children)) if reverse else list(line.children)
|
|
1169
|
+
for i, child in enumerate(ordered):
|
|
1170
|
+
cm_l, cm_t, cm_r, cm_b = _child_margins(child, content_w, content_h, direction)
|
|
1171
|
+
margin_main_start = (cm_r if rtl else cm_l) if is_row else cm_t
|
|
1172
|
+
margin_cross_start = cm_t if is_row else (cm_r if rtl else cm_l)
|
|
1173
|
+
margin_cross_end = cm_b if is_row else (cm_l if rtl else cm_r)
|
|
1174
|
+
|
|
1175
|
+
cross_pos = _align_offset(
|
|
1176
|
+
child,
|
|
1177
|
+
align_items,
|
|
1178
|
+
line_cross,
|
|
1179
|
+
is_row,
|
|
1180
|
+
margin_cross_start,
|
|
1181
|
+
margin_cross_end,
|
|
1182
|
+
rtl,
|
|
1183
|
+
)
|
|
881
1184
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1185
|
+
if is_row:
|
|
1186
|
+
child.x = pad_l + cursor + margin_main_start
|
|
1187
|
+
child.y = pad_t + cross_cursor + cross_pos
|
|
1188
|
+
else:
|
|
1189
|
+
child.x = pad_l + cross_cursor + cross_pos
|
|
1190
|
+
child.y = pad_t + cursor + margin_main_start
|
|
888
1191
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1192
|
+
cursor += _child_outer_main(child, is_row, content_w, content_h, direction)
|
|
1193
|
+
if i < len(ordered) - 1:
|
|
1194
|
+
cursor += main_gap + between
|
|
892
1195
|
|
|
893
|
-
|
|
1196
|
+
_position_children(child)
|
|
1197
|
+
|
|
1198
|
+
cross_cursor += line_cross
|
|
1199
|
+
if line_index < len(ordered_lines) - 1:
|
|
1200
|
+
cross_cursor += cross_gap + cross_between
|
|
894
1201
|
|
|
895
1202
|
for child in absolute:
|
|
896
|
-
_position_absolute(child, content_w, content_h, pad_l, pad_t)
|
|
1203
|
+
_position_absolute(child, content_w, content_h, pad_l, pad_t, direction)
|
|
897
1204
|
_position_children(child)
|
|
898
1205
|
|
|
899
1206
|
|
|
@@ -925,11 +1232,18 @@ def _align_offset(
|
|
|
925
1232
|
is_row: bool,
|
|
926
1233
|
margin_start: float,
|
|
927
1234
|
margin_end: float,
|
|
1235
|
+
rtl: bool = False,
|
|
928
1236
|
) -> float:
|
|
929
|
-
"""Return the cross-axis offset for ``child`` inside its
|
|
1237
|
+
"""Return the cross-axis offset for ``child`` inside its line."""
|
|
930
1238
|
align = _resolve_align(child.style.get("align_self"), default=parent_align)
|
|
931
1239
|
if align == ALIGN_AUTO:
|
|
932
1240
|
align = parent_align
|
|
1241
|
+
# A column's cross axis is horizontal, so RTL flips start/end.
|
|
1242
|
+
if rtl and not is_row:
|
|
1243
|
+
if align == ALIGN_FLEX_START:
|
|
1244
|
+
align = ALIGN_FLEX_END
|
|
1245
|
+
elif align == ALIGN_FLEX_END:
|
|
1246
|
+
align = ALIGN_FLEX_START
|
|
933
1247
|
|
|
934
1248
|
child_cross = _child_cross_size(child, is_row)
|
|
935
1249
|
margin_cross = margin_start + margin_end
|
|
@@ -946,11 +1260,11 @@ def _position_absolute(
|
|
|
946
1260
|
content_h: float,
|
|
947
1261
|
pad_l: float,
|
|
948
1262
|
pad_t: float,
|
|
1263
|
+
direction: str,
|
|
949
1264
|
) -> None:
|
|
950
|
-
"""Position an absolutely-positioned child via `top` / `left` / etc."""
|
|
1265
|
+
"""Position an absolutely-positioned child via `top` / `left` / `start` / etc."""
|
|
951
1266
|
style = child.style
|
|
952
|
-
left =
|
|
953
|
-
right = _resolve_value(style.get("right"), content_w)
|
|
1267
|
+
left, right = _absolute_horizontal_insets(style, content_w, direction)
|
|
954
1268
|
top = _resolve_value(style.get("top"), content_h)
|
|
955
1269
|
bottom = _resolve_value(style.get("bottom"), content_h)
|
|
956
1270
|
|