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,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ class TestIncrementalRealize < Minitest::Test
7
+ include TeekTestHelper
8
+
9
+ def test_add_creates_and_shows_new_widgets_in_a_running_app
10
+ assert_tk_app("ui.add should create and show new widgets in an already-running app") do
11
+ require 'teek/ui'
12
+
13
+ session = Teek::UI.app(title: 'Incremental Realize Test') { |ui| ui.column(:list) }
14
+ session.run_async
15
+ session.app.update
16
+
17
+ session.add(:list) { |a| a.button(:item1, text: 'Item 1') }
18
+ session.app.update
19
+
20
+ assert session.app.winfo.exists?(session[:item1].path)
21
+ assert session.app.winfo.ismapped?(session[:item1].path)
22
+
23
+ session.app.destroy
24
+ end
25
+ end
26
+
27
+ def test_add_creates_multiple_new_siblings_declared_in_one_block
28
+ assert_tk_app("adding several new widgets inside ONE session.add block should not crash arrange_children on a not-yet-created sibling, and should arrange all of them correctly") do
29
+ require 'teek/ui'
30
+
31
+ session = Teek::UI.app(title: 'Incremental Realize Test') { |ui| ui.column(:list, gap: 10) }
32
+ session.run_async
33
+ session.app.update
34
+
35
+ handles = []
36
+ session.add(:list) { |a| 3.times { |i| handles << a.button(text: "Item #{i}") } }
37
+ session.app.update
38
+
39
+ children = session.document.find(:list).children
40
+ assert_equal 3, children.length
41
+ children.each { |child| assert session.app.winfo.exists?(child.realized.path) }
42
+
43
+ # a not-yet-created batch-mate shouldn't just avoid crashing - it
44
+ # should end up correctly positioned too, exactly like an ordinary
45
+ # sequence of separate session.add calls would (see
46
+ # test_add_respects_gap_relative_to_pre_existing_siblings above).
47
+ handles.each_cons(2) do |above, below|
48
+ above_bottom = session.app.winfo.rooty(above.path) + session.app.winfo.height(above.path)
49
+ below_top = session.app.winfo.rooty(below.path)
50
+ assert_equal 10, below_top - above_bottom
51
+ end
52
+
53
+ session.app.destroy
54
+ end
55
+ end
56
+
57
+ def test_add_respects_gap_relative_to_pre_existing_siblings
58
+ assert_tk_app("a widget added to a flow container should respect gap: relative to widgets already there") do
59
+ require 'teek/ui'
60
+
61
+ session = Teek::UI.app(title: 'Incremental Realize Test') do |ui|
62
+ ui.column(:list, gap: 20) { |c| c.button(:first, text: 'First') }
63
+ end
64
+ session.run_async
65
+ session.app.update
66
+
67
+ session.add(:list) { |a| a.button(:second, text: 'Second') }
68
+ session.app.update
69
+
70
+ first_bottom = session.app.winfo.rooty(session[:first].path) + session.app.winfo.height(session[:first].path)
71
+ second_top = session.app.winfo.rooty(session[:second].path)
72
+
73
+ assert_equal 20, second_top - first_bottom
74
+
75
+ session.app.destroy
76
+ end
77
+ end
78
+
79
+ def test_events_wired_inside_add_fire_correctly
80
+ assert_tk_app("on_click wired inside an add block should fire, using the same queue-then-wire-at-link path the initial build uses") do
81
+ require 'teek/ui'
82
+
83
+ session = Teek::UI.app(title: 'Incremental Realize Test') { |ui| ui.column(:list) }
84
+ session.run_async
85
+ session.app.update
86
+
87
+ clicked = false
88
+ session.add(:list) { |a| a.button(:item1, text: 'Item 1').on_click { clicked = true } }
89
+ session.app.update
90
+
91
+ session.app.tcl_eval("event generate #{session[:item1].path} <Button-1>")
92
+ session.app.update
93
+
94
+ assert clicked, "on_click wired inside ui.add did not fire"
95
+
96
+ session.app.destroy
97
+ end
98
+ end
99
+
100
+ def test_destroying_an_added_widget_reclaims_its_callback
101
+ assert_tk_app("destroying an incrementally-added widget should release its callback via teek's existing <Destroy> machinery") do
102
+ require 'teek/ui'
103
+
104
+ session = Teek::UI.app(title: 'Incremental Realize Test') { |ui| ui.column(:list) }
105
+ session.run_async
106
+ session.app.update
107
+ baseline = session.app.interp.callback_ids.length
108
+
109
+ session.add(:list) { |a| a.button(:item1, text: 'Item 1').on_click { } }
110
+ session.app.update
111
+ assert_equal baseline + 1, session.app.interp.callback_ids.length, "adding should register one callback"
112
+
113
+ session.app.destroy(session[:item1].path)
114
+ session.app.update
115
+
116
+ assert_equal baseline, session.app.interp.callback_ids.length, "destroying the added widget should release its callback"
117
+
118
+ session.app.destroy
119
+ end
120
+ end
121
+
122
+ def test_a_var_declared_inside_add_is_realized_and_works
123
+ assert_tk_app("ui.var declared inside an add block should get its backing Tcl variable, not stay forever unrealized") do
124
+ require 'teek/ui'
125
+
126
+ session = Teek::UI.app(title: 'Incremental Realize Test') { |ui| ui.column(:list) }
127
+ session.run_async
128
+ session.app.update
129
+
130
+ count = nil
131
+ session.add(:list) do |a|
132
+ count = a.var(5)
133
+ a.label(:count_label, bind: count)
134
+ end
135
+ session.app.update
136
+
137
+ assert_equal 5, count.value
138
+ assert_equal '5', session.app.command(session[:count_label].path, :cget, '-text')
139
+
140
+ count.value = 9
141
+ session.app.update
142
+ assert_equal '9', session.app.command(session[:count_label].path, :cget, '-text')
143
+
144
+ session.app.destroy
145
+ end
146
+ end
147
+
148
+ def test_an_image_declared_inside_add_is_realized_and_displays
149
+ assert_tk_app("ui.image declared inside an add block should actually load, not raise 'image does not exist' - the ChildWindow fresh-mount-per-open pattern depends on this") do
150
+ require 'teek/ui'
151
+ require 'tmpdir'
152
+
153
+ Dir.mktmpdir do |dir|
154
+ path = File.join(dir, 'test.png')
155
+ seed = Teek::Photo.new(app, width: 4, height: 4)
156
+ seed.put_block(([0, 0, 0, 255].pack('CCCC')) * 16, 4, 4)
157
+ app.tcl_eval("#{seed.name} write {#{path}} -format png")
158
+ seed.delete
159
+
160
+ session = Teek::UI.app(title: 'Incremental Realize Test') { |ui| ui.column(:list) }
161
+ session.run_async
162
+ session.app.update
163
+
164
+ icon = nil
165
+ session.add(:list) do |a|
166
+ icon = a.image(path)
167
+ a.label(:pic, image: icon)
168
+ end
169
+ session.app.update
170
+
171
+ assert_equal icon.name, session.app.command(session[:pic].path, :cget, '-image')
172
+ assert_equal 'photo', session.app.tcl_eval("image type #{icon.name}")
173
+
174
+ session.app.destroy
175
+ end
176
+ end
177
+ end
178
+
179
+ def test_add_on_an_unrealized_session_raises
180
+ assert_tk_app("ui.add before the session is realized should raise a clear error") do
181
+ require 'teek/ui'
182
+
183
+ session = Teek::UI.app(title: 'Incremental Realize Test') { |ui| ui.column(:list) }
184
+
185
+ assert_raises(Teek::UI::NotRealizedError) { session.add(:list) { |a| a.button(:item1) } }
186
+ end
187
+ end
188
+
189
+ def test_add_to_an_unknown_parent_name_raises
190
+ assert_tk_app("ui.add with a name that doesn't exist should raise a clear error") do
191
+ require 'teek/ui'
192
+
193
+ session = Teek::UI.app(title: 'Incremental Realize Test') { |ui| ui.column(:list) }
194
+ session.run_async
195
+ session.app.update
196
+
197
+ error = assert_raises(ArgumentError) { session.add(:nope) { |a| a.button(:item1) } }
198
+ assert_match(/nope/, error.message)
199
+
200
+ session.app.destroy
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+ require 'teek/ui/keysyms'
5
+
6
+ class TestKeysyms < Minitest::Test
7
+ def test_resolve_a_friendly_symbol_has_no_modifiers
8
+ modifiers, keysym = Teek::UI::Keysyms.resolve(:enter)
9
+
10
+ assert_equal [], modifiers
11
+ assert_equal 'Return', keysym
12
+ end
13
+
14
+ def test_resolve_covers_the_common_named_keys
15
+ {
16
+ escape: 'Escape', tab: 'Tab', space: 'space', backspace: 'BackSpace',
17
+ delete: 'Delete', up: 'Up', down: 'Down', left: 'Left', right: 'Right',
18
+ f1: 'F1', f12: 'F12',
19
+ }.each do |friendly, tk_keysym|
20
+ _, keysym = Teek::UI::Keysyms.resolve(friendly)
21
+ assert_equal tk_keysym, keysym, "#{friendly.inspect} should resolve to #{tk_keysym}"
22
+ end
23
+ end
24
+
25
+ def test_resolve_an_unknown_symbol_passes_through_as_the_literal_keysym
26
+ _, keysym = Teek::UI::Keysyms.resolve(:q)
27
+
28
+ assert_equal 'q', keysym
29
+ end
30
+
31
+ def test_resolve_a_single_modifier_string
32
+ modifiers, keysym = Teek::UI::Keysyms.resolve('Ctrl-s')
33
+
34
+ assert_equal ['Control'], modifiers
35
+ assert_equal 's', keysym
36
+ end
37
+
38
+ def test_resolve_a_multi_modifier_string
39
+ modifiers, keysym = Teek::UI::Keysyms.resolve('Ctrl-Shift-s')
40
+
41
+ assert_equal %w[Control Shift], modifiers
42
+ assert_equal 's', keysym
43
+ end
44
+
45
+ def test_resolve_a_modifier_string_with_a_friendly_base_key
46
+ modifiers, keysym = Teek::UI::Keysyms.resolve('Ctrl-Enter')
47
+
48
+ assert_equal ['Control'], modifiers
49
+ assert_equal 'Return', keysym
50
+ end
51
+
52
+ def test_patterns_for_the_common_case_is_a_single_pattern
53
+ patterns = Teek::UI::Keysyms.patterns_for(['Control'], 's')
54
+
55
+ assert_equal ['<Control-s>'], patterns
56
+ end
57
+
58
+ def test_patterns_for_no_modifiers
59
+ patterns = Teek::UI::Keysyms.patterns_for([], 'Return')
60
+
61
+ assert_equal ['<Return>'], patterns
62
+ end
63
+
64
+ def test_patterns_for_shift_tab_covers_the_iso_left_tab_gotcha
65
+ patterns = Teek::UI::Keysyms.patterns_for(['Shift'], 'Tab')
66
+
67
+ assert_includes patterns, '<Shift-Tab>'
68
+ assert_includes patterns, '<ISO_Left_Tab>'
69
+ end
70
+
71
+ def test_patterns_for_plain_tab_is_unaffected_by_the_shift_tab_special_case
72
+ patterns = Teek::UI::Keysyms.patterns_for([], 'Tab')
73
+
74
+ assert_equal ['<Tab>'], patterns
75
+ end
76
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ class TestLayout < Minitest::Test
7
+ include TeekTestHelper
8
+
9
+ def test_column_stacks_children_top_to_bottom_with_the_given_gap
10
+ assert_tk_app("column should stack children vertically with `gap` pixels between them") do
11
+ require 'teek/ui'
12
+
13
+ session = Teek::UI.app(title: 'Layout Test') do |ui|
14
+ ui.column(:c, gap: 20) do |c|
15
+ c.button(:a, text: 'A')
16
+ c.button(:b, text: 'B')
17
+ end
18
+ end
19
+ session.run_async
20
+ session.app.update
21
+
22
+ a_bottom = session.app.winfo.rooty(session[:a].path) + session.app.winfo.height(session[:a].path)
23
+ b_top = session.app.winfo.rooty(session[:b].path)
24
+
25
+ assert_equal 20, b_top - a_bottom
26
+
27
+ session.app.destroy
28
+ end
29
+ end
30
+
31
+ def test_row_stacks_children_left_to_right_with_the_given_gap
32
+ assert_tk_app("row should stack children horizontally with `gap` pixels between them") do
33
+ require 'teek/ui'
34
+
35
+ session = Teek::UI.app(title: 'Layout Test') do |ui|
36
+ ui.row(:r, gap: 15) do |r|
37
+ r.button(:a, text: 'A')
38
+ r.button(:b, text: 'B')
39
+ end
40
+ end
41
+ session.run_async
42
+ session.app.update
43
+
44
+ a_right = session.app.winfo.rootx(session[:a].path) + session.app.winfo.width(session[:a].path)
45
+ b_left = session.app.winfo.rootx(session[:b].path)
46
+
47
+ assert_equal 15, b_left - a_right
48
+
49
+ session.app.destroy
50
+ end
51
+ end
52
+
53
+ def test_align_stretch_fills_the_cross_axis_in_a_column
54
+ assert_tk_app("align: :stretch should make a narrower child match the widest sibling's width") do
55
+ require 'teek/ui'
56
+
57
+ session = Teek::UI.app(title: 'Layout Test') do |ui|
58
+ ui.column(:c, align: :stretch) do |c|
59
+ c.button(:narrow, text: 'Go')
60
+ c.button(:wide, text: 'A Much Longer Button Label')
61
+ end
62
+ end
63
+ session.run_async
64
+ session.app.update
65
+
66
+ narrow_width = session.app.winfo.width(session[:narrow].path)
67
+ wide_width = session.app.winfo.width(session[:wide].path)
68
+
69
+ assert_equal wide_width, narrow_width
70
+
71
+ session.app.destroy
72
+ end
73
+ end
74
+
75
+ def test_align_start_does_not_stretch_children
76
+ assert_tk_app("without align: :stretch, a narrower child should keep its own natural width") do
77
+ require 'teek/ui'
78
+
79
+ session = Teek::UI.app(title: 'Layout Test') do |ui|
80
+ ui.column(:c) do |c|
81
+ c.button(:narrow, text: 'Go')
82
+ c.button(:wide, text: 'A Much Longer Button Label')
83
+ end
84
+ end
85
+ session.run_async
86
+ session.app.update
87
+
88
+ narrow_width = session.app.winfo.width(session[:narrow].path)
89
+ wide_width = session.app.winfo.width(session[:wide].path)
90
+
91
+ refute_equal wide_width, narrow_width
92
+
93
+ session.app.destroy
94
+ end
95
+ end
96
+
97
+ def test_pad_adds_a_margin_around_the_whole_stack
98
+ assert_tk_app("pad: should add space before the first child and after the last") do
99
+ require 'teek/ui'
100
+
101
+ session = Teek::UI.app(title: 'Layout Test') do |ui|
102
+ ui.column(:c, pad: 10) { |c| c.button(:only, text: 'Only') }
103
+ end
104
+ session.run_async
105
+ session.app.update
106
+
107
+ col_top = session.app.winfo.rooty(session[:c].path)
108
+ only_top = session.app.winfo.rooty(session[:only].path)
109
+
110
+ assert_equal 10, only_top - col_top
111
+
112
+ session.app.destroy
113
+ end
114
+ end
115
+
116
+ def test_spacer_with_grow_pushes_trailing_children_to_the_far_edge
117
+ assert_tk_app("a spacer should absorb leftover space, pushing what follows it to the bottom - the spring-row replacement") do
118
+ require 'teek/ui'
119
+
120
+ session = Teek::UI.app(title: 'Layout Test') do |ui|
121
+ ui.column(:c, height: 300) do |c|
122
+ c.button(:top, text: 'Top')
123
+ c.spacer
124
+ c.button(:bottom, text: 'Bottom')
125
+ end
126
+ end
127
+ session.run_async
128
+ # disable geometry propagation so the column keeps its explicit height
129
+ # instead of shrinking to fit its (currently tiny) content - this is
130
+ # test setup via the escape hatch, not part of the ported app code.
131
+ session.app.tcl_eval("pack propagate #{session[:c].path} 0")
132
+ session.app.update
133
+
134
+ col_bottom = session.app.winfo.rooty(session[:c].path) + session.app.winfo.height(session[:c].path)
135
+ bottom_button_bottom = session.app.winfo.rooty(session[:bottom].path) + session.app.winfo.height(session[:bottom].path)
136
+
137
+ # the bottom button should end close to the column's own bottom edge,
138
+ # not sitting right under the top button
139
+ assert_in_delta col_bottom, bottom_button_bottom, 2
140
+
141
+ top_bottom = session.app.winfo.rooty(session[:top].path) + session.app.winfo.height(session[:top].path)
142
+ bottom_top = session.app.winfo.rooty(session[:bottom].path)
143
+ assert_operator (bottom_top - top_bottom), :>, 100, "the spacer should have absorbed most of the leftover height"
144
+
145
+ session.app.destroy
146
+ end
147
+ end
148
+
149
+ def test_goldberg_control_panel_prototype_realizes_with_only_the_layout_dsl
150
+ assert_tk_app("goldberg's control panel column should realize correctly using only column/gap/align/spacer") do
151
+ require 'teek/ui'
152
+
153
+ speed = nil
154
+ session = Teek::UI.app(title: 'Layout Test') do |ui|
155
+ speed = ui.var(5)
156
+ ui.column(:ctrl, gap: 4, align: :stretch, pad: 5) do |c|
157
+ c.button(:start, text: 'Start')
158
+ c.checkbox(:pause, text: 'Pause')
159
+ c.button(:step, text: 'Single Step')
160
+ c.button(:bstep, text: 'Big Step')
161
+ c.button(:reset, text: 'Reset')
162
+ c.checkbox(:details, text: 'Details')
163
+ c.spacer
164
+ c.text_box(:msg_entry)
165
+ c.slider(:speed_scale, from: 1, to: 10, bind: speed)
166
+ c.button(:about, text: 'About')
167
+ end
168
+ end
169
+ session.run_async
170
+ session.app.update
171
+
172
+ %i[start pause step bstep reset details msg_entry speed_scale about].each do |name|
173
+ path = session[name].path
174
+ assert session.app.winfo.exists?(path), "#{name} should exist"
175
+ assert session.app.winfo.ismapped?(path), "#{name} should be mapped/visible"
176
+ end
177
+
178
+ # every widget stretches to the column's own width (align: :stretch)
179
+ widths = %i[start pause about].map { |n| session.app.winfo.width(session[n].path) }
180
+ assert_equal 1, widths.uniq.length
181
+
182
+ session.app.destroy
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+ require 'teek/ui/document'
5
+ require 'teek/ui/realizer'
6
+ require 'teek/ui/event_binding'
7
+
8
+ # Headless coverage of the lazy: true skip/on-demand-realize mechanism
9
+ # itself, isolated from Handle (which stays a thin, private wrapper
10
+ # around this - see test_screens_realtk.rb/test_modal_stack_realtk.rb
11
+ # for the real-Tk, public-facing behavior through ui.screens/ui.modal)
12
+ # and from real Tk entirely - built by hand against Document/Node/
13
+ # Realizer's own public API, using the shared FakeApp (support/fake_app.rb)
14
+ # to record what Realizer would have created, the same way test_screens.rb
15
+ # does for Screens' own reveal/conceal calls.
16
+ class TestLazyRealize < Minitest::Test
17
+ def build_host_and_picker(picker_children: [])
18
+ document = Teek::UI::Document.new
19
+ host = document.create(type: :panel, name: :host)
20
+ document.root.add_child(host)
21
+ picker = document.create(type: :panel, name: :picker)
22
+ picker.lazy = true
23
+ host.add_child(picker)
24
+ picker_children.each { |child| picker.add_child(child) }
25
+ [document, host, picker]
26
+ end
27
+
28
+ def test_a_lazy_node_is_not_created_by_the_initial_realize
29
+ document, _host, picker = build_host_and_picker
30
+ app = FakeApp.new
31
+
32
+ Teek::UI::Realizer.new(app, document).realize
33
+
34
+ assert_nil picker.realized
35
+ refute app.calls.any? { |(args, _)| args.include?('.host.picker') }
36
+ end
37
+
38
+ def test_a_lazy_nodes_own_children_are_also_skipped_by_the_initial_realize
39
+ document, _host, picker = build_host_and_picker
40
+ load_node = document.create(type: :button, name: :load, opts: { text: 'Load' })
41
+ picker.add_child(load_node)
42
+ app = FakeApp.new
43
+
44
+ Teek::UI::Realizer.new(app, document).realize
45
+
46
+ assert_nil load_node.realized
47
+ refute app.calls.any? { |(args, _)| args.include?('.host.picker.load') }
48
+ end
49
+
50
+ def test_a_non_lazy_sibling_still_realizes_normally_next_to_a_lazy_one
51
+ document, host, _picker = build_host_and_picker
52
+ go = document.create(type: :button, name: :go, opts: { text: 'Go' })
53
+ host.add_child(go)
54
+ app = FakeApp.new
55
+
56
+ Teek::UI::Realizer.new(app, document).realize
57
+
58
+ refute_nil go.realized
59
+ assert_equal '.host.go', go.realized.path
60
+ assert app.calls.any? { |(args, _)| args.include?('.host.go') }
61
+ end
62
+
63
+ def test_arrange_children_does_not_choke_on_a_still_unrealized_lazy_sibling
64
+ document, host, _picker = build_host_and_picker
65
+ go = document.create(type: :button, name: :go, opts: { text: 'Go' })
66
+ host.add_child(go)
67
+ app = FakeApp.new
68
+
69
+ # link() (which arrange_children is part of) walks the WHOLE tree
70
+ # after create finishes - reaching this line without a NoMethodError
71
+ # on a nil .realized for the still-lazy :picker sibling IS the
72
+ # assertion (see Realizer#arrange_children's own lazy-aware filter).
73
+ Teek::UI::Realizer.new(app, document).realize
74
+
75
+ assert app.calls.any? { |(args, kwargs)| args == [:pack, '.host.go'] || (args.first == :pack && kwargs.empty?) }
76
+ end
77
+
78
+ def test_realize_subtree_realizes_a_lazy_node_and_its_own_children_on_demand
79
+ document, host, picker = build_host_and_picker
80
+ load_node = document.create(type: :button, name: :load, opts: { text: 'Load' })
81
+ picker.add_child(load_node)
82
+ app = FakeApp.new
83
+ Teek::UI::Realizer.new(app, document).realize
84
+
85
+ Teek::UI::Realizer.new(app, document).realize_subtree(picker, host)
86
+
87
+ refute_nil picker.realized
88
+ assert_equal '.host.picker', picker.realized.path
89
+ refute_nil load_node.realized
90
+ assert_equal '.host.picker.load', load_node.realized.path
91
+ assert app.calls.any? { |(args, _)| args.include?('.host.picker') }
92
+ assert app.calls.any? { |(args, _)| args.include?('.host.picker.load') }
93
+ end
94
+
95
+ def test_realize_subtree_wires_events_queued_before_realize
96
+ document, host, picker = build_host_and_picker
97
+ fired = false
98
+ picker.events << Teek::UI::EventBinding.new(event: '<Button-1>', handler: -> { fired = true }, subs: [])
99
+ app = FakeApp.new
100
+ Teek::UI::Realizer.new(app, document).realize
101
+
102
+ Teek::UI::Realizer.new(app, document).realize_subtree(picker, host)
103
+
104
+ bound = app.binds.find { |b| b[:path] == '.host.picker' && b[:event] == '<Button-1>' }
105
+ refute_nil bound, "the event queued on the lazy node before realize should get wired once it's realized"
106
+ bound[:block].call
107
+ assert fired
108
+ end
109
+
110
+ def test_a_later_arrange_children_call_picks_up_a_now_realized_former_lazy_sibling
111
+ document, host, picker = build_host_and_picker
112
+ app = FakeApp.new
113
+ Teek::UI::Realizer.new(app, document).realize
114
+ Teek::UI::Realizer.new(app, document).realize_subtree(picker, host)
115
+ app.calls.clear
116
+
117
+ # mirrors Session#add adding a further sibling to :host later - its
118
+ # own realize_subtree re-arranges ALL of host's children, which by
119
+ # now includes the no-longer-lazy :picker (see Realizer#arrange_children's
120
+ # "lazy? && !realized" - not bare "lazy?" - filter).
121
+ another = document.create(type: :button, name: :another, opts: { text: 'Another' })
122
+ host.add_child(another)
123
+ Teek::UI::Realizer.new(app, document).realize_subtree(another, host)
124
+
125
+ assert app.calls.any? { |(args, _)| args.include?('.host.picker') },
126
+ "the now-realized :picker should be re-arranged alongside the new sibling, not skipped forever"
127
+ end
128
+ end