MyreMylar / pygame_gui

A GUI system for pygame.
MIT License
701 stars 83 forks source link

Use of a markup language to design the ui #370

Open oscar0urselli opened 1 year ago

oscar0urselli commented 1 year ago

Is your feature request related to a problem? Please describe. I'm currently working on a project where I need to create an high number of gui elements. The problem is that this way things start to get confusing and I end up with a verbose code.

Describe the solution you'd like In order to organize better my code, I moved the declarations of the gui elements in a separated XML file. This way is almost like writing a web page in HTML. Plus, using an XML file (or something like that), it could be possible to introduce a dynamic refresh of the elements every time there is a change.

Describe alternatives you've considered I don't have other ideas, I just wanted to share this concept I implemented (in a doubius way) in my code.

Additional context I understand that it is a very specific request and so there could be no interest in adding it, but I hope you like the idea.

cobyj33 commented 1 year ago

I've also somewhat been experimenting with this because it's a great idea. keeping track of all of the constructors, managers, rects, containers, and anchors somewhat feels overcomplicated once there are more than a few elements. Something like HTML would be perfect.

I've somewhat tried to implement it myself, and as of now it does work pretty well for what I've tried The data would look something like this

<?xml version="1.0"?>
<!DOCTYPE pygamegui>

<pygamegui>
<head>
    <themes>
        <theme>data/styles.json</theme>
        <theme>data/myGlobalTheme.json</theme>
        <theme>
            <package>data.themes</package>
            <resource>mainMenuTheme.json</resource>
        </theme>
    </themes>

</head>

<body resolution="800 600">
    <panel rect="0 0 800 600">
        <panel rect="0 0 600 400" id="title_rect">
            <label rect="0 0 -1 -1" anchors="center: center">Game Title</label>
        </panel>

        <panel rect="0 0 600 100" anchors="top: top, top: #title_rect">
            <button rect="0 0 100 100" id="play_button" class="menu_button" anchors="right: right, right_target: #options_button">Play</button>
            <button rect="0 0 100 100" anchors="center: center" id="options_button" class="menu_button">Options</button>
            <button rect="0 0 100 100" anchors="left: left, left_target: #options_button" id="quit_button" class="menu_button">Quit</button>
        </panel>
    </panel>

    <window rect="0 0 200 200" anchors="center: center">
        <image src="data/myimage.png" rect="0 0 100 100" anchors="center: center" />
    </window>
</body>

</pygamegui>

I'd say that it works pretty well and isn't too verbose, but the structure is not final at all

This isn't the exact output of the example above, but it's very similar and was rendered with my markup implementation Pygame GUI XML Main Menu Example

However, there's some problems that I could see arising from creating a markup language though (somewhat ranting)

Everything has its problems though, and markup is definitely doable and would make life 100x easier, so I agree with you. Would like to know what you think about the direction I'm thinking of and if you agree or disagree with any of it.

oscar0urselli commented 1 year ago

Your XML implementation is almost the same as mine, except fot the theme part.

About the problems you mentionated:

This is the Python code I use in the project I'm working on:

import pygame
import pygame_gui
import os
import xml.etree.ElementTree as xmlET

class XMLStructure:
    def __init__(self, xml_path: str, screen, manager) -> None:
        self.xml_path = xml_path
        self.screen = screen
        self.manager = manager

        # Allowed gui elements
        self.allowed_elements = [
            'image', 'button', 'textbox', 'textentryline', 'horizontalslider', 'dropdownmenu', 'panel'
        ]

        # This dictionary contains all the gui elements
        self.elements = {}

        # Parse the XML file
        self.parse()

    def parse(self):
        tree = xmlET.parse(self.xml_path)
        root = tree.getroot()

        self.expand(root)

        return self.elements

    def expand(self, parent):
        """
        For every element contained in a tag, create an object and store it in the dictionary
        """
        for child in parent:
            if child.tag in self.allowed_elements:
                self.elements[child.attrib['name']] = self.add_ui_element(child.tag, child.attrib, parent)
            self.expand(child)

    def add_ui_element(self, tag: str, attrib: dict, parent):
        """
        Read the tag name and create the associated gui elements with all the attributes
        """

        # In order to use the screen size in the pos and size attributes, without hardcoding it in the files,
        # use 'WIDTH' and 'HEIGHT' as placeholders.
        # Ex: pos="(WIDTH - 900, 0)"
        # If the WIDTH of the screen is 1920 then the resulting tuple will be (1020, 0)
        pos = eval(attrib['pos'].replace('WIDTH', str(self.screen[0])).replace('HEIGHT', str(self.screen[1])))
        size = eval(attrib['size'].replace('WIDTH', str(self.screen[0])).replace('HEIGHT', str(self.screen[1])))
        rect = pygame.Rect(pos, size)
        container = None
        object_id = None
        visible = 1

        # Set the container of the current element as the parent tag
        if parent.tag in self.allowed_elements:
            container = self.elements[parent.attrib['name']]
        if 'class' in attrib and 'id' in attrib:
            object_id = pygame_gui.core.ObjectID(class_id = '@' + attrib['class'], object_id = '#' + attrib['id'])
        if 'visible' in attrib:
            visible = int(attrib['visible'])

        element = None
        if tag == 'image':
            element = pygame_gui.elements.UIImage(
                relative_rect = rect,
                image_surface = pygame.image.load(os.path.join(os.getcwd(), attrib['src'])),
                manager = self.manager,
                container = container,
                object_id = object_id
            )
        elif tag == 'button':
            element = pygame_gui.elements.UIButton(
                relative_rect = rect,
                text = attrib['text'],
                tool_tip_text = attrib['tool_tip_text'],
                manager = self.manager,
                container = container,
                object_id = object_id
            )   
            if len(attrib['tool_tip_text']) == 0:
                element.tool_tip_text = None
        elif tag == 'textbox':
            element = pygame_gui.elements.UITextBox(
                relative_rect = rect,
                html_text = attrib['html_text'],
                manager = self.manager,
                container = container,
                object_id = object_id
            )
        elif tag == 'textentryline':
            element = pygame_gui.elements.UITextEntryLine(
                relative_rect = rect,
                manager = self.manager,
                object_id = object_id,
                container = container
            )
            if 'white_list' in attrib:
                element.set_allowed_characters(attrib['white_list'])
        elif tag == 'horizontalslider':
            element = pygame_gui.elements.UIHorizontalSlider(
                relative_rect = rect,
                start_value = float(attrib['start_value']),
                value_range = eval(attrib['value_range']),
                manager = self.manager,
                container = container,
                click_increment = float(attrib['click_increment']),
                object_id = object_id
            )
        elif tag == 'dropdownmenu':
            element = pygame_gui.elements.UIDropDownMenu(
                relative_rect = rect,
                options_list = attrib['options_list'].split(';'),
                starting_option = attrib['starting_option'],
                manager = self.manager,
                object_id = object_id,
                container = container,
                visible = visible
            )
        elif tag == 'panel':
            element = pygame_gui.elements.UIPanel(
                relative_rect = rect,
                starting_layer_height = int(attrib['starting_layer_height']),
                manager = self.manager,
                container = container,
                object_id = object_id,
                visible = visible
            )

        return element

