Skip to content

Commit 6962d38

Browse files
committed
feat(components,core)!: add layout/styling APIs and fluent setters
1 parent 3ac84b1 commit 6962d38

File tree

13 files changed

+683
-36
lines changed

13 files changed

+683
-36
lines changed

docs/api/component-properties.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
## Component Property Reference (v0.4.0)
2+
3+
This page summarizes common properties and fluent setters added in v0.4.0. Unless noted, methods return `self` for chaining.
4+
5+
### View (base)
6+
7+
- `set_background_color(color)`
8+
- Accepts ARGB int or `#RRGGBB` / `#AARRGGBB` string.
9+
10+
- `set_padding(*, all=None, horizontal=None, vertical=None, left=None, top=None, right=None, bottom=None)`
11+
- Android: applies padding in dp.
12+
- iOS: currently a no-op for most views.
13+
14+
- `set_margin(*, all=None, horizontal=None, vertical=None, left=None, top=None, right=None, bottom=None)`
15+
- Android: applied when added to `StackView` (LinearLayout) as `LayoutParams` margins (dp).
16+
- iOS: not currently applied.
17+
18+
- `wrap_in_scroll()``ScrollView`
19+
- Returns a `ScrollView` containing this view.
20+
21+
### ScrollView
22+
23+
- `ScrollView.wrap(view)``ScrollView`
24+
- Class helper to wrap a single child.
25+
26+
### StackView
27+
28+
- `set_axis('vertical'|'horizontal')`
29+
- `set_spacing(n)`
30+
- Android: dp via divider drawable.
31+
- iOS: `UIStackView.spacing` (points).
32+
- `set_alignment('fill'|'center'|'leading'|'trailing'|'top'|'bottom')`
33+
- Cross-axis alignment; top/bottom map appropriately for horizontal stacks.
34+
35+
### Text components
36+
37+
Applies to `Label`, `TextField`, `TextView`:
38+
39+
- `set_text(text)`
40+
- `set_text_color(color)`
41+
- `set_text_size(size)`
42+
43+
Platform notes:
44+
- Android: `setTextColor(int)`, `setTextSize(sp)`.
45+
- iOS: `setTextColor(UIColor)`, `UIFont.systemFont(ofSize:)`.
46+
47+
### Button
48+
49+
- `set_title(text)`
50+
- `set_on_click(callback)`
51+
52+

docs/guides/styling.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
## Styling
2+
3+
This guide covers the lightweight styling APIs introduced in v0.4.0.
4+
5+
The goal is to provide a small, predictable set of cross-platform styling hooks. Some features are Android-only today and will expand over time.
6+
7+
### Colors
8+
9+
- Use `set_background_color(color)` on any view.
10+
- Color can be an ARGB int or a hex string like `#RRGGBB` or `#AARRGGBB`.
11+
12+
```python
13+
stack = pn.StackView().set_background_color("#FFF5F5F5")
14+
```
15+
16+
### Padding and Margin
17+
18+
- `set_padding(...)` is available on all views.
19+
- Android: applies using `View.setPadding` with dp units.
20+
- iOS: currently a no-op for most views; prefer container spacing (`StackView.set_spacing`) and layout.
21+
22+
- `set_margin(...)` records margins on the child view.
23+
- Android: applied automatically when added to a `StackView` (LinearLayout) via `LayoutParams.setMargins` (dp).
24+
- iOS: not currently applied.
25+
26+
`set_padding`/`set_margin` accept these parameters (integers): `all`, `horizontal`, `vertical`, `left`, `top`, `right`, `bottom`. Individual sides override group values.
27+
28+
```python
29+
pn.Label("Name").set_margin(bottom=8)
30+
pn.TextField().set_padding(horizontal=12, vertical=8)
31+
```
32+
33+
### Text styling
34+
35+
Text components expose:
36+
- `set_text(text) -> self`
37+
- `set_text_color(color) -> self` (hex or ARGB int)
38+
- `set_text_size(size) -> self` (sp on Android; pt on iOS via system font)
39+
40+
Applies to `Label`, `TextField`, and `TextView`.
41+
42+
```python
43+
pn.Label("Hello").set_text_color("#FF3366").set_text_size(18)
44+
```
45+
46+
### StackView layout
47+
48+
`StackView` (Android LinearLayout / iOS UIStackView) adds configuration helpers:
49+
50+
- `set_axis('vertical'|'horizontal') -> self`
51+
- `set_spacing(n) -> self` (dp on Android; points on iOS)
52+
- `set_alignment(value) -> self`
53+
- Cross-axis alignment. Supported values: `fill`, `center`, `leading`/`top`, `trailing`/`bottom`.
54+
55+
```python
56+
form = (
57+
pn.StackView()
58+
.set_axis('vertical')
59+
.set_spacing(8)
60+
.set_alignment('fill')
61+
.set_padding(all=16)
62+
)
63+
form.add_view(pn.Label("Username").set_margin(bottom=4))
64+
form.add_view(pn.TextField().set_padding(horizontal=12, vertical=8))
65+
```
66+
67+
### Scroll helpers
68+
69+
Wrap any view in a `ScrollView` using either approach:
70+
71+
```python
72+
scroll = pn.ScrollView.wrap(form)
73+
# or
74+
scroll = form.wrap_in_scroll()
75+
```
76+
77+
Attach the scroll view as your page root:
78+
79+
```python
80+
self.set_root_view(scroll)
81+
```
82+
83+
### Fluent setters
84+
85+
Most setters now return `self` for chaining, e.g.:
86+
87+
```python
88+
pn.Button("Tap me").set_on_click(lambda: print("hi")).set_padding(all=8)
89+
```
90+
91+
Note: Where platform limitations exist, the methods are no-ops and still return `self`.
92+
93+

