Compare commits

...

3 Commits

9 changed files with 163 additions and 105 deletions

View File

@ -5,6 +5,7 @@ This is a work in progress program in unusable state.
# Short term design decisions
Subject to change.
* Efficiency, simplicity and clarity are key goals.
* Fully keyboard driven modal editing. Mouse support is secondary.
* GUI application made in [Zig](https://ziglang.org/) with
[raylib](https://www.raylib.com/).
@ -23,7 +24,7 @@ $ zig build run
Press escape to close the program.
# Default keybindings
Movement, hold Shift to expand selection:
Movement:
* `h`: left
* `j`: down
* `k`: up
@ -32,15 +33,21 @@ Movement, hold Shift to expand selection:
* `u`: up-right
* `b`: down-left
* `n`: down-right
* `<space>`: reset selection
Actions:
Verbs:
* `<space>`: clear selection
* `d`: delete selection
* `r`: replace selection
* `+`: increase scale (zoom)
* `-`: decrease scale (dezoom)
* `=`: reset scale
Modes:
* `<return>`: normal mode, default
* `i`: free selection mode
* `v`: rectangle selection mode
* `c`: camera mode [not implemented]
# Mouse control
Right click and left click are the same, except right click reset
selection before taking effect.

View File

@ -12,21 +12,23 @@ const scaling = @import("scaling.zig");
const verbs = @import("verbs.zig");
const Vec2 = @import("vec2.zig");
const Level = @import("level.zig");
const Mode = @import("modes.zig").Mode;
pub const ActionCat = enum {
none, // do nothing
movement, // move and change selection
verb, // do stuff with selection
scale, // change draw scaling
mode, // change mode
};
pub const Action = struct {
category: ActionCat,
toggle: bool = false, // bool that can be passed to actions, i.e. exclusive for movement
// Only one of these should be set, and only one should be used.
function_move: fn (*Vec2, conf.arg_type, bool) movement.SelectionUpdate = movement.move_left,
function_move: fn (*Vec2, conf.arg_type) movement.SelectionUpdate = movement.move_left,
function_verb: fn (*Level, conf.arg_type) void = verbs.delete,
function_scale: fn (scaling.scale_type) scaling.scale_type = scaling.scale_reset,
next_mode: Mode = Mode.normal,
};
pub const ActionsDef = .{
@ -36,92 +38,55 @@ pub const ActionsDef = .{
// Movement.
// Reset selection.
.move_but_dont = Action{
.toggle = true,
.category = ActionCat.movement,
.function_move = movement.move_but_dont,
},
// Left.
.move_left = Action{
.toggle = true,
.category = ActionCat.movement,
.function_move = movement.move_left,
},
.move_LEFT = Action{
.category = ActionCat.movement,
.function_move = movement.move_left,
},
// Right.
.move_right = Action{
.toggle = true,
.category = ActionCat.movement,
.function_move = movement.move_right,
},
.move_RIGHT = Action{
.category = ActionCat.movement,
.function_move = movement.move_right,
},
// Up.
.move_up = Action{
.toggle = true,
.category = ActionCat.movement,
.function_move = movement.move_up,
},
.move_UP = Action{
.category = ActionCat.movement,
.function_move = movement.move_up,
},
// Down.
.move_down = Action{
.toggle = true,
.category = ActionCat.movement,
.function_move = movement.move_down,
},
.move_DOWN = Action{
.category = ActionCat.movement,
.function_move = movement.move_down,
},
// Up-left.
.move_up_left = Action{
.toggle = true,
.category = ActionCat.movement,
.function_move = movement.move_up_left,
},
.move_UP_LEFT = Action{
.category = ActionCat.movement,
.function_move = movement.move_up_left,
},
// Up-right.
.move_up_right = Action{
.toggle = true,
.category = ActionCat.movement,
.function_move = movement.move_up_right,
},
.move_UP_RIGHT = Action{
.category = ActionCat.movement,
.function_move = movement.move_up_right,
},
// Down-left.
.move_down_left = Action{
.toggle = true,
.category = ActionCat.movement,
.function_move = movement.move_down_left,
},
.move_DOWN_LEFT = Action{
.category = ActionCat.movement,
.function_move = movement.move_down_left,
},
// Down-right.
.move_down_right = Action{
.toggle = true,
.category = ActionCat.movement,
.function_move = movement.move_down_right,
},
.move_DOWN_RIGHT = Action{
.category = ActionCat.movement,
.function_move = movement.move_down_right,
},
// Verbs.
.verb_clear_selection = Action{
.category = ActionCat.verb,
.function_verb = verbs.clear_selection,
},
.verb_delete = Action{
.category = ActionCat.verb,
.function_verb = verbs.delete,
@ -144,4 +109,22 @@ pub const ActionsDef = .{
.category = ActionCat.scale,
.function_scale = scaling.scale_down,
},
// Mode change.
.mode_normal = Action{
.category = ActionCat.mode,
.next_mode = Mode.normal,
},
.mode_select = Action{
.category = ActionCat.mode,
.next_mode = Mode.select,
},
.mode_rectangle = Action{
.category = ActionCat.mode,
.next_mode = Mode.rectangle,
},
.mode_camera = Action{
.category = ActionCat.mode,
.next_mode = Mode.camera,
},
};

35
src/draw.zig Normal file
View File

@ -0,0 +1,35 @@
// 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 = @cImport({
@cInclude("raylib.h");
});
const Vec2 = @import("vec2.zig");
const Rect = @import("rect.zig");
const scaling = @import("scaling.zig");
const Mode = @import("modes.zig").Mode;
/// Draw cursor.
pub fn cursor(scale: scaling.scale_type, offset: Vec2, pos: Vec2, mode: Mode) void {
const x = (pos.x - offset.y) * scale;
const y = (pos.y - offset.y) * scale;
const color: ray.Color = switch (mode) {
.normal => ray.GRAY,
.select => ray.BLUE,
.rectangle => ray.SKYBLUE,
.camera => ray.PURPLE,
};
ray.DrawRectangleLines(x, y, scale, scale, color);
}
/// Draw rectangle selection preview.
pub fn rectangle_selection(scale: scaling.scale_type, offset: Vec2, rect: Rect) void {
const x = (rect.left_x - offset.x) * scale;
const w = (rect.right_x - rect.left_x + 1) * scale;
const y = (rect.top_y - offset.y) * scale;
const h = (rect.bottom_y - rect.top_y + 1) * scale;
ray.DrawRectangleLines(x, y, w, h, ray.SKYBLUE);
}

View File

@ -42,7 +42,7 @@ pub fn init(allocator: *std.mem.Allocator, width: u16, height: u16) !Self {
// Fill with default value to avoid undefined behavior.
var i: u32 = 0;
while (i < size) : (i += 1) {
self.content[i] = 1;
self.content[i] = 0;
self.selection[i] = false;
}
return self;
@ -66,8 +66,8 @@ pub fn draw(self: *Self, scale: u16, offset: Vec2) void {
while (cy < self.height) {
const cell_content: cell_type = self.content[cy * self.width + cx];
const color = switch (cell_content) {
0 => ray.BLACK,
1 => ray.GRAY,
0 => ray.Color{ .r = 26, .g = 26, .b = 26, .a = 255 },
1 => ray.Color{ .r = 144, .g = 144, .b = 144, .a = 255 },
else => ray.PURPLE, //unknown
};
ray.DrawRectangle(x + 1, y + 1, scale - 2, scale - 2, color);
@ -135,12 +135,6 @@ pub fn select_rect(self: *Self, rect: Rect, state: bool) void {
/// Apply selection update to selection *kof*.
pub fn apply_selection_update(self: *Self, selection_update: SelectionUpdate) void {
// The update is exclusive, forget everything before it.
if (selection_update.exclusive) {
// Clear selection.
self.select_clear(false);
}
// Apply changes.
self.select_rect(selection_update.area, selection_update.state);
}

View File

@ -15,15 +15,18 @@ 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 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;
pub fn main() !void {
pub fn main() void {
// Create allocator
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
@ -38,49 +41,53 @@ pub fn main() !void {
ray.SetTargetFPS(60);
// Create level.
var level: Level = try Level.init(allocator, 16, 16);
var level: Level = Level.init(allocator, 16, 16) catch unreachable;
defer level.deinit(allocator);
// Create camera.
var camera: Vec2 = Vec2.init(0, 0);
// 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 = Mouse.init();
var mouse: Mouse = comptime Mouse.init();
// Create cursor.
var cursor: Vec2 = Vec2.init(0, 0);
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;
// Create binding "table".
var bindings = [_]*const actions.Action{&actions.ActionsDef.none} ** char_range;
// Set default bindings.
// Movement.
bindings[' '] = &ActionsDef.move_but_dont;
bindings['h'] = &ActionsDef.move_left;
bindings['H'] = &ActionsDef.move_LEFT;
bindings['j'] = &ActionsDef.move_down;
bindings['J'] = &ActionsDef.move_DOWN;
bindings['k'] = &ActionsDef.move_up;
bindings['K'] = &ActionsDef.move_UP;
bindings['l'] = &ActionsDef.move_right;
bindings['L'] = &ActionsDef.move_RIGHT;
bindings['y'] = &ActionsDef.move_up_left;
bindings['Y'] = &ActionsDef.move_UP_LEFT;
bindings['u'] = &ActionsDef.move_up_right;
bindings['U'] = &ActionsDef.move_UP_RIGHT;
bindings['b'] = &ActionsDef.move_down_left;
bindings['B'] = &ActionsDef.move_DOWN_LEFT;
bindings['n'] = &ActionsDef.move_down_right;
bindings['N'] = &ActionsDef.move_DOWN_RIGHT;
// Verbs.
bindings[' '] = &ActionsDef.verb_clear_selection;
bindings['d'] = &ActionsDef.verb_delete;
bindings['r'] = &ActionsDef.verb_replace;
// Scale.
bindings['='] = &ActionsDef.scale_reset;
bindings['+'] = &ActionsDef.scale_up;
bindings['-'] = &ActionsDef.scale_down;
// Mode.
bindings['\n'] = &ActionsDef.mode_normal;
bindings['i'] = &ActionsDef.mode_select;
bindings['v'] = &ActionsDef.mode_rectangle;
bindings['c'] = &ActionsDef.mode_camera;
// Create input buffer.
const input_buffer_len = 255;
@ -95,6 +102,7 @@ pub fn main() !void {
while (!ray.WindowShouldClose()) {
{
// TODO: apply DRY
// Get keyboard input.
var key = ray.GetCharPressed();
// Check if more characters have been pressed.
@ -108,6 +116,14 @@ pub fn main() !void {
key = ray.GetCharPressed();
}
// Check for special keys, not detected by GetCharPressed.
if (ray.IsKeyDown(ray.KEY_ENTER)) {
input_buffer[input_cursor] = '\n';
input_cursor += 1;
// Avoid writing out of memory.
if (input_cursor >= input_buffer_len)
input_cursor = input_buffer_len - 1;
}
}
// Process buffer content.
@ -120,13 +136,14 @@ pub fn main() !void {
0;
const action: actions.Action = bindings[key].*;
const apply_selection: bool = (mode == Mode.select);
switch (action.category) {
.none => std.log.info("No action bound to {}.", .{key}),
.movement => {
const selection_update: movement.SelectionUpdate =
action.function_move(&cursor, 1, action.toggle);
if (selection_update.active) {
action.function_move(&cursor, 1);
if (apply_selection and selection_update.active) {
level.apply_selection_update(selection_update);
}
},
@ -136,6 +153,15 @@ pub fn main() !void {
.scale => {
scale = action.function_scale(scale);
},
.mode => {
// Rectangle selection!
if (mode == Mode.rectangle) {
const selection = Rect.init_from_vec2(cursor, cursor_before);
level.select_rect(selection, true);
}
cursor_before = cursor; // Save position before selection.
mode = action.next_mode;
},
}
}
@ -176,7 +202,7 @@ pub fn main() !void {
cursor = mouse.pos;
// Reset selection on right click.
if (right_click) {
level.apply_selection_update(movement.move_but_dont(&cursor, 1, true));
verbs.clear_selection(&level, 1);
}
// Set position.
mouse.start_pos = mouse.pos;
@ -201,6 +227,9 @@ pub fn main() !void {
ray.ClearBackground(ray.BLACK);
level.draw(scale, camera);
level.draw_selection(scale, camera);
draw.cursor(scale, camera, cursor, mode);
if (mode == Mode.rectangle)
draw.rectangle_selection(scale, camera, Rect.init_from_vec2(cursor, cursor_before));
mouse.draw(scale, camera);
//ray.DrawFPS(0, 0);
}

12
src/modes.zig Normal file
View File

@ -0,0 +1,12 @@
// 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.
/// Different cursor modes (for keyboard).
pub const Mode = enum {
normal, // move cursor and do stuff
select, // same, but select while doing so
rectangle, // same, but select as a rectangle
camera, // move camera instead
};

View File

@ -12,11 +12,10 @@ const std = @import("std");
const Vec2 = @import("vec2.zig");
const Rect = @import("rect.zig");
const scaling = @import("scaling.zig");
const draw_fn = @import("draw.zig");
const Self = @This();
const preview_color = ray.SKYBLUE;
pub const MouseMode = enum {
wait, // Skip one turn.
idle,
@ -43,15 +42,11 @@ pub fn draw(self: *Self, scale: scaling.scale_type, offset: Vec2) void {
MouseMode.sel => {
const x = (self.pos.x - offset.y) * scale;
const y = (self.pos.y - offset.y) * scale;
ray.DrawRectangleLines(x, y, scale, scale, preview_color);
ray.DrawRectangleLines(x, y, scale, scale, ray.SKYBLUE);
},
MouseMode.rect_sel => {
const rect = Rect.init_from_vec2(self.pos, self.start_pos);
const x = (rect.left_x - offset.x) * scale;
const w = (rect.right_x - rect.left_x + 1) * scale;
const y = (rect.top_y - offset.y) * scale;
const h = (rect.bottom_y - rect.top_y + 1) * scale;
ray.DrawRectangleLines(x, y, w, h, preview_color);
draw_fn.rectangle_selection(scale, offset, rect);
},
else => {},
}

View File

@ -17,13 +17,12 @@ const maxIntVec2 = comptime maxInt(Vec2.int_type);
/// Describe changes to make on selection.
pub const SelectionUpdate = struct {
active: bool,
exclusive: bool,
area: Rect,
state: bool,
};
/// Universal move system, prefer direction wrappers.
fn move(cursor: *Vec2, arg: conf.arg_type, exclusive_selection: bool, dx: i2, dy: i2) SelectionUpdate {
fn move(cursor: *Vec2, arg: conf.arg_type, dx: i2, dy: i2) SelectionUpdate {
const before: Vec2 = cursor.*;
if (dx > 0) {
@ -51,58 +50,54 @@ fn move(cursor: *Vec2, arg: conf.arg_type, exclusive_selection: bool, dx: i2, dy
return SelectionUpdate{
.active = true,
.exclusive = exclusive_selection,
.area = if (exclusive_selection)
Rect.init_from_vec2(cursor.*, cursor.*)
else
Rect.init_from_vec2(before, cursor.*),
.area = Rect.init_from_vec2(before, cursor.*),
.state = true,
};
}
/// Just don't move.
pub fn move_but_dont(cursor: *Vec2, arg: conf.arg_type, exclusive_selection: bool) SelectionUpdate {
return move(cursor, arg, exclusive_selection, 0, 0);
pub fn move_but_dont(cursor: *Vec2, arg: conf.arg_type) SelectionUpdate {
return move(cursor, arg, 0, 0);
}
/// Try to move the cursor `n` times left.
pub fn move_left(cursor: *Vec2, arg: conf.arg_type, exclusive_selection: bool) SelectionUpdate {
return move(cursor, arg, exclusive_selection, -1, 0);
pub fn move_left(cursor: *Vec2, arg: conf.arg_type) SelectionUpdate {
return move(cursor, arg, -1, 0);
}
/// Try to move the cursor `n` times right.
pub fn move_right(cursor: *Vec2, arg: conf.arg_type, exclusive_selection: bool) SelectionUpdate {
return move(cursor, arg, exclusive_selection, 1, 0);
pub fn move_right(cursor: *Vec2, arg: conf.arg_type) SelectionUpdate {
return move(cursor, arg, 1, 0);
}
/// Try to move the cursor `n` times up.
pub fn move_up(cursor: *Vec2, arg: conf.arg_type, exclusive_selection: bool) SelectionUpdate {
return move(cursor, arg, exclusive_selection, 0, -1);
pub fn move_up(cursor: *Vec2, arg: conf.arg_type) SelectionUpdate {
return move(cursor, arg, 0, -1);
}
/// Try to move the cursor `n` times down.
pub fn move_down(cursor: *Vec2, arg: conf.arg_type, exclusive_selection: bool) SelectionUpdate {
return move(cursor, arg, exclusive_selection, 0, 1);
pub fn move_down(cursor: *Vec2, arg: conf.arg_type) SelectionUpdate {
return move(cursor, arg, 0, 1);
}
/// Try to move the cursor `n` times up and left.
pub fn move_up_left(cursor: *Vec2, arg: conf.arg_type, exclusive_selection: bool) SelectionUpdate {
return move(cursor, arg, exclusive_selection, -1, -1);
pub fn move_up_left(cursor: *Vec2, arg: conf.arg_type) SelectionUpdate {
return move(cursor, arg, -1, -1);
}
/// Try to move the cursor `n` times up and right.
pub fn move_up_right(cursor: *Vec2, arg: conf.arg_type, exclusive_selection: bool) SelectionUpdate {
return move(cursor, arg, exclusive_selection, 1, -1);
pub fn move_up_right(cursor: *Vec2, arg: conf.arg_type) SelectionUpdate {
return move(cursor, arg, 1, -1);
}
/// Try to move the cursor `n` times down and left.
pub fn move_down_left(cursor: *Vec2, arg: conf.arg_type, exclusive_selection: bool) SelectionUpdate {
return move(cursor, arg, exclusive_selection, -1, 1);
pub fn move_down_left(cursor: *Vec2, arg: conf.arg_type) SelectionUpdate {
return move(cursor, arg, -1, 1);
}
/// Try to move the cursor `n` times down and right.
pub fn move_down_right(cursor: *Vec2, arg: conf.arg_type, exclusive_selection: bool) SelectionUpdate {
return move(cursor, arg, exclusive_selection, 1, 1);
pub fn move_down_right(cursor: *Vec2, arg: conf.arg_type) SelectionUpdate {
return move(cursor, arg, 1, 1);
}
test "move left" {

View File

@ -7,6 +7,14 @@
const conf = @import("conf.zig");
const Level = @import("level.zig");
/// Clear selection (deselect everything).
pub fn clear_selection(level: *Level, arg: conf.arg_type) void {
var i: u32 = 0;
while (i < level.width * level.height) : (i += 1) {
level.selection[i] = false;
}
}
/// Delete selected cells (set to 0).
pub fn delete(level: *Level, arg: conf.arg_type) void {
var i: u32 = 0;