fidgit 0.1.10 → 0.2.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.
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ Fidgit changelog
2
+ ================
3
+
4
+ v0.2.0
5
+ ------
6
+
7
+ * Added editable attribute to TextArea (Allows selection, but not alteration).
8
+ * Added Element#font= and :font option.
9
+ * Added Gosu::Color#colorize to use when using in-line text styling.
10
+ * Managed layout of entities and XML tags (Used by Gosu) in TextArea text better (tags still don't like newlines inside them).
11
+ * Changed license from LGPL to MIT.
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Bil Bas (Spooner)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile CHANGED
@@ -21,7 +21,7 @@ or for options screens and menus within a regular Gosu game.
21
21
 
22
22
  h2. License
23
23
 
24
- GPL v3 (see COPYING.txt)
24
+ MIT (see COPYING.txt)
25
25
 
26
26
  h2. Requirements
27
27
 
@@ -4,22 +4,27 @@ class ExampleState < Fidgit::GuiState
4
4
  def initialize
5
5
  super
6
6
 
7
+ Gosu.register_entity(:entity, Gosu::Image["head_icon.png"])
8
+
9
+ string = "<c=3333ff>Hello, my name</c> is <c=ff0000>Brian</c> the&entity;&entity;snail<c=00ff00>!\nHi\nHello!</c>"
7
10
  horizontal do
8
11
  vertical do
9
- label 'editable'
10
- text_area(width: 200)
12
+ label 'disabled'
13
+ text_area(text: "Can't even select this text", width: 200, enabled: false)
11
14
  end
12
15
 
13
16
  vertical do
14
17
  label 'mirrors to right'
15
- text_area(width: 200, text: "Hello, my name in brian the snail!") do |sender, text|
18
+ text_area(width: 200, text: string) do |_, text|
16
19
  @mirror.text = text
17
20
  end
18
21
  end
19
22
 
20
23
  vertical do
21
- label 'not editable'
22
- @mirror = text_area(width: 200, enabled: false)
24
+ my_label = label 'not editable'
25
+ font = Gosu::Font.new($window, "", my_label.font.height)
26
+ font["a"] = Gosu::Image["head_icon.png"]
27
+ @mirror = text_area(text: string, width: 200, editable: false, font: font)
23
28
  end
24
29
  end
25
30
  end
data/fidgit.gemspec CHANGED
@@ -20,12 +20,13 @@ Gem::Specification.new do |s|
20
20
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
21
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
22
  s.require_paths = ["lib"]
23
+ s.license = "MIT"
23
24
 
24
- s.add_dependency('gosu', '~> 0.7.33')
25
+ s.add_dependency('gosu', '~> 0.7.41')
25
26
  s.add_dependency('chingu', '~> 0.9rc7')
26
27
  s.add_dependency('clipboard', '~> 0.9.9')
27
28
  s.add_dependency('ffi', '= 1.0.9') # 1.0.10 is borked :(
28
- s.add_development_dependency('rspec', '~> 2.1.0')
29
+ s.add_development_dependency('rspec', '~> 2.8.0')
29
30
  s.add_development_dependency('texplay', '~> 0.3.5')
30
31
  s.add_development_dependency('rake')
31
32
  s.add_development_dependency('yard')
@@ -121,6 +121,7 @@ module Fidgit
121
121
  # @option options [String] :tip ('') Tool-tip text
122
122
  # @option options [String, :default] :font_name (:default, which resolves as the default Gosu font)
123
123
  # @option options [String] :font_height (30)
124
+ # @option options [Gosu::Font] :font Use this instead of :font_name and :font_height
124
125
  #
125
126
  # @option options [Gosu::Color] :background_color (transparent)
126
127
  # @option options [Gosu::Color] :border_color (transparent)
@@ -191,11 +192,18 @@ module Fidgit
191
192
  options[:font_name].dup
192
193
  end
193
194
 
194
- @font = Gosu::Font[font_name, options[:font_height]]
195
+ @font = options[:font] || Gosu::Font[font_name, options[:font_height]]
195
196
 
196
197
  @rect = Chingu::Rect.new(options[:x], options[:y], options[:width] || 0, options[:height] || 0)
197
198
  end
198
199
 
200
+ def font=(font)
201
+ raise TypeError unless font.is_a? Gosu::Font
202
+ @font = font
203
+ recalc
204
+ font
205
+ end
206
+
199
207
  def recalc
200
208
  old_width, old_height = width, height
201
209
  layout
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Fidgit
4
4
  class TextArea < Element
5
+ ENTITY_PLACEHOLDER = "*"
6
+
5
7
  # @return [Number]
6
8
  attr_reader :min_height
7
9
  # @return [Number]
@@ -14,13 +16,16 @@ module Fidgit
14
16
  # @return [Boolean]
15
17
  attr_writer :editable
16
18
 
19
+ # @return [String] Text, but stripped of tags.
20
+ attr_reader :stripped_text
21
+
17
22
  event :changed
18
23
  event :focus
19
24
  event :blur
20
25
 
21
- # Is the area editable?
26
+ # Is the area editable? This will always be false if the Element is disabled.
22
27
  def editable?
23
- enabled?
28
+ enabled? and @editable
24
29
  end
25
30
 
26
31
  # Text within the element.
@@ -43,7 +48,7 @@ module Fidgit
43
48
  #
44
49
  # @return [String]
45
50
  def selection_text
46
- text[selection_range]
51
+ stripped_text[selection_range]
47
52
  end
48
53
 
49
54
  # Sets the text within the selection. The caret will be placed at the end of the inserted text.
@@ -52,10 +57,14 @@ module Fidgit
52
57
  # @return [String] The new selection text.
53
58
  def selection_text=(str)
54
59
  from = [@text_input.selection_start, @text_input.caret_pos].min
60
+ to = [@text_input.selection_start, @text_input.caret_pos].max
55
61
  new_length = str.length
56
62
 
57
63
  full_text = text
58
- full_text[selection_range] = str.encode('UTF-8', undef: :replace)
64
+ tags_length_before = (0...from).inject(0) {|m, i| m + @tags[i].length }
65
+ tags_length_inside = (from...to).inject(0) {|m, i| m + @tags[i].length }
66
+ range = (selection_range.first + tags_length_before)...(selection_range.last + tags_length_before + tags_length_inside)
67
+ full_text[range] = str.encode('UTF-8', undef: :replace)
59
68
  @text_input.text = full_text
60
69
 
61
70
  @text_input.selection_start = @text_input.caret_pos = from + new_length
@@ -79,7 +88,7 @@ module Fidgit
79
88
  # @param [Integer] pos Position of caret in the text.
80
89
  # @return [Integer] New position of caret.
81
90
  def caret_position=(position)
82
- raise ArgumentError, "Caret position must be in the range 0 to the length of the text (inclusive)" unless position.between?(0, text.length)
91
+ raise ArgumentError, "Caret position must be in the range 0 to the length of the text (inclusive)" unless position.between?(0, stripped_text.length)
83
92
  @text_input.caret_pos = position
84
93
 
85
94
  position
@@ -105,6 +114,7 @@ module Fidgit
105
114
  # @option options [Integer] :min_height
106
115
  # @option options [Integer] :max_height (Infinite)
107
116
  # @option options [Number] :line_spacing (0)
117
+ # @option options [Boolean] :editable (true)
108
118
  def initialize(options = {}, &block)
109
119
  options = {
110
120
  text: '',
@@ -116,6 +126,7 @@ module Fidgit
116
126
  caret_period: default(:caret_period),
117
127
  focused_border_color: default(:focused, :border_color),
118
128
  selection_color: default(:selection_color),
129
+ editable: true,
119
130
  }.merge! options
120
131
 
121
132
  @line_spacing = options[:line_spacing]
@@ -123,6 +134,7 @@ module Fidgit
123
134
  @caret_period = options[:caret_period]
124
135
  @focused_border_color = options[:focused_border_color].dup
125
136
  @selection_color = options[:selection_color].dup
137
+ @editable = options[:editable]
126
138
 
127
139
  @lines = [''] # List of lines of wrapped text.
128
140
  @caret_positions = [[0, 0]] # [x, y] of each position the caret can be in.
@@ -131,8 +143,10 @@ module Fidgit
131
143
  @old_text = ''
132
144
  @old_caret_position = 0
133
145
  @old_selection_start = 0
146
+ @tags = Hash.new("") # Hash of tags embedded in the text.
134
147
 
135
148
  @text_input.text = options[:text].dup
149
+ @stripped_text = '' # Text stripped of xml tags.
136
150
 
137
151
  super(options)
138
152
 
@@ -195,14 +209,20 @@ module Fidgit
195
209
  # @return [nil]
196
210
  def draw_foreground
197
211
  # Always roll back changes made by the user unless the text is editable.
198
- if not editable? and text != @old_text
199
- @text_input.text = @old_text
200
- @text_input.selection_start = @old_selection_start
201
- self.caret_position = @old_caret_position
202
- else
212
+ if editable? or text == @old_text
203
213
  recalc if focused? # Workaround for Windows draw/update bug.
204
214
  @old_caret_position = caret_position
205
215
  @old_selection_start = @text_input.selection_start
216
+ else
217
+ roll_back
218
+ end
219
+
220
+ if caret_position > stripped_text.length
221
+ self.caret_position = stripped_text.length
222
+ end
223
+
224
+ if @text_input.selection_start >= stripped_text.length
225
+ @text_input.selection_start = stripped_text.length
206
226
  end
207
227
 
208
228
  # Draw the selection.
@@ -236,7 +256,8 @@ module Fidgit
236
256
  # Helper for #recalc
237
257
  # @return [Integer]
238
258
  def position_letters_in_word(word, line_width)
239
- word.each_char do |c|
259
+ # Strip tags before measuring word.
260
+ word.gsub(/<[^>]*>|&[^;];/, '').each_char do |c|
240
261
  char_width = font.text_width(c)
241
262
  line_width += char_width
242
263
  @caret_positions.push [line_width, y_at_line(@lines.size)]
@@ -271,8 +292,23 @@ module Fidgit
271
292
  word = ''
272
293
  word_width = 0
273
294
 
274
- text.each_char do |char|
275
- char_width = (char == "\n") ? 0 : font.text_width(char)
295
+ strip_tags
296
+
297
+ stripped_text.each_char.with_index do |char, i|
298
+ tag = @tags[i]
299
+
300
+ # \x0 is just a place-holder for an entity: &entity;
301
+ if char == ENTITY_PLACEHOLDER
302
+ char = tag
303
+ tag = ""
304
+ end
305
+
306
+ case char
307
+ when "\n"
308
+ char_width = 0
309
+ else
310
+ char_width = font.text_width char
311
+ end
276
312
 
277
313
  overall_width = line_width + (line_width == 0 ? 0 : space_width) + word_width + char_width
278
314
  if overall_width > max_width and not (char == ' ' and not word.empty?)
@@ -282,7 +318,7 @@ module Fidgit
282
318
  position_letters_in_word(word, line_width)
283
319
 
284
320
  # Push as much of the current word as possible as a complete line.
285
- @lines.push word + (char == ' ' ? '' : '-')
321
+ @lines.push word + tag + (char == ' ' ? '' : '-')
286
322
  line_width = font.text_width(word)
287
323
 
288
324
  word = ''
@@ -294,18 +330,18 @@ module Fidgit
294
330
  line = ''
295
331
  end
296
332
 
297
- @char_widths[-1] += (width - line_width - padding_left - padding_right) unless @char_widths.empty?
333
+ widen_last_character line_width
298
334
  line_width = 0
299
335
  end
300
336
 
301
337
  case char
302
338
  when "\n"
303
339
  # A new-line ends the word and puts it on the line.
304
- line += word
340
+ line += word + tag
305
341
  line_width = position_letters_in_word(word, line_width)
306
342
  @caret_positions.push [line_width, y_at_line(@lines.size)]
307
- @char_widths[-1] += (width - line_width - (padding_left + padding_right)) unless @char_widths.empty?
308
343
  @char_widths.push 0
344
+ widen_last_character line_width
309
345
  @lines.push line
310
346
  word = ''
311
347
  word_width = 0
@@ -314,7 +350,7 @@ module Fidgit
314
350
 
315
351
  when ' '
316
352
  # A space ends a word and puts it on the line.
317
- line += word + char
353
+ line += word + tag + char
318
354
  line_width = position_letters_in_word(word, line_width)
319
355
  line_width += space_width
320
356
  @caret_positions.push [line_width, y_at_line(@lines.size)]
@@ -330,14 +366,15 @@ module Fidgit
330
366
  end
331
367
 
332
368
  # Start building up a new word.
333
- word += char
369
+ word += tag + char
334
370
  word_width += char_width
335
371
  end
336
372
  end
337
373
 
338
374
  # Add any remaining word on the last line.
339
375
  unless word.empty?
340
- position_letters_in_word(word, line_width)
376
+ line_width = position_letters_in_word(word, line_width)
377
+ @char_widths << width - line_width - padding_left - padding_right
341
378
  line += word
342
379
  end
343
380
 
@@ -351,25 +388,32 @@ module Fidgit
351
388
  @old_caret_position = caret_position
352
389
  @old_selection_start = @text_input.selection_start
353
390
  else
354
- # Roll back!
355
- @lines = old_lines
356
- @caret_positions = old_caret_positions
357
- @char_widths = old_char_widths
358
- @text_input.text = @old_text
359
- self.caret_position = @old_caret_position
360
- @text_input.selection_start = @old_selection_start
391
+ roll_back
361
392
  end
362
393
 
363
394
  nil
364
395
  end
365
396
 
397
+ protected
398
+ def roll_back
399
+ @text_input.text = @old_text
400
+ self.caret_position = @old_caret_position
401
+ @text_input.selection_start = @old_selection_start
402
+ recalc
403
+ end
404
+
405
+ protected
406
+ def widen_last_character(line_width)
407
+ @char_widths[-1] += (width - line_width - padding_left - padding_right) unless @char_widths.empty?
408
+ end
409
+
366
410
  public
367
411
  # Cut the selection and copy it to the clipboard.
368
412
  def cut
369
413
  str = selection_text
370
- unless selection_text.empty?
414
+ unless str.empty?
371
415
  Clipboard.copy str
372
- self.selection_text = ''
416
+ self.selection_text = '' if editable?
373
417
  end
374
418
  end
375
419
 
@@ -377,9 +421,7 @@ module Fidgit
377
421
  # Copy the selection to the clipboard.
378
422
  def copy
379
423
  str = selection_text
380
- unless selection_text.empty?
381
- Clipboard.copy str
382
- end
424
+ Clipboard.copy str unless str.empty?
383
425
  end
384
426
 
385
427
  public
@@ -393,5 +435,28 @@ module Fidgit
393
435
  def post_init_block(&block)
394
436
  subscribe :changed, &block
395
437
  end
438
+
439
+ protected
440
+ # Strip XML tags and entities ("<c=000000></c>" and "&entity;")
441
+ # @note Entities will mess up the system because we don't know how wide they are.
442
+ def strip_tags
443
+ tags_length = 0
444
+ @tags = Hash.new('')
445
+
446
+ @stripped_text = text.gsub(%r[(<[^>]*?>|&[^;]+;)]) do |tag|
447
+ pos = $`.length - tags_length
448
+ tags_length += tag.length
449
+ @tags[pos] += tag
450
+
451
+ # Entities need to have a non-printing character that can represent them.
452
+ # Still not right, but does mean there are the right number of characters.
453
+ if tag[0] == '&'
454
+ tags_length -= 1
455
+ ENTITY_PLACEHOLDER # Will be expanded later.
456
+ else
457
+ '' # Tags don't use up space, so ignore them.
458
+ end
459
+ end
460
+ end
396
461
  end
397
462
  end
data/lib/fidgit/event.rb CHANGED
@@ -23,23 +23,87 @@ module Fidgit
23
23
  # # JumpingBean jumped 4 metres up
24
24
  #
25
25
  module Event
26
- # @overload subscribe(event, method)
27
- # Add an event handler for an event, using a method.
28
- # @return [nil]
29
- #
30
- # @overload subscribe(event, &block)
31
- # Add an event handler for an event, using a block.
32
- # @return [nil]
26
+ # Created and returned by {Event#subscribe} and can be used to unsubscribe from the event.
27
+ class Subscription
28
+ attr_reader :publisher, :event, :handler
29
+
30
+ def initialize(publisher, event, handler)
31
+ raise TypeError unless publisher.is_a? Event
32
+ raise TypeError unless event.is_a? Symbol
33
+ raise TypeError unless handler.is_a? Proc or handler.is_a? Method
34
+
35
+ @publisher, @event, @handler = publisher, event, handler
36
+ end
37
+
38
+ def unsubscribe
39
+ @publisher.unsubscribe self
40
+ end
41
+ end
42
+
43
+ class << self
44
+ def new_event_handlers
45
+ # Don't use Set, since it is not guaranteed to be ordered.
46
+ Hash.new {|h, k| h[k] = [] }
47
+ end
48
+ end
49
+
50
+ # @return [Subscription] Definition of this the handler created by this subscription, to be used with {#unsubscribe}
33
51
  def subscribe(event, method = nil, &block)
34
52
  raise ArgumentError, "Expected method or block for event handler" unless !block.nil? ^ !method.nil?
35
53
  raise ArgumentError, "#{self.class} does not handle #{event.inspect}" unless events.include? event
36
54
 
37
- @_event_handlers ||= Hash.new() { |hash, key| hash[key] = [] }
38
- @_event_handlers[event].push(method ? method : block)
55
+ @_event_handlers ||= Event.new_event_handlers
56
+ handler = method || block
57
+ @_event_handlers[event] << handler
39
58
 
40
- nil
59
+ Subscription.new self, event, handler
41
60
  end
42
61
 
62
+ # @overload unsubscribe(subscription)
63
+ # Unsubscribe from a #{Subscription}, as returned from {#subscribe}
64
+ # @param subscription [Subscription]
65
+ # @return [Boolean] true if the handler was able to be deleted.
66
+ #
67
+ # @overload unsubscribe(handler)
68
+ # Unsubscribe from first event this handler has been used to subscribe to..
69
+ # @param handler [Block, Method] Event handler used.
70
+ # @return [Boolean] true if the handler was able to be deleted.
71
+ #
72
+ # @overload unsubscribe(event, handler)
73
+ # Unsubscribe from specific handler on particular event.
74
+ # @param event [Symbol] Name of event originally subscribed to.
75
+ # @param handler [Block, Method] Event handler used.
76
+ # @return [Boolean] true if the handler was able to be deleted.
77
+ #
78
+ def unsubscribe(*args)
79
+ @_event_handlers ||= Event.new_event_handlers
80
+
81
+ case args.size
82
+ when 1
83
+ case args.first
84
+ when Subscription
85
+ # Delete specific event handler.
86
+ subscription = args.first
87
+ raise ArgumentError, "Incorrect publisher for #{Subscription}: #{subscription.publisher}" unless subscription.publisher == self
88
+ unsubscribe subscription.event, subscription.handler
89
+ when Proc, Method
90
+ # Delete first events that use the handler.
91
+ handler = args.first
92
+ !!@_event_handlers.find {|_, handlers| handlers.delete handler }
93
+ else
94
+ raise TypeError, "handler must be a #{Subscription}, Block or Method: #{args.first}"
95
+ end
96
+ when 2
97
+ event, handler = args
98
+ raise TypeError, "event name must be a Symbol: #{event}" unless event.is_a? Symbol
99
+ raise TypeError, "handler name must be a Proc/Method: #{handler}" unless handler.is_a? Proc or handler.is_a? Method
100
+ !!@_event_handlers[event].delete(handler)
101
+ else
102
+ raise ArgumentError, "Requires 1..2 arguments, but received #{args.size} arguments"
103
+ end
104
+ end
105
+
106
+
43
107
  # Publish an event to all previously added handlers in the order they were added.
44
108
  # It will automatically call the publishing object with the method named after the event if it is defined
45
109
  # (this will be done before the manually added handlers are called).
@@ -78,15 +142,11 @@ module Fidgit
78
142
  class << base
79
143
  def events
80
144
  # Copy the events already set up for your parent.
81
- unless defined? @events
82
- @events = if superclass.respond_to? :events
83
- superclass.events.dup
84
- else
85
- []
86
- end
87
- end
88
-
89
- @events
145
+ @events ||= if superclass.respond_to? :events
146
+ superclass.events.dup
147
+ else
148
+ []
149
+ end
90
150
  end
91
151
 
92
152
  def event(event)