whirled_peas 0.1.0 → 0.4.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.
- checksums.yaml +4 -4
- data/.travis.yml +2 -0
- data/CHANGELOG.md +29 -0
- data/Gemfile +1 -0
- data/README.md +213 -48
- data/bin/title_fonts +6 -0
- data/exe/whirled_peas +7 -0
- data/lib/whirled_peas.rb +12 -30
- data/lib/whirled_peas/command_line.rb +270 -0
- data/lib/whirled_peas/config.rb +21 -0
- data/lib/whirled_peas/errors.rb +5 -0
- data/lib/whirled_peas/frame.rb +0 -7
- data/lib/whirled_peas/frame/event_loop.rb +91 -0
- data/lib/whirled_peas/frame/print_consumer.rb +33 -0
- data/lib/whirled_peas/frame/producer.rb +35 -31
- data/lib/whirled_peas/template.rb +5 -0
- data/lib/whirled_peas/template/element.rb +230 -0
- data/lib/whirled_peas/{ui → template}/settings.rb +28 -10
- data/lib/whirled_peas/ui.rb +1 -3
- data/lib/whirled_peas/ui/canvas.rb +37 -4
- data/lib/whirled_peas/ui/painter.rb +22 -18
- data/lib/whirled_peas/ui/screen.rb +24 -23
- data/lib/whirled_peas/utils.rb +5 -0
- data/lib/whirled_peas/utils/ansi.rb +103 -0
- data/lib/whirled_peas/{ui/ansi.rb → utils/color.rb} +23 -76
- data/lib/whirled_peas/utils/title_font.rb +75 -0
- data/lib/whirled_peas/version.rb +1 -1
- data/whirled_peas.gemspec +4 -2
- metadata +22 -18
- data/lib/whirled_peas/frame/consumer.rb +0 -61
- data/lib/whirled_peas/frame/loop.rb +0 -56
- data/lib/whirled_peas/ui/element.rb +0 -199
- data/lib/whirled_peas/ui/stroke.rb +0 -29
- data/sandbox/auto.rb +0 -13
- data/sandbox/box.rb +0 -19
- data/sandbox/grid.rb +0 -13
- data/sandbox/sandbox.rb +0 -17
- data/sandbox/text.rb +0 -33
@@ -0,0 +1,75 @@
|
|
1
|
+
module WhirledPeas
|
2
|
+
module Utils
|
3
|
+
module TitleFont
|
4
|
+
PREFERRED_DEFAULT_FONT = :ansishadow
|
5
|
+
FONT_CACHE = File.join(ENV['HOME'] || '.', '.whirled_peas.fonts')
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def validate!(font_key)
|
9
|
+
return unless font_key
|
10
|
+
return font_key if fonts.key?(font_key)
|
11
|
+
allowed = fonts.keys.map(&:inspect).sort
|
12
|
+
message = "Unrecognized font: '#{font_key.inspect}', expecting one of #{allowed.join(', ')}"
|
13
|
+
raise ArgumentError, message
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s(string, font_key)
|
17
|
+
require 'ruby_figlet'
|
18
|
+
RubyFiglet::Figlet.new(string, fonts[font_key]).to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def fonts
|
22
|
+
return @fonts if @fonts
|
23
|
+
File.exist?(FONT_CACHE) ? load_fonts_from_cache : load_fonts_from_figlet
|
24
|
+
@fonts
|
25
|
+
end
|
26
|
+
|
27
|
+
def load_fonts_from_cache
|
28
|
+
require 'json'
|
29
|
+
@fonts = {}
|
30
|
+
File.open(FONT_CACHE, 'r') do |fp|
|
31
|
+
JSON.parse(fp.read).each do |key, value|
|
32
|
+
@fonts[key.to_sym] = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@fonts
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_fonts_from_figlet
|
39
|
+
require 'ruby_figlet'
|
40
|
+
require 'json'
|
41
|
+
@fonts = {}
|
42
|
+
RubyFiglet::Figlet.available.strip.split("\n").each do |font|
|
43
|
+
# Load the font to be sure it exists on the system
|
44
|
+
next unless available?(font)
|
45
|
+
string_key = font.downcase.gsub(/\W/, '')
|
46
|
+
string_key = "_#{string_key}" if string_key[0] !~ /[a-z]/
|
47
|
+
string_key.gsub!(/\d+$/, '')
|
48
|
+
next if @fonts.key?(string_key)
|
49
|
+
@fonts[string_key.to_sym] = font
|
50
|
+
end
|
51
|
+
@fonts[:default] ||= @fonts[PREFERRED_DEFAULT_FONT] || @fonts.values.first
|
52
|
+
File.open(FONT_CACHE, 'w') { |fp| fp.write(JSON.generate(@fonts)) }
|
53
|
+
@fonts
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def available?(font)
|
59
|
+
# Figlet's font loader prints to STDOUT if the font file is not found.
|
60
|
+
# Temporarily redirect STDOUT to /dev/null while the font file is loaded
|
61
|
+
# then remove the redirect in an ensure block. Also, if a font file is
|
62
|
+
# not available, a SystemExit is raise :/
|
63
|
+
original_stdout = $stdout.clone
|
64
|
+
$stdout.reopen(File.new('/dev/null', 'w'))
|
65
|
+
RubyFiglet::Figlet.new('Text', font)
|
66
|
+
true
|
67
|
+
rescue SystemExit, NoMethodError
|
68
|
+
false
|
69
|
+
ensure
|
70
|
+
$stdout.reopen(original_stdout)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/whirled_peas/version.rb
CHANGED
data/whirled_peas.gemspec
CHANGED
@@ -18,11 +18,13 @@ Gem::Specification.new do |spec|
|
|
18
18
|
# Specify which files should be added to the gem when it is released.
|
19
19
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
20
|
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
21
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|sandbox)/}) }
|
22
22
|
end
|
23
|
+
spec.bindir = 'exe'
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
25
|
spec.require_paths = ['lib']
|
24
26
|
|
25
27
|
spec.add_runtime_dependency 'highline', '~> 2.0'
|
26
28
|
spec.add_runtime_dependency 'json', '~> 2.5'
|
27
|
-
spec.add_runtime_dependency '
|
29
|
+
spec.add_runtime_dependency 'ruby_figlet', '~> 0.6'
|
28
30
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: whirled_peas
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tom Collier
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-01-
|
11
|
+
date: 2021-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: highline
|
@@ -39,23 +39,24 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '2.5'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: ruby_figlet
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0.
|
47
|
+
version: '0.6'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '0.
|
54
|
+
version: '0.6'
|
55
55
|
description:
|
56
56
|
email:
|
57
57
|
- tcollier@gmail.com
|
58
|
-
executables:
|
58
|
+
executables:
|
59
|
+
- whirled_peas
|
59
60
|
extensions: []
|
60
61
|
extra_rdoc_files: []
|
61
62
|
files:
|
@@ -70,26 +71,29 @@ files:
|
|
70
71
|
- Rakefile
|
71
72
|
- bin/console
|
72
73
|
- bin/setup
|
74
|
+
- bin/title_fonts
|
75
|
+
- exe/whirled_peas
|
73
76
|
- lib/whirled_peas.rb
|
77
|
+
- lib/whirled_peas/command_line.rb
|
78
|
+
- lib/whirled_peas/config.rb
|
79
|
+
- lib/whirled_peas/errors.rb
|
74
80
|
- lib/whirled_peas/frame.rb
|
75
|
-
- lib/whirled_peas/frame/
|
76
|
-
- lib/whirled_peas/frame/
|
81
|
+
- lib/whirled_peas/frame/event_loop.rb
|
82
|
+
- lib/whirled_peas/frame/print_consumer.rb
|
77
83
|
- lib/whirled_peas/frame/producer.rb
|
78
84
|
- lib/whirled_peas/null_logger.rb
|
85
|
+
- lib/whirled_peas/template.rb
|
86
|
+
- lib/whirled_peas/template/element.rb
|
87
|
+
- lib/whirled_peas/template/settings.rb
|
79
88
|
- lib/whirled_peas/ui.rb
|
80
|
-
- lib/whirled_peas/ui/ansi.rb
|
81
89
|
- lib/whirled_peas/ui/canvas.rb
|
82
|
-
- lib/whirled_peas/ui/element.rb
|
83
90
|
- lib/whirled_peas/ui/painter.rb
|
84
91
|
- lib/whirled_peas/ui/screen.rb
|
85
|
-
- lib/whirled_peas/
|
86
|
-
- lib/whirled_peas/
|
92
|
+
- lib/whirled_peas/utils.rb
|
93
|
+
- lib/whirled_peas/utils/ansi.rb
|
94
|
+
- lib/whirled_peas/utils/color.rb
|
95
|
+
- lib/whirled_peas/utils/title_font.rb
|
87
96
|
- lib/whirled_peas/version.rb
|
88
|
-
- sandbox/auto.rb
|
89
|
-
- sandbox/box.rb
|
90
|
-
- sandbox/grid.rb
|
91
|
-
- sandbox/sandbox.rb
|
92
|
-
- sandbox/text.rb
|
93
97
|
- whirled_peas.gemspec
|
94
98
|
homepage: https://github.com/tcollier/whirled_peas
|
95
99
|
licenses:
|
@@ -1,61 +0,0 @@
|
|
1
|
-
require 'socket'
|
2
|
-
require 'json'
|
3
|
-
|
4
|
-
require_relative 'loop'
|
5
|
-
|
6
|
-
module WhirledPeas
|
7
|
-
module Frame
|
8
|
-
class Consumer
|
9
|
-
def initialize(template_factory, refresh_rate, logger=NullLogger.new)
|
10
|
-
@loop = Loop.new(template_factory, refresh_rate, logger)
|
11
|
-
@logger = logger
|
12
|
-
@running = false
|
13
|
-
@mutex = Mutex.new
|
14
|
-
end
|
15
|
-
|
16
|
-
def start(host:, port:)
|
17
|
-
mutex.synchronize { @running = true }
|
18
|
-
loop_thread = Thread.new { loop.start }
|
19
|
-
socket = TCPSocket.new(host, port)
|
20
|
-
logger.info('CONSUMER') { "Connected to #{host}:#{port}" }
|
21
|
-
while @running
|
22
|
-
line = socket.gets
|
23
|
-
if line.nil?
|
24
|
-
sleep(0.001)
|
25
|
-
next
|
26
|
-
end
|
27
|
-
args = JSON.parse(line)
|
28
|
-
name = args.delete('name')
|
29
|
-
if [Frame::EOF, Frame::TERMINATE].include?(name)
|
30
|
-
logger.info('CONSUMER') { "Received #{name} event, stopping..." }
|
31
|
-
loop.stop if name == Frame::TERMINATE
|
32
|
-
@running = false
|
33
|
-
else
|
34
|
-
duration = args.delete('duration')
|
35
|
-
loop.enqueue(name, duration, args)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
logger.info('CONSUMER') { "Exited normally" }
|
39
|
-
rescue => e
|
40
|
-
logger.warn('CONSUMER') { "Exited with error" }
|
41
|
-
logger.error('CONSUMER') { e.message }
|
42
|
-
logger.error('CONSUMER') { e.backtrace.join("\n") }
|
43
|
-
loop.stop
|
44
|
-
ensure
|
45
|
-
logger.info('CONSUMER') { "Waiting for loop thread to exit" }
|
46
|
-
loop_thread.join
|
47
|
-
logger.info('CONSUMER') { "Closing socket" }
|
48
|
-
socket.close if socket
|
49
|
-
end
|
50
|
-
|
51
|
-
def stop
|
52
|
-
logger.info('CONSUMER') { "Stopping..." }
|
53
|
-
mutex.synchronize { @running = false }
|
54
|
-
end
|
55
|
-
|
56
|
-
private
|
57
|
-
|
58
|
-
attr_reader :loop, :logger, :mutex
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
@@ -1,56 +0,0 @@
|
|
1
|
-
module WhirledPeas
|
2
|
-
module Frame
|
3
|
-
class Loop
|
4
|
-
def initialize(template_factory, refresh_rate, logger=NullLogger.new)
|
5
|
-
@template_factory = template_factory
|
6
|
-
@queue = Queue.new
|
7
|
-
@refresh_rate = refresh_rate
|
8
|
-
@logger = logger
|
9
|
-
end
|
10
|
-
|
11
|
-
def enqueue(name, duration, args)
|
12
|
-
queue.push([name, duration, args])
|
13
|
-
end
|
14
|
-
|
15
|
-
def start
|
16
|
-
logger.info('EVENT LOOP') { "Starting" }
|
17
|
-
@running = true
|
18
|
-
screen = UI::Screen.new
|
19
|
-
sleep(0.01) while queue.empty? # Wait for the first event
|
20
|
-
remaining_frames = 1
|
21
|
-
template = nil
|
22
|
-
while @running && remaining_frames > 0
|
23
|
-
frame_at = Time.now
|
24
|
-
next_frame_at = frame_at + 1.0 / refresh_rate
|
25
|
-
remaining_frames -= 1
|
26
|
-
if remaining_frames > 0
|
27
|
-
screen.refresh if screen.needs_refresh?
|
28
|
-
elsif !queue.empty?
|
29
|
-
name, duration, args = queue.pop
|
30
|
-
remaining_frames = duration ? duration * refresh_rate : 1
|
31
|
-
template = template_factory.build(name, args)
|
32
|
-
screen.paint(template)
|
33
|
-
end
|
34
|
-
sleep(next_frame_at - Time.now)
|
35
|
-
end
|
36
|
-
logger.info('EVENT LOOP') { "Exiting normally" }
|
37
|
-
rescue => e
|
38
|
-
logger.warn('EVENT LOOP') { "Exiting with error" }
|
39
|
-
logger.error('EVENT LOOP') { e.message }
|
40
|
-
logger.error('EVENT LOOP') { e.backtrace.join("\n") }
|
41
|
-
ensure
|
42
|
-
screen.finalize if screen
|
43
|
-
end
|
44
|
-
|
45
|
-
def stop
|
46
|
-
logger.info('EVENT LOOP') { "Stopping..." }
|
47
|
-
@running = false
|
48
|
-
end
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
attr_reader :template_factory, :queue, :refresh_rate, :logger
|
53
|
-
end
|
54
|
-
private_constant :Loop
|
55
|
-
end
|
56
|
-
end
|
@@ -1,199 +0,0 @@
|
|
1
|
-
require_relative 'settings'
|
2
|
-
|
3
|
-
module WhirledPeas
|
4
|
-
module UI
|
5
|
-
class Element
|
6
|
-
attr_accessor :preferred_width, :preferred_height
|
7
|
-
attr_reader :settings
|
8
|
-
|
9
|
-
def initialize(settings)
|
10
|
-
@settings = settings
|
11
|
-
end
|
12
|
-
end
|
13
|
-
private_constant :Element
|
14
|
-
|
15
|
-
class TextElement < Element
|
16
|
-
attr_reader :value
|
17
|
-
|
18
|
-
def initialize(settings)
|
19
|
-
super(TextSettings.merge(settings))
|
20
|
-
end
|
21
|
-
|
22
|
-
def value=(val)
|
23
|
-
@value = val
|
24
|
-
@preferred_width = settings.width || value.length
|
25
|
-
@preferred_height = 1
|
26
|
-
end
|
27
|
-
|
28
|
-
def inspect(indent='')
|
29
|
-
dims = unless preferred_width.nil?
|
30
|
-
"#{indent + ' '}Dimensions: #{preferred_width}x#{preferred_height}"
|
31
|
-
end
|
32
|
-
[
|
33
|
-
"#{indent}#{self.class.name}",
|
34
|
-
dims,
|
35
|
-
"#{indent + ' '}Settings",
|
36
|
-
settings.inspect(indent + ' ')
|
37
|
-
].compact.join("\n")
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
class ComposableElement < Element
|
42
|
-
def initialize(settings)
|
43
|
-
super
|
44
|
-
end
|
45
|
-
|
46
|
-
def children
|
47
|
-
@children ||= []
|
48
|
-
end
|
49
|
-
|
50
|
-
def add_text(&block)
|
51
|
-
element = TextElement.new(settings)
|
52
|
-
element.value = yield nil, element.settings
|
53
|
-
children << element
|
54
|
-
end
|
55
|
-
|
56
|
-
def add_box(&block)
|
57
|
-
element = BoxElement.new(settings)
|
58
|
-
value = yield element, element.settings
|
59
|
-
children << element
|
60
|
-
if element.children.empty? && value.is_a?(String)
|
61
|
-
element.add_text { value }
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def add_grid(&block)
|
66
|
-
element = GridElement.new(settings)
|
67
|
-
values = yield element, element.settings
|
68
|
-
children << element
|
69
|
-
if element.children.empty? && values.is_a?(Array)
|
70
|
-
values.each { |v| element.add_text { v } }
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def inspect(indent='')
|
75
|
-
kids = children.map { |c| c.inspect(indent + ' ') }.join("\n")
|
76
|
-
dims = unless preferred_width.nil?
|
77
|
-
"#{indent + ' '}Dimensions: #{preferred_width}x#{preferred_height}"
|
78
|
-
end
|
79
|
-
[
|
80
|
-
"#{indent}#{self.class.name}",
|
81
|
-
dims,
|
82
|
-
"#{indent + ' '}Settings",
|
83
|
-
settings.inspect(indent + ' '),
|
84
|
-
"#{indent + ' '}Children",
|
85
|
-
kids
|
86
|
-
].compact.join("\n")
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
class Template < ComposableElement
|
91
|
-
def initialize(settings=TemplateSettings.new)
|
92
|
-
super(settings)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
class BoxElement < ComposableElement
|
97
|
-
attr_writer :content_width, :content_height
|
98
|
-
|
99
|
-
def initialize(settings)
|
100
|
-
super(BoxSettings.merge(settings))
|
101
|
-
end
|
102
|
-
|
103
|
-
def self.from_template(template, width, height)
|
104
|
-
box = new(template.settings)
|
105
|
-
template.children.each { |c| box.children << c }
|
106
|
-
box.content_width = box.preferred_width = width
|
107
|
-
box.content_height = box.preferred_height = height
|
108
|
-
box
|
109
|
-
end
|
110
|
-
|
111
|
-
def content_width
|
112
|
-
@content_width ||= begin
|
113
|
-
child_widths = children.map(&:preferred_width)
|
114
|
-
width = settings.horizontal_flow? ? child_widths.sum : (child_widths.max || 0)
|
115
|
-
[width, *settings.width].max
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def preferred_width
|
120
|
-
@preferred_width ||= settings.margin.left +
|
121
|
-
(settings.border.left? ? 1 : 0) +
|
122
|
-
settings.padding.left +
|
123
|
-
content_width +
|
124
|
-
settings.padding.right +
|
125
|
-
(settings.border.right? ? 1 : 0) +
|
126
|
-
settings.margin.right
|
127
|
-
end
|
128
|
-
|
129
|
-
def content_height
|
130
|
-
@content_height ||= begin
|
131
|
-
child_heights = children.map(&:preferred_height)
|
132
|
-
settings.vertical_flow? ? child_heights.sum : (child_heights.max || 0)
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def preferred_height
|
137
|
-
@preferred_height ||= settings.margin.top +
|
138
|
-
(settings.border.top? ? 1 : 0) +
|
139
|
-
settings.padding.top +
|
140
|
-
content_height +
|
141
|
-
settings.padding.bottom +
|
142
|
-
(settings.border.bottom? ? 1 : 0) +
|
143
|
-
settings.margin.bottom
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
class GridElement < ComposableElement
|
148
|
-
def initialize(settings)
|
149
|
-
super(GridSettings.merge(settings))
|
150
|
-
end
|
151
|
-
|
152
|
-
def col_width
|
153
|
-
return @col_width if @col_width
|
154
|
-
@col_width = 0
|
155
|
-
children.each do |child|
|
156
|
-
@col_width = child.preferred_width if child.preferred_width > @col_width
|
157
|
-
end
|
158
|
-
@col_width
|
159
|
-
end
|
160
|
-
|
161
|
-
def row_height
|
162
|
-
return @row_height if @row_height
|
163
|
-
@row_height = 0
|
164
|
-
children.each do |child|
|
165
|
-
@row_height = child.preferred_height if child.preferred_height > @row_height
|
166
|
-
end
|
167
|
-
@row_height
|
168
|
-
end
|
169
|
-
|
170
|
-
|
171
|
-
def preferred_width
|
172
|
-
settings.margin.left +
|
173
|
-
(settings.border.left? ? 1 : 0) +
|
174
|
-
settings.num_cols * (
|
175
|
-
settings.padding.left +
|
176
|
-
col_width +
|
177
|
-
settings.padding.right
|
178
|
-
) +
|
179
|
-
(settings.num_cols - 1) * (settings.border.inner_vert? ? 1 : 0) +
|
180
|
-
(settings.border.right? ? 1 : 0) +
|
181
|
-
settings.margin.right
|
182
|
-
end
|
183
|
-
|
184
|
-
def preferred_height
|
185
|
-
num_rows = (children.length / settings.num_cols).ceil
|
186
|
-
settings.margin.top +
|
187
|
-
(settings.border.top? ? 1 : 0) +
|
188
|
-
num_rows * (
|
189
|
-
settings.padding.top +
|
190
|
-
row_height +
|
191
|
-
settings.padding.bottom
|
192
|
-
) +
|
193
|
-
(num_rows - 1) * (settings.border.inner_horiz? ? 1 : 0) +
|
194
|
-
(settings.border.bottom? ? 1 : 0) +
|
195
|
-
settings.margin.bottom
|
196
|
-
end
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|