╭ changed ────╮ │build.zig.zon│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 3 .version = "0.0.0", │ │ 4 .dependencies = .{ │ │ 5 .xit = .{ │ │ 6 - .url = "git+https://github.com/xit-vcs/xit#abda08bcf18b09ae5fcd3655192ef78149b3b982",│ │ 7 - .hash = "xit-0.0.0-Ub3nDAjvTwGcTd2CEMMxQOt3EqEvup-W3GSeqiv0Y4x3", │ │ 6 + .url = "git+https://github.com/xit-vcs/xit#2c4a80ebdf42cdb655b946d6f2e9d5a87102d7b8",│ │ 7 + .hash = "xit-0.0.0-Ub3nDAjvTwEHoGrwkjP0hIEKUlvz417H-z6MFK9Su7o0", │ │ 8 }, │ │ 9 }, │ │10 .paths = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │361 var diff_scroll = blk2: { │ │362 var diff_inner = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │363 errdefer diff_inner.deinit(allocator); │ │364 - break :blk2 try wgt.Scroll(ui.Widget).init(allocator, .{ .box = diff_inner }, .{ .direction = .both, .web_native = !session.is_terminal }); │ │364 + // fill the pane (content top-left, scroll bars pinned to the │ │365 + // edges) rather than shrinking to the diff content. │ │366 + break :blk2 try wgt.Scroll(ui.Widget).init(allocator, .{ .box = diff_inner }, .{ .direction = .both, .web_native = !session.is_terminal, .fill = true });│ │367 }; │ │368 errdefer diff_scroll.deinit(allocator); │ │369 var frame = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = .hidden, .direction = .vert }); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │538 const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + diff_min_width else true; │ │539 self.contentBox().children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null; │ │540 │ │541 + // stretch the diff pane across the rest of the width so it fills the area │ │542 + // rather than shrinking to its content; its scroll fills the pane. when │ │543 + // too narrow for both, it fills the whole width. │ │544 + if (constraint.max_size.width) |w| { │ │545 + self.contentBox().children.values()[diff_index].min_size = .{ .width = if (both_panes_fit) w - list_max_width else w, .height = null };│ │546 + } else { │ │547 + self.contentBox().children.values()[diff_index].min_size = .{ .width = diff_min_width, .height = null }; │ │548 + } │ │549 + │ │550 // the web bounds the layout to the browser viewport like the terminal; │ │551 // each Scroll's web-native mode hands its full content to a real │ │552 // scrollable element, so we no longer build unbounded here. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │319 // populateDetail. │ │320 { │ │321 var detail_outer = blk: { │ │322 - var frame = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = .hidden, .direction = .vert });│ │322 + var frame = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │323 errdefer frame.deinit(allocator); │ │324 │ │325 { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │328 try frame.children.put(allocator, nav_box.getFocus().id, .{ .widget = .{ .box = nav_box }, .rect = null, .min_size = null }); │ │329 } │ │330 { │ │331 - var detail_scroll = blk2: { │ │332 - var detail_inner = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │333 - errdefer detail_inner.deinit(allocator); │ │334 - break :blk2 try wgt.Scroll(ui.Widget).init(allocator, .{ .box = detail_inner }, .{ .direction = .both, .web_native = !session.is_terminal }); │ │331 + // the content scroll wrapped in a bordered box; build sets the │ │332 + // border single normally and double when the content is focused │ │333 + // (the content box itself is borderless). │ │334 + var scroll_frame = blk2: { │ │335 + var detail_scroll = blk3: { │ │336 + var detail_inner = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │337 + errdefer detail_inner.deinit(allocator); │ │338 + break :blk3 try wgt.Scroll(ui.Widget).init(allocator, .{ .box = detail_inner }, .{ .direction = .both, .web_native = !session.is_terminal, .fill = true });│ │339 + }; │ │340 + errdefer detail_scroll.deinit(allocator); │ │341 + var sf = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = .single, .rounded_corners = true, .direction = .vert }); │ │342 + errdefer sf.deinit(allocator); │ │343 + // the wrapper's selected child is its scroll, so the focus │ │344 + // chain reaches the content box (populateDetail points the │ │345 + // scroll's inner box at it). │ │346 + sf.getFocus().child_id = detail_scroll.getFocus().id; │ │347 + try sf.children.put(allocator, detail_scroll.getFocus().id, .{ .widget = .{ .scroll = detail_scroll }, .rect = null, .min_size = null }); │ │348 + break :blk2 sf; │ │349 }; │ │336 - errdefer detail_scroll.deinit(allocator); │ │350 + errdefer scroll_frame.deinit(allocator); │ │351 // entering the detail pane lands on the content scroll (focus │ │338 - // recovery descends the frame's selected child); populateDetail │ │339 - // points the scroll's inner box at the content. the nav links │ │352 + // recovery descends the frame's selected child). the nav links │ │353 // above are reached by scrolling to the top, then up. │ │341 - frame.getFocus().child_id = detail_scroll.getFocus().id; │ │342 - try frame.children.put(allocator, detail_scroll.getFocus().id, .{ .widget = .{ .scroll = detail_scroll }, .rect = null, .min_size = null }); │ │354 + frame.getFocus().child_id = scroll_frame.getFocus().id; │ │355 + try frame.children.put(allocator, scroll_frame.getFocus().id, .{ .widget = .{ .box = scroll_frame }, .rect = null, .min_size = null }); │ │356 } │ │357 break :blk frame; │ │358 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │377 } │ │378 │ │379 // the selected file's contents as a single focusable multi-line text box. │ │367 - // its hidden border reserves the space the double border occupies when │ │368 - // focused, so focusing doesn't shift layout. `text` lives in the page arena. │ │380 + // it's borderless — the border lives on the surrounding scroll frame, which │ │381 + // doubles when this content is focused. `text` lives in the page arena. │ │382 fn addContentBox(self: *View, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), text: []const u8) !void { │ │383 _ = self; │ │371 - var tb = try wgt.TextBox(ui.Widget).init(allocator, text, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .none });│ │384 + var tb = try wgt.TextBox(ui.Widget).init(allocator, text, .{ .border_style = null, .wrap_kind = .none }); │ │385 errdefer tb.deinit(allocator); │ │386 tb.getFocus().focusable = true; │ │387 try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │411 return &self.detailOuter().children.values()[detail_nav_index].widget.box; │ │412 } │ │413 │ │414 + // the bordered box wrapping the content scroll (border toggles with focus). │ │415 + fn detailScrollFrame(self: *View) *wgt.Box(ui.Widget) { │ │416 + return &self.detailOuter().children.values()[detail_scroll_index].widget.box; │ │417 + } │ │418 + │ │419 fn detailScroll(self: *View) *wgt.Scroll(ui.Widget) { │ │402 - return &self.detailOuter().children.values()[detail_scroll_index].widget.scroll;│ │420 + return &self.detailScrollFrame().children.values()[0].widget.scroll; │ │421 } │ │422 │ │423 fn detailInner(self: *View) *wgt.Box(ui.Widget) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │490 } │ │491 } │ │492 │ │475 - // the focused content box shows a single border (the focused TextBox │ │476 - // upgrades it to a double border itself, so it stays single when focus is │ │477 - // on the list); the rest keep their space-reserving hidden border. │ │478 - const inner = self.detailInner(); │ │479 - for (inner.children.keys(), inner.children.values()) |id, *child| { │ │480 - switch (child.widget) { │ │481 - .text_box => |*tb| tb.options.border_style = if (inner.getFocus().child_id == id) .single else .hidden, │ │482 - else => {}, │ │483 - } │ │484 - } │ │493 + // the content scroll's border is single normally and double when the │ │494 + // content is focused (the content box itself is borderless). │ │495 + const has_content = self.detailInner().children.count() > 0; │ │496 + const content_focused = if (root_focus.grandchild_id) |g| self.detailInner().children.contains(g) else false; │ │497 + self.detailScrollFrame().options.border_style = if (!has_content) null else if (content_focused) .double else .single;│ │498 │ │499 // same for the "next" link above the content. │ │500 const nav = self.navBox(); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │512 const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true; │ │513 self.contentBox().children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null;│ │514 │ │515 + // stretch the detail pane and its bordered scroll frame across the rest │ │516 + // of the width so the pane fills the area rather than shrinking to its │ │517 + // content, like the refs view's columns. the content scroll inside keeps │ │518 + // its natural width: forcing it to the full inner width would make it one │ │519 + // column too wide whenever the vertical scrollbar appears, leaving a │ │520 + // phantom horizontal scroll. (height fills via the scroll clipping to the │ │521 + // viewport.) │ │522 + if (constraint.max_size.width) |w| { │ │523 + const detail_w = if (both_panes_fit) w - list_max_width else w; │ │524 + self.contentBox().children.values()[detail_index].min_size = .{ .width = detail_w, .height = null }; │ │525 + self.detailOuter().children.values()[detail_scroll_index].min_size = .{ .width = detail_w, .height = null }; │ │526 + } else { │ │527 + self.contentBox().children.values()[detail_index].min_size = .{ .width = detail_min_width, .height = null }; │ │528 + self.detailOuter().children.values()[detail_scroll_index].min_size = null; │ │529 + } │ │530 + │ │531 // crossing panes only re-selects the content box's child (focusList / │ │532 // focusDetail); when the window is too narrow to show both, the pane that │ │533 // was holding focus is dropped here. the framework recovers focus after │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │753 if (self.navBox().children.count() > 0) { │ │754 frame.getFocus().child_id = self.navBox().getFocus().id; │ │755 } else if (self.detailInner().children.count() > 0) { │ │727 - frame.getFocus().child_id = self.detailScroll().getFocus().id; │ │756 + frame.getFocus().child_id = self.detailScrollFrame().getFocus().id;│ │757 } else return; │ │758 root_focus.setFocus(frame.getFocus().id); │ │759 } │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