From 2b49398404979f9e53860fa96db7c693a43de32d Mon Sep 17 00:00:00 2001
From: zenith391 <39484230+zenith391@users.noreply.github.com>
Date: Wed, 14 Aug 2024 18:54:01 +0200
Subject: [PATCH] feat: add Dropdown component
---
build.zig | 2 -
src/backends/gtk/Dropdown.zig | 62 +++++++++++++++
src/backends/gtk/Window.zig | 7 --
src/backends/gtk/backend.zig | 7 +-
src/backends/win32/Dropdown.zig | 77 +++++++++++++++++++
src/backends/win32/backend.zig | 60 ++++++++++-----
src/backends/win32/res/manifest.xml | 4 +-
src/backends/win32/win32.zig | 9 +++
src/components/Dropdown.zig | 92 ++++++++++++++++++++++
src/data.zig | 114 ++++++++++++++++++++++------
src/internal.zig | 19 +++++
src/main.zig | 10 +++
src/timer.zig | 49 ++++++++----
13 files changed, 442 insertions(+), 70 deletions(-)
create mode 100644 src/backends/gtk/Dropdown.zig
create mode 100644 src/backends/win32/Dropdown.zig
create mode 100644 src/components/Dropdown.zig
diff --git a/build.zig b/build.zig
index 9b6f2b1b..babfaf0f 100644
--- a/build.zig
+++ b/build.zig
@@ -209,11 +209,9 @@ pub fn build(b: *std.Build) !void {
.install_dir = .prefix,
.install_subdir = "docs",
});
- install_docs.step.dependOn(&docs.step);
const docs_step = b.step("docs", "Generate documentation and run unit tests");
docs_step.dependOn(&install_docs.step);
- // docs_step.dependOn(run_docs);
b.getInstallStep().dependOn(&install_docs.step);
diff --git a/src/backends/gtk/Dropdown.zig b/src/backends/gtk/Dropdown.zig
new file mode 100644
index 00000000..19d8b72b
--- /dev/null
+++ b/src/backends/gtk/Dropdown.zig
@@ -0,0 +1,62 @@
+const std = @import("std");
+const c = @import("gtk.zig");
+const lib = @import("../../main.zig");
+const common = @import("common.zig");
+
+const Dropdown = @This();
+
+peer: *c.GtkWidget,
+owned_strings: ?[:null]const ?[*:0]const u8 = null,
+
+pub usingnamespace common.Events(Dropdown);
+
+fn gtkSelected(peer: *c.GtkWidget, userdata: usize) callconv(.C) void {
+ _ = userdata;
+ const data = common.getEventUserData(peer);
+
+ if (data.user.propertyChangeHandler) |handler| {
+ const index: usize = c.gtk_drop_down_get_selected(@ptrCast(peer));
+ handler("selected", &index, data.userdata);
+ }
+}
+
+pub fn create() common.BackendError!Dropdown {
+ const dropdown = c.gtk_drop_down_new_from_strings(null);
+ try Dropdown.setupEvents(dropdown);
+ _ = c.g_signal_connect_data(dropdown, "notify::selected", @as(c.GCallback, @ptrCast(>kSelected)), null, @as(c.GClosureNotify, null), 0);
+ return Dropdown{ .peer = dropdown };
+}
+
+pub fn getSelectedIndex(self: *const Dropdown) usize {
+ return c.gtk_drop_down_get_selected(@ptrCast(self.peer));
+}
+
+pub fn setSelectedIndex(self: *const Dropdown, index: usize) void {
+ c.gtk_drop_down_set_selected(@ptrCast(self.peer), @intCast(index));
+}
+
+pub fn setValues(self: *Dropdown, values: []const []const u8) void {
+ const allocator = lib.internal.lasting_allocator;
+ if (self.owned_strings) |strings| {
+ for (strings) |string| {
+ allocator.free(std.mem.span(string.?));
+ }
+ allocator.free(strings);
+ }
+
+ const duplicated = allocator.allocSentinel(?[*:0]const u8, values.len, null) catch return;
+ errdefer allocator.free(duplicated);
+ for (values, 0..) |value, i| {
+ const slice = allocator.dupeZ(u8, value) catch return;
+ duplicated[i] = slice.ptr;
+ }
+ self.owned_strings = duplicated;
+
+ const old_index = self.getSelectedIndex();
+ c.gtk_drop_down_set_model(@ptrCast(self.peer), @ptrCast(c.gtk_string_list_new(duplicated.ptr).?));
+ self.setSelectedIndex(old_index);
+}
+
+pub fn setEnabled(self: *const Dropdown, enabled: bool) void {
+ c.gtk_widget_set_sensitive(self.peer, @intFromBool(enabled));
+}
diff --git a/src/backends/gtk/Window.zig b/src/backends/gtk/Window.zig
index a9fc8c83..8e0ca819 100644
--- a/src/backends/gtk/Window.zig
+++ b/src/backends/gtk/Window.zig
@@ -10,7 +10,6 @@ const wbin_new = @import("windowbin.zig").wbin_new;
const wbin_set_child = @import("windowbin.zig").wbin_set_child;
// === GLOBAL VARIABLES ===
-pub var activeWindows = std.atomic.Value(usize).init(0);
pub var randomWindow: *c.GtkWidget = undefined;
// === END GLOBAL VARIABLES ===
@@ -27,10 +26,6 @@ child: ?*c.GtkWidget = null,
pub usingnamespace common.Events(Window);
-fn gtkWindowHidden(_: *c.GtkWidget, _: usize) callconv(.C) void {
- _ = activeWindows.fetchSub(1, .release);
-}
-
pub fn create() common.BackendError!Window {
const window = c.gtk_window_new() orelse return error.UnknownError;
const wbin = wbin_new() orelse unreachable;
@@ -44,7 +39,6 @@ pub fn create() common.BackendError!Window {
c.gtk_widget_show(window);
c.gtk_widget_map(window);
- _ = c.g_signal_connect_data(window, "hide", @as(c.GCallback, @ptrCast(>kWindowHidden)), null, null, c.G_CONNECT_AFTER);
randomWindow = window;
try Window.setupEvents(window);
@@ -168,7 +162,6 @@ pub fn unfullscreen(self: *Window) void {
pub fn show(self: *Window) void {
c.gtk_widget_show(self.peer);
- _ = activeWindows.fetchAdd(1, .release);
}
pub fn registerTickCallback(self: *Window) void {
diff --git a/src/backends/gtk/backend.zig b/src/backends/gtk/backend.zig
index 6113b8df..96dbaa4c 100644
--- a/src/backends/gtk/backend.zig
+++ b/src/backends/gtk/backend.zig
@@ -62,6 +62,7 @@ pub const Monitor = @import("Monitor.zig");
pub const Window = @import("Window.zig");
pub const Button = @import("Button.zig");
pub const CheckBox = @import("CheckBox.zig");
+pub const Dropdown = @import("Dropdown.zig");
pub const Slider = @import("Slider.zig");
pub const Label = @import("Label.zig");
pub const TextArea = @import("TextArea.zig");
@@ -91,9 +92,5 @@ pub fn runStep(step: shared.EventLoopStep) bool {
const context = c.g_main_context_default();
_ = c.g_main_context_iteration(context, @intFromBool(step == .Blocking));
- if (GTK_VERSION.min.order(.{ .major = 4, .minor = 0, .patch = 0 }) != .lt) {
- return c.g_list_model_get_n_items(c.gtk_window_get_toplevels()) > 0;
- } else {
- return Window.activeWindows.load(.acquire) != 0;
- }
+ return c.g_list_model_get_n_items(c.gtk_window_get_toplevels()) > 0;
}
diff --git a/src/backends/win32/Dropdown.zig b/src/backends/win32/Dropdown.zig
new file mode 100644
index 00000000..3cb1d684
--- /dev/null
+++ b/src/backends/win32/Dropdown.zig
@@ -0,0 +1,77 @@
+const std = @import("std");
+const lib = @import("../../main.zig");
+
+const win32Backend = @import("win32.zig");
+const zigwin32 = @import("zigwin32");
+const win32 = zigwin32.everything;
+const Events = @import("backend.zig").Events;
+const getEventUserData = @import("backend.zig").getEventUserData;
+const _T = zigwin32.zig._T;
+const L = zigwin32.zig.L;
+
+const Dropdown = @This();
+
+peer: win32.HWND,
+arena: std.heap.ArenaAllocator,
+owned_strings: ?[:null]const ?[*:0]const u16 = null,
+
+pub usingnamespace Events(Dropdown);
+
+pub fn create() !Dropdown {
+ const hwnd = win32.CreateWindowExW(win32.WS_EX_LEFT, // dwExtStyle
+ _T("COMBOBOX"), // lpClassName
+ _T(""), // lpWindowName
+ @as(win32.WINDOW_STYLE, @enumFromInt(@intFromEnum(win32.WS_TABSTOP) | @intFromEnum(win32.WS_CHILD) | @intFromEnum(win32.WS_BORDER) | win32.CBS_DROPDOWNLIST | win32.CBS_HASSTRINGS)), // dwStyle
+ 0, // X
+ 0, // Y
+ 100, // nWidth
+ 400, // nHeight
+ @import("backend.zig").defaultWHWND, // hWindParent
+ null, // hMenu
+ @import("backend.zig").hInst, // hInstance
+ null // lpParam
+ ) orelse return @import("backend.zig").Win32Error.InitializationError;
+ try Dropdown.setupEvents(hwnd);
+ _ = win32.SendMessageW(hwnd, win32.WM_SETFONT, @intFromPtr(@import("backend.zig").captionFont), 1);
+
+ getEventUserData(hwnd).extra_height = 500;
+
+ return Dropdown{ .peer = hwnd, .arena = std.heap.ArenaAllocator.init(lib.internal.lasting_allocator) };
+}
+
+pub fn getSelectedIndex(self: *const Dropdown) usize {
+ const result = win32.SendMessageW(self.peer, win32.CB_GETCURSEL, 0, 0);
+ return if (result != win32.CB_ERR) @intCast(result) else 0;
+}
+
+pub fn setSelectedIndex(self: *const Dropdown, index: usize) void {
+ _ = win32.SendMessageW(self.peer, win32.CB_SETCURSEL, index, 0);
+}
+
+pub fn setValues(self: *Dropdown, values: []const []const u8) void {
+ // Remove previous values
+ const old_index = self.getSelectedIndex();
+ _ = win32.SendMessageW(self.peer, win32.CB_RESETCONTENT, 0, 0);
+
+ const allocator = lib.internal.lasting_allocator;
+ if (self.owned_strings) |strings| {
+ for (strings) |string| {
+ allocator.free(std.mem.span(string.?));
+ }
+ allocator.free(strings);
+ }
+
+ const duplicated = allocator.allocSentinel(?[*:0]const u16, values.len, null) catch return;
+ errdefer allocator.free(duplicated);
+ for (values, 0..) |value, i| {
+ const utf16 = std.unicode.utf8ToUtf16LeWithNull(allocator, value) catch return;
+ duplicated[i] = utf16.ptr;
+ std.debug.assert(win32.SendMessageW(self.peer, win32.CB_ADDSTRING, 0, @bitCast(@intFromPtr(utf16.ptr))) != win32.CB_ERR);
+ }
+ self.owned_strings = duplicated;
+ self.setSelectedIndex(old_index);
+}
+
+pub fn setEnabled(self: *Dropdown, enabled: bool) void {
+ _ = win32.EnableWindow(self.peer, @intFromBool(enabled));
+}
diff --git a/src/backends/win32/backend.zig b/src/backends/win32/backend.zig
index dab2c182..545f724b 100644
--- a/src/backends/win32/backend.zig
+++ b/src/backends/win32/backend.zig
@@ -47,22 +47,22 @@ pub const TCIF_STATE = 0x0010;
const _T = zigwin32.zig._T;
const L = zigwin32.zig.L;
-const Win32Error = error{ UnknownError, InitializationError };
+pub const Win32Error = error{ UnknownError, InitializationError };
pub const Capabilities = .{ .useEventLoop = true };
pub const PeerType = HWND;
-var hInst: HINSTANCE = undefined;
+pub var hInst: HINSTANCE = undefined;
/// By default, win32 controls use DEFAULT_GUI_FONT which is an outdated
/// font from Windows 95 days, by default it doesn't even use ClearType
/// anti-aliasing. So we take the real default caption font from
/// NONFCLIENTEMETRICS and apply it manually to every widget.
-var captionFont: win32.HFONT = undefined;
-var monospaceFont: win32.HFONT = undefined;
+pub var captionFont: win32.HFONT = undefined;
+pub var monospaceFont: win32.HFONT = undefined;
/// Default arrow cursor used to avoid components keeping the last cursor icon
/// that's been set (which is usually the resize cursor or loading cursor)
-var defaultCursor: win32.HCURSOR = undefined;
+pub var defaultCursor: win32.HCURSOR = undefined;
var d2dFactory: *win32.ID2D1Factory = undefined;
@@ -87,7 +87,7 @@ pub fn init() !void {
const initEx = win32.INITCOMMONCONTROLSEX{
.dwSize = @sizeOf(win32.INITCOMMONCONTROLSEX),
- .dwICC = win32.INITCOMMONCONTROLSEX_ICC.initFlags(.{ .STANDARD_CLASSES = 1, .WIN95_CLASSES = 1 }),
+ .dwICC = win32.INITCOMMONCONTROLSEX_ICC.initFlags(.{ .STANDARD_CLASSES = 1, .WIN95_CLASSES = 1, .USEREX_CLASSES = 1 }),
};
const code = win32.InitCommonControlsEx(&initEx);
if (code == 0) {
@@ -161,7 +161,7 @@ pub fn showNativeMessageDialog(msgType: MessageType, comptime fmt: []const u8, a
_ = win32.MessageBoxW(null, msg_utf16, _T("Dialog"), icon);
}
-var defaultWHWND: HWND = undefined;
+pub var defaultWHWND: HWND = undefined;
pub const Window = struct {
hwnd: HWND,
@@ -428,9 +428,10 @@ const EventUserData = struct {
classUserdata: usize = 0,
// (very) weak method to detect if a text box's text has actually changed
last_text_len: std.os.windows.INT = 0,
+ extra_height: i32 = 0,
};
-inline fn getEventUserData(peer: HWND) *EventUserData {
+pub inline fn getEventUserData(peer: HWND) *EventUserData {
return @as(*EventUserData, @ptrFromInt(@as(usize, @bitCast(win32Backend.getWindowLongPtr(peer, win32.GWL_USERDATA)))));
}
@@ -468,6 +469,16 @@ pub fn Events(comptime T: type) type {
if (data.user.changedTextHandler) |handler|
handler(data.userdata);
},
+ win32.CBN_SELCHANGE => {
+ const index: usize = @intCast(win32.SendMessageW(
+ @ptrFromInt(@as(usize, @bitCast(lp))),
+ win32.CB_GETCURSEL,
+ 0,
+ 0,
+ ));
+ if (data.user.propertyChangeHandler) |handler|
+ handler("selected", &index, data.userdata);
+ },
else => {},
}
}
@@ -711,9 +722,10 @@ pub fn Events(comptime T: type) type {
}
pub fn getHeight(self: *const T) c_int {
+ const data = getEventUserData(self.peer);
var rect: RECT = undefined;
_ = win32.GetWindowRect(self.peer, &rect);
- return rect.bottom - rect.top;
+ return rect.bottom - rect.top -| data.extra_height;
}
pub fn getPreferredSize(self: *const T) lib.Size {
@@ -944,7 +956,8 @@ pub const Canvas = struct {
pub const TextField = struct {
peer: HWND,
- arena: std.heap.ArenaAllocator,
+ /// Cache of the text field's text converted to UTF-8
+ text_utf8: std.ArrayList(u8) = std.ArrayList(u8).init(lib.internal.lasting_allocator),
pub usingnamespace Events(TextField);
@@ -965,7 +978,7 @@ pub const TextField = struct {
try TextField.setupEvents(hwnd);
_ = win32.SendMessageW(hwnd, win32.WM_SETFONT, @intFromPtr(captionFont), 1);
- return TextField{ .peer = hwnd, .arena = std.heap.ArenaAllocator.init(lib.internal.lasting_allocator) };
+ return TextField{ .peer = hwnd };
}
pub fn setText(self: *TextField, text: []const u8) void {
@@ -981,14 +994,16 @@ pub const TextField = struct {
}
pub fn getText(self: *TextField) [:0]const u8 {
- const allocator = self.arena.allocator();
const len = win32.GetWindowTextLengthW(self.peer);
- var buf = allocator.allocSentinel(u16, @as(usize, @intCast(len)), 0) catch unreachable; // TODO return error
- defer allocator.free(buf);
+ var buf = lib.internal.scratch_allocator.allocSentinel(u16, @as(usize, @intCast(len)), 0) catch unreachable; // TODO return error
+ defer lib.internal.scratch_allocator.free(buf);
const realLen = @as(usize, @intCast(win32.GetWindowTextW(self.peer, buf.ptr, len + 1)));
const utf16Slice = buf[0..realLen];
- const text = std.unicode.utf16leToUtf8AllocZ(allocator, utf16Slice) catch unreachable; // TODO return error
- return text;
+
+ self.text_utf8.clearAndFree();
+ std.unicode.utf16LeToUtf8ArrayList(&self.text_utf8, utf16Slice) catch @panic("OOM");
+ self.text_utf8.append(0) catch @panic("OOM");
+ return self.text_utf8.items[0 .. self.text_utf8.items.len - 1 :0];
}
pub fn setReadOnly(self: *TextField, readOnly: bool) void {
@@ -1109,6 +1124,8 @@ pub const Button = struct {
}
};
+pub const Dropdown = @import("Dropdown.zig");
+
pub const CheckBox = struct {
peer: HWND,
arena: std.heap.ArenaAllocator,
@@ -1625,6 +1642,8 @@ pub const Container = struct {
}
pub fn resize(self: *const Container, peer: PeerType, width: u32, height: u32) void {
+ const data = getEventUserData(peer);
+
var rect: RECT = undefined;
_ = win32.GetWindowRect(peer, &rect);
if (rect.right - rect.left == width and rect.bottom - rect.top == height) {
@@ -1633,7 +1652,14 @@ pub const Container = struct {
var parent: RECT = undefined;
_ = win32.GetWindowRect(self.peer, &parent);
- _ = win32.MoveWindow(peer, rect.left - parent.left, rect.top - parent.top, @as(c_int, @intCast(width)), @as(c_int, @intCast(height)), 1);
+ _ = win32.MoveWindow(
+ peer,
+ rect.left - parent.left,
+ rect.top - parent.top,
+ @as(c_int, @intCast(width)),
+ @as(c_int, @intCast(height)) + data.extra_height,
+ 1,
+ );
}
/// In order to work, 'peers' should contain all peers and be sorted in tab order
diff --git a/src/backends/win32/res/manifest.xml b/src/backends/win32/res/manifest.xml
index 25b7aee7..b02ac9e7 100644
--- a/src/backends/win32/res/manifest.xml
+++ b/src/backends/win32/res/manifest.xml
@@ -23,7 +23,7 @@
-
+
@@ -39,4 +39,4 @@
/>
-
\ No newline at end of file
+
diff --git a/src/backends/win32/win32.zig b/src/backends/win32/win32.zig
index e8869b49..a3dc2925 100644
--- a/src/backends/win32/win32.zig
+++ b/src/backends/win32/win32.zig
@@ -55,6 +55,15 @@ pub const SS_CENTER = 0x00000001;
/// Centers text vertically.
pub const SS_CENTERIMAGE = 0x00000200;
+// COMBOBOXEX controls
+pub const CBS_SIMPLE = 0x0001;
+pub const CBS_DROPDOWN = 0x0002;
+pub const CBS_DROPDOWNLIST = 0x0003;
+pub const CBS_HASSTRINGS = 0x0200;
+pub const CB_GETCURSEL = 0x0147;
+pub const CB_SETCURSEL = 0x014E;
+pub const CB_ERR: LRESULT = -1;
+
pub const SWP_NOACTIVATE = 0x0010;
pub const SWP_NOOWNERZORDER = 0x0200;
pub const SWP_NOZORDER = 0x0004;
diff --git a/src/components/Dropdown.zig b/src/components/Dropdown.zig
new file mode 100644
index 00000000..2ed74533
--- /dev/null
+++ b/src/components/Dropdown.zig
@@ -0,0 +1,92 @@
+const std = @import("std");
+const backend = @import("../backend.zig");
+const internal = @import("../internal.zig");
+const Size = @import("../data.zig").Size;
+const ListAtom = @import("../data.zig").ListAtom;
+const Atom = @import("../data.zig").Atom;
+
+/// A dropdown to select a value.
+pub const Dropdown = struct {
+ pub usingnamespace @import("../internal.zig").All(Dropdown);
+
+ peer: ?backend.Dropdown = null,
+ widget_data: Dropdown.WidgetData = .{},
+ /// The list of values that the user can select in the dropdown.
+ /// The strings are owned by the caller.
+ values: ListAtom([]const u8),
+ /// Whether the user can interact with the button, that is
+ /// whether the button can be pressed or not.
+ enabled: Atom(bool) = Atom(bool).of(true),
+ selected_index: Atom(usize) = Atom(usize).of(0),
+ // TODO: exclude of Dropdown.Config
+ /// This is a read-only property.
+ selected_value: Atom([]const u8) = Atom([]const u8).of(""),
+
+ pub fn init(config: Dropdown.Config) Dropdown {
+ var component = Dropdown.init_events(Dropdown{
+ .values = ListAtom([]const u8).init(internal.lasting_allocator),
+ });
+ internal.applyConfigStruct(&component, config);
+ // TODO: self.selected_value.dependOn(&.{ self.values, self.selected_index })
+ return component;
+ }
+
+ fn onEnabledAtomChange(newValue: bool, userdata: ?*anyopaque) void {
+ const self: *Dropdown = @ptrCast(@alignCast(userdata));
+ self.peer.?.setEnabled(newValue);
+ }
+
+ fn onSelectedIndexAtomChange(newValue: usize, userdata: ?*anyopaque) void {
+ const self: *Dropdown = @ptrCast(@alignCast(userdata));
+ self.peer.?.setSelectedIndex(newValue);
+ self.selected_value.set(self.values.get(newValue));
+ }
+
+ fn onValuesChange(list: *ListAtom([]const u8), userdata: ?*anyopaque) void {
+ const self: *Dropdown = @ptrCast(@alignCast(userdata));
+ self.selected_value.set(list.get(self.selected_index.get()));
+ var iterator = list.iterate();
+ defer iterator.deinit();
+ self.peer.?.setValues(iterator.getSlice());
+ }
+
+ fn onPropertyChange(self: *Dropdown, property_name: []const u8, new_value: *const anyopaque) !void {
+ if (std.mem.eql(u8, property_name, "selected")) {
+ const value: *const usize = @ptrCast(@alignCast(new_value));
+ self.selected_index.set(value.*);
+ }
+ }
+
+ pub fn show(self: *Dropdown) !void {
+ if (self.peer == null) {
+ var peer = try backend.Dropdown.create();
+ peer.setEnabled(self.enabled.get());
+ {
+ var iterator = self.values.iterate();
+ defer iterator.deinit();
+ peer.setValues(iterator.getSlice());
+ }
+ self.selected_value.set(self.values.get(self.selected_index.get()));
+ peer.setSelectedIndex(self.selected_index.get());
+ self.peer = peer;
+ try self.setupEvents();
+ _ = try self.enabled.addChangeListener(.{ .function = onEnabledAtomChange, .userdata = self });
+ _ = try self.selected_index.addChangeListener(.{ .function = onSelectedIndexAtomChange, .userdata = self });
+ _ = try self.values.addChangeListener(.{ .function = onValuesChange, .userdata = self });
+ try self.addPropertyChangeHandler(&onPropertyChange);
+ }
+ }
+
+ pub fn getPreferredSize(self: *Dropdown, available: Size) Size {
+ _ = available;
+ if (self.peer) |peer| {
+ return peer.getPreferredSize();
+ } else {
+ return Size{ .width = 100.0, .height = 40.0 };
+ }
+ }
+};
+
+pub fn dropdown(config: Dropdown.Config) *Dropdown {
+ return Dropdown.alloc(config);
+}
diff --git a/src/data.zig b/src/data.zig
index a7af142b..974bb879 100644
--- a/src/data.zig
+++ b/src/data.zig
@@ -101,6 +101,12 @@ pub fn isAtom(comptime T: type) bool {
return @hasDecl(T, "ValueType") and T == Atom(T.ValueType);
}
+pub fn isListAtom(comptime T: type) bool {
+ if (!comptime trait.is(.Struct)(T))
+ return false;
+ return @hasDecl(T, "ValueType") and T == ListAtom(T.ValueType);
+}
+
// TODO: use ListAtom when it's done
pub var _animatedAtoms = std.ArrayList(struct {
fnPtr: *const fn (data: *anyopaque) bool,
@@ -119,7 +125,7 @@ fn isAnimatableType(comptime T: type) bool {
}
fn isPointer(comptime T: type) bool {
- return @typeInfo(T) == .Pointer;
+ return @typeInfo(T) == .Pointer and std.meta.activeTag(@typeInfo(std.meta.Child(T))) != .Fn;
}
/// An atom is used to add binding, change listening, thread safety and animation capabilities to
@@ -665,16 +671,26 @@ pub fn ListAtom(comptime T: type) type {
length: Atom(usize),
// TODO: since RwLock doesn't report deadlocks in Debug mode like Mutex does, do it manually here in ListAtom
lock: std.Thread.RwLock = .{},
+ /// List of every change listener listening to this atom.
+ onChange: ChangeListenerList = .{},
allocator: std.mem.Allocator,
+ pub const ValueType = T;
const Self = @This();
const ListType = std.ArrayListUnmanaged(T);
+ pub const ChangeListener = struct {
+ function: *const fn (list: *Self, userdata: ?*anyopaque) void,
+ userdata: ?*anyopaque = null,
+ type: enum { Change, Destroy } = .Change,
+ };
+
+ const ChangeListenerList = std.SinglyLinkedList(ChangeListener);
+
// Possible events to be handled by ListAtom:
// - list size changed
// - an item in the list got replaced by another
- // TODO: a function to get a slice (or an iterator) but it also locks the list
pub const Iterator = struct {
lock: *std.Thread.RwLock,
items: []const T,
@@ -706,6 +722,8 @@ pub fn ListAtom(comptime T: type) type {
};
}
+ // TODO: init from list like .{ "a", "b", "c" }
+
pub fn get(self: *Self, index: usize) T {
self.lock.lockShared();
defer self.lock.unlockShared();
@@ -728,27 +746,37 @@ pub fn ListAtom(comptime T: type) type {
}
pub fn set(self: *Self, index: usize, value: T) void {
- self.lock.lock();
- defer self.lock.unlock();
+ {
+ self.lock.lock();
+ defer self.lock.unlock();
- self.backing_list.items[index] = value;
+ self.backing_list.items[index] = value;
+ }
+ self.callHandlers();
}
pub fn append(self: *Self, value: T) !void {
- self.lock.lock();
- defer self.lock.unlock();
- // Given that the length is updated only at the end, the operation doesn't need a lock
+ {
+ self.lock.lock();
+ defer self.lock.unlock();
+ // Given that the length is updated only at the end, the operation doesn't need a lock
- try self.backing_list.append(self.allocator, value);
- self.length.set(self.backing_list.items.len);
+ try self.backing_list.append(self.allocator, value);
+ self.length.set(self.backing_list.items.len);
+ }
+ self.callHandlers();
}
pub fn popOrNull(self: *Self) ?T {
- self.lock.lock();
- defer self.lock.unlock();
+ const result = blk: {
+ self.lock.lock();
+ defer self.lock.unlock();
- const result = self.backing_list.popOrNull();
- self.length.set(self.backing_list.items.len);
+ const result = self.backing_list.popOrNull();
+ self.length.set(self.backing_list.items.len);
+ break :blk result;
+ };
+ self.callHandlers();
return result;
}
@@ -763,27 +791,35 @@ pub fn ListAtom(comptime T: type) type {
const result = self.backing_list.swapRemove(index);
self.length.set(self.backing_list.items.len);
+ self.callHandlers();
return result;
}
pub fn orderedRemove(self: *Self, index: usize) T {
- self.lock.lock();
- defer self.lock.unlock();
+ const result = blk: {
+ self.lock.lock();
+ defer self.lock.unlock();
- const result = self.backing_list.orderedRemove(index);
- self.length.set(self.backing_list.items.len);
+ const result = self.backing_list.orderedRemove(index);
+ self.length.set(self.backing_list.items.len);
+ break :blk result;
+ };
+ self.callHandlers();
return result;
}
pub fn clear(self: *Self, mode: enum { free, retain_capacity }) void {
- self.lock.lock();
- defer self.lock.unlock();
+ {
+ self.lock.lock();
+ defer self.lock.unlock();
- switch (mode) {
- .free => self.backing_list.clearAndFree(self.allocator),
- .retain_capacity => self.backing_list.clearRetainingCapacity(),
+ switch (mode) {
+ .free => self.backing_list.clearAndFree(self.allocator),
+ .retain_capacity => self.backing_list.clearRetainingCapacity(),
+ }
+ self.length.set(0);
}
- self.length.set(0);
+ self.callHandlers();
}
/// Lock the list and return an iterator.
@@ -802,10 +838,40 @@ pub fn ListAtom(comptime T: type) type {
return undefined;
}
+ pub fn addChangeListener(self: *Self, listener: ChangeListener) !usize {
+ const node = try lasting_allocator.create(ChangeListenerList.Node);
+ node.* = .{ .data = listener };
+ self.onChange.prepend(node);
+ return self.onChange.len() - 1;
+ }
+
+ fn callHandlers(self: *Self) void {
+ // Iterate over each node of the linked list
+ var nullableNode = self.onChange.first;
+ while (nullableNode) |node| {
+ if (node.data.type == .Change) {
+ node.data.function(self, node.data.userdata);
+ }
+ nullableNode = node.next;
+ }
+ }
+
pub fn deinit(self: *Self) void {
self.lock.lock();
defer self.lock.unlock();
+ {
+ var nullableNode = self.onChange.first;
+ while (nullableNode) |node| {
+ nullableNode = node.next;
+ if (node.data.type == .Destroy) {
+ node.data.function(self, node.data.userdata);
+ }
+ lasting_allocator.destroy(node);
+ }
+ }
+
+ self.length.deinit();
self.backing_list.deinit(self.allocator);
}
};
diff --git a/src/internal.zig b/src/internal.zig
index 86cd63df..a42c334a 100644
--- a/src/internal.zig
+++ b/src/internal.zig
@@ -380,6 +380,19 @@ fn iterateFields(comptime config_fields: *[]const std.builtin.Type.StructField,
.is_comptime = false,
.alignment = @alignOf(FieldType.ValueType),
}};
+ } else if (dataStructures.isListAtom(FieldType)) {
+ // const default_value = if (field.default_value) |default| @as(*const FieldType, @ptrCast(@alignCast(default))).getUnsafe() else null;
+ const default_value = null;
+ // const has_default_value = field.default_value != null;
+ const has_default_value = false;
+
+ config_fields.* = config_fields.* ++ &[1]std.builtin.Type.StructField{.{
+ .name = field.name,
+ .type = []const FieldType.ValueType,
+ .default_value = if (has_default_value) @as(?*const anyopaque, @ptrCast(@alignCast(&default_value))) else null,
+ .is_comptime = false,
+ .alignment = @alignOf(FieldType.ValueType),
+ }};
} else if (comptime trait.is(.Struct)(FieldType)) {
iterateFields(config_fields, FieldType);
}
@@ -408,6 +421,12 @@ fn iterateApplyFields(comptime T: type, target: anytype, config: GenerateConfigS
@field(target, field.name).set(
@field(config, name),
);
+ } else if (comptime dataStructures.isListAtom(FieldType)) {
+ const name = field.name;
+ const value = @field(config, name);
+ for (value) |item| {
+ @field(target, field.name).append(item) catch @panic("OOM");
+ }
} else if (comptime trait.is(.Struct)(FieldType)) {
iterateApplyFields(T, &@field(target, field.name), config);
}
diff --git a/src/main.zig b/src/main.zig
index 72f6a4cb..10ab7cf5 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -7,6 +7,7 @@ pub usingnamespace @import("components/Alignment.zig");
pub usingnamespace @import("components/Button.zig");
pub usingnamespace @import("components/Canvas.zig");
pub usingnamespace @import("components/CheckBox.zig");
+pub usingnamespace @import("components/Dropdown.zig");
pub usingnamespace @import("components/Image.zig");
pub usingnamespace @import("components/Label.zig");
pub usingnamespace @import("components/Menu.zig");
@@ -69,6 +70,14 @@ pub fn init() !void {
return num >= 1;
}
}.a) catch unreachable;
+
+ var timerListener = eventStep.listen(.{ .callback = @import("timer.zig").handleTimersTick }) catch unreachable;
+ // The listener is enabled only if there is at least 1 atom currently being animated
+ timerListener.enabled.dependOn(.{&@import("timer.zig").runningTimers.length}, &struct {
+ fn a(num: usize) bool {
+ return num >= 1;
+ }
+ }.a) catch unreachable;
}
pub fn deinit() void {
@@ -76,6 +85,7 @@ pub fn deinit() void {
@import("data.zig")._animatedAtoms.deinit();
@import("data.zig")._animatedAtomsLength.deinit();
+ @import("timer.zig").runningTimers.deinit();
eventStep.deinitAllListeners();
if (ENABLE_DEV_TOOLS) {
diff --git a/src/timer.zig b/src/timer.zig
index c8ae05bf..4ad07193 100644
--- a/src/timer.zig
+++ b/src/timer.zig
@@ -2,42 +2,60 @@ const std = @import("std");
const internal = @import("internal.zig");
const lasting_allocator = internal.lasting_allocator;
const Atom = @import("data.zig").Atom;
+const ListAtom = @import("data.zig").ListAtom;
const EventSource = @import("listener.zig").EventSource;
-pub var _runningTimers = std.ArrayList(*Timer).init(lasting_allocator);
+pub var runningTimers = ListAtom(*Timer).init(internal.lasting_allocator);
-pub fn handleTimersTick() void {
+pub fn handleTimersTick(_: ?*anyopaque) void {
const now = std.time.Instant.now() catch unreachable;
- for (_runningTimers.items) |timer| {
+
+ var iterator = runningTimers.iterate();
+ defer iterator.deinit();
+ while (iterator.next()) |timer| {
if (now.since(timer.started.?) >= timer.duration.get()) {
timer.started = now;
timer.tick();
+ if (timer.single_shot) {
+ timer.stop();
+ }
}
}
}
pub const Timer = struct {
- single_shot: bool = false,
+ /// Whether the timer should only fire once.
+ single_shot: bool,
started: ?std.time.Instant = null,
- /// Duration in milliseconds
- duration: Atom(u64) = Atom(u64).of(0),
+ /// Duration in nanoseconds
+ /// Note that despite the fact that the duration is in nanoseconds, this does not mean
+ /// that a sub-millisecond precision is guarenteed.
+ duration: Atom(u64),
+ /// The event source corresponding to the timer. It is fired every time the timer triggers.
event_source: EventSource,
- pub fn init() !*Timer {
+ pub const Options = struct {
+ single_shot: bool,
+ /// Duration in nanoseconds
+ duration: u64,
+ };
+
+ pub fn init(options: Options) !*Timer {
const timer = try lasting_allocator.create(Timer);
timer.* = .{
+ .single_shot = options.single_shot,
+ .duration = Atom(u64).of(options.duration),
.event_source = EventSource.init(lasting_allocator),
};
return timer;
}
- pub fn start(self: *Timer, duration: u64) !void {
+ pub fn start(self: *Timer) !void {
if (self.started != null) {
return error.TimerAlreadyRunning;
}
self.started = try std.time.Instant.now();
- self.duration = Atom(u64).of(duration * std.time.ns_per_ms);
- try _runningTimers.append(self);
+ try runningTimers.append(self);
}
pub fn bindFrequency(self: *Timer, frequency: *Atom(f32)) !void {
@@ -56,8 +74,13 @@ pub const Timer = struct {
}
pub fn stop(self: *Timer) void {
- // TODO: make it atomic so as to avoid race conditions (or use a mutex)
- const index = std.mem.indexOfScalar(*Timer, _runningTimers.items, self) orelse return;
- std.debug.assert(_runningTimers.swapRemove(index) == self);
+ // TODO: make it atomic so as to avoid race conditions
+ const index = blk: {
+ var iterator = runningTimers.iterate();
+ defer iterator.deinit();
+
+ break :blk std.mem.indexOfScalar(*Timer, iterator.getSlice(), self) orelse return;
+ };
+ std.debug.assert(runningTimers.swapRemove(index) == self);
}
};