Add css caching, adapt to LineLayout and Textlayout, add link support, add a resolve_url function, cache colors and space widths for fonts, fix bug crashing if font doesnt exist

This commit is contained in:
csd4ni3l
2025-07-22 18:32:27 +02:00
parent 385552bb08
commit 8b9b70c475
6 changed files with 200 additions and 71 deletions

View File

@@ -3,13 +3,28 @@ 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
from http_client.connection import HTTPClient, resolve_url
from http_client.html_parser import CSSParser, Text, Element, style, cascade_priority, replace_symbols, tree_to_list
from pyglet.font.base import Font as BaseFont
from functools import lru_cache
HSTEP = 13
VSTEP = 18
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(" ")[0]
class DrawText:
def __init__(self, x1, y1, text, font, color):
self.top = y1
@@ -26,6 +41,75 @@ class DrawRect:
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
size = int(float(self.node.style["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
@@ -33,8 +117,7 @@ class BlockLayout:
self.previous = previous
self.children = []
self.display_list = []
self.line = []
self.cursor_x = 0
self.x, self.y, self.width, self.height = None, None, None, None
@@ -46,17 +129,12 @@ class BlockLayout:
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]):
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"
@@ -80,26 +158,13 @@ class BlockLayout:
self.children.append(next)
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
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)]
self.height = sum([child.height for child in self.children])
def recurse(self, node):
if isinstance(node, Text):
@@ -109,10 +174,10 @@ class BlockLayout:
if emoji_pattern.fullmatch(word):
self.word(self.node, word, emoji=True)
else:
self.word(self.node, replace_symbols(word))
self.word(self.node, word)
else:
if node.tag == "br":
self.flush()
self.new_line()
for child in node.children:
self.recurse(child)
@@ -123,36 +188,28 @@ class BlockLayout:
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)
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.flush()
self.new_line()
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
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)
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 += w + get_space_width(font)
def new_line(self):
self.cursor_x = 0
self.line = []
self.cursor_y = baseline + 2 * 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)
class DocumentLayout:
def __init__(self, node):
@@ -169,7 +226,6 @@ class DocumentLayout:
self.y = VSTEP
child.layout()
self.height = child.height
self.display_list = child.display_list
def paint(self):
return []
@@ -196,6 +252,7 @@ class Renderer():
self.text_to_create = []
self.window.on_mouse_scroll = self.on_mouse_scroll
self.window.on_mouse_press = self.on_mouse_press
self.window.on_resize = self.on_resize
self.batch = pyglet.graphics.Batch()
@@ -222,11 +279,36 @@ class Renderer():
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 on_mouse_press(self, x: int, y: int, button: int, modifiers: int):
y -= self.scroll_y
objs = [
obj for obj in tree_to_list(self.document, [])
if obj.x <= x < obj.x + obj.width
and ((self.window.height * 0.925) - obj.y - obj.height) <= y < ((self.window.height * 0.925) - obj.y)
]
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(self.http_client.scheme, self.http_client.host, self.http_client.port, self.http_client.path, elt.attributes["href"])
self.http_client.get_request(url, self.http_client.request_headers)
elt = elt.parent
def add_text(self, x, y, text, font, color, multiline=False):
self.widgets.append(
pyglet.text.Label(