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,3 @@
1
+ module WindowBlessing
2
+ SMALLEST_FLOAT_DELTA = 0.000000001
3
+ end
@@ -0,0 +1,75 @@
1
+ module WindowBlessing
2
+
3
+ # Event handlers are procs which have one input: the event.
4
+ # There can be more than one handler per event-type. Handlers for the same event type are called in the reverse of the order they were added with add_handler.
5
+ # Event handlers return a true value if they handled the event and no more handlers should be called.
6
+ #
7
+ # Events are hashs. The :type field is a symbol specifying the event type. Other key/values are event-specific
8
+ #
9
+ # Special handlers:
10
+ # :all => gets all (real) events. Returning true will NOT stop event processing.
11
+ # All gets access to the events first - and can alter them
12
+ # All does NOT get :tick events
13
+ # :unhandled_event => if the event has no handler, this handler is used instead. New event looks like this:
14
+ # :type => :unhandled_event, :event => unhandled_event.clone
15
+ # :event_exception => if an exception escaped the event handler, a new event is handed to this handler. New event looks like this:
16
+ # :type => :event_exception, :event => original_event.clone, :exception => exception_caught, :handler => handler_that_threw_error
17
+ class EventManager
18
+ attr_accessor :event_handlers, :parent
19
+
20
+ def initialize(parent)
21
+ @parent = parent
22
+ @event_handlers = {}
23
+ add_handler(:event_exception) do |e|
24
+ XtermLog.log "#{self.class}(parent=#{parent.inspect}): event_exception: #{e[:exception].inspect} event: #{e[:event].inspect}"
25
+ XtermLog.log " "+ e[:exception].backtrace.join("\n ")
26
+ end
27
+ add_handler(){}
28
+ end
29
+
30
+ def inspect
31
+ "<#{self.class} :parent => #{parent.inspect} :handled_events => #{event_handlers.keys}>"
32
+ end
33
+
34
+ def add_handler(*event_type, &block)
35
+ event_handlers[event_type] ||= []
36
+ event_handlers[event_type] << block
37
+ end
38
+
39
+ def add_last_handler(*event_type, &block)
40
+ event_handlers[event_type] = [block] + (event_handlers[event_type] || [])
41
+ end
42
+
43
+ def send_to_each_handler(handlers, event)
44
+ return if !handlers && event[:type] == :unhandled_event
45
+ return handle_event :type => :unhandled_event, :event => event.clone unless handlers
46
+
47
+ handlers.reverse_each do |handler|
48
+ begin
49
+ handler.call event
50
+ rescue Exception => e
51
+ if event[:type] != :event_exception
52
+ handle_event :type => :event_exception, :event => event.clone, :exception => e, :handler => handler
53
+ else
54
+ XtermLog.log "exception in :event_exception handler: #{e.inspect}"
55
+ end
56
+ false
57
+ end
58
+ end
59
+ end
60
+
61
+ def handle_event(event)
62
+ type = event[:type]
63
+ type = [type] unless type.kind_of?(Array)
64
+
65
+ type.length.times.reverse_each do |l|
66
+ send_to_each_handler(event_handlers[type[0..l]], event)
67
+ end
68
+ send_to_each_handler(event_handlers[[]], event) unless type == [:tick] || type == [:unhandled_event]
69
+ end
70
+
71
+ def handle_events(events)
72
+ events.each {|event| handle_event(event)}
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,20 @@
1
+ module WindowBlessing
2
+ class EventQueue
3
+ attr_accessor :queue
4
+
5
+ def initialize
6
+ @queue = []
7
+ end
8
+
9
+ def <<(a)
10
+ case a
11
+ when Array then @queue += a
12
+ else @queue << a
13
+ end
14
+ end
15
+
16
+ def clear; @queue = [] end
17
+ def pop_all; @queue.tap {clear} end
18
+ def empty?; @queue.length == 0 end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ module WindowBlessing
2
+ module Evented
3
+ def event_manager
4
+ @event_manager ||= EventManager.new(self)
5
+ end
6
+
7
+ # define event handler
8
+ def on(*args,&block)
9
+ event_manager.add_handler *args, &block
10
+ self
11
+ end
12
+
13
+ def handle_event(event)
14
+ event[:object] = self
15
+ event_manager.handle_event(event)
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ module WindowBlessing
2
+
3
+ # There are two events to subscribe to on evented variables:
4
+ #
5
+ # on :change
6
+ # Subscribe if you need to update the Model when the value changes
7
+ # NOTE: a :refresh event is fired before every :change event
8
+ # Ex: if the user change a Slider, this event is fired allowing you to respond to that change
9
+ #
10
+ # on :refresh
11
+ # Subscribe if you only need to update the View when the value changes
12
+ # Ex: Sliders subscribe to :refresh to update their view when the value changes
13
+ # If you want to update the position of the slider, but not trigger any :change events, call .refresh(value)
14
+ #
15
+ # both :change and :refresh events only fire if the value actually changed
16
+ class EventedVariable
17
+ include Evented
18
+
19
+ def initialize(value)
20
+ @value = value
21
+ end
22
+
23
+ def inspect
24
+ "<#{self.class}:#{object_id} value:#{@value.inspect}>"
25
+ end
26
+
27
+ def get; clone_value(@value) end
28
+
29
+ # update the value & trigger :change and :refresh events
30
+ def set(value)
31
+ old_value = refresh(value)
32
+ handle_event :type => :change, :old_value => old_value, :value => value if old_value != value
33
+ old_value
34
+ end
35
+
36
+ # update the value & only trigger :refresh events
37
+ # subscribe to :refresh events if you need to know when the value changes, but you shouldn't change any model-state because of it
38
+ # if you are changing model-state, subscribe to :change
39
+ def refresh(value)
40
+ old_value = @value
41
+ @value = value
42
+ handle_event :type => :refresh, :old_value => old_value, :value => value if old_value != value
43
+ old_value
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,124 @@
1
+ require "gui_geometry"
2
+
3
+ module WindowBlessing
4
+ module Tools
5
+ include GuiGeo::Tools
6
+
7
+ # returns pos, span
8
+ # on return, pos is within 0..length and pos + span.length is <= length
9
+ def overlapping_span(pos, span, length)
10
+ if pos <= -span.length || pos >= length || length <= 0
11
+ return length, span.class.new
12
+ elsif pos < 0
13
+ span = span[-pos..-1]
14
+ pos = 0
15
+ end
16
+ return pos, span[0..(length - pos - 1)]
17
+ end
18
+
19
+ def clone_value(v)
20
+ case v
21
+ when Fixnum, Bignum, Float then v
22
+ else v.clone
23
+ end
24
+ end
25
+
26
+ # if the block is provided, yields the source elements and the target elements they are overlaying, in order, one at a time
27
+ def overlay_span(pos, source_span, target_span, &block)
28
+ pos, span = overlapping_span pos, source_span, target_span.length
29
+
30
+ return target_span if span.length == 0
31
+
32
+ if block
33
+ span = span.each_with_index.collect {|s,i| yield s, target_span[i+pos]}
34
+ end
35
+
36
+ if pos == 0
37
+ span + target_span[span.length..-1]
38
+ else
39
+ target_span[0..pos-1] + span + target_span[pos + span.length..-1]
40
+ end
41
+ end
42
+
43
+ def overlay2d(loc, source, target)
44
+ overlay_span(loc.y, source, target) do |s, t|
45
+ overlay_span loc.x, s, t
46
+ end
47
+ end
48
+
49
+ def resize2d(array2d, size, blank_element)
50
+ array2d ||= []
51
+ blank_element = [blank_element] unless blank_element.kind_of?(String)
52
+
53
+ if array2d.length != size.y
54
+ array2d = array2d[0..(size.y-1)]
55
+ blank_line = blank_element * size.x
56
+ array2d += (size.y - array2d.length).times.collect {blank_line.clone}
57
+ end
58
+
59
+ array2d.collect do |line|
60
+ if line.length!=size.x
61
+ line = line[0..(size.x-1)]
62
+ line + blank_element * (size.x - line.length)
63
+ else
64
+ line
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ def subarray2d(array2d, area)
71
+ size = point(array2d[0].length,array2d.length)
72
+ area = area | rect(size)
73
+ x_range = area.x_range
74
+ array2d[area.y_range].collect do |line|
75
+ line[x_range]
76
+ end
77
+ end
78
+
79
+ def fill_line(fill, length)
80
+ line = fill * (length/fill.length)
81
+ line = (line+fill)[0..length-1] if line.length != length
82
+ line
83
+ end
84
+
85
+ def gen_array2d(size, fill)
86
+ fill = case fill
87
+ when String, Array then fill
88
+ else [fill]
89
+ end
90
+
91
+ a = (size.x * size.y)
92
+ full = fill * ((a / fill.length) + 1)
93
+
94
+ if fill.kind_of?(String)
95
+ full.scan /.{#{size.x}}/
96
+ else
97
+ full.each_slice(size.x).collect {|a|a}
98
+ end[0..size.y-1]
99
+
100
+ end
101
+
102
+
103
+ # r, g, b are in 0..1
104
+ def rgb_screen_color(r, g, b)
105
+ return gray_screen_color(r) if r==g && g==b
106
+ 16 + (r*5.9).to_i * 36 + (g*5.9).to_i * 6 + (b*5.9).to_i
107
+ end
108
+
109
+ # g is in 0..1
110
+ def gray_screen_color(g)
111
+ g = (g*24.9).to_i
112
+ case g
113
+ when 0 then 0
114
+ when 24 then 15
115
+ else 232 + g
116
+ end
117
+ end
118
+
119
+ def log(str); XtermLog.log "#{self.inspect}: #{str}" end
120
+ def color(*args); WindowBlessing::Color.new *args end
121
+ def window(*args); WindowBlessing::Window.new *args end
122
+ def buffer(*args); WindowBlessing::Buffer.new *args end
123
+ end
124
+ end
@@ -0,0 +1,3 @@
1
+ module WindowBlessing
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,13 @@
1
+ module WindowBlessing
2
+ module Widgets
3
+ module DraggableBackground
4
+
5
+ def initialize(*args)
6
+ super *args
7
+ on :pointer, :button1_down do |event| @mouse_offset = event[:loc] end
8
+ on :pointer, :drag do |event| self.loc += event[:loc] - @mouse_offset end
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module WindowBlessing
2
+ module Widgets
3
+ class Label < WindowBlessing::Window
4
+ attr_accessor_with_redraw :text, :fg, :bg
5
+
6
+ def initialize(rect, text, fill_options={})
7
+ super rect
8
+ @text = text
9
+ @fg = fill_options[:fg]
10
+ @bg = fill_options[:bg]
11
+ request_redraw_internal
12
+ end
13
+
14
+ def pointer_inside?(loc) false; end
15
+
16
+ def draw_internal
17
+ buffer.contents = text
18
+ buffer.fill :fg => fg, :bg => bg
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ module WindowBlessing
2
+ module Widgets
3
+ class Slider < WindowBlessing::Window
4
+ include Evented
5
+ attr_reader :background, :evented_value
6
+ attr_accessor :key_press_step
7
+
8
+ # options
9
+ # :key_press_step => 0.1
10
+ def initialize(rect, evented_value, options={})
11
+ rect.size.y = 1
12
+ super rect
13
+ @evented_value = evented_value = case evented_value
14
+ when EventedVariable then evented_value
15
+ when Float then EventedVariable.new(evented_value)
16
+ else raise ArgumentError.new "invalid text type #{evented_value.inspect}(#{evented_value.class})"
17
+ end
18
+ self.bg = gray_screen_color(0.25)
19
+ @key_press_step = options[:key_press_step] || 0.1
20
+
21
+ on :pointer do |event|
22
+ x = event[:loc].x
23
+ evented_value.set bound(0.0, x / screen_value_range, 1.0)
24
+ end
25
+
26
+ evented_value.on :refresh do |event|
27
+ request_redraw_internal
28
+ end
29
+
30
+ on :key_press do |event|
31
+ case event[:key]
32
+ when :home then self.value = 0
33
+ when :end then self.value = 1
34
+ when :left then
35
+ self.value = max(0.0, self.value - @key_press_step) if @key_press_step
36
+ when :right then
37
+ self.value = min(1.0, self.value + @key_press_step) if @key_press_step
38
+ end
39
+ end
40
+ end
41
+
42
+ def draw_internal
43
+ super
44
+ buffer.fill :area => rect(point(handle_x,0),point(1,1)), :string => "+"
45
+ end
46
+
47
+ def handle_x; (value * screen_value_range).to_i end
48
+ def screen_value_range; size.x - 1.0 end
49
+ def value; evented_value.get end
50
+ def value=(v) evented_value.set(v) end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,92 @@
1
+ module WindowBlessing
2
+ module Widgets
3
+ class TextField < WindowBlessing::Window
4
+ NON_NEGATIVE_INTEGER_VALIDATOR = /^[0-9]*$/
5
+ attr_accessor_with_redraw :cursor_bg, :cursor_pos
6
+ attr_accessor :validator
7
+ attr_accessor :evented_value
8
+
9
+ def initialize(rect, evented_value, options={})
10
+ super rect
11
+ @evented_value = evented_value = case evented_value
12
+ when EventedVariable then evented_value
13
+ when String then EventedVariable.new(evented_value)
14
+ else raise ArgumentError.new "invalid text type #{evented_value.inspect}(#{evented_value.class})"
15
+ end
16
+
17
+ @validator = options[:validator]
18
+
19
+ @fg = options[:fg] || Color.gray
20
+ @bg = options[:bg] || Color.black
21
+ @cursor_bg = options[:cursor_bg] || (@fg + @bg) / 2
22
+ @cursor_pos = text.length
23
+ request_redraw_internal
24
+
25
+ on :pointer do |event|
26
+ self.cursor_pos = event[:loc].x
27
+ end
28
+
29
+ evented_value.on :refresh do |event|
30
+ request_redraw_internal
31
+ end
32
+
33
+ on :string_input do |event|
34
+ p = cursor_pos
35
+ s = event[:string]
36
+
37
+ self.text = if p==0
38
+ s + text
39
+ elsif p==text.length
40
+ text + s
41
+ else
42
+ text[0..p] + s + text[p+1..-1]
43
+ end
44
+ self.cursor_pos += s.length
45
+ end
46
+
47
+ on :key_press do |event|
48
+ case event[:key]
49
+ when :backspace then
50
+ if cursor_pos > 0
51
+ p = cursor_pos
52
+ before = text
53
+ self.text = if cursor_pos == 1
54
+ text[1..-1]
55
+ elsif cursor_pos == text.length
56
+ self.text = text[0..-2]
57
+ else
58
+ self.text = text[0..p-2] + text[p..-1]
59
+ end
60
+ log " before = #{before.inspect} after = #{text.inspect}"
61
+ self.cursor_pos -= 1
62
+ end
63
+
64
+ when :home then self.cursor_pos = 0
65
+ when :end then self.cursor_pos = text.length
66
+ when :left then self.cursor_pos -= 1
67
+ when :right then self.cursor_pos += 1
68
+ end
69
+ end
70
+
71
+ on :focus do
72
+ request_redraw_internal
73
+ end
74
+
75
+ on :blur do
76
+ request_redraw_internal
77
+ end
78
+ end
79
+
80
+ def text; evented_value.get end
81
+ def text=(t); evented_value.set(t) if !validator || t[validator] end
82
+
83
+ def draw_internal
84
+ @cursor_pos = bound(0, @cursor_pos, text.length)
85
+ buffer.contents = text
86
+ buffer.fill :fg => fg, :bg => bg
87
+ buffer.fill :area => rect(point(cursor_pos,0),point(1,1)), :bg => cursor_bg if focused?
88
+ end
89
+
90
+ end
91
+ end
92
+ end