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,334 @@
|
|
|
1
|
+
"""Kivy widgets of the host. Each draws its own background; the position comes from the flex engine."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from kivy.core.text.markup import MarkupLabel as CoreLabel
|
|
5
|
+
from kivy.graphics import Color, Line, Rectangle, InstructionGroup
|
|
6
|
+
from kivy.uix.relativelayout import RelativeLayout
|
|
7
|
+
from kivy.uix.textinput import TextInput
|
|
8
|
+
from kivy.uix.widget import Widget
|
|
9
|
+
from kivy.utils import escape_markup
|
|
10
|
+
|
|
11
|
+
from nativelayout.core.tree import (Align, Direction, FontFamily, Justify, LineHeightFixed,
|
|
12
|
+
LineHeightMultiple, NativeStyle, Span, TextAlign)
|
|
13
|
+
from nativelayout.core.tree import ImagePos
|
|
14
|
+
|
|
15
|
+
from . import draw
|
|
16
|
+
|
|
17
|
+
TAP_SLOP = 12.0 # pixels of movement after which a touch is no longer a click
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _font_name(f: FontFamily) -> str | None:
|
|
21
|
+
if f == FontFamily.MONOSPACE:
|
|
22
|
+
from kivy.resources import resource_find
|
|
23
|
+
return resource_find("data/fonts/RobotoMono-Regular.ttf") or None
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------- text
|
|
28
|
+
def markup_of(spans: list[Span], px) -> str:
|
|
29
|
+
out = []
|
|
30
|
+
for s in spans:
|
|
31
|
+
t = escape_markup(s.text)
|
|
32
|
+
if not t:
|
|
33
|
+
continue
|
|
34
|
+
c = s.color
|
|
35
|
+
hexcol = "%02x%02x%02x%02x" % ((c >> 16) & 255, (c >> 8) & 255, c & 255, (c >> 24) & 255)
|
|
36
|
+
t = f"[size={max(1, int(round(px(s.size))))}]{t}[/size]"
|
|
37
|
+
if s.bold:
|
|
38
|
+
t = f"[b]{t}[/b]"
|
|
39
|
+
if s.italic:
|
|
40
|
+
t = f"[i]{t}[/i]"
|
|
41
|
+
if s.underline:
|
|
42
|
+
t = f"[u]{t}[/u]"
|
|
43
|
+
if s.strike:
|
|
44
|
+
t = f"[s]{t}[/s]"
|
|
45
|
+
out.append(f"[color={hexcol}]{t}[/color]")
|
|
46
|
+
return "".join(out)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_measure_cache: dict = {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def make_label(markup: str, style: NativeStyle, px, max_w: float | None, halign: str = "left") -> CoreLabel:
|
|
53
|
+
size = max(1, int(round(px(style.font_size))))
|
|
54
|
+
kwargs = dict(text=markup, markup=True, font_size=size, halign=halign, bold=style.bold,
|
|
55
|
+
italic=style.italic, text_size=(max_w, None))
|
|
56
|
+
fn = _font_name(style.font_family)
|
|
57
|
+
if fn:
|
|
58
|
+
kwargs["font_name"] = fn
|
|
59
|
+
lh = style.line_height
|
|
60
|
+
if isinstance(lh, LineHeightMultiple):
|
|
61
|
+
kwargs["line_height"] = lh.value
|
|
62
|
+
elif isinstance(lh, LineHeightFixed):
|
|
63
|
+
kwargs["line_height"] = px(lh.length) / max(size, 1)
|
|
64
|
+
label = CoreLabel(**kwargs)
|
|
65
|
+
label.refresh()
|
|
66
|
+
return label
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def measure_text(markup: str, style: NativeStyle, px, max_w: float | None) -> tuple[float, float]:
|
|
70
|
+
"""Natural size of the text; if it does not fit in `max_w`, it wraps and returns the widest line."""
|
|
71
|
+
if not markup:
|
|
72
|
+
return 0.0, px(style.font_size) * 1.2
|
|
73
|
+
key = (markup, style.font_size, style.bold, style.italic, style.font_family, style.line_height,
|
|
74
|
+
None if max_w is None else round(max_w, 1), px(style.font_size))
|
|
75
|
+
hit = _measure_cache.get(key)
|
|
76
|
+
if hit is not None:
|
|
77
|
+
return hit
|
|
78
|
+
label = make_label(markup, style, px, None)
|
|
79
|
+
w, h = label.content_size if label.texture else (0, 0)
|
|
80
|
+
if max_w is not None and w > max_w:
|
|
81
|
+
label = make_label(markup, style, px, max_w)
|
|
82
|
+
lines = getattr(label, "_cached_lines", None)
|
|
83
|
+
w = max((ln.w for ln in lines), default=max_w) if lines else max_w
|
|
84
|
+
h = label.content_size[1] if label.texture else h
|
|
85
|
+
if len(_measure_cache) > 2000:
|
|
86
|
+
_measure_cache.clear()
|
|
87
|
+
_measure_cache[key] = (float(w), float(h))
|
|
88
|
+
return float(w), float(h)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def text_gravity(style: NativeStyle, is_button: bool) -> tuple[str, str]:
|
|
92
|
+
"""(horizontal, vertical) = ('left'|'center'|'right', 'top'|'center'|'bottom'), like the Android host."""
|
|
93
|
+
if is_button:
|
|
94
|
+
h = {"CENTER": "center", "END": "right"}.get(style.text_align.name, "left")
|
|
95
|
+
return h, "center"
|
|
96
|
+
h = {"CENTER": "center", "END": "right"}.get(style.text_align.name, "left")
|
|
97
|
+
if style.direction == Direction.COLUMN:
|
|
98
|
+
if style.align_items != Align.STRETCH and style.text_align == TextAlign.START:
|
|
99
|
+
h = {"CENTER": "center", "END": "right"}.get(style.align_items.name, "left")
|
|
100
|
+
v = {"CENTER": "center", "END": "bottom"}.get(style.justify.name, "top")
|
|
101
|
+
else:
|
|
102
|
+
if style.justify != Justify.START:
|
|
103
|
+
h = {"CENTER": "center", "END": "right"}.get(style.justify.name, "left")
|
|
104
|
+
v = {"CENTER": "center", "END": "bottom"}.get(style.align_items.name, "top")
|
|
105
|
+
return h, v
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------- base
|
|
109
|
+
class NLWidget(Widget):
|
|
110
|
+
"""A widget whose background we draw. `entry` links it to the model of the host."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, entry, **kw):
|
|
113
|
+
super().__init__(size_hint=(None, None), **kw)
|
|
114
|
+
self.entry = entry
|
|
115
|
+
self.pressed = False
|
|
116
|
+
self._touch_start = None
|
|
117
|
+
self._moved = False
|
|
118
|
+
|
|
119
|
+
# drawing: subclasses add on top of the background
|
|
120
|
+
def redraw(self, px) -> None:
|
|
121
|
+
e = self.entry
|
|
122
|
+
st = e.style
|
|
123
|
+
self.canvas.before.clear()
|
|
124
|
+
self.canvas.clear()
|
|
125
|
+
bg = border = None
|
|
126
|
+
states = st.states
|
|
127
|
+
if self.pressed and states and states.pressed:
|
|
128
|
+
bg = states.pressed.background_color
|
|
129
|
+
border = states.pressed.border_color
|
|
130
|
+
elif e.is_disabled() and states and states.disabled:
|
|
131
|
+
bg = states.disabled.background_color
|
|
132
|
+
border = states.disabled.border_color
|
|
133
|
+
elif getattr(self, "focused", False) and states and states.focused:
|
|
134
|
+
bg = states.focused.background_color
|
|
135
|
+
border = states.focused.border_color
|
|
136
|
+
draw.draw_box(self.canvas.before, self.x, self.y, self.width, self.height, st, px, bg, border,
|
|
137
|
+
image=e.host.background_texture(e))
|
|
138
|
+
self.draw_content(px)
|
|
139
|
+
self.opacity = e.effective_opacity()
|
|
140
|
+
|
|
141
|
+
def draw_content(self, px) -> None:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# touches
|
|
145
|
+
def wants_touch(self) -> bool:
|
|
146
|
+
return self.entry.interactive() and not self.entry.is_disabled()
|
|
147
|
+
|
|
148
|
+
def on_touch_down(self, touch):
|
|
149
|
+
if not self.collide_point(*touch.pos):
|
|
150
|
+
return False
|
|
151
|
+
if self.entry.is_layer:
|
|
152
|
+
if self.wants_touch():
|
|
153
|
+
pass
|
|
154
|
+
self._touch_start = touch.pos
|
|
155
|
+
self._moved = False
|
|
156
|
+
touch.grab(self)
|
|
157
|
+
return True
|
|
158
|
+
if not self.wants_touch():
|
|
159
|
+
return False
|
|
160
|
+
self._touch_start = touch.pos
|
|
161
|
+
self._moved = False
|
|
162
|
+
self.pressed = True
|
|
163
|
+
touch.grab(self)
|
|
164
|
+
self.entry.host.redraw_entry(self.entry)
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
def on_touch_move(self, touch):
|
|
168
|
+
if touch.grab_current is self and self._touch_start is not None:
|
|
169
|
+
dx = touch.pos[0] - self._touch_start[0]
|
|
170
|
+
dy = touch.pos[1] - self._touch_start[1]
|
|
171
|
+
if abs(dx) > TAP_SLOP or abs(dy) > TAP_SLOP:
|
|
172
|
+
self._moved = True
|
|
173
|
+
if self.pressed:
|
|
174
|
+
self.pressed = False
|
|
175
|
+
self.entry.host.redraw_entry(self.entry)
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
def on_touch_up(self, touch):
|
|
179
|
+
if touch.grab_current is self:
|
|
180
|
+
touch.ungrab(self)
|
|
181
|
+
was = self.pressed
|
|
182
|
+
self.pressed = False
|
|
183
|
+
if was:
|
|
184
|
+
self.entry.host.redraw_entry(self.entry)
|
|
185
|
+
if was and not self._moved and self.collide_point(*touch.pos):
|
|
186
|
+
self.on_tap()
|
|
187
|
+
return True
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
def on_tap(self) -> None:
|
|
191
|
+
self.entry.host.publish(self.entry.node.key)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class NLBoxWidget(NLWidget):
|
|
195
|
+
def draw_content(self, px) -> None:
|
|
196
|
+
e = self.entry
|
|
197
|
+
mb = e.marker_box
|
|
198
|
+
if mb is None:
|
|
199
|
+
return
|
|
200
|
+
mx, my, mw, mh = mb
|
|
201
|
+
label = make_label(escape_markup(e.node.marker + " "), e.style, px, None)
|
|
202
|
+
tex = label.texture
|
|
203
|
+
if tex is None:
|
|
204
|
+
return
|
|
205
|
+
self.canvas.add(Color(*draw.rgba(e.style.text_color)))
|
|
206
|
+
self.canvas.add(Rectangle(texture=tex, pos=(self.x + mx, self.y + self.height - my - tex.size[1]),
|
|
207
|
+
size=tex.size))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class NLTextWidget(NLWidget):
|
|
211
|
+
"""Text (and buttons): draws the text inside the content area, with the gravity of the style."""
|
|
212
|
+
|
|
213
|
+
def draw_content(self, px) -> None:
|
|
214
|
+
e = self.entry
|
|
215
|
+
st = e.style
|
|
216
|
+
markup = e.markup()
|
|
217
|
+
if not markup:
|
|
218
|
+
return
|
|
219
|
+
pl, pr = px(st.padding.left), px(st.padding.right)
|
|
220
|
+
pt, pb = px(st.padding.top), px(st.padding.bottom)
|
|
221
|
+
cw = max(self.width - pl - pr, 1.0)
|
|
222
|
+
ch = max(self.height - pt - pb, 1.0)
|
|
223
|
+
hal, val = text_gravity(st, e.is_button)
|
|
224
|
+
label = make_label(markup, st, px, cw, hal)
|
|
225
|
+
tex = label.texture
|
|
226
|
+
if tex is None:
|
|
227
|
+
return
|
|
228
|
+
tw, th = tex.size
|
|
229
|
+
x = self.x + pl
|
|
230
|
+
if hal == "center":
|
|
231
|
+
x += (cw - tw) / 2
|
|
232
|
+
elif hal == "right":
|
|
233
|
+
x += cw - tw
|
|
234
|
+
if val == "center":
|
|
235
|
+
y = self.y + pb + (ch - th) / 2
|
|
236
|
+
elif val == "bottom":
|
|
237
|
+
y = self.y + pb
|
|
238
|
+
else:
|
|
239
|
+
y = self.y + pb + ch - th
|
|
240
|
+
self.canvas.add(Color(1, 1, 1, 1))
|
|
241
|
+
self.canvas.add(Rectangle(texture=tex, pos=(x, y), size=(tw, th)))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class NLCheckWidget(NLWidget):
|
|
245
|
+
def __init__(self, entry, **kw):
|
|
246
|
+
super().__init__(entry, **kw)
|
|
247
|
+
self.active = False
|
|
248
|
+
|
|
249
|
+
def draw_content(self, px) -> None:
|
|
250
|
+
e = self.entry
|
|
251
|
+
st = e.style
|
|
252
|
+
s = min(self.width, self.height)
|
|
253
|
+
x = self.x + (self.width - s) / 2
|
|
254
|
+
y = self.y + (self.height - s) / 2
|
|
255
|
+
box = s * 0.72
|
|
256
|
+
bx, by = x + (s - box) / 2, y + (s - box) / 2
|
|
257
|
+
if self.active:
|
|
258
|
+
self.canvas.add(Color(*draw.rgba(0xFF2F81F7)))
|
|
259
|
+
self.canvas.add(draw.RoundedRectangle(pos=(bx, by), size=(box, box), radius=[box * 0.15] * 4))
|
|
260
|
+
self.canvas.add(Color(1, 1, 1, 1))
|
|
261
|
+
self.canvas.add(Line(points=[bx + box * 0.2, by + box * 0.5, bx + box * 0.42, by + box * 0.28,
|
|
262
|
+
bx + box * 0.8, by + box * 0.74],
|
|
263
|
+
width=max(1.5, box * 0.12), cap="round", joint="round"))
|
|
264
|
+
else:
|
|
265
|
+
self.canvas.add(Color(*draw.rgba(0xFF767676)))
|
|
266
|
+
self.canvas.add(Line(rounded_rectangle=(bx, by, box, box, box * 0.15), width=max(1.5, box * 0.09)))
|
|
267
|
+
|
|
268
|
+
def on_tap(self) -> None:
|
|
269
|
+
self.active = not self.active
|
|
270
|
+
self.entry.host.redraw_entry(self.entry)
|
|
271
|
+
if self.entry.interactive():
|
|
272
|
+
self.entry.host.publish(self.entry.node.key)
|
|
273
|
+
|
|
274
|
+
def wants_touch(self) -> bool:
|
|
275
|
+
return not self.entry.is_disabled()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class NLImageWidget(NLWidget):
|
|
279
|
+
def draw_content(self, px) -> None:
|
|
280
|
+
e = self.entry
|
|
281
|
+
st = e.style
|
|
282
|
+
icon = e.icon_name()
|
|
283
|
+
if icon is not None:
|
|
284
|
+
s = min(self.width, self.height)
|
|
285
|
+
draw.draw_icon(self.canvas, icon, self.x + (self.width - s) / 2, self.y + (self.height - s) / 2,
|
|
286
|
+
s, st.text_color)
|
|
287
|
+
return
|
|
288
|
+
tex = e.texture
|
|
289
|
+
if tex is None:
|
|
290
|
+
self.canvas.add(Color(*draw.rgba(0xFFDDDDDD)))
|
|
291
|
+
self.canvas.add(Rectangle(pos=self.pos, size=self.size))
|
|
292
|
+
return
|
|
293
|
+
# object-fit: contain (default), cover or fill; the image is always centered
|
|
294
|
+
draw.draw_image(self.canvas, tex, self.x, self.y, self.width, self.height, st.object_fit, ImagePos.CENTER)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class NLFieldWidget(TextInput):
|
|
298
|
+
"""Text field: we draw the background and the border; the TextInput only shows the text."""
|
|
299
|
+
|
|
300
|
+
def __init__(self, entry, **kw):
|
|
301
|
+
super().__init__(size_hint=(None, None), background_normal="", background_active="",
|
|
302
|
+
background_disabled_normal="",
|
|
303
|
+
background_color=(0, 0, 0, 0), write_tab=False, **kw)
|
|
304
|
+
self.entry = entry
|
|
305
|
+
self.focused = False
|
|
306
|
+
self.bind(focus=self._on_focus)
|
|
307
|
+
|
|
308
|
+
def _on_focus(self, _w, value) -> None:
|
|
309
|
+
self.focused = bool(value)
|
|
310
|
+
self.entry.host.redraw_entry(self.entry)
|
|
311
|
+
|
|
312
|
+
def redraw(self, px) -> None:
|
|
313
|
+
e = self.entry
|
|
314
|
+
st = e.style
|
|
315
|
+
self.canvas.before.clear()
|
|
316
|
+
bg = border = None
|
|
317
|
+
states = st.states
|
|
318
|
+
if e.is_disabled() and states and states.disabled:
|
|
319
|
+
bg, border = states.disabled.background_color, states.disabled.border_color
|
|
320
|
+
elif self.focused and states and states.focused:
|
|
321
|
+
bg, border = states.focused.background_color, states.focused.border_color
|
|
322
|
+
draw.draw_box(self.canvas.before, self.x, self.y, self.width, self.height, st, px, bg, border,
|
|
323
|
+
image=e.host.background_texture(e))
|
|
324
|
+
pad = [px(st.padding.left), px(st.padding.top), px(st.padding.right), px(st.padding.bottom)]
|
|
325
|
+
self.padding = pad
|
|
326
|
+
self.font_size = max(1, int(round(px(st.font_size))))
|
|
327
|
+
self.foreground_color = draw.rgba(st.text_color)
|
|
328
|
+
self.cursor_color = draw.rgba(st.text_color)
|
|
329
|
+
self.hint_text_color = draw.rgba((st.text_color & 0x00FFFFFF) | 0x80000000)
|
|
330
|
+
self.opacity = e.effective_opacity()
|
|
331
|
+
self.disabled = e.is_disabled()
|
|
332
|
+
fn = _font_name(st.font_family)
|
|
333
|
+
if fn:
|
|
334
|
+
self.font_name = fn
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nativelayout
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Create native interfaces from HTML, in any language. Python core and Kivy host.
|
|
5
|
+
Author: Adilson C. Rafael
|
|
6
|
+
License-Expression: MPL-2.0
|
|
7
|
+
Project-URL: Homepage, https://getnativelayout.github.io/nativelayout/
|
|
8
|
+
Project-URL: Source, https://github.com/getnativelayout/nativelayout
|
|
9
|
+
Project-URL: Issues, https://github.com/getnativelayout/nativelayout/issues
|
|
10
|
+
Project-URL: Specification, https://github.com/getnativelayout/nativelayout/blob/main/SPEC.md
|
|
11
|
+
Keywords: ui,layout,html,css,native,kivy,android
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: kivy
|
|
24
|
+
Requires-Dist: kivy>=2.0; extra == "kivy"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# nativelayout (Python)
|
|
28
|
+
|
|
29
|
+
Python port of the nativelayout core. It follows the Kotlin core feature for feature
|
|
30
|
+
(currently v1.2.0: `<include>`, identity inside `for`, `<footer>` and `dock`,
|
|
31
|
+
`position: absolute`, `background-image`, `object-fit`).
|
|
32
|
+
|
|
33
|
+
- `core/` pure Python, no Kivy: parser, expander, layout engine, differ, program side
|
|
34
|
+
- `android/` Kivy host for Android (`nativelayout/host`)
|
|
35
|
+
- `desktop/` Kivy host for the desktop (`nativelayout/host`)
|
|
36
|
+
- `examples/` examples (`contador/`)
|
|
37
|
+
|
|
38
|
+
The Kivy hosts draw on a canvas; they do not build platform-native views.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
pip install nativelayout # core only (pure Python, no dependencies)
|
|
44
|
+
pip install "nativelayout[kivy]" # core + Kivy host
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Requires Python 3.10 or newer. On Android, package with buildozer (`requirements = python3,kivy,nativelayout`).
|
|
48
|
+
|
|
49
|
+
`nativelayout` is a namespace package (no `__init__.py` at its root): the core is `nativelayout.core` and the Kivy host is `nativelayout.host`. The package published on PyPI carries the Android host (`android/`), which includes the stencil mask that rounds `background-image` corners. To work from the repository instead, put `core` and the folder of the host on `sys.path`.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import kivy # noqa: F401 (Pydroid starts the Kivy window only if the opened file imports kivy)
|
|
55
|
+
from nativelayout.core.program import state, view, click
|
|
56
|
+
from nativelayout.host import run_app, file_loader
|
|
57
|
+
|
|
58
|
+
def screen():
|
|
59
|
+
s = state({"n": 0})
|
|
60
|
+
if click(".more"):
|
|
61
|
+
s["n"] += 1
|
|
62
|
+
return view("screen.nl", s)
|
|
63
|
+
|
|
64
|
+
run_app(file_loader("."), app=screen)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The host needs Kivy 2.x.
|
|
68
|
+
|
|
69
|
+
## Tests
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
cd python/core
|
|
73
|
+
python -m unittest discover -s tests -t .
|
|
74
|
+
|
|
75
|
+
cd python/android
|
|
76
|
+
python -m unittest discover -s tests -t .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The core tests mirror the Kotlin ones (`IncludeTest`, `FooterTest`, `AbsoluteTest`). The host tests
|
|
80
|
+
cover `flex.py`, which does not need Kivy.
|
|
81
|
+
|
|
82
|
+
## Status of the host (v1.2.0)
|
|
83
|
+
|
|
84
|
+
Done: columns and rows, `flex`, `gap`, `justify`, `align`, wrap, header, footer and `dock`, dialogs,
|
|
85
|
+
`position: absolute` (with `z-index`), `background-image` (`background-size`, `background-position`),
|
|
86
|
+
`object-fit`, icons, gradients, shadows, rounded corners.
|
|
87
|
+
|
|
88
|
+
Not done yet:
|
|
89
|
+
- `virtual` lists draw all items (no recycling)
|
|
90
|
+
- animations: only the fade-in of the screen (slide-up and slide-left are missing)
|
|
91
|
+
- `letter-spacing` and `overflow: hidden` (clipping) are not applied
|
|
92
|
+
- the Android keyboard can cover fields (adjust `softinput_mode`)
|
|
93
|
+
- the Kivy drawing of `background-image` and `object-fit` has not been tried on a device yet
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
nativelayout/core/__init__.py,sha256=xpPrQJud9pdJlfRBK0vtVHdHljWZb0h9-fjB5h3CAD0,812
|
|
2
|
+
nativelayout/core/css.py,sha256=xjMq7L3mF0Eo9FK8xQzY1tdehhdS-IER62kIZUKgNeU,6550
|
|
3
|
+
nativelayout/core/differ.py,sha256=9YLMgvde1yhkp6IC_NGdWDJ61ca4oNiPLVNxuMcLSlY,4353
|
|
4
|
+
nativelayout/core/errors.py,sha256=USVPk1OZXgOHHWlkd1jwh_tCiH1zoqyFcEdCmPj-sP8,841
|
|
5
|
+
nativelayout/core/expander.py,sha256=6bzHcf9xdRWCSHQqrLR_K7TtxrK54fjltpyRwLaoZYo,50088
|
|
6
|
+
nativelayout/core/html_parser.py,sha256=Y4SZUrygqWF7yZ7TrGRwcy2mOGsZz3dE4X5p3BuPz-I,11122
|
|
7
|
+
nativelayout/core/icons.py,sha256=irZeRjXkISW6uLRxOVXyum3FtmonmmLnVjlVZPyeDfg,3396
|
|
8
|
+
nativelayout/core/layout_engine.py,sha256=8OiR3YSiLn2rXEqpOdtNfsrklF21VJ60wp2P3B8PDws,44621
|
|
9
|
+
nativelayout/core/layout_host.py,sha256=yi3XLHkXNo2b1h0fEbRnM4N-AccQxhWG-b5gj_Nkt3E,8481
|
|
10
|
+
nativelayout/core/program.py,sha256=B-BvyQVUoSY6Kk13wf0uoK2AUxgEMjoXSrgE5z-J8Yg,10286
|
|
11
|
+
nativelayout/core/scopes.py,sha256=jKFn-vUT8VfKNAjT3I7exUy88K5JYqtg6Qn2n3NsEEg,1592
|
|
12
|
+
nativelayout/core/tree.py,sha256=uJEghKKQgS7-yAD4ZU3XeGJ-qop-kG1VZ1HZqrIH35A,9811
|
|
13
|
+
nativelayout/host/__init__.py,sha256=raatzN60CQboJNdy2F-5biQ9Z5ae28huMcSpyoQ16yw,211
|
|
14
|
+
nativelayout/host/draw.py,sha256=HthL22HER8EpPZoviV588xx3uIZyigAJj7edHSKzmgc,9132
|
|
15
|
+
nativelayout/host/flex.py,sha256=5cMDWGAcRvkS8ovF4Ktfih7SSIySTqd1wQeegSAItfc,14597
|
|
16
|
+
nativelayout/host/kivy_host.py,sha256=8Q_U8zHo7kPP1DfBHF_1UFRpIpzUihKAWi9-NS1Igd4,25045
|
|
17
|
+
nativelayout/host/native_view.py,sha256=T0bgw-oGfRxap2JJDd-yjvY3Tc4fNzWoriBMjhOKoEo,3033
|
|
18
|
+
nativelayout/host/widgets.py,sha256=V6-AcDM24QGUla52eqiV-L1lDcT5nP7Cw4NMWYKJaow,13114
|
|
19
|
+
nativelayout-1.2.0.dist-info/licenses/LICENSE,sha256=ZqMQfVrWoFiqt1PqrCBHzLLtDjlGXdD-WETaPjANUXI,16727
|
|
20
|
+
nativelayout-1.2.0.dist-info/METADATA,sha256=ay1xlcjwpnsBNsPakmWQ7jUrSMgi1qcoG90lCC6_2c8,3700
|
|
21
|
+
nativelayout-1.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
22
|
+
nativelayout-1.2.0.dist-info/top_level.txt,sha256=Nwt6HOc6nsE7dnYAJCmq4uiKWLGT-9Dyur_8iCGDh5g,13
|
|
23
|
+
nativelayout-1.2.0.dist-info/RECORD,,
|