It's not pretty but it works. Keep in mind I wrote this code just to get the things work in my project. I plan to rewrite this code and add more functionalities.

cobyj33 commented 1 year ago

It's dope to see that we're both on about the same path. Like, literally all of my tag names are the same as yours. I wonder if there's some sort of way to communicate about this library beyond just the issues section because that would be really helpful to just be able to sort things out.

oscar0urselli commented 1 year ago

We can use Telegram or even better Discord, if you are ok with that, I will send you an email.

MyreMylar commented 1 year ago

Hello!

I do not have anything against markup languages (the library already uses json for theming after all), nor them being being used for layout. What you are discussing here sounds a lot like XAML to me, at least that is the version of this concept I am most familiar with.

I'm not as convinced of a layout markup's usefulness in an interpreted programming language like python right now, as it is pretty quick in most of my applications so far to make a quick change to the layout in the code and then re-run the program without any lengthy compile times. Then again most of my usage has been pretty simple layouts, and if it does get more complicated then I tend to build a UIElement in the library to make it less complicated.

Though perhaps a markup language could be a half-way house to a visual layout editor program? Something which would undoubtably be useful.


I will also say that I am always trying to reduce the amount of boilerplate. I recently reduced a simple Button creation to this:

hello_button = UIButton((350, 280), 'Hello')

As a test (this works right now in 0.6.6), and am looking to extend these particular changes across the whole project where possible so that you don't always have to pass around UIManagers and pygame.Rects.

So, I would say to your somewhat infectious markup enthusiasm - make sure you are definitely solving for the right problem. If markup is mostly just to eliminate boilerplate stuff when creating elements - lets try and cut straight to eliminating that first. If it more for dynamic refreshing, or a stepping stone towards a visual GUI layout editor then I'm definitely more into it.

cobyj33 commented 1 year ago

It's funny because a builder was actually what I secretly had in mind, but that's definitely its own project in itself since its such a huge undertaking. I figured that it would need its own XML and I wanted it to be written in pygame_gui itself so that everything is guaranteed to be the same from program to project (it's also one of the reasons I proposed more layouts in #382 to be honest)

Here's like a really alpha prototype view that I just threw together in Inkscape

pygameguibuilder

It's just a dream though for now, just a pitch

cobyj33 commented 1 year ago

We can use Telegram or even better Discord, if you are ok with that, I will send you an email.

Yeah discord should be just fine if you're down, cobyj33#0489

dylanxyz commented 5 months ago

I also started working with something similar, using XML-like markup for design the ui with pygame_gui, and i came up with this:

@component('''
<Window title="Some Window" top="10" left="10" width="{width}" height="400">
    <Button name="button">Click-me!</Button>
</Window>
''')
class DummyUI:
    button: pygame_gui.elements.UIButton

    def __init__(self, manager) -> None:
        self.__build__(manager, width=340)

Essentially, a decorator @component is used to inject a __build__ method that builds the UI based on the template markup. Any elements in the markup with the name attribute gets assigned as an attribute in the target class.

Then, this class can be used like:

manager = pygame_gui.UIManager((800, 600))
my_ui = DummyUI(manager)
my_ui.button # points to the <Button name="button"> element

For bigger templates, embedding them inside python code could be uglier, so the @component decorator could accept a file path for the xml file.

xXLarryTFVWXx commented 5 months ago

I want to get in on this as well!

I wrote a proof of concept HTML file and interpreter using exclusively built-in functions. It's in a repo on my account.