teek-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +452 -0
  4. data/lib/teek/ui/canvas_item.rb +270 -0
  5. data/lib/teek/ui/component_handle.rb +32 -0
  6. data/lib/teek/ui/document.rb +185 -0
  7. data/lib/teek/ui/errors.rb +41 -0
  8. data/lib/teek/ui/event_binding.rb +22 -0
  9. data/lib/teek/ui/event_bus.rb +53 -0
  10. data/lib/teek/ui/grid_validator.rb +89 -0
  11. data/lib/teek/ui/handle.rb +585 -0
  12. data/lib/teek/ui/image.rb +58 -0
  13. data/lib/teek/ui/keysyms.rb +72 -0
  14. data/lib/teek/ui/menu_builder.rb +130 -0
  15. data/lib/teek/ui/menu_entry_addressing.rb +62 -0
  16. data/lib/teek/ui/modal_stack.rb +99 -0
  17. data/lib/teek/ui/mouse_events.rb +31 -0
  18. data/lib/teek/ui/node.rb +141 -0
  19. data/lib/teek/ui/option_dump_parsing.rb +31 -0
  20. data/lib/teek/ui/overlay_anchors.rb +29 -0
  21. data/lib/teek/ui/overlay_validator.rb +33 -0
  22. data/lib/teek/ui/pane_validator.rb +30 -0
  23. data/lib/teek/ui/realized_node.rb +25 -0
  24. data/lib/teek/ui/realizer.rb +577 -0
  25. data/lib/teek/ui/scope.rb +40 -0
  26. data/lib/teek/ui/screens.rb +141 -0
  27. data/lib/teek/ui/session.rb +417 -0
  28. data/lib/teek/ui/tab_validator.rb +30 -0
  29. data/lib/teek/ui/text_content.rb +321 -0
  30. data/lib/teek/ui/tree_inspector.rb +97 -0
  31. data/lib/teek/ui/validator.rb +131 -0
  32. data/lib/teek/ui/var.rb +98 -0
  33. data/lib/teek/ui/version.rb +7 -0
  34. data/lib/teek/ui/widget_addressing.rb +47 -0
  35. data/lib/teek/ui/widget_dsl.rb +512 -0
  36. data/lib/teek/ui/widget_type.rb +242 -0
  37. data/lib/teek/ui/widget_types/button.rb +7 -0
  38. data/lib/teek/ui/widget_types/canvas.rb +15 -0
  39. data/lib/teek/ui/widget_types/checkbox.rb +7 -0
  40. data/lib/teek/ui/widget_types/column.rb +14 -0
  41. data/lib/teek/ui/widget_types/context_menu.rb +15 -0
  42. data/lib/teek/ui/widget_types/divider.rb +10 -0
  43. data/lib/teek/ui/widget_types/dropdown.rb +7 -0
  44. data/lib/teek/ui/widget_types/grid.rb +12 -0
  45. data/lib/teek/ui/widget_types/group.rb +7 -0
  46. data/lib/teek/ui/widget_types/label.rb +7 -0
  47. data/lib/teek/ui/widget_types/list.rb +7 -0
  48. data/lib/teek/ui/widget_types/menu_bar.rb +26 -0
  49. data/lib/teek/ui/widget_types/menu_checkbox.rb +14 -0
  50. data/lib/teek/ui/widget_types/menu_item.rb +20 -0
  51. data/lib/teek/ui/widget_types/menu_radio.rb +14 -0
  52. data/lib/teek/ui/widget_types/number_box.rb +7 -0
  53. data/lib/teek/ui/widget_types/pane.rb +37 -0
  54. data/lib/teek/ui/widget_types/panel.rb +7 -0
  55. data/lib/teek/ui/widget_types/progress.rb +7 -0
  56. data/lib/teek/ui/widget_types/radio.rb +7 -0
  57. data/lib/teek/ui/widget_types/row.rb +14 -0
  58. data/lib/teek/ui/widget_types/scrollable.rb +17 -0
  59. data/lib/teek/ui/widget_types/slider.rb +7 -0
  60. data/lib/teek/ui/widget_types/spacer.rb +14 -0
  61. data/lib/teek/ui/widget_types/split.rb +17 -0
  62. data/lib/teek/ui/widget_types/tab.rb +35 -0
  63. data/lib/teek/ui/widget_types/table.rb +9 -0
  64. data/lib/teek/ui/widget_types/tabs.rb +10 -0
  65. data/lib/teek/ui/widget_types/text_area.rb +7 -0
  66. data/lib/teek/ui/widget_types/text_box.rb +7 -0
  67. data/lib/teek/ui/widget_types/tree.rb +9 -0
  68. data/lib/teek/ui/widget_types/window.rb +50 -0
  69. data/lib/teek/ui/widget_types.rb +121 -0
  70. data/lib/teek/ui/widget_validators.rb +62 -0
  71. data/lib/teek/ui.rb +54 -0
  72. data/teek-ui.gemspec +24 -0
  73. data/test/support/fake_app.rb +57 -0
  74. data/test/test_busy.rb +82 -0
  75. data/test/test_canvas_items.rb +551 -0
  76. data/test/test_clipboard.rb +87 -0
  77. data/test/test_close_handling.rb +118 -0
  78. data/test/test_component.rb +151 -0
  79. data/test/test_component_realtk.rb +120 -0
  80. data/test/test_debug_info.rb +158 -0
  81. data/test/test_document.rb +317 -0
  82. data/test/test_event_bus.rb +163 -0
  83. data/test/test_events.rb +164 -0
  84. data/test/test_fake_app_contract.rb +62 -0
  85. data/test/test_grid.rb +97 -0
  86. data/test/test_handle.rb +386 -0
  87. data/test/test_handle_destroy_realtk.rb +344 -0
  88. data/test/test_helper.rb +20 -0
  89. data/test/test_image.rb +24 -0
  90. data/test/test_image_realtk.rb +146 -0
  91. data/test/test_incremental_realize.rb +203 -0
  92. data/test/test_keysyms.rb +76 -0
  93. data/test/test_layout.rb +185 -0
  94. data/test/test_lazy_realize.rb +128 -0
  95. data/test/test_managed_window.rb +304 -0
  96. data/test/test_menu_dsl.rb +230 -0
  97. data/test/test_menu_entry_addressing.rb +73 -0
  98. data/test/test_menu_realize.rb +296 -0
  99. data/test/test_modal_handle.rb +147 -0
  100. data/test/test_modal_stack.rb +219 -0
  101. data/test/test_modal_stack_realtk.rb +131 -0
  102. data/test/test_native_scrollable.rb +250 -0
  103. data/test/test_node.rb +193 -0
  104. data/test/test_overlay.rb +139 -0
  105. data/test/test_raw.rb +58 -0
  106. data/test/test_reactive_vars.rb +118 -0
  107. data/test/test_realizer.rb +264 -0
  108. data/test/test_scope.rb +38 -0
  109. data/test/test_screens.rb +260 -0
  110. data/test/test_screens_realtk.rb +314 -0
  111. data/test/test_scrollable.rb +185 -0
  112. data/test/test_scrollable_wheel.rb +172 -0
  113. data/test/test_scrollbar_auto_hide.rb +127 -0
  114. data/test/test_split.rb +145 -0
  115. data/test/test_tabs.rb +201 -0
  116. data/test/test_text_content.rb +664 -0
  117. data/test/test_toast.rb +126 -0
  118. data/test/test_tree_inspector.rb +183 -0
  119. data/test/test_ui.rb +428 -0
  120. data/test/test_validator.rb +209 -0
  121. data/test/test_var.rb +32 -0
  122. data/test/test_widget_dsl.rb +621 -0
  123. data/test/test_widget_introspection.rb +126 -0
  124. data/test/test_widget_types.rb +429 -0
  125. data/test/test_widget_types_realtk.rb +82 -0
  126. data/test/test_widget_validators.rb +82 -0
  127. metadata +207 -0
