import arcade, pyglet, platform from utils.constants import BLOCK_ELEMENTS, token_pattern, emoji_pattern, INHERITED_PROPERTIES 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 from pyglet.font.base import Font as BaseFont from functools import lru_cache HSTEP = 13 VSTEP = 18 SPACE_MULTIPLIER = 0.25 if not platform.system() == "Windows" else 0.33 font_cache = {} def ensure_font(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 if pyglet.font.have_font(font_family) else "Arial", 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)] @lru_cache def get_space_width(font: BaseFont): return font.get_text_size("a")[0] * SPACE_MULTIPLIER # i have to do this because space width is 0 on Windows (of course, why wouldnt Windows ruin everything?) 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 LineLayout: def __init__(self, node, parent, previous): self.node = node self.parent = parent self.previous = previous self.children = [] def paint(self): return [] 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() if not self.children: self.height = 0 return fonts_on_line = [word.font for word in self.children] max_ascent = max(font.ascent for font in fonts_on_line) baseline = self.y + 2 * max_ascent for word in self.children: word.y = baseline - word.font.ascent max_descent = min(font.descent for font in fonts_on_line) self.height = 2 * (max_ascent + max_descent) class TextLayout(): def __init__(self, node, word, emoji, parent, previous): self.node = node self.word = word self.children = [] self.parent = parent self.previous = previous self.emoji = emoji def paint(self): color = get_color_from_name(self.node.style["color"]) return [DrawText(self.x, self.y, self.word, self.font, color)] def layout(self): weight = self.node.style["font-weight"] style = self.node.style["font-style"] font_family = self.node.style["font-family"] style = "roman" if style == "normal" else style if not self.node.style["font-size"].endswith("em") and not self.node.style["font-size"].endswith("rem"): size = int(float(self.node.style["font-size"][:-2])) else: size = int(INHERITED_PROPERTIES["font-size"][:-2]) self.font = ensure_font(font_family, size, weight, style, self.emoji) self.width = self.font.get_text_size(self.word + (" " if not self.emoji else " "))[0] if self.previous: space = get_space_width(self.previous.font) self.x = self.previous.x + space + self.previous.width else: self.x = self.parent.x self.height = self.font.ascent + self.font.descent class BlockLayout: def __init__(self, node, parent, previous): self.node = node self.parent = parent self.previous = previous self.children = [] self.cursor_x = 0 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) 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.new_line() self.recurse(self.node) for child in self.children: child.layout() self.height = sum([child.height for child in self.children]) 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, word) else: if node.tag == "br": self.new_line() 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 if not node.style["font-size"].endswith("em") and not node.style["font-size"].endswith("rem"): size = int(float(node.style["font-size"][:-2])) else: size = int(INHERITED_PROPERTIES["font-size"][:-2]) font = 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.new_line() line = self.children[-1] previous_word = line.children[-1] if line.children else None text = TextLayout(node, word, emoji, line, previous_word) line.children.append(text) self.cursor_x += w + get_space_width(font) def new_line(self): self.cursor_x = 0 last_line = self.children[-1] if self.children else None new_line = LineLayout(self.node, self, last_line) self.children.append(new_line) 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 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): self.content = '' self.request_scheme = 'http' self.window: arcade.Window = window self.current_window_size = self.window.size self.http_client = http_client self.scroll_y = 0 self.scroll_y_speed = 50 self.allow_scroll = False self.smallest_y = 0 self.document = None self.widgets: list[pyglet.text.Label] = [] self.text_to_create = [] 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): self.current_window_size = self.window.size 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, width=self.window.width * 0.5 if multiline else None, 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=self.window.height * 0.05, text=self.http_client.content_response, font=pyglet.font.load("Roboto", 16), color=arcade.color.BLACK, multiline=True) elif self.http_client.scheme == "http" or self.http_client.scheme == "https": if self.http_client.nodes: 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()