Skip to content

Commit 094d997

Browse files
committed
feat(native_views,components)!: add flexbox-inspired layout system
1 parent d0068fd commit 094d997

File tree

13 files changed

+598
-253
lines changed

13 files changed

+598
-253
lines changed

docs/api/component-properties.md

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,39 @@ All components accept these layout properties in their `style` dict:
88

99
- `width` — fixed width in dp (Android) / pt (iOS)
1010
- `height` — fixed height
11-
- `flex` — flex grow factor within Column/Row
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
1214
- `margin` — outer spacing (int, float, or dict with `horizontal`, `vertical`, `left`, `top`, `right`, `bottom`)
1315
- `min_width`, `max_width` — width constraints
1416
- `min_height`, `max_height` — height constraints
15-
- `align_self` — override parent alignment (`"fill"`, `"center"`, etc.)
17+
- `align_self` — override parent alignment (`"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`)
1618
- `key` — stable identity for reconciliation (passed as a kwarg, not inside `style`)
1719

20+
## View
21+
22+
```python
23+
pn.View(*children, style={
24+
"flex_direction": "column",
25+
"justify_content": "center",
26+
"align_items": "center",
27+
"overflow": "hidden",
28+
"spacing": 8,
29+
"padding": 16,
30+
"background_color": "#F5F5F5",
31+
})
32+
```
33+
34+
Universal flex container (like React Native's `View`). Defaults to `flex_direction: "column"`.
35+
36+
Flex container properties (inside `style`):
37+
38+
- `flex_direction``"column"` (default), `"row"`, `"column_reverse"`, `"row_reverse"`
39+
- `justify_content``"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"`
40+
- `align_items``"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`
41+
- `overflow``"visible"` (default), `"hidden"`
42+
- `spacing`, `padding`, `background_color`
43+
1844
## Text
1945

2046
```python
@@ -42,23 +68,20 @@ pn.Column(*children, style={"spacing": 12, "padding": 16, "align_items": "center
4268
pn.Row(*children, style={"spacing": 8, "justify_content": "space_between"})
4369
```
4470

71+
Convenience wrappers for `View` with fixed `flex_direction`:
72+
73+
- `Column` = `View` with `flex_direction: "column"` (always vertical)
74+
- `Row` = `View` with `flex_direction: "row"` (always horizontal)
75+
4576
- `*children` — child elements (positional)
4677
- Style properties:
4778
- `spacing` — gap between children (dp / pt)
4879
- `padding` — inner padding (int for all sides, or dict with `horizontal`, `vertical`, `left`, `top`, `right`, `bottom`)
49-
- `alignment` — cross-axis alignment shorthand
50-
- `align_items`cross-axis alignment: `"fill"`, `"center"`, `"leading"` / `"start"`, `"trailing"` / `"end"`, `"stretch"`
51-
- `justify_content`main-axis distribution: `"start"`, `"center"`, `"end"`, `"space_between"`, `"space_around"`
80+
- `align_items` — cross-axis alignment: `"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`, `"leading"`, `"trailing"`
81+
- `justify_content`main-axis distribution: `"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"`
82+
- `overflow``"visible"` (default), `"hidden"`
5283
- `background_color` — container background
5384

54-
## View
55-
56-
```python
57-
pn.View(*children, style={"background_color": "#F5F5F5", "padding": 16})
58-
```
59-
60-
Generic container (UIView / FrameLayout). Supports all layout properties in `style`.
61-
6285
## SafeAreaView
6386

6487
```python

docs/concepts/architecture.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,42 @@ The entry point `create_page()` is called internally by native templates to boot
5454

5555
## Layout
5656

57-
All components support layout properties inside the `style` dict: `width`, `height`, `flex`, `margin`, `min_width`, `max_width`, `min_height`, `max_height`, `align_self`. Containers (`Column`, `Row`) support `spacing`, `padding`, `alignment`, `align_items`, and `justify_content`.
57+
PythonNative uses a **flexbox-inspired layout model** built on platform-native layout managers.
58+
59+
`View` is the **universal flex container** (like React Native's `View`). It defaults to `flex_direction: "column"`. `Column` and `Row` are convenience wrappers that fix the direction.
60+
61+
### Flex container properties (inside `style`)
62+
63+
- `flex_direction``"column"` (default), `"row"`, `"column_reverse"`, `"row_reverse"`
64+
- `justify_content` — main-axis distribution: `"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"`
65+
- `align_items` — cross-axis alignment: `"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`
66+
- `overflow``"visible"` (default), `"hidden"`
67+
- `spacing` — gap between children (dp / pt)
68+
- `padding` — inner spacing
69+
70+
### Child layout properties
71+
72+
- `flex` — flex grow factor (shorthand)
73+
- `flex_grow`, `flex_shrink` — individual flex properties
74+
- `align_self` — override the parent's `align_items` for this child
75+
- `width`, `height` — fixed dimensions
76+
- `min_width`, `min_height` — minimum size constraints
77+
- `margin` — outer spacing
78+
79+
Under the hood:
80+
- **Android:** `LinearLayout` with gravity, weights, and divider-based spacing
81+
- **iOS:** `UIStackView` with axis, alignment, distribution, and layout margins
5882

5983
## Native view handlers
6084

6185
Platform-specific rendering logic lives in the `native_views` package, organised into dedicated submodules:
6286

63-
- `native_views.base` — shared `ViewHandler` protocol and common utilities (colour parsing, padding resolution, layout keys)
87+
- `native_views.base` — shared `ViewHandler` protocol and common utilities (colour parsing, padding resolution, layout keys, flex constants)
6488
- `native_views.android` — Android handlers using Chaquopy's Java bridge (`jclass`, `dynamic_proxy`)
6589
- `native_views.ios` — iOS handlers using rubicon-objc (`ObjCClass`, `objc_method`)
6690

91+
Column, Row, and View share a single `FlexContainerHandler` class on each platform. The handler reads `flex_direction` from the element's props to configure the native layout container.
92+
6793
Each handler class maps an element type name (e.g. `"Text"`, `"Button"`) to platform-native widget creation, property updates, and child management. The `NativeViewRegistry` lazily imports only the relevant platform module at runtime, so the package can be imported on any platform for testing.
6894

6995
## Comparison

docs/concepts/components.md

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ pn.Column(
2222

2323
**Layout:**
2424

25-
- `Column(*children, style=...)` — vertical stack
26-
- `Row(*children, style=...)` — horizontal stack
25+
- `View(*children, style=...)` — universal flex container (default `flex_direction: "column"`)
26+
- `Column(*children, style=...)` — vertical flex container (fixed `flex_direction: "column"`)
27+
- `Row(*children, style=...)` — horizontal flex container (fixed `flex_direction: "row"`)
2728
- `ScrollView(child, style=...)` — scrollable container
28-
- `View(*children, style=...)` — generic container
2929
- `SafeAreaView(*children, style=...)` — safe-area-aware container
3030
- `Spacer(size, flex)` — empty space
3131

@@ -56,16 +56,51 @@ pn.Column(
5656

5757
- `FlatList(data, render_item, key_extractor, separator_height)` — scrollable data list
5858

59-
### Layout properties
59+
### Flex layout model
6060

61-
All components accept layout properties inside the `style` dict:
61+
PythonNative uses a **flexbox-inspired layout model**. `View` is the universal flex container — `Column` and `Row` are convenience wrappers.
62+
63+
#### Flex container properties (inside `style`)
64+
65+
- `flex_direction``"column"` (default), `"row"`, `"column_reverse"`, `"row_reverse"`
66+
- `justify_content` — main-axis distribution: `"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"`
67+
- `align_items` — cross-axis alignment: `"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`
68+
- `overflow``"visible"` (default), `"hidden"`
69+
- `spacing` — gap between children (dp / pt)
70+
- `padding` — inner spacing
71+
72+
#### Child layout properties
73+
74+
All components accept these in their `style` dict:
6275

6376
- `width`, `height` — fixed dimensions (dp / pt)
64-
- `flex` — flex grow factor
77+
- `flex` — flex grow factor (shorthand)
78+
- `flex_grow`, `flex_shrink` — individual flex properties
6579
- `margin` — outer margin (int, float, or dict like padding)
66-
- `min_width`, `max_width`, `min_height`, `max_height` — size constraints
80+
- `min_width`, `min_height` — minimum size constraints
81+
- `max_width`, `max_height` — maximum size constraints
6782
- `align_self` — override parent alignment for this child
6883

84+
#### Example: centering content
85+
86+
```python
87+
pn.View(
88+
pn.Text("Centered"),
89+
style={"flex": 1, "justify_content": "center", "align_items": "center"},
90+
)
91+
```
92+
93+
#### Example: horizontal row with spacing
94+
95+
```python
96+
pn.Row(
97+
pn.Button("Cancel"),
98+
pn.Spacer(flex=1),
99+
pn.Button("OK"),
100+
style={"padding": 16, "align_items": "center"},
101+
)
102+
```
103+
69104
## Function components — the building block
70105

71106
All UI in PythonNative is built with `@pn.component` function components. Each screen is a function component that returns an element tree:

docs/guides/styling.md

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import pythonnative as pn
2222
styles = pn.StyleSheet.create(
2323
title={"font_size": 28, "bold": True, "color": "#333"},
2424
subtitle={"font_size": 14, "color": "#666"},
25-
container={"padding": 16, "spacing": 12, "alignment": "fill"},
25+
container={"padding": 16, "spacing": 12, "align_items": "stretch"},
2626
)
2727

2828
pn.Text("Welcome", style=styles["title"])
@@ -78,25 +78,80 @@ pn.Text("Title", style={"font_size": 24, "bold": True, "text_align": "center"})
7878
pn.Text("Subtitle", style={"font_size": 14, "color": "#666666"})
7979
```
8080

81-
## Layout properties
81+
## Flex layout
8282

83-
All components support common layout properties inside `style`:
83+
PythonNative uses a flexbox-inspired layout model. `View` is the universal flex container, and `Column`/`Row` are convenience wrappers.
8484

85-
```python
86-
pn.Text("Fixed size", style={"width": 200, "height": 50})
87-
pn.View(child, style={"flex": 1, "margin": 8})
88-
pn.Column(items, style={"margin": {"horizontal": 16, "vertical": 8}})
89-
```
85+
### Flex container properties
86+
87+
These go in the `style` dict of `View`, `Column`, or `Row`:
88+
89+
- `flex_direction``"column"` (default), `"row"`, `"column_reverse"`, `"row_reverse"` (only for `View`; `Column`/`Row` have fixed directions)
90+
- `justify_content` — main-axis distribution: `"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"`
91+
- `align_items` — cross-axis alignment: `"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`
92+
- `overflow``"visible"` (default), `"hidden"`
93+
- `spacing` — gap between children (dp / pt)
94+
- `padding` — inner spacing (int for all sides, or dict)
95+
96+
### Child layout properties
97+
98+
All components accept these in `style`:
9099

91100
- `width`, `height` — fixed dimensions in dp (Android) / pt (iOS)
92-
- `flex` — flex grow factor within Column/Row
101+
- `flex` — flex grow factor (shorthand for `flex_grow`)
102+
- `flex_grow` — how much a child should grow to fill available space
103+
- `flex_shrink` — how much a child should shrink when space is tight
93104
- `margin` — outer spacing (int for all sides, or dict)
94-
- `min_width`, `max_width`, `min_height`, `max_height` — size constraints
95-
- `align_self` — override parent alignment
105+
- `min_width`, `min_height` — minimum size constraints
106+
- `max_width`, `max_height` — maximum size constraints
107+
- `align_self` — override parent alignment: `"flex_start"`, `"center"`, `"flex_end"`, `"stretch"`
108+
109+
### Layout examples
110+
111+
**Centering content:**
112+
113+
```python
114+
pn.View(
115+
pn.Text("Centered!"),
116+
style={"flex": 1, "justify_content": "center", "align_items": "center"},
117+
)
118+
```
119+
120+
**Horizontal row with spacer:**
121+
122+
```python
123+
pn.Row(
124+
pn.Text("Left"),
125+
pn.Spacer(flex=1),
126+
pn.Text("Right"),
127+
style={"padding": 16, "align_items": "center"},
128+
)
129+
```
130+
131+
**Child with flex grow:**
132+
133+
```python
134+
pn.Column(
135+
pn.Text("Header", style={"font_size": 20, "bold": True}),
136+
pn.View(pn.Text("Content area"), style={"flex": 1}),
137+
pn.Text("Footer"),
138+
style={"flex": 1, "spacing": 8},
139+
)
140+
```
141+
142+
**Horizontal button bar:**
143+
144+
```python
145+
pn.Row(
146+
pn.Button("Cancel", style={"flex": 1}),
147+
pn.Button("OK", style={"flex": 1, "background_color": "#007AFF", "color": "#FFF"}),
148+
style={"spacing": 8, "padding": 16},
149+
)
150+
```
96151

97152
## Layout with Column and Row
98153

99-
`Column` (vertical) and `Row` (horizontal):
154+
`Column` (vertical) and `Row` (horizontal) are convenience wrappers for `View`:
100155

101156
```python
102157
pn.Column(
@@ -105,17 +160,16 @@ pn.Column(
105160
pn.Text("Password"),
106161
pn.TextInput(placeholder="Enter password", secure=True),
107162
pn.Button("Login", on_click=handle_login),
108-
style={"spacing": 8, "padding": 16, "alignment": "fill"},
163+
style={"spacing": 8, "padding": 16, "align_items": "stretch"},
109164
)
110165
```
111166

112167
### Alignment properties
113168

114169
Column and Row support `align_items` and `justify_content` inside `style`:
115170

116-
- **`align_items`** — cross-axis alignment: `"fill"`, `"center"`, `"leading"` / `"start"`, `"trailing"` / `"end"`
117-
- **`justify_content`** — main-axis distribution: `"start"`, `"center"`, `"end"`, `"space_between"`, `"space_around"`
118-
- **`alignment`** — shorthand for cross-axis alignment (same values as `align_items`)
171+
- **`align_items`** — cross-axis alignment: `"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`, `"leading"`, `"trailing"`
172+
- **`justify_content`** — main-axis distribution: `"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"`
119173

120174
```python
121175
pn.Row(

examples/hello-world/app/main_page.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
title={"font_size": 24, "bold": True},
1010
subtitle={"font_size": 16, "color": "#666666"},
1111
medal={"font_size": 32},
12-
section={"spacing": 12, "padding": 16, "align_items": "stretch"},
12+
card={
13+
"spacing": 12,
14+
"padding": 16,
15+
"background_color": "#F8F9FA",
16+
"align_items": "center",
17+
},
18+
section={"spacing": 16, "padding": 24, "align_items": "stretch"},
19+
button_row={"spacing": 8, "align_items": "center"},
1320
)
1421

1522

@@ -19,11 +26,15 @@ def counter_badge(initial: int = 0) -> pn.Element:
1926
count, set_count = pn.use_state(initial)
2027
medal = emoji.emojize(MEDALS[count] if count < len(MEDALS) else ":star:")
2128

22-
return pn.Column(
29+
return pn.View(
2330
pn.Text(f"Tapped {count} times", style=styles["subtitle"]),
2431
pn.Text(medal, style=styles["medal"]),
25-
pn.Button("Tap me", on_click=lambda: set_count(count + 1)),
26-
style={"spacing": 4},
32+
pn.Row(
33+
pn.Button("Tap me", on_click=lambda: set_count(count + 1)),
34+
pn.Button("Reset", on_click=lambda: set_count(0)),
35+
style=styles["button_row"],
36+
),
37+
style=styles["card"],
2738
)
2839

2940

examples/hello-world/app/second_page.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ def SecondPage() -> pn.Element:
77
message = nav.get_args().get("message", "Second Page")
88
return pn.ScrollView(
99
pn.Column(
10-
pn.Text(message, style={"font_size": 20}),
10+
pn.Text(message, style={"font_size": 24, "bold": True}),
1111
pn.Button(
1212
"Go to Third Page",
1313
on_click=lambda: nav.push("app.third_page.ThirdPage"),
1414
),
1515
pn.Button("Back", on_click=nav.pop),
16-
style={"spacing": 12, "padding": 16, "align_items": "stretch"},
16+
style={"spacing": 16, "padding": 24, "align_items": "stretch"},
1717
)
1818
)

examples/hello-world/app/third_page.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
@pn.component
55
def ThirdPage() -> pn.Element:
66
nav = pn.use_navigation()
7-
return pn.Column(
8-
pn.Text("Third Page", style={"font_size": 24, "bold": True}),
9-
pn.Text("You navigated two levels deep."),
10-
pn.Button("Back to Second", on_click=nav.pop),
11-
style={"spacing": 12, "padding": 16, "align_items": "stretch"},
7+
return pn.ScrollView(
8+
pn.Column(
9+
pn.Text("Third Page", style={"font_size": 24, "bold": True}),
10+
pn.Text("You navigated two levels deep."),
11+
pn.Button("Back to Second", on_click=nav.pop),
12+
style={"spacing": 16, "padding": 24, "align_items": "stretch"},
13+
)
1214
)

0 commit comments

Comments
 (0)