Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
45d5bc4d31 | |||
23aab8414b | |||
e30773870f | |||
8263ccad82 | |||
88252a151b | |||
0bff9feb35 | |||
082a77a5eb | |||
0c14bd6d32 |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
|
||||
/.zig-cache/
|
||||
/zig-out/
|
70
build.zig
Normal file
70
build.zig
Normal file
@ -0,0 +1,70 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// ---- deps
|
||||
const dvui_dep = b.dependency("dvui", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.backend = .sdl,
|
||||
.sdl3 = true,
|
||||
});
|
||||
|
||||
// ---- logic library
|
||||
const lib_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/hncore/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const lib = b.addLibrary(.{
|
||||
.linkage = .static,
|
||||
.name = "hncore",
|
||||
.root_module = lib_mod,
|
||||
});
|
||||
b.installArtifact(lib);
|
||||
|
||||
// ---- executable
|
||||
const exe_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/heavens-net/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe_mod.addImport("hncore", lib_mod);
|
||||
exe_mod.addImport("dvui", dvui_dep.module("dvui_sdl"));
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "heavens-net",
|
||||
.root_module = exe_mod,
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
// ---- running
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// ---- testing
|
||||
const lib_unit_tests = b.addTest(.{
|
||||
.root_module = lib_mod,
|
||||
});
|
||||
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_lib_unit_tests.step);
|
||||
|
||||
// ---- document generation
|
||||
const install_docs = b.addInstallDirectory(.{
|
||||
.source_dir = lib.getEmittedDocs(),
|
||||
.install_dir = .prefix,
|
||||
.install_subdir = "docs",
|
||||
});
|
||||
const docs_step = b.step("docs", "Install docs into zig-out/docs");
|
||||
docs_step.dependOn(&install_docs.step);
|
||||
}
|
19
build.zig.zon
Normal file
19
build.zig.zon
Normal file
@ -0,0 +1,19 @@
|
||||
.{
|
||||
.name = .heavens_net,
|
||||
.version = "0.0.1",
|
||||
.fingerprint = 0x5cbe403baf740fcb,
|
||||
.minimum_zig_version = "0.15.0-dev.149+2b57f6b71",
|
||||
|
||||
.dependencies = .{
|
||||
.dvui = .{
|
||||
.url = "https://github.com/david-vanderson/dvui/archive/9f446c8600b3385418c8926481076c335261f222.zip",
|
||||
.hash = "dvui-0.2.0-AQFJmesqywBKCM6r9orR324EGhZvGsjhJw0ZHWcvwzyh",
|
||||
},
|
||||
},
|
||||
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
43
src/heavens-net/App.zig
Normal file
43
src/heavens-net/App.zig
Normal file
@ -0,0 +1,43 @@
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
const hncore = @import("hncore");
|
||||
|
||||
const compo = @import("./compo/root.zig");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
stage: compo.stage.Mock,
|
||||
search: compo.search.Mock,
|
||||
taskedit: compo.taskedit.Mock,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !Self {
|
||||
return Self {
|
||||
.search = try compo.search.Mock.init(alloc),
|
||||
.stage = try compo.stage.Mock.init(alloc),
|
||||
.taskedit = try compo.taskedit.Mock.init(alloc),
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.taskedit.deinit();
|
||||
self.stage.deinit();
|
||||
self.search.deinit();
|
||||
}
|
||||
|
||||
pub fn gui(self: *Self) !void {
|
||||
try compo.menu.gui(.{});
|
||||
|
||||
// background
|
||||
{
|
||||
var box = try dvui.box(@src(), .vertical, .{
|
||||
.expand = .both,
|
||||
.background = true,
|
||||
.color_fill = .{ .color = dvui.Color.white, },
|
||||
});
|
||||
defer box.deinit();
|
||||
}
|
||||
|
||||
// ---- windows
|
||||
try compo.search.gui(&self.search);
|
||||
try compo.stage.gui(&self.stage);
|
||||
try compo.taskedit.gui(&self.taskedit);
|
||||
}
|
36
src/heavens-net/compo/menu.zig
Normal file
36
src/heavens-net/compo/menu.zig
Normal file
@ -0,0 +1,36 @@
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
|
||||
pub fn gui(ctx: anytype) !void {
|
||||
_ = ctx;
|
||||
|
||||
var root = try dvui.menu(@src(), .horizontal, .{ .background = true, .expand = .horizontal });
|
||||
defer root.deinit();
|
||||
|
||||
if (try dvui.menuItemLabel(@src(), "Workspace", .{ .submenu = true }, .{})) |r| {
|
||||
var float = try dvui.floatingMenu(
|
||||
@src(), .{.from = dvui.Rect.fromPoint(dvui.Point{ .x = r.x, .y = r.y + r.h })}, .{});
|
||||
defer float.deinit();
|
||||
|
||||
if (try dvui.menuItemLabel(@src(), "New", .{}, .{})) |_| {
|
||||
}
|
||||
if (try dvui.menuItemLabel(@src(), "Open", .{}, .{})) |_| {
|
||||
}
|
||||
|
||||
try dvui.separator(@src(), .{});
|
||||
|
||||
if (try dvui.menuItemLabel(@src(), "Save", .{}, .{})) |_| {
|
||||
}
|
||||
if (try dvui.menuItemLabel(@src(), "Save as", .{}, .{})) |_| {
|
||||
}
|
||||
}
|
||||
|
||||
if (try dvui.menuItemLabel(@src(), "Task", .{ .submenu = true }, .{})) |r| {
|
||||
var float = try dvui.floatingMenu(
|
||||
@src(), .{.from = dvui.Rect.fromPoint(dvui.Point{ .x = r.x, .y = r.y + r.h })}, .{});
|
||||
defer float.deinit();
|
||||
|
||||
if (try dvui.menuItemLabel(@src(), "New", .{}, .{})) |_| {
|
||||
}
|
||||
}
|
||||
}
|
5
src/heavens-net/compo/root.zig
Normal file
5
src/heavens-net/compo/root.zig
Normal file
@ -0,0 +1,5 @@
|
||||
pub const menu = @import("./menu.zig");
|
||||
pub const search = @import("./search.zig");
|
||||
pub const stage = @import("./stage.zig");
|
||||
pub const taskcard = @import("./taskcard.zig");
|
||||
pub const taskedit = @import("./taskedit.zig");
|
119
src/heavens-net/compo/search.zig
Normal file
119
src/heavens-net/compo/search.zig
Normal file
@ -0,0 +1,119 @@
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
|
||||
const ui = @import("../ui/root.zig");
|
||||
const compo = @import("./root.zig");
|
||||
|
||||
pub const Task = struct {
|
||||
id: usize,
|
||||
summary: [:0]const u8,
|
||||
|
||||
done : bool = false,
|
||||
staged : bool = false,
|
||||
archived: bool = false,
|
||||
};
|
||||
|
||||
pub fn gui(ctx: anytype) !void {
|
||||
var win = try dvui.floatingWindow(@src(), .{}, .{});
|
||||
defer win.deinit();
|
||||
|
||||
try dvui.windowHeader("Search", "", null);
|
||||
|
||||
// task adder
|
||||
{
|
||||
var adder = try dvui.textEntry(@src(), .{}, .{
|
||||
.expand = .horizontal,
|
||||
});
|
||||
defer adder.deinit();
|
||||
|
||||
if (dvui.focusedWidgetId() == adder.data().id) {
|
||||
if (ui.event.keyPress("activate")) {
|
||||
std.debug.print("hello\n", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// task list
|
||||
{
|
||||
var scrollArea = try dvui.scrollArea(@src(), .{}, .{
|
||||
.expand = .both,
|
||||
});
|
||||
defer scrollArea.deinit();
|
||||
|
||||
var list = try dvui.box(@src(), .vertical, .{
|
||||
.expand = .both,
|
||||
.padding = dvui.Rect.all(4),
|
||||
});
|
||||
defer list.deinit();
|
||||
|
||||
for (0.., ctx.tasks()) |idx, task| {
|
||||
if (idx > 0) {
|
||||
_ = try dvui.spacer(@src(), .{ .w = 0, .h = 4}, .{
|
||||
.id_extra = idx,
|
||||
.expand = .horizontal,
|
||||
});
|
||||
}
|
||||
|
||||
var subctx = TaskCardCtx(@TypeOf(ctx)) { .ctx = ctx, .task = task };
|
||||
try compo.taskcard.gui(@src(), &subctx);
|
||||
}
|
||||
|
||||
try dvui.labelNoFmt(@src(), "no tasks anymore :)", .{ .gravity_x = 0.5 });
|
||||
}
|
||||
}
|
||||
|
||||
fn TaskCardCtx(T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
ctx : T,
|
||||
task: Task,
|
||||
|
||||
pub fn open(self: *Self) void {
|
||||
_ = self;
|
||||
std.debug.print("OPEN\n", .{});
|
||||
}
|
||||
|
||||
pub fn id(self: *const Self) usize {
|
||||
return self.task.id;
|
||||
}
|
||||
pub fn summary(self: *const Self) []const u8 {
|
||||
return self.task.summary;
|
||||
}
|
||||
pub fn done(self: *const Self) bool {
|
||||
return self.task.done;
|
||||
}
|
||||
pub fn archived(self: *const Self) bool {
|
||||
return self.task.archived;
|
||||
}
|
||||
pub fn staged(self: *const Self) bool {
|
||||
return self.task.staged;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const Mock = struct {
|
||||
const Self = @This();
|
||||
const TaskList = std.ArrayList(Task);
|
||||
|
||||
_tasks: TaskList,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !Self {
|
||||
var ts = TaskList.init(alloc);
|
||||
errdefer ts.deinit();
|
||||
|
||||
try ts.append(Task { .id = 0, .summary = "helloworld", });
|
||||
try ts.append(Task { .id = 1, .summary = "goodbye", });
|
||||
|
||||
return Mock {
|
||||
._tasks = ts,
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *Self) void {
|
||||
self._tasks.deinit();
|
||||
}
|
||||
|
||||
fn tasks(self: *const Self) []const Task {
|
||||
return self._tasks.items;
|
||||
}
|
||||
};
|
124
src/heavens-net/compo/stage.zig
Normal file
124
src/heavens-net/compo/stage.zig
Normal file
@ -0,0 +1,124 @@
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
|
||||
const compo = @import("./root.zig");
|
||||
|
||||
pub const Task = struct {
|
||||
id: usize,
|
||||
summary: [:0]const u8,
|
||||
done: bool,
|
||||
};
|
||||
|
||||
pub fn gui(ctx: anytype) !void {
|
||||
var win = try dvui.floatingWindow(@src(), .{}, .{});
|
||||
defer win.deinit();
|
||||
|
||||
try dvui.windowHeader("Stage", "", null);
|
||||
|
||||
var scrollArea = try dvui.scrollArea(@src(), .{}, .{
|
||||
.expand = .both,
|
||||
});
|
||||
defer scrollArea.deinit();
|
||||
|
||||
var reorder = try dvui.reorder(@src(), .{
|
||||
.background = true,
|
||||
.expand = .both,
|
||||
});
|
||||
defer reorder.deinit();
|
||||
|
||||
var list = try dvui.box(@src(), .vertical, .{
|
||||
.expand = .both,
|
||||
.padding = dvui.Rect.all(4),
|
||||
});
|
||||
defer list.deinit();
|
||||
|
||||
const tasks = ctx.get_tasks();
|
||||
for (0.., tasks) |idx, task| {
|
||||
if (idx > 0) {
|
||||
_ = try dvui.spacer(@src(), .{ .w = 0, .h = 4}, .{
|
||||
.id_extra = idx,
|
||||
.expand = .horizontal,
|
||||
});
|
||||
}
|
||||
|
||||
var reorderable = try reorder.reorderable(@src(), .{
|
||||
.draw_target = false,
|
||||
}, .{
|
||||
.id_extra = idx,
|
||||
.expand = .horizontal,
|
||||
});
|
||||
defer reorderable.deinit();
|
||||
|
||||
var subctx = TaskCardCtx(@TypeOf(ctx)) {
|
||||
.ctx = ctx,
|
||||
.task = task,
|
||||
.reorderable = reorderable,
|
||||
};
|
||||
try compo.taskcard.gui(@src(), &subctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn TaskCardCtx(T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
ctx : T,
|
||||
task: Task,
|
||||
reorderable: *dvui.Reorderable,
|
||||
|
||||
pub fn open(self: *Self) void {
|
||||
_ = self;
|
||||
std.debug.print("OPEN\n", .{});
|
||||
}
|
||||
pub fn startDragging(self: *Self, p: dvui.Point) void {
|
||||
self.reorderable.reorder.dragStart(self.reorderable.data().id, p);
|
||||
}
|
||||
|
||||
pub fn id(self: *const Self) usize {
|
||||
return self.task.id;
|
||||
}
|
||||
pub fn summary(self: *const Self) []const u8 {
|
||||
return self.task.summary;
|
||||
}
|
||||
pub fn done(self: *const Self) bool {
|
||||
return self.task.done;
|
||||
}
|
||||
pub fn staged(_: *const Self) bool {
|
||||
return true;
|
||||
}
|
||||
pub fn archived(_: *const Self) bool {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const Mock = struct {
|
||||
const Self = @This();
|
||||
const TaskList = std.ArrayList(Task);
|
||||
|
||||
tasks: TaskList,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !Self {
|
||||
var tasks = TaskList.init(alloc);
|
||||
errdefer tasks.deinit();
|
||||
|
||||
for (0..100) |id| {
|
||||
try tasks.append(Task { .id = id*2, .summary = "helloworld", .done = true, });
|
||||
try tasks.append(Task { .id = id*2+1, .summary = "goodbye", .done = false, });
|
||||
}
|
||||
|
||||
return Mock {
|
||||
.tasks = tasks,
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.tasks.deinit();
|
||||
}
|
||||
|
||||
pub fn mark(self: *Self, idx: usize, check: bool) void {
|
||||
self.tasks.items[idx].done = check;
|
||||
}
|
||||
pub fn get_tasks(self: *const Self) []const Task {
|
||||
return self.tasks.items;
|
||||
}
|
||||
};
|
185
src/heavens-net/compo/taskcard.zig
Normal file
185
src/heavens-net/compo/taskcard.zig
Normal file
@ -0,0 +1,185 @@
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
|
||||
const ui = @import("../ui/root.zig");
|
||||
|
||||
pub fn gui(src: std.builtin.SourceLocation, ctx: anytype) !void {
|
||||
var card = try dvui.box(src, .horizontal, .{
|
||||
.id_extra = ctx.id(),
|
||||
.expand = .horizontal,
|
||||
.background = true,
|
||||
.border = dvui.Rect.all(1),
|
||||
});
|
||||
defer card.deinit();
|
||||
|
||||
{
|
||||
var check = ctx.done();
|
||||
const changed = try dvui.checkbox(@src(), &check, null, .{
|
||||
.gravity_y = 0.5,
|
||||
.gravity_x = 0,
|
||||
});
|
||||
if (changed) {
|
||||
std.debug.print("CHANGED\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
const hover = true;
|
||||
if (hover) {
|
||||
var icons = try dvui.box(@src(), .horizontal, .{
|
||||
.expand = .vertical,
|
||||
.gravity_x = 1,
|
||||
});
|
||||
defer icons.deinit();
|
||||
|
||||
if (ctx.staged()) {
|
||||
if (try buttonIcon(@src(), "unstage", dvui.entypo.light_down)) {
|
||||
std.debug.print("UNSTAGE\n", .{});
|
||||
}
|
||||
} else {
|
||||
if (try buttonIcon(@src(), "stage", dvui.entypo.light_up)) {
|
||||
std.debug.print("STAGE\n", .{});
|
||||
}
|
||||
}
|
||||
if (ctx.archived()) {
|
||||
if (try buttonIcon(@src(), "unarchive", dvui.entypo.back_in_time)) {
|
||||
std.debug.print("UNARCHIVE\n", .{});
|
||||
}
|
||||
} else {
|
||||
if (try buttonIcon(@src(), "archive", dvui.entypo.archive)) {
|
||||
std.debug.print("ARCHIVE\n", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rc = try title(ctx, card.data().rectScale().r.topLeft());
|
||||
|
||||
// dragging link
|
||||
if (!@hasDecl(@TypeOf(ctx.*), "startDragging")) {
|
||||
// WIP
|
||||
// dvui.pathStroke([_]dvui.Point{rc.topLeft(), cw.});
|
||||
}
|
||||
}
|
||||
|
||||
fn buttonIcon(src: std.builtin.SourceLocation, name: []const u8, icon: []const u8) !bool {
|
||||
return try dvui.buttonIcon(src, name, icon, .{}, .{
|
||||
.margin = dvui.Rect.all(0),
|
||||
.padding = dvui.Rect.all(4),
|
||||
.gravity_y = 0.5,
|
||||
});
|
||||
}
|
||||
|
||||
fn title(ctx: anytype, topLeft: dvui.Point) !dvui.Rect {
|
||||
// this codes are based on dvui.labelClick
|
||||
|
||||
var lw = dvui.LabelWidget.init(@src(), "#{d} {s}", .{ctx.id(), ctx.summary()}, .{
|
||||
.expand = .horizontal,
|
||||
.gravity_x = 0,
|
||||
.name = "LabelClick"
|
||||
});
|
||||
// now lw has a Rect from its parent but hasn't processed events or drawn
|
||||
|
||||
const lwid = lw.data().id;
|
||||
|
||||
// if lw is visible, we want to be able to keyboard navigate to it
|
||||
if (lw.data().visible()) {
|
||||
try dvui.tabIndexSet(lwid, lw.data().options.tab_index);
|
||||
}
|
||||
|
||||
// draw border and background
|
||||
try lw.install();
|
||||
|
||||
// loop over all events this frame in order of arrival
|
||||
for (dvui.events()) |*e| {
|
||||
|
||||
// skip if lw would not normally process this event
|
||||
if (!lw.matchEvent(e))
|
||||
continue;
|
||||
|
||||
switch (e.evt) {
|
||||
.mouse => |me| {
|
||||
if (me.action == .focus) {
|
||||
e.handled = true;
|
||||
|
||||
// focus this widget for events after this one (starting with e.num)
|
||||
dvui.focusWidget(lwid, null, e.num);
|
||||
} else if (me.action == .press and me.button.pointer()) {
|
||||
e.handled = true;
|
||||
dvui.captureMouse(lwid);
|
||||
|
||||
// for touch events, we want to cancel our click if a drag is started
|
||||
dvui.dragPreStart(me.p, .{ .name = "TASK", .offset = topLeft.diff(me.p), });
|
||||
} else if (me.action == .release and me.button.pointer()) {
|
||||
// mouse button was released, do we still have mouse capture?
|
||||
if (dvui.captured(lwid)) {
|
||||
e.handled = true;
|
||||
|
||||
// cancel our capture
|
||||
dvui.captureMouse(null);
|
||||
dvui.dragEnd();
|
||||
|
||||
// if the release was within our border, the click is successful
|
||||
if (lw.data().borderRectScale().r.contains(me.p)) {
|
||||
ctx.open();
|
||||
|
||||
// if the user interacts successfully with a
|
||||
// widget, it usually means part of the GUI is
|
||||
// changing, so the convention is to call refresh
|
||||
// so the user doesn't have to remember
|
||||
dvui.refresh(null, @src(), lwid);
|
||||
}
|
||||
}
|
||||
} else if (me.action == .motion) {
|
||||
if (me.button.touch()) {
|
||||
if (dvui.captured(lwid)) {
|
||||
if (dvui.dragging(me.p)) |_| {
|
||||
// touch: if we overcame the drag threshold, then
|
||||
// that means the person probably didn't want to
|
||||
// touch this button, they were trying to scroll
|
||||
dvui.captureMouse(null);
|
||||
dvui.dragEnd();
|
||||
}
|
||||
}
|
||||
} else if (@hasDecl(@TypeOf(ctx.*), "startDragging")) {
|
||||
// call ctx.startDragging if the method exists
|
||||
if (dvui.captured(lwid)) {
|
||||
e.handled = true;
|
||||
if (dvui.dragging(me.p)) |_| {
|
||||
ctx.startDragging(me.p);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (me.action == .position) {
|
||||
// a single .position mouse event is at the end of each
|
||||
// frame, so this means the mouse ended above us
|
||||
dvui.cursorSet(.hand);
|
||||
}
|
||||
},
|
||||
.key => |ke| {
|
||||
if (ke.action == .down and ke.matchBind("activate")) {
|
||||
e.handled = true;
|
||||
ctx.open();
|
||||
dvui.refresh(null, @src(), lwid);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
// if we didn't handle this event, send it to lw - this means we don't
|
||||
// need to call lw.processEvents()
|
||||
if (!e.handled) {
|
||||
lw.processEvent(e, false);
|
||||
}
|
||||
}
|
||||
|
||||
// draw text
|
||||
try lw.draw();
|
||||
|
||||
// draw an accent border if we are focused
|
||||
if (lwid == dvui.focusedWidgetId()) {
|
||||
try lw.data().focusBorder();
|
||||
}
|
||||
|
||||
// done with lw, have it report min size to parent
|
||||
defer lw.deinit();
|
||||
return lw.data().scaleRect().r;
|
||||
}
|
158
src/heavens-net/compo/taskedit.zig
Normal file
158
src/heavens-net/compo/taskedit.zig
Normal file
@ -0,0 +1,158 @@
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
|
||||
const ui = @import("../ui/root.zig");
|
||||
|
||||
pub const Tab = enum {
|
||||
info,
|
||||
detail,
|
||||
network,
|
||||
gantt,
|
||||
};
|
||||
|
||||
pub fn gui(ctx: anytype) !void {
|
||||
var win = try dvui.floatingWindow(@src(), .{}, .{});
|
||||
defer win.deinit();
|
||||
|
||||
// window header
|
||||
{
|
||||
var buf: [128]u8 = undefined;
|
||||
const subtitle = std.fmt.bufPrint(&buf, "#{d}", .{ctx.taskId()}) catch unreachable;
|
||||
try dvui.windowHeader("Task", subtitle, null);
|
||||
}
|
||||
|
||||
// task summary
|
||||
{
|
||||
var hbox = try dvui.box(@src(), .horizontal, .{ .expand = .horizontal, });
|
||||
defer hbox.deinit();
|
||||
|
||||
var mark: bool = false;
|
||||
if (try dvui.checkbox(@src(), &mark, null, .{ .gravity_y = 0.5, })) {
|
||||
}
|
||||
|
||||
var summary = try dvui.textEntry(@src(), .{}, .{
|
||||
.expand = .horizontal,
|
||||
});
|
||||
defer summary.deinit();
|
||||
}
|
||||
|
||||
// tabs
|
||||
{
|
||||
const shown = ctx.shownTab();
|
||||
|
||||
var tbox = try dvui.box(@src(), .vertical, .{ .expand = .both, });
|
||||
defer tbox.deinit();
|
||||
|
||||
{
|
||||
var tabs = dvui.TabsWidget.init(@src(), .{ .dir = .horizontal }, .{
|
||||
.expand = .horizontal,
|
||||
});
|
||||
try tabs.install();
|
||||
defer tabs.deinit();
|
||||
|
||||
inline for (std.meta.fields(Tab)) |tabMeta| {
|
||||
const tab = @field(Tab, tabMeta.name);
|
||||
if (try tabs.addTabLabel(shown == tab, tabMeta.name)) {
|
||||
ctx.switchTab(tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var vbox = try dvui.box(@src(), .vertical, .{ .expand = .both, });
|
||||
defer vbox.deinit();
|
||||
|
||||
switch (shown) {
|
||||
.info => try tabInfo(ctx),
|
||||
.detail => try tabDetail(ctx),
|
||||
else => try dvui.labelNoFmt(@src(), "NOT IMPLEMENTED YET :(", .{
|
||||
.gravity_x = 0.5,
|
||||
.gravity_y = 0.5,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tabInfo(ctx: anytype) !void {
|
||||
_ = ctx;
|
||||
|
||||
var vbox = try dvui.box(@src(), .vertical, .{ .expand = .both, });
|
||||
defer vbox.deinit();
|
||||
|
||||
const fields = [_][:0]const u8 {"foo", "baz", "bar"};
|
||||
for (0.., fields) |idx, name| {
|
||||
var hbox = try dvui.box(@src(), .horizontal, .{
|
||||
.id_extra = idx,
|
||||
.expand = .horizontal,
|
||||
});
|
||||
defer hbox.deinit();
|
||||
|
||||
{
|
||||
var box = try dvui.box(@src(), .horizontal, .{
|
||||
.min_size_content = .{ .w = 64, .h = 0, },
|
||||
});
|
||||
defer box.deinit();
|
||||
try dvui.label(@src(), "{s}", .{name}, .{
|
||||
.gravity_x = 1,
|
||||
});
|
||||
}
|
||||
{
|
||||
var box = try dvui.box(@src(), .horizontal, .{
|
||||
.expand = .horizontal,
|
||||
});
|
||||
defer box.deinit();
|
||||
try dvui.labelNoFmt(@src(), "this is a field value", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tabDetail(ctx: anytype) !void {
|
||||
_ = ctx;
|
||||
// const task = ctx.task();
|
||||
|
||||
var vbox = try dvui.box(@src(), .vertical, .{ .expand = .both, });
|
||||
defer vbox.deinit();
|
||||
|
||||
// detail
|
||||
{
|
||||
var detail = try dvui.textEntry(@src(), .{
|
||||
.break_lines = true,
|
||||
.scroll_vertical = true,
|
||||
.scroll_horizontal = true,
|
||||
.multiline = true,
|
||||
}, .{
|
||||
.expand = .both,
|
||||
});
|
||||
defer detail.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
pub const Mock = struct {
|
||||
const Self = @This();
|
||||
|
||||
_taskId: usize,
|
||||
_summary: []const u8,
|
||||
|
||||
_shownTab: Tab,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !Self {
|
||||
_ = alloc;
|
||||
return Mock {
|
||||
._taskId = 0,
|
||||
._summary = "hello",
|
||||
._shownTab = .info,
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *Self) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
fn switchTab(self: *Self, tab: Tab) void {
|
||||
self._shownTab = tab;
|
||||
}
|
||||
|
||||
fn taskId(self: *const Self) usize { return self._taskId; }
|
||||
fn summary(self: *const Self) []const u8 { return self._summary; }
|
||||
|
||||
fn shownTab(self: *const Self) Tab { return self._shownTab; }
|
||||
};
|
||||
|
49
src/heavens-net/main.zig
Normal file
49
src/heavens-net/main.zig
Normal file
@ -0,0 +1,49 @@
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
const hncore = @import("hncore");
|
||||
|
||||
const App = @import("./App.zig");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer if (gpa.deinit() == .leak) {
|
||||
@panic("memory leak detected");
|
||||
};
|
||||
|
||||
var backend = try dvui.backend.initWindow(.{
|
||||
.allocator = gpa.allocator(),
|
||||
.size = .{ .w = 800, .h = 600, },
|
||||
.min_size = .{ .w = 250, .h = 350, },
|
||||
.vsync = true,
|
||||
.title = "Heaven's Net",
|
||||
});
|
||||
defer backend.deinit();
|
||||
|
||||
var win = try dvui.Window.init(@src(), gpa.allocator(), backend.backend(), .{});
|
||||
defer win.deinit();
|
||||
|
||||
var app = try App.init(gpa.allocator());
|
||||
defer app.deinit();
|
||||
while (true) {
|
||||
try win.begin(
|
||||
win.beginWait(backend.hasEvent()),
|
||||
);
|
||||
|
||||
const quit = try backend.addAllEvents(&win);
|
||||
if (quit) {
|
||||
break;
|
||||
}
|
||||
|
||||
try app.gui();
|
||||
|
||||
const end_micros = try win.end(.{});
|
||||
|
||||
backend.setCursor(win.cursorRequested());
|
||||
backend.textInputRect(win.textInputRequested());
|
||||
backend.renderPresent();
|
||||
|
||||
backend.waitEventTimeout(
|
||||
win.waitTime(end_micros, null),
|
||||
);
|
||||
}
|
||||
}
|
30
src/heavens-net/ui/event.zig
Normal file
30
src/heavens-net/ui/event.zig
Normal file
@ -0,0 +1,30 @@
|
||||
const dvui = @import("dvui");
|
||||
|
||||
pub fn key(bind: []const u8) ?*dvui.Event {
|
||||
for (dvui.events()) |*e| {
|
||||
if (e.evt == .key) {
|
||||
if (e.evt.key.matchBind(bind)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
pub fn keyDown(bind: []const u8) bool {
|
||||
return if (key(bind)) |e| e.evt.key.action == .down else false;
|
||||
}
|
||||
pub fn keyPress(bind: []const u8) bool {
|
||||
return if (key(bind)) |e| (e.evt.key.action == .down or e.evt.key.action == .repeat) else false;
|
||||
}
|
||||
pub fn keyUp(bind: []const u8) bool {
|
||||
return if (key(bind)) |e| e.evt.key.action == .up else false;
|
||||
}
|
||||
|
||||
pub fn mouse(action: dvui.Event.Mouse.Action, button: dvui.enums.Button) ?*dvui.Event {
|
||||
for (dvui.events()) |*e| {
|
||||
if (e.evt == .mouse and e.evt.mouse.action == action and e.evt.mouse.button == button) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
1
src/heavens-net/ui/root.zig
Normal file
1
src/heavens-net/ui/root.zig
Normal file
@ -0,0 +1 @@
|
||||
pub const event = @import("./event.zig");
|
134
src/hncore/CommandHistory.zig
Normal file
134
src/hncore/CommandHistory.zig
Normal file
@ -0,0 +1,134 @@
|
||||
const std = @import("std");
|
||||
|
||||
const CommandIF = @import("./command/root.zig").Interface;
|
||||
|
||||
///
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
const CommandList = std.ArrayList(CommandIF);
|
||||
|
||||
///
|
||||
commands: CommandList,
|
||||
|
||||
/// next command to be applied by redoing
|
||||
head: usize,
|
||||
|
||||
///
|
||||
pub fn init(alloc: std.mem.Allocator) Self {
|
||||
return Self {
|
||||
.commands = CommandList.init(alloc),
|
||||
.head = 0,
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
pub fn deinit(self: *Self) void {
|
||||
for (self.commands.items) |*command| {
|
||||
command.deinit();
|
||||
}
|
||||
self.commands.deinit();
|
||||
}
|
||||
|
||||
///
|
||||
pub fn exec(self: *Self, command: CommandIF) !void {
|
||||
if (self.head >= self.commands.items.len) {
|
||||
try self.commands.append(command);
|
||||
} else {
|
||||
try self.commands.resize(self.head + 1);
|
||||
self.commands.items[self.head] = command;
|
||||
}
|
||||
errdefer _ = self.commands.pop();
|
||||
|
||||
self.head = self.commands.items.len;
|
||||
try command.apply();
|
||||
}
|
||||
|
||||
///
|
||||
pub fn undo(self: *Self) !void {
|
||||
if (self.head == 0) {
|
||||
return error.NoCommand;
|
||||
}
|
||||
self.head -= 1;
|
||||
errdefer self.head += 1;
|
||||
|
||||
try self.commands.items[self.head].revert();
|
||||
}
|
||||
|
||||
///
|
||||
pub fn redo(self: *Self) !void {
|
||||
if (self.head >= self.commands.items.len) {
|
||||
return error.NoCommand;
|
||||
}
|
||||
try self.commands.items[self.head].apply();
|
||||
self.head += 1;
|
||||
}
|
||||
|
||||
///
|
||||
pub fn isUndoAvailable(self: *const Self) bool {
|
||||
return self.head > 0;
|
||||
}
|
||||
///
|
||||
pub fn isRedoAvailable(self: *const Self) bool {
|
||||
return self.head < self.commands.items.len;
|
||||
}
|
||||
|
||||
test {
|
||||
var value1: i32 = 0;
|
||||
var value2: i32 = 0;
|
||||
|
||||
var history = init(std.testing.allocator);
|
||||
defer history.deinit();
|
||||
|
||||
try std.testing.expectEqual(false, history.isUndoAvailable());
|
||||
try std.testing.expectEqual(false, history.isRedoAvailable());
|
||||
try std.testing.expectError(error.NoCommand, history.undo());
|
||||
try std.testing.expectError(error.NoCommand, history.redo());
|
||||
|
||||
try history.exec(
|
||||
try CommandIF.make(std.testing.allocator, CommandIF.Mock { .target = &value1, }));
|
||||
try std.testing.expectEqual(history.isUndoAvailable(), true);
|
||||
try std.testing.expectEqual(history.isRedoAvailable(), false);
|
||||
try std.testing.expectEqual(value1, 1);
|
||||
|
||||
try history.undo();
|
||||
try std.testing.expectEqual(history.isUndoAvailable(), false);
|
||||
try std.testing.expectEqual(history.isRedoAvailable(), true);
|
||||
try std.testing.expectEqual(value1, 0);
|
||||
|
||||
try history.redo();
|
||||
try std.testing.expectEqual(history.isUndoAvailable(), true);
|
||||
try std.testing.expectEqual(history.isRedoAvailable(), false);
|
||||
try std.testing.expectEqual(value1, 1);
|
||||
|
||||
try history.exec(
|
||||
try CommandIF.make(std.testing.allocator, CommandIF.Mock { .target = &value2, }));
|
||||
try std.testing.expectEqual(history.isUndoAvailable(), true);
|
||||
try std.testing.expectEqual(history.isRedoAvailable(), false);
|
||||
try std.testing.expectEqual(value1, 1);
|
||||
try std.testing.expectEqual(value2, 1);
|
||||
|
||||
try history.undo();
|
||||
try std.testing.expectEqual(history.isUndoAvailable(), true);
|
||||
try std.testing.expectEqual(history.isRedoAvailable(), true);
|
||||
try std.testing.expectEqual(value1, 1);
|
||||
try std.testing.expectEqual(value2, 0);
|
||||
|
||||
try history.redo();
|
||||
try std.testing.expectEqual(history.isUndoAvailable(), true);
|
||||
try std.testing.expectEqual(history.isRedoAvailable(), false);
|
||||
try std.testing.expectEqual(value1, 1);
|
||||
try std.testing.expectEqual(value2, 1);
|
||||
|
||||
try history.undo();
|
||||
try std.testing.expectEqual(history.isUndoAvailable(), true);
|
||||
try std.testing.expectEqual(history.isRedoAvailable(), true);
|
||||
try std.testing.expectEqual(value1, 1);
|
||||
try std.testing.expectEqual(value2, 0);
|
||||
|
||||
try history.undo();
|
||||
try std.testing.expectEqual(history.isUndoAvailable(), false);
|
||||
try std.testing.expectEqual(history.isRedoAvailable(), true);
|
||||
try std.testing.expectEqual(value1, 0);
|
||||
try std.testing.expectEqual(value2, 0);
|
||||
}
|
48
src/hncore/Stage.zig
Normal file
48
src/hncore/Stage.zig
Normal file
@ -0,0 +1,48 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Task = @import("./Task.zig");
|
||||
|
||||
///
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
const TaskList = std.ArrayList(*Task);
|
||||
|
||||
///
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
///
|
||||
name: [:0]const u8,
|
||||
|
||||
///
|
||||
tasks: TaskList,
|
||||
|
||||
///
|
||||
pub fn init(alloc: std.mem.Allocator, name: []const u8) !Self {
|
||||
return Self {
|
||||
.alloc = alloc,
|
||||
.name = try alloc.dupeZ(u8, name),
|
||||
.tasks = TaskList.init(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.tasks.deinit();
|
||||
self.alloc.free(self.name);
|
||||
}
|
||||
|
||||
///
|
||||
pub fn rename(self: *Self, name: []const u8) !void {
|
||||
self.alloc.free(self.name);
|
||||
self.name = try self.alloc.dupeZ(u8, name);
|
||||
}
|
||||
|
||||
test {
|
||||
var stage = try init(std.testing.allocator, "helloworld");
|
||||
defer stage.deinit();
|
||||
try std.testing.expectEqualStrings("helloworld", stage.name);
|
||||
|
||||
try stage.rename("goodbye");
|
||||
try std.testing.expectEqualStrings("goodbye", stage.name);
|
||||
}
|
65
src/hncore/Task.zig
Normal file
65
src/hncore/Task.zig
Normal file
@ -0,0 +1,65 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
///
|
||||
id: usize,
|
||||
|
||||
///
|
||||
summary: [:0]const u8,
|
||||
|
||||
///
|
||||
details: [:0]const u8,
|
||||
|
||||
///
|
||||
done: bool,
|
||||
|
||||
///
|
||||
pub fn init(alloc: std.mem.Allocator, id: usize) !Self {
|
||||
return Self {
|
||||
.alloc = alloc,
|
||||
.id = id,
|
||||
.summary = try alloc.dupeZ(u8, ""),
|
||||
.details = try alloc.dupeZ(u8, ""),
|
||||
.done = true,
|
||||
};
|
||||
}
|
||||
///
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.alloc.free(self.details);
|
||||
self.alloc.free(self.summary);
|
||||
}
|
||||
|
||||
///
|
||||
pub fn setSummary(self: *Self, v: []const u8) !void {
|
||||
self.alloc.free(self.summary);
|
||||
self.summary = try self.alloc.dupeZ(u8, v);
|
||||
}
|
||||
///
|
||||
pub fn setDetails(self: *Self, v: []const u8) !void {
|
||||
self.alloc.free(self.details);
|
||||
self.details = try self.alloc.dupeZ(u8, v);
|
||||
}
|
||||
|
||||
test {
|
||||
var task = try init(std.testing.allocator, 0);
|
||||
defer task.deinit();
|
||||
try std.testing.expectEqual(task.id, 0);
|
||||
try std.testing.expectEqualStrings("", task.summary);
|
||||
try std.testing.expectEqualStrings("", task.details);
|
||||
|
||||
try task.setSummary("helloworld");
|
||||
try std.testing.expectEqualStrings("helloworld", task.summary);
|
||||
try std.testing.expectEqualStrings("", task.details);
|
||||
|
||||
try task.setDetails("good afternoon");
|
||||
try std.testing.expectEqualStrings("helloworld", task.summary);
|
||||
try std.testing.expectEqualStrings("good afternoon", task.details);
|
||||
|
||||
try task.setSummary("goodbye");
|
||||
try std.testing.expectEqualStrings("goodbye", task.summary);
|
||||
try std.testing.expectEqualStrings("good afternoon", task.details);
|
||||
}
|
67
src/hncore/TaskStore.zig
Normal file
67
src/hncore/TaskStore.zig
Normal file
@ -0,0 +1,67 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Task = @import("./Task.zig");
|
||||
|
||||
///
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
const Map = std.AutoHashMap(usize, *Task);
|
||||
|
||||
///
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
///
|
||||
map: Map,
|
||||
|
||||
///
|
||||
nextId: usize,
|
||||
|
||||
///
|
||||
pub fn init(alloc: std.mem.Allocator) Self {
|
||||
return Self {
|
||||
.alloc = alloc,
|
||||
.map = Map.init(alloc),
|
||||
.nextId = 0,
|
||||
};
|
||||
}
|
||||
///
|
||||
pub fn deinit(self: *Self) void {
|
||||
var itr = self.map.valueIterator();
|
||||
while (itr.next()) |task| {
|
||||
task.*.deinit();
|
||||
self.alloc.destroy(task.*);
|
||||
}
|
||||
self.map.deinit();
|
||||
}
|
||||
///
|
||||
pub fn add(self: *Self) !*Task {
|
||||
var task = try self.alloc.create(Task);
|
||||
errdefer self.alloc.destroy(task);
|
||||
|
||||
task.* = try Task.init(self.alloc, self.nextId);
|
||||
errdefer task.deinit();
|
||||
|
||||
try self.map.putNoClobber(task.id, task);
|
||||
|
||||
self.nextId += 1;
|
||||
return task;
|
||||
}
|
||||
///
|
||||
pub fn query(self: *const Self, id: usize) !*Task {
|
||||
return self.map.get(id) orelse error.NotFound;
|
||||
}
|
||||
|
||||
test {
|
||||
var store = init(std.testing.allocator);
|
||||
defer store.deinit();
|
||||
|
||||
const task1 = try store.add();
|
||||
const task2 = try store.add();
|
||||
|
||||
try std.testing.expect(task1.id != task2.id);
|
||||
|
||||
try std.testing.expectEqual(task1, store.query(task1.id));
|
||||
try std.testing.expectEqual(task2, store.query(task2.id));
|
||||
try std.testing.expectError(error.NotFound, store.query(1234));
|
||||
}
|
39
src/hncore/Workspace.zig
Normal file
39
src/hncore/Workspace.zig
Normal file
@ -0,0 +1,39 @@
|
||||
const std = @import("std");
|
||||
|
||||
const CommandHistory = @import("./CommandHistory.zig");
|
||||
const TaskStore = @import("./TaskStore.zig");
|
||||
|
||||
///
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
const Perma = struct {
|
||||
tasks: TaskStore,
|
||||
};
|
||||
|
||||
///
|
||||
commands: CommandHistory,
|
||||
|
||||
///
|
||||
perma: Perma,
|
||||
|
||||
///
|
||||
pub fn init(allocator: std.mem.Allocator) Self {
|
||||
return Self {
|
||||
.commands = CommandHistory.init(allocator),
|
||||
.perma = .{
|
||||
.tasks = TaskStore.init(allocator),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.perma.tasks.deinit();
|
||||
self.commands.deinit();
|
||||
}
|
||||
|
||||
test {
|
||||
var ws = init(std.testing.allocator);
|
||||
defer ws.deinit();
|
||||
}
|
137
src/hncore/command/Interface.zig
Normal file
137
src/hncore/command/Interface.zig
Normal file
@ -0,0 +1,137 @@
|
||||
//!
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
///
|
||||
pub const Self = @This();
|
||||
|
||||
///
|
||||
pub const Error = error {
|
||||
///
|
||||
OutOfMemory,
|
||||
|
||||
///
|
||||
InvalidState,
|
||||
|
||||
///
|
||||
Unknown,
|
||||
};
|
||||
|
||||
///
|
||||
pub const VTable = struct {
|
||||
///
|
||||
deinit: *const fn(ctx: *anyopaque) void,
|
||||
|
||||
///
|
||||
apply: *const fn(ctx: *anyopaque) Error!void,
|
||||
|
||||
///
|
||||
revert: *const fn(ctx: *anyopaque) Error!void,
|
||||
};
|
||||
|
||||
///
|
||||
pub const Mock = struct {
|
||||
target : ?*i32 = null,
|
||||
destroyed: ?*bool = null,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, target: ?*i32, destroyed: ?*bool) !Self {
|
||||
return make(alloc, Mock {
|
||||
.target = target,
|
||||
.destroyed = destroyed,
|
||||
});
|
||||
}
|
||||
pub fn deinit(self: *@This(), _: anytype) void {
|
||||
if (self.destroyed) |ptr| {
|
||||
ptr.* = true;
|
||||
}
|
||||
}
|
||||
pub fn apply(self: *@This(), _: anytype) !void {
|
||||
if (self.target) |ptr| {
|
||||
ptr.* += 1;
|
||||
}
|
||||
}
|
||||
pub fn revert(self: *@This(), _: anytype) !void {
|
||||
if (self.target) |ptr| {
|
||||
ptr.* -= 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
ctx: *anyopaque,
|
||||
|
||||
///
|
||||
vtable: *const VTable,
|
||||
|
||||
///
|
||||
pub fn deinit(self: Self) void {
|
||||
self.vtable.deinit(self.ctx);
|
||||
}
|
||||
|
||||
///
|
||||
pub fn apply(self: Self) !void {
|
||||
return self.vtable.apply(self.ctx);
|
||||
}
|
||||
|
||||
///
|
||||
pub fn revert(self: Self) !void {
|
||||
return self.vtable.revert(self.ctx);
|
||||
}
|
||||
|
||||
///
|
||||
pub fn make(alloc: std.mem.Allocator, ctx: anytype) !Self {
|
||||
const Target = @TypeOf(ctx);
|
||||
const Wrapper = struct {
|
||||
const vtable = VTable {
|
||||
.deinit = @This().del,
|
||||
.apply = @This().apply,
|
||||
.revert = @This().revert,
|
||||
};
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
ctx : Target,
|
||||
|
||||
fn new(alloc2: std.mem.Allocator, ctx2: Target) !*@This() {
|
||||
const self = try alloc2.create(@This());
|
||||
self.* = .{
|
||||
.alloc = alloc2,
|
||||
.ctx = ctx2,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
fn del(selfp: *anyopaque) void {
|
||||
const self: *@This() = @ptrCast(@alignCast(selfp));
|
||||
self.ctx.deinit(self.alloc);
|
||||
self.alloc.destroy(self);
|
||||
}
|
||||
fn apply(selfp: *anyopaque) Error!void {
|
||||
const self: *@This() = @ptrCast(@alignCast(selfp));
|
||||
return self.ctx.apply(self.alloc);
|
||||
}
|
||||
fn revert(selfp: *anyopaque) Error!void {
|
||||
const self: *@This() = @ptrCast(@alignCast(selfp));
|
||||
return self.ctx.revert(self.alloc);
|
||||
}
|
||||
};
|
||||
return Self {
|
||||
.ctx = try Wrapper.new(alloc, ctx),
|
||||
.vtable = &Wrapper.vtable,
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
var value: i32 = 0;
|
||||
var destroyed = false;
|
||||
{
|
||||
const cmd = try Mock.init(std.testing.allocator, &value, &destroyed);
|
||||
defer cmd.deinit();
|
||||
|
||||
try cmd.apply();
|
||||
try std.testing.expectEqual(value, 1);
|
||||
|
||||
try cmd.revert();
|
||||
try std.testing.expectEqual(value, 0);
|
||||
}
|
||||
|
||||
try std.testing.expect(destroyed);
|
||||
}
|
88
src/hncore/command/generic.zig
Normal file
88
src/hncore/command/generic.zig
Normal file
@ -0,0 +1,88 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Interface = @import("./Interface.zig");
|
||||
|
||||
///
|
||||
const Sequence = struct {
|
||||
///
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
commands: []Interface,
|
||||
|
||||
///
|
||||
pub fn init(alloc: std.mem.Allocator, commands: []const Interface) !Interface {
|
||||
const list = try alloc.dupe(Interface, commands);
|
||||
errdefer alloc.free(list);
|
||||
return try Interface.make(alloc, Self {
|
||||
.commands = list,
|
||||
});
|
||||
}
|
||||
|
||||
///
|
||||
pub fn deinit(self: *Self, alloc: std.mem.Allocator) void {
|
||||
for (self.commands) |command| {
|
||||
command.deinit();
|
||||
}
|
||||
alloc.free(self.commands);
|
||||
}
|
||||
|
||||
///
|
||||
pub fn apply(self: *Self, _: std.mem.Allocator) Interface.Error!void {
|
||||
for (self.commands) |command| {
|
||||
try command.apply();
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn revert(self: *Self, _: std.mem.Allocator) Interface.Error!void {
|
||||
var i = self.commands.len;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
try self.commands[i].revert();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
var value1: i32 = 0;
|
||||
var value2: i32 = 0;
|
||||
var value3: i32 = 0;
|
||||
|
||||
var finished1 = false;
|
||||
var finished2 = false;
|
||||
var finished3 = false;
|
||||
|
||||
{
|
||||
var seq = blk: {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var cmd1 = try Interface.Mock.init(alloc, &value1, &finished1);
|
||||
errdefer cmd1.deinit();
|
||||
|
||||
var cmd2 = try Interface.Mock.init(alloc, &value2, &finished2);
|
||||
errdefer cmd2.deinit();
|
||||
|
||||
var cmd3 = try Interface.Mock.init(alloc, &value3, &finished3);
|
||||
errdefer cmd3.deinit();
|
||||
|
||||
break :blk try Sequence.init(std.testing.allocator, &[_]Interface {
|
||||
cmd1, cmd2, cmd3,
|
||||
});
|
||||
};
|
||||
defer seq.deinit();
|
||||
|
||||
try seq.apply();
|
||||
try std.testing.expectEqual(1, value1);
|
||||
try std.testing.expectEqual(1, value2);
|
||||
try std.testing.expectEqual(1, value3);
|
||||
|
||||
try seq.revert();
|
||||
try std.testing.expectEqual(0, value1);
|
||||
try std.testing.expectEqual(0, value2);
|
||||
try std.testing.expectEqual(0, value3);
|
||||
}
|
||||
try std.testing.expect(finished1);
|
||||
try std.testing.expect(finished2);
|
||||
try std.testing.expect(finished3);
|
||||
}
|
8
src/hncore/command/root.zig
Normal file
8
src/hncore/command/root.zig
Normal file
@ -0,0 +1,8 @@
|
||||
pub const Interface = @import("./Interface.zig");
|
||||
|
||||
pub const generic = @import("./generic.zig");
|
||||
pub const task = @import("./task.zig");
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
67
src/hncore/command/task.zig
Normal file
67
src/hncore/command/task.zig
Normal file
@ -0,0 +1,67 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Interface = @import("./Interface.zig");
|
||||
|
||||
const Task = @import("../Task.zig");
|
||||
|
||||
///
|
||||
const SetSummary = struct {
|
||||
///
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
target: *Task,
|
||||
|
||||
///
|
||||
summary: [:0]const u8,
|
||||
|
||||
///
|
||||
pub fn init(alloc: std.mem.Allocator, task: *Task, summary: []const u8) !Interface {
|
||||
const summaryDup = try alloc.dupeZ(u8, summary);
|
||||
errdefer alloc.free(summaryDup);
|
||||
return try Interface.make(alloc, Self {
|
||||
.target = task,
|
||||
.summary = summaryDup,
|
||||
});
|
||||
}
|
||||
|
||||
///
|
||||
pub fn deinit(self: *Self, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.summary);
|
||||
}
|
||||
|
||||
///
|
||||
pub fn apply(self: *Self, alloc: std.mem.Allocator) Interface.Error!void {
|
||||
const old = try alloc.dupeZ(u8, self.target.summary);
|
||||
errdefer alloc.free(old);
|
||||
|
||||
try self.target.setSummary(self.summary);
|
||||
alloc.free(self.summary);
|
||||
|
||||
self.summary = old;
|
||||
}
|
||||
|
||||
///
|
||||
pub fn revert(self: *Self, alloc: std.mem.Allocator) Interface.Error!void {
|
||||
return self.apply(alloc);
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
const TaskStore = @import("../TaskStore.zig");
|
||||
|
||||
var tasks = TaskStore.init(std.testing.allocator);
|
||||
defer tasks.deinit();
|
||||
|
||||
var task = try tasks.add();
|
||||
try task.setSummary("helloworld");
|
||||
|
||||
var sut = try SetSummary.init(std.testing.allocator, task, "goodbye");
|
||||
defer sut.deinit();
|
||||
|
||||
try sut.apply();
|
||||
try std.testing.expectEqualStrings("goodbye", task.summary);
|
||||
|
||||
try sut.revert();
|
||||
try std.testing.expectEqualStrings("helloworld", task.summary);
|
||||
}
|
12
src/hncore/root.zig
Normal file
12
src/hncore/root.zig
Normal file
@ -0,0 +1,12 @@
|
||||
pub const command = @import("./command/root.zig");
|
||||
|
||||
pub const CommandHistory = @import("./CommandHistory.zig");
|
||||
pub const Stage = @import("./Stage.zig");
|
||||
pub const Task = @import("./Task.zig");
|
||||
pub const TaskStore = @import("./TaskStore.zig");
|
||||
pub const Workspace = @import("./Workspace.zig");
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user