teek 0.1.0

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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +139 -0
  5. data/Rakefile +316 -0
  6. data/ext/teek/extconf.rb +79 -0
  7. data/ext/teek/stubs.h +33 -0
  8. data/ext/teek/tcl9compat.h +211 -0
  9. data/ext/teek/tcltkbridge.c +1597 -0
  10. data/ext/teek/tcltkbridge.h +42 -0
  11. data/ext/teek/tkfont.c +218 -0
  12. data/ext/teek/tkphoto.c +477 -0
  13. data/ext/teek/tkwin.c +144 -0
  14. data/lib/teek/background_none.rb +158 -0
  15. data/lib/teek/background_ractor4x.rb +410 -0
  16. data/lib/teek/background_thread.rb +272 -0
  17. data/lib/teek/debugger.rb +742 -0
  18. data/lib/teek/demo_support.rb +150 -0
  19. data/lib/teek/ractor_support.rb +246 -0
  20. data/lib/teek/version.rb +5 -0
  21. data/lib/teek.rb +540 -0
  22. data/sample/calculator.rb +260 -0
  23. data/sample/debug_demo.rb +45 -0
  24. data/sample/goldberg.rb +1803 -0
  25. data/sample/goldberg_helpers.rb +170 -0
  26. data/sample/minesweeper/assets/MINESWEEPER_0.png +0 -0
  27. data/sample/minesweeper/assets/MINESWEEPER_1.png +0 -0
  28. data/sample/minesweeper/assets/MINESWEEPER_2.png +0 -0
  29. data/sample/minesweeper/assets/MINESWEEPER_3.png +0 -0
  30. data/sample/minesweeper/assets/MINESWEEPER_4.png +0 -0
  31. data/sample/minesweeper/assets/MINESWEEPER_5.png +0 -0
  32. data/sample/minesweeper/assets/MINESWEEPER_6.png +0 -0
  33. data/sample/minesweeper/assets/MINESWEEPER_7.png +0 -0
  34. data/sample/minesweeper/assets/MINESWEEPER_8.png +0 -0
  35. data/sample/minesweeper/assets/MINESWEEPER_F.png +0 -0
  36. data/sample/minesweeper/assets/MINESWEEPER_M.png +0 -0
  37. data/sample/minesweeper/assets/MINESWEEPER_X.png +0 -0
  38. data/sample/minesweeper/minesweeper.rb +452 -0
  39. data/sample/threading_demo.rb +499 -0
  40. data/teek.gemspec +32 -0
  41. metadata +179 -0
