layouter 0.1.0
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/mnt/stuff/projects/layouter/lib/layouter.rb +67 -0
- data/mnt/stuff/projects/layouter/lib/layouter/element.rb +30 -0
- data/mnt/stuff/projects/layouter/lib/layouter/errors.rb +25 -0
- data/mnt/stuff/projects/layouter/lib/layouter/leaf/annotation.rb +49 -0
- data/mnt/stuff/projects/layouter/lib/layouter/leaf/base.rb +30 -0
- data/mnt/stuff/projects/layouter/lib/layouter/leaf/custom.rb +41 -0
- data/mnt/stuff/projects/layouter/lib/layouter/leaf/spacer.rb +24 -0
- data/mnt/stuff/projects/layouter/lib/layouter/parent.rb +111 -0
- data/mnt/stuff/projects/layouter/lib/layouter/version.rb +3 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1fcb645c443a118766c615a471ccfda1577ca686391ebab9085a52bb82fa8401
|
4
|
+
data.tar.gz: 83514b4eef2fd89f7685ea9fdc61b16203d0c0c3b648f568132bd0116a545ac7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d0f23593d4f7ab1eec56ea70cf048f82ebb84a0157400718acd210fdd1bcdefce0902d439b97b7f01dae3b3a1327d1ca8f11f93dec1342022bbacda6bf485f25
|
7
|
+
data.tar.gz: cbb5abd09375ab153d1b0d6894f124049effa7136a11b0050bbaf2a265e47b82fc2ece1f57e11452829a7b3f382ee76d7e10962359c4feca2175e0b69db556dd
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative 'layouter/version'
|
2
|
+
require_relative 'layouter/errors'
|
3
|
+
require_relative 'layouter/element'
|
4
|
+
require_relative 'layouter/parent'
|
5
|
+
require_relative 'layouter/leaf/base'
|
6
|
+
require_relative 'layouter/leaf/spacer'
|
7
|
+
require_relative 'layouter/leaf/annotation'
|
8
|
+
require_relative 'layouter/leaf/custom'
|
9
|
+
|
10
|
+
module Layouter
|
11
|
+
class << self
|
12
|
+
def rows(*children)
|
13
|
+
Parent.new(:rows, children)
|
14
|
+
end
|
15
|
+
|
16
|
+
def cols(*children)
|
17
|
+
Parent.new(:cols, children)
|
18
|
+
end
|
19
|
+
|
20
|
+
def spacer(*args)
|
21
|
+
Leaf::Spacer.new(*args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def annotation(*args)
|
25
|
+
Leaf::Annotation.new(*args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def literal(content)
|
29
|
+
annotation(content, trim: false)
|
30
|
+
end
|
31
|
+
|
32
|
+
def custom(*args, &block)
|
33
|
+
Leaf::Custom.new(*args, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def horizontal(char)
|
37
|
+
raise(ArgumentError.new("Must pass single character")) if char.length != 1
|
38
|
+
custom(height: 1) { |w, h| char * w }
|
39
|
+
end
|
40
|
+
|
41
|
+
def vertical(char)
|
42
|
+
raise(ArgumentError.new("Must pass single character")) if char.length != 1
|
43
|
+
custom(width: 1) { |w, h| ([char] * h).join("\n") }
|
44
|
+
end
|
45
|
+
|
46
|
+
def bordered(chars, *args, &block)
|
47
|
+
rows(
|
48
|
+
cols(
|
49
|
+
literal(chars[:tl]),
|
50
|
+
horizontal(chars[:t]),
|
51
|
+
literal(chars[:tr]),
|
52
|
+
),
|
53
|
+
cols(
|
54
|
+
vertical(chars[:l]),
|
55
|
+
custom(*args, &block),
|
56
|
+
vertical(chars[:r]),
|
57
|
+
),
|
58
|
+
cols(
|
59
|
+
literal(chars[:bl]),
|
60
|
+
horizontal(chars[:b]),
|
61
|
+
literal(chars[:br]),
|
62
|
+
)
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Layouter
|
2
|
+
class Element
|
3
|
+
|
4
|
+
attr_reader :calculated_width, :calculated_height
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@calculated_width = @calculated_height = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
[
|
11
|
+
:min_width, :max_width, :min_height, :max_height,
|
12
|
+
:importance, :layout, :render
|
13
|
+
].each do |m|
|
14
|
+
define_method(m) do
|
15
|
+
raise(NotImplementedError, "Implemented by subclasses")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def layout?
|
20
|
+
!!@calculated_width && !!@calculated_height
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def layout!
|
26
|
+
raise(StateError.new("Must layout first")) unless layout?
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Layouter
|
2
|
+
|
3
|
+
# Root error, never raised.
|
4
|
+
class GenericError < StandardError; end
|
5
|
+
|
6
|
+
# Raised if the state of an object is inconsistent with the called method.
|
7
|
+
class StateError < GenericError; end
|
8
|
+
|
9
|
+
# Should never be raised, unless our code is buggy.
|
10
|
+
class AssertionError < GenericError; end
|
11
|
+
|
12
|
+
# Only raised while laying out, if it is not possible to layout.
|
13
|
+
class LayoutError < GenericError
|
14
|
+
|
15
|
+
attr_reader :dimension, :reason
|
16
|
+
|
17
|
+
def initialize(dimension, reason)
|
18
|
+
@dimension, @reason = dimension, reason
|
19
|
+
msg = "#{dimension.to_s.capitalize} is #{reason.to_s.gsub("_", " ")}"
|
20
|
+
super(msg)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Layouter
|
2
|
+
module Leaf
|
3
|
+
class Annotation < Base
|
4
|
+
|
5
|
+
ALMOST = "~"
|
6
|
+
ELLIPSIS = "\u2026"
|
7
|
+
|
8
|
+
attr_accessor :min_height, :max_height
|
9
|
+
|
10
|
+
def initialize(content, importance: 1, trim: true)
|
11
|
+
super(importance: importance)
|
12
|
+
unless content.is_a?(Numeric) || content.is_a?(String)
|
13
|
+
raise(ArgumentError, "Must be a number or strings")
|
14
|
+
end
|
15
|
+
@content = content
|
16
|
+
@trim = trim
|
17
|
+
@min_width = @max_width = @content.to_s.length # TODO: make smarter.
|
18
|
+
@min_height = @max_height = 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def min_width
|
22
|
+
if @trim && @content.is_a?(String)
|
23
|
+
2
|
24
|
+
elsif @trim && @content.is_a?(Numeric)
|
25
|
+
dot = @content.to_s.index(".")
|
26
|
+
dot ? dot + 1 : @content.to_s.length
|
27
|
+
else
|
28
|
+
@content.to_s.length
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def max_width
|
33
|
+
@content.to_s.length
|
34
|
+
end
|
35
|
+
|
36
|
+
def render
|
37
|
+
layout!
|
38
|
+
if @calculated_width == @content.to_s.length
|
39
|
+
@content.to_s
|
40
|
+
elsif @content.is_a?(String)
|
41
|
+
@content[0...(@calculated_width - 1)] + ELLIPSIS
|
42
|
+
elsif @content.is_a?(Numeric)
|
43
|
+
ALMOST + @content.to_s[0...(@calculated_width - 1)]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Layouter
|
2
|
+
module Leaf
|
3
|
+
class Base < Element
|
4
|
+
|
5
|
+
INF = Float::INFINITY
|
6
|
+
EPS = Float::EPSILON
|
7
|
+
|
8
|
+
attr_reader :importance
|
9
|
+
|
10
|
+
def initialize(importance:)
|
11
|
+
super()
|
12
|
+
if !importance.is_a?(Numeric) || importance < 0
|
13
|
+
raise(ArgumentError, "Invalid importance")
|
14
|
+
end
|
15
|
+
@importance = importance == 0 ? EPS : importance
|
16
|
+
end
|
17
|
+
|
18
|
+
def layout(width, height)
|
19
|
+
# These layout errors could occur in the dimension not being
|
20
|
+
# distributed by the parent, or if the leaf is layed our directly.
|
21
|
+
raise(LayoutError.new(:width, :too_small)) if width < min_width
|
22
|
+
raise(LayoutError.new(:width, :too_big)) if width > max_width
|
23
|
+
raise(LayoutError.new(:height, :too_small)) if height < min_height
|
24
|
+
raise(LayoutError.new(:height, :too_big)) if height > max_height
|
25
|
+
@calculated_width, @calculated_height = width, height
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Layouter
|
2
|
+
module Leaf
|
3
|
+
class Custom < Base
|
4
|
+
|
5
|
+
attr_reader :min_width, :max_width, :min_height, :max_height
|
6
|
+
|
7
|
+
def initialize(width: (0..INF), height: (0..INF), importance: 1, &block)
|
8
|
+
super(importance: importance)
|
9
|
+
[:width, :height].each do |dim|
|
10
|
+
eval <<-CODE, binding, __FILE__ , __LINE__ + 1
|
11
|
+
if #{dim}.is_a?(Integer)
|
12
|
+
@min_#{dim} = @max_#{dim} = #{dim}
|
13
|
+
elsif #{dim}.is_a?(Range) && #{dim}.first.is_a?(Integer)
|
14
|
+
@min_#{dim} = #{dim}.first
|
15
|
+
@max_#{dim} = #{dim}.exclude_end? ? #{dim}.last - 1 : #{dim}.last
|
16
|
+
else
|
17
|
+
raise(ArgumentError.new("Invalid #{dim}"))
|
18
|
+
end
|
19
|
+
if @min_#{dim} > @max_#{dim}
|
20
|
+
raise(ArgumentError.new("Inconsistent minimum and maximum #{dim}"))
|
21
|
+
end
|
22
|
+
CODE
|
23
|
+
end
|
24
|
+
raise(ArgumentError.new("Must pass block")) unless block_given?
|
25
|
+
@block = block
|
26
|
+
end
|
27
|
+
|
28
|
+
def render
|
29
|
+
layout!
|
30
|
+
w, h = @calculated_width, @calculated_height
|
31
|
+
res = @block.call(w, h)
|
32
|
+
lines = res.split("\n")
|
33
|
+
if lines.length != h || !lines.all? { |l| l.length == w }
|
34
|
+
raise(ArgumentError.new("Custom render has incorrect dimensions"))
|
35
|
+
end
|
36
|
+
res
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Layouter
|
2
|
+
module Leaf
|
3
|
+
class Spacer < Base
|
4
|
+
|
5
|
+
attr_accessor :min_width, :max_width, :min_height, :max_height
|
6
|
+
|
7
|
+
def initialize(weight: 1)
|
8
|
+
unless weight.is_a?(Numeric)
|
9
|
+
raise(ArgumentError, "Weight must be a number")
|
10
|
+
end
|
11
|
+
raise(ArgumentError, "Weight must more than 1") if weight < 1
|
12
|
+
super(importance: EPS * weight)
|
13
|
+
@min_width = @min_height = 0
|
14
|
+
@max_width = @max_height = INF
|
15
|
+
end
|
16
|
+
|
17
|
+
def render
|
18
|
+
layout!
|
19
|
+
([" " * @calculated_width] * @calculated_height).join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Layouter
|
2
|
+
class Parent < Element
|
3
|
+
|
4
|
+
DIM = {
|
5
|
+
rows: :height,
|
6
|
+
cols: :width,
|
7
|
+
}.freeze
|
8
|
+
|
9
|
+
def initialize(orientation, children)
|
10
|
+
super()
|
11
|
+
unless DIM.keys.include?(orientation)
|
12
|
+
raise(ArgumentError.new("Invalid orientation"))
|
13
|
+
end
|
14
|
+
unless children.is_a?(Array) && children.all? { |c| c.is_a?(Element) }
|
15
|
+
raise(ArgumentError.new("Invalid children"))
|
16
|
+
end
|
17
|
+
@orientation = orientation
|
18
|
+
@children = children
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](index)
|
22
|
+
@children[index]
|
23
|
+
end
|
24
|
+
|
25
|
+
def layout(width, height)
|
26
|
+
dim = DIM[@orientation]
|
27
|
+
value = binding.local_variable_get(dim)
|
28
|
+
min, max = send(:"min_#{dim}"), send(:"max_#{dim}")
|
29
|
+
raise(AssertionError) if min > max
|
30
|
+
raise(LayoutError.new(dim, :too_small)) unless value >= min
|
31
|
+
raise(LayoutError.new(dim, :too_big)) unless value <= max
|
32
|
+
importances = @children.map(&:importance)
|
33
|
+
maxs = @children.map { |c| c.send(:"max_#{dim}") - c.send(:"min_#{dim}") }
|
34
|
+
extras = self.class.distribute(value - min, importances, maxs)
|
35
|
+
@children.zip(extras).each do |child, extra|
|
36
|
+
child.layout(
|
37
|
+
dim == :width ? child.min_width + extra : width,
|
38
|
+
dim == :height ? child.min_height + extra : height,
|
39
|
+
)
|
40
|
+
end
|
41
|
+
@calculated_width, @calculated_height = width, height
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
[:width, :height].each do |dim|
|
46
|
+
define_method(:"min_#{dim}") do
|
47
|
+
mins = @children.map(&:"min_#{dim}")
|
48
|
+
DIM[@orientation] == dim ? mins.sum : mins.max
|
49
|
+
end
|
50
|
+
define_method(:"max_#{dim}") do
|
51
|
+
maxs = @children.map(&:"max_#{dim}")
|
52
|
+
DIM[@orientation] == dim ? maxs.sum : maxs.min
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def importance
|
57
|
+
@children.map(&:importance).max
|
58
|
+
end
|
59
|
+
|
60
|
+
def render
|
61
|
+
layout!
|
62
|
+
if @orientation == :rows
|
63
|
+
@children.map(&:render).join("\n")
|
64
|
+
elsif @orientation == :cols
|
65
|
+
tmp = @children.map { |child| child.render.split("\n") }
|
66
|
+
tmp[0].zip(*tmp[1..-1]).map(&:join).join("\n")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.distribute(value, importances, maxs)
|
71
|
+
raise(AssertionError) unless value.is_a?(Integer) # Avoid endless loops.
|
72
|
+
raise(AssertionError) unless value <= maxs.sum # Avoid endless loops.
|
73
|
+
raise(AssertionError) unless importances.length == maxs.length
|
74
|
+
unless maxs.all? { |v| v.is_a?(Integer) || v == Float::INFINITY }
|
75
|
+
raise(AssertionError)
|
76
|
+
end
|
77
|
+
data = importances.zip(maxs).map.with_index do |(importance, max), i|
|
78
|
+
{ index: i, value: 0, importance: importance, max: max }
|
79
|
+
end
|
80
|
+
while true # Distribute based on the importance, adhering to the maxs.
|
81
|
+
extra = value - data.map { |h| h[:value] }.sum
|
82
|
+
break if extra <= Float::EPSILON * data.length
|
83
|
+
candidates = data.select { |h| h[:value] < h[:max] }
|
84
|
+
denominator = candidates.map { |h| h[:importance] }.sum.to_f
|
85
|
+
candidates.each do |h|
|
86
|
+
share = extra * h[:importance] / denominator
|
87
|
+
h[:value] = h[:value] + share > h[:max] ? h[:max] : h[:value] + share
|
88
|
+
end
|
89
|
+
end
|
90
|
+
data.each { |h| h[:value] = h[:value].round }
|
91
|
+
while true # Fill up any extra or missing value from turning to integers.
|
92
|
+
extra = value - data.map { |h| h[:value] }.sum
|
93
|
+
break if extra == 0
|
94
|
+
delta = extra > 0 ? 1 : -1
|
95
|
+
data = data.sort_by { |h| h[:importance] }
|
96
|
+
data = data.reverse if extra > 0
|
97
|
+
data.cycle do |h|
|
98
|
+
if h[:max] > h[:value] && h[:value] > 0
|
99
|
+
h[:value] += delta
|
100
|
+
extra -= delta
|
101
|
+
break if extra == 0
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
raise(AssertionError) unless data.map { |h| h[:value] }.sum == value
|
106
|
+
raise(AssertionError) unless data.all? { |h| h[:value] <= h[:max] }
|
107
|
+
data.sort_by { |h| h[:index] }.map { |h| h[:value] }
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: layouter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sinan Taifour
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2011-12-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '12.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '12.0'
|
27
|
+
description: A layout engine for terminals.
|
28
|
+
email: sinan@taifour.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- "/mnt/stuff/projects/layouter/lib/layouter.rb"
|
34
|
+
- "/mnt/stuff/projects/layouter/lib/layouter/element.rb"
|
35
|
+
- "/mnt/stuff/projects/layouter/lib/layouter/errors.rb"
|
36
|
+
- "/mnt/stuff/projects/layouter/lib/layouter/leaf/annotation.rb"
|
37
|
+
- "/mnt/stuff/projects/layouter/lib/layouter/leaf/base.rb"
|
38
|
+
- "/mnt/stuff/projects/layouter/lib/layouter/leaf/custom.rb"
|
39
|
+
- "/mnt/stuff/projects/layouter/lib/layouter/leaf/spacer.rb"
|
40
|
+
- "/mnt/stuff/projects/layouter/lib/layouter/parent.rb"
|
41
|
+
- "/mnt/stuff/projects/layouter/lib/layouter/version.rb"
|
42
|
+
homepage:
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 2.7.6
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: A layout engine for terminals.
|
66
|
+
test_files: []
|