package main import engine "../zephr" // M3 selection controls and indicators. // // These are drawn rather than assembled from buttons: a switch thumb that grows // on selection, a checkbox tick, a slider track split at the handle. All of them // are containers with a custom draw, plus a clickable box for the signal. // -- Switch ------------------------------------------------------------------ // // Track 52x32 fully rounded. Thumb 16dp unselected, 24dp selected, and the // growth plus travel is what sells the control -- both are animated. M3_SWITCH_W :: 52 M3_SWITCH_H :: 32 M3_SWITCH_THUMB_OFF :: 16 M3_SWITCH_THUMB_ON :: 24 M3_SWITCH_BORDER :: 2 @(private = "file") Switch_Draw :: struct { t: f32, // 0 = off, 1 = on } @(private = "file") switch_draw :: proc(box: ^engine.Box, user_data: rawptr) { d := cast(^Switch_Draw)user_data if d == nil { return } t := d.t r := box.rect h := r.y1 - r.y0 track_bg := m3_lerp_color(m3.surface_variant, m3.primary, t) engine.ui_draw_rect(r, track_bg, 0, 0, h / 2) // Unselected the track is outlined; the outline fades out as it fills. if t < 1 { engine.ui_draw_rect(r, m3_alpha(m3.outline, 1 - t), M3_SWITCH_BORDER, 0, h / 2) } thumb_d := M3_SWITCH_THUMB_OFF + (M3_SWITCH_THUMB_ON - M3_SWITCH_THUMB_OFF) * t thumb_c := m3_lerp_color(m3.outline, m3.on_primary, t) // Travel is measured between thumb centres so the inset stays even at both // ends despite the thumb changing size. pad := (h - M3_SWITCH_THUMB_ON) / 2 cx0 := r.x0 + pad + M3_SWITCH_THUMB_ON / 2 cx1 := r.x1 - pad - M3_SWITCH_THUMB_ON / 2 cx := cx0 + (cx1 - cx0) * t cy := (r.y0 + r.y1) / 2 thumb := engine.ui_rect(cx - thumb_d / 2, cy - thumb_d / 2, cx + thumb_d / 2, cy + thumb_d / 2) engine.ui_draw_rect(thumb, thumb_c, 0, 0, thumb_d / 2) } m3_switch :: proc(on: ^bool, id: engine.Id) -> bool { engine.ui_set_next_width(engine.ui_px(M3_SWITCH_W, 1)) engine.ui_set_next_height(engine.ui_px(M3_SWITCH_H, 1)) box := engine.ui_create_box_with_id({.MouseClickable, .KeyboardClickable}, id) sig := engine.ui_signal_from_box(box) clicked := engine.ui_clicked(sig) if clicked { on^ = !on^ } d := new(Switch_Draw, context.temp_allocator) d.t = engine.ui_anim_f32(id, on^ ? 1 : 0) engine.ui_box_equip_custom_draw(box, switch_draw, d) return clicked } // -- Checkbox ---------------------------------------------------------------- // // 18dp box with a 2dp outline and 2dp radius, centred in a 48dp target. M3_CHECKBOX_SIZE :: 18 M3_CHECKBOX_TARGET :: 48 @(private = "file") Check_Draw :: struct { t: f32, } @(private = "file") checkbox_draw :: proc(box: ^engine.Box, user_data: rawptr) { d := cast(^Check_Draw)user_data if d == nil { return } t := d.t r := box.rect cx := (r.x0 + r.x1) / 2 cy := (r.y0 + r.y1) / 2 half := f32(M3_CHECKBOX_SIZE) / 2 inner := engine.ui_rect(cx - half, cy - half, cx + half, cy + half) if t > 0 { engine.ui_draw_rect(inner, m3_alpha(m3.primary, t), 0, 0, M3_SHAPE_XS / 2) } if t < 1 { engine.ui_draw_rect(inner, m3_alpha(m3.on_surface_variant, 1 - t), 2, 0, M3_SHAPE_XS / 2) } } // The tick is a glyph rather than geometry: the icon font already has one that // matches the spec, and drawing two rotated strokes would only approximate it. m3_checkbox :: proc(checked: ^bool, id: engine.Id) -> bool { engine.ui_set_next_width(engine.ui_px(M3_CHECKBOX_TARGET, 1)) engine.ui_set_next_height(engine.ui_px(M3_CHECKBOX_TARGET, 1)) engine.ui_set_next_align_x(.Center) engine.ui_set_next_align_y(.Center) box := engine.ui_create_box_with_id({.MouseClickable, .KeyboardClickable}, id) sig := engine.ui_signal_from_box(box) clicked := engine.ui_clicked(sig) if clicked { checked^ = !checked^ } d := new(Check_Draw, context.temp_allocator) d.t = engine.ui_anim_f32(id, checked^ ? 1 : 0) engine.ui_box_equip_custom_draw(box, checkbox_draw, d) if checked^ { engine.push_parent(box) engine.ui_set_next_font_size(M3_CHECKBOX_SIZE) engine.ui_set_next_text_color(m3.on_primary) engine.ui_text(ICON_CHECK) engine.pop_parent() } return clicked } // -- Radio button ------------------------------------------------------------ // // 20dp circle, 2dp outline, 10dp inner dot when selected. M3_RADIO_SIZE :: 20 M3_RADIO_TARGET :: 48 @(private = "file") Radio_Draw :: struct { t: f32, } @(private = "file") radio_draw :: proc(box: ^engine.Box, user_data: rawptr) { d := cast(^Radio_Draw)user_data if d == nil { return } r := box.rect cx := (r.x0 + r.x1) / 2 cy := (r.y0 + r.y1) / 2 half := f32(M3_RADIO_SIZE) / 2 ring := engine.ui_rect(cx - half, cy - half, cx + half, cy + half) ring_c := m3_lerp_color(m3.on_surface_variant, m3.primary, d.t) engine.ui_draw_rect(ring, ring_c, 2, 0, half) // The dot scales up from nothing rather than appearing, which is what the // spec's selection transition does. dot_half := (f32(M3_RADIO_SIZE) / 2 - 4) * d.t if dot_half > 0 { dot := engine.ui_rect(cx - dot_half, cy - dot_half, cx + dot_half, cy + dot_half) engine.ui_draw_rect(dot, m3.primary, 0, 0, dot_half) } } m3_radio :: proc(selected: bool, id: engine.Id) -> bool { engine.ui_set_next_width(engine.ui_px(M3_RADIO_TARGET, 1)) engine.ui_set_next_height(engine.ui_px(M3_RADIO_TARGET, 1)) box := engine.ui_create_box_with_id({.MouseClickable, .KeyboardClickable}, id) sig := engine.ui_signal_from_box(box) d := new(Radio_Draw, context.temp_allocator) d.t = engine.ui_anim_f32(id, selected ? 1 : 0) engine.ui_box_equip_custom_draw(box, radio_draw, d) return engine.ui_clicked(sig) } // -- Slider ------------------------------------------------------------------ // // 4dp track, 20dp handle. The inactive track is drawn only to the right of the // handle so the two never overlap through the handle's translucent edge. M3_SLIDER_TRACK_H :: 4 M3_SLIDER_HANDLE :: 20 M3_SLIDER_HEIGHT :: 48 @(private = "file") Slider_Draw :: struct { t: f32, } @(private = "file") slider_draw :: proc(box: ^engine.Box, user_data: rawptr) { d := cast(^Slider_Draw)user_data if d == nil { return } r := box.rect cy := (r.y0 + r.y1) / 2 half_h := f32(M3_SLIDER_TRACK_H) / 2 hh := f32(M3_SLIDER_HANDLE) / 2 // The handle centre stays inside the box at both extremes. x0 := r.x0 + hh x1 := r.x1 - hh cx := x0 + (x1 - x0) * d.t engine.ui_draw_rect(engine.ui_rect(r.x0, cy - half_h, cx - hh - 2, cy + half_h), m3.primary, 0, 0, half_h) engine.ui_draw_rect(engine.ui_rect(cx + hh + 2, cy - half_h, r.x1, cy + half_h), m3.surface_c_highest, 0, 0, half_h) engine.ui_draw_rect(engine.ui_rect(cx - hh, cy - hh, cx + hh, cy + hh), m3.primary, 0, 0, hh) } // Returns the value in 0..1, dragging while held. m3_slider :: proc(value: ^f32, id: engine.Id) -> bool { engine.ui_set_next_width(engine.ui_fill()) engine.ui_set_next_height(engine.ui_px(M3_SLIDER_HEIGHT, 1)) box := engine.ui_create_box_with_id({.MouseClickable, .KeyboardClickable}, id) sig := engine.ui_signal_from_box(box) changed := false if engine.ui_dragging(sig) || engine.ui_pressed(sig) { hh := f32(M3_SLIDER_HANDLE) / 2 x0 := box.rect.x0 + hh x1 := box.rect.x1 - hh if x1 > x0 { // Pointer, not mouse: this tracks a finger just as well. t := (engine.ui_pointer_pos().x - x0) / (x1 - x0) value^ = clamp(t, 0, 1) changed = true } } d := new(Slider_Draw, context.temp_allocator) d.t = clamp(value^, 0, 1) engine.ui_box_equip_custom_draw(box, slider_draw, d) return changed } // -- Linear progress --------------------------------------------------------- M3_PROGRESS_H :: 4 @(private = "file") Progress_Draw :: struct { t: f32, } @(private = "file") progress_draw :: proc(box: ^engine.Box, user_data: rawptr) { d := cast(^Progress_Draw)user_data if d == nil { return } r := box.rect half := f32(M3_PROGRESS_H) / 2 engine.ui_draw_rect(r, m3.surface_c_highest, 0, 0, half) w := (r.x1 - r.x0) * clamp(d.t, 0, 1) if w > 0 { engine.ui_draw_rect(engine.ui_rect(r.x0, r.y0, r.x0 + w, r.y1), m3.primary, 0, 0, half) } } m3_progress_linear :: proc(value: f32, id: engine.Id) { engine.ui_set_next_width(engine.ui_fill()) engine.ui_set_next_height(engine.ui_px(M3_PROGRESS_H, 1)) box := engine.ui_create_box_with_id({}, id) d := new(Progress_Draw, context.temp_allocator) d.t = value engine.ui_box_equip_custom_draw(box, progress_draw, d) } m3_lerp_color :: proc(a, b: engine.Color, t: f32) -> engine.Color { return { a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t, a.a + (b.a - a.a) * t, } }