From 62181cdd1705e2046d1edfed14f6970ddc286791 Mon Sep 17 00:00:00 2001 From: zenith391 <39484230+zenith391@users.noreply.github.com> Date: Sun, 5 Feb 2023 18:39:20 +0100 Subject: [PATCH] basic time feed example --- android/src/jni.zig | 1 + examples/graph.zig | 2 +- examples/time-feed.zig | 105 +++++++++++++++++++++++++++++++++++++++++ src/containers.zig | 15 +++++- src/internal.zig | 7 +++ src/text.zig | 4 +- src/widget.zig | 5 ++ 7 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 examples/time-feed.zig diff --git a/android/src/jni.zig b/android/src/jni.zig index 8f130541..44dab4c9 100644 --- a/android/src/jni.zig +++ b/android/src/jni.zig @@ -77,6 +77,7 @@ pub const JNI = opaque { return jni.invokeJni(.CallObjectMethod, .{ object, method_id } ++ args); } + // This is required as float arguments get corrupted when passed as varargs fn argsToValueArray(args: anytype) [args.len]android.jvalue { var values: [args.len]android.jvalue = undefined; inline for (args) |arg, i| { diff --git a/examples/graph.zig b/examples/graph.zig index cfa65788..95278ceb 100644 --- a/examples/graph.zig +++ b/examples/graph.zig @@ -66,7 +66,7 @@ pub const LineGraph_Impl = struct { ctx.setColor(0, 0, 0); ctx.line(oldX, oldY, dx, dy); ctx.stroke(); - ctx.ellipse(oldX, oldY, 3, 3); + ctx.ellipse(oldX - 3, oldY - 3, 6, 6); ctx.fill(); oldX = dx; oldY = dy; diff --git a/examples/time-feed.zig b/examples/time-feed.zig new file mode 100644 index 00000000..dfee4194 --- /dev/null +++ b/examples/time-feed.zig @@ -0,0 +1,105 @@ +const std = @import("std"); +const capy = @import("capy"); +pub usingnamespace capy.cross_platform; + +// All time values are in UNIX timestamp +const TimeActivity = struct { + start: u64, + end: u64, + description: []const u8, +}; + +const ListModel = struct { + size: capy.DataWrapper(usize) = capy.DataWrapper(usize).of(0), + arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(capy.internal.lasting_allocator), + data: std.ArrayList(TimeActivity), + + pub fn add(self: *ListModel, activity: TimeActivity) !void { + try self.data.append(activity); + self.size.set(self.size.get() + 1); + } + + pub fn getComponent(self: *ListModel, index: usize) capy.Container_Impl { + const activity = self.data.items[index]; + const start_epoch = std.time.epoch.EpochSeconds{ .secs = activity.start }; + const start_day = start_epoch.getDaySeconds(); + + const end_epoch = std.time.epoch.EpochSeconds{ .secs = activity.end }; + const end_day = end_epoch.getDaySeconds(); + return Card(capy.Column(.{}, .{ + capy.Label(.{ + .text = std.fmt.allocPrintZ(self.arena.allocator(), "{d:0>2}:{d:0>2} - {d:0>2}:{d:0>2}", .{ + start_day.getHoursIntoDay(), + start_day.getMinutesIntoHour(), + end_day.getHoursIntoDay(), + end_day.getMinutesIntoHour(), + }) catch unreachable, + }), + capy.Label(.{ .text = activity.description }), + capy.Align(.{ .x = 1 }, capy.Button(.{ .label = "Edit" })), + })) catch unreachable; + } +}; + +pub fn Card(child: anytype) anyerror!capy.Container_Impl { + return try capy.Stack(.{ + capy.Rect(.{ .color = capy.Color.comptimeFromString("#ffffff") }), + capy.Margin(capy.Rectangle.init(10, 10, 10, 10), try child), + }); +} + +var submitDesc = capy.StringDataWrapper.of(""); +var submitEnabled = capy.DataWrapper(bool).of(false); +var list_model: ListModel = undefined; + +fn onSubmit(_: *anyopaque) !void { + try list_model.add(.{ + .start = @intCast(u64, std.time.timestamp() - 1000), + .end = @intCast(u64, std.time.timestamp()), + .description = submitDesc.get(), + }); + + // clear description + submitDesc.set(""); +} + +pub fn InsertCard() anyerror!capy.Container_Impl { + submitEnabled.dependOn(.{&submitDesc}, &(struct { + fn callback(description: []const u8) bool { + return description.len > 0; + } + }.callback)) catch unreachable; + + return try capy.Column(.{}, .{ + // TODO: TextArea when it supports data wrappers + capy.TextField(.{ .name = "description" }) + .bind("text", &submitDesc), // placeholder = "Task description..." + capy.Label(.{ .text = "Going on since.. 00:00:20" }), + capy.Align(.{ .x = 1 }, capy.Row(.{}, .{ + capy.Button(.{ .label = "Submit", .onclick = onSubmit }) + .bind("enabled", &submitEnabled), + capy.Button(.{ .label = "Delete" }), // TODO: icon + })), + }); +} + +pub fn main() !void { + try capy.backend.init(); + + list_model = ListModel{ + .data = std.ArrayList(TimeActivity).init(capy.internal.lasting_allocator), + }; + var window = try capy.Window.init(); + try window.set(capy.Column(.{}, .{ + capy.Label(.{ .text = "Feed" }), // TODO: capy.Heading ? + InsertCard(), + // TODO: days labels / list categories + capy.ColumnList(.{}, &list_model), + })); + + window.setTitle("Time Feed"); + window.resize(250, 100); + window.show(); + + capy.runEventLoop(); +} diff --git a/src/containers.zig b/src/containers.zig index 133acb8c..8397e735 100644 --- a/src/containers.zig +++ b/src/containers.zig @@ -188,7 +188,7 @@ pub fn MarginLayout(peer: Callbacks, widgets: []Widget) void { //const finalSize = Size.intersect(preferredSize, available); _ = preferredSize; const finalSize = available; - + //peer.moveResize(peer.userdata, widgetPeer, 0, 0, finalSize.width, finalSize.height); peer.moveResize(peer.userdata, widgetPeer, left, top, finalSize.width -| left -| right, finalSize.height -| top -| bottom); } @@ -359,7 +359,18 @@ pub const Container_Impl = struct { .computingPreferredSize = false, .layoutConfig = self.layoutConfig, }; - self.layout(callbacks, self.childrens.items); + + var tempItems = std.ArrayList(Widget).init(self.childrens.allocator); + defer tempItems.deinit(); + for (self.childrens.items) |child| { + if (child.isDisplayed()) { + tempItems.append(child) catch return; + } else { + peer.remove(child.peer.?); + } + } + + self.layout(callbacks, tempItems.items); self.relayouting.store(false, .SeqCst); } } diff --git a/src/internal.zig b/src/internal.zig index 8f91e936..20ad91f7 100644 --- a/src/internal.zig +++ b/src/internal.zig @@ -63,10 +63,12 @@ pub fn Widgeting(comptime T: type) type { .preferredSizeFn = getPreferredSizeWidget, .setWidgetFn = setWidgetFn, .getParentFn = widget_getParent, + .isDisplayedFn = isDisplayedFn, }; pub const DataWrappers = struct { opacity: DataWrapper(f32) = DataWrapper(f32).of(1.0), + displayed: DataWrapper(bool) = DataWrapper(bool).of(true), name: DataWrapper(?[]const u8) = DataWrapper(?[]const u8).of(null), /// The widget representing this component @@ -91,6 +93,11 @@ pub fn Widgeting(comptime T: type) type { component.dataWrappers.widget = widget; } + pub fn isDisplayedFn(widget: *const Widget) bool { + const component = widget.as(T); + return component.dataWrappers.displayed.get(); + } + pub fn deinitWidget(widget: *Widget) void { const component = widget.as(T); component.deinit(); diff --git a/src/text.zig b/src/text.zig index 34b2a34e..173f64bb 100644 --- a/src/text.zig +++ b/src/text.zig @@ -64,10 +64,12 @@ pub const TextField_Impl = struct { _wrapperTextBlock: std.atomic.Atomic(bool) = std.atomic.Atomic(bool).init(false), pub fn init(config: TextField_Impl.Config) TextField_Impl { - return TextField_Impl.init_events(TextField_Impl{ + var field = TextField_Impl.init_events(TextField_Impl{ .text = StringDataWrapper.of(config.text), .readOnly = DataWrapper(bool).of(config.readOnly), }); + field.setName(config.name); + return field; } /// Internal function used at initialization. diff --git a/src/widget.zig b/src/widget.zig index e9f280d3..55e767e2 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -18,6 +18,7 @@ pub const Class = struct { /// unknown. This function is thus called internally to pair the widget. setWidgetFn: *const fn (widget: *Widget) void, getParentFn: *const fn (widget: *const Widget) ?*Widget, + isDisplayedFn: *const fn (widget: *const Widget) bool, // offset into a list of updater optional pointers //updaters: []const usize, }; @@ -53,6 +54,10 @@ pub const Widget = struct { return self.class.getParentFn(self); } + pub fn isDisplayed(self: *const Widget) bool { + return self.class.isDisplayedFn(self); + } + /// Asserts widget data is of type T pub fn as(self: *const Widget, comptime T: type) *T { if (std.debug.runtime_safety) {