package main import engine "shared:zephr" // Material 3 "top app bar - small", to spec. // // Every constant here is a Material dp/sp value, which only reads correctly // because layout points are dp -- the backend adopts DisplayMetrics.density as // backing_scale. Expressed in pixels these would be right on exactly one device. APP_BAR_HEIGHT :: 64 // dp, M3 small top app bar container APP_BAR_EDGE_GAP :: 4 // dp, edge -> 48dp touch target (puts the glyph at 16dp) APP_BAR_TOUCH_TARGET :: 48 // dp, minimum accessible target APP_BAR_ICON_SIZE :: 24 // sp, glyph size APP_BAR_TITLE_SIZE :: 22 // sp, titleLarge APP_BAR_TITLE_INSET :: 56 // dp from the leading edge when a nav icon is present APP_BAR_TITLE_PLAIN :: 16 // dp from the leading edge when there is not App_Bar :: struct { title: string, leading: string, // icon glyph, or "" for none actions: []string, // trailing icon glyphs, in order // M3 marks the scrolled state with a container colour change, not a shadow. scrolled: bool, // Status bar height in dp. The bar draws under it and pads its content down, // which is what edge-to-edge looks like natively. top_inset: f32, } App_Bar_Result :: struct { leading_clicked: bool, action_clicked: int, // index into actions, or -1 } // Icon slots are real buttons so they get signals and keyboard focus, but M3 // draws no container at rest -- only the state layer on hover/press. @(private = "file") icon_button_style :: proc() -> engine.ButtonStyle { s: engine.ButtonStyle s.background_idle = {0, 0, 0, 0} s.background_hover = {1, 1, 1, 0.08} // M3 state layer opacities s.background_active = {1, 1, 1, 0.12} s.background_disabled = {0, 0, 0, 0} s.border_idle = {0, 0, 0, 0} s.border_hover = {0, 0, 0, 0} s.border_active = {0, 0, 0, 0} s.border_disabled = {0, 0, 0, 0} s.text_idle = m3.on_surface s.text_hover = m3.on_surface s.text_active = m3.on_surface s.text_disabled = {1, 1, 1, 0.38} s.border_radius = APP_BAR_TOUCH_TARGET / 2 // circular state layer s.border_thickness = 0 s.padding_x = 0 s.padding_y = 0 return s } @(private = "file") icon_button :: proc(glyph: string, id: engine.Id) -> bool { engine.ui_set_next_width(engine.ui_px(APP_BAR_TOUCH_TARGET, 1)) engine.ui_set_next_height(engine.ui_px(APP_BAR_TOUCH_TARGET, 1)) // Must be push, not set_next: a button builds its container and an inner // content row before it ever creates the label box, so a set_next size is // consumed by the container and the glyph falls back to the ambient default. engine.ui_push_font_size(APP_BAR_ICON_SIZE) defer engine.ui_pop_font_size() click, _ := engine.ui_button_styled(glyph, icon_button_style(), true, id) return click } ui_app_bar :: proc(bar: App_Bar) -> App_Bar_Result { res := App_Bar_Result{action_clicked = -1} engine.ui_set_next_width(engine.ui_percent(1, 1)) engine.ui_set_next_height(engine.ui_px(APP_BAR_HEIGHT + bar.top_inset, 1)) engine.ui_set_next_background_color(bar.scrolled ? m3.surface_c : m3.surface) engine.ui_set_next_flags({.DrawBackground}) engine.ui_column() // pops at the end of this proc // The container spans the status bar; the content row sits below it. engine.ui_padding(bar.top_inset, {.Top}) engine.ui_set_next_width(engine.ui_percent(1, 1)) engine.ui_set_next_height(engine.ui_px(APP_BAR_HEIGHT, 1)) engine.ui_set_next_align_y(.Center) engine.ui_row() // pops at the end of this proc if len(bar.leading) > 0 { engine.ui_spacer(engine.ui_px(APP_BAR_EDGE_GAP, 1)) res.leading_clicked = icon_button(bar.leading, 1) // 4 + 48 already consumed; pad out to the 56dp headline inset. engine.ui_spacer(engine.ui_px(APP_BAR_TITLE_INSET - APP_BAR_EDGE_GAP - APP_BAR_TOUCH_TARGET, 1)) } else { engine.ui_spacer(engine.ui_px(APP_BAR_TITLE_PLAIN, 1)) } engine.ui_set_next_font_size(APP_BAR_TITLE_SIZE) engine.ui_set_next_text_color(m3.on_surface) engine.ui_text(bar.title) // Shove the actions to the trailing edge. engine.ui_spacer(engine.ui_percent(1, 0)) for glyph, i in bar.actions { if icon_button(glyph, engine.Id(100 + i)) { res.action_clicked = i } } engine.ui_spacer(engine.ui_px(APP_BAR_EDGE_GAP, 1)) return res }