╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │2042 } else Widget{ .background = try AnsiBackground.init(allocator, page_widget, session) };│ │2043 errdefer root.deinit(allocator); │ │2044 │ │2045 - // input-owning views build their TextInputs in init — so reset the │ │2046 - // focus-id -> *TextInput map here │ │2045 + // input-owning views build their TextInputs in init, so reset their │ │2046 + // registrations here │ │2047 session.text_inputs.clearRetainingCapacity(); │ │2048 session.input_values.clearRetainingCapacity(); │ │2049 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/ui/CloneUrl.zig│ ╰───────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │81 │ │82 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │83 errdefer box.deinit(allocator); │ │84 - box.getFocus().kind = .{ .custom = "form:" }; │ │84 │ │85 for (protocols[0..protocol_count]) |protocol| { │ │86 var label = try wgt.TextBox(ui.Widget).init(allocator, protocol.name, .{ .border_style = .hidden, .wrap_kind = .none });│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │92 var text_input = try wgt.TextInput(ui.Widget).init(allocator, .{│ │93 .border_style = .single, │ │94 .label = input_label, │ │95 + .read_only = true, │ │96 .render_content = session.is_terminal, │ │97 .visible_width = max_text_len, │ │98 }); │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │1494 │ │1495 const snapshot: ui.Snapshot = .{ .page = page, .session = session.data };│ │1496 │ │1497 - const content = try generateHtml(allocator, &root); │ │1497 + const content = try generateHtml(allocator, &root, &session); │ │1498 defer allocator.free(content); │ │1499 const overlay = try generateOverlay(allocator, &root, &session); │ │1500 defer allocator.free(overlay); │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1553 // emits the TUI grid cells as static HTML. each web-native Scroll becomes its │ │1554 // own absolutely-positioned, natively-scrollable <div> holding its full content, │ │1555 // rendered recursively so nested scrolls work. │ │1556 -pub fn generateHtml(allocator: std.mem.Allocator, root: *ui.Widget) ![]const u8 { │ │1556 +pub fn generateHtml(allocator: std.mem.Allocator, root: *ui.Widget, session: *ui.Session) ![]const u8 {│ │1557 const grid = root.getGrid() orelse return error.MissingGrid; │ │1558 │ │1559 var out: std.ArrayList(u8) = .empty; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1564 if (background.art.getGrid()) |art| { │ │1565 var position_buf: [64]u8 = undefined; │ │1566 try out.appendSlice(allocator, try std.fmt.bufPrint(&position_buf, "<div class=\"ansi-art\" aria-hidden=\"true\" style=\"left:{d}ch\">", .{grid.size.width -| art.size.width}));│ │1567 - try renderPanel(allocator, &out, background.art.getFocus(), art); │ │1567 + try renderPanel(allocator, &out, background.art.getFocus(), art, session); │ │1568 try out.appendSlice(allocator, "</div>"); │ │1569 } │ │1570 }, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │1571 else => {}, │ │1572 } │ │1573 try out.appendSlice(allocator, "<div class=\"grid-content\">"); │ │1574 - try renderPanel(allocator, &out, root.getFocus(), grid); │ │1574 + try renderPanel(allocator, &out, root.getFocus(), grid, session);│ │1575 try out.appendSlice(allocator, "</div>"); │ │1576 return try out.toOwnedSlice(allocator); │ │1577 } │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1581 // Scroll becomes a nested, natively-scrollable div holding its full content. │ │1582 // emits the panel's grid cells as text, leaving holes where child scrolls sit, │ │1583 // then the child scroll divs over those holes. │ │1584 -fn renderPanel(allocator: std.mem.Allocator, output: *std.ArrayList(u8), focus: *Focus, grid: Grid) !void { │ │1584 +fn renderPanel(allocator: std.mem.Allocator, output: *std.ArrayList(u8), focus: *Focus, grid: Grid, session: *ui.Session) !void {│ │1585 // `direct` is this panel's scroll children, each drawn as an overlaid div. │ │1586 // `excluded` is every focus id that belongs to a scroll (the scroll nodes │ │1587 // plus their descendants, which the focus tree flattens into this panel's │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1752 try output.append(allocator, '\n'); │ │1753 } │ │1754 │ │1755 + // read-only inputs have no live form state to preserve, so emit them in │ │1756 + // their panel and let a native scroll move and clip them with the grid. │ │1757 + var input_iter = focus.children.iterator(); │ │1758 + while (input_iter.next()) |input_entry| { │ │1759 + const input_id = input_entry.key_ptr.*; │ │1760 + if (excluded.contains(input_id)) continue; │ │1761 + const child = input_entry.value_ptr.*; │ │1762 + switch (child.focus.kind) { │ │1763 + .text_input => {}, │ │1764 + else => continue, │ │1765 + } │ │1766 + const text_input = session.text_inputs.get(input_id) orelse continue; │ │1767 + if (!text_input.options.read_only) continue; │ │1768 + const value = session.input_values.get(input_id) orelse continue; │ │1769 + const r = child.rect; │ │1770 + const inner_width = if (r.size.width > 2) r.size.width - 2 else 0; │ │1771 + │ │1772 + try output.appendSlice(allocator, "<input type=\"text\" readonly data-focus-id=\""); │ │1773 + var id_buf: [32]u8 = undefined; │ │1774 + try output.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "{d}", .{input_id})); │ │1775 + try output.appendSlice(allocator, "\" value=\""); │ │1776 + try appendEscapedHtml(allocator, output, value); │ │1777 + try output.appendSlice(allocator, "\" style=\"left:"); │ │1778 + var pos_buf: [64]u8 = undefined; │ │1779 + try output.appendSlice(allocator, try std.fmt.bufPrint(&pos_buf, "{d}ch;top:{d}em;width:{d}ch;height:1em\">", .{ r.x + 1, r.y + 1, inner_width }));│ │1780 + } │ │1781 + │ │1782 // each child scroll becomes a natively-scrollable div positioned over its │ │1783 // viewport, holding its full content rendered recursively. │ │1784 for (direct.items) |id| { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1801 // scrolling to its selected row on page load) reach the browser. │ │1802 var buf: [256]u8 = undefined; │ │1803 try output.appendSlice(allocator, try std.fmt.bufPrint(&buf, "<div class=\"scroll\" data-scroll-id=\"{d}-{d}\" data-scroll-direction=\"{s}\" data-scroll-x=\"{d}\" data-scroll-y=\"{d}\" style=\"left:{d}ch;top:{d}em;width:{d}ch;height:{d}em;{s}\">", .{ id, child.focus.version, @tagName(info.direction), info.offset_x, info.offset_y, r.x, r.y, r.size.width, r.size.height, overflow }));│ │1777 - try renderPanel(allocator, output, child.focus, info.content); │ │1804 + try renderPanel(allocator, output, child.focus, info.content, session); │ │1805 try output.appendSlice(allocator, "</div>"); │ │1806 } │ │1807 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