#import "Basic"; #import "Math"; Button :: struct { x0, y0, x1, y1: float; text: string; alpha: float = 0.4; scale: float = 1.0; } Menu_Data :: struct { selection: int; font: *Simp.Dynamic_Font; buttons: [..] Button; } init_menu_app :: (widget: *Widget_State) { data := New(Menu_Data); data.selection = 0; data.font = null; // Define the buttons up front array_add(*data.buttons, .{ 100.0, 500.0, 400.0, 600.0, "Example widget", 0.4, 1.0 }); array_add(*data.buttons, .{ 100.0, 350.0, 400.0, 450.0, "Maze App", 0.4, 1.0 }); array_add(*data.buttons, .{ 100.0, 200.0, 400.0, 300.0, "Quit", 0.4, 1.0 }); widget.data = data; widget.init = (widget: *Widget_State) { data := cast(*Menu_Data) widget.data; // Load the font data.font = Simp.get_font_at_size("data", "Anonymous_Pro.ttf", 48); // Dynamically resize our buttons so they always fit their text! if data.font { for * data.buttons { text_width := Simp.prepare_text(data.font, it.text); center_x := (it.x0 + it.x1) / 2.0; // Ensure the button is at least as wide as the text + 60px of padding needed_width := cast(float)text_width + 60.0; actual_width := max(it.x1 - it.x0, needed_width); it.x0 = center_x - actual_width / 2.0; it.x1 = center_x + actual_width / 2.0; } } }; widget.update = update; widget.handle_input = handle_input; widget.render = render; widget.shutdown = (widget: *Widget_State) { data := cast(*Menu_Data) widget.data; array_free(data.buttons); free(data); }; } #scope_file time_selected := 0.0; update :: (widget: *Widget_State, dt: float) { data := cast(*Menu_Data) widget.data; time_selected += dt; debug_add_value("time_selected: %", time_selected); for * data.buttons { // 1. Set our targets target_scale := ifx data.selection == it_index then 1.05 else 1.0; target_alpha: float; if data.selection == it_index { // make button GROW target_alpha = 1.0; target_scale = target_scale + (sin(cast(float)time_selected * 2.0) * 0.03); // STOP GROW if time_selected > (3.14 / 4.0) { target_scale = 1.08; } } else { // Dimmer when unselected target_alpha = 0.4; } // 2. Smoothly lerp current values towards the targets using exponential decay it.scale = it.scale + (target_scale - it.scale) * (15.0 * dt); it.alpha = it.alpha + (target_alpha - it.alpha) * (15.0 * dt); } } handle_input :: (widget: *Widget_State, occluded: bool) -> (occludes: bool) { data := cast(*Menu_Data) widget.data; start_select := data.selection; occludes := false; if !occluded { // Query the mouse position! By passing true for right_handed, // Y=0 is at the bottom, which perfectly matches our rendering projection. mouse_x, mouse_y, success := get_mouse_pointer_position(window, true); if success { mouse_x_f := cast(float) mouse_x; mouse_y_f := cast(float) mouse_y; for data.buttons { if mouse_x_f >= it.x0 && mouse_x_f <= it.x1 && mouse_y_f >= it.y0 && mouse_y_f <= it.y1 { occludes = true; data.selection = it_index; } } } if Input.mouse_delta_x + Input.mouse_delta_y + Input.mouse_delta_z != 0 { if !occludes { data.selection = -1; // Deselect if moving mouse outside } } } else { data.selection = -1; } for event: Input.events_this_frame { if event.type == .KEYBOARD && event.key_pressed { if event.key_code == .ARROW_UP { if data.selection <= 0 { data.selection = data.buttons.count - 1; } else { data.selection -= 1; } } if event.key_code == .ARROW_DOWN { data.selection += 1; if data.selection >= data.buttons.count then data.selection = 0; } if event.key_code == .ENTER || event.key_code == .MOUSE_BUTTON_LEFT { // Execute button action if data.selection == 0 { switch_app(*example_state); } else if data.selection == 1 { switch_app(*maze_state); } else if data.selection == 2 { exit(0); } } } } if start_select != data.selection { time_selected = 0; } return occludes; } render :: (widget: *Widget_State) { data := cast(*Menu_Data) widget.data; // Clear to a blueish color to represent the menu Simp.clear_render_target(0.2, 0.3, 0.4, 1.0); Simp.set_shader_for_color(true); Simp.immediate_begin(); Simp.immediate_set_2d_projection(window_width, window_height); // Iterate through and draw all buttons dynamically based on their struct definition for data.buttons { color := ifx data.selection == it_index then Vector4.{0.8, 0.8, 0.2, it.alpha} else Vector4.{0.5, 0.5, 0.5, it.alpha}; // Scale the button from its center center_x := (it.x0 + it.x1) / 2.0; center_y := (it.y0 + it.y1) / 2.0; width := it.x1 - it.x0; height := it.y1 - it.y0; draw_x0 := center_x - (width / 2.0) * it.scale; draw_x1 := center_x + (width / 2.0) * it.scale; draw_y0 := center_y - (height / 2.0) * it.scale; draw_y1 := center_y + (height / 2.0) * it.scale; Simp.immediate_quad(draw_x0, draw_y0, draw_x1, draw_y1, color); } Simp.immediate_flush(); // Draw text on the buttons (perfectly centered!) if data.font { for data.buttons { text_width := Simp.prepare_text(data.font, it.text); // Calculate the dead-center of the unscaled button center_x := (it.x0 + it.x1) / 2.0; center_y := (it.y0 + it.y1) / 2.0; // Calculate text placement (subtract half text width/height) // Note: Font size is 48, so we subtract ~24 for vertical centering text_x := center_x - (cast(float)text_width / 2.0); text_y := center_y - 24.0 + 8.0; // 8.0 is a minor visual baseline tweak // 1. Generate the unscaled quads in world space Simp.generate_quads_for_prepared_text(data.font, cast(int)text_x, cast(int)text_y, 0); // 2. Mathematically scale every quad outward from the button's center! for * quad: data.font.current_quads { quad.p0.x = center_x + (quad.p0.x - center_x) * it.scale; quad.p0.y = center_y + (quad.p0.y - center_y) * it.scale; quad.p1.x = center_x + (quad.p1.x - center_x) * it.scale; quad.p1.y = center_y + (quad.p1.y - center_y) * it.scale; quad.p2.x = center_x + (quad.p2.x - center_x) * it.scale; quad.p2.y = center_y + (quad.p2.y - center_y) * it.scale; quad.p3.x = center_x + (quad.p3.x - center_x) * it.scale; quad.p3.y = center_y + (quad.p3.y - center_y) * it.scale; } // 3. Draw the newly scaled quads Simp.draw_generated_quads(data.font, .{0, 0, 0, it.alpha}); } } }