nativelayout 1.2.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.
- nativelayout/core/__init__.py +14 -0
- nativelayout/core/css.py +221 -0
- nativelayout/core/differ.py +140 -0
- nativelayout/core/errors.py +25 -0
- nativelayout/core/expander.py +1266 -0
- nativelayout/core/html_parser.py +326 -0
- nativelayout/core/icons.py +82 -0
- nativelayout/core/layout_engine.py +1004 -0
- nativelayout/core/layout_host.py +248 -0
- nativelayout/core/program.py +290 -0
- nativelayout/core/scopes.py +52 -0
- nativelayout/core/tree.py +406 -0
- nativelayout/host/__init__.py +4 -0
- nativelayout/host/draw.py +189 -0
- nativelayout/host/flex.py +365 -0
- nativelayout/host/kivy_host.py +619 -0
- nativelayout/host/native_view.py +86 -0
- nativelayout/host/widgets.py +334 -0
- nativelayout-1.2.0.dist-info/METADATA +93 -0
- nativelayout-1.2.0.dist-info/RECORD +23 -0
- nativelayout-1.2.0.dist-info/WHEEL +5 -0
- nativelayout-1.2.0.dist-info/licenses/LICENSE +373 -0
- nativelayout-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Flex layout engine in pure Python (no Kivy), used by the host.
|
|
2
|
+
|
|
3
|
+
It follows the Android host (LinearLayout + NLBox): columns and rows, `flex`, `gap`,
|
|
4
|
+
`justify-content`, `align-items`/`align-self`, `flex-wrap`, margins, padding, min/max and percentages.
|
|
5
|
+
`position: absolute` nodes are placed apart, by `layout_absolute`.
|
|
6
|
+
|
|
7
|
+
Each `LNode` has the style, the children and (for a leaf) a function `leaf(max_w) -> (w, h)` that measures
|
|
8
|
+
the content without padding. After `layout`, each node has `x, y, w, h` (outer box, relative to the
|
|
9
|
+
box of its parent).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from nativelayout.core.tree import (Align, Dim, Direction, Justify, Length, NativeStyle, Percent)
|
|
14
|
+
|
|
15
|
+
INF = float("inf")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Metrics:
|
|
19
|
+
def __init__(self, density: float = 1.0, font_scale: float = 1.0):
|
|
20
|
+
self.density = density
|
|
21
|
+
self.font_scale = font_scale
|
|
22
|
+
|
|
23
|
+
def px(self, l: Length | None) -> float:
|
|
24
|
+
return 0.0 if l is None else l.to_pixels(self.density, self.font_scale)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LNode:
|
|
28
|
+
def __init__(self, style: NativeStyle, children: list["LNode"] | None = None, leaf=None, key=None):
|
|
29
|
+
self.style = style
|
|
30
|
+
self.children = children if children is not None else []
|
|
31
|
+
self.leaf = leaf
|
|
32
|
+
self.key = key
|
|
33
|
+
self.x = self.y = self.w = self.h = 0.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _clamp(v: float, lo: float, hi: float) -> float:
|
|
37
|
+
return max(lo, min(hi, v))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _limits(n: LNode, m: Metrics):
|
|
41
|
+
s = n.style
|
|
42
|
+
minw = m.px(s.min_width) if s.min_width is not None else 0.0
|
|
43
|
+
minh = m.px(s.min_height) if s.min_height is not None else 0.0
|
|
44
|
+
maxw = m.px(s.max_width) if s.max_width is not None else INF
|
|
45
|
+
maxh = m.px(s.max_height) if s.max_height is not None else INF
|
|
46
|
+
return minw, maxw, minh, maxh
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def layout(root: LNode, m: Metrics, width: float, height: float, fill_height: bool = True) -> None:
|
|
50
|
+
"""Places the tree inside `width` x `height` (the viewport). The root takes the whole width;
|
|
51
|
+
if the content is shorter, it stretches to the viewport (like a ScrollView with fillViewport)."""
|
|
52
|
+
s = root.style
|
|
53
|
+
mg = s.margin
|
|
54
|
+
ml, mr, mt, mb = m.px(mg.left), m.px(mg.right), m.px(mg.top), m.px(mg.bottom)
|
|
55
|
+
fw = m.px(s.width.length) if isinstance(s.width, Dim) else max(width - ml - mr, 0.0)
|
|
56
|
+
fh = None
|
|
57
|
+
if s.flex > 0 or (isinstance(s.height, Percent) and s.height.value >= 100):
|
|
58
|
+
fh = max(height - mt - mb, 0.0)
|
|
59
|
+
elif isinstance(s.height, Dim):
|
|
60
|
+
fh = m.px(s.height.length)
|
|
61
|
+
_lay(root, m, fw, INF, fw, fh)
|
|
62
|
+
if fh is None and fill_height and root.h < height - mt - mb:
|
|
63
|
+
_lay(root, m, fw, INF, fw, height - mt - mb)
|
|
64
|
+
root.x, root.y = ml, mt
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _lay(n: LNode, m: Metrics, aw: float, ah: float, fw: float | None, fh: float | None):
|
|
68
|
+
"""Measures and places `n`. `aw/ah`: maximum space; `fw/fh`: exact size imposed (or None)."""
|
|
69
|
+
st = n.style
|
|
70
|
+
minw, maxw, minh, maxh = _limits(n, m)
|
|
71
|
+
if fw is not None:
|
|
72
|
+
fw = _clamp(fw, minw, maxw)
|
|
73
|
+
if fh is not None:
|
|
74
|
+
fh = _clamp(fh, minh, maxh)
|
|
75
|
+
pl, pr = m.px(st.padding.left), m.px(st.padding.right)
|
|
76
|
+
pt, pb = m.px(st.padding.top), m.px(st.padding.bottom)
|
|
77
|
+
ph, pv = pl + pr, pt + pb
|
|
78
|
+
aw = min(aw, maxw)
|
|
79
|
+
ah = min(ah, maxh)
|
|
80
|
+
inner_aw = (fw if fw is not None else aw) - ph
|
|
81
|
+
inner_ah = (fh if fh is not None else ah) - pv
|
|
82
|
+
|
|
83
|
+
if n.leaf is not None or not n.children:
|
|
84
|
+
if n.leaf is not None:
|
|
85
|
+
cw, ch = n.leaf(inner_aw if inner_aw != INF else None)
|
|
86
|
+
else:
|
|
87
|
+
cw = ch = 0.0
|
|
88
|
+
w = fw if fw is not None else min(cw + ph, aw)
|
|
89
|
+
h = fh if fh is not None else min(ch + pv, ah if ah != INF else ch + pv)
|
|
90
|
+
n.w = _clamp(w, minw, maxw)
|
|
91
|
+
n.h = _clamp(h, minh, maxh)
|
|
92
|
+
return n.w, n.h
|
|
93
|
+
|
|
94
|
+
vertical = st.direction == Direction.COLUMN
|
|
95
|
+
gap = m.px(st.gap)
|
|
96
|
+
wrapped = st.wrap and not vertical
|
|
97
|
+
|
|
98
|
+
# main axis (M) and cross axis (C), from (width, height)
|
|
99
|
+
def split(w, h):
|
|
100
|
+
return (h, w) if vertical else (w, h)
|
|
101
|
+
|
|
102
|
+
def join(mn, cr):
|
|
103
|
+
return (cr, mn) if vertical else (mn, cr)
|
|
104
|
+
|
|
105
|
+
inner_main = (fh if vertical else fw)
|
|
106
|
+
inner_main = None if inner_main is None else inner_main - (pv if vertical else ph)
|
|
107
|
+
inner_cross = (fw if vertical else fh)
|
|
108
|
+
inner_cross = None if inner_cross is None else inner_cross - (ph if vertical else pv)
|
|
109
|
+
avail_main = (inner_ah if vertical else inner_aw)
|
|
110
|
+
avail_cross = (inner_aw if vertical else inner_ah)
|
|
111
|
+
|
|
112
|
+
kids = n.children
|
|
113
|
+
|
|
114
|
+
def margins(c):
|
|
115
|
+
g = c.style.margin
|
|
116
|
+
l, r, t, b = m.px(g.left), m.px(g.right), m.px(g.top), m.px(g.bottom)
|
|
117
|
+
return (t, b, l, r) if vertical else (l, r, t, b) # (main_before, main_after, cross_before, cross_after)
|
|
118
|
+
|
|
119
|
+
def child_specs(c):
|
|
120
|
+
cs = c.style
|
|
121
|
+
main_sz, cross_sz = (cs.height, cs.width) if vertical else (cs.width, cs.height)
|
|
122
|
+
align = cs.align_self or st.align_items
|
|
123
|
+
mb, ma, cb, ca = margins(c)
|
|
124
|
+
# cross axis
|
|
125
|
+
cross_fixed = None
|
|
126
|
+
cross_fill = False
|
|
127
|
+
if isinstance(cross_sz, Dim):
|
|
128
|
+
cross_fixed = m.px(cross_sz.length)
|
|
129
|
+
elif isinstance(cross_sz, Percent):
|
|
130
|
+
if cross_sz.value >= 100:
|
|
131
|
+
cross_fill = True
|
|
132
|
+
elif inner_cross is not None:
|
|
133
|
+
cross_fixed = inner_cross * cross_sz.value / 100.0
|
|
134
|
+
elif align == Align.STRETCH:
|
|
135
|
+
cross_fill = True
|
|
136
|
+
if cross_fill and inner_cross is not None:
|
|
137
|
+
cross_fixed = max(inner_cross - cb - ca, 0.0)
|
|
138
|
+
cross_fill = False
|
|
139
|
+
# main axis
|
|
140
|
+
main_fixed = None
|
|
141
|
+
if isinstance(main_sz, Dim):
|
|
142
|
+
main_fixed = m.px(main_sz.length)
|
|
143
|
+
elif isinstance(main_sz, Percent) and inner_main is not None:
|
|
144
|
+
main_fixed = inner_main * (min(main_sz.value, 100.0)) / 100.0
|
|
145
|
+
flex = cs.flex if (cs.flex > 0 and not wrapped) else 0.0
|
|
146
|
+
return main_fixed, cross_fixed, cross_fill, flex, align, (mb, ma, cb, ca)
|
|
147
|
+
|
|
148
|
+
def call(c, main_fixed, cross_fixed, avail_m, avail_c):
|
|
149
|
+
fw_, fh_ = join(main_fixed, cross_fixed)
|
|
150
|
+
aw_, ah_ = join(avail_m, avail_c)
|
|
151
|
+
return _lay(c, m, aw_, ah_, fw_, fh_)
|
|
152
|
+
|
|
153
|
+
specs = [child_specs(c) for c in kids]
|
|
154
|
+
sizes: list[tuple[float, float]] = [(0.0, 0.0)] * len(kids) # (main, cross) of each child
|
|
155
|
+
|
|
156
|
+
if wrapped:
|
|
157
|
+
return _lay_wrap(n, m, kids, specs, call, inner_aw, aw, ah, fw, fh, st, pl, pt, ph, pv, gap,
|
|
158
|
+
minw, maxw, minh, maxh)
|
|
159
|
+
|
|
160
|
+
# step 1: children without flex
|
|
161
|
+
total_main = 0.0
|
|
162
|
+
flex_sum = 0.0
|
|
163
|
+
for i, c in enumerate(kids):
|
|
164
|
+
main_fixed, cross_fixed, cross_fill, flex, align, (mb, ma, cb, ca) = specs[i]
|
|
165
|
+
if flex > 0:
|
|
166
|
+
flex_sum += flex
|
|
167
|
+
total_main += mb + ma
|
|
168
|
+
continue
|
|
169
|
+
avail_c = (avail_cross - cb - ca) if avail_cross != INF else INF
|
|
170
|
+
avail_m = (avail_main - total_main - mb - ma) if avail_main != INF else INF
|
|
171
|
+
w, h = call(c, main_fixed, cross_fixed, max(avail_m, 0.0) if avail_m != INF else INF, max(avail_c, 0.0) if avail_c != INF else INF)
|
|
172
|
+
mn, cr = split(w, h)
|
|
173
|
+
sizes[i] = (mn, cr)
|
|
174
|
+
total_main += mn + mb + ma
|
|
175
|
+
count = len(kids)
|
|
176
|
+
gaps = gap * (count - 1) if count > 1 else 0.0
|
|
177
|
+
total_main += gaps
|
|
178
|
+
|
|
179
|
+
# step 2: children with flex (they only grow if the main axis of the parent is exact)
|
|
180
|
+
if flex_sum > 0:
|
|
181
|
+
free = (inner_main - total_main) if inner_main is not None else 0.0
|
|
182
|
+
free = max(free, 0.0)
|
|
183
|
+
for i, c in enumerate(kids):
|
|
184
|
+
main_fixed, cross_fixed, cross_fill, flex, align, (mb, ma, cb, ca) = specs[i]
|
|
185
|
+
if flex <= 0:
|
|
186
|
+
continue
|
|
187
|
+
share = free * flex / flex_sum
|
|
188
|
+
avail_c = (avail_cross - cb - ca) if avail_cross != INF else INF
|
|
189
|
+
w, h = call(c, share, cross_fixed, share, max(avail_c, 0.0) if avail_c != INF else INF)
|
|
190
|
+
mn, cr = split(w, h)
|
|
191
|
+
sizes[i] = (mn, cr)
|
|
192
|
+
total_main += mn
|
|
193
|
+
# main size of the parent
|
|
194
|
+
content_main = total_main
|
|
195
|
+
if inner_main is not None:
|
|
196
|
+
main_size = inner_main
|
|
197
|
+
else:
|
|
198
|
+
main_size = min(content_main, avail_main) if avail_main != INF else content_main
|
|
199
|
+
# cross size of the parent
|
|
200
|
+
max_cross = 0.0
|
|
201
|
+
for i in range(count):
|
|
202
|
+
_, _, _, _, _, (mb, ma, cb, ca) = specs[i]
|
|
203
|
+
max_cross = max(max_cross, sizes[i][1] + cb + ca)
|
|
204
|
+
cross_size = inner_cross if inner_cross is not None else max_cross
|
|
205
|
+
if inner_cross is None and avail_cross != INF:
|
|
206
|
+
cross_size = min(cross_size, avail_cross)
|
|
207
|
+
|
|
208
|
+
# children with a "stretch" cross axis that ended up narrower than the parent are measured again
|
|
209
|
+
for i, c in enumerate(kids):
|
|
210
|
+
main_fixed, cross_fixed, cross_fill, flex, align, (mb, ma, cb, ca) = specs[i]
|
|
211
|
+
if cross_fill:
|
|
212
|
+
target = max(cross_size - cb - ca, 0.0)
|
|
213
|
+
if abs(sizes[i][1] - target) > 0.5:
|
|
214
|
+
w, h = call(c, sizes[i][0], target, sizes[i][0], target)
|
|
215
|
+
sizes[i] = split(w, h)
|
|
216
|
+
|
|
217
|
+
# justify
|
|
218
|
+
free = max(main_size - content_main, 0.0)
|
|
219
|
+
j = st.justify
|
|
220
|
+
start = 0.0
|
|
221
|
+
between = gap
|
|
222
|
+
if count:
|
|
223
|
+
if j == Justify.CENTER:
|
|
224
|
+
start = free / 2
|
|
225
|
+
elif j == Justify.END:
|
|
226
|
+
start = free
|
|
227
|
+
elif j == Justify.BETWEEN and count > 1:
|
|
228
|
+
between = gap + free / (count - 1)
|
|
229
|
+
elif j == Justify.AROUND:
|
|
230
|
+
start = free / (2 * count)
|
|
231
|
+
between = gap + free / count
|
|
232
|
+
elif j == Justify.EVENLY:
|
|
233
|
+
start = free / (count + 1)
|
|
234
|
+
between = gap + free / (count + 1)
|
|
235
|
+
pos = start
|
|
236
|
+
pm, pc = (pt, pl) if vertical else (pl, pt)
|
|
237
|
+
for i, c in enumerate(kids):
|
|
238
|
+
main_fixed, cross_fixed, cross_fill, flex, align, (mb, ma, cb, ca) = specs[i]
|
|
239
|
+
pos += mb
|
|
240
|
+
mn, cr = sizes[i]
|
|
241
|
+
if align == Align.CENTER:
|
|
242
|
+
cpos = (cross_size - cr - cb - ca) / 2 + cb
|
|
243
|
+
elif align == Align.END:
|
|
244
|
+
cpos = cross_size - cr - ca
|
|
245
|
+
else:
|
|
246
|
+
cpos = cb
|
|
247
|
+
if vertical:
|
|
248
|
+
c.x, c.y = pc + cpos, pm + pos
|
|
249
|
+
else:
|
|
250
|
+
c.x, c.y = pm + pos, pc + cpos
|
|
251
|
+
pos += mn + ma + between
|
|
252
|
+
|
|
253
|
+
w, h = join(main_size, cross_size)
|
|
254
|
+
w += ph
|
|
255
|
+
h += pv
|
|
256
|
+
n.w = _clamp(fw if fw is not None else w, minw, maxw)
|
|
257
|
+
n.h = _clamp(fh if fh is not None else h, minh, maxh)
|
|
258
|
+
return n.w, n.h
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _lay_wrap(n, m, kids, specs, call, inner_aw, aw, ah, fw, fh, st, pl, pt, ph, pv, gap,
|
|
262
|
+
minw, maxw, minh, maxh):
|
|
263
|
+
"""A row with `flex-wrap: wrap`: flex does not grow, each child keeps its width."""
|
|
264
|
+
avail = inner_aw if inner_aw != INF else INF
|
|
265
|
+
lines: list[list[tuple[int, float, float]]] = [[]]
|
|
266
|
+
line_w = 0.0
|
|
267
|
+
for i, c in enumerate(kids):
|
|
268
|
+
main_fixed, cross_fixed, cross_fill, flex, align, (mb, ma, cb, ca) = specs[i]
|
|
269
|
+
w, h = call(c, main_fixed, cross_fixed, avail if avail != INF else INF, INF)
|
|
270
|
+
cw = w + mb + ma
|
|
271
|
+
ch = h + cb + ca
|
|
272
|
+
if lines[-1] and line_w + gap + cw > avail:
|
|
273
|
+
lines.append([])
|
|
274
|
+
line_w = 0.0
|
|
275
|
+
line_w += (gap if lines[-1] else 0.0) + cw
|
|
276
|
+
lines[-1].append((i, w, h))
|
|
277
|
+
y = 0.0
|
|
278
|
+
max_w = 0.0
|
|
279
|
+
inner_w = (fw - ph) if fw is not None else None
|
|
280
|
+
for line in lines:
|
|
281
|
+
if not line:
|
|
282
|
+
continue
|
|
283
|
+
lh = max(h + specs[i][5][2] + specs[i][5][3] for i, w, h in line)
|
|
284
|
+
tw = sum(w + specs[i][5][0] + specs[i][5][1] for i, w, h in line) + gap * (len(line) - 1)
|
|
285
|
+
free = max((inner_w if inner_w is not None else tw) - tw, 0.0)
|
|
286
|
+
j = st.justify
|
|
287
|
+
x = free / 2 if j == Justify.CENTER else free if j == Justify.END else 0.0
|
|
288
|
+
between = gap
|
|
289
|
+
k = len(line)
|
|
290
|
+
if j == Justify.BETWEEN and k > 1:
|
|
291
|
+
between = gap + free / (k - 1)
|
|
292
|
+
elif j == Justify.AROUND:
|
|
293
|
+
x = free / (2 * k)
|
|
294
|
+
between = gap + free / k
|
|
295
|
+
elif j == Justify.EVENLY:
|
|
296
|
+
x = free / (k + 1)
|
|
297
|
+
between = gap + free / (k + 1)
|
|
298
|
+
for i, w, h in line:
|
|
299
|
+
c = kids[i]
|
|
300
|
+
mb, ma, cb, ca = specs[i][5]
|
|
301
|
+
align = specs[i][4]
|
|
302
|
+
x += mb
|
|
303
|
+
if align == Align.CENTER:
|
|
304
|
+
cy = (lh - h - cb - ca) / 2 + cb
|
|
305
|
+
elif align == Align.END:
|
|
306
|
+
cy = lh - h - ca
|
|
307
|
+
else:
|
|
308
|
+
cy = cb
|
|
309
|
+
c.x, c.y = pl + x, pt + y + cy
|
|
310
|
+
x += w + ma + between
|
|
311
|
+
max_w = max(max_w, tw)
|
|
312
|
+
y += lh + gap
|
|
313
|
+
total_h = max(y - gap, 0.0)
|
|
314
|
+
w = (fw if fw is not None else max_w + ph)
|
|
315
|
+
h = (fh if fh is not None else total_h + pv)
|
|
316
|
+
n.w = _clamp(w, minw, maxw)
|
|
317
|
+
n.h = _clamp(h, minh, maxh)
|
|
318
|
+
return n.w, n.h
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def lay_node(n: LNode, m: Metrics, aw: float, ah: float, fw: float | None, fh: float | None):
|
|
322
|
+
"""Measures and places a node with the limits/exact size given (used by header, footer and layers)."""
|
|
323
|
+
return _lay(n, m, aw, ah, fw, fh)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def layout_absolute(n: LNode, m: Metrics, pw: float, ph: float) -> None:
|
|
327
|
+
"""Places a `position: absolute` node inside a parent box of `pw` x `ph` (its outer box).
|
|
328
|
+
|
|
329
|
+
It follows the Android host (AbsFrame): `left/top/right/bottom` become offsets from the named edge,
|
|
330
|
+
added to the margin. With both `left` and `right` (or `top` and `bottom`) and no size it stretches.
|
|
331
|
+
With only `right` (or only `bottom`) it sits against that edge. The node takes no space in the flow.
|
|
332
|
+
After the call `n.x, n.y, n.w, n.h` are relative to the parent box.
|
|
333
|
+
"""
|
|
334
|
+
s = n.style
|
|
335
|
+
left = m.px(s.left) if s.left is not None else None
|
|
336
|
+
right = m.px(s.right) if s.right is not None else None
|
|
337
|
+
top = m.px(s.top) if s.top is not None else None
|
|
338
|
+
bottom = m.px(s.bottom) if s.bottom is not None else None
|
|
339
|
+
mg = s.margin
|
|
340
|
+
ml, mr, mt, mb = m.px(mg.left), m.px(mg.right), m.px(mg.top), m.px(mg.bottom)
|
|
341
|
+
|
|
342
|
+
avail_w = max(pw - ml - mr - (left or 0.0) - (right or 0.0), 0.0)
|
|
343
|
+
avail_h = max(ph - mt - mb - (top or 0.0) - (bottom or 0.0), 0.0)
|
|
344
|
+
|
|
345
|
+
fw = None
|
|
346
|
+
if isinstance(s.width, Dim):
|
|
347
|
+
fw = m.px(s.width.length)
|
|
348
|
+
elif (isinstance(s.width, Percent) and s.width.value >= 100) or (left is not None and right is not None):
|
|
349
|
+
fw = avail_w
|
|
350
|
+
fh = None
|
|
351
|
+
if isinstance(s.height, Dim):
|
|
352
|
+
fh = m.px(s.height.length)
|
|
353
|
+
elif (isinstance(s.height, Percent) and s.height.value >= 100) or (top is not None and bottom is not None):
|
|
354
|
+
fh = avail_h
|
|
355
|
+
|
|
356
|
+
_lay(n, m, avail_w, avail_h, fw, fh)
|
|
357
|
+
|
|
358
|
+
if right is not None and left is None:
|
|
359
|
+
n.x = pw - mr - right - n.w
|
|
360
|
+
else:
|
|
361
|
+
n.x = ml + (left or 0.0)
|
|
362
|
+
if bottom is not None and top is None:
|
|
363
|
+
n.y = ph - mb - bottom - n.h
|
|
364
|
+
else:
|
|
365
|
+
n.y = mt + (top or 0.0)
|