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 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,9 @@
1
+ pyproject.toml
2
+ nyuGUI/__init__.py
3
+ nyuGUI/core.py
4
+ nyuGUI/widgets.py
5
+ nyuGUI.egg-info/PKG-INFO
6
+ nyuGUI.egg-info/SOURCES.txt
7
+ nyuGUI.egg-info/dependency_links.txt
8
+ nyuGUI.egg-info/requires.txt
9
+ nyuGUI.egg-info/top_level.txt
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+