Intuitive desktop UI framework for Python
Project description
Nuiitivet
An intuitive UI framework for Python.
1. Why Nuiitivet?
I have just one thing to say: I want to write UI intuitively.
1.1 Declarative UI
Nuiitivet offers a declarative UI that blends the best parts of frameworks like Flutter, SwiftUI, and WPF.
At its core, you build UIs by composing widgets, just like in Flutter.
login_form = Column(
[
# Username and Password fields
TextField(
value="",
label="Username",
width=300,
),
TextField(
value="",
label="Password",
width=300,
),
# Login Button
Button(
"Login",
on_click=lambda: print("Login clicked"),
width=300,
)
],
gap=20,
padding=20,
)
What sets Nuiitivet apart from Flutter is that size, alignment, and spacing are specified as widget parameters. Treating them as parameters of a widget — rather than as widgets in their own right — feels more natural, and it lets you avoid the deep nesting hell that Flutter tends to fall into.
# Writing in Flutter style often leads to deep nesting
Padding(
padding=EdgeInsets.all(12),
child=SizedBox(
width=200,
child=Text("Hello"),
),
)
# With Nuiitivet, you can specify them directly
Text(
"Hello",
padding=12,
width=200,
)
Nuiitivet also adopts modifiers from SwiftUI and Jetpack Compose.
Instead of wrapping a widget, you attach decoration and behavior so they feel like they grow out of the widget — and they chain together naturally with |.
Button("OK").modifier(
tooltip("Submit") | clickable(...) | background("#2196F3")
)
For why modifiers exist and what kinds are available, see docs/guide/modifier.md.
1.2 Data Binding
Dynamic UIs need state management. Starting with React (the JavaScript one), many frameworks rebuild the UI whenever state changes. Nuiitivet doesn't rebuild — it binds state to the UI instead.
That mechanism is Observable. It binds directly to the UI like Signals (SolidJS, MobX), and it also carries operators like throttle() and debounce() like Rx — the best of both worlds. (It's inspired by WPF's ReactiveProperty.)
Let me walk you through three things I like about it.
1. Complete separation of state and UI
When you set a value on an Observable, the bound UI updates automatically. Inside build(), all you ever write is the UI declaration.
class CounterApp(ComposableWidget):
def __init__(self):
super().__init__()
self.count = Observable(0)
def increment(self):
self.count.value += 1
def build(self):
return Column(
[
# Count display
Text(self.count),
# Increment button
Button(
"Increment",
on_click=self.increment,
)
]
)
With the ViewModel pattern, you can take this even further — separating cleanly at the class level rather than the method level.
2. Declarative data flow
State derived from multiple values can be declared as a formula. Below, total is defined as the sum of count_a and count_b; whenever either one changes, it's recalculated automatically. On the UI side, you just drop in total as is.
self.count_a = Observable(0)
self.count_b = Observable(0)
# total is declared as a + b; it recalculates automatically when a or b changes
self.total = self.count_a.combine(self.count_b).compute(lambda a, b: a + b)
3. Async in the same style
This is where the ReactiveProperty heritage really shines. You can slot in an Rx-style operator like debounce() and then bind the result straight to the UI. Even something like a search box — "thin out the keystrokes, then process" — is a single line.
self.query = Observable("")
# debounce like Rx, then bind the result to the UI like Signals
self.results = self.query.debounce(0.3).map(search_api)
The full guide to Observable is in docs/guide/observable.md.
1.3 Event Handlers
Event handlers like on_click() are written imperatively. A handler does procedural things — popping up a dialog, branching on its result — so writing it imperatively feels natural.
class CounterApp(ComposableWidget):
count = Observable(0)
# Write procedures in event handler
def handle_increment(self):
# 1. Output log
print(f"Current count: {self.count.value}")
# 2. Increment count
self.count.value += 1
# 3. Milestone check
if self.count.value % 10 == 0:
print("Milestone reached!")
def build(self):
return Column(
[
Text(f"count: {self.count.value}"),
Button(
"Increment",
on_click=self.handle_increment, # Execute on click
)
]
)
Logic → UI declaratively, UI → logic imperatively. What matters in both directions is that it stays intuitive to write — and that's the one thing Nuiitivet stands for.
2. First Steps
2.1. Requirements
- Python 3.10 or higher
- macOS / Windows / Linux(not tested)
Main internal libraries used (drawing/rendering):
- pyglet
- PyOpenGL
- skia-python
- material-color-utilities
See LICENSES/ for third-party licenses.
2.2. Installation
You can install it easily with pip.
pip install nuiitivet
2.3. Your First App
To create an application with Nuiitivet, follow these two steps:
- Inherit from
ComposableWidgetto create a UI component - Pass the UI component to
Appand start the application
from nuiitivet import Column, ComposableWidget, Observable
from nuiitivet.material import App, Text, Button
class CounterApp(ComposableWidget):
def __init__(self):
super().__init__()
self.count = Observable(0)
def handle_increment(self):
# 1. Output log
print(f"Current count: {self.count.value}")
# 2. Increment count
self.count.value += 1
# 3. Milestone check
if self.count.value % 10 == 0:
print("Milestone reached!")
def build(self):
return Column(
[
Text(self.count),
Button(
"Increment",
on_click=self.handle_increment,
)
],
gap=20,
padding=20,
)
def main():
# Create counter app
counter_app = CounterApp()
# Start with App
app = App(content=counter_app)
app.run()
if __name__ == "__main__":
main()
3. Documentation
For a deep dive into Nuiitivet's design, visit the docs site. Browse runnable examples in samples/ — every snippet in this README lives there as a runnable module under samples/readme/.
Core Concepts
| Guide | Summary |
|---|---|
| Layout | Build UIs with widgets and parameters. |
| Observable | Reactive state that auto-updates the UI. |
| Modifier | Attach decoration and behavior to widgets. |
| UI Design System | Theming and design tokens. |
Material Design
| Guide | Summary |
|---|---|
| Material App | App entry point and structure. |
| Material Theme | Color schemes generated from a seed. |
| Material Widgets | Catalog of built-in widgets. |
| Navigation | Screens, routes, and transitions. |
| Dialogs & Overlays | Dialogs, loading, and overlays. |
Going Further
| Guide | Summary |
|---|---|
| Window & Chrome | Window sizing, position, and custom chrome. |
| Async & Threading | Safe UI updates from background work. |
| Packaging | Ship your app to users. |
4. License
Nuiitivet is licensed under the Apache License 2.0. See the LICENSE file for more info.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nuiitivet-0.9.0.tar.gz.
File metadata
- Download URL: nuiitivet-0.9.0.tar.gz
- Upload date:
- Size: 16.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9a15321254930fc8533a1c34edaab2b7d636b7449f8f64c74342f3d0e6825c8f
|
|
| MD5 |
a0cf728b9a312db25c549d2248f59cf3
|
|
| BLAKE2b-256 |
7475166fd4fa1f71b9499fed9c72c17b0ef218abbc4b962cdaeb93ea8cfbc8dd
|
Provenance
The following attestation bundles were made for nuiitivet-0.9.0.tar.gz:
Publisher:
release.yml on yuksblog/nuiitivet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nuiitivet-0.9.0.tar.gz -
Subject digest:
9a15321254930fc8533a1c34edaab2b7d636b7449f8f64c74342f3d0e6825c8f - Sigstore transparency entry: 1988827183
- Sigstore integration time:
-
Permalink:
yuksblog/nuiitivet@ffe84e2e79b05266d28407b5b3c411046ea4fba2 -
Branch / Tag:
refs/tags/0.9.0 - Owner: https://github.com/yuksblog
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ffe84e2e79b05266d28407b5b3c411046ea4fba2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file nuiitivet-0.9.0-py3-none-any.whl.
File metadata
- Download URL: nuiitivet-0.9.0-py3-none-any.whl
- Upload date:
- Size: 16.6 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79d2e820d73c4a9488f65c13eac5a14fde23260ed0be1c318a23db023871fb1e
|
|
| MD5 |
3c1454dfb877ecf0ad2fcc0b907e57d3
|
|
| BLAKE2b-256 |
4f13385016f76726f40216f4d4afa6b71bdb8220b1c445c35cf261bc58fb76ff
|
Provenance
The following attestation bundles were made for nuiitivet-0.9.0-py3-none-any.whl:
Publisher:
release.yml on yuksblog/nuiitivet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nuiitivet-0.9.0-py3-none-any.whl -
Subject digest:
79d2e820d73c4a9488f65c13eac5a14fde23260ed0be1c318a23db023871fb1e - Sigstore transparency entry: 1988827266
- Sigstore integration time:
-
Permalink:
yuksblog/nuiitivet@ffe84e2e79b05266d28407b5b3c411046ea4fba2 -
Branch / Tag:
refs/tags/0.9.0 - Owner: https://github.com/yuksblog
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ffe84e2e79b05266d28407b5b3c411046ea4fba2 -
Trigger Event:
release
-
Statement type: