├── mkdocs.yml ├── pygame_widgets ├── animations │ ├── __init__.py │ └── animation.py ├── __init__.py ├── exceptions.py ├── util.py ├── toggle.py ├── progressbar.py ├── mouse.py ├── popup.py ├── slider.py ├── widget.py ├── combobox.py └── selection.py ├── site ├── img │ ├── grid.png │ └── favicon.ico ├── sitemap.xml.gz ├── images │ ├── combobox.gif │ ├── dropdown.gif │ ├── slider.png │ └── textbox.png ├── fonts │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── sitemap.xml ├── search │ ├── main.js │ └── worker.js ├── 404.html ├── css │ └── base.css ├── js │ └── base.js ├── common │ └── index.html ├── animations │ └── index.html ├── toggle │ └── index.html ├── progressbar │ └── index.html ├── index.html ├── slider │ └── index.html └── textbox │ └── index.html ├── docs ├── images │ ├── combobox.gif │ ├── dropdown.gif │ ├── slider.png │ └── textbox.png ├── widgets │ ├── common.md │ ├── toggle.md │ ├── progressbar.md │ ├── slider.md │ ├── textbox.md │ ├── buttonarray.md │ ├── dropdown.md │ ├── combobox.md │ └── button.md ├── animations │ └── animations.md ├── CONTRIBUTING.md └── index.md ├── .gitattributes ├── .readthedocs.yml ├── .github └── ISSUE_TEMPLATE │ └── bug-report.md ├── LICENSE ├── examples ├── button_array_example.py └── button_example.py ├── README.md └── CODE_OF_CONDUCT.md /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Pygame Widgets 2 | theme: 3 | name: readthedocs 4 | docs_dir: docs -------------------------------------------------------------------------------- /pygame_widgets/animations/__init__.py: -------------------------------------------------------------------------------- 1 | from animation import Resize, Recolour, Translate 2 | -------------------------------------------------------------------------------- /site/img/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/site/img/grid.png -------------------------------------------------------------------------------- /site/sitemap.xml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/site/sitemap.xml.gz -------------------------------------------------------------------------------- /site/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/site/img/favicon.ico -------------------------------------------------------------------------------- /docs/images/combobox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/docs/images/combobox.gif -------------------------------------------------------------------------------- /docs/images/dropdown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/docs/images/dropdown.gif -------------------------------------------------------------------------------- /docs/images/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/docs/images/slider.png -------------------------------------------------------------------------------- /docs/images/textbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/docs/images/textbox.png -------------------------------------------------------------------------------- /site/images/combobox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/site/images/combobox.gif -------------------------------------------------------------------------------- /site/images/dropdown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/site/images/dropdown.gif -------------------------------------------------------------------------------- /site/images/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/site/images/slider.png -------------------------------------------------------------------------------- /site/images/textbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/site/images/textbox.png -------------------------------------------------------------------------------- /site/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/site/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /site/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/site/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /site/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/site/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /site/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustL/PygameWidgets/HEAD/site/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-detectable=false 2 | *.java linguist-detectable=false 3 | *.py linguist-detectable=true 4 | *.js linguist-detectable=false 5 | *.html linguist-detectable=false 6 | *.xml linguist-detectable=false -------------------------------------------------------------------------------- /pygame_widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from pygame_widgets.mouse import Mouse 2 | from pygame_widgets.widget import WidgetHandler 3 | 4 | from pygame.event import Event 5 | 6 | 7 | def update(events: [Event]): 8 | Mouse.updateMouseState() 9 | WidgetHandler.main(events) 10 | -------------------------------------------------------------------------------- /pygame_widgets/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidParameter(Exception): 2 | """ Exception called if an invalid parameter is passed into an animation""" 3 | 4 | 5 | class InvalidParameterType(Exception): 6 | """ Exception called if an invalid parameter type is passed into an animation""" 7 | -------------------------------------------------------------------------------- /docs/widgets/common.md: -------------------------------------------------------------------------------- 1 | # Common 2 | 3 | Functionality provided and required for all widgets. 4 | 5 | ## Mandatory Parameters 6 | 7 | _Note: Mandatory parameters must be supplied in order._ 8 | 9 | | Parameter | Description | Type | 10 | | :---: | --- | :---: | 11 | | win | Surface to be displayed on. | pygame.Surface | 12 | | x | X-coordinate of top left. | int | 13 | | y | Y-coordinate of top left. | int | 14 | | width | Width of button in pixels. | int | 15 | | height | Height of button in pixels. | int | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation with MkDocs 9 | #mkdocs: 10 | # configuration: mkdocs.yml 11 | mkdocs: 12 | configuration: mkdocs.yml 13 | fail_on_warning: true 14 | build: 15 | os: ubuntu-lts-latest 16 | tools: 17 | python: "3.10" 18 | # Optionally build your docs in additional formats such as PDF and ePub 19 | formats: all 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behaviour** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Version Numbers** 27 | - Pygame Widgets 28 | - Pygame 29 | - Python 30 | -------------------------------------------------------------------------------- /pygame_widgets/util.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | def drawText(win, text, colour, rect, font, align='centre'): 5 | rect = pygame.Rect(rect) 6 | y = rect.top 7 | lineSpacing = -2 8 | 9 | fontHeight = font.size('Tg')[1] 10 | 11 | while text: 12 | i = 1 13 | 14 | if y + fontHeight > rect.bottom: 15 | break 16 | 17 | while font.size(text[:i])[0] < rect.width and i < len(text): 18 | i += 1 19 | 20 | if i < len(text): 21 | i = text.rfind(' ', 0, i) + 1 22 | 23 | image: pygame.Surface = font.render(text[:i], 1, colour) 24 | 25 | imageRect: pygame.Rect = image.get_rect() 26 | 27 | imageRect.center = rect.center 28 | 29 | if align == 'left': 30 | imageRect.left = rect.left 31 | elif align == 'right': 32 | imageRect.right = rect.right 33 | 34 | win.blit(image, (imageRect.left, y)) 35 | y += fontHeight + lineSpacing 36 | 37 | text = text[i:] 38 | 39 | return text 40 | -------------------------------------------------------------------------------- /docs/animations/animations.md: -------------------------------------------------------------------------------- 1 | # Animations 2 | 3 | Create an animation by using the default Translate or Resize, inheriting from AnimationBase, or using AnimationBase 4 | directly. 5 | 6 | ## Example Usage 7 | 8 | ```Python 9 | import pygame_widgets 10 | import pygame 11 | from pygame_widgets.button import Button 12 | from pygame_widgets.animations import Resize 13 | 14 | pygame.init() 15 | win = pygame.display.set_mode((600, 600)) 16 | 17 | button = Button(win, 100, 100, 300, 150) 18 | 19 | animation = Resize(button, 3, 200, 200) 20 | animation.start() 21 | 22 | run = True 23 | while run: 24 | events = pygame.event.get() 25 | for event in events: 26 | if event.type == pygame.QUIT: 27 | pygame.quit() 28 | run = False 29 | quit() 30 | 31 | win.fill((255, 255, 255)) 32 | 33 | pygame_widgets.update(events) 34 | pygame.display.update() 35 | ``` 36 | 37 | Over 3 seconds, the width of the button was changed from 300 to 200 and its height from 150 to 200. Since it is 38 | performed on a separate thread, the button is still able to function during the animation. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 The Python Packaging Authority 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /examples/button_array_example.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import pygame_widgets 4 | from pygame_widgets.button import ButtonArray 5 | 6 | # Set up Pygame 7 | pygame.init() 8 | win = pygame.display.set_mode((600, 600)) 9 | 10 | # Creates an array of buttons 11 | buttonArray = ButtonArray( 12 | # Mandatory Parameters 13 | win, # Surface to place button array on 14 | 50, # X-coordinate 15 | 50, # Y-coordinate 16 | 500, # Width 17 | 500, # Height 18 | (2, 2), # Shape: 2 buttons wide, 2 buttons tall 19 | border=100, # Distance between buttons and edge of array 20 | texts=('1', '2', '3', '4'), # Sets the texts of each button (counts left to right then top to bottom) 21 | # When clicked, print number 22 | onClicks=(lambda: print('1'), lambda: print('2'), lambda: print('3'), lambda: print('4')) 23 | ) 24 | 25 | run = True 26 | while run: 27 | events = pygame.event.get() 28 | for event in events: 29 | if event.type == pygame.QUIT: 30 | pygame.quit() 31 | run = False 32 | quit() 33 | 34 | win.fill((255, 255, 255)) 35 | 36 | pygame_widgets.update(events) # Call once every loop to allow widgets to render and listen 37 | pygame.display.update() 38 | -------------------------------------------------------------------------------- /docs/widgets/toggle.md: -------------------------------------------------------------------------------- 1 | # Toggle 2 | 3 | Allows switching between true and false options 4 | 5 | ## Example Usage 6 | 7 | ```Python 8 | import pygame_widgets 9 | import pygame 10 | from pygame_widgets.toggle import Toggle 11 | 12 | pygame.init() 13 | win = pygame.display.set_mode((1000, 600)) 14 | 15 | toggle = Toggle(win, 100, 100, 100, 40) 16 | 17 | run = True 18 | while run: 19 | events = pygame.event.get() 20 | for event in events: 21 | if event.type == pygame.QUIT: 22 | pygame.quit() 23 | run = False 24 | quit() 25 | 26 | win.fill((255, 255, 255)) 27 | 28 | pygame_widgets.update(events) 29 | pygame.display.update() 30 | ``` 31 | 32 | ## Optional Parameters 33 | 34 | | Parameter | Description | Type | Default | 35 | | :---: | --- | :---: | :---: | 36 | | startOn | Default value. | bool | False | 37 | | onColour | Colour of toggle when on. | (int, int, int) | (141, 185, 244) | 38 | | offColour | Colour of toggle when off. | (int, int, int) | (150, 150, 150) | 39 | | handleOnColour | Thickness of toggle handle when on. | (int, int, int) | (26, 115, 232) | 40 | | handleOffColour | Thickness of toggle handle when off. | (int, int, int) | (200, 200, 200) | 41 | | handleRadius | Radius of handle. | int | height / 1.3 | -------------------------------------------------------------------------------- /examples/button_example.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import pygame_widgets 4 | from pygame_widgets.button import Button 5 | 6 | # Set up Pygame 7 | pygame.init() 8 | win = pygame.display.set_mode((600, 600)) 9 | 10 | # Creates the button with optional parameters 11 | button = Button( 12 | # Mandatory Parameters 13 | win, # Surface to place button on 14 | 100, # X-coordinate of top left corner 15 | 100, # Y-coordinate of top left corner 16 | 300, # Width 17 | 150, # Height 18 | 19 | # Optional Parameters 20 | text='Hello', # Text to display 21 | fontSize=50, # Size of font 22 | margin=20, # Minimum distance between text/image and edge of button 23 | inactiveColour=(200, 50, 0), # Colour of button when not being interacted with 24 | hoverColour=(150, 0, 0), # Colour of button when being hovered over 25 | pressedColour=(0, 200, 20), # Colour of button when being clicked 26 | radius=20, # Radius of border corners (leave empty for not curved) 27 | onClick=lambda: print('Click') # Function to call when clicked on 28 | ) 29 | 30 | run = True 31 | while run: 32 | events = pygame.event.get() 33 | for event in events: 34 | if event.type == pygame.QUIT: 35 | pygame.quit() 36 | run = False 37 | quit() 38 | 39 | win.fill((255, 255, 255)) 40 | 41 | pygame_widgets.update(events) # Call once every loop to allow widgets to render and listen 42 | pygame.display.update() 43 | -------------------------------------------------------------------------------- /docs/widgets/progressbar.md: -------------------------------------------------------------------------------- 1 | # Progress Bar 2 | 3 | Displays a continuously changing percentage 4 | 5 | ## Example Usage 6 | 7 | ```Python 8 | import pygame_widgets 9 | import pygame 10 | import time 11 | from pygame_widgets.progressbar import ProgressBar 12 | 13 | startTime = time.time() 14 | 15 | pygame.init() 16 | win = pygame.display.set_mode((1000, 600)) 17 | 18 | progressBar = ProgressBar(win, 100, 100, 500, 40, lambda: 1 - (time.time() - startTime) / 10, curved=True) 19 | 20 | run = True 21 | while run: 22 | events = pygame.event.get() 23 | for event in events: 24 | if event.type == pygame.QUIT: 25 | pygame.quit() 26 | run = False 27 | quit() 28 | 29 | win.fill((255, 255, 255)) 30 | 31 | pygame_widgets.update(events) 32 | pygame.display.update() 33 | ``` 34 | 35 | This progress bar uses time to fill up, however, the progress function can be replaced by 36 | any other function call that provides a percentage. 37 | 38 | 39 | ## Mandatory Parameters 40 | 41 | | Parameter | Description | Type | 42 | | :---: | --- | :---: | 43 | | progress | Function that defines the percentage of the bar filled. | function -> float | 44 | 45 | ## Optional Parameters 46 | 47 | | Parameter | Description | Type | Default | 48 | | :---: | --- | :---: | :---: | 49 | | curved | Adds curved ends to the progress bar. | bool | False | 50 | | completedColour | Colour of completed section of progress bar. | (int, int, int) | (0, 200, 0) | 51 | | incompletedColour | Colour of incompleted section of progress bar. | (int, int, int) | (100, 100, 100) | 52 | -------------------------------------------------------------------------------- /site/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | None 5 | 2021-07-31 6 | daily 7 | 8 | 9 | None 10 | 2021-07-31 11 | daily 12 | 13 | 14 | None 15 | 2021-07-31 16 | daily 17 | 18 | 19 | None 20 | 2021-07-31 21 | daily 22 | 23 | 24 | None 25 | 2021-07-31 26 | daily 27 | 28 | 29 | None 30 | 2021-07-31 31 | daily 32 | 33 | 34 | None 35 | 2021-07-31 36 | daily 37 | 38 | 39 | None 40 | 2021-07-31 41 | daily 42 | 43 | 44 | None 45 | 2021-07-31 46 | daily 47 | 48 | 49 | None 50 | 2021-07-31 51 | daily 52 | 53 | 54 | None 55 | 2021-07-31 56 | daily 57 | 58 | -------------------------------------------------------------------------------- /docs/widgets/slider.md: -------------------------------------------------------------------------------- 1 | # Slider 2 | 3 | A slider for discrete numeric value selection 4 | 5 | ![slider.png](../images/slider.png) 6 | 7 | ## Example Usage 8 | 9 | ```Python 10 | import pygame_widgets 11 | import pygame 12 | from pygame_widgets.slider import Slider 13 | from pygame_widgets.textbox import TextBox 14 | 15 | pygame.init() 16 | win = pygame.display.set_mode((1000, 600)) 17 | 18 | slider = Slider(win, 100, 100, 800, 40, min=0, max=99, step=1) 19 | output = TextBox(win, 475, 200, 50, 50, fontSize=30) 20 | 21 | output.disable() # Act as label instead of textbox 22 | 23 | run = True 24 | while run: 25 | events = pygame.event.get() 26 | for event in events: 27 | if event.type == pygame.QUIT: 28 | pygame.quit() 29 | run = False 30 | quit() 31 | 32 | win.fill((255, 255, 255)) 33 | 34 | output.setText(slider.getValue()) 35 | 36 | pygame_widgets.update(events) 37 | pygame.display.update() 38 | ``` 39 | 40 | As you can see, TextBox can be used to display text as well, by not calling its listen method. 41 | 42 | ## Optional Parameters 43 | 44 | | Parameter | Description | Type | Default | 45 | | :---: | --- | :---: | :---: | 46 | | min | Minimum value of the slider (left). | int or float | 0 | 47 | | max | Maximum value of the slider (right). | int or float | 99 | 48 | | step | Value to increment by. | int or float | 1 | 49 | | colour | Colour of slider. | (int, int, int) | (200, 200, 200) | 50 | | handleColour | Colour of handle. | (int, int, int) | (0, 0, 0) | 51 | | initial | Initial value of the slider. | int or float | Average of min and max | 52 | | handleRadius | Radius of handle. | int | height / 1.3 | 53 | | curved | Add curved ends to the slider. | bool | True | 54 | -------------------------------------------------------------------------------- /docs/widgets/textbox.md: -------------------------------------------------------------------------------- 1 | # TextBox 2 | 3 | A box for text input or display 4 | 5 | ![textbox.png](../images/textbox.png) 6 | 7 | ## Example Usage 8 | 9 | ```Python 10 | import pygame_widgets 11 | import pygame 12 | from pygame_widgets.textbox import TextBox 13 | 14 | 15 | def output(): 16 | # Get text in the textbox 17 | print(textbox.getText()) 18 | 19 | 20 | pygame.init() 21 | win = pygame.display.set_mode((1000, 600)) 22 | 23 | textbox = TextBox(win, 100, 100, 800, 80, fontSize=50, 24 | borderColour=(255, 0, 0), textColour=(0, 200, 0), 25 | onSubmit=output, radius=10, borderThickness=5) 26 | 27 | run = True 28 | while run: 29 | events = pygame.event.get() 30 | for event in events: 31 | if event.type == pygame.QUIT: 32 | pygame.quit() 33 | run = False 34 | quit() 35 | 36 | win.fill((255, 255, 255)) 37 | 38 | pygame_widgets.update(events) 39 | pygame.display.update() 40 | ``` 41 | 42 | ## Optional Parameters 43 | 44 | | Parameter | Description | Type | Default | 45 | | :---: | --- | :---: | :---: | 46 | | colour | Background colour. | (int, int, int) | (220, 220, 220) | 47 | | textColour | Colour of text. | (int, int, int) | (0, 0, 0) | 48 | | borderColour | Colour of border. | (int, int, int) | (0, 0, 0) | 49 | | borderThickness | Thickness of border. | int | 3 | 50 | | radius | Border radius. Set to 0 for no radius. | int | 0 | 51 | | onSubmit | Function to be called when return / enter is pressed. | function | None | 52 | | onSubmitParams | Parameters to be fed into onSubmit function. | (*any) | () | 53 | | onTextChanged | Function to be called when the text in the box is changed. | function | None | 54 | | onTextChangedParams | Parameters to be fed into onTextChanged function. | (*any) | () | 55 | | placeholderText | Text to be displayed when empty. | str | '' | 56 | | fontSize | Size of text. | int | 20 | 57 | | font | Font of text. | pygame.font.Font | Calibri -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide for Pygame Widgets 2 | 3 | If you want to contribute to this project, it would be greatly appreciated 4 | if you keep to the following code style guide as much as possible. 5 | 6 | _Note: If ever in doubt, check the existing code._ 7 | 8 | ## Naming 9 | * Variables, functions and keyword arguments should be in lowerCamelCase ***NOT*** lower_snake_case 10 | * Classes should be UpperCamelCase 11 | * Constants should be UPPERCASE_WITH_UNDERSCORES 12 | * Names should be concise but also descriptive 13 | * The names of properties and methods should be consistent across all widgets 14 | * Where possible, Australian English should be used 15 | * E.g. _colour_ instead of _color_ 16 | * _Note: This is to ensure consistency across the project for users and does not matter as much for comments_ 17 | 18 | ## Whitespace 19 | * Two lines before and after class and function declarations 20 | * A single line between separate blocks of logic 21 | * E.g. Between an _if_ and _elif_ statement 22 | * A single space between operators 23 | * E.g. _n = (x + y) / 3 == (z ** 2)_ 24 | * _Note: The only exception is if - is used for negatives_ 25 | * A single space after commas, not before 26 | * E.g. _[1, 2, 3, 4]_ 27 | * No space when setting keyword arguments 28 | * E.g. _function(param, x=1, y=2)_ 29 | 30 | ## Other 31 | * **SINGLE** quotes for 'strings' 32 | * Double quotes for """docstrings""" only 33 | * Extracting keyword arguments should be done with _kwargs.get('key', default)_ 34 | * Only use built-in exceptions if logical, otherwise create a custom exception in exceptions.py 35 | 36 | ## Documentation 37 | * Docstrings should include a short description of the function or class followed by parameter descriptions and types 38 | * Single line comments should be placed whenever complex logic is used 39 | * If unclear, type hints should be used to clarify functions 40 | -------------------------------------------------------------------------------- /docs/widgets/buttonarray.md: -------------------------------------------------------------------------------- 1 | # ButtonArray 2 | 3 | A collection of buttons with similar properties. 4 | 5 | ## Example Usage 6 | 7 | Note: See example 8 | 9 | ```Python 10 | import pygame 11 | 12 | import pygame_widgets 13 | from pygame_widgets.button import ButtonArray 14 | 15 | # Set up Pygame 16 | pygame.init() 17 | win = pygame.display.set_mode((600, 600)) 18 | 19 | # Creates an array of buttons 20 | buttonArray = ButtonArray( 21 | # Mandatory Parameters 22 | win, # Surface to place button array on 23 | 50, # X-coordinate 24 | 50, # Y-coordinate 25 | 500, # Width 26 | 500, # Height 27 | (2, 2), # Shape: 2 buttons wide, 2 buttons tall 28 | border=100, # Distance between buttons and edge of array 29 | texts=('1', '2', '3', '4'), # Sets the texts of each button (counts left to right then top to bottom) 30 | # When clicked, print number 31 | onClicks=(lambda: print('1'), lambda: print('2'), lambda: print('3'), lambda: print('4')) 32 | ) 33 | 34 | run = True 35 | while run: 36 | events = pygame.event.get() 37 | for event in events: 38 | if event.type == pygame.QUIT: 39 | pygame.quit() 40 | run = False 41 | quit() 42 | 43 | win.fill((255, 255, 255)) 44 | 45 | pygame_widgets.update(events) # Call once every loop to allow widgets to render and listen 46 | pygame.display.update() 47 | ``` 48 | 49 | ## Mandatory Parameters 50 | 51 | _Note: Mandatory parameters must be supplied in order._ 52 | 53 | | Parameter | Description | Type | 54 | | :---: | --- | :---: | 55 | | shape | Number of columns and rows of buttons (columns, rows). | (int, int) | 56 | 57 | ## Optional Parameters 58 | 59 | _Note: Optional parameters of ButtonArray are similar to those of Button._ 60 | 61 | | Parameter | Description | Type | Default | 62 | | :---: | --- | :---: | :---: | 63 | | colour | Background colour of array. | (int, int, int) | (210, 210, 180) | 64 | | border | Thickness between buttons and between the edges of array and buttons. | int | 10 | 65 | | topBorder | Thickness between top of array and top of button. Overrides border. | int | border | 66 | | bottomBorder | Thickness between bottom of array and bottom of button. Overrides border. | int | border | 67 | | leftBorder | Thickness between left of array and left of button. Overrides border. | int | border | 68 | | rightBorder | Thickness between right of array and right of button. Overrides border. | int | border | 69 | | separationThickness | Thickness between buttons. Overrides border. | int | border | 70 | -------------------------------------------------------------------------------- /pygame_widgets/toggle.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame import gfxdraw 3 | 4 | import pygame_widgets 5 | from pygame_widgets.widget import WidgetBase 6 | from pygame_widgets.mouse import Mouse, MouseState 7 | 8 | 9 | class Toggle(WidgetBase): 10 | def __init__(self, win, x, y, width, height, **kwargs): 11 | super().__init__(win, x, y, width, height) 12 | 13 | self.value = kwargs.get('startOn', False) 14 | self.onColour = kwargs.get('onColour', (141, 185, 244)) 15 | self.offColour = kwargs.get('offColour', (150, 150, 150)) 16 | self.handleOnColour = kwargs.get('handleOnColour', (26, 115, 232)) 17 | self.handleOffColour = kwargs.get('handleOffColour', (200, 200, 200)) 18 | 19 | self.handleRadius = kwargs.get('handleRadius', int(self._height / 1.3)) 20 | self.radius = self._height // 2 21 | 22 | self.colour = self.onColour if self.value else self.offColour 23 | self.handleColour = self.handleOnColour if self.value else self.handleOffColour 24 | 25 | def toggle(self): 26 | self.value = not self.value 27 | self.colour = self.onColour if self.value else self.offColour 28 | self.handleColour = self.handleOnColour if self.value else self.handleOffColour 29 | 30 | def listen(self, events): 31 | if not self._hidden and not self._disabled: 32 | mouseState = Mouse.getMouseState() 33 | x, y = Mouse.getMousePos() 34 | 35 | if self.contains(x, y): 36 | if mouseState == MouseState.CLICK: 37 | self.toggle() 38 | 39 | def draw(self): 40 | if not self._hidden: 41 | pygame.draw.rect(self.win, self.colour, (self._x, self._y, self._width, self._height)) 42 | 43 | pygame.draw.circle(self.win, self.colour, (self._x, self._y + self._height // 2), self.radius) 44 | pygame.draw.circle(self.win, self.colour, (self._x + self._width, self._y + self._height // 2), self.radius) 45 | 46 | circle = ( 47 | self._x + ( 48 | self._width - self.handleRadius + self.radius if self.value else self.handleRadius - self.radius 49 | ), 50 | self._y + self._height // 2 51 | ) 52 | 53 | gfxdraw.filled_circle(self.win, *circle, self.handleRadius, self.handleColour) 54 | gfxdraw.aacircle(self.win, *circle, self.handleRadius, self.handleColour) 55 | 56 | def getValue(self): 57 | return self.value 58 | 59 | 60 | if __name__ == '__main__': 61 | pygame.init() 62 | win = pygame.display.set_mode((1000, 600)) 63 | 64 | toggle = Toggle(win, 100, 100, 100, 40) 65 | 66 | run = True 67 | while run: 68 | events = pygame.event.get() 69 | for event in events: 70 | if event.type == pygame.QUIT: 71 | pygame.quit() 72 | run = False 73 | quit() 74 | 75 | win.fill((255, 255, 255)) 76 | 77 | pygame_widgets.update(events) 78 | pygame.display.update() 79 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Pygame Widgets 2 | 3 | ![](https://img.shields.io/pypi/dm/pygame-widgets) 4 | 5 | A helper module for common widgets that may be required in developing applications with Pygame. It supports fully 6 | customisable buttons, collections of buttons, textboxes, sliders and many more! If there are any widgets that you would like to see 7 | added, please create an issue! 8 | 9 | ## Changes in Pygame Widgets v1.0.0 10 | In v1.0.0, there are some minor changes to the use of the module, which may affect existing projects. 11 | This outlines the changes that will affect current users in the new version. 12 | 13 | * As more widgets are added, importing is now different 14 | ```Python 15 | # Now 16 | from pygame_widgets.button import Button 17 | 18 | # Instead of 19 | from pygame_widgets import Button # Will not work 20 | ``` 21 | * All widgets are now updated (draw and listen) by the update method 22 | 23 | ```Python 24 | import pygame 25 | import pygame_widgets 26 | from pygame_widgets.button import Button 27 | 28 | pygame.init() 29 | win = pygame.display.set_mode((600, 600)) 30 | button = Button(win, 100, 100, 300, 150) 31 | 32 | run = True 33 | while run: 34 | events = pygame.event.get() 35 | for event in events: 36 | if event.type == pygame.QUIT: 37 | pygame.quit() 38 | run = False 39 | quit() 40 | 41 | win.fill((255, 255, 255)) 42 | 43 | # Now 44 | pygame_widgets.update(events) 45 | 46 | # Instead of 47 | button.listen(events) 48 | button.draw() 49 | 50 | pygame.display.update() 51 | ``` 52 | 53 | 54 | ## Prerequisites 55 | 56 | * [Python 3](https://www.python.org/downloads) `>= 3.7` 57 | * [Pygame](https://www.pygame.org/wiki/GettingStarted) `>= 2.0.0` 58 | 59 | ## Installation 60 | 61 | Ensure that Python 3 and pip are installed and added to your environment PATH. 62 | 63 | ```python -m pip install pygame-widgets``` 64 | 65 | Open a Python console and run the following command. 66 | 67 | ```import pygame_widgets``` 68 | 69 | If you receive no errors, the installation was successful. 70 | 71 | ## Usage 72 | 73 | For full documentation, see [pygamewidgets.readthedocs.io](https://pygamewidgets.readthedocs.io/en/latest/). 74 | 75 | * [Common](widgets/common.md) 76 | * [Button](widgets/button.md) 77 | * [ButtonArray](widgets/buttonarray.md) 78 | * [TextBox](widgets/textbox.md) 79 | * [Slider](widgets/slider.md) 80 | * [Toggle](widgets/toggle.md) 81 | * [ProgressBar](widgets/progressbar.md) 82 | * [Dropdown](widgets/dropdown.md) 83 | * [ComboBox](widgets/combobox.md) 84 | * [Animations](animations/animations.md) 85 | 86 | ## How to Contribute 87 | 88 | Any contribution to this project would be greatly appreciated. 89 | This can include: 90 | * Finding errors or bugs and creating a new issue 91 | * Addressing active issues 92 | * Adding functionality 93 | * Improving documentation 94 | 95 | If applicable, you should make any changes in a forked repository and then create a pull 96 | request once the changes are ***complete*** and preferably tested if possible. 97 | 98 | _Note: If writing any code, please attempt to follow the [Code Style Guide](CONTRIBUTING.md)_ 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pygame Widgets 2 | 3 | ![](https://img.shields.io/pypi/dm/pygame-widgets) 4 | 5 | A helper module for common widgets that may be required in developing applications with Pygame. It supports fully 6 | customisable buttons, collections of buttons, textboxes, sliders and many more! If there are any widgets that you would like to see 7 | added, please create an issue! 8 | 9 | ## Changes in Pygame Widgets v1.0.0 10 | In v1.0.0, there are some minor changes to the use of the module, which may affect existing projects. 11 | This outlines the changes that will affect current users in the new version. 12 | 13 | * As more widgets are added, importing is now different 14 | ```Python 15 | # Now 16 | from pygame_widgets.button import Button 17 | 18 | # Instead of 19 | from pygame_widgets import Button # Will not work 20 | ``` 21 | * All widgets are now updated (draw and listen) by the update method 22 | 23 | ```Python 24 | import pygame 25 | import pygame_widgets 26 | from pygame_widgets.button import Button 27 | 28 | pygame.init() 29 | win = pygame.display.set_mode((600, 600)) 30 | button = Button(win, 100, 100, 300, 150) 31 | 32 | run = True 33 | while run: 34 | events = pygame.event.get() 35 | for event in events: 36 | if event.type == pygame.QUIT: 37 | pygame.quit() 38 | run = False 39 | quit() 40 | 41 | win.fill((255, 255, 255)) 42 | 43 | # Now 44 | pygame_widgets.update(events) 45 | 46 | # Instead of 47 | button.listen(events) 48 | button.draw() 49 | 50 | pygame.display.update() 51 | ``` 52 | 53 | 54 | ## Prerequisites 55 | 56 | * [Python 3](https://www.python.org/downloads) `>= 3.10` 57 | * [Pygame](https://www.pygame.org/wiki/GettingStarted) `>= 2.0.0` 58 | * [Pyperclip](https://github.com/asweigart/pyperclip) `>= 1.8.0` 59 | 60 | ## Installation 61 | 62 | Ensure that Python 3 and pip are installed and added to your environment PATH. 63 | 64 | ```python -m pip install pygame-widgets``` 65 | 66 | Open a Python console and run the following command. 67 | 68 | ```import pygame_widgets``` 69 | 70 | If you receive no errors, the installation was successful. 71 | 72 | ## Usage 73 | 74 | For full documentation, see [pygamewidgets.readthedocs.io](https://pygamewidgets.readthedocs.io/en/latest/). 75 | 76 | * [Common](docs/widgets/common.md) 77 | * [Button](docs/widgets/button.md) 78 | * [ButtonArray](docs/widgets/buttonarray.md) 79 | * [TextBox](docs/widgets/textbox.md) 80 | * [Slider](docs/widgets/slider.md) 81 | * [Toggle](docs/widgets/toggle.md) 82 | * [ProgressBar](docs/widgets/progressbar.md) 83 | * [Dropdown](docs/widgets/dropdown.md) 84 | * [ComboBox](docs/widgets/combobox.md) 85 | * [Animations](docs/animations/animations.md) 86 | 87 | ## How to Contribute 88 | 89 | Any contribution to this project would be greatly appreciated. 90 | This can include: 91 | * Finding errors or bugs and creating a new issue 92 | * Addressing active issues 93 | * Adding functionality 94 | * Improving documentation 95 | 96 | If applicable, you should make any changes in a forked repository and then create a pull 97 | request once the changes are ***complete*** and preferably tested if possible. 98 | 99 | _Note: If writing any code, please attempt to follow the [Code Style Guide](docs/CONTRIBUTING.md)_ 100 | -------------------------------------------------------------------------------- /pygame_widgets/progressbar.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | import pygame 3 | 4 | import pygame_widgets 5 | from pygame_widgets.widget import WidgetBase 6 | 7 | 8 | class ProgressBar(WidgetBase): 9 | def __init__(self, win, x, y, width, height, progress: Callable[[], float], **kwargs): 10 | super().__init__(win, x, y, width, height) 11 | self.progress = progress 12 | 13 | self.curved = kwargs.get('curved', False) 14 | 15 | self.completedColour = kwargs.get('completedColour', (0, 200, 0)) 16 | self.incompletedColour = kwargs.get('incompletedColour', (100, 100, 100)) 17 | 18 | self.percent = self.progress() 19 | 20 | self.radius = self._height / 2 if self.curved else 0 21 | 22 | self.disable() 23 | 24 | def listen(self, events): 25 | pass 26 | 27 | def draw(self): 28 | """ Display to surface """ 29 | self.percent = min(max(self.progress(), 0), 1) 30 | 31 | if not self._hidden: 32 | if self.curved: 33 | if self.percent == 0: 34 | pygame.draw.circle(self.win, self.incompletedColour, 35 | (self._x, self._y + self._height // 2), self.radius) 36 | pygame.draw.circle(self.win, self.incompletedColour, 37 | (self._x + self._width, self._y + self._height // 2), 38 | self.radius) 39 | elif self.percent == 1: 40 | pygame.draw.circle(self.win, self.completedColour, 41 | (self._x, self._y + self._height // 2), self.radius) 42 | pygame.draw.circle(self.win, self.completedColour, 43 | (self._x + self._width, self._y + self._height // 2), 44 | self.radius) 45 | else: 46 | pygame.draw.circle(self.win, self.completedColour, (self._x, self._y + self._height // 2), 47 | self.radius) 48 | pygame.draw.circle(self.win, self.incompletedColour, 49 | (self._x + self._width, self._y + self._height // 2), 50 | self.radius) 51 | 52 | pygame.draw.rect(self.win, self.completedColour, 53 | (self._x, self._y, int(self._width * self.percent), self._height)) 54 | pygame.draw.rect(self.win, self.incompletedColour, 55 | (self._x + int(self._width * self.percent), self._y, 56 | int(self._width * (1 - self.percent)), self._height)) 57 | 58 | 59 | if __name__ == '__main__': 60 | import time 61 | 62 | startTime = time.time() 63 | 64 | pygame.init() 65 | win = pygame.display.set_mode((1000, 600)) 66 | 67 | progressBar = ProgressBar(win, 100, 100, 500, 40, lambda: 1 - (time.time() - startTime) / 10, curved=True) 68 | 69 | run = True 70 | while run: 71 | events = pygame.event.get() 72 | for event in events: 73 | if event.type == pygame.QUIT: 74 | pygame.quit() 75 | run = False 76 | quit() 77 | 78 | win.fill((255, 255, 255)) 79 | 80 | pygame_widgets.update(events) 81 | pygame.display.update() 82 | -------------------------------------------------------------------------------- /site/search/main.js: -------------------------------------------------------------------------------- 1 | function getSearchTermFromLocation() { 2 | var sPageURL = window.location.search.substring(1); 3 | var sURLVariables = sPageURL.split('&'); 4 | for (var i = 0; i < sURLVariables.length; i++) { 5 | var sParameterName = sURLVariables[i].split('='); 6 | if (sParameterName[0] == 'q') { 7 | return decodeURIComponent(sParameterName[1].replace(/\+/g, '%20')); 8 | } 9 | } 10 | } 11 | 12 | function joinUrl (base, path) { 13 | if (path.substring(0, 1) === "/") { 14 | // path starts with `/`. Thus it is absolute. 15 | return path; 16 | } 17 | if (base.substring(base.length-1) === "/") { 18 | // base ends with `/` 19 | return base + path; 20 | } 21 | return base + "/" + path; 22 | } 23 | 24 | function formatResult (location, title, summary) { 25 | return '

