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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../widget_type'
4
+
5
+ Teek::UI::WidgetTypes.register(
6
+ Teek::UI::WidgetType.new(type: :text_area, tk_command: 'text', natively_scrollable: true)
7
+ )
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../widget_type'
4
+
5
+ Teek::UI::WidgetTypes.register(
6
+ Teek::UI::WidgetType.new(type: :text_box, tk_command: 'ttk::entry', bind_option: :textvariable)
7
+ )
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../widget_type'
4
+
5
+ # `tree` and `table` are two DSL names over the same Tk widget
6
+ # (ttk::treeview, used with/without -show tree) - see table.rb.
7
+ Teek::UI::WidgetTypes.register(
8
+ Teek::UI::WidgetType.new(type: :tree, tk_command: 'ttk::treeview', natively_scrollable: true)
9
+ )
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../widget_type'
4
+
5
+ module Teek
6
+ module UI
7
+ # @api private
8
+ #
9
+ # A freshly created :window's post-creation setup: title/geometry/
10
+ # resizable setup, transient-to-parent, the macOS shared-menubar quirk
11
+ # (each platform other than macOS gets its own menu bar per window;
12
+ # macOS has a single app-wide menu bar, so without this a new window
13
+ # falls back to Tk's default "wish" menu instead of the parent's), and
14
+ # withdrawn by default - shown explicitly via Handle#show. Registered
15
+ # as :window's own `post_create:` below.
16
+ module WindowRealize
17
+ def self.post_create(app, node, path, parent_path)
18
+ opts = node.opts
19
+ window = app.window(path)
20
+
21
+ window.set_title(opts[:title]) if opts[:title]
22
+ window.set_geometry(opts[:geometry]) if opts[:geometry]
23
+ if opts.key?(:resizable)
24
+ pair = opts[:resizable]
25
+ width, height = pair.is_a?(Array) ? pair : [pair, pair]
26
+ window.set_resizable(width, height)
27
+ end
28
+ app.command(:wm, :transient, path, parent_path) unless opts[:transient] == false
29
+ share_macos_menu(app, path, parent_path) if Teek.platform.darwin?
30
+ window.withdraw
31
+ end
32
+
33
+ def self.share_macos_menu(app, path, parent_path)
34
+ parent_menu = app.command(parent_path, :cget, '-menu')
35
+ app.command(path, :configure, menu: parent_menu) unless parent_menu.nil? || parent_menu.empty?
36
+ rescue Teek::TclError
37
+ nil
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ # A toplevel - placed by the window manager, never pack/grid-managed by
44
+ # its nominal parent.
45
+ Teek::UI::WidgetTypes.register(
46
+ Teek::UI::WidgetType.new(
47
+ type: :window, tk_command: 'toplevel', leaf: false, arranged: false,
48
+ post_create: Teek::UI::WindowRealize.method(:post_create)
49
+ )
50
+ )
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'widget_type'
4
+ require_relative 'widget_validators'
5
+
6
+ module Teek
7
+ module UI
8
+ # @api private
9
+ #
10
+ # Central registry of {WidgetType} descriptors, mirroring
11
+ # {Teek::CommandInterceptors}'s own register/for_type shape - one
12
+ # registered descriptor per node type is the single source of truth
13
+ # {WidgetDSL}, {Realizer}, and {Validator} each dispatch to for that
14
+ # type's own build/realize/validate behavior.
15
+ #
16
+ # Unlike {Teek::CommandInterceptors} (consulted per-call with a type
17
+ # already in hand) or {WidgetValidators} (many validators can share one
18
+ # type), exactly one descriptor owns a given type here, and consumers
19
+ # need the full set up front to drive codegen - so this adds two things
20
+ # CommandInterceptors doesn't need: {.each} (enumerate every registered
21
+ # type) and {.on_register} (subscribe to every past AND future
22
+ # registration, so load order between this file and a consumer like
23
+ # {WidgetDSL} never matters).
24
+ #
25
+ # A descriptor's +validator:+ (see {WidgetType}) is forwarded into
26
+ # {WidgetValidators} right here, at registration time, so it's
27
+ # dispatched through the exact same +WidgetValidators.for_type+ call
28
+ # every other validator goes through - {Validator} itself carries no
29
+ # awareness of descriptors at all.
30
+ class WidgetTypes
31
+ class << self
32
+ # @param widget_type [WidgetType]
33
+ # @return [WidgetType] +widget_type+, for chaining
34
+ # @raise [ArgumentError] if +widget_type.type+ is already registered
35
+ def register(widget_type)
36
+ key = widget_type.type.to_s
37
+ if types.key?(key)
38
+ raise ArgumentError, "widget type :#{widget_type.type} is already registered"
39
+ end
40
+
41
+ types[key] = widget_type
42
+ if widget_type.validator
43
+ WidgetValidators.register(widget_type.type) { |*args| widget_type.validator.call(*args) }
44
+ end
45
+ callbacks.each { |callback| callback.call(widget_type) }
46
+
47
+ widget_type
48
+ end
49
+
50
+ # @param type [Symbol, String]
51
+ # @return [WidgetType, nil]
52
+ def for_type(type)
53
+ types[type.to_s]
54
+ end
55
+
56
+ # @yieldparam widget_type [WidgetType]
57
+ # @return [Enumerator] if no block given
58
+ def each(&block)
59
+ return enum_for(:each) unless block
60
+
61
+ types.each_value(&block)
62
+ end
63
+
64
+ # Subscribes +block+ to every type registered from now on, and
65
+ # immediately replays every type already registered - so a
66
+ # subscriber (namely {WidgetDSL}'s own codegen) sees every type
67
+ # regardless of whether it loaded before or after this file
68
+ # populated its own built-ins.
69
+ # @yieldparam widget_type [WidgetType]
70
+ # @return [void]
71
+ def on_register(&block)
72
+ types.each_value(&block)
73
+ callbacks << block
74
+ end
75
+
76
+ private
77
+
78
+ def types
79
+ @types ||= {}
80
+ end
81
+
82
+ def callbacks
83
+ @callbacks ||= []
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ require_relative 'widget_types/divider'
91
+ require_relative 'widget_types/text_box'
92
+ require_relative 'widget_types/text_area'
93
+ require_relative 'widget_types/label'
94
+ require_relative 'widget_types/button'
95
+ require_relative 'widget_types/checkbox'
96
+ require_relative 'widget_types/radio'
97
+ require_relative 'widget_types/slider'
98
+ require_relative 'widget_types/dropdown'
99
+ require_relative 'widget_types/number_box'
100
+ require_relative 'widget_types/list'
101
+ require_relative 'widget_types/table'
102
+ require_relative 'widget_types/tree'
103
+ require_relative 'widget_types/progress'
104
+ require_relative 'widget_types/panel'
105
+ require_relative 'widget_types/group'
106
+ require_relative 'widget_types/canvas'
107
+ require_relative 'widget_types/window'
108
+ require_relative 'widget_types/column'
109
+ require_relative 'widget_types/row'
110
+ require_relative 'widget_types/spacer'
111
+ require_relative 'widget_types/grid'
112
+ require_relative 'widget_types/scrollable'
113
+ require_relative 'widget_types/tabs'
114
+ require_relative 'widget_types/tab'
115
+ require_relative 'widget_types/split'
116
+ require_relative 'widget_types/pane'
117
+ require_relative 'widget_types/menu_bar'
118
+ require_relative 'widget_types/context_menu'
119
+ require_relative 'widget_types/menu_item'
120
+ require_relative 'widget_types/menu_checkbox'
121
+ require_relative 'widget_types/menu_radio'
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teek
4
+ module UI
5
+ # @api private
6
+ #
7
+ # Registry of per-node-type validators, mirroring {Teek::CommandInterceptors}'
8
+ # own register(type, ...)/for_type(type) shape. {Validator} (the
9
+ # coordinator) walks the whole {Document} exactly once; at each node it
10
+ # looks up +WidgetValidators.for_type(node.type)+ and calls every
11
+ # validator registered there.
12
+ #
13
+ # A registered validator receives +(node, parent, document, errors)+ and
14
+ # must APPEND problem strings to +errors+ - it must never raise itself.
15
+ # Unlike a {Teek::CommandInterceptors} entry (which "claims" a call and
16
+ # returns its result), a widget validator has nothing to return and
17
+ # nothing to claim exclusively - multiple validators for the same type
18
+ # can coexist freely, and there's no ambiguity to detect.
19
+ #
20
+ # Registering a validator for a type is purely additive: a custom or
21
+ # third-party widget can call {.register} to get its own contract
22
+ # checked without editing this file or {Validator}, the same way a
23
+ # custom widget can register a {Teek::CommandInterceptors} entry without
24
+ # editing +teek.rb+.
25
+ class WidgetValidators
26
+ class << self
27
+ # @param type [Symbol, String] the node type this validator applies to
28
+ # @yieldparam node [Node]
29
+ # @yieldparam parent [Node, nil]
30
+ # @yieldparam document [Document]
31
+ # @yieldparam errors [Array<String>]
32
+ # @return [void]
33
+ def register(type, &block)
34
+ validators[type.to_s] << block
35
+ end
36
+
37
+ # @param type [Symbol, String]
38
+ # @return [Array<Proc>] every validator registered for +type+, in
39
+ # registration order - empty if none are
40
+ def for_type(type)
41
+ validators[type.to_s]
42
+ end
43
+
44
+ # Shared node-describing helper every validator's error messages use
45
+ # ("#label(:name)" or "an unnamed #label") - kept here rather than
46
+ # duplicated per validator file, since every one of them needs it.
47
+ # @param node [Node, nil]
48
+ # @return [String]
49
+ def describe(node)
50
+ return 'the document root' unless node
51
+ node.name ? "##{node.type}(:#{node.name})" : "an unnamed ##{node.type}"
52
+ end
53
+
54
+ private
55
+
56
+ def validators
57
+ @validators ||= Hash.new { |h, k| h[k] = [] }
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/teek/ui.rb ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "teek"
4
+ require_relative "ui/version"
5
+ require_relative "ui/event_binding"
6
+ require_relative "ui/session"
7
+ require_relative "ui/tree_inspector"
8
+
9
+ module Teek
10
+ # A DSL for building teek (Tk) apps - sugar over teek, not a wall around it.
11
+ # Everything here compiles down to plain teek calls, and every handle keeps
12
+ # an escape hatch back to the underlying {Teek::App}.
13
+ module UI
14
+ class << self
15
+ # @return [Boolean] whether a bare list/text_area/table/tree
16
+ # auto-attaches a scrollbar with no `ui.scrollable` wrapper needed -
17
+ # true by default. Three levels can override it, most specific
18
+ # wins: a widget's own `scroll:` option, then `Teek::UI.app`'s own
19
+ # `scroll:`, then this global default.
20
+ attr_accessor :auto_scroll
21
+
22
+ # @return [Boolean] the same default, but for canvas specifically -
23
+ # false by default, since a canvas is as often fixed drawing as
24
+ # scrollable content, unlike the other native types.
25
+ attr_accessor :auto_scroll_canvas
26
+ end
27
+ self.auto_scroll = true
28
+ self.auto_scroll_canvas = false
29
+
30
+ # Build an app. Constructs a {Session} (Tk-free - no {Teek::App} exists
31
+ # yet), yields it to the block, and returns that same session so
32
+ # `.run`/`.run_async` can be chained directly off the call. The
33
+ # underlying app is created lazily, at realize (see {Session#realize}).
34
+ #
35
+ # @param title [String, nil] window title
36
+ # @param scroll [Boolean, nil] app-wide override for whether native
37
+ # scrollable widgets auto-attach a scrollbar - between a widget's own
38
+ # `scroll:` (most specific) and {.auto_scroll}/{.auto_scroll_canvas}
39
+ # (global default). `nil` (the default) defers straight to the global.
40
+ # @param app_opts [Hash] forwarded to {Teek::App.new} at realize (e.g. +debug:+, +track_widgets:+)
41
+ # @yieldparam ui [Session]
42
+ # @return [Session]
43
+ #
44
+ # @example
45
+ # Teek::UI.app(title: "Hello") do |ui|
46
+ # # widget/layout DSL calls go here
47
+ # end.run
48
+ def self.app(title: nil, scroll: nil, **app_opts, &block)
49
+ session = Session.new(title: title, scroll: scroll, app_opts: app_opts)
50
+ block.call(session) if block
51
+ session
52
+ end
53
+ end
54
+ end
data/teek-ui.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ require_relative "lib/teek/ui/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "teek-ui"
5
+ spec.version = Teek::UI::VERSION
6
+ spec.authors = ["James Cook"]
7
+ spec.email = ["jcook.rubyist@gmail.com"]
8
+
9
+ spec.summary = "The friendly way to build Tk apps in Ruby"
10
+ spec.description = "Declarative widgets, layout, events, and reactive state on top of teek"
11
+ spec.homepage = "https://github.com/jamescook/teek"
12
+ spec.licenses = ["MIT"]
13
+
14
+ spec.files = Dir.glob("{lib,test}/**/*").select { |f|
15
+ File.file?(f) && f !~ /\.log$/
16
+ } + %w[teek-ui.gemspec CHANGELOG.md README.md]
17
+ spec.require_paths = ["lib"]
18
+ spec.required_ruby_version = ">= 3.2"
19
+
20
+ spec.add_dependency "teek", "~> 0.3"
21
+
22
+ spec.add_development_dependency "rake", "~> 13.0"
23
+ spec.add_development_dependency "minitest", "~> 6.0"
24
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A minimal stand-in for Teek::App, for headless tests that need
4
+ # something Realizer/Handle-shaped without a real Tk interpreter -
5
+ # Realizer#create/#link/#realize_subtree only ever call .command/.bind/
6
+ # .on_close (and, for a menu/window handle, .popup_menu/.window) on
7
+ # whatever app they're given, so a fake recording every call is enough
8
+ # to assert on exactly what WOULD have happened against real Tk. Shared
9
+ # across every headless suite that needs one, so there's a single
10
+ # definition to keep in sync with the real Teek::App/Teek::Window
11
+ # signatures (see test_fake_app_contract.rb) rather than one per file.
12
+ FakeWindow = Struct.new(:path, :modal_calls, :grab_releases) do
13
+ def initialize(path, modal_calls = [], grab_releases = [])
14
+ super
15
+ end
16
+
17
+ def modal(global: false, &block)
18
+ modal_calls << { global: global }
19
+ block.call if block
20
+ end
21
+
22
+ def grab_release
23
+ grab_releases << true
24
+ end
25
+ end
26
+
27
+ FakeApp = Struct.new(:calls, :binds, :on_closes, :popups, :windows) do
28
+ def initialize(calls = [], binds = [], on_closes = [], popups = [], windows = [])
29
+ super
30
+ end
31
+
32
+ def command(cmd, *args, **kwargs)
33
+ calls << [[cmd, *args], kwargs]
34
+ nil
35
+ end
36
+
37
+ def bind(path, event, *subs, &block)
38
+ binds << { path: path, event: event, subs: subs, block: block }
39
+ nil
40
+ end
41
+
42
+ def on_close(window:, &block)
43
+ on_closes << { window: window, block: block }
44
+ nil
45
+ end
46
+
47
+ def popup_menu(menu, x:, y:, entry: nil)
48
+ popups << { menu: menu, x: x, y: y, entry: entry }
49
+ nil
50
+ end
51
+
52
+ def window(path = '.')
53
+ win = FakeWindow.new(path)
54
+ windows << win
55
+ win
56
+ end
57
+ end
data/test/test_busy.rb ADDED
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # session.busy is a thin, realize-only delegation to Teek::App#busy (see
7
+ # teek core's own test/test_busy.rb for the underlying busy-cursor
8
+ # set/clear behavior, including exception safety). What's new to verify
9
+ # at this layer: the delegation itself (window:, block value, exception
10
+ # safety all still work through Session), and that it raises before
11
+ # realize like every other realize-only Session method.
12
+ class TestBusy < Minitest::Test
13
+ include TeekTestHelper
14
+
15
+ def test_busy_raises_before_realize
16
+ assert_tk_app("session.busy should raise a clear error before realize, not queue") do
17
+ require 'teek/ui'
18
+
19
+ session = Teek::UI.app(title: 'Busy Test')
20
+
21
+ assert_raises(Teek::UI::NotRealizedError) { session.busy { } }
22
+ end
23
+ end
24
+
25
+ def test_busy_sets_and_clears_busy_state_and_returns_the_blocks_value
26
+ assert_tk_app("session.busy should show the busy cursor for the duration of the block, then clear it") do
27
+ require 'teek/ui'
28
+
29
+ session = Teek::UI.app(title: 'Busy Test')
30
+ session.run_async
31
+ session.app.update
32
+
33
+ was_busy = nil
34
+ result = session.busy {
35
+ was_busy = session.app.tcl_eval('tk busy status .') == '1'
36
+ 42
37
+ }
38
+
39
+ assert was_busy, "expected the window to be busy during the block"
40
+ assert_equal '0', session.app.tcl_eval('tk busy status .')
41
+ assert_equal 42, result
42
+
43
+ session.app.destroy
44
+ end
45
+ end
46
+
47
+ def test_busy_clears_on_exception
48
+ assert_tk_app("session.busy should clear the busy cursor even if the block raises") do
49
+ require 'teek/ui'
50
+
51
+ session = Teek::UI.app(title: 'Busy Test')
52
+ session.run_async
53
+ session.app.update
54
+
55
+ assert_raises(RuntimeError) { session.busy { raise 'boom' } }
56
+ assert_equal '0', session.app.tcl_eval('tk busy status .')
57
+
58
+ session.app.destroy
59
+ end
60
+ end
61
+
62
+ def test_busy_works_on_a_specific_window
63
+ assert_tk_app("session.busy's window: should target that window, not just the root") do
64
+ require 'teek/ui'
65
+
66
+ session = Teek::UI.app(title: 'Busy Test') { |ui| ui.window(:extra) }
67
+ session.run_async
68
+ session[:extra].show
69
+ session.app.update
70
+ extra_path = session[:extra].path
71
+
72
+ was_busy = nil
73
+ session.busy(window: extra_path) {
74
+ was_busy = session.app.tcl_eval("tk busy status #{extra_path}") == '1'
75
+ }
76
+
77
+ assert was_busy, "expected the extra window to be busy during the block"
78
+
79
+ session.app.destroy
80
+ end
81
+ end
82
+ end