@@ -0,0 +1,477 @@
1
+ /* tkphoto.c - Photo image C functions for tk-ng
2
+ *
3
+ * Fast pixel manipulation using Tk's photo image C API.
4
+ * These functions bypass Tcl string parsing for better performance.
5
+ */
6
+
7
+ #include "tcltkbridge.h"
8
+
9
+ /* ---------------------------------------------------------
10
+ * Interp#photo_put_block(photo_path, pixel_data, width, height, opts={})
11
+ *
12
+ * Fast pixel writes to a photo image using Tk_PhotoPutBlock.
13
+ * Much faster than Tcl's 'photo put' which requires parsing hex strings.
14
+ *
15
+ * Arguments:
16
+ * photo_path - Tcl path of the photo image (e.g., "i00001")
17
+ * pixel_data - Binary string of pixels (4 bytes per pixel)
18
+ * width - Image width in pixels
19
+ * height - Image height in pixels
20
+ * opts - Optional hash:
21
+ * :x, :y - destination offsets (default 0,0)
22
+ * :format - :rgba (default) or :argb
23
+ *
24
+ * The pixel_data must be exactly width * height * 4 bytes.
25
+ *
26
+ * Format :argb expects pixels packed as 0xAARRGGBB integers (little-endian: B,G,R,A bytes).
27
+ * This matches SDL2 and many graphics libraries.
28
+ *
29
+ * See: https://www.tcl-lang.org/man/tcl8.6/TkLib/FindPhoto.htm
30
+ * --------------------------------------------------------- */
31
+
32
+ static VALUE
33
+ interp_photo_put_block(int argc, VALUE *argv, VALUE self)
34
+ {
35
+ struct tcltk_interp *tip = get_interp(self);
36
+ VALUE photo_path, pixel_data, width_val, height_val, opts;
37
+ Tk_PhotoHandle photo;
38
+ Tk_PhotoImageBlock block;
39
+ int width, height, x_off, y_off;
40
+ int is_argb = 0;
41
+ long expected_size;
42
+
43
+ rb_scan_args(argc, argv, "41", &photo_path, &pixel_data, &width_val, &height_val, &opts);
44
+
45
+ StringValue(photo_path);
46
+ StringValue(pixel_data);
47
+ width = NUM2INT(width_val);
48
+ height = NUM2INT(height_val);
49
+
50
+ /* Validate dimensions */
51
+ if (width <= 0 || height <= 0) {
52
+ rb_raise(rb_eArgError, "width and height must be positive");
53
+ }
54
+
55
+ /* Validate data size */
56
+ expected_size = (long)width * height * 4;
57
+ if (RSTRING_LEN(pixel_data) != expected_size) {
58
+ rb_raise(rb_eArgError, "pixel_data size mismatch: expected %ld bytes, got %ld",
59
+ expected_size, RSTRING_LEN(pixel_data));
60
+ }
61
+
62
+ /* Parse options */
63
+ x_off = 0;
64
+ y_off = 0;
65
+ if (!NIL_P(opts) && TYPE(opts) == T_HASH) {
66
+ VALUE val;
67
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("x")));
68
+ if (!NIL_P(val)) x_off = NUM2INT(val);
69
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("y")));
70
+ if (!NIL_P(val)) y_off = NUM2INT(val);
71
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("format")));
72
+ if (!NIL_P(val) && TYPE(val) == T_SYMBOL) {
73
+ if (rb_intern("argb") == SYM2ID(val)) {
74
+ is_argb = 1;
75
+ }
76
+ }
77
+ }
78
+
79
+ /* Find the photo image by Tcl path */
80
+ photo = Tk_FindPhoto(tip->interp, StringValueCStr(photo_path));
81
+ if (!photo) {
82
+ rb_raise(eTclError, "photo image not found: %s", StringValueCStr(photo_path));
83
+ }
84
+
85
+ /* Set up the pixel block structure */
86
+ block.pixelPtr = (unsigned char *)RSTRING_PTR(pixel_data);
87
+ block.width = width;
88
+ block.height = height;
89
+ block.pitch = width * 4;
90
+ block.pixelSize = 4;
91
+
92
+ if (is_argb) {
93
+ /* ARGB: 0xAARRGGBB stored little-endian as bytes: [B, G, R, A] */
94
+ block.offset[0] = 2; /* Red at byte 2 */
95
+ block.offset[1] = 1; /* Green at byte 1 */
96
+ block.offset[2] = 0; /* Blue at byte 0 */
97
+ block.offset[3] = 3; /* Alpha at byte 3 */
98
+ } else {
99
+ /* RGBA: [R, G, B, A] */
100
+ block.offset[0] = 0;
101
+ block.offset[1] = 1;
102
+ block.offset[2] = 2;
103
+ block.offset[3] = 3;
104
+ }
105
+
106
+ /* Write pixels to the photo image */
107
+ if (Tk_PhotoPutBlock(tip->interp, photo, &block, x_off, y_off,
108
+ width, height, TK_PHOTO_COMPOSITE_SET) != TCL_OK) {
109
+ rb_raise(eTclError, "Tk_PhotoPutBlock failed: %s",
110
+ Tcl_GetStringResult(tip->interp));
111
+ }
112
+
113
+ return Qnil;
114
+ }
115
+
116
+ /* ---------------------------------------------------------
117
+ * Interp#photo_put_zoomed_block(photo_path, pixel_data, width, height, opts={})
118
+ *
119
+ * Fast pixel writes with zoom/subsample using Tk_PhotoPutZoomedBlock.
120
+ * Writes pixels and scales in a single operation - faster than put_block + copy.
121
+ *
122
+ * Arguments:
123
+ * photo_path - Tcl path of the photo image (e.g., "i00001")
124
+ * pixel_data - Binary string of pixels (4 bytes per pixel)
125
+ * width - Source image width in pixels
126
+ * height - Source image height in pixels
127
+ * opts - Optional hash:
128
+ * :x, :y - destination offsets (default 0,0)
129
+ * :zoom_x, :zoom_y - zoom factors (default 1,1)
130
+ * :subsample_x, :subsample_y - subsample factors (default 1,1)
131
+ * :format - :rgba (default) or :argb
132
+ *
133
+ * The pixel_data must be exactly width * height * 4 bytes.
134
+ * Zoom replicates pixels (zoom=3 makes each pixel 3x3).
135
+ * Subsample skips pixels (subsample=2 takes every other pixel).
136
+ *
137
+ * Format :argb expects pixels packed as 0xAARRGGBB integers (little-endian: B,G,R,A bytes).
138
+ * This matches SDL2 and many graphics libraries.
139
+ *
140
+ * See: https://www.tcl-lang.org/man/tcl8.6/TkLib/FindPhoto.htm
141
+ * --------------------------------------------------------- */
142
+
143
+ static VALUE
144
+ interp_photo_put_zoomed_block(int argc, VALUE *argv, VALUE self)
145
+ {
146
+ struct tcltk_interp *tip = get_interp(self);
147
+ VALUE photo_path, pixel_data, width_val, height_val, opts;
148
+ Tk_PhotoHandle photo;
149
+ Tk_PhotoImageBlock block;
150
+ int width, height, x_off, y_off;
151
+ int zoom_x, zoom_y, subsample_x, subsample_y;
152
+ int dest_width, dest_height;
153
+ int is_argb = 0;
154
+ long expected_size;
155
+
156
+ rb_scan_args(argc, argv, "41", &photo_path, &pixel_data, &width_val, &height_val, &opts);
157
+
158
+ StringValue(photo_path);
159
+ StringValue(pixel_data);
160
+ width = NUM2INT(width_val);
161
+ height = NUM2INT(height_val);
162
+
163
+ /* Validate dimensions */
164
+ if (width <= 0 || height <= 0) {
165
+ rb_raise(rb_eArgError, "width and height must be positive");
166
+ }
167
+
168
+ /* Validate data size */
169
+ expected_size = (long)width * height * 4;
170
+ if (RSTRING_LEN(pixel_data) != expected_size) {
171
+ rb_raise(rb_eArgError, "pixel_data size mismatch: expected %ld bytes, got %ld",
172
+ expected_size, RSTRING_LEN(pixel_data));
173
+ }
174
+
175
+ /* Parse options with defaults */
176
+ x_off = 0;
177
+ y_off = 0;
178
+ zoom_x = 1;
179
+ zoom_y = 1;
180
+ subsample_x = 1;
181
+ subsample_y = 1;
182
+
183
+ if (!NIL_P(opts) && TYPE(opts) == T_HASH) {
184
+ VALUE val;
185
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("x")));
186
+ if (!NIL_P(val)) x_off = NUM2INT(val);
187
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("y")));
188
+ if (!NIL_P(val)) y_off = NUM2INT(val);
189
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("zoom_x")));
190
+ if (!NIL_P(val)) zoom_x = NUM2INT(val);
191
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("zoom_y")));
192
+ if (!NIL_P(val)) zoom_y = NUM2INT(val);
193
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("subsample_x")));
194
+ if (!NIL_P(val)) subsample_x = NUM2INT(val);
195
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("subsample_y")));
196
+ if (!NIL_P(val)) subsample_y = NUM2INT(val);
197
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("format")));
198
+ if (!NIL_P(val) && TYPE(val) == T_SYMBOL) {
199
+ if (rb_intern("argb") == SYM2ID(val)) {
200
+ is_argb = 1;
201
+ }
202
+ }
203
+ }
204
+
205
+ /* Validate zoom/subsample */
206
+ if (zoom_x <= 0 || zoom_y <= 0) {
207
+ rb_raise(rb_eArgError, "zoom factors must be positive");
208
+ }
209
+ if (subsample_x <= 0 || subsample_y <= 0) {
210
+ rb_raise(rb_eArgError, "subsample factors must be positive");
211
+ }
212
+
213
+ /* Find the photo image by Tcl path */
214
+ photo = Tk_FindPhoto(tip->interp, StringValueCStr(photo_path));
215
+ if (!photo) {
216
+ rb_raise(eTclError, "photo image not found: %s", StringValueCStr(photo_path));
217
+ }
218
+
219
+ /* Set up the pixel block structure */
220
+ block.pixelPtr = (unsigned char *)RSTRING_PTR(pixel_data);
221
+ block.width = width;
222
+ block.height = height;
223
+ block.pitch = width * 4;
224
+ block.pixelSize = 4;
225
+
226
+ if (is_argb) {
227
+ /* ARGB: 0xAARRGGBB stored little-endian as bytes: [B, G, R, A] */
228
+ block.offset[0] = 2; /* Red at byte 2 */
229
+ block.offset[1] = 1; /* Green at byte 1 */
230
+ block.offset[2] = 0; /* Blue at byte 0 */
231
+ block.offset[3] = 3; /* Alpha at byte 3 */
232
+ } else {
233
+ /* RGBA: [R, G, B, A] */
234
+ block.offset[0] = 0;
235
+ block.offset[1] = 1;
236
+ block.offset[2] = 2;
237
+ block.offset[3] = 3;
238
+ }
239
+
240
+ /* Calculate destination dimensions */
241
+ dest_width = (width / subsample_x) * zoom_x;
242
+ dest_height = (height / subsample_y) * zoom_y;
243
+
244
+ /* Write pixels with zoom/subsample */
245
+ if (Tk_PhotoPutZoomedBlock(tip->interp, photo, &block, x_off, y_off,
246
+ dest_width, dest_height,
247
+ zoom_x, zoom_y, subsample_x, subsample_y,
248
+ TK_PHOTO_COMPOSITE_SET) != TCL_OK) {
249
+ rb_raise(eTclError, "Tk_PhotoPutZoomedBlock failed: %s",
250
+ Tcl_GetStringResult(tip->interp));
251
+ }
252
+
253
+ return Qnil;
254
+ }
255
+
256
+ /* ---------------------------------------------------------
257
+ * Interp#photo_get_image(photo_path, opts={})
258
+ *
259
+ * Read pixel data from a photo image using Tk_PhotoGetImage.
260
+ *
261
+ * Arguments:
262
+ * photo_path - Tcl path of the photo image (e.g., "i00001")
263
+ * opts - Optional hash:
264
+ * :x, :y - source offsets (default 0,0)
265
+ * :width, :height - region size (default: full image)
266
+ * :unpack - if true, return flat array of integers
267
+ * instead of binary string (default: false)
268
+ *
269
+ * Returns a Hash with:
270
+ * :data - Binary string of RGBA pixels (4 bytes per pixel), OR
271
+ * :pixels - Flat array of integers [r,g,b,a,r,g,b,a,...] if unpack: true
272
+ * :width - Width of returned data
273
+ * :height - Height of returned data
274
+ *
275
+ * See: https://www.tcl-lang.org/man/tcl9.0/TkLib/FindPhoto.htm
276
+ * --------------------------------------------------------- */
277
+
278
+ static VALUE
279
+ interp_photo_get_image(int argc, VALUE *argv, VALUE self)
280
+ {
281
+ struct tcltk_interp *tip = get_interp(self);
282
+ VALUE photo_path, opts, result;
283
+ Tk_PhotoHandle photo;
284
+ Tk_PhotoImageBlock block;
285
+ int x_off, y_off, req_width, req_height;
286
+ int img_width, img_height;
287
+ int actual_width, actual_height;
288
+ int do_unpack;
289
+ unsigned char *src;
290
+ int x, y;
291
+ int r_off, g_off, b_off, a_off;
292
+
293
+ rb_scan_args(argc, argv, "11", &photo_path, &opts);
294
+
295
+ StringValue(photo_path);
296
+
297
+ /* Find the photo image by Tcl path */
298
+ photo = Tk_FindPhoto(tip->interp, StringValueCStr(photo_path));
299
+ if (!photo) {
300
+ rb_raise(eTclError, "photo image not found: %s", StringValueCStr(photo_path));
301
+ }
302
+
303
+ /* Get image info */
304
+ if (!Tk_PhotoGetImage(photo, &block)) {
305
+ rb_raise(eTclError, "failed to get photo image data");
306
+ }
307
+
308
+ img_width = block.width;
309
+ img_height = block.height;
310
+
311
+ /* Parse options */
312
+ x_off = 0;
313
+ y_off = 0;
314
+ req_width = img_width;
315
+ req_height = img_height;
316
+ do_unpack = 0;
317
+
318
+ if (!NIL_P(opts) && TYPE(opts) == T_HASH) {
319
+ VALUE val;
320
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("x")));
321
+ if (!NIL_P(val)) x_off = NUM2INT(val);
322
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("y")));
323
+ if (!NIL_P(val)) y_off = NUM2INT(val);
324
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("width")));
325
+ if (!NIL_P(val)) req_width = NUM2INT(val);
326
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("height")));
327
+ if (!NIL_P(val)) req_height = NUM2INT(val);
328
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("unpack")));
329
+ if (RTEST(val)) do_unpack = 1;
330
+ }
331
+
332
+ /* Validate and clamp region */
333
+ if (x_off < 0) x_off = 0;
334
+ if (y_off < 0) y_off = 0;
335
+ if (x_off >= img_width || y_off >= img_height) {
336
+ rb_raise(rb_eArgError, "offset outside image bounds");
337
+ }
338
+
339
+ actual_width = req_width;
340
+ actual_height = req_height;
341
+ if (x_off + actual_width > img_width) actual_width = img_width - x_off;
342
+ if (y_off + actual_height > img_height) actual_height = img_height - y_off;
343
+
344
+ if (actual_width <= 0 || actual_height <= 0) {
345
+ rb_raise(rb_eArgError, "invalid region size");
346
+ }
347
+
348
+ /* Get channel offsets from the block */
349
+ r_off = block.offset[0];
350
+ g_off = block.offset[1];
351
+ b_off = block.offset[2];
352
+ a_off = block.offset[3];
353
+
354
+ /* Build result hash */
355
+ result = rb_hash_new();
356
+ rb_hash_aset(result, ID2SYM(rb_intern("width")), INT2NUM(actual_width));
357
+ rb_hash_aset(result, ID2SYM(rb_intern("height")), INT2NUM(actual_height));
358
+
359
+ if (do_unpack) {
360
+ /* Return flat array of integers: [r,g,b,a,r,g,b,a,...] */
361
+ long num_values = (long)actual_width * actual_height * 4;
362
+ VALUE pixels = rb_ary_new_capa(num_values);
363
+
364
+ for (y = 0; y < actual_height; y++) {
365
+ src = block.pixelPtr + (y_off + y) * block.pitch + x_off * block.pixelSize;
366
+ for (x = 0; x < actual_width; x++) {
367
+ rb_ary_push(pixels, INT2FIX(src[r_off]));
368
+ rb_ary_push(pixels, INT2FIX(src[g_off]));
369
+ rb_ary_push(pixels, INT2FIX(src[b_off]));
370
+ rb_ary_push(pixels, INT2FIX((block.pixelSize >= 4) ? src[a_off] : 255));
371
+ src += block.pixelSize;
372
+ }
373
+ }
374
+
375
+ rb_hash_aset(result, ID2SYM(rb_intern("pixels")), pixels);
376
+ } else {
377
+ /* Return binary string */
378
+ VALUE data_str = rb_str_new(NULL, (long)actual_width * actual_height * 4);
379
+ unsigned char *dst = (unsigned char *)RSTRING_PTR(data_str);
380
+
381
+ for (y = 0; y < actual_height; y++) {
382
+ src = block.pixelPtr + (y_off + y) * block.pitch + x_off * block.pixelSize;
383
+ for (x = 0; x < actual_width; x++) {
384
+ *dst++ = src[r_off];
385
+ *dst++ = src[g_off];
386
+ *dst++ = src[b_off];
387
+ *dst++ = (block.pixelSize >= 4) ? src[a_off] : 255;
388
+ src += block.pixelSize;
389
+ }
390
+ }
391
+
392
+ rb_hash_aset(result, ID2SYM(rb_intern("data")), data_str);
393
+ }
394
+
395
+ return result;
396
+ }
397
+
398
+ /* ---------------------------------------------------------
399
+ * Interp#photo_get_size(photo_path)
400
+ *
401
+ * Get dimensions of a photo image using Tk_PhotoGetSize.
402
+ * Faster than querying via Tcl commands.
403
+ *
404
+ * Arguments:
405
+ * photo_path - Tcl path of the photo image (e.g., "i00001")
406
+ *
407
+ * Returns [width, height] array.
408
+ *
409
+ * See: https://www.tcl-lang.org/man/tcl9.0/TkLib/FindPhoto.htm
410
+ * --------------------------------------------------------- */
411
+
412
+ static VALUE
413
+ interp_photo_get_size(VALUE self, VALUE photo_path)
414
+ {
415
+ struct tcltk_interp *tip = get_interp(self);
416
+ Tk_PhotoHandle photo;
417
+ int width, height;
418
+
419
+ StringValue(photo_path);
420
+
421
+ photo = Tk_FindPhoto(tip->interp, StringValueCStr(photo_path));
422
+ if (!photo) {
423
+ rb_raise(eTclError, "photo image not found: %s", StringValueCStr(photo_path));
424
+ }
425
+
426
+ Tk_PhotoGetSize(photo, &width, &height);
427
+
428
+ return rb_ary_new_from_args(2, INT2NUM(width), INT2NUM(height));
429
+ }
430
+
431
+ /* ---------------------------------------------------------
432
+ * Interp#photo_blank(photo_path)
433
+ *
434
+ * Clear a photo image to transparent using Tk_PhotoBlank.
435
+ * Faster than setting all pixels manually.
436
+ *
437
+ * Arguments:
438
+ * photo_path - Tcl path of the photo image (e.g., "i00001")
439
+ *
440
+ * Returns nil.
441
+ *
442
+ * See: https://www.tcl-lang.org/man/tcl9.0/TkLib/FindPhoto.htm
443
+ * --------------------------------------------------------- */
444
+
445
+ static VALUE
446
+ interp_photo_blank(VALUE self, VALUE photo_path)
447
+ {
448
+ struct tcltk_interp *tip = get_interp(self);
449
+ Tk_PhotoHandle photo;
450
+
451
+ StringValue(photo_path);
452
+
453
+ photo = Tk_FindPhoto(tip->interp, StringValueCStr(photo_path));
454
+ if (!photo) {
455
+ rb_raise(eTclError, "photo image not found: %s", StringValueCStr(photo_path));
456
+ }
457
+
458
+ Tk_PhotoBlank(photo);
459
+
460
+ return Qnil;
461
+ }
462
+
463
+ /* ---------------------------------------------------------
464
+ * Init_tkphoto - Register photo image methods on Teek::Interp class
465
+ *
466
+ * Called from Init_tcltklib in tcltkbridge.c
467
+ * --------------------------------------------------------- */
468
+
469
+ void
470
+ Init_tkphoto(VALUE cInterp)
471
+ {
472
+ rb_define_method(cInterp, "photo_put_block", interp_photo_put_block, -1);
473
+ rb_define_method(cInterp, "photo_put_zoomed_block", interp_photo_put_zoomed_block, -1);
474
+ rb_define_method(cInterp, "photo_get_image", interp_photo_get_image, -1);
475
+ rb_define_method(cInterp, "photo_get_size", interp_photo_get_size, 1);
476
+ rb_define_method(cInterp, "photo_blank", interp_photo_blank, 1);
477
+ }
data/ext/teek/tkwin.c ADDED
@@ -0,0 +1,144 @@
1
+ /* tkwin.c - Tk window query functions
2
+ *
3
+ * Interp methods that require a live Tk display: idle detection,
4
+ * coordinate queries, hit testing.
5
+ */
6
+
7
+ #include "tcltkbridge.h"
8
+
9
+ /* ---------------------------------------------------------
10
+ * Interp#user_inactive_time
11
+ *
12
+ * Get milliseconds since last user activity using Tk_GetUserInactiveTime.
13
+ * Useful for implementing screensavers, idle timeouts, etc.
14
+ *
15
+ * Returns:
16
+ * Integer milliseconds of inactivity, or
17
+ * -1 if the display doesn't support inactivity queries
18
+ *
19
+ * See: https://www.tcl-lang.org/man/tcl9.0/TkLib/Inactive.html
20
+ * --------------------------------------------------------- */
21
+
22
+ static VALUE
23
+ interp_user_inactive_time(VALUE self)
24
+ {
25
+ struct tcltk_interp *tip = get_interp(self);
26
+ Tk_Window mainWin;
27
+ Display *display;
28
+ long inactive_ms;
29
+
30
+ /* Get the main window for display access */
31
+ mainWin = Tk_MainWindow(tip->interp);
32
+ if (!mainWin) {
33
+ rb_raise(eTclError, "Tk not initialized (no main window)");
34
+ }
35
+
36
+ /* Get the display */
37
+ display = Tk_Display(mainWin);
38
+ if (!display) {
39
+ rb_raise(eTclError, "Could not get display");
40
+ }
41
+
42
+ /* Query user inactive time */
43
+ inactive_ms = Tk_GetUserInactiveTime(display);
44
+
45
+ return LONG2NUM(inactive_ms);
46
+ }
47
+
48
+ /* ---------------------------------------------------------
49
+ * Interp#get_root_coords(window_path)
50
+ *
51
+ * Get absolute screen coordinates of a window's upper-left corner.
52
+ *
53
+ * Arguments:
54
+ * window_path - Tk window path (e.g., ".", ".frame.button")
55
+ *
56
+ * Returns [x, y] array of root window coordinates.
57
+ *
58
+ * See: https://www.tcl-lang.org/man/tcl9.0/TkLib/GetRootCrd.html
59
+ * --------------------------------------------------------- */
60
+
61
+ static VALUE
62
+ interp_get_root_coords(VALUE self, VALUE window_path)
63
+ {
64
+ struct tcltk_interp *tip = get_interp(self);
65
+ Tk_Window mainWin;
66
+ Tk_Window tkwin;
67
+ int x, y;
68
+
69
+ StringValue(window_path);
70
+
71
+ /* Get the main window for hierarchy reference */
72
+ mainWin = Tk_MainWindow(tip->interp);
73
+ if (!mainWin) {
74
+ rb_raise(eTclError, "Tk not initialized (no main window)");
75
+ }
76
+
77
+ /* Find the target window by path */
78
+ tkwin = Tk_NameToWindow(tip->interp, StringValueCStr(window_path), mainWin);
79
+ if (!tkwin) {
80
+ rb_raise(eTclError, "window not found: %s", StringValueCStr(window_path));
81
+ }
82
+
83
+ /* Get root coordinates */
84
+ Tk_GetRootCoords(tkwin, &x, &y);
85
+
86
+ return rb_ary_new_from_args(2, INT2NUM(x), INT2NUM(y));
87
+ }
88
+
89
+ /* ---------------------------------------------------------
90
+ * Interp#coords_to_window(root_x, root_y)
91
+ *
92
+ * Find which window contains the given screen coordinates (hit testing).
93
+ *
94
+ * Arguments:
95
+ * root_x - X coordinate in root window (screen) coordinates
96
+ * root_y - Y coordinate in root window (screen) coordinates
97
+ *
98
+ * Returns window path string, or nil if no Tk window at that location.
99
+ *
100
+ * See: https://manpages.ubuntu.com/manpages/kinetic/man3/Tk_CoordsToWindow.3tk.html
101
+ * --------------------------------------------------------- */
102
+
103
+ static VALUE
104
+ interp_coords_to_window(VALUE self, VALUE root_x, VALUE root_y)
105
+ {
106
+ struct tcltk_interp *tip = get_interp(self);
107
+ Tk_Window mainWin;
108
+ Tk_Window foundWin;
109
+ const char *pathName;
110
+
111
+ /* Get the main window for application reference */
112
+ mainWin = Tk_MainWindow(tip->interp);
113
+ if (!mainWin) {
114
+ rb_raise(eTclError, "Tk not initialized (no main window)");
115
+ }
116
+
117
+ /* Find window at coordinates */
118
+ foundWin = Tk_CoordsToWindow(NUM2INT(root_x), NUM2INT(root_y), mainWin);
119
+ if (!foundWin) {
120
+ return Qnil;
121
+ }
122
+
123
+ /* Get window path name */
124
+ pathName = Tk_PathName(foundWin);
125
+ if (!pathName) {
126
+ return Qnil;
127
+ }
128
+
129
+ return rb_utf8_str_new_cstr(pathName);
130
+ }
131
+
132
+ /* ---------------------------------------------------------
133
+ * Init_tkwin - Register Tk window query methods on Interp
134
+ *
135
+ * Called from Init_tcltklib in tcltkbridge.c.
136
+ * --------------------------------------------------------- */
137
+
138
+ void
139
+ Init_tkwin(VALUE cInterp)
140
+ {
141
+ rb_define_method(cInterp, "user_inactive_time", interp_user_inactive_time, 0);
142
+ rb_define_method(cInterp, "get_root_coords", interp_get_root_coords, 1);
143
+ rb_define_method(cInterp, "coords_to_window", interp_coords_to_window, 2);
144
+ }