colstrom-fidgit 0.2.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +31 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +20 -0
- data/README.md +154 -0
- data/Rakefile +38 -0
- data/config/default_schema.yml +216 -0
- data/examples/_all_examples.rb +9 -0
- data/examples/align_example.rb +56 -0
- data/examples/button_and_toggle_button_example.rb +38 -0
- data/examples/color_picker_example.rb +17 -0
- data/examples/color_well_example.rb +25 -0
- data/examples/combo_box_example.rb +24 -0
- data/examples/file_dialog_example.rb +42 -0
- data/examples/grid_packer_example.rb +29 -0
- data/examples/helpers/example_window.rb +17 -0
- data/examples/label_example.rb +23 -0
- data/examples/list_example.rb +23 -0
- data/examples/media/images/head_icon.png +0 -0
- data/examples/menu_pane_example.rb +27 -0
- data/examples/message_dialog_example.rb +65 -0
- data/examples/radio_button_example.rb +37 -0
- data/examples/readme_example.rb +32 -0
- data/examples/scroll_window_example.rb +49 -0
- data/examples/slider_example.rb +34 -0
- data/examples/splash_example.rb +42 -0
- data/examples/text_area_example.rb +33 -0
- data/fidgit.gemspec +35 -0
- data/lib/fidgit.rb +51 -0
- data/lib/fidgit/chingu_ext/window.rb +6 -0
- data/lib/fidgit/cursor.rb +38 -0
- data/lib/fidgit/elements/button.rb +113 -0
- data/lib/fidgit/elements/color_picker.rb +63 -0
- data/lib/fidgit/elements/color_well.rb +39 -0
- data/lib/fidgit/elements/combo_box.rb +115 -0
- data/lib/fidgit/elements/composite.rb +17 -0
- data/lib/fidgit/elements/container.rb +210 -0
- data/lib/fidgit/elements/element.rb +298 -0
- data/lib/fidgit/elements/file_browser.rb +152 -0
- data/lib/fidgit/elements/grid.rb +227 -0
- data/lib/fidgit/elements/group.rb +64 -0
- data/lib/fidgit/elements/horizontal.rb +12 -0
- data/lib/fidgit/elements/image_frame.rb +65 -0
- data/lib/fidgit/elements/label.rb +85 -0
- data/lib/fidgit/elements/list.rb +47 -0
- data/lib/fidgit/elements/main_packer.rb +25 -0
- data/lib/fidgit/elements/menu_pane.rb +163 -0
- data/lib/fidgit/elements/packer.rb +42 -0
- data/lib/fidgit/elements/radio_button.rb +86 -0
- data/lib/fidgit/elements/scroll_area.rb +68 -0
- data/lib/fidgit/elements/scroll_bar.rb +128 -0
- data/lib/fidgit/elements/scroll_window.rb +83 -0
- data/lib/fidgit/elements/slider.rb +125 -0
- data/lib/fidgit/elements/text_area.rb +494 -0
- data/lib/fidgit/elements/text_line.rb +92 -0
- data/lib/fidgit/elements/toggle_button.rb +67 -0
- data/lib/fidgit/elements/tool_tip.rb +35 -0
- data/lib/fidgit/elements/vertical.rb +12 -0
- data/lib/fidgit/event.rb +159 -0
- data/lib/fidgit/gosu_ext/color.rb +136 -0
- data/lib/fidgit/gosu_ext/gosu_module.rb +25 -0
- data/lib/fidgit/history.rb +91 -0
- data/lib/fidgit/redirector.rb +83 -0
- data/lib/fidgit/schema.rb +123 -0
- data/lib/fidgit/selection.rb +106 -0
- data/lib/fidgit/standard_ext/hash.rb +21 -0
- data/lib/fidgit/states/dialog_state.rb +52 -0
- data/lib/fidgit/states/file_dialog.rb +24 -0
- data/lib/fidgit/states/gui_state.rb +331 -0
- data/lib/fidgit/states/message_dialog.rb +61 -0
- data/lib/fidgit/version.rb +5 -0
- data/lib/fidgit/window.rb +19 -0
- data/media/images/arrow.png +0 -0
- data/media/images/combo_arrow.png +0 -0
- data/media/images/file_directory.png +0 -0
- data/media/images/file_file.png +0 -0
- data/media/images/pixel.png +0 -0
- data/spec/fidgit/elements/helpers/helper.rb +3 -0
- data/spec/fidgit/elements/helpers/tex_play_helper.rb +9 -0
- data/spec/fidgit/elements/image_frame_spec.rb +69 -0
- data/spec/fidgit/elements/label_spec.rb +37 -0
- data/spec/fidgit/event_spec.rb +210 -0
- data/spec/fidgit/gosu_ext/color_spec.rb +130 -0
- data/spec/fidgit/gosu_ext/helpers/helper.rb +3 -0
- data/spec/fidgit/helpers/helper.rb +4 -0
- data/spec/fidgit/history_spec.rb +153 -0
- data/spec/fidgit/redirector_spec.rb +78 -0
- data/spec/fidgit/schema_spec.rb +67 -0
- data/spec/fidgit/schema_test.yml +32 -0
- 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
|