simple style import with a subset of css

This commit is contained in:
Alessandro Mauri 2025-07-04 11:17:42 +02:00
parent a390a8908c
commit 9fc1d90455
4 changed files with 430 additions and 21 deletions

View File

@ -6,6 +6,7 @@ import fifo;
import std::io;
import std::core::string;
import std::core::mem::allocator;
// element ids are just long ints
@ -74,25 +75,14 @@ const uint MAX_CMDS = 2048;
const uint ROOT_ID = 1;
const uint TEXT_MAX = 64;
// global style, similar to the css box model
struct Style { // css box model
Rect padding;
Rect border;
Rect margin;
Color bgcolor; // background color
Color fgcolor; // foreground color
Color brcolor; // border color
ushort radius;
}
struct Ctx {
IdTree tree;
ElemCache cache;
CmdQueue cmd_queue;
StyleMap styles;
// total size in pixels of the context
ushort width, height;
Style style;
Style* style; // TODO: rename to "active_style" or something
Font font;
SpriteAtlas sprite_atlas;
@ -206,16 +196,12 @@ fn void? Ctx.init(&ctx)
ctx.cmd_queue.init(MAX_CMDS)!;
defer catch { (void)ctx.cmd_queue.free(); }
ctx.styles.init(allocator::heap());
defer catch { ctx.styles.free(); }
ctx.active_div = 0;
// TODO: add style config
ctx.style.margin = {2, 2, 2, 2};
ctx.style.border = {2, 2, 2, 2};
ctx.style.padding = {1, 1, 1, 1};
ctx.style.radius = 12;
ctx.style.bgcolor = 0x282828ffu.to_rgba();
ctx.style.fgcolor = 0xfbf1c7ffu.to_rgba();
ctx.style.brcolor = 0xd79921ffu.to_rgba();
ctx.style = &DEFAULT_STYLE;
}
fn void Ctx.free(&ctx)
@ -225,6 +211,7 @@ fn void Ctx.free(&ctx)
(void)ctx.cmd_queue.free();
(void)ctx.font.free();
(void)ctx.sprite_atlas.free();
(void)ctx.styles.free();
}
fn void? Ctx.frame_begin(&ctx)

View File

@ -203,6 +203,17 @@ macro Color uint.to_rgba(u)
};
}
macro Color uint.@to_rgba($u)
{
return {
.r = (char)(($u >> 24) & 0xff),
.g = (char)(($u >> 16) & 0xff),
.b = (char)(($u >> 8) & 0xff),
.a = (char)(($u >> 0) & 0xff)
};
}
macro uint Color.to_uint(c)
{
uint u = c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);

View File

@ -0,0 +1,402 @@
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 bgcolor; // background color
Color fgcolor; // foreground color
Color brcolor; // border color
ushort radius;
}
const Style DEFAULT_STYLE = {
.margin = {2, 2, 2, 2},
.border = {2, 2, 2, 2},
.padding = {1, 1, 1, 1},
.bgcolor = 0x282828ffu.@to_rgba(),
.fgcolor = 0xfbf1c7ffu.@to_rgba(),
.brcolor = 0xd79921ffu.@to_rgba(),
.radius = 12,
};
// 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 e = s) {
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++;
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;
* bgcolor : #RRGGBBAA;
* fgcolor : #RRGGBBAA;
* brcolor : #RRGGBBAA;
* radius : uint;
* }
* 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,
PUNCT,
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.type = PUNCT;
t.text = lex.text[lex.off:1];
if (lex.advance() == 0) { t.type = INVALID; break; }
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())) {
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_text(&p, Token* t, TokenType type, String text)
{
*t = p.lex.next_token();
if (t.type == type && t.text == text) {
return true;
}
io::eprintfn("CSS parsing error at %d:%d: expected type:%s text:'%s' but got type:%s text:'%s'",
t.line, t.col, type, text, t.type, t.text);
return false;
}
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;
// style name
if (p.expect(&t, IDENTIFIER) == false) return false;
p.style_id = t.text.hash();
// style body
if (p.expect_text(&t, PUNCT, "{") == 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_text(&t, PUNCT, "}") == 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_text(&t, PUNCT, ":") == 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 "bgcolor":
Color bgcolor;
if (p.parse_color(&bgcolor) == false) return false;
p.style.bgcolor = bgcolor;
case "fgcolor":
Color fgcolor;
if (p.parse_color(&fgcolor) == false) return false;
p.style.fgcolor = fgcolor;
case "brcolor":
Color brcolor;
if (p.parse_color(&brcolor) == false) return false;
p.style.brcolor = brcolor;
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;
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_text(&t, PUNCT, ";") == 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 == PUNCT && t.text == ";") {
// just one number, all dimensions are the same
r.x = r.y = r.w = r.h = x;
return true;
}
return false;
}

View File

@ -113,6 +113,15 @@ fn int main(String[] args)
ren.create_pipeline("UGUI_PIPELINE_RECT", RECT);
// TESTING CSS
String stylesheet = `
ciao {
margin: 12 24px 12mm 3;
brcolor: #ffff00ff;
radius: 10;
}`;
io::printfn("imported %d styles", ui.import_style_from_string(stylesheet));
isz frame;
double fps;
bool toggle = true;