Skip to content

Add window example #2626

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/reST/ref/window.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
window class will continue to be developed, and we're excited to share
the new functionality this class offers.

Example code showing the Window API in action:
https://github.com/pygame-community/pygame-ce/blob/main/examples/window.py

:param str title: The title of the window.
:param (int, int) size: The size of the window, in screen coordinates.
:param (int, int) or int position: A tuple specifying the window position, or
Expand Down
5 changes: 5 additions & 0 deletions examples/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ video.py
It explores some new video APIs in pygame 2.
Including multiple windows, Textures, and such.

window.py
Shows how to use the Window() API, where Windows are managed
as objects instead of globally through pygame.display.set_mode.
Allows for more than one window at once!

data/
Directory with the resources for the examples.

Expand Down
146 changes: 146 additions & 0 deletions examples/window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env python
""" pygame.examples.window

Demonstrates the new Window API, which can be used in place of
pygame.display.set_mode, providing more control and an object oriented
interface.
"""
import pygame

WIN_MOVE_SPEED = 25
WIN_GROW_SPEED = 15
COLOR_PROGRESSION = ["cadetblue2", "darkorange2", "lightslateblue", "seagreen"]
UNHOVERED_OPACITY = 0.8
SHOW_WINDOW = pygame.event.custom_type()

pygame.init()
pygame.key.set_repeat(500, 100)

