python – Full screen to windowed screen workaround causing segmentation fault in pygame

I’m having an issue that causes the window bar to appear above the screen when jumping between full-screen and windowed mode. Did some googling on the issue and it turns out it’s actually a pygame bug, here

So I implemented the fix, and ended up hitting up against another issue where I get a segmentation fault from pygame.

When I delete this line pygame.display.quit() the segmentation fault goes away, but so does the workaround. So it’s not really a fix.

Error 1

Error depends on whether I use pygame.quit() or the display quit above.

Fatal Python error: pygame_parachute: (pygame parachute) Segmentation Fault
Python runtime state: initialized

I ended up with even a different error when I tried to create a little example program for this question

Error 2

self.txt_surface = self.FONT.render(” “, True, self.color)
pygame.error:

Text has zero width

But let’s focus on the first one. I enabled faulthandler on python which has helped me narrow down what line is causing the issue, and I narrowed it down to these few lines. After following back the error I found a problem with my textBox class, further found these offending lines.

def draw(self, screen):
    # Blit the text
    self.__hitkey__()
    roundedEdgeNum = 5
    pygame.draw.rect(screen, self.backgroundColor, self.rectBackground, 0,roundedEdgeNum)
    screen.blit(self.txt_surface, (self.rect.x+2, self.rect.y+2))

    pygame.draw.rect(screen, self.color, self.rect, 1,roundedEdgeNum)

What am I doing wrong with these draw and blit functions that causes an issue? I’m a little stumped on this one. Here is a full code example you can run to reproduce the issue. Thanks for any help! I really appreciate this community.

p.s. I know I’m mixing tkinter with a homegrown textbox, and I could just use a well tested GUI, but I spent a few days getting this text box to work the way I wanted it too, and so I want to use the code.

Code Reproducing Issue

import pygame
import tkinter
import os
import faulthandler
faulthandler.enable()

COLOR_INACTIVE = pygame.Color((128,128,128))
COLOR_ACTIVE = pygame.Color((0,0,0))
BACKGROUND_ACTIVE = (255,255,255)
BACKGROUND_INACTIVE = ((230,230,230))

