Skip to content

Commit

Permalink
Add block-based layout
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinL10 committed Aug 25, 2023
1 parent fdb38d6 commit 3ecb27b
Show file tree
Hide file tree
Showing 8 changed files with 446 additions and 58 deletions.
27 changes: 18 additions & 9 deletions browser/graphics.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from browser.lexer import HTMLParser
from browser.parser import HTMLParser
from browser.request import request
from browser.constants import VSTEP, HEIGHT, WIDTH, SCROLL_STEP
from browser.layout import Layout
from browser.layout import DocumentLayout
import tkinter
import tkinter.font

Expand All @@ -25,7 +25,9 @@ def scrollup(self, e):
self.draw()

def scrolldown(self, e):
self.scroll += SCROLL_STEP
# Prevent scrolling past bottom of the page
max_y = max(self.document.height - HEIGHT, 0)
self.scroll = min(self.scroll + SCROLL_STEP, max_y)
self.draw()


Expand All @@ -34,18 +36,25 @@ def scrolldown(self, e):
# (i.e. how far the user is down the page)
def draw(self):
self.canvas.delete("all")
for x, y, c, font in self.display_list:
if y > self.scroll + HEIGHT:
for cmd in self.display_list:
if cmd.top > self.scroll + HEIGHT:
continue
if y + VSTEP < self.scroll:
if cmd.bottom + VSTEP < self.scroll:
continue
self.canvas.create_text(x, y - self.scroll, text=c, anchor='nw', font=font)

cmd.execute(self.scroll, self.canvas)


# Renders the contents of the url to the canvas
def load(self, url):
headers, body = request(url)
self.node = HTMLParser(body).parse()
self.display_list = Layout(self.node).display_list
self.nodes = HTMLParser(body).parse()
self.document = DocumentLayout(self.nodes)
self.document.layout()

# The display_list consists of commands like DrawText and DrawRect
self.display_list = []
self.document.paint(self.display_list)
self.draw()


Expand Down
229 changes: 199 additions & 30 deletions browser/layout.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from browser.constants import HSTEP, VSTEP, WIDTH
from browser.lexer import Text
from browser.parser import Text, Element
import tkinter.font

FONTS = {}
Expand All @@ -16,29 +16,178 @@ def get_font(size, weight, slant):
)
return FONTS[key]

# Represents the layout of the web page (including font, position, size, etc.)
class Layout:
def __init__(self, node):

# DrawText represents a display_list command to draw text to screen
class DrawText:
def __init__(self, x1, y1, text, font):
self.top = y1
self.left = x1
self.text = text
self.font = font
self.bottom = y1 + font.metrics("linespace")

# Draws text to the given canvas
def execute(self, scroll, canvas):
canvas.create_text(
self.left,
self.top - scroll,
text=self.text,
anchor="nw",
font=self.font,
)


# DrawRect represents a display_list command to draw rectangles to screen
class DrawRect:
def __init__(self, x1, y1, x2, y2, color):
self.top = y1
self.left = x1
self.bottom = y2
self.right = x2
self.color = color

# Draws rectangle to the given canvas
def execute(self, scroll, canvas: tkinter.Canvas):
canvas.create_rectangle(
self.left, self.top - scroll,
self.right, self.bottom - scroll,
width=0, # border width
fill=self.color,
)


# Represents the layout of a block element
class BlockLayout:
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",
]

def __init__(self, node: Element, parent: Element, previous: "BlockLayout"):
self.node = node
self.parent = parent
self.previous = previous
self.children = []
self.display_list = []
self.cursor_x = HSTEP
self.cursor_y = VSTEP
self.weight = "normal"
self.style = "roman"
self.size = 16

# A list of (x, word, font) tuples representing the current line
# The final display_list is computed by aligning the words along the
# bottom of the line
self.line = []

self.recurse(node)
def paint(self, display_list):
if isinstance(self.node, Element) and self.node.tag == "pre":
x2, y2 = self.x + self.width, self.y + self.height
display_list.append(DrawRect(self.x, self.y, x2, y2, "gray"))

for child in self.children:
child.paint(display_list)

for x, y, word, font in self.display_list:
display_list.append(DrawText(x, y, word, font))

"""
Returns the type of layout of the given HTML nodes
"""

@staticmethod
def layout_mode(node):
if isinstance(node, Text):
return "inline"
elif node.children:
if any(
[
isinstance(child, Element)
and child.tag in BlockLayout.BLOCK_ELEMENTS
for child in node.children
]
):
return "block"
else:
return "inline"
else:
return "block"

