colstrom-fidgit 0.2.7

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