package main /* Rules of matrices: 1) Order matters when multiplying with vectors - m * v = Column vector x matrix - v * m = Row vector x matrix 2) Odin matrices are column-major for math efficiency fmt.println(matrix[3, 3]f32 { a, -b, 0, b, a, 0, 0, 0, 1, }) >> [0.8660254, 0.5, 0, -0.5, 0.8660254, 0, 0, 0, 1] Additionally this means that the first index of a matrix is the column. mat[, ]. If you had an array of 9 floats ([9]f32) and you accessed the first one It'd be the same as accessing the first colmun of a matrix. array[1] = mat[1, 0] 3) Matrix multiplication order matters // scale first then translate transformation_a := translation * scale // translate first then scale transformation_b := scale * translation */ import rl "libraries/raylib" import ui "libraries/snths_ui" import "core:mem" import "core:fmt" import "core:math/linalg" import "core:strings" import "core:slice" RAYGUI_ICON_NEW :: "#009#" RAYGUI_ICON_SAVE :: "#002#" RAYGUI_ICON_SAVE_AS :: "#001#" RAYGUI_ICON_LOAD :: "#006#" RAYGUI_ICON_IDENTITY_MATRIX :: "#000#" RAYGUI_ICON_TRANSLATION_MATRIX :: "#068#" RAYGUI_ICON_SCALE_MATRIX :: "#070#" RAYGUI_ICON_ROTATION_MATRIX :: "#211#" RAYGUI_ICON_SKEW_MATRIX :: "#192#" RAYGUI_ICON_PLAY :: "#131#" RAYGUI_ICON_PAUSE :: "#132#" RAYGUI_ICON_RESET :: "#211#" RAYGUI_ICON_TRASH :: "#143#" RAYGUI_ICON_DOWN :: "#120#" RAYGUI_ICON_UP :: "#121#" TRANSFORMATION_RECTANGLE_COLOR_ORIGIN :: rl.RED TRANSFORMATION_RECTANGLE_COLOR_MIDDLE :: rl.MAGENTA TRANSFORMATION_RECTANGLE_COLOR_NEXT :: rl.GRAY TRANSFORMATION_RECTANGLE_COLOR_LAST :: rl.BLUE @rodata default_project_load_location := "default" @rodata font_data := #load("assets/fonts/cmuntb.ttf") UiContainerType :: enum { Normal, Matrix, Scroll, } UiElementData_Container :: struct { type: UiContainerType, thickness: f32, scroll: f32, matrix_index: int, } UiElementData_Slider :: struct { min, max: f32, value: ^f32, } UiElementData_FloatInput :: struct { value: ^f32, value_string: cstring, tag: bit_set[enum{UpdateMatrix}], } UiElementData_TextInput :: struct { str: cstring, } UiElementData_ToggleGroup :: struct { value: ^i32, } UiButtonType :: enum { Nothing, ProjectSave, ProjectSaveAs, ProjectLoad, ProjectNew, ProjectSaveConfirm, ProjectLoadConfirm, DismissError, AddIdentity, AddTranslation, AddScale, AddRotation, AddSkewMatrix, AddTranslationMatrixConfirm, AddScaleMatrixConfirm, AddRotationMatrixConfirm, AddSkewMatrixConfirm, ExitDialog, StartTransformation, PauseTransformation, ResumeTransformation, ResetTransformation, ResetGrid, MatrixDelete, MatrixMoveUp, MatrixMoveDown, CenterView, } UiButtonRecalculateFinishGroup :: bit_set[UiButtonType]{ .AddIdentity, .AddTranslationMatrixConfirm, .AddScaleMatrixConfirm, .AddRotationMatrixConfirm, .MatrixDelete, .MatrixMoveUp, .MatrixMoveDown, } UiElementData_Button :: struct { type: UiButtonType, target_matrix: int, options: cstring, } UI_ALLOWED_FOCUS :: bit_set[ui.ElementType]{.InputFloat} HotkeyTypes :: enum { AddIdentity, AddTranslation, AddScale, AddRotation, ZoomXOnly, ZoomYOnly, } @rodata hotkey_without_modifier := [HotkeyTypes]rl.KeyboardKey { .AddIdentity = .I, .AddTranslation = .T, .AddScale = .S, .AddRotation = .R, .ZoomXOnly = .X, .ZoomYOnly = .Y, } HotkeyWithModifier :: enum { ProjectSave, ProjectLoad, } @rodata hotkey_with_modifier := [HotkeyWithModifier]rl.KeyboardKey { .ProjectSave = .S, .ProjectLoad = .O, } FontStyle :: struct { font: rl.Font, size, spacing: f32 } ProgramState :: enum { Normal, Dialog, } MouseState :: enum { Hover, DraggingGrid, DraggingDraggable, HoverMatrices, } ProgramDialogType :: enum { CreateTranslationMatrix, CreateScaleMatrix, CreateRotationMatrix, CreateSkewMatrix, SaveProject, LoadProject, Error, } @rodata program_dialog_messages := [ProgramDialogType]cstring { .CreateTranslationMatrix = "Create tranlation matrix", .CreateScaleMatrix = "Create scale matrix", .CreateRotationMatrix = "Create rotation matrix", .CreateSkewMatrix = "Create skew matrix", .SaveProject = "Save project", .LoadProject = "Load project", .Error = "Error", } ProgramMessageType :: enum { ChangeMatrixTypeClearData, } @rodata program_messages := [ProgramMessageType]cstring { .ChangeMatrixTypeClearData = "Changing the matrix type will delete all current matrices.\nAre you sure you want to do this?", } program: Program InputFloatField :: [32]byte MATRIX_POOL_SIZE :: 4096 DIALOG_STATE_INPUT_FIELD_COUNT :: 16 DIALOG_STATE_TEXT_INPUT_FIELD_SIZE :: 256 DIALOG_STATE_TEXT_INPUT_FIELD_COUNT :: 8 ProgramData_DialogState :: struct { input_float_value_strings: [DIALOG_STATE_INPUT_FIELD_COUNT]InputFloatField, input_float_values: [DIALOG_STATE_INPUT_FIELD_COUNT]f32, input_text_buffers: [DIALOG_STATE_TEXT_INPUT_FIELD_SIZE][DIALOG_STATE_TEXT_INPUT_FIELD_COUNT]byte, } ProgramAnimationState :: enum { Ready, Playing, Paused, } MouseDraggableType :: enum { Point, RectangleCorner, Rectangle, } MouseDraggable_Point :: struct { ref: ^v2, } MouseDraggable_RectangleCornerData :: struct { corner: RectangleCornersCorner, corners_ref: ^RectangleCorners, corner_ref: ^v2, } MouseDraggable_RectangleData :: struct { corners: ^RectangleCorners, } MouseDraggable :: union { MouseDraggable_Point, MouseDraggable_RectangleCornerData, MouseDraggable_RectangleData, } RotationMode :: enum { Degrees, Radians, } Program :: struct { ui_context: ui.Context, ui_element_focus: ui.ElementId, ui_overlay_context: ui.Context, ui_overlay_element_focus: ui.ElementId, ui_overlay_queue_focus_first: bool, ui_tooltip_offset: v2, matrix_float_pool: []f32, matrix_value_string_pool: []InputFloatField, matrix_create_dropdown_value: HotkeyTypes, matrix_count: int, matrix_scroll: f32, matrix_scroll_speed: f32, mouse_state: MouseState, mouse_draggables: [dynamic; 64]MouseDraggable, mouse_current_draggable: MouseDraggable, mouse_draggable_radius: f32, project_save_string: [DIALOG_STATE_TEXT_INPUT_FIELD_COUNT]byte, rotation_mode: RotationMode, grid: Grid, grid_zoom_x_only: bool, grid_zoom_y_only: bool, grid_zoom_speed: f32, font_style: FontStyle, state: ProgramState, message_type: ProgramMessageType, dialog_type: ProgramDialogType, dialog_data: ProgramData_DialogState, dialog_message: cstring, animation: Animation, animation_state: ProgramAnimationState, animation_speed: f32, window_should_close: bool, } float_slice_get_mat3 :: proc(floats: []f32, index: int) -> ^mat3 { return cast(^mat3)(&floats[index * 9]) } matrix_pool_max :: proc() -> int { return len(program.matrix_float_pool) / 9 } matrix_pool_get_mat3 :: proc() -> (value_strings: []InputFloatField, values: []f32) { assert(program.matrix_count + 1 < matrix_pool_max(), "Max matrix count exceeded") value_strings = program.matrix_value_string_pool[program.matrix_count * 9 : program.matrix_count * 9 + 9] values = program.matrix_float_pool[program.matrix_count * 9 : program.matrix_count * 9 + 9] for i in 0.. ProjectWriteError { project := ProjectData { matrix_count = u32(program.matrix_count), matrices = program.matrix_float_pool, grid = program.grid, shapes = program.animation.shapes, } project_write(&project, path) or_return return nil } program_save_project_from_project_string_or_open_dialog :: proc(program: ^Program) { path := strings.string_from_null_terminated_ptr(&program.project_save_string[0], DIALOG_STATE_TEXT_INPUT_FIELD_SIZE) if len(path) > 0 { if err := program_save_project(program, path); err != nil { msg := fmt.caprint("Failed to save project to '", path, "'\n" ,typeid_of(type_of(err)), ".", err, sep = "") program_show_dialog(program, .Error, msg) } else { program.state = .Normal } } else { program_show_dialog(program, .SaveProject) } } program_load_project :: proc(program: ^Program, path: string) -> ProjectReadError { project: ProjectData project.matrices = program.matrix_float_pool project_read(&project, path) or_return program.matrix_count = int(project.matrix_count) program.grid = project.grid program.animation.shapes = project.shapes matrices := slice.reinterpret([]mat3, program.matrix_float_pool[:program.matrix_count * 9]) for mat, i in matrices { start := i * 9 end := (i + 1) * 9 write_matrix_to_values_and_value_strings(program.matrix_float_pool[start:end], program.matrix_value_string_pool[start:end], mat) } program.animation.matrix_count = program.matrix_count return nil } program_show_dialog :: proc(program: ^Program, dialog_type: ProgramDialogType, message: cstring = "") { program.dialog_data = {} program.ui_overlay_queue_focus_first = true program.state = .Dialog program.dialog_type = dialog_type program.dialog_message = message mem.set(&program.dialog_data, 0, size_of(program.dialog_data)) for i in 0.. mat3 { matrices := (cast([^]mat3)raw_data(program.matrix_float_pool))[:program.matrix_count] master_matrix := linalg.identity_matrix(mat3) #reverse for mat in matrices { master_matrix *= mat } return master_matrix } program_set_animation_state :: proc(state: ProgramAnimationState) { if program.animation_state == state do return previous_state := program.animation_state program.animation_state = state switch program.animation_state { case .Ready: program.animation.matrix_index = 0.0 case .Playing: case .Paused: } } program_update :: proc() { if rl.IsKeyPressed(.F4) { rl.ToggleFullscreen() } ui_elements, top_bar_container, matrix_container := program_build_workspace_ui(&program.ui_context) ui_overlay_elements: []ui.Element switch program.state { case .Normal: program.mouse_state = program_handle_mouse(program.mouse_state, top_bar_container, matrix_container) if rl.IsKeyPressed(.TAB) { program.ui_element_focus = ui.focus_next_element(program.ui_element_focus, ui_elements, UI_ALLOWED_FOCUS) } if rl.IsKeyDown(.LEFT_CONTROL) { for hotkey, index in hotkey_with_modifier { if rl.IsKeyPressed(hotkey) { switch index { case .ProjectSave: program_save_project_from_project_string_or_open_dialog(&program) case .ProjectLoad: program_show_dialog(&program, .LoadProject) } } } } else { for hotkey, index in hotkey_without_modifier { if rl.IsKeyPressed(hotkey) { #partial switch index { case .AddIdentity: program_add_identity_matrix() case .AddTranslation: program_show_dialog(&program, .CreateTranslationMatrix) case .AddScale: program_show_dialog(&program, .CreateScaleMatrix) case .AddRotation: program_show_dialog(&program, .CreateRotationMatrix) case .ZoomXOnly: if program.grid_zoom_x_only { program.grid_zoom_x_only = false program.grid_zoom_y_only = false } else { program.grid_zoom_x_only = true program.grid_zoom_y_only = false } case .ZoomYOnly: if program.grid_zoom_y_only { program.grid_zoom_x_only = false program.grid_zoom_y_only = false } else { program.grid_zoom_x_only = false program.grid_zoom_y_only = true } } } } } if rl.WindowShouldClose() { program.window_should_close = true } case .Dialog: ui_overlay_elements = program_build_dialog_ui(&program.ui_overlay_context, program.dialog_type) if program.ui_overlay_queue_focus_first { program.ui_overlay_queue_focus_first = false program.ui_overlay_element_focus = ui.focus_next_element({container_index = 0, element_index = 0}, ui_overlay_elements, UI_ALLOWED_FOCUS) } if rl.IsKeyPressed(.TAB) { program.ui_overlay_element_focus = ui.focus_next_element(program.ui_overlay_element_focus, ui_overlay_elements, UI_ALLOWED_FOCUS) } if rl.IsKeyDown(.ENTER) { vals := program.dialog_data.input_float_values switch program.dialog_type { case .CreateTranslationMatrix: program_add_translation_matrix(vals[0], vals[1], vals[2]) case .CreateScaleMatrix: program_add_scalar_matrix(vals[0], vals[1], vals[2]) case .CreateRotationMatrix: program_add_rotation_matrix(vals[0]) case .CreateSkewMatrix: program_add_skew_matrix(vals[0], vals[1]) case .SaveProject: path := strings.string_from_null_terminated_ptr(&program.dialog_data.input_text_buffers[0][0], DIALOG_STATE_TEXT_INPUT_FIELD_SIZE) if err := program_save_project(&program, path); err != nil { msg := fmt.caprint("Tried saving but got error: '", typeid_of(type_of(err)), ".", err, "'", sep = "") program_show_dialog(&program, .Error, msg) } else { program.project_save_string = program.dialog_data.input_text_buffers[0] program.state = .Normal } case .LoadProject: path := strings.string_from_null_terminated_ptr(&program.dialog_data.input_text_buffers[0][0], DIALOG_STATE_TEXT_INPUT_FIELD_SIZE) if err := program_load_project(&program, path); err != nil { msg := fmt.caprint("Tried loading but got error: '", typeid_of(type_of(err)), ".", err, "'", sep = "") program_show_dialog(&program, .Error, msg) } else { program.state = .Normal } case .Error: program.state = .Normal } program.animation.matrix_count = program.matrix_count program.state = .Normal } if rl.WindowShouldClose() { program.state = .Normal } } clear(&program.mouse_draggables) top_bar_container_br := rect_get_br(rect2(top_bar_container.area)) matrix_container_br := rect_get_br(rect2(matrix_container.area)) grid_text_clip := rect_from_area(v2{matrix_container_br.x, top_bar_container_br.y}, program_screen_size()) grid_area := rect2{0.0, 0.0, **program_screen_size()} grid_color := rl.Color{255, 255, 255, 64} grid_origin_color := rl.Color{255, 255, 255, 128} draw_grid(program.grid, grid_area, rect_grow(grid_text_clip, -4.0), rect_grow(grid_text_clip, -32.0), grid_color, grid_origin_color) { animation := &program.animation if program.animation_state == .Playing { animation.matrix_index += 0.01 } animation_draw(animation^, linalg.inverse(grid_world_to_grid_matrix(program.grid.zoom, program.grid.offset))) switch animation.type { case .Rectangle: append(&program.mouse_draggables, MouseDraggable_RectangleCornerData{ corner = .Tl, corners_ref = &animation.shapes.rectangle, corner_ref = &animation.shapes.rectangle.tl}) append(&program.mouse_draggables, MouseDraggable_RectangleCornerData{ corner = .Tr, corners_ref = &animation.shapes.rectangle, corner_ref = &animation.shapes.rectangle.tr}) append(&program.mouse_draggables, MouseDraggable_RectangleCornerData{ corner = .Bl, corners_ref = &animation.shapes.rectangle, corner_ref = &animation.shapes.rectangle.bl}) append(&program.mouse_draggables, MouseDraggable_RectangleCornerData{ corner = .Br, corners_ref = &animation.shapes.rectangle, corner_ref = &animation.shapes.rectangle.br}) append(&program.mouse_draggables, MouseDraggable_RectangleData{&animation.shapes.rectangle}) } } grid_to_world_matrix := linalg.inverse(grid_world_to_grid_matrix(program.grid.zoom, program.grid.offset)) for draggable in program.mouse_draggables { switch draggable in draggable { case MouseDraggable_Point: position_world := matrix3_transform_xy(grid_to_world_matrix, draggable.ref^) rl.DrawCircleV(position_world, program.mouse_draggable_radius, ANIMATION_COLOR_ORIGIN) case MouseDraggable_RectangleCornerData: position_world := matrix3_transform_xy(grid_to_world_matrix, draggable.corner_ref^) rl.DrawCircleV(position_world, program.mouse_draggable_radius, ANIMATION_COLOR_ORIGIN) case MouseDraggable_RectangleData: } } ui_element_focus := program.state == .Normal ? program.ui_element_focus : ui.element_id_invalid() program.ui_element_focus = program_render_ui_and_handle_focus(&program.ui_context, ui_element_focus, program.font_style, ui_elements) if len(ui_overlay_elements) > 0 { rl.DrawRectangle(0, 0, rl.GetScreenWidth(), rl.GetScreenHeight(), {0, 0, 0, 192}) program.ui_overlay_element_focus = program_render_ui_and_handle_focus( &program.ui_overlay_context, program.ui_overlay_element_focus, program.font_style, ui_overlay_elements) } } program_add_identity_matrix :: proc() { value_strings, values := matrix_pool_get_mat3() write_matrix_to_values_and_value_strings(values, value_strings, mat3(1)) } program_add_translation_matrix :: proc(x, y, z: f32) { value_strings, values := matrix_pool_get_mat3() write_matrix_to_values_and_value_strings(values, value_strings, mat3 { 1, 0, x, 0, 1, y, 0, 0, z, }) } program_add_scalar_matrix :: proc(x, y, z: f32) { value_strings, values := matrix_pool_get_mat3() write_matrix_to_values_and_value_strings(values, value_strings, linalg.matrix3_scale(v3{x, y, z})) } program_add_rotation_matrix :: proc(angle: f32) { value_strings, values := matrix_pool_get_mat3() radians: f32 switch (program.rotation_mode) { case .Degrees: radians = linalg.RAD_PER_DEG * angle case .Radians: radians = angle } write_matrix_to_values_and_value_strings(values, value_strings, matrix3_rotate2(radians)) } program_add_skew_matrix :: proc(x, y: f32) { value_strings, values := matrix_pool_get_mat3() mat := mat3 { 1, x, 0, y, 1, 0, 0, 0, 1, } write_matrix_to_values_and_value_strings(values, value_strings, mat) } write_matrix_to_values_and_value_strings :: proc(values: []f32, value_strings: []InputFloatField, mat: mat3) { for i in 0..<9 { col := i % 3 row := i / 3 values[i] = mat[col, row] write_float_to_buffer(value_strings[i][:], mat[col, row]) } } write_float_to_buffer :: proc(buffer: []byte, value: f32) { switch { case fract(value) == 0.0: fmt.bprintf(buffer, "%.0f", value) case fract(value * 10.0) == 0.0: fmt.bprintf(buffer, "%.1f", value) case fract(value * 100.0) == 0.0: fmt.bprintf(buffer, "%.2f", value) case: fmt.bprintf(buffer, "%.3f", value) } } program_build_dialog_ui :: proc(ctx: ^ui.Context, dialog_type: ProgramDialogType) -> []ui.Element { size := program_screen_size() container_margin := size * 0.33 element_margin: f32 = 4.0 dialog_data := &program.dialog_data ui.start(ctx, V2_ZERO, program_screen_size()) ui.container_begin(ui.dimensions_auto(), {margin = container_margin, padding = 8.0}) header_label_config := ui.ElementConfiguration{label = program_dialog_messages[program.dialog_type], margin = element_margin} if dialog_type == .Error { header_label_config.color = ui.col(rl.RED) } else { header_label_config.color = ui.col(rl.WHITE) } ui.element(.Label, {}, header_label_config) if (len(program.dialog_message) > 0) { ui.element(.Label, {}, {label = program.dialog_message, color = ui.col(rl.WHITE), margin = element_margin}) } switch dialog_type { case .CreateTranslationMatrix, .CreateScaleMatrix: ui_labeled_input_float_element("X", 4.0, &dialog_data.input_float_values[0], &dialog_data.input_float_value_strings[0]) ui_labeled_input_float_element("Y", 4.0, &dialog_data.input_float_values[1], &dialog_data.input_float_value_strings[1]) ui_labeled_input_float_element("Z", 4.0, &dialog_data.input_float_values[2], &dialog_data.input_float_value_strings[2]) button_type: UiButtonType if dialog_type == .CreateTranslationMatrix { button_type = .AddTranslationMatrixConfirm } else { button_type = .AddScaleMatrixConfirm } ui_confirm_and_cancel_buttons(button_type, element_margin) case .CreateRotationMatrix: ui_labeled_input_float_element("Angle", 4.0, &dialog_data.input_float_values[0], &dialog_data.input_float_value_strings[0]) toggle_group_dimensions := ui.dimensions_pixels(128.0, 32.0) toggle_group_options := enum_to_raygui_options_string(RotationMode, context.temp_allocator) toggle_group_config := ui.ElementConfiguration {label = toggle_group_options, margin = element_margin} toggle_group_data := new(UiElementData_ToggleGroup) toggle_group_data^ = {value = cast(^i32)&program.rotation_mode} ui.element(.ToggleGroup, toggle_group_dimensions, toggle_group_config, toggle_group_data) ui_confirm_and_cancel_buttons(.AddRotationMatrixConfirm, element_margin) case .CreateSkewMatrix: ui_labeled_input_float_element("X", 4.0, &dialog_data.input_float_values[0], &dialog_data.input_float_value_strings[0]) ui_labeled_input_float_element("Y", 4.0, &dialog_data.input_float_values[1], &dialog_data.input_float_value_strings[1]) ui_confirm_and_cancel_buttons(.AddSkewMatrixConfirm, element_margin) case .SaveProject: ui_labeled_input_text_element("Path", 4.0, dialog_data.input_text_buffers[0][:]) ui_confirm_and_cancel_buttons(.ProjectSaveConfirm, element_margin) case .LoadProject: ui_labeled_input_text_element("Path", 4.0, dialog_data.input_text_buffers[0][:]) ui_confirm_and_cancel_buttons(.ProjectLoadConfirm, element_margin) case .Error: button_data: ^UiElementData_Button button_data = new(UiElementData_Button, context.temp_allocator) button_data^ = {type = .DismissError} ui.element(.Button, ui.dimensions_pixels(96.0, 32.0), {label = "Ok", margin = element_margin}, button_data) } ui.container_end() return ui.end() } program_screen_size :: proc() -> v2 { return {f32(rl.GetScreenWidth()), f32(rl.GetScreenHeight())} } program_build_workspace_ui :: proc(ctx: ^ui.Context) -> (elements: []ui.Element, top_bar_container, matrix_container: ^ui.Element) { ui.start(ctx, {0, 0}, program_screen_size()) top_bar_container_config := ui.ElementConfiguration {margin = 4.0} top_bar_element_margin: f32 = 4.0 top_bar_container = ui.container_begin({ui.Size_Percentage(1.0), ui.Size_Pixels(48.0)}, top_bar_container_config) top_bar_inner_height := ui.get_element_inner_area(top_bar_container^).height top_bar_button_width := ui.Size_Pixels(top_bar_inner_height) top_bar_button_dimensions := ui.Dimensions {top_bar_button_width, ui.Size_Percentage(1.0)} top_bar_button_config := ui.ElementConfiguration {margin = top_bar_element_margin} top_bar_dropdown_dimensions := ui.Dimensions {ui.Size_Pixels(160.0), ui.Size_Percentage(1.0)} top_bar_dropdown_config := ui.ElementConfiguration {margin = top_bar_element_margin} { button_margin := top_bar_element_margin button_config: ui.ElementConfiguration button_tooltip: cstring button_dimensions := top_bar_button_dimensions button_data: ^UiElementData_Button divider_dimensions := ui.Dimensions{ui.Size_Pixels(1.0), ui.Size_Percentage(1.0)} divider_config := ui.ElementConfiguration{padding = 4.0, color = ui.col(rl.WHITE)} button_data = new(UiElementData_Button, context.temp_allocator) button_tooltip = fmt.ctprint("Save project\n(CTRL+", hotkey_with_modifier[.ProjectSave], ")", sep = "") button_config = {margin = button_margin, label = RAYGUI_ICON_SAVE, tooltip = button_tooltip} button_data^ = {type = .ProjectSave} ui.element(.Button, button_dimensions, button_config, button_data) button_data = new(UiElementData_Button, context.temp_allocator) button_config = {margin = button_margin, label = RAYGUI_ICON_SAVE_AS, tooltip = "Save project as..."} button_data^ = {type = .ProjectSaveAs} ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) button_data = new(UiElementData_Button, context.temp_allocator) button_tooltip = fmt.ctprint("Load project\n(CTRL+", hotkey_with_modifier[.ProjectLoad], ")", sep = "") button_config = {margin = button_margin, label = RAYGUI_ICON_LOAD, tooltip = button_tooltip} button_data^ = {type = .ProjectLoad} ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) button_data = new(UiElementData_Button, context.temp_allocator) button_config = {margin = button_margin, label = RAYGUI_ICON_NEW, tooltip = "New project"} button_data^ = {type = .ProjectNew} ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) ui.same_line() ui.element(.Divider, divider_dimensions, divider_config) button_data = new(UiElementData_Button, context.temp_allocator) button_tooltip = fmt.ctprint("Add identity\nmatrix (", hotkey_without_modifier[.AddIdentity], ")", sep = "") button_config = {margin = button_margin, label = "1", tooltip = button_tooltip} button_data^ = {type = .AddIdentity} ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) button_data = new(UiElementData_Button, context.temp_allocator) button_tooltip = fmt.ctprint("Add translation\nmatrix (", hotkey_without_modifier[.AddTranslation], ")", sep = "") button_config = {margin = button_margin, label = RAYGUI_ICON_TRANSLATION_MATRIX, tooltip = button_tooltip} button_data^ = {type = .AddTranslation} ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) button_data = new(UiElementData_Button, context.temp_allocator) button_tooltip = fmt.ctprint("Add scale\nmatrix (", hotkey_without_modifier[.AddScale], ")", sep = "") button_config = {margin = button_margin, label = RAYGUI_ICON_SCALE_MATRIX, tooltip = button_tooltip} button_data^ = {type = .AddScale} ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) button_data = new(UiElementData_Button, context.temp_allocator) button_tooltip = fmt.ctprint("Add rotation\nmatrix (", hotkey_without_modifier[.AddRotation], ")", sep = "") button_config = {margin = button_margin, label = RAYGUI_ICON_ROTATION_MATRIX, tooltip = button_tooltip} button_data^ = {type = .AddRotation} ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) button_data = new(UiElementData_Button, context.temp_allocator) button_tooltip = fmt.ctprint("Add rotation\nmatrix", sep = "") button_config = {margin = button_margin, label = RAYGUI_ICON_SKEW_MATRIX, tooltip = button_tooltip} button_data^ = {type = .AddSkewMatrix} ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) // TODO (synthas): Use raygui border theme color for consistency // TODO (synthas): Padding and margin doesn't work here for some reason ui.same_line() ui.element(.Divider, divider_dimensions, divider_config) play_button_label: cstring button_data = new(UiElementData_Button, context.temp_allocator) switch program.animation_state { case .Ready: button_config = {margin = button_margin, label = RAYGUI_ICON_PLAY, tooltip = "Play\nanimation"} button_data^ = {type = .StartTransformation} case .Paused: button_config = {margin = button_margin, label = RAYGUI_ICON_PLAY, tooltip = "Resume\nanimation"} button_data^ = {type = .ResumeTransformation} case .Playing: button_config = {margin = button_margin, label = RAYGUI_ICON_PAUSE, tooltip = "Pause\nanimation"} button_data^ = {type = .PauseTransformation} } ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) button_config = {margin = button_margin, label = RAYGUI_ICON_RESET, tooltip = "Reset\nanimation"} button_data = new(UiElementData_Button, context.temp_allocator) button_data^ = {type = .ResetTransformation} ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) button_config = {margin = button_margin, label = "1", tooltip = "Center\nanimation"} button_data = new(UiElementData_Button, context.temp_allocator) button_data^ = {type = .CenterView} ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) ui.same_line() ui.element(.Divider, divider_dimensions, divider_config) slider_config: ui.ElementConfiguration slider_data: ^UiElementData_Slider slider_dimensions := ui.Dimensions {top_bar_button_width * 5.0, ui.Size_Percentage(1.0)} slider_config = {margin = top_bar_element_margin * 2} slider_data = new(UiElementData_Slider, context.temp_allocator) slider_data^ = { value = &program.animation.matrix_index, max = f32(program.animation.matrix_count), min = 0.0 } ui.same_line() ui.element(.Slider, slider_dimensions, slider_config, slider_data) } ui.container_end() side_bar_container_config := ui.ElementConfiguration {margin = 4.0} side_bar_container_data := new(UiElementData_Container, context.temp_allocator) side_bar_container_data^ = {type = .Scroll, scroll = program.matrix_scroll} matrix_container = ui.container_begin({ui.Size_Pixels(320.0), ui.Size_PercentageRemainder(1.0)}, side_bar_container_config, side_bar_container_data) for i in 0.. MouseState { mouse := rl.GetMousePosition() mouse_wheel := rl.GetMouseWheelMoveV() world_to_grid_matrix := grid_world_to_grid_matrix(program.grid.zoom, program.grid.offset) grid_to_world_matrix := linalg.inverse(world_to_grid_matrix) mouse_state := mouse_state state_label: switch mouse_state { case .Hover: if rect_has_point(rect2(matrix_container.area), mouse) { mouse_state = .HoverMatrices break state_label } if rect_has_point(rect2(top_bar_container.area), mouse) { break state_label } if rl.IsMouseButtonPressed(.LEFT) { for draggable in program.mouse_draggables { switch draggable in draggable { case MouseDraggable_Point: circle_position := matrix3_transform_xy(grid_to_world_matrix, draggable.ref^) if linalg.distance(mouse, circle_position) <= program.mouse_draggable_radius { program.mouse_current_draggable = draggable mouse_state = .DraggingDraggable break state_label } case MouseDraggable_RectangleCornerData: circle_position := matrix3_transform_xy(grid_to_world_matrix, draggable.corner_ref^) if linalg.distance(mouse, circle_position) <= program.mouse_draggable_radius { program.mouse_current_draggable = draggable mouse_state = .DraggingDraggable break state_label } case MouseDraggable_RectangleData: corners_world := rectangle_corners_transform_mat3(grid_to_world_matrix, draggable.corners^) rect := rect_from(corners_world.tl, corners_world.tr, corners_world.br, corners_world.bl) if rect_has_point(rect, mouse) { program.mouse_current_draggable = draggable mouse_state = .DraggingDraggable break state_label } } } mouse_state = .DraggingGrid break state_label } screen_size := v2(program_screen_size()) grid_zoom_in_on(&program.grid, {0, 0, screen_size.x, screen_size.y}, rl.GetMousePosition(), mouse_wheel.y * 0.1 * program.grid_zoom_speed) case .DraggingDraggable: if !rl.IsMouseButtonDown(.LEFT) { mouse_state = .Hover break state_label } draggable := program.mouse_current_draggable switch draggable in draggable { case MouseDraggable_Point: draggable.ref^ = matrix3_transform_xy(world_to_grid_matrix, mouse) case MouseDraggable_RectangleCornerData: position := matrix3_transform_xy(world_to_grid_matrix, mouse) corners := rectangle_corners_move_corner(draggable.corners_ref^, position, draggable.corner) draggable.corners_ref^ = corners case MouseDraggable_RectangleData: mouse_delta_grid := rl.GetMouseDelta() * grid_get_zoom(program.grid.zoom) draggable.corners^ = rectangle_corners_add(draggable.corners^, mouse_delta_grid) } case .DraggingGrid: program.grid.offset -= rl.GetMouseDelta() * grid_get_zoom(program.grid.zoom) if !rl.IsMouseButtonDown(.LEFT) { mouse_state = .Hover break state_label } case .HoverMatrices: program.matrix_scroll = max(0, program.matrix_scroll - mouse_wheel.y * program.matrix_scroll_speed) if !rect_has_point(rect2(matrix_container.area), mouse) { mouse_state = .Hover break state_label } } return mouse_state } program_render_ui_and_handle_focus :: proc(ctx: ^ui.Context, focus_element_id: ui.ElementId, font_style: FontStyle, elements: []ui.Element) -> (focus: ui.ElementId) { tooltip_text: cstring tooltip_position: v2 scissor_container: ^ui.Element render_above: ui.Element render_above.id = ui.element_id_invalid() focus_element_id := focus_element_id for &element in elements { offset: v2 if scissor_container != nil { if element.id.container_index < scissor_container.id.container_index { rl.EndScissorMode() scissor_container = nil } else { offset.y = -(cast(^UiElementData_Container)scissor_container.data).scroll } } area := rect_translate(rl.Rectangle(element.area), offset) color := rl.Color(element.color) #partial switch element.type { case .Container: dim := rl.Color {0, 0, 0, 192} if element.data != nil { data := cast(^UiElementData_Container)element.data switch data.type { case .Normal: rl.DrawRectangleRounded(area, 0.1, 4.0, dim) rl.GuiGroupBox(area, element.label) case .Matrix: element_matrix_index := f32(data.matrix_index + 1) animation_matrix_index := animation_get_eased_progress(program.animation) matrix_animation_distance := element_matrix_index - animation_matrix_index alpha := max(0.0, 1.0 - abs(matrix_animation_distance)) color := rl.Color{128, 255, 128, u8(alpha * 64.0)} rl.DrawRectangleRec(area, color) matrix_area := ui.rect_grow(ui.rect2(area), element.margin / 2.0) tl, tr, br, bl := ui.rect_get_corners(matrix_area) col := rl.WHITE hook_length: f32 = 4.0 rl.DrawLineEx(tl, bl, data.thickness, col) rl.DrawLineEx(tl, tl + V2_RIGHT * hook_length, data.thickness, col) rl.DrawLineEx(bl, bl + V2_RIGHT * hook_length, data.thickness, col) rl.DrawLineEx(tr, br, data.thickness, col) rl.DrawLineEx(tr, tr + V2_LEFT * hook_length, data.thickness, col) rl.DrawLineEx(br, br + V2_LEFT * hook_length, data.thickness, col) case .Scroll: rl.DrawRectangleRounded(area, 0.1, 4.0, dim) rl.GuiGroupBox(area, element.label) scissor_container = &element rl.BeginScissorMode(**rect2_to_irect2(area)) } } else { rl.DrawRectangleRounded(area, 0.1, 4.0, dim) rl.GuiGroupBox(area, element.label) } case .Button: if len(element.tooltip) > 0 && rect_has_point(area, rl.GetMousePosition()) { tooltip_text = element.tooltip // TODO (synthas): implement keeping the rectangle within screen borders tooltip_position = rl.GetMousePosition() } if rl.GuiButton(area, element.label) { if element.data == nil do break data := cast(^UiElementData_Button)element.data switch data.type { case .Nothing: case .AddIdentity: program_add_identity_matrix() case .AddTranslation: program_show_dialog(&program, .CreateTranslationMatrix) case .AddScale: program_show_dialog(&program, .CreateScaleMatrix) case .AddRotation: program_show_dialog(&program, .CreateRotationMatrix) case .AddSkewMatrix: program_show_dialog(&program, .CreateSkewMatrix) case .AddTranslationMatrixConfirm: inputs := program.dialog_data.input_float_values program_add_translation_matrix(inputs[0], inputs[1], inputs[2]) program.state = .Normal case .AddScaleMatrixConfirm: inputs := program.dialog_data.input_float_values program_add_scalar_matrix(inputs[0], inputs[1], inputs[2]) program.state = .Normal case .AddRotationMatrixConfirm: program_add_rotation_matrix(program.dialog_data.input_float_values[0]) program.state = .Normal case .AddSkewMatrixConfirm: inputs := program.dialog_data.input_float_values program_add_skew_matrix(inputs[0], inputs[1]) case .ExitDialog: program.state = .Normal case .StartTransformation: if program.matrix_count > 0 { program.animation_state = .Playing program.animation.matrix_index = 0.0 } case .PauseTransformation: program.animation_state = .Paused case .ResumeTransformation: if animation_is_finished(program.animation) { program.animation.matrix_index = 0.0 } program.animation_state = .Playing case .ResetTransformation: program.animation.matrix_index = 0.0 case .ProjectSaveConfirm: path := strings.string_from_null_terminated_ptr(&program.dialog_data.input_text_buffers[0][0], DIALOG_STATE_TEXT_INPUT_FIELD_SIZE) if err := program_save_project(&program, path); err != nil { program_show_dialog(&program, .Error, fmt.caprint(typeid_of(type_of(err)), ".", err)) } else { program.state = .Normal } case .ProjectLoadConfirm: path := strings.string_from_null_terminated_ptr(&program.dialog_data.input_text_buffers[0][0], DIALOG_STATE_TEXT_INPUT_FIELD_SIZE) if err := program_load_project(&program, path); err != nil { msg := fmt.caprint("Tried loading but got error: '", typeid_of(type_of(err)), ".", err, "'", sep = "") program_show_dialog(&program, .Error, msg) } else { program.state = .Normal } case .MatrixDelete: if data.target_matrix + 1 < program.matrix_count { floats := program.matrix_float_pool inputs := program.matrix_value_string_pool amount := program.matrix_count - data.target_matrix matrix_values_size := size_of(f32) * 9 matrix_inputs_size := size_of(InputFloatField) * 9 mem.copy(&floats[data.target_matrix * 9], &floats[data.target_matrix * 9 + 9], matrix_values_size * amount) mem.copy(&inputs[data.target_matrix * 9][0], &inputs[data.target_matrix * 9 + 9][0], matrix_inputs_size * amount) } program.matrix_count = max(program.matrix_count - 1, 0) case .MatrixMoveUp: if data.target_matrix <= 0 do break; matrix_move_to(program.matrix_float_pool, program.matrix_value_string_pool, data.target_matrix, data.target_matrix - 1) case .MatrixMoveDown: if data.target_matrix + 1 >= program.matrix_count do break; matrix_move_to(program.matrix_float_pool, program.matrix_value_string_pool, data.target_matrix, data.target_matrix + 1) case .CenterView: // TODO (synthas): Could be improved to center the whole animation // TODO (synthas): Reset zoom program.grid.offset = animation_get_center(program.animation) - program_screen_size() * grid_get_zoom(program.grid.zoom) / 2.0 case .ProjectSave: program_save_project_from_project_string_or_open_dialog(&program) case .ProjectSaveAs: program_show_dialog(&program, .SaveProject) case .ProjectLoad: program_show_dialog(&program, .LoadProject) case .ProjectNew: case .ResetGrid: program.grid.offset = program_screen_size() / 2.0 program.grid.zoom = 1.0 program.grid.cell_size_px = 50.0 case .DismissError: program.state = .Normal } if data.type in UiButtonRecalculateFinishGroup { program.animation.matrix_count = program.matrix_count } } case .Slider: data := cast(^UiElementData_Slider)element.data rl.GuiSlider(area, element.label, "", data.value, data.min, data.max) case .InputFloat: data := cast(^UiElementData_FloatInput)element.data value_previous := data.value^ if rl.GuiValueBoxFloat(area, element.label, data.value_string, data.value, focus_element_id == element.id) == 1 { focus_element_id = element.id } if value_previous != data.value^ && .UpdateMatrix in data.tag { program.animation.matrix_count = program.matrix_count } case .InputText: data := cast(^UiElementData_TextInput)element.data if rl.GuiTextBox(area, data.str, i32(program.font_style.size), focus_element_id == element.id) { focus_element_id = element.id } case .Label: rl.DrawTextEx(font_style.font, element.label, rect_get_tl(area), font_style.size, font_style.spacing, color) case .Divider: rl.DrawRectangleRec(rect2(ui.get_element_outer_area(element)), color) case .ToggleGroup: data := cast(^UiElementData_ToggleGroup)element.data rl.GuiToggleGroup(area, element.label, data.value) case: rl.DrawRectangleRec(area, rl.MAGENTA) } } if scissor_container != nil { rl.EndScissorMode() scissor_container = nil } if len(tooltip_text) > 0 { position := tooltip_position + program.ui_tooltip_offset size := measure_text(tooltip_text, program.font_style) bounds := rect_grow(rect_from(position, size), 4.0) rl.DrawRectangleRec(bounds, rl.BLACK) rl.DrawRectangleLinesEx(bounds, 1.0, rl.WHITE) draw_multiline_text_centered(tooltip_text, position, program.font_style, rl.WHITE, context.temp_allocator) } return focus_element_id } ui_confirm_and_cancel_buttons :: proc(button_type_add: UiButtonType, margin: f32) { button_data: ^UiElementData_Button button_data = new(UiElementData_Button, context.temp_allocator) button_data^ = {type = button_type_add} ui.element(.Button, ui.dimensions_pixels(96.0, 32.0), {label = "Add", margin = margin}, button_data) button_data = new(UiElementData_Button, context.temp_allocator) button_data^ = {type = .ExitDialog} ui.same_line() ui.element(.Button, ui.dimensions_pixels(96.0, 32.0), {label = "Cancel", margin = margin}, button_data) } ui_measure_text :: proc(str: cstring, font_data: rawptr) -> ui.v2 { style := cast(^FontStyle)font_data return rl.MeasureTextEx(style.font, str, style.size, style.spacing) } ui_mat3 :: proc(idx: int, mat: []f32, value_strings: []InputFloatField, dimensions: ui.Dimensions, config: ui.ElementConfiguration, allocator := context.temp_allocator) { ui.container_begin(dimensions, config) defer ui.container_end() button_config := ui.ElementConfiguration {margin = 4.0} button_data: ^UiElementData_Button button_config.label = RAYGUI_ICON_TRASH button_data = new(UiElementData_Button, allocator) button_data^ = {target_matrix = idx, type = .MatrixDelete} ui.element(.Button, ui.dimensions_pixels(32.0), button_config, button_data) button_config.label = RAYGUI_ICON_UP button_data = new(UiElementData_Button, allocator) button_data^ = {target_matrix = idx, type = .MatrixMoveUp} ui.same_line() ui.element(.Button, ui.dimensions_pixels(32.0), button_config, button_data) button_config.label = RAYGUI_ICON_DOWN button_data = new(UiElementData_Button, allocator) button_data^ = {target_matrix = idx, type = .MatrixMoveDown} ui.same_line() ui.element(.Button, ui.dimensions_pixels(32.0), button_config, button_data) matrix_container_dimensions := ui.Dimensions {ui.Size_Percentage(1.0), ui.Size_PercentageRemainder(1.0)} matrix_container_config := ui.ElementConfiguration {margin = 4.0} matrix_container_data := new(UiElementData_Container, allocator) matrix_container_data^ = {type = .Matrix, thickness = 2.0, matrix_index = idx} ui.container_begin(matrix_container_dimensions, matrix_container_config, matrix_container_data) matrix_input_dimensions := ui.Dimensions {ui.Size_Percentage(1.0 / 3.0), ui.Size_Percentage(1.0 / 3.0)} matrix_input_config := ui.ElementConfiguration {margin = 4.0} #unroll for col in 0..<3 { #unroll for row in 0..<3 { data := new(UiElementData_FloatInput, allocator) index := row * 3 + col data^ = {value = &mat[index], value_string = cstring(&value_strings[index][0]), tag = {.UpdateMatrix}} ui.element(.InputFloat, matrix_input_dimensions, matrix_input_config, data) ui.same_line() } program.ui_context.same_line = false } ui.container_end() } ui_labeled_input_float_element :: proc(label: cstring, margin: f32, value: ^f32, value_string: ^InputFloatField, allocator := context.temp_allocator) -> ^ui.Element { label := fmt.ctprintf("%s: ", label) label_element := ui.element(.Label, {}, {label = label, color = ui.col(rl.WHITE), margin = margin}) dimensions := ui.Dimensions{ui.Size_Pixels(64.0), ui.dimensions_pixels(ui.rect_get_size(ui.get_element_outer_area(label_element^))).height} ui.same_line() input_data := new(UiElementData_FloatInput, context.temp_allocator) input_data^ = {value = value, value_string = cstring(&value_string[0])} return ui.element(.InputFloat, dimensions, {margin = margin}, input_data) } ui_labeled_input_text_element :: proc(label: cstring, margin: f32, str: []byte, allocator := context.temp_allocator) -> ^ui.Element { label := fmt.ctprintf("%s: ", label) label_element := ui.element(.Label, {}, {label = label, color = ui.col(rl.WHITE), margin = margin}) dimensions := ui.Dimensions{ui.Size_Pixels(64.0), ui.dimensions_pixels(ui.rect_get_size(ui.get_element_outer_area(label_element^))).height} ui.same_line() input_data := new(UiElementData_TextInput, context.temp_allocator) input_data^ = {str = cstring(&str[0])} return ui.element(.InputText, dimensions, {margin = margin}, input_data) }