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