package main import engine "shared: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 }