layouter 0.1.0

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