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