'+ title + '

' + summary +'

'; 26 | } 27 | 28 | function displayResults (results) { 29 | var search_results = document.getElementById("mkdocs-search-results"); 30 | while (search_results.firstChild) { 31 | search_results.removeChild(search_results.firstChild); 32 | } 33 | if (results.length > 0){ 34 | for (var i=0; i < results.length; i++){ 35 | var result = results[i]; 36 | var html = formatResult(result.location, result.title, result.summary); 37 | search_results.insertAdjacentHTML('beforeend', html); 38 | } 39 | } else { 40 | var noResultsText = search_results.getAttribute('data-no-results-text'); 41 | if (!noResultsText) { 42 | noResultsText = "No results found"; 43 | } 44 | search_results.insertAdjacentHTML('beforeend', '

' + noResultsText + '

'); 45 | } 46 | } 47 | 48 | function doSearch () { 49 | var query = document.getElementById('mkdocs-search-query').value; 50 | if (query.length > min_search_length) { 51 | if (!window.Worker) { 52 | displayResults(search(query)); 53 | } else { 54 | searchWorker.postMessage({query: query}); 55 | } 56 | } else { 57 | // Clear results for short queries 58 | displayResults([]); 59 | } 60 | } 61 | 62 | function initSearch () { 63 | var search_input = document.getElementById('mkdocs-search-query'); 64 | if (search_input) { 65 | search_input.addEventListener("keyup", doSearch); 66 | } 67 | var term = getSearchTermFromLocation(); 68 | if (term) { 69 | search_input.value = term; 70 | doSearch(); 71 | } 72 | } 73 | 74 | function onWorkerMessage (e) { 75 | if (e.data.allowSearch) { 76 | initSearch(); 77 | } else if (e.data.results) { 78 | var results = e.data.results; 79 | displayResults(results); 80 | } else if (e.data.config) { 81 | min_search_length = e.data.config.min_search_length-1; 82 | } 83 | } 84 | 85 | if (!window.Worker) { 86 | console.log('Web Worker API not supported'); 87 | // load index in main thread 88 | $.getScript(joinUrl(base_url, "search/worker.js")).done(function () { 89 | console.log('Loaded worker'); 90 | init(); 91 | window.postMessage = function (msg) { 92 | onWorkerMessage({data: msg}); 93 | }; 94 | }).fail(function (jqxhr, settings, exception) { 95 | console.error('Could not load worker.js'); 96 | }); 97 | } else { 98 | // Wrap search in a web worker 99 | var searchWorker = new Worker(joinUrl(base_url, "search/worker.js")); 100 | searchWorker.postMessage({init: true}); 101 | searchWorker.onmessage = onWorkerMessage; 102 | } 103 | -------------------------------------------------------------------------------- /docs/widgets/dropdown.md: -------------------------------------------------------------------------------- 1 | # Dropdown 2 | 3 | A dropdown menu allowing the selection of various elements. 4 | 5 | ![dropdown.gif](../images/dropdown.gif) 6 | 7 | ```Python 8 | import pygame_widgets 9 | import pygame 10 | from pygame_widgets.button import Button 11 | from pygame_widgets.dropdown import Dropdown 12 | 13 | pygame.init() 14 | win = pygame.display.set_mode((400, 280)) 15 | 16 | dropdown = Dropdown( 17 | win, 120, 10, 100, 50, name='Select Color', 18 | choices=[ 19 | 'Red', 20 | 'Blue', 21 | 'Yellow', 22 | ], 23 | borderRadius=3, colour=pygame.Color('green'), values=[1, 2, 'true'], direction='down', textHAlign='left' 24 | ) 25 | 26 | 27 | def print_value(): 28 | print(dropdown.getSelected()) 29 | 30 | 31 | button = Button( 32 | win, 10, 10, 100, 50, text='Print Value', fontSize=30, 33 | margin=20, inactiveColour=(255, 0, 0), pressedColour=(0, 255, 0), 34 | radius=5, onClick=print_value, font=pygame.font.SysFont('calibri', 10), 35 | textVAlign='bottom' 36 | ) 37 | 38 | run = True 39 | while run: 40 | events = pygame.event.get() 41 | for event in events: 42 | if event.type == pygame.QUIT: 43 | pygame.quit() 44 | run = False 45 | quit() 46 | 47 | win.fill((255, 255, 255)) 48 | 49 | pygame_widgets.update(events) 50 | pygame.display.update() 51 | ``` 52 | 53 | This is a classic dropdown, but with a twist: if you right-click on the top, it reset itself. To get the current value 54 | of the dropdown, we use the `getSelected()` methods. 55 | 56 | It returns: 57 | 58 | - `None` if nothing is selected 59 | - A string with the choice you selected if the optional arg `value` is not set 60 | - If the optional arg `value` is set, we return the value corresponding to the choice. 61 | 62 | For the example above: 63 | 64 | | Choice | Value | 65 | | :---: | :---: | 66 | | Red | 1 | 67 | | Blue | 2 | 68 | | Yellow | 3 | 69 | 70 | ## Mandatory Parameters 71 | 72 | _Note: Mandatory parameters must be supplied in order._ 73 | 74 | | Parameter | Description | Type | 75 | | :---: | --- | :---: | 76 | | name | Main name of the dropdown | str | 77 | | choices | Choices to display | list of str | 78 | 79 | ## Optional Parameters 80 | 81 | | Parameter | Description | Type | Default | 82 | | :---: | --- | :---: | :---: | 83 | | direction | Expansion direction. Can be 'down', 'up', 'left' or 'right'. | str | down | 84 | | values | optional return value corresponding to the choices. Must be the same length as `choices` |list|a copy of choices| 85 | | inactiveColour | Default colour when not pressed or hovered over. | (int, int, int) | (150, 150, 150) | 86 | | pressedColour | Colour when pressed. | (int, int, int) | (100, 100, 100) | 87 | | hoverColour | Colour when hovered over. | (int, int, int) | (125, 125, 125) | 88 | | onClick | Function to be called when clicked. | function | None | 89 | | onClickParams | Parameters to be fed into onClick function. | (*any) | () | 90 | | onRelease | Function to be called when released. | function | None | 91 | | onReleaseParams | Parameters to be fed into onRelease function. | (*any) | () | 92 | | textColour | Colour of text. | (int, int, int) | (0, 0, 0) | 93 | | fontSize | Size of text. | int | 20 | 94 | | font | Font of text. | pygame.font.Font | sans-serif | 95 | | textHAlign | Horizontal alignment of text. Can be 'centre', 'left' or 'right'. | str | 'centre' | 96 | | borderColour | Colour of border. | (int, int, int) | (0, 0, 0) | 97 | | borderThickness | Thickness of border. | int | 3 | 98 | | borderRadius | Border radius. Set to 0 for no radius. | int | 0 | 99 | -------------------------------------------------------------------------------- /pygame_widgets/mouse.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import pygame 3 | import time 4 | 5 | 6 | class MouseState(Enum): 7 | HOVER = 0 8 | CLICK = 1 9 | RIGHT_CLICK = 2 10 | DRAG = 3 11 | RIGHT_DRAG = 4 # Not sure when this is ever used but added anyway for completeness 12 | RELEASE = 5 13 | RIGHT_RELEASE = 6 14 | 15 | 16 | class Mouse: 17 | _refreshTime = 0.01 18 | 19 | # Redundant currently, may use for double click handling 20 | lastLeftClick = 0 21 | lastRightClick = 0 22 | leftClickElapsedTime = 0 23 | rightClickElapsedTime = 0 24 | 25 | _mouseState = MouseState.HOVER 26 | 27 | @staticmethod 28 | def listen(): 29 | listening = True 30 | while listening: 31 | try: 32 | Mouse.updateMouseState() 33 | except pygame.error: 34 | listening = False 35 | time.sleep(Mouse._refreshTime) 36 | 37 | @staticmethod 38 | def updateMouseState(): 39 | leftPressed = pygame.mouse.get_pressed()[0] 40 | rightPressed = pygame.mouse.get_pressed()[2] 41 | 42 | if leftPressed: 43 | if Mouse._mouseState == MouseState.CLICK or Mouse._mouseState == MouseState.DRAG: 44 | Mouse._mouseState = MouseState.DRAG 45 | else: 46 | Mouse._mouseState = MouseState.CLICK 47 | 48 | elif rightPressed: 49 | if Mouse._mouseState == MouseState.RIGHT_CLICK or Mouse._mouseState == MouseState.RIGHT_DRAG: 50 | Mouse._mouseState = MouseState.RIGHT_DRAG 51 | else: 52 | Mouse._mouseState = MouseState.RIGHT_CLICK 53 | else: 54 | # If previously was held down, call the release 55 | if Mouse._mouseState == MouseState.CLICK or Mouse._mouseState == MouseState.DRAG: 56 | Mouse._mouseState = MouseState.RELEASE 57 | 58 | elif Mouse._mouseState == MouseState.RIGHT_CLICK or Mouse._mouseState == MouseState.RIGHT_DRAG: 59 | Mouse._mouseState = MouseState.RIGHT_RELEASE 60 | 61 | else: 62 | Mouse._mouseState = MouseState.HOVER 63 | 64 | @staticmethod 65 | def updateElapsedTime(): 66 | """Also redundant until double click functionality implemented""" 67 | if Mouse._mouseState == MouseState.CLICK or Mouse._mouseState == MouseState.DRAG: 68 | Mouse.leftClickElapsedTime = time.time() - Mouse.lastLeftClick 69 | elif Mouse._mouseState == MouseState.RIGHT_CLICK or Mouse._mouseState == MouseState.RIGHT_DRAG: 70 | Mouse.rightClickElapsedTime = time.time() - Mouse.lastRightClick 71 | 72 | @staticmethod 73 | def getMouseState() -> MouseState: 74 | return Mouse._mouseState 75 | 76 | @staticmethod 77 | def getMousePos() -> (int, int): 78 | return pygame.mouse.get_pos() 79 | 80 | @staticmethod 81 | def setRefreshRatePerSec(refreshRate): 82 | Mouse._refreshTime = 1 / refreshRate if refreshRate != 0 else 0 83 | 84 | 85 | if __name__ == '__main__': 86 | pygame.init() 87 | win = pygame.display.set_mode((600, 600)) 88 | 89 | run = True 90 | while run: 91 | events = pygame.event.get() 92 | for event in events: 93 | if event.type == pygame.QUIT: 94 | pygame.quit() 95 | run = False 96 | quit() 97 | 98 | win.fill((255, 255, 255)) 99 | 100 | Mouse.updateMouseState() 101 | 102 | pygame.display.update() 103 | time.sleep(0.1) 104 | -------------------------------------------------------------------------------- /docs/widgets/combobox.md: -------------------------------------------------------------------------------- 1 | # ComboBox 2 | 3 | A dropdown menu allowing the selection of various elements using a search bar. 4 | 5 | It is similar to `Dropdown` but includes a `TextBox` that allows searching of options. 6 | 7 | The parameters of the `TextBox` can be made different 8 | from the ones of the dropdown if they are specified in 9 | the `textboxKwargs` parameter. 10 | 11 | ![combobox.gif](../images/combobox.gif) 12 | 13 | ```Python 14 | import pygame_widgets 15 | import pygame 16 | 17 | from pygame_widgets.combobox import ComboBox 18 | from pygame_widgets.button import Button 19 | 20 | pygame.init() 21 | win = pygame.display.set_mode((600, 600)) 22 | 23 | comboBox = ComboBox( 24 | win, 120, 10, 250, 50, name='Select Color', 25 | choices=pygame.colordict.THECOLORS.keys(), 26 | maxResults=4, 27 | font=pygame.font.SysFont('calibri', 30), 28 | borderRadius=3, colour=(0, 200, 50), direction='down', 29 | textHAlign='left' 30 | ) 31 | 32 | 33 | def output(): 34 | comboBox.textBar.colour = comboBox.getText() 35 | 36 | 37 | button = Button( 38 | win, 10, 10, 100, 50, text='Set Colour', fontSize=30, 39 | margin=15, inactiveColour=(200, 0, 100), pressedColour=(0, 255, 0), 40 | radius=5, onClick=output, font=pygame.font.SysFont('calibri', 18), 41 | textVAlign='bottom' 42 | ) 43 | 44 | run = True 45 | while run: 46 | events = pygame.event.get() 47 | for event in events: 48 | if event.type == pygame.QUIT: 49 | pygame.quit() 50 | run = False 51 | quit() 52 | 53 | win.fill((255, 255, 255)) 54 | 55 | pygame_widgets.update(events) 56 | pygame.display.update() 57 | ``` 58 | 59 | This is a classic combo box. The current selected text 60 | can be accessed through the `getText()` methods. 61 | 62 | It returns the current text in the search bar. 63 | 64 | 65 | ## Mandatory Parameters 66 | 67 | _Note: Mandatory parameters must be supplied in order._ 68 | 69 | | Parameter | Description | Type | 70 | | :---: | --- | :---: | 71 | | choices | Choices to appear in the list | list of str | 72 | 73 | ## Optional Parameters 74 | 75 | | Parameter | Description | Type | Default | 76 | | :---: | --- | :---: | :---: | 77 | | direction | Expansion direction. Can be 'down', 'up', 'left' or 'right'. | str | down | 78 | | inactiveColour | Default colour when not pressed or hovered over. | (int, int, int) | (150, 150, 150) | 79 | | pressedColour | Colour when pressed. | (int, int, int) | (100, 100, 100) | 80 | | hoverColour | Colour when hovered over. | (int, int, int) | (125, 125, 125) | 81 | | maxChoices | Maximum number of choices to display | int | len(choices) | 82 | | searchAlgo | Algorithm to be used to search through choices. | function(str, list) -> list | ComboBox._defaultSearch | 83 | | onSelected | Function to be called when a search choice is selected. | function | None | 84 | | onSelectedParams | Parameters to be fed into onSelected function. | (*any) | () | 85 | | onStartSearch | Function to be called when a search is started by user (clicking on the search box). | function | None | 86 | | onStartSearchParams | Parameters to be fed into onStartSearch function. | (*any) | () | 87 | | onStopSearch | Function to be called when a search is stopped (clicking outside the search dropdown, or selecting a choice). | function | None | 88 | | onStopSearchParams | Parameters to be fed into onStopSearch function. | (*any) | () | 89 | | textColour | Colour of text. | (int, int, int) | (0, 0, 0) | 90 | | fontSize | Size of text. | int | 20 | 91 | | font | Font of text. | pygame.font.Font | sans-serif | 92 | | textHAlign | Horizontal alignment of text. Can be 'centre', 'left' or 'right'. | str | 'centre' | 93 | | borderColour | Colour of border. | (int, int, int) | (0, 0, 0) | 94 | | borderThickness | Thickness of border. | int | 3 | 95 | | borderRadius | Border radius. Set to 0 for no radius. | int | 0 | 96 | | textboxKwargs | Optional different parameters only for the `TextBox`. | dict | {} | 97 | -------------------------------------------------------------------------------- /site/search/worker.js: -------------------------------------------------------------------------------- 1 | var base_path = 'function' === typeof importScripts ? '.' : '/search/'; 2 | var allowSearch = false; 3 | var index; 4 | var documents = {}; 5 | var lang = ['en']; 6 | var data; 7 | 8 | function getScript(script, callback) { 9 | console.log('Loading script: ' + script); 10 | $.getScript(base_path + script).done(function () { 11 | callback(); 12 | }).fail(function (jqxhr, settings, exception) { 13 | console.log('Error: ' + exception); 14 | }); 15 | } 16 | 17 | function getScriptsInOrder(scripts, callback) { 18 | if (scripts.length === 0) { 19 | callback(); 20 | return; 21 | } 22 | getScript(scripts[0], function() { 23 | getScriptsInOrder(scripts.slice(1), callback); 24 | }); 25 | } 26 | 27 | function loadScripts(urls, callback) { 28 | if( 'function' === typeof importScripts ) { 29 | importScripts.apply(null, urls); 30 | callback(); 31 | } else { 32 | getScriptsInOrder(urls, callback); 33 | } 34 | } 35 | 36 | function onJSONLoaded () { 37 | data = JSON.parse(this.responseText); 38 | var scriptsToLoad = ['lunr.js']; 39 | if (data.config && data.config.lang && data.config.lang.length) { 40 | lang = data.config.lang; 41 | } 42 | if (lang.length > 1 || lang[0] !== "en") { 43 | scriptsToLoad.push('lunr.stemmer.support.js'); 44 | if (lang.length > 1) { 45 | scriptsToLoad.push('lunr.multi.js'); 46 | } 47 | if (lang.includes("ja") || lang.includes("jp")) { 48 | scriptsToLoad.push('tinyseg.js'); 49 | } 50 | for (var i=0; i < lang.length; i++) { 51 | if (lang[i] != 'en') { 52 | scriptsToLoad.push(['lunr', lang[i], 'js'].join('.')); 53 | } 54 | } 55 | } 56 | loadScripts(scriptsToLoad, onScriptsLoaded); 57 | } 58 | 59 | function onScriptsLoaded () { 60 | console.log('All search scripts loaded, building Lunr index...'); 61 | if (data.config && data.config.separator && data.config.separator.length) { 62 | lunr.tokenizer.separator = new RegExp(data.config.separator); 63 | } 64 | 65 | if (data.index) { 66 | index = lunr.Index.load(data.index); 67 | data.docs.forEach(function (doc) { 68 | documents[doc.location] = doc; 69 | }); 70 | console.log('Lunr pre-built index loaded, search ready'); 71 | } else { 72 | index = lunr(function () { 73 | if (lang.length === 1 && lang[0] !== "en" && lunr[lang[0]]) { 74 | this.use(lunr[lang[0]]); 75 | } else if (lang.length > 1) { 76 | this.use(lunr.multiLanguage.apply(null, lang)); // spread operator not supported in all browsers: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator#Browser_compatibility 77 | } 78 | this.field('title'); 79 | this.field('text'); 80 | this.ref('location'); 81 | 82 | for (var i=0; i < data.docs.length; i++) { 83 | var doc = data.docs[i]; 84 | this.add(doc); 85 | documents[doc.location] = doc; 86 | } 87 | }); 88 | console.log('Lunr index built, search ready'); 89 | } 90 | allowSearch = true; 91 | postMessage({config: data.config}); 92 | postMessage({allowSearch: allowSearch}); 93 | } 94 | 95 | function init () { 96 | var oReq = new XMLHttpRequest(); 97 | oReq.addEventListener("load", onJSONLoaded); 98 | var index_path = base_path + '/search_index.json'; 99 | if( 'function' === typeof importScripts ){ 100 | index_path = 'search_index.json'; 101 | } 102 | oReq.open("GET", index_path); 103 | oReq.send(); 104 | } 105 | 106 | function search (query) { 107 | if (!allowSearch) { 108 | console.error('Assets for search still loading'); 109 | return; 110 | } 111 | 112 | var resultDocuments = []; 113 | var results = index.search(query); 114 | for (var i=0; i < results.length; i++){ 115 | var result = results[i]; 116 | doc = documents[result.ref]; 117 | doc.summary = doc.text.substring(0, 200); 118 | resultDocuments.push(doc); 119 | } 120 | return resultDocuments; 121 | } 122 | 123 | if( 'function' === typeof importScripts ) { 124 | onmessage = function (e) { 125 | if (e.data.init) { 126 | init(); 127 | } else if (e.data.query) { 128 | postMessage({ results: search(e.data.query) }); 129 | } else { 130 | console.error("Worker - Unrecognized message: " + e); 131 | } 132 | }; 133 | } 134 | -------------------------------------------------------------------------------- /pygame_widgets/animations/animation.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | import time 3 | import pygame 4 | 5 | import pygame_widgets 6 | from pygame_widgets.exceptions import InvalidParameter, InvalidParameterType 7 | 8 | 9 | class AnimationBase: 10 | def __init__(self, widget, timeout, allowMultiple=False, **kwargs): 11 | """Base for animations 12 | 13 | :param widget: The widget that the animation targets 14 | :param timeout: The time of the animation in seconds 15 | :param kwargs: The target of the animation, e.g. x=10 changes x position to 10 16 | """ 17 | self.widget = widget 18 | self.timeout = timeout 19 | self.allowMultiple = allowMultiple 20 | self.params = kwargs 21 | self.thread = Thread() 22 | 23 | self.started = False 24 | self.runOnce = False 25 | 26 | self.checkValidParams() 27 | 28 | def checkValidParams(self): 29 | for param, target in self.params.items(): 30 | value = self.widget.get(param) 31 | if value is None: 32 | raise InvalidParameter( 33 | f'The parameter <{param}> is not a valid attribute of type {type(self.widget)}' 34 | ) 35 | elif type(value) != type(target): 36 | raise InvalidParameterType( 37 | f'Expected parameter <{param}> to be of type {type(value)} but found type {type(target)}' 38 | ) 39 | 40 | def start(self): 41 | if not self.started and not (self.runOnce and not self.allowMultiple): 42 | self.thread = Thread(target=self.loop) 43 | self.thread.start() 44 | 45 | def loop(self): 46 | self.started = self.runOnce = True 47 | 48 | start = time.time() 49 | 50 | initialNumberParams = {} 51 | initialTupleParams = {} 52 | for param, target in self.params.items(): 53 | initialValue = self.widget.get(param) 54 | if isinstance(initialValue, (int, float)): 55 | initialNumberParams[param] = initialValue 56 | elif isinstance(initialValue, (tuple, list)): 57 | initialTupleParams[param] = tuple(initialValue) 58 | 59 | # Animate 60 | while time.time() - start < self.timeout: 61 | step = (time.time() - start) / self.timeout 62 | 63 | # Numeric animation 64 | for param, initialValue in initialNumberParams.items(): 65 | target = self.params[param] 66 | newValue = initialValue + step * (target - initialValue) 67 | self.widget.set(param, newValue) 68 | 69 | # Tuple animation 70 | for param, initialTuple in initialTupleParams.items(): 71 | target = self.params[param] 72 | newValue = tuple( 73 | initialTuple[i] + step * (target[i] - initialTuple[i]) for i in range(len(initialTuple))) 74 | self.widget.set(param, newValue) 75 | 76 | # Ensure value is exactly correct at end 77 | for param, target in self.params.items(): 78 | self.widget.set(param, target) 79 | 80 | self.started = False 81 | 82 | 83 | class Translate(AnimationBase): 84 | def __init__(self, widget, timeout, x, y): 85 | super().__init__(widget, timeout, x=x, y=y) 86 | 87 | 88 | class Resize(AnimationBase): 89 | def __init__(self, widget, timeout, width, height): 90 | super().__init__(widget, timeout, width=width, height=height) 91 | 92 | 93 | class Recolour(AnimationBase): 94 | def __init__(self, widget, timeout, colour): 95 | super().__init__(widget, timeout, colour=colour) 96 | 97 | 98 | if __name__ == '__main__': 99 | from pygame_widgets.button import Button 100 | 101 | 102 | def animate(): 103 | resize.start() 104 | translate.start() 105 | 106 | 107 | pygame.init() 108 | win = pygame.display.set_mode((600, 600)) 109 | 110 | button = Button(win, 100, 100, 300, 150, text="Hello", inactiveColour=(0, 200, 0), hoverColour=(0, 200, 0)) 111 | 112 | resize = Resize(button, 3, 200, 200) 113 | translate = Recolour(button, 5, (0, 100, 100)) 114 | button.setOnClick(animate) 115 | 116 | run = True 117 | while run: 118 | events = pygame.event.get() 119 | for event in events: 120 | if event.type == pygame.QUIT: 121 | pygame.quit() 122 | run = False 123 | quit() 124 | 125 | win.fill((255, 255, 255)) 126 | 127 | pygame_widgets.update(events) 128 | pygame.display.update() 129 | -------------------------------------------------------------------------------- /pygame_widgets/popup.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import tkinter as tk 3 | from tkinter import messagebox 4 | from enum import Enum 5 | 6 | import pygame_widgets 7 | from pygame_widgets.widget import WidgetBase 8 | 9 | tk.Tk().wm_withdraw() 10 | 11 | 12 | class PopupType(Enum): 13 | INFO = 0 14 | ERROR = 1 15 | WARNING = 2 16 | QUESTION = 3 17 | OK_CANCEL = 4 18 | YES_NO = 5 19 | YES_NO_CANCEL = 6 20 | RETRY_CANCEL = 7 21 | 22 | 23 | class Popup(WidgetBase): 24 | def __init__(self, win: pygame.Surface, x: int, y: int, width: int, height: int, popupType: PopupType, 25 | title: str, text: str, trigger=lambda *args: None, *buttons, **kwargs): 26 | super().__init__(win, x, y, width, height) 27 | self.popupType = popupType 28 | self.title = title 29 | self.text = text 30 | self.trigger = trigger 31 | self.buttons = buttons 32 | 33 | self.margin = kwargs.get('margin', 20) 34 | 35 | self.titleColour = kwargs.get('titleColour', (0, 0, 0)) 36 | self.titleSize = kwargs.get('titleSize', 40) 37 | self.titleFont = kwargs.get('titleFont', pygame.font.SysFont('calibri', self.titleSize, True)) 38 | self.titleRect = self.alignTitleRect() 39 | 40 | self.textColour = kwargs.get('textColour', (0, 0, 0)) 41 | self.textSize = kwargs.get('textSize', 18) 42 | self.textFont = kwargs.get('textFont', pygame.font.SysFont('calibri', self.textSize)) 43 | self.textRect = self.alignTextRect() 44 | 45 | self.radius = kwargs.get('radius', 0) 46 | 47 | self.colour = kwargs.get('colour', (150, 150, 150)) 48 | self.shadowDistance = kwargs.get('shadowDistance', 0) 49 | self.shadowColour = kwargs.get('shadowColour', (210, 210, 180)) 50 | 51 | self.result = None 52 | 53 | self.hide() 54 | 55 | def alignTitleRect(self): 56 | return pygame.Rect(self._x + self.margin, self._y + self.margin, 57 | self._width - self.margin * 2, self._height // 3 - self.margin * 2) 58 | 59 | def alignTextRect(self): 60 | return pygame.Rect(self._x + self.margin, self._y + self._height // 3, 61 | self._width - self.margin * 2, self._height // 2 - self.margin * 2) 62 | 63 | def listen(self, events): 64 | if self.trigger(): 65 | self.show() 66 | messagebox.showinfo(self.title, self.text) 67 | 68 | def draw(self): 69 | pass 70 | 71 | def show(self): 72 | super().show() 73 | match self.popupType: 74 | case PopupType.INFO: 75 | messagebox.showinfo(self.title, self.text) 76 | case PopupType.ERROR: 77 | messagebox.showerror(self.title, self.text) 78 | case PopupType.WARNING: 79 | messagebox.showwarning(self.title, self.text) 80 | case PopupType.QUESTION: 81 | self.result = messagebox.askquestion(self.title, self.text) 82 | case PopupType.OK_CANCEL: 83 | self.result = messagebox.askokcancel(self.title, self.text) 84 | case PopupType.YES_NO: 85 | self.result = messagebox.askyesno(self.title, self.text) 86 | case PopupType.YES_NO_CANCEL: 87 | self.result = messagebox.askyesnocancel(self.title, self.text) 88 | case PopupType.RETRY_CANCEL: 89 | self.result = messagebox.askretrycancel(self.title, self.text) 90 | 91 | def getResult(self): 92 | return self.result 93 | 94 | 95 | if __name__ == '__main__': 96 | from pygame_widgets.button import Button 97 | 98 | def setButtonColour(): 99 | if popup.getResult(): 100 | button.setInactiveColour('green') 101 | elif popup.getResult() == False: 102 | button.setInactiveColour('red') 103 | 104 | pygame.init() 105 | win = pygame.display.set_mode((600, 600)) 106 | 107 | popup = Popup(win, 100, 100, 400, 400, PopupType.YES_NO, 'Popup', 108 | 'This is the text in the popup. Would you like to continue? The buttons below can be customised.', 109 | radius=20, textSize=20) 110 | 111 | button = Button(win, 100, 100, 400, 400, text='Popup', onClick=popup.show) 112 | 113 | run = True 114 | while run: 115 | events = pygame.event.get() 116 | for event in events: 117 | if event.type == pygame.QUIT: 118 | pygame.quit() 119 | run = False 120 | quit() 121 | 122 | win.fill((255, 255, 255)) 123 | 124 | pygame_widgets.update(events) 125 | pygame.display.update() 126 | setButtonColour() 127 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /docs/widgets/button.md: -------------------------------------------------------------------------------- 1 | # Button 2 | 3 | A button that allows fully customisable text, images, colours and functions. 4 | 5 | ## Example Usage 6 | 7 | Note: See example 8 | 9 | ```Python 10 | import pygame 11 | 12 | import pygame_widgets 13 | from pygame_widgets.button import Button 14 | 15 | # Set up Pygame 16 | pygame.init() 17 | win = pygame.display.set_mode((600, 600)) 18 | 19 | # Creates the button with optional parameters 20 | button = Button( 21 | # Mandatory Parameters 22 | win, # Surface to place button on 23 | 100, # X-coordinate of top left corner 24 | 100, # Y-coordinate of top left corner 25 | 300, # Width 26 | 150, # Height 27 | 28 | # Optional Parameters 29 | text='Hello', # Text to display 30 | fontSize=50, # Size of font 31 | margin=20, # Minimum distance between text/image and edge of button 32 | inactiveColour=(200, 50, 0), # Colour of button when not being interacted with 33 | hoverColour=(150, 0, 0), # Colour of button when being hovered over 34 | pressedColour=(0, 200, 20), # Colour of button when being clicked 35 | radius=20, # Radius of border corners (leave empty for not curved) 36 | onClick=lambda: print('Click') # Function to call when clicked on 37 | ) 38 | 39 | run = True 40 | while run: 41 | events = pygame.event.get() 42 | for event in events: 43 | if event.type == pygame.QUIT: 44 | pygame.quit() 45 | run = False 46 | quit() 47 | 48 | win.fill((255, 255, 255)) 49 | 50 | pygame_widgets.update(events) # Call once every loop to allow widgets to render and listen 51 | pygame.display.update() 52 | ``` 53 | 54 | This button will be placed at (100, 100) with a width of 300 and a height of 150, display the text 'Hello' with font 55 | size 50, leaving a margin of 20 and a radius of 20. When hovered over, the button changes to a darker red. 56 | When clicked, the button will change colour from red to green and 'Click' will be printed to the console. 57 | 58 | ## Optional Parameters 59 | 60 | | Parameter | Description | Type | Default | 61 | |:--------------------:|----------------------------------------------------------------------------------|:----------------:|:---------------:| 62 | | inactiveColour | Default colour when not pressed or hovered over. | (int, int, int) | (150, 150, 150) | 63 | | pressedColour | Colour when pressed. | (int, int, int) | (100, 100, 100) | 64 | | hoverColour | Colour when hovered over. | (int, int, int) | (125, 125, 125) | 65 | | shadowDistance | Distance to projected shadow. Set to 0 if no shadow desired. | int | 0 | 66 | | shadowColour | Colour of shadow | (int, int, int) | (210, 210, 180) | 67 | | onClick | Function to be called when clicked. | function | None | 68 | | onClickParams | Parameters to be fed into onClick function. | (*any) | () | 69 | | onRelease | Function to be called when released. | function | None | 70 | | onReleaseParams | Parameters to be fed into onRelease function. | (*any) | () | 71 | | onHover | Function to be continuously called when the mouse hovers over the button. | function | None | 72 | | onHoverParams | Parameters to be fed into onHover function. | (*any) | () | 73 | | onHoverRelease | Function to be called once when the mouse stops hovering over the button. | function | None | 74 | | onHoverReleaseParams | Parameters to be fed into onHoverRelease function. | (*any) | () | 75 | | textColour | Colour of text. | (int, int, int) | (0, 0, 0) | 76 | | fontSize | Size of text. | int | 20 | 77 | | text | String to be displayed. | str | '' | 78 | | font | Font of text. | pygame.font.Font | Calibri | 79 | | textHAlign | Horizontal alignment of text. Can be 'centre', 'left' or 'right'. | str | 'centre' | 80 | | textVAlign | Vertical alignment of text. Can be 'centre', 'top' or 'bottom'. | str | 'centre' | 81 | | margin | Minimum distance between text / image and edge. | int | 20 | 82 | | image | Image to be displayed. | pygame.Surface | None | 83 | | imageHAlign | Horizontal alignment of image. Can be 'centre', 'left' or 'right'. | str | 'centre' | 84 | | imageVAlign | Vertical alignment of image. Can be 'centre', 'top' or 'bottom'. | str | 'centre' | 85 | | radius | Border radius. Set to half of width for circular button. Set to 0 for no radius. | int | 0 | 86 | -------------------------------------------------------------------------------- /pygame_widgets/slider.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame import gfxdraw 3 | import math 4 | 5 | import pygame_widgets 6 | from pygame_widgets.widget import WidgetBase 7 | from pygame_widgets.mouse import Mouse, MouseState 8 | 9 | 10 | class Slider(WidgetBase): 11 | def __init__(self, win, x, y, width, height, **kwargs): 12 | super().__init__(win, x, y, width, height) 13 | 14 | self.selected = False 15 | self.valueColour = kwargs.get('valueColour', (0, 35, 255)) 16 | 17 | self.min = kwargs.get('min', 0) 18 | self.max = kwargs.get('max', 99) 19 | self.step = kwargs.get('step', 1) 20 | 21 | self.colour = kwargs.get('colour', (200, 200, 200)) 22 | self.handleColour = kwargs.get('handleColour', (0, 0, 0)) 23 | 24 | self.borderThickness = kwargs.get('borderThickness', 3) 25 | self.borderColour = kwargs.get('borderColour', (0, 0, 0)) 26 | 27 | self.value = self.round(kwargs.get('initial', (self.max + self.min) / 2)) 28 | self.value = max(min(self.value, self.max), self.min) 29 | 30 | self.curved = kwargs.get('curved', True) 31 | 32 | self.vertical = kwargs.get('vertical', False) 33 | 34 | self.draggableAnywhere = kwargs.get('draggableAnywhere', True) 35 | 36 | if self.curved: 37 | if self.vertical: 38 | self.radius = self._width // 2 39 | else: 40 | self.radius = self._height // 2 41 | 42 | if self.vertical: 43 | self.handleRadius = kwargs.get('handleRadius', int(self._width / 1.3)) 44 | else: 45 | self.handleRadius = kwargs.get('handleRadius', int(self._height / 1.3)) 46 | 47 | def listen(self, events): 48 | if not self._hidden and not self._disabled: 49 | mouseState = Mouse.getMouseState() 50 | x, y = Mouse.getMousePos() 51 | 52 | if self.contains(x, y): 53 | if mouseState == MouseState.CLICK: 54 | self.selected = True 55 | 56 | if mouseState == MouseState.RELEASE: 57 | self.selected = False 58 | 59 | if self.selected: 60 | if self.vertical: 61 | self.value = self.max - self.round((y - self._y) / self._height * (self.max - self.min)) 62 | self.value = max(min(self.value, self.max), self.min) 63 | else: 64 | self.value = self.round((x - self._x) / self._width * (self.max - self.min) + self.min) 65 | self.value = max(min(self.value, self.max), self.min) 66 | 67 | def draw(self): 68 | if not self._hidden: 69 | pygame.draw.rect(self.win, self.colour, (self._x, self._y, self._width, self._height)) 70 | 71 | if self.vertical: 72 | if self.curved: 73 | pygame.draw.circle(self.win, self.colour, (self._x + self._width // 2, self._y), self.radius) 74 | pygame.draw.circle(self.win, self.colour, (self._x + self._width // 2, self._y + self._height), 75 | self.radius) 76 | circle = (self._x + self._width // 2, 77 | int(self._y + (self.max - self.value) / (self.max - self.min) * self._height)) 78 | 79 | pygame.draw.circle(self.win, self.valueColour, (self._x + self._width // 2, self._y + self._height), 80 | self.radius) 81 | pygame.draw.rect(self.win, self.valueColour, 82 | (self._x, self._y + int(self._height * (1 - self.value/self.max)), self._width, int(self._height * self.value/self.max))) 83 | 84 | else: 85 | if self.curved: 86 | pygame.draw.circle(self.win, self.colour, (self._x, self._y + self._height // 2), self.radius) 87 | pygame.draw.circle(self.win, self.colour, (self._x + self._width, self._y + self._height // 2), 88 | self.radius) 89 | circle = (int(self._x + (self.value - self.min) / (self.max - self.min) * self._width), 90 | self._y + self._height // 2) 91 | pygame.draw.circle(self.win, self.valueColour, (self._x, self._y + self._height // 2), 92 | self.radius) 93 | pygame.draw.rect(self.win, self.valueColour, 94 | (self._x, self._y, int(self._width * self.value/self.max)-self.radius, self._height)) 95 | 96 | gfxdraw.filled_circle(self.win, *circle, self.handleRadius, self.handleColour) 97 | gfxdraw.aacircle(self.win, *circle, self.handleRadius, self.handleColour) 98 | 99 | def contains(self, x, y): 100 | if self.draggableAnywhere: 101 | return pygame.rect.Rect(self._x, self._y, self._width, self._height).collidepoint(x, y) 102 | 103 | else: 104 | if self.vertical: 105 | handleX = self._x + self._width // 2 106 | handleY = int(self._y + (self.max - self.value) / (self.max - self.min) * self._height) 107 | else: 108 | handleX = int(self._x + (self.value - self.min) / (self.max - self.min) * self._width) 109 | handleY = self._y + self._height // 2 110 | 111 | if math.sqrt((handleX - x) ** 2 + (handleY - y) ** 2) <= self.handleRadius: 112 | return True 113 | 114 | return False 115 | 116 | 117 | def round(self, value): 118 | return self.step * round(value / self.step) 119 | 120 | def getValue(self): 121 | return self.value 122 | 123 | def setValue(self, value): 124 | self.value = value 125 | 126 | 127 | if __name__ == '__main__': 128 | from pygame_widgets.textbox import TextBox 129 | 130 | pygame.init() 131 | win = pygame.display.set_mode((1000, 600)) 132 | 133 | slider = Slider(win, 100, 100, 800, 40, min=0, max=99, step=1) 134 | output = TextBox(win, 475, 200, 50, 50, fontSize=30) 135 | 136 | v_slider = Slider(win, 900, 200, 40, 300, min=0, max=99, step=1, vertical=True) 137 | v_output = TextBox(win, 800, 320, 50, 50, fontSize=30) 138 | 139 | output.disable() 140 | v_output.disable() 141 | 142 | run = True 143 | while run: 144 | events = pygame.event.get() 145 | for event in events: 146 | if event.type == pygame.QUIT: 147 | pygame.quit() 148 | run = False 149 | quit() 150 | 151 | win.fill((255, 255, 255)) 152 | 153 | output.setText(slider.getValue()) 154 | v_output.setText(v_slider.getValue()) 155 | 156 | pygame_widgets.update(events) 157 | pygame.display.update() 158 | -------------------------------------------------------------------------------- /pygame_widgets/widget.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | 3 | from collections.abc import MutableSet 4 | from collections import OrderedDict 5 | 6 | from abc import abstractmethod, ABC 7 | 8 | from pygame.event import Event 9 | 10 | from pygame_widgets.mouse import Mouse 11 | 12 | 13 | # Implementation of an insertion-ordered set. Necessary to keep track of the order in which widgets are added. 14 | class OrderedSet(MutableSet): 15 | def __init__(self, values=()): 16 | self._od = OrderedDict().fromkeys(values) 17 | 18 | def __len__(self): 19 | return len(self._od) 20 | 21 | def __iter__(self): 22 | return iter(self._od) 23 | 24 | def __contains__(self, value): 25 | return value in self._od 26 | 27 | def add(self, value): 28 | self._od[value] = None 29 | 30 | def discard(self, value): 31 | self._od.pop(value, None) 32 | 33 | def move_to_end(self, value): 34 | self._od.move_to_end(value) 35 | 36 | def move_to_start(self, value): 37 | self._od.move_to_end(value, last=False) 38 | 39 | 40 | 41 | class OrderedWeakset(weakref.WeakSet): 42 | _remove = ... # Getting defined after the super().__init__() call 43 | 44 | def __init__(self, values=()): 45 | super(OrderedWeakset, self).__init__() 46 | 47 | self.data = OrderedSet() 48 | for elem in values: 49 | self.add(elem) 50 | 51 | def move_to_end(self, item): 52 | self.data.move_to_end(weakref.ref(item, self._remove)) 53 | 54 | def move_to_start(self, item): 55 | self.data.move_to_start(weakref.ref(item, self._remove)) 56 | 57 | 58 | 59 | class WidgetBase(ABC): 60 | def __init__(self, win, x, y, width, height, isSubWidget=False): 61 | """ Base for all widgets 62 | 63 | :param win: Surface on which to draw 64 | :type win: pygame.Surface 65 | :param x: X-coordinate of top left 66 | :type x: int 67 | :param y: Y-coordinate of top left 68 | :type y: int 69 | :param width: Width of button 70 | :type width: int 71 | :param height: Height of button 72 | :type height: int 73 | """ 74 | self.win = win 75 | self._x = x 76 | self._y = y 77 | self._width = width 78 | self._height = height 79 | self._isSubWidget = isSubWidget 80 | 81 | self._hidden = False 82 | self._disabled = False 83 | 84 | if not isSubWidget: 85 | WidgetHandler.addWidget(self) 86 | 87 | @abstractmethod 88 | def listen(self, events): 89 | pass 90 | 91 | @abstractmethod 92 | def draw(self): 93 | pass 94 | 95 | def __repr__(self): 96 | return f'{type(self).__name__}(x = {self._x}, y = {self._y}, width = {self._width}, height = {self._height})' 97 | 98 | def contains(self, x, y): 99 | return (self._x < x - self.win.get_abs_offset()[0] < self._x + self._width) and \ 100 | (self._y < y - self.win.get_abs_offset()[1] < self._y + self._height) 101 | 102 | def hide(self): 103 | self._hidden = True 104 | if not self._isSubWidget: 105 | WidgetHandler.moveToBottom(self) 106 | 107 | def show(self): 108 | self._hidden = False 109 | if not self._isSubWidget: 110 | WidgetHandler.moveToTop(self) 111 | 112 | def disable(self): 113 | self._disabled = True 114 | 115 | def enable(self): 116 | self._disabled = False 117 | 118 | def isSubWidget(self): 119 | return self._isSubWidget 120 | 121 | def moveToTop(self): 122 | WidgetHandler.moveToTop(self) 123 | 124 | def moveToBottom(self): 125 | WidgetHandler.moveToBottom(self) 126 | 127 | def moveX(self, x): 128 | self._x += x 129 | 130 | def moveY(self, y): 131 | self._y += y 132 | 133 | def get(self, attr): 134 | """Default setter for any attributes. Call super if overriding 135 | 136 | :param attr: Attribute to get 137 | :return: Value of the attribute 138 | """ 139 | if attr == 'x': 140 | return self._x 141 | 142 | if attr == 'y': 143 | return self._y 144 | 145 | if attr == 'width': 146 | return self._width 147 | 148 | if attr == 'height': 149 | return self._height 150 | 151 | def getX(self): 152 | return self._x 153 | 154 | def getY(self): 155 | return self._y 156 | 157 | def getWidth(self): 158 | return self._width 159 | 160 | def getHeight(self): 161 | return self._height 162 | 163 | def isVisible(self): 164 | return not self._hidden 165 | 166 | def isEnabled(self): 167 | return not self._disabled 168 | 169 | def set(self, attr, value): 170 | """Default setter for any attributes. Call super if overriding 171 | 172 | :param attr: Attribute to set 173 | :param value: Value to set 174 | """ 175 | if attr == 'x': 176 | self._x = value 177 | 178 | if attr == 'y': 179 | self._y = value 180 | 181 | if attr == 'width': 182 | self._width = value 183 | 184 | if attr == 'height': 185 | self._height = value 186 | 187 | def setX(self, x): 188 | self._x = x 189 | 190 | def setY(self, y): 191 | self._y = y 192 | 193 | def setWidth(self, width): 194 | self._width = width 195 | 196 | def setHeight(self, height): 197 | self._height = height 198 | 199 | def setIsSubWidget(self, isSubWidget): 200 | self._isSubWidget = isSubWidget 201 | if isSubWidget: 202 | WidgetHandler.removeWidget(self) 203 | else: 204 | WidgetHandler.addWidget(self) 205 | 206 | 207 | class WidgetHandler: 208 | _widgets: OrderedWeakset[weakref.ref] = OrderedWeakset() 209 | 210 | @staticmethod 211 | def main(events: [Event]) -> None: 212 | blocked = False 213 | 214 | # Conversion is used to prevent errors when widgets are added/removed during iteration a.k.a safe iteration 215 | widgets = list(WidgetHandler._widgets) 216 | 217 | for widget in widgets[::-1]: 218 | if not blocked or not widget.contains(*Mouse.getMousePos()): 219 | widget.listen(events) 220 | 221 | # Ensure widgets covered by others are not affected (widgets created later) 222 | if widget.contains(*Mouse.getMousePos()): # TODO: Unless 'transparent' 223 | blocked = True 224 | 225 | for widget in widgets: 226 | widget.draw() 227 | 228 | @staticmethod 229 | def addWidget(widget: WidgetBase) -> None: 230 | if widget not in WidgetHandler._widgets: 231 | WidgetHandler._widgets.add(widget) 232 | WidgetHandler.moveToTop(widget) 233 | 234 | @staticmethod 235 | def removeWidget(widget: WidgetBase) -> None: 236 | try: 237 | WidgetHandler._widgets.remove(widget) 238 | except ValueError: 239 | print(f'Error: Tried to remove {widget} when {widget} not in WidgetHandler.') 240 | 241 | @staticmethod 242 | def moveToTop(widget: WidgetBase): 243 | try: 244 | WidgetHandler._widgets.move_to_end(widget) 245 | except KeyError: 246 | print(f'Error: Tried to move {widget} to top when {widget} not in WidgetHandler.') 247 | 248 | @staticmethod 249 | def moveToBottom(widget: WidgetBase): 250 | try: 251 | WidgetHandler._widgets.move_to_start(widget) 252 | except KeyError: 253 | print(f'Error: Tried to move {widget} to bottom when {widget} not in WidgetHandler.') 254 | 255 | @staticmethod 256 | def getWidgets() -> [WidgetBase]: 257 | return WidgetHandler._widgets 258 | -------------------------------------------------------------------------------- /site/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | My Docs 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 81 | 82 |
83 |
84 | 85 |
86 |
87 |