@@ -0,0 +1,344 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # Real-Tk coverage of Handle#destroy!'s auto-defer behavior - needs a
7
+ # genuine Tcl callback dispatch to exercise (Teek.in_callback? reflects
8
+ # the real interpreter's live callback depth, meaningless without one),
9
+ # so this can't be headless like test_lazy_realize.rb's Realizer-level
10
+ # coverage.
11
+ class TestHandleDestroyRealTk < Minitest::Test
12
+ include TeekTestHelper
13
+
14
+ def test_a_close_button_destroying_its_own_containing_window_does_not_error
15
+ assert_tk_app("a close button tearing down its own containing window via destroy! should not race ttk's own internal same-click bindings") do
16
+ require 'teek/ui'
17
+
18
+ handle = nil
19
+ session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
20
+ handle = ui.window(:dialog, modal: true) { |w| w.button(:close, text: 'Close').on_click { handle.destroy! } }
21
+ end
22
+ session.run_async
23
+ session.app.update
24
+
25
+ # the "invalid command name" hazard this fixes only ever surfaced
26
+ # via Tcl's bgerror (a background/asynchronous error), never as a
27
+ # Ruby exception raised back through event generate - override it
28
+ # to capture the message instead of letting the default handler
29
+ # print/ignore it.
30
+ session.app.tcl_eval(<<~'TCL')
31
+ proc _test_bgerror {msg opts} {
32
+ set ::bgerror_msg $msg
33
+ }
34
+ interp bgerror {} _test_bgerror
35
+ set ::bgerror_msg {}
36
+ TCL
37
+
38
+ session.app.tcl_eval("event generate #{handle.path}.close <Button-1>")
39
+ session.app.tcl_eval("event generate #{handle.path}.close <ButtonRelease-1>")
40
+ session.app.update
41
+
42
+ bgerror = session.app.tcl_eval('set ::bgerror_msg')
43
+ assert_empty bgerror, "no Tcl-level error should have been raised by ttk's own bindings running against an already-destroyed widget"
44
+
45
+ session.app.destroy
46
+ end
47
+ end
48
+
49
+ def test_destroy_outside_any_callback_is_synchronous
50
+ assert_tk_app("destroy! called outside a callback (default defer: nil) should tear down immediately, with no update needed") do
51
+ require 'teek/ui'
52
+
53
+ session = Teek::UI.app(title: 'Destroy Defer Test') { |ui| ui.panel(:box) }
54
+ session.run_async
55
+ session.app.update
56
+
57
+ path = session[:box].path
58
+ session[:box].destroy!
59
+
60
+ refute session.app.winfo.exists?(path), "destroy! outside a callback should destroy synchronously, before the call returns"
61
+
62
+ session.app.destroy
63
+ end
64
+ end
65
+
66
+ def test_defer_false_forces_synchronous_destroy_even_inside_a_callback
67
+ assert_tk_app("destroy!(defer: false) should force an immediate synchronous destroy even from inside a callback") do
68
+ require 'teek/ui'
69
+
70
+ handle = nil
71
+ session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
72
+ ui.panel(:host) do |p|
73
+ handle = p.panel(:box)
74
+ p.button(:go, text: 'Go').on_click { handle.destroy!(defer: false) }
75
+ end
76
+ end
77
+ session.run_async
78
+ session.app.update
79
+ path = handle.path
80
+
81
+ session.app.tcl_eval("event generate #{session[:go].path} <Button-1>")
82
+ # deliberately NOT calling session.app.update before checking - a
83
+ # forced synchronous destroy should already be gone, without
84
+ # needing an idle pass to catch up.
85
+ refute session.app.winfo.exists?(path)
86
+
87
+ session.app.destroy
88
+ end
89
+ end
90
+
91
+ def test_defer_true_forces_deferred_destroy_even_outside_a_callback
92
+ assert_tk_app("destroy!(defer: true) should defer even outside a callback - the widget is still there right after the call, gone after the next idle") do
93
+ require 'teek/ui'
94
+
95
+ session = Teek::UI.app(title: 'Destroy Defer Test') { |ui| ui.panel(:box) }
96
+ session.run_async
97
+ session.app.update
98
+
99
+ path = session[:box].path
100
+ session[:box].destroy!(defer: true)
101
+
102
+ assert session.app.winfo.exists?(path), "a deferred destroy should not have run yet, synchronously, right after the call"
103
+
104
+ session.app.update
105
+
106
+ refute session.app.winfo.exists?(path), "the deferred destroy should have run by the next update (which processes idle callbacks)"
107
+
108
+ session.app.destroy
109
+ end
110
+ end
111
+
112
+ def test_calling_destroy_twice_while_a_deferred_destroy_is_still_pending_is_safe
113
+ assert_tk_app("calling destroy! a second time on the same handle before its own deferred teardown has run should not raise or double-schedule") do
114
+ require 'teek/ui'
115
+
116
+ handle = nil
117
+ session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
118
+ ui.panel(:host) do |p|
119
+ handle = p.panel(:box)
120
+ p.button(:go, text: 'Go').on_click {
121
+ handle.destroy!
122
+ handle.destroy! # same handle, still pending - must not raise
123
+ }
124
+ end
125
+ end
126
+ session.run_async
127
+ session.app.update
128
+ path = handle.path
129
+
130
+ session.app.tcl_eval("event generate #{session[:go].path} <Button-1>")
131
+ session.app.update
132
+
133
+ refute session.app.winfo.exists?(path)
134
+
135
+ session.app.destroy
136
+ end
137
+ end
138
+
139
+ def test_destroying_an_ancestor_then_a_descendant_is_safe
140
+ assert_tk_app("destroying an ancestor's handle, then a descendant's own handle, should not raise even though the descendant's own Tk widget is already gone") do
141
+ require 'teek/ui'
142
+
143
+ parent_handle = nil
144
+ child_handle = nil
145
+ session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
146
+ ui.panel(:host) do |p|
147
+ parent_handle = p.panel(:box) { |b| child_handle = b.button(:inner, text: 'Inner') }
148
+ end
149
+ end
150
+ session.run_async
151
+ session.app.update
152
+
153
+ parent_handle.destroy!(defer: false)
154
+ # the descendant's OWN Node#realized is untouched by the ancestor's
155
+ # destroy (only the ancestor's own node gets reset) - its Tk widget
156
+ # is gone already, but Tcl's own destroy command is a documented
157
+ # no-op on an already-gone path, so this should not raise.
158
+ child_handle.destroy!(defer: false)
159
+
160
+ session.app.destroy
161
+ end
162
+ end
163
+
164
+ def test_a_deferred_destroy_releases_its_callback_by_the_next_update
165
+ assert_tk_app("a deferred destroy should release its widget's callback once the idle pass runs, same as a synchronous destroy does immediately") do
166
+ require 'teek/ui'
167
+
168
+ handle = nil
169
+ session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
170
+ ui.panel(:host) do |p|
171
+ handle = p.panel(:box) { |b| b.button(:a, text: 'A').on_click { } }
172
+ end
173
+ end
174
+ session.run_async
175
+ session.app.update
176
+ with_button_baseline = session.app.interp.callback_ids.length
177
+
178
+ handle.destroy!(defer: true)
179
+ # scheduling the deferred teardown itself registers one new
180
+ # after_idle callback - :a's own on_click isn't released yet.
181
+ assert_equal with_button_baseline + 1, session.app.interp.callback_ids.length,
182
+ "a still-pending deferred destroy should not have released :a's callback yet"
183
+
184
+ session.app.update
185
+
186
+ assert_equal with_button_baseline - 1, session.app.interp.callback_ids.length,
187
+ "once the deferred destroy actually runs, both its own after_idle registration and :a's on_click should be released"
188
+
189
+ session.app.destroy
190
+ end
191
+ end
192
+
193
+ def test_destroying_a_widget_unlinks_it_from_its_parents_children
194
+ assert_tk_app("destroy! should remove the destroyed node from its parent's own children, not just clear its Tk-realized state") do
195
+ require 'teek/ui'
196
+
197
+ session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
198
+ ui.panel(:host) { |p| p.button(:item, text: 'Item') }
199
+ end
200
+ session.run_async
201
+ session.app.update
202
+ host_node = session.document.find(:host)
203
+ item_node = session.document.find(:item)
204
+
205
+ session[:item].destroy!(defer: false)
206
+
207
+ refute_includes host_node.children, item_node
208
+
209
+ session.app.destroy
210
+ end
211
+ end
212
+
213
+ def test_destroying_a_named_widget_frees_its_name_for_reuse
214
+ assert_tk_app("destroying a named widget should let a later widget reuse the same name in the same scope, and ui[:name] should be nil in between") do
215
+ require 'teek/ui'
216
+
217
+ session = Teek::UI.app(title: 'Destroy Defer Test') { |ui| ui.panel(:host) }
218
+ session.run_async
219
+ session.app.update
220
+
221
+ session.add(:host) { |a| a.button(:item, text: 'First') }
222
+ session.app.update
223
+ first_path = session[:item].path
224
+ session[:item].destroy!(defer: false)
225
+
226
+ assert_nil session[:item], "ui[:item] should be nil once the widget that owned that name is destroyed"
227
+
228
+ session.add(:host) { |a| a.button(:item, text: 'Second') }
229
+ session.app.update
230
+
231
+ refute_nil session[:item], "a new :item should be addable again under the same name once the old one is gone"
232
+ assert_equal 'Second', session.app.command(session[:item].path, :cget, '-text')
233
+ refute_equal first_path, session[:item].path
234
+
235
+ session.app.destroy
236
+ end
237
+ end
238
+
239
+ def test_repeated_add_and_destroy_of_a_named_widget_under_one_parent_does_not_crash
240
+ assert_tk_app("repeatedly adding and destroying a plain (non-window) named widget under one shared parent should never crash arrange_children") do
241
+ require 'teek/ui'
242
+
243
+ session = Teek::UI.app(title: 'Destroy Defer Test') { |ui| ui.panel(:host) }
244
+ session.run_async
245
+ session.app.update
246
+
247
+ 5.times do |i|
248
+ handle = nil
249
+ session.add(:host) { |a| handle = a.button(:item, text: "Item #{i}") }
250
+ session.app.update
251
+ assert_equal "Item #{i}", session.app.command(handle.path, :cget, '-text')
252
+ handle.destroy!(defer: false)
253
+ session.app.update
254
+ end
255
+
256
+ assert_nil session[:item]
257
+ assert_equal 0, session.document.find(:host).children.length
258
+
259
+ session.app.destroy
260
+ end
261
+ end
262
+
263
+ def test_a_sibling_added_after_a_destroy_still_arranges_correctly
264
+ assert_tk_app("arrange_children should correctly position a new sibling relative to survivors, with no trace of a destroyed sibling") do
265
+ require 'teek/ui'
266
+
267
+ session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
268
+ ui.column(:host, gap: 10) { |c| c.button(:first, text: 'First') }
269
+ end
270
+ session.run_async
271
+ session.app.update
272
+
273
+ doomed = nil
274
+ session.add(:host) { |a| doomed = a.button(:doomed, text: 'Doomed') }
275
+ session.app.update
276
+ doomed.destroy!(defer: false)
277
+ session.app.update
278
+
279
+ session.add(:host) { |a| a.button(:second, text: 'Second') }
280
+ session.app.update
281
+
282
+ first_bottom = session.app.winfo.rooty(session[:first].path) + session.app.winfo.height(session[:first].path)
283
+ second_top = session.app.winfo.rooty(session[:second].path)
284
+ assert_equal 10, second_top - first_bottom,
285
+ "the new sibling should be positioned relative to :first, as if :doomed never existed"
286
+
287
+ session.app.destroy
288
+ end
289
+ end
290
+
291
+ def test_popping_and_destroying_a_screen_leaves_no_stale_entry_in_screens
292
+ assert_tk_app("Screens#pop already removes its own Entry before returning the handle, so destroy! afterward can't leave anything stale in Screens' own bookkeeping") do
293
+ require 'teek/ui'
294
+
295
+ session = Teek::UI.app(title: 'Destroy Defer Test') { |ui| ui.panel(:picker) }
296
+ session.run_async
297
+ session.app.update
298
+
299
+ session.screens.push(:picker, session[:picker])
300
+ popped = session.screens.pop
301
+ popped.destroy!(defer: false)
302
+
303
+ refute session.screens.active?
304
+ assert_nil session.screens.current
305
+ assert_nil session.screens.current_screen
306
+ assert_equal 0, session.screens.size
307
+
308
+ session.app.destroy
309
+ end
310
+ end
311
+
312
+ def test_destroying_an_unrelated_widget_does_not_disturb_an_already_built_menu
313
+ assert_tk_app("destroying (and unlinking) an unrelated widget elsewhere in the tree should have zero effect on an already-built menu's own entries/ordering") do
314
+ require 'teek/ui'
315
+
316
+ clicked = []
317
+ session = Teek::UI.app(title: 'Destroy Defer Test') do |ui|
318
+ ui.menu_bar do |mb|
319
+ mb.menu(:file, label: 'File') do |f|
320
+ f.item(label: 'One') { clicked << :one }
321
+ f.item(label: 'Two') { clicked << :two }
322
+ end
323
+ end
324
+ ui.panel(:host) { |p| p.button(:item, text: 'Item') }
325
+ end
326
+ session.run_async
327
+ session.app.update
328
+
329
+ session[:item].destroy!(defer: false)
330
+ session.app.update
331
+
332
+ file_path = session[:file].path
333
+ assert_equal 'One', session.app.command(file_path, :entrycget, 0, '-label')
334
+ assert_equal 'Two', session.app.command(file_path, :entrycget, 1, '-label')
335
+
336
+ session.app.tcl_eval("#{file_path} invoke 0")
337
+ session.app.tcl_eval("#{file_path} invoke 1")
338
+
339
+ assert_equal [:one, :two], clicked, "menu entries should still fire in their original order, unaffected by an unrelated destroy elsewhere"
340
+
341
+ session.app.destroy
342
+ end
343
+ end
344
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ if ENV['COVERAGE']
4
+ require 'simplecov'
5
+ require_relative '../../test/simplecov_config'
6
+
7
+ coverage_name = ENV['COVERAGE_NAME'] || 'ui'
8
+ SimpleCov.coverage_dir "#{SimpleCovConfig::PROJECT_ROOT}/coverage/results/#{coverage_name}"
9
+ SimpleCov.command_name "ui:#{coverage_name}"
10
+ SimpleCov.print_error_status = false
11
+ SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
12
+
13
+ SimpleCov.start do
14
+ SimpleCovConfig.apply_filters(self)
15
+ track_files "#{SimpleCovConfig::PROJECT_ROOT}/lib/**/*.rb"
16
+ end
17
+ end
18
+
19
+ require "minitest/autorun"
20
+ require_relative "support/fake_app"
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+ require 'teek/ui/image'
5
+
6
+ class TestImage < Minitest::Test
7
+ def test_name_is_the_allocated_tcl_image_name
8
+ image = Teek::UI::Image.new('teek_ui_image_1', '/tmp/whatever.png', {})
9
+
10
+ assert_equal 'teek_ui_image_1', image.name
11
+ end
12
+
13
+ def test_to_s_is_the_allocated_tcl_image_name
14
+ image = Teek::UI::Image.new('teek_ui_image_1', '/tmp/whatever.png', {})
15
+
16
+ assert_equal 'teek_ui_image_1', image.to_s
17
+ end
18
+
19
+ def test_photo_raises_before_realize
20
+ image = Teek::UI::Image.new('teek_ui_image_1', '/tmp/whatever.png', {})
21
+
22
+ assert_raises(Teek::UI::NotRealizedError) { image.photo }
23
+ end
24
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # Real-Tk half of image support's coverage - Image's own build-time shape
7
+ # (name allocation, raises before realize) is covered headlessly in
8
+ # test_image.rb; these exercise the actual load-and-display path against
9
+ # a real Tk photo image.
10
+ class TestImageRealTk < Minitest::Test
11
+ include TeekTestHelper
12
+
13
+ def test_a_label_shows_an_image_loaded_via_the_dsl
14
+ assert_tk_app("ui.image(path) + image: on a label should load and display a real Tk photo image") do
15
+ require 'teek/ui'
16
+ require 'tmpdir'
17
+
18
+ Dir.mktmpdir do |dir|
19
+ path = File.join(dir, 'test.png')
20
+ seed = Teek::Photo.new(app, width: 8, height: 8)
21
+ red = ([255, 0, 0, 255].pack('CCCC')) * (8 * 8)
22
+ seed.put_block(red, 8, 8)
23
+ app.tcl_eval("#{seed.name} write {#{path}} -format png")
24
+ seed.delete
25
+
26
+ icon = nil
27
+ session = Teek::UI.app(title: 'Image Test') do |ui|
28
+ icon = ui.image(path)
29
+ ui.label(:pic, image: icon)
30
+ end
31
+ session.run_async
32
+ session.app.update
33
+
34
+ pic_path = session[:pic].path
35
+ assert_equal icon.name, session.app.command(pic_path, :cget, '-image')
36
+ assert_equal 'photo', session.app.tcl_eval("image type #{icon.name}")
37
+ w, h = icon.photo.get_size
38
+ assert_equal 8, w
39
+ assert_equal 8, h
40
+
41
+ session.app.destroy
42
+ end
43
+ end
44
+ end
45
+
46
+ def test_a_button_shows_an_image_loaded_via_the_dsl
47
+ assert_tk_app("ui.image(path) + image: on a button should load and display a real Tk photo image") do
48
+ require 'teek/ui'
49
+ require 'tmpdir'
50
+
51
+ Dir.mktmpdir do |dir|
52
+ path = File.join(dir, 'test.png')
53
+ seed = Teek::Photo.new(app, width: 8, height: 8)
54
+ blue = ([0, 0, 255, 255].pack('CCCC')) * (8 * 8)
55
+ seed.put_block(blue, 8, 8)
56
+ app.tcl_eval("#{seed.name} write {#{path}} -format png")
57
+ seed.delete
58
+
59
+ icon = nil
60
+ session = Teek::UI.app(title: 'Image Test') do |ui|
61
+ icon = ui.image(path)
62
+ ui.button(:go, image: icon)
63
+ end
64
+ session.run_async
65
+ session.app.update
66
+
67
+ assert_equal icon.name, session.app.command(session[:go].path, :cget, '-image')
68
+
69
+ session.app.destroy
70
+ end
71
+ end
72
+ end
73
+
74
+ def test_replacing_a_widgets_image_via_configure_works
75
+ assert_tk_app("handle.configure(image: another_image) should swap the displayed image") do
76
+ require 'teek/ui'
77
+ require 'tmpdir'
78
+
79
+ Dir.mktmpdir do |dir|
80
+ path_a = File.join(dir, 'a.png')
81
+ path_b = File.join(dir, 'b.png')
82
+ [path_a, path_b].each_with_index do |path, i|
83
+ seed = Teek::Photo.new(app, width: 4, height: 4)
84
+ color = i.zero? ? [255, 0, 0, 255] : [0, 255, 0, 255]
85
+ seed.put_block((color.pack('CCCC')) * 16, 4, 4)
86
+ app.tcl_eval("#{seed.name} write {#{path}} -format png")
87
+ seed.delete
88
+ end
89
+
90
+ icon_a = nil
91
+ icon_b = nil
92
+ session = Teek::UI.app(title: 'Image Test') do |ui|
93
+ icon_a = ui.image(path_a)
94
+ icon_b = ui.image(path_b)
95
+ ui.label(:pic, image: icon_a)
96
+ end
97
+ session.run_async
98
+ session.app.update
99
+
100
+ pic_path = session[:pic].path
101
+ assert_equal icon_a.name, session.app.command(pic_path, :cget, '-image')
102
+
103
+ session[:pic].configure(image: icon_b)
104
+
105
+ assert_equal icon_b.name, session.app.command(pic_path, :cget, '-image')
106
+
107
+ session.app.destroy
108
+ end
109
+ end
110
+ end
111
+
112
+ def test_declared_images_are_retained_on_the_session_and_not_leaked_out_from_under_the_widget
113
+ assert_tk_app("an image declared via ui.image should stay alive (retained on the session) for as long as the widget referencing it exists") do
114
+ require 'teek/ui'
115
+ require 'tmpdir'
116
+
117
+ Dir.mktmpdir do |dir|
118
+ path = File.join(dir, 'test.png')
119
+ seed = Teek::Photo.new(app, width: 4, height: 4)
120
+ seed.put_block(([0, 0, 0, 255].pack('CCCC')) * 16, 4, 4)
121
+ app.tcl_eval("#{seed.name} write {#{path}} -format png")
122
+ seed.delete
123
+
124
+ session = Teek::UI.app(title: 'Image Test') do |ui|
125
+ ui.label(:pic, image: ui.image(path))
126
+ end
127
+ session.run_async
128
+ session.app.update
129
+
130
+ # nothing outside the session holds a reference to the Image -
131
+ # only Session#images does. Forcing a GC pass here is the whole
132
+ # point: if the session weren't retaining it, this would be the
133
+ # moment the image gets collected (and the widget's -image would
134
+ # go stale/broken).
135
+ GC.start
136
+
137
+ assert_equal 1, session.images.length
138
+ image = session.images.first
139
+ assert_equal 'photo', session.app.tcl_eval("image type #{image.name}")
140
+ assert_equal image.name, session.app.command(session[:pic].path, :cget, '-image')
141
+
142
+ session.app.destroy
143
+ end
144
+ end
145
+ end
146
+ end