lacci 0.2.2 → 0.3.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +8 -1
  4. data/lib/lacci/scarpe_cli.rb +2 -1
  5. data/lib/lacci/scarpe_core.rb +2 -1
  6. data/lib/lacci/version.rb +1 -1
  7. data/lib/scarpe/niente/app.rb +23 -0
  8. data/lib/scarpe/niente/display_service.rb +62 -0
  9. data/lib/scarpe/niente/drawable.rb +57 -0
  10. data/lib/scarpe/niente/logger.rb +29 -0
  11. data/lib/scarpe/niente/shoes_spec.rb +87 -0
  12. data/lib/scarpe/niente.rb +20 -0
  13. data/lib/shoes/app.rb +88 -43
  14. data/lib/shoes/background.rb +2 -2
  15. data/lib/shoes/border.rb +2 -2
  16. data/lib/shoes/builtins.rb +63 -0
  17. data/lib/shoes/changelog.rb +52 -0
  18. data/lib/shoes/colors.rb +3 -1
  19. data/lib/shoes/constants.rb +19 -1
  20. data/lib/shoes/display_service.rb +39 -16
  21. data/lib/shoes/download.rb +2 -2
  22. data/lib/shoes/drawable.rb +380 -0
  23. data/lib/shoes/drawables/arc.rb +49 -0
  24. data/lib/shoes/drawables/arrow.rb +41 -0
  25. data/lib/shoes/drawables/button.rb +73 -0
  26. data/lib/shoes/{widgets → drawables}/check.rb +5 -4
  27. data/lib/shoes/{widgets → drawables}/document_root.rb +3 -3
  28. data/lib/shoes/{widgets → drawables}/edit_box.rb +6 -6
  29. data/lib/shoes/{widgets → drawables}/edit_line.rb +6 -6
  30. data/lib/shoes/{widgets → drawables}/flow.rb +6 -6
  31. data/lib/shoes/{widgets → drawables}/image.rb +6 -6
  32. data/lib/shoes/{widgets → drawables}/line.rb +7 -5
  33. data/lib/shoes/drawables/link.rb +34 -0
  34. data/lib/shoes/drawables/list_box.rb +56 -0
  35. data/lib/shoes/drawables/para.rb +118 -0
  36. data/lib/shoes/drawables/progress.rb +14 -0
  37. data/lib/shoes/drawables/radio.rb +33 -0
  38. data/lib/shoes/drawables/rect.rb +17 -0
  39. data/lib/shoes/{widgets → drawables}/shape.rb +6 -7
  40. data/lib/shoes/{widgets → drawables}/slot.rb +32 -20
  41. data/lib/shoes/{widgets → drawables}/span.rb +8 -7
  42. data/lib/shoes/{widgets → drawables}/stack.rb +6 -4
  43. data/lib/shoes/drawables/star.rb +50 -0
  44. data/lib/shoes/drawables/subscription_item.rb +93 -0
  45. data/lib/shoes/drawables/text_drawable.rb +63 -0
  46. data/lib/shoes/drawables/video.rb +16 -0
  47. data/lib/shoes/drawables/widget.rb +69 -0
  48. data/lib/shoes/drawables.rb +31 -0
  49. data/lib/shoes/errors.rb +28 -0
  50. data/lib/shoes/log.rb +2 -2
  51. data/lib/shoes/ruby_extensions.rb +15 -0
  52. data/lib/shoes/spacing.rb +2 -2
  53. data/lib/shoes-spec.rb +93 -0
  54. data/lib/shoes.rb +27 -7
  55. metadata +55 -28
  56. data/lib/shoes/widget.rb +0 -218
  57. data/lib/shoes/widgets/alert.rb +0 -19
  58. data/lib/shoes/widgets/arc.rb +0 -51
  59. data/lib/shoes/widgets/button.rb +0 -35
  60. data/lib/shoes/widgets/font.rb +0 -14
  61. data/lib/shoes/widgets/link.rb +0 -25
  62. data/lib/shoes/widgets/list_box.rb +0 -25
  63. data/lib/shoes/widgets/para.rb +0 -68
  64. data/lib/shoes/widgets/radio.rb +0 -35
  65. data/lib/shoes/widgets/star.rb +0 -44
  66. data/lib/shoes/widgets/subscription_item.rb +0 -60
  67. data/lib/shoes/widgets/text_widget.rb +0 -51
  68. data/lib/shoes/widgets/video.rb +0 -15
  69. data/lib/shoes/widgets.rb +0 -29
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shoes has a number of built-in methods that are intended to be available everywhere,
4
+ # in every Shoes and non-Shoes class, for every Shoes app.
5
+ module Shoes::Builtins
6
+ # Register the given font with Shoes so that text that wants it can use it.
7
+ # Also add it to the FONTS constant.
8
+ #
9
+ # @param font_file_or_url [String] the filename or URL for the font
10
+ # @return [void]
11
+ def font(font_file_or_url)
12
+ shoes_builtin("font", font_file_or_url)
13
+
14
+ font_name = File.basename(font_file_or_url, ".*")
15
+ Shoes::FONTS << font_name
16
+ end
17
+
18
+ def ask(message_string)
19
+ shoes_builtin("ask", message_string)
20
+ end
21
+
22
+ def alert(message)
23
+ shoes_builtin("alert", message)
24
+ end
25
+
26
+ def ask_color(title_bar)
27
+ shoes_builtin("ask_color", title_bar)
28
+ end
29
+
30
+ def ask_open_file()
31
+ shoes_builtin("ask_open_file")
32
+ end
33
+
34
+ def ask_save_file()
35
+ shoes_builtin("ask_save_file")
36
+ end
37
+
38
+ def ask_open_folder()
39
+ shoes_builtin("ask_open_folder")
40
+ end
41
+
42
+ def ask_save_folder()
43
+ shoes_builtin("ask_save_folder")
44
+ end
45
+
46
+ def confirm(question)
47
+ shoes_builtin("confirm", question)
48
+ end
49
+
50
+ # TO ADD: debug, error, info, warn
51
+ # TO VERIFY OR ADD: gradient, gray, rgb
52
+
53
+ private
54
+
55
+ def shoes_builtin(cmd_name, *args)
56
+ Shoes::DisplayService.dispatch_event("builtin", nil, cmd_name, args)
57
+ nil
58
+ end
59
+ end
60
+
61
+ module Kernel
62
+ include Shoes::Builtins
63
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "log"
4
+ # require "scarpe/components/modular_logger"
5
+
6
+ class Shoes
7
+ class Changelog
8
+ # include Shoes::Log
9
+
10
+ def initialize
11
+ #TODO : refer to https://github.com/scarpe-team/scarpe/pull/400
12
+ # and figure out how to use scarpe logger here without getting duplicate or nil error
13
+ # Shoes::Log.instance = Scarpe::Components::ModularLogImpl.new
14
+ # log_init("Changelog")
15
+ end
16
+
17
+ def get_latest_release_info
18
+ root_dir = File.dirname(__FILE__, 4) # this duplicates constants.rb, but how to share?
19
+
20
+ git_dir = "#{root_dir}/.git"
21
+ revision = nil
22
+ if File.exist?(git_dir)
23
+ revision = `git rev-parse HEAD`.chomp
24
+ end
25
+
26
+ changelog_file = "#{root_dir}/CHANGELOG.md"
27
+ if File.exist?(changelog_file)
28
+ changelog_content = File.read(changelog_file)
29
+ release_name_pattern = /^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2}) - (\w+)$/m
30
+ release_matches = changelog_content.scan(release_name_pattern)
31
+ latest_release = release_matches.max_by { |version, _date, _name| Gem::Version.new(version) }
32
+
33
+ if latest_release
34
+ #puts "Found release #{latest_release[0]} in CHANGELOG.md"
35
+ # @log.debug("Found release #{latest_release[0]} in CHANGELOG.md") # Logger isn't initialized yet
36
+ version_parts = latest_release[0].split(".").map(&:to_i)
37
+ rel_id = ("%02d%02d%02d" % version_parts).to_i
38
+
39
+ return({
40
+ RELEASE_NAME: latest_release[2],
41
+ RELEASE_BUILD_DATE: latest_release[1],
42
+ RELEASE_ID: rel_id,
43
+ REVISION: revision,
44
+ })
45
+ end
46
+ end
47
+
48
+ puts "No release found in CHANGELOG.md"
49
+ { RELEASE_NAME: nil, RELEASE_BUILD_DATE: nil, RELEASE_ID: nil, REVISION: revision }
50
+ end
51
+ end
52
+ end
data/lib/shoes/colors.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Shoes
3
+ class Shoes
4
4
  module Colors
