├── README.md ├── .gitignore ├── font ├── times-new-roman.ttf └── schrift.zig ├── alext.zig ├── test ├── hello.html └── svg.html ├── revit.zig ├── Refcounted.zig ├── make-renderer-webpage.zig ├── lint.zig ├── htmlid.zig ├── Layout.md ├── render.zig ├── wasmrenderer.zig ├── html-css-renderer.template.html ├── testrunner.zig ├── imagerenderer.zig ├── dom.zig ├── x11renderer.zig ├── layout.zig └── HtmlTokenizer.zig /README.md: -------------------------------------------------------------------------------- 1 | # Html Css Renderer 2 | 3 | An HTML/CSS Renderer. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | /dep/ 4 | /htmlidmaps.zig 5 | -------------------------------------------------------------------------------- /font/times-new-roman.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marler8997/html-css-renderer/HEAD/font/times-new-roman.ttf -------------------------------------------------------------------------------- /alext.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const unmanaged = struct { 4 | pub fn finalize(comptime T: type, self: *std.ArrayListUnmanaged(T), allocator: std.mem.Allocator) void { 5 | const old_memory = self.allocatedSlice(); 6 | if (allocator.resize(old_memory, self.items.len)) { 7 | self.capacity = self.items.len; 8 | } 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /test/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 14 |
Hello 15 | -------------------------------------------------------------------------------- /revit.zig: -------------------------------------------------------------------------------- 1 | // PR to add this to std here: https://github.com/ziglang/zig/pull/13743 2 | fn ReverseIterator(comptime T: type) type { 3 | const info: struct { Child: type, Pointer: type } = blk: { 4 | switch (@typeInfo(T)) { 5 | .Pointer => |info| switch (info.size) { 6 | .Slice => break :blk .{ 7 | .Child = info.child, 8 | .Pointer = @Type(.{ .Pointer = .{ 9 | .size = .Many, 10 | .is_const = info.is_const, 11 | .is_volatile = info.is_volatile, 12 | .alignment = info.alignment, 13 | .address_space = info.address_space, 14 | .child = info.child, 15 | .is_allowzero = info.is_allowzero, 16 | .sentinel = info.sentinel, 17 | }}), 18 | }, 19 | else => {}, 20 | }, 21 | else => {}, 22 | } 23 | @compileError("reverse iterator expects slice, found " ++ @typeName(T)); 24 | }; 25 | return struct { 26 | ptr: info.Pointer, 27 | index: usize, 28 | pub fn next(self: *@This()) ?info.Child { 29 | if (self.index == 0) return null; 30 | self.index -= 1; 31 | return self.ptr[self.index]; 32 | } 33 | }; 34 | } 35 | pub fn reverseIterator(slice: anytype) ReverseIterator(@TypeOf(slice)) { 36 | return .{ .ptr = slice.ptr, .index = slice.len }; 37 | } 38 | -------------------------------------------------------------------------------- /Refcounted.zig: -------------------------------------------------------------------------------- 1 | const Refcounted = @This(); 2 | 3 | const std = @import("std"); 4 | const arc = std.log.scoped(.arc); 5 | 6 | const Metadata = struct { 7 | refcount: usize, 8 | }; 9 | const alloc_prefix_len = std.mem.alignForward(usize, @sizeOf(Metadata), @alignOf(Metadata)); 10 | 11 | data_ptr: [*]u8, 12 | pub fn alloc(allocator: std.mem.Allocator, len: usize) error{OutOfMemory}!Refcounted { 13 | const alloc_len = Refcounted.alloc_prefix_len + len; 14 | const full = try allocator.alignedAlloc(u8, @alignOf(Refcounted.Metadata), alloc_len); 15 | const buf = Refcounted{ .data_ptr = full.ptr + Refcounted.alloc_prefix_len }; 16 | buf.getMetadataRef().refcount = 1; 17 | arc.debug( 18 | "alloc {} (full={}) returning data_ptr 0x{x}", 19 | .{len, alloc_len, @intFromPtr(buf.data_ptr)}, 20 | ); 21 | return buf; 22 | } 23 | pub fn getMetadataRef(self: Refcounted) *Metadata { 24 | const addr = @intFromPtr(self.data_ptr); 25 | return @ptrFromInt(addr - alloc_prefix_len); 26 | } 27 | pub fn addRef(self: Refcounted) void { 28 | // TODO: what is AtomicOrder supposed to be? 29 | const old_count = @atomicRmw(usize, &self.getMetadataRef().refcount, .Add, 1, .seq_cst); 30 | arc.debug("addRef data_ptr=0x{x} new_count={}", .{@intFromPtr(self.data_ptr), old_count + 1}); 31 | } 32 | pub fn unref(self: Refcounted, allocator: std.mem.Allocator, len: usize) void { 33 | const base_addr = @intFromPtr(self.data_ptr) - alloc_prefix_len; 34 | // TODO: what is AtomicOrder supposed to be? 35 | const old_count = @atomicRmw(usize, &@as(*Metadata, @ptrFromInt(base_addr)).refcount, .Sub, 1, .seq_cst); 36 | std.debug.assert(old_count != 0); 37 | if (old_count == 1) { 38 | arc.debug("free full_len={} (data_ptr=0x{x})", .{alloc_prefix_len + len, @intFromPtr(self.data_ptr)}); 39 | allocator.free(@as([*]u8, @ptrFromInt(base_addr))[0 .. alloc_prefix_len + len]); 40 | } else { 41 | arc.debug("unref full_len={} (data_ptr=0x{x}) new_count={}", .{ 42 | alloc_prefix_len + len, 43 | @intFromPtr(self.data_ptr), 44 | old_count - 1, 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /make-renderer-webpage.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | fn oom(e: error{OutOfMemory}) noreturn { 4 | @panic(@errorName(e)); 5 | } 6 | fn fatal(comptime fmt: []const u8, args: anytype) noreturn { 7 | std.log.err(fmt, args); 8 | std.process.exit(0xff); 9 | } 10 | 11 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 12 | 13 | pub fn main() !u8 { 14 | const args = blk: { 15 | const all_args = try std.process.argsAlloc(arena.allocator()); 16 | var non_option_len: usize = 0; 17 | for (all_args[1..]) |arg| { 18 | if (!std.mem.startsWith(u8, arg, "-")) { 19 | all_args[non_option_len] = arg; 20 | non_option_len += 1; 21 | } else { 22 | fatal("unknown cmdline option '{s}'", .{arg}); 23 | } 24 | } 25 | break :blk all_args[0 .. non_option_len]; 26 | }; 27 | if (args.len != 3) { 28 | try std.io.getStdErr().writer().writeAll( 29 | "Usage: make-renderer-webpage WASM_FILE HTML_TEMPLATE OUT_FILE\n", 30 | ); 31 | return 0xff; 32 | } 33 | const wasm_filename = args[0]; 34 | const html_template_filename = args[1]; 35 | const out_filename = args[2]; 36 | const wasm_base64 = try getWasmBase64(wasm_filename); 37 | const html_template = try readFile(html_template_filename); 38 | const marker = "<@INSERT_WASM_HERE@>"; 39 | const wasm_marker = std.mem.indexOf(u8, html_template, marker) orelse { 40 | std.log.err("{s} is missing wasm marker '{s}'", .{html_template_filename, marker}); 41 | return 0xff; 42 | }; 43 | { 44 | if (std.fs.path.dirname(out_filename)) |out_dir| { 45 | try std.fs.cwd().makePath(out_dir); 46 | } 47 | var out_file = try std.fs.cwd().createFile(out_filename, .{}); 48 | defer out_file.close(); 49 | try out_file.writer().writeAll(html_template[0 .. wasm_marker]); 50 | try out_file.writer().writeAll(wasm_base64); 51 | try out_file.writer().writeAll(html_template[wasm_marker + marker.len..]); 52 | } 53 | return 0; 54 | } 55 | 56 | fn getWasmBase64(filename: []const u8) ![]u8 { 57 | const bin = try readFile(filename); 58 | defer arena.allocator().free(bin); 59 | 60 | const encoder = &std.base64.standard.Encoder; 61 | const b64 = try arena.allocator().alloc(u8, encoder.calcSize(bin.len)); 62 | const len = encoder.encode(b64, bin).len; 63 | std.debug.assert(len == b64.len); 64 | return b64; 65 | } 66 | 67 | 68 | fn readFile(filename: []const u8) ![]u8 { 69 | var file = try std.fs.cwd().openFile(filename, .{}); 70 | defer file.close(); 71 | return file.readToEndAlloc(arena.allocator(), std.math.maxInt(usize)); 72 | } 73 | -------------------------------------------------------------------------------- /lint.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const dom = @import("dom.zig"); 4 | const alext = @import("alext.zig"); 5 | 6 | pub fn oom(e: error{OutOfMemory}) noreturn { 7 | @panic(@errorName(e)); 8 | } 9 | 10 | pub fn fatal(comptime fmt: []const u8, args: anytype) noreturn { 11 | std.log.err(fmt, args); 12 | std.process.exit(0xff); 13 | } 14 | 15 | var windows_args_arena = if (builtin.os.tag == .windows) 16 | std.heap.ArenaAllocator.init(std.heap.page_allocator) else struct{}{}; 17 | 18 | pub fn cmdlineArgs() [][*:0]u8 { 19 | if (builtin.os.tag == .windows) { 20 | const slices = std.process.argsAlloc(windows_args_arena.allocator()) catch |err| switch (err) { 21 | error.OutOfMemory => oom(error.OutOfMemory), 22 | error.Overflow => @panic("Overflow while parsing command line"), 23 | }; 24 | const args = windows_args_arena.allocator().alloc([*:0]u8, slices.len - 1) catch |e| oom(e); 25 | for (slices[1..], 0..) |slice, i| { 26 | args[i] = slice.ptr; 27 | } 28 | return args; 29 | } 30 | return std.posix.argv.ptr[1 .. std.posix.argv.len]; 31 | } 32 | 33 | pub fn main() !u8 { 34 | const args = blk: { 35 | const all_args = cmdlineArgs(); 36 | var non_option_len: usize = 0; 37 | for (all_args) |arg_ptr| { 38 | const arg = std.mem.span(arg_ptr); 39 | if (!std.mem.startsWith(u8, arg, "-")) { 40 | all_args[non_option_len] = arg; 41 | non_option_len += 1; 42 | } else { 43 | fatal("unknown cmdline option '{s}'", .{arg}); 44 | } 45 | } 46 | break :blk all_args[0 .. non_option_len]; 47 | }; 48 | if (args.len != 1) { 49 | try std.io.getStdErr().writer().writeAll("Usage: lint FILE\n"); 50 | return 0xff; 51 | } 52 | const filename = std.mem.span(args[0]); 53 | 54 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 55 | defer arena.deinit(); 56 | 57 | const content = blk: { 58 | var file = std.fs.cwd().openFile(filename, .{}) catch |err| { 59 | std.log.err("failed to open '{s}' with {s}", .{filename, @errorName(err)}); 60 | return 0xff; 61 | }; 62 | defer file.close(); 63 | break :blk try file.readToEndAlloc(arena.allocator(), std.math.maxInt(usize)); 64 | }; 65 | 66 | var parse_context = ParseContext{ .filename = filename }; 67 | var nodes = dom.parse(arena.allocator(), content, .{ 68 | .context = &parse_context, 69 | .on_error = onParseError, 70 | }) catch |err| switch (err) { 71 | error.ReportedParseError => return 0xff, 72 | else => |e| return e, 73 | }; 74 | alext.unmanaged.finalize(dom.Node, &nodes, arena.allocator()); 75 | try dom.dump(content, nodes.items); 76 | return 0; 77 | } 78 | 79 | const ParseContext = struct { 80 | filename: []const u8, 81 | }; 82 | 83 | fn onParseError(context_ptr: ?*anyopaque, msg: []const u8) void { 84 | const context: *ParseContext = @alignCast(@ptrCast(context_ptr)); 85 | std.io.getStdErr().writer().print("{s}: parse error: {s}\n", .{context.filename, msg}) catch |err| 86 | std.debug.panic("failed to print parse error with {s}", .{@errorName(err)}); 87 | } 88 | -------------------------------------------------------------------------------- /htmlid.zig: -------------------------------------------------------------------------------- 1 | pub const TagId = enum { 2 | a, abbr, acronym, defines, address, applet, area, article, aside, audio, 3 | b, base, basefont, Specifies, bdi, bdo, big, blockquote, body, br, button, 4 | canvas, caption, center, cite, code, col, colgroup, command, 5 | data, datalist, dd, del, details, dfn, dialog, dir, div, dl, dt, em, embed, 6 | fieldset, figcaption, figure, font, footer, form, frame, frameset, 7 | h1, h2, h3, h4, h5, h6, head, header, hr, html, i, iframe, img, input, ins, kbd, keygen, 8 | label, legend, li, link, main, map, mark, meta, meter, nav, noframes, noscript, 9 | object, ol, optgroup, option, output, p, param, picture, pre, progress, q, 10 | rp, rt, ruby, s, samp, script, section, select, small, source, span, strike, 11 | strong, style, sub, summary, sup, svg, 12 | table, tbody, td, template, textarea, tfoot, th, thead, time, title, tr, track, tt, 13 | u, ul, @"var", video, wbr, 14 | 15 | // SVG IDs (maybe this should be its own type?) 16 | circle, defs, g, line, path, polygon, polyline, text, tspan, use, 17 | }; 18 | pub const AttrId = enum { 19 | accept, @"accept-charset", accesskey, action, @"align", alt, as, @"async", autocomplete, autofocus, autoplay, 20 | bgcolor, border, charset, checked, cite, class, color, cols, colspan, content, contenteditable, controls, coords, 21 | data, datetime, default, @"defer", dir, dirname, disabled, download, draggable, 22 | enctype, @"for", form, formaction, headers, height, hidden, high, href, hreflang, @"http-equiv", 23 | id, ismap, kind, label, lang, list, loop, low, max, maxlength, media, method, min, multiple, muted, 24 | name, nomodule, novalidate, onabort, onafterprint, onbeforeprint, onbeforeunload, onblur, oncanplay, 25 | oncanplaythrough, onchange, onclick, oncontextmenu, oncopy, oncuechange, oncut, ondblclick, 26 | ondrag, ondragend, ondragenter, ondragleave, ondragover, ondragstart, ondrop, ondurationchange, 27 | onemptied, onended, onerror, onfocus, onhashchange, oninput, oninvalid, onkeydown, onkeypress, 28 | onkeyup, onload, onloadeddata, onloadedmetadata, onloadstart, onmousedown, onmousemove, 29 | onmouseout, onmouseover, onmouseup, onmousewheel, onoffline, ononline, onpagehide, onpageshow, 30 | onpaste, onpause, onplay, onplaying, onpopstate, onprogress, onratechange, onreset, onresize, 31 | onscroll, onsearch, onseeked, onseeking, onselect, onstalled, onstorage, onsubmit, onsuspend, 32 | ontimeupdate, ontoggle, onunload, onvolumechange, onwaiting, onwheel, open, optimum, pattern, 33 | placeholder, poster, preload, readonly, rel, required, reversed, rows, rowspan, 34 | sandbox, scope, selected, shape, size, sizes, span, spellcheck, src, srcdoc, srclang, srcset, 35 | start, step, style, tabindex, target, title, translate, type, usemap, value, width, wrap, 36 | 37 | // SVG IDs (maybe this should be its own type?) 38 | @"alignment-baseline", @"baseline-shift", clip, @"clip-path", @"clip-rule", 39 | @"color-interpolation", @"color-interpolation-filters", @"color-profile", @"color-rendering", 40 | cursor, cx, cy, d, direction, display, @"dominant-baseline", @"enable-background", fill, @"fill-opacity", 41 | @"fill-rule", filter, @"flood-color", @"flood-opacity", @"font-family", @"font-size", @"font-size-adjust", 42 | @"font-stretch", @"font-style", @"font-variant", @"font-weight", @"glyph-orientation-horizontal", 43 | @"glyph-orientation-vertical", @"image-rendering", kerning, @"letter-spacing", @"lighting-color", 44 | @"marker-end", @"marker-mid", @"marker-start", mask, opacity, overflow, @"pointer-events", points, r, 45 | @"shape-rendering", space, @"solid-color", @"solid-opacity", @"stop-color", @"stop-opacity", stroke, 46 | @"stroke-dasharray", @"stroke-dashoffset", @"stroke-linecap", @"stroke-linejoin", @"stroke-miterlimit", 47 | @"stroke-opacity", @"stroke-width", @"text-anchor", @"text-decoration", @"text-rendering", transform, 48 | @"unicode-bidi", @"vector-effect", viewBox, visibility, @"word-spacing", @"writing-mode", 49 | x, x1, x2, xmlns, y, y1, y2, 50 | }; 51 | -------------------------------------------------------------------------------- /Layout.md: -------------------------------------------------------------------------------- 1 | # Layout 2 | 3 | My notes on HTML Layout. 4 | 5 | ## Differences between Horizontal and Vertical 6 | 7 | Who determines the size of things in an HTML/CSS layout? 8 | Here's my understanding of the defaults so far: 9 | 10 | ```css 11 | /*[viewport]*/ { 12 | width: [readonly-set-for-us]; 13 | height: [readonly-set-for-us]; 14 | } 15 | html { 16 | width: auto; 17 | height: max-content; /* is this right, maybe fit or min content */ 18 | } 19 | body { 20 | width: auto; 21 | height: max-content; /* is this right, maybe fit or min content */ 22 | margin: 8; // seems to be the default in chrome at least 23 | } 24 | ``` 25 | 26 | My understanding is that for `display: block` elements, `width: auto` means `width: 100%`. 27 | Note that percentage sizes are a percentage of the size of the parent container. 28 | This means the size comes from the parent container rather than the content. 29 | 30 | From the defaults above, the top-level elements get their width from the viewport and their 31 | height from their content, meaning that HTML behaves differently in the X/Y direction by default. 32 | 33 | > NOTE: for `display: inline-block` elements, `width: auto` means `max-content` I think? 34 | you can see this by setting display to `inline-block` on the body and see that its 35 | width will grow to fit its content like it normally does in the y direction. 36 | 37 | Also note that `display: flex` seems to behave like `display: block` in this respect, namely, 38 | that by default its width is `100%` (even for elements who default to `display: inline-block` like `span`) 39 | and its height is `max-content` (I think?). 40 | 41 | NOTE: fit-content is a value between min/max content determined by this conditional: 42 | ``` 43 | if available >= max-content 44 | fit-content = max-content 45 | if available >= min-content 46 | fit-content = available 47 | else 48 | fit-content = min-content 49 | ``` 50 | 51 | ## Flexbox 52 | 53 | There's a "main axis" and "cross axis". 54 | Set `display: flex` to make an element a "flex container". 55 | All its "direct children" become "flex items". 56 | 57 | ### Flex Container Properties 58 | 59 | #### flex-direction: direction to place items 60 | 61 | - row: left to right 62 | - row-reverse: right to left 63 | - column: top to bottom 64 | - coloumn-reverse: bottom to top 65 | 66 | #### justify-content: where to put the "extra space" on the main axis 67 | 68 | - flex-start (default): items packed to start so all "extra space" at the end 69 | - flex-end: items packed to end so all "extra space" at the start 70 | - center: "extra space" evenly split between start/end 71 | - space-between: "extra space" evenly split between all items 72 | - space-evenly: "exta space" evently split between and around all items 73 | - space-around (dumb): like space-evenly but start/end space is halfed 74 | 75 | #### align-items: how to align (or stretch) items on the cross axis 76 | 77 | - flex-start 78 | - flex-end 79 | - center 80 | - baseline: all items aligned so their "baselines" align 81 | - stretch 82 | 83 | 84 | By default flexbox only has a single main axis, the following properties apply to flex containers 85 | that allow multiple lines: 86 | 87 | #### flex-wrap 88 | 89 | - nowrap (default): keep all items on the same main axis, may cause overflow 90 | - wrap: allow multiple "main axis" 91 | - wrap-reverse: new axis are added in the "opposite cross direction" of a normal wrap 92 | for example, for flex-direction "row", new wrapped lines would go 93 | on top of the previous line instead of below. 94 | 95 | ### align-content: where to put the "extra space" on the cross axis 96 | 97 | Note that this is only applicable when wrapping multiple lines. 98 | 99 | Same values as "justify-content" except it doesn't have "space-evenly" 100 | and it adds "stretch", which is the default. 101 | 102 | #### flex-flow 103 | 104 | Shorthand for `flex-direction` and `flex-wrap`. 105 | 106 | ### Flex Item Properties 107 | 108 | #### order: set the item's "order group" 109 | 110 | All items in a lower "order group" come first. 111 | The default "order group" is 0. 112 | Order can be negative. 113 | 114 | #### align-self: how to align (or strech) this item on the cross axis 115 | 116 | Same as "align-items" on the container except it affects this one item. 117 | 118 | 119 | ### Flex Layout Algorithm 120 | 121 | See if I can come up with a set of steps that can be done independently of each other to layout a flexbox. 122 | 123 | - Step ?: if there is "extra space" on the main axis, position items based on justify-content 124 | -------------------------------------------------------------------------------- /test/svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 73 | 74 | -------------------------------------------------------------------------------- /render.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const dom = @import("dom.zig"); 3 | const layout = @import("layout.zig"); 4 | const XY = layout.XY; 5 | const LayoutNode = layout.LayoutNode; 6 | 7 | pub const Op = union(enum) { 8 | rect: struct { 9 | x: u32, y: u32, 10 | w: u32, h: u32, 11 | fill: bool, 12 | color: u32, 13 | }, 14 | text: struct { 15 | x: u32, y: u32, 16 | size: u32, 17 | slice: []const u8, 18 | }, 19 | }; 20 | 21 | // TODO: it would be better to make this an iterator, but, that would be 22 | // more complex/harder to implement. I can maybe do that later an 23 | // an improvement to the API. 24 | pub fn render( 25 | html: []const u8, 26 | dom_nodes: []const dom.Node, 27 | layout_nodes: []const LayoutNode, 28 | comptime Ctx: type, 29 | onRender: anytype, 30 | ctx: Ctx, 31 | ) !void { 32 | _ = html; 33 | 34 | var next_color_index: usize = 0; 35 | var next_no_relative_position_box_y: i32 = 200; 36 | 37 | var current_box_content_pos = XY(i32){ .x = 0, .y = 0 }; 38 | 39 | for (layout_nodes, 0..) |node, node_index| switch (node) { 40 | .box => |b| { 41 | if (b.content_size.x.getResolved() == null or b.content_size.y.getResolved() == null) { 42 | std.log.warn("box size at index {} not resolved, should be impossible once fully implemented", .{node_index}); 43 | } else { 44 | const content_size = XY(u32){ 45 | .x = b.content_size.x.getResolved().?, 46 | .y = b.content_size.y.getResolved().?, 47 | }; 48 | 49 | const color = unique_colors[next_color_index]; 50 | next_color_index = (next_color_index + 1) % unique_colors.len; 51 | 52 | const x = current_box_content_pos.x + @as(i32, @intCast(b.relative_content_pos.x)); 53 | { 54 | const y = current_box_content_pos.y + @as(i32, @intCast(b.relative_content_pos.y)); 55 | try onRender(ctx, .{ .rect = .{ 56 | .x = @intCast(x), .y = @intCast(y), 57 | .w = content_size.x, .h = content_size.y, 58 | .fill = true, .color = color, 59 | }}); 60 | } 61 | 62 | const explode_view = true; 63 | if (explode_view) { 64 | const y = next_no_relative_position_box_y; 65 | next_no_relative_position_box_y += @as(i32, @intCast(content_size.y)) + 5; 66 | 67 | try onRender(ctx, .{ .rect = .{ 68 | .x = @intCast(x), .y = @intCast(y), 69 | .w = content_size.x, .h = content_size.y, 70 | .fill = false, .color = color, 71 | }}); 72 | var text_buf: [300]u8 = undefined; 73 | const msg = std.fmt.bufPrint( 74 | &text_buf, 75 | "box index={} {s} {}x{}", .{ 76 | node_index, 77 | switch (dom_nodes[b.dom_node]) { 78 | .start_tag => |t| @tagName(t.id), 79 | .text => @as([]const u8, "text"), 80 | else => unreachable, 81 | }, 82 | content_size.x, 83 | content_size.y, 84 | }, 85 | ) catch unreachable; 86 | const font_size = 10; 87 | try onRender(ctx, .{ .text = .{ 88 | .x = @as(u32, @intCast(x))+1, .y = @as(u32, @intCast(y))+1, 89 | .size = font_size, .slice = msg, 90 | }}); 91 | } 92 | } 93 | 94 | current_box_content_pos = .{ 95 | .x = current_box_content_pos.x + @as(i32, @intCast(b.relative_content_pos.x)), 96 | .y = current_box_content_pos.y + @as(i32, @intCast(b.relative_content_pos.y)), 97 | }; 98 | }, 99 | .end_box => |box_index| { 100 | const b = switch (layout_nodes[box_index]) { 101 | .box => |*b| b, 102 | else => unreachable, 103 | }; 104 | current_box_content_pos = .{ 105 | .x = current_box_content_pos.x - @as(i32, @intCast(b.relative_content_pos.x)), 106 | .y = current_box_content_pos.y - @as(i32, @intCast(b.relative_content_pos.y)), 107 | }; 108 | }, 109 | .text => |t| { 110 | var line_it = layout.textLineIterator(t.font, t.first_line_x, t.max_width, t.slice); 111 | 112 | const first_line = line_it.first(); 113 | // TODO: set this correctly 114 | const abs_x_i32 = current_box_content_pos.x + @as(i32, @intCast(t.relative_content_pos.x)); 115 | var abs_y_i32 = current_box_content_pos.y + @as(i32, @intCast(t.relative_content_pos.y)); 116 | // TODO: abs_x should be signed 117 | const abs_x_u32: u32 = @intCast(abs_x_i32); 118 | // TODO: abs_y should be signed 119 | var abs_y_u32: u32 = @intCast(abs_y_i32); 120 | try onRender(ctx, .{ .text = .{ .x = abs_x_u32 + t.first_line_x, .y = abs_y_u32, .size = t.font.size, .slice = first_line.slice }}); 121 | // TODO: this first_line_height won't be correct right now if there 122 | // is another element after us on the same line with a bigger height 123 | abs_y_i32 += @intCast(t.first_line_height); 124 | abs_y_u32 += t.first_line_height; 125 | while (line_it.next()) |line| { 126 | try onRender(ctx, .{ .text = .{ .x = abs_x_u32, .y = abs_y_u32, .size = t.font.size, .slice = line.slice }}); 127 | abs_y_i32 += @intCast(t.font.getLineHeight()); 128 | abs_y_u32 += t.font.getLineHeight(); 129 | } 130 | }, 131 | .svg => { 132 | std.log.info("TODO: draw svg!", .{}); 133 | }, 134 | }; 135 | } 136 | 137 | var unique_colors = [_]u32 { 138 | 0xe6194b, 0x3cb44b, 0xffe119, 0x4363d8, 0xf58231, 0x911eb4, 0x46f0f0, 139 | 0xf032e6, 0xbcf60c, 0xfabebe, 0x008080, 0xe6beff, 0x9a6324, 0xfffac8, 140 | 0x800000, 0xaaffc3, 0x808000, 0xffd8b1, 0x000075, 0x808080, 141 | }; 142 | -------------------------------------------------------------------------------- /wasmrenderer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const dom = @import("dom.zig"); 3 | const layout = @import("layout.zig"); 4 | const Styler = layout.Styler; 5 | const LayoutNode = layout.LayoutNode; 6 | const alext = @import("alext.zig"); 7 | const render = @import("render.zig"); 8 | 9 | const Refcounted = @import("Refcounted.zig"); 10 | 11 | const XY = layout.XY; 12 | 13 | const js = struct { 14 | extern fn logWrite(ptr: [*]const u8, len: usize) void; 15 | extern fn logFlush() void; 16 | extern fn initCanvas() void; 17 | extern fn canvasClear() void; 18 | extern fn strokeRgb(rgb: u32) void; 19 | extern fn strokeRect(x: u32, y: u32, width: u32, height: u32) void; 20 | extern fn fillRgb(rgb: u32) void; 21 | extern fn fillRect(x: u32, y: u32, width: u32, height: u32) void; 22 | extern fn drawText(x: u32, y: u32, font_size: usize, ptr: [*]const u8, len: usize) void; 23 | }; 24 | 25 | var gpa = std.heap.GeneralPurposeAllocator(.{}){ }; 26 | 27 | export fn alloc(len: usize) ?[*]u8 { 28 | //std.log.debug("alloc {}", .{len}); 29 | const buf = Refcounted.alloc(gpa.allocator(), len) catch { 30 | std.log.warn("alloc failed with OutOfMemory", .{}); 31 | return null; 32 | }; 33 | //std.log.debug("alloc returning 0x{x}", .{@ptrToInt(buf.data_ptr)}); 34 | return buf.data_ptr; 35 | } 36 | export fn release(ptr: [*]u8, len: usize) void { 37 | //std.log.debug("free {} (ptr=0x{x})", .{len, ptr}); 38 | const buf = Refcounted{ .data_ptr = ptr }; 39 | buf.unref(gpa.allocator(), len); 40 | } 41 | 42 | export fn onResize(width: u32, height: u32) void { 43 | const html_buf = global_opt_html_buf orelse { 44 | std.log.warn("onResize called without an html doc being loaded", .{}); 45 | return; 46 | }; 47 | const html = html_buf.buf.data_ptr[0 .. html_buf.len]; 48 | const dom_nodes = global_opt_dom_nodes orelse { 49 | std.log.warn("onResize called but there's no dom nodes", .{}); 50 | return; 51 | }; 52 | 53 | doRender(html, XY(u32).init(width, height), dom_nodes.items); 54 | } 55 | 56 | var global_opt_html_buf: ?struct { 57 | buf: Refcounted, 58 | len: usize, 59 | } = null; 60 | var global_opt_dom_nodes: ?std.ArrayListUnmanaged(dom.Node) = null; 61 | 62 | export fn loadHtml( 63 | name_ptr: [*]u8, name_len: usize, 64 | html_ptr: [*]u8, html_len: usize, 65 | viewport_width: u32, viewport_height: u32, 66 | ) void { 67 | const name = name_ptr[0 .. name_len]; 68 | 69 | if (global_opt_html_buf) |html_buf| { 70 | html_buf.buf.unref(gpa.allocator(), html_buf.len); 71 | global_opt_html_buf = null; 72 | } 73 | global_opt_html_buf = .{ .buf = Refcounted{ .data_ptr = html_ptr }, .len = html_len }; 74 | global_opt_html_buf.?.buf.addRef(); 75 | 76 | loadHtmlSlice(name, html_ptr[0 .. html_len], XY(u32).init(viewport_width, viewport_height)); 77 | } 78 | 79 | fn loadHtmlSlice( 80 | name: []const u8, 81 | html: []const u8, 82 | viewport_size: XY(u32), 83 | ) void { 84 | if (global_opt_dom_nodes) |*nodes| { 85 | nodes.deinit(gpa.allocator()); 86 | global_opt_dom_nodes = null; 87 | } 88 | 89 | std.log.info("load html from '{s}'...", .{name}); 90 | var parse_context = ParseContext{ .name = name }; 91 | 92 | var nodes = dom.parse(gpa.allocator(), html, .{ 93 | .context = &parse_context, 94 | .on_error = onParseError, 95 | }) catch |err| switch (err) { 96 | error.ReportedParseError => return, 97 | else => |e| { 98 | onParseError(&parse_context, @errorName(e)); 99 | return; 100 | }, 101 | }; 102 | alext.unmanaged.finalize(dom.Node, &nodes, gpa.allocator()); 103 | global_opt_dom_nodes = nodes; 104 | 105 | js.initCanvas(); 106 | doRender(html, viewport_size, nodes.items); 107 | } 108 | 109 | fn doRender( 110 | html: []const u8, 111 | viewport_size: XY(u32), 112 | dom_nodes: []const dom.Node, 113 | ) void { 114 | js.canvasClear(); 115 | 116 | var arena = std.heap.ArenaAllocator.init(gpa.allocator()); 117 | defer arena.deinit(); 118 | 119 | var layout_nodes = layout.layout( 120 | arena.allocator(), 121 | html, 122 | dom_nodes, 123 | viewport_size, 124 | Styler{ }, 125 | ) catch |err| { 126 | // TODO: maybe draw this error as text? 127 | std.log.err("layout failed, error={s}", .{@errorName(err)}); 128 | return; 129 | }; 130 | alext.unmanaged.finalize(LayoutNode, &layout_nodes, gpa.allocator()); 131 | render.render( 132 | html, 133 | dom_nodes, 134 | layout_nodes.items, 135 | void, 136 | &onRender, 137 | {}, 138 | ) catch |err| switch (err) { }; 139 | } 140 | 141 | fn onRender(ctx: void, op: render.Op) !void { 142 | _ = ctx; 143 | switch (op) { 144 | .rect => |r| { 145 | if (r.fill) { 146 | js.fillRgb(r.color); 147 | js.fillRect(r.x, r.y, r.w, r.h); 148 | } else { 149 | js.strokeRgb(r.color); 150 | js.strokeRect(r.x, r.y, r.w, r.h); 151 | } 152 | }, 153 | .text => |t| { 154 | js.drawText(t.x, t.y + t.size, t.size, t.slice.ptr, t.slice.len); 155 | }, 156 | } 157 | } 158 | 159 | const ParseContext = struct { 160 | name: []const u8, 161 | }; 162 | fn onParseError(context_ptr: ?*anyopaque, msg: []const u8) void { 163 | const context: *ParseContext = @alignCast(@ptrCast(context_ptr)); 164 | std.log.err("{s}: parse error: {s}", .{context.name, msg}); 165 | } 166 | 167 | const JsLogWriter = std.io.Writer(void, error{}, jsLogWrite); 168 | fn jsLogWrite(context: void, bytes: []const u8) !usize { 169 | _ = context; 170 | js.logWrite(bytes.ptr, bytes.len); 171 | return bytes.len; 172 | } 173 | pub const std_options: std.Options = .{ 174 | .logFn = log, 175 | }; 176 | pub fn log( 177 | comptime message_level: std.log.Level, 178 | comptime scope: @Type(.EnumLiteral), 179 | comptime format: []const u8, 180 | args: anytype, 181 | ) void { 182 | const level_txt = comptime message_level.asText(); 183 | const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; 184 | const log_fmt = level_txt ++ prefix ++ format; 185 | const writer = JsLogWriter{ .context = {} }; 186 | std.fmt.format(writer, log_fmt, args) catch unreachable; 187 | js.logFlush(); 188 | } 189 | -------------------------------------------------------------------------------- /html-css-renderer.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |