crystalwindow 1.4.7.5__py3-none-any.whl → 2.9.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crystalwindow/FileHelper.py +83 -18
- crystalwindow/Icons/default_icon.png +0 -0
- crystalwindow/__init__.py +47 -5
- crystalwindow/assets.py +11 -14
- crystalwindow/draw_rects.py +16 -31
- crystalwindow/draw_text_helper.py +1 -0
- crystalwindow/draw_tool.py +49 -0
- crystalwindow/gametests/__init__.py +14 -0
- crystalwindow/gametests/__main__.py +23 -0
- crystalwindow/gametests/gravitytest.py +47 -0
- crystalwindow/gametests/guitesting.py +56 -0
- crystalwindow/gametests/sandbox.py +49 -0
- crystalwindow/gametests/windowtesting.py +5 -0
- crystalwindow/gui.py +11 -6
- crystalwindow/gui_ext.py +19 -15
- crystalwindow/math.py +40 -0
- crystalwindow/sprites.py +26 -14
- crystalwindow/ver_warner.py +54 -0
- crystalwindow/window.py +190 -175
- crystalwindow-2.9.8.dist-info/METADATA +256 -0
- crystalwindow-2.9.8.dist-info/RECORD +31 -0
- crystalwindow-2.9.8.dist-info/licenses/LICENSE +21 -0
- crystalwindow-1.4.7.5.dist-info/METADATA +0 -19
- crystalwindow-1.4.7.5.dist-info/RECORD +0 -20
- {crystalwindow-1.4.7.5.dist-info → crystalwindow-2.9.8.dist-info}/WHEEL +0 -0
- {crystalwindow-1.4.7.5.dist-info → crystalwindow-2.9.8.dist-info}/top_level.txt +0 -0
crystalwindow/gui_ext.py
CHANGED
|
@@ -5,28 +5,33 @@ from .gui import Button
|
|
|
5
5
|
|
|
6
6
|
class Toggle:
|
|
7
7
|
def __init__(self, rect, value=False, color=(200,200,200), hover_color=(255,255,255)):
|
|
8
|
+
# rect = (x, y, w, h)
|
|
8
9
|
self.rect = rect
|
|
9
10
|
self.value = value
|
|
10
11
|
self.color = color
|
|
11
12
|
self.hover_color = hover_color
|
|
12
13
|
self.hovered = False
|
|
13
14
|
|
|
14
|
-
def update(self, win):
|
|
15
|
+
def update(self, win: Window):
|
|
15
16
|
mx, my = win.mouse_pos
|
|
16
|
-
x,y,w,h = self.rect
|
|
17
|
+
x, y, w, h = self.rect
|
|
17
18
|
self.hovered = x <= mx <= x+w and y <= my <= y+h
|
|
18
|
-
if self.hovered and win.
|
|
19
|
+
if self.hovered and win.mouse_pressed(1):
|
|
19
20
|
self.value = not self.value
|
|
20
21
|
|
|
21
|
-
def draw(self, win):
|
|
22
|
+
def draw(self, win: Window):
|
|
22
23
|
draw_color = self.hover_color if self.hovered else self.color
|
|
23
24
|
win.draw_rect(draw_color, self.rect)
|
|
25
|
+
# optional: small “on” indicator
|
|
26
|
+
if self.value:
|
|
27
|
+
inner = (self.rect[0]+4, self.rect[1]+4, self.rect[2]-8, self.rect[3]-8)
|
|
28
|
+
win.draw_rect((0,255,0), inner)
|
|
24
29
|
|
|
25
|
-
from .window import Window
|
|
26
30
|
|
|
27
31
|
class Slider:
|
|
28
32
|
def __init__(self, rect, min_val=0, max_val=100, value=50, color=(150,150,150), handle_radius=10):
|
|
29
|
-
|
|
33
|
+
# rect = (x, y, w, h)
|
|
34
|
+
self.rect = rect
|
|
30
35
|
self.min_val = min_val
|
|
31
36
|
self.max_val = max_val
|
|
32
37
|
self.value = value
|
|
@@ -38,25 +43,24 @@ class Slider:
|
|
|
38
43
|
mx, my = win.mouse_pos
|
|
39
44
|
x, y, w, h = self.rect
|
|
40
45
|
|
|
41
|
-
# Start dragging if clicked on the slider track or handle
|
|
42
46
|
handle_x = x + ((self.value - self.min_val) / (self.max_val - self.min_val)) * w
|
|
43
47
|
handle_y = y + h // 2
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
|
|
49
|
+
# start drag if mouse clicks inside slider bar area
|
|
50
|
+
if win.mouse_pressed(1):
|
|
51
|
+
if not self.dragging and (x <= mx <= x+w and y <= my <= y+h):
|
|
46
52
|
self.dragging = True
|
|
47
53
|
else:
|
|
48
|
-
self.dragging = False
|
|
54
|
+
self.dragging = False # stop drag when released
|
|
49
55
|
|
|
50
|
-
#
|
|
56
|
+
# update value while dragging
|
|
51
57
|
if self.dragging:
|
|
52
|
-
rel_x = max(0, min(mx - x, w))
|
|
58
|
+
rel_x = max(0, min(mx - x, w)) # clamp
|
|
53
59
|
self.value = self.min_val + (rel_x / w) * (self.max_val - self.min_val)
|
|
54
60
|
|
|
55
61
|
def draw(self, win: Window):
|
|
56
62
|
x, y, w, h = self.rect
|
|
57
|
-
# draw slider track
|
|
58
63
|
win.draw_rect(self.color, (x, y + h//2 - 2, w, 4))
|
|
59
|
-
# draw handle as circle
|
|
60
64
|
handle_x = x + ((self.value - self.min_val) / (self.max_val - self.min_val)) * w
|
|
61
65
|
handle_y = y + h // 2
|
|
62
|
-
win.draw_circle((255,0,0), (int(handle_x), int(handle_y)), self.handle_radius)
|
|
66
|
+
win.draw_circle((255, 0, 0), (int(handle_x), int(handle_y)), self.handle_radius)
|
crystalwindow/math.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class Mathematics:
|
|
2
|
+
"""
|
|
3
|
+
A class used to perform basic mathematical operations.
|
|
4
|
+
|
|
5
|
+
Methods:
|
|
6
|
+
--------
|
|
7
|
+
add(num1, num2) : Adds two numbers.
|
|
8
|
+
subtract(num1, num2) : Subtracts two numbers.
|
|
9
|
+
multiply(num1, num2) : Multiplies two numbers.
|
|
10
|
+
divide(num1, num2) : Divides two numbers.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def add(x, y):
|
|
15
|
+
"""Adds two numbers together"""
|
|
16
|
+
return x + y
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def subtract(num1, num2):
|
|
20
|
+
"""Subtracts two numbers."""
|
|
21
|
+
return num1 - num2
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def multiply(num1, num2):
|
|
25
|
+
"""Multiplies two numbers."""
|
|
26
|
+
return num1 * num2
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def divide(num1, num2):
|
|
30
|
+
"""
|
|
31
|
+
Divides two numbers.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
------
|
|
35
|
+
ZeroDivisionError : If num2 is zero.
|
|
36
|
+
"""
|
|
37
|
+
if num2 == 0:
|
|
38
|
+
raise ZeroDivisionError("Cannot divide by zero!")
|
|
39
|
+
return num1 / num2
|
|
40
|
+
|
crystalwindow/sprites.py
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
#Sprites.py
|
|
2
|
+
from tkinter import PhotoImage
|
|
2
3
|
|
|
3
4
|
class Sprite:
|
|
4
|
-
def __init__(self,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
def __init__(self, pos, size=None, image=None, color=(255, 0, 0)):
|
|
6
|
+
"""
|
|
7
|
+
pos: (x, y) top-left
|
|
8
|
+
size: (w, h) if you want a plain colored rect
|
|
9
|
+
image: PhotoImage (Tkinter)
|
|
10
|
+
color: fill color if size is used
|
|
11
|
+
"""
|
|
12
|
+
self.pos = pos
|
|
13
|
+
self.x, self.y = pos
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
self.
|
|
15
|
+
if image is not None:
|
|
16
|
+
self.image = image
|
|
17
|
+
self.width = image.width()
|
|
18
|
+
self.height = image.height()
|
|
19
|
+
elif size is not None:
|
|
20
|
+
self.width, self.height = size
|
|
21
|
+
self.image = None
|
|
22
|
+
self.color = color
|
|
13
23
|
else:
|
|
14
|
-
|
|
24
|
+
raise ValueError("Sprite needs either size or image")
|
|
15
25
|
|
|
16
|
-
def draw(self,
|
|
26
|
+
def draw(self, win):
|
|
27
|
+
"""Draw sprite on given Window"""
|
|
17
28
|
if self.image:
|
|
18
|
-
|
|
29
|
+
win.canvas.create_image(self.x, self.y, anchor="nw", image=self.image)
|
|
19
30
|
else:
|
|
20
|
-
|
|
31
|
+
win.draw_rect(self.color, (self.x, self.y, self.width, self.height))
|
|
21
32
|
|
|
22
33
|
def move(self, dx, dy):
|
|
34
|
+
"""Move sprite by dx/dy"""
|
|
23
35
|
self.x += dx
|
|
24
36
|
self.y += dy
|
|
25
|
-
self.
|
|
37
|
+
self.pos = (self.x, self.y)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from importlib.metadata import version as get_version, PackageNotFoundError
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
def check_for_update(package_name="crystalwindow"):
|
|
7
|
+
"""
|
|
8
|
+
Checks PyPI for updates.
|
|
9
|
+
Warns every run if outdated.
|
|
10
|
+
Only skips check when version is already latest.
|
|
11
|
+
"""
|
|
12
|
+
try:
|
|
13
|
+
# get current version
|
|
14
|
+
try:
|
|
15
|
+
current_version = get_version(package_name)
|
|
16
|
+
except PackageNotFoundError:
|
|
17
|
+
print(f"(⚠️ Package '{package_name}' not found)")
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
cache_file = os.path.join(os.path.expanduser("~"), f".{package_name}_version_cache.json")
|
|
21
|
+
|
|
22
|
+
# load cache
|
|
23
|
+
if os.path.exists(cache_file):
|
|
24
|
+
with open(cache_file, "r") as f:
|
|
25
|
+
cache = json.load(f)
|
|
26
|
+
else:
|
|
27
|
+
cache = {}
|
|
28
|
+
|
|
29
|
+
# get newest version from PyPI
|
|
30
|
+
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
31
|
+
response = requests.get(url, timeout=3)
|
|
32
|
+
|
|
33
|
+
if response.status_code == 200:
|
|
34
|
+
latest_version = response.json()["info"]["version"]
|
|
35
|
+
|
|
36
|
+
if latest_version == current_version:
|
|
37
|
+
# only print once per version
|
|
38
|
+
if cache.get("last_checked") != current_version:
|
|
39
|
+
print(f"✅ Up to date! ver = {current_version}.")
|
|
40
|
+
cache["last_checked"] = current_version
|
|
41
|
+
with open(cache_file, "w") as f:
|
|
42
|
+
json.dump(cache, f)
|
|
43
|
+
else:
|
|
44
|
+
# always warn until updated
|
|
45
|
+
print(f"\n⚠️ Yo dev! '{package_name}' is outdated ({current_version})")
|
|
46
|
+
print(f"👉 Newest is {latest_version}! Run:")
|
|
47
|
+
print(f" pip install --upgrade {package_name}")
|
|
48
|
+
print(f"Or peep: https://pypi.org/project/{package_name}/{latest_version}/\n")
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
print("(⚠️ PyPI request failed, skipping version check)")
|
|
52
|
+
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"(⚠️ Version check failed: {e})")
|
crystalwindow/window.py
CHANGED
|
@@ -1,203 +1,218 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
import contextlib
|
|
6
|
-
import base64, io
|
|
1
|
+
# === Window Management (tkinter version) ===
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
import base64, io, os, sys, contextlib
|
|
7
4
|
|
|
5
|
+
# === Boot ===
|
|
6
|
+
main_file = os.path.basename(sys.argv[0])
|
|
7
|
+
print(f"Welcome Future Dev!, running.. '{main_file}'")
|
|
8
|
+
|
|
9
|
+
# Base64 fallback logo (optional)
|
|
8
10
|
DEFAULT_LOGO_BASE64 = "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"
|
|
9
11
|
|
|
12
|
+
def decode_logo():
|
|
13
|
+
"""Decode the base64 logo and return a Tk PhotoImage."""
|
|
14
|
+
try:
|
|
15
|
+
logo_data = base64.b64decode(DEFAULT_LOGO_BASE64)
|
|
16
|
+
with io.BytesIO(logo_data) as buf:
|
|
17
|
+
return tk.PhotoImage(data=buf.read())
|
|
18
|
+
except Exception:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
10
22
|
class Window:
|
|
11
|
-
#
|
|
23
|
+
# === Keymap to simplify key usage ===
|
|
12
24
|
KEY_MAP = {
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"keyg":
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"f7": pygame.K_F7,
|
|
70
|
-
"f8": pygame.K_F8,
|
|
71
|
-
"f9": pygame.K_F9,
|
|
72
|
-
"f10": pygame.K_F10,
|
|
73
|
-
"f11": pygame.K_F11,
|
|
74
|
-
"f12": pygame.K_F12,
|
|
75
|
-
"insert": pygame.K_INSERT,
|
|
76
|
-
"delete": pygame.K_DELETE,
|
|
77
|
-
"home": pygame.K_HOME,
|
|
78
|
-
"end": pygame.K_END,
|
|
79
|
-
"pageup": pygame.K_PAGEUP,
|
|
80
|
-
"pagedown": pygame.K_PAGEDOWN,
|
|
81
|
-
"capslock": pygame.K_CAPSLOCK,
|
|
82
|
-
"numlock": pygame.K_NUMLOCK,
|
|
83
|
-
"scrolllock": pygame.K_SCROLLOCK,
|
|
25
|
+
# Arrows & Movement
|
|
26
|
+
"left": "Left",
|
|
27
|
+
"right": "Right",
|
|
28
|
+
"up": "Up",
|
|
29
|
+
"down": "Down",
|
|
30
|
+
|
|
31
|
+
# Common control keys
|
|
32
|
+
"space": "space",
|
|
33
|
+
"enter": "Return",
|
|
34
|
+
"backspace": "BackSpace",
|
|
35
|
+
"tab": "Tab",
|
|
36
|
+
"escape": "Escape",
|
|
37
|
+
"Rshift": "Shift_R",
|
|
38
|
+
"Lshift": "Shift_L",
|
|
39
|
+
"Rctrl": "Control_R",
|
|
40
|
+
"Lctrl": "Control_L",
|
|
41
|
+
"alt": "Alt_L",
|
|
42
|
+
|
|
43
|
+
# Letters
|
|
44
|
+
"keya": "a", "keyb": "b", "keyc": "c", "keyd": "d",
|
|
45
|
+
"keye": "e", "keyf": "f", "keyg": "g", "keyh": "h",
|
|
46
|
+
"keyi": "i", "keyj": "j", "keyk": "k", "keyl": "l",
|
|
47
|
+
"keym": "m", "keyn": "n", "keyo": "o", "keyp": "p",
|
|
48
|
+
"keyq": "q", "keyr": "r", "keys": "s", "keyt": "t",
|
|
49
|
+
"keyu": "u", "keyv": "v", "keyw": "w", "keyx": "x",
|
|
50
|
+
"keyy": "y", "keyz": "z",
|
|
51
|
+
|
|
52
|
+
# Numbers
|
|
53
|
+
"0": "0", "1": "1", "2": "2", "3": "3", "4": "4",
|
|
54
|
+
"5": "5", "6": "6", "7": "7", "8": "8", "9": "9",
|
|
55
|
+
|
|
56
|
+
# Function keys
|
|
57
|
+
"f1": "F1", "f2": "F2", "f3": "F3", "f4": "F4",
|
|
58
|
+
"f5": "F5", "f6": "F6", "f7": "F7", "f8": "F8",
|
|
59
|
+
"f9": "F9", "f10": "F10", "f11": "F11", "f12": "F12",
|
|
60
|
+
|
|
61
|
+
# Editing and navigation
|
|
62
|
+
"insert": "Insert",
|
|
63
|
+
"delete": "Delete",
|
|
64
|
+
"home": "Home",
|
|
65
|
+
"end": "End",
|
|
66
|
+
"pageup": "Prior", # "Page Up" in Tkinter is "Prior"
|
|
67
|
+
"pagedown": "Next", # "Page Down" is "Next"
|
|
68
|
+
|
|
69
|
+
# Lock keys
|
|
70
|
+
"capslock": "Caps_Lock",
|
|
71
|
+
"numlock": "Num_Lock",
|
|
72
|
+
"scrolllock": "Scroll_Lock",
|
|
73
|
+
|
|
74
|
+
# Symbols
|
|
75
|
+
"+": "plus",
|
|
76
|
+
"-": "minus",
|
|
77
|
+
"/": "slash",
|
|
78
|
+
"backslash": "backslash",
|
|
79
|
+
"*": "asterisk",
|
|
80
|
+
"=": "equal"
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
|
|
87
83
|
def __init__(self, width=640, height=480, title="Game Window", icon=None):
|
|
88
|
-
# temporarily silence pygame print
|
|
89
|
-
with open(os.devnull, "w") as f, contextlib.redirect_stdout(f):
|
|
90
|
-
pygame.init() # <- no more hello message
|
|
91
|
-
|
|
92
84
|
self.width = width
|
|
93
85
|
self.height = height
|
|
94
86
|
self.title = title
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
print(f"⚠️ Icon file not found: {icon_path}")
|
|
110
|
-
else:
|
|
111
|
-
pygame.display.set_icon(icon)
|
|
87
|
+
|
|
88
|
+
self.root = tk.Tk()
|
|
89
|
+
self.root.title(title)
|
|
90
|
+
self.root.protocol("WM_DELETE_WINDOW", self.quit)
|
|
91
|
+
|
|
92
|
+
# === ICON HANDLING ===
|
|
93
|
+
if icon and os.path.exists(icon):
|
|
94
|
+
try:
|
|
95
|
+
img = tk.PhotoImage(file=icon)
|
|
96
|
+
self.root.iconphoto(True, img)
|
|
97
|
+
except Exception:
|
|
98
|
+
print("⚠️ Icon load failed, using default")
|
|
99
|
+
img = decode_logo()
|
|
100
|
+
if img: self.root.iconphoto(True, img)
|
|
112
101
|
else:
|
|
113
|
-
|
|
114
|
-
if
|
|
115
|
-
|
|
116
|
-
pygame.display.set_icon(img)
|
|
117
|
-
else:
|
|
118
|
-
icon_bytes = base64.b64decode(DEFAULT_LOGO_BASE64)
|
|
119
|
-
icon_file = io.BytesIO(icon_bytes)
|
|
120
|
-
img = image.load(icon_file).convert_alpha()
|
|
121
|
-
pygame.display.set_icon(img)
|
|
102
|
+
logo = decode_logo()
|
|
103
|
+
if logo:
|
|
104
|
+
self.root.iconphoto(True, logo)
|
|
122
105
|
|
|
123
|
-
|
|
106
|
+
# === Canvas setup ===
|
|
107
|
+
self.canvas = tk.Canvas(self.root, width=width, height=height, bg="#141432", highlightthickness=0)
|
|
108
|
+
self.canvas.pack(fill="both", expand=True)
|
|
109
|
+
|
|
110
|
+
# === State vars ===
|
|
124
111
|
self.running = True
|
|
125
112
|
self.keys = {}
|
|
126
|
-
self.mouse_pos = (0,0)
|
|
127
|
-
self.
|
|
113
|
+
self.mouse_pos = (0, 0)
|
|
114
|
+
self._mouse_pressed = (False, False, False)
|
|
115
|
+
self._bg_color = (20, 20, 50)
|
|
128
116
|
self.draw_calls = []
|
|
129
117
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
118
|
+
# === Event bindings ===
|
|
119
|
+
self.root.bind("<KeyPress>", self._on_keydown)
|
|
120
|
+
self.root.bind("<KeyRelease>", self._on_keyup)
|
|
121
|
+
self.root.bind("<Motion>", self._on_mousemove)
|
|
122
|
+
self.root.bind("<ButtonPress>", self._on_mousedown)
|
|
123
|
+
self.root.bind("<ButtonRelease>", self._on_mouseup)
|
|
124
|
+
|
|
125
|
+
# === Input ===
|
|
126
|
+
def _on_keydown(self, event):
|
|
127
|
+
self.keys[event.keysym] = True
|
|
128
|
+
|
|
129
|
+
def _on_keyup(self, event):
|
|
130
|
+
self.keys[event.keysym] = False
|
|
131
|
+
|
|
132
|
+
def _on_mousemove(self, event):
|
|
133
|
+
self.mouse_pos = (event.x, event.y)
|
|
134
|
+
|
|
135
|
+
def _on_mousedown(self, event):
|
|
136
|
+
if event.num in [1, 2, 3]:
|
|
137
|
+
pressed = list(self._mouse_pressed)
|
|
138
|
+
pressed[event.num - 1] = True
|
|
139
|
+
self._mouse_pressed = tuple(pressed)
|
|
140
|
+
|
|
141
|
+
def _on_mouseup(self, event):
|
|
142
|
+
if event.num in [1, 2, 3]:
|
|
143
|
+
pressed = list(self._mouse_pressed)
|
|
144
|
+
pressed[event.num - 1] = False
|
|
145
|
+
self._mouse_pressed = tuple(pressed)
|
|
146
|
+
|
|
147
|
+
# === Helpers ===
|
|
148
|
+
def key_pressed(self, key):
|
|
133
149
|
if isinstance(key, str):
|
|
134
|
-
key = self.KEY_MAP.get(key,
|
|
135
|
-
if key is None:
|
|
136
|
-
return False
|
|
150
|
+
key = self.KEY_MAP.get(key, key)
|
|
137
151
|
return self.keys.get(key, False)
|
|
138
152
|
|
|
139
|
-
def
|
|
140
|
-
return self.
|
|
153
|
+
def mouse_pressed(self, button=1):
|
|
154
|
+
return self._mouse_pressed[button - 1]
|
|
155
|
+
|
|
156
|
+
# === Drawing ===
|
|
157
|
+
def fill(self, color):
|
|
158
|
+
"""Fill background with a color"""
|
|
159
|
+
if isinstance(color, str):
|
|
160
|
+
if color.startswith("#"):
|
|
161
|
+
fill_color = color
|
|
162
|
+
else:
|
|
163
|
+
fill_color = f"#{color}"
|
|
164
|
+
elif isinstance(color, tuple):
|
|
165
|
+
fill_color = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
|
|
166
|
+
else:
|
|
167
|
+
fill_color = "#000000"
|
|
168
|
+
|
|
169
|
+
self._bg_color = fill_color
|
|
170
|
+
self.canvas.delete("all")
|
|
171
|
+
self.canvas.configure(bg=fill_color)
|
|
141
172
|
|
|
142
|
-
# === font system ===
|
|
143
|
-
def get_font(self, name="Arial", size=16, bold=False, italic=False):
|
|
144
|
-
if not hasattr(self, "font_cache"):
|
|
145
|
-
self.font_cache = {}
|
|
146
|
-
key = (name, size, bold, italic)
|
|
147
|
-
if key not in self.font_cache:
|
|
148
|
-
try:
|
|
149
|
-
font = pygame.font.SysFont(name, size, bold, italic)
|
|
150
|
-
except:
|
|
151
|
-
font = pygame.font.Font(None, size)
|
|
152
|
-
self.font_cache[key] = font
|
|
153
|
-
return self.font_cache[key]
|
|
154
|
-
|
|
155
|
-
def draw_text(self, text, font="Arial", size=16, color=(255,255,255), pos=(0,0), bold=False, italic=False):
|
|
156
|
-
f = self.get_font(font, size, bold, italic)
|
|
157
|
-
surf = f.render(text, True, color)
|
|
158
|
-
self.screen.blit(surf, pos)
|
|
159
|
-
|
|
160
|
-
# === drawing helpers ===a
|
|
161
173
|
def draw_rect(self, color, rect):
|
|
162
|
-
|
|
174
|
+
x, y, w, h = rect
|
|
175
|
+
self.draw_calls.append(("rect", color, x, y, w, h))
|
|
163
176
|
|
|
164
177
|
def draw_circle(self, color, pos, radius):
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
elif event.type == pygame.MOUSEBUTTONUP:
|
|
183
|
-
self.mouse_pressed = pygame.mouse.get_pressed()
|
|
184
|
-
elif event.type == pygame.MOUSEMOTION:
|
|
185
|
-
self.mouse_pos = event.pos
|
|
186
|
-
|
|
178
|
+
x, y = pos
|
|
179
|
+
self.draw_calls.append(("circle", color, x, y, radius))
|
|
180
|
+
|
|
181
|
+
def draw_text(self, text, font="Arial", size=16, color=(255, 255, 255), pos=(0, 0), **kwargs):
|
|
182
|
+
fill = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
|
|
183
|
+
self.canvas.create_text(pos[0], pos[1], text=text, fill=fill, font=(font, size), anchor="nw", **kwargs)
|
|
184
|
+
|
|
185
|
+
def draw_text_later(self, *args, **kwargs):
|
|
186
|
+
self.draw_calls.append(("text", args, kwargs))
|
|
187
|
+
|
|
188
|
+
# === Update + Loop ===
|
|
189
|
+
def run(self, update_func=None, bg=(20, 20, 50)):
|
|
190
|
+
self._bg_color = f"#{bg[0]:02x}{bg[1]:02x}{bg[2]:02x}"
|
|
191
|
+
def loop():
|
|
192
|
+
if not self.running:
|
|
193
|
+
return
|
|
194
|
+
self.fill(self._bg_color)
|
|
187
195
|
if update_func:
|
|
188
196
|
update_func(self)
|
|
189
|
-
|
|
190
|
-
self.screen.fill((20,20,50))
|
|
191
197
|
for call in self.draw_calls:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
ctype = call[0]
|
|
199
|
+
if ctype == "rect":
|
|
200
|
+
_, color, x, y, w, h = call
|
|
201
|
+
col = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
|
|
202
|
+
self.canvas.create_rectangle(x, y, x + w, y + h, fill=col, outline="")
|
|
203
|
+
elif ctype == "circle":
|
|
204
|
+
_, color, x, y, r = call
|
|
205
|
+
col = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
|
|
206
|
+
self.canvas.create_oval(x - r, y - r, x + r, y + r, fill=col, outline="")
|
|
207
|
+
elif ctype == "text":
|
|
208
|
+
self.draw_text(*call[1], **call[2])
|
|
198
209
|
self.draw_calls.clear()
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
210
|
+
self.root.after(16, loop) # ~60fps
|
|
211
|
+
loop()
|
|
212
|
+
self.root.mainloop()
|
|
213
|
+
|
|
214
|
+
def quit(self):
|
|
215
|
+
print(f"Bye Bye!, Future Dev!. quitting '{main_file}'......")
|
|
216
|
+
self.running = False
|
|
217
|
+
self.root.destroy()
|
|
218
|
+
sys.exit()
|