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

Change the core loop for handling of mouse events. We now do a layout
phase, which calls draw on the root widget. From here, we see if the
widget underneath the mouse has changed and, if so, send mouse_enter and
mouse_exit events, then do another layout, and then *finally* we render.

This fixes a case where the mouse hasn't moved, but the content of the
screen has changed. The hover state of any widget will be updated in the
second layout phase if the app indicates a redraw is necessary from the
mouse_enter and mouse_exit events.

+137 -31
+137 -31
src/vxfw/App.zig
··· 83 83 var arena = std.heap.ArenaAllocator.init(self.allocator); 84 84 defer arena.deinit(); 85 85 86 - var buffered = tty.bufferedWriter(); 87 - 88 86 var mouse_handler = MouseHandler.init(widget); 89 87 defer mouse_handler.deinit(self.allocator); 90 88 var focus_handler = FocusHandler.init(self.allocator, widget); ··· 132 130 .focus_out => try mouse_handler.mouseExit(self, &ctx), 133 131 .mouse => |mouse| try mouse_handler.handleMouse(self, &ctx, mouse), 134 132 .winsize => |ws| { 135 - try vx.resize(self.allocator, buffered.writer().any(), ws); 136 - try buffered.flush(); 133 + try vx.resize(self.allocator, tty.anyWriter(), ws); 137 134 ctx.redraw = true; 138 135 }, 139 136 else => { ··· 143 140 } 144 141 } 145 142 143 + // If we have a focus change, handle that event before we layout 146 144 if (self.wants_focus) |wants_focus| { 147 145 try focus_handler.focusWidget(&ctx, wants_focus); 148 146 try self.handleCommand(&ctx.cmds); 147 + self.wants_focus = null; 149 148 } 150 149 151 150 // Check if we should quit ··· 154 153 // Check if we need a redraw 155 154 if (!ctx.redraw) continue; 156 155 ctx.redraw = false; 156 + // Clear the arena. 157 + _ = arena.reset(.free_all); 157 158 // Assert that we have handled all commands 158 159 assert(ctx.cmds.items.len == 0); 159 160 160 - _ = arena.reset(.retain_capacity); 161 + const surface: vxfw.Surface = blk: { 162 + // Draw the root widget 163 + const surface = try self.doLayout(widget, &arena); 161 164 162 - const draw_context: vxfw.DrawContext = .{ 163 - .arena = arena.allocator(), 164 - .min = .{ .width = 0, .height = 0 }, 165 - .max = .{ 166 - .width = @intCast(vx.screen.width), 167 - .height = @intCast(vx.screen.height), 168 - }, 169 - .cell_size = .{ 170 - .width = vx.screen.width_pix / vx.screen.width, 171 - .height = vx.screen.height_pix / vx.screen.height, 172 - }, 173 - }; 174 - const win = vx.window(); 175 - win.clear(); 176 - win.hideCursor(); 177 - win.setCursorShape(.default); 178 - const surface = try widget.draw(draw_context); 165 + // Check if any hover or mouse effects changed 166 + try mouse_handler.updateMouse(self, surface, &ctx); 167 + // Our focus may have changed. Handle that here 168 + if (self.wants_focus) |wants_focus| { 169 + try focus_handler.focusWidget(&ctx, wants_focus); 170 + try self.handleCommand(&ctx.cmds); 171 + self.wants_focus = null; 172 + } 179 173 180 - const root_win = win.child(.{ 181 - .width = surface.size.width, 182 - .height = surface.size.height, 183 - }); 184 - surface.render(root_win, focus_handler.focused_widget); 185 - try vx.render(buffered.writer().any()); 186 - try buffered.flush(); 174 + assert(ctx.cmds.items.len == 0); 175 + if (!ctx.redraw) break :blk surface; 176 + // If updating the mouse required a redraw, we do the layout again 177 + break :blk try self.doLayout(widget, &arena); 178 + }; 187 179 188 180 // Store the last frame 189 181 mouse_handler.last_frame = surface; 182 + // Update the focus handler list 190 183 try focus_handler.update(surface); 191 - self.wants_focus = null; 184 + try self.render(surface, focus_handler.focused_widget); 192 185 } 193 186 } 194 187 188 + fn doLayout( 189 + self: *App, 190 + widget: vxfw.Widget, 191 + arena: *std.heap.ArenaAllocator, 192 + ) !vxfw.Surface { 193 + const vx = &self.vx; 194 + 195 + const draw_context: vxfw.DrawContext = .{ 196 + .arena = arena.allocator(), 197 + .min = .{ .width = 0, .height = 0 }, 198 + .max = .{ 199 + .width = @intCast(vx.screen.width), 200 + .height = @intCast(vx.screen.height), 201 + }, 202 + .cell_size = .{ 203 + .width = vx.screen.width_pix / vx.screen.width, 204 + .height = vx.screen.height_pix / vx.screen.height, 205 + }, 206 + }; 207 + return widget.draw(draw_context); 208 + } 209 + 210 + fn render( 211 + self: *App, 212 + surface: vxfw.Surface, 213 + focused_widget: vxfw.Widget, 214 + ) !void { 215 + const vx = &self.vx; 216 + const tty = &self.tty; 217 + 218 + const win = vx.window(); 219 + win.clear(); 220 + win.hideCursor(); 221 + win.setCursorShape(.default); 222 + 223 + const root_win = win.child(.{ 224 + .width = surface.size.width, 225 + .height = surface.size.height, 226 + }); 227 + surface.render(root_win, focused_widget); 228 + 229 + var buffered = tty.bufferedWriter(); 230 + try vx.render(buffered.writer().any()); 231 + try buffered.flush(); 232 + } 233 + 195 234 fn addTick(self: *App, tick: vxfw.Tick) Allocator.Error!void { 196 235 try self.timers.append(tick); 197 236 std.sort.insertion(vxfw.Tick, self.timers.items, {}, vxfw.Tick.lessThan); ··· 239 278 const MouseHandler = struct { 240 279 last_frame: vxfw.Surface, 241 280 last_hit_list: []vxfw.HitResult, 281 + mouse: ?vaxis.Mouse, 242 282 243 283 fn init(root: Widget) MouseHandler { 244 284 return .{ ··· 249 289 .children = &.{}, 250 290 }, 251 291 .last_hit_list = &.{}, 292 + .mouse = null, 252 293 }; 253 294 } 254 295 ··· 256 297 gpa.free(self.last_hit_list); 257 298 } 258 299 300 + fn updateMouse( 301 + self: *MouseHandler, 302 + app: *App, 303 + surface: vxfw.Surface, 304 + ctx: *vxfw.EventContext, 305 + ) anyerror!void { 306 + const mouse = self.mouse orelse return; 307 + // For mouse events we store the last frame and use that for hit testing 308 + const last_frame = surface; 309 + 310 + var hits = std.ArrayList(vxfw.HitResult).init(app.allocator); 311 + defer hits.deinit(); 312 + const sub: vxfw.SubSurface = .{ 313 + .origin = .{ .row = 0, .col = 0 }, 314 + .surface = last_frame, 315 + .z_index = 0, 316 + }; 317 + const mouse_point: vxfw.Point = .{ 318 + .row = @intCast(mouse.row), 319 + .col = @intCast(mouse.col), 320 + }; 321 + if (sub.containsPoint(mouse_point)) { 322 + try last_frame.hitTest(&hits, mouse_point); 323 + } 324 + 325 + // We store the hit list from the last mouse event to determine mouse_enter and mouse_leave 326 + // events. If list a is the previous hit list, and list b is the current hit list: 327 + // - Widgets in a but not in b get a mouse_leave event 328 + // - Widgets in b but not in a get a mouse_enter event 329 + // - Widgets in both receive nothing 330 + const a = self.last_hit_list; 331 + const b = hits.items; 332 + 333 + // Find widgets in a but not b 334 + for (a) |a_item| { 335 + const a_widget = a_item.widget; 336 + for (b) |b_item| { 337 + const b_widget = b_item.widget; 338 + if (a_widget.eql(b_widget)) break; 339 + } else { 340 + // a_item is not in b 341 + try a_widget.handleEvent(ctx, .mouse_leave); 342 + try app.handleCommand(&ctx.cmds); 343 + } 344 + } 345 + 346 + // Widgets in b but not in a 347 + for (b) |b_item| { 348 + const b_widget = b_item.widget; 349 + for (a) |a_item| { 350 + const a_widget = a_item.widget; 351 + if (b_widget.eql(a_widget)) break; 352 + } else { 353 + // b_item is not in a. 354 + try b_widget.handleEvent(ctx, .mouse_enter); 355 + try app.handleCommand(&ctx.cmds); 356 + } 357 + } 358 + 359 + // Store a copy of this hit list for next frame 360 + app.allocator.free(self.last_hit_list); 361 + self.last_hit_list = try app.allocator.dupe(vxfw.HitResult, hits.items); 362 + } 363 + 259 364 fn handleMouse(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext, mouse: vaxis.Mouse) anyerror!void { 260 365 // For mouse events we store the last frame and use that for hit testing 261 366 const last_frame = self.last_frame; 367 + self.mouse = mouse; 262 368 263 369 var hits = std.ArrayList(vxfw.HitResult).init(app.allocator); 264 370 defer hits.deinit(); ··· 502 608 503 609 /// Update the focus list 504 610 fn update(self: *FocusHandler, root: vxfw.Surface) Allocator.Error!void { 505 - _ = self.arena.reset(.retain_capacity); 611 + _ = self.arena.reset(.free_all); 506 612 507 613 var list = std.ArrayList(*Node).init(self.arena.allocator()); 508 614 for (root.children) |child| {