2026-09-04 14:26:42 +02:00
|
|
|
package main
|
|
|
|
|
|
2026-09-04 16:08:21 +02:00
|
|
|
import engine "shared:zephr"
|
2026-09-04 14:26:42 +02:00
|
|
|
|
|
|
|
|
// M3 common buttons, FABs and chips.
|
|
|
|
|
//
|
|
|
|
|
// Spec: common buttons are 40dp tall, fully rounded (20dp), labelLarge 14sp/500,
|
|
|
|
|
// with 24dp horizontal padding -- 16dp on the leading side when an icon is
|
|
|
|
|
// present, since the icon already supplies optical weight.
|
|
|
|
|
|
|
|
|
|
M3_BUTTON_HEIGHT :: 40
|
|
|
|
|
M3_BUTTON_PAD_X :: 24
|
|
|
|
|
M3_BUTTON_PAD_X_ICON :: 16
|
|
|
|
|
M3_BUTTON_ICON_SIZE :: 18 // dp, smaller than an app bar icon
|
|
|
|
|
M3_BUTTON_GAP :: 8 // dp, icon -> label
|
|
|
|
|
|
|
|
|
|
M3_Button_Kind :: enum {
|
|
|
|
|
Filled, // highest emphasis
|
|
|
|
|
Tonal, // filled tonal
|
|
|
|
|
Outlined, // medium emphasis, 1dp outline
|
|
|
|
|
Elevated, // surfaceContainerLow, reads as lifted
|
|
|
|
|
Text, // lowest emphasis, no container
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@(private = "file")
|
|
|
|
|
button_style :: proc(kind: M3_Button_Kind) -> engine.ButtonStyle {
|
|
|
|
|
s: engine.ButtonStyle
|
|
|
|
|
s.border_radius = M3_BUTTON_HEIGHT / 2 // fully rounded
|
|
|
|
|
s.border_thickness = 0
|
|
|
|
|
s.padding_x = M3_BUTTON_PAD_X
|
|
|
|
|
s.padding_y = 0
|
|
|
|
|
|
|
|
|
|
container, on_container: engine.Color
|
|
|
|
|
switch kind {
|
|
|
|
|
case .Filled:
|
|
|
|
|
container, on_container = m3.primary, m3.on_primary
|
|
|
|
|
case .Tonal:
|
|
|
|
|
container, on_container = m3.secondary_container, m3.on_secondary_container
|
|
|
|
|
case .Elevated:
|
|
|
|
|
container, on_container = m3.surface_c_low, m3.primary
|
|
|
|
|
case .Outlined:
|
|
|
|
|
container, on_container = m3_alpha(m3.surface, 0), m3.primary
|
|
|
|
|
s.border_thickness = 1
|
|
|
|
|
case .Text:
|
|
|
|
|
container, on_container = m3_alpha(m3.surface, 0), m3.primary
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.background_idle = container
|
|
|
|
|
// Every state is the container with an on-container layer over it -- the
|
|
|
|
|
// spec defines states this way rather than naming separate colours.
|
|
|
|
|
s.background_hover = m3_state_layer(container, on_container, M3_STATE_HOVER)
|
|
|
|
|
s.background_active = m3_state_layer(container, on_container, M3_STATE_PRESSED)
|
|
|
|
|
s.background_disabled = kind == .Text || kind == .Outlined \
|
|
|
|
|
? m3_alpha(m3.surface, 0) \
|
|
|
|
|
: m3_alpha(m3.on_surface, M3_DISABLED_BG)
|
|
|
|
|
|
|
|
|
|
outline := kind == .Outlined ? m3.outline : m3_alpha(m3.surface, 0)
|
|
|
|
|
s.border_idle = outline
|
|
|
|
|
s.border_hover = outline
|
|
|
|
|
s.border_active = outline
|
|
|
|
|
s.border_disabled = kind == .Outlined ? m3_alpha(m3.on_surface, M3_DISABLED_BG) : outline
|
|
|
|
|
|
|
|
|
|
s.text_idle = on_container
|
|
|
|
|
s.text_hover = on_container
|
|
|
|
|
s.text_active = on_container
|
|
|
|
|
s.text_disabled = m3_alpha(m3.on_surface, M3_DISABLED_FG)
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m3_button :: proc(label: string, kind: M3_Button_Kind, id: engine.Id, enabled := true) -> bool {
|
|
|
|
|
engine.ui_set_next_height(engine.ui_px(M3_BUTTON_HEIGHT, 1))
|
|
|
|
|
engine.ui_set_next_width(engine.ui_children_sum(1))
|
|
|
|
|
|
|
|
|
|
// push, not set_next: a button builds its container and content row before
|
|
|
|
|
// the label box, so a set_next value never reaches the text.
|
|
|
|
|
engine.ui_push_font_size(M3_LABEL_LARGE)
|
|
|
|
|
defer engine.ui_pop_font_size()
|
|
|
|
|
engine.ui_push_font_weight(M3_WEIGHT_MEDIUM)
|
|
|
|
|
defer engine.ui_pop_font_weight()
|
|
|
|
|
|
|
|
|
|
click, _ := engine.ui_button_styled(label, button_style(kind), enabled, id)
|
|
|
|
|
return click
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m3_button_icon :: proc(label, icon: string, kind: M3_Button_Kind, id: engine.Id, enabled := true) -> bool {
|
|
|
|
|
st := button_style(kind)
|
|
|
|
|
st.padding_x = M3_BUTTON_PAD_X_ICON
|
|
|
|
|
st.icon_size = M3_BUTTON_ICON_SIZE // 18dp, per spec -- not the 14sp label size
|
|
|
|
|
|
|
|
|
|
engine.ui_set_next_height(engine.ui_px(M3_BUTTON_HEIGHT, 1))
|
|
|
|
|
engine.ui_set_next_width(engine.ui_children_sum(1))
|
|
|
|
|
engine.ui_push_font_size(M3_LABEL_LARGE)
|
|
|
|
|
defer engine.ui_pop_font_size()
|
|
|
|
|
engine.ui_push_font_weight(M3_WEIGHT_MEDIUM)
|
|
|
|
|
defer engine.ui_pop_font_weight()
|
|
|
|
|
|
|
|
|
|
click, _ := engine.ui_button_styled_with_icons(label, st, icon, "", enabled, id)
|
|
|
|
|
return click
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -- FAB ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
M3_Fab_Size :: enum {
|
|
|
|
|
Small, // 40dp, 12dp radius
|
|
|
|
|
Regular, // 56dp, 16dp radius
|
|
|
|
|
Large, // 96dp, 28dp radius
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m3_fab :: proc(icon: string, size: M3_Fab_Size, id: engine.Id) -> bool {
|
|
|
|
|
dim, radius, icon_size: f32
|
|
|
|
|
switch size {
|
|
|
|
|
case .Small: dim, radius, icon_size = 40, M3_SHAPE_MD, 24
|
|
|
|
|
case .Regular: dim, radius, icon_size = 56, M3_SHAPE_LG, 24
|
|
|
|
|
case .Large: dim, radius, icon_size = 96, M3_SHAPE_XL, 36
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s: engine.ButtonStyle
|
|
|
|
|
// The primary-container FAB is the default of the three colour mappings.
|
|
|
|
|
s.background_idle = m3.primary_container
|
|
|
|
|
s.background_hover = m3_state_layer(m3.primary_container, m3.on_primary_container, M3_STATE_HOVER)
|
|
|
|
|
s.background_active = m3_state_layer(m3.primary_container, m3.on_primary_container, M3_STATE_PRESSED)
|
|
|
|
|
s.background_disabled = m3_alpha(m3.on_surface, M3_DISABLED_BG)
|
|
|
|
|
s.text_idle = m3.on_primary_container
|
|
|
|
|
s.text_hover = m3.on_primary_container
|
|
|
|
|
s.text_active = m3.on_primary_container
|
|
|
|
|
s.text_disabled = m3_alpha(m3.on_surface, M3_DISABLED_FG)
|
|
|
|
|
s.border_radius = radius
|
|
|
|
|
s.border_thickness = 0
|
|
|
|
|
s.padding_x = 0
|
|
|
|
|
s.padding_y = 0
|
|
|
|
|
|
|
|
|
|
engine.ui_set_next_width(engine.ui_px(dim, 1))
|
|
|
|
|
engine.ui_set_next_height(engine.ui_px(dim, 1))
|
|
|
|
|
engine.ui_push_font_size(i32(icon_size))
|
|
|
|
|
defer engine.ui_pop_font_size()
|
|
|
|
|
|
|
|
|
|
click, _ := engine.ui_button_styled(icon, s, true, id)
|
|
|
|
|
return click
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extended FAB: 56dp tall, 16dp radius, icon + label, 16dp padding.
|
|
|
|
|
m3_fab_extended :: proc(label, icon: string, id: engine.Id) -> bool {
|
|
|
|
|
s: engine.ButtonStyle
|
|
|
|
|
s.background_idle = m3.primary_container
|
|
|
|
|
s.background_hover = m3_state_layer(m3.primary_container, m3.on_primary_container, M3_STATE_HOVER)
|
|
|
|
|
s.background_active = m3_state_layer(m3.primary_container, m3.on_primary_container, M3_STATE_PRESSED)
|
|
|
|
|
s.background_disabled = m3_alpha(m3.on_surface, M3_DISABLED_BG)
|
|
|
|
|
s.text_idle = m3.on_primary_container
|
|
|
|
|
s.text_hover = m3.on_primary_container
|
|
|
|
|
s.text_active = m3.on_primary_container
|
|
|
|
|
s.text_disabled = m3_alpha(m3.on_surface, M3_DISABLED_FG)
|
|
|
|
|
s.border_radius = M3_SHAPE_LG
|
|
|
|
|
s.border_thickness = 0
|
|
|
|
|
s.padding_x = 16
|
|
|
|
|
s.padding_y = 0
|
|
|
|
|
s.icon_size = 24 // spec FAB icon; the 14sp label must not drag it down
|
|
|
|
|
|
|
|
|
|
engine.ui_set_next_height(engine.ui_px(56, 1))
|
|
|
|
|
engine.ui_set_next_width(engine.ui_children_sum(1))
|
|
|
|
|
engine.ui_push_font_size(M3_LABEL_LARGE)
|
|
|
|
|
defer engine.ui_pop_font_size()
|
|
|
|
|
engine.ui_push_font_weight(M3_WEIGHT_MEDIUM)
|
|
|
|
|
defer engine.ui_pop_font_weight()
|
|
|
|
|
|
|
|
|
|
click, _ := engine.ui_button_styled_with_icons(label, s, icon, "", true, id)
|
|
|
|
|
return click
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -- Chips -------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
M3_CHIP_HEIGHT :: 32
|
|
|
|
|
M3_CHIP_RADIUS :: M3_SHAPE_SM
|
|
|
|
|
M3_CHIP_PAD_X :: 16
|
|
|
|
|
M3_CHIP_ICON_SIZE :: 18 // dp, per spec
|
|
|
|
|
|
|
|
|
|
// Assist chip: outlined container, labelLarge, optional leading icon.
|
|
|
|
|
m3_chip_assist :: proc(label, icon: string, id: engine.Id) -> bool {
|
|
|
|
|
s: engine.ButtonStyle
|
|
|
|
|
s.background_idle = m3_alpha(m3.surface, 0)
|
|
|
|
|
s.background_hover = m3_state_layer(m3.surface, m3.on_surface, M3_STATE_HOVER)
|
|
|
|
|
s.background_active = m3_state_layer(m3.surface, m3.on_surface, M3_STATE_PRESSED)
|
|
|
|
|
s.background_disabled = m3_alpha(m3.surface, 0)
|
|
|
|
|
s.border_idle = m3.outline
|
|
|
|
|
s.border_hover = m3.outline
|
|
|
|
|
s.border_active = m3.outline
|
|
|
|
|
s.border_disabled = m3_alpha(m3.on_surface, M3_DISABLED_BG)
|
|
|
|
|
s.text_idle = m3.on_surface
|
|
|
|
|
s.text_hover = m3.on_surface
|
|
|
|
|
s.text_active = m3.on_surface
|
|
|
|
|
s.text_disabled = m3_alpha(m3.on_surface, M3_DISABLED_FG)
|
|
|
|
|
s.border_radius = M3_CHIP_RADIUS
|
|
|
|
|
s.border_thickness = 1
|
|
|
|
|
s.padding_x = M3_CHIP_PAD_X
|
|
|
|
|
s.padding_y = 0
|
|
|
|
|
s.icon_size = M3_CHIP_ICON_SIZE
|
|
|
|
|
|
|
|
|
|
engine.ui_set_next_height(engine.ui_px(M3_CHIP_HEIGHT, 1))
|
|
|
|
|
engine.ui_set_next_width(engine.ui_children_sum(1))
|
|
|
|
|
engine.ui_push_font_size(M3_LABEL_LARGE)
|
|
|
|
|
defer engine.ui_pop_font_size()
|
|
|
|
|
engine.ui_push_font_weight(M3_WEIGHT_MEDIUM)
|
|
|
|
|
defer engine.ui_pop_font_weight()
|
|
|
|
|
|
|
|
|
|
click, _ := engine.ui_button_styled_with_icons(label, s, icon, "", true, id)
|
|
|
|
|
return click
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter chip: selection swaps the container to secondaryContainer, drops the
|
|
|
|
|
// outline, and shows a leading check. Toggles `selected` in place.
|
|
|
|
|
m3_chip_filter :: proc(label: string, selected: ^bool, id: engine.Id) -> bool {
|
|
|
|
|
s: engine.ButtonStyle
|
|
|
|
|
container := selected^ ? m3.secondary_container : m3_alpha(m3.surface, 0)
|
|
|
|
|
on_container := selected^ ? m3.on_secondary_container : m3.on_surface_variant
|
|
|
|
|
|
|
|
|
|
s.background_idle = container
|
|
|
|
|
s.background_hover = m3_state_layer(container, on_container, M3_STATE_HOVER)
|
|
|
|
|
s.background_active = m3_state_layer(container, on_container, M3_STATE_PRESSED)
|
|
|
|
|
s.background_disabled = m3_alpha(m3.surface, 0)
|
|
|
|
|
outline := selected^ ? m3_alpha(m3.surface, 0) : m3.outline
|
|
|
|
|
s.border_idle = outline
|
|
|
|
|
s.border_hover = outline
|
|
|
|
|
s.border_active = outline
|
|
|
|
|
s.border_disabled = outline
|
|
|
|
|
s.text_idle = on_container
|
|
|
|
|
s.text_hover = on_container
|
|
|
|
|
s.text_active = on_container
|
|
|
|
|
s.text_disabled = m3_alpha(m3.on_surface, M3_DISABLED_FG)
|
|
|
|
|
s.border_radius = M3_CHIP_RADIUS
|
|
|
|
|
s.border_thickness = selected^ ? 0 : 1
|
|
|
|
|
s.padding_x = M3_CHIP_PAD_X
|
|
|
|
|
s.padding_y = 0
|
|
|
|
|
s.icon_size = M3_CHIP_ICON_SIZE
|
|
|
|
|
|
|
|
|
|
engine.ui_set_next_height(engine.ui_px(M3_CHIP_HEIGHT, 1))
|
|
|
|
|
engine.ui_set_next_width(engine.ui_children_sum(1))
|
|
|
|
|
engine.ui_push_font_size(M3_LABEL_LARGE)
|
|
|
|
|
defer engine.ui_pop_font_size()
|
|
|
|
|
engine.ui_push_font_weight(M3_WEIGHT_MEDIUM)
|
|
|
|
|
defer engine.ui_pop_font_weight()
|
|
|
|
|
|
|
|
|
|
leading := selected^ ? ICON_CHECK : ""
|
|
|
|
|
click, _ := engine.ui_button_styled_with_icons(label, s, leading, "", true, id)
|
|
|
|
|
if click {
|
|
|
|
|
selected^ = !selected^
|
|
|
|
|
}
|
|
|
|
|
return click
|
|
|
|
|
}
|