window_blessing 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,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,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
|