Skip to main content

Intuitive desktop UI framework for Python

Project description

Nuiitivet

Nuiitivet showcase

An intuitive UI framework for Python.

PyPI version Python versions License

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,
)

Login form

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,
                )
            ]
        )

Counter

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)

Multi counter

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 ComposableWidget to create a UI component
  • Pass the UI component to App and 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nuiitivet-0.9.0.tar.gz (16.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nuiitivet-0.9.0-py3-none-any.whl (16.6 MB view details)

Uploaded Python 3

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

Hashes for nuiitivet-0.9.0.tar.gz
Algorithm Hash digest
SHA256 9a15321254930fc8533a1c34edaab2b7d636b7449f8f64c74342f3d0e6825c8f
MD5 a0cf728b9a312db25c549d2248f59cf3
BLAKE2b-256 7475166fd4fa1f71b9499fed9c72c17b0ef218abbc4b962cdaeb93ea8cfbc8dd

See more details on using hashes here.

Provenance

The following attestation bundles were made for nuiitivet-0.9.0.tar.gz:

Publisher: release.yml on yuksblog/nuiitivet

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for nuiitivet-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 79d2e820d73c4a9488f65c13eac5a14fde23260ed0be1c318a23db023871fb1e
MD5 3c1454dfb877ecf0ad2fcc0b907e57d3
BLAKE2b-256 4f13385016f76726f40216f4d4afa6b71bdb8220b1c445c35cf261bc58fb76ff

See more details on using hashes here.

Provenance

The following attestation bundles were made for nuiitivet-0.9.0-py3-none-any.whl:

Publisher: release.yml on yuksblog/nuiitivet

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page