def layout(self):
# Compute x, y, and width from parent/previous sibling element
self.width = self.parent.width
self.x = self.parent.x

if self.previous:
self.y = self.previous.y + self.previous.height
else:
self.y = self.parent.y

mode = BlockLayout.layout_mode(self.node)
if mode == "block":
previous = None
# Create a BlockLayout for every child in the HTML tree of the current node
for child in self.node.children:
next = BlockLayout(child, self, previous)
self.children.append(next)
previous = next
else:
self.display_list = []
self.cursor_x = 0
self.cursor_y = 0
self.weight = "normal"
self.style = "roman"
self.size = 16

# A list of (x, word, font) tuples representing the current line
# The final display_list is computed by aligning the words along the
# bottom of the line
self.line = []

# Flushy any remaining layout elements
self.flush()
self.recurse(self.node)

'''
# Flushy any remaining layout elements
self.flush()

# Recursively layout each block child
for child in self.children:
child.layout()
self.display_list.extend(child.display_list)

if mode == "block":
self.height = sum([child.height for child in self.children])
else:
self.height = self.cursor_y

"""
Recursively layout the parsed HTML tree
'''
"""

def recurse(self, node):
if isinstance(node, Text):
self.add_text(node)
Expand All @@ -47,10 +196,11 @@ def recurse(self, node):
for child in node.children:
self.recurse(child)
self.close_tag(node.tag)
'''

"""
Updates the current weight/style/size based on the given open tag
'''
"""

def open_tag(self, tag):
if tag == "i":
self.style = "italic"
Expand All @@ -65,9 +215,10 @@ def open_tag(self, tag):
elif tag == "br":
self.flush()

'''
"""
Updates the current weight/style/size based on the given close tag
'''
"""

def close_tag(self, tag):
if tag == "i":
self.style = "roman"
Expand All @@ -87,10 +238,8 @@ def add_text(self, token):
font = get_font(self.size, self.weight, self.style)
for word in token.text.split():
w = font.measure(word)

if self.cursor_x + w >= WIDTH - HSTEP:
if self.cursor_x + w > self.width:
self.flush()

self.line.append((self.cursor_x, word, font))
self.cursor_x += w + font.measure(" ")

Expand All @@ -107,11 +256,31 @@ def flush(self):
max_descent = max([metric["descent"] for metric in metrics])
baseline = self.cursor_y + max_ascent

for x, word, font in self.line:
y = baseline - font.metrics("ascent")
for rel_x, word, font in self.line:
x = self.x + rel_x
y = self.y + baseline - font.metrics("ascent")
self.display_list.append((x, y, word, font))

self.cursor_y = baseline + 1.25 * max_descent
self.cursor_x = HSTEP
self.cursor_x = 0

self.line = []


class DocumentLayout:
def __init__(self, node):
self.node = node
self.parent = None
self.children = []

def paint(self, display_list):
self.children[0].paint(display_list)

def layout(self):
child = BlockLayout(self.node, self, None)
self.children.append(child)
self.x = HSTEP
self.y = VSTEP
self.width = WIDTH - 2 * HSTEP
child.layout()
self.height = child.height + 2 * VSTEP
5 changes: 4 additions & 1 deletion browser/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
if len(sys.argv) == 2:
browser.load(sys.argv[1])
else:
browser.load("https://example.org")
browser.load("file:///home/vever/cs/browser/browser/tests/index.html")
browser.load("https://browser.engineering/layout.html")

# browser.load("https://www.w3.org/Style/CSS/Test/CSS1/current/test5526c.htm")

tkinter.mainloop()
34 changes: 17 additions & 17 deletions browser/lexer.py → browser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,20 +153,20 @@ def print_tree(node, indent=0):
print_tree(child, indent + 2)


k = HTMLParser(
"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
Hi! this is a test
<b>bold text</b>
</body>
</html>"""
)

print_tree(k.parse())
# k = HTMLParser(
# """<!DOCTYPE html>
# <html lang="en">
# <head>
# <meta charset="UTF-8">
# <meta http-equiv="X-UA-Compatible" content="IE=edge">
# <meta name="viewport" content="width=device-width, initial-scale=1.0">
# <title>Document</title>
# </head>
# <body>
# Hi! this is a test
# <b>bold text</b>
# </body>
# </html>"""
# )

# print_tree(k.parse())
Loading

0 comments on commit 3ecb27b

Please sign in to comment.