╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Undo.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
375 } 376 fn listScroll(self: *View) *wgt.Scroll(ui.Widget) { 377 return &self.contentBox().children.values()[0].widget.scroll; 378 } 379 fn detailScroll(self: *View) *wgt.Scroll(ui.Widget) { 380 return &self.contentBox().children.values()[1].widget.scroll; 381 } 382 fn selected(self: *View) ?usize { 383 const rows = &self.listScroll().child.box; 384 const index = rows.children.getIndex(rows.getFocus().child_id orelse return null) orelse return null; 385 return if (index < self.data.items.len) index else null; 386 } 387 fn detailActive(self: *View) bool { 388 return self.contentBox().getFocus().child_id == self.detailScroll().getFocus().id; 389 } 390 391 fn refreshDetail(self: *View, allocator: std.mem.Allocator) !void { 392 const index = self.selected() orelse return; 393 const failure = self.session.data.undo_failure orelse self.data.failure; 394 if (self.detailed_index == index and std.meta.eql(self.shown_failure, failure)) return; 395 const details = &self.detailScroll().child.box; 396 while (details.children.count() > fixed_rows) { 397 var entry = details.children.pop() orelse break; 398 entry.value.widget.deinit(allocator); 399 } 400 const item = self.data.items[index]; 401 const enabled = self.data.can_undo and item.index > 0; 402 403 // the fixed rows are filled first, since adding the ones below 404 // invalidates every pointer into the box 405 { 406 const button = &details.children.values()[0].widget.text_box; 407 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"); 408 button.getFocus().kind = if (enabled) .{ .custom = "submit" } else .text_box; 409 } 410 try details.children.values()[1].widget.text_box.setContent(allocator, item.timestamp); 411 412 // some actions put buttons where the description would go 413 if (item.buttons.len == 0) { 414 try addLabeled(allocator, details, " description ", item.description, null); 415 } else for (item.buttons) |detail_button| { 416 try addLabeled(allocator, details, detail_button.label, detail_button.text, detail_button.link); 417 } 418 if (failure) |message| try addText(allocator, details, try std.fmt.allocPrint(self.session.page_arena.allocator(), "error: {s}", .{message}), null, .single); 419 self.shown_failure = failure; 420 details.getFocus().kind = if (enabled) .{ .custom = item.form } else .container; 421 details.getFocus().child_id = details.children.keys()[0]; 422 self.detailScroll().x = 0; 423 self.detailScroll().y = 0; 424 self.detailScroll().getFocus().version +%= 1; 425 self.detailed_index = index; 426 } 427 428 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 429 self.clearGrid(); 430 if (self.data.clear) return try self.box.build(allocator, constraint, root_focus); 431 try self.refreshDetail(allocator); 432 const rows = &self.listScroll().child.box; 433 if (self.data.items.len == 0) { 434 const failure = self.session.data.undo_failure orelse self.data.failure; 435 if (!std.meta.eql(self.shown_failure, failure)) { 436 const message = if (failure) |name| try std.fmt.allocPrint(self.session.page_arena.allocator(), "error: {s}", .{name}) else "no undo history"; 437 try rows.children.values()[0].widget.text_box.setContent(allocator, message); 438 self.shown_failure = failure; 439 } 440 } 441 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; 442 const both_fit = if (constraint.max_size.width) |width| width >= list_max_width + detail_min_width else true; 443 self.contentBox().children.values()[0].max_size = if (both_fit) .{ .width = list_max_width, .height = null } else null; 444 const width = if (constraint.max_size.width) |value| if (both_fit) value - list_max_width else value else detail_min_width; 445 self.contentBox().children.values()[1].min_size = .{ .width = width, .height = null }; 446 for (self.detailScroll().child.box.children.values()) |*child| child.max_size = .{ .width = width -| 2, .height = null }; 447 try self.box.build(allocator, constraint, root_focus); 448 } 449 450 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 451 // the sub header sits above whichever content the route selects. 452 // scrolling crosses the boundary the same way the arrow keys do 453 const direction = inp.vertDirection(key); 454 if (self.headerActive()) { 455 if (direction == .down) root_focus.setFocus(self.box.children.values()[content_index].widget.getFocus().id); 456 return; 457 } 458 if (self.data.clear) { 459 if (direction == .up) root_focus.setFocus(self.headerBox().getFocus().id) else try self.clearInput(key, root_focus); 460 return; 461 } 462 if (direction == .up and self.contentAtTop()) { 463 root_focus.setFocus(self.headerBox().getFocus().id); 464 return; 465 } 466 try self.refreshDetail(allocator); 467 if (self.detailActive()) { 468 const details = &self.detailScroll().child.box; 469 if (self.selected()) |index| { 470 if (activated(root_focus, details.children.keys()[0], key) and self.data.can_undo and self.data.items[index].index > 0) { 471 self.requestUndo(ui.RoutablePage.repoUndoRoute(self.data.identity, self.data.items[index].index) orelse return error.RouteTooLong); 472 return; 473 } 474 } 475 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); 476 } else if (inp.rowDelta(key, @intCast(self.listScroll().child.box.children.count()))) |delta| { 477 ui.widget.moveRowFocus(&self.listScroll().child.box, self.listScroll(), root_focus, delta); 478 } else switch (key) { 479 .arrow_right, .enter => if (self.selected() != null) { 480 root_focus.setFocus(self.detailScroll().getFocus().id); 481 }, 482 else => {}, 483 } 484 } 485 486 // the confirm button submits the form the web renderer posts 487 fn clearInput(self: *View, key: Key, root_focus: *Focus) !void { 488 const center = &self.box.children.values()[content_index].widget.center; 489 const button_id = center.child.box.getFocus().child_id orelse return; 490 if (!activated(root_focus, button_id, key) or !self.data.can_undo) return; 491 self.requestUndo(ui.RoutablePage.repoUndoClearRoute(self.data.identity) orelse return error.RouteTooLong); 492 } 493 494 fn contentAtTop(self: *View) bool { 495 const scroll = if (self.detailActive()) self.detailScroll() else self.listScroll(); 496 const rows = &scroll.child.box; 497 const id = rows.getFocus().child_id orelse return true; 498 return rows.children.getIndex(id) == 0 and scroll.y == 0; 499 } 500 501 // moving up from the sub header returns to the page's tabs 502 pub fn atTop(self: *View) bool { 503 return self.headerActive(); 504 } 505 506 // arriving from the page's tabs lands on the sub header, not the content 507 pub fn focusHeader(self: *View, root_focus: *Focus) bool { 508 root_focus.setFocus(self.headerBox().getFocus().id); 509 return true; 510 } 511 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 512 self.box.deinit(allocator); 513 } 514 pub fn clearGrid(self: *View) void { 515 self.box.clearGrid(); 516 } 517 pub fn getGrid(self: View) ?Grid { 518 return self.box.getGrid(); 519 } 520 pub fn getFocus(self: *View) *Focus { 521 return self.box.getFocus(); 522 } 523 }; 524