Files
android-ui-dogfooding/m3_buttons.odin
T

248 lines
9.4 KiB
Odin
Raw Normal View History

2026-09-04 14:26:42 +02:00
package main
import engine "../zephr"
// 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
}