-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGlueUi.py
More file actions
357 lines (288 loc) · 14.8 KB
/
GlueUi.py
File metadata and controls
357 lines (288 loc) · 14.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# GlueUi: A basic UI library built upon pySDL2 for Python 3.12
# Copyright (C) 2024 Ahmad "JailMan" S.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Literal
import sdl2
import sdl2.ext as sdl2ext
import sdl2.sdlttf as sdlttf
import ctypes
from SettingsHandler import GetSettings
class Nat2:
def __init__(self, X: int, Y: int):
self.X = X
self.Y = Y
def __add__(self, other):
# If adding another Nat2 instance
if isinstance(other, Nat2):
return Nat2(self.X + other.X, self.Y + other.Y)
# If adding an integer to both X and Y
elif isinstance(other, int):
return Nat2(self.X + other, self.Y + other)
# If adding an unsupported type, raise an error
else:
return NotImplemented
def __repr__(self):
return f'Nat2({self.X}, {self.Y})'
class Rect:
def __init__(self, XY: Nat2, WH: Nat2):
self.XY = XY
self.WH = WH
class Vec4:
def __init__(self, X: float, Y: float, Z: float, W: float = 1):
self.X = X
self.Y = Y
self.Z = Z
self.W = W
def ToSdl2Color(self):
return sdl2ext.Color(int(self.X * 255), int(self.Y * 255), int(self.Z * 255), int(self.W * 255))
def TupleToVec4(t: tuple[int, int, int]) -> Vec4:
return Vec4(t[0], t[1], t[2])
def ColorPaletteToVec4(l: list[tuple]) -> list[Vec4]:
return list([TupleToVec4(tup) for tup in l])
class GlueUiManager:
def __init__(self):
# Initialize SDL and TTF
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)
sdlttf.TTF_Init()
self.Window = sdl2.SDL_CreateWindow(
b"ChannelNotif",
sdl2.SDL_WINDOWPOS_CENTERED,
sdl2.SDL_WINDOWPOS_CENTERED,
848, 480, sdl2.SDL_WINDOW_SHOWN
)
# Use SDL_RENDERER_PRESENTVSYNC for smooth rendering and double buffering
self.Renderer = sdl2.SDL_CreateRenderer(self.Window, -1, sdl2.SDL_RENDERER_ACCELERATED | sdl2.SDL_RENDERER_PRESENTVSYNC)
# Get window surface for legacy purposes (if needed later)
self.Surface = sdl2.SDL_GetWindowSurface(self.Window)
self.Event = sdl2.SDL_Event()
self.Running: bool = True
self.Fonts: dict[str, any] = {}
self.ActiveMenu: Literal["Main", "Settings", "Schedule"] = "Main"
# Initialize the list to store images and textures
self.Images: list = [] # List to store surfaces
self.Textures: list = [] # List to store textures
# Checkbox and button states
self.CheckboxStates: dict = {}
self.Buttons: dict[str, dict] = {} # Dictionary to store states of all buttons
# Color palette and UI mode
self.ColorPalette: list[Vec4] = ColorPaletteToVec4(GetSettings().Ui.ColorPalette)
self.IsDarkMode: bool = GetSettings().Ui.IsDarkMode
def Begin(self):
sdl2.SDL_RenderClear(self.Renderer)
def Button(self, Label: str, Pos: Nat2, FontName: str, OnClick: callable,
ColorTextIdx: int = 2, ColorHoverIdx: int = 1, ColorNormalIdx: int = 3,
) -> tuple[Rect, bool, bool]:
"""
Creates and renders a button UI element.
Parameters:
Label (str): The text displayed on the button. This also serves as its identifier.
Pos (Nat2): The position (top-left corner) of the button in the window.
OnClick (callable): The function to call when the button is clicked.
ColorTextIdx (int): The color palette index for the button text.
ColorHoverIdx (int): The color palette index for the button background when hovered.
ColorNormalIdx (int): The color palette index for the button background in its normal state.
FontIdx (int, optional): The index of the font to use for rendering the label. Defaults to 0.
Returns:
tuple[Rect, bool, bool]:
- `Rect`: The rectangle defining the button's position and size.
- `bool`: Whether the button was clicked in the current frame.
- `bool`: Whether the button is currently being hovered.
"""
# Initialize button state if not already present
if Label not in self.Buttons:
self.Buttons[Label] = {"active": True, "mouse_down": True, "prev_mouse_down": True} # HACK
# Calculate button rectangle
TextRect: Rect = self.Text(Label, Pos, FontName, ColorTextIdx, NoDraw=True)
ButtonRect: Rect = Rect(TextRect.XY + -10, TextRect.WH + 20)
clicked = False # Tracks whether the button was clicked in this frame
hovered = self.RectIsHover(ButtonRect) # Tracks whether the button is hovered
# Get the current mouse state (pressed or released)
mouse_down = self.IsClick("Left")
# Check if mouse is hovering and clicked
if hovered:
ColorIdx = ColorHoverIdx
# We register a click only when the mouse goes from "not pressed" to "pressed"
if mouse_down and not self.Buttons[Label]["prev_mouse_down"]:
OnClick() # Call the click handler
clicked = True # Mark the button as clicked
else:
ColorIdx = ColorNormalIdx
# Render the button
self.Rect(ButtonRect.XY, ButtonRect.WH, ColorIdx)
self.Text(Label, Pos, FontName, ColorTextIdx)
# Update button state
self.Buttons[Label]["prev_mouse_down"] = mouse_down
# Return the button's rectangle, clicked state, and hovered state
return ButtonRect, clicked, hovered
def GetModeIcon(self):
return "" if not self.IsDarkMode else ""
def SwitchMode(self):
self.IsDarkMode = not self.IsDarkMode
def ColorPaletteMode(self) -> list[Vec4]:
return self.ColorPalette[::-1] if not self.IsDarkMode else self.ColorPalette
def RectBorder(self, Pos: Nat2, Size: Nat2, ColorIdx: int, Thickness: int = 1):
"""
Draws a border for a rectangle at a specified position and size using `self.Rect`.
Parameters:
Pos (Nat2): Top-left position of the rectangle.
Size (Nat2): Width and height of the rectangle.
Color (Vec4): RGBA color of the border.
Thickness (int): Thickness of the border lines.
"""
# Top border
self.Rect(Pos, Nat2(Size.X, Thickness), ColorIdx)
# Left border
self.Rect(Pos, Nat2(Thickness, Size.Y), ColorIdx)
# Bottom border
self.Rect(Nat2(Pos.X, Pos.Y + Size.Y - Thickness), Nat2(Size.X, Thickness), ColorIdx)
# Right border
self.Rect(Nat2(Pos.X + Size.X - Thickness, Pos.Y), Nat2(Thickness, Size.Y), ColorIdx)
def Checkbox(self, Label: str, Pos: Nat2, FontName: str,
ColorBorderIdx: int = 0, ColorInnerIdx: int = 1,
ColorInnerHoverIdx: int = 2, ColorTextIdx: int = 0) -> tuple[bool, bool]:
"""
Render a checkbox UI component and toggle its state when clicked.
Parameters:
Label (str): The label to display next to the checkbox.
Pos (Nat2): The position of the checkbox (top-left corner of the checkbox).
ColorBorderIdx (int): The color palette index for the checkbox border.
ColorInnerIdx (int): The color palette index for the checkbox fill when checked.
ColorInnerHoverIdx (int): The color palette index for the checkbox fill when hovered.
ColorTextIdx (int): The color palette index for the checkbox text.
Returns:
tuple[bool, bool]:
- `bool`: The current state of the checkbox (True for checked, False for unchecked).
- `bool`: Whether the checkbox is currently being hovered.
"""
# Define the checkbox size
CheckboxSize = Nat2(20, 20)
CheckboxRect = Rect(Pos, CheckboxSize)
# Initialize checkbox state if not already present
if Label not in self.CheckboxStates:
self.CheckboxStates[Label] = {"checked": False, "mouse_down": False}
hovered = self.RectIsHover(CheckboxRect) # Tracks whether the checkbox is hovered
# Handle mouse hover and click for toggling the state
if hovered:
if not self.CheckboxStates[Label]["checked"]:
self.Rect(Pos + Nat2(4, 4), Nat2(CheckboxSize.X - 8, CheckboxSize.Y - 8), ColorInnerHoverIdx)
if self.IsClick("Left"):
if not self.CheckboxStates[Label]["mouse_down"]:
self.CheckboxStates[Label]["checked"] = not self.CheckboxStates[Label]["checked"]
self.CheckboxStates[Label]["mouse_down"] = True
else:
self.CheckboxStates[Label]["mouse_down"] = False
else:
self.CheckboxStates[Label]["mouse_down"] = False
# Draw the filled rectangle if checked
if self.CheckboxStates[Label]["checked"]:
self.Rect(Pos + Nat2(4, 4), Nat2(CheckboxSize.X - 8, CheckboxSize.Y - 8), ColorInnerIdx)
# Draw the checkbox border
self.RectBorder(Pos, CheckboxSize, ColorBorderIdx, Thickness=2)
# Render the label
LabelPos = Pos + Nat2(CheckboxSize.X + 10, 0)
self.Text(Label, LabelPos, FontName, ColorIdx=ColorTextIdx)
# Return the checkbox's checked state and hovered state
return self.CheckboxStates[Label]["checked"], hovered
def ChangeActiveMenu(self, MenuName: str):
self.ActiveMenu = MenuName
def GetMouseState(self):
"""Get the current cursor position and mouse button states."""
x_pos = ctypes.c_int(0)
y_pos = ctypes.c_int(0)
# Get the mouse position and button states
button_state = sdl2.SDL_GetMouseState(ctypes.byref(x_pos), ctypes.byref(y_pos))
return x_pos.value, y_pos.value, button_state
def IsClick(self, Button: Literal["Left", "Right", "Middle"]) -> bool:
"""Check if the specified mouse button is currently clicked."""
_, _, button_state = self.GetMouseState()
if Button == "Left":
return bool(button_state & sdl2.SDL_BUTTON(sdl2.SDL_BUTTON_LEFT))
elif Button == "Right":
return bool(button_state & sdl2.SDL_BUTTON(sdl2.SDL_BUTTON_RIGHT))
elif Button == "Middle":
return bool(button_state & sdl2.SDL_BUTTON(sdl2.SDL_BUTTON_MIDDLE))
else:
raise ValueError("Button must be 'Left', 'Right', or 'Middle'")
def RectIsHover(self, TRect: Rect):
return self.PointInsideRect(self.GetCursorPosition(), TRect.XY, TRect.WH)
def ColorOnHover(self, TRect: Rect, Color1Idx: int, Color2Idx: int):
return Color1Idx if self.RectIsHover(TRect) else Color2Idx
def PointInsideRect(self, Point: Nat2, RectPos: Nat2, RectSize: Nat2) -> bool:
return RectPos.X <= Point.X <= RectPos.X + RectSize.X and RectPos.Y <= Point.Y <= RectPos.Y + RectSize.Y
def GetCursorPosition(self):
"""Get the current cursor position relative to the window."""
x_pos = ctypes.c_int(0)
y_pos = ctypes.c_int(0)
sdl2.SDL_GetMouseState(ctypes.byref(x_pos), ctypes.byref(y_pos))
return Nat2(x_pos.value, y_pos.value)
def Update(self):
"""Update the display surface."""
sdl2.SDL_UpdateWindowSurface(self.Window)
def CreateButton(self):
self.Buttons.append([False, False])
def Rect(self, Pos: Nat2, Size: Nat2, ColorIdx: int):
"""Draw a rectangle on the surface."""
Color = self.ColorPaletteMode()[ColorIdx]
ColorSdl = Color.ToSdl2Color()
RectSdl = sdl2.SDL_Rect(Pos.X, Pos.Y, Size.X, Size.Y)
sdl2.SDL_FillRect(self.Surface, RectSdl, sdl2.SDL_MapRGBA(self.Surface.contents.format,
ColorSdl.r, ColorSdl.g, ColorSdl.b, ColorSdl.a))
def LoadFont(self, Filepath: str, Name: str, Size: int = 24, Bold: bool = False):
"""Load a TTF font into the UiManager."""
Font = sdlttf.TTF_OpenFont(Filepath.encode('utf-8'), Size)
if Bold:
sdl2.sdlttf.TTF_SetFontStyle(Font, sdl2.sdlttf.TTF_STYLE_BOLD)
self.Fonts[Name] = Font
if not Font:
raise RuntimeError(f"Failed to load font from {Filepath}: {sdl2.SDL_GetError().decode('utf-8')}")
def Text(self, Str: str, Pos: Nat2, FontName: str, ColorIdx: int = 2, NoDraw: bool = False) -> Rect:
"""Render text on the surface."""
Color = self.ColorPaletteMode()[ColorIdx]
if len(self.Fonts) == 0:
raise RuntimeError("No font loaded. Use LoadFont() before rendering text.")
# Convert the color
ColorSdl = sdl2.SDL_Color(int(Color.X * 255), int(Color.Y * 255), int(Color.Z * 255), int(Color.W * 255))
# Render the text to an SDL surface
TextSurface = sdlttf.TTF_RenderUTF8_Blended(self.Fonts[FontName], Str.encode('utf-8'), ColorSdl)
if not TextSurface:
raise RuntimeError(f"Failed to render text: {sdl2.SDL_GetError().decode('utf-8')}")
# Blit the text surface onto the window surface
TextRect = sdl2.SDL_Rect(Pos.X, Pos.Y, TextSurface.contents.w, TextSurface.contents.h)
Rxy = Nat2(TextRect.x, TextRect.y)
Rwh = Nat2(TextRect.w, TextRect.h)
RRect = Rect(Rxy, Rwh)
if not NoDraw: sdl2.SDL_BlitSurface(TextSurface, None, self.Surface, TextRect)
# Free the temporary text surface
sdl2.SDL_FreeSurface(TextSurface)
return RRect
def TextWrapped(self, Str: str, Pos: Nat2, FontName: str, ColorIdx: int):
Strings = Str.split("\n")
for Idx, String in enumerate(Strings):
if String == "":
continue
self.Text(String, Pos + Nat2(0, Idx * 16), FontName, ColorIdx=ColorIdx)
def End(self):
"""Render each frame."""
while sdl2.SDL_PollEvent(ctypes.byref(self.Event)) != 0:
if self.Event.type == sdl2.SDL_QUIT:
self.Running = False
break
sdl2.SDL_RenderPresent(self.Renderer)
self.Update()
sdl2.SDL_Delay(1)
def Quit(self):
"""Clean up resources."""
for Font in self.Fonts:
sdlttf.TTF_CloseFont(self.Fonts[Font])
sdl2.SDL_DestroyWindow(self.Window)
sdl2.SDL_Quit()
sdlttf.TTF_Quit()