Material3 components

This commit is contained in:
2026-09-04 14:26:42 +02:00
parent 891fadca85
commit 0419237433
11 changed files with 1362 additions and 37 deletions
+242
View File
@@ -0,0 +1,242 @@
package main
import engine "../zephr"
// M3 containers, list items and navigation.
// -- Card --------------------------------------------------------------------
//
// 12dp radius. The three variants differ only in container role and outline --
// M3 expresses elevation as colour, so no shadow is missing here.
M3_Card_Kind :: enum {
Elevated,
Filled,
Outlined,
}
m3_card_begin :: proc(kind: M3_Card_Kind, id: engine.Id) {
bg: engine.Color
outline := m3_alpha(m3.surface, 0)
thickness: f32 = 0
switch kind {
case .Elevated:
bg = m3.surface_c_low
case .Filled:
bg = m3.surface_c_highest
case .Outlined:
bg = m3.surface
outline = m3.outline_variant
thickness = 1
}
engine.ui_set_next_width(engine.ui_fill())
engine.ui_set_next_height(engine.ui_children_sum(1))
engine.ui_set_next_background_color(bg)
engine.ui_set_next_border_color(outline)
engine.ui_set_next_border_thickness(thickness)
engine.ui_set_next_border_radius(M3_SHAPE_MD)
engine.ui_set_next_flags({.DrawBackground, .DrawBorder})
open_column(id)
engine.ui_padding(16, {.Left, .Right, .Top, .Bottom})
}
m3_card_end :: proc() {
close_box()
}
// -- Divider -----------------------------------------------------------------
m3_divider :: proc(id: engine.Id) {
engine.ui_set_next_width(engine.ui_fill())
engine.ui_set_next_height(engine.ui_px(1, 1))
engine.ui_set_next_background_color(m3.outline_variant)
engine.ui_set_next_flags({.DrawBackground})
engine.ui_create_box_with_id({}, id)
}
// -- List item ---------------------------------------------------------------
//
// One-line list item: 56dp tall, 16dp horizontal padding, bodyLarge headline,
// with an optional leading icon and trailing text.
M3_LIST_ITEM_H :: 56
m3_list_item :: proc(headline, leading_icon, trailing: string, id: engine.Id) -> bool {
engine.ui_set_next_width(engine.ui_fill())
engine.ui_set_next_height(engine.ui_px(M3_LIST_ITEM_H, 1))
engine.ui_set_next_align_y(.Center)
box := engine.ui_create_box_with_id({.MouseClickable, .KeyboardClickable}, id)
sig := engine.ui_signal_from_box(box)
// The state layer is the only container a list item has at rest.
if engine.ui_hovering(sig) {
box.background_color = m3_state_layer(m3.surface, m3.on_surface, M3_STATE_HOVER)
box.flags |= {.DrawBackground}
}
engine.push_parent(box)
engine.ui_set_next_width(engine.ui_fill())
engine.ui_set_next_height(engine.ui_fill())
engine.ui_set_next_align_y(.Center)
open_row()
engine.ui_spacer(engine.ui_px(16, 1))
if len(leading_icon) > 0 {
engine.ui_set_next_font_size(24)
engine.ui_set_next_text_color(m3.on_surface_variant)
engine.ui_text(leading_icon)
engine.ui_spacer(engine.ui_px(16, 1))
}
engine.ui_set_next_font_size(M3_BODY_LARGE)
engine.ui_set_next_text_color(m3.on_surface)
engine.ui_text(headline)
engine.ui_spacer(engine.ui_percent(1, 0))
if len(trailing) > 0 {
engine.ui_set_next_font_size(M3_LABEL_SMALL)
engine.ui_set_next_text_color(m3.on_surface_variant)
engine.ui_text(trailing)
}
engine.ui_spacer(engine.ui_px(16, 1))
close_box() // row
close_box() // box
return engine.ui_clicked(sig)
}
// -- Snackbar ----------------------------------------------------------------
//
// inverseSurface container at 4dp radius, 48dp min height, with the action
// label in inversePrimary.
m3_snackbar :: proc(message, action: string, id: engine.Id) -> bool {
engine.ui_set_next_width(engine.ui_fill())
engine.ui_set_next_height(engine.ui_px(48, 1))
engine.ui_set_next_background_color(m3.inverse_surface)
engine.ui_set_next_border_radius(M3_SHAPE_XS)
engine.ui_set_next_align_y(.Center)
engine.ui_set_next_flags({.DrawBackground})
engine.ui_row(id) // pops at the end of this proc
engine.ui_spacer(engine.ui_px(16, 1))
engine.ui_set_next_font_size(M3_BODY_MEDIUM)
engine.ui_set_next_text_color(m3.inverse_on_surface)
engine.ui_text(message)
engine.ui_spacer(engine.ui_percent(1, 0))
clicked := false
if len(action) > 0 {
s: engine.ButtonStyle
s.background_idle = m3_alpha(m3.surface, 0)
s.background_hover = m3_state_layer(m3.inverse_surface, m3.inverse_primary, M3_STATE_HOVER)
s.background_active = m3_state_layer(m3.inverse_surface, m3.inverse_primary, M3_STATE_PRESSED)
s.background_disabled = m3_alpha(m3.surface, 0)
s.text_idle = m3.inverse_primary
s.text_hover = m3.inverse_primary
s.text_active = m3.inverse_primary
s.text_disabled = m3.inverse_primary
s.border_radius = M3_SHAPE_XS
s.padding_x = 8
s.padding_y = 0
engine.ui_set_next_height(engine.ui_px(36, 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()
clicked, _ = engine.ui_button_styled(action, s, true, id + 1)
}
engine.ui_spacer(engine.ui_px(8, 1))
return clicked
}
// -- Navigation bar ----------------------------------------------------------
//
// 80dp tall. The selected item gets a 64x32 pill in secondaryContainer behind
// its icon -- that indicator, not a colour change, is what marks selection.
M3_NAV_BAR_H :: 80
M3_NAV_INDICATOR_W :: 64
M3_NAV_INDICATOR_H :: 32
M3_Nav_Item :: struct {
icon: string,
label: string,
}
m3_nav_bar :: proc(items: []M3_Nav_Item, selected: ^int, bottom_inset: f32, id: engine.Id) -> bool {
changed := false
engine.ui_set_next_width(engine.ui_fill())
engine.ui_set_next_height(engine.ui_px(M3_NAV_BAR_H + bottom_inset, 1))
engine.ui_set_next_background_color(m3.surface_c)
engine.ui_set_next_flags({.DrawBackground})
engine.ui_column(id) // pops at the end of this proc
engine.ui_set_next_width(engine.ui_fill())
engine.ui_set_next_height(engine.ui_px(M3_NAV_BAR_H, 1))
engine.ui_row() // pops at the end of this proc
for item, i in items {
is_sel := i == selected^
engine.ui_set_next_width(engine.ui_fill())
engine.ui_set_next_height(engine.ui_fill())
engine.ui_set_next_align_x(.Center)
// Start, not Center: the spec positions the item from the top (12dp pad,
// 32dp indicator, 4dp gap, label, 16dp bottom = 80dp). Centring the group
// instead splits the leftover evenly and lifts the icon off its 12dp mark.
engine.ui_set_next_align_y(.Start)
cell := engine.ui_create_box_with_id({.MouseClickable, .KeyboardClickable}, id + engine.Id(10 + i))
sig := engine.ui_signal_from_box(cell)
if engine.ui_clicked(sig) {
selected^ = i
changed = true
}
engine.push_parent(cell)
engine.ui_set_next_width(engine.ui_children_sum(1))
engine.ui_set_next_height(engine.ui_children_sum(1))
engine.ui_set_next_align_x(.Center)
open_column()
engine.ui_spacer(engine.ui_px(12, 1))
// Indicator pill; unselected items get a transparent one of the same
// size so the icon never shifts between states.
engine.ui_set_next_width(engine.ui_px(M3_NAV_INDICATOR_W, 1))
engine.ui_set_next_height(engine.ui_px(M3_NAV_INDICATOR_H, 1))
engine.ui_set_next_align_x(.Center)
engine.ui_set_next_align_y(.Center)
engine.ui_set_next_background_color(is_sel ? m3.secondary_container : m3_alpha(m3.surface, 0))
engine.ui_set_next_border_radius(M3_NAV_INDICATOR_H / 2)
engine.ui_set_next_flags({.DrawBackground})
open_row()
engine.ui_set_next_font_size(24)
engine.ui_set_next_text_color(is_sel ? m3.on_secondary_container : m3.on_surface_variant)
engine.ui_text(item.icon)
close_box() // indicator
engine.ui_spacer(engine.ui_px(4, 1))
engine.ui_set_next_font_size(M3_LABEL_MEDIUM)
engine.ui_set_next_font_weight(M3_WEIGHT_MEDIUM)
engine.ui_set_next_text_color(is_sel ? m3.on_surface : m3.on_surface_variant)
engine.ui_text(item.label)
close_box() // column
close_box() // cell
}
return changed
}