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,664 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # Handle#text_content's rich text API (TextContent) - a companion
7
+ # object, not methods bolted onto Handle; Tk index syntax passed
8
+ # through verbatim; friendly names primary with Tk-named aliases;
9
+ # leak-safe format/tag event bindings; the read-only footgun
10
+ # transparently absorbed.
11
+ class TestTextContent < Minitest::Test
12
+ include TeekTestHelper
13
+
14
+ def test_text_content_raises_before_realize
15
+ assert_tk_app("text_content should raise before realize, matching every other realize-only accessor") do
16
+ require 'teek/ui'
17
+
18
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
19
+
20
+ assert_raises(Teek::UI::NotRealizedError) { session[:notes].text_content }
21
+ end
22
+ end
23
+
24
+ def test_text_content_raises_on_a_non_text_area_handle
25
+ assert_tk_app("text_content should raise a clear error on a widget that isn't a text_area") do
26
+ require 'teek/ui'
27
+
28
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.button(:go, text: 'Go') }
29
+ session.run_async
30
+
31
+ error = assert_raises(ArgumentError) { session[:go].text_content }
32
+ assert_match(/text_area/i, error.message)
33
+
34
+ session.app.destroy
35
+ end
36
+ end
37
+
38
+ # -- Content -----------------------------------------------------------
39
+
40
+ def test_insert_and_get
41
+ assert_tk_app("insert should add text at an index, get should read it back") do
42
+ require 'teek/ui'
43
+
44
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
45
+ session.run_async
46
+ text = session[:notes].text_content
47
+
48
+ text.insert('1.0', 'hello world')
49
+
50
+ assert_equal 'hello', text.get('1.0', '1.5')
51
+ assert_equal 'hello world', text.value
52
+
53
+ session.app.destroy
54
+ end
55
+ end
56
+
57
+ def test_delete
58
+ assert_tk_app("delete should remove a range") do
59
+ require 'teek/ui'
60
+
61
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
62
+ session.run_async
63
+ text = session[:notes].text_content
64
+ text.insert('1.0', 'hello world')
65
+
66
+ text.delete('1.0', '1.6')
67
+
68
+ assert_equal 'world', text.value
69
+
70
+ session.app.destroy
71
+ end
72
+ end
73
+
74
+ def test_replace
75
+ assert_tk_app("replace should atomically swap a range for new text") do
76
+ require 'teek/ui'
77
+
78
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
79
+ session.run_async
80
+ text = session[:notes].text_content
81
+ text.insert('1.0', 'hello world')
82
+
83
+ text.replace('1.0', '1.5', 'goodbye')
84
+
85
+ assert_equal 'goodbye world', text.value
86
+
87
+ session.app.destroy
88
+ end
89
+ end
90
+
91
+ def test_value_setter_replaces_the_whole_buffer
92
+ assert_tk_app("value= should replace the entire buffer's content") do
93
+ require 'teek/ui'
94
+
95
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
96
+ session.run_async
97
+ text = session[:notes].text_content
98
+ text.insert('1.0', 'old content')
99
+
100
+ text.value = 'new content'
101
+
102
+ assert_equal 'new content', text.value
103
+
104
+ session.app.destroy
105
+ end
106
+ end
107
+
108
+ def test_clear_empties_the_buffer
109
+ assert_tk_app("clear should empty the whole buffer") do
110
+ require 'teek/ui'
111
+
112
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
113
+ session.run_async
114
+ text = session[:notes].text_content
115
+ text.insert('1.0', 'something')
116
+
117
+ text.clear
118
+
119
+ assert_equal '', text.value
120
+
121
+ session.app.destroy
122
+ end
123
+ end
124
+
125
+ def test_end_and_cursor_symbol_aliases_resolve
126
+ assert_tk_app(":end and :cursor should resolve to Tk's own end/insert indices") do
127
+ require 'teek/ui'
128
+
129
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
130
+ session.run_async
131
+ text = session[:notes].text_content
132
+
133
+ text.insert(:end, 'first')
134
+ text.insert(:end, ' second')
135
+
136
+ assert_equal 'first second', text.value
137
+ assert_equal text.index(:end), text.index('end')
138
+
139
+ session.app.destroy
140
+ end
141
+ end
142
+
143
+ # -- Read-only footgun ---------------------------------------------------
144
+
145
+ def test_mutating_methods_transparently_lift_and_restore_read_only_state
146
+ assert_tk_app("insert/delete/replace/value=/clear should work on a read-only widget and leave it read-only after") do
147
+ require 'teek/ui'
148
+
149
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:log, state: :disabled) }
150
+ session.run_async
151
+ text = session[:log].text_content
152
+ assert text.read_only, "should have started read-only, matching state: :disabled"
153
+
154
+ text.insert(:end, 'line one\n')
155
+ assert text.read_only, "should still be read-only after insert"
156
+
157
+ text.value = 'replaced'
158
+ assert text.read_only, "should still be read-only after value="
159
+
160
+ text.clear
161
+ assert text.read_only, "should still be read-only after clear"
162
+ assert_equal '', text.value
163
+
164
+ session.app.destroy
165
+ end
166
+ end
167
+
168
+ def test_read_only_accessor_round_trips
169
+ assert_tk_app("read_only/read_only= should read and drive the widget's own -state") do
170
+ require 'teek/ui'
171
+
172
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
173
+ session.run_async
174
+ text = session[:notes].text_content
175
+ refute text.read_only, "should start editable by default"
176
+
177
+ text.read_only = true
178
+ assert text.read_only
179
+
180
+ text.read_only = false
181
+ refute text.read_only
182
+
183
+ session.app.destroy
184
+ end
185
+ end
186
+
187
+ # -- Formats (Tk tags) ---------------------------------------------------
188
+
189
+ def test_format_apply_format_and_format_ranges
190
+ assert_tk_app("format should define display properties, apply_format should apply them to a range") do
191
+ require 'teek/ui'
192
+
193
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:code) }
194
+ session.run_async
195
+ text = session[:code].text_content
196
+ text.insert(:end, 'errorline')
197
+
198
+ text.format(:error, foreground: 'red')
199
+ text.apply_format(:error, '1.0', '1.5')
200
+
201
+ assert_equal 'red', session.app.command(session[:code].path, :tag, :cget, :error, '-foreground')
202
+ assert_equal ['1.0', '1.5'], text.format_ranges(:error)
203
+
204
+ session.app.destroy
205
+ end
206
+ end
207
+
208
+ def test_clear_format_removes_from_a_range_without_deleting_the_definition
209
+ assert_tk_app("clear_format should remove the format from a range but keep the definition applyable elsewhere") do
210
+ require 'teek/ui'
211
+
212
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:code) }
213
+ session.run_async
214
+ text = session[:code].text_content
215
+ text.insert(:end, 'abcdef')
216
+ text.format(:hl, foreground: 'red')
217
+ text.apply_format(:hl, '1.0', '1.3')
218
+
219
+ text.clear_format(:hl, '1.0', '1.3')
220
+
221
+ assert_equal [], text.format_ranges(:hl)
222
+
223
+ text.apply_format(:hl, '1.3', '1.6')
224
+ assert_equal ['1.3', '1.6'], text.format_ranges(:hl)
225
+
226
+ session.app.destroy
227
+ end
228
+ end
229
+
230
+ def test_delete_format_removes_the_definition_entirely
231
+ assert_tk_app("delete_format should remove the format definition and every range it was applied to") do
232
+ require 'teek/ui'
233
+
234
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:code) }
235
+ session.run_async
236
+ text = session[:code].text_content
237
+ text.insert(:end, 'abcdef')
238
+ text.format(:hl, foreground: 'red')
239
+ text.apply_format(:hl, '1.0', '1.3')
240
+
241
+ text.delete_format(:hl)
242
+
243
+ names = session.app.split_list(session.app.command(session[:code].path, :tag, :names))
244
+ refute_includes names, 'hl'
245
+
246
+ session.app.destroy
247
+ end
248
+ end
249
+
250
+ def test_tk_named_format_aliases_work_identically
251
+ assert_tk_app("tag_configure/tag_add/tag_remove/tag_delete/tag_ranges should alias the friendly names") do
252
+ require 'teek/ui'
253
+
254
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:code) }
255
+ session.run_async
256
+ text = session[:code].text_content
257
+ text.insert(:end, 'abcdef')
258
+
259
+ text.tag_configure(:hl, foreground: 'blue')
260
+ text.tag_add(:hl, '1.0', '1.3')
261
+ assert_equal ['1.0', '1.3'], text.tag_ranges(:hl)
262
+
263
+ text.tag_remove(:hl, '1.0', '1.3')
264
+ assert_equal [], text.tag_ranges(:hl)
265
+
266
+ text.tag_add(:hl, '1.0', '1.3')
267
+ text.tag_delete(:hl)
268
+ names = session.app.split_list(session.app.command(session[:code].path, :tag, :names))
269
+ refute_includes names, 'hl'
270
+
271
+ session.app.destroy
272
+ end
273
+ end
274
+
275
+ # -- Leak-safe format/tag event bindings ---------------------------------
276
+
277
+ def test_on_format_click_fires
278
+ assert_tk_app("on_format_click should fire when text carrying that format is clicked") do
279
+ require 'teek/ui'
280
+
281
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:log) }
282
+ session.run_async
283
+ session.app.update
284
+ text = session[:log].text_content
285
+ text.insert(:end, 'click me')
286
+ text.format(:link, foreground: 'blue')
287
+ text.apply_format(:link, '1.0', '1.8')
288
+ session.app.update
289
+
290
+ clicked = false
291
+ text.on_format_click(:link) { clicked = true }
292
+
293
+ # tag bind hit-tests by pixel position, unlike a widget-level bind -
294
+ # query the real bbox of a character inside the tagged range rather
295
+ # than guess a pixel offset that may land in padding/margin instead.
296
+ bbox = session.app.split_list(session.app.command(session[:log].path, :bbox, '1.2')).map(&:to_i)
297
+ x, y = bbox[0] + 2, bbox[1] + 2
298
+ session.app.tcl_eval("focus -force #{session[:log].path}")
299
+ session.app.update
300
+ # Tk's "current tag under the pointer" is motion-tracked, same as
301
+ # canvas's own "current item" - a bare synthetic Button-1 with no
302
+ # prior Motion to that position never updates it, so the click
303
+ # dispatches as if nothing were under the pointer.
304
+ session.app.tcl_eval("event generate #{session[:log].path} <Motion> -x #{x} -y #{y}")
305
+ session.app.update
306
+ session.app.tcl_eval("event generate #{session[:log].path} <Button-1> -x #{x} -y #{y}")
307
+
308
+ assert wait_until { clicked }, "on_format_click did not fire"
309
+
310
+ session.app.destroy
311
+ end
312
+ end
313
+
314
+ def test_on_format_click_is_leak_safe_via_the_registered_callback_count
315
+ assert_tk_app("removing a formatted range's callback should release it, not leak, proving this routes through app.command (not tcl_eval)") do
316
+ require 'teek/ui'
317
+
318
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:log) }
319
+ session.run_async
320
+ text = session[:log].text_content
321
+ text.insert(:end, 'click me')
322
+ text.format(:link, foreground: 'blue')
323
+ text.apply_format(:link, '1.0', '1.8')
324
+
325
+ baseline = session.debug_info[:tag_binds] || 0
326
+ text.on_format_click(:link) { }
327
+ assert_equal baseline + 1, session.debug_info[:tag_binds]
328
+
329
+ # Rebinding the SAME tag+event replaces (not stacks) the callback -
330
+ # Tk's own tag bind semantics - so the count should stay at +1,
331
+ # never climb, across repeated rebinds.
332
+ 3.times { text.on_format_click(:link) { } }
333
+ assert_equal baseline + 1, session.debug_info[:tag_binds],
334
+ "rebinding the same format+event should replace, not accumulate"
335
+
336
+ text.delete_format(:link)
337
+ session.app.update
338
+
339
+ assert_equal baseline, session.debug_info[:tag_binds] || 0,
340
+ "deleting the format should release its callback via the leak-safe tag_bind reconcile"
341
+
342
+ session.app.destroy
343
+ end
344
+ end
345
+
346
+ def test_on_format_with_a_custom_event_pattern
347
+ assert_tk_app("on_format should accept an arbitrary Tk event pattern, auto-wrapped in angle brackets") do
348
+ require 'teek/ui'
349
+
350
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:log) }
351
+ session.run_async
352
+ session.app.update
353
+ text = session[:log].text_content
354
+ text.insert(:end, 'ctrl click me')
355
+ text.format(:special, foreground: 'green')
356
+ text.apply_format(:special, '1.0', '1.13')
357
+ session.app.update
358
+
359
+ fired = false
360
+ # Double-/Triple-/Quadruple- click patterns can't be synthetically
361
+ # generated via `event generate` (Tk only synthesizes those from
362
+ # real, timed events) - a modifier+button combo like this one can be.
363
+ text.on_format('special', 'Control-Button-1') { fired = true }
364
+
365
+ bbox = session.app.split_list(session.app.command(session[:log].path, :bbox, '1.2')).map(&:to_i)
366
+ x, y = bbox[0] + 2, bbox[1] + 2
367
+ session.app.tcl_eval("focus -force #{session[:log].path}")
368
+ session.app.update
369
+ session.app.tcl_eval("event generate #{session[:log].path} <Motion> -x #{x} -y #{y}")
370
+ session.app.update
371
+ session.app.tcl_eval("event generate #{session[:log].path} <Control-Button-1> -x #{x} -y #{y}")
372
+
373
+ assert wait_until { fired }, "on_format with a custom event pattern did not fire"
374
+
375
+ session.app.destroy
376
+ end
377
+ end
378
+
379
+ # -- Markers -------------------------------------------------------------
380
+
381
+ def test_add_marker_remove_marker_and_markers
382
+ assert_tk_app("add_marker/remove_marker/markers should manage named floating positions") do
383
+ require 'teek/ui'
384
+
385
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
386
+ session.run_async
387
+ text = session[:notes].text_content
388
+ text.insert(:end, 'hello world')
389
+
390
+ text.add_marker(:checkpoint, at: '1.6')
391
+
392
+ assert_includes text.markers, 'checkpoint'
393
+ assert_equal '1.6', text.index(:checkpoint)
394
+
395
+ text.remove_marker(:checkpoint)
396
+ refute_includes text.markers, 'checkpoint'
397
+
398
+ session.app.destroy
399
+ end
400
+ end
401
+
402
+ def test_mark_gravity_reads_and_writes
403
+ assert_tk_app("mark_gravity should read the default and accept an explicit direction") do
404
+ require 'teek/ui'
405
+
406
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
407
+ session.run_async
408
+ text = session[:notes].text_content
409
+ text.insert(:end, 'hello world')
410
+ text.add_marker(:checkpoint, at: '1.3')
411
+
412
+ assert_equal 'right', text.mark_gravity(:checkpoint)
413
+
414
+ text.mark_gravity(:checkpoint, :left)
415
+ assert_equal 'left', text.mark_gravity(:checkpoint)
416
+
417
+ session.app.destroy
418
+ end
419
+ end
420
+
421
+ # -- Search ----------------------------------------------------------------
422
+
423
+ def test_search_finds_a_match
424
+ assert_tk_app("search should return the matching index, or nil if not found") do
425
+ require 'teek/ui'
426
+
427
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
428
+ session.run_async
429
+ text = session[:notes].text_content
430
+ text.insert(:end, 'the quick brown fox')
431
+
432
+ assert_equal '1.4', text.search('quick', from: '1.0')
433
+ assert_nil text.search('nonexistent', from: '1.0')
434
+
435
+ session.app.destroy
436
+ end
437
+ end
438
+
439
+ def test_search_backwards_and_regexp_and_nocase
440
+ assert_tk_app("search's backwards/regexp/nocase switches should all forward correctly") do
441
+ require 'teek/ui'
442
+
443
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
444
+ session.run_async
445
+ text = session[:notes].text_content
446
+ text.insert(:end, 'FOO bar foo baz')
447
+
448
+ assert_equal '1.8', text.search('foo', from: :end, to: '1.0', backwards: true)
449
+ assert_equal '1.0', text.search('F[A-Z]{2}', from: '1.0', regexp: true)
450
+ assert_equal '1.0', text.search('foo', from: '1.0', nocase: true)
451
+
452
+ session.app.destroy
453
+ end
454
+ end
455
+
456
+ # -- View / cursor / state --------------------------------------------------
457
+
458
+ def test_scroll_to_and_see_alias
459
+ assert_tk_app("scroll_to (and its see alias) should not raise against a real index") do
460
+ require 'teek/ui'
461
+
462
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes, height: 3) }
463
+ session.run_async
464
+ text = session[:notes].text_content
465
+ 20.times { |i| text.insert(:end, "line #{i}\n") }
466
+
467
+ text.scroll_to('10.0')
468
+ text.see('1.0')
469
+
470
+ session.app.destroy
471
+ end
472
+ end
473
+
474
+ def test_index_resolves_to_canonical_form
475
+ assert_tk_app("index should resolve any index expression to canonical line.char form") do
476
+ require 'teek/ui'
477
+
478
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
479
+ session.run_async
480
+ text = session[:notes].text_content
481
+ text.insert(:end, "line one\nline two\n")
482
+
483
+ assert_equal '2.0', text.index('2.0')
484
+ assert_equal '1.4', text.index('1.0 +4 chars')
485
+
486
+ session.app.destroy
487
+ end
488
+ end
489
+
490
+ def test_cursor_getter_and_setter
491
+ assert_tk_app("cursor should read and move the text insertion point") do
492
+ require 'teek/ui'
493
+
494
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
495
+ session.run_async
496
+ text = session[:notes].text_content
497
+ text.insert(:end, 'hello world')
498
+
499
+ text.cursor = '1.5'
500
+
501
+ assert_equal '1.5', text.cursor
502
+ assert_equal text.index(:cursor), text.cursor
503
+
504
+ session.app.destroy
505
+ end
506
+ end
507
+
508
+ # -- Embedded images ---------------------------------------------------------
509
+
510
+ def test_insert_image_embeds_a_ui_image
511
+ assert_tk_app("insert_image should embed a ui.image inline in the text flow") do
512
+ require 'teek/ui'
513
+ require 'tmpdir'
514
+
515
+ Dir.mktmpdir do |dir|
516
+ path = File.join(dir, 'test.png')
517
+ seed = Teek::Photo.new(app, width: 4, height: 4)
518
+ seed.put_block(([0, 0, 0, 255].pack('CCCC')) * 16, 4, 4)
519
+ app.tcl_eval("#{seed.name} write {#{path}} -format png")
520
+ seed.delete
521
+
522
+ icon = nil
523
+ session = Teek::UI.app(title: 'Text Content Test') { |ui|
524
+ icon = ui.image(path)
525
+ ui.text_area(:notes)
526
+ }
527
+ session.run_async
528
+ text = session[:notes].text_content
529
+ text.insert(:end, 'before ')
530
+
531
+ text.insert_image(:end, image: icon)
532
+ text.insert(:end, ' after')
533
+
534
+ dump = session.app.command(session[:notes].path, :dump, '-image', '1.0', 'end')
535
+ assert_match(/#{Regexp.escape(icon.name)}/, dump)
536
+
537
+ session.app.destroy
538
+ end
539
+ end
540
+ end
541
+
542
+ def test_insert_image_respects_read_only
543
+ assert_tk_app("insert_image should transparently lift and restore read-only state, same as the other mutators") do
544
+ require 'teek/ui'
545
+ require 'tmpdir'
546
+
547
+ Dir.mktmpdir do |dir|
548
+ path = File.join(dir, 'test.png')
549
+ seed = Teek::Photo.new(app, width: 4, height: 4)
550
+ seed.put_block(([0, 0, 0, 255].pack('CCCC')) * 16, 4, 4)
551
+ app.tcl_eval("#{seed.name} write {#{path}} -format png")
552
+ seed.delete
553
+
554
+ icon = nil
555
+ session = Teek::UI.app(title: 'Text Content Test') { |ui|
556
+ icon = ui.image(path)
557
+ ui.text_area(:notes, state: :disabled)
558
+ }
559
+ session.run_async
560
+ text = session[:notes].text_content
561
+
562
+ text.insert_image(:end, image: icon)
563
+
564
+ assert text.read_only, "should still be read-only after insert_image"
565
+
566
+ session.app.destroy
567
+ end
568
+ end
569
+ end
570
+
571
+ # -- Driving use cases ---------------------------------------------------
572
+
573
+ def test_use_case_syntax_highlighted_log_view
574
+ assert_tk_app("driving use case: a syntax-highlighted code/log view via format + apply_format") do
575
+ require 'teek/ui'
576
+
577
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:code) }
578
+ session.run_async
579
+ text = session[:code].text_content
580
+
581
+ text.insert(:end, "def hello\n puts 'hi'\nend\n")
582
+ text.format(:keyword, foreground: 'purple', font: ['Courier', 10, :bold])
583
+ text.apply_format(:keyword, '1.0', '1.3')
584
+ text.apply_format(:keyword, '3.0', '3.3')
585
+
586
+ assert_equal ['1.0', '1.3', '3.0', '3.3'], text.format_ranges(:keyword)
587
+
588
+ session.app.destroy
589
+ end
590
+ end
591
+
592
+ def test_use_case_search_and_replace
593
+ assert_tk_app("driving use case: search-and-replace via search + replace") do
594
+ require 'teek/ui'
595
+
596
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
597
+ session.run_async
598
+ text = session[:notes].text_content
599
+ text.insert(:end, 'the cat sat on the mat')
600
+
601
+ loop do
602
+ found = text.search('the', from: '1.0')
603
+ break unless found
604
+
605
+ text.replace(found, "#{found} +3 chars", 'THE')
606
+ # advance search past the just-replaced word next iteration
607
+ break if text.search('the', from: "#{found} +3 chars").nil?
608
+ end
609
+
610
+ assert_equal 'THE cat sat on THE mat', text.value
611
+
612
+ session.app.destroy
613
+ end
614
+ end
615
+
616
+ def test_use_case_appending_read_only_log_pane_with_auto_scroll
617
+ assert_tk_app("driving use case: an appending, auto-scrolling, read-only log pane") do
618
+ require 'teek/ui'
619
+
620
+ session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:log, height: 3, state: :disabled) }
621
+ session.run_async
622
+ text = session[:log].text_content
623
+
624
+ 5.times { |i| text.insert(:end, "log line #{i}\n") }
625
+ text.scroll_to(:end)
626
+
627
+ assert text.read_only, "the log pane should still be read-only after appending"
628
+ assert_match(/log line 4/, text.value)
629
+
630
+ session.app.destroy
631
+ end
632
+ end
633
+
634
+ def test_use_case_inline_images
635
+ assert_tk_app("driving use case: inline images via insert_image") do
636
+ require 'teek/ui'
637
+ require 'tmpdir'
638
+
639
+ Dir.mktmpdir do |dir|
640
+ path = File.join(dir, 'test.png')
641
+ seed = Teek::Photo.new(app, width: 4, height: 4)
642
+ seed.put_block(([0, 0, 0, 255].pack('CCCC')) * 16, 4, 4)
643
+ app.tcl_eval("#{seed.name} write {#{path}} -format png")
644
+ seed.delete
645
+
646
+ icon = nil
647
+ session = Teek::UI.app(title: 'Text Content Test') { |ui|
648
+ icon = ui.image(path)
649
+ ui.text_area(:notes)
650
+ }
651
+ session.run_async
652
+ text = session[:notes].text_content
653
+
654
+ text.insert(:end, 'Logo: ')
655
+ text.insert_image(:end, image: icon)
656
+
657
+ dump = session.app.command(session[:notes].path, :dump, '-image', '1.0', 'end')
658
+ assert_match(/#{Regexp.escape(icon.name)}/, dump)
659
+
660
+ session.app.destroy
661
+ end
662
+ end
663
+ end
664
+ end