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
data/test/test_toast.rb
ADDED
|
@@ -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
|