Compare commits

..

4 Commits

Author SHA1 Message Date
8263ccad82 implement Command interface 2025-05-05 09:56:28 +09:00
88252a151b implement data structure 2025-05-05 08:17:13 +09:00
0bff9feb35 add new common component, taskcard 2025-04-29 16:44:29 +09:00
082a77a5eb add implementations of floating windows 2025-04-29 15:18:14 +09:00
19 changed files with 1177 additions and 115 deletions

View File

@ -2,49 +2,42 @@ const std = @import("std");
const dvui = @import("dvui");
const hncore = @import("hncore");
const win = @import("./win/root.zig");
const compo = @import("./compo/root.zig");
const Self = @This();
alloc: std.mem.Allocator,
today: win.today.Mock,
stage: compo.stage.Mock,
search: compo.search.Mock,
taskedit: compo.taskedit.Mock,
pub fn init(alloc: std.mem.Allocator) !Self {
return Self {
.alloc = alloc,
.today = try win.today.Mock.init(alloc),
.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.today.deinit();
self.taskedit.deinit();
self.stage.deinit();
self.search.deinit();
}
pub fn gui(self: *Self) !void {
// ---- menu
try compo.menu.gui(.{});
// background
{
var root = try dvui.menu(@src(), .horizontal, .{ .background = true, .expand = .horizontal });
defer root.deinit();
if (try dvui.menuItemLabel(@src(), "File", .{ .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(), "Open", .{}, .{})) |_| {
}
}
}
// ---- project tabs
{
var tbox = try dvui.box(@src(), .vertical, .{
var box = try dvui.box(@src(), .vertical, .{
.expand = .both,
.background = true,
.color_fill = .{ .name = .fill_window },
.color_fill = .{ .color = dvui.Color.white, },
});
defer tbox.deinit();
defer box.deinit();
}
// ---- windows
try win.today.gui(&self.today);
try compo.search.gui(&self.search);
try compo.stage.gui(&self.stage);
try compo.taskedit.gui(&self.taskedit);
}

View 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", .{}, .{})) |_| {
}
}
}

View 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");

View 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;
}
};

View 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;
}
};

View 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;
}

View 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; }
};

View 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;
}

View File

@ -0,0 +1 @@
pub const event = @import("./event.zig");

View File

@ -1 +0,0 @@
pub const today = @import("./today.zig");

View File

@ -1,88 +0,0 @@
const std = @import("std");
const dvui = @import("dvui");
pub const Task = struct {
name: [:0]const u8,
mark: bool,
};
pub fn gui(ctx: anytype) !void {
var win = try dvui.floatingWindow(@src(), .{}, .{});
defer win.deinit();
try dvui.windowHeader("Today", "", 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, });
defer list.deinit();
const tasks = ctx.get_tasks();
for (0.., tasks) |idx, task| {
var reorderable = try reorder.reorderable(@src(), .{}, .{
.id_extra = idx,
.expand = .horizontal,
});
defer reorderable.deinit();
var hbox = try dvui.box(@src(), .horizontal, .{
.expand = .both,
.background = true,
.border = dvui.Rect.all(1),
});
defer hbox.deinit();
var mark: bool = task.mark;
if (try dvui.checkbox(@src(), &mark, null, .{})) {
ctx.mark(idx, mark);
}
try dvui.label(@src(), "{s}", .{ task.name, }, .{});
_ = try dvui.ReorderWidget.draggable(@src(), .{ .reorderable = reorderable, }, .{
.expand = .vertical,
.gravity_x = 1.0,
.gravity_y = 0.5,
});
}
try dvui.labelNoFmt(@src(), "no tasks anymore :)", .{ .gravity_x = 0.5 });
}
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();
try tasks.append(Task { .name = "helloworld", .mark = true, });
try tasks.append(Task { .name = "goodbye", .mark = false, });
return Mock {
.tasks = tasks,
};
}
pub fn deinit(self: *Self) void {
self.tasks.deinit();
}
fn mark(self: *Self, idx: usize, check: bool) void {
self.tasks.items[idx].mark = check;
}
fn get_tasks(self: *const Self) []const Task {
return self.tasks.items;
}
};

View 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);
}

69
src/hncore/Task.zig Normal file
View File

@ -0,0 +1,69 @@
const std = @import("std");
const Self = @This();
///
alloc: std.mem.Allocator,
///
id: usize,
///
summary: [:0]const u8,
///
details: [:0]const u8,
///
done: bool,
///
archived: 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,
.archived = 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);
}

59
src/hncore/TaskStore.zig Normal file
View File

@ -0,0 +1,59 @@
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));
}

23
src/hncore/Workspace.zig Normal file
View File

@ -0,0 +1,23 @@
const std = @import("std");
const CommandHistory = @import("./CommandHistory.zig");
const TaskStore = @import("./TaskStore.zig");
const Self = @This();
tasks: TaskStore,
commands: CommandHistory,
pub fn init(allocator: std.mem.Allocator) Self {
return Self {
.commands = CommandHistory.init(allocator),
.tasks = TaskStore.init(allocator),
};
}
pub fn deinit(self: *Self) void {
self.tasks.deinit();
}
test {
var ws = init(std.testing.allocator);
defer ws.deinit();
}

View File

@ -0,0 +1,134 @@
//!
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 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 Self.make(std.testing.allocator, Mock {
.target = &value,
.destroyed = &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);
}

View File

@ -0,0 +1,7 @@
pub const Interface = @import("./Interface.zig");
pub const task = @import("./task.zig");
test {
@import("std").testing.refAllDecls(@This());
}

View 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);
}

View File

@ -1,3 +1,10 @@
pub const command = @import("./command/root.zig");
pub const CommandHistory = @import("./CommandHistory.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());
}