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,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # ui.toast: a brief, auto-dismissing notification (e.g. "Saved" after a
7
+ # save action). Genericized from an SDL2-rendered original that drew its
8
+ # own background/text directly to a renderer - this is a real ttk::label,
9
+ # floated over the bottom of the window via place, reused across calls
10
+ # rather than rebuilt each time (there's only ever one toast on screen).
11
+ class TestToast < Minitest::Test
12
+ include TeekTestHelper
13
+
14
+ def test_toast_raises_before_realize
15
+ assert_tk_app("session.toast should raise before realize, matching every other realize-only action") do
16
+ require 'teek/ui'
17
+
18
+ session = Teek::UI.app(title: 'Toast Test')
19
+
20
+ assert_raises(Teek::UI::NotRealizedError) { session.toast('Saved') }
21
+ end
22
+ end
23
+
24
+ def test_toast_shows_the_message
25
+ assert_tk_app("toast should create a visible widget carrying the given text") do
26
+ require 'teek/ui'
27
+
28
+ session = Teek::UI.app(title: 'Toast Test')
29
+ session.run_async
30
+
31
+ session.toast('Saved')
32
+ session.app.update
33
+
34
+ assert_equal 'Saved', session.app.command('.toast', :cget, '-text')
35
+ assert session.app.winfo.ismapped?('.toast')
36
+
37
+ session.app.destroy
38
+ end
39
+ end
40
+
41
+ def test_toast_auto_dismisses_after_a_custom_duration
42
+ assert_tk_app("toast should auto-dismiss after the given duration, not stay forever") do
43
+ require 'teek/ui'
44
+
45
+ session = Teek::UI.app(title: 'Toast Test')
46
+ session.run_async
47
+
48
+ session.toast('Saved', duration: 30)
49
+ session.app.update
50
+ assert session.app.winfo.ismapped?('.toast'), "should be visible immediately after showing"
51
+
52
+ deadline = Time.now + 2
53
+ session.app.update while session.app.winfo.ismapped?('.toast') && Time.now < deadline
54
+
55
+ refute session.app.winfo.ismapped?('.toast'), "should have auto-dismissed by now"
56
+
57
+ session.app.destroy
58
+ end
59
+ end
60
+
61
+ def test_toast_auto_dismisses_after_the_default_duration
62
+ assert_tk_app("toast with no duration: override should still auto-dismiss, using the built-in default") do
63
+ require 'teek/ui'
64
+
65
+ session = Teek::UI.app(title: 'Toast Test')
66
+ session.run_async
67
+
68
+ session.toast('Saved')
69
+ session.app.update
70
+ assert session.app.winfo.ismapped?('.toast')
71
+
72
+ deadline = Time.now + 3
73
+ session.app.update while session.app.winfo.ismapped?('.toast') && Time.now < deadline
74
+
75
+ refute session.app.winfo.ismapped?('.toast'), "the default duration should have elapsed by now"
76
+
77
+ session.app.destroy
78
+ end
79
+ end
80
+
81
+ def test_toast_called_again_replaces_rather_than_stacks
82
+ assert_tk_app("a second toast while one is showing should replace it, not create a second widget") do
83
+ require 'teek/ui'
84
+
85
+ session = Teek::UI.app(title: 'Toast Test')
86
+ session.run_async
87
+
88
+ before = session.app.split_list(session.app.tcl_eval('winfo children .')).length
89
+
90
+ session.toast('Saved')
91
+ session.app.update
92
+ session.toast('Settings')
93
+ session.app.update
94
+
95
+ after = session.app.split_list(session.app.tcl_eval('winfo children .')).length
96
+
97
+ assert_equal before + 1, after, "calling toast twice should not leave two separate widgets behind"
98
+ assert_equal 'Settings', session.app.command('.toast', :cget, '-text')
99
+
100
+ session.app.destroy
101
+ end
102
+ end
103
+
104
+ def test_toast_called_again_cancels_the_earlier_dismiss_timer
105
+ assert_tk_app("replacing a toast should cancel the earlier one's auto-dismiss - it must not hide the NEW toast early") do
106
+ require 'teek/ui'
107
+
108
+ session = Teek::UI.app(title: 'Toast Test')
109
+ session.run_async
110
+
111
+ session.toast('Saved', duration: 30)
112
+ session.app.update
113
+ session.toast('Settings', duration: 1000)
114
+ session.app.update
115
+
116
+ sleep 0.08
117
+ session.app.update
118
+
119
+ assert session.app.winfo.ismapped?('.toast'),
120
+ "the first toast's short timer should have been cancelled, not fired and hidden the replacement"
121
+ assert_equal 'Settings', session.app.command('.toast', :cget, '-text')
122
+
123
+ session.app.destroy
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+ require 'teek/ui/tree_inspector'
5
+ require 'teek/ui/document'
6
+ require 'teek/ui/session'
7
+
8
+ # TreeInspector only ever calls #root and #subscribe on whatever it's
9
+ # given - these two minimal doubles exist to prove that's genuinely true
10
+ # (it works against ANY object shaped like this, not just an instance of
11
+ # Teek::UI::Document/Node), not merely that it's convenient to unit test
12
+ # through the real classes.
13
+ FakeInspectorNode = Struct.new(:type, :name, :opts, :children) do
14
+ def display_name
15
+ name ? "#{type}(:#{name})" : type.to_s
16
+ end
17
+ end
18
+
19
+ FakeInspectorDocument = Struct.new(:root) do
20
+ def initialize(root)
21
+ super
22
+ @listeners = Hash.new { |h, k| h[k] = [] }
23
+ end
24
+
25
+ def subscribe(event, &block)
26
+ @listeners[event] << block
27
+ end
28
+
29
+ def notify(event, *args)
30
+ @listeners[event].each { |listener| listener.call(*args) }
31
+ end
32
+ end
33
+
34
+ class TestTreeInspector < Minitest::Test
35
+ def test_to_s_and_log_work_against_a_fake_document_not_the_real_teek_ui_one
36
+ button = FakeInspectorNode.new(:button, nil, { text: 'Go' }, [])
37
+ root = FakeInspectorNode.new(:root, nil, {}, [button])
38
+ document = FakeInspectorDocument.new(root)
39
+
40
+ inspector = Teek::UI::TreeInspector.new(document, trace: true)
41
+ document.notify(:append, root, button)
42
+
43
+ assert_equal "root\n└─ button \"Go\"", inspector.to_s
44
+ assert_equal 1, inspector.log.length
45
+ assert_equal :append, inspector.log.first.action
46
+ assert_same button, inspector.log.first.node
47
+ end
48
+
49
+ def test_to_s_renders_the_example_shape
50
+ document = Teek::UI::Document.new
51
+ column = document.root.add_child(document.create(type: :column))
52
+ column.add_child(document.create(type: :label, opts: { text: 'Title' }))
53
+ row = column.add_child(document.create(type: :row))
54
+ row.add_child(document.create(type: :button, opts: { text: 'OK' }))
55
+ row.add_child(document.create(type: :button, opts: { text: 'Cancel' }))
56
+ column.add_child(document.create(type: :label, opts: { text: 'Footer' }))
57
+
58
+ expected = <<~TREE.chomp
59
+ root
60
+ └─ column
61
+ ├─ label "Title"
62
+ ├─ row
63
+ │ ├─ button "OK"
64
+ │ └─ button "Cancel"
65
+ └─ label "Footer"
66
+ TREE
67
+
68
+ assert_equal expected, Teek::UI::TreeInspector.new(document).to_s
69
+ end
70
+
71
+ def test_to_s_on_an_empty_document_is_just_root
72
+ document = Teek::UI::Document.new
73
+
74
+ assert_equal 'root', Teek::UI::TreeInspector.new(document).to_s
75
+ end
76
+
77
+ def test_to_s_shows_a_name_alongside_the_type
78
+ document = Teek::UI::Document.new
79
+ document.root.add_child(document.create(type: :column, name: :ctrl))
80
+
81
+ assert_equal "root\n└─ column(:ctrl)", Teek::UI::TreeInspector.new(document).to_s
82
+ end
83
+
84
+ def test_to_s_prefers_text_over_label_when_a_node_somehow_has_both
85
+ document = Teek::UI::Document.new
86
+ document.root.add_child(document.create(type: :button, opts: { text: 'OK', label: 'ignored' }))
87
+
88
+ assert_equal "root\n└─ button \"OK\"", Teek::UI::TreeInspector.new(document).to_s
89
+ end
90
+
91
+ def test_to_s_falls_back_to_label_for_a_menu_item_shaped_node
92
+ document = Teek::UI::Document.new
93
+ document.root.add_child(document.create(type: :menu_item, opts: { label: 'Open' }))
94
+
95
+ assert_equal "root\n└─ menu_item \"Open\"", Teek::UI::TreeInspector.new(document).to_s
96
+ end
97
+
98
+ def test_to_s_reflects_the_trees_current_shape_when_called_again_later
99
+ document = Teek::UI::Document.new
100
+ inspector = Teek::UI::TreeInspector.new(document)
101
+
102
+ assert_equal 'root', inspector.to_s
103
+
104
+ document.root.add_child(document.create(type: :button, opts: { text: 'Go' }))
105
+
106
+ assert_equal "root\n└─ button \"Go\"", inspector.to_s
107
+ end
108
+
109
+ def test_print_tree_writes_to_s_to_stdout
110
+ document = Teek::UI::Document.new
111
+ document.root.add_child(document.create(type: :button, opts: { text: 'Go' }))
112
+ inspector = Teek::UI::TreeInspector.new(document)
113
+
114
+ out, = capture_io { inspector.print_tree }
115
+
116
+ assert_equal "#{inspector}\n", out
117
+ end
118
+
119
+ def test_log_is_empty_without_trace_true
120
+ document = Teek::UI::Document.new
121
+ inspector = Teek::UI::TreeInspector.new(document)
122
+
123
+ document.root.add_child(document.create(type: :button))
124
+
125
+ assert_equal [], inspector.log
126
+ end
127
+
128
+ def test_log_records_append_events_once_tracing
129
+ document = Teek::UI::Document.new
130
+ inspector = Teek::UI::TreeInspector.new(document, trace: true)
131
+ parent = document.create(type: :column, name: :ctrl)
132
+ child = document.create(type: :button, opts: { text: 'Go' })
133
+
134
+ parent.add_child(child)
135
+
136
+ assert_equal 1, inspector.log.length
137
+ event = inspector.log.first
138
+ assert_equal :append, event.action
139
+ assert_same child, event.node
140
+ assert_equal 'column(:ctrl)', event.path
141
+ end
142
+
143
+ def test_log_records_push_and_pop_events_once_tracing_through_the_real_builder
144
+ session = Teek::UI::Session.new(title: 'Tree Inspector Test')
145
+ inspector = Teek::UI::TreeInspector.new(session.document, trace: true)
146
+
147
+ session.column(:ctrl) { |c| c.row(:speed_row) { } }
148
+
149
+ actions = inspector.log.map(&:action)
150
+ assert_equal %i[append push append push pop pop], actions
151
+ assert_equal 'root', inspector.log[0].path, "column appended under the (unnamed) root"
152
+ assert_equal 'column(:ctrl)', inspector.log[1].path, "pushing column - path now includes it"
153
+ assert_equal 'column(:ctrl)', inspector.log[2].path, "row appended under column"
154
+ assert_equal 'column(:ctrl) > row(:speed_row)', inspector.log[3].path, "pushing row - path now includes it too"
155
+ end
156
+
157
+ def test_two_inspectors_on_the_same_document_are_independent
158
+ document = Teek::UI::Document.new
159
+ tracing = Teek::UI::TreeInspector.new(document, trace: true)
160
+ not_tracing = Teek::UI::TreeInspector.new(document)
161
+
162
+ document.root.add_child(document.create(type: :button))
163
+
164
+ refute_empty tracing.log
165
+ assert_empty not_tracing.log
166
+ end
167
+
168
+ def test_event_to_s_is_readable_for_each_action
169
+ document = Teek::UI::Document.new
170
+ inspector = Teek::UI::TreeInspector.new(document, trace: true)
171
+ parent = document.create(type: :column)
172
+ child = document.create(type: :button)
173
+
174
+ document.notify(:push, parent, 'column')
175
+ parent.add_child(child)
176
+ document.notify(:pop, parent, 'column')
177
+
178
+ push_s, append_s, pop_s = inspector.log.map(&:to_s)
179
+ assert_match(/column/, push_s)
180
+ assert_match(/button/, append_s)
181
+ assert_match(/column/, pop_s)
182
+ end
183
+ end