teek 0.1.0 → 0.1.2

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +99 -15
  3. data/Rakefile +201 -2
  4. data/ext/teek/extconf.rb +1 -1
  5. data/ext/teek/tcltkbridge.c +3 -110
  6. data/ext/teek/tcltkbridge.h +3 -0
  7. data/ext/teek/tkeventsource.c +195 -0
  8. data/ext/teek/tkphoto.c +169 -5
  9. data/ext/teek/tkwin.c +84 -0
  10. data/lib/teek/background_ractor4x.rb +35 -6
  11. data/lib/teek/debugger.rb +37 -32
  12. data/lib/teek/method_coverage_service.rb +265 -0
  13. data/lib/teek/photo.rb +232 -0
  14. data/lib/teek/ractor_support.rb +1 -1
  15. data/lib/teek/version.rb +1 -1
  16. data/lib/teek/widget.rb +104 -0
  17. data/lib/teek.rb +144 -1
  18. data/sample/calculator.rb +16 -21
  19. data/sample/debug_demo.rb +20 -22
  20. data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
  21. data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
  22. data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
  23. data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
  24. data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
  25. data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
  26. data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
  27. data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
  28. data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
  29. data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
  30. data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
  31. data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
  32. data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
  33. data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
  34. data/sample/optcarrot/vendor/optcarrot.rb +14 -0
  35. data/sample/optcarrot.rb +354 -0
  36. data/sample/paint/assets/bucket.png +0 -0
  37. data/sample/paint/assets/cursor.png +0 -0
  38. data/sample/paint/assets/eraser.png +0 -0
  39. data/sample/paint/assets/pencil.png +0 -0
  40. data/sample/paint/assets/spray.png +0 -0
  41. data/sample/paint/layer.rb +255 -0
  42. data/sample/paint/layer_manager.rb +179 -0
  43. data/sample/paint/paint_demo.rb +837 -0
  44. data/sample/paint/sparse_pixel_buffer.rb +202 -0
  45. data/sample/sdl2_demo.rb +318 -0
  46. data/sample/threading_demo.rb +127 -132
  47. metadata +31 -1
