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
[ ] Ctx.set_z_index()
[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] 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
[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot
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
[ ] Maybe cache codepoint converted strings
[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
be better to use a different allcation strategy.
[ ] 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
border is effectively the border size plus the padding since there is a gap between the border
rect and the internal rect. A better solution is to leave it up to the renderer to draw the rect
correctly
[x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the
content, the background color is applied starting from the border. Right now push_rect() offsets
the background rect by both border and padding
## Layout

View File

@ -8,110 +8,80 @@ struct ElemButton {
}
// draw a button, return the events on that button
macro Ctx.button(&ctx, Rect size, bool state = false, ...)
=> ctx.button_id(@compute_id($vasplat), size, state);
fn ElemEvents? Ctx.button_id(&ctx, Id id, Rect size, bool active)
macro Ctx.button(&ctx, String label = "", String icon = "", ...)
=> ctx.button_id(@compute_id($vasplat), label, icon);
fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon)
{
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"));
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 {}; }
Style* style = ctx.styles.get_style(@str_hash("button"));
text_size.x = elem.bounds.x;
text_size.y = elem.bounds.y;
text_size = text_size.center_to(elem.bounds);
Sprite* sprite = icon != "" ? ctx.sprite_atlas.get(icon)! : &&(Sprite){};
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);
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
Style* style = is_active ? style_active : style_norm;
// Draw the button
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
ctx.push_string(text_size, label, parent.div.z_index, style.fg)!;
return elem.events;
}
macro Ctx.button_icon(&ctx, String icon, String on_icon = "", bool active = false, ...)
=> ctx.button_icon_id(@compute_id($vasplat), icon, on_icon, active);
fn ElemEvents? Ctx.button_icon_id(&ctx, Id id, String icon, String on_icon, bool active)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Sprite* def_sprite = ctx.sprite_atlas.get(icon)!;
Sprite* on_sprite = ctx.sprite_atlas.get(on_icon) ?? &&(Sprite){};
Rect max_size = def_sprite.rect().max(on_sprite.rect());
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)!;
Rect content_bounds = elem.content_bounds(style);
Rect text_bounds = {
.x = content_bounds.x + icon_size.w + left_pad + inner_pad,
.y = content_bounds.y,
.w = content_bounds.w - icon_size.w - left_pad - inner_pad - right_pad,
.h = content_bounds.h
};
text_bounds = text_size.center_to(text_bounds);
Rect icon_bounds = {
.x = content_bounds.x,
.y = content_bounds.y,
.w = icon_size.w + right_pad + inner_pad,
.h = (short)max(icon_size.h, content_bounds.h)
};
icon_bounds = icon_size.center_to(icon_bounds);
bool is_active = ctx.elem_focus(elem) || elem.events.mouse_hover;
Style s = *style;
if (is_active) {
s.secondary = s.primary;
s.bg = s.accent;
}
// Draw the button
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
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;
}
// FIXME: this should be inside the style
macro Ctx.checkbox(&ctx, String desc, Point off, bool* active, String 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)
{
Rect border = style.border;
Rect padding = style.padding;
ushort radius = style.radius;
Color bg = style.bg;
Color border_color = style.secondary;
@ -111,10 +110,10 @@ fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = {
.x = rect.x + border.x + padding.x,
.y = rect.y + border.y + padding.y,
.w = rect.w - (border.x+border.w) - (padding.x+padding.w),
.h = rect.h - (border.y+border.h) - (padding.y+padding.h),
.x = rect.x + border.x,
.y = rect.y + border.y,
.w = rect.w - (border.x+border.w),
.h = rect.h - (border.y+border.h),
},
.rect.color = bg,
.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)!;
}
// 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)
{
Cmd cmd = {

View File

@ -78,6 +78,7 @@ fn void? Ctx.layout_next_column(&ctx)
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)
{

View File

@ -156,8 +156,22 @@ macro Point Rect.bottom_right(Rect r)
macro Rect Rect.center_to(Rect a, Rect b)
{
Point off = {.x = (b.w - a.w)/2, .y = (b.h - a.h)/2};
return a.off(off);
return {
.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 {
margin: 2 2 2 2;
border: 2 2 2 2;
padding: 1 1 1 1;
padding: 2 2 2 2;
radius: 10;
size: 32;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
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;
accent: #504945ff;
}
checkbox {

View File

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