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,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # ui.scrollable wraps arbitrary content in a scrolled frame - a canvas +
7
+ # embedded viewport, since arbitrary widgets have no Tk scrolling protocol
8
+ # of their own to hook a scrollbar into. A bare list/text_area/table/
9
+ # tree/canvas auto-attaches a scrollbar wherever it's declared instead,
10
+ # with no ui.scrollable wrapper needed - see test_native_scrollable.rb.
11
+ # See Realizer#create_scrollable.
12
+ class TestScrollable < Minitest::Test
13
+ include TeekTestHelper
14
+
15
+ def test_wraps_content_in_a_canvas_and_embedded_viewport
16
+ assert_tk_app("a scrollable panel of arbitrary widgets should be wrapped in a canvas + embedded frame") do
17
+ require 'teek/ui'
18
+
19
+ session = Teek::UI.app(title: 'Scrollable Test') do |ui|
20
+ ui.scrollable(:region) do |s|
21
+ s.column(:rows) { |c| 30.times { |i| c.button(text: "Row #{i}") } }
22
+ end
23
+ end
24
+ session.run_async
25
+ session.app.update
26
+ session.app.update_idletasks
27
+
28
+ region_path = session[:region].path
29
+ canvas_path = "#{region_path}.canvas"
30
+ viewport_path = "#{canvas_path}.viewport"
31
+ rows_path = session[:rows].path
32
+
33
+ assert_equal '1', session.app.tcl_eval("winfo exists #{canvas_path}")
34
+ assert_equal '1', session.app.tcl_eval("winfo exists #{viewport_path}")
35
+ assert_equal viewport_path, rows_path.sub(/\.\w+\z/, ''), "the column should live inside the viewport, not directly under the scrollable"
36
+ assert_equal '1', session.app.tcl_eval("winfo exists #{region_path}.vsb")
37
+
38
+ scrollregion = session.app.tcl_eval("#{canvas_path} cget -scrollregion")
39
+ refute_empty scrollregion, "the scrollregion should track the viewport's content size"
40
+ height = scrollregion.split.last.to_i
41
+ assert_operator height, :>, 200, "30 stacked buttons should produce a tall scrollregion"
42
+
43
+ session.app.destroy
44
+ end
45
+ end
46
+
47
+ def test_y_defaults_to_true_and_x_to_false
48
+ assert_tk_app("a scrollable should default to a vertical scrollbar only") do
49
+ require 'teek/ui'
50
+
51
+ session = Teek::UI.app(title: 'Scrollable Test') do |ui|
52
+ ui.scrollable(:region) { |s| s.column { |c| c.button(text: 'Row') } }
53
+ end
54
+ session.run_async
55
+ session.app.update
56
+
57
+ region_path = session[:region].path
58
+ assert_equal '1', session.app.tcl_eval("winfo exists #{region_path}.vsb")
59
+ assert_equal '0', session.app.tcl_eval("winfo exists #{region_path}.hsb")
60
+
61
+ session.app.destroy
62
+ end
63
+ end
64
+
65
+ def test_y_false_omits_the_vertical_scrollbar
66
+ assert_tk_app("y: false should leave no scrollbar at all if x: isn't given either") do
67
+ require 'teek/ui'
68
+
69
+ session = Teek::UI.app(title: 'Scrollable Test') do |ui|
70
+ ui.scrollable(:region, y: false) { |s| s.column { |c| c.button(text: 'Row') } }
71
+ end
72
+ session.run_async
73
+ session.app.update
74
+
75
+ region_path = session[:region].path
76
+ assert_equal '0', session.app.tcl_eval("winfo exists #{region_path}.vsb")
77
+ assert_equal '0', session.app.tcl_eval("winfo exists #{region_path}.hsb")
78
+
79
+ session.app.destroy
80
+ end
81
+ end
82
+
83
+ def test_x_true_also_wires_a_horizontal_scrollbar
84
+ assert_tk_app("x: true should additionally wire a horizontal scrollbar") do
85
+ require 'teek/ui'
86
+
87
+ session = Teek::UI.app(title: 'Scrollable Test') do |ui|
88
+ ui.scrollable(:region, x: true) { |s| s.column { |c| c.button(text: 'Row') } }
89
+ end
90
+ session.run_async
91
+ session.app.update
92
+
93
+ region_path = session[:region].path
94
+ assert_equal '1', session.app.tcl_eval("winfo exists #{region_path}.hsb")
95
+
96
+ session.app.destroy
97
+ end
98
+ end
99
+
100
+ def test_syncs_the_viewport_width_to_the_canvas_by_default
101
+ assert_tk_app("without x: scrolling, resizing the canvas should resize the embedded viewport to match") do
102
+ require 'teek/ui'
103
+
104
+ session = Teek::UI.app(title: 'Scrollable Test') do |ui|
105
+ ui.window(:win, geometry: '400x300') do |w|
106
+ w.scrollable(:region) { |s| s.column(:rows) { |c| c.button(text: 'Row') } }
107
+ end
108
+ end
109
+ session.run_async
110
+ session[:win].show
111
+ session.app.update
112
+ session.app.update_idletasks
113
+
114
+ canvas_path = "#{session[:region].path}.canvas"
115
+ viewport_path = "#{canvas_path}.viewport"
116
+
117
+ canvas_width = session.app.tcl_eval("winfo width #{canvas_path}")
118
+ viewport_width = session.app.tcl_eval("winfo width #{viewport_path}")
119
+ assert_equal canvas_width, viewport_width
120
+
121
+ session.app.destroy
122
+ end
123
+ end
124
+
125
+ def test_with_x_true_does_not_force_the_viewport_width_to_match_the_canvas
126
+ assert_tk_app("x: true should leave the viewport free to be wider than the canvas, for horizontal scrolling") do
127
+ require 'teek/ui'
128
+
129
+ session = Teek::UI.app(title: 'Scrollable Test') do |ui|
130
+ ui.scrollable(:region, x: true) { |s| s.column(:rows) { |c| c.button(text: 'Row') } }
131
+ end
132
+ session.run_async
133
+ session.app.update
134
+
135
+ canvas_path = "#{session[:region].path}.canvas"
136
+ assert_equal '', session.app.tcl_eval("bind #{canvas_path} <Configure>"),
137
+ "no width-sync binding should be wired when x: true"
138
+
139
+ session.app.destroy
140
+ end
141
+ end
142
+
143
+ def test_participates_in_normal_layout_like_any_other_container
144
+ assert_tk_app("a scrollable nested in a column with grow: true should realize and map without error") do
145
+ require 'teek/ui'
146
+
147
+ session = Teek::UI.app(title: 'Scrollable Test') do |ui|
148
+ ui.column do |c|
149
+ c.label(text: 'Header')
150
+ c.scrollable(:region, grow: true) { |s| s.checkbox(text: 'A checkbox') }
151
+ end
152
+ end
153
+ session.run_async
154
+ session.app.update
155
+
156
+ assert session.app.winfo.ismapped?(session[:region].path)
157
+
158
+ session.app.destroy
159
+ end
160
+ end
161
+
162
+ def test_no_widget_in_the_subtree_has_conflicting_geometry_managers
163
+ assert_tk_app("grid (region) and pack (viewport content) must never collide on the same master") do
164
+ require 'teek/ui'
165
+
166
+ session = Teek::UI.app(title: 'Scrollable Test') do |ui|
167
+ ui.scrollable(:region_a) { |s| s.column { |c| c.button(text: 'A') } }
168
+ ui.scrollable(:region_b, x: true) { |s| s.row { |r| r.button(text: 'B') } }
169
+ end
170
+ session.run_async
171
+ session.app.update
172
+
173
+ check = lambda do |path|
174
+ children = session.app.split_list(session.app.tcl_eval("winfo children #{path}"))
175
+ managers = children.map { |child| session.app.tcl_eval("winfo manager #{child}") }.reject(&:empty?).uniq
176
+ assert_operator managers.length, :<=, 1,
177
+ "#{path} has children managed by more than one geometry manager: #{managers.inspect}"
178
+ children.each { |child| check.call(child) }
179
+ end
180
+ check.call('.')
181
+
182
+ session.app.destroy
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # create_scrollable wires the scrollbar and scrollregion sync (see
7
+ # test_scrollable.rb), but the frame case's canvas + embedded viewport has
8
+ # no wheel handling of its own - a bare canvas doesn't respond to
9
+ # <MouseWheel>, and neither do arbitrary widgets nested inside it. These
10
+ # confirm the fix: a shared bindtag (see Realizer#wire_wheel_scroll)
11
+ # applied to the canvas, the viewport, and everything already inside it,
12
+ # so wheeling over any of them scrolls the same region - and that a
13
+ # widget with no such tag is left alone.
14
+ #
15
+ # yview/xview reading is inlined as a local lambda in every block (not a
16
+ # helper method) - assert_tk_app re-evaluates each block's own source text
17
+ # in a separate worker process, which has no access to methods defined on
18
+ # this class.
19
+ class TestScrollableWheel < Minitest::Test
20
+ include TeekTestHelper
21
+
22
+ def test_wheel_over_the_canvas_scrolls_the_frame_case
23
+ assert_tk_app("MouseWheel over the canvas itself should scroll it") do
24
+ require 'teek/ui'
25
+
26
+ session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
27
+ ui.scrollable(:region) { |s| s.column { |c| 30.times { |i| c.button(text: "Row #{i}") } } }
28
+ end
29
+ session.run_async
30
+ session.app.update
31
+ session.app.update_idletasks
32
+
33
+ canvas_path = "#{session[:region].path}.canvas"
34
+ yview_first = -> { session.app.tcl_eval("#{canvas_path} yview").split.first.to_f }
35
+ assert_equal 0.0, yview_first.call
36
+
37
+ session.app.tcl_eval("event generate #{canvas_path} <MouseWheel> -delta -120")
38
+ session.app.update
39
+
40
+ assert_operator yview_first.call, :>, 0.0
41
+
42
+ session.app.destroy
43
+ end
44
+ end
45
+
46
+ def test_wheel_over_a_nested_widget_also_scrolls_the_region
47
+ assert_tk_app("MouseWheel over a widget nested deep inside the viewport should still scroll the canvas") do
48
+ require 'teek/ui'
49
+
50
+ session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
51
+ ui.scrollable(:region) do |s|
52
+ s.column { |c| 30.times { |i| c.button("row_#{i}".to_sym, text: "Row #{i}") } }
53
+ end
54
+ end
55
+ session.run_async
56
+ session.app.update
57
+ session.app.update_idletasks
58
+
59
+ canvas_path = "#{session[:region].path}.canvas"
60
+ nested_button_path = session[:row_5].path
61
+ yview_first = -> { session.app.tcl_eval("#{canvas_path} yview").split.first.to_f }
62
+
63
+ session.app.tcl_eval("event generate #{nested_button_path} <MouseWheel> -delta -120")
64
+ session.app.update
65
+
66
+ assert_operator yview_first.call, :>, 0.0
67
+
68
+ session.app.destroy
69
+ end
70
+ end
71
+
72
+ def test_wheel_over_an_unrelated_widget_does_not_scroll_the_region
73
+ assert_tk_app("MouseWheel over a widget with no bindtag from the scrollable region should leave it alone") do
74
+ require 'teek/ui'
75
+
76
+ session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
77
+ ui.scrollable(:region) { |s| s.column { |c| 30.times { |i| c.button(text: "Row #{i}") } } }
78
+ ui.button(:elsewhere, text: 'Elsewhere')
79
+ end
80
+ session.run_async
81
+ session.app.update
82
+ session.app.update_idletasks
83
+
84
+ canvas_path = "#{session[:region].path}.canvas"
85
+ yview_first = -> { session.app.tcl_eval("#{canvas_path} yview").split.first.to_f }
86
+
87
+ session.app.tcl_eval("event generate #{session[:elsewhere].path} <MouseWheel> -delta -120")
88
+ session.app.update
89
+
90
+ assert_equal 0.0, yview_first.call
91
+
92
+ session.app.destroy
93
+ end
94
+ end
95
+
96
+ def test_button_4_and_5_also_scroll_the_frame_case
97
+ assert_tk_app("the X11 Button-4/Button-5 wheel fallback should scroll the region too") do
98
+ require 'teek/ui'
99
+
100
+ session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
101
+ ui.scrollable(:region) { |s| s.column { |c| 30.times { |i| c.button(text: "Row #{i}") } } }
102
+ end
103
+ session.run_async
104
+ session.app.update
105
+ session.app.update_idletasks
106
+
107
+ canvas_path = "#{session[:region].path}.canvas"
108
+ yview_first = -> { session.app.tcl_eval("#{canvas_path} yview").split.first.to_f }
109
+
110
+ session.app.tcl_eval("event generate #{canvas_path} <Button-5>")
111
+ session.app.update
112
+ after_down = yview_first.call
113
+ assert_operator after_down, :>, 0.0
114
+
115
+ session.app.tcl_eval("event generate #{canvas_path} <Button-4>")
116
+ session.app.update
117
+ assert_operator yview_first.call, :<, after_down
118
+
119
+ session.app.destroy
120
+ end
121
+ end
122
+
123
+ def test_shift_mouse_wheel_scrolls_horizontally_when_x_is_enabled
124
+ assert_tk_app("Shift-MouseWheel should drive xview when x: true") do
125
+ require 'teek/ui'
126
+
127
+ session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
128
+ ui.scrollable(:region, x: true) { |s| s.row { |r| 30.times { |i| r.button(text: "Col #{i}") } } }
129
+ end
130
+ session.run_async
131
+ session.app.update
132
+ session.app.update_idletasks
133
+
134
+ canvas_path = "#{session[:region].path}.canvas"
135
+ xview_first = -> { session.app.tcl_eval("#{canvas_path} xview").split.first.to_f }
136
+ assert_equal 0.0, xview_first.call
137
+
138
+ session.app.tcl_eval("event generate #{canvas_path} <Shift-MouseWheel> -delta -120")
139
+ session.app.update
140
+
141
+ assert_operator xview_first.call, :>, 0.0
142
+
143
+ session.app.destroy
144
+ end
145
+ end
146
+
147
+ def test_native_scrollable_widget_wheel_scrolls_through_the_dsl
148
+ assert_tk_app("MouseWheel over a native scrollable widget (Tk's own class binding) should work through the DSL") do
149
+ require 'teek/ui'
150
+
151
+ session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
152
+ ui.scrollable(:region) { |s| s.list(:items, height: 5) }
153
+ end
154
+ session.run_async
155
+ session.app.update
156
+
157
+ list_path = session[:items].path
158
+ session.app.command(list_path, :insert, :end, *(1..60).map { |i| "Item #{i}" })
159
+ session.app.update_idletasks
160
+
161
+ yview_first = -> { session.app.tcl_eval("#{list_path} yview").split.first.to_f }
162
+ assert_equal 0.0, yview_first.call
163
+
164
+ session.app.tcl_eval("event generate #{list_path} <MouseWheel> -delta -120")
165
+ session.app.update
166
+
167
+ assert_operator yview_first.call, :>, 0.0
168
+
169
+ session.app.destroy
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # wire_scrollbars wires -yscrollcommand/-xscrollcommand through
7
+ # Realizer#auto_hide_scrollbar instead of a plain literal string, so a
8
+ # scrollbar is grid-removed (not just left dangling and useless) whenever
9
+ # its content fully fits, and re-shown the moment it doesn't - real
10
+ # "overflow: auto" rather than a bar that's always there whether it's
11
+ # needed or not. Shared by both the native-widget auto-attach path and
12
+ # ui.scrollable's own frame case, so a couple of each is enough to prove
13
+ # the one underlying mechanism.
14
+ #
15
+ # The initial hide/show state settles via an #after_idle check (see
16
+ # Realizer#auto_hide_scrollbar's own comment - Tk only re-invokes
17
+ # -yscrollcommand on an actual fraction *change*, so an empty widget
18
+ # gaining a few rows never fires it on its own). A single
19
+ # +update_idletasks+ isn't reliably enough to flush that under Xvfb - this
20
+ # project has hit that class of timing gap before (see
21
+ # TeekTestHelper#wait_until's other callers) - so the mapped/unmapped
22
+ # assertions below poll for it instead of asserting immediately after one
23
+ # idle flush.
24
+ class TestScrollbarAutoHide < Minitest::Test
25
+ include TeekTestHelper
26
+
27
+ def test_scrollbar_is_hidden_when_content_fits
28
+ assert_tk_app("a scrollbar should be grid-removed when its content doesn't overflow the visible area") do
29
+ require 'teek/ui'
30
+
31
+ session = Teek::UI.app(title: 'Auto-Hide Test') { |ui| ui.list(:items, height: 10) }
32
+ session.run_async
33
+ session.app.command(session[:items].path, :insert, :end, 'one', 'two', 'three')
34
+
35
+ wrapper = session[:items].path.sub(/\.widget\z/, '')
36
+ assert_equal '1', session.app.tcl_eval("winfo exists #{wrapper}.vsb"), "the scrollbar widget itself still exists"
37
+ assert wait_until { !session.app.winfo.ismapped?("#{wrapper}.vsb") },
38
+ "should be grid-removed once idle processing settles, since everything fits"
39
+
40
+ session.app.destroy
41
+ end
42
+ end
43
+
44
+ def test_scrollbar_is_shown_when_content_overflows
45
+ assert_tk_app("a scrollbar should be mapped once its content overflows the visible area") do
46
+ require 'teek/ui'
47
+
48
+ session = Teek::UI.app(title: 'Auto-Hide Test') { |ui| ui.list(:items, height: 5) }
49
+ session.run_async
50
+ session.app.command(session[:items].path, :insert, :end, *(1..50).map { |i| "Item #{i}" })
51
+
52
+ wrapper = session[:items].path.sub(/\.widget\z/, '')
53
+ assert wait_until { session.app.winfo.ismapped?("#{wrapper}.vsb") }
54
+
55
+ session.app.destroy
56
+ end
57
+ end
58
+
59
+ def test_scrollbar_appears_once_content_grows_past_fitting
60
+ assert_tk_app("a scrollbar hidden at realize should reappear once enough content is added later") do
61
+ require 'teek/ui'
62
+
63
+ session = Teek::UI.app(title: 'Auto-Hide Test') { |ui| ui.list(:items, height: 5) }
64
+ session.run_async
65
+ session.app.command(session[:items].path, :insert, :end, 'one', 'two')
66
+
67
+ wrapper = session[:items].path.sub(/\.widget\z/, '')
68
+ assert wait_until { !session.app.winfo.ismapped?("#{wrapper}.vsb") }, "should start hidden with only 2 items"
69
+
70
+ session.app.command(session[:items].path, :insert, :end, *(1..50).map { |i| "More #{i}" })
71
+
72
+ assert wait_until { session.app.winfo.ismapped?("#{wrapper}.vsb") }, "should reappear once content overflows"
73
+
74
+ session.app.destroy
75
+ end
76
+ end
77
+
78
+ def test_scrollbar_disappears_again_once_content_shrinks_back_to_fitting
79
+ assert_tk_app("a visible scrollbar should hide again once enough content is removed") do
80
+ require 'teek/ui'
81
+
82
+ session = Teek::UI.app(title: 'Auto-Hide Test') { |ui| ui.list(:items, height: 5) }
83
+ session.run_async
84
+ session.app.command(session[:items].path, :insert, :end, *(1..50).map { |i| "Item #{i}" })
85
+
86
+ wrapper = session[:items].path.sub(/\.widget\z/, '')
87
+ assert wait_until { session.app.winfo.ismapped?("#{wrapper}.vsb") }, "should start shown with 50 items"
88
+
89
+ session.app.command(session[:items].path, :delete, 2, :end)
90
+
91
+ assert wait_until { !session.app.winfo.ismapped?("#{wrapper}.vsb") }, "should hide again once only 2 items remain"
92
+
93
+ session.app.destroy
94
+ end
95
+ end
96
+
97
+ def test_the_frame_case_also_auto_hides_its_scrollbar
98
+ assert_tk_app("ui.scrollable's own canvas-driven scrollbar should auto-hide the same way") do
99
+ require 'teek/ui'
100
+
101
+ session = Teek::UI.app(title: 'Auto-Hide Test') do |ui|
102
+ ui.scrollable(:region) { |s| s.column(:rows) { |c| c.button(text: 'One lone button') } }
103
+ end
104
+ session.run_async
105
+
106
+ assert wait_until { !session.app.winfo.ismapped?("#{session[:region].path}.vsb") },
107
+ "one small button shouldn't overflow the canvas's own default size"
108
+
109
+ session.app.destroy
110
+ end
111
+ end
112
+
113
+ def test_the_frame_case_shows_its_scrollbar_once_content_overflows
114
+ assert_tk_app("ui.scrollable's canvas-driven scrollbar should appear once its content overflows") do
115
+ require 'teek/ui'
116
+
117
+ session = Teek::UI.app(title: 'Auto-Hide Test') do |ui|
118
+ ui.scrollable(:region) { |s| s.column(:rows) { |c| 40.times { |i| c.button(text: "Row #{i}") } } }
119
+ end
120
+ session.run_async
121
+
122
+ assert wait_until { session.app.winfo.ismapped?("#{session[:region].path}.vsb") }
123
+
124
+ session.app.destroy
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # ui.split/s.pane realize as a working ttk::panedwindow - each pane a real
7
+ # Tk frame added as a managed pane (PaneRealize.post_create), never
8
+ # pack/grid managed on its own (:pane's own arranged: false).
9
+ class TestSplit < Minitest::Test
10
+ include TeekTestHelper
11
+
12
+ def test_split_realizes_as_a_working_panedwindow_with_panes
13
+ assert_tk_app("ui.split should realize as a ttk::panedwindow with each pane added as a managed region") do
14
+ require 'teek/ui'
15
+
16
+ session = Teek::UI.app(title: 'Split Test') do |ui|
17
+ ui.split(:s) do |s|
18
+ s.pane { |a| a.button(:go, text: 'Go') }
19
+ s.pane { |b| b.label(:info, text: 'Info') }
20
+ end
21
+ end
22
+ session.run_async
23
+ session.app.update
24
+
25
+ split_path = session[:s].path
26
+ pane_paths = session.app.split_list(session.app.command(split_path, :panes))
27
+ assert_equal 2, pane_paths.length
28
+
29
+ session.app.destroy
30
+ end
31
+ end
32
+
33
+ def test_pane_content_is_addressable_and_configurable_like_any_other_widget
34
+ assert_tk_app("a widget declared inside a pane should be a normal, addressable, configurable widget") do
35
+ require 'teek/ui'
36
+
37
+ session = Teek::UI.app(title: 'Split Test') do |ui|
38
+ ui.split { |s| s.pane { |a| a.button(:go, text: 'Go') } }
39
+ end
40
+ session.run_async
41
+ session.app.update
42
+
43
+ session[:go].configure(text: 'Changed')
44
+ session.app.update
45
+
46
+ assert_equal 'Changed', session.app.command(session[:go].path, :cget, '-text')
47
+
48
+ session.app.destroy
49
+ end
50
+ end
51
+
52
+ def test_pane_frame_is_not_independently_pack_or_grid_managed
53
+ assert_tk_app("a pane's own frame should be placed only by panedwindow add, not pack/grid") do
54
+ require 'teek/ui'
55
+
56
+ session = Teek::UI.app(title: 'Split Test') do |ui|
57
+ ui.split(:s) { |s| s.pane(:left) { |a| a.button(text: 'Go') } }
58
+ end
59
+ session.run_async
60
+ session.app.update
61
+
62
+ assert_equal 'panedwindow', session.app.tcl_eval("winfo manager #{session[:left].path}"),
63
+ "the panedwindow itself should be the only geometry manager - proves no pack/grid call also ran"
64
+
65
+ session.app.destroy
66
+ end
67
+ end
68
+
69
+ def test_split_defaults_to_horizontal_orientation
70
+ assert_tk_app("ui.split with no orientation given should realize with -orient horizontal") do
71
+ require 'teek/ui'
72
+
73
+ session = Teek::UI.app(title: 'Split Test') do |ui|
74
+ ui.split(:s) { |s| s.pane { } }
75
+ end
76
+ session.run_async
77
+ session.app.update
78
+
79
+ assert_equal 'horizontal', session.app.command(session[:s].path, :cget, '-orient')
80
+
81
+ session.app.destroy
82
+ end
83
+ end
84
+
85
+ def test_split_vertical_orientation_sets_the_real_tk_orient_option
86
+ assert_tk_app("ui.split(orientation: :vertical) should realize with -orient vertical") do
87
+ require 'teek/ui'
88
+
89
+ session = Teek::UI.app(title: 'Split Test') do |ui|
90
+ ui.split(:s, orientation: :vertical) { |s| s.pane { } }
91
+ end
92
+ session.run_async
93
+ session.app.update
94
+
95
+ assert_equal 'vertical', session.app.command(session[:s].path, :cget, '-orient')
96
+
97
+ session.app.destroy
98
+ end
99
+ end
100
+
101
+ def test_pane_weight_is_passed_through_to_panedwindow_add
102
+ assert_tk_app("s.pane(weight:) should set the real -weight panedwindow uses to divide leftover space") do
103
+ require 'teek/ui'
104
+
105
+ session = Teek::UI.app(title: 'Split Test') do |ui|
106
+ ui.split(:s) do |s|
107
+ s.pane(:left, weight: 1) { }
108
+ s.pane(:right, weight: 3) { }
109
+ end
110
+ end
111
+ session.run_async
112
+ session.app.update
113
+
114
+ split_path = session[:s].path
115
+ assert_equal '1', session.app.command(split_path, :pane, session[:left].path, '-weight')
116
+ assert_equal '3', session.app.command(split_path, :pane, session[:right].path, '-weight')
117
+
118
+ session.app.destroy
119
+ end
120
+ end
121
+
122
+ def test_a_new_pane_can_be_added_at_runtime_via_incremental_realize
123
+ assert_tk_app("session.add should be able to add a whole new pane to an already-realized ui.split") do
124
+ require 'teek/ui'
125
+
126
+ session = Teek::UI.app(title: 'Split Test') do |ui|
127
+ ui.split(:s) { |s| s.pane { |a| a.button(text: 'Go') } }
128
+ end
129
+ session.run_async
130
+ session.app.update
131
+
132
+ split_path = session[:s].path
133
+ assert_equal 1, session.app.split_list(session.app.command(split_path, :panes)).length
134
+
135
+ session.add(:s) { |a| a.pane(:new_pane) { |n| n.button(:new_button, text: 'New') } }
136
+ session.app.update
137
+
138
+ pane_paths = session.app.split_list(session.app.command(split_path, :panes))
139
+ assert_equal 2, pane_paths.length
140
+ assert session.app.winfo.ismapped?(session[:new_button].path)
141
+
142
+ session.app.destroy
143
+ end
144
+ end
145
+ end