@@ -0,0 +1,195 @@
1
+ /*
2
+ * tkeventsource.c - External event source integration via Tcl_CreateEventSource
3
+ *
4
+ * Allows other C extensions (e.g. teek-sdl2) to register poll callbacks
5
+ * that run inside Tcl's event loop with zero Ruby overhead in the hot path.
6
+ *
7
+ * The consumer passes a C function pointer via a Ruby method call at
8
+ * registration time. The Tcl event source setup/check procs call that
9
+ * pointer directly — no rb_funcall, no method dispatch.
10
+ */
11
+
12
+ #include "tcltkbridge.h"
13
+ #include <stdint.h>
14
+
15
+ static VALUE cEventSource;
16
+
17
+ /* ---------------------------------------------------------
18
+ * Event source struct — wrapped as Ruby TypedData
19
+ * --------------------------------------------------------- */
20
+
21
+ typedef void (*event_source_check_fn)(void *client_data);
22
+
23
+ struct event_source {
24
+ event_source_check_fn check_fn; /* C function pointer from consumer */
25
+ void *client_data; /* Opaque data from consumer */
26
+ Tcl_Time max_block; /* Max block time for setup proc */
27
+ int registered; /* Whether Tcl event source is active */
28
+ };
29
+
30
+ /* Forward declarations */
31
+ static void es_setup_proc(ClientData cd, int flags);
32
+ static void es_check_proc(ClientData cd, int flags);
33
+
34
+ /* ---------------------------------------------------------
35
+ * TypedData functions
36
+ * --------------------------------------------------------- */
37
+
38
+ static void
39
+ event_source_free(void *ptr)
40
+ {
41
+ struct event_source *es = ptr;
42
+ if (es->registered) {
43
+ Tcl_DeleteEventSource(es_setup_proc, es_check_proc, (ClientData)es);
44
+ es->registered = 0;
45
+ }
46
+ xfree(es);
47
+ }
48
+
49
+ static size_t
50
+ event_source_memsize(const void *ptr)
51
+ {
52
+ return sizeof(struct event_source);
53
+ }
54
+
55
+ static const rb_data_type_t event_source_type = {
56
+ .wrap_struct_name = "Teek::EventSource",
57
+ .function = {
58
+ .dmark = NULL, /* No Ruby VALUEs to mark */
59
+ .dfree = event_source_free,
60
+ .dsize = event_source_memsize,
61
+ },
62
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
63
+ };
64
+
65
+ /* ---------------------------------------------------------
66
+ * Tcl event source callbacks (hot path — pure C)
67
+ * --------------------------------------------------------- */
68
+
69
+ /*
70
+ * Setup proc: called before Tcl_WaitForEvent.
71
+ * Caps the block time so our check proc runs frequently.
72
+ */
73
+ static void
74
+ es_setup_proc(ClientData cd, int flags)
75
+ {
76
+ struct event_source *es = (struct event_source *)cd;
77
+
78
+ if (!(flags & TCL_FILE_EVENTS) && !(flags & TCL_ALL_EVENTS))
79
+ return;
80
+
81
+ Tcl_SetMaxBlockTime(&es->max_block);
82
+ }
83
+
84
+ /*
85
+ * Check proc: called after Tcl_WaitForEvent returns.
86
+ * Calls the consumer's C function pointer directly.
87
+ * No rb_funcall, no Ruby method dispatch — just a function pointer call.
88
+ */
89
+ static void
90
+ es_check_proc(ClientData cd, int flags)
91
+ {
92
+ struct event_source *es = (struct event_source *)cd;
93
+
94
+ if (!(flags & TCL_FILE_EVENTS) && !(flags & TCL_ALL_EVENTS))
95
+ return;
96
+
97
+ es->check_fn(es->client_data);
98
+ }
99
+
100
+ /* ---------------------------------------------------------
101
+ * Ruby methods
102
+ * --------------------------------------------------------- */
103
+
104
+ /*
105
+ * Teek._register_event_source(check_fn_ptr, client_data_ptr, interval_ms) -> EventSource
106
+ *
107
+ * Registers a C function as a Tcl event source. The function will be called
108
+ * on every event loop iteration with no Ruby overhead.
109
+ *
110
+ * check_fn_ptr: Integer — address of a C function with signature void(*)(void*)
111
+ * client_data_ptr: Integer — address passed to check_fn (0 for NULL)
112
+ * interval_ms: Integer — max block time in ms (e.g. 16 for ~60fps)
113
+ *
114
+ * Returns an opaque EventSource object. Hold a reference to keep it alive.
115
+ * Call #unregister or let GC collect it to remove the event source.
116
+ */
117
+ static VALUE
118
+ teek_register_event_source(VALUE self, VALUE fn_ptr, VALUE data_ptr, VALUE interval)
119
+ {
120
+ struct event_source *es;
121
+ VALUE obj;
122
+ int ms;
123
+
124
+ /* Validate */
125
+ event_source_check_fn fn = (event_source_check_fn)(uintptr_t)NUM2ULL(fn_ptr);
126
+ if (!fn) {
127
+ rb_raise(rb_eArgError, "check_fn_ptr must not be NULL");
128
+ }
129
+
130
+ ms = NUM2INT(interval);
131
+ if (ms < 1) ms = 1;
132
+
133
+ /* Allocate and populate */
134
+ obj = TypedData_Make_Struct(cEventSource, struct event_source, &event_source_type, es);
135
+ es->check_fn = fn;
136
+ es->client_data = (void *)(uintptr_t)NUM2ULL(data_ptr);
137
+ es->max_block.sec = ms / 1000;
138
+ es->max_block.usec = (ms % 1000) * 1000;
139
+ es->registered = 0;
140
+
141
+ /* Register with Tcl */
142
+ Tcl_CreateEventSource(es_setup_proc, es_check_proc, (ClientData)es);
143
+ es->registered = 1;
144
+
145
+ return obj;
146
+ }
147
+
148
+ /*
149
+ * EventSource#unregister -> nil
150
+ *
151
+ * Explicitly removes the event source from Tcl's notifier.
152
+ * Safe to call multiple times.
153
+ */
154
+ static VALUE
155
+ event_source_unregister(VALUE self)
156
+ {
157
+ struct event_source *es;
158
+ TypedData_Get_Struct(self, struct event_source, &event_source_type, es);
159
+
160
+ if (es->registered) {
161
+ Tcl_DeleteEventSource(es_setup_proc, es_check_proc, (ClientData)es);
162
+ es->registered = 0;
163
+ }
164
+ return Qnil;
165
+ }
166
+
167
+ /*
168
+ * EventSource#registered? -> true/false
169
+ */
170
+ static VALUE
171
+ event_source_registered_p(VALUE self)
172
+ {
173
+ struct event_source *es;
174
+ TypedData_Get_Struct(self, struct event_source, &event_source_type, es);
175
+ return es->registered ? Qtrue : Qfalse;
176
+ }
177
+
178
+ /* ---------------------------------------------------------
179
+ * Init — called from Init_tcltklib
180
+ * --------------------------------------------------------- */
181
+
182
+ static VALUE cEventSource;
183
+
184
+ void
185
+ Init_tkeventsource(VALUE mTeek)
186
+ {
187
+ cEventSource = rb_define_class_under(mTeek, "EventSource", rb_cObject);
188
+ rb_undef_alloc_func(cEventSource); /* No Ruby-side new */
189
+
190
+ rb_define_method(cEventSource, "unregister", event_source_unregister, 0);
191
+ rb_define_method(cEventSource, "registered?", event_source_registered_p, 0);
192
+
193
+ rb_define_module_function(mTeek, "_register_event_source",
194
+ teek_register_event_source, 3);
195
+ }
data/ext/teek/tkphoto.c CHANGED
@@ -1,4 +1,4 @@
1
- /* tkphoto.c - Photo image C functions for tk-ng
1
+ /* tkphoto.c - Photo image C functions for teek
2
2
  *
3
3
  * Fast pixel manipulation using Tk's photo image C API.
4
4
  * These functions bypass Tcl string parsing for better performance.
@@ -18,8 +18,9 @@
18
18
  * width - Image width in pixels
19
19
  * height - Image height in pixels
20
20
  * opts - Optional hash:
21
- * :x, :y - destination offsets (default 0,0)
22
- * :format - :rgba (default) or :argb
21
+ * :x, :y - destination offsets (default 0,0)
22
+ * :format - :rgba (default) or :argb
23
+ * :composite - :set (default, overwrite) or :overlay (alpha blend)
23
24
  *
24
25
  * The pixel_data must be exactly width * height * 4 bytes.
25
26
  *
@@ -38,6 +39,7 @@ interp_photo_put_block(int argc, VALUE *argv, VALUE self)
38
39
  Tk_PhotoImageBlock block;
39
40
  int width, height, x_off, y_off;
40
41
  int is_argb = 0;
42
+ int comp_rule = TK_PHOTO_COMPOSITE_SET;
41
43
  long expected_size;
42
44
 
43
45
  rb_scan_args(argc, argv, "41", &photo_path, &pixel_data, &width_val, &height_val, &opts);
@@ -74,6 +76,12 @@ interp_photo_put_block(int argc, VALUE *argv, VALUE self)
74
76
  is_argb = 1;
75
77
  }
76
78
  }
79
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("composite")));
80
+ if (!NIL_P(val) && TYPE(val) == T_SYMBOL) {
81
+ if (rb_intern("overlay") == SYM2ID(val)) {
82
+ comp_rule = TK_PHOTO_COMPOSITE_OVERLAY;
83
+ }
84
+ }
77
85
  }
78
86
 
79
87
  /* Find the photo image by Tcl path */
