cudag 0.3.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cudag/__init__.py +334 -0
- cudag/annotation/__init__.py +77 -0
- cudag/annotation/codegen.py +648 -0
- cudag/annotation/config.py +545 -0
- cudag/annotation/loader.py +342 -0
- cudag/annotation/scaffold.py +121 -0
- cudag/annotation/transcription.py +296 -0
- cudag/cli/__init__.py +5 -0
- cudag/cli/main.py +315 -0
- cudag/cli/new.py +873 -0
- cudag/core/__init__.py +364 -0
- cudag/core/button.py +137 -0
- cudag/core/canvas.py +222 -0
- cudag/core/config.py +70 -0
- cudag/core/coords.py +233 -0
- cudag/core/data_grid.py +804 -0
- cudag/core/dataset.py +678 -0
- cudag/core/distribution.py +136 -0
- cudag/core/drawing.py +75 -0
- cudag/core/fonts.py +156 -0
- cudag/core/generator.py +163 -0
- cudag/core/grid.py +367 -0
- cudag/core/grounding_task.py +247 -0
- cudag/core/icon.py +207 -0
- cudag/core/iconlist_task.py +301 -0
- cudag/core/models.py +1251 -0
- cudag/core/random.py +130 -0
- cudag/core/renderer.py +190 -0
- cudag/core/screen.py +402 -0
- cudag/core/scroll_task.py +254 -0
- cudag/core/scrollable_grid.py +447 -0
- cudag/core/state.py +110 -0
- cudag/core/task.py +293 -0
- cudag/core/taskbar.py +350 -0
- cudag/core/text.py +212 -0
- cudag/core/utils.py +82 -0
- cudag/data/surnames.txt +5000 -0
- cudag/modal_apps/__init__.py +4 -0
- cudag/modal_apps/archive.py +103 -0
- cudag/modal_apps/extract.py +138 -0
- cudag/modal_apps/preprocess.py +529 -0
- cudag/modal_apps/upload.py +317 -0
- cudag/prompts/SYSTEM_PROMPT.txt +104 -0
- cudag/prompts/__init__.py +33 -0
- cudag/prompts/system.py +43 -0
- cudag/prompts/tools.py +382 -0
- cudag/py.typed +0 -0
- cudag/schemas/filesystem.json +90 -0
- cudag/schemas/test_record.schema.json +113 -0
- cudag/schemas/train_record.schema.json +90 -0
- cudag/server/__init__.py +21 -0
- cudag/server/app.py +232 -0
- cudag/server/services/__init__.py +9 -0
- cudag/server/services/generator.py +128 -0
- cudag/templates/scripts/archive.sh +35 -0
- cudag/templates/scripts/build.sh +13 -0
- cudag/templates/scripts/extract.sh +54 -0
- cudag/templates/scripts/generate.sh +116 -0
- cudag/templates/scripts/pre-commit.sh +44 -0
- cudag/templates/scripts/preprocess.sh +46 -0
- cudag/templates/scripts/upload.sh +63 -0
- cudag/templates/scripts/verify.py +428 -0
- cudag/validation/__init__.py +35 -0
- cudag/validation/validate.py +508 -0
- cudag-0.3.10.dist-info/METADATA +570 -0
- cudag-0.3.10.dist-info/RECORD +69 -0
- cudag-0.3.10.dist-info/WHEEL +4 -0
- cudag-0.3.10.dist-info/entry_points.txt +2 -0
- cudag-0.3.10.dist-info/licenses/LICENSE +66 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# Copyright (c) 2025 Tylt LLC. All rights reserved.
|
|
2
|
+
# CONFIDENTIAL AND PROPRIETARY. Unauthorized use, copying, or distribution
|
|
3
|
+
# is strictly prohibited. For licensing inquiries: hello@claimhawk.app
|
|
4
|
+
|
|
5
|
+
"""Scrollable data grid abstraction for VLM training.
|
|
6
|
+
|
|
7
|
+
Provides reusable components for rendering scrollable grids with:
|
|
8
|
+
- Column definitions and text wrapping
|
|
9
|
+
- Row rendering with variable heights
|
|
10
|
+
- Scroll position and viewport calculations
|
|
11
|
+
- Scrollbar rendering
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Sequence
|
|
18
|
+
|
|
19
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ColumnDef:
|
|
24
|
+
"""Definition for a grid column."""
|
|
25
|
+
|
|
26
|
+
id: str
|
|
27
|
+
"""Column identifier (matches row dict keys)."""
|
|
28
|
+
|
|
29
|
+
label: str
|
|
30
|
+
"""Display label for header."""
|
|
31
|
+
|
|
32
|
+
x: int
|
|
33
|
+
"""X position of column start."""
|
|
34
|
+
|
|
35
|
+
align: str = "left"
|
|
36
|
+
"""Text alignment: 'left' or 'right'."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ScrollableGridGeometry:
|
|
41
|
+
"""Geometry for a scrollable grid area."""
|
|
42
|
+
|
|
43
|
+
x: int
|
|
44
|
+
"""X position of grid top-left."""
|
|
45
|
+
|
|
46
|
+
y: int
|
|
47
|
+
"""Y position of grid top-left."""
|
|
48
|
+
|
|
49
|
+
width: int
|
|
50
|
+
"""Total width of grid area."""
|
|
51
|
+
|
|
52
|
+
height: int
|
|
53
|
+
"""Total height of grid area."""
|
|
54
|
+
|
|
55
|
+
padding: int = 0
|
|
56
|
+
"""Internal padding."""
|
|
57
|
+
|
|
58
|
+
header_height: int = 0
|
|
59
|
+
"""Height of fixed header row."""
|
|
60
|
+
|
|
61
|
+
scrollbar_width: int = 19
|
|
62
|
+
"""Width of scrollbar track."""
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def content_width(self) -> int:
|
|
66
|
+
"""Width available for content (excluding scrollbar)."""
|
|
67
|
+
return self.width - self.scrollbar_width - self.padding * 2
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def content_height(self) -> int:
|
|
71
|
+
"""Height available for content (excluding header)."""
|
|
72
|
+
return self.height - self.header_height - self.padding * 2
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def center(self) -> tuple[int, int]:
|
|
76
|
+
"""Center point of the grid (for scroll actions)."""
|
|
77
|
+
cx = self.x + (self.width - self.scrollbar_width) // 2
|
|
78
|
+
cy = self.y + self.height // 2
|
|
79
|
+
return (cx, cy)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class ScrollState:
|
|
84
|
+
"""Scroll position state."""
|
|
85
|
+
|
|
86
|
+
page: int = 1
|
|
87
|
+
"""Current page number (1-based)."""
|
|
88
|
+
|
|
89
|
+
has_more: bool = True
|
|
90
|
+
"""Whether more content exists below."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class RowLayout:
|
|
95
|
+
"""Layout information for a rendered row."""
|
|
96
|
+
|
|
97
|
+
height: int
|
|
98
|
+
"""Rendered height of row."""
|
|
99
|
+
|
|
100
|
+
wrapped_text: dict[str, list[str]]
|
|
101
|
+
"""Wrapped text lines per column."""
|
|
102
|
+
|
|
103
|
+
data: dict[str, str]
|
|
104
|
+
"""Original row data."""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ScrollableGrid:
|
|
108
|
+
"""A scrollable data grid that renders rows with text wrapping.
|
|
109
|
+
|
|
110
|
+
Usage:
|
|
111
|
+
grid = ScrollableGrid(
|
|
112
|
+
geometry=ScrollableGridGeometry(x=0, y=100, width=800, height=300),
|
|
113
|
+
columns=[
|
|
114
|
+
ColumnDef(id="name", label="Name", x=0),
|
|
115
|
+
ColumnDef(id="value", label="Value", x=200, align="right"),
|
|
116
|
+
],
|
|
117
|
+
font=ImageFont.truetype("arial.ttf", 12),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Render rows
|
|
121
|
+
body_image, row_layouts = grid.render_rows(rows)
|
|
122
|
+
|
|
123
|
+
# Get visible portion
|
|
124
|
+
visible = grid.get_visible_slice(body_image, scroll_state)
|
|
125
|
+
|
|
126
|
+
# Compose onto base image
|
|
127
|
+
grid.compose_onto(base_image, visible, scroll_state, body_image.height)
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
geometry: ScrollableGridGeometry,
|
|
133
|
+
columns: Sequence[ColumnDef],
|
|
134
|
+
font: ImageFont.FreeTypeFont,
|
|
135
|
+
cell_padding: int = 3,
|
|
136
|
+
line_color: tuple[int, int, int] = (210, 210, 210),
|
|
137
|
+
text_color: tuple[int, int, int] = (0, 0, 0),
|
|
138
|
+
bg_color: tuple[int, int, int] = (255, 255, 255),
|
|
139
|
+
):
|
|
140
|
+
"""Initialize scrollable grid.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
geometry: Grid positioning and sizing.
|
|
144
|
+
columns: Column definitions.
|
|
145
|
+
font: Font for rendering text.
|
|
146
|
+
cell_padding: Padding inside each cell.
|
|
147
|
+
line_color: Color for grid lines.
|
|
148
|
+
text_color: Color for text.
|
|
149
|
+
bg_color: Background color.
|
|
150
|
+
"""
|
|
151
|
+
self.geometry = geometry
|
|
152
|
+
self.columns = list(columns)
|
|
153
|
+
self.font = font
|
|
154
|
+
self.cell_padding = cell_padding
|
|
155
|
+
self.line_color = line_color
|
|
156
|
+
self.text_color = text_color
|
|
157
|
+
self.bg_color = bg_color
|
|
158
|
+
|
|
159
|
+
# Compute column widths
|
|
160
|
+
self._column_widths = self._compute_column_widths()
|
|
161
|
+
self._line_height = self._compute_line_height()
|
|
162
|
+
|
|
163
|
+
def _compute_column_widths(self) -> list[int]:
|
|
164
|
+
"""Compute widths from column x positions."""
|
|
165
|
+
widths: list[int] = []
|
|
166
|
+
total_width = self.geometry.content_width
|
|
167
|
+
for i, col in enumerate(self.columns):
|
|
168
|
+
if i + 1 < len(self.columns):
|
|
169
|
+
widths.append(self.columns[i + 1].x - col.x)
|
|
170
|
+
else:
|
|
171
|
+
widths.append(total_width - col.x)
|
|
172
|
+
return widths
|
|
173
|
+
|
|
174
|
+
def _compute_line_height(self) -> int:
|
|
175
|
+
"""Get line height from font metrics."""
|
|
176
|
+
ascent, descent = self.font.getmetrics()
|
|
177
|
+
return int(ascent) + int(descent)
|
|
178
|
+
|
|
179
|
+
def _wrap_text(self, text: str, max_width: int) -> list[str]:
|
|
180
|
+
"""Wrap text to fit within max_width pixels."""
|
|
181
|
+
words = text.split()
|
|
182
|
+
if not words:
|
|
183
|
+
return [""]
|
|
184
|
+
lines: list[str] = []
|
|
185
|
+
current = words[0]
|
|
186
|
+
for word in words[1:]:
|
|
187
|
+
candidate = f"{current} {word}"
|
|
188
|
+
if self.font.getlength(candidate) <= max_width:
|
|
189
|
+
current = candidate
|
|
190
|
+
else:
|
|
191
|
+
lines.append(current)
|
|
192
|
+
current = word
|
|
193
|
+
lines.append(current)
|
|
194
|
+
return lines
|
|
195
|
+
|
|
196
|
+
def _compute_row_height(self, row: dict[str, str]) -> tuple[int, dict[str, list[str]]]:
|
|
197
|
+
"""Calculate row height and wrapped text for all columns."""
|
|
198
|
+
wrapped: dict[str, list[str]] = {}
|
|
199
|
+
max_lines = 1
|
|
200
|
+
for idx, col in enumerate(self.columns):
|
|
201
|
+
width = self._column_widths[idx]
|
|
202
|
+
lines = self._wrap_text(
|
|
203
|
+
row.get(col.id, ""),
|
|
204
|
+
max_width=max(width - self.cell_padding * 2, 10),
|
|
205
|
+
)
|
|
206
|
+
wrapped[col.id] = lines
|
|
207
|
+
max_lines = max(max_lines, len(lines))
|
|
208
|
+
height = max_lines * self._line_height + self.cell_padding * 2
|
|
209
|
+
return height, wrapped
|
|
210
|
+
|
|
211
|
+
def render_rows(self, rows: Sequence[dict[str, str]]) -> tuple[Image.Image, list[RowLayout]]:
|
|
212
|
+
"""Render all rows to a body image.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
rows: List of row data dicts.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
(body_image, row_layouts) - The rendered image and layout info.
|
|
219
|
+
"""
|
|
220
|
+
# First pass: compute layouts
|
|
221
|
+
row_layouts: list[RowLayout] = []
|
|
222
|
+
total_height = 0
|
|
223
|
+
for row in rows:
|
|
224
|
+
height, wrapped = self._compute_row_height(row)
|
|
225
|
+
row_layouts.append(RowLayout(height=height, wrapped_text=wrapped, data=row))
|
|
226
|
+
total_height += height
|
|
227
|
+
|
|
228
|
+
# Create body image
|
|
229
|
+
body_width = sum(self._column_widths)
|
|
230
|
+
body_image = Image.new("RGB", (body_width, max(total_height, 1)), color=self.bg_color)
|
|
231
|
+
draw = ImageDraw.Draw(body_image)
|
|
232
|
+
|
|
233
|
+
# Second pass: render
|
|
234
|
+
y = 0
|
|
235
|
+
for layout in row_layouts:
|
|
236
|
+
for col_idx, col in enumerate(self.columns):
|
|
237
|
+
x_start = col.x
|
|
238
|
+
x_end = col.x + self._column_widths[col_idx]
|
|
239
|
+
lines = layout.wrapped_text[col.id]
|
|
240
|
+
for line_idx, line in enumerate(lines):
|
|
241
|
+
if col.align == "right":
|
|
242
|
+
text_width = int(self.font.getlength(line))
|
|
243
|
+
text_x = max(x_end - self.cell_padding - text_width, x_start + self.cell_padding)
|
|
244
|
+
else:
|
|
245
|
+
text_x = x_start + self.cell_padding
|
|
246
|
+
text_y = y + self.cell_padding + line_idx * self._line_height
|
|
247
|
+
draw.text((text_x, text_y), line, font=self.font, fill=self.text_color)
|
|
248
|
+
|
|
249
|
+
y += layout.height
|
|
250
|
+
# Horizontal line after row
|
|
251
|
+
draw.line([(0, y - 1), (body_width, y - 1)], fill=self.line_color)
|
|
252
|
+
|
|
253
|
+
# Vertical column separators
|
|
254
|
+
for col in self.columns:
|
|
255
|
+
draw.line([(col.x, 0), (col.x, total_height)], fill=self.line_color)
|
|
256
|
+
|
|
257
|
+
return body_image, row_layouts
|
|
258
|
+
|
|
259
|
+
def get_scroll_offset(
|
|
260
|
+
self,
|
|
261
|
+
scroll_state: ScrollState,
|
|
262
|
+
content_height: int,
|
|
263
|
+
) -> int:
|
|
264
|
+
"""Calculate scroll offset from scroll state.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
scroll_state: Current scroll state.
|
|
268
|
+
content_height: Total height of content.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Pixel offset from top.
|
|
272
|
+
"""
|
|
273
|
+
visible_height = self.geometry.content_height
|
|
274
|
+
max_offset = max(content_height - visible_height, 0)
|
|
275
|
+
|
|
276
|
+
if not scroll_state.has_more:
|
|
277
|
+
return max_offset
|
|
278
|
+
|
|
279
|
+
page_height = max(visible_height, 1)
|
|
280
|
+
offset = max((scroll_state.page - 1) * page_height, 0)
|
|
281
|
+
return min(offset, max_offset)
|
|
282
|
+
|
|
283
|
+
def get_visible_slice(
|
|
284
|
+
self,
|
|
285
|
+
body_image: Image.Image,
|
|
286
|
+
scroll_state: ScrollState,
|
|
287
|
+
) -> Image.Image:
|
|
288
|
+
"""Get visible portion of body image.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
body_image: Full rendered body.
|
|
292
|
+
scroll_state: Current scroll state.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Cropped image for visible viewport.
|
|
296
|
+
"""
|
|
297
|
+
visible_height = self.geometry.content_height
|
|
298
|
+
start_offset = self.get_scroll_offset(scroll_state, body_image.height)
|
|
299
|
+
|
|
300
|
+
if body_image.height <= visible_height:
|
|
301
|
+
canvas = Image.new("RGB", (body_image.width, visible_height), color=self.bg_color)
|
|
302
|
+
canvas.paste(body_image, (0, 0))
|
|
303
|
+
return canvas
|
|
304
|
+
|
|
305
|
+
end_offset = min(start_offset + visible_height, body_image.height)
|
|
306
|
+
cropped = body_image.crop((0, start_offset, body_image.width, end_offset))
|
|
307
|
+
|
|
308
|
+
if cropped.height == visible_height:
|
|
309
|
+
return cropped
|
|
310
|
+
|
|
311
|
+
canvas = Image.new("RGB", (body_image.width, visible_height), color=self.bg_color)
|
|
312
|
+
canvas.paste(cropped, (0, 0))
|
|
313
|
+
return canvas
|
|
314
|
+
|
|
315
|
+
def get_visible_row_indices(
|
|
316
|
+
self,
|
|
317
|
+
row_layouts: Sequence[RowLayout],
|
|
318
|
+
scroll_state: ScrollState,
|
|
319
|
+
content_height: int,
|
|
320
|
+
) -> list[int]:
|
|
321
|
+
"""Get indices of rows visible in current viewport.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
row_layouts: Layout info for all rows.
|
|
325
|
+
scroll_state: Current scroll state.
|
|
326
|
+
content_height: Total content height.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
List of visible row indices.
|
|
330
|
+
"""
|
|
331
|
+
start_offset = self.get_scroll_offset(scroll_state, content_height)
|
|
332
|
+
end_offset = start_offset + self.geometry.content_height
|
|
333
|
+
visible: list[int] = []
|
|
334
|
+
|
|
335
|
+
y = 0
|
|
336
|
+
for idx, layout in enumerate(row_layouts):
|
|
337
|
+
row_top = y
|
|
338
|
+
row_bottom = y + layout.height
|
|
339
|
+
if row_bottom > start_offset and row_top < end_offset:
|
|
340
|
+
visible.append(idx)
|
|
341
|
+
y = row_bottom
|
|
342
|
+
|
|
343
|
+
return visible
|
|
344
|
+
|
|
345
|
+
def render_scrollbar(
|
|
346
|
+
self,
|
|
347
|
+
content_height: int,
|
|
348
|
+
scroll_state: ScrollState,
|
|
349
|
+
min_thumb: int = 30,
|
|
350
|
+
) -> Image.Image:
|
|
351
|
+
"""Render scrollbar track with thumb.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
content_height: Total content height.
|
|
355
|
+
scroll_state: Current scroll state.
|
|
356
|
+
min_thumb: Minimum thumb height.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Scrollbar image.
|
|
360
|
+
"""
|
|
361
|
+
track_height = self.geometry.content_height
|
|
362
|
+
visible_height = track_height
|
|
363
|
+
width = self.geometry.scrollbar_width
|
|
364
|
+
|
|
365
|
+
# Create track
|
|
366
|
+
track = Image.new("RGB", (width, track_height), color=(240, 240, 240))
|
|
367
|
+
draw = ImageDraw.Draw(track)
|
|
368
|
+
|
|
369
|
+
if content_height <= 0 or visible_height <= 0:
|
|
370
|
+
return track
|
|
371
|
+
|
|
372
|
+
# Calculate thumb size and position
|
|
373
|
+
ratio = visible_height / content_height
|
|
374
|
+
thumb_height = max(min_thumb, int(track_height * ratio))
|
|
375
|
+
max_offset = max(content_height - visible_height, 1)
|
|
376
|
+
start_offset = self.get_scroll_offset(scroll_state, content_height)
|
|
377
|
+
travel = track_height - thumb_height
|
|
378
|
+
thumb_y = int((start_offset / max_offset) * travel) if travel > 0 else 0
|
|
379
|
+
|
|
380
|
+
# Draw thumb (thin dark line)
|
|
381
|
+
thumb_width = 2
|
|
382
|
+
thumb_x0 = (width - thumb_width) // 2
|
|
383
|
+
thumb_x1 = thumb_x0 + thumb_width - 1
|
|
384
|
+
draw.rectangle(
|
|
385
|
+
[(thumb_x0, thumb_y), (thumb_x1, thumb_y + thumb_height)],
|
|
386
|
+
fill=(100, 100, 100),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return track
|
|
390
|
+
|
|
391
|
+
def compose_onto(
|
|
392
|
+
self,
|
|
393
|
+
base_image: Image.Image,
|
|
394
|
+
visible_content: Image.Image,
|
|
395
|
+
scroll_state: ScrollState,
|
|
396
|
+
content_height: int,
|
|
397
|
+
header_image: Image.Image | None = None,
|
|
398
|
+
) -> dict[str, Any]:
|
|
399
|
+
"""Compose grid onto base image.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
base_image: Base image to paste onto (modified in place).
|
|
403
|
+
visible_content: Visible portion of content.
|
|
404
|
+
scroll_state: Current scroll state.
|
|
405
|
+
content_height: Total content height.
|
|
406
|
+
header_image: Optional header row image.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Metadata dict with scroll info.
|
|
410
|
+
"""
|
|
411
|
+
geom = self.geometry
|
|
412
|
+
|
|
413
|
+
# Create grid canvas
|
|
414
|
+
grid_canvas = Image.new("RGB", (geom.width, geom.height), color=self.bg_color)
|
|
415
|
+
|
|
416
|
+
# Add header if provided
|
|
417
|
+
y_offset = geom.padding
|
|
418
|
+
if header_image and geom.header_height > 0:
|
|
419
|
+
grid_canvas.paste(header_image, (geom.padding, y_offset))
|
|
420
|
+
y_offset += geom.header_height
|
|
421
|
+
|
|
422
|
+
# Add visible content
|
|
423
|
+
grid_canvas.paste(visible_content, (geom.padding, y_offset))
|
|
424
|
+
|
|
425
|
+
# Add scrollbar
|
|
426
|
+
if geom.scrollbar_width > 0:
|
|
427
|
+
scrollbar = self.render_scrollbar(content_height, scroll_state)
|
|
428
|
+
grid_canvas.paste(
|
|
429
|
+
scrollbar,
|
|
430
|
+
(geom.width - geom.scrollbar_width, geom.padding),
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Paste onto base
|
|
434
|
+
base_image.paste(grid_canvas, (geom.x, geom.y))
|
|
435
|
+
|
|
436
|
+
# Return metadata
|
|
437
|
+
start_offset = self.get_scroll_offset(scroll_state, content_height)
|
|
438
|
+
max_offset = max(content_height - geom.content_height, 0)
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
"grid_center": geom.center,
|
|
442
|
+
"scroll_offset": start_offset,
|
|
443
|
+
"visible_height": geom.content_height,
|
|
444
|
+
"content_height": content_height,
|
|
445
|
+
"has_more_above": start_offset > 0,
|
|
446
|
+
"has_more_below": start_offset < max_offset,
|
|
447
|
+
}
|
cudag/core/state.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Copyright (c) 2025 Tylt LLC. All rights reserved.
|
|
2
|
+
# CONFIDENTIAL AND PROPRIETARY. Unauthorized use, copying, or distribution
|
|
3
|
+
# is strictly prohibited. For licensing inquiries: hello@claimhawk.app
|
|
4
|
+
|
|
5
|
+
"""Base state class for screen state data.
|
|
6
|
+
|
|
7
|
+
State represents the dynamic data that populates a screen at render time.
|
|
8
|
+
Each generator project defines its own State class with relevant fields.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, ClassVar
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BaseState:
|
|
19
|
+
"""Base class for screen state.
|
|
20
|
+
|
|
21
|
+
Subclass this to define the dynamic data for your screen.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
@dataclass
|
|
25
|
+
class CalendarState(BaseState):
|
|
26
|
+
year: int
|
|
27
|
+
month: int
|
|
28
|
+
selected_day: int
|
|
29
|
+
current_day: int
|
|
30
|
+
target_day: int
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def month_name(self) -> str:
|
|
34
|
+
import calendar
|
|
35
|
+
return calendar.month_name[self.month]
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# Subclasses can define validation rules
|
|
39
|
+
_validators: ClassVar[list[str]] = []
|
|
40
|
+
|
|
41
|
+
def validate(self) -> list[str]:
|
|
42
|
+
"""Validate the state and return list of errors.
|
|
43
|
+
|
|
44
|
+
Override in subclass to add custom validation.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
List of error messages (empty if valid)
|
|
48
|
+
"""
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict[str, Any]:
|
|
52
|
+
"""Convert state to dictionary.
|
|
53
|
+
|
|
54
|
+
Useful for serialization and metadata.
|
|
55
|
+
"""
|
|
56
|
+
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, data: dict[str, Any]) -> BaseState:
|
|
60
|
+
"""Create state from dictionary."""
|
|
61
|
+
return cls(**data)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ScrollState(BaseState):
|
|
66
|
+
"""Common scroll state for scrollable regions.
|
|
67
|
+
|
|
68
|
+
Reusable across different generators that have scrolling.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
scroll_position: int = 0
|
|
72
|
+
"""Current scroll offset in pixels."""
|
|
73
|
+
|
|
74
|
+
has_more_above: bool = False
|
|
75
|
+
"""Whether there's content above the visible area."""
|
|
76
|
+
|
|
77
|
+
has_more_below: bool = True
|
|
78
|
+
"""Whether there's content below the visible area."""
|
|
79
|
+
|
|
80
|
+
page_size: int = 100
|
|
81
|
+
"""Visible area height in pixels."""
|
|
82
|
+
|
|
83
|
+
content_height: int = 0
|
|
84
|
+
"""Total content height in pixels."""
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def max_scroll(self) -> int:
|
|
88
|
+
"""Maximum scroll position."""
|
|
89
|
+
return max(0, self.content_height - self.page_size)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def at_top(self) -> bool:
|
|
93
|
+
"""Whether scrolled to top."""
|
|
94
|
+
return self.scroll_position <= 0
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def at_bottom(self) -> bool:
|
|
98
|
+
"""Whether scrolled to bottom."""
|
|
99
|
+
return self.scroll_position >= self.max_scroll
|
|
100
|
+
|
|
101
|
+
def scroll_by(self, pixels: int) -> ScrollState:
|
|
102
|
+
"""Return new state scrolled by given pixels."""
|
|
103
|
+
new_pos = max(0, min(self.scroll_position + pixels, self.max_scroll))
|
|
104
|
+
return ScrollState(
|
|
105
|
+
scroll_position=new_pos,
|
|
106
|
+
has_more_above=new_pos > 0,
|
|
107
|
+
has_more_below=new_pos < self.max_scroll,
|
|
108
|
+
page_size=self.page_size,
|
|
109
|
+
content_height=self.content_height,
|
|
110
|
+
)
|