class InputBox:

    def __init__(self, screen,x, y, w, h, text=''):
        self.screen = screen
        self.x = x
        self.y = y
        self.w = w
        self.h = h
        self.LIGHT_GREY = (180,180,180)
        self.textBackground = (255,255,255)
        self.outershadowWidth = 2
        self.outershadowHeight = 2
        self.shadowWidth = 2
        self.shadowHeight = 2
        self.xoffset = 1
        self.yoffset = 1
        self.shadowRect = pygame.Rect(x+self.xoffset ,y+self.yoffset ,w-self.shadowWidth,h-self.shadowHeight)
        self.shadowRectOuter = pygame.Rect(x-self.xoffset ,y-self.yoffset ,w+self.shadowWidth,h+self.shadowHeight)
        self.rect = pygame.Rect(x, y, w, h)
        self.rectBackground = pygame.Rect(x+1,y+1,w-1,h-1)
        self.color = COLOR_INACTIVE
        self.backgroundColor = BACKGROUND_INACTIVE
        self.text = text
        self.textCopy = ''
        self.FONT = pygame.font.SysFont("cambriacambriamath", 16)
        self.txt_surface = self.FONT.render(text, True, self.color)
        self.active = False
        self.font_size = 18
        self.cursor_color=(0, 0, 1)
        self.cursor_surface = pygame.Surface((int(self.font_size / 40 + 1), self.font_size-2))
        self.cursor_surface.fill(self.cursor_color)
        self.textBackgroundSurface = pygame.Surface((self.w, self.h))
        self.cursor_position    = len("")  # Inside text
        self.cursor_visible     = True  # Switches every self.cursor_switch_ms ms
        self.cursor_switch_ms   = 500
        self.cursor_ms_counter  = 0
        self.max_string_length  = 100
        self.keydown            = False
        self.backspaceDelay     = 100
        self.timer              = 0
        self.keyTimer           = 0
        self.backspaceTimer     = 0
        self.clock = pygame.time.Clock()
        self.wipeTextOnReturn = False
        self.countWipe        = 0


        self.screen_resolution = self.getScreenResolution()
        self.nonFullScreenSize=(int(self.screen_resolution(0)/2.0),int(self.screen_resolution(1)/2.0))
        self.fullscreen = True
        self.setScreenResolution()

    def handle_event(self,events):
        for event in events:
            if event.type == pygame.MOUSEBUTTONDOWN:
                # If the user clicked on the input_box rect.
                if self.rect.collidepoint(event.pos):
                    # Toggle the active variable.
                    self.active = not self.active
                else:
                    self.active = False
                # Change the current color of the input box.
                self.color = COLOR_ACTIVE if self.active else COLOR_INACTIVE
                self.backgroundColor = BACKGROUND_ACTIVE if self.active else BACKGROUND_INACTIVE
            if event.type == pygame.KEYDOWN:
                if self.active:
                    if event.key == pygame.K_RETURN:
                        self.textCopy = self.text
                        self.text = ''
                    else:
                        if ((len(self.text) < self.max_string_length) or (self.max_string_length == -1)):
                            if (event.unicode.isnumeric() or event.unicode == 'x'):
                                # If no special key is pressed, add unicode of key to text
                                self.text = self.text(:self.cursor_position) + event.unicode + self.text(self.cursor_position:)
                                self.cursor_position = len(self.text)  # Some are empty, e.g. K_UP
                    if (event.key == pygame.K_BACKSPACE):
                        self.text = self.text(:-1)
                        # Subtract one from cursor_pos, but do not go below zero:
                        self.cursor_position = max(self.cursor_position - 1, 0)
                        self.keydown         = True
                    if (event.key == pygame.K_LEFT):
                        if (self.cursor_position > 0):
                            self.cursor_position = self.cursor_position - 1
                    if (event.key == pygame.K_RIGHT):
                        # Add one to cursor_pos, but do not exceed len(text)
                        if (self.cursor_position < len(self.text)):
                            self.cursor_position = self.cursor_position + 1

            if event.type == pygame.KEYUP:
                self.keydown          = False
                self.backspaceTimer   = 0
                self.timer            = 0
                self.keyTimer         = 0

        #Handles backspace helddown, alternative to putting extra events on queue or using set_repeat
        #Putting set_repeat(1,30) in a program using this will mess up the event queue
        if (self.keydown == True):
            self.backspaceTimer += self.clock.get_time()
            self.timer += 1
            if (self.backspaceTimer >= self.backspaceDelay and (self.timer == 10)):
                self.text = self.text(:-1)
                #Subtract one from cursor_pos, but do not go below zero:
                self.cursor_position = max(self.cursor_position - 1, 0)
                self.timer = 0

        #Get key states
        key_states = pygame.key.get_pressed()
            
        if (key_states(pygame.K_END)):
            self.cursor_position = len(self.text)

        if (key_states(pygame.K_HOME)):
            self.cursor_position = 0

        # Re-render the text.
        if (self.active and self.text == ""):
            self.txt_surface = self.FONT.render(" ", True, self.color)
        else: 
            self.txt_surface = self.FONT.render(self.text, True, self.color)

        # Update self.cursor_visible
        self.cursor_ms_counter += self.clock.get_time()
        if (self.cursor_ms_counter >= self.cursor_switch_ms):
            self.cursor_ms_counter %= self.cursor_switch_ms
            self.cursor_visible = not self.cursor_visible

        if self.cursor_visible:
            cursor_y_pos = self.FONT.size(self.text(:self.cursor_position))(0)
            # Without this, the cursor is invisible when self.cursor_position > 0:
            if (self.cursor_position > 0):
                if (cursor_y_pos > 0):
                    cursor_y_pos -= self.cursor_surface.get_width()
            self.txt_surface.blit(self.cursor_surface, (cursor_y_pos, 2))
        
        self.update()

        self.draw(self.screen)

        self.clock.tick()

    def update(self):
        # Resize the box if the text is too long.
        width = max(200, self.txt_surface.get_width()+10)
        self.rect.w            = width
        self.shadowRect.w      = width-self.shadowWidth
        self.shadowRectOuter.w = width+self.shadowWidth
        self.rectBackground.w  = width-self.shadowWidth

    def draw(self, screen):
        # Blit the text
        self.__hitkey__()
        roundedEdgeNum = 5
        pygame.draw.rect(screen, self.backgroundColor, self.rectBackground, 0,roundedEdgeNum)
        screen.blit(self.txt_surface, (self.rect.x+2, self.rect.y+2))

        pygame.draw.rect(screen, self.color, self.rect, 1,roundedEdgeNum)



    def setScreenResolution(self):

        self.fullscreen = not self.fullscreen

        if (self.fullscreen):
            self.screen = pygame.display.set_mode(self.screen_resolution, pygame.FULLSCREEN,0)
        else:
            pygame.quit() # ???
            pygame.init() # ???
            os.environ('SDL_VIDEO_WINDOW_POS') = str(100)+ "," + str(100)
            #os.environ('SDL_VIDEO_CENTERED') = '1'
            
            self.screen = pygame.display.set_mode(self.nonFullScreenSize,pygame.RESIZABLE,0)

        pygame.display.update()

    def __hitkey__(self):
        '''Handle key events '''
        key_states = pygame.key.get_pressed()
        if (key_states(pygame.K_F3)):
            if (self.keyStateCounter == 0):
                self.setScreenResolution()
            self.keyStateCounter += 1
        if (not key_states(pygame.K_F3)):
            self.keyStateCounter = 0

    def getScreenResolution(self):
        """
        Workaround to get the size of the current screen in a multi-screen setup.

        Returns:
            geometry (str): The standard tkinter geometry string.
                (width)x(height)+(left)+(top)
        """
        root = tkinter.Tk()
        root.update_idletasks()
        root.attributes('-fullscreen', True)
        root.state('iconic')
        screen_width = root.winfo_screenwidth()
        screen_height = root.winfo_screenheight()
        root.destroy()

        return (screen_width,screen_height)


def main():
    screen = pygame.display.set_mode((640, 480))
    pygame.init()
    input_box1 = InputBox(screen,100, 100, 140, 32)

    done = False

    while not done:
        events = pygame.event.get()
        for event in events:
            if event.type == pygame.QUIT:
                done = True

        stuff = input_box1.handle_event(events)

        pygame.display.flip()



if __name__ == '__main__':
    main()
    pygame.quit()