MyreMylar / pygame_gui

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

Chaining anchor movements - doesn't work right #403

Closed WebTiger89 closed 7 months ago

WebTiger89 commented 1 year ago

Hi, is there any logic that when an element gets hidden, other elements around will take its room and when the hidden element is showed again, it claims its room back. I'm looking for such behavior like in HTML.

For example, having a login mask, you have a username field and below a password field. But for validation you will also need an error label below the username or between username and password field respectively. The label element is hidden by default and gets visible when validation fails. Is there built-in logic to move the password field down when the label element gets visible and move up when it gets invisible again by using anchor targets?

MyreMylar commented 1 year ago

Hello,

There isn't with just hiding as that just makes the elements not be drawn and not interactable. You could probably make something like this with setting the relative position and hiding the error label. Though this isn't something I've really considered until just now.

Just had a quick tinker:

import pygame

from pygame import Rect
from pygame_gui import UIManager, UI_BUTTON_PRESSED
from pygame_gui.elements import UIButton, UITextEntryLine, UIWindow, UILabel

pygame.init()

pygame.display.set_caption('Login window test')
window_surface = pygame.display.set_mode((800, 600))
manager = UIManager((800, 600), 'data/themes/quick_theme.json')

background = pygame.Surface((800, 600))
background.fill(manager.ui_theme.get_colour('dark_bg'))

window = UIWindow(rect=pygame.Rect((50, 50), (400, 300)),
                  manager=manager, resizable=False,
                  window_display_title='Login')
window.set_minimum_dimensions((400, 300))

confirm_button = UIButton(relative_rect=Rect(10, -40, 100, 30),
                          text='Confirm',
                          container=window,
                          anchors={'top': 'bottom',
                                   'left': 'left',
                                   'bottom': 'bottom',
                                   'right': 'left'})

cancel_button = UIButton(relative_rect=Rect((10, -40), (100, 30)),
                         text='Cancel',
                         container=window,
                         anchors={'top': 'bottom',
                                  'left': 'left',
                                  'bottom': 'bottom',
                                  'right': 'left',
                                  'left_target': confirm_button})

password_entry = UITextEntryLine(Rect((90, -100), (210, 30)),
                                 container=window,
                                 anchors={'top': 'bottom',
                                          'left': 'left',
                                          'bottom': 'bottom',
                                          'right': 'left',
                                          'bottom_target': confirm_button}
                                 )
password_entry.set_text_hidden(True)
username_error_label = UILabel(Rect((90, 0), (210, 30)),
                               text="",
                               container=window,
                               anchors={'top': 'bottom',
                                        'left': 'left',
                                        'bottom': 'bottom',
                                        'right': 'left',
                                        'bottom_target': password_entry}
                               )
username_error_label.hide()
username_entry = UITextEntryLine(Rect((90, -40), (210, 30)),
                                 container=window,
                                 anchors={'top': 'bottom',
                                          'left': 'left',
                                          'bottom': 'bottom',
                                          'right': 'left',
                                          'bottom_target': username_error_label}
                                 )
username_entry_label = UILabel(Rect((-72, -40), (72, 30)),
                               text="Username:",
                               container=window,
                               anchors={'top': 'bottom',
                                        'left': 'right',
                                        'bottom': 'bottom',
                                        'right': 'right',
                                        'right_target': username_entry,
                                        'bottom_target': username_error_label}
                               )
password_entry_label = UILabel(Rect((-72, -100), (72, 30)),
                               text="Password:",
                               container=window,
                               anchors={'top': 'bottom',
                                        'left': 'right',
                                        'bottom': 'bottom',
                                        'right': 'right',
                                        'right_target': password_entry,
                                        'bottom_target': confirm_button}
                               )

clock = pygame.time.Clock()
is_running = True

while is_running:
    time_delta = clock.tick(60)/1000.0
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            is_running = False
        if event.type == UI_BUTTON_PRESSED:
            if event.ui_element == confirm_button:
                print('Confirmed!')
        if event.type == pygame.KEYDOWN and event.key == pygame.K_F1:
            username_error_label.set_relative_position((60, -40))
            username_error_label.set_text("Error with username")
            username_error_label.show()
        if event.type == pygame.KEYDOWN and event.key == pygame.K_F2:
            username_error_label.set_relative_position((60, 0))
            username_error_label.set_text("")
            username_error_label.hide()
        manager.process_events(event)

    manager.update(time_delta)

    window_surface.blit(background, (0, 0))
    manager.draw_ui(window_surface)

    pygame.display.update()

