this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

vxfw: improve mouse handling and hit testing

Modify the handling and hit testing:

- Only widgets which have event handlers or capture handlers are
considered for the hit list
- The topmost widget is always the target. We used to consider the last
widget which handled the mouse as the target. Now we consider the
topmost the target. This lets us generate an explicit mouse_enter
event since we can determine this before sending events
- Modify relevant widgets to remove noopEventHandler and remove this
function entirely
- mouse_enter and mouse_leave events are based on how browsers determine
these events. Any widget hit this frame that was not hit last frame
gets a mouse_enter. Any widget which was hit last_frame but not this
frame gets a mouse_leave.

+67 -74
+55 -56
src/vxfw/App.zig
··· 81 81 var buffered = tty.bufferedWriter(); 82 82 83 83 var mouse_handler = MouseHandler.init(widget); 84 + defer mouse_handler.deinit(self.allocator); 84 85 var focus_handler = FocusHandler.init(self.allocator, widget); 85 86 focus_handler.intrusiveInit(); 86 87 try focus_handler.path_to_focused.append(widget); ··· 208 209 209 210 const MouseHandler = struct { 210 211 last_frame: vxfw.Surface, 211 - maybe_last_handler: ?vxfw.Widget = null, 212 + last_hit_list: []vxfw.HitResult, 212 213 213 214 fn init(root: Widget) MouseHandler { 214 215 return .{ ··· 218 219 .buffer = &.{}, 219 220 .children = &.{}, 220 221 }, 221 - .maybe_last_handler = null, 222 + .last_hit_list = &.{}, 222 223 }; 223 224 } 224 225 226 + fn deinit(self: MouseHandler, gpa: Allocator) void { 227 + gpa.free(self.last_hit_list); 228 + } 229 + 225 230 fn handleMouse(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext, mouse: vaxis.Mouse) anyerror!void { 231 + // For mouse events we store the last frame and use that for hit testing 226 232 const last_frame = self.last_frame; 227 233 228 - // For mouse events we store the last frame and use that for hit testing 229 234 var hits = std.ArrayList(vxfw.HitResult).init(app.allocator); 230 235 defer hits.deinit(); 231 236 const sub: vxfw.SubSurface = .{ ··· 241 246 try last_frame.hitTest(&hits, mouse_point); 242 247 } 243 248 244 - // See if our new hit test contains our last handler. If it doesn't we'll send a mouse_leave 245 - // event 246 - if (self.maybe_last_handler) |last_handler| { 247 - for (hits.items) |item| { 248 - if (item.widget.eql(last_handler)) break; 249 - } else { 250 - try last_handler.handleEvent(ctx, .mouse_leave); 251 - self.maybe_last_handler = null; 252 - try app.handleCommand(&ctx.cmds); 249 + // Handle mouse_enter and mouse_leave events 250 + { 251 + // We store the hit list from the last mouse event to determine mouse_enter and mouse_leave 252 + // events. If list a is the previous hit list, and list b is the current hit list: 253 + // - Widgets in a but not in b get a mouse_leave event 254 + // - Widgets in b but not in a get a mouse_enter event 255 + // - Widgets in both receive nothing 256 + const a = self.last_hit_list; 257 + const b = hits.items; 258 + 259 + // Find widgets in a but not b 260 + for (a) |a_item| { 261 + const a_widget = a_item.widget; 262 + for (b) |b_item| { 263 + const b_widget = b_item.widget; 264 + if (a_widget.eql(b_widget)) break; 265 + } else { 266 + // a_item is not in b 267 + try a_widget.handleEvent(ctx, .mouse_leave); 268 + try app.handleCommand(&ctx.cmds); 269 + } 270 + } 271 + 272 + // Widgets in b but not in a 273 + for (b) |b_item| { 274 + const b_widget = b_item.widget; 275 + for (a) |a_item| { 276 + const a_widget = a_item.widget; 277 + if (b_widget.eql(a_widget)) break; 278 + } else { 279 + // b_item is not in a. 280 + try b_widget.handleEvent(ctx, .mouse_enter); 281 + try app.handleCommand(&ctx.cmds); 282 + } 253 283 } 284 + 285 + // Store a copy of this hit list for next frame 286 + app.allocator.free(self.last_hit_list); 287 + self.last_hit_list = try app.allocator.dupe(vxfw.HitResult, hits.items); 254 288 } 255 289 256 - const maybe_target = hits.popOrNull(); 290 + const target = hits.popOrNull() orelse return; 257 291 258 292 // capturing phase 259 293 ctx.phase = .capturing; ··· 264 298 try item.widget.captureEvent(ctx, .{ .mouse = m_local }); 265 299 try app.handleCommand(&ctx.cmds); 266 300 267 - // If the event was consumed, we check if we need to send a mouse_leave and return 268 - if (ctx.consume_event) { 269 - if (self.maybe_last_handler) |last_handler| { 270 - if (!last_handler.eql(item.widget)) { 271 - try last_handler.handleEvent(ctx, .mouse_leave); 272 - self.maybe_last_handler = item.widget; 273 - try app.handleCommand(&ctx.cmds); 274 - } 275 - } 276 - self.maybe_last_handler = item.widget; 277 - return; 278 - } 301 + if (ctx.consume_event) return; 279 302 } 280 303 281 304 // target phase 282 305 ctx.phase = .at_target; 283 - if (maybe_target) |target| { 306 + { 284 307 var m_local = mouse; 285 308 m_local.col = target.local.col; 286 309 m_local.row = target.local.row; 287 310 try target.widget.handleEvent(ctx, .{ .mouse = m_local }); 288 311 try app.handleCommand(&ctx.cmds); 289 - // If the event was consumed, we check if we need to send a mouse_leave and return 290 - if (ctx.consume_event) { 291 - if (self.maybe_last_handler) |last_handler| { 292 - if (!last_handler.eql(target.widget)) { 293 - try last_handler.handleEvent(ctx, .mouse_leave); 294 - self.maybe_last_handler = target.widget; 295 - try app.handleCommand(&ctx.cmds); 296 - } 297 - } 298 - self.maybe_last_handler = target.widget; 299 - return; 300 - } 312 + 313 + if (ctx.consume_event) return; 301 314 } 302 315 303 316 // Bubbling phase ··· 309 322 try item.widget.handleEvent(ctx, .{ .mouse = m_local }); 310 323 try app.handleCommand(&ctx.cmds); 311 324 312 - // If the event was consumed, we check if we need to send a mouse_leave and return 313 - if (ctx.consume_event) { 314 - if (self.maybe_last_handler) |last_handler| { 315 - if (!last_handler.eql(item.widget)) { 316 - try last_handler.handleEvent(ctx, .mouse_leave); 317 - self.maybe_last_handler = item.widget; 318 - try app.handleCommand(&ctx.cmds); 319 - } 320 - } 321 - self.maybe_last_handler = item.widget; 322 - return; 323 - } 325 + if (ctx.consume_event) return; 324 326 } 325 - 326 - // If no one handled the mouse, we assume it exited 327 - return self.mouseExit(app, ctx); 328 327 } 329 328 329 + /// sends .mouse_leave to all of the widgets from the last_hit_list 330 330 fn mouseExit(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext) anyerror!void { 331 - if (self.maybe_last_handler) |last_handler| { 332 - try last_handler.handleEvent(ctx, .mouse_leave); 331 + for (self.last_hit_list) |item| { 332 + try item.widget.handleEvent(ctx, .mouse_leave); 333 333 try app.handleCommand(&ctx.cmds); 334 - self.maybe_last_handler = null; 335 334 } 336 335 } 337 336 };
+6 -7
src/vxfw/Button.zig
··· 57 57 self.mouse_down = true; 58 58 return ctx.consumeAndRedraw(); 59 59 } 60 - if (!self.has_mouse) { 61 - self.has_mouse = true; 62 - 63 - // implicit redraw 64 - try ctx.setMouseShape(.pointer); 65 - return ctx.consumeAndRedraw(); 66 - } 67 60 return ctx.consumeEvent(); 61 + }, 62 + .mouse_enter => { 63 + // implicit redraw 64 + self.has_mouse = true; 65 + try ctx.setMouseShape(.pointer); 66 + return ctx.consumeAndRedraw(); 68 67 }, 69 68 .mouse_leave => { 70 69 self.has_mouse = false;
-1
src/vxfw/FlexColumn.zig
··· 12 12 pub fn widget(self: *const FlexColumn) vxfw.Widget { 13 13 return .{ 14 14 .userdata = @constCast(self), 15 - .eventHandler = vxfw.noopEventHandler, 16 15 .drawFn = typeErasedDrawFn, 17 16 }; 18 17 }
-1
src/vxfw/FlexRow.zig
··· 12 12 pub fn widget(self: *const FlexRow) vxfw.Widget { 13 13 return .{ 14 14 .userdata = @constCast(self), 15 - .eventHandler = vxfw.noopEventHandler, 16 15 .drawFn = typeErasedDrawFn, 17 16 }; 18 17 }
-1
src/vxfw/RichText.zig
··· 22 22 pub fn widget(self: *const RichText) vxfw.Widget { 23 23 return .{ 24 24 .userdata = @constCast(self), 25 - .eventHandler = vxfw.noopEventHandler, 26 25 .drawFn = typeErasedDrawFn, 27 26 }; 28 27 }
-1
src/vxfw/SizedBox.zig
··· 39 39 pub fn widget(self: *@This()) vxfw.Widget { 40 40 return .{ 41 41 .userdata = self, 42 - .eventHandler = vxfw.noopEventHandler, 43 42 .drawFn = @This().typeErasedDrawFn, 44 43 }; 45 44 }
-1
src/vxfw/Text.zig
··· 17 17 pub fn widget(self: *const Text) vxfw.Widget { 18 18 return .{ 19 19 .userdata = @constCast(self), 20 - .eventHandler = vxfw.noopEventHandler, 21 20 .drawFn = typeErasedDrawFn, 22 21 }; 23 22 }
+6 -6
src/vxfw/vxfw.zig
··· 47 47 tick, // An event from a Tick command 48 48 init, // sent when the application starts 49 49 mouse_leave, // The mouse has left the widget 50 + mouse_enter, // The mouse has enterred the widget 50 51 }; 51 52 52 53 pub const Tick = struct { ··· 216 217 return self.drawFn(self.userdata, ctx); 217 218 } 218 219 219 - /// Returns true if the Widgets point to the same widget instance 220 + /// Returns true if the Widgets point to the same widget instance. To be considered the same, 221 + /// the userdata and drawFn fields must point to the same values in both widgets 220 222 pub fn eql(self: Widget, other: Widget) bool { 221 223 return @intFromPtr(self.userdata) == @intFromPtr(other.userdata) and 222 - @intFromPtr(self.eventHandler) == @intFromPtr(other.eventHandler) and 223 224 @intFromPtr(self.drawFn) == @intFromPtr(other.drawFn); 224 225 } 225 226 }; ··· 337 338 /// always be translated to local Surface coordinates. Asserts that this Surface does contain Point 338 339 pub fn hitTest(self: Surface, list: *std.ArrayList(HitResult), point: Point) Allocator.Error!void { 339 340 assert(point.col < self.size.width and point.row < self.size.height); 340 - try list.append(.{ .local = point, .widget = self.widget }); 341 + // Add this widget to the hit list if it has an event or capture handler 342 + if (self.widget.eventHandler != null or self.widget.captureHandler != null) 343 + try list.append(.{ .local = point, .widget = self.widget }); 341 344 for (self.children) |child| { 342 345 if (!child.containsPoint(point)) continue; 343 346 const child_point: Point = .{ ··· 411 414 point.row < (self.origin.row + self.surface.size.height); 412 415 } 413 416 }; 414 - 415 - /// A noop event handler for widgets which don't require any event handling 416 - pub fn noopEventHandler(_: *anyopaque, _: *EventContext, _: Event) anyerror!void {} 417 417 418 418 test { 419 419 std.testing.refAllDecls(@This());