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
data/lib/teek.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require 'tcltklib'
4
4
  require_relative 'teek/version'
5
5
  require_relative 'teek/ractor_support'
6
+ require_relative 'teek/widget'
7
+ require_relative 'teek/photo'
6
8
 
7
9
  # Ruby interface to Tcl/Tk. Provides a thin wrapper around a Tcl interpreter
8
10
  # with Ruby callbacks, event bindings, and background work support.
@@ -52,6 +54,7 @@ module Teek
52
54
  @interp.tcl_eval('package require Tk')
53
55
  hide
54
56
  @widgets = {}
57
+ @widget_counters = Hash.new(0)
55
58
  debug ||= !!ENV['TEEK_DEBUG']
56
59
  track_widgets = true if debug
57
60
  setup_widget_tracking if track_widgets
@@ -118,6 +121,7 @@ module Teek
118
121
  # @param ms [Integer] delay in milliseconds
119
122
  # @yield block to call when the timer fires
120
123
  # @return [String] timer ID, pass to {#after_cancel} to cancel
124
+ # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/after.htm#M5 after ms
121
125
  def after(ms, &block)
122
126
  cb_id = nil
123
127
  cb_id = @interp.register_callback(proc { |*|
@@ -132,6 +136,7 @@ module Teek
132
136
  # Schedule a block to run once when the event loop is idle.
133
137
  # @yield block to call when the event loop is idle
134
138
  # @return [String] timer ID, pass to {#after_cancel} to cancel
139
+ # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/after.htm#M9 after idle
135
140
  def after_idle(&block)
136
141
  cb_id = nil
137
142
  cb_id = @interp.register_callback(proc { |*|
@@ -146,6 +151,7 @@ module Teek
146
151
  # Cancel a pending {#after} or {#after_idle} timer.
147
152
  # @param after_id [String] timer ID returned by {#after} or {#after_idle}
148
153
  # @return [void]
154
+ # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/after.htm#M7 after cancel
149
155
  def after_cancel(after_id)
150
156
  @interp.tcl_eval("after cancel #{after_id}")
151
157
  if (cb_id = after_id.instance_variable_get(:@cb_id))
@@ -206,6 +212,36 @@ module Teek
206
212
  @interp.tcl_eval(parts.join(' '))
207
213
  end
208
214
 
215
+ # Create a Tk widget and return a {Widget} wrapper.
216
+ #
217
+ # Auto-generates a unique path if none is given. The path is derived from
218
+ # the widget type and a monotonic counter.
219
+ #
220
+ # @param type [String, Symbol] Tk widget command (e.g. 'ttk::button', :canvas)
221
+ # @param path [String, nil] explicit Tk path, or nil for auto-naming
222
+ # @param parent [Widget, String, nil] parent widget for path nesting
223
+ # @param kwargs keyword arguments passed to the Tk widget command
224
+ # @return [Widget] the created widget
225
+ #
226
+ # @example Auto-named
227
+ # btn = app.create_widget('ttk::button', text: 'Click')
228
+ # # btn.path => ".ttkbtn1"
229
+ #
230
+ # @example Explicit path
231
+ # frm = app.create_widget('ttk::frame', '.myframe')
232
+ #
233
+ # @example Nested under a parent
234
+ # frm = app.create_widget('ttk::frame')
235
+ # btn = app.create_widget('ttk::button', parent: frm, text: 'Click')
236
+ # # btn.path => ".ttkfrm1.ttkbtn1"
237
+ #
238
+ def create_widget(type, path = nil, parent: nil, **kwargs)
239
+ type_s = type.to_s
240
+ path ||= next_widget_path(type_s, parent)
241
+ command(type_s, path, **kwargs)
242
+ Widget.new(self, path)
243
+ end
244
+
209
245
  # Add a directory to Tcl's package search path.
210
246
  # @param path [String] directory containing Tcl packages
211
247
  # @return [void]
@@ -218,6 +254,7 @@ module Teek
218
254
  # @param version [String, nil] minimum version constraint
219
255
  # @return [String] the version that was loaded
220
256
  # @raise [Teek::TclError] if the package is not found
257
+ # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/package.htm#M10 package require
221
258
  def require_package(name, version = nil)
222
259
  cmd = version ? "package require #{name} #{version}" : "package require #{name}"
223
260
  tcl_eval(cmd)
@@ -228,6 +265,7 @@ module Teek
228
265
  # List all packages known to this interpreter.
229
266
  # Scans +auto_path+ for package indexes before querying.
230
267
  # @return [Array<String>]
268
+ # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/package.htm#M7 package names
231
269
  def package_names
232
270
  scan_packages
233
271
  split_list(tcl_eval('package names'))
@@ -236,6 +274,7 @@ module Teek
236
274
  # Check if a package is already loaded in this interpreter.
237
275
  # @param name [String] package name
238
276
  # @return [Boolean]
277
+ # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/package.htm#M8 package present
239
278
  def package_present?(name)
240
279
  tcl_eval("package present #{name}")
241
280
  true
@@ -247,6 +286,7 @@ module Teek
247
286
  # Scans +auto_path+ for package indexes before querying.
248
287
  # @param name [String] package name
249
288
  # @return [Array<String>]
289
+ # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/package.htm#M14 package versions
250
290
  def package_versions(name)
251
291
  scan_packages
252
292
  split_list(tcl_eval("package versions #{name}"))
@@ -256,6 +296,7 @@ module Teek
256
296
  # @param name [String] variable name
257
297
  # @param value [String] value to set
258
298
  # @return [String] the value
299
+ # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/set.htm set
259
300
  def set_variable(name, value)
260
301
  tcl_eval("set #{name} {#{value}}")
261
302
  end
@@ -264,6 +305,7 @@ module Teek
264
305
  # @param name [String] variable name
265
306
  # @return [String] the value
266
307
  # @raise [Teek::TclError] if the variable doesn't exist
308
+ # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/set.htm set
267
309
  def get_variable(name)
268
310
  tcl_eval("set #{name}")
269
311
  end
@@ -271,15 +313,55 @@ module Teek
271
313
  # Destroy a widget and all its children.
272
314
  # @param widget [String] Tk widget path (e.g. ".frame1")
273
315
  # @return [void]
274
- def destroy(widget)
316
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/destroy.htm destroy
317
+ def destroy(widget = '.')
318
+ raise ArgumentError, 'widget path cannot be nil' if widget.nil?
275
319
  tcl_eval("destroy #{widget}")
276
320
  end
277
321
 
322
+ # Measure the pixel width of a text string in a given font.
323
+ # Uses Tk's C font API directly — faster than the Tcl +font measure+ command.
324
+ # @param font [String] font description (e.g. "Helvetica 12", "TkDefaultFont")
325
+ # @param text [String] text to measure
326
+ # @return [Integer] pixel width
327
+ # @raise [Teek::TclError] if the font is not found
328
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkLib/MeasureChar.htm Tk_TextWidth
329
+ def text_width(font, text)
330
+ @interp.text_width(font, text)
331
+ end
332
+
333
+ # Get font metrics (ascent, descent, linespace) for a given font.
334
+ # Uses Tk's C font API directly.
335
+ # @param font [String] font description (e.g. "Helvetica 12", "TkDefaultFont")
336
+ # @return [Hash{Symbol => Integer}] +:ascent+, +:descent+, +:linespace+
337
+ # @raise [Teek::TclError] if the font is not found
338
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkLib/FontId.htm Tk_GetFontMetrics
339
+ def font_metrics(font)
340
+ @interp.font_metrics(font)
341
+ end
342
+
343
+ # Measure how many bytes of text fit within a pixel width limit.
344
+ # Useful for text truncation, ellipsis, and line wrapping.
345
+ # @param font [String] font description (e.g. "Helvetica 12")
346
+ # @param text [String] text to measure
347
+ # @param max_pixels [Integer] maximum pixel width (-1 for unlimited)
348
+ # @param opts [Hash] options
349
+ # @option opts [Boolean] :partial_ok allow partial character at boundary
350
+ # @option opts [Boolean] :whole_words break only at word boundaries
351
+ # @option opts [Boolean] :at_least_one always return at least one character
352
+ # @return [Hash{Symbol => Integer}] +:bytes+ and +:width+
353
+ # @raise [Teek::TclError] if the font is not found
354
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkLib/MeasureChar.htm Tk_MeasureChars
355
+ def measure_chars(font, text, max_pixels, **opts)
356
+ @interp.measure_chars(font, text, max_pixels, opts)
357
+ end
358
+
278
359
  # Show a busy cursor on a window while executing a block.
279
360
  # The cursor is restored even if the block raises.
280
361
  # @param window [String] Tk window path
281
362
  # @yield the work to perform while busy
282
363
  # @return the block's return value
364
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/busy.htm tk busy
283
365
  def busy(window: '.')
284
366
  tcl_eval("tk busy hold #{window}")
285
367
  tcl_eval('update idletasks')
@@ -290,6 +372,7 @@ module Teek
290
372
 
291
373
  # Enter the Tk event loop. Blocks until the application exits.
292
374
  # @return [void]
375
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkLib/MainLoop.htm Tk_MainLoop
293
376
  def mainloop
294
377
  if defined?(IRB) || defined?(Pry) || $0 == 'irb' || $0 == 'pry'
295
378
  warn "Teek: mainloop blocks the current thread and will make your REPL unresponsive.\n" \
@@ -304,12 +387,14 @@ module Teek
304
387
 
305
388
  # Process all pending events and idle callbacks, then return.
306
389
  # @return [void]
390
+ # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/update.htm update
307
391
  def update
308
392
  @interp.tcl_eval('update')
309
393
  end
310
394
 
311
395
  # Process only pending idle callbacks (e.g. geometry redraws), then return.
312
396
  # @return [void]
397
+ # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/update.htm update idletasks
313
398
  def update_idletasks
314
399
  @interp.tcl_eval('update idletasks')
315
400
  end
@@ -317,6 +402,7 @@ module Teek
317
402
  # Show a window. Defaults to the root window (".").
318
403
  # @param window [String] Tk window path
319
404
  # @return [void]
405
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M38 wm deiconify
320
406
  def show(window = '.')
321
407
  @interp.tcl_eval("wm deiconify #{window}")
322
408
  end
@@ -324,6 +410,7 @@ module Teek
324
410
  # Hide a window without destroying it. Defaults to the root window (".").
325
411
  # @param window [String] Tk window path
326
412
  # @return [void]
413
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M65 wm withdraw
327
414
  def hide(window = '.')
328
415
  @interp.tcl_eval("wm withdraw #{window}")
329
416
  end
@@ -332,6 +419,7 @@ module Teek
332
419
  # @param title [String] new title
333
420
  # @param window [String] Tk window path
334
421
  # @return [String] the title
422
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M63 wm title
335
423
  def set_window_title(title, window: '.')
336
424
  tcl_eval("wm title #{window} {#{title}}")
337
425
  end
@@ -339,6 +427,7 @@ module Teek
339
427
  # Get a window's current title.
340
428
  # @param window [String] Tk window path
341
429
  # @return [String] current title
430
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M63 wm title
342
431
  def window_title(window: '.')
343
432
  tcl_eval("wm title #{window}")
344
433
  end
@@ -347,6 +436,7 @@ module Teek
347
436
  # @param geometry [String] geometry string
348
437
  # @param window [String] Tk window path
349
438
  # @return [String] the geometry
439
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M42 wm geometry
350
440
  def set_window_geometry(geometry, window: '.')
351
441
  tcl_eval("wm geometry #{window} #{geometry}")
352
442
  end
@@ -354,6 +444,7 @@ module Teek
354
444
  # Get a window's current geometry.
355
445
  # @param window [String] Tk window path
356
446
  # @return [String] geometry string (e.g. "400x300+0+0")
447
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M42 wm geometry
357
448
  def window_geometry(window: '.')
358
449
  tcl_eval("wm geometry #{window}")
359
450
  end
@@ -363,6 +454,7 @@ module Teek
363
454
  # @param height [Boolean] allow vertical resize
364
455
  # @param window [String] Tk window path
365
456
  # @return [void]
457
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M59 wm resizable
366
458
  def set_window_resizable(width, height, window: '.')
367
459
  tcl_eval("wm resizable #{window} #{width ? 1 : 0} #{height ? 1 : 0}")
368
460
  end
@@ -370,6 +462,7 @@ module Teek
370
462
  # Get whether a window is resizable.
371
463
  # @param window [String] Tk window path
372
464
  # @return [Array(Boolean, Boolean)] [width_resizable, height_resizable]
465
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M59 wm resizable
373
466
  def window_resizable(window: '.')
374
467
  parts = tcl_eval("wm resizable #{window}").split
375
468
  [parts[0] == '1', parts[1] == '1']
@@ -405,6 +498,7 @@ module Teek
405
498
  # @yield [*values] called when the event fires, with substitution values
406
499
  # @return [void]
407
500
  # @see #unbind
501
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/bind.htm bind
408
502
  #
409
503
  BIND_SUBS = {
410
504
  x: '%x', y: '%y', # window coordinates
@@ -431,6 +525,7 @@ module Teek
431
525
  # @param event [String] Tk event name, with or without angle brackets
432
526
  # @return [void]
433
527
  # @see #bind
528
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/bind.htm bind
434
529
  def unbind(widget, event)
435
530
  event_str = event.start_with?('<') ? event : "<#{event}>"
436
531
  @interp.tcl_eval("bind #{widget} #{event_str} {}")
@@ -481,6 +576,54 @@ module Teek
481
576
 
482
577
  private
483
578
 
579
+ # Short prefixes for common Tk widget types.
580
+ # The base name (after the last ::) is looked up here; the namespace
581
+ # prefix (e.g. "ttk") is prepended verbatim. Unmapped types fall
582
+ # back to the full lowercased name with colons stripped.
583
+ WIDGET_PREFIXES = {
584
+ 'button' => 'btn',
585
+ 'label' => 'lbl',
586
+ 'entry' => 'ent',
587
+ 'frame' => 'frm',
588
+ 'text' => 'txt',
589
+ 'canvas' => 'cvs',
590
+ 'scrollbar' => 'sb',
591
+ 'scale' => 'scl',
592
+ 'checkbutton' => 'chk',
593
+ 'radiobutton' => 'rad',
594
+ 'combobox' => 'cbx',
595
+ 'labelframe' => 'lfrm',
596
+ 'treeview' => 'tv',
597
+ 'notebook' => 'nb',
598
+ 'progressbar' => 'pbar',
599
+ 'separator' => 'sep',
600
+ 'spinbox' => 'spn',
601
+ 'panedwindow' => 'pw',
602
+ 'toplevel' => 'top',
603
+ 'menubutton' => 'mbtn',
604
+ 'sizegrip' => 'sg',
605
+ }.freeze
606
+ private_constant :WIDGET_PREFIXES
607
+
608
+ def next_widget_path(type, parent)
609
+ prefix = widget_prefix(type)
610
+ @widget_counters[prefix] += 1
611
+ parent_path = parent ? parent.to_s : ''
612
+ if parent_path.empty? || parent_path == '.'
613
+ ".#{prefix}#{@widget_counters[prefix]}"
614
+ else
615
+ "#{parent_path}.#{prefix}#{@widget_counters[prefix]}"
616
+ end
617
+ end
618
+
619
+ def widget_prefix(type)
620
+ parts = type.downcase.split('::')
621
+ base = parts.pop
622
+ ns = parts.join
623
+ short = WIDGET_PREFIXES[base] || base
624
+ "#{ns}#{short}"
625
+ end
626
+
484
627
  # Force Tcl to scan auto_path for pkgIndex.tcl files so that
485
628
  # package_names and package_versions reflect all discoverable packages.
486
629
  def scan_packages
data/sample/calculator.rb CHANGED
@@ -23,21 +23,18 @@ class Calculator
23
23
 
24
24
  def build_ui
25
25
  @app.show
26
- @app.command(:wm, 'title', '.', 'Calculator')
27
- @app.command(:wm, 'resizable', '.', 0, 0)
26
+ @app.set_window_title('Calculator')
27
+ @app.set_window_resizable(false, false)
28
28
 
29
29
  # Button style — use a larger font since macOS aqua theme
30
30
  # ignores vertical stretch; font size drives button height
31
31
  @app.tcl_eval('ttk::style configure Calc.TButton -font {{TkDefaultFont} 18}')
32
32
 
33
33
  # Display
34
- @app.command(:set, '::display', '0')
35
- @app.command('ttk::entry', '.display',
36
- textvariable: '::display',
37
- justify: :right,
38
- state: :readonly,
39
- font: '{TkDefaultFont} 24')
40
- @app.command(:grid, '.display', row: 0, column: 0, columnspan: 4,
34
+ @app.set_variable('::display', '0')
35
+ @display = @app.create_widget('ttk::entry', textvariable: '::display',
36
+ justify: :right, state: :readonly, font: '{TkDefaultFont} 24')
37
+ @display.grid(row: 0, column: 0, columnspan: 4,
41
38
  sticky: :ew, padx: 4, pady: 4, ipady: 8)
42
39
 
43
40
  build_buttons
@@ -82,32 +79,30 @@ class Calculator
82
79
  # Click a button by its label (for demo/testing).
83
80
  # In recording mode, shows the pressed visual state briefly before invoking.
84
81
  def click(label, recording: false)
85
- path = @buttons[label]
86
- return unless path
82
+ widget = @buttons[label]
83
+ return unless widget
87
84
  if recording
88
- @app.command(path, 'state', 'pressed')
85
+ widget.command('state', 'pressed')
89
86
  @app.after(80) {
90
- @app.command(path, 'state', '!pressed')
91
- @app.command(path, 'invoke')
87
+ widget.command('state', '!pressed')
88
+ widget.command(:invoke)
92
89
  }
93
90
  else
94
- @app.command(path, 'invoke')
91
+ widget.command(:invoke)
95
92
  end
96
93
  end
97
94
 
98
95
  def button(text, row, col, style: :num, colspan: 1, &action)
99
- @btn_id = (@btn_id || 0) + 1
100
96
  @buttons ||= {}
101
- path = ".btn_#{@btn_id}"
102
- @buttons[text] = path
103
- @app.command('ttk::button', path, text: text, style: 'Calc.TButton',
97
+ widget = @app.create_widget('ttk::button', text: text, style: 'Calc.TButton',
104
98
  command: proc { |*| action.call })
105
- @app.command(:grid, path, row: row, column: col, columnspan: colspan,
99
+ @buttons[text] = widget
100
+ widget.grid(row: row, column: col, columnspan: colspan,
106
101
  sticky: :nsew, padx: 2, pady: 2)
107
102
  end
108
103
 
109
104
  def update_display
110
- @app.command(:set, '::display', @display_value)
105
+ @app.set_variable('::display', @display_value)
111
106
  end
112
107
 
113
108
  # --- Calculator logic ---
data/sample/debug_demo.rb CHANGED
@@ -13,33 +13,31 @@ app.set_window_title('Debug Demo App')
13
13
  app.set_window_geometry('300x200')
14
14
 
15
15
  # Create some widgets
16
- app.tcl_eval('ttk::frame .f')
17
- app.tcl_eval('pack .f -fill both -expand 1 -padx 10 -pady 10')
16
+ frame = app.create_widget('ttk::frame')
17
+ app.command(:pack, frame, fill: :both, expand: 1, padx: 10, pady: 10)
18
18
 
19
- app.tcl_eval('ttk::label .f.lbl -text "Hello from the app"')
20
- app.tcl_eval('pack .f.lbl -pady 5')
19
+ lbl = app.create_widget('ttk::label', parent: frame, text: 'Hello from the app')
20
+ app.command(:pack, lbl, pady: 5)
21
21
 
22
- app.tcl_eval('ttk::entry .f.ent')
23
- app.tcl_eval('pack .f.ent -pady 5')
22
+ ent = app.create_widget('ttk::entry', parent: frame)
23
+ app.command(:pack, ent, pady: 5)
24
24
 
25
25
  # Button that creates more widgets dynamically
26
- counter = 0
27
- cb = app.register_callback(proc { |*|
28
- counter += 1
29
- app.tcl_eval("ttk::button .f.btn#{counter} -text {Button #{counter}}")
30
- app.tcl_eval("pack .f.btn#{counter} -pady 2")
31
- })
32
- app.tcl_eval("ttk::button .f.add -text {Add Widget} -command {ruby_callback #{cb}}")
33
- app.tcl_eval('pack .f.add -pady 5')
26
+ dynamic_widgets = []
27
+ add_btn = app.create_widget('ttk::button', parent: frame, text: 'Add Widget',
28
+ command: proc { |*|
29
+ btn = app.create_widget('ttk::button', parent: frame, text: "Button #{dynamic_widgets.size + 1}")
30
+ app.command(:pack, btn, pady: 2)
31
+ dynamic_widgets << btn
32
+ })
33
+ app.command(:pack, add_btn, pady: 5)
34
34
 
35
35
  # Button to destroy last widget
36
- rm_cb = app.register_callback(proc { |*|
37
- if counter > 0
38
- app.destroy(".f.btn#{counter}")
39
- counter -= 1
40
- end
41
- })
42
- app.tcl_eval("ttk::button .f.rm -text {Remove Widget} -command {ruby_callback #{rm_cb}}")
43
- app.tcl_eval('pack .f.rm -pady 5')
36
+ rm_btn = app.create_widget('ttk::button', parent: frame, text: 'Remove Widget',
37
+ command: proc { |*|
38
+ widget = dynamic_widgets.pop
39
+ widget&.destroy
40
+ })
41
+ app.command(:pack, rm_btn, pady: 5)
44
42
 
45
43
  app.mainloop