From 4de0b92b8f4ffea2a49753623e568049a313353e Mon Sep 17 00:00:00 2001 From: zenith391 <39484230+zenith391@users.noreply.github.com> Date: Sun, 27 Oct 2024 21:06:28 +0100 Subject: [PATCH] wasm: split into multiple files and implement setEnabled --- src/backends/wasm/Button.zig | 51 +++ src/backends/wasm/Canvas.zig | 128 +++++++ src/backends/wasm/Container.zig | 50 +++ src/backends/wasm/Dropdown.zig | 20 + src/backends/wasm/ImageData.zig | 22 ++ src/backends/wasm/Label.zig | 45 +++ src/backends/wasm/Slider.zig | 64 ++++ src/backends/wasm/TextField.zig | 41 ++ src/backends/wasm/Window.zig | 64 ++++ src/backends/wasm/backend.zig | 618 +------------------------------ src/backends/wasm/capy-worker.js | 3 + src/backends/wasm/capy.js | 3 + src/backends/wasm/common.zig | 164 ++++++++ src/backends/wasm/js.zig | 5 + src/data.zig | 1 - 15 files changed, 677 insertions(+), 602 deletions(-) create mode 100644 src/backends/wasm/Button.zig create mode 100644 src/backends/wasm/Canvas.zig create mode 100644 src/backends/wasm/Container.zig create mode 100644 src/backends/wasm/Dropdown.zig create mode 100644 src/backends/wasm/ImageData.zig create mode 100644 src/backends/wasm/Label.zig create mode 100644 src/backends/wasm/Slider.zig create mode 100644 src/backends/wasm/TextField.zig create mode 100644 src/backends/wasm/Window.zig create mode 100644 src/backends/wasm/common.zig diff --git a/src/backends/wasm/Button.zig b/src/backends/wasm/Button.zig new file mode 100644 index 0000000..11e47fa --- /dev/null +++ b/src/backends/wasm/Button.zig @@ -0,0 +1,51 @@ +const common = @import("common.zig"); +const js = @import("js.zig"); +const lib = @import("../../capy.zig"); +const GuiWidget = common.GuiWidget; +const Events = common.Events; + +const Button = @This(); + +peer: *GuiWidget, +/// The label returned by getLabel(), it's invalidated everytime setLabel is called +temp_label: ?[:0]const u8 = null, + +pub usingnamespace Events(Button); + +pub fn create() !Button { + return Button{ .peer = try GuiWidget.init( + Button, + lib.lasting_allocator, + "button", + "button", + ) }; +} + +pub fn setLabel(self: *Button, label: [:0]const u8) void { + js.setText(self.peer.element, label.ptr, label.len); + if (self.temp_label) |slice| { + lib.lasting_allocator.free(slice); + self.temp_label = null; + } +} + +pub fn getLabel(self: *const Button) [:0]const u8 { + if (self.temp_label) |text| { + return text; + } else { + const len = js.getTextLen(self.peer.element); + const text = lib.lasting_allocator.allocSentinel(u8, len, 0) catch unreachable; + js.getText(self.peer.element, text.ptr); + self.temp_label = text; + + return text; + } +} + +pub fn setEnabled(self: *const Button, enable: bool) void { + if (enable) { + js.removeAttribute(self.peer.element, "disabled"); + } else { + js.setAttribute(self.peer.element, "disabled", "disabled"); + } +} diff --git a/src/backends/wasm/Canvas.zig b/src/backends/wasm/Canvas.zig new file mode 100644 index 0000000..0c510a5 --- /dev/null +++ b/src/backends/wasm/Canvas.zig @@ -0,0 +1,128 @@ +const std = @import("std"); +const common = @import("common.zig"); +const js = @import("js.zig"); +const lib = @import("../../capy.zig"); +const GuiWidget = common.GuiWidget; +const Events = common.Events; + +const Canvas = @This(); + +peer: *GuiWidget, + +pub usingnamespace Events(Canvas); + +pub const DrawContextImpl = struct { + ctx: js.CanvasContextId, + + pub const Font = struct { + face: [:0]const u8, + size: f64, + }; + + pub const TextSize = struct { width: u32, height: u32 }; + + pub const TextLayout = struct { + wrap: ?f64 = null, + + pub fn setFont(self: *TextLayout, font: Font) void { + // TODO + _ = self; + _ = font; + } + + pub fn deinit(self: *TextLayout) void { + // TODO + _ = self; + } + + pub fn getTextSize(self: *TextLayout, str: []const u8) TextSize { + // TODO + _ = self; + _ = str; + return TextSize{ .width = 0, .height = 0 }; + } + + pub fn init() TextLayout { + return TextLayout{}; + } + }; + + pub fn setColorRGBA(self: *DrawContextImpl, r: f32, g: f32, b: f32, a: f32) void { + const color = lib.Color{ + .red = @as(u8, @intFromFloat(std.math.clamp(r, 0, 1) * 255)), + .green = @as(u8, @intFromFloat(std.math.clamp(g, 0, 1) * 255)), + .blue = @as(u8, @intFromFloat(std.math.clamp(b, 0, 1) * 255)), + .alpha = @as(u8, @intFromFloat(std.math.clamp(a, 0, 1) * 255)), + }; + js.setColor(self.ctx, color.red, color.green, color.blue, color.alpha); + } + + pub fn rectangle(self: *DrawContextImpl, x: i32, y: i32, w: u32, h: u32) void { + js.rectPath(self.ctx, x, y, w, h); + } + + pub fn roundedRectangleEx(self: *DrawContextImpl, x: i32, y: i32, w: u32, h: u32, corner_radiuses: [4]f32) void { + _ = corner_radiuses; + js.rectPath(self.ctx, x, y, w, h); + } + + pub fn text(self: *DrawContextImpl, x: i32, y: i32, layout: TextLayout, str: []const u8) void { + // TODO: layout + _ = layout; + js.fillText(self.ctx, str.ptr, str.len, x, y); + } + + pub fn image(self: *DrawContextImpl, x: i32, y: i32, w: u32, h: u32, data: lib.ImageData) void { + _ = w; + _ = h; // TODO: scaling + js.fillImage(self.ctx, data.peer.id, x, y); + } + + pub fn line(self: *DrawContextImpl, x1: i32, y1: i32, x2: i32, y2: i32) void { + js.moveTo(self.ctx, x1, y1); + js.lineTo(self.ctx, x2, y2); + js.stroke(self.ctx); + } + + pub fn ellipse(self: *DrawContextImpl, x: i32, y: i32, w: u32, h: u32) void { + js.ellipse(self.ctx, x, y, w, h); + } + + pub fn clear(self: *DrawContextImpl, x: u32, y: u32, w: u32, h: u32) void { + // TODO + _ = self; + _ = x; + _ = y; + _ = w; + _ = h; + } + + pub fn stroke(self: *DrawContextImpl) void { + js.stroke(self.ctx); + } + + pub fn fill(self: *DrawContextImpl) void { + js.fill(self.ctx); + } +}; + +pub fn create() !Canvas { + return Canvas{ .peer = try GuiWidget.init( + Canvas, + lib.lasting_allocator, + "canvas", + "canvas", + ) }; +} + +pub fn _requestDraw(self: *Canvas) !void { + const ctxId = js.openContext(self.peer.element); + const impl = DrawContextImpl{ .ctx = ctxId }; + var ctx = @import("../../backend.zig").DrawContext{ .impl = impl }; + if (self.peer.class.drawHandler) |handler| { + handler(&ctx, self.peer.classUserdata); + } + if (self.peer.user.drawHandler) |handler| { + handler(&ctx, self.peer.userdata); + } +} diff --git a/src/backends/wasm/Container.zig b/src/backends/wasm/Container.zig new file mode 100644 index 0000000..47c13ec --- /dev/null +++ b/src/backends/wasm/Container.zig @@ -0,0 +1,50 @@ +const common = @import("common.zig"); +const js = @import("js.zig"); +const lib = @import("../../capy.zig"); +const GuiWidget = common.GuiWidget; +const Events = common.Events; + +const Container = @This(); + +peer: *GuiWidget, + +pub usingnamespace Events(Container); + +pub fn create() !Container { + return Container{ + .peer = try GuiWidget.init( + Container, + lib.lasting_allocator, + "div", + "container", + ), + }; +} + +pub fn add(self: *Container, peer: *GuiWidget) void { + js.appendElement(self.peer.element, peer.element); + self.peer.children.append(peer) catch unreachable; +} + +pub fn remove(self: *const Container, peer: *GuiWidget) void { + _ = peer; + _ = self; +} + +pub fn setTabOrder(self: *Container, peers: []const *GuiWidget) void { + _ = peers; + _ = self; +} + +pub fn move(self: *const Container, peer: *GuiWidget, x: u32, y: u32) void { + _ = self; + js.setPos(peer.element, x, y); +} + +pub fn resize(self: *const Container, peer: *GuiWidget, w: u32, h: u32) void { + _ = self; + js.setSize(peer.element, w, h); + if (peer.user.resizeHandler) |handler| { + handler(w, h, peer.userdata); + } +} diff --git a/src/backends/wasm/Dropdown.zig b/src/backends/wasm/Dropdown.zig new file mode 100644 index 0000000..a7a019e --- /dev/null +++ b/src/backends/wasm/Dropdown.zig @@ -0,0 +1,20 @@ +const common = @import("common.zig"); +const js = @import("js.zig"); +const lib = @import("../../capy.zig"); +const GuiWidget = common.GuiWidget; +const Events = common.Events; + +const Dropdown = @This(); + +peer: js.ElementId, + +pub usingnamespace Events(Dropdown); + +pub fn create() !Dropdown { + return Dropdown{ .peer = try GuiWidget.init( + Dropdown, + lib.lasting_allocator, + "select", + "select", + ) }; +} diff --git a/src/backends/wasm/ImageData.zig b/src/backends/wasm/ImageData.zig new file mode 100644 index 0000000..ef4d6a0 --- /dev/null +++ b/src/backends/wasm/ImageData.zig @@ -0,0 +1,22 @@ +const common = @import("common.zig"); +const js = @import("js.zig"); +const lib = @import("../../capy.zig"); +const GuiWidget = common.GuiWidget; +const Events = common.Events; + +const ImageData = @This(); + +// TODO +id: js.ResourceId, + +pub fn from(width: usize, height: usize, stride: usize, cs: lib.Colorspace, bytes: []const u8) !ImageData { + return ImageData{ + .id = js.uploadImage( + width, + height, + stride, + cs == .RGB, + bytes.ptr, + ), + }; +} diff --git a/src/backends/wasm/Label.zig b/src/backends/wasm/Label.zig new file mode 100644 index 0000000..4ffab1e --- /dev/null +++ b/src/backends/wasm/Label.zig @@ -0,0 +1,45 @@ +const common = @import("common.zig"); +const js = @import("js.zig"); +const lib = @import("../../capy.zig"); +const GuiWidget = common.GuiWidget; +const Events = common.Events; + +const Label = @This(); + +peer: *GuiWidget, +/// The text returned by getText(), it's invalidated everytime setText is called +temp_text: ?[]const u8 = null, + +pub usingnamespace Events(Label); + +pub fn create() !Label { + return Label{ .peer = try GuiWidget.init( + Label, + lib.lasting_allocator, + "span", + "label", + ) }; +} + +pub fn setAlignment(_: *Label, _: f32) void {} + +pub fn setText(self: *Label, text: []const u8) void { + js.setText(self.peer.element, text.ptr, text.len); + if (self.temp_text) |slice| { + lib.lasting_allocator.free(slice); + self.temp_text = null; + } +} + +pub fn getText(self: *Label) []const u8 { + if (self.temp_text) |text| { + return text; + } else { + const len = js.getTextLen(self.peer.element); + const text = lib.lasting_allocator.allocSentinel(u8, len, 0) catch unreachable; + js.getText(self.peer.element, text.ptr); + self.temp_text = text; + + return text; + } +} diff --git a/src/backends/wasm/Slider.zig b/src/backends/wasm/Slider.zig new file mode 100644 index 0000000..3d45437 --- /dev/null +++ b/src/backends/wasm/Slider.zig @@ -0,0 +1,64 @@ +const std = @import("std"); +const common = @import("common.zig"); +const js = @import("js.zig"); +const lib = @import("../../capy.zig"); +const GuiWidget = common.GuiWidget; +const Events = common.Events; + +const Slider = @This(); + +peer: *GuiWidget, + +pub usingnamespace Events(Slider); + +pub fn create() !Slider { + return Slider{ + .peer = try GuiWidget.init( + Slider, + lib.lasting_allocator, + "input", + "slider", + ), + }; +} + +pub fn getValue(self: *const Slider) f32 { + return js.getValue(self.peer.element); +} + +pub fn setValue(self: *Slider, value: f32) void { + var buf: [100]u8 = undefined; + const slice = std.fmt.bufPrint(&buf, "{}", .{value}) catch unreachable; + js.setAttribute(self.peer.element, "value", slice); +} + +pub fn setMinimum(self: *Slider, minimum: f32) void { + var buf: [100]u8 = undefined; + const slice = std.fmt.bufPrint(&buf, "{}", .{minimum}) catch unreachable; + js.setAttribute(self.peer.element, "min", slice); +} + +pub fn setMaximum(self: *Slider, maximum: f32) void { + var buf: [100]u8 = undefined; + const slice = std.fmt.bufPrint(&buf, "{}", .{maximum}) catch unreachable; + js.setAttribute(self.peer.element, "max", slice); +} + +pub fn setStepSize(self: *Slider, stepSize: f32) void { + var buf: [100]u8 = undefined; + const slice = std.fmt.bufPrint(&buf, "{}", .{stepSize}) catch unreachable; + js.setAttribute(self.peer.element, "step", slice); +} + +pub fn setEnabled(self: *Slider, enable: bool) void { + if (enable) { + js.removeAttribute(self.peer.element, "disabled"); + } else { + js.setAttribute(self.peer.element, "disabled", "disabled"); + } +} + +pub fn setOrientation(self: *Slider, orientation: lib.Orientation) void { + _ = orientation; + _ = self; +} diff --git a/src/backends/wasm/TextField.zig b/src/backends/wasm/TextField.zig new file mode 100644 index 0000000..08734e6 --- /dev/null +++ b/src/backends/wasm/TextField.zig @@ -0,0 +1,41 @@ +const common = @import("common.zig"); +const js = @import("js.zig"); +const lib = @import("../../capy.zig"); +const GuiWidget = common.GuiWidget; +const Events = common.Events; + +const TextField = @This(); + +peer: *GuiWidget, + +pub usingnamespace Events(TextField); + +pub fn create() !TextField { + return TextField{ .peer = try GuiWidget.init( + TextField, + lib.lasting_allocator, + "input", + "textfield", + ) }; +} + +pub fn setText(self: *TextField, text: []const u8) void { + js.setText(self.peer.element, text.ptr, text.len); +} + +pub fn getText(self: *TextField) [:0]const u8 { + const len = js.getTextLen(self.peer.element); + // TODO: fix the obvious memory leak + const text = lib.lasting_allocator.allocSentinel(u8, len, 0) catch unreachable; + js.getText(self.peer.element, text.ptr); + + return text; +} + +pub fn setReadOnly(self: *TextField, readOnly: bool) void { + if (readOnly) { + js.removeAttribute(self.peer.element, "readonly"); + } else { + js.setAttribute(self.peer.element, "readonly", "readonly"); + } +} diff --git a/src/backends/wasm/Window.zig b/src/backends/wasm/Window.zig new file mode 100644 index 0000000..1c8a293 --- /dev/null +++ b/src/backends/wasm/Window.zig @@ -0,0 +1,64 @@ +const common = @import("common.zig"); +const js = @import("js.zig"); +const lib = @import("../../capy.zig"); +const GuiWidget = common.GuiWidget; +const Events = common.Events; + +const Window = @This(); +pub var globalWindow: ?*Window = null; + +peer: *GuiWidget, +child: ?*GuiWidget = null, +scale: f32 = 1.0, + +pub usingnamespace Events(Window); + +pub fn create() !Window { + return Window{ + .peer = try GuiWidget.init( + Window, + lib.lasting_allocator, + "div", + "window", + ), + }; +} + +pub fn show(self: *Window) void { + // TODO: handle multiple windows + if (globalWindow != null) { + js.print("one window already showed!"); + return; + } + globalWindow = self; +} + +pub fn resize(_: *Window, _: c_int, _: c_int) void { + // Not implemented. +} + +pub fn setChild(self: *Window, peer: ?*GuiWidget) void { + if (peer) |p| { + js.setRoot(p.element); + self.child = peer; + } else { + // TODO: js.clearRoot(); + } +} + +pub fn setTitle(self: *Window, title: [*:0]const u8) void { + // TODO. This should be configured in the javascript + _ = self; + _ = title; +} + +pub fn setSourceDpi(self: *Window, dpi: u32) void { + // CSS pixels are somewhat undefined given they're based on the confortableness of the reader + const resolution = @as(f32, @floatFromInt(dpi)); + self.scale = resolution / 96.0; +} + +pub fn registerTickCallback(self: *Window) void { + _ = self; + // TODO +} diff --git a/src/backends/wasm/backend.zig b/src/backends/wasm/backend.zig index 2597d35..9420d07 100644 --- a/src/backends/wasm/backend.zig +++ b/src/backends/wasm/backend.zig @@ -11,31 +11,9 @@ const EventFunctions = shared.EventFunctions(@This()); const MouseButton = shared.MouseButton; // What the backend exports -pub const PeerType = *GuiWidget; +pub const PeerType = *@import("common.zig").GuiWidget; -const GuiWidget = struct { - user: EventFunctions = .{}, - class: EventFunctions = .{}, - userdata: usize = 0, - classUserdata: usize = 0, - - /// Pointer to the component (of type T) - object: ?*anyopaque = null, - element: js.ElementId = 0, - - processEventFn: *const fn (object: ?*anyopaque, event: js.EventId) void, - children: std.ArrayList(*GuiWidget), - - pub fn init(comptime T: type, allocator: std.mem.Allocator, name: []const u8, typeName: []const u8) !*GuiWidget { - const self = try allocator.create(GuiWidget); - self.* = .{ - .processEventFn = T.processEvent, - .element = js.createElement(name, typeName), - .children = std.ArrayList(*GuiWidget).init(allocator), - }; - return self; - } -}; +const Events = @import("common.zig").Events; pub fn showNativeMessageDialog(msgType: shared.MessageType, comptime fmt: []const u8, args: anytype) void { const msg = std.fmt.allocPrintZ(lib.internal.scratch_allocator, fmt, args) catch { @@ -50,522 +28,16 @@ pub fn init() !void { // no initialization to do } -var globalWindow: ?*Window = null; - pub const Monitor = @import("Monitor.zig"); - -pub const Window = struct { - peer: *GuiWidget, - child: ?PeerType = null, - scale: f32 = 1.0, - - pub usingnamespace Events(Window); - - pub fn create() !Window { - return Window{ - .peer = try GuiWidget.init(Window, lasting_allocator, "div", "window"), - }; - } - - pub fn show(self: *Window) void { - // TODO: handle multiple windows - if (globalWindow != null) { - js.print("one window already showed!"); - return; - } - globalWindow = self; - } - - pub fn resize(_: *Window, _: c_int, _: c_int) void { - // Not implemented. - } - - pub fn setChild(self: *Window, peer: ?PeerType) void { - if (peer) |p| { - js.setRoot(p.element); - self.child = peer; - } else { - // TODO: js.clearRoot(); - } - } - - pub fn setTitle(self: *Window, title: [*:0]const u8) void { - // TODO. This should be configured in the javascript - _ = self; - _ = title; - } - - pub fn setSourceDpi(self: *Window, dpi: u32) void { - // CSS pixels are somewhat undefined given they're based on the confortableness of the reader - const resolution = @as(f32, @floatFromInt(dpi)); - self.scale = resolution / 96.0; - } - - pub fn registerTickCallback(self: *Window) void { - _ = self; - // TODO - } -}; - -pub fn Events(comptime T: type) type { - return struct { - const Self = @This(); - - pub fn setupEvents() !void {} - - pub inline fn setUserData(self: *T, data: anytype) void { - comptime { - if (!trait.isSingleItemPtr(@TypeOf(data))) { - @compileError(std.fmt.comptimePrint("Expected single item pointer, got {s}", .{@typeName(@TypeOf(data))})); - } - } - - self.peer.userdata = @intFromPtr(data); - self.peer.object = self; - } - - pub inline fn setCallback(self: *T, comptime eType: EventType, cb: anytype) !void { - self.peer.object = self; - switch (eType) { - .Click => self.peer.user.clickHandler = cb, - .Draw => self.peer.user.drawHandler = cb, - .MouseButton => self.peer.user.mouseButtonHandler = cb, - .MouseMotion => self.peer.user.mouseMotionHandler = cb, - .Scroll => self.peer.user.scrollHandler = cb, - .TextChanged => self.peer.user.changedTextHandler = cb, - .Resize => { - self.peer.user.resizeHandler = cb; - self.requestDraw() catch {}; - }, - .KeyType => self.peer.user.keyTypeHandler = cb, - .KeyPress => self.peer.user.keyPressHandler = cb, - .PropertyChange => self.peer.user.propertyChangeHandler = cb, - } - } - - pub fn setOpacity(self: *T, opacity: f64) void { - _ = self; - _ = opacity; - } - - /// Requests a redraw - pub fn requestDraw(self: *T) !void { - if (@hasDecl(T, "_requestDraw")) { - try self._requestDraw(); - } - } - - pub fn processEvent(object: ?*anyopaque, event: js.EventId) void { - const self = @as(*T, @ptrCast(@alignCast(object.?))); - - if (js.getEventTarget(event) == self.peer.element) { - // handle event - switch (js.getEventType(event)) { - .OnClick => { - if (self.peer.user.clickHandler) |handler| { - handler(self.peer.userdata); - } - }, - .TextChange => { - if (self.peer.user.changedTextHandler) |handler| { - handler(self.peer.userdata); - } - }, - .Resize => { - if (self.peer.user.resizeHandler) |handler| { - handler(@as(u32, @intCast(self.getWidth())), @as(u32, @intCast(self.getHeight())), self.peer.userdata); - } - self.requestDraw() catch unreachable; - }, - .MouseButton => { - if (self.peer.user.mouseButtonHandler) |handler| { - const button = @as(MouseButton, @enumFromInt(js.getEventArg(event, 0))); - const pressed = js.getEventArg(event, 1) != 0; - const x = @as(i32, @bitCast(js.getEventArg(event, 2))); - const y = @as(i32, @bitCast(js.getEventArg(event, 3))); - handler(button, pressed, x, y, self.peer.userdata); - } - }, - .MouseMotion => { - if (self.peer.user.mouseMotionHandler) |handler| { - const x = @as(i32, @bitCast(js.getEventArg(event, 0))); - const y = @as(i32, @bitCast(js.getEventArg(event, 1))); - handler(x, y, self.peer.userdata); - } - }, - .MouseScroll => { - if (self.peer.user.scrollHandler) |handler| { - const dx = @as(f32, @floatFromInt(@as(i32, @bitCast(js.getEventArg(event, 0))))); - const dy = @as(f32, @floatFromInt(@as(i32, @bitCast(js.getEventArg(event, 1))))); - handler(dx, dy, self.peer.userdata); - } - }, - .UpdateAudio => unreachable, - .PropertyChange => { - if (self.peer.user.propertyChangeHandler) |handler| { - const value_f32 = js.getValue(self.peer.element); - handler("value", &value_f32, self.peer.userdata); - } - }, - } - } else if (T == Container) { // if we're a container, iterate over our children to propagate the event - for (self.peer.children.items) |child| { - child.processEventFn(child.object, event); - } - } - } - - pub fn getWidth(self: *const T) c_int { - return @max(10, js.getWidth(self.peer.element)); - } - - pub fn getHeight(self: *const T) c_int { - return @max(10, js.getHeight(self.peer.element)); - } - - pub fn getPreferredSize(self: *const T) lib.Size { - // TODO - _ = self; - return lib.Size.init(100, 100); - } - - pub fn deinit(self: *const T) void { - // TODO: actually remove the element - _ = self; - @panic("TODO"); - } - }; -} - -pub const TextField = struct { - peer: *GuiWidget, - - pub usingnamespace Events(TextField); - - pub fn create() !TextField { - return TextField{ .peer = try GuiWidget.init(TextField, lasting_allocator, "input", "textfield") }; - } - - pub fn setText(self: *TextField, text: []const u8) void { - js.setText(self.peer.element, text.ptr, text.len); - } - - pub fn getText(self: *TextField) [:0]const u8 { - const len = js.getTextLen(self.peer.element); - // TODO: fix the obvious memory leak - const text = lasting_allocator.allocSentinel(u8, len, 0) catch unreachable; - js.getText(self.peer.element, text.ptr); - - return text; - } - - pub fn setReadOnly(self: *TextField, readOnly: bool) void { - _ = self; - _ = readOnly; - // TODO: set read only - } -}; - -pub const Label = struct { - peer: *GuiWidget, - /// The text returned by getText(), it's invalidated everytime setText is called - temp_text: ?[]const u8 = null, - - pub usingnamespace Events(Label); - - pub fn create() !Label { - return Label{ .peer = try GuiWidget.init(Label, lasting_allocator, "span", "label") }; - } - - pub fn setAlignment(_: *Label, _: f32) void {} - - pub fn setText(self: *Label, text: []const u8) void { - js.setText(self.peer.element, text.ptr, text.len); - if (self.temp_text) |slice| { - lasting_allocator.free(slice); - self.temp_text = null; - } - } - - pub fn getText(self: *Label) []const u8 { - if (self.temp_text) |text| { - return text; - } else { - const len = js.getTextLen(self.peer.element); - const text = lasting_allocator.allocSentinel(u8, len, 0) catch unreachable; - js.getText(self.peer.element, text.ptr); - self.temp_text = text; - - return text; - } - } -}; - -pub const Button = struct { - peer: *GuiWidget, - /// The label returned by getLabel(), it's invalidated everytime setLabel is called - temp_label: ?[:0]const u8 = null, - - pub usingnamespace Events(Button); - - pub fn create() !Button { - return Button{ .peer = try GuiWidget.init(Button, lasting_allocator, "button", "button") }; - } - - pub fn setLabel(self: *Button, label: [:0]const u8) void { - js.setText(self.peer.element, label.ptr, label.len); - if (self.temp_label) |slice| { - lasting_allocator.free(slice); - self.temp_label = null; - } - } - - pub fn getLabel(self: *const Button) [:0]const u8 { - if (self.temp_label) |text| { - return text; - } else { - const len = js.getTextLen(self.peer.element); - const text = lasting_allocator.allocSentinel(u8, len, 0) catch unreachable; - js.getText(self.peer.element, text.ptr); - self.temp_label = text; - - return text; - } - } - - pub fn setEnabled(self: *const Button, enabled: bool) void { - _ = self; - _ = enabled; - // TODO: enabled property - } -}; - -pub const Slider = struct { - peer: *GuiWidget, - - pub usingnamespace Events(Slider); - - pub fn create() !Slider { - return Slider{ - .peer = try GuiWidget.init(Slider, lasting_allocator, "input", "slider"), - }; - } - - pub fn getValue(self: *const Slider) f32 { - return js.getValue(self.peer.element); - } - - pub fn setValue(self: *Slider, value: f32) void { - var buf: [100]u8 = undefined; - const slice = std.fmt.bufPrint(&buf, "{}", .{value}) catch unreachable; - js.setAttribute(self.peer.element, "value", slice); - } - - pub fn setMinimum(self: *Slider, minimum: f32) void { - var buf: [100]u8 = undefined; - const slice = std.fmt.bufPrint(&buf, "{}", .{minimum}) catch unreachable; - js.setAttribute(self.peer.element, "min", slice); - } - - pub fn setMaximum(self: *Slider, maximum: f32) void { - var buf: [100]u8 = undefined; - const slice = std.fmt.bufPrint(&buf, "{}", .{maximum}) catch unreachable; - js.setAttribute(self.peer.element, "max", slice); - } - - pub fn setStepSize(self: *Slider, stepSize: f32) void { - var buf: [100]u8 = undefined; - const slice = std.fmt.bufPrint(&buf, "{}", .{stepSize}) catch unreachable; - js.setAttribute(self.peer.element, "step", slice); - } - - pub fn setEnabled(self: *Slider, enabled: bool) void { - var buf: [100]u8 = undefined; - const slice = std.fmt.bufPrint(&buf, "{}", .{enabled}) catch unreachable; - js.setAttribute(self.peer.element, "enabled", slice); - } - - pub fn setOrientation(self: *Slider, orientation: lib.Orientation) void { - _ = orientation; - _ = self; - } -}; - -pub const Canvas = struct { - peer: *GuiWidget, - - pub usingnamespace Events(Canvas); - - pub const DrawContextImpl = struct { - ctx: js.CanvasContextId, - - pub const Font = struct { - face: [:0]const u8, - size: f64, - }; - - pub const TextSize = struct { width: u32, height: u32 }; - - pub const TextLayout = struct { - wrap: ?f64 = null, - - pub fn setFont(self: *TextLayout, font: Font) void { - // TODO - _ = self; - _ = font; - } - - pub fn deinit(self: *TextLayout) void { - // TODO - _ = self; - } - - pub fn getTextSize(self: *TextLayout, str: []const u8) TextSize { - // TODO - _ = self; - _ = str; - return TextSize{ .width = 0, .height = 0 }; - } - - pub fn init() TextLayout { - return TextLayout{}; - } - }; - - pub fn setColorRGBA(self: *DrawContextImpl, r: f32, g: f32, b: f32, a: f32) void { - const color = lib.Color{ - .red = @as(u8, @intFromFloat(std.math.clamp(r, 0, 1) * 255)), - .green = @as(u8, @intFromFloat(std.math.clamp(g, 0, 1) * 255)), - .blue = @as(u8, @intFromFloat(std.math.clamp(b, 0, 1) * 255)), - .alpha = @as(u8, @intFromFloat(std.math.clamp(a, 0, 1) * 255)), - }; - js.setColor(self.ctx, color.red, color.green, color.blue, color.alpha); - } - - pub fn rectangle(self: *DrawContextImpl, x: i32, y: i32, w: u32, h: u32) void { - js.rectPath(self.ctx, x, y, w, h); - } - - pub fn roundedRectangleEx(self: *DrawContextImpl, x: i32, y: i32, w: u32, h: u32, corner_radiuses: [4]f32) void { - _ = corner_radiuses; - js.rectPath(self.ctx, x, y, w, h); - } - - pub fn text(self: *DrawContextImpl, x: i32, y: i32, layout: TextLayout, str: []const u8) void { - // TODO: layout - _ = layout; - js.fillText(self.ctx, str.ptr, str.len, x, y); - } - - pub fn image(self: *DrawContextImpl, x: i32, y: i32, w: u32, h: u32, data: lib.ImageData) void { - _ = w; - _ = h; // TODO: scaling - js.fillImage(self.ctx, data.peer.id, x, y); - } - - pub fn line(self: *DrawContextImpl, x1: i32, y1: i32, x2: i32, y2: i32) void { - js.moveTo(self.ctx, x1, y1); - js.lineTo(self.ctx, x2, y2); - js.stroke(self.ctx); - } - - pub fn ellipse(self: *DrawContextImpl, x: i32, y: i32, w: u32, h: u32) void { - js.ellipse(self.ctx, x, y, w, h); - } - - pub fn clear(self: *DrawContextImpl, x: u32, y: u32, w: u32, h: u32) void { - // TODO - _ = self; - _ = x; - _ = y; - _ = w; - _ = h; - } - - pub fn stroke(self: *DrawContextImpl) void { - js.stroke(self.ctx); - } - - pub fn fill(self: *DrawContextImpl) void { - js.fill(self.ctx); - } - }; - - pub fn create() !Canvas { - return Canvas{ .peer = try GuiWidget.init(Canvas, lasting_allocator, "canvas", "canvas") }; - } - - pub fn _requestDraw(self: *Canvas) !void { - const ctxId = js.openContext(self.peer.element); - const impl = DrawContextImpl{ .ctx = ctxId }; - var ctx = @import("../../backend.zig").DrawContext{ .impl = impl }; - if (self.peer.class.drawHandler) |handler| { - handler(&ctx, self.peer.classUserdata); - } - if (self.peer.user.drawHandler) |handler| { - handler(&ctx, self.peer.userdata); - } - } -}; - -pub const Dropdown = struct { - peer: js.ElementId, - - pub usingnamespace Events(Dropdown); - - pub fn create() !Dropdown { - return Dropdown{ .peer = try GuiWidget.init(Dropdown, lasting_allocator, "select", "select") }; - } -}; - -pub const ImageData = struct { - // TODO - id: js.ResourceId, - - pub fn from(width: usize, height: usize, stride: usize, cs: lib.Colorspace, bytes: []const u8) !ImageData { - return ImageData{ .id = js.uploadImage(width, height, stride, cs == .RGB, bytes.ptr) }; - } -}; - -pub const Container = struct { - peer: *GuiWidget, - - pub usingnamespace Events(Container); - - pub fn create() !Container { - return Container{ - .peer = try GuiWidget.init(Container, lasting_allocator, "div", "container"), - }; - } - - pub fn add(self: *Container, peer: PeerType) void { - js.appendElement(self.peer.element, peer.element); - self.peer.children.append(peer) catch unreachable; - } - - pub fn remove(self: *const Container, peer: PeerType) void { - _ = peer; - _ = self; - } - - pub fn setTabOrder(self: *Container, peers: []const PeerType) void { - _ = peers; - _ = self; - } - - pub fn move(self: *const Container, peer: PeerType, x: u32, y: u32) void { - _ = self; - js.setPos(peer.element, x, y); - } - - pub fn resize(self: *const Container, peer: PeerType, w: u32, h: u32) void { - _ = self; - js.setSize(peer.element, w, h); - if (peer.user.resizeHandler) |handler| { - handler(w, h, peer.userdata); - } - } -}; +pub const Window = @import("Window.zig"); +pub const Container = @import("Container.zig"); +pub const TextField = @import("TextField.zig"); +pub const Label = @import("Label.zig"); +pub const Button = @import("Button.zig"); +pub const Slider = @import("Slider.zig"); +pub const Canvas = @import("Canvas.zig"); +pub const Dropdown = @import("Dropdown.zig"); +pub const ImageData = @import("ImageData.zig"); pub const AudioGenerator = struct { source: js.AudioSourceId, @@ -642,7 +114,7 @@ pub fn runStep(step: shared.EventLoopStep) bool { lib.audio.backendUpdate(); }, else => { - if (globalWindow) |window| { + if (@import("Window.zig").globalWindow) |window| { if (window.child) |child| { child.processEventFn(child.object, eventId); } @@ -666,75 +138,19 @@ fn executeMain() void { } // Execution -fn milliTimestamp() i64 { - return @as(i64, @intFromFloat(js.now())); -} // The following WASI Preview 1 functions are implemented on JS's side: +// - environ_sizes_get +// - environ_get // - clock_time_get // - clock_res_get // - poll_oneoff (only for CLOCK pollables!) +// - path_open // - fd_write (FOR STANDARD OUTPUT AND STANDARD ERROR ONLY!) +// - fd_read +// - fd_seek pub const backendExport = struct { - // pub const os = struct { - // pub const system = struct { - // pub const E = std.os.linux.E; - // fn errno(e: E) usize { - // const signed_r = @as(isize, 0) - @intFromEnum(e); - // return @as(usize, @bitCast(signed_r)); - // } - - // pub fn getErrno(r: usize) E { - // const signed_r = @as(isize, @bitCast(r)); - // const int = if (signed_r > -4096 and signed_r < 0) -signed_r else 0; - // return @as(E, @enumFromInt(int)); - // } - - // // Time - // pub const CLOCK = std.os.linux.CLOCK; - // pub const timespec = std.os.linux.timespec; - - // pub fn clock_gettime(clk_id: i32, tp: *timespec) usize { - // _ = clk_id; - - // // Time in milliseconds - // const millis = milliTimestamp(); - // tp.tv_sec = @as(isize, @intCast(@divTrunc(millis, std.time.ms_per_s))); - // tp.tv_nsec = @as(isize, @intCast(@rem(millis, std.time.ms_per_s) * std.time.ns_per_ms)); - // return 0; - // } - - // /// Precision DEFINITELY not guarenteed (can have up to 20ms delays) - // pub fn nanosleep(req: *const timespec, rem: ?*timespec) usize { - // _ = rem; - // // Duration in milliseconds - // const duration = @as(u64, @intCast(req.tv_sec)) * 1000 + @as(u64, @intCast(req.tv_nsec)) / 1000; - - // const start = milliTimestamp(); - // while (milliTimestamp() < start + @as(i64, @intCast(duration))) { - // // TODO: better way to sleep like calling a jS function for sleep - // } - // return 0; - // } - - // // I/O - // pub const fd_t = u32; - // pub const STDOUT_FILENO = 1; - // pub const STDERR_FILENO = 1; - - // pub fn write(fd: fd_t, buf: [*]const u8, size: usize) usize { - // if (fd == STDOUT_FILENO or fd == STDERR_FILENO) { - // // TODO: buffer and write for each new line - // js.print(buf[0..size]); - // return size; - // } else { - // return errno(E.BADF); - // } - // } - // }; - // }; - pub fn log( comptime message_level: std.log.Level, comptime scope: @Type(.EnumLiteral), diff --git a/src/backends/wasm/capy-worker.js b/src/backends/wasm/capy-worker.js index 3837022..e57d9d2 100644 --- a/src/backends/wasm/capy-worker.js +++ b/src/backends/wasm/capy-worker.js @@ -236,6 +236,9 @@ const env = { jsSetAttribute: function(element, name, nameLen, value, valueLen) { self.postMessage(["jsSetAttribute", element, readString(name, nameLen), readString(value, valueLen)]); }, + jsRemoveAttribute: function(element, name, nameLen) { + self.postMessage(["jsRemoveAttribute", element, readString(name, nameLen)]); + }, getAttributeLen: function(element, name, nameLen) { self.postMessage(["getAttributeLen", element, readString(name, nameLen)]); const a = waitForAnswer("int"); diff --git a/src/backends/wasm/capy.js b/src/backends/wasm/capy.js index 103782b..40f1cae 100644 --- a/src/backends/wasm/capy.js +++ b/src/backends/wasm/capy.js @@ -257,6 +257,9 @@ let env = { jsSetAttribute: function(element, name, value) { domObjects[element].setAttribute(name, value); }, + jsRemoveAttribute: function(element, name) { + domObjects[element].removeAttribute(name); + }, getAttributeLen: function(element, name) { return domObjects[element].getAttribute(name).length; }, diff --git a/src/backends/wasm/common.zig b/src/backends/wasm/common.zig new file mode 100644 index 0000000..8e77ca3 --- /dev/null +++ b/src/backends/wasm/common.zig @@ -0,0 +1,164 @@ +const std = @import("std"); +const shared = @import("../shared.zig"); +const js = @import("js.zig"); +const trait = @import("../../trait.zig"); +const lib = @import("../../capy.zig"); + +const EventType = shared.BackendEventType; +const EventFunctions = shared.EventFunctions(@This()); +const MouseButton = shared.MouseButton; +const Container = @import("Container.zig"); + +pub const GuiWidget = struct { + user: EventFunctions = .{}, + class: EventFunctions = .{}, + userdata: usize = 0, + classUserdata: usize = 0, + + /// Pointer to the component (of type T) + object: ?*anyopaque = null, + element: js.ElementId = 0, + + processEventFn: *const fn (object: ?*anyopaque, event: js.EventId) void, + children: std.ArrayList(*GuiWidget), + + pub fn init(comptime T: type, allocator: std.mem.Allocator, name: []const u8, typeName: []const u8) !*GuiWidget { + const self = try allocator.create(GuiWidget); + self.* = .{ + .processEventFn = T.processEvent, + .element = js.createElement(name, typeName), + .children = std.ArrayList(*GuiWidget).init(allocator), + }; + return self; + } +}; + +pub fn Events(comptime T: type) type { + return struct { + const Self = @This(); + + pub fn setupEvents() !void {} + + pub inline fn setUserData(self: *T, data: anytype) void { + comptime { + if (!trait.isSingleItemPtr(@TypeOf(data))) { + @compileError(std.fmt.comptimePrint("Expected single item pointer, got {s}", .{@typeName(@TypeOf(data))})); + } + } + + self.peer.userdata = @intFromPtr(data); + self.peer.object = self; + } + + pub inline fn setCallback(self: *T, comptime eType: EventType, cb: anytype) !void { + self.peer.object = self; + switch (eType) { + .Click => self.peer.user.clickHandler = cb, + .Draw => self.peer.user.drawHandler = cb, + .MouseButton => self.peer.user.mouseButtonHandler = cb, + .MouseMotion => self.peer.user.mouseMotionHandler = cb, + .Scroll => self.peer.user.scrollHandler = cb, + .TextChanged => self.peer.user.changedTextHandler = cb, + .Resize => { + self.peer.user.resizeHandler = cb; + self.requestDraw() catch {}; + }, + .KeyType => self.peer.user.keyTypeHandler = cb, + .KeyPress => self.peer.user.keyPressHandler = cb, + .PropertyChange => self.peer.user.propertyChangeHandler = cb, + } + } + + pub fn setOpacity(self: *T, opacity: f64) void { + _ = self; + _ = opacity; + } + + /// Requests a redraw + pub fn requestDraw(self: *T) !void { + if (@hasDecl(T, "_requestDraw")) { + try self._requestDraw(); + } + } + + pub fn processEvent(object: ?*anyopaque, event: js.EventId) void { + const self = @as(*T, @ptrCast(@alignCast(object.?))); + + if (js.getEventTarget(event) == self.peer.element) { + // handle event + switch (js.getEventType(event)) { + .OnClick => { + if (self.peer.user.clickHandler) |handler| { + handler(self.peer.userdata); + } + }, + .TextChange => { + if (self.peer.user.changedTextHandler) |handler| { + handler(self.peer.userdata); + } + }, + .Resize => { + if (self.peer.user.resizeHandler) |handler| { + handler(@as(u32, @intCast(self.getWidth())), @as(u32, @intCast(self.getHeight())), self.peer.userdata); + } + self.requestDraw() catch unreachable; + }, + .MouseButton => { + if (self.peer.user.mouseButtonHandler) |handler| { + const button = @as(MouseButton, @enumFromInt(js.getEventArg(event, 0))); + const pressed = js.getEventArg(event, 1) != 0; + const x = @as(i32, @bitCast(js.getEventArg(event, 2))); + const y = @as(i32, @bitCast(js.getEventArg(event, 3))); + handler(button, pressed, x, y, self.peer.userdata); + } + }, + .MouseMotion => { + if (self.peer.user.mouseMotionHandler) |handler| { + const x = @as(i32, @bitCast(js.getEventArg(event, 0))); + const y = @as(i32, @bitCast(js.getEventArg(event, 1))); + handler(x, y, self.peer.userdata); + } + }, + .MouseScroll => { + if (self.peer.user.scrollHandler) |handler| { + const dx = @as(f32, @floatFromInt(@as(i32, @bitCast(js.getEventArg(event, 0))))); + const dy = @as(f32, @floatFromInt(@as(i32, @bitCast(js.getEventArg(event, 1))))); + handler(dx, dy, self.peer.userdata); + } + }, + .UpdateAudio => unreachable, + .PropertyChange => { + if (self.peer.user.propertyChangeHandler) |handler| { + const value_f32 = js.getValue(self.peer.element); + handler("value", &value_f32, self.peer.userdata); + } + }, + } + } else if (T == Container) { // if we're a container, iterate over our children to propagate the event + for (self.peer.children.items) |child| { + child.processEventFn(child.object, event); + } + } + } + + pub fn getWidth(self: *const T) c_int { + return @max(10, js.getWidth(self.peer.element)); + } + + pub fn getHeight(self: *const T) c_int { + return @max(10, js.getHeight(self.peer.element)); + } + + pub fn getPreferredSize(self: *const T) lib.Size { + // TODO + _ = self; + return lib.Size.init(100, 100); + } + + pub fn deinit(self: *const T) void { + // TODO: actually remove the element + _ = self; + @panic("TODO"); + } + }; +} diff --git a/src/backends/wasm/js.zig b/src/backends/wasm/js.zig index 8ecc9a6..70ea87b 100644 --- a/src/backends/wasm/js.zig +++ b/src/backends/wasm/js.zig @@ -21,6 +21,7 @@ pub extern fn jsCreateElement(name: [*]const u8, nameLen: usize, elementType: [* pub extern fn jsSetAttribute(element: ElementId, name: [*]const u8, nameLen: usize, value: [*]const u8, valueLen: usize) void; pub extern fn getAttributeLen(element: ElementId, name: [*]const u8, nameLen: usize) usize; pub extern fn jsGetAttribute(element: ElementId, name: [*]const u8, nameLen: usize, bufPtr: [*]u8) void; +pub extern fn jsRemoveAttribute(element: ElementId, name: [*]const u8, nameLen: usize) void; pub extern fn getValue(element: ElementId) f32; pub extern fn appendElement(parent: ElementId, child: ElementId) void; pub extern fn setRoot(root: ElementId) void; @@ -82,6 +83,10 @@ pub fn setAttribute(element: ElementId, name: []const u8, value: []const u8) voi jsSetAttribute(element, name.ptr, name.len, value.ptr, value.len); } +pub fn removeAttribute(element: ElementId, name: []const u8) void { + jsRemoveAttribute(element, name.ptr, name.len); +} + pub fn write(_: void, msg: []const u8) error{}!usize { jsPrint(msg.ptr, msg.len); return msg.len; diff --git a/src/data.zig b/src/data.zig index 415fbd6..f95e29a 100644 --- a/src/data.zig +++ b/src/data.zig @@ -302,7 +302,6 @@ pub fn Atom(comptime T: type) type { const ptr: *AnimationParameters = @ptrCast(@alignCast(uncast)); ptr.is_deinit = true; ptr.original_ptr.removeChangeListener(ptr.change_listener_id); - // TODO: remove change listener on original atom } }.a;