major project restructure
This commit is contained in:
parent
bd31f562fc
commit
4f7fa7d50c
67 changed files with 25 additions and 2427 deletions
461
src/style.c3
Normal file
461
src/style.c3
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
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 = 0,
|
||||
.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 bottom;
|
||||
* 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.
|
||||
*/
|
||||
// TODO: implement <style name> : <another style> to easily inherit all properties
|
||||
// of a previously defined style
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
// LEXER //
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
|
||||
|
||||
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; }
|
||||
}
|
||||
usz h_len = lex.off - hex_start;
|
||||
if (h_len != 8 && h_len != 6) {
|
||||
io::eprintfn("CSS lexing error at %d:%d: the only suppported color formats are #RRGGBBAA or #RRGGBB", t.line, t.col);
|
||||
t.type = INVALID;
|
||||
break;
|
||||
}
|
||||
|
||||
char[10] hex_str = (char[])"0x";
|
||||
hex_str[2:h_len] = lex.text[hex_start:h_len];
|
||||
if (h_len == 6) hex_str[8..9] = "ff"[..];
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
// PARSER //
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// style := ( IDENTIFIER "{" property_list "}" )
|
||||
// property_list := property property_list
|
||||
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;
|
||||
}
|
||||
|
||||
// property := ( number_property | color_property | size_property ) ";"
|
||||
// number_property := ( "radius" | "size" ) "=" number
|
||||
// color_property := ( "bg" | "fg" | "primary" | "secondary" | "accent" ) "=" color
|
||||
// size_property := ( "padding" | "border" | "margin" ) "=" size
|
||||
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;
|
||||
}
|
||||
|
||||
// number := NUMBER
|
||||
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;
|
||||
}
|
||||
|
||||
// color := COLOR
|
||||
// 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;
|
||||
}
|
||||
|
||||
// size := number | number_list
|
||||
// number_list := number number number number
|
||||
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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue