Files
browser/http_client/renderer.py

291 lines
9.5 KiB
Python

import arcade, pyglet
from utils.constants import BLOCK_ELEMENTS, token_pattern, emoji_pattern
from utils.utils import get_color_from_name, hex_to_rgb
from http_client.connection import HTTPClient
from http_client.html_parser import CSSParser, Text, Element, style, cascade_priority, replace_symbols
HSTEP = 13
VSTEP = 18
font_cache = {}
class DrawText:
def __init__(self, x1, y1, text, font, color):
self.top = y1
self.left = x1
self.text = text
self.font = font
self.color = color
class DrawRect:
def __init__(self, x1, y1, width, height, color):
self.top = y1
self.left = x1
self.width = width
self.height = height
self.color = color
class BlockLayout:
def __init__(self, node, parent, previous):
self.node = node
self.parent = parent
self.previous = previous
self.children = []
self.display_list = []
self.line = []
self.x, self.y, self.width, self.height = None, None, None, None
def paint(self):
cmds = []
if self.layout_mode() == "inline":
bgcolor = self.node.style.get("background-color", "transparent")
if bgcolor != "transparent":
rect = DrawRect(self.x, self.y, self.width, self.height, hex_to_rgb(bgcolor) if bgcolor.startswith("#") else get_color_from_name(bgcolor))
cmds.append(rect)
for x, y, word, font, color in self.display_list:
cmds.append(DrawText(x, y, word, font, color))
return cmds
def layout_mode(self):
if isinstance(self.node, Text):
return "inline"
elif any([isinstance(child, Element) and \
child.tag in BLOCK_ELEMENTS
for child in self.node.children]):
return "block"
elif self.node.children:
return "inline"
else:
return "block"
def layout(self):
self.x = self.parent.x
self.width = self.parent.width
if self.previous:
self.y = self.previous.y + self.previous.height
else:
self.y = self.parent.y
mode = self.layout_mode()
if mode == "block":
previous = None
for child in self.node.children:
next = BlockLayout(child, self, previous)
self.children.append(next)
previous = next
else:
self.cursor_x = 0
self.cursor_y = 0
self.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
def ensure_font(self, font_family, size, weight, style, emoji):
if not (font_family, size, weight, style, emoji) in font_cache:
font_cache[(font_family, size, weight, style, emoji)] = pyglet.font.load(font_family, size, weight, style == "italic") if not emoji else pyglet.font.load("OpenMoji Color", size, weight, style == "italic")
return font_cache[(font_family, size, weight, style, emoji)]
def recurse(self, node):
if isinstance(node, Text):
word_list = [match.group(0) for match in token_pattern.finditer(node.text)]
for word in word_list:
if emoji_pattern.fullmatch(word):
self.word(self.node, word, emoji=True)
else:
self.word(self.node, replace_symbols(word))
else:
if node.tag == "br":
self.flush()
for child in node.children:
self.recurse(child)
def word(self, node, word: str, emoji=False):
weight = node.style["font-weight"]
style = node.style["font-style"]
font_family = node.style["font-family"]
style = "roman" if style == "normal" else style
size = int(float(node.style["font-size"][:-2]))
color = get_color_from_name(node.style["color"])
font = self.ensure_font(font_family, size, weight, style, emoji)
w = font.get_text_size(word + (" " if not emoji else " "))[0]
if self.cursor_x + w > self.width:
self.flush()
self.line.append((self.cursor_x, word, font, color))
self.cursor_x += w + font.get_text_size(" ")[0]
def flush(self):
if not self.line:
return
fonts_on_line = [font for x, word, font, color in self.line]
max_ascent = max(font.ascent for font in fonts_on_line)
max_descent = min(font.descent for font in fonts_on_line)
baseline = self.cursor_y + 2 * max_ascent
for rel_x, word, font, color in self.line:
x = self.x + rel_x
y = self.y + baseline - font.ascent
self.display_list.append((x, y, word, font, color))
self.cursor_x = 0
self.line = []
self.cursor_y = baseline + 2 * max_descent
class DocumentLayout:
def __init__(self, node):
self.node = node
self.parent = None
self.children = []
def layout(self):
child = BlockLayout(self.node, self, None)
self.children.append(child)
self.width = arcade.get_window().width - 2 * HSTEP
self.x = HSTEP
self.y = VSTEP
child.layout()
self.height = child.height
self.display_list = child.display_list
def paint(self):
return []
def paint_tree(layout_object, display_list):
display_list.extend(layout_object.paint())
for child in layout_object.children:
paint_tree(child, display_list)
class Renderer():
def __init__(self, http_client: HTTPClient, window: arcade.Window):
self.content = ''
self.request_scheme = 'http'
self.window = window
self.http_client = http_client
self.scroll_y = 0
self.scroll_y_speed = 50
self.allow_scroll = False
self.smallest_y = 0
self.widgets: list[pyglet.text.Label] = []
self.text_to_create = []
self.window.on_mouse_scroll = self.on_mouse_scroll
self.window.on_resize = self.on_resize
self.batch = pyglet.graphics.Batch()
def hide_out_of_bounds_labels(self):
for widget in self.widgets:
invisible = (widget.y + (widget.content_height if not isinstance(widget, pyglet.shapes.Rectangle) else widget.height)) > self.window.height * 0.925
# Doing visible flag set manually since it takes a lot of time
if widget.visible:
if invisible:
widget.visible = False
else:
if not invisible:
widget.visible = True
def on_resize(self, width, height):
if self.http_client.css_rules:
self.http_client.needs_render = True
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
if not self.allow_scroll:
return
old_y = self.scroll_y
self.scroll_y = max(0, min(abs(self.scroll_y - (scroll_y * self.scroll_y_speed)), abs(self.smallest_y) - (self.window.height * 0.925) + 5)) # flip scroll direction
for widget in self.widgets:
widget.y += (self.scroll_y - old_y)
self.hide_out_of_bounds_labels()
def add_text(self, x, y, text, font, color, multiline=False):
self.widgets.append(
pyglet.text.Label(
text=text,
font_name=font.name,
italic=font.italic,
weight=font.weight,
font_size=font.size,
multiline=multiline,
color=color,
x=x,
y=(self.window.height * 0.925) - y,
batch=self.batch
)
)
if (self.window.height * 0.925) - y < self.smallest_y:
self.smallest_y = y
def add_background(self, left, top, width, height, color):
self.widgets.append(
pyglet.shapes.Rectangle(
left,
(self.window.height * 0.925) - top - height,
width,
height,
color,
batch=self.batch
)
)
def update(self):
if not self.http_client.needs_render:
return
self.http_client.needs_render = False
self.allow_scroll = True
for child in self.widgets:
child.delete()
del child
self.widgets.clear()
self.smallest_y = 0
if self.http_client.view_source or self.http_client.scheme == "file":
self.add_text(x=HSTEP, y=0, text=self.http_client.content_response, font=pyglet.font.load("Roboto", 16), multiline=True)
elif self.http_client.scheme == "http" or self.http_client.scheme == "https":
style(self.http_client.nodes, sorted(self.http_client.css_rules + CSSParser(open("assets/css/browser.css").read()).parse(), key=cascade_priority))
self.document = DocumentLayout(self.http_client.nodes)
self.document.layout()
self.cmds = []
paint_tree(self.document, self.cmds)
for cmd in self.cmds:
if isinstance(cmd, DrawText):
self.add_text(cmd.left, cmd.top, cmd.text, cmd.font, cmd.color)
elif isinstance(cmd, DrawRect):
self.add_background(cmd.left, cmd.top, cmd.width, cmd.height, cmd.color)
self.hide_out_of_bounds_labels()