pane-ui 0.1.0__tar.gz
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.
- pane_ui-0.1.0/LICENSE +21 -0
- pane_ui-0.1.0/PKG-INFO +107 -0
- pane_ui-0.1.0/README.md +84 -0
- pane_ui-0.1.0/pane/__init__.py +79 -0
- pane_ui-0.1.0/pane/_bootstrap.py +92 -0
- pane_ui-0.1.0/pane/_convert.py +42 -0
- pane_ui-0.1.0/pane/_events.py +32 -0
- pane_ui-0.1.0/pane/_native/Pane.dll +0 -0
- pane_ui-0.1.0/pane/_native/Pane.runtimeconfig.json +10 -0
- pane_ui-0.1.0/pane/app.py +112 -0
- pane_ui-0.1.0/pane/layout.py +113 -0
- pane_ui-0.1.0/pane/theme.py +31 -0
- pane_ui-0.1.0/pane/widgets.py +306 -0
- pane_ui-0.1.0/pane_ui.egg-info/PKG-INFO +107 -0
- pane_ui-0.1.0/pane_ui.egg-info/SOURCES.txt +18 -0
- pane_ui-0.1.0/pane_ui.egg-info/dependency_links.txt +1 -0
- pane_ui-0.1.0/pane_ui.egg-info/requires.txt +1 -0
- pane_ui-0.1.0/pane_ui.egg-info/top_level.txt +1 -0
- pane_ui-0.1.0/pyproject.toml +39 -0
- pane_ui-0.1.0/setup.cfg +4 -0
pane_ui-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hunter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pane_ui-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pane-ui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bindings for Pane, a modern minimalist Windows UI engine built on WPF
|
|
5
|
+
Author: Hunter
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/HunterH1218/pane
|
|
8
|
+
Project-URL: Repository, https://github.com/HunterH1218/pane
|
|
9
|
+
Project-URL: Issues, https://github.com/HunterH1218/pane/issues
|
|
10
|
+
Keywords: wpf,windows,ui,gui,desktop,pythonnet
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: C#
|
|
16
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
17
|
+
Classifier: Topic :: Software Development :: Widget Sets
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: pythonnet>=3.0
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# pane
|
|
25
|
+
|
|
26
|
+
Python bindings for [Pane](https://github.com/HunterH1218/pane) — a modern, minimalist
|
|
27
|
+
Windows UI engine built on WPF. Build real native Windows app UIs from Python:
|
|
28
|
+
buttons, checkboxes, sliders, text input, tabs, live light/dark theming and
|
|
29
|
+
accent-color switching, all with a single custom-chrome window.
|
|
30
|
+
|
|
31
|
+
The engine itself is C#/WPF; this package drives it in-process via
|
|
32
|
+
[pythonnet](https://pythonnet.github.io/), so widgets are the real native
|
|
33
|
+
controls, not a re-implementation.
|
|
34
|
+
|
|
35
|
+
**Windows only** (WPF has no cross-platform equivalent). Requires the free
|
|
36
|
+
[.NET 8 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/8.0) —
|
|
37
|
+
not the SDK, just the runtime.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pip install pane-ui
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import pane
|
|
49
|
+
|
|
50
|
+
def build(window):
|
|
51
|
+
window.Content = pane.stack(
|
|
52
|
+
pane.text("Hello from Python", size="title", bold=True),
|
|
53
|
+
pane.button("Click me", style="primary", on_click=lambda: print("clicked!")),
|
|
54
|
+
pane.checkbox("Enable notifications", checked=True),
|
|
55
|
+
spacing=12,
|
|
56
|
+
margin=24,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
pane.run(build, title="My App")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run the full widget gallery from the source repo for a tour of everything
|
|
63
|
+
available: `python examples/gallery.py`.
|
|
64
|
+
|
|
65
|
+
## Widgets
|
|
66
|
+
|
|
67
|
+
`button`, `checkbox`, `radio_button`, `toggle_switch`, `text_box`,
|
|
68
|
+
`password_box`, `progress_bar`, `slider`, `combo_box`, `list_box`, `nav_list`,
|
|
69
|
+
`tab_control`, `expander`, `group_box`, `separator`, `text`, plus
|
|
70
|
+
`set_tooltip` / `set_context_menu` helpers.
|
|
71
|
+
|
|
72
|
+
## Layout
|
|
73
|
+
|
|
74
|
+
`stack(*children, orientation=, spacing=, margin=)`,
|
|
75
|
+
`grid(children, rows=, columns=)`, `card(*children)`, `sidebar(*children)`,
|
|
76
|
+
`scroll(child)`.
|
|
77
|
+
|
|
78
|
+
## Theming
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
pane.set_theme("light") # or "dark"
|
|
82
|
+
pane.toggle_theme()
|
|
83
|
+
pane.set_accent("#4C82F7") # hex string or (r, g, b) tuple
|
|
84
|
+
pane.reset_accent()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Every widget re-themes live, mid-run, no restart needed.
|
|
88
|
+
|
|
89
|
+
## Events
|
|
90
|
+
|
|
91
|
+
Callbacks adapt to whatever you write - zero args or one:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
pane.button("Save", on_click=lambda: print("saved"))
|
|
95
|
+
pane.slider(on_change=lambda value: print(value))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Threading
|
|
99
|
+
|
|
100
|
+
`pane.run(builder)` builds the window on a dedicated UI thread and blocks
|
|
101
|
+
until it's closed. Build your widget tree inside `builder`. To update the UI
|
|
102
|
+
later from another thread (a timer, a background task), use
|
|
103
|
+
`pane.invoke(fn)` to marshal back onto the UI thread.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
pane_ui-0.1.0/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# pane
|
|
2
|
+
|
|
3
|
+
Python bindings for [Pane](https://github.com/HunterH1218/pane) — a modern, minimalist
|
|
4
|
+
Windows UI engine built on WPF. Build real native Windows app UIs from Python:
|
|
5
|
+
buttons, checkboxes, sliders, text input, tabs, live light/dark theming and
|
|
6
|
+
accent-color switching, all with a single custom-chrome window.
|
|
7
|
+
|
|
8
|
+
The engine itself is C#/WPF; this package drives it in-process via
|
|
9
|
+
[pythonnet](https://pythonnet.github.io/), so widgets are the real native
|
|
10
|
+
controls, not a re-implementation.
|
|
11
|
+
|
|
12
|
+
**Windows only** (WPF has no cross-platform equivalent). Requires the free
|
|
13
|
+
[.NET 8 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/8.0) —
|
|
14
|
+
not the SDK, just the runtime.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
pip install pane-ui
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import pane
|
|
26
|
+
|
|
27
|
+
def build(window):
|
|
28
|
+
window.Content = pane.stack(
|
|
29
|
+
pane.text("Hello from Python", size="title", bold=True),
|
|
30
|
+
pane.button("Click me", style="primary", on_click=lambda: print("clicked!")),
|
|
31
|
+
pane.checkbox("Enable notifications", checked=True),
|
|
32
|
+
spacing=12,
|
|
33
|
+
margin=24,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
pane.run(build, title="My App")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Run the full widget gallery from the source repo for a tour of everything
|
|
40
|
+
available: `python examples/gallery.py`.
|
|
41
|
+
|
|
42
|
+
## Widgets
|
|
43
|
+
|
|
44
|
+
`button`, `checkbox`, `radio_button`, `toggle_switch`, `text_box`,
|
|
45
|
+
`password_box`, `progress_bar`, `slider`, `combo_box`, `list_box`, `nav_list`,
|
|
46
|
+
`tab_control`, `expander`, `group_box`, `separator`, `text`, plus
|
|
47
|
+
`set_tooltip` / `set_context_menu` helpers.
|
|
48
|
+
|
|
49
|
+
## Layout
|
|
50
|
+
|
|
51
|
+
`stack(*children, orientation=, spacing=, margin=)`,
|
|
52
|
+
`grid(children, rows=, columns=)`, `card(*children)`, `sidebar(*children)`,
|
|
53
|
+
`scroll(child)`.
|
|
54
|
+
|
|
55
|
+
## Theming
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
pane.set_theme("light") # or "dark"
|
|
59
|
+
pane.toggle_theme()
|
|
60
|
+
pane.set_accent("#4C82F7") # hex string or (r, g, b) tuple
|
|
61
|
+
pane.reset_accent()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Every widget re-themes live, mid-run, no restart needed.
|
|
65
|
+
|
|
66
|
+
## Events
|
|
67
|
+
|
|
68
|
+
Callbacks adapt to whatever you write - zero args or one:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
pane.button("Save", on_click=lambda: print("saved"))
|
|
72
|
+
pane.slider(on_change=lambda value: print(value))
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Threading
|
|
76
|
+
|
|
77
|
+
`pane.run(builder)` builds the window on a dedicated UI thread and blocks
|
|
78
|
+
until it's closed. Build your widget tree inside `builder`. To update the UI
|
|
79
|
+
later from another thread (a timer, a background task), use
|
|
80
|
+
`pane.invoke(fn)` to marshal back onto the UI thread.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pane - Python bindings for the Pane WPF UI engine.
|
|
3
|
+
|
|
4
|
+
Builds real Pane-styled WPF windows and widgets from Python, in-process, via
|
|
5
|
+
pythonnet. The engine itself (Pane.dll, the XAML control styles) is plain
|
|
6
|
+
C#/WPF and untouched by this package - this is a thin, Pythonic layer that
|
|
7
|
+
creates and drives the same native controls the WPF gallery app uses.
|
|
8
|
+
|
|
9
|
+
import pane
|
|
10
|
+
|
|
11
|
+
def build(window):
|
|
12
|
+
window.Content = pane.stack(
|
|
13
|
+
pane.text("Hello from Python", size="title"),
|
|
14
|
+
pane.button("Click me", style="primary", on_click=lambda: print("clicked")),
|
|
15
|
+
spacing=12, margin=24,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
pane.run(build, title="My App")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .app import close, current_window, invoke, is_ui_thread, run
|
|
22
|
+
from .layout import card, grid, scroll, sidebar, stack
|
|
23
|
+
from .theme import current_theme, reset_accent, set_accent, set_theme, toggle_theme
|
|
24
|
+
from .widgets import (
|
|
25
|
+
button,
|
|
26
|
+
checkbox,
|
|
27
|
+
combo_box,
|
|
28
|
+
expander,
|
|
29
|
+
group_box,
|
|
30
|
+
list_box,
|
|
31
|
+
nav_list,
|
|
32
|
+
password_box,
|
|
33
|
+
progress_bar,
|
|
34
|
+
radio_button,
|
|
35
|
+
separator,
|
|
36
|
+
set_context_menu,
|
|
37
|
+
set_tooltip,
|
|
38
|
+
slider,
|
|
39
|
+
tab_control,
|
|
40
|
+
text,
|
|
41
|
+
text_box,
|
|
42
|
+
toggle_switch,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"run",
|
|
47
|
+
"invoke",
|
|
48
|
+
"close",
|
|
49
|
+
"is_ui_thread",
|
|
50
|
+
"current_window",
|
|
51
|
+
"set_theme",
|
|
52
|
+
"toggle_theme",
|
|
53
|
+
"current_theme",
|
|
54
|
+
"set_accent",
|
|
55
|
+
"reset_accent",
|
|
56
|
+
"button",
|
|
57
|
+
"checkbox",
|
|
58
|
+
"radio_button",
|
|
59
|
+
"toggle_switch",
|
|
60
|
+
"text_box",
|
|
61
|
+
"password_box",
|
|
62
|
+
"progress_bar",
|
|
63
|
+
"slider",
|
|
64
|
+
"combo_box",
|
|
65
|
+
"list_box",
|
|
66
|
+
"nav_list",
|
|
67
|
+
"tab_control",
|
|
68
|
+
"expander",
|
|
69
|
+
"group_box",
|
|
70
|
+
"separator",
|
|
71
|
+
"text",
|
|
72
|
+
"set_tooltip",
|
|
73
|
+
"set_context_menu",
|
|
74
|
+
"stack",
|
|
75
|
+
"grid",
|
|
76
|
+
"card",
|
|
77
|
+
"sidebar",
|
|
78
|
+
"scroll",
|
|
79
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Internal: loads the CLR runtime and the Pane assembly exactly once.
|
|
3
|
+
|
|
4
|
+
This is the only module that knows about pythonnet/clr - everything else in
|
|
5
|
+
the package imports .NET types lazily, after ensure_loaded() has run.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
_initialized = False
|
|
13
|
+
_lock = threading.Lock()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _find_build_output():
|
|
17
|
+
"""Locate Pane.dll plus the runtimeconfig.json CoreCLR needs to host the
|
|
18
|
+
WPF shared framework. Prefers the bundled copy shipped inside this
|
|
19
|
+
installed package; falls back to a locally built dev-repo output so
|
|
20
|
+
`pip install -e .` from a source checkout works without a separate
|
|
21
|
+
packaging step."""
|
|
22
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
23
|
+
|
|
24
|
+
bundled_dir = os.path.join(here, "_native")
|
|
25
|
+
bundled_config = os.path.join(bundled_dir, "Pane.runtimeconfig.json")
|
|
26
|
+
if os.path.exists(bundled_config):
|
|
27
|
+
return bundled_dir, bundled_config
|
|
28
|
+
|
|
29
|
+
repo_root = os.path.abspath(os.path.join(here, "..", ".."))
|
|
30
|
+
dev_candidates = [
|
|
31
|
+
os.path.join(repo_root, "src", "Pane.Template", "bin", "Debug", "net8.0-windows"),
|
|
32
|
+
os.path.join(repo_root, "src", "Pane.Template", "bin", "Release", "net8.0-windows"),
|
|
33
|
+
]
|
|
34
|
+
for candidate in dev_candidates:
|
|
35
|
+
config = os.path.join(candidate, "Pane.Template.runtimeconfig.json")
|
|
36
|
+
if os.path.exists(config):
|
|
37
|
+
return candidate, config
|
|
38
|
+
|
|
39
|
+
raise RuntimeError(
|
|
40
|
+
"Could not find Pane.dll. If you're developing from source, run "
|
|
41
|
+
"`dotnet build Pane.sln` in the repo root first. If you installed via "
|
|
42
|
+
"pip, this package build is missing its bundled native files."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ensure_loaded():
|
|
47
|
+
global _initialized
|
|
48
|
+
if _initialized:
|
|
49
|
+
return
|
|
50
|
+
with _lock:
|
|
51
|
+
if _initialized:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
if sys.platform != "win32":
|
|
55
|
+
raise ImportError(
|
|
56
|
+
"pane only runs on Windows - it's built on WPF, which has no "
|
|
57
|
+
"cross-platform equivalent."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
bin_dir, runtimeconfig = _find_build_output()
|
|
61
|
+
|
|
62
|
+
from pythonnet import load
|
|
63
|
+
try:
|
|
64
|
+
load("coreclr", runtime_config=runtimeconfig)
|
|
65
|
+
except Exception as ex:
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
"pane couldn't start the .NET runtime. This usually means the "
|
|
68
|
+
".NET 8 Desktop Runtime isn't installed - get it from "
|
|
69
|
+
"https://dotnet.microsoft.com/download/dotnet/8.0 "
|
|
70
|
+
"(the \"Desktop Runtime\" download for your platform, not the SDK) "
|
|
71
|
+
"and try again."
|
|
72
|
+
) from ex
|
|
73
|
+
|
|
74
|
+
import clr
|
|
75
|
+
sys.path.append(bin_dir)
|
|
76
|
+
clr.AddReference("Pane")
|
|
77
|
+
clr.AddReference("PresentationFramework")
|
|
78
|
+
clr.AddReference("PresentationCore")
|
|
79
|
+
clr.AddReference("WindowsBase")
|
|
80
|
+
|
|
81
|
+
# WPF's pack:// URI scheme (used everywhere Pane loads its own XAML
|
|
82
|
+
# resource dictionaries) is registered lazily inside PackUriHelper's
|
|
83
|
+
# static constructor. In a normal WPF .exe that happens automatically
|
|
84
|
+
# during app startup; hosted from Python it never runs on its own, so
|
|
85
|
+
# the first pack:// Uri construction throws UriFormatException:
|
|
86
|
+
# "Invalid port specified". Touching PackUriHelper once, up front,
|
|
87
|
+
# forces that registration before any Pane code needs it.
|
|
88
|
+
from System.IO.Packaging import PackUriHelper
|
|
89
|
+
from System import Uri
|
|
90
|
+
PackUriHelper.Create(Uri("http://pane-bootstrap/"))
|
|
91
|
+
|
|
92
|
+
_initialized = True
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Internal: Python <-> .NET value conversion helpers shared by widgets and layout."""
|
|
2
|
+
|
|
3
|
+
from . import _bootstrap
|
|
4
|
+
|
|
5
|
+
_bootstrap.ensure_loaded()
|
|
6
|
+
|
|
7
|
+
from System.Windows import Thickness # noqa: E402
|
|
8
|
+
from System.Windows.Media import Color # noqa: E402
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def to_thickness(value):
|
|
12
|
+
if isinstance(value, Thickness):
|
|
13
|
+
return value
|
|
14
|
+
if isinstance(value, (int, float)):
|
|
15
|
+
return Thickness(value)
|
|
16
|
+
if isinstance(value, (tuple, list)):
|
|
17
|
+
if len(value) == 2:
|
|
18
|
+
h, v = value
|
|
19
|
+
return Thickness(h, v, h, v)
|
|
20
|
+
if len(value) == 4:
|
|
21
|
+
return Thickness(*value)
|
|
22
|
+
raise ValueError(f"Invalid margin/padding value: {value!r} (expected a number, or a 2/4-tuple)")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def to_color(value):
|
|
26
|
+
if isinstance(value, Color):
|
|
27
|
+
return value
|
|
28
|
+
if isinstance(value, str):
|
|
29
|
+
s = value.lstrip("#")
|
|
30
|
+
if len(s) == 6:
|
|
31
|
+
r, g, b = int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)
|
|
32
|
+
return Color.FromRgb(r, g, b)
|
|
33
|
+
if len(s) == 8:
|
|
34
|
+
a, r, g, b = int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16), int(s[6:8], 16)
|
|
35
|
+
return Color.FromArgb(a, r, g, b)
|
|
36
|
+
raise ValueError(f"Invalid hex color: {value!r} (expected '#RRGGBB' or '#AARRGGBB')")
|
|
37
|
+
if isinstance(value, (tuple, list)):
|
|
38
|
+
if len(value) == 3:
|
|
39
|
+
return Color.FromRgb(*value)
|
|
40
|
+
if len(value) == 4:
|
|
41
|
+
return Color.FromArgb(*value)
|
|
42
|
+
raise ValueError(f"Invalid color value: {value!r} (expected a hex string or an (r,g,b) / (a,r,g,b) tuple)")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Internal: arity-aware callback wiring so `on_click=lambda: ...` and
|
|
2
|
+
`on_click=lambda value: ...` both work without the caller having to match a
|
|
3
|
+
specific .NET event-handler signature."""
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def arity(fn):
|
|
9
|
+
try:
|
|
10
|
+
sig = inspect.signature(fn)
|
|
11
|
+
params = [
|
|
12
|
+
p
|
|
13
|
+
for p in sig.parameters.values()
|
|
14
|
+
if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
|
|
15
|
+
]
|
|
16
|
+
return len(params)
|
|
17
|
+
except (TypeError, ValueError):
|
|
18
|
+
return 1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def make_handler(callback, value_fn):
|
|
22
|
+
"""Returns a (sender, args) -> None .NET-event-shaped handler that calls
|
|
23
|
+
`callback()` if it takes no arguments, or `callback(value_fn())` otherwise."""
|
|
24
|
+
n = arity(callback)
|
|
25
|
+
|
|
26
|
+
def handler(sender, args):
|
|
27
|
+
if n == 0:
|
|
28
|
+
callback()
|
|
29
|
+
else:
|
|
30
|
+
callback(value_fn())
|
|
31
|
+
|
|
32
|
+
return handler
|
|
Binary file
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""The entry point for building and running a Pane UI from Python."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from . import _bootstrap
|
|
6
|
+
|
|
7
|
+
_bootstrap.ensure_loaded()
|
|
8
|
+
|
|
9
|
+
from Pane import PaneTheme, PaneWindow, ThemeManager # noqa: E402
|
|
10
|
+
from System import Action, Uri # noqa: E402
|
|
11
|
+
from System.Threading import ApartmentState, Thread # noqa: E402
|
|
12
|
+
from System.Threading import ThreadStart # noqa: E402
|
|
13
|
+
from System.Windows import Application, ResizeMode, ResourceDictionary # noqa: E402
|
|
14
|
+
|
|
15
|
+
_PANE_XAML_URI = "pack://application:,,,/Pane;component/Themes/Pane.xaml"
|
|
16
|
+
|
|
17
|
+
_state = {
|
|
18
|
+
"app": None,
|
|
19
|
+
"window": None,
|
|
20
|
+
"dispatcher": None,
|
|
21
|
+
"ui_thread_id": None,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_ui_thread():
|
|
26
|
+
return threading.get_ident() == _state["ui_thread_id"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def invoke(fn):
|
|
30
|
+
"""Runs fn on the UI thread and returns its result, blocking the caller.
|
|
31
|
+
Safe to call from any thread, including the UI thread itself."""
|
|
32
|
+
if _state["dispatcher"] is None:
|
|
33
|
+
raise RuntimeError("The Pane UI is not running - call pane.run() first")
|
|
34
|
+
if is_ui_thread():
|
|
35
|
+
return fn()
|
|
36
|
+
|
|
37
|
+
box = {}
|
|
38
|
+
|
|
39
|
+
def wrapper():
|
|
40
|
+
box["result"] = fn()
|
|
41
|
+
|
|
42
|
+
_state["dispatcher"].Invoke(Action(wrapper))
|
|
43
|
+
return box.get("result")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def current_window():
|
|
47
|
+
return _state["window"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def close():
|
|
51
|
+
"""Closes the running window. Safe to call from any thread."""
|
|
52
|
+
invoke(lambda: _state["window"].Close())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run(builder, *, title="Pane App", width=1000, height=700, theme="dark", resizable=True):
|
|
56
|
+
"""
|
|
57
|
+
Builds and shows a window, then blocks the calling thread until it's closed.
|
|
58
|
+
|
|
59
|
+
`builder(window)` runs on the dedicated UI thread before the window is
|
|
60
|
+
shown - build your widget tree there and assign it to `window.Content`.
|
|
61
|
+
Use pane.button(...), pane.stack(...), etc. only inside `builder` (or
|
|
62
|
+
inside a callback that pane.invoke()s back onto the UI thread).
|
|
63
|
+
"""
|
|
64
|
+
ready = threading.Event()
|
|
65
|
+
error_box = {}
|
|
66
|
+
|
|
67
|
+
def ui_main():
|
|
68
|
+
try:
|
|
69
|
+
app = Application()
|
|
70
|
+
_state["app"] = app
|
|
71
|
+
|
|
72
|
+
# App.xaml normally does this merge declaratively; a bare Application()
|
|
73
|
+
# built from Python has no resources until we merge Pane.xaml ourselves.
|
|
74
|
+
merged = ResourceDictionary()
|
|
75
|
+
merged.Source = Uri(_PANE_XAML_URI)
|
|
76
|
+
app.Resources.MergedDictionaries.Add(merged)
|
|
77
|
+
|
|
78
|
+
ThemeManager.Initialize(PaneTheme.Dark if theme == "dark" else PaneTheme.Light)
|
|
79
|
+
|
|
80
|
+
window = PaneWindow()
|
|
81
|
+
window.Title = title
|
|
82
|
+
window.Width = width
|
|
83
|
+
window.Height = height
|
|
84
|
+
if not resizable:
|
|
85
|
+
window.ResizeMode = ResizeMode.NoResize
|
|
86
|
+
|
|
87
|
+
_state["window"] = window
|
|
88
|
+
_state["dispatcher"] = window.Dispatcher
|
|
89
|
+
_state["ui_thread_id"] = threading.get_ident()
|
|
90
|
+
|
|
91
|
+
builder(window)
|
|
92
|
+
|
|
93
|
+
ready.set()
|
|
94
|
+
app.Run(window)
|
|
95
|
+
except Exception as ex: # noqa: BLE001 - surfaced to the caller below
|
|
96
|
+
error_box["error"] = ex
|
|
97
|
+
ready.set()
|
|
98
|
+
finally:
|
|
99
|
+
_state["app"] = None
|
|
100
|
+
_state["window"] = None
|
|
101
|
+
_state["dispatcher"] = None
|
|
102
|
+
_state["ui_thread_id"] = None
|
|
103
|
+
|
|
104
|
+
thread = Thread(ThreadStart(ui_main))
|
|
105
|
+
thread.SetApartmentState(ApartmentState.STA)
|
|
106
|
+
thread.Start()
|
|
107
|
+
|
|
108
|
+
ready.wait()
|
|
109
|
+
if "error" in error_box:
|
|
110
|
+
raise error_box["error"]
|
|
111
|
+
|
|
112
|
+
thread.Join()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Layout container helpers: stack, grid, card, scroll."""
|
|
2
|
+
|
|
3
|
+
from . import _bootstrap
|
|
4
|
+
|
|
5
|
+
_bootstrap.ensure_loaded()
|
|
6
|
+
|
|
7
|
+
from System.Windows import GridLength, GridUnitType, Thickness # noqa: E402
|
|
8
|
+
|
|
9
|
+
from ._convert import to_thickness # noqa: E402
|
|
10
|
+
from .widgets import _find_style # noqa: E402
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def stack(*children, orientation="vertical", spacing=0, margin=None):
|
|
14
|
+
from System.Windows.Controls import Orientation, StackPanel
|
|
15
|
+
|
|
16
|
+
panel = StackPanel()
|
|
17
|
+
panel.Orientation = Orientation.Vertical if orientation == "vertical" else Orientation.Horizontal
|
|
18
|
+
if margin is not None:
|
|
19
|
+
panel.Margin = to_thickness(margin)
|
|
20
|
+
|
|
21
|
+
for i, child in enumerate(children):
|
|
22
|
+
if spacing and i > 0:
|
|
23
|
+
m = child.Margin
|
|
24
|
+
if orientation == "vertical":
|
|
25
|
+
child.Margin = Thickness(m.Left, spacing, m.Right, m.Bottom)
|
|
26
|
+
else:
|
|
27
|
+
child.Margin = Thickness(spacing, m.Top, m.Right, m.Bottom)
|
|
28
|
+
panel.Children.Add(child)
|
|
29
|
+
return panel
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parse_length(value):
|
|
33
|
+
if value == "auto":
|
|
34
|
+
return GridLength(1, GridUnitType.Auto)
|
|
35
|
+
if isinstance(value, str) and value.endswith("*"):
|
|
36
|
+
factor = 1.0 if value == "*" else float(value[:-1])
|
|
37
|
+
return GridLength(factor, GridUnitType.Star)
|
|
38
|
+
return GridLength(float(value), GridUnitType.Pixel)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def grid(children, *, rows=None, columns=None, margin=None):
|
|
42
|
+
"""
|
|
43
|
+
children: a list of (row, col, widget) or (row, col, widget, row_span, col_span) tuples.
|
|
44
|
+
rows / columns: lists of "auto", "*", "2*", or a pixel size, e.g. rows=["auto", "*"].
|
|
45
|
+
"""
|
|
46
|
+
from System.Windows.Controls import ColumnDefinition, Grid, RowDefinition
|
|
47
|
+
|
|
48
|
+
g = Grid()
|
|
49
|
+
for r in rows or ["auto"]:
|
|
50
|
+
rd = RowDefinition()
|
|
51
|
+
rd.Height = _parse_length(r)
|
|
52
|
+
g.RowDefinitions.Add(rd)
|
|
53
|
+
for c in columns or ["auto"]:
|
|
54
|
+
cd = ColumnDefinition()
|
|
55
|
+
cd.Width = _parse_length(c)
|
|
56
|
+
g.ColumnDefinitions.Add(cd)
|
|
57
|
+
|
|
58
|
+
for entry in children:
|
|
59
|
+
row, col, widget = entry[0], entry[1], entry[2]
|
|
60
|
+
row_span = entry[3] if len(entry) > 3 else 1
|
|
61
|
+
col_span = entry[4] if len(entry) > 4 else 1
|
|
62
|
+
Grid.SetRow(widget, row)
|
|
63
|
+
Grid.SetColumn(widget, col)
|
|
64
|
+
if row_span > 1:
|
|
65
|
+
Grid.SetRowSpan(widget, row_span)
|
|
66
|
+
if col_span > 1:
|
|
67
|
+
Grid.SetColumnSpan(widget, col_span)
|
|
68
|
+
g.Children.Add(widget)
|
|
69
|
+
|
|
70
|
+
if margin is not None:
|
|
71
|
+
g.Margin = to_thickness(margin)
|
|
72
|
+
return g
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def card(*children, padding=20, spacing=12, orientation="vertical", margin=None):
|
|
76
|
+
from System.Windows.Controls import Border
|
|
77
|
+
|
|
78
|
+
border = Border()
|
|
79
|
+
border.Style = _find_style("Pane.Card")
|
|
80
|
+
if padding is not None:
|
|
81
|
+
border.Padding = to_thickness(padding)
|
|
82
|
+
if margin is not None:
|
|
83
|
+
border.Margin = to_thickness(margin)
|
|
84
|
+
border.Child = stack(*children, orientation=orientation, spacing=spacing)
|
|
85
|
+
return border
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def sidebar(*children, width=240):
|
|
89
|
+
from System.Windows.Controls import DockPanel
|
|
90
|
+
|
|
91
|
+
border_style = _find_style("Pane.Sidebar")
|
|
92
|
+
from System.Windows.Controls import Border
|
|
93
|
+
|
|
94
|
+
border = Border()
|
|
95
|
+
border.Style = border_style
|
|
96
|
+
border.Width = width
|
|
97
|
+
|
|
98
|
+
panel = DockPanel()
|
|
99
|
+
panel.LastChildFill = True
|
|
100
|
+
for child in children:
|
|
101
|
+
panel.Children.Add(child)
|
|
102
|
+
border.Child = panel
|
|
103
|
+
return border
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def scroll(child, *, padding=None):
|
|
107
|
+
from System.Windows.Controls import ScrollViewer
|
|
108
|
+
|
|
109
|
+
sv = ScrollViewer()
|
|
110
|
+
sv.Content = child
|
|
111
|
+
if padding is not None:
|
|
112
|
+
sv.Padding = to_thickness(padding)
|
|
113
|
+
return sv
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Theme and accent-color control - thin wrappers over Pane.ThemeManager."""
|
|
2
|
+
|
|
3
|
+
from . import _bootstrap
|
|
4
|
+
|
|
5
|
+
_bootstrap.ensure_loaded()
|
|
6
|
+
|
|
7
|
+
from Pane import PaneTheme, ThemeManager # noqa: E402
|
|
8
|
+
|
|
9
|
+
from ._convert import to_color # noqa: E402
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def set_theme(theme):
|
|
13
|
+
"""theme: "dark" or "light"."""
|
|
14
|
+
ThemeManager.SetTheme(PaneTheme.Dark if theme == "dark" else PaneTheme.Light)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def toggle_theme():
|
|
18
|
+
ThemeManager.ToggleTheme()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def current_theme():
|
|
22
|
+
return "dark" if ThemeManager.CurrentTheme == PaneTheme.Dark else "light"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_accent(color):
|
|
26
|
+
"""color: a "#RRGGBB" / "#AARRGGBB" hex string, or an (r,g,b) / (a,r,g,b) tuple."""
|
|
27
|
+
ThemeManager.SetAccentColor(to_color(color))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def reset_accent():
|
|
31
|
+
ThemeManager.ResetAccentColor()
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Widget factory functions - each returns a real, Pane-styled WPF control.
|
|
2
|
+
|
|
3
|
+
Most Pane controls (checkboxes, radio buttons, text boxes, sliders, ...) are
|
|
4
|
+
styled implicitly by Pane.xaml, so a plain `CheckBox()` picks up Pane's look
|
|
5
|
+
automatically once it's added to a window built by pane.run(). Only the
|
|
6
|
+
handful of controls with named styles (Button variants, ToggleSwitch, Card,
|
|
7
|
+
ColorSwatch, NavigationList) need an explicit style lookup here.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from . import _bootstrap
|
|
11
|
+
|
|
12
|
+
_bootstrap.ensure_loaded()
|
|
13
|
+
|
|
14
|
+
from System.Windows import Application, FontWeights, TextWrapping # noqa: E402
|
|
15
|
+
|
|
16
|
+
from ._convert import to_thickness # noqa: E402
|
|
17
|
+
from ._events import make_handler # noqa: E402
|
|
18
|
+
|
|
19
|
+
_FONT_SIZE_KEYS = {
|
|
20
|
+
"caption": "Pane.Font.Size.Caption",
|
|
21
|
+
"body": "Pane.Font.Size.Body",
|
|
22
|
+
"subtitle": "Pane.Font.Size.Subtitle",
|
|
23
|
+
"title": "Pane.Font.Size.Title",
|
|
24
|
+
"title-large": "Pane.Font.Size.TitleLarge",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_TEXT_COLOR_KEYS = {
|
|
28
|
+
"primary": "Pane.Brush.Text.Primary",
|
|
29
|
+
"secondary": "Pane.Brush.Text.Secondary",
|
|
30
|
+
"disabled": "Pane.Brush.Text.Disabled",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _find_style(key):
|
|
35
|
+
app = Application.Current
|
|
36
|
+
if app is None:
|
|
37
|
+
raise RuntimeError(
|
|
38
|
+
"No Pane window is running yet - build widgets inside the function "
|
|
39
|
+
"you pass to pane.run(), not before it."
|
|
40
|
+
)
|
|
41
|
+
return app.FindResource(key)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _apply_common(element, *, width=None, height=None, margin=None, enabled=None):
|
|
45
|
+
if width is not None:
|
|
46
|
+
element.Width = width
|
|
47
|
+
if height is not None:
|
|
48
|
+
element.Height = height
|
|
49
|
+
if margin is not None:
|
|
50
|
+
element.Margin = to_thickness(margin)
|
|
51
|
+
if enabled is not None:
|
|
52
|
+
element.IsEnabled = enabled
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --- Buttons ----------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
def button(label, *, style="secondary", on_click=None, width=None, height=None, margin=None, enabled=True):
|
|
58
|
+
from System.Windows.Controls import Button
|
|
59
|
+
|
|
60
|
+
btn = Button()
|
|
61
|
+
btn.Content = label
|
|
62
|
+
if style == "primary":
|
|
63
|
+
btn.Style = _find_style("Pane.Button.Primary")
|
|
64
|
+
elif style == "ghost":
|
|
65
|
+
btn.Style = _find_style("Pane.Button.Ghost")
|
|
66
|
+
# else "secondary": leave unset - Pane's implicit Button style applies.
|
|
67
|
+
_apply_common(btn, width=width, height=height, margin=margin, enabled=enabled)
|
|
68
|
+
if on_click:
|
|
69
|
+
btn.Click += make_handler(on_click, lambda: None)
|
|
70
|
+
return btn
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --- Selection ----------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def checkbox(label, *, checked=False, on_change=None, width=None, margin=None, enabled=True):
|
|
76
|
+
from System.Windows.Controls import CheckBox
|
|
77
|
+
|
|
78
|
+
cb = CheckBox()
|
|
79
|
+
cb.Content = label
|
|
80
|
+
cb.IsChecked = checked
|
|
81
|
+
_apply_common(cb, width=width, margin=margin, enabled=enabled)
|
|
82
|
+
if on_change:
|
|
83
|
+
handler = make_handler(on_change, lambda: cb.IsChecked)
|
|
84
|
+
cb.Checked += handler
|
|
85
|
+
cb.Unchecked += handler
|
|
86
|
+
cb.Indeterminate += handler
|
|
87
|
+
return cb
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def radio_button(label, group, *, checked=False, on_change=None, width=None, margin=None, enabled=True):
|
|
91
|
+
from System.Windows.Controls import RadioButton
|
|
92
|
+
|
|
93
|
+
rb = RadioButton()
|
|
94
|
+
rb.Content = label
|
|
95
|
+
rb.GroupName = group
|
|
96
|
+
rb.IsChecked = checked
|
|
97
|
+
_apply_common(rb, width=width, margin=margin, enabled=enabled)
|
|
98
|
+
if on_change:
|
|
99
|
+
rb.Checked += make_handler(on_change, lambda: True)
|
|
100
|
+
return rb
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def toggle_switch(*, checked=False, on_change=None, margin=None, enabled=True):
|
|
104
|
+
from System.Windows.Controls.Primitives import ToggleButton
|
|
105
|
+
|
|
106
|
+
tb = ToggleButton()
|
|
107
|
+
tb.Style = _find_style("Pane.ToggleSwitch")
|
|
108
|
+
tb.IsChecked = checked
|
|
109
|
+
_apply_common(tb, margin=margin, enabled=enabled)
|
|
110
|
+
if on_change:
|
|
111
|
+
handler = make_handler(on_change, lambda: bool(tb.IsChecked))
|
|
112
|
+
tb.Checked += handler
|
|
113
|
+
tb.Unchecked += handler
|
|
114
|
+
return tb
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# --- Text input ----------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def text_box(*, text="", placeholder="", on_change=None, width=None, margin=None, enabled=True):
|
|
120
|
+
from System.Windows.Controls import TextBox
|
|
121
|
+
from Pane.Controls import PaneAssist
|
|
122
|
+
|
|
123
|
+
tbx = TextBox()
|
|
124
|
+
tbx.Text = text
|
|
125
|
+
if placeholder:
|
|
126
|
+
PaneAssist.SetPlaceholder(tbx, placeholder)
|
|
127
|
+
_apply_common(tbx, width=width, margin=margin, enabled=enabled)
|
|
128
|
+
if on_change:
|
|
129
|
+
tbx.TextChanged += make_handler(on_change, lambda: tbx.Text)
|
|
130
|
+
return tbx
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def password_box(*, on_change=None, width=None, margin=None, enabled=True):
|
|
134
|
+
from System.Windows.Controls import PasswordBox
|
|
135
|
+
|
|
136
|
+
pwd = PasswordBox()
|
|
137
|
+
_apply_common(pwd, width=width, margin=margin, enabled=enabled)
|
|
138
|
+
if on_change:
|
|
139
|
+
pwd.PasswordChanged += make_handler(on_change, lambda: pwd.Password)
|
|
140
|
+
return pwd
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# --- Progress & range ----------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def progress_bar(*, value=0, minimum=0, maximum=100, indeterminate=False, width=None, margin=None):
|
|
146
|
+
from System.Windows.Controls import ProgressBar
|
|
147
|
+
|
|
148
|
+
pb = ProgressBar()
|
|
149
|
+
pb.Minimum = minimum
|
|
150
|
+
pb.Maximum = maximum
|
|
151
|
+
pb.Value = value
|
|
152
|
+
pb.IsIndeterminate = indeterminate
|
|
153
|
+
_apply_common(pb, width=width, margin=margin)
|
|
154
|
+
return pb
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def slider(*, value=0, minimum=0, maximum=100, on_change=None, width=None, margin=None, enabled=True):
|
|
158
|
+
from System.Windows.Controls import Slider
|
|
159
|
+
|
|
160
|
+
s = Slider()
|
|
161
|
+
s.Minimum = minimum
|
|
162
|
+
s.Maximum = maximum
|
|
163
|
+
s.Value = value
|
|
164
|
+
_apply_common(s, width=width, margin=margin, enabled=enabled)
|
|
165
|
+
if on_change:
|
|
166
|
+
s.ValueChanged += make_handler(on_change, lambda: s.Value)
|
|
167
|
+
return s
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# --- Choices & lists ----------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def combo_box(items, *, selected_index=0, on_change=None, width=None, margin=None, enabled=True):
|
|
173
|
+
from System.Windows.Controls import ComboBox, ComboBoxItem
|
|
174
|
+
|
|
175
|
+
cb = ComboBox()
|
|
176
|
+
for item in items:
|
|
177
|
+
cbi = ComboBoxItem()
|
|
178
|
+
cbi.Content = str(item)
|
|
179
|
+
cb.Items.Add(cbi)
|
|
180
|
+
if items:
|
|
181
|
+
cb.SelectedIndex = selected_index
|
|
182
|
+
_apply_common(cb, width=width, margin=margin, enabled=enabled)
|
|
183
|
+
if on_change:
|
|
184
|
+
cb.SelectionChanged += make_handler(on_change, lambda: cb.SelectedIndex)
|
|
185
|
+
return cb
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def list_box(items, *, selected_index=0, on_select=None, width=None, height=None, margin=None, enabled=True):
|
|
189
|
+
from System.Windows.Controls import ListBox, ListBoxItem
|
|
190
|
+
|
|
191
|
+
lb = ListBox()
|
|
192
|
+
for item in items:
|
|
193
|
+
lbi = ListBoxItem()
|
|
194
|
+
lbi.Content = str(item)
|
|
195
|
+
lb.Items.Add(lbi)
|
|
196
|
+
if items:
|
|
197
|
+
lb.SelectedIndex = selected_index
|
|
198
|
+
_apply_common(lb, width=width, height=height, margin=margin, enabled=enabled)
|
|
199
|
+
if on_select:
|
|
200
|
+
lb.SelectionChanged += make_handler(on_select, lambda: lb.SelectedIndex)
|
|
201
|
+
return lb
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def nav_list(items, *, selected_index=0, on_select=None):
|
|
205
|
+
"""A Pane-styled sidebar navigation list (see Pane.NavigationList)."""
|
|
206
|
+
from System.Windows.Controls import ListBox, ListBoxItem
|
|
207
|
+
|
|
208
|
+
lb = ListBox()
|
|
209
|
+
lb.Style = _find_style("Pane.NavigationList")
|
|
210
|
+
for item in items:
|
|
211
|
+
lbi = ListBoxItem()
|
|
212
|
+
lbi.Content = str(item)
|
|
213
|
+
lb.Items.Add(lbi)
|
|
214
|
+
if items:
|
|
215
|
+
lb.SelectedIndex = selected_index
|
|
216
|
+
if on_select:
|
|
217
|
+
lb.SelectionChanged += make_handler(on_select, lambda: lb.SelectedIndex)
|
|
218
|
+
return lb
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def tab_control(tabs, *, width=None, height=None, margin=None):
|
|
222
|
+
"""tabs: a dict of {header: content_element}, or a list of (header, content) pairs."""
|
|
223
|
+
from System.Windows.Controls import TabControl, TabItem
|
|
224
|
+
|
|
225
|
+
tc = TabControl()
|
|
226
|
+
entries = tabs.items() if isinstance(tabs, dict) else tabs
|
|
227
|
+
for header, content in entries:
|
|
228
|
+
ti = TabItem()
|
|
229
|
+
ti.Header = header
|
|
230
|
+
ti.Content = content
|
|
231
|
+
tc.Items.Add(ti)
|
|
232
|
+
_apply_common(tc, width=width, height=height, margin=margin)
|
|
233
|
+
return tc
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# --- Layout & overlays ----------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
def expander(header, content, *, expanded=False, margin=None):
|
|
239
|
+
from System.Windows.Controls import Expander
|
|
240
|
+
|
|
241
|
+
e = Expander()
|
|
242
|
+
e.Header = header
|
|
243
|
+
e.Content = content
|
|
244
|
+
e.IsExpanded = expanded
|
|
245
|
+
_apply_common(e, margin=margin)
|
|
246
|
+
return e
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def group_box(header, content, *, margin=None):
|
|
250
|
+
from System.Windows.Controls import GroupBox
|
|
251
|
+
|
|
252
|
+
g = GroupBox()
|
|
253
|
+
g.Header = header
|
|
254
|
+
g.Content = content
|
|
255
|
+
_apply_common(g, margin=margin)
|
|
256
|
+
return g
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def separator(*, margin=None):
|
|
260
|
+
from System.Windows.Controls import Separator
|
|
261
|
+
|
|
262
|
+
s = Separator()
|
|
263
|
+
_apply_common(s, margin=margin)
|
|
264
|
+
return s
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def text(content, *, size="body", bold=False, color="primary", wrap=False, margin=None):
|
|
268
|
+
from System.Windows.Controls import TextBlock
|
|
269
|
+
|
|
270
|
+
tb = TextBlock()
|
|
271
|
+
tb.Text = str(content)
|
|
272
|
+
# SetResourceReference (not a direct value assignment) so this text keeps
|
|
273
|
+
# tracking theme/accent changes live, the same way {DynamicResource} does in XAML.
|
|
274
|
+
tb.SetResourceReference(TextBlock.FontFamilyProperty, "Pane.Font.Family")
|
|
275
|
+
tb.SetResourceReference(TextBlock.FontSizeProperty, _FONT_SIZE_KEYS.get(size, _FONT_SIZE_KEYS["body"]))
|
|
276
|
+
tb.SetResourceReference(TextBlock.ForegroundProperty, _TEXT_COLOR_KEYS.get(color, _TEXT_COLOR_KEYS["primary"]))
|
|
277
|
+
if bold:
|
|
278
|
+
tb.FontWeight = FontWeights.SemiBold
|
|
279
|
+
if wrap:
|
|
280
|
+
tb.TextWrapping = TextWrapping.Wrap
|
|
281
|
+
_apply_common(tb, margin=margin)
|
|
282
|
+
return tb
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def set_tooltip(element, tooltip_text):
|
|
286
|
+
element.ToolTip = tooltip_text
|
|
287
|
+
return element
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def set_context_menu(element, items):
|
|
291
|
+
"""items: a list of (label, on_click) pairs; use None for a separator."""
|
|
292
|
+
from System.Windows.Controls import ContextMenu, MenuItem, Separator
|
|
293
|
+
|
|
294
|
+
menu = ContextMenu()
|
|
295
|
+
for item in items:
|
|
296
|
+
if item is None:
|
|
297
|
+
menu.Items.Add(Separator())
|
|
298
|
+
continue
|
|
299
|
+
label, on_click = item
|
|
300
|
+
mi = MenuItem()
|
|
301
|
+
mi.Header = label
|
|
302
|
+
if on_click:
|
|
303
|
+
mi.Click += make_handler(on_click, lambda: None)
|
|
304
|
+
menu.Items.Add(mi)
|
|
305
|
+
element.ContextMenu = menu
|
|
306
|
+
return element
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pane-ui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bindings for Pane, a modern minimalist Windows UI engine built on WPF
|
|
5
|
+
Author: Hunter
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/HunterH1218/pane
|
|
8
|
+
Project-URL: Repository, https://github.com/HunterH1218/pane
|
|
9
|
+
Project-URL: Issues, https://github.com/HunterH1218/pane/issues
|
|
10
|
+
Keywords: wpf,windows,ui,gui,desktop,pythonnet
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: C#
|
|
16
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
17
|
+
Classifier: Topic :: Software Development :: Widget Sets
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: pythonnet>=3.0
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# pane
|
|
25
|
+
|
|
26
|
+
Python bindings for [Pane](https://github.com/HunterH1218/pane) — a modern, minimalist
|
|
27
|
+
Windows UI engine built on WPF. Build real native Windows app UIs from Python:
|
|
28
|
+
buttons, checkboxes, sliders, text input, tabs, live light/dark theming and
|
|
29
|
+
accent-color switching, all with a single custom-chrome window.
|
|
30
|
+
|
|
31
|
+
The engine itself is C#/WPF; this package drives it in-process via
|
|
32
|
+
[pythonnet](https://pythonnet.github.io/), so widgets are the real native
|
|
33
|
+
controls, not a re-implementation.
|
|
34
|
+
|
|
35
|
+
**Windows only** (WPF has no cross-platform equivalent). Requires the free
|
|
36
|
+
[.NET 8 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/8.0) —
|
|
37
|
+
not the SDK, just the runtime.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pip install pane-ui
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import pane
|
|
49
|
+
|
|
50
|
+
def build(window):
|
|
51
|
+
window.Content = pane.stack(
|
|
52
|
+
pane.text("Hello from Python", size="title", bold=True),
|
|
53
|
+
pane.button("Click me", style="primary", on_click=lambda: print("clicked!")),
|
|
54
|
+
pane.checkbox("Enable notifications", checked=True),
|
|
55
|
+
spacing=12,
|
|
56
|
+
margin=24,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
pane.run(build, title="My App")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run the full widget gallery from the source repo for a tour of everything
|
|
63
|
+
available: `python examples/gallery.py`.
|
|
64
|
+
|
|
65
|
+
## Widgets
|
|
66
|
+
|
|
67
|
+
`button`, `checkbox`, `radio_button`, `toggle_switch`, `text_box`,
|
|
68
|
+
`password_box`, `progress_bar`, `slider`, `combo_box`, `list_box`, `nav_list`,
|
|
69
|
+
`tab_control`, `expander`, `group_box`, `separator`, `text`, plus
|
|
70
|
+
`set_tooltip` / `set_context_menu` helpers.
|
|
71
|
+
|
|
72
|
+
## Layout
|
|
73
|
+
|
|
74
|
+
`stack(*children, orientation=, spacing=, margin=)`,
|
|
75
|
+
`grid(children, rows=, columns=)`, `card(*children)`, `sidebar(*children)`,
|
|
76
|
+
`scroll(child)`.
|
|
77
|
+
|
|
78
|
+
## Theming
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
pane.set_theme("light") # or "dark"
|
|
82
|
+
pane.toggle_theme()
|
|
83
|
+
pane.set_accent("#4C82F7") # hex string or (r, g, b) tuple
|
|
84
|
+
pane.reset_accent()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Every widget re-themes live, mid-run, no restart needed.
|
|
88
|
+
|
|
89
|
+
## Events
|
|
90
|
+
|
|
91
|
+
Callbacks adapt to whatever you write - zero args or one:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
pane.button("Save", on_click=lambda: print("saved"))
|
|
95
|
+
pane.slider(on_change=lambda value: print(value))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Threading
|
|
99
|
+
|
|
100
|
+
`pane.run(builder)` builds the window on a dedicated UI thread and blocks
|
|
101
|
+
until it's closed. Build your widget tree inside `builder`. To update the UI
|
|
102
|
+
later from another thread (a timer, a background task), use
|
|
103
|
+
`pane.invoke(fn)` to marshal back onto the UI thread.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pane/__init__.py
|
|
5
|
+
pane/_bootstrap.py
|
|
6
|
+
pane/_convert.py
|
|
7
|
+
pane/_events.py
|
|
8
|
+
pane/app.py
|
|
9
|
+
pane/layout.py
|
|
10
|
+
pane/theme.py
|
|
11
|
+
pane/widgets.py
|
|
12
|
+
pane/_native/Pane.dll
|
|
13
|
+
pane/_native/Pane.runtimeconfig.json
|
|
14
|
+
pane_ui.egg-info/PKG-INFO
|
|
15
|
+
pane_ui.egg-info/SOURCES.txt
|
|
16
|
+
pane_ui.egg-info/dependency_links.txt
|
|
17
|
+
pane_ui.egg-info/requires.txt
|
|
18
|
+
pane_ui.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pythonnet>=3.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pane
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pane-ui"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python bindings for Pane, a modern minimalist Windows UI engine built on WPF"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Hunter" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["wpf", "windows", "ui", "gui", "desktop", "pythonnet"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: Microsoft :: Windows",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: C#",
|
|
23
|
+
"Topic :: Software Development :: User Interfaces",
|
|
24
|
+
"Topic :: Software Development :: Widget Sets",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"pythonnet>=3.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/HunterH1218/pane"
|
|
32
|
+
Repository = "https://github.com/HunterH1218/pane"
|
|
33
|
+
Issues = "https://github.com/HunterH1218/pane/issues"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
include = ["pane*"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
pane = ["_native/*.dll", "_native/*.json"]
|
pane_ui-0.1.0/setup.cfg
ADDED