5
+ extend self
6
+
5
7
  COLORS = {
6
8
  aliceblue: [240, 248, 255],
7
9
  antiquewhite: [250, 235, 215],
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Shoes
3
+ require_relative "changelog"
4
+ class Shoes
4
5
  module Constants
5
6
  def self.find_lib_dir
6
7
  begin
@@ -14,16 +15,33 @@ module Shoes
14
15
  [ENV["HOME"], ".shoes"],
15
16
  [Dir.tmpdir, "shoes"],
16
17
  ]
18
+
17
19
  top, file = homes.detect { |home_top, _| home_top && File.exist?(home_top) }
18
20
  File.join(top, file)
19
21
  end
20
22
 
23
+ # A temp dir for Shoes app files
21
24
  LIB_DIR = find_lib_dir
22
25
 
26
+ # the Shoes library dir
27
+ DIR = File.dirname(__FILE__, 4)
28
+
23
29
  # Math constants from Shoes3
24
30
  RAD2PI = 0.01745329251994329577
25
31
  TWO_PI = 6.28318530717958647693
26
32
  HALF_PI = 1.57079632679489661923
27
33
  PI = 3.14159265358979323846
34
+
35
+ # This should be set up by the Display Service when it loads
36
+ FONTS = []
28
37
  end
38
+
39
+ # Access and assign the release constants
40
+ changelog_instance = Shoes::Changelog.new
41
+ RELEASE_INFO = changelog_instance.get_latest_release_info
42
+ RELEASE_NAME = RELEASE_INFO[:RELEASE_NAME]
43
+ RELEASE_ID = RELEASE_INFO[:RELEASE_ID]
44
+ RELEASE_BUILD_DATE = RELEASE_INFO[:RELEASE_BUILD_DATE]
45
+ RELEASE_TYPE = "LOOSE_SHOES" # This isn't really a thing any more
46
+ REVISION = RELEASE_INFO[:REVISION]
29
47
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Lacci Shoes apps operate in multiple layers. A Shoes widget tree exists as fairly
4
- # plain, simple Ruby objects. And then a display-service widget tree integrates with
3
+ # Lacci Shoes apps operate in multiple layers. A Shoes drawable tree exists as fairly
4
+ # plain, simple Ruby objects. And then a display-service drawable tree integrates with
5
5
  # the display technology. This lets us use Ruby as our API while
