Compare commits
	
		
			11 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 89f41c9d78 | |||
| 8d53a185a4 | |||
| ca3d26a18d | |||
| a339d2a392 | |||
| fc303ba553 | |||
| ee8d0cde34 | |||
| 845582a14d | |||
| 907d71a19a | |||
| b9153e2708 | |||
| a1ede8d1a0 | |||
| 0bfa0f1098 | 
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | .DS_Store | ||||||
|  |  | ||||||
|  | /.zig-cache/ | ||||||
|  | /zig-out/ | ||||||
							
								
								
									
										71
									
								
								build.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								build.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | |||||||
|  | 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); | ||||||
|  |  | ||||||
|  |     // ---- 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", | ||||||
|  |     }, | ||||||
|  | } | ||||||
							
								
								
									
										295
									
								
								src/hncore/Digraph.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								src/hncore/Digraph.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,295 @@ | |||||||
|  | const std = @import("std"); | ||||||
|  |  | ||||||
|  | /// A data type to store connections of nodes in directional-graph. | ||||||
|  | pub fn Container(comptime T: type, comptime lessThanFn: LessThanFunc(T)) type { | ||||||
|  |     return struct { | ||||||
|  |         /// | ||||||
|  |         pub const Node = T; | ||||||
|  |  | ||||||
|  |         /// A connection between 2 nodes. | ||||||
|  |         /// e.g.) `Conn { .from = X, .to = Y }` =>  "X is connected to Y" | ||||||
|  |         pub const Conn = struct { from: T, to: T, }; | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         pub const ConnList = std.ArrayList(Conn); | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         pub const Error = error { | ||||||
|  |             AlreadyConnected, | ||||||
|  |             NotConnected, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         map: ConnList, | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         pub fn init(alloc: std.mem.Allocator, mapUnsorted: []const Conn) !@This() { | ||||||
|  |             var mapSorted = ConnList.init(alloc); | ||||||
|  |             try mapSorted.ensureTotalCapacity(mapUnsorted.len); | ||||||
|  |             for (mapUnsorted) |conn| { | ||||||
|  |                 try mapSorted.append(conn); | ||||||
|  |             } | ||||||
|  |             std.mem.sort(Conn, mapSorted.items, {}, compareConn); | ||||||
|  |  | ||||||
|  |             return .{ | ||||||
|  |                 .map  = mapSorted, | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |         /// | ||||||
|  |         pub fn deinit(self: *@This()) void { | ||||||
|  |             self.map.deinit(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// Makes new connection between `from` to `to`. | ||||||
|  |         /// Returns false if they are already connected otherwise true. | ||||||
|  |         pub fn connectIf(self: *@This(), from: T, to: T) !bool { | ||||||
|  |             const begin, const end = self.findSegment(from); | ||||||
|  |             if (self.findConnectionInSegment(from, to, begin, end)) |_| { | ||||||
|  |                 return false; | ||||||
|  |             } else { | ||||||
|  |                 try self.map.insert(end, Conn { .from = from, .to = to, }); | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         /// Same to `connectIf`, but returns an error if it's already connected. | ||||||
|  |         pub fn connect(self: *@This(), from: T, to: T) !void { | ||||||
|  |             if (!try self.connectIf(from, to)) { | ||||||
|  |                 return Error.AlreadyConnected; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// Removes an existing connection between `from` to `to`. | ||||||
|  |         /// Returns false if they are not connected otherwise true. | ||||||
|  |         pub fn disconnectIf(self: *@This(), from: T, to: T) bool { | ||||||
|  |             const begin, const end = self.findSegment(from); | ||||||
|  |             if (self.findConnectionInSegment(from, to, begin, end)) |idx| { | ||||||
|  |                 _ = self.map.orderedRemove(idx); | ||||||
|  |                 return true; | ||||||
|  |             } else { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         /// Same to `disconnectIf`, but returns an error if it's not connected. | ||||||
|  |         pub fn disconnect(self: *@This(), from: T, to: T) !void { | ||||||
|  |             if (!self.disconnectIf(from, to)) { | ||||||
|  |                 return Error.NotConnected; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         pub fn isConnected(self: *const @This(), from: T, to: T) bool { | ||||||
|  |             const begin, const end = self.findSegment(from); | ||||||
|  |             return self.findConnectionInSegment(from, to, begin, end) != null; | ||||||
|  |  | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         pub fn getChildrenOf(self: *const @This(), from: T) []const Conn { | ||||||
|  |             const begin, const end = self.findSegment(from); | ||||||
|  |             return self.map.items[begin..end]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         fn findConnectionInSegment(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 findSegment(self: *const @This(), from: T) struct { usize, usize } { | ||||||
|  |             const n = self.map.items.len; | ||||||
|  |             if (n == 0) { | ||||||
|  |                 return .{ 0, 0, }; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const baseIdx  = self.binsearch(from).?; | ||||||
|  |             const baseFrom = self.map.items[baseIdx].from; | ||||||
|  |  | ||||||
|  |             var begin: usize = undefined; | ||||||
|  |             var end  : usize = undefined; | ||||||
|  |             if (lessThanFn(baseFrom, from)) { | ||||||
|  |                 begin = baseIdx; | ||||||
|  |                 while ((begin < n) and lessThanFn(self.map.items[begin].from, from)) { begin += 1; } | ||||||
|  |  | ||||||
|  |                 end = begin; | ||||||
|  |                 while ((end < n) and (self.map.items[end].from == from)) { end += 1; } | ||||||
|  |  | ||||||
|  |             } else if (lessThanFn(from, baseFrom)) { | ||||||
|  |                 end = baseIdx; | ||||||
|  |                 while ((end > 0) and lessThanFn(from, self.map.items[end-1].from)) { end -= 1; } | ||||||
|  |  | ||||||
|  |                 begin = end; | ||||||
|  |                 while ((begin > 0) and (self.map.items[begin-1].from == from)) { begin -= 1; } | ||||||
|  |  | ||||||
|  |             } else { | ||||||
|  |                 begin = baseIdx; | ||||||
|  |                 while ((begin > 0) and (self.map.items[begin-1].from == from)) { begin -= 1; } | ||||||
|  |  | ||||||
|  |                 end = baseIdx; | ||||||
|  |                 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 (lessThanFn(target, from)) { | ||||||
|  |                     left = idx + 1; | ||||||
|  |                 } else if (lessThanFn(from, target)) { | ||||||
|  |                     right = idx -| 1; | ||||||
|  |                 } else { | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return idx; | ||||||
|  |         } | ||||||
|  |         fn compareConn(_: void, a: Conn, b: Conn) bool { | ||||||
|  |             return lessThanFn(a.from, b.from); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// A type of comparator function for the type T, which is to be passed as an argument of `Container()`. | ||||||
|  | pub fn LessThanFunc(comptime T: type) type { | ||||||
|  |     return fn (lhs: T, rhs: T) bool; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Returns a lessThanFunc for the comparable type T. | ||||||
|  | pub fn lessThanFuncFor(comptime T: type) LessThanFunc(T) { | ||||||
|  |     return struct { | ||||||
|  |         fn inner(lhs: T, rhs: T) bool { | ||||||
|  |             if (@typeInfo(T) == .pointer) { | ||||||
|  |                 return @intFromPtr(lhs) < @intFromPtr(rhs); | ||||||
|  |             } else { | ||||||
|  |                 return lhs < rhs; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }.inner; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | test "compile check for various types" { | ||||||
|  |     _ = Container(u8,  lessThanFuncFor(u8)); | ||||||
|  |     _ = Container(u16, lessThanFuncFor(u16)); | ||||||
|  |     _ = Container(i8,  lessThanFuncFor(i8)); | ||||||
|  |     _ = Container(i16, lessThanFuncFor(i16)); | ||||||
|  |     _ = Container(*i8, lessThanFuncFor(*i8)); | ||||||
|  |     _ = Container(*anyopaque, lessThanFuncFor(*anyopaque)); | ||||||
|  | } | ||||||
|  | test "check if connected" { | ||||||
|  |     const Sut = Container(u8, lessThanFuncFor(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.isConnected(0, 1)); | ||||||
|  |     try std.testing.expect(!sut.isConnected(1, 0)); | ||||||
|  |  | ||||||
|  |     try std.testing.expect(sut.isConnected(1, 3)); | ||||||
|  |     try std.testing.expect(!sut.isConnected(3, 1)); | ||||||
|  |  | ||||||
|  |     try std.testing.expect(sut.isConnected(3, 0)); | ||||||
|  |     try std.testing.expect(!sut.isConnected(0, 3)); | ||||||
|  |  | ||||||
|  |     try std.testing.expect(!sut.isConnected(0, 2)); | ||||||
|  |     try std.testing.expect(!sut.isConnected(2, 0)); | ||||||
|  |  | ||||||
|  |     try std.testing.expect(!sut.isConnected(1, 2)); | ||||||
|  |     try std.testing.expect(!sut.isConnected(2, 1)); | ||||||
|  | } | ||||||
|  | test "make new connection" { | ||||||
|  |     const Sut = Container(u8, lessThanFuncFor(u8)); | ||||||
|  |  | ||||||
|  |     var sut = try Sut.init(std.testing.allocator, &.{}); | ||||||
|  |     defer sut.deinit(); | ||||||
|  |  | ||||||
|  |     try std.testing.expect(try sut.connectIf(2, 1)); | ||||||
|  |  | ||||||
|  |     try std.testing.expect(sut.isConnected(2, 1)); | ||||||
|  |     try std.testing.expect(!sut.isConnected(1, 2)); | ||||||
|  |  | ||||||
|  |     try sut.connect(3, 1); | ||||||
|  |  | ||||||
|  |     try std.testing.expect(sut.isConnected(3, 1)); | ||||||
|  |     try std.testing.expect(!sut.isConnected(1, 3)); | ||||||
|  | } | ||||||
|  | test "making an existing connection fails" { | ||||||
|  |     const Sut = Container(u8, lessThanFuncFor(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.connectIf(0, 1)); | ||||||
|  |     try std.testing.expectError(Sut.Error.AlreadyConnected, sut.connect(0, 1)); | ||||||
|  | } | ||||||
|  | test "disconnect an existing connection" { | ||||||
|  |     const Sut = Container(u8, lessThanFuncFor(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.disconnectIf(0, 1)); | ||||||
|  |     try std.testing.expect(!sut.isConnected(0, 1)); | ||||||
|  |  | ||||||
|  |     try sut.disconnect(2, 3); | ||||||
|  |     try std.testing.expect(!sut.isConnected(2, 3)); | ||||||
|  | } | ||||||
|  | test "disconnecting a missing connection fails" { | ||||||
|  |     const Sut = Container(u8, lessThanFuncFor(u8)); | ||||||
|  |  | ||||||
|  |     var sut = try Sut.init(std.testing.allocator, &.{}); | ||||||
|  |     defer sut.deinit(); | ||||||
|  |  | ||||||
|  |     try std.testing.expect(!sut.disconnectIf(0, 1)); | ||||||
|  |     try std.testing.expectError(Sut.Error.NotConnected, sut.disconnect(1, 0)); | ||||||
|  | } | ||||||
|  | test "chaotic operation" { | ||||||
|  |     const Sut = Container(u16, lessThanFuncFor(u16)); | ||||||
|  |  | ||||||
|  |     var sut = try Sut.init(std.testing.allocator, &.{}); | ||||||
|  |     defer sut.deinit(); | ||||||
|  |  | ||||||
|  |     const N = 100; | ||||||
|  |     const P = 109; | ||||||
|  |     const Q = 113; | ||||||
|  |     const R = 127; | ||||||
|  |     for (0..N) |v| { | ||||||
|  |         const x: Sut.Node = @intCast(v); | ||||||
|  |         try sut.connect((x*Q)%P, (x*R)%P); | ||||||
|  |     } | ||||||
|  |     for (N/2..N) |v| { | ||||||
|  |         const x: Sut.Node = @intCast(v); | ||||||
|  |         try sut.disconnect((x*Q)%P, (x*R)%P); | ||||||
|  |     } | ||||||
|  |     for (0..N/2) |v| { | ||||||
|  |         const x: Sut.Node = @intCast(v); | ||||||
|  |         try std.testing.expect(sut.isConnected((x*Q)%P, (x*R)%P)); | ||||||
|  |     } | ||||||
|  |     for (N/2..N) |v| { | ||||||
|  |         const x: Sut.Node = @intCast(v); | ||||||
|  |         try std.testing.expect(!sut.isConnected((x*Q)%P, (x*R)%P)); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										71
									
								
								src/hncore/Merged.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/hncore/Merged.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | |||||||
|  | const std = @import("std"); | ||||||
|  |  | ||||||
|  | /// Returns a type having fields that type A and type B have. | ||||||
|  | /// Fields from B are more preferred than from A in case that field names are duplicated. | ||||||
|  | pub fn Merged(comptime A: type, comptime B: type) type { | ||||||
|  |     const at = @typeInfo(A).@"struct"; | ||||||
|  |     const bt = @typeInfo(B).@"struct"; | ||||||
|  |  | ||||||
|  |     return @Type(.{ | ||||||
|  |         .@"struct" = .{ | ||||||
|  |             .layout   = .auto, | ||||||
|  |             .fields   = mergeFields(at.fields, bt.fields), | ||||||
|  |             .is_tuple = false, | ||||||
|  |             .decls    = &.{}, | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | fn mergeFields( | ||||||
|  |     comptime a: []const std.builtin.Type.StructField, | ||||||
|  |     comptime b: []const std.builtin.Type.StructField) []std.builtin.Type.StructField { | ||||||
|  |     var ret: [a.len+b.len]std.builtin.Type.StructField = undefined; | ||||||
|  |     var len: usize = 0; | ||||||
|  |  | ||||||
|  |     inline for (b) |f| { | ||||||
|  |         ret[len] = f; | ||||||
|  |         ret[len].is_comptime = false; | ||||||
|  |         len += 1; | ||||||
|  |     } | ||||||
|  |     inline for (a) |f| { | ||||||
|  |         inline for (b) |fb| { | ||||||
|  |             if (std.mem.eql(u8, f.name, fb.name)) { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             ret[len] = f; | ||||||
|  |             ret[len].is_comptime = false; | ||||||
|  |             len += 1; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return ret[0..len]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Same to `Merged(@TypeOf(a), @TypeOf(b))` but returns a merged value, not type. | ||||||
|  | pub fn merge(a: anytype, b: anytype) Merged(@TypeOf(a), @TypeOf(b)) { | ||||||
|  |     var ret: Merged(@TypeOf(a), @TypeOf(b)) = undefined; | ||||||
|  |  | ||||||
|  |     const af = @typeInfo(@TypeOf(a)).@"struct".fields; | ||||||
|  |     const bf = @typeInfo(@TypeOf(b)).@"struct".fields; | ||||||
|  |  | ||||||
|  |     inline for (bf) |f| { | ||||||
|  |         @field(ret, f.name) = @field(b, f.name); | ||||||
|  |     } | ||||||
|  |     inline for (af) |f| { | ||||||
|  |         inline for (bf) |fb| { | ||||||
|  |             if (std.mem.eql(u8, f.name, fb.name)) { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             @field(ret, f.name) = @field(a, f.name); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return ret; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | test "merging 2 structs" { | ||||||
|  |     const merged = merge(.{.a = 0, .b = 1, .c = 2,}, .{.b = 44, .d = 53,}); | ||||||
|  |     try std.testing.expect(merged.a == 0); | ||||||
|  |     try std.testing.expect(merged.b == 44); | ||||||
|  |     try std.testing.expect(merged.c == 2); | ||||||
|  |     try std.testing.expect(merged.d == 53); | ||||||
|  | } | ||||||
							
								
								
									
										76
									
								
								src/hncore/Mindmap.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/hncore/Mindmap.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | const std = @import("std"); | ||||||
|  |  | ||||||
|  | const Digraph = @import("./Digraph.zig"); | ||||||
|  |  | ||||||
|  | pub const Node        = @import("./Node.zig"); | ||||||
|  | pub const NodeList    = std.ArrayList(*Node); | ||||||
|  | pub const NodeDigraph = Digraph.Container(*const Node, Digraph.lessThanFuncFor(*const Node)); | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | alloc: std.mem.Allocator, | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | nodes: NodeList, | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | digraph: NodeDigraph, | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | root: *Node, | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | pub fn init(alloc: std.mem.Allocator) !@This() { | ||||||
|  |     var nodes = NodeList.init(alloc); | ||||||
|  |     errdefer nodes.deinit(); | ||||||
|  |  | ||||||
|  |     var digraph = try NodeDigraph.init(alloc, &.{}); | ||||||
|  |     errdefer digraph.deinit(); | ||||||
|  |  | ||||||
|  |     var root = try alloc.create(Node); | ||||||
|  |     errdefer alloc.destroy(root); | ||||||
|  |  | ||||||
|  |     root.* = try Node.init(alloc, 0, "helloworld"); | ||||||
|  |     errdefer root.deinit(alloc); | ||||||
|  |  | ||||||
|  |     try nodes.append(root); | ||||||
|  |  | ||||||
|  |     var node1 = try alloc.create(Node); | ||||||
|  |     errdefer alloc.destroy(node1); | ||||||
|  |     node1.* = try Node.init(alloc, 1, "node1"); | ||||||
|  |     errdefer node1.deinit(alloc); | ||||||
|  |     try nodes.append(node1); | ||||||
|  |     try digraph.connect(root, node1); | ||||||
|  |  | ||||||
|  |     var node2 = try alloc.create(Node); | ||||||
|  |     errdefer alloc.destroy(node2); | ||||||
|  |     node2.* = try Node.init(alloc, 2, "node1"); | ||||||
|  |     errdefer node2.deinit(alloc); | ||||||
|  |     try nodes.append(node2); | ||||||
|  |     try digraph.connect(root, node2); | ||||||
|  |  | ||||||
|  |     return .{ | ||||||
|  |         .alloc   = alloc, | ||||||
|  |         .nodes   = nodes, | ||||||
|  |         .digraph = digraph, | ||||||
|  |         .root    = root, | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | /// | ||||||
|  | pub fn deinit(self: *@This()) void { | ||||||
|  |     for (self.nodes.items) |node| { | ||||||
|  |         node.deinit(self.alloc); | ||||||
|  |         self.alloc.destroy(node); | ||||||
|  |     } | ||||||
|  |     self.digraph.deinit(); | ||||||
|  |     self.nodes.deinit(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | pub fn serialize(self: *const @This(), writer: anytype) !void { | ||||||
|  |     _ = self; | ||||||
|  |     _ = writer; | ||||||
|  | } | ||||||
|  | /// | ||||||
|  | pub fn deserialize(reader: anytype) !@This() { | ||||||
|  |     _ = reader; | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								src/hncore/Node.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/hncore/Node.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | const std = @import("std"); | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | /// unique and immutable integer | ||||||
|  | id: usize, | ||||||
|  |  | ||||||
|  | /// summary text of this node | ||||||
|  | summary: []const u8, | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | pub fn init(alloc: std.mem.Allocator, id: usize, summary: []const u8) !@This() { | ||||||
|  |     return .{ | ||||||
|  |         .id        = id, | ||||||
|  |         .summary   = try alloc.dupe(u8, summary), | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | /// pass the same allocator as init() call | ||||||
|  | pub fn deinit(self: *@This(), alloc: std.mem.Allocator) void { | ||||||
|  |     alloc.free(self.summary); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | test "serialize" { | ||||||
|  |     const alloc = std.testing.allocator; | ||||||
|  |     var   node  = try init(alloc, 0, "helloworld"); | ||||||
|  |     defer node.deinit(alloc); | ||||||
|  |  | ||||||
|  |     var json = std.ArrayList(u8).init(alloc); | ||||||
|  |     defer json.deinit(); | ||||||
|  |     try std.json.stringify(node, .{}, json.writer()); | ||||||
|  |  | ||||||
|  |     try std.testing.expectEqualStrings( | ||||||
|  |         json.items, | ||||||
|  |         \\{"id":0,"summary":"helloworld"} | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								src/hncore/Project.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/hncore/Project.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | const std = @import("std"); | ||||||
|  |  | ||||||
|  | const Mindmap = @import("./Mindmap.zig"); | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | mindmap: Mindmap, | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | pub fn init(alloc: std.mem.Allocator) !@This() { | ||||||
|  |     return .{ | ||||||
|  |         .mindmap = try .init(alloc), | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | pub fn deinit(self: *@This()) void { | ||||||
|  |     self.mindmap.deinit(); | ||||||
|  | } | ||||||
							
								
								
									
										210
									
								
								src/hncore/Store.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								src/hncore/Store.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | |||||||
|  | const std = @import("std"); | ||||||
|  |  | ||||||
|  | /// A container for all existing instances of T. | ||||||
|  | /// This is like a dedicated allocator for the type, T, with a refcount system. | ||||||
|  | pub fn Store(comptime T: type) type { | ||||||
|  |     return struct { | ||||||
|  |         /// | ||||||
|  |         pub const Item = T; | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         pub const VTable = struct { | ||||||
|  |             /// | ||||||
|  |             udata: ?*anyopaque = null, | ||||||
|  |  | ||||||
|  |             /// | ||||||
|  |             deinitItem: ?*fn (?*anyopaque, *T) void = null, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         /// A struct which holds an item and its reference count. | ||||||
|  |         pub const Slot = struct { | ||||||
|  |             item  : Item, | ||||||
|  |             refcnt: usize, | ||||||
|  |  | ||||||
|  |             pub fn ref(self: *@This()) void { | ||||||
|  |                 self.refcnt += 1; | ||||||
|  |             } | ||||||
|  |             pub fn unref(self: *@This()) void { | ||||||
|  |                 self.refcnt -= 1; | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         pub const Error = error { | ||||||
|  |             DetectedItemLeak, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         alloc : std.mem.Allocator, | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         vtable: VTable, | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         slots : std.ArrayList(*Slot), | ||||||
|  |  | ||||||
|  |         /// | ||||||
|  |         pub fn init(alloc: std.mem.Allocator, vtable: VTable) @This() { | ||||||
|  |             return .{ | ||||||
|  |                 .alloc  = alloc, | ||||||
|  |                 .vtable = vtable, | ||||||
|  |                 .slots  = std.ArrayList(*Slot).init(alloc), | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |         /// | ||||||
|  |         pub fn deinit(self: *@This()) !void { | ||||||
|  |             defer self.slots.deinit(); | ||||||
|  |  | ||||||
|  |             try self.collectGarbage(); | ||||||
|  |             if (self.slots.items.len > 0) { | ||||||
|  |                 return Error.DetectedItemLeak; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// Returns a slot which contains newly-allocated item. | ||||||
|  |         /// - A reference count of the returned slot is 1 | ||||||
|  |         /// - The returned slot and its contents are alive till the reference count becomes 0 | ||||||
|  |         pub fn add(self: *@This(), item: Item) !*Slot { | ||||||
|  |             for (self.slots.items) |slot| { | ||||||
|  |                 if (slot.refcnt == 0) { | ||||||
|  |                     self.deinitItem(&slot.item); | ||||||
|  |                     slot.* = .{ .item = item, .refcnt = 1, }; | ||||||
|  |                     return slot; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const newSlot = try self.alloc.create(Slot); | ||||||
|  |             errdefer self.alloc.destroy(newSlot); | ||||||
|  |  | ||||||
|  |             newSlot.* = .{ .item = item, .refcnt = 1, }; | ||||||
|  |             try self.slots.append(newSlot); | ||||||
|  |             return newSlot; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// Releases memory of slots whose reference count is 0. | ||||||
|  |         /// Usually no need to call this function because such slots will be reused. | ||||||
|  |         pub fn collectGarbage(self: *@This()) !void { | ||||||
|  |             var slots = self.slots.items; | ||||||
|  |  | ||||||
|  |             var set: usize = 0; | ||||||
|  |             var get: usize = 0; | ||||||
|  |             while (set < slots.len) { | ||||||
|  |                 if (slots[set].refcnt == 0) { | ||||||
|  |                     self.deinitItem(&slots[set].item); | ||||||
|  |                     self.alloc.destroy(slots[set]); | ||||||
|  |  | ||||||
|  |                     get += 1; | ||||||
|  |                     if (get >= slots.len) { | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                     slots[set] = slots[get]; | ||||||
|  |                 } else { | ||||||
|  |                     set += 1; | ||||||
|  |                     get += 1; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             try self.slots.resize(set); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         fn deinitItem(self: *@This(), item: *Item) void { | ||||||
|  |             if (self.vtable.deinitItem != null) { | ||||||
|  |                 self.vtable.deinitItem.?(self.vtable.udata, item); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | test "add new item" { | ||||||
|  |     const Item = u16; | ||||||
|  |  | ||||||
|  |     var sut = Store(Item).init(std.testing.allocator, .{}); | ||||||
|  |     defer sut.deinit() catch unreachable; | ||||||
|  |  | ||||||
|  |     { | ||||||
|  |         var slot = try sut.add(123); | ||||||
|  |         defer slot.unref(); | ||||||
|  |  | ||||||
|  |         // check initial value | ||||||
|  |         try std.testing.expect(slot.refcnt == 1); | ||||||
|  |         try std.testing.expect(slot.item   == 123); | ||||||
|  |  | ||||||
|  |         // check refcnt handling | ||||||
|  |         slot.ref(); | ||||||
|  |         try std.testing.expect(slot.refcnt == 2); | ||||||
|  |         slot.unref(); | ||||||
|  |         try std.testing.expect(slot.refcnt == 1); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | test "reuse an expired slot by adding right after removal" { | ||||||
|  |     const Item = u16; | ||||||
|  |  | ||||||
|  |     var sut = Store(Item).init(std.testing.allocator, .{}); | ||||||
|  |     defer sut.deinit() catch unreachable; | ||||||
|  |  | ||||||
|  |     var address: usize = undefined; | ||||||
|  |     { | ||||||
|  |         var slot = try sut.add(123); | ||||||
|  |         defer slot.unref(); | ||||||
|  |         address = @intFromPtr(slot); | ||||||
|  |         try std.testing.expect(slot.item == 123); | ||||||
|  |     } | ||||||
|  |     { | ||||||
|  |         var slot = try sut.add(456); | ||||||
|  |         defer slot.unref(); | ||||||
|  |         try std.testing.expect(address == @intFromPtr(slot)); | ||||||
|  |         try std.testing.expect(slot.item == 456); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | test "reusing doesn't happen by adding twice" { | ||||||
|  |     const Item = u16; | ||||||
|  |  | ||||||
|  |     var sut = Store(Item).init(std.testing.allocator, .{}); | ||||||
|  |     defer sut.deinit() catch unreachable; | ||||||
|  |  | ||||||
|  |     var slot1 = try sut.add(123); | ||||||
|  |     defer slot1.unref(); | ||||||
|  |  | ||||||
|  |     var slot2 = try sut.add(456); | ||||||
|  |     defer slot2.unref(); | ||||||
|  |  | ||||||
|  |     try std.testing.expect(slot1 != slot2); | ||||||
|  |     try std.testing.expect(slot1.item == 123); | ||||||
|  |     try std.testing.expect(slot2.item == 456); | ||||||
|  | } | ||||||
|  | test "chaotic addition and removals" { | ||||||
|  |     const Item = u16; | ||||||
|  |  | ||||||
|  |     var sut = Store(Item).init(std.testing.allocator, .{}); | ||||||
|  |     defer sut.deinit() catch unreachable; | ||||||
|  |  | ||||||
|  |     var slots = std.ArrayList(*Store(Item).Slot).init(std.testing.allocator); | ||||||
|  |     defer { | ||||||
|  |         for (slots.items) |slot| { slot.unref(); } | ||||||
|  |         slots.deinit(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // add 0~300 | ||||||
|  |     for (0..300) |i| { | ||||||
|  |         const x: u16 = @intCast(i); | ||||||
|  |         try slots.append(try sut.add(x)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // removes other than multiples of 3 | ||||||
|  |     for (0..100) |i| { | ||||||
|  |         slots.orderedRemove(300 - (i+1)*3 + 2).unref(); | ||||||
|  |         slots.orderedRemove(300 - (i+1)*3 + 1).unref(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // add multiples of 3 from 300 to 600 | ||||||
|  |     for (0..100) |i| { | ||||||
|  |         const x: u16 = @intCast(i); | ||||||
|  |         try slots.append(try sut.add(300 + x*3)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // check if the list is composed of multiples of 3 from 0~600 | ||||||
|  |     for (0..200) |i| { | ||||||
|  |         const x: u16 = @intCast(i); | ||||||
|  |         try std.testing.expectEqual(slots.items[i].item, x*3); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								src/hncore/root.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/hncore/root.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | pub const Mindmap = @import("./Mindmap.zig"); | ||||||
|  | pub const Node    = @import("./Node.zig"); | ||||||
|  | pub const Project = @import("./Project.zig"); | ||||||
|  |  | ||||||
|  | pub const merge  = @import("./Merged.zig").merge; | ||||||
|  |  | ||||||
|  | test { | ||||||
|  |     @import("std").testing.refAllDecls(@This()); | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								src/hnet/App.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/hnet/App.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | const std    = @import("std"); | ||||||
|  | const dvui   = @import("dvui"); | ||||||
|  | const hncore = @import("hncore"); | ||||||
|  |  | ||||||
|  | const ui = @import("./ui.zig"); | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | pub const App = struct { | ||||||
|  |     /// | ||||||
|  |     project: hncore.Project, | ||||||
|  |  | ||||||
|  |     /// | ||||||
|  |     pub fn init(alloc: std.mem.Allocator) !App { | ||||||
|  |         return App { | ||||||
|  |             .project = try .init(alloc), | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |     /// | ||||||
|  |     pub fn deinit(self: *App) void { | ||||||
|  |         self.project.deinit(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// | ||||||
|  |     pub fn gui(self: *App) !void { | ||||||
|  |         try self.guiMenu(); | ||||||
|  |         try self.guiMain(); | ||||||
|  |     } | ||||||
|  |     fn guiMenu(self: *@This()) !void { | ||||||
|  |         _ = self; | ||||||
|  |  | ||||||
|  |         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 guiMain(self: *@This()) !void { | ||||||
|  |         var overlay = try dvui.overlay(@src(), .{ | ||||||
|  |             .expand     = .both, | ||||||
|  |             .padding    = dvui.Rect.all(8), | ||||||
|  |             .color_fill = .{ .name = .fill_window }, | ||||||
|  |             .background = true, | ||||||
|  |         }); | ||||||
|  |         defer overlay.deinit(); | ||||||
|  |  | ||||||
|  |         try ui.manipulator(self.project.mindmap.root, &self.project.mindmap.digraph); | ||||||
|  |     } | ||||||
|  | }; | ||||||
							
								
								
									
										49
									
								
								src/hnet/main.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/hnet/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").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()); | ||||||
|  |     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), | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								src/hnet/ui.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/hnet/ui.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | const std    = @import("std"); | ||||||
|  | const dvui   = @import("dvui"); | ||||||
|  | const hncore = @import("hncore"); | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | pub fn manipulator(root: *const hncore.Mindmap.Node, digraph: *const hncore.Mindmap.NodeDigraph) !void { | ||||||
|  |     var overlay = try dvui.overlay(@src(), .{ | ||||||
|  |         .expand = .both, | ||||||
|  |     }); | ||||||
|  |     defer overlay.deinit(); | ||||||
|  |  | ||||||
|  |     _ = try nodeTreeInManipulator(.{ .x = 100, .y = 100, }, root, digraph); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | pub fn nodeTreeInManipulator(base: dvui.Point, node: *const hncore.Mindmap.Node, digraph: *const hncore.Mindmap.NodeDigraph) !dvui.Rect { | ||||||
|  |     const size = try nodeInManipulator(base, node); | ||||||
|  |     const childrenRect = nodeChildrenInManipulator( | ||||||
|  |         .{ .x = base.x + size.x, .y = base.y, }, node, digraph); | ||||||
|  |     return .{ | ||||||
|  |         .x = base.x, | ||||||
|  |         .y = base.y, | ||||||
|  |         .w = childrenRect.w + size.w, | ||||||
|  |         .h = @max(childrenRect.h, size.h), | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | pub fn nodeInManipulator(base: dvui.Point, node: *const hncore.Mindmap.Node) !dvui.Rect { | ||||||
|  |     var box = try dvui.box(@src(), .vertical, .{ | ||||||
|  |         .id_extra = node.id, | ||||||
|  |         .border   = .all(1), | ||||||
|  |         .rect     = .{ .x = base.x, .y = base.y, .w = 100, .h = 20, }, | ||||||
|  |     }); | ||||||
|  |     defer box.deinit(); | ||||||
|  |     return box.wd.borderRect(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// | ||||||
|  | pub fn nodeChildrenInManipulator(base: dvui.Point, node: *const hncore.Mindmap.Node, digraph: *const hncore.Mindmap.NodeDigraph) dvui.Rect { | ||||||
|  |     const parentMargin  = 16; | ||||||
|  |     const siblingMargin = 8; | ||||||
|  |  | ||||||
|  |     var childDepth : f32 = 0; | ||||||
|  |     var childOffset: f32 = 0; | ||||||
|  |     for (digraph.getChildrenOf(node)) |conn| { | ||||||
|  |         const childRect = nodeTreeInManipulator( | ||||||
|  |             .{ .x = base.x + parentMargin, .y = base.y + childOffset, }, conn.to, digraph) catch unreachable; | ||||||
|  |         childOffset += childRect.h + siblingMargin; | ||||||
|  |         childDepth   = @max(childRect.w, childDepth); | ||||||
|  |     } | ||||||
|  |     return .{ .x = base.x, .y = base.y, .w = childDepth, .h = childOffset, }; | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user