teek 0.1.1 → 0.1.3
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.
- checksums.yaml +4 -4
- data/README.md +46 -0
- data/Rakefile +162 -5
- data/ext/teek/extconf.rb +1 -1
- data/ext/teek/tcltkbridge.c +9 -0
- data/ext/teek/tcltkbridge.h +3 -0
- data/ext/teek/tkeventsource.c +195 -0
- data/ext/teek/tkphoto.c +169 -5
- data/ext/teek/tkwin.c +84 -0
- data/lib/teek/background_ractor4x.rb +32 -4
- data/lib/teek/photo.rb +232 -0
- data/lib/teek/version.rb +1 -1
- data/lib/teek.rb +202 -5
- data/sample/gamepad_viewer/assets/controller.png +0 -0
- data/sample/gamepad_viewer/gamepad_viewer.rb +554 -0
- data/sample/optcarrot/thwaite.nes +0 -0
- data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
- data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
- data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
- data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
- data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
- data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
- data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
- data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
- data/sample/optcarrot/vendor/optcarrot.rb +14 -0
- data/sample/optcarrot.rb +354 -0
- data/sample/paint/assets/bucket.png +0 -0
- data/sample/paint/assets/cursor.png +0 -0
- data/sample/paint/assets/eraser.png +0 -0
- data/sample/paint/assets/pencil.png +0 -0
- data/sample/paint/assets/spray.png +0 -0
- data/sample/paint/layer.rb +255 -0
- data/sample/paint/layer_manager.rb +179 -0
- data/sample/paint/paint_demo.rb +837 -0
- data/sample/paint/sparse_pixel_buffer.rb +202 -0
- data/sample/sdl2_demo.rb +318 -0
- data/sample/yam/assets/click.wav +0 -0
- data/sample/yam/assets/explosion.wav +0 -0
- data/sample/yam/assets/flag.wav +0 -0
- data/sample/yam/assets/music.mp3 +0 -0
- data/sample/yam/assets/sweep.wav +0 -0
- data/sample/{minesweeper/minesweeper.rb → yam/yam.rb} +147 -12
- metadata +50 -14
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_0.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_1.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_2.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_3.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_4.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_5.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_6.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_7.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_8.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_F.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_M.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_X.png +0 -0
data/ext/teek/tkphoto.c
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* tkphoto.c - Photo image C functions for
|
|
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
|
|
22
|
-
* :format
|
|
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,
|
|
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
|
-
|
|
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
|
}
|
|
@@ -154,13 +154,20 @@ module Teek
|
|
|
154
154
|
def close
|
|
155
155
|
@done = true
|
|
156
156
|
# Send stop to let the worker terminate itself — Ruby 4.x doesn't
|
|
157
|
-
# allow closing a Ractor from outside.
|
|
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.
|
|
158
161
|
begin
|
|
159
162
|
@control_port&.send(:stop)
|
|
160
163
|
rescue Ractor::ClosedError
|
|
161
164
|
# Already closed
|
|
162
165
|
end
|
|
163
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)
|
|
164
171
|
self
|
|
165
172
|
end
|
|
166
173
|
|
|
@@ -174,7 +181,17 @@ module Teek
|
|
|
174
181
|
|
|
175
182
|
# Wrap in isolated proc for Ractor sharing. The block can only access
|
|
176
183
|
# its parameters (task, data), not outer-scope variables.
|
|
177
|
-
|
|
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
|
|
178
195
|
|
|
179
196
|
start_ractor(shareable_block)
|
|
180
197
|
start_polling
|
|
@@ -202,6 +219,7 @@ module Teek
|
|
|
202
219
|
output_port = Ractor::Port.new
|
|
203
220
|
|
|
204
221
|
@worker_ractor = Ractor.new(data, output_port, shareable_block) do |d, out, blk|
|
|
222
|
+
# :nocov: -- Coverage.so cannot observe execution inside a Ractor
|
|
205
223
|
# Worker creates its own control port for receiving messages
|
|
206
224
|
control_port = Ractor::Port.new
|
|
207
225
|
msg_queue = Thread::Queue.new
|
|
@@ -209,13 +227,20 @@ module Teek
|
|
|
209
227
|
# Send control port back to main thread
|
|
210
228
|
out.send([:control_port, control_port])
|
|
211
229
|
|
|
212
|
-
# 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
|
|
213
235
|
Thread.new do
|
|
214
236
|
loop do
|
|
215
237
|
begin
|
|
216
238
|
msg = control_port.receive
|
|
217
239
|
msg_queue << msg
|
|
218
|
-
|
|
240
|
+
if msg == :stop
|
|
241
|
+
main_thread.raise(StopIteration)
|
|
242
|
+
break
|
|
243
|
+
end
|
|
219
244
|
rescue Ractor::ClosedError
|
|
220
245
|
break
|
|
221
246
|
end
|
|
@@ -233,6 +258,7 @@ module Teek
|
|
|
233
258
|
out.send([:error, "#{e.class}: #{e.message}\n#{e.backtrace.first(3).join("\n")}"])
|
|
234
259
|
out.send([:done])
|
|
235
260
|
end
|
|
261
|
+
# :nocov:
|
|
236
262
|
end
|
|
237
263
|
|
|
238
264
|
# Bridge thread: Port.receive -> Queue
|
|
@@ -335,6 +361,7 @@ module Teek
|
|
|
335
361
|
# Context object passed to the worker block inside the Ractor.
|
|
336
362
|
# Provides methods for yielding results, sending/receiving messages,
|
|
337
363
|
# and responding to pause/stop signals.
|
|
364
|
+
# :nocov: -- TaskContext runs exclusively inside a Ractor; invisible to Coverage.so
|
|
338
365
|
class TaskContext
|
|
339
366
|
# @api private
|
|
340
367
|
def initialize(output_port, msg_queue)
|
|
@@ -406,6 +433,7 @@ module Teek
|
|
|
406
433
|
end
|
|
407
434
|
end
|
|
408
435
|
end
|
|
436
|
+
# :nocov:
|
|
409
437
|
end
|
|
410
438
|
end
|
|
411
439
|
end
|
data/lib/teek/photo.rb
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
# CPU-side RGBA pixel buffer backed by Tk's "photo image" format.
|
|
5
|
+
#
|
|
6
|
+
# Despite the name, this is really a raw pixel manipulation surface.
|
|
7
|
+
# Tk has two built-in image types: "bitmap" (two colors + transparency)
|
|
8
|
+
# and "photo" (full-color, 32-bit RGBA). The naming reflects Tk's
|
|
9
|
+
# image type system, not the contents — a "photo" is just Tk's term
|
|
10
|
+
# for a full-color pixel buffer.
|
|
11
|
+
#
|
|
12
|
+
# Think of it as a software framebuffer: you pack RGBA bytes, write
|
|
13
|
+
# them in bulk, read them back, zoom/subsample, and blit to a canvas
|
|
14
|
+
# or label for display. All work is CPU-driven — there is no GPU
|
|
15
|
+
# acceleration.
|
|
16
|
+
#
|
|
17
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/photo.html Tk photo image type
|
|
18
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/bitmap.html Tk bitmap image type
|
|
19
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/image.html Tk image command (lists all types)
|
|
20
|
+
#
|
|
21
|
+
# The C methods ({#put_block}, {#get_image}, {#get_pixel}, etc.) call
|
|
22
|
+
# Tk_PhotoPutBlock / Tk_PhotoGetImage directly, bypassing Tcl string
|
|
23
|
+
# parsing for much better performance than the Tcl-level +$photo put+
|
|
24
|
+
# command. Designed for games, visualizations, and real-time drawing.
|
|
25
|
+
#
|
|
26
|
+
# @example Create and fill with red pixels
|
|
27
|
+
# photo = Teek::Photo.new(app, width: 100, height: 100)
|
|
28
|
+
# red = ([255, 0, 0, 255].pack('CCCC')) * (100 * 100)
|
|
29
|
+
# photo.put_block(red, 100, 100)
|
|
30
|
+
#
|
|
31
|
+
# @example Read pixels back
|
|
32
|
+
# result = photo.get_image
|
|
33
|
+
# r, g, b, a = result[:data][0, 4].unpack('CCCC')
|
|
34
|
+
#
|
|
35
|
+
# @example Zoom a small sprite
|
|
36
|
+
# sprite = Teek::Photo.new(app, width: 64, height: 64)
|
|
37
|
+
# # ... fill sprite pixels ...
|
|
38
|
+
# dest = Teek::Photo.new(app, width: 192, height: 192)
|
|
39
|
+
# dest.put_zoomed_block(sprite_data, 64, 64, zoom_x: 3, zoom_y: 3)
|
|
40
|
+
#
|
|
41
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkLib/FindPhoto.htm Tk Photo C API
|
|
42
|
+
class Photo
|
|
43
|
+
attr_reader :app, :name
|
|
44
|
+
|
|
45
|
+
@counter = 0
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
# @api private
|
|
49
|
+
def next_name
|
|
50
|
+
@counter += 1
|
|
51
|
+
"teek_photo#{@counter}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Create a new photo image.
|
|
56
|
+
#
|
|
57
|
+
# @param app [Teek::App] the application instance
|
|
58
|
+
# @param name [String, nil] Tcl image name (auto-generated if nil)
|
|
59
|
+
# @param width [Integer, nil] image width in pixels
|
|
60
|
+
# @param height [Integer, nil] image height in pixels
|
|
61
|
+
# @param file [String, nil] path to an image file to load
|
|
62
|
+
# @param data [String, nil] base64-encoded image data
|
|
63
|
+
# @param format [String, nil] image format (e.g. "png", "gif")
|
|
64
|
+
# @param palette [String, nil] palette specification
|
|
65
|
+
# @param gamma [Float, nil] gamma correction value
|
|
66
|
+
def initialize(app, name: nil, width: nil, height: nil,
|
|
67
|
+
file: nil, data: nil, format: nil, palette: nil, gamma: nil)
|
|
68
|
+
@app = app
|
|
69
|
+
@name = name || self.class.next_name
|
|
70
|
+
|
|
71
|
+
kwargs = {}
|
|
72
|
+
kwargs[:width] = width if width
|
|
73
|
+
kwargs[:height] = height if height
|
|
74
|
+
kwargs[:file] = file if file
|
|
75
|
+
kwargs[:data] = data if data
|
|
76
|
+
kwargs[:format] = format if format
|
|
77
|
+
kwargs[:palette] = palette if palette
|
|
78
|
+
kwargs[:gamma] = gamma if gamma
|
|
79
|
+
|
|
80
|
+
@app.command(:image, :create, :photo, @name, **kwargs)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Write RGBA pixel data to the image.
|
|
84
|
+
#
|
|
85
|
+
# @param pixel_data [String] binary string, 4 bytes (RGBA) per pixel
|
|
86
|
+
# @param width [Integer] width of the pixel block
|
|
87
|
+
# @param height [Integer] height of the pixel block
|
|
88
|
+
# @param x [Integer] destination X offset
|
|
89
|
+
# @param y [Integer] destination Y offset
|
|
90
|
+
# @param format [:rgba, :argb] pixel format
|
|
91
|
+
# @param composite [:set, :overlay] compositing rule
|
|
92
|
+
# @return [self]
|
|
93
|
+
def put_block(pixel_data, width, height, x: 0, y: 0, format: :rgba, composite: :set)
|
|
94
|
+
opts = { x: x, y: y, format: format, composite: composite }
|
|
95
|
+
@app.interp.photo_put_block(@name, pixel_data, width, height, opts)
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Write RGBA pixel data with zoom and subsample.
|
|
100
|
+
#
|
|
101
|
+
# Zoom replicates each pixel (zoom=3 makes each source pixel 3x3).
|
|
102
|
+
# Subsample skips source pixels (subsample=2 takes every other pixel).
|
|
103
|
+
#
|
|
104
|
+
# @param pixel_data [String] binary string, 4 bytes (RGBA) per pixel
|
|
105
|
+
# @param width [Integer] source width in pixels
|
|
106
|
+
# @param height [Integer] source height in pixels
|
|
107
|
+
# @param x [Integer] destination X offset
|
|
108
|
+
# @param y [Integer] destination Y offset
|
|
109
|
+
# @param zoom_x [Integer] horizontal zoom factor
|
|
110
|
+
# @param zoom_y [Integer] vertical zoom factor
|
|
111
|
+
# @param subsample_x [Integer] horizontal subsample factor
|
|
112
|
+
# @param subsample_y [Integer] vertical subsample factor
|
|
113
|
+
# @param format [:rgba, :argb] pixel format
|
|
114
|
+
# @param composite [:set, :overlay] compositing rule
|
|
115
|
+
# @return [self]
|
|
116
|
+
def put_zoomed_block(pixel_data, width, height,
|
|
117
|
+
x: 0, y: 0, zoom_x: 1, zoom_y: 1,
|
|
118
|
+
subsample_x: 1, subsample_y: 1,
|
|
119
|
+
format: :rgba, composite: :set)
|
|
120
|
+
opts = {
|
|
121
|
+
x: x, y: y,
|
|
122
|
+
zoom_x: zoom_x, zoom_y: zoom_y,
|
|
123
|
+
subsample_x: subsample_x, subsample_y: subsample_y,
|
|
124
|
+
format: format, composite: composite
|
|
125
|
+
}
|
|
126
|
+
@app.interp.photo_put_zoomed_block(@name, pixel_data, width, height, opts)
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Read pixel data from the image.
|
|
131
|
+
#
|
|
132
|
+
# @param x [Integer] source X offset
|
|
133
|
+
# @param y [Integer] source Y offset
|
|
134
|
+
# @param width [Integer, nil] region width (nil for full image)
|
|
135
|
+
# @param height [Integer, nil] region height (nil for full image)
|
|
136
|
+
# @param unpack [Boolean] if true, return flat array of integers instead of binary string
|
|
137
|
+
# @return [Hash] +{ data: String, width: Integer, height: Integer }+ or
|
|
138
|
+
# +{ pixels: Array<Integer>, width: Integer, height: Integer }+ if unpack is true
|
|
139
|
+
def get_image(x: nil, y: nil, width: nil, height: nil, unpack: false)
|
|
140
|
+
opts = { unpack: unpack }
|
|
141
|
+
opts[:x] = x if x
|
|
142
|
+
opts[:y] = y if y
|
|
143
|
+
opts[:width] = width if width
|
|
144
|
+
opts[:height] = height if height
|
|
145
|
+
@app.interp.photo_get_image(@name, opts)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Read a single pixel.
|
|
149
|
+
#
|
|
150
|
+
# @param x [Integer] X coordinate
|
|
151
|
+
# @param y [Integer] Y coordinate
|
|
152
|
+
# @return [Array<Integer>] [r, g, b, a] values (0-255)
|
|
153
|
+
def get_pixel(x, y)
|
|
154
|
+
@app.interp.photo_get_pixel(@name, x, y)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Get image dimensions.
|
|
158
|
+
#
|
|
159
|
+
# @return [Array<Integer>] [width, height]
|
|
160
|
+
def get_size
|
|
161
|
+
@app.interp.photo_get_size(@name)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Set image dimensions. May crop or add transparent pixels.
|
|
165
|
+
#
|
|
166
|
+
# @param width [Integer] new width
|
|
167
|
+
# @param height [Integer] new height
|
|
168
|
+
# @return [self]
|
|
169
|
+
def set_size(width, height)
|
|
170
|
+
@app.interp.photo_set_size(@name, width, height)
|
|
171
|
+
self
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Expand image to at least the given dimensions. Will not shrink.
|
|
175
|
+
#
|
|
176
|
+
# @note Has no effect on photos created with explicit +width:+ / +height:+
|
|
177
|
+
# options. Only works on auto-sized photos (those whose size was set by
|
|
178
|
+
# writing pixel data). This is a Tk limitation.
|
|
179
|
+
#
|
|
180
|
+
# @param width [Integer] minimum width
|
|
181
|
+
# @param height [Integer] minimum height
|
|
182
|
+
# @return [self]
|
|
183
|
+
def expand(width, height)
|
|
184
|
+
@app.interp.photo_expand(@name, width, height)
|
|
185
|
+
self
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Clear the image to fully transparent.
|
|
189
|
+
#
|
|
190
|
+
# @return [self]
|
|
191
|
+
def blank
|
|
192
|
+
@app.interp.photo_blank(@name)
|
|
193
|
+
self
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
alias clear blank
|
|
197
|
+
|
|
198
|
+
# Delete this photo image and free its resources.
|
|
199
|
+
#
|
|
200
|
+
# @return [void]
|
|
201
|
+
def delete
|
|
202
|
+
@app.tcl_eval("image delete #{@name}")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Check if this photo image still exists.
|
|
206
|
+
#
|
|
207
|
+
# @return [Boolean]
|
|
208
|
+
def exist?
|
|
209
|
+
@app.tcl_eval("image type #{@name}") == 'photo'
|
|
210
|
+
rescue Teek::TclError
|
|
211
|
+
false
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# @return [String] the Tcl image name
|
|
215
|
+
def to_s
|
|
216
|
+
@name
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def inspect
|
|
220
|
+
"#<Teek::Photo #{@name}>"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def ==(other)
|
|
224
|
+
other.is_a?(Photo) ? @name == other.name : @name == other.to_s
|
|
225
|
+
end
|
|
226
|
+
alias eql? ==
|
|
227
|
+
|
|
228
|
+
def hash
|
|
229
|
+
@name.hash
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
data/lib/teek/version.rb
CHANGED