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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +41 -0
- data/README.md +452 -0
- data/lib/teek/ui/canvas_item.rb +270 -0
- data/lib/teek/ui/component_handle.rb +32 -0
- data/lib/teek/ui/document.rb +185 -0
- data/lib/teek/ui/errors.rb +41 -0
- data/lib/teek/ui/event_binding.rb +22 -0
- data/lib/teek/ui/event_bus.rb +53 -0
- data/lib/teek/ui/grid_validator.rb +89 -0
- data/lib/teek/ui/handle.rb +585 -0
- data/lib/teek/ui/image.rb +58 -0
- data/lib/teek/ui/keysyms.rb +72 -0
- data/lib/teek/ui/menu_builder.rb +130 -0
- data/lib/teek/ui/menu_entry_addressing.rb +62 -0
- data/lib/teek/ui/modal_stack.rb +99 -0
- data/lib/teek/ui/mouse_events.rb +31 -0
- data/lib/teek/ui/node.rb +141 -0
- data/lib/teek/ui/option_dump_parsing.rb +31 -0
- data/lib/teek/ui/overlay_anchors.rb +29 -0
- data/lib/teek/ui/overlay_validator.rb +33 -0
- data/lib/teek/ui/pane_validator.rb +30 -0
- data/lib/teek/ui/realized_node.rb +25 -0
- data/lib/teek/ui/realizer.rb +577 -0
- data/lib/teek/ui/scope.rb +40 -0
- data/lib/teek/ui/screens.rb +141 -0
- data/lib/teek/ui/session.rb +417 -0
- data/lib/teek/ui/tab_validator.rb +30 -0
- data/lib/teek/ui/text_content.rb +321 -0
- data/lib/teek/ui/tree_inspector.rb +97 -0
- data/lib/teek/ui/validator.rb +131 -0
- data/lib/teek/ui/var.rb +98 -0
- data/lib/teek/ui/version.rb +7 -0
- data/lib/teek/ui/widget_addressing.rb +47 -0
- data/lib/teek/ui/widget_dsl.rb +512 -0
- data/lib/teek/ui/widget_type.rb +242 -0
- data/lib/teek/ui/widget_types/button.rb +7 -0
- data/lib/teek/ui/widget_types/canvas.rb +15 -0
- data/lib/teek/ui/widget_types/checkbox.rb +7 -0
- data/lib/teek/ui/widget_types/column.rb +14 -0
- data/lib/teek/ui/widget_types/context_menu.rb +15 -0
- data/lib/teek/ui/widget_types/divider.rb +10 -0
- data/lib/teek/ui/widget_types/dropdown.rb +7 -0
- data/lib/teek/ui/widget_types/grid.rb +12 -0
- data/lib/teek/ui/widget_types/group.rb +7 -0
- data/lib/teek/ui/widget_types/label.rb +7 -0
- data/lib/teek/ui/widget_types/list.rb +7 -0
- data/lib/teek/ui/widget_types/menu_bar.rb +26 -0
- data/lib/teek/ui/widget_types/menu_checkbox.rb +14 -0
- data/lib/teek/ui/widget_types/menu_item.rb +20 -0
- data/lib/teek/ui/widget_types/menu_radio.rb +14 -0
- data/lib/teek/ui/widget_types/number_box.rb +7 -0
- data/lib/teek/ui/widget_types/pane.rb +37 -0
- data/lib/teek/ui/widget_types/panel.rb +7 -0
- data/lib/teek/ui/widget_types/progress.rb +7 -0
- data/lib/teek/ui/widget_types/radio.rb +7 -0
- data/lib/teek/ui/widget_types/row.rb +14 -0
- data/lib/teek/ui/widget_types/scrollable.rb +17 -0
- data/lib/teek/ui/widget_types/slider.rb +7 -0
- data/lib/teek/ui/widget_types/spacer.rb +14 -0
- data/lib/teek/ui/widget_types/split.rb +17 -0
- data/lib/teek/ui/widget_types/tab.rb +35 -0
- data/lib/teek/ui/widget_types/table.rb +9 -0
- data/lib/teek/ui/widget_types/tabs.rb +10 -0
- data/lib/teek/ui/widget_types/text_area.rb +7 -0
- data/lib/teek/ui/widget_types/text_box.rb +7 -0
- data/lib/teek/ui/widget_types/tree.rb +9 -0
- data/lib/teek/ui/widget_types/window.rb +50 -0
- data/lib/teek/ui/widget_types.rb +121 -0
- data/lib/teek/ui/widget_validators.rb +62 -0
- data/lib/teek/ui.rb +54 -0
- data/teek-ui.gemspec +24 -0
- data/test/support/fake_app.rb +57 -0
- data/test/test_busy.rb +82 -0
- data/test/test_canvas_items.rb +551 -0
- data/test/test_clipboard.rb +87 -0
- data/test/test_close_handling.rb +118 -0
- data/test/test_component.rb +151 -0
- data/test/test_component_realtk.rb +120 -0
- data/test/test_debug_info.rb +158 -0
- data/test/test_document.rb +317 -0
- data/test/test_event_bus.rb +163 -0
- data/test/test_events.rb +164 -0
- data/test/test_fake_app_contract.rb +62 -0
- data/test/test_grid.rb +97 -0
- data/test/test_handle.rb +386 -0
- data/test/test_handle_destroy_realtk.rb +344 -0
- data/test/test_helper.rb +20 -0
- data/test/test_image.rb +24 -0
- data/test/test_image_realtk.rb +146 -0
- data/test/test_incremental_realize.rb +203 -0
- data/test/test_keysyms.rb +76 -0
- data/test/test_layout.rb +185 -0
- data/test/test_lazy_realize.rb +128 -0
- data/test/test_managed_window.rb +304 -0
- data/test/test_menu_dsl.rb +230 -0
- data/test/test_menu_entry_addressing.rb +73 -0
- data/test/test_menu_realize.rb +296 -0
- data/test/test_modal_handle.rb +147 -0
- data/test/test_modal_stack.rb +219 -0
- data/test/test_modal_stack_realtk.rb +131 -0
- data/test/test_native_scrollable.rb +250 -0
- data/test/test_node.rb +193 -0
- data/test/test_overlay.rb +139 -0
- data/test/test_raw.rb +58 -0
- data/test/test_reactive_vars.rb +118 -0
- data/test/test_realizer.rb +264 -0
- data/test/test_scope.rb +38 -0
- data/test/test_screens.rb +260 -0
- data/test/test_screens_realtk.rb +314 -0
- data/test/test_scrollable.rb +185 -0
- data/test/test_scrollable_wheel.rb +172 -0
- data/test/test_scrollbar_auto_hide.rb +127 -0
- data/test/test_split.rb +145 -0
- data/test/test_tabs.rb +201 -0
- data/test/test_text_content.rb +664 -0
- data/test/test_toast.rb +126 -0
- data/test/test_tree_inspector.rb +183 -0
- data/test/test_ui.rb +428 -0
- data/test/test_validator.rb +209 -0
- data/test/test_var.rb +32 -0
- data/test/test_widget_dsl.rb +621 -0
- data/test/test_widget_introspection.rb +126 -0
- data/test/test_widget_types.rb +429 -0
- data/test/test_widget_types_realtk.rb +82 -0
- data/test/test_widget_validators.rb +82 -0
- 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
|
data/test/test_layout.rb
ADDED
|
@@ -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
|