Looks like this: image

Then when you hit F1: image

WebTiger89 commented 1 year ago

Works for me, thanks for your invest. I still have one problem, might you have a look please:

import pygame
import pygame_gui
import csv
import os.path

# noinspection SpellCheckingInspection
class StartMenu:
    scores_csv = 'scores.csv'
    scores = []

    is_running = False

    width = 800
    height = 600

    def create_window(self):
        pygame.display.set_caption('Startmenü')
        window_surface = pygame.display.set_mode((self.width, self.height))
        manager = pygame_gui.UIManager((self.width, self.height))

        background = pygame.Surface((self.width, self.height))
        background.fill(pygame.Color("white"))

        x_mid = self.width / 2
        y_mid = self.height / 2

        entry_box_width = 200

        x_pos_elements = x_mid - (entry_box_width / 2)

        # Erstellt ein single line TextInput Feld
        # Das Element ist zentriert ausgerichtet
        # Alle weiteren Elemente werden relativ zu ihren Vorgängern ausgerichtet, siehe anchors
        username_input = pygame_gui.elements.UITextEntryLine(
            relative_rect=pygame.Rect((0, -80), (entry_box_width, 50)), manager=manager, anchors={'center': 'center'})
        # Placeholder text setzen
        username_input.placeholder_text = "Benutzername"
        # Hintergrundfarbe setzen
        username_input.background_colour = pygame.Color("white")
        # Cursorfarbe so wie Textfarbe setzen
        username_input.text_cursor_colour = username_input.text_colour
        # Element neu rendern
        username_input.rebuild()

        # Focus state speichern, um einen Wechsel zu erkennen
        previous_focus = username_input.is_focused

        error_label_width = 250
        error_label_height = 35

        # username_error_label_height = 50
        # Verstecktes Fehler label element erstellen
        username_error_label = pygame_gui.elements.UILabel(text="",
                                                           relative_rect=pygame.Rect(
                                                               (-entry_box_width, -error_label_height),
                                                               (error_label_width, error_label_height)),
                                                           manager=manager, anchors={'top_target': username_input,
                                                                                     'left_target': username_input})
        # Horizontale Textausrichtung setzen
        username_error_label.text_horiz_alignment = 'left'
        username_error_label.text_colour = pygame.Color("red")
        username_error_label.hide()
        username_error_label.rebuild()

        # Passwort TextInput Feld erstellen
        password_input = pygame_gui.elements.UITextEntryLine(
            relative_rect=pygame.Rect((-error_label_width, 0), (entry_box_width, 50)), manager=manager,
            anchors={'top_target': username_error_label,
                     'left_target': username_error_label})

        password_input.placeholder_text = "Passwort"
        password_input.background_colour = pygame.Color("white")
        password_input.text_cursor_colour = password_input.text_colour
        # Eingabe wird unkenntlich gemacht
        password_input.set_text_hidden(True)
        password_input.rebuild()

        # Verstecktes Fehler label element erstellen
        password_error_label = pygame_gui.elements.UILabel(text="",
                                                           relative_rect=pygame.Rect(
                                                               (-entry_box_width, -error_label_height),
                                                               (error_label_width, error_label_height)),
                                                           manager=manager, anchors={'top_target': password_input,
                                                                                     'left_target': password_input})
        # Horizontale Textausrichtung setzen
        password_error_label.text_horiz_alignment = 'left'
        password_error_label.text_colour = pygame.Color("red")
        password_error_label.hide()
        password_error_label.rebuild()

        # password_input_confirmation = pygame_gui.elements.UITextEntryBox(
        #     relative_rect=pygame.Rect((x_pos_elements + entry_box_width + 20, 150), (entry_box_width, 50)), manager=manager)

        login_button = pygame_gui.elements.UIButton(
            relative_rect=pygame.Rect((-error_label_width, 0), (entry_box_width, 50)), text='Login',
            manager=manager, anchors={'top_target': password_error_label, 'left_target': password_error_label})

        self.load_scores()

        is_running = True
        clock = pygame.time.Clock()

        while is_running:
            time_delta = clock.tick(60) / 1000.0
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    is_running = False
                if event.type == pygame.KEYDOWN and event.key == pygame.K_F1:
                    password_error_label.update_containing_rect_position()
                    password_error_label.rebuild()
                    password_input.update_containing_rect_position()
                    password_input.rebuild()

                if event.type == pygame_gui.UI_BUTTON_PRESSED:
                    if event.ui_element == login_button:
                        print('Hello World!')
                        if len(username_input.text) == 0:
                            username_error_label.set_relative_position((-entry_box_width, 0))
                            username_error_label.set_text("Bitte füllen Sie das Feld.")
                            username_error_label.show()
                        if len(password_input.text) < 1:
                            password_error_label.set_text("Bitte füllen Sie das Feld.")
                            if len(password_input.text) in range(1, 5):
                                password_error_label.set_text("Mindestens 5 Zeichen!")
                            password_error_label.set_relative_position((-entry_box_width, 0))
                            password_error_label.show()

                if event.type == pygame_gui.UI_TEXT_ENTRY_CHANGED:
                    if event.ui_element == username_input:
                        if len(event.text) > 1:
                            print("Changed text:", event.text)
                            username_error_label.set_relative_position((-entry_box_width, -error_label_height))
                            username_error_label.hide()
                    if event.ui_element == password_input:
                        if len(password_input.text) in range(1, 5):
                            password_error_label.set_text("Mindestens 5 Zeichen!")
                            password_error_label.show()
                        else:
                            password_error_label.set_relative_position((-entry_box_width, -error_label_height))
                            password_error_label.hide()

                if username_input.is_focused:
                    previous_focus = True

                if not username_input.is_focused and previous_focus:
                    previous_focus = False
                    print("lost focus")
                    if not self.check_user(username_input.text):
                        print("user not registered")

                manager.process_events(event)

            manager.update(time_delta)

            window_surface.blit(background, (0, 0))
            manager.draw_ui(window_surface)

            pygame.display.update()

    # def validate_input(self, username, password):
    #     if len(username) == 0:

    def load_scores(self):
        # Prüfen, ob Datei existiert
        if os.path.isfile(self.scores_csv):
            # Öffnet Datei zum Lesen
            # Datei wird automatisch geschlossen, siehe 'with'
            with open(self.scores_csv, newline='') as csv_file:
                # CSV Parser/Reader Instanz
                csv_reader = csv.DictReader(csv_file)
                # Über Zeilen iterieren
                for row in csv_reader:
                    # Existierende scores in Liste eintragen
                    self.scores.append(row)
        # Wenn Datei nicht existiert, wird diese mit initialem Text erstellt
        else:
            # Öffnet Datei zum Schreiben
            with open(self.scores_csv, 'w', newline='') as csvfile:
                writer = csv.writer(csvfile, delimiter=',')
                writer.writerow(["Username"] + ["Password"] + ["Punkte1"] + ["Punkte2"])

    def create_user(self, username, password):
        with open(self.scores_csv, 'w', newline='') as csvfile:
            writer = csv.writer(csvfile, delimiter=',')

    # def create_scores(self):

    def check_user(self, username):
        for profile in self.scores:
            print(profile["Username"])
            if profile["Username"] == username:
                return True
        return False

if __name__ == "__main__":
    pygame.init()
    menu = StartMenu()
    menu.create_window()

When username_error_label and password_error_label are shown at the same time (press button while text fields are empty) and I fill the username, username_error_label hides, password_input moves up but unfortunately the position of password_error_label does not update. I would expect it moves up too so it is directly below password_input (y is 0 at this time). I tried it with update_containing_rect_position() but with no luck. Any idea how I can overcome this?

MyreMylar commented 1 year ago

Hmm, I guess it is not chaining the anchor movements correctly here. Probably a bug, but you can fix for now by adding:

password_error_label._update_absolute_rect_position_from_anchors()

In this block at the end:

if len(event.text) > 1:
    print("Changed text:", event.text)
    username_error_label.set_relative_position((-entry_box_width, -error_label_height))
    username_error_label.hide()
    # Add line here
GimLala commented 7 months ago

@MyreMylar @WebTiger89 Check if this works now with the latest main branch as, if this issue is labelled correctly, it should have been fixed when I had found and fixed this issue.

MyreMylar commented 7 months ago

I will have a look into this.

MyreMylar commented 7 months ago

Tested this again and it all seems to work correctly now 👍