@@ -105,7 +113,7 @@ interp_photo_put_block(int argc, VALUE *argv, VALUE self)
105
113
 
106
114
  /* Write pixels to the photo image */
107
115
  if (Tk_PhotoPutBlock(tip->interp, photo, &block, x_off, y_off,
108
- width, height, TK_PHOTO_COMPOSITE_SET) != TCL_OK) {
116
+ width, height, comp_rule) != TCL_OK) {
109
117
  rb_raise(eTclError, "Tk_PhotoPutBlock failed: %s",
110
118
  Tcl_GetStringResult(tip->interp));
111
119
  }
@@ -129,6 +137,7 @@ interp_photo_put_block(int argc, VALUE *argv, VALUE self)
129
137
  * :zoom_x, :zoom_y - zoom factors (default 1,1)
130
138
  * :subsample_x, :subsample_y - subsample factors (default 1,1)
131
139
  * :format - :rgba (default) or :argb
140
+ * :composite - :set (default, overwrite) or :overlay (alpha blend)
132
141
  *
133
142
  * The pixel_data must be exactly width * height * 4 bytes.
134
143
  * Zoom replicates pixels (zoom=3 makes each pixel 3x3).
@@ -151,6 +160,7 @@ interp_photo_put_zoomed_block(int argc, VALUE *argv, VALUE self)
151
160
  int zoom_x, zoom_y, subsample_x, subsample_y;
