window_blessing 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +4 -0
  3. data/Guardfile +7 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +43 -0
  6. data/Rakefile +12 -0
  7. data/bin/buffered_screen_demo.rb +92 -0
  8. data/bin/color_picker_demo.rb +176 -0
  9. data/bin/foiled_demo.rb +27 -0
  10. data/bin/text_editor_demo.rb +292 -0
  11. data/bin/windowed_screen_demo.rb +71 -0
  12. data/bin/xterm_screen_demo.rb +33 -0
  13. data/lib/window_blessing.rb +54 -0
  14. data/lib/window_blessing/buffer.rb +216 -0
  15. data/lib/window_blessing/buffered_screen.rb +29 -0
  16. data/lib/window_blessing/color.rb +71 -0
  17. data/lib/window_blessing/constants.rb +3 -0
  18. data/lib/window_blessing/event_manager.rb +75 -0
  19. data/lib/window_blessing/event_queue.rb +20 -0
  20. data/lib/window_blessing/evented.rb +19 -0
  21. data/lib/window_blessing/evented_variable.rb +47 -0
  22. data/lib/window_blessing/tools.rb +124 -0
  23. data/lib/window_blessing/version.rb +3 -0
  24. data/lib/window_blessing/widgets/draggable_background.rb +13 -0
  25. data/lib/window_blessing/widgets/label.rb +23 -0
  26. data/lib/window_blessing/widgets/slider.rb +53 -0
  27. data/lib/window_blessing/widgets/text_field.rb +92 -0
  28. data/lib/window_blessing/window.rb +273 -0
  29. data/lib/window_blessing/windowed_screen.rb +53 -0
  30. data/lib/window_blessing/xterm_event_parser.rb +156 -0
  31. data/lib/window_blessing/xterm_input.rb +40 -0
  32. data/lib/window_blessing/xterm_log.rb +7 -0
  33. data/lib/window_blessing/xterm_output.rb +213 -0
  34. data/lib/window_blessing/xterm_screen.rb +109 -0
  35. data/lib/window_blessing/xterm_state.rb +27 -0
  36. data/spec/buffer_spec.rb +170 -0
  37. data/spec/color_spec.rb +36 -0
  38. data/spec/spec_helper.rb +6 -0
  39. data/spec/tools_spec.rb +142 -0
  40. data/spec/window_spec.rb +61 -0
  41. data/window_blessing.gemspec +28 -0
  42. 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