Skip to content

Commit

Permalink
feat: add Dropdown component
Browse files Browse the repository at this point in the history
  • Loading branch information
zenith391 committed Aug 14, 2024
1 parent 3b33ac5 commit 2b49398
Show file tree
Hide file tree
Showing 13 changed files with 442 additions and 70 deletions.
2 changes: 0 additions & 2 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
62 changes: 62 additions & 0 deletions src/backends/gtk/Dropdown.zig
Original file line number Diff line number Diff line change
@@ -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(&gtkSelected)), 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));
}
7 changes: 0 additions & 7 deletions src/backends/gtk/Window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===

Expand All @@ -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;
Expand All @@ -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(&gtkWindowHidden)), null, null, c.G_CONNECT_AFTER);
randomWindow = window;
try Window.setupEvents(window);

Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 2 additions & 5 deletions src/backends/gtk/backend.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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;
}
77 changes: 77 additions & 0 deletions src/backends/win32/Dropdown.zig
Original file line number Diff line number Diff line change
@@ -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));
}
60 changes: 43 additions & 17 deletions src/backends/win32/backend.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)))));
}

Expand Down Expand Up @@ -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 => {},
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);

Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -1109,6 +1124,8 @@ pub const Button = struct {
}
};

pub const Dropdown = @import("Dropdown.zig");

pub const CheckBox = struct {
peer: HWND,
arena: std.heap.ArenaAllocator,
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/backends/win32/res/manifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />

<!-- Windows 10 -->
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
Expand All @@ -39,4 +39,4 @@
/>
</dependentAssembly>
</dependency>
</assembly>
</assembly>
Loading

0 comments on commit 2b49398

Please sign in to comment.