// SPDX-License-Identifier: MIT // Copyright (c) 2021 KikooDX // This file is part of [KBLE](https://sr.ht/~kikoodx/kble), which is // MIT licensed. The MIT license requires this copyright notice to be // included in all copies and substantial portions of the software. const ray = @import("raylib.zig"); const std = @import("std"); const maxInt = std.math.maxInt; const log = std.log.info; const conf = @import("conf.zig"); const Level = @import("level.zig"); const Vec2 = @import("vec2.zig"); const Rect = @import("rect.zig"); const Mouse = @import("mouse.zig"); const draw = @import("draw.zig"); const Parameter = @import("parameter.zig"); const movement = @import("movement.zig"); const verbs = @import("verbs.zig"); const scaling = @import("scaling.zig"); const actions = @import("actions.zig"); const ActionsDef = actions.ActionsDef; const ActionCat = actions.ActionCat; const Mode = @import("modes.zig").Mode; const char_range = 255; const input_buffer_len = 255; pub fn main() void { const level_path: [*:0]const u8 = if (std.os.argv.len > 1) std.os.argv[1] else nopath: { std.log.notice("No path provided, defaults to \"level.kble\".", .{}); break :nopath "level.kble"; }; // Create allocator var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = &arena.allocator; // Create window. ray.SetConfigFlags(ray.FLAG_WINDOW_RESIZABLE); ray.InitWindow(640, 480, "KBLE"); defer ray.CloseWindow(); // Limit FPS for performance. ray.SetTargetFPS(60); // Try to load level, is doesn't exist create it. var level: Level = Level.init_read(allocator, level_path) catch create: { break :create Level.init(allocator, conf.default_grid_size.width, conf.default_grid_size.height) catch unreachable; }; defer level.deinit(allocator); // Create camera. // TODO: Can't move camera, implement camera mode. var camera: Vec2 = comptime Vec2.init(0, 0); // Init scale, used by drawing code. var scale: scaling.scale_type = scaling.scale_default; // Create mouse. var mouse: Mouse = comptime Mouse.init(); // Create cursor. var cursor: Vec2 = comptime Vec2.init(0, 0); // Used by rectangle selection mode. var cursor_before: Vec2 = comptime Vec2.init(0, 0); // Set default mode. var mode: Mode = Mode.normal; // Parameter buffer. var parameter: Parameter = Parameter{}; // Create binding "table". var bindings = [_]*const actions.Action{&actions.ActionsDef.none} ** char_range; comptime { // Set default bindings. var default_bindings = [_]*const actions.Action{&actions.ActionsDef.none} ** char_range; comptime { var i: u8 = 0; while (i < 10) : (i += 1) { default_bindings['0' + i] = &ActionsDef.parameter; } } // Movement. default_bindings['h'] = &ActionsDef.move_left; default_bindings['j'] = &ActionsDef.move_down; default_bindings['k'] = &ActionsDef.move_up; default_bindings['l'] = &ActionsDef.move_right; default_bindings['y'] = &ActionsDef.move_up_left; default_bindings['u'] = &ActionsDef.move_up_right; default_bindings['b'] = &ActionsDef.move_down_left; default_bindings['n'] = &ActionsDef.move_down_right; // Verbs. default_bindings[' '] = &ActionsDef.verb_clear_selection; default_bindings['d'] = &ActionsDef.verb_delete; default_bindings['r'] = &ActionsDef.verb_replace; // Scale. default_bindings['='] = &ActionsDef.scale_reset; default_bindings['+'] = &ActionsDef.scale_up; default_bindings['-'] = &ActionsDef.scale_down; // File. default_bindings['e'] = &ActionsDef.file_read; default_bindings['w'] = &ActionsDef.file_write; // Mode. default_bindings['\n'] = &ActionsDef.mode_normal; default_bindings['i'] = &ActionsDef.mode_select; default_bindings['v'] = &ActionsDef.mode_rectangle; default_bindings['I'] = &ActionsDef.mode_unselect; default_bindings['V'] = &ActionsDef.mode_unrectangle; default_bindings['c'] = &ActionsDef.mode_camera; // Map user bindings. comptime { var i: u8 = 0; while (i < char_range) : (i += 1) { bindings[i] = default_bindings[conf.bind_key(i)]; } } } // Create input buffer. var input_buffer: [input_buffer_len]u32 = undefined; comptime { var i: u8 = 0; while (i < input_buffer_len) : (i += 1) { input_buffer[i] = 0; } } var input_cursor: u8 = 0; while (!ray.WindowShouldClose()) { { // Get keyboard input. var key = ray.GetCharPressed(); // Check if more characters have been pressed. while (key != 0) { add_key_to_buffer(&input_buffer, &input_cursor, key); key = ray.GetCharPressed(); } // Check for special keys, not detected by GetCharPressed. if (ray.IsKeyPressed(ray.KEY_ENTER)) add_key_to_buffer(&input_buffer, &input_cursor, '\n'); if (ray.IsKeyPressed(ray.KEY_TAB)) add_key_to_buffer(&input_buffer, &input_cursor, '\t'); } // Process buffer content. // Read the buffer backwards. This is placeholder logic. while (input_cursor > 0) { input_cursor -= 1; const key: u8 = if (input_buffer[input_cursor] <= char_range) @intCast(u8, input_buffer[input_cursor]) else 0; // TODO: move everything from this to a function (for config and macros support). const action: actions.Action = bindings[key].*; const apply_selection: bool = (mode == .select); switch (action.category) { .none => std.log.info("No action bound to {}.", .{key}), .parameter => { action.function_parameter(¶meter, key); }, .movement => { const selection_update: movement.SelectionUpdate = action.function_move(&cursor, parameter.pop(1)); if (apply_selection and selection_update.active) { level.apply_selection_update(selection_update); } }, .verb => { action.function_verb(&level, parameter.pop(1)); }, .scale => { scale = action.function_scale(scale, parameter.pop(1)); }, .file => { action.function_file(&level, allocator, level_path); }, .mode => { // Rectangle selection! if (mode == .rectangle or mode == .unrectangle) { const selection = Rect.init_from_vec2(cursor, cursor_before); level.select_rect(selection, mode == .rectangle); } cursor_before = cursor; // Save position before selection. mode = action.next_mode; if (mode == .select) // Select first tile. level.select_cell(cursor, true); }, } // TODO end } // Mouse operations. if (conf.mouse_enabled) { // Update position. { // Set mouse scaling. ray.SetMouseScale(1.0 / @intToFloat(f32, scale), 1.0 / @intToFloat(f32, scale)); mouse.pos.x = @intCast(Vec2.int_type, ray.GetMouseX()) + camera.x; mouse.pos.y = @intCast(Vec2.int_type, ray.GetMouseY()) + camera.y; } const left_click: bool = ray.IsMouseButtonPressed(conf.mouse_left_btn); const left_release: bool = ray.IsMouseButtonReleased(conf.mouse_left_btn); const right_click: bool = ray.IsMouseButtonPressed(conf.mouse_right_btn); const right_release: bool = ray.IsMouseButtonReleased(conf.mouse_right_btn); const click: bool = left_click or right_click; const release: bool = left_release or right_release; const end_sel_event = if (conf.mouse_graphic_tablet) release else click; // When end selection event, get out of current mode and apply changes if necessary. if (end_sel_event and mouse.mode != Mouse.MouseMode.idle) { defer mouse.mode = .wait; // Select area. if (mouse.mode == .rect_sel or mouse.mode == .unrect_sel) { const selection = Rect.init_from_vec2(mouse.start_pos, mouse.pos); level.select_rect(selection, mouse.mode == .rect_sel); } } // State machine. switch (mouse.mode) { .wait => mouse.mode = Mouse.MouseMode.idle, // See if can switch mode. .idle => { const mod_rect_sel: bool = ray.IsKeyDown(ray.KEY_LEFT_SHIFT) or ray.IsKeyDown(ray.KEY_RIGHT_SHIFT); if (click) { // Switch mode. cursor = mouse.pos; // Set position. mouse.start_pos = mouse.pos; // If right click then un mouse.mode = if (right_click and mod_rect_sel) Mouse.MouseMode.unrect_sel else if (right_click and !mod_rect_sel) Mouse.MouseMode.unsel else if (mod_rect_sel) Mouse.MouseMode.rect_sel else Mouse.MouseMode.sel; } }, // Select stuff under the cursor. .sel, .unsel => { cursor = mouse.pos; level.select_cell(mouse.pos, mouse.mode == .sel); }, else => {}, } } ray.BeginDrawing(); defer ray.EndDrawing(); ray.ClearBackground(conf.theme.background); level.draw(scale, camera); level.draw_selection(scale, camera); draw.cursor(scale, camera, cursor, mode); if (mode == Mode.rectangle or mode == Mode.unrectangle) draw.rectangle_selection(scale, camera, Rect.init_from_vec2(cursor, cursor_before), mode == .rectangle); if (conf.mouse_enabled) mouse.draw(scale, camera); } } fn add_key_to_buffer(input_buffer: *[input_buffer_len]u32, input_cursor: *u8, key: c_int) void { input_buffer[input_cursor.*] = @intCast(u32, key); input_cursor.* += 1; // Avoid writing out of memory. if (input_cursor.* >= input_buffer_len) input_cursor.* = input_buffer_len - 1; }