152
161
  int dest_width, dest_height;
153
162
  int is_argb = 0;
163
+ int comp_rule = TK_PHOTO_COMPOSITE_SET;
154
164
  long expected_size;
155
165
 
156
166
  rb_scan_args(argc, argv, "41", &photo_path, &pixel_data, &width_val, &height_val, &opts);
@@ -200,6 +210,12 @@ interp_photo_put_zoomed_block(int argc, VALUE *argv, VALUE self)
200
210
  is_argb = 1;
201
211
  }
202
212
  }
213
+ val = rb_hash_aref(opts, ID2SYM(rb_intern("composite")));
214
+ if (!NIL_P(val) && TYPE(val) == T_SYMBOL) {
215
+ if (rb_intern("overlay") == SYM2ID(val)) {
216
+ comp_rule = TK_PHOTO_COMPOSITE_OVERLAY;
217
+ }
218
+ }
203
219
  }
204
220
 
205
221
  /* Validate zoom/subsample */
@@ -245,7 +261,7 @@ interp_photo_put_zoomed_block(int argc, VALUE *argv, VALUE self)
245
261
  if (Tk_PhotoPutZoomedBlock(tip->interp, photo, &block, x_off, y_off,
246
262
  dest_width, dest_height,
247
263
  zoom_x, zoom_y, subsample_x, subsample_y,
248
- TK_PHOTO_COMPOSITE_SET) != TCL_OK) {
264
+ comp_rule) != TCL_OK) {
249
265
  rb_raise(eTclError, "Tk_PhotoPutZoomedBlock failed: %s",
250
266
  Tcl_GetStringResult(tip->interp));
251
267
  }
@@ -460,6 +476,151 @@ interp_photo_blank(VALUE self, VALUE photo_path)
460
476
  return Qnil;
461
477
  }
462
478
 
479
+ /* ---------------------------------------------------------
480
+ * Interp#photo_set_size(photo_path, width, height)
481
+ *
482
+ * Set the dimensions of a photo image using Tk_PhotoSetSize.
483
+ *
484
+ * Arguments:
485
+ * photo_path - Tcl path of the photo image
486
+ * width - New width in pixels
487
+ * height - New height in pixels
488
+ *
489
+ * Returns nil.
490
+ *
491
+ * See: https://www.tcl-lang.org/man/tcl8.6/TkLib/FindPhoto.htm
492
+ * --------------------------------------------------------- */
493
+
494
+ static VALUE
495
+ interp_photo_set_size(VALUE self, VALUE photo_path, VALUE width_val, VALUE height_val)
496
+ {
497
+ struct tcltk_interp *tip = get_interp(self);
498
+ Tk_PhotoHandle photo;
499
+ int width, height;
500
+
501
+ StringValue(photo_path);
502
+ width = NUM2INT(width_val);
503
+ height = NUM2INT(height_val);
504
+
505
+ if (width < 0 || height < 0) {
506
+ rb_raise(rb_eArgError, "width and height must be non-negative");
507
+ }
508
+
509
+ photo = Tk_FindPhoto(tip->interp, StringValueCStr(photo_path));
510
+ if (!photo) {
511
+ rb_raise(eTclError, "photo image not found: %s", StringValueCStr(photo_path));
512
+ }
513
+
514
+ if (Tk_PhotoSetSize(tip->interp, photo, width, height) != TCL_OK) {
515
+ rb_raise(eTclError, "Tk_PhotoSetSize failed: %s",
516
+ Tcl_GetStringResult(tip->interp));
517
+ }
518
+
519
+ return Qnil;
520
+ }
521
+
522
+ /* ---------------------------------------------------------
523
+ * Interp#photo_expand(photo_path, width, height)
524
+ *
525
+ * Expand a photo image to at least the given dimensions using Tk_PhotoExpand.
526
+ * Will not shrink the image if it is already larger.
527
+ *
528
+ * Arguments:
529
+ * photo_path - Tcl path of the photo image
530
+ * width - Minimum width in pixels
531
+ * height - Minimum height in pixels
532
+ *
533
+ * Returns nil.
534
+ *
535
+ * See: https://www.tcl-lang.org/man/tcl8.6/TkLib/FindPhoto.htm
536
+ * --------------------------------------------------------- */
537
+
538
+ static VALUE
539
+ interp_photo_expand(VALUE self, VALUE photo_path, VALUE width_val, VALUE height_val)
540
+ {
541
+ struct tcltk_interp *tip = get_interp(self);
542
+ Tk_PhotoHandle photo;
543
+ int width, height;
544
+
545
+ StringValue(photo_path);
546
+ width = NUM2INT(width_val);
547
+ height = NUM2INT(height_val);
548
+
549
+ if (width < 0 || height < 0) {
550
+ rb_raise(rb_eArgError, "width and height must be non-negative");
551
+ }
552
+
553
+ photo = Tk_FindPhoto(tip->interp, StringValueCStr(photo_path));
554
+ if (!photo) {
555
+ rb_raise(eTclError, "photo image not found: %s", StringValueCStr(photo_path));
556
+ }
557
+
558
+ if (Tk_PhotoExpand(tip->interp, photo, width, height) != TCL_OK) {
559
+ rb_raise(eTclError, "Tk_PhotoExpand failed: %s",
560
+ Tcl_GetStringResult(tip->interp));
561
+ }
562
+
563
+ return Qnil;
564
+ }
565
+
566
+ /* ---------------------------------------------------------
567
+ * Interp#photo_get_pixel(photo_path, x, y)
568
+ *
569
+ * Read a single pixel from a photo image using Tk_PhotoGetImage.
570
+ * Faster than going through Tcl's "$photo get x y" string parsing.
571
+ *
572
+ * Arguments:
573
+ * photo_path - Tcl path of the photo image
574
+ * x - X coordinate
575
+ * y - Y coordinate
576
+ *
577
+ * Returns [r, g, b, a] array.
578
+ *
579
+ * See: https://www.tcl-lang.org/man/tcl8.6/TkLib/FindPhoto.htm
580
+ * --------------------------------------------------------- */
581
+
582
+ static VALUE
583
+ interp_photo_get_pixel(VALUE self, VALUE photo_path, VALUE x_val, VALUE y_val)
584
+ {
585
+ struct tcltk_interp *tip = get_interp(self);
586
+ Tk_PhotoHandle photo;
587
+ Tk_PhotoImageBlock block;
588
+ int x, y;
589
+ unsigned char *src;
590
+ int r_off, g_off, b_off, a_off;
591
+
592
+ StringValue(photo_path);
593
+ x = NUM2INT(x_val);
594
+ y = NUM2INT(y_val);
595
+
596
+ photo = Tk_FindPhoto(tip->interp, StringValueCStr(photo_path));
597
+ if (!photo) {
598
+ rb_raise(eTclError, "photo image not found: %s", StringValueCStr(photo_path));
599
+ }
600
+
601
+ if (!Tk_PhotoGetImage(photo, &block)) {
602
+ rb_raise(eTclError, "failed to get photo image data");
603
+ }
604
+
605
+ if (x < 0 || x >= block.width || y < 0 || y >= block.height) {
606
+ rb_raise(rb_eArgError, "coordinates (%d, %d) outside image bounds (%d x %d)",
607
+ x, y, block.width, block.height);
608
+ }
609
+
610
+ r_off = block.offset[0];
611
+ g_off = block.offset[1];
612
+ b_off = block.offset[2];
613
+ a_off = block.offset[3];
614
+
615
+ src = block.pixelPtr + y * block.pitch + x * block.pixelSize;
616
+
617
+ return rb_ary_new_from_args(4,
618
+ INT2FIX(src[r_off]),
619
+ INT2FIX(src[g_off]),
620
+ INT2FIX(src[b_off]),
621
+ INT2FIX((block.pixelSize >= 4) ? src[a_off] : 255));
622
+ }
623
+
463
624
  /* ---------------------------------------------------------
464
625
  * Init_tkphoto - Register photo image methods on Teek::Interp class
465
626
  *
@@ -473,5 +634,8 @@ Init_tkphoto(VALUE cInterp)
473
634
  rb_define_method(cInterp, "photo_put_zoomed_block", interp_photo_put_zoomed_block, -1);
474
635
  rb_define_method(cInterp, "photo_get_image", interp_photo_get_image, -1);
475
636
  rb_define_method(cInterp, "photo_get_size", interp_photo_get_size, 1);
637
+ rb_define_method(cInterp, "photo_set_size", interp_photo_set_size, 3);
638
+ rb_define_method(cInterp, "photo_expand", interp_photo_expand, 3);
639
+ rb_define_method(cInterp, "photo_get_pixel", interp_photo_get_pixel, 3);
476
640
  rb_define_method(cInterp, "photo_blank", interp_photo_blank, 1);
477
641
  }
data/ext/teek/tkwin.c CHANGED
@@ -5,6 +5,21 @@
5
5
  */
