╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Undo.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
301 errdefer row.deinit(allocator); 302 row.getFocus().mode = .all; 303 if (link) |value| row.getFocus().kind = .{ .custom = value }; 304 try box.children.put(allocator, row.getFocus().id, .{ .widget = .{ .text_box = row }, .rect = null, .min_size = null }); 305 } 306 307 fn headerBox(self: *View) *wgt.Box(ui.Widget) { 308 return &self.box.children.values()[header_index].widget.box; 309 } 310 fn headerActive(self: *View) bool { 311 return self.box.getFocus().child_id == self.headerBox().getFocus().id; 312 } 313 fn contentBox(self: *View) *wgt.Box(ui.Widget) { 314 return &self.box.children.values()[content_index].widget.box; 315 } 316 fn listScroll(self: *View) *wgt.Scroll(ui.Widget) { 317 return &self.contentBox().children.values()[0].widget.scroll; 318 } 319 fn detailScroll(self: *View) *wgt.Scroll(ui.Widget) { 320 return &self.contentBox().children.values()[1].widget.scroll; 321 } 322 fn selected(self: *View) ?usize { 323 const rows = &self.listScroll().child.box; 324 const index = rows.children.getIndex(rows.getFocus().child_id orelse return null) orelse return null; 325 return if (index < self.data.items.len) index else null; 326 } 327 fn detailActive(self: *View) bool { 328 return self.contentBox().getFocus().child_id == self.detailScroll().getFocus().id; 329 } 330 331 fn refreshDetail(self: *View, allocator: std.mem.Allocator) !void { 332 const index = self.selected() orelse return; 333 const failure = self.session.data.undo_failure orelse self.data.failure; 334 if (self.detailed_index == index and std.meta.eql(self.shown_failure, failure)) return; 335 const details = &self.detailScroll().child.box; 336 if (self.failure_id) |id| { 337 if (details.children.getPtr(id)) |child| child.widget.deinit(allocator); 338 _ = details.children.orderedRemove(id); 339 self.failure_id = null; 340 } 341 const item = self.data.items[index]; 342 const button = &details.children.values()[0].widget.text_box; 343 const description = &details.children.values()[2].widget.text_box; 344 try button.setContent(allocator, if (item.index == 0) "initial state cannot be undone" else if (item.index == self.data.count - 1) "undo this" else "undo this and all above it"); 345 // a transaction that wrote events links to them instead of describing itself 346 if (item.events) |events| { 347 try description.setContent(allocator, events.label); 348 description.options.label = " events "; 349 description.getFocus().kind = .{ .custom = events.link }; 350 } else { 351 try description.setContent(allocator, item.description); 352 description.options.label = " description "; 353 description.getFocus().kind = .text_box; 354 } 355 const enabled = self.data.can_undo and item.index > 0; 356 button.getFocus().kind = if (enabled) .{ .custom = "submit" } else .text_box; 357 try details.children.values()[1].widget.text_box.setContent(allocator, item.timestamp); 358 if (failure) |message| { 359 try addText(allocator, details, try std.fmt.allocPrint(self.session.page_arena.allocator(), "error: {s}", .{message}), null, .single); 360 self.failure_id = details.children.keys()[details.children.count() - 1]; 361 } 362 self.shown_failure = failure; 363 details.getFocus().kind = if (enabled) .{ .custom = item.form } else .container; 364 details.getFocus().child_id = details.children.keys()[0]; 365 self.detailScroll().x = 0; 366 self.detailScroll().y = 0; 367 self.detailScroll().getFocus().version +%= 1; 368 self.detailed_index = index; 369 } 370 371 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 372 self.clearGrid(); 373 if (self.data.clear) return try self.box.build(allocator, constraint, root_focus); 374 try self.refreshDetail(allocator); 375 const rows = &self.listScroll().child.box; 376 if (self.data.items.len == 0) { 377 const failure = self.session.data.undo_failure orelse self.data.failure; 378 if (!std.meta.eql(self.shown_failure, failure)) { 379 const message = if (failure) |name| try std.fmt.allocPrint(self.session.page_arena.allocator(), "error: {s}", .{name}) else "no undo history"; 380 try rows.children.values()[0].widget.text_box.setContent(allocator, message); 381 self.shown_failure = failure; 382 } 383 } 384 for (rows.children.keys(), rows.children.values()) |id, *child| child.widget.text_box.options.border_style = if (rows.getFocus().child_id == id) .single else .hidden; 385 const both_fit = if (constraint.max_size.width) |width| width >= list_max_width + detail_min_width else true; 386 self.contentBox().children.values()[0].max_size = if (both_fit) .{ .width = list_max_width, .height = null } else null; 387 const width = if (constraint.max_size.width) |value| if (both_fit) value - list_max_width else value else detail_min_width; 388 self.contentBox().children.values()[1].min_size = .{ .width = width, .height = null }; 389 for (self.detailScroll().child.box.children.values()) |*child| child.max_size = .{ .width = width -| 2, .height = null }; 390 try self.box.build(allocator, constraint, root_focus); 391 } 392 393 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 394 // the sub header sits above whichever content the route selects. 395 // scrolling crosses the boundary the same way the arrow keys do 396 const direction = inp.vertDirection(key); 397 if (self.headerActive()) { 398 if (direction == .down) root_focus.setFocus(self.box.children.values()[content_index].widget.getFocus().id); 399 return; 400 } 401 if (self.data.clear) { 402 if (direction == .up) root_focus.setFocus(self.headerBox().getFocus().id) else try self.clearInput(key, root_focus); 403 return; 404 } 405 if (direction == .up and self.contentAtTop()) { 406 root_focus.setFocus(self.headerBox().getFocus().id); 407 return; 408 } 409 try self.refreshDetail(allocator); 410 if (self.detailActive()) { 411 const details = &self.detailScroll().child.box; 412 if (self.selected()) |index| { 413 const button_id = details.children.keys()[0]; 414 const activated = switch (key) { 415 .enter => root_focus.grandchild_id == button_id, 416 .mouse => |mouse| inp.leftClickOn(root_focus, button_id, mouse), 417 else => false, 418 }; 419 if (activated and self.data.can_undo and self.data.items[index].index > 0) { 420 if (builtin.target.cpu.arch != .wasm32 and self.session.is_terminal and self.session.host_request == null) { 421 const route = ui.RoutablePage.repoUndoRoute(self.data.identity, self.data.items[index].index) orelse return error.RouteTooLong; 422 self.session.host_request = .{ .undo = route.repo_undo }; 423 } 424 return; 425 } 426 } 427 if (key == .arrow_left) root_focus.setFocus(self.listScroll().getFocus().id) else if (inp.rowDelta(key, @intCast(details.children.count()))) |delta| ui.widget.moveRowFocus(details, self.detailScroll(), root_focus, delta); 428 } else if (inp.rowDelta(key, @intCast(self.listScroll().child.box.children.count()))) |delta| { 429 ui.widget.moveRowFocus(&self.listScroll().child.box, self.listScroll(), root_focus, delta); 430 } else switch (key) { 431 .arrow_right, .enter => if (self.selected() != null) { 432 root_focus.setFocus(self.detailScroll().getFocus().id); 433 }, 434 else => {}, 435 } 436 } 437 438 // the confirm button submits the form the web renderer posts 439 fn clearInput(self: *View, key: Key, root_focus: *Focus) !void { 440 const center = &self.box.children.values()[content_index].widget.center; 441 const button_id = center.child.box.getFocus().child_id orelse return; 442 const activated = switch (key) { 443 .enter => root_focus.grandchild_id == button_id, 444 .mouse => |mouse| inp.leftClickOn(root_focus, button_id, mouse), 445 else => false, 446 }; 447 if (!activated or !self.data.can_undo) return; 448 if (builtin.target.cpu.arch != .wasm32 and self.session.is_terminal and self.session.host_request == null) { 449 const route = ui.RoutablePage.repoUndoClearRoute(self.data.identity) orelse return error.RouteTooLong; 450 self.session.host_request = .{ .undo = route.repo_undo }; 451 } 452 } 453 454 fn contentAtTop(self: *View) bool { 455 const scroll = if (self.detailActive()) self.detailScroll() else self.listScroll(); 456 const rows = &scroll.child.box; 457 const id = rows.getFocus().child_id orelse return true; 458 return rows.children.getIndex(id) == 0 and scroll.y == 0; 459 } 460 461 // moving up from the sub header returns to the page's tabs 462 pub fn atTop(self: *View) bool { 463 return self.headerActive(); 464 } 465 466 // arriving from the page's tabs lands on the sub header, not the content 467 pub fn focusHeader(self: *View, root_focus: *Focus) bool { 468 root_focus.setFocus(self.headerBox().getFocus().id); 469 return true; 470 } 471 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 472 self.box.deinit(allocator); 473 } 474 pub fn clearGrid(self: *View) void { 475 self.box.clearGrid(); 476 } 477 pub fn getGrid(self: View) ?Grid { 478 return self.box.getGrid(); 479 } 480 pub fn getFocus(self: *View) *Focus { 481 return self.box.getFocus(); 482 } 483 }; 484