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.
- checksums.yaml +4 -4
- data/README.md +99 -15
- data/Rakefile +201 -2
- data/ext/teek/extconf.rb +1 -1
- data/ext/teek/tcltkbridge.c +3 -110
- 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 +35 -6
- data/lib/teek/debugger.rb +37 -32
- data/lib/teek/method_coverage_service.rb +265 -0
- data/lib/teek/photo.rb +232 -0
- data/lib/teek/ractor_support.rb +1 -1
- data/lib/teek/version.rb +1 -1
- data/lib/teek/widget.rb +104 -0
- data/lib/teek.rb +144 -1
- data/sample/calculator.rb +16 -21
- data/sample/debug_demo.rb +20 -22
- 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/threading_demo.rb +127 -132
- 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
|
|
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
|
}
|
|
@@ -153,13 +153,21 @@ module Teek
|
|
|
153
153
|
# @return [self]
|
|
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. 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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|