╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │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 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1833 try out.appendSlice(allocator, "<form action=\""); │ │1834 try appendEscapedHtml(allocator, &out, action_url); │ │1835 try out.appendSlice(allocator, "\" method=\"post\""); │ │1809 - // an empty action must not POST — block it and send the input to the TUI instead │ │1810 - if (action_url.len == 0) try out.appendSlice(allocator, " onsubmit=\"event.preventDefault();sendEnter(this);\"");│ │1836 try out.appendSlice(allocator, ">"); │ │1837 │ │1838 // form.focus.children was populated by addChild, which flattens the │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │1847 switch (root_child.focus.kind) { │ │1848 .text_input, .text_input_password => { │ │1849 const ti = session.text_inputs.get(inner_id) orelse continue; │ │1850 + if (ti.options.read_only) continue; // rendered with the grid │ │1851 const inner_left = r.x + 1; │ │1852 const inner_top = r.y + 1; │ │1853 const inner_width = if (r.size.width > 2) r.size.width - 2 else 0;│ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1854 │ │1855 try out.appendSlice(allocator, "<input type=\""); │ │1856 try out.appendSlice(allocator, if (root_child.focus.kind == .text_input_password) "password" else "text"); │ │1831 - try out.appendSlice(allocator, if (action_url.len == 0) "\" readonly data-focus-id=\"" else "\" data-focus-id=\"");│ │1857 + try out.appendSlice(allocator, "\" data-focus-id=\""); │ │1858 var id_buf: [32]u8 = undefined; │ │1859 try out.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "{d}", .{inner_id})); │ │1860 if (ti.options.name.len > 0) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