commit cf799aa49bba7637f4f0a7bfece023a8559f6047
parent b6c26acd163aa5fdb4341bd8dd8dde6d4fc909eb
Author: Jake Bauer <jbauer@paritybit.ca>
Date: Tue, 28 Feb 2023 13:10:38 -0500
Finish Chapter 7
Also add some hacks to handle cases that would cause the browser
to crash.
Diffstat:
M | browser.py | | | 371 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------- |
1 file changed, 278 insertions(+), 93 deletions(-)
diff --git a/browser.py b/browser.py
@@ -9,6 +9,7 @@ FONTS = {}
WIDTH, HEIGHT = 800, 600
HSTEP, VSTEP = 13, 18
SCROLLSTEP = 100
+CHROME_PX = 100
BLOCK_ELEMENTS = [
"html", "body", "article", "section", "nav", "aside",
@@ -28,7 +29,6 @@ INHERITED_PROPERTIES = {
class Browser:
def __init__(self):
- self.scroll = 0
self.window = tkinter.Tk()
self.canvas = tkinter.Canvas(
self.window,
@@ -37,77 +37,92 @@ class Browser:
bg="white",
)
self.canvas.pack()
- self.window.bind("<Down>", self.scrolldown)
- self.window.bind("<Up>", self.scrollup)
- self.document = None
- with open("browser.css") as f:
- self.default_style_sheet = CSSParser(f.read()).parse()
-
-
- def scrolldown(self, e):
- max_y = self.document.height - HEIGHT
- self.scroll = min(self.scroll + SCROLLSTEP, max_y)
+ self.window.bind("<Down>", self.handle_down)
+ self.window.bind("<Up>", self.handle_up)
+ self.window.bind("<Button-1>", self.handle_click)
+ self.window.bind("<Key>", self.handle_key)
+ self.window.bind("<Return>", self.handle_enter)
+ self.tabs = []
+ self.active_tab = None
+ self.focus = None
+ self.address_bar = ""
+
+
+ def handle_down(self, e):
+ self.tabs[self.active_tab].scrolldown()
self.draw()
-
- def scrollup(self, e):
- self.scroll = max(self.scroll - SCROLLSTEP, 0)
+ def handle_up(self, e):
+ self.tabs[self.active_tab].scrollup()
self.draw()
-
- def load(self, url):
- time_to_first_draw = timer()
-
- start = timer()
- headers, body = request(url)
- end = timer()
- print('{0:.5f}'.format(end - start), "- Request to", url)
-
- start = timer()
- self.nodes = HTMLParser(body).parse()
- end = timer()
- print('{0:.5f}'.format(end - start), "- Lexed response body")
-
- start = timer()
- rules = self.default_style_sheet.copy()
- links = [node.attributes["href"]
- for node in tree_to_list(self.nodes, [])
- if isinstance(node, Element)
- and node.tag == "link"
- and "href" in node.attributes
- and node.attributes.get("rel") == "stylesheet"]
- for link in links:
- try:
- header, body = request(resolve_url(link, url))
- except:
- continue
- rules.extend(CSSParser(body).parse())
- style(self.nodes, sorted(rules, key=cascade_priority))
- end = timer()
- print('{0:.5f}'.format(end - start), "- Styled nodes")
-
- start = timer()
- self.document = DocumentLayout(self.nodes)
- self.document.layout()
- end = timer()
- print('{0:.5f}'.format(end - start), "- Computed page layout")
-
- start = timer()
- self.display_list = []
- self.document.paint(self.display_list)
+ def handle_click(self, e):
+ self.focus = None
+ if e.y < CHROME_PX:
+ if 40 <= e.x < 40 + 80 * len(self.tabs) and 0 <= e.y < 40:
+ self.active_tab = int((e.x - 40) / 80)
+ elif 10 <= e.x < 30 and 10 <= e.y < 30:
+ self.load("http://www.paritybit.ca/")
+ elif 10 <= e.x < 35 and 40 <= e.y < 90:
+ self.tabs[self.active_tab].go_back()
+ elif 50 <= e.x < WIDTH - 10 and 40 <= e.y < 90:
+ self.focus = "address bar"
+ self.address_bar = ""
+ else:
+ self.tabs[self.active_tab].click(e.x, e.y - CHROME_PX)
self.draw()
- end = timer()
- print('{0:.5f}'.format(end - start), "- Canvas drawn")
- print('{0:.5f}'.format(timer() - time_to_first_draw), "- Time to first draw")
+ def handle_key(self, e):
+ if len(e.char) == 0: return
+ if not (0x20 <= ord(e.char) < 0x7f): return
+ if self.focus == "address bar":
+ self.address_bar += e.char
+ self.draw()
+ def handle_enter(self, e):
+ if self.focus == "address bar":
+ self.tabs[self.active_tab].load(self.address_bar)
+ self.focus = None
+ self.draw()
def draw(self):
self.canvas.delete("all")
- for cmd in self.display_list:
- if cmd.top > self.scroll + HEIGHT: continue
- if cmd.bottom < self.scroll: continue
- cmd.execute(self.scroll, self.canvas)
+ self.tabs[self.active_tab].draw(self.canvas)
+ self.canvas.create_rectangle(0, 0, WIDTH, CHROME_PX, fill="white", outline="black")
+ tabfont = get_font(16, "normal", "roman")
+ for i, tab in enumerate(self.tabs):
+ name = "Tab {}".format(i)
+ x1, x2 = 40 + 80 * i, 120 + 80 * i
+ self.canvas.create_line(x1, 0, x1, 40, fill="black")
+ self.canvas.create_line(x2, 0, x2, 40, fill="black")
+ self.canvas.create_text(x1 + 10, 10, anchor="nw", text=name, font=tabfont, fill="black")
+ if i == self.active_tab:
+ self.canvas.create_line(0, 40, x1, 40, fill="black")
+ self.canvas.create_line(x2, 40, WIDTH, 40, fill="black")
+ # New Tab Button
+ buttonfont = get_font(16, "normal", "roman")
+ self.canvas.create_rectangle(10, 10, 30, 30, outline="black", width=1)
+ self.canvas.create_text(11, 0, anchor="nw", text="+", font=buttonfont, fill="black")
+ # URL Bar
+ self.canvas.create_rectangle(40, 50, WIDTH - 10, 90, outline="black", width=1)
+ if self.focus == "address bar":
+ self.canvas.create_text( 55, 55, anchor='nw', text=self.address_bar, font=buttonfont, fill="black")
+ w = buttonfont.measure(self.address_bar)
+ self.canvas.create_line(55 + w, 55, 55 + w, 85, fill="black")
+ else:
+ url = self.tabs[self.active_tab].url
+ self.canvas.create_text(55, 55, anchor='nw', text=url, font=buttonfont, fill="black")
+ # Back Button
+ self.canvas.create_rectangle(10, 50, 35, 90, outline="black", width=1)
+ self.canvas.create_polygon( 15, 70, 30, 55, 30, 85, fill='black')
+
+
+ def load(self, url):
+ new_tab = Tab()
+ new_tab.load(url)
+ self.active_tab = len(self.tabs)
+ self.tabs.append(new_tab)
+ self.draw()
class DocumentLayout:
@@ -144,6 +159,7 @@ class BlockLayout:
self.width = None
self.height = None
self.display_list = []
+ self.previous_word = None
def layout(self):
@@ -164,20 +180,13 @@ class BlockLayout:
previous = next
else:
self.cursor_x = 0
- self.cursor_y = 0
-
- self.line = []
+ self.new_line()
self.recurse(self.node)
- self.flush()
for child in self.children:
child.layout()
- if mode == "block":
- self.height = sum([
- child.height for child in self.children])
- else:
- self.height = self.cursor_y
+ self.height = sum([child.height for child in self.children])
def paint(self, display_list):
@@ -186,26 +195,17 @@ class BlockLayout:
x2, y2 = self.x + self.width, self.y + self.height
rect = DrawRect(self.x, self.y, x2, y2, bgcolor)
display_list.append(rect)
- for x, y, word, font, color in self.display_list:
- display_list.append(DrawText(x, y, word, font, color))
+
for child in self.children:
child.paint(display_list)
- def flush(self):
- if not self.line: return
- metrics = [font.metrics() for x, word, font, color in self.line]
- max_ascent = max([metric["ascent"] for metric in metrics])
- baseline = self.cursor_y + 1.25 * max_ascent
- for rel_x, word, font, color in self.line:
- x = self.x + rel_x
- y = self.y + baseline - font.metrics("ascent")
- self.display_list.append((x, y, word, font, color))
+ def new_line(self):
+ self.previous_word = None
self.cursor_x = 0
- self.line = []
- max_descent = max([metric["descent"] for metric in metrics])
- self.cursor_y = baseline + 1.25 * max_descent
-
+ last_line = self.children[-1] if self.children else None
+ new_line = LineLayout(self.node, self, last_line)
+ self.children.append(new_line)
def text(self, node):
color = node.style["color"]
@@ -219,8 +219,11 @@ class BlockLayout:
for word in node.text.split():
w = font.measure(word)
if self.cursor_x + w >= self.width:
- self.flush()
- self.line.append((self.cursor_x, word, font, color))
+ self.new_line()
+ line = self.children[-1]
+ text = TextLayout(node, word, line, self.previous_word)
+ line.children.append(text)
+ self.previous_word = text
self.cursor_x += w + font.measure(" ")
@@ -229,11 +232,81 @@ class BlockLayout:
self.text(node)
else:
if node.tag == "br":
- self.flush()
+ self.new_line()
for child in node.children:
self.recurse(child)
+class LineLayout:
+ def __init__(self, node, parent, previous):
+ self.node = node
+ self.parent = parent
+ self.previous = previous
+ self.children = []
+
+
+ def layout(self):
+ 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
+
+ for word in self.children:
+ word.layout()
+
+ try:
+ max_ascent = max([word.font.metrics("ascent") for word in self.children])
+ except:
+ max_ascent = 0
+ baseline = self.y + 1.25 * max_ascent
+ for word in self.children:
+ word.y = baseline - word.font.metrics("ascent")
+ try:
+ max_descent = max([word.font.metrics("descent") for word in self.children])
+ except:
+ max_descent = 0
+ self.height = 1.25 * (max_ascent + max_descent)
+
+
+ def paint(self, display_list):
+ for child in self.children:
+ child.paint(display_list)
+
+
+class TextLayout:
+ def __init__(self, node, word, parent, previous):
+ self.node = node
+ self.word = word
+ self.children = []
+ self.parent = parent
+ self.previous = previous
+
+
+ def layout(self):
+ weight = self.node.style["font-weight"]
+ style = self.node.style["font-style"]
+ if style == "normal": style = "roman"
+ size = int(float(self.node.style["font-size"][:-2]) * .75)
+ self.font = get_font(size, weight, style)
+ self.width = self.font.measure(self.word)
+
+ if self.previous:
+ space = self.previous.font.measure(" ")
+ self.x = self.previous.x + space + self.previous.width
+ else:
+ self.x = self.parent.x
+
+ self.height = self.font.metrics("linespace")
+
+
+ def paint(self, display_list):
+ color = self.node.style["color"]
+ display_list.append(DrawText(self.x, self.y, self.word, self.font, color))
+
+
class DrawText:
def __init__(self, x1, y1, text, font, color):
self.top = y1
@@ -518,14 +591,112 @@ class DescendantSelector:
return False
+class Tab:
+ def __init__(self):
+ self.document = None
+ self.url = None
+ self.scroll = 0
+ self.history = []
+ with open("browser.css") as f:
+ self.default_style_sheet = CSSParser(f.read()).parse()
+
+
+ def scrolldown(self):
+ max_y = self.document.height - (HEIGHT - CHROME_PX)
+ self.scroll = min(self.scroll + SCROLLSTEP, max_y)
+
+
+ def scrollup(self):
+ self.scroll = max(self.scroll - SCROLLSTEP, CHROME_PX)
+
+
+ def click(self, x, y):
+ y += self.scroll
+ objs = [obj for obj in tree_to_list(self.document, [])
+ if obj.x <= x < obj.x + obj.width
+ and obj.y <= y < obj.y + obj.height]
+ if not objs: return
+ elt = objs[-1].node
+ while elt:
+ if isinstance(elt, Text):
+ pass
+ elif elt.tag == "a" and "href" in elt.attributes:
+ url = resolve_url(elt.attributes["href"], self.url)
+ return self.load(url)
+ elt = elt.parent
+
+
+ def go_back(self):
+ if len(self.history) > 1:
+ self.history.pop()
+ back = self.history.pop()
+ self.load(back)
+
+
+
+ def load(self, url):
+ time_to_first_draw = timer()
+
+ start = timer()
+ self.url = url
+ self.history.append(url)
+ headers, body = request(url)
+ end = timer()
+ print('{0:.5f}'.format(end - start), "- Request to", url)
+
+ start = timer()
+ self.nodes = HTMLParser(body).parse()
+ end = timer()
+ print('{0:.5f}'.format(end - start), "- Lexed response body")
+
+ start = timer()
+ rules = self.default_style_sheet.copy()
+ links = [node.attributes["href"]
+ for node in tree_to_list(self.nodes, [])
+ if isinstance(node, Element)
+ and node.tag == "link"
+ and "href" in node.attributes
+ and node.attributes.get("rel") == "stylesheet"]
+ for link in links:
+ try:
+ header, body = request(resolve_url(link, url))
+ except:
+ continue
+ rules.extend(CSSParser(body).parse())
+ style(self.nodes, sorted(rules, key=cascade_priority))
+ end = timer()
+ print('{0:.5f}'.format(end - start), "- Styled nodes")
+
+ start = timer()
+ self.document = DocumentLayout(self.nodes)
+ self.document.layout()
+ end = timer()
+ print('{0:.5f}'.format(end - start), "- Computed page layout")
+
+ start = timer()
+ self.display_list = []
+ self.document.paint(self.display_list)
+ end = timer()
+ print('{0:.5f}'.format(end - start), "- Canvas drawn")
+
+ print('{0:.5f}'.format(timer() - time_to_first_draw), "- Time to first draw")
+
+
+ def draw(self, canvas):
+ for cmd in self.display_list:
+ if cmd.top > self.scroll + HEIGHT - CHROME_PX: continue
+ if cmd.bottom < self.scroll: continue
+ cmd.execute(self.scroll - CHROME_PX, canvas)
+
+
def request(url):
+ if not "://" in url: url = "http://" + url
scheme,url = url.split("://", 1)
assert scheme in ["http", "https"], \
"Unknown scheme {}".format(scheme)
url_components = url.split("/", 1)
host = url_components[0]
path = "/" + (url_components[1] if len(url_components) > 1 else "")
-
s = socket.socket(
family=socket.AF_INET,
type=socket.SOCK_STREAM,
@@ -542,6 +713,11 @@ def request(url):
host, port = host.split(":", 1)
port = int(port)
+ print("DEBUG:\n Scheme:", scheme,
+ "\n Port:", port,
+ "\n Host:", host,
+ "\n Path:", path)
+
s.connect((host, port))
s.send("GET {} HTTP/1.1\r\n".format(path).encode("utf8") +
@@ -551,8 +727,6 @@ def request(url):
response = s.makefile("r", encoding="utf8", newline="\r\n")
statusline = response.readline()
version, status, explanation = statusline.split(" ", 2)
- assert status == "200", "{}: {}".format(status, explanation)
-
headers = {}
while True:
line = response.readline()
@@ -563,6 +737,12 @@ def request(url):
assert "transfer-encoding" not in headers
assert "content-encoding" not in headers
+ if (status == "301" or status == "302") and "location" in headers:
+ s.close()
+ return request(headers["location"])
+ else:
+ assert status == "200", "{}: {}".format(status, explanation)
+
body = response.read()
s.close()
@@ -641,11 +821,16 @@ def tree_to_list(tree, list):
def resolve_url(url, current):
+ print("DEBUG:")
if "://" in url:
return url
elif url.startswith("/"):
scheme, hostpath = current.split("://", 1)
- host, oldpath = hostpath.split("/",)
+ print(" Scheme:", scheme)
+ print(" Hostpath:", hostpath)
+ host, oldpath = hostpath.split("/", 1)
+ print(" Host:", host)
+ print(" Oldpath:", oldpath)
return scheme + "://" + host + url
else:
dir, _ = current.rsplit("/", 1)