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