nyuGUI 0.1.0__tar.gz
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.
- nyugui-0.1.0/PKG-INFO +8 -0
- nyugui-0.1.0/nyuGUI/__init__.py +21 -0
- nyugui-0.1.0/nyuGUI/core.py +322 -0
- nyugui-0.1.0/nyuGUI/widgets.py +831 -0
- nyugui-0.1.0/nyuGUI.egg-info/PKG-INFO +8 -0
- nyugui-0.1.0/nyuGUI.egg-info/SOURCES.txt +9 -0
- nyugui-0.1.0/nyuGUI.egg-info/dependency_links.txt +1 -0
- nyugui-0.1.0/nyuGUI.egg-info/requires.txt +1 -0
- nyugui-0.1.0/nyuGUI.egg-info/top_level.txt +1 -0
- nyugui-0.1.0/pyproject.toml +19 -0
- nyugui-0.1.0/setup.cfg +4 -0
nyugui-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nyuGUI
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight GUI library featuring dynamic scale/offset layouting.
|
|
5
|
+
Author: Wjanek13
|
|
6
|
+
Project-URL: Documentation, https://nyugui-documentation.netlify.app
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Requires-Dist: pygame>=2.0.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# nyugui/__init__.py
|
|
2
|
+
|
|
3
|
+
from nyuGUI.core import Dim, Widget, Root
|
|
4
|
+
from nyuGUI.widgets import (
|
|
5
|
+
TextLabel,
|
|
6
|
+
Button,
|
|
7
|
+
ImageLabel,
|
|
8
|
+
TextInput,
|
|
9
|
+
ScrollingFrame,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Dim",
|
|
14
|
+
"Widget",
|
|
15
|
+
"Root",
|
|
16
|
+
"TextLabel",
|
|
17
|
+
"Button",
|
|
18
|
+
"ImageLabel",
|
|
19
|
+
"TextInput",
|
|
20
|
+
"ScrollingFrame",
|
|
21
|
+
]
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import ctypes
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
|
|
8
|
+
warnings.filterwarnings("ignore", category=UserWarning, module="pygame")
|
|
9
|
+
|
|
10
|
+
import pygame
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Dim:
|
|
14
|
+
__slots__ = ("scale", "offset")
|
|
15
|
+
|
|
16
|
+
def __init__(self, scale: float = 0.0, offset: int = 0):
|
|
17
|
+
self.scale = float(scale)
|
|
18
|
+
self.offset = int(offset)
|
|
19
|
+
|
|
20
|
+
def calculate(self, parent_size: int) -> int:
|
|
21
|
+
return int(parent_size * self.scale + self.offset)
|
|
22
|
+
|
|
23
|
+
def __repr__(self) -> str:
|
|
24
|
+
return f"Dim(scale={self.scale}, offset={self.offset})"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Widget:
|
|
28
|
+
_creation_counter = 0
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
x: Dim = None,
|
|
33
|
+
y: Dim = None,
|
|
34
|
+
width: Dim = None,
|
|
35
|
+
height: Dim = None,
|
|
36
|
+
parent=None,
|
|
37
|
+
bgcolor=None,
|
|
38
|
+
background_transparency: float = 0.0,
|
|
39
|
+
zindex: int = 0,
|
|
40
|
+
visible: bool = True,
|
|
41
|
+
):
|
|
42
|
+
Widget._creation_counter += 1
|
|
43
|
+
self.age = Widget._creation_counter
|
|
44
|
+
|
|
45
|
+
self.x_dim = x if x is not None else Dim()
|
|
46
|
+
self.y_dim = y if y is not None else Dim()
|
|
47
|
+
self.w_dim = width if width is not None else Dim()
|
|
48
|
+
self.h_dim = height if height is not None else Dim()
|
|
49
|
+
|
|
50
|
+
self.bgcolor = bgcolor
|
|
51
|
+
self._background_transparency = max(0.0, min(1.0, float(background_transparency)))
|
|
52
|
+
self.zindex = zindex
|
|
53
|
+
self.parent = parent
|
|
54
|
+
self.visible = visible
|
|
55
|
+
self.is_hovered = False
|
|
56
|
+
|
|
57
|
+
self.children = []
|
|
58
|
+
|
|
59
|
+
self.real_x = 0
|
|
60
|
+
self.real_y = 0
|
|
61
|
+
self.real_width = 0
|
|
62
|
+
self.real_height = 0
|
|
63
|
+
|
|
64
|
+
if self.parent:
|
|
65
|
+
self.parent.add_child(self)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def background_transparency(self) -> float:
|
|
69
|
+
return self._background_transparency
|
|
70
|
+
|
|
71
|
+
@background_transparency.setter
|
|
72
|
+
def background_transparency(self, value: float):
|
|
73
|
+
self._background_transparency = max(0.0, min(1.0, float(value)))
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def is_globally_visible(self) -> bool:
|
|
77
|
+
current = self
|
|
78
|
+
while current is not None:
|
|
79
|
+
if not current.visible:
|
|
80
|
+
return False
|
|
81
|
+
current = current.parent
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def add_child(self, child: "Widget"):
|
|
85
|
+
if child not in self.children:
|
|
86
|
+
child.parent = self
|
|
87
|
+
self.children.append(child)
|
|
88
|
+
|
|
89
|
+
def remove_child(self, child: "Widget"):
|
|
90
|
+
if child in self.children:
|
|
91
|
+
child.parent = None
|
|
92
|
+
self.children.remove(child)
|
|
93
|
+
|
|
94
|
+
def get_sorted_children(self):
|
|
95
|
+
return sorted(self.children, key=lambda w: (w.zindex, w.age))
|
|
96
|
+
|
|
97
|
+
def contains_point(self, px: int, py: int) -> bool:
|
|
98
|
+
return (self.real_x <= px < self.real_x + self.real_width) and (
|
|
99
|
+
self.real_y <= py < self.real_y + self.real_height
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def resolve_layout(self, parent_x: int, parent_y: int, parent_w: int, parent_h: int):
|
|
103
|
+
self.real_width = self.w_dim.calculate(parent_w)
|
|
104
|
+
self.real_height = self.h_dim.calculate(parent_h)
|
|
105
|
+
self.real_x = parent_x + self.x_dim.calculate(parent_w)
|
|
106
|
+
self.real_y = parent_y + self.y_dim.calculate(parent_h)
|
|
107
|
+
|
|
108
|
+
for child in self.children:
|
|
109
|
+
child.resolve_layout(self.real_x, self.real_y, self.real_width, self.real_height)
|
|
110
|
+
|
|
111
|
+
def handle_event(self, event) -> bool:
|
|
112
|
+
if not self.is_globally_visible:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
if event.type == pygame.MOUSEMOTION:
|
|
116
|
+
self.is_hovered = self.contains_point(*event.pos)
|
|
117
|
+
|
|
118
|
+
for child in reversed(self.get_sorted_children()):
|
|
119
|
+
if child.handle_event(event):
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def draw_background(self, surface: pygame.Surface):
|
|
125
|
+
if self.bgcolor and self.background_transparency < 1.0 and self.real_width > 0 and self.real_height > 0:
|
|
126
|
+
alpha = int(255 * (1.0 - self.background_transparency))
|
|
127
|
+
if alpha == 255:
|
|
128
|
+
pygame.draw.rect(
|
|
129
|
+
surface,
|
|
130
|
+
self.bgcolor,
|
|
131
|
+
(self.real_x, self.real_y, self.real_width, self.real_height),
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
bg_surface = pygame.Surface((self.real_width, self.real_height), pygame.SRCALPHA)
|
|
135
|
+
bg_color_with_alpha = (*self.bgcolor[:3], alpha)
|
|
136
|
+
bg_surface.fill(bg_color_with_alpha)
|
|
137
|
+
surface.blit(bg_surface, (self.real_x, self.real_y))
|
|
138
|
+
|
|
139
|
+
def draw_foreground(self, surface: pygame.Surface):
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
def draw(self, surface: pygame.Surface):
|
|
143
|
+
if not self.is_globally_visible:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
self.draw_background(surface)
|
|
147
|
+
|
|
148
|
+
prev_clip = surface.get_clip()
|
|
149
|
+
widget_rect = pygame.Rect(self.real_x, self.real_y, self.real_width, self.real_height)
|
|
150
|
+
surface.set_clip(widget_rect.clip(prev_clip))
|
|
151
|
+
|
|
152
|
+
for child in self.get_sorted_children():
|
|
153
|
+
child.draw(surface)
|
|
154
|
+
|
|
155
|
+
surface.set_clip(prev_clip)
|
|
156
|
+
|
|
157
|
+
self.draw_foreground(surface)
|
|
158
|
+
|
|
159
|
+
def _collect_flat(self, out: list, clip: "pygame.Rect"):
|
|
160
|
+
if not self.visible:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
out.append((self, clip))
|
|
164
|
+
|
|
165
|
+
own_rect = pygame.Rect(self.real_x, self.real_y, self.real_width, self.real_height)
|
|
166
|
+
child_clip = own_rect.clip(clip)
|
|
167
|
+
|
|
168
|
+
for child in self.children:
|
|
169
|
+
child._collect_flat(out, child_clip)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
SDL_WINDOWEVENT = 0x200
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class _SDL_Event(ctypes.Structure):
|
|
176
|
+
_fields_ = [("type", ctypes.c_uint32), ("padding", ctypes.c_ubyte * 52)]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
_WATCH_FUNC_TYPE = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(_SDL_Event))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _find_sdl2():
|
|
183
|
+
if hasattr(pygame, "get_sdl_dll_file"):
|
|
184
|
+
try:
|
|
185
|
+
return ctypes.CDLL(pygame.get_sdl_dll_file())
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
candidates = ["SDL2.dll", "libSDL2-2.0.so.0", "libSDL2.so", "libSDL2-2.0.0.dylib"]
|
|
190
|
+
for name in candidates:
|
|
191
|
+
try:
|
|
192
|
+
return ctypes.CDLL(name)
|
|
193
|
+
except OSError:
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
raise OSError("Could not locate the SDL2 library.")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def install_resize_watcher(root: "Root"):
|
|
200
|
+
sdl2 = _find_sdl2()
|
|
201
|
+
|
|
202
|
+
def _watch(_userdata, event):
|
|
203
|
+
if event.contents.type == SDL_WINDOWEVENT:
|
|
204
|
+
root.update_layout()
|
|
205
|
+
root.render()
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
root._resize_watch_cb = _WATCH_FUNC_TYPE(_watch)
|
|
209
|
+
sdl2.SDL_AddEventWatch(root._resize_watch_cb, None)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def cleanup_resize_watcher(root: "Root"):
|
|
213
|
+
if hasattr(root, "_resize_watch_cb"):
|
|
214
|
+
try:
|
|
215
|
+
sdl2 = _find_sdl2()
|
|
216
|
+
sdl2.SDL_DelEventWatch(root._resize_watch_cb, None)
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
from pathlib import Path
|
|
222
|
+
import pygame
|
|
223
|
+
|
|
224
|
+
PACKAGE_DIR = Path(__file__).parent
|
|
225
|
+
DEFAULT_ICON_PATH = PACKAGE_DIR / "assets" / "logo.png"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Root(Widget):
|
|
229
|
+
def __init__(
|
|
230
|
+
self,
|
|
231
|
+
width: int = 800,
|
|
232
|
+
height: int = 600,
|
|
233
|
+
title: str = "nyuGUI App",
|
|
234
|
+
icon: Optional[str] = None,
|
|
235
|
+
bgcolor=(30, 30, 30),
|
|
236
|
+
resizable: bool = True,
|
|
237
|
+
fps: int = 60,
|
|
238
|
+
live_resize: bool = True,
|
|
239
|
+
global_zindex: bool = False,
|
|
240
|
+
):
|
|
241
|
+
pygame.init()
|
|
242
|
+
pygame.font.init()
|
|
243
|
+
|
|
244
|
+
icon_to_load = icon if icon else DEFAULT_ICON_PATH
|
|
245
|
+
if Path(icon_to_load).exists():
|
|
246
|
+
try:
|
|
247
|
+
icon_surface = pygame.image.load(str(icon_to_load))
|
|
248
|
+
pygame.display.set_icon(icon_surface)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
print(f"[nyuGUI] Warning: Failed to load icon '{icon_to_load}': {e}")
|
|
251
|
+
|
|
252
|
+
flags = pygame.RESIZABLE if resizable else 0
|
|
253
|
+
self.surface = pygame.display.set_mode((width, height), flags)
|
|
254
|
+
pygame.display.set_caption(title)
|
|
255
|
+
self.clock = pygame.time.Clock()
|
|
256
|
+
self.fps = fps
|
|
257
|
+
self.is_running = False
|
|
258
|
+
self.global_zindex = global_zindex
|
|
259
|
+
|
|
260
|
+
super().__init__(
|
|
261
|
+
x=Dim(0, 0),
|
|
262
|
+
y=Dim(0, 0),
|
|
263
|
+
width=Dim(1.0, 0),
|
|
264
|
+
height=Dim(1.0, 0),
|
|
265
|
+
parent=None,
|
|
266
|
+
bgcolor=bgcolor,
|
|
267
|
+
zindex=0,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if resizable and live_resize:
|
|
271
|
+
try:
|
|
272
|
+
install_resize_watcher(self)
|
|
273
|
+
except OSError as e:
|
|
274
|
+
print(f"[nyuGUI] live-resize watcher disabled: {e}")
|
|
275
|
+
|
|
276
|
+
def update_layout(self):
|
|
277
|
+
win_w, win_h = self.surface.get_size()
|
|
278
|
+
self.resolve_layout(0, 0, win_w, win_h)
|
|
279
|
+
|
|
280
|
+
def run_frame(self, events):
|
|
281
|
+
for event in events:
|
|
282
|
+
self.handle_event(event)
|
|
283
|
+
|
|
284
|
+
def _render_flat(self):
|
|
285
|
+
win_w, win_h = self.surface.get_size()
|
|
286
|
+
flat = []
|
|
287
|
+
self._collect_flat(flat, pygame.Rect(0, 0, win_w, win_h))
|
|
288
|
+
|
|
289
|
+
flat.sort(key=lambda item: (item[0].zindex, item[0].age))
|
|
290
|
+
|
|
291
|
+
for widget, clip in flat:
|
|
292
|
+
self.surface.set_clip(clip)
|
|
293
|
+
widget.draw_background(self.surface)
|
|
294
|
+
widget.draw_foreground(self.surface)
|
|
295
|
+
|
|
296
|
+
self.surface.set_clip(None)
|
|
297
|
+
|
|
298
|
+
def render(self):
|
|
299
|
+
if self.global_zindex:
|
|
300
|
+
self._render_flat()
|
|
301
|
+
else:
|
|
302
|
+
self.draw(self.surface)
|
|
303
|
+
pygame.display.flip()
|
|
304
|
+
|
|
305
|
+
def run(self):
|
|
306
|
+
self.is_running = True
|
|
307
|
+
|
|
308
|
+
while self.is_running:
|
|
309
|
+
events = pygame.event.get()
|
|
310
|
+
for event in events:
|
|
311
|
+
if event.type == pygame.QUIT:
|
|
312
|
+
self.is_running = False
|
|
313
|
+
|
|
314
|
+
self.update_layout()
|
|
315
|
+
self.run_frame(events)
|
|
316
|
+
self.render()
|
|
317
|
+
|
|
318
|
+
self.clock.tick(self.fps)
|
|
319
|
+
|
|
320
|
+
cleanup_resize_watcher(self)
|
|
321
|
+
pygame.quit()
|
|
322
|
+
sys.exit()
|
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
from typing import Callable, Optional, Union
|
|
2
|
+
import pygame
|
|
3
|
+
|
|
4
|
+
from nyuGUI.core import Dim, Widget
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TextLabel(Widget):
|
|
8
|
+
_font_cache = {}
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
text: str = "",
|
|
13
|
+
font_size: int = 18,
|
|
14
|
+
font_name: str = "Segoe UI",
|
|
15
|
+
text_color=(255, 255, 255),
|
|
16
|
+
align: str = "center",
|
|
17
|
+
text_visible: bool = True,
|
|
18
|
+
text_transparency: float = 0.0,
|
|
19
|
+
x: Dim = None,
|
|
20
|
+
y: Dim = None,
|
|
21
|
+
width: Dim = None,
|
|
22
|
+
height: Dim = None,
|
|
23
|
+
parent=None,
|
|
24
|
+
bgcolor=None,
|
|
25
|
+
background_transparency: float = 0.0,
|
|
26
|
+
zindex: int = 0,
|
|
27
|
+
visible: bool = True,
|
|
28
|
+
):
|
|
29
|
+
self._text = text
|
|
30
|
+
self.font_size = font_size
|
|
31
|
+
self.font_name = font_name
|
|
32
|
+
self.text_color = text_color
|
|
33
|
+
self.align = align.lower()
|
|
34
|
+
self.text_visible = text_visible
|
|
35
|
+
self._text_transparency = max(0.0, min(1.0, float(text_transparency)))
|
|
36
|
+
|
|
37
|
+
self.rendered_surface = None
|
|
38
|
+
self._dirty = True
|
|
39
|
+
|
|
40
|
+
font_key = (self.font_name, self.font_size)
|
|
41
|
+
if font_key not in TextLabel._font_cache:
|
|
42
|
+
TextLabel._font_cache[font_key] = pygame.font.SysFont(self.font_name, self.font_size)
|
|
43
|
+
self.font = TextLabel._font_cache[font_key]
|
|
44
|
+
|
|
45
|
+
super().__init__(
|
|
46
|
+
x=x,
|
|
47
|
+
y=y,
|
|
48
|
+
width=width,
|
|
49
|
+
height=height,
|
|
50
|
+
parent=parent,
|
|
51
|
+
bgcolor=bgcolor,
|
|
52
|
+
background_transparency=background_transparency,
|
|
53
|
+
zindex=zindex,
|
|
54
|
+
visible=visible,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
self._render_text()
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def text(self) -> str:
|
|
61
|
+
return self._text
|
|
62
|
+
|
|
63
|
+
@text.setter
|
|
64
|
+
def text(self, value: str):
|
|
65
|
+
if self._text != value:
|
|
66
|
+
self._text = value
|
|
67
|
+
self._dirty = True
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def text_transparency(self) -> float:
|
|
71
|
+
return self._text_transparency
|
|
72
|
+
|
|
73
|
+
@text_transparency.setter
|
|
74
|
+
def text_transparency(self, value: float):
|
|
75
|
+
value = max(0.0, min(1.0, float(value)))
|
|
76
|
+
if self._text_transparency != value:
|
|
77
|
+
self._text_transparency = value
|
|
78
|
+
self._dirty = True
|
|
79
|
+
|
|
80
|
+
def _render_text(self):
|
|
81
|
+
if self._dirty or self.rendered_surface is None:
|
|
82
|
+
raw_surface = self.font.render(self._text, True, self.text_color)
|
|
83
|
+
alpha = int(255 * (1.0 - self._text_transparency))
|
|
84
|
+
|
|
85
|
+
self.rendered_surface = raw_surface.convert_alpha()
|
|
86
|
+
self.rendered_surface.set_alpha(alpha)
|
|
87
|
+
|
|
88
|
+
self._dirty = False
|
|
89
|
+
|
|
90
|
+
if self.w_dim.scale == 0.0 and self.w_dim.offset == 0:
|
|
91
|
+
self.w_dim.offset = self.rendered_surface.get_width()
|
|
92
|
+
if self.h_dim.scale == 0.0 and self.h_dim.offset == 0:
|
|
93
|
+
self.h_dim.offset = self.rendered_surface.get_height()
|
|
94
|
+
|
|
95
|
+
def draw_foreground(self, surface: pygame.Surface):
|
|
96
|
+
if not self.text_visible or self.text_transparency >= 1.0:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
self._render_text()
|
|
100
|
+
if not self.rendered_surface:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
txt_w, txt_h = self.rendered_surface.get_size()
|
|
104
|
+
draw_y = self.real_y + (self.real_height - txt_h) // 2
|
|
105
|
+
|
|
106
|
+
if self.align == "left":
|
|
107
|
+
draw_x = self.real_x
|
|
108
|
+
elif self.align == "right":
|
|
109
|
+
draw_x = self.real_x + (self.real_width - txt_w)
|
|
110
|
+
else:
|
|
111
|
+
draw_x = self.real_x + (self.real_width - txt_w) // 2
|
|
112
|
+
|
|
113
|
+
surface.blit(self.rendered_surface, (draw_x, draw_y))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Button(TextLabel):
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
text: str = "Button",
|
|
120
|
+
font_size: int = 18,
|
|
121
|
+
font_name: str = "Segoe UI",
|
|
122
|
+
text_color=(255, 255, 255),
|
|
123
|
+
align: str = "center",
|
|
124
|
+
text_visible: bool = True,
|
|
125
|
+
text_transparency: float = 0.0,
|
|
126
|
+
x: Dim = None,
|
|
127
|
+
y: Dim = None,
|
|
128
|
+
width: Dim = None,
|
|
129
|
+
height: Dim = None,
|
|
130
|
+
parent=None,
|
|
131
|
+
bgcolor=(0, 120, 215),
|
|
132
|
+
hover_bgcolor=(0, 140, 240),
|
|
133
|
+
pressed_bgcolor=(0, 90, 170),
|
|
134
|
+
background_transparency: float = 0.0,
|
|
135
|
+
zindex: int = 0,
|
|
136
|
+
visible: bool = True,
|
|
137
|
+
on_press: Optional[Callable[[], None]] = None,
|
|
138
|
+
on_release: Optional[Callable[[], None]] = None,
|
|
139
|
+
):
|
|
140
|
+
super().__init__(
|
|
141
|
+
text=text,
|
|
142
|
+
font_size=font_size,
|
|
143
|
+
font_name=font_name,
|
|
144
|
+
text_color=text_color,
|
|
145
|
+
align=align,
|
|
146
|
+
text_visible=text_visible,
|
|
147
|
+
text_transparency=text_transparency,
|
|
148
|
+
x=x,
|
|
149
|
+
y=y,
|
|
150
|
+
width=width,
|
|
151
|
+
height=height,
|
|
152
|
+
parent=parent,
|
|
153
|
+
bgcolor=bgcolor,
|
|
154
|
+
background_transparency=background_transparency,
|
|
155
|
+
zindex=zindex,
|
|
156
|
+
visible=visible,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
self.normal_bgcolor = bgcolor
|
|
160
|
+
self.hover_bgcolor = hover_bgcolor
|
|
161
|
+
self.pressed_bgcolor = pressed_bgcolor
|
|
162
|
+
|
|
163
|
+
self.on_press = on_press
|
|
164
|
+
self.on_release = on_release
|
|
165
|
+
|
|
166
|
+
self.is_pressed = False
|
|
167
|
+
|
|
168
|
+
def handle_event(self, event) -> bool:
|
|
169
|
+
if not self.is_globally_visible:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
if super().handle_event(event):
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
|
176
|
+
if self.contains_point(*event.pos):
|
|
177
|
+
self.is_pressed = True
|
|
178
|
+
if self.on_press:
|
|
179
|
+
self.on_press()
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
|
|
183
|
+
if self.is_pressed:
|
|
184
|
+
self.is_pressed = False
|
|
185
|
+
if self.contains_point(*event.pos):
|
|
186
|
+
if self.on_release:
|
|
187
|
+
self.on_release()
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
def draw_background(self, surface: pygame.Surface):
|
|
193
|
+
original_bgcolor = self.bgcolor
|
|
194
|
+
if self.is_pressed and self.pressed_bgcolor:
|
|
195
|
+
self.bgcolor = self.pressed_bgcolor
|
|
196
|
+
elif self.is_hovered and self.hover_bgcolor:
|
|
197
|
+
self.bgcolor = self.hover_bgcolor
|
|
198
|
+
else:
|
|
199
|
+
self.bgcolor = self.normal_bgcolor
|
|
200
|
+
|
|
201
|
+
super().draw_background(surface)
|
|
202
|
+
self.bgcolor = original_bgcolor
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class ImageLabel(Widget):
|
|
206
|
+
_image_cache = {}
|
|
207
|
+
|
|
208
|
+
def __init__(
|
|
209
|
+
self,
|
|
210
|
+
image: Optional[Union[str, pygame.Surface]] = None,
|
|
211
|
+
scale_mode: str = "fit",
|
|
212
|
+
image_transparency: float = 0.0,
|
|
213
|
+
x: Dim = None,
|
|
214
|
+
y: Dim = None,
|
|
215
|
+
width: Dim = None,
|
|
216
|
+
height: Dim = None,
|
|
217
|
+
parent=None,
|
|
218
|
+
bgcolor=None,
|
|
219
|
+
background_transparency: float = 0.0,
|
|
220
|
+
zindex: int = 0,
|
|
221
|
+
visible: bool = True,
|
|
222
|
+
):
|
|
223
|
+
super().__init__(
|
|
224
|
+
x=x,
|
|
225
|
+
y=y,
|
|
226
|
+
width=width,
|
|
227
|
+
height=height,
|
|
228
|
+
parent=parent,
|
|
229
|
+
bgcolor=bgcolor,
|
|
230
|
+
background_transparency=background_transparency,
|
|
231
|
+
zindex=zindex,
|
|
232
|
+
visible=visible,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
self.scale_mode = scale_mode.lower()
|
|
236
|
+
self._image_transparency = max(0.0, min(1.0, float(image_transparency)))
|
|
237
|
+
|
|
238
|
+
self._source_image: Optional[pygame.Surface] = None
|
|
239
|
+
self._scaled_image: Optional[pygame.Surface] = None
|
|
240
|
+
self._last_draw_size = (0, 0)
|
|
241
|
+
|
|
242
|
+
if image is not None:
|
|
243
|
+
self.set_image(image)
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def image_transparency(self) -> float:
|
|
247
|
+
return self._image_transparency
|
|
248
|
+
|
|
249
|
+
@image_transparency.setter
|
|
250
|
+
def image_transparency(self, value: float):
|
|
251
|
+
value = max(0.0, min(1.0, float(value)))
|
|
252
|
+
if self._image_transparency != value:
|
|
253
|
+
self._image_transparency = value
|
|
254
|
+
self._scaled_image = None
|
|
255
|
+
|
|
256
|
+
def set_image(self, image: Union[str, pygame.Surface]):
|
|
257
|
+
if isinstance(image, str):
|
|
258
|
+
if image not in ImageLabel._image_cache:
|
|
259
|
+
loaded = pygame.image.load(image).convert_alpha()
|
|
260
|
+
ImageLabel._image_cache[image] = loaded
|
|
261
|
+
self._source_image = ImageLabel._image_cache[image]
|
|
262
|
+
elif isinstance(image, pygame.Surface):
|
|
263
|
+
self._source_image = image.convert_alpha()
|
|
264
|
+
else:
|
|
265
|
+
self._source_image = None
|
|
266
|
+
|
|
267
|
+
self._scaled_image = None
|
|
268
|
+
|
|
269
|
+
if self._source_image and self.w_dim.scale == 0.0 and self.w_dim.offset == 0:
|
|
270
|
+
self.w_dim.offset = self._source_image.get_width()
|
|
271
|
+
if self._source_image and self.h_dim.scale == 0.0 and self.h_dim.offset == 0:
|
|
272
|
+
self.h_dim.offset = self._source_image.get_height()
|
|
273
|
+
|
|
274
|
+
def _get_scaled_surface(self, target_w: int, target_h: int) -> Optional[pygame.Surface]:
|
|
275
|
+
if not self._source_image or target_w <= 0 or target_h <= 0:
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
if self._scaled_image and self._last_draw_size == (target_w, target_h):
|
|
279
|
+
return self._scaled_image
|
|
280
|
+
|
|
281
|
+
src_w, src_h = self._source_image.get_size()
|
|
282
|
+
mode = self.scale_mode
|
|
283
|
+
|
|
284
|
+
if mode == "stretch":
|
|
285
|
+
scaled = pygame.transform.smoothscale(self._source_image, (target_w, target_h))
|
|
286
|
+
|
|
287
|
+
elif mode == "fit":
|
|
288
|
+
scale = min(target_w / src_w, target_h / src_h)
|
|
289
|
+
new_w, new_h = max(1, int(src_w * scale)), max(1, int(src_h * scale))
|
|
290
|
+
scaled = pygame.transform.smoothscale(self._source_image, (new_w, new_h))
|
|
291
|
+
|
|
292
|
+
elif mode == "crop":
|
|
293
|
+
scale = max(target_w / src_w, target_h / src_h)
|
|
294
|
+
new_w, new_h = max(1, int(src_w * scale)), max(1, int(src_h * scale))
|
|
295
|
+
resized = pygame.transform.smoothscale(self._source_image, (new_w, new_h))
|
|
296
|
+
|
|
297
|
+
crop_x = (new_w - target_w) // 2
|
|
298
|
+
crop_y = (new_h - target_h) // 2
|
|
299
|
+
scaled = resized.subsurface((crop_x, crop_y, target_w, target_h)).copy()
|
|
300
|
+
|
|
301
|
+
else:
|
|
302
|
+
scaled = self._source_image.copy()
|
|
303
|
+
|
|
304
|
+
alpha = int(255 * (1.0 - self._image_transparency))
|
|
305
|
+
if alpha < 255:
|
|
306
|
+
scaled = scaled.copy()
|
|
307
|
+
scaled.set_alpha(alpha)
|
|
308
|
+
|
|
309
|
+
self._scaled_image = scaled
|
|
310
|
+
self._last_draw_size = (target_w, target_h)
|
|
311
|
+
return self._scaled_image
|
|
312
|
+
|
|
313
|
+
def draw_foreground(self, surface: pygame.Surface):
|
|
314
|
+
if not self._source_image or self.image_transparency >= 1.0:
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
img_surface = self._get_scaled_surface(self.real_width, self.real_height)
|
|
318
|
+
if not img_surface:
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
img_w, img_h = img_surface.get_size()
|
|
322
|
+
|
|
323
|
+
draw_x = self.real_x + (self.real_width - img_w) // 2
|
|
324
|
+
draw_y = self.real_y + (self.real_height - img_h) // 2
|
|
325
|
+
|
|
326
|
+
surface.blit(img_surface, (draw_x, draw_y))
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class TextInput(TextLabel):
|
|
330
|
+
def __init__(
|
|
331
|
+
self,
|
|
332
|
+
text: str = "",
|
|
333
|
+
placeholder: str = "Enter text...",
|
|
334
|
+
placeholder_color=(140, 140, 140),
|
|
335
|
+
font_size: int = 18,
|
|
336
|
+
font_name: str = "Segoe UI",
|
|
337
|
+
text_color=(255, 255, 255),
|
|
338
|
+
align: str = "left",
|
|
339
|
+
x: Dim = None,
|
|
340
|
+
y: Dim = None,
|
|
341
|
+
width: Dim = None,
|
|
342
|
+
height: Dim = None,
|
|
343
|
+
parent=None,
|
|
344
|
+
bgcolor=(40, 40, 40),
|
|
345
|
+
focused_bgcolor=(50, 50, 50),
|
|
346
|
+
hover_bgcolor=(45, 45, 45),
|
|
347
|
+
border_color=(100, 100, 100),
|
|
348
|
+
focused_border_color=(0, 120, 215),
|
|
349
|
+
cursor_color=(255, 255, 255),
|
|
350
|
+
background_transparency: float = 0.0,
|
|
351
|
+
zindex: int = 0,
|
|
352
|
+
visible: bool = True,
|
|
353
|
+
on_text_changed: Optional[Callable[[str], None]] = None,
|
|
354
|
+
on_submit: Optional[Callable[[str], None]] = None,
|
|
355
|
+
):
|
|
356
|
+
super().__init__(
|
|
357
|
+
text=text,
|
|
358
|
+
font_size=font_size,
|
|
359
|
+
font_name=font_name,
|
|
360
|
+
text_color=text_color,
|
|
361
|
+
align=align,
|
|
362
|
+
text_visible=True,
|
|
363
|
+
x=x,
|
|
364
|
+
y=y,
|
|
365
|
+
width=width,
|
|
366
|
+
height=height,
|
|
367
|
+
parent=parent,
|
|
368
|
+
bgcolor=bgcolor,
|
|
369
|
+
background_transparency=background_transparency,
|
|
370
|
+
zindex=zindex,
|
|
371
|
+
visible=visible,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
self.placeholder = placeholder
|
|
375
|
+
self.placeholder_color = placeholder_color
|
|
376
|
+
|
|
377
|
+
self.normal_bgcolor = bgcolor
|
|
378
|
+
self.focused_bgcolor = focused_bgcolor
|
|
379
|
+
self.hover_bgcolor = hover_bgcolor
|
|
380
|
+
|
|
381
|
+
self.border_color = border_color
|
|
382
|
+
self.focused_border_color = focused_border_color
|
|
383
|
+
self.cursor_color = cursor_color
|
|
384
|
+
|
|
385
|
+
self.on_text_changed = on_text_changed
|
|
386
|
+
self.on_submit = on_submit
|
|
387
|
+
|
|
388
|
+
self.is_focused = False
|
|
389
|
+
self.cursor_index = len(text)
|
|
390
|
+
self.cursor_timer = 0
|
|
391
|
+
self.cursor_visible = True
|
|
392
|
+
|
|
393
|
+
@TextLabel.text.setter
|
|
394
|
+
def text(self, value: str):
|
|
395
|
+
if self._text != value:
|
|
396
|
+
self._text = value
|
|
397
|
+
self._dirty = True
|
|
398
|
+
self.cursor_index = min(self.cursor_index, len(self._text))
|
|
399
|
+
if self.on_text_changed:
|
|
400
|
+
self.on_text_changed(self._text)
|
|
401
|
+
|
|
402
|
+
def handle_event(self, event) -> bool:
|
|
403
|
+
if not self.is_globally_visible:
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
if super(TextLabel, self).handle_event(event):
|
|
407
|
+
return True
|
|
408
|
+
|
|
409
|
+
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
|
410
|
+
was_focused = self.is_focused
|
|
411
|
+
self.is_focused = self.contains_point(*event.pos)
|
|
412
|
+
|
|
413
|
+
if self.is_focused:
|
|
414
|
+
self.cursor_timer = pygame.time.get_ticks()
|
|
415
|
+
self.cursor_visible = True
|
|
416
|
+
if not was_focused:
|
|
417
|
+
self.cursor_index = len(self._text)
|
|
418
|
+
return True
|
|
419
|
+
|
|
420
|
+
if self.is_focused and event.type == pygame.KEYDOWN:
|
|
421
|
+
self.cursor_timer = pygame.time.get_ticks()
|
|
422
|
+
self.cursor_visible = True
|
|
423
|
+
|
|
424
|
+
if event.key == pygame.K_BACKSPACE:
|
|
425
|
+
if self.cursor_index > 0:
|
|
426
|
+
self._text = (
|
|
427
|
+
self._text[: self.cursor_index - 1]
|
|
428
|
+
+ self._text[self.cursor_index :]
|
|
429
|
+
)
|
|
430
|
+
self.cursor_index -= 1
|
|
431
|
+
self._dirty = True
|
|
432
|
+
if self.on_text_changed:
|
|
433
|
+
self.on_text_changed(self._text)
|
|
434
|
+
|
|
435
|
+
elif event.key == pygame.K_DELETE:
|
|
436
|
+
if self.cursor_index < len(self._text):
|
|
437
|
+
self._text = (
|
|
438
|
+
self._text[: self.cursor_index]
|
|
439
|
+
+ self._text[self.cursor_index + 1 :]
|
|
440
|
+
)
|
|
441
|
+
self._dirty = True
|
|
442
|
+
if self.on_text_changed:
|
|
443
|
+
self.on_text_changed(self._text)
|
|
444
|
+
|
|
445
|
+
elif event.key == pygame.K_LEFT:
|
|
446
|
+
if self.cursor_index > 0:
|
|
447
|
+
self.cursor_index -= 1
|
|
448
|
+
|
|
449
|
+
elif event.key == pygame.K_RIGHT:
|
|
450
|
+
if self.cursor_index < len(self._text):
|
|
451
|
+
self.cursor_index += 1
|
|
452
|
+
|
|
453
|
+
elif event.key == pygame.K_HOME:
|
|
454
|
+
self.cursor_index = 0
|
|
455
|
+
|
|
456
|
+
elif event.key == pygame.K_END:
|
|
457
|
+
self.cursor_index = len(self._text)
|
|
458
|
+
|
|
459
|
+
elif event.key in (pygame.K_RETURN, pygame.K_KP_ENTER):
|
|
460
|
+
if self.on_submit:
|
|
461
|
+
self.on_submit(self._text)
|
|
462
|
+
|
|
463
|
+
else:
|
|
464
|
+
if event.unicode and event.unicode.isprintable():
|
|
465
|
+
self._text = (
|
|
466
|
+
self._text[: self.cursor_index]
|
|
467
|
+
+ event.unicode
|
|
468
|
+
+ self._text[self.cursor_index :]
|
|
469
|
+
)
|
|
470
|
+
self.cursor_index += len(event.unicode)
|
|
471
|
+
self._dirty = True
|
|
472
|
+
if self.on_text_changed:
|
|
473
|
+
self.on_text_changed(self._text)
|
|
474
|
+
|
|
475
|
+
return True
|
|
476
|
+
|
|
477
|
+
return False
|
|
478
|
+
|
|
479
|
+
def draw_background(self, surface: pygame.Surface):
|
|
480
|
+
original_bgcolor = self.bgcolor
|
|
481
|
+
if self.is_focused and self.focused_bgcolor:
|
|
482
|
+
self.bgcolor = self.focused_bgcolor
|
|
483
|
+
elif self.is_hovered and self.hover_bgcolor:
|
|
484
|
+
self.bgcolor = self.hover_bgcolor
|
|
485
|
+
else:
|
|
486
|
+
self.bgcolor = self.normal_bgcolor
|
|
487
|
+
|
|
488
|
+
super().draw_background(surface)
|
|
489
|
+
self.bgcolor = original_bgcolor
|
|
490
|
+
|
|
491
|
+
b_color = self.focused_border_color if self.is_focused else self.border_color
|
|
492
|
+
if b_color and self.real_width > 0 and self.real_height > 0:
|
|
493
|
+
border_rect = (self.real_x, self.real_y, self.real_width, self.real_height)
|
|
494
|
+
pygame.draw.rect(surface, b_color, border_rect, width=1)
|
|
495
|
+
|
|
496
|
+
def draw_foreground(self, surface: pygame.Surface):
|
|
497
|
+
if not self._text and not self.is_focused and self.placeholder:
|
|
498
|
+
placeholder_surf = self.font.render(
|
|
499
|
+
self.placeholder, True, self.placeholder_color
|
|
500
|
+
)
|
|
501
|
+
txt_h = placeholder_surf.get_height()
|
|
502
|
+
draw_y = self.real_y + (self.real_height - txt_h) // 2
|
|
503
|
+
draw_x = self.real_x + 8
|
|
504
|
+
surface.blit(placeholder_surf, (draw_x, draw_y))
|
|
505
|
+
return
|
|
506
|
+
|
|
507
|
+
self._render_text()
|
|
508
|
+
if self.rendered_surface:
|
|
509
|
+
txt_w, txt_h = self.rendered_surface.get_size()
|
|
510
|
+
draw_y = self.real_y + (self.real_height - txt_h) // 2
|
|
511
|
+
|
|
512
|
+
left_padding = 8
|
|
513
|
+
if self.align == "left":
|
|
514
|
+
draw_x = self.real_x + left_padding
|
|
515
|
+
elif self.align == "right":
|
|
516
|
+
draw_x = self.real_x + (self.real_width - txt_w - left_padding)
|
|
517
|
+
else:
|
|
518
|
+
draw_x = self.real_x + (self.real_width - txt_w) // 2
|
|
519
|
+
|
|
520
|
+
surface.blit(self.rendered_surface, (draw_x, draw_y))
|
|
521
|
+
|
|
522
|
+
if self.is_focused:
|
|
523
|
+
if (pygame.time.get_ticks() - self.cursor_timer) % 1000 < 500:
|
|
524
|
+
prefix_text = self._text[: self.cursor_index]
|
|
525
|
+
cursor_x_offset = self.font.size(prefix_text)[0]
|
|
526
|
+
cursor_x = draw_x + cursor_x_offset
|
|
527
|
+
|
|
528
|
+
cursor_top = draw_y
|
|
529
|
+
cursor_bottom = draw_y + txt_h
|
|
530
|
+
|
|
531
|
+
pygame.draw.line(
|
|
532
|
+
surface,
|
|
533
|
+
self.cursor_color,
|
|
534
|
+
(cursor_x, cursor_top),
|
|
535
|
+
(cursor_x, cursor_bottom),
|
|
536
|
+
width=2,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class ScrollingFrame(Widget):
|
|
541
|
+
def __init__(
|
|
542
|
+
self,
|
|
543
|
+
canvas_width: Dim = None,
|
|
544
|
+
canvas_height: Dim = None,
|
|
545
|
+
x: Dim = None,
|
|
546
|
+
y: Dim = None,
|
|
547
|
+
width: Dim = None,
|
|
548
|
+
height: Dim = None,
|
|
549
|
+
parent=None,
|
|
550
|
+
bgcolor=(35, 35, 35),
|
|
551
|
+
background_transparency: float = 0.0,
|
|
552
|
+
zindex: int = 0,
|
|
553
|
+
visible: bool = True,
|
|
554
|
+
scroll_speed: int = 25,
|
|
555
|
+
show_vertical_scrollbar: bool = True,
|
|
556
|
+
show_horizontal_scrollbar: bool = False,
|
|
557
|
+
scrollbar_thickness: int = 10,
|
|
558
|
+
scrollbar_track_color=(20, 20, 20),
|
|
559
|
+
scrollbar_thumb_color=(80, 80, 80),
|
|
560
|
+
scrollbar_hover_color=(120, 120, 120),
|
|
561
|
+
scrollbar_active_color=(0, 120, 215),
|
|
562
|
+
):
|
|
563
|
+
super().__init__(
|
|
564
|
+
x=x,
|
|
565
|
+
y=y,
|
|
566
|
+
width=width,
|
|
567
|
+
height=height,
|
|
568
|
+
parent=parent,
|
|
569
|
+
bgcolor=bgcolor,
|
|
570
|
+
background_transparency=background_transparency,
|
|
571
|
+
zindex=zindex,
|
|
572
|
+
visible=visible,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
self.canvas_w_dim = canvas_width if canvas_width is not None else Dim(1.0, 0)
|
|
576
|
+
self.canvas_h_dim = canvas_height if canvas_height is not None else Dim(1.0, 0)
|
|
577
|
+
|
|
578
|
+
self.real_canvas_width = 0
|
|
579
|
+
self.real_canvas_height = 0
|
|
580
|
+
|
|
581
|
+
self.scroll_x = 0
|
|
582
|
+
self.scroll_y = 0
|
|
583
|
+
self.scroll_speed = scroll_speed
|
|
584
|
+
|
|
585
|
+
self.show_vertical_scrollbar = show_vertical_scrollbar
|
|
586
|
+
self.show_horizontal_scrollbar = show_horizontal_scrollbar
|
|
587
|
+
self.scrollbar_thickness = scrollbar_thickness
|
|
588
|
+
self.scrollbar_track_color = scrollbar_track_color
|
|
589
|
+
self.scrollbar_thumb_color = scrollbar_thumb_color
|
|
590
|
+
self.scrollbar_hover_color = scrollbar_hover_color
|
|
591
|
+
self.scrollbar_active_color = scrollbar_active_color
|
|
592
|
+
|
|
593
|
+
self._dragging_v_thumb = False
|
|
594
|
+
self._dragging_h_thumb = False
|
|
595
|
+
self._drag_start_mouse = (0, 0)
|
|
596
|
+
self._drag_start_scroll = (0, 0)
|
|
597
|
+
|
|
598
|
+
self._v_thumb_hovered = False
|
|
599
|
+
self._h_thumb_hovered = False
|
|
600
|
+
|
|
601
|
+
self.canvas_surface: Optional[pygame.Surface] = None
|
|
602
|
+
|
|
603
|
+
def _get_scrollbar_rects(self):
|
|
604
|
+
v_track, v_thumb = None, None
|
|
605
|
+
h_track, h_thumb = None, None
|
|
606
|
+
|
|
607
|
+
thick = self.scrollbar_thickness
|
|
608
|
+
max_scroll_x = max(0, self.real_canvas_width - self.real_width)
|
|
609
|
+
max_scroll_y = max(0, self.real_canvas_height - self.real_height)
|
|
610
|
+
|
|
611
|
+
h_offset = thick if self.show_horizontal_scrollbar and max_scroll_x > 0 else 0
|
|
612
|
+
v_offset = thick if self.show_vertical_scrollbar and max_scroll_y > 0 else 0
|
|
613
|
+
|
|
614
|
+
if self.show_vertical_scrollbar and max_scroll_y > 0:
|
|
615
|
+
track_h = max(1, self.real_height - h_offset)
|
|
616
|
+
v_track = pygame.Rect(
|
|
617
|
+
self.real_x + self.real_width - thick, self.real_y, thick, track_h
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
visible_ratio = self.real_height / self.real_canvas_height
|
|
621
|
+
thumb_h = max(20, int(track_h * visible_ratio))
|
|
622
|
+
scroll_ratio = self.scroll_y / max_scroll_y
|
|
623
|
+
thumb_y = v_track.y + int(scroll_ratio * (track_h - thumb_h))
|
|
624
|
+
|
|
625
|
+
v_thumb = pygame.Rect(v_track.x, thumb_y, thick, thumb_h)
|
|
626
|
+
|
|
627
|
+
if self.show_horizontal_scrollbar and max_scroll_x > 0:
|
|
628
|
+
track_w = max(1, self.real_width - v_offset)
|
|
629
|
+
h_track = pygame.Rect(
|
|
630
|
+
self.real_x, self.real_y + self.real_height - thick, track_w, thick
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
visible_ratio = self.real_width / self.real_canvas_width
|
|
634
|
+
thumb_w = max(20, int(track_w * visible_ratio))
|
|
635
|
+
scroll_ratio = self.scroll_x / max_scroll_x
|
|
636
|
+
thumb_x = h_track.x + int(scroll_ratio * (track_w - thumb_w))
|
|
637
|
+
|
|
638
|
+
h_thumb = pygame.Rect(thumb_x, h_track.y, thumb_w, thick)
|
|
639
|
+
|
|
640
|
+
return v_track, v_thumb, h_track, h_thumb
|
|
641
|
+
|
|
642
|
+
def resolve_layout(self, parent_x: int, parent_y: int, parent_w: int, parent_h: int):
|
|
643
|
+
self.real_width = self.w_dim.calculate(parent_w)
|
|
644
|
+
self.real_height = self.h_dim.calculate(parent_h)
|
|
645
|
+
self.real_x = parent_x + self.x_dim.calculate(parent_w)
|
|
646
|
+
self.real_y = parent_y + self.y_dim.calculate(parent_h)
|
|
647
|
+
|
|
648
|
+
self.real_canvas_width = max(
|
|
649
|
+
self.real_width, self.canvas_w_dim.calculate(self.real_width)
|
|
650
|
+
)
|
|
651
|
+
self.real_canvas_height = max(
|
|
652
|
+
self.real_height, self.canvas_h_dim.calculate(self.real_height)
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
max_scroll_x = max(0, self.real_canvas_width - self.real_width)
|
|
656
|
+
max_scroll_y = max(0, self.real_canvas_height - self.real_height)
|
|
657
|
+
self.scroll_x = max(0, min(self.scroll_x, max_scroll_x))
|
|
658
|
+
self.scroll_y = max(0, min(self.scroll_y, max_scroll_y))
|
|
659
|
+
|
|
660
|
+
if (
|
|
661
|
+
not self.canvas_surface
|
|
662
|
+
or self.canvas_surface.get_width() != self.real_canvas_width
|
|
663
|
+
or self.canvas_surface.get_height() != self.real_canvas_height
|
|
664
|
+
):
|
|
665
|
+
if self.real_canvas_width > 0 and self.real_canvas_height > 0:
|
|
666
|
+
self.canvas_surface = pygame.Surface(
|
|
667
|
+
(self.real_canvas_width, self.real_canvas_height), pygame.SRCALPHA
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
for child in self.children:
|
|
671
|
+
child.resolve_layout(
|
|
672
|
+
0, 0, self.real_canvas_width, self.real_canvas_height
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def handle_event(self, event) -> bool:
|
|
676
|
+
if not self.is_globally_visible:
|
|
677
|
+
return False
|
|
678
|
+
|
|
679
|
+
v_track, v_thumb, h_track, h_thumb = self._get_scrollbar_rects()
|
|
680
|
+
max_scroll_x = max(0, self.real_canvas_width - self.real_width)
|
|
681
|
+
max_scroll_y = max(0, self.real_canvas_height - self.real_height)
|
|
682
|
+
|
|
683
|
+
if event.type == pygame.MOUSEMOTION:
|
|
684
|
+
self.is_hovered = self.contains_point(*event.pos)
|
|
685
|
+
self._v_thumb_hovered = v_thumb.collidepoint(event.pos) if v_thumb else False
|
|
686
|
+
self._h_thumb_hovered = h_thumb.collidepoint(event.pos) if h_thumb else False
|
|
687
|
+
|
|
688
|
+
if self._dragging_v_thumb and v_track and v_thumb:
|
|
689
|
+
delta_y = event.pos[1] - self._drag_start_mouse[1]
|
|
690
|
+
available_track = v_track.height - v_thumb.height
|
|
691
|
+
if available_track > 0:
|
|
692
|
+
scroll_delta = (delta_y / available_track) * max_scroll_y
|
|
693
|
+
self.scroll_y = max(
|
|
694
|
+
0, min(max_scroll_y, self._drag_start_scroll[1] + scroll_delta)
|
|
695
|
+
)
|
|
696
|
+
return True
|
|
697
|
+
|
|
698
|
+
if self._dragging_h_thumb and h_track and h_thumb:
|
|
699
|
+
delta_x = event.pos[0] - self._drag_start_mouse[0]
|
|
700
|
+
available_track = h_track.width - h_thumb.width
|
|
701
|
+
if available_track > 0:
|
|
702
|
+
scroll_delta = (delta_x / available_track) * max_scroll_x
|
|
703
|
+
self.scroll_x = max(
|
|
704
|
+
0, min(max_scroll_x, self._drag_start_scroll[0] + scroll_delta)
|
|
705
|
+
)
|
|
706
|
+
return True
|
|
707
|
+
|
|
708
|
+
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
|
709
|
+
if v_thumb and v_thumb.collidepoint(event.pos):
|
|
710
|
+
self._dragging_v_thumb = True
|
|
711
|
+
self._drag_start_mouse = event.pos
|
|
712
|
+
self._drag_start_scroll = (self.scroll_x, self.scroll_y)
|
|
713
|
+
return True
|
|
714
|
+
|
|
715
|
+
if h_thumb and h_thumb.collidepoint(event.pos):
|
|
716
|
+
self._dragging_h_thumb = True
|
|
717
|
+
self._drag_start_mouse = event.pos
|
|
718
|
+
self._drag_start_scroll = (self.scroll_x, self.scroll_y)
|
|
719
|
+
return True
|
|
720
|
+
|
|
721
|
+
if v_track and v_track.collidepoint(event.pos):
|
|
722
|
+
click_y = event.pos[1] - v_track.y
|
|
723
|
+
ratio = click_y / v_track.height
|
|
724
|
+
self.scroll_y = max(0, min(max_scroll_y, ratio * max_scroll_y))
|
|
725
|
+
return True
|
|
726
|
+
|
|
727
|
+
if h_track and h_track.collidepoint(event.pos):
|
|
728
|
+
click_x = event.pos[0] - h_track.x
|
|
729
|
+
ratio = click_x / h_track.width
|
|
730
|
+
self.scroll_x = max(0, min(max_scroll_x, ratio * max_scroll_x))
|
|
731
|
+
return True
|
|
732
|
+
|
|
733
|
+
if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
|
|
734
|
+
if self._dragging_v_thumb or self._dragging_h_thumb:
|
|
735
|
+
self._dragging_v_thumb = False
|
|
736
|
+
self._dragging_h_thumb = False
|
|
737
|
+
return True
|
|
738
|
+
|
|
739
|
+
if self.is_hovered and event.type == pygame.MOUSEWHEEL:
|
|
740
|
+
if pygame.key.get_mods() & pygame.KMOD_SHIFT:
|
|
741
|
+
self.scroll_x = max(
|
|
742
|
+
0, min(max_scroll_x, self.scroll_x - event.y * self.scroll_speed)
|
|
743
|
+
)
|
|
744
|
+
else:
|
|
745
|
+
self.scroll_y = max(
|
|
746
|
+
0, min(max_scroll_y, self.scroll_y - event.y * self.scroll_speed)
|
|
747
|
+
)
|
|
748
|
+
return True
|
|
749
|
+
|
|
750
|
+
if hasattr(event, "pos"):
|
|
751
|
+
px, py = event.pos
|
|
752
|
+
if not self.contains_point(px, py) and event.type in (
|
|
753
|
+
pygame.MOUSEBUTTONDOWN,
|
|
754
|
+
pygame.MOUSEBUTTONUP,
|
|
755
|
+
):
|
|
756
|
+
return False
|
|
757
|
+
|
|
758
|
+
canvas_px = px - self.real_x + self.scroll_x
|
|
759
|
+
canvas_py = py - self.real_y + self.scroll_y
|
|
760
|
+
|
|
761
|
+
translated_dict = dict(event.__dict__)
|
|
762
|
+
translated_dict["pos"] = (canvas_px, canvas_py)
|
|
763
|
+
translated_event = pygame.event.Event(event.type, translated_dict)
|
|
764
|
+
|
|
765
|
+
for child in reversed(self.get_sorted_children()):
|
|
766
|
+
if child.handle_event(translated_event):
|
|
767
|
+
return True
|
|
768
|
+
else:
|
|
769
|
+
for child in reversed(self.get_sorted_children()):
|
|
770
|
+
if child.handle_event(event):
|
|
771
|
+
return True
|
|
772
|
+
|
|
773
|
+
return False
|
|
774
|
+
|
|
775
|
+
def draw_foreground(self, surface: pygame.Surface):
|
|
776
|
+
v_track, v_thumb, h_track, h_thumb = self._get_scrollbar_rects()
|
|
777
|
+
|
|
778
|
+
if v_track and v_thumb:
|
|
779
|
+
if self.scrollbar_track_color:
|
|
780
|
+
pygame.draw.rect(surface, self.scrollbar_track_color, v_track)
|
|
781
|
+
|
|
782
|
+
if self._dragging_v_thumb:
|
|
783
|
+
thumb_color = self.scrollbar_active_color
|
|
784
|
+
elif self._v_thumb_hovered:
|
|
785
|
+
thumb_color = self.scrollbar_hover_color
|
|
786
|
+
else:
|
|
787
|
+
thumb_color = self.scrollbar_thumb_color
|
|
788
|
+
|
|
789
|
+
pygame.draw.rect(surface, thumb_color, v_thumb)
|
|
790
|
+
|
|
791
|
+
if h_track and h_thumb:
|
|
792
|
+
if self.scrollbar_track_color:
|
|
793
|
+
pygame.draw.rect(surface, self.scrollbar_track_color, h_track)
|
|
794
|
+
|
|
795
|
+
if self._dragging_h_thumb:
|
|
796
|
+
thumb_color = self.scrollbar_active_color
|
|
797
|
+
elif self._h_thumb_hovered:
|
|
798
|
+
thumb_color = self.scrollbar_hover_color
|
|
799
|
+
else:
|
|
800
|
+
thumb_color = self.scrollbar_thumb_color
|
|
801
|
+
|
|
802
|
+
pygame.draw.rect(surface, thumb_color, h_thumb)
|
|
803
|
+
|
|
804
|
+
def draw(self, surface: pygame.Surface):
|
|
805
|
+
if not self.is_globally_visible or not self.canvas_surface:
|
|
806
|
+
return
|
|
807
|
+
|
|
808
|
+
self.draw_background(surface)
|
|
809
|
+
|
|
810
|
+
self.canvas_surface.fill((0, 0, 0, 0))
|
|
811
|
+
for child in self.get_sorted_children():
|
|
812
|
+
child.draw(self.canvas_surface)
|
|
813
|
+
|
|
814
|
+
prev_clip = surface.get_clip()
|
|
815
|
+
frame_rect = pygame.Rect(
|
|
816
|
+
self.real_x, self.real_y, self.real_width, self.real_height
|
|
817
|
+
)
|
|
818
|
+
surface.set_clip(frame_rect.clip(prev_clip))
|
|
819
|
+
|
|
820
|
+
visible_area = pygame.Rect(
|
|
821
|
+
self.scroll_x, self.scroll_y, self.real_width, self.real_height
|
|
822
|
+
)
|
|
823
|
+
surface.blit(
|
|
824
|
+
self.canvas_surface,
|
|
825
|
+
(self.real_x, self.real_y),
|
|
826
|
+
area=visible_area,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
surface.set_clip(prev_clip)
|
|
830
|
+
|
|
831
|
+
self.draw_foreground(surface)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nyuGUI
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight GUI library featuring dynamic scale/offset layouting.
|
|
5
|
+
Author: Wjanek13
|
|
6
|
+
Project-URL: Documentation, https://nyugui-documentation.netlify.app
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Requires-Dist: pygame>=2.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pygame>=2.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nyuGUI
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nyuGUI"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A lightweight GUI library featuring dynamic scale/offset layouting."
|
|
9
|
+
authors = [{ name = "Wjanek13" }]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pygame>=2.0.0"
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
|
|
15
|
+
[tool.setuptools.package-data]
|
|
16
|
+
nyugui = ["assets/*.png"]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
"Documentation" = "https://nyugui-documentation.netlify.app"
|
nyugui-0.1.0/setup.cfg
ADDED