main_window = pygame.Window("demo window", (500, 500), resizable=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could the main window also be always_on_top?

Another trivial note, how about setting an icon with set_icon?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least on Windows, the spawned windows spawn in the center of the main window (I'm not controlling the spawn point). So if the main window was always_on_top it would seem like the spawn window command does nothing, because the other windows would not be visible unless you move the main window with the mouse.

I was looking around the examples data folder for art to use for this, (icons, backgrounds), and none of it spoke to me. I don't feel the need to set an icon in this example.

main_surface = main_window.get_surface()
windows_and_surfaces = [(main_window, main_surface)]

instructions = """Welcome to the window demo!
Controls:
m.) maximize main window
n.) minimize main window
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to maximise/minimise, could also two keys for add/remove full screen?

r.) restore main window
(from being minimized or maximized)
h.) hide main window for 1 second
s.) spawn a new window
arrow keys.) move window(s) around screen
escape.) destroy the most recently created window
1.) shrink all windows
2.) grow all windows
"""

font = pygame.font.SysFont("Arial", 24)
rendered_instructions = font.render(instructions, True, "black")

# Make sure instructions can be shown
main_window.minimum_size = rendered_instructions.get_size()

clock = pygame.Clock()
running = True

while running:
for event in pygame.event.get():
# If there are multiple windows, QUIT fires when the last one is destroyed
if event.type == pygame.QUIT:
running = False

if event.type == pygame.WINDOWCLOSE:
index = [win_surface[0] for win_surface in windows_and_surfaces].index(
event.window
)
del windows_and_surfaces[index]
event.window.destroy()

if event.type == pygame.WINDOWENTER:
if event.window != main_window:
try:
event.window.opacity = 1.0
except pygame.error:
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding a event.window.focus() here?


if event.type == pygame.WINDOWLEAVE:
# Test not None because WINDOWLEAVE will trigger when a window
# closes, can't set opacity of now-nonexistent Window.
if event.window != main_window and event.window is not None:
try:
event.window.opacity = UNHOVERED_OPACITY
except pygame.error:
pass

if event.type == SHOW_WINDOW:
main_window.show()

if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
win, _ = windows_and_surfaces.pop()
win.destroy()

# Destroying the windows will not automatically do a QUIT on
# last window, unlike closing the windows manually.
if len(windows_and_surfaces) == 0:
pygame.event.post(pygame.Event(pygame.QUIT))

if event.key == pygame.K_m:
main_window.maximize()

if event.key == pygame.K_n:
main_window.minimize()

if event.key == pygame.K_r:
main_window.restore()

if event.key == pygame.K_h:
main_window.hide()
pygame.time.set_timer(SHOW_WINDOW, 1000, 1)

if event.key == pygame.K_s:
win = pygame.Window("spawned window", (300, 300))
try:
win.opacity = UNHOVERED_OPACITY
except pygame.error:
pass
windows_and_surfaces.append((win, win.get_surface()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API comment

This has gotten me thinking about the API. As a user, I think it would be much neater if we could do something like win.surface and not have to explicitly track the output of win.get_surface. Even helps conceptually to show that "this surface instance is a special window-surface tied to the window"

Copy link
Member Author

@Starbuck5 Starbuck5 Dec 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I think I could be calling get_surface() every time I need the surface. I'm just not sure how that works SDL-side.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also why I thought of subclassing window, issue raised as #2625. In this demo it would be nice to store the color of the window with the window. (Store arbitrary things in instance of class, subclass)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, SDL stores the surface in the window struct (both in SDL2 and SDL3), and if the surface needs updating it updates the surface, so for most of the calls to this function it is just a cheap struct member access.

On the python side, we could basically the change this get_surface to a property.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a commit on a branch I made on top of yours, implementing my idea: 6b620f1

I also updated this example to see/test the property in action


if event.key == pygame.K_UP:
for win, _ in windows_and_surfaces:
win.position += pygame.Vector2(0, -WIN_MOVE_SPEED)

if event.key == pygame.K_DOWN:
for win, _ in windows_and_surfaces:
win.position += pygame.Vector2(0, WIN_MOVE_SPEED)

if event.key == pygame.K_LEFT:
for win, _ in windows_and_surfaces:
win.position += pygame.Vector2(-WIN_MOVE_SPEED, 0)

if event.key == pygame.K_RIGHT:
for win, _ in windows_and_surfaces:
win.position += pygame.Vector2(WIN_MOVE_SPEED, 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API comment

SMH my head, window.position is also broken under direct wayland

Nothing moves when I press the arrow keys. 😢

All these "direct wayland" issues are fine/ignorable for now because these issues don't happen under XWayland which SDL2 picks by default, but this could be a problem in the SDL3 era if SDL3 devs decide to do wayland-default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is by design. Wayland does not want applications to know this for API design and security reasons. There is nothing SDL3 can do about it short of sending patches to KDE and GNOME and sway

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I need to add try/except pygame.error blocks around them then? I did it for opacity given your previous report.

I'm also unsure if we really want to raise an exception if the operation is not supported. We could return True/False about success?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case the position is silently ignored by SDL, like always_on_top. Only opacity explicitly errors.

I think for this example, we should not have this position-moving thing as a highlight, and make it more low-key somehow?


if event.key == pygame.K_1:
for win, _ in windows_and_surfaces:
win.size -= pygame.Vector2(WIN_GROW_SPEED, WIN_GROW_SPEED)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API comment

Continuously pressing 1 leads to the error
ValueError: width or height should not be less than or equal to zero

Interestingly, this issue does not happen for the "main window" which has minimum_size set to some larger than (0, 0). In that case, the size is implicitly capped with no python errors raised.

This could be an API inconsistency? I'd like an error to be not raised here and it should just cap at (0, 0)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear reading your comment whether you see the minimum_size of the window is explicitly set to contain the instruction text.

It would happen to the main window if it was ever told to do a negative size there. Which it can't because the minimum size dimensions are higher than WIN_GROW_SPEED.

I agree it shouldn't be possible to crash the example by messing around with it, but I'm not sure what the best solution would be.


if event.key == pygame.K_2:
for win, _ in windows_and_surfaces:
win.size += pygame.Vector2(WIN_GROW_SPEED, WIN_GROW_SPEED)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might also be a good idea to cap it to some upper bound? Perhaps using the maximum_size property?


for i, win_surface in enumerate(windows_and_surfaces):
win, surface = win_surface

surface.fill(COLOR_PROGRESSION[win.id % len(COLOR_PROGRESSION)])
if surface == main_surface:
centered_rect = rendered_instructions.get_rect(
center=pygame.Vector2(win.size) / 2
)
surface.blit(rendered_instructions, centered_rect)
win.flip()

clock.tick(144)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be a small and nice addition to set FPS in the title for the main window, also serves as a demonstration of the title property.

Also how about something like window.id in the titles for all windows?


pygame.quit()