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