437 lines
9.6 KiB
Plaintext
437 lines
9.6 KiB
Plaintext
module ugui;
|
|
|
|
import std::collections::map;
|
|
import std::core::mem::allocator;
|
|
import std::io;
|
|
|
|
// global style, similar to the css box model
|
|
struct Style { // css box model
|
|
Rect padding;
|
|
Rect border;
|
|
Rect margin;
|
|
|
|
Color bg; // background color
|
|
Color fg; // foreground color
|
|
Color primary; // primary color
|
|
Color secondary; // secondary color
|
|
Color accent; // accent color
|
|
|
|
ushort radius;
|
|
short size;
|
|
}
|
|
|
|
const Style DEFAULT_STYLE = {
|
|
.margin = {2, 2, 2, 2},
|
|
.border = {2, 2, 2, 2},
|
|
.padding = {1, 1, 1, 1},
|
|
.radius = 12,
|
|
.size = 16,
|
|
|
|
.bg = 0x282828ffu.@to_rgba(),
|
|
.fg = 0xfbf1c7ffu.@to_rgba(),
|
|
.primary = 0xcc241dffu.@to_rgba(),
|
|
.secondary = 0x458588ffu.@to_rgba(),
|
|
.accent = 0xfabd2fffu.@to_rgba(),
|
|
};
|
|
|
|
// style is stored in a hashmap, each style has an Id that can be generated by a string or whatever
|
|
alias StyleMap = map::HashMap{Id, Style};
|
|
|
|
// push or update a new style into the map
|
|
fn void StyleMap.register_style(&map, Style* style, Id id)
|
|
{
|
|
if (style == null) return;
|
|
map.set(id, *style);
|
|
}
|
|
|
|
// get a style from the map, if the style is not found then use a default style.
|
|
fn Style* StyleMap.get_style(&map, Id id)
|
|
{
|
|
Style*? s = map.get_ref(id);
|
|
if (catch s) {
|
|
// io::eprintfn("WARNING: style %x not found, using default style", id);
|
|
return &DEFAULT_STYLE;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
fn int StyleMap.import_style_string(&map, String text)
|
|
{
|
|
Parser p;
|
|
p.lex.text = text;
|
|
int added;
|
|
|
|
while (p.parse_style() == true) {
|
|
added++;
|
|
// set the default style correctly
|
|
map.register_style(&p.style, p.style_id);
|
|
if (p.lex.peep_token().type == EOF) break;
|
|
}
|
|
|
|
return added;
|
|
}
|
|
|
|
fn int Ctx.import_style_from_string(&ctx, String text) => ctx.styles.import_style_string(text);
|
|
fn int Ctx.import_style_from_file(&ctx, String path)
|
|
{
|
|
char[] text;
|
|
usz size = file::get_size(path)!!;
|
|
text = mem::new_array(char, size);
|
|
file::load_buffer(path, text)!!;
|
|
defer mem::free(text);
|
|
|
|
int added = ctx.import_style_from_string((String)text);
|
|
return added;
|
|
}
|
|
|
|
|
|
/*
|
|
* Style can be serialized and deserialized with a subset of CSS
|
|
* <style name> {
|
|
* padding: left right top bottom;
|
|
* border: left right top bottom;
|
|
* margin: left right top bottoms;
|
|
* radius: uint;
|
|
* size: uint;
|
|
* Color: #RRGGBBAA;
|
|
* Color: #RRGGBBAA;
|
|
* Color: #RRGGBBAA;
|
|
* Color: #RRGGBBAA;
|
|
* Color: #RRGGBBAA;
|
|
* }
|
|
* The field "style name" will be hashed and the hash used as the id int the style map.
|
|
* Fields may be excluded, each excluded field is set to zero.
|
|
* The default unit is pixels, but millimeters is also available. The parser function accepts a scale
|
|
* factor that has to be obtained with the window manager functions.
|
|
*/
|
|
|
|
module ugui::css;
|
|
|
|
import std::ascii;
|
|
import std::io;
|
|
|
|
// CSS parser module
|
|
|
|
enum TokenType {
|
|
INVALID,
|
|
IDENTIFIER,
|
|
RCURLY,
|
|
LCURLY,
|
|
SEMICOLON,
|
|
COLON,
|
|
NUMBER,
|
|
COLOR,
|
|
EOF,
|
|
}
|
|
|
|
enum Unit {
|
|
PIXELS,
|
|
MILLIMETERS
|
|
}
|
|
|
|
struct Token {
|
|
TokenType type;
|
|
usz line, col, off;
|
|
String text;
|
|
union {
|
|
struct {
|
|
float value;
|
|
Unit unit;
|
|
}
|
|
Color color;
|
|
}
|
|
}
|
|
|
|
fn short Token.to_px(&t, float mm_to_px)
|
|
{
|
|
if (t.type != NUMBER) {
|
|
unreachable("WFT you cannot convert to pixels a non-number");
|
|
}
|
|
if (t.unit == PIXELS) return (short)(t.value);
|
|
return (short)(t.value * mm_to_px);
|
|
}
|
|
|
|
struct Lexer {
|
|
String text;
|
|
usz line, col, off;
|
|
}
|
|
|
|
macro char Lexer.peep(&lex) => lex.text[lex.off];
|
|
|
|
fn char Lexer.advance(&lex)
|
|
{
|
|
if (lex.off >= lex.text.len) return '\0';
|
|
|
|
char c = lex.text[lex.off];
|
|
if (c == '\n') {
|
|
lex.col = 0;
|
|
lex.line++;
|
|
} else {
|
|
lex.col++;
|
|
}
|
|
lex.off++;
|
|
return c;
|
|
}
|
|
|
|
fn Token Lexer.next_token(&lex)
|
|
{
|
|
Token t;
|
|
t.type = INVALID;
|
|
t.off = lex.off;
|
|
t.col = lex.col;
|
|
t.line = lex.line;
|
|
|
|
if (lex.off >= lex.text.len) {
|
|
return {.type = EOF};
|
|
}
|
|
|
|
// skip whitespace
|
|
while (ascii::is_space_m(lex.peep())) {
|
|
if (lex.advance() == 0) return {.type = EOF};
|
|
if (lex.off >= lex.text.len) return {.type = EOF};
|
|
}
|
|
t.off = lex.off;
|
|
|
|
switch (true) {
|
|
case ascii::is_punct_m(lex.peep()) && lex.peep() != '#': // punctuation
|
|
t.text = lex.text[lex.off:1];
|
|
if (lex.advance() == 0) { t.type = INVALID; break; }
|
|
switch (t.text[0]) {
|
|
case ':': t.type = COLON;
|
|
case ';': t.type = SEMICOLON;
|
|
case '{': t.type = LCURLY;
|
|
case '}': t.type = RCURLY;
|
|
default: t.type = INVALID;
|
|
}
|
|
|
|
case lex.peep() == '#': // color
|
|
t.type = COLOR;
|
|
if (lex.advance() == 0) { t.type = INVALID; break; }
|
|
usz hex_start = t.off+1;
|
|
while (ascii::is_alnum_m(lex.peep())) {
|
|
if (lex.advance() == 0) { t.type = INVALID; break; }
|
|
}
|
|
if (lex.off - hex_start != 8) {
|
|
io::eprintfn("CSS lexing error at %d:%d: the only suppported color format is #RRGGBBAA", t.line, t.col);
|
|
t.type = INVALID;
|
|
break;
|
|
}
|
|
|
|
char[10] hex_str = (char[])"0x";
|
|
hex_str[2..] = lex.text[hex_start..lex.off-1];
|
|
uint? color_hex = ((String)hex_str[..]).to_uint();
|
|
if (catch color_hex) {
|
|
t.type = INVALID;
|
|
break;
|
|
}
|
|
t.color = color_hex.to_rgba();
|
|
|
|
case ascii::is_alpha_m(lex.peep()): // identifier
|
|
t.type = IDENTIFIER;
|
|
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '-' || lex.peep() == '_') {
|
|
if (lex.advance() == 0) { t.type = INVALID; break; }
|
|
}
|
|
t.text = lex.text[t.off..lex.off-1];
|
|
|
|
case ascii::is_digit_m(lex.peep()): // number
|
|
t.type = NUMBER;
|
|
t.unit = PIXELS;
|
|
// find the end of the number
|
|
usz end;
|
|
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '+' || lex.peep() == '-' || lex.peep() == '.') {
|
|
if (lex.advance() == 0) { t.type = INVALID; break; }
|
|
}
|
|
end = lex.off;
|
|
if (end - t.off > 2) {
|
|
if (lex.text[end-2:2] == "px") {
|
|
t.unit = PIXELS;
|
|
end -= 2;
|
|
} else if (lex.text[end-2:2] == "mm") {
|
|
t.unit = MILLIMETERS;
|
|
end -= 2;
|
|
} else if (lex.text[end-2:2] == "pt" || lex.text[end-2:2] == "em") {
|
|
io::eprintn("units 'em' or 'pt' are not supported at the moment");
|
|
t.type = INVALID;
|
|
break;
|
|
}
|
|
}
|
|
String number_str = lex.text[t.off..end-1];
|
|
float? value = number_str.to_float();
|
|
if (catch value) { t.type = INVALID; break; }
|
|
t.value = value;
|
|
t.text = lex.text[t.off..lex.off-1];
|
|
}
|
|
|
|
if (t.type == INVALID) {
|
|
io::eprintfn("CSS Lexing ERROR at %d:%d: '%s' is not a valid token", t.line, t.col, lex.text[t.off..lex.off]);
|
|
}
|
|
return t;
|
|
}
|
|
|
|
fn Token Lexer.peep_token(&lex)
|
|
{
|
|
Lexer start_state = *lex;
|
|
Token t;
|
|
t = lex.next_token();
|
|
*lex = start_state;
|
|
return t;
|
|
}
|
|
|
|
|
|
struct Parser {
|
|
Lexer lex;
|
|
Style style;
|
|
Id style_id;
|
|
float mm_to_px;
|
|
}
|
|
|
|
macro bool Parser.expect(&p, Token* t, TokenType type)
|
|
{
|
|
*t = p.lex.next_token();
|
|
if (t.type == type) return true;
|
|
io::eprintfn("CSS parsing error at %d:%d: expected %s but got %s", t.line, t.col, type, t.type);
|
|
return false;
|
|
}
|
|
|
|
fn bool Parser.parse_style(&p)
|
|
{
|
|
Token t;
|
|
p.style = {};
|
|
p.style_id = 0;
|
|
|
|
// style name
|
|
if (p.expect(&t, IDENTIFIER) == false) return false;
|
|
p.style_id = t.text.hash();
|
|
|
|
// style body
|
|
if (p.expect(&t, LCURLY) == false) return false;
|
|
|
|
while (true) {
|
|
if (p.parse_property() == false) return false;
|
|
t = p.lex.peep_token();
|
|
if (t.type != IDENTIFIER) break;
|
|
}
|
|
|
|
if (p.expect(&t, RCURLY) == false) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
fn bool Parser.parse_property(&p)
|
|
{
|
|
Token t, prop;
|
|
if (p.expect(&prop, IDENTIFIER) == false) return false;
|
|
if (p.expect(&t, COLON) == false) return false;
|
|
|
|
switch (prop.text) {
|
|
case "padding":
|
|
Rect padding;
|
|
if (p.parse_size(&padding) == false) return false;
|
|
p.style.padding = padding;
|
|
|
|
case "border":
|
|
Rect border;
|
|
if (p.parse_size(&border) == false) return false;
|
|
p.style.border = border;
|
|
|
|
case "margin":
|
|
Rect margin;
|
|
if (p.parse_size(&margin) == false) return false;
|
|
p.style.margin = margin;
|
|
|
|
case "bg":
|
|
Color bg;
|
|
if (p.parse_color(&bg) == false) return false;
|
|
p.style.bg = bg;
|
|
|
|
case "fg":
|
|
Color fg;
|
|
if (p.parse_color(&fg) == false) return false;
|
|
p.style.fg = fg;
|
|
|
|
case "primary":
|
|
Color primary;
|
|
if (p.parse_color(&primary) == false) return false;
|
|
p.style.primary = primary;
|
|
|
|
case "secondary":
|
|
Color secondary;
|
|
if (p.parse_color(&secondary) == false) return false;
|
|
p.style.secondary = secondary;
|
|
|
|
case "accent":
|
|
Color accent;
|
|
if (p.parse_color(&accent) == false) return false;
|
|
p.style.accent = accent;
|
|
|
|
case "radius":
|
|
short r;
|
|
if (p.parse_number(&r) == false) return false;
|
|
if (r < 0) {
|
|
io::eprintfn("CSS parsing error at %d:%d: 'radius' must be a positive number, got %d", t.line, t.col, r);
|
|
return false;
|
|
}
|
|
p.style.radius = (ushort)r;
|
|
|
|
case "size":
|
|
short s;
|
|
if (p.parse_number(&s) == false) return false;
|
|
if (s < 0) {
|
|
io::eprintfn("CSS parsing error at %d:%d: 'size' must be a positive number, got %d", t.line, t.col, s);
|
|
return false;
|
|
}
|
|
p.style.size = (ushort)s;
|
|
|
|
|
|
default:
|
|
io::eprintfn("CSS parsing error at %d:%d: '%s' is not a valid property", prop.line, prop.col, prop.text);
|
|
return false;
|
|
}
|
|
if (p.expect(&t, SEMICOLON) == false) return false;
|
|
return true;
|
|
}
|
|
|
|
fn bool Parser.parse_number(&p, short* n)
|
|
{
|
|
Token t;
|
|
if (p.expect(&t, NUMBER) == false) return false;
|
|
*n = t.to_px(p.mm_to_px);
|
|
return true;
|
|
}
|
|
|
|
// FIXME: since '#' is punctuation this cannot be done in parsing but it has to be done in lexing
|
|
fn bool Parser.parse_color(&p, Color* c)
|
|
{
|
|
Token t;
|
|
if (p.expect(&t, COLOR) == false) return false;
|
|
*c = t.color;
|
|
return true;
|
|
}
|
|
|
|
fn bool Parser.parse_size(&p, Rect* r)
|
|
{
|
|
short x;
|
|
Token t;
|
|
|
|
if (p.parse_number(&x) == false) return false;
|
|
|
|
t = p.lex.peep_token();
|
|
if (t.type == NUMBER) {
|
|
// we got another number so we expect three more
|
|
r.x = x;
|
|
if (p.parse_number(&x) == false) return false;
|
|
r.y = x;
|
|
if (p.parse_number(&x) == false) return false;
|
|
r.w = x;
|
|
if (p.parse_number(&x) == false) return false;
|
|
r.h = x;
|
|
return true;
|
|
} else if (t.type == SEMICOLON) {
|
|
// just one number, all dimensions are the same
|
|
r.x = r.y = r.w = r.h = x;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|