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,551 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+ require 'teek/ui/canvas_item'
6
+
7
+ # ui.canvas gains a real item-drawing vocabulary (Handle#line/oval/polygon/
8
+ # rectangle/text/arc/bitmap, each returning a CanvasItem) instead of being a
9
+ # bare container only reachable via ui.raw + raw app.command(:create, ...).
10
+ # CanvasItem addresses Tk's own tagOrId uniformly, so a shared `tags:` group
11
+ # is movable/configurable/deletable exactly like a single item - see
12
+ # test_tagged_addresses_every_item_sharing_a_tag_as_one_group below.
13
+ class TestCanvasItems < Minitest::Test
14
+ include TeekTestHelper
15
+
16
+ def test_virtual_path_marks_past_the_real_tk_path
17
+ item = Teek::UI::CanvasItem.new(:fake_app, '.board', 'I3')
18
+
19
+ assert_equal '.board!I3', item.virtual_path
20
+ end
21
+
22
+ def test_line_creates_a_real_item_addressable_by_the_returned_handle
23
+ assert_tk_app("ui.canvas#line should create a real line item") do
24
+ require 'teek/ui'
25
+
26
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
27
+ session.run_async
28
+ session.app.update
29
+
30
+ item = session[:board].line(10, 10, 50, 50, fill: 'red')
31
+
32
+ assert_equal 'line', session.app.tcl_eval("#{session[:board].path} type #{item.tag_or_id}")
33
+ assert_equal [10.0, 10.0, 50.0, 50.0], item.coords
34
+
35
+ session.app.destroy
36
+ end
37
+ end
38
+
39
+ def test_oval_rectangle_polygon_text_arc_bitmap_all_create_the_right_item_type
40
+ assert_tk_app("every shape method should create the matching Tk item type") do
41
+ require 'teek/ui'
42
+
43
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
44
+ session.run_async
45
+ session.app.update
46
+
47
+ board = session[:board]
48
+ items = {
49
+ 'oval' => board.oval(10, 10, 40, 40),
50
+ 'rectangle' => board.rectangle(10, 10, 40, 40),
51
+ 'polygon' => board.polygon(10, 10, 40, 10, 25, 40),
52
+ 'text' => board.text(10, 10, text: 'Hi'),
53
+ 'arc' => board.arc(10, 10, 40, 40, start: 0, extent: 90),
54
+ 'bitmap' => board.bitmap(10, 10, bitmap: 'gray25'),
55
+ }
56
+
57
+ items.each do |expected_type, item|
58
+ assert_equal expected_type, session.app.tcl_eval("#{board.path} type #{item.tag_or_id}")
59
+ end
60
+
61
+ session.app.destroy
62
+ end
63
+ end
64
+
65
+ def test_coords_accepts_flat_or_nested_arguments_equivalently
66
+ assert_tk_app("flat and nested coordinate arguments should produce the same item") do
67
+ require 'teek/ui'
68
+
69
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
70
+ session.run_async
71
+ session.app.update
72
+
73
+ flat = session[:board].line(10, 10, 50, 50)
74
+ nested = session[:board].line([10, 10], [50, 50])
75
+
76
+ assert_equal flat.coords, nested.coords
77
+
78
+ session.app.destroy
79
+ end
80
+ end
81
+
82
+ def test_move_shifts_relative_to_the_current_position
83
+ assert_tk_app("move should shift by a relative delta") do
84
+ require 'teek/ui'
85
+
86
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
87
+ session.run_async
88
+ session.app.update
89
+
90
+ item = session[:board].oval(10, 10, 30, 30)
91
+ item.move(5, -3)
92
+
93
+ assert_equal [15.0, 7.0, 35.0, 27.0], item.coords
94
+
95
+ session.app.destroy
96
+ end
97
+ end
98
+
99
+ def test_coords_writer_replaces_the_coordinate_list_outright
100
+ assert_tk_app("coords= should replace the coordinate list, not shift it") do
101
+ require 'teek/ui'
102
+
103
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
104
+ session.run_async
105
+ session.app.update
106
+
107
+ item = session[:board].line(0, 0, 10, 10)
108
+ item.coords = [1, 2, 3, 4, 5, 6]
109
+
110
+ assert_equal [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], item.coords
111
+
112
+ session.app.destroy
113
+ end
114
+ end
115
+
116
+ def test_configure_mutates_multiple_options_at_once
117
+ assert_tk_app("configure should mutate item options") do
118
+ require 'teek/ui'
119
+
120
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
121
+ session.run_async
122
+ session.app.update
123
+
124
+ item = session[:board].rectangle(10, 10, 40, 40, fill: 'blue')
125
+ item.configure(fill: 'green', width: 3)
126
+
127
+ assert_equal 'green', item[:fill]
128
+ assert_equal '3.0', item[:width]
129
+
130
+ session.app.destroy
131
+ end
132
+ end
133
+
134
+ def test_bracket_read_and_write_a_single_option
135
+ assert_tk_app("item[:opt] should read, item[:opt] = value should write") do
136
+ require 'teek/ui'
137
+
138
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
139
+ session.run_async
140
+ session.app.update
141
+
142
+ item = session[:board].oval(10, 10, 40, 40, fill: 'red')
143
+ assert_equal 'red', item[:fill]
144
+
145
+ item[:fill] = 'purple'
146
+ assert_equal 'purple', item[:fill]
147
+
148
+ session.app.destroy
149
+ end
150
+ end
151
+
152
+ def test_delete_removes_the_item
153
+ assert_tk_app("delete should remove the item from the canvas") do
154
+ require 'teek/ui'
155
+
156
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
157
+ session.run_async
158
+ session.app.update
159
+
160
+ item = session[:board].oval(10, 10, 40, 40)
161
+ assert item.exists?
162
+
163
+ item.delete
164
+
165
+ refute item.exists?
166
+ assert_nil item.bounds
167
+
168
+ session.app.destroy
169
+ end
170
+ end
171
+
172
+ def test_bring_to_front_and_send_to_back_change_stacking_order
173
+ assert_tk_app("bring_to_front/send_to_back with no target should move all the way") do
174
+ require 'teek/ui'
175
+
176
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
177
+ session.run_async
178
+ session.app.update
179
+
180
+ board = session[:board]
181
+ a = board.oval(10, 10, 20, 20)
182
+ b = board.oval(10, 10, 20, 20)
183
+ c = board.oval(10, 10, 20, 20)
184
+ stacking = -> { session.app.split_list(session.app.tcl_eval("#{board.path} find all")) }
185
+
186
+ assert_equal [a.tag_or_id, b.tag_or_id, c.tag_or_id], stacking.call
187
+
188
+ a.bring_to_front
189
+ assert_equal [b.tag_or_id, c.tag_or_id, a.tag_or_id], stacking.call
190
+
191
+ c.send_to_back
192
+ assert_equal [c.tag_or_id, b.tag_or_id, a.tag_or_id], stacking.call
193
+
194
+ session.app.destroy
195
+ end
196
+ end
197
+
198
+ def test_bring_to_front_with_a_target_stacks_just_above_it
199
+ assert_tk_app("bring_to_front(target) should reposition just above target, not to the very top") do
200
+ require 'teek/ui'
201
+
202
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
203
+ session.run_async
204
+ session.app.update
205
+
206
+ board = session[:board]
207
+ a = board.oval(10, 10, 20, 20)
208
+ b = board.oval(10, 10, 20, 20)
209
+ c = board.oval(10, 10, 20, 20)
210
+ stacking = -> { session.app.split_list(session.app.tcl_eval("#{board.path} find all")) }
211
+
212
+ a.bring_to_front(b)
213
+ assert_equal [b.tag_or_id, a.tag_or_id, c.tag_or_id], stacking.call
214
+
215
+ session.app.destroy
216
+ end
217
+ end
218
+
219
+ def test_scale_resizes_coordinates_relative_to_an_origin
220
+ assert_tk_app("scale should resize relative to the given origin") do
221
+ require 'teek/ui'
222
+
223
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
224
+ session.run_async
225
+ session.app.update
226
+
227
+ item = session[:board].rectangle(10, 10, 20, 20)
228
+ item.scale(10, 10, 2, 2)
229
+
230
+ assert_equal [10.0, 10.0, 30.0, 30.0], item.coords
231
+
232
+ session.app.destroy
233
+ end
234
+ end
235
+
236
+ def test_bounds_returns_the_bounding_box
237
+ assert_tk_app("bounds should return the item's bounding box") do
238
+ require 'teek/ui'
239
+
240
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
241
+ session.run_async
242
+ session.app.update
243
+
244
+ item = session[:board].rectangle(10, 10, 40, 40, outline: '', fill: 'red')
245
+ box = item.bounds
246
+
247
+ refute_nil box
248
+ assert_equal 4, box.length
249
+
250
+ session.app.destroy
251
+ end
252
+ end
253
+
254
+ def test_tagged_addresses_every_item_sharing_a_tag_as_one_group
255
+ assert_tk_app("tagged should address a shared tag as a single movable/configurable/deletable group") do
256
+ require 'teek/ui'
257
+
258
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
259
+ session.run_async
260
+ session.app.update
261
+
262
+ board = session[:board]
263
+ one = board.oval(10, 10, 20, 20, tags: 'group_a')
264
+ two = board.oval(30, 30, 40, 40, tags: 'group_a')
265
+ board.oval(50, 50, 60, 60, tags: 'group_b')
266
+
267
+ group = board.tagged('group_a')
268
+ assert group.exists?
269
+
270
+ group.move(5, 5)
271
+ assert_equal [15.0, 15.0, 25.0, 25.0], one.coords
272
+ assert_equal [35.0, 35.0, 45.0, 45.0], two.coords
273
+
274
+ group.configure(fill: 'orange')
275
+ assert_equal 'orange', one[:fill]
276
+ assert_equal 'orange', two[:fill]
277
+
278
+ group.delete
279
+ refute one.exists?
280
+ refute two.exists?
281
+ assert board.tagged('group_b').exists?
282
+
283
+ session.app.destroy
284
+ end
285
+ end
286
+
287
+ def test_tagged_on_a_tag_matching_nothing_reports_it_does_not_exist
288
+ assert_tk_app("tagged on an unused tag should report exists? false, not raise") do
289
+ require 'teek/ui'
290
+
291
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
292
+ session.run_async
293
+ session.app.update
294
+
295
+ refute session[:board].tagged('nothing_has_this_tag').exists?
296
+
297
+ session.app.destroy
298
+ end
299
+ end
300
+
301
+ # Item-level canvas bindings only fire through Tk's "current item"
302
+ # tracking, which real event generate positioning depends on real X11
303
+ # pointer state - unreliable under Xvfb (see teek core's own
304
+ # test_canvas_bindings.rb). Tk has no "invoke this item binding"
305
+ # command either way, so these read back the bound script and eval it
306
+ # directly - the same pattern teek core already uses to verify item
307
+ # binding dispatch without needing a real synthetic click to land.
308
+
309
+ def test_on_click_fires_only_for_the_specific_item_clicked
310
+ assert_tk_app("on_click's bound script should be scoped per-item, not shared") do
311
+ require 'teek/ui'
312
+
313
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
314
+ session.run_async
315
+ session.app.update
316
+
317
+ board = session[:board]
318
+ a = board.oval(10, 10, 30, 30)
319
+ b = board.oval(100, 100, 120, 120)
320
+
321
+ a_hits = 0
322
+ b_hits = 0
323
+ a.on_click { a_hits += 1 }
324
+ b.on_click { b_hits += 1 }
325
+
326
+ a_script = session.app.tcl_eval("#{board.path} bind #{a.tag_or_id} <Button-1>")
327
+ session.app.tcl_eval(a_script)
328
+ assert_equal 1, a_hits, "invoking A's own binding should fire A's handler"
329
+ assert_equal 0, b_hits, "invoking A's own binding should not fire B's handler"
330
+
331
+ b_script = session.app.tcl_eval("#{board.path} bind #{b.tag_or_id} <Button-1>")
332
+ session.app.tcl_eval(b_script)
333
+ assert_equal 1, a_hits, "invoking B's own binding should not fire A's handler again"
334
+ assert_equal 1, b_hits, "invoking B's own binding should fire B's handler"
335
+
336
+ session.app.destroy
337
+ end
338
+ end
339
+
340
+ def test_on_right_click_fires_a_block_on_the_right_click_event
341
+ assert_tk_app("on_right_click should bind a script that fires the given block") do
342
+ require 'teek/ui'
343
+
344
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
345
+ session.run_async
346
+ session.app.update
347
+
348
+ board = session[:board]
349
+ item = board.oval(10, 10, 30, 30)
350
+ fired = false
351
+ item.on_right_click { fired = true }
352
+
353
+ script = session.app.tcl_eval("#{board.path} bind #{item.tag_or_id} <Button-3>")
354
+ session.app.tcl_eval(script)
355
+
356
+ assert fired, "on_right_click's bound script did not fire the given block"
357
+
358
+ session.app.destroy
359
+ end
360
+ end
361
+
362
+ def test_on_right_click_with_a_menu_pops_up_at_the_clicks_root_coordinates
363
+ assert_tk_app("on_right_click(menu) should tk_popup the given menu at the click's root coordinates") do
364
+ require 'teek/ui'
365
+
366
+ session = Teek::UI.app(title: 'Canvas Items Test') do |ui|
367
+ ui.context_menu(:ctx) { |m| m.item(label: 'Delete') { } }
368
+ ui.canvas(:board, width: 200, height: 200)
369
+ end
370
+ session.run_async
371
+ session.app.update
372
+
373
+ board = session[:board]
374
+ item = board.oval(10, 10, 30, 30)
375
+ item.on_right_click(session[:ctx])
376
+
377
+ session.app.tcl_eval(<<~TCL)
378
+ proc tk_popup {args} {
379
+ set ::last_popup_call $args
380
+ }
381
+ TCL
382
+
383
+ # %X/%Y are literal %-substitution codes Tk fills in at real dispatch
384
+ # time - substitute them ourselves to simulate a click at (123, 456)
385
+ # root coordinates without needing a real synthetic click to land.
386
+ script = session.app.tcl_eval("#{board.path} bind #{item.tag_or_id} <Button-3>")
387
+ session.app.tcl_eval(script.sub('%X', '123').sub('%Y', '456'))
388
+
389
+ captured = session.app.split_list(session.app.tcl_eval('set ::last_popup_call'))
390
+ assert_equal [session[:ctx].path, '123', '456'], captured
391
+
392
+ session.app.destroy
393
+ end
394
+ end
395
+
396
+ def test_on_right_click_with_neither_menu_nor_block_raises
397
+ assert_tk_app("on_right_click with neither a menu nor a block should raise") do
398
+ require 'teek/ui'
399
+
400
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
401
+ session.run_async
402
+ session.app.update
403
+
404
+ item = session[:board].oval(10, 10, 30, 30)
405
+ assert_raises(ArgumentError) { item.on_right_click }
406
+
407
+ session.app.destroy
408
+ end
409
+ end
410
+
411
+ def test_on_right_click_with_both_menu_and_block_raises
412
+ assert_tk_app("on_right_click with both a menu and a block should raise") do
413
+ require 'teek/ui'
414
+
415
+ session = Teek::UI.app(title: 'Canvas Items Test') do |ui|
416
+ ui.context_menu(:ctx) { |m| m.item(label: 'Delete') { } }
417
+ ui.canvas(:board, width: 200, height: 200)
418
+ end
419
+ session.run_async
420
+ session.app.update
421
+
422
+ item = session[:board].oval(10, 10, 30, 30)
423
+ error = assert_raises(ArgumentError) { item.on_right_click(session[:ctx]) { } }
424
+ assert_match(/menu/i, error.message)
425
+
426
+ session.app.destroy
427
+ end
428
+ end
429
+
430
+ def test_on_right_click_with_a_non_menu_handle_raises
431
+ assert_tk_app("on_right_click given a non-menu handle should raise a clear error") do
432
+ require 'teek/ui'
433
+
434
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
435
+ session.run_async
436
+ session.app.update
437
+
438
+ item = session[:board].oval(10, 10, 30, 30)
439
+ error = assert_raises(ArgumentError) { item.on_right_click(session[:board]) }
440
+ assert_match(/menu/i, error.message)
441
+
442
+ session.app.destroy
443
+ end
444
+ end
445
+
446
+ def test_on_drag_delivers_canvas_relative_coordinates
447
+ assert_tk_app("on_drag should deliver canvasx/canvasy-converted coordinates, not raw window coordinates") do
448
+ require 'teek/ui'
449
+
450
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
451
+ session.run_async
452
+ session.app.update
453
+
454
+ board_path = session[:board].path
455
+ # scroll the canvas so canvasx/canvasy meaningfully differ from raw %x/%y -
456
+ # otherwise a bug that skips conversion entirely could still pass by luck.
457
+ session.app.tcl_eval("#{board_path} configure -scrollregion {0 0 1000 1000}")
458
+ session.app.tcl_eval("#{board_path} xview moveto 0.5")
459
+ session.app.update
460
+
461
+ item = session[:board].rectangle(0, 0, 1000, 1000)
462
+ received = nil
463
+ item.on_drag { |x, y| received = [x, y] }
464
+
465
+ # %x/%y are literal %-substitution codes Tk fills in at real dispatch
466
+ # time - substitute them ourselves to simulate a motion at raw window
467
+ # coordinates (50, 60) without needing a real synthetic drag to land.
468
+ script = session.app.tcl_eval("#{board_path} bind #{item.tag_or_id} <B1-Motion>")
469
+ session.app.tcl_eval(script.sub('%x', '50').sub('%y', '60'))
470
+
471
+ expected_x = session.app.command(board_path, :canvasx, 50).to_f.round
472
+ expected_y = session.app.command(board_path, :canvasy, 60).to_f.round
473
+
474
+ refute_nil received
475
+ assert_equal [expected_x, expected_y], received
476
+ refute_equal [50, 60], received, "the canvas is scrolled, so conversion should actually change the coordinates"
477
+
478
+ session.app.destroy
479
+ end
480
+ end
481
+
482
+ def test_draggable_moves_the_item_by_the_drag_delta
483
+ assert_tk_app("draggable should move the item by the drag delta, with no coordinate math in app code") do
484
+ require 'teek/ui'
485
+
486
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
487
+ session.run_async
488
+ session.app.update
489
+
490
+ board = session[:board]
491
+ item = board.oval(10, 10, 30, 30)
492
+ item.draggable
493
+
494
+ # Same script-readback approach as the other item-event tests above -
495
+ # substitute %x/%y ourselves to simulate a press at (20, 20) then a
496
+ # drag to (30, 25), a (10, 5) delta.
497
+ press_script = session.app.tcl_eval("#{board.path} bind #{item.tag_or_id} <Button-1>")
498
+ session.app.tcl_eval(press_script.sub('%x', '20').sub('%y', '20'))
499
+
500
+ drag_script = session.app.tcl_eval("#{board.path} bind #{item.tag_or_id} <B1-Motion>")
501
+ session.app.tcl_eval(drag_script.sub('%x', '30').sub('%y', '25'))
502
+
503
+ assert_equal [20.0, 15.0, 40.0, 35.0], item.coords, "the item should have moved by the (10, 5) drag delta"
504
+
505
+ session.app.destroy
506
+ end
507
+ end
508
+
509
+ def test_deleting_an_item_releases_its_click_and_drag_callbacks
510
+ assert_tk_app("deleting an item should release its on_click/on_drag callbacks, not leak them") do
511
+ require 'teek/ui'
512
+
513
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
514
+ session.run_async
515
+ session.app.update
516
+
517
+ baseline = session.app.interp.callback_ids.length
518
+
519
+ item = session[:board].oval(10, 10, 30, 30)
520
+ item.on_click { }
521
+ item.on_drag { |_x, _y| }
522
+ assert_equal baseline + 2, session.app.interp.callback_ids.length,
523
+ "on_click and on_drag should each register one callback"
524
+
525
+ item.delete
526
+
527
+ assert_equal baseline, session.app.interp.callback_ids.length,
528
+ "deleting the item should release both its click and drag callbacks - on_drag's %-substitution " \
529
+ "args must not stop the leak-tracking regex from recognizing the binding"
530
+
531
+ session.app.destroy
532
+ end
533
+ end
534
+
535
+ def test_shape_methods_raise_on_a_non_canvas_handle
536
+ assert_tk_app("shape creation and tagged should raise a clear error on a non-canvas handle") do
537
+ require 'teek/ui'
538
+
539
+ session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.panel(:not_a_canvas) }
540
+ session.run_async
541
+ session.app.update
542
+
543
+ error = assert_raises(ArgumentError) { session[:not_a_canvas].line(0, 0, 10, 10) }
544
+ assert_match(/canvas/i, error.message)
545
+
546
+ assert_raises(ArgumentError) { session[:not_a_canvas].tagged('whatever') }
547
+
548
+ session.app.destroy
549
+ end
550
+ end
551
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # session.clipboard is a thin, realize-only delegation to Teek::Clipboard
7
+ # (see teek core's own test/test_clipboard.rb for the underlying set/get/
8
+ # clear behavior). What's new to verify at this layer: the delegation
9
+ # itself, and that DSL text widgets need no extra wiring at all for
10
+ # platform-standard copy/cut/paste - Tk's own class bindings already
11
+ # cover it.
12
+ class TestClipboard < Minitest::Test
13
+ include TeekTestHelper
14
+
15
+ def test_clipboard_raises_before_realize
16
+ assert_tk_app("session.clipboard should raise a clear error before realize, not queue") do
17
+ require 'teek/ui'
18
+
19
+ session = Teek::UI.app(title: 'Clipboard Test')
20
+
21
+ assert_raises(Teek::UI::NotRealizedError) { session.clipboard }
22
+ end
23
+ end
24
+
25
+ def test_clipboard_set_and_get_round_trips_through_the_session
26
+ assert_tk_app("session.clipboard.set/.get should round-trip, delegating to the underlying app") do
27
+ require 'teek/ui'
28
+
29
+ session = Teek::UI.app(title: 'Clipboard Test')
30
+ session.run_async
31
+
32
+ session.clipboard.set('hello from teek-ui')
33
+
34
+ assert_equal 'hello from teek-ui', session.clipboard.get
35
+
36
+ session.app.destroy
37
+ end
38
+ end
39
+
40
+ def test_text_box_supports_platform_copy_with_no_manual_wiring
41
+ assert_tk_app("a DSL text_box should support Ctrl-C copy out of the box - no on_key wiring needed") do
42
+ require 'teek/ui'
43
+
44
+ session = Teek::UI.app(title: 'Clipboard Test') { |ui| ui.text_box(:field) }
45
+ session.run_async
46
+ session.app.update
47
+
48
+ path = session[:field].path
49
+ session.app.command(path, :insert, 0, 'hello world')
50
+ session.app.tcl_eval("#{path} selection range 0 5")
51
+ session.app.tcl_eval("focus -force #{path}")
52
+ session.app.update
53
+
54
+ session.clipboard.clear
55
+ session.app.tcl_eval("event generate #{path} <Control-c>")
56
+ session.app.update
57
+
58
+ assert_equal 'hello', session.clipboard.get
59
+
60
+ session.app.destroy
61
+ end
62
+ end
63
+
64
+ def test_text_area_supports_platform_copy_with_no_manual_wiring
65
+ assert_tk_app("a DSL text_area should support Ctrl-C copy out of the box - no on_key wiring needed") do
66
+ require 'teek/ui'
67
+
68
+ session = Teek::UI.app(title: 'Clipboard Test') { |ui| ui.text_area(:notes) }
69
+ session.run_async
70
+ session.app.update
71
+
72
+ path = session[:notes].path
73
+ session.app.command(path, :insert, '1.0', 'hello world')
74
+ session.app.tcl_eval("#{path} tag add sel 1.0 1.5")
75
+ session.app.tcl_eval("focus -force #{path}")
76
+ session.app.update
77
+
78
+ session.clipboard.clear
79
+ session.app.tcl_eval("event generate #{path} <Control-c>")
80
+ session.app.update
81
+
82
+ assert_equal 'hello', session.clipboard.get
83
+
84
+ session.app.destroy
85
+ end
86
+ end
87
+ end