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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Layouter
2
+ VERSION = "0.1.0"
3
+ 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: []