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