Add backgrounds to finish layout tutorial, add inline and remote CSS support with a default of browser.css which currently supports fonts and backgrounds

This commit is contained in:
csd4ni3l
2025-07-21 12:29:32 +02:00
parent a604a5dbd1
commit 09c1ce7f5a
8 changed files with 460 additions and 127 deletions

43
assets/css/browser.css Normal file
View File

@@ -0,0 +1,43 @@
a {
color: blue;
}
i {
font-style: italic;
}
b {
font-weight: bold;
}
small {
font-size: 90%;
}
big {
font-size: 110%;
}
pre {
color: black;
font-weight: bold;
background-color: gray;
}
h1 {
display: block;
font-size: 200%;
font-weight: bold;
}
h2 {
display: block;
font-size: 150%;
font-weight: bold;
}
h3 {
display: block;
font-size: 117%;
font-weight: bold;
}

View File

@@ -1,4 +1,6 @@
import socket, logging, ssl, threading, os import socket, logging, ssl, threading, os, ujson, time
from http_client.html_parser import HTML, CSSParser, Element, tree_to_list, get_inline_styles
class HTTPClient(): class HTTPClient():
def __init__(self): def __init__(self):
@@ -11,6 +13,8 @@ class HTTPClient():
self.response_headers = {} self.response_headers = {}
self.response_http_version = None self.response_http_version = None
self.response_status = None self.response_status = None
self.nodes = []
self.css_rules = []
self.content_response = "" self.content_response = ""
self.view_source = False self.view_source = False
self.redirect_count = 0 self.redirect_count = 0
@@ -20,7 +24,7 @@ class HTTPClient():
with open(url.split("file://", 1)[1], "r") as file: with open(url.split("file://", 1)[1], "r") as file:
self.content_response = file.read() self.content_response = file.read()
def get_request(self, url, request_headers): def get_request(self, url, request_headers, css=False):
if url.startswith("view-source:"): if url.startswith("view-source:"):
url = url.split("view-source:")[1] url = url.split("view-source:")[1]
self.view_source = True self.view_source = True
@@ -54,7 +58,7 @@ class HTTPClient():
cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.json" cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.json"
if os.path.exists(f"http_cache/{cache_filename}"): if os.path.exists(f"http_cache/{cache_filename}"):
self.needs_render = True threading.Thread(target=self.parse, daemon=True).start()
return return
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -75,9 +79,9 @@ class HTTPClient():
self.socket.send(request.encode()) self.socket.send(request.encode())
threading.Thread(target=self.receive_response, daemon=True).start() threading.Thread(target=self.receive_response, daemon=True, args=(css,)).start()
def receive_response(self): def receive_response(self, css=False):
buffer = b"" buffer = b""
headers_parsed = False headers_parsed = False
content_length = None content_length = None
@@ -123,6 +127,9 @@ class HTTPClient():
logging.error(f"Error receiving messages: {e}") logging.error(f"Error receiving messages: {e}")
break break
self.socket.close()
self.socket = None
if 300 <= int(self.response_status) < 400: if 300 <= int(self.response_status) < 400:
if self.redirect_count >= 4: if self.redirect_count >= 4:
return return
@@ -134,9 +141,9 @@ class HTTPClient():
self.get_request(f"{self.scheme}://{self.host}{location_header}", self.request_headers) self.get_request(f"{self.scheme}://{self.host}{location_header}", self.request_headers)
else: else:
self.redirect_count = 0 self.redirect_count = 0
self.socket.close()
if not css:
self.needs_render = True self.parse()
def _parse_headers(self, header_data): def _parse_headers(self, header_data):
lines = header_data.splitlines() lines = header_data.splitlines()
@@ -164,3 +171,61 @@ class HTTPClient():
except ValueError: except ValueError:
logging.error(f"Error parsing header line: {line}") logging.error(f"Error parsing header line: {line}")
self.response_headers = headers self.response_headers = headers
def parse(self):
self.css_rules = []
cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.json"
original_scheme = self.scheme
original_host = self.host
original_port = self.port
original_path = self.path
original_response = self.content_response
if cache_filename in os.listdir("http_cache"):
with open(f"http_cache/{cache_filename}", "r") as file:
self.nodes = HTML.from_json(ujson.load(file))
else:
self.nodes = HTML(self.content_response).parse()
with open(f"http_cache/{cache_filename}", "w") as file:
json_list = HTML.to_json(self.nodes)
file.write(ujson.dumps(json_list))
css_links = [
node.attributes["href"]
for node in tree_to_list(self.nodes, [])
if isinstance(node, Element)
and node.tag == "link"
and node.attributes.get("rel") == "stylesheet"
and "href" in node.attributes
]
for css_link in css_links:
self.content_response = ""
if "://" in css_link:
self.get_request(css_link, self.request_headers, True)
if not css_link.startswith("/"):
dir, _ = self.path.rsplit("/", 1)
css_link = dir + "/" + css_link
if css_link.startswith("//"):
self.get_request(self.scheme + ":" + css_link, self.request_headers, True)
else:
self.get_request(self.scheme + "://" + self.host + ":" + str(self.port) + css_link, self.request_headers, True)
while not self.content_response:
time.sleep(0.025)
self.css_rules.extend(CSSParser(self.content_response).parse())
self.css_rules.extend(get_inline_styles(self.nodes))
self.scheme = original_scheme
self.host = original_host
self.port = original_port
self.path = original_path
self.content_response = original_response
self.needs_render = True