6
6
  # not tying it too closely to the limitations of Webview, WASM, LibUI, etc.
7
7
  #
@@ -15,18 +15,18 @@
15
15
  #
16
16
  # ## Events
17
17
  #
18
- # Events are a lot of what tie the Shoes widgets and the display service together.
18
+ # Events are a lot of what tie the Shoes drawables and the display service together.
19
19
  #
20
- # Shoes widgets *expect* to operate in a fairly "hands off" mode where they record
20
+ # Shoes drawables *expect* to operate in a fairly "hands off" mode where they record
21
21
  # to an event queue to send to the display service, and the display service records
22
22
  # events to send back.
23
23
  #
24
24
  # When a Shoes handler takes an action (e.g. some_para.replace(),) the relevant
25
25
  # call will be dispatched as a :display event, to be sent to the display service.
26
26
  # And when a display-side event occurs (e.g. user pushes a button,) it will be
27
- # dispatched as a :shoes event, to be sent to the Shoes tree of widgets.
27
+ # dispatched as a :shoes event, to be sent to the Shoes tree of drawables.
28
28
  #
29
- module Shoes
29
+ class Shoes
30
30
  class DisplayService
31
31
  class << self
32
32
  # This is in the eigenclass/metaclass, *not* instances of DisplayService
@@ -128,22 +128,32 @@ module Shoes
128
128
 
129
129
  # These methods are an interface to DisplayService objects.
130
130
 
131
- def create_display_widget_for(widget_class_name, widget_id, properties)
131
+ def create_display_drawable_for(drawable_class_name, drawable_id, properties, is_widget:)
132
132
  raise "Override in DisplayService implementation!"
133
133
  end
134
134
 
