aboutsummaryrefslogtreecommitdiff
path: root/src/layout.nim
blob: 2be061084e249d5e481de9ba8de313f166cdacb1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import std/[options, sequtils, sugar]
import formats/html
import print

# Notes on std/options:
# std/options provides an Option[T] type, that can be some(T) or none(T).
# The get(value: Option[T], otherwise: Option[T]) provides sugar for unwrapping with a default value.
# Note that Nim's UFCS means that typically calls to get() look like value.get(T()).
# (T() is an object constructor, and [] are both openarray accesses and generic brackets)

const
  window_width = 500
  window_height = 500
  hstep = 13
  vstep = 18

# const inline_elements = []

const block_elements = [
  "html", "body", "article", "section", "nav", "aside",
  "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "header",
  "footer", "address", "p", "hr", "pre", "blockquote",
  "ol", "ul", "menu", "li", "dl", "dt", "dd", "figure",
  "figcaption", "main", "div", "table", "form", "fieldset",
  "legend", "details", "summary"
]

# Will expand to cover flexbox and grid in the future.
# The Document layout is included because pages can
# have multiple Documents through the use of iframes.
type LayoutKind = enum
  Document, Block, Inline

# Improve error messages: suggest a ref object when failing from recursion
type Layout = ref object
  node: Node
  children: seq[Layout]
  parent: Option[Layout]
  previous: Option[Layout]
  x, y, width, height: float
  cursorX, cursorY: float
  case kind: LayoutKind:
    of Document:
      discard
    of Block:
      discard
    of Inline:
      cx, cy: float # cursor
      weight: int

# The layout tree is constructed in a two-part process:
# 1. We iterate through the node tree to create a simple layout tree.
# 2. We iterate through the new layout tree to create references to parent and previous nodes.
# Other functions will use these references to compute positioning coordinates.
# This greatly cuts down code complexity. It likely has a performance impact on large trees.
func construct_tree(node: Node): Layout =
  var children: seq[Layout] = @[]
  var kind: LayoutKind = Inline
  case node.kind:
    of Element:
      for child in node.children:
        let current = child.construct_tree()
        children.add(current)
        if kind == Inline and child.kind == Element and child.tag in block_elements:
          kind = Block
      if node.children.len == 0:
        kind = Block
    of Text:
      discard
  return Layout(node: node, children: children, kind: kind)

# This function does another pass through the layout tree to add references to parents and siblings.
func populate_tree(self: Layout, parent: Option[Layout] = none(Layout), previous: Option[Layout] = none(Layout)) =
  self.parent = parent
  self.previous = previous
  var prevchild: Option[Layout] = none(Layout)
  for child in self.children:
    child.populate_tree(parent=some(self), previous=prevchild)
    prevchild = some(child)

# Overload the `value.get(otherwise)` function for brevity.
func get(self: Option[Layout]): auto =
  self.get(Layout())

# This function does a third pass through the layout tree to calculate positions.
func calculate_position(self: Layout) =
  case self.kind:
  of Document:
    self.width = window_width - 2*hstep
    self.x = hstep
    self.y = vstep
  of Block:
    self.x = self.parent.get.x # every object starts at its parent's left edge...
    self.y = # if there is no previous sibiling, they start at their parent's top edge...
      if self.previous.isSome(): self.previous.get.y + self.previous.get.height
      else: self.parent.get.y
    self.width = self.parent.get.width # by default, objects are greedy and take up all the space they can get...
  of Inline:
    self.x = self.parent.get.x
    self.y =
      if self.previous.isSome(): self.previous.get.y + self.previous.get.height
      else: self.parent.get.y
    self.width = self.parent.get.width
    self.cx = self.x
    self.cy = self.y

  for child in self.children:
    calculate_position(child)

  case self.kind:
  of Document:
    self.height =
      if self.children.len != 0: self.children.map(x => x.height).foldl(a + b) + 2*vstep
      else: 2*vstep
  of Block:
    self.height = # height calculation must come after the recursive call
      if self.children.len != 0: self.children.map(x => x.height).foldl(a + b)
      else: 0
  of Inline:
    self.height = self.cy - self.y

# These implicitly-mutating parameters are a bit gross, but
# really seem like the best way to build this layout tree.
# (they're implicitly mutable because Layouts are ref objects)

# Assuming the first node of an HTML object is the <html> tag.
# Right now, HTML generation is Bad so this will change in the future
func layout(html: Html): Layout =
  var self = Layout(kind: Document, node: html[0], children: @[])
  for child in self.node.children:
    self.children.add(child.construct_tree())
  self.populate_tree()
  self.calculate_position()
  return self

when isMainModule:
  import formats/uri, protocols/http, std/strutils

  proc print_layout(layout: Layout, indentation=0) =
    if layout.node.kind == Element:
      stdout.write(layout.x, " ", layout.y, " ", layout.width, " ", layout.height, " ")
      stdout.write(" ".repeat(indentation))
      stdout.write(layout.node.tag)
      stdout.write(":")
      stdout.write(layout.kind)
      let parent: Layout = layout.parent.get(Layout(node: Node(kind: Text)))
      if parent.node.kind == Element:
        stdout.write(" ")
        stdout.write("parent:")
        stdout.write(parent.node.tag)
      let previous: Layout = layout.previous.get(Layout(node: Node(kind: Text)))
      if previous.node.kind == Element:
        stdout.write(" ")
        stdout.write("previous:")
        stdout.write(previous.node.tag)
      stdout.write("\n")
      for child in layout.children:
        child.printLayout(indentation + 2)

  let text = parseHTML(httpRequest(parseURL("https://example.org:443/index.html")).body)
  print_layout layout(text)
  for node in layout(text).children:
    print node.node.tag