examples/hello-world/app/main_page.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,9 @@ def __init__(self, native_instance):
1515

1616
def on_create(self):
1717
super().on_create()
18-
stack = pn.StackView()
19-
# Ensure vertical stacking
20-
try:
21-
stack.native_instance.setAxis_(1) # 1 = vertical
22-
except Exception:
23-
pass
24-
stack.add_view(pn.Label("Hello from PythonNative Demo!"))
25-
button = pn.Button("Go to Second Page")
18+
stack = pn.StackView().set_axis("vertical").set_spacing(12).set_alignment("fill").set_padding(all=16)
19+
stack.add_view(pn.Label("Hello from PythonNative Demo!").set_text_size(18))
20+
button = pn.Button("Go to Second Page").set_padding(vertical=10, horizontal=14)
2621

2722
def on_next():
2823
# Visual confirmation that tap worked (iOS only)
@@ -37,14 +32,9 @@ def on_next():
3732

3833
button.set_on_click(on_next)
3934
# Make the button visually obvious
40-
try:
41-
if UIColor is not None:
42-
button.native_instance.setBackgroundColor_(UIColor.systemBlueColor())
43-
button.native_instance.setTitleColor_forState_(UIColor.whiteColor(), 0)
44-
except Exception:
45-
pass
35+
button.set_background_color("#FF1E88E5")
4636
stack.add_view(button)
47-
self.set_root_view(stack)
37+
self.set_root_view(stack.wrap_in_scroll())
4838

4939
def on_start(self):
5040
super().on_start()

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ nav:
2424
- Android: guides/android.md
2525
- iOS: guides/ios.md
2626
- Navigation: guides/navigation.md
27+
- Styling: guides/styling.md
2728
- API Reference:
2829
- Package: api/pythonnative.md
30+
- Component Properties: api/component-properties.md
2931
- Meta:
3032
- Roadmap: meta/roadmap.md
3133
- Contributing: meta/contributing.md

src/pythonnative/button.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ def __init__(self, title: str = "") -> None:
4343
self.native_instance = self.native_class(context)
4444
self.set_title(title)
4545

46-
def set_title(self, title: str) -> None:
46+
def set_title(self, title: str):
4747
self.native_instance.setText(title)
48+
return self
4849

4950
def get_title(self) -> str:
5051
return self.native_instance.getText().toString()
5152

52-
def set_on_click(self, callback: Callable[[], None]) -> None:
53+
def set_on_click(self, callback: Callable[[], None]):
5354
class OnClickListener(dynamic_proxy(jclass("android.view.View").OnClickListener)):
5455
def __init__(self, callback):
5556
super().__init__()
@@ -60,6 +61,7 @@ def onClick(self, view):
6061

6162
listener = OnClickListener(callback)
6263
self.native_instance.setOnClickListener(listener)
64+
return self
6365

6466
else:
6567
# ========================================
@@ -93,17 +95,19 @@ def __init__(self, title: str = "") -> None:
9395
self.native_instance = self.native_class.alloc().init()
9496
self.set_title(title)
9597

96-
def set_title(self, title: str) -> None:
98+
def set_title(self, title: str):
9799
self.native_instance.setTitle_forState_(title, 0)
100+
return self
98101

99102
def get_title(self) -> str:
100103
return self.native_instance.titleForState_(0)
101104

102-
def set_on_click(self, callback: Callable[[], None]) -> None:
105+
def set_on_click(self, callback: Callable[[], None]):
103106
# Create a handler object with an Objective-C method `onTap:` and attach the Python callback
104107
handler = _PNButtonHandler.new()
105108
# Keep strong references to the handler and callback
106109
self._click_handler = handler
107110
handler._callback = callback
108111
# UIControlEventTouchUpInside = 1 << 6
109112
self.native_instance.addTarget_action_forControlEvents_(handler, SEL("onTap:"), 1 << 6)
113+
return self