135
- def set_widget_pairing(id, display_widget)
136
- @display_widget_for ||= {}
137
- @display_widget_for[id] = display_widget
135
+ def set_drawable_pairing(id, display_drawable)
136
+ if id.nil?
137
+ raise Shoes::Errors::BadLinkableIdError, "Linkable ID may not be nil!"
138
+ end
139
+
140
+ @display_drawable_for ||= {}
141
+ if @display_drawable_for[id]
142
+ raise Shoes::Errors::DuplicateCreateDrawableError, "There is already a drawable for #{id.inspect}! Not setting a new one."
143
+ end
144
+
145
+ @display_drawable_for[id] = display_drawable
146
+ nil
138
147
  end
139
148
 
140
- def query_display_widget_for(id, nil_ok: false)
141
- display_widget = @display_widget_for[id]
142
- unless display_widget || nil_ok
143
- raise "Could not find display widget for linkable ID #{id.inspect}!"
149
+ def query_display_drawable_for(id, nil_ok: false)
150
+ @display_drawable_for ||= {}
151
+ display_drawable = @display_drawable_for[id]
152
+ unless display_drawable || nil_ok
153
+ raise "Could not find display drawable for linkable ID #{id.inspect}!"
144
154
  end
145
155
 
146
- display_widget
156
+ display_drawable
147
157
  end
148
158
 
149
159
  def destroy
@@ -163,6 +173,8 @@ module Shoes
163
173
 
164
174
  def initialize(linkable_id: object_id)
165
175
  @linkable_id = linkable_id
176
+ @subscriptions = {}
177
+ @display_drawable_for ||= {}
166
178
  end
167
179
 
168
180
  def send_self_event(*args, event_name:, **kwargs)
@@ -174,11 +186,22 @@ module Shoes
174
186
  end
175
187
 
176
188
  def bind_shoes_event(event_name:, target: nil, &handler)
177
- DisplayService.subscribe_to_event(event_name, target, &handler)
189
+ sub = DisplayService.subscribe_to_event(event_name, target, &handler)
190
+ @subscriptions[sub] = true
191
+ sub
178
192
  end
179
193
 
180
194
  def unsub_shoes_event(unsub_id)
195
+ unless @subscriptions[unsub_id]
196
+ $stderr.puts "Unsubscribing from event that isn't in subscriptions! #{unsub_id.inspect}"
197
+ end
181
198
  DisplayService.unsub_from_events(unsub_id)
199
+ @subscriptions.delete unsub_id
200
+ end
201
+
202
+ def unsub_all_shoes_events
203
+ @subscriptions.keys.each { |k| DisplayService.unsub_from_events(k) }
204
+ @subscriptions.clear
182
205
  end
183
206
  end
184
207
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Shoes
4
- class Widget
3
+ class Shoes
4
+ class Drawable
5
5
  class ResponseWrapper
6
6
  attr_reader :response
7
7
 
