unified button element

This commit is contained in:
Alessandro Mauri 2025-07-13 20:08:18 +02:00
parent b48c413d2d
commit 80d17d7b33
7 changed files with 107 additions and 132 deletions

14
TODO
View File

@ -15,8 +15,10 @@ to maintain focus until mouse release (fix scroll bars)
[x] Implement a z index and sort command buffer based on that [x] Implement a z index and sort command buffer based on that
[ ] Ctx.set_z_index() [ ] Ctx.set_z_index()
[x] Sort command buffer on insertion [x] Sort command buffer on insertion
[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot of boiler plate and reuse it [x] Standardize element handling, for example all buttons do almost the same thing, so write a lot
[x] The id combination in gen_id() uses an intger division, which is costly, use another combination function that is non-linear and doesn't use division of boiler plate and reuse it
[x] The id combination in gen_id() uses an intger division, which is costly, use another combination
function that is non-linear and doesn't use division
[ ] Animations, somehow [ ] Animations, somehow
[ ] Maybe cache codepoint converted strings [ ] Maybe cache codepoint converted strings
[x] Fix scroll wheel when div is scrolled [x] Fix scroll wheel when div is scrolled
@ -39,10 +41,10 @@ to maintain focus until mouse release (fix scroll bars)
each struct Elem, struct Cmd, etc. is as big as the largest element. It would each struct Elem, struct Cmd, etc. is as big as the largest element. It would
be better to use a different allcation strategy. be better to use a different allcation strategy.
[ ] Add a way to handle time events like double clicks [ ] Add a way to handle time events like double clicks
[ ] Border and padding do not go well together if the library issues two rect commands, the visible [x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the
border is effectively the border size plus the padding since there is a gap between the border content, the background color is applied starting from the border. Right now push_rect() offsets
rect and the internal rect. A better solution is to leave it up to the renderer to draw the rect the background rect by both border and padding
correctly
## Layout ## Layout

View File

@ -8,110 +8,80 @@ struct ElemButton {
} }
// draw a button, return the events on that button macro Ctx.button(&ctx, String label = "", String icon = "", ...)
macro Ctx.button(&ctx, Rect size, bool state = false, ...) => ctx.button_id(@compute_id($vasplat), label, icon);
=> ctx.button_id(@compute_id($vasplat), size, state); fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon)
fn ElemEvents? Ctx.button_id(&ctx, Id id, Rect size, bool active)
{ {
id = ctx.gen_id(id)!; id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!; Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Style* style_norm = ctx.styles.get_style(@str_hash("button")); Style* style = ctx.styles.get_style(@str_hash("button"));
elem.bounds = ctx.position_element(parent, size, style_norm);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) { return {}; }
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
elem.events = ctx.get_elem_events(elem);
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
// Draw the button
ctx.push_rect(elem.bounds, parent.div.z_index, is_active ? style_active : style_norm)!;
return elem.events;
}
macro Ctx.button_label(&ctx, String label, Rect size = {0,0,short.max,short.max}, bool active = false, ...)
=> ctx.button_label_id(@compute_id($vasplat), label, size, active);
fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool active)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
// TODO: cache text_size just like text_unbounded() does
Rect text_size = ctx.get_text_bounds(label)!;
Rect btn_size = text_size.grow({10,10});
// 2. Layout
elem.bounds = ctx.position_element(parent, btn_size, style_norm);
if (elem.bounds.is_null()) { return {}; }
text_size.x = elem.bounds.x; Sprite* sprite = icon != "" ? ctx.sprite_atlas.get(icon)! : &&(Sprite){};
text_size.y = elem.bounds.y;
text_size = text_size.center_to(elem.bounds);
Rect min_size = {0, 0, style.size, style.size};
Rect text_size = ctx.get_text_bounds(label)!;
Rect icon_size = sprite.rect();
ushort h_lh = (ushort)(ctx.font.line_height() / 2);
ushort left_pad = label != "" ? h_lh : 0;
ushort inner_pad = label != "" && icon != "" ? h_lh : 0;
ushort right_pad = left_pad;
/* |left_pad
* +--v-----------------------------------+
* |<->+--------+ |
* | | | +-----------------+ |
* | | icon | | label | |
* | | | +-----------------+ |
* | +--------+<->| |<->|
* +-------------^--------------------^---+
* |inner_pad |right_pad
*/
Rect tot_size = {
.w = (short)max(min_size.w, icon_size.w + text_size.w + left_pad + inner_pad + right_pad),
.h = (short)max(min_size.h, max(icon_size.h, text_size.h)),
};
elem.bounds = ctx.position_element(parent, tot_size, style);
if (elem.bounds.is_null()) return {};
elem.events = ctx.get_elem_events(elem); elem.events = ctx.get_elem_events(elem);
Rect content_bounds = elem.content_bounds(style);
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
Style* style = is_active ? style_active : style_norm; Rect text_bounds = {
.x = content_bounds.x + icon_size.w + left_pad + inner_pad,
// Draw the button .y = content_bounds.y,
ctx.push_rect(elem.bounds, parent.div.z_index, style)!; .w = content_bounds.w - icon_size.w - left_pad - inner_pad - right_pad,
ctx.push_string(text_size, label, parent.div.z_index, style.fg)!; .h = content_bounds.h
};
return elem.events; text_bounds = text_size.center_to(text_bounds);
}
Rect icon_bounds = {
macro Ctx.button_icon(&ctx, String icon, String on_icon = "", bool active = false, ...) .x = content_bounds.x,
=> ctx.button_icon_id(@compute_id($vasplat), icon, on_icon, active); .y = content_bounds.y,
fn ElemEvents? Ctx.button_icon_id(&ctx, Id id, String icon, String on_icon, bool active) .w = icon_size.w + right_pad + inner_pad,
{ .h = (short)max(icon_size.h, content_bounds.h)
id = ctx.gen_id(id)!; };
icon_bounds = icon_size.center_to(icon_bounds);
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; bool is_active = ctx.elem_focus(elem) || elem.events.mouse_hover;
Style s = *style;
Sprite* def_sprite = ctx.sprite_atlas.get(icon)!; if (is_active) {
Sprite* on_sprite = ctx.sprite_atlas.get(on_icon) ?? &&(Sprite){}; s.secondary = s.primary;
Rect max_size = def_sprite.rect().max(on_sprite.rect()); s.bg = s.accent;
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
elem.bounds = ctx.position_element(parent, max_size, style_norm);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) { return {}; }
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
elem.events = ctx.get_elem_events(elem);
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
Style* style = is_active ? style_active : style_norm;
Id tex_id = ctx.sprite_atlas.id;
if (active && on_icon != "") {
ctx.push_sprite(elem.bounds, on_sprite.uv(), tex_id, parent.div.z_index, type: on_sprite.type)!;
} else {
ctx.push_sprite(elem.bounds, def_sprite.uv(), tex_id, parent.div.z_index, type: def_sprite.type)!;
} }
// Draw the button ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
ctx.push_rect(elem.bounds, parent.div.z_index, style)!; ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!;
// Style ss = {.bg = 0x000000ffu.@to_rgba()};
// ctx.push_rect(content_bounds, parent.div.z_index, &ss)!;
ctx.push_string(text_bounds, label, parent.div.z_index, style.fg)!;
return elem.events; return elem.events;
} }
// FIXME: this should be inside the style // FIXME: this should be inside the style
macro Ctx.checkbox(&ctx, String desc, Point off, bool* active, String tick_sprite = {}, ...) macro Ctx.checkbox(&ctx, String desc, Point off, bool* active, String tick_sprite = {}, ...)
=> ctx.checkbox_id(@compute_id($vasplat), desc, off, active, tick_sprite); => ctx.checkbox_id(@compute_id($vasplat), desc, off, active, tick_sprite);

