colstrom-fidgit 0.2.7

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 (92) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +31 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.md +154 -0
  8. data/Rakefile +38 -0
  9. data/config/default_schema.yml +216 -0
  10. data/examples/_all_examples.rb +9 -0
  11. data/examples/align_example.rb +56 -0
  12. data/examples/button_and_toggle_button_example.rb +38 -0
  13. data/examples/color_picker_example.rb +17 -0
  14. data/examples/color_well_example.rb +25 -0
  15. data/examples/combo_box_example.rb +24 -0
  16. data/examples/file_dialog_example.rb +42 -0
  17. data/examples/grid_packer_example.rb +29 -0
  18. data/examples/helpers/example_window.rb +17 -0
  19. data/examples/label_example.rb +23 -0
  20. data/examples/list_example.rb +23 -0
  21. data/examples/media/images/head_icon.png +0 -0
  22. data/examples/menu_pane_example.rb +27 -0
  23. data/examples/message_dialog_example.rb +65 -0
  24. data/examples/radio_button_example.rb +37 -0
  25. data/examples/readme_example.rb +32 -0
  26. data/examples/scroll_window_example.rb +49 -0
  27. data/examples/slider_example.rb +34 -0
  28. data/examples/splash_example.rb +42 -0
  29. data/examples/text_area_example.rb +33 -0
  30. data/fidgit.gemspec +35 -0
  31. data/lib/fidgit.rb +51 -0
  32. data/lib/fidgit/chingu_ext/window.rb +6 -0
  33. data/lib/fidgit/cursor.rb +38 -0
  34. data/lib/fidgit/elements/button.rb +113 -0
  35. data/lib/fidgit/elements/color_picker.rb +63 -0
  36. data/lib/fidgit/elements/color_well.rb +39 -0
  37. data/lib/fidgit/elements/combo_box.rb +115 -0
  38. data/lib/fidgit/elements/composite.rb +17 -0
  39. data/lib/fidgit/elements/container.rb +210 -0
  40. data/lib/fidgit/elements/element.rb +298 -0
  41. data/lib/fidgit/elements/file_browser.rb +152 -0
  42. data/lib/fidgit/elements/grid.rb +227 -0
  43. data/lib/fidgit/elements/group.rb +64 -0
  44. data/lib/fidgit/elements/horizontal.rb +12 -0
  45. data/lib/fidgit/elements/image_frame.rb +65 -0
  46. data/lib/fidgit/elements/label.rb +85 -0
  47. data/lib/fidgit/elements/list.rb +47 -0
  48. data/lib/fidgit/elements/main_packer.rb +25 -0
  49. data/lib/fidgit/elements/menu_pane.rb +163 -0
  50. data/lib/fidgit/elements/packer.rb +42 -0
  51. data/lib/fidgit/elements/radio_button.rb +86 -0
  52. data/lib/fidgit/elements/scroll_area.rb +68 -0
  53. data/lib/fidgit/elements/scroll_bar.rb +128 -0
  54. data/lib/fidgit/elements/scroll_window.rb +83 -0
  55. data/lib/fidgit/elements/slider.rb +125 -0
  56. data/lib/fidgit/elements/text_area.rb +494 -0
  57. data/lib/fidgit/elements/text_line.rb +92 -0
  58. data/lib/fidgit/elements/toggle_button.rb +67 -0
  59. data/lib/fidgit/elements/tool_tip.rb +35 -0
  60. data/lib/fidgit/elements/vertical.rb +12 -0
  61. data/lib/fidgit/event.rb +159 -0
  62. data/lib/fidgit/gosu_ext/color.rb +136 -0
  63. data/lib/fidgit/gosu_ext/gosu_module.rb +25 -0
  64. data/lib/fidgit/history.rb +91 -0
  65. data/lib/fidgit/redirector.rb +83 -0
  66. data/lib/fidgit/schema.rb +123 -0
  67. data/lib/fidgit/selection.rb +106 -0
  68. data/lib/fidgit/standard_ext/hash.rb +21 -0
  69. data/lib/fidgit/states/dialog_state.rb +52 -0
  70. data/lib/fidgit/states/file_dialog.rb +24 -0
  71. data/lib/fidgit/states/gui_state.rb +331 -0
  72. data/lib/fidgit/states/message_dialog.rb +61 -0
  73. data/lib/fidgit/version.rb +5 -0
  74. data/lib/fidgit/window.rb +19 -0
  75. data/media/images/arrow.png +0 -0
  76. data/media/images/combo_arrow.png +0 -0
  77. data/media/images/file_directory.png +0 -0
  78. data/media/images/file_file.png +0 -0
  79. data/media/images/pixel.png +0 -0
  80. data/spec/fidgit/elements/helpers/helper.rb +3 -0
  81. data/spec/fidgit/elements/helpers/tex_play_helper.rb +9 -0
  82. data/spec/fidgit/elements/image_frame_spec.rb +69 -0
  83. data/spec/fidgit/elements/label_spec.rb +37 -0
  84. data/spec/fidgit/event_spec.rb +210 -0
  85. data/spec/fidgit/gosu_ext/color_spec.rb +130 -0
  86. data/spec/fidgit/gosu_ext/helpers/helper.rb +3 -0
  87. data/spec/fidgit/helpers/helper.rb +4 -0
  88. data/spec/fidgit/history_spec.rb +153 -0
  89. data/spec/fidgit/redirector_spec.rb +78 -0
  90. data/spec/fidgit/schema_spec.rb +67 -0
  91. data/spec/fidgit/schema_test.yml +32 -0
  92. metadata +320 -0
