diff --git a/packages/core/src/layout/LayoutTree.zig b/packages/core/src/layout/LayoutTree.zig index 0394015..f64263a 100644 --- a/packages/core/src/layout/LayoutTree.zig +++ b/packages/core/src/layout/LayoutTree.zig @@ -50,6 +50,7 @@ pub fn getNodePtr(self: *Self, id: LayoutNode.Id) *LayoutNode { pub const LayoutNode = struct { id: Id, + parent: ?Id = null, data: Data, pub const Id = u32; pub const Data = union(enum) { @@ -77,8 +78,10 @@ pub const TextNode = struct { pub const InlineNode = struct { ref: DocRef, - is_atomic: bool, + is_atomic: bool = false, children: Array(LayoutNode.Id) = .{}, + continuation: ?LayoutNode.Id = null, + continuationOf: ?LayoutNode.Id = null, pub fn deinit(self: *InlineNode, allocator: std.mem.Allocator) void { self.children.deinit(allocator); } @@ -106,6 +109,8 @@ pub const InlineContainerNode = struct { ref: DocRef, children: Array(LayoutNode.Id) = .{}, line_boxes: Array(LineBox) = .{}, + continuation: ?LayoutNode.Id = null, + continuationOf: ?LayoutNode.Id = null, pub fn deinit(self: *InlineContainerNode, allocator: std.mem.Allocator) void { self.children.deinit(allocator); self.line_boxes.deinit(allocator); @@ -136,107 +141,211 @@ fn nodeIsInline(tree: *DocTree, node_id: DocNodeId) bool { if (kind == .text) return true; return tree.getStyle(node_id).display.outside == .@"inline"; } +fn isOnlyInlineSubtree(tree: *DocTree, node_id: DocNodeId) bool { + const kind = tree.getNodeKind(node_id); + if (kind == .text) return true; + const style = tree.getStyle(node_id); + if (style.display.outside != .@"inline") return false; + if (isAtomicInline(tree, node_id)) return true; + for (tree.getNodeChildren(node_id)) |child| { + if (!isOnlyInlineSubtree(tree, child)) return false; + } + return true; +} +pub fn isDisplayNone(tree: *DocTree, node_id: DocNodeId) bool { + return tree.getStyle(node_id).display.outside == .none; +} +pub fn isInlineFlow(tree: *DocTree, node_id: DocNodeId) bool { + const style = tree.getStyle(node_id); + return style.display.outside == .@"inline" and style.display.inside == .flow; +} +pub fn isAtomicInline(tree: *DocTree, node_id: DocNodeId) bool { + const style = tree.getStyle(node_id); + return style.display.outside == .@"inline" and style.display.inside != .flow; +} + +const BuildError = error{ + OutOfMemory, + InvalidParent, +}; +const MixedContextBuilder = struct { + layout_tree: *Self, + doc_tree: *DocTree, + root_container_id: LayoutNode.Id, + current_container_id: LayoutNode.Id, + allocator: std.mem.Allocator, + stack: Array(LayoutNode.Id) = .{}, + pub fn isCurrentContainerInline(self: *MixedContextBuilder) bool { + const current_container = self.layout_tree.getNodePtr(self.current_container_id); + return switch (current_container.data) { + .inline_container_node => true, + .block_container_node => false, + else => unreachable, + }; + } + pub fn init(allocator: std.mem.Allocator, layout_tree: *Self, doc_tree: *DocTree, root_container_id: LayoutNode.Id) !MixedContextBuilder { + return MixedContextBuilder{ + .allocator = allocator, + .layout_tree = layout_tree, + .doc_tree = doc_tree, + .root_container_id = root_container_id, + .current_container_id = root_container_id, + }; + } + pub fn getCurrentParent(self: *MixedContextBuilder) LayoutNode.Id { + return if (self.stack.items.len > 0) self.stack.items[self.stack.items.len - 1] else self.current_container_id; + } + pub fn createBlockContainer(self: *MixedContextBuilder) !LayoutNode.Id { + const id = try self.layout_tree.createNode(.{ .block_container_node = .{ .ref = .anonymous } }); + try self.layout_tree.appendNode(self.root_container_id, id); + self.current_container_id = id; + return id; + } + pub fn createInlineContainer(self: *MixedContextBuilder, parent_id: LayoutNode.Id) !LayoutNode.Id { + const id = try self.layout_tree.createNode(.{ .inline_container_node = .{ .ref = .anonymous } }); + try self.layout_tree.appendNode(parent_id, id); + self.current_container_id = id; + return id; + } + pub fn appendNode(self: *MixedContextBuilder, child_id: LayoutNode.Id) !void { + if (!self.isCurrentContainerInline()) { + try self.splitStack(); + } + try self.layout_tree.appendNode(self.getCurrentParent(), child_id); + } + pub fn splitStack(self: *MixedContextBuilder) !void { + var parent = try self.createInlineContainer(self.root_container_id); + for (0..self.stack.items.len) |i| { + const id = self.stack.items[i]; + var node = self.layout_tree.getNodePtr(id); + switch (node.data) { + .inline_container_node => { + const clone_inline_container_node_id = try self.layout_tree.createNode(.{ .inline_container_node = .{ .ref = .anonymous } }); + node = self.layout_tree.getNodePtr(id); + node.data.inline_container_node.continuation = clone_inline_container_node_id; + const clone_node = self.layout_tree.getNodePtr(clone_inline_container_node_id); + clone_node.data.inline_container_node.continuationOf = id; + + try self.layout_tree.appendNode(parent, clone_inline_container_node_id); + parent = clone_inline_container_node_id; + self.stack.items[i] = clone_inline_container_node_id; + }, + .inline_node => { + const clone_inline_node_id = try self.layout_tree.createNode(.{ .inline_node = .{ .ref = .{ .doc_node = node.data.inline_node.ref.doc_node } } }); + node = self.layout_tree.getNodePtr(id); + node.data.inline_node.continuation = clone_inline_node_id; + const clone_node = self.layout_tree.getNodePtr(clone_inline_node_id); + clone_node.data.inline_node.continuationOf = id; + try self.layout_tree.appendNode(parent, clone_inline_node_id); + parent = clone_inline_node_id; + self.stack.items[i] = clone_inline_node_id; + }, + else => unreachable, + } + } + } + pub fn build(self: *MixedContextBuilder) BuildError!void { + const children = self.doc_tree.getNodeChildren(self.root_container_id); + for (children) |child| { + if (isDisplayNone(self.doc_tree, child)) continue; + try self.buildFromNode(child); + } + } + pub fn buildFromNode(self: *MixedContextBuilder, node_id: DocNodeId) BuildError!void { + const kind = self.doc_tree.getNodeKind(node_id); + if (kind == .text) { + const text = self.doc_tree.getText(node_id).bytes.items; + const id = try self.layout_tree.createTextNode(text); + try self.appendNode(id); + + // return id; + return; + } + if (isAtomicInline(self.doc_tree, node_id)) { + const id = try self.layout_tree.buildInsideBlock(self.doc_tree, node_id); + try self.appendNode(id); + return; + } + if (isInlineFlow(self.doc_tree, node_id)) { + const id = try self.layout_tree.createNode(.{ .inline_node = .{ .ref = .{ .doc_node = node_id } } }); + try self.layout_tree.appendNode(self.getCurrentParent(), id); + // push to the stack + try self.stack.append(self.allocator, id); + defer _ = self.stack.pop(); + const children = self.doc_tree.getNodeChildren(node_id); + for (children) |child| { + if (isDisplayNone(self.doc_tree, child)) continue; + try self.buildFromNode(child); + } + return; + } + + // otherwise it's a block + const block_container_id = if (self.isCurrentContainerInline()) try self.createBlockContainer() else self.current_container_id; + const block_node = try self.layout_tree.buildInsideBlock(self.doc_tree, node_id); + try self.layout_tree.appendNode(block_container_id, block_node); + } + pub fn deinit(self: *MixedContextBuilder) void { + self.stack.deinit(self.allocator); + } +}; /// Recursively convert the DOM starting at `node_id` into layout nodes. /// Returns the id of the created layout node or `null` if the DOM node should /// not produce a layout representation. -fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) !?LayoutNode.Id { +fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) BuildError!LayoutNode.Id { const kind = tree.getNodeKind(node_id); - // 1. Text DOM nodes map directly to layout text nodes. Empty text nodes are - // ignored. + // 1. Text DOM nodes map directly to layout text nodes. if (kind == .text) { const text = tree.getText(node_id).bytes.items; - if (text.len == 0) return null; const id = try self.createTextNode(text); return id; } - const style = tree.getStyle(node_id); + if (style.display.inside != .flow) { + return self.buildInsideBlock(tree, node_id); + } - // 2. Nodes with `display: none` do not participate in layout. - if (style.display.outside == .none) return null; - - // 3. Inline-level elements produce an `InlineNode` and simply convert all of - // their children. - if (style.display.outside == .@"inline") { - const id = try self.createNode(.{ .inline_node = .{ .ref = .{ .doc_node = node_id }, .is_atomic = false } }); + // 2. Atomic inline elements produce an `InlineNode`, right now we dont have other types of atomic inline elements besides inline-block or inline-flex + if (isInlineFlow(tree, node_id)) { + const id = try self.createNode(.{ .inline_node = .{ .ref = .{ .doc_node = node_id } } }); for (tree.getNodeChildren(node_id)) |child| { - if (try self.build(tree, child)) |child_id| { - try self.appendNode(id, child_id); - } + if (isDisplayNone(tree, child)) continue; + const child_layout_node_id = try self.build(tree, child); + try self.appendNode(id, child_layout_node_id); } return id; } - + unreachable; +} +pub fn buildInsideBlock(self: *Self, tree: *DocTree, node_id: DocNodeId) !LayoutNode.Id { const children = tree.getNodeChildren(node_id); - var only_inline = true; - - // Determine whether every visible child is inline-level so we know what - // kind of container to create. + var only_inline_children = true; for (children) |child| { - if (!nodeIsInline(tree, child)) { - if (tree.getStyle(child).display.outside != .none) { - only_inline = false; - break; - } + if (isDisplayNone(tree, child)) continue; + if (!isOnlyInlineSubtree(tree, child)) { + only_inline_children = false; } } - - if (only_inline) { - // 4. If all children are inline, wrap them in an `InlineContainerNode` - // so they participate in the inline formatting context. - const id = try self.createNode(.{ .inline_container_node = .{ .ref = .{ .doc_node = node_id } } }); + if (only_inline_children) { + const inline_container_id = try self.createNode(.{ .inline_container_node = .{ .ref = .{ .doc_node = node_id } } }); for (children) |child| { - if (try self.build(tree, child)) |child_id| { - try self.appendNode(id, child_id); - } + if (isDisplayNone(tree, child)) continue; + const child_layout_node_id = try self.build(tree, child); + try self.appendNode(inline_container_id, child_layout_node_id); } - return id; + return inline_container_id; } - // 5. Otherwise we create a `BlockContainerNode` and insert anonymous inline - // containers around contiguous inline children to preserve block model - // invariants. const container_id = try self.createNode(.{ .block_container_node = .{ .ref = .{ .doc_node = node_id } } }); - var inline_seq: Array(LayoutNode.Id) = .{}; - defer inline_seq.deinit(self.allocator); - - for (children) |child| { - const child_is_inline = nodeIsInline(tree, child); - const maybe_child = try self.build(tree, child); - if (maybe_child == null) continue; - const l_id = maybe_child.?; - - if (child_is_inline) { - // Accumulate inline children so they can be wrapped together. - try inline_seq.append(self.allocator, l_id); - } else { - // Flush any collected inline children before appending the block. - if (inline_seq.items.len > 0) { - const anon = try self.createNode(.{ .inline_container_node = .{ .ref = .anonymous } }); - for (inline_seq.items) |iid| { - try self.appendNode(anon, iid); - } - try self.appendNode(container_id, anon); - inline_seq.clearRetainingCapacity(); - } - try self.appendNode(container_id, l_id); - } - } - - // Flush trailing inline children. - if (inline_seq.items.len > 0) { - const anon = try self.createNode(.{ .inline_container_node = .{ .ref = .anonymous } }); - for (inline_seq.items) |iid| { - try self.appendNode(anon, iid); - } - try self.appendNode(container_id, anon); - } - + var mixed_context_builder = try MixedContextBuilder.init(self.allocator, self, tree, container_id); + try mixed_context_builder.build(); + mixed_context_builder.deinit(); + // try self.build(node_id); + // const container_id = try self.appendNode(parent_id: LayoutNode.Id, child_id: LayoutNode.Id) return container_id; } - fn writeDocRef(writer: std.io.AnyWriter, ref: DocRef) !void { switch (ref) { .anonymous => try writer.writeAll("{anon}"), @@ -276,6 +385,13 @@ fn printNodeInternal(self: *Self, node_id: LayoutNode.Id, writer: std.io.AnyWrit if (inline_node.is_atomic) { try writer.print(" atomic", .{}); } + if (inline_node.continuationOf) |continuation_of| { + try writer.print(" continuationOf={{#{d}}}", .{continuation_of}); + } + if (inline_node.continuation) |continuation| { + try writer.print(" continuation={{#{d}}}", .{continuation}); + } + try writer.print(" ref=", .{}); try writeDocRef(writer, inline_node.ref); try writer.print(" children={{{d}}}]", .{inline_node.children.items.len}); @@ -283,11 +399,18 @@ fn printNodeInternal(self: *Self, node_id: LayoutNode.Id, writer: std.io.AnyWrit .block_container_node => |block| { try writer.print("[{s} #{d} ref=", .{ @tagName(node.data), node.id }); try writeDocRef(writer, block.ref); + try writer.print(" children={{{d}}}]", .{block.children.items.len}); }, .inline_container_node => |container| { try writer.print("[{s} #{d} ref=", .{ @tagName(node.data), node.id }); try writeDocRef(writer, container.ref); + if (container.continuationOf) |continuation_of| { + try writer.print(" continuationOf={{#{d}}}", .{continuation_of}); + } + if (container.continuation) |continuation| { + try writer.print(" continuation={{#{d}}}", .{continuation}); + } try writer.print(" children={{{d}}} lines={{{d}}}]", .{ container.children.items.len, container.line_boxes.items.len }); }, } @@ -349,36 +472,13 @@ test "LayoutTree" { \\ zzz \\ , - \\[block_container_node #0 ref={anon} children={2}] - \\├── [inline_container_node #1 ref={anon} children={1} lines={0}] - \\│ └── [inline_node #2 atomic={false} ref={anon} children={2}] - \\│ ├── [text_node #3] "abc" - \\│ └── [text_node #4] "def" - \\└── [text_node #5] "zzz" - ); -} - -test "fromTree inline only" { - const allocator = std.testing.allocator; - var doc = try docFromXml(allocator, "