window_blessing 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Guardfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/bin/buffered_screen_demo.rb +92 -0
- data/bin/color_picker_demo.rb +176 -0
- data/bin/foiled_demo.rb +27 -0
- data/bin/text_editor_demo.rb +292 -0
- data/bin/windowed_screen_demo.rb +71 -0
- data/bin/xterm_screen_demo.rb +33 -0
- data/lib/window_blessing.rb +54 -0
- data/lib/window_blessing/buffer.rb +216 -0
- data/lib/window_blessing/buffered_screen.rb +29 -0
- data/lib/window_blessing/color.rb +71 -0
- data/lib/window_blessing/constants.rb +3 -0
- data/lib/window_blessing/event_manager.rb +75 -0
- data/lib/window_blessing/event_queue.rb +20 -0
- data/lib/window_blessing/evented.rb +19 -0
- data/lib/window_blessing/evented_variable.rb +47 -0
- data/lib/window_blessing/tools.rb +124 -0
- data/lib/window_blessing/version.rb +3 -0
- data/lib/window_blessing/widgets/draggable_background.rb +13 -0
- data/lib/window_blessing/widgets/label.rb +23 -0
- data/lib/window_blessing/widgets/slider.rb +53 -0
- data/lib/window_blessing/widgets/text_field.rb +92 -0
- data/lib/window_blessing/window.rb +273 -0
- data/lib/window_blessing/windowed_screen.rb +53 -0
- data/lib/window_blessing/xterm_event_parser.rb +156 -0
- data/lib/window_blessing/xterm_input.rb +40 -0
- data/lib/window_blessing/xterm_log.rb +7 -0
- data/lib/window_blessing/xterm_output.rb +213 -0
- data/lib/window_blessing/xterm_screen.rb +109 -0
- data/lib/window_blessing/xterm_state.rb +27 -0
- data/spec/buffer_spec.rb +170 -0
- data/spec/color_spec.rb +36 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/tools_spec.rb +142 -0
- data/spec/window_spec.rb +61 -0
- data/window_blessing.gemspec +28 -0
- metadata +226 -0
@@ -0,0 +1,273 @@
|
|
1
|
+
module WindowBlessing
|
2
|
+
class Window
|
3
|
+
include Tools
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def Window.attr_accessor_with_redraw( *symbols )
|
7
|
+
symbols.each do | symbol |
|
8
|
+
class_eval <<ENDCODE
|
9
|
+
def #{symbol}
|
10
|
+
@#{symbol}
|
11
|
+
end
|
12
|
+
|
13
|
+
def #{symbol}=(value)
|
14
|
+
old_value = @#{symbol}
|
15
|
+
@#{symbol} = value
|
16
|
+
request_redraw_internal if old_value != @#{symbol}
|
17
|
+
end
|
18
|
+
ENDCODE
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
include Evented
|
24
|
+
attr_accessor :name
|
25
|
+
|
26
|
+
def initialize(area=rect(0,0,20,20))
|
27
|
+
@area = rect
|
28
|
+
@children = []
|
29
|
+
@bg = Buffer.default_bg
|
30
|
+
@fg = Buffer.default_fg
|
31
|
+
@buffer = Buffer.new area.size, :bg => @bg, :fg => @fg
|
32
|
+
self.area = area
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
"<Window:#{name || object_id} area:#{area.to_s} children:#{children.length}>"
|
37
|
+
end
|
38
|
+
|
39
|
+
# event is in parent-space
|
40
|
+
def pointer_event(event)
|
41
|
+
focus
|
42
|
+
event[:loc] -= area.loc
|
43
|
+
@pointer_focused ||= children.reverse_each.find do |child|
|
44
|
+
child.pointer_inside? event[:loc]
|
45
|
+
end || :background
|
46
|
+
if @pointer_focused==:background
|
47
|
+
handle_event(event)
|
48
|
+
else
|
49
|
+
@pointer_focused.pointer_event event
|
50
|
+
end
|
51
|
+
@pointer_focused = nil if event[:button] == :button_up
|
52
|
+
end
|
53
|
+
|
54
|
+
module KeyboardFocus
|
55
|
+
# keyboard focusing
|
56
|
+
attr_reader :focused_child, :focused
|
57
|
+
|
58
|
+
# for internal use only
|
59
|
+
def focused_child=(child)
|
60
|
+
@focused_child = child
|
61
|
+
end
|
62
|
+
|
63
|
+
def focus
|
64
|
+
return if focused?
|
65
|
+
if parent
|
66
|
+
parent.focus
|
67
|
+
parent_focused_child = parent.focused_child
|
68
|
+
parent_focused_child.blur if parent_focused_child
|
69
|
+
parent.focused_child = self
|
70
|
+
end
|
71
|
+
|
72
|
+
@focused = true
|
73
|
+
handle_event :type => :focus
|
74
|
+
end
|
75
|
+
|
76
|
+
def blur
|
77
|
+
return if blurred?
|
78
|
+
if focused_child
|
79
|
+
focused_child.blur
|
80
|
+
@focused_child = nil
|
81
|
+
end
|
82
|
+
|
83
|
+
@focused = false
|
84
|
+
handle_event :type => :blur
|
85
|
+
end
|
86
|
+
|
87
|
+
def blurred?; !@focused end
|
88
|
+
def focused?; !!@focused end
|
89
|
+
|
90
|
+
def route_keyboard_event(event)
|
91
|
+
if focused_child
|
92
|
+
focused_child.route_keyboard_event event
|
93
|
+
end
|
94
|
+
handle_event event
|
95
|
+
end
|
96
|
+
end
|
97
|
+
include KeyboardFocus
|
98
|
+
|
99
|
+
module Geometry
|
100
|
+
attr_reader :area
|
101
|
+
def area=(area)
|
102
|
+
raise "rectangle area required" unless area.kind_of? GuiGeo::Rectangle
|
103
|
+
raise "size must be at least 1x1" unless area.size > point
|
104
|
+
return if area == @area
|
105
|
+
request_redraw # request redraw before changing the area
|
106
|
+
|
107
|
+
old_size = @area.size
|
108
|
+
@area = area
|
109
|
+
|
110
|
+
if area.size != old_size
|
111
|
+
resize_buffer old_size
|
112
|
+
children.each {|c| c.handle_event type: :parent_resize, old_size:old_size, size:area.size }
|
113
|
+
handle_event :type => :resize, :old_size => old_size, :size => area.size
|
114
|
+
else
|
115
|
+
# only location changed, request external redraw at the new location
|
116
|
+
request_redraw
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def loc; area.loc; end
|
121
|
+
def loc=(new_loc) self.area = rect new_loc, area.size end
|
122
|
+
|
123
|
+
def size; area.size; end
|
124
|
+
def size=(new_size) self.area = rect area.loc, new_size end
|
125
|
+
|
126
|
+
def pointer_inside?(loc) area.contains? loc end
|
127
|
+
|
128
|
+
def move_onscreen
|
129
|
+
return unless parent
|
130
|
+
parent_area = rect(point, parent.area.size)
|
131
|
+
self.area = parent_area.bound(area)
|
132
|
+
end
|
133
|
+
|
134
|
+
def internal_area; rect(@area.size); end
|
135
|
+
|
136
|
+
private
|
137
|
+
def resize_buffer(old_size)
|
138
|
+
@requested_redraw_area = internal_area | @requested_redraw_area if @requested_redraw_area
|
139
|
+
if area.size <= old_size
|
140
|
+
@buffer = @buffer.subbuffer(rect(area.size))
|
141
|
+
request_redraw
|
142
|
+
else
|
143
|
+
@buffer = Buffer.new area.size
|
144
|
+
request_redraw_internal
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
include Geometry
|
149
|
+
|
150
|
+
module ParentsAndChildren
|
151
|
+
attr_reader :parent
|
152
|
+
attr_reader :children
|
153
|
+
|
154
|
+
# returns nil if nothing was done, otherwise returns child
|
155
|
+
def remove_child(child)
|
156
|
+
length_before = children.length
|
157
|
+
@children = children.select {|c| c!=child}
|
158
|
+
if @children.length!=length_before
|
159
|
+
child.request_redraw
|
160
|
+
child.parent= nil
|
161
|
+
child
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def add_child(child)
|
166
|
+
children << child
|
167
|
+
child.parent= self
|
168
|
+
child.request_redraw
|
169
|
+
child
|
170
|
+
end
|
171
|
+
|
172
|
+
# for internal use only!
|
173
|
+
def parent=(p)
|
174
|
+
@parent = p
|
175
|
+
handle_event type: :parent_set
|
176
|
+
end
|
177
|
+
|
178
|
+
def each_child(&block)
|
179
|
+
children.each &block
|
180
|
+
end
|
181
|
+
|
182
|
+
def each_child_with_index(&block)
|
183
|
+
children.each_with_index &block
|
184
|
+
end
|
185
|
+
|
186
|
+
def path
|
187
|
+
[parent && parent.path,"#{self.class}#{self.area}"].flatten.compact.join(',')
|
188
|
+
end
|
189
|
+
|
190
|
+
def parent_path
|
191
|
+
parent && parent.path
|
192
|
+
end
|
193
|
+
end
|
194
|
+
include ParentsAndChildren
|
195
|
+
|
196
|
+
module Drawing
|
197
|
+
Window.attr_accessor_with_redraw :bg, :fg
|
198
|
+
attr_reader :requested_redraw_area, :buffer
|
199
|
+
|
200
|
+
# sometimes you want to know where redraw requests are coming from
|
201
|
+
# Since request_redraw_internal is recursive, you don't want to log the stack trace with every call - just the first one
|
202
|
+
# This will log a stack-trace once per call
|
203
|
+
def log_request_redraw_internal
|
204
|
+
trace = Kernel.caller
|
205
|
+
return if trace.count {|line| line["request_redraw_internal"]} > 1
|
206
|
+
log "request_redraw_internal trace @requested_redraw_area=#{@requested_redraw_area} path:#{path}\n "+ trace.join("\n ")
|
207
|
+
end
|
208
|
+
|
209
|
+
def request_redraw_internal(area = internal_area)
|
210
|
+
#return if @requested_redraw_area && @requested_redraw_area.contains?(area) - the color_picker demo's info label fails to update with this uncommented - why?
|
211
|
+
@requested_redraw_area = internal_area | (area & @requested_redraw_area)
|
212
|
+
#log_request_redraw_internal
|
213
|
+
|
214
|
+
request_redraw @requested_redraw_area
|
215
|
+
end
|
216
|
+
|
217
|
+
# ask the parent to redraw all, or, if area is set, some of the area covered by this window
|
218
|
+
def request_redraw(redraw_area = nil)
|
219
|
+
redraw_area ||= internal_area
|
220
|
+
parent && parent.request_redraw_internal(rect(redraw_area.loc + @area.loc, redraw_area.size))
|
221
|
+
end
|
222
|
+
|
223
|
+
def redraw_requested?; !!requested_redraw_area end
|
224
|
+
|
225
|
+
# Reset @buffer to the designated background. The default implementation resets it to the ' ' character with @bg and @fg colors.
|
226
|
+
#
|
227
|
+
# NOTE: Buffer may have a cropping area set
|
228
|
+
#
|
229
|
+
# NOTE: Safe to override. Calling 'super' is optional. Should fully replace all character, foreground and background colors for @buffer's current croparea.
|
230
|
+
def draw_background
|
231
|
+
buffer.fill :string => ' ', :bg => bg, :fg => fg
|
232
|
+
end
|
233
|
+
|
234
|
+
# Update @buffer
|
235
|
+
#
|
236
|
+
# The default implementation calls #draw_background and then calls #draw on each child.
|
237
|
+
#
|
238
|
+
# NOTE: Buffer may have a cropping area set
|
239
|
+
#
|
240
|
+
# NOTE: Safe to override. Calling 'super' is optional.
|
241
|
+
def draw_internal
|
242
|
+
draw_background
|
243
|
+
children.each do |child|
|
244
|
+
child.draw buffer, (buffer.crop_area - child.loc)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# marks "redrawn_area" of @buffer "up to date" (redraw no longer required)
|
249
|
+
def clean(redrawn_area = @requested_redraw_area)
|
250
|
+
@requested_redraw_area = nil if redrawn_area && redrawn_area.contains?(@requested_redraw_area)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Draw the window:
|
254
|
+
#
|
255
|
+
# 1) Draw the specified internal_area, or @requested_redraw_area by default, into @buffer
|
256
|
+
# 2) Draw @buffer to target_buffer (if set)
|
257
|
+
# 3) returns the internal_area that was updated
|
258
|
+
def draw(target_buffer = nil, internal_area = @requested_redraw_area)
|
259
|
+
internal_area = self.internal_area | internal_area
|
260
|
+
|
261
|
+
if internal_area.overlaps? @requested_redraw_area
|
262
|
+
buffer.cropped(internal_area | @requested_redraw_area) {draw_internal}
|
263
|
+
clean internal_area
|
264
|
+
end
|
265
|
+
|
266
|
+
target_buffer.draw_buffer(loc, buffer, internal_area) if target_buffer
|
267
|
+
|
268
|
+
internal_area
|
269
|
+
end
|
270
|
+
end
|
271
|
+
include Drawing
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module WindowBlessing
|
2
|
+
|
3
|
+
class WindowedScreen < XtermScreen
|
4
|
+
attr_accessor :root_window
|
5
|
+
|
6
|
+
def time(category,info)
|
7
|
+
start = Time.now
|
8
|
+
yield
|
9
|
+
stop = Time.now
|
10
|
+
@total_time ||= 0
|
11
|
+
@draw_count ||= 0
|
12
|
+
@draw_count += 1
|
13
|
+
@total_time += stop - start
|
14
|
+
XtermLog.log "#{category} time = #{((@total_time/@draw_count)*1000).to_i}ms #{info}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
super
|
19
|
+
@root_window = Window.new
|
20
|
+
@root_window.buffer.dirty
|
21
|
+
@root_window.name = "root_window"
|
22
|
+
|
23
|
+
event_manager.add_handler :tick do
|
24
|
+
if redraw_area = root_window.requested_redraw_area
|
25
|
+
time(:redraw, "size=#{redraw_area.size}.area = #{redraw_area.size.x * redraw_area.size.y}") do
|
26
|
+
root_window.draw
|
27
|
+
buffer = root_window.buffer
|
28
|
+
output.draw_buffer buffer.dirty_area.loc, buffer.dirty_subbuffer if buffer.dirty_area
|
29
|
+
buffer.clean
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
event_manager.add_handler :key_press do |event|
|
35
|
+
root_window.route_keyboard_event event
|
36
|
+
end
|
37
|
+
|
38
|
+
event_manager.add_handler :string_input do |event|
|
39
|
+
root_window.route_keyboard_event event
|
40
|
+
end
|
41
|
+
|
42
|
+
event_manager.add_handler :resize do |event|
|
43
|
+
root_window.size = event[:size]
|
44
|
+
root_window.request_redraw_internal
|
45
|
+
end
|
46
|
+
|
47
|
+
event_manager.add_handler :pointer do |event|
|
48
|
+
root_window.pointer_event event.clone
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require "babel_bridge"
|
2
|
+
|
3
|
+
module WindowBlessing
|
4
|
+
class XtermEventParser < BabelBridge::Parser
|
5
|
+
rule :root, many(:event) do
|
6
|
+
def events
|
7
|
+
event.collect {|e| ev = e.event; ev[:string] ? ev : ev.merge(raw:e.to_s)}
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
rule :root do # ok to have empty string
|
12
|
+
def events; []; end
|
13
|
+
end
|
14
|
+
|
15
|
+
rule :event, /[^\x00-\x1f\x7F]+/ do
|
16
|
+
def event; {:type => :string_input, :string => to_s} end
|
17
|
+
end
|
18
|
+
|
19
|
+
rule :command, /[a-zA-Z]/
|
20
|
+
rule :number, /[0-9]+/ do
|
21
|
+
def to_i; to_s.to_i; end
|
22
|
+
end
|
23
|
+
rule :numbers, many(:number,";") do
|
24
|
+
def to_a; @array||= number.collect{|n| n.to_i} end
|
25
|
+
end
|
26
|
+
|
27
|
+
rule :event, "\e[8;", :numbers, "t" do
|
28
|
+
include GuiGeo
|
29
|
+
def event
|
30
|
+
{:type => :xterm_state, :state_type => :size, :state => point(*numbers.to_a.reverse)}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
rule :event, "\e[4;", :numbers, "t" do
|
35
|
+
include GuiGeo
|
36
|
+
def event
|
37
|
+
{:type => :xterm_state, :state_type => :display_pixel_size, :state => point(*numbers.to_a.reverse)}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
rule(:event, "\e\x7F") {def event;{:type => [:key_press,:backspace], :key => :backspace, :modifiers => [:alt]};end}
|
42
|
+
rule(:event, "\x7F") {def event;{:type => [:key_press,:backspace], :key => :backspace, :modifiers => []};end}
|
43
|
+
rule(:event, "\e[O") {def event;{:type => :blur};end}
|
44
|
+
rule(:event, "\e[I") {def event;{:type => :focus};end}
|
45
|
+
|
46
|
+
rule :event, :key_press do
|
47
|
+
def event; {:type => [:key_press,key], :key => key, :modifiers => modifiers} end
|
48
|
+
|
49
|
+
def modifiers
|
50
|
+
m = key_press.modifier
|
51
|
+
m ? m.modifiers : []
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
rule(:key_press, "\e", :modifier, "B") {def key;:down;end}
|
56
|
+
rule(:key_press, "\e", :modifier, "D") {def key;:left;end}
|
57
|
+
rule(:key_press, "\e", :modifier, "E") {def key;:begin;end}
|
58
|
+
rule(:key_press, "\e", :modifier, "C") {def key;:right;end}
|
59
|
+
rule(:key_press, "\e", :modifier, "A") {def key;:up;end}
|
60
|
+
rule(:key_press, "\e", :modifier, "Z") {def key;:reverse_tab;end}
|
61
|
+
rule(:key_press, "\e", :modifier, "H") {def key;:home;end}
|
62
|
+
rule(:key_press, "\e", :modifier, "F") {def key;:end;end}
|
63
|
+
rule(:key_press, "\e", :modifier, "P") {def key;:f1;end}
|
64
|
+
rule(:key_press, "\e", :modifier, "Q") {def key;:f2;end}
|
65
|
+
rule(:key_press, "\e", :modifier, "R") {def key;:f3;end}
|
66
|
+
rule(:key_press, "\e", :modifier, "S") {def key;:f4;end}
|
67
|
+
rule(:key_press, "\e", :modifier, "F") {def key;:home;end}
|
68
|
+
rule(:key_press, "\e", :modifier, "H") {def key;:end;end}
|
69
|
+
|
70
|
+
rule :modifier, "\e", :modifier do
|
71
|
+
def modifiers
|
72
|
+
(modifier.modifiers || []) + [:alt]
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
rule :modifier, "[", :numbers do
|
77
|
+
def modifiers
|
78
|
+
{
|
79
|
+
2 => [:shift],
|
80
|
+
3 => [:alt],
|
81
|
+
4 => [:shift, :alt],
|
82
|
+
5 => [:control],
|
83
|
+
6 => [:shift, :control],
|
84
|
+
7 => [:alt, :control],
|
85
|
+
8 => [:shift, :alt, :control],
|
86
|
+
}[numbers.to_a[-1].to_i] || []
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
rule :modifier, /[\[O]/ do
|
91
|
+
def modifiers; []; end
|
92
|
+
end
|
93
|
+
|
94
|
+
rule :event, "\e[", :number, "~" do
|
95
|
+
def event
|
96
|
+
{
|
97
|
+
:type => :key_press,
|
98
|
+
:key => {
|
99
|
+
3 => :delete,
|
100
|
+
2 => :insert,
|
101
|
+
6 => :page_down,
|
102
|
+
5 => :page_up,
|
103
|
+
13 => :f3,
|
104
|
+
14 => :f4,
|
105
|
+
15 => :f5,
|
106
|
+
17 => :f6,
|
107
|
+
18 => :f7,
|
108
|
+
19 => :f8,
|
109
|
+
20 => :f9,
|
110
|
+
21 => :f10,
|
111
|
+
23 => :f11,
|
112
|
+
24 => :f12,
|
113
|
+
}[number.to_i]
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
rule :event, "\e[", "M", match(/.../).as(:state) do
|
119
|
+
def event
|
120
|
+
s, x, y = state.to_s.unpack "CCC"
|
121
|
+
x -= 33
|
122
|
+
y -= 33
|
123
|
+
button_actions = {
|
124
|
+
32 => :button1_down, 33 => :button2_down, 34=> :button3_down, 35=>:button_up,
|
125
|
+
64 => :drag,
|
126
|
+
96 => :wheel_down, 97 => :wheel_up
|
127
|
+
}
|
128
|
+
{
|
129
|
+
type: [:pointer, button_actions[s&99]],
|
130
|
+
button: button_actions[s&99],
|
131
|
+
state: s,
|
132
|
+
loc: point(x,y)
|
133
|
+
}.tap do |h|
|
134
|
+
h[:shift_down] = true if (s&4)!=0
|
135
|
+
h[:alt_down] = true if (s&8)!=0
|
136
|
+
h[:control_down] = true if (s&16)!=0
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# catch-all for unknown xterm escape codes
|
142
|
+
rule :event, /\e\[[^a-zA-Z]*[a-zA-Z]/ do
|
143
|
+
def event
|
144
|
+
{:type => :unknown_xterm_code}
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
rule :event, /[\x00-\x1f]/ do
|
149
|
+
def event
|
150
|
+
char = "%c"%(to_s.getbyte(0)+"`".getbyte(0))
|
151
|
+
{:type => :key_press, :key => "control_#{char}".to_sym}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|