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 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
@@ -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,10 @@
1
+ {
2
+ "runtimeOptions": {
3
+ "tfm": "net8.0",
4
+ "frameworks": [
5
+ { "name": "Microsoft.NETCore.App", "version": "8.0.0" },
6
+ { "name": "Microsoft.WindowsDesktop.App", "version": "8.0.0" }
7
+ ],
8
+ "rollForward": "LatestMinor"
9
+ }
10
+ }
@@ -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
+ 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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+