RhetTbull / guitk

Python GUI Toolkit for Tk (guitk): simplify the layout and construction of tkinter graphical user interfaces in python using a declarative syntax.
MIT License
16 stars 1 forks source link

[Discussion] Path to composability #52

Closed kevinrpb closed 2 weeks ago

kevinrpb commented 3 weeks ago

Preface

Hey! Thought I'd open an issue to discuss something that I would like to implement. It should also help me with anything I may be missing.

Please note that this is a huge wishlist. The goal of posting it is to discuss the idea and decide a path forward (or decide against). Also, disclaimer: the way I present it is the 'ideal' way I think makes sense, but I also didn't fully research how everything could actually be implemented in python.

TLDR: Can/should we implement something similar to reactpy?


The issue

Perhaps the main 'blocker' I have right now to scale up a full fledge app with the library is composing UIs. While it allows to create simple windows super easily (btw I love the with _: syntax for making it declarative!), it becomes cumbersome (IMO) more complex UIs. Think of the 'example' window. The layout takes ~100 lines of code, which is not much, but it's hard to navigate and match different elements with their event handling.

Instead, I would like to be able to create standalone 'components' and 'hooks' (a la React) where I could do something like this:

from collections.abc import Callable

import guitk as ui

from components import TextEntry

class MainWindow(ui.Window):
    text: str
    set_text: Callable[[str], None]

    def __init__(self):
        super().__init__()

        self.text, self.set_text = self.use_state("Initial text")

    def config(self):
        self.title = "Main Window"

        with ui.VLayout():
            TextEntry(text=self.text, set_text=self.set_text)
            ui.HSeparator()
            ui.Label(f"Text is {self.text}")

Now, I am not 100% of how this can be achieved. I see a few things for sure to be done.

Components

We would need a Component class (or something similar). Currently we have BaseWidget, but that is highly tied to tk/ttk widgets. The components would instead use the widgets (or simply wrap them) to compose a piece of UI. In my mind the usage would be something similar to Window. Here's a super simple example of a TextEntry component that's backed by guitk.Entry:

from collections.abc import Callable

import guitk as ui

class TextEntry(ui.Component):
    _text: str
    _set_text: Callable[[str], None]

    def __init__(self, text: str, set_text: Callable[[str], None]):
        super().__init__()

        self._text = text
        self._set_text = set_text

    def config(self):
        ui.Entry(key="text_entry", default=self._text)

    @ui.on("text_entry")
    def _on_text_entry(self):
        self._set_text(self["text_entry"])

Now, these components should also be composable. Once we have a text entry, we could also create a FormField:

from collections.abc import Callable

import guitk as ui

from . import TextEntry

class FormField(ui.Component):
    _label: str
    _text: str
    _set_text: Callable[[str], None]

    def __init__(self, label: str, text: str, set_text: Callable[[str], None]):
        super().__init__()

        self._label = label
        self._text = text
        self._set_text = set_text

    def config(self):
        with ui.VStack(vexpand=False, halign="left"):
            ui.Label(self._label)

            TextEntry(self._text, set_text=self._set_text)

Containers

A special kind of components would be those that can contain other components/widgets. Here's where my mind goes to following the example and building a Form:

from collections.abc import Callable

import guitk as ui

from . import FormField

class Form(ui.ContainerComponent):
    _title: str

    def __init__(self, title: str):
        super().__init__()

        self._title: title

    def config(self):
        with ui.VStack(vexpand=False, halign="left"):
            ui.Label(self._title)

            *self.children # this property would be a list that we can unpack here, maybe?

We could then use this component and pass 'children' to it:

from collections.abc import Callable

import guitk as ui

from components import Form

class FormWindow(ui.Window):
    name: str
    set_name: Callable[[str], None]

    surname: str
    set_surname: Callable[[str], None]

    def __init__(self):
        super().__init__()

        self.name, self.set_name = self.use_state("")
        self.surname, self.set_surname = self.use_state("")

    def config(self):
        self.title = "Form Window"

        with Form("My form"):
            FormField("Name", text=self.name, set_text=self.set_name)

            FormField("Surname", text=self.surname, set_text=self.set_surname)

Go functional?

Another thing that came to mind (I use React a lot) is creating functional components. After all, not everything needs to be a class.

