layouter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|