View File

@ -93,7 +93,6 @@ fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index)
fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style) fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
{ {
Rect border = style.border; Rect border = style.border;
Rect padding = style.padding;
ushort radius = style.radius; ushort radius = style.radius;
Color bg = style.bg; Color bg = style.bg;
Color border_color = style.secondary; Color border_color = style.secondary;
@ -111,10 +110,10 @@ fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
Cmd cmd = { Cmd cmd = {
.type = CMD_RECT, .type = CMD_RECT,
.rect.rect = { .rect.rect = {
.x = rect.x + border.x + padding.x, .x = rect.x + border.x,
.y = rect.y + border.y + padding.y, .y = rect.y + border.y,
.w = rect.w - (border.x+border.w) - (padding.x+padding.w), .w = rect.w - (border.x+border.w),
.h = rect.h - (border.y+border.h) - (padding.y+padding.h), .h = rect.h - (border.y+border.h),
}, },
.rect.color = bg, .rect.color = bg,
.rect.radius = radius, .rect.radius = radius,
@ -123,6 +122,7 @@ fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
ctx.push_cmd(&cmd, z_index)!; ctx.push_cmd(&cmd, z_index)!;
} }
// TODO: accept a Sprite* instead of all this shit
fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_index, Color hue = 0xffffffffu.to_rgba(), SpriteType type = SPRITE_NORMAL) fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_index, Color hue = 0xffffffffu.to_rgba(), SpriteType type = SPRITE_NORMAL)
{ {
Cmd cmd = { Cmd cmd = {

View File

@ -78,6 +78,7 @@ fn void? Ctx.layout_next_column(&ctx)
parent.div.origin_r = parent.div.origin_c; parent.div.origin_r = parent.div.origin_c;
} }
macro Rect Elem.content_bounds(&elem, style) => elem.bounds.pad(style.border).pad(style.padding);
macro Rect Elem.get_view(&elem) macro Rect Elem.get_view(&elem)
{ {

View File

@ -156,8 +156,22 @@ macro Point Rect.bottom_right(Rect r)
macro Rect Rect.center_to(Rect a, Rect b) macro Rect Rect.center_to(Rect a, Rect b)
{ {
Point off = {.x = (b.w - a.w)/2, .y = (b.h - a.h)/2}; return {
return a.off(off); .x = b.x + (b.w - a.w)/2,
.y = b.y + (b.h - a.h)/2,
.w = a.w,
.h = a.h,
};
}
macro Rect Rect.pad(Rect a, Rect b)
{
return {
.x = a.x + b.x,
.y = a.y + b.y,
.w = a.w - b.x - b.w,
.h = a.h - b.y - b.h,
};
} }

View File

@ -9,27 +9,15 @@ default {
button { button {
margin: 2 2 2 2; margin: 2 2 2 2;
border: 2 2 2 2; border: 2 2 2 2;
padding: 1 1 1 1; padding: 2 2 2 2;
radius: 10; radius: 10;
size: 32;
bg: #3c3836ff; bg: #3c3836ff;
fg: #fbf1c7ff; fg: #fbf1c7ff;
primary: #cc241dff; primary: #cc241dff;
secondary: #458588ff; secondary: #458588ff;
accent: #fabd2fff; accent: #504945ff;
}
button-active {
margin: 2 2 2 2;
border: 2 2 2 2;
padding: 1 1 1 1;
radius: 10;
bg: #504945ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #cc241dff;
accent: #fabd2fff;
} }
checkbox { checkbox {

View File

@ -202,16 +202,16 @@ fn int main(String[] args)
ui.div_begin({.w=-100})!!; ui.div_begin({.w=-100})!!;
{ {
ui.layout_set_column()!!; ui.layout_set_column()!!;
if (ui.button({0,0,30,30}, toggle)!!.mouse_press) { if (ui.button(icon: "tux")!!.mouse_press) {
io::printn("press button0"); io::printn("press button0");
toggle = !toggle; toggle = !toggle;
} }
//ui.layout_next_column()!!; //ui.layout_next_column()!!;
if (ui.button({0,0,30,30})!!.mouse_press) { if (ui.button(label: "ciao", icon: "tick")!!.mouse_press) {
io::printn("press button1"); io::printn("press button1");
} }
//ui.layout_next_column()!!; //ui.layout_next_column()!!;
if (ui.button({0,0,30,30})!!.mouse_release) { if (ui.button()!!.mouse_release) {
io::printn("release button2"); io::printn("release button2");
} }
@ -227,7 +227,7 @@ fn int main(String[] args)
ui.text_unbounded("Ciao Mamma\nAbilità ⚡\n'\udb80\udd2c'")!!; ui.text_unbounded("Ciao Mamma\nAbilità ⚡\n'\udb80\udd2c'")!!;
ui.layout_next_column()!!; ui.layout_next_column()!!;
ui.button_label("Continua!")!!; ui.button("Continua!")!!;
ui.layout_next_row()!!; ui.layout_next_row()!!;
static bool check; static bool check;
@ -250,15 +250,15 @@ fn int main(String[] args)
if (ui.slider_ver({0,0,30,100}, &slider2)!!.update) { if (ui.slider_ver({0,0,30,100}, &slider2)!!.update) {
io::printfn("other slider: %f", slider2); io::printfn("other slider: %f", slider2);
} }
ui.button({0,0,50,50})!!; ui.button()!!;
ui.button({0,0,50,50})!!; ui.button()!!;
ui.button({0,0,50,50})!!; ui.button()!!;
ui.button({0,0,50,50})!!; ui.button()!!;
if (toggle) { if (toggle) {
ui.button({0,0,50,50})!!; ui.button()!!;
ui.button({0,0,50,50})!!; ui.button()!!;
ui.button({0,0,50,50})!!; ui.button()!!;
ui.button({0,0,50,50})!!; ui.button()!!;
} }
ui.layout_next_column()!!; ui.layout_next_column()!!;
ui.layout_set_row()!!; ui.layout_set_row()!!;