Now, I haven't put much thought into what this would mean in Python wrt implementation. Thise would perhaps need to be something similar to the current widget_class_factory.

import guitk as ui

@ui.container_component
def Form(title: str, children: list[ui.Component]):
    with ui.VStack(vexpand=False, halign="left"):
        ui.Label(title)

        *self.children

State

The second thing we need is state management. The current approach is using events and handling them in the window class. I think this is still valid and components that use widgets should be able to use a similar approach.

However, windows and containers should be able to have other mechanisms to 'automatically' handle state. For this I was thinking of something similar to React's hooks (the examples above use it). With this, windows/components could have 'hook' methods that support the mechanism. Under the hood, this could still use the current event system for updates, but one main thing is that the view needs to know when to redraw component/widgets based on state changes.

This needs thinking...


References we can use

Now, I went and look around for similar usecases where people implemented React-ish features in Python. I found two notable things:

python-hooks

This is a somehwat simple library that provides a couple implementations of hooks similar to React's (e.g. use_state or use_effect). The library hasn't seen updates in a while and it seems like the project comes from academic work. May be useful as a reference.

reactpy

I just found this after I had written my original ideas, but it looks pretty much like what I was looking for.

This focuses on web development, but looking around the repo it may be possible to either implement something very similar or straight up build a backend for the library. The docs are still under construction, so I am not 100% sure how everything works. Will keep investigating.

kevinrpb commented 3 weeks ago

For reference, here are the docs on how state management works in reactpy: ReactPy - Adding Interactivity.

RhetTbull commented 3 weeks ago

This is an interesting idea. I think it would be ambitious to implement though. I'm not a web dev and have never used React so it took me a couple reads of this to understand what you're proposing but I think I get it now. As you proposed, adding a Tkinter backend for ReactPy might be easier than adding this on to GUITk.

I wrote GUITk mostly as an experiment to try a declarative approach. I wanted something simple for creating GUIs for small utilities. I initially tried PySimpleGUI but when I ran into issues on macOS (I'm primarily a Mac user), I ran into philosophical differences with the maintainer that made working on PySimpleGUI difficult. I thus created GUITk to make it easy to implement basic GUIs and used Tkinter as it was universally available on Python and permissively licensed. I personally would not try to build a big app in Tkinter. I use GUITk regularly for my small utilities but haven't tried to build anything big with it.

(As an aside, PySimpleGUI has now gone commercial license only, requiring registration to receive a license key and they've yanked the old versions off PyPI and purged the GitHub history so that users cannot use the old GPL'd version any longer. In my opinion a very terrible way to treat an open source project that others contributed to.)

I personally don't have much time to devote to such an implementation as I'm busy with several other projects that take up quite a bit of time and for my simple uses, GUITk works well enough. I am happy to help where I can and collaborate on ideas but can't spend much time coding on this. I will warn you that the guts of GUITk leave much to be desired in particular in the layout code. GUITk was initially implemented using a "list of lists" layout approach where the user passed in a list of of list of widgets. Thus there's some internal conversions to/from list of lists for layout. In practice, this didn't work well as the lists became very unwieldy for more than a few widgets and even with a handful of widgets, black formatted the code in such a way as it wasn't very readable. If I did this again, I'd rip all that out and clean up the layout code.

Another idea would be to use something like CustomTkinter as the base for a modern look/feel.

kevinrpb commented 2 weeks ago

Hey, sorry it took me some time to get back to this.

I totally get it, and I didn't mean to seem pushy or anything like that! I wasn't sure of what the status of this as a project was, but it makes sense to me πŸ™‚. GUITk is really useful as is for small utilities, that much is clear and I will continue to use it (and maybe contribute if I find areas where I can). Re: layout code, etc. Makes sense. Let's keep it as the experiment it is and use it for what it's worth.

About PySimpleGUI... Yeah. That's the initial reason I stumbled across this library in the first place. I was looking for alternatives after learning the news... It was really saddening to see, agreed. But alas πŸ€·πŸ½β€β™‚οΈ .

In any case, thank you for taking the time to read and answer! πŸ˜€

PS: Doing some more digging around declarative UIs in Python, I found pyedifice, which uses Qt as a backend, and is more or less what I had in my mind. Here some examples for reference.

RhetTbull commented 2 weeks ago

pyedifice looks interesting --I'll take a look! Always happy for any contributions for GUITk!