diff --git a/TODO.MD b/TODO.MD index 916277e..ea31f9c 100644 --- a/TODO.MD +++ b/TODO.MD @@ -1,12 +1,9 @@ -- Grid line changing -- Zoom sensitivity - Playback slider - Save grid and animation data to file -- Load project - Fix focus on submenu - Centered justification in UI - Get a more legible theme + consistency - Fix delay on control points -- Fix build - Error handling (dialog with message) - Make web build +- Zoom sensitivity diff --git a/program.odin b/program.odin index 261901e..fa6d0bb 100644 --- a/program.odin +++ b/program.odin @@ -20,7 +20,8 @@ TRANSFORMATION_RECTANGLE_COLOR_MIDDLE :: rl.MAGENTA TRANSFORMATION_RECTANGLE_COLOR_NEXT :: rl.GRAY TRANSFORMATION_RECTANGLE_COLOR_LAST :: rl.BLUE -DEFAULT_PROJECT_LOAD_LOCATION :: "default" + +@rodata default_project_load_location := "default" UiContainerType :: enum { Normal, @@ -56,6 +57,13 @@ UiElementData_Dropdown :: struct { UiButtonType :: enum { Nothing, + ProjectSave, + ProjectSaveAs, + ProjectLoad, + ProjectNew, + ProjectSaveConfirm, + ProjectLoadConfirm, + DismissError, AddIdentity, AddTranslation, AddScale, @@ -68,7 +76,6 @@ UiButtonType :: enum { PauseTransformation, ResumeTransformation, ResetTransformation, - SaveProjectConfirm, MatrixDelete, MatrixMoveUp, MatrixMoveDown, @@ -145,6 +152,7 @@ ProgramDialogType :: enum { CreateRotationMatrix, SaveProject, LoadProject, + Error, } @rodata program_dialog_messages := [ProgramDialogType]cstring { @@ -153,6 +161,7 @@ ProgramDialogType :: enum { .CreateRotationMatrix = "Create rotation matrix", .SaveProject = "Save project", .LoadProject = "Load project", + .Error = "Error", } ProgramMessageType :: enum { @@ -226,6 +235,7 @@ Program :: struct { mouse_draggables: [dynamic; 64]MouseDraggable, mouse_current_draggable: MouseDraggable, mouse_draggable_radius: f32, + project_save_string: [DIALOG_STATE_TEXT_INPUT_FIELD_COUNT]byte, grid: Grid, grid_zoom_x_only: bool, grid_zoom_y_only: bool, @@ -235,6 +245,7 @@ Program :: struct { message_type: ProgramMessageType, dialog_type: ProgramDialogType, dialog_data: ProgramData_DialogState, + dialog_message: cstring, animation: Animation, animation_state: ProgramAnimationState, animation_speed: f32, @@ -259,20 +270,35 @@ matrix_pool_get_mat3 :: proc() -> (value_strings: []InputFloatField, values: []f return value_strings, values } -program_save_project :: proc(program: ^Program, path: string) -> os.Error { +program_save_project :: proc(program: ^Program, path: string) -> ProjectWriteError { project := ProjectData { matrix_count = u32(program.matrix_count), matrices = program.matrix_float_pool, } - project_write(&project, string(program.dialog_data.input_text_buffers[0][:])) or_return + project_write(&project, path) or_return return nil } +program_save_project_from_project_string_or_open_dialog :: proc(program: ^Program) { + path: string + project_save_string := strings.string_from_null_terminated_ptr(&program.project_save_string[0], DIALOG_STATE_TEXT_INPUT_FIELD_SIZE) + if len(project_save_string) > 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_read(&project, path, context.temp_allocator) or_return + project.matrices = program.matrix_float_pool + project_read(&project, path) or_return program.matrix_count = int(project.matrix_count) - mem.copy(raw_data(program.matrix_float_pool), raw_data(project.matrices), len(project.matrices) * size_of(f32) * 9) for val, i in program.matrix_float_pool[:program.matrix_count * 9] { fmt.bprint(program.matrix_value_string_pool[i][:], val) } @@ -280,11 +306,12 @@ program_load_project :: proc(program: ^Program, path: string) -> ProjectReadErro return nil } -program_show_dialog :: proc(program: ^Program, dialog_type: ProgramDialogType) { +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 } program_init :: proc(allocator := context.allocator) { @@ -313,8 +340,10 @@ program_init :: proc(allocator := context.allocator) { animation_init(&program.animation, program.matrix_float_pool) - if err := program_load_project(&program, DEFAULT_PROJECT_LOAD_LOCATION); err != nil { - fmt.println("ERROR:", err) + if err := program_load_project(&program, default_project_load_location); err != nil { + program_show_dialog(&program, .Error, fmt.caprint(typeid_of(type_of(err)), ".", err)) + } else { + mem.copy(&program.project_save_string[0], raw_data(default_project_load_location), len(default_project_load_location)) } } @@ -359,8 +388,9 @@ program_update :: proc() { if rl.IsKeyPressed(hotkey) { switch index { case .Save: - program_show_dialog(&program, .SaveProject) + program_save_project_from_project_string_or_open_dialog(&program) case .Open: + program_show_dialog(&program, .LoadProject) } } } @@ -370,16 +400,12 @@ program_update :: proc() { #partial switch index { case .AddIdentity: program_add_identity_matrix() - program.ui_overlay_queue_focus_first = true case .AddTranslation: program_show_dialog(&program, .CreateTranslationMatrix) - program.ui_overlay_queue_focus_first = true case .AddScale: program_show_dialog(&program, .CreateScaleMatrix) - program.ui_overlay_queue_focus_first = true case .AddRotation: program_show_dialog(&program, .CreateRotationMatrix) - program.ui_overlay_queue_focus_first = true case .ZoomXOnly: if program.grid_zoom_x_only { program.grid_zoom_x_only = false @@ -413,14 +439,22 @@ program_update :: proc() { case .CreateScaleMatrix: program_add_scalar_matrix(vals[0], vals[1], vals[2]) case .CreateRotationMatrix: program_add_rotation_matrix(vals[0]) case .SaveProject: - input := &program.dialog_data.input_text_buffers[0][0] - path := strings.string_from_null_terminated_ptr(input, DIALOG_STATE_TEXT_INPUT_FIELD_SIZE) + 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 { // TODO (Synthas): show error message + } 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 { + // TODO (Synthas): show error message } else { program.state = .Normal } - case .LoadProject: + case .Error: + program.state = .Normal } program.animation.matrix_count = program.matrix_count program.state = .Normal @@ -539,7 +573,18 @@ program_build_dialog_ui :: proc(ctx: ^ui.Context, dialog_type: ProgramDialogType ui.start(ctx, V2_ZERO, program_screen_size()) ui.container_begin(ui.dimensions_auto(), {margin = container_margin, padding = 8.0}) - ui.element(.Label, {}, {label = program_dialog_messages[program.dialog_type], color = ui.col(rl.WHITE), margin = element_margin}) + 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]) @@ -551,14 +596,21 @@ program_build_dialog_ui :: proc(ctx: ^ui.Context, dialog_type: ProgramDialogType } else { button_type = .AddScaleMatrixConfirm } - ui_add_matrix_footer_buttons(button_type, element_margin) + 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]) - ui_add_matrix_footer_buttons(.AddRotationMatrixConfirm, element_margin) + ui_confirm_and_cancel_buttons(.AddRotationMatrixConfirm, element_margin) case .SaveProject: ui_labeled_input_text_element("Path", 4.0, dialog_data.input_text_buffers[0][:]) - ui_add_matrix_footer_buttons(.SaveProjectConfirm, element_margin) + 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() @@ -584,10 +636,39 @@ program_build_workspace_ui :: proc(ctx: ^ui.Context) -> button_config: ui.ElementConfiguration 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_config = {margin = button_margin, label = "S", tooltip = "Save project\n(ctrl+s)"} + 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 = "SA", 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_config = {margin = button_margin, label = "L", tooltip = "Load project\n(ctrl+o)"} + 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 = "N", 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_config = {margin = button_margin, label = "I", tooltip = "Add identity\nmatrix"} button_data^ = {type = .AddIdentity} + ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) button_data = new(UiElementData_Button, context.temp_allocator) @@ -609,10 +690,8 @@ program_build_workspace_ui :: proc(ctx: ^ui.Context) -> ui.same_line() ui.element(.Button, button_dimensions, button_config, button_data) - divider_dimensions := ui.Dimensions{ui.Size_Pixels(1.0), ui.Size_Percentage(1.0)} // TODO (synthas): Use raygui border theme color for consistency // TODO (synthas): Padding and margin doesn't work here for some reason - divider_config := ui.ElementConfiguration{padding = 4.0, color = ui.col(rl.WHITE)} ui.same_line() ui.element(.Divider, divider_dimensions, divider_config) @@ -859,10 +938,16 @@ program_render_ui_and_handle_focus :: proc(ctx: ^ui.Context, focus_element_id: u program.animation_state = .Playing case .ResetTransformation: program.animation.matrix_index = 0.0 - case .SaveProjectConfirm: - input := &program.dialog_data.input_text_buffers[0][0] - path := strings.string_from_null_terminated_ptr(input, DIALOG_STATE_TEXT_INPUT_FIELD_SIZE) + 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 { // TODO (Synthas): show error message } else { program.state = .Normal @@ -891,6 +976,15 @@ program_render_ui_and_handle_focus :: proc(ctx: ^ui.Context, focus_element_id: u grid_to_world_matrix := linalg.inverse(world_to_grid_matrix) center := animation_get_center(program.animation) / grid_get_zoom(program.grid.zoom) - program.grid.world_size / 2.0 program.grid.offset = center + 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 .DismissError: + program.state = .Normal } if data.type in UiButtonRecalculateFinishGroup { program.animation.matrix_count = program.matrix_count @@ -987,7 +1081,7 @@ matrix_move_to :: proc(floats: []f32, inputs: []InputFloatField, from, to: int) mem.copy(inputs_moved_to_matrix, &inputs_temp[0][0], inputs_buffer_size) } -ui_add_matrix_footer_buttons :: proc(button_type_add: UiButtonType, margin: f32) { +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} diff --git a/project.odin b/project.odin index 46386be..3356ee2 100644 --- a/project.odin +++ b/project.odin @@ -17,27 +17,28 @@ ProjectData :: struct { version: u32, matrix_count: u32, matrices: []f32, + grid: Grid, + animation: Animation, } -WriteProjectError :: union { +ProjectWriteError :: union { os.Error, + io.Error, } -WriteProjectErrorType :: enum { - IncompatibleVersion, -} - -project_write :: proc(project: ^ProjectData, path: string) -> os.Error { +project_write :: proc(project: ^ProjectData, path: string) -> ProjectWriteError { f := os.open(path, {.Create, .Trunc, .Write}) or_return defer os.close(f) w := io.to_writer(os.to_stream(f)) version: u32 = PROJECT_DATA_VERSION header := PROJECT_DATA_HEADER - io.write_full(w, transmute([]u8)header) - io.write_ptr(w, &version, size_of(version)) - io.write_ptr(w, &project.matrix_count, size_of(project.matrix_count)) - io.write_ptr(w, raw_data(project.matrices), int(project.matrix_count) * 9 * size_of(f32)) + io.write_full(w, transmute([]u8)header) or_return + io.write_ptr(w, &version, size_of(version)) or_return + io.write_ptr(w, &project.matrix_count, size_of(project.matrix_count)) or_return + n := io.write_ptr(w, raw_data(project.matrices), int(project.matrix_count) * 9 * size_of(f32)) or_return + fmt.println(n) + // io.write_ptr(w, &project.grid, size_of(Grid)) os.close(f) return nil } @@ -54,7 +55,9 @@ ProjectReadErrorType :: enum { VersionNeedsUpgrade, } -project_read :: proc(project: ^ProjectData, path: string, allocator: mem.Allocator) -> ProjectReadError { +import "core:fmt" + +project_read :: proc(project: ^ProjectData, path: string) -> ProjectReadError { f := os.open(path, {.Read}) or_return defer os.close(f) r := io.to_reader(os.to_stream(f)) @@ -71,7 +74,9 @@ project_read :: proc(project: ^ProjectData, path: string, allocator: mem.Allocat return ProjectReadErrorType.VersionNeedsUpgrade } _ = io.read_ptr(r, &project.matrix_count, size_of(project.matrix_count)) or_return - project.matrices = make([]f32, size_of(f32) * int(project.matrix_count) * 9) - _ = io.read_ptr(r, raw_data(project.matrices), size_of(f32) * len(project.matrices)) or_return + n := io.read_ptr(r, raw_data(project.matrices), size_of(f32) * len(project.matrices)) or_return + fmt.println(n) + // _ = io.read_ptr(r, &project.grid, size_of(Grid)) or_return + // _ = io.read_ptr(r, &project.animation, size_of(Animation)) or_return return nil }