termgui 0.0.4
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.
- checksums.yaml +7 -0
- data/Gemfile +14 -0
- data/README.md +321 -0
- data/lib/termgui.rb +1 -0
- data/src/action.rb +58 -0
- data/src/box.rb +90 -0
- data/src/color.rb +174 -0
- data/src/cursor.rb +69 -0
- data/src/editor/editor_base.rb +152 -0
- data/src/editor/editor_base_handlers.rb +116 -0
- data/src/element.rb +61 -0
- data/src/element_bounds.rb +111 -0
- data/src/element_box.rb +64 -0
- data/src/element_render.rb +102 -0
- data/src/element_style.rb +51 -0
- data/src/emitter.rb +102 -0
- data/src/emitter_state.rb +19 -0
- data/src/enterable.rb +93 -0
- data/src/event.rb +92 -0
- data/src/focus.rb +102 -0
- data/src/geometry.rb +53 -0
- data/src/image.rb +60 -0
- data/src/input.rb +85 -0
- data/src/input_grab.rb +17 -0
- data/src/input_time.rb +97 -0
- data/src/key.rb +114 -0
- data/src/log.rb +24 -0
- data/src/node.rb +117 -0
- data/src/node_attributes.rb +27 -0
- data/src/node_visit.rb +52 -0
- data/src/renderer.rb +119 -0
- data/src/renderer_cursor.rb +18 -0
- data/src/renderer_draw.rb +28 -0
- data/src/renderer_image.rb +31 -0
- data/src/renderer_print.rb +40 -0
- data/src/screen.rb +96 -0
- data/src/screen_element.rb +59 -0
- data/src/screen_input.rb +43 -0
- data/src/screen_renderer.rb +53 -0
- data/src/style.rb +149 -0
- data/src/tco/colouring.rb +248 -0
- data/src/tco/config.rb +57 -0
- data/src/tco/palette.rb +603 -0
- data/src/tco/tco_termgui.rb +30 -0
- data/src/termgui.rb +29 -0
- data/src/util/css.rb +98 -0
- data/src/util/css_query.rb +23 -0
- data/src/util/easing.rb +364 -0
- data/src/util/hash_object.rb +131 -0
- data/src/util/imagemagick.rb +27 -0
- data/src/util/justify.rb +20 -0
- data/src/util/unicode-categories.rb +572 -0
- data/src/util/wrap.rb +102 -0
- data/src/util.rb +110 -0
- data/src/widget/button.rb +33 -0
- data/src/widget/checkbox.rb +47 -0
- data/src/widget/col.rb +30 -0
- data/src/widget/image.rb +106 -0
- data/src/widget/inline.rb +40 -0
- data/src/widget/input_number.rb +73 -0
- data/src/widget/inputbox.rb +85 -0
- data/src/widget/label.rb +33 -0
- data/src/widget/modal.rb +69 -0
- data/src/widget/row.rb +26 -0
- data/src/widget/selectbox.rb +100 -0
- data/src/widget/textarea.rb +54 -0
- data/src/xml/xml.rb +80 -0
- data/test/action_test.rb +34 -0
- data/test/box_test.rb +15 -0
- data/test/css_test.rb +39 -0
- data/test/editor/editor_base_test.rb +201 -0
- data/test/element_bounds_test.rb +77 -0
- data/test/element_box_test.rb +8 -0
- data/test/element_render_test.rb +124 -0
- data/test/element_style_test.rb +85 -0
- data/test/element_test.rb +10 -0
- data/test/emitter_test.rb +108 -0
- data/test/event_test.rb +19 -0
- data/test/focus_test.rb +37 -0
- data/test/geometry_test.rb +12 -0
- data/test/input_test.rb +47 -0
- data/test/key_test.rb +14 -0
- data/test/log_test.rb +21 -0
- data/test/node_test.rb +105 -0
- data/test/performance/performance1.rb +48 -0
- data/test/renderer_test.rb +74 -0
- data/test/renderer_test_rect.rb +4 -0
- data/test/screen_test.rb +58 -0
- data/test/style_test.rb +18 -0
- data/test/termgui_test.rb +10 -0
- data/test/test_all.rb +30 -0
- data/test/util_hash_object_test.rb +93 -0
- data/test/util_test.rb +26 -0
- data/test/widget/checkbox_test.rb +99 -0
- data/test/widget/col_test.rb +87 -0
- data/test/widget/inline_test.rb +40 -0
- data/test/widget/label_test.rb +94 -0
- data/test/widget/row_test.rb +40 -0
- data/test/wrap_test.rb +11 -0
- data/test/xml_test.rb +77 -0
- metadata +148 -0
data/src/cursor.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require_relative 'style'
|
2
|
+
module TermGui
|
3
|
+
# minimalist artificial screen cursor. TODO: cursor can be manipulated via ansi escape codes - see npm.org/blessed
|
4
|
+
class Cursor
|
5
|
+
attr_accessor :interval, :x, :y, :enabled, :off, :on
|
6
|
+
|
7
|
+
def initialize(
|
8
|
+
on_interval: 0.3, off_interval: on_interval / 2,
|
9
|
+
x: 0, y: 0, enabled: false,
|
10
|
+
# by default we render the cursor next to its real x-coordinate
|
11
|
+
x_offset: 1,
|
12
|
+
screen: nil, off: ' ', on: '_',
|
13
|
+
on_style: Style.new(bg: 'white', fg: 'black'), off_style: Style.new(bg: 'black', fg: 'white')
|
14
|
+
)
|
15
|
+
@x = x
|
16
|
+
@y = y
|
17
|
+
@on_interval = on_interval
|
18
|
+
@off_interval = off_interval
|
19
|
+
@enabled = enabled
|
20
|
+
@x_offset = x_offset
|
21
|
+
@screen = screen
|
22
|
+
throw 'screen must be provided' unless @screen
|
23
|
+
@state = 0
|
24
|
+
@off = off
|
25
|
+
@on = on
|
26
|
+
@on_style = on_style
|
27
|
+
@off_style = off_style
|
28
|
+
end
|
29
|
+
|
30
|
+
def enable
|
31
|
+
disable
|
32
|
+
@enabled = true
|
33
|
+
@state = 0
|
34
|
+
tick
|
35
|
+
end
|
36
|
+
|
37
|
+
def draw_off
|
38
|
+
@screen.text(x: @x + @x_offset, y: @y, text: @off, style: @off_style)
|
39
|
+
end
|
40
|
+
|
41
|
+
def draw_on
|
42
|
+
@screen.text(x: @x + @x_offset, y: @y, text: @on, style: @on_style)
|
43
|
+
end
|
44
|
+
|
45
|
+
def disable
|
46
|
+
@enabled = false
|
47
|
+
@screen.clear_timeout @timeout if @timeout
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
# renders the cursor according to its state, toggling the state and finally schedulling to call it self according to on_interval, off_interval
|
53
|
+
def tick
|
54
|
+
if @state == 0
|
55
|
+
@state = 1
|
56
|
+
draw_on
|
57
|
+
elsif @state == 1
|
58
|
+
@state = 0
|
59
|
+
draw_off
|
60
|
+
else
|
61
|
+
throw 'unknown state ' + @state
|
62
|
+
end
|
63
|
+
@screen.clear_timeout @timeout if @timeout
|
64
|
+
@timeout = @screen.set_timeout(@state == 0 ? @on_interval : @off_interval) { tick } if @enabled
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
Cursor = TermGui::Cursor
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require_relative '../cursor'
|
2
|
+
require_relative '../log'
|
3
|
+
require_relative '../key'
|
4
|
+
require_relative 'editor_base_handlers'
|
5
|
+
|
6
|
+
# See TODO.md section Editor for status and TODOs
|
7
|
+
module TermGui
|
8
|
+
class EditorBase
|
9
|
+
include EditorBaseHandlers
|
10
|
+
|
11
|
+
attr_accessor :x, :y, :width, :height
|
12
|
+
|
13
|
+
def initialize(
|
14
|
+
text: '', screen: nil, x: 0, y: 0,
|
15
|
+
width: screen.width, height: screen.height,
|
16
|
+
cursor_x: nil, cursor_y: nil,
|
17
|
+
# if true other party will listen keys and call handle_key - if false it will listen any key by itlself
|
18
|
+
managed: false
|
19
|
+
)
|
20
|
+
@screen = screen || (throw 'screen not given')
|
21
|
+
@x = x
|
22
|
+
@y = y
|
23
|
+
@width = width
|
24
|
+
@height = height
|
25
|
+
@managed = managed
|
26
|
+
@cursor = Cursor.new(screen: @screen)
|
27
|
+
self.text = text
|
28
|
+
@cursor.y = @y + cursor_y if cursor_y
|
29
|
+
@cursor.x = @x + cursor_x if cursor_x
|
30
|
+
end
|
31
|
+
|
32
|
+
def cursor_x
|
33
|
+
@cursor.x - @x
|
34
|
+
end
|
35
|
+
|
36
|
+
def cursor_y
|
37
|
+
@cursor.y - @y
|
38
|
+
end
|
39
|
+
|
40
|
+
def text=(text)
|
41
|
+
@lines = text.split('\n')
|
42
|
+
@cursor.y = @y + @lines.length - 1
|
43
|
+
@cursor.x = @x + current_line.length - 1
|
44
|
+
end
|
45
|
+
|
46
|
+
def value=(v)
|
47
|
+
self.text = v
|
48
|
+
end
|
49
|
+
|
50
|
+
def text
|
51
|
+
@lines.join('\n')
|
52
|
+
end
|
53
|
+
|
54
|
+
def value
|
55
|
+
text
|
56
|
+
end
|
57
|
+
|
58
|
+
def enable(and_render = true)
|
59
|
+
disable
|
60
|
+
@cursor.enable
|
61
|
+
unless @managed
|
62
|
+
@key_listener = @screen.event.add_any_key_listener do |event|
|
63
|
+
handle_key event
|
64
|
+
end
|
65
|
+
end
|
66
|
+
render if and_render
|
67
|
+
end
|
68
|
+
|
69
|
+
def disable
|
70
|
+
@cursor.disable
|
71
|
+
@screen.event.remove_any_key_listener @key_listener if @key_listener
|
72
|
+
@key_listener = nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def enabled
|
76
|
+
@cursor.enabled
|
77
|
+
end
|
78
|
+
|
79
|
+
def render
|
80
|
+
@screen.clear
|
81
|
+
# @screen.rect(x: x, y: y, width: width, height: height, style: Style.new)
|
82
|
+
@screen.render
|
83
|
+
@lines.each_with_index do |line, i|
|
84
|
+
@screen.text(x: @x, y: @y + i, text: line)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# public so keys can be programmatically simlated - useful for tests instead of calling screen.event.handle_keythat emits globally.
|
89
|
+
def handle_key(event)
|
90
|
+
if !enabled
|
91
|
+
return
|
92
|
+
elsif ['down'].include? event.key
|
93
|
+
cursor_down
|
94
|
+
elsif ['up'].include? event.key
|
95
|
+
cursor_up
|
96
|
+
elsif ['right'].include? event.key
|
97
|
+
cursor_right
|
98
|
+
elsif ['left'].include? event.key
|
99
|
+
cursor_left
|
100
|
+
elsif ['enter'].include? event.key
|
101
|
+
handle_enter
|
102
|
+
elsif ['tab'].include? event.key
|
103
|
+
insert_chars(' ') # TODO: hack - adding a tab will break since we assume all chars width is 1 (1 column per char) - it will also fail for unicode chars width>1
|
104
|
+
elsif ['S-tab'].include? event.key
|
105
|
+
# TODO: remove tab line prefix
|
106
|
+
elsif event.key == 'backspace'
|
107
|
+
handle_backspace
|
108
|
+
elsif event.key == 'delete'
|
109
|
+
handle_delete
|
110
|
+
elsif alphanumeric?(event.raw) || %w[space tab].include?(event.key)
|
111
|
+
insert_chars(event.raw)
|
112
|
+
end
|
113
|
+
|
114
|
+
@cursor.off = current_char || @cursor.off
|
115
|
+
@cursor.on = current_char || @cursor.on
|
116
|
+
render
|
117
|
+
@cursor.draw_on
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
|
122
|
+
def current_line
|
123
|
+
@lines[current_y] || ''
|
124
|
+
end
|
125
|
+
|
126
|
+
def current_line=(value)
|
127
|
+
@lines[current_y] = value || ''
|
128
|
+
end
|
129
|
+
|
130
|
+
def current_char
|
131
|
+
current_line[[[current_x, current_line.length].min, 0].max] || ' '
|
132
|
+
end
|
133
|
+
|
134
|
+
def current_x
|
135
|
+
@cursor.x - @x
|
136
|
+
end
|
137
|
+
|
138
|
+
def current_x=(x)
|
139
|
+
@cursor.x = @x + x
|
140
|
+
end
|
141
|
+
|
142
|
+
def current_y
|
143
|
+
@cursor.y - @y
|
144
|
+
end
|
145
|
+
|
146
|
+
def current_y=(y)
|
147
|
+
@cursor.y = @y + y
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
EditorBase = TermGui::EditorBase
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require_relative '../cursor'
|
2
|
+
require_relative '../log'
|
3
|
+
require_relative '../key'
|
4
|
+
|
5
|
+
module TermGui
|
6
|
+
module EditorBaseHandlers
|
7
|
+
def handle_enter
|
8
|
+
if current_x >= current_line.length
|
9
|
+
@lines.insert(current_y + 1, '')
|
10
|
+
else
|
11
|
+
line1 = current_x == 0 ? '' : current_line[0..[current_x - 1, 0].max]
|
12
|
+
line2 = current_line[[current_x, current_line.length].min..current_line.length]
|
13
|
+
@lines.delete_at(current_y)
|
14
|
+
@lines.insert(current_y, line1, line2)
|
15
|
+
end
|
16
|
+
self.current_y += 1
|
17
|
+
self.current_x = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def insert_chars(s)
|
21
|
+
prefix = current_x < 1 ? '' : current_line[0..[current_x - 1, 0].max] || ''
|
22
|
+
postfix = current_line[[current_x, 0].max..current_line.length] || ''
|
23
|
+
self.current_line = prefix + s + postfix
|
24
|
+
self.current_x += s.length
|
25
|
+
end
|
26
|
+
|
27
|
+
def handle_delete
|
28
|
+
if current_x >= current_line.length && current_y >= @lines.length - 1
|
29
|
+
@screen.alert
|
30
|
+
else
|
31
|
+
if current_line.length <= 1
|
32
|
+
self.current_x = 0
|
33
|
+
self.current_line = ''
|
34
|
+
else
|
35
|
+
prefix = current_line[0..[current_x - 1, 0].max]
|
36
|
+
postfix = current_line[[current_x + 1, current_line.length].min..current_line.length] || ''
|
37
|
+
self.current_line = prefix + postfix
|
38
|
+
# TODO: join lines if at the end of line and there's a following
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def cursor_down
|
44
|
+
if self.current_y == @lines.length - 1
|
45
|
+
if current_x > current_line.length
|
46
|
+
@screen.alert
|
47
|
+
else
|
48
|
+
self.current_x = current_line.length
|
49
|
+
end
|
50
|
+
else
|
51
|
+
self.current_y = self.current_y == @lines.length - 1 ? current_y : current_y + 1
|
52
|
+
self.current_x = [current_x, current_line.length].min
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def cursor_right
|
57
|
+
if current_x >= current_line.length
|
58
|
+
if current_y < @lines.length - 1
|
59
|
+
self.current_y += 1
|
60
|
+
self.current_x = 0
|
61
|
+
else
|
62
|
+
@screen.alert
|
63
|
+
end
|
64
|
+
else
|
65
|
+
self.current_x += 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def cursor_left
|
70
|
+
if current_x < 1
|
71
|
+
if current_y > 0
|
72
|
+
self.current_y -= 1
|
73
|
+
self.current_x = current_line.length
|
74
|
+
else
|
75
|
+
@screen.alert
|
76
|
+
end
|
77
|
+
else
|
78
|
+
self.current_x -= 1
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def handle_backspace
|
83
|
+
if current_x < 1 && current_y <= 0
|
84
|
+
@screen.alert
|
85
|
+
elsif current_x < 1
|
86
|
+
old_line = (@lines.delete_at(current_y) unless @lines.empty?) || ''
|
87
|
+
self.current_y = [current_y - 1, 0].max
|
88
|
+
self.current_x = current_line.length
|
89
|
+
self.current_line = current_line + old_line
|
90
|
+
elsif current_line.length <= 1
|
91
|
+
self.current_x = 0
|
92
|
+
self.current_line = ''
|
93
|
+
else
|
94
|
+
prefix = current_line[0..[current_x - 2, 0].max]
|
95
|
+
postfix = current_line[[current_x, current_line.length].min..current_line.length] || ''
|
96
|
+
self.current_line = prefix + postfix
|
97
|
+
self.current_x = [current_x - 1, 0].max
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def cursor_up
|
102
|
+
if self.current_y == 0
|
103
|
+
if self.current_x == 0
|
104
|
+
@screen.alert
|
105
|
+
else
|
106
|
+
self.current_x = 0
|
107
|
+
end
|
108
|
+
else
|
109
|
+
self.current_y = [current_y - 1, 0].max
|
110
|
+
self.current_x = [current_x, current_line.length - 1].min
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
EditorBaseHandlers = TermGui::EditorBaseHandlers
|
data/src/element.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative 'node'
|
2
|
+
require_relative 'style'
|
3
|
+
require_relative 'renderer'
|
4
|
+
require_relative 'element_bounds'
|
5
|
+
require_relative 'element_box'
|
6
|
+
require_relative 'element_render'
|
7
|
+
require_relative 'element_style'
|
8
|
+
|
9
|
+
module TermGui
|
10
|
+
# Node responsible of
|
11
|
+
# * x, y, width, height, abs_x, abs_y
|
12
|
+
# * rendering text, wrap (TODO)
|
13
|
+
# * border, margin & padding & abs_* update (TODO)
|
14
|
+
# * scroll
|
15
|
+
# TODO: separate each responsibility on its module or subclass
|
16
|
+
class Element < Node
|
17
|
+
include ElementBounds
|
18
|
+
include ElementBox
|
19
|
+
include ElementRender
|
20
|
+
include ElementStyle
|
21
|
+
|
22
|
+
def initialize(**args)
|
23
|
+
super
|
24
|
+
install(%i[focus blur action enter escape])
|
25
|
+
args[:attributes] = { x: args[:x] || 0, y: args[:y] || 0, width: args[:width] || 0, height: args[:height] || 0 } if args[:attributes] == nil
|
26
|
+
a = {}.merge(args, args[:attributes] || {})
|
27
|
+
a[:style] = default_style.assign(Style.from_hash(a[:attributes][:style])).assign(Style.from_hash(a[:style]))
|
28
|
+
self.attributes = a
|
29
|
+
self.style = a[:style]
|
30
|
+
on(%i[focus blur action enter escape]) do |e|
|
31
|
+
s = root_screen
|
32
|
+
if s
|
33
|
+
if e.name == 'action'
|
34
|
+
set_attribute('actioned', true)
|
35
|
+
render s
|
36
|
+
s.set_timeout(get_attribute('actioned-interval') || 0.2) do
|
37
|
+
set_attribute('actioned', false)
|
38
|
+
render s
|
39
|
+
end
|
40
|
+
else
|
41
|
+
render s
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def focus
|
48
|
+
if get_attribute('focusable')
|
49
|
+
root_screen&.focus&.focused = self
|
50
|
+
trigger :bounds_change
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def text=(v)
|
55
|
+
@text = v
|
56
|
+
trigger :bounds_change
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Element = TermGui::Element
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require_relative 'util'
|
2
|
+
require_relative 'geometry'
|
3
|
+
|
4
|
+
# Adds support for Element's x, y, abs_x, abs_y, width, height, abs_width, abs_height, offset (scroll viewport)
|
5
|
+
module ElementBounds
|
6
|
+
def initialize(**args)
|
7
|
+
super
|
8
|
+
install(:bounds_change)
|
9
|
+
end
|
10
|
+
|
11
|
+
def x=(x)
|
12
|
+
set_attribute('x', x)
|
13
|
+
trigger :bounds_change
|
14
|
+
end
|
15
|
+
|
16
|
+
def x
|
17
|
+
get_attribute('x') || 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def abs_x
|
21
|
+
val = if is_percent x
|
22
|
+
((@parent ? @parent.abs_x : 0) + x * (@parent ? @parent.abs_width : abs_width)).truncate
|
23
|
+
else
|
24
|
+
((@parent ? @parent.abs_x : 0) + x).truncate
|
25
|
+
end
|
26
|
+
o = @parent && parent.offset
|
27
|
+
val -= o.left if o
|
28
|
+
# val -= 1 if border
|
29
|
+
val
|
30
|
+
end
|
31
|
+
|
32
|
+
def offset
|
33
|
+
v = get_attribute('offset')
|
34
|
+
set_attribute('offset', v = Offset.new) unless v
|
35
|
+
v
|
36
|
+
end
|
37
|
+
|
38
|
+
def offset=(value)
|
39
|
+
set_attribute('offset', value)
|
40
|
+
trigger :bounds_change
|
41
|
+
end
|
42
|
+
|
43
|
+
def abs_x=(value)
|
44
|
+
# TODO: parent offset
|
45
|
+
self.x = value - (@parent ? @parent.abs_x : 0).truncate
|
46
|
+
end
|
47
|
+
|
48
|
+
def y=(y)
|
49
|
+
set_attribute('y', y)
|
50
|
+
trigger :bounds_change
|
51
|
+
end
|
52
|
+
|
53
|
+
def y
|
54
|
+
get_attribute('y') || 0
|
55
|
+
end
|
56
|
+
|
57
|
+
def abs_y
|
58
|
+
val = if is_percent y
|
59
|
+
((@parent ? @parent.abs_y : 0) + y * (@parent ? @parent.abs_height : abs_height)).truncate
|
60
|
+
else
|
61
|
+
((@parent ? @parent.abs_y : 0) + y).truncate
|
62
|
+
end
|
63
|
+
o = @parent && parent.offset
|
64
|
+
val -= o.top if o
|
65
|
+
# val += 1 if border
|
66
|
+
val
|
67
|
+
end
|
68
|
+
|
69
|
+
def abs_y=(value)
|
70
|
+
# TODO: parent offset
|
71
|
+
self.y = value - (@parent ? @parent.abs_y : 0).truncate
|
72
|
+
end
|
73
|
+
|
74
|
+
def width=(width)
|
75
|
+
set_attribute('width', width)
|
76
|
+
trigger :bounds_change
|
77
|
+
end
|
78
|
+
|
79
|
+
def width
|
80
|
+
get_attribute('width') || 0
|
81
|
+
end
|
82
|
+
|
83
|
+
def abs_width
|
84
|
+
val = if (is_percent width) && @parent
|
85
|
+
(@parent.abs_width * width).truncate
|
86
|
+
else
|
87
|
+
width.truncate
|
88
|
+
end
|
89
|
+
val += 2 if border
|
90
|
+
val
|
91
|
+
end
|
92
|
+
|
93
|
+
def height=(height)
|
94
|
+
set_attribute('height', height)
|
95
|
+
trigger :bounds_change
|
96
|
+
end
|
97
|
+
|
98
|
+
def height
|
99
|
+
get_attribute('height') || 0
|
100
|
+
end
|
101
|
+
|
102
|
+
def abs_height
|
103
|
+
val = if is_percent height
|
104
|
+
(@parent ? @parent.abs_height * height : 0).truncate
|
105
|
+
else
|
106
|
+
height.truncate
|
107
|
+
end
|
108
|
+
val += 2 if border&.style
|
109
|
+
val
|
110
|
+
end
|
111
|
+
end
|
data/src/element_box.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative 'geometry'
|
2
|
+
require_relative 'element_bounds'
|
3
|
+
require_relative 'util/hash_object'
|
4
|
+
|
5
|
+
# Adds html-like box-model support for Element: margin, padding, border
|
6
|
+
module ElementBox
|
7
|
+
include ElementBounds
|
8
|
+
|
9
|
+
def abs_content_x
|
10
|
+
abs_x + abs_padding.left + (border ? 1 : 0)
|
11
|
+
end
|
12
|
+
|
13
|
+
def abs_content_y
|
14
|
+
abs_y + abs_padding.top + (border ? 1 : 0)
|
15
|
+
end
|
16
|
+
|
17
|
+
def abs_content_width
|
18
|
+
m = abs_padding
|
19
|
+
abs_width - m.left - m.right - (border ? 2 : 0)
|
20
|
+
end
|
21
|
+
|
22
|
+
def abs_content_height
|
23
|
+
m = abs_padding
|
24
|
+
abs_height - m.top - m.bottom - (border ? 2 : 0)
|
25
|
+
end
|
26
|
+
|
27
|
+
def abs_content_bounds
|
28
|
+
y = abs_content_y
|
29
|
+
x = abs_content_x
|
30
|
+
Bounds.new(left: x, right: x + abs_content_width, top: y, bottom: y + abs_content_height)
|
31
|
+
end
|
32
|
+
|
33
|
+
def abs_content_box
|
34
|
+
Rectangle.new(x: abs_content_x, y: abs_content_y, width: abs_content_width, height: abs_content - abs_content_height)
|
35
|
+
end
|
36
|
+
|
37
|
+
# returns padding as Offset instance
|
38
|
+
def padding
|
39
|
+
padding = get_style('padding')
|
40
|
+
if !padding
|
41
|
+
Bounds.new
|
42
|
+
elsif padding.instance_of? Hash
|
43
|
+
Offset.from_hash(padding)
|
44
|
+
else
|
45
|
+
padding
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def padding=(padding)
|
50
|
+
set_style('padding', padding)
|
51
|
+
trigger :bounds_change
|
52
|
+
end
|
53
|
+
|
54
|
+
# computes absolute padding transforming padding percents to absolute pixel amounts.
|
55
|
+
def abs_padding
|
56
|
+
p = padding
|
57
|
+
Bounds.new(
|
58
|
+
top: is_percent(p.top) ? (p.top * abs_height).truncate : p.top,
|
59
|
+
bottom: is_percent(p.bottom) ? (p.bottom * abs_height).truncate : p.bottom,
|
60
|
+
left: is_percent(p.left) ? (p.left * abs_width).truncate : p.left,
|
61
|
+
right: is_percent(p.right) ? (p.right * abs_width).truncate : p.right
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require_relative 'element_box'
|
2
|
+
require_relative 'element_style'
|
3
|
+
require_relative 'util'
|
4
|
+
require_relative 'util/wrap'
|
5
|
+
require_relative 'log'
|
6
|
+
require_relative 'box'
|
7
|
+
|
8
|
+
# implements element rendering (self, border, child, text) for which it depends on ElementBox
|
9
|
+
module ElementRender
|
10
|
+
include ElementBox
|
11
|
+
include ElementStyle
|
12
|
+
|
13
|
+
attr_accessor :dirty
|
14
|
+
|
15
|
+
def initialize(**args)
|
16
|
+
super
|
17
|
+
@render_cache = args[:render_cache] || false
|
18
|
+
@dirty = true
|
19
|
+
@render_cache_data = nil
|
20
|
+
on(:bounds_change) do
|
21
|
+
@dirty = true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def border
|
26
|
+
style&.border
|
27
|
+
end
|
28
|
+
|
29
|
+
def render(screen = root_screen, force = false)
|
30
|
+
self.dirty = true if force
|
31
|
+
return '' unless screen
|
32
|
+
|
33
|
+
trigger(:before_render)
|
34
|
+
|
35
|
+
layout
|
36
|
+
if @render_cache && !dirty && @render_cache_data
|
37
|
+
screen.write @render_cache_data
|
38
|
+
else
|
39
|
+
@render_cache_data = [
|
40
|
+
render_border(screen),
|
41
|
+
render_self(screen),
|
42
|
+
render_text(screen)
|
43
|
+
].join('')
|
44
|
+
end
|
45
|
+
trigger(:after_render)
|
46
|
+
self.dirty = false if @render_cache
|
47
|
+
[@render_cache_data, render_children(screen)].join('')
|
48
|
+
end
|
49
|
+
|
50
|
+
def root_screen
|
51
|
+
@parent&.root_screen
|
52
|
+
end
|
53
|
+
|
54
|
+
def clear
|
55
|
+
root_screen.rect(x: abs_x, y: abs_y, width: abs_width, height: abs_height, style: parent.final_style) if parent && root_screen
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def render_self(screen)
|
61
|
+
screen.rect(
|
62
|
+
x: abs_x + (border ? 1 : 0),
|
63
|
+
y: abs_y + (border ? 1 : 0),
|
64
|
+
width: abs_width - (border ? 2 : 0),
|
65
|
+
height: abs_height - (border ? 2 : 0),
|
66
|
+
ch: get_attribute('ch'),
|
67
|
+
style: final_style
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
# IMPORTANT: border is rendered in a +2 bigger rectangle that sourounds actual element bounds (abs_* methods)
|
72
|
+
def render_border(screen)
|
73
|
+
border ? screen.box(abs_x, abs_y, abs_width, abs_height, border.style, border_style) : ''
|
74
|
+
end
|
75
|
+
|
76
|
+
def render_text(screen)
|
77
|
+
(render_text_lines.map.with_index do |line, i|
|
78
|
+
screen.text(x: abs_content_x, y: abs_content_y + i, text: line, style: final_style)
|
79
|
+
end).join('')
|
80
|
+
end
|
81
|
+
|
82
|
+
def render_children(screen)
|
83
|
+
(@children.map do |c|
|
84
|
+
c.render(screen)
|
85
|
+
end).join('')
|
86
|
+
end
|
87
|
+
|
88
|
+
def render_text_lines(text = @text || '')
|
89
|
+
text&.length ? (style.wrap ? wrap_text(text, abs_content_width) : text.split('\n')) : []
|
90
|
+
end
|
91
|
+
|
92
|
+
# can be used by text widgets like labels or buttons to automatically set preffered size according to its text
|
93
|
+
def render_text_size(text = @text)
|
94
|
+
lines = render_text_lines(text)
|
95
|
+
width = lines.map(&:length).max
|
96
|
+
height = lines.length
|
97
|
+
{ width: width, height: height }
|
98
|
+
end
|
99
|
+
|
100
|
+
def layout
|
101
|
+
end
|
102
|
+
end
|