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.
- 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
|