diff --git a/src/vxfw/App.zig b/src/vxfw/App.zig index 797e49d..61f89f5 100644 --- a/src/vxfw/App.zig +++ b/src/vxfw/App.zig @@ -81,6 +81,7 @@ pub fn run(self: *App, widget: vxfw.Widget, opts: Options) anyerror!void { var buffered = tty.bufferedWriter(); var mouse_handler = MouseHandler.init(widget); + defer mouse_handler.deinit(self.allocator); var focus_handler = FocusHandler.init(self.allocator, widget); focus_handler.intrusiveInit(); try focus_handler.path_to_focused.append(widget); @@ -208,7 +209,7 @@ fn checkTimers(self: *App, ctx: *vxfw.EventContext) anyerror!void { const MouseHandler = struct { last_frame: vxfw.Surface, - maybe_last_handler: ?vxfw.Widget = null, + last_hit_list: []vxfw.HitResult, fn init(root: Widget) MouseHandler { return .{ @@ -218,14 +219,18 @@ const MouseHandler = struct { .buffer = &.{}, .children = &.{}, }, - .maybe_last_handler = null, + .last_hit_list = &.{}, }; } + fn deinit(self: MouseHandler, gpa: Allocator) void { + gpa.free(self.last_hit_list); + } + fn handleMouse(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext, mouse: vaxis.Mouse) anyerror!void { + // For mouse events we store the last frame and use that for hit testing const last_frame = self.last_frame; - // For mouse events we store the last frame and use that for hit testing var hits = std.ArrayList(vxfw.HitResult).init(app.allocator); defer hits.deinit(); const sub: vxfw.SubSurface = .{ @@ -241,19 +246,48 @@ const MouseHandler = struct { try last_frame.hitTest(&hits, mouse_point); } - // See if our new hit test contains our last handler. If it doesn't we'll send a mouse_leave - // event - if (self.maybe_last_handler) |last_handler| { - for (hits.items) |item| { - if (item.widget.eql(last_handler)) break; - } else { - try last_handler.handleEvent(ctx, .mouse_leave); - self.maybe_last_handler = null; - try app.handleCommand(&ctx.cmds); + // Handle mouse_enter and mouse_leave events + { + // We store the hit list from the last mouse event to determine mouse_enter and mouse_leave + // events. If list a is the previous hit list, and list b is the current hit list: + // - Widgets in a but not in b get a mouse_leave event + // - Widgets in b but not in a get a mouse_enter event + // - Widgets in both receive nothing + const a = self.last_hit_list; + const b = hits.items; + + // Find widgets in a but not b + for (a) |a_item| { + const a_widget = a_item.widget; + for (b) |b_item| { + const b_widget = b_item.widget; + if (a_widget.eql(b_widget)) break; + } else { + // a_item is not in b + try a_widget.handleEvent(ctx, .mouse_leave); + try app.handleCommand(&ctx.cmds); + } } + + // Widgets in b but not in a + for (b) |b_item| { + const b_widget = b_item.widget; + for (a) |a_item| { + const a_widget = a_item.widget; + if (b_widget.eql(a_widget)) break; + } else { + // b_item is not in a. + try b_widget.handleEvent(ctx, .mouse_enter); + try app.handleCommand(&ctx.cmds); + } + } + + // Store a copy of this hit list for next frame + app.allocator.free(self.last_hit_list); + self.last_hit_list = try app.allocator.dupe(vxfw.HitResult, hits.items); } - const maybe_target = hits.popOrNull(); + const target = hits.popOrNull() orelse return; // capturing phase ctx.phase = .capturing; @@ -264,40 +298,19 @@ const MouseHandler = struct { try item.widget.captureEvent(ctx, .{ .mouse = m_local }); try app.handleCommand(&ctx.cmds); - // If the event was consumed, we check if we need to send a mouse_leave and return - if (ctx.consume_event) { - if (self.maybe_last_handler) |last_handler| { - if (!last_handler.eql(item.widget)) { - try last_handler.handleEvent(ctx, .mouse_leave); - self.maybe_last_handler = item.widget; - try app.handleCommand(&ctx.cmds); - } - } - self.maybe_last_handler = item.widget; - return; - } + if (ctx.consume_event) return; } // target phase ctx.phase = .at_target; - if (maybe_target) |target| { + { var m_local = mouse; m_local.col = target.local.col; m_local.row = target.local.row; try target.widget.handleEvent(ctx, .{ .mouse = m_local }); try app.handleCommand(&ctx.cmds); - // If the event was consumed, we check if we need to send a mouse_leave and return - if (ctx.consume_event) { - if (self.maybe_last_handler) |last_handler| { - if (!last_handler.eql(target.widget)) { - try last_handler.handleEvent(ctx, .mouse_leave); - self.maybe_last_handler = target.widget; - try app.handleCommand(&ctx.cmds); - } - } - self.maybe_last_handler = target.widget; - return; - } + + if (ctx.consume_event) return; } // Bubbling phase @@ -309,29 +322,15 @@ const MouseHandler = struct { try item.widget.handleEvent(ctx, .{ .mouse = m_local }); try app.handleCommand(&ctx.cmds); - // If the event was consumed, we check if we need to send a mouse_leave and return - if (ctx.consume_event) { - if (self.maybe_last_handler) |last_handler| { - if (!last_handler.eql(item.widget)) { - try last_handler.handleEvent(ctx, .mouse_leave); - self.maybe_last_handler = item.widget; - try app.handleCommand(&ctx.cmds); - } - } - self.maybe_last_handler = item.widget; - return; - } + if (ctx.consume_event) return; } - - // If no one handled the mouse, we assume it exited - return self.mouseExit(app, ctx); } + /// sends .mouse_leave to all of the widgets from the last_hit_list fn mouseExit(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext) anyerror!void { - if (self.maybe_last_handler) |last_handler| { - try last_handler.handleEvent(ctx, .mouse_leave); + for (self.last_hit_list) |item| { + try item.widget.handleEvent(ctx, .mouse_leave); try app.handleCommand(&ctx.cmds); - self.maybe_last_handler = null; } } }; diff --git a/src/vxfw/Button.zig b/src/vxfw/Button.zig index 70c3c8d..978acce 100644 --- a/src/vxfw/Button.zig +++ b/src/vxfw/Button.zig @@ -57,15 +57,14 @@ pub fn handleEvent(self: *Button, ctx: *vxfw.EventContext, event: vxfw.Event) an self.mouse_down = true; return ctx.consumeAndRedraw(); } - if (!self.has_mouse) { - self.has_mouse = true; - - // implicit redraw - try ctx.setMouseShape(.pointer); - return ctx.consumeAndRedraw(); - } return ctx.consumeEvent(); }, + .mouse_enter => { + // implicit redraw + self.has_mouse = true; + try ctx.setMouseShape(.pointer); + return ctx.consumeAndRedraw(); + }, .mouse_leave => { self.has_mouse = false; self.mouse_down = false; diff --git a/src/vxfw/FlexColumn.zig b/src/vxfw/FlexColumn.zig index 55a8890..219f7cc 100644 --- a/src/vxfw/FlexColumn.zig +++ b/src/vxfw/FlexColumn.zig @@ -12,7 +12,6 @@ children: []const vxfw.FlexItem, pub fn widget(self: *const FlexColumn) vxfw.Widget { return .{ .userdata = @constCast(self), - .eventHandler = vxfw.noopEventHandler, .drawFn = typeErasedDrawFn, }; } diff --git a/src/vxfw/FlexRow.zig b/src/vxfw/FlexRow.zig index 238f818..6ad9fd9 100644 --- a/src/vxfw/FlexRow.zig +++ b/src/vxfw/FlexRow.zig @@ -12,7 +12,6 @@ children: []const vxfw.FlexItem, pub fn widget(self: *const FlexRow) vxfw.Widget { return .{ .userdata = @constCast(self), - .eventHandler = vxfw.noopEventHandler, .drawFn = typeErasedDrawFn, }; } diff --git a/src/vxfw/RichText.zig b/src/vxfw/RichText.zig index c287c3b..8878f65 100644 --- a/src/vxfw/RichText.zig +++ b/src/vxfw/RichText.zig @@ -22,7 +22,6 @@ width_basis: enum { parent, longest_line } = .longest_line, pub fn widget(self: *const RichText) vxfw.Widget { return .{ .userdata = @constCast(self), - .eventHandler = vxfw.noopEventHandler, .drawFn = typeErasedDrawFn, }; } diff --git a/src/vxfw/SizedBox.zig b/src/vxfw/SizedBox.zig index b65e4ca..730c436 100644 --- a/src/vxfw/SizedBox.zig +++ b/src/vxfw/SizedBox.zig @@ -39,7 +39,6 @@ test SizedBox { pub fn widget(self: *@This()) vxfw.Widget { return .{ .userdata = self, - .eventHandler = vxfw.noopEventHandler, .drawFn = @This().typeErasedDrawFn, }; } diff --git a/src/vxfw/Text.zig b/src/vxfw/Text.zig index 8ed2aa0..6345e93 100644 --- a/src/vxfw/Text.zig +++ b/src/vxfw/Text.zig @@ -17,7 +17,6 @@ width_basis: enum { parent, longest_line } = .longest_line, pub fn widget(self: *const Text) vxfw.Widget { return .{ .userdata = @constCast(self), - .eventHandler = vxfw.noopEventHandler, .drawFn = typeErasedDrawFn, }; } diff --git a/src/vxfw/vxfw.zig b/src/vxfw/vxfw.zig index bb98731..0ea01f2 100644 --- a/src/vxfw/vxfw.zig +++ b/src/vxfw/vxfw.zig @@ -47,6 +47,7 @@ pub const Event = union(enum) { tick, // An event from a Tick command init, // sent when the application starts mouse_leave, // The mouse has left the widget + mouse_enter, // The mouse has enterred the widget }; pub const Tick = struct { @@ -216,10 +217,10 @@ pub const Widget = struct { return self.drawFn(self.userdata, ctx); } - /// Returns true if the Widgets point to the same widget instance + /// Returns true if the Widgets point to the same widget instance. To be considered the same, + /// the userdata and drawFn fields must point to the same values in both widgets pub fn eql(self: Widget, other: Widget) bool { return @intFromPtr(self.userdata) == @intFromPtr(other.userdata) and - @intFromPtr(self.eventHandler) == @intFromPtr(other.eventHandler) and @intFromPtr(self.drawFn) == @intFromPtr(other.drawFn); } }; @@ -337,7 +338,9 @@ pub const Surface = struct { /// always be translated to local Surface coordinates. Asserts that this Surface does contain Point pub fn hitTest(self: Surface, list: *std.ArrayList(HitResult), point: Point) Allocator.Error!void { assert(point.col < self.size.width and point.row < self.size.height); - try list.append(.{ .local = point, .widget = self.widget }); + // Add this widget to the hit list if it has an event or capture handler + if (self.widget.eventHandler != null or self.widget.captureHandler != null) + try list.append(.{ .local = point, .widget = self.widget }); for (self.children) |child| { if (!child.containsPoint(point)) continue; const child_point: Point = .{ @@ -412,9 +415,6 @@ pub const SubSurface = struct { } }; -/// A noop event handler for widgets which don't require any event handling -pub fn noopEventHandler(_: *anyopaque, _: *EventContext, _: Event) anyerror!void {} - test { std.testing.refAllDecls(@This()); }