fidgit 0.1.10 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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)