View File

@@ -1,13 +1,5 @@
SELF_CLOSING_TAGS = [ from utils.constants import SELF_CLOSING_TAGS, HEAD_TAGS, INHERITED_PROPERTIES
"area", "base", "br", "col", "embed", "hr", "img", "input", import html.entities
"link", "meta", "param", "source", "track", "wbr",
]
HEAD_TAGS = [
"base", "basefont", "bgsound", "noscript",
"link", "meta", "title", "style", "script",
]
class Element: class Element:
def __init__(self, tag, attributes, parent): def __init__(self, tag, attributes, parent):
self.tag = tag self.tag = tag
@@ -81,7 +73,6 @@ class HTML():
return tag, attributes return tag, attributes
def add_tag(self, tag): def add_tag(self, tag):
tag, attributes = self.get_attributes(tag) tag, attributes = self.get_attributes(tag)
@@ -150,4 +141,186 @@ class HTML():
elif json_list[0] == "element": elif json_list[0] == "element":
element = Element(json_list[1], json_list[2], parent) element = Element(json_list[1], json_list[2], parent)
element.children = [HTML.from_json(child, element) for child in json_list[3]] element.children = [HTML.from_json(child, element) for child in json_list[3]]
return element return element
class TagSelector:
def __init__(self, tag):
self.tag = tag
self.priority = 1
def matches(self, node):
return isinstance(node, Element) and self.tag == node.tag
class DescendantSelector:
def __init__(self, ancestor, descendant):
self.ancestor = ancestor
self.descendant = descendant
self.priority = ancestor.priority + descendant.priority
def matches(self, node):
if not self.descendant.matches(node): return False
while node.parent:
if self.ancestor.matches(node.parent): return True
node = node.parent
return False
def cascade_priority(rule):
selector, body = rule
return selector.priority
def get_inline_styles(node):
all_rules = []
for node in node.children:
if isinstance(node, Element) and node.tag == "style":
all_rules.extend(CSSParser(node.children[0].text).parse()) # node's first children will just be a text element that contains the css
all_rules.extend(get_inline_styles(node))
return all_rules
class CSSParser:
def __init__(self, s):
self.s = s
self.i = 0
def whitespace(self):
while self.i < len(self.s) and self.s[self.i].isspace():
self.i += 1
def literal(self, literal):
if not (self.i < len(self.s) and self.s[self.i] == literal):
raise Exception("Parsing error")
self.i += 1
def word(self):
start = self.i
while self.i < len(self.s):
if self.s[self.i].isalnum() or self.s[self.i] in "#-.%":
self.i += 1
else:
break
if not (self.i > start):
raise Exception("Parsing error")
return self.s[start:self.i]
def pair(self):
prop = self.word()
self.whitespace()
self.literal(":")
self.whitespace()
val = self.word()
return prop.casefold(), val
def ignore_until(self, chars):
while self.i < len(self.s):
if self.s[self.i] in chars:
return self.s[self.i]
else:
self.i += 1
return None
def body(self):
pairs = {}
while self.i < len(self.s) and self.s[self.i] != "}":
try:
prop, val = self.pair()
pairs[prop] = val
self.whitespace()
self.literal(";")
self.whitespace()
except Exception:
why = self.ignore_until([";", "}"])
if why == ";":
self.literal(";")
self.whitespace()
else:
break
return pairs
def selector(self):
out = TagSelector(self.word().casefold())
self.whitespace()
while self.i < len(self.s) and self.s[self.i] != "{":
tag = self.word()
descendant = TagSelector(tag.casefold())
out = DescendantSelector(out, descendant)
self.whitespace()
return out
def parse(self):
rules = []
while self.i < len(self.s):
try:
self.whitespace()
selector = self.selector()
self.literal("{")
self.whitespace()
body = self.body()
self.literal("}")
rules.append((selector, body))
except Exception:
why = self.ignore_until(["}"])
if why == "}":
self.literal("}")
self.whitespace()
else:
break
return rules
def style(node, rules):
node.style = {}
for property, default_value in INHERITED_PROPERTIES.items():
if node.parent:
node.style[property] = node.parent.style[property]
else:
node.style[property] = default_value
for selector, body in rules:
if not selector.matches(node): continue
for property, value in body.items():
node.style[property] = value
if isinstance(node, Element) and "style" in node.attributes:
pairs = CSSParser(node.attributes["style"]).body()
for property, value in pairs.items():
node.style[property] = value
if node.style["font-size"].endswith("%"):
if node.parent:
parent_font_size = node.parent.style["font-size"]
else:
parent_font_size = INHERITED_PROPERTIES["font-size"]
node_pct = float(node.style["font-size"][:-1]) / 100
parent_px = float(parent_font_size[:-2])
node.style["font-size"] = str(node_pct * parent_px) + "px"
for child in node.children:
style(child, rules)
def tree_to_list(tree, list):
list.append(tree)
for child in tree.children:
tree_to_list(child, list)
return list
def replace_symbols(text):
for key, value in html.entities.html5.items():
text = text.replace(f"&{key};", value)
return text