@@ -0,0 +1,68 @@
1
+ # encoding: utf-8
2
+
3
+ module Fidgit
4
+ # A basic scrolling area. It is not managed in any way (use ScrollWindow for that).
5
+ class ScrollArea < Container
6
+ # @return [Vertical] The content shown within this ScrollArea
7
+ attr_reader :content
8
+
9
+ def offset_x; x - @content.x; end
10
+ def offset_y; y - @content.y; end
11
+
12
+ def offset_x=(value)
13
+ @content.x = x - [[@content.width - width, value].min, 0].max
14
+ end
15
+
16
+ def offset_y=(value)
17
+ @content.y = y - [[@content.height - height, value].min, 0].max
18
+ end
19
+
20
+ # @option options [Number] :offset (0)
21
+ # @option options [Number] :offset_x (value of :offset option)
22
+ # @option options [Number] :offset_y (value of :offset option)
23
+ # @option options [Element] :owner The owner of the content, such as the scroll-window containing the content.
24
+ def initialize(options = {})
25
+ options = {
26
+ offset: 0,
27
+ owner: nil,
28
+ }.merge! options
29
+
30
+ @owner = options[:owner]
31
+
32
+ super(options)
33
+
34
+ @content = Vertical.new(parent: self, padding: 0)
35
+
36
+ self.offset_x = options[:offset_x] || options[:offset]
37
+ self.offset_y = options[:offset_y] || options[:offset]
38
+ end
39
+
40
+ def hit_element(x, y)
41
+ # Only pass on mouse events if they are inside the window.
42
+ if hit?(x, y)
43
+ @content.hit_element(x, y) || self
44
+ else
45
+ nil
46
+ end
47
+ end
48
+
49
+ def recalc
50
+ super
51
+ # Always recalc our owner if our content resizes, even though our size can't change even if the content changes
52
+ # (may encourage ScrollWindow to show/hide scroll-bars, for example)
53
+ @owner.recalc if @owner
54
+ end
55
+
56
+ protected
57
+ def draw_foreground
58
+ $window.clip_to(*rect) do
59
+ @content.draw
60
+ end
61
+ end
62
+
63
+ protected
64
+ def post_init_block(&block)
65
+ with(&block)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,128 @@
1
+ # encoding: utf-8
2
+
3
+ module Fidgit
4
+ # @abstract
5
+ class ScrollBar < Composite
6
+ class Handle < Element
7
+ event :begin_drag
8
+ event :update_drag
9
+ event :end_drag
10
+
11
+ def drag?(button); button == :left; end
12
+
13
+ def initialize(options = {})
14
+ super options
15
+
16
+ subscribe :begin_drag do |sender, x, y|
17
+ # Store position of the handle when it starts to drag.
18
+ @drag_start_pos = [x - self.x, y - self.y]
19
+ end
20
+
21
+ subscribe :update_drag do |sender, x, y|
22
+ parent.parent.handle_dragged_to x - @drag_start_pos[0], y - @drag_start_pos[1]
23
+ end
24
+
25
+ subscribe :end_drag do
26
+ @drag_start_pos = nil
27
+ end
28
+ end
29
+ end
30
+
31
+ def initialize(options = {})
32
+ options = {
33
+ background_color: default(:background_color),
34
+ border_color: default(:border_color),
35
+ rail_width: default(:rail_width),
36
+ rail_color: default(:rail_color),
37
+ handle_color: default(:handle_color),
38
+ owner: nil,
39
+ }.merge! options
40
+
41
+ @owner = options[:owner]
42
+ @rail_thickness = options[:rail_width]
43
+ @rail_color = options[:rail_color]
44
+
45
+ super options
46
+
47
+ @handle_container = Container.new(parent: self, width: options[:width], height: options[:height]) do
48
+ @handle = Handle.new(parent: self, x: x, y: y, background_color: options[:handle_color])
49
+ end
50
+
51
+ subscribe :left_mouse_button do |sender, x, y|
52
+ clicked_to_move x, y
53
+ end
54
+ end
55
+ end
56
+
57
+ class HorizontalScrollBar < ScrollBar
58
+ attr_reader :owner
59
+
60
+ def initialize(options = {})
61
+ super options
62
+
63
+ @handle.height = height
64
+
65
+ @handle_container.subscribe :left_mouse_button do |sender, x, y|
66
+ distance = @owner.view_width
67
+ @owner.offset_x += (x > @handle.x)? +distance : -distance
68
+ end
69
+ end
70
+
71
+ def update
72
+ window = parent.parent
73
+
74
+ # Resize and re-locate the handles based on changes to the scroll-window.
75
+ content_width = window.content_width.to_f
76
+ @handle.width = (window.view_width * width) / content_width
77
+ @handle.x = x + (window.offset_x * width) / content_width
78
+ end
79
+
80
+ def draw_foreground
81
+ draw_rect x + padding_left, y + (height - @rail_thickness) / 2, width, @rail_thickness, z, @rail_color
82
+ super
83
+ end
84
+
85
+ def handle_dragged_to(x, y)
86
+ @owner.offset_x = @owner.content_width * ((x - self.x) / width.to_f)
87
+ end
88
+
89
+ def clicked_to_move(x, y)
90
+ new_x = x < @handle.x ? @handle.x - @handle.width : @handle.x + @handle.width
91
+ handle_dragged_to new_x, @handle.y
92
+ end
93
+ end
94
+
95
+ class VerticalScrollBar < ScrollBar
96
+ def initialize(options = {})
97
+ super options
98
+
99
+ @handle.width = width
100
+
101
+ @handle_container.subscribe :left_mouse_button do |sender, x, y|
102
+ distance = @owner.view_height
103
+ @owner.offset_y += (y > @handle.y)? +distance : -distance
104
+ end
105
+ end
106
+
107
+ def update
108
+ window = parent.parent
109
+ content_height = window.content_height.to_f
110
+ @handle.height = (window.view_height * height) / content_height
111
+ @handle.y = y + (window.offset_y * height) / content_height
112
+ end
113
+
114
+ def draw_foreground
115
+ draw_rect x + (width - @rail_thickness) / 2, y + padding_top, @rail_thickness, height, z, @rail_color
116
+ super
117
+ end
118
+
119
+ def handle_dragged_to(x, y)
120
+ @owner.offset_y = @owner.content_height * ((y - self.y) / height.to_f)
121
+ end
122
+
123
+ def clicked_to_move(x, y)
124
+ new_y = y < @handle.y ? @handle.y - @handle.height : @handle.y + @handle.height
125
+ handle_dragged_to @handle.x, new_y
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,83 @@
1
+ # encoding: utf-8
2
+
3
+ module Fidgit
4
+ class ScrollWindow < Composite
5
+ def content; @view.content; end
6
+ def offset_x; @view.offset_x; end
7
+ def offset_x=(value); @view.offset_x = value; end
8
+ def offset_y; @view.offset_y; end
9
+ def offset_y=(value); @view.offset_y = value; end
10
+
11
+ def view_width; @view.width; end
12
+ def view_height; @view.height; end
13
+ def content_width; @view.content.width; end
14
+ def content_height; @view.content.height; end
15
+ def width=(value); super(value); end
16
+ def height=(value); super(value); end
17
+
18
+ def initialize(options = {})
19
+ options = {
20
+ scroll_bar_thickness: default(:scroll_bar_thickness),
21
+ }.merge! options
22
+
23
+ super(options)
24
+
25
+ @grid = grid num_columns: 2, padding: 0, spacing: 0 do
26
+ @view = scroll_area(owner: self, width: options[:width], height: options[:height])
27
+ @spacer = label '', padding: 0, width: 0, height: 0
28
+ end
29
+
30
+ @scroll_bar_v = VerticalScrollBar.new(owner: self, width: options[:scroll_bar_thickness], align_v: :fill)
31
+ @scroll_bar_h = HorizontalScrollBar.new(owner: self, height: options[:scroll_bar_thickness], align_h: :fill)
32
+ end
33
+
34
+ protected
35
+ def layout
36
+ # Prevent recursive layouts.
37
+ return if @in_layout
38
+
39
+ @in_layout = true
40
+
41
+ if @view
42
+ # Constrain the values of the offsets.
43
+ @view.offset_x = @view.offset_x
44
+ @view.offset_y = @view.offset_y
45
+
46
+ if content_height > view_height
47
+ unless @scroll_bar_v.parent
48
+ @view.send(:rect).width -= @scroll_bar_v.width
49
+ @grid.remove @spacer
50
+ @grid.insert 1, @scroll_bar_v
51
+ end
52
+ else
53
+ if @scroll_bar_v.parent
54
+ @view.send(:rect).width += @scroll_bar_v.width
55
+ @grid.remove @scroll_bar_v
56
+ @grid.insert 1, @spacer
57
+ end
58
+ end
59
+
60
+ if content_width > view_width
61
+ unless @scroll_bar_h.parent
62
+ @view.send(:rect).height -= @scroll_bar_h.height
63
+ @grid.add @scroll_bar_h
64
+ end
65
+ else
66
+ if @scroll_bar_h.parent
67
+ @view.send(:rect).height += @scroll_bar_h.height
68
+ @grid.remove @scroll_bar_h
69
+ end
70
+ end
71
+ end
72
+
73
+ super
74
+
75
+ @in_layout = false
76
+ end
77
+
78
+ protected
79
+ def post_init_block(&block)
80
+ @view.content.with(&block)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,125 @@
1
+ # encoding: utf-8
2
+
3
+ module Fidgit
4
+ class Slider < Composite
5
+ # @private
6
+ class Handle < Element
7
+ event :begin_drag
8
+ event :end_drag
9
+ event :update_drag
10
+
11
+ def drag?(button); button == :left; end
12
+
13
+ # @param (see Element#initialize)
14
+ #
15
+ # @option (see Element#initialize)
16
+ def initialize(options = {}, &block)
17
+ options = {
18
+ background_color: default(:background_color),
19
+ border_color: default(:border_color),
20
+ }.merge! options
21
+
22
+ super options
23
+
24
+ subscribe :begin_drag do |sender, x, y|
25
+ # Store position of the handle when it starts to drag.
26
+ @drag_start_pos = [x - self.x, y - self.y]
27
+ end
28
+
29
+ subscribe :update_drag do |sender, x, y|
30
+ if parent.enabled?
31
+ parent.handle_dragged_to x - @drag_start_pos[0], y - @drag_start_pos[1]
32
+ else
33
+ publish :end_drag
34
+ end
35
+ end
36
+
37
+ subscribe :end_drag do
38
+ @drag_start_pos = nil
39
+ end
40
+ end
41
+
42
+ def tip; parent.tip; end
43
+ end
44
+
45
+ event :changed
46
+
47
+ attr_reader :value, :range, :handle
48
+
49
+ # @param (see Composite#initialize)
50
+ #
51
+ # @option (see Composite#initialize)
52
+ # @option options [Range] :range (0.0..1.0)
53
+ # @option options [Range] :value (minimum of :range)
54
+ def initialize(options = {}, &block)
55
+ options = {
56
+ range: 0.0..1.0,
57
+ height: 25,
58
+ background_color: default(:background_color),
59
+ border_color: default(:border_color),
60
+ groove_color: default(:groove_color),
61
+ handle_color: default(:handle_color),
62
+ groove_thickness: 5,
63
+ }.merge! options
64
+
65
+ @range = options[:range].dup
66
+ @groove_color = options[:groove_color].dup
67
+ @groove_thickness = options[:groove_thickness]
68
+ @continuous = @range.min.is_a?(Float) || @range.max.is_a?(Float)
69
+
70
+ super(options)
71
+
72
+ @handle = Handle.new(parent: self, width: (height / 2 - padding_left), height: height - padding_top + padding_bottom,
73
+ background_color: options[:handle_color])
74
+
75
+ self.value = options.has_key?(:value) ? options[:value] : @range.min
76
+ end
77
+
78
+ def value=(value)
79
+ @value = @continuous ? value.to_f : value.round
80
+ @value = [[@value, @range.min].max, @range.max].min
81
+ @handle.x = x + padding_left + ((width - @handle.width) * (@value - @range.min) / (@range.max - @range.min).to_f)
82
+ publish :changed, @value
83
+
84
+ @value
85
+ end
86
+
87
+ def tip
88
+ tip = super
89
+ tip.empty? ? @value.to_s : "#{tip}: #{@value}"
90
+ end
91
+
92
+ def left_mouse_button(sender, x, y)
93
+ # In this case, x should be the centre of the handle after it has moved.
94
+ self.value = ((x - (@handle.width / 2) - self.x) / (width - @handle.width)) * (@range.max - @range.min) + @range.min
95
+ @mouse_down = true
96
+
97
+ nil
98
+ end
99
+
100
+ def handle_dragged_to(x, y)
101
+ # In this case, x is the left-hand side fo the handle.
102
+ self.value = ((x - self.x) / (width - @handle.width)) * (@range.max - @range.min) + @range.min
103
+ end
104
+
105
+ protected
106
+ # Prevent standard packing layout change.
107
+ def layout
108
+ nil
109
+ end
110
+
111
+ protected
112
+ def draw_background
113
+ super
114
+ # Draw a groove for the handle to move along.
115
+ draw_rect x + (@handle.width / 2), y + (height - @groove_thickness) / 2, width - @handle.width, @groove_thickness, z, @groove_color
116
+ nil
117
+ end
118
+
119
+ protected
120
+ # Use block as an event handler.
121
+ def post_init_block(&block)
122
+ subscribe :changed, &block
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,494 @@
1
+ # encoding: utf-8
2
+
3
+ module Fidgit
4
+ class TextArea < Element
5
+ ENTITY_PLACEHOLDER = "*"
6
+ ENTITIES_AND_TAGS_PATTERN = %r%<[a-z](?:=[a-f0-9]+)?>|</[a-z]>|&\w+;%i
7
+
8
+ # @return [Number]
9
+ attr_reader :min_height
10
+ # @return [Number]
11
+ attr_reader :max_height
12
+
13
+ # @return [Number]
14
+ attr_reader :line_spacing
15
+
16
+ # @param [Boolean] value
17
+ # @return [Boolean]
18
+ attr_writer :editable
19
+
20
+ # @return [String] Text, but stripped of tags.
21
+ attr_reader :stripped_text
22
+
23
+ event :begin_drag
24
+ event :update_drag
25
+ event :end_drag
26
+
27
+ event :changed
28
+ event :focus
29
+ event :blur
30
+
31
+ def drag?(button); button == :left; end
32
+
33
+ # Is the area editable? This will always be false if the Element is disabled.
34
+ def editable?
35
+ enabled? and @editable
36
+ end
37
+
38
+ # Text within the element.
39
+ # @return [String]
40
+ def text
41
+ @text_input.text.force_encoding 'UTF-8'
42
+ end
43
+
44
+ # Returns the range of the selection.
45
+ #
46
+ # @return [Range]
47
+ def selection_range
48
+ from = [@text_input.selection_start, caret_position].min
49
+ to = [@text_input.selection_start, caret_position].max
50
+
51
+ (from...to)
52
+ end
53
+
54
+ # Returns the text within the selection.
55
+ #
56
+ # @return [String]
57
+ def selection_text
58
+ stripped_text[selection_range]
59
+ end
60
+
61
+ # Sets the text within the selection. The caret will be placed at the end of the inserted text.
62
+ #
63
+ # @param [String] str Text to insert.
64
+ # @return [String] The new selection text.
65
+ def selection_text=(str)
66
+ from = [@text_input.selection_start, @text_input.caret_pos].min
67
+ to = [@text_input.selection_start, @text_input.caret_pos].max
68
+ new_length = str.length
69
+
70
+ full_text = text
71
+ tags_length_before = (0...from).inject(0) {|m, i| m + @tags[i].length }
72
+ tags_length_inside = (from...to).inject(0) {|m, i| m + @tags[i].length }
73
+ range = (selection_range.first + tags_length_before)...(selection_range.last + tags_length_before + tags_length_inside)
74
+ full_text[range] = str.encode('UTF-8', undef: :replace)
75
+ @text_input.text = full_text
76
+
77
+ @text_input.selection_start = @text_input.caret_pos = from + new_length
78
+
79
+ recalc # This may roll back the text if it is too long!
80
+
81
+ publish :changed, self.text
82
+
83
+ str
84
+ end
85
+
86
+ # Position of the caret.
87
+ #
88
+ # @return [Integer] Number in range 0..text.length
89
+ def caret_position
90
+ @text_input.caret_pos
91
+ end
92
+
93
+ # Position of the caret.
94
+ #
95
+ # @param [Integer] pos Position of caret in the text.
96
+ # @return [Integer] New position of caret.
97
+ def caret_position=(position)
98
+ raise ArgumentError, "Caret position must be in the range 0 to the length of the text (inclusive)" unless position.between?(0, stripped_text.length)
99
+ @text_input.caret_pos = position
100
+
101
+ position
102
+ end
103
+
104
+ # Sets caret to the end of the text.
105
+ #
106
+ # @param [String] text
107
+ # @return [String] Current string (may be the old one if passed on was too long).
108
+ def text=(text)
109
+ @text_input.text = text
110
+ recalc # This may roll back the text if it is too long.
111
+ publish :changed, self.text
112
+ self.text
113
+ end
114
+
115
+
116
+ # @param (see Element#initialize)
117
+ #
118
+ # @option (see Element#initialize)
119
+ # @option options [String] :text ("")
120
+ # @option options [Integer] :height Sets both min and max height at once.
121
+ # @option options [Integer] :min_height
122
+ # @option options [Integer] :max_height (Infinite)
123
+ # @option options [Number] :line_spacing (0)
124
+ # @option options [Boolean] :editable (true)
125
+ def initialize(options = {}, &block)
126
+ options = {
127
+ text: '',
128
+ max_height: Float::INFINITY,
129
+ line_spacing: default(:line_spacing),
130
+ background_color: default(:background_color),
131
+ border_color: default(:border_color),
132
+ caret_color: default(:caret_color),
133
+ caret_period: default(:caret_period),
134
+ focused_border_color: default(:focused, :border_color),
135
+ selection_color: default(:selection_color),
136
+ editable: true,
137
+ }.merge! options
138
+
139
+ @line_spacing = options[:line_spacing]
140
+ @caret_color = options[:caret_color].dup
141
+ @caret_period = options[:caret_period]
142
+ @focused_border_color = options[:focused_border_color].dup
143
+ @selection_color = options[:selection_color].dup
144
+ @editable = options[:editable]
145
+
146
+ @lines = [''] # List of lines of wrapped text.
147
+ @caret_positions = [[0, 0]] # [x, y] of each position the caret can be in.
148
+ @char_widths = [] # Width of each character in the text.
149
+ @text_input = Gosu::TextInput.new
150
+ @old_text = ''
151
+ @old_caret_position = 0
152
+ @old_selection_start = 0
153
+ @tags = Hash.new("") # Hash of tags embedded in the text.
154
+
155
+ @text_input.text = options[:text].dup
156
+ @stripped_text = '' # Text stripped of xml tags.
157
+
158
+ super(options)
159
+
160
+ min_height = padding_left + padding_right + font.height
161
+ if options[:height]
162
+ @max_height = @min_height = [options[:height], min_height].max
163
+ else
164
+ @max_height = [options[:max_height], min_height].max
165
+ @min_height = options[:min_height] ? [options[:min_height], min_height].max : min_height
166
+ end
167
+ rect.height = [padding_left + padding_right + font.height, @min_height].max
168
+
169
+ subscribe :left_mouse_button, method(:click_in_text)
170
+ subscribe :right_mouse_button, method(:click_in_text)
171
+
172
+ # Handle dragging.
173
+ subscribe :begin_drag do |sender, x, y|
174
+ # Store position of the handle when it starts to drag.
175
+ @drag_start_pos = [x - self.x, y - self.y]
176
+ end
177
+
178
+ subscribe :update_drag do |sender, x, y|
179
+ index = text_index_at_position(x, y)
180
+ self.caret_position = [index, @stripped_text.length].min if index
181
+ end
182
+
183
+ subscribe :end_drag do
184
+ @drag_start_pos = nil
185
+ end
186
+ end
187
+
188
+ # @return [nil]
189
+ def click_in_text(sender, x, y)
190
+ publish :focus unless focused?
191
+
192
+ # Move caret to position the user clicks on.
193
+ index = text_index_at_position x, y
194
+ self.caret_position = @text_input.selection_start = [index, @stripped_text.length].min if index
195
+
196
+ nil
197
+ end
198
+
199
+ # Does the element have the focus?
200
+ def focused?; @focused; end
201
+
202
+ # @return [nil]
203
+ def focus(sender)
204
+ @focused = true
205
+ $window.current_game_state.focus = self
206
+ $window.text_input = @text_input
207
+
208
+ nil
209
+ end
210
+
211
+ # @return [nil]
212
+ def blur(sender)
213
+ if focused?
214
+ $window.current_game_state.focus = nil
215
+ $window.text_input = nil
216
+ end
217
+
218
+ @focused = false
219
+
220
+ nil
221
+ end
222
+
223
+ # Draw the text area.
224
+ #
225
+ # @return [nil]
226
+ def draw_foreground
227
+ # Always roll back changes made by the user unless the text is editable.
228
+ if editable? or text == @old_text
229
+ recalc if focused? # Workaround for Windows draw/update bug.
230
+ @old_caret_position = caret_position
231
+ @old_selection_start = @text_input.selection_start
232
+ else
233
+ roll_back
234
+ end
235
+
236
+ if caret_position > stripped_text.length
237
+ self.caret_position = stripped_text.length
238
+ end
239
+
240
+ if @text_input.selection_start >= stripped_text.length
241
+ @text_input.selection_start = stripped_text.length
242
+ end
243
+
244
+ # Draw the selection.
245
+ selection_range.each do |pos|
246
+ char_x, char_y = @caret_positions[pos]
247
+ char_width = @char_widths[pos]
248
+ left, top = x + padding_left + char_x, y + padding_top + char_y
249
+ draw_rect left, top, char_width, font.height, z, @selection_color
250
+ end
251
+
252
+ # Draw text.
253
+ @lines.each_with_index do |line, index|
254
+ font.draw(line, x + padding_left, y + padding_top + y_at_line(index), z)
255
+ end
256
+
257
+ # Draw the caret.
258
+ if focused? and ((Gosu::milliseconds / @caret_period) % 2 == 0)
259
+ caret_x, caret_y = @caret_positions[caret_position]
260
+ left, top = x + padding_left + caret_x, y + padding_top + caret_y
261
+ draw_rect left, top, 1, font.height, z, @caret_color
262
+ end
263
+ end
264
+
265
+ protected
266
+ # Index of character in reference to the displayable text.
267
+ def text_index_at_position(x, y)
268
+ # Move caret to position the user clicks on.
269
+ mouse_x, mouse_y = x - (self.x + padding_left), y - (self.y + padding_top)
270
+ @char_widths.each.with_index do |width, i|
271
+ char_x, char_y = @caret_positions[i]
272
+ if mouse_x.between?(char_x, char_x + width) and mouse_y.between?(char_y, char_y + font.height)
273
+ return i
274
+ end
275
+ end
276
+
277
+ nil # Didn't find a character at that position.
278
+ end
279
+
280
+ # y position of the
281
+ protected
282
+ def y_at_line(lines_number)
283
+ lines_number * (font.height + line_spacing)
284
+ end
285
+
286
+
287
+ protected
288
+ # Helper for #recalc
289
+ # @return [Integer]
290
+ def position_letters_in_word(word, line_width)
291
+ # Strip tags before measuring word.
292
+ word.gsub(ENTITIES_AND_TAGS_PATTERN, '').each_char do |c|
293
+ char_width = font.text_width(c)
294
+ line_width += char_width
295
+ @caret_positions.push [line_width, y_at_line(@lines.size)]
296
+ @char_widths.push char_width
297
+ end
298
+
299
+ line_width
300
+ end
301
+
302
+ protected
303
+ # @return [nil]
304
+ def layout
305
+ # Don't need to re-layout if the text hasn't changed.
306
+ return if @old_text == text
307
+
308
+ publish :changed, self.text
309
+
310
+ # Save these in case we are too long.
311
+ old_lines = @lines
312
+ old_caret_positions = @caret_positions
313
+ old_char_widths = @char_widths
314
+
315
+ @lines = []
316
+ @caret_positions = [[0, 0]] # Position 0 is before the first character.
317
+ @char_widths = []
318
+
319
+ space_width = font.text_width ' '
320
+ max_width = width - padding_left - padding_right - space_width
321
+
322
+ line = ''
323
+ line_width = 0
324
+ word = ''
325
+ word_width = 0
326
+
327
+ strip_tags
328
+
329
+ stripped_text.each_char.with_index do |char, i|
330
+ tag = @tags[i]
331
+
332
+ # \x0 is just a place-holder for an entity: &entity;
333
+ if char == ENTITY_PLACEHOLDER
334
+ char = tag
335
+ tag = ""
336
+ end
337
+
338
+ case char
339
+ when "\n"
340
+ char_width = 0
341
+ else
342
+ char_width = font.text_width char
343
+ end
344
+
345
+ overall_width = line_width + (line_width == 0 ? 0 : space_width) + word_width + char_width
346
+ if overall_width > max_width and not (char == ' ' and not word.empty?)
347
+ if line.empty?
348
+ # The current word is longer than the whole word, so split it.
349
+ # Go back and set all the character positions we have.
350
+ position_letters_in_word(word, line_width)
351
+
352
+ # Push as much of the current word as possible as a complete line.
353
+ @lines.push word + tag + (char == ' ' ? '' : '-')
354
+ line_width = font.text_width(word)
355
+
356
+ word = ''
357
+ word_width = 0
358
+ else
359
+
360
+ # Adding the current word would be too wide, so add the current line and start a new one.
361
+ @lines.push line
362
+ line = ''
363
+ end
364
+
365
+ widen_last_character line_width
366
+ line_width = 0
367
+ end
368
+
369
+ case char
370
+ when "\n"
371
+ # A new-line ends the word and puts it on the line.
372
+ line += word + tag
373
+ line_width = position_letters_in_word(word, line_width)
374
+ @caret_positions.push [line_width, y_at_line(@lines.size)]
375
+ @char_widths.push 0
376
+ widen_last_character line_width
377
+ @lines.push line
378
+ word = ''
379
+ word_width = 0
380
+ line = ''
381
+ line_width = 0
382
+
383
+ when ' '
384
+ # A space ends a word and puts it on the line.
385
+ line += word + tag + char
386
+ line_width = position_letters_in_word(word, line_width)
387
+ line_width += space_width
388
+ @caret_positions.push [line_width, y_at_line(@lines.size)]
389
+ @char_widths.push space_width
390
+
391
+ word = ''
392
+ word_width = 0
393
+
394
+ else
395
+ # If there was a previous line and we start a new line, put the caret pos on the current line.
396
+ if line.empty?
397
+ @caret_positions[-1] = [0, y_at_line(@lines.size)]
398
+ end
399
+
400
+ # Start building up a new word.
401
+ word += tag + char
402
+ word_width += char_width
403
+ end
404
+ end
405
+
406
+ # Add any remaining word on the last line.
407
+ unless word.empty?
408
+ line_width = position_letters_in_word(word, line_width)
409
+ @char_widths << width - line_width - padding_left - padding_right
410
+ line += word
411
+ end
412
+
413
+ @lines.push line if @lines.empty? or not line.empty?
414
+
415
+ # Roll back if the height is too long.
416
+ new_height = padding_left + padding_right + y_at_line(@lines.size)
417
+ if new_height <= max_height
418
+ @old_text = text
419
+ rect.height = [new_height, @min_height].max
420
+ @old_caret_position = caret_position
421
+ @old_selection_start = @text_input.selection_start
422
+ else
423
+ roll_back
424
+ end
425
+
426
+ nil
427
+ end
428
+
429
+ protected
430
+ def roll_back
431
+ @text_input.text = @old_text
432
+ self.caret_position = @old_caret_position
433
+ @text_input.selection_start = @old_selection_start
434
+ recalc
435
+ end
436
+
437
+ protected
438
+ def widen_last_character(line_width)
439
+ @char_widths[-1] += (width - line_width - padding_left - padding_right) unless @char_widths.empty?
440
+ end
441
+
442
+ public
443
+ # Cut the selection and copy it to the clipboard.
444
+ def cut
445
+ str = selection_text
446
+ unless str.empty?
447
+ Clipboard.copy str
448
+ self.selection_text = '' if editable?
449
+ end
450
+ end
451
+
452
+ public
453
+ # Copy the selection to the clipboard.
454
+ def copy
455
+ str = selection_text
456
+ Clipboard.copy str unless str.empty?
457
+ end
458
+
459
+ public
460
+ # Paste the contents of the clipboard into the TextArea.
461
+ def paste
462
+ self.selection_text = Clipboard.paste
463
+ end
464
+
465
+ protected
466
+ # Use block as an event handler.
467
+ def post_init_block(&block)
468
+ subscribe :changed, &block
469
+ end
470
+
471
+ protected
472
+ # Strip XML tags and entities ("<c=000000></c>" and "&entity;")
473
+ # @note Entities will mess up the system because we don't know how wide they are.
474
+ def strip_tags
475
+ tags_length = 0
476
+ @tags = Hash.new('')
477
+
478
+ @stripped_text = text.gsub(ENTITIES_AND_TAGS_PATTERN) do |tag|
479
+ pos = $`.length - tags_length
480
+ tags_length += tag.length
481
+ @tags[pos] += tag
482
+
483
+ # Entities need to have a non-printing character that can represent them.
484
+ # Still not right, but does mean there are the right number of characters.
485
+ if tag[0] == '&'
486
+ tags_length -= 1
487
+ ENTITY_PLACEHOLDER # Will be expanded later.
488
+ else
489
+ '' # Tags don't use up space, so ignore them.
490
+ end
491
+ end
492
+ end
493
+ end
494
+ end