404

88 |

Page not found

89 |
90 |
91 | 92 | 93 |
94 |
95 | 96 | 100 | 104 | 105 | 106 | 107 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /site/css/base.css: -------------------------------------------------------------------------------- 1 | html { 2 | /* csslint ignore:start */ 3 | /* The nav header is 3.5rem high, plus 20px for the margin-top of the 4 | main container. */ 5 | scroll-padding-top: calc(3.5rem + 20px); 6 | /* csslint ignore:end */ 7 | } 8 | 9 | /* Replacement for `body { background-attachment: fixed; }`, which has 10 | performance issues when scrolling on large displays. See #1394. */ 11 | body::before { 12 | content: ' '; 13 | position: fixed; 14 | width: 100%; 15 | height: 100%; 16 | top: 0; 17 | left: 0; 18 | background-color: #f8f8f8; 19 | background: url(../img/grid.png) repeat-x; 20 | will-change: transform; 21 | z-index: -1; 22 | } 23 | 24 | body > .container { 25 | margin-top: 20px; 26 | min-height: 400px; 27 | } 28 | 29 | .navbar.fixed-top { /* csslint allow: adjoining-classes */ 30 | /* csslint ignore:start */ 31 | position: -webkit-sticky; 32 | position: sticky; 33 | /* csslint ignore:end */ 34 | } 35 | 36 | .source-links { 37 | float: right; 38 | } 39 | 40 | .col-md-9 img { 41 | max-width: 100%; 42 | display: inline-block; 43 | padding: 4px; 44 | line-height: 1.428571429; 45 | background-color: #fff; 46 | border: 1px solid #ddd; 47 | border-radius: 4px; 48 | margin: 20px auto 30px auto; 49 | } 50 | 51 | h1 { 52 | color: #444; 53 | font-weight: 400; 54 | font-size: 42px; 55 | } 56 | 57 | h2, h3, h4, h5, h6 { 58 | color: #444; 59 | font-weight: 300; 60 | } 61 | 62 | hr { 63 | border-top: 1px solid #aaa; 64 | } 65 | 66 | pre, .rst-content tt { 67 | max-width: 100%; 68 | background: #fff; 69 | border: solid 1px #e1e4e5; 70 | color: #333; 71 | overflow-x: auto; 72 | } 73 | 74 | code.code-large, .rst-content tt.code-large { 75 | font-size: 90%; 76 | } 77 | 78 | code { 79 | padding: 2px 5px; 80 | background: #fff; 81 | border: solid 1px #e1e4e5; 82 | color: #333; 83 | white-space: pre-wrap; 84 | word-wrap: break-word; 85 | } 86 | 87 | pre code { 88 | display: block; 89 | background: transparent; 90 | border: none; 91 | white-space: pre; 92 | word-wrap: normal; 93 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 94 | font-size: 12px; 95 | } 96 | 97 | kbd { 98 | padding: 2px 4px; 99 | font-size: 90%; 100 | color: #fff; 101 | background-color: #333; 102 | border-radius: 3px; 103 | -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); 104 | box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); 105 | } 106 | 107 | a code { 108 | color: #2FA4E7; 109 | } 110 | 111 | a:hover code, a:focus code { 112 | color: #157AB5; 113 | } 114 | 115 | footer { 116 | margin-top: 30px; 117 | margin-bottom: 10px; 118 | text-align: center; 119 | font-weight: 200; 120 | } 121 | 122 | .modal-dialog { 123 | margin-top: 60px; 124 | } 125 | 126 | /* 127 | * Side navigation 128 | * 129 | * Scrollspy and affixed enhanced navigation to highlight sections and secondary 130 | * sections of docs content. 131 | */ 132 | 133 | .bs-sidebar.affix { /* csslint allow: adjoining-classes */ 134 | /* csslint ignore:start */ 135 | position: -webkit-sticky; 136 | position: sticky; 137 | /* csslint ignore:end */ 138 | /* The nav header is 3.5rem high, plus 20px for the margin-top of the 139 | main container. */ 140 | top: calc(3.5rem + 20px); 141 | } 142 | 143 | .bs-sidebar.card { /* csslint allow: adjoining-classes */ 144 | padding: 0; 145 | max-height: 90%; 146 | overflow-y: auto; 147 | } 148 | 149 | /* Toggle (vertically flip) sidebar collapse icon */ 150 | .bs-sidebar .navbar-toggler span { 151 | -moz-transform: scale(1, -1); 152 | -webkit-transform: scale(1, -1); 153 | -o-transform: scale(1, -1); 154 | -ms-transform: scale(1, -1); 155 | transform: scale(1, -1); 156 | } 157 | 158 | .bs-sidebar .navbar-toggler.collapsed span { /* csslint allow: adjoining-classes */ 159 | -moz-transform: scale(1, 1); 160 | -webkit-transform: scale(1, 1); 161 | -o-transform: scale(1, 1); 162 | -ms-transform: scale(1, 1); 163 | transform: scale(1, 1); 164 | } 165 | 166 | /* First level of nav */ 167 | .bs-sidebar > .navbar-collapse > .nav { 168 | padding-top: 10px; 169 | padding-bottom: 10px; 170 | border-radius: 5px; 171 | width: 100%; 172 | } 173 | 174 | /* All levels of nav */ 175 | .bs-sidebar .nav > li > a { 176 | display: block; 177 | padding: 5px 20px; 178 | z-index: 1; 179 | } 180 | .bs-sidebar .nav > li > a:hover, 181 | .bs-sidebar .nav > li > a:focus { 182 | text-decoration: none; 183 | border-right: 1px solid; 184 | } 185 | .bs-sidebar .nav > li > a.active, 186 | .bs-sidebar .nav > li > a.active:hover, 187 | .bs-sidebar .nav > li > a.active:focus { 188 | font-weight: bold; 189 | background-color: transparent; 190 | border-right: 1px solid; 191 | } 192 | 193 | .bs-sidebar .nav .nav .nav { 194 | margin-left: 1em; 195 | } 196 | 197 | .bs-sidebar .nav > li > a { 198 | font-weight: bold; 199 | } 200 | 201 | .bs-sidebar .nav .nav > li > a { 202 | font-weight: normal; 203 | } 204 | 205 | .headerlink { 206 | font-family: FontAwesome; 207 | font-size: 14px; 208 | display: none; 209 | padding-left: .5em; 210 | } 211 | 212 | h1:hover .headerlink, h2:hover .headerlink, h3:hover .headerlink, h4:hover .headerlink, h5:hover .headerlink, h6:hover .headerlink{ 213 | display:inline-block; 214 | } 215 | 216 | 217 | 218 | .admonition { 219 | padding: 15px; 220 | margin-bottom: 20px; 221 | border: 1px solid transparent; 222 | border-radius: 4px; 223 | text-align: left; 224 | } 225 | 226 | .admonition.note { /* csslint allow: adjoining-classes */ 227 | color: #3a87ad; 228 | background-color: #d9edf7; 229 | border-color: #bce8f1; 230 | } 231 | 232 | .admonition.warning { /* csslint allow: adjoining-classes */ 233 | color: #c09853; 234 | background-color: #fcf8e3; 235 | border-color: #fbeed5; 236 | } 237 | 238 | .admonition.danger { /* csslint allow: adjoining-classes */ 239 | color: #b94a48; 240 | background-color: #f2dede; 241 | border-color: #eed3d7; 242 | } 243 | 244 | .admonition-title { 245 | font-weight: bold; 246 | text-align: left; 247 | } 248 | 249 | @media (max-width: 991.98px) { 250 | .navbar-collapse.show { /* csslint allow: adjoining-classes */ 251 | overflow-y: auto; 252 | max-height: calc(100vh - 3.5rem); 253 | } 254 | } 255 | 256 | .dropdown-item.open { /* csslint allow: adjoining-classes */ 257 | color: #fff; 258 | background-color: #2FA4E7; 259 | } 260 | 261 | .dropdown-submenu > .dropdown-menu { 262 | margin: 0 0 0 1.5rem; 263 | padding: 0; 264 | border-width: 0; 265 | } 266 | 267 | .dropdown-submenu > a::after { 268 | display: block; 269 | content: " "; 270 | float: right; 271 | width: 0; 272 | height: 0; 273 | border-color: transparent; 274 | border-style: solid; 275 | border-width: 5px 0 5px 5px; 276 | border-left-color: #ccc; 277 | margin-top: 5px; 278 | margin-right: -10px; 279 | } 280 | 281 | .dropdown-submenu:hover > a::after { 282 | border-left-color: #fff; 283 | } 284 | 285 | @media (min-width: 992px) { 286 | .dropdown-menu { 287 | overflow-y: auto; 288 | max-height: calc(100vh - 3.5rem); 289 | } 290 | 291 | .dropdown-submenu { 292 | position: relative; 293 | } 294 | 295 | .dropdown-submenu > .dropdown-menu { 296 | /* csslint ignore:start */ 297 | position: fixed !important; 298 | /* csslint ignore:end */ 299 | margin-top: -9px; 300 | margin-left: -2px; 301 | border-width: 1px; 302 | padding: 0.5rem 0; 303 | } 304 | 305 | .dropdown-submenu.pull-left { /* csslint allow: adjoining-classes */ 306 | float: none; 307 | } 308 | 309 | .dropdown-submenu.pull-left > .dropdown-menu { /* csslint allow: adjoining-classes */ 310 | left: -100%; 311 | margin-left: 10px; 312 | } 313 | } 314 | 315 | @media print { 316 | /* Remove sidebar when print */ 317 | .col-md-3 { display: none; } 318 | } 319 | -------------------------------------------------------------------------------- /pygame_widgets/combobox.py: -------------------------------------------------------------------------------- 1 | import pygame_widgets 2 | from pygame_widgets import Mouse 3 | from pygame_widgets.widget import WidgetBase 4 | from pygame_widgets.textbox import TextBox 5 | from pygame_widgets.dropdown import Dropdown, DropdownChoice 6 | 7 | 8 | class ComboBox(Dropdown): 9 | def __init__( 10 | self, win, x, y, width, height, 11 | choices, 12 | textboxKwargs=None, 13 | **kwargs 14 | ): 15 | """Initialise a customisable combo box for Pygame. Acts like a searchable dropdown. 16 | 17 | :param win: Surface on which to draw 18 | :type win: pygame.Surface 19 | :param x: X-coordinate of top left 20 | :type x: int 21 | :param y: Y-coordinate of top left 22 | :type y: int 23 | :param width: Width of button 24 | :type width: int 25 | :param height: Height of button 26 | :type height: int 27 | :param choices: Possible search values 28 | :type choices: list(str) 29 | :param textboxKwargs: Kwargs to be passed to the search box 30 | :type textboxKwargs: dict(str: Any) 31 | :param maxResults: The maximum number of results to display 32 | :type maxResults: int 33 | :param kwargs: Optional parameters 34 | """ 35 | WidgetBase.__init__(self, win, x, y, width, height) 36 | 37 | if textboxKwargs is None: 38 | textboxKwargs = {} 39 | self._dropped = False 40 | 41 | self.choices = choices 42 | self.suggestions = choices # Stores the current suggestions 43 | 44 | self._searchAlgo = kwargs.get('searchAlgo', self._defaultSearch) 45 | 46 | # Adds params that are not specified in text box 47 | for key, value in kwargs.items(): 48 | if key not in textboxKwargs: 49 | textboxKwargs[key] = value 50 | 51 | self.textBar = TextBox( 52 | win, x, y, width, height, isSubWidget=True, 53 | onTextChanged=self.updateSearchResults, 54 | **textboxKwargs 55 | ) 56 | self.__main = self.textBar 57 | # Set the number of choices if not given 58 | self.maxResults = kwargs.get('maxResults', len(choices)) 59 | 60 | self.createDropdownChoices(x, y, width, height, **kwargs) 61 | 62 | self.getText = self.textBar.getText 63 | 64 | # Function 65 | self.onSelected = kwargs.get('onSelected', lambda *args: None) 66 | self.onSelectedParams = kwargs.get('onSelectedParams', ()) 67 | self.onStartSearch = kwargs.get('onStartSearch', lambda *args: None) 68 | self.onStartSearchParams = kwargs.get('onStartSearchParams', ()) 69 | self.onStopSearch = kwargs.get('onStopSearch', lambda *args: None) 70 | self.onStopSearchParams = kwargs.get('onStopSearchParams', ()) 71 | 72 | def createDropdownChoices(self, x, y, width, height, **kwargs): 73 | """Create the widgets for the choices.""" 74 | # We create the DropdownChoice(s) 75 | direction = kwargs.get('direction', 'down') 76 | self.__choices = [] 77 | for i, text in enumerate(self.choices): 78 | if i == self.maxResults: 79 | return 80 | 81 | if direction == 'down': 82 | x = 0 83 | y = (i + 1) * height 84 | 85 | elif direction == 'up': 86 | x = 0 87 | y = -(i + 1) * height 88 | 89 | elif direction == 'right': 90 | x = (i + 1) * width 91 | y = 0 92 | 93 | elif direction == 'left': 94 | x = -(i + 1) * width 95 | y = 0 96 | 97 | self.__choices.append( 98 | DropdownChoice( 99 | self.win, x, y, width, height, 100 | text=text, dropdown=self, value=i, 101 | last=(i == self.maxResults - 1), 102 | **kwargs, 103 | ) 104 | ) 105 | 106 | def listen(self, events): 107 | """Wait for input. 108 | 109 | :param events: Use pygame.event.get() 110 | :type events: list of pygame.event.Event 111 | """ 112 | if not self._hidden and not self._disabled: 113 | # Keeps state of selected 114 | previouslySelected = self.textBar.selected 115 | self.textBar.listen(events) 116 | 117 | if self._dropped: 118 | for dropdownChoice in self.__choices: 119 | dropdownChoice.listen(events) 120 | if dropdownChoice.clicked: 121 | # The choice was clicked by user 122 | self.textBar.setText(dropdownChoice.text) 123 | self.onSelected(*self.onSelectedParams) 124 | 125 | # Whether the search is started or stopped 126 | if previouslySelected and not self.textBar.selected: 127 | self.onStopSearch(*self.onStopSearchParams) 128 | self._dropped = False 129 | 130 | if not previouslySelected and self.textBar.selected: 131 | self.onStartSearch(*self.onStartSearchParams) 132 | self.updateSearchResults() 133 | 134 | def draw(self): 135 | """Draw the widget.""" 136 | if not self._hidden: 137 | self.textBar.draw() 138 | if self._dropped: 139 | # Find how many choices should be shown 140 | numberVisible = min(len(self.suggestions), self.maxResults) 141 | for i, dropdownChoice in enumerate(self.__choices): 142 | # Define if the the dropdown should be shown 143 | if i < numberVisible: 144 | dropdownChoice.show() 145 | self.moveToTop() 146 | # Choose the text to show 147 | dropdownChoice.text = self.suggestions[i] 148 | else: 149 | dropdownChoice.hide() 150 | dropdownChoice.draw() 151 | 152 | def contains(self, x, y): 153 | return super(Dropdown, self).contains(x, y) or \ 154 | (any([c.contains(x, y) for c in self.__choices]) and self._dropped) 155 | 156 | def updateSearchResults(self): 157 | """Update the suggested results based on selected text. 158 | 159 | Uses a 'contains' research. Could be improved by other 160 | search algorithms. 161 | """ 162 | text = self.textBar.getText() 163 | 164 | if text != '': 165 | # Finds all the texts that start with the same text 166 | self.suggestions = self._searchAlgo(text, self.choices) 167 | self._dropped = True 168 | else: 169 | self._dropped = False 170 | 171 | def _searchAlgo(self, text, choices): 172 | """Return the suggestions of text in choices.""" 173 | raise NotImplementedError('A search method must override this.') 174 | 175 | @staticmethod 176 | def _defaultSearch(text, choices): 177 | """Return the suggestions of text in choices.""" 178 | 179 | # First add the ones that perfectly match case 180 | suggestions = [ 181 | choice for choice in choices 182 | if choice.startswith(text) 183 | ] 184 | # Then add the ones that include text 185 | suggestions += [ 186 | choice for choice in choices 187 | if text in choice and choice not in suggestions 188 | ] 189 | return suggestions 190 | 191 | 192 | if __name__ == '__main__': 193 | import pygame 194 | from pygame_widgets.button import Button 195 | 196 | pygame.init() 197 | win = pygame.display.set_mode((600, 600)) 198 | 199 | comboBox = ComboBox( 200 | win, 120, 10, 250, 50, name='Select Colour', 201 | choices=pygame.colordict.THECOLORS.keys(), 202 | maxResults=4, 203 | font=pygame.font.SysFont('calibri', 30), 204 | borderRadius=3, colour=(0, 200, 50), direction='down', 205 | textHAlign='left' 206 | ) 207 | 208 | 209 | def output(): 210 | comboBox.textBar.colour = comboBox.getText() 211 | 212 | 213 | button = Button( 214 | win, 10, 10, 100, 50, text='Set Colour', fontSize=30, 215 | margin=15, inactiveColour=(200, 0, 100), pressedColour=(0, 255, 0), 216 | radius=5, onClick=output, font=pygame.font.SysFont('calibri', 18), 217 | textVAlign='bottom' 218 | ) 219 | 220 | run = True 221 | while run: 222 | events = pygame.event.get() 223 | for event in events: 224 | if event.type == pygame.QUIT: 225 | pygame.quit() 226 | run = False 227 | quit() 228 | 229 | win.fill((255, 255, 255)) 230 | 231 | pygame_widgets.update(events) 232 | pygame.display.update() 233 | -------------------------------------------------------------------------------- /site/js/base.js: -------------------------------------------------------------------------------- 1 | function getSearchTerm() { 2 | var sPageURL = window.location.search.substring(1); 3 | var sURLVariables = sPageURL.split('&'); 4 | for (var i = 0; i < sURLVariables.length; i++) { 5 | var sParameterName = sURLVariables[i].split('='); 6 | if (sParameterName[0] == 'q') { 7 | return sParameterName[1]; 8 | } 9 | } 10 | } 11 | 12 | function applyTopPadding() { 13 | // Update various absolute positions to match where the main container 14 | // starts. This is necessary for handling multi-line nav headers, since 15 | // that pushes the main container down. 16 | var offset = $('body > .container').offset(); 17 | $('html').css('scroll-padding-top', offset.top + 'px'); 18 | $('.bs-sidebar.affix').css('top', offset.top + 'px'); 19 | } 20 | 21 | $(document).ready(function() { 22 | 23 | applyTopPadding(); 24 | 25 | var search_term = getSearchTerm(), 26 | $search_modal = $('#mkdocs_search_modal'), 27 | $keyboard_modal = $('#mkdocs_keyboard_modal'); 28 | 29 | if (search_term) { 30 | $search_modal.modal(); 31 | } 32 | 33 | // make sure search input gets autofocus everytime modal opens. 34 | $search_modal.on('shown.bs.modal', function() { 35 | $search_modal.find('#mkdocs-search-query').focus(); 36 | }); 37 | 38 | // Close search modal when result is selected 39 | // The links get added later so listen to parent 40 | $('#mkdocs-search-results').click(function(e) { 41 | if ($(e.target).is('a')) { 42 | $search_modal.modal('hide'); 43 | } 44 | }); 45 | 46 | // Populate keyboard modal with proper Keys 47 | $keyboard_modal.find('.help.shortcut kbd')[0].innerHTML = keyCodes[shortcuts.help]; 48 | $keyboard_modal.find('.prev.shortcut kbd')[0].innerHTML = keyCodes[shortcuts.previous]; 49 | $keyboard_modal.find('.next.shortcut kbd')[0].innerHTML = keyCodes[shortcuts.next]; 50 | $keyboard_modal.find('.search.shortcut kbd')[0].innerHTML = keyCodes[shortcuts.search]; 51 | 52 | // Keyboard navigation 53 | document.addEventListener("keydown", function(e) { 54 | if ($(e.target).is(':input')) return true; 55 | var key = e.which || e.keyCode || window.event && window.event.keyCode; 56 | var page; 57 | switch (key) { 58 | case shortcuts.next: 59 | page = $('.navbar a[rel="next"]:first').prop('href'); 60 | break; 61 | case shortcuts.previous: 62 | page = $('.navbar a[rel="prev"]:first').prop('href'); 63 | break; 64 | case shortcuts.search: 65 | e.preventDefault(); 66 | $keyboard_modal.modal('hide'); 67 | $search_modal.modal('show'); 68 | $search_modal.find('#mkdocs-search-query').focus(); 69 | break; 70 | case shortcuts.help: 71 | $search_modal.modal('hide'); 72 | $keyboard_modal.modal('show'); 73 | break; 74 | default: break; 75 | } 76 | if (page) { 77 | $keyboard_modal.modal('hide'); 78 | window.location.href = page; 79 | } 80 | }); 81 | 82 | $('table').addClass('table table-striped table-hover'); 83 | 84 | // Improve the scrollspy behaviour when users click on a TOC item. 85 | $(".bs-sidenav a").on("click", function() { 86 | var clicked = this; 87 | setTimeout(function() { 88 | var active = $('.nav li.active a'); 89 | active = active[active.length - 1]; 90 | if (clicked !== active) { 91 | $(active).parent().removeClass("active"); 92 | $(clicked).parent().addClass("active"); 93 | } 94 | }, 50); 95 | }); 96 | 97 | function showInnerDropdown(item) { 98 | var popup = $(item).next('.dropdown-menu'); 99 | popup.addClass('show'); 100 | $(item).addClass('open'); 101 | 102 | // First, close any sibling dropdowns. 103 | var container = $(item).parent().parent(); 104 | container.find('> .dropdown-submenu > a').each(function(i, el) { 105 | if (el !== item) { 106 | hideInnerDropdown(el); 107 | } 108 | }); 109 | 110 | var popupMargin = 10; 111 | var maxBottom = $(window).height() - popupMargin; 112 | var bounds = item.getBoundingClientRect(); 113 | 114 | popup.css('left', bounds.right + 'px'); 115 | if (bounds.top + popup.height() > maxBottom && 116 | bounds.top > $(window).height() / 2) { 117 | popup.css({ 118 | 'top': (bounds.bottom - popup.height()) + 'px', 119 | 'max-height': (bounds.bottom - popupMargin) + 'px', 120 | }); 121 | } else { 122 | popup.css({ 123 | 'top': bounds.top + 'px', 124 | 'max-height': (maxBottom - bounds.top) + 'px', 125 | }); 126 | } 127 | } 128 | 129 | function hideInnerDropdown(item) { 130 | var popup = $(item).next('.dropdown-menu'); 131 | popup.removeClass('show'); 132 | $(item).removeClass('open'); 133 | 134 | popup.scrollTop(0); 135 | popup.find('.dropdown-menu').scrollTop(0).removeClass('show'); 136 | popup.find('.dropdown-submenu > a').removeClass('open'); 137 | } 138 | 139 | $('.dropdown-submenu > a').on('click', function(e) { 140 | if ($(this).next('.dropdown-menu').hasClass('show')) { 141 | hideInnerDropdown(this); 142 | } else { 143 | showInnerDropdown(this); 144 | } 145 | 146 | e.stopPropagation(); 147 | e.preventDefault(); 148 | }); 149 | 150 | $('.dropdown-menu').parent().on('hide.bs.dropdown', function(e) { 151 | $(this).find('.dropdown-menu').scrollTop(0); 152 | $(this).find('.dropdown-submenu > a').removeClass('open'); 153 | $(this).find('.dropdown-menu .dropdown-menu').removeClass('show'); 154 | }); 155 | }); 156 | 157 | $(window).on('resize', applyTopPadding); 158 | 159 | $('body').scrollspy({ 160 | target: '.bs-sidebar', 161 | offset: 100 162 | }); 163 | 164 | /* Prevent disabled links from causing a page reload */ 165 | $("li.disabled a").click(function() { 166 | event.preventDefault(); 167 | }); 168 | 169 | // See https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes 170 | // We only list common keys below. Obscure keys are omitted and their use is discouraged. 171 | var keyCodes = { 172 | 8: 'backspace', 173 | 9: 'tab', 174 | 13: 'enter', 175 | 16: 'shift', 176 | 17: 'ctrl', 177 | 18: 'alt', 178 | 19: 'pause/break', 179 | 20: 'caps lock', 180 | 27: 'escape', 181 | 32: 'spacebar', 182 | 33: 'page up', 183 | 34: 'page down', 184 | 35: 'end', 185 | 36: 'home', 186 | 37: '←', 187 | 38: '↑', 188 | 39: '→', 189 | 40: '↓', 190 | 45: 'insert', 191 | 46: 'delete', 192 | 48: '0', 193 | 49: '1', 194 | 50: '2', 195 | 51: '3', 196 | 52: '4', 197 | 53: '5', 198 | 54: '6', 199 | 55: '7', 200 | 56: '8', 201 | 57: '9', 202 | 65: 'a', 203 | 66: 'b', 204 | 67: 'c', 205 | 68: 'd', 206 | 69: 'e', 207 | 70: 'f', 208 | 71: 'g', 209 | 72: 'h', 210 | 73: 'i', 211 | 74: 'j', 212 | 75: 'k', 213 | 76: 'l', 214 | 77: 'm', 215 | 78: 'n', 216 | 79: 'o', 217 | 80: 'p', 218 | 81: 'q', 219 | 82: 'r', 220 | 83: 's', 221 | 84: 't', 222 | 85: 'u', 223 | 86: 'v', 224 | 87: 'w', 225 | 88: 'x', 226 | 89: 'y', 227 | 90: 'z', 228 | 91: 'Left Windows Key / Left ⌘', 229 | 92: 'Right Windows Key', 230 | 93: 'Windows Menu / Right ⌘', 231 | 96: 'numpad 0', 232 | 97: 'numpad 1', 233 | 98: 'numpad 2', 234 | 99: 'numpad 3', 235 | 100: 'numpad 4', 236 | 101: 'numpad 5', 237 | 102: 'numpad 6', 238 | 103: 'numpad 7', 239 | 104: 'numpad 8', 240 | 105: 'numpad 9', 241 | 106: 'multiply', 242 | 107: 'add', 243 | 109: 'subtract', 244 | 110: 'decimal point', 245 | 111: 'divide', 246 | 112: 'f1', 247 | 113: 'f2', 248 | 114: 'f3', 249 | 115: 'f4', 250 | 116: 'f5', 251 | 117: 'f6', 252 | 118: 'f7', 253 | 119: 'f8', 254 | 120: 'f9', 255 | 121: 'f10', 256 | 122: 'f11', 257 | 123: 'f12', 258 | 124: 'f13', 259 | 125: 'f14', 260 | 126: 'f15', 261 | 127: 'f16', 262 | 128: 'f17', 263 | 129: 'f18', 264 | 130: 'f19', 265 | 131: 'f20', 266 | 132: 'f21', 267 | 133: 'f22', 268 | 134: 'f23', 269 | 135: 'f24', 270 | 144: 'num lock', 271 | 145: 'scroll lock', 272 | 186: ';', 273 | 187: '=', 274 | 188: ',', 275 | 189: '‐', 276 | 190: '.', 277 | 191: '?', 278 | 192: '`', 279 | 219: '[', 280 | 220: '\', 281 | 221: ']', 282 | 222: ''', 283 | }; 284 | -------------------------------------------------------------------------------- /site/common/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Common - My Docs 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 91 | 92 |
93 |
94 |
116 |
117 | 118 |

Common

119 |

Functionality provided and required for all widgets.

120 |

Mandatory Parameters

121 |

Note: Mandatory parameters must be supplied in order.

122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 |
ParameterDescriptionType
winSurface to be displayed on.pygame.Surface
xX-coordinate of top left.int
yY-coordinate of top left.int
widthWidth of button in pixels.int
heightHeight of button in pixels.int
158 |
159 |
160 | 161 | 165 | 169 | 170 | 171 | 172 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /site/animations/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Animations - My Docs 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 91 | 92 |
93 |
94 |
116 |
117 | 118 |

Animations

119 |

Create an animation by using the default Translate or Resize, inheriting from AnimationBase, or using AnimationBase 120 | directly.

121 |

Example Usage

122 |
import pygame
123 | from pygame_widgets import Button, Resize
124 | 
125 | pygame.init()
126 | win = pygame.display.set_mode((600, 600))
127 | 
128 | button = Button(win, 100, 100, 300, 150)
129 | 
130 | animation = Resize(button, 3, 200, 200)
131 | animation.start()
132 | 
133 | run = True
134 | while run:
135 |     events = pygame.event.get()
136 |     for event in events:
137 |         if event.type == pygame.QUIT:
138 |             pygame.quit()
139 |             run = False
140 |             quit()
141 | 
142 |     win.fill((255, 255, 255))
143 | 
144 |     button.listen(events)
145 |     button.draw()
146 | 
147 |     pygame.display.update()
148 | 
149 |

Over 3 seconds, the width of the button was changed from 300 to 200 and its height from 150 to 200. Since it is 150 | performed on a separate thread, the button is still able to function during the animation.

151 |
152 |
153 | 154 | 158 | 162 | 163 | 164 | 165 | 225 | 226 | 227 | 228 | -------------------------------------------------------------------------------- /site/toggle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Toggle - My Docs 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 91 | 92 |
93 |
94 |
120 |
121 | 122 |

Toggle

123 |

Allows switching between true and false options

124 |

Example Usage

125 |
import pygame
126 | from pygame_widgets import Toggle
127 | 
128 | pygame.init()
129 | win = pygame.display.set_mode((1000, 600))
130 | 
131 | toggle = Toggle(win, 100, 100, 100, 40)
132 | 
133 | run = True
134 | while run:
135 |     events = pygame.event.get()
136 |     for event in events:
137 |         if event.type == pygame.QUIT:
138 |             pygame.quit()
139 |             run = False
140 |             quit()
141 | 
142 |     win.fill((255, 255, 255))
143 | 
144 |     toggle.listen(events)
145 |     toggle.draw()
146 | 
147 |     pygame.display.update()
148 | 
149 |

Optional Parameters

150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 |
ParameterDescriptionTypeDefault
startOnDefault value.boolFalse
onColourColour of toggle when on.(int, int, int)(141, 185, 244)
offColourColour of toggle when off.(int, int, int)(150, 150, 150)
handleOnColourThickness of toggle handle when on.(int, int, int)(26, 115, 232)
handleOffColourThickness of toggle handle when off.(int, int, int)(200, 200, 200)
handleRadiusRadius of handle.intheight / 1.3
198 |
199 |
200 | 201 | 205 | 209 | 210 | 211 | 212 | 272 | 273 | 274 | 275 | -------------------------------------------------------------------------------- /pygame_widgets/selection.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import math 3 | 4 | import pygame_widgets 5 | from pygame_widgets.widget import WidgetBase 6 | from pygame_widgets.mouse import Mouse, MouseState 7 | 8 | 9 | class Checkbox(WidgetBase): 10 | def __init__(self, win, x, y, width, height, items, **kwargs): 11 | """ A list of buttons that allows multiple selections 12 | 13 | :param win: Surface on which to draw 14 | :type win: pygame.Surface 15 | :param x: X-coordinate of top left 16 | :type x: int 17 | :param y: Y-coordinate of top left 18 | :type y: int 19 | :param width: Width of list 20 | :type width: int 21 | :param height: Height of list 22 | :type height: int 23 | :param items: Names of list items 24 | :type items: tuple of str 25 | :param kwargs: Optional parameters 26 | """ 27 | super().__init__(win, x, y, width, height) 28 | 29 | self.items = items 30 | self.rows = len(items) 31 | self.rowHeight = self._height // self.rows 32 | self.selected = [False for _ in range(self.rows)] 33 | 34 | # Border 35 | self.borderThickness = kwargs.get('borderThickness', 3) 36 | self.borderColour = kwargs.get('borderColour', (0, 0, 0)) 37 | self.radius = kwargs.get('radius', 0) 38 | 39 | # Checkbox 40 | self.boxSize = int(kwargs.get('boxSize', self._height / self.rows // 3)) 41 | self.boxThickness = kwargs.get('boxThickness', 3) 42 | self.boxColour = kwargs.get('boxColour', (0, 0, 0)) 43 | # TODO: selected image (tick) / colour 44 | 45 | # Colour 46 | self.colour = kwargs.get('colour', (255, 255, 255)) 47 | 48 | # Alternating colours: overrides colour 49 | self.colour1 = kwargs.get('colour1', self.colour) 50 | self.colour2 = kwargs.get('colour2', self.colour) 51 | 52 | # Text 53 | self.textColour = kwargs.get('textColour', (0, 0, 0)) 54 | self.fontSize = kwargs.get('fontSize', 20) 55 | self.font = kwargs.get('font', pygame.font.SysFont('calibri', self.fontSize)) 56 | self.texts = [self.font.render(self.items[row], True, self.textColour) for row in range(self.rows)] 57 | self.textRects = self.createTextRects() 58 | 59 | self.clicked = False 60 | 61 | self.boxes = self.createBoxLocations() 62 | 63 | def createTextRects(self): 64 | textRects = [] 65 | for row in range(self.rows): 66 | textRects.append( 67 | self.texts[row].get_rect( 68 | center=( 69 | self._x + self.boxSize * 2 + (self._width - self.boxSize * 2) // 2, 70 | self._y + self.rowHeight * row + self.rowHeight // 2 71 | ) 72 | ) 73 | ) 74 | 75 | return textRects 76 | 77 | def createBoxLocations(self): 78 | boxes = [] 79 | for row in range(self.rows): 80 | boxes.append(pygame.Rect( 81 | self._x + self.boxSize, 82 | self._y + self.rowHeight * row + self.boxSize, 83 | self.boxSize, self.boxSize 84 | )) 85 | return boxes 86 | 87 | def listen(self, events): 88 | """ Wait for inputs 89 | 90 | :param events: Use pygame.event.get() 91 | :type events: list of pygame.event.Event 92 | """ 93 | if not self._hidden and not self._disabled: 94 | mouseState = Mouse.getMouseState() 95 | x, y = Mouse.getMousePos() 96 | 97 | if self.contains(x, y): 98 | if mouseState == MouseState.CLICK: 99 | for row in range(self.rows): 100 | if self.boxes[row].collidepoint(x, y): 101 | self.selected[row] = not self.selected[row] 102 | 103 | def draw(self): 104 | """ Display to surface """ 105 | if not self._hidden: 106 | for row in range(self.rows): 107 | colour = self.colour1 if not row % 2 else self.colour2 108 | if pygame.version.vernum[0] < 2: 109 | pygame.draw.rect( 110 | self.win, colour, (self._x, self._y + self.rowHeight * row, self._width, self.rowHeight) 111 | ) 112 | else: 113 | if row == 0: 114 | pygame.draw.rect( 115 | self.win, colour, (self._x, self._y + self.rowHeight * row, self._width, self.rowHeight), 116 | border_top_left_radius=self.radius, border_top_right_radius=self.radius 117 | ) 118 | 119 | elif row == self.rows - 1: 120 | pygame.draw.rect( 121 | self.win, colour, (self._x, self._y + self.rowHeight * row, self._width, self.rowHeight), 122 | border_bottom_left_radius=self.radius, border_bottom_right_radius=self.radius 123 | ) 124 | 125 | else: 126 | pygame.draw.rect( 127 | self.win, colour, (self._x, self._y + self.rowHeight * row, self._width, self.rowHeight) 128 | ) 129 | 130 | width = 0 if self.selected[row] else self.boxThickness 131 | pygame.draw.rect( 132 | self.win, self.boxColour, 133 | self.boxes[row], 134 | width 135 | ) 136 | 137 | self.win.blit(self.texts[row], self.textRects[row]) 138 | 139 | def getSelected(self): 140 | return [self.items[row] for row in range(self.rows) if self.selected[row]] 141 | 142 | 143 | class Radio(WidgetBase): 144 | def __init__(self, win, x, y, width, height, items, **kwargs): 145 | """ A list of buttons that allows a single selections 146 | 147 | :param win: Surface on which to draw 148 | :type win: pygame.Surface 149 | :param x: X-coordinate of top left 150 | :type x: int 151 | :param y: Y-coordinate of top left 152 | :type y: int 153 | :param width: Width of list 154 | :type width: int 155 | :param height: Height of list 156 | :type height: int 157 | :param items: Names of list items 158 | :type items: tuple of str 159 | :param kwargs: Optional parameters 160 | """ 161 | super().__init__(win, x, y, width, height) 162 | 163 | self.items = items 164 | self.rows = len(items) 165 | self.rowHeight = self._height // self.rows 166 | self.selected = kwargs.get('default', 0) 167 | 168 | # Border 169 | self.borderThickness = kwargs.get('borderThickness', 3) 170 | self.borderColour = kwargs.get('borderColour', (0, 0, 0)) 171 | self.radius = kwargs.get('radius', 0) 172 | 173 | # Radio 174 | self.circleRadius = int(kwargs.get('circleRadius', self._height / self.rows // 6)) 175 | self.circleThickness = kwargs.get('circleThickness', 3) 176 | self.circleColour = kwargs.get('circleColour', (0, 0, 0)) 177 | 178 | # Colour 179 | self.colour = kwargs.get('colour', (255, 255, 255)) 180 | 181 | # Alternating colours: overrides colour 182 | self.colour1 = kwargs.get('colour1', self.colour) 183 | self.colour2 = kwargs.get('colour2', self.colour) 184 | 185 | # Text 186 | self.textColour = kwargs.get('textColour', (0, 0, 0)) 187 | self.fontSize = kwargs.get('fontSize', 20) 188 | self.font = kwargs.get('font', pygame.font.SysFont('sans-serif', self.fontSize)) 189 | self.texts = [self.font.render(self.items[row], True, self.textColour) for row in range(self.rows)] 190 | self.textRects = self.createTextRects() 191 | 192 | self.clicked = False 193 | 194 | self.circles = self.createCircleLocations() 195 | 196 | def createTextRects(self): 197 | textRects = [] 198 | for row in range(self.rows): 199 | textRects.append( 200 | self.texts[row].get_rect( 201 | center=( 202 | self._x + self.circleRadius * 6 + (self._width - self.circleRadius * 6) // 2, 203 | self._y + self.rowHeight * row + self.rowHeight // 2 204 | ) 205 | ) 206 | ) 207 | 208 | return textRects 209 | 210 | def createCircleLocations(self): 211 | circles = [] 212 | for row in range(self.rows): 213 | circles.append(( 214 | self._x + self.circleRadius * 3, 215 | self._y + self.rowHeight * row + self.rowHeight // 2, 216 | )) 217 | return circles 218 | 219 | def listen(self, events): 220 | """ Wait for inputs 221 | 222 | :param events: Use pygame.event.get() 223 | :type events: list of pygame.event.Event 224 | """ 225 | if not self._hidden and not self._disabled: 226 | mouseState = Mouse.getMouseState() 227 | x, y = Mouse.getMousePos() 228 | 229 | if self.contains(x, y): 230 | if mouseState == MouseState.CLICK: 231 | for row in range(self.rows): 232 | if math.sqrt((self.circles[row][0] - x) ** 2 + 233 | (self.circles[row][1] - y) ** 2) <= self.circleRadius: 234 | self.selected = row 235 | 236 | def draw(self): 237 | """ Display to surface """ 238 | if not self._hidden: 239 | for row in range(self.rows): 240 | colour = self.colour1 if not row % 2 else self.colour2 241 | if pygame.version.vernum[0] < 2: 242 | pygame.draw.rect( 243 | self.win, colour, (self._x, self._y + self.rowHeight * row, self._width, self.rowHeight) 244 | ) 245 | 246 | else: 247 | if row == 0: 248 | pygame.draw.rect( 249 | self.win, colour, (self._x, self._y + self.rowHeight * row, self._width, self.rowHeight), 250 | border_top_left_radius=self.radius, border_top_right_radius=self.radius 251 | ) 252 | 253 | elif row == self.rows - 1: 254 | pygame.draw.rect( 255 | self.win, colour, (self._x, self._y + self.rowHeight * row, self._width, self.rowHeight), 256 | border_bottom_left_radius=self.radius, border_bottom_right_radius=self.radius 257 | ) 258 | 259 | else: 260 | pygame.draw.rect( 261 | self.win, colour, (self._x, self._y + self.rowHeight * row, self._width, self.rowHeight) 262 | ) 263 | 264 | width = 0 if row == self.selected else self.circleThickness 265 | pygame.draw.circle( 266 | self.win, self.circleColour, 267 | self.circles[row], self.circleRadius, 268 | width 269 | ) 270 | 271 | self.win.blit(self.texts[row], self.textRects[row]) 272 | 273 | 274 | if __name__ == '__main__': 275 | pygame.init() 276 | win = pygame.display.set_mode((1000, 800)) 277 | 278 | checkbox = Checkbox(win, 100, 100, 400, 300, ('Apples', 'Bananas', 'Pears'), colour1=(0, 180, 0), 279 | colour2=(0, 50, 200), fontSize=30, radius=10) 280 | radio = Radio(win, 550, 400, 400, 300, ('Apples', 'Bananas', 'Pears'), colour1=(0, 180, 0), 281 | colour2=(0, 50, 200), fontSize=30, radius=10) 282 | 283 | run = True 284 | while run: 285 | events = pygame.event.get() 286 | for event in events: 287 | if event.type == pygame.QUIT: 288 | pygame.quit() 289 | run = False 290 | quit() 291 | 292 | win.fill((255, 255, 255)) 293 | 294 | pygame_widgets.update(events) 295 | pygame.display.update() 296 | -------------------------------------------------------------------------------- /site/progressbar/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Progress Bar - My Docs 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 91 | 92 |
93 |
94 |
124 |
125 | 126 |

Progress Bar

127 |

Displays a continuously changing percentage

128 |

Example Usage

129 |
import pygame
130 | import time
131 | from pygame_widgets import ProgressBar
132 | 
133 | startTime = time.time()
134 | 
135 | pygame.init()
136 | win = pygame.display.set_mode((1000, 600))
137 | 
138 | progressBar = ProgressBar(win, 100, 100, 500, 40, lambda: 1 - (time.time() - startTime) / 10, curved=True)
139 | 
140 | run = True
141 | while run:
142 |     events = pygame.event.get()
143 |     for event in events:
144 |         if event.type == pygame.QUIT:
145 |             pygame.quit()
146 |             run = False
147 |             quit()
148 | 
149 |     win.fill((255, 255, 255))
150 | 
151 |     progressBar.draw()
152 | 
153 |     pygame.display.update()
154 | 
155 |

This progress bar uses time to fill up, however, the progress function can be replaced by 156 | any other function call that provides a percentage.

157 |

Mandatory Parameters

158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 |
ParameterDescriptionType
progressFunction that defines the percentage of the bar filled.function -> float
174 |

Optional Parameters

175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 |
ParameterDescriptionTypeDefault
curvedAdds curved ends to the progress bar.boolFalse
completedColourColour of completed section of progress bar.(int, int, int)(0, 200, 0)
incompletedColourColour of incompleted section of progress bar.(int, int, int)(100, 100, 100)
205 |
206 |
207 | 208 | 212 | 216 | 217 | 218 | 219 | 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | My Docs 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 91 | 92 |
93 |
94 |
128 |
129 | 130 |

Pygame Widgets

131 |

132 |

A helper module for common widgets that may be required in developing applications with Pygame.

133 |

Prerequisites

134 | 138 |

Installation

139 |

Ensure that Python 3 and pip are installed and added to your environment PATH.

140 |

python -m pip install pygame-widgets

141 |

Open a Python console and run the following command.

142 |

import pygame_widgets

143 |

If you receive no errors, the installation was successful.

144 |

Usage

145 | 157 |

How to Contribute

158 |

Any contribution to this project would be greatly appreciated. 159 | This can include:

160 |
    161 |
  • Finding errors or bugs and creating a new issue
  • 162 |
  • Addressing active issues
  • 163 |
  • Adding functionality
  • 164 |
  • Improving documentation
  • 165 |
166 |

If applicable, you should make any changes in a forked repository and then create a pull 167 | request once the changes are complete and preferably tested if possible.

168 |

Note: If writing any code, please attempt to follow the Code Style Guide

169 |
170 |
171 | 172 | 176 | 180 | 181 | 182 | 183 | 243 | 244 | 245 | 246 | 247 | 251 | -------------------------------------------------------------------------------- /site/slider/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Slider - My Docs 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 91 | 92 |
93 |
94 |
120 |
121 | 122 |

Slider

123 |

A slider for discrete numeric value selection

124 |

slider.png

125 |

Example Usage

126 |
import pygame
127 | from pygame_widgets import Slider, TextBox
128 | 
129 | pygame.init()
130 | win = pygame.display.set_mode((1000, 600))
131 | 
132 | slider = Slider(win, 100, 100, 800, 40, min=0, max=99, step=1)
133 | output = TextBox(win, 475, 200, 60, 50, fontSize=30)
134 | 
135 | run = True
136 | while run:
137 |     events = pygame.event.get()
138 |     for event in events:
139 |         if event.type == pygame.QUIT:
140 |             pygame.quit()
141 |             run = False
142 |             quit()
143 | 
144 |     win.fill((255, 255, 255))
145 | 
146 |     slider.listen(events)
147 |     slider.draw()
148 | 
149 |     output.setText(str(slider.getValue()))
150 | 
151 |     output.draw()
152 | 
153 |     pygame.display.update()
154 | 
155 |

As you can see, TextBox can be used to display text as well, by not calling its listen method.

156 |

Optional Parameters

157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 |
ParameterDescriptionTypeDefault
minMinimum value of the slider (left).int or float0
maxMaximum value of the slider (right).int or float99
stepValue to increment by.int or float1
colourColour of slider.(int, int, int)(200, 200, 200)
handleColourColour of handle.(int, int, int)(0, 0, 0)
initialInitial value of the slider.int or floatAverage of min and max
handleRadiusRadius of handle.intheight / 1.3
curvedAdd curved ends to the slider.boolTrue
217 |
218 |
219 | 220 | 224 | 228 | 229 | 230 | 231 | 291 | 292 | 293 | 294 | -------------------------------------------------------------------------------- /site/textbox/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | TextBox - My Docs 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 91 | 92 |
93 |
94 |
120 |
121 | 122 |

TextBox

123 |

A box for text input or display

124 |

textbox.png

125 |

Example Usage

126 |
import pygame
127 | from pygame_widgets import TextBox
128 | 
129 | 
130 | def output():
131 |     # Get text in the textbox
132 |     print(textbox.getText())
133 | 
134 | 
135 | pygame.init()
136 | win = pygame.display.set_mode((1000, 600))
137 | 
138 | textbox = TextBox(win, 100, 100, 800, 80, fontSize=50,
139 |                   borderColour=(255, 0, 0), textColour=(0, 200, 0),
140 |                   onSubmit=output, radius=10, borderThickness=5)
141 | 
142 | run = True
143 | while run:
144 |     events = pygame.event.get()
145 |     for event in events:
146 |         if event.type == pygame.QUIT:
147 |             pygame.quit()
148 |             run = False
149 |             quit()
150 | 
151 |     win.fill((255, 255, 255))
152 | 
153 |     textbox.listen(events)
154 |     textbox.draw()
155 | 
156 |     pygame.display.update()
157 | 
158 |

Optional Parameters

159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 |
ParameterDescriptionTypeDefault
colourBackground colour.(int, int, int)(220, 220, 220)
textColourColour of text.(int, int, int)(0, 0, 0)
borderColourColour of border.(int, int, int)(0, 0, 0)
borderThicknessThickness of border.int3
radiusBorder radius. Set to 0 for no radius.int0
onSubmitFunction to be called when return / enter is pressed.functionNone
onSubmitParamsParameters to be fed into onSubmit function.(*any)()
onTextChangedFunction to be called when the text in the box is changed.functionNone
onTextChangedParamsParameters to be fed into onTextChanged function.(*any)()
placeholderTextText to be displayed when empty.str''
fontSizeSize of text.int20
fontFont of text.pygame.font.FontCalibri
243 |
244 |
245 | 246 | 250 | 254 | 255 | 256 | 257 | 317 | 318 | 319 | 320 | --------------------------------------------------------------------------------