View File

@@ -1,22 +1,31 @@
import arcade, arcade.gui, pyglet, os, ujson import arcade, pyglet
from utils.constants import token_pattern, emoji_pattern 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.connection import HTTPClient
from http_client.html_parser import HTML, Text, Element from http_client.html_parser import CSSParser, Text, Element, style, cascade_priority, replace_symbols
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"
]
HSTEP = 13 HSTEP = 13
VSTEP = 18 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: class BlockLayout:
def __init__(self, node, parent, previous): def __init__(self, node, parent, previous):
self.node = node self.node = node
@@ -27,12 +36,20 @@ class BlockLayout:
self.display_list = [] self.display_list = []
self.line = [] self.line = []
self.font_cache = {}
self.x, self.y, self.width, self.height = None, None, None, None self.x, self.y, self.width, self.height = None, None, None, None
def paint(self): def paint(self):
return self.display_list 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): def layout_mode(self):
if isinstance(self.node, Text): if isinstance(self.node, Text):
@@ -65,9 +82,6 @@ class BlockLayout:
else: else:
self.cursor_x = 0 self.cursor_x = 0
self.cursor_y = 0 self.cursor_y = 0
self.weight = "normal"
self.style = "roman"
self.size = 16
self.line = [] self.line = []
self.recurse(self.node) self.recurse(self.node)
@@ -77,89 +91,68 @@ class BlockLayout:
child.layout() child.layout()
if mode == "block": if mode == "block":
self.height = sum([ self.height = sum([child.height for child in self.children])
child.height for child in self.children])
else: else:
self.height = self.cursor_y self.height = self.cursor_y
def ensure_font(self, size, weight, style, emoji): def ensure_font(self, font_family, size, weight, style, emoji):
if not (size, weight, style, emoji) in self.font_cache: if not (font_family, size, weight, style, emoji) in font_cache:
self.font_cache[(size, weight, style, emoji)] = pyglet.font.load("Roboto", size, weight, style == "italic") if not emoji else pyglet.font.load("OpenMoji Color", size, weight, style == "italic") 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 self.font_cache[(size, weight, style, emoji)] return font_cache[(font_family, size, weight, style, emoji)]
def word(self, word: str, emoji=False): def recurse(self, node):
font = self.ensure_font(self.size, self.weight, self.style, emoji) 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] w = font.get_text_size(word + (" " if not emoji else " "))[0]
if self.cursor_x + w > self.width: if self.cursor_x + w > self.width:
self.flush() self.flush()
self.line.append((self.cursor_x, word, font)) self.line.append((self.cursor_x, word, font, color))
self.cursor_x += w + font.get_text_size(" ")[0] self.cursor_x += w + font.get_text_size(" ")[0]
def flush(self): def flush(self):
if not self.line: if not self.line:
return return
fonts_on_line = [font for x, word, font in self.line] fonts_on_line = [font for x, word, font, color in self.line]
max_ascent = max(font.ascent for font in fonts_on_line) max_ascent = max(font.ascent for font in fonts_on_line)
max_descent = min(font.descent for font in fonts_on_line) max_descent = min(font.descent for font in fonts_on_line)
baseline = self.cursor_y + 1.25 * max_ascent baseline = self.cursor_y + 2 * max_ascent
for rel_x, word, font in self.line: for rel_x, word, font, color in self.line:
x = self.x + rel_x x = self.x + rel_x
y = self.y + baseline - font.ascent y = self.y + baseline - font.ascent
self.display_list.append((x, y, word, font)) self.display_list.append((x, y, word, font, color))
self.cursor_x = 0 self.cursor_x = 0
self.line = [] self.line = []
self.cursor_y = baseline + 1.25 * max_descent self.cursor_y = baseline + 2 * max_descent
def recurse(self, tree):
if isinstance(tree, Text):
if "{" in tree.text or "}" in tree.text:
return
word_list = [match.group(0) for match in token_pattern.finditer(tree.text)]
for word in word_list:
if emoji_pattern.fullmatch(word):
self.word(word, emoji=True)
else:
self.word(word)
else:
self.open_tag(tree.tag)
for child in tree.children:
self.recurse(child)
self.close_tag(tree.tag)
def open_tag(self, tag):
if tag == "i":
self.style = "italic"
elif tag == "b":
self.weight = "bold"
elif tag == "small":
self.size -= 2
elif tag == "big":
self.size += 4
elif tag == "br":
self.flush()
def close_tag(self, tag):
if tag == "i":
self.style = "roman"
elif tag == "b":
self.weight = "normal"
elif tag == "small":
self.size += 2
elif tag == "big":
self.size -= 4
elif tag == "p":
self.flush()
self.cursor_y += VSTEP
class DocumentLayout: class DocumentLayout:
def __init__(self, node): def __init__(self, node):
@@ -191,7 +184,7 @@ class Renderer():
def __init__(self, http_client: HTTPClient, window: arcade.Window): def __init__(self, http_client: HTTPClient, window: arcade.Window):
self.content = '' self.content = ''
self.request_scheme = 'http' self.request_scheme = 'http'
self.window = window
self.http_client = http_client self.http_client = http_client
self.scroll_y = 0 self.scroll_y = 0
@@ -199,18 +192,17 @@ class Renderer():
self.allow_scroll = False self.allow_scroll = False
self.smallest_y = 0 self.smallest_y = 0
self.text_labels: list[pyglet.text.Label] = [] self.widgets: list[pyglet.text.Label] = []
self.text_to_create = [] self.text_to_create = []
self.window = window
self.window.on_mouse_scroll = self.on_mouse_scroll self.window.on_mouse_scroll = self.on_mouse_scroll
self.window.on_resize = self.on_resize self.window.on_resize = self.on_resize
self.batch = pyglet.graphics.Batch() self.batch = pyglet.graphics.Batch()
def hide_out_of_bounds_labels(self): def hide_out_of_bounds_labels(self):
for widget in self.text_labels: for widget in self.widgets:
invisible = (widget.y + widget.content_height) > self.window.height * 0.925 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 # Doing visible flag set manually since it takes a lot of time
if widget.visible: if widget.visible:
if invisible: if invisible:
@@ -220,22 +212,23 @@ class Renderer():
widget.visible = True widget.visible = True
def on_resize(self, width, height): def on_resize(self, width, height):
self.http_client.needs_render = True if self.http_client.css_rules:
self.http_client.needs_render = True
def on_mouse_scroll(self, x, y, scroll_x, scroll_y): def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
if not self.allow_scroll: if not self.allow_scroll:
return return
old_y = self.scroll_y 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) - 10)) # flip scroll direction 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.text_labels: for widget in self.widgets:
widget.y += (self.scroll_y - old_y) widget.y += (self.scroll_y - old_y)
self.hide_out_of_bounds_labels() self.hide_out_of_bounds_labels()
def add_text(self, x, y, text, font, multiline=False): def add_text(self, x, y, text, font, color, multiline=False):
self.text_labels.append( self.widgets.append(
pyglet.text.Label( pyglet.text.Label(
text=text, text=text,
font_name=font.name, font_name=font.name,
@@ -243,7 +236,7 @@ class Renderer():
weight=font.weight, weight=font.weight,
font_size=font.size, font_size=font.size,
multiline=multiline, multiline=multiline,
color=arcade.color.BLACK, color=color,
x=x, x=x,
y=(self.window.height * 0.925) - y, y=(self.window.height * 0.925) - y,
batch=self.batch batch=self.batch
@@ -253,6 +246,18 @@ class Renderer():
if (self.window.height * 0.925) - y < self.smallest_y: if (self.window.height * 0.925) - y < self.smallest_y:
self.smallest_y = 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): def update(self):
if not self.http_client.needs_render: if not self.http_client.needs_render:
return return
@@ -260,36 +265,27 @@ class Renderer():
self.http_client.needs_render = False self.http_client.needs_render = False
self.allow_scroll = True self.allow_scroll = True
for child in self.text_labels: for child in self.widgets:
child.delete() child.delete()
del child del child
self.text_labels.clear() self.widgets.clear()
self.smallest_y = 0 self.smallest_y = 0
if self.http_client.view_source or self.http_client.scheme == "file": 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) 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": elif self.http_client.scheme == "http" or self.http_client.scheme == "https":
if not os.path.exists("http_cache"): style(self.http_client.nodes, sorted(self.http_client.css_rules + CSSParser(open("assets/css/browser.css").read()).parse(), key=cascade_priority))
os.makedirs("http_cache")
cache_filename = f"{self.http_client.scheme}_{self.http_client.host}_{self.http_client.port}_{self.http_client.path.replace('/', '_')}.json" self.document = DocumentLayout(self.http_client.nodes)
if cache_filename in os.listdir("http_cache"):
with open(f"http_cache/{cache_filename}", "r") as file:
self.nodes = HTML.from_json(ujson.load(file))
else:
self.nodes = HTML(self.http_client.content_response).parse()
with open(f"http_cache/{cache_filename}", "w") as file:
json_list = HTML.to_json(self.nodes)
file.write(ujson.dumps(json_list))
self.document = DocumentLayout(self.nodes)
self.document.layout() self.document.layout()
self.display_list = [] self.cmds = []
paint_tree(self.document, self.display_list) paint_tree(self.document, self.cmds)
for x, y, text, font in self.display_list: for cmd in self.cmds:
self.add_text(x, y, text, font) 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() self.hide_out_of_bounds_labels()

5
run.py
View File

@@ -14,9 +14,12 @@ sys.excepthook = on_exception
pyglet.resource.path.append(os.getcwd()) pyglet.resource.path.append(os.getcwd())
pyglet.font.add_directory('./assets/fonts') pyglet.font.add_directory('./assets/fonts')
if not log_dir in os.listdir(): if not os.path.exists(log_dir):
os.makedirs(log_dir) os.makedirs(log_dir)
if not os.path.exists("http_cache"):
os.makedirs("http_cache")
while len(os.listdir(log_dir)) >= 5: while len(os.listdir(log_dir)) >= 5:
files = [(file, os.path.getctime(os.path.join(log_dir, file))) for file in os.listdir(log_dir)] files = [(file, os.path.getctime(os.path.join(log_dir, file))) for file in os.listdir(log_dir)]
oldest_file = sorted(files, key=lambda x: x[1])[0][0] oldest_file = sorted(files, key=lambda x: x[1])[0][0]

View File

@@ -32,6 +32,36 @@ menu_background_color = arcade.color.WHITE
log_dir = 'logs' log_dir = 'logs'
discord_presence_id = 1393164073566208051 discord_presence_id = 1393164073566208051
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"
]
SELF_CLOSING_TAGS = [
"area", "base", "br", "col", "embed", "hr", "img", "input",
"link", "meta", "param", "source", "track", "wbr",
]
HEAD_TAGS = [
"base", "basefont", "bgsound", "noscript",
"link", "meta", "title", "style", "script",
]
INHERITED_PROPERTIES = {
"font-family": "Arial",
"font-size": "16px",
"font-style": "normal",
"font-weight": "normal",
"color": "black",
"display": "inline",
"width": "auto",
"height": "auto"
}
button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK),
'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK)} 'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK)}
big_button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), big_button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26),

View File

@@ -1,4 +1,7 @@
import arcade.gui, arcade import arcade.gui, arcade
from http_client.html_parser import CSSParser
button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button.png")) button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button.png"))
button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button_hovered.png")) button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button_hovered.png"))
DEFAULT_STYLE_SHEET = CSSParser(open("assets/css/browser.css").read()).parse()

View File

@@ -63,3 +63,23 @@ class FakePyPresence():
... ...
def close(self, *args, **kwargs): def close(self, *args, **kwargs):
... ...
def hex_to_rgb(hex_color):
hex_color = hex_color.lstrip('#')
if len(hex_color) != 6:
return (127, 127, 127)
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def get_color_from_name(rgb_name):
rgb_name = rgb_name.upper()
if rgb_name.startswith("LIGHT"):
color_name = rgb_name.split("LIGHT")[1]
color_value = arcade.csscolor.__dict__.get(f"LIGHT_{color_name}")
if not color_value:
arcade.color.__dict__.get(f"LIGHT_{color_name}")
if color_value:
return color_value
color_value = arcade.csscolor.__dict__.get(rgb_name)
return color_value if color_value else arcade.color.__dict__.get(rgb_name, arcade.color.GRAY)