src/pythonnative/cli/pn.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,17 @@ def __init__(self, native_instance):
5555
5656
def on_create(self):
5757
super().on_create()
58-
stack = pn.StackView()
59-
stack.add_view(pn.Label("Hello from PythonNative!"))
60-
button = pn.Button("Tap me")
61-
button.set_on_click(lambda: print("Button clicked"))
58+
stack = (
59+
pn.StackView()
60+
.set_axis("vertical")
61+
.set_spacing(12)
62+
.set_alignment("fill")
63+
.set_padding(all=16)
64+
)
65+
stack.add_view(pn.Label("Hello from PythonNative!").set_text_size(18))
66+
button = pn.Button("Tap me").set_on_click(lambda: print("Button clicked"))
6267
stack.add_view(button)
63-
self.set_root_view(stack)
68+
self.set_root_view(stack.wrap_in_scroll())
6469
"""
6570
)
6671

src/pythonnative/label.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from abc import ABC, abstractmethod
2+
from typing import Any
23

34
from .utils import IS_ANDROID, get_android_context
45
from .view import ViewBase
@@ -21,6 +22,14 @@ def set_text(self, text: str) -> None:
2122
def get_text(self) -> str:
2223
pass
2324

25+
@abstractmethod
26+
def set_text_color(self, color: Any) -> None:
27+
pass
28+
29+
@abstractmethod
30+
def set_text_size(self, size: float) -> None:
31+
pass
32+
2433

2534
if IS_ANDROID:
2635
# ========================================
@@ -38,12 +47,37 @@ def __init__(self, text: str = "") -> None:
3847
self.native_instance = self.native_class(context)
3948
self.set_text(text)
4049

41-
def set_text(self, text: str) -> None:
50+
def set_text(self, text: str):
4251
self.native_instance.setText(text)
52+
return self
4353

4454
def get_text(self) -> str:
4555
return self.native_instance.getText().toString()
4656

57+
def set_text_color(self, color: Any):
58+
# Accept int ARGB or hex string
59+
if isinstance(color, str):
60+
c = color.strip()
61+
if c.startswith("#"):
62+
c = c[1:]
63+
if len(c) == 6:
64+
c = "FF" + c
65+
color_int = int(c, 16)
66+
else:
67+
color_int = int(color)
68+
try:
69+
self.native_instance.setTextColor(color_int)
70+
except Exception:
71+
pass
72+
return self
73+
74+
def set_text_size(self, size_sp: float):
75+
try:
76+
self.native_instance.setTextSize(float(size_sp))
77+
except Exception:
78+
pass
79+
return self
80+
4781
else:
4882
# ========================================
4983
# iOS class
@@ -59,8 +93,41 @@ def __init__(self, text: str = "") -> None:
5993
self.native_instance = self.native_class.alloc().init()
6094
self.set_text(text)
6195

62-
def set_text(self, text: str) -> None:
96+
def set_text(self, text: str):
6397
self.native_instance.setText_(text)
98+
return self
6499

65100
def get_text(self) -> str:
66101
return self.native_instance.text()
102+
103+
def set_text_color(self, color: Any):
104+
# Accept int ARGB or hex string
105+
if isinstance(color, str):
106+
c = color.strip()
107+
if c.startswith("#"):
108+
c = c[1:]
109+
if len(c) == 6:
110+
c = "FF" + c
111+
color_int = int(c, 16)
112+
else:
113+
color_int = int(color)
114+
try:
115+
UIColor = ObjCClass("UIColor")
116+
a = ((color_int >> 24) & 0xFF) / 255.0
117+
r = ((color_int >> 16) & 0xFF) / 255.0
118+
g = ((color_int >> 8) & 0xFF) / 255.0
119+
b = (color_int & 0xFF) / 255.0
120+
color_obj = UIColor.colorWithRed_green_blue_alpha_(r, g, b, a)
121+
self.native_instance.setTextColor_(color_obj)
122+
except Exception:
123+
pass
124+
return self
125+
126+
def set_text_size(self, size: float):
127+
try:
128+
UIFont = ObjCClass("UIFont")
129+
font = UIFont.systemFontOfSize_(float(size))
130+
self.native_instance.setFont_(font)
131+
except Exception:
132+
pass
133+
return self

src/pythonnative/list_view.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@ def __init__(self, context, data: list = []) -> None:
3838
self.native_instance = self.native_class(context)
3939
self.set_data(data)
4040

41-
def set_data(self, data: list) -> None:
41+
def set_data(self, data: list):
4242
adapter = jclass("android.widget.ArrayAdapter")(
4343
self.context, jclass("android.R$layout").simple_list_item_1, data
4444
)
4545
self.native_instance.setAdapter(adapter)
46+
return self
4647

4748
def get_data(self) -> list:
4849
adapter = self.native_instance.getAdapter()
@@ -63,9 +64,10 @@ def __init__(self, data: list = []) -> None:
6364
self.native_instance = self.native_class.alloc().init()
6465
self.set_data(data)
6566

66-
def set_data(self, data: list) -> None:
67+
def set_data(self, data: list):
6768
# Note: This is a simplified representation. Normally, you would need to create a UITableViewDataSource.
6869
self.native_instance.reloadData()
70+
return self
6971

7072
def get_data(self) -> list:
7173
# Note: This is a simplified representation.

0 commit comments

Comments
 (0)