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,14 @@
|
|
|
1
|
+
from .errors import NativeLayoutError, extract_line
|
|
2
|
+
from . import scopes
|
|
3
|
+
from .tree import * # noqa: F401,F403
|
|
4
|
+
from .differ import diff
|
|
5
|
+
from . import css
|
|
6
|
+
from .html_parser import (HtmlParser, HtmlElement, HtmlText, HtmlNode, HtmlCachedElement,
|
|
7
|
+
Chunk, ChunkCache, MarkupError)
|
|
8
|
+
from .layout_engine import LayoutEngine, LayoutResult, LayoutWarning
|
|
9
|
+
from .layout_host import (LayoutHost, LayoutSession, LayoutEvent, FilledMarkup,
|
|
10
|
+
PreparedRender, BACK_EVENT_KEY)
|
|
11
|
+
from .expander import Expander, ExpandedScreen, ExpandError
|
|
12
|
+
from .program import (NativeViewBase, Page, Screen, screen, state, view, go, go_root, go_back,
|
|
13
|
+
can_go_back, back, click, click_index, clicked, value, checked, set_value,
|
|
14
|
+
save, load, exists, delete)
|
nativelayout/core/css.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Parser of CSS declarations and values (port of Css.kt)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
import re
|
|
6
|
+
import threading
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from .tree import AUTO, Dim, Length, Percent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class CssDeclaration:
|
|
15
|
+
name: str
|
|
16
|
+
value: str
|
|
17
|
+
line: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _round(x: float) -> int:
|
|
21
|
+
"""Like Kotlin roundToInt: halves round up."""
|
|
22
|
+
return int(math.floor(x + 0.5))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _to_float(s: str) -> float | None:
|
|
26
|
+
"""Like toFloatOrNull: a finite decimal number, without '_'."""
|
|
27
|
+
if "_" in s or not s.strip() == s:
|
|
28
|
+
return None
|
|
29
|
+
try:
|
|
30
|
+
v = float(s)
|
|
31
|
+
except ValueError:
|
|
32
|
+
return None
|
|
33
|
+
return v if math.isfinite(v) else None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _clamp(v, lo, hi):
|
|
37
|
+
return max(lo, min(hi, v))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def split_top_level(text: str, sep: str) -> list[str]:
|
|
41
|
+
parts: list[str] = []
|
|
42
|
+
cur: list[str] = []
|
|
43
|
+
depth = 0
|
|
44
|
+
quote: str | None = None
|
|
45
|
+
for c in text:
|
|
46
|
+
if quote is not None:
|
|
47
|
+
cur.append(c)
|
|
48
|
+
if c == quote:
|
|
49
|
+
quote = None
|
|
50
|
+
elif c == '"' or c == "'":
|
|
51
|
+
quote = c
|
|
52
|
+
cur.append(c)
|
|
53
|
+
elif c == "(":
|
|
54
|
+
depth += 1
|
|
55
|
+
cur.append(c)
|
|
56
|
+
elif c == ")":
|
|
57
|
+
if depth > 0:
|
|
58
|
+
depth -= 1
|
|
59
|
+
cur.append(c)
|
|
60
|
+
elif c == sep and depth == 0:
|
|
61
|
+
parts.append("".join(cur))
|
|
62
|
+
cur = []
|
|
63
|
+
else:
|
|
64
|
+
cur.append(c)
|
|
65
|
+
parts.append("".join(cur))
|
|
66
|
+
return parts
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_IMPORTANT = re.compile(r"\s*!important\s*$", re.IGNORECASE)
|
|
70
|
+
_cache: dict[str, tuple[list[tuple[str, str]], list[str]]] = {}
|
|
71
|
+
_lock = threading.Lock()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse(text: str):
|
|
75
|
+
items: list[tuple[str, str]] = []
|
|
76
|
+
warnings: list[str] = []
|
|
77
|
+
for part in split_top_level(text, ";"):
|
|
78
|
+
p = part.strip()
|
|
79
|
+
if not p:
|
|
80
|
+
continue
|
|
81
|
+
colon = p.find(":")
|
|
82
|
+
if colon <= 0:
|
|
83
|
+
warnings.append(f"CSS: declaration without ':' was ignored ('{p}')")
|
|
84
|
+
continue
|
|
85
|
+
name = p[:colon].strip().lower()
|
|
86
|
+
value = _IMPORTANT.sub("", p[colon + 1:].strip())
|
|
87
|
+
if not value:
|
|
88
|
+
warnings.append(f"CSS: property '{name}' has no value and was ignored")
|
|
89
|
+
continue
|
|
90
|
+
items.append((name, value))
|
|
91
|
+
return items, warnings
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def declarations(text: str, line: int, warn: Callable[[str, int], None]) -> list[CssDeclaration]:
|
|
95
|
+
with _lock:
|
|
96
|
+
parsed = _cache.get(text)
|
|
97
|
+
if parsed is None:
|
|
98
|
+
parsed = _parse(text)
|
|
99
|
+
with _lock:
|
|
100
|
+
if len(_cache) > 512:
|
|
101
|
+
_cache.clear()
|
|
102
|
+
_cache[text] = parsed
|
|
103
|
+
items, warnings = parsed
|
|
104
|
+
for w in warnings:
|
|
105
|
+
warn(w, line)
|
|
106
|
+
return [CssDeclaration(n, v, line) for n, v in items]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_OPAQUE = 0xFF000000
|
|
110
|
+
|
|
111
|
+
_NAMES = {
|
|
112
|
+
"black": 0x000000, "white": 0xFFFFFF, "red": 0xFF0000, "green": 0x008000,
|
|
113
|
+
"blue": 0x0000FF, "yellow": 0xFFFF00, "orange": 0xFFA500, "purple": 0x800080,
|
|
114
|
+
"pink": 0xFFC0CB, "gray": 0x808080, "grey": 0x808080, "silver": 0xC0C0C0,
|
|
115
|
+
"maroon": 0x800000, "navy": 0x000080, "teal": 0x008080, "lime": 0x00FF00,
|
|
116
|
+
"aqua": 0x00FFFF, "cyan": 0x00FFFF, "magenta": 0xFF00FF, "fuchsia": 0xFF00FF,
|
|
117
|
+
"olive": 0x808000, "brown": 0xA52A2A, "gold": 0xFFD700, "indigo": 0x4B0082,
|
|
118
|
+
"violet": 0xEE82EE, "coral": 0xFF7F50, "crimson": 0xDC143C, "tomato": 0xFF6347,
|
|
119
|
+
"salmon": 0xFA8072, "khaki": 0xF0E68C, "beige": 0xF5F5DC, "ivory": 0xFFFFF0,
|
|
120
|
+
"lavender": 0xE6E6FA, "turquoise": 0x40E0D0, "darkgray": 0xA9A9A9,
|
|
121
|
+
"darkgrey": 0xA9A9A9, "lightgray": 0xD3D3D3, "lightgrey": 0xD3D3D3,
|
|
122
|
+
"darkblue": 0x00008B, "darkgreen": 0x006400, "darkred": 0x8B0000,
|
|
123
|
+
"lightblue": 0xADD8E6, "lightgreen": 0x90EE90, "whitesmoke": 0xF5F5F5,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_REGEX_RGB = re.compile(r"^rgba?\(([^)]*)\)$")
|
|
127
|
+
_REGEX_LENGTH = re.compile(r"^(-?\d*\.?\d+)(px|rem)$", re.ASCII)
|
|
128
|
+
_HEXCHARS = set("0123456789abcdef")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _pack(r: int, g: int, b: int, a: int) -> int:
|
|
132
|
+
return (a << 24) | (r << 16) | (g << 8) | b
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def color(text: str) -> int | None:
|
|
136
|
+
t = text.strip().lower()
|
|
137
|
+
if not t:
|
|
138
|
+
return None
|
|
139
|
+
if t == "transparent":
|
|
140
|
+
return 0
|
|
141
|
+
if t in _NAMES:
|
|
142
|
+
return _NAMES[t] | _OPAQUE
|
|
143
|
+
if t.startswith("#"):
|
|
144
|
+
h = t[1:]
|
|
145
|
+
if not h or any(ch not in _HEXCHARS for ch in h):
|
|
146
|
+
return None
|
|
147
|
+
dbl = lambda p: int(h[p], 16) * 17
|
|
148
|
+
pair = lambda p: int(h[p:p + 2], 16)
|
|
149
|
+
n = len(h)
|
|
150
|
+
if n == 3:
|
|
151
|
+
return _pack(dbl(0), dbl(1), dbl(2), 255)
|
|
152
|
+
if n == 4:
|
|
153
|
+
return _pack(dbl(0), dbl(1), dbl(2), dbl(3))
|
|
154
|
+
if n == 6:
|
|
155
|
+
return _pack(pair(0), pair(2), pair(4), 255)
|
|
156
|
+
if n == 8:
|
|
157
|
+
return _pack(pair(0), pair(2), pair(4), pair(6))
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
m = _REGEX_RGB.match(t)
|
|
161
|
+
if m is None:
|
|
162
|
+
return None
|
|
163
|
+
parts = [p.strip() for p in re.split(r"[, /]", m.group(1)) if p.strip()]
|
|
164
|
+
if not 3 <= len(parts) <= 4:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def channel(s: str) -> int | None:
|
|
168
|
+
if s.endswith("%"):
|
|
169
|
+
v = _to_float(s[:-1])
|
|
170
|
+
return None if v is None else _round(v * 2.55)
|
|
171
|
+
v = _to_float(s)
|
|
172
|
+
return None if v is None else _round(v)
|
|
173
|
+
|
|
174
|
+
r, g, b = channel(parts[0]), channel(parts[1]), channel(parts[2])
|
|
175
|
+
if r is None or g is None or b is None:
|
|
176
|
+
return None
|
|
177
|
+
a = 255
|
|
178
|
+
if len(parts) == 4:
|
|
179
|
+
s = parts[3]
|
|
180
|
+
v = _to_float(s[:-1]) if s.endswith("%") else _to_float(s)
|
|
181
|
+
if v is None:
|
|
182
|
+
return None
|
|
183
|
+
if s.endswith("%"):
|
|
184
|
+
v = v / 100.0
|
|
185
|
+
a = _round(_clamp(v, 0.0, 1.0) * 255.0)
|
|
186
|
+
return _pack(_clamp(r, 0, 255), _clamp(g, 0, 255), _clamp(b, 0, 255), a)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def length(text: str) -> Length | None:
|
|
190
|
+
t = text.strip().lower()
|
|
191
|
+
if t == "0":
|
|
192
|
+
return Length.ZERO
|
|
193
|
+
m = _REGEX_LENGTH.match(t)
|
|
194
|
+
if m is None:
|
|
195
|
+
return None
|
|
196
|
+
n = _to_float(m.group(1))
|
|
197
|
+
if n is None:
|
|
198
|
+
return None
|
|
199
|
+
return Length.rem(n) if m.group(2) == "rem" else Length.px(n)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def fraction(text: str) -> float | None:
|
|
203
|
+
"""A number from 0 to 1, or a percentage (opacity)."""
|
|
204
|
+
t = text.strip().lower()
|
|
205
|
+
if t.endswith("%"):
|
|
206
|
+
v = _to_float(t[:-1])
|
|
207
|
+
v = None if v is None else v / 100.0
|
|
208
|
+
else:
|
|
209
|
+
v = _to_float(t)
|
|
210
|
+
return None if v is None else _clamp(v, 0.0, 1.0)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def size(text: str):
|
|
214
|
+
t = text.strip().lower()
|
|
215
|
+
if t == "auto":
|
|
216
|
+
return AUTO
|
|
217
|
+
if t.endswith("%"):
|
|
218
|
+
v = _to_float(t[:-1])
|
|
219
|
+
return None if v is None else Percent(v)
|
|
220
|
+
ln = length(t)
|
|
221
|
+
return None if ln is None else Dim(ln)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Diff between two native trees (port of LayoutDiffer.kt)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from .tree import (Native, NativeBox, NativeButton, NativeCheckbox, NativeField,
|
|
7
|
+
NativeImage, NativeItem, NativeStyle, NativeText, Position, Span,
|
|
8
|
+
native_children, same_as)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Change:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SetRoot(Change):
|
|
17
|
+
node: Native
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Create(Change):
|
|
22
|
+
parent_key: str
|
|
23
|
+
position: int
|
|
24
|
+
node: Native
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Remove(Change):
|
|
29
|
+
key: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Replace(Change):
|
|
34
|
+
key: str
|
|
35
|
+
node: Native
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class UpdateText(Change):
|
|
40
|
+
key: str
|
|
41
|
+
spans: list[Span]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class UpdateStyle(Change):
|
|
46
|
+
key: str
|
|
47
|
+
style: NativeStyle
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class UpdateAttributes(Change):
|
|
52
|
+
key: str
|
|
53
|
+
node: Native
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class SetItems(Change):
|
|
58
|
+
key: str
|
|
59
|
+
items: list[Native]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def diff(previous: Native | None, nxt: Native) -> list[Change]:
|
|
63
|
+
if previous is None or previous.key != nxt.key or type(previous) is not type(nxt):
|
|
64
|
+
return [SetRoot(nxt)]
|
|
65
|
+
removed: list[Change] = []
|
|
66
|
+
rest: list[Change] = []
|
|
67
|
+
_compare(previous, nxt, removed, rest)
|
|
68
|
+
return removed + rest
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _compare(a: Native, n: Native, removed: list, rest: list) -> None:
|
|
72
|
+
if isinstance(a, NativeBox) and isinstance(n, NativeBox):
|
|
73
|
+
if (a.virtual != n.virtual or a.overlay != n.overlay
|
|
74
|
+
or a.header != n.header or a.footer != n.footer
|
|
75
|
+
or a.has_absolute != n.has_absolute):
|
|
76
|
+
rest.append(Replace(n.key, n))
|
|
77
|
+
return
|
|
78
|
+
if n.virtual:
|
|
79
|
+
if a.style != n.style:
|
|
80
|
+
rest.append(UpdateStyle(n.key, n.style))
|
|
81
|
+
if _attributes_differ(a, n):
|
|
82
|
+
rest.append(UpdateAttributes(n.key, n))
|
|
83
|
+
if not _same_children(a.children, n.children):
|
|
84
|
+
rest.append(SetItems(n.key, n.children))
|
|
85
|
+
return
|
|
86
|
+
if a.style != n.style:
|
|
87
|
+
rest.append(UpdateStyle(n.key, n.style))
|
|
88
|
+
if _attributes_differ(a, n):
|
|
89
|
+
rest.append(UpdateAttributes(n.key, n))
|
|
90
|
+
if isinstance(a, NativeText) and isinstance(n, NativeText) and a.spans != n.spans:
|
|
91
|
+
rest.append(UpdateText(n.key, n.spans))
|
|
92
|
+
ca, cn = native_children(a), native_children(n)
|
|
93
|
+
if not ca and not cn:
|
|
94
|
+
return
|
|
95
|
+
_compare_children(n, ca, cn, removed, rest)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _same_children(a: list, b: list) -> bool:
|
|
99
|
+
return len(a) == len(b) and all(same_as(x, y) for x, y in zip(a, b))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _attributes_differ(a: Native, n: Native) -> bool:
|
|
103
|
+
if a.classes != n.classes:
|
|
104
|
+
return True
|
|
105
|
+
if isinstance(a, NativeButton) and isinstance(n, NativeButton):
|
|
106
|
+
return a.label != n.label or a.disabled != n.disabled
|
|
107
|
+
if isinstance(a, NativeField) and isinstance(n, NativeField):
|
|
108
|
+
return (a.type != n.type or a.hint != n.hint or a.disabled != n.disabled
|
|
109
|
+
or a.read_only != n.read_only or a.rows != n.rows)
|
|
110
|
+
if isinstance(a, NativeCheckbox) and isinstance(n, NativeCheckbox):
|
|
111
|
+
return a.disabled != n.disabled
|
|
112
|
+
if isinstance(a, NativeImage) and isinstance(n, NativeImage):
|
|
113
|
+
return a.source != n.source or a.description != n.description
|
|
114
|
+
if isinstance(a, NativeItem) and isinstance(n, NativeItem):
|
|
115
|
+
return a.marker != n.marker
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _compare_children(parent: Native, prev: list, nxt: list, removed: list, rest: list) -> None:
|
|
120
|
+
new_keys = {c.key for c in nxt}
|
|
121
|
+
old_by_key = {c.key: c for c in prev}
|
|
122
|
+
kept_old = [c.key for c in prev if c.key in new_keys]
|
|
123
|
+
kept_new = [c.key for c in nxt if c.key in old_by_key]
|
|
124
|
+
if kept_old != kept_new:
|
|
125
|
+
rest.append(Replace(parent.key, parent))
|
|
126
|
+
return
|
|
127
|
+
for a in prev:
|
|
128
|
+
if a.key not in new_keys:
|
|
129
|
+
removed.append(Remove(a.key))
|
|
130
|
+
for i, n in enumerate(nxt):
|
|
131
|
+
a = old_by_key.get(n.key)
|
|
132
|
+
if a is None:
|
|
133
|
+
# an absolute child lives apart from the flow: its position counts only the absolute ones
|
|
134
|
+
absolute = n.style.position == Position.ABSOLUTE
|
|
135
|
+
before = sum(1 for c in nxt[:i] if (c.style.position == Position.ABSOLUTE) == absolute)
|
|
136
|
+
rest.append(Create(parent.key, before, n))
|
|
137
|
+
elif type(a) is not type(n):
|
|
138
|
+
rest.append(Replace(n.key, n))
|
|
139
|
+
else:
|
|
140
|
+
_compare(a, n, removed, rest)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Errors of the core (port of NativeLayoutError.kt)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class NativeLayoutError(Exception):
|
|
6
|
+
def __init__(self, message: str, line: int, context: str,
|
|
7
|
+
word: str | None = None, suggestion: str | None = None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.message = message
|
|
10
|
+
self.line = line
|
|
11
|
+
self.context = context
|
|
12
|
+
self.word = word
|
|
13
|
+
self.suggestion = suggestion
|
|
14
|
+
|
|
15
|
+
def format(self) -> str:
|
|
16
|
+
out = f"Error: line {self.line}\n{self.context}\n\n{self.message}"
|
|
17
|
+
if self.suggestion is not None:
|
|
18
|
+
out += f"\n\nSuggestion: {self.suggestion}"
|
|
19
|
+
return out
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_line(source: str, line_number: int) -> str:
|
|
23
|
+
lines = source.split("\n")
|
|
24
|
+
i = line_number - 1
|
|
25
|
+
return lines[i].strip() if 0 <= i < len(lines) else ""
|