@@ -0,0 +1,380 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shoes
4
+ # Shoes::Drawable
5
+ #
6
+ # This is the display-service portable Shoes Drawable interface. Visible Shoes
7
+ # drawables like buttons inherit from this. Compound drawables made of multiple
8
+ # different smaller Drawables inherit from it in their various apps or libraries.
9
+ # The Shoes Drawable helps build a Shoes-side drawable tree, with parents and
10
+ # children. Any API that applies to all drawables (e.g. remove) should be
11
+ # defined here.
12
+ #
13
+ class Drawable < Shoes::Linkable
14
+ include Shoes::Log
15
+ include Shoes::Colors
16
+
17
+ # All Drawables have these so they go in Shoes::Drawable and are inherited
18
+ @shoes_events = ["parent", "destroy", "prop_change"]
19
+
20
+ class << self
21
+ attr_accessor :drawable_classes
22
+ attr_accessor :drawable_default_styles
23
+ attr_accessor :widget_classes
24
+
25
+ def inherited(subclass)
26
+ Shoes::Drawable.drawable_classes ||= []
27
+ Shoes::Drawable.drawable_classes << subclass
28
+
29
+ Shoes::Drawable.drawable_default_styles ||= {}
30
+ Shoes::Drawable.drawable_default_styles[subclass] = {}
31
+
32
+ Shoes::Drawable.widget_classes ||= []
33
+ if subclass < Shoes::Widget
34
+ Shoes::Drawable.widget_classes << subclass.name
35
+ end
36
+
37
+ super
38
+ end
39
+
40
+ def dsl_name
41
+ n = name.split("::").last.chomp("Drawable")
42
+ n.gsub(/(.)([A-Z])/, '\1_\2').downcase
43
+ end
44
+
45
+ def drawable_class_by_name(name)
46
+ drawable_classes.detect { |k| k.dsl_name == name.to_s }
47
+ end
48
+
49
+ def is_widget_class?(name)
50
+ !!Shoes::Drawable.widget_classes.intersect?([name.to_s])
51
+ end
52
+
53
+ def validate_as(prop_name, value)
54
+ prop_name = prop_name.to_s
55
+ hashes = shoes_style_hashes
56
+
57
+ h = hashes.detect { |hash| hash[:name] == prop_name }
58
+ raise(Shoes::Errors::NoSuchStyleError, "Can't find property #{prop_name.inspect} in #{self} property list: #{hashes.inspect}!") unless h
59
+
60
+ return value if h[:validator].nil?
61
+
62
+ h[:validator].call(value)
63
+ end
64
+
65
+ # Return a list of Shoes events for this class.
66
+ #
67
+ # @return Array[String] the list of event names
68
+ def get_shoes_events
69
+ if @shoes_events.nil?
70
+ raise UnknownEventsForClass, "Drawable type #{self.class} hasn't defined its list of Shoes events!"
71
+ end
72
+
73
+ @shoes_events
74
+ end
75
+
76
+ # Set the list of Shoes event names that are allowed for this class.
77
+ #
78
+ # @param args [Array] an array of event names, which will be coerced to Strings
79
+ # @return [void]
80
+ def shoes_events(*args)
81
+ @shoes_events ||= args.map(&:to_s) + self.superclass.get_shoes_events
82
+ end
83
+
84
+ # Assign a new Shoes Drawable ID number, starting from 1.
85
+ # This allows non-overlapping small integer IDs for Shoes
86
+ # linkable IDs - the number part of making it clear what
87
+ # widget you're talking about.
88
+ def allocate_drawable_id
89
+ @drawable_id_counter ||= 0
90
+ @drawable_id_counter += 1
91
+ @drawable_id_counter
92
+ end
93
+
94
+ def register_drawable_id(id, drawable)
95
+ @drawables_by_id ||= {}
96
+ @drawables_by_id[id] = drawable
97
+ end
98
+
99
+ def unregister_drawable_id(id)
100
+ @drawables_by_id ||= {}
101
+ @drawables_by_id.delete(id)
102
+ end
103
+
104
+ def drawable_by_id(id, none_ok: false)
105
+ val = @drawables_by_id[id]
106
+ unless val || none_ok
107
+ raise "No Drawable Found! #{@drawables_by_id.inspect}"
108
+ end
109
+
110
+ val
111
+ end
112
+
113
+ private
114
+
115
+ def linkable_properties
116
+ @linkable_properties ||= []
117
+ end
118
+
119
+ def linkable_properties_hash
120
+ @linkable_properties_hash ||= {}
121
+ end
122
+
123
+ public
124
+
125
+ # Shoes styles in Shoes Linkables are automatically sync'd with the display side objects.
126
+ # If a block is passed to shoes_style, that's the validation for the property. It should
127
+ # convert a given value to a valid value for the property or throw an exception.
128
+ def shoes_style(name, &validator)
129
+ name = name.to_s
130
+
131
+ return if linkable_properties_hash[name]
132
+
133
+ linkable_properties << { name: name, validator: }
134
+ linkable_properties_hash[name] = true
135
+ end
136
+
137
+ # Add these names as Shoes styles
138
+ def shoes_styles(*names)
139
+ names.each { |n| shoes_style(n) }
140
+ end
141
+
142
+ def shoes_style_names
143
+ parent_prop_names = self != Shoes::Drawable ? self.superclass.shoes_style_names : []
144
+
145
+ parent_prop_names | linkable_properties.map { |prop| prop[:name] }
146
+ end
147
+
148
+ def shoes_style_hashes
149
+ parent_hashes = self != Shoes::Drawable ? self.superclass.shoes_style_hashes : []
150
+
151
+ parent_hashes + linkable_properties
152
+ end
153
+
154
+ def shoes_style_name?(name)
155
+ linkable_properties_hash[name.to_s] ||
156
+ (self != Shoes::Drawable && superclass.shoes_style_name?(name))
157
+ end
158
+ end
159
+
160
+ # Shoes uses a "hidden" style property for hide/show
161
+ shoes_style :hidden
162
+
163
+ attr_reader :debug_id
164
+
165
+ def initialize(*args, **kwargs)
166
+ log_init("Shoes::#{self.class.name}")
167
+
168
+ default_styles = Shoes::Drawable.drawable_default_styles[self.class]
169
+
170
+ self.class.shoes_style_names.each do |prop|
171
+ prop_sym = prop.to_sym
172
+ if kwargs.key?(prop_sym)
173
+ val = self.class.validate_as(prop, kwargs[prop_sym])
174
+ instance_variable_set("@" + prop, val)
175
+ elsif default_styles.key?(prop_sym)
176
+ val = self.class.validate_as(prop, default_styles[prop_sym])
177
+ instance_variable_set("@" + prop, val)
178
+ end
179
+ end
180
+
181
+ super(linkable_id: Shoes::Drawable.allocate_drawable_id)
182
+ Shoes::Drawable.register_drawable_id(self.linkable_id, self)
183
+
184
+ generate_debug_id
185
+ end
186
+
187
+ # Calling stack.app or drawable.app will execute the block
188
+ # with the Shoes::App as self, and with that stack or
189
+ # flow as the current slot.
190
+ #
191
+ # @incompatibility In Shoes Classic this is the only way
192
+ # to change self, while Scarpe will also change self
193
+ # with the other Slot Manipulation methods: #clear,
194
+ # #append, #prepend, #before and #after.
195
+ #
196
+ # @return [Shoes::App] the Shoes app
197
+ # @yield the block to call with the Shoes App as self
198
+ def app(&block)
199
+ Shoes::App.instance.with_slot(self, &block) if block_given?
200
+ Shoes::App.instance
201
+ end
202
+
203
+ private
204
+
205
+ def generate_debug_id
206
+ cl = caller_locations(3)
207
+ da = cl.detect { |loc| !loc.path.include?("lacci/lib/shoes") }
208
+ @drawable_defined_at = "#{File.basename(da.path)}:#{da.lineno}"
209
+
210
+ class_name = self.class.name.split("::")[-1]
211
+
212
+ @debug_id = "#{class_name}##{@linkable_id}(#{@drawable_defined_at})"
213
+ end
214
+
215
+ public
216
+
217
+ def inspect
218
+ "#<#{debug_id} " +
219
+ " @parent=#{@parent ? @parent.debug_id : "(none)"} " +
220
+ "@children=#{@children ? @children.map(&:debug_id) : "(none)"} properties=#{shoes_style_values.inspect}>"
221
+ end
222
+
223
+ private
224
+
225
+ def validate_event_name(event_name)
226
+ unless self.class.get_shoes_events.include?(event_name.to_s)
227
+ raise Shoes::UnregisteredShoesEvent, "Drawable #{self.inspect} tried to bind Shoes event #{event_name}, which is not in #{evetns.inspect}!"
228
+ end
229
+ end
230
+
231
+ def bind_self_event(event_name, &block)
232
+ raise(Shoes::Errors::NoLinkableIdError, "Drawable has no linkable_id! #{inspect}") unless linkable_id
233
+
234
+ validate_event_name(event_name)
235
+
236
+ bind_shoes_event(event_name: event_name, target: linkable_id, &block)
237
+ end
238
+
239
+ def bind_no_target_event(event_name, &block)
240
+ validate_event_name(event_name)
241
+
242
+ bind_shoes_event(event_name:, &block)
243
+ end
244
+
245
+ public
246
+
247
+ def event(event_name, *args, **kwargs)
248
+ validate_event_name(event_name)
249
+
250
+ send_shoes_event(*args, **kwargs, event_name:, target: linkable_id)
251
+ end
252
+
253
+ def shoes_style_values
254
+ all_property_names = self.class.shoes_style_names
255
+
256
+ properties = {}
257
+ all_property_names.each do |prop|
258
+ properties[prop] = instance_variable_get("@" + prop)
259
+ end
260
+ properties["shoes_linkable_id"] = self.linkable_id
261
+ properties
262
+ end
263
+
264
+ def style(*args, **kwargs)
265
+ if args.empty? && kwargs.empty?
266
+ # Just called as .style()
267
+ shoes_style_values
268
+ elsif args.empty?
269
+ # This is called to set one or more Shoes styles
270
+ prop_names = self.class.shoes_style_names
271
+ unknown_styles = kwargs.keys.select { |k| !prop_names.include?(k.to_s) }
272
+ unless unknown_styles.empty?
273
+ raise Shoes::Errors::NoSuchStyleError, "Unknown styles for drawable type #{self.class.name}: #{unknown_styles.join(", ")}"
274
+ end
275
+
276
+ kwargs.each do |name, val|
277
+ instance_variable_set("@#{name}", val)
278
+ end
279
+ elsif args.length == 1 && args[0] < Shoes::Drawable
280
+ # Shoes supports calling .style with a Shoes class, e.g. .style(Shoes::Button, displace_left: 5)
281
+ kwargs.each do |name, val|
282
+ Shoes::Drawable.drawable_default_styles[args[0]][name.to_sym] = val
283
+ end
284
+ else
285
+ raise Shoes::Errors::InvalidAttributeValueError, "Unexpected arguments to style! args: #{args.inspect}, keyword args: #{kwargs.inspect}"
286
+ end
287
+ end
288
+
289
+ private
290
+
291
+ def create_display_drawable
292
+ klass_name = self.class.name.delete_prefix("Scarpe::").delete_prefix("Shoes::")
293
+
294
+ is_widget = Shoes::Drawable.is_widget_class?(klass_name)
295
+
296
+ # Should we send an event so this can be discovered from someplace other than
297
+ # the DisplayService?
298
+ ::Shoes::DisplayService.display_service.create_display_drawable_for(klass_name, self.linkable_id, shoes_style_values, is_widget:)
299
+ end
300
+
301
+ public
302
+
303
+ attr_reader :parent
304
+ attr_reader :destroyed
305
+
306
+ def set_parent(new_parent)
307
+ @parent&.remove_child(self)
308
+ new_parent&.add_child(self)
309
+ @parent = new_parent
310
+ send_shoes_event(new_parent.linkable_id, event_name: "parent", target: linkable_id)
311
+ end
312
+
313
+ # Removes the element from the Shoes::Drawable tree and removes all event subscriptions
314
+ def destroy
315
+ @parent&.remove_child(self)
316
+ @parent = nil
317
+ @destroyed = true
318
+ unsub_all_shoes_events
319
+ send_shoes_event(event_name: "destroy", target: linkable_id)
320
+ Shoes::Drawable.unregister_drawable_id(linkable_id)
321
+ end
322
+ alias_method :remove, :destroy
323
+
324
+ # Hide the drawable.
325
+ def hide
326
+ self.hidden = true
327
+ end
328
+
329
+ # Show the drawable.
330
+ def show
331
+ self.hidden = false
332
+ end
333
+
334
+ # Hide the drawable if it is currently shown. Show it if it is currently hidden.
335
+ def toggle
336
+ self.hidden = !self.hidden
337
+ end
338
+
339
+ # We use method_missing to auto-create Shoes style getters and setters.
340
+ def method_missing(name, *args, **kwargs, &block)
341
+ name_s = name.to_s
342
+
343
+ if name_s[-1] == "="
344
+ prop_name = name_s[0..-2]
345
+ if self.class.shoes_style_name?(prop_name)
346
+ self.class.define_method(name) do |new_value|
347
+ raise(Shoes::Errors::NoLinkableIdError, "Trying to set Shoes styles in an object with no linkable ID! #{inspect}") unless linkable_id
348
+
349
+ new_value = self.class.validate_as(prop_name, new_value)
350
+ instance_variable_set("@" + prop_name, new_value)
351
+ send_shoes_event({ prop_name => new_value }, event_name: "prop_change", target: linkable_id)
352
+ end
353
+
354
+ return self.send(name, *args, **kwargs, &block)
355
+ end
356
+ end
357
+
358
+ if self.class.shoes_style_name?(name_s)
359
+ self.class.define_method(name) do
360
+ raise(Shoes::Errors::NoLinkableIdError, "Trying to get Shoes styles in an object with no linkable ID! #{inspect}") unless linkable_id
361
+
362
+ instance_variable_get("@" + name_s)
363
+ end
364
+
365
+ return self.send(name, *args, **kwargs, &block)
366
+ end
367
+
368
+ super(name, *args, **kwargs, &block)
369
+ end
370
+
371
+ def respond_to_missing?(name, include_private = false)
372
+ name_s = name.to_s
373
+ return true if self.class.shoes_style_name?(name_s)
374
+ return true if self.class.shoes_style_name?(name_s[0..-2]) && name_s[-1] == "="
375
+ return true if Drawable.drawable_class_by_name(name_s)
376
+
377
+ super
378
+ end
379
+ end
380
+ end