├── .flake8 ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── DEV-NOTIZEN.md ├── LICENSE ├── README.md ├── how-to-use.py ├── poetry.lock ├── pyproject.toml ├── resources ├── example_one.png └── example_two.png └── src ├── __init__.py └── pygame_texteditor ├── ColorFormatter.py ├── TextEditor.py ├── __init__.py ├── _caret.py ├── _customization.py ├── _editor_getters.py ├── _input_handling_keyboard.py ├── _input_handling_keyboard_highlight.py ├── _input_handling_mouse.py ├── _letter_operations.py ├── _other.py ├── _rendering.py ├── _rendering_highlighting.py ├── _rendering_syntax_coloring.py ├── _scrollbar_vertical.py ├── _usage.py └── elements ├── colorstyles ├── bright.yml └── dark.yml ├── fonts └── Courier.ttf └── graphics └── Scroll_Bar.png /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore=D100,W503 3 | per-file-ignores= 4 | src/imow_ia01/imow_datamodel/main.py:F401 5 | src/imow_ia01/imow_datamodel_custom/main.py:F401 6 | src/imow_ia01/imow_transform_basic/steps.py:B023 7 | tests/*/*.py:F401,F811 8 | tests/testing_util.py:F403,F405,T201 9 | src/imow_api/update_swagger/update_swagger_docs.py:T201 10 | 11 | max-line-length = 120 12 | exclude= 13 | venv/* 14 | src_dev/* 15 | tests_manual/* 16 | docs/* 17 | .idea/* 18 | .vscode/* 19 | .git/* 20 | __pycache__/* 21 | */__init__.py 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project folders & files 2 | .idea/ 3 | __pycache__/ 4 | venv/ 5 | # package folders & files 6 | dist/ 7 | build/ 8 | *.egg-info/ 9 | .vs/ 10 | .vscode/ 11 | pygame_texteditor_env/ 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-ast 7 | - id: check-merge-conflict 8 | - id: check-toml 9 | - id: check-yaml 10 | - id: end-of-file-fixer 11 | - id: mixed-line-ending 12 | - id: trailing-whitespace 13 | - id: pretty-format-json 14 | args: [ "--autofix", "--no-sort-keys" ] 15 | - id: name-tests-test 16 | args: [ "--pytest-test-first" ] 17 | exclude: tests/testing_* 18 | - repo: https://github.com/psf/black 19 | rev: 22.3.0 20 | hooks: 21 | - id: black 22 | language_version: python3.8 23 | exclude: ^notebooks 24 | - repo: https://github.com/pycqa/isort 25 | rev: 5.10.1 26 | hooks: 27 | - id: isort 28 | name: isort (python) 29 | - repo: https://github.com/pycqa/flake8 30 | rev: 5.0.4 31 | hooks: 32 | - id: flake8 33 | additional_dependencies: [ 34 | 'flake8-bugbear', 35 | 'flake8-comprehensions', 36 | 'flake8-debugger', 37 | 'flake8-deprecated', 38 | 'flake8-docstrings', 39 | 'flake8-print', 40 | 'flake8-builtins', 41 | ] 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.7.3: Added ability to customize the color of the cursor 4 | 5 | The yaml files used to customize the editor now support a field called `caretColor` which can be used to customize the color of the cursor. 6 | 7 | ### 0.7.2: Added parameter to usage of pygment lexer 8 | 9 | Added the parameter to `PythonLexer(ensurenl=False)` in order to avoid the issue from 10 | [pygment](https://github.com/pygments/pygments/issues/610) as identified by [brno32](https://github.com/brno32)! 11 | 12 | 13 | ### 0.7.1: Bugfix for displaying line numbers correctly and doc-update 14 | 15 | - Minor fix concerning a bug which caused the line number background to have a wrong width when first initialized. 16 | - Improved documentation concerning README.md and [how-to-use.py](https://github.com/CribberSix/pygame-texteditor/blob/master/how-to-use.py). 17 | 18 | ### 0.7.0: QoL update - BREAKING CHANGES 19 | 20 | - A lot of internal variables but also some parameters have been renamed to enable easier maintenance of the codebase. This might break existing implementations using the package if you hardcoded parameter names upon function calls or if you called internal variables. 21 | - Removed unnecessary class attributes. 22 | - Fixed a visual bug which caused the end of th escrollbar to be displayed outside the area of the texteditor. 23 | - Fixed an issue [#11](https://github.com/CribberSix/pygame-texteditor/issues/11) which caused line numbers to be rendered incorrectly if a large font size was set. 24 | - Fixed an isse [#12](https://github.com/CribberSix/pygame-texteditor/issues/10) which caused some lines to be un-selectable when a large font was set. 25 | - Fixed an issue which caused the editor to crash if you used the arrow keys to scroll out of the visible lines. 26 | - Fixed an issue which caused line numbers to extend over their background and even outside the editor if numbers went above two digits. 27 | 28 | ### 0.6.8: Font customization 29 | 30 | Added option to use a custom (monospace) font, thanks to [brno32](https://github.com/brno32)'s contribution! 31 | 32 | ### 0.6.7: Cursor customization 33 | 34 | Added option to have a static (or blinking) cursor. 35 | 36 | ### 0.6.5: Pygame version 37 | 38 | Changed requirements concerning pygame to `>= 1.9.6` as version `2.0.1` has backwards compatibility. 39 | 40 | ### 0.6.4: Bugfix 41 | 42 | Fixed highlight-rendering (adjusted to actual line height + gap!) 43 | 44 | ### 0.6.3: Font size customization 45 | 46 | Implemented new function to customize font-size. 47 | -------------------------------------------------------------------------------- /DEV-NOTIZEN.md: -------------------------------------------------------------------------------- 1 | # PyBrainzz 2 | 3 | # MOUSE BEHAVIOR - ~~Click~~ vs Drag 4 | 5 | **There is no "mouse click"**. There is only: 6 | 7 | - MouseDOWN - which sets the following variables: 8 | - drag_chosen_LineIndex_start 9 | - drag_chosen_LetterIndex_start 10 | - MouseUP - which sets the following variables: 11 | - drag_chosen_LineIndex_end 12 | - drag_chosen_LetterIndex_end 13 | 14 | If the two have the same position (line_index + letter_index) then it is a "**click**" as it functions like a click, if the two have a different start- and end-point, then we drag and highlight some text between the two points. 15 | 16 | The following variables are used when typing to keep the caret in the correct place. They are also set AFTER a mouse drag has been finished. 17 | 18 | - chosen_LetterIndex 19 | - chosen_LineIndex 20 | 21 | The variables are set to the END of the mouse drag action (z.b. drag_chosen_LetterIndex_end) 22 | 23 | ### Caret 24 | 25 | The caret is solely set via the variables 26 | 27 | - caret_x 28 | - caret_y 29 | 30 | ### self.dragged_finished 31 | 32 | An area **is being** highlighted. 33 | 34 | Is set to FALSE upon left mouse DOWN 35 | 36 | Is set to TRUE upon left mouse UP 37 | 38 | ### self.dragged_active 39 | 40 | An area **was** highlighted **or is being** highlighted. 41 | 42 | Is set to TRUE upon left mouse DOWN 43 | 44 | Is set to FALSE upon an input (key or mouse-click) after a area was highlighted → signals that the highlighted area undergoes changes based on the input 45 | 46 | - key-input → is being modified/replaced 47 | - the mouse was clicked somewhere else → nothing happens, deselection 48 | - arrow-key → nothing happens, deselection 49 | 50 | # Logical vs visual text 51 | 52 | The logical text is stored in an list of strings called ```self.line_String_array```. 53 | 54 | The visual text is stored in an list of lists of dicts called ```self.line_Text_array```. 55 | For every line, there exists one sublist. The sublists are populated with surfaces which represent 56 | the differently blitted words / operators etc, based on syntax highlighting. If syntax highlighting 57 | is disabled, the sublist contains only one dict with the entire text. 58 | 59 | The dicts have three keys: 60 | - ```chars```: actual characters making up the portion of the text 61 | - ```type```: String, describing the content, e.g. 'quoted' 62 | - ```color```: rgb-color code, e.g. (0,0,0) 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2020] [CribberSix] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A WYSIWYG-texteditor based on pygame for pygame 2 | 3 | ![PyPI](https://img.shields.io/pypi/v/pygame-texteditor?color=%233775A9&label=pypi%20package&style=plastic) 4 | ![GitHub](https://img.shields.io/github/license/CribberSix/pygame-texteditor?style=plastic) 5 | 6 | ## Introduction & examples 7 | 8 | The text editor can be inserted into any existing pygame window. 9 | A minimal code example of it being activated within an existing pygame window can be found below. 10 | 11 | The code editor comes with line numbers and syntax highlighting for python if enabled: 12 | 13 | ![](./resources/example_one.png) 14 | 15 | Example with default configuration: 16 | 17 | ![](./resources/example_two.png) 18 | 19 | ## Usage 20 | 21 | The texteditor takes 5 obligatory parameters and 4 optional parameters. 22 | 23 | ##### Obligatory parameters 24 | - ```offset_X``` : integer - the offset from the left border of the pygame screen 25 | - ```offset_y``` : integer - the offset from the top border of the pygame screen 26 | - ```editor_width``` : integer - the width of texteditor 27 | - ```editor_height``` : integer - the height of texteditor 28 | - ```screen``` : pygame display surface - on which the texteditor is to be displayed 29 | 30 | ##### Optional Parameters with default values 31 | 32 | - ```display_line_numbers``` - a boolean enabling showing line numbers 33 | > Default: ```False``` 34 | - ```style``` - a String setting the color scheme of editor and syntax highlighting 35 | > Default: ```'dark'``` 36 | - ```syntax_highlighting_python``` - a boolean enabling syntax highlighting for Python code 37 | > Default: ```False``` 38 | - ```font_size``` - an integer to set for the font size. 39 | > Default: ```16``` 40 | 41 | ## Setup and configuration 42 | 43 | ##### Minimal texteditor setup example 44 | 45 | ```python 46 | import pygame 47 | from pygame_texteditor import TextEditor 48 | 49 | # minimal pygame setup 50 | pygame.init() 51 | screen = pygame.display.set_mode((500, 600)) 52 | pygame.display.set_caption("Pygame") 53 | pygame.display.get_surface().fill((200, 200, 200)) # background coloring 54 | 55 | # Instantiation & customization of the text editor 56 | TX = TextEditor( 57 | offset_x=50, offset_y=50, editor_width=500, editor_height=400, screen=pygame.display.get_surface() 58 | ) 59 | TX.set_line_numbers(True) 60 | TX.set_syntax_highlighting(True) 61 | TX.set_font_size(18) 62 | 63 | # TextEditor in the pygame-loop 64 | while True: 65 | # INPUT - Mouse + Keyboard 66 | pygame_events = pygame.event.get() 67 | pressed_keys = pygame.key.get_pressed() 68 | mouse_x, mouse_y = pygame.mouse.get_pos() 69 | mouse_pressed = pygame.mouse.get_pressed() 70 | 71 | # displays editor functionality once per loop 72 | TX.display_editor(pygame_events, pressed_keys, mouse_x, mouse_y, mouse_pressed) 73 | pygame.display.flip() # updates pygame window 74 | 75 | ``` 76 | 77 | ##### Retrieving text from the editor 78 | 79 | The editor offers the function `get_text_as_string()` to retrieve the entire text 80 | as a String from the editor. Lines are separated by the new line character ```\n```. 81 | 82 | The editor offers the function `get_text_as_list()` to retrieve the entire text as a list from the editor. 83 | Each String-item in the list represents one line from the editor. 84 | 85 | ##### Removing text from the editor 86 | 87 | The editor offers the function `clear_text()` to clear the editor of any text. 88 | 89 | ##### Inserting text into the editor 90 | 91 | Inserting text can be done by using one of the two available functions: 92 | 1. With a list of strings in which each string represents one line, or 93 | 2. With a string which includes linebreak characters which get parsed. 94 | 95 | ``` 96 | set_text_from_list(["First line", "Second Line.", "Third Line."] 97 | set_text_from_string("First line.\nSecond line.\nThird Line") 98 | ``` 99 | 100 | ## Customization 101 | 102 | #### Cursor mode 103 | 104 | Cursor mode can either be `static` or `blinking` (=default). 105 | 106 | ```python 107 | TX = TextEditor(...) 108 | TX.set_cursor_mode("static") 109 | TX.set_cursor_mode("blinking") 110 | ``` 111 | 112 | #### Key repetition speeds 113 | 114 | While a key is being held, multiple key events are being triggered. 115 | The delay of the first repetition as well as the interval between all sequential key triggers can be 116 | customized by using the function `set_key_repetition(delay=300, intervall=30)`. 117 | 118 | From the [official documentation](http://www.pygame.org/docs/ref/key.html#pygame.key.set_repeat): 119 | > The delay parameter is the number of milliseconds before the first repeated pygame.KEYDOWN event will be sent. 120 | > After that, another pygame.KEYDOWN event will be sent every interval milliseconds. 121 | 122 | 123 | #### Font Customization 124 | 125 | The editor uses a ttf file to set the font for the editor. By default, the Courier monospace font is used. 126 | 127 | A custom font can be loaded with the following method, passing an *absolute* path: 128 | - `set_font_from_ttf("X:\path\to\custom\font.ttf")` 129 | 130 | DISCLAIMER: As the width of a letter (space) is only calculated once after setting the font_size, any fonts that are not monospace will lead to the editor not working correctly anymore, as it cannot be determined correctly between which letters the user clicked. 131 | 132 | #### Font size 133 | 134 | Font size can be customized with the command `set_font_size(size)` - the parameter is an integer 135 | with the default value `16` to be able to reset it. 136 | 137 | #### Line Numbers 138 | Line numbers can be shown on the left side of the editor. Line numbers begin with 0 as is the Pythonian way. 139 | 140 | Line numbers can be enabled and disabled with ```set_line_numbers(Boolean)```. 141 | 142 | 143 | #### Syntax Highlighting 144 | 145 | The editor comes with syntax highlighting for Python code. Tokenization is based on the ```pygment``` package. 146 | 147 | Syntax highlighting can be enabled/disabled with ```set_syntax_coloring(boolean_value)```. 148 | 149 | The syntax colors being used are also specified in the yml style file. 150 | 151 | 152 | #### Color-scheme customization 153 | 154 | The editor uses a yml file to set the color-scheme for the editor itself and for the syntax coloring. 155 | 156 | Two styles are delivered with the editor, they can be activated respectively by: 157 | - `set_colorscheme("dark")` 158 | - `set_colorscheme("bright")` 159 | 160 | A custom style can be loaded with the following method from a created yml file: 161 | - `set_colorscheme_from_yaml("X:\path\to\custom\filename.yml")` 162 | 163 | All keys must be present with values. Acceptable values are 164 | RGB colors in the following format: ```(255, 255, 255)``` or ```255, 255, 255```. 165 | 166 | The following keys are required in the ```stylename.yml``` file, syntax colors are only used if syntax 167 | highlighting is enabled, but are still required to be included. 168 | 169 | **Editor colors** (source: bright.yml) 170 | 171 | - `codingBackgroundColor: (255, 255, 255)` 172 | - `codingScrollBarBackgroundColor: (49, 50, 50)` 173 | - `lineNumberColor: (255, 255, 255)` 174 | - `lineNumberBackgroundColor: (60, 61, 61)` 175 | - `textColor: (255, 255, 255)` 176 | - `caretColor: (255, 255, 255)` 177 | 178 | **Syntax colors** (source: bright.yml) 179 | 180 | - `textColor_normal: (0, 255, 255)` 181 | - `textColor_comments: (119, 115, 115)` 182 | - `textColor_quotes: (227, 215, 115)` 183 | - `textColor_operators: (237, 36, 36)` 184 | - `textColor_keywords: (237, 36, 36)` 185 | - `textColor_function: (50, 150, 36)` 186 | - `textColor_builtin: (50, 50, 136)` 187 | -------------------------------------------------------------------------------- /how-to-use.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | from pygame_texteditor import TextEditor 4 | 5 | # minimal pygame setup 6 | pygame.init() 7 | screen = pygame.display.set_mode((500, 600)) 8 | pygame.display.set_caption("Pygame") 9 | pygame.display.get_surface().fill((200, 200, 200)) # background coloring 10 | 11 | # Instantiation & customization of the text editor 12 | TX = TextEditor(offset_x=50, offset_y=50, editor_width=500, editor_height=400, screen=pygame.display.get_surface()) 13 | TX.set_line_numbers(True) 14 | TX.set_syntax_highlighting(True) 15 | TX.set_font_size(18) 16 | 17 | # TextEditor in the pygame-loop 18 | while True: 19 | # INPUT - Mouse + Keyboard 20 | pygame_events = pygame.event.get() 21 | pressed_keys = pygame.key.get_pressed() 22 | mouse_x, mouse_y = pygame.mouse.get_pos() 23 | mouse_pressed = pygame.mouse.get_pressed() 24 | 25 | # displays editor functionality once per loop 26 | TX.display_editor(pygame_events, pressed_keys, mouse_x, mouse_y, mouse_pressed) 27 | pygame.display.flip() # updates pygame window 28 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "23.3.0" 6 | description = "The uncompromising code formatter." 7 | category = "dev" 8 | optional = false 9 | python-versions = ">=3.7" 10 | files = [ 11 | {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, 12 | {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, 13 | {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, 14 | {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, 15 | {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, 16 | {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, 17 | {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, 18 | {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, 19 | {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, 20 | {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, 21 | {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, 22 | {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, 23 | {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, 24 | {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, 25 | {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, 26 | {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, 27 | {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, 28 | {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, 29 | {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, 30 | {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, 31 | {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, 32 | {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, 33 | {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, 34 | {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, 35 | {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, 36 | ] 37 | 38 | [package.dependencies] 39 | click = ">=8.0.0" 40 | mypy-extensions = ">=0.4.3" 41 | packaging = ">=22.0" 42 | pathspec = ">=0.9.0" 43 | platformdirs = ">=2" 44 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 45 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 46 | 47 | [package.extras] 48 | colorama = ["colorama (>=0.4.3)"] 49 | d = ["aiohttp (>=3.7.4)"] 50 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 51 | uvloop = ["uvloop (>=0.15.2)"] 52 | 53 | [[package]] 54 | name = "cfgv" 55 | version = "3.3.1" 56 | description = "Validate configuration and produce human readable error messages." 57 | category = "dev" 58 | optional = false 59 | python-versions = ">=3.6.1" 60 | files = [ 61 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 62 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 63 | ] 64 | 65 | [[package]] 66 | name = "click" 67 | version = "8.1.3" 68 | description = "Composable command line interface toolkit" 69 | category = "dev" 70 | optional = false 71 | python-versions = ">=3.7" 72 | files = [ 73 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 74 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 75 | ] 76 | 77 | [package.dependencies] 78 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 79 | 80 | [[package]] 81 | name = "colorama" 82 | version = "0.4.6" 83 | description = "Cross-platform colored terminal text." 84 | category = "dev" 85 | optional = false 86 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 87 | files = [ 88 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 89 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 90 | ] 91 | 92 | [[package]] 93 | name = "distlib" 94 | version = "0.3.6" 95 | description = "Distribution utilities" 96 | category = "dev" 97 | optional = false 98 | python-versions = "*" 99 | files = [ 100 | {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, 101 | {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, 102 | ] 103 | 104 | [[package]] 105 | name = "exceptiongroup" 106 | version = "1.1.1" 107 | description = "Backport of PEP 654 (exception groups)" 108 | category = "dev" 109 | optional = false 110 | python-versions = ">=3.7" 111 | files = [ 112 | {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, 113 | {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, 114 | ] 115 | 116 | [package.extras] 117 | test = ["pytest (>=6)"] 118 | 119 | [[package]] 120 | name = "filelock" 121 | version = "3.12.0" 122 | description = "A platform independent file lock." 123 | category = "dev" 124 | optional = false 125 | python-versions = ">=3.7" 126 | files = [ 127 | {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"}, 128 | {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"}, 129 | ] 130 | 131 | [package.extras] 132 | docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] 133 | testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] 134 | 135 | [[package]] 136 | name = "identify" 137 | version = "2.5.22" 138 | description = "File identification library for Python" 139 | category = "dev" 140 | optional = false 141 | python-versions = ">=3.7" 142 | files = [ 143 | {file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"}, 144 | {file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"}, 145 | ] 146 | 147 | [package.extras] 148 | license = ["ukkonen"] 149 | 150 | [[package]] 151 | name = "iniconfig" 152 | version = "2.0.0" 153 | description = "brain-dead simple config-ini parsing" 154 | category = "dev" 155 | optional = false 156 | python-versions = ">=3.7" 157 | files = [ 158 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 159 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 160 | ] 161 | 162 | [[package]] 163 | name = "mypy-extensions" 164 | version = "1.0.0" 165 | description = "Type system extensions for programs checked with the mypy type checker." 166 | category = "dev" 167 | optional = false 168 | python-versions = ">=3.5" 169 | files = [ 170 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 171 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 172 | ] 173 | 174 | [[package]] 175 | name = "nodeenv" 176 | version = "1.7.0" 177 | description = "Node.js virtual environment builder" 178 | category = "dev" 179 | optional = false 180 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 181 | files = [ 182 | {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, 183 | {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, 184 | ] 185 | 186 | [package.dependencies] 187 | setuptools = "*" 188 | 189 | [[package]] 190 | name = "packaging" 191 | version = "23.1" 192 | description = "Core utilities for Python packages" 193 | category = "dev" 194 | optional = false 195 | python-versions = ">=3.7" 196 | files = [ 197 | {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, 198 | {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, 199 | ] 200 | 201 | [[package]] 202 | name = "pathspec" 203 | version = "0.11.1" 204 | description = "Utility library for gitignore style pattern matching of file paths." 205 | category = "dev" 206 | optional = false 207 | python-versions = ">=3.7" 208 | files = [ 209 | {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, 210 | {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, 211 | ] 212 | 213 | [[package]] 214 | name = "platformdirs" 215 | version = "3.2.0" 216 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 217 | category = "dev" 218 | optional = false 219 | python-versions = ">=3.7" 220 | files = [ 221 | {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, 222 | {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, 223 | ] 224 | 225 | [package.extras] 226 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] 227 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 228 | 229 | [[package]] 230 | name = "pluggy" 231 | version = "1.0.0" 232 | description = "plugin and hook calling mechanisms for python" 233 | category = "dev" 234 | optional = false 235 | python-versions = ">=3.6" 236 | files = [ 237 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 238 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 239 | ] 240 | 241 | [package.extras] 242 | dev = ["pre-commit", "tox"] 243 | testing = ["pytest", "pytest-benchmark"] 244 | 245 | [[package]] 246 | name = "pre-commit" 247 | version = "3.2.2" 248 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 249 | category = "dev" 250 | optional = false 251 | python-versions = ">=3.8" 252 | files = [ 253 | {file = "pre_commit-3.2.2-py2.py3-none-any.whl", hash = "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4"}, 254 | {file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"}, 255 | ] 256 | 257 | [package.dependencies] 258 | cfgv = ">=2.0.0" 259 | identify = ">=1.0.0" 260 | nodeenv = ">=0.11.1" 261 | pyyaml = ">=5.1" 262 | virtualenv = ">=20.10.0" 263 | 264 | [[package]] 265 | name = "pygame" 266 | version = "2.3.0" 267 | description = "Python Game Development" 268 | category = "main" 269 | optional = false 270 | python-versions = ">=3.6" 271 | files = [ 272 | {file = "pygame-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e9535cf1af0c6ca38d94e0b492fc41057d7bf05e9bd64d3ed3e216d336d6d11"}, 273 | {file = "pygame-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:23bd3c3a6d4e8acddee2297d609dbc5953d6ba99b0f0cc5ccc2f567889db3785"}, 274 | {file = "pygame-2.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:619eed2d97f28af9d4cdb217a5517fd6f59b873f2f1d31b4489ed852b9a175c3"}, 275 | {file = "pygame-2.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9ccac73a8c913809ba2c1408d750abf14e45666b3c83493370441c52e99222b4"}, 276 | {file = "pygame-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ec8e691407b6c91525b2d7c8386fd6232b97d8f8c33d134ec0c0165b1d52c24"}, 277 | {file = "pygame-2.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8308b21804d137a3b7cafbd020d2159eb5bcc18ffc9c3993b20311069c326a2c"}, 278 | {file = "pygame-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1d737db18f4c94b620613c6a047a3a1eecc0f36df7d5da4070de575930cc5f0"}, 279 | {file = "pygame-2.3.0-cp310-cp310-win32.whl", hash = "sha256:788717d0b9a0d0828a763381e1eb6a127ceef815f9a91ff52217ed4b78df62fc"}, 280 | {file = "pygame-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:e3948be800b5f251a0741ec3aab3ca508dfc391095726a69af7064fa4d3e0547"}, 281 | {file = "pygame-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:82e5806fd797bd1b27fae705683f6822ae5276ec9cda42e6e21bba61985b763a"}, 282 | {file = "pygame-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fab0457ab07e8abb99de2b83c0a71f98bdf79afb01ff611873e4333fd8649f02"}, 283 | {file = "pygame-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad8fa7a91fa8f2a4fa46366142763675a0a11b7c34b06dfc20b1095d116da820"}, 284 | {file = "pygame-2.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfff49dbb7fcc2a9a88e3f25fda7f181ee4957fd89df78c47fa64c689d19b8a9"}, 285 | {file = "pygame-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5afd712bd7307d034e6940f3025c4b769656fd4cbb38fbdbd6af0f93d6c8386"}, 286 | {file = "pygame-2.3.0-cp311-cp311-win32.whl", hash = "sha256:fa18acc2d6f0d09575802e1db11845fc0f83f9777cc385c51380125df92f3dc9"}, 287 | {file = "pygame-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:101c57141d705ca1930377c324d2c7acd3099f1b4ac676981bdf5d5b329842c8"}, 288 | {file = "pygame-2.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:17730a2ed1001e5876745702c92906ad31ecedc13825efba56a0cba92e273b7a"}, 289 | {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b334f6dd6c1412dd4b161a8562b7a422db957f67b7eb93e927606e2dd435882"}, 290 | {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4db1b103025fd4b451dfa409c0da16d2ff31714ae82bdf45b1434863cd69370b"}, 291 | {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d339f90cc30de4b013670de84abd46de4be602d5c52bbe4e569fa15d17b204ca"}, 292 | {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7390815dad55a2db9f8daac6f2c2e593801daea2d674433a72b91ea1caee0d3"}, 293 | {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a1e473c627acf369b30bb52fb5f39d1f68f8c204aa857578b72f07a23c952b"}, 294 | {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:228514c0d034c840b8ee6bf99185df34ac15e6a6a99684b8a3900124417c8d8f"}, 295 | {file = "pygame-2.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a8b315203925724f89a81a741682589ba1c36ec858d98e6accb7501ece9e99a3"}, 296 | {file = "pygame-2.3.0-cp36-cp36m-win32.whl", hash = "sha256:38642c6cc6477db6ebddd52be39bad0a9e19cf097f83feaaf8e7573b9a9d2405"}, 297 | {file = "pygame-2.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:525e11a2b9182ec84d690634016009e382ab8b488593c3f150a0b8aae28aa165"}, 298 | {file = "pygame-2.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:32bdf1d5d9e0763779d0b915d4617253949a6c118c4c6b5ae1a77cf1df964e4c"}, 299 | {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f57b1ee40387e43ab5c3cf20437283477b5ef52ead4bb1d9bff254ef9ee70623"}, 300 | {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ccde93b51d2393216f98e8f81cf5cc628513d837c89dcf5b588f52031659c09"}, 301 | {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c60be419d7cca1222895dfe9d520628b7346015208382a19fa678356a22664b3"}, 302 | {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43f238229b3a9e5692ba5a31638f1c148257b37a49ef21f03b23b34d7f00b2d9"}, 303 | {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d628637d4f0c55613f258b84eef932faf89e683aa842f4fd483a676f44a38606"}, 304 | {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:35f5a9cc7a9a2ea3d048e418e79f30e1506cb47015939330903026c636761aab"}, 305 | {file = "pygame-2.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:703d5def9d4dbe9c358f63151bee4a55e328dd7737e692f52522bc44be7c7c8c"}, 306 | {file = "pygame-2.3.0-cp37-cp37m-win32.whl", hash = "sha256:53e9418c457fa549294feee7947bc0b24b048b4eba133f0e757dd2348d15af3b"}, 307 | {file = "pygame-2.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0a664cd6c50870f6749c389a8844318afc8a2d02f8cb7b05d67930fdf99252bd"}, 308 | {file = "pygame-2.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf236758429d9b9cdadd1fcf40901588818ee440178b932409c40157ab41e902"}, 309 | {file = "pygame-2.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d035ba196c258876a87451fa7de65b62c087d7016e51000e8d95bc67c8584f7"}, 310 | {file = "pygame-2.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:57180b3aabbe17d8017aa724887019943d96ea69810f4315f5c1b7d4f64861f9"}, 311 | {file = "pygame-2.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:246f75f67d2ad4c2dad21b1f35c6092d67c4c0db13b2fa0a42d794e6e2794f47"}, 312 | {file = "pygame-2.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033352321cc49d60fdc3c3ae4b3e10ecb6614846fb2eb3453c729aba48a2874d"}, 313 | {file = "pygame-2.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ee86606c6c7f61176ed24b427fa230fe4fc9f552aa555b8db21ddb608b4ce88"}, 314 | {file = "pygame-2.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d949e93fbdaf5b43f69a484639104c07028f93686c8305afb0d8e382fde8ff5d"}, 315 | {file = "pygame-2.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2acf958513bd1612960ec68aa5e388262218f7365db59e54e1ee68a55bc544b"}, 316 | {file = "pygame-2.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6c5d33355dfb66382bcac1fcf3db64ba71bc9e97082db3ae45a7a0d335e73268"}, 317 | {file = "pygame-2.3.0-cp38-cp38-win32.whl", hash = "sha256:1eda9f30d376d4205e8204e542ab1348dcbb31755c8ba38772e48a3b2f91b2fc"}, 318 | {file = "pygame-2.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b507df9ea606a87c29e5028b8de9f35066a15f6a5d7f3e5b47b3719e9403f924"}, 319 | {file = "pygame-2.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25c1b1819211aaa0f98264e6b670a496a9975079d5ae2dffd304b0aca6b1aa3c"}, 320 | {file = "pygame-2.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e516bc6bba5455817bbb0038f4c44d1914aac13c7f7954dee9213c9ae28bd9ac"}, 321 | {file = "pygame-2.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:740b9f311c693b00d86a89cc6846afc1d1e013b006975eb8be0b18d5481c5b32"}, 322 | {file = "pygame-2.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:932034e1738873a55c4e2eb83b6e8c03f9a55feaa6a04a7da7b1e0e5a5050b4a"}, 323 | {file = "pygame-2.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774233845099d632de676ad4d4dd08ba27ebce5bfa550b1dc9f6cce145e21c35"}, 324 | {file = "pygame-2.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f79a3c5e7f24474d6e722d597ee03d2b0d17958c77d4307787147cf339b4ad9"}, 325 | {file = "pygame-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84fad9538012f1d6b298dcf690c4336e0317fe97ac10993b4d847ff547e919dd"}, 326 | {file = "pygame-2.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:910678441d02c3b55ac59fcbc4220a824b094407de084734b5d84e0900d6448b"}, 327 | {file = "pygame-2.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:653ec5102b9cb13a24e26663a81d7810790e56b88113b90aa5fdca681c01a5b9"}, 328 | {file = "pygame-2.3.0-cp39-cp39-win32.whl", hash = "sha256:e62607c86e02d29ba5cb00837f73b1dce7b325a1f1f6d93150a0f96fa68da1a1"}, 329 | {file = "pygame-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:90931a210325274184860d898df4e87a0972654edbb2a6185afcdce32244dfb6"}, 330 | {file = "pygame-2.3.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:1dc89d825e0ccba5ba3605abbd83be1401e0a32de7ab64b9647a6bb1ecb0a4f7"}, 331 | {file = "pygame-2.3.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e323b75abda43345aff5ab2f6b1c017135f937f8a114d7aac8d95a07d200e19f"}, 332 | {file = "pygame-2.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e13de2947c496fcb600fa4b5cd00a5fa33d4b3af9d13c169a5f79268268de0a8"}, 333 | {file = "pygame-2.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:555234ed6b08242af95406fd3eb43255c3ce8e915e8c751f2d411bd40d574df4"}, 334 | {file = "pygame-2.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:858d3968aebaca5015ef0ec82c513114a3c3fe64ce910222cfa852a39f03b135"}, 335 | {file = "pygame-2.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:250b3ec3f90b05ad50cb0070d994a0a1f39fffe8181fc9508b8749884c313431"}, 336 | {file = "pygame-2.3.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a5e83bd89da26f8360e02d5de2d2575981b0ebad81ea6d48aba610dabf167b88"}, 337 | {file = "pygame-2.3.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2961d44593aaa99580971e4123db00d4ca72fb4b30fa56350b3f6792331a41e"}, 338 | {file = "pygame-2.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:385163fd1ed8809a72be68fddc9c76876c304e8712695aff2ea49adf3831caf9"}, 339 | {file = "pygame-2.3.0.tar.gz", hash = "sha256:884b92c9cbf0bfaf8b8dd0f75a746613c55447d307ddd1addf903709b3b9f89f"}, 340 | ] 341 | 342 | [[package]] 343 | name = "pygments" 344 | version = "2.14.0" 345 | description = "Pygments is a syntax highlighting package written in Python." 346 | category = "main" 347 | optional = false 348 | python-versions = ">=3.6" 349 | files = [ 350 | {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, 351 | {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, 352 | ] 353 | 354 | [package.extras] 355 | plugins = ["importlib-metadata"] 356 | 357 | [[package]] 358 | name = "pyperclip" 359 | version = "1.8.1" 360 | description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" 361 | category = "main" 362 | optional = false 363 | python-versions = "*" 364 | files = [ 365 | {file = "pyperclip-1.8.1.tar.gz", hash = "sha256:9abef1e79ce635eb62309ecae02dfb5a3eb952fa7d6dce09c1aef063f81424d3"}, 366 | ] 367 | 368 | [[package]] 369 | name = "pytest" 370 | version = "7.3.1" 371 | description = "pytest: simple powerful testing with Python" 372 | category = "dev" 373 | optional = false 374 | python-versions = ">=3.7" 375 | files = [ 376 | {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"}, 377 | {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"}, 378 | ] 379 | 380 | [package.dependencies] 381 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 382 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 383 | iniconfig = "*" 384 | packaging = "*" 385 | pluggy = ">=0.12,<2.0" 386 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 387 | 388 | [package.extras] 389 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 390 | 391 | [[package]] 392 | name = "pyyaml" 393 | version = "6.0" 394 | description = "YAML parser and emitter for Python" 395 | category = "main" 396 | optional = false 397 | python-versions = ">=3.6" 398 | files = [ 399 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 400 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 401 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 402 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 403 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 404 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 405 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 406 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, 407 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, 408 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, 409 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, 410 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, 411 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, 412 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, 413 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 414 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 415 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 416 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 417 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 418 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 419 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 420 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 421 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 422 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 423 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 424 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 425 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 426 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 427 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 428 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 429 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 430 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 431 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 432 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 433 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 434 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 435 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 436 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 437 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 438 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 439 | ] 440 | 441 | [[package]] 442 | name = "setuptools" 443 | version = "67.6.1" 444 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 445 | category = "dev" 446 | optional = false 447 | python-versions = ">=3.7" 448 | files = [ 449 | {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, 450 | {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, 451 | ] 452 | 453 | [package.extras] 454 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 455 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 456 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 457 | 458 | [[package]] 459 | name = "tomli" 460 | version = "2.0.1" 461 | description = "A lil' TOML parser" 462 | category = "dev" 463 | optional = false 464 | python-versions = ">=3.7" 465 | files = [ 466 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 467 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 468 | ] 469 | 470 | [[package]] 471 | name = "typing-extensions" 472 | version = "4.5.0" 473 | description = "Backported and Experimental Type Hints for Python 3.7+" 474 | category = "dev" 475 | optional = false 476 | python-versions = ">=3.7" 477 | files = [ 478 | {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, 479 | {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, 480 | ] 481 | 482 | [[package]] 483 | name = "virtualenv" 484 | version = "20.21.0" 485 | description = "Virtual Python Environment builder" 486 | category = "dev" 487 | optional = false 488 | python-versions = ">=3.7" 489 | files = [ 490 | {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"}, 491 | {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"}, 492 | ] 493 | 494 | [package.dependencies] 495 | distlib = ">=0.3.6,<1" 496 | filelock = ">=3.4.1,<4" 497 | platformdirs = ">=2.4,<4" 498 | 499 | [package.extras] 500 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] 501 | test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] 502 | 503 | [metadata] 504 | lock-version = "2.0" 505 | python-versions = "^3.8" 506 | content-hash = "a539c62a983a709c7e4a967f673997c440f5e40b9c26ad66e90d1cb9bef59269" 507 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pygame-texteditor" 3 | version = "0.7.3" 4 | description = "A WYSIWYG-texteditor based on pygame." 5 | authors = ["Victor Seifert "] 6 | license = "MIT" 7 | readme = "README.md" 8 | packages = [{include = "pygame_texteditor", from="src"}] 9 | classifiers = [ 10 | "Development Status :: 5 - Production/Stable", 11 | "License :: OSI Approved :: MIT License", 12 | "Programming Language :: Python", 13 | "Programming Language :: Python :: 3.6", 14 | "Topic :: Software Development :: Libraries", 15 | "Topic :: Software Development :: Libraries :: Python Modules", 16 | "Intended Audience :: Developers", 17 | "Natural Language :: English", 18 | "Topic :: Software Development", 19 | ] 20 | keywords=[ 21 | "pygame", 22 | "texteditor", 23 | "text", 24 | "editor", 25 | ] 26 | include = [ 27 | "src/pygame_texteditor/elements/graphics/*.png", 28 | "src/pygame_texteditor/elements/fonts/.ttf", 29 | "src/pygame_texteditor/elements/colorstyles/*.yml", 30 | ] 31 | 32 | 33 | [tool.poetry.dependencies] 34 | python = "^3.8" 35 | pygame = ">=1.9.6" 36 | PyYAML = ">=5.3.1" 37 | Pygments = ">=2.6.1" 38 | pyperclip = ">=1.8.1" 39 | 40 | 41 | [tool.poetry.group.dev.dependencies] 42 | black = "^23.3.0" 43 | pre-commit = "^3.2.2" 44 | pytest = "^7.3.1" 45 | 46 | [build-system] 47 | requires = ["poetry-core"] 48 | build-backend = "poetry.core.masonry.api" 49 | 50 | [tool.isort] 51 | profile = "black" 52 | line_length = 120 53 | src_paths = ["src", "test"] 54 | py_version=38 55 | group_by_package = true 56 | no_lines_before = ["FUTURE", "STDLIB", "THIRDPARTY"] 57 | known_first_party = ["pyspark_framework"] 58 | 59 | [tool.black] 60 | line_length = 120 61 | -------------------------------------------------------------------------------- /resources/example_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CribberSix/pygame-texteditor/3b08a6c928d4cb7b1f05ccd0928799e531547867/resources/example_one.png -------------------------------------------------------------------------------- /resources/example_two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CribberSix/pygame-texteditor/3b08a6c928d4cb7b1f05ccd0928799e531547867/resources/example_two.png -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CribberSix/pygame-texteditor/3b08a6c928d4cb7b1f05ccd0928799e531547867/src/__init__.py -------------------------------------------------------------------------------- /src/pygame_texteditor/ColorFormatter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from typing import Dict, List, Tuple 4 | 5 | import yaml 6 | from pygments import highlight 7 | from pygments.formatter import Formatter 8 | from pygments.lexers import PythonLexer 9 | from pygments.token import Token, is_token_subtype 10 | 11 | 12 | class ColorFormatter: 13 | def __init__(self): 14 | self.textColor_normal = (0, 0, 0) 15 | self.textColor_comments = (0, 0, 0) 16 | self.textColor_quotes = (0, 0, 0) 17 | self.textColor_operators = (0, 0, 0) 18 | self.textColor_keywords = (0, 0, 0) 19 | self.textColor_function = (0, 0, 0) 20 | self.textColor_builtin = (0, 0, 0) 21 | 22 | def format(self, tokensource: List[Tuple]) -> List[Dict]: 23 | """ 24 | Input is a list of tuples. Each tuple contains the Token and the corresponding characters of said token. 25 | For different types of tokens the function creates a dict with the three keys: 26 | - chars -> String 27 | - type -> String 28 | - color (rgb color -> (0,0,0) ) 29 | Depending on the type of token a color is assigned. 30 | """ 31 | 32 | dicts = [] 33 | for ttype, value in tokensource: 34 | # NAME.FUNCTION 35 | if ttype == Token.Name.Function: 36 | dicts.append( 37 | { 38 | "chars": value, 39 | "type": "function", 40 | "color": self.textColor_function, 41 | } 42 | ) 43 | 44 | # BUILTIN 45 | elif ttype == Token.Name.Builtin: 46 | dicts.append( 47 | {"chars": value, "type": "builtin", "color": self.textColor_builtin} 48 | ) 49 | 50 | # LITERAL.STRING(.*) 51 | elif ttype == Token.Literal.String.Affix: 52 | dicts.append( 53 | {"chars": value, "type": "builtin", "color": self.textColor_builtin} 54 | ) 55 | elif is_token_subtype(ttype, Token.Literal.String): 56 | dicts.append( 57 | {"chars": value, "type": "quotes", "color": self.textColor_quotes} 58 | ) 59 | 60 | # OPERATOR.* 61 | elif is_token_subtype(ttype, Token.Operator): 62 | dicts.append( 63 | { 64 | "chars": value, 65 | "type": "operators", 66 | "color": self.textColor_operators, 67 | } 68 | ) 69 | 70 | # KEYWORD.* 71 | elif is_token_subtype(ttype, Token.Keyword): 72 | dicts.append( 73 | { 74 | "chars": value, 75 | "type": "keywords", 76 | "color": self.textColor_keywords, 77 | } 78 | ) 79 | 80 | # COMMENT.* 81 | elif is_token_subtype(ttype, Token.Comment): 82 | dicts.append( 83 | { 84 | "chars": value, 85 | "type": "comments", 86 | "color": self.textColor_comments, 87 | } 88 | ) 89 | 90 | elif ( 91 | value == "\n" 92 | ): # since we parse line-by-line, we need to exclude the linebreak character 93 | pass 94 | 95 | # REST 96 | else: 97 | dicts.append( 98 | {"chars": value, "type": "text", "color": self.textColor_normal} 99 | ) 100 | 101 | return dicts 102 | -------------------------------------------------------------------------------- /src/pygame_texteditor/TextEditor.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | from typing import Optional 4 | import pygame 5 | from pygments.lexers import PythonLexer 6 | 7 | from .ColorFormatter import ColorFormatter 8 | 9 | current_dir = os.path.dirname(__file__) 10 | 11 | 12 | class TextEditor: 13 | """A WYSIWYG-texteditor for pygame.""" 14 | 15 | # Scroll functionality 16 | # caret 17 | from ._caret import ( 18 | set_drag_end_after_last_line, 19 | set_drag_end_by_mouse, 20 | set_drag_end_letter_by_mouse, 21 | set_drag_end_line_by_mouse, 22 | set_drag_start_after_last_line, 23 | set_drag_start_before_first_line, 24 | set_drag_start_by_mouse, 25 | update_caret_position, 26 | update_caret_position_by_drag_end, 27 | update_caret_position_by_drag_start, 28 | ) 29 | 30 | # files for customization of the editor: 31 | from ._customization import ( 32 | set_colorscheme, 33 | set_colorscheme_from_yaml, 34 | set_cursor_mode, 35 | set_font_from_ttf, 36 | set_font_size, 37 | set_line_numbers, 38 | set_syntax_highlighting, 39 | ) 40 | from ._editor_getters import ( 41 | get_letter_index, 42 | get_line_index, 43 | get_number_of_letters_in_line_by_index, 44 | get_number_of_letters_in_line_by_mouse, 45 | get_showable_lines, 46 | line_is_visible, 47 | ) 48 | 49 | # input handling KEYBOARD 50 | from ._input_handling_keyboard import ( 51 | handle_keyboard_arrow_down, 52 | handle_keyboard_arrow_left, 53 | handle_keyboard_arrow_right, 54 | handle_keyboard_arrow_up, 55 | handle_keyboard_backspace, 56 | handle_keyboard_delete, 57 | handle_keyboard_input, 58 | handle_keyboard_return, 59 | handle_keyboard_space, 60 | handle_keyboard_tab, 61 | insert_unicode, 62 | ) 63 | from ._input_handling_keyboard_highlight import ( 64 | get_highlighted_characters, 65 | handle_highlight_and_copy, 66 | handle_highlight_and_cut, 67 | handle_highlight_and_paste, 68 | handle_input_with_highlight, 69 | highlight_all, 70 | ) 71 | 72 | # input handling MOUSE 73 | from ._input_handling_mouse import handle_mouse_input, mouse_within_existing_lines, mouse_within_texteditor 74 | 75 | # letter operations (delete, get) 76 | from ._letter_operations import ( 77 | delete_entire_line, 78 | delete_letter_to_end, 79 | delete_letter_to_letter, 80 | delete_start_to_letter, 81 | get_entire_line, 82 | get_line_from_char_to_char, 83 | get_line_from_char_to_end, 84 | get_line_from_start_to_char, 85 | ) 86 | from ._other import jump_to_end, jump_to_start, reset_after_highlight 87 | 88 | # rendering (basic, highlighting, syntax coloring) 89 | from ._rendering import ( 90 | caret_within_texteditor, 91 | get_rect_coord_from_indizes, 92 | get_rect_coord_from_mouse, 93 | render_background_coloring, 94 | render_caret, 95 | render_line_contents, 96 | render_line_numbers, 97 | reset_text_area_to_caret, 98 | update_line_number_display, 99 | ) 100 | from ._rendering_highlighting import ( 101 | highlight_entire_line, 102 | highlight_from_letter_to_end, 103 | highlight_from_letter_to_letter, 104 | highlight_from_start_to_letter, 105 | highlight_lines, 106 | render_highlight, 107 | ) 108 | from ._rendering_syntax_coloring import get_single_color_dicts, get_syntax_coloring_dicts 109 | from ._scrollbar_vertical import display_scrollbar, render_scrollbar_vertical, scrollbar_down, scrollbar_up 110 | from ._usage import clear_text, get_text_as_list, get_text_as_string, set_text_from_list, set_text_from_string 111 | 112 | def __init__( 113 | self, 114 | offset_x, 115 | offset_y, 116 | editor_width, 117 | editor_height, 118 | screen, 119 | display_line_numbers=False, 120 | style="dark", 121 | syntax_highlighting_python=False, 122 | font_size=16, 123 | ): 124 | """Initialize the class.""" 125 | self.screen = screen 126 | 127 | # VISUALS 128 | self.editor_offset_x = offset_x 129 | self.editor_offset_y = offset_y 130 | self.editor_width = editor_width 131 | self.editor_height = editor_height 132 | self.conclusion_bar_height = 18 133 | self.ttf_path = os.path.join(current_dir, "elements/fonts/Courier.ttf") 134 | self.editor_font = pygame.font.Font(self.ttf_path, size=font_size) 135 | self.letter_height = font_size 136 | self.letter_width = self.editor_font.render(" ", 1, (0, 0, 0)).get_width() 137 | self.syntax_highlighting_python = syntax_highlighting_python 138 | 139 | # LINES (line height equals the letter height). 140 | self.max_line_counter = 0 141 | # Fill the entire editor with empty lines in the beginning 142 | self.editor_lines = ["" for _ in range(int(math.floor(self.editor_height / self.letter_height)))] 143 | self.first_showable_line_index = 0 # first line, shown at the top of the editor 144 | self.line_margin = 3 145 | self.line_height_including_margin = self.letter_height + self.line_margin 146 | self.showable_line_numbers_in_editor = int(math.floor(self.editor_height / self.line_height_including_margin)) 147 | 148 | # SCROLLBAR 149 | # TODO: replace image with PyGame drawn rect 150 | self.scrollbar_image = pygame.image.load( 151 | os.path.join(current_dir, "elements/graphics/Scroll_Bar.png") 152 | ).convert() 153 | self.scrollbar_width = 8 # must be an even number 154 | self.padding_between_edge_and_scrollbar = 2 155 | 156 | self.scrollbar: Optional[pygame.Rect] = None 157 | self.scrollbar_start_y: Optional[int] = None 158 | self.scrollbar_is_being_dragged: bool = False 159 | 160 | # LINE NUMBERS 161 | self.display_line_numbers = display_line_numbers 162 | self.line_number_width = 0 163 | if self.display_line_numbers: 164 | self.line_number_width = self.editor_font.render(" ", 1, (0, 0, 0)).get_width() * 2 165 | self.line_numbers_y = self.editor_offset_y 166 | 167 | # TEXT COORDINATES 168 | self.chosen_line_index = 0 169 | self.chosen_letter_index = 0 170 | self.line_start_y = self.editor_offset_y + self.line_margin # add initial margin to offset 171 | self.line_start_x = self.editor_offset_x 172 | if self.display_line_numbers: 173 | self.line_start_x += self.line_number_width 174 | 175 | # CURSOR - coordinates for displaying the caret while typing 176 | self.caret_display_counter = 0 177 | self.caret_display_intervals_per_second = 5 178 | self.static_cursor = False 179 | self.caret_y = self.editor_offset_y 180 | self.caret_x = self.line_start_x 181 | 182 | # click down - coordinates used to identify start-point of drag 183 | self.dragged_active = False 184 | self.dragged_finished = True 185 | self.drag_chosen_line_index_start = 0 186 | self.drag_chosen_letter_index_start = 0 187 | self.last_clickdown_cycle = 0 188 | 189 | # click up - coordinates used to identify end-point of drag 190 | self.drag_chosen_line_index_end = 0 191 | self.drag_chosen_letter_index_end = 0 192 | self.last_clickup_cycle = 0 193 | 194 | # Colors 195 | self.color_coding_background = (40, 41, 41) 196 | self.color_scrollbar = (60, 61, 61) 197 | self.color_scrollbar_background = (49, 50, 50) 198 | self.color_line_number_font = (255, 255, 255) 199 | self.color_line_number_background = (60, 61, 61) 200 | self.color_text = (255, 255, 255) 201 | self.color_caret = (255, 255, 255) 202 | 203 | self.lexer = PythonLexer(ensurenl=False) 204 | self.color_formatter = ColorFormatter() 205 | self.set_colorscheme(style) 206 | 207 | # Key input variables+ 208 | self.key_initial_delay = 300 209 | self.key_continued_interval = 30 210 | pygame.key.set_repeat(self.key_initial_delay, self.key_continued_interval) 211 | 212 | # Performance enhancing variables 213 | self.first_iteration_boolean = True 214 | self.render_line_numbers_flag = True 215 | self.click_hold_flag = False 216 | self.cycleCounter = 0 # Used to be able to tell whether a mouse-drag action has been handled already or not. 217 | 218 | self.clock = pygame.time.Clock() 219 | self.FPS = 60 # we need to limit the FPS so we don't trigger the same actions too often (e.g. deletions) 220 | self.max_line_number_rendered = len(self.editor_lines) # TODO: move to a fitting location. 221 | 222 | def display_editor(self, pygame_events, pressed_keys, mouse_x, mouse_y, mouse_pressed): 223 | """Display the editor. 224 | 225 | The function should be called once within every pygame loop. 226 | 227 | :param pygame_events: 228 | :param pressed_keys: 229 | :param mouse_x: 230 | :param mouse_y: 231 | :param mouse_pressed: 232 | """ 233 | # needs to be called within a while loop to be able to catch key/mouse input and update visuals throughout use. 234 | self.cycleCounter = self.cycleCounter + 1 235 | # first iteration 236 | if self.first_iteration_boolean: 237 | # paint entire area to avoid pixel error beneath line numbers 238 | pygame.draw.rect( 239 | self.screen, 240 | self.color_coding_background, 241 | ( 242 | self.editor_offset_x, 243 | self.editor_offset_y, 244 | self.editor_width, 245 | self.editor_height, 246 | ), 247 | ) 248 | self.first_iteration_boolean = False 249 | 250 | for event in pygame_events: # handle QUIT operation 251 | if event.type == pygame.QUIT: 252 | pygame.quit() 253 | exit() 254 | 255 | self.handle_keyboard_input(pygame_events, pressed_keys) 256 | self.handle_mouse_input(pygame_events, mouse_x, mouse_y, mouse_pressed) 257 | 258 | self.update_line_number_display() 259 | 260 | # RENDERING 1 - Background objects 261 | self.render_background_coloring() 262 | self.render_line_numbers() 263 | 264 | # RENDERING 2 - Line contents, caret 265 | self.render_highlight(mouse_x, mouse_y) 266 | if self.syntax_highlighting_python: 267 | # syntax highlighting for code 268 | line_contents = self.get_syntax_coloring_dicts() 269 | else: 270 | # single-color text 271 | line_contents = self.get_single_color_dicts() 272 | self.render_line_contents(line_contents) 273 | self.render_caret() 274 | 275 | # RENDERING 3 - scrollbar 276 | self.render_scrollbar_vertical() 277 | self.clock.tick(self.FPS) 278 | -------------------------------------------------------------------------------- /src/pygame_texteditor/__init__.py: -------------------------------------------------------------------------------- 1 | from .TextEditor import TextEditor 2 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_caret.py: -------------------------------------------------------------------------------- 1 | def set_drag_start_by_mouse(self, mouse_x, mouse_y) -> None: 2 | # get line 3 | self.drag_chosen_line_index_start = self.get_line_index(mouse_y) 4 | 5 | # end of line 6 | if self.get_number_of_letters_in_line_by_mouse(mouse_y) < self.get_letter_index( 7 | mouse_x 8 | ): 9 | self.drag_chosen_letter_index_start = len( 10 | self.editor_lines[self.drag_chosen_line_index_start] 11 | ) 12 | 13 | else: # within existing line 14 | self.drag_chosen_letter_index_start = self.get_letter_index(mouse_x) 15 | 16 | 17 | def set_drag_end_by_mouse(self, mouse_x, mouse_y) -> None: 18 | """ 19 | Compact method to set both line and letter of drag_end based on mouse coordinates. 20 | """ 21 | # set line 22 | self.set_drag_end_line_by_mouse(mouse_y) 23 | # set letter 24 | self.set_drag_end_letter_by_mouse(mouse_x) 25 | 26 | 27 | def set_drag_end_line_by_mouse(self, mouse_y) -> None: 28 | """ 29 | Sets self.drag_chosen_LineIndex_end by mouse_y. 30 | """ 31 | self.drag_chosen_line_index_end = self.get_line_index(mouse_y) 32 | 33 | 34 | def set_drag_end_letter_by_mouse(self, mouse_x) -> None: 35 | """ 36 | Sets self.drag_chosen_LetterIndex_end by mouse_x. 37 | Dependent on self.drag_chosen_LineIndex_end. 38 | """ 39 | # end of line 40 | if self.get_letter_index(mouse_x) > self.get_number_of_letters_in_line_by_index( 41 | self.drag_chosen_line_index_end 42 | ): 43 | self.drag_chosen_letter_index_end = len( 44 | self.editor_lines[self.drag_chosen_line_index_end] 45 | ) 46 | else: # within existing line 47 | self.drag_chosen_letter_index_end = self.get_letter_index(mouse_x) 48 | 49 | 50 | def set_drag_start_after_last_line(self) -> None: 51 | # select last line 52 | self.drag_chosen_line_index_start = len(self.editor_lines) - 1 53 | # select last letter of the line 54 | self.drag_chosen_letter_index_start = len( 55 | self.editor_lines[self.drag_chosen_line_index_start] 56 | ) 57 | 58 | 59 | def set_drag_start_before_first_line(self) -> None: 60 | self.drag_chosen_line_index_start = 0 61 | self.drag_chosen_letter_index_start = 0 62 | 63 | 64 | def set_drag_end_after_last_line(self) -> None: 65 | # select last line 66 | self.drag_chosen_line_index_end = len(self.editor_lines) - 1 67 | # select last letter of the line 68 | self.drag_chosen_letter_index_end = len( 69 | self.editor_lines[self.drag_chosen_line_index_end] 70 | ) 71 | 72 | 73 | def update_caret_position_by_drag_start(self) -> None: 74 | """ 75 | # Updates cursor_X and cursor_Y positions based on the start position of a dragging operation. 76 | """ 77 | # X Position 78 | self.caret_x = self.line_start_x + ( 79 | self.drag_chosen_letter_index_start * self.letter_width 80 | ) 81 | # Y Position 82 | self.caret_y = ( 83 | self.editor_offset_y 84 | + (self.drag_chosen_line_index_start * self.line_height_including_margin) 85 | - (self.first_showable_line_index * self.letter_height) 86 | ) 87 | 88 | 89 | def update_caret_position_by_drag_end(self) -> None: 90 | """ 91 | # Updates cursor_X and cursor_Y positions based on the end position of a dragging operation. 92 | """ 93 | # X Position 94 | self.caret_x = self.line_start_x + ( 95 | self.drag_chosen_letter_index_end * self.letter_width 96 | ) 97 | # Y Position 98 | self.caret_y = ( 99 | self.editor_offset_y 100 | + (self.drag_chosen_line_index_end * self.line_height_including_margin) 101 | - (self.first_showable_line_index * self.letter_height) 102 | ) 103 | 104 | 105 | def update_caret_position(self) -> None: 106 | """Update the caret position based on current position by line and letter indices. 107 | 108 | We add one pixel to the x coordinate so the caret is fully visible if it is at the start of the line. 109 | """ 110 | self.caret_x = ( 111 | self.line_start_x + (self.chosen_letter_index * self.letter_width) + 1 112 | ) 113 | self.caret_y = self.editor_offset_y + ( 114 | (self.chosen_line_index - self.first_showable_line_index) 115 | * self.line_height_including_margin 116 | ) 117 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_customization.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import pathlib 4 | import re 5 | import pygame 6 | import yaml 7 | 8 | current_dir = os.path.dirname(__file__) 9 | 10 | 11 | def set_key_repetition(self, delay=300, interval=30) -> None: 12 | """ 13 | The delay parameter is the number of milliseconds before the first repeated pygame.KEYDOWN event will be sent. 14 | After that, another pygame.KEYDOWN event will be sent every interval milliseconds. 15 | """ 16 | self.key_initial_delay = delay 17 | self.key_continued_interval = interval 18 | pygame.key.set_repeat(self.key_initial_delay, self.key_continued_interval) 19 | 20 | 21 | def set_font_from_ttf(self, absolute_path_to_ttf: str) -> None: 22 | """ 23 | Sets the font given a ttf file. 24 | 25 | Disclaimer: As the width of a letter (space) is only calculated once after setting the font_size, any fonts that are not 26 | monospace will lead to the editor not working correctly anymore, as it cannot be determined anymore, between which 27 | letters the user clicked. 28 | """ 29 | self.ttf_path = absolute_path_to_ttf 30 | 31 | # force all the font settings to re-render 32 | set_font_size(self, self.letter_height) 33 | 34 | 35 | def set_font_size(self, size=16) -> None: 36 | """Sets the given size as font size and re-calculates necessary changes.""" 37 | self.editor_font = pygame.font.Font(self.ttf_path, size=size) 38 | self.letter_height = size 39 | self.letter_width = self.editor_font.render(" ", 1, (0, 0, 0)).get_width() 40 | self.line_height_including_margin = self.letter_height + self.line_margin 41 | self.showable_line_numbers_in_editor = int(math.floor(self.editor_height / self.line_height_including_margin)) 42 | # As the font size also influences the size of the line numbers, we need to recalculate some their spacing 43 | self.max_line_number_rendered = -1 44 | self.update_line_number_display() 45 | 46 | 47 | def set_line_numbers(self, b) -> None: 48 | """ 49 | Activates/deactivates showing the line numbers in the editor 50 | """ 51 | self.display_line_numbers = b 52 | if self.display_line_numbers: 53 | self.max_line_number_rendered = -1 54 | self.update_line_number_display() 55 | else: 56 | self.line_number_width = 0 57 | self.line_start_x = self.editor_offset_x 58 | 59 | 60 | def set_syntax_highlighting(self, b) -> None: 61 | """ 62 | Activates / deactivates syntax highlighting. 63 | If activated, creates the lexer and formatter and sets the formatter's style. 64 | """ 65 | self.syntax_highlighting_python = b 66 | 67 | 68 | def set_colorscheme_from_yaml(self, path_to_yaml) -> None: 69 | def get_rgb_by_key(dict, key): 70 | return tuple([int(x) for x in re.findall(r"(\d{1,3})", dict[key])]) 71 | 72 | try: 73 | with open(path_to_yaml) as file: 74 | color_dict = yaml.load(file, Loader=yaml.FullLoader) 75 | 76 | self.color_coding_background = get_rgb_by_key(color_dict, "codingBackgroundColor") 77 | self.color_scrollbar_background = get_rgb_by_key(color_dict, "codingScrollBarBackgroundColor") 78 | self.color_line_number_font = get_rgb_by_key(color_dict, "lineNumberColor") 79 | self.color_line_number_background = get_rgb_by_key(color_dict, "lineNumberBackgroundColor") 80 | self.color_text = get_rgb_by_key(color_dict, "textColor") 81 | self.color_caret = get_rgb_by_key(color_dict, "caretColor") 82 | 83 | self.color_formatter.textColor_normal = get_rgb_by_key(color_dict, "textColor_normal") 84 | self.color_formatter.textColor_comments = get_rgb_by_key(color_dict, "textColor_comments") 85 | self.color_formatter.textColor_quotes = get_rgb_by_key(color_dict, "textColor_quotes") 86 | self.color_formatter.textColor_operators = get_rgb_by_key(color_dict, "textColor_operators") 87 | self.color_formatter.textColor_keywords = get_rgb_by_key(color_dict, "textColor_keywords") 88 | self.color_formatter.textColor_function = get_rgb_by_key(color_dict, "textColor_function") 89 | self.color_formatter.textColor_builtin = get_rgb_by_key(color_dict, "textColor_builtin") 90 | 91 | except FileNotFoundError: 92 | raise FileNotFoundError("Could not find the style file '" + path_to_yaml + "'.") 93 | except KeyError as e: 94 | raise KeyError( 95 | "Could not find all necessary color-keys in the style file '" 96 | + path_to_yaml 97 | + "'. " 98 | + "Missing key: " 99 | + str(e) 100 | ) 101 | 102 | 103 | def set_colorscheme(self, style) -> None: 104 | """ 105 | Sets the color scheme for the editor and for syntax highlighting if the latter is enabled based on 106 | one of the editor's default styles. 107 | """ 108 | if style in ("bright", "dark"): 109 | default_path = os.path.join( 110 | str(pathlib.Path(__file__).parent.absolute()), 111 | "elements", 112 | "colorstyles", 113 | style + ".yml", 114 | ) 115 | self.set_colorscheme_from_yaml(default_path) 116 | else: 117 | raise ValueError( 118 | "No default style with the name '" 119 | + style 120 | + "' available. " 121 | + "\n\tAvailable styles are 'dark' and 'bright'. \n\tFor your " 122 | + "own custom styles, use the method 'set_colorscheme_from_yaml(path_to_yaml)' " 123 | + "and have a look at the docs." 124 | ) 125 | 126 | 127 | def set_cursor_mode(self, mode: str = "blinking"): 128 | if mode == "blinking": 129 | self.static_cursor = False 130 | elif mode == "static": 131 | self.static_cursor = True 132 | else: 133 | raise ValueError(f"Value '{mode}' is not a valid cursor mode. Set either to 'blinking' or 'static'.") 134 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_editor_getters.py: -------------------------------------------------------------------------------- 1 | def get_line_index(self, mouse_y) -> int: 2 | """ 3 | Returns possible line-position of mouse -> does not take into account 4 | how many lines there actually are! 5 | """ 6 | return int( 7 | ((mouse_y - self.editor_offset_y) / self.line_height_including_margin) 8 | + self.first_showable_line_index 9 | ) 10 | 11 | 12 | def get_letter_index(self, mouse_x) -> int: 13 | """ 14 | Returns possible letter-position of mouse. 15 | 16 | The function is independent of any specific line, so we could possibly return a letter_index which 17 | is bigger than the letters in the line. 18 | Returns at least 0 to make sure it is possibly a valid index. 19 | """ 20 | letter = int((mouse_x - self.line_start_x) / self.letter_width) 21 | letter = 0 if letter < 0 else letter 22 | return letter 23 | 24 | 25 | def get_number_of_letters_in_line_by_mouse(self, mouse_y) -> int: 26 | line_index = get_line_index(self, mouse_y) 27 | return get_number_of_letters_in_line_by_index(self, line_index) 28 | 29 | 30 | def get_number_of_letters_in_line_by_index(self, index) -> int: 31 | return len(self.editor_lines[index]) 32 | 33 | 34 | def get_showable_lines(self) -> int: 35 | """ 36 | Return the number of lines which are shown. Less than maximum if less lines are in the array. 37 | """ 38 | if self.showable_line_numbers_in_editor + self.first_showable_line_index < len( 39 | self.editor_lines 40 | ): 41 | return self.showable_line_numbers_in_editor + self.first_showable_line_index 42 | else: 43 | return len(self.editor_lines) 44 | 45 | 46 | def line_is_visible(self, line) -> bool: 47 | """ 48 | Calculate whether the line is being shown in the editor 49 | """ 50 | return ( 51 | self.first_showable_line_index 52 | <= line 53 | < self.first_showable_line_index + self.showable_line_numbers_in_editor 54 | ) 55 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_input_handling_keyboard.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import pygame 4 | 5 | 6 | def handle_keyboard_input(self, pygame_events, pressed_keys) -> None: 7 | 8 | for event in pygame_events: 9 | if event.type == pygame.KEYDOWN: 10 | 11 | # ___ COMBINATION KEY INPUTS ___ 12 | # Functionality whether something is highlighted or not (highlight all / paste) 13 | if ( 14 | pressed_keys[pygame.K_LCTRL] or pressed_keys[pygame.K_RCTRL] 15 | ) and event.key == pygame.K_a: 16 | self.highlight_all() 17 | elif ( 18 | pressed_keys[pygame.K_LCTRL] or pressed_keys[pygame.K_RCTRL] 19 | ) and event.key == pygame.K_v: 20 | self.handle_highlight_and_paste() 21 | 22 | # Functionality for when something is highlighted (cut / copy) 23 | elif self.dragged_finished and self.dragged_active: 24 | if ( 25 | pressed_keys[pygame.K_LCTRL] or pressed_keys[pygame.K_RCTRL] 26 | ) and event.key == pygame.K_x: 27 | self.handle_highlight_and_cut() 28 | elif ( 29 | pressed_keys[pygame.K_LCTRL] or pressed_keys[pygame.K_RCTRL] 30 | ) and event.key == pygame.K_c: 31 | self.handle_highlight_and_copy() 32 | else: 33 | self.handle_input_with_highlight( 34 | event 35 | ) # handle char input on highlight 36 | 37 | # ___ SINGLE KEY INPUTS ___ 38 | else: 39 | self.reset_text_area_to_caret() # reset visual area to include line of caret if necessary 40 | self.chosen_letter_index = int(self.chosen_letter_index) 41 | 42 | # Detect tapping/holding of the "DELETE" and "BACKSPACE" key while something is highlighted 43 | if ( 44 | self.dragged_finished 45 | and self.dragged_active 46 | and (event.unicode == "\x08" or event.unicode == "\x7f") 47 | ): 48 | # create the uniform event for both keys so we don't have to write two functions 49 | deletion_event = pygame.event.Event( 50 | pygame.KEYDOWN, key=pygame.K_DELETE 51 | ) 52 | self.handle_input_with_highlight( 53 | deletion_event 54 | ) # delete and backspace have the same functionality 55 | 56 | # ___ DELETIONS ___ 57 | elif event.unicode == "\x08": # K_BACKSPACE 58 | self.handle_keyboard_backspace() 59 | self.reset_text_area_to_caret() # reset caret if necessary 60 | elif event.unicode == "\x7f": # K_DELETE 61 | self.handle_keyboard_delete() 62 | self.reset_text_area_to_caret() # reset caret if necessary 63 | 64 | # ___ NORMAL KEYS ___ 65 | # This covers all letters and numbers (not those on numpad). 66 | elif len(pygame.key.name(event.key)) == 1: 67 | self.insert_unicode(event.unicode) 68 | 69 | # ___ NUMPAD KEYS ___ 70 | # for the numbers, numpad must be activated (mod = 4096) 71 | elif event.mod == 4096 and 1073741913 <= event.key <= 1073741922: 72 | self.insert_unicode(event.unicode) 73 | # all other numpad keys can be triggered with & without mod 74 | elif event.key in [ 75 | pygame.K_KP_PERIOD, 76 | pygame.K_KP_DIVIDE, 77 | pygame.K_KP_MULTIPLY, 78 | pygame.K_KP_MINUS, 79 | pygame.K_KP_PLUS, 80 | pygame.K_KP_EQUALS, 81 | ]: 82 | self.insert_unicode(event.unicode) 83 | 84 | # ___ SPECIAL KEYS ___ 85 | elif event.key == pygame.K_TAB: # TABULATOR 86 | self.handle_keyboard_tab() 87 | elif event.key == pygame.K_SPACE: # SPACEBAR 88 | self.handle_keyboard_space() 89 | elif ( 90 | event.key == pygame.K_RETURN or event.key == pygame.K_KP_ENTER 91 | ): # RETURN 92 | self.handle_keyboard_return() 93 | elif event.key == pygame.K_UP: # ARROW_UP 94 | self.handle_keyboard_arrow_up() 95 | elif event.key == pygame.K_DOWN: # ARROW_DOWN 96 | self.handle_keyboard_arrow_down() 97 | elif event.key == pygame.K_RIGHT: # ARROW_RIGHT 98 | self.handle_keyboard_arrow_right() 99 | elif event.key == pygame.K_LEFT: # ARROW_LEFT 100 | self.handle_keyboard_arrow_left() 101 | 102 | else: 103 | if event.key not in [ 104 | pygame.K_RSHIFT, 105 | pygame.K_LSHIFT, 106 | pygame.K_DELETE, 107 | pygame.K_BACKSPACE, 108 | pygame.K_CAPSLOCK, 109 | pygame.K_LCTRL, 110 | pygame.K_RCTRL, 111 | ]: 112 | # We handled the keys separately 113 | # Capslock is apparently implicitly handled when using it in combination 114 | warnings.warn( 115 | "No implementation for key: " 116 | + str(pygame.key.name(event.key)), 117 | Warning, 118 | ) 119 | 120 | 121 | def insert_unicode(self, unicode) -> None: 122 | self.editor_lines[self.chosen_line_index] = ( 123 | self.editor_lines[self.chosen_line_index][: self.chosen_letter_index] 124 | + unicode 125 | + self.editor_lines[self.chosen_line_index][self.chosen_letter_index :] 126 | ) 127 | self.caret_x += self.letter_width 128 | self.chosen_letter_index += 1 129 | 130 | 131 | def handle_keyboard_backspace(self) -> None: 132 | if self.chosen_letter_index == 0 and self.chosen_line_index == 0: 133 | # First position and in the first Line -> nothing happens 134 | pass 135 | elif self.chosen_letter_index == 0 and self.chosen_line_index > 0: 136 | # One Line back if at X-Position 0 and not in the first Line 137 | 138 | # set letter and line index to newly current line 139 | self.chosen_line_index -= 1 140 | self.chosen_letter_index = len(self.editor_lines[self.chosen_line_index]) 141 | # set visual cursor one line above and at the end of the line 142 | self.caret_y -= self.line_height_including_margin 143 | self.caret_x = self.line_start_x + ( 144 | len(self.editor_lines[self.chosen_line_index]) * self.letter_width 145 | ) 146 | 147 | # take the rest of the former line into the current line 148 | self.editor_lines[self.chosen_line_index] = ( 149 | self.editor_lines[self.chosen_line_index] 150 | + self.editor_lines[self.chosen_line_index + 1] 151 | ) 152 | 153 | # delete the former line 154 | # LOGICAL lines 155 | self.editor_lines.pop(self.chosen_line_index + 1) 156 | # VISUAL lines 157 | self.render_line_numbers_flag = True 158 | 159 | # Handling of the resulting scrolling functionality of removing one line 160 | if self.first_showable_line_index > 0: 161 | if ( 162 | self.first_showable_line_index + self.showable_line_numbers_in_editor 163 | ) > len(self.editor_lines): 164 | # The scrollbar is all the way down. We delete a line, 165 | # so we have to "pull everything one visual line down" 166 | self.first_showable_line_index -= ( 167 | 1 # "pull one visual line down" (array-based) 168 | ) 169 | self.caret_y += ( 170 | self.line_height_including_margin 171 | ) # move the curser one down. (visually based) 172 | if self.chosen_line_index == (self.first_showable_line_index - 1): 173 | # Im in the first rendered line (but NOT the "0" line) and at the beginning of the line. 174 | # => move one upward, change showstartLine & cursor placement. 175 | self.first_showable_line_index -= 1 176 | self.caret_y += self.line_height_including_margin 177 | 178 | elif self.chosen_letter_index > 0: 179 | # mid-line or end of the line -> Delete a letter 180 | self.editor_lines[self.chosen_line_index] = ( 181 | self.editor_lines[self.chosen_line_index][: (self.chosen_letter_index - 1)] 182 | + self.editor_lines[self.chosen_line_index][self.chosen_letter_index :] 183 | ) 184 | 185 | self.caret_x -= self.letter_width 186 | self.chosen_letter_index -= 1 187 | else: 188 | raise ValueError( 189 | "INVALID CONSTRUCT: handle_keyboard_backspace. \ 190 | \nLine:" 191 | + str(self.chosen_line_index) 192 | + "\nLetter: " 193 | + str(self.chosen_letter_index) 194 | ) 195 | 196 | 197 | def handle_keyboard_delete(self) -> None: 198 | 199 | if self.chosen_letter_index < (len(self.editor_lines[self.chosen_line_index])): 200 | # start of the line or mid-line (Cursor stays on point), cut one letter out 201 | self.editor_lines[self.chosen_line_index] = ( 202 | self.editor_lines[self.chosen_line_index][: self.chosen_letter_index] 203 | + self.editor_lines[self.chosen_line_index][ 204 | (self.chosen_letter_index + 1) : 205 | ] 206 | ) 207 | 208 | elif self.chosen_letter_index == len(self.editor_lines[self.chosen_line_index]): 209 | # End of a line (choose next line) 210 | if self.chosen_line_index != ( 211 | len(self.editor_lines) - 1 212 | ): # NOT in the last line &(prev) at the end of the line, I cannot delete anything 213 | self.editor_lines[self.chosen_line_index] += self.editor_lines[ 214 | self.chosen_line_index + 1 215 | ] # add the contents of the next line to the current one 216 | self.editor_lines.pop( 217 | self.chosen_line_index + 1 218 | ) # delete the Strings-line in order to move the following lines one upwards 219 | self.render_line_numbers_flag = True 220 | if self.first_showable_line_index > 0: 221 | if ( 222 | self.first_showable_line_index 223 | + self.showable_line_numbers_in_editor 224 | ) > len(self.editor_lines): 225 | # The scrollbar is all the way down. 226 | # We delete a line, so we have to "pull everything one visual line down" 227 | self.first_showable_line_index -= ( 228 | 1 # "pull one visual line down" (array-based) 229 | ) 230 | self.caret_y += ( 231 | self.line_height_including_margin 232 | ) # move the curser one down. (visually based) 233 | else: 234 | raise ValueError( 235 | " INVALID CONSTRUCT: handle_keyboard_delete. \ 236 | \nLine:" 237 | + str(self.chosen_line_index) 238 | + "\nLetter: " 239 | + str(self.chosen_letter_index) 240 | ) 241 | 242 | 243 | def handle_keyboard_arrow_left(self) -> None: 244 | if self.chosen_letter_index > 0: # mid-line or end of line 245 | self.chosen_letter_index -= 1 246 | self.caret_x -= self.letter_width 247 | elif self.chosen_letter_index == 0 and self.chosen_line_index == 0: 248 | # first line, first position, nothing happens 249 | pass 250 | elif ( 251 | self.chosen_letter_index == 0 and self.chosen_line_index > 0 252 | ): # Move over into previous Line (if there is any) 253 | self.chosen_line_index -= 1 254 | self.chosen_letter_index = len( 255 | self.editor_lines[self.chosen_line_index] 256 | ) # end of previous line 257 | self.caret_x = self.line_start_x + ( 258 | len(self.editor_lines[self.chosen_line_index]) * self.letter_width 259 | ) 260 | self.caret_y -= self.line_height_including_margin 261 | if self.chosen_line_index < self.first_showable_line_index: 262 | # handling scroll functionality if necessary (moved above shown lines) 263 | self.first_showable_line_index -= 1 264 | self.caret_y += self.line_height_including_margin 265 | self.render_line_numbers_flag = True 266 | 267 | 268 | def handle_keyboard_arrow_right(self) -> None: 269 | if self.chosen_letter_index < (len(self.editor_lines[self.chosen_line_index])): 270 | # mid-line or start of the line 271 | self.chosen_letter_index += 1 272 | self.caret_x += self.letter_width 273 | elif self.chosen_letter_index == len( 274 | self.editor_lines[self.chosen_line_index] 275 | ) and not (self.chosen_line_index == (len(self.editor_lines) - 1)): 276 | # end of line => move over into the start of the next line 277 | 278 | self.chosen_letter_index = 0 279 | self.chosen_line_index += 1 280 | self.caret_x = self.line_start_x 281 | self.caret_y += self.line_height_including_margin 282 | if self.chosen_line_index > ( 283 | self.first_showable_line_index + self.showable_line_numbers_in_editor - 1 284 | ): 285 | # handling scroll functionality if necessary (moved below showed lines) 286 | self.first_showable_line_index += 1 287 | self.caret_y -= self.line_height_including_margin 288 | self.render_line_numbers_flag = True 289 | 290 | 291 | def handle_keyboard_arrow_down(self) -> None: 292 | if self.chosen_line_index < (len(self.editor_lines) - 1): 293 | # Not in the last line, downward movement possible 294 | self.chosen_line_index += 1 295 | self.caret_y += self.line_height_including_margin 296 | 297 | if len(self.editor_lines[self.chosen_line_index]) < self.chosen_letter_index: 298 | # reset letter-index to the end 299 | self.chosen_letter_index = len(self.editor_lines[self.chosen_line_index]) 300 | self.caret_x = ( 301 | len(self.editor_lines[self.chosen_line_index]) * self.letter_width 302 | ) + self.line_start_x 303 | 304 | if self.chosen_line_index > ( 305 | self.first_showable_line_index + self.showable_line_numbers_in_editor - 1 306 | ): 307 | # handle scrolling functionality if necessary (moved below shown lines) 308 | self.scrollbar_down() 309 | 310 | elif self.chosen_line_index == (len(self.editor_lines) - 1): 311 | # In the last line and want to jump to its end. 312 | self.chosen_letter_index = len( 313 | self.editor_lines[self.chosen_line_index] 314 | ) # end of the line 315 | self.caret_x = self.line_start_x + ( 316 | len(self.editor_lines[self.chosen_line_index]) * self.letter_width 317 | ) 318 | 319 | 320 | def handle_keyboard_arrow_up(self) -> None: 321 | if self.chosen_line_index == 0: 322 | # first line, cannot go upwards, so we go to the first position 323 | self.chosen_letter_index = 0 324 | self.caret_x = self.line_start_x 325 | 326 | elif self.chosen_line_index > 0: 327 | # subsequent lines, upwards movement possible 328 | self.chosen_line_index -= 1 329 | self.caret_y -= self.line_height_including_margin 330 | 331 | if len(self.editor_lines[self.chosen_line_index]) < self.chosen_letter_index: 332 | # less letters in this line, reset toward the end of the line (to the left) 333 | self.chosen_letter_index = len(self.editor_lines[self.chosen_line_index]) 334 | self.caret_x = ( 335 | len(self.editor_lines[self.chosen_line_index]) 336 | ) * self.letter_width + self.line_start_x 337 | 338 | if ( 339 | self.chosen_line_index < self.first_showable_line_index 340 | ): # scroll up one line 341 | self.scrollbar_up() 342 | 343 | 344 | def handle_keyboard_tab(self) -> None: 345 | for x in range(0, 4): # insert 4 spaces 346 | self.handle_keyboard_space() 347 | 348 | 349 | def handle_keyboard_space(self) -> None: 350 | # insert 1 space 351 | self.editor_lines[self.chosen_line_index] = ( 352 | self.editor_lines[self.chosen_line_index][: self.chosen_letter_index] 353 | + " " 354 | + self.editor_lines[self.chosen_line_index][self.chosen_letter_index :] 355 | ) 356 | 357 | self.caret_x += self.letter_width 358 | self.chosen_letter_index += 1 359 | 360 | 361 | def handle_keyboard_return(self) -> None: 362 | # Get "transfer letters" behind cursor up to the end of the line to next line 363 | # If the cursor is at the end of the line, transferString is an empty String ("") 364 | transferString = self.editor_lines[self.chosen_line_index][ 365 | self.chosen_letter_index : 366 | ] 367 | 368 | # Remove transfer letters from the current line 369 | self.editor_lines[self.chosen_line_index] = self.editor_lines[ 370 | self.chosen_line_index 371 | ][: self.chosen_letter_index] 372 | 373 | # set logical cursor indizes and add a new line 374 | self.chosen_line_index += 1 375 | self.chosen_letter_index = 0 376 | self.caret_x = ( 377 | self.line_start_x + 1 378 | ) # reset cursor to start of the line, plus 1 pixel to be visible. 379 | 380 | # insert empty line 381 | self.editor_lines.insert(self.chosen_line_index, "") # logical line 382 | 383 | # Edit the new line -> append transfer letters 384 | self.editor_lines[self.chosen_line_index] = ( 385 | self.editor_lines[self.chosen_line_index] + transferString 386 | ) 387 | self.render_line_numbers_flag = True 388 | 389 | # handle scrolling functionality 390 | if self.chosen_line_index > ( 391 | self.showable_line_numbers_in_editor - 1 392 | ): # Last row, visual representation moves down 393 | self.first_showable_line_index += 1 394 | else: # not in last row, put courser one line down without changing the shown line numbers 395 | self.caret_y += self.line_height_including_margin 396 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_input_handling_keyboard_highlight.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pyperclip 3 | 4 | 5 | def handle_input_with_highlight(self, input_event) -> None: 6 | """ 7 | Handles key-downs after a drag operation was finished and the highlighted area (drag) is still active. 8 | For arrow keys we merely jump to the destination. 9 | For other character-keys we remove the highlighted area and replace it (also over multiple lines) 10 | with the chosen letter. 11 | """ 12 | # for readability & maintainability we use shorter variable names 13 | line_start = self.drag_chosen_line_index_start 14 | line_end = self.drag_chosen_line_index_end 15 | letter_start = self.drag_chosen_letter_index_start 16 | letter_end = self.drag_chosen_letter_index_end 17 | 18 | if self.dragged_finished and self.dragged_active: 19 | if input_event.key in ( 20 | pygame.K_DOWN, 21 | pygame.K_UP, 22 | pygame.K_RIGHT, 23 | pygame.K_LEFT, 24 | ): 25 | # deselect highlight 26 | if input_event.key == pygame.K_DOWN: 27 | self.jump_to_end(line_start, line_end, letter_start, letter_end) 28 | elif input_event.key == pygame.K_UP: 29 | self.jump_to_start(line_start, line_end, letter_start, letter_end) 30 | elif input_event.key == pygame.K_RIGHT: 31 | self.jump_to_end(line_start, line_end, letter_start, letter_end) 32 | elif input_event.key == pygame.K_LEFT: 33 | self.jump_to_start(line_start, line_end, letter_start, letter_end) 34 | self.reset_after_highlight() 35 | 36 | elif input_event.key in ( 37 | pygame.K_RSHIFT, 38 | pygame.K_LSHIFT, 39 | pygame.K_CAPSLOCK, 40 | pygame.K_RCTRL, 41 | pygame.K_LCTRL, 42 | ): 43 | pass # nothing happens, we wait for the second key with which it is being used in combination 44 | 45 | else: # other key -> delete highlighted area and insert key (if not esc/delete) 46 | if line_start == line_end: # delete in single line 47 | if ( 48 | letter_start > letter_end 49 | ): # swap variables based on left/right highlight to make code more readable 50 | letter_start, letter_end = letter_end, letter_start 51 | self.delete_letter_to_letter(line_start, letter_start, letter_end) 52 | 53 | else: # multi-line delete 54 | if ( 55 | line_start > line_end 56 | ): # swap variables based on up/downward highlight to make code more readable 57 | line_start, line_end = line_end, line_start 58 | letter_start, letter_end = letter_end, letter_start 59 | 60 | for i, line_number in enumerate(range(line_start, line_end + 1)): 61 | if i == 0: # first line 62 | self.delete_letter_to_end( 63 | line_start, letter_start 64 | ) # delete right side from start 65 | elif i < (line_end - line_start): 66 | self.delete_entire_line( 67 | line_start + 1 68 | ) # stays at line_start +1 as we delete on the fly (!) 69 | else: # last line 70 | self.delete_start_to_letter( 71 | line_start + 1, letter_end 72 | ) # delete left side of new last line 73 | 74 | # join rest of start/end lines into new line in multiline delete 75 | self.editor_lines[line_start] = ( 76 | self.editor_lines[line_start] + self.editor_lines[line_start + 1] 77 | ) 78 | self.delete_entire_line( 79 | line_start + 1 80 | ) # after copying contents, we need to delete the other line 81 | 82 | # set caret and rerender line_numbers 83 | self.chosen_line_index = ( 84 | line_start if line_start <= line_end else line_end 85 | ) # start for single_line 86 | self.chosen_letter_index = ( 87 | letter_start if line_start <= line_end else letter_end 88 | ) 89 | self.render_line_numbers_flag = True 90 | self.reset_after_highlight() 91 | 92 | # insert key unless delete/backspace 93 | if input_event.key not in (pygame.K_DELETE, pygame.K_BACKSPACE): 94 | self.insert_unicode(input_event.unicode) 95 | 96 | 97 | def handle_highlight_and_paste(self): 98 | """ 99 | Paste clipboard into cursor position. 100 | Replace highlighted area if highlight, else normal insert. 101 | """ 102 | 103 | # DELETE highlighted section if something is highlighted 104 | if self.dragged_finished and self.dragged_active: 105 | delete_event = pygame.event.Event( 106 | pygame.KEYDOWN, key=pygame.K_DELETE 107 | ) # create artifical delete event 108 | self.handle_input_with_highlight(delete_event) 109 | 110 | # PASTE from clipboard 111 | paste_string = pyperclip.paste() 112 | line_split = paste_string.split("\r\n") # split into lines 113 | if len(line_split) == 1: # no linebreaks 114 | self.editor_lines[self.chosen_line_index] = ( 115 | self.editor_lines[self.chosen_line_index][: self.chosen_letter_index] 116 | + line_split[0] 117 | + self.editor_lines[self.chosen_line_index][self.chosen_letter_index :] 118 | ) 119 | 120 | self.chosen_letter_index = self.chosen_letter_index + len(line_split[0]) 121 | 122 | else: 123 | rest_of_line = self.editor_lines[self.chosen_line_index][ 124 | self.chosen_letter_index : 125 | ] # store for later 126 | for i, line in enumerate(line_split): 127 | if i == 0: # first line to insert 128 | self.editor_lines[self.chosen_line_index] = ( 129 | self.editor_lines[self.chosen_line_index][ 130 | : self.chosen_letter_index 131 | ] 132 | + line 133 | ) 134 | elif i < len(line_split) - 1: # middle line -> insert new line! 135 | self.editor_lines[ 136 | self.chosen_line_index + i : self.chosen_line_index + i 137 | ] = [line] 138 | else: # last line 139 | self.editor_lines[ 140 | self.chosen_line_index + i : self.chosen_line_index + i 141 | ] = [line + rest_of_line] 142 | self.chosen_letter_index = len(line) 143 | self.chosen_line_index = self.chosen_line_index + i 144 | 145 | self.update_caret_position() 146 | self.render_line_numbers_flag = True 147 | 148 | 149 | def handle_highlight_and_copy(self): 150 | """ 151 | Copy highlighted String into clipboard if anything is highlighted, else no action. 152 | """ 153 | copy_string = self.get_highlighted_characters() 154 | pyperclip.copy(copy_string) 155 | 156 | 157 | def handle_highlight_and_cut(self): 158 | """ 159 | Copy highlighted String into clipboard if anything is highlighted, else no action. 160 | Delete highlighted part of the text. 161 | """ 162 | # Copy functionality 163 | copy_string = self.get_highlighted_characters() # copy characters 164 | pyperclip.copy(copy_string) 165 | 166 | # Cut / delete functionality 167 | delete_event = pygame.event.Event( 168 | pygame.KEYDOWN, key=pygame.K_DELETE 169 | ) # create artifical event 170 | self.handle_input_with_highlight(delete_event) 171 | 172 | self.update_caret_position() 173 | 174 | 175 | def highlight_all(self): 176 | """ 177 | Highlight entire text. 178 | """ 179 | # set artifical drag and cursor position 180 | self.set_drag_start_before_first_line() 181 | self.set_drag_end_after_last_line() 182 | self.update_caret_position_by_drag_end() 183 | # activate highlight 184 | self.dragged_finished = True 185 | self.dragged_active = True 186 | 187 | 188 | def get_highlighted_characters(self) -> str: 189 | """ 190 | Returns the highlighted characters (single- and multiple-line) from the editor (self.line_string_list) 191 | """ 192 | if self.dragged_finished and self.dragged_active: 193 | line_start = self.drag_chosen_line_index_start 194 | line_end = self.drag_chosen_line_index_end 195 | letter_start = self.drag_chosen_letter_index_start 196 | letter_end = self.drag_chosen_letter_index_end 197 | 198 | if self.drag_chosen_line_index_start == self.drag_chosen_line_index_end: 199 | # single-line highlight 200 | return self.get_line_from_char_to_char( 201 | self.drag_chosen_line_index_start, 202 | self.drag_chosen_letter_index_start, 203 | self.drag_chosen_letter_index_end, 204 | ) 205 | 206 | else: # multi-line highlight 207 | if ( 208 | line_start > line_end 209 | ): # swap variables based on up/downward highlight to make code more readable 210 | line_start, line_end = line_end, line_start 211 | letter_start, letter_end = letter_end, letter_start 212 | 213 | # loop through highlighted lines 214 | copied_chars = "" 215 | for i, line_index in enumerate(range(line_start, line_end + 1)): 216 | if i == 0: # first line 217 | copied_chars = self.get_line_from_char_to_end( 218 | line_index, letter_start 219 | ) 220 | elif i < len(range(line_start, line_end)): # middle line 221 | copied_chars = ( 222 | copied_chars + "\r\n" + self.get_entire_line(line_index) 223 | ) 224 | else: # last line 225 | copied_chars = ( 226 | copied_chars 227 | + "\r\n" 228 | + self.get_line_from_start_to_char(line_index, letter_end) 229 | ) 230 | 231 | return copied_chars 232 | else: 233 | return "" 234 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_input_handling_mouse.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import pygame 4 | 5 | from ._scrollbar_vertical import scrollbar_up 6 | 7 | 8 | def handle_mouse_input( 9 | self, 10 | pygame_events: list, 11 | mouse_x: int, 12 | mouse_y: int, 13 | mouse_pressed: Tuple[int, int, int], 14 | ) -> None: 15 | """ 16 | Handles mouse input based on mouse events (Buttons down/up + coordinates). 17 | Handles drag-and-drop-select as well as single-click. 18 | The code only differentiates the single-click only as so far, that 19 | the DOWN-event is on the same position as the UP-event. 20 | 21 | Implemented so far: 22 | - left-click (selecting as drag-and-drop or single-click) 23 | - mouse-wheel (scrolling) 24 | TODO: 25 | - right-click 26 | 27 | :param list pygame_events: 28 | :param int mouse_x: 29 | :param int mouse_y: 30 | :param Tuple[int, int, int] mouse_pressed: 31 | """ 32 | 33 | # ___ MOUSE ___ # 34 | for event in pygame_events: 35 | # ___ MOUSE CLICKING DOWN ___ # 36 | # Scrollbar-handling 37 | if event.type == pygame.MOUSEBUTTONDOWN and not self.mouse_within_texteditor( 38 | mouse_x, mouse_y 39 | ): 40 | if self.scrollbar is not None: 41 | if self.scrollbar.collidepoint(mouse_x, mouse_y): 42 | self.scrollbar_start_y = mouse_y 43 | self.scrollbar_is_being_dragged = True 44 | 45 | # Mouse scrolling wheel should only work if it is within the coding area (excluding scrollbar area) 46 | if event.type == pygame.MOUSEBUTTONDOWN and self.mouse_within_texteditor( 47 | mouse_x, mouse_y 48 | ): 49 | # ___ MOUSE SCROLLING ___ # 50 | if event.button == 4 and self.first_showable_line_index > 0: 51 | self.scrollbar_up() 52 | elif ( 53 | event.button == 5 54 | and self.first_showable_line_index 55 | + self.showable_line_numbers_in_editor 56 | < len(self.editor_lines) 57 | ): 58 | self.scrollbar_down() 59 | 60 | # ___ MOUSE LEFT CLICK DOWN ___ # 61 | elif event.button == 1: # left mouse button 62 | if not self.click_hold_flag: 63 | # in order not to have the mouse move around after a click, 64 | # we need to disable this function until we RELEASE it. 65 | self.last_clickdown_cycle = self.cycleCounter 66 | self.click_hold_flag = True 67 | self.dragged_active = True 68 | self.dragged_finished = False 69 | if self.mouse_within_texteditor(mouse_x, mouse_y): # editor area 70 | if self.mouse_within_existing_lines( 71 | mouse_y 72 | ): # in area of existing lines 73 | self.set_drag_start_by_mouse(mouse_x, mouse_y) 74 | else: # clicked below the existing lines 75 | self.set_drag_start_after_last_line() 76 | self.update_caret_position_by_drag_start() 77 | else: # mouse outside of editor, don't care. 78 | pass 79 | 80 | # ___ MOUSE LEFT CLICK UP ___ # 81 | if event.type == pygame.MOUSEBUTTONUP and event.button == 1: 82 | 83 | self.scrollbar_is_being_dragged = False # reset scroll (if necessary) 84 | 85 | if self.click_hold_flag: 86 | # mouse dragging only with left mouse up 87 | # mouse-up only valid if we registered a mouse-down within the editor via click_hold earlier 88 | 89 | self.last_clickup_cycle = self.cycleCounter 90 | self.click_hold_flag = False 91 | 92 | if self.mouse_within_texteditor(mouse_x, mouse_y): # editor area 93 | if self.mouse_within_existing_lines( 94 | mouse_y 95 | ): # in area of existing lines 96 | self.set_drag_end_by_mouse(mouse_x, mouse_y) 97 | else: # clicked beneath the existing lines 98 | self.set_drag_end_after_last_line() 99 | self.update_caret_position_by_drag_end() 100 | 101 | else: # mouse-up outside of editor 102 | if mouse_y < self.editor_offset_y: 103 | # Mouse-up above editor -> set to first visible line 104 | self.drag_chosen_line_index_end = self.first_showable_line_index 105 | elif mouse_y > ( 106 | self.editor_offset_y 107 | + self.editor_height 108 | - self.conclusion_bar_height 109 | ): 110 | # Mouse-up below the editor -> set to last visible line 111 | if ( 112 | len(self.editor_lines) 113 | >= self.showable_line_numbers_in_editor 114 | ): 115 | self.drag_chosen_line_index_end = ( 116 | self.first_showable_line_index 117 | + self.showable_line_numbers_in_editor 118 | - 1 119 | ) 120 | else: 121 | self.drag_chosen_line_index_end = len(self.editor_lines) - 1 122 | else: # mouse left or right of the editor outside 123 | self.set_drag_end_line_by_mouse(mouse_y) 124 | # Now we can determine the letter based on mouse_x (and selected line within the function) 125 | self.set_drag_end_letter_by_mouse(mouse_x) 126 | 127 | # _______ CHECK FOR MOUSE DRAG AND HANDLE CLICK _______ # 128 | if (self.last_clickup_cycle - self.last_clickdown_cycle) >= 0: 129 | # Clicked the mouse lately and has not been handled yet. 130 | # To differentiate between click and drag we check whether the down-click 131 | # is on the same letter and line as the up-click! 132 | # We set the boolean variables here and handle the caret positioning. 133 | if ( 134 | self.drag_chosen_line_index_end == self.drag_chosen_line_index_start 135 | and self.drag_chosen_letter_index_end 136 | == self.drag_chosen_letter_index_start 137 | ): 138 | self.dragged_active = ( 139 | False # no letters are actually selected -> Actual click 140 | ) 141 | else: 142 | self.dragged_active = True # Actual highlight 143 | 144 | self.dragged_finished = ( 145 | True # we finished the highlighting operation either way 146 | ) 147 | 148 | # handle caret positioning 149 | self.chosen_line_index = self.drag_chosen_line_index_end 150 | self.chosen_letter_index = self.drag_chosen_letter_index_end 151 | self.update_caret_position() 152 | 153 | # reset after upclick so we don't execute this block again and again. 154 | self.last_clickdown_cycle = 0 155 | self.last_clickup_cycle = -1 156 | 157 | # Scrollbar - Dragging 158 | if mouse_pressed[0] == 1 and self.scrollbar_is_being_dragged: 159 | # left mouse is being pressed after click on scrollbar 160 | if mouse_y < self.scrollbar_start_y and self.first_showable_line_index > 0: 161 | # dragged higher 162 | self.scrollbar_up() 163 | elif ( 164 | mouse_y > self.scrollbar_start_y 165 | and self.first_showable_line_index + self.showable_line_numbers_in_editor 166 | < len(self.editor_lines) 167 | ): 168 | # dragged lower 169 | self.scrollbar_down() 170 | 171 | 172 | def mouse_within_texteditor(self, mouse_x: int, mouse_y: int) -> bool: 173 | """ 174 | Returns True if the given coordinates are within the text-editor area of the pygame window, otherwise False. 175 | 176 | :param int mouse_x: 177 | :param int mouse_y: 178 | :return bool: 179 | """ 180 | return self.editor_offset_x + self.line_number_width < mouse_x < ( 181 | self.editor_offset_x + self.editor_width - self.scrollbar_width 182 | ) and self.editor_offset_y < mouse_y < ( 183 | self.editor_height + self.editor_offset_y - self.conclusion_bar_height 184 | ) 185 | 186 | 187 | def mouse_within_existing_lines(self, mouse_y): 188 | """ 189 | Returns True if the given Y-coordinate is within the height of the text-editor's existing lines. 190 | Returns False if the coordinate is below existing lines or outside the editor. 191 | """ 192 | return ( 193 | self.editor_offset_y 194 | < mouse_y 195 | < self.editor_offset_y + (self.letter_height * len(self.editor_lines)) 196 | ) 197 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_letter_operations.py: -------------------------------------------------------------------------------- 1 | def delete_letter_to_end(self, line, letter) -> None: 2 | """ 3 | Deletes within a line from a letter by index to the end of the line. 4 | """ 5 | self.editor_lines[line] = self.editor_lines[line][:letter] 6 | 7 | 8 | def delete_start_to_letter(self, line, letter) -> None: 9 | """ 10 | Deletes within a line from the start of the line to a letter by index.. 11 | """ 12 | self.editor_lines[line] = self.editor_lines[line][letter:] 13 | 14 | 15 | def delete_letter_to_letter(self, line, letter_start, letter_end) -> None: 16 | """ 17 | Deletes within a line from a letter to a letter by index.. 18 | """ 19 | self.editor_lines[line] = ( 20 | self.editor_lines[line][:letter_start] + self.editor_lines[line][letter_end:] 21 | ) 22 | 23 | 24 | def delete_entire_line(self, line) -> None: 25 | """ 26 | Deletes an entire line 27 | """ 28 | self.editor_lines.pop(line) 29 | 30 | 31 | def get_entire_line(self, line_index) -> str: 32 | return self.editor_lines[line_index] 33 | 34 | 35 | def get_line_from_start_to_char(self, line_index, char_index) -> str: 36 | return self.editor_lines[line_index][0:char_index] 37 | 38 | 39 | def get_line_from_char_to_end(self, line_index, char_index) -> str: 40 | return self.editor_lines[line_index][char_index:] 41 | 42 | 43 | def get_line_from_char_to_char(self, line_index, char1, char2) -> str: 44 | if char1 < char2: 45 | return self.editor_lines[line_index][char1:char2] 46 | else: 47 | return self.editor_lines[line_index][char2:char1] 48 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_other.py: -------------------------------------------------------------------------------- 1 | def jump_to_start(self, line_start, line_end, letter_start, letter_end) -> None: 2 | """ 3 | Chosen LineIndex set to start of highlighted area 4 | """ 5 | if line_start <= line_end: 6 | # downward highlight or the same line 7 | self.chosen_line_index = line_start 8 | self.chosen_letter_index = letter_start 9 | else: # upward highlight 10 | self.chosen_line_index = line_end 11 | self.chosen_letter_index = letter_end 12 | 13 | 14 | def jump_to_end(self, line_start, line_end, letter_start, letter_end) -> None: 15 | """ 16 | Chosen LineIndex set to end of highlighted area 17 | """ 18 | if line_start <= line_end: 19 | # downward highlight or the same line 20 | self.chosen_line_index = line_end 21 | self.chosen_letter_index = letter_end 22 | else: # upward highlight 23 | self.chosen_line_index = line_start 24 | self.chosen_letter_index = letter_start 25 | 26 | 27 | def reset_after_highlight(self) -> None: 28 | """ 29 | Reset caret, clickdown_cycles and dragged booleans. 30 | """ 31 | self.dragged_active = False # deactivate highlight 32 | self.dragged_finished = True # highlight is finished 33 | self.update_caret_position() # update caret position to chosen_Index (Line+Letter) 34 | self.last_clickdown_cycle = 0 # reset drag-cycle 35 | self.last_clickup_cycle = -1 36 | self.render_line_numbers_flag = True 37 | 38 | if len(self.editor_lines) <= self.showable_line_numbers_in_editor: 39 | self.first_showable_line_index = 0 # update first showable line 40 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_rendering.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Tuple 2 | 3 | import pygame 4 | 5 | 6 | def get_rect_coord_from_mouse(self, mouse_x, mouse_y) -> Tuple[int, int]: 7 | """Return x and y pixel-coordinates for the position of the mouse.""" 8 | line = self.get_line_index(mouse_y) 9 | letter = self.get_letter_index(mouse_x) 10 | return self.get_rect_coord_from_indizes(line, letter) 11 | 12 | 13 | def get_rect_coord_from_indizes(self, line, letter) -> Tuple[int, int]: 14 | """Return x and y pixel-coordinates for line and letter by index.""" 15 | line_coord = self.editor_offset_y + ( 16 | self.line_height_including_margin * (line - self.first_showable_line_index) 17 | ) 18 | letter_coord = self.line_start_x + (letter * self.letter_width) 19 | return letter_coord, line_coord 20 | 21 | 22 | def render_background_coloring(self) -> None: 23 | """Render background color of the text area.""" 24 | bg_left = self.editor_offset_x + self.line_number_width 25 | bg_top = self.editor_offset_y 26 | bg_width = self.editor_width - self.line_number_width 27 | bg_height = self.editor_height 28 | pygame.draw.rect( 29 | self.screen, 30 | self.color_coding_background, 31 | (bg_left, bg_top, bg_width, bg_height), 32 | ) 33 | 34 | 35 | def render_line_numbers(self) -> None: 36 | """Render the line numbers next to the text area. 37 | 38 | While background rendering is done for all "line-slots" (to overpaint remaining "old" numbers without lines) 39 | we render line-numbers only for existing string-lines. 40 | """ 41 | if self.display_line_numbers and self.render_line_numbers_flag: 42 | # Render background for the line numbers. 43 | rect_measurements_background = ( 44 | self.editor_offset_x, 45 | self.editor_offset_y, 46 | self.line_number_width, 47 | self.editor_height, 48 | ) 49 | pygame.draw.rect( 50 | self.screen, self.color_line_number_background, rect_measurements_background 51 | ) 52 | 53 | # Render the actual line numbers 54 | line_numbers_y = self.editor_offset_y # init for first line 55 | for x in range( 56 | self.first_showable_line_index, 57 | self.first_showable_line_index + self.showable_line_numbers_in_editor, 58 | ): 59 | if x < self.get_showable_lines(): 60 | rect_measurements = ( 61 | self.editor_offset_x, 62 | line_numbers_y, 63 | self.line_number_width, 64 | self.line_height_including_margin, 65 | ) 66 | # x + 1 in order to start with line number "1" instead of index "0". 67 | text = self.editor_font.render( 68 | # Prepend line number with spaces to achieve a uniform display: 69 | # - the min is 2 characters 70 | # - the max is the length of the max line number represented as a string. 71 | str(x + 1).rjust(max(2, len(str(len(self.editor_lines))))), 72 | 1, 73 | self.color_line_number_font, 74 | ) 75 | text_rect = text.get_rect() 76 | text_rect.center = pygame.Rect(rect_measurements).center 77 | self.screen.blit(text, text_rect) # render on center of bg block 78 | line_numbers_y += self.line_height_including_margin 79 | self.render_line_numbers_flag = False 80 | 81 | 82 | def render_line_contents(self, line_contents: Dict) -> None: 83 | # Preparation of the rendering: 84 | y_coordinate = self.line_start_y 85 | first_line = self.first_showable_line_index 86 | if self.showable_line_numbers_in_editor < len(self.editor_lines): 87 | # we got more text than we are able to display 88 | last_line = ( 89 | self.first_showable_line_index + self.showable_line_numbers_in_editor 90 | ) 91 | else: 92 | last_line = len(self.editor_lines) 93 | 94 | # Actual line rendering based on dict-keys 95 | for line_list in line_contents[first_line:last_line]: 96 | xcoord = self.line_start_x 97 | for dict in line_list: 98 | surface = self.editor_font.render( 99 | dict["chars"], 1, dict["color"] 100 | ) # create surface 101 | self.screen.blit( 102 | surface, (xcoord, y_coordinate) 103 | ) # blit surface onto screen 104 | xcoord = xcoord + ( 105 | len(dict["chars"]) * self.letter_width 106 | ) # next line-part prep 107 | 108 | y_coordinate += self.line_height_including_margin # next line prep 109 | 110 | 111 | def caret_within_texteditor(self) -> bool: 112 | """Check whether the caret's coordinates are within the visible text area. 113 | 114 | If the caret can possibly be in a line which is not currently displayed after using the mouse wheel for scrolling. 115 | Test for 'self.editor_offset_Y <= self.cursor_Y' as the caret can have the exact same Y-coordinate as the offset if 116 | the caret is in the first line. 117 | 118 | :return bool: 119 | """ 120 | return ( 121 | # left border 122 | self.editor_offset_x + self.line_number_width < self.caret_x < 123 | # right border 124 | ( 125 | self.editor_offset_x 126 | + self.editor_width 127 | - self.scrollbar_width 128 | - self.padding_between_edge_and_scrollbar 129 | ) 130 | and 131 | # top border 132 | self.editor_offset_y <= self.caret_y < 133 | # bottom border 134 | (self.editor_height + self.editor_offset_y - self.conclusion_bar_height) 135 | ) 136 | 137 | 138 | def render_caret(self) -> None: 139 | """ 140 | Called every frame. Displays a cursor for x frames, then none for x frames. Only displayed if line in which 141 | caret resides is visible and there is no active dragging operation going on. 142 | Dependent on FPS -> 5 intervals per second 143 | Creates 'blinking' animation 144 | """ 145 | self.caret_display_counter += 1 146 | if self.static_cursor or ( 147 | self.caret_display_counter 148 | > (self.FPS / self.caret_display_intervals_per_second) 149 | and self.caret_within_texteditor() 150 | and self.dragged_finished 151 | ): 152 | self.caret_display_counter = self.caret_display_counter % ( 153 | (self.FPS / self.caret_display_intervals_per_second) * 2 154 | ) 155 | pygame.draw.line( 156 | surface=self.screen, 157 | color=self.color_caret, 158 | start_pos=(self.caret_x, self.caret_y), 159 | end_pos=(self.caret_x, self.caret_y + self.letter_height), 160 | width=1, 161 | ) 162 | 163 | 164 | def reset_text_area_to_caret(self) -> None: 165 | """ 166 | Reset visual area to include the line of caret if it is currently not visible. This function ensures 167 | that whenever we type, the line in which the caret resides becomes visible, even after scrolling. 168 | """ 169 | if self.chosen_line_index < self.first_showable_line_index: 170 | # Caret is above the visible area, "jump" to show the line of the caret 171 | self.first_showable_line_index = self.chosen_line_index 172 | self.render_line_numbers_flag = True 173 | self.update_caret_position() 174 | elif self.chosen_line_index > ( 175 | self.first_showable_line_index + self.showable_line_numbers_in_editor - 1 176 | ): 177 | # caret is below visible area, "jump" to show the line of the caret 178 | self.first_showable_line_index = ( 179 | self.chosen_line_index - self.showable_line_numbers_in_editor + 1 180 | ) 181 | self.render_line_numbers_flag = True 182 | self.update_caret_position() 183 | 184 | 185 | def update_line_number_display(self) -> None: 186 | """Update the display of the line numbers, specifically background width if the number of lines changed. 187 | 188 | If the existing space is not wide enough anymore, e.g. if we go from 99 to 100 lines or from 999 to 1000. 189 | :return None: 190 | """ 191 | if len(self.editor_lines) != self.max_line_number_rendered: 192 | self.line_number_width = self.editor_font.render( 193 | " ", 1, (0, 0, 0) 194 | ).get_width() * max(2, len(str(len(self.editor_lines)))) 195 | self.line_start_x = self.editor_offset_x + self.line_number_width 196 | self.max_line_number_rendered = len(self.editor_lines) 197 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_rendering_highlighting.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | def render_highlight(self, mouse_x, mouse_y) -> None: 5 | """ 6 | Renders highlighted area: 7 | 1. During drag-action -> area starts at drag_start and follows mouse 8 | 2. After drag-action -> area stays confined to selected area by drag_start and drag_end 9 | """ 10 | 11 | if self.dragged_active: # some text is highlighted or being highlighted 12 | line_start = self.drag_chosen_line_index_start 13 | letter_start = self.drag_chosen_letter_index_start 14 | 15 | if ( 16 | self.dragged_finished 17 | ): # highlighting operation is done, user "clicked-up" with the left mouse button 18 | line_end = self.drag_chosen_line_index_end 19 | letter_end = self.drag_chosen_letter_index_end 20 | if letter_end < 0: 21 | letter_end = 0 22 | self.highlight_lines( 23 | line_start, letter_start, line_end, letter_end 24 | ) # Actual highlighting 25 | 26 | else: # active highlighting -> highlighted area follows mouse movements 27 | line_end = self.get_line_index(mouse_y) 28 | letter_end = self.get_letter_index(mouse_x) 29 | # adapt line_end: if mouse_y below showable area / existing lines, 30 | if line_end >= self.get_showable_lines(): 31 | line_end = ( 32 | self.get_showable_lines() - 1 33 | ) # select last showable/existing line as line_end 34 | 35 | # Correct letter_end based on cursor position / letters in the cursor's line 36 | if letter_end < 0: # cursor is left of the line 37 | letter_end = 0 38 | elif letter_end > len(self.editor_lines[line_end]): 39 | letter_end = len(self.editor_lines[line_end]) 40 | 41 | self.highlight_lines( 42 | line_start, letter_start, line_end, letter_end 43 | ) # Actual highlighting 44 | 45 | 46 | def highlight_lines(self, line_start, letter_start, line_end, letter_end) -> None: 47 | """ 48 | Highlights multiple lines based on indizies of starting & ending lines and letters. 49 | """ 50 | if line_start == line_end: # single-line highlight 51 | self.highlight_from_letter_to_letter(line_start, letter_start, letter_end) 52 | else: # multi-line highlighting 53 | if ( 54 | line_start > line_end 55 | ): # swap variables based on up/downward highlight to make code more readable 56 | line_start, line_end = line_end, line_start 57 | letter_start, letter_end = letter_end, letter_start 58 | 59 | for i, line_number in enumerate( 60 | range(line_start, line_end + 1) 61 | ): # for each line 62 | if i == 0: # first line 63 | self.highlight_from_letter_to_end( 64 | line_number, letter_start 65 | ) # right leaning highlight 66 | elif i < len(range(line_start, line_end)): # middle line 67 | self.highlight_entire_line(line_number) 68 | else: # last line 69 | self.highlight_from_start_to_letter( 70 | line_number, letter_end 71 | ) # left leaning highlight 72 | 73 | 74 | def highlight_from_letter_to_end(self, line, letter) -> None: 75 | """ 76 | Highlight from a specific letter by index to the end of a line. 77 | """ 78 | if self.line_is_visible(line): 79 | x1, y1 = self.get_rect_coord_from_indizes(line, letter) 80 | x2, y2 = self.get_rect_coord_from_indizes(line, len(self.editor_lines[line])) 81 | pygame.draw.rect( 82 | self.screen, 83 | (0, 0, 0), 84 | pygame.Rect(x1, y1, x2 - x1, self.line_height_including_margin), 85 | ) 86 | 87 | 88 | def highlight_from_start_to_letter(self, line, letter) -> None: 89 | """ 90 | Highlight from the beginning of a line to a specific letter by index. 91 | """ 92 | if self.line_is_visible(line): 93 | x1, y1 = self.get_rect_coord_from_indizes(line, 0) 94 | x2, y2 = self.get_rect_coord_from_indizes(line, letter) 95 | pygame.draw.rect( 96 | self.screen, 97 | (0, 0, 0), 98 | pygame.Rect(x1, y1, x2 - x1, self.line_height_including_margin), 99 | ) 100 | 101 | 102 | def highlight_entire_line(self, line) -> None: 103 | """ 104 | Full highlight of the entire line - first until last letter. 105 | """ 106 | if self.line_is_visible(line): 107 | x1, y1 = self.get_rect_coord_from_indizes(line, 0) 108 | x2, y2 = self.get_rect_coord_from_indizes(line, len(self.editor_lines[line])) 109 | pygame.draw.rect( 110 | self.screen, 111 | (0, 0, 0), 112 | pygame.Rect(x1, y1, x2 - x1, self.line_height_including_margin), 113 | ) 114 | 115 | 116 | def highlight_from_letter_to_letter(self, line, letter_start, letter_end) -> None: 117 | """ 118 | Highlights within a single line from letter to letter by indizes. 119 | """ 120 | if self.line_is_visible(line): 121 | x1, y1 = self.get_rect_coord_from_indizes(line, letter_start) 122 | x2, y2 = self.get_rect_coord_from_indizes(line, letter_end) 123 | pygame.draw.rect( 124 | self.screen, 125 | (0, 0, 0), 126 | pygame.Rect(x1, y1, x2 - x1, self.line_height_including_margin), 127 | ) 128 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_rendering_syntax_coloring.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | 4 | def get_single_color_dicts(self) -> List[List[Dict]]: 5 | """Convert the editor text into a list of single color tokens which can be used to be rendered. 6 | 7 | Every line is one sublist. Since only one color is applied, an entire line can be a single token. 8 | 9 | :return List[List[Dict]]: 10 | """ 11 | return [ 12 | [{"chars": line, "type": "normal", "color": self.color_text}] 13 | for line in self.editor_lines 14 | ] 15 | 16 | 17 | def get_syntax_coloring_dicts(self) -> List[List[Dict]]: 18 | """Convert the editor text into a list of differently colored tokens which can be used to be rendered. 19 | 20 | Every line is one sublist which contains different tokens (dicts) based on its contents. 21 | We create a dict for every token of every line and include the characters, a pseudo token-type and the color. 22 | 23 | :return List[List[Dict]]: a list containing lines (=a list of tokens (=dicts)). 24 | """ 25 | return [ 26 | self.color_formatter.format(self.lexer.get_tokens(line)) 27 | for line in self.editor_lines 28 | ] 29 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_scrollbar_vertical.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | def render_scrollbar_vertical(self) -> None: 5 | self.display_scrollbar() 6 | 7 | 8 | def display_scrollbar(self): 9 | # Scroll Bar 10 | if len(self.editor_lines) <= self.showable_line_numbers_in_editor: 11 | self.scrollbar = None 12 | return # if scrollbar is not needed, don't show. 13 | 14 | # scroll bar is a fraction of the space 15 | w = self.scrollbar_width 16 | x = ( 17 | self.editor_offset_x 18 | + self.editor_width 19 | - self.scrollbar_width 20 | - self.padding_between_edge_and_scrollbar 21 | ) 22 | y = int( 23 | self.editor_offset_y 24 | + (self.scrollbar_width / 2) 25 | + ( 26 | (self.editor_height - self.scrollbar_width) 27 | * ((self.first_showable_line_index * 1.0) / len(self.editor_lines)) 28 | ) 29 | ) 30 | # Entire height minus the diameter as the ends are rounded. Multiplied with the fraction of displayable lines. 31 | h = int( 32 | (self.editor_height - (2 * self.scrollbar_width)) 33 | * ((self.showable_line_numbers_in_editor * 1.0) / len(self.editor_lines)) 34 | ) 35 | self.scrollbar = pygame.Rect(x, y, w, h) 36 | 37 | pygame.draw.circle( 38 | self.screen, self.color_scrollbar, (int(x + (w / 2)), y), int(w / 2) 39 | ) # top round corner 40 | pygame.draw.rect( 41 | self.screen, self.color_scrollbar, self.scrollbar 42 | ) # actual scrollbar 43 | pygame.draw.circle( 44 | self.screen, self.color_scrollbar, (int(x + (w / 2)), y + h), int(w / 2) 45 | ) # bottom round corner 46 | 47 | 48 | def scrollbar_up(self) -> None: 49 | self.first_showable_line_index -= 1 50 | self.caret_y += self.line_height_including_margin 51 | self.render_line_numbers_flag = True 52 | 53 | 54 | def scrollbar_down(self) -> None: 55 | self.first_showable_line_index += 1 56 | self.caret_y -= self.line_height_including_margin 57 | self.render_line_numbers_flag = True 58 | -------------------------------------------------------------------------------- /src/pygame_texteditor/_usage.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | 4 | import pygame 5 | 6 | 7 | def get_text_as_string(self) -> str: 8 | """ 9 | Returns the entire text of the editor as a single string. 10 | Linebreak characters are used to differentiate between lines. 11 | :param self: Texteditor-Class 12 | :return: String 13 | """ 14 | return "\n".join(self.editor_lines) 15 | 16 | 17 | def get_text_as_list(self) -> List: 18 | """ 19 | Returns the text in it's logical form as a list of lines. 20 | :param self: Texteditor-Class 21 | :return: List of lines containing the text. Lines cane be empty Strings. 22 | """ 23 | return self.editor_lines 24 | 25 | 26 | def clear_text(self) -> None: 27 | """ 28 | Clears the textarea. 29 | :param self: Texteditor-Class 30 | :return: None 31 | """ 32 | # Create lines 33 | self.editor_lines = [ 34 | "" for _ in range(int(math.floor(self.editor_height / self.letter_height))) 35 | ] 36 | self.first_showable_line_index = 0 37 | 38 | # reset caret 39 | self.first_iteration_boolean = True # redraws background 40 | self.render_line_numbers_flag = True 41 | self.chosen_line_index = 0 42 | self.chosen_letter_index = 0 43 | self.dragged_active = False 44 | self.dragged_finished = True 45 | self.scrollbar_is_being_dragged = False 46 | self.drag_chosen_line_index_start = 0 47 | self.drag_chosen_letter_index_start = 0 48 | self.drag_chosen_line_index_end = 0 49 | self.drag_chosen_letter_index_end = 0 50 | self.last_clickdown_cycle = 0 51 | self.last_clickup_cycle = 0 52 | self.cycleCounter = 0 53 | self.update_caret_position() 54 | self.cycleCounter = 0 55 | 56 | 57 | def set_text_from_list(self, text_list) -> None: 58 | """ 59 | Sets the text of the editor based on a list of strings. Each item in the list represents one line. 60 | """ 61 | self.clear_text() 62 | self.editor_lines = text_list 63 | self.render_line_numbers_flag = True 64 | 65 | 66 | def set_text_from_string(self, string) -> None: 67 | """ 68 | Sets the text of the editor based on a string. Linebreak characters are parsed. 69 | """ 70 | self.clear_text() 71 | self.editor_lines = string.split("\n") 72 | self.render_line_numbers_flag = True 73 | -------------------------------------------------------------------------------- /src/pygame_texteditor/elements/colorstyles/bright.yml: -------------------------------------------------------------------------------- 1 | # Editor colors 2 | codingBackgroundColor: (255, 255, 255) 3 | codingScrollBarBackgroundColor: (49, 50, 50) 4 | lineNumberColor: (255, 255, 255) 5 | lineNumberBackgroundColor: (60, 61, 61) 6 | textColor: (255, 255, 255) 7 | caretColor: (255, 255, 255) 8 | # Syntax colors 9 | textColor_normal: (0, 255, 255) 10 | textColor_comments: (119, 115, 115) 11 | textColor_quotes: (227, 215, 115) 12 | textColor_operators: (237, 36, 36) 13 | textColor_keywords: (237, 36, 36) 14 | textColor_function: (50, 150, 36) 15 | textColor_builtin: (50, 50, 136) 16 | -------------------------------------------------------------------------------- /src/pygame_texteditor/elements/colorstyles/dark.yml: -------------------------------------------------------------------------------- 1 | # Editor colors 2 | codingBackgroundColor: (40,41,35) 3 | codingScrollBarBackgroundColor: (50, 51, 45) 4 | lineNumberColor: (255, 255, 255) 5 | lineNumberBackgroundColor: (70, 71, 71) 6 | textColor: (255, 255, 255) 7 | caretColor: (255, 255, 255) 8 | # Syntax colors 9 | textColor_normal: (255, 255, 255) 10 | textColor_comments: (119, 115, 115) 11 | textColor_quotes: (231, 219, 116) 12 | textColor_operators: (249, 36, 114) 13 | textColor_keywords: (237, 36, 36) 14 | textColor_function: (166, 226, 43) 15 | textColor_builtin: (104, 216, 239) 16 | -------------------------------------------------------------------------------- /src/pygame_texteditor/elements/fonts/Courier.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CribberSix/pygame-texteditor/3b08a6c928d4cb7b1f05ccd0928799e531547867/src/pygame_texteditor/elements/fonts/Courier.ttf -------------------------------------------------------------------------------- /src/pygame_texteditor/elements/graphics/Scroll_Bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CribberSix/pygame-texteditor/3b08a6c928d4cb7b1f05ccd0928799e531547867/src/pygame_texteditor/elements/graphics/Scroll_Bar.png --------------------------------------------------------------------------------