6
6
 
7
7
  #include "tcltkbridge.h"
8
+ #include <stdint.h>
9
+
10
+ #ifdef __APPLE__
11
+ /*
12
+ * Platform stubs for macOS. Tk_InitStubs sets up tkStubsPtr but
13
+ * tkPlatStubsPtr needs to be pulled from the hooks table.
14
+ * MAC_OSX_TK is needed for tkPlatDecls.h to expose macOS APIs.
15
+ */
16
+ #ifndef MAC_OSX_TK
17
+ #define MAC_OSX_TK
18
+ #endif
19
+ #include "tkPlatDecls.h"
20
+ #elif defined(_WIN32) || defined(__CYGWIN__)
21
+ #include "tkPlatDecls.h"
22
+ #endif
8
23
 
9
24
  /* ---------------------------------------------------------
10
25
  * Interp#user_inactive_time
@@ -129,6 +144,74 @@ interp_coords_to_window(VALUE self, VALUE root_x, VALUE root_y)
129
144
  return rb_utf8_str_new_cstr(pathName);
130
145
  }
131
146
 
147
+ /* ---------------------------------------------------------
148
+ * Interp#native_window_handle(window_path)
149
+ *
150
+ * Returns the platform-native window handle for SDL2 embedding.
151
+ *
152
+ * macOS: NSWindow* (via Tk_MacOSXGetNSWindowForDrawable)
153
+ * X11: X Window ID (via Tk_WindowId)
154
+ * Windows: HWND (via Tk_GetHWND)
155
+ *
156
+ * The return value is an Integer suitable for passing to
157
+ * SDL_CreateWindowFrom. On macOS this is a pointer to an
158
+ * NSWindow object; on X11/Windows it's a window identifier.
159
+ *
160
+ * The window must be mapped (visible) before calling this.
161
+ * Use `update_idletasks` first to ensure geometry is committed.
162
+ * --------------------------------------------------------- */
163
+
164
+ static VALUE
165
+ interp_native_window_handle(VALUE self, VALUE window_path)
166
+ {
167
+ struct tcltk_interp *tip = get_interp(self);
168
+ Tk_Window mainWin;
169
+ Tk_Window tkwin;
170
+ Drawable drawable;
171
+
172
+ StringValue(window_path);
173
+
174
+ mainWin = Tk_MainWindow(tip->interp);
175
+ if (!mainWin) {
176
+ rb_raise(eTclError, "Tk not initialized (no main window)");
177
+ }
178
+
179
+ tkwin = Tk_NameToWindow(tip->interp, StringValueCStr(window_path), mainWin);
180
+ if (!tkwin) {
181
+ rb_raise(eTclError, "window not found: %s", StringValueCStr(window_path));
182
+ }
183
+
184
+ /* Force the window to be mapped so we get a valid native handle */
185
+ Tk_MakeWindowExist(tkwin);
186
+
187
+ drawable = Tk_WindowId(tkwin);
188
+ if (!drawable) {
189
+ rb_raise(eTclError, "window has no native handle (not mapped?): %s",
190
+ StringValueCStr(window_path));
191
+ }
192
+
193
+ #ifdef __APPLE__
194
+ {
195
+ /* macOS: convert Tk drawable to NSWindow* for SDL2 */
196
+ void *nswindow = Tk_MacOSXGetNSWindowForDrawable(drawable);
197
+ if (!nswindow) {
198
+ rb_raise(eTclError, "could not get NSWindow for: %s",
199
+ StringValueCStr(window_path));
200
+ }
201
+ return ULL2NUM((uintptr_t)nswindow);
202
+ }
203
+ #elif defined(_WIN32) || defined(__CYGWIN__)
204
+ {
205
+ /* Windows: convert to HWND */
206
+ HWND hwnd = Tk_GetHWND(drawable);
207
+ return ULL2NUM((uintptr_t)hwnd);
208
+ }
209
+ #else
210
+ /* X11: Drawable is already the X Window ID */
211
+ return ULL2NUM((uintptr_t)drawable);
212
+ #endif
213
+ }
214
+
132
215
  /* ---------------------------------------------------------
133
216
  * Init_tkwin - Register Tk window query methods on Interp
134
217
  *
@@ -141,4 +224,5 @@ Init_tkwin(VALUE cInterp)
141
224
  rb_define_method(cInterp, "user_inactive_time", interp_user_inactive_time, 0);
142
225
  rb_define_method(cInterp, "get_root_coords", interp_get_root_coords, 1);
143
226
  rb_define_method(cInterp, "coords_to_window", interp_coords_to_window, 2);
227
+ rb_define_method(cInterp, "native_window_handle", interp_native_window_handle, 1);
144
228
  }
@@ -153,13 +153,21 @@ module Teek
153
153
  # @return [self]
154
154
  def close
155
155
  @done = true
156
- @control_port = nil # Prevent further message sends
156
+ # Send stop to let the worker terminate itself — Ruby 4.x doesn't
157
+ # allow closing a Ractor from outside. The message thread will
158
+ # raise StopIteration on the Ractor's main thread, which triggers
159
+ # the rescue block that sends [:done] to the output port and exits
160
+ # the Ractor cleanly.
157
161
  begin
158
- @worker_ractor&.close_incoming
159
- @worker_ractor&.close_outgoing
162
+ @control_port&.send(:stop)
160
163
  rescue Ractor::ClosedError
161
164
  # Already closed
162
165
  end
166
+ @control_port = nil
167
+ # Wait for the bridge thread to receive [:done] and exit. Without
168
+ # this, the zombie bridge thread blocks subsequent operations on
169
+ # Windows (Ractor::Port#receive holds the GVL).
170
+ @bridge_thread&.join(2)
163
171
  self
164
172
  end
165
173
 
@@ -173,7 +181,17 @@ module Teek
173
181
 
174
182
  # Wrap in isolated proc for Ractor sharing. The block can only access
175
183
  # its parameters (task, data), not outer-scope variables.
176
- shareable_block = Ractor.shareable_proc(&@work_block)
184
+ isolation_error = false
185
+ begin
186
+ shareable_block = Ractor.shareable_proc(&@work_block)
187
+ rescue Ractor::IsolationError
188
+ isolation_error = true
189
+ end
190
+ if isolation_error
191
+ raise Ractor::IsolationError,
192
+ "Background work block must not reference outside variables (including `app`). " \
193
+ "Use t.yield() to send results to on_progress, which runs on the main thread."
194
+ end
177
195
 
178
196
  start_ractor(shareable_block)
179
197
  start_polling
@@ -201,6 +219,7 @@ module Teek
201
219
  output_port = Ractor::Port.new
202
220
 
203
221
  @worker_ractor = Ractor.new(data, output_port, shareable_block) do |d, out, blk|
222
+ # :nocov: -- Coverage.so cannot observe execution inside a Ractor
204
223
  # Worker creates its own control port for receiving messages
205
224
  control_port = Ractor::Port.new
206
225
  msg_queue = Thread::Queue.new
@@ -208,13 +227,20 @@ module Teek
208
227
  # Send control port back to main thread
209
228
  out.send([:control_port, control_port])
210
229
 
211
- # Background thread receives from control port, forwards to queue
230
+ # Background thread receives from control port, forwards to queue.
231
+ # On :stop, interrupts the main Ractor thread with StopIteration
232
+ # (the main block may never call check_message) and signals the
233
+ # bridge thread via [:done] on the output port.
234
+ main_thread = Thread.current
212
235
  Thread.new do
213
236
  loop do
214
237
  begin
215
238
  msg = control_port.receive
216
239
  msg_queue << msg
217
- break if msg == :stop
240
+ if msg == :stop
241
+ main_thread.raise(StopIteration)
242
+ break
243
+ end
218
244
  rescue Ractor::ClosedError
219
245
  break
220
246
  end
@@ -232,6 +258,7 @@ module Teek
232
258
  out.send([:error, "#{e.class}: #{e.message}\n#{e.backtrace.first(3).join("\n")}"])
233
259
  out.send([:done])
234
260
  end
261
+ # :nocov:
235
262
  end
236
263
 
237
264
  # Bridge thread: Port.receive -> Queue
@@ -334,6 +361,7 @@ module Teek
334
361
  # Context object passed to the worker block inside the Ractor.
335
362
  # Provides methods for yielding results, sending/receiving messages,
336
363
  # and responding to pause/stop signals.
364
+ # :nocov: -- TaskContext runs exclusively inside a Ractor; invisible to Coverage.so
337
365
  class TaskContext
338
366
  # @api private
339
367
  def initialize(output_port, msg_queue)
@@ -405,6 +433,7 @@ module Teek
405
433
  end
406
434
  end
407
435
  end
436
+ # :nocov:
408
437
  end
409
438
  end
410
439
  end