diff --git a/examples/7gui/counter.zig b/examples/7gui/counter.zig index de3c828a..6a835cff 100644 --- a/examples/7gui/counter.zig +++ b/examples/7gui/counter.zig @@ -55,6 +55,6 @@ pub fn main() !void { window.show(); // Count to 100 in 2000ms - count.animate(capy.Easings.InOut, 100, 2000); + count.animate(window.animation_controller, capy.Easings.InOut, 100, 2000); capy.runEventLoop(); } diff --git a/examples/colors.zig b/examples/colors.zig index e0dbe88b..ff2661a0 100644 --- a/examples/colors.zig +++ b/examples/colors.zig @@ -12,7 +12,8 @@ pub fn animateRandomColor(button_: *anyopaque) !void { const root = button.getRoot().?.as(capy.Container); const rect = root.getChild("background-rectangle").?.as(capy.Rect); const randomColor = capy.Color{ .red = random.int(u8), .green = random.int(u8), .blue = random.int(u8) }; - rect.color.animate(capy.Easings.InOut, randomColor, 1000); + const animator = button.getAnimationController(); + rect.color.animate(animator, capy.Easings.InOut, randomColor, 1000); } pub fn main() !void { diff --git a/examples/demo.zig b/examples/demo.zig index 0fdb2478..82fa030d 100644 --- a/examples/demo.zig +++ b/examples/demo.zig @@ -163,13 +163,14 @@ fn moveButton(button_: *anyopaque) !void { const button = @as(*capy.Button, @ptrCast(@alignCast(button_))); const parent = button.getParent().?.as(capy.Alignment); + const animator = button.getAnimationController(); const alignX = &parent.x; // Ensure the current animation is done before starting another if (!alignX.hasAnimation()) { if (alignX.get() == 0) { // if on the left - alignX.animate(capy.Easings.InOut, 1, 1000); + alignX.animate(animator, capy.Easings.InOut, 1, 1000); } else { - alignX.animate(capy.Easings.InOut, 0, 1000); + alignX.animate(animator, capy.Easings.InOut, 0, 1000); } } } diff --git a/examples/fade.zig b/examples/fade.zig index 516416c6..61dbbd56 100644 --- a/examples/fade.zig +++ b/examples/fade.zig @@ -7,16 +7,17 @@ var opacity = capy.Atom(f32).of(1.0); // TODO: switch back to *capy.Button_Impl when ziglang/zig#12325 is fixed fn startAnimation(button_: *anyopaque) !void { const button = @as(*capy.Button, @ptrCast(@alignCast(button_))); + const animator = button.getAnimationController(); // Ensure the current animation is done before starting another if (!opacity.hasAnimation()) { if (opacity.get() == 0) { // if hidden // Show the label in 1000ms - opacity.animate(capy.Easings.In, 1, 1000); + opacity.animate(animator, capy.Easings.In, 1, 1000); button.setLabel("Hide"); } else { // Hide the label in 1000ms - opacity.animate(capy.Easings.Out, 0, 1000); + opacity.animate(animator, capy.Easings.Out, 0, 1000); button.setLabel("Show"); } } diff --git a/examples/osm-viewer.zig b/examples/osm-viewer.zig index 4ab7aedb..2421d961 100644 --- a/examples/osm-viewer.zig +++ b/examples/osm-viewer.zig @@ -237,8 +237,8 @@ pub const MapViewer = struct { self.peer = try capy.backend.Canvas.create(); try self.setupEvents(); } - try self.centerX.implicitlyAnimate(&self.targetCenterX, capy.Easings.InOut, 50); - try self.centerY.implicitlyAnimate(&self.targetCenterY, capy.Easings.InOut, 50); + try self.centerX.implicitlyAnimate(&self.targetCenterX, self.getAnimationController(), capy.Easings.InOut, 50); + try self.centerY.implicitlyAnimate(&self.targetCenterY, self.getAnimationController(), capy.Easings.InOut, 50); _ = try self.centerX.addChangeListener(.{ .function = onCenterChange, .userdata = self }); _ = try self.centerY.addChangeListener(.{ .function = onCenterChange, .userdata = self }); diff --git a/src/AnimationController.zig b/src/AnimationController.zig index 4e9d216e..11222928 100644 --- a/src/AnimationController.zig +++ b/src/AnimationController.zig @@ -30,7 +30,7 @@ pub fn init(allocator: std.mem.Allocator, on_frame: *EventSource) !*AnimationCon }); try listener.enabled.dependOn(.{&controller.animated_atoms.length}, &struct { fn callback(length: usize) bool { - return length > 0; + return length >= 0; } }.callback); controller.listener = listener; @@ -39,12 +39,40 @@ pub fn init(allocator: std.mem.Allocator, on_frame: *EventSource) !*AnimationCon fn update(ptr: ?*anyopaque) void { const self: *AnimationController = @ptrCast(@alignCast(ptr.?)); - var iterator = self.animated_atoms.iterate(); - defer iterator.deinit(); - while (iterator.next()) |item| { - if (item.fnPtr(item.userdata) == true) { - // TODO: remove. + // List of atoms that are no longer animated and that need to be removed from the list + var toRemove = std.BoundedArray(usize, 64).init(0) catch unreachable; + { + var iterator = self.animated_atoms.iterate(); + defer iterator.deinit(); + { + var i: usize = 0; + while (iterator.next()) |item| : (i += 1) { + if (item.fnPtr(item.userdata) == false) { // animation ended + toRemove.append(i) catch |err| switch (err) { + error.Overflow => {}, // It can be removed on the next call to animateAtoms() + }; + } + } + } + + // The following code is part of the same block as swapRemove relies on the caller locking + // the mutex + { + // The index list is ordered in increasing index order + const indexList = toRemove.constSlice(); + // So we iterate it backward in order to avoid indices being invalidated + if (indexList.len > 0) { + var i: usize = indexList.len - 1; + while (i >= 0) { + _ = self.animated_atoms.swapRemove(indexList[i]); + if (i == 0) { + break; + } else { + i -= 1; + } + } + } } } } @@ -63,4 +91,4 @@ var null_animation_controller_instance = AnimationController{ /// This animation controller is never triggered. It is used by components that don't have a proper /// animation controller. /// It cannot be deinitialized. -pub var null_animation_controller = &null_animation_controller_instance; +pub const null_animation_controller = &null_animation_controller_instance; diff --git a/src/capy.zig b/src/capy.zig index 2adf5987..fe936cfe 100644 --- a/src/capy.zig +++ b/src/capy.zig @@ -108,14 +108,6 @@ pub fn init() !void { Monitors.init(); - var listener = eventStep.listen(.{ .callback = animateAtoms }) catch unreachable; - // The listener is enabled only if there is at least 1 atom currently being animated - listener.enabled.dependOn(.{&@import("data.zig")._animatedAtomsLength}, &struct { - fn a(num: usize) bool { - 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 timer is running timerListener.enabled.dependOn(.{&@import("timer.zig").runningTimers.length}, &struct { @@ -130,8 +122,6 @@ pub fn deinit() void { isCapyInitialized = false; Monitors.deinit(); - @import("data.zig")._animatedAtoms.deinit(); - @import("data.zig")._animatedAtomsLength.deinit(); @import("timer.zig").runningTimers.deinit(); eventStep.deinitAllListeners(); diff --git a/src/components/Alignment.zig b/src/components/Alignment.zig index 22ac5a0e..b4b6d114 100644 --- a/src/components/Alignment.zig +++ b/src/components/Alignment.zig @@ -4,6 +4,7 @@ const internal = @import("../internal.zig"); const Size = @import("../data.zig").Size; const Atom = @import("../data.zig").Atom; const Widget = @import("../widget.zig").Widget; +const AnimationController = @import("../AnimationController.zig"); /// `Alignment` is a component used to align the enclosed component within the space /// it's been given. @@ -99,6 +100,12 @@ pub const Alignment = struct { peer.move(widgetPeer, x, y); peer.resize(widgetPeer, finalSize.width, finalSize.height); } + + _ = self.widget_data.atoms.animation_controller.addChangeListener(.{ + .function = onAnimationControllerChange, + .userdata = self, + }) catch {}; + onAnimationControllerChange(self.widget_data.atoms.animation_controller.get(), self); } } @@ -106,6 +113,11 @@ pub const Alignment = struct { return self.child.get().getPreferredSize(available); } + fn onAnimationControllerChange(new_value: *AnimationController, userdata: ?*anyopaque) void { + const self: *Alignment = @ptrCast(@alignCast(userdata)); + self.child.get().animation_controller.set(new_value); + } + pub fn cloneImpl(self: *Alignment) !*Alignment { _ = self; // const widget_clone = try self.child.get().clone(); diff --git a/src/containers.zig b/src/containers.zig index 619bb6f8..7bca81d7 100644 --- a/src/containers.zig +++ b/src/containers.zig @@ -5,6 +5,7 @@ const scratch_allocator = @import("internal.zig").scratch_allocator; const lasting_allocator = @import("internal.zig").lasting_allocator; const Size = @import("data.zig").Size; const Rectangle = @import("data.zig").Rectangle; +const AnimationController = @import("AnimationController.zig"); const capy = @import("capy.zig"); const isErrorUnion = @import("internal.zig").isErrorUnion; @@ -385,12 +386,13 @@ pub const Container = struct { pub fn show(self: *Container) !void { if (self.peer == null) { + _ = try self.widget_data.atoms.animation_controller.addChangeListener(.{ .function = onAnimationControllerChange, .userdata = self }); + // Trigger the onAnimationControllerChange function now so that the current animation + // controller propagates to children + onAnimationControllerChange(self.widget_data.atoms.animation_controller.get(), self); + var peer = try backend.Container.create(); for (self.children.items) |widget| { - if (self.expand) { - widget.container_expanded = true; - } - widget.parent = self.asWidget(); try widget.show(); peer.add(widget.peer.?); } @@ -490,6 +492,7 @@ pub const Container = struct { } genericWidget.parent = self.asWidget(); + genericWidget.animation_controller.set(self.widget_data.atoms.animation_controller.get()); genericWidget.ref(); try self.children.append(genericWidget); @@ -538,6 +541,13 @@ pub const Container = struct { self.children.deinit(); } + fn onAnimationControllerChange(newValue: *AnimationController, userdata: ?*anyopaque) void { + const self: *Container = @ptrCast(@alignCast(userdata)); + for (self.children.items) |child| { + child.animation_controller.set(newValue); + } + } + pub fn cloneImpl(self: *Container) !*Container { _ = self; // var children = std.ArrayList(Widget).init(lasting_allocator); diff --git a/src/data.zig b/src/data.zig index f95e29af..e00a8c9e 100644 --- a/src/data.zig +++ b/src/data.zig @@ -3,6 +3,7 @@ const Container_Impl = @import("containers.zig").Container_Impl; const internal = @import("internal.zig"); const lasting_allocator = internal.lasting_allocator; const trait = @import("trait.zig"); +const AnimationController = @import("AnimationController.zig"); /// Linear interpolation between floats a and b with factor t. fn lerpFloat(a: anytype, b: @TypeOf(a), t: f64) @TypeOf(a) { @@ -118,14 +119,6 @@ test isListAtom { try std.testing.expect(!isListAtom(Rectangle)); } -// TODO: use ListAtom when it's done -pub var _animatedAtoms = std.ArrayList(struct { - fnPtr: *const fn (data: *anyopaque) bool, - userdata: *anyopaque, -}).init(lasting_allocator); -pub var _animatedAtomsLength = Atom(usize).of(0); -pub var _animatedAtomsMutex = std.Thread.Mutex{}; - fn isAnimatableType(comptime T: type) bool { if (trait.isNumber(T) or (comptime trait.isContainer(T) and @hasDecl(T, "lerp"))) { return true; @@ -208,6 +201,11 @@ pub fn Atom(comptime T: type) type { fn computeChecksum(value: T) u8 { const Crc = std.hash.crc.Crc8Wcdma; + + // comptime causes a lot of problems with hashing, so we just set the checksum to + // 0, it's only used to detect potentially changed states, so it is not a problem. + if (@inComptime()) return 0; + return switch (@typeInfo(T).pointer.size) { .One => Crc.hash(std.mem.asBytes(value)), .Many, .C, .Slice => Crc.hash(std.mem.sliceAsBytes(value)), @@ -258,7 +256,7 @@ pub fn Atom(comptime T: type) type { /// Makes the atom follow the value of the given atom, but with an animation added. /// Note that the animated atom is automatically destroyed when the original atom is destroyed. - pub fn implicitlyAnimate(self: *Self, original: *Self, easing: Easing, duration: u64) !void { + pub fn implicitlyAnimate(self: *Self, original: *Self, controller: *AnimationController, easing: Easing, duration: u64) !void { self.set(original.get()); const AnimationParameters = struct { easing: Easing, @@ -267,6 +265,7 @@ pub fn Atom(comptime T: type) type { original_ptr: *Self, is_deinit: bool = false, change_listener_id: usize, + controller: *AnimationController, }; const userdata = try internal.lasting_allocator.create(AnimationParameters); @@ -276,12 +275,13 @@ pub fn Atom(comptime T: type) type { .self_ptr = self, .original_ptr = original, .change_listener_id = undefined, + .controller = controller, }; const animate_fn = struct { fn a(new_value: T, uncast: ?*anyopaque) void { const ptr: *AnimationParameters = @ptrCast(@alignCast(uncast)); - ptr.self_ptr.animate(ptr.easing, new_value, ptr.duration); + ptr.self_ptr.animate(ptr.controller, ptr.easing, new_value, ptr.duration); } }.a; @@ -349,9 +349,9 @@ pub fn Atom(comptime T: type) type { /// Starts an animation on the atom, from the current value to the `target` value. The /// animation will last `duration` milliseconds. - pub fn animate(self: *Self, anim: *const fn (f64) f64, target: T, duration: u64) void { + pub fn animate(self: *Self, controller: *AnimationController, anim: *const fn (f64) f64, target: T, duration: u64) void { if (comptime !isAnimatable) { - @compileError("animate() called on data that is not animable"); + @compileError("animate() called on data that is not animatable"); } const currentValue = self.get(); self.value = .{ .Animated = Animation(T){ @@ -362,19 +362,22 @@ pub fn Atom(comptime T: type) type { .animFn = anim, } }; - var contains = false; - _animatedAtomsMutex.lock(); - defer _animatedAtomsMutex.unlock(); - - for (_animatedAtoms.items) |item| { - if (@as(*anyopaque, @ptrCast(self)) == item.userdata) { - contains = true; - break; + const is_already_animated = blk: { + var iterator = controller.animated_atoms.iterate(); + defer iterator.deinit(); + while (iterator.next()) |item| { + if (@as(*anyopaque, @ptrCast(self)) == item.userdata) { + break :blk true; + } } - } - if (!contains) { - _animatedAtoms.append(.{ .fnPtr = @as(*const fn (*anyopaque) bool, @ptrCast(&Self.update)), .userdata = self }) catch {}; - _animatedAtomsLength.set(_animatedAtoms.items.len); + break :blk false; + }; + + if (!is_already_animated) { + controller.animated_atoms.append(.{ + .fnPtr = @as(*const fn (*anyopaque) bool, @ptrCast(&Self.update)), + .userdata = self, + }) catch {}; } } @@ -1238,10 +1241,14 @@ test "animated atom" { defer original.deinit(); { - var animated = try Atom(i32).withImplicitAnimation(&original, Easings.Linear, 5000); + const animation_controller = AnimationController.null_animation_controller; + var animated = try Atom(i32).withImplicitAnimation( + animation_controller, + &original, + Easings.Linear, + 5000, + ); defer animated.deinit(); - defer _animatedAtoms.clearAndFree(); - defer _animatedAtomsLength.set(0); original.set(1000); try std.testing.expect(animated.hasAnimation()); diff --git a/src/internal.zig b/src/internal.zig index 6ce5287c..e132f8d7 100644 --- a/src/internal.zig +++ b/src/internal.zig @@ -14,6 +14,7 @@ const Container = @import("containers.zig").Container; const Layout = @import("containers.zig").Layout; const MouseButton = @import("backends/shared.zig").MouseButton; const trait = @import("trait.zig"); +const AnimationController = @import("AnimationController.zig"); const link_libc = @import("builtin").link_libc; @@ -79,6 +80,7 @@ pub fn Widgeting(comptime T: type) type { opacity: Atom(f32) = Atom(f32).of(1.0), displayed: Atom(bool) = Atom(bool).of(true), name: Atom(?[]const u8) = Atom(?[]const u8).of(null), + animation_controller: Atom(*AnimationController) = Atom(*AnimationController).of(AnimationController.null_animation_controller), }; pub const Config = GenerateConfigStruct(T); @@ -293,8 +295,8 @@ pub fn Widgeting(comptime T: type) type { pub fn getRoot(self: *T) ?*Widget { var parent = self.getParent() orelse return null; while (true) { - const ancester = parent.getParent(); - if (ancester) |newParent| { + const ancestor = parent.getParent(); + if (ancestor) |newParent| { parent = newParent; } else { break; @@ -304,6 +306,10 @@ pub fn Widgeting(comptime T: type) type { return parent; } + pub fn getAnimationController(self: *T) *AnimationController { + return self.widget_data.atoms.animation_controller.get(); + } + // /// Clone the component but with the peer set to null pub fn clone(self: *T) !*T { _ = self; @@ -475,6 +481,7 @@ pub fn genericWidgetFrom(component: anytype) Widget { .data = component, .class = &Dereferenced.WidgetClass, .name = &component.widget_data.atoms.name, + .animation_controller = &component.widget_data.atoms.animation_controller, }; } diff --git a/src/listener.zig b/src/listener.zig index a87a1af5..f3028328 100644 --- a/src/listener.zig +++ b/src/listener.zig @@ -10,6 +10,12 @@ pub var null_event_source = EventSource.init(internal.lasting_allocator); pub const EventSource = struct { listeners: ListAtom(*Listener), + pub fn alloc(allocator: std.mem.Allocator) !*EventSource { + const source = try allocator.create(EventSource); + source.* = EventSource.init(allocator); + return source; + } + pub fn init(allocator: std.mem.Allocator) EventSource { return .{ .listeners = ListAtom(*Listener).init(allocator) }; } diff --git a/src/widget.zig b/src/widget.zig index 9c8743f3..0b04dbb4 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -3,6 +3,7 @@ const backend = @import("backend.zig"); const data = @import("data.zig"); const Allocator = std.mem.Allocator; +const AnimationController = @import("AnimationController.zig"); /// A class is a constant list of methods that can be called using Widget. // Note: it is called Class instead of VTable as it was made before allocgate @@ -38,6 +39,7 @@ pub const Widget = struct { // TODO: store @offsetOf these fields in the Class instead of having the cost of 3 pointers name: *data.Atom(?[]const u8), + animation_controller: *data.Atom(*AnimationController), pub fn show(self: *Widget) anyerror!void { try self.class.showFn(self); diff --git a/src/window.zig b/src/window.zig index c2920898..f66c1c4c 100644 --- a/src/window.zig +++ b/src/window.zig @@ -9,6 +9,7 @@ const Size = @import("data.zig").Size; const Atom = @import("data.zig").Atom; const EventSource = listener.EventSource; +const AnimationController = @import("AnimationController.zig"); const Monitor = @import("monitor.zig").Monitor; const VideoMode = @import("monitor.zig").VideoMode; const Display = struct { resolution: Size, dpi: u32 }; @@ -34,7 +35,8 @@ pub const Window = struct { screenRefreshRate: Atom(f32) = Atom(f32).of(60), /// Event source called whenever a frame would be drawn. /// This can be used for synchronizing animations to the window's monitor's sync rate. - on_frame: EventSource, + on_frame: *EventSource, + animation_controller: *AnimationController, pub const Feature = enum { Title, @@ -44,9 +46,14 @@ pub const Window = struct { pub fn init() !Window { const peer = try backend.Window.create(); + const on_frame = try EventSource.alloc(internal.lasting_allocator); var window = Window{ .peer = peer, - .on_frame = EventSource.init(internal.lasting_allocator), + .on_frame = on_frame, + .animation_controller = try AnimationController.init( + internal.lasting_allocator, + on_frame, + ), }; window.setSourceDpi(96); window.setPreferredSize(640, 480); @@ -77,6 +84,9 @@ pub const Window = struct { wrappedContainer; self._child = internal.getWidgetFrom(container); self._child.?.ref(); + + // Set the child's animation controller + self._child.?.animation_controller.set(self.animation_controller); try self._child.?.show(); self.peer.setChild(self._child.?.peer); }