diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db3cb54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.zig-cache/ +/zig-out/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..09916ba --- /dev/null +++ b/build.zig @@ -0,0 +1,62 @@ +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, + }); + + // ---- 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/hnet/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 = "hnet", + .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); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..811bb1d --- /dev/null +++ b/build.zig.zon @@ -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", + }, +} diff --git a/src/hncore/.DS_Store b/src/hncore/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/src/hncore/.DS_Store differ diff --git a/src/hncore/Digraph.zig b/src/hncore/Digraph.zig new file mode 100644 index 0000000..7a2c603 --- /dev/null +++ b/src/hncore/Digraph.zig @@ -0,0 +1,249 @@ +const std = @import("std"); + +/// A data type to store relations of nodes in directional-graph. +pub fn Digraph(comptime T: type) type { + return struct { + const Node = T; + const Conn = struct { from: T, to: T, }; + const ConnList = std.ArrayList(Conn); + const Error = error { + AlreadyConnected, + NotConnected, + }; + + map: ConnList, + + /// + pub fn init(alloc: std.mem.Allocator, map_unsorted: []const Conn) !@This() { + var map_sorted = ConnList.init(alloc); + try map_sorted.ensureTotalCapacity(map_unsorted.len); + for (map_unsorted) |conn| { + try map_sorted.append(conn); + } + std.mem.sort(Conn, map_sorted.items, {}, compare_conn); + + return .{ + .map = map_sorted, + }; + } + /// + pub fn deinit(self: *@This()) void { + self.map.deinit(); + } + + /// + pub fn connect_if(self: *@This(), from: T, to: T) !bool { + const begin, const end = self.find_segment(from); + if (self.find_connection_in_segment(from, to, begin, end)) |_| { + return false; + } else { + try self.map.insert(end, Conn { .from = from, .to = to, }); + return true; + } + } + /// Same to `connect_if`, but returns an error if it's already connected. + pub fn connect(self: *@This(), from: T, to: T) !void { + if (!try self.connect_if(from, to)) { + return Error.AlreadyConnected; + } + } + + /// + pub fn disconnect_if(self: *@This(), from: T, to: T) bool { + const begin, const end = self.find_segment(from); + if (self.find_connection_in_segment(from, to, begin, end)) |idx| { + _ = self.map.orderedRemove(idx); + return true; + } else { + return false; + } + } + /// Same to `disconnect_if`, but returns an error if it's not connected. + pub fn disconnect(self: *@This(), from: T, to: T) !void { + if (!self.disconnect_if(from, to)) { + return Error.NotConnected; + } + } + + /// + pub fn is_connected(self: *const @This(), from: T, to: T) bool { + const begin, const end = self.find_segment(from); + return self.find_connection_in_segment(from, to, begin, end) != null; + + } + + fn find_connection_in_segment(self: *const @This(), from: T, to: T, begin: usize, end: usize) ?usize { + for (self.map.items[begin..end], begin..) |v, idx| { + if ((v.from == from) and (v.to == to)) { + return idx; + } + } + return null; + } + fn find_segment(self: *const @This(), from: T) struct { usize, usize } { + const n = self.map.items.len; + if (n == 0) { + return .{ 0, 0, }; + } + + const base_idx = self.binsearch(from).?; + const base_from = self.map.items[base_idx].from; + + var begin: usize = undefined; + var end : usize = undefined; + if (base_from < from) { + begin = base_idx; + while ((begin < n) and (self.map.items[begin].from != from)) { begin += 1; } + + end = begin; + while ((end < n) and (self.map.items[end].from == from)) { end += 1; } + + } else if (base_from > from) { + end = base_idx; + while ((end > 0) and (self.map.items[end-1].from != from)) { end -= 1; } + + begin = end; + while ((begin > 0) and (self.map.items[begin-1].from == from)) { begin -= 1; } + + } else { + begin = base_idx; + while ((begin > 0) and (self.map.items[begin-1].from == from)) { begin -= 1; } + + end = base_idx; + while ((end < n) and (self.map.items[end].from == from)) { end += 1; } + } + return .{ begin, end, }; + } + fn binsearch(self: *const @This(), from: T) ?usize { + if (self.map.items.len == 0) { + return null; + } + + var left : usize = 0; + var right: usize = self.map.items.len; + + var idx: usize = undefined; + while (left < right) { + idx = (left + right) / 2; + + const target = self.map.items[idx].from; + if (target < from) { + left = idx + 1; + } else if (target > from) { + right = idx -| 1; + } else { + break; + } + } + return idx; + } + fn compare_conn(_: void, a: Conn, b: Conn) bool { + return a.from < b.from; + } + }; +} + +test "check if connected" { + const Sut = Digraph(u8); + + const map = [_]Sut.Conn { + .{ .from = 3, .to = 0, }, + .{ .from = 0, .to = 1, }, + .{ .from = 1, .to = 3, }, + }; + + var sut = try Sut.init(std.testing.allocator, map[0..]); + defer sut.deinit(); + + try std.testing.expect(sut.is_connected(0, 1)); + try std.testing.expect(!sut.is_connected(1, 0)); + + try std.testing.expect(sut.is_connected(1, 3)); + try std.testing.expect(!sut.is_connected(3, 1)); + + try std.testing.expect(sut.is_connected(3, 0)); + try std.testing.expect(!sut.is_connected(0, 3)); + + try std.testing.expect(!sut.is_connected(0, 2)); + try std.testing.expect(!sut.is_connected(2, 0)); + + try std.testing.expect(!sut.is_connected(1, 2)); + try std.testing.expect(!sut.is_connected(2, 1)); +} +test "make new connection" { + const Sut = Digraph(u8); + + var sut = try Sut.init(std.testing.allocator, &.{}); + defer sut.deinit(); + + try std.testing.expect(try sut.connect_if(2, 1)); + + try std.testing.expect(sut.is_connected(2, 1)); + try std.testing.expect(!sut.is_connected(1, 2)); + + try sut.connect(3, 1); + + try std.testing.expect(sut.is_connected(3, 1)); + try std.testing.expect(!sut.is_connected(1, 3)); +} +test "making an existing connection fails" { + const Sut = Digraph(u8); + + const map = [_]Sut.Conn { + .{ .from = 0, .to = 1, }, + }; + var sut = try Sut.init(std.testing.allocator, map[0..]); + defer sut.deinit(); + + try std.testing.expect(!try sut.connect_if(0, 1)); + try std.testing.expectError(Sut.Error.AlreadyConnected, sut.connect(0, 1)); +} +test "disconnect an existing connection" { + const Sut = Digraph(u8); + + const map = [_]Sut.Conn { + .{ .from = 0, .to = 1, }, + .{ .from = 2, .to = 3, }, + }; + var sut = try Sut.init(std.testing.allocator, map[0..]); + defer sut.deinit(); + + try std.testing.expect(sut.disconnect_if(0, 1)); + try std.testing.expect(!sut.is_connected(0, 1)); + + try sut.disconnect(2, 3); + try std.testing.expect(!sut.is_connected(2, 3)); +} +test "disconnecting a missing connection fails" { + const Sut = Digraph(u8); + + var sut = try Sut.init(std.testing.allocator, &.{}); + defer sut.deinit(); + + try std.testing.expect(!sut.disconnect_if(0, 1)); + try std.testing.expectError(Sut.Error.NotConnected, sut.disconnect(1, 0)); +} +test "chaotic operation" { + const Sut = Digraph(u16); + + var sut = try Sut.init(std.testing.allocator, &.{}); + defer sut.deinit(); + + const N = 100; + for (0..N) |v| { + const x: Sut.Node = @intCast(v); + try sut.connect(x*%7, x*%13); + } + for (N/2..N) |v| { + const x: Sut.Node = @intCast(v); + try sut.disconnect(x*%7, x*%13); + } + for (0..N/2) |v| { + const x: Sut.Node = @intCast(v); + try std.testing.expect(sut.is_connected(x*%7, x*%13)); + } + for (N/2..N) |v| { + const x: Sut.Node = @intCast(v); + try std.testing.expect(!sut.is_connected(x*%7, x*%13)); + } +} diff --git a/src/hncore/root.zig b/src/hncore/root.zig new file mode 100644 index 0000000..d9fa89a --- /dev/null +++ b/src/hncore/root.zig @@ -0,0 +1,5 @@ +pub const Digraph = @import("./Digraph.zig").Digraph; + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/hnet/App.zig b/src/hnet/App.zig new file mode 100644 index 0000000..3b3c2d4 --- /dev/null +++ b/src/hnet/App.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const dvui = @import("dvui"); +const hncore = @import("hncore"); + +pub const App = struct { + pub fn init(alloc: std.mem.Allocator) !App { + _ = alloc; + return App { + }; + } + pub fn deinit(self: *App) void { + _ = self; + } + + pub fn gui(self: *App) !void { + _ = self; + + try gui_menu(); + try gui_main(); + } + fn gui_menu() !void { + 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", .{}, .{})) |_| { + } + } + } + fn gui_main() !void { + var box = try dvui.scrollArea(@src(), .{}, .{ + .expand = .both, + .color_fill = .{ .name = .fill_window }, + .padding = dvui.Rect.all(8), + }); + defer box.deinit(); + + if (try dvui.button(@src(), "Zoom In", .{}, .{})) { + } + if (try dvui.button(@src(), "Zoom In", .{}, .{})) { + } + } +}; diff --git a/src/hnet/main.zig b/src/hnet/main.zig new file mode 100644 index 0000000..1fd6c5f --- /dev/null +++ b/src/hnet/main.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const dvui = @import("dvui"); +const hncore = @import("hncore"); + +const App = @import("./App.zig").App; + +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()); + 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), + ); + } +}