Code Coverage
 
Lines
Covered
99.34% covered (success)
99.34%
299 / 301
1
require 'thefox-ext'
1
require 'pp'
1
module TheFox
1
module TermKit
##
# Base View class.
#
# A View is an abstraction of any view object.
1
class View
# The `name` variable is **FOR DEBUGGING ONLY**.
1
attr_accessor :name
1
attr_accessor :parent_view
1
attr_accessor :subviews
# Holds the content points for this View. A single point content is an instance of ViewContent class.
1
attr_accessor :grid
# Will be used for the actual rendering.
# The `@grid_cache` variable can hold *foreign* content points (see ViewContent) as well as own content points.
# Foreign content points are owned by subviews that are shown on this View as well. If a View has subviews but no own content on the `@grid` the `@grid_cache` variable holds only content points from its subviews. The View not just holds the content points of the subviews but also the content points of the subviews of subviews and so on. Through the deepest level of subviews. If you draw a point on a View calling `draw_point()` the point will also be drawn on the parent view through the top view. `@grid` holds only ViewContents of its own View. Not so the `@grid_cache` variable that also holds foreign content points.
1
attr_accessor :grid_cache
1
attr_reader :position
1
attr_reader :is_init_position
# Defines a maximum `width` and `height` (see Size) for a View to be rendered.
1
attr_reader :size
# Defines the stack order. This variable will only be used when the View has a parent view. The subview on the parent view with the highest zindex will be shown on the parent view. See `redraw_point_zindex()` method for details.
1
attr_reader :zindex
1
def initialize(name = nil)
#puts 'View->initialize'
495
@name = name # FOR DEBUG ONLY
495
@parent_view = nil
495
@subviews = Set.new
# @grid = Hash.new
495
@grid = ViewGrid.new
# @grid_cache = Hash.new
495
@grid_cache = ViewGrid.new
495
@is_visible = false
495
@position = Point.new(0, 0)
495
@is_init_position = true
495
@size = nil
495
@zindex = 1
end
##
# FOR DEBUG ONLY
# :nocov:
1
def pp_grid
@grid.map{ |y_pos, row|
0
[y_pos, row.map{ |x_pos, content| [x_pos, content.char] }.to_h]
0
}.to_h
end
##
# FOR DEBUG ONLY
1
def pp_grid_cache
@grid_cache.map{ |y_pos, row|
0
[y_pos,
row.map{ |x_pos, content|
# [x_pos, {'c' => content.char, 'v' => content.view.name}]
0
[x_pos, content.char]
}.to_h,
]
0
}.to_h
end
# :nocov:
1
def is_visible=(is_visible)
# puts "#{@name} -- is_visible= #{is_visible}"
431
trend = 0
431
if @is_visible && !is_visible
7
trend = -1
elsif !@is_visible && is_visible
420
trend = 1
end
431
@is_visible = is_visible
431
redraw_parent(trend)
end
1
def is_visible?
3385
@is_visible
end
1
def position=(new_position)
149
if !new_position.is_a?(Point)
1
raise ArgumentError, "Argument is not a Point -- #{new_position.class} given"
end
# puts "#{@name} -- position= old=#{@position} new=#{new_position}"
148
if @position != new_position
# puts "#{@name} -- position= diff"
107
if @parent_view.nil?
69
@position = new_position
else
# Keep old position.
38
old_position = @position
# Move it.
38
@position = new_position
38
x_max_i = x_max.to_i + 1
38
y_max_i = y_max.to_i + 1
# puts "x_max '#{x_max_i}'"
# puts "y_max '#{y_max_i}'"
38
new_area = Rect.new(nil, nil, x_max_i, y_max_i)
38
new_area.origin = new_position
38
new_area_points = new_area.to_points
38
old_area = Rect.new(nil, nil, x_max_i, y_max_i)
38
old_area.origin = old_position
# puts "#{@name} -- plain area a=#{area.inspect}"
# Redraw new position.
# puts "#{@name} -- new area #{new_area.inspect}"
# changes_new = {}
# changes_new = @parent_view.redraw_area_zindex(new_area)
38
@parent_view.redraw_area_zindex(new_area)
# puts "#{@name} -- redraw_area_zindex OK #{new_area.inspect}"
# STDIN.gets
# changes_new.each do |y_pos, row|
# row.each do |x_pos, content|
# new_point = Point.new(x_pos, y_pos)
# puts "#{@name} -- new content #{new_point} c=#{content.inspect}"
# end
# end
# puts
# puts "#{@name} -- @is_init_position = #{@is_init_position}"
# puts
# Redraw old position.
38
if !@is_init_position
26
parent_view = @parent_view
26
parent_level = 0
26
point_offset = Point.new(0, 0)
26
while parent_view
# puts "#{@name} -- l=#{parent_level} '#{parent_view}' -- old area #{old_area.inspect} #{point_offset}"
42
old_points = old_area.to_points
294
old_points_s = old_points.map{ |point| point.to_s }
294
new_points = new_area_points.map{ |point| (point + point_offset) }
294
new_points_s = new_points.map{ |point| point.to_s }
#bottom_points = old_points.map{ |point| (point - point_offset).to_s }
# rest_points = bottom_points - new_area_points
42
rest_points_s = old_points_s - new_points_s
244
rest_points = rest_points_s.map{ |point| Point.from_s(point) }
#rest_points = old_points - new_points
# puts "#{@name} -- old points #{old_points_s}"
# puts "#{@name} -- new points #{new_points_s}"
# puts "#{@name} -- bottom points #{bottom_points}"
# puts "#{@name} -- new points #{new_area_points}"
# puts "#{@name} -- rest points #{rest_points_s}"
42
rest_points.each do |point|
# puts "#{@name} -- #{parent_view} -- old content #{point}"
# changed = false
# changed = parent_view.grid_cache_erase_point(point)
202
parent_view.grid_cache_erase_point(point)
# puts "#{@name} -- #{parent_view} -- old content #{point} c=#{changed.inspect}"
end
42
old_area.origin += parent_view.position
42
point_offset += parent_view.position
# puts "#{@name} -- #{parent_view} -- pos parent #{parent_view.position} -> #{old_area.inspect} #{point_offset.inspect}"
# puts
42
parent_view = parent_view.parent_view
42
parent_level += 1
# puts
end
end
end
end
148
@is_init_position = false
end
1
def top_position
18
if @parent_view.nil?
9
@position
else
9
@parent_view.top_position
end
end
1
def size=(size)
30
if !size.is_a?(Size)
1
raise ArgumentError, "Argument is not a Size -- #{size.class} given"
end
29
@size = size
end
1
def zindex=(zindex)
17
@zindex = zindex
# puts "#{@name} -- set zindex #{zindex} p=#{@parent_view.nil? ? 'N' : 'Y'}"
17
if !@parent_view.nil?
4
@grid_cache.each do |y_pos, row|
4
row.each do |x_pos, content|
6
point = Point.new(x_pos + @position.x, y_pos + @position.y)
# puts "#{@name} -- set zindex #{zindex}, #{point.x}:#{point.y}"
6
@parent_view.redraw_point_zindex(point)
end
end
end
end
1
def width
24
keys = @grid_cache.map{ |y_pos, row| row.keys }.flatten
# pp keys
11
min = keys.min.to_i
11
max = keys.max.to_i
# puts "min '#{min}'"
# puts "max '#{max}'"
# puts
11
if keys.count > 0
8
max - min + 1
else
3
0
end
end
1
def height
234
keys = @grid_cache.keys
# pp keys
234
min = keys.min.to_i
234
max = keys.max.to_i
# puts "min '#{min}'"
# puts "max '#{max}'"
# puts
234
if keys.count > 0
232
max - min + 1
else
2
0
end
end
1
def x_max
# puts 'x_max'
# pp @grid_cache.map{ |y_pos, row| row.keys.max }.flatten.max
73
@grid_cache.map{ |y_pos, row| row.keys.max }.flatten.max
end
1
def y_max
38
@grid_cache.keys.max
end
1
def add_subview(subview)
291
if subview == self
1
raise ArgumentError, 'self given'
end
290
if !subview.is_a?(View)
1
raise ArgumentError, "Argument is not a View -- #{subview.class} given"
end
289
return unless @subviews.add?(subview)
289
subview.parent_view = self
289
@subviews.add(subview)
289
subview.grid_cache.each do |y_pos, row|
241
row.each do |x_pos, content|
1208
point = Point.new(x_pos + subview.position.x, y_pos + subview.position.y)
# puts "#{@name} -- add_subview, redraw_point_zindex #{point.x}:#{point.y}"
1208
redraw_point_zindex(point)
end
end
289
subview
end
1
def is_subview?(subview)
69
@subviews.include?(subview)
end
1
def remove_subview(subview)
66
if subview == self
1
raise ArgumentError, 'self given'
end
65
if !subview.is_a?(View)
1
raise ArgumentError, "Argument is not a View -- #{subview.class} given"
end
64
return unless @subviews.delete?(subview)
24
@subviews.delete(subview)
24
subview.grid_cache.each do |y_pos, row|
20
row.each do |x_pos, content|
106
point = Point.new(x_pos + subview.position.x, y_pos + subview.position.y)
# puts "#{@name} -- remove_subview, grid cache erase point #{point.x}:#{point.y}"
106
grid_cache_erase_point(point)
end
end
24
subview
end
1
def remove_subviews
1
@subviews.each do |subview|
3
remove_subview(subview)
end
end
##
# Draw a single Point to the current view.
1
def draw_point(point, content)
1708
case point
when Array, Hash
349
point = Point.new(point)
when Point
else
1
raise NotImplementedError, "#{content.class} class not implemented"
end
1707
case content
when String
1300
content = ViewContent.new(content, self)
when ViewContent
else
1
raise NotImplementedError, "#{content.class} class not implemented"
end
1706
is_foreign_point = content.view != self
1706
x_pos = point.x
1706
y_pos = point.y
1706
if is_foreign_point
else
1300
if !@grid[y_pos]
298
@grid[y_pos] = {}
end
1300
@grid[y_pos][x_pos] = content
1300
content.origin = point
end
# puts "#{@name} -- draw #{point} #{content.inspect}"
1706
new_point = Point.new(x_pos, y_pos)
# puts "#{@name} -- draw '#{content}' #{x_pos}:#{y_pos} foreign=#{is_foreign_point ? 'Y' : 'N'} from=#{content.view}"
# puts "#{@name} -- subviews: #{@subviews.count}"
1706
changed = nil
1706
if @subviews.count == 0
1218
changed = set_grid_cache(new_point, content)
else
# puts "#{@name} -- has subviews"
488
if @grid_cache[y_pos] && @grid_cache[y_pos][x_pos]
# puts "#{@name} -- found something on cached grid"
215
redraw_point_zindex(new_point)
else
# puts "#{@name} -- draw free point"
273
changed = set_grid_cache(new_point, content)
end
end
1706
if changed
1491
parent_draw_point(new_point, content)
end
1706
changed
end
# Draw a point on the parent View (`@parent_view`).
1
def parent_draw_point(point, content)
3148
if !@parent_view.nil? && is_visible?
406
new_point = Point.new(point.x + @position.x, point.y + @position.y)
# puts "#{@name} -- draw parent: #{@parent_view} #{new_point.x}:#{new_point.y} (#{point.x}:#{point.y})"
406
@parent_view.draw_point(new_point, content)
end
end
##
# Redraw to Parent View based on the visibility trend.
# The visibility trend is `0` for unchanged, `-1` will hide, `1` will appear.
#
# - `-1` means `is_visible` was set from `true` to `false`.
# - `1` means `is_visible` was set from `false` to `true`.
1
def redraw_parent(visibility_trend)
# puts "#{@name} -- redraw parent, t=#{visibility_trend}"
431
unless @parent_view.nil?
9
if visibility_trend == 1
3
@grid_cache.each do |y_pos, row|
3
row.each do |x_pos, content|
9
point = Point.new(x_pos, y_pos)
# puts "#{@name} -- redraw parent, draw, #{point}"
9
parent_draw_point(point, content)
end
end
6
elsif visibility_trend == -1
6
@grid_cache.each do |y_pos, row|
6
row.each do |x_pos, content|
# puts "#{@name} -- redraw parent, hide (#{@position.x}:#{@position.y}) #{x_pos}:#{y_pos}"
14
view = @parent_view
14
view_x_pos = x_pos + @position.x
14
view_y_pos = y_pos + @position.y
# Erase the content on all parent views.
14
while !view.nil?
# puts "#{@name} -- redraw parent, hide #{x_pos}:#{y_pos}, #{view} #{view_x_pos}:#{view_y_pos}"
17
view_content = view.grid_cache[view_y_pos] && view.grid_cache[view_y_pos][view_x_pos] ? view.grid_cache[view_y_pos][view_x_pos] : nil
# view_content = view.grid[view_y_pos] && view.grid[view_y_pos][view_x_pos] ? view.grid[view_y_pos][view_x_pos] : nil
17
if view_content
# puts "#{@name} -- redraw parent, hide #{x_pos}:#{y_pos}, #{view} #{view_x_pos}:#{view_y_pos}, content '#{view_content}'"
# Erase the content on the parent view only when the content is viewable on the parent view.
17
if view_content == content
# puts "#{@name} -- redraw parent, hide #{x_pos}:#{y_pos}, #{view} #{view_x_pos}:#{view_y_pos}, same"
16
view.grid_cache_erase_point(Point.new(view_x_pos, view_y_pos))
else
# puts "#{@name} -- redraw parent, hide #{x_pos}:#{y_pos}, #{view} #{view_x_pos}:#{view_y_pos}, not same"
# Break when reaching a foreign layer (view). This can happen when this view
# has a lower zindex and is concealed by another view.
1
break
end
else
# puts "#{@name} -- redraw parent, hide #{x_pos}:#{y_pos}, #{view} #{view_x_pos}:#{view_y_pos}, empty"
end
16
view_x_pos += view.position.x
16
view_y_pos += view.position.y
16
view = view.parent_view
end
end
end
end
end
end
1
def grid_erase
1
@grid.each do |y_pos, row|
1
row.each do |x_pos, content|
#puts "clean #{x_pos}:#{y_pos} '#{content}'"
3
point = Point.new(x_pos, y_pos)
3
grid_erase_point(point)
end
end
end
1
def grid_erase_point(point)
5
x_pos, y_pos = point.to_a
5
@grid[y_pos][x_pos] = ClearViewContent.new(nil, self, point)
5
grid_cache_erase_point(point)
end
##
# Erase a single Point of the cached Grid (`@grid_cache`).
#
# First call `redraw_point_zindex(point)` to redraw the `point`. If the `point` didn't change use a new ClearViewContent instance and set it only on `@grid_cache`. Not on `@grid` because this clearing point instance will be removed by `render()`.
1
def grid_cache_erase_point(point)
331
x_pos, y_pos = point.to_a
# puts "#{@name} -- erase point #{point}"
331
changed = nil
331
if @grid_cache[y_pos] && @grid_cache[y_pos][x_pos] && !@grid_cache[y_pos][x_pos].is_a?(ClearViewContent)
# puts "#{@name} -- erase point #{point}, ok found & delete"
270
@grid_cache[y_pos].delete(x_pos)
270
if @grid_cache[y_pos].count == 0
4
@grid_cache.delete(y_pos)
end
# puts "#{@name} -- erase point #{point}, redraw point zindex"
270
changed = redraw_point_zindex(point)
# puts "#{@name} -- erase point #{point}, changed=#{changed ? 'Y' : 'N'} #{changed.inspect}"
# When nothing has changed.
270
unless changed
# puts "#{@name} -- erase point #{point}, nothing changed"
112
content = ClearViewContent.new(nil, self, point)
# puts "#{@name} -- erase point #{point}, set ClearViewContent"
112
changed = set_grid_cache(point, content)
# puts "#{@name} -- erase point #{point}, set ClearViewContent: #{changed.inspect}"
#changed = content
else
# puts "#{@name} -- erase point #{point}, CHANGED #{changed.inspect}"
#set_grid_cache(point, changed)
end
else
# puts "#{@name} -- erase point #{point}, not found"
end
331
changed
end
1
def grid_cache_remove_point(point, cls = nil)
# puts "#{@name} -- remove point #{point}"
55
x_pos, y_pos = point.to_a
55
if @grid_cache[y_pos] && @grid_cache[y_pos][x_pos]
# puts "#{@name} -- cls=#{cls.inspect} #{@grid_cache[y_pos][x_pos].class}"
55
if cls.nil? || @grid_cache[y_pos][x_pos].is_a?(cls)
# puts "#{@name} -- remove point #{point}, ok"
55
@grid_cache[y_pos].delete(x_pos)
55
if @grid_cache[y_pos].count == 0
8
@grid_cache.delete(y_pos)
end
end
else
# puts "#{@name} -- remove point #{point}, not found"
end
end
##
# Redraw a single Point based on the `zindexes` of the subviews.
# Happens when a subview added, removed, hides, `zindex` changes, or draws.
#
# The subview with the highest `zindex` will be selected to set the content for this `point`. When no subview exists or all subviews are hidden look-up the Point on the `@grid` variable to set the Point on `@grid_cache`.
1
def redraw_point_zindex(point)
1886
x_pos = point.x
1886
y_pos = point.y
# puts "#{@name} -- redraw point zindex #{point}"
1886
views = @subviews
2921
.select{ |subview| subview.is_visible? && subview.zindex >= 1 }
.select{ |subview|
2905
subview_x_pos = x_pos - subview.position.x
2905
subview_y_pos = y_pos - subview.position.y
2905
content = subview.grid_cache[subview_y_pos] && subview.grid_cache[subview_y_pos][subview_x_pos]
# puts "#{@name} -- find '#{subview}' #{subview_x_pos}:#{subview_y_pos}, #{content.inspect}"
2905
!content.nil?
}
121
.sort{ |subview1, subview2| subview1.zindex <=> subview2.zindex }
# pp views.map{ |subview| subview.name }
1886
view = views.last
1886
content = nil
1886
if view.nil?
# When no subview was found, draw the current view
# if a point on the current view's grid exist.
# puts "#{@name} -- redraw point zindex #{point}, no view found"
169
if @grid[y_pos] && @grid[y_pos][x_pos]
# puts "#{@name} -- redraw point zindex #{point}, found something on the grid: '#{@grid[y_pos][x_pos]}'"
14
content = @grid[y_pos][x_pos]
else
# puts "#{@name} -- redraw point zindex #{point}, nothing on grid @ #{x_pos}:#{y_pos}"
155
if @grid_cache[y_pos] && @grid_cache[y_pos][x_pos]
3
content = @grid_cache[y_pos][x_pos]
3
unless content.is_a?(ClearViewContent)
# puts "#{@name} -- redraw point zindex #{point}, found something on the grid_cache: '#{@grid_cache[y_pos][x_pos]}', DELETE"
2
content = ClearViewContent.new(nil, self, point)
end
else
# puts "#{@name} -- redraw point zindex #{point}, nothing on grid_cache @ #{x_pos}:#{y_pos}"
end
end
else
1717
subview_x_pos = x_pos - view.position.x
1717
subview_y_pos = y_pos - view.position.y
1717
content = view.grid_cache[subview_y_pos][subview_x_pos]
# puts "#{@name} -- redraw point zindex #{point}, last view: '#{view}' #{subview_x_pos}:#{subview_y_pos} #{content.inspect}"
end
1886
changed = nil
1886
unless content.nil?
# puts "#{@name} -- redraw point zindex #{point}, set grid cache"
1734
changed = set_grid_cache(point, content)
end
1886
if changed
# puts "#{@name} -- redraw point zindex #{point}, changed #{content.inspect}"
1648
parent_draw_point(point, content)
else
# puts "#{@name} -- redraw point zindex #{point}, NOT changed"
end
1886
changed
end
1
def redraw_area_zindex(area)
39
if !area.is_a?(Rect)
1
raise ArgumentError, "Argument is not a Rect -- #{area.class} given"
end
# puts "#{@name} -- redraw area zindex #{area}"
38
changes = {}
38
area.y_range.each do |y_pos|
58
area.x_range.each do |x_pos|
176
point = Point.new(x_pos, y_pos)
176
unless changes[y_pos]
58
changes[y_pos] = {}
end
176
changes[y_pos][x_pos] = redraw_point_zindex(point)
end
end
38
changes
end
##
# Set a single Point on the cached Grid (`@grid_cache`).
# This method returns `true` only if the content of the `point` has changed.
1
def set_grid_cache(point, new_content)
3341
x_pos, y_pos = point.to_a
3341
if !@grid_cache[y_pos]
554
@grid_cache[y_pos] = {}
end
3341
changed =
if @grid_cache[y_pos][x_pos]
# puts "#{@name} -- set grid #{point}, x + y OK"
474
old_content = @grid_cache[y_pos][x_pos]
474
if old_content == new_content # && old_content.class == new_content.class
# puts "#{@name} -- set grid #{point}, equals, #{old_content.inspect} == #{new_content.inspect}"
87
false
else
# puts "#{@name} -- set grid #{point}, diff A #{@grid_cache[y_pos][x_pos].inspect}"
387
true
end
else
# puts "#{@name} -- set grid #{point}, x + y N/A"
2867
true
end
# puts "#{@name} -- set grid #{point} '#{new_content}' changed=#{changed ? 'Y' : 'N'}"
3341
if changed
3254
new_content.needs_rendering = true
3254
@grid_cache[y_pos][x_pos] = new_content
end
end
##
# Renders a View.
#
# Only ViewContents that needs a rendering (see ViewContent, `needs_rendering` attribute) will be returned. `needs_rendering` attribute is set to `false` by `render()`.
1
def render(area = nil)
# puts "#{@name} -- render area=#{area ? 'Y' : 'N'}"
89
if !@size.nil?
25
if area.nil?
24
area = Rect.new(0, 0)
24
area.size = @size
end
end
89
grid_filtered = @grid_cache
89
grid_filtered = grid_filtered
.map{ |y_pos, row|
1097
[y_pos, row.select{ |x_pos, content| content.needs_rendering }]
}
.to_h
89
if area.nil? || area.has_default_values?
else
36
grid_filtered = grid_filtered
.select{ |y_pos, row|
128
y_pos >= area.y
}
.map{ |y_pos, row|
613
[y_pos, row.select{ |x_pos, content| x_pos >= area.x }]
}
.to_h
36
if area.height
31
grid_filtered = grid_filtered
.select{ |y_pos, row|
102
y_pos <= area.y_max
}
end
36
if area.width
17
grid_filtered = grid_filtered
.map{ |y_pos, row|
294
[y_pos, row.select{ |x_pos, content| x_pos <= area.x_max }]
}
.to_h
end
end
275
grid_filtered = grid_filtered.select{ |y_pos, row| row.count > 0 }
89
grid_filtered.each do |y_pos, row|
169
row.each do |x_pos, content|
#point = Point.new(x_pos, y_pos)
# puts "#{@name} -- render #{point} #{content.inspect}"
602
content.needs_rendering = false
602
if content.is_a?(ClearViewContent)
37
if @grid[y_pos] && @grid[y_pos][x_pos] && @grid[y_pos][x_pos].is_a?(ClearViewContent)
# puts "#{@name} -- render remove grid ClearViewContent"
3
@grid[y_pos].delete(x_pos)
end
37
parent_view = content.view
37
parent_point = content.origin
# puts "#{@name} -- render PARENT START '#{parent_point}'"
37
while parent_view
# puts "#{@name} -- render PARENT '#{parent_view}' '#{parent_point}' (#{parent_view.position})"
# puts "#{@name} -- render remove grid_cache ClearViewContent"
55
parent_view.grid_cache_remove_point(parent_point, ClearViewContent)
55
parent_point += parent_view.position
55
parent_view = parent_view.parent_view
# sleep 0.1
end
end
end
end
# @grid.values.map{ |row| row.values }.flatten.select{ |content| content.needs_rendering }.each do |content|
# puts "render '#{content}'"
# content.needs_rendering = false
# end
89
grid_filtered
end
1
def needs_rendering?
@grid_cache
0
.map{ |y_pos, row| row.values.map{ |content| content.needs_rendering ? 1 : 0 } }
.flatten
0
.inject(:+) > 0
end
1
def to_s
21
@name
end
1
def inspect
6
"#<View name=#{@name} w=#{width}>"
end
end
end
end