Skip to content

Commit 4247386

Browse files
committed
feat(layout): add pure-Python flexbox engine; rewrite native containers
1 parent fa53206 commit 4247386

34 files changed

Lines changed: 4745 additions & 597 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
3333
- **Declarative UI:** Describe *what* your UI should look like with element functions (`Text`, `Button`, `Column`, `Row`, etc.). PythonNative creates and updates native views automatically.
3434
- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
3535
- **`style` prop:** Pass all visual and layout properties through a single `style` dict, composable via `StyleSheet`.
36+
- **Cross-platform flexbox engine:** A pure-Python, Yoga-style layout engine computes frames once and applies them to native views, so `flex`, `padding`, `aspect_ratio`, and `position: "absolute"` produce the same geometry on Android and iOS.
3637
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
3738
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
3839
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.

docs/api/component-properties.md

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,57 @@
22

33
All visual and layout properties are passed via the `style` dict (or list of dicts) to element functions. Behavioural properties (callbacks, data, content) remain as keyword arguments.
44

5+
> Layout is computed by PythonNative's pure-Python flexbox engine (see
6+
> [Layout engine](../concepts/layout.md)). The same `style` keys produce the
7+
> same frames on Android and iOS — every property listed below is honoured by
8+
> the engine and applied via `set_frame` on the underlying native view.
9+
510
## Common layout properties (inside `style`)
611

712
All components accept these layout properties in their `style` dict:
813

9-
- `width` — fixed width in dp (Android) / pt (iOS)
10-
- `height` — fixed height
11-
- `flex` — flex grow factor (shorthand for `flex_grow`)
12-
- `flex_grow` — how much a child grows to fill available space
13-
- `flex_shrink` — how much a child shrinks when space is limited
14-
- `margin` — outer spacing (int, float, or dict with `horizontal`, `vertical`, `left`, `top`, `right`, `bottom`)
15-
- `min_width`, `max_width` — width constraints
16-
- `min_height`, `max_height` — height constraints
17-
- `align_self` — override parent alignment (`"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`)
18-
- `key` — stable identity for reconciliation (passed as a kwarg, not inside `style`)
14+
### Sizing
15+
16+
- `width` — fixed width in dp (Android) / pt (iOS). Accepts an `int`/`float`,
17+
or a percentage string like `"50%"` (resolved against the parent's content
18+
width).
19+
- `height` — fixed height. Same number / percentage rules as `width`.
20+
- `min_width`, `max_width` — width constraints (numbers or `"%"` strings).
21+
- `min_height`, `max_height` — height constraints.
22+
- `aspect_ratio` — width / height ratio. When only one of `width` / `height`
23+
is known, the other axis is derived from this ratio.
24+
25+
### Flex
26+
27+
- `flex` — shorthand. Setting `flex: N` is equivalent to
28+
`flex_grow: N, flex_shrink: 1, flex_basis: 0`.
29+
- `flex_grow` — how much a child grows to fill remaining main-axis space.
30+
- `flex_shrink` — how much a child shrinks when its parent runs out of space.
31+
- `flex_basis` — initial main-axis size before grow / shrink is applied
32+
(`"auto"`, a number, or a percentage string).
33+
- `align_self` — override parent alignment for this child (`"auto"`,
34+
`"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`).
35+
36+
### Spacing
37+
38+
- `margin` — outer spacing. Accepts a number (all sides), or a dict with any
39+
of `horizontal`, `vertical`, `left`, `top`, `right`, `bottom`.
40+
- `padding` — inner spacing on container elements. Same shape as `margin`.
41+
- `spacing` — gap between children of a flex container (applied along the
42+
main axis).
43+
- `gap` — alias for `spacing`.
44+
45+
### Position
46+
47+
- `position``"relative"` (default) or `"absolute"`. Absolute children are
48+
removed from the normal flow and placed using `top` / `right` / `bottom` /
49+
`left` (numbers or percentage strings).
50+
- `top`, `right`, `bottom`, `left` — offsets used when `position: "absolute"`.
51+
52+
### Other
53+
54+
- `key` — stable identity for reconciliation (passed as a kwarg, not inside
55+
`style`).
1956

2057
## View
2158

@@ -41,6 +78,18 @@ Flex container properties (inside `style`):
4178
- `overflow``"visible"` (default), `"hidden"`
4279
- `spacing`, `padding`, `background_color`
4380

81+
Containers also fully support absolute positioning for their children:
82+
83+
```python
84+
pn.View(
85+
pn.View(style={"position": "absolute", "top": 0, "left": 0,
86+
"width": 40, "height": 40, "background_color": "#F00"}),
87+
pn.View(style={"position": "absolute", "bottom": 8, "right": 8,
88+
"width": 40, "height": 40, "background_color": "#0A0"}),
89+
style={"width": 200, "height": 200, "background_color": "#EEE"},
90+
)
91+
```
92+
4493
## Text
4594

4695
```python

docs/api/layout.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Layout
2+
3+
The pure-Python flexbox engine that computes a frame
4+
`(x, y, width, height)` for every node in the rendered tree. The
5+
[`Reconciler`][pythonnative.reconciler.Reconciler] runs
6+
[`calculate_layout`][pythonnative.layout.calculate_layout] after every
7+
commit and forwards the resulting frames to the platform handlers via
8+
`set_frame`.
9+
10+
For a conceptual overview, see [Layout engine](../concepts/layout.md).
11+
12+
::: pythonnative.layout
13+
options:
14+
show_root_heading: false
15+
show_root_toc_entry: false
16+
members_order: source
17+
filters: ["!^_"]
18+
19+
## Next steps
20+
21+
- Browse the supported style keys:
22+
[Component properties](component-properties.md).
23+
- See how leaf widgets contribute their intrinsic size:
24+
[Native views](native_views.md).
25+
- Read the conceptual walkthrough:
26+
[Layout engine](../concepts/layout.md).

docs/concepts/architecture.md

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,23 @@ component. App code does not call it directly.
132132

133133
## Layout
134134

135-
PythonNative uses a **flexbox-inspired layout model** built on
136-
platform-native layout managers.
135+
PythonNative ships its own **pure-Python flexbox engine** (a small,
136+
React-Native-compatible re-implementation of Yoga's algorithm). All
137+
layout decisions are made in Python and then pushed to native views as
138+
absolute frames via `set_frame`. This means the *exact same* layout
139+
rules apply on Android and iOS — there is no platform drift between
140+
`LinearLayout` and `UIStackView`.
141+
142+
The engine is implemented in `pythonnative.layout` and runs as a
143+
dedicated **layout pass** after every commit:
144+
145+
```text
146+
render -> commit (create / update native views)
147+
-> flush effects
148+
-> build LayoutNode tree from VNodes
149+
-> calculate_layout(viewport_w, viewport_h)
150+
-> backend.set_frame(view, x, y, w, h) for every node
151+
```
137152

138153
[`View`][pythonnative.View] is the **universal flex container** (like
139154
React Native's `View`). It defaults to `flex_direction: "column"`.
@@ -155,19 +170,32 @@ convenience wrappers that fix the direction.
155170

156171
### Child layout properties
157172

158-
- `flex`: flex grow factor (shorthand).
159-
- `flex_grow`, `flex_shrink`: individual flex properties.
173+
- `flex`: shorthand for `flex_grow: N, flex_shrink: 1, flex_basis: 0`.
174+
- `flex_grow`, `flex_shrink`, `flex_basis`: individual flex properties.
160175
- `align_self`: override the parent's `align_items` for this child.
161-
- `width`, `height`: fixed dimensions.
162-
- `min_width`, `min_height`: minimum size constraints.
176+
- `width`, `height`: fixed dimensions (numbers or `"%"` strings).
177+
- `min_width`, `min_height`, `max_width`, `max_height`: size
178+
constraints.
179+
- `aspect_ratio`: derive the unknown axis from the known one.
163180
- `margin`: outer spacing.
181+
- `position`: `"relative"` (default) or `"absolute"`. Absolute children
182+
are removed from the flex flow and positioned via `top` / `right` /
183+
`bottom` / `left`.
164184

165185
Under the hood:
166186

167-
- **Android**: `LinearLayout` with gravity, weights, and
168-
divider-based spacing.
169-
- **iOS**: `UIStackView` with axis, alignment, distribution, and
170-
layout margins.
187+
- **Layout**: `pythonnative.layout.calculate_layout` computes a frame
188+
`(x, y, w, h)` for every node.
189+
- **Android**: every container is a `FrameLayout`; computed frames are
190+
applied through `MarginLayoutParams` and `View.setX/setY/setLayoutParams`.
191+
- **iOS**: every container is a plain `UIView` with
192+
`translatesAutoresizingMaskIntoConstraints = NO`; computed frames are
193+
applied through `view.frame = CGRect(...)`.
194+
- **Intrinsic content size**: leaf widgets (`Text`, `Button`, `Image`,
195+
`TextInput`, …) implement `measure_intrinsic` so the engine can ask
196+
them how big they want to be when no explicit size is set.
197+
198+
See the [Layout engine](layout.md) concept page for a full walkthrough.
171199

172200
## Native view handlers
173201

@@ -177,16 +205,26 @@ submodules:
177205

178206
- `native_views.base`: shared
179207
[`ViewHandler`][pythonnative.native_views.base.ViewHandler] protocol
180-
and common utilities (color parsing, padding resolution, layout
181-
keys, flex constants).
208+
and common utilities (color parsing, padding resolution, container
209+
visual keys).
182210
- `native_views.android`: Android handlers using Chaquopy's Java
183211
bridge (`jclass`, `dynamic_proxy`).
184212
- `native_views.ios`: iOS handlers using rubicon-objc
185213
(`ObjCClass`, `objc_method`).
186214

215+
Every handler implements two layout-facing methods:
216+
217+
- `set_frame(view, x, y, width, height)` — apply an absolute frame
218+
computed by the layout engine.
219+
- `measure_intrinsic(view, max_width, max_height)` — return the
220+
natural content size for leaf widgets (used as a hint by the layout
221+
engine).
222+
187223
`Column`, `Row`, and `View` share a single flex-container handler on
188-
each platform. The handler reads `flex_direction` from the element's
189-
props to configure the native layout container.
224+
each platform. Containers are simple `FrameLayout` (Android) /
225+
`UIView` (iOS) instances; all flex math lives in
226+
`pythonnative.layout`, so the handlers themselves contain no layout
227+
logic.
190228

191229
Each handler class maps an element type name (e.g., `"Text"`,
192230
`"Button"`) to platform-native widget creation, property updates, and
@@ -200,8 +238,9 @@ package can be imported on any platform for testing.
200238
!!! note "Versus React Native"
201239
React Native uses JSX plus a JavaScript bridge (or JSI in newer
202240
versions) plus Yoga layout. PythonNative uses Python plus direct
203-
native calls plus platform layout managers; no JS bridge, no
204-
serialization overhead.
241+
native calls plus a Python-implemented Yoga-style flex engine; no
242+
JS bridge, no serialization overhead, and the same layout rules on
243+
both platforms.
205244

206245
!!! note "Versus NativeScript"
207246
NativeScript shares the philosophy of direct, synchronous native
@@ -287,5 +326,6 @@ See the [Navigation guide](../guides/navigation.md) for full details.
287326
- Read the [Mental model](mental-model.md) for the high-level
288327
comparisons.
289328
- Walk through the render loop in [Lifecycle](lifecycle.md).
329+
- Dive into the flexbox engine in [Layout engine](layout.md).
290330
- See the platform handlers up close in [Native views](native-views.md).
291331
- Browse the API: [Package overview](../api/pythonnative.md).

0 commit comments

Comments
 (0)