1
0
Fork 0
zenolith/src/attreebute.zig
2023-11-26 14:01:22 +01:00

169 lines
6.3 KiB
Zig

/// This is a type-indexed map used for storing attreebutes
/// (inherited attributes in the widget tree).
///
/// Each widget may store an AttreebuteMap which will take precedence over it's parent's map
/// when a child widget is getting an attreebute. When resolving an attreebute, the widget tree
/// is walked toward the root node starting at the widget the attreebute is to be retrieved for,
/// checking all AttreebuteMaps until one containing the requested attreebute is found, otherwise
/// the attreebute is considered to be null.
///
/// Attreebutes which require a destructor should be avoided, and if necessary be removed manually
/// using remove prior to deinit. Due to the nature of it's implementation, the map can only call
/// destructors in remove and mod when overriding it, not in deinit.
const std = @import("std");
const util = @import("util.zig");
test {
_ = BoxStyle;
_ = ButtonStyle;
_ = CurrentFont;
}
pub const BoxStyle = @import("attreebutes/BoxStyle.zig");
pub const ButtonStyle = @import("attreebutes/ButtonStyle.zig");
pub const CurrentFont = @import("attreebutes/CurrentFont.zig");
pub const AttreebuteMap = struct {
const Context = struct {
pub fn hash(self: Context, t: u64) u64 {
_ = self;
return t;
}
pub fn eql(self: Context, a: u64, b: u64) bool {
_ = self;
return a == b;
}
};
/// An entry in the underlying map. Contains a slice to the entry's bytes as well as it's
/// alignment to make the allocator happy when freeing with unknown type.
const Entry = struct {
bytes: []u8,
log2_align: u8,
fn of(ptr: anytype) Entry {
return .{
.bytes = std.mem.asBytes(ptr),
.log2_align = std.math.log2_int(usize, @typeInfo(@TypeOf(ptr)).Pointer.alignment),
};
}
fn to(self: Entry, comptime T: type) *T {
std.debug.assert(std.math.log2_int(usize, @alignOf(T)) == self.log2_align);
return @alignCast(std.mem.bytesAsValue(T, self.bytes[0..@sizeOf(T)]));
}
};
// TODO: implement this without tricking a HashMap
const TypeHashMap = std.HashMapUnmanaged(u64, Entry, Context, std.hash_map.default_max_load_percentage);
/// The backing hash map. Be very careful here, if a given invariant is not held up, this will
/// lead to invalid pointer casts!
///
/// The key of this map is the type hash as returned by util.hashType and the value is a slice
/// consisting of a pointer to the data and it's size allocated on the map's allocator.
inner: TypeHashMap,
/// Creates a new AttreebuteMap. Caller owns returned memory and must free it by calling deinit.
pub fn init() AttreebuteMap {
return .{ .inner = TypeHashMap{} };
}
/// Frees all resources owned by this AttreebuteMap.
pub fn deinit(self: *AttreebuteMap, alloc: std.mem.Allocator) void {
var iter = self.inner.valueIterator();
while (iter.next()) |value| {
// We need to invoke rawFree here to keep the alignment consistent.
alloc.rawFree(
value.bytes,
value.log2_align,
@returnAddress(),
);
}
self.inner.deinit(alloc);
self.* = undefined;
}
/// Initialize an attreebute. If the key exists, the data behind it will be freed and it's
/// destructor will be called in accordance to util.deinitGeneric.
/// The returned value will be a pointer to the attreebute stored in the map. It remains valid
/// until removed or util the map is deinitialized.
/// The returned pointer points to uninitialized memory and must be set by the caller.
pub fn mod(self: *AttreebuteMap, alloc: std.mem.Allocator, comptime T: type) !*T {
const valp = try alloc.create(T);
errdefer alloc.destroy(valp);
if (try self.inner.fetchPut(alloc, util.hashType(T), Entry.of(valp))) |old| {
const old_val: *T = old.value.to(T);
util.deinitGeneric(old_val, alloc);
alloc.destroy(old_val);
}
return valp;
}
/// Retrieves the attreebute of the given type from this map, or returns null if not present.
/// The returned pointer remains valid as long as this map, unless the attreebute is removed,
/// in which case it is invalidated. If the attreebute is to be modified, the data behind the
/// return value is mutable.
///
/// The caller should NOT free the returned pointer!
pub fn get(self: AttreebuteMap, comptime T: type) ?*T {
return if (self.inner.get(util.hashType(T))) |val| val.to(T) else null;
}
/// If present, removes the given attreebute type from the map and returns true, otherwise
/// returns false. The allocator is required for deinitializing the attreebute. This will call
/// the attreebute's destructor in accordance with util.deinitGeneric.
pub fn remove(self: *AttreebuteMap, alloc: std.mem.Allocator, comptime T: type) bool {
if (self.inner.fetchRemove(util.hashType(T))) |old| {
const old_val: *T = old.value.to(T);
util.deinitGeneric(old_val, alloc);
alloc.destroy(old_val);
return true;
} else {
return false;
}
}
/// Returns true if the given attreebute type is present within the map, false otherwise.
///
/// If the attreebute value is required, the caller should prefer calling get
/// and handling a null return value instead.
pub fn has(self: AttreebuteMap, comptime T: type) bool {
return self.inner.contains(util.hashType(T));
}
};
test "basic type store" {
var map = AttreebuteMap.init();
defer map.deinit(std.testing.allocator);
var a_state: enum { start, init, deinit } = .start;
const A = struct {
state: *@TypeOf(a_state),
pub fn deinit(self: *@This()) void {
self.state.* = .deinit;
}
};
const a = try map.mod(std.testing.allocator, A);
a.* = .{ .state = &a_state };
map.get(A).?.state.* = .init;
try std.testing.expect(map.has(A));
try std.testing.expect(a.state.* == .init);
try std.testing.expectEqual(a_state, a.state.*);
_ = map.remove(std.testing.allocator, A);
try std.testing.expectEqual(a_state, .deinit);
_ = try map.